A small, safe predicate language for Elixir: parse a rule a person wrote, such
as renewals < 3 AND NOT on_hold, and evaluate it against a context of data.
A rule compiles to a flat list of instructions that a small stack machine
runs, so the text never becomes code. Short programs - assignments, if/else
and while - run the same way and write their results into the context.
Without one, an application that lets its users or its configuration decide
when something may happen has two poor choices. It can write every condition
in code, so each new rule waits for a release; or it can evaluate the text a
person typed, so a typo or a hostile string runs inside the application. With
Predicator the rule is data: it is parsed by a grammar of comparisons, logic,
arithmetic, membership, dates, lists and function calls, compiled to
instructions that are plain terms you can store, and evaluated with no eval
anywhere in the path. A rule that names a value the data does not carry comes
back as an error value rather than a crash, and the same compiled rule runs as
often as you like.
Add predicator to your list of dependencies in mix.exs:
def deps do
[
{:predicator, "~> 9.4"}
]
endA library loan: a copy of a book lent to a patron, due on a date, renewed, returned or lost. The library lets staff write the renewal rule as text. Compile it once, evaluate it against each loan, and read a missing value as an error rather than a crash; a short program records the renewal itself.
iex> {:ok, may_renew} = Predicator.compile("status == 'on_loan' AND renewals < 3 AND NOT on_hold")
iex> Predicator.evaluate(may_renew, %{"status" => "on_loan", "renewals" => 1, "on_hold" => false})
{:ok, true}
iex> Predicator.evaluate(may_renew, %{"status" => "on_loan", "renewals" => 3, "on_hold" => false})
{:ok, false}
iex> {:error, error} = Predicator.evaluate(may_renew, %{"status" => "on_loan"})
iex> error.variable
"renewals"
iex> {:ok, context} = Predicator.execute("if renewals < 3 { renewals = renewals + 1; due_in_days = 21 }", %{"renewals" => 1})
iex> context.data
%{"due_in_days" => 21, "renewals" => 2}may_renew is a plain list of instructions, so it can sit in ETS, in a
process's state or in a database column until the next loan comes back.
- Learn
- Basic usage: a renewal rule compiled once and evaluated against loans.
- Do
- Add custom functions: give rules a function of your own, and the host state it reads.
- Embed compiled programs: store a compiled rule, check its instruction-set version, and evaluate it later.
- Port Predicator to another language: implement the instruction set and verify it against the conformance corpus.
- Look up
- Language reference: operators, builtin functions, data types, statements, and error shapes.
- Running a program from Elixir:
execute/3,execute_value/3, and the roots a program may not overwrite. - The vocabulary for an editor: every operator, keyword and function name, for a completion list.
- Nested data access: dot and bracket notation over deep contexts.
- Location expressions: the targets an assignment may write, and how they resolve.
- The simple subset: a rule as field, operator and value rows, for a form-based editor.
- AST reference: the tree
Predicator.parse/2returns, node by node. - ISA reference: the instruction set's opcodes, stack effects, error semantics and versions.
- API reference: every public module and function.
- The changelog: what changed in each version.
- Understand
- Why a predicate language of its own: the alternatives turned down - rules in code,
eval, host calls by name, a JSON rule format - and what the choice costs. - Architecture: the compilation pipeline, the component map, and the design decisions behind them.
- Cross-language siblings: how implementations in other languages adopt each instruction-set version.
- Why
=is assignment, never equality: the grammar break and what it left untouched. - Why this implementation leads the instruction set: the versioned contract the siblings follow.
- The decision records: the reasoning behind the rest of the design.
- Why a predicate language of its own: the alternatives turned down - rules in code,
Predicator needs Elixir 1.18 or later (elixir: "~> 1.18" in mix.exs) and
has no runtime dependencies.
A compiled rule carries the version of the instruction set it was compiled for, which moves separately from the package version. Check that version before evaluating a rule stored under an earlier release, and the rule either still runs or is refused with a message naming why, never silently mis-run; Embed compiled programs shows the check. Every change a caller can observe is recorded in the changelog.
docs/contributing.md has the quality-check commands and the checklists for adding operators and data types.
MIT - see LICENSE.