Commit graph

156 commits

Author SHA1 Message Date
f88aa11cef feat(runtime): fiber run queue + reduction budget (arc T2)
- fiber states (RUNNABLE/PARKED/DONE), intrusive FIFO run queue,
  wo_vm_spawn_fiber (calloc'd context, frame 0 set up like wo_vm_call)
- reduction budget: WO_REDUCTIONS (default 4000), checked at loop
  BACK-EDGES AFTER the jump lands so the saved pc is the loop head —
  a pre-instruction save at budget 1 re-executes the jump into the
  same decrement and livelocks (found by reasoning, pinned by the
  budget-1 test; deviation from the spec's three-site wording,
  recorded in the yield macro's comment)
- FIBER_DONE: main returning ends the program and reaps every
  remaining fiber through vm_unwind (drop maps run); a spawned fiber
  ending frees silently; its return value is discarded by contract
- TRAPF: an uncaught trap in a spawned fiber kills that fiber ALONE
  (stderr report, program lives); in main it stays the program's death
- WO_SYS_STOPPED reaps all fibers wherever it lands (main unlinked
  from the queue and unwound if a spawned fiber caught the stop)
- vm_gc_roots walks the live fiber plus every queued one
- test_fiber (45 checks, ASan): EXACT round-robin interleave at budget
  1 across three fibers pushing tags into one shared multi;
  main-return reaps a spinning fiber holding an owned Big (ASan proves
  the free); a DIV0 fiber dies alone, main answers 0
- full battery green: wovm-test, oop-e2e 89/0, woc-test, log-watcher
  7/0, employee 8/0, web-app 21/0, deps-accept 8/0 (scheduler dormant
  = one branch per back-edge)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 07:03:40 +02:00
58e37cc19e refactor(runtime): extract the fiber context from wo_vm (arc T1)
- wo_fiber = the interpreter state wo_vm held inline: register window,
  frame stack, catch stack, caught-error slot; wo_vm keeps module,
  runtime, the embedded fiber 0 (main) and the cur pointer every
  interpreter access now reads through
- vm_gc_roots split into a per-fiber walker + the all-fibers caller
  (one fiber today; the loop is where stage 1 T2 adds the rest)
- PURE refactor, no functional change to hide behind: full battery
  byte-identical — wovm-test (test, test-iso, cli_smoke) green,
  oop-e2e 89/0, woc-test green, log-watcher 7/0, employee 8/0,
  web-app 21/0, deps-accept 8/0
- plan: docs/superpowers/plans/2026-08-20-shard-fiber-arc.md task 1
  (plan/spec docs live on branch language-surface-strictness)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 06:40:58 +02:00
3ad7e8b3dc docs: board doctrine notes the stories-folder exception
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 04:33:48 +02:00
2a4c304378 docs: stories foldered by state — done/ and refine/
- done/ (11): 1, 2, 3, 4, 6, 7, 7b, 9, 9b, 15, 16 — landed iterations
  (9/9b remainders live in the post-12 drain list, not in the files)
- refine/ (8): 9c, 9d, 9e, 9f, 9g, 11, 13, 14 — everything marked
  "no spec yet / brainstorm before planning"
- root keeps: 00-story (index), 05 (partial, plan 8 open), 8/10/12
  (specs or plans exist), 17 (parked, spec+plan approved), 18 (next)
- every cross-reference re-pathed and VERIFIED resolving: board, specs,
  plans, employee-list README, story table, intra-story links (moved
  files' relative links deepened one level; done/7b's 9e pointer now
  crosses to refine/)
- pre-existing dead link noted, not touched: refine/11-fibers.md points
  at docs/plan/exploration/fibers/00-fibers.md which does not exist
  (predates the move)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 04:33:24 +02:00
ba428c362c docs: story iterations table re-sequenced by dependency
- 00-story.md iterations table rows reordered into implementation order
  with a Seq column; # stays an immutable ID (files never renumber —
  every board/spec/plan references by number)
- order: 1-7b, 9, 9b, 15, 16 (landed, landing order) -> 18 NEXT (spec
  approved) -> 9c -> 9d -> 9e -> 8 -> 9f -> 11 -> 10 -> 12 -> 9g -> 14
  -> 13; 17 parked row at the end, slots anywhere after 16 on directive
- story note + board implementation-order list synced (18 inserted as
  item 2 after the parked-17 note; 9g now explicitly before 14; list
  renumbered)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 04:25:43 +02:00
7362915b50 docs: dependency graph rebuilt — 17 edges, concurrency chain, drain
- re-analyzed every story/spec/plan markdown for dependency statements
- added: iteration 17's OUTGOING edge (framework internal/ reorg + check
  mode, WO-E108/E109 reserved); 1-6 foundation anchor; 9b -> 10 ("service
  blocks want query results"); 14's gap fan-out node; post-12 parked
  drain cluster with dashed scope-directive edges (13 + drain are
  directive-held, not technically blocked)
- new graph 2: the concurrency chain — 8/9f/11 and EVERYTHING they gate:
  keep-alive parking retirement, h2c, body/response streaming + commit
  point, cancellation, pub/sub+WS, 9c's rejected async-statement
  alternative, schedulable idle timeouts, fiber jobs (+18),
  cancellation->rollback (+18+cancel)
- graph 3 notes 9d's keypair crypto is its own C impl, neither waits for
  nor feeds the crypto-fork gate; storage-integration rows point at
  their real owners (future migrations story, 9-series query surface)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 04:19:13 +02:00
4c4aafc71e docs: NEXT PLAN reflects approved 18 spec + graph link
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 04:14:10 +02:00
6a0ce35edd docs: dependency graphs for iterations + framework features
- docs/00-dependency-graph.md: three mermaid graphs — story iterations
  (hard edges only; 18 is the only spec-approved node with all
  prerequisites green), framework v1 ledger items (three recurring
  gates: net seams, crypto fork, iterations 8/11; nine slices startable
  today in any order), framework v2 internals (cache/flags independent,
  transaction{} is the critical path, jobs compose on it)
- maintenance rule: node classes update in the same change as board rows
- board links the graph up top; spec 18 banner -> APPROVED, plan next

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 04:13:49 +02:00
234bd43466 docs: framework v1/v2 split + per-feature status ledger
- framework README core checklist expanded into the v1 STATUS LEDGER:
  seven categories (transport, routing, request/response, context &
  middleware, storage integration, security, crypto), every item
  marked done / partial-with-named-gap / candidate / parked-behind-8-11
  / needs-runtime-seam
- verified before labeling: BODY_MAX caps headers AND body (size limits
  done); net has no timeout or unix-socket or peer-address surface
  (runtime seams); language has NO bitwise operators, so SHA/HMAC/CRC32
  must be C runtime builtins or bit ops land first (fork to brainstorm);
  radix routing waits for 9e to measure the linear scan first
- crypto hard stop recorded: HS256 unlocks and nothing past it
- memory-rich features relabeled FRAMEWORK V2 = iteration 18 (story +
  spec banners + board rows); v1 gaps land as slices per the ledger

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 04:07:39 +02:00
24ff960950 docs(spec): iteration 18 — transaction { } + cache/flags/jobs design
- Part A transaction: one wal_commit at block end over the existing
  staged batch; reads see own writes (RAM stays authoritative); trap
  unwinding out = abort (undo list: insert->remove, update/delete->
  pre-image, captured before RAM apply, txn-only cost); try inside
  keeps the block alive; WO-E110 lexical nesting, WO_T_DB dynamic;
  no new opcodes, no .wob bump (internal builtins + catch-frame-shaped
  abort marker); E108/E109 stay reserved for parked 17
- Part B: cache.wo (ttl_ms/cap, lazy time.now-ms expiry, FIFO over LRU
  with the tradeoff stated, Text values via json); flags.wo (@table
  wf_flags, on as Int 0/1 - Bool columns unproven, read-through map,
  set updates table+map); jobs.wo (@table wf_jobs, enqueue composes
  with transaction, JobRunner interface, App.jobs(take r, budget),
  Dispatcher.idle() called post-accept PRE-PARSE - deterministic for
  the SIGKILL durability proof, unlike after-response)
- web-app demo: transactional order+confirm enqueue, GET /jobs count,
  POST /flags/:name with a flag-gated header on the product list
- gate: SIGKILL-after-201/restart/drain proof + flags persistence;
  corpus carries transaction-commit/abort + WO-E110 + cache-ttl
  (stamps injected, no sleeps)
- board row 18 -> spec written, awaiting review

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 04:00:27 +02:00
deb2dc805c docs: iteration 18 — memory-rich features over the embedded DB (no code)
- brainstorm settled four forks same-day: scope = TTL cache + @table
  feature flags + durable @table job queue; jobs = drain-on-request
  (idle server drains nothing, disclosed); transaction { } ships in 18
  (WAL already stages batches, db.c merely commits per statement);
  pub/sub REJECTED until 8/11 (no WebSockets, starvation lesson)
- ground truths verified and recorded: time.now exists, no timers (lazy
  expiry only), accept blocks without timeout, state lives on wired
  instances, wal_append*/wal_commit is the txn seam
- headline: enqueue + business write in ONE commit — outbox dissolved
- draft acceptance incl. SIGKILL/restart transactional-jobs proof
- board row 18 + NEXT PLAN next-step + story roadmap row

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 03:52:59 +02:00
0dd5fda180 feat(framework): multipart/form-data parsing — Part, multipart_parts, part_named
- http/multipart.wo: RFC 7578 whole-body parsing within BODY_MAX —
  boundary from the raw content-type (quoted or bare, key
  case-insensitive), parts split on --boundary, each part = headers,
  blank line, content; filename + per-part content-type kept (lowercased)
- strict malformed-is-nil: no closing --boundary-- marker, a part
  without content-disposition, missing blank line, no boundary param,
  wrong media type — all nil, the caller's 400
- part_named(parts, name): first matching field's content, caller-owned
- web-app CreateProduct now accepts multipart/form/JSON (curl -F shape)
  into the shared insert path
- probe 13/13 + 3x reuse loop (fields, crlf-in-content, quoted boundary,
  file part, zero-part close, five malformed shapes) release + ASan
- gate grows 19 -> 21: multipart create 201, missing closing marker 400
- README: multipart row ✅ (all three body hooks done), limits updated;
  story 16 + board record the landing
- gates: web-app 21/0, oop-e2e 89/0, deps-accept 8/0, log-watcher 7/0,
  employee 8/0, woc-test green

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 03:33:14 +02:00
ed29ccadbc fix(compiler): return of a Text interp of a place handed out the borrow
- emit_return's place test matched only bare Ident/Field/Index, so
  `return "${p.content}"` (p a loop borrow) returned the part's own
  string; the caller's eventual drop freed it under the container —
  arena corruption surfacing two requests later (multipart slice)
- the return test now sees through Interp exactly as copy_place_text
  does (is_borrowed_value_t, container reads excluded); bare
  Ident/Field/Index behavior at return unchanged
- interp-borrowed-field fixture grows the return flavor (fn first),
  50 iterations exact
- gates: oop-e2e 89/0 (ASan stage), woc-test green

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 03:32:31 +02:00
a5826495e9 feat(framework): form-encoded body parsing — media_type + form_values
- media_type(req): content-type lowercased, "; charset=..." stripped,
  "" when absent — the content-negotiation hook
- form_values(req): application/x-www-form-urlencoded body -> decoded
  pairs through the existing query decoder ('+' as space, %XX); nil on
  any other content-type so a JSON body is never misread as a form key
- web-app CreateProduct accepts form OR JSON; shared create_product
  insert path; field/number validation answers 400
- probe 7/7 (plus/pct decode, empty value, case + charset param, json
  and missing content-type nil, empty body, media_type strip) + ASan
- gate grows 17 -> 19: form create 201 with decoded name, non-numeric
  price 400; hit() gains a content-type argument
- README: checklist row form ✅ (multipart stays candidate), limits
  paragraph updated; story 16 + board record the landing
- gates: web-app 19/0, oop-e2e 89/0, deps-accept 8/0, log-watcher 7/0,
  employee 8/0, woc-test green

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 03:21:48 +02:00
e05a27c898 docs: milestone closing line reflects the parked 17
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 03:15:38 +02:00
fb86156d04 feat(framework): auth in core — Bearer/Basic mechanism + principal slot
- http/auth.wo: auth_header (scheme split, case-insensitive, RFC 9110),
  bearer_token, basic_credentials (first-colon split, RFC 7617),
  pure-.wo base64_decode (RFC 4648, strict padding), ct_eq constant-time
  compare (no early exit, both Basic fields always compared)
- req.principal: the blessed "who is this" slot, "" until authenticated;
  Middleware.before now takes mut req so auth can write it
- BearerAuth { token, principal } and BasicAuth { user, pass, realm }
  middlewares; BasicAuth answers the WWW-Authenticate challenge; policy
  (routes/users/secrets) stays app-side on the exposed fns
- web-app dogfoods BearerAuth; its hand-rolled Auth class deleted
- probe matrix 26/26 (RFC 4648 vectors, rfc7617 pair, pass-with-colon,
  bad padding/chars/length, deny paths, challenge header) release+ASan
- gate grows 16 -> 17: wrong bearer token answers 401 over the wire
- README: auth bullet + the core CHECKLIST (done / candidate / parked
  behind 8-11 by design); story 16 + board record the landing
- all gates green: web-app 17/0, oop-e2e 89/0, deps-accept 8/0,
  log-watcher 7/0, employee 8/0, woc-test green

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 03:15:16 +02:00
d84b72f0b4 feat(framework): v1 polish — helpers, 405+Allow, HEAD, Logging, set_header
- App.get/post/put/delete_(pattern, take h: Handler) — the take-interface
  shape probe-proven release + ASan before landing; retires plan-16
  deviation 1; delete_ because delete is the query keyword
- dispatch matches path-first: wrong method on a known path answers 405
  with Allow in registration order; unknown path stays 404
- HEAD routed as GET, body suppressed, Content-Length names the body a
  GET would carry (serialize gains head_only)
- Logging middleware (request line to stderr) ships in router/
- set_header(mut r, name, value) — the builder escape hatch
- web-app registers through the helpers (dogfood); README documents all
- gate grows 14 -> 16: 405+Allow, HEAD-vs-GET content-length equality
- all gates green: web-app 16/0, woc-test 540/0, oop-e2e 89/0,
  deps-accept 8/0, log-watcher 7/0, employee 8/0
- board/story: iteration 17 parked (spec+plan ready on library-internal),
  16 carries the v1-polish landing, order list updated

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 03:04:57 +02:00
81a9f882b5 fix(compiler): Text interp of a place crossed let/assign uncopied
- copy_place_text matched only bare Ident/Field/Index, so a Text-typed
  single-segment interpolation ("${r.method}", r a loop borrow) passed
  the place's own register through a binding/assignment boundary — the
  local aliased the row's field and its overwrite freed it
- release-build crash; invisible to ASan (in-arena free, no redzones)
- now asks is_borrowed_value_t && not is_container_read — exactly
  drop_fresh_text's place test; Int segments (fresh int_to_text) and
  container reads (already copies) stay uncopied as before
- pinned by tests/corpus/run/interp-borrowed-field (crashed both
  runtimes before the fix, 50 iterations now exact)
- gates: woc-test 540/0, oop-e2e 89/0 (ASan stage included)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 03:04:43 +02:00
b264c06d5a docs: state why web-framework merges before iteration 17 starts
- NEXT PLAN step 1 now carries the reason: 17's edit targets (framework
  sources, [deps] resolution in main.ml, just web-app gate) exist only on
  the web-framework branch; unmerged start = branch stacked on unreviewed
  branch

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 02:10:32 +02:00
2b5762500b docs: goal shift — log-watcher (met) to web framework as library
- NEXT PLAN rewritten: iteration 17 is the goal slice (merge branch, spec/
  plan, kind = "library", internal/ WO-E1xx, framework reorg, gate list)
