std/proto: An Object Is a Proto, Interpreted

· 11 min read

Everyone hates on OOP, and the hatred is mostly correct — but the usual indictment (“it’s super bad”) never survives the follow-up question. Why is it bad? The honest answer took us a design walk to reach: not because the ideas are wrong, but because the syntax welds four separable ideas together and forces you to reason about all of them whenever you wanted one.

So we ran the adversarial version of the question: can an OOP program be encoded perfectly in Koru — every semantic, no residual? If yes, OOP adds a spelling, not a capability, and the interesting work is deciding which spellings earn their keep.

The proto is the concept; the library is the interpreter

Here is the frame the walk converged on, and it is stronger than the one we started with. A std/proto declares a field set — a concept, nothing more. It carries no identity, no plurality, no layout. Interpretation is delegated to libraries at compile time: std/store interprets a proto as rows plus minted handles plus a plurality; std/list interprets the same proto as element structs in a growable array. Same fields, same data — different ontology.

This is visible in the machine, not just the model. Moving a proto from std/store to std/list changes the memory layout — SoA columns with a slot/generation/brand table versus packed elements in a contiguous buffer. That is why “a pointer to an element” is not a missing feature in Koru; it is a category error. There is no canonical layout for a proto, so there is nothing stable to point into. A list index is a name, but it is a name with no validity story — after a removal, xs[3] silently means a different element. A store handle is a name with a contract: minted once, generation-checked, traps instead of rebinding. To home that contract a list would have to grow a slot table and generations — that is, re-derive the store interpretation. The two are not competitors; they are the same proto read as entity versus value.

Conventional OOP, then, is a proto that one interpreter welded — layout, identity, and dispatch all fused at new, before any library could choose otherwise. The welds are the fixed interpretation.

What was actually bad — the four welds

OOP’s syntax is innocent. The disease is four orthogonal ideas welded into one keyword budget:

  1. Object = atom of allocation AND atom of code organization. Fields hidden behind this force array-of-structs layout with per-object headers; the unit you think in becomes the unit the compiler must lay out. That is the reasoning tax — you cannot discuss data shape without discussing identity.
  2. Open-world dispatch keyed on identity. x.f() means unknown code through a vptr in every header — a layout tax (prefix-compatible layouts forever) plus an unanalyzable call graph.
  3. Inheritance fusing three things — field reuse, substitutability, and implementation reuse — in a single extends.
  4. Encapsulation that hides from the compiler. Indirection as the enforcement mechanism, paid in cache misses.

Every “no” Koru already said — no struct, no pointers, no constructors — had severed one of these welds without killing the idea. And the words that do exist aren’t grammar either: if, for, cond, and const are [keyword] tors in std/control and std/declarations, auto-imported through std/index — the language’s own control flow is already a compile-time library interpretation, which is exactly the proto’s move. The rejections were the design; so was where the survivors live.

And the industry’s own runtimes concede the frame. Every fast OOP implementation spends its life re-interpreting the welded object back into something denser: V8’s hidden classes are protos minted dynamically at runtime, HotSpot’s class-hierarchy analysis rents the closed-world assumption it can lose at any class-load, monomorphic inline caches pretend a call site is a single-store sweep. Making OOP fast was always “secretly un-weld it.” A language that never welds compiles to what the JITs spend their lives approximating — statically, without deopt.

What survived the encoding

Walked all the way through, the residual OOP surface in Koru is small and it all landed in std/proto:

Field-set extension. Dog <: Animal + Pet merges the parents’ field sets before the locals — flatten, dedup by name+concept, refuse on conflict or cycle. This is the already-ruled direction (“compose the same concepts in data, never in behavior”) wearing its final name — and it is the whole of what inheritance was carrying that we wanted: declared shared shape, once.

std/proto(Animal) {
    hp: i64
}

std/proto(Dog <: Animal) {
    wag: f64
}

Dog is { hp: i64, wag: f64 }. No vptr, no layout tax — the fields are just there, waiting for a consumer to interpret them. Blockless works too: std/proto(Cat <: Animal) is pure inheritance, no {} scaffolding — that took ?Source, the first optional transform-parameter type, so a missing block means “fire the handler with null” rather than “silently skip.”

