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| … }`),
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user