match statements
This commit is contained in:
@@ -175,6 +175,16 @@ symbol_text :: proc(checker: ^Checker, id: symbol.Id) -> string {
|
||||
return symbol.resolve(checker.symbols, id)
|
||||
}
|
||||
|
||||
// type_label renders a type for a diagnostic, resolving a named type (enum/union/struct/
|
||||
// distinct) to its declared source name; primitives and unnamed types fall back to
|
||||
// `types.name` (which prints `<type N>` for anonymous nodes).
|
||||
type_label :: proc(checker: ^Checker, value: types.Type) -> string {
|
||||
if node, ok := types.node(&checker.module.types, value); ok && node.name != 0 {
|
||||
return symbol_text(checker, symbol.Id(node.name))
|
||||
}
|
||||
return types.name(value)
|
||||
}
|
||||
|
||||
Constant_Frame :: struct {
|
||||
expr: ast.Expr_Id,
|
||||
stage: u8,
|
||||
@@ -776,6 +786,11 @@ mark_block_imports_used :: proc(checker: ^Checker, statements: []ast.Stmt_Id, fi
|
||||
case .Defer:
|
||||
deferred := [1]ast.Stmt_Id{statement.update}
|
||||
mark_block_imports_used(checker, deferred[:], file)
|
||||
case .Match, .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`.
|
||||
mark_expr_imports_used(checker, statement.expr, file)
|
||||
mark_block_imports_used(checker, statement.body, file)
|
||||
case .Break, .Continue:
|
||||
case .Invalid:
|
||||
}
|
||||
@@ -4923,6 +4938,18 @@ build_block :: proc(
|
||||
ctx.loop_floor = saved_floor
|
||||
ctx.defer_depth -= 1
|
||||
append(ctx.defers, entry)
|
||||
case .Match:
|
||||
build_match(ctx, &body, statement)
|
||||
case .Match_Arm:
|
||||
// Arms are only reachable through their enclosing `.Match`; one on its own
|
||||
// is a parser bug.
|
||||
append(&body, hir.stmt_id(len(checker.module.statements)))
|
||||
append(&checker.module.statements, hir.Stmt{
|
||||
kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR,
|
||||
local = hir.INVALID_LOCAL,
|
||||
diagnostic = source.add(checker.diagnostics, statement.span, "unexpected match arm outside 'match'"),
|
||||
})
|
||||
ctx.problematic^ = true
|
||||
case .Invalid:
|
||||
append(&body, hir.stmt_id(len(checker.module.statements)))
|
||||
append(&checker.module.statements, hir.Stmt{
|
||||
@@ -5051,6 +5078,8 @@ build_value_source :: proc(
|
||||
return build_value_if(ctx, body, body_stmts[0], expected, span)
|
||||
case .For, .While:
|
||||
return build_value_loop(ctx, body, body_stmts[0], expected, span)
|
||||
case .Match:
|
||||
return build_value_match(ctx, body, body_stmts[0], expected, span)
|
||||
}
|
||||
}
|
||||
return build_value_block(ctx, body, body_stmts, expected, span)
|
||||
@@ -5309,6 +5338,415 @@ emit_value_branch :: proc(
|
||||
return true
|
||||
}
|
||||
|
||||
// Match_Built_Arm holds one already-built arm: its dispatch condition (`INVALID_EXPR`
|
||||
// for the terminal `else`/exhaustive arm) and its body statements. The chain is
|
||||
// assembled backward from these so diagnostics stay in source order.
|
||||
Match_Built_Arm :: struct {
|
||||
condition: hir.Expr_Id,
|
||||
body: []hir.Stmt_Id,
|
||||
terminal: bool,
|
||||
}
|
||||
|
||||
// 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
|
||||
// through the value-branch machinery so the construct produces a value; otherwise arm
|
||||
// bodies are ordinary statement blocks. Returns false (and emits a `.Trap`) on any error.
|
||||
emit_match :: proc(
|
||||
ctx: ^Build_Ctx,
|
||||
out: ^[dynamic]hir.Stmt_Id,
|
||||
statement: ast.Stmt,
|
||||
as_value: bool,
|
||||
slot: ^hir.Local_Id,
|
||||
slot_type: ^types.Type,
|
||||
) -> bool {
|
||||
checker := ctx.checker
|
||||
store := &checker.module.types
|
||||
span := statement.span
|
||||
|
||||
fail :: proc(ctx: ^Build_Ctx, out: ^[dynamic]hir.Stmt_Id, span: source.Span, diagnostic: source.Diagnostic_Id) -> bool {
|
||||
checker := ctx.checker
|
||||
append(out, hir.stmt_id(len(checker.module.statements)))
|
||||
append(&checker.module.statements, hir.Stmt{
|
||||
kind = .Trap, span = span, expr = hir.INVALID_EXPR,
|
||||
local = hir.INVALID_LOCAL, diagnostic = diagnostic,
|
||||
})
|
||||
ctx.problematic^ = true
|
||||
return false
|
||||
}
|
||||
|
||||
// 1. Subject, spilled into an addressable temp so the tag read and any payload
|
||||
// captures reference one evaluation.
|
||||
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
|
||||
if checker.module.exprs[subject].kind == .Invalid {
|
||||
return fail(ctx, out, span, checker.module.exprs[subject].diagnostic)
|
||||
}
|
||||
|
||||
is_tagged := types.is_tagged_union(subject_type, store)
|
||||
is_enum_subject := types.is_enum(subject_type, store)
|
||||
if types.is_union(subject_type, store) && !is_tagged {
|
||||
return fail(ctx, out, span, source.add(checker.diagnostics, span, "cannot 'match' on an untagged union; it has no tag to dispatch on"))
|
||||
}
|
||||
if !is_tagged && !is_enum_subject && !types.is_concrete_scalar(subject_type) {
|
||||
return fail(ctx, out, span, source.addf(checker.diagnostics, span,
|
||||
"'match' subject must be a tagged union, enum, or scalar value, not '%s'", type_label(checker, subject_type)))
|
||||
}
|
||||
|
||||
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(out, hir.stmt_id(len(checker.module.statements)))
|
||||
append(&checker.module.statements, hir.Stmt{
|
||||
kind = .Declaration, span = span, local = subj_local, expr = subject,
|
||||
diagnostic = source.INVALID_DIAGNOSTIC,
|
||||
})
|
||||
|
||||
// 2. Dispatch key: a tagged union reads its discriminant into its own temp; an enum
|
||||
// or scalar compares the subject directly.
|
||||
key_local := subj_local
|
||||
key_type := subject_type
|
||||
tag_enum := types.INVALID
|
||||
if is_tagged {
|
||||
tag_enum = types.union_tag_enum(subject_type, store)
|
||||
tag_read := add_hir_expr(checker, hir.Expr{
|
||||
kind = .Union_Tag, span = span, type = tag_enum,
|
||||
left = slot_read(checker, subj_local, subject_type, span),
|
||||
target = hir.INVALID_REF, right = hir.INVALID_EXPR, diagnostic = source.INVALID_DIAGNOSTIC,
|
||||
})
|
||||
tag_local := hir.local_id(len(ctx.hir_locals^))
|
||||
append(ctx.hir_locals, hir.Local{name = checker.sink_symbol, type = tag_enum, mutable = false})
|
||||
append(out, hir.stmt_id(len(checker.module.statements)))
|
||||
append(&checker.module.statements, hir.Stmt{
|
||||
kind = .Declaration, span = span, local = tag_local, expr = tag_read,
|
||||
diagnostic = source.INVALID_DIAGNOSTIC,
|
||||
})
|
||||
key_local = tag_local
|
||||
key_type = tag_enum
|
||||
}
|
||||
|
||||
// 3. Build each arm (forward, for source-order diagnostics).
|
||||
built: [dynamic]Match_Built_Arm
|
||||
built.allocator = checker.allocator
|
||||
defer delete(built)
|
||||
covered: [dynamic]symbol.Id
|
||||
covered.allocator = checker.allocator
|
||||
defer delete(covered)
|
||||
has_else := false
|
||||
ok := true
|
||||
|
||||
for arm_id in statement.body {
|
||||
arm := checker.ast_module.statements[arm_id]
|
||||
if arm.kind != .Match_Arm {
|
||||
ok = false
|
||||
continue
|
||||
}
|
||||
if has_else {
|
||||
source.add(checker.diagnostics, arm.span, "arms after 'else' are unreachable")
|
||||
ok = false
|
||||
}
|
||||
is_else := arm.expr == ast.INVALID_EXPR
|
||||
condition := hir.INVALID_EXPR
|
||||
field_index := -1
|
||||
payload_type := types.INVALID
|
||||
has_capture := len(arm.captures) > 0
|
||||
|
||||
if is_else {
|
||||
if has_capture {
|
||||
source.add(checker.diagnostics, arm.span, "the 'else' arm cannot capture a payload")
|
||||
ok = false
|
||||
}
|
||||
has_else = true
|
||||
} else if is_tagged || is_enum_subject {
|
||||
pattern := checker.ast_module.exprs[arm.expr]
|
||||
if pattern.kind != .Enum_Literal {
|
||||
source.add(checker.diagnostics, arm.span, "an enum or tagged-union 'match' arm must be a '.variant' pattern")
|
||||
ok = false
|
||||
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
|
||||
}
|
||||
field_index = index
|
||||
payload_type = field.type
|
||||
member := enum_member_hir(checker, tag_enum, 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 {
|
||||
if has_capture {
|
||||
source.add(checker.diagnostics, arm.span, "only tagged-union variants can capture a payload")
|
||||
ok = false
|
||||
}
|
||||
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, 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 {
|
||||
if has_capture {
|
||||
source.add(checker.diagnostics, arm.span, "only tagged-union variants can capture a payload")
|
||||
ok = false
|
||||
}
|
||||
pattern := build_expr(checker, arm.expr, 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
|
||||
}
|
||||
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)
|
||||
if !body_ok {
|
||||
ok = false
|
||||
}
|
||||
append(&built, Match_Built_Arm{condition = condition, body = arm_body, terminal = is_else})
|
||||
}
|
||||
|
||||
// 4. Exhaustiveness. Enum/union matches must cover every variant or supply `else`;
|
||||
// an already-exhaustive match must not carry a redundant `else`. The last
|
||||
// covered arm is promoted to the unconditional `else` so the chain terminates.
|
||||
if is_tagged || is_enum_subject {
|
||||
all_names := types.enum_members_for(store, tag_enum) if is_tagged else types.enum_members_for(store, subject_type)
|
||||
names: [dynamic]symbol.Id
|
||||
names.allocator = checker.allocator
|
||||
defer delete(names)
|
||||
if is_tagged {
|
||||
for field in types.fields_for(store, subject_type) {
|
||||
append(&names, symbol.Id(field.name))
|
||||
}
|
||||
} else {
|
||||
for member in all_names {
|
||||
append(&names, symbol.Id(member.name))
|
||||
}
|
||||
}
|
||||
missing: [dynamic]symbol.Id
|
||||
missing.allocator = checker.allocator
|
||||
defer delete(missing)
|
||||
for name in names {
|
||||
if !contains_name(covered[:], name) {
|
||||
append(&missing, name)
|
||||
}
|
||||
}
|
||||
if has_else {
|
||||
if len(missing) == 0 {
|
||||
source.add(checker.diagnostics, span, "redundant 'else': the 'match' already covers every variant")
|
||||
ok = false
|
||||
}
|
||||
} else if len(missing) > 0 {
|
||||
builder: strings.Builder
|
||||
strings.builder_init(&builder, checker.allocator)
|
||||
defer strings.builder_destroy(&builder)
|
||||
for name, index in missing {
|
||||
if index > 0 {
|
||||
strings.write_string(&builder, ", ")
|
||||
}
|
||||
strings.write_string(&builder, ".")
|
||||
strings.write_string(&builder, symbol_text(checker, name))
|
||||
}
|
||||
source.addf(checker.diagnostics, span, "'match' on '%s' is not exhaustive; missing variants: %s (add the arms or an 'else')",
|
||||
type_label(checker, subject_type), strings.to_string(builder))
|
||||
ok = false
|
||||
} else if len(built) > 0 {
|
||||
built[len(built) - 1].terminal = true
|
||||
}
|
||||
} else if !has_else {
|
||||
source.addf(checker.diagnostics, span, "a 'match' on '%s' requires an 'else' arm", type_label(checker, subject_type))
|
||||
ok = false
|
||||
}
|
||||
|
||||
if !ok {
|
||||
// The arm bodies never get wired into the (un-assembled) chain, so free them here.
|
||||
for arm in built {
|
||||
delete(arm.body, checker.allocator)
|
||||
}
|
||||
return fail(ctx, out, span, source.INVALID_DIAGNOSTIC)
|
||||
}
|
||||
|
||||
// 5. Assemble the if/else chain backward from the built arms. The terminal arm is the
|
||||
// final (else / promoted) one; the rest nest as `if cond { body } else { … }`.
|
||||
else_chain: []hir.Stmt_Id = nil
|
||||
start := len(built)
|
||||
if len(built) > 0 && built[len(built) - 1].terminal {
|
||||
else_chain = built[len(built) - 1].body
|
||||
start = len(built) - 1
|
||||
}
|
||||
for i := start - 1; i >= 0; i -= 1 {
|
||||
arm := built[i]
|
||||
wrapper := make([]hir.Stmt_Id, 1, checker.allocator)
|
||||
wrapper[0] = hir.stmt_id(len(checker.module.statements))
|
||||
append(&checker.module.statements, hir.Stmt{
|
||||
kind = .If, span = span, expr = arm.condition, guard = hir.INVALID_EXPR,
|
||||
then_body = arm.body, else_body = else_chain,
|
||||
local = hir.INVALID_LOCAL, target = hir.INVALID_EXPR, diagnostic = source.INVALID_DIAGNOSTIC,
|
||||
})
|
||||
else_chain = wrapper
|
||||
}
|
||||
for s in else_chain {
|
||||
append(out, s)
|
||||
}
|
||||
// The outermost chain slice's ids are now copied into `out`; the inner slices are
|
||||
// owned by their enclosing `.If` (freed with the HIR module).
|
||||
delete(else_chain, checker.allocator)
|
||||
return true
|
||||
}
|
||||
|
||||
// build_match_arm_body builds one arm's body, prefixed with the optional payload capture
|
||||
// (`cap := subject.variant`, an unchecked reinterpret like a Zig union field read). For a
|
||||
// statement match it is a plain block; for a value match each path assigns the result slot
|
||||
// (a single-expression arm yields implicitly).
|
||||
build_match_arm_body :: proc(
|
||||
ctx: ^Build_Ctx,
|
||||
arm: ast.Stmt,
|
||||
subject_type: types.Type,
|
||||
subj_local: hir.Local_Id,
|
||||
field_index: int,
|
||||
payload_type: types.Type,
|
||||
as_value: bool,
|
||||
slot: ^hir.Local_Id,
|
||||
slot_type: ^types.Type,
|
||||
span: source.Span,
|
||||
) -> ([]hir.Stmt_Id, bool) {
|
||||
checker := ctx.checker
|
||||
result: [dynamic]hir.Stmt_Id
|
||||
result.allocator = checker.allocator
|
||||
capture_start := len(ctx.locals^)
|
||||
|
||||
if len(arm.captures) > 0 && field_index >= 0 {
|
||||
capture := arm.captures[0]
|
||||
if capture != checker.sink_symbol {
|
||||
cap_local := hir.local_id(len(ctx.hir_locals^))
|
||||
append(ctx.hir_locals, hir.Local{name = capture, type = payload_type, mutable = false})
|
||||
append(ctx.locals, Build_Local{name = capture, type = payload_type, mutable = false, id = cap_local})
|
||||
field_read := add_hir_expr(checker, hir.Expr{
|
||||
kind = .Field, span = span, type = payload_type, integer = i64(field_index),
|
||||
left = slot_read(checker, subj_local, subject_type, span),
|
||||
target = hir.INVALID_REF, right = hir.INVALID_EXPR, diagnostic = source.INVALID_DIAGNOSTIC,
|
||||
})
|
||||
append(&result, hir.stmt_id(len(checker.module.statements)))
|
||||
append(&checker.module.statements, hir.Stmt{
|
||||
kind = .Declaration, span = span, local = cap_local, expr = field_read,
|
||||
diagnostic = source.INVALID_DIAGNOSTIC,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
body_ok := true
|
||||
if !as_value {
|
||||
built := build_block(ctx, arm.body)
|
||||
for s in built {
|
||||
append(&result, s)
|
||||
}
|
||||
delete(built, checker.allocator)
|
||||
} else {
|
||||
body_ok = build_value_arm(ctx, &result, arm.body, slot, slot_type, span)
|
||||
}
|
||||
resize(ctx.locals, capture_start)
|
||||
return result[:], body_ok
|
||||
}
|
||||
|
||||
// build_value_arm appends a value-match arm's slot assignment(s) to `out`: a single bare
|
||||
// expression yields implicitly; anything else reuses the value-branch rule (trailing
|
||||
// `yield`, or exit on every path).
|
||||
build_value_arm :: proc(
|
||||
ctx: ^Build_Ctx,
|
||||
out: ^[dynamic]hir.Stmt_Id,
|
||||
arm_body: []ast.Stmt_Id,
|
||||
slot: ^hir.Local_Id,
|
||||
slot_type: ^types.Type,
|
||||
span: source.Span,
|
||||
) -> bool {
|
||||
checker := ctx.checker
|
||||
if len(arm_body) == 1 && checker.ast_module.statements[arm_body[0]].kind == .Expression {
|
||||
expr_stmt := checker.ast_module.statements[arm_body[0]]
|
||||
expected := slot_type^ if slot^ != hir.INVALID_LOCAL else types.INVALID
|
||||
value := build_expr(checker, expr_stmt.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, expected, ctx.pkg, ctx.file)
|
||||
if checker.module.exprs[value].kind == .Invalid {
|
||||
ctx.problematic^ = true
|
||||
return false
|
||||
}
|
||||
vtype := checker.module.exprs[value].type
|
||||
if slot^ == hir.INVALID_LOCAL {
|
||||
slot_type^ = vtype
|
||||
slot^ = new_value_slot(ctx, slot_type^)
|
||||
} else {
|
||||
value = coerce_expr(checker, value, slot_type^, span)
|
||||
}
|
||||
emit_slot_assign(checker, out, slot^, value, span)
|
||||
return true
|
||||
}
|
||||
return emit_value_branch(ctx, out, arm_body, slot, slot_type, span)
|
||||
}
|
||||
|
||||
// build_match desugars a statement-position `match` into its if/else chain.
|
||||
build_match :: proc(ctx: ^Build_Ctx, body: ^[dynamic]hir.Stmt_Id, statement: ast.Stmt) {
|
||||
slot := hir.INVALID_LOCAL
|
||||
slot_type := types.INVALID
|
||||
emit_match(ctx, body, statement, false, &slot, &slot_type)
|
||||
}
|
||||
|
||||
// build_value_match desugars a `match` used as a declaration/assignment RHS: a result slot
|
||||
// each arm assigns, read after the chain. Mirrors build_value_if.
|
||||
build_value_match :: proc(
|
||||
ctx: ^Build_Ctx,
|
||||
body: ^[dynamic]hir.Stmt_Id,
|
||||
match_id: ast.Stmt_Id,
|
||||
expected: types.Type,
|
||||
span: source.Span,
|
||||
) -> (value: hir.Expr_Id, value_type: types.Type) {
|
||||
checker := ctx.checker
|
||||
statement := checker.ast_module.statements[match_id]
|
||||
slot := hir.INVALID_LOCAL
|
||||
slot_type := types.INVALID
|
||||
if is_runtime_type(checker, expected) {
|
||||
slot_type = expected
|
||||
slot = new_value_slot(ctx, slot_type)
|
||||
}
|
||||
subtree: [dynamic]hir.Stmt_Id
|
||||
subtree.allocator = checker.allocator
|
||||
ok := emit_match(ctx, &subtree, statement, true, &slot, &slot_type)
|
||||
if !ok || slot == hir.INVALID_LOCAL {
|
||||
for s in subtree {
|
||||
append(body, s)
|
||||
}
|
||||
delete(subtree)
|
||||
ctx.problematic^ = true
|
||||
return invalid_hir_expr(checker, span, source.INVALID_DIAGNOSTIC), types.INVALID
|
||||
}
|
||||
append(body, hir.stmt_id(len(checker.module.statements)))
|
||||
append(&checker.module.statements, hir.Stmt{
|
||||
kind = .Declaration, span = span, local = slot, expr = hir.INVALID_EXPR,
|
||||
diagnostic = source.INVALID_DIAGNOSTIC,
|
||||
})
|
||||
for s in subtree {
|
||||
append(body, s)
|
||||
}
|
||||
delete(subtree)
|
||||
return slot_read(checker, slot, slot_type, span), slot_type
|
||||
}
|
||||
|
||||
// loop_yields_none reports whether any `yield` that targets this loop (a labeled
|
||||
// `yield :blk` inside `if`/block branches, or the trailing fall-through) yields the
|
||||
// literal `none` — making the loop's result optional. Pure AST walk; does not descend
|
||||
|
||||
Reference in New Issue
Block a user