How a parsed Aether AST turns into an actual result -- a sandboxed
program's final value, a config's map/struct, a query's result set, or
transpiled output. One mechanism, used differently per grammar: a
grammar's Actions module only implements the rules/tokens it needs
custom behavior for (@optional_callbacks); everything else falls back
to the defaults below.
handle_rule/handle_token are never invoked directly by user code --
Grammar.VM builds the raw capture tree during matching (an
Ichor.Capture.raw_captures/0 list, in true source order at every
level -- never a plain map, which Elixir/Erlang make no cross-version
iteration-order guarantee about), and this module's evaluate/5 walks
it, calling into the actions module (or the default fallback) as it
goes, threading context throughout.
evaluate_node/3 is the companion entry point for a raw capture node
that didn't come from the original parse -- a grammar with
macro-like features (LISP's own defmacro/expansion is the reference
case) needs to evaluate the result of its own expansion through this
same handle_rule/handle_token dispatch, not skip it. Not
Lisp-specific in principle: any grammar with macro-like features needs
this same re-entry point. Genuinely a runtime concern (called from
generated/hand-written Actions code while actually running a parse),
unlike Ichor.generate/3/Ichor.__using__/1, which only ever run at
compile time -- that split is exactly why this lives here, in
ichor_runtime, rather than alongside those two in ichor proper.
Summary
Functions
Eagerly invokes every capture's eval, in true source order (each
%Capture{}'s own position, not captures' incidental map
iteration order -- see Ichor.Capture's moduledoc for why those two
can differ), threading context through -- the plain bottom-up fold
that's correct for grammars with no special-form semantics (most
config/markup/query grammars, as opposed to something like LISP's
if/quote, which need to not evaluate an untaken branch at all).
A list-valued capture (the same name captured more than once, e.g.
inside a *) resolves to a list of values in the same order. Halts
and returns immediately on the first error.
The single entry point tying Grammar.VM's raw capture tree to a
grammar's Actions module: evaluates the root rule, then runs
finalize/1 over whatever context that produced. Returns the final
context alongside the value -- needed whenever more than one top-level
match must thread context forward (e.g. loading a multi-form source
file one top-level form at a time); callers that only need the value
can just ignore it.
Evaluates a raw capture node (as found on any Ichor.Capture.node, or
built directly by something like a macro's unreify) against
actions_module, starting from context. See this module's own
moduledoc for when you need this instead of an ordinary .eval thunk.
Types
@type captures() :: %{optional(atom()) => Ichor.Capture.t() | [Ichor.Capture.t()]}
@type context() :: term()
Callbacks
@callback finalize(context :: context()) :: :ok | {:error, [Ichor.Error.t()]}
@callback handle_token(token :: atom(), text :: String.t(), context :: context()) :: {:ok, term()} | {:error, Ichor.Error.t()}
Functions
@spec eval_all(captures(), context()) :: {:ok, %{optional(atom()) => term()}, context()} | {:error, Ichor.Error.t()}
Eagerly invokes every capture's eval, in true source order (each
%Capture{}'s own position, not captures' incidental map
iteration order -- see Ichor.Capture's moduledoc for why those two
can differ), threading context through -- the plain bottom-up fold
that's correct for grammars with no special-form semantics (most
config/markup/query grammars, as opposed to something like LISP's
if/quote, which need to not evaluate an untaken branch at all).
A list-valued capture (the same name captured more than once, e.g.
inside a *) resolves to a list of values in the same order. Halts
and returns immediately on the first error.
A %Capture{} built by hand rather than via dispatch_rule/5
(directly constructing this map for a test, say) defaults every
position to 0 -- ties between those fall back to whatever order
captures itself happens to enumerate in, same as this function's
behavior before position existed. Give hand-built captures distinct
positions if you need this function's ordering guarantee to hold
for them too.
@spec evaluate( atom(), Ichor.Capture.raw_captures(), module(), context(), capture_shapes() ) :: {:ok, term(), context()} | {:error, Ichor.Error.t() | [Ichor.Error.t()]}
The single entry point tying Grammar.VM's raw capture tree to a
grammar's Actions module: evaluates the root rule, then runs
finalize/1 over whatever context that produced. Returns the final
context alongside the value -- needed whenever more than one top-level
match must thread context forward (e.g. loading a multi-form source
file one top-level form at a time); callers that only need the value
can just ignore it.
capture_shapes (from Grammar.VM.RuleCompiler.capture_shapes/1) says,
per rule, which capture names sit under a */+/{n,m} -- needed so
e.g. an op: that matched zero times still shows up as [], not as a
missing key indistinguishable from "this rule shape never has an
:op" (see that function's docs for why this can't just be inferred
from the raw tree at dispatch time).
@spec evaluate_node(Ichor.Capture.node_t(), module(), context()) :: {:ok, term(), context()} | {:error, Ichor.Error.t()}
Evaluates a raw capture node (as found on any Ichor.Capture.node, or
built directly by something like a macro's unreify) against
actions_module, starting from context. See this module's own
moduledoc for when you need this instead of an ordinary .eval thunk.