diff options
| author | 2026-10-14 21:36:55 +0330 | |
|---|---|---|
| committer | 2026-10-14 22:35:10 +0330 | |
| commit | 65b44a70ed3f4b6b202d983f27ef7a068fb56aa5 (patch) | |
| tree | 6b3413a476bb0ac4801a4709fb60b7e4b9624c51 /docs | |
| parent | docs: revise destructors draft to acorn's rulings (drop on any heap value, he... (diff) | |
docs: destructors: value and type drops both run, side table storage, F_HASDROP/F_DROPPED bits
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/design/destructors.md | 15 |
1 files changed, 6 insertions, 9 deletions
diff --git a/docs/design/destructors.md b/docs/design/destructors.md index 36c9f20..8fb6498 100644 --- a/docs/design/destructors.md +++ b/docs/design/destructors.md @@ -45,7 +45,7 @@ let y = new(int); y.drop = fn() { std.put_n(*this); }; // state } ``` - a per-value drop on such a value is an error, the same as setting it twice (see open question 1). + 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. @@ -160,13 +160,10 @@ untyped `rt.new(n)` blocks take a drop like any other value, so there is no sepa ## 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 needs per-value storage. proposal: a header flag `F_HASDROP` plus a side table keyed by block address (open hash), so values without a drop pay nothing in size. the type default needs no table entry. see open question 2. -- **values without drop pay one test.** the runtime's free paths (unchecked `rt.free`, children found by the walk, the collector) add one load and test: `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`. -- **header bits.** `F_HASDROP` and `F_DROPPED` take two free header flags; a header bit audit should confirm which. +- **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. - **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. - -## open questions for acorn - -1. a struct with a type-level `drop`: should a per-value drop be an error (as written), replace the default, or run in addition to it? -2. per-value drop storage: side table keyed by address (no size cost, one hash lookup per drop) or an extra header word on every block (fast, costs memory everywhere)? |
