The intermediate representation: what the staging pass turns a schema into.
A schema says what you meant. The IR says what the decoder will do, in the order it will do it, with everything the schema only implied made explicit:
- refs are resolved — a ref that is not on a cycle is replaced by the definition itself, so the decoder never looks anything up; only genuinely recursive refs stay as nodes, and those live in a table beside the tree
- an object's fields become an ordered list, each with the wire key it arrives under, the
one it leaves under, the key it takes in the decoded map, whether it is required, and
what it defaults to — so
rename_all:,from:,to:andkeys:are all spent here into:stays a bare module name on the object, checked against that module'sdefstructwhile staging, so a backend builds the struct without asking anything- a scalar's constraints become an ordered list of checks
enumandliteralbecome one lookup table from wire value to decoded value- a tagged union becomes a table from tag string to branch, with the decoded tag atom interned here and never from wire data
Nothing here is a runtime artifact: a pattern is still its source string, not a compiled
regex, because the module backend has to embed these nodes in generated code. Compiling the
regex is the backend's job.
Both backends consume this, which is the point: a feature is written once. Rupa.explain/1
prints it, so what you read is what runs, whichever backend you are on.
Summary
Types
A staged schema: the root node, plus the definitions that recursion needs.
@type t() ::
Rupa.IR.Scalar.t()
| Rupa.IR.Const.t()
| Rupa.IR.Object.t()
| Rupa.IR.Array.t()
| Rupa.IR.Fixed.t()
| Rupa.IR.Dict.t()
| Rupa.IR.Nullable.t()
| Rupa.IR.Union.t()
| Rupa.IR.Tagged.t()
| Rupa.IR.Ref.t()