Ichor.Actions behaviour (Ichor v0.1.1)

Copy Markdown View Source

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, and this module's evaluate/5 walks it, calling into the actions module (or the default fallback) as it goes, threading context throughout.

Summary

Functions

Eagerly invokes every capture's eval, in order, 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.

Types

capture_shapes()

@type capture_shapes() :: %{optional(atom()) => MapSet.t(atom())}

captures()

@type captures() :: %{optional(atom()) => Ichor.Capture.t() | [Ichor.Capture.t()]}

context()

@type context() :: term()

Callbacks

finalize(context)

(optional)
@callback finalize(context :: context()) :: :ok | {:error, [Ichor.Error.t()]}

handle_rule(rule, captures, context)

(optional)
@callback handle_rule(rule :: atom(), captures :: captures(), context :: context()) ::
  {:ok, term(), context()} | {:error, Ichor.Error.t()}

handle_token(token, text, context)

(optional)
@callback handle_token(token :: atom(), text :: String.t(), context :: context()) ::
  {:ok, term()} | {:error, Ichor.Error.t()}

Functions

eval_all(captures, context)

@spec eval_all(captures(), context()) ::
  {:ok, %{optional(atom()) => term()}, context()} | {:error, Ichor.Error.t()}

Eagerly invokes every capture's eval, in order, 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.

evaluate(root_rule, raw_captures, actions_module, initial_context, capture_shapes \\ %{})

@spec evaluate(atom(), map(), 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).