Board row 16 -> landed (web-app 14/0), pending row removed; story header
records the landing + the two as-built discoveries (idle-keep-alive
starvation policy; the two compiler gaps the chain exposed and fixed);
README gains the framework+web-app sample entry; plan checkboxes ticked.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- docs/examples/web-app: Product (@unique name, backlink orders) / Order
(ref Product) as @table classes; handlers as Handler classes —
ListProducts (ordered query -> JSON array), ShowProduct (unique-index
probe, 404), CreateProduct (checked json.decode -> 400; @unique trap ->
409), CreateOrder (FK insert), DeleteProduct (FK restrict trap -> 409);
Auth middleware reads WA_TOKEN. Entry validates port + token honestly.
- The [deps] KEY is the module name `use` imports: hyphens are not
identifier characters, so the app keys the dep `framework` while the
repository keeps its long name (recorded in the manifest comment).
- driver fix (real gap the chain exposed): a dependency's INTERNAL `use`
paths are written against its own root (`use http` inside the framework)
but compile under `<depname>/...` — compile_image now prefixes dep files'
use paths with the dep name (stdlib namespaces stay bare; a path already
starting with the dep name is untouched).
Verified end to end through the full chain (temp git remote of the
framework, file:// substituted, fetch -> lock -> build -> serve): 401
without the token; [] empty list; 201 create; 409 duplicate (@unique);
400 malformed json; list/show payloads exact; 404 unknown product; 201
order; 409 delete-while-referenced (FK restrict) with the server still
serving; SIGTERM clean; the product survives a process restart (WAL
replay). Gates: woc-test 540/0, oop-e2e 88/0, deps-accept 8/0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- router/router.wo: Handler (handle(req) -> Resp) and Middleware
(before(req) -> ?Resp; nil = continue) structural interfaces; Route/Mw
record classes built as ctor literals at the registration site — the
ownership shape the corpus pins (run/container-owned-move);
route_match with :param captures over '/'-split segments (empties
dropped, first mismatch wins).
- app.wo: App holds the middleware chain + route table (take-push),
satisfies http's Dispatcher, dispatches middleware-then-first-match,
fills the captures onto the borrowed request in place (Dispatcher takes
`mut req` — the borrow checker rightly refused rebuilding a Req from
borrowed maps; captures collect locally so a failed match never touches
the request), 404 fallback, serve(host, port) delegation.
Verified against a throwaway app (not committed): middleware 401
short-circuit without the token; /things/:id captures 42 into the body;
unknown path 404; a DIV0 handler answers 500 and the next request is
served; SIGTERM clean. Framework image emits at 12161 bytes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- http/parse.wo: bounded-read buffering to the header terminator, then
exactly Content-Length body bytes; %XX decoding ('+' = space in query
strings only, malformed escapes pass through — parsing stays total);
path/query split with decoded pairs; header names lowercased; the
three-state Parsed record (closed / malformed / request) with keep-alive
carry-over — bytes past this request belong to the next one on the
connection.
- http/serve.wo: Dispatcher interface (the router's seam), status/reason
serialization with computed Content-Length, and the blocking loop:
malformed -> 400 + close; a trapping handler -> 500 AND the loop lives;
fds closed on every path; env.stopping() honored.
- Connection policy discovered by probing, not assumed: a parked keep-alive
connection BLOCKS accept on a single-threaded server (probe: client 1
idles open, client 2 starves). Policy: serve PIPELINED requests on one
connection (carry non-empty), close when the client would idle; a proxy
reconnects. README states it.
Verified against a throwaway echo app (not committed): %20 query decode;
two pipelined requests -> two responses on one connection; DIV0 handler ->
500 and the NEXT connection served; GARBAGE -> 400; SIGTERM stops clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- docs/examples/writeonce-framework: library project (no fn main) — wo.toml,
README (what it is + the honest v1 limits + the proxy TLS/h2 story), and
http/types.wo: pub Req/Resp records + the response builders (ok_text/
ok_json/created/not_found/bad_request/unauthorized/conflict/server_error/
redirect). Typechecks + emits entry-less via woc --emit (1767-byte image).
- docs/examples/web-app: manifest with the real [deps] entry (future GitHub
URL as documentation; the gate substitutes a file:// remote) + README
(routes table, run instructions, nginx h2-in-front sketch). Code lands
with Task 4.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The disclosed "move-on-push" gap detonated on iteration 16's route-table
pattern: `push(self.routes, r)` moved the Route into the container while the
`take r` parameter's scope-end DROP still fired — the container's own drop
plan (multi_free) then freed the element a second time. ASan: SEGV in
class_free during trap unwind; latent until now because pushed elements were
Texts, which copy at the boundary (2026-08-14).
owner.ml analyze_call: `push`'s value slot and `set`'s key/value slots now
TRANSFER an Owned, non-copy-stored place (record_move, exactly the take-arg
shape), so the pusher's drop disappears. Text/json.Value keep the copy-store
path (stores_by_copy) and the caller still drops the fresh copy. Traced (gc)
values remain exempt (tracing owns them). A user-declared push/set fn of the
same name wins, per the builtin shadowing rule.
Pinned by tests/corpus/run/container-owned-move (route table: interface-
typed field values pushed via take params, dispatched by ICALL, mixed with
Text pushes) — the exact iteration-16 shape, ASan-clean.
Verified: woc-test 540/0; oop-e2e 88/0; log-watcher 7/0; employee 8/0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- error catalog: WO-E106 (dependency fetch/shape failures, one code, message
names dep + step) and WO-E107 (dep/local module-name collision) rows.
- README: a Dependencies subsection under the manifest docs — [deps] syntax,
.wo-deps/wo.lock behavior, offline-when-locked, --update-deps, flat-only.
- board: iteration 15 row -> landed (deps-accept 8/0), pending row removed;
story 15 header records the landing; 08-project-structure notes
.wo-deps (gitignored) + wo.lock (committed); plan checkboxes all ticked.
(One self-inflicted casualty during this task, restored from git before
commit: a buggy doc-edit script truncated 08-project-structure.md; the file
was recovered intact and the intended one-liner applied by hand.)
Gates at closeout: deps-accept 8/0, woc-test 540/0, oop-e2e 87/0,
log-watcher 7/0, employee 8/0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
scripts/deps-accept.sh: 8 checks over local file:// remotes built at run
time (network-free) — cold fetch + lock + use <dep>/<dep>/sub + app-entry-
wins (the dep's fn main returns 99 and must never run), offline rebuild
with the remote deleted, lock-beats-moved-tag, --update-deps refresh,
cache/lock drift WO-E106, transitive refusal, WO-E107 collision, missing-rev
manifest shape. `just deps-accept` wired. 8/0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
[deps] entries now resolve to fetched, locked checkouts and compile as
module roots.
- resolve_deps (driver): .wo-deps/<name>/ cache beside wo.toml, wo.lock
pinning name -> commit SHA. Cold+unlocked: clone at the manifest rev,
record HEAD. Cold+locked: clone and checkout the LOCKED sha — a moved tag
cannot change the build. Warm+locked: HEAD==lock -> zero network.
Divergence is WO-E106 "lock drift" naming both SHAs and pointing at
--update-deps; every git failure (missing binary, bad URL, bad rev,
missing locked commit) is WO-E106 naming the dep and step. Guard rails:
fetched dep must be a writeonce project; a dep with its own [deps] is
refused (flat-only); dep name colliding with a local module dir is
WO-E107. All git via the git binary (Sys.command; output reads through a
temp file) — no network code in the compiler. Full clone, not --depth 1
(a locked SHA must be reachable regardless of tag movement) — recorded
deviation from the plan's clone sketch.
- woc --update-deps <dir>: re-fetch at manifest revs, rewrite the lock.
- Multi-root compile: compile_image gains ~deps; app files first then deps
sorted by name; module_of_multi maps a dep file to `<name>` /
`<name>/<sub>`, so existing use/pub/collision machinery works across the
boundary unchanged. The app root's walk skips .wo-deps via the existing
dot-rule.
- Entry restriction: Emit.emit gains ?entry_ok (default true — test helpers
untouched); the driver excludes dep files, so a dependency's fn main is
never the entry.
Verified end to end against local file:// remotes: cold fetch + lock; `use
niceframework` + `use niceframework/strutil` build and run; offline rebuild
with the remote deleted; moved tag -> cold rebuild stays at the locked SHA;
--update-deps follows the tag and rewrites the lock; cache/lock drift,
transitive [deps], and name collision each produce their named diagnostic;
the dep's fn main (returning 99) never becomes the entry. woc-test 540/0;
oop-e2e 87/0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The manifest grows [deps]: `name = { git = "...", rev = "..." }` — a
one-line inline table accepted ONLY under [deps] (a tiny scanner, not
split-on-comma: URLs may contain any character). git+rev both mandatory and
non-empty; duplicate dep names, unknown table keys, unquoted values, and
inline tables outside [deps] each keep/get a named diagnostic. Parse-only:
no fetch yet (Task 2).
Verified: well-formed parses; missing-rev / bare-value / outside-deps each
diagnose; woc-test 540/0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
5 tasks, words-only per house rule, each with its verify step: (1) manifest
grows the [deps] section + one-line inline-table value (only under [deps]);
(2) resolver — git-binary fetch into .wo-deps/, wo.lock pinning, warm-path
offline guarantee, drift diagnostic, --update-deps, guard rails (transitive
refusal, non-writeonce dep, name collision WO-E107, all fetch failures
WO-E106); (3) multi-root discovery + module_of prefixing dep roots by dep
name + entry restricted to the app's own files; (4) scripts/deps-accept.sh
gate over file:// remotes (8 checks, network-free) + just recipe; (5) docs
closeout (catalog E106/E107, README deps subsection, board/story/structure).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Brainstorm outcome (forks locked with the developer):
- TLS: proxy-terminated (nginx/caddy gives browsers TLS+ALPN+h2; the
framework speaks HTTP/1.1 behind it) — zero TLS in the toolchain, no
doctrine fight; homegrown TLS refused outright.
- Dependencies: a real mini package manager — wo.toml [deps] with exact-rev
git deps, wo.lock, .wo-deps cache, `use <dep>` as a module root; fetch by
shelling to the git binary (no network code in woc); flat-only v1.
- HTTP/2: v1 is HTTP/1.1 keep-alive; h2c is the parked successor behind
iterations 8/9f/11 (multiplexing needs a scheduler to pay off); the
bytes/buffer type rides with it, not v1.
- Handler model: no function values by doctrine, so Handler/Middleware are
structural interfaces (ICALL dispatch, WO-E205-checked); middleware returns
?Resp and rides the shipped ?T narrowing.
- Incubation: framework at docs/examples/writeonce-framework/, consuming
storefront at docs/examples/web-app/ importing it THROUGH [deps] — the
sample exercises fetch -> lock -> build -> serve -> durable-restart.
- Iteration 10 relationship: service blocks later LOWER ONTO this library.
Files: specs/2026-08-18-web-framework-design.md (A deps normative, B
framework normative, C h2c parked); stories 15-deps-package-manager.md +
16-web-framework.md; roadmap + board rows.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closes json's two documented fidelity limits (iteration 5 strictness):
- field_class gains WOB_FIELD_BOOL (a plain `Bool` field) and
WOB_FIELD_NIL_BOOL (a `?Bool`: WO_NIL_SCALAR nil + bool encoding) — the
kind byte alone cannot tell a Bool slot from an Int slot, so the metadata
carries it. Emitter writes them (field_class_meta); loader whitelists
them; json.c encodes `true`/`false` (and `null` for a ?Bool nil), decode's
null/omitted-key pre-write covers NIL_BOOL.
- A JSON number with a fraction or exponent is MALFORMED for an Int field:
the checked decode (`json.decode(t) as T`) yields nil for the whole
document instead of silently truncating 3.7 to 3 — the language has no
float, and corrupting data quietly was the one thing a "checked decode"
must never do. Floats stay representable through a raw `json.Value` field.
- corpus: run/json-bool-fidelity pins the round-trip (true/false both ways,
?Bool null both ways, fraction AND exponent rejected).
- Board's two known-gap entries struck; format doc's field_class marker list
extended.
Verified: oop-e2e 87/0; runtime test + test-iso OK; woc-test 540/0;
log-watcher 7/0; employee 8/0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The verdict table's reject half is enforced: a Haxe habit fails loudly at
its own position with the doctrine reason, instead of a generic syntax
error — or, worst, compiling clean: `return super.f()` used to exit 0 (the
unresolved ident placeholder swallowed it).
- parser.ml: doctrine_reject_reason maps each rejected word to its spec
reason (inheritance quartet -> principle 4; cast; Dynamic/untyped ->
principle 13; macro; extern -> principle 10; operator). Fired at three
chokepoints: `class B extends A` (with skip-to-brace recovery so the body
still parses), an expression head (`super`, `cast 3`, `untyped x`), and a
top-level declaration head (`macro fn`, `extern fn`).
- types.ml: `Dynamic`/`untyped` as a TYPE name keep their WO-E225 site but
carry the doctrine message.
- corpus: compile-fail/{reject-inheritance,reject-cast,reject-dynamic}.
- catalog WO-E105 row; plan 8 Task 8 reject half ticked (#if still open);
board updated.
Verified: woc-test 540/0 + test_diag 14/0; oop-e2e 86/0; log-watcher 7/0;
employee 8/0; legit identifiers (`extended`) untouched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The hybrid-boundary inversion is closed: a statically provable interface
violation now fails at COMPILE time instead of reaching wovm as an ICALL
that traps WO_T_BOUNDS at runtime.
- types.ml class_satisfies: the same rule emit.ml's `satisfies` builds
vtable rows from (instance method with matching name + parameter count
for every interface method; `static fn` never satisfies) — one rule, two
consumers, so the check and the vtable can never disagree.
- check_iface_boundary fires wherever a confidently class-typed value flows
into an interface-typed slot: call arguments against the callee's declared
parameters (free fns, methods off confident receivers, interface-method
sigs, statics — resolved exactly as confident_typ resolves returns),
annotated `let`s, and `return`s. Silent when underivable.
- The same per-argument pass extends the ?T boundary to CALL ARGUMENTS
(the previous slice covered stores/returns/operands): nil into a
non-nullable parameter is WO-E212, an unnarrowed ?T argument is WO-E211.
- tests/corpus/trap/unsatisfied-interface -> compile-fail/ with
fixture.code WO-E205, per the fixture's own standing instruction; its
header comment rewritten to the wired reality.
- The new arg checks caught a real mistyped signature in the sample:
log-watcher's rpc_error/rpc_result/call_tool declared `id: json.Value`
while every caller legitimately passes nil (JSON-RPC id-absent) — now
`?json.Value`; dispatch/call_tool/cron-row sites moved to the
bind-then-narrow idiom (including an `or`-guard narrowing:
`if spath == nil or spat == nil { return }`).
- Catalog: E205 gains its main-table row; the "owed gap" section is
rewritten as closed. Board known-gap struck through.
Verified: woc-test 540/0 + test_diag 14/0; oop-e2e 83/0 (fixture now
compile-fail, satisfying-class negative probe compiles clean); oop-accept
ALL MET; log-watcher 7/0; employee 8/0; gc-cycle clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
- runtime/src/CODE-LOGIC.md: gc.c row describes the tri-color mark-sweep
(traced list, snapshot roots, Yuasa barrier, budgeted slices) instead of
RC + Bacon-Rajan; obj.c row gains the traced-list/may-gcref notes; main.c
row gains the post-exit pump.
- compiler/src/CODE-LOGIC.md: pipeline diagram gains gcinfer.ml between
types and owner; the owner-tables section drops rc sites and names the
inference pass as the source of GC-ness.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The spec's §8 migration table, applied:
- 00-principles.md P3: "@gc is a per-class opt-in, reference-counted" ->
GC-ness is inferred; incremental per-shard mark-sweep in budgeted slices;
still no global pause by construction.
- OOP spec: decision-table GC row -> inferred (hybrid rule named); §3 rule 5
-> traced classes alias freely, which classes is inferred; §4 memory model
-> the RC + Bacon-Rajan paragraph replaced by tracing (snapshot roots,
Yuasa barrier, born-black, budgeted slices); header rc comment -> union'd
sweep link; mixing rule restated for tracing.
- 00-wob-format.md: header says version 4; opcodes 27-28 -> reserved (loader
rejects); the owned-temporary rule's @gc exclusion restated for tracing.
- 08-builtin-surface.md: the push RC_INC special case and the set(m,k,v)
retention gap DELETED — neither exists without RC; the corpus cycle is
collected by tracing.
- story 07b: status -> LANDED 2026-08-18 (with the historical note kept);
board: 7b row ✅ (supersedes iteration 2's RC memory model), pending row
removed.
- gc-cycle README: Phase 3 flipped to landed (the ring runs, is reclaimed,
ASan-clean; the ?Node RC_DEC-on-nil trap no longer exists); the barrier
prose corrected to the as-built design (snapshot-at-beginning + deletion
barrier + born-black, not per-slice root re-reads).
- plan 2026-08-18: all checkboxes ticked + a completion banner recording the
four deviations from the plan as written.
(Error catalog was already amended with the keyword-removal commit: WO-E104
added, WO-W201 retired.)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The compiler no longer emits reference-counting ops anywhere, and the format
reserves them. With Phase 3a's collector this completes the runtime half of
iteration 7b: spec success criteria 3 (no RC ops in any image, opcodes
reserved) and 6 (corpus ASan-clean) are met — `just oop-accept` is fully green.
- owner.ml: the rc machinery is deleted outright — rc_site/rc_op types, the
rcs table, fn_rcs/rc_groups/rc_escaped, record_rc, release_gc, gc_escape,
resolve_rc, and the clobber rule (its only consumer was elision). The
`push`-of-a-gc-value RC_INC special case is gone (the bug class cannot recur
without RC). Drop tables (owned + LGc kinds) are untouched — the gc mask is
what feeds the collector's root maps.
- emit.ml: emit_rc, the v_rc view, the escape-acquire anchor, and every
caller deleted; assignment displacing a traced value emits nothing (the VM's
store barrier owns it); scope-ended LGc handles clear their gc-mask bit so
root maps stay precise.
- .wob v4: WOB_VERSION 3 -> 4 in wob.h + emit.ml + disasm.ml + the runner's
loader battery; opcodes 27-28 removed from the enum/jump table/interpreter
and REJECTED by the loader like any unknown opcode.
- dump.ml: the == RC == owner-dump section is gone; 6 goldens re-blessed
(owner dumps lose the section, elision.wo's bc dump loses its RC ops).
- runner.ml: rc-table/ELIDED assertions deleted; the elision test now asserts
the WHOLE image contains no RC op; the table-contract sweep asserts rc ops
never appear.
- test_unwind.c: the rc-opcodes test becomes two — the loader rejects reserved
opcode 27, and an abandoned traced instance is freed by rt_destroy
(ASan-proven).
Verified: woc-test 540/0 + test_diag 14/0; runtime test + test-iso all suites
ASan/UBSan (test_unwind 12/0); cli_smoke; oop-e2e 79/0 (v4 images end to end);
employee 8/0; log-watcher 7/0; ring runs + reclaimed (freed=3) with zero RC
ops in its image; `just oop-accept` ALL CRITERIA MET.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reference counting and Bacon-Rajan trial deletion are gone from the runtime.
Traced (inferred-gc) objects now die only by the collector; owned values keep
deterministic drops exactly as before.
- wo_hdr: rc retired; borrow and the freed 4 bytes become a union — non-traced
values keep the borrow word, traced objects use the 8 bytes as the intrusive
sweep-list link. Header stays exactly 16 bytes. WHITE is now the all-zero
color (allocations born white by memset); WO_F_BUF retired.
- gc.c rewritten: snapshot-at-beginning tri-color mark-sweep. Roots (frames'
gc+owned masks) shaded atomically at cycle start; Yuasa deletion barrier
shades the OLD target of every gcref edge deleted while marking (SETF
overwrites + every owned-death path, which all funnel through wo_drop_kind's
GCREF case); allocations mid-cycle born black. Mark AND sweep budgeted
(WO_GC_BUDGET objects/slice), sweep resumes via a cursor; gray-worklist OOM
degrades to a blacken-all cycle (frees nothing, never wrong). Owned interiors
walked eagerly (single-owner trees), pruned by a per-class may-gcref bit
computed at rt_init (fixpoint over kinds + v2 field_class/field_elem;
conservative when metadata is absent).
- vm.c: safepoints at NEW (the heap-goal trigger), CALL, and backward JMP;
root scan follows vm_unwind's governing-pc convention. Unwind's gc-mask
branch just nulls the register. RC_INC/RC_DEC are accepted as no-ops until
the emitter stops producing them (next commit) — which also deletes the old
RC_DEC-on-nil trap that broke `?Node` gcref field stores.
- main.c pump: post-exit, a rootless cycle frees everything unreachable in
budgeted slices; the trace line moved into wo_gc_slice (one format for pump
and in-program slices). rt_destroy frees traced remnants (trap paths, tests).
- WO_GC_GOAL joins WO_GC_BUDGET/WO_GC_TRACE as an rt-owned knob (default 256
KiB; a tiny goal forces mid-program cycles for testing).
- tests: test_cycle.c rewritten (abandoned cycle freed, rooted cycle survives,
slices bounded, cycle-through-multi, repeated-cycle leak-freedom, and the
spec's load-bearing DELETION-BARRIER test: an object hidden behind a black
object mid-mark must survive). test_rc.c re-pinned to owned drops + the
owned/traced boundary; test_obj.c asserts tracked-white-linked instead of
rc=1.
Verified: make test + test-iso (all suites, ASan/UBSan; test_cycle 42/0,
test_rc 14/0) + cli_smoke; oop-e2e 79/0 (gc corpus traces unchanged: the new
slice math reproduces steps=1/freed=2 and steps=2/freed=4); employee 8/0;
log-watcher 7/0. THE RING RUNS: docs/examples/gc-cycle prints
`ring a -> b -> c -> a`, is reclaimed post-exit (freed=3 remaining=0), is ASan
clean, and survives an in-program cycle while rooted (WO_GC_GOAL=64: mid-run
slice frees 0, post-exit frees 3).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`@gc` is no longer part of the language: a developer never writes or mentions
it. GC-ness is decided entirely by inference (structural cycles + demand
promotion), which the earlier 7b commits made complete and precise.
- parser: `@gc` on a class is now WO-E104 ("GC-ness is inferred; run
`woc --dump-gc`. Remove it."). `is_gc` stays false; the class classifies by
inference. No `.wo` in the repo carries `@gc` anymore.
- types.ml: retired the WO-W201 machinery (suggest_gc_annotation,
has_recursive_structure(_type), has_unique_field, gc_suggestion_code) — it
suggested `@gc`, now obsolete since inference traces exactly those classes.
- runner.ml: deleted the 8 WO-W201 gc-suggestion test blocks; the @gc-exemption
test's `Cache` is made self-referential so inference classifies it gc without
an annotation.
- fixtures: dropped `@gc` from rc.wo (Cache demand-promotes via its escape),
elision.wo (Cache given a self-ref to stay structurally gc for the rc-elision
dump), pricing-demo.wo (PriceCache doesn't escape -> now owned), and the
abandoned-cycle/budget-steps corpus (Node is structurally gc). rc.wo keeps a
placeholder comment line so its line-indexed rc assertions hold. Goldens
re-blessed.
- docs: error catalog gains WO-E104 and marks WO-W201 retired; gc-cycle README
records the keyword removal.
Verified: woc-test 553/0 (was 566 minus the 13 retired WO-W201 checks),
test_diag 14/0, oop-e2e 79/0, employee 8/0, log-watcher 7/0. `git grep '@gc'`
finds only comments — success criterion 1 met.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Wire Gcinfer.infer into runner.ml's owner_str/emit_str so the unit tests
classify GC-ness identically to the driver (prerequisite for removing @gc: its
tests read the golden .wo through the library).
That exposed owner-err/borrow-escape.wo, which tested WO-E304 on *class*
escapes — now legally demand-promoted. Retargeted it to CONTAINER (`multi Text`)
escapes, which are owned and never promoted, so it still exercises the three
WO-E304 shapes (stored-in-field, returned, moved-to-take). Assertion positions
+ golden re-blessed.
Verified: woc-test 566/0, test_diag 14/0, oop-e2e 79/0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The escape hook promoted the borrow-root local's class, so `return h.box`
over-promoted the container `Holder` alongside `Box`. Now `owner.ml`'s
`transfer` passes the escaping place's type (`place_ty p`) as `~promote_class`,
so only the value that actually escapes is promoted.
- escape: takes ~promote_class; records it in collect mode, else reports WO-E304.
- borrow-escape.wo now promotes only Box (was Box + Holder); rc.wo sans @gc
still promotes Cache.
Verified: woc-test 566/0, test_diag 14/0, oop-e2e 79/0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Extract the structural+demand classification out of main.ml's typecheck_all
into `Gcinfer.infer : parsed -> symbols -> symbols`, so a single library entry
point runs the whole pass. The driver calls it once (before typecheck); the
unit-test helpers can call the same function, so is_gc_class classifies
identically in the binary and in tests (prerequisite for removing the @gc
keyword, whose tests read the golden fixtures through the library directly).
- dune: gcinfer moved after owner (it now runs ownership in collect mode).
- main.ml typecheck_all: the split structural-inject + post-typecheck demand
loop become one `Gcinfer.infer parsed syms` call.
- Documented the known demand-promotion imprecision (promotes the escaping root
local's class, over-promoting the container) to refine with Phase 3.
Behavior-neutral: woc-test 566/0, test_diag 14/0, oop-e2e 79/0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Structural inference (2a) covers cyclic classes; this adds the DEMAND half for
the acyclic-but-aliased case, so @gc is now redundant everywhere.
- owner.ml: a `promote` sink on ctx. In collect mode, a class value that would
raise WO-E304 (escape) records its class instead of erroring — because a
class that MUST escape cannot be owned (second-class borrows can't be stored
or returned), so it must be traced. `class_of_ty` extracts the class from the
escaping local's type. analyze/analyze_fn take ?promote.
- main.ml typecheck_all: after structural injection, a fixpoint runs ownership
in collect mode over every file, unioning promotions into syms.traced (and
every module table), before owner/emit see it. Terminates (promotions only
grow, bounded by class count).
- gcinfer.render_final: --dump-gc now reads the authoritative is_gc_class
(structural + demand + the remaining @gc bridge), with the reason.
Effect: a class that escapes is inferred `gc` with no annotation — e.g. `Cache`
(rc.wo sans @gc) shows `gc (alias escape (demand))`; `Box` returned out of
`leak` is promoted and the program is valid. No false positives: employee's
Department/Employee stay owned; the 566 goldens + 14 test_diag unchanged.
Corpus: compile-fail/borrow-escape-return reclassified to run/ (prints 1) —
returning a borrowed class is now legal under demand promotion; the fixture
encoded pre-7b behavior.
Verified: woc-test 566/0 + test_diag 14/0; oop-e2e 79/0; employee 8/0;
log-watcher 7/0.
NOT in this slice: removing the `@gc` KEYWORD (parser rejection + rewriting the
RC/@gc golden + test_diag assertions + moving the inference injection into the
library so unit tests see it) — coupled to Phase 3, which deletes the RC
machinery those tests cover. The ring still needs Phase 3 to RUN (nullable
`?Node` gcref path).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
GC-ness now comes from the inference pass, not only the annotation. A class is
traced if the structural SCC put it in `syms.traced`, OR (temporary bridge
until demand-promotion lands) it still carries `@gc`.
- Types.symbols gains a `traced : StringSet.t`; is_gc_class reads it (union'd
with the surviving @gc annotation). All symbols literals + both merges carry
the field.
- typecheck_all injects the classification once (Gcinfer.classify -> traced)
into the merged table AND every module table, before typecheck/owner/emit.
- emit.ml routes the class gc-flag and the union/drop decision through
is_gc_class instead of the raw `.is_gc`, so structurally-inferred gc classes
get the runtime flag. Field-kind derivation already routed through is_gc_class.
- gcinfer.traced_names exposes the traced set for injection.
Effect: docs/examples/gc-cycle now COMPILES with no annotation (the WO-E301
use-after-move at the ring-closing store is gone) — traced classes alias
freely. Bytecode is byte-identical to writing `@gc class Node`.
Verified: woc-test 566/0 (goldens unchanged — every current @gc class stays gc
via the annotation branch, and no golden has a structural-gc-non-annotated
class); oop-e2e 79/0 (gc corpus green).
Not in this slice: demand-promotion (the acyclic-aliased PriceCache case still
needs the @gc bridge) and @gc-in-source-as-error (Phase 2b); the ring RUNNING
(the RC runtime doesn't implement nullable-gcref `?Node` fields — Phase 3).
WO-W201 still fires on gc-cycle (retired in Phase 4).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Structural half of the inference pass, additive — it backs `woc --dump-gc`
and does NOT yet feed field-kind derivation (that is Phase 2 at the
Types.is_gc_class seam), so no emitted bytecode or golden changes.
- compiler/src/gcinfer.ml: build the class-reference graph (edges from
Scalar/Multi/Map fields, unwrapping ?; ref and backlink contribute NO edge),
run Tarjan SCC, classify any class in a non-trivial SCC or with a self-loop
as `gc`, else `owned`; carry a cycle-path reason.
- --dump-gc mode in bin/main.ml (mirrors --dump-owner) + usage line + dune.
Verified:
- docs/examples/gc-cycle -> `Node gc (cycle Node -> Node)`, `Segment owned`.
- docs/examples/employee -> Department/Employee both `owned` (ref/backlink
make no edge, so no false cycle) — the load-bearing correctness case.
- just woc-test 566/0 (goldens untouched, build clean both flavors).
Remaining Phase 1: a --dump-gc golden fixture (deferred — verified manually to
avoid golden-harness churn this slice). Phases 2-4 per the plan.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
Master now reflects only the current woc/wovm project. The Rust `wo` runtime
was the prior, abandoned architecture; it is fully independent of the woc/wovm
stack (dune + make, no Cargo dependency), so it lifts out cleanly. Recoverable
via git history.
Removed:
- crates/ (29 files) + Cargo.toml + Cargo.lock — the Rust runtime workspace
- prototypes/ — wo-rt-c (a stale duplicate of runtime/) + wo-db C++ ref
- justfile: the rt-c-demo / rt-c-bench recipes (drove prototypes/wo-rt-c)
- docs/plan/05..16 (11 Rust engineering plans) + docs/plan/done/ (4 Rust
Stage-2 done plans)
- docs/runtime/ (11): the old runtime overview + 7-phase DB design series +
async/fibers/gc/surreal concept essays
- docs/cm.md — legacy scratch note
Kept: compiler/, runtime/ (C VM), database/, the current-track docs
(stories, superpowers, plan/{oop-vm,compiler,exploration}, examples), the
discarded/learnings registers, and the syscall/postgres/assembly/c-runtime
studies. Follow-up commits fix the status board, project-structure doc, and
any dangling links to the removed docs.
Verified: woc + wovm still build; woc/wovm --version green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The ##ui/.htmlx/LIVE track (plan 7) was removed as stale, so drop its dangling
references in 08-project-structure.md: the `ws, sub, htmlx, assets (plan 7)`
runtime/src line, the `06-ui-live` oop-vm contract entry, the `plan 7
UI/.htmlx/LIVE` server-track step, and `ui` in the build rule-of-thumb.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`client/` (wo-live.js live-patch runtime) never existed and was tied to the
removed plan 7 (##ui/.htmlx UI track). Dropped its tree entry, its proof-layer
section header + bullet, and its lifecycle-table mention (plans 4–7 -> 4–6,
http/ui -> http).
Left as-is: the Rust-track phase prose still names a `ui` crate scaffold and
plan-7 sequencing — that describes the maintained (non-advancing) Rust
roadmap, not a removed file.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Cleanup off master. None of this is referenced by the current woc/wovm
toolchain or the maintained Rust crates; it belongs to abandoned/scratch
state.
Removed:
- .planboard/ + .planboard.json — planboard task-tracking state for the
haxe-parity plan (tooling scratch)
- prompt.md — a stray one-line note about the old runtime's 13c task
- infra/ (deploy.sh, setup.sh, sync.sh) — deploy scripts for the old Rust
runtime (cargo build -p wo-rt, scp to writeonce.de, nginx/SSL/systemd)
- static/ + templates/ — the v1 blog's .htmlx frontend assets (svg/css +
about/article/home/layout templates); the UI track that used them was
already removed as stale
- content/ data/ wo-data/ — empty untracked v1 runtime dirs
Updated docs/08-project-structure.md: dropped the "v1 blog operating assets"
tree line and section. (cm.md keeps a historical mention of infra/sync.sh as
a legacy note — left as-is.)
Nothing kept references the removed paths (verified tree-wide; crates/rt hits
were `'static` lifetimes, not file reads).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The manifest pinned `[build] runtime = "../../../runtime/wovm"`, a repo-only
relative path that overrides woc's runtime resolution — so `woc <copied-dir>`
failed off-repo (e.g. an installed toolchain on a test server) with "runtime
binary not found".
- Drop the [build] section: the project no longer hardcodes a machine path, so
an installed `woc` self-locates `wovm` beside its own binary.
- The module justfile's `build` recipe now sets WO_RUNTIME=<repo>/runtime/wovm
so the in-repo build still uses the freshly built VM.
- Acceptance is unaffected (it compiles via `woc --emit` + an explicit $WOVM,
never the manifest [build] key).
Verified: just log-watcher::build OK; just log-watcher 7/0; and building a
copied tree with the installed-layout woc from an unrelated cwd self-locates
the sibling wovm and produces a runnable binary.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Close the 3 gaps between "builds in the repo" and "installs from a tarball
like Go", so writeonce can ship to other developers.
- VERSION file at repo root single-sources the toolchain version (0.1.0).
- `woc version` -> "writeonce 0.1.0 linux/amd64"; `wovm --version` -> "wovm
0.1.0" (stamped by the Makefile from VERSION; --version handled only in the
plain-wovm path so a built app never shadows its own `version` arg).
- wo.toml `[runtime] wo = ">= X.Y"` is now ENFORCED: woc refuses a project
requiring a newer toolchain than itself (>= and bare version parsed;
unknown operators accepted forward-compatibly). Was parsed-and-ignored.
- woc self-locates wovm: --runtime > [build] runtime > $WO_RUNTIME > a `wovm`
beside the woc binary (Sys.executable_name) > runtime/wovm rel CWD. An
installed woc in <prefix>/bin finds its sibling wovm from any cwd.
- `just dist` (scripts/mkdist.sh) packages writeonce-<ver>-linux-amd64.tar.gz,
Go-shaped (archive root writeonce/, bin/{woc,wovm}, README, VERSION), with a
drift guard asserting VERSION == woc == wovm. dist/ gitignored.
- `just install-accept` (scripts/install-accept.sh) is the gate: extract, PATH,
version, build+run a project from an unrelated cwd, constraint refusal — 6/0.
Verified: install-accept 6/0; woc-test 565/0; wovm suites + cli_smoke;
log-watcher 7/0. Linux-amd64 only (a cross matrix is future work).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Analyzed the full 131-file docs tree (4 parallel classifiers) against the
shipped woc/wovm toolchain. Removed 21 stale docs, kept all intentional
history (Rust-track plans/done, the runtime/database design series cited by
current specs, syscall/postgres/assembly/c-runtime studies, discarded/
learnings). Deleted:
- old-runtime "front door": writeonce-pl.md, runtime/wo-language.md
(pitched the Rust wo runtime -- REST/LiveView/SQL+Cypher -- as the current
language; contradicted the new README)
- v1 design set: 02-recovery, 03-data, 04-ui, 05-datalayer,
06-markdown-render, 07-ssl; runtime/database/05-go-sdk
- future-scope/ai-agents-content-management (unfinished old-runtime CMS)
- the ##ui/.htmlx LiveView frontend track (product decision to abandon):
9 plan/exploration/ui/*, plan/14-mvc-ui-implementation,
superpowers/plans/2026-08-01-ui-htmlx-live; 13d pricing-UI board row
Tree left link-clean: 46 dead links to the removed docs neutralized to plain
text or deleted as pure see-also bullets across 20 kept docs; whole-tree
link-resolving scan reports zero links to any deleted file. Removal recorded
in discarded.md; board Frontend section + project-structure tree updated.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- README.md now IS the getting-started page (moved from
docs/writeonce.md; single source, no duplication): current
woc -> .wob -> wovm toolchain, system requirements, build, hello-
world, language + stdlib, embedded database, samples, roadmap
- deletes the old README's Rust `wo` runtime pitch (cargo run, axum
REST, Postgres mirror, blog/ecommerce) — that runtime is not
advancing and no longer the project's front door
- content unchanged from the verified doc (hello-world + switch
compiled live before the prior commit)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- docs/writeonce.md — external-developer front door for the current
woc -> .wob -> wovm toolchain (NOT the stale root README's old Rust
wo runtime): what writeonce is, system requirements, how to build
the toolchain, hello-world + the two build paths, language surface,
stdlib, the embedded database, project/manifest layout, samples
- shipped-only by decision: every feature described compiles and runs
today; hello-world + switch snippet verified live before commit;
employee/log-watcher cited as the working acceptance samples
- unshipped roadmap (aggregates, HTTP service, concurrency/fibers,
cross-program attach + keypair auth, blue-green, @derive) kept in a
clearly-separated Roadmap section, plus named current limits (net
TCP-only, proc.run no timeout, no stdin/stdout, no FFI)
- note: root README.md is stale (documents the older Rust axum/REST
runtime, no mention of woc/wovm); left untouched, flagged for the
developer
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- frames a writeonce port of ~/projects/skillhost (C++ MCP host that
links libllama in-process, discovers filesystem skills, runs their
scripts confined) as the sample that drives host capabilities into
the open — the way log-watcher drove the systems stdlib
- names each gap as a candidate iteration (surveyed 2026-08-16 vs the
running compiler + skillhost source):
- Blocker A: in-process native-lib FFI (no FFI today) — fork:
FFI-as-language vs out-of-process model driver over proc/net+json
(llama-server, needs nothing new); leaning out-of-process
- Blocker B: stdio transport — no stdin/stdout builtins; port uses
a TCP socket meanwhile; io.stdin_read/stdout_write a candidate
- Blocker C: bounded/killable subprocess — proc.run has no timeout/
signal/process-group kill; smallest + most broadly useful, do 1st
- partials: recursive fs walk, exec-bit check, symlink-resolving
confinement (realpath) — one small fs-metadata iteration
- records what is already expressible (catalog via @table/9g skill-
catalog, discovery, frontmatter text-parse, config, single-thread
serve, context-gate arithmetic — no VRAM query needed)
- out of scope: in-process libllama/CUDA, VRAM introspection, exact
sampler chain / per-turn memory clear
- roadmap + board rows added
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- method: an embedded-SQL app is a grammar corpus; catalogue what it
actually uses and add only that, translating its statements 1:1 as
the acceptance (the postgres/System.Linq reference pattern applied to
a whole application)
- corpus #1 = ~/projects/skillhost (C++ MCP host, embedded SQLite skill
catalog): surveyed, entire SQL footprint is one file — 1 table, a
single-row parameterized INSERT, 4 SELECTs. 3 of 5 statements already
run on the 9b surface (insert, where name==?, order by name; PK ≈
@unique). Exactly 2 are the real gap:
- whole-query `count` (group-free; the degenerate aggregate, NOT
the parked group-by)
- correlated `not exists` subquery (skillhost's roots-of-the-tree)
- notable finding: skillhost's NOT EXISTS is naturally a `backlink`
emptiness in writeonce (children backlink + len==0), so the corpus
may be fully expressible once len(query) is confirmed — the iteration
may collapse to "confirm len(query) + add exists"; forks record this
- explicitly parks everything skillhost does NOT use (join/having/
offset/distinct/CTE/window/union/upsert/returning/json/fts/triggers)
and the full group-by; each enters only when a corpus demands it
- acceptance: docs/examples/skill-catalog mirroring skillhost's schema
+ its 5 catalog ops as writeonce translations
- roadmap + board rows added
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- FK restrict: deleting a row a non-nullable `ref` still points at traps
WO_T_FK (11), catchable. The compiler now records a `ref` field's
target class in the class-table field_class metadata; the engine
(wo_row_has_referrers) scans referencing scalar columns before a
delete. Correctness-first full scan; the backlink-index optimization
is recorded for later
- docs/examples/employee now COMPILES AND RUNS all six modes against a
WAL-durable database: seed (+@unique trap across restart), report
(per-dept aggregates + payroll), staff (unique probe + backlink +
ref nav), raise (update-through-row), drop (FK restrict), and
persistence via replay
- group-by SYNTAX parked to a future iteration (user decision): the
report mode is hand-rolled from the shipped primitives meanwhile
(same numbers). "table relations and FK" is complete
- scripts/employee-accept.sh (8 checks) + a `just employee` module;
manifest parser tolerates iteration 9c's [share]/[[share.clients]]
sections so `woc .` builds the sample on this branch
- fixtures trap/db-fk-restrict (code 11) + run/db-fk-restrict-catch;
oop-e2e 79/0, woc-test 566/0, 15 runtime suites, log-watcher 7/0,
employee-accept 8/0
- 9b story + status board updated
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- `e.field = v` where e is a table row lowers to DB_UPDATE_FIELD
(class, id, field, value) — the row's indexes maintained at the choke
point; heap-object field assignment still emits SETF unchanged
- `delete <row>` expression: DB_DELETE(class, id), yields the id so it
composes in `try delete x catch (e) nil` (restrict/trap surfaces
catchably); Delete AST node threaded through type/owner/dump/emit
- disassembly-caught bug fixed: in tail position dst == the builtin
window's first reg, so moving the id into dst clobbered the class id
— reserve dst past the window (the emit_ctor guard)
- run/db-update-delete fixture; oop-e2e up, woc-test 566/0,
log-watcher 7/0
- employee seed/list/staff/raise/drop now compile+run; only `report`
(group-by aggregation + projection record) remains
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>