add defer
This commit is contained in:
@@ -286,9 +286,24 @@
|
|||||||
- the LLVM emitter opens a fresh recovery block after any terminator (not just `ret`),
|
- the LLVM emitter opens a fresh recovery block after any terminator (not just `ret`),
|
||||||
so dead code following a `break`/`continue` branch stays well-formed
|
so dead code following a `break`/`continue` branch stays well-formed
|
||||||
|
|
||||||
19. add `defer` statement (inspired by zig)
|
19. add `defer` statement (inspired by zig) (implemented; also adds bare block statements)
|
||||||
- now unblocked: `defer` reuses the loop-target stack and loop tracking added in
|
- `defer <stmt>` runs the statement when the enclosing block scope exits, in reverse
|
||||||
milestone 18 to flush deferred statements on `break`/`continue` exits too
|
(LIFO) order, on every exit path: fall-through, `return`, `break`, `continue`. The
|
||||||
|
deferred statement may be a block (`defer { ... }`)
|
||||||
|
- 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
|
||||||
|
- the return value is captured *before* defers run (a `defer` that mutates the returned
|
||||||
|
local can't change what is returned) — the checker spills the return value into a temp
|
||||||
|
local, then flushes, matching Zig
|
||||||
|
- `return` flushes all active defers; `break`/`continue` flush only down to and including
|
||||||
|
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
|
||||||
|
|
||||||
20. add `yield` statement (see below)
|
20. add `yield` statement (see below)
|
||||||
|
|
||||||
|
|||||||
@@ -131,6 +131,8 @@ Stmt_Kind :: enum u8 {
|
|||||||
For,
|
For,
|
||||||
Break,
|
Break,
|
||||||
Continue,
|
Continue,
|
||||||
|
Block,
|
||||||
|
Defer,
|
||||||
}
|
}
|
||||||
|
|
||||||
Assignment_Op :: enum u8 {
|
Assignment_Op :: enum u8 {
|
||||||
@@ -165,6 +167,9 @@ Stmt :: struct {
|
|||||||
// `For` statements use `expr` as the iterable, `name` as the item capture,
|
// `For` statements use `expr` as the iterable, `name` as the item capture,
|
||||||
// `index_name` as the optional index capture, and `pointer_capture` to
|
// `index_name` as the optional index capture, and `pointer_capture` to
|
||||||
// distinguish `|@item|` from copy capture.
|
// 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`).
|
||||||
captures: []symbol.Id,
|
captures: []symbol.Id,
|
||||||
guard: Expr_Id,
|
guard: Expr_Id,
|
||||||
body: []Stmt_Id,
|
body: []Stmt_Id,
|
||||||
|
|||||||
@@ -68,9 +68,18 @@ Build_Ctx :: struct {
|
|||||||
global_reads: ^[dynamic]hir.Global_Id,
|
global_reads: ^[dynamic]hir.Global_Id,
|
||||||
calls: ^[dynamic]hir.Function_Id,
|
calls: ^[dynamic]hir.Function_Id,
|
||||||
problematic: ^bool,
|
problematic: ^bool,
|
||||||
// Number of enclosing loops being built. `break`/`continue` are only valid
|
// `defer` lowering. Deferred statements are built once at the `defer` site and
|
||||||
// when this is > 0; bumped around loop-body builds in `build_block`.
|
// their hir stmt ids stored here as a flat stack across scopes (one entry per
|
||||||
loop_depth: int,
|
// deferred statement); they are replayed (appended) at each scope exit in LIFO
|
||||||
|
// order. `loop_defer_starts` records `len(defers)` at each enclosing loop body
|
||||||
|
// entry: `break`/`continue` flush down to that mark (and need `len > loop_floor`
|
||||||
|
// 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,
|
||||||
|
loop_defer_starts: ^[dynamic]int,
|
||||||
|
defer_depth: int,
|
||||||
|
loop_floor: int,
|
||||||
}
|
}
|
||||||
|
|
||||||
Constant_Kind :: enum {
|
Constant_Kind :: enum {
|
||||||
@@ -737,6 +746,11 @@ mark_block_imports_used :: proc(checker: ^Checker, statements: []ast.Stmt_Id, fi
|
|||||||
case .For:
|
case .For:
|
||||||
mark_expr_imports_used(checker, statement.expr, file)
|
mark_expr_imports_used(checker, statement.expr, file)
|
||||||
mark_block_imports_used(checker, statement.body, file)
|
mark_block_imports_used(checker, statement.body, file)
|
||||||
|
case .Block:
|
||||||
|
mark_block_imports_used(checker, statement.body, file)
|
||||||
|
case .Defer:
|
||||||
|
deferred := [1]ast.Stmt_Id{statement.update}
|
||||||
|
mark_block_imports_used(checker, deferred[:], file)
|
||||||
case .Break, .Continue:
|
case .Break, .Continue:
|
||||||
case .Invalid:
|
case .Invalid:
|
||||||
}
|
}
|
||||||
@@ -1875,6 +1889,11 @@ infer_statements :: proc(
|
|||||||
}
|
}
|
||||||
infer_statements(checker, statement.body, locals, local_types, pkg, file, demanded, result, result_hint)
|
infer_statements(checker, statement.body, locals, local_types, pkg, file, demanded, result, result_hint)
|
||||||
resize(locals, capture_start)
|
resize(locals, capture_start)
|
||||||
|
case .Block:
|
||||||
|
infer_statements(checker, statement.body, locals, local_types, pkg, file, demanded, result, result_hint)
|
||||||
|
case .Defer:
|
||||||
|
deferred := [1]ast.Stmt_Id{statement.update}
|
||||||
|
infer_statements(checker, deferred[:], locals, local_types, pkg, file, demanded, result, result_hint)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
resize(locals, scope_start)
|
resize(locals, scope_start)
|
||||||
@@ -3962,6 +3981,17 @@ make_link_name :: proc(checker: ^Checker, id: Spec_Id) -> string {
|
|||||||
return fmt.aprintf("%s", strings.to_string(builder), allocator = checker.allocator)
|
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) {
|
||||||
|
for i := len(ctx.defers^) - 1; i >= lo; i -= 1 {
|
||||||
|
for stmt_id in ctx.defers^[i] {
|
||||||
|
append(body, stmt_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
build_block :: proc(
|
build_block :: proc(
|
||||||
ctx: ^Build_Ctx,
|
ctx: ^Build_Ctx,
|
||||||
statements: []ast.Stmt_Id,
|
statements: []ast.Stmt_Id,
|
||||||
@@ -3971,6 +4001,7 @@ build_block :: proc(
|
|||||||
body: [dynamic]hir.Stmt_Id
|
body: [dynamic]hir.Stmt_Id
|
||||||
body.allocator = checker.allocator
|
body.allocator = checker.allocator
|
||||||
scope_start := len(ctx.locals^)
|
scope_start := len(ctx.locals^)
|
||||||
|
defer_start := len(ctx.defers^)
|
||||||
duplicate_start := scope_start if duplicate_scope_start < 0 else duplicate_scope_start
|
duplicate_start := scope_start if duplicate_scope_start < 0 else duplicate_scope_start
|
||||||
for statement_id in statements {
|
for statement_id in statements {
|
||||||
statement := checker.ast_module.statements[statement_id]
|
statement := checker.ast_module.statements[statement_id]
|
||||||
@@ -4224,6 +4255,16 @@ build_block :: proc(
|
|||||||
})
|
})
|
||||||
ctx.problematic^ = ctx.problematic^ || checker.module.exprs[value].kind == .Invalid
|
ctx.problematic^ = ctx.problematic^ || checker.module.exprs[value].kind == .Invalid
|
||||||
case .Return:
|
case .Return:
|
||||||
|
if ctx.defer_depth > 0 {
|
||||||
|
id := source.add(checker.diagnostics, statement.span, "cannot 'return' inside a 'defer'")
|
||||||
|
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
|
||||||
|
}
|
||||||
if statement.expr == ast.INVALID_EXPR {
|
if statement.expr == ast.INVALID_EXPR {
|
||||||
if !types.is_void(ctx.result) {
|
if !types.is_void(ctx.result) {
|
||||||
id := source.add(checker.diagnostics, statement.span, "'return _' is only valid in a void function")
|
id := source.add(checker.diagnostics, statement.span, "'return _' is only valid in a void function")
|
||||||
@@ -4234,6 +4275,7 @@ build_block :: proc(
|
|||||||
})
|
})
|
||||||
ctx.problematic^ = true
|
ctx.problematic^ = true
|
||||||
} else {
|
} else {
|
||||||
|
flush_defers(ctx, &body, 0)
|
||||||
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{
|
||||||
kind = .Return, span = statement.span, expr = hir.INVALID_EXPR,
|
kind = .Return, span = statement.span, expr = hir.INVALID_EXPR,
|
||||||
@@ -4257,12 +4299,32 @@ build_block :: proc(
|
|||||||
ctx.result, ctx.pkg, ctx.file,
|
ctx.result, ctx.pkg, ctx.file,
|
||||||
)
|
)
|
||||||
value = coerce_expr(checker, value, ctx.result, statement.span)
|
value = coerce_expr(checker, value, ctx.result, statement.span)
|
||||||
|
ctx.problematic^ = ctx.problematic^ || checker.module.exprs[value].kind == .Invalid
|
||||||
|
// Run deferred statements before returning, but capture the return value
|
||||||
|
// first (spill it to a temp) so a defer that mutates the returned local
|
||||||
|
// 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 {
|
||||||
|
tmp := hir.local_id(len(ctx.hir_locals^))
|
||||||
|
append(ctx.hir_locals, hir.Local{name = checker.sink_symbol, type = ctx.result, mutable = false})
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
value = hir.expr_id(len(checker.module.exprs))
|
||||||
|
append(&checker.module.exprs, hir.Expr{
|
||||||
|
kind = .Local, span = statement.span, type = ctx.result, target = hir.local_ref(tmp),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
flush_defers(ctx, &body, 0)
|
||||||
|
}
|
||||||
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{
|
||||||
kind = .Return, span = statement.span, expr = value,
|
kind = .Return, span = statement.span, expr = value,
|
||||||
local = hir.INVALID_LOCAL, diagnostic = source.INVALID_DIAGNOSTIC,
|
local = hir.INVALID_LOCAL, diagnostic = source.INVALID_DIAGNOSTIC,
|
||||||
})
|
})
|
||||||
ctx.problematic^ = ctx.problematic^ || checker.module.exprs[value].kind == .Invalid
|
|
||||||
case .Expression:
|
case .Expression:
|
||||||
value := build_expr(checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, types.INVALID, ctx.pkg, ctx.file)
|
value := build_expr(checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, types.INVALID, ctx.pkg, ctx.file)
|
||||||
if !types.is_void(checker.module.exprs[value].type) {
|
if !types.is_void(checker.module.exprs[value].type) {
|
||||||
@@ -4443,9 +4505,9 @@ build_block :: proc(
|
|||||||
condition = invalid_hir_expr(checker, statement.span, id, types.BOOL)
|
condition = invalid_hir_expr(checker, statement.span, id, types.BOOL)
|
||||||
ctx.problematic^ = true
|
ctx.problematic^ = true
|
||||||
}
|
}
|
||||||
ctx.loop_depth += 1
|
append(ctx.loop_defer_starts, len(ctx.defers^))
|
||||||
loop_body := build_block(ctx, statement.body)
|
loop_body := build_block(ctx, statement.body)
|
||||||
ctx.loop_depth -= 1
|
pop(ctx.loop_defer_starts)
|
||||||
update := hir.INVALID_STMT
|
update := hir.INVALID_STMT
|
||||||
if statement.update != ast.INVALID_STMT {
|
if statement.update != ast.INVALID_STMT {
|
||||||
update_ast := [1]ast.Stmt_Id{statement.update}
|
update_ast := [1]ast.Stmt_Id{statement.update}
|
||||||
@@ -4537,9 +4599,9 @@ build_block :: proc(
|
|||||||
append(ctx.locals, Build_Local{name=statement.index_name, type=types.USIZE, mutable=false, id=index_local})
|
append(ctx.locals, Build_Local{name=statement.index_name, type=types.USIZE, mutable=false, id=index_local})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ctx.loop_depth += 1
|
append(ctx.loop_defer_starts, len(ctx.defers^))
|
||||||
loop_body := build_block(ctx, statement.body, capture_start)
|
loop_body := build_block(ctx, statement.body, capture_start)
|
||||||
ctx.loop_depth -= 1
|
pop(ctx.loop_defer_starts)
|
||||||
resize(ctx.locals, capture_start)
|
resize(ctx.locals, capture_start)
|
||||||
|
|
||||||
append(&body, hir.stmt_id(len(checker.module.statements)))
|
append(&body, hir.stmt_id(len(checker.module.statements)))
|
||||||
@@ -4571,7 +4633,9 @@ build_block :: proc(
|
|||||||
ctx.problematic^ = true
|
ctx.problematic^ = true
|
||||||
}
|
}
|
||||||
case .Break, .Continue:
|
case .Break, .Continue:
|
||||||
if ctx.loop_depth == 0 {
|
// Inside a `defer`, `loop_floor` hides the enclosing loops so only loops
|
||||||
|
// opened within the defer count.
|
||||||
|
if len(ctx.loop_defer_starts^) <= ctx.loop_floor {
|
||||||
keyword := "break" if statement.kind == .Break else "continue"
|
keyword := "break" if statement.kind == .Break else "continue"
|
||||||
id := source.addf(checker.diagnostics, statement.span, "'%s' outside of a loop", keyword)
|
id := source.addf(checker.diagnostics, statement.span, "'%s' outside of a loop", keyword)
|
||||||
append(&body, hir.stmt_id(len(checker.module.statements)))
|
append(&body, hir.stmt_id(len(checker.module.statements)))
|
||||||
@@ -4582,12 +4646,50 @@ build_block :: proc(
|
|||||||
ctx.problematic^ = true
|
ctx.problematic^ = true
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
// Exit the loop body and any blocks between here and it: run their
|
||||||
|
// deferred statements down to and including the innermost loop body.
|
||||||
|
flush_defers(ctx, &body, ctx.loop_defer_starts^[len(ctx.loop_defer_starts^) - 1])
|
||||||
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{
|
||||||
kind = .Break if statement.kind == .Break else .Continue,
|
kind = .Break if statement.kind == .Break else .Continue,
|
||||||
span = statement.span, expr = hir.INVALID_EXPR,
|
span = statement.span, expr = hir.INVALID_EXPR,
|
||||||
local = hir.INVALID_LOCAL, diagnostic = source.INVALID_DIAGNOSTIC,
|
local = hir.INVALID_LOCAL, diagnostic = source.INVALID_DIAGNOSTIC,
|
||||||
})
|
})
|
||||||
|
case .Block:
|
||||||
|
// A bare `{ ... }` scope: build it (its own locals/defers are scoped by
|
||||||
|
// the recursive call) and splice its statements in.
|
||||||
|
block := build_block(ctx, statement.body)
|
||||||
|
for stmt in block {
|
||||||
|
append(&body, stmt)
|
||||||
|
}
|
||||||
|
delete(block, checker.allocator)
|
||||||
|
case .Defer:
|
||||||
|
deferred := checker.ast_module.statements[statement.update]
|
||||||
|
if deferred.kind == .Return || deferred.kind == .Break ||
|
||||||
|
deferred.kind == .Continue || deferred.kind == .Defer {
|
||||||
|
keyword := "return"
|
||||||
|
if deferred.kind == .Break { keyword = "break" }
|
||||||
|
if deferred.kind == .Continue { keyword = "continue" }
|
||||||
|
if deferred.kind == .Defer { keyword = "defer" }
|
||||||
|
id := source.addf(checker.diagnostics, statement.span, "cannot defer a '%s' statement", keyword)
|
||||||
|
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.
|
||||||
|
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)
|
||||||
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{
|
||||||
@@ -4597,6 +4699,18 @@ build_block :: proc(
|
|||||||
ctx.problematic^ = true
|
ctx.problematic^ = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Normal fall-through exit: run this block's own deferred statements, unless
|
||||||
|
// every path already exited early (return/break/continue) — that would only
|
||||||
|
// emit unreachable duplicates.
|
||||||
|
if !all_paths_exit(&checker.module, body[:]) {
|
||||||
|
flush_defers(ctx, &body, defer_start)
|
||||||
|
}
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
resize(ctx.defers, defer_start)
|
||||||
resize(ctx.locals, scope_start)
|
resize(ctx.locals, scope_start)
|
||||||
return body[:]
|
return body[:]
|
||||||
}
|
}
|
||||||
@@ -4652,6 +4766,34 @@ loop_body_breaks :: proc(module: ^hir.Module, stmts: []hir.Stmt_Id) -> bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Like `all_paths_return`, but also treats `break`/`continue` as terminating the
|
||||||
|
// block. Used only to decide whether `build_block` may skip the fall-through defer
|
||||||
|
// flush (a block that always exits early would otherwise emit unreachable copies).
|
||||||
|
all_paths_exit :: proc(module: ^hir.Module, stmts: []hir.Stmt_Id) -> bool {
|
||||||
|
for id in stmts {
|
||||||
|
statement := module.statements[id]
|
||||||
|
#partial switch statement.kind {
|
||||||
|
case .Return, .Trap, .Break, .Continue:
|
||||||
|
return true
|
||||||
|
case .If:
|
||||||
|
if statement.else_body != nil &&
|
||||||
|
all_paths_exit(module, statement.then_body) &&
|
||||||
|
all_paths_exit(module, statement.else_body) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
case .While:
|
||||||
|
if statement.expr != hir.INVALID_EXPR && int(statement.expr) < len(module.exprs) {
|
||||||
|
condition := module.exprs[statement.expr]
|
||||||
|
if condition.kind == .Bool && condition.integer != 0 &&
|
||||||
|
!loop_body_breaks(module, statement.then_body) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
build_function :: proc(checker: ^Checker, id: Spec_Id) {
|
build_function :: proc(checker: ^Checker, id: Spec_Id) {
|
||||||
spec := checker.specs[id]
|
spec := checker.specs[id]
|
||||||
function := checker.ast_module.functions[spec.template]
|
function := checker.ast_module.functions[spec.template]
|
||||||
@@ -4751,6 +4893,10 @@ build_function :: proc(checker: ^Checker, id: Spec_Id) {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
defers: [dynamic][]hir.Stmt_Id
|
||||||
|
defers.allocator = checker.allocator
|
||||||
|
loop_defer_starts: [dynamic]int
|
||||||
|
loop_defer_starts.allocator = checker.allocator
|
||||||
ctx := Build_Ctx{
|
ctx := Build_Ctx{
|
||||||
checker = checker,
|
checker = checker,
|
||||||
pkg = function.pkg,
|
pkg = function.pkg,
|
||||||
@@ -4762,6 +4908,8 @@ build_function :: proc(checker: ^Checker, id: Spec_Id) {
|
|||||||
global_reads = &global_reads,
|
global_reads = &global_reads,
|
||||||
calls = &calls,
|
calls = &calls,
|
||||||
problematic = &problematic,
|
problematic = &problematic,
|
||||||
|
defers = &defers,
|
||||||
|
loop_defer_starts = &loop_defer_starts,
|
||||||
}
|
}
|
||||||
block := build_block(&ctx, function.body)
|
block := build_block(&ctx, function.body)
|
||||||
returns := all_paths_return(&checker.module, block)
|
returns := all_paths_return(&checker.module, block)
|
||||||
@@ -4806,6 +4954,11 @@ build_function :: proc(checker: ^Checker, id: Spec_Id) {
|
|||||||
diagnostic = source.INVALID_DIAGNOSTIC,
|
diagnostic = source.INVALID_DIAGNOSTIC,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
for entry in defers {
|
||||||
|
delete(entry, checker.allocator)
|
||||||
|
}
|
||||||
|
delete(defers)
|
||||||
|
delete(loop_defer_starts)
|
||||||
delete(locals)
|
delete(locals)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ keyword_kind :: proc(text: string) -> token.Kind {
|
|||||||
case "for": return .Keyword_For
|
case "for": return .Keyword_For
|
||||||
case "break": return .Keyword_Break
|
case "break": return .Keyword_Break
|
||||||
case "continue": return .Keyword_Continue
|
case "continue": return .Keyword_Continue
|
||||||
|
case "defer": return .Keyword_Defer
|
||||||
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
|
||||||
|
|||||||
@@ -1001,6 +1001,41 @@ parse_loop_control :: proc(parser: ^Parser, kind: ast.Stmt_Kind) -> ast.Stmt_Id
|
|||||||
return id
|
return id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A bare `{ ... }` introduces a nested scope. Locals declared inside are not
|
||||||
|
// visible after it, and any `defer`s inside it run at the closing brace.
|
||||||
|
parse_block_statement :: proc(parser: ^Parser) -> ast.Stmt_Id {
|
||||||
|
start := current(parser).span // the '{'
|
||||||
|
body := parse_block(parser)
|
||||||
|
id := ast.stmt_id(len(parser.module.statements))
|
||||||
|
append(&parser.module.statements, ast.Stmt{
|
||||||
|
kind=.Block,
|
||||||
|
span=start,
|
||||||
|
body=body,
|
||||||
|
expr=ast.INVALID_EXPR,
|
||||||
|
update=ast.INVALID_STMT,
|
||||||
|
diagnostic=source.INVALID_DIAGNOSTIC,
|
||||||
|
})
|
||||||
|
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`.
|
||||||
|
parse_defer :: proc(parser: ^Parser) -> ast.Stmt_Id {
|
||||||
|
marker := advance(parser) // consume 'defer'
|
||||||
|
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,
|
||||||
|
expr=ast.INVALID_EXPR,
|
||||||
|
diagnostic=source.INVALID_DIAGNOSTIC,
|
||||||
|
})
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
starts_declared_type :: proc(parser: ^Parser) -> bool {
|
starts_declared_type :: proc(parser: ^Parser) -> bool {
|
||||||
if current(parser).kind != .Left_Bracket {
|
if current(parser).kind != .Left_Bracket {
|
||||||
return is_type_token(current(parser).kind)
|
return is_type_token(current(parser).kind)
|
||||||
@@ -1045,6 +1080,13 @@ parse_statement :: proc(parser: ^Parser) -> ast.Stmt_Id {
|
|||||||
if current(parser).kind == .Keyword_Continue {
|
if current(parser).kind == .Keyword_Continue {
|
||||||
return parse_loop_control(parser, .Continue)
|
return parse_loop_control(parser, .Continue)
|
||||||
}
|
}
|
||||||
|
if current(parser).kind == .Keyword_Defer {
|
||||||
|
return parse_defer(parser)
|
||||||
|
}
|
||||||
|
// A leading `{` opens a bare block scope (struct literals are postfix only).
|
||||||
|
if current(parser).kind == .Left_Brace {
|
||||||
|
return parse_block_statement(parser)
|
||||||
|
}
|
||||||
|
|
||||||
if current(parser).kind == .Identifier || current(parser).kind == .Underscore {
|
if current(parser).kind == .Identifier || current(parser).kind == .Underscore {
|
||||||
start_cursor := parser.cursor
|
start_cursor := parser.cursor
|
||||||
@@ -1333,7 +1375,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:
|
case .Invalid, .Declaration, .Return, .If, .While, .For, .Break, .Continue, .Block, .Defer:
|
||||||
diagnostic := source.add(
|
diagnostic := source.add(
|
||||||
parser.diagnostics,
|
parser.diagnostics,
|
||||||
statement.span,
|
statement.span,
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ Kind :: enum u8 {
|
|||||||
Keyword_For,
|
Keyword_For,
|
||||||
Keyword_Break,
|
Keyword_Break,
|
||||||
Keyword_Continue,
|
Keyword_Continue,
|
||||||
|
Keyword_Defer,
|
||||||
Keyword_Else,
|
Keyword_Else,
|
||||||
Keyword_True,
|
Keyword_True,
|
||||||
Keyword_False,
|
Keyword_False,
|
||||||
|
|||||||
@@ -2061,6 +2061,69 @@ missing_return :: func() i32 {
|
|||||||
testing.expect(t, missing)
|
testing.expect(t, missing)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
defer_compiles_and_runs :: proc(t: ^testing.T) {
|
||||||
|
output := "/tmp/brolang-test-defer"
|
||||||
|
defer _ = os.remove(output)
|
||||||
|
status := compiler_core.compile_package("examples/programs/defer", output)
|
||||||
|
testing.expect_value(t, status, 0)
|
||||||
|
state := run_executable(output)
|
||||||
|
// LIFO, runs on fall-through / break / continue / return (with the return value
|
||||||
|
// captured before defers run), scoped bare blocks, and `defer { ... }` blocks
|
||||||
|
// together produce 42.
|
||||||
|
testing.expect_value(t, state.exit_code, 42)
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
defer_misuse_is_diagnosed :: proc(t: ^testing.T) {
|
||||||
|
// Deferring control flow that would escape the defer is rejected: `defer return`,
|
||||||
|
// `defer break`, and a `return` inside a `defer { ... }` block.
|
||||||
|
text := `main :: func() i32 {
|
||||||
|
bad_defer_return()
|
||||||
|
bad_defer_break()
|
||||||
|
bad_return_in_defer()
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
bad_defer_return :: func() void {
|
||||||
|
defer return
|
||||||
|
}
|
||||||
|
bad_defer_break :: func() void {
|
||||||
|
for 0..3 |i| {
|
||||||
|
defer break
|
||||||
|
_ = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bad_return_in_defer :: func() void {
|
||||||
|
defer {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
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)
|
||||||
|
|
||||||
|
defer_return := false
|
||||||
|
defer_break := false
|
||||||
|
return_in_defer := false
|
||||||
|
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'")
|
||||||
|
}
|
||||||
|
testing.expect(t, defer_return)
|
||||||
|
testing.expect(t, defer_break)
|
||||||
|
testing.expect(t, return_in_defer)
|
||||||
|
}
|
||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
foreign_function_links_from_c_source :: proc(t: ^testing.T) {
|
foreign_function_links_from_c_source :: proc(t: ^testing.T) {
|
||||||
output := "/tmp/brolang-test-foreign-source"
|
output := "/tmp/brolang-test-foreign-source"
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
# Milestone 19: `defer` 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
|
||||||
|
# section returns a distinct code on failure; success falls through to 42.
|
||||||
|
|
||||||
|
# The return value is captured before defers run, so the mutation here does not
|
||||||
|
# change what is returned (Zig semantics).
|
||||||
|
spill_check :: func() i32 {
|
||||||
|
x i32 = 5
|
||||||
|
defer x = 999
|
||||||
|
return x
|
||||||
|
}
|
||||||
|
|
||||||
|
# A function-scope defer runs only at function exit; a `break` runs the loop-body
|
||||||
|
# defer but NOT the enclosing function-scope defer.
|
||||||
|
enclosing_defer_check :: func() i32 {
|
||||||
|
v i32 = 0
|
||||||
|
defer v = v + 100
|
||||||
|
for 0..3 |i| {
|
||||||
|
defer v = v + 1
|
||||||
|
if (i == 1) break
|
||||||
|
}
|
||||||
|
return v # 0->1 (i=0 fall-through), ->2 (i=1 break); the +100 runs after capture
|
||||||
|
}
|
||||||
|
|
||||||
|
main :: func() i32 {
|
||||||
|
# 1. return value captured before defers run.
|
||||||
|
if (spill_check() != 5) return 101
|
||||||
|
|
||||||
|
# 2. LIFO ordering, run at end of each loop iteration.
|
||||||
|
r i32 = 0
|
||||||
|
for 0..1 |i| {
|
||||||
|
defer r = r * 2 + 1 # registered first -> runs last
|
||||||
|
defer r = r * 2 # registered second -> runs first
|
||||||
|
_ = i
|
||||||
|
}
|
||||||
|
if (r != 1) return 102 # 0 -> (r*2)=0 -> (r*2+1)=1 ; FIFO would give 2
|
||||||
|
|
||||||
|
# 3. scoped bare block + scoped defer (defer fires at the closing brace, and
|
||||||
|
# the block-local is not visible afterwards).
|
||||||
|
a i32 = 1
|
||||||
|
{
|
||||||
|
defer a = 4
|
||||||
|
c i32 = 3
|
||||||
|
_ = c
|
||||||
|
}
|
||||||
|
if (a != 4) return 103
|
||||||
|
|
||||||
|
# 4. `defer { ... }` block: all its statements run (in order) at scope close.
|
||||||
|
s i32 = 0
|
||||||
|
{
|
||||||
|
defer {
|
||||||
|
s = s + 1
|
||||||
|
s = s * 10
|
||||||
|
}
|
||||||
|
s = 5
|
||||||
|
}
|
||||||
|
if (s != 60) return 104 # 5 -> 6 -> 60
|
||||||
|
|
||||||
|
# 5. `break` flushes the loop-body defer.
|
||||||
|
bc i32 = 0
|
||||||
|
for 0..5 |i| {
|
||||||
|
defer bc = bc + 1
|
||||||
|
if (i == 2) break
|
||||||
|
}
|
||||||
|
if (bc != 3) return 105 # i=0,1 fall-through + i=2 break
|
||||||
|
|
||||||
|
# 6. `continue` flushes the loop-body defer.
|
||||||
|
cc i32 = 0
|
||||||
|
for 0..3 |i| {
|
||||||
|
defer cc = cc + 1
|
||||||
|
if (i == 1) continue
|
||||||
|
cc = cc + 10
|
||||||
|
}
|
||||||
|
if (cc != 23) return 106 # 10,+1 ; +1 (continue) ; +10,+1
|
||||||
|
|
||||||
|
# 7. break does not run an enclosing function-scope defer.
|
||||||
|
if (enclosing_defer_check() != 2) return 107
|
||||||
|
|
||||||
|
return 42
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user