Commit graph

13 commits

Author SHA1 Message Date
dd7dd42bd1 feat: bounded mailboxes + WO_T_ACTOR (trap 13); try-arm place-copy fix
- cap 1024 (WO_MAILBOX override at boot): sender-side atomic
  reserve/release on every path — same-shard, cross-shard envelope,
  OOM rollbacks; full mailbox traps the SENDER catchably; delivery
  pop releases; overshoot bounded by in-flight sends (disclosed)
- test_mailbox 12/0: exact cap single-threaded, two racing senders win
  exactly cap slots, drain/refill clean
- corpus run/mailbox-full-trap: parked sleeper, send loop catches
  "actor mailbox full" after >= 1024 sends
- pre-existing compiler bug found + fixed: a try ARM yielding a Text
  PLACE (bare e.msg, try box.field) aliased a register the arm's scope
  end freed — ASan use-after-free, SEGV on the next unwind's
  double-walk; emit_try now applies copy_place_text to both arm
  results; pinned by corpus run/catch-msg-place
- db-bench driver: msgrate keeps iteration 22's unbounded-flood
  contract via WO_MAILBOX=MSG_N (the cap is 24's policy, not 22's)
- battery 12/12 fresh-built

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-23 01:05:13 +02:00
07c1f7b257 feat: arc stage 3 T7 — transparent DB actor (WO_T_DB hole closed)
- worker DB builtins marshal to shard 0: requester-side slot encode
  (VM heaps never read cross-shard), owner executes serialized in
  adopt, reply unparks via new WO_PARK_INBOX park + envelope 3/4
- engine gains thread-agnostic slot entry points (insert_slots,
  update_field_slot, val_encode/clone, wo_db_exec_req); traps and
  messages byte-identical to the local path
- main.c: engine + replay boot BEFORE shards spawn; workers assert
  rt.db/rt.wal NULL; busy shard adopts inbox once per slice
- latent stage-1 bug fixed: shared io_uring params static raced by
  lazy worker init lost park wakes (~1/20 hangs); params per-vm,
  short submit now fails loud
- new sample docs/examples/db-actor + just db-actor gate 8/0 (multi
  x3, uring/epoll forced, single byte-exact, WAL replay pair);
  ASan+TSan 6/6; full battery green

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 13:10:26 +02:00
d24c705860 feat: iterations 19 + 17 — Float/Bytes scalars (.wob v5), library kind + internal/
- Float full stack: literals (fraction/exponent; `0..10` still a range), f64
  opcodes 34-41, @table column, WAL bit-exact replay, json fractions in and
  shortest-round-trip out. IEEE-quiet — FDIV never traps where DIV does.
- Bytes: a wo_str with its own class id, so alloc/free/copy are shared but no
  Text builtin accepts one; len/at/slice/eq/concat, base64 both ways, json
  boundary as base64; TEXT_COPY preserves the kind.
- No implicit Int/Float mixing (WO-E201 in the typechecker, not the emitter,
  which picks the opcode from one side and would misread the other).
- One IEEE deviation: float_cmp total order (NaN last, -0.0 == +0.0) for
  indexes and order-by, keys canonicalized to match. `?Float` nil is a
  reserved quiet NaN — the zero word is +0.0, WO_NIL_SCALAR's bits are -2.0.
- Renderer prefers fixed over exponential in 1e-6..1e21: pure shortest makes
  a price of 900.0 read `9e+02`. One renderer for interp/json/float_to_text.
- Fixed en route: lexer double-counted the leading digit; is_scalar_shaped
  took Float/Bytes as Int-shaped; Bytes ownership needed a shared heap-scalar
  predicate or temps never dropped; order-by bit-compared negatives backwards.
- Iteration 17: `kind = "library"` (absent = program; bad value = WO-E109),
  entry-less check mode retiring the `--emit` workaround, Go's `internal/` as
  WO-E108 at the consumer's `use`. Driver-only; VM/.wob/GC untouched.
- Framework reorg: internal/{parse,serve}.wo; http/form.wo split out to keep
  media_type/form_values public (parse.wo had grown public surface).
