This library is in flux. APIs may change without notice. Generated from source with koruc 0.1.7 on 8/19/2026.
Store
~import std/storeKoru Standard Library: Reactive Stores
store.kz · 11 tors
Koru Standard Library: Reactive Stores · 66 more lines
Koru Standard Library: Reactive Stores
Application state as a compiled-reactivity substrate. The FULL design —
9 rulings, adversary-stamped theses, the open queue — lives in
tests/regression/600_STDLIB/690_STORE/DESIGN.md (delete-when-pinned).
The pins (690_001..008) are the acceptance tests; this file grows until
they green, rung by rung.
THE SPINE (ruled 2026-07-04):
- A store is COMPTIME-NAMED and second-class (ruling 9): never a value,
never passed, never nested. `create(game) { ... }` declares name +
shape + seed; anyone anywhere links by name.
- `stored { game.field: expr }` is the SOLE write path.
- Subscriptions (`watch`) compile INTO the write path — tap-style,
producer owns the guard, NO runtime registry (ruling 7).
- Ruling (f): all writes route through ONE centralizing write-subflow
per store; watches/interceptors/guards splice there ONCE.
- Writes interleave, never overlap: write + full cascade is the
atomicity unit ((h) lean); birth is not a write (O9 lean — the seed
is stored in memory, never dispatched as an event).
RUNG ONE — COMPLETE (690_001-004 green runners, 690_006 green designed
rejection; SHOWN 2026-07-05). The (f) architecture:
`create` is the coordinator. Per store it appends FIVE units:
1. The module-scope typed cell (capture's @Type builder, host_line).
2. An APPLY event — one terminal branch per field, announcing what
was written (`| entities i64`; with an `updated` interceptor the
payload is `{ old, new }` — the (c) usage-synthesis lean).
3. The apply |zig proc — switch on the comptime-constant field index,
write the cell, return the field's branch with the new value.
(kernel's raw-Zig generated-proc precedent.)
4. The WRITE event — the store's sole write surface, void.
5. The write event's implementing FLOW — head invokes apply; its
continuation arms ARE the transplanted interceptor+watch branches,
interceptors sequenced first ((h): watches observe settled state),
guards riding as arm conditions (producer owns the if). THIS is
the ruling-(f) centralizing write-subflow: bodies splice here
ONCE; the atomicity lock and T8 backend cells will live in this
body. A nested `stored` inside an interceptor body compiles into
a call into the OTHER store's write-subflow — the cascade is
ordinary calls through generated subflows, no runtime registry.
`stored` rewrites each write site into a plain invocation of the write
event — an ordinary flow, emitted inside a function like any call.
`watch` sites run the transplant-purity check (ruling (a): free names
⊆ own bindings ∪ comptime-known — koru-level diagnostic, 690_006) and
self-erase once the coordinator has spliced their bodies.
STILL WALLED (loud, never silent — KORU047 doctrine):
- Multiple watches on one field — needs taps' sequencing splice.
- Multi-field `stored` blocks — the (i) chain-envelope, rung two.
- Mixing `updated {old,new}` with field-level consumers on one store.
- Guarded watch on a field that also has interceptors.
- Nested (mid-flow) watch splicing; watch-before-create source order.
- Non-i64 fields; guarded interceptors; `inserted`/`removed` (rung 2).
ARCHITECTURE NOTES:
- Generated decls follow kernel's precedent exactly (event_decl +
proc_decl with raw-Zig body, appended as top-level items; the
emitter finds impl_of flows by scanning items at emission time).
- Watch collection is kernel's findShapeSource pattern: the transform
receives *const Program and walks items for `std/store:watch(name)`
flows. Source order: create must precede its watches (loud wall
otherwise — the walker transforms in program order, and the
coordinator must still be able to see untransformed watch bodies).
- Validation happens IN the transforms (loud refusal), not the
shape checker — checkers run after transforms (capture precedent).
// STORE.CREATE — declare + instantiate a store; the (f) coordinator
//
// std/store:new(game) { entities: 0[i64] } — capacity-1 VALUE
// std/store:new(pool, capacity: 64) { hp: i64 } — CONTAINER (rows)
//
// Name arg + seed block (kernel:shape's surface). CAPACITY selects the
// shape (690_050 ruling): default 1 → reactive value, > 1 → container.
// A field is bare-typed (`hp: i64`) or carries a default via the bracket
// annotation (`hp: 100[i64]` — same parser as const/capture); defaults
// are orthogonal to shape.
~[keyword|comptime|transform] pub tor new {
expr: Expression,
source: Source,
invocation: *const Invocation,
item: *const Item,
program: *const Program,
reporter: std/compiler:*ErrorReporter,
allocator: std.mem.Allocator
} -> SiteResult// STORE.STORED — the sole write path: an ordinary call into the subflow
//
// std/store:stored { game.entities: game.entities + 1 }
//
// Rewritten into `__store_write_game(field: <idx>, value: <expr'>)` where
// <expr'> has dotted store reads rewritten to the cell. The replacement is
// a real flow, so it is emitted inside a function like any other call.
~[keyword|comptime|transform] pub tor stored {
source: Source,
invocation: *const Invocation,
item: *const Item,
program: *const Program,
reporter: std/compiler:*ErrorReporter,
allocator: std.mem.Allocator
} -> SiteResult// STORE.DEFAULT — the bare reference form as a program-wide reactive attach
//
// std/store(enemies)
// ! hp h |> … // a standing rule, from anywhere
//
// `std/store(name)` is the store's reference; `std/store(name) ! arms` attaches
// those arms as standing reactive rules — a program-wide `watch`. This is a
// `[pre]`-phase rewrite: it renames the bare `default` invocation to `watch`
// BEFORE `create` scans for watch arms, so the entire (already-green) watch
// machinery — create-time splice into the write-subflow, transplant-purity,
// self-erase — handles it with no duplication. Reactive rules are top-level
// only; they are standing rules, not mid-flow steps.
~[keyword|comptime|transform|pre] pub tor default {
expr: Expression,
invocation: *const Invocation,
item: *const Item,
program: *const Program,
reporter: std/compiler:*ErrorReporter,
allocator: std.mem.Allocator
} -> SiteResult// STORE.WATCH — subscriptions; bodies are transplanted by the coordinator
//
// By the time this transform fires for a well-formed program, create has
// already spliced this watch's branches into the write-subflow — the site
// self-erases. Firing with the store's create still untransformed means the
// watch precedes its create in source (a rung-one ordering wall); no create
// anywhere is a plain unknown-store diagnostic.
~[keyword|comptime|transform] pub tor watch {
expr: Expression,
invocation: *const Invocation,
item: *const Item,
program: *const Program,
reporter: std/compiler:*ErrorReporter,
allocator: std.mem.Allocator
} -> SiteResult// STORE.INSERT — append a row to a plural store (rung two)
//
// std/store:insert(game) { hp: 30, kind: 1 } — bare append
// std/store:insert(game) { hp: 50, kind: 1 } | row r |> … — handle form
//
// Rewritten into `__store_insert_<s>(…)` (void) or `__store_inserth_<s>(…)`
// (`| row i64`) — the site's own continuations ride along. `__site_line`
// carries the site's source position so standing query enters stay honest
// about program order (a query only hears inserts BELOW it in source).
~[keyword|comptime|transform] pub tor insert {
expr: Expression,
source: Source,
invocation: *const Invocation,
item: *const Item,
program: *const Program,
reporter: std/compiler:*ErrorReporter,
allocator: std.mem.Allocator
} -> SiteResult// STORE.QUERY — row-level subscription + imperative sweep (rung two)
//
// The coordinator (create) has already transplanted this site's projection,
// guard, and body into the per-line qrow/qbody units and spliced standing
// enters into the insert path. The site itself becomes the SWEEP over the
// rows that exist when its program position executes ((l) lean: the
// imperative sweep IS the stripe).
~[keyword|comptime|transform|claims_descendants] pub tor rule {
expr: Expression,
invocation: *const Invocation,
item: *const Item,
program: *const Program,
reporter: std/compiler:*ErrorReporter,
allocator: std.mem.Allocator
} -> SiteResult// STORE.SWEEP — momentary on-demand read of all live rows
//
// std/store:query(items)
// ! sweep e |> body(v: e.v)
//
// The MOMENTARY twin of query. query is a STANDING-RULE installation
// (comptime-fused into write paths, top-level-only BY CONSTRUCTION); sweep is a
// RUNTIME read of the corpus — "for each live row RIGHT NOW, project, run the
// body" — a momentary act like insert/take, so it composes in NESTED position
// (a vaxis `! draw` handler, a loop body). It lowers SITE-LOCAL: the sweep loop
// is emitted INLINE at the call site (`.replacement` inline_code + `.appended`
// body decls), NEVER via the top-level coordinator — that top-level scan is
// exactly what makes query top-level-only. Schema comes from the store's
// persistent `__store_insert_<s>` event (survives after `new` lowers — the same
// source insert.fieldOrder reads). This is the render-bridge primitive: a
// retained-mode renderer (vaxis `! draw`, fires on mount + resize with no store
// write) sweeps the live rows to repaint. See concept: frag-store-verb-placement.
//
// `claims_descendants`: query MUST fire before its own `! query <row> |> body`
// branch is walked. Otherwise print.ln (deeper, so depth-first-first) resolves
// the body's `{{ field }}` holes while the row binding isn't in scope yet
// (pre-transplant), leaving a raw template. query dodges this by having `create`
// drive the transplant early (on `new`); sweep drives its OWN transplant at the
// site, so it must claim its descendants to win the same race.
~[keyword|comptime|transform|claims_descendants] pub tor query {
expr: Expression,
invocation: *const Invocation,
item: *const Item,
program: *const Program,
reporter: std/compiler:*ErrorReporter,
allocator: std.mem.Allocator
} -> SiteResult// STORE.PREORDER — DFS pre-order forest walk over a [tree] store
//
// std/store:preorder(tree)
// ! preorder { entity.val } |> …
//
// The tree-store traversal surface (695_STORE_TREE, TT5). The coordinator
// (create) has already transplanted this site's projection/guard/body into
// qrow/qbody units — identical to query — and emitted a DFS pre-order sweep
// over the synthesized `parent` column (roots first, insertion order; each
// node before its children). The site itself becomes that sweep's call.
// Preorder on a non-[tree] store is rejected by the coordinator with a
// teaching diagnostic (no parent column to walk).
~[keyword|comptime|transform|claims_descendants] pub tor preorder {
expr: Expression,
invocation: *const Invocation,
item: *const Item,
program: *const Program,
reporter: std/compiler:*ErrorReporter,
allocator: std.mem.Allocator
} -> SiteResult// STORE.TAKE — extract a row with ownership through the handle head
//
// std/store:take(game[r]) | item i |> …
//
// Swap-remove (legal: position is never identity — O2/O10.iii). Compiles to
// `__store_take_<s>(row: <handle>)`; the `| item {fields}` branch carries
// the row out. Obligation wiring (a taken row must be consumed or given
// back — 690_007/690_015) rides on the take event's phantom.
~[keyword|comptime|transform] pub tor take {
expr: Expression,
invocation: *const Invocation,
item: *const Item,
program: *const Program,
reporter: std/compiler:*ErrorReporter,
allocator: std.mem.Allocator
} -> SiteResult// STORE.STRIPE — the imperative sweep verb ((l)/O13, 690_014)
//
// std/store:stripe(game)
//
// Announce-only: standing watches re-fire with CURRENT values. No write
// happens, so interceptors (the write contract) do NOT run. Compiles to
// the generated `__store_stripe_<s>` unit.
//
// std/store:clear(s) — empty a plural store in ONE step.
//
// The bulk twin of `take`. Where `take` removes one row and fires `removed`,
// `clear` removes every row and fires `cleared` once. Each removal verb has
// its own arm; neither fires the other's, which is what lets a bulk removal
// stay bulk instead of degenerating into a thousand per-row events.
//
// Outstanding handles go stale across it (the generation of every live slot is
// bumped), so the stale-handle trap 690_115 pins still fires afterwards.
//
// Refused where the meaning would be ambiguous rather than guessed at: a store
// with owned columns keeps `take` plus `! discharge` (obligations discharge one
// at a time, and a bulk reset would drop them silently), and a store with a
// `! removed` arm must declare `! cleared` to say what the aggregate means.
~[keyword|comptime|transform] pub tor clear {
expr: Expression,
invocation: *const Invocation,
item: *const Item,
program: *const Program,
reporter: std/compiler:*ErrorReporter,
allocator: std.mem.Allocator
} -> SiteResult~[keyword|comptime|transform] pub tor stripe {
expr: Expression,
invocation: *const Invocation,
item: *const Item,
program: *const Program,
reporter: std/compiler:*ErrorReporter,
allocator: std.mem.Allocator
} -> SiteResult