diff options
| -rw-r--r-- | README.md | 2 | ||||
| -rw-r--r-- | TODO.md | 4 | ||||
| -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, 514 insertions, 7 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. @@ -294,7 +294,6 @@ worktree is uploaded to the owner after each update. - runtime line tables and backtraces, see "runtime source positions". - bounds checking with `unbounded`, see "bounds checking". - the cross-reference report, see "a cross-reference report". -- the `defer` statement, see "`defer` statement". ### decisions - no structs in zc0 (owner's call): zc0 stays the small trusted root of the @@ -317,7 +316,6 @@ brief someone without re-reading the code. - [ ] group the compiler's parallel per-local arrays (`lname`, `ltype`, `lstate`, `lhome`, `lown`, `lslot`, `ltag`, `lpos`, `lnsz`, `lleaf`, ...) without structs: declare each family together in one place, with one comment block stating each array's meaning and invariant (for example when `lhome` is meaningful), route snapshot and restore of the family through one pair of functions, and use the cross-reference report to confirm nothing else writes them. then symbols and types the same way. fixpoint and analyzer behavior unchanged. - [ ] 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. -- [ ] (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`). - [ ] 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. @@ -327,6 +325,7 @@ brief someone without re-reading the code. - [ ] 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). - [ ] x86-64 jit phase 2: compile o3 regions to x86-64 (port `rcompile` and the region register allocation; regions now run on the region interpreter and are 2 to 4 times slower than their plain versions on this jit) and inline enhancements (lower the optimized micro-ops to x86 registers as the arm64 path does, instead of calling `xrun`). validate with `tests/drivers/jit_x64_test.sh` and `tests/bench/bench.sh`, which now has an x86-64 jit column. add the driver to `tests/test.sh` on x86-64 linux. - [ ] `memdebug`'s `obj_base` searches only 64 words back for a block header, so under `memdebug on` an interior access more than 64 words into a block (`buf[255]` on a 256-word block) reports "bad heap pointer". find the header from the buddy allocator's size classes instead of a bounded scan, with a regression program. +- [ ] `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`). - [ ] build the llvm core by default when `llvm-config` is available (the makefile falls back to the interpreter otherwise), so x86 hosts get a native tier and the language server stops being interpreted; keep `ZYNOLLVM=1` and `-DZYINTERP` as the opt-outs. - [ ] 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. @@ -339,6 +338,7 @@ brief someone without re-reading the code. - [ ] run the full suite once everything above is done (`make test JOBS=4`; `make check` is the quick one), fix what it finds, and only then tick this item. ## done in the sorted list +- [x] `defer expr;` and `defer { ... }`: tokens grabbed at the `defer`, recompiled inline at every exit (block end through `block_close`, `return` after its value, `break`/`continue` for entries deeper than the loop), newest first; only defers reached so far register. names are lexical (a local declared after the `defer` is hidden in the replay). a deferred `rt.free(p)` is `p`'s transfer to the analyzer. each entry replays in the analyzer mode it was written in; closures get their own base through `ctx_push`. `return`, `break` and `continue` that would leave a deferred statement are rejected. a failed entry reports once. limit 256 pending per function (`DFR_MAX`). spec 4.14, learn section 34, `tests/programs/defer.zc`, seven reject fixtures. - [x] four small fixes: hover on standard-library members (`hover_embedded_sym` sends std/icon symbols to the library lookup; `bs_hover` renders a member hit), `capture.end`'s terminator (the buffer has `cap + 1` words; latent, since the allocator's power-of-two slack hid it; `tests/programs/capture_long.zc`), bare sibling static calls inside a struct's methods (`mstruct` set for every non-generic struct; the struct's own constant or static beats a top-level name, a local beats both; `tests/programs/struct_static_call.zc`, spec 5.5.5), and `void main()` in run-only images (`run.zc` declares an untyped `fn main();`; spec 5.2.5). - [x] a hand-written x86-64 jit, phase 1 (`ZYJIT_X64` in `src/core/core.c`, default on x86-64 linux; `-DZYINTERP` and `make build/bin/zyi` give the interpreter). r12 memory, r13 data stack, r14 return stack, r15 table, rbx state; stack slots addressed with a virtual offset so r13 moves only at calls and exits; shares the table, coverage map, store hook and invalidation with the arm64 jit. enhancements are called through `xrun`, o3 regions run on the region interpreter (`ZYNOO3=1` runs their vm code through the jit instead). 179 of 179 programs agree with the interpreter (`tests/drivers/jit_x64_test.sh`), the self-compile is byte-identical and about 7x faster (1.5 s against 11 s), and the rejection, learn, icon, actor, lsp and hint suites pass on it. 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 8a6d089..3ad93c8 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 a43f910..372724f 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 a0175c3..22fdf30 100755 --- a/tests/test.sh +++ b/tests/test.sh @@ -109,6 +109,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" |
