From 4de7c8bfc39f7596ce8343114f1359816f24b810 Mon Sep 17 00:00:00 2001 From: hl-valdemar Date: Mon, 26 Oct 2026 15:43:28 +0100 Subject: [PATCH] feat: evaluate compile-time aggregates and explicit expressions --- README.md | 4 +- TODO.md | 10 +- source/ast/ast.hon | 42 ++- source/compilation/compilation.test.hon | 6 +- source/diagnostics/diagnostics.hon | 2 + source/lower/lower.hon | 9 + source/parser/parse.hon | 15 ++ source/sema/check/check.hon | 2 + source/sema/resolve.hon | 1 + source/sema/specialize/specialize.hon | 327 +++++++++++++++++++++--- tests/native.py | 145 +++++++++++ 11 files changed, 518 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index c2b021a..db9d5b2 100644 --- a/README.md +++ b/README.md @@ -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. -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. diff --git a/TODO.md b/TODO.md index eb27810..d6724a8 100644 --- a/TODO.md +++ b/TODO.md @@ -19,9 +19,10 @@ explicit fallible channels, composed error sets, `try`/`catch` and scoped LIFO traps or compiler infrastructure failures. cached generic specialization and type-producing procedures support explicit/inferred compile-time arguments and generated record/array storage through both paths. -ordinary procedures can compute integer/boolean/string compile-time arguments using -mutable locals, loops and recursive calls; general aggregate/type-valued execution, -reflection and generic library acceptance remain open. +ordinary procedures can compute scalar and nested record/array compile-time values +using mutable locals, loops and recursive calls; explicit `$()` expressions use +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 `@source/compilation`, with compilation-wide AST node identities. 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. - [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. - - [ ] 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`. - [ ] 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. diff --git a/source/ast/ast.hon b/source/ast/ast.hon index a7b70c6..427f1a5 100644 --- a/source/ast/ast.hon +++ b/source/ast/ast.hon @@ -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) #! * 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. +#! '$(' expr ')' is an explicit compile-time expression with primary-expression precedence. #! * block .... -> '{' {stmt} '}' #! * arm ...... -> block | stmt (single statements become canonical blocks) #! * match_arm -> (expr {',' expr} ['|' identifier '|'] | 'else') ':' (block | stmt | expr); arms are comma/newline separated. @@ -69,6 +70,7 @@ ExprKind :: enum { try_expr catch_expr typed_constant + comptime_expr } DeclKind :: enum { @@ -172,6 +174,8 @@ NodeKind :: enum { stmt_errdefer stmt_block expr_typed_constant + expr_comptime + literal_float_value } @hide:file @@ -540,6 +544,23 @@ add_typed_constant proc( }, 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( builder @mut Builder, span Span, left, right NodeId, fallible bool, ) NodeId ! mem.AllocError { @@ -1385,6 +1406,8 @@ expression_kind proc(kind NodeKind) ExprKind { .expr_try: .try_expr .expr_catch: .catch_expr .expr_typed_constant: .typed_constant + .expr_comptime: .comptime_expr + .literal_float_value: .literal_float 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 { node :: get_node(&tree.storage, id) debug.assert(node.kind == .expr_typed_constant) @@ -1765,7 +1802,7 @@ NodeLayout :: struct { node_layout proc(node Node) NodeLayout { layout := NodeLayout{} 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_unary: layout.direct_children = 2 .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 .expr_pointer_type: layout.direct_children = 4 .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_record_type, .expr_record_literal, .expr_enum_type, .expr_match, .block, .package: { @@ -2089,6 +2126,7 @@ add_owned_literal proc( } #! 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 { node :: get_node(&tree.storage, id) if (node.data0.extra == 1) return tree.storage.generated.items[usize(node.main.extra)] diff --git a/source/compilation/compilation.test.hon b/source/compilation/compilation.test.hon index 3f9be57..8d87f2a 100644 --- a/source/compilation/compilation.test.hon +++ b/source/compilation/compilation.test.hon @@ -85,11 +85,13 @@ recompiles_specialized_calls_without_borrowing_previous_ir test { ` return result `} `Fixed proc($N usize) type { return struct { values [N]i16 } } + `data proc() [2]i16 { return [40, 2] } `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] } ` same Fixed(2) :: fixed - ` return identity(same.values[0]) + same.values[1] + ` return identity(same.values[0]) + same.values[$(usize(1))] `} , ) diff --git a/source/diagnostics/diagnostics.hon b/source/diagnostics/diagnostics.hon index 85417b8..e00be70 100644 --- a/source/diagnostics/diagnostics.hon +++ b/source/diagnostics/diagnostics.hon @@ -112,6 +112,7 @@ Code :: enum { specialization_cycle specialization_limit comptime_execution_limit + comptime_value_escape } Diagnostic :: struct { @@ -245,5 +246,6 @@ messages :: $enummap.init(Code, []u8, { specialization_cycle = "compile-time specialization depends on its unfinished result", specialization_limit = "compile-time specialization exceeds its recursion or instance 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", }) diff --git a/source/lower/lower.hon b/source/lower/lower.hon index c7ff6c3..8d8dd91 100644 --- a/source/lower/lower.hon +++ b/source/lower/lower.hon @@ -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) match ast.expr_kind(tree, id) { + .comptime_expr: { + if (!trap_type(state, id)) unreachable + } .typed_constant: { 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)) @@ -2130,6 +2133,12 @@ exact_float_integer proc(text []u8, value f64) bool { @hide:file lower_number proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) { 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) type :: lower_type(state, typecheck.type_of(state.checked, id)) if (ir.integer_bits(type) != 0 and !ir.unsigned_integer(type)) { diff --git a/source/parser/parse.hon b/source/parser/parse.hon index 7501dee..fe1d789 100644 --- a/source/parser/parse.hon +++ b/source/parser/parse.hon @@ -1606,6 +1606,21 @@ parse_primary proc(state @mut State) ast.NodeId ! ParseError { .union: return try parse_union_type(state) .distinct, .alias: return try parse_type_wrapper(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: { state.next_token += 1 member :: try consume_member(state, false) diff --git a/source/sema/check/check.hon b/source/sema/check/check.hon index 7b23602..fa17070 100644 --- a/source/sema/check/check.hon +++ b/source/sema/check/check.hon @@ -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)].narrowing_arm = state.narrowing_arm match ast.expr_kind(tree, id) { + .comptime_expr: try fail(state, id, .comptime_argument_required, ast.node_span(tree, id)) .typed_constant: { value :: ast.typed_constant(tree, id) state.cells[usize(id)].fixed = true @@ -3978,6 +3979,7 @@ constant_order proc(left, right ConstantInteger) i32 { @hide:file 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 if (ast.expr_kind(state.tree, id) == .literal_float) { return strconv.parse_float(state.edges.allocator, ast.literal_text(state.tree, id)) catch |err| { diff --git a/source/sema/resolve.hon b/source/sema/resolve.hon index 5e52840..5a64a05 100644 --- a/source/sema/resolve.hon +++ b/source/sema/resolve.hon @@ -526,6 +526,7 @@ resolve_expr proc( if (state.bindings[usize(id)].kind != .none) return tree :: state.tree match ast.expr_kind(tree, id) { + .comptime_expr: try resolve_expr(state, scope, ast.comptime_operand(tree, id)) .typed_constant: { value :: ast.typed_constant(tree, id) try resolve_expr(state, scope, value.type) diff --git a/source/sema/specialize/specialize.hon b/source/sema/specialize/specialize.hon index 3eb154c..a31a1e5 100644 --- a/source/sema/specialize/specialize.hon +++ b/source/sema/specialize/specialize.hon @@ -26,6 +26,8 @@ ValueKind :: enum { string function null + real + aggregate } @hide:file @@ -101,19 +103,21 @@ elaborate proc( ) Result ! Error { result := Result{ builder = builder, changes = arraylist.init(allocator), constraints = arraylist.init(allocator) } errdefer deinit(&result) + # Explicit evaluation is useful even in packages without generic declarations. original :: ast.view(builder, root) - generic := false - for (0..ast.package_decl_count(&original)) |i| { - id :: ast.package_decl(&original, i) - if (ast.decl_kind(&original, id) != .function) continue - result_type :: sema.binding(resolution, ast.function_decl(&original, id).return_type) - if (ast.function_generic(&original, id) - or (result_type.kind == .builtin and result_type.data.builtin == .type)) { - generic = true - break + needed := false + for (0..ast.node_count(&original)) |i| { + id :: ast.NodeId(i) + 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) + if (result_type.kind == .builtin and result_type.data.builtin == .type) needed = true } + if (needed) break } - if (!generic) return result + if (!needed) return result mapping :: try mem.alloc(ast.NodeId, allocator, ast.node_count(&original)) defer mem.free(allocator, mapping) state := State{ @@ -185,6 +189,26 @@ expand proc(state @mut State, id ast.NodeId) void ! Error { if (state.expanded.items[usize(id)] != 0) return state.expanded.items[usize(id)] = 1 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) { callee :: ast.call_expr(&state.tree, id).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) if (view.comptime) { 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) { try reject_evaluation(state, call) return @@ -527,9 +554,10 @@ evaluation_dependencies proc( resolved :: binding(state, id) if (resolved.kind == .declaration) { target :: resolved.data.declaration - if ((flags[usize(target)] & 1) != 0 and (flags[usize(target)] & 2) == 0) { - if (ast.node_class(&state.tree, target) == .statement - and ast.statement_decl(&state.tree, target).mutability == .variable) return false + if ((flags[usize(target)] & 8) == 0 and (flags[usize(target)] & 2) == 0) { + class :: ast.node_class(&state.tree, target) + 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 try arraylist.append(declarations, target) } @@ -540,20 +568,212 @@ evaluation_dependencies proc( 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 evaluate_call proc(state @mut State, call ast.NodeId) Value ! Error { callee :: try evaluate(state, ast.call_expr(&state.tree, call).callee) if (callee.kind != .function and callee.kind != .type) return Value{} result_type := callee.node + function := no_node if (callee.kind == .function) { resolved :: binding(state, callee.node) if (resolved.kind != .declaration) return Value{} + function = resolved.data.declaration result_type = ast.function_decl(&state.tree, resolved.data.declaration).return_type } arguments := arraylist.init(ast.NodeId, state.allocator) defer arraylist.deinit(&arguments) 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{} concrete := try materialize(state, value) 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. span :: ast.node_span(&state.tree, call) 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] 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) @@ -578,17 +806,12 @@ evaluate_call proc(state @mut State, call ast.NodeId) Value ! Error { flags := arraylist.init(u8, state.allocator) defer arraylist.deinit(&flags) index := usize(0) - registered := usize(0) while (index < declarations.items.len) { declaration :: declarations.items[index] try expand(state, declaration) 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 + evaluation_owned(state, declaration, flags.items) if (!try evaluation_dependencies(state, declaration, flags.items, &declarations)) return Value{} index += 1 } @@ -605,7 +828,11 @@ evaluate_call proc(state @mut State, call ast.NodeId) Value ! Error { original_diagnostics :: diagnostics.count(&diagnostic_store) checked := try typecheck.check(state.allocator, &tree, state.resolution, &diagnostic_store) 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) defer ir.deinit(&module) 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{} } value :: outcome.value - if (ir.integer_bits(value.type) != 0) { - negative :: !ir.unsigned_integer(value.type) and value.data.integer < 0 - magnitude :: if (negative) u64(-(value.data.integer + 1)) + 1 else value.data.unsigned - return Value{ kind = .integer, node = call, magnitude = magnitude, negative = negative, annotation = result_type } + source_type :: typecheck.type_of(&checked, expression) + node :: try serialize_value(state, &module, &checked, source_type, value, span) + if (node == no_node) { + state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .comptime_value_escape, span) + return Value{} } - 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{} + 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 @@ -726,6 +950,12 @@ evaluate proc(state @mut State, id ast.NodeId) Value ! Error { } 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, boolean = mem.eql(ast.literal_text(&state.tree, id), "true") } .literal_string: { @@ -749,6 +979,8 @@ evaluate proc(state @mut State, id ast.NodeId) Value ! Error { value := try evaluate(state, view.operand) if (view.operator == .negate and value.kind == .integer) { 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) { value.boolean = !value.boolean } else return Value{} @@ -757,6 +989,8 @@ evaluate proc(state @mut State, id ast.NodeId) Value ! Error { } .binary: return try evaluate_binary(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{} } } @@ -764,6 +998,7 @@ evaluate proc(state @mut State, id ast.NodeId) Value ! Error { @hide:file evaluate_binary proc(state @mut State, id ast.NodeId) Value ! Error { 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) if (left.kind == .invalid) return Value{} 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) 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 and left.kind == .integer and right.kind == .integer 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 .boolean: left.boolean == right.boolean .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) } } @@ -903,6 +1152,8 @@ value_hash proc(state @mut State, value Value) usize ! Error { .boolean: mix(hash, boolean) .function: mix(hash, usize(binding(state, value.node).data.declaration)) .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 } } @@ -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) } else if (value.kind == .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) return result @@ -1230,6 +1483,7 @@ valid_value proc(state @mut State, annotation ast.NodeId, value Value) bool ! Er kind :: resolved.data.builtin if (kind == .bool) return value.kind == .boolean if (kind == .string) return value.kind == .string + if (kind == .float or kind == .f64) return value.kind == .real if (value.kind != .integer) return false bits := u32(64) signed := false @@ -1264,6 +1518,7 @@ valid_value proc(state @mut State, annotation ast.NodeId, value Value) bool ! Er return value.magnitude <= limit } 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 == .slice_type and value.kind == .string) { view :: ast.slice_type(&state.tree, type) diff --git a/tests/native.py b/tests/native.py index 1858637..fcee14d 100644 --- a/tests/native.py +++ b/tests/native.py @@ -11,6 +11,151 @@ HONEY = ROOT / "build/honey" # Explicit expectations guard against frontend bugs shared by both execution paths. 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": ( """ length proc() usize {