Noun Types: Why Nox Drops cell? and What Trident Does Instead
Problem
Nock op 3 tests whether a noun is an atom or a cell at runtime:
*[a 3 b] -> 0 if *[a b] is a cell
1 if *[a b] is an atom
Nox drops this. The question is whether to add it back.
Why not
A STARK trace is a fixed-width table of field elements. Every nox
pattern maps to a polynomial transition constraint selected by a
4-bit tag. cell? breaks this model in one specific way: atoms and
cells have structurally different representations. An atom is a
Goldilocks field element. A cell is a pointer into the noun DAG.
The distinction between them is not a field predicate — it is a type
judgment that requires a separate witness column in the AIR trace.
That column either:
- adds prover cost and proof size for a predicate used rarely, or
- must be known statically anyway — in which case the runtime check is dead weight
Nock needs cell? because Hoon compiles through Nock at runtime:
the evaluator is the runtime. Nox's evaluator is the proof system.
Dynamic type dispatch at the VM level defeats static arithmetization.
Rule: if a predicate is always a compile-time constant in the intended source language, it does not belong in the VM.
What Trident does instead
The expressiveness that cell? provides in Nock comes from one
pattern: dispatching on unknown noun structure. Trident recovers
this with noun types — discriminated unions over noun structure,
resolved statically.
Noun type syntax
type Noun =
| Atom of Field -- a single Goldilocks element
| Word of u64 -- bitwise-domain atom
| Hash of [Field; 4] -- 256-bit identity
| Pair of Noun * Noun -- a cell
A value of type Noun carries its structure in the type, not at runtime. The compiler knows at every call site which branch is live.
Pattern match syntax
match expr {
Atom f => ...
Word w => ...
Hash h => ...
Pair(l, r) => ...
}
Matches must be exhaustive. The compiler rejects missing branches. Each arm compiles to a branch chain in nox with statically known offsets.
Generic traversal
Functions over unknown structure use Noun as a type parameter:
fn depth(n: Noun) -> Field {
match n {
Atom _ => 0
Word _ => 0
Hash _ => 0
Pair(l, r) => 1 + max(depth(l), depth(r))
}
}
The recursion is bounded by the type structure, not by a runtime tag check. The compiler inlines or specializes based on call-site types.
What this compiles to
-- match n { Atom f => e1, Pair(l,r) => e2 }
-- compiles to: branch on static tag, then axis into the pair
[4 [axis(n, tag_slot)] [e1] [e2]]
No extra witness column. No runtime type oracle. The branch is a standard nox op 4, driven by a field element the compiler placed at a known axis.
Design tension
language.md currently says "No enums. No sum types." The Noun
type is a discriminated union — a sum type. Two paths:
Path A: Noun as a built-in type. Keep the "no sum types" rule for user code. Make Noun a compiler-intrinsic type like Field or Digest. Match syntax works only on Noun. Minimal surface change, but one-off magic.
Path B: Add sum types to the language. Remove the "no enums"
restriction. Noun becomes a library type defined in vm/nock/.
Orthogonal and composable, but every sum type needs a tag —
what does that cost in the AIR trace on stack targets?
Leaning Path A until there's evidence other targets benefit from general sum types.
Summary
| Need | Nock solution | Nox + Trident solution |
|---|---|---|
| Atom vs cell dispatch | cell? op at runtime |
Exhaustive match on Noun type, static |
| Generic noun traversal | Runtime recursion + cell? |
Parameterized functions over Noun |
| Defensive input checking | Op 3 guard | Type-checked at Trident boundary; ill-typed input rejected before nox |
| Circuit cost | Not applicable | Zero — branch compiles to op 4 |
The type system is the right layer. The VM should not carry type predicates that are always statically decidable in the source language.