aboutsummaryrefslogtreecommitdiff
path: root/docs/design/destructors.md
diff options
context:
space:
mode:
authorGravatar zinc <zinc@downsta.ir> 2026-10-14 21:36:55 +0330
committerGravatar zinc <zinc@downsta.ir> 2026-10-14 22:35:10 +0330
commit65b44a70ed3f4b6b202d983f27ef7a068fb56aa5 (patch)
tree6b3413a476bb0ac4801a4709fb60b7e4b9624c51 /docs/design/destructors.md
parentdocs: 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/design/destructors.md')
-rw-r--r--docs/design/destructors.md15
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)?