feat: evaluate compile-time aggregates and explicit expressions

This commit is contained in:
2026-10-26 15:43:28 +01:00
parent ecd20675ea
commit 4de7c8bfc3
11 changed files with 518 additions and 45 deletions
+3 -1
View File
@@ -98,7 +98,9 @@ main proc() i16 {
the specialization checkpoint supports type arguments and constant integer, boolean, string, callback and null value arguments; declared value-parameter types survive specialization, including narrow checked arithmetic and optional captures/unwrap. integer arguments are range-checked even when unused, and erased argument expressions still receive ordinary checking. immutable constant expressions may use unary signs, `!`, `+`, `-`, `*`, comparisons and logical operators. type-producing procedures require compile-time parameters (or no parameters) and support immutable locals, conditional returns, nested type-producing calls and generated record/array storage, including pointer-recursive records. runaway specialization is diagnosed at 128 nested evaluation/specialization calls or 4096 instances. the specialization checkpoint supports type arguments and constant integer, boolean, string, callback and null value arguments; declared value-parameter types survive specialization, including narrow checked arithmetic and optional captures/unwrap. integer arguments are range-checked even when unused, and erased argument expressions still receive ordinary checking. immutable constant expressions may use unary signs, `!`, `+`, `-`, `*`, comparisons and logical operators. type-producing procedures require compile-time parameters (or no parameters) and support immutable locals, conditional returns, nested type-producing calls and generated record/array storage, including pointer-recursive records. runaway specialization is diagnosed at 128 nested evaluation/specialization calls or 4096 instances.
ordinary procedures may compute integer, boolean and string compile-time arguments from constant inputs, such as `Fixed(i16, length(2))`. calls reuse the ordinary checker, typed IR and interpreter, preserving local type inference, checked arithmetic, mutable locals, range/while loops, break/continue and recursive or specialized helper calls. each evaluation is isolated and limited to 100,000 IR instructions/terminators and 128 active frames; failures retain their originating source locations. runtime parameters and mutable global dependencies cannot supply compile-time inputs, and executed foreign calls remain interpreter traps. aggregate/type-valued execution, explicit `$()` expressions, reflection, `inline` expansion and existing generic-library acceptance remain outside this checkpoint. ordinary procedures may compute integer, boolean, float and string compile-time arguments and return nested records/tuples and fixed arrays from constant inputs, such as `Fixed(i16, length(2))`. calls reuse the ordinary checker, typed IR and interpreter, preserving local type inference, checked arithmetic, mutable locals, range/while loops, break/continue and recursive or specialized helper calls. aggregate arguments receive their declared context, including dependent `[N]T` parameters; cached specializations compare normalized values, not literal spelling or evaluation identity. returned aggregates become owned source literals before temporary interpreter storage is released, preserving by-value copies, exact integer types, string bytes, enum fields, optional fields and IEEE float bits.
`$(expression)` explicitly evaluates a constant expression, including an ordinary procedure call, even in a package without templates. use a typed expression when its shape cannot be inferred independently, such as `$(Point { x = 40 })`; integer literals retain contextual typing. the resulting value works in runtime expressions, bindings and array lengths, while the erased expression still receives ordinary checking. each interpreter evaluation is isolated and limited to 100,000 IR instructions/terminators and 128 active frames; failures retain their originating source locations. runtime parameters and mutable local/global dependencies cannot supply compile-time inputs. data pointers, slices, callbacks, unions and fallible results cannot escape temporary evaluation as generated constants; records/arrays containing these unsupported values are diagnosed rather than retaining interpreter handles. executed foreign calls remain interpreter traps. general type-valued execution, compile-time branch/`inline` expansion, reflection, generated type/default arrays and existing generic-library acceptance remain open.
callback signatures use `@proc(value i16) i16`, including grouped parameter names and trailing commas. compatible named functions can initialize callbacks, which may be passed, returned, replaced and stored in records/arrays; `Callback :: alias @proc(value i16) i16` names a reusable signature. parameter/result types match structurally, retaining nominal record identity and exact pointer mutability. callback fields call directly, such as `allocator.vtable.alloc(context, size, alignment)` once the remaining argument types are supported. `@mut proc` and `*proc` are invalid; `@mut Callback` instead points to writable callback storage. callback signatures use `@proc(value i16) i16`, including grouped parameter names and trailing commas. compatible named functions can initialize callbacks, which may be passed, returned, replaced and stored in records/arrays; `Callback :: alias @proc(value i16) i16` names a reusable signature. parameter/result types match structurally, retaining nominal record identity and exact pointer mutability. callback fields call directly, such as `allocator.vtable.alloc(context, size, alignment)` once the remaining argument types are supported. `@mut proc` and `*proc` are invalid; `@mut Callback` instead points to writable callback storage.
+6 -4
View File
@@ -19,9 +19,10 @@ explicit fallible channels, composed error sets, `try`/`catch` and scoped LIFO
traps or compiler infrastructure failures. traps or compiler infrastructure failures.
cached generic specialization and type-producing procedures support explicit/inferred cached generic specialization and type-producing procedures support explicit/inferred
compile-time arguments and generated record/array storage through both paths. compile-time arguments and generated record/array storage through both paths.
ordinary procedures can compute integer/boolean/string compile-time arguments using ordinary procedures can compute scalar and nested record/array compile-time values
mutable locals, loops and recursive calls; general aggregate/type-valued execution, using mutable locals, loops and recursive calls; explicit `$()` expressions use
reflection and generic library acceptance remain open. the same constant evaluation path. general type-valued execution, reflection and
generic library acceptance remain open.
the driver retains frontend storage and source provenance through the driver retains frontend storage and source provenance through
`@source/compilation`, with compilation-wide AST node identities. `@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
@@ -60,7 +61,8 @@ test discovery and FFI remain outside these completed slices.
- [ ] 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.
- [x] specialization checkpoint: explicit/inferred type and integer/boolean/string/callback/null value parameters, cached concrete runtime calls, nominal type-producing procedures, generated record/array storage and recursive data pointers through interpreter/native execution. preserve declared value types, source diagnostics and repeatable compilation; runtime values cannot supply compile-time arguments. - [x] specialization checkpoint: explicit/inferred type and integer/boolean/string/callback/null value parameters, cached concrete runtime calls, nominal type-producing procedures, generated record/array storage and recursive data pointers through interpreter/native execution. preserve declared value types, source diagnostics and repeatable compilation; runtime values cannot supply compile-time arguments.
- [x] compile-time execution checkpoint: ordinary procedure calls with constant inputs and integer/boolean/string results reuse checking, typed IR and bounded interpreter execution. mutable scalar locals, range/while loops, break/continue, nested/generic/recursive calls, exact numeric types, runtime traps and dependency-file provenance survive argument erasure through interpreter/native execution. runtime parameters and mutable global dependencies are rejected; recompilation retains no prior execution state. - [x] compile-time execution checkpoint: ordinary procedure calls with constant inputs and integer/boolean/string results reuse checking, typed IR and bounded interpreter execution. mutable scalar locals, range/while loops, break/continue, nested/generic/recursive calls, exact numeric types, runtime traps and dependency-file provenance survive argument erasure through interpreter/native execution. runtime parameters and mutable global dependencies are rejected; recompilation retains no prior execution state.
- [ ] remaining gate: general aggregate/type-valued compile-time execution, explicit compile-time expressions, compile-time branch/inline expansion and reflection (`typeinfo!`, `field!`, `struct_type!`, generated type/default arrays), plus existing-library acceptance. required collection/index iteration, expression-valued control flow, compound updates, formatting parsing and imported C-binding support may interleave with later gates. - [x] aggregate/explicit-expression checkpoint: computed nested records/tuples and fixed arrays, contextual/dependent aggregate value parameters, normalized specialization caching and explicit `$()` expressions through interpreter/native execution. preserve by-value isolation, zero-cell shapes, integer widths, string ownership, enum/optional fields and exact IEEE float bits; reject runtime dependencies and values that would retain temporary interpreter storage. repeatable compilation restores erased expressions.
- [ ] remaining gate: general type-valued compile-time execution, compile-time branch/inline expansion and reflection (`typeinfo!`, `field!`, `struct_type!`, generated type/default arrays), broader constant materialization (including unions, fallible results and storage-bearing values), plus existing-library acceptance. required collection/index iteration, expression-valued control flow, compound updates, formatting parsing and imported C-binding support may interleave with later gates.
- [ ] remaining source constructs: collection/index iteration, expression-valued control flow and `yield`, compound updates and multiline strings. examples: `source/parser/parse.hon`, `source/llvm/runtime.hon`. - [ ] remaining source constructs: collection/index iteration, expression-valued control flow and `yield`, compound updates and multiline strings. examples: `source/parser/parse.hon`, `source/llvm/runtime.hon`.
- [ ] standard-library milestone: compile/run a multi-package program exercising existing allocation and IO, including error cleanup; no replacement miniature standard library. - [ ] standard-library milestone: compile/run a multi-package program exercising existing allocation and IO, including error cleanup; no replacement miniature standard library.
- [ ] native driver integration: required C ABI declarations, `process.Init` startup arguments/IO, actual CLI exit statuses, file loading, temporary files and spawning Clang. Honey-generated programs currently accept only parameterless `main` and print its result; Bro's bootstrap startup support is not Honey backend support. - [ ] native driver integration: required C ABI declarations, `process.Init` startup arguments/IO, actual CLI exit statuses, file loading, temporary files and spawning Clang. Honey-generated programs currently accept only parameterless `main` and print its result; Bro's bootstrap startup support is not Honey backend support.
+40 -2
View File
@@ -6,6 +6,7 @@
#! * expr ..... -> identifier | literal | type | '.' member | '[' [expr {',' expr} [',']] ']' | [type] '{' [initializer {',' initializer} [',']] '}' | '(' expr ')' | ('-' | '!' | '&' | 'try') expr | expr ('^' | '?') | expr op expr | expr '(' [expr {',' expr} [',']] ')' | identifier '!' '(' [expr {',' expr} [',']] ')' | expr '[' expr ']' | expr '[' [expr] '..' [expr] [';' expr] ']' | expr '.' (member | int) | expr ('..' | '..=') expr | 'match' expr '{' match_arms '}' | expr 'catch' ['|' identifier '|'] (expr | block) #! * expr ..... -> identifier | literal | type | '.' member | '[' [expr {',' expr} [',']] ']' | [type] '{' [initializer {',' initializer} [',']] '}' | '(' expr ')' | ('-' | '!' | '&' | 'try') expr | expr ('^' | '?') | expr op expr | expr '(' [expr {',' expr} [',']] ')' | identifier '!' '(' [expr {',' expr} [',']] ')' | expr '[' expr ']' | expr '[' [expr] '..' [expr] [';' expr] ']' | expr '.' (member | int) | expr ('..' | '..=') expr | 'match' expr '{' match_arms '}' | expr 'catch' ['|' identifier '|'] (expr | block)
#! * decl ..... -> identifier [type] binding expr | identifier 'c_func' '(' parameters ')' type | ['test'] [identifier '::'] 'import' string #! * decl ..... -> identifier [type] binding expr | identifier 'c_func' '(' parameters ')' type | ['test'] [identifier '::'] 'import' string
#! * parameters -> [['$'] identifier {',' ['$'] identifier} type {',' ['$'] identifier {',' ['$'] identifier} type} [',']]; '$' marks each compile-time parameter independently. #! * parameters -> [['$'] identifier {',' ['$'] identifier} type {',' ['$'] identifier {',' ['$'] identifier} type} [',']]; '$' marks each compile-time parameter independently.
#! '$(' expr ')' is an explicit compile-time expression with primary-expression precedence.
#! * block .... -> '{' {stmt} '}' #! * block .... -> '{' {stmt} '}'
#! * arm ...... -> block | stmt (single statements become canonical blocks) #! * arm ...... -> block | stmt (single statements become canonical blocks)
#! * match_arm -> (expr {',' expr} ['|' identifier '|'] | 'else') ':' (block | stmt | expr); arms are comma/newline separated. #! * match_arm -> (expr {',' expr} ['|' identifier '|'] | 'else') ':' (block | stmt | expr); arms are comma/newline separated.
@@ -69,6 +70,7 @@ ExprKind :: enum {
try_expr try_expr
catch_expr catch_expr
typed_constant typed_constant
comptime_expr
} }
DeclKind :: enum { DeclKind :: enum {
@@ -172,6 +174,8 @@ NodeKind :: enum {
stmt_errdefer stmt_errdefer
stmt_block stmt_block
expr_typed_constant expr_typed_constant
expr_comptime
literal_float_value
} }
@hide:file @hide:file
@@ -540,6 +544,23 @@ add_typed_constant proc(
}, at) }, at)
} }
add_comptime_expr proc(builder @mut Builder, at Span, operand NodeId) NodeId ! mem.AllocError {
return try add_node(builder, Node{
kind = .expr_comptime,
main = NodeData{ node_id = operand },
}, at)
}
#! retains the exact IEEE representation, including signed zero and NaN payloads.
add_float proc(builder @mut Builder, at Span, value f64) NodeId ! mem.AllocError {
bits :: ptrcast!(@u64, &value)^
return try add_node(builder, Node{
kind = .literal_float_value,
data0 = NodeData{ extra = u32(bits & 4294967295) },
data1 = NodeData{ extra = u32(bits >> 32) },
}, at)
}
add_binary_type proc( add_binary_type proc(
builder @mut Builder, span Span, left, right NodeId, fallible bool, builder @mut Builder, span Span, left, right NodeId, fallible bool,
) NodeId ! mem.AllocError { ) NodeId ! mem.AllocError {
@@ -1385,6 +1406,8 @@ expression_kind proc(kind NodeKind) ExprKind {
.expr_try: .try_expr .expr_try: .try_expr
.expr_catch: .catch_expr .expr_catch: .catch_expr
.expr_typed_constant: .typed_constant .expr_typed_constant: .typed_constant
.expr_comptime: .comptime_expr
.literal_float_value: .literal_float
else: unreachable else: unreachable
} }
} }
@@ -1410,6 +1433,20 @@ binary_expr proc(tree @Tree, id NodeId) BinaryExprView {
} }
} }
comptime_operand proc(tree @Tree, id NodeId) NodeId {
node :: get_node(&tree.storage, id)
debug.assert(node.kind == .expr_comptime)
return node.main.node_id
}
#! generated IEEE literals have no textual spelling; parsed float literals return null.
float_value proc(tree @Tree, id NodeId) ?f64 {
node :: get_node(&tree.storage, id)
if (node.kind != .literal_float_value) return null
bits :: u64(node.data0.extra) | (u64(node.data1.extra) << 32)
return ptrcast!(@f64, &bits)^
}
typed_constant proc(tree @Tree, id NodeId) TypedConstantView { typed_constant proc(tree @Tree, id NodeId) TypedConstantView {
node :: get_node(&tree.storage, id) node :: get_node(&tree.storage, id)
debug.assert(node.kind == .expr_typed_constant) debug.assert(node.kind == .expr_typed_constant)
@@ -1765,7 +1802,7 @@ NodeLayout :: struct {
node_layout proc(node Node) NodeLayout { node_layout proc(node Node) NodeLayout {
layout := NodeLayout{} layout := NodeLayout{}
match node.kind { match node.kind {
.literal_int, .literal_float, .literal_string, .literal_bool, .literal_null, .literal_int, .literal_float, .literal_float_value, .literal_string, .literal_bool, .literal_null,
.expr_identifier, .invalid_expr: {} .expr_identifier, .invalid_expr: {}
.expr_unary: layout.direct_children = 2 .expr_unary: layout.direct_children = 2
.expr_binary, .expr_range, .expr_index, .expr_selection, .expr_typed_constant, .expr_binary, .expr_range, .expr_index, .expr_selection, .expr_typed_constant,
@@ -1773,7 +1810,7 @@ node_layout proc(node Node) NodeLayout {
layout.direct_children = 6 layout.direct_children = 6
.expr_pointer_type: layout.direct_children = 4 .expr_pointer_type: layout.direct_children = 4
.expr_optional_type, .expr_distinct_type, .expr_alias, .expr_enum_literal, .expr_optional_type, .expr_distinct_type, .expr_alias, .expr_enum_literal,
.expr_try: layout.direct_children = 1 .expr_try, .expr_comptime: layout.direct_children = 1
.expr_call, .expr_builtin_call, .expr_function_type, .expr_array_literal, .expr_call, .expr_builtin_call, .expr_function_type, .expr_array_literal,
.expr_record_type, .expr_record_literal, .expr_enum_type, .expr_match, .expr_record_type, .expr_record_literal, .expr_enum_type, .expr_match,
.block, .package: { .block, .package: {
@@ -2089,6 +2126,7 @@ add_owned_literal proc(
} }
#! borrows a literal's spelling, independently of its diagnostic source span. #! borrows a literal's spelling, independently of its diagnostic source span.
#! query float_value first for generated IEEE literals.
literal_text proc(tree @Tree, id NodeId) []u8 { literal_text proc(tree @Tree, id NodeId) []u8 {
node :: get_node(&tree.storage, id) node :: get_node(&tree.storage, id)
if (node.data0.extra == 1) return tree.storage.generated.items[usize(node.main.extra)] if (node.data0.extra == 1) return tree.storage.generated.items[usize(node.main.extra)]
+4 -2
View File
@@ -85,11 +85,13 @@ recompiles_specialized_calls_without_borrowing_previous_ir test {
` return result ` return result
`} `}
`Fixed proc($N usize) type { return struct { values [N]i16 } } `Fixed proc($N usize) type { return struct { values [N]i16 } }
`data proc() [2]i16 { return [40, 2] }
`main proc() i16 { `main proc() i16 {
` box Box(i16) :: { value = identity(i16, 40) } ` values :: $(data())
` box Box(i16) :: { value = identity(i16, values[0]) }
` fixed Fixed(count()) :: { values = [box.value, 2] } ` fixed Fixed(count()) :: { values = [box.value, 2] }
` same Fixed(2) :: fixed ` same Fixed(2) :: fixed
` return identity(same.values[0]) + same.values[1] ` return identity(same.values[0]) + same.values[$(usize(1))]
`} `}
, ,
) )
+2
View File
@@ -112,6 +112,7 @@ Code :: enum {
specialization_cycle specialization_cycle
specialization_limit specialization_limit
comptime_execution_limit comptime_execution_limit
comptime_value_escape
} }
Diagnostic :: struct { Diagnostic :: struct {
@@ -245,5 +246,6 @@ messages :: $enummap.init(Code, []u8, {
specialization_cycle = "compile-time specialization depends on its unfinished result", specialization_cycle = "compile-time specialization depends on its unfinished result",
specialization_limit = "compile-time specialization exceeds its recursion or instance limit", specialization_limit = "compile-time specialization exceeds its recursion or instance limit",
comptime_execution_limit = "compile-time execution exceeds its recursion or step limit", comptime_execution_limit = "compile-time execution exceeds its recursion or step limit",
comptime_value_escape = "compile-time result cannot escape evaluation as a source value",
}) })
+9
View File
@@ -1795,6 +1795,9 @@ lower_expression proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocEr
} }
if (typecheck.is_union_variant(state.checked, id)) return try lower_union(state, id) if (typecheck.is_union_variant(state.checked, id)) return try lower_union(state, id)
match ast.expr_kind(tree, id) { match ast.expr_kind(tree, id) {
.comptime_expr: {
if (!trap_type(state, id)) unreachable
}
.typed_constant: { .typed_constant: {
if (trap_type(state, id)) return ir.no_value if (trap_type(state, id)) return ir.no_value
return try lower_as(state, ast.typed_constant(tree, id).value, typecheck.type_of(state.checked, id)) return try lower_as(state, ast.typed_constant(tree, id).value, typecheck.type_of(state.checked, id))
@@ -2130,6 +2133,12 @@ exact_float_integer proc(text []u8, value f64) bool {
@hide:file @hide:file
lower_number proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) { lower_number proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) {
span :: ast.node_span(state.tree, id) span :: ast.node_span(state.tree, id)
if (ast.float_value(state.tree, id)) |value| {
return try constant(state, ir.Value{
type = .float,
data = ir.ValueData{ real = value },
}, span)
}
text :: ast.literal_text(state.tree, id) text :: ast.literal_text(state.tree, id)
type :: lower_type(state, typecheck.type_of(state.checked, id)) type :: lower_type(state, typecheck.type_of(state.checked, id))
if (ir.integer_bits(type) != 0 and !ir.unsigned_integer(type)) { if (ir.integer_bits(type) != 0 and !ir.unsigned_integer(type)) {
+15
View File
@@ -1606,6 +1606,21 @@ parse_primary proc(state @mut State) ast.NodeId ! ParseError {
.union: return try parse_union_type(state) .union: return try parse_union_type(state)
.distinct, .alias: return try parse_type_wrapper(state) .distinct, .alias: return try parse_type_wrapper(state)
.match: return try parse_match(state) .match: return try parse_match(state)
.dollar: {
state.next_token += 1
_ = try consume(state, .open_paren)
previous :: state.allow_record_literals
state.allow_record_literals = true
defer state.allow_record_literals = previous
state.paren_depth += 1
defer state.paren_depth -= 1
expression :: try parse_expr(state)
skip_operand_newlines(state)
close :: try consume(state, .close_paren)
return try ast.add_comptime_expr(state.builder, ast.Span{
start = token.span.start, end = close.span.end, source = token.span.source,
}, expression)
}
.dot: { .dot: {
state.next_token += 1 state.next_token += 1
member :: try consume_member(state, false) member :: try consume_member(state, false)
+2
View File
@@ -1144,6 +1144,7 @@ collect_expression proc(state @mut State, id ast.NodeId) void ! (mem.AllocError
state.cells[usize(id)].in_cleanup = state.in_cleanup state.cells[usize(id)].in_cleanup = state.in_cleanup
state.cells[usize(id)].narrowing_arm = state.narrowing_arm state.cells[usize(id)].narrowing_arm = state.narrowing_arm
match ast.expr_kind(tree, id) { match ast.expr_kind(tree, id) {
.comptime_expr: try fail(state, id, .comptime_argument_required, ast.node_span(tree, id))
.typed_constant: { .typed_constant: {
value :: ast.typed_constant(tree, id) value :: ast.typed_constant(tree, id)
state.cells[usize(id)].fixed = true state.cells[usize(id)].fixed = true
@@ -3978,6 +3979,7 @@ constant_order proc(left, right ConstantInteger) i32 {
@hide:file @hide:file
constant_real proc(state @mut State, id ast.NodeId) ?f64 ! mem.AllocError { constant_real proc(state @mut State, id ast.NodeId) ?f64 ! mem.AllocError {
if (ast.float_value(state.tree, id)) |value| return value
missing ?f64 :: null missing ?f64 :: null
if (ast.expr_kind(state.tree, id) == .literal_float) { if (ast.expr_kind(state.tree, id) == .literal_float) {
return strconv.parse_float(state.edges.allocator, ast.literal_text(state.tree, id)) catch |err| { return strconv.parse_float(state.edges.allocator, ast.literal_text(state.tree, id)) catch |err| {
+1
View File
@@ -526,6 +526,7 @@ resolve_expr proc(
if (state.bindings[usize(id)].kind != .none) return if (state.bindings[usize(id)].kind != .none) return
tree :: state.tree tree :: state.tree
match ast.expr_kind(tree, id) { match ast.expr_kind(tree, id) {
.comptime_expr: try resolve_expr(state, scope, ast.comptime_operand(tree, id))
.typed_constant: { .typed_constant: {
value :: ast.typed_constant(tree, id) value :: ast.typed_constant(tree, id)
try resolve_expr(state, scope, value.type) try resolve_expr(state, scope, value.type)
+290 -35
View File
@@ -26,6 +26,8 @@ ValueKind :: enum {
string string
function function
null null
real
aggregate
} }
@hide:file @hide:file
@@ -101,19 +103,21 @@ elaborate proc(
) Result ! Error { ) Result ! Error {
result := Result{ builder = builder, changes = arraylist.init(allocator), constraints = arraylist.init(allocator) } result := Result{ builder = builder, changes = arraylist.init(allocator), constraints = arraylist.init(allocator) }
errdefer deinit(&result) errdefer deinit(&result)
# Explicit evaluation is useful even in packages without generic declarations.
original :: ast.view(builder, root) original :: ast.view(builder, root)
generic := false needed := false
for (0..ast.package_decl_count(&original)) |i| { for (0..ast.node_count(&original)) |i| {
id :: ast.package_decl(&original, i) id :: ast.NodeId(i)
if (ast.decl_kind(&original, id) != .function) continue class :: ast.node_class(&original, id)
if (class == .expression and ast.expr_kind(&original, id) == .comptime_expr) needed = true
if (class == .function and ast.function_generic(&original, id)) needed = true
if (class == .function) {
result_type :: sema.binding(resolution, ast.function_decl(&original, id).return_type) result_type :: sema.binding(resolution, ast.function_decl(&original, id).return_type)
if (ast.function_generic(&original, id) if (result_type.kind == .builtin and result_type.data.builtin == .type) needed = true
or (result_type.kind == .builtin and result_type.data.builtin == .type)) {
generic = true
break
} }
if (needed) break
} }
if (!generic) return result if (!needed) return result
mapping :: try mem.alloc(ast.NodeId, allocator, ast.node_count(&original)) mapping :: try mem.alloc(ast.NodeId, allocator, ast.node_count(&original))
defer mem.free(allocator, mapping) defer mem.free(allocator, mapping)
state := State{ state := State{
@@ -185,6 +189,26 @@ expand proc(state @mut State, id ast.NodeId) void ! Error {
if (state.expanded.items[usize(id)] != 0) return if (state.expanded.items[usize(id)] != 0) return
state.expanded.items[usize(id)] = 1 state.expanded.items[usize(id)] = 1
class :: ast.node_class(&state.tree, id) class :: ast.node_class(&state.tree, id)
if (class == .expression and ast.expr_kind(&state.tree, id) == .comptime_expr) {
operand :: ast.comptime_operand(&state.tree, id)
try arraylist.append(&state.rewrites.constraints, operand)
state.evaluation_diagnostic = diagnostics.no_diagnostic
value :: try evaluate(state, operand)
if (value.kind == .invalid) {
try reject_evaluation(state, id)
return
}
concrete :: try materialize(state, value)
replacement := concrete.node
if (value.annotation != no_node and value.kind != .type) {
replacement = try ast.add_typed_constant(state.builder, ast.node_span(&state.tree, id),
value.annotation, replacement)
try refresh(state)
}
try replace(state, id, replacement)
try refresh(state)
return
}
if (class == .expression and ast.expr_kind(&state.tree, id) == .call) { if (class == .expression and ast.expr_kind(&state.tree, id) == .call) {
callee :: ast.call_expr(&state.tree, id).callee callee :: ast.call_expr(&state.tree, id).callee
try expand(state, callee) try expand(state, callee)
@@ -277,7 +301,10 @@ specialize_call proc(state @mut State, call, function ast.NodeId) void ! Error {
try expand(state, argument) try expand(state, argument)
if (view.comptime) { if (view.comptime) {
state.evaluation_diagnostic = diagnostics.no_diagnostic state.evaluation_diagnostic = diagnostics.no_diagnostic
arguments[i] = try evaluate(state, argument) annotation := view.type
if (contextual_literal(state, argument))
annotation = try argument_annotation(state, function, view.type, arguments)
arguments[i] = try evaluate_expected(state, argument, annotation)
if (arguments[i].kind == .invalid) { if (arguments[i].kind == .invalid) {
try reject_evaluation(state, call) try reject_evaluation(state, call)
return return
@@ -527,9 +554,10 @@ evaluation_dependencies proc(
resolved :: binding(state, id) resolved :: binding(state, id)
if (resolved.kind == .declaration) { if (resolved.kind == .declaration) {
target :: resolved.data.declaration target :: resolved.data.declaration
if ((flags[usize(target)] & 1) != 0 and (flags[usize(target)] & 2) == 0) { if ((flags[usize(target)] & 8) == 0 and (flags[usize(target)] & 2) == 0) {
if (ast.node_class(&state.tree, target) == .statement class :: ast.node_class(&state.tree, target)
and ast.statement_decl(&state.tree, target).mutability == .variable) return false if (class == .parameter) return false
if (class == .statement and ast.statement_decl(&state.tree, target).mutability == .variable) return false
flags[usize(target)] = flags[usize(target)] | 2 flags[usize(target)] = flags[usize(target)] | 2
try arraylist.append(declarations, target) try arraylist.append(declarations, target)
} }
@@ -540,20 +568,212 @@ evaluation_dependencies proc(
return true return true
} }
@hide:file
evaluation_owned proc(state @State, id ast.NodeId, flags []mut u8) void {
if ((flags[usize(id)] & 8) != 0) return
flags[usize(id)] = flags[usize(id)] | 8
for (0..ast.child_count(&state.tree, id)) |i| evaluation_owned(state, ast.child(&state.tree, id, i), flags)
}
@hide:file
contextual_literal proc(state @State, id ast.NodeId) bool {
kind :: ast.expr_kind(&state.tree, id)
return kind == .record_literal or kind == .array_literal or kind == .enum_literal
}
@hide:file
argument_annotation proc(
state @mut State, function, annotation ast.NodeId, arguments []Value,
) ast.NodeId ! Error {
mapping :: try mem.alloc(ast.NodeId, state.allocator, state.original_count)
defer mem.free(state.allocator, mapping)
mem.set(mapping, no_node)
for (0..ast.function_param_count(&state.tree, function)) |i| {
if (arguments[i].kind == .invalid) continue
parameter :: ast.function_param(&state.tree, function, i)
value :: try materialize(state, arguments[i])
for (0..mapping.len) |n| {
resolved :: binding(state, ast.NodeId(n))
if (resolved.kind == .declaration and resolved.data.declaration == parameter
and ast.node_class(&state.tree, ast.NodeId(n)) == .expression) mapping[n] = value.node
}
}
start :: ast.node_count(&state.tree)
result :: try ast.clone_subtree(state.builder, annotation, mapping)
try refresh(state)
for (0..mapping.len) |i| {
target :: mapping[i]
if (target != no_node and usize(target) >= start) state.bindings.items[usize(target)] = state.bindings.items[i]
}
return result
}
@hide:file
evaluate_expected proc(state @mut State, id, annotation ast.NodeId) Value ! Error {
if (contextual_literal(state, id)) {
type :: try canonical_type(state, annotation)
if (type != no_node) return try evaluate_ir(state, id, type)
}
return try evaluate(state, id)
}
@hide:file
aggregate_child proc(module @ir.Module, parent ir.Value, type ir.Type, shape ir.ArrayId, offset usize) ir.Value {
if (!ir.aggregate(type)) return parent.data.elements[offset]
cells :: module.arrays.items[usize(shape)].cells
return ir.Value{ type = type, array_type = shape,
data = ir.ValueData{ elements = parent.data.elements[offset..offset + cells] } }
}
# Values leaving the temporary interpreter must own no interpreter addresses or module IDs.
# Rebuild contextual source literals while the module and its escaped flat cells are alive.
@hide:file
serialize_value proc(
state @mut State, module @ir.Module, checked @typecheck.Result,
type typecheck.Type, value ir.Value, span ast.Span,
) ast.NodeId ! Error {
result := no_node
if (type.kind == .array) {
array :: typecheck.array_type(checked, type.array_type)
shape :: module.arrays.items[usize(value.array_type)]
stride :: if (ir.aggregate(shape.element)) module.arrays.items[usize(shape.element_array)].cells else usize(1)
elements := arraylist.init(ast.NodeId, state.allocator)
defer arraylist.deinit(&elements)
for (0..array.len) |i| {
child :: aggregate_child(module, value, shape.element, shape.element_array, i * stride)
node :: try serialize_value(state, module, checked, array.element, child, span)
if (node == no_node) return no_node
try arraylist.append(&elements, node)
}
result = try ast.add_array_literal(state.builder, span, elements.items)
} else if (type.kind == .record) {
record :: typecheck.record_type(checked, type.record_type)
if (record.is_union) return no_node
shape :: module.arrays.items[usize(value.array_type)]
fields := arraylist.init(ast.NodeId, state.allocator)
defer arraylist.deinit(&fields)
for (record.fields) |field, i| {
layout :: shape.fields[i]
child :: aggregate_child(module, value, layout.type, layout.array_type, layout.offset)
node :: try serialize_value(state, module, checked, field.type, child, span)
if (node == no_node) return no_node
generated :: try ast.add_field(state.builder, span, field.name, null, node)
try refresh(state)
try arraylist.append(&fields, generated)
}
result = try ast.add_record_literal(state.builder, span, null, fields.items)
} else if (type.kind == .enumeration) {
enumeration :: typecheck.enum_type(checked, type.enum_type)
negative :: !ir.unsigned_integer(value.type) and value.data.integer < 0
magnitude :: if (negative) u64(-(value.data.integer + 1)) + 1 else value.data.unsigned
for (enumeration.members) |member| {
if (member.magnitude == magnitude and member.negative == negative) {
result = try ast.add_enum_literal(state.builder, span, member.name)
break
}
}
} else if (type.kind == .optional) {
container :: typecheck.array_type(checked, type.array_type)
if (container.element.kind == .pointer or container.element.kind == .function) {
if (value.data.unsigned != 0) return no_node
result = try ast.add_literal(state.builder, .null, span)
} else if (!value.data.elements[0].data.boolean) {
result = try ast.add_literal(state.builder, .null, span)
} else {
shape :: module.arrays.items[usize(value.array_type)]
payload :: shape.fields[1]
child :: aggregate_child(module, value, payload.type, payload.array_type, payload.offset)
return try serialize_value(state, module, checked, container.element, child, span)
}
} else if (ir.integer_bits(value.type) != 0 and type.kind != .pointer) {
negative :: !ir.unsigned_integer(value.type) and value.data.integer < 0
magnitude :: if (negative) u64(-(value.data.integer + 1)) + 1 else value.data.unsigned
result = try ast.add_integer(state.builder, span, magnitude)
if (negative) result = try ast.add_unary(state.builder, span, .negate, result)
} else if (type.kind == .float) {
result = try ast.add_float(state.builder, span, value.data.real)
} else if (type.kind == .bool) {
result = try ast.add_boolean(state.builder, span, value.data.boolean)
} else if (type.kind == .string) {
result = try ast.add_string(state.builder, span, ir.string_bytes(module, value.data.string))
}
try refresh(state)
return result
}
@hide:file
constant_equal proc(state @mut State, left, right ast.NodeId) bool ! Error {
kind :: ast.expr_kind(&state.tree, left)
if (kind != ast.expr_kind(&state.tree, right)) return false
if (kind == .array_literal) {
count :: ast.array_element_count(&state.tree, left)
if (count != ast.array_element_count(&state.tree, right)) return false
for (0..count) |i| {
if (!try constant_equal(state, ast.array_element(&state.tree, left, i), ast.array_element(&state.tree, right, i))) return false
}
return true
}
if (kind == .record_literal) {
count :: ast.record_field_count(&state.tree, left)
if (count != ast.record_field_count(&state.tree, right)) return false
for (0..count) |i| {
a :: ast.field(&state.tree, ast.record_field(&state.tree, left, i))
b :: ast.field(&state.tree, ast.record_field(&state.tree, right, i))
if ((a.name == null) != (b.name == null)) return false
if (a.name != null and ast.name_id(&state.tree, a.name?) != ast.name_id(&state.tree, b.name?)) return false
if (!try constant_equal(state, a.value?, b.value?)) return false
}
return true
}
if (kind == .enum_literal)
return ast.name_id(&state.tree, ast.enum_literal(&state.tree, left)) == ast.name_id(&state.tree, ast.enum_literal(&state.tree, right))
return try value_equal(state, try evaluate(state, left), try evaluate(state, right))
}
@hide:file
constant_hash proc(state @mut State, id ast.NodeId) usize ! Error {
kind :: ast.expr_kind(&state.tree, id)
hash :: enum_index(kind)
if (kind == .array_literal) {
result := hash
for (0..ast.array_element_count(&state.tree, id)) |i|
result = mix(result, try constant_hash(state, ast.array_element(&state.tree, id, i)))
return result
}
if (kind == .record_literal) {
result := hash
for (0..ast.record_field_count(&state.tree, id)) |i| {
field :: ast.field(&state.tree, ast.record_field(&state.tree, id, i))
if (field.name) |name| result = mix(result, usize(ast.name_id(&state.tree, name)))
result = mix(result, try constant_hash(state, field.value?))
}
return result
}
if (kind == .enum_literal) return mix(hash, usize(ast.name_id(&state.tree, ast.enum_literal(&state.tree, id))))
return try value_hash(state, try evaluate(state, id))
}
@hide:file @hide:file
evaluate_call proc(state @mut State, call ast.NodeId) Value ! Error { evaluate_call proc(state @mut State, call ast.NodeId) Value ! Error {
callee :: try evaluate(state, ast.call_expr(&state.tree, call).callee) callee :: try evaluate(state, ast.call_expr(&state.tree, call).callee)
if (callee.kind != .function and callee.kind != .type) return Value{} if (callee.kind != .function and callee.kind != .type) return Value{}
result_type := callee.node result_type := callee.node
function := no_node
if (callee.kind == .function) { if (callee.kind == .function) {
resolved :: binding(state, callee.node) resolved :: binding(state, callee.node)
if (resolved.kind != .declaration) return Value{} if (resolved.kind != .declaration) return Value{}
function = resolved.data.declaration
result_type = ast.function_decl(&state.tree, resolved.data.declaration).return_type result_type = ast.function_decl(&state.tree, resolved.data.declaration).return_type
} }
arguments := arraylist.init(ast.NodeId, state.allocator) arguments := arraylist.init(ast.NodeId, state.allocator)
defer arraylist.deinit(&arguments) defer arraylist.deinit(&arguments)
for (0..ast.call_arg_count(&state.tree, call)) |i| { for (0..ast.call_arg_count(&state.tree, call)) |i| {
value :: try evaluate(state, ast.call_arg(&state.tree, call, i)) argument :: ast.call_arg(&state.tree, call, i)
value := Value{}
if (function != no_node and i < ast.function_param_count(&state.tree, function)) {
annotation :: ast.param(&state.tree, ast.function_param(&state.tree, function, i)).type
value = try evaluate_expected(state, argument, annotation)
} else value = try evaluate(state, argument)
if (value.kind == .invalid) return Value{} if (value.kind == .invalid) return Value{}
concrete := try materialize(state, value) concrete := try materialize(state, value)
if (value.annotation != no_node) { if (value.annotation != no_node) {
@@ -567,7 +787,15 @@ evaluate_call proc(state @mut State, call ast.NodeId) Value ! Error {
# inference must have exactly the same semantics as an ordinary runtime call. # inference must have exactly the same semantics as an ordinary runtime call.
span :: ast.node_span(&state.tree, call) span :: ast.node_span(&state.tree, call)
invocation :: try ast.add_call(state.builder, span, callee.node, arguments.items) invocation :: try ast.add_call(state.builder, span, callee.node, arguments.items)
returned :: try ast.add_return_stmt(state.builder, span, invocation) try refresh(state)
return try evaluate_ir(state, invocation, result_type)
}
@hide:file
evaluate_ir proc(state @mut State, expression, result_type ast.NodeId) Value ! Error {
if (result_type == no_node) return Value{}
span :: ast.node_span(&state.tree, expression)
returned :: try ast.add_return_stmt(state.builder, span, expression)
statements [1]ast.NodeId :: [returned] statements [1]ast.NodeId :: [returned]
body :: try ast.add_block(state.builder, span, statements[..]) body :: try ast.add_block(state.builder, span, statements[..])
wrapper :: try ast.add_function_decl(state.builder, span, span, strpool.no_str, result_type, &[], body) wrapper :: try ast.add_function_decl(state.builder, span, span, strpool.no_str, result_type, &[], body)
@@ -578,17 +806,12 @@ evaluate_call proc(state @mut State, call ast.NodeId) Value ! Error {
flags := arraylist.init(u8, state.allocator) flags := arraylist.init(u8, state.allocator)
defer arraylist.deinit(&flags) defer arraylist.deinit(&flags)
index := usize(0) index := usize(0)
registered := usize(0)
while (index < declarations.items.len) { while (index < declarations.items.len) {
declaration :: declarations.items[index] declaration :: declarations.items[index]
try expand(state, declaration) try expand(state, declaration)
while (flags.items.len < ast.node_count(&state.tree)) try arraylist.append(&flags, u8(0)) while (flags.items.len < ast.node_count(&state.tree)) try arraylist.append(&flags, u8(0))
while (registered < state.declarations.items.len) {
target :: state.declarations.items[registered]
flags.items[usize(target)] = flags.items[usize(target)] | 1
registered += 1
}
flags.items[usize(declaration)] = flags.items[usize(declaration)] | 2 flags.items[usize(declaration)] = flags.items[usize(declaration)] | 2
evaluation_owned(state, declaration, flags.items)
if (!try evaluation_dependencies(state, declaration, flags.items, &declarations)) return Value{} if (!try evaluation_dependencies(state, declaration, flags.items, &declarations)) return Value{}
index += 1 index += 1
} }
@@ -605,7 +828,11 @@ evaluate_call proc(state @mut State, call ast.NodeId) Value ! Error {
original_diagnostics :: diagnostics.count(&diagnostic_store) original_diagnostics :: diagnostics.count(&diagnostic_store)
checked := try typecheck.check(state.allocator, &tree, state.resolution, &diagnostic_store) checked := try typecheck.check(state.allocator, &tree, state.resolution, &diagnostic_store)
defer typecheck.deinit(&checked) defer typecheck.deinit(&checked)
if (diagnostics.count(&diagnostic_store) != original_diagnostics) return Value{} if (diagnostics.count(&diagnostic_store) != original_diagnostics) {
diagnostic :: diagnostic_store.list.items[original_diagnostics]
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, diagnostic.code, diagnostic.span)
return Value{}
}
module := try lower.lower_evaluation(state.allocator, &tree, state.resolution, &checked, wrapper, &diagnostic_store) module := try lower.lower_evaluation(state.allocator, &tree, state.resolution, &checked, wrapper, &diagnostic_store)
defer ir.deinit(&module) defer ir.deinit(&module)
outcome := try interpreter.run_evaluation(state.allocator, &module, &diagnostic_store) outcome := try interpreter.run_evaluation(state.allocator, &module, &diagnostic_store)
@@ -619,20 +846,17 @@ evaluate_call proc(state @mut State, call ast.NodeId) Value ! Error {
return Value{} return Value{}
} }
value :: outcome.value value :: outcome.value
if (ir.integer_bits(value.type) != 0) { source_type :: typecheck.type_of(&checked, expression)
negative :: !ir.unsigned_integer(value.type) and value.data.integer < 0 node :: try serialize_value(state, &module, &checked, source_type, value, span)
magnitude :: if (negative) u64(-(value.data.integer + 1)) + 1 else value.data.unsigned if (node == no_node) {
return Value{ kind = .integer, node = call, magnitude = magnitude, negative = negative, annotation = result_type } state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .comptime_value_escape, span)
}
if (value.type == .bool) return Value{
kind = .boolean, node = call, boolean = value.data.boolean, annotation = result_type,
}
if (value.type == .string) {
literal :: try ast.add_string(state.builder, span, ir.string_bytes(&module, value.data.string))
try refresh(state)
return Value{ kind = .string, node = literal, annotation = result_type }
}
return Value{} return Value{}
}
if (ir.aggregate(value.type) or source_type.kind == .enumeration)
return Value{ kind = .aggregate, node = node, annotation = result_type }
result := try evaluate(state, node)
result.annotation = result_type
return result
} }
@hide:file @hide:file
@@ -726,6 +950,12 @@ evaluate proc(state @mut State, id ast.NodeId) Value ! Error {
} }
return Value{ kind = .integer, node = id, magnitude = value } return Value{ kind = .integer, node = id, magnitude = value }
} }
.literal_float: {
if (ast.float_value(&state.tree, id)) |value| return Value{
kind = .real, node = id, magnitude = ptrcast!(@u64, &value)^,
}
return try evaluate_ir(state, id, try builtin(state, id, .float))
}
.literal_bool: return Value{ kind = .boolean, node = id, .literal_bool: return Value{ kind = .boolean, node = id,
boolean = mem.eql(ast.literal_text(&state.tree, id), "true") } boolean = mem.eql(ast.literal_text(&state.tree, id), "true") }
.literal_string: { .literal_string: {
@@ -749,6 +979,8 @@ evaluate proc(state @mut State, id ast.NodeId) Value ! Error {
value := try evaluate(state, view.operand) value := try evaluate(state, view.operand)
if (view.operator == .negate and value.kind == .integer) { if (view.operator == .negate and value.kind == .integer) {
value.negative = value.magnitude != 0 and !value.negative value.negative = value.magnitude != 0 and !value.negative
} else if (view.operator == .negate and value.kind == .real) {
value.magnitude = value.magnitude xor (u64(1) << 63)
} else if (view.operator == .logical_not and value.kind == .boolean) { } else if (view.operator == .logical_not and value.kind == .boolean) {
value.boolean = !value.boolean value.boolean = !value.boolean
} else return Value{} } else return Value{}
@@ -757,6 +989,8 @@ evaluate proc(state @mut State, id ast.NodeId) Value ! Error {
} }
.binary: return try evaluate_binary(state, id) .binary: return try evaluate_binary(state, id)
.call: return try evaluate_call(state, id) .call: return try evaluate_call(state, id)
.record_literal, .array_literal, .selection, .index, .match_expr, .builtin_call:
return try evaluate_ir(state, id, try expression_type(state, id, 0))
else: return Value{} else: return Value{}
} }
} }
@@ -764,6 +998,7 @@ evaluate proc(state @mut State, id ast.NodeId) Value ! Error {
@hide:file @hide:file
evaluate_binary proc(state @mut State, id ast.NodeId) Value ! Error { evaluate_binary proc(state @mut State, id ast.NodeId) Value ! Error {
view :: ast.binary_expr(&state.tree, id) view :: ast.binary_expr(&state.tree, id)
if (view.operator == .divide) return try evaluate_ir(state, id, try builtin(state, id, .float))
left :: try evaluate(state, view.left) left :: try evaluate(state, view.left)
if (left.kind == .invalid) return Value{} if (left.kind == .invalid) return Value{}
if (left.kind == .boolean) { if (left.kind == .boolean) {
@@ -772,6 +1007,15 @@ evaluate_binary proc(state @mut State, id ast.NodeId) Value ! Error {
} }
right := try evaluate(state, view.right) right := try evaluate(state, view.right)
if (left.kind == .invalid or right.kind == .invalid) return Value{} if (left.kind == .invalid or right.kind == .invalid) return Value{}
if (left.kind == .real or right.kind == .real) {
boolean :: (
view.operator == .equal or view.operator == .not_equal or view.operator == .less
or view.operator == .less_equal or view.operator == .greater or view.operator == .greater_equal
)
kind sema.BuiltinType :: if (boolean) .bool else .float
result_type :: try builtin(state, id, kind)
return try evaluate_ir(state, id, result_type)
}
if (left.annotation != no_node and right.annotation != no_node if (left.annotation != no_node and right.annotation != no_node
and left.kind == .integer and right.kind == .integer and left.kind == .integer and right.kind == .integer
and !try same_type(state, left.annotation, right.annotation)) return Value{} and !try same_type(state, left.annotation, right.annotation)) return Value{}
@@ -888,6 +1132,11 @@ value_equal proc(state @mut State, left, right Value) bool ! Error {
.integer: left.magnitude == right.magnitude and left.negative == right.negative .integer: left.magnitude == right.magnitude and left.negative == right.negative
.boolean: left.boolean == right.boolean .boolean: left.boolean == right.boolean
.function: binding(state, left.node).data.declaration == binding(state, right.node).data.declaration .function: binding(state, left.node).data.declaration == binding(state, right.node).data.declaration
.real: left.magnitude == right.magnitude
.aggregate: (
try same_type(state, left.annotation, right.annotation)
and try constant_equal(state, left.node, right.node)
)
.string: try same_string(state, left.node, right.node) .string: try same_string(state, left.node, right.node)
} }
} }
@@ -903,6 +1152,8 @@ value_hash proc(state @mut State, value Value) usize ! Error {
.boolean: mix(hash, boolean) .boolean: mix(hash, boolean)
.function: mix(hash, usize(binding(state, value.node).data.declaration)) .function: mix(hash, usize(binding(state, value.node).data.declaration))
.string: string_hash(hash, ast.literal_text(&state.tree, value.node)) .string: string_hash(hash, ast.literal_text(&state.tree, value.node))
.real: mix(hash, usize(value.magnitude))
.aggregate: mix(mix(hash, try type_hash(state, value.annotation)), try constant_hash(state, value.node))
.invalid, .null: hash .invalid, .null: hash
} }
} }
@@ -1216,6 +1467,8 @@ materialize proc(state @mut State, value Value) Value ! Error {
if (value.negative) result.node = try ast.add_unary(state.builder, span, .negate, result.node) if (value.negative) result.node = try ast.add_unary(state.builder, span, .negate, result.node)
} else if (value.kind == .boolean) { } else if (value.kind == .boolean) {
result.node = try ast.add_boolean(state.builder, span, value.boolean) result.node = try ast.add_boolean(state.builder, span, value.boolean)
} else if (value.kind == .real) {
result.node = try ast.add_float(state.builder, span, ptrcast!(@f64, &value.magnitude)^)
} }
try refresh(state) try refresh(state)
return result return result
@@ -1230,6 +1483,7 @@ valid_value proc(state @mut State, annotation ast.NodeId, value Value) bool ! Er
kind :: resolved.data.builtin kind :: resolved.data.builtin
if (kind == .bool) return value.kind == .boolean if (kind == .bool) return value.kind == .boolean
if (kind == .string) return value.kind == .string if (kind == .string) return value.kind == .string
if (kind == .float or kind == .f64) return value.kind == .real
if (value.kind != .integer) return false if (value.kind != .integer) return false
bits := u32(64) bits := u32(64)
signed := false signed := false
@@ -1264,6 +1518,7 @@ valid_value proc(state @mut State, annotation ast.NodeId, value Value) bool ! Er
return value.magnitude <= limit return value.magnitude <= limit
} }
kind :: ast.expr_kind(&state.tree, type) kind :: ast.expr_kind(&state.tree, type)
if (value.kind == .aggregate) return value.annotation != no_node and try same_type(state, type, value.annotation)
if (kind == .optional_type) return value.kind == .null or try valid_value(state, ast.type_operand(&state.tree, type), value) if (kind == .optional_type) return value.kind == .null or try valid_value(state, ast.type_operand(&state.tree, type), value)
if (kind == .slice_type and value.kind == .string) { if (kind == .slice_type and value.kind == .string) {
view :: ast.slice_type(&state.tree, type) view :: ast.slice_type(&state.tree, type)
+145
View File
@@ -11,6 +11,151 @@ HONEY = ROOT / "build/honey"
# Explicit expectations guard against frontend bugs shared by both execution paths. # Explicit expectations guard against frontend bugs shared by both execution paths.
CASES = { CASES = {
"comptime_contextual_aggregate_arguments_keep_dependent_types": (
"""
sum proc(items [2]i16) i16 { return items[0] + items[1] }
answer proc($T type, $N usize, $items [N]T) T { return items[0] + items[1] }
main proc() i16 { return answer(i16, 2, [20, 20]) + $(sum([1, 1])) }
""", b"42\n", None,
),
"comptime_zero_cell_aggregates_keep_logical_lengths": (
"""
Empty :: struct {}
Storage :: struct { items [3]Empty, empty [0]i16 }
make proc() Storage { return Storage { items = [{}, {}, {}], empty = [] } }
main proc() usize {
value :: $(make())
return value.items.len + value.empty.len
}
""", b"3\n", None,
),
"explicit_comptime_float_arithmetic_keeps_ieee_equality": (
"""
main proc() bool { return $(1.5 + 2.5) == 4.0 and $(-0.0 == 0.0) and $(4 / 2) == 2.0 }
""", b"true\n", None,
),
"comptime_aggregate_specialization_uses_normalized_values": (
"""
Config :: struct { count usize, bias i16 = 2 }
config proc() Config {
result Config := { count = 0 }
while (result.count < 2) result.count = result.count + 1
return result
}
Fixed proc($settings Config) type {
return struct { values [settings.count]i16 }
}
main proc() i16 {
first Fixed(config()) :: { values = [40, 2] }
same Fixed({ bias = 2, count = 2 }) :: first
return same.values[0] + same.values[1]
}
""", b"42\n", None,
),
"comptime_array_arguments_and_explicit_copy_isolation": (
"""
values proc() [2]mut i16 {
result [2]mut i16 := [0, 2]
for (0..usize(2)) |index| result[index] = result[index] + 20
return result
}
sum proc($items [2]mut i16) i16 { return items[0] + items[1] }
main proc() i16 {
first := $(values())
copy := first
first[0] = 99
return sum(values()) + copy[0] - 20
}
""", b"42\n", None,
),
"explicit_comptime_without_templates_preserves_nested_values": (
r"""
Kind :: enum { ready, empty }
Inner :: struct { text string, count ?i16 }
Packet :: struct { rows [2]mut Inner, kind Kind, weight f64 }
make proc() Packet {
result Packet := {
rows = [{ text = "a\"\\\x00é", count = 20 }, { text = "ok", count = null }],
kind = .ready, weight = 2.5,
}
result.rows[0].count = 40
return result
}
main proc() i16 {
packet := $(make())
copy :: packet
packet.rows[0].text = "changed"
if (copy.kind != .ready or copy.weight != 2.5 or copy.rows[1].count != null) return 0
return copy.rows[0].count? + i16(copy.rows[1].text.len)
}
""", b"42\n", None,
),
"explicit_comptime_composes_and_supplies_array_lengths": (
"""
size proc() usize { return 2 }
main proc() i16 {
amount :: 40
values [$(size())]i16 :: [$(amount + 1), $(1)]
return values[$(usize(0))] + values[1]
}
""", b"42\n", None,
),
"explicit_comptime_cannot_read_runtime_parameters": (
"""
select proc(value i16) i16 { return $(value + 1) }
main proc() i16 { return select(41) }
""", b"", b"generic argument must be a compile-time value",
),
"comptime_aggregate_cannot_read_mutable_globals": (
"""
state i16 := 42
make proc() [1]i16 { return [state] }
main proc() i16 { return $(make())[0] }
""", b"", b"generic argument must be a compile-time value",
),
"comptime_aggregate_keeps_narrow_overflow": (
"""
Counter :: struct { value i8 }
make proc() Counter {
result Counter := { value = 127 }
result.value = result.value + 1
return result
}
main proc() i8 { return $(make()).value }
""", b"", b"integer arithmetic overflow",
),
"comptime_aggregate_rejects_pointer_escape": (
"""
Borrow :: struct { pointer @i16 }
make proc() Borrow {
value i16 :: 42
return Borrow { pointer = &value }
}
main proc() i16 { return $(make()).pointer^ }
""", b"", b"compile-time result cannot escape evaluation",
),
"comptime_aggregate_rejects_slice_escape": (
"""
make proc() [1][]u8 { return ["borrowed"] }
main proc() usize { return $(make())[0].len }
""", b"", b"compile-time result cannot escape evaluation",
),
"comptime_float_bits_survive_aggregate_materialization": (
"""
Floats :: struct { zero f64, nan f64 }
make proc() Floats {
bits u64 :: 9221120237041090626
return Floats { zero = -0.0, nan = ptrcast!(@f64, &bits)^ }
}
main proc() bool {
values :: $(make())
return (
ptrcast!(@u64, &values.zero)^ == 9223372036854775808
and ptrcast!(@u64, &values.nan)^ == 9221120237041090626
)
}
""", b"true\n", None,
),
"comptime_calls_share_runtime_inference_loops_and_recursion": ( "comptime_calls_share_runtime_inference_loops_and_recursion": (
""" """
length proc() usize { length proc() usize {