aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar zinc <zinc@downsta.ir> 2026-10-15 00:53:25 +0330
committerGravatar zinc <zinc@downsta.ir> 2026-10-15 00:53:25 +0330
commit0e333abab5d684be37043196b4c006b71bdf850e (patch)
treed7e6664befd82268d2681c817542b3592bab379d
parentbench: sieve.zc and trees.zc take namespace and include rt, so run.py's check... (diff)
parenttodo: destructors item closed with what landed; what is left (reassignment fr... (diff)
merge work/dtor: destructors (drop on any heap value, heap(), struct drop, collector drops once with resurrection, freed blocks forget their drop)
-rw-r--r--TODO.md3
-rw-r--r--docs/SPEC.md16
-rw-r--r--docs/design/destructors.md182
-rw-r--r--src/compiler/analyzer.zc5
-rw-r--r--src/compiler/base.zc5
-rw-r--r--src/compiler/builtins.zc93
-rw-r--r--src/compiler/expr.zc18
-rw-r--r--src/compiler/stmt.zc10
-rw-r--r--src/runtime/rt.zc196
-rw-r--r--tests/actor/reject/drop_message.zc13
-rw-r--r--tests/actor/reject/drop_value.zc12
-rw-r--r--tests/conformance/6-ownership/drop_ends_in_o3_rejected.zc9
-rw-r--r--tests/conformance/6-ownership/drop_forms.zc14
-rw-r--r--tests/conformance/6-ownership/drop_free_in_o3_rejected.zc10
-rw-r--r--tests/conformance/6-ownership/drop_gc_once.zc17
-rw-r--r--tests/conformance/6-ownership/drop_gc_resurrect.zc21
-rw-r--r--tests/conformance/6-ownership/drop_heap_values.zc15
-rw-r--r--tests/conformance/6-ownership/drop_in_o3_rejected.zc9
-rw-r--r--tests/conformance/6-ownership/drop_parent_before_child.zc17
-rw-r--r--tests/conformance/6-ownership/drop_reused_address.zc22
-rw-r--r--tests/conformance/6-ownership/drop_set_late.zc10
-rw-r--r--tests/conformance/6-ownership/drop_set_twice.zc10
-rw-r--r--tests/conformance/6-ownership/drop_temporary_statement.zc13
-rw-r--r--tests/conformance/6-ownership/drop_value_and_type.zc11
-rw-r--r--tests/conformance/8-runtime/o3_no_gc_inside.zc39
-rw-r--r--tests/data/drop_hint.zc2
-rw-r--r--tests/drivers/actor_test.py2
-rwxr-xr-xtests/test.sh3
-rw-r--r--tests/xref.baseline96
29 files changed, 841 insertions, 32 deletions
diff --git a/TODO.md b/TODO.md
index 27799dc..b3108e2 100644
--- a/TODO.md
+++ b/TODO.md
@@ -515,7 +515,8 @@ brief someone without re-reading the code.
- [ ] (partly done, see "agent work at the end of the session") 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`.
- [x] (done on work/bounds, see "agent work at the end of the session") 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.
-- [ ] (partly done, see "agent work at the end of the session") struct destructors: a struct may declare `fn drop()`, called wherever the compiler frees an owned value of that type, before the memory is released. architecture: the compiler frees owned locals in `freevar` (analyzer part), called from `exit_vars`, reassignment and explicit `rt.free`/`rt.recycle` calls (`s_free`, `s_recycle` handling in the call code); when the local's static type is a pointer to a struct with a `Name.drop` method, emit a call to it first. objects freed by the collector or inside unchecked code do not run `drop`: document that. first users: file descriptors in `std.Host`, processes in `actor.zc`.
+- [x] (done on work/dtor, docs/design/destructors.md, spec 3.11.6, 4.10.3-4, 6.15) destructors: drop works on any heap value, set once per value with `drop(v, fn(){...this...})`, `v.drop(f)` or `v.drop = f`, and a struct may declare `fn drop()` as its type default (both run: the value's, then the type's). `heap(v)` boxes an int, a string or a struct into a fresh owned block. drops run where the compiler frees an owned value (block end, early exit, explicit `rt.free`/`rt.recycle`, a temporary at statement end with a hint), parent before children, and the collector runs an unreachable value's drop once after the sweep, resurrection included; a freed block forgets its drop, so a reused address runs no old drop. no drops set or ended inside o3, and no collection inside an o3 region (pending until the outermost exit). copying a value with drop into an actor message is an error. tests: `tests/conformance/6-ownership/drop_*.zc`, `tests/actor/reject/drop_*.zc`, `tests/data/drop_hint.zc`.
+- [ ] destructors, what is left (docs/design/destructors.md, "not yet"): reassigning an owner that still holds a value frees it with a hint (today still error 6.8.5); the no-move-out-of-a-droppable rule; the spawn hint; memreport's `(drop)` marker; `break`/`continue` out of an `o3 { }` block skip `o3_leave`; o3 methods called from normal code through a method call; a cycle of unreachable droppable values is never finalized. first users: file descriptors in `std.Host`, processes in `actor.zc`.
- [x] (done on work/line-tables, see "agent work at the end of the session") runtime source positions. architecture: at each statement start (`stmt()`) record (code address `here`, line); per function a permanent data block [entry, end, name, file, count, pairs...] linked into a runtime list (`rt.line_blocks`), or appended to run-only images by `build_image` like the symbol blob. `lines on;`/`lines off;` like `docs on;`; on for user programs, off for the genome unless the growth is small. a new syscall op returns the return-stack pointer (the return stack is in vm memory, 0x1A4100..0x1B40FF, growing up); implement it in `src/core/core.c`, `src/micro/micro.c`, `src/js/tinyvm.ts` and the wasm port. `rt.trace()` walks the return stack, printing ` at file:line in function` for words inside a known function range. the runtime's fatal reports (`oob`, bad heap pointer, double free, out of memory, frame overflow) print the trace after their unchanged first line. check mode skips all of it.
- [ ] 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).
diff --git a/docs/SPEC.md b/docs/SPEC.md
index aad7ff6..7177b14 100644
--- a/docs/SPEC.md
+++ b/docs/SPEC.md
@@ -467,6 +467,8 @@ pn((int)(p + 1) - (int)p); // 1: p + 1 moves one word
3.11.5 `new(T)` without `rt.` is accepted as `rt.new(T)`; the name `new` alone is the only rt name that resolves unqualified (any other, `put_c(65)`, is error: put_c is used but never defined; did you mean to use rt.put_c?).
+3.11.6 `heap(e)` copies the value of `e` into a fresh owned heap block sized to it and returns a pointer to the block (a fresh object, 6.1): one word for a scalar or pointer (`heap(10)` is an `int*`), the characters and the terminating 0 for a `char*` word string (`heap("hi")` is a `char*`), and `sizeof(T)` words for a struct or array value (`heap(v)` with `v` a `foo` is a `foo*`). pointer fields of a copied struct are copied as plain words. a struct whose body declares `drop` gives the block its type drop (6.15). `heap` is found only when no variable or function of that name is in scope.
+
### 3.12 `sizeof` and `typeof`
3.12.1 `sizeof(T)` and `sizeof(e)` give the size in words. the expression form is compiled but never run: `int i = 0; int n = sizeof(i++);` leaves `i` 0 and `n` 1. a `u8[100]` is 13 words, a struct of four `u8` fields is 1.
@@ -622,6 +624,9 @@ switch (s) { case circle(r): pn(r); case rect(w, h): pn(w * h); case dot: ps("do
o3 int sum(fixed int[?] a) { int s = 0; for (int i = 0; i < len(a); i++) s += a[i]; return s; }
```
+4.10.3 no collection runs inside an o3 region, from any code path. an allocation that would collect grows the heap instead and leaves a collection pending; an explicit `rt.gc()` inside the region is pending too. when the outermost region is left (the end of an `o3 { }` block, a `return` out of it, or the return of an o3 function called from normal code), the pending collection runs once. if the heap can't grow, the allocation is a runtime trap (out of heap memory, exit status 2).
+4.10.4 drops (6.15) don't run inside o3: setting a drop inside an o3 region is error: a drop can't be set inside an o3 region, and ending the lifetime of a value with a drop inside an o3 region, by `rt.free`, at a scope end, or as a temporary, is error: x: has drop and can't end inside an o3 region. a drop the compiler can't see (unchecked code) that comes to run inside o3 stops the program: zy: a drop can't run inside an o3 region.
+
### 4.11 memdebug
`memdebug on;` turns on heap header checks and allocation-site tracking from this point in the file (top level) or from this point in the function (statement); the runtime enables the checks before `main` for the top-level form. `memdebug scrub;` additionally fills freed payloads with 57005; `memdebug off;` stops. with checks on, a use after free prints `zy: use after free from line N`, a double free prints `zy: double free from line N` and returns, a stray address prints `zy: bad heap pointer`. `rt.memreport()` prints a line `leak: line N W words` per live recorded object (W is the n words new asked for, not the block's payload) and returns the count.
@@ -982,6 +987,17 @@ the `tests/errors/*.zc` files are exact reproductions; a verifier can run `bash
the compiler also prints non-error findings as `zc: line N: warning: ...` (a switch that does not cover a tagged enum, a constant index out of range of a `T[?]`, a use after free provable at compile time, ...). warnings do not stop the program.
+### 6.15 drops
+
+6.15.1 any heap value can have a drop: a `fn()` that runs once, when the value dies. three forms set it: `drop(v, f)` and `v.drop(f)` give `v` back (same type and ownership), and `v.drop = f;` is a statement. inside `f`, `this` is the value, borrowed. a drop is set once (error: x: drop is already set) and before the value is first read, assigned through or passed (error: x: drop set after the value was used). a struct body may declare `void drop() { ... }`: it is the default drop of every value of that type made by `new(T)` or `heap`; when a value has both, its own drop runs first, then the type's.
+```
+rt.put_n(*heap(15).drop(fn() { rt.put_s(" bye "); rt.put_n(*this); })); // 15 bye 15
+let guard = drop(heap(0), fn() { unlock(); }); // unlocks at block end
+```
+6.15.2 a value dies, and its drop runs, where 6.5 frees it: at the end of its owner's block or an early exit, at `rt.free`, or with its container, whose drop runs before its children's. a value with a drop that nothing holds (a temporary) dies at the end of its full statement, with hint: value with drop is never bound; it drops at the end of this statement. an owned local with a drop is not "never used" (6.5.2): holding it is its use.
+6.15.3 the collector runs the drop of an unreachable value after its sweep, never during it, and at most once: the value is kept until a later collection finds it unreachable again. a drop that stores `this` somewhere reachable keeps the value alive, and its drop does not run again. there is no collection at exit, so collector-owned values are not dropped then. a freed block forgets its drop, so a block reused at the same address has none.
+6.15.4 copying a value with a drop into an actor message is error: x: has drop and can't be copied into a message.
+
## 7. compile-time execution
the compiler lives in vm memory with the program. functions and globals of the compiler are callable through the module `run` (`include run;`).
diff --git a/docs/design/destructors.md b/docs/design/destructors.md
new file mode 100644
index 0000000..0691228
--- /dev/null
+++ b/docs/design/destructors.md
@@ -0,0 +1,182 @@
+# destructors on heap values (draft)
+
+status: implemented on work/dtor (spec 3.11.6, 4.10.3, 4.10.4, 6.15); see "what is implemented" at the end. revised to acorn's rulings on the nine questions of the first draft. it parks `return defer x;` and covers that use case with owned results and drops on values instead. section numbers refer to `docs/SPEC.md` at `d728a06`.
+
+## the problem
+
+an owned heap value can stand for something outside the heap: a file descriptor, a socket, a lock, a native buffer, a registration in a table. today the analyzer frees the memory at scope end (6.5.1, `rt.recycle`), and the collector reclaims what nobody owns (6.9.3). neither one closes the outside resource. the caller has to remember a `defer close(x);`, and a function that opens something can't hand the cleanup up to its caller. a drop fills that gap: the cleanup is attached to the value, so it follows the value wherever ownership goes.
+
+## 1. heap(v): boxing any value
+
+`heap(v)` is a new builtin. it copies any value into a fresh owned heap slot sized to it and returns a pointer to that slot.
+
+```zc
+int*.u a = heap(10); // one word
+char*.u s = heap("hi"); // string block, len 2
+foo*.u f = heap((struct foo) { .bar = 4, .baz = p }); // struct block
+```
+
+- **size.** the slot is sized to the value: one word for a scalar (`int`, `word`, pointer, fn), `len + 1` bytes rounded up for a string (the bytes are copied, `len(s)` works on the result), and `sizeof(T)` for a struct literal or struct value. the size is fixed at the call; "dynamic" means it follows the argument, not that the block grows.
+- **ownership.** the result is fresh with tag `.u` (6.1), like `rt.new`. bind it to an `m` local and it is freed at block end; return it and the caller owns it. pointer fields inside a boxed struct are copied as plain words: `heap` doesn't take ownership of what they point to.
+- **type.** `heap(e)` where `e : T` has type `T*.u`. a string literal gives `char*.u`. the block carries `T`'s type record, so typed frees, the collector walk and `drop` (below) work on it.
+- `heap` of an already-owned pointer boxes the pointer word, not the object it points to. it is not a move.
+
+## 2. drop: attaching cleanup to a value
+
+any heap value can have a drop, not only structs: `heap` slots, `rt.new` blocks (typed or raw), strings, arrays. a drop is a `fn()` with no parameters and no result. inside it, `this` is the value (borrowed, `.m`).
+
+three forms, all setting the same thing:
+
+```zc
+let g = drop(heap(0), fn() { unlock(); }); // call form, returns the value
+let x = new(int).drop(fn() { std.put_s("one two buckle my shoe"); }); // method form, returns the value
+let y = new(int); y.drop = fn() { std.put_n(*this); }; // statement form
+```
+
+- `drop(v, f)` and `v.drop(f)` return `v` itself, with `v`'s type and tag, so they chain and can be passed along. `v.drop = f;` is a statement and has no value.
+- **set once.** a value's drop can be set only once (error: x: drop is already set).
+- **set early.** the drop must be set before the value is first read, reassigned or passed anywhere (error: x: drop set after the value was used). this is checked statically by the analyzer on the owning local; in practice the drop is set right at the allocation or on the next statement. this rule keeps every reader of the value seeing the same cleanup and means no copy of the pointer can escape without it.
+- **type default.** a struct body may still declare a `drop` method. it is the default drop for every value of that type:
+
+ ```zc
+ struct file {
+ int fd;
+ void drop() { os.close(this.fd); }
+ }
+ ```
+
+ a value of such a type may still set its own drop. then both run: the value's drop first, then the type's. set-once applies to the per-value drop only.
+- `this` can be read and passed to borrowing calls, but not freed. storing it somewhere reachable revives the value (section 6).
+- a drop is never called by hand. an early close is `rt.free(x)`, which runs it.
+
+example (this is the spec example):
+
+```zc
+std.put_n(*heap(15).drop(fn() { std.put_s("i'm free as a bird! "); std.put_n(*this); }));
+```
+
+prints `15`, then `i'm free as a bird! 15`: the temporary is dropped at the end of the statement, after `put_n` returns. note the `*`: `drop` returns the pointer, so without it `put_n` would print an address.
+
+## 3. when it runs
+
+a drop runs exactly once, when the value dies:
+- an `m` local reaches the end of its block or an early exit (`return`, `break`, `continue`), in the same reverse declaration order as today's inserted frees (6.5.1).
+- an explicit `rt.free(x)` or `rt.recycle(x)`.
+- **temporaries.** a value nothing holds (a temporary in a hidden frame slot, 6.5.4) dies at the end of its full statement. so
+
+ ```zc
+ heap(0).drop = fn() { unlock(); }; // unlocks right away: nothing holds it
+ ```
+
+ unlocks immediately. the scope guard idiom binds it:
+
+ ```zc
+ let guard = drop(heap(0), fn() { unlock(); }); // unlocks at block end
+ ```
+
+ the analyzer gives a hint when a value with a drop is never bound (hint: value with drop is never bound; it drops at the end of this statement).
+- a container is freed and its children go with it (6.5.3, 6.8.6). a `T[?]` of droppable elements drops them in index order. `pop` hands the element out fresh, so the container no longer drops it.
+- the owner is reassigned (section 4).
+- the collector reclaims an unowned value (section 6).
+
+**order: parent before children.** a child owned by a field (`own`, `w[-2] == p`) is dropped after its parent's drop returns, in field order. this matches `release`/`recycle`, which already walk the children after the parent is marked going, so the parent's drop still sees live children.
+
+## 4. ownership and moves
+
+the analyzer already proves that an owner is freed once: giving a value away (`.d` parameter, store into a global or container, `return`) makes the local dead (6.9.2), and the dead local isn't freed. drops ride on that same path, so they inherit the guarantee. no runtime flag is needed for checked code.
+
+- **no moving children out.** moving a child out of a value that has a drop (`word c = p.child;` used as an owner, or giving `p.child` away) is an error (error: p.child: can't move out of a value with drop). the parent's drop may depend on that child, and moving it out would leave the drop with a dangling or missing field. there is no `take(p.field)`. borrowing a child (`.m`) is fine.
+- **conditional moves.** 6.10.1 already handles a value given away on one path: the compiler frees it on the other paths on their way into the join. the drop runs at that inserted free. a loop can't give a value away on a path that comes back around (6.10.2), so there is no "maybe moved" state at runtime.
+- **reassigning an owner** (changes 6.8.5). overwriting a still-owning variable is no longer an error: the old value is freed, running its drop, before the new value is stored. the analyzer gives a hint (hint: x: reassigning frees the old value). `x = x.next` style self-reference is evaluated first: the right side is computed, then the old value is freed, then stored.
+- **aliases and borrows** never drop. only the owner's death does.
+
+## 5. returning an owned value
+
+a function that returns a `.u` result gives the caller the value, and with it the drop:
+
+```zc
+file*.u open_log() { file*.u f = rt.new(file); f.fd = os.open("log"); return f; }
+
+fn main() {
+ file*.m log = open_log();
+ write(log, "hi");
+} // log's drop closes the fd here
+```
+
+this covers what `return defer x;` was for. `return f();` hands the value up because a call to an owned-result function is fresh (6.1). a cleanup with no object behind it uses a guard value:
+
+```zc
+int*.u locked() { lock(); return drop(heap(0), fn() { unlock(); }); }
+
+fn main() {
+ let g = locked();
+ ...
+} // unlocks here
+```
+
+so no extra feature is needed. `return defer` stays parked.
+
+## 6. the collector
+
+values reachable only through globals, plain pointers, or unchecked code belong to the collector (6.9.3). the collector **does** run their drops, but only when a sweep happens to reclaim them, so the drop runs late. rule: **a resource that must be closed on time has to be owned by a checked local or by an owned chain from one.** the docs say so plainly, and memdebug can warn (section 8).
+
+- **after the sweep.** the sweep itself never runs user code. `gc()` puts unmarked live values that have a drop onto a finalize list instead of calling `bfree`; those blocks keep their state and stay out of the free lists. after `gc_active` is cleared, the runtime runs each drop, then frees the block. running user code mid-sweep would let it call `new` (reusing a block the sweep hasn't reached) or store pointers into blocks that are about to go.
+- **at most once.** a header bit `F_DROPPED` is set just before the drop is called. a value with `F_DROPPED` set never runs its drop again, on any path (collector, `rt.free`, a double free without memdebug).
+- **resurrection.** a drop that stores `this` (or a child) somewhere reachable, a global or a live container, keeps the value alive: after the drop returns, the runtime checks whether the block was reached again and, if so, does not free it. the value is then an ordinary live value with `F_DROPPED` set: it is usable, it is reclaimed by a later sweep or free once unreachable again, but its drop does not run a second time. under memdebug, finding a dropped value alive at the next sweep prints `zy: value revived by drop from line N`. the simplest implementation: the finalize list is itself a root for the next mark; a finalized block is only freed by the following sweep if it is still unmarked.
+- **order.** within one sweep, finalize order is the heap address order of the sweep walk, so it is unspecified. a parent and its owned child are both unmarked; the parent's drop runs first and its children follow through its release, keeping parent-before-children. two unrelated unreachable values have no defined order, and a drop must not assume the other is still open.
+- **shared values** (`F_SHARE`) have no single owner. `free` and `release` skip them today. their drop runs only from the collector.
+- **no final sweep at exit.** at a normal exit, collector-owned values are left to the os; their drops don't run.
+
+## 7. interactions
+
+### defer
+at a scope end, the order is: the block's defers (newest first, 4.14.2), then the inserted frees of the block's `m` locals in reverse declaration order, each running its drop. a deferred statement sees its locals still alive (4.14.7). an explicit `defer rt.free(x);` runs the drop at the defer's point, and the local isn't freed again.
+
+### o3 regions
+o3 changes speed, not meaning (4.10.1), so drop calls stay. an inserted free of a value that may have a drop is a call out of the region, which costs inlining in the llvm/x86 tiers; acceptable, since droppable values are rarely in hot loops. the optimiser must not reorder a drop past another side effect.
+
+### unchecked and unbounded
+unchecked code has no inserted frees (6.12). what it allocates belongs to the collector or to an explicit `rt.free`, which still runs the drop (the runtime does it, not the analyzer). the set-once and set-early rules can't be checked in unchecked code; there the runtime refuses a second set (trap) and the set-early rule is the programmer's job. unbounded affects only index checks.
+
+### traps and exit
+- a runtime trap (exit 2) does not unwind. no defers and no drops run. resources are left to the os.
+- a normal exit (main returns, or `rt.exit`): main's scope-end frees run their drops as usual. collector-owned values are not finalized (no final sweep).
+- a trap inside a drop is a trap like any other.
+
+### recycle, quarantine, memdebug
+- `free` → `release` runs the drop before the quarantine, while the value is still fully valid (header live, `len` and indexing work), since `release` clears `n` when it quarantines the block.
+- `recycle` and `recycle_leaf`: run the drop before the child walk. a value that may have a drop doesn't take the `recycle_leaf` fast path.
+- double free under memdebug is caught by `quar(p)` before the drop. without memdebug, `F_DROPPED` stops a second run.
+- `memdebug scrub` fills the payload after the drop, never before.
+- `rt.memreport()` marks leaked values that have a drop (`leak: line N W words (drop)`), since those are leaked resources, not just leaked words.
+
+### raw blocks
+untyped `rt.new(n)` blocks take a drop like any other value, so there is no separate `rt.on_free` hook.
+
+### actors
+- **messages.** copying a value with a drop into an actor message is a hard error (error: x: has drop and can't be copied into a message). a copied fd handle is just an integer, and two drops would close or commit twice. sending it by move, when actors gain move sends, transfers the duty to the receiver and makes the sender's local dead.
+- `spawn` snapshots the whole vm into a worker process, so both processes hold and drop their copies. document that a droppable value must not be live across `spawn` unless its drop is safe to run once per process; the analyzer gives a hint on a live droppable local at a `spawn` call (4.12).
+- a worker exiting normally runs its main-level drops, like any program.
+
+## 8. cost
+
+- **where the drop lives.** a type default lives in the type record (`p[-1]`, one new field: drop address or 0). a per-value drop lives in a runtime side table keyed by block address (open hash, address → fn), and the block gets the header flag `F_HASDROP` so the free paths only look in the table when the flag is set. values without a drop pay nothing in size.
+- **header bits.** `p[-4]` holds the state in bits 0..2 (values 1..4), `F_SHARE` = 8 (bit 3), `F_INTERNAL` = 512 (bit 9), `F_STACK` = 1024 (bit 10), all in the low 16 flag bits (`F_LOW`); the size class is in bits 16..20 and the requested size n in bits 32..62. free flag bits: 4..8 (16..256) and 11..15 (2048..32768). chosen: **`F_HASDROP = 16` (bit 4)** and **`F_DROPPED = 32` (bit 5)**, next to `F_SHARE` in `rt.zc`.
+- **fast paths stay correct for free.** `recycle_leaf` already sends a block to the general `recycle` unless `(f & F_LOW) == 1`, so a block with `F_HASDROP` set can't take the leaf fast path even if the compiler guessed wrong. the collector's sweep tests `(b[1] & 7) == 1` for an unmarked live block; it adds `F_HASDROP` (or a type default) as the test for the finalize list.
+- **values without drop pay one test.** the runtime's free paths (unchecked `rt.free`, children found by the walk, the collector) add `if (hdr & F_HASDROP || (t = p[-1]) > 0 && t[DROP]) ...`, cheap next to `scan_ptr`. where the analyzer can see that a local never had a drop set and its type has no default, it emits today's free code, including `recycle_leaf`.
+- **moving blocks.** the heap never moves a live block on its own: it is a buddy allocator in `rt.zc` (`balloc`/`bfree`), the collector marks and sweeps in place with no compaction, and `core.c` only offers word fill/copy primitives (syscall cases 18 and 19) with no relocation. the one place a payload moves is a reallocation: `dgrow` (behind `push`, `dresize`, byte arrays) allocates a bigger items block, copies, and frees the old one. any such resize must move the side-table entry and the `F_HASDROP` bit to the new block before freeing the old one, and that free must not run the drop. a future `rt.realloc` follows the same rule.
+- **clearing on free.** when `release` or `recycle` frees a block (and `recycle_leaf` falls back to `recycle` for a block with the bit), its side-table entry is removed and `F_HASDROP` and `F_DROPPED` are cleared right after its drop returns, before the walk over its children (`dropfree`: `dropnow`, then `dclear`). the block is already past its drop, so nothing needs the entry while the children go. so a reused address never inherits a drop from the block that was there before.
+- **the collector.** one extra test per unmarked block in the sweep, a finalize list that grows like the mark stack (`reserve_mark`), and one extra mark root.
+- **genome.** the parser adds `heap`, the three drop forms, and the type-default method. the analyzer adds set-once/set-early, the no-move-out rule, the never-bound hint, the reassignment free, and the actor send check. the runtime adds the drop test in three places, the side table, and the finalize list. a rough guess is several hundred words; zc-lines should measure the fixpoint once code exists.
+
+## 9. planned tests
+
+- **reused address inherits no drop.** set a drop on a block, free it (the drop fires once), then allocate a block of the same size, which under memdebug comes back at the same address. check the address matches, then free the new block and check that no drop fires for it and that its header has neither `F_HASDROP` nor `F_DROPPED` set.
+
+## what is implemented
+
+- runtime (`rt.zc`): `dtab` side table ([key, value drop + 1, type drop + 1], open hash, `F_STACK` so it is kept but not scanned), `set_drop`, `set_tdrop`, `dropnow`, `dropfree`, `dclear`, `dmove` (used by `dgrow`); `F_HASDROP` 16, `F_DROPPED` 32 and `F_FINAL` 64 (collector found it unreachable). `release` and `recycle` take one `F_HASDROP` test and call `dropfree`, which runs the drop and then clears the entry, before they walk the children; `recycle_leaf` already falls back to `recycle` for a block with the bit. `gc` is `gc_sweep` (mark, `gc_drops` between mark and sweep, sweep) then `gc_finalize`, which runs the drops outside the collector's frame.
+- `gc_drops`: drops of marked values are marked (to a fixpoint); each unmarked value whose drop hasn't run gets `F_FINAL` and is kept with what it reaches; a candidate reached by another candidate waits for a later collection, so parents drop before children.
+- o3: `rt.o3_enter`/`rt.o3_leave` around `o3 { }` blocks and calls from normal code into o3 functions; `gc` inside sets `gc_pending`, the outermost leave runs it.
+- compiler: `heap(e)`, the three drop forms (a closure there gets `this`), the struct `drop` method as type drop (`set_tdrop` after `new(T)` and `heap`), set-once and set-early on locals, temporaries held in a hidden slot until the statement ends with a hint, the actor `send` check, the o3 errors.
+- not yet: reassigning an owner that still holds a value (still error 6.8.5, not the free-with-hint), the no-move-out-of-a-droppable rule, the spawn hint, memreport's `(drop)` marker, `break`/`continue` out of an `o3 { }` block (they skip `o3_leave`), o3 methods called from normal code through a method call, and a cycle of unreachable droppable values (each reaches the other, so neither is finalized).
diff --git a/src/compiler/analyzer.zc b/src/compiler/analyzer.zc
index d1f6f3d..34c0a8b 100644
--- a/src/compiler/analyzer.zc
+++ b/src/compiler/analyzer.zc
@@ -389,6 +389,7 @@ int newloc(int n) {
if (if_local) if_local[i] = 0;
if (!local_origin_mask) local_origin_mask = chk_resize(0, 0, loccap); local_origin_mask[i] = 0;
if (!local_cell) local_cell = chk_resize(0, 0, loccap); local_cell[i] = 0;
+ if (!local_dropset) local_dropset = chk_resize(0, 0, loccap); local_dropset[i] = 0;
if (!local_captured) local_captured = chk_resize(0, 0, loccap); local_captured[i] = 0;
if (!local_ended) local_ended = chk_resize(0, 0, loccap); local_ended[i] = 0;
if (!local_end_info) local_end_info = chk_resize(0, 0, loccap); local_end_info[i] = 0;
@@ -920,6 +921,7 @@ int touch(int i) {
// an expression of class c used as a plain value (a lend, an operand). a new
// object can't be used that way, since nothing would own it.
int plainuse(int c) {
+ if (c == C_FRESH && drop_tail == here && fchk && infn) { drop_temporary(); return C_PLAIN; }
if (c == C_FRESH) cerr("a new object can't be used as a plain value; bind it to a name first");
if (c >= C_VAR) touch(c - C_VAR);
return C_PLAIN;
@@ -933,6 +935,7 @@ int szclass(int n) {
// freeing an owned local. one that holds a typed object with no pointer
// fields goes back through recycle_leaf, which knows its class
int freevar(int i) {
+ if (o3on && fchk && ldroppable(i)) cerrn(i, "has drop and can't end inside an o3 region");
if (local_captured && local_captured[i]) { lit(0); laddr(lslot[i]); op(O_ST); return 0; }
vaddr(i); op(O_LD);
if (lleaf[i] && !o3on && !dry && lookup(s_recycle_leaf) >= 0) {
@@ -966,7 +969,7 @@ int exit_vars(int dep, int final) {
// block ends.
int unused_check(int from) {
while (from < nloc) {
- if (fchk && lown[from] && !lused[from] && !(lrskind[from] == RS_PARAM && rt.eq_w(lname[from], "_"))) serrn(lname[from], "owned object is never used");
+ if (fchk && lown[from] && !lused[from] && !ldroppable(from) && !(lrskind[from] == RS_PARAM && rt.eq_w(lname[from], "_"))) serrn(lname[from], "owned object is never used");
from++;
}
return 0;
diff --git a/src/compiler/base.zc b/src/compiler/base.zc
index b326c80..88708f0 100644
--- a/src/compiler/base.zc
+++ b/src/compiler/base.zc
@@ -255,6 +255,10 @@ int fn_return_type; // a consistent return type inferred from the function body
const int EF_READ = 1, EF_WRITE = 2, EF_RETAIN = 4, EF_CONSUME = 8, EF_UNKNOWN = 16, EF_ALIAS = 32, EF_IDENTITY = 64;
word effect_symbols, effect_edges, effect_requirements, effect_current; int effect_floor, effect_argument_base;
word local_origin, local_origin_mask, local_cell, local_captured, local_ended, local_end_info; word effect_value, argorigin_mask; int capture_enabled; word[1] capture_dummy;
+// drops (docs/design/destructors.md): the code position just past a drop form (a fresh
+// value with a drop still on the stack there), the type `this` gets in the next closure,
+// a local whose drop is set, a mailbox send whose arguments are checked, and o3 block depth.
+int drop_tail = -1, send_check, o3blk; word closure_this, local_dropset;
int borrow_pending, borrow_depth, borrow_jump = -1, borrow_entry; word borrow_slots;
int argorigin = -1, argtemporary, argowner = -1; word argowner_fact;
int stubgen; // 1 while an extern's generated stub is compiled: the stub gets no hints
@@ -748,6 +752,7 @@ int reserve_locals(int need) {
if (local_origin) local_origin = chk_resize(local_origin, nloc, cap);
if (local_origin_mask) local_origin_mask = chk_resize(local_origin_mask, nloc, cap);
if (local_cell) local_cell = chk_resize(local_cell, nloc, cap);
+ if (local_dropset) local_dropset = chk_resize(local_dropset, nloc, cap);
if (local_captured) local_captured = chk_resize(local_captured, nloc, cap);
if (local_ended) local_ended = chk_resize(local_ended, nloc, cap);
if (local_end_info) local_end_info = chk_resize(local_end_info, nloc, cap);
diff --git a/src/compiler/builtins.zc b/src/compiler/builtins.zc
index 5efe379..8c00636 100644
--- a/src/compiler/builtins.zc
+++ b/src/compiler/builtins.zc
@@ -838,6 +838,99 @@ int array_member(int c, int n) {
if (n == intern("pop")) return array_pop(t);
return array_resize(t);
}
+// ---- drops and heap() (docs/design/destructors.md) ----
+
+// a pointer to a struct whose body declares drop: its values drop through the type
+int tdroppable(word t) {
+ word e;
+ if (!t || !isptr(t)) return 0;
+ e = t[4];
+ return e[0] == TY_STRUCT && e[3] >= 0 && methof(e, intern("drop")) >= 0;
+}
+// local i holds a value with a drop: one set on it, or one its type declares
+int ldroppable(int i) { return (local_dropset && local_dropset[i]) || tdroppable(ltype[i]); }
+// entering o3 from normal code: the collector waits until the outermost region is left
+int o3_enter_code() { callname(intern("rt.o3_enter")); op(O_DROP); return 0; }
+// a fresh value with a drop that nothing holds: it is held in a hidden slot and
+// dropped when the full statement ends
+int drop_temporary() {
+ emit_borrow_hold();
+ if (hint_begin(tpack(), HINT_SMALL)) { rt.put_s("value with drop is never bound; it drops at the end of this statement"); dg_close(); }
+ return 0;
+}
+// the value is on the stack. compile the drop (the next expression, usually fn() { ... }
+// with this bound to the value), attach it with rt.set_drop, and leave the value there
+// with the class and facts it had. owner is the local that owns the value, or -1.
+int drop_attach(word vty, int owner) {
+ word t = xty, af = xaf, ev = effect_value; int bor = xbor, st = xstr, c;
+ if (o3on) serr("a drop can't be set inside an o3 region");
+ if (owner >= 0 && fchk && local_dropset) {
+ if (local_dropset[owner]) cerrn(owner, "drop is already set");
+ local_dropset[owner] = 1;
+ }
+ lit(0); op(O_PICK);
+ closure_this = vty; c = expr(); rval(); closure_this = 0;
+ if (c >= C_VAR) touch(c - C_VAR);
+ callname(intern("rt.set_drop")); op(O_DROP);
+ xty = t; xaf = af; effect_value = ev; xbor = bor; xstr = st;
+ lv = 0; lvk = LV_NONE; xzero = 0; xconst = 0; xfloat = 0;
+ return 0;
+}
+// drop(v, fn() { ... }): v, with the drop attached
+int drop_call() {
+ int c, prior = 0, i = 0 - 1;
+ next(); expect(s_lp);
+ if (tk == T_ID) { peek(); if (pk_tk == T_OP && (pk_tv == s_comma)) { i = findloc(tv); if (i >= 0) prior = ltag[i] & LREAD; } }
+ c = expr(); rval();
+ i = 0 - 1;
+ if (c >= C_VAR) i = lroot(c - C_VAR);
+ if (i >= 0 && prior && fchk) cerrn(i, "drop set after the value was used");
+ tcneed(isptr(xty) || xty == tword, "drop needs a heap value", xty);
+ expect(s_comma);
+ drop_attach(xty, i);
+ expect(s_rp);
+ if (c == C_FRESH) drop_tail = here;
+ return c;
+}
+// v.drop(fn() { ... }) gives v back; v.drop = fn() { ... }; is a statement
+int drop_member(int c, word borrowed) {
+ int prior = 0, i = 0 - 1, assign = is_op(s_eq);
+ if (lv && lvk == LV_LOCAL) prior = ltag[lvi] & LREAD;
+ rval();
+ xaf = borrowed;
+ if (c >= C_VAR) { i = lroot(c - C_VAR); touch(c - C_VAR); }
+ if (i >= 0 && prior && fchk) cerrn(i, "drop set after the value was used");
+ next();
+ if (assign && c == C_FRESH && fchk && infn) { drop_temporary(); c = C_PLAIN; }
+ drop_attach(xty, i);
+ if (!assign) expect(s_rp);
+ if (c == C_FRESH) drop_tail = here;
+ if (assign) { xty = tint; xaf = 0; xbor = 0 - 1; return C_PLAIN; }
+ return c;
+}
+// heap(v): v copied into a fresh owned block sized to it
+int heap_box() {
+ int c, place = tpack(), ms; word t;
+ next(); expect(s_lp);
+ c = expr(); rval(); plainuse(c);
+ expect(s_rp);
+ t = xty;
+ stamp_site();
+ if (isagg(t)) {
+ lit(tsz(t)); lit(t); callname(intern("rt.heap_copy"));
+ newsz = tsz(t); xty = tptr(t);
+ if (t[0] == TY_STRUCT) { ms = methof(t, intern("drop")); if (ms >= 0) { fnref(ms); callname(intern("rt.set_tdrop")); } }
+ } else if (isptr(t) && t[4][0] == TY_CHAR) {
+ callname(intern("rt.heap_s")); newsz = -1; xty = t;
+ } else {
+ if (xfloat) xfloat = 0;
+ lit(t); callname(intern("rt.heap_w")); newsz = 1; xty = tptr(t);
+ }
+ newleaf = 0;
+ xaf = af_new(newsz, 2, place);
+ lv = 0; lvk = LV_NONE; xzero = 0; xconst = 0; xstr = 0; xbor = 0 - 1; effect_value = 0;
+ return C_FRESH;
+}
// compile sizeof(T) or sizeof(e): the size of a type or expression in words.
int kw_sizeof() {
int t;
diff --git a/src/compiler/expr.zc b/src/compiler/expr.zc
index 8768658..e6acfc8 100644
--- a/src/compiler/expr.zc
+++ b/src/compiler/expr.zc
@@ -68,6 +68,7 @@ int args(int tags, word ft, int bs, int fat) {
freeing = sname[cs] == s_free || sname[cs] == s_recycle;
harmless = sname[cs] == s_put_s || rt.eq_w(sname[cs], "rt.put_n") || rt.eq_w(sname[cs], "rt.put_c") || rt.eq_w(sname[cs], "len") || rt.eq_w(sname[cs], "cap");
}
+ int sending = send_check; send_check = 0;
xgive = 0; ifcall = 0 - 1; effect_argument_base = 0;
expect(s_lp);
if (!is_op(s_rp)) {
@@ -76,6 +77,10 @@ int args(int tags, word ft, int bs, int fat) {
closure_expected = 0;
if (ft && np + parameter_base < ft[3] && is_id(s_fn)) closure_expected = subst(ft[7 + np + parameter_base], bs);
place = tpack(); c = expr(); rval(); closure_expected = expected;
+ if (sending && fchk && ((c >= C_VAR && ldroppable(lroot(c - C_VAR))) || tdroppable(xty) || (c == C_FRESH && drop_tail == here))) {
+ if (c >= C_VAR) serrn(lname[lroot(c - C_VAR)], "has drop and can't be copied into a message"); else serr("a value with drop can't be copied into a message");
+ }
+ if (freeing && o3on && fchk && c >= C_VAR && ldroppable(lroot(c - C_VAR))) serrn(lname[lroot(c - C_VAR)], "has drop and can't end inside an o3 region");
current = effect_origin(c); current_mask = effect_mask(c); origins = origins | current_mask; effect_args_mask(cs, np + parameter_base, current_mask);
if (!np) origin = current; else if (origin != current) origin = -1;
tag = (tags >> (np + np)) & 3;
@@ -176,6 +181,7 @@ int newtyped() {
expect(s_rp);
stamp_site();
if (fast) callname(s_new_k); else callname(s_new_t); vf_effect();
+ if (t[0] == TY_STRUCT && t[3] >= 0) { int dm = methof(t, intern("drop")); if (dm >= 0) { fnref(dm); callname(intern("rt.set_tdrop")); } }
xaf = af_new(length, 2, place);
lv = 0; lvk = LV_NONE; xty = tptr(t); xzero = 0; xstamp = 1;
return C_FRESH;
@@ -462,6 +468,7 @@ int call(int s) {
}
if (!(sflag[s] & F_DEF) || s == finsym) sflag[s] = sflag[s] | F_EARLY; // called before its body: an owned return is not inferred for it
if (sflag[s] & F_EXT) { lit(sval[s][-1]); op(620); } // an extern: the native call opcode, with the block's address
+ else if (!o3on && (sflag[s] & F_O3)) { o3_enter_code(); fnref(s); op(O_CALL); callname(intern("rt.o3_leave")); op(O_DROP); } // into o3 from normal code: no collection until it returns
else { fnref(s); op(O_CALL); }
if (!o3on && (sflag[s] & F_O3) && ft) { // non-o3 called o3: fatten the result here, not inside the callee
rhs_type = subst(ft[4], bs);
@@ -649,14 +656,18 @@ word closure_layout(int closure_index, int count) {
int closure() {
char* xcf;
int np = 0; char*[24] pn; word[24] pty; int hop, entry, i, t, envl, ncap, c, result, declared = 0, tags = 0, tag, lt, lfl = tline; word[24] ptags;
- word expected = closure_expected; closure_expected = 0;
+ word expected = closure_expected, this_type = closure_this; closure_expected = 0; closure_this = 0;
reserve_closures(nclo + 1);
if (o3on) serr("closures can't be made inside an o3 region");
next();
// fn:type(args) names the result; returns are coerced and checked against it
if (accept(s_colon)) declared = ptype0();
expect(s_lp);
- if (!is_op(s_rp)) {
+ if (this_type) { // a drop: fn() { ... } gets the value as this, borrowed
+ pn[0] = s_this; pty[0] = this_type; ptags[0] = 1; pnoffs[0] = toff; pnplaces[0] = tpack(); np = 1;
+ if (!is_op(s_rp)) serr("a drop takes no parameters; the value is this");
+ }
+ if (!is_op(s_rp) && !this_type) {
while (1) {
pty[np] = tword;
if (expected && expected[0] == TY_FN && np < expected[3]) pty[np] = expected[7 + np];
@@ -1022,6 +1033,7 @@ int primary() {
// wins over a global (a top-level function or constant) of the same name
if (mstruct && !ginst) { ms = lookup(methname(mstruct, n)); if (ms >= 0 && (skind[ms] == K_CONST || (skind[ms] == K_FN && (sflag[ms] & F_STATIC)))) s = ms; }
if (s < 0 && rt.eq_w(n, "new")) s = source_lookup(s_new);
+ if (s < 0 && (rt.eq_w(n, "heap") || rt.eq_w(n, "drop"))) { peek(); if (pk_tk == T_OP && pk_tv == s_lp) { if (rt.eq_w(n, "heap")) return heap_box(); return drop_call(); } }
if (s >= 0) rs_put(toff, RS_SYM, rs_sym(s), s);
if (s >= 0 && skind[s] == K_KW) {
if (!(sflag[s] & F_EXPR)) dien(n, "can't be used inside an expression");
@@ -1185,6 +1197,8 @@ word field(int c) {
}
fieldpos = tpack(); n = name(); fieldoff = nameoff;
st = xty;
+ if (rt.eq_w(n, "drop") && (is_op(s_lp) || is_op(s_eq)) && (isptr(st) || st == tword) && !(isptr(st) && st[4][0] == TY_STRUCT && tfindfield(st[4], n) >= 0)) return drop_member(c, borrowed);
+ if (rt.eq_w(n, "send") && is_op(s_lp) && isptr(st) && st[4][0] == TY_STRUCT && tfindfield(st[4], intern("__mailbox")) >= 0) send_check = 1;
if (st[0] == TY_ARR || st[0] == TY_DYN) {
if (pmatch) probe_member(st);
if (is_op(s_lp) && array_method(n)) { xaf = borrowed; return array_member(c, n); }
diff --git a/src/compiler/stmt.zc b/src/compiler/stmt.zc
index 0182728..6d5abe7 100644
--- a/src/compiler/stmt.zc
+++ b/src/compiler/stmt.zc
@@ -30,7 +30,7 @@ int stmt_inner() {
if (tv == s_semi) { next(); return 0; }
if (tv == s_lb) { block(); return 0; }
}
- c = expr(); rval(); op(O_DROP);
+ c = expr(); rval(); if (c == C_FRESH && drop_tail == here) c = plainuse(c); op(O_DROP);
expect(s_semi);
discard(c);
return 0;
@@ -145,7 +145,7 @@ int localdecl(int typed) {
if (t && t[0] == TY_DYN && is_op(s_lb)) c = array_dynamic_literal(t); else c = expr();
rval(); nomagic = 0; kc = xconst && !xfloat;
if (kc) ltag[i] = ltag[i] | LCONST;
- if (c == C_FRESH) { lnsz[i] = newsz; lleaf[i] = newleaf; }
+ if (c == C_FRESH) { lnsz[i] = newsz; lleaf[i] = newleaf; if (drop_tail == here) local_dropset[i] = 1; }
if (t) fcoerce(t);
vaddr(i);
op(O_ST);
@@ -683,6 +683,7 @@ int kw_return() {
borrow_cleanup();
defer_run(1, 0); // the value is on the stack; the deferred code is stack neutral
exit_vars(1, 0);
+ if (o3blk) { callname(intern("rt.o3_leave")); op(O_DROP); } // a return leaves the o3 block too
epichain = fixnew(here, epichain);
lit(rt.MW - 1); op(O_JMP);
live = 0;
@@ -699,7 +700,9 @@ int kw_o3() {
next();
if (!is_op(s_lb)) { o3one = 1; localdecl(1); o3one = 0; return 0; }
if (o3on || !infn) return block();
+ o3_enter_code();
reserve_code(2); lo = here + 2; lit(lo); op(O_JMP);
+ o3blk++;
o3on = 1; o3reset();
save = nloc; outer = nloc;
i = lbase;
@@ -713,7 +716,7 @@ int kw_o3() {
i++;
}
block();
- o3on = 0;
+ o3on = 0; o3blk--;
if (no3pend >= o3pendcap) { o3pend = chk_resize(o3pend, no3pend * 67, o3pendcap * 2 * 67); o3pendcap = o3pendcap * 2; }
{
p = o3pend + no3pend * 67; p[0] = lo; p[1] = here; p[2] = no3sl;
@@ -733,6 +736,7 @@ int kw_o3() {
}
i++;
}
+ callname(intern("rt.o3_leave")); op(O_DROP);
return 0;
}
// unchecked turns the analyzer off for one statement
diff --git a/src/runtime/rt.zc b/src/runtime/rt.zc
index ad18f85..968be87 100644
--- a/src/runtime/rt.zc
+++ b/src/runtime/rt.zc
@@ -424,6 +424,11 @@ int zshl(int a, int b) { return a * pow2[b & 63]; }
const int HDR = 5;
// p[-4] bit, orthogonal to live / mark / releasing.
const int F_SHARE = 8;
+// p[-4] bits for drops (docs/design/destructors.md): the value has a drop in the side
+// table (dtab), its drop has run (it never runs again), and the collector found it
+// unreachable and runs its drop once the sweep is over.
+const int F_HASDROP = 16, F_DROPPED = 32, F_FINAL = 64;
+int o3_depth, gc_pending;
// flags live in these bits; the size class is above them.
const int F_LOW = 65535;
int hbump; word[32] freel; int gc_runs, gc_freed, gc_auto = 1;
@@ -822,7 +827,9 @@ int typeof_obj(word p) {
int paylen(word p) { return p[-4] >> 32; }
// frees p and, recursively, every object p owns. the block stays quarantined.
int release(word p) {
- word b = p - HDR; int n = paylen(p), i = 0; word w;
+ word b = p - HDR; int n, i = 0; word w;
+ if (b[1] & F_HASDROP) dropfree(p);
+ n = paylen(p);
b[1] = (b[1] & ~7) | 2; // going: not live, and not free yet either, so a child's buddy check can't merge into it
while (i < n) {
i = i + scan_ptr(p + i, n - i);
@@ -863,6 +870,7 @@ unchecked int recycle(.d p) {
}
if (!isobj(p) || (p[-4] & F_SHARE)) return 0;
b = p - HDR;
+ if (b[1] & F_HASDROP) dropfree(p);
n = paylen(p);
b[1] = (b[1] & ~7) | 2;
while (i < n) {
@@ -892,6 +900,111 @@ unchecked word recycle_leaf(.d p, int t, int k) {
freel[k] = b;
return 0;
}
+// ---- drops ----
+
+// a value's drop lives in dtab, an open hash keyed by the payload address:
+// three words an entry, [key, value drop, type drop]; key 0 is empty, 1 a removed
+// entry. the block carries F_HASDROP so the free paths look in the table only then.
+// dtab is F_STACK, so the collector keeps it without scanning it: a key in the
+// table doesn't keep its value alive. gc marks the drops of live values itself.
+word dtab; int dt_cap, dcount, dfill;
+int dhash(word p) { return ((p >> 3) ^ (p >> 11)) & (dt_cap - 1); }
+// the index of p's entry, or -1
+int dfind(word p) {
+ int i;
+ if (!dt_cap) return 0 - 1;
+ i = dhash(p);
+ while (dtab[i * 3]) {
+ if (dtab[i * 3] == p) return i;
+ i = (i + 1) & (dt_cap - 1);
+ }
+ return 0 - 1;
+}
+// make room for one more entry: rehash into a table twice the size when 3/4 full
+int dgrowtab() {
+ word old = dtab; int oc = dt_cap, i = 0, j, auto = gc_auto;
+ if ((dfill + 1) * 4 < dt_cap * 3) return 0;
+ dt_cap = 16; while (dt_cap * 3 <= (dcount + 1) * 8) dt_cap = dt_cap * 2;
+ gc_auto = 0; dtab = new_oz(dt_cap * 3); gc_auto = auto;
+ dtab[-4] = dtab[-4] | F_INTERNAL | F_STACK;
+ dfill = 0;
+ while (i < oc) {
+ if (old[i * 3] > 1) {
+ j = dhash(old[i * 3]);
+ while (dtab[j * 3]) j = (j + 1) & (dt_cap - 1);
+ dtab[j * 3] = old[i * 3]; dtab[j * 3 + 1] = old[i * 3 + 1]; dtab[j * 3 + 2] = old[i * 3 + 2];
+ dfill++;
+ }
+ i++;
+ }
+ if (old) recycle(old);
+ return 0;
+}
+// the entry for p, made when it has none
+int dentry(word p) {
+ int i = dfind(p);
+ if (i >= 0) return i;
+ dgrowtab();
+ i = dhash(p);
+ while (dtab[i * 3] > 1) i = (i + 1) & (dt_cap - 1);
+ if (!dtab[i * 3]) dfill++;
+ dtab[i * 3] = p; dtab[i * 3 + 1] = 0; dtab[i * 3 + 2] = 0;
+ dcount++;
+ p[-4] = p[-4] | F_HASDROP;
+ return i;
+}
+// set the drop of value p to f, and return p. a value has one drop of its own;
+// checked code proves that statically, this traps in unchecked code.
+word set_drop(word p, word f) {
+ int i;
+ if (!isobj(p)) { put_s("zy: drop needs a heap value\n"); trace_at(1); trap(); return p; }
+ i = dentry(p);
+ if (dtab[i * 3 + 1] || (p[-4] & F_DROPPED)) { put_s("zy: drop is already set\n"); trace_at(1); trap(); return p; }
+ dtab[i * 3 + 1] = f + 1; // stored plus one: a function can sit at address 0
+ return p;
+}
+// a value of a type whose struct declares drop: the compiler adds the type's drop at new
+word set_tdrop(word p, word f) {
+ int i;
+ if (!isobj(p)) return p;
+ i = dentry(p);
+ dtab[i * 3 + 2] = f + 1;
+ return p;
+}
+// run p's drops, once: the value's own, then its type's. p is still live, so the drop
+// sees its fields and children. the entry stays until the block is freed.
+int dropnow(word p) {
+ int i; word f, g;
+ if (p[-4] & F_DROPPED) return 0;
+ if (o3_depth) { put_s("zy: a drop can't run inside an o3 region\n"); trace_at(1); trap(); return 0; }
+ p[-4] = p[-4] | F_DROPPED;
+ i = dfind(p);
+ if (i < 0) return 0;
+ f = dtab[i * 3 + 1]; g = dtab[i * 3 + 2];
+ if (f) { f = f - 1; f(p); }
+ if (g) { g = g - 1; g(p); }
+ return 0;
+}
+// the free paths' one test leads here: run the drop while p is live, then forget it
+int dropfree(word p) { dropnow(p); dclear(p); return 0; }
+// p's block is going back: forget its entry and bits, so a reused address has no drop
+int dclear(word p) {
+ int i = dfind(p);
+ if (i >= 0) { dtab[i * 3] = 1; dtab[i * 3 + 1] = 0; dtab[i * 3 + 2] = 0; dcount--; }
+ p[-4] = p[-4] & ~(F_HASDROP | F_DROPPED | F_FINAL);
+ return 0;
+}
+// a resize copied old into new: the entry moves to new, and freeing old drops nothing
+int dmove(word old, word nw) {
+ int i = dfind(old), j; word f, g; int bits = old[-4] & (F_DROPPED);
+ if (i < 0) { old[-4] = old[-4] & ~(F_HASDROP | F_DROPPED); return 0; }
+ f = dtab[i * 3 + 1]; g = dtab[i * 3 + 2];
+ dtab[i * 3] = 1; dtab[i * 3 + 1] = 0; dtab[i * 3 + 2] = 0; dcount--;
+ old[-4] = old[-4] & ~(F_HASDROP | F_DROPPED);
+ j = dentry(nw); dtab[j * 3 + 1] = f; dtab[j * 3 + 2] = g;
+ nw[-4] = nw[-4] | bits;
+ return 0;
+}
// make p the owner of c, so that freeing p also frees c. it returns c, so the call
// composes: h[0] = own(b, h). a shared object has no owner and is left alone.
word own(word c, int p) {
@@ -1002,6 +1115,7 @@ unchecked int dgrow(word h, int need) {
i++;
}
h[2] = own(it, h); h[1] = c;
+ if (old[-4] & F_HASDROP) dmove(old, it);
free(old);
return 0;
}
@@ -1284,12 +1398,79 @@ int gc_frames() {
while (p) { mark(p); markrange(p[0], p[2]); p = p[3]; }
return 0;
}
+// heap(v): a fresh block holding a copy of v. a word, typed t
+word .u heap_w(int v, int t) { word p = new_t(1, t); p[0] = v; return p; }
+// n words copied from src (a struct or array value), typed t
+unchecked word .u heap_copy(word src, int n, int t) { word p = new_t(n, t); copy_w(p, src, n); return p; }
+// a zero-terminated character string
+unchecked char* .u heap_s(char* s) { int n = len_w(s) + 1; char* p = new(n); copy_w(p, s, n); return p; }
+// o3 regions never collect: an allocation that would collect grows the heap instead and
+// leaves gc_pending, and leaving the outermost region runs that one collection
+int o3_enter() { o3_depth++; return 0; }
+int o3_leave() {
+ if (o3_depth > 0) o3_depth--;
+ if (!o3_depth && gc_pending) { gc_pending = 0; gc(); }
+ return 0;
+}
+// the collector's part, between mark and sweep. the drops of live values are kept;
+// an unreachable value with a drop that hasn't run is kept for one more cycle and
+// flagged F_FINAL, unless another such value reaches it (its parent drops first and
+// it waits for a later collection). nothing runs user code here.
+int gc_drops() {
+ int i, again = 1; word k;
+ while (again) {
+ i = 0;
+ while (i < dt_cap) {
+ k = dtab[i * 3];
+ if (k > 1 && (k[-4] & 7) == 3) { mark(dtab[i * 3 + 1] - 1); mark(dtab[i * 3 + 2] - 1); }
+ i++;
+ }
+ again = ms_top; drain();
+ }
+ // candidates: unmarked live values whose drop is still to run. what they reach is
+ // marked, so a candidate inside another candidate isn't finalized this time
+ i = 0;
+ while (i < dt_cap) {
+ k = dtab[i * 3];
+ if (k > 1 && (k[-4] & 7) == 1 && !(k[-4] & F_DROPPED)) {
+ k[-4] = k[-4] | F_FINAL;
+ markrange(k, k + k[-5] - HDR); mark(dtab[i * 3 + 1] - 1); mark(dtab[i * 3 + 2] - 1);
+ drain();
+ }
+ i++;
+ }
+ i = 0;
+ while (i < dt_cap) {
+ k = dtab[i * 3];
+ if (k > 1 && (k[-4] & F_FINAL)) {
+ if ((k[-4] & 7) == 1) mark(k); else k[-4] = k[-4] & ~F_FINAL; // reached from another candidate: later
+ }
+ i++;
+ }
+ drain();
+ return 0;
+}
+// after the sweep: run the drops the collector found. a drop may store its value
+// somewhere reachable (it lives on, and its drop never runs again); otherwise a
+// later collection frees it.
+int gc_finalize() {
+ int i = 0, found = 1; word k;
+ while (found) {
+ found = 0; i = 0;
+ while (i < dt_cap) {
+ k = dtab[i * 3];
+ if (k > 1 && (k[-4] & F_FINAL)) { k[-4] = k[-4] & ~F_FINAL; dropnow(k); found = 1; }
+ i++;
+ }
+ }
+ return 0;
+}
// gc_roots is defined in the compiler half (it identifies the compiler's
// own tables live); it calls markrange over every root range
int run.gc_roots();
// runs a collection: the blocks nothing points at are reclaimed, quarantined blocks
// included, and the number of blocks freed is returned. cycles are reclaimed too.
-unchecked int gc() {
+unchecked int gc_sweep() {
word b = 0; int sz = 0, n = 0; // a leftover pointer here would be a root and pin a block
ms_top = 0; ms_over = 0; gc_active = 1;
run.gc_roots();
@@ -1303,10 +1484,11 @@ unchecked int gc() {
drain();
}
}
+ if (dcount) gc_drops();
b = HEAP_LO;
while (b < hstop) {
sz = b[0];
- if ((b[1] & 7) == 1) { bfree(b); n++; b = fend; } // unmarked live object, shared or not
+ if ((b[1] & 7) == 1) { if (b[1] & F_HASDROP) dclear(b + HDR); bfree(b); n++; b = fend; } // unmarked live object, shared or not
else if ((b[1] & 7) == 3) { b[1] = (b[1] & ~7) | 1; b = b + sz; }
else if ((b[1] & 7) == 4) { b[1] = b[1] & ~7; b = b + sz; } // still pointed at: stays quarantined
else if (!(b[1] & 7) && b[2] == ((b + HDR) ^ MAGIC)) { bfree(b); n++; b = fend; }
@@ -1317,6 +1499,14 @@ unchecked int gc() {
gc_freed = gc_freed + n;
return n;
}
+// a collection, then the drops it found, outside the collector's frame
+unchecked int gc() {
+ int n;
+ if (o3_depth) { gc_pending = 1; return 0; }
+ n = gc_sweep();
+ if (dcount) gc_finalize();
+ return n;
+}
// the number of live heap objects, for tests and leak checks.
int live_objects() {
word b = HEAP_LO; int n = 0;
diff --git a/tests/actor/reject/drop_message.zc b/tests/actor/reject/drop_message.zc
new file mode 100644
index 0000000..fb37d5f
--- /dev/null
+++ b/tests/actor/reject/drop_message.zc
@@ -0,0 +1,13 @@
+// destructors 7: copying a value with a drop into an actor message is an error; two processes would drop it twice
+namespace;
+include rt, actor;
+struct res { int fd; void drop() { rt.put_s("closed"); } }
+actor Keeper { int total; int handle(res* r) { return r.fd; } }
+fn main() {
+ Keeper* k = rt.new(Keeper);
+ res* r = rt.new(res); r.fd = 3;
+ k.spawn();
+ k.send(r);
+ k.close();
+ return 0;
+}
diff --git a/tests/actor/reject/drop_value.zc b/tests/actor/reject/drop_value.zc
new file mode 100644
index 0000000..10e941d
--- /dev/null
+++ b/tests/actor/reject/drop_value.zc
@@ -0,0 +1,12 @@
+// destructors 7: a value whose own drop is set can't be copied into an actor message either
+namespace;
+include rt, actor;
+actor Echo { int handle(int* p) { return *p; } }
+fn main() {
+ Echo* e = rt.new(Echo);
+ int* p = rt.new(int); p.drop = fn() { rt.put_s("dropped"); };
+ e.spawn();
+ e.send(p);
+ e.close();
+ return 0;
+}
diff --git a/tests/conformance/6-ownership/drop_ends_in_o3_rejected.zc b/tests/conformance/6-ownership/drop_ends_in_o3_rejected.zc
new file mode 100644
index 0000000..4f29537
--- /dev/null
+++ b/tests/conformance/6-ownership/drop_ends_in_o3_rejected.zc
@@ -0,0 +1,9 @@
+// expect error: r: has drop and can't end inside an o3 region
+// destructors, o3: a droppable value's lifetime can't end inside an o3 scope
+include rt;
+struct res { int fd; void drop() { rt.put_s("closed"); } }
+fn main() {
+ o3 { res* r = rt.new(res); r.fd = 1; }
+ return 0;
+}
+// zc0: skip
diff --git a/tests/conformance/6-ownership/drop_forms.zc b/tests/conformance/6-ownership/drop_forms.zc
new file mode 100644
index 0000000..e9e4c07
--- /dev/null
+++ b/tests/conformance/6-ownership/drop_forms.zc
@@ -0,0 +1,14 @@
+// expect: 15i'm free as a bird! 15/body y7 x g
+// destructors 2: drop(v, f) and v.drop(f) return v, v.drop = f; is a statement; this is the value. the spec example drops its temporary at the end of the statement
+include rt;
+fn main() {
+ rt.put_n(*heap(15).drop(fn() { rt.put_s("i'm free as a bird! "); rt.put_n(*this); }));
+ rt.put_c(10);
+ let g = drop(heap(1), fn() { rt.put_s("g "); });
+ let x = rt.new(int).drop(fn() { rt.put_s("x "); });
+ let y = rt.new(int); y.drop = fn() { rt.put_s("y"); rt.put_n(*this); rt.put_s(" "); };
+ *y = 7;
+ rt.put_s("body ");
+ return 0;
+}
+// zc0: skip
diff --git a/tests/conformance/6-ownership/drop_free_in_o3_rejected.zc b/tests/conformance/6-ownership/drop_free_in_o3_rejected.zc
new file mode 100644
index 0000000..b318d1f
--- /dev/null
+++ b/tests/conformance/6-ownership/drop_free_in_o3_rejected.zc
@@ -0,0 +1,10 @@
+// expect error: x: has drop and can't end inside an o3 region
+// destructors, o3: an explicit free of a droppable value inside o3 is an error
+include rt;
+fn main() {
+ int* x = rt.new(int);
+ x.drop = fn() { rt.put_s("a"); };
+ o3 { rt.free(x); }
+ return 0;
+}
+// zc0: skip
diff --git a/tests/conformance/6-ownership/drop_gc_once.zc b/tests/conformance/6-ownership/drop_gc_once.zc
new file mode 100644
index 0000000..abe1d95
--- /dev/null
+++ b/tests/conformance/6-ownership/drop_gc_once.zc
@@ -0,0 +1,17 @@
+// expect: 0 drop 7 1 1 1 0/
+// destructors 6: the collector runs an unreachable value's drop after the sweep, exactly once; later collections never run it again
+include rt;
+int n;
+word g;
+fn make() { g = drop(heap(7), fn() { n++; rt.put_s("drop "); rt.put_n(*this); rt.put_c(32); }); return 0; }
+fn main() {
+ make();
+ rt.gc(); rt.put_n(n); rt.put_c(32);
+ g = 0;
+ rt.gc(); rt.put_n(n); rt.put_c(32);
+ rt.gc(); rt.put_n(n); rt.put_c(32);
+ rt.gc(); rt.put_n(n); rt.put_c(32);
+ rt.put_n(rt.dcount); rt.put_c(10); // its entry is gone with the block
+ return 0;
+}
+// zc0: skip
diff --git a/tests/conformance/6-ownership/drop_gc_resurrect.zc b/tests/conformance/6-ownership/drop_gc_resurrect.zc
new file mode 100644
index 0000000..d964eba
--- /dev/null
+++ b/tests/conformance/6-ownership/drop_gc_resurrect.zc
@@ -0,0 +1,21 @@
+// expect: drop 8 alive 8 1 gone 1/
+// destructors 6: a drop that stores this somewhere reachable revives the value; it stays usable, its drop never runs again, and a later collection reclaims it
+include rt;
+int n;
+word g, saved;
+unchecked int keep(word p) { saved = p; return 0; }
+fn make() { g = drop(heap(8), fn() { n++; keep(this); rt.put_s("drop "); rt.put_n(*this); rt.put_c(32); }); return 0; }
+fn main() {
+ make();
+ g = 0;
+ rt.gc();
+ rt.gc();
+ unchecked { word p = saved; rt.put_s("alive "); rt.put_n(p[0]); rt.put_c(32); }
+ rt.put_n(n); rt.put_c(32);
+ saved = 0;
+ rt.gc(); rt.gc();
+ rt.put_s("gone "); rt.put_n(n);
+ rt.put_c(10);
+ return 0;
+}
+// zc0: skip
diff --git a/tests/conformance/6-ownership/drop_heap_values.zc b/tests/conformance/6-ownership/drop_heap_values.zc
new file mode 100644
index 0000000..63d07c9
--- /dev/null
+++ b/tests/conformance/6-ownership/drop_heap_values.zc
@@ -0,0 +1,15 @@
+// expect: 10 hi 2 4 9 /
+// destructors 1: heap(v) boxes an int, a string and a struct into a fresh owned block sized to the value
+include rt;
+struct foo { int bar; int baz; }
+fn main() {
+ int* a = heap(10);
+ char* s = heap("hi");
+ foo v; v.bar = 4; v.baz = 9;
+ foo* f = heap(v);
+ rt.put_n(*a); rt.put_c(32); rt.put_s(s); rt.put_c(32); rt.put_n(rt.len_w(s)); rt.put_c(32);
+ rt.put_n(f.bar); rt.put_c(32); rt.put_n(f.baz); rt.put_c(32);
+ rt.put_c(10);
+ return 0;
+}
+// zc0: skip
diff --git a/tests/conformance/6-ownership/drop_in_o3_rejected.zc b/tests/conformance/6-ownership/drop_in_o3_rejected.zc
new file mode 100644
index 0000000..9095269
--- /dev/null
+++ b/tests/conformance/6-ownership/drop_in_o3_rejected.zc
@@ -0,0 +1,9 @@
+// expect error: a drop can't be set inside an o3 region
+// destructors, o3: setting a drop inside o3 is an error
+include rt;
+fn main() {
+ int* x = rt.new(int);
+ o3 { x.drop = fn() { rt.put_s("a"); }; }
+ return 0;
+}
+// zc0: skip
diff --git a/tests/conformance/6-ownership/drop_parent_before_child.zc b/tests/conformance/6-ownership/drop_parent_before_child.zc
new file mode 100644
index 0000000..9f68c52
--- /dev/null
+++ b/tests/conformance/6-ownership/drop_parent_before_child.zc
@@ -0,0 +1,17 @@
+// expect: parent sees 5 child /
+// destructors 3: a child owned by a field drops after its parent's drop returns, so the parent still sees it
+include rt;
+struct node { int* kid; }
+int* seen;
+fn main() {
+ {
+ node* p = rt.new(node);
+ p.drop = fn() { rt.put_s("parent sees "); rt.put_n(*this.kid); rt.put_c(32); };
+ int* k = rt.new(int); k.drop = fn() { rt.put_s("child "); };
+ *k = 5;
+ p.kid = k;
+ }
+ rt.put_c(10);
+ return 0;
+}
+// zc0: skip
diff --git a/tests/conformance/6-ownership/drop_reused_address.zc b/tests/conformance/6-ownership/drop_reused_address.zc
new file mode 100644
index 0000000..97ef21d
--- /dev/null
+++ b/tests/conformance/6-ownership/drop_reused_address.zc
@@ -0,0 +1,22 @@
+// expect: drop 1 same 1 1 0/
+// destructors 9: a freed block's drop and its bits are cleared, so a block reused at the same address runs no old drop
+include rt;
+int n, first;
+int addr(int* p) { return p as int; }
+int clean(int* p) { unbounded { return (p[-4] & (rt.F_HASDROP | rt.F_DROPPED)) == 0; } }
+// the drop is set in a helper, so main has no closure and its locals take no cells from the heap
+int* .u armed() { int* p = rt.new(int); p.drop = fn() { n++; rt.put_s("drop "); }; return p; }
+fn main() {
+ { int* p = armed(); *p = 1; first = addr(p); }
+ rt.put_n(n); rt.put_c(32);
+ {
+ int* q = rt.new(int); *q = 2;
+ if (addr(q) == first) rt.put_s("same "); else rt.put_s("moved ");
+ rt.put_n(clean(q)); rt.put_c(32);
+ }
+ rt.put_n(n); rt.put_c(32);
+ rt.put_n(rt.dcount);
+ rt.put_c(10);
+ return 0;
+}
+// zc0: skip
diff --git a/tests/conformance/6-ownership/drop_set_late.zc b/tests/conformance/6-ownership/drop_set_late.zc
new file mode 100644
index 0000000..9bd8310
--- /dev/null
+++ b/tests/conformance/6-ownership/drop_set_late.zc
@@ -0,0 +1,10 @@
+// expect error: x: drop set after the value was used
+// destructors 2: the drop must be set before the value is first read, reassigned or passed
+include rt;
+fn main() {
+ int* x = rt.new(int);
+ *x = 4;
+ x.drop = fn() { rt.put_s("a"); };
+ return 0;
+}
+// zc0: skip
diff --git a/tests/conformance/6-ownership/drop_set_twice.zc b/tests/conformance/6-ownership/drop_set_twice.zc
new file mode 100644
index 0000000..843694a
--- /dev/null
+++ b/tests/conformance/6-ownership/drop_set_twice.zc
@@ -0,0 +1,10 @@
+// expect error: x: drop is already set
+// destructors 2: a value's drop can be set only once
+include rt;
+fn main() {
+ int* x = rt.new(int);
+ x.drop = fn() { rt.put_s("a"); };
+ x.drop = fn() { rt.put_s("b"); };
+ return 0;
+}
+// zc0: skip
diff --git a/tests/conformance/6-ownership/drop_temporary_statement.zc b/tests/conformance/6-ownership/drop_temporary_statement.zc
new file mode 100644
index 0000000..fd27623
--- /dev/null
+++ b/tests/conformance/6-ownership/drop_temporary_statement.zc
@@ -0,0 +1,13 @@
+// expect: unlocked a b unlocked c /
+// destructors 3: a value nothing holds drops at the end of its full statement; a bound guard drops at block end
+include rt;
+void unlock() { rt.put_s("unlocked "); }
+fn main() {
+ heap(0).drop = fn() { unlock(); };
+ rt.put_s("a ");
+ { let guard = drop(heap(0), fn() { unlock(); }); rt.put_s("b "); }
+ rt.put_s("c ");
+ rt.put_c(10);
+ return 0;
+}
+// zc0: skip
diff --git a/tests/conformance/6-ownership/drop_value_and_type.zc b/tests/conformance/6-ownership/drop_value_and_type.zc
new file mode 100644
index 0000000..2979e4f
--- /dev/null
+++ b/tests/conformance/6-ownership/drop_value_and_type.zc
@@ -0,0 +1,11 @@
+// expect: body value type only-type /
+// destructors 2: a struct body's drop is the default for its type; a value's own drop runs first, then the type's
+include rt;
+struct res { int fd; void drop() { if (this.fd) rt.put_s("type "); else rt.put_s("only-type "); } }
+fn main() {
+ { res* r = rt.new(res); r.drop = fn() { rt.put_s("value "); }; r.fd = 1; rt.put_s("body "); }
+ { res* q = rt.new(res); q.fd = 0; }
+ rt.put_c(10);
+ return 0;
+}
+// zc0: skip
diff --git a/tests/conformance/8-runtime/o3_no_gc_inside.zc b/tests/conformance/8-runtime/o3_no_gc_inside.zc
new file mode 100644
index 0000000..532765c
--- /dev/null
+++ b/tests/conformance/8-runtime/o3_no_gc_inside.zc
@@ -0,0 +1,39 @@
+// expect: 0 1 0 1 0 1/
+// spec 4.10.3: no collection runs inside an o3 region; an allocation that would collect grows the heap, and leaving the outermost region runs the pending collection once
+include rt;
+word sink;
+int inside;
+o3 int churn(int n) {
+ int i = 0;
+ while (i < n) { sink = rt.new(64); i++; }
+ rt.gc();
+ return 0;
+}
+int early(int base) {
+ o3 {
+ int i = 0;
+ while (i < 20000) { sink = rt.new(64); i++; }
+ rt.gc();
+ inside = rt.gc_runs - base;
+ return 0;
+ }
+ return 1;
+}
+fn main() {
+ int before = rt.gc_runs;
+ o3 {
+ int i = 0;
+ while (i < 20000) { sink = rt.new(64); i++; }
+ inside = rt.gc_runs - before;
+ }
+ rt.put_n(inside); rt.put_c(32); rt.put_n(rt.gc_runs - before); rt.put_c(32);
+ before = rt.gc_runs;
+ churn(20000);
+ rt.put_n(0); rt.put_c(32); rt.put_n(rt.gc_runs - before); rt.put_c(32);
+ before = rt.gc_runs;
+ early(before);
+ rt.put_n(inside); rt.put_c(32); rt.put_n(rt.gc_runs - before);
+ rt.put_c(10);
+ return 0;
+}
+// zc0: skip
diff --git a/tests/data/drop_hint.zc b/tests/data/drop_hint.zc
new file mode 100644
index 0000000..c690bc5
--- /dev/null
+++ b/tests/data/drop_hint.zc
@@ -0,0 +1,2 @@
+include rt;
+fn main() { heap(0).drop = fn() { rt.put_s("u"); }; return 0; }
diff --git a/tests/drivers/actor_test.py b/tests/drivers/actor_test.py
index 0cfa51f..929bbf4 100644
--- a/tests/drivers/actor_test.py
+++ b/tests/drivers/actor_test.py
@@ -23,7 +23,7 @@ for case in sorted((root / 'tests/actor').glob('*/*.zc')):
proc = subprocess.run([str(root / args.vm)], input=source, text=True, capture_output=True, cwd=root, timeout=15)
elapsed = time.monotonic() - start
if case.parent.name == 'reject':
- required = {'no_include': "actor: isn't a type", 'input_type': 'type error:', 'output_type': 'type error:', 'explicit_this': 'this is implicit', 'nested_missing_import': 'module access requires `include rt;`', 'generic_input': 'type error:', 'generic_method_return': 'type error:'}[case.stem]
+ required = {'no_include': "actor: isn't a type", 'input_type': 'type error:', 'output_type': 'type error:', 'explicit_this': 'this is implicit', 'nested_missing_import': 'module access requires `include rt;`', 'generic_input': 'type error:', 'generic_method_return': 'type error:', 'drop_message': "r: has drop and can't be copied into a message", 'drop_value': "p: has drop and can't be copied into a message"}[case.stem]
assert required in proc.stdout, f'{case}: expected {required!r}, got {proc.stdout!r}'
else:
actual = proc.stdout
diff --git a/tests/test.sh b/tests/test.sh
index 1be4557..a3370f2 100755
--- a/tests/test.sh
+++ b/tests/test.sh
@@ -347,7 +347,7 @@ s_small() {
# code literals can look like pointers on a compact map. test collection and
# the retained root without assuming the default map's exact reclaim count.
for vm in $T/zyms $T/zys; do
- out=$(sed 's/f >= 990/f > 0/' tests/programs/gc.zc | $vm | tr '\n' '/')
+ out=$(sed -E 's/f >= [0-9]+/f > 0/' tests/programs/gc.zc | $vm | tr '\n' '/')
chk "$out" "1000 1 900 1 199900/" "small collector makes progress and preserves its root"
done
@@ -578,6 +578,7 @@ s_lsp() {
case "$h" in *"$w"*) chk 1 1 "lsp hint: $w";; *) chk 0 1 "lsp hint: $w";; esac
done
chk "$($lsp < examples/struct_closure_opt.zc | grep -c error)" "0" "lsp: methods and closures check clean"
+ case "$($lsp < tests/data/drop_hint.zc)" in *"value with drop is never bound; it drops at the end of this statement"*) chk 1 1 "lsp hint: a value with drop that is never bound";; *) chk 0 1 "lsp hint: a value with drop that is never bound";; esac
# a file of a larger build checks on its own: its bodyless declarations are not errors in check mode
for f in src/runtime/rt.zc src/runtime/run.zc src/lsp/server.zc; do chk "$($lsp < $f | grep -c ': error:')" "0" "lsp: $f checks alone"; done
# the compiler is several files that only make sense together: check them as one document
diff --git a/tests/xref.baseline b/tests/xref.baseline
index 985f9c7..8731091 100644
--- a/tests/xref.baseline
+++ b/tests/xref.baseline
@@ -35,12 +35,18 @@ asm_sites reserve_asm_labels
bincap reserve_binary
blen add_binop reserve_binary
bname add_binop reserve_binary
+bnxt chk_reset fn_begin topitem1
borrow_depth call ctx_pop fn_begin
borrow_entry borrow_prologue ctx_pop fn_begin
borrow_jump borrow_prologue ctx_pop fn_begin
borrow_pending ctx_pop emit_borrow_hold fn_begin
borrow_slots ctx_pop emit_borrow_hold fn_begin
+bound_on chk_reset topitem1
bprec add_binop reserve_binary
+bs_pk body_scan bs_adv
+bs_pk2 body_scan bs_adv
+bs_pv body_scan bs_adv
+bs_pv2 body_scan bs_adv
bseq add_binop reserve_binary
bxdepth blockexpr chk_reset ctx_pop fn_begin
callpos chk_reset primary
@@ -76,6 +82,7 @@ clo_via clocap_at reserve_captures reserve_closures
clo_via_initial
closure_cap reserve_closures
closure_expected args closure zc_init
+closure_this closure drop_attach
cmode chk_begin chk_end
code_chunk chk_reset reserve_code syntax_resident
ctxsp ctx_pop ctx_push zc_init
@@ -109,9 +116,10 @@ dnmb_initial
docbufcap doc_char
docnamecap doc_name_room
docpending chk_reset docblock doccomment lex1
-docs_on capture_scan chk_reset kw_docs
+docs_on body_scan capture_scan chk_reset kw_docs
docsnapcap topitem0
-dry capture_scan chk_reset hl_begin hl_end prescan typearg
+drop_tail drop_call drop_member
+dry body_scan capture_scan chk_reset hl_begin hl_end prescan typearg
dsect chk_reset docblock doccomment
dsn chk_reset doc_attach topitem0
dsnap topitem0
@@ -128,7 +136,8 @@ effect_edges effect_arg zc_init
effect_floor compile_list_context ctx_pop unit_top zc_init
effect_requirements effect_finish effect_require_mode zc_init
effect_symbols effect_record reserve_symbols zc_init
-effect_value array_cap array_len array_pop arrvalue bin call ctx_pop field fn_begin kw_share_value methcall postfix_tail primary ternary unary
+effect_value array_cap array_len array_pop arrvalue bin call ctx_pop drop_attach field fn_begin heap_box kw_share_value methcall postfix_tail primary ternary unary
+enum_open kw_enum
epichain chk_reset ctx_pop fn_begin kw_return
evcap ev_put
evn chk_unit ev_put
@@ -138,6 +147,7 @@ export_path export_owner
export_tail export_add export_owners
extop chk_reset exit_rec loop_end
factcap putfact
+fbnd chk_reset closure ctx_pop fn_begin kw_unbounded
fchk chk_reset ctx_pop defer_one fn_begin kw_unchecked
fdepth block blockexpr casebody chk_reset ctx_pop fn_begin kw_for kw_switch substmt
fframe chk_reset ctx_pop prologue
@@ -170,8 +180,9 @@ fortest bin chk_reset kw_for
fret chk_reset ctx_pop fn_begin retcheck
fs_ok expr incdec kw_for
fsite fixnew reserve_fixes
-fslots array_dynamic_literal array_literal_tokens chk_reset closure ctx_pop embed_bind emit_borrow_hold field fn_begin fndef kw_switch localdecl o3mkcache
+fslots array_dynamic_literal array_literal_tokens chk_reset closure ctx_pop embed_bind emit_borrow_hold field fn_begin fndef hoist_guard kw_switch localdecl o3mkcache
ft_bd bin
+ft_ck bin kw_for
ft_ir bin
ft_iv bin kw_for
ft_nops bin
@@ -194,7 +205,7 @@ gnew_nm
gnew_np
gng gcompile
gnm
-grec capture_scan chk_reset topitem
+grec body_scan capture_scan chk_reset topitem
gt_imports ginit gtmake reserve_generics
gt_module ginit gtmake reserve_generics
gt_n chk_reset gtmake
@@ -261,7 +272,7 @@ kops addkop reserve_operators
kw_expr chk_reset ctx_pop kwcall
laaddr array_literal_tokens localdecl newloc reserve_locals unary
laaddr_initial
-lafact af_forget af_store array_literal_tokens expr incdec localdecl newloc reserve_locals
+lafact af_forget af_store array_literal_tokens expr incdec lafact_keep localdecl newloc reserve_locals
lafact_initial
larr array_literal_tokens embed_bind localdecl newloc reserve_locals
lbase ctx_pop fn_begin zc_init
@@ -271,6 +282,7 @@ lheads defer_one local_bind local_drop
lheads_initial
lhome block_close join newloc reserve_locals snap_restore touch
line chk_reset nextch src_pop src_push
+lines_on kw_lines lt_apply_marks
literal_doc_cap literal_doc_note
literal_doc_n chk_unit literal_doc_note
literal_docs literal_doc_note
@@ -286,6 +298,7 @@ lnsz assigncheck embed_bind localdecl reserve_locals
lnsz_initial
local_captured clocap_at newloc reserve_locals
local_cell capture_cell newloc reserve_locals
+local_dropset drop_attach localdecl newloc reserve_locals
local_end_info assigncheck end_owner newloc reserve_locals
local_ended assigncheck end_owner newloc reserve_locals
local_origin assigncheck newloc reserve_locals
@@ -297,6 +310,7 @@ lown assigncheck closure fndef newloc reserve_locals touch
lowner assigncheck join newloc reserve_locals snap_restore
lpfact bpoison kw_for loop_push reserve_R_LOOP
lpfact_initial
+lpinfo linfo_new loop_push
lpos closure embed_bind fndef localdecl newloc reserve_locals
lpos_initial
lprev local_bind reserve_locals
@@ -314,10 +328,24 @@ lslot newloc reserve_locals
lsp ctx_pop loop_end loop_push zc_init
lstate alias_end assigncheck backedge block_close closure end_owner fndef handup join newloc recast reserve_locals retcheck snap_cmp snap_restore touch
lt_at lt_note
+lt_base lt_begin lt_end
+lt_buf lt_begin lt_room lt_stmt
+lt_cap lt_room
+lt_file build_image_grow lt_file_data
+lt_file_at build_image_grow lt_file_data
+lt_file_img lt_file_data
+lt_head build_image_grow lt_end
lt_len lt_note
+lt_mark_base build_image_grow lt_apply_marks
+lt_mark_file build_image_grow lt_apply_marks
+lt_markcap mark_line
+lt_marks mark_line
lt_n lt_note
+lt_next_mark build_image_grow lt_apply_marks
+lt_nmarks build_image_grow mark_line
lt_off lt_note
lt_on
+lt_top lt_begin lt_end lt_stmt
lt_type lt_note
ltag asmblock embed_bind expr fndef incdec kw_o3 localdecl luse newloc o3mkcache o3promote reserve_locals rval setflt setstr touch unary
ltcap lt_note
@@ -326,10 +354,10 @@ ltxt lt_note
ltxtcap lt_note
ltype array_literal_tokens closure embed_bind fndef kw_switch localdecl newloc reserve_locals
lused assigncheck luse newloc reserve_locals
-lv array_dynamic_literal array_literal_tokens bin blockexpr call chk_reset closure ctx_pop enhcall expr field fn_begin incdec kwcall methcall newtyped postfix_tail primary rval ternary unary
+lv array_dynamic_literal array_literal_tokens bin blockexpr call chk_reset closure ctx_pop drop_attach enhcall expr field fn_begin heap_box incdec kwcall methcall newtyped postfix_tail primary rval ternary unary
lvescaped closure newloc reserve_locals unary
lvi chk_reset ctx_pop field postfix_tail primary
-lvk array_dynamic_literal array_literal_tokens bin blockexpr call chk_reset closure ctx_pop enhcall expr field fn_begin incdec kwcall methcall newtyped postfix_tail primary ternary unary
+lvk array_dynamic_literal array_literal_tokens bin blockexpr call chk_reset closure ctx_pop drop_attach enhcall expr field fn_begin heap_box incdec kwcall methcall newtyped postfix_tail primary ternary unary
lvkind closure expr incdec newloc reserve_locals unary vf_effect vf_store
lvvalue newloc reserve_locals vf_store
mdbg chk_reset kw_memdebug
@@ -351,8 +379,8 @@ nclo chk_reset closure
ndfr defer_drop kw_defer
nenh blob_load build_image_grow chk_reset enhdef
nerr chk_begin chk_reset errat prescan replace_function
-newleaf array_dynamic_literal array_pop call localdecl newtyped
-newsz array_dynamic_literal array_pop call chk_reset localdecl newtyped
+newleaf array_dynamic_literal array_pop call heap_box localdecl newtyped
+newsz array_dynamic_literal array_pop call chk_reset heap_box localdecl newtyped
nfact chk_reset fdrop putfact
nfixu fixnew zc_init
ngbind call chk_reset primary
@@ -377,6 +405,7 @@ ntok next
ntp chk_reset ctx_pop fndef fndone genparams kw_struct methdef ptype1 topitem1
numops types_init
nun add_unop zc_init
+o3blk kw_o3
o3cache kw_o3 newloc o3mkcache reserve_locals
o3cache_initial
o3dsc o3region
@@ -393,7 +422,7 @@ o3pend_initial
o3pendcap kw_o3
o3rg o3promote
o3sl o3promote
-panic chk_begin chk_reset die diefound dien effect_finish expect hl_begin prescan replace_function resync topsync unit_end
+panic body_scan capture_scan chk_begin chk_reset die diefound dien effect_finish expect hl_begin prescan replace_function resync topsync unit_end
pe_first enum_tagged reserve_tagged_enums
pe_first_initial
pe_n enum_tagged reserve_tagged_enums
@@ -433,7 +462,7 @@ poolend intern zc_init
pooltop intern zc_init
pq_gl next peekat src_pop
pq_ln next peekat src_pop
-pq_n capture_scan chk_reset hl_begin hl_end next peekat src_pop src_push
+pq_n body_scan capture_scan chk_reset hl_begin hl_end next peekat src_pop src_push
pq_tk next peekat src_pop
pq_tv next peekat src_pop
probe_cap probe_add
@@ -465,6 +494,8 @@ s_andand zc_init
s_arrow zc_init
s_as zc_init
s_at_s types_init
+s_bchk types_init
+s_bnd types_init
s_case zc_init
s_cat_s types_init
s_check zc_init
@@ -520,6 +551,7 @@ s_le types_init
s_le_s types_init
s_len_s types_init
s_lfa types_init
+s_lines types_init
s_lk zc_init
s_lp zc_init
s_lt types_init
@@ -574,12 +606,14 @@ s_type types_init
s_typecheck types_init
s_typedef types_init
s_u zc_init
+s_unbounded zc_init
s_unchecked zc_init
s_var zc_init
s_vm zc_init
s_while zc_init
sdoff addsym def_note reserve_symbols
sdsrc addsym def_note reserve_symbols
+send_check args field
seqtop seqasm zc_init
sfix addsym enhdef fndef fnref reserve_symbols
sflag add_kw addsym blob_load call enhdef fndef fndone fwdfn_at ginstance kw_extern kw_struct methcall methdecl prescan_fn reserve_symbols retcheck
@@ -600,6 +634,23 @@ stype addsym embed_bind enum_tagged fndef gdecl2 kw_enum reserve_symbols retchec
sval addsym blob_emit fndef reserve_symbols
svkind expr reserve_symbols vf_symbols
svvalue expr reserve_symbols vf_symbols
+sx_asm types_init
+sx_bool types_init
+sx_char types_init
+sx_defer types_init
+sx_do types_init
+sx_else types_init
+sx_float types_init
+sx_for types_init
+sx_if types_init
+sx_int types_init
+sx_new types_init
+sx_pop types_init
+sx_push types_init
+sx_quote types_init
+sx_resize types_init
+sx_switch types_init
+sx_while types_init
symcap reserve_symbols
syntax_host_floor syntax_resident
syntax_host_from syntax_resident
@@ -618,7 +669,7 @@ tfloat types_init
tglue chk_reset lex1 next peekat src_pop tunpack
tint types_init
tk chk_reset floatok lex1 next next0 oplex peekat src_pop
-tline chk_reset lex1 tunpack
+tline chk_reset docblock lex1 tunpack
toff chk_reset lex1 tunpack
token_block_doc lex1
toklen chk_reset lex tunpack
@@ -640,19 +691,24 @@ uname add_unop reserve_unary
uncap reserve_unary
useq add_unop reserve_unary
vislo build_image_grow chk_unit start syntax_resident
-xaf af_forget args array_cap array_dynamic_literal array_len array_literal_tokens array_pop bin call chk_reset expr field incdec kw_share_value kwcall methcall newtyped postfix_tail primary ternary unary
-xbor array_cap array_dynamic_literal array_len array_literal_tokens array_pop assigncheck bin blockexpr call chk_reset closure discard enhcall field fn_begin kw_share_value kwcall methcall postfix_tail primary ternary typearg unary
+xaf af_forget args array_cap array_dynamic_literal array_len array_literal_tokens array_pop bin call chk_reset drop_attach drop_member expr field heap_box incdec kw_share_value kwcall methcall newtyped postfix_tail primary ternary unary
+xbor array_cap array_dynamic_literal array_len array_literal_tokens array_pop assigncheck bin blockexpr call chk_reset closure discard drop_attach drop_member enhcall field fn_begin heap_box kw_share_value kwcall methcall postfix_tail primary ternary typearg unary
+xc_file xc_init
+xc_fn closure fndef
+xc_hook xc_init
+xc_on xc_init
+xc_src unit_stdin
xcls array_cap array_len array_pop array_push array_resize chk_reset ctx_pop kw_asm kwcall quote_tokens
-xconst array_dynamic_literal array_literal_tokens bin call chk_reset closure kw_share_value methcall postfix_tail primary unary
+xconst array_dynamic_literal array_literal_tokens bin call chk_reset closure drop_attach heap_box kw_share_value methcall postfix_tail primary unary
xcval array_literal_tokens primary unary
-xfloat array_dynamic_literal array_len array_literal_tokens array_pop array_push array_resize bin blockexpr call closure expr fcoerce femit ftest incdec kw_cfunc kw_for methcall postfix_tail primary ternary unary
+xfloat array_dynamic_literal array_len array_literal_tokens array_pop array_push array_resize bin blockexpr call closure drop_attach expr fcoerce femit ftest heap_box incdec kw_cfunc kw_for methcall postfix_tail primary ternary unary
xgive argcheck args chk_reset
xlend assigncheck expr unary
xlenloc array_len bin call closure enhcall methcall postfix_tail primary unary
xprojection ctx_pop field postfix_tail primary
xstamp array_dynamic_literal array_len bin call closure enhcall methcall newtyped postfix_tail primary unary
-xstr array_dynamic_literal array_len array_literal_tokens bin blockexpr call closure incdec methcall postfix_tail primary unary
-xty args array_cap array_dynamic_literal array_len array_literal_tokens array_pop array_push array_resize bin blockexpr call closure ctx_pop enhcall expr fcoerce field ftest kw_cfunc kw_share_value kw_sizeof kw_typeof kwcall methcall newtyped postfix_tail primary ternary unary
+xstr array_dynamic_literal array_len array_literal_tokens bin blockexpr call closure drop_attach heap_box incdec methcall postfix_tail primary unary
+xty args array_cap array_dynamic_literal array_len array_literal_tokens array_pop array_push array_resize bin blockexpr call closure ctx_pop drop_attach drop_member enhcall expr fcoerce field ftest heap_box kw_cfunc kw_share_value kw_sizeof kw_typeof kwcall methcall newtyped postfix_tail primary ternary unary
xvk array_dynamic_literal array_literal_tokens bin blockexpr chk_reset ctx_pop expr field fn_begin incdec kw_share_value kwcall postfix_tail primary ternary unary vf_coerce vf_effect vf_forget
xvv blockexpr primary unary vf_coerce
-xzero array_dynamic_literal array_literal_tokens bin blockexpr call closure enhcall expr field incdec kwcall methcall newtyped postfix_tail primary ternary unary
+xzero array_dynamic_literal array_literal_tokens bin blockexpr call closure drop_attach enhcall expr field heap_box incdec kwcall methcall newtyped postfix_tail primary ternary unary