9.1 KiB
Iteration 29 — compile-time metaprogramming (derive from the class table)
Format:
product/story-iteration-template. Part of Story — one language, one runtime, one database, one binary.Inserted 2026-08-16. A language-capability iteration, deliberately numbered to echo the principle it lives inside: principle 13, "statically typed, all the way to the register". It comes late because it earns its keep only once there are enough types worth deriving over (the
@tableclasses of iteration 9/9b, the records the query surface projects), and it must never be the excuse that re-opens a dynamic hole.No spec exists yet. The forks in Info are genuine decisions.
Why this iteration exists
Principle 13 forbids runtime reflection: no Dynamic, no runtime type tags,
no walking an unknown value's fields at run time. That ban is correct — the
untagged VM, the borrow checker, and the ORM all stand on it. But it leaves a
real gap: a generic capability like "serialize any type to CSV" cannot be a
user-written function, because such a function would need to enumerate a
value's fields at run time, which is exactly what is forbidden. Today the only
escape is a hand-written function per type, or a single compiler
builtin (json.encode) that already does the right thing — it is lowered by
the compiler and walks the class-table metadata (field_names / field_class
/ field_elem, .wob v2/v3), never a runtime type tag.
Rust faced the identical ban (it has no runtime field reflection either) and
answered with compile-time metaprogramming: #[derive(Serialize)] reads a
type's fields at compile time and emits per-type field-naming code, so
serde serializes any deriving type with zero reflection. json.encode is,
in effect, a single hand-built instance of exactly that mechanism. This
iteration generalizes json.encode's mechanism into a reusable derive
facility: the compiler generates per-type code from the class-table metadata
it already emits, so generic-feeling capabilities exist within principle 13
rather than against it.
Goals
- A closed, compiler-known set of derivable capabilities requestable on a
class — the first set:
Json(retrofitting the existingjson.encode),Csv, structuralEq,Hash, andShow(a debug rendering). Each is generated by the compiler from the class's field names and kinds; none is a runtime reflective loop. - Generation preserves principle 13 exactly. The emitted code is ordinary
bytecode over statically-known offsets and kinds — monomorphic per type, no
Dynamic, no runtime type tag, no dynamic dispatch. Disassembly must show a plain per-type routine, not a reflection opcode. json.encodebecomes theJsonderive, reimplemented on the framework so the framework is proven by rebuilding the thing that already works — byte-identical output, or the change is wrong.- The query-result serialization gap closes: a
multi EmployeewhoseEmployeederivesCsvcan be serialized whole, which is precisely thetoCSV(from e in Employee where … select e)case that has no expression today (a generic serializer can neither be user-written under principle 13 nor attached as a method to a nativemulti).
Acceptance Criteria
- What to achieve?
- Given a class annotated to derive
Csv(surface per the spec), - when the program is compiled and a value (or a
multiof values) is encoded, - then the output is the expected CSV, the encoder is generated from the class table, and disassembly shows ordinary bytecode with no reflection and no dynamic dispatch — principle 13 provable, not asserted.
- Given a class annotated to derive
- What to achieve?
- Given
json.encodereimplemented as theJsonderive, - when the existing db, log-watcher, and json corpus run,
- then every output is byte-identical to today — the framework generalizes the mechanism without changing its result.
- Given
- What to achieve?
- Given a derive requested on a class one of whose fields the derive
cannot handle (a
@gcfield for a value derive, a kind with no CSV rendering), - when it is compiled,
- then it is a compile error naming the field and the reason — no silent partial output, no runtime failure. A derive's applicability is decided entirely at compile time.
- Given a derive requested on a class one of whose fields the derive
cannot handle (a
- What to achieve?
- Given two classes deriving
Eqwhere one embeds the other, - when structural equality is generated,
- then it recurses through the embedded type's own derived
Eq— the framework composes across types the way the field kinds nest.
- Given two classes deriving
Out Of Scope
- A full trait / typeclass system — bounds like
fn f<T: Serialize>(x: T), generic functions, and the inference they need. That is a large, separate language iteration; this one ships a closed, compiler-known derivable set, not open generics. The derive facility is the pragmatic 80% without the type-system weight. - User-defined / procedural macros. Rust lets users write
proc_macroderives; writeonce does not, and this iteration keeps it that way — only the compiler-builtin derive set. A user-macro system is a much larger surface and likely never wanted (KISS). - Monomorphized generics as a general feature. Per-type generation here is specific to the derive set, not a general generics engine.
- Deriving across the attach channel — a client generating an encoder over the owner's types (iterations 20/21). Composes later; the class-table metadata already crosses the channel's schema handshake, so the pieces are in place, but it is not this iteration's problem.
- Reopening principle 13 in any form. If a derive appears to need runtime reflection, the derive is wrong, not the principle — that is a defect report against this iteration.
Info
Prior art in the tree:
json.encode/decode(runtime/src/json.c) is already this mechanism, built once by hand: metadata-driven, compiler-lowered with the class id, no reflection. This iteration lifts its shape into a reusable framework.- The class-table metadata (
.wobv2'sfield_names/field_class/field_elem, v3's index metadata) is the substrate every derive reads. It already exists and is already whatjson.encode's lowering walks. - Principle 13 is both the constraint and the enabler: because every type is known at compile time, per-type generation needs no runtime dispatch, so the generated code is as fast and as untagged as hand-written.
Forks the spec must settle:
1. The request surface. Options: an annotation in the existing ORM style
(@derive(Csv, Json, Eq) on the class, matching @table/@unique); a
derive keyword; or trait-style impl-blocks. Leaning: the @derive(...)
annotation — smallest surface, consistent with the annotation-driven design
the language already has, and it keeps derives a closed compiler-known set
rather than implying an open trait system.
2. How a derived capability is invoked. With no UFCS and no methods on
native containers, value.to_csv() cannot be a method on a multi. Options:
a compiler-recognized builtin per capability (csv.encode(x), exactly like
json.encode(x) is lowered today), or generated free functions named by
convention (Employee_to_csv). Leaning: compiler-recognized builtins
(json.encode/csv.encode/…), so the invocation is uniform and the
collection case (csv.encode(a_multi)) is handled by the same lowering that
already special-cases a value's static kind.
3. Whether Eq/Hash change what the VM already does. Structural
equality and hashing over stored/embedded types touch the same metadata the
engine's indexes use — the spec should decide whether derived Eq/Hash
share code with the engine's key comparison (database/src/table.c's
idx_cols_equal/idx_hash) or generate independent routines. Leaning: share
where the shapes match (one definition of "these two values are equal"), so a
derived Eq and an index's uniqueness check can never disagree.
4. Applicability checking. A derive must reject at compile time any field
it cannot handle (a @gc field in a by-value derive, a kind with no rendering
for the target format). The spec pins the rule per capability — and this is
the mechanism by which the facility stays inside principle 13: applicability
is a static question with a static answer, never a runtime probe.
Proposed Solution
- Brainstorm the spec settling the four forks, then a plan whose first
task is retrofitting
json.encodeonto the framework — the proof that the generalization changes nothing observable — before addingCsv/Eq/Hash/Show. - Expected shape: a
@derive(...)annotation parsed like@table; a compiler pass that, per derived capability per type, generates a routine from the class-table metadata (the same metadatajson.encodewalks); compiler-recognized encode builtins that lower to those routines; and applicability diagnostics in a new WO-E range. The runtime gains no new reflective machinery — only, at most, small shared helpers the generated code calls.