- previous NEXT PLAN retitled "Landed 2026-08-15 — the executable milestone";
  all six measured items were already done, board rows were stale
- board row 7: in-progress -> landed 2026-08-15 (ASan-clean, SIGTERM, fd-flat,
  soak, just log-watcher 7/0); iteration 07 story banner updated to match
  its plan doc's done banner
- in-progress table now carries iteration 17 spec/plan
- implementation order re-sequenced for framework goal: 17, 9c/9d, 9e, 8,
  9f, 11 (+ h2c unparks), 10, 12, then 14/9g demoted (skillhost no longer
  the driving workload), 13 + parked drain last
- story notes record the shift and the new order

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 02:04:41 +02:00
d1aab85dbe docs: sequence pending iterations in implementation order
- new "Implementation order (sequenced 2026-08-20)" section in 00-status.md
  pending bucket: 7-finish, 17, 9g, 14, 9c/9d, 9e, 8, 9f, 11, 10, 12,
  13 + parked drain
- forcing rules recorded: 9f after 8+9e; 9c precedes 10; 9d folds into 9c;
  12 after 9+10; 11 rides 8's scheduler; h2c behind 8/9f/11; post-12 park
  directive unchanged
- 9e placed before 8 so restructure/perf work has a signed baseline
- 14 early as next driving workload (stdlib-shaped, shard-independent)
- story 00-story.md notes point at the sequenced list

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 01:50:03 +02:00
3e22e42da5 docs: iteration 17 forks settled — kind key, internal/ rule (no code)
- fork 1: library-ness manifest-declared, kind = "library", default program
- fork 2: privacy = Go internal/ directory rule, named diagnostic at use
- fork 3: dep-boundary-only scope; Go subtree rule recorded as later tightening
- fork 4: lib+bin dual — library default action is check, explicit build works
- Go-inherited rule pinned: internal type in public signature allowed, no check
- impact analysis added: framework loses --emit workaround, plumbing under
  internal/; compiler = two seams (driver kind + dep-use refusal WO-E1xx)
