Built-in predicates: comparisons, arithmetic, type checks, string predicates, membership, and extensible constraint evaluation.
Constraints appear in rule bodies alongside relational atoms. They come in several categories:
Comparison constraints — filter bindings. They do not introduce new variable bindings. Both
leftandrightmust be bound before the constraint is evaluated. Theresultfield isnil.Arithmetic constraints — bind a new variable. The
resultfield names the variable that receives the computed value.leftandrightmust be bound; after evaluationresultis added to the binding environment.Type predicate constraints — unary filters that check the Elixir type of a bound value. The
rightandresultfields arenil.String predicate constraints — binary filters for string operations. Both operands must be bound and resolve to binaries. The
resultfield isnil.Membership constraints — test whether a value is in a constant list.
leftmust be bound;rightis a constant list. Theresultfield isnil.
Extensible evaluation
The ExDatalog.Constraint behaviour defines a evaluate/3 callback that
constraint modules implement. Evaluation dispatches based on the constraint's
op field:
- Comparison ops (
gt,lt,gte,lte,eq,neq) dispatch toExDatalog.Constraints.Comparison. - Arithmetic ops (
add,sub,mul,div) dispatch toExDatalog.Constraints.Arithmetic. - Type predicate ops (
is_integer,is_binary,is_atom) dispatch toExDatalog.Constraints.Type. - String predicate ops (
starts_with,contains) dispatch toExDatalog.Constraints.StringPredicate. - Membership op (
member) dispatch toExDatalog.Constraints.Membership.
New constraint types require adding the op to the relevant category list
and a dispatch clause in constraint_module/1 within this module. The
dispatch is closed (not a runtime registry) to keep evaluation deterministic
and easily auditable.
Comparison operators
| Constructor | Meaning |
|---|---|
gt/2 | left > right |
lt/2 | left < right |
gte/2 | left >= right |
lte/2 | left <= right |
eq/2 | left == right |
neq/2 | left != right |
Arithmetic operators
| Constructor | Meaning |
|---|---|
add/3 | result = left + right |
sub/3 | result = left - right |
mul/3 | result = left * right |
div/3 | result = div(left, right) (integer division) |
All arithmetic is integer-only. The :div operator uses Elixir's
Kernel.div/2 (truncating integer division). Division by zero returns
:div_by_zero and filters the binding.
Type predicates
| Constructor | Meaning |
|---|---|
type_integer/1 | checks if the bound value is an integer |
type_binary/1 | checks if the bound value is a binary (string) |
type_atom/1 | checks if the bound value is an atom |
String predicates
| Constructor | Meaning |
|---|---|
starts_with/2 | String.starts_with?(left, right) |
contains/2 | String.contains?(left, right) |
Membership
| Constructor | Meaning |
|---|---|
member/2 | left in right (right is a constant list) |
Examples
iex> ExDatalog.Constraint.gt({:var, "X"}, {:const, 0})
%ExDatalog.Constraint{op: :gt, left: {:var, "X"}, right: {:const, 0}, result: nil}
iex> ExDatalog.Constraint.add({:var, "X"}, {:var, "Y"}, {:var, "Z"})
%ExDatalog.Constraint{op: :add, left: {:var, "X"}, right: {:var, "Y"}, result: {:var, "Z"}}
iex> ExDatalog.Constraint.type_integer({:var, "X"})
%ExDatalog.Constraint{op: :is_integer, left: {:var, "X"}, right: nil, result: nil}
iex> ExDatalog.Constraint.starts_with({:var, "X"}, {:const, "hello"})
%ExDatalog.Constraint{op: :starts_with, left: {:var, "X"}, right: {:const, "hello"}, result: nil}
iex> ExDatalog.Constraint.member({:var, "X"}, {:const, [:a, :b, :c]})
%ExDatalog.Constraint{op: :member, left: {:var, "X"}, right: {:const, [:a, :b, :c]}, result: nil}
Summary
Callbacks
Evaluates a constraint against a binding environment.
Functions
Constructs an addition constraint: result = left + right.
Returns true if the constraint is arithmetic (binds a result variable).
Returns true if the constraint is a comparison (filters, does not bind).
Constructs a contains constraint: String.contains?(left, right).
Constructs an integer division constraint: result = div(left, right).
Constructs an equality constraint: left == right.
Constructs a greater-than constraint: left > right.
Constructs a greater-than-or-equal constraint: left >= right.
Returns all input variable names referenced by the constraint.
Constructs a less-than constraint: left < right.
Constructs a less-than-or-equal constraint: left <= right.
Constructs a membership constraint: left in right.
Returns true if the constraint is a membership test.
Constructs a multiplication constraint: result = left * right.
Constructs an inequality constraint: left != right.
Returns the result variable name for an arithmetic constraint, or nil for comparisons.
Constructs a starts-with constraint: String.starts_with?(left, right).
Returns true if the constraint is a string predicate (binary, filters).
Constructs a subtraction constraint: result = left - right.
Constructs an atom type-check constraint.
Constructs a binary (string) type-check constraint.
Constructs an integer type-check constraint.
Returns true if the constraint is a type predicate (unary, filters).
Returns true if the constraint is structurally valid.
Types
@type arithmetic() :: %ExDatalog.Constraint{ left: ExDatalog.Term.t(), op: :add | :sub | :mul | :div, result: {:var, String.t()}, right: ExDatalog.Term.t() }
@type comparison() :: %ExDatalog.Constraint{ left: ExDatalog.Term.t(), op: :gt | :lt | :gte | :lte | :eq | :neq, result: nil, right: ExDatalog.Term.t() }
@type membership() :: %ExDatalog.Constraint{ left: ExDatalog.Term.t(), op: :member, result: nil, right: ExDatalog.Term.t() }
@type op() ::
:gt
| :lt
| :gte
| :lte
| :eq
| :neq
| :add
| :sub
| :mul
| :div
| :is_integer
| :is_binary
| :is_atom
| :starts_with
| :contains
| :member
@type string_predicate() :: %ExDatalog.Constraint{ left: ExDatalog.Term.t(), op: :starts_with | :contains, result: nil, right: ExDatalog.Term.t() }
@type t() :: comparison() | arithmetic() | type_predicate() | string_predicate() | membership()
@type type_predicate() :: %ExDatalog.Constraint{ left: ExDatalog.Term.t(), op: :is_integer | :is_binary | :is_atom, result: nil, right: nil }
Callbacks
@callback evaluate( constraint :: ExDatalog.IR.Constraint.t(), bindings :: ExDatalog.Engine.Binding.t(), context :: ExDatalog.Constraint.Context.t() ) :: {:ok, ExDatalog.Engine.Binding.t()} | :filter
Evaluates a constraint against a binding environment.
Accepts either a %Constraint{} (public struct) or a
%IR.Constraint{} (compiled form). The IR clause is the hot path used
by the engine; the public-struct clause converts to IR first, then
delegates. External callers and tests may use either form.
Dispatches to the appropriate constraint module based on the constraint's
op field:
- Comparison ops dispatch to
ExDatalog.Constraints.Comparison. - Arithmetic ops dispatch to
ExDatalog.Constraints.Arithmetic. - Type predicate ops dispatch to
ExDatalog.Constraints.Type. - String predicate ops dispatch to
ExDatalog.Constraints.StringPredicate. - Membership ops dispatch to
ExDatalog.Constraints.Membership.
Adding a new constraint type requires adding the op to the relevant
category list and a dispatch clause in constraint_module/1 — this is
a closed dispatch, not a runtime registry.
Returns {:ok, extended_binding} if the constraint succeeds (for
arithmetic, the binding includes the result variable), or :filter
if the constraint fails or an unbound input variable is encountered.
The context parameter carries evaluation metadata (capabilities,
provenance). For v0.2.0, no constraint implementation reads from the
context, but it is reserved for future use.
Examples
iex> c1 = ExDatalog.Constraint.gt({:var, "X"}, {:var, "Y"})
iex> ExDatalog.Constraint.evaluate(c1, %{"X" => 10, "Y" => 3}, %ExDatalog.Constraint.Context{})
{:ok, %{"X" => 10, "Y" => 3}}
iex> c2 = ExDatalog.Constraint.add({:var, "X"}, {:var, "Y"}, {:var, "Z"})
iex> ExDatalog.Constraint.evaluate(c2, %{"X" => 3, "Y" => 7}, %ExDatalog.Constraint.Context{})
{:ok, %{"X" => 3, "Y" => 7, "Z" => 10}}
Functions
@spec add(ExDatalog.Term.t(), ExDatalog.Term.t(), ExDatalog.Term.t()) :: t()
Constructs an addition constraint: result = left + right.
Examples
iex> ExDatalog.Constraint.add({:var, "X"}, {:var, "Y"}, {:var, "Z"})
%ExDatalog.Constraint{op: :add, left: {:var, "X"}, right: {:var, "Y"}, result: {:var, "Z"}}
Returns true if the constraint is arithmetic (binds a result variable).
Examples
iex> ExDatalog.Constraint.arithmetic?(ExDatalog.Constraint.add({:var, "X"}, {:var, "Y"}, {:var, "Z"}))
true
iex> ExDatalog.Constraint.arithmetic?(ExDatalog.Constraint.lt({:var, "X"}, {:const, 5}))
false
Returns true if the constraint is a comparison (filters, does not bind).
Examples
iex> ExDatalog.Constraint.comparison?(ExDatalog.Constraint.gt({:var, "X"}, {:const, 0}))
true
iex> ExDatalog.Constraint.comparison?(ExDatalog.Constraint.add({:var, "X"}, {:var, "Y"}, {:var, "Z"}))
false
@spec contains(ExDatalog.Term.t(), ExDatalog.Term.t()) :: t()
Constructs a contains constraint: String.contains?(left, right).
Both operands must be bound and resolve to binaries (strings). Returns
:filter if either operand is unbound or not a binary.
Examples
iex> ExDatalog.Constraint.contains({:var, "X"}, {:const, "ell"})
%ExDatalog.Constraint{op: :contains, left: {:var, "X"}, right: {:const, "ell"}, result: nil}
@spec div(ExDatalog.Term.t(), ExDatalog.Term.t(), ExDatalog.Term.t()) :: t()
Constructs an integer division constraint: result = div(left, right).
Uses truncating integer division (Kernel.div/2). Division by zero
filters the binding (returns :div_by_zero).
Examples
iex> ExDatalog.Constraint.div({:var, "X"}, {:const, 2}, {:var, "Y"})
%ExDatalog.Constraint{op: :div, left: {:var, "X"}, right: {:const, 2}, result: {:var, "Y"}}
@spec eq(ExDatalog.Term.t(), ExDatalog.Term.t()) :: t()
Constructs an equality constraint: left == right.
Examples
iex> ExDatalog.Constraint.eq({:var, "X"}, {:const, :alice})
%ExDatalog.Constraint{op: :eq, left: {:var, "X"}, right: {:const, :alice}, result: nil}
@spec evaluate( t() | ExDatalog.IR.Constraint.t(), ExDatalog.Engine.Binding.t(), ExDatalog.Constraint.Context.t() ) :: {:ok, ExDatalog.Engine.Binding.t()} | :filter
@spec gt(ExDatalog.Term.t(), ExDatalog.Term.t()) :: t()
Constructs a greater-than constraint: left > right.
Examples
iex> ExDatalog.Constraint.gt({:var, "A"}, {:const, 5})
%ExDatalog.Constraint{op: :gt, left: {:var, "A"}, right: {:const, 5}, result: nil}
@spec gte(ExDatalog.Term.t(), ExDatalog.Term.t()) :: t()
Constructs a greater-than-or-equal constraint: left >= right.
Examples
iex> ExDatalog.Constraint.gte({:var, "A"}, {:const, 0})
%ExDatalog.Constraint{op: :gte, left: {:var, "A"}, right: {:const, 0}, result: nil}
@spec input_variables(t()) :: [ExDatalog.Term.var_name()]
Returns all input variable names referenced by the constraint.
These are the variables that must be bound before the constraint is evaluated.
Examples
iex> ExDatalog.Constraint.input_variables(ExDatalog.Constraint.gt({:var, "X"}, {:var, "Y"}))
["X", "Y"]
iex> ExDatalog.Constraint.input_variables(ExDatalog.Constraint.add({:var, "A"}, {:const, 1}, {:var, "B"}))
["A"]
@spec lt(ExDatalog.Term.t(), ExDatalog.Term.t()) :: t()
Constructs a less-than constraint: left < right.
Examples
iex> ExDatalog.Constraint.lt({:var, "A"}, {:const, 10})
%ExDatalog.Constraint{op: :lt, left: {:var, "A"}, right: {:const, 10}, result: nil}
@spec lte(ExDatalog.Term.t(), ExDatalog.Term.t()) :: t()
Constructs a less-than-or-equal constraint: left <= right.
Examples
iex> ExDatalog.Constraint.lte({:var, "A"}, {:const, 100})
%ExDatalog.Constraint{op: :lte, left: {:var, "A"}, right: {:const, 100}, result: nil}
@spec member(ExDatalog.Term.t(), ExDatalog.Term.t()) :: t()
Constructs a membership constraint: left in right.
The left operand must be bound. The right operand must be a constant
list ({:const, list}). Returns :filter if the left value is not
a member of the right list, or if the left variable is unbound.
Examples
iex> ExDatalog.Constraint.member({:var, "X"}, {:const, [:a, :b, :c]})
%ExDatalog.Constraint{op: :member, left: {:var, "X"}, right: {:const, [:a, :b, :c]}, result: nil}
Returns true if the constraint is a membership test.
Examples
iex> ExDatalog.Constraint.membership?(ExDatalog.Constraint.member({:var, "X"}, {:const, [:a, :b]}))
true
iex> ExDatalog.Constraint.membership?(ExDatalog.Constraint.gt({:var, "X"}, {:const, 0}))
false
@spec mul(ExDatalog.Term.t(), ExDatalog.Term.t(), ExDatalog.Term.t()) :: t()
Constructs a multiplication constraint: result = left * right.
Examples
iex> ExDatalog.Constraint.mul({:var, "X"}, {:const, 2}, {:var, "Y"})
%ExDatalog.Constraint{op: :mul, left: {:var, "X"}, right: {:const, 2}, result: {:var, "Y"}}
@spec neq(ExDatalog.Term.t(), ExDatalog.Term.t()) :: t()
Constructs an inequality constraint: left != right.
Examples
iex> ExDatalog.Constraint.neq({:var, "X"}, {:var, "Y"})
%ExDatalog.Constraint{op: :neq, left: {:var, "X"}, right: {:var, "Y"}, result: nil}
@spec result_variable(t()) :: ExDatalog.Term.var_name() | nil
Returns the result variable name for an arithmetic constraint, or nil for comparisons.
Examples
iex> ExDatalog.Constraint.result_variable(ExDatalog.Constraint.add({:var, "X"}, {:var, "Y"}, {:var, "Z"}))
"Z"
iex> ExDatalog.Constraint.result_variable(ExDatalog.Constraint.gt({:var, "X"}, {:const, 0}))
nil
@spec starts_with(ExDatalog.Term.t(), ExDatalog.Term.t()) :: t()
Constructs a starts-with constraint: String.starts_with?(left, right).
Both operands must be bound and resolve to binaries (strings). Returns
:filter if either operand is unbound or not a binary.
Examples
iex> ExDatalog.Constraint.starts_with({:var, "X"}, {:const, "hello"})
%ExDatalog.Constraint{op: :starts_with, left: {:var, "X"}, right: {:const, "hello"}, result: nil}
Returns true if the constraint is a string predicate (binary, filters).
Examples
iex> ExDatalog.Constraint.string_predicate?(ExDatalog.Constraint.starts_with({:var, "X"}, {:const, "foo"}))
true
iex> ExDatalog.Constraint.string_predicate?(ExDatalog.Constraint.gt({:var, "X"}, {:const, 0}))
false
@spec sub(ExDatalog.Term.t(), ExDatalog.Term.t(), ExDatalog.Term.t()) :: t()
Constructs a subtraction constraint: result = left - right.
Examples
iex> ExDatalog.Constraint.sub({:var, "X"}, {:const, 1}, {:var, "Y"})
%ExDatalog.Constraint{op: :sub, left: {:var, "X"}, right: {:const, 1}, result: {:var, "Y"}}
@spec type_atom(ExDatalog.Term.t()) :: t()
Constructs an atom type-check constraint.
Filters bindings where the operand is not an atom. The operand must be bound before evaluation.
Examples
iex> ExDatalog.Constraint.type_atom({:var, "X"})
%ExDatalog.Constraint{op: :is_atom, left: {:var, "X"}, right: nil, result: nil}
@spec type_binary(ExDatalog.Term.t()) :: t()
Constructs a binary (string) type-check constraint.
Filters bindings where the operand is not a binary (string). The operand must be bound before evaluation.
Examples
iex> ExDatalog.Constraint.type_binary({:var, "X"})
%ExDatalog.Constraint{op: :is_binary, left: {:var, "X"}, right: nil, result: nil}
@spec type_integer(ExDatalog.Term.t()) :: t()
Constructs an integer type-check constraint.
Filters bindings where the operand is not an integer. The operand must be bound before evaluation.
Examples
iex> ExDatalog.Constraint.type_integer({:var, "X"})
%ExDatalog.Constraint{op: :is_integer, left: {:var, "X"}, right: nil, result: nil}
Returns true if the constraint is a type predicate (unary, filters).
Examples
iex> ExDatalog.Constraint.type_predicate?(ExDatalog.Constraint.type_integer({:var, "X"}))
true
iex> ExDatalog.Constraint.type_predicate?(ExDatalog.Constraint.gt({:var, "X"}, {:const, 0}))
false
Returns true if the constraint is structurally valid.
Examples
iex> ExDatalog.Constraint.valid?(ExDatalog.Constraint.gt({:var, "X"}, {:const, 0}))
true
iex> ExDatalog.Constraint.valid?(%ExDatalog.Constraint{op: :bad, left: {:var, "X"}, right: {:const, 0}, result: nil})
false