- Docs: link audit (97 -> 88 broken, conflict markers resolved, 2 duplicate
  stories removed), 00-code-review verified 26/27, iterations re-sequenced.
- Also carries the pre-staged pub(read)/using/#if work from the index.
- Gates: corpus 103/0, test_wal 156/0, web-app 26/0, oop-accept ALL MET.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 19:24:15 +02:00
ec9d264355 feat: cross-shard actors — placement, envelopes, home-routed frees, WO-E222 (arc T6)
- spawn placement: round-robin across shards (same-shard when the
  engine is absent/single); the actor's mailbox and delivery belong to
  its HOME thread — a spawn to another shard travels as an ADOPT
  envelope, a send as a SEND envelope (mutex-guarded inbox + eventfd
  wake; the spec's lock-free rings stay a disclosed deviation until
  9e measures the mutex)
- workers: first envelope triggers lazy full-vm init UNDER the inbox
  mutex (TSan caught the memset racing a concurrent push, twice — the
  second was inbox_push reading wake_efd outside the lock; both fixed,
  gate x8 + battery clean); serve loop = adopt -> run to drained ->
  wait on the plane (the wake eventfd is watched by io_uring POLL_ADD
  oneshot / epoll level-triggered on BOTH backends)
- ownership across heaps: every allocation stamps rt->shard_id into
  the header (the field reserved since iteration 2); a drop on the
  wrong shard routes home as a FREE envelope — the owner's arena stays
  single-threaded by construction; at teardown routed frees become
  no-ops (arenas die wholesale) which is what un-danced the freed-mutex
  ASan SEGV the first ordering had
- WO-E222: an actor's state or message type that is (or transitively
  contains) an inferred-traced class refuses at the spawn/send — with
  round-robin every actor is potentially remote; corpus-pinned
  (compile-fail/traced-send, inference-aware: Box contains ?Node)
- determinism narrowed per spec: oop-e2e pins WO_SHARDS=1 (exact
  outputs); the fibers gate grows multi-shard SET assertions + a TSan
  run (wovm-tsan target; setarch -R fallback for kernel 6.5+ ASLR)
- NEXT_RUNNABLE honors engine shutdown for parked workers (deadlock
  hole closed); io_wait's adopt-wake (rc 1) no longer reads as fatal
- battery: oop-e2e 93/0, fibers 10/0 x8 (+WO_IO=epoll), log-watcher
  7/0, employee 8/0, web-app 21/0, deps 8/0, runtime tests 16/16

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 12:06:13 +02:00
5bd8813b9b feat(runtime): shard engine — pinned worker threads, all cores default (arc T5)
- wo_engine + wo_engine_start/stop: one pinned pthread per extra core
  (pthread_setaffinity_np); shard 0 is the primary (entry + database);
  workers idle on a wake eventfd until fibers arrive (T6) or shutdown
