Design-first deliverable for iteration 7b (no runtime/compiler code yet). docs/examples/gc-cycle explains, by example, how pointers flow through the heap and the collector's mark-sweep logic: - types.wo: Node (self-referential ?Node -> inferred `gc`/traced) vs Segment (acyclic -> `owned`, deterministically dropped) - main.wo: ring_demo builds a->b->c->a and abandons it; owned_demo shows the drop path with no collector - README.md: the model (ownership frees the 99%, tracing only the cyclic/ aliased residue, inference decides), the 16-byte header rewrite (retire rc+borrow -> 8-byte sweep-list link, colors in flag bits), where a traced pointer lives (root via pc gc-mask / GCREF field / container), and the tri-color incremental algorithm with the Yuasa deletion barrier. Two mermaid step diagrams (heap+roots, collector cycle) + the owned contrast. Grounded in the approved spec (2026-08-11-inferred-gc-mark-sweep-design.md) and the real runtime structures (obj.h/wob.h: wo_hdr, WO_K_GCREF, arena, wo_obj_size). Run status: honest — the sample does NOT build today; woc reports WO-E301 (use-after-move at the ring-closing store), which is exactly the aliasing that "traced classes alias freely" unblocks under 7b. README records this. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
40 lines
1.8 KiB
Text
40 lines
1.8 KiB
Text
-- gc-cycle — the smallest program that needs a tracing collector, and the
|
|
-- smallest that does not. Iteration 7b's target sample (see README).
|
|
--
|
|
-- ring build a 3-node cycle, abandon it, let a collection slice reclaim it
|
|
-- owned build a Segment, show it dies at scope end with no collector at all
|
|
|
|
fn main(args: multi Text) -> Int {
|
|
if len(args) >= 1 and args[0] == "owned" { return owned_demo(); }
|
|
return ring_demo();
|
|
}
|
|
|
|
-- The cyclic case. a -> b -> c -> a. Every Node is reachable from `a` while `a`
|
|
-- is a live root (on the value stack). The moment `a` leaves scope the whole
|
|
-- ring becomes unreachable but is NOT freed by any drop — ownership cannot
|
|
-- reclaim a cycle. The next marking slice finds no root reaching the ring, so
|
|
-- all three sweep white and are freed together.
|
|
fn ring_demo() -> Int {
|
|
let a = Node { label: "a", next: nil };
|
|
let b = Node { label: "b", next: nil };
|
|
let c = Node { label: "c", next: nil };
|
|
|
|
a.next = b; -- store into a GCREF slot: barrier-relevant while marking
|
|
b.next = c;
|
|
c.next = a; -- closes the cycle; c.next aliases the same Node as `a`
|
|
|
|
print("ring ${a.label} -> ${a.next.label} -> ${a.next.next.label} -> ${a.next.next.next.label}");
|
|
-- prints: ring a -> b -> c -> a
|
|
-- `a`, `b`, `c` go out of scope here. No DROP frees the Nodes (they are
|
|
-- traced, not owned). The ring is now abandoned; a later slice collects it.
|
|
return 0;
|
|
}
|
|
|
|
-- The acyclic case, for contrast. Segment is `owned`: at the `}` the drop table
|
|
-- lists its register in the OWNED mask, the VM frees the object and its Text
|
|
-- field deterministically, and the collector never sees it.
|
|
fn owned_demo() -> Int {
|
|
let s = Segment { from: "auth.log", len: 4096 };
|
|
print("segment ${s.from} len=${s.len}");
|
|
return 0; -- `s` (and its Text) freed here by DROP, no tracing
|
|
}
|