diff --git a/README.md b/README.md index 4423387..7c151eb 100644 --- a/README.md +++ b/README.md @@ -136,7 +136,7 @@ ordinary procedures may compute integer, boolean, float and string compile-time type `==` and `!=` comparisons in compile-time expressions and specializations preserve aliases, nominal declaration identity and structural pointer/array/callback signatures. composed error-set identity and specialization caching ignore member ordering, grouping and repetition. for example, `read proc($T type, value T) i16 { if (T == i16) return value else return value.count }` checks the scalar or record path for each concrete call without requiring both operations on every `T`. -`field!(base, name)` selects a member using a compile-time string, without runtime reflection metadata. names may come from constants, constant-input procedures or erased generic parameters; only the name is evaluated at compile time, while the base retains ordinary selection semantics. records, tuples (canonical decimal names such as `"0"`), enum/error-set members, tagged payloads, callbacks and sequence `"len"`/`"ptr"` members reuse exact typing, single base evaluation, address-taking and writable-place checks. type-producing bodies also snapshot selector names for nested aggregate updates, preserving earlier by-value copies. unknown members, runtime/nonstring names and invalid calls are diagnosed; tagged payload reads still check the active field. `typeinfo!` and inline expansion remain open. +`field!(base, name)` selects a member using a compile-time string, without runtime reflection metadata. names may come from constants, constant-input procedures or erased generic parameters; only the name is evaluated at compile time, while the base retains ordinary selection semantics. records, tuples (canonical decimal names such as `"0"`), enum/error-set members, tagged payloads, callbacks and sequence `"len"`/`"ptr"` members reuse exact typing, single base evaluation, address-taking and writable-place checks. type-producing bodies also snapshot selector names for nested aggregate updates, preserving earlier by-value copies. unknown members, runtime/nonstring names and invalid calls are diagnosed; tagged payload reads still check the active field. `typeinfo!` and metadata collections remain open. `struct_type!(layout, names, types, defaults)` generates an ordinary record from constant fixed arrays or tuples of equal length. for example, `struct_type!(.auto, ["value", "bias"], [i16, i8], [null, 2])` requires a `value` initializer and supplies an `i8` default for `bias`. literal collections retain each field's context, including heterogeneous types and defaults; computed collections can come from helpers and loop-filled producer locals. names must be unique ASCII identifiers. each default is checked as `?Field`: absent means required, present supplies an owned field value. generated fields retain ordinary exact typing, aggregate copy isolation, field selection and checked updates through interpreter/native execution. producer specializations cache nominal identity; imported producers and repeated compilation retain source provenance and fresh owned values. incomplete arrays and runtime dependencies are rejected, and construction shares the type-control execution bound. @@ -190,7 +190,9 @@ enums use `Kind :: enum { number, empty }`, qualified members such as `Kind.numb untagged `Bits :: union { integer u64, real f64 }` overlays its members in shared storage. keyed construction selects exactly one member; reading another member reinterprets the same bytes. tagged unions use `union(enum)` or `union(Kind)`; an explicit enum must contain every union variant. construct them with `Choice { number = 42 }`, contextual `.number{42}`, `.point{x = 40, y = 2}` for record payloads, or `.empty` for `void` payloads. field reads/writes validate the active tag. nested aggregates copy by value, including across calls. CLI union results print `{payload}` (or `{}` for a void payload); untagged results print the first member. -`match value { .number |n|: n + 2, .empty: 0 }` selects one arm after evaluating its subject once. matches support enums, tagged unions, composed error sets, numeric/boolean scalars, comma-separated patterns, numeric ranges, final `else` arms, arm-local payload captures, statement bodies and expression results. checking rejects missing coverage, duplicate/overlapping patterns, unreachable arms and incompatible captures/results. block arms can return or transfer loop control; yielding value blocks, pointer captures and inline specialization belong to later gates. +`match value { .number |n|: n + 2, .empty: 0 }` selects one arm after evaluating its subject once. matches support enums, tagged unions, composed error sets, numeric/boolean scalars, comma-separated patterns, numeric ranges, final `else` arms, arm-local payload captures, statement bodies and expression results. checking rejects missing coverage, duplicate/overlapping patterns, unreachable arms and incompatible captures/results. block arms can return or transfer loop control; yielding value blocks and pointer captures belong to later gates. + +a final `inline |tag|:` arm on a nominal enum, or `inline |payload[, tag]|:` on a tagged union, expands into one ordinary arm for each variant not covered earlier. dispatch remains runtime and evaluates the subject once. enum captures and optional union tags are immutable compile-time enum values; payloads are immutable by-value captures with each variant's exact type, including a `void` value for empty variants. either capture may be discarded with `_`. static tag matches/conditions select variant-specific operations before checking, including generic calls, nested inline arms and type-producing procedure bodies. ordinary payloads remain runtime values, and return/break/continue/deferred cleanup retain ordinary match semantics. redundant/nonfinal inline arms, invalid captures and scalar/untagged subjects are diagnosed. expansion shares the 4096-step type-control bound. composed error-set inline arms and metadata reflection remain open. `while` repeats a block or single statement while its `bool` condition holds, re-evaluating the condition on every iteration. guards follow the same braced/braceless convention as `if`. loop bodies introduce child scopes; their locals, including locals in nested conditional arms, begin uninitialized on each scope entry. diff --git a/TODO.md b/TODO.md index 69eab53..be28f7a 100644 --- a/TODO.md +++ b/TODO.md @@ -45,6 +45,8 @@ including runtime places and type-control updates without runtime reflection. arrays or tuples, including loop-filled producer storage and owned defaults. `inline for` expands constant ranges, fixed value/type arrays, heterogeneous value/type tuples and strings with immutable exact captures and compile-time-selected exits. +final inline match arms expand nominal enums and tagged unions with static tags +and exact by-value payloads, preserving runtime dispatch and scoped control flow. general type-valued execution, metadata reflection and generic library acceptance remain open. the driver retains frontend storage and source provenance through `@source/compilation`, with compilation-wide AST node identities. @@ -104,9 +106,10 @@ test discovery and FFI remain outside these completed slices. - [x] type-record storage checkpoint: initialized records/tuples containing type values live inside type/type-array producers, including local schemas/defaults, typed/contextual/inferred construction, nested records/arrays, checked field/index reads and updates, whole-value replacement and immutable by-value collection captures. persistent snapshots isolate copies/captures while borrowed array places observe later updates; type leaves retain nominal identity and ordinary value leaves reuse checked IR. generated storage/defaults, invalid field/type/default diagnostics, readonly places, narrow overflow, imported producers/provenance, cached nominal results and repeatable compilation work through interpreter/native execution. field validation/construction shares the type-control bound. type-record procedure inputs/results are covered below; runtime type-valued records, partial record initialization, metadata reflection and inline expansion remain open. - [x] type-record procedure checkpoint: records/tuples containing type values cross compile-time procedure inputs/results, including nested arrays/records, aliases, defaults and dependent annotations. compile-time producers accept ordinary constant inputs; runtime generics accept erased `$` metadata parameters. owned snapshots isolate argument/result copies, exact type/value leaves retain checked diagnostics, and normalized cache keys preserve nominal generated types. generated storage/defaults, imported producers/provenance, shared execution bounds and repeatable compilation work through interpreter/native execution. runtime metadata storage, partial record initialization, reflection and inline expansion remain open. - [x] generated-record checkpoint: `struct_type!(layout, names, types, defaults)` builds ordinary records from constant fixed arrays or tuples with unique ASCII field names, concrete types and contextual optional defaults. literal heterogeneous collections, helper results and loop-filled producer arrays preserve exact types, owned defaults/copies and cached nominal identity through interpreter/native execution. `.auto` permits empty records; `.c` requires nonempty C-compatible fields and retains native field order/layout. malformed inputs, incomplete arrays, runtime dependencies, field/default validation, imported provenance, shared construction bounds and repeatable compilation retain diagnostics and isolation. metadata inspection, inline expansion, aggregate C calling conventions and storage-bearing defaults remain open. - - [x] inline-loop checkpoint: `inline for` expands constant integer ranges, fixed arrays (including type-valued arrays) and strings into separately checked runtime bodies through interpreter/native execution. exact immutable captures, `usize` collection indices, narrow inclusive maxima, selected conditional/tagged-match bodies, static break/continue tail pruning, nested inline/ordinary loops, runtime returns/errors and scoped cleanup retain ordinary semantics. empty loops skip body elaboration; runtime-dependent inputs/transfers, pointer captures, storage-bearing escapes and shared expansion bounds retain diagnostics. imported producers/provenance and repeatable compilation are verified. heterogeneous tuples are covered below; metadata iteration and inline-match syntax remain open. - - [x] heterogeneous inline-tuple checkpoint: `inline for` iterates constant tuples with exact per-field value/type captures and `usize` indices through interpreter/native execution. literal/helper/aliased tuples, erased generic inputs, nested tuple projections, generated storage, empty shapes, static exits, cleanup and aggregate copy isolation retain existing semantics. type-producing bodies borrow tuple places so later fields observe updates while saved copies remain isolated. runtime dependencies and storage-bearing escapes are rejected before iteration, including discarded captures and early exits; named records and ordinary tuple loops remain invalid. imported producers, nominal type leaves and repeatable compilation are verified. metadata collections and inline-match syntax remain open. - - [ ] remaining gate: general type-valued compile-time execution, inline match expansion and metadata reflection (`typeinfo!` and metadata collections), broader constant materialization (including untagged unions and storage-bearing values), plus existing-library acceptance. required expression-valued control flow, compound updates, formatting parsing and imported C-binding support may interleave with later gates. + - [x] inline-loop checkpoint: `inline for` expands constant integer ranges, fixed arrays (including type-valued arrays) and strings into separately checked runtime bodies through interpreter/native execution. exact immutable captures, `usize` collection indices, narrow inclusive maxima, selected conditional/tagged-match bodies, static break/continue tail pruning, nested inline/ordinary loops, runtime returns/errors and scoped cleanup retain ordinary semantics. empty loops skip body elaboration; runtime-dependent inputs/transfers, pointer captures, storage-bearing escapes and shared expansion bounds retain diagnostics. imported producers/provenance and repeatable compilation are verified. heterogeneous tuples and inline-match syntax are covered below; metadata iteration remains open. + - [x] heterogeneous inline-tuple checkpoint: `inline for` iterates constant tuples with exact per-field value/type captures and `usize` indices through interpreter/native execution. literal/helper/aliased tuples, erased generic inputs, nested tuple projections, generated storage, empty shapes, static exits, cleanup and aggregate copy isolation retain existing semantics. type-producing bodies borrow tuple places so later fields observe updates while saved copies remain isolated. runtime dependencies and storage-bearing escapes are rejected before iteration, including discarded captures and early exits; named records and ordinary tuple loops remain invalid. imported producers, nominal type leaves and repeatable compilation are verified. inline-match syntax is covered below; metadata collections remain open. + - [x] nominal inline-match checkpoint: final `inline |tag|:` enum arms and `inline |payload[, tag]|:` tagged-union arms expand remaining variants through interpreter/native execution. runtime subjects evaluate once; immutable static enum/tag captures specialize nested conditions, matches and generic calls while exact by-value scalar/aggregate/string/void payloads retain runtime semantics and copy isolation. statement/expression arms, ordinary loop/optional/match captures, scoped cleanup and return/break/continue work; type-producing bodies select generated storage and update outer type/value locals, including collection captures. invalid subjects/captures, redundant/nonfinal arms, ordinary pattern checks, imported provenance, shared expansion bounds and repeatable compilation retain diagnostics and ownership. composed error-set inline arms, pointer captures and metadata reflection remain open. + - [ ] remaining gate: general type-valued compile-time execution, composed error-set inline arms and metadata reflection (`typeinfo!` and metadata collections), broader constant materialization (including untagged unions and storage-bearing values), plus existing-library acceptance. required expression-valued control flow, compound updates, formatting parsing and imported C-binding support may interleave with later gates. - [ ] remaining source constructs: 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/ast/ast.hon b/source/ast/ast.hon index 9efa58f..fe1b602 100644 --- a/source/ast/ast.hon +++ b/source/ast/ast.hon @@ -62,6 +62,7 @@ ExprKind :: enum { union_type distinct_type alias_expr + union_tag_type enum_literal match_expr index @@ -148,6 +149,7 @@ NodeKind :: enum { expr_union_type expr_distinct_type expr_alias + expr_union_tag_type expr_enum_literal expr_match match_arm @@ -345,6 +347,9 @@ MatchArmView :: struct { body NodeId expression bool catch_all bool + is_inline bool = false + tag_capture ?NodeId = null + tag_capture_present bool = false } IndexView :: struct { @@ -795,6 +800,13 @@ add_type_operand proc(builder @mut Builder, span Span, operand NodeId, transpare }, span) } +add_union_tag_type proc(builder @mut Builder, span Span, operand NodeId) NodeId ! mem.AllocError { + return try add_node(builder, Node{ + kind = .expr_union_tag_type, + main = NodeData{ node_id = operand }, + }, span) +} + add_enum_literal proc(builder @mut Builder, span Span, member NodeId) NodeId ! mem.AllocError { return try add_node(builder, Node{ kind = .expr_enum_literal, @@ -823,6 +835,7 @@ add_match_arm proc( start :: try add_extra(builder, raw_capture) _ = try add_extra(builder, expression_flag) _ = try add_extra(builder, catch_all_flag) + _ = try add_extra(builder, u32(no_node)) for (patterns) |pattern| _ = try add_extra(builder, u32(pattern)) return try add_node(builder, Node{ kind = .match_arm, @@ -832,6 +845,18 @@ add_match_arm proc( }, span) } +set_match_arm_inline proc( + builder @mut Builder, id NodeId, tag_capture ?NodeId, tag_capture_present bool, +) void { + node :: get_node(&builder.storage, id) + debug.assert(node.kind == .match_arm) + extra :: usize(node.data0.extra) + builder.storage.extra.items[extra + 1] = builder.storage.extra.items[extra + 1] | 2 + if (tag_capture_present) + builder.storage.extra.items[extra + 1] = builder.storage.extra.items[extra + 1] | 4 + builder.storage.extra.items[extra + 3] = if (tag_capture) |capture| u32(capture) else u32(no_node) +} + add_record_literal proc( builder @mut Builder, span Span, type ?NodeId, fields []NodeId, ) NodeId ! mem.AllocError { @@ -1440,6 +1465,7 @@ expression_kind proc(kind NodeKind) ExprKind { .expr_union_type: .union_type .expr_distinct_type: .distinct_type .expr_alias: .alias_expr + .expr_union_tag_type: .union_tag_type .expr_enum_literal: .enum_literal .expr_match: .match_expr .expr_index: .index @@ -1664,7 +1690,7 @@ union_type proc(tree @Tree, id NodeId) UnionTypeView { type_operand proc(tree @Tree, id NodeId) NodeId { node :: get_node(&tree.storage, id) - debug.assert(node.kind == .expr_distinct_type or node.kind == .expr_alias or node.kind == .expr_optional_type or node.kind == .expr_try) + debug.assert(node.kind == .expr_distinct_type or node.kind == .expr_alias or node.kind == .expr_optional_type or node.kind == .expr_try or node.kind == .expr_union_tag_type) return node.main.node_id } @@ -1699,21 +1725,24 @@ match_arm_view proc(tree @Tree, id NodeId) MatchArmView { return MatchArmView{ capture = maybe_node_id(tree.storage.extra.items[extra]), body = node.main.node_id, - expression = tree.storage.extra.items[extra + 1] != 0, + expression = (tree.storage.extra.items[extra + 1] & 1) != 0, catch_all = tree.storage.extra.items[extra + 2] != 0, + is_inline = (tree.storage.extra.items[extra + 1] & 2) != 0, + tag_capture = maybe_node_id(tree.storage.extra.items[extra + 3]), + tag_capture_present = (tree.storage.extra.items[extra + 1] & 4) != 0, } } match_pattern_count proc(tree @Tree, id NodeId) usize { node :: get_node(&tree.storage, id) debug.assert(node.kind == .match_arm) - return usize(node.data1.extra - node.data0.extra - 3) + return usize(node.data1.extra - node.data0.extra - 4) } match_pattern proc(tree @Tree, id NodeId, index usize) NodeId { node :: get_node(&tree.storage, id) debug.assert(index < match_pattern_count(tree, id)) - return NodeId(tree.storage.extra.items[usize(node.data0.extra) + 3 + index]) + return NodeId(tree.storage.extra.items[usize(node.data0.extra) + 4 + index]) } record_field_count proc(tree @Tree, id NodeId) usize { @@ -1860,7 +1889,7 @@ node_layout proc(node Node) NodeLayout { .expr_fallible_type, .expr_sum_type, .expr_array_type, .expr_slice_type: layout.direct_children = 6 .expr_pointer_type: layout.direct_children = 4 - .expr_optional_type, .expr_distinct_type, .expr_alias, .expr_enum_literal, + .expr_optional_type, .expr_distinct_type, .expr_alias, .expr_union_tag_type, .expr_enum_literal, .expr_try, .expr_comptime: layout.direct_children = 1 .expr_call, .expr_builtin_call, .expr_function_type, .expr_array_literal, .expr_record_type, .expr_record_literal, .expr_enum_type, .expr_match, @@ -1892,8 +1921,8 @@ node_layout proc(node Node) NodeLayout { layout.extra_pointers = 6 layout.extra_start = usize(node.data0.extra) layout.extra_count = usize(node.data1.extra - node.data0.extra) - layout.extra_children = 1 - layout.list_start = 3 + layout.extra_children = 9 + layout.list_start = 4 layout.list_end = layout.extra_count } .expr_catch: { diff --git a/source/compilation/compilation.test.hon b/source/compilation/compilation.test.hon index 3b5786d..e6e2db4 100644 --- a/source/compilation/compilation.test.hon +++ b/source/compilation/compilation.test.hon @@ -415,6 +415,87 @@ recompiles_imported_inline_for_without_retaining_expansions test { try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) } +recompiles_imported_inline_match_arms_without_retaining_expansions test { + diagnostic_store := diagnostics.init(mem.c_allocator) + defer diagnostics.deinit(&diagnostic_store) + modules [2]mut ir.Module := undefined + { + session := init(mem.c_allocator) + defer deinit(&session) + root :: try add_package(&session) + dependency :: try add_package(&session) + root_bytes := try owned_text( + `dep :: import "../dependency" + `known proc($tag dep.Mode) i16 { + ` if (tag == .answer) return 0 else return false + `} + `enum_score proc(input dep.Mode) i16 { + ` return match input { .skip: 0, inline |tag|: known(tag) } + `} + `main proc() i16 { + ` values :: dep.values() + ` if (enum_score(.skip) + enum_score(dep.mode()) != 0) return -1 + ` return ( + ` dep.read(dep.Choice, values[0]) + dep.read(dep.Choice, values[1]) + ` + dep.read(dep.Choice, values[2]) + dep.read(dep.Choice, values[3]) + ` ) + `} + , + ) + defer arraylist.deinit(&root_bytes) + dependency_bytes := try owned_text( + `Mode :: enum(i16) { skip = -7, answer = 42 } + `Tag :: enum(i16) { number = -7, record = 3, text = 42, empty = 90 } + `Pair :: struct { values [2]i16 } + `Choice :: union(Tag) { text string, empty void, number i8, record Pair } + `mode proc() Mode { return .answer } + `values proc() [4]Choice { + ` return [.number{19}, .record{values = [9, 11]}, .text{"ok"}, .empty] + `} + `narrow proc(value i8) i16 { return i16(value) } + `pair proc(value Pair) i16 { return value.values[0] + value.values[1] } + `read proc($T type, input T) i16 { + ` match input { + ` inline |payload, tag|: { + ` match tag { + ` inline |selected|: { + ` if (selected == .number) { return narrow(payload) } + ` else if (selected == .record) { return pair(payload) } + ` else if (selected == .text) { return i16(payload.len) } + ` else { + ` payload + ` return 1 + ` } + ` }, + ` } + ` }, + ` } + `} + , + ) + defer arraylist.deinit(&dependency_bytes) + root_source :: try add_source(&session, root, "app/main.hon", &root_bytes, &diagnostic_store) + _ = try add_source(&session, dependency, "dependency/values.hon", &dependency_bytes, &diagnostic_store) + try arraylist.append(&session.imports, Import{ + node = declaration(&session, root_source, 0).node, + target = dependency, + }) + modules[0] = try compile(&session, root, &diagnostic_store) + errdefer ir.deinit(&modules[0]) + modules[1] = try compile(&session, root, &diagnostic_store) + } + defer ir.deinit(&modules[0]) + defer ir.deinit(&modules[1]) + try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) + for (0..modules.len) |index| { + outcome := try interpreter.run(mem.c_allocator, &modules[index], &diagnostic_store) + defer interpreter.deinit(&outcome) + try testing.expect(outcome.trap == null) + try testing.expect_equal(i64(42), outcome.value.data.integer) + } + try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) +} + recompiles_generated_records_without_borrowing_previous_ir test { diagnostic_store := diagnostics.init(mem.c_allocator) defer diagnostics.deinit(&diagnostic_store) diff --git a/source/diagnostics/diagnostics.hon b/source/diagnostics/diagnostics.hon index b44041a..5d5ce5e 100644 --- a/source/diagnostics/diagnostics.hon +++ b/source/diagnostics/diagnostics.hon @@ -88,6 +88,8 @@ Code :: enum { non_exhaustive_match unreachable_match_arm invalid_match_capture + invalid_inline_match_subject + invalid_inline_match_capture inactive_union_field invalid_pointer_type invalid_address_operand @@ -226,6 +228,8 @@ messages :: $enummap.init(Code, []u8, { non_exhaustive_match = "match is not exhaustive", unreachable_match_arm = "match arm is unreachable", invalid_match_capture = "payload capture requires compatible nonvoid tagged union variants", + invalid_inline_match_subject = "inline match arm requires an enum or tagged union", + invalid_inline_match_capture = "inline match arm requires one enum capture or one payload capture and an optional tag", inactive_union_field = "tagged union field is not active", invalid_pointer_type = "pointer requires a supported concrete pointee type", invalid_address_operand = "address-of requires addressable storage", diff --git a/source/lower/lower.hon b/source/lower/lower.hon index a8d749a..65266f9 100644 --- a/source/lower/lower.hon +++ b/source/lower/lower.hon @@ -1889,7 +1889,7 @@ lower_expression proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocEr # ranges have no physical value; only lower_for decomposes an iterable. if (!trap_type(state, id)) unreachable } - .array_type, .pointer_type, .function_type, .record_type, .enum_type, .union_type, .distinct_type, .slice_type, .optional_type, .fallible_type, .sum_type: { + .array_type, .pointer_type, .function_type, .record_type, .enum_type, .union_type, .distinct_type, .union_tag_type, .slice_type, .optional_type, .fallible_type, .sum_type: { if (!trap_type(state, id)) trap(state, try diagnostics.add(state.diagnostics, .type_not_value, span), span) } .alias_expr: return try lower_expression(state, ast.type_operand(tree, id)) diff --git a/source/parser/parse.hon b/source/parser/parse.hon index 6216500..47486d5 100644 --- a/source/parser/parse.hon +++ b/source/parser/parse.hon @@ -1048,27 +1048,39 @@ parse_match_arm proc(state @mut State) ast.NodeId ! ParseError { start :: current(state).span patterns arraylist.ArrayList(ast.NodeId) := arraylist.init(state.allocator) defer arraylist.deinit(&patterns) - catch_all :: next_is(state, .else) + is_inline :: next_is(state, .inline) + if (is_inline) state.next_token += 1 + catch_all :: !is_inline and next_is(state, .else) capture ?ast.NodeId := null + tag_capture ?ast.NodeId := null + tag_capture_present := false if (catch_all) state.next_token += 1 - else { + else if (!is_inline) { while true { try arraylist.append(&patterns, try parse_guard_expr(state)) if (!next_is(state, .comma)) break state.next_token += 1 skip_newlines(state) } - if next_is(state, .pipe) { + } + if (is_inline or (!catch_all and next_is(state, .pipe))) { + _ = try consume(state, .pipe) + # captures are by value; pointer capture tokens are not identifiers. + token :: try consume(state, .ident) + if ( + token.span.end != token.span.start + 1 + or state.builder.source[usize(token.span.start)] != '_' + ) capture = try ast.add_identifier(state.builder, token.span, token.data.str_id) + if (is_inline and next_is(state, .comma)) { state.next_token += 1 - # pointer payload captures are outside the current runtime representation. - if (next_is(state, .at) or next_is(state, .star)) return .unexpected_token - token :: try consume(state, .ident) + tag_capture_present = true + tag :: try consume(state, .ident) if ( - token.span.end != token.span.start + 1 - or state.builder.source[usize(token.span.start)] != '_' - ) capture = try ast.add_identifier(state.builder, token.span, token.data.str_id) - _ = try consume(state, .pipe) + tag.span.end != tag.span.start + 1 + or state.builder.source[usize(tag.span.start)] != '_' + ) tag_capture = try ast.add_identifier(state.builder, tag.span, tag.data.str_id) } + _ = try consume(state, .pipe) } _ = try consume(state, .colon) skip_newlines(state) @@ -1079,24 +1091,37 @@ parse_match_arm proc(state @mut State) ast.NodeId ! ParseError { .open_curly, .return, .break, .continue, .if, .while, .for, .inline, .defer, .errdefer: true else: false } + body ast.NodeId := undefined + expression := false if (statement_body or named_binding_starts(state)) { - body :: try parse_arm(state, false) - return try ast.add_match_arm(state.builder, ast.Span{ - start = start.start, end = ast.builder_span(state.builder, body).end, source = start.source, - }, patterns.items, capture, body, false, catch_all) + body = try parse_arm(state, false) + } else { + value :: try parse_expr(state) + if next_is(state, .equal) { + statement :: try parse_assignment(state, value, false) + statements [1]ast.NodeId :: [statement] + body = try ast.add_block(state.builder, ast.builder_span(state.builder, statement), statements[..]) + } else { + body = value + expression = true + } + tree :: ast.view(state.builder, value) + if (expression and ast.expr_kind(&tree, value) == .match_expr + and ast.match_arm_count(&tree, value) > 0) { + nested :: ast.match_arm_view(&tree, ast.match_arm(&tree, value, 0)) + if (!nested.expression) { + statement :: try ast.add_expression_stmt(state.builder, value) + statements [1]ast.NodeId :: [statement] + body = try ast.add_block(state.builder, ast.builder_span(state.builder, value), statements[..]) + expression = false + } + } } - value :: try parse_expr(state) - if next_is(state, .equal) { - statement :: try parse_assignment(state, value, false) - statements [1]ast.NodeId :: [statement] - body :: try ast.add_block(state.builder, ast.builder_span(state.builder, statement), statements[..]) - return try ast.add_match_arm(state.builder, ast.Span{ - start = start.start, end = ast.builder_span(state.builder, body).end, source = start.source, - }, patterns.items, capture, body, false, catch_all) - } - return try ast.add_match_arm(state.builder, ast.Span{ - start = start.start, end = ast.builder_span(state.builder, value).end, source = start.source, - }, patterns.items, capture, value, true, catch_all) + arm :: try ast.add_match_arm(state.builder, ast.Span{ + start = start.start, end = ast.builder_span(state.builder, body).end, source = start.source, + }, patterns.items, capture, body, expression, catch_all) + if (is_inline) ast.set_match_arm_inline(state.builder, arm, tag_capture, tag_capture_present) + return arm } @hide:file diff --git a/source/parser/parse.test.hon b/source/parser/parse.test.hon index 5473da5..2e34f70 100644 --- a/source/parser/parse.test.hon +++ b/source/parser/parse.test.hon @@ -1899,3 +1899,79 @@ rejects_pointer_match_captures_and_preserves_siblings test { try testing.expect_equal("after", ast.source_text(&tree, ast.statement_decl(&tree, ast.package_decl(&tree, 1)).name)) try testing.expect_equal("last", ast.source_text(&tree, ast.statement_decl(&tree, ast.package_decl(&tree, 3)).name)) } + +parses_and_clones_inline_match_captures_and_body_forms test { + strings := strpool.init(mem.c_allocator) + defer strpool.deinit(&strings) + diagnostic_store := diagnostics.init(mem.c_allocator) + defer diagnostics.deinit(&diagnostic_store) + source :: + `run proc(value int) void { + ` match value { .if, .return: 1, inline |payload, tag|: payload } + ` match value { inline |_, _|: return } + ` match value { inline |_|: value = 1 } + `} + scan_state := lexer.init(mem.c_allocator) + defer lexer.deinit(&scan_state) + try lexer.scan(&scan_state, source, &strings, &diagnostic_store) + builder := ast.init_builder(mem.c_allocator, source) + defer ast.deinit_builder(&builder) + root :: try parse_source(&builder, scan_state.tokens.items, &diagnostic_store) + original :: ast.view(&builder, root) + mapping :: try mem.alloc(ast.NodeId, mem.c_allocator, ast.node_count(&original)) + defer mem.free(mem.c_allocator, mapping) + mem.set(mapping, maxval!(ast.NodeId)) + cloned :: try ast.clone_subtree(&builder, root, mapping) + tree :: ast.view(&builder, root) + try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) + roots [2]ast.NodeId :: [root, cloned] + for (roots) |package| { + body :: ast.function_decl(&tree, ast.child(&tree, package, 0)).body + matched :: ast.statement_expr(&tree, ast.block_statement(&tree, body, 0)) + ordinary :: ast.match_arm(&tree, matched, 0) + try testing.expect_equal(2, ast.match_pattern_count(&tree, ordinary)) + try testing.expect(!ast.match_arm_view(&tree, ordinary).is_inline) + arm_id :: ast.match_arm(&tree, matched, 1) + arm :: ast.match_arm_view(&tree, arm_id) + try testing.expect(arm.is_inline and arm.expression and arm.tag_capture_present) + try testing.expect_equal(0, ast.match_pattern_count(&tree, arm_id)) + try testing.expect_equal(3, ast.child_count(&tree, arm_id)) + try testing.expect_equal(arm.body, ast.child(&tree, arm_id, 0)) + try testing.expect_equal(arm.capture?, ast.child(&tree, arm_id, 1)) + try testing.expect_equal(arm.tag_capture?, ast.child(&tree, arm_id, 2)) + try testing.expect_equal("payload", ast.source_text(&tree, ast.node_span(&tree, arm.capture?))) + try testing.expect_equal("tag", ast.source_text(&tree, ast.node_span(&tree, arm.tag_capture?))) + for (usize(1)..usize(3)) |i| { + statement_match :: ast.statement_expr(&tree, ast.block_statement(&tree, body, i)) + discarded :: ast.match_arm_view(&tree, ast.match_arm(&tree, statement_match, 0)) + try testing.expect(discarded.is_inline and !discarded.expression) + try testing.expect(discarded.capture == null and discarded.tag_capture == null) + try testing.expect_equal(i == 1, discarded.tag_capture_present) + } + } +} + +rejects_malformed_inline_match_captures test { + sources [8][]u8 :: [ + "run proc() void { match value { inline ||: 1 } }", + "run proc() void { match value { inline |a, b, c|: 1 } }", + "run proc() void { match value { inline |@a|: 1 } }", + "run proc() void { match value { inline |*a|: 1 } }", + "run proc() void { match value { inline |a, @b|: 1 } }", + "run proc() void { match value { inline |a: 1 } }", + "run proc() void { match value { inline a|: 1 } }", + "run proc() void { match value { inline |a,|: 1 } }", + ] + for (sources) |source| { + strings := strpool.init(mem.c_allocator) + defer strpool.deinit(&strings) + diagnostic_store := diagnostics.init(mem.c_allocator) + defer diagnostics.deinit(&diagnostic_store) + scan_state := lexer.init(mem.c_allocator) + defer lexer.deinit(&scan_state) + try lexer.scan(&scan_state, source, &strings, &diagnostic_store) + tree := try parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store) + defer ast.deinit(&tree) + try testing.expect(diagnostics.count(&diagnostic_store) > 0) + } +} diff --git a/source/sema/check/check.hon b/source/sema/check/check.hon index 419af1a..7f07774 100644 --- a/source/sema/check/check.hon +++ b/source/sema/check/check.hon @@ -1315,7 +1315,7 @@ collect_expression proc(state @mut State, id ast.NodeId) void ! (mem.AllocError .array_type: { state.cells[usize(id)].type = invalid_type(try diagnostics.add(state.diagnostics, .type_not_value, ast.node_span(tree, id))) } - .record_type, .union_type, .enum_type, .distinct_type, + .record_type, .union_type, .enum_type, .distinct_type, .union_tag_type, .pointer_type, .function_type, .slice_type, .optional_type, .fallible_type, .sum_type: { type := try check_type_expression(state, id, state.cells[usize(id)].nominal) if (type.kind != .invalid) { @@ -3536,7 +3536,7 @@ type_syntax proc(kind ast.ExprKind) bool { kind == .record_type or kind == .union_type or kind == .enum_type or kind == .distinct_type or kind == .alias_expr or kind == .pointer_type or kind == .function_type or kind == .slice_type or kind == .optional_type - or kind == .fallible_type or kind == .sum_type + or kind == .fallible_type or kind == .sum_type or kind == .union_tag_type ) } @@ -3576,6 +3576,21 @@ check_type_expression proc(state @mut State, id ast.NodeId, nominal bool) Type ! state.cells[usize(id)].type = type return type } + if (kind == .union_tag_type) { + operand :: try check_annotation(state, ast.type_operand(state.tree, id), false) + if (operand.kind == .invalid) return operand + if (operand.kind == .record and operand.nominal == ast.NodeId(maxval!(u32))) { + descriptor :: state.records.items[usize(operand.record_type)] + if (descriptor.is_union and descriptor.tag_type.kind == .enumeration) { + tag :: if (descriptor.declared_tag_type.kind == .enumeration) + descriptor.declared_tag_type else descriptor.tag_type + state.cells[usize(id)].type = tag + return tag + } + } + return invalid_type(try diagnostics.add(state.diagnostics, .invalid_inline_match_subject, + ast.node_span(state.tree, id))) + } cached := state.cells[usize(id)].type if (cached.kind == .type_value) { cached.kind = cached.value_kind diff --git a/source/sema/resolve.hon b/source/sema/resolve.hon index 681556c..5b9bd12 100644 --- a/source/sema/resolve.hon +++ b/source/sema/resolve.hon @@ -525,7 +525,7 @@ resolve_expr proc( .literal_int, .literal_float, .literal_bool, .literal_string, .literal_null, .literal_void, .literal_undefined: {} .enum_literal: {} - .distinct_type, .alias_expr, .optional_type, .try_expr: try resolve_expr(state, scope, ast.type_operand(tree, id)) + .distinct_type, .alias_expr, .union_tag_type, .optional_type, .try_expr: try resolve_expr(state, scope, ast.type_operand(tree, id)) .enum_type: { if (ast.enum_type(tree, id).backing) |backing| try resolve_expr(state, scope, backing) for (0..ast.enum_member_count(tree, id)) |i| { @@ -544,6 +544,7 @@ resolve_expr proc( arm_scope := Scope{ names = hashmap.init(state.allocator), parent = scope } defer hashmap.deinit(&arm_scope.names) if (arm.capture) |capture| try predeclare(state, &arm_scope, capture, ast.node_span(tree, capture)) + if (arm.tag_capture) |capture| try predeclare(state, &arm_scope, capture, ast.node_span(tree, capture)) if (arm.expression) try resolve_expr(state, &arm_scope, arm.body) else try resolve_block(state, &arm_scope, arm.body) } diff --git a/source/sema/resolve.test.hon b/source/sema/resolve.test.hon index 60b9183..50563a7 100644 --- a/source/sema/resolve.test.hon +++ b/source/sema/resolve.test.hon @@ -931,3 +931,44 @@ canonicalizes_imported_aliases_and_namespace_alias_selections test { namespace :: ast.package_decl_at(&tree, root, 3) try testing.expect_equal(BindingKind.namespace, binding(&resolution, namespace).kind) } + +resolves_inline_match_captures_and_all_bodies test { + pool := strpool.init(mem.c_allocator) + defer strpool.deinit(&pool) + diagnostic_store := diagnostics.init(mem.c_allocator) + defer diagnostics.deinit(&diagnostic_store) + source :: + `inspect proc(value int) void { + ` match value { inline |payload, tag|: { + ` payload + ` tag + ` } } + ` match value { inline |same, same|: same } + ` match value { inline |payload, value|: value } + ` match value { .if: 1, inline |_|: missing } + `} + scan_state := lexer.init(mem.c_allocator) + defer lexer.deinit(&scan_state) + try lexer.scan(&scan_state, source, &pool, &diagnostic_store) + tree := try parser.parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store) + defer ast.deinit(&tree) + try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) + resolution := try resolve(mem.c_allocator, &tree, &pool, &diagnostic_store) + defer deinit(&resolution) + try testing.expect_equal(3, diagnostics.count(&diagnostic_store)) + body :: ast.function_decl(&tree, ast.package_decl(&tree, 0)).body + first_match :: ast.statement_expr(&tree, ast.block_statement(&tree, body, 0)) + first :: ast.match_arm_view(&tree, ast.match_arm(&tree, first_match, 0)) + payload_ref :: ast.statement_expr(&tree, ast.block_statement(&tree, first.body, 0)) + tag_ref :: ast.statement_expr(&tree, ast.block_statement(&tree, first.body, 1)) + try testing.expect_equal(first.capture?, binding(&resolution, payload_ref).data.declaration) + try testing.expect_equal(first.tag_capture?, binding(&resolution, tag_ref).data.declaration) + for (usize(1)..usize(3)) |i| { + matched :: ast.statement_expr(&tree, ast.block_statement(&tree, body, i)) + arm :: ast.match_arm_view(&tree, ast.match_arm(&tree, matched, 0)) + try testing.expect_equal(BindingKind.invalid, binding(&resolution, arm.tag_capture?).kind) + } + last_match :: ast.statement_expr(&tree, ast.block_statement(&tree, body, 3)) + last :: ast.match_arm_view(&tree, ast.match_arm(&tree, last_match, 1)) + try testing.expect_equal(BindingKind.invalid, binding(&resolution, last.body).kind) +} diff --git a/source/sema/specialize/specialize.hon b/source/sema/specialize/specialize.hon index cd85205..860bc48 100644 --- a/source/sema/specialize/specialize.hon +++ b/source/sema/specialize/specialize.hon @@ -88,6 +88,7 @@ State :: struct { instances arraylist.ArrayList(Instance) cache hashmap.AutoHashMap(usize, usize) expanded arraylist.ArrayList(u8) + capture_types hashmap.AutoHashMap(ast.NodeId, ast.NodeId) diagnostics @mut diagnostics.Store depth usize = 0 original_count usize @@ -123,6 +124,7 @@ elaborate proc( if (class == .expression and ast.expr_kind(&original, id) == .comptime_expr) needed = true if (class == .statement and ast.stmt_kind(&original, id) == .for_stmt and ast.for_stmt(&original, id).is_inline) needed = true + if (class == .match_arm and ast.match_arm_view(&original, id).is_inline) needed = true if (class == .expression and ast.expr_kind(&original, id) == .builtin_call) { name :: ast.builtin_call(&original, id).name text :: ast.source_text(&original, ast.node_span(&original, name)) @@ -145,6 +147,7 @@ elaborate proc( allocator = allocator, builder = builder, tree = ast.view(builder, root), strings = strings, bindings = arraylist.init(allocator), declarations = arraylist.init(allocator), instances = arraylist.init(allocator), cache = hashmap.init(allocator), + capture_types = hashmap.init(allocator), expanded = arraylist.init(allocator), diagnostics = diagnostic_store, original_count = ast.node_count(&original), rewrites = &result, @@ -155,6 +158,7 @@ elaborate proc( defer arraylist.deinit(&state.declarations) defer arraylist.deinit(&state.expanded) defer hashmap.deinit(&state.cache) + defer hashmap.deinit(&state.capture_types) defer { for (state.instances.items) |instance| mem.free(allocator, instance.arguments) arraylist.deinit(&state.instances) @@ -238,6 +242,55 @@ expand proc(state @mut State, id ast.NodeId) void ! Error { try expand_inline_for(state, id) return } + if (class == .statement and ast.stmt_kind(&state.tree, id) == .for_stmt) { + loop :: ast.for_stmt(&state.tree, id) + try expand(state, loop.iterable) + if (!loop.pointer_capture and loop.capture != null) { + iterable :: try expression_type(state, loop.iterable, 0) + element := no_node + if (ast.expr_kind(&state.tree, loop.iterable) == .range) { + element = try expression_type(state, ast.range_expr(&state.tree, loop.iterable).start, 0) + } else if (iterable != no_node) { + kind :: ast.expr_kind(&state.tree, iterable) + if (kind == .array_type) element = ast.array_type(&state.tree, iterable).element + if (kind == .slice_type) element = ast.slice_type(&state.tree, iterable).element + if (kind == .pointer_type) { + array :: try canonical_type(state, ast.pointer_type(&state.tree, iterable).element) + if (array != no_node and ast.expr_kind(&state.tree, array) == .array_type) + element = ast.array_type(&state.tree, array).element + } + if (binding(state, iterable).kind == .builtin + and binding(state, iterable).data.builtin == .string) + element = try builtin(state, id, .u8) + } + if (element != no_node) { + hashmap.put(&state.capture_types, loop.capture?, element) catch |err| { + match err { + .key_exists: {} + else: return err + } + } + } + } + } + if (class == .statement + and (ast.stmt_kind(&state.tree, id) == .if_stmt or ast.stmt_kind(&state.tree, id) == .while_stmt)) { + looping :: ast.stmt_kind(&state.tree, id) == .while_stmt + capture :: if (looping) ast.while_stmt(&state.tree, id).capture else ast.if_stmt(&state.tree, id).capture + condition :: if (looping) ast.while_stmt(&state.tree, id).condition else ast.if_stmt(&state.tree, id).condition + if (capture) |name| { + annotation :: try expression_type(state, condition, 0) + if (annotation != no_node and ast.expr_kind(&state.tree, annotation) == .optional_type) { + child :: ast.type_operand(&state.tree, annotation) + hashmap.put(&state.capture_types, name, child) catch |err| { + match err { + .key_exists: {} + else: return err + } + } + } + } + } if (class == .statement and ast.stmt_kind(&state.tree, id) == .expression) { expression :: ast.statement_expr(&state.tree, id) if ( @@ -654,12 +707,215 @@ expand_struct_type proc(state @mut State, id ast.NodeId) void ! Error { try expand(state, id) } +# expand dispatch, not the subject: ordinary checking/lowering retains its single +# evaluation and by-value payload captures. only tags are compile-time constants. +@hide:file +expand_inline_match proc(state @mut State, id, subject_type ast.NodeId) void ! Error { + inline_arm := no_node + for (0..ast.match_arm_count(&state.tree, id)) |i| { + candidate :: ast.match_arm(&state.tree, id, i) + if (ast.match_arm_view(&state.tree, candidate).is_inline) { + inline_arm = candidate + break + } + } + if (inline_arm == no_node) return + 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 + arm :: ast.match_arm_view(&state.tree, inline_arm) + for (0..usize(2)) |i| { + capture :: if (i == 0) arm.capture else arm.tag_capture + if (capture) |name| { + resolved :: binding(state, name) + if (resolved.kind == .invalid) { + state.evaluation_diagnostic = resolved.data.diagnostic + try reject_evaluation(state, id) + return + } + } + } + count :: ast.match_arm_count(&state.tree, id) + if (ast.match_arm(&state.tree, id, count - 1) != inline_arm) { + try reject(state, id, .unreachable_match_arm) + return + } + subject :: ast.match_expr(&state.tree, id).subject + annotation := subject_type + if (annotation == no_node) annotation = try expression_type(state, subject, 0) + if (annotation == no_node) { + try reject(state, id, .invalid_inline_match_subject) + return + } + kind :: ast.expr_kind(&state.tree, annotation) + tag_only :: kind == .union_tag_type + members_type := annotation + if (tag_only) members_type = try canonical_type(state, ast.type_operand(&state.tree, annotation)) + if (members_type == no_node) { + try reject(state, id, .invalid_inline_match_subject) + return + } + tagged := false + if (kind == .union_type) { + view :: ast.union_type(&state.tree, annotation) + tagged = view.auto_tag or view.tag != null + } + if (kind != .enum_type and !tag_only and !tagged) { + try reject(state, id, .invalid_inline_match_subject) + return + } + if (!tagged and arm.tag_capture_present) { + try reject(state, id, .invalid_inline_match_capture) + return + } + arms := arraylist.init(ast.NodeId, state.allocator) + defer arraylist.deinit(&arms) + for (0..(count - 1)) |i| { + prior :: ast.match_arm(&state.tree, id, i) + if (ast.match_arm_view(&state.tree, prior).catch_all) { + try reject(state, id, .unreachable_match_arm) + return + } + try arraylist.append(&arms, prior) + } + 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), result_type = no_node, + } + defer arraylist.deinit(&execution.locals) + defer mem.free(state.allocator, execution.mapping) + span :: ast.node_span(&state.tree, inline_arm) + static_capture :: if (tagged) arm.tag_capture else arm.capture + tag_type := annotation + if (static_capture != null) { + if (tagged) { + union_view :: ast.union_type(&state.tree, annotation) + if (union_view.tag) |tag| tag_type = try canonical_type(state, tag) + else { + concrete :: try materialize(state, Value{ kind = .type, node = annotation }) + tag_type = try ast.add_union_tag_type(state.builder, span, concrete.node) + try refresh(state) + } + } + concrete_tag :: try materialize(state, Value{ kind = .type, node = tag_type }) + tag_type = concrete_tag.node + } + union_members :: tagged or tag_only + members :: if (union_members) ast.record_field_count(&state.tree, members_type) + else ast.enum_member_count(&state.tree, members_type) + for (0..members) |i| { + member :: if (union_members) ast.record_field(&state.tree, members_type, i) + else ast.enum_member(&state.tree, members_type, i) + field :: ast.field(&state.tree, member) + if (field.name == null or (tagged and field.type == null)) { + try reject(state, id, .invalid_record_field_type) + return + } + covered := false + for (arms.items) |prior| { + for (0..ast.match_pattern_count(&state.tree, prior)) |j| { + pattern :: ast.match_pattern(&state.tree, prior, j) + if (ast.expr_kind(&state.tree, pattern) == .enum_literal + and ast.name_id(&state.tree, ast.enum_literal(&state.tree, pattern)) + == ast.name_id(&state.tree, field.name?)) covered = true + } + } + if (covered) continue + if (!try type_step(state, inline_arm)) { + try reject_evaluation(state, id) + return + } + execution.locals.items = execution.locals.items[..0] + mem.set(execution.mapping, no_node) + literal :: try ast.add_enum_literal(state.builder, span, field.name?) + if (static_capture) |capture| { + # runtime patterns use the union's dispatch enum, not its declared tag enum. + tag_literal :: try ast.add_enum_literal(state.builder, span, field.name?) + tag :: try ast.add_typed_constant(state.builder, span, tag_type, tag_literal) + try refresh(state) + try arraylist.append(&execution.locals, TypeLocal{ + declaration = capture, value = tag, annotation = tag_type, + }) + } + payload_capture ?ast.NodeId := null + if (tagged and arm.capture != null) { + payload_type :: try canonical_type(state, field.type?) + if (payload_type == no_node) { + try reject(state, id, .invalid_record_field_type) + return + } + if (binding(state, payload_type).kind == .builtin + and binding(state, payload_type).data.builtin == .void) { + value :: try ast.add_literal(state.builder, .void, span) + try refresh(state) + try arraylist.append(&execution.locals, TypeLocal{ + declaration = arm.capture?, value = value, annotation = payload_type, + }) + } else { + name :: arm.capture? + payload_capture = try ast.add_identifier(state.builder, + ast.node_span(&state.tree, name), ast.name_id(&state.tree, name)) + try refresh(state) + state.bindings.items[usize(payload_capture?)] = sema.Binding{ + kind = .declaration, data = sema.BindingData{ declaration = payload_capture? }, + } + execution.mapping[usize(name)] = payload_capture? + hashmap.put(&state.capture_types, payload_capture?, payload_type) catch |err| { + match err { + .key_exists: unreachable + else: return err + } + } + } + } + body :: try type_snapshot(state, &execution, arm.body) + patterns [1]ast.NodeId :: [literal] + generated :: try ast.add_match_arm(state.builder, span, patterns[..], + payload_capture, body, arm.expression, false) + try refresh(state) + try arraylist.append(&arms, generated) + } + if (arms.items.len == count - 1) { + try reject(state, id, .unreachable_match_arm) + return + } + replacement :: try ast.add_match(state.builder, ast.node_span(&state.tree, id), subject, arms.items) + try replace(state, id, replacement) + try refresh(state) +} + # runtime bodies share the type-producer selector, but retain ordinary runtime # parameters and control transfers in the selected arm. @hide:file specialize_match proc(state @mut State, id, statement ast.NodeId) bool ! Error { + explicit :: ast.expr_kind(&state.tree, ast.match_expr(&state.tree, id).subject) == .comptime_expr + try expand_inline_match(state, id, no_node) + if (ast.expr_kind(&state.tree, id) != .match_expr) return true subject :: ast.match_expr(&state.tree, id).subject - explicit :: ast.expr_kind(&state.tree, subject) == .comptime_expr + annotation := no_node + 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 (explicit or arm.capture == null or ast.match_pattern_count(&state.tree, arm_id) == 0) continue + if (hashmap.get(&state.capture_types, arm.capture?) != null) continue + if (annotation == no_node) annotation = try expression_type(state, subject, 0) + if (annotation == no_node or ast.expr_kind(&state.tree, annotation) != .union_type) break + pattern :: ast.match_pattern(&state.tree, arm_id, 0) + if (ast.expr_kind(&state.tree, pattern) != .enum_literal) continue + slot :: type_record_slot(state, annotation, ast.enum_literal(&state.tree, pattern)) + if (slot == no_index) continue + field :: ast.field(&state.tree, ast.record_field(&state.tree, annotation, slot)) + if (field.type) |type| { + hashmap.put(&state.capture_types, arm.capture?, type) catch |err| { + match err { + .key_exists: unreachable + else: return err + } + } + } + } if (!explicit and usize(id) < state.original_count) return false try expand(state, subject) if (!explicit and !branch_constant(state, subject, 0)) return false @@ -753,7 +1009,7 @@ branch_constant proc(state @State, id ast.NodeId, depth usize) bool { .pointer_type: return branch_constant(state, ast.pointer_type(&state.tree, id).element, depth + 1) .optional_type, .alias_expr: return branch_constant(state, ast.type_operand(&state.tree, id), depth + 1) .record_type, .union_type, .enum_type, .distinct_type, .array_type, .slice_type, .function_type, - .fallible_type, .sum_type: return true + .fallible_type, .sum_type, .union_tag_type: return true else: return false } } @@ -1587,7 +1843,7 @@ prepare_type_snapshot proc(state @mut State, execution @mut TypeExecution, id as } @hide:file -bind_type_snapshot proc(state @mut State, execution @TypeExecution, id, start ast.NodeId) void { +bind_type_snapshot proc(state @mut State, execution @TypeExecution, id, start ast.NodeId) void ! Error { if (usize(id) >= execution.mapping.len) return target :: execution.mapping[usize(id)] if (target == no_node or target < start) return @@ -1598,8 +1854,16 @@ bind_type_snapshot proc(state @mut State, execution @TypeExecution, id, start as resolved.data.declaration = execution.mapping[usize(declaration)] } state.bindings.items[usize(target)] = resolved + if (hashmap.get(&state.capture_types, id)) |annotation| { + hashmap.put(&state.capture_types, target, annotation) catch |err| { + match err { + .key_exists: {} + else: return err + } + } + } for (0..ast.child_count(&state.tree, id)) |i| - bind_type_snapshot(state, execution, ast.child(&state.tree, id, i), start) + try bind_type_snapshot(state, execution, ast.child(&state.tree, id, i), start) } @hide:file @@ -1608,7 +1872,7 @@ type_snapshot proc(state @mut State, execution @mut TypeExecution, id ast.NodeId start :: ast.NodeId(ast.node_count(&state.tree)) result :: try ast.clone_subtree(state.builder, id, execution.mapping) try refresh(state) - bind_type_snapshot(state, execution, id, start) + try bind_type_snapshot(state, execution, id, start) return result } @@ -1768,6 +2032,8 @@ expand_inline_for proc(state @mut State, id ast.NodeId) void ! Error { # 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 { + try expand_inline_match(state, id, no_node) + if (ast.expr_kind(&state.tree, id) != .match_expr) return no_index if (!try expand_type_matches(state, ast.match_expr(&state.tree, id).subject)) return no_index try expand(state, ast.match_expr(&state.tree, id).subject) arms := arraylist.init(ast.NodeId, state.allocator) @@ -2751,6 +3017,21 @@ execute_type_statement proc(state @mut State, execution @mut TypeExecution, stat .expression: { expression :: ast.statement_expr(&state.tree, statement) if (ast.expr_kind(&state.tree, expression) != .match_expr) return .invalid + subject_type := no_node + arms :: ast.match_arm_count(&state.tree, expression) + if (arms > 0 + and ast.match_arm_view(&state.tree, ast.match_arm(&state.tree, expression, arms - 1)).is_inline) { + subject :: try type_snapshot(state, execution, ast.match_expr(&state.tree, expression).subject) + subject_type = try expression_type(state, subject, 0) + } + before :: ast.node_count(&state.tree) + try expand_inline_match(state, expression, subject_type) + if (ast.expr_kind(&state.tree, expression) != .match_expr) return .invalid + if (ast.node_count(&state.tree) != before) { + previous :: execution.mapping.len + execution.mapping = try mem.realloc(state.allocator, execution.mapping, ast.node_count(&state.tree)) + mem.set(execution.mapping[previous..], no_node) + } snapshot :: try type_snapshot(state, execution, expression) index :: try type_match_arm(state, snapshot) if (index == no_index) return .invalid @@ -3061,7 +3342,7 @@ evaluate proc(state @mut State, id ast.NodeId) Value ! Error { .literal_null: return Value{ kind = .null, node = id } .literal_void: return Value{ kind = .aggregate, node = id, annotation = try builtin(state, id, .void) } .record_type, .array_type, .slice_type, .pointer_type, .function_type, .optional_type, - .enum_type, .union_type, .distinct_type, .fallible_type, .sum_type: + .enum_type, .union_type, .union_tag_type, .distinct_type, .fallible_type, .sum_type: return Value{ kind = .type, node = id } .alias_expr: return try evaluate(state, ast.type_operand(&state.tree, id)) .unary: { @@ -3205,6 +3486,8 @@ same_type_context proc(state @mut State, left, right ast.NodeId, metadata bool) return try type_subset(state, a, b) and try type_subset(state, b, a) if (kind != ast.expr_kind(&state.tree, b)) return false match kind { + .union_tag_type: return try same_type_context(state, + ast.type_operand(&state.tree, a), ast.type_operand(&state.tree, b), metadata) .record_type: { if (!metadata and (!type_record_storage(state, a, 0) or !type_record_storage(state, b, 0))) return false if (!structural_type_record(state, a) or !structural_type_record(state, b)) return false @@ -3338,6 +3621,8 @@ type_hash_context proc(state @mut State, id ast.NodeId, metadata bool) usize ! E kind :: ast.expr_kind(&state.tree, node) hash := enum_index(kind) match kind { + .union_tag_type: return mix(hash, + try type_hash_context(state, ast.type_operand(&state.tree, node), metadata)) .record_type: { if ((!metadata and !type_record_storage(state, node, 0)) or !structural_type_record(state, node)) return mix(hash, usize(node)) @@ -3401,6 +3686,7 @@ expression_type proc(state @mut State, id ast.NodeId, depth usize) ast.NodeId ! if (resolved.kind == .builtin and resolved.data.builtin != .type) return try builtin(state, id, .type) if (resolved.kind == .declaration) { target :: resolved.data.declaration + if (hashmap.get(&state.capture_types, target)) |annotation| return try canonical_type(state, annotation) class :: ast.node_class(&state.tree, target) if (class == .parameter) { annotation :: ast.param(&state.tree, target).type @@ -3426,7 +3712,7 @@ expression_type proc(state @mut State, id ast.NodeId, depth usize) ast.NodeId ! kind :: ast.expr_kind(&state.tree, id) match kind { .record_type, .array_type, .slice_type, .pointer_type, .function_type, .optional_type, - .enum_type, .union_type, .distinct_type, .fallible_type, .sum_type, .alias_expr: + .enum_type, .union_type, .union_tag_type, .distinct_type, .fallible_type, .sum_type, .alias_expr: return try builtin(state, id, .type) .typed_constant: return try canonical_type(state, ast.typed_constant(&state.tree, id).type) .literal_int: return try builtin(state, id, .i64) diff --git a/tests/native.py b/tests/native.py index 5d0a832..c709acf 100644 --- a/tests/native.py +++ b/tests/native.py @@ -11,6 +11,290 @@ HONEY = ROOT / "build/honey" # Explicit expectations guard against frontend bugs shared by both execution paths. CASES = { + "inline_match_runtime_enum_static_capture_expression_and_single_subject": ( + """ + Mode :: enum(i16) { handled = -7, first = 3, second = 8, answer = 42, zero = 90 } + selected proc(value Mode, calls @mut i16) Mode { + calls^ = calls^ + 1 + return value + } + remaining proc($tag Mode) i16 { + if (tag == .answer) { return 42 } + else if (tag == .zero) { return 0 } + else { return false } + } + read proc(value Mode, calls @mut i16) i16 { + return match selected(value, calls) { + .handled: 0, + .first, .second: 0, + inline |tag|: remaining(tag), + } + } + main proc() [2]i16 { + calls i16 := 0 + total :: ( + read(.handled, &calls) + read(.first, &calls) + read(.second, &calls) + + read(.answer, &calls) + read(.zero, &calls) + ) + return [total, calls] + } + """, b"[42, 5]\n", None, + ), + "inline_match_generic_heterogeneous_payloads_static_tags_and_nested_selection": ( + """ + Tag :: enum(i16) { handled = -7, first = 3, second = 8, number = 42, record = 90, text = 100, empty = 120 } + Pair :: struct { values [2]i16 } + Choice :: union(Tag) { text string, empty void, record Pair, number i8, handled bool, second bool, first bool } + narrow proc($tag Tag, value i8) i16 { + if (tag == .number) return i16(value) else return false + } + pair proc(value Pair) i16 { return value.values[0] + value.values[1] } + read proc($T type, input T) i16 { + return match input { + .handled: 0, + .first, .second: 0, + inline |payload, tag|: match tag { + .number: narrow(tag, payload), + .record: pair(payload), + .text: i16(payload.len), + .empty: 1, + else: false, + }, + } + } + nested proc(input Choice) i16 { + match input { + .handled, .first, .second: return 0, + inline |payload, tag|: { + match tag { + inline |selected|: { + if (selected == .number) { return narrow(tag, payload) } + else if (selected == .record) { return pair(payload) } + else if (selected == .text) { return i16(payload.len) } + else { + payload + return 1 + } + }, + } + }, + } + } + main proc() i16 { + if (read(Choice, .handled{false}) + read(Choice, .first{true}) + read(Choice, .second{false}) != 0) return -1 + if (nested(.number{19}) + nested(.record{values = [9, 11]}) + nested(.text{"ok"}) + nested(.empty) != 42) return -2 + return ( + read(Choice, .number{19}) + read(Choice, .record{values = [9, 11]}) + + read(Choice, .text{"ok"}) + read(Choice, .empty) + ) + } + """, b"42\n", None, + ), + "inline_match_union_subject_once_and_payload_copy_isolation_without_tag": ( + """ + Pair :: struct { values [2]mut i16 } + Choice :: union(enum) { record Pair, other Pair } + exact proc(value Pair) i16 { return value.values[0] + value.values[1] } + selected proc(input @Choice, calls @mut i16) Choice { + calls^ = calls^ + 1 + return input^ + } + main proc() i16 { + input Choice := .record{values = [19, 23]} + calls i16 := 0 + match selected(&input, &calls) { + inline |payload|: { + input.record.values[0] = 0 + copied := payload + copied.values[1] = 0 + if (calls != 1 or input.record.values[0] != 0 or copied.values[1] != 0) return -1 + return exact(payload) + }, + } + } + """, b"42\n", None, + ), + "inline_match_return_break_continue_run_arm_cleanup": ( + """ + Mode :: enum { skip, add, stop } + finish proc(input Mode, cleanups @mut i16) i16 { + match input { + .skip, .stop: return 0, + inline |tag|: { + defer cleanups^ = cleanups^ + 1 + if (tag == .add) return 19 else return false + }, + } + } + main proc() i16 { + total i16 := 0 + cleanups i16 := 0 + for [Mode.skip, .add, .stop, .add] |input| { + match input { + inline |tag|: { + defer cleanups = cleanups + 1 + if (tag == .skip) { continue } + else if (tag == .stop) { break } + else { total = total + 19 } + }, + } + } + result :: finish(.add, &cleanups) + return total + result + cleanups + } + """, b"42\n", None, + ), + "inline_match_rejects_scalar_subject": ( + "main proc() i16 { return match i16(42) { inline |value|: value } }", + b"", b"inline match arm requires an enum or tagged union", + ), + "inline_match_rejects_untagged_union_subject": ( + "Choice :: union { number i16 }\nmain proc() i16 { input Choice :: { number = 42 }\nreturn match input { inline |value|: value } }", + b"", b"inline match arm requires an enum or tagged union", + ), + "inline_match_rejects_second_enum_capture": ( + "Mode :: enum { answer }\nmain proc() i16 { return match Mode.answer { inline |value, tag|: 42 } }", + b"", b"inline match arm requires one enum capture", + ), + "inline_match_rejects_redundant_remaining_arm": ( + "Mode :: enum { first, second }\nmain proc() i16 { return match Mode.first { .first, .second: 42, inline |_|: 0 } }", + b"", b"match arm is unreachable", + ), + "inline_match_rejects_following_arm": ( + "Mode :: enum { first, second }\nmain proc() i16 { return match Mode.first { inline |_|: 42, .second: 0 } }", + b"", b"match arm is unreachable", + ), + "inline_match_rejects_arm_after_else": ( + "Mode :: enum { first, second }\nmain proc() i16 { return match Mode.first { else: 42, inline |_|: 0 } }", + b"", b"match arm is unreachable", + ), + "inline_match_checks_preceding_duplicate_patterns": ( + "Mode :: enum { first, second }\nmain proc() i16 { return match Mode.first { .first: 42, .first: 0, inline |_|: 0 } }", + b"", b"duplicate or overlapping match pattern", + ), + "inline_match_rejects_duplicate_capture_names": ( + "Choice :: union(enum) { number i8 }\nmain proc() i16 { input Choice :: .number{42}\nreturn match input { inline |value, value|: 42 } }", + b"", b"duplicate name or forbidden shadowing", + ), + "inline_match_rejects_shadowed_enum_capture": ( + "Mode :: enum { answer }\nmain proc() i16 { value :: 42\nreturn match Mode.answer { inline |value|: 42 } }", + b"", b"duplicate name or forbidden shadowing", + ), + "inline_match_rejects_shadowed_tag_capture": ( + "Choice :: union(enum) { number i8 }\nmain proc() i16 { tag :: 42\ninput Choice :: .number{42}\nreturn match input { inline |value, tag|: 42 } }", + b"", b"duplicate name or forbidden shadowing", + ), + "inline_match_enum_capture_is_immutable": ( + "Mode :: enum { first, second }\nmain proc() i16 { match Mode.first { inline |value|: value = .second }\nreturn 42 }", + b"", b"assignment requires a mutable local binding", + ), + "inline_match_payload_capture_is_immutable": ( + "Choice :: union(enum) { number i8 }\nmain proc() i16 { input Choice :: .number{42}\nmatch input { inline |value|: value = 0 }\nreturn 42 }", + b"", b"assignment requires a mutable local binding", + ), + "inline_match_tag_capture_is_immutable": ( + "Choice :: union(enum) { number i8, empty void }\nmain proc() i16 { input Choice :: .number{42}\nmatch input { inline |value, tag|: tag = .empty }\nreturn 42 }", + b"", b"assignment requires a mutable local binding", + ), + "inline_match_payload_preserves_narrow_type": ( + "Choice :: union(enum) { number i8 }\nread proc(input Choice) i16 { match input { inline |value|: return value } }\nmain proc() i16 { return read(.number{42}) }", + b"", b"return does not match", + ), + "inline_match_payload_is_not_a_comptime_binding": ( + "Choice :: union(enum) { number i8 }\nknown proc($value i8) i16 { return i16(value) }\nread proc(input Choice) i16 { return match input { inline |value|: known(value) } }\nmain proc() i16 { return read(.number{42}) }", + b"", b"compile-time", + ), + "inline_match_resolves_unselected_bodies": ( + "Mode :: enum { first, second }\nmain proc() i16 { return match Mode.first { inline |tag|: match tag { .first: 42, .second: missing } } }", + b"42\n", b"unknown name", + ), + "inline_match_rejects_pointer_capture_syntax": ( + "Choice :: union(enum) { number i8 }\nmain proc() i16 { input Choice :: .number{42}\nreturn match input { inline |*value|: 42 } }", + b"", b"unexpected token", + ), + "inline_match_nested_optional_loop_and_ordinary_match_captures": ( + """ + Mode :: enum { skip, answer } + Choice :: union(enum) { mode Mode } + score proc(value Mode) i16 { + return match value { inline |tag|: match tag { .skip: 0, .answer: 14 } } + } + read proc(input ?Mode, choice Choice) i16 { + total i16 := 0 + if input |value| { + total = match value { inline |tag|: score(tag) } + } + pending ?Mode := input + while pending |value| { + total = total + match value { inline |tag|: score(tag) } + pending = null + } + match choice { + .mode |value|: { + total = total + match value { inline |tag|: score(tag) } + } + } + return total + } + main proc() i16 { return read(.answer, .mode{.answer}) + read(null, .mode{.skip}) } + """, b"42\n", None, + ), + "inline_match_type_producer_selects_generated_storage": ( + """ + Mode :: enum { small, wide } + Storage proc(input Mode) type { + match input { inline |tag|: { + if (tag == .small) { return struct { value i8 = 19 } } + else { return struct { value i16 = 23 } } + } } + } + main proc() i16 { + small Storage(.small) :: {} + wide Storage(.wide) :: {} + return i16(small.value) + wide.value + } + """, b"42\n", None, + ), + "inline_match_auto_tag_nested_expansion_infers_exact_payload_generics": ( + """ + Choice :: union(enum) { number i8, empty void } + identity proc($T type, value T) T { return value } + read proc(input Choice) i16 { + match input { inline |payload, tag|: match tag { + inline |value|: { + if (value == .number) { return i16(identity(payload)) } + else { + payload + return 2 + } + } + } } + } + main proc() i16 { return read(.number{40}) + read(.empty) } + """, b"42\n", None, + ), + "inline_match_type_producer_updates_outer_type_and_value_locals": ( + """ + Choice :: union(enum) { number u8, empty void } + Storage proc(input Choice) type { + length usize := 1 + Element := i8 + for [input] |item| { + match item { inline |payload, tag|: { + if (tag == .number) { + length = usize(payload) + Element = i16 + } else { length = 2 } + } } + } + return struct { values [length]Element } + } + main proc() i16 { + value Storage(.number{2}) :: { values = [19, 23] } + return value.values[0] + value.values[1] + } + """, b"42\n", None, + ), "inline_for_heterogeneous_tuple_exact_captures_and_nested_values": ( """ Choice :: union(enum) { number i8, empty void } @@ -6979,6 +7263,18 @@ def check_dependency_provenance(temporary, executable): return 42 } """, 5, 16, False), + (""" + Choice :: union(enum) { number int, empty void } + fail proc() int { + input Choice :: .number{9223372036854775807} + return match input { + inline |payload, tag|: match tag { + .number: payload + 1, + .empty: 0, + }, + } + } + """, 8, 22, False), (""" fail proc() int { a :: [42]