writeonce/docs/stories/language-runtime-database/13-compile-time-metaprogramming.md
shoney.arickathil ad8cfb456e docs: iteration 13 story -- compile-time metaprogramming (derive)
- captures the toCSV/reflection thread: principle 13 forbids runtime
  reflection, so a generic serializer can't be a user-written function;
  Rust answers with derive macros (compile-time codegen), and
  json.encode is already a single hand-built instance of exactly that
- iteration generalizes json.encode's mechanism into a reusable derive
  facility: @derive(Json/Csv/Eq/Hash/Show) -> the compiler generates
  per-type routines from the class-table metadata it already emits,
  monomorphic, no runtime type tag, no dynamic dispatch
- closes the query-result-serialization gap
  (csv.encode(from e in Employee ... select e)) that has no expression
  today; acceptance requires json.encode retrofitted onto the framework
  with byte-identical output, and disassembly proving no reflection
- four forks: request surface (lean @derive annotation), invocation
  (lean compiler-recognized encode builtins, no UFCS/methods), Eq/Hash
  sharing the engine's key comparison, static applicability checking
- out of scope: full trait/typeclass system, user proc-macros, general
  generics, cross-channel derive -- a CLOSED compiler-known derivable
  set, the pragmatic 80% without the type-system weight
- numbered 13 to echo the principle it lives inside; roadmap + board
  rows added

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 04:57:00 +02:00

167 lines
9.1 KiB
Markdown

# Iteration 13 — compile-time metaprogramming (derive from the class table)
> Format: `product/story-iteration-template`. Part of
> [Story — one language, one runtime, one database, one binary](00-story.md).
>
> **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"](../../00-principles.md).
> It comes late because it earns its keep only once there are enough
> types worth deriving over (the `@table` classes 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 existing `json.encode`),
`Csv`, structural `Eq`, `Hash`, and `Show` (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.encode` becomes the `Json` derive**, 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 Employee` whose
`Employee` derives `Csv` can be serialized whole, which is precisely the
`toCSV(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 native `multi`).
## 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 `multi` of 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.
- What to achieve?
- **Given** `json.encode` reimplemented as the `Json` derive,
- **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.
- What to achieve?
- **Given** a derive requested on a class one of whose fields the derive
cannot handle (a `@gc` field 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.
- What to achieve?
- **Given** two classes deriving `Eq` where 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.
## 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_macro`
derives; 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 9c/9d). 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** (`.wob` v2's `field_names`/`field_class`/
`field_elem`, v3's index metadata) is the substrate every derive reads. It
already exists and is already what `json.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.encode` onto the framework — the proof that the
generalization changes nothing observable — before adding `Csv`/`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 metadata `json.encode` walks);
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.