feat: expand nominal inline match arms

This commit is contained in:
2026-10-28 12:39:28 +01:00
parent dc7b2e40e2
commit 5cf9e15661
13 changed files with 908 additions and 49 deletions
+36 -7
View File
@@ -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: {
+81
View File
@@ -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)
+4
View File
@@ -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",
+1 -1
View File
@@ -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))
+51 -26
View File
@@ -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
+76
View File
@@ -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)
}
}
+17 -2
View File
@@ -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
+2 -1
View File
@@ -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)
}
+41
View File
@@ -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)
}
+293 -7
View File
@@ -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)