From e036070bf2a54eb47db145b0437b403919e1cd90 Mon Sep 17 00:00:00 2001 From: hl-valdemar Date: Sat, 31 Oct 2026 10:50:37 +0100 Subject: [PATCH] feat: infer compile-time metadata locals --- README.md | 4 +- TODO.md | 5 +- source/compilation/compilation.test.hon | 2 +- source/diagnostics/diagnostics.hon | 2 +- source/lower/lower.hon | 2 +- source/sema/specialize/specialize.hon | 285 ++++++++++++++++++++---- tests/native.py | 197 ++++++++++++++-- 7 files changed, 419 insertions(+), 78 deletions(-) diff --git a/README.md b/README.md index a62d58f..c306519 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ main proc() int { } ``` -this prints `42`. both interpreted and Honey-compiled `main` must be a top-level function with no parameters; nonvoid results are printed, not used as process exit statuses. +this prints `42`. both interpreted and Honey-compiled `main` must be a top-level runtime function with no parameters; compile-time-only procedures cannot be entry points. nonvoid results are printed, not used as process exit statuses. compile the same supported language subset to a standalone Apple Silicon macOS executable: @@ -134,7 +134,7 @@ procedures returning fixed arrays or records/tuples containing `type`, such as ` ordinary parameters containing `type` also make a procedure compile-time-only, even when its result is a runtime value. for example, `count proc(types []type) usize { return types.len }` computes `count([i16, bool])` without runtime metadata. every input must be explicitly supplied and constant; use `$` parameters when later signature annotations depend on them. helpers can return owned scalar, aggregate and string values, recurse over metadata and use checked `try`/`catch` with scoped cleanup. plain and fallible `void` support bare returns and successful fallthrough. ordinary runtime dependencies and incomplete metadata arguments remain diagnostic; mutable metadata borrowing and general type-valued execution remain open. -explicit local annotations containing `type`, such as `T type := i16`, also make a procedure compile-time-only, even with ordinary scalar inputs and results. calls evaluate with owned local metadata and are erased. nested metadata records/arrays, partial initialization, checked updates, loops, recursion and scoped cleanup reuse type-control execution and its shared 4096-step bound. every input remains explicitly supplied and constant; this is not partial evaluation of runtime inputs. ordinary local type aliases without metadata storage annotations keep their runtime-call contract. inferred metadata-only locals and mutable metadata borrowing remain open. +local metadata storage also makes a procedure compile-time-only, even with ordinary scalar inputs and results. explicit annotations (`T type := i16`) and inferred mutable type bindings (`T := i16`), metadata array/record/tuple literals, helper results, checked projections and yielding blocks use owned snapshots and erased calls. nested storage, partial initialization, checked updates, loops, recursion and scoped cleanup reuse type-control execution and its shared 4096-step bound. inferred arrays retain readonly elements unless their type explicitly permits element updates; whole-value replacement remains available for mutable bindings. every input remains explicitly supplied and constant; this is not partial evaluation of runtime inputs. ordinary local `alias` declarations and scalar projections from metadata helpers keep their runtime-call contract. mixed runtime/type-valued execution and mutable metadata borrowing remain open. readonly type-valued slices (`[]type` and slices of metadata records/arrays) also cross compile-time procedure boundaries as owned snapshots. contextual array literals, including `[]`, construct them; explicit views of immutable readonly arrays and existing metadata slices support checked `usize` bounds, empty end views, indexing and `.len`. nested slices, ordinary/inline by-value iteration, erased `$` inputs and optional/fallible channels retain exact nominal leaves and content-based specialization identity. snapshots generate runtime types/defaults, but metadata itself has no runtime storage. mutable slice types, sentinels and views of mutable array places remain rejected rather than silently losing borrowing semantics; copy an array into an immutable binding before taking an owned view. construction shares the type-control step bound, and uninitialized leaves cannot cross procedure boundaries. diff --git a/TODO.md b/TODO.md index 039f194..d1777ed 100644 --- a/TODO.md +++ b/TODO.md @@ -58,7 +58,7 @@ checked immutable-array/slice views, by-value iteration and erased arguments; mutable metadata borrowing remains open. ordinary metadata-input procedures also compute owned runtime values, including reflected field names/counts, recursive helpers and fallible cleanup. -explicitly annotated local metadata storage also makes ordinary procedures +explicit and inferred local metadata storage also make ordinary procedures constant-input producers, preserving owned updates, loops, recursion and cleanup. `field!` selects record/tuple/enum and sequence members using compile-time names, including runtime places and type-control updates without runtime reflection. @@ -174,7 +174,8 @@ test discovery and FFI remain outside these completed slices. - [x] readonly type-slice checkpoint: `[]type` and slices of type-valued records/arrays use owned compile-time snapshots. contextual literals, immutable readonly array views, nested slice views, checked `usize` bounds/indexing, `.len`, empty shapes and ordinary/inline by-value iteration preserve exact nominal leaves. constant-input procedure parameters/results, erased generic inputs, optional/fallible nesting and normalized cache keys generate runtime storage/defaults through interpreter/native execution. mutable annotations/backing, metadata sentinels, invalid ranges/indices, readonly writes, incomplete leaves and runtime dependencies retain diagnostics. imported private types, source/session disposal, shared construction bounds and repeat compilation preserve ownership. mutable metadata borrowing, runtime metadata storage, general type-valued execution and broader constant materialization remain open. - [x] tagged type-storage checkpoint: tagged unions containing type leaves own active payload snapshots through typed/contextual construction, direct and nested field/index updates, immutable by-value matches and whole-value variant replacement. constant-input procedure arguments/results, erased generic inputs and normalized cache keys preserve nominal leaves and saved copies while generating runtime storage/defaults through interpreter/native execution. undefined union locals and nested record/array storage require whole-value initialization; inactive reads/writes, malformed explicit tags, nominal initializer conflicts, readonly updates, incomplete results and narrow overflow retain diagnostics. explicit sparse tag enums may contain unused members without adding match cases. imported private types, source/session disposal and repeat compilation preserve ownership. untagged metadata layouts remain invalid; mutable metadata borrowing, runtime metadata storage, general type-valued execution and broader constant materialization remain open. - [x] metadata-input execution checkpoint: procedures with ordinary type-valued parameters execute entirely at compile time even when returning runtime-representable values. all inputs remain explicit constants; owned scalar/record/array/string results, recursive readonly metadata iteration, reflected field names/counts and private nominal leaves work through interpreter/native execution. plain/fallible `void` retain bare-return/fallthrough semantics; ordinary fallible results propagate through `try`/`catch` and run scoped `defer`/`errdefer`. runtime dependencies and incomplete payloads retain diagnostics; source/session disposal and repeat compilation preserve ownership. dependent signature parameters still require `$`; general type-valued execution, mutable metadata borrowing and broader constant materialization remain open. - - [x] explicit metadata-local execution checkpoint: procedures with explicitly annotated type-valued locals execute entirely at compile time without metadata inputs or results. scalar inputs remain explicit constants; nested metadata records/arrays, undefined initialization, owned copies, checked field/index updates, loops, captures, recursion and scoped fallible cleanup reuse existing type-control execution through interpreter/native paths. runtime inputs and uninitialized leaves diagnose; ordinary unannotated local type aliases retain runtime parameters. imported private nominal leaves, source/session disposal and repeat compilation preserve ownership. inferred metadata-only locals, mixed runtime/type-valued execution, mutable metadata borrowing and broader constant materialization remain open. + - [x] explicit metadata-local execution checkpoint: procedures with explicitly annotated type-valued locals execute entirely at compile time without metadata inputs or results. scalar inputs remain explicit constants; nested metadata records/arrays, undefined initialization, owned copies, checked field/index updates, loops, captures, recursion and scoped fallible cleanup reuse existing type-control execution through interpreter/native paths. runtime inputs and uninitialized leaves diagnose; ordinary unannotated local type aliases retain runtime parameters. imported private nominal leaves, source/session disposal and repeat compilation preserve ownership. inferred metadata-only locals are covered below; mixed runtime/type-valued execution, mutable metadata borrowing and broader constant materialization remain open. + - [x] inferred metadata-local execution checkpoint: inferred mutable type bindings and metadata array/record/tuple locals select fully constant-input execution without metadata inputs, results or local annotations. literal/helper results, checked field/index/optional projections, yielding blocks and conditional/fallible selection preserve owned copies, nominal leaves, whole-value replacement, loops, recursion and scoped cleanup through interpreter/native execution. inferred array elements remain readonly; runtime inputs diagnose. ordinary local aliases and scalar metadata projections retain runtime parameters. ordinary fallible success/error snapshots use checked channel discrimination rather than assuming every payload carries a nominal type. imported private types, source/session disposal and repeat compilation preserve ownership. erased compile-time-only procedures cannot become runtime entries. mixed runtime/type-valued execution, mutable metadata borrowing and broader materialization remain open. - [ ] remaining gate: general type-valued compile-time execution, broader constant materialization (including address-bearing untagged unions, nonnull data pointers and mutable slices), plus remaining existing-library acceptance for broader `meta` operations and `io.print`. formatting parsing and broader imported C-binding support may interleave with later gates. - [ ] remaining source constructs: multiline strings. boolean and optional-capture conditional expressions, yielding value blocks, `yield`, compound updates and bitwise operators are covered above. 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. diff --git a/source/compilation/compilation.test.hon b/source/compilation/compilation.test.hon index ab5aefd..5104e85 100644 --- a/source/compilation/compilation.test.hon +++ b/source/compilation/compilation.test.hon @@ -1470,7 +1470,7 @@ tagged_type_metadata_recompiles_owned_private_nominal_payloads test { ` return i16(saved.payload.count) + bias `} `score_local proc(bias i16) i16 { - ` config Config := { children = [Child, null], count = 2 } + ` config := Config { children = [Child, null], count = 2 } ` saved :: config ` config.children[0] = bool ` if (saved.children[0]? != Child) return -1 diff --git a/source/diagnostics/diagnostics.hon b/source/diagnostics/diagnostics.hon index 7b76857..31d34d4 100644 --- a/source/diagnostics/diagnostics.hon +++ b/source/diagnostics/diagnostics.hon @@ -238,7 +238,7 @@ messages :: $enummap.init(Code, []u8, { recursive_record = "record has a recursive by-value layout", invalid_record_field_type = "record fields require a concrete value type", missing_entry = "program requires a main function", - invalid_entry = "main must be a function with no parameters", + invalid_entry = "main must be a runtime function with no parameters", literal_out_of_range = "numeric literal is outside its runtime type's range", inexact_integer_literal = "integer literal is not exactly representable as float; use a floating literal to allow rounding", invalid_escape = "invalid string escape", diff --git a/source/lower/lower.hon b/source/lower/lower.hon index e316355..fd18e8f 100644 --- a/source/lower/lower.hon +++ b/source/lower/lower.hon @@ -393,7 +393,7 @@ find_entry proc(state @mut State, root_package ast.NodeId) void ! (mem.AllocErro .import, .invalid: continue } if (!mem.eql(ast.source_text(tree, name), "main")) continue - if (ast.decl_kind(tree, id) != .function) { + if (ast.decl_kind(tree, id) != .function or state.storage[usize(id)].kind != .function) { state.module.entry_diagnostic = try diagnostics.add(state.diagnostics, .invalid_entry, name) return } diff --git a/source/sema/specialize/specialize.hon b/source/sema/specialize/specialize.hon index cd4f061..705a74f 100644 --- a/source/sema/specialize/specialize.hon +++ b/source/sema/specialize/specialize.hon @@ -664,7 +664,7 @@ expand_expected proc(state @mut State, id, expected ast.NodeId) void ! Error { ) { for (0..ast.record_field_count(&state.tree, id)) |i| { field :: ast.field(&state.tree, ast.record_field(&state.tree, id, i)) - slot :: if (field.name != null) type_record_slot(state, shape, field.name?) else i + slot :: if (field.name != null) type_record_slot(&state.tree, shape, field.name?) else i field_type := no_node if (slot < ast.record_field_count(&state.tree, shape)) { if (ast.field(&state.tree, ast.record_field(&state.tree, shape, slot)).type) |type| @@ -1688,7 +1688,7 @@ specialize_match proc(state @mut State, id, statement, expected ast.NodeId) bool if (ast.expr_kind(&state.tree, annotation) != .union_type) break pattern :: ast.match_pattern(&state.tree, arm_id, 0) if (ast.expr_kind(&state.tree, pattern) != .enum_literal) continue - slot :: type_record_slot(state, annotation, ast.enum_literal(&state.tree, pattern)) + slot :: type_record_slot(&state.tree, annotation, ast.enum_literal(&state.tree, pattern)) if (slot == no_index) continue field :: ast.field(&state.tree, ast.record_field(&state.tree, annotation, slot)) if (field.type) |type| payload = type @@ -1860,8 +1860,181 @@ type_storage_annotation proc(tree @ast.Tree, bindings []sema.Binding, input ast. return false } -# explicit metadata locals have no runtime storage, just like metadata parameters. -# do not classify ordinary local type aliases as constant-input procedures. +@hide:file +storage_schema proc(tree @ast.Tree, bindings []sema.Binding, input ast.NodeId, depth usize) ast.NodeId { + if (depth >= 128 or usize(input) >= bindings.len) return no_node + resolved :: bindings[usize(input)] + if (resolved.kind == .declaration) { + target :: resolved.data.declaration + if (ast.node_class(tree, target) == .function) return target + if (ast.node_class(tree, target) == .statement) + return storage_schema(tree, bindings, ast.statement_decl(tree, target).initializer, depth + 1) + } + if (ast.expr_kind(tree, input) == .alias_expr) + return storage_schema(tree, bindings, ast.type_operand(tree, input), depth + 1) + return input +} + +# read only existing annotations: discovery must not specialize recursive calls. +@hide:file +storage_expression_annotation proc( + tree @ast.Tree, + bindings []sema.Binding, + input ast.NodeId, + depth usize, +) ast.NodeId { + if (depth >= 128 or usize(input) >= bindings.len) return no_node + resolved :: bindings[usize(input)] + if (resolved.kind == .declaration) { + target :: resolved.data.declaration + class :: ast.node_class(tree, target) + if (class == .parameter) return ast.param(tree, target).type + if (class == .statement) { + declaration :: ast.statement_decl(tree, target) + if (declaration.type) |annotation| return annotation + return storage_expression_annotation(tree, bindings, declaration.initializer, depth + 1) + } + return no_node + } + kind :: ast.expr_kind(tree, input) + if (kind == .typed_constant) return ast.typed_constant(tree, input).type + if (kind == .union_constant) return ast.union_constant(tree, input).type + if (kind == .slice_constant) return ast.slice_constant(tree, input).type + if (kind == .record_literal) { + if (ast.record_literal(tree, input).type) |type| return type + return no_node + } + if (kind == .call) { + callee :: storage_schema(tree, bindings, ast.call_expr(tree, input).callee, depth + 1) + if (callee != no_node and ast.node_class(tree, callee) == .function) + return ast.function_decl(tree, callee).return_type + return no_node + } + base := no_node + if (kind == .selection) base = ast.selection_expr(tree, input).base + else if (kind == .index) base = ast.index_expr(tree, input).base + else if (kind == .slice) base = ast.slice_expr(tree, input).base + else if (kind == .unary) base = ast.unary_expr(tree, input).operand + else if (kind == .try_expr) base = ast.type_operand(tree, input) + else if (kind == .catch_expr) base = ast.catch_expr(tree, input).operand + else if (kind == .comptime_expr) return storage_expression_annotation(tree, bindings, + ast.comptime_operand(tree, input), depth + 1) + if (base == no_node) return no_node + annotation :: storage_schema(tree, bindings, + storage_expression_annotation(tree, bindings, base, depth + 1), depth + 1) + if (annotation == no_node) return no_node + shape :: ast.expr_kind(tree, annotation) + if (kind == .slice) return annotation + if (kind == .try_expr or kind == .catch_expr) + return if (shape == .fallible_type) ast.binary_expr(tree, annotation).left else no_node + if (kind == .unary) + return if (shape == .optional_type and ast.unary_expr(tree, input).operator == .unwrap) + ast.type_operand(tree, annotation) else no_node + if (kind == .index) { + if (shape == .array_type) return ast.array_type(tree, annotation).element + if (shape == .slice_type) return ast.slice_type(tree, annotation).element + return no_node + } + if (shape != .record_type and shape != .union_type) return no_node + member :: ast.selection_expr(tree, input).member + slot :: type_record_slot(tree, annotation, member) + if (slot == no_index) return no_node + if (ast.field(tree, ast.record_field(tree, annotation, slot)).type) |type| return type + return no_node +} + +@hide:file +type_storage_yields proc( + tree @ast.Tree, + bindings []sema.Binding, + id ast.NodeId, + scalar_type bool, + depth usize, +) bool { + if (depth >= 128) return false + class :: ast.node_class(tree, id) + if (class != .block and class != .statement) return false + if (class == .statement and ast.stmt_kind(tree, id) == .yield_stmt) { + value :: ast.yield_stmt(tree, id).value + return value != null and type_storage_initializer(tree, bindings, value?, scalar_type, depth + 1) + } + for (0..ast.child_count(tree, id)) |i| { + if (type_storage_yields(tree, bindings, ast.child(tree, id, i), scalar_type, depth + 1)) return true + } + return false +} + +# discover inferred storage without elaborating calls or executing initializers. +# immutable scalar type bindings remain aliases in ordinary runtime procedures. +@hide:file +type_storage_initializer proc( + tree @ast.Tree, bindings []sema.Binding, input ast.NodeId, scalar_type bool, depth usize, +) bool { + if (depth >= 128 or usize(input) >= bindings.len) return false + annotation :: storage_schema(tree, bindings, + storage_expression_annotation(tree, bindings, input, depth + 1), depth + 1) + if (annotation != no_node) { + if (!scalar_type and bindings[usize(annotation)].kind == .builtin + and bindings[usize(annotation)].data.builtin == .type) return false + return type_storage_annotation(tree, bindings, annotation, depth + 1) + } + resolved :: bindings[usize(input)] + if (resolved.kind == .builtin) return scalar_type + if (resolved.kind == .declaration) { + target :: resolved.data.declaration + class :: ast.node_class(tree, target) + if (class == .statement) { + declaration :: ast.statement_decl(tree, target) + if (declaration.type != null) return false + return type_storage_initializer(tree, bindings, declaration.initializer, scalar_type, depth + 1) + } + return false + } + kind :: ast.expr_kind(tree, input) + match kind { + .record_type, .array_type, .slice_type, .pointer_type, .function_type, .optional_type, + .enum_type, .union_type, .union_tag_type, .distinct_type, .fallible_type, .sum_type, .alias_expr: + return scalar_type + .record_literal: { + for (0..ast.record_field_count(tree, input)) |i| { + field :: ast.field(tree, ast.record_field(tree, input, i)) + if (field.value != null + and type_storage_initializer(tree, bindings, field.value?, true, depth + 1)) return true + } + return false + } + .array_literal: { + for (0..ast.array_element_count(tree, input)) |i| { + if (type_storage_initializer(tree, bindings, ast.array_element(tree, input, i), true, depth + 1)) + return true + } + return false + } + .comptime_expr: + return type_storage_initializer(tree, bindings, ast.comptime_operand(tree, input), scalar_type, depth + 1) + .match_expr: { + for (0..ast.match_arm_count(tree, input)) |i| { + arm :: ast.match_arm_view(tree, ast.match_arm(tree, input, i)) + if (arm.expression) { + if (type_storage_initializer(tree, bindings, arm.body, scalar_type, depth + 1)) return true + } else if (type_storage_yields(tree, bindings, arm.body, scalar_type, depth + 1)) return true + } + return false + } + .block_expr: + return type_storage_yields(tree, bindings, ast.block_expr(tree, input), scalar_type, depth + 1) + .builtin_call: { + name :: ast.literal_text(tree, ast.builtin_call(tree, input).name) + if (mem.eql(name, "typeinfo")) return true + if (mem.eql(name, "struct_type")) return scalar_type + if (mem.eql(name, "some") and ast.call_arg_count(tree, input) == 1) + return type_storage_initializer(tree, bindings, ast.call_arg(tree, input, 0), true, depth + 1) + return false + } + else: return false + } +} + @hide:file type_local_storage proc(tree @ast.Tree, bindings []sema.Binding, id ast.NodeId, depth usize) bool { if (depth >= 128) return false @@ -1870,6 +2043,8 @@ type_local_storage proc(tree @ast.Tree, bindings []sema.Binding, id ast.NodeId, if (declaration.type) |annotation| { if (type_storage_annotation(tree, bindings, annotation, 0)) return true } + else if (type_storage_initializer(tree, bindings, declaration.initializer, + declaration.mutability == .variable, 0)) return true } for (0..ast.child_count(tree, id)) |i| { if (type_local_storage(tree, bindings, ast.child(tree, id, i), depth + 1)) return true @@ -1939,7 +2114,7 @@ specialize_call proc(state @mut State, call, function, expected ast.NodeId) void state.depth += 1 defer state.depth -= 1 count :: ast.function_param_count(&state.tree, function) - # metadata inputs, results and explicit locals require a fully constant call. + # metadata inputs, results and locals require a fully constant call. type_result :: compiletime_call(state, function) runtime := usize(0) for (0..count) |i| { @@ -3331,9 +3506,41 @@ type_snapshot_expected proc( return try type_snapshot(state, execution, id) } +@hide:file +type_fallible_failed proc( + state @mut State, + owned, type, at ast.NodeId, +) ?bool ! Error { + missing ?bool :: null + if (type_record_storage(state, type, 0)) { + if (!try type_array_initialized(state, owned, at)) return missing + return !try same_type(state, + ast.typed_constant(&state.tree, ast.typed_constant(&state.tree, owned).value).type, + ast.binary_expr(&state.tree, type).left) + } + span :: ast.node_span(&state.tree, at) + boolean :: try builtin(state, at, .bool) + yes :: try ast.add_boolean(state.builder, span, true) + no :: try ast.add_boolean(state.builder, span, false) + returned :: try ast.add_return_stmt(state.builder, span, yes) + failure_statements [1]ast.NodeId :: [returned] + handler :: try ast.add_block(state.builder, span, failure_statements[..]) + caught :: try ast.add_catch_expr(state.builder, span, owned, null, handler, true) + discarded :: try ast.add_expression_stmt(state.builder, caught) + success :: try ast.add_return_stmt(state.builder, span, no) + statements [2]ast.NodeId :: [discarded, success] + body :: try ast.add_block(state.builder, span, statements[..]) + try refresh(state) + guard :: try evaluate_ir_body(state, at, body, boolean) + if (guard.kind != .boolean) return missing + return guard.boolean +} + @hide:file type_snapshot_catch proc( - state @mut State, execution @mut TypeExecution, id, annotation ast.NodeId, + state @mut State, + execution @mut TypeExecution, + id, annotation ast.NodeId, ) ast.NodeId ! Error { view :: ast.catch_expr(&state.tree, id) operand :: try type_snapshot(state, execution, view.operand) @@ -3344,29 +3551,9 @@ type_snapshot_catch proc( channels :: ast.binary_expr(&state.tree, operand_type) owned :: try type_execution_value(state, operand, operand_type) if (owned == no_node) return no_node - span :: ast.node_span(&state.tree, id) - failed := false - if (type_record_storage(state, operand_type, 0)) { - if (!try type_array_initialized(state, owned, id)) return no_node - failed = !try same_type(state, - ast.typed_constant(&state.tree, ast.typed_constant(&state.tree, owned).value).type, channels.left) - } else { - boolean :: try builtin(state, id, .bool) - yes :: try ast.add_boolean(state.builder, span, true) - no :: try ast.add_boolean(state.builder, span, false) - returned :: try ast.add_return_stmt(state.builder, span, yes) - failure_statements [1]ast.NodeId :: [returned] - handler :: try ast.add_block(state.builder, span, failure_statements[..]) - caught :: try ast.add_catch_expr(state.builder, span, owned, null, handler, true) - discarded :: try ast.add_expression_stmt(state.builder, caught) - success :: try ast.add_return_stmt(state.builder, span, no) - statements [2]ast.NodeId :: [discarded, success] - body :: try ast.add_block(state.builder, span, statements[..]) - try refresh(state) - guard :: try evaluate_ir_body(state, id, body, boolean) - if (guard.kind != .boolean) return no_node - failed = guard.boolean - } + failure :: try type_fallible_failed(state, owned, operand_type, id) + if (failure == null) return no_node + failed :: failure? payload :: ast.typed_constant(&state.tree, owned).value if (!failed) return try type_execution_value(state, payload, channels.left) saved :: execution.locals.items.len @@ -3791,7 +3978,7 @@ type_match_arm proc(state @mut State, id ast.NodeId) usize ! Error { continue } shape :: try inline_match_shape(state, annotation) - slot :: type_record_slot(state, shape, ast.enum_literal(&state.tree, pattern)) + slot :: type_record_slot(&state.tree, shape, ast.enum_literal(&state.tree, pattern)) if (slot == no_index) { valid = false continue @@ -4002,7 +4189,7 @@ type_array_projection proc(state @mut State, id ast.NodeId) bool ! Error { if (record) { if (kind != .selection) return true member :: ast.selection_expr(&state.tree, id).member - slot :: type_record_slot(state, type, member) + slot :: type_record_slot(&state.tree, type, member) if (slot == no_index) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .unknown_field, ast.node_span(&state.tree, member)) @@ -4087,24 +4274,24 @@ type_record_storage proc(state @State, annotation ast.NodeId, depth usize) bool } @hide:file -type_record_slot proc(state @State, record, member ast.NodeId) usize { - if (ast.expr_kind(&state.tree, member) != .literal_int) { - for (0..ast.record_field_count(&state.tree, record)) |i| { - field :: ast.field(&state.tree, ast.record_field(&state.tree, record, i)) - if (field.name != null and ast.name_id(&state.tree, field.name?) == ast.name_id(&state.tree, member)) +type_record_slot proc(tree @ast.Tree, record, member ast.NodeId) usize { + if (ast.expr_kind(tree, member) != .literal_int) { + for (0..ast.record_field_count(tree, record)) |i| { + field :: ast.field(tree, ast.record_field(tree, record, i)) + if (field.name != null and ast.name_id(tree, field.name?) == ast.name_id(tree, member)) return i } return no_index } ordinal := usize(0) - for (ast.literal_text(&state.tree, member)) |byte| { + for (ast.literal_text(tree, member)) |byte| { if (byte < '0' or byte > '9') return no_index digit :: usize(byte - '0') if (ordinal > divtrunc!(maxval!(usize) - digit, usize(10))) return no_index ordinal = ordinal * 10 + digit } - if (ordinal >= ast.record_field_count(&state.tree, record)) return no_index - if (ast.field(&state.tree, ast.record_field(&state.tree, record, ordinal)).name != null) return no_index + if (ordinal >= ast.record_field_count(tree, record)) return no_index + if (ast.field(tree, ast.record_field(tree, record, ordinal)).name != null) return no_index return ordinal } @@ -4157,7 +4344,7 @@ type_union_value proc(state @mut State, expression, type ast.NodeId) ast.NodeId if (field.value) |value| input = value } } - slot :: if (name != no_node) type_record_slot(state, type, name) else no_index + slot :: if (name != no_node) type_record_slot(&state.tree, type, name) else no_index if (slot == no_index) { code diagnostics.Code :: if (name != no_node) .unknown_field else .union_initializer_count state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, @@ -4309,7 +4496,7 @@ type_record_check proc(state @mut State, annotation ast.NodeId) bool ! Error { } } } - if (field.name != null and type_record_slot(state, type, field.name?) != i) { + if (field.name != null and type_record_slot(&state.tree, type, field.name?) != i) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .duplicate_field, field.span) return false } @@ -4362,7 +4549,7 @@ type_record_value proc(state @mut State, expression, annotation ast.NodeId) ast. for (0..ast.record_field_count(&state.tree, expression)) |i| { field :: ast.field(&state.tree, ast.record_field(&state.tree, expression, i)) slot := i - if (field.name) |name| slot = type_record_slot(state, type, name) + if (field.name) |name| slot = type_record_slot(&state.tree, type, name) if (slot >= count or values[slot] != no_node) { code :: if (slot < count) diagnostics.Code.duplicate_field else if (field.name != null) diagnostics.Code.unknown_field else diagnostics.Code.tuple_arity_mismatch @@ -4376,7 +4563,7 @@ type_record_value proc(state @mut State, expression, annotation ast.NodeId) ast. for (0..count) |i| { field :: ast.field(&state.tree, ast.record_field(&state.tree, type, i)) if (field.name) |name| { - if (type_record_slot(state, type, name) != i) { + if (type_record_slot(&state.tree, type, name) != i) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .duplicate_field, field.span) return no_node } @@ -4750,7 +4937,7 @@ type_array_update proc( previous := no_node if (record) { if (ast.expr_kind(&state.tree, first) == .selection) - index = type_record_slot(state, type, ast.selection_expr(&state.tree, first).member) + index = type_record_slot(&state.tree, type, ast.selection_expr(&state.tree, first).member) if (index == no_index) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .unknown_field, ast.node_span(&state.tree, first)) @@ -4860,7 +5047,7 @@ type_variant proc(state @mut State, annotation, name ast.NodeId) bool ! Error { members :: ast.binary_expr(&state.tree, type) return try type_variant(state, members.left, name) or try type_variant(state, members.right, name) } - if (kind == .union_type) return type_record_slot(state, type, name) != no_index + if (kind == .union_type) return type_record_slot(&state.tree, type, name) != no_index if (kind != .enum_type) return false for (0..ast.enum_member_count(&state.tree, type)) |i| { member :: ast.field(&state.tree, ast.enum_member(&state.tree, type, i)).name? @@ -5294,9 +5481,9 @@ execute_type_statement_body proc(state @mut State, execution @mut TypeExecution, if (execution.returned != no_node) { type :: try canonical_type(state, execution.result_type) if (type != no_node and ast.expr_kind(&state.tree, type) == .fallible_type) { - payload :: ast.typed_constant(&state.tree, execution.returned).value - execution.failed = !try same_type(state, ast.typed_constant(&state.tree, payload).type, - ast.binary_expr(&state.tree, type).left) + failed :: try type_fallible_failed(state, execution.returned, type, value) + if (failed == null) return .invalid + execution.failed = failed? } } if (execution.returned != no_node) return .returned @@ -6425,7 +6612,7 @@ expression_type proc(state @mut State, id ast.NodeId, depth usize) ast.NodeId ! } } if (base_kind != .record_type and base_kind != .union_type) return no_node - slot :: type_record_slot(state, base, view.member) + slot :: type_record_slot(&state.tree, base, view.member) if (slot == no_index) return no_node return try canonical_type(state, ast.field(&state.tree, ast.record_field(&state.tree, base, slot)).type?) } diff --git a/tests/native.py b/tests/native.py index d6100c6..e9eb62d 100644 --- a/tests/native.py +++ b/tests/native.py @@ -11,6 +11,161 @@ HONEY = ROOT / "build/honey" # Explicit expectations guard against frontend bugs shared by both execution paths. CASES = { + "compiletime_only_main_cannot_select_another_runtime_function": ( + """ + fallback proc() i16 { return -99 } + main proc() i16 { + types := [i16, bool] + return 42 + } + """, b"", b"main must be a runtime function with no parameters", + ), + "inferred_metadata_locals_preserve_owned_copies_and_updates": ( + """ + Config :: struct { children [2]mut ?type, count usize } + compute proc(count usize) i16 { + config := Config { children = [i8, null], count = 0 } + saved :: config + while (config.count < count) { + config.children[0] = i16 + config.children[1] = config.children[0] + config.count += 1 + } + types := [i8, bool] + before :: types + types = [i16, bool] + tuple := { types[0], config.count } + tuple.0 = i8 + if (saved.children[0]? != i8 or saved.count != 0 or before[0] != i8) return -1 + if (tuple.0 != i8 or tuple.1 != 2) return -2 + for config.children |child| { if child |T| { if (T != i16) return -3 } } + Element :: types[0] + values [count]Element :: [19, 23] + return values[0] + values[1] + } + main proc() i16 { return compute(2) } + """, b"42\n", None, + ), + "inferred_type_locals_recurse_and_preserve_cleanup": ( + """ + Error :: enum { failed } + count proc(depth usize) i16 { + T := i8 + T = i16 + defer if (T != i16) unreachable + if (depth == 0) return 21 + return count(depth - 1) + 21 + } + fail proc() i16 ! Error { + T := ?i16 + T = ?i8 + errdefer if (T != ?i8) unreachable + return .failed + } + main proc() i16 { return fail() catch { yield count(1) } } + """, b"42\n", None, + ), + "inferred_metadata_call_results_and_checked_projections": ( + """ + Config :: struct { child type, count usize } + Schema :: alias Config + create proc() Schema { return { child = i8, count = 2 } } + children proc() [2]type { return [i8, bool] } + optional proc() ?type { return i8 } + pair proc() struct { type, usize } { return {i8, 2} } + field_type proc(value i16) i16 { + T := create().child + T = i16 + result T :: value + return result + } + indexed_type proc(value i16) i16 { + T := children()[0] + T = i16 + result T :: value + return result + } + unwrapped_type proc(value i16) i16 { + T := optional()? + T = i16 + result T :: value + return result + } + tuple_type proc(value i16) i16 { + T := pair().0 + T = i16 + result T :: value + return result + } + compute proc() i16 { + config := create() + saved :: config + config.child = i16 + if (saved.child != i8) return -1 + return field_type(19) + indexed_type(19) + unwrapped_type(2) + tuple_type(2) + } + main proc() i16 { return compute() } + """, b"42\n", None, + ), + "inferred_metadata_value_blocks_matches_and_fallible_calls": ( + """ + Error :: enum { failed } + Config :: struct { child type, count usize } + create proc() Config ! Error { return { child = i8, count = 2 } } + compute proc(flag bool) i16 ! Error { + errdefer unreachable + config := try create() + saved :: config + config.child = i16 + fields := { yield [i8, bool] } + selected := if (flag) { yield { child = i16, count = config.count } } + else { yield { child = i8, count = usize(1) } } + if (saved.child != i8 or fields[1] != bool or selected.child != i16) return -1 + Element :: selected.child + values [selected.count]Element :: [19, 23] + return values[0] + values[1] + } + main proc() i16 { return compute(true) catch -2 } + """, b"42\n", None, + ), + "inferred_metadata_locals_require_constant_inputs": ( + """ + Config :: struct { child type, count usize } + count proc(value usize) usize { + config := Config { child = i16, count = value } + return config.count + } + runtime proc(value usize) usize { return count(value) } + main proc() usize { return runtime(42) } + """, b"", b"generic argument must be a compile-time value", + ), + "inferred_metadata_arrays_keep_readonly_elements": ( + """ + compute proc() usize { + types := [i8, bool] + types[0] = i16 + return types.len + } + main proc() usize { return compute() } + """, b"", b"indexed assignment requires a writable array element", + ), + "immutable_type_aliases_and_scalar_metadata_projections_stay_runtime": ( + """ + Config :: struct { child type, count usize } + create proc() Config { return { child = i16, count = 2 } } + Child proc() type { return i16 } + add proc(value i16) i16 { + Local :: alias i16 + Other :: alias Child() + amount := create().count + argument Local :: value + result Other :: argument + i16(amount) + return result + } + forward proc(value i16) i16 { return add(value) } + main proc() i16 { return forward(40) } + """, b"42\n", None, + ), "explicit_metadata_locals_compute_without_metadata_inputs": ( """ Config :: struct { children [2]mut ?type, count usize } @@ -3919,15 +4074,6 @@ CASES = { main proc() Selected() { return 42 } """, b"", b"compile-time execution exceeds its recursion or step limit", ), - "type_record_procedure_results_do_not_enable_runtime_storage": ( - """ - config proc() struct { child type, count i16 } { return { child = i16, count = 42 } } - main proc() i16 { - input :: config() - return input.count - } - """, b"", b"", - ), "type_records_reject_unknown_initializer_field": ( """ Bad proc() type { @@ -4020,14 +4166,6 @@ CASES = { main proc() Bad() { return 42 } """, b"", b"outside its runtime type", ), - "type_records_do_not_enable_runtime_storage": ( - """ - main proc() i16 { - config :: { child = i16, count = 42 } - return config.count - } - """, b"", b"type name cannot be used as a value", - ), "field_reflection_runtime_places_callbacks_and_single_evaluation": ( """ Point :: struct { value i16, callback @proc(value i16) i16 } @@ -11964,13 +12102,28 @@ def check_typeinfo_packages(temporary, executable): """, b"indexed assignment requires a writable array element", ), - ( - "runtime_metadata_escape", - "main proc() i16 { info :: typeinfo!(i16)\nreturn 42 }", - b"record fields require a concrete value type", - ), ): cases[name] = ('import "@std/meta"\n' + text, b"", error) + cases["inferred_reflection_locals_compute_without_metadata_parameters"] = ( + """ + import "@std/meta" + Record :: struct { child i16, enabled bool } + compute proc(bias i16) i16 { + info :: typeinfo!(Record) + match info { + .record |record|: { + saved :: record.fields[0] + field := saved + field.type = i8 + if (saved.type != i16 or field.type != i8) return -1 + return bias + i16(record.fields.len) + } + else: return -2, + } + } + main proc() i16 { return compute(40) } + """, b"42\n", None, + ) cases["missing_meta_import"] = ( "main proc() i16 { typeinfo!(i16)\nreturn 42 }", b"", b"typeinfo! requires importing @std/meta",