From e5215c4092bea0c347b186f6847dd61c64bb7ad7 Mon Sep 17 00:00:00 2001 From: hl-valdemar Date: Tue, 27 Oct 2026 01:03:19 +0100 Subject: [PATCH] feat: execute nested compile-time type matches --- README.md | 4 +- TODO.md | 8 +- source/ast/ast.hon | 6 ++ source/compilation/compilation.test.hon | 10 ++- source/parser/parse.hon | 14 ++- source/sema/specialize/specialize.hon | 64 ++++++++++++-- tests/native.py | 109 ++++++++++++++++++++++-- 7 files changed, 193 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index fe7d315..867086d 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/TODO.md b/TODO.md index 56e7d1d..e2cc1c9 100644 --- a/TODO.md +++ b/TODO.md @@ -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. diff --git a/source/ast/ast.hon b/source/ast/ast.hon index 427f1a5..fa9567a 100644 --- a/source/ast/ast.hon +++ b/source/ast/ast.hon @@ -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) diff --git a/source/compilation/compilation.test.hon b/source/compilation/compilation.test.hon index 71c06fb..1293281 100644 --- a/source/compilation/compilation.test.hon +++ b/source/compilation/compilation.test.hon @@ -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 `} , ) diff --git a/source/parser/parse.hon b/source/parser/parse.hon index fe1d789..01af45a 100644 --- a/source/parser/parse.hon +++ b/source/parser/parse.hon @@ -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)) diff --git a/source/sema/specialize/specialize.hon b/source/sema/specialize/specialize.hon index 80ff095..29555db 100644 --- a/source/sema/specialize/specialize.hon +++ b/source/sema/specialize/specialize.hon @@ -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) { diff --git a/tests/native.py b/tests/native.py index 564db4e..6f9cbba 100644 --- a/tests/native.py +++ b/tests/native.py @@ -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("""