feat: support optional-capture conditional expressions

Reuse canonical matches for optional presence and exact by-value payload captures across checking, lowering and compile-time selection.

Snapshot the optional-match flag before selector construction: AST additions can reallocate storage and invalidate the borrowed tree until refresh.

Verified 268 bootstrap tests and 908 interpreter/native programs at -O0 and -O2, plus native allocator, LLVM and CLI checks.
This commit is contained in:
2026-10-29 22:20:48 +01:00
parent 46fc7f4c46
commit 9838b253ee
9 changed files with 293 additions and 27 deletions
+33 -7
View File
@@ -4118,6 +4118,9 @@ collect_match proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | dia
state.cells[usize(capture)].local = true
binding :: sema.binding(state.resolution, capture)
if (binding.kind == .invalid) state.cells[usize(capture)].type = invalid_type(binding.data.diagnostic)
if (ast.match_expr(state.tree, id).optional_capture and i == 0) {
state.cells[usize(subject)].capture = capture
}
try depend(state, capture, arm.body)
}
saved_arm :: state.narrowing_arm
@@ -4135,12 +4138,21 @@ collect_match proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | dia
@hide:file
infer_match proc(state @mut State, id ast.NodeId, defaults bool) Type ! (mem.AllocError | diagnostics.CapacityError) {
subject :: try infer_shape(state, ast.match_expr(state.tree, id).subject, defaults)
view :: ast.match_expr(state.tree, id)
subject :: try infer_shape(state, view.subject, defaults)
if (subject.kind == .invalid) {
seed(state, id, subject)
return subject
}
key := subject
if (view.optional_capture) {
if (subject.kind != .none and subject.kind != .optional) {
try fail(state, id, .condition_not_bool, ast.node_span(state.tree, view.subject))
seed(state, id, state.types[usize(id)])
return state.types[usize(id)]
}
if (subject.kind == .optional) key = Type{ kind = .bool }
}
tagged := false
if (subject.kind == .record) {
descriptor :: state.records.items[usize(subject.record_type)]
@@ -4151,7 +4163,7 @@ infer_match proc(state @mut State, id ast.NodeId, defaults bool) Type ! (mem.All
if (tagged) key = descriptor.tag_type
}
if (
subject.kind != .none and !numeric_kind(subject.kind) and subject.kind != .bool
!view.optional_capture and subject.kind != .none and !numeric_kind(subject.kind) and subject.kind != .bool
and !(subject.kind == .enumeration and subject.nominal == ast.NodeId(maxval!(u32))) and !tagged
) {
try fail(state, id, .invalid_match_subject, ast.node_span(state.tree, id))
@@ -4167,6 +4179,10 @@ infer_match proc(state @mut State, id ast.NodeId, defaults bool) Type ! (mem.All
arm :: ast.match_arm_view(state.tree, arm_id)
payload := Type{}
capture_valid := tagged and !arm.catch_all
if (view.optional_capture and subject.kind == .optional and i == 0) {
payload = state.arrays.items[usize(subject.array_type)].element
capture_valid = true
}
for 0..ast.match_pattern_count(state.tree, arm_id) |j| {
pattern :: ast.match_pattern(state.tree, arm_id, j)
kind :: ast.expr_kind(state.tree, pattern)
@@ -4191,7 +4207,7 @@ infer_match proc(state @mut State, id ast.NodeId, defaults bool) Type ! (mem.All
}
}
if (arm.capture) |capture| {
if (key.kind != .none and (!capture_valid or payload.kind == .void or payload.kind == .none)) {
if (key.kind != .none and (!capture_valid or (!view.optional_capture and payload.kind == .void) or payload.kind == .none)) {
try fail(state, id, .invalid_match_capture, ast.node_span(state.tree, capture))
} else if (payload.kind != .none and state.cells[usize(capture)].type.kind != .invalid) {
seed(state, capture, payload)
@@ -4317,9 +4333,17 @@ interval_overlap proc(left, right PatternInterval) bool {
@hide:file
validate_match proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
subject :: state.types[usize(ast.match_expr(state.tree, id).subject)]
view :: ast.match_expr(state.tree, id)
subject :: state.types[usize(view.subject)]
if (subject.kind == .invalid) return
key := subject
if (view.optional_capture) {
if (subject.kind != .optional) {
try fail(state, id, .condition_not_bool, ast.node_span(state.tree, view.subject))
return
}
key = Type{ kind = .bool }
}
if (subject.kind == .record) key = state.records.items[usize(subject.record_type)].tag_type
if (key.kind != .enumeration and !numeric_kind(key.kind) and key.kind != .bool) {
try fail(state, id, .invalid_match_subject, ast.node_span(state.tree, id))
@@ -4724,9 +4748,11 @@ matching_narrowing_arm proc(state @State, id ast.NodeId) ast.NodeId {
arm := state.cells[usize(id)].narrowing_arm
while (arm != ast.NodeId(maxval!(u32))) {
owner :: state.cells[usize(arm)].match_owner
subject :: ast.match_expr(state.tree, owner).subject
original :: sema.binding(state.resolution, subject)
if (original.kind == .declaration and original.data.declaration == binding.data.declaration) return arm
view :: ast.match_expr(state.tree, owner)
if (!view.optional_capture) {
original :: sema.binding(state.resolution, view.subject)
if (original.kind == .declaration and original.data.declaration == binding.data.declaration) return arm
}
arm = state.cells[usize(arm)].enclosing_arm
}
return ast.NodeId(maxval!(u32))
+26 -8
View File
@@ -1335,14 +1335,22 @@ specialize_match proc(state @mut State, id, statement ast.NodeId) bool ! Error {
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
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| {
if (annotation == no_node) break
payload := no_node
if (ast.match_expr(&state.tree, id).optional_capture) {
if (ast.expr_kind(&state.tree, annotation) != .optional_type) break
payload = ast.type_operand(&state.tree, annotation)
} else {
if (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| payload = type
}
if (payload != no_node) {
hashmap.put(&state.capture_types, arm.capture?, payload) catch |err| {
match err {
.key_exists: unreachable
else: return err
@@ -2508,6 +2516,7 @@ expand_inline_for proc(state @mut State, id ast.NodeId) void ! Error {
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
optional_capture :: ast.match_expr(&state.tree, id).optional_capture
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)
@@ -2581,6 +2590,8 @@ type_match_arm proc(state @mut State, id ast.NodeId) usize ! Error {
}
selector :: try ast.add_match(state.builder, ast.node_span(&state.tree, id),
dispatch, arms.items)
if (optional_capture)
ast.set_match_optional(state.builder, selector)
try refresh(state)
# selector arms contain only indices. do not specialize this synthetic match
# again while its checked evaluation expands the wrapper.
@@ -2595,6 +2606,13 @@ type_match_arm proc(state @mut State, id ast.NodeId) usize ! Error {
@hide:file
type_match_capture proc(state @mut State, id ast.NodeId) ast.NodeId ! Error {
subject :: ast.match_expr(&state.tree, id).subject
if (ast.match_expr(&state.tree, id).optional_capture) {
unwrapped :: try ast.add_unary(state.builder,
ast.node_span(&state.tree, subject), .unwrap, subject)
try refresh(state)
child :: try expression_type(state, unwrapped, 0)
return try type_execution_value(state, unwrapped, child)
}
annotation :: try expression_type(state, subject, 0)
value :: if (type_record_storage(state, annotation, 0))
try evaluate_expected(state, subject, annotation) else try evaluate_ir(state, subject, annotation)