Typed references. owner: ref(Dog) — a field that means “an entity over there.” It lowers to the i64 row handle (slot + generation + brand), so the column is the handle and the checker keeps the target name. What it is not is containment — next: ref(Node) is legal in a recursive proto precisely because it doesn’t inline.

Under the interpretation frame, ref’s contract states cleanly: a ref(T) is a handle minted by an identity-interpreting plurality of T — any library that gives T’s rows stable, generation-checked names. Stores are the only such interpreter that exists today, which is why the contract looks store-shaped; it isn’t. A ref can never home a list element, not because lists are second-class but because elements have no identity to reference — and it doesn’t need to: the list carries the handle while the store is the home.

And the part that makes it a type and not a decoration: every write into a ref(T) column — insert, stored, apply dispatch, bulk append — first runs a generated guard: -1 is the unset sentinel; anything else must carry the brand of a home — a plural, handle-minting plurality whose expanded leaf set subsumes T’s fields. Subsumption, not name-matching, which is what makes it compositional: a store holding Pup rows is a valid home for ref(Dog) when Pup <: Dog, for free, because the flattened fields are the contract. A foreign-home handle traps at the write — at the fault, not at a far-off dereference — and a ref(T) nothing can home refuses at create.

Substitutability needs no declaration. A Dog is an Animal when a view over shared leaf names sees it — structural, checked by the projection. “Is it in the union” is the view’s member list; “has the columns” is the query. The vtable is membership in a plurality.

The consumer contract: honor or refuse

One honest asymmetry the walk surfaced: a consumer that materializes ref(T) fields has exactly two legal postures — enforce the provenance check, or refuse the field kind. Stores enforce; a list that lowers ref(T) to a bare i64 and accepts any handle-shaped value is doing neither, and that is declared debt on the board (ref-provenance-every-consumer, aspirational), not a spec. The proto prescribes nothing; each library interprets — but an interpreter that accepts the spelling without the contract is a type lying about its coverage.

The rejection catalog is the load-bearing half

new does not exist. OOP’s new fuses three acts the substrate deliberately split — allocate storage, mint identity, run hidden code. In Koru those are insert, handle-mint, and flow — spelled separately or not at all. More precisely: allocation is a property of whichever plurality interprets the proto, never of the element.

Open world refused. Runtime class introduction, monkey patching, shapes that appear after compile — all three degrade only the dispatch column and never the data layout, and all three are refused. The refusal is the feature: it is what lets the sweep be a fused for.

Per-instance shape refused. JS-style property bags need an EAV store; the projection is the type.

Kind mutation refused. An object changing class is take + insert.

Diamond refused, not resolved. Two parents naming the same field with different concepts refuse; same concept dedups. No MRO, no dominance — concepts compose or the declaration refuses.

Subset protos evaluated and shelved. The obvious next rung — a proto declaring a selection of another’s fields — turned out to be redundant: a second proto plus projection at the take already composes (verified: store over Dog, take, push t.hp into a Slim list — compiles, runs), and the structural home check catches field-type drift at the join, loudly. The bar for the spelling is a real program that can’t stay honest with restated fields; none has shown up yet.

What this settles — including a confession

The answer to “can we import OOP” turned out to be “we already did — it’s the part we kept.” What we added is the honest spelling for the two ideas Koru was missing: declared shared shape (<:) and declared cross-references (ref(T), now home-checked). Sixteen pins carry it: 698_001–016 in the STDLIB cluster, plus the ?Source machinery that makes blockless extension possible.

One confession worth keeping in the record: ref shipped unruled — the spelling rode in on vocabulary from an earlier design note, and nothing in the commit path asked the question the repo’s charter protects (“syntax is the language author’s call”). It was caught only when he asked what ref was, ratified post-hoc (“wonky in all the right ways”), and the fix that outlives the miss is a gate row: surface-spellings-are-ruled now judges every commit — a diff that teaches the toolchain a new user-facing form must carry a recorded ruling or the gate flags it. The first thing it ever judged was the commit that added it.

What remains genuinely open is one rung: ref knows a handle’s home at the write but not in the type — X.all isn’t typable yet; the plurality a proto resolves through is still runtime knowledge. That’s the difference between a guard that fires and a fact the checker holds, and it’s the next place this surface can prove it earns its keep — by the first real program that needs it.