Skip to main content

guides/advanced.md

# Advanced Topics

This 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:

```elixir
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:

```elixir
{: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/2` raises `RuntimeError` on failures.
> `Elex.evaluate/2` catches 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`:

```elixir
{:+, [
  {: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:

```elixir
info = Elex.Parser.debug("( 1 + 2")
info.status   # :error
info.rest     # "( 1 + 2"
info.reason   # raw parser message
```

The 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).

```elixir
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

```elixir
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
end
```

Register the function on a context:

```elixir
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`:

```elixir
def signature do
  %{name: :sum, variadic: true, min_arity: 2}
end
```

The 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_depth`
- `Elex.Validator` — direct AST validation
- `Elex.Evaluator` — direct AST evaluation
- `Elex.Function` — behaviour reference
- [Functions](functions.md) — built-in function list