diff options
| author | 2026-10-14 10:39:47 +0000 | |
|---|---|---|
| committer | 2026-10-14 10:39:47 +0000 | |
| commit | 54b8a8918bb63c98a8f58e823033232243647562 (patch) | |
| tree | e0608f7dab1f568c22390262e5a6d39966519760 | |
| parent | todo: handoff section with state, conventions, in-flight work and architectur... (diff) | |
zc: add the defer statement
defer expr; and defer { ... } register code that runs when the enclosing
block ends: at the end of the block, and at return, break and continue
that leave it. the tokens are kept at the defer and compiled again at each
exit (block_close, body, kw_return, kw_break, kw_continue), newest first,
before the block's variables are released, so the analyzer sees a deferred
rt.free(p) as the transfer of p where it runs. names mean what they meant
at the defer; values are read when it runs. return, break and continue
inside a deferred statement are errors, reported once. works in unchecked
code and closures (own base per function context).
| -rw-r--r-- | README.md | 2 | ||||
| -rw-r--r-- | TODO.md | 2 | ||||
| -rw-r--r-- | docs/LEARN.md | 38 | ||||
| -rw-r--r-- | docs/SPEC.md | 19 | ||||
| -rw-r--r-- | editors/vscode/syntaxes/zc.tmLanguage.json | 2 | ||||
| -rw-r--r-- | examples/learn/34-defer.out | 4 | ||||
| -rw-r--r-- | examples/learn/34-defer.zc | 20 | ||||
| -rw-r--r-- | src/compiler/analyzer.zc | 1 | ||||
| -rw-r--r-- | src/compiler/base.zc | 8 | ||||
| -rw-r--r-- | src/compiler/check.zc | 1 | ||||
| -rw-r--r-- | src/compiler/decl.zc | 8 | ||||
| -rw-r--r-- | src/compiler/setup.zc | 1 | ||||
| -rw-r--r-- | src/compiler/stmt.zc | 100 | ||||
| -rw-r--r-- | src/lsp/server.zc | 3 | ||||
| -rw-r--r-- | tests/errors/defer_freed_twice.zc | 10 | ||||
| -rw-r--r-- | tests/errors/defer_moved.zc | 11 | ||||
| -rw-r--r-- | tests/errors/defer_moved_early.zc | 14 | ||||
| -rw-r--r-- | tests/langerrors/defer_break.zc | 11 | ||||
| -rw-r--r-- | tests/langerrors/defer_continue.zc | 11 | ||||
| -rw-r--r-- | tests/langerrors/defer_later_name.zc | 9 | ||||
| -rw-r--r-- | tests/langerrors/defer_return.zc | 8 | ||||
| -rw-r--r-- | tests/programs/defer.zc | 235 | ||||
| -rwxr-xr-x | tests/test.sh | 1 |
23 files changed, 513 insertions, 6 deletions
@@ -168,7 +168,7 @@ fn main() { } ``` -declarations are C-style, type first: `int a = 1, b;`, `node* p = rt.new(node);`, `char[4] buf;`, `point origin;`. `let` declares without a type: it infers `char*` from a word-string literal and `bstr` from a packed byte-string literal, infers other pointers and function types from the initializer, uses a pointer alias for an in-place array or struct, and keeps numbers as plain words. `let a[10];` is an untyped array. `const k = 5;` inside a function is a local constant with no type: it takes the initializer's type, needs an initializer, and can't be assigned. statements are declarations, `if`/`else`, `while`, `do`/`while`, `for`, `switch`, `break`, `continue`, `return`, `{ }` blocks, `asm { }` and expressions. `do` body `while (` cond `);` runs the body once before the test, and `continue` jumps to the test. `enum color { RED, GREEN = 5, BLUE };` makes global constants, and `color` is a type of its own in typed code (`typecheck on;`): a member takes the enum, a plain int does not become a `color`, and a `color` still becomes an `int`. untyped code treats the members as plain ints. each member is also reached as `color.BLUE`, in an expression or a `case` label. a member can carry fields too: `enum shape { circle { int r; }, rect { int w; int h; }, dot };` makes the constructors `shape.circle(3)` and `shape.rect(4, 5)`, which return an owned `shape*`, and `shape.dot()`. the tag is the word at offset 0 of the object. a `switch` over a `shape*` matches a member with `case circle(r):`, which binds its fields in that arm only. a switch on a tagged enum that leaves out a member and has no `default` gets a warning on its line (not an error). `(T)e` is a cast with the same meaning as `e as T`: the bits stay, the type changes. it has to be a simple type the parser can see in a few tokens, `int` or `*node`, not a nested array. `include "path";` reads a file and compiles it at that point, signatures first, so a library can live in its own file. each file has its own namespace and imports; dependencies included by a file do not import into the caller. `u`, `m` and `d` are ordinary names. a tag is `.u`, `.m` or `.d`, and `u*x` with no space before the star is still a recast. `switch (e) { case 1, 2: ... default: ... }` takes constant labels, and its cases don't fall through: each ends where the next begins, and `break` leaves early. operators follow C precedence, including `c ? a : b`, plus `p.x` and `p->x` (the same thing: `.` goes through a pointer on its own), `e as T` to change a type without changing the bits, and the casts below. comparisons and `&& ||` give 0 or 1. `/`, `%` and `<<` compile to native enhancement opcodes. character literals, `0x` hex and the usual escapes are there. +declarations are C-style, type first: `int a = 1, b;`, `node* p = rt.new(node);`, `char[4] buf;`, `point origin;`. `let` declares without a type: it infers `char*` from a word-string literal and `bstr` from a packed byte-string literal, infers other pointers and function types from the initializer, uses a pointer alias for an in-place array or struct, and keeps numbers as plain words. `let a[10];` is an untyped array. `const k = 5;` inside a function is a local constant with no type: it takes the initializer's type, needs an initializer, and can't be assigned. statements are declarations, `if`/`else`, `while`, `do`/`while`, `for`, `switch`, `break`, `continue`, `return`, `defer`, `{ }` blocks, `asm { }` and expressions. `do` body `while (` cond `);` runs the body once before the test, and `continue` jumps to the test. `defer expr;` or `defer { ... }` registers code that runs when the enclosing block ends, on every way out of it: falling off the end, `return`, `break` and `continue`. several defers in a block run last registered first, the deferred code is compiled at each exit and so reads the variables as they are when it runs (a `return e;` computes `e` first, then runs the defers), a defer that was never reached registers nothing, and `return`, `break` and `continue` inside a deferred statement are errors. a deferred `rt.free(p);` is the transfer of `p` for the analyzer at the point it runs: `p` stays usable until the block ends, is not freed or reported a second time, and freeing an owner that was moved away meanwhile is the usual use-after-move error. defer also works in `unchecked` code. `enum color { RED, GREEN = 5, BLUE };` makes global constants, and `color` is a type of its own in typed code (`typecheck on;`): a member takes the enum, a plain int does not become a `color`, and a `color` still becomes an `int`. untyped code treats the members as plain ints. each member is also reached as `color.BLUE`, in an expression or a `case` label. a member can carry fields too: `enum shape { circle { int r; }, rect { int w; int h; }, dot };` makes the constructors `shape.circle(3)` and `shape.rect(4, 5)`, which return an owned `shape*`, and `shape.dot()`. the tag is the word at offset 0 of the object. a `switch` over a `shape*` matches a member with `case circle(r):`, which binds its fields in that arm only. a switch on a tagged enum that leaves out a member and has no `default` gets a warning on its line (not an error). `(T)e` is a cast with the same meaning as `e as T`: the bits stay, the type changes. it has to be a simple type the parser can see in a few tokens, `int` or `*node`, not a nested array. `include "path";` reads a file and compiles it at that point, signatures first, so a library can live in its own file. each file has its own namespace and imports; dependencies included by a file do not import into the caller. `u`, `m` and `d` are ordinary names. a tag is `.u`, `.m` or `.d`, and `u*x` with no space before the star is still a recast. `switch (e) { case 1, 2: ... default: ... }` takes constant labels, and its cases don't fall through: each ends where the next begins, and `break` leaves early. operators follow C precedence, including `c ? a : b`, plus `p.x` and `p->x` (the same thing: `.` goes through a pointer on its own), `e as T` to change a type without changing the bits, and the casts below. comparisons and `&& ||` give 0 or 1. `/`, `%` and `<<` compile to native enhancement opcodes. character literals, `0x` hex and the usual escapes are there. `typedef T name;` gives a type another name, as in c, at the top level or inside a function (where it lasts to the end of its block, like a local): `typedef node* link;`, `typedef int[4] quad;`, `typedef int(int, int) binop;`, `typedef list<int> ints;`. `type name = T;` says the same thing. a typedef is only a new spelling: `link` and `node*` are the very same type record, so they mix freely in declarations, parameters, results, casts, `sizeof`, struct fields, generic arguments and `typecheck on;` code, and they print as the target type. `T` can be any type the parser accepts, but not a tag (`.u`, `.m` and `.d` belong to a declaration). a typedef can't name a type that isn't declared yet, so a struct comes before the typedefs of its pointers; using a name that is already defined is an error, and a typedef of `void` is `void` (a variable of it is the usual error). `string` is built in as a typedef of `char*`, so `string s = "hi";` and `string f(string a)` work with no declaration; a function, variable or parameter of the same name shadows it. @@ -219,7 +219,7 @@ brief someone without re-reading the code. - [ ] (needs zc0 structs) group the compiler's parallel per-local arrays (`lname`, `ltype`, `lstate`, `lhome`, `lown`, `lslot`, `ltag`, `lpos`, `lnsz`, `lleaf`, ...; many are declared as `word[NL] x_initial; word x = x_initial;` and grow) into one record per local with accessor functions, so a branch snapshot is one block copy and each field's invariant is written once next to the record. then symbols, then types. the genome must keep reaching its fixpoint and the analyzer's behavior must not change (rejection suites and program checks identical). - [ ] continue removing `unchecked` from the runtime. a reverted trial showed: `copy_w`/`fill_w` pass the analyzer when their gate stores go through `syscall(SYS_COPY, ...)`, `dgrow` with `own(w, it)` instead of `w[-2] = it`, `dpush` with `own` before the store, `dnew` as is; but the checked versions changed behavior (7 program checks failed, `tests/programs/share.zc` hung with a checked `cons`), so checked code inserts cleanup these functions can't afford. each needs its contract taught to the analyzer. `free`, `recycle`, `recycle_leaf`, `gc` are the allocator and collector the analyzer's own cleanup calls: document them as the irreducible escapes. `src/lib/actor.zc` has about 20 more. - [ ] revise diagnostics, comments and learning/reference prose using professional compiler documentation guidance, preserving lowercase and adding message regressions. touches messages everywhere, so do it when few other compiler changes are in flight. -- [ ] `defer` statement: `defer expr;` or `defer { ... }` runs when the enclosing block ends, on every exit path, in reverse order, evaluating its expressions when it runs (like the c defer proposal), with ownership awareness: a deferred call that consumes an owner is that owner's transfer. architecture: grab the deferred statement's tokens at the `defer` (like `grab()` in the statements part) and keep a stack of [block depth, token list]. replay a list with `src_push(1, q, 0, 0); next(); stmt(); src_pop();`. block ends go through `block_close` (call it there before `exit_vars`, for entries at the current depth, newest first); `kw_return` evaluates its value first, then must replay every entry of the function (depth >= 1) before `exit_vars(1, 0)`; `kw_break` and `kw_continue` replay entries deeper than the loop's depth (`f[2] + 1`) before their `exit_vars`. after an early exit `live = 0`, so the analyzer state changed by the replay is discarded. closures keep their own base index. reject `return`, `break`, `continue` inside a deferred statement. defer must also work in unchecked code (`exit_vars` returns early there, so replay independently of `fchk`). +- [x] `defer` statement (`kw_defer`, `defer_run`, `block_close`, `kw_return`, `kw_break`, `kw_continue`): see `docs/SPEC.md` 4.14. - [ ] test blocks: `test "name" { ... }` with `expect(cond)`, compiled only by `zy --test file.zc`, which runs each block and prints one line per test with the position of the first failed `expect`; skipped at parse time otherwise. build it as a library through `run.syntax` and `run.statement_hook`, like `src/lib/actor.zc`. - [ ] (in flight) bounds checking on by default, `unbounded` to turn it off (function prefix, block, top-level `unbounded;`, mirroring `unchecked`), implied by `o3`. architecture: indexing is compiled in the postfix `[` handling of the expressions part; growable arrays already check (`dat`, `range_jz`, the `outside` enhancement, `proven()` removing checks a loop test already did). fixed arrays `T[n]` with a variable index get the same inline check against the constant `n`. pointers get a runtime call (for example `rt.bchk(p, i)`) that checks only when `p` is the start of a live heap block (header check word `p ^ MAGIC`, `paylen`), skipping constant index 0 and sizes the analyzer knows (`lnsz`). reports go through `oob`. the runtime, core, compiler parts, server, `run.zc` and `build.zc` start with `unbounded;`, which zc0 accepts and ignores (genome bytes unchanged). keep std, icon and actor checked unless a function touches raw memory on purpose. measure interpreter time before and after. - [ ] after the fuzzing work, add more general functionality to the standard library. diff --git a/docs/LEARN.md b/docs/LEARN.md index a3588e7..d514d85 100644 --- a/docs/LEARN.md +++ b/docs/LEARN.md @@ -174,6 +174,44 @@ output: 0 5 6 ``` +### 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. + +[example source](../examples/learn/34-defer.zc): + +```zc +namespace; +include rt; +fn say(n) { rt.put_n(n); rt.put_c(32); return 0; } +int work(int n) { + int* p = rt.new(int); + defer rt.free(p); + defer say(100); + *p = n * 2; + say(*p); + if (n > 2) return *p + 1; + say(1); + return 0; +} +fn main() { + for (let i = 0; i < 4; i++) { + defer { say(i); rt.put_c(10); } + if (i == 1) continue; + say(work(i)); + } +} +``` + +output: + +```text +0 1 100 0 0 +1 +4 1 100 0 2 +6 100 7 3 +``` + ## numbers and strings ### sized integers diff --git a/docs/SPEC.md b/docs/SPEC.md index 51ed5d1..f3e47a7 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -53,6 +53,7 @@ a name that looks like `x.y` where `x` is not a known module is the three tokens | `while` `for` `do` | S | | | loops (4.4 to 4.6) | | `switch` | S | | | 4.7 | | `break` `continue` `return` | S | | | 4.8 | +| `defer` | S | | | 4.14 | | `enum` | S | T | | 5.8 | | `include` | S | T | | 5.12 (a statement only by accident, 5.12.7) | | `embed` | S | T | | 5.11 | @@ -636,6 +637,24 @@ int x = asm { 2 3 + }; // 5 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.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. + +4.14.3 the deferred code is evaluated when it runs, not when it is registered (as in the c defer proposal): it reads the current values of the variables it names. which variable a name means is fixed where the `defer` is written: only the locals declared before the `defer` are visible to it, so a variable that an inner block declares again, or one declared later in the block, is not seen (naming one that does not exist yet is error: y is used but never defined). + +4.14.4 `return e;` evaluates `e` first, then runs the defers, then returns the value; a defer that changes a variable `e` read does not change the result. + +4.14.5 a `defer` registers when control reaches it. a `defer` in an `if` branch belongs to that branch's block and ends with it; an early `return` before a `defer` in the same block does not run it. + +4.14.6 `return`, `break` and `continue` that would leave a deferred statement are errors: return can't be used in a deferred statement, break can't leave a deferred statement, continue can't leave a deferred statement. loops, `switch` and closures inside the deferred statement may use their own. a deferred statement is compiled once per exit, but an error in it is reported once. + +4.14.7 ownership (6): the analyzer sees the deferred statement at the exit, before the block's locals are released. `defer rt.free(p);` is the transfer of `p` at that point: `p` stays alive and usable from the `defer` until the block ends, it is not freed again nor reported as never freed, and if `p` was given away meanwhile (a second `rt.free(p)`, a call to a `.d` parameter, a store into a global) the deferred `rt.free(p)` is error: p: used after it was given away. + +4.14.8 defer works in `unchecked` code: the analyzer's cleanup is off there, but the deferred code runs. a defer keeps the checked or unchecked mode it was written in. a function that falls off its end runs its function-level defers before returning 0. + ## 5. declarations at the top level ### 5.1 items diff --git a/editors/vscode/syntaxes/zc.tmLanguage.json b/editors/vscode/syntaxes/zc.tmLanguage.json index 6f76e03..fab026b 100644 --- a/editors/vscode/syntaxes/zc.tmLanguage.json +++ b/editors/vscode/syntaxes/zc.tmLanguage.json @@ -83,7 +83,7 @@ "patterns": [ { "name": "keyword.control.zc", - "match": "\\b(if|else|while|do|for|switch|case|default|break|continue|return)\\b" + "match": "\\b(if|else|while|do|for|switch|case|default|break|continue|return|defer)\\b" }, { "name": "keyword.declaration.zc", diff --git a/examples/learn/34-defer.out b/examples/learn/34-defer.out new file mode 100644 index 0000000..284c915 --- /dev/null +++ b/examples/learn/34-defer.out @@ -0,0 +1,4 @@ +0 1 100 0 0 +1 +4 1 100 0 2 +6 100 7 3 diff --git a/examples/learn/34-defer.zc b/examples/learn/34-defer.zc new file mode 100644 index 0000000..10e46e1 --- /dev/null +++ b/examples/learn/34-defer.zc @@ -0,0 +1,20 @@ +namespace; +include rt; +fn say(n) { rt.put_n(n); rt.put_c(32); return 0; } +int work(int n) { + int* p = rt.new(int); + defer rt.free(p); + defer say(100); + *p = n * 2; + say(*p); + if (n > 2) return *p + 1; + say(1); + return 0; +} +fn main() { + for (let i = 0; i < 4; i++) { + defer { say(i); rt.put_c(10); } + if (i == 1) continue; + say(work(i)); + } +} diff --git a/src/compiler/analyzer.zc b/src/compiler/analyzer.zc index 505ac09..8c63bdf 100644 --- a/src/compiler/analyzer.zc +++ b/src/compiler/analyzer.zc @@ -682,6 +682,7 @@ int local_hints(int from) { // object is an error, not a free. int block_close(int save) { int i = save, k; + defer_run(fdepth, 1); // deferred statements of this block, before its variables go af_close(save); local_hints(save); if (o3on) { diff --git a/src/compiler/base.zc b/src/compiler/base.zc index 17ec80b..d83dc9d 100644 --- a/src/compiler/base.zc +++ b/src/compiler/base.zc @@ -248,6 +248,14 @@ word xprojection; int af_mutation_tick; int xbor = -1; // local a pointer value is borrowed from, or -1 int mdbg, mdbg_flags; // memdebug: compile checks in, and the flags start() turns on int lsp, loopbase, ctxsp, snaptop; +// defer: the statements registered and not yet run, DFR_W words each (the block +// depth of the defer, its token list, whether the analyzer was on, whether compiling +// it failed, so the error is reported once, and the locals there were at the defer), +// newest last. dfr_base is where the current function's entries start. dfr_on is set +// while a deferred statement is compiled; dfr_lsp and dfr_ctx say where its own loops +// and function begin. +const int DFR_MAX = 256, DFR_W = 5; +word[1280] dfr; int ndfr, dfr_base, dfr_on, dfr_lsp, dfr_ctx; int lv, lvk, lvi, kw_expr, xcls, fty; // interned spellings the parser compares against diff --git a/src/compiler/check.zc b/src/compiler/check.zc index a22bcdd..f3374b1 100644 --- a/src/compiler/check.zc +++ b/src/compiler/check.zc @@ -29,6 +29,7 @@ int chk_reset() { tline = 1; toff = 0 - 1; toklen = 0; tpend = 0 - 1; tpline = 1; infn = 0; + defer_drop(0); dfr_base = 0; dfr_on = 0; fdepth = 0; fslots = 0; fchk = 0; diff --git a/src/compiler/decl.zc b/src/compiler/decl.zc index 45269a4..19b1811 100644 --- a/src/compiler/decl.zc +++ b/src/compiler/decl.zc @@ -153,7 +153,7 @@ int unit_stdin() { // that a nested compile would otherwise clobber (locals, depth, frame size, // loop stack position, the expression-state globals). closures use the same // pair. -const int CTX_WORDS = 48; +const int CTX_WORDS = 49; int ctx_push() { af_forget(); word f; @@ -164,7 +164,7 @@ int ctx_push() { f[13] = lv; f[14] = lvk; f[15] = lvi; f[16] = kw_expr; f[17] = xcls; f[18] = snaptop; f[19] = fty; f[20] = ntp; f[21] = xty; rt.copy_w(f + 22, tpn, 8); f[30] = finsym; f[31] = finfer; f[32] = ifcur; f[33] = ifcall; f[34] = effect_current; f[35] = borrow_slots; f[36] = borrow_pending; f[37] = borrow_depth; f[38] = borrow_jump; f[39] = borrow_entry; f[40] = capture_enabled; f[41] = effect_value; f[42] = xprojection; f[43] = effect_floor; f[44] = module_name; f[45] = module_imports; - f[46] = fn_return_type; f[47] = if_value; + f[46] = fn_return_type; f[47] = if_value; f[48] = dfr_base; return 0; } // pop the innermost function context. @@ -175,6 +175,7 @@ int ctx_pop() { local_drop(f[0]); fdrop(nloc); lend_drop(1 << 30, f[0]); + defer_drop(dfr_base); dfr_base = f[48]; lbase = f[1]; fdepth = f[2]; fslots = f[3]; @@ -204,6 +205,7 @@ int fn_begin(int chk, int ret) { capture_enabled = 0; effect_value = 0; effect_current = 0; borrow_slots = 0; borrow_pending = 0; borrow_depth = 0; borrow_jump = -1; borrow_entry = 0; if (cmode && svkind && !dry) rt.fill_w(svkind, 0, nsym); xvk = 0; lend_drop(1 << 30, nloc); + dfr_base = ndfr; lbase = nloc; fdepth = 1; fslots = 0; fchk = chk && check_on; fret = ret; fty = 0; finfer = 0; fn_return_type = 0; live = 1; bxdepth = 0; epichain = 0; infn = 1; loopbase = lsp; lv = 0; lvk = LV_NONE; xbor = -1; return 0; @@ -258,6 +260,7 @@ int body() { } closed = is_op(s_rb); endplace = toff + toklen; if (probe_off >= 0 && !chkabort && !dry && !ginst && incdepth == 0 && ctxsp <= nclo && toff >= probe_off) { probe_snap(); chkabort = 1; } + defer_run(1, 1); // the function's own deferred statements, before its locals are checked local_hints(lbase); unused_check(lbase); if (ifcur) if_fin(); @@ -1343,6 +1346,7 @@ int compile_list_context(int q, int top, char* module, word imports) { fn_begin(1, 0); prologue(); borrow_prologue(); borrow_begin(); while (tk != T_EOF) stmt(); + defer_run(1, 1); unused_check(lbase); if (fchk && live) exit_vars(1, 1); epilogue(); diff --git a/src/compiler/setup.zc b/src/compiler/setup.zc index 4986fc3..1d8ce6f 100644 --- a/src/compiler/setup.zc +++ b/src/compiler/setup.zc @@ -110,6 +110,7 @@ int zc_tables() { add_kw("switch", kw_switch, F_STMT); add_kw("break", kw_break, F_STMT); add_kw("continue", kw_continue, F_STMT); + add_kw("defer", kw_defer, F_STMT); add_kw("return", kw_return, F_STMT); add_kw("unchecked", kw_unchecked, F_STMT); add_kw("asm", kw_asm, F_STMT | F_EXPR); diff --git a/src/compiler/stmt.zc b/src/compiler/stmt.zc index a2ad0d1..21d5801 100644 --- a/src/compiler/stmt.zc +++ b/src/compiler/stmt.zc @@ -391,6 +391,8 @@ int kw_break() { if (lsp <= loopbase) { serr("break outside a loop or a switch"); return 0; } f = R_LOOP + (lsp - 1) * 8; if (f[4] != bxdepth) { serr("break can't leave a block expression"); return 0; } + if (dfr_on && dfr_ctx == ctxsp && lsp <= dfr_lsp) { serr("break can't leave a deferred statement"); return 0; } + defer_run(f[2] + 1, 0); exit_vars(f[2] + 1, 0); if (live) exit_rec(hole()); else hole(); op(O_JMP); @@ -406,6 +408,8 @@ int kw_continue() { if (j < loopbase) { serr("continue outside a loop"); return 0; } f = R_LOOP + j * 8; if (f[4] != bxdepth) { serr("continue can't leave a block expression"); return 0; } + if (dfr_on && dfr_ctx == ctxsp && j < dfr_lsp) { serr("continue can't leave a deferred statement"); return 0; } + defer_run(f[2] + 1, 0); backedge(f[3]); exit_vars(f[2] + 1, 0); if (f[0] == -2) { f[7] = fixnew(hole(), f[7]); op(O_JMP); } // do/while: the condition isn't placed yet @@ -413,6 +417,100 @@ int kw_continue() { live = 0; return 0; } +// ---- defer ---- + +// defer expr; or defer { statements } registers code that runs when the +// enclosing block ends. nothing is compiled at the defer: its tokens are kept +// on the dfr stack (DFR_W words per entry: block depth, token list, whether +// the analyzer was on, whether it failed, locals at the defer) and compiled +// again, inline, at every exit that leaves the block: the end of the block +// (block_close, and body for a function), return, break and continue. so the +// deferred code is evaluated when it runs and sees the variables as they are +// then, and the analyzer sees its frees and moves at the exit, where they +// 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. +word .u defer_tokens() { + int n = 0, depth = 0, blk = is_op(s_lb), semi, bad = 0; word q; + while (1) { + if (tk == T_EOF) { bad = 1; break; } + semi = 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_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) break; + if (blk && depth == 0) { accept(s_semi); break; } + } + if (bad) { die("missing ; after the deferred statement"); n = 0; } + q = rt.new(n * 3 + 1); + q[0] = n; + rt.copy_w(q + 1, R_QBUF, n * 3); + return u*q; +} +int kw_defer() { + int k; word q; + stmtonly(); + if (!infn) { serr("defer outside a function"); return 0; } + if (ndfr >= DFR_MAX) { die("too many pending defers in one function"); return 0; } + next(); + q = defer_tokens(); + k = ndfr * DFR_W; + dfr[k] = fdepth; dfr[k + 1] = d*q; dfr[k + 2] = fchk; dfr[k + 3] = 0; dfr[k + 4] = nloc; + ndfr++; + return 0; +} +// forget the deferred statements from entry base up +int defer_drop(int base) { + word q; + while (ndfr > base) { ndfr--; q = dfr[ndfr * DFR_W + 1]; dfr[ndfr * DFR_W + 1] = 0; rt.free(q); } + return 0; +} +// compile one deferred statement here. the locals declared after the defer are +// hidden while it is compiled, so its names mean what they meant where it was +// written (a name declared later in the block, or one that an inner block +// declares again, is not seen). +int defer_one(int e) { + word q = dfr[e * DFR_W + 1]; int j, k, mark = dfr[e * DFR_W + 4], top = nloc, e0 = nerr; + int ofchk = fchk, oon = dfr_on, olsp = dfr_lsp, octx = dfr_ctx, n0 = ndfr; + j = nloc; + while (j > mark) { j--; k = local_key(lname[j]); lheads[k] = lprev[j]; } + fchk = dfr[e * DFR_W + 2]; dfr_on = 1; dfr_lsp = lsp; dfr_ctx = ctxsp; + src_push(1, q, 0, 0); next(); stmt(); + if (tk != T_EOF) serr("unexpected tokens after the deferred statement"); + src_pop(); + defer_drop(n0); + if (nloc > top) local_drop(top); + j = mark; + while (j < top) { k = local_key(lname[j]); lheads[k] = j; j++; } + fchk = ofchk; dfr_on = oon; dfr_lsp = olsp; dfr_ctx = octx; + if (nerr != e0) dfr[e * DFR_W + 3] = 1; + return 0; +} +// compile the deferred statements registered at block depth dep or deeper in +// this function, newest first, here. pop is for the end of a block, which +// forgets them; an early exit (return, break, continue) leaves them, the +// block's normal end still runs them on the other path. nothing is emitted +// where control can't arrive, and a statement that failed once is not +// compiled again, so its error is reported once. +int defer_run(int dep, int pop) { + int i = ndfr; + if (live) { + while (i > dfr_base && dfr[(i - 1) * DFR_W] >= dep) { + i--; + if (!dfr[i * DFR_W + 3]) defer_one(i); + } + } + if (pop) { + while (ndfr > dfr_base && dfr[(ndfr - 1) * DFR_W] >= dep) defer_drop(ndfr - 1); + } + return 0; +} // switch (e) { case 1, 2: ... case 'x': ... default: ... } // cases don't fall through: each one ends where the next begins, and break // leaves the switch early. case labels are constants. @@ -540,6 +638,7 @@ int kw_return() { next(); if (!infn) serr("return outside a function"); if (bxdepth) serr("return can't leave a block expression"); + if (dfr_on && dfr_ctx == ctxsp) { serr("return can't be used in a deferred statement"); return resync(); } if (is_op(s_semi)) { lit(0); c = C_PLAIN; if_ret(0, 1); } else { c = expr(); rval(); @@ -560,6 +659,7 @@ int kw_return() { expect(s_semi); retcheck(c); borrow_cleanup(); + defer_run(1, 0); // the value is on the stack; the deferred code is stack neutral exit_vars(1, 0); epichain = fixnew(here, epichain); lit(rt.MW - 1); op(O_JMP); diff --git a/src/lsp/server.zc b/src/lsp/server.zc index afa8de4..3698236 100644 --- a/src/lsp/server.zc +++ b/src/lsp/server.zc @@ -1365,7 +1365,7 @@ word hl_is(int base, int q, int e, char* w) { word hl_kw(int base, int q) { int e = q; while (hid(dc(base, e))) e++; - return hl_is(base, q, e, "return") || hl_is(base, q, e, "if") || hl_is(base, q, e, "while") || hl_is(base, q, e, "else") || hl_is(base, q, e, "break") || hl_is(base, q, e, "continue"); + return hl_is(base, q, e, "return") || hl_is(base, q, e, "if") || hl_is(base, q, e, "while") || hl_is(base, q, e, "else") || hl_is(base, q, e, "break") || hl_is(base, q, e, "continue") || hl_is(base, q, e, "defer"); } // where a declaration's name ends on the line from q: at = ; , { ( or a [ that // follows a name (let b[24]); the [ of a type (fixed int[?]) is passed over @@ -1691,6 +1691,7 @@ char* kwtext() { if (hname_is("switch")) return "switch (e) { case 1, 2: ... default: ... }~a branch on constant labels. the cases don't fall through, and break leaves early."; if (hname_is("break")) return "break;~leaves the innermost loop or switch."; if (hname_is("continue")) return "continue;~goes on to the next pass of the innermost loop."; + if (hname_is("defer")) return "defer e; or defer { }~runs the statement when the enclosing block ends, on every way out of it (falling off the end, return, break, continue). several run last registered first, and what they name is read when they run. return, break and continue are not allowed inside. defer rt.free(p); is the transfer of p for the ownership analysis."; if (hname_is("return")) return "return e; or return;~leaves the function with a value, or leaves a void function. returning an owned local transfers it to the caller automatically."; if (hname_is("enum")) return "enum name { A, B = 5 };~global constants. under typecheck on they are a type of their own, and a member can carry fields to make a constructor."; if (hname_is("struct")) return "struct name { fields };~a record type. struct name<T> { } makes it generic, one copy for each set of type arguments."; diff --git a/tests/errors/defer_freed_twice.zc b/tests/errors/defer_freed_twice.zc new file mode 100644 index 0000000..d4fe08d --- /dev/null +++ b/tests/errors/defer_freed_twice.zc @@ -0,0 +1,10 @@ +namespace; +include rt; + +// expect: p: used after it was given away +fn main() { + int* p = rt.new(int); + defer rt.free(p); + rt.free(p); + return 0; +} diff --git a/tests/errors/defer_moved.zc b/tests/errors/defer_moved.zc new file mode 100644 index 0000000..d6086cd --- /dev/null +++ b/tests/errors/defer_moved.zc @@ -0,0 +1,11 @@ +namespace; +include rt; + +// expect: p: used after it was given away +fn take(int* .d p) { rt.free(p); return 0; } +fn main() { + int* p = rt.new(int); + defer rt.free(p); + take(p); + return 0; +} diff --git a/tests/errors/defer_moved_early.zc b/tests/errors/defer_moved_early.zc new file mode 100644 index 0000000..2c6feb9 --- /dev/null +++ b/tests/errors/defer_moved_early.zc @@ -0,0 +1,14 @@ +namespace; +include rt; + +// expect: p: used after it was given away +fn take(int* .d p) { rt.free(p); return 0; } +fn main(a) { + int* p = rt.new(int); + defer rt.free(p); + if (a) { + take(p); + return 1; + } + return 0; +} diff --git a/tests/langerrors/defer_break.zc b/tests/langerrors/defer_break.zc new file mode 100644 index 0000000..193f3f0 --- /dev/null +++ b/tests/langerrors/defer_break.zc @@ -0,0 +1,11 @@ +namespace; +include rt; + +// expect: break can't leave a deferred statement +fn main() { + while (1) { + defer { break; } + rt.put_n(1); + } + return 0; +} diff --git a/tests/langerrors/defer_continue.zc b/tests/langerrors/defer_continue.zc new file mode 100644 index 0000000..079b02b --- /dev/null +++ b/tests/langerrors/defer_continue.zc @@ -0,0 +1,11 @@ +namespace; +include rt; + +// expect: continue can't leave a deferred statement +fn main() { + for (let i = 0; i < 2; i++) { + defer { continue; } + rt.put_n(i); + } + return 0; +} diff --git a/tests/langerrors/defer_later_name.zc b/tests/langerrors/defer_later_name.zc new file mode 100644 index 0000000..17f175d --- /dev/null +++ b/tests/langerrors/defer_later_name.zc @@ -0,0 +1,9 @@ +namespace; +include rt; + +// expect: y is used but never defined +fn main() { + defer rt.put_n(y); + int y = 3; + return 0; +} diff --git a/tests/langerrors/defer_return.zc b/tests/langerrors/defer_return.zc new file mode 100644 index 0000000..bceefe0 --- /dev/null +++ b/tests/langerrors/defer_return.zc @@ -0,0 +1,8 @@ +namespace; +include rt; + +// expect: return can't be used in a deferred statement +fn main() { + defer { rt.put_n(1); return 1; } + return 0; +} diff --git a/tests/programs/defer.zc b/tests/programs/defer.zc new file mode 100644 index 0000000..bdf85e1 --- /dev/null +++ b/tests/programs/defer.zc @@ -0,0 +1,235 @@ +namespace; +include rt; + +// defer: code that runs when the enclosing block ends, on every way out of it + +fn pn(x) { rt.put_n(x); rt.put_c(' '); return 0; } +fn nl() { rt.put_c(10); return 0; } + +// several defers run in reverse order of registration +fn order() { + pn(1); + defer pn(30); + defer pn(20); + defer { pn(10); pn(11); } + pn(2); + return 0; +} + +// the deferred code sees the values the variables have when it runs +fn late() { + let x = 1; + defer pn(x); + x = 2; + x = 7; + return 0; +} + +// the value of return is computed before the defers run +fn early(a) { + let x = a; + defer x = x + 100; + defer pn(x); + if (a > 5) return x * 2; + x = x + 1; + return x; +} + +// a defer that is never reached registers nothing +fn skipped(a) { + if (a == 0) return 0 - 1; + defer pn(500); + if (a == 1) return 1; + defer pn(501); + return 2; +} + +// a defer inside an if only applies when that branch ran +fn branch(a) { + defer pn(9); + if (a) { defer pn(1); pn(2); } + else { defer pn(3); pn(4); } + pn(5); + return 0; +} + +// break and continue run the defers of the blocks they leave +fn loops() { + let i = 0; + defer pn(900); + while (i < 5) { + defer pn(100 + i); + i++; + if (i == 2) continue; + if (i == 4) break; + pn(i); + } + pn(777); + for (let j = 0; j < 3; j++) { + defer { pn(200 + j); } + if (j == 1) { defer pn(300); continue; } + pn(j); + } + let k = 0; + do { + k++; + defer pn(400 + k); + if (k == 2) continue; + if (k == 3) break; + } while (k < 10); + return 0; +} + +// the deferred code means what it meant where it was written: a variable an +// inner block declares again does not capture its names +fn lexical(a) { + let x = 1; + defer pn(x); + { + let x = 50; + defer pn(x + 1); + if (a) return x; + pn(x); + } + x = 2; + let y = 3; + return y; +} + +// a defer outside the loop is not run by break or continue +fn outer() { + defer pn(1000); + for (let i = 0; i < 3; i++) { + defer pn(i); + { + defer pn(50 + i); + if (i == 1) continue; + pn(60 + i); + } + if (i == 2) break; + } + pn(2000); + return 0; +} + +// nested blocks, each with its own +fn nest() { + defer pn(1); + { + defer pn(2); + { + defer pn(3); + pn(4); + } + pn(5); + } + pn(6); + return 7; +} + +// switch arms are blocks; break leaves the arm and the arm's defers run +fn arms(v) { + defer pn(0); + switch (v) { + case 1: defer pn(11); pn(10); + case 2: defer pn(21); if (v == 2) break; pn(20); + default: defer pn(31); return 30; + } + pn(99); + return 0; +} + +// a block expression is a block too +fn bexpr() { + let v = { defer pn(1); pn(2); 40 + 2 }; + pn(v); + return 0; +} + +// the owner stays usable until the block ends; the defer is the transfer +fn owner(n) { + let p = rt.new(4); + defer rt.free(p); + p[0] = n; + p[1] = n * 2; + if (n == 3) return p[0] + p[1]; + pn(p[0] + p[1]); + return 0; +} + +fn owner_loop() { + for (let i = 0; i < 3; i++) { + let p = rt.new(2); + defer rt.free(p); + p[0] = i; + if (i == 1) continue; + pn(p[0]); + } + return 0; +} + +// defer in a closure belongs to the closure +fn closure_defer() { + let f = fn(a) { defer pn(a + 1); pn(a); return a * 2; }; + defer pn(0); + pn(f(10)); + return 0; +} + +// unchecked code: no analysis, the deferred code still runs +unchecked fn fn_in() { + defer pn(5); + pn(4); + if (1) return 0; + return 1; +} +fn unchecked_defer() { + unchecked { + defer pn(3); + pn(2); + } + fn_in(); + return 0; +} + +// a deferred block can have its own loops, with break and continue +fn deferred_loop() { + defer { + for (let i = 0; i < 5; i++) { + if (i == 1) continue; + if (i == 4) break; + pn(i); + } + } + pn(9); + return 0; +} + +fn main() { + order(); nl(); + late(); nl(); + pn(early(1)); nl(); + pn(early(9)); nl(); + pn(skipped(0)); nl(); + pn(skipped(1)); nl(); + pn(skipped(2)); nl(); + branch(1); nl(); + branch(0); nl(); + loops(); nl(); + outer(); nl(); + pn(lexical(1)); nl(); + pn(lexical(0)); nl(); + pn(nest()); nl(); + arms(1); nl(); + arms(2); nl(); + arms(3); nl(); + bexpr(); nl(); + let before = rt.live_objects(); + pn(owner(3)); owner(4); nl(); + owner_loop(); nl(); + pn(rt.live_objects() - before); nl(); + closure_defer(); nl(); + unchecked_defer(); nl(); + deferred_loop(); nl(); + return 0; +} diff --git a/tests/test.sh b/tests/test.sh index 298c356..ae404d9 100755 --- a/tests/test.sh +++ b/tests/test.sh @@ -108,6 +108,7 @@ run(){ # $1 = vm command, $2 = label chk "$($vm < tests/programs/gc.zc)" "1000 1 900 1 199900" "$l mark-sweep collector" chk "$($vm < tests/programs/meta.zc | tr '\n' '/')" "1024 19/unless ok/rrr/15 42/7/42/7/144/" "$l parse words, quote, eval, load, operator tables" chk "$($vm < tests/programs/misc.zc | tr '\n' '/')" "45/54321/ok/" "$l compile-time eval, patched mnemonics, unchecked" + chk "$($vm < tests/programs/defer.zc | tr '\n' '/')" "1 2 10 11 20 30 /7 /2 2 /9 18 /-1 /500 1 /501 500 2 /2 1 5 9 /4 3 5 9 /1 101 102 3 103 104 777 0 200 300 201 2 202 401 402 403 900 /60 50 0 51 1 62 52 2 2000 1000 /51 1 50 /50 51 2 3 /4 3 5 2 6 1 7 /10 11 99 0 /21 99 0 /31 0 /2 1 42 /9 12 /0 2 /0 /10 11 20 0 /2 3 4 5 /9 0 2 3 /" "$l defer: block end, return, break, continue, ownership" chk "$($vm < tests/programs/types.zc | tr '\n' '/')" "12 5 2 15 56 42 7 14 3/rect rect* list<int>* list<T>*.u(int) T(list<T>*) fn(word.d)/a:point@0 b:point@2 tag:int@4 /point 1 1/" "$l types, generics, reflection" chk "$($vm < tests/programs/typedef.zc | tr '\n' '/')" "7 1 42 5 30/13 4 19 4/4 3 3 12/5 11/hihihihellook/8/" "$l typedef, global and local, in every place a type goes, and string" chk "$($vm < tests/programs/typeok.zc)" "14" "$l a program the typechecker accepts" |
