Quick reference for everything in ichor_runtime, worked examples for
the standalone pieces (Pratt, TermWalk, Backtrack, Result). See
the README for the big picture, or
Ichor's own docs for grammar authoring
(.aether syntax, mix ichor.gen, use Ichor).
What an Ichor.Actions module receives
You never call Ichor.Actions.evaluate/5 yourself -- a generated
run/1,2 calls it, dispatching into your own Ichor.Actions
implementation. This is the shape your callbacks see:
defmodule MyLang.Actions do
@behaviour Ichor.Actions
@impl true
def handle_token(:NUMBER, text, _ctx), do: {:ok, String.to_integer(text)}
@impl true
def handle_rule(:add, %{left: l, right: r}, ctx) do
{:ok, lv, ctx} = l.eval.(ctx)
{:ok, rv, ctx} = r.eval.(ctx)
{:ok, lv + rv, ctx}
end
# optional: runs once, after everything else
@impl true
def finalize(ctx), do: :ok
end- Implement only the rules/tokens that need custom behavior -- anything
else falls back automatically: a rule with exactly one meaningful
capture passes its value straight through; more than one builds an
%Ichor.Node{rule: ..., captures: %{...}}; an unhandled token's raw text passes through unchanged. - A capture under
*/+/{n,m}always arrives as a list, even if it matched zero or one times -- never a bare value, never a missing key. - Each capture is an
%Ichor.Capture{}:.nodeis the raw, unevaluated parse (Ichor.Capture.node_t()--{:token, name, text}/{:rule, name, sub_captures}/{:text, text});.eval.(ctx)evaluates it and returns{:ok, value, new_ctx}. Call.evalonly when you actually want the value -- this is what lets anif/quote- style special form skip evaluating a branch entirely. Ichor.Actions.evaluate_node/3re-enters this same dispatch for a capture node that didn't come from the original parse -- a macro's own expansion, most notably (LISP'sdefmacro):Ichor.Actions.evaluate_node({:rule, :form, captures}, MyLang.Actions, ctx).- Errors are
%Ichor.Error{}(Ichor.Error.new/1,Ichor.Error.format/1) -- the same struct every stage (lexer, parser, your own actions) uses, so error output looks consistent regardless of where it came from.
@native(...) callbacks
The behaviours a grammar's own @native("Module", "function", ...)
escape hatch dispatches to -- you implement these, nothing here calls
them for you:
defmodule MyRuleCallback do
@behaviour Ichor.CustomRule
@impl true
def match(stream, pos, context, rule_matchers) do
# stream/pos: the token stream and current position, same contract
# as any compiled rule matcher. context: read-only, whatever the
# previous top-level form evaluated to. rule_matchers: a map
# restricted to this node's own declared dependencies, each
# (stream, pos, context -> {:ok, new_pos, ref_stack, raw_captures} | :fail).
:fail
end
end
defmodule MyLexemeCallback do
@behaviour Ichor.CustomLexeme
@impl true
def scan(input, context, rule_matchers) do
# input: the *remaining suffix* of the source text. Returns
# {:ok, text, rest, capture} where text <> rest == input, and
# capture is nil (ordinary {:token, name, text} capture) or an
# explicit Ichor.Capture.node_t() override.
:fail
end
endIchor.Toolkit.Pratt: operator-precedence expression parsing
A runtime-mutable table (prefix/infix/postfix operators, any precedence)
plus a driver that climbs it. Complete worked example -- arithmetic with
prefix -, infix + - * /, postfix ! (factorial), and % registered
as both infix (modulo) and postfix (percent) -- the one genuinely
ambiguous case can_start_operand? exists to resolve:
alias Ichor.Toolkit.Pratt
table =
Pratt.new()
|> Pratt.prefix("-", 100)
|> Pratt.infix("+", 10)
|> Pratt.infix("-", 10)
|> Pratt.infix("*", 20)
|> Pratt.infix("/", 20)
|> Pratt.infix("%", 20)
|> Pratt.postfix("!", 30)
|> Pratt.postfix("%", 30)
tokens = {{:num, 5}, {:op, "!"}, {:op, "+"}, {:num, 2}} # "5! + 2"
# peek_op must return nil, not crash, once pos runs off the end --
# elem/2 doesn't bounds-check, so every callback goes through this.
at = fn pos -> if pos < tuple_size(tokens), do: elem(tokens, pos), else: nil end
callbacks = %{
peek_op: fn pos ->
case at.(pos) do
{:op, name} -> {name, pos + 1}
_ -> nil
end
end,
parse_primary: fn pos ->
case at.(pos) do
{:num, n} -> {:ok, pos + 1, n}
_ -> :fail
end
end,
# only needed because "%" is registered both infix and postfix --
# resolves the ambiguity by checking whether a valid operand follows
can_start_operand?: fn pos -> match?({:num, _}, at.(pos)) end,
build: fn
:prefix, "-", [a] -> -a
:infix, "+", [a, b] -> a + b
:infix, "-", [a, b] -> a - b
:infix, "*", [a, b] -> a * b
:infix, "/", [a, b] -> a / b
:infix, "%", [a, b] -> rem(a, b)
:postfix, "!", [a] -> Enum.reduce(1..a, 1, &*/2)
:postfix, "%", [a] -> a / 100
end
}
Pratt.parse(table, 0, callbacks)
#=> {:ok, 4, 122} -- pos 4 (consumed everything), value 5! + 2 = 122min_prec (the optional 4th argument to parse/4, default 0) is what
recursion uses internally to only accept an operator binding at least
that tightly -- pass a higher value yourself if you're embedding a
sub-expression that itself must bind tighter than some enclosing
context.
Ichor.Backtrack: logic-language evaluation (Prolog-style SLD-resolution)
A substitution (Bindings) plus a lazy, depth-first search
(Tree) over any term representation you supply (Term):
defmodule MyTerms do
@behaviour Ichor.Backtrack.Term
def variable?({:var, _id}), do: true
def variable?(_), do: false
def var_id({:var, id}), do: id
def compound?({:compound, _f, _args}), do: true
def compound?(_), do: false
def deconstruct({:compound, f, args}), do: {f, args}
# optional -- only needed if you ever rebuild a term (substitution):
def reconstruct(f, args), do: {:compound, f, args}
end
alias Ichor.Backtrack.{Tree, Bindings}
x = {:var, make_ref()}
t1 = {:compound, :father, [:tom, x]}
t2 = {:compound, :father, [:tom, :bob]}
Bindings.unify(MyTerms, Bindings.new(), t1, t2)
#=> {:ok, bindings} -- x now bound to :bob
Bindings.resolve(MyTerms, bindings, x)
#=> :bob
Bindings.unify_occurs_check(MyTerms, Bindings.new(), x, {:compound, :f, [x]})
#=> :fail -- unify/4 would instead silently build an infinite term,
# matching ISO Prolog's own default (unify_with_occurs_check/2
# is opt-in there too); a type checker generally wants thisA goal is a function bindings -> lazy solution sequence
(Tree.unit/0/Tree.fail/0 are the two trivial ones; Tree.disjunction/2/
Tree.conjunction/2/Tree.once/1 combine goals into bigger ones). A
goal that unifies a query against one fact, tried against a small
"database" via disjunction/2:
unify_goal = fn fact, query ->
fn bindings ->
case Bindings.unify(MyTerms, bindings, query, fact) do
{:ok, new_bindings} -> Tree.of_one(new_bindings)
:fail -> Tree.empty()
end
end
end
facts = [
{:compound, :father, [:tom, :bob]},
{:compound, :father, [:bob, :ann]}
]
query = {:compound, :father, [:tom, {:var, :who}]}
goal =
facts
|> Enum.map(&unify_goal.(&1, query))
|> Enum.reduce(Tree.fail(), &Tree.disjunction/2)
goal.(Bindings.new())
|> Tree.to_list()
|> Enum.map(&Bindings.resolve(MyTerms, &1, {:var, :who}))
#=> [:bob] -- every solution's own binding for `who`, eagerly collected
# (Tree.to_list/1 is for tests/finite searches only -- pull
# one at a time via Tree.next/1 for a search that might not
# terminate)Ichor.Toolkit.TermWalk: generic recursion over a Term
fold/4 (visit every node, accumulate) and rewrite/3 (transform every
node, top-down, rebuilding compounds) over any Ichor.Backtrack.Term:
alias Ichor.Toolkit.TermWalk
term = {:compound, :add, [{:var, :x}, {:compound, :mul, [{:var, :y}, 2]}]}
TermWalk.fold(MyTerms, term, MapSet.new(), fn
{:var, id}, acc -> MapSet.put(acc, id)
_other, acc -> acc
end)
#=> MapSet.new([:x, :y]) -- every variable id referenced anywhere in term
TermWalk.rewrite(MyTerms, term, fn
{:var, :x} -> 10
other -> other
end)
#=> {:compound, :add, [10, {:compound, :mul, [{:var, :y}, 2]}]}Ichor.Toolkit.Result: fallible-step traversal
alias Ichor.Toolkit.Result
Result.reduce_ok([1, 2, 3], 0, fn x, acc -> {:ok, acc + x} end)
#=> {:ok, 6}
Result.reduce_ok([1, -1, 2], 0, fn
x, _acc when x < 0 -> {:error, "negative: #{x}"}
x, acc -> {:ok, acc + x}
end)
#=> {:error, "negative: -1"} -- stops at the first failure
Result.map_ok([1, 2, 3], :state, fn x, state -> {:ok, x * 2, state} end)
#=> {:ok, [2, 4, 6], :state}reduce_ok/3 passes any non-{:ok, _} step result straight through
unexamined -- works equally for a bare :fail atom or {:error, reason},
whatever your own step function returns as failure. map_ok/3 is built
on reduce_ok/3.