writeonce/docs/examples/gc-cycle/main.wo
shoney.arickathil 4f570a74e6 feat(compiler): ?T forced handling — WO-E211/E212/E213 + narrowing (iter 5)
The type system now keeps its nullability promise: a `?T` value cannot be
used, stored, or dereferenced as a plain `T` without narrowing. The canonical
evidence probe (return b.v where v: ?Int, fn -> Int) that compiled clean for
months now fails with WO-E211.

- WO-E211 (un-narrowed use): arithmetic and </<=/>/>= operands, and/or
  operands (?Bool), interpolation segments, for-iterables, and returns whose
  declared type is not nullable.
- WO-E212 (boundary): nil or ?T stored into a non-nullable slot — annotated
  let, assignment to a confidently-typed local (cenv, never the placeholder
  env — a placeholder target must stay silent) or a resolvable class field.
- WO-E213 (deref): field/index access through a possibly-nil base.
- Narrowing (locals only — a field place can be re-assigned between check
  and use, so chains bind to a local first): `if x != nil { }` narrows the
  branch; a DIVERGING then-branch (`if x == nil { return }`) narrows after
  the if; `x != nil and x.n > 3` narrows and/or right operands
  (short-circuit); `while x != nil` narrows the body. The narrow is
  un-applied when an else-less then-env leaks out un-diverged (the existing
  env-leak convention must not leak the narrow).
- No false positives by construction: env/cenv types are declared or
  confidently inferred; the placeholder fallbacks are plain scalars, never
  ?T. The whole golden suite passed untouched (540/0).
- Samples updated to the bind-then-narrow idiom (log-watcher config decode +
  supervisor lock/next_fire, gc-cycle ring print) — 22 genuine unnarrowed-nil
  sites; employee needed zero changes. All acceptances green.
- Corpus: compile-fail/{nullable-unnarrowed-use,nullable-nil-into-plain,
  nullable-deref-unchecked} + run/nullable-narrowing (all four forms) — 83/0.
- Catalog: E211/E212/E213 move from "Reserved, not yet emitted" to the main
  table; nullable-types-implementation.md status flipped to ENFORCED
  (historical record kept); plan 8 Task 6 ticked (boxed scalar cells
  superseded by WO_NIL_SCALAR); board updated.

Verified: woc-test 540/0 + test_diag 14/0; oop-e2e 83/0; oop-accept ALL MET;
log-watcher 7/0; employee 8/0; gc-cycle ring prints + reclaims.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 17:47:13 +02:00

47 lines
2 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`
-- ?T enforcement: a field place (`a.next`) never narrows, so each hop
-- binds to a local and the guard narrows the locals.
let n1 = a.next;
let n2 = b.next;
let n3 = c.next;
if n1 != nil and n2 != nil and n3 != nil {
print("ring ${a.label} -> ${n1.label} -> ${n2.label} -> ${n3.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
}