writeonce/docs/stories/language-runtime-database/refine/08-shard-actor-runtime.md
shoney.arickathil 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

5.4 KiB
Raw Blame History

Iteration 8 — shard-actor runtime (the 8+11 concurrency arc, part 1)

Format: product/story-iteration-template. Part of Story — one language, one runtime, one database, one binary.

REFINED 2026-08-20 (developer decisions, no code): iterations 8 and 11 are one arc — the scheduler, fibers on it, then serving — because the database-ownership decision below makes a fiberless multi-shard server block a whole thread per cross-shard call. The arc's driving workload is iteration 24 (chat: WebSocket pub/sub). The pre-existing plan (docs/superpowers/plans/2026-08-01-shard-actor-vm-runtime.md) predates inferred GC (7b), the unified surface, and the DB decision — it is a source of ideas, NOT the plan of record; the arc starts with a fresh brainstorm → spec → plan.

Settled decisions (2026-08-20)

  1. The database is an actor. The engine (wo_db + WAL) lives on one owner shard; every query/write from another shard is a message send, and results come back materialized (queries already copy rows out — the model was built for this). Doctrine-pure: no lock, no shared mutable state. Consequence, accepted deliberately: callers must PARK while the reply travels, which is why 8 and 11 ship as one arc. (Rejected: a coarse engine lock — bends the doctrine and caps write scaling anyway; partitioned tables — the real scale answer, but it waits for a measured need, not v1.)
  2. One concurrency surface: everything is an actor address. spawn returns an address whether the spawnee lands on this shard (a fiber) or another (placement policy's call); send always moves ownership; a same-heap send skips the ring and is cheap. The language never shows a fiber-vs-actor split. (This dissolves iteration 11's "handle vs address" open question.)
  3. Driving workload: chat (iteration 24) — rooms, broadcast, N concurrent WebSocket clients, one binary. The arc's acceptance is the chat sample's, not only synthetic corpora.
  4. Order: 22 → the 8+11 arc → 23. The io_uring write path waits for the arc (its batch boundary is the shard tick) and for 22's baseline.

Goals

  • The runtime scales past one core the doctrine way: pinned thread-per-core shards, each owning its own heap and event loop; cross-shard communication is a message send that moves ownership — shared mutable state never exists.
  • The language grows spawn/send (the unified address surface); garbage collection stays per-shard (7b's collector is already per-shard by construction), so no global pause appears at any core count.
  • The database keeps its single-writer truth by BEING an actor on its owner shard.

Acceptance Criteria

  • What to achieve?
    • Given a program spawning actors across shards,
    • when an owned object is sent to another shard,
    • then the sender can no longer touch it (compile-time move), the receiver owns it, and its eventual free routes back to its allocation-home arena.
  • What to achieve?
    • Given debug builds with shard-ownership asserts,
    • when the deterministic actor corpus runs under ASan and TSan,
    • then zero races, zero leaks, and identical output across runs.
  • What to achieve?
    • Given a reference to a TRACED object (GC-ness is inferred since 7b — there is no @gc to write),
    • when code attempts to send it cross-shard,
    • then the compiler rejects it: aliased references cannot cross heap boundaries, and the diagnostic names the inferred-traced class and why it is traced. (This criterion originally said @gc; restated 2026-08-20 in inference terms — same rule, current language.)
  • What to achieve?
    • Given handlers on serving shards querying and writing through the DB-owner shard,
    • when the employee/web-app matrices run multi-shard,
    • then every answer is byte-identical to the single-shard run and the WAL's ack-after-durable contract is unchanged.

Out Of Scope

  • Cross-shard transactions (2PC) — the DB actor serializes writers, so iteration 18's transaction { } is unaffected; distributing it is a later story.
  • Fiber details beyond the shared scheduler substrate — part 2 (iteration 11) owns them.
  • WebSocket framing — the framework's (iteration 24's) job.

Info

  • The C proving ground (docs/plan/exploration/c-runtime/, phases A–F: epoll loops, eventfd mail) is the substrate this lifts into wovm. (Path restated 2026-08-20; the old runtime/wo-rt.c reference was stale — that tree was removed with the Rust runtime.)
  • The VM's object header has carried a shard id since iteration 2 — no relayout.
  • Gated by the benchmark: landing the arc means re-running 22 at the concurrency scale it unlocks and recording the before/after delta; it is also where 23 gets a thread to overlap durability against.

Proposed Solution

Fresh brainstorm → spec → plan for the WHOLE arc (8+11), staged: the pinned-worker scheduler + shard-stamped heaps + MPSC mailboxes + the unified spawn/send surface and the traced-send rejection; fibers on that scheduler (part 2's document); the DB-actor migration; then iteration 24 proves it. The 2026-08-01 plan is reference material for the mailbox and heap-stamping shapes only.