void-payloads, multi-pattern arms, and range patterns (match statements)

This commit is contained in:
2026-06-29 19:52:05 +02:00
parent 462632554c
commit c82f070d55
8 changed files with 499 additions and 106 deletions
+36
View File
@@ -504,6 +504,42 @@
is not a runtime field type yet, so 21.5 can't declare them, though the no-capture arm is not a runtime field type yet, so 21.5 can't declare them, though the no-capture arm
form is already wired; and multi-pattern arms (`.a, .b:`) / range patterns form is already wired; and multi-pattern arms (`.a, .b:`) / range patterns
22.5. `void`-payloads and multi-pattern arms / range patterns in match statements (implemented; see below)
- **void-payload variants**: a tagged union may declare a `void`-payload variant
(`quit void`). As in Zig, a void field carries no runtime value — it is allowed only on a
tagged union (the record-decl check skips the runtime-value requirement for it), contributes
nothing to the layout (`size` 0 / `align` 1, so it is never the carrier), and is constructed
with the **bare-key** literal `T{ variant }` (no `= value`). Construction stores only the tag;
codegen skips the payload store. Matched with a plain no-capture arm (`.quit:`); a capture on a
void variant, a `= value` on a void variant, a bare key on a non-void field, and a direct
`x.quit` payload read are all diagnosed
- **multi-pattern arms**: an arm may list several patterns (`.a, .b:` / `0, 1, 2:`); the AST
`Match_Arm` now carries a `patterns` list and the checker ORs their dispatch conditions. A
capturing multi-pattern arm over a tagged union is allowed when every listed variant has the
same payload type (Zig parity — payloads share the carrier offset, so it is one read);
mismatched payload types are a "capture group with incompatible types" error
- **range patterns**: a scalar arm may be a range (`0..10:` / `0..=10:`), desugared to
`key >= lo and key <(=) hi` (existing `.Ge`/`.Le`/`.Lt`/`.And` HIR); a range pattern on an
enum/union subject is rejected
- **pointer captures**: `|@cap|` binds a pointer into the subject's payload (mutate in place),
reusing the for-loop `@`-capture and `pointer_capture` flag; mutability follows the subject. It
requires an addressable subject — verified to need **no lowering/codegen change**: the subject
is spilled as `&subject` and captures route through a `Deref` (`lower_location(Deref)` is the
pointee address), so `Address(Field(Deref(ptr)))` aliases the original storage
- the only new codegen is the void construction skip (one `llvm` site) plus a one-line `lower`
guard so a void variant's absent payload operand is not lowered into a trapping recovery value;
everything else is parser + checker desugar
- deferred (`// ponytail:` follow-ups): contextual void construction (`e Event = .quit`) needs
enum-literal→union coercion (milestone 23); Zig `inline .a, .b => |v|` per-tag comptime
captures need monomorphization. Separately, a **call expression directly as a match subject**
(`match get()`) is a pre-existing gap (assign to a variable first, as the spec examples do);
the rvalue pointer-capture guard is defensive for when that lands
22.6. (`// ponytail:` follow-ups): contextual void construction (`e Event = .quit`) needs
enum-literal→union coercion. Separately, a **call expression directly as a match subject**
(`match get()`) is a pre-existing gap (assign to a variable first, as the spec examples do);
the rvalue pointer-capture guard is defensive for when that lands
23. error types (see below) 23. error types (see below)
24. dynamic heap allocation 24. dynamic heap allocation
+8 -3
View File
@@ -178,10 +178,14 @@ Stmt :: struct {
// `Defer` statements use `update` as the deferred statement (which may itself // `Defer` statements use `update` as the deferred statement (which may itself
// be a `Block`). // be a `Block`).
// `Match` statements use `expr` as the subject and `body` as the list of arm // `Match` statements use `expr` as the subject and `body` as the list of arm
// statements (each a `Match_Arm`). A `Match_Arm` uses `expr` as its pattern // statements (each a `Match_Arm`). A `Match_Arm` uses `patterns` as its pattern
// (`INVALID_EXPR` marks the `else` arm), `captures` for the optional payload // list (empty marks the `else` arm; more than one is a multi-pattern arm),
// capture (0 or 1 name, tagged-union variants only), and `body` as the arm body. // `captures` for the optional payload capture (0 or 1 name, tagged-union variants
// only) with `pointer_capture` distinguishing `|@cap|` from `|cap|`, and `body`
// as the arm body.
captures: []symbol.Id, captures: []symbol.Id,
// `Match_Arm` pattern list; empty the `else` arm.
patterns: []Expr_Id,
guard: Expr_Id, guard: Expr_Id,
body: []Stmt_Id, body: []Stmt_Id,
else_body: []Stmt_Id, else_body: []Stmt_Id,
@@ -303,6 +307,7 @@ destroy_module :: proc(module: ^Module) {
} }
for statement in module.statements { for statement in module.statements {
delete(statement.captures, module.allocator) delete(statement.captures, module.allocator)
delete(statement.patterns, module.allocator)
delete(statement.body, module.allocator) delete(statement.body, module.allocator)
delete(statement.else_body, module.allocator) delete(statement.else_body, module.allocator)
} }
+224 -65
View File
@@ -788,8 +788,11 @@ mark_block_imports_used :: proc(checker: ^Checker, statements: []ast.Stmt_Id, fi
mark_block_imports_used(checker, deferred[:], file) mark_block_imports_used(checker, deferred[:], file)
case .Match, .Match_Arm: case .Match, .Match_Arm:
// `Match` carries the subject in `expr` and arms in `body`; each `Match_Arm` // `Match` carries the subject in `expr` and arms in `body`; each `Match_Arm`
// carries its pattern in `expr` and the arm body in `body`. // carries its patterns in `patterns` and the arm body in `body`.
mark_expr_imports_used(checker, statement.expr, file) mark_expr_imports_used(checker, statement.expr, file)
for pattern in statement.patterns {
mark_expr_imports_used(checker, pattern, file)
}
mark_block_imports_used(checker, statement.body, file) mark_block_imports_used(checker, statement.body, file)
case .Break, .Continue: case .Break, .Continue:
case .Invalid: case .Invalid:
@@ -1048,8 +1051,14 @@ validate_type_nodes :: proc(checker: ^Checker) {
"c_struct definitions require at least one field", "c_struct definitions require at least one field",
) )
} }
// A tagged union may carry `void`-payload variants (`.quit void`): the
// variant has no runtime value, only a tag. Allowed only here, not for
// structs, untagged unions, or c_structs.
tagged_union := item.kind == .Union && types.is_enum(item.child, &checker.module.types)
for field in types.fields_for(&checker.module.types, id) { for field in types.fields_for(&checker.module.types, id) {
if !types.is_runtime_value(field.type, &checker.module.types) { if tagged_union && types.is_void(field.type) {
// void variant: no payload to validate.
} else if !types.is_runtime_value(field.type, &checker.module.types) {
source.add( source.add(
checker.diagnostics, checker.diagnostics,
source.Span{}, source.Span{},
@@ -3202,6 +3211,10 @@ build_compound_expr :: proc(
id := source.addf(checker.diagnostics, expr.span, "unknown struct field '%s'", symbol_text(checker, expr.name)) id := source.addf(checker.diagnostics, expr.span, "unknown struct field '%s'", symbol_text(checker, expr.name))
return invalid_hir_expr(checker, expr.span, id) return invalid_hir_expr(checker, expr.span, id)
} }
if types.is_void(field.type) {
id := source.addf(checker.diagnostics, expr.span, "variant '%s' has no payload to read", symbol_text(checker, expr.name))
return invalid_hir_expr(checker, expr.span, id)
}
return add_hir_expr(checker, hir.Expr{ return add_hir_expr(checker, hir.Expr{
kind=.Field, span=expr.span, type=field.type, integer=i64(index), left=base, kind=.Field, span=expr.span, type=field.type, integer=i64(index), left=base,
target=hir.INVALID_REF, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, target=hir.INVALID_REF, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC,
@@ -3402,8 +3415,20 @@ build_compound_expr :: proc(
} }
initialized[index] = true initialized[index] = true
value_index := 0 if union_record else index value_index := 0 if union_record else index
values[value_index] = build_nested_expr(checker, keyed_expr.left, locals, global_reads, calls, field.type, pkg, file) is_void_field := union_record && types.is_void(field.type)
values[value_index] = coerce_expr(checker, values[value_index], field.type, keyed_expr.span) if keyed_expr.left == ast.INVALID_EXPR {
// Bare key `T{ variant }`: valid only to construct a void-payload
// variant (no value); the payload slot stays INVALID_EXPR.
if !is_void_field {
source.addf(checker.diagnostics, keyed_expr.span, "field '%s' requires a value", symbol_text(checker, keyed_expr.name))
}
} else {
if is_void_field {
source.addf(checker.diagnostics, keyed_expr.span, "void variant '%s' takes no value", symbol_text(checker, keyed_expr.name))
}
values[value_index] = build_nested_expr(checker, keyed_expr.left, locals, global_reads, calls, field.type, pkg, file)
values[value_index] = coerce_expr(checker, values[value_index], field.type, keyed_expr.span)
}
} }
if !union_record { if !union_record {
for field, index in fields { for field, index in fields {
@@ -3417,16 +3442,21 @@ build_compound_expr :: proc(
} }
active_field: i64 active_field: i64
if union_record { if union_record {
if values[0] == hir.INVALID_EXPR { // The active variant is the one initialized field. Its payload slot
delete(values, checker.allocator) // (`values[0]`) may legitimately be INVALID_EXPR for a void variant, so
return invalid_hir_expr(checker, expr.span, source.add(checker.diagnostics, expr.span, "union literal requires a known field"), struct_type) // detect "no field" via `initialized`, not the payload value.
} found_any := false
for value, index in initialized { for value, index in initialized {
if value { if value {
active_field = i64(index) active_field = i64(index)
found_any = true
break break
} }
} }
if !found_any {
delete(values, checker.allocator)
return invalid_hir_expr(checker, expr.span, source.add(checker.diagnostics, expr.span, "union literal requires a known field"), struct_type)
}
} }
return add_hir_expr(checker, hir.Expr{ return add_hir_expr(checker, hir.Expr{
kind=.Struct, span=expr.span, type=struct_type, args=values, integer=active_field, kind=.Struct, span=expr.span, type=struct_type, args=values, integer=active_field,
@@ -5347,6 +5377,32 @@ Match_Built_Arm :: struct {
terminal: bool, terminal: bool,
} }
// match_subject_location yields a fresh location expr for the `match` subject: a direct
// read of the value temp, or a deref of the pointer temp when an arm pointer-captures (so
// captures alias the original storage). Either way its lowered address is the subject's.
match_subject_location :: proc(checker: ^Checker, subj_local: hir.Local_Id, is_pointer: bool, subject_type, ptr_type: types.Type, span: source.Span) -> hir.Expr_Id {
if !is_pointer {
return slot_read(checker, subj_local, subject_type, span)
}
return add_hir_expr(checker, hir.Expr{
kind = .Deref, span = span, type = subject_type,
left = slot_read(checker, subj_local, ptr_type, span),
target = hir.INVALID_REF, right = hir.INVALID_EXPR, diagnostic = source.INVALID_DIAGNOSTIC,
})
}
// match_or ORs a fresh dispatch comparison into an arm's accumulating condition (for a
// multi-pattern arm), or returns it directly for the first pattern.
match_or :: proc(checker: ^Checker, condition, cmp: hir.Expr_Id, span: source.Span) -> hir.Expr_Id {
if condition == hir.INVALID_EXPR {
return cmp
}
return add_hir_expr(checker, hir.Expr{
kind = .Or, span = span, type = types.BOOL,
left = condition, right = cmp, target = hir.INVALID_REF, diagnostic = source.INVALID_DIAGNOSTIC,
})
}
// emit_match desugars a `match` into a single subject spill, one dispatch key read, and // emit_match desugars a `match` into a single subject spill, one dispatch key read, and
// an `if`/`else if` chain. `as_value` (with `slot`/`slot_type`) routes each arm body // an `if`/`else if` chain. `as_value` (with `slot`/`slot_type`) routes each arm body
// through the value-branch machinery so the construct produces a value; otherwise arm // through the value-branch machinery so the construct produces a value; otherwise arm
@@ -5374,8 +5430,10 @@ emit_match :: proc(
return false return false
} }
// 1. Subject, spilled into an addressable temp so the tag read and any payload // 1. Subject. A pointer capture (`|@cap|`) must alias the original storage, so when
// captures reference one evaluation. // any arm requests one we spill the subject's *address* (it must be an addressable
// lvalue) and route reads through a deref; otherwise we spill the value as a copy.
// Either spill evaluates the subject exactly once.
subject := build_expr(checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, types.INVALID, ctx.pkg, ctx.file) subject := build_expr(checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, types.INVALID, ctx.pkg, ctx.file)
subject_type := checker.module.exprs[subject].type subject_type := checker.module.exprs[subject].type
if checker.module.exprs[subject].kind == .Invalid { if checker.module.exprs[subject].kind == .Invalid {
@@ -5392,24 +5450,56 @@ emit_match :: proc(
"'match' subject must be a tagged union, enum, or scalar value, not '%s'", type_label(checker, subject_type))) "'match' subject must be a tagged union, enum, or scalar value, not '%s'", type_label(checker, subject_type)))
} }
ok := true
wants_pointer := false
for arm_id in statement.body {
arm := checker.ast_module.statements[arm_id]
if arm.kind == .Match_Arm && arm.pointer_capture {
wants_pointer = true
break
}
}
subj_is_pointer := false
subj_writable := false
ptr_type := types.INVALID
if wants_pointer && is_tagged {
if hir_is_location(checker, subject) {
subj_is_pointer = true
subj_writable = hir_location_writable(checker, subject, ctx.locals^[:])
ptr_type = types.pointer(store, subject_type, subj_writable, false)
} else {
source.add(checker.diagnostics, span, "a pointer capture requires an addressable 'match' subject (bind it to a variable first)")
ok = false
}
}
spill_type := ptr_type if subj_is_pointer else subject_type
spill_value := subject
if subj_is_pointer {
spill_value = add_hir_expr(checker, hir.Expr{
kind = .Address, span = span, type = ptr_type, left = subject,
target = hir.INVALID_REF, right = hir.INVALID_EXPR, diagnostic = source.INVALID_DIAGNOSTIC,
})
}
subj_local := hir.local_id(len(ctx.hir_locals^)) subj_local := hir.local_id(len(ctx.hir_locals^))
append(ctx.hir_locals, hir.Local{name = checker.sink_symbol, type = subject_type, mutable = false}) append(ctx.hir_locals, hir.Local{name = checker.sink_symbol, type = spill_type, mutable = false})
append(out, hir.stmt_id(len(checker.module.statements))) append(out, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{ append(&checker.module.statements, hir.Stmt{
kind = .Declaration, span = span, local = subj_local, expr = subject, kind = .Declaration, span = span, local = subj_local, expr = spill_value,
diagnostic = source.INVALID_DIAGNOSTIC, diagnostic = source.INVALID_DIAGNOSTIC,
}) })
// 2. Dispatch key: a tagged union reads its discriminant into its own temp; an enum // 2. Dispatch key: a tagged union reads its discriminant into its own temp; an enum
// or scalar compares the subject directly. // or scalar compares the subject directly.
key_local := subj_local key_local := subj_local
key_type := subject_type key_type := spill_type
tag_enum := types.INVALID tag_enum := types.INVALID
if is_tagged { if is_tagged {
tag_enum = types.union_tag_enum(subject_type, store) tag_enum = types.union_tag_enum(subject_type, store)
tag_read := add_hir_expr(checker, hir.Expr{ tag_read := add_hir_expr(checker, hir.Expr{
kind = .Union_Tag, span = span, type = tag_enum, kind = .Union_Tag, span = span, type = tag_enum,
left = slot_read(checker, subj_local, subject_type, span), left = match_subject_location(checker, subj_local, subj_is_pointer, subject_type, ptr_type, span),
target = hir.INVALID_REF, right = hir.INVALID_EXPR, diagnostic = source.INVALID_DIAGNOSTIC, target = hir.INVALID_REF, right = hir.INVALID_EXPR, diagnostic = source.INVALID_DIAGNOSTIC,
}) })
tag_local := hir.local_id(len(ctx.hir_locals^)) tag_local := hir.local_id(len(ctx.hir_locals^))
@@ -5423,7 +5513,8 @@ emit_match :: proc(
key_type = tag_enum key_type = tag_enum
} }
// 3. Build each arm (forward, for source-order diagnostics). // 3. Build each arm (forward, for source-order diagnostics). An arm's `patterns` may
// list several alternatives (`.a, .b:` / `0, 1:`); their conditions are OR'd.
built: [dynamic]Match_Built_Arm built: [dynamic]Match_Built_Arm
built.allocator = checker.allocator built.allocator = checker.allocator
defer delete(built) defer delete(built)
@@ -5431,7 +5522,6 @@ emit_match :: proc(
covered.allocator = checker.allocator covered.allocator = checker.allocator
defer delete(covered) defer delete(covered)
has_else := false has_else := false
ok := true
for arm_id in statement.body { for arm_id in statement.body {
arm := checker.ast_module.statements[arm_id] arm := checker.ast_module.statements[arm_id]
@@ -5443,7 +5533,7 @@ emit_match :: proc(
source.add(checker.diagnostics, arm.span, "arms after 'else' are unreachable") source.add(checker.diagnostics, arm.span, "arms after 'else' are unreachable")
ok = false ok = false
} }
is_else := arm.expr == ast.INVALID_EXPR is_else := len(arm.patterns) == 0
condition := hir.INVALID_EXPR condition := hir.INVALID_EXPR
field_index := -1 field_index := -1
payload_type := types.INVALID payload_type := types.INVALID
@@ -5456,69 +5546,123 @@ emit_match :: proc(
} }
has_else = true has_else = true
} else if is_tagged || is_enum_subject { } else if is_tagged || is_enum_subject {
pattern := checker.ast_module.exprs[arm.expr] // The capture payload (if any) must be one type across every listed variant.
if pattern.kind != .Enum_Literal { capture_field := -1
source.add(checker.diagnostics, arm.span, "an enum or tagged-union 'match' arm must be a '.variant' pattern") capture_payload := types.INVALID
ok = false capture_conflict := types.INVALID
continue member_enum := tag_enum if is_tagged else subject_type
} for pat_id in arm.patterns {
if contains_name(covered[:], pattern.name) { pattern := checker.ast_module.exprs[pat_id]
source.addf(checker.diagnostics, arm.span, "duplicate 'match' arm for '.%s'", symbol_text(checker, pattern.name)) if pattern.kind == .Range {
ok = false source.add(checker.diagnostics, arm.span, "range patterns only apply to scalar 'match' subjects")
} else {
append(&covered, pattern.name)
}
if is_tagged {
index, field, found := find_struct_field(checker, subject_type, pattern.name)
if !found {
source.addf(checker.diagnostics, arm.span, "unknown variant '.%s' on '%s'", symbol_text(checker, pattern.name), type_label(checker, subject_type))
ok = false ok = false
continue continue
} }
field_index = index if pattern.kind != .Enum_Literal {
payload_type = field.type source.add(checker.diagnostics, arm.span, "an enum or tagged-union 'match' arm must be a '.variant' pattern")
member := enum_member_hir(checker, tag_enum, pattern.name, arm.span) ok = false
condition = add_hir_expr(checker, hir.Expr{ continue
}
if contains_name(covered[:], pattern.name) {
source.addf(checker.diagnostics, arm.span, "duplicate 'match' arm for '.%s'", symbol_text(checker, pattern.name))
ok = false
} else {
append(&covered, pattern.name)
}
if is_tagged {
index, field, found := find_struct_field(checker, subject_type, pattern.name)
if !found {
source.addf(checker.diagnostics, arm.span, "unknown variant '.%s' on '%s'", symbol_text(checker, pattern.name), type_label(checker, subject_type))
ok = false
continue
}
if capture_field < 0 {
capture_field = index
capture_payload = field.type
} else if !types.equal(capture_payload, field.type) {
capture_conflict = field.type
}
} else {
if _, found := find_enum_member(checker, subject_type, pattern.name); !found {
source.addf(checker.diagnostics, arm.span, "unknown member '.%s' on '%s'", symbol_text(checker, pattern.name), type_label(checker, subject_type))
ok = false
continue
}
}
member := enum_member_hir(checker, member_enum, pattern.name, arm.span)
cmp := add_hir_expr(checker, hir.Expr{
kind = .Eq, span = arm.span, type = types.BOOL, kind = .Eq, span = arm.span, type = types.BOOL,
left = slot_read(checker, key_local, key_type, arm.span), right = member, left = slot_read(checker, key_local, key_type, arm.span), right = member,
target = hir.INVALID_REF, diagnostic = source.INVALID_DIAGNOSTIC, target = hir.INVALID_REF, diagnostic = source.INVALID_DIAGNOSTIC,
}) })
} else { condition = match_or(checker, condition, cmp, arm.span)
if has_capture { }
if has_capture {
if !is_tagged {
source.add(checker.diagnostics, arm.span, "only tagged-union variants can capture a payload") source.add(checker.diagnostics, arm.span, "only tagged-union variants can capture a payload")
ok = false ok = false
} } else if types.is_valid(capture_conflict) {
if _, found := find_enum_member(checker, subject_type, pattern.name); !found { source.addf(checker.diagnostics, arm.span, "capture group with incompatible types '%s' and '%s'",
source.addf(checker.diagnostics, arm.span, "unknown member '.%s' on '%s'", symbol_text(checker, pattern.name), type_label(checker, subject_type)) type_label(checker, capture_payload), type_label(checker, capture_conflict))
ok = false ok = false
continue } else if types.is_void(capture_payload) {
source.add(checker.diagnostics, arm.span, "this variant has a void payload; there is nothing to capture")
ok = false
} else {
field_index = capture_field
payload_type = capture_payload
} }
member := enum_member_hir(checker, subject_type, pattern.name, arm.span)
condition = add_hir_expr(checker, hir.Expr{
kind = .Eq, span = arm.span, type = types.BOOL,
left = slot_read(checker, key_local, key_type, arm.span), right = member,
target = hir.INVALID_REF, diagnostic = source.INVALID_DIAGNOSTIC,
})
} }
} else { } else {
// Scalar subject: each pattern is a literal or a range, compared to the subject.
if has_capture { if has_capture {
source.add(checker.diagnostics, arm.span, "only tagged-union variants can capture a payload") source.add(checker.diagnostics, arm.span, "only tagged-union variants can capture a payload")
ok = false ok = false
} }
pattern := build_expr(checker, arm.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, subject_type, ctx.pkg, ctx.file) for pat_id in arm.patterns {
pattern = coerce_expr(checker, pattern, subject_type, arm.span) pat_ast := checker.ast_module.exprs[pat_id]
if checker.module.exprs[pattern].kind == .Invalid { cmp := hir.INVALID_EXPR
ok = false if pat_ast.kind == .Range {
continue lo := build_expr(checker, pat_ast.left, ctx.locals^[:], ctx.global_reads, ctx.calls, subject_type, ctx.pkg, ctx.file)
lo = coerce_expr(checker, lo, subject_type, arm.span)
hi := build_expr(checker, pat_ast.right, ctx.locals^[:], ctx.global_reads, ctx.calls, subject_type, ctx.pkg, ctx.file)
hi = coerce_expr(checker, hi, subject_type, arm.span)
if checker.module.exprs[lo].kind == .Invalid || checker.module.exprs[hi].kind == .Invalid {
ok = false
continue
}
ge := add_hir_expr(checker, hir.Expr{
kind = .Ge, span = arm.span, type = types.BOOL,
left = slot_read(checker, key_local, key_type, arm.span), right = lo,
target = hir.INVALID_REF, diagnostic = source.INVALID_DIAGNOSTIC,
})
hi_cmp := add_hir_expr(checker, hir.Expr{
kind = .Le if pat_ast.integer == 1 else .Lt, span = arm.span, type = types.BOOL,
left = slot_read(checker, key_local, key_type, arm.span), right = hi,
target = hir.INVALID_REF, diagnostic = source.INVALID_DIAGNOSTIC,
})
cmp = add_hir_expr(checker, hir.Expr{
kind = .And, span = arm.span, type = types.BOOL,
left = ge, right = hi_cmp, target = hir.INVALID_REF, diagnostic = source.INVALID_DIAGNOSTIC,
})
} else {
pattern := build_expr(checker, pat_id, ctx.locals^[:], ctx.global_reads, ctx.calls, subject_type, ctx.pkg, ctx.file)
pattern = coerce_expr(checker, pattern, subject_type, arm.span)
if checker.module.exprs[pattern].kind == .Invalid {
ok = false
continue
}
cmp = add_hir_expr(checker, hir.Expr{
kind = .Eq, span = arm.span, type = types.BOOL,
left = slot_read(checker, key_local, key_type, arm.span), right = pattern,
target = hir.INVALID_REF, diagnostic = source.INVALID_DIAGNOSTIC,
})
}
condition = match_or(checker, condition, cmp, arm.span)
} }
condition = add_hir_expr(checker, hir.Expr{
kind = .Eq, span = arm.span, type = types.BOOL,
left = slot_read(checker, key_local, key_type, arm.span), right = pattern,
target = hir.INVALID_REF, diagnostic = source.INVALID_DIAGNOSTIC,
})
} }
arm_body, body_ok := build_match_arm_body(ctx, arm, subject_type, subj_local, field_index, payload_type, as_value, slot, slot_type, span) arm_body, body_ok := build_match_arm_body(ctx, arm, subject_type, subj_local, subj_is_pointer, ptr_type, subj_writable, field_index, payload_type, as_value, slot, slot_type, span)
if !body_ok { if !body_ok {
ok = false ok = false
} }
@@ -5622,6 +5766,9 @@ build_match_arm_body :: proc(
arm: ast.Stmt, arm: ast.Stmt,
subject_type: types.Type, subject_type: types.Type,
subj_local: hir.Local_Id, subj_local: hir.Local_Id,
subj_is_pointer: bool,
ptr_type: types.Type,
subj_writable: bool,
field_index: int, field_index: int,
payload_type: types.Type, payload_type: types.Type,
as_value: bool, as_value: bool,
@@ -5637,17 +5784,29 @@ build_match_arm_body :: proc(
if len(arm.captures) > 0 && field_index >= 0 { if len(arm.captures) > 0 && field_index >= 0 {
capture := arm.captures[0] capture := arm.captures[0]
if capture != checker.sink_symbol { if capture != checker.sink_symbol {
cap_local := hir.local_id(len(ctx.hir_locals^)) // The payload sits at the subject's shared carrier offset. A value capture
append(ctx.hir_locals, hir.Local{name = capture, type = payload_type, mutable = false}) // loads it; a `|@cap|` capture binds a pointer to it (mutability follows the
append(ctx.locals, Build_Local{name = capture, type = payload_type, mutable = false, id = cap_local}) // subject), aliasing the original storage via the subject location.
field_read := add_hir_expr(checker, hir.Expr{ field_read := add_hir_expr(checker, hir.Expr{
kind = .Field, span = span, type = payload_type, integer = i64(field_index), kind = .Field, span = span, type = payload_type, integer = i64(field_index),
left = slot_read(checker, subj_local, subject_type, span), left = match_subject_location(checker, subj_local, subj_is_pointer, subject_type, ptr_type, span),
target = hir.INVALID_REF, right = hir.INVALID_EXPR, diagnostic = source.INVALID_DIAGNOSTIC, target = hir.INVALID_REF, right = hir.INVALID_EXPR, diagnostic = source.INVALID_DIAGNOSTIC,
}) })
cap_type := payload_type
cap_value := field_read
if arm.pointer_capture {
cap_type = types.pointer(&checker.module.types, payload_type, subj_writable, false)
cap_value = add_hir_expr(checker, hir.Expr{
kind = .Address, span = span, type = cap_type, left = field_read,
target = hir.INVALID_REF, right = hir.INVALID_EXPR, diagnostic = source.INVALID_DIAGNOSTIC,
})
}
cap_local := hir.local_id(len(ctx.hir_locals^))
append(ctx.hir_locals, hir.Local{name = capture, type = cap_type, mutable = false})
append(ctx.locals, Build_Local{name = capture, type = cap_type, mutable = false, id = cap_local})
append(&result, hir.stmt_id(len(checker.module.statements))) append(&result, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{ append(&checker.module.statements, hir.Stmt{
kind = .Declaration, span = span, local = cap_local, expr = field_read, kind = .Declaration, span = span, local = cap_local, expr = cap_value,
diagnostic = source.INVALID_DIAGNOSTIC, diagnostic = source.INVALID_DIAGNOSTIC,
}) })
} }
+14 -7
View File
@@ -704,8 +704,11 @@ emit_instruction_stream :: proc(
if item.kind == .Union { if item.kind == .Union {
fields := types.fields_for(&emitter.module.types, instruction.type) fields := types.fields_for(&emitter.module.types, instruction.type)
field_index := int(instruction.integer) field_index := int(instruction.integer)
// A void-payload variant carries no value (`T{ variant }`): only the tag
// is stored, so there is no operand to validate or write.
void_payload := field_index >= 0 && field_index < len(fields) && types.is_void(fields[field_index].type)
if field_index < 0 || field_index >= len(fields) || if field_index < 0 || field_index >= len(fields) ||
!valid_value(instructions, instruction.args[0], fields[field_index].type, &emitter.module.types) { (!void_payload && !valid_value(instructions, instruction.args[0], fields[field_index].type, &emitter.module.types)) {
emit_recovery_value(emitter, instruction_index, instruction, "invalid union aggregate operands") emit_recovery_value(emitter, instruction_index, instruction, "invalid union aggregate operands")
continue continue
} }
@@ -725,13 +728,17 @@ emit_instruction_stream :: proc(
} }
} }
fmt.sbprintf(&emitter.builder, " store %s %d, ptr %%union_slot%d\n", llvm_type(tag_enum, &emitter.module.types), tag_value, instruction_index) fmt.sbprintf(&emitter.builder, " store %s %d, ptr %%union_slot%d\n", llvm_type(tag_enum, &emitter.module.types), tag_value, instruction_index)
offset := types.union_payload_offset(instruction.type, &emitter.module.types, emitter.module.target) if !void_payload {
fmt.sbprintf(&emitter.builder, " %%union_payload%d = getelementptr i8, ptr %%union_slot%d, i64 %d\n", instruction_index, instruction_index, offset) offset := types.union_payload_offset(instruction.type, &emitter.module.types, emitter.module.target)
payload_ptr = fmt.tprintf("%%union_payload%d", instruction_index) fmt.sbprintf(&emitter.builder, " %%union_payload%d = getelementptr i8, ptr %%union_slot%d, i64 %d\n", instruction_index, instruction_index, offset)
payload_ptr = fmt.tprintf("%%union_payload%d", instruction_index)
}
}
if !void_payload {
fmt.sbprintf(&emitter.builder, " store %s ", llvm_type(fields[field_index].type, &emitter.module.types))
write_operand(&emitter.builder, instructions, instruction.args[0], fields[field_index].type, &emitter.module.types)
fmt.sbprintf(&emitter.builder, ", ptr %s\n", payload_ptr)
} }
fmt.sbprintf(&emitter.builder, " store %s ", llvm_type(fields[field_index].type, &emitter.module.types))
write_operand(&emitter.builder, instructions, instruction.args[0], fields[field_index].type, &emitter.module.types)
fmt.sbprintf(&emitter.builder, ", ptr %s\n", payload_ptr)
fmt.sbprintf(&emitter.builder, " %%v%d = load %s, ptr %%union_slot%d\n", instruction_index, type_name, instruction_index) fmt.sbprintf(&emitter.builder, " %%v%d = load %s, ptr %%union_slot%d\n", instruction_index, type_name, instruction_index)
continue continue
} }
+3 -1
View File
@@ -211,7 +211,9 @@ lower_compound_expr :: proc(state: ^State, expr_id: hir.Expr_Id) -> ir.Instructi
case .Array, .Struct: case .Array, .Struct:
args := make([]ir.Instruction_Id, len(expr.args), state.allocator) args := make([]ir.Instruction_Id, len(expr.args), state.allocator)
for arg, index in expr.args { for arg, index in expr.args {
args[index] = lower_nested_expr(state, arg) // A void union variant (`T{ variant }`) has no payload operand; leave it
// invalid so codegen emits only the tag, not a trapping recovery value.
args[index] = lower_nested_expr(state, arg) if arg != hir.INVALID_EXPR else ir.INVALID_INSTRUCTION
} }
return append_instruction(state, ir.Instruction{ return append_instruction(state, ir.Instruction{
op=.Aggregate, span=expr.span, type=expr.type, args=args, integer=expr.integer, op=.Aggregate, span=expr.span, type=expr.type, args=args, integer=expr.integer,
+30 -11
View File
@@ -477,14 +477,18 @@ parse_struct_literal :: proc(
break break
} }
advance(parser) advance(parser)
if _, ok := allow(parser, .Equal); !ok { // A bare key (`T{ variant }`, no `= value`) constructs a void-payload union
source.add(parser.diagnostics, current(parser).span, "expected '=' after struct field name") // variant; the checker validates that the field actually has a void type.
value := ast.INVALID_EXPR
key_end := field.span
if _, ok := allow(parser, .Equal); ok {
skip_newlines(parser)
value = parse_expression_bp(parser, 0, nesting+1)
key_end = parser.module.exprs[value].span
} }
skip_newlines(parser)
value := parse_expression_bp(parser, 0, nesting+1)
append(&args, add_expr(parser, ast.Expr{ append(&args, add_expr(parser, ast.Expr{
kind=.Keyed, kind=.Keyed,
span=span_from(field.span, parser.module.exprs[value].span), span=span_from(field.span, key_end),
name=field.symbol, name=field.symbol,
left=value, left=value,
right=ast.INVALID_EXPR, right=ast.INVALID_EXPR,
@@ -1544,20 +1548,33 @@ parse_arm_body :: proc(parser: ^Parser) -> []ast.Stmt_Id {
return single return single
} }
// parse_match_arm parses one `<pattern> [|capture|]: <body>` arm (or `else: <body>`). // parse_match_arm parses one `<pattern,...> [|[@]capture|]: <body>` arm (or
// The pattern is `INVALID_EXPR` for `else`; `captures` holds the optional 0-or-1 // `else: <body>`). `patterns` is empty for `else`, one expr for a single pattern, or
// payload capture name (tagged-union variants only). // several for a multi-pattern arm; `captures` holds the optional 0-or-1 payload capture
// name with `pointer_capture` set for the `|@cap|` form (validated in the checker).
parse_match_arm :: proc(parser: ^Parser) -> ast.Stmt_Id { parse_match_arm :: proc(parser: ^Parser) -> ast.Stmt_Id {
start := current(parser).span start := current(parser).span
pattern := ast.INVALID_EXPR patterns: [dynamic]ast.Expr_Id
patterns.allocator = parser.module.allocator
captures: [dynamic]symbol.Id captures: [dynamic]symbol.Id
captures.allocator = parser.module.allocator captures.allocator = parser.module.allocator
pointer_capture := false
if _, is_else := allow(parser, .Keyword_Else); !is_else { if _, is_else := allow(parser, .Keyword_Else); !is_else {
saved := parser.no_struct_literal saved := parser.no_struct_literal
parser.no_struct_literal = true parser.no_struct_literal = true
pattern = parse_expression(parser) append(&patterns, parse_expression(parser))
for {
if _, ok := allow(parser, .Comma); !ok {
break
}
skip_newlines(parser)
append(&patterns, parse_expression(parser))
}
parser.no_struct_literal = saved parser.no_struct_literal = saved
if _, ok := allow(parser, .Pipe); ok { if _, ok := allow(parser, .Pipe); ok {
if _, at_ok := allow(parser, .At); at_ok {
pointer_capture = true
}
name_tok := current(parser) name_tok := current(parser)
if name_tok.kind == .Identifier || name_tok.kind == .Underscore { if name_tok.kind == .Identifier || name_tok.kind == .Underscore {
advance(parser) advance(parser)
@@ -1578,8 +1595,10 @@ parse_match_arm :: proc(parser: ^Parser) -> ast.Stmt_Id {
append(&parser.module.statements, ast.Stmt{ append(&parser.module.statements, ast.Stmt{
kind=.Match_Arm, kind=.Match_Arm,
span=span_from(start, previous(parser).span), span=span_from(start, previous(parser).span),
expr=pattern, expr=ast.INVALID_EXPR,
patterns=patterns[:],
captures=captures[:], captures=captures[:],
pointer_capture=pointer_capture,
body=body, body=body,
target=ast.INVALID_EXPR, target=ast.INVALID_EXPR,
update=ast.INVALID_STMT, update=ast.INVALID_STMT,
+125
View File
@@ -2402,6 +2402,131 @@ main :: func() i32 {
testing.expect(t, found_unknown) testing.expect(t, found_unknown)
} }
@(test)
match_range_arm_emits_bounds :: proc(t: ^testing.T) {
// A scalar range arm `lo..hi:` desugars to `key >= lo and key < hi` (inclusive uses
// `<=`), emitted as signed integer comparisons for an i32 subject.
text := `main :: func() i32 {
n i32 = 5
out i32 = 0
match n {
0..10: out = 1
10..=20: out = 2
else: out = 3
}
return out
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, strings.contains(llvm_text, "icmp sge i32")) // key >= lo
testing.expect(t, strings.contains(llvm_text, "icmp slt i32")) // key < hi (exclusive)
testing.expect(t, strings.contains(llvm_text, "icmp sle i32")) // key <= hi (inclusive)
}
@(test)
match_extended_misuse_is_diagnosed :: proc(t: ^testing.T) {
// Five rejected forms from milestone 22.5: (1) a capture on a void variant, (2) a value
// given to a void variant in construction, (3) a bare key on a non-void field, (4) a
// range pattern on an enum subject, and (5) a multi-pattern capture whose variants have
// different payload types. (Pointer capture on an rvalue subject is also rejected, but
// match-on-call-result is a separate pre-existing gap so it isn't exercised here.)
text := `Animal :: enum {
dog
cat
bird
}
Point :: struct {
x i32
y i32
}
Box :: union(enum) {
point Point
count i32
empty void
}
void_capture :: func(b Box) i32 {
match b {
.point |p|: { return p.x }
.count |c|: { return c }
.empty |x|: { return 0 }
}
return 0
}
void_value :: func() i32 {
b Box = Box{ empty = 5 }
return 0
}
bare_on_nonvoid :: func() i32 {
b Box = Box{ count }
return 0
}
range_on_enum :: func(a Animal) i32 {
match a {
0..2: { return 1 }
else: { return 0 }
}
return 0
}
incompatible_capture :: func(b Box) i32 {
match b {
.point, .count |v|: { return 0 }
.empty: { return 0 }
}
return 0
}
main :: func() i32 {
b Box = Box{ count = 1 }
return void_capture(b) + void_value() + bare_on_nonvoid() + range_on_enum(.dog) +
incompatible_capture(b)
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
found_void_capture := false
found_void_value := false
found_bare := false
found_range_enum := false
found_incompatible := false
for diagnostic in diagnostics.items {
found_void_capture = found_void_capture || strings.contains(diagnostic.message, "void payload")
found_void_value = found_void_value || strings.contains(diagnostic.message, "void variant 'empty' takes no value")
found_bare = found_bare || strings.contains(diagnostic.message, "field 'count' requires a value")
found_range_enum = found_range_enum || strings.contains(diagnostic.message, "range patterns only apply to scalar")
found_incompatible = found_incompatible || strings.contains(diagnostic.message, "capture group with incompatible types")
}
testing.expect(t, found_void_capture)
testing.expect(t, found_void_value)
testing.expect(t, found_bare)
testing.expect(t, found_range_enum)
testing.expect(t, found_incompatible)
}
@(test) @(test)
yield_misuse_is_diagnosed :: proc(t: ^testing.T) { yield_misuse_is_diagnosed :: proc(t: ^testing.T) {
// A value block that does not end in `yield`, and a `yield` nested inside an // A value block that does not end in `yield`, and a `yield` nested inside an
+59 -19
View File
@@ -4,7 +4,8 @@ Animal :: enum {
bird bird
} }
# tagged union over an existing enum (variants are a subset of the enum's members) # tagged union over an existing enum (variants are a subset of the enum's members);
# dog and bird share the same payload type (i32), so a multi-pattern arm may capture both.
Data :: union(Animal) { Data :: union(Animal) {
dog i32 dog i32
bird i32 bird i32
@@ -16,6 +17,17 @@ Shape :: union(enum) {
circle i32 # radius circle i32 # radius
} }
Point :: struct {
x i32
y i32
}
# tagged union with a void-payload variant (`empty` carries no value)
Box :: union(enum) {
point Point
empty void
}
# Statement match with payload capture; every variant returns, so the function needs # Statement match with payload capture; every variant returns, so the function needs
# no trailing return (the desugared if/else chain covers all paths). # no trailing return (the desugared if/else chain covers all paths).
describe :: func(d Data) i32 { describe :: func(d Data) i32 {
@@ -37,39 +49,67 @@ area :: func(s Shape) i32 {
return result return result
} }
# Same-type multi-pattern capture: `.dog` and `.bird` are both i32, so one capture binds
# either payload (read once at the union's shared carrier offset).
payload_of :: func(d Data) i32 {
v :: match d {
.dog, .bird |n|: n
}
return v
}
main :: func() i32 { main :: func() i32 {
acc i32 = 0
dog Data = Data{ dog = 9 } dog Data = Data{ dog = 9 }
bird Data = Data{ bird = 38 } bird Data = Data{ bird = 38 }
total i32 = describe(dog) + describe(bird) # 10 + 40 = 50 acc = acc + describe(dog) + describe(bird) # 10 + 40 = 50
# enum statement match, exhaustive without an `else` # same-type multi-pattern capture
acc = acc + payload_of(dog) + payload_of(bird) # 9 + 38 = 47
# enum statement match, exhaustive, with a multi-pattern arm
a Animal = .bird a Animal = .bird
rank i32 = 0 rank i32 = 0
match a { match a {
.dog: rank = 1 .dog, .cat: rank = 1
.cat: rank = 2 .bird: rank = 3
.bird: rank = 3
} }
acc = acc + rank # +3
# enum value match # enum value match with a multi-pattern arm
legs :: match a { legs :: match a {
.dog: 4 .dog, .cat: 4
.cat: 4 .bird: 2
.bird: 2
} }
acc = acc + legs # +2
# integer match with a mandatory `else` # scalar match: a range arm, a multi-literal arm, and a mandatory else
bucket i32 = 0 bucket i32 = 0
match rank { match rank {
1: bucket = 100 0..3: bucket = 1 # exclusive 0,1,2 — does not include 3
3: bucket = 5 3, 4: bucket = 5 # rank is 3
else: bucket = 99 else: bucket = 99
} }
acc = acc + bucket # +5
sq Shape = Shape{ square = 4 } # void-payload variant: bare-key construction + a no-capture arm
ci Shape = Shape{ circle = 2 } e Box = Box{ empty }
shapes i32 = area(sq) + area(ci) # 16 + 12 = 28 hit i32 = 0
match e {
.point |pt|: hit = pt.x
.empty: hit = 7
}
acc = acc + hit # +7
# 50 + 3 + 2 + 5 + 28 = 88 # pointer capture mutates the subject's payload in place
return total + rank + legs + bucket + shapes - 88 b Box = Box{ point = Point{ x = 1, y = 2 } }
match b {
.point |@p|: p.x = 10
.empty: hit = hit
}
acc = acc + b.point.x # +10 (mutated through the @mut Point)
# 50 + 47 + 3 + 2 + 5 + 7 + 10 = 124
return acc - 124
} }