- VM zero impact by construction: no .wob change, libs compile whole-program
  into consumer image, internal modules still emitted (privacy strips nothing)
- GC zero mechanism impact; pinned: inference stays whole-program, app usage
  can promote dep classes, internal/ invisible to gcinfer — intended, not bug
- board + roadmap rows: needs-refinement -> forks settled, spec/plan next

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 01:45:06 +02:00
f430bff5c4 docs: iteration 17 — library projects + dependency privacy (needs refinement)
Brainstorm outcome, deliberately NOT implemented (developer decision: keep
as an iteration needing further refinement). Records:
- the two gaps iterations 15/16 exposed: library-ness is implicit (a
  no-main project fails woc <dir> build mode — the framework is verified
  via an --emit workaround) and the dep boundary leaks internals (pub has
  no dep-private tier: parse_request is as importable as Handler).
- the conventions corpus: Go (package decides program-ness; cmd/;
  internal/ = directory-shaped privacy, zero keywords) vs Rust ([lib]/
  [[bin]] manifest targets; pub(crate)-family keyword visibility). Doctrine
  fit points at Go's shape with an explicit manifest key (writeonce HAS a
  manifest; explicit beats inference in errors).
- four open forks for the spec: kind declaration form; internal/ vs
  pub(lib) vs export-allowlist; dep-boundary-only vs Go's subtree rule;
  lib+bin duality. Draft acceptance criteria; web-app 14/0 as the
  regression gate. Roadmap + board rows added.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 01:23:10 +02:00
