defmodule ExCellerate do @moduledoc """ ExCellerate is a high-performance expression evaluation engine for Elixir. It parses text expressions into an intermediate representation (IR) and then compiles them into native Elixir AST for near-native performance. ## Operators - **Arithmetic**: `+`, `-`, `*`, `/`, `^` (power), `%` (modulo), `n!` (factorial) - **Comparison**: `==`, `!=`, `<`, `<=`, `>`, `>=` - **Logical**: `&&`, `||`, `not` - **Bitwise**: `&`, `|`, `|^` (xor), `<<`, `>>`, `~` (bnot) - **Ternary**: `condition ? true_val : false_val` - **Data access**: `user.profile.name`, `list[0]`, `list[*].field` (spread) ## Built-in Functions ### Math | Function | Description | |----------|-------------| | `abs(n)` | Absolute value | | `round(n)` or `round(n, digits)` | Rounds to nearest integer, or to `digits` decimal places | | `floor(n)` | Largest integer ≤ `n` | | `ceil(n)` | Smallest integer ≥ `n` | | `trunc(n)` | Truncates toward zero (unlike `floor` for negatives) | | `max(a, b, ...)` or `max(list)` | Maximum of arguments or a list | | `min(a, b, ...)` or `min(list)` | Minimum of arguments or a list | | `sign(n)` | Returns -1, 0, or 1 | | `sqrt(n)` | Square root | | `exp(n)` | e raised to the power `n` | | `ln(n)` | Natural logarithm (base e) | | `log(n, base)` | Logarithm with specified base | | `log10(n)` | Base-10 logarithm | | `sum(a, b, ...)` or `sum(list)` | Sums arguments or a list | | `avg(a, b, ...)` or `avg(list)` | Arithmetic mean of arguments or a list | ### String | Function | Description | |----------|-------------| | `len(s)` or `len(list)` | String length or list length | | `left(s)` or `left(s, n)` | First character, or first `n` characters | | `right(s)` or `right(s, n)` | Last character, or last `n` characters | | `substring(s, start)` | Substring from `start` to end | | `substring(s, start, len)` | Substring of `len` characters | | `upper(s)` | Converts to uppercase | | `lower(s)` | Converts to lowercase | | `trim(s)` | Removes leading/trailing whitespace | | `concat(a, b, ...)` | Concatenates values into a string | | `textjoin(delim, a, b, ...)` | Joins values with a delimiter | | `replace(s, old, new)` | Replaces all occurrences of `old` with `new` | | `find(search, text)` or `find(search, text, start)` | 0-based position of `search` in `text`, optionally from `start` | | `contains(s, term)` | Returns `true` if `term` exists within `s` | | `underscore(s)` | Downcases, replaces spaces/slashes with underscores, strips special chars | | `slug(s)` | Downcases, replaces spaces/slashes with hyphens, strips special chars | ### Utility | Function | Description | |----------|-------------| | `if(cond, t)` or `if(cond, t, f)` | Returns `t` if truthy; `f` (or `nil`) otherwise | | `ifs(c1, v1, c2, v2, ...)` | Returns value for first truthy condition; `nil` if none match | | `ifnull(val, default)` | Returns `default` if `val` is nil | | `isnull(val)` | Returns `true` if `val` is nil, `false` otherwise | | `isblank(val)` | Returns `true` if `val` is nil or a whitespace-only string | | `coalesce(a, b, ...)` | Returns the first non-nil value | | `switch(expr, c1, v1, ..., default)` | Multi-way value matching | | `and(a, b, ...)` | Returns `true` if all arguments are truthy | | `or(a, b, ...)` | Returns `true` if any argument is truthy | | `lookup(coll, key)` | Looks up `key` in a map or index in a list | | `lookup(coll, key, default)` | Same, with a default for missing keys | | `match(value, list)` | Returns the 0-based position of `value` in `list` (exact match) | | `match(value, list, type)` | Approximate match: `1` for ascending (<=), `-1` for descending (>=), `0` for exact | | `index(list, row)` | Returns the element at 0-based position `row` | | `index(array, row, col)` | Returns the element at `row` and `col` in a 2D array | | `filter(list, predicates)` | Returns items where predicate is `true` | | `table(key1, list1, ...)` | Builds a list of maps from key/list pairs | | `take(list, rows)` | Takes the first/last `rows` elements (negative counts from end) | | `take(list, rows, cols)` | Takes rows and columns from a 2D array; pass `null` to skip a dimension | | `slice(list, start)` | Returns elements from `start` index to end (negative counts from end) | | `slice(list, start, len)` | Returns `len` elements starting at `start` | ### Special Forms | Form | Description | |------|-------------| | `let(name, value, expr)` | Lexically binds `name` within `expr` only | Custom functions can be added via the `ExCellerate.Registry` system. ## Multi-line Expressions Expressions can be formatted across multiple lines for readability. Newlines are treated as whitespace by the parser: expr = \"\"\" ifs( score > 90, 'A', score > 80, 'B', score > 70, 'C', true, 'F' ) \"\"\" ExCellerate.eval!(expr, %{"score" => 85}) # => "B" This works with `validate/2` and `compile/2` as well. ## Nil Propagation Dot and bracket access use **nil propagation**: if any key in a path is missing or the target is `nil`, the entire expression returns `nil` instead of raising an error. This mirrors how spreadsheets treat empty cells and avoids the need for defensive checks at every level of a nested path. ExCellerate.eval!("user.profile.name", %{"user" => %{}}) # => nil (profile is missing, so .name is never attempted) ExCellerate.eval!("user.name", %{"user" => nil}) # => nil (user is nil, short-circuits) ExCellerate.eval!("list[99]", %{"list" => [1, 2, 3]}) # => nil (index out of bounds) Use `ifnull/2`, `coalesce/2+`, or a ternary to provide defaults: ExCellerate.eval!("ifnull(user.name, 'anonymous')", %{"user" => %{}}) # => "anonymous" **Root variable lookup still raises.** Referencing a variable that doesn't exist in the scope at all (e.g., `unknown_var`) is treated as a likely typo and returns `{:error, %ExCellerate.Error{}}`: ExCellerate.eval("totally_unknown", %{}) # => {:error, %ExCellerate.Error{message: "variable not found: totally_unknown"}} ## Examples iex> ExCellerate.eval!("1 + 2 * 3") 7 iex> ExCellerate.eval("a + b", %{"a" => 10, "b" => 20}) {:ok, 30} iex> ExCellerate.eval!("user.name", %{"user" => %{"name" => "Alice"}}) "Alice" ## Column Spread (`[*]`) The `[*]` operator extracts a field from every element of a list, returning a new list of values. This enables column-oriented operations on lists of maps, structs, or nested lists: # Given a list of maps in scope scope = %{"orders" => [ %{"product" => "Widget", "price" => 10, "qty" => 2}, %{"product" => "Gadget", "price" => 25, "qty" => 1} ]} ExCellerate.eval!("orders[*].product", scope) # => ["Widget", "Gadget"] ExCellerate.eval!("sum(orders[*].price)", scope) # => 35 Spread results work with any function that accepts a list: `sum`, `avg`, `max`, `min`, `len`, and `textjoin`. Subsequent access chains apply to each element: ExCellerate.eval!("orders[*].price", scope) # => [10, 25] ExCellerate.eval!("users[*].profile.name", scope) # deep access Bracket indexing after a spread selects a specific position from each element's sub-list: # scores[1] from each row ExCellerate.eval!("rows[*].scores[1]", scope) Nested `[*]` operators flatten across levels: # departments[*].employees[*].name => all employee names, flattened ### Computed Spread (`.(expr)`) To evaluate an expression *per element* of a spread, use the `.(expr)` syntax. Inside the parentheses, bare variable names resolve against each element rather than the outer scope: ExCellerate.eval!("orders[*].(qty * price)", scope) # => [20, 25] (per-row product) ExCellerate.eval!("sum(orders[*].(qty * price))", scope) # => 45 (total of per-row products) Computed spreads also compose with nested `[*]`: # departments[*].employees[*].(salary * 12) => annualised salaries, flattened ## Let, Filter, and Table `let/3` introduces a lexical binding that is visible only inside the body expression; it does not mutate the outer scope: # Given scope = %{"orders" => [%{"price" => 10, "qty" => 2}, ...]} let(total, sum(orders[*].price), total + 5) `filter/2` selects items from a list using a boolean list produced by a computed spread. The predicate list must be the same length as the input list: filter(orders, orders[*].(qty > 1)) `table` builds a list of maps from alternating key/list pairs. Use spread or computed spread to produce the list columns: table('product', orders[*].product, 'total', orders[*].(qty * price)) # => [%{"product" => "Widget", "total" => 20}, ...] These compose naturally — filter first, then build a summary table: let(big, filter(orders, orders[*].(qty > 1)), table('product', big[*].product, 'total', big[*].(qty * price))) ## Take and Slice `take/2` and `take/3` extract rows, columns, or both from a list or 2D array. Positive counts take from the beginning, negative from the end. Pass `null` to skip a dimension: take(data, 3) # first 3 rows take(data, -3) # last 3 rows take(data, null, 2) # first 2 columns (all rows) take(data, 2, 2) # first 2 rows and 2 columns `slice/2` and `slice/3` extract a contiguous section of a list by start index and optional length. The start index is zero-based, and negative indices count from the end: slice(items, 1) # from index 1 to end slice(items, 1, 3) # 3 elements starting at index 1 slice(items, -2) # last 2 elements """ alias ExCellerate.Compiler alias ExCellerate.Parser @type scope :: %{optional(String.t()) => any()} @type registry :: module() | nil @doc """ Evaluates a text expression against an optional scope and registry. Returns `{:ok, result}` on success or `{:error, %ExCellerate.Error{}}` on failure. ## Parameters - `expression` — a string containing the expression. - `scope` — a map of variables available to the expression. Supports string keys, atom keys, and structs. Defaults to `%{}`. - `registry` — an optional module created with `use ExCellerate.Registry`. ## Examples iex> ExCellerate.eval("1 + 2 * 3") {:ok, 7} iex> ExCellerate.eval("a + b", %{"a" => 10, "b" => 20}) {:ok, 30} iex> ExCellerate.eval("user.name", %{"user" => %{"name" => "Alice"}}) {:ok, "Alice"} iex> {:error, _} = ExCellerate.eval("1 + * 2") """ @spec eval(String.t(), scope(), registry()) :: {:ok, any()} | {:error, Exception.t()} def eval(expression, scope \\ %{}, registry \\ nil) do case compile(expression, registry) do {:ok, fun} -> try do {:ok, fun.(scope)} rescue e -> {:error, e} end {:error, reason} -> {:error, reason} end end @doc """ Similar to `eval/3`, but returns the result directly or raises on error. ## Examples iex> ExCellerate.eval!("1 + 2 * 3") 7 iex> ExCellerate.eval!("a > 10 ? 'high' : 'low'", %{"a" => 15}) "high" iex> ExCellerate.eval!("concat('Hello', ' ', name)", %{"name" => "Alice"}) "Hello Alice" iex> ExCellerate.eval!("user.profile.id", %{"user" => %{"profile" => %{"id" => 1}}}) 1 """ @spec eval!(String.t(), scope(), registry()) :: any() def eval!(expression, scope \\ %{}, registry \\ nil) do case eval(expression, scope, registry) do {:ok, result} -> result {:error, error} -> raise error end end @doc """ Compiles an expression into a reusable function. Returns `{:ok, fun}` where `fun` is a 1-arity function that accepts a scope map and returns the result. The compiled function is cached, so calling `compile/2` repeatedly with the same expression is cheap. This is useful when you need to evaluate the same expression many times with different scopes — the parsing and compilation happen only once. ## Examples iex> {:ok, fun} = ExCellerate.compile("a + b") iex> fun.(%{"a" => 1, "b" => 2}) 3 """ @spec compile(String.t(), registry()) :: {:ok, (scope() -> any())} | {:error, ExCellerate.Error.t()} def compile(expression, registry \\ nil) do case ExCellerate.Cache.get(registry, expression) do {:ok, fun} -> {:ok, fun} :error -> case compile_to_function(expression, registry) do {:ok, fun} -> ExCellerate.Cache.put(registry, expression, fun) {:ok, fun} {:error, _} = error -> error end end end @doc """ Similar to `compile/2`, but returns the function directly or raises on error. ## Examples iex> fun = ExCellerate.compile!("x * 2") iex> fun.(%{"x" => 5}) 10 """ @spec compile!(String.t(), registry()) :: (scope() -> any()) def compile!(expression, registry \\ nil) do case compile(expression, registry) do {:ok, fun} -> fun {:error, error} -> raise error end end @doc """ Validates that an expression is syntactically correct and all referenced functions exist in the registry or defaults. Returns `:ok` if the expression is valid, or `{:error, %ExCellerate.Error{}}` if it contains syntax errors or references unknown functions. **Note:** Validation checks syntax, function existence, and arity, but does not perform type checking. Since scope values are not known until runtime, type errors (e.g., passing a number to `upper/1`) will only be caught at evaluation time. ## Examples iex> ExCellerate.validate("1 + 2") :ok iex> ExCellerate.validate("abs(-1)") :ok iex> {:error, _} = ExCellerate.validate("1 +") iex> {:error, _} = ExCellerate.validate("unknown_func(1)") """ @spec validate(String.t(), registry()) :: :ok | {:error, ExCellerate.Error.t()} def validate(expression, registry \\ nil) do case compile(expression, registry) do {:ok, _fun} -> :ok {:error, reason} -> {:error, reason} end end # Parses and compiles an expression string into a reusable function. # The function accepts a scope map and returns the evaluation result. # The scope parameter must use var!(scope) in the Compiler context # to match the references generated by Compiler.compile/2. defp compile_to_function(expression, registry) do case Parser.parse(expression) do {:ok, ast} -> try do elixir_ast = Compiler.compile(ast, registry) scope_var = Compiler.scope_var() fun_ast = {:fn, [], [{:->, [], [[scope_var], elixir_ast]}]} {fun, _} = Code.eval_quoted(fun_ast, [], __ENV__) {:ok, fun} rescue e -> {:error, e} end {:error, reason} -> {:error, reason} end end end