feat: expand composed error-set inline match arms

This commit is contained in:
2026-10-28 13:33:20 +01:00
parent 5cf9e15661
commit c6acff91fd
5 changed files with 278 additions and 19 deletions
+123 -12
View File
@@ -291,6 +291,21 @@ expand proc(state @mut State, id ast.NodeId) void ! Error {
}
}
}
if (class == .expression and ast.expr_kind(&state.tree, id) == .catch_expr) {
expression :: ast.catch_expr(&state.tree, id)
if (expression.capture) |capture| {
try expand(state, expression.operand)
annotation :: try expression_type(state, expression.operand, 0)
if (annotation != no_node and ast.expr_kind(&state.tree, annotation) == .fallible_type) {
hashmap.put(&state.capture_types, capture, ast.binary_expr(&state.tree, annotation).right) 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 (
@@ -707,6 +722,90 @@ expand_struct_type proc(state @mut State, id ast.NodeId) void ! Error {
try expand(state, id)
}
# composed struct variants use declaration IDs internally, but generated syntax
# must not embed those declarations and make dependency discovery treat them as owned.
@hide:file
member_name proc(state @mut State, name ast.NodeId) ast.NodeId ! Error {
if (ast.node_class(&state.tree, name) != .statement) return name
result :: try ast.add_identifier(state.builder, ast.node_span(&state.tree, name), ast.name_id(&state.tree, name))
try refresh(state)
return result
}
# flatten source member names and payload annotations only; ordinary checking still
# validates composition, variant identity and collisions on the original sum type.
@hide:file
inline_sum_fields proc(state @mut State, input ast.NodeId, fields @mut arraylist.ArrayList(ast.NodeId)) bool ! Error {
if (!try type_step(state, input)) return false
type :: try canonical_type(state, input)
if (type == no_node) return false
kind :: ast.expr_kind(&state.tree, type)
if (kind == .sum_type) {
sum :: ast.binary_expr(&state.tree, type)
if (!try inline_sum_fields(state, sum.left, fields)) return false
return try inline_sum_fields(state, sum.right, fields)
}
if (kind == .record_type) {
for (0..ast.node_count(&state.tree)) |i| {
declaration :: ast.NodeId(i)
if (ast.node_class(&state.tree, declaration) != .statement
or ast.stmt_kind(&state.tree, declaration) != .declaration) continue
if (ast.statement_decl(&state.tree, declaration).initializer != type) continue
name :: try member_name(state, declaration)
field :: try ast.add_field(state.builder, ast.node_span(&state.tree, type), name, type, null)
try refresh(state)
try arraylist.append(fields, field)
return true
}
return false
}
if (kind != .enum_type and kind != .union_type) return false
count :: if (kind == .enum_type) ast.enum_member_count(&state.tree, type)
else ast.record_field_count(&state.tree, type)
for (0..count) |i| {
if (!try type_step(state, input)) return false
member :: if (kind == .enum_type) ast.enum_member(&state.tree, type, i)
else ast.record_field(&state.tree, type, i)
field :: ast.field(&state.tree, member)
if (field.name == null) return false
if (kind == .union_type and field.type == null) return false
duplicate := false
for (fields.items) |previous| {
if (ast.name_id(&state.tree, ast.field(&state.tree, previous).name?)
== ast.name_id(&state.tree, field.name?)) duplicate = true
}
if (duplicate) continue
payload :: if (kind == .enum_type) try builtin(state, member, .void) else field.type?
generated :: try ast.add_field(state.builder, field.span, field.name, payload, null)
try refresh(state)
try arraylist.append(fields, generated)
}
return true
}
@hide:file
inline_match_shape proc(state @mut State, annotation ast.NodeId) ast.NodeId ! Error {
if (ast.expr_kind(&state.tree, annotation) != .sum_type) return annotation
if (state.type_execution_depth == 0) state.type_execution_remaining = 4096
state.type_execution_depth += 1
defer state.type_execution_depth -= 1
fields := arraylist.init(ast.NodeId, state.allocator)
defer arraylist.deinit(&fields)
if (!try inline_sum_fields(state, annotation, &fields)) return no_node
tagged := false
for (fields.items) |member| {
payload :: try canonical_type(state, ast.field(&state.tree, member).type?)
if (payload == no_node) return no_node
resolved :: binding(state, payload)
if (resolved.kind != .builtin or resolved.data.builtin != .void) tagged = true
}
span :: ast.node_span(&state.tree, annotation)
result :: if (tagged) try ast.add_union_type(state.builder, span, null, true, fields.items)
else try ast.add_enum_type(state.builder, span, null, fields.items)
try refresh(state)
return result
}
# 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
@@ -752,16 +851,18 @@ expand_inline_match proc(state @mut State, id, subject_type ast.NodeId) void ! E
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) members_type = try inline_match_shape(state, members_type)
if (members_type == no_node) {
try reject(state, id, .invalid_inline_match_subject)
if (state.evaluation_diagnostic != diagnostics.no_diagnostic) try reject_evaluation(state, id)
else try reject(state, id, .invalid_sum_type)
return
}
tagged := false
if (kind == .union_type) {
view :: ast.union_type(&state.tree, annotation)
if (!tag_only and ast.expr_kind(&state.tree, members_type) == .union_type) {
view :: ast.union_type(&state.tree, members_type)
tagged = view.auto_tag or view.tag != null
}
if (kind != .enum_type and !tag_only and !tagged) {
if (ast.expr_kind(&state.tree, members_type) != .enum_type and !tag_only and !tagged) {
try reject(state, id, .invalid_inline_match_subject)
return
}
@@ -791,8 +892,9 @@ expand_inline_match proc(state @mut State, id, subject_type ast.NodeId) void ! E
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)
union_view :: ast.union_type(&state.tree, members_type)
if (kind != .sum_type and union_view.tag != null)
tag_type = try canonical_type(state, union_view.tag?)
else {
concrete :: try materialize(state, Value{ kind = .type, node = annotation })
tag_type = try ast.add_union_tag_type(state.builder, span, concrete.node)
@@ -900,7 +1002,10 @@ specialize_match proc(state @mut State, id, statement ast.NodeId) bool ! Error {
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) {
annotation = try expression_type(state, subject, 0)
if (annotation != no_node) annotation = try inline_match_shape(state, annotation)
}
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
@@ -1521,14 +1626,17 @@ serialize_value proc(
for (record.fields) |field, i| {
if (record.is_union and !ir.union_active(module, value, i)) continue
if (record.is_union and field.type.kind == .void) {
result = try ast.add_enum_literal(state.builder, span, field.name?)
name :: try member_name(state, field.name?)
result = try ast.add_enum_literal(state.builder, span, name)
try refresh(state)
return result
}
child :: ir.aggregate_item(module, value, i)
node :: try serialize_value(state, module, checked, field.type, child, span)
if (node == no_node) return no_node
generated :: try ast.add_field(state.builder, span, field.name, null, node)
name := field.name
if (name != null) name = try member_name(state, name?)
generated :: try ast.add_field(state.builder, span, name, null, node)
try refresh(state)
try arraylist.append(&fields, generated)
}
@@ -1556,7 +1664,8 @@ serialize_value proc(
magnitude :: if (negative) u64(-(value.data.integer + 1)) + 1 else value.data.unsigned
for (enumeration.members) |member| {
if (member.magnitude == magnitude and member.negative == negative) {
result = try ast.add_enum_literal(state.builder, span, member.name)
name :: try member_name(state, member.name)
result = try ast.add_enum_literal(state.builder, span, name)
break
}
}
@@ -2096,8 +2205,10 @@ type_match_capture proc(state @mut State, id ast.NodeId) ast.NodeId ! Error {
value :: try evaluate_ir(state, subject, annotation)
if (value.kind != .aggregate or ast.expr_kind(&state.tree, value.node) != .record_literal) return no_node
payload :: ast.field(&state.tree, ast.record_field(&state.tree, value.node, 0))
for (0..ast.record_field_count(&state.tree, annotation)) |i| {
field :: ast.field(&state.tree, ast.record_field(&state.tree, annotation, i))
shape :: try inline_match_shape(state, annotation)
if (shape == no_node) return no_node
for (0..ast.record_field_count(&state.tree, shape)) |i| {
field :: ast.field(&state.tree, ast.record_field(&state.tree, shape, i))
if (
field.name != null and payload.name != null
and ast.name_id(&state.tree, field.name?) == ast.name_id(&state.tree, payload.name?)