d6025e131d docs: iteration 16 closeout (Task 6)
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>
2026-08-19 19:58:13 +02:00
c72b3354ff test: web-app-accept — iteration 16's gate (Task 5)
scripts/web-app-accept.sh: 14 checks, network-free — temp git remote from
the framework dir, file:// substituted into a temp app copy, then fetch +
wo.lock + build; 401-without-token; empty list; 201 create; 409 duplicate
(@unique); 400 malformed json; list/show payloads; 404; 201 order (FK); 409
delete-while-referenced with the server surviving; pipelined keep-alive (2
responses, 1 connection); SIGTERM; WAL restart persistence. Wired as
`just web-app`. 14/0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 19:57:30 +02:00
24c991069d feat: web-app storefront + dep-relative use resolution (iter 16 Task 4)
- 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>
2026-08-19 19:56:18 +02:00
67484f799a feat(framework): router + Handler/Middleware + App (iter 16 Task 3)
- 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>
2026-08-19 19:53:19 +02:00
994d151a44 feat(framework): HTTP/1.1 parse + serialize + serve loop (iter 16 Task 2)
- 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>
2026-08-19 19:50:55 +02:00
20322d4905 feat(examples): framework skeleton + web-app scaffold (iter 16 Task 1)
- 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>
2026-08-19 19:43:19 +02:00
4c75ba4fbd docs: implementation plan for iteration 16 (web framework + web-app)
6 tasks, words-only: (1) framework skeleton — Req/Resp records + builders,
standalone-typechecking project; (2) HTTP/1.1 parse/serialize + keep-alive
serve loop with the try-bounded dispatch seam (400-close on malformed, 500-
survive on handler traps, fd/stop clean); (3) router with :param captures +
Handler/Middleware interfaces + App.serve; (4) web-app storefront —
@table Product/Order, auth middleware, json routes, [deps] manifest carrying
the future GitHub URL; (5) scripts/web-app-accept.sh — temp git remote from
the framework dir, file:// substitution into a temp app copy, full curl
matrix + restart persistence + SIGTERM + opt-in soak, wired as just web-app;
(6) docs closeout. Both enabling risks retired pre-plan (interface-field
dispatch probe; owned-move container fix pinned by run/container-owned-move).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 19:38:21 +02:00
9eb8baef9f fix(compiler): storing an owned value in a container is a MOVE
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>
2026-08-19 19:36:49 +02:00
f6b5333d40 docs: iteration 15 closeout (Task 5)
- 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>
2026-08-19 19:23:18 +02:00
77d387fc0a test: deps-accept — iteration 15's gate (Task 4)
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>
2026-08-19 19:21:32 +02:00
970ae2566c feat(woc): dependency resolver + multi-root builds (iter 15 Tasks 2+3)
[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>
2026-08-19 19:19:57 +02:00
17a493e930 feat(woc): wo.toml [deps] section — inline-table parsing (iter 15 Task 1)
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>
2026-08-19 19:16:06 +02:00
8fcebe60f9 docs: implementation plan for iteration 15 (deps)
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>
2026-08-19 19:11:05 +02:00
976ccda225 docs: web framework + deps design — spec + iterations 15/16
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>
2026-08-19 19:07:16 +02:00
f282451867 feat(json): Bool encodes true/false; fraction/exponent decode fails honestly
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>
2026-08-19 18:04:15 +02:00
a43232dc72 feat(compiler): doctrine reject rows — WO-E105 (iter 5 strictness)
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>
2026-08-19 17:57:36 +02:00
8f6ff1e865 feat(compiler): WO-E205 structural interface satisfaction + call-arg checks
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>
2026-08-19 17:53:40 +02:00
934780c54c 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
c1881fc9f3 docs: CODE-LOGIC files reflect the 7b collector and inference pass
- 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>
2026-08-19 17:12:52 +02:00
28485a271c docs: iteration 7b migration — amend the normative docs (Phase 4)
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>
2026-08-19 17:11:35 +02:00
092b528081 feat: retire RC from the emitter and the format — .wob v4 (7b Phase 3b)
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>
2026-08-19 17:07:43 +02:00
841cb41c6b feat(runtime): incremental tri-color mark-sweep replaces RC (7b Phase 3a)
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>
2026-08-19 16:52:48 +02:00
3f842f854b feat(compiler): remove @gc from the language — GC-ness is fully inferred (7b)
`@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>
2026-08-18 19:19:09 +02:00
023233895c test(compiler): unit helpers run inference; retarget borrow-escape fixture
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>
2026-08-18 19:10:39 +02:00
c301d1acb1 fix(compiler): demand promotion targets the escaping projection's class
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>
2026-08-18 19:07:10 +02:00
cc5228a2cf refactor(compiler): inference is one library pass, Gcinfer.infer
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>
2026-08-18 19:00:11 +02:00
6d589824e7 feat(compiler): demand promotion — GC-ness fully inferred (7b Phase 2b core)
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>
2026-08-18 18:53:08 +02:00