The closure backend: Rupa.IR in, a tree of anonymous functions out.
One function per IR node, each closing over everything the node decided at staging time — the compiled regex, the format's decoder, the wire key, the default. A decode is then a walk down a tree of calls with no interpretation and no lookups, except at a recursive ref, which is the one place a table is consulted.
Every decoder has the same shape, (value, ctx) -> {:ok, decoded} | {:error, [Rupa.Error]),
where ctx is {mode, defs}. Paths are built on the way back up: a leaf reports an error
with an empty path and each parent prepends its own segment, so a successful decode allocates
nothing for paths at all.
Summary
Types
Functions
@spec build(Rupa.IR.program()) :: {decoder(), %{required(atom()) => decoder()}}
Builds the root decoder and the table the recursive refs resolve against.
Rupa.Codec.new/2 calls this and its two siblings; you reach what they build through
Rupa.decode/3 rather than by holding one:
{:ok, program} = Rupa.Stage.run(%{name: Rupa.T.string()})
{decode, defs} = Rupa.Closure.build(program)
decode.(%{"name" => "Ada"}, {:halt, defs})
#=> {:ok, %{name: "Ada"}}
@spec build_encoder(Rupa.IR.program()) :: {decoder(), %{required(atom()) => decoder()}}
The same as build/1, and used the same way, in the other direction.
@spec build_json(Rupa.IR.program()) :: {decoder(), %{required(atom()) => decoder()}}
The other direction again, straight to JSON iodata rather than to a wire term.
Built and used exactly as build/1 is; what it hands back emits bytes rather than a term.