diff --git a/README.md b/README.md index c6ae26e..7fee8f6 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,9 @@ type-producing bodies support mutable type and value locals, direct binding assi type-producing bodies also execute scalar/enum `match` statements and type-valued match expressions in initializers, assignments and returns, such as `return match wide { true: i16, false: i8 }`. selected expression arms may themselves be matches. an ordinary checked IR match returns the selected arm index, preserving subject/pattern typing, range boundaries, overlap, unreachable-arm and exhaustiveness diagnostics without checking unexecuted bodies. statement arms retain scoped locals, assignments, early returns and enclosing-loop break/continue; expression selection shares the type-control step bound. -matches also select nested types, array lengths, generic arguments, callback signatures and field defaults in type-producing bodies. for example, `return struct { values [2](match wide { true: i16, false: i8 }) }` generates concrete storage without elaborating the unselected arm. snapshots preserve loop-local type/value bindings, selected literals retain their spelling and source provenance, and nominal results are cached before their recursive fields are expanded. deferred type selection shares the producer's execution bound; failed elaboration retains its original diagnostic without leaving unchecked generated types behind. tagged-union payload captures, general type-valued storage and broader generic-body match specialization remain open. +matches also select nested types, array lengths, generic arguments, callback signatures and field defaults in type-producing bodies. for example, `return struct { values [2](match wide { true: i16, false: i8 }) }` generates concrete storage without elaborating the unselected arm. snapshots preserve loop-local type/value bindings, selected literals retain their spelling and source provenance, and nominal results are cached before their recursive fields are expanded. deferred type selection shares the producer's execution bound; failed elaboration retains its original diagnostic without leaving unchecked generated types behind. general type-valued storage and broader generic-body match specialization remain open. + +tagged-union matches in type-producing bodies support immutable by-value payload captures in statements, expressions and nested type/storage positions. for example, `match choice { .count |length|: return [length]i16, .empty: return bool }` uses a captured `usize` to generate storage. grouped variants require identical nonvoid payload types; invalid captures in unselected arms are still diagnosed. selected payloads become owned typed constants, preserving narrow integers, nested aggregates and strings across generated lengths/defaults, loop snapshots and repeatable compilation. pointer/slice/callback-bearing active payloads retain the constant-materialization escape boundary below. ordinary procedures may compute integer, boolean, float and string compile-time arguments and return nested records/tuples, fixed arrays, tagged unions and explicit fallible results 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 and fallible arguments receive their declared context, including dependent `[N]T` parameters; cached specializations compare normalized values, not literal spelling or evaluation identity. returned constants become owned source literals before temporary interpreter storage is released, preserving by-value copies, exact integer types, string bytes, enum/optional fields and IEEE float bits. tagged unions retain their actual discriminant and only the active payload, including nested aggregates and void/zero-cell variants. fallible constants retain success/error identity, composed or payload-bearing errors and void success; `catch` can consume them in generated lengths, defaults and runtime expressions. diff --git a/TODO.md b/TODO.md index 46415ab..7cd6876 100644 --- a/TODO.md +++ b/TODO.md @@ -25,10 +25,10 @@ explicit `$()` expressions use the same constant evaluation path. compile-time `if` selection and type comparisons specialize only the selected body. type-producing procedures also accept ordinary constant inputs, cached and erased like explicit compile-time arguments. mutable -type/value locals, direct assignments, branches, scalar/enum matching and bounded -integer-range/while loops now generate types and storage. matches select nested -constructors, lengths, signatures and defaults; general type-valued execution, -reflection and generic library acceptance remain open. +type/value locals, direct assignments, branches, scalar/enum/tagged-union matching +and bounded integer-range/while loops now generate types and storage. payload +captures and matches select nested constructors, lengths, signatures and defaults; +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 @@ -71,9 +71,10 @@ test discovery and FFI remain outside these completed slices. - [x] conditional specialization checkpoint: explicit compile-time `if` guards and side-effect-free constant guards in concrete generic procedures select a body before specialization/checking. type equality retains aliases, nominal identity and structural signatures; composed error-set identity/cache keys ignore ordering, grouping and repetition. guard checking, selected-body diagnostics, scoped cleanup, runtime guards and repeatable compilation work through interpreter/native execution; both arms remain parsed and name-resolved. - [x] constant-input type-procedure checkpoint: type-producing procedures accept ordinary parameters alongside `$` parameters, require every argument explicitly and evaluate/cache/erase all inputs. generated record/array storage, nominal cache identity, nested calls, dependent contextual aggregate inputs, exact erased-argument checking, source diagnostics, imported producers and repeatable compilation work through interpreter/native execution. - [x] mutable type-execution checkpoint: type-producing bodies execute mutable type/value locals, direct binding assignments, scoped blocks, if/else and early returns, integer-range/while loops, break/continue and nested/recursive producers. generated nested types, scalar-computed lengths/defaults, nominal cache identity, exact value types, overflow/guard/assignment diagnostics, imported producers and repeatable compilation work through interpreter/native execution. type-control execution shares a 4096-step bound across nested producers; scalar operations reuse checked IR execution. field/index updates, optional captures, cleanup and type-valued aggregate storage in these bodies remain open. - - [x] type-match checkpoint: type-producing bodies execute scalar/enum match statements and direct type-valued match expressions in initializers, assignments and returns, including nested selected arms. ordinary checking/IR execution select the arm and validate subject/pattern types, ranges, overlap, unreachable arms and exhaustiveness; only the selected body is elaborated. generated storage, scoped locals, loop transfers, early returns, nominal cache identity, selected-body diagnostics, dependency provenance and repeatable compilation work through interpreter/native execution. all bodies remain parsed/name-resolved and execution shares the type-control bound. tagged-union payload captures remain open. + - [x] type-match checkpoint: type-producing bodies execute scalar/enum match statements and direct type-valued match expressions in initializers, assignments and returns, including nested selected arms. ordinary checking/IR execution select the arm and validate subject/pattern types, ranges, overlap, unreachable arms and exhaustiveness; only the selected body is elaborated. generated storage, scoped locals, loop transfers, early returns, nominal cache identity, selected-body diagnostics, dependency provenance and repeatable compilation work through interpreter/native execution. all bodies remain parsed/name-resolved and execution shares the type-control bound. - [x] nested type-match checkpoint: matches select types inside constructors, array lengths, generic arguments, local annotations, callback signatures and field defaults in type-producing bodies. selection precedes ordinary expansion, preserves literal spelling/source provenance and loop snapshots, and caches nominal results before expanding recursive fields. selected-body and pattern diagnostics, failed deferred elaboration, imported producers and repeatable compilation work through interpreter/native execution; deferred selection shares the producer's type-control bound. - [x] tagged/fallible constant checkpoint: explicit `$()` expressions, generic value parameters and type-procedure inputs materialize tagged unions and both fallible channels through interpreter/native execution. preserve active tags/payloads, nested raw aggregate projections, string ownership, void/zero-cell variants, composed/payload errors, declared channel types, normalized cache identity and repeatable compilation. `catch` computes generated lengths and runtime values. untagged unions and storage-bearing active payloads remain escape diagnostics rather than retaining temporary interpreter handles. + - [x] tagged type-match checkpoint: type-producing bodies execute tagged-union matches with immutable by-value payload captures in statement, expression and nested storage positions. ordinary match checking validates every arm's captures, including grouped variants; selected payloads retain exact scalar/aggregate types and owned strings. generated storage/defaults, loop snapshots/transfers, cache identity, selected-body diagnostics, imported producers, dependency provenance and repeatable compilation work through interpreter/native execution. storage-bearing payloads retain the constant-materialization escape boundary. - [ ] remaining gate: general type-valued compile-time execution, broader compile-time match/inline expansion and reflection (`typeinfo!`, `field!`, `struct_type!`, generated type/default arrays), broader constant materialization (including untagged unions 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. diff --git a/source/compilation/compilation.test.hon b/source/compilation/compilation.test.hon index 21f7b75..7df359b 100644 --- a/source/compilation/compilation.test.hon +++ b/source/compilation/compilation.test.hon @@ -116,6 +116,15 @@ recompiles_specialized_calls_without_borrowing_previous_ir test { ` } `} `Token proc($value Packet) type { return struct {} } + `Captured proc(choice Choice) type { + ` match choice { + ` .payload |detail|: return struct { + ` values [match detail.text.len { 2: usize(2), else: usize(0) }]i16 + ` bias i16 = match choice { .payload |selected|: selected.values[1], .empty: 0 } + ` }, + ` .empty: return bool, + ` } + `} `main proc() i16 { ` if $(i16 != i16) { return $(i8(128)) } ` values :: $(data()) @@ -124,6 +133,8 @@ recompiles_specialized_calls_without_borrowing_previous_ir test { ` same Fixed(2) :: fixed ` packet :: $(packet_data()) ` if (Token(packet_data()) != Token(packet)) return 0 + ` captured Captured(packet.choices[0]) :: { values = [19, 21] } + ` if (captured.values[0] + captured.values[1] + captured.bias != 42) return 0 ` payload :: packet.choices[0].payload ` if (payload.values[0] != 40 or payload.values[1] != 2 or payload.text.len != 2) return 0 ` match packet.choices[1] { .empty: {}, .payload: return 0 } diff --git a/source/sema/specialize/specialize.hon b/source/sema/specialize/specialize.hon index 6142188..cca3637 100644 --- a/source/sema/specialize/specialize.hon +++ b/source/sema/specialize/specialize.hon @@ -1117,6 +1117,14 @@ type_match_arm proc(state @mut State, id ast.NodeId) usize ! Error { for (0..ast.match_arm_count(&state.tree, id)) |i| { arm_id :: ast.match_arm(&state.tree, id, i) arm :: ast.match_arm_view(&state.tree, arm_id) + # Selector bodies omit source capture uses, but must retain their resolver failures. + if (arm.capture) |capture| { + resolved :: binding(state, capture) + if (resolved.kind == .invalid) { + state.evaluation_diagnostic = resolved.data.diagnostic + return no_index + } + } if (i > 0 and arm.expression != ast.match_arm_view(&state.tree, ast.match_arm(&state.tree, id, 0)).expression) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, @@ -1144,6 +1152,48 @@ type_match_arm proc(state @mut State, id ast.NodeId) usize ! Error { return usize(selected.magnitude) } +# Match checking has already validated every capture. Copy the active payload +# out of owned constant storage, never an address in the temporary interpreter. +@hide:file +type_match_capture proc(state @mut State, id ast.NodeId) ast.NodeId ! Error { + subject :: ast.match_expr(&state.tree, id).subject + annotation :: try expression_type(state, subject, 0) + value :: try evaluate_ir(state, subject, annotation) + if (value.kind != .aggregate or ast.expr_kind(&state.tree, value.node) != .record_literal) return no_node + payload :: ast.field(&state.tree, ast.record_field(&state.tree, value.node, 0)) + for (0..ast.record_field_count(&state.tree, annotation)) |i| { + field :: ast.field(&state.tree, ast.record_field(&state.tree, annotation, i)) + if (field.name != null and payload.name != null + and ast.name_id(&state.tree, field.name?) == ast.name_id(&state.tree, payload.name?)) { + result :: try ast.add_typed_constant(state.builder, + ast.node_span(&state.tree, subject), field.type?, payload.value?) + try refresh(state) + return result + } + } + return no_node +} + +@hide:file +type_match_expression_body proc(state @mut State, id ast.NodeId, arm ast.MatchArmView) ast.NodeId ! Error { + if (!arm.expression) return no_node + if (arm.capture == null) return arm.body + value :: try type_match_capture(state, id) + if (value == no_node) return no_node + execution := TypeExecution{ + locals = arraylist.init(state.allocator), + mapping = try mem.alloc(ast.NodeId, state.allocator, ast.node_count(&state.tree)), + first_owned = ast.NodeId(0), + } + defer arraylist.deinit(&execution.locals) + defer mem.free(state.allocator, execution.mapping) + mem.set(execution.mapping, no_node) + try arraylist.append(&execution.locals, TypeLocal{ + declaration = arm.capture?, value = value, annotation = ast.typed_constant(&state.tree, value).type, + }) + return try type_snapshot(state, &execution, arm.body) +} + @hide:file type_value_expression proc(state @mut State, input ast.NodeId) ast.NodeId ! Error { expression := input @@ -1152,8 +1202,8 @@ type_value_expression proc(state @mut State, input ast.NodeId) ast.NodeId ! Erro index :: try type_match_arm(state, expression) if (index == no_index) return no_node arm :: ast.match_arm_view(&state.tree, ast.match_arm(&state.tree, expression, index)) - if (!arm.expression or arm.capture != null) return no_node - expression = arm.body + expression = try type_match_expression_body(state, expression, arm) + if (expression == no_node) return no_node } return expression } @@ -1334,10 +1384,18 @@ execute_type_statement proc(state @mut State, execution @mut TypeExecution, stat index :: try type_match_arm(state, snapshot) if (index == no_index) return .invalid arm :: ast.match_arm_view(&state.tree, ast.match_arm(&state.tree, expression, index)) - if (arm.capture != null) return .invalid + saved :: execution.locals.items.len + defer execution.locals.items = execution.locals.items[..saved] + if (arm.capture) |capture| { + value :: try type_match_capture(state, snapshot) + if (value == no_node) return .invalid + try arraylist.append(&execution.locals, TypeLocal{ + declaration = capture, value = value, annotation = ast.typed_constant(&state.tree, value).type, + }) + } if (!arm.expression) return try execute_type_block(state, execution, arm.body) - value :: try type_value_expression(state, ast.match_arm_view(&state.tree, - ast.match_arm(&state.tree, snapshot, index)).body) + body :: try type_snapshot(state, execution, arm.body) + value :: try type_value_expression(state, body) if (value == no_node) return .invalid annotation :: try expression_type(state, value, 0) if (try type_execution_value(state, value, annotation) == no_node) return .invalid diff --git a/tests/native.py b/tests/native.py index 7aa64bd..cbfcf57 100644 --- a/tests/native.py +++ b/tests/native.py @@ -11,6 +11,133 @@ HONEY = ROOT / "build/honey" # Explicit expectations guard against frontend bugs shared by both execution paths. CASES = { + "type_match_tagged_capture_preserves_resolution_errors": ( + """ + data proc() [2]i16 { return [40, 2] } + Choice :: union(enum) { payload struct { values [2]i16 }, empty void } + Storage proc(input Choice) type { + return match input { + .payload |data|: struct { bias i16 = data.values[1] }, + .empty: bool, + } + } + main proc() Storage(.payload{ values = [40, 2] }) { return {} } + """, b"", b"duplicate name", + ), + "type_match_tagged_capture_statements_and_cache_identity": ( + """ + Tag :: enum(i16) { empty = -7, second = 3, count = 90 } + Choice :: union(Tag) { count usize, second usize, empty void } + choice proc() Choice { return .second{2} } + Storage proc(input Choice) type { + Result := i8 + match input { + .count, .second |length|: { + Result = struct { values [length]i16 } + } + .empty: return struct { values [0]i16 }, + } + return Result + } + main proc() i16 { + value Storage(choice()) :: { values = [40, 2] } + cached Storage(.second{2}) :: value + empty Storage(.empty) :: { values = [] } + return cached.values[0] + cached.values[1] + i16(empty.values.len) + } + """, b"42\n", None, + ), + "type_match_tagged_capture_nested_expressions_and_storage": ( + """ + Inner :: union(enum) { count usize, empty void } + Detail :: struct { inner Inner, wide bool, bias i16, text string } + Choice :: union(enum) { detail Detail, unused i8 } + Box proc(T type) type { return struct { value T } } + Storage proc(input Choice) type { + Element :: match input { + .detail |detail|: match detail.wide { true: i16, false: i8 }, + .unused |small|: [small]bool, + } + return struct { + values [match input { + .detail |detail|: match detail.inner { .count |n|: n, .empty: usize(0) }, + .unused |small|: usize(small), + }]Element + boxed Box(match input { .detail |detail|: Element, .unused: bool }) + bias i16 = match input { .detail |detail|: detail.bias, .unused: i8(128) } + label string = match input { .detail |detail|: detail.text, .unused: "" } + } + } + main proc() i16 { + value Storage(.detail{ inner = .count{2}, wide = true, bias = 2, text = "owned" }) :: { + values = [19, 21], boxed = { value = 0 }, + } + if (value.label.len != 5 or value.label[0] != 111) return -1 + return value.values[0] + value.values[1] + value.boxed.value + value.bias + } + """, b"42\n", None, + ), + "type_match_tagged_capture_loop_snapshots_and_transfers": ( + """ + Choice :: union(enum) { count usize, empty void } + Nest proc() type { + Result := i16 + current Choice := .count{0} + for (0..usize(4)) |index| { + current = .count{index} + match current { + .count |length|: { + if (length == 0) continue + if (length == 3) break + Result = [length]Result + } + .empty: return bool, + } + } + return Result + } + main proc() i16 { + value Nest() :: [[40], [2]] + return value[0][0] + value[1][0] + } + """, b"42\n", None, + ), + "type_match_tagged_capture_checks_unselected_capture_types": ( + """ + Choice :: union(enum) { count usize, small i8, empty void } + Bad proc(input Choice) type { + return match input { .count, .small |value|: [value]i16, .empty: bool } + } + main proc() Bad(.empty) { return true } + """, b"", b"payload capture requires compatible nonvoid tagged union variants", + ), + "type_match_tagged_capture_preserves_exact_payload_type": ( + """ + Choice :: union(enum) { small i8, empty void } + Bad proc(input Choice) type { + match input { + .small |value|: { + next :: value + 1 + return [usize(next)]i16 + } + .empty: return bool, + } + } + main proc() Bad(.small{127}) { return [] } + """, b"", b"integer arithmetic overflow", + ), + "type_match_tagged_capture_is_immutable": ( + """ + Choice :: union(enum) { count usize } + Bad proc(input Choice) type { + match input { .count |length|: { + length = 2 + return [length]i16 + } } + } + main proc() Bad(.count{1}) { return [0, 0] } + """, b"", b"mutable local binding", + ), "nested_type_match_failure_preserves_shared_return_types": ( """ Original :: alias [2]i16 @@ -3713,6 +3840,13 @@ def check_specialization_packages(temporary, executable): } return struct { values [match length { 0: usize(0), else: length }]mut Element } } + Choice :: union(enum) { count usize, empty void } + Captured proc(T type, choice Choice) type { + return match choice { + .count |length|: struct { values [length]T }, + .empty: bool, + } + } """).strip() + "\n") (library / "functions.hon").write_text(textwrap.dedent(""" identity proc($T type, value T) T { return value } @@ -3733,8 +3867,9 @@ def check_specialization_packages(temporary, executable): main proc() i16 { imported ImportedBox :: { value = 40 } copy lib.Box(i16) :: imported + captured lib.Captured(ImportedBox, .count{2}) :: { values = [imported, copy] } fixed lib.Fixed(i16, lib.count()) :: { values = [2, 0] } - return copy.value + lib.identity(lib.first(&fixed)) + return captured.values[0].value + lib.identity(lib.first(&fixed)) } """).strip() + "\n") check(app, executable, (b"42\n", None)) @@ -3782,7 +3917,23 @@ def check_specialization_packages(temporary, executable): interpreted = run(str(HONEY), "run", str(app)) assert interpreted.stderr.startswith(f"{sizing.resolve()}:2:".encode()), outcome(interpreted) assert len(interpreted.stderr.splitlines()) == 1, outcome(interpreted) - return 4 + sizing.write_text(textwrap.dedent(""" + SizingChoice :: union(enum) { small i8 } + Bad proc() type { + input SizingChoice :: .small{127} + match input { + .small |value|: { + next :: value + 1 + return [usize(next)]i16 + } + } + } + """).strip() + "\n") + check(app, executable, (b"", b"integer arithmetic overflow")) + interpreted = run(str(HONEY), "run", str(app)) + assert interpreted.stderr.startswith(f"{sizing.resolve()}:6:".encode()), outcome(interpreted) + assert len(interpreted.stderr.splitlines()) == 1, outcome(interpreted) + return 5 def check_dependency_provenance(temporary, executable):