match statements

This commit is contained in:
2026-06-28 23:44:01 +02:00
parent 981ccb047a
commit 462632554c
12 changed files with 1057 additions and 6 deletions
+246 -3
View File
@@ -468,10 +468,43 @@
- deferred to milestone 22 (`match`): safe tag dispatch + payload capture (`match x { .bird |v| … }`),
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
- brolang should feature errors as values
23. error types (see below)
24. dynamic heap allocation
- 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
(NOTE THAT SYNTAX MAY NOT MATCH BROLANG EXACTLY AND SHOULD BE TAKEN WITH A GRAIN OF SALT - INSPIRATION ONLY)