errdefer and try/defer fix

This commit is contained in:
2026-07-13 19:20:22 +02:00
parent 9e75549d02
commit 6de4d9f9f3
23 changed files with 113190 additions and 105482 deletions
+1 -1
View File
@@ -76,7 +76,7 @@ fields. `_` is not a keyword member name.
- `while` loops with optional post-iteration update clauses
- `for` loops over ranges, arrays, slices, and pointers-to-arrays with copy captures, pointer captures `|@item|`, and optional `usize` index captures
- `break`, `continue`, labeled `break :label`, labeled `continue :label`, and labeled plain blocks
- bare block scopes and `defer`, including LIFO flushing on fall-through, `return`, `break`, and `continue`
- bare block scopes, `defer`, and fallible-function `errdefer` with optional error capture; cleanup is block-scoped and LIFO
- value blocks, value `if`, value loops, value `match`, `yield`, and labeled `yield :label value`
- `match` statements/expressions over enums, tagged unions, and scalars, including exhaustiveness checks, payload captures, pointer payload captures, multi-pattern arms, and scalar range patterns
- fallible `try`, fallback `catch`, and `catch |e| { ... }` handler blocks
+1 -1
View File
@@ -196,7 +196,7 @@ Current prototype features:
- Integer and type comptime parameters (`func($N usize) [N]u8`, `func($T type, value T) T`) specialized by explicit or uniquely inferred leading comptime arguments
- Forced typed comptime expressions (`$sum(1, 2)`, `$Point { x = 1, y = 2 }`) and comptime value blocks (`${ yield 4 }`)
- Zig-style comptime type factories returning anonymous native structs (`Box func($T type) type`, used as `Box(i32)`)
- Comptime execution for bodyful Brolang functions with mutable locals, loops, `defer`, `match`, `try`/`catch`, pointer/slice storage mutation, pointer captures, and calls through comptime-known function values
- Comptime execution for bodyful Brolang functions with mutable locals, loops, `defer`/`errdefer`, `match`, `try`/`catch`, pointer/slice storage mutation, pointer captures, and calls through comptime-known function values
- Native function pointer values and types (`*func(...) R`, `*func(...) R ! E`, `?*func(...) R`)
- Typed allocation/reallocation through `std/mem` and generic dynamic arrays through `std/arraylist`
- Bodyless concrete C function declarations with exact external symbol names
+9 -4
View File
@@ -300,6 +300,9 @@
- `defer <stmt>` runs the statement when the enclosing block scope exits, in reverse
(LIFO) order, on every exit path: fall-through, `return`, `break`, `continue`. The
deferred statement may be a block (`defer { ... }`)
- `errdefer [|error|] <stmt>` is the fallible-function counterpart: it runs only when
an explicit or `try`-propagated error exits its active block scope, can capture the
widened enclosing error, and stays interleaved with ordinary defers in LIFO order
- bare block statements `{ ... }` were added as the enabling feature: a `{ ... }`
introduces a nested scope (locals are name-scoped to it; defers inside it fire at the
closing brace). A leading `{` is unambiguous since struct literals are postfix only
@@ -310,10 +313,8 @@
the innermost loop body; fall-through flushes the current block's own defers. Deferring
a `return`/`break`/`continue`/`defer`, a `return` inside a `defer`, or a `break`/
`continue` that would escape a `defer` are all rejected
- implemented entirely in lexer/parser/checker (new `Keyword_Defer`; `Block`/`Defer` AST
kinds reusing `body`/`update`; `parse_block_statement`/`parse_defer`). No HIR opcode:
a bare block is built and spliced inline, and a deferred statement is built once at the
`defer` site and its hir replayed at each exit, so lowering/codegen are unchanged
- cleanup is statically expanded with no runtime registration stack; `try` carries its
active error-exit cleanup into lowering so propagation cannot bypass either defer form
20. add `yield` statement (implemented; first pass — value blocks only; see below)
- a `{ ... }` on the right of a declaration or assignment is a *value block*: its final
@@ -823,6 +824,10 @@
conflicting targets
- root `std` re-exports only `ArrayList(T)` for now; operations remain under `std/arraylist`
35. syntax highlighting (tree-sitter) updates (implemented)
- pointer sigils are highlighted as operators
- type-factory calls in type positions and struct literals are highlighted as functions
## A word on unchecked casts
For casts that bypass safety checks, Honey provides builtin functions:
+3 -1
View File
@@ -169,6 +169,7 @@ Stmt :: struct {
immutable: bool,
value_control_flow: bool,
pointer_capture: bool,
error_only: bool,
// Assignments store the lvalue in `target`, the right-hand side in `expr`,
// and the source operator in `assignment_op`. `Set` is ordinary `=`;
// the arithmetic variants are `+=`, `-=`, `*=`, and `/=`.
@@ -187,7 +188,8 @@ Stmt :: struct {
// distinguish `|@item|` from copy capture.
// `Block` statements (a bare `{ ... }` scope) use `body` as their statements.
// `Defer` statements use `update` as the deferred statement (which may itself
// be a `Block`).
// be a `Block`), `error_only` for `errdefer`, and `captures` for its optional
// error capture.
// `Match` statements use `expr` as the subject and `body` as the list of arm
// statements (each a `Match_Arm`). A `Match_Arm` uses `patterns` as its pattern
// list (empty marks the `else` arm; more than one is a multi-pattern arm),
+121 -21
View File
@@ -72,6 +72,12 @@ Yield_Target :: struct {
defer_floor: int,
}
Defer_Entry :: struct {
body: []hir.Stmt_Id,
error_only: bool,
capture: hir.Local_Id,
}
Build_Ctx :: struct {
checker: ^Checker,
pkg: ast.Package_Id,
@@ -96,7 +102,7 @@ Build_Ctx :: struct {
// to be valid). `defer_depth`/`loop_floor` guard control flow inside a deferred
// statement: `return` is rejected while `defer_depth > 0`, and `break`/`continue`
// only see loops opened within the defer (those past `loop_floor`).
defers: ^[dynamic][]hir.Stmt_Id,
defers: ^[dynamic]Defer_Entry,
loop_defer_starts: ^[dynamic]int,
// Parallel to `loop_defer_starts`: the label of each enclosing break target (INVALID
// when unlabeled), so a `break :L` / `continue :L` can target an outer one. A labeled
@@ -3686,8 +3692,19 @@ infer_statements :: proc(
case .Block:
infer_statements(checker, statement.body, locals, local_types, pkg, file, demanded, result, result_hint)
case .Defer:
capture_start := len(locals^)
if statement.error_only && len(statement.captures) > 0 &&
statement.captures[0] != checker.sink_symbol {
append(locals, Infer_Local{
name=statement.captures[0],
type=types.fallible_error(result^, &checker.module.types),
declared=types.fallible_error(result^, &checker.module.types),
statement=ast.INVALID_STMT,
})
}
deferred := [1]ast.Stmt_Id{statement.update}
infer_statements(checker, deferred[:], locals, local_types, pkg, file, demanded, result, result_hint)
resize(locals, capture_start)
case .Match:
// The build pass desugars `match` to an if/else chain, but inference runs first
// and must still visit the subject and arm bodies so calls there get specialized
@@ -5508,6 +5525,10 @@ build_compound_expr :: proc(
target=hir.INVALID_REF, diagnostic=source.INVALID_DIAGNOSTIC,
})
case .Try:
if checker.current_build_ctx != nil && checker.current_build_ctx.defer_depth > 0 {
id := source.add(checker.diagnostics, expr.span, "cannot 'try' inside a 'defer'")
return invalid_hir_expr(checker, expr.span, id)
}
left_expected := expected if checker.ast_module.exprs[expr.left].kind == .Call else types.INVALID
channel := build_nested_expr(checker, expr.left, locals, global_reads, calls, left_expected, pkg, file)
channel_type := checker.module.exprs[channel].type
@@ -5532,11 +5553,18 @@ build_compound_expr :: proc(
id := source.add(checker.diagnostics, expr.span, "'try' error channel cannot be widened to the enclosing error channel")
return invalid_hir_expr(checker, expr.span, id, success)
}
cleanup: []hir.Stmt_Id
captures: []hir.Expr_Id
if checker.current_build_ctx != nil {
cleanup, captures = try_cleanup(checker.current_build_ctx)
}
return add_hir_expr(checker, hir.Expr{
kind=.Try,
span=expr.span,
type=success,
left=channel,
body=cleanup,
args=captures,
target=hir.INVALID_REF,
right=hir.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
@@ -6701,17 +6729,51 @@ make_link_name :: proc(checker: ^Checker, id: Spec_Id) -> string {
return fmt.aprintf("%s", strings.to_string(builder), allocator = checker.allocator)
}
// Replay the deferred statements in frames `[lo, len(defers))` into `body`,
// innermost-most-recent first (LIFO across frames); each frame's own statements
// keep their forward order. Used at every scope-exit path in `build_block`.
flush_defers :: proc(ctx: ^Build_Ctx, body: ^[dynamic]hir.Stmt_Id, lo: int) {
// Replay eligible cleanup in `[lo, len(defers))` in LIFO order. Error exits run
// both defer forms; other exits skip errdefer. A direct error return supplies its
// preserved payload so captured errors can be initialized before each cleanup.
flush_defers :: proc(
ctx: ^Build_Ctx,
body: ^[dynamic]hir.Stmt_Id,
lo: int,
error_exit := false,
error_value := hir.INVALID_EXPR,
) {
for i := len(ctx.defers^) - 1; i >= lo; i -= 1 {
for stmt_id in ctx.defers^[i] {
entry := ctx.defers^[i]
if entry.error_only && !error_exit {
continue
}
if error_exit && entry.capture != hir.INVALID_LOCAL && error_value != hir.INVALID_EXPR {
append(body, hir.stmt_id(len(ctx.checker.module.statements)))
append(&ctx.checker.module.statements, hir.Stmt{
kind=.Declaration, local=entry.capture, expr=error_value,
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
for stmt_id in entry.body {
append(body, stmt_id)
}
}
}
// Copy the active error-exit cleanup onto a Try expression. Capture locals are
// initialized by lowering once the propagated error has been extracted/widened.
try_cleanup :: proc(ctx: ^Build_Ctx) -> ([]hir.Stmt_Id, []hir.Expr_Id) {
body: [dynamic]hir.Stmt_Id
body.allocator = ctx.checker.allocator
captures: [dynamic]hir.Expr_Id
captures.allocator = ctx.checker.allocator
for i := len(ctx.defers^) - 1; i >= 0; i -= 1 {
entry := ctx.defers^[i]
if entry.error_only && entry.capture != hir.INVALID_LOCAL {
append(&captures, hir.Expr_Id(entry.capture))
}
append(&body, ..entry.body)
}
return body[:], captures[:]
}
build_block :: proc(
ctx: ^Build_Ctx,
statements: []ast.Stmt_Id,
@@ -7144,6 +7206,8 @@ build_block :: proc(
continue
}
value := hir.INVALID_EXPR
error_exit := false
error_value := hir.INVALID_EXPR
if statement.value_control_flow {
if types.kind(ctx.result, store) == .Fallible {
success := types.fallible_success(ctx.result, store)
@@ -7157,13 +7221,12 @@ build_block :: proc(
} else if types.kind(ctx.result, store) == .Fallible {
success := types.fallible_success(ctx.result, store)
error_type := types.fallible_error(ctx.result, store)
error_path := false
expr_ast := checker.ast_module.exprs[statement.expr]
if expr_ast.kind == .Enum_Literal {
success_has := types.sum_has_name(store, success, u32(expr_ast.name))
error_has := types.sum_has_name(store, error_type, u32(expr_ast.name))
if error_has && !success_has {
error_path = true
error_exit = true
} else if error_has && success_has {
id := source.add(checker.diagnostics, expr_ast.span, "ambiguous fallible return member")
value = invalid_hir_expr(checker, expr_ast.span, id, ctx.result)
@@ -7172,7 +7235,7 @@ build_block :: proc(
target_pkg, available := expr_package(checker, expr_ast, ctx.pkg, ctx.file, true)
named := types.find_named(store, u32(target_pkg), u32(expr_ast.name), file=u32(expr_lookup_file(expr_ast, ctx.file))) if available else types.INVALID
named = types.resolve_alias(named, store)
error_path = can_implicitly_convert_type(checker, named, error_type)
error_exit = can_implicitly_convert_type(checker, named, error_type)
} else {
probe := build_expr(
checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls,
@@ -7181,22 +7244,38 @@ build_block :: proc(
probe_type := checker.module.exprs[probe].type
if can_implicitly_convert_type(checker, probe_type, error_type) &&
!can_implicitly_convert_type(checker, probe_type, success) {
error_path = true
error_exit = true
value = probe
} else if can_implicitly_convert_type(checker, probe_type, success) {
value = probe
}
}
if value == hir.INVALID_EXPR {
expected := error_type if error_path else success
expected := error_type if error_exit else success
value = build_expr(
checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls,
expected, ctx.pkg, ctx.file,
)
}
expected := error_type if error_path else success
expected := error_type if error_exit else success
value = coerce_expr(checker, value, expected, statement.span)
value = fallible_aggregate(checker, statement.span, ctx.result, value, error_path)
if error_exit && len(ctx.defers^) > 0 && checker.module.exprs[value].kind != .Invalid {
tmp := append_tracked_local(
ctx.hir_locals, ctx.local_spans, ctx.local_used, ctx.local_warnable,
hir.Local{name=checker.sink_symbol, type=error_type, mutable=false}, source.Span{},
)
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind=.Declaration, span=statement.span, local=tmp, expr=value,
diagnostic=source.INVALID_DIAGNOSTIC,
})
error_value = hir.expr_id(len(checker.module.exprs))
append(&checker.module.exprs, hir.Expr{
kind=.Local, span=statement.span, type=error_type, target=hir.local_ref(tmp),
})
value = error_value
}
value = fallible_aggregate(checker, statement.span, ctx.result, value, error_exit)
} else {
value = build_expr(
checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls,
@@ -7210,7 +7289,7 @@ build_block :: proc(
// can't change what is returned Zig evaluates the return value, then
// runs defers.
if len(ctx.defers^) > 0 {
if checker.module.exprs[value].kind != .Invalid {
if !error_exit && checker.module.exprs[value].kind != .Invalid {
tmp := append_tracked_local(
ctx.hir_locals,
ctx.local_spans,
@@ -7229,7 +7308,7 @@ build_block :: proc(
kind = .Local, span = statement.span, type = ctx.result, target = hir.local_ref(tmp),
})
}
flush_defers(ctx, &body, 0)
flush_defers(ctx, &body, 0, error_exit, error_value)
}
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
@@ -7753,16 +7832,37 @@ build_block :: proc(
ctx.problematic^ = true
continue
}
if statement.error_only && types.kind(ctx.result, store) != .Fallible {
id := source.add(checker.diagnostics, statement.span, "'errdefer' requires an enclosing fallible function")
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=id,
})
ctx.problematic^ = true
continue
}
// Build the deferred statement once, guarded so a `return` inside it is
// rejected and `break`/`continue` only target loops opened within the
// defer; its hir is replayed at each scope exit, not emitted here.
capture_start := len(ctx.locals^)
capture := hir.INVALID_LOCAL
if statement.error_only && len(statement.captures) > 0 &&
statement.captures[0] != checker.sink_symbol {
capture = append_build_local(
ctx, statement.captures[0], types.fallible_error(ctx.result, store), false, statement.span,
)
}
saved_floor := ctx.loop_floor
ctx.defer_depth += 1
ctx.loop_floor = len(ctx.loop_defer_starts^)
entry := build_block(ctx, []ast.Stmt_Id{statement.update})
ctx.loop_floor = saved_floor
ctx.defer_depth -= 1
append(ctx.defers, entry)
resize(ctx.locals, capture_start)
append(ctx.defers, Defer_Entry{
body=entry, error_only=statement.error_only, capture=capture,
})
case .Match:
build_match(ctx, &body, statement)
case .Match_Arm:
@@ -7795,7 +7895,7 @@ build_block :: proc(
// Free this block's deferred-statement entry slices (their stmt ids were
// already replayed at every path that can leave this block) and pop the frame.
for i := defer_start; i < len(ctx.defers^); i += 1 {
delete(ctx.defers^[i], checker.allocator)
delete(ctx.defers^[i].body, checker.allocator)
}
resize(ctx.defers, defer_start)
resize(ctx.locals, scope_start)
@@ -7887,7 +7987,7 @@ build_value_block :: proc(
}
// Close the scope (build_block left it open for us).
for i := defer_start; i < len(ctx.defers^); i += 1 {
delete(ctx.defers^[i], checker.allocator)
delete(ctx.defers^[i].body, checker.allocator)
}
resize(ctx.defers, defer_start)
resize(ctx.locals, scope_start)
@@ -8924,7 +9024,7 @@ block_element_type :: proc(ctx: ^Build_Ctx, block_stmts: []ast.Stmt_Id) -> types
result := checker.module.exprs[probe].type if checker.module.exprs[probe].kind != .Invalid else types.INVALID
// Discard the throwaway leading build's scope (its hir stmts/locals are dead but stable).
for i := defer_start; i < len(ctx.defers^); i += 1 {
delete(ctx.defers^[i], checker.allocator)
delete(ctx.defers^[i].body, checker.allocator)
}
resize(ctx.defers, defer_start)
resize(ctx.locals, scope_start)
@@ -9310,7 +9410,7 @@ build_function :: proc(checker: ^Checker, id: Spec_Id) {
},
)
}
defers: [dynamic][]hir.Stmt_Id
defers: [dynamic]Defer_Entry
defers.allocator = checker.allocator
loop_defer_starts: [dynamic]int
loop_defer_starts.allocator = checker.allocator
@@ -9392,7 +9492,7 @@ build_function :: proc(checker: ^Checker, id: Spec_Id) {
},
)
for entry in defers {
delete(entry, checker.allocator)
delete(entry.body, checker.allocator)
}
delete(defers)
delete(loop_defer_starts)
+54 -6
View File
@@ -290,7 +290,8 @@ Ct_State :: struct {
places: [dynamic]Ct_Place,
paths: [dynamic]Ct_Path_Elem,
bindings: [dynamic]Ct_Binding,
defers: [dynamic]ast.Stmt_Id,
defers: [dynamic]Ct_Defer,
defer_depth: int,
steps: int,
error: Ct_Error_Kind,
diagnostic: source.Diagnostic_Id,
@@ -298,6 +299,12 @@ Ct_State :: struct {
demanded: ^[dynamic]Spec_Id,
}
Ct_Defer :: struct {
statement: ast.Stmt_Id,
error_only: bool,
capture: symbol.Id,
}
ct_state_make :: proc(
checker: ^Checker,
pkg: ast.Package_Id,
@@ -2217,6 +2224,9 @@ ct_clone_value :: proc(dst, src: ^Ct_State, id: Ct_Value_Id) -> Ct_Value_Id {
}
ct_eval_try_expr :: proc(state: ^Ct_State, expr: ast.Expr, depth: int) -> (Ct_Value_Id, Ct_Flow, bool) {
if state.defer_depth > 0 {
return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, expr.span, "cannot 'try' inside a 'defer'")
}
checker := state.checker
channel, flow, ok := ct_eval_expr(state, expr.left, types.INVALID, depth+1)
if !ok || flow.kind != .Normal {
@@ -2465,7 +2475,17 @@ ct_exec_statements :: proc(
case .Block:
flow, ok = ct_exec_statements(state, statement.body, yield_returns, depth+1)
case .Defer:
append(&state.defers, statement.update)
if statement.error_only && types.kind(state.result, &checker.module.types) != .Fallible {
ok = ct_fail(state, .Not_Comptime, statement.span, "'errdefer' requires an enclosing fallible function")
} else {
capture := symbol.INVALID
if len(statement.captures) > 0 {
capture = statement.captures[0]
}
append(&state.defers, Ct_Defer{
statement=statement.update, error_only=statement.error_only, capture=capture,
})
}
case .Match:
flow, ok = ct_exec_match(state, statement, yield_returns, depth+1)
case .Match_Arm:
@@ -2477,22 +2497,50 @@ ct_exec_statements :: proc(
return flow, false
}
if flow.kind != .Normal {
if !ct_flush_defers(state, defer_start, depth+1) {
if !ct_flush_defers(state, defer_start, flow, depth+1) {
return flow, false
}
return flow, true
}
}
if !ct_flush_defers(state, defer_start, depth+1) {
if !ct_flush_defers(state, defer_start, ct_flow(.Normal), depth+1) {
return ct_flow(.Normal), false
}
return ct_flow(.Normal), true
}
ct_flush_defers :: proc(state: ^Ct_State, start: int, depth: int) -> bool {
ct_flush_defers :: proc(state: ^Ct_State, start: int, exit: Ct_Flow, depth: int) -> bool {
error_exit := false
error_value := INVALID_CT_VALUE
if exit.kind == .Return && exit.value != INVALID_CT_VALUE && int(exit.value) < len(state.values) {
returned := state.values[exit.value]
if returned.kind == .Fallible && returned.active != 0 {
error_exit = true
children := ct_child_slice(state, returned)
if len(children) > 0 {
error_value = children[0]
}
}
}
for index := len(state.defers) - 1; index >= start; index -= 1 {
stmt := [1]ast.Stmt_Id{state.defers[index]}
entry := state.defers[index]
if entry.error_only && !error_exit {
continue
}
binding_start := len(state.bindings)
bound_capture := false
if entry.error_only && symbol.is_valid(entry.capture) &&
entry.capture != state.checker.sink_symbol && error_value != INVALID_CT_VALUE {
ct_bind_value(state, entry.capture, state.values[error_value].type, error_value, false)
bound_capture = true
}
stmt := [1]ast.Stmt_Id{entry.statement}
state.defer_depth += 1
flow, ok := ct_exec_statements(state, stmt[:], false, depth+1)
state.defer_depth -= 1
if bound_capture {
ct_pop_bindings(state, binding_start)
}
if !ok || flow.kind != .Normal {
return false
}
+4 -1
View File
@@ -136,10 +136,13 @@ Expr :: struct {
span: source.Span,
type: types.Type,
integer: i64,
// Aggregate/call children normally; Try stores errdefer capture local IDs
// encoded as Expr_Id because it otherwise has no args.
args: []Expr_Id,
// `Catch` block handlers use `body` for the handler statements and `target`
// for the optional captured error local. A missing `right` means the handler
// exits on every path and therefore has no fallback value.
// exits on every path and therefore has no fallback value. `Try` uses `body`
// for active error-exit cleanup.
body: []Stmt_Id,
target: Ref,
left: Expr_Id,
+1
View File
@@ -39,6 +39,7 @@ keyword_kind :: proc(text: string) -> token.Kind {
case "break": return .Keyword_Break
case "continue": return .Keyword_Continue
case "defer": return .Keyword_Defer
case "errdefer": return .Keyword_Errdefer
case "yield": return .Keyword_Yield
case "match": return .Keyword_Match
case "else": return .Keyword_Else
+25 -2
View File
@@ -416,10 +416,11 @@ lower_compound_expr :: proc(state: ^State, expr_id: hir.Expr_Id) -> ir.Instructi
})
if expr.kind == .Try {
result := channel
if !types.equal(channel_type, state.func_result) {
error_value := ir.INVALID_INSTRUCTION
if !types.equal(channel_type, state.func_result) || len(expr.args) > 0 {
error_type := types.fallible_error(channel_type, &state.hir_module.types)
enclosing_error := types.fallible_error(state.func_result, &state.hir_module.types)
error_value := append_instruction(state, ir.Instruction{
error_value = append_instruction(state, ir.Instruction{
op=.Fallible_Error, span=expr.span, type=error_type,
target=ir.INVALID_REF, a=channel_slot, b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
@@ -431,6 +432,7 @@ lower_compound_expr :: proc(state: ^State, expr_id: hir.Expr_Id) -> ir.Instructi
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
if !types.equal(channel_type, state.func_result) {
args := make([]ir.Instruction_Id, 1, state.allocator)
args[0] = error_value
result = append_instruction(state, ir.Instruction{
@@ -439,6 +441,27 @@ lower_compound_expr :: proc(state: ^State, expr_id: hir.Expr_Id) -> ir.Instructi
b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC,
})
}
}
for encoded_capture in expr.args {
capture := hir.Local_Id(encoded_capture)
if capture == hir.INVALID_LOCAL || int(capture) >= len(state.func_locals) {
continue
}
error_type := state.func_locals[capture].type
capture_slot := append_instruction(state, ir.Instruction{
op=.Alloca, span=expr.span, type=error_type,
target=ir.local_ref(ir.Local_Id(capture)),
a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
state.local_slots[capture] = capture_slot
append_instruction(state, ir.Instruction{
op=.Store, span=expr.span, type=error_type,
target=ir.INVALID_REF, a=capture_slot, b=error_value,
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
lower_statements(state, expr.body)
append_instruction(state, ir.Instruction{
op=.Return, span=expr.span, type=state.func_result,
target=ir.INVALID_REF, a=result, b=ir.INVALID_INSTRUCTION,
+31 -5
View File
@@ -1410,18 +1410,44 @@ parse_block_statement :: proc(parser: ^Parser, label := symbol.INVALID) -> ast.S
return id
}
// `defer <statement>` runs the statement when the enclosing scope exits. The
// statement may be a block (`defer { ... }`). The checker rejects deferring a
// `return`/`break`/`continue`/`defer`.
// `defer <statement>` runs whenever the enclosing scope exits; `errdefer`
// runs only when it exits through a function error and may capture that error.
parse_defer :: proc(parser: ^Parser) -> ast.Stmt_Id {
marker := advance(parser) // consume 'defer'
marker := advance(parser)
error_only := marker.kind == .Keyword_Errdefer
skip_newlines(parser)
captures: []symbol.Id
if error_only {
if _, ok := allow(parser, .Pipe); ok {
capture := current(parser)
if capture.kind != .Identifier && capture.kind != .Underscore {
source.add(parser.diagnostics, capture.span, "expected an errdefer capture name")
} else {
advance(parser)
captures = make([]symbol.Id, 1, parser.module.allocator)
captures[0] = capture.symbol
}
if current(parser).kind == .Comma {
source.add(parser.diagnostics, current(parser).span, "'errdefer' accepts exactly one capture")
for current(parser).kind != .Pipe && current(parser).kind != .Newline &&
current(parser).kind != .Eof {
advance(parser)
}
}
if _, close_ok := allow(parser, .Pipe); !close_ok {
source.add(parser.diagnostics, current(parser).span, "expected '|' after errdefer capture")
}
skip_newlines(parser)
}
}
inner := parse_statement(parser)
id := ast.stmt_id(len(parser.module.statements))
append(&parser.module.statements, ast.Stmt{
kind=.Defer,
span=marker.span,
update=inner,
error_only=error_only,
captures=captures,
expr=ast.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
@@ -1486,7 +1512,7 @@ parse_statement :: proc(parser: ^Parser) -> ast.Stmt_Id {
if current(parser).kind == .Keyword_Continue {
return parse_loop_control(parser, .Continue)
}
if current(parser).kind == .Keyword_Defer {
if current(parser).kind == .Keyword_Defer || current(parser).kind == .Keyword_Errdefer {
return parse_defer(parser)
}
if current(parser).kind == .Keyword_Yield {
+1
View File
@@ -75,6 +75,7 @@ Kind :: enum u8 {
Keyword_Break,
Keyword_Continue,
Keyword_Defer,
Keyword_Errdefer,
Keyword_Yield,
Keyword_Match,
Keyword_Else,
+52
View File
@@ -3747,8 +3747,13 @@ defer_misuse_is_diagnosed :: proc(t: ^testing.T) {
bad_defer_return()
bad_defer_break()
bad_return_in_defer()
bad_errdefer_nonfallible()
_ = bad_try_in_defer() catch 0
_ = bad_try_in_errdefer() catch 0
return 0
}
Failure :: enum { bad }
fail func() i32 ! Failure { return .bad }
bad_defer_return func() void {
defer return
}
@@ -3763,6 +3768,17 @@ bad_return_in_defer func() void {
return
}
}
bad_errdefer_nonfallible func() void {
errdefer {}
}
bad_try_in_defer func() i32 ! Failure {
defer _ = try fail()
return 1
}
bad_try_in_errdefer func() i32 ! Failure {
errdefer _ = try fail()
return 1
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
@@ -3779,14 +3795,50 @@ bad_return_in_defer func() void {
defer_return := false
defer_break := false
return_in_defer := false
errdefer_nonfallible := false
try_in_defer := 0
for diagnostic in diagnostics.items {
defer_return = defer_return || strings.contains(diagnostic.message, "cannot defer a 'return' statement")
defer_break = defer_break || strings.contains(diagnostic.message, "cannot defer a 'break' statement")
return_in_defer = return_in_defer || strings.contains(diagnostic.message, "cannot 'return' inside a 'defer'")
errdefer_nonfallible = errdefer_nonfallible || strings.contains(diagnostic.message, "'errdefer' requires an enclosing fallible function")
try_in_defer += 1 if strings.contains(diagnostic.message, "cannot 'try' inside a 'defer'") else 0
}
testing.expect(t, defer_return)
testing.expect(t, defer_break)
testing.expect(t, return_in_defer)
testing.expect(t, errdefer_nonfallible)
testing.expect_value(t, try_in_defer, 2)
}
@(test)
errdefer_capture_syntax_is_diagnosed :: proc(t: ^testing.T) {
text := `Failure :: enum { bad }
bad func() i32 ! Failure {
errdefer || {}
errdefer |first, second| {}
return .bad
}
main func() i32 { return bad() catch 0 }
`
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)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
missing := false
multiple := false
for diagnostic in diagnostics.items {
missing = missing || strings.contains(diagnostic.message, "expected an errdefer capture name")
multiple = multiple || strings.contains(diagnostic.message, "'errdefer' accepts exactly one capture")
}
testing.expect(t, missing)
testing.expect(t, multiple)
}
@(test)
+34
View File
@@ -91,6 +91,36 @@ use_try func() i32 ! Error {
return value + 1
}
ct_errdefer func(fail bool) i32 ! Error {
trace i32 = 0
defer trace = trace * 10 + 1
errdefer |err| {
if (err == .bad) trace = trace * 10 + 2
}
defer trace = trace * 10 + 3
if (fail) return .bad
return 7
}
ct_try_errdefer func() i32 ! Error {
trace i32 = 0
defer trace = trace * 10 + 4
errdefer |err| {
if (err == .bad) trace = trace * 10 + 5
}
return try may_fail(true)
}
ct_errdefer_check func() i32 {
ok i32 :: ct_errdefer(false) catch 0
if (ok != 7) return 1
explicit_error i32 :: ct_errdefer(true) catch 9
if (explicit_error != 9) return 2
try_error i32 :: ct_try_errdefer() catch 9
if (try_error != 9) return 3
return 42
}
recover func() i32 {
return may_fail(true) catch |e| {
match e {
@@ -135,6 +165,7 @@ storage_mutation func() i32 {
}
GLOBAL :: $sum_loop(4)
ERRDEFER :: $ct_errdefer_check()
main func() i32 {
point Point :: $make_point()
@@ -205,5 +236,8 @@ main func() i32 {
if fallible_err != 13 {
return 16
}
if ERRDEFER != 42 {
return 17
}
return 0
}
+87 -1
View File
@@ -1,4 +1,4 @@
# Milestone 19: `defer` and bare block statements.
# Milestone 19: `defer`, `errdefer`, and bare block statements.
#
# `defer <stmt>` runs the statement when the enclosing scope exits, in reverse
# (LIFO) order, on every exit path. A bare `{ ... }` introduces a scope. Each
@@ -24,6 +24,66 @@ enclosing_defer_check func() i32 {
return v # 0->1 (i=0 fall-through), ->2 (i=1 break); the +100 runs after capture
}
CleanupError :: enum {
bad
}
ExtraError :: enum {
extra
}
CleanupErrors :: alias CleanupError | ExtraError
explicit_cleanup func(fail bool, trace *mut i32) i32 ! CleanupError {
defer trace^ = trace^ * 10 + 1
errdefer |err| {
if (err == .bad) {
trace^ = trace^ * 10 + 2
} else {
trace^ = 99
}
}
defer trace^ = trace^ * 10 + 3
if (fail) return .bad
return 7
}
fail_cleanup func() i32 ! CleanupError {
return .bad
}
try_cleanup func(trace *mut i32) i32 ! CleanupError {
defer trace^ = trace^ * 10 + 4
errdefer trace^ = trace^ * 10 + 5
return try fail_cleanup()
}
widen_cleanup func(trace *mut i32) i32 ! CleanupErrors {
errdefer |err| {
match err {
.bad: trace^ = trace^ * 10 + 6
.extra: trace^ = 99
}
}
return try fail_cleanup()
}
scoped_cleanup func(trace *mut i32) i32 ! CleanupError {
errdefer trace^ = trace^ * 10 + 7
{
errdefer trace^ = 99
}
return .bad
}
multi_exit_cleanup func(direct bool, trace *mut i32) i32 ! CleanupError {
errdefer |err| {
if (err == .bad) trace^ = trace^ + 8
}
if (direct) return .bad
return try fail_cleanup()
}
main func() i32 {
# 1. return value captured before defers run.
if (spill_check() != 5) return 101
@@ -78,5 +138,31 @@ main func() i32 {
# 7. break does not run an enclosing function-scope defer.
if (enclosing_defer_check() != 2) return 107
# 8. errdefer is skipped on success; ordinary defers stay interleaved.
trace i32 = 0
if ((explicit_cleanup(false, &trace) catch 0) != 7 or trace != 31) return 108
# 9. Explicit errors run errdefer and expose the captured error.
trace = 0
if ((explicit_cleanup(true, &trace) catch 9) != 9 or trace != 321) return 109
# 10. Propagated errors run both errdefer and ordinary defer.
trace = 0
if ((try_cleanup(&trace) catch 9) != 9 or trace != 54) return 110
# 11. Captures observe the widened enclosing error type.
trace = 0
if ((widen_cleanup(&trace) catch 9) != 9 or trace != 6) return 111
# 12. An errdefer expires when its block exits normally.
trace = 0
if ((scoped_cleanup(&trace) catch 9) != 9 or trace != 7) return 112
# 13. One captured errdefer can be replayed at several distinct error exits.
trace = 0
_ = multi_exit_cleanup(true, &trace) catch 0
_ = multi_exit_cleanup(false, &trace) catch 0
if (trace != 16) return 113
return 42
}
+2 -2
View File
@@ -1,6 +1,6 @@
id = "brolang"
name = "Brolang"
version = "0.1.0"
version = "0.1.1"
schema_version = 1
authors = ["Brolang contributors"]
description = "Brolang language support"
@@ -9,5 +9,5 @@ languages = ["languages/brolang"]
[grammars.brolang]
repository = "file:///Users/valdemar/Developer/Personal/Languages/brolang"
rev = "zed-dev"
rev = "ba171a2d5e248fa24f5f4ee960e21056893f72e7"
path = "tree-sitter-brolang"
Binary file not shown.
+12
View File
@@ -23,6 +23,16 @@
(builtin_type) @type.builtin
(named_type) @type
(named_type
(qualified_identifier
(identifier) @function .)
(argument_list))
(struct_literal
type: (qualified_identifier
(identifier) @function .)
(argument_list))
(type_declaration name: (identifier) @type)
(function_declaration name: (identifier) @function)
(parameter name: (identifier) @variable.parameter)
@@ -62,6 +72,7 @@
"break"
"continue"
"defer"
"errdefer"
"yield"
"match"
"else"
@@ -86,6 +97,7 @@
"/"
"!"
"&"
"@"
"?"
"^"
".."
+11 -1
View File
@@ -278,7 +278,17 @@ module.exports = grammar({
break_statement: $ => seq('break', optional(seq(':', field('label', $.identifier)))),
continue_statement: $ => seq('continue', optional(seq(':', field('label', $.identifier)))),
defer_statement: $ => seq('defer', repeat($._newline), field('body', $.statement)),
defer_statement: $ => choice(
seq('defer', repeat($._newline), field('body', $.statement)),
seq(
'errdefer',
repeat($._newline),
optional(seq(field('capture', $.error_capture), repeat($._newline))),
field('body', $.statement),
),
),
error_capture: $ => seq('|', choice($.identifier, $.sink), '|'),
labeled_block: $ => seq(
field('label', $.identifier),
@@ -23,6 +23,16 @@
(builtin_type) @type.builtin
(named_type) @type
(named_type
(qualified_identifier
(identifier) @function .)
(argument_list))
(struct_literal
type: (qualified_identifier
(identifier) @function .)
(argument_list))
(type_declaration name: (identifier) @type)
(function_declaration name: (identifier) @function)
(parameter name: (identifier) @variable.parameter)
@@ -62,6 +72,7 @@
"break"
"continue"
"defer"
"errdefer"
"yield"
"match"
"else"
@@ -86,6 +97,7 @@
"/"
"!"
"&"
"@"
"?"
"^"
".."
+83
View File
@@ -1855,6 +1855,9 @@
]
},
"defer_statement": {
"type": "CHOICE",
"members": [
{
"type": "SEQ",
"members": [
{
@@ -1878,6 +1881,86 @@
}
]
},
{
"type": "SEQ",
"members": [
{
"type": "STRING",
"value": "errdefer"
},
{
"type": "REPEAT",
"content": {
"type": "SYMBOL",
"name": "_newline"
}
},
{
"type": "CHOICE",
"members": [
{
"type": "SEQ",
"members": [
{
"type": "FIELD",
"name": "capture",
"content": {
"type": "SYMBOL",
"name": "error_capture"
}
},
{
"type": "REPEAT",
"content": {
"type": "SYMBOL",
"name": "_newline"
}
}
]
},
{
"type": "BLANK"
}
]
},
{
"type": "FIELD",
"name": "body",
"content": {
"type": "SYMBOL",
"name": "statement"
}
}
]
}
]
},
"error_capture": {
"type": "SEQ",
"members": [
{
"type": "STRING",
"value": "|"
},
{
"type": "CHOICE",
"members": [
{
"type": "SYMBOL",
"name": "identifier"
},
{
"type": "SYMBOL",
"name": "sink"
}
]
},
{
"type": "STRING",
"value": "|"
}
]
},
"labeled_block": {
"type": "SEQ",
"members": [
+33
View File
@@ -517,6 +517,16 @@
"named": true
}
]
},
"capture": {
"multiple": false,
"required": false,
"types": [
{
"type": "error_capture",
"named": true
}
]
}
}
},
@@ -629,6 +639,25 @@
]
}
},
{
"type": "error_capture",
"named": true,
"fields": {},
"children": {
"multiple": false,
"required": true,
"types": [
{
"type": "identifier",
"named": true
},
{
"type": "sink",
"named": true
}
]
}
},
{
"type": "expression",
"named": true,
@@ -2471,6 +2500,10 @@
"type": "enum",
"named": false
},
{
"type": "errdefer",
"named": false
},
{
"type": "escape_sequence",
"named": true
+112536 -105418
View File
File diff suppressed because it is too large Load Diff
@@ -95,3 +95,62 @@ sum func(a, b i32) i32 ! Status {
(expression
(enum_literal
(identifier))))))))))))
==================
Errdefer
==================
Failure :: enum { bad }
work func() i32 ! Failure {
errdefer cleanup()
errdefer |err| {
_ = err
}
return 1
}
---
(source_file
(type_declaration
(identifier)
(enum_type
(enum_body
(enum_member
(identifier)))))
(function_declaration
(identifier)
(parameter_list)
(type
(builtin_type))
(type
(named_type
(qualified_identifier
(identifier))))
(block
(statement
(defer_statement
(statement
(expression_statement
(expression
(call_expression
(expression
(identifier))
(argument_list)))))))
(statement
(defer_statement
(error_capture
(identifier))
(statement
(block
(statement
(assignment_statement
(expression
(sink))
(expression
(identifier))))))))
(statement
(return_statement
(expression
(integer)))))))