- default = all cores (the arc's brave landing), WO_SHARDS=1..64
  overrides; N=1 spawns no threads — byte-identical to stage 1
- worker vms are LAZY: identity + wake fd only until their first fiber
  arrives — 20 idle shards must not cost 1.25 GiB of eager arenas
  (they did: the web-app gate flaked on exactly that before the fix;
  3 consecutive green runs after)
- engine stops (join + destroy) before the primary's teardown
- battery at the 20-core default: oop-e2e 92/0, fibers 8/0,
  log-watcher 7/0 (+WO_SHARDS=1 identical), web-app 21/0 x3,
  employee 8/0, deps 8/0, runtime tests 16/16 files green

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 11:46:28 +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
f0971e1481 feat: installable toolchain — version, wovm self-locate, dist tarball
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>
2026-08-18 01:19:20 +02:00
30ef8d8533 feat: insert executes (iteration 9, Task 3) -- DB_STUB retires for insert
- compiler: `insert Class { ... }` is a typed Ast.Insert in statement
  AND expression position, sharing the ctor literal's field grammar;
  typechecked with the ctor's omittable rule; result = the row id (Int)
- owner pass: the engine copies at the row API, so an insert BORROWS
  its field values -- no transfer, no E304; node is trap-capable and
  carries a live-mask drop entry like DbStub did
- emit: builtin 61 window = class-id const + one slot per DECLARED
  field in declaration order; omitted defaults emitted, omitted ?scalar
  gets WO_NIL_SCALAR, other omitted optionals the zero word; fresh
  argument values reaped after (the push/set copy semantics)
- runtime: database/src/db.c executes via the choke-point row API;
  rt.db/rt.wal opaque handles on wo_rt; WO_DATA=<dir> = replay
  <dir>/shard-0.wal at boot + commit-before-ack per statement (the
  builtin's return IS the ack until iteration 8 ticks); failed commit
  un-applies the row and traps WO_T_IO; loader validates the class-id
  slot (variable window documented in wob.h + format doc)
- the promised diff: trap/pricing-set-price-db-stub is now
  run/pricing-set-price-insert printing engine-allocated ids;
  durability smoke prints 1,2 then 3,4 across two WO_DATA runs
- old "bare insert is an Ident" unit test rewritten to the new
  contract; runner's loader mirror accepts id 61; goldens re-blessed
- gates: oop-accept ALL CRITERIA MET, oop-e2e 71/0, woc-test 566/0,
  wovm-test green, log-watcher 7/0

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-15 11:15:06 +02:00
22910e3974 fix: a stopping program stops (executable plan, Task 4)
- blocking stdlib calls that PARK (net.accept, socket read/write,
  time.sleep, a child wait) no longer restart the syscall when the
  stop flag is set on an interruption: a server sitting in accept
  ignored SIGTERM and only `kill -9` ended it
- a stop is NOT a trap -- builtin.h's WO_SYS_STOPPED carries no error
  record and no catch handler sees it (`try` must not swallow
  SIGTERM); the VM unwinds the whole stack through the same drop
  machinery an uncaught trap uses, so nothing leaks on the way out
- wo_vm_call gained a third outcome (1 = stopped); the CLI maps it to
  the status the program's own `return 0` would have given, and a
  regular-file read keeps its plain EINTR retry -- it does not park
- an ASSIGNMENT was not an ownership boundary: `api_key =
  j.mcp.apiKey` moved the field pointer into the local, so the local
  aliased the record and the first unwind freed the same string twice
  (SIGSEGV in class_free). `let` copied a Text place, assignment now
  does too -- the same double free was latent on the normal exit path,
  hidden by the order the compiler happens to emit drops in
- log-watcher-accept is 7 checks: the seventh is the stop itself, with
  the hard kill demoted to a fallback whose use is the failure
- measured under ASan: mcp parked, mcp after traffic, watch and run
  all exit rc 0 with zero leaks; SIGINT behaves as SIGTERM
- gates: oop-accept ALL CRITERIA MET, oop-e2e 71/0, woc-test 565/0,
  wovm-test green, log-watcher 7/0

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 23:58:21 +02:00
4a2fc2041e fix: release the argv container (executable plan, Task 3)
- program mode built the entry's `multi Text` of arguments and never
  freed it: the entry only BORROWS a parameter (never a `take`, and
  the drop tables never drop one), so the runtime that built the
  container owns it
- dropped after the entry returns and after a trap alike -- the
  container outlives the unwind; `multi_free` recurses, so the
  argument strings go with it
- one site covers both invocation shapes: `self_rc` picks the argv
  offset, it does not build a second container
- measured: watch, run and the full MCP mix now report ZERO leaks
  under ASan -- the clean baseline the soak (Task 6) reads against
- gates: oop-accept ALL CRITERIA MET, oop-e2e 71/0, wovm-test green,
  log-watcher 6/0

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 23:41:13 +02:00
b973ea107e feat: program mode (argv + exit code); reject + on Text; nil compares by word
docs/examples/log-watcher now COMPILES AND RUNS: `wovm lw.wob watch app.log 2 1`
tails a live file, classifies levels and fires its alert
("last entry is error, quiet for 2s"). corpus 71/0, woc 565/0, wovm gates green.

- program mode: the entry is `fn main` taking nothing or one `multi Text`;
  runtime/src/main.c builds that list from the program's own arguments (not
  the program name, not the image path) and the entry's return value is the
  process exit code (low byte); the loader accepts a 0- or 1-arg free-fn entry
- `+` on Text is now WO-E201 pointing at `..`. This was a memory-safety hole,
  not a style nit: the emitter lowered it to ADD on two heap pointers, and the
  workload's own `out = out + char_of(c)` produced a wild pointer that
  segfaulted the VM inside starts_with. Reported off confident types only
- `x == nil` / `x != nil` lower to EQ (a word compare), never EQS: nil is the
  zero word and EQS dereferences its operands, so a nil guard would trap
  instead of answering
- docs/examples/log-watcher: seven `+`-on-Text sites corrected to `..`
  (logtail sanitize, mcp header/body/carry assembly, supervisor detections
  line) — the sample was carrying the Haxe habit, and the language reserves
  `+` for arithmetic by doctrine
- wovm CLI takes arguments after the image path (`wovm <file.wob> [args...]`)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 17:18:01 +02:00
79605cbb4f feat: milestone 1 complete — .wob emitter, conformance corpus, single binary; GC redesign specced
- `woc` now emits `.wob` that `wovm` runs: emit.ml lowers the typed,
  owner-annotated AST (scope-stack registers with a >64 WO-E401 diagnostic,
  Lua-style call windows, ICALL by slot, dedup const pool, drop maps, line
  tables, implicit terminators); disasm.ml backs `--dump-bc` goldens.
- Ownership lowering consumes the four owner tables verbatim; RESIDUAL is the
  only source of borrow ops, coalesced per operand. Review caught the emitter
  consuming only 2 of owner.ml's 4 residual producers — an assignment-anchored
  aliasing violation ran to exit 0 instead of trapping; fixed, plus a backstop
  raising WO-E404 for any residual region left unconsumed.
- Conformance harness `scripts/oop-e2e.sh` (`just oop-e2e`): four fixture
  kinds with exact outcomes — byte-exact stdout, one WO-E### anchored on
  `error CODE:`, numeric trap code, gc trace. 25 fixtures incl. pricing-demo
  logic, the ownership suite, and DB_STUB's parse-but-trap. `tests/` un-ignored
  so the corpus is actually tracked.
- `woc build` produces a self-contained binary: wovm copy + appended image +
  20-byte trailer, self-exec via /proc/self/exe. Verified relocated outside
  the repo, argless, and against adversarial trailer corruption.
- Milestone 1's five spec criteria all MET (`just oop-accept`). Criterion 3
  closed by WO-E405 — the entry must return `Int`, since program mode already
  says its return value is the exit code — which deletes the leak class
  without adding return-type metadata to the format. `gc/held-cycle` retired:
  an externally-held cycle is not expressible in a post-exit pump.
- New spec: inferred GC + incremental per-shard tri-color mark-sweep, retiring
  `@gc` and reference counting. Story gains iterations 7b (that work) and 9b
  (`@table`, relations, compiler-checked query); `.dev/reference` gains a
  sparse System.Linq checkout. Priority: 5→6→7 (log-watcher) then 7b, 8, 9, 9b.
2026-08-11 19:31:26 +02:00
bab88a53b9 feat(runtime): wovm VM core (Iteration 2)
- 16 tasks complete: arena, object model, borrow word, containers,
  RC + budgeted cycle collector, wob_build, validating loader,
  interpreter core (dual dispatch), object opcodes, drop-map unwinding,
  builtins + DB_STUB + TRAP, ICALL, wovm CLI + just recipes
- 13 test suites × 2 dispatch flavors (ASan+UBSan) + CLI smoke, all green
- .wob v1 format pinned in src/wob.h + docs/plan/oop-vm/00-wob-format.md
- wo-rt.c reference event-loop preserved for sub-project 2

This is Iteration 2 of the OOP milestone; compiler front (Iteration 3)
is in progress on this branch. They meet at Iteration 4 (emitter+e2e).
2026-08-10 09:35:55 +02:00