From a3450d687628661aa0316cfc995934f6ce0c4e90 Mon Sep 17 00:00:00 2001 From: zinc Date: Wed, 14 Oct 2026 19:56:53 +0330 Subject: defer: any single statement (if/else, loops, switch, unchecked, unbounded), not just an expression or a block --- TODO.md | 2 +- docs/LEARN.md | 2 +- docs/SPEC.md | 2 +- src/compiler/stmt.zc | 12 ++++++++---- .../conformance/4-statements/defer_statement_forms.zc | 19 +++++++++++++++++++ 5 files changed, 30 insertions(+), 7 deletions(-) create mode 100644 tests/conformance/4-statements/defer_statement_forms.zc diff --git a/TODO.md b/TODO.md index db13720..06fcf0b 100644 --- a/TODO.md +++ b/TODO.md @@ -520,7 +520,7 @@ brief someone without re-reading the code. - [ ] borrow returns tied to an argument. inference already works for a callee defined before the call: `char* skip(char* s) { ...; return s + i; }` makes `let r = skip(t); rt.free(t); r[0]` an error. the gap: when the call comes before the callee's definition, nothing is reported and the program reads freed memory. extend the deferred effect requirements (`effect_require`, `effect_finish` at unit end) so a forward-called function whose return turns out to alias a parameter produces the same error. for bodyless declarations, externs and function types, add an unobtrusive spelling, for example a `borrows s` clause after the parameter list (`char* skip(char* s) borrows s;`), checked against the body when one exists. - [ ] variadic arguments and `std.print`. design: a pack is passed as one hidden argument, a caller-built read-only growable-array handle (`fixed T[?]` view: [len, cap, items, element size] in the caller's frame, so `len(a)` and `a[i]` work unchanged and nothing is allocated). `...int a` is a typed pack; several packs per function when their element types differ (`void f(...int a, ...char b)`): each extra argument goes to the pack whose element type matches its static type, an ambiguous match is an error, packs come after the fixed parameters. `...any args` packs each value with a kind code (int, float, char, string, bstr, pointer, ...) and its type record: `any` is a small built-in record { kind, value, type }. `...*any args` makes the call comma-free: after the fixed arguments, the parser reads unary expressions until `)`, commas optional, so `std.print("x = " name ", age " age "\n");` works and binary expressions must be parenthesized. `std.print(...*any args)` prints each argument by its kind (`put_n`, the float printer, `put_s`, `put_bs`, a character for `char`, hex for pointers). - [x] `memdebug`'s `obj_base` used to search only 64 words back for a block header, so `buf[255]` on a 256-word block reported "bad heap pointer". it now rounds the pointer down to each size class's alignment (largest first) and takes the first header whose check word, size and class agree and whose payload holds the pointer, so interiors at any depth work, a deep pointer into a freed block reports use after free, and header words and free space still report "bad heap pointer". regression: `tests/programs/memdebug_interior.zc` (in `tests/test.sh` beside the other memdebug checks). note that a stray pointer that lands inside another live block is accepted, since memdebug cannot know which block it came from. -- [ ] (partly done, see "agent work at the end of the session") `defer` follow-ups (small): accept `defer if (...) ...;` and other single statements besides an expression; decide whether deferred code may name an owner declared after the `defer` (now rejected by the lexical rule; one line in `defer_one`). +- [ ] (partly done, see "agent work at the end of the session") `defer` follow-ups (small): done on work/defer: `defer` takes any single statement (if/else, loops, switch, unchecked, unbounded). still open: decide whether deferred code may name an owner declared after the `defer` (now rejected by the lexical rule; one line in `defer_one`). - [ ] stack-effect checking: every function and `asm` block declares or is inferred to have a fixed data-stack effect, and the compiler reports a block whose paths leave the stack unbalanced. the first step of a proving scaffold. - [ ] declared side effects at module boundaries (globals touched, syscalls or not), checked against the effect masks the compiler already computes. - [ ] `requires`/`ensures` contracts on functions, compiled to run-time checks under a switch, with the cases the value facts settle (constants, simple ranges) checked statically. diff --git a/docs/LEARN.md b/docs/LEARN.md index e9cdbcf..fd399e3 100644 --- a/docs/LEARN.md +++ b/docs/LEARN.md @@ -176,7 +176,7 @@ output: ### defer -`defer expr;` or `defer { statements }` runs code when the enclosing block ends: after the last statement, and also on `return`, `break` and `continue` out of it. defers in a block run last registered first. the deferred code is compiled at each exit, so it sees the variables as they are when it runs, and `return e;` computes `e` before any defer runs. `defer rt.free(p);` hands the owner `p` over at that point: `p` stays usable until the block ends and is not freed twice. `return`, `break` and `continue` are not allowed inside a deferred statement. +`defer expr;`, `defer { statements }` or any other single statement (`defer if (c) a; else b;`, a loop, a `switch`) runs code when the enclosing block ends: after the last statement, and also on `return`, `break` and `continue` out of it. defers in a block run last registered first. the deferred code is compiled at each exit, so it sees the variables as they are when it runs, and `return e;` computes `e` before any defer runs. `defer rt.free(p);` hands the owner `p` over at that point: `p` stays usable until the block ends and is not freed twice. `return`, `break` and `continue` are not allowed inside a deferred statement. [example source](../examples/learn/34-defer.zc): diff --git a/docs/SPEC.md b/docs/SPEC.md index 32afdfb..aad7ff6 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -640,7 +640,7 @@ int y = 7; asm { y @ 1 + y ! } // y is 8 ### 4.14 defer -4.14.1 `defer expr;` and `defer { statements }` register a statement that runs when the block that contains the `defer` ends. the tokens are kept, not compiled, at the `defer`; the compiler compiles them again, in place, at every point where control leaves the block: the end of the block, and a `return`, `break` or `continue` that leaves it. (a function body is a block; so are a loop or `if` body, a `switch` case and a block expression.) the code is therefore duplicated at each exit. +4.14.1 `defer` followed by one statement (`defer expr;`, `defer { statements }`, `defer if (c) a; else b;`, a loop, a `switch`, or an `unchecked` or `unbounded` statement) registers that statement; it runs when the block that contains the `defer` ends. the tokens are kept, not compiled, at the `defer`; the compiler compiles them again, in place, at every point where control leaves the block: the end of the block, and a `return`, `break` or `continue` that leaves it. (a function body is a block; so are a loop or `if` body, a `switch` case and a block expression.) the code is therefore duplicated at each exit. 4.14.2 several defers in one block run in reverse order of registration. an early exit runs the defers of every block it leaves, innermost block first, and within a block newest first. `return` runs those of the whole function, `break` and `continue` only those inside the loop or switch they leave. diff --git a/src/compiler/stmt.zc b/src/compiler/stmt.zc index d23cb52..0182728 100644 --- a/src/compiler/stmt.zc +++ b/src/compiler/stmt.zc @@ -448,22 +448,26 @@ int kw_continue() { // happen. a defer a branch never reached is not on the stack at an exit on // another path, because the stack only holds the defers read so far in the // blocks still open. the entries of an exit run newest first. +// a block, or an if, while, for, switch, unchecked or unbounded statement, can end +// at a } (blk); either way an else after the end of an if branch belongs to it. word .u defer_tokens() { - int n = 0, depth = 0, blk = is_op(s_lb), semi, bad = 0; word q; + int n = 0, depth = 0, blk = is_op(s_lb), semi, bad = 0, rb; word q; + if (tk == T_ID && (tv == intern("if") || tv == s_while || tv == intern("for") || tv == intern("switch") || tv == s_unchecked || tv == s_unbounded)) blk = 2; while (1) { if (tk == T_EOF) { bad = 1; break; } - semi = 0; + semi = 0; rb = 0; if (tk == T_OP) { if (tv == s_lb || tv == s_lp || tv == s_lk) depth++; - else if (tv == s_rb || tv == s_rp || tv == s_rk) depth--; + else if (tv == s_rb || tv == s_rp || tv == s_rk) { depth--; rb = tv == s_rb; } else if (tv == s_semi && depth == 0) semi = 1; } if (depth < 0) { bad = 1; break; } reserve_R_QBUF((n + 1) * 3); R_QBUF[n * 3] = tk; R_QBUF[n * 3 + 1] = tv; R_QBUF[n * 3 + 2] = tpack(); n++; next(); + if ((semi || (blk && rb && depth == 0)) && blk == 2 && is_id(s_else)) continue; if (semi) break; - if (blk && depth == 0) { accept(s_semi); break; } + if (blk && rb && depth == 0) { if (blk == 1) accept(s_semi); break; } } if (bad) { die("missing ; after the deferred statement"); n = 0; } q = rt.new(n * 3 + 1); diff --git a/tests/conformance/4-statements/defer_statement_forms.zc b/tests/conformance/4-statements/defer_statement_forms.zc new file mode 100644 index 0000000..00ddc35 --- /dev/null +++ b/tests/conformance/4-statements/defer_statement_forms.zc @@ -0,0 +1,19 @@ +// expect: 0 6 20 10 11 5 2 2 /0 6 21 10 11 3 1 2 /9 8 7 1 / +// spec 4.14.1: defer takes any single statement: if/else chains, loops, switch, unchecked and blocks +include rt; +void pn(int x) { rt.put_n(x); rt.put_c(32); } +int f(int c) { + int i = 0; + { + defer if (c) pn(1); else pn(2); + defer if (c > 1) { pn(3); } else if (c) { pn(4); } else { pn(5); } + defer while (i < 2) i++; + defer for (int k = 0; k < 2; k++) pn(10 + k); + defer switch (c) { case 0: pn(20); break; default: pn(21); } + defer { pn(6); }; + pn(0); + } + return i; +} +fn main() { int c = 1; pn(f(0)); rt.put_c(10); pn(f(2)); rt.put_c(10); { defer if (c) { pn(1); } pn(9); defer unchecked { pn(7); } pn(8); } rt.put_c(10); return 0; } +// zc0: skip -- cgit v1.2.3