diff --git a/README.md b/README.md index 955176c..fe7d315 100644 --- a/README.md +++ b/README.md @@ -100,9 +100,11 @@ the specialization checkpoint supports type arguments and constant integer, bool type-producing bodies support mutable type and value locals, direct binding assignment, scoped blocks, conditional/early returns, integer range and while loops, break/continue and nested/recursive type-producing calls. for example, start with `Result := T`, repeat `Result = [2]Result` in a loop, then `return Result` to generate nested array storage. snapshots retain each local's current value without changing the source AST; scalar operations reuse checking, typed IR and interpreter execution, preserving exact types and overflow diagnostics. type-control execution is limited to 4096 statement/loop-guard steps shared with nested producers; individual scalar evaluations retain the interpreter limits below. only executed bodies are elaborated, while every body is still parsed and name-resolved. field/index assignment, optional captures, cleanup and type-valued aggregate storage in type-producing bodies remain outside this checkpoint. +type-producing bodies also execute scalar/enum `match` statements and direct 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 nested inside type constructors/defaults, tagged-union payload captures and broader generic-body match specialization remain open. + 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 match/`inline` expansion, reflection, generated type/default arrays and existing generic-library acceptance remain open. +`$(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, broader compile-time match/`inline` expansion, reflection, generated type/default arrays and existing generic-library acceptance remain open. `if $(condition) { ... } else { ... }` evaluates a boolean guard before expanding or checking either body, including calls to ordinary constant-input procedures. concrete generic specializations also select branches whose guards use constants, immutable constant bindings, type comparisons or substituted compile-time parameters. only the selected body is specialized and checked; both bodies are still parsed and name-resolved. selected bodies retain their scope, cleanup and return behavior, and erased guards retain ordinary type checking and source diagnostics. ordinary calls in an implicit guard stay runtime; use `$()` to request their evaluation. optional captures remain runtime, and ordinary nongeneric `if (true)` still checks both bodies. diff --git a/TODO.md b/TODO.md index e718a61..56e7d1d 100644 --- a/TODO.md +++ b/TODO.md @@ -24,9 +24,9 @@ using mutable locals, loops and recursive calls; 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 and bounded integer-range/while -loops now generate types and storage; general type-valued execution, reflection -and generic library acceptance remain open. +type/value locals, direct assignments, branches, scalar/enum matching and bounded +integer-range/while loops now generate types and storage; 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 @@ -69,7 +69,8 @@ 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. - - [ ] remaining gate: general type-valued compile-time execution, compile-time match/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. + - [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. matches nested inside type constructors/defaults and tagged-union payload captures remain open. + - [ ] 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 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/compilation/compilation.test.hon b/source/compilation/compilation.test.hon index 035ad8a..71c06fb 100644 --- a/source/compilation/compilation.test.hon +++ b/source/compilation/compilation.test.hon @@ -90,10 +90,13 @@ recompiles_specialized_calls_without_borrowing_previous_ir test { ` Element := i8 ` length usize := 0 ` while (length < N) { - ` Element = i16 + ` match length { 0: Element = i16, else: Element = i16 } ` length = length + 1 ` } - ` return struct { values [length]Element } + ` return match length { + ` 2: struct { values [length]Element }, + ` else: i8(128), + ` } `} `data proc() [2]i16 { return [40, 2] } `main proc() i16 { diff --git a/source/sema/specialize/specialize.hon b/source/sema/specialize/specialize.hon index 23c6e8e..80ff095 100644 --- a/source/sema/specialize/specialize.hon +++ b/source/sema/specialize/specialize.hon @@ -1056,8 +1056,59 @@ nominal_type_value proc(state @State, id ast.NodeId) bool { return kind == .record_type or kind == .union_type or kind == .enum_type or kind == .distinct_type } +# Select with ordinary checked IR so patterns, overlap and exhaustiveness have +# the same rules as runtime matching, without checking unexecuted type bodies. @hide:file -type_execution_value proc(state @mut State, expression, annotation ast.NodeId) ast.NodeId ! Error { +type_match_arm proc(state @mut State, id ast.NodeId) usize ! Error { + arms := arraylist.init(ast.NodeId, state.allocator) + defer arraylist.deinit(&arms) + patterns := arraylist.init(ast.NodeId, state.allocator) + defer arraylist.deinit(&patterns) + 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) + 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, + .type_mismatch, ast.node_span(&state.tree, arm_id)) + return no_index + } + patterns.items = patterns.items[..0] + for (0..ast.match_pattern_count(&state.tree, arm_id)) |j| + try arraylist.append(&patterns, ast.match_pattern(&state.tree, arm_id, j)) + span :: ast.node_span(&state.tree, arm_id) + index :: try ast.add_integer(state.builder, span, u64(i)) + selector :: try ast.add_match_arm(state.builder, span, patterns.items, + arm.capture, index, true, arm.catch_all) + try refresh(state) + try arraylist.append(&arms, selector) + } + selector :: try ast.add_match(state.builder, ast.node_span(&state.tree, id), + ast.match_expr(&state.tree, id).subject, arms.items) + try refresh(state) + selected :: try evaluate_ir(state, selector, try builtin(state, id, .usize)) + if (selected.kind != .integer) return no_index + return usize(selected.magnitude) +} + +@hide:file +type_value_expression proc(state @mut State, input ast.NodeId) ast.NodeId ! Error { + expression := input + while (ast.expr_kind(&state.tree, expression) == .match_expr) { + if (!try type_step(state, expression)) return no_node + 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 + } + return expression +} + +@hide:file +type_execution_value proc(state @mut State, input, annotation ast.NodeId) ast.NodeId ! Error { + expression :: try type_value_expression(state, input) + if (expression == no_node) return no_node # A recursive record needs its cached declaration before expanding its fields. if (is_type(state, annotation) and nominal_type_value(state, expression)) { concrete :: try materialize(state, Value{ kind = .type, node = expression }) @@ -1120,7 +1171,9 @@ execute_type_statement proc(state @mut State, execution @mut TypeExecution, stat } .declaration: { declaration :: ast.statement_decl(&state.tree, statement) - expression :: try type_snapshot(state, execution, declaration.initializer) + snapshot :: try type_snapshot(state, execution, declaration.initializer) + expression :: try type_value_expression(state, snapshot) + if (expression == no_node) return .invalid annotation := no_node if (declaration.type) |type| annotation = try type_snapshot(state, execution, type) else if (nominal_type_value(state, expression)) annotation = try builtin(state, expression, .type) @@ -1198,6 +1251,21 @@ execute_type_statement proc(state @mut State, execution @mut TypeExecution, stat if (flow == .break) return .next } } + .expression: { + expression :: ast.statement_expr(&state.tree, statement) + if (ast.expr_kind(&state.tree, expression) != .match_expr) return .invalid + snapshot :: try type_snapshot(state, execution, expression) + 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 + 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) + 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 + } .for_stmt: return try execute_type_for(state, execution, statement) .block_stmt: return try execute_type_block(state, execution, ast.statement_block(&state.tree, statement)) .break_stmt, .continue_stmt: { diff --git a/tests/native.py b/tests/native.py index 43ec795..564db4e 100644 --- a/tests/native.py +++ b/tests/native.py @@ -11,6 +11,171 @@ HONEY = ROOT / "build/honey" # Explicit expectations guard against frontend bugs shared by both execution paths. CASES = { + "type_match_enum_storage_and_cache_identity": ( + """ + Mode :: enum { tiny, pair, wide } + pair proc() Mode { return .pair } + Storage proc(mode Mode) type { + Result := i8 + match mode { + .tiny: Result = i8, + .pair, .wide: { + Element :: i16 + Result = [2]Element + } + } + return struct { values Result } + } + main proc() i16 { + value Storage(.pair) :: { values = [40, 2] } + cached Storage(pair()) :: value + tiny Storage(.tiny) :: { values = 1 } + return cached.values[0] + cached.values[1] + i16(tiny.values - 1) + } + """, b"42\n", None, + ), + "type_match_ranges_transfers_and_mutable_subject": ( + """ + Storage proc() type { + Result := i8 + size usize := 0 + for (0..usize(5)) |index| { + match index { + 0: continue, + 1..=2: { + Result = [1]Result + size = size + 1 + } + else: break, + } + } + match size { + 0..2: return bool, + else: return struct { values Result, bias i16 = i16(size) }, + } + } + main proc() i16 { + value Storage() :: { values = [[40]] } + return i16(value.values[0][0]) + value.bias + } + """, b"42\n", None, + ), + "type_match_type_expressions_and_untaken_bodies": ( + """ + Select proc(wide bool) type { + Result := match wide { + true: match wide { true: i16, false: i8(128) }, + false: i8, + } + Result = match wide { true: [2]Result, false: bool } + return match wide { + true: struct { values Result }, + false: struct { value Result }, + } + } + main proc() i16 { + wide Select(true) :: { values = [40, 2] } + narrow Select(false) :: { value = true } + if (narrow.value) return wide.values[0] + wide.values[1] + return 0 + } + """, b"42\n", None, + ), + "type_match_float_and_unsigned_boundaries": ( + """ + Select proc() type { + Result := i8 + match u64(18446744073709551615) { + 0..18446744073709551615: Result = bool, + 18446744073709551615: Result = i16, + else: Result = i8, + } + match -1.5 { + (-2.0)..(-1.0): return Result, + else: return bool, + } + } + main proc() Select() { return 42 } + """, b"42\n", None, + ), + "type_match_checks_nonexhaustive_patterns": ( + """ + Bad proc() type { + match true { true: return i16 } + return i8 + } + main proc() Bad() { return 42 } + """, b"", b"match is not exhaustive", + ), + "type_match_checks_overlapping_patterns": ( + """ + Bad proc() type { + match u8(2) { + 0..=2: return i16, + 2..4: return i8, + else: return bool, + } + } + main proc() Bad() { return 42 } + """, b"", b"duplicate or overlapping match pattern", + ), + "type_match_checks_unreachable_arms": ( + """ + Bad proc() type { + return match true { else: i16, false: i8 } + } + main proc() Bad() { return 42 } + """, b"", b"match arm is unreachable", + ), + "type_match_checks_pattern_types": ( + """ + Bad proc() type { + match u8(2) { true: return i16, else: return i8 } + } + main proc() Bad() { return 42 } + """, b"", b"incompatible value type", + ), + "type_match_checks_subject_types": ( + """ + Bad proc() type { + return match "text" { else: i16 } + } + main proc() Bad() { return 42 } + """, b"", b"match subject", + ), + "type_match_checks_selected_body": ( + """ + Bad proc() type { + match false { + true: return i16, + false: { + count u8 := 255 + count = count + 1 + return [count]i16 + } + } + } + main proc() Bad() { return 42 } + """, b"", b"integer arithmetic overflow", + ), + "type_match_resolves_untaken_bodies": ( + """ + Bad proc() type { + return match true { true: i16, false: missing } + } + main proc() Bad() { return 42 } + """, b"42\n", b"unknown name", + ), + "type_match_remains_bounded": ( + """ + Bad proc() type { + while (true) { + match true { true: continue, false: return i16 } + } + } + main proc() Bad() { return 42 } + """, b"", b"compile-time execution exceeds its recursion or step limit", + ), "type_execution_retains_existing_nominal_type_identity": ( """ Original :: struct { value i16 } @@ -3237,6 +3402,9 @@ def check_specialization_packages(temporary, executable): library.mkdir() (library / "types.hon").write_text(textwrap.dedent(""" Box proc(T type) type { return struct { value T } } + Selected proc(wide bool) type { + return match wide { true: i16, false: u8 } + } Fixed proc($T type, N usize) type { Element := i8 length usize := 0 @@ -3261,7 +3429,7 @@ def check_specialization_packages(temporary, executable): source = app / "main.hon" source.write_text(textwrap.dedent(""" lib :: import "../library" - Element :: alias i16 + Element :: alias lib.Selected(true) ImportedBox :: alias lib.Box(Element) main proc() i16 { imported ImportedBox :: { value = 40 } @@ -3302,7 +3470,20 @@ def check_specialization_packages(temporary, executable): interpreted = run(str(HONEY), "run", str(app)) assert interpreted.stderr.startswith(f"{sizing.resolve()}:3:12: ".encode()), outcome(interpreted) assert len(interpreted.stderr.splitlines()) == 1, outcome(interpreted) - return 3 + sizing.write_text(textwrap.dedent(""" + Bad proc() type { + return match true { true: i16, true: i8, else: bool } + } + """).strip() + "\n") + source.write_text(textwrap.dedent(""" + lib :: import "../library" + main proc() lib.Bad() { return 42 } + """).strip() + "\n") + check(app, executable, (b"", b"duplicate or overlapping match pattern")) + 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 def check_dependency_provenance(temporary, executable):