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>
47 lines
2 KiB
Text
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
|
|
}
|