ExDatalog.Constraint behaviour (ExDatalog v0.2.0)

Copy Markdown View Source

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 left and right must be bound before the constraint is evaluated. The result field is nil.

  • Arithmetic constraints — bind a new variable. The result field names the variable that receives the computed value. left and right must be bound; after evaluation result is added to the binding environment.

  • Type predicate constraints — unary filters that check the Elixir type of a bound value. The right and result fields are nil.

  • String predicate constraints — binary filters for string operations. Both operands must be bound and resolve to binaries. The result field is nil.

  • Membership constraints — test whether a value is in a constant list. left must be bound; right is a constant list. The result field is nil.

Extensible evaluation

The ExDatalog.Constraint behaviour defines a evaluate/3 callback that constraint modules implement. Evaluation dispatches based on the constraint's op field:

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

ConstructorMeaning
gt/2left > right
lt/2left < right
gte/2left >= right
lte/2left <= right
eq/2left == right
neq/2left != right

Arithmetic operators

ConstructorMeaning
add/3result = left + right
sub/3result = left - right
mul/3result = left * right
div/3result = 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

ConstructorMeaning
type_integer/1checks if the bound value is an integer
type_binary/1checks if the bound value is a binary (string)
type_atom/1checks if the bound value is an atom

String predicates

ConstructorMeaning
starts_with/2String.starts_with?(left, right)
contains/2String.contains?(left, right)

Membership

ConstructorMeaning
member/2left 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

arithmetic()

@type arithmetic() :: %ExDatalog.Constraint{
  left: ExDatalog.Term.t(),
  op: :add | :sub | :mul | :div,
  result: {:var, String.t()},
  right: ExDatalog.Term.t()
}

comparison()

@type comparison() :: %ExDatalog.Constraint{
  left: ExDatalog.Term.t(),
  op: :gt | :lt | :gte | :lte | :eq | :neq,
  result: nil,
  right: ExDatalog.Term.t()
}

membership()

@type membership() :: %ExDatalog.Constraint{
  left: ExDatalog.Term.t(),
  op: :member,
  result: nil,
  right: ExDatalog.Term.t()
}

op()

@type op() ::
  :gt
  | :lt
  | :gte
  | :lte
  | :eq
  | :neq
  | :add
  | :sub
  | :mul
  | :div
  | :is_integer
  | :is_binary
  | :is_atom
  | :starts_with
  | :contains
  | :member

string_predicate()

@type string_predicate() :: %ExDatalog.Constraint{
  left: ExDatalog.Term.t(),
  op: :starts_with | :contains,
  result: nil,
  right: ExDatalog.Term.t()
}

t()

type_predicate()

@type type_predicate() :: %ExDatalog.Constraint{
  left: ExDatalog.Term.t(),
  op: :is_integer | :is_binary | :is_atom,
  result: nil,
  right: nil
}

Callbacks

evaluate(constraint, bindings, context)

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

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

add(left, right, result)

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"}}

arithmetic?(constraint)

@spec arithmetic?(t()) :: boolean()

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

comparison?(constraint)

@spec comparison?(t()) :: boolean()

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

contains(left, right)

@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}

div(left, right, result)

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"}}

eq(left, right)

@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}

evaluate(constraint, binding, context)

gt(left, right)

@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}

gte(left, right)

@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}

input_variables(constraint)

@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"]

lt(left, right)

@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}

lte(left, right)

@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}

member(left, right)

@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}

membership?(constraint)

@spec membership?(t()) :: boolean()

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

mul(left, right, result)

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"}}

neq(left, right)

@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}

result_variable(constraint)

@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

starts_with(left, right)

@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}

string_predicate?(constraint)

@spec string_predicate?(t()) :: boolean()

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

sub(left, right, result)

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"}}

type_atom(term)

@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}

type_binary(term)

@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}

type_integer(term)

@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}

type_predicate?(constraint)

@spec type_predicate?(t()) :: boolean()

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

valid?(arg1)

@spec valid?(t()) :: boolean()

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