error handling kickoff
This commit is contained in:
@@ -552,11 +552,46 @@
|
||||
inside arm bodies. As a side effect the rvalue pointer-capture guard from 22.5 is now reachable
|
||||
(`match make_box() { .v |@p|: … }` correctly errors "requires an addressable subject")
|
||||
- checker-only; no parser/AST/HIR/IR/lowering/codegen change
|
||||
- deferred: payload-carrying contextual construction (`.variant{…}`, milestone 23); a
|
||||
`yield call()` inside a value-block/value-match arm still misses its spec (`infer_statements`
|
||||
has no `.Yield` case either — a separate pre-existing gap)
|
||||
- deferred: payload-carrying contextual construction (`.variant{…}`) remains outside milestone
|
||||
23 v1. The separate `yield call()` inference gap is fixed in milestone 23.
|
||||
|
||||
23. error types (see below)
|
||||
23. sum-type composition, fallible channels, and yield inference (implemented; v1)
|
||||
- `.Yield` is now visited by specialization-demand inference, so `yield call()` inside value
|
||||
blocks, value-if/value-match branches, and match arms requests the needed call specialization
|
||||
- native unbacked enums and native `union(enum)` / `union(SomeEnum)` variants are registered in a
|
||||
program-global variant table keyed by `(name, payload-type)`. Runtime tags are fixed `u16`
|
||||
global ids; `0` is reserved for fallible success/no-error, and overflow is diagnosed
|
||||
- tagged-union construction, tag reads, matching, and LLVM layout use those global ids rather
|
||||
than per-type dense tags. Component-to-composite widening preserves the tag and copies only the
|
||||
active payload carrier bytes from the source carrier offset to the destination carrier offset
|
||||
- `A | B` composes native unbacked enums and native tagged unions: matching `(name, payload-type)`
|
||||
variants merge, same-name/different-payload conflicts are diagnosed, and backed enums, C enums,
|
||||
and untagged unions are rejected for v1
|
||||
- `T ! E` is represented as a real synthetic fallible channel type in `types.Store`. A fallible
|
||||
function returns success with code `0` or an error variant's global id; `return expr` dispatches
|
||||
based on whether `expr` coerces to the success type `T` or the error type `E`
|
||||
- `try expr` unwraps success and propagates the enclosing function's exact error channel;
|
||||
`expr catch fallback` unwraps success or evaluates the fallback value
|
||||
- runtime coverage lives in `examples/programs/errors`; compiler coverage includes global-id sum
|
||||
merge/conflict/widening/rejections and the `.Yield` inference regression
|
||||
- deferred follow-ups are split below; 23.5 keeps the next user-visible slice small
|
||||
|
||||
23.5. fallible ergonomics: catch blocks + composable try widening
|
||||
- implement `expr catch |e| { ... }` with `e : E`
|
||||
- allow `try` to widen `T ! E1` into an enclosing `T ! (E1 | E2)` channel
|
||||
- add focused tests/examples for both
|
||||
- leave ABI/layout/lint/design polish for later milestones
|
||||
|
||||
23.6. contextual payload construction + inline error types
|
||||
- implement contextual payload `.variant{...}` construction
|
||||
- allow named fallible signatures to use inline enum / tagged-union error types
|
||||
- settle qualified same-name disambiguation only if contextual construction needs it
|
||||
|
||||
23.7. sum-type ABI/layout polish
|
||||
- consider dynamic tag-width shrinking after the fixed-`u16` ABI has real pressure
|
||||
- consider all-void channel collapse after fallible channels are otherwise stable
|
||||
- define cross-module/global-id ABI determinism before multi-module builds depend on it
|
||||
- keep backed/C enum composition and must-consume fallible linting as later policy work
|
||||
|
||||
24. dynamic heap allocation
|
||||
- see below for direction
|
||||
@@ -572,7 +607,7 @@ name: ?[]u8 = get_name()
|
||||
age: ?u8 = get_age()
|
||||
if name and age |n, a| {
|
||||
# both n and a are guaranteed non-none here
|
||||
print("{s} is {d} years old", {n, a})
|
||||
print("{s} is {d} years old", n, a)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -580,7 +615,7 @@ if name and age |n, a| {
|
||||
|
||||
```
|
||||
if name and hat |n, h : n == "Huginn" and h.brand == .gucci| {
|
||||
print("{s}'s got that drip\n", {n})
|
||||
print("{s}'s got that drip\n", n)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -765,7 +800,7 @@ message = "Header:\t" ++
|
||||
|
||||
The `yield` keyword provides a value from a block to its enclosing expression and **exits the block immediately** — just as `return` exits a function, `yield` exits the enclosing scope. Code after a `yield` is unreachable, and the compiler flags it. This makes `yield` part of a consistent set of scope-exiting control flow: `return` exits a function, `yield` exits a block, `break` exits a loop, and `continue` skips to the next iteration.
|
||||
|
||||
It is used in scoped blocks, match arms, and catch handlers.
|
||||
It is used in scoped blocks and match arms today; catch-handler blocks are planned.
|
||||
|
||||
**General rule:** When a block needs to produce a value, single expressions yield implicitly while multi-statement blocks require explicit `yield`. This rule applies uniformly across the language:
|
||||
|
||||
@@ -785,8 +820,7 @@ label []u8 = match p {
|
||||
},
|
||||
}
|
||||
|
||||
# catch handlers
|
||||
data []u8 = read(path) catch default_data # single expression: implicit
|
||||
# catch handlers (planned; block form deferred in milestone 23 v1)
|
||||
data []u8 = read(path) catch |e| {
|
||||
log(e)
|
||||
yield fallback_data # block: explicit yield
|
||||
@@ -895,7 +929,7 @@ match result {
|
||||
process(data)
|
||||
}
|
||||
.failure |info|: {
|
||||
print("error {d}: {s}", { info.code, info.msg })
|
||||
print("error {d}: {s}", info.code, info.msg)
|
||||
}
|
||||
.pending: {
|
||||
# void payload - no capture needed
|
||||
@@ -938,14 +972,16 @@ 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.
|
||||
Brolang 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.
|
||||
|
||||
Milestone 23 v1 implements named error channels, native sum composition, `return`-based error dispatch, exact-channel `try`, and fallback `catch`. It intentionally defers the `error` keyword shorthand, inline error types, `catch |e|` blocks, and `try` widening across different-but-composable error channels.
|
||||
|
||||
### Fallible Functions
|
||||
|
||||
Functions that can fail declare their error type after `!`:
|
||||
|
||||
```honey
|
||||
read_file :: func(path: []u8) []u8 ! IoError { ... }
|
||||
```
|
||||
read_file :: func(path []u8) []u8 ! IoError { ... }
|
||||
```
|
||||
|
||||
This reads as: "returns `[]u8` or fails with `IoError`." The space around `!` is idiomatic but not required.
|
||||
@@ -956,7 +992,7 @@ Errors can be enums (when you only need to identify what went wrong) or tagged u
|
||||
|
||||
**Simple errors (enum):**
|
||||
|
||||
```honey
|
||||
```
|
||||
SimpleError :: enum {
|
||||
OutOfMemory,
|
||||
InvalidSize,
|
||||
@@ -966,12 +1002,12 @@ SimpleError :: enum {
|
||||
|
||||
**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
|
||||
NotFound struct { path: []u8 },
|
||||
PermissionDenied struct { path []u8, operation []u8 },
|
||||
Timeout struct { after_ms u64 },
|
||||
ConnectionReset void, # no additional data needed
|
||||
}
|
||||
```
|
||||
|
||||
@@ -979,7 +1015,7 @@ IoError :: union(enum) {
|
||||
|
||||
When you have a predefined set of error kinds, you can constrain the union:
|
||||
|
||||
```honey
|
||||
```
|
||||
IoErrorKind :: enum {
|
||||
not_found,
|
||||
permission_denied,
|
||||
@@ -987,64 +1023,43 @@ IoErrorKind :: enum {
|
||||
}
|
||||
|
||||
IoError :: union(IoErrorKind) {
|
||||
not_found: struct { path: []u8 },
|
||||
permission_denied: struct { path: []u8 },
|
||||
timeout: struct { after_ms: u64 },
|
||||
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:
|
||||
Functions that can fail with multiple error types use `|` to compose a named error type:
|
||||
|
||||
```honey
|
||||
process :: func(path: []u8) Ast ! IoError | ParseError { ... }
|
||||
```
|
||||
ProcessError :: alias IoError | ParseError
|
||||
process :: func(path []u8) Ast ! ProcessError { ... }
|
||||
```
|
||||
|
||||
Parentheses are optional but can aid readability:
|
||||
Parentheses are optional in the composed type and 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 { ... }
|
||||
ProcessError :: alias (IoError | ParseError)
|
||||
```
|
||||
|
||||
### 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.
|
||||
Inline error types are planned, but not part of milestone 23 v1. Use named enums, named tagged unions, or named aliases for now.
|
||||
|
||||
### Returning Errors
|
||||
|
||||
The `error` keyword is a control flow statement that exits the function via the error channel — symmetric to return for success values:
|
||||
Fallible functions use ordinary `return` for both channels. If the returned expression coerces to the success type `T`, the function returns success with channel code `0`. If it coerces to the error type `E`, the function returns the error with that variant's global tag id:
|
||||
|
||||
```honey
|
||||
```
|
||||
parse_section :: func(p: @mut Parser) void ! ParseError {
|
||||
start_line := p.line
|
||||
start_line Line = p.line
|
||||
p.advance()
|
||||
|
||||
# ... parsing logic ...
|
||||
|
||||
if p.pos >= p.input.len or p.input[p.pos] != ']' {
|
||||
error .unclosed_section{.line = start_line}
|
||||
}
|
||||
if p.pos >= p.input.len or p.input[p.pos] != ']' return ParseError{ unclosed_section = start_line }
|
||||
|
||||
# ... continue on success ...
|
||||
}
|
||||
@@ -1052,48 +1067,49 @@ parse_section :: func(p: @mut Parser) void ! ParseError {
|
||||
|
||||
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}
|
||||
e ParseError = ParseError{ timeout = 500 }
|
||||
|
||||
# Return it via error channel later
|
||||
error e
|
||||
return e
|
||||
```
|
||||
|
||||
The symmetry:
|
||||
|
||||
* `return x` — exit function with success value
|
||||
* `error e` — exit function with error value
|
||||
* `return x` — exits with success when `x` is the success type
|
||||
* `return e` — exits with error when `e` is the error type
|
||||
|
||||
### 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
|
||||
```
|
||||
ProcessError :: alias IoError | ParseError
|
||||
process :: func(path []u8) Ast ! ProcessError {
|
||||
data :: try read_file(path) # read_file also returns []u8 ! ProcessError in v1
|
||||
ast :: try parse(data) # parse also returns Ast ! ProcessError in v1
|
||||
return ast
|
||||
}
|
||||
```
|
||||
|
||||
When the function's error type is a composition, `try` automatically widens narrower error types to match the return type.
|
||||
In v1, `try` propagates only when the callee's error channel exactly matches the enclosing function's error channel. Widening narrower error types into a composed return channel is a planned follow-up.
|
||||
|
||||
### Handling with `catch`
|
||||
|
||||
The `catch` keyword handles errors and provides a value to continue with.
|
||||
The `catch` keyword handles errors and provides a value to continue with. Milestone 23 v1 implements the fallback-value form.
|
||||
|
||||
**Provide a fallback value:**
|
||||
|
||||
```honey
|
||||
data := read_file(path) catch default_data
|
||||
```
|
||||
data :: read_file(path) catch default_data
|
||||
```
|
||||
|
||||
**Handle with a block using** `yield`:
|
||||
**Planned block form using** `yield` (deferred in v1):
|
||||
|
||||
```honey
|
||||
data := read_file(path) catch |e| {
|
||||
log("read failed: {}", {e})
|
||||
```
|
||||
data :: read_file(path) catch |e| {
|
||||
log("read failed: {}", e)
|
||||
yield empty_data
|
||||
}
|
||||
use(data) # continues with data = empty_data
|
||||
@@ -1101,11 +1117,11 @@ 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:**
|
||||
**Planned return-from-handler form** (deferred in v1):
|
||||
|
||||
```honey
|
||||
data := read_file(path) catch |e| {
|
||||
log("read failed: {}", {e})
|
||||
```
|
||||
data :: read_file(path) catch |e| {
|
||||
log("read failed: {}", e)
|
||||
return # exits the enclosing function
|
||||
}
|
||||
use(data) # never reached if error occurred
|
||||
@@ -1113,16 +1129,16 @@ use(data) # never reached if error occurred
|
||||
|
||||
Use `return` when the error is unrecoverable at this level.
|
||||
|
||||
**Match on specific error variants:**
|
||||
**Planned match-on-error form** (deferred in v1):
|
||||
|
||||
```honey
|
||||
data := read_file(path) catch |e| match e {
|
||||
```
|
||||
data :: read_file(path) catch |e| match e {
|
||||
.NotFound |info|: {
|
||||
print("file not found: {s}", {info.path})
|
||||
print("file not found: {s}", info.path)
|
||||
yield create_default(info.path)
|
||||
},
|
||||
.Timeout |info|: {
|
||||
print("timed out after {d}ms", {info.after_ms})
|
||||
print("timed out after {d}ms", info.after_ms)
|
||||
yield retry(path)
|
||||
},
|
||||
.PermissionDenied: panic("cannot recover from permission error"),
|
||||
@@ -1135,16 +1151,17 @@ data := read_file(path) catch |e| match e {
|
||||
| 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 |
|
||||
| `T ! E1 | E2` | Planned direct spelling; use a named alias in v1 |
|
||||
| `T ! (E1 | E2)` | Planned direct spelling with parentheses; use a named alias in v1 |
|
||||
| `return e` | Exit function via error channel when `e : E` |
|
||||
| `error e` | Planned shorthand, not v1 |
|
||||
| `error .variant{...}` | Planned shorthand, not v1 |
|
||||
| `try expr` | Unwrap success or propagate an exact matching error channel |
|
||||
| `expr catch fallback` | Provide fallback value on error |
|
||||
| `expr catch |e| { ... }` | Handle error with block |
|
||||
| `expr catch |e| { ... }` | Planned block handler, not v1 |
|
||||
| `yield value` | Provide value from innermost block |
|
||||
| `yield :label value` | Provide value from labeled block |
|
||||
| `return` / `return value` | Exit function from within catch block |
|
||||
| `return` / `return value` | Exit the current function; in fallible functions, return value dispatches by type |
|
||||
|
||||
## A word on memory allocation
|
||||
|
||||
|
||||
Reference in New Issue
Block a user