- `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.
6.8 KiB
Iteration 9b — @table, relations, and language-integrated query
Format:
product/story-iteration-template. Part of Story — one language, one runtime, one database, one binary.Inserted 2026-08-11, hence
9brather than a renumber. It follows iteration 9 because a query surface needs tables that actually execute, and precedes iteration 10 becauseserviceblocks will want to return query results.No spec exists yet. This iteration frames the outcome and records the open questions; the design must be brainstormed before a plan is written. The three questions in Info are genuine forks, not details.
Goals
@tablegraduates from a parsed-but-inert annotation into the declaration that makes a class persistent: named storage, declared indexes, and a primary identity.- Relations become first-class and typed —
ref Tforeign keys,backlinkinverses, andmulticollections — so a developer navigates their data by following fields rather than by hand-writing joins. - Queries are written in the language, checked by the compiler: a LINQ-shaped operator vocabulary (filter, project, join, group, order, aggregate) over tables and relations, with the result's type inferred and every column reference resolved at compile time. A typo in a field name is a compile error, not a runtime one.
Acceptance Criteria
- What to achieve?
- Given a class annotated
@tablewith a declared index, - when the program is compiled and run,
- then its instances persist through the engine, the index is built, and a query that could use the index does use it — demonstrated, not assumed.
- Given a class annotated
- What to achieve?
- Given two classes related by
refwith abacklinkinverse, - when a query navigates the relation in either direction,
- then it typechecks with the related class's field set in scope, and navigating a field that does not exist is a compile error naming it.
- Given two classes related by
- What to achieve?
- Given a query whose result shape is a projection rather than a whole row,
- when it is assigned or returned,
- then its type is the projected shape — so a later use of a column the projection dropped is a compile error.
- What to achieve?
- Given a query written against tables,
- when the compiler lowers it,
- then it becomes engine operations, not a string handed to a parser at runtime — provable by disassembly, and by the absence of any SQL-text construction in the emitted image.
- What to achieve?
- Given the ecommerce sample's existing relational shapes
(
Order.user: ref User,User.orders: backlink Order.user, line-item collections), - when they are expressed as queries in this surface,
- then each produces the same results as the equivalent hand-written query, and the sample's README records anything that could not be expressed.
- Given the ecommerce sample's existing relational shapes
(
Out Of Scope
- Cross-shard queries and distributed joins — iteration 8 owns ownership movement, and a query spanning shards is a 2PC concern recorded with the database track.
LIVEsubscriptions over queries. The subscription registry is the HTTP/UI track's; a query that pushes updates is a later composition of the two.- Migrations. Changing a
@tableclass's shape is the blue-green spec's additive-only differ (iteration 12), not this iteration's problem. - Query optimisation beyond index selection. A cost-based planner is a separate, much later concern; this iteration must only prove that declared indexes are used.
Info
Three open forks the spec has to settle. Each is a real decision, and I have a leaning on all three but no mandate.
1. Where does this leave the existing SQL + Cypher query layer?
docs/runtime/database/02-wo-language.md specifies a two-layer design — a
schema layer plus a query layer of literal SQL and Cypher with five
"fixed-glue" rules. A language-integrated surface either replaces that layer,
sits beside it, or becomes the only surface with SQL retained purely as an
export format. Replacing it is the coherent choice and also the most
disruptive, because that document is normative and the wo-db C++ prototype
implements the SQL/Cypher grammar it describes.
2. There are no function values, so what is the syntax?
LINQ-to-Objects is built on delegates: .Where(x => x.Age > 18) passes a
lambda. writeonce has no function-value type, and the systems-track spec
deliberately rejected closure builtins (map/filter/reduce) for exactly
this reason. So the surface cannot be method-chaining-with-lambdas as written
in C#. The realistic options are a comprehension syntax the compiler desugars
(from o in orders where o.total > 100 select o.id), or method chaining whose
"lambda" argument is a compiler-recognised expression form rather than a
value. Either way the predicate is compile-time syntax, never a runtime
closure — which is also what lets the whole query lower to engine ops.
3. What does the reference actually contribute?
.dev/reference/dotnet-runtime/src/libraries/System.Linq/src/System/Linq/
(sparse checkout, added with this iteration) is the operator catalogue: read
Where.cs, Select.cs, Join.cs, GroupBy.cs, OrderBy.cs for what each
operator means and which edge cases it has to answer, and the *.SpeedOpt.cs
files for how LINQ specialises when the source's shape is known statically —
directly relevant, since writeonce knows every shape statically. What does
not transfer is the machinery: IEnumerable iterator composition,
delegates, and IQueryable's runtime expression trees, the last of which
depends on reflection that principle 13 forbids outright. Take the vocabulary
and the semantics; leave the plumbing.
Also relevant: @table(name:, index:) already parses today with known-key
validation (WO-E102), the Rust runtime already ships secondary indexes and
find_by behind that annotation, and ref T already classifies as a scalar
id rather than a pointer — so the relational vocabulary partly exists and this
iteration makes it mean something in the C stack.
Proposed Solution
- Brainstorm a spec first, settling the three forks above; only then write the plan. This iteration deliberately ships no plan pointer, because choosing between "replace the SQL layer" and "sit beside it" changes what the plan contains.
- Study
.dev/reference/dotnet-runtime'sSystem.Linqoperator set for the vocabulary, anddocs/runtime/database/02-wo-language.mdplusprototypes/wo-db/for the semantics already committed to. - Expect the work to span the front end (query syntax, relation typing, projection types), the emitter (lowering to engine operations rather than text), and the engine (index selection, relation traversal) — which is why it follows iteration 9 rather than preceding it.