feat: add integer widths and checked numeric conversions

This commit is contained in:
2026-10-25 22:19:14 +01:00
parent 0385e46295
commit 3e93c6f000
12 changed files with 500 additions and 66 deletions
+5 -3
View File
@@ -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. `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`. 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`. `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. 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. 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. 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. `@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. 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.
+6 -2
View File
@@ -5,8 +5,10 @@
## current boundary ## current boundary
directory packages support file-local import namespaces and postfix declaration directory packages support file-local import namespaces and postfix declaration
selection through the interpreter and native backend. the richer types/control selection through the interpreter and native backend. concrete 8/16/32/64-bit
flow used by the compiler remain unsupported. the driver retains frontend storage 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. and source provenance through `@source/compilation`, with compilation-wide AST node identities.
file commands retain single-file behavior; directory commands compile the file commands retain single-file behavior; directory commands compile the
package's production files and load its dependencies in separate scopes. 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 ## 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`. - [ ] 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`. - [ ] 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`. - [ ] 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. - [ ] 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.
+60 -7
View File
@@ -308,15 +308,20 @@ run proc(
continue 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: { .negate: {
operand :: frames.items[frame_index].values[usize(instruction.left)] operand :: frames.items[frame_index].values[usize(instruction.left)]
if instruction.type == .float { if instruction.type == .float {
value = ir.Value{ type = .float, data = ir.ValueData{ real = -operand.data.real } } value = ir.Value{ type = .float, data = ir.ValueData{ real = -operand.data.real } }
} else { } 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) 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: { .logical_not: {
@@ -336,17 +341,21 @@ run proc(
else: unreachable else: unreachable
} }
value = ir.Value{ type = .float, data = ir.ValueData{ real = result } } 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) result :: checked_unsigned(instruction.op, left.data.unsigned, right.data.unsigned)
if result |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 { } else {
return try runtime_trap(diagnostic_store, .integer_overflow, instruction.span) return try runtime_trap(diagnostic_store, .integer_overflow, instruction.span)
} }
} else { } else {
result :: checked_integer(instruction.op, left.data.integer, right.data.integer) result :: checked_integer(instruction.op, left.data.integer, right.data.integer)
if result |result_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 { } else {
return try runtime_trap(diagnostic_store, .integer_overflow, instruction.span) 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)] right :: frames.items[frame_index].values[usize(instruction.right)]
result := false result := false
match left.type { match left.type {
.int: result = match instruction.op { .int, .isize, .i8, .i16, .i32: result = match instruction.op {
.equal: left.data.integer == right.data.integer .equal: left.data.integer == right.data.integer
.not_equal: left.data.integer != right.data.integer .not_equal: left.data.integer != right.data.integer
.less: 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 .greater_equal: left.data.integer >= right.data.integer
else: unreachable else: unreachable
} }
.usize: result = match instruction.op { .usize, .u8, .u16, .u32, .u64: result = match instruction.op {
.equal: left.data.unsigned == right.data.unsigned .equal: left.data.unsigned == right.data.unsigned
.not_equal: left.data.unsigned != right.data.unsigned .not_equal: left.data.unsigned != right.data.unsigned
.less: 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 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
}
+43
View File
@@ -24,6 +24,48 @@ Type :: enum {
array, array,
usize, usize,
address, 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 { ValueData :: union {
@@ -74,6 +116,7 @@ Op :: enum {
load_address load_address
store_address store_address
negate negate
convert
logical_not logical_not
add add
subtract subtract
+78 -8
View File
@@ -136,6 +136,7 @@ emit_instruction proc(state @mut State, block @ir.Block, instruction ir.Instruct
} }
try io.write_all(output, ")\n") try io.write_all(output, ")\n")
} }
.convert: result = try emit_conversion(state, instruction)
.negate: { .negate: {
operand :: try load(state, instruction.left) operand :: try load(state, instruction.left)
if (instruction.type == .float) { if (instruction.type == .float) {
@@ -143,10 +144,12 @@ emit_instruction proc(state @mut State, block @ir.Block, instruction ir.Instruct
} else { } else {
pair :: fresh(state) pair :: fresh(state)
overflow :: 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}) bits :: ir.integer_bits(instruction.type)
try io.print(" %r{} = extractvalue {{ i64, i1 }} %r{}, 1\n", output, {overflow, pair}) 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 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: { .logical_not: {
@@ -172,13 +175,14 @@ emit_instruction proc(state @mut State, block @ir.Block, instruction ir.Instruct
.multiply: "mul" .multiply: "mul"
else: unreachable 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) pair :: fresh(state)
overflow :: 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{} = 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 {{ i64, i1 }} %r{}, 1\n", output, {overflow, pair}) try io.print(" %r{} = extractvalue {{ i{}, i1 }} %r{}, 1\n", output, {overflow, bits, pair})
try check_failure(state, overflow, .integer_overflow, instruction.span) 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: { .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 { } else {
prefix []u8 := "" prefix []u8 := ""
if (instruction.op != .equal and instruction.op != .not_equal) { 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" else prefix = "s"
} }
predicate []u8 :: match instruction.op { 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) 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
}
+15 -4
View File
@@ -123,7 +123,10 @@ bytes_constant proc(output io.Writer, bytes []u8) void ! io.WriteError {
@hide @hide
write_type proc(output io.Writer, module @ir.Module, type Type) void ! io.WriteError { write_type proc(output io.Writer, module @ir.Module, type Type) void ! io.WriteError {
match type.kind { 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") .float: try io.write_all(output, "double")
.bool: try io.write_all(output, "i1") .bool: try io.write_all(output, "i1")
.string: try io.write_all(output, "%string") .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 @hide:file
print_call proc(output io.Writer, module @ir.Module, type Type, operand []u8) void ! io.WriteError { print_call proc(output io.Writer, module @ir.Module, type Type, operand []u8) void ! io.WriteError {
match type.kind { 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}) .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}) .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}) .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 @hide
write_constant proc(output io.Writer, module @ir.Module, value ir.Value) void ! io.WriteError { write_constant proc(output io.Writer, module @ir.Module, value ir.Value) void ! io.WriteError {
match value.type { match value.type {
.int: try io.print("{}", output, {value.data.integer}) .i8, .i16, .i32, .int, .isize: try io.print("{}", output, {value.data.integer})
.usize: try io.print("{}", output, {value.data.unsigned}) .u8, .u16, .u32, .u64, .usize: try io.print("{}", output, {value.data.unsigned})
.float: { .float: {
bits :: ptrcast!(@u64, &value.data.real)^ bits :: ptrcast!(@u64, &value.data.real)^
try io.print("bitcast (i64 {} to double)", output, {bits}) try io.print("bitcast (i64 {} to double)", output, {bits})
+19
View File
@@ -7,6 +7,25 @@ runtime ::
`declare ptr @__error() `declare ptr @__error()
`declare i32 @snprintf(ptr, i64, ptr, ...) `declare i32 @snprintf(ptr, i64, ptr, ...)
`declare void @exit(i32) noreturn `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.sadd.with.overflow.i64(i64, i64)
`declare { i64, i1 } @llvm.ssub.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) `declare { i64, i1 } @llvm.smul.with.overflow.i64(i64, i64)
+34 -14
View File
@@ -126,6 +126,14 @@ lower_type proc(type typecheck.Type) ir.Type {
return match type.kind { return match type.kind {
.int: .int .int: .int
.usize: .usize .usize: .usize
.i8: .i8
.i16: .i16
.i32: .i32
.isize: .isize
.u8: .u8
.u16: .u16
.u32: .u32
.u64: .u64
.array: .array .array: .array
.float: .float .float: .float
.bool: .bool .bool: .bool
@@ -325,7 +333,7 @@ predeclare_locals proc(state @mut State, block ast.NodeId) void ! (mem.AllocErro
.for_stmt: { .for_stmt: {
loop :: ast.for_stmt(state.tree, statement) loop :: ast.for_stmt(state.tree, statement)
family :: typecheck.type_of(state.checked, loop.iterable).range_element 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) 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) } state.storage[usize(statement)] = Storage{ kind = .local, index = u32(slot) }
if (loop.capture) |capture| state.storage[usize(capture)] = state.storage[usize(statement)] 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, kind = .branch, value = finished, target = exit, alternative = increment, span = interval.span,
} }
state.block = increment state.block = increment
unit := ir.Value{ type = .int, data = ir.ValueData{ integer = 1 } } unit := ir.Value{ type = family, data = ir.ValueData{ integer = 1 } }
if (family == .usize) unit = ir.Value{ type = .usize, data = ir.ValueData{ unsigned = 1 } } if (ir.unsigned_integer(family)) unit.data = ir.ValueData{ unsigned = 1 }
one :: try constant(state, unit, interval.span) one :: try constant(state, unit, interval.span)
next :: try emit(state, ir.Instruction{ op = .add, type = family, left = last, right = one, span = 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 }) _ = 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: { .unary: {
expression :: ast.unary_expr(tree, id) 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)) text :: ast.source_text(tree, ast.node_span(tree, expression.operand))
if (integer_magnitude(text, u64(maxval!(i64)) + 1)) |magnitude| { magnitude_limit :: ir.integer_max(type) + 1
if (magnitude == u64(maxval!(i64)) + 1) { if (integer_magnitude(text, magnitude_limit)) |magnitude| {
return try constant(state, ir.Value{ type = .int, data = ir.ValueData{ integer = minval!(i64) } }, span) 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 }) return try emit(state, ir.Instruction{ op = op, type = type, left = left, right = right, span = span })
} }
.call: { .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) callee :: try lower_expression(state, ast.call_expr(tree, id).callee)
if (state.terminated) return ir.no_value if (state.terminated) return ir.no_value
# recursion borrows later scratch slots; no items pointer survives a child. # 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| { for (text) |byte| {
if (byte < '0' or byte > '9') return null if (byte < '0' or byte > '9') return null
digit :: u64(byte - '0') 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 value = value * 10 + digit
} }
return value 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) span :: ast.node_span(state.tree, id)
text :: ast.source_text(state.tree, span) text :: ast.source_text(state.tree, span)
type :: lower_type(typecheck.type_of(state.checked, id)) type :: lower_type(typecheck.type_of(state.checked, id))
if (type == .int) { if (ir.integer_bits(type) != 0 and !ir.unsigned_integer(type)) {
if (integer_magnitude(text, u64(maxval!(i64)))) |magnitude| { if (integer_magnitude(text, ir.integer_max(type))) |magnitude| {
return try constant(state, ir.Value{ type = .int, data = ir.ValueData{ integer = i64(magnitude) } }, span) return try constant(state, ir.Value{ type = type, data = ir.ValueData{ integer = i64(magnitude) } }, span)
} }
} else if (type == .usize) { } else if (ir.unsigned_integer(type)) {
if (integer_magnitude(text, maxval!(u64))) |magnitude| { if (integer_magnitude(text, ir.integer_max(type))) |magnitude| {
return try constant(state, ir.Value{ type = .usize, data = ir.ValueData{ unsigned = magnitude } }, span) return try constant(state, ir.Value{ type = type, data = ir.ValueData{ unsigned = magnitude } }, span)
} }
} else { } else {
value :: strconv.parse_float(state.module.allocator, text) catch |err| { value :: strconv.parse_float(state.module.allocator, text) catch |err| {
+2 -2
View File
@@ -203,8 +203,8 @@ print_result proc(output io.Writer, module @ir.Module, value ir.Value) void ! io
@hide:file @hide:file
print_value proc(output io.Writer, module @ir.Module, value ir.Value) void ! io.WriteError { print_value proc(output io.Writer, module @ir.Module, value ir.Value) void ! io.WriteError {
match value.type { match value.type {
.int: try io.print("{}", output, {value.data.integer}) .int, .i8, .i16, .i32, .isize: try io.print("{}", output, {value.data.integer})
.usize: try io.print("{}", output, {value.data.unsigned}) .usize, .u8, .u16, .u32, .u64: try io.print("{}", output, {value.data.unsigned})
.float: try io.print("{d}", output, {value.data.real}) .float: try io.print("{d}", output, {value.data.real})
.bool: try io.print("{}", output, {value.data.boolean}) .bool: try io.print("{}", output, {value.data.boolean})
.string: try io.print("{s}", output, {ir.string_bytes(module, value.data.string)}) .string: try io.print("{s}", output, {ir.string_bytes(module, value.data.string)})
+110 -23
View File
@@ -18,6 +18,14 @@ TypeKind :: enum {
range, range,
array, array,
usize, usize,
i8,
i16,
i32,
isize,
u8,
u16,
u32,
u64,
} }
ArrayId :: distinct u32 ArrayId :: distinct u32
@@ -84,6 +92,7 @@ Cell :: struct {
numeric bool = false numeric bool = false
anchored bool = false anchored bool = false
demand TypeKind = .none demand TypeKind = .none
conversion_unsigned bool = false
fixed bool = false fixed bool = false
local bool = false local bool = false
assignment_value bool = false assignment_value bool = false
@@ -224,6 +233,7 @@ check proc(
for (0..count) |i| { for (0..count) |i| {
id :: ast.NodeId(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 (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 callee :: ast.call_expr(tree, id).callee
signature :: try infer_shape(&state, callee, false) signature :: try infer_shape(&state, callee, false)
if (signature.function != ast.NodeId(maxval!(u32))) { if (signature.function != ast.NodeId(maxval!(u32))) {
@@ -236,6 +246,7 @@ check proc(
for (0..count) |i| { for (0..count) |i| {
id :: ast.NodeId(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 (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 callee :: ast.call_expr(tree, id).callee
signature :: group_type(&state, callee) signature :: group_type(&state, callee)
if (signature.function != ast.NodeId(maxval!(u32))) continue 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 (root(&state, i) != i or cells[i].assignment_value != assignment_values) continue
if (cells[i].type.kind == .invalid) continue if (cells[i].type.kind == .invalid) continue
if (cells[i].numeric and cells[i].anchored) { 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 } cells[i].type = Type{ kind = kind }
} }
} }
@@ -323,6 +335,7 @@ check proc(
for (0..count) |i| { for (0..count) |i| {
id :: ast.NodeId(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 (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 callee :: ast.call_expr(tree, id).callee
signature :: group_type(&state, callee) signature :: group_type(&state, callee)
if (signature.function != ast.NodeId(maxval!(u32)) or signature.kind == .none or signature.kind == .invalid) continue 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].numeric = state.cells[a].numeric or state.cells[b].numeric
state.cells[a].anchored = state.cells[a].anchored or state.cells[b].anchored 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 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].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 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 { match binding.kind {
.invalid: type = invalid_type(binding.data.diagnostic) .invalid: type = invalid_type(binding.data.diagnostic)
.builtin: { .builtin: {
type.kind = match binding.data.builtin { type.kind = builtin_kind(binding.data.builtin)
.int, .i64: .int
.float, .f64: .float
.usize: .usize
.bool: .bool
.string: .string
.void: .void
}
if (type.kind == .void and !allow_void) { if (type.kind == .void and !allow_void) {
type = invalid_type(try diagnostics.add(state.diagnostics, .invalid_void_type, ast.node_span(state.tree, id))) 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: { .call: {
callee :: ast.call_expr(tree, id).callee callee :: ast.call_expr(tree, id).callee
try collect_expression(state, callee) destination :: conversion_kind(state, id)
try depend(state, callee, 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| { for (0..ast.call_arg_count(tree, id)) |i| {
argument :: ast.call_arg(tree, id, i) argument :: ast.call_arg(tree, id, i)
try collect_expression(state, argument) 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 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 @hide:file
numeric_kind proc(kind TypeKind) bool { 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 @hide:file
@@ -1072,7 +1129,7 @@ validate proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnost
return return
} }
operand :: state.types[usize(expression.operand)] 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)) 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) expression :: ast.range_expr(tree, id)
start_type :: state.types[usize(expression.start)] start_type :: state.types[usize(expression.start)]
end_type :: state.types[usize(expression.end)] end_type :: state.types[usize(expression.end)]
family TypeKind :: if (start_type.kind == .usize or end_type.kind == .usize) .usize else .int family TypeKind :: if (integer_kind(start_type.kind)) start_type.kind
if (start_type.kind != .invalid and start_type.kind != family) { 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)) 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)) 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) { validate_call proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
tree :: state.tree tree :: state.tree
expression :: ast.call_expr(tree, id) 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)] callee :: state.types[usize(expression.callee)]
signature :: group_type(state, expression.callee) signature :: group_type(state, expression.callee)
if (signature.function == ast.NodeId(maxval!(u32))) { 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.mutability != .constant) return null
if (declaration.type) |annotation| { if (declaration.type) |annotation| {
type_binding :: sema.binding(state.resolution, annotation) type_binding :: sema.binding(state.resolution, annotation)
if (type_binding.kind != .builtin) return null if (type_binding.kind != .builtin or !integer_kind(builtin_kind(type_binding.data.builtin))) return null
kind :: type_binding.data.builtin
if (kind != .int and kind != .i64 and kind != .usize) return null
} }
return constant_integer(state, declaration.initializer) 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 if (state.cells[index].type.kind == .invalid) return state.cells[index].type
state.cells[index].visiting = true state.cells[index].visiting = true
defer state.cells[index].visiting = false 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)) { if (state.cells[usize(id)].role == .expression and declaration_reference(state.tree, id)) {
binding :: sema.binding(state.resolution, id) binding :: sema.binding(state.resolution, id)
if (binding.kind == .declaration) { 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 (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 (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) signature :: group_type(state, ast.call_expr(tree, id).callee)
if (signature.function == ast.NodeId(maxval!(u32))) continue if (signature.function == ast.NodeId(maxval!(u32))) continue
count :: ast.function_param_count(tree, signature.function) 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 @hide:file
context_demand proc(state @mut State, id ast.NodeId, expected Type, code diagnostics.Code, early bool) void ! (mem.AllocError | diagnostics.CapacityError) { 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) 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) left :: bound_family(state, view.left)
right :: bound_family(state, view.right) right :: bound_family(state, view.right)
if (left == .float or right == .float) return .float if (left == .float or right == .float) return .float
if (left == .usize or right == .usize) return .usize if (integer_kind(left)) return left
if (left == .int or right == .int) return .int if (integer_kind(right)) return right
return .none return .none
} }
@@ -1480,7 +1567,7 @@ prepare_ranges proc(state @mut State, assignment_values, constrain bool) void !
view :: ast.range_expr(tree, id) view :: ast.range_expr(tree, id)
left :: bound_family(state, view.start) left :: bound_family(state, view.start)
right :: bound_family(state, view.end) 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) { if (constrain) {
try range_bound_context(state, view.start, family) try range_bound_context(state, view.start, family)
try range_bound_context(state, view.end, family) try range_bound_context(state, view.end, family)
+11
View File
@@ -11,6 +11,8 @@ BuiltinType :: enum {
bool, string, bool, string,
void, i64, void, i64,
f64, usize, f64, usize,
i8, i16, i32, isize,
u8, u16, u32, u64, uint,
} }
@hide:file @hide:file
@@ -29,6 +31,15 @@ builtins [_]Builtin :: [
{ name = "i64", type = .i64 }, { name = "i64", type = .i64 },
{ name = "f64", type = .f64 }, { name = "f64", type = .f64 },
{ name = "usize", type = .usize }, { 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 @hide:file
+117 -3
View File
@@ -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): def run(*args, env=None):
return subprocess.run(args, cwd=ROOT, capture_output=True, timeout=30, env=env) 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: if expected is not None:
stdout, error = expected stdout, error = expected
assert interpreted.stdout == stdout, (source.name, outcome(interpreted), stdout) assert interpreted.stdout == stdout, (source.name, outcome(interpreted), stdout)
assert interpreted.returncode == (1 if error else 0), outcome(interpreted) assert interpreted.returncode == (1 if error is not None else 0), outcome(interpreted)
if error: if error is not None:
assert error in interpreted.stderr, outcome(interpreted) assert interpreted.stderr and error in interpreted.stderr, outcome(interpreted)
for optimization in ("-O0", "-O2"): for optimization in ("-O0", "-O2"):
built = run(str(HONEY), "build", str(source), "-o", str(executable), optimization) built = run(str(HONEY), "build", str(source), "-o", str(executable), optimization)
assert built.returncode in (0, 1), (source.name, optimization, outcome(built)) assert built.returncode in (0, 1), (source.name, optimization, outcome(built))