From 3e93c6f0005aa3cf15980801b0c8d41f1e21147a Mon Sep 17 00:00:00 2001 From: hl-valdemar Date: Sun, 25 Oct 2026 22:19:14 +0100 Subject: [PATCH] feat: add integer widths and checked numeric conversions --- README.md | 8 +- TODO.md | 8 +- source/interpreter/interpreter.hon | 67 +++++++++++++-- source/ir/ir.hon | 43 ++++++++++ source/llvm/instructions.hon | 86 +++++++++++++++++-- source/llvm/llvm.hon | 19 ++++- source/llvm/runtime.hon | 19 +++++ source/lower/lower.hon | 48 ++++++++--- source/main.hon | 4 +- source/sema/check/check.hon | 133 ++++++++++++++++++++++++----- source/sema/resolve.hon | 11 +++ tests/native.py | 120 +++++++++++++++++++++++++- 12 files changed, 500 insertions(+), 66 deletions(-) diff --git a/README.md b/README.md index e60d5d7..01609b7 100644 --- a/README.md +++ b/README.md @@ -95,13 +95,13 @@ indices require `usize`; integer literals and flexible inferred values receive t `while` repeats a block or single statement while its `bool` condition holds, re-evaluating the condition on every iteration. guards follow the same braced/braceless convention as `if`. loop bodies introduce child scopes; their locals, including locals in nested conditional arms, begin uninitialized on each scope entry. -`for 0..10 |i| { ... }` iterates an ascending, upper-exclusive integer range; `..=` includes the upper bound. a capture is required: `|i|` binds an immutable value with the bounds' integer family (`int` or `usize`) in the body scope, while `|_|` discards it. bounds resolve in the enclosing scope and are evaluated left-to-right once, so changing a bound variable in the body does not change the range. reversed ranges are empty; equal bounds produce zero iterations with `..` and one with `..=`. inclusive ranges terminate safely at the maximum integer. +`for 0..10 |i| { ... }` iterates an ascending, upper-exclusive integer range; `..=` includes the upper bound. a capture is required: `|i|` binds an immutable value with the bounds' exact integer type in the body scope, while `|_|` discards it. bounds resolve in the enclosing scope and are evaluated left-to-right once, so changing a bound variable in the body does not change the range. reversed ranges are empty; equal bounds produce zero iterations with `..` and one with `..=`. inclusive ranges terminate safely at the maximum of each integer type. range bounds that are compound or unary expressions require their own parentheses, such as `(-2)..(limit + 1)`. a braceless body also requires parentheses around the entire iterable: `for (0..10) |i| total = total + i`. current execution supports direct integer ranges only; first-class range values, collection iteration, pointer captures and index captures remain unimplemented. [programs/range_loop.hon](programs/range_loop.hon) skips `5` while summing `0..10`; `./build/honey run programs/range_loop.hon` prints `40`. `break` exits the nearest enclosing `for` or `while`; `continue` restarts a `while` condition or advances a `for` range through its guarded update. both are operand-free statements permitted within loop bodies and nested conditional arms; labels and values are unsupported. controls outside loops produce keyword-spanned diagnostics, including in untaken or unreachable code, and trap only if reached. code after a transfer is still resolved and type-checked but does not execute on that path. body locals are reset on re-entry after `continue`; loop transfers do not guarantee a function return. both controls lower to existing IR jumps, without new interpreter instructions or loop frames. [programs/loop_control.hon](programs/loop_control.hon) skips an iteration and exits early; `./build/honey run programs/loop_control.hon` prints `12`. -name resolution (`@source/sema`) runs after parsing. it predeclares package names and each block's direct locals, allowing forward references and mutually recursive functions. parameters and top-level function-body locals share a scope; each conditional arm and loop body introduces a child scope whose names do not escape it. signatures resolve names in the enclosing package scope. shadowing is forbidden, including reuse of builtin type names (`int`, `float`, `i64`, `f64`, `usize`, `bool`, `string`, `void`); sibling scopes may independently reuse names. +name resolution (`@source/sema`) runs after parsing. it predeclares package names and each block's direct locals, allowing forward references and mutually recursive functions. parameters and top-level function-body locals share a scope; each conditional arm and loop body introduces a child scope whose names do not escape it. signatures resolve names in the enclosing package scope. shadowing is forbidden, including reuse of builtin type names; sibling scopes may independently reuse names. the driver uses `@source/compilation` to own loaded source buffers, copied paths, one AST arena, the package graph and a shared identifier pool. `add_source` transfers its `ArrayList(u8)` allocation only on success, resetting the caller's list; infrastructure failure leaves the bytes caller-owned and rolls back the source registration, partial AST and source diagnostics. interned names may remain in the shared pool. `deinit` releases all retained frontend storage. metadata borrows are invalidated by registration growth, but source/package/node IDs remain stable within their compilation. @@ -119,10 +119,12 @@ function fallthrough obligations are separate from body types. `check.fallthroug the checker keeps union-find parent links and group sizes in a compact array separate from per-node inference metadata. root walks and path compression access only that link storage; both temporary arrays are released before `check.check` returns. -numeric typing follows contextual rules within Honey's current `int` and `float` families. integer literals can adopt a supported numeric family only when their value is exactly representable; float literals remain float and opt into ordinary IEEE rounding. numeric demands propagate through arithmetic and inferred bindings before undemanded integers default to `int`. conflicting demands are errors; fixed `int`/`i64`, `usize`, and `float`/`f64` values do not implicitly convert between families. `+`, `-`, `*` and unary `-` require compatible numeric operands; `/` requires and supplies float context. `3 / 2`, `3.0 / 2`, and `ratio float :: 3 / 2` are valid. `value f64 :: 9007199254740993` is diagnosed, including when float context arrives through aliases, division, or array elements; `9007199254740993.0` explicitly permits rounding. exactly representable integers beyond the signed/unsigned integer ranges remain valid in float context. explicit numeric casts are not yet implemented. +numeric types include `i8`, `i16`, `i32`, `i64`, `u8`, `u16`, `u32`, `u64`, `isize`, `usize` and `f64`. on the supported 64-bit target, `isize` and `usize` are signed and unsigned 64-bit types. current `int`/`i64`, `uint`/`u64` and `float`/`f64` annotations share their runtime types; other concrete types remain distinct even when their widths coincide. integer literals adopt numeric context through arithmetic, inferred bindings, arrays and calls before undemanded integers default to `int`. fixed values never convert implicitly between types. integer arithmetic checks the selected type's bounds; unsigned negation is invalid. `/` requires and supplies float context. implicit integer-to-float literal context requires exact representation: `value f64 :: 9007199254740993` is diagnosed, while `9007199254740993.0` permits IEEE rounding. the conversion policy is value-preserving by default: any future implicit widening must preserve every value of the source type, not merely the value observed at one use. narrowing, potentially inexact integer-to-float conversion, float-to-integer conversion, and signedness changes that cannot preserve the full source range require an explicit operation. integer arithmetic remains checked rather than wrapping; floating arithmetic retains IEEE rounding. +explicit numeric conversion uses `T(value)`, such as `u8(index)`, `usize(byte)` or `f64(count)`, with exactly one numeric argument. integer conversions trap with `integer arithmetic overflow` if the value is outside the destination range; float-to-integer conversion truncates toward zero, then checks bounds, rejecting NaN and infinity. integer-to-float conversion permits rounding, including `f64(9007199254740993)`. operands retain their independent types rather than being narrowed by the result context; a positive constant integer conversion operand above `i64`'s maximum defaults to `u64`. boolean conversions, arbitrary-width integers, `f32`, C scalar types and implicit typed-value widening are not implemented. native storage uses LLVM `i8`/`i16`/`i32`/`i64`, including array elements and function parameters/results. + `@std/strconv.parse_float(allocator, text)` parses a complete borrowed `[]u8` decimal into `f64`. it accepts an optional sign, decimal point, and exponent; whitespace, embedded NUL bytes, trailing input, hexadecimal, infinities, and NaNs return `invalid_number`. finite subnormals and signed zero are preserved; overflow and nonzero values rounded to zero return `out_of_range`. allocation failures propagate as `out_of_memory`, and temporary storage is always released. the Darwin backend uses libc's C-locale conversion without changing the process locale; rounding follows the caller's floating-point rounding mode. numeric lowering uses this interface, keeping exact integer-to-float validation compiler-specific. type errors carry source spans and diagnostic identities; invalid operands propagate their existing identity without duplicate errors, while independent expressions, arguments and nested bodies are still checked. inference cycles without a type anchor are diagnosed. function signatures are checked independently of their bodies. bare `return` and same-line `return expression` are supported inside function bodies and must match the declared result type. nonvoid return-path checking is structural: every path must return or terminate in a diagnostic trap, and both conditional successors are considered without constant folding. a missing `else` or a loop permits fallthrough, even for a literal `true` condition; a later return can close that path. statements after a return and untaken branches or loops are still type-checked but are not executed. diff --git a/TODO.md b/TODO.md index 3f6578d..e180c8e 100644 --- a/TODO.md +++ b/TODO.md @@ -5,8 +5,10 @@ ## current boundary directory packages support file-local import namespaces and postfix declaration -selection through the interpreter and native backend. the richer types/control -flow used by the compiler remain unsupported. the driver retains frontend storage +selection through the interpreter and native backend. concrete 8/16/32/64-bit +integers and checked numeric conversions work through both execution paths; +records and the richer types/control flow used by the compiler remain unsupported. +the driver retains frontend storage and source provenance through `@source/compilation`, with compilation-wide AST node identities. file commands retain single-file behavior; directory commands compile the package's production files and load its dependencies in separate scopes. @@ -32,6 +34,8 @@ general record fields, imported types, test discovery or FFI. ## subsequent dependency gates - [ ] concrete data models: required integer widths/conversions, records/tuples and construction, fields, enums, distinct/alias types, unions and matching. examples: `source/lexer/token.hon`, `source/sema/resolve.hon`, `std/mem/mem.hon`. + - [x] native integer widths (`i8`–`i64`, `u8`–`u64`, `isize`/`usize`) and checked explicit numeric conversions, including float truncation/range checks and integer-to-float rounding. preserve exact types through arrays, calls and range captures; retain strict implicit conversion rules. + - [ ] records/tuples and construction, fields, enums, distinct/alias types, unions and matching. - [ ] memory representation: source pointers/references, mutable and sentinel slices, optionals, layout/cast builtins and allocator-required libc calls. internal IR array addresses are not source pointers. examples: `std/mem/mem.hon`, `std/arraylist/arraylist.hon`. - [ ] fallible results and cleanup: error-set composition, `try`/`catch`, `defer`/`errdefer`; infrastructure errors remain distinct from diagnostic traps. examples: `source/main.hon:41`, `source/sema/check/check.hon:117`. - [ ] generic specialization and compile-time execution/reflection sufficient for existing `ArrayList`, hash maps, `enummap`, `meta` and `io.print`; include type/value parameters and compile-time generated storage. diff --git a/source/interpreter/interpreter.hon b/source/interpreter/interpreter.hon index 6779726..bf621e6 100644 --- a/source/interpreter/interpreter.hon +++ b/source/interpreter/interpreter.hon @@ -308,15 +308,20 @@ run proc( continue } } + .convert: { + operand :: frames.items[frame_index].values[usize(instruction.left)] + if (convert(operand, instruction.type)) |converted| value = converted + else return try runtime_trap(diagnostic_store, .integer_overflow, instruction.span) + } .negate: { operand :: frames.items[frame_index].values[usize(instruction.left)] if instruction.type == .float { value = ir.Value{ type = .float, data = ir.ValueData{ real = -operand.data.real } } } else { - if operand.data.integer == minval!(i64) { + if operand.data.integer == ir.integer_min(instruction.type) { return try runtime_trap(diagnostic_store, .integer_overflow, instruction.span) } - value = ir.Value{ type = .int, data = ir.ValueData{ integer = -operand.data.integer } } + value = ir.Value{ type = instruction.type, data = ir.ValueData{ integer = -operand.data.integer } } } } .logical_not: { @@ -336,17 +341,21 @@ run proc( else: unreachable } value = ir.Value{ type = .float, data = ir.ValueData{ real = result } } - } else if instruction.type == .usize { + } else if ir.unsigned_integer(instruction.type) { result :: checked_unsigned(instruction.op, left.data.unsigned, right.data.unsigned) if result |unsigned| { - value = ir.Value{ type = .usize, data = ir.ValueData{ unsigned = unsigned } } + if (unsigned > ir.integer_max(instruction.type)) return try runtime_trap(diagnostic_store, .integer_overflow, instruction.span) + value = ir.Value{ type = instruction.type, data = ir.ValueData{ unsigned = unsigned } } } else { return try runtime_trap(diagnostic_store, .integer_overflow, instruction.span) } } else { result :: checked_integer(instruction.op, left.data.integer, right.data.integer) if result |result_integer| { - value = ir.Value{ type = .int, data = ir.ValueData{ integer = result_integer } } + if (result_integer < ir.integer_min(instruction.type) or result_integer > i64(ir.integer_max(instruction.type))) { + return try runtime_trap(diagnostic_store, .integer_overflow, instruction.span) + } + value = ir.Value{ type = instruction.type, data = ir.ValueData{ integer = result_integer } } } else { return try runtime_trap(diagnostic_store, .integer_overflow, instruction.span) } @@ -357,7 +366,7 @@ run proc( right :: frames.items[frame_index].values[usize(instruction.right)] result := false match left.type { - .int: result = match instruction.op { + .int, .isize, .i8, .i16, .i32: result = match instruction.op { .equal: left.data.integer == right.data.integer .not_equal: left.data.integer != right.data.integer .less: left.data.integer < right.data.integer @@ -366,7 +375,7 @@ run proc( .greater_equal: left.data.integer >= right.data.integer else: unreachable } - .usize: result = match instruction.op { + .usize, .u8, .u16, .u32, .u64: result = match instruction.op { .equal: left.data.unsigned == right.data.unsigned .not_equal: left.data.unsigned != right.data.unsigned .less: left.data.unsigned < right.data.unsigned @@ -533,3 +542,47 @@ checked_unsigned proc(op ir.Op, left u64, right u64) ?u64 { else: unreachable } } + +@hide:file +convert proc(source ir.Value, target ir.Type) ?ir.Value { + result := ir.Value{ type = target } + if (target == .float) { + if (source.type == .float) result.data = source.data + else if (ir.unsigned_integer(source.type)) result.data = ir.ValueData{ real = f64(source.data.unsigned) } + else result.data = ir.ValueData{ real = f64(source.data.integer) } + return result + } + if (source.type == .float) { + real :: source.data.real + # Guard the conversion itself before testing narrower bounds; NaN fails + # ordered comparisons, and the upper 64-bit bound is exclusive. + if (ir.unsigned_integer(target)) { + if (!(real > -1.0 and real < 18446744073709551616.0)) return null + integer :: u64(real) + if (integer > ir.integer_max(target)) return null + result.data = ir.ValueData{ unsigned = integer } + } else { + if (!(real >= -9223372036854775808.0 and real < 9223372036854775808.0)) return null + integer :: i64(real) + if (integer < ir.integer_min(target) or integer > i64(ir.integer_max(target))) return null + result.data = ir.ValueData{ integer = integer } + } + return result + } + if (ir.unsigned_integer(source.type)) { + integer :: source.data.unsigned + if (integer > ir.integer_max(target)) return null + if (ir.unsigned_integer(target)) result.data = ir.ValueData{ unsigned = integer } + else result.data = ir.ValueData{ integer = i64(integer) } + } else { + integer :: source.data.integer + if (ir.unsigned_integer(target)) { + if (integer < 0 or u64(integer) > ir.integer_max(target)) return null + result.data = ir.ValueData{ unsigned = u64(integer) } + } else { + if (integer < ir.integer_min(target) or integer > i64(ir.integer_max(target))) return null + result.data = ir.ValueData{ integer = integer } + } + } + return result +} diff --git a/source/ir/ir.hon b/source/ir/ir.hon index 2890197..2a21280 100644 --- a/source/ir/ir.hon +++ b/source/ir/ir.hon @@ -24,6 +24,48 @@ Type :: enum { array, usize, address, + i8, i16, i32, isize, + u8, u16, u32, u64, +} + +#! integer storage widths for the supported 64-bit target; zero for noninteger types. +integer_bits proc(type Type) usize { + return match type { + .i8, .u8: 8 + .i16, .u16: 16 + .i32, .u32: 32 + .int, .isize, .usize, .u64: 64 + else: 0 + } +} + +unsigned_integer proc(type Type) bool { + return type == .u8 or type == .u16 or type == .u32 or type == .u64 or type == .usize +} + +integer_max proc(type Type) u64 { + return match type { + .i8: 127 + .i16: 32767 + .i32: 2147483647 + .int, .isize: u64(maxval!(i64)) + .u8: 255 + .u16: 65535 + .u32: 4294967295 + .u64, .usize: maxval!(u64) + else: unreachable + } +} + +integer_min proc(type Type) i64 { + return match type { + .i8: -128 + .i16: -32768 + .i32: -2147483648 + .int, .isize: minval!(i64) + .u8, .u16, .u32, .u64, .usize: 0 + else: unreachable + } } ValueData :: union { @@ -74,6 +116,7 @@ Op :: enum { load_address store_address negate + convert logical_not add subtract diff --git a/source/llvm/instructions.hon b/source/llvm/instructions.hon index 4c71d3e..4a28496 100644 --- a/source/llvm/instructions.hon +++ b/source/llvm/instructions.hon @@ -136,6 +136,7 @@ emit_instruction proc(state @mut State, block @ir.Block, instruction ir.Instruct } try io.write_all(output, ")\n") } + .convert: result = try emit_conversion(state, instruction) .negate: { operand :: try load(state, instruction.left) if (instruction.type == .float) { @@ -143,10 +144,12 @@ emit_instruction proc(state @mut State, block @ir.Block, instruction ir.Instruct } else { pair :: fresh(state) overflow :: fresh(state) - try io.print(" %r{} = call {{ i64, i1 }} @llvm.ssub.with.overflow.i64(i64 0, i64 %r{})\n", output, {pair, operand}) - try io.print(" %r{} = extractvalue {{ i64, i1 }} %r{}, 1\n", output, {overflow, pair}) + bits :: ir.integer_bits(instruction.type) + sign []u8 :: if (ir.unsigned_integer(instruction.type)) "u" else "s" + try io.print(" %r{} = call {{ i{}, i1 }} @llvm.{s}sub.with.overflow.i{}(i{} 0, i{} %r{})\n", output, {pair, bits, sign, bits, bits, bits, operand}) + try io.print(" %r{} = extractvalue {{ i{}, i1 }} %r{}, 1\n", output, {overflow, bits, pair}) try check_failure(state, overflow, .integer_overflow, instruction.span) - try io.print(" %r{} = extractvalue {{ i64, i1 }} %r{}, 0\n", output, {result, pair}) + try io.print(" %r{} = extractvalue {{ i{}, i1 }} %r{}, 0\n", output, {result, bits, pair}) } } .logical_not: { @@ -172,13 +175,14 @@ emit_instruction proc(state @mut State, block @ir.Block, instruction ir.Instruct .multiply: "mul" else: unreachable } - sign []u8 :: if (instruction.type == .usize) "u" else "s" + bits :: ir.integer_bits(instruction.type) + sign []u8 :: if (ir.unsigned_integer(instruction.type)) "u" else "s" pair :: fresh(state) overflow :: fresh(state) - try io.print(" %r{} = call {{ i64, i1 }} @llvm.{s}{s}.with.overflow.i64(i64 %r{}, i64 %r{})\n", output, {pair, sign, opcode, left, right}) - try io.print(" %r{} = extractvalue {{ i64, i1 }} %r{}, 1\n", output, {overflow, pair}) + try io.print(" %r{} = call {{ i{}, i1 }} @llvm.{s}{s}.with.overflow.i{}(i{} %r{}, i{} %r{})\n", output, {pair, bits, sign, opcode, bits, bits, left, bits, right}) + try io.print(" %r{} = extractvalue {{ i{}, i1 }} %r{}, 1\n", output, {overflow, bits, pair}) try check_failure(state, overflow, .integer_overflow, instruction.span) - try io.print(" %r{} = extractvalue {{ i64, i1 }} %r{}, 0\n", output, {result, pair}) + try io.print(" %r{} = extractvalue {{ i{}, i1 }} %r{}, 0\n", output, {result, bits, pair}) } } .equal, .not_equal, .less, .less_equal, .greater, .greater_equal: { @@ -199,7 +203,7 @@ emit_instruction proc(state @mut State, block @ir.Block, instruction ir.Instruct } else { prefix []u8 := "" if (instruction.op != .equal and instruction.op != .not_equal) { - if (operand_type.kind == .usize) prefix = "u" + if (ir.unsigned_integer(operand_type.kind)) prefix = "u" else prefix = "s" } predicate []u8 :: match instruction.op { @@ -219,3 +223,69 @@ emit_instruction proc(state @mut State, block @ir.Block, instruction ir.Instruct } try store(state, instruction.destination, result) } + +@hide:file +emit_conversion proc(state @mut State, instruction ir.Instruction) usize ! io.WriteError { + output :: state.output + source :: state.values[usize(instruction.left)].kind + destination :: instruction.type + operand :: try load(state, instruction.left) + if (source == .float and destination == .float) return operand + source_bits :: ir.integer_bits(source) + destination_bits :: ir.integer_bits(destination) + source_unsigned :: ir.unsigned_integer(source) + destination_unsigned :: ir.unsigned_integer(destination) + if (destination == .float) { + result :: fresh(state) + opcode []u8 :: if (source_unsigned) "uitofp" else "sitofp" + try io.print(" %r{} = {s} i{} %r{} to double\n", output, {result, opcode, source_bits, operand}) + return result + } + if (source == .float) { + # compare the truncated value before fptosi/fptoui, whose out-of-range result is poison. + truncated :: fresh(state) + lower_valid :: fresh(state) + upper_valid :: fresh(state) + valid :: fresh(state) + failed :: fresh(state) + upper []u8 :: match destination { + .i8: "1.280000e+02" + .i16: "3.276800e+04" + .i32: "2.147483648e+09" + .int, .isize: "9.223372036854775808e+18" + .u8: "2.560000e+02" + .u16: "6.553600e+04" + .u32: "4.294967296e+09" + .u64, .usize: "1.8446744073709551616e+19" + else: unreachable + } + try io.print(" %r{} = call double @llvm.trunc.f64(double %r{})\n", output, {truncated, operand}) + if (destination_unsigned) try io.print(" %r{} = fcmp oge double %r{}, 0.0\n", output, {lower_valid, truncated}) + else try io.print(" %r{} = fcmp oge double %r{}, -{s}\n", output, {lower_valid, truncated, upper}) + try io.print(" %r{} = fcmp olt double %r{}, {s}\n", output, {upper_valid, truncated, upper}) + try io.print(" %r{} = and i1 %r{}, %r{}\n", output, {valid, lower_valid, upper_valid}) + try io.print(" %r{} = xor i1 %r{}, true\n", output, {failed, valid}) + try check_failure(state, failed, .integer_overflow, instruction.span) + result :: fresh(state) + opcode []u8 :: if (destination_unsigned) "fptoui" else "fptosi" + try io.print(" %r{} = {s} double %r{} to i{}\n", output, {result, opcode, truncated, destination_bits}) + return result + } + if (!source_unsigned and (destination_unsigned or destination_bits < source_bits)) { + failed :: fresh(state) + try io.print(" %r{} = icmp slt i{} %r{}, {}\n", output, {failed, source_bits, operand, ir.integer_min(destination)}) + try check_failure(state, failed, .integer_overflow, instruction.span) + } + if (destination_bits < source_bits or (source_unsigned and !destination_unsigned and destination_bits == source_bits)) { + failed :: fresh(state) + predicate []u8 :: if (source_unsigned) "ugt" else "sgt" + try io.print(" %r{} = icmp {s} i{} %r{}, {}\n", output, {failed, predicate, source_bits, operand, ir.integer_max(destination)}) + try check_failure(state, failed, .integer_overflow, instruction.span) + } + if (source_bits == destination_bits) return operand + result :: fresh(state) + opcode []u8 := "trunc" + if (source_bits < destination_bits) opcode = if (source_unsigned) "zext" else "sext" + try io.print(" %r{} = {s} i{} %r{} to i{}\n", output, {result, opcode, source_bits, operand, destination_bits}) + return result +} diff --git a/source/llvm/llvm.hon b/source/llvm/llvm.hon index 0fc5f44..bed0bb8 100644 --- a/source/llvm/llvm.hon +++ b/source/llvm/llvm.hon @@ -123,7 +123,10 @@ bytes_constant proc(output io.Writer, bytes []u8) void ! io.WriteError { @hide write_type proc(output io.Writer, module @ir.Module, type Type) void ! io.WriteError { match type.kind { - .int, .usize: try io.write_all(output, "i64") + .i8, .u8: try io.write_all(output, "i8") + .i16, .u16: try io.write_all(output, "i16") + .i32, .u32: try io.write_all(output, "i32") + .int, .isize, .u64, .usize: try io.write_all(output, "i64") .float: try io.write_all(output, "double") .bool: try io.write_all(output, "i1") .string: try io.write_all(output, "%string") @@ -243,7 +246,15 @@ emit_global proc(output io.Writer, module @ir.Module, global ir.Global, index us @hide:file print_call proc(output io.Writer, module @ir.Module, type Type, operand []u8) void ! io.WriteError { match type.kind { - .int, .usize: try io.print(" call void @print_integer(i32 1, i64 {s}, i1 {})\n", output, {operand, type.kind == .usize}) + .i8, .i16, .i32, .int, .isize, .u8, .u16, .u32, .u64, .usize: { + bits :: ir.integer_bits(type.kind) + unsigned :: ir.unsigned_integer(type.kind) + if (bits < 64) { + extension []u8 :: if (unsigned) "zext" else "sext" + try io.print(" %print_value = {s} i{} {s} to i64\n", output, {extension, bits, operand}) + try io.print(" call void @print_integer(i32 1, i64 %print_value, i1 {})\n", output, {unsigned}) + } else try io.print(" call void @print_integer(i32 1, i64 {s}, i1 {})\n", output, {operand, unsigned}) + } .float: try io.print(" call void @print_float(double {s})\n", output, {operand}) .bool: try io.print(" call void @print_bool(i1 {s})\n", output, {operand}) .string: try io.print(" call void @print_string(%string {s})\n", output, {operand}) @@ -334,8 +345,8 @@ check_failure proc(state @mut State, condition usize, code diagnostics.Code, spa @hide write_constant proc(output io.Writer, module @ir.Module, value ir.Value) void ! io.WriteError { match value.type { - .int: try io.print("{}", output, {value.data.integer}) - .usize: try io.print("{}", output, {value.data.unsigned}) + .i8, .i16, .i32, .int, .isize: try io.print("{}", output, {value.data.integer}) + .u8, .u16, .u32, .u64, .usize: try io.print("{}", output, {value.data.unsigned}) .float: { bits :: ptrcast!(@u64, &value.data.real)^ try io.print("bitcast (i64 {} to double)", output, {bits}) diff --git a/source/llvm/runtime.hon b/source/llvm/runtime.hon index efb7205..2614677 100644 --- a/source/llvm/runtime.hon +++ b/source/llvm/runtime.hon @@ -7,6 +7,25 @@ runtime :: `declare ptr @__error() `declare i32 @snprintf(ptr, i64, ptr, ...) `declare void @exit(i32) noreturn + `declare double @llvm.trunc.f64(double) + `declare { i8, i1 } @llvm.sadd.with.overflow.i8(i8, i8) + `declare { i8, i1 } @llvm.ssub.with.overflow.i8(i8, i8) + `declare { i8, i1 } @llvm.smul.with.overflow.i8(i8, i8) + `declare { i8, i1 } @llvm.uadd.with.overflow.i8(i8, i8) + `declare { i8, i1 } @llvm.usub.with.overflow.i8(i8, i8) + `declare { i8, i1 } @llvm.umul.with.overflow.i8(i8, i8) + `declare { i16, i1 } @llvm.sadd.with.overflow.i16(i16, i16) + `declare { i16, i1 } @llvm.ssub.with.overflow.i16(i16, i16) + `declare { i16, i1 } @llvm.smul.with.overflow.i16(i16, i16) + `declare { i16, i1 } @llvm.uadd.with.overflow.i16(i16, i16) + `declare { i16, i1 } @llvm.usub.with.overflow.i16(i16, i16) + `declare { i16, i1 } @llvm.umul.with.overflow.i16(i16, i16) + `declare { i32, i1 } @llvm.sadd.with.overflow.i32(i32, i32) + `declare { i32, i1 } @llvm.ssub.with.overflow.i32(i32, i32) + `declare { i32, i1 } @llvm.smul.with.overflow.i32(i32, i32) + `declare { i32, i1 } @llvm.uadd.with.overflow.i32(i32, i32) + `declare { i32, i1 } @llvm.usub.with.overflow.i32(i32, i32) + `declare { i32, i1 } @llvm.umul.with.overflow.i32(i32, i32) `declare { i64, i1 } @llvm.sadd.with.overflow.i64(i64, i64) `declare { i64, i1 } @llvm.ssub.with.overflow.i64(i64, i64) `declare { i64, i1 } @llvm.smul.with.overflow.i64(i64, i64) diff --git a/source/lower/lower.hon b/source/lower/lower.hon index 9d42cca..db48c62 100644 --- a/source/lower/lower.hon +++ b/source/lower/lower.hon @@ -126,6 +126,14 @@ lower_type proc(type typecheck.Type) ir.Type { return match type.kind { .int: .int .usize: .usize + .i8: .i8 + .i16: .i16 + .i32: .i32 + .isize: .isize + .u8: .u8 + .u16: .u16 + .u32: .u32 + .u64: .u64 .array: .array .float: .float .bool: .bool @@ -325,7 +333,7 @@ predeclare_locals proc(state @mut State, block ast.NodeId) void ! (mem.AllocErro .for_stmt: { loop :: ast.for_stmt(state.tree, statement) family :: typecheck.type_of(state.checked, loop.iterable).range_element - type ir.Type :: if (family == .usize) .usize else .int + type :: lower_type(typecheck.Type{ kind = family }) slot :: try ir.add_local(state.module, ir.FunctionId(state.function), type, ir.no_array) state.storage[usize(statement)] = Storage{ kind = .local, index = u32(slot) } if (loop.capture) |capture| state.storage[usize(capture)] = state.storage[usize(statement)] @@ -498,8 +506,8 @@ lower_for proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnos kind = .branch, value = finished, target = exit, alternative = increment, span = interval.span, } state.block = increment - unit := ir.Value{ type = .int, data = ir.ValueData{ integer = 1 } } - if (family == .usize) unit = ir.Value{ type = .usize, data = ir.ValueData{ unsigned = 1 } } + unit := ir.Value{ type = family, data = ir.ValueData{ integer = 1 } } + if (ir.unsigned_integer(family)) unit.data = ir.ValueData{ unsigned = 1 } one :: try constant(state, unit, interval.span) next :: try emit(state, ir.Instruction{ op = .add, type = family, left = last, right = one, span = interval.span }) _ = try emit(state, ir.Instruction{ op = .store_local, type = family, left = next, slot = slot, span = interval.span }) @@ -776,11 +784,12 @@ lower_expression proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocEr } .unary: { expression :: ast.unary_expr(tree, id) - if (expression.operator == .negate and type == .int and ast.expr_kind(tree, expression.operand) == .literal_int) { + if (expression.operator == .negate and ir.integer_bits(type) != 0 and !ir.unsigned_integer(type) and ast.expr_kind(tree, expression.operand) == .literal_int) { text :: ast.source_text(tree, ast.node_span(tree, expression.operand)) - if (integer_magnitude(text, u64(maxval!(i64)) + 1)) |magnitude| { - if (magnitude == u64(maxval!(i64)) + 1) { - return try constant(state, ir.Value{ type = .int, data = ir.ValueData{ integer = minval!(i64) } }, span) + magnitude_limit :: ir.integer_max(type) + 1 + if (integer_magnitude(text, magnitude_limit)) |magnitude| { + if (magnitude == magnitude_limit) { + return try constant(state, ir.Value{ type = type, data = ir.ValueData{ integer = ir.integer_min(type) } }, span) } } } @@ -817,6 +826,17 @@ lower_expression proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocEr return try emit(state, ir.Instruction{ op = op, type = type, left = left, right = right, span = span }) } .call: { + target :: sema.binding(state.resolution, ast.call_expr(tree, id).callee) + if (target.kind == .builtin) { + operand := ir.no_value + for (0..ast.call_arg_count(tree, id)) |i| { + value :: try lower_expression(state, ast.call_arg(tree, id, i)) + if (state.terminated) return ir.no_value + if (i == 0) operand = value + } + if (trap_type(state, id)) return ir.no_value + return try emit(state, ir.Instruction{ op = .convert, type = type, left = operand, span = span }) + } callee :: try lower_expression(state, ast.call_expr(tree, id).callee) if (state.terminated) return ir.no_value # recursion borrows later scratch slots; no items pointer survives a child. @@ -856,7 +876,7 @@ integer_magnitude proc(text []u8, limit u64) ?u64 { for (text) |byte| { if (byte < '0' or byte > '9') return null digit :: u64(byte - '0') - if (value > divtrunc!(limit - digit, u64(10))) return null + if (digit > limit or value > divtrunc!(limit - digit, u64(10))) return null value = value * 10 + digit } return value @@ -878,13 +898,13 @@ lower_number proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError span :: ast.node_span(state.tree, id) text :: ast.source_text(state.tree, span) type :: lower_type(typecheck.type_of(state.checked, id)) - if (type == .int) { - if (integer_magnitude(text, u64(maxval!(i64)))) |magnitude| { - return try constant(state, ir.Value{ type = .int, data = ir.ValueData{ integer = i64(magnitude) } }, span) + if (ir.integer_bits(type) != 0 and !ir.unsigned_integer(type)) { + if (integer_magnitude(text, ir.integer_max(type))) |magnitude| { + return try constant(state, ir.Value{ type = type, data = ir.ValueData{ integer = i64(magnitude) } }, span) } - } else if (type == .usize) { - if (integer_magnitude(text, maxval!(u64))) |magnitude| { - return try constant(state, ir.Value{ type = .usize, data = ir.ValueData{ unsigned = magnitude } }, span) + } else if (ir.unsigned_integer(type)) { + if (integer_magnitude(text, ir.integer_max(type))) |magnitude| { + return try constant(state, ir.Value{ type = type, data = ir.ValueData{ unsigned = magnitude } }, span) } } else { value :: strconv.parse_float(state.module.allocator, text) catch |err| { diff --git a/source/main.hon b/source/main.hon index 41e0824..2b7bfb3 100644 --- a/source/main.hon +++ b/source/main.hon @@ -203,8 +203,8 @@ print_result proc(output io.Writer, module @ir.Module, value ir.Value) void ! io @hide:file print_value proc(output io.Writer, module @ir.Module, value ir.Value) void ! io.WriteError { match value.type { - .int: try io.print("{}", output, {value.data.integer}) - .usize: try io.print("{}", output, {value.data.unsigned}) + .int, .i8, .i16, .i32, .isize: try io.print("{}", output, {value.data.integer}) + .usize, .u8, .u16, .u32, .u64: try io.print("{}", output, {value.data.unsigned}) .float: try io.print("{d}", output, {value.data.real}) .bool: try io.print("{}", output, {value.data.boolean}) .string: try io.print("{s}", output, {ir.string_bytes(module, value.data.string)}) diff --git a/source/sema/check/check.hon b/source/sema/check/check.hon index 69ebfba..7911cc5 100644 --- a/source/sema/check/check.hon +++ b/source/sema/check/check.hon @@ -18,6 +18,14 @@ TypeKind :: enum { range, array, usize, + i8, + i16, + i32, + isize, + u8, + u16, + u32, + u64, } ArrayId :: distinct u32 @@ -84,6 +92,7 @@ Cell :: struct { numeric bool = false anchored bool = false demand TypeKind = .none + conversion_unsigned bool = false fixed bool = false local bool = false assignment_value bool = false @@ -224,6 +233,7 @@ check proc( for (0..count) |i| { id :: ast.NodeId(i) if (cells[i].role != .expression or cells[i].assignment_value != assignment_values or ast.expr_kind(tree, id) != .call) continue + if (conversion_kind(&state, id) != .none) continue callee :: ast.call_expr(tree, id).callee signature :: try infer_shape(&state, callee, false) if (signature.function != ast.NodeId(maxval!(u32))) { @@ -236,6 +246,7 @@ check proc( for (0..count) |i| { id :: ast.NodeId(i) if (cells[i].role != .expression or cells[i].assignment_value != assignment_values or ast.expr_kind(tree, id) != .call) continue + if (conversion_kind(&state, id) != .none) continue callee :: ast.call_expr(tree, id).callee signature :: group_type(&state, callee) if (signature.function != ast.NodeId(maxval!(u32))) continue @@ -314,7 +325,8 @@ check proc( if (root(&state, i) != i or cells[i].assignment_value != assignment_values) continue if (cells[i].type.kind == .invalid) continue if (cells[i].numeric and cells[i].anchored) { - kind TypeKind :: if (cells[i].demand != .none) cells[i].demand else .int + kind TypeKind :: if (cells[i].demand != .none) cells[i].demand + else if (cells[i].conversion_unsigned) .u64 else .int cells[i].type = Type{ kind = kind } } } @@ -323,6 +335,7 @@ check proc( for (0..count) |i| { id :: ast.NodeId(i) if (cells[i].role != .expression or cells[i].assignment_value != assignment_values or ast.expr_kind(tree, id) != .call) continue + if (conversion_kind(&state, id) != .none) continue callee :: ast.call_expr(tree, id).callee signature :: group_type(&state, callee) if (signature.function != ast.NodeId(maxval!(u32)) or signature.kind == .none or signature.kind == .invalid) continue @@ -505,6 +518,7 @@ join proc(state @mut State, left, right ast.NodeId) void { state.cells[a].numeric = state.cells[a].numeric or state.cells[b].numeric state.cells[a].anchored = state.cells[a].anchored or state.cells[b].anchored if (state.cells[a].demand == .none) state.cells[a].demand = state.cells[b].demand + state.cells[a].conversion_unsigned = state.cells[a].conversion_unsigned or state.cells[b].conversion_unsigned if (state.cells[a].literal == ast.NodeId(maxval!(u32))) state.cells[a].literal = state.cells[b].literal if (state.cells[a].producer == ast.NodeId(maxval!(u32))) state.cells[a].producer = state.cells[b].producer } @@ -563,14 +577,7 @@ check_annotation proc(state @mut State, id ast.NodeId, allow_void bool) Type ! ( match binding.kind { .invalid: type = invalid_type(binding.data.diagnostic) .builtin: { - type.kind = match binding.data.builtin { - .int, .i64: .int - .float, .f64: .float - .usize: .usize - .bool: .bool - .string: .string - .void: .void - } + type.kind = builtin_kind(binding.data.builtin) if (type.kind == .void and !allow_void) { type = invalid_type(try diagnostics.add(state.diagnostics, .invalid_void_type, ast.node_span(state.tree, id))) } @@ -813,8 +820,16 @@ collect_expression proc(state @mut State, id ast.NodeId) void ! (mem.AllocError } .call: { callee :: ast.call_expr(tree, id).callee - try collect_expression(state, callee) - try depend(state, callee, id) + destination :: conversion_kind(state, id) + if (destination != .none) { + state.cells[usize(callee)].role = .annotation + state.cells[usize(callee)].type = Type{ kind = destination } + state.types[usize(callee)] = Type{ kind = destination } + state.cells[usize(id)].type = Type{ kind = destination } + } else { + try collect_expression(state, callee) + try depend(state, callee, id) + } for (0..ast.call_arg_count(tree, id)) |i| { argument :: ast.call_arg(tree, id, i) try collect_expression(state, argument) @@ -849,9 +864,51 @@ declaration_reference proc(tree @ast.Tree, id ast.NodeId) bool { return kind == .identifier or kind == .selection } +@hide:file +integer_kind proc(kind TypeKind) bool { + return ( + kind == .int or kind == .i8 or kind == .i16 or kind == .i32 or kind == .isize + or unsigned_kind(kind) + ) +} + +@hide:file +unsigned_kind proc(kind TypeKind) bool { + return kind == .u8 or kind == .u16 or kind == .u32 or kind == .u64 or kind == .usize +} + @hide:file numeric_kind proc(kind TypeKind) bool { - return kind == .int or kind == .float or kind == .usize + return integer_kind(kind) or kind == .float +} + +@hide:file +builtin_kind proc(type sema.BuiltinType) TypeKind { + return match type { + .int, .i64: .int + .float, .f64: .float + .i8: .i8 + .i16: .i16 + .i32: .i32 + .isize: .isize + .u8: .u8 + .u16: .u16 + .u32: .u32 + .u64, .uint: .u64 + .usize: .usize + .bool: .bool + .string: .string + .void: .void + } +} + +@hide:file +conversion_kind proc(state @State, id ast.NodeId) TypeKind { + callee :: ast.call_expr(state.tree, id).callee + binding :: sema.binding(state.resolution, callee) + if (binding.kind != .builtin) return .none + kind :: builtin_kind(binding.data.builtin) + return if (numeric_kind(kind)) kind else .none } @hide:file @@ -1072,7 +1129,7 @@ validate proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnost return } operand :: state.types[usize(expression.operand)] - if (operand.kind != .invalid and (!numeric_kind(operand.kind) or operand.kind == .usize)) { + if (operand.kind != .invalid and (!numeric_kind(operand.kind) or unsigned_kind(operand.kind))) { try fail(state, id, .invalid_operand, ast.node_span(tree, id)) } } @@ -1098,11 +1155,12 @@ validate proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnost expression :: ast.range_expr(tree, id) start_type :: state.types[usize(expression.start)] end_type :: state.types[usize(expression.end)] - family TypeKind :: if (start_type.kind == .usize or end_type.kind == .usize) .usize else .int - if (start_type.kind != .invalid and start_type.kind != family) { + family TypeKind :: if (integer_kind(start_type.kind)) start_type.kind + else if (integer_kind(end_type.kind)) end_type.kind else .int + if (start_type.kind != .invalid and (!integer_kind(start_type.kind) or start_type.kind != family)) { try fail(state, expression.start, .invalid_range_bound, ast.node_span(tree, expression.start)) } - if (end_type.kind != .invalid and end_type.kind != family) { + if (end_type.kind != .invalid and (!integer_kind(end_type.kind) or end_type.kind != family)) { try fail(state, expression.end, .invalid_range_bound, ast.node_span(tree, expression.end)) } } @@ -1143,6 +1201,19 @@ validate proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnost validate_call proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) { tree :: state.tree expression :: ast.call_expr(tree, id) + if (conversion_kind(state, id) != .none) { + if (ast.call_arg_count(tree, id) != 1) { + try fail(state, id, .argument_count_mismatch, expression.span) + } + for 0..ast.call_arg_count(tree, id) |i| { + argument :: ast.call_arg(tree, id, i) + actual :: state.types[usize(argument)] + if (actual.kind != .invalid and !numeric_kind(actual.kind)) { + try fail(state, argument, .invalid_operand, ast.node_span(tree, argument)) + } + } + return + } callee :: state.types[usize(expression.callee)] signature :: group_type(state, expression.callee) if (signature.function == ast.NodeId(maxval!(u32))) { @@ -1219,9 +1290,7 @@ constant_integer proc(state @mut State, id ast.NodeId) ?ConstantInteger { if (declaration.mutability != .constant) return null if (declaration.type) |annotation| { type_binding :: sema.binding(state.resolution, annotation) - if (type_binding.kind != .builtin) return null - kind :: type_binding.data.builtin - if (kind != .int and kind != .i64 and kind != .usize) return null + if (type_binding.kind != .builtin or !integer_kind(builtin_kind(type_binding.data.builtin))) return null } return constant_integer(state, declaration.initializer) } @@ -1338,6 +1407,13 @@ infer_shape proc(state @mut State, id ast.NodeId, defaults bool) Type ! (mem.All if (state.cells[index].type.kind == .invalid) return state.cells[index].type state.cells[index].visiting = true defer state.cells[index].visiting = false + if (state.cells[usize(id)].role == .expression and ast.expr_kind(state.tree, id) == .call) { + destination :: conversion_kind(state, id) + if (destination != .none) { + seed(state, id, Type{ kind = destination }) + return group_type(state, id) + } + } if (state.cells[usize(id)].role == .expression and declaration_reference(state.tree, id)) { binding :: sema.binding(state.resolution, id) if (binding.kind == .declaration) { @@ -1435,6 +1511,17 @@ apply_contexts proc(state @mut State, assignment_values, early bool) void ! (mem if (mutable_target(state, view.target)) try context_demand(state, view.value, state.types[usize(view.target)], .type_mismatch, early) } if (state.cells[i].role != .expression or state.cells[i].assignment_value != assignment_values or ast.expr_kind(tree, id) != .call) continue + if (conversion_kind(state, id) != .none) { + for 0..ast.call_arg_count(tree, id) |j| { + argument :: ast.call_arg(tree, id, j) + if (constant_integer(state, argument)) |value| { + if (!value.negative and value.magnitude > u64(maxval!(i64))) { + state.cells[root(state, usize(argument))].conversion_unsigned = true + } + } + } + continue + } signature :: group_type(state, ast.call_expr(tree, id).callee) if (signature.function == ast.NodeId(maxval!(u32))) continue count :: ast.function_param_count(tree, signature.function) @@ -1448,7 +1535,7 @@ apply_contexts proc(state @mut State, assignment_values, early bool) void ! (mem @hide:file context_demand proc(state @mut State, id ast.NodeId, expected Type, code diagnostics.Code, early bool) void ! (mem.AllocError | diagnostics.CapacityError) { - if (early and expected.kind != .array and expected.kind != .usize) return + if (early and expected.kind != .array and !integer_kind(expected.kind)) return try demand(state, id, expected, code) } @@ -1466,8 +1553,8 @@ bound_family proc(state @mut State, id ast.NodeId) TypeKind { left :: bound_family(state, view.left) right :: bound_family(state, view.right) if (left == .float or right == .float) return .float - if (left == .usize or right == .usize) return .usize - if (left == .int or right == .int) return .int + if (integer_kind(left)) return left + if (integer_kind(right)) return right return .none } @@ -1480,7 +1567,7 @@ prepare_ranges proc(state @mut State, assignment_values, constrain bool) void ! view :: ast.range_expr(tree, id) left :: bound_family(state, view.start) right :: bound_family(state, view.end) - family TypeKind :: if (left == .usize or right == .usize) .usize else .int + family TypeKind :: if (integer_kind(left)) left else if (integer_kind(right)) right else .int if (constrain) { try range_bound_context(state, view.start, family) try range_bound_context(state, view.end, family) diff --git a/source/sema/resolve.hon b/source/sema/resolve.hon index d25656b..a78161f 100644 --- a/source/sema/resolve.hon +++ b/source/sema/resolve.hon @@ -11,6 +11,8 @@ BuiltinType :: enum { bool, string, void, i64, f64, usize, + i8, i16, i32, isize, + u8, u16, u32, u64, uint, } @hide:file @@ -29,6 +31,15 @@ builtins [_]Builtin :: [ { name = "i64", type = .i64 }, { name = "f64", type = .f64 }, { name = "usize", type = .usize }, + { name = "i8", type = .i8 }, + { name = "i16", type = .i16 }, + { name = "i32", type = .i32 }, + { name = "isize", type = .isize }, + { name = "u8", type = .u8 }, + { name = "u16", type = .u16 }, + { name = "u32", type = .u32 }, + { name = "u64", type = .u64 }, + { name = "uint", type = .uint }, ] @hide:file diff --git a/tests/native.py b/tests/native.py index 8038c69..37fd432 100644 --- a/tests/native.py +++ b/tests/native.py @@ -230,6 +230,120 @@ CASES = { ), } +# Exercise each representation through storage, calls, ranges and checked updates. +for integer_type, bits, signed in ( + ("i8", 8, True), ("i16", 16, True), ("i32", 32, True), + ("i64", 64, True), ("isize", 64, True), + ("u8", 8, False), ("u16", 16, False), ("u32", 32, False), + ("u64", 64, False), ("usize", 64, False), +): + minimum = -(1 << (bits - 1)) if signed else 0 + maximum = (1 << (bits - int(signed))) - 1 + CASES[f"{integer_type}_storage"] = ( + f""" + seed [2]mut {integer_type} :: [{minimum}, {maximum}] + identity proc(a [2]mut {integer_type}) [2]mut {integer_type} {{ return a }} + main proc() [2]mut {integer_type} {{ + a := identity(seed) + b := a + a[0] = 1 + return b + }} + """, f"[{minimum}, {maximum}]\n".encode(), None, + ) + CASES[f"{integer_type}_inclusive_max"] = ( + f""" + main proc() int {{ + start {integer_type} :: {maximum - 1} + end {integer_type} :: {maximum} + count := 0 + for start..=end |i| {{ + if (i < start) return -1 + count = count + 1 + continue + }} + return count + }} + """, b"2\n", None, + ) + for operation, left, right in ( + ("+", maximum, 1), ("-", minimum, 1), ("*", maximum, 2), + ): + CASES[f"{integer_type}_overflow_{operation}"] = ( + f"main proc() {integer_type} {{ a {integer_type} :: {left}\nreturn a {operation} {right} }}", + b"", b"integer arithmetic overflow", + ) + CASES[f"{integer_type}_literal_range"] = ( + f"main proc() {integer_type} {{ return {maximum + 1} }}", + b"", b"numeric literal is outside", + ) + if signed: + CASES[f"{integer_type}_negate_min"] = ( + f"main proc() {integer_type} {{ a {integer_type} :: {minimum}\nreturn -a }}", + b"", b"integer arithmetic overflow", + ) + +CASES.update({ + "integer_casts": ( + """ + identity proc(n u8) u8 { return n } + main proc() [4]i64 { + large u64 :: 18446744073709551615 + small := identity(u8(i16(255))) + if (large != u64(18446744073709551615)) return [0, 0, 0, 0] + return [i64(small), i64(i8(-128)), i64(u32(4294967295)), i64(u8(255))] + } + """, + b"[255, -128, 4294967295, 255]\n", None, + ), + "cast_unsigned_max": ("main proc() u64 { return u64(18446744073709551615) }", b"18446744073709551615\n", None), + "cast_float_rounding": ("main proc() f64 { return f64(9007199254740993) - 9007199254740992.0 }", b"0\n", None), + "cast_fractional": ("main proc() [4]i64 { return [i64(i8(-128.9)), i64(u8(-0.9)), i64(u8(255.9)), i64(i8(127.9))] }", b"[-128, 0, 255, 127]\n", None), + "cast_float_u64_max_below": ("main proc() u64 { return u64(18446744073709549568.0) }", b"18446744073709549568\n", None), + "cast_float_i64_min": ("main proc() i64 { return i64(-9223372036854775808.0) }", b"-9223372036854775808\n", None), + "cast_unused_trap": ("bad proc() u8 { return u8(256) }\nmain proc() int { return 42 }", b"42\n", None), + "cast_short_circuit": ("main proc() bool { return true or u8(256) == 0 }", b"true\n", None), + "cast_bool_invalid": ("main proc() u8 { return u8(true) }", b"", b""), + "cast_zero_arguments": ("main proc() u8 { return u8() }", b"", b"argument count"), + "cast_extra_arguments": ("main proc() u8 { return u8(1, 2) }", b"", b"argument count"), + "cast_type_as_value": ("main proc() int { u8\nreturn 42 }", b"", b"type name cannot"), + "integer_no_implicit_narrowing": ("main proc() u8 { a u16 :: 1\nreturn a }", b"", b"return does not match"), + "integer_no_implicit_signedness": ("main proc() i8 { a u8 :: 1\nreturn a }", b"", b"return does not match"), + "integer_no_implicit_widening": ("main proc() u16 { a u8 :: 1\nreturn a }", b"", b"return does not match"), + "integer_assignment_cast": ( + "main proc() u8 { a u8 := 1\nb u16 :: 254\na = u8(b + 1)\nreturn a }", + b"255\n", None, + ), + "integer_function_value": ( + "identity proc(n i16) i16 { return n }\nmain proc() i16 { f :: identity\nreturn f(i16(-32768)) }", + b"-32768\n", None, + ), + "integer_typed_float_cast": ( + "main proc() f64 { a u64 :: 18446744073709551615\nreturn f64(a) - 18446744073709551616.0 }", + b"0\n", None, + ), + "integer_bad_index": ("main proc() i64 { a :: [42]\ni u8 :: 0\nreturn a[i] }", b"", b"array index must have type usize"), + "integer_cast_index": ("main proc() i64 { a :: [42]\ni u8 :: 0\nreturn a[usize(i)] }", b"42\n", None), + "integer_cast_unknown": ("main proc() u8 { return u8(missing) }", b"", b"unknown name"), + "integer_cast_bad_unused": ("bad proc() u8 { return u8(true) }\nmain proc() int { return 42 }", b"42\n", b""), +}) +for name, expression in { + "narrow": "u8(256)", + "signed_negative": "u8(-1)", + "signed_high": "i8(128)", + "unsigned_to_signed": "i64(u64(18446744073709551615))", + "float_high": "u8(256.0)", + "float_low": "i8(-129.0)", + "float_unsigned_negative": "u8(-1.0)", + "float_signed_limit": "i64(9223372036854775808.0)", + "float_unsigned_limit": "u64(18446744073709551616.0)", + "nan": "i32(0.0 / 0.0)", + "infinity": "u32(1.0 / 0.0)", +}.items(): + CASES[f"cast_trap_{name}"] = ( + f"main proc() i64 {{ return i64({expression}) }}", b"", b"integer arithmetic overflow", + ) + def run(*args, env=None): return subprocess.run(args, cwd=ROOT, capture_output=True, timeout=30, env=env) @@ -245,9 +359,9 @@ def check(source, executable, expected=None): if expected is not None: stdout, error = expected assert interpreted.stdout == stdout, (source.name, outcome(interpreted), stdout) - assert interpreted.returncode == (1 if error else 0), outcome(interpreted) - if error: - assert error in interpreted.stderr, outcome(interpreted) + assert interpreted.returncode == (1 if error is not None else 0), outcome(interpreted) + if error is not None: + assert interpreted.stderr and error in interpreted.stderr, outcome(interpreted) for optimization in ("-O0", "-O2"): built = run(str(HONEY), "build", str(source), "-o", str(executable), optimization) assert built.returncode in (0, 1), (source.name, optimization, outcome(built))