feat: execute nested compile-time type matches

This commit is contained in:
2026-10-27 01:03:19 +01:00
parent 85c60f2416
commit e5215c4092
7 changed files with 193 additions and 22 deletions
+3 -1
View File
@@ -100,7 +100,9 @@ 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.
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.
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.
+5 -3
View File
@@ -25,8 +25,9 @@ the same constant evaluation path. compile-time `if` selection and type comparis
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; general type-valued
execution, reflection and generic library acceptance remain open.
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.
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 +70,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.
- [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.
- [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] 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.
- [ ] 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.
+6
View File
@@ -2040,6 +2040,12 @@ replace_node proc(builder @mut Builder, target, source NodeId) void {
builder.storage.nodes.items[usize(target)] = get_node(&builder.storage, source)
}
#! selects a source node, retaining its literal spelling and diagnostic provenance.
replace_node_from_source proc(builder @mut Builder, target, source NodeId) void {
replace_node(builder, target, source)
builder.storage.spans.items[usize(target)] = get_span(&builder.storage, source)
}
#! shallow-copies kind, payload, and span; extra storage remains shared.
copy_node proc(builder @mut Builder, id NodeId) NodeId ! mem.AllocError {
node :: get_node(&builder.storage, id)
+8 -2
View File
@@ -94,7 +94,13 @@ recompiles_specialized_calls_without_borrowing_previous_ir test {
` length = length + 1
` }
` return match length {
` 2: struct { values [length]Element },
` 2: struct {
` values [match length { 2: length, else: usize(1) }](match true {
` true: Element,
` false: i8(128),
` })
` bias i16 = match true { true: 0, false: i8(128) }
` },
` else: i8(128),
` }
`}
@@ -105,7 +111,7 @@ recompiles_specialized_calls_without_borrowing_previous_ir test {
` 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[$(usize(1))]
` return identity(same.values[0]) + same.values[$(usize(1))] + same.bias
`}
,
)
+11 -3
View File
@@ -282,6 +282,7 @@ parse_statement proc(state @mut State, allow_else bool) ast.NodeId ! ParseError
or next_n_is(state, 1, .struct)
or next_n_is(state, 1, .enum) or next_n_is(state, 1, .union)
or next_n_is(state, 1, .distinct)
or next_n_is(state, 1, .match)
or next_n_is(state, 1, .at)
or next_n_is(state, 1, .question)
or (next_n_is(state, 1, .open_paren) and pointer_binding_starts(state))
@@ -800,6 +801,7 @@ parse_type_atom proc(state @mut State) ast.NodeId ! ParseError {
if next_is(state, .struct) return try parse_record_type(state)
if next_is(state, .enum) return try parse_enum_type(state)
if next_is(state, .union) return try parse_union_type(state)
if next_is(state, .match) return try parse_match(state)
if (next_is(state, .distinct) or next_is(state, .alias)) return try parse_type_wrapper(state)
token :: try consume(state, .ident)
type := try ast.add_identifier(state.builder, token.span, token.data.str_id)
@@ -846,7 +848,7 @@ parse_function_type proc(state @mut State, start ast.Span, invalid bool) ast.Nod
starts_type proc(kind lexer.TokenKind) bool {
return (
kind == .ident or kind == .open_bracket or kind == .struct
or kind == .enum or kind == .union or kind == .distinct or kind == .alias
or kind == .enum or kind == .union or kind == .distinct or kind == .alias or kind == .match
or kind == .at or kind == .star or kind == .question or kind == .open_paren or kind == .proc
)
}
@@ -1498,6 +1500,7 @@ parse_index proc(state @mut State, base ast.NodeId, start u32) ast.NodeId ! Pars
@hide:file
array_starts_type proc(state @State) bool {
depth usize := 0
curly_depth := usize(0)
for (state.next_token)..(state.tokens.len) |i| {
kind :: state.tokens[i].kind
if (kind == .open_bracket) depth += 1
@@ -1509,7 +1512,12 @@ array_starts_type proc(state @State) bool {
return starts_type(next) or next == .mut
}
}
if (kind == .open_curly or kind == .close_curly or kind == .eof) return false
if (kind == .open_curly) curly_depth += 1
if (kind == .close_curly) {
if (curly_depth == 0) return false
curly_depth -= 1
}
if (kind == .eof) return false
}
return false
}
@@ -1587,7 +1595,7 @@ named_binding_starts proc(state @State) bool {
next_is(state, .ident)
and (
next_n_is(state, 1, .double_colon) or next_n_is(state, 1, .colon_equal)
or next_n_is(state, 1, .ident) or next_n_is(state, 1, .proc)
or next_n_is(state, 1, .ident) or next_n_is(state, 1, .proc) or next_n_is(state, 1, .match)
or next_n_is(state, 1, .at)
or next_n_is(state, 1, .question)
or (next_n_is(state, 1, .open_paren) and pointer_binding_starts(state))
+56 -8
View File
@@ -67,7 +67,7 @@ deinit proc(result @mut Result) void {
while (index > 0) {
index -= 1
change :: result.changes.items[index]
ast.replace_node(result.builder, change.target, change.saved)
ast.replace_node_from_source(result.builder, change.target, change.saved)
}
arraylist.deinit(&result.changes)
arraylist.deinit(&result.constraints)
@@ -583,11 +583,14 @@ instantiate proc(state @mut State, index usize, call ast.NodeId) void ! Error {
}
}
if (is_type(state, view.return_type)) {
if (state.type_execution_depth == 0) state.type_execution_remaining = 4096
state.type_execution_depth += 1
defer state.type_execution_depth -= 1
state.evaluation_diagnostic = diagnostics.no_diagnostic
constraints_start :: state.rewrites.constraints.items.len
returned :: try return_type_value(state, view.body, ast.NodeId(clone_start))
if (returned == no_node) {
try reject_evaluation(state, call)
try reject_type_result(state, call, returned, constraints_start)
return
}
result := returned
@@ -603,9 +606,20 @@ instantiate proc(state @mut State, index usize, call ast.NodeId) void ! Error {
state.instances.items[index].type = returned
state.bindings.items[usize(declaration)] = sema.Binding{ kind = .declaration, data = sema.BindingData{ declaration = declaration } }
try arraylist.append(&state.declarations, declaration)
if (!try expand_type_matches(state, declaration)) {
try reject_type_result(state, call, result, constraints_start)
return
}
try expand(state, declaration)
constraint_count :: state.rewrites.constraints.items.len
for (constraints_start..constraint_count) |i| try expand(state, state.rewrites.constraints.items[i])
for (constraints_start..constraint_count) |i| {
constraint :: state.rewrites.constraints.items[i]
if (!try expand_type_matches(state, constraint)) {
try reject_type_result(state, call, result, constraints_start)
return
}
try expand(state, constraint)
}
} else {
runtime := arraylist.init(ast.NodeId, state.allocator)
defer arraylist.deinit(&runtime)
@@ -637,6 +651,16 @@ reject_evaluation proc(state @mut State, call ast.NodeId) void ! Error {
state.bindings.items[usize(call)] = sema.Binding{ kind = .invalid, data = sema.BindingData{ diagnostic = diagnostic } }
}
# Failed deferred elaboration must not leave unchecked type expressions in the package.
@hide:file
reject_type_result proc(state @mut State, call, result ast.NodeId, constraints_start usize) void ! Error {
try reject_evaluation(state, call)
state.evaluation_diagnostic = binding(state, call).data.diagnostic
if (result != no_node) try reject_evaluation(state, result)
for (constraints_start..(state.rewrites.constraints.items.len)) |i|
try reject_evaluation(state, state.rewrites.constraints.items[i])
}
@hide:file
evaluation_dependencies proc(
state @mut State, id ast.NodeId, flags []mut u8, declarations @mut arraylist.ArrayList(ast.NodeId),
@@ -976,9 +1000,6 @@ TypeFlow :: enum { next, returned, break, continue, invalid }
return_type_value proc(state @mut State, block, first_owned ast.NodeId) ast.NodeId ! Error {
# ponytail: snapshots recheck scalar IR; cap type-control work at 4096 steps
# until type values can share one persistent checked execution frame.
if (state.type_execution_depth == 0) state.type_execution_remaining = 4096
state.type_execution_depth += 1
defer state.type_execution_depth -= 1
execution := TypeExecution{
locals = arraylist.init(state.allocator),
mapping = try mem.alloc(ast.NodeId, state.allocator, ast.node_count(&state.tree)),
@@ -1060,6 +1081,7 @@ nominal_type_value proc(state @State, id ast.NodeId) bool {
# the same rules as runtime matching, without checking unexecuted type bodies.
@hide:file
type_match_arm proc(state @mut State, id ast.NodeId) usize ! Error {
if (!try expand_type_matches(state, ast.match_expr(&state.tree, id).subject)) return no_index
arms := arraylist.init(ast.NodeId, state.allocator)
defer arraylist.deinit(&arms)
patterns := arraylist.init(ast.NodeId, state.allocator)
@@ -1074,8 +1096,11 @@ type_match_arm proc(state @mut State, id ast.NodeId) usize ! Error {
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))
for (0..ast.match_pattern_count(&state.tree, arm_id)) |j| {
pattern :: ast.match_pattern(&state.tree, arm_id, j)
if (!try expand_type_matches(state, pattern)) return no_index
try arraylist.append(&patterns, pattern)
}
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,
@@ -1105,10 +1130,31 @@ type_value_expression proc(state @mut State, input ast.NodeId) ast.NodeId ! Erro
return expression
}
# Normalize only selected expression arms before ordinary expansion can visit them.
# Nominal results enter here after their instance declaration is cached.
@hide:file
expand_type_matches proc(state @mut State, id ast.NodeId) bool ! Error {
if (ast.node_class(&state.tree, id) == .expression and ast.expr_kind(&state.tree, id) == .match_expr) {
selected :: try type_value_expression(state, id)
if (selected == no_node) return false
resolved :: binding(state, selected)
try remember(state, id)
ast.replace_node_from_source(state.builder, id, selected)
try refresh(state)
state.bindings.items[usize(id)] = resolved
}
for (0..ast.child_count(&state.tree, id)) |i| {
if (!try expand_type_matches(state, ast.child(&state.tree, id, i))) return false
}
return true
}
@hide:file
type_execution_value proc(state @mut State, input, annotation ast.NodeId) ast.NodeId ! Error {
if (annotation == no_node) return no_node
expression :: try type_value_expression(state, input)
if (expression == no_node) return no_node
if (!try expand_type_matches(state, annotation)) 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 })
@@ -1117,6 +1163,7 @@ type_execution_value proc(state @mut State, input, annotation ast.NodeId) ast.No
try arraylist.append(&state.rewrites.constraints, constraint)
return concrete.node
}
if (!try expand_type_matches(state, expression)) return no_node
try expand(state, expression)
resolved :: binding(state, expression)
if (resolved.kind == .invalid) {
@@ -1178,6 +1225,7 @@ execute_type_statement proc(state @mut State, execution @mut TypeExecution, stat
if (declaration.type) |type| annotation = try type_snapshot(state, execution, type)
else if (nominal_type_value(state, expression)) annotation = try builtin(state, expression, .type)
else {
if (!try expand_type_matches(state, expression)) return .invalid
try expand(state, expression)
annotation = try expression_type(state, expression, 0)
if (annotation == no_node) {
+104 -5
View File
@@ -11,6 +11,100 @@ HONEY = ROOT / "build/honey"
# Explicit expectations guard against frontend bugs shared by both execution paths.
CASES = {
"nested_type_match_failure_preserves_shared_return_types": (
"""
Original :: alias [2]i16
Broken proc() type {
Unused :: struct { value match true { true: i16 } }
return Original
}
main proc() i16 {
if (false) {
bad Broken() :: [0, 0]
return bad[0]
}
good Original :: [40, 2]
return good[0] + good[1]
}
""", b"42\n", b"match is not exhaustive",
),
"nested_type_matches_in_storage_calls_and_defaults": (
"""
Box proc(T type) type { return struct { value T } }
increment proc(value i16) i16 { return value + 2 }
Storage proc(wide bool) type {
return struct {
values [match wide { true: usize(2), false: usize(1) }]mut (match wide { true: i16, false: i8 })
boxed Box(match wide { true: i16, false: i8 })
callback @proc(value match wide { true: i16, false: i8 }) match wide { true: i16, false: i8 }
bias i16 = match (match wide { true: true, false: false }) { true: 0, false: i8(128) }
enabled bool = match wide { true: true, false: false }
label []u8 = match wide { true: "ok", false: "bad" }
fraction f64 = match wide { true: 0.5, false: 0.0 }
}
}
main proc() i16 {
value Storage(true) :: { values = [20, 20], boxed = { value = 0 }, callback = increment }
cached Storage(!false) :: value
if (!cached.enabled or cached.label[0] != 111 or cached.fraction != 0.5) return -1
return cached.callback(cached.values[0] + cached.values[1]) + cached.boxed.value + cached.bias
}
""", b"42\n", None,
),
"nested_type_matches_preserve_recursive_nominal_identity": (
"""
Node proc(wide bool) type {
return struct {
value match wide { true: i16, false: i8 }
next ?@match wide { true: Node(wide), false: bool } = match wide { true: null, false: null }
}
}
main proc() i16 {
leaf Node(true) :: { value = 40 }
head Node(!false) :: { value = 2, next = &leaf }
small Node(false) :: { value = 0 }
return head.value + head.next?.value + i16(small.value)
}
""", b"42\n", None,
),
"nested_type_matches_retain_loop_snapshots": (
"""
Nest proc(depth usize) type {
Result match true { true: type, false: i8 } := i16
count match true { true: usize, false: i8 } := depth
for (0..count) |index| {
Result = [match index { 0: usize(2), else: usize(1) }](match index {
0: Result,
else: match true { true: Result, false: i8(128) },
})
}
return Result
}
main proc() i16 {
values Nest(2) :: [[40, 2]]
return values[0][0] + values[0][1]
}
""", b"42\n", None,
),
"nested_type_matches_check_patterns_before_selecting": (
"""
Bad proc() type {
return struct { value [1](match true { true: i16 }) }
}
main proc() Bad() { return {} }
""", b"", b"match is not exhaustive",
),
"nested_type_matches_check_selected_defaults": (
"""
Bad proc() type {
return struct { value i8 = match true { true: 128, false: 0 } }
}
main proc() i16 {
value Bad() :: {}
return i16(value.value)
}
""", b"", b"numeric literal is outside",
),
"type_match_enum_storage_and_cache_identity": (
"""
Mode :: enum { tiny, pair, wide }
@@ -161,9 +255,12 @@ CASES = {
"type_match_resolves_untaken_bodies": (
"""
Bad proc() type {
return match true { true: i16, false: missing }
return struct { value match true { true: i16, false: missing } }
}
main proc() i16 {
value Bad() :: { value = 42 }
return value.value
}
main proc() Bad() { return 42 }
""", b"42\n", b"unknown name",
),
"type_match_remains_bounded": (
@@ -3401,7 +3498,9 @@ def check_specialization_packages(temporary, executable):
app.mkdir(parents=True)
library.mkdir()
(library / "types.hon").write_text(textwrap.dedent("""
Box proc(T type) type { return struct { value T } }
Box proc(T type) type {
return struct { value match true { true: T, false: i8(128) } }
}
Selected proc(wide bool) type {
return match wide { true: i16, false: u8 }
}
@@ -3412,7 +3511,7 @@ def check_specialization_packages(temporary, executable):
Element = T
length = length + 1
}
return struct { values [length]mut Element }
return struct { values [match length { 0: usize(0), else: length }]mut Element }
}
""").strip() + "\n")
(library / "functions.hon").write_text(textwrap.dedent("""
@@ -3472,7 +3571,7 @@ def check_specialization_packages(temporary, executable):
assert len(interpreted.stderr.splitlines()) == 1, outcome(interpreted)
sizing.write_text(textwrap.dedent("""
Bad proc() type {
return match true { true: i16, true: i8, else: bool }
return struct { value match true { true: i16, true: i8, else: bool } }
}
""").strip() + "\n")
source.write_text(textwrap.dedent("""