diff --git a/TODO.md b/TODO.md index 6662207..35a3852 100644 --- a/TODO.md +++ b/TODO.md @@ -286,9 +286,24 @@ - 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 -19. add `defer` statement (inspired by zig) - - now unblocked: `defer` reuses the loop-target stack and loop tracking added in - milestone 18 to flush deferred statements on `break`/`continue` exits too +19. add `defer` statement (inspired by zig) (implemented; also adds bare block statements) + - `defer ` 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 { ... }`) + - 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) diff --git a/compiler/ast/ast.odin b/compiler/ast/ast.odin index e3194f3..f0c0423 100644 --- a/compiler/ast/ast.odin +++ b/compiler/ast/ast.odin @@ -131,6 +131,8 @@ Stmt_Kind :: enum u8 { For, Break, Continue, + Block, + Defer, } Assignment_Op :: enum u8 { @@ -165,6 +167,9 @@ Stmt :: struct { // `For` statements use `expr` as the iterable, `name` as the item capture, // `index_name` as the optional index capture, and `pointer_capture` to // 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, guard: Expr_Id, body: []Stmt_Id, diff --git a/compiler/checker/checker.odin b/compiler/checker/checker.odin index 042bdcc..874f10e 100644 --- a/compiler/checker/checker.odin +++ b/compiler/checker/checker.odin @@ -68,9 +68,18 @@ Build_Ctx :: struct { global_reads: ^[dynamic]hir.Global_Id, calls: ^[dynamic]hir.Function_Id, problematic: ^bool, - // Number of enclosing loops being built. `break`/`continue` are only valid - // when this is > 0; bumped around loop-body builds in `build_block`. - loop_depth: int, + // `defer` lowering. Deferred statements are built once at the `defer` site and + // their hir stmt ids stored here as a flat stack across scopes (one entry per + // 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 { @@ -737,6 +746,11 @@ mark_block_imports_used :: proc(checker: ^Checker, statements: []ast.Stmt_Id, fi case .For: mark_expr_imports_used(checker, statement.expr, 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 .Invalid: } @@ -1875,6 +1889,11 @@ infer_statements :: proc( } infer_statements(checker, statement.body, locals, local_types, pkg, file, demanded, result, result_hint) 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) @@ -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) } +// 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( ctx: ^Build_Ctx, statements: []ast.Stmt_Id, @@ -3971,6 +4001,7 @@ build_block :: proc( body: [dynamic]hir.Stmt_Id body.allocator = checker.allocator scope_start := len(ctx.locals^) + defer_start := len(ctx.defers^) duplicate_start := scope_start if duplicate_scope_start < 0 else duplicate_scope_start for statement_id in statements { statement := checker.ast_module.statements[statement_id] @@ -4224,6 +4255,16 @@ build_block :: proc( }) ctx.problematic^ = ctx.problematic^ || checker.module.exprs[value].kind == .Invalid 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 !types.is_void(ctx.result) { 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 } else { + flush_defers(ctx, &body, 0) append(&body, hir.stmt_id(len(checker.module.statements))) append(&checker.module.statements, hir.Stmt{ kind = .Return, span = statement.span, expr = hir.INVALID_EXPR, @@ -4257,12 +4299,32 @@ build_block :: proc( ctx.result, ctx.pkg, ctx.file, ) 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(&checker.module.statements, hir.Stmt{ kind = .Return, span = statement.span, expr = value, local = hir.INVALID_LOCAL, diagnostic = source.INVALID_DIAGNOSTIC, }) - ctx.problematic^ = ctx.problematic^ || checker.module.exprs[value].kind == .Invalid case .Expression: 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) { @@ -4443,9 +4505,9 @@ build_block :: proc( condition = invalid_hir_expr(checker, statement.span, id, types.BOOL) ctx.problematic^ = true } - ctx.loop_depth += 1 + append(ctx.loop_defer_starts, len(ctx.defers^)) loop_body := build_block(ctx, statement.body) - ctx.loop_depth -= 1 + pop(ctx.loop_defer_starts) update := hir.INVALID_STMT if statement.update != ast.INVALID_STMT { 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}) } } - ctx.loop_depth += 1 + append(ctx.loop_defer_starts, len(ctx.defers^)) loop_body := build_block(ctx, statement.body, capture_start) - ctx.loop_depth -= 1 + pop(ctx.loop_defer_starts) resize(ctx.locals, capture_start) append(&body, hir.stmt_id(len(checker.module.statements))) @@ -4571,7 +4633,9 @@ build_block :: proc( ctx.problematic^ = true } 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" id := source.addf(checker.diagnostics, statement.span, "'%s' outside of a loop", keyword) append(&body, hir.stmt_id(len(checker.module.statements))) @@ -4582,12 +4646,50 @@ build_block :: proc( ctx.problematic^ = true 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(&checker.module.statements, hir.Stmt{ kind = .Break if statement.kind == .Break else .Continue, span = statement.span, expr = hir.INVALID_EXPR, 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: append(&body, hir.stmt_id(len(checker.module.statements))) append(&checker.module.statements, hir.Stmt{ @@ -4597,6 +4699,18 @@ build_block :: proc( 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) return body[:] } @@ -4652,6 +4766,34 @@ loop_body_breaks :: proc(module: ^hir.Module, stmts: []hir.Stmt_Id) -> bool { 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) { spec := checker.specs[id] 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{ checker = checker, pkg = function.pkg, @@ -4762,6 +4908,8 @@ build_function :: proc(checker: ^Checker, id: Spec_Id) { global_reads = &global_reads, calls = &calls, problematic = &problematic, + defers = &defers, + loop_defer_starts = &loop_defer_starts, } block := build_block(&ctx, function.body) returns := all_paths_return(&checker.module, block) @@ -4806,6 +4954,11 @@ build_function :: proc(checker: ^Checker, id: Spec_Id) { diagnostic = source.INVALID_DIAGNOSTIC, }, ) + for entry in defers { + delete(entry, checker.allocator) + } + delete(defers) + delete(loop_defer_starts) delete(locals) } diff --git a/compiler/lexer/lexer.odin b/compiler/lexer/lexer.odin index 0360951..ebe3fa4 100644 --- a/compiler/lexer/lexer.odin +++ b/compiler/lexer/lexer.odin @@ -34,6 +34,7 @@ keyword_kind :: proc(text: string) -> token.Kind { case "for": return .Keyword_For case "break": return .Keyword_Break case "continue": return .Keyword_Continue + case "defer": return .Keyword_Defer case "else": return .Keyword_Else case "true": return .Keyword_True case "false": return .Keyword_False diff --git a/compiler/parser/parser.odin b/compiler/parser/parser.odin index 585399f..2286b1f 100644 --- a/compiler/parser/parser.odin +++ b/compiler/parser/parser.odin @@ -1001,6 +1001,41 @@ parse_loop_control :: proc(parser: ^Parser, kind: ast.Stmt_Kind) -> ast.Stmt_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 ` 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 { if current(parser).kind != .Left_Bracket { 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 { 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 { start_cursor := parser.cursor @@ -1333,7 +1375,7 @@ parse_while_update :: proc(parser: ^Parser) -> ast.Stmt_Id { statement := &parser.module.statements[update] switch statement.kind { 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( parser.diagnostics, statement.span, diff --git a/compiler/token/token.odin b/compiler/token/token.odin index 5e11d79..3b296e8 100644 --- a/compiler/token/token.odin +++ b/compiler/token/token.odin @@ -69,6 +69,7 @@ Kind :: enum u8 { Keyword_For, Keyword_Break, Keyword_Continue, + Keyword_Defer, Keyword_Else, Keyword_True, Keyword_False, diff --git a/compiler_tests.odin b/compiler_tests.odin index 6fe5465..87e6724 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -2061,6 +2061,69 @@ missing_return :: func() i32 { 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) foreign_function_links_from_c_source :: proc(t: ^testing.T) { output := "/tmp/brolang-test-foreign-source" diff --git a/examples/programs/defer/main.bro b/examples/programs/defer/main.bro new file mode 100644 index 0000000..3af208d --- /dev/null +++ b/examples/programs/defer/main.bro @@ -0,0 +1,82 @@ +# Milestone 19: `defer` and bare block statements. +# +# `defer ` 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 +}