diff options
| author | 2026-10-14 23:10:49 +0330 | |
|---|---|---|
| committer | 2026-10-14 23:10:49 +0330 | |
| commit | 8af3174b04f18d858653ad70669dced71f5eaa99 (patch) | |
| tree | 741ad2afd7cf3b39be763d17eea6a8c01c9101e7 /docs | |
| parent | docs: destructors: clear drop entry and bits on free; planned reuse test (diff) | |
destructors: heap(v), drop(v, f) / v.drop(f) / v.drop = f, struct drop methods, collector drops after the sweep (once, resurrection), freed blocks forget their drop, temporaries drop at statement end with a hint, actor send check; o3: no collection inside a region (pending until the outermost exit), no drops set or ended inside o3; spec 3.11.6, 4.10.3-4, 6.15
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/SPEC.md | 16 | ||||
| -rw-r--r-- | docs/design/destructors.md | 10 |
2 files changed, 25 insertions, 1 deletions
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 index 1f085f3..777db2c 100644 --- a/docs/design/destructors.md +++ b/docs/design/destructors.md @@ -1,6 +1,6 @@ # destructors on heap values (draft) -status: design draft, no compiler code. 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`. +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 @@ -172,3 +172,11 @@ untyped `rt.new(n)` blocks take a drop like any other value, so there is no sepa ## 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`, `dclear`, `dmove` (used by `dgrow`); `F_HASDROP` 16, `F_DROPPED` 32 and `F_FINAL` 64 (collector found it unreachable). `release` and `recycle` run the drop first and clear the entry when the block goes back; `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). |
