Advanced Topics
View SourceThis guide covers lower-level APIs for applications that need direct access to the parse tree, expression inversion, or custom functions.
The parse → validate → evaluate pipeline
Most callers use Elex.evaluate/2, which runs all three stages. For more
control, use the modules directly:
context = Elex.new_context() |> Elex.add_variable("x", 10)
{:ok, ast, type} = Elex.Parser.parse("x + 5", context)
# type => :decimal
Elex.Evaluator.evaluate(ast, context)
# => #Decimal<15>Elex.Parser.parse/3 validates by default. Pass validate: false to parse
without checking variable types — useful for extracting structure before a
context is fully built:
{:ok, ast, nil} = Elex.Parser.parse("x + y", context, validate: false)
Elex.Validator.validate(ast, context)
# => {:ok, :decimal} or {:error, reason}Note:
Elex.Evaluator.evaluate/2raisesRuntimeErroron failures.Elex.evaluate/2catches these and returns{:error, reason}. Prefer the high-level API unless you handle exceptions yourself.
AST format
The parser produces plain Erlang terms (tagged tuples). Common shapes:
| Form | Meaning |
|---|---|
%Decimal{} | Decimal literal |
true, false | Boolean literal |
"string" | String literal |
nil | Null literal |
{:var, "name"} | Variable reference |
{op, [left, right]} | Binary operator (:+, :-, :*, :/, :%, comparisons, and, or) |
{:not, operand} | Logical not |
{-, operand} | Unary minus |
{:func, name, arity, args} | Function call (name is a string, args is a list of AST nodes) |
Example AST for max(x, 10) + 1:
{:+, [
{:func, "max", 2, [{:var, "x"}, #Decimal<10>]},
#Decimal<1>
]}Parser debugging
Elex.Parser.debug/2 exposes raw NimbleParsec output for troubleshooting. It
does not validate against a context:
info = Elex.Parser.debug("( 1 + 2")
info.status # :error
info.rest # "( 1 + 2"
info.reason # raw parser messageThe result is a map documented as debug_info/0 on Elex.Parser. For end-user
error messages, use Elex.Parser.parse/3 or Elex.evaluate/2 instead.
Expression inversion
Elex.Inverter solves simple single-variable arithmetic expressions. Given an
AST and a target variable name, it returns an inverted AST representing the
inverse operation.
Supported operations: +, -, *, / (with the variable on one side only).
alias Elex.{Parser, Inverter}
context = Elex.new_context() |> Elex.add_variable("value", 0)
{:ok, ast, _} = Parser.parse("value * 2 + 5", context, validate: false)
{:ok, inverted} = Inverter.invert(ast, "value")
# Evaluate inverted AST with a known result to recover the original variable:
result_context = Elex.new_context() |> Elex.add_variable("value", 21)
Elex.Evaluator.evaluate(inverted, result_context)
# => #Decimal<8> (because (8 * 2) + 5 = 21)Inverter.invert/2 returns {:error, reason} when:
- The expression contains more than one variable
- The target variable is not present
- The expression uses unsupported operations (comparisons, functions, etc.)
- Division by zero would occur during inversion
Custom functions
Implement the Elex.Function behaviour with four callbacks:
| Callback | Purpose |
|---|---|
signature/0 | Function name and arity (or variadic spec) |
validate/2 | Type-check unevaluated argument ASTs at parse time |
call/1 | Execute with evaluated argument values |
documentation/0 | Human-readable signature and description |
Example
defmodule MyApp.Functions.Double do
@behaviour Elex.Function
@impl true
def signature, do: %{name: :double, arity: 1}
@impl true
def validate([arg], ctx) do
case Elex.Validator.validate(arg, ctx) do
{:ok, :decimal} -> {:ok, :decimal}
{:ok, type} -> {:error, "double expects a number, got #{inspect(type)}"}
{:error, reason} -> {:error, reason}
end
end
@impl true
def call([arg]), do: {:ok, Decimal.mult(arg, Decimal.new(2))}
@impl true
def documentation do
%{signature: "double(x)", description: "returns x multiplied by 2"}
end
endRegister the function on a context:
context =
Elex.new_context()
|> Elex.Context.add_function(MyApp.Functions.Double)
{:ok, result} = Elex.evaluate("double(5)", context)
# => #Decimal<10>Variadic functions
Return a variadic signature with min_arity:
def signature do
%{name: :sum, variadic: true, min_arity: 2}
endThe context stores variadic functions under {name, :variadic}.
Localizing type error labels
Elex.Labels.label/1 maps type atoms to human-readable names used in error
messages (:decimal → "number", etc.). Override this function in your
application to integrate with Gettext for localization.
Further reading
Elex.Parser— parse options including:max_depthElex.Validator— direct AST validationElex.Evaluator— direct AST evaluationElex.Function— behaviour reference- Functions — built-in function list