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:
+24
-4
@@ -7,7 +7,7 @@
|
||||
#! * decl ..... -> identifier [type] binding expr | identifier 'c_func' '(' parameters ')' type | ['test'] [identifier '::'] 'import' string
|
||||
#! * parameters -> [['$'] identifier {',' ['$'] identifier} type {',' ['$'] identifier {',' ['$'] identifier} type} [',']]; '$' marks each compile-time parameter independently.
|
||||
#! '$(' expr ')' is an explicit compile-time expression with primary-expression precedence.
|
||||
#! boolean value expressions also accept 'if' guard expr 'else' expr, canonicalized to matches; else is required.
|
||||
#! value expressions also accept 'if' guard ['|' identifier '|'] expr 'else' expr, canonicalized to matches; else is required.
|
||||
#! * block .... -> '{' {stmt} '}'
|
||||
#! * arm ...... -> block | stmt (single statements become canonical blocks)
|
||||
#! * match_arm -> (expr {',' expr} ['|' identifier '|'] | 'else') ':' (block | stmt | expr); arms are comma/newline separated.
|
||||
@@ -207,6 +207,7 @@ NodeData :: union {
|
||||
@hide:file foreign_type_flag u8 :: 0b0010
|
||||
@hide:file variadic_flag u8 :: 0b0100
|
||||
@hide:file opaque_flag u8 :: 0b1000
|
||||
@hide:file optional_match_flag u8 :: 0b0001_0000
|
||||
|
||||
@hide:file
|
||||
Node :: struct {
|
||||
@@ -348,8 +349,17 @@ FieldView :: struct {
|
||||
}
|
||||
|
||||
EnumTypeView :: struct { backing ?NodeId }
|
||||
UnionTypeView :: struct { tag ?NodeId, auto_tag bool }
|
||||
MatchView :: struct { subject NodeId }
|
||||
|
||||
UnionTypeView :: struct {
|
||||
tag ?NodeId,
|
||||
auto_tag bool,
|
||||
}
|
||||
|
||||
MatchView :: struct {
|
||||
subject NodeId,
|
||||
optional_capture bool = false,
|
||||
}
|
||||
|
||||
MatchArmView :: struct {
|
||||
capture ?NodeId
|
||||
body NodeId
|
||||
@@ -833,6 +843,13 @@ add_match proc(builder @mut Builder, span Span, subject NodeId, arms []NodeId) N
|
||||
}, span)
|
||||
}
|
||||
|
||||
#! marks the canonical presence match emitted for an optional-capture conditional.
|
||||
set_match_optional proc(builder @mut Builder, id NodeId) void {
|
||||
node :: &builder.storage.nodes.items[usize(id)]
|
||||
debug.assert(node.kind == .expr_match)
|
||||
node.flags = node.flags | optional_match_flag
|
||||
}
|
||||
|
||||
add_match_arm proc(
|
||||
builder @mut Builder, span Span, patterns []NodeId, capture ?NodeId,
|
||||
body NodeId, expression, catch_all bool,
|
||||
@@ -1747,7 +1764,10 @@ enum_literal proc(tree @Tree, id NodeId) NodeId {
|
||||
match_expr proc(tree @Tree, id NodeId) MatchView {
|
||||
node :: get_node(&tree.storage, id)
|
||||
debug.assert(node.kind == .expr_match)
|
||||
return MatchView{ subject = node.main.node_id }
|
||||
return MatchView{
|
||||
subject = node.main.node_id,
|
||||
optional_capture = (node.flags & optional_match_flag) != 0,
|
||||
}
|
||||
}
|
||||
|
||||
match_arm_count proc(tree @Tree, id NodeId) usize {
|
||||
|
||||
@@ -528,8 +528,17 @@ recompiles_generated_records_without_borrowing_previous_ir test {
|
||||
` [child, 2, "owned"],
|
||||
` )
|
||||
`}
|
||||
`Captured proc(value ?i16) type {
|
||||
` return if value |payload| struct { value i16 = payload } else bool
|
||||
`}
|
||||
`selected proc($value ?i16) i16 {
|
||||
` return if $(value) |payload| payload else 7
|
||||
`}
|
||||
`main proc() i16 {
|
||||
` parent Parent(40) :: {}
|
||||
` captured Captured(40) :: {}
|
||||
` absent Captured(null) :: false
|
||||
` if (absent or captured.value != selected(40) or selected(null) != 7) return 0
|
||||
` same Parent(40) :: parent
|
||||
` child Child(40) :: same.child
|
||||
` if (Child(40) == Child(41) or Parent(40) == Parent(41)) return 0
|
||||
|
||||
@@ -1043,6 +1043,7 @@ lower_match proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError |
|
||||
subject_type :: typecheck.type_of(state.checked, view.subject)
|
||||
tagged :: subject_type.kind == .record
|
||||
discriminant := subject
|
||||
if (view.optional_capture) discriminant = try optional_present(state, subject, subject_type, span)
|
||||
if (tagged) {
|
||||
tag_type :: typecheck.record_type(state.checked, subject_type.record_type).tag_type
|
||||
discriminant = try emit(state, ir.Instruction{
|
||||
@@ -1095,6 +1096,7 @@ lower_match proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError |
|
||||
if (arm_view.capture) |capture| {
|
||||
capture_type :: typecheck.type_of(state.checked, capture)
|
||||
captured := subject
|
||||
if (view.optional_capture) captured = try optional_payload(state, subject, subject_type, span)
|
||||
if (tagged) captured = try emit(state, ir.Instruction{
|
||||
op = .field, type = lower_type(state, capture_type), array_type = try array_shape(state, capture_type),
|
||||
left = subject, slot = u32(typecheck.union_variant_index(state.checked, pattern)), span = ast.node_span(tree, pattern),
|
||||
|
||||
@@ -465,6 +465,7 @@ parse_if proc(state @mut State) ast.NodeId ! ParseError {
|
||||
parse_if_expr proc(state @mut State) ast.NodeId ! ParseError {
|
||||
keyword :: try consume(state, .if)
|
||||
condition :: try parse_guard(state)
|
||||
capture :: try parse_optional_capture(state)
|
||||
then_value :: try parse_expr(state)
|
||||
skip_newlines(state)
|
||||
if (!next_is(state, .else) or next_n_is(state, 1, .colon)) return .unexpected_token
|
||||
@@ -476,18 +477,20 @@ parse_if_expr proc(state @mut State) ast.NodeId ! ParseError {
|
||||
end = ast.builder_span(state.builder, else_value).end,
|
||||
source = keyword.span.source,
|
||||
}
|
||||
# canonical boolean matches share inference, specialization and branch execution.
|
||||
# canonical matches share inference, specialization and branch execution.
|
||||
patterns [1]ast.NodeId :: [try ast.add_boolean(state.builder, keyword.span, true)]
|
||||
then_arm :: try ast.add_match_arm(
|
||||
state.builder, ast.builder_span(state.builder, then_value),
|
||||
patterns[..], null, then_value, true, false,
|
||||
patterns[..], capture, then_value, true, false,
|
||||
)
|
||||
else_arm :: try ast.add_match_arm(
|
||||
state.builder, ast.builder_span(state.builder, else_value),
|
||||
&[], null, else_value, true, true,
|
||||
)
|
||||
arms [2]ast.NodeId :: [then_arm, else_arm]
|
||||
return try ast.add_match(state.builder, span, condition, arms[..])
|
||||
result :: try ast.add_match(state.builder, span, condition, arms[..])
|
||||
if (capture != null) ast.set_match_optional(state.builder, result)
|
||||
return result
|
||||
}
|
||||
|
||||
@hide: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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user