match statements
This commit is contained in:
@@ -468,10 +468,43 @@
|
|||||||
- deferred to milestone 22 (`match`): safe tag dispatch + payload capture (`match x { .bird |v| … }`),
|
- deferred to milestone 22 (`match`): safe tag dispatch + payload capture (`match x { .bird |v| … }`),
|
||||||
exhaustiveness checking, and any first-class tag-read accessor
|
exhaustiveness checking, and any first-class tag-read accessor
|
||||||
|
|
||||||
22. match statements with tagged unions payload unwrapping
|
22. match statements with tagged unions payload unwrapping (implemented; see below)
|
||||||
|
- `match <subject> { <arm>* }` dispatches on a tagged union, a plain enum, or a scalar
|
||||||
|
value. Arms are `.variant [|cap|]: <body>`, `<value>: <body>`, or `else: <body>`; a body
|
||||||
|
is a braced block or a single brace-less statement. Works as a **statement** and (on a
|
||||||
|
declaration/assignment RHS) as a **value source** (`x :: match … { … }`), where a
|
||||||
|
single-expression arm yields implicitly and a `{ … }` arm ends in `yield` (or exits on
|
||||||
|
every path), reusing the value-if branch rule from 20.6
|
||||||
|
- **tagged unions:** `.variant |cap|:` binds the payload (`cap := subject.variant`, the
|
||||||
|
unchecked field reinterpret from 21.5); `.variant:` ignores it. Exhaustive over the
|
||||||
|
union's *variants* (its fields), not the whole tag enum
|
||||||
|
- **enums:** `.member:` arms, exhaustive over the enum's members; no capture
|
||||||
|
- **scalars (int/float/bool/char):** arbitrary-expression patterns compared with `==`; an
|
||||||
|
`else` is mandatory (the domain can't be enumerated)
|
||||||
|
- exhaustiveness is a **compile error**: a non-exhaustive enum/union match with no `else`
|
||||||
|
lists the missing variants, and a redundant `else` on an already-exhaustive match is
|
||||||
|
rejected. Other diagnostics: unknown variant/member, duplicate arm, a non-`.variant`
|
||||||
|
pattern for an enum/union subject, a capture on a non-union/`else` arm, an untagged-union
|
||||||
|
subject, and arms after `else`
|
||||||
|
- **the only new runtime capability is reading the discriminant** (the 21.5-deferred tag
|
||||||
|
accessor): a new `hir.Union_Tag` expr + `ir.Union_Tag` op load the tag enum at the
|
||||||
|
union's offset 0 (the union address *is* the tag address — no GEP). Everything else is a
|
||||||
|
checker-only desugar to existing HIR (mirrors 18/19/20): the subject is spilled to one
|
||||||
|
temp, the tag read once into another, and the arms become an `if tag == .a { … } else if
|
||||||
|
… else { … }` chain (the last covered arm is promoted to the unconditional `else` so the
|
||||||
|
chain stays exhaustive and `all_paths_return` flows through). Value-match reuses the
|
||||||
|
value-if result-slot pattern (`new_value_slot`/`emit_value_branch`); captures reuse the
|
||||||
|
unwrap-if capture scoping. No new HIR/IR statement kinds; lowering/codegen add only the
|
||||||
|
one `Union_Tag` load
|
||||||
|
- parser/AST add a `match` keyword and `Match`/`Match_Arm` statement kinds (arms stored in
|
||||||
|
the `Match`'s `body`, pattern in `expr` with `INVALID` marking `else`, capture in
|
||||||
|
`captures`); a `union(enum)`/`union(Enum)` match resolves variant tags via the same
|
||||||
|
name→tag-enum-member lookup construction uses
|
||||||
|
- deferred (`// ponytail:` follow-ups): `void`-payload variants (`pending void`) — `void`
|
||||||
|
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
|
||||||
|
|
||||||
23. error types
|
23. error types (see below)
|
||||||
- brolang should feature errors as values
|
|
||||||
|
|
||||||
24. dynamic heap allocation
|
24. dynamic heap allocation
|
||||||
- see below for direction
|
- see below for direction
|
||||||
@@ -851,6 +884,216 @@ message :: match code {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## A word on error handling
|
||||||
|
|
||||||
|
Honey handles errors as values. There is no hidden control flow — a function that can fail declares this in its signature, and the caller must explicitly handle the possibility of failure.
|
||||||
|
|
||||||
|
### Fallible Functions
|
||||||
|
|
||||||
|
Functions that can fail declare their error type after `!`:
|
||||||
|
|
||||||
|
```honey
|
||||||
|
read_file :: func(path: []u8) []u8 ! IoError { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
This reads as: "returns `[]u8` or fails with `IoError`." The space around `!` is idiomatic but not required.
|
||||||
|
|
||||||
|
### Error Types
|
||||||
|
|
||||||
|
Errors can be enums (when you only need to identify what went wrong) or tagged unions (when errors need to carry additional context).
|
||||||
|
|
||||||
|
**Simple errors (enum):**
|
||||||
|
|
||||||
|
```honey
|
||||||
|
SimpleError :: enum {
|
||||||
|
OutOfMemory,
|
||||||
|
InvalidSize,
|
||||||
|
Timeout,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Rich errors (tagged union):**
|
||||||
|
|
||||||
|
```honey
|
||||||
|
IoError :: union(enum) {
|
||||||
|
NotFound: struct { path: []u8 },
|
||||||
|
PermissionDenied: struct { path: []u8, operation: []u8 },
|
||||||
|
Timeout: struct { after_ms: u64 },
|
||||||
|
ConnectionReset: void, # no additional data needed
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Constrained tagged union:**
|
||||||
|
|
||||||
|
When you have a predefined set of error kinds, you can constrain the union:
|
||||||
|
|
||||||
|
```honey
|
||||||
|
IoErrorKind :: enum {
|
||||||
|
not_found,
|
||||||
|
permission_denied,
|
||||||
|
timeout,
|
||||||
|
}
|
||||||
|
|
||||||
|
IoError :: union(IoErrorKind) {
|
||||||
|
not_found: struct { path: []u8 },
|
||||||
|
permission_denied: struct { path: []u8 },
|
||||||
|
timeout: struct { after_ms: u64 },
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Composition
|
||||||
|
|
||||||
|
Functions that can fail with multiple error types use `|` to compose them:
|
||||||
|
|
||||||
|
```honey
|
||||||
|
process :: func(path: []u8) Ast ! IoError | ParseError { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
Parentheses are optional but can aid readability:
|
||||||
|
|
||||||
|
```honey
|
||||||
|
process :: func(path: []u8) Ast ! (IoError | ParseError) { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
Composed error types can also be defined standalone:
|
||||||
|
|
||||||
|
```honey
|
||||||
|
ProcessError :: IoError | ParseError | ValidationError
|
||||||
|
process :: func(path: []u8) Ast ! ProcessError { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
### Inline Error Types
|
||||||
|
|
||||||
|
For simple one-off cases, you can inline the error type:
|
||||||
|
|
||||||
|
```honey
|
||||||
|
# Inline enum
|
||||||
|
simple :: func() void ! enum{Failed, Timeout} { ... }
|
||||||
|
|
||||||
|
# Inline tagged union (when payloads are needed)
|
||||||
|
complex :: func() Data ! union(enum) {
|
||||||
|
OutOfMemory: struct { requested: usize },
|
||||||
|
InvalidInput: void,
|
||||||
|
} { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
For anything non-trivial, prefer named error types for clarity.
|
||||||
|
|
||||||
|
### Returning Errors
|
||||||
|
|
||||||
|
The `error` keyword is a control flow statement that exits the function via the error channel — symmetric to return for success values:
|
||||||
|
|
||||||
|
```honey
|
||||||
|
parse_section :: func(p: @mut Parser) void ! ParseError {
|
||||||
|
start_line := p.line
|
||||||
|
p.advance()
|
||||||
|
|
||||||
|
# ... parsing logic ...
|
||||||
|
|
||||||
|
if p.pos >= p.input.len or p.input[p.pos] != ']' {
|
||||||
|
error .unclosed_section{.line = start_line}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ... continue on success ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Since errors are just union values, you can also construct them separately:
|
||||||
|
|
||||||
|
```honey
|
||||||
|
# Construct error value (it's just a union)
|
||||||
|
e: ParseError = .timeout{.ms = 500}
|
||||||
|
|
||||||
|
# Return it via error channel later
|
||||||
|
error e
|
||||||
|
```
|
||||||
|
|
||||||
|
The symmetry:
|
||||||
|
|
||||||
|
* `return x` — exit function with success value
|
||||||
|
* `error e` — exit function with error value
|
||||||
|
|
||||||
|
### Propagation with `try`
|
||||||
|
|
||||||
|
The `try` keyword unwraps a successful result or returns early with the error:
|
||||||
|
|
||||||
|
```honey
|
||||||
|
process :: func(path: []u8) Ast ! IoError | ParseError {
|
||||||
|
data := try read_file(path) # returns early if IoError
|
||||||
|
ast := try parse(data) # returns early if ParseError
|
||||||
|
return ast
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
When the function's error type is a composition, `try` automatically widens narrower error types to match the return type.
|
||||||
|
|
||||||
|
### Handling with `catch`
|
||||||
|
|
||||||
|
The `catch` keyword handles errors and provides a value to continue with.
|
||||||
|
|
||||||
|
**Provide a fallback value:**
|
||||||
|
|
||||||
|
```honey
|
||||||
|
data := read_file(path) catch default_data
|
||||||
|
```
|
||||||
|
|
||||||
|
**Handle with a block using** `yield`:
|
||||||
|
|
||||||
|
```honey
|
||||||
|
data := read_file(path) catch |e| {
|
||||||
|
log("read failed: {}", {e})
|
||||||
|
yield empty_data
|
||||||
|
}
|
||||||
|
use(data) # continues with data = empty_data
|
||||||
|
```
|
||||||
|
|
||||||
|
The `yield` keyword provides a value from a block to the enclosing expression. Execution continues after the statement. For single expressions, yield is implicit (e.g., `catch default_data`). For blocks, explicit `yield` is required. See the Yield section under Control Flow for the full rule.
|
||||||
|
|
||||||
|
**Return from the function instead:**
|
||||||
|
|
||||||
|
```honey
|
||||||
|
data := read_file(path) catch |e| {
|
||||||
|
log("read failed: {}", {e})
|
||||||
|
return # exits the enclosing function
|
||||||
|
}
|
||||||
|
use(data) # never reached if error occurred
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `return` when the error is unrecoverable at this level.
|
||||||
|
|
||||||
|
**Match on specific error variants:**
|
||||||
|
|
||||||
|
```honey
|
||||||
|
data := read_file(path) catch |e| match e {
|
||||||
|
.NotFound |info|: {
|
||||||
|
print("file not found: {s}", {info.path})
|
||||||
|
yield create_default(info.path)
|
||||||
|
},
|
||||||
|
.Timeout |info|: {
|
||||||
|
print("timed out after {d}ms", {info.after_ms})
|
||||||
|
yield retry(path)
|
||||||
|
},
|
||||||
|
.PermissionDenied: panic("cannot recover from permission error"),
|
||||||
|
.ConnectionReset: retry(path),
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
|
||||||
|
| Syntax | Meaning |
|
||||||
|
| -- | -- |
|
||||||
|
| `T ! E` | Function returns `T` or fails with `E` |
|
||||||
|
| `T ! E1 | E2` | Function can fail with either error type |
|
||||||
|
| `T ! (E1 | E2)` | Same, with optional parentheses |
|
||||||
|
| `error e` | Exit function via error channel |
|
||||||
|
| `error .variant{...}` | Construct and return error in one step |
|
||||||
|
| `try expr` | Unwrap success or propagate error |
|
||||||
|
| `expr catch fallback` | Provide fallback value on error |
|
||||||
|
| `expr catch |e| { ... }` | Handle error with block |
|
||||||
|
| `yield value` | Provide value from innermost block |
|
||||||
|
| `yield :label value` | Provide value from labeled block |
|
||||||
|
| `return` / `return value` | Exit function from within catch block |
|
||||||
|
|
||||||
## A word on memory allocation
|
## A word on memory allocation
|
||||||
|
|
||||||
(NOTE THAT SYNTAX MAY NOT MATCH BROLANG EXACTLY AND SHOULD BE TAKEN WITH A GRAIN OF SALT - INSPIRATION ONLY)
|
(NOTE THAT SYNTAX MAY NOT MATCH BROLANG EXACTLY AND SHOULD BE TAKEN WITH A GRAIN OF SALT - INSPIRATION ONLY)
|
||||||
|
|||||||
@@ -134,6 +134,8 @@ Stmt_Kind :: enum u8 {
|
|||||||
Block,
|
Block,
|
||||||
Defer,
|
Defer,
|
||||||
Yield,
|
Yield,
|
||||||
|
Match,
|
||||||
|
Match_Arm,
|
||||||
}
|
}
|
||||||
|
|
||||||
Assignment_Op :: enum u8 {
|
Assignment_Op :: enum u8 {
|
||||||
@@ -175,6 +177,10 @@ Stmt :: struct {
|
|||||||
// `Block` statements (a bare `{ ... }` scope) use `body` as their statements.
|
// `Block` statements (a bare `{ ... }` scope) use `body` as their statements.
|
||||||
// `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
|
||||||
|
// statements (each a `Match_Arm`). A `Match_Arm` uses `expr` as its pattern
|
||||||
|
// (`INVALID_EXPR` marks the `else` arm), `captures` for the optional payload
|
||||||
|
// capture (0 or 1 name, tagged-union variants only), and `body` as the arm body.
|
||||||
captures: []symbol.Id,
|
captures: []symbol.Id,
|
||||||
guard: Expr_Id,
|
guard: Expr_Id,
|
||||||
body: []Stmt_Id,
|
body: []Stmt_Id,
|
||||||
|
|||||||
@@ -175,6 +175,16 @@ symbol_text :: proc(checker: ^Checker, id: symbol.Id) -> string {
|
|||||||
return symbol.resolve(checker.symbols, id)
|
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 {
|
Constant_Frame :: struct {
|
||||||
expr: ast.Expr_Id,
|
expr: ast.Expr_Id,
|
||||||
stage: u8,
|
stage: u8,
|
||||||
@@ -776,6 +786,11 @@ mark_block_imports_used :: proc(checker: ^Checker, statements: []ast.Stmt_Id, fi
|
|||||||
case .Defer:
|
case .Defer:
|
||||||
deferred := [1]ast.Stmt_Id{statement.update}
|
deferred := [1]ast.Stmt_Id{statement.update}
|
||||||
mark_block_imports_used(checker, deferred[:], file)
|
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 .Break, .Continue:
|
||||||
case .Invalid:
|
case .Invalid:
|
||||||
}
|
}
|
||||||
@@ -4923,6 +4938,18 @@ build_block :: proc(
|
|||||||
ctx.loop_floor = saved_floor
|
ctx.loop_floor = saved_floor
|
||||||
ctx.defer_depth -= 1
|
ctx.defer_depth -= 1
|
||||||
append(ctx.defers, entry)
|
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:
|
case .Invalid:
|
||||||
append(&body, hir.stmt_id(len(checker.module.statements)))
|
append(&body, hir.stmt_id(len(checker.module.statements)))
|
||||||
append(&checker.module.statements, hir.Stmt{
|
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)
|
return build_value_if(ctx, body, body_stmts[0], expected, span)
|
||||||
case .For, .While:
|
case .For, .While:
|
||||||
return build_value_loop(ctx, body, body_stmts[0], expected, span)
|
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)
|
return build_value_block(ctx, body, body_stmts, expected, span)
|
||||||
@@ -5309,6 +5338,415 @@ emit_value_branch :: proc(
|
|||||||
return true
|
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
|
// 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
|
// `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
|
// literal `none` — making the loop's result optional. Pure AST walk; does not descend
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ Expr_Kind :: enum u8 {
|
|||||||
Index,
|
Index,
|
||||||
Slice,
|
Slice,
|
||||||
Field,
|
Field,
|
||||||
|
Union_Tag,
|
||||||
Length,
|
Length,
|
||||||
Slice_Ptr,
|
Slice_Ptr,
|
||||||
Unwrap,
|
Unwrap,
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ Opcode :: enum u8 {
|
|||||||
Alloca,
|
Alloca,
|
||||||
Index_Address,
|
Index_Address,
|
||||||
Field_Address,
|
Field_Address,
|
||||||
|
Union_Tag,
|
||||||
Load,
|
Load,
|
||||||
Store,
|
Store,
|
||||||
Fill,
|
Fill,
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ keyword_kind :: proc(text: string) -> token.Kind {
|
|||||||
case "continue": return .Keyword_Continue
|
case "continue": return .Keyword_Continue
|
||||||
case "defer": return .Keyword_Defer
|
case "defer": return .Keyword_Defer
|
||||||
case "yield": return .Keyword_Yield
|
case "yield": return .Keyword_Yield
|
||||||
|
case "match": return .Keyword_Match
|
||||||
case "else": return .Keyword_Else
|
case "else": return .Keyword_Else
|
||||||
case "true": return .Keyword_True
|
case "true": return .Keyword_True
|
||||||
case "false": return .Keyword_False
|
case "false": return .Keyword_False
|
||||||
|
|||||||
+15
-1
@@ -253,7 +253,7 @@ valid_value :: proc(
|
|||||||
}
|
}
|
||||||
switch instructions[value_id].op {
|
switch instructions[value_id].op {
|
||||||
case .Param, .Const, .String, .Aggregate, .None, .Optional_Some,
|
case .Param, .Const, .String, .Aggregate, .None, .Optional_Some,
|
||||||
.Load_Global, .Function_Address, .Address_Of, .Load, .Slice, .Length, .Slice_Ptr,
|
.Load_Global, .Function_Address, .Address_Of, .Load, .Union_Tag, .Slice, .Length, .Slice_Ptr,
|
||||||
.Extract, .Select, .Unwrap,
|
.Extract, .Select, .Unwrap,
|
||||||
.Optional_Is_Some, .Optional_Value, .Orelse,
|
.Optional_Is_Some, .Optional_Value, .Orelse,
|
||||||
.Widen, .C_Coerce, .C_Vararg_Promote, .Retype, .Weaken_Pointer, .Weaken_Slice, .Decay_Array_Pointer,
|
.Widen, .C_Coerce, .C_Vararg_Promote, .Retype, .Weaken_Pointer, .Weaken_Slice, .Decay_Array_Pointer,
|
||||||
@@ -979,6 +979,20 @@ emit_instruction_stream :: proc(
|
|||||||
llvm_type(instruction.type, &emitter.module.types),
|
llvm_type(instruction.type, &emitter.module.types),
|
||||||
instruction.a,
|
instruction.a,
|
||||||
)
|
)
|
||||||
|
case .Union_Tag:
|
||||||
|
// `instruction.a` is the address of a tagged union; the discriminant lives at
|
||||||
|
// offset 0, so load the tag enum (`instruction.type`) straight from it.
|
||||||
|
if !valid_instruction(instructions, instruction.a) {
|
||||||
|
emit_recovery_value(emitter, instruction_index, instruction, "invalid union tag base")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fmt.sbprintf(
|
||||||
|
&emitter.builder,
|
||||||
|
" %%v%d = load %s, ptr %%v%d\n",
|
||||||
|
instruction_index,
|
||||||
|
llvm_type(instruction.type, &emitter.module.types),
|
||||||
|
instruction.a,
|
||||||
|
)
|
||||||
case .Store:
|
case .Store:
|
||||||
if !valid_address(instructions, instruction.a, instruction.type, &emitter.module.types) ||
|
if !valid_address(instructions, instruction.a, instruction.type, &emitter.module.types) ||
|
||||||
!valid_value(instructions, instruction.b, instruction.type, &emitter.module.types) {
|
!valid_value(instructions, instruction.b, instruction.type, &emitter.module.types) {
|
||||||
|
|||||||
@@ -275,6 +275,18 @@ lower_compound_expr :: proc(state: ^State, expr_id: hir.Expr_Id) -> ir.Instructi
|
|||||||
target=ir.INVALID_REF, a=location, b=ir.INVALID_INSTRUCTION,
|
target=ir.INVALID_REF, a=location, b=ir.INVALID_INSTRUCTION,
|
||||||
diagnostic=source.INVALID_DIAGNOSTIC,
|
diagnostic=source.INVALID_DIAGNOSTIC,
|
||||||
})
|
})
|
||||||
|
case .Union_Tag:
|
||||||
|
// Read a tagged union's discriminant: the tag sits at offset 0, so the union's
|
||||||
|
// address is the tag's address — load the tag enum (`expr.type`) directly.
|
||||||
|
address := lower_location(state, expr.left)
|
||||||
|
if address == ir.INVALID_INSTRUCTION {
|
||||||
|
return append_recovery_value(state, expr.span, expr.type, expr.diagnostic)
|
||||||
|
}
|
||||||
|
return append_instruction(state, ir.Instruction{
|
||||||
|
op=.Union_Tag, span=expr.span, type=expr.type,
|
||||||
|
target=ir.INVALID_REF, a=address, b=ir.INVALID_INSTRUCTION,
|
||||||
|
diagnostic=source.INVALID_DIAGNOSTIC,
|
||||||
|
})
|
||||||
case .Slice:
|
case .Slice:
|
||||||
// Array operands are addressed (locations) or spilled to a temporary
|
// Array operands are addressed (locations) or spilled to a temporary
|
||||||
// (rvalues) by lower_location; other containers are slice/pointer values.
|
// (rvalues) by lower_location; other containers are slice/pointer values.
|
||||||
@@ -460,7 +472,7 @@ lower_expr :: proc(state: ^State, expr_id: hir.Expr_Id) -> ir.Instruction_Id {
|
|||||||
})
|
})
|
||||||
_ = pop(&stack)
|
_ = pop(&stack)
|
||||||
case .String, .Array, .Struct, .Range, .None, .Optional_Some, .Address, .Deref,
|
case .String, .Array, .Struct, .Range, .None, .Optional_Some, .Address, .Deref,
|
||||||
.Index, .Slice, .Field, .Length, .Slice_Ptr, .Unwrap, .Orelse,
|
.Index, .Slice, .Field, .Union_Tag, .Length, .Slice_Ptr, .Unwrap, .Orelse,
|
||||||
.Not, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or:
|
.Not, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or:
|
||||||
last = lower_compound_expr(state, frame.expr)
|
last = lower_compound_expr(state, frame.expr)
|
||||||
_ = pop(&stack)
|
_ = pop(&stack)
|
||||||
|
|||||||
+109
-1
@@ -1128,6 +1128,7 @@ parse_value_control_flow :: proc(parser: ^Parser) -> (ast.Stmt_Id, bool) {
|
|||||||
case .Keyword_If: return parse_if(parser), true
|
case .Keyword_If: return parse_if(parser), true
|
||||||
case .Keyword_For: return parse_for(parser), true
|
case .Keyword_For: return parse_for(parser), true
|
||||||
case .Keyword_While: return parse_while(parser), true
|
case .Keyword_While: return parse_while(parser), true
|
||||||
|
case .Keyword_Match: return parse_match(parser), true
|
||||||
}
|
}
|
||||||
return ast.INVALID_STMT, false
|
return ast.INVALID_STMT, false
|
||||||
}
|
}
|
||||||
@@ -1157,6 +1158,9 @@ parse_statement :: proc(parser: ^Parser) -> ast.Stmt_Id {
|
|||||||
if current(parser).kind == .Keyword_Yield {
|
if current(parser).kind == .Keyword_Yield {
|
||||||
return parse_yield(parser)
|
return parse_yield(parser)
|
||||||
}
|
}
|
||||||
|
if current(parser).kind == .Keyword_Match {
|
||||||
|
return parse_match(parser)
|
||||||
|
}
|
||||||
// A leading `{` opens a bare block scope (struct literals are postfix only).
|
// A leading `{` opens a bare block scope (struct literals are postfix only).
|
||||||
if current(parser).kind == .Left_Brace {
|
if current(parser).kind == .Left_Brace {
|
||||||
return parse_block_statement(parser)
|
return parse_block_statement(parser)
|
||||||
@@ -1526,6 +1530,110 @@ parse_if :: proc(parser: ^Parser) -> ast.Stmt_Id {
|
|||||||
return id
|
return id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parse_arm_body parses a match arm's body after the `:`: a braced block (whose
|
||||||
|
// inner statements are returned unwrapped, like `parse_branch_body`) or a single
|
||||||
|
// brace-less statement. For value-match a brace-less body is a single expression
|
||||||
|
// that the checker yields implicitly.
|
||||||
|
parse_arm_body :: proc(parser: ^Parser) -> []ast.Stmt_Id {
|
||||||
|
skip_newlines(parser)
|
||||||
|
if current(parser).kind == .Left_Brace {
|
||||||
|
return parse_block(parser)
|
||||||
|
}
|
||||||
|
single := make([]ast.Stmt_Id, 1, parser.module.allocator)
|
||||||
|
single[0] = parse_statement(parser)
|
||||||
|
return single
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse_match_arm parses one `<pattern> [|capture|]: <body>` arm (or `else: <body>`).
|
||||||
|
// The pattern is `INVALID_EXPR` for `else`; `captures` holds the optional 0-or-1
|
||||||
|
// payload capture name (tagged-union variants only).
|
||||||
|
parse_match_arm :: proc(parser: ^Parser) -> ast.Stmt_Id {
|
||||||
|
start := current(parser).span
|
||||||
|
pattern := ast.INVALID_EXPR
|
||||||
|
captures: [dynamic]symbol.Id
|
||||||
|
captures.allocator = parser.module.allocator
|
||||||
|
if _, is_else := allow(parser, .Keyword_Else); !is_else {
|
||||||
|
saved := parser.no_struct_literal
|
||||||
|
parser.no_struct_literal = true
|
||||||
|
pattern = parse_expression(parser)
|
||||||
|
parser.no_struct_literal = saved
|
||||||
|
if _, ok := allow(parser, .Pipe); ok {
|
||||||
|
name_tok := current(parser)
|
||||||
|
if name_tok.kind == .Identifier || name_tok.kind == .Underscore {
|
||||||
|
advance(parser)
|
||||||
|
append(&captures, name_tok.symbol)
|
||||||
|
} else {
|
||||||
|
source.add(parser.diagnostics, current(parser).span, "expected a capture name after '|'")
|
||||||
|
}
|
||||||
|
if _, close_ok := allow(parser, .Pipe); !close_ok {
|
||||||
|
source.add(parser.diagnostics, current(parser).span, "expected '|' to close the match capture")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, ok := allow(parser, .Colon); !ok {
|
||||||
|
source.add(parser.diagnostics, current(parser).span, "expected ':' after a match pattern")
|
||||||
|
}
|
||||||
|
body := parse_arm_body(parser)
|
||||||
|
id := ast.stmt_id(len(parser.module.statements))
|
||||||
|
append(&parser.module.statements, ast.Stmt{
|
||||||
|
kind=.Match_Arm,
|
||||||
|
span=span_from(start, previous(parser).span),
|
||||||
|
expr=pattern,
|
||||||
|
captures=captures[:],
|
||||||
|
body=body,
|
||||||
|
target=ast.INVALID_EXPR,
|
||||||
|
update=ast.INVALID_STMT,
|
||||||
|
diagnostic=source.INVALID_DIAGNOSTIC,
|
||||||
|
})
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse_match parses `match <subject> { <arm>* }`. Arms are newline-separated; each
|
||||||
|
// is a `Match_Arm` statement stored in the `Match`'s `body`. Usable as a statement
|
||||||
|
// and (via `parse_value_control_flow`) as a value source on a declaration/assignment.
|
||||||
|
parse_match :: proc(parser: ^Parser) -> ast.Stmt_Id {
|
||||||
|
start := advance(parser) // consume 'match'
|
||||||
|
skip_newlines(parser)
|
||||||
|
saved := parser.no_struct_literal
|
||||||
|
parser.no_struct_literal = true
|
||||||
|
subject := parse_expression(parser)
|
||||||
|
parser.no_struct_literal = saved
|
||||||
|
arms: [dynamic]ast.Stmt_Id
|
||||||
|
arms.allocator = parser.module.allocator
|
||||||
|
skip_newlines(parser)
|
||||||
|
if _, ok := allow(parser, .Left_Brace); !ok {
|
||||||
|
source.add(parser.diagnostics, current(parser).span, "expected '{' to open match arms")
|
||||||
|
}
|
||||||
|
skip_newlines(parser)
|
||||||
|
for current(parser).kind != .Right_Brace && current(parser).kind != .Eof {
|
||||||
|
append(&arms, parse_match_arm(parser))
|
||||||
|
if diagnostic := finish_statement(parser, true); diagnostic != source.INVALID_DIAGNOSTIC {
|
||||||
|
arm_id := ast.stmt_id(len(parser.module.statements))
|
||||||
|
append(&parser.module.statements, ast.Stmt{
|
||||||
|
kind=.Invalid,
|
||||||
|
span=current(parser).span,
|
||||||
|
expr=ast.INVALID_EXPR,
|
||||||
|
diagnostic=diagnostic,
|
||||||
|
})
|
||||||
|
append(&arms, arm_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, ok := allow(parser, .Right_Brace); !ok {
|
||||||
|
source.add(parser.diagnostics, current(parser).span, "expected '}' to close match arms")
|
||||||
|
}
|
||||||
|
id := ast.stmt_id(len(parser.module.statements))
|
||||||
|
append(&parser.module.statements, ast.Stmt{
|
||||||
|
kind=.Match,
|
||||||
|
span=span_from(start.span, previous(parser).span),
|
||||||
|
expr=subject,
|
||||||
|
body=arms[:],
|
||||||
|
target=ast.INVALID_EXPR,
|
||||||
|
update=ast.INVALID_STMT,
|
||||||
|
diagnostic=source.INVALID_DIAGNOSTIC,
|
||||||
|
})
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
parse_while_update :: proc(parser: ^Parser) -> ast.Stmt_Id {
|
parse_while_update :: proc(parser: ^Parser) -> ast.Stmt_Id {
|
||||||
parenthesized := false
|
parenthesized := false
|
||||||
if _, ok := allow(parser, .Left_Paren); ok {
|
if _, ok := allow(parser, .Left_Paren); ok {
|
||||||
@@ -1559,7 +1667,7 @@ parse_while_update :: proc(parser: ^Parser) -> ast.Stmt_Id {
|
|||||||
statement := &parser.module.statements[update]
|
statement := &parser.module.statements[update]
|
||||||
switch statement.kind {
|
switch statement.kind {
|
||||||
case .Assignment, .Expression:
|
case .Assignment, .Expression:
|
||||||
case .Invalid, .Declaration, .Return, .If, .While, .For, .Break, .Continue, .Block, .Defer, .Yield:
|
case .Invalid, .Declaration, .Return, .If, .While, .For, .Break, .Continue, .Block, .Defer, .Yield, .Match, .Match_Arm:
|
||||||
diagnostic := source.add(
|
diagnostic := source.add(
|
||||||
parser.diagnostics,
|
parser.diagnostics,
|
||||||
statement.span,
|
statement.span,
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ Kind :: enum u8 {
|
|||||||
Keyword_Continue,
|
Keyword_Continue,
|
||||||
Keyword_Defer,
|
Keyword_Defer,
|
||||||
Keyword_Yield,
|
Keyword_Yield,
|
||||||
|
Keyword_Match,
|
||||||
Keyword_Else,
|
Keyword_Else,
|
||||||
Keyword_True,
|
Keyword_True,
|
||||||
Keyword_False,
|
Keyword_False,
|
||||||
|
|||||||
@@ -2251,6 +2251,157 @@ main :: func() i32 {
|
|||||||
testing.expect(t, found_variant)
|
testing.expect(t, found_variant)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
match_compiles_and_runs :: proc(t: ^testing.T) {
|
||||||
|
output := "/tmp/brolang-test-match"
|
||||||
|
defer _ = os.remove(output)
|
||||||
|
status := compiler_core.compile_package("examples/programs/match", output)
|
||||||
|
testing.expect_value(t, status, 0)
|
||||||
|
state := run_executable(output)
|
||||||
|
// Exercises every match form — tagged-union statement match with payload capture,
|
||||||
|
// tagged-union value match (implicit + explicit yield), enum statement/value match,
|
||||||
|
// and integer match with `else` — all summed to a self-checking 0 on success.
|
||||||
|
testing.expect_value(t, state.exit_code, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
match_dispatches_on_the_tag :: proc(t: ^testing.T) {
|
||||||
|
// A tagged-union match desugars to: read the discriminant once (a load of the tag
|
||||||
|
// enum at the union's offset 0), then compare it against each variant's tag value.
|
||||||
|
// `Animal` is dense in a u8 backing (dog=0, bird=2), so the dog arm compares the
|
||||||
|
// loaded i8 tag against 0.
|
||||||
|
text := `Animal :: enum {
|
||||||
|
dog
|
||||||
|
cat
|
||||||
|
bird
|
||||||
|
}
|
||||||
|
Data :: union(Animal) {
|
||||||
|
dog i32
|
||||||
|
bird i32
|
||||||
|
}
|
||||||
|
main :: func() i32 {
|
||||||
|
d Data = Data{ bird = 7 }
|
||||||
|
out i32 = 0
|
||||||
|
match d {
|
||||||
|
.dog |v|: out = v
|
||||||
|
.bird |v|: out = v + 1
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
// The tag is loaded as an i8 and compared (icmp eq) against the dog variant's tag (0).
|
||||||
|
testing.expect(t, strings.contains(llvm_text, "load i8"))
|
||||||
|
testing.expect(t, strings.contains(llvm_text, "icmp eq i8"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
match_misuse_is_diagnosed :: proc(t: ^testing.T) {
|
||||||
|
// Five rejected matches: (1) a non-exhaustive enum match with no `else`, (2) a scalar
|
||||||
|
// match missing its mandatory `else`, (3) a payload capture on a non-union arm, (4) a
|
||||||
|
// redundant `else` on an already-exhaustive match, and (5) an unknown variant.
|
||||||
|
text := `Animal :: enum {
|
||||||
|
dog
|
||||||
|
cat
|
||||||
|
bird
|
||||||
|
}
|
||||||
|
Data :: union(Animal) {
|
||||||
|
dog i32
|
||||||
|
bird i32
|
||||||
|
}
|
||||||
|
not_exhaustive :: func(a Animal) i32 {
|
||||||
|
match a {
|
||||||
|
.dog: { return 1 }
|
||||||
|
.cat: { return 2 }
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
missing_else :: func(n i32) i32 {
|
||||||
|
match n {
|
||||||
|
0: { return 1 }
|
||||||
|
1: { return 2 }
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
bad_capture :: func(a Animal) i32 {
|
||||||
|
match a {
|
||||||
|
.dog |v|: { return 1 }
|
||||||
|
.cat: { return 2 }
|
||||||
|
.bird: { return 3 }
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
redundant_else :: func(a Animal) i32 {
|
||||||
|
match a {
|
||||||
|
.dog: { return 1 }
|
||||||
|
.cat: { return 2 }
|
||||||
|
.bird: { return 3 }
|
||||||
|
else: { return 4 }
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
unknown_variant :: func(d Data) i32 {
|
||||||
|
match d {
|
||||||
|
.dog: { return 1 }
|
||||||
|
.snake: { return 2 }
|
||||||
|
else: { return 3 }
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
main :: func() i32 {
|
||||||
|
# Functions are specialized on use, so call each so its body is type-checked.
|
||||||
|
d Data = Data{ dog = 0 }
|
||||||
|
return not_exhaustive(.dog) + missing_else(0) + bad_capture(.dog) +
|
||||||
|
redundant_else(.dog) + unknown_variant(d)
|
||||||
|
}
|
||||||
|
`
|
||||||
|
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_exhaustive := false
|
||||||
|
found_missing_else := false
|
||||||
|
found_capture := false
|
||||||
|
found_redundant := false
|
||||||
|
found_unknown := false
|
||||||
|
for diagnostic in diagnostics.items {
|
||||||
|
found_exhaustive = found_exhaustive || strings.contains(diagnostic.message, "is not exhaustive")
|
||||||
|
found_missing_else = found_missing_else || strings.contains(diagnostic.message, "requires an 'else' arm")
|
||||||
|
found_capture = found_capture || strings.contains(diagnostic.message, "only tagged-union variants can capture")
|
||||||
|
found_redundant = found_redundant || strings.contains(diagnostic.message, "redundant 'else'")
|
||||||
|
found_unknown = found_unknown || strings.contains(diagnostic.message, "unknown variant '.snake'")
|
||||||
|
}
|
||||||
|
testing.expect(t, found_exhaustive)
|
||||||
|
testing.expect(t, found_missing_else)
|
||||||
|
testing.expect(t, found_capture)
|
||||||
|
testing.expect(t, found_redundant)
|
||||||
|
testing.expect(t, found_unknown)
|
||||||
|
}
|
||||||
|
|
||||||
@(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
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
Animal :: enum {
|
||||||
|
dog
|
||||||
|
cat
|
||||||
|
bird
|
||||||
|
}
|
||||||
|
|
||||||
|
# tagged union over an existing enum (variants are a subset of the enum's members)
|
||||||
|
Data :: union(Animal) {
|
||||||
|
dog i32
|
||||||
|
bird i32
|
||||||
|
}
|
||||||
|
|
||||||
|
# tagged union with a synthesized tag enum
|
||||||
|
Shape :: union(enum) {
|
||||||
|
square i32 # side
|
||||||
|
circle i32 # radius
|
||||||
|
}
|
||||||
|
|
||||||
|
# Statement match with payload capture; every variant returns, so the function needs
|
||||||
|
# no trailing return (the desugared if/else chain covers all paths).
|
||||||
|
describe :: func(d Data) i32 {
|
||||||
|
match d {
|
||||||
|
.dog |age|: return age + 1
|
||||||
|
.bird |wingspan|: return wingspan + 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Value match: single-expression arms yield implicitly, a block arm yields explicitly.
|
||||||
|
area :: func(s Shape) i32 {
|
||||||
|
result :: match s {
|
||||||
|
.square |side|: side * side
|
||||||
|
.circle |r|: {
|
||||||
|
# 3 ~ pi, integer arithmetic
|
||||||
|
yield 3 * r * r
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
main :: func() i32 {
|
||||||
|
dog Data = Data{ dog = 9 }
|
||||||
|
bird Data = Data{ bird = 38 }
|
||||||
|
total i32 = describe(dog) + describe(bird) # 10 + 40 = 50
|
||||||
|
|
||||||
|
# enum statement match, exhaustive without an `else`
|
||||||
|
a Animal = .bird
|
||||||
|
rank i32 = 0
|
||||||
|
match a {
|
||||||
|
.dog: rank = 1
|
||||||
|
.cat: rank = 2
|
||||||
|
.bird: rank = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
# enum value match
|
||||||
|
legs :: match a {
|
||||||
|
.dog: 4
|
||||||
|
.cat: 4
|
||||||
|
.bird: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
# integer match with a mandatory `else`
|
||||||
|
bucket i32 = 0
|
||||||
|
match rank {
|
||||||
|
1: bucket = 100
|
||||||
|
3: bucket = 5
|
||||||
|
else: bucket = 99
|
||||||
|
}
|
||||||
|
|
||||||
|
sq Shape = Shape{ square = 4 }
|
||||||
|
ci Shape = Shape{ circle = 2 }
|
||||||
|
shapes i32 = area(sq) + area(ci) # 16 + 12 = 28
|
||||||
|
|
||||||
|
# 50 + 3 + 2 + 5 + 28 = 88
|
||||||
|
return total + rank + legs + bucket + shapes - 88
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user