Current section
Files
Jump to
Current section
Files
lib/ex_datalog.ex
defmodule ExDatalog do
@moduledoc """
ExDatalog — a pure Elixir Datalog engine.
## Overview
ExDatalog implements a bottom-up Datalog evaluation engine with
semi-naive fixpoint computation. Programs are built using a
builder API, validated, compiled to an intermediate representation,
and evaluated by a pluggable engine backend.
## Quick Start
alias ExDatalog
alias ExDatalog.{Program, Rule, Atom, Term}
result =
Program.new()
|> Program.add_relation("parent", [:atom, :atom])
|> Program.add_relation("ancestor", [:atom, :atom])
|> Program.add_fact("parent", [:alice, :bob])
|> Program.add_fact("parent", [:bob, :carol])
|> Program.add_rule(
Rule.new(
Atom.new("ancestor", [Term.var("X"), Term.var("Y")]),
[{:positive, Atom.new("parent", [Term.var("X"), Term.var("Y")])}]
)
)
|> Program.add_rule(
Rule.new(
Atom.new("ancestor", [Term.var("X"), Term.var("Z")]),
[
{:positive, Atom.new("parent", [Term.var("X"), Term.var("Y")])},
{:positive, Atom.new("ancestor", [Term.var("Y"), Term.var("Z")])}
]
)
)
|> ExDatalog.query()
## Pipeline
The evaluation pipeline is:
1. `ExDatalog.Program` — builder API
2. `ExDatalog.Validator` — structural + semantic validation
3. `ExDatalog.Compiler` — AST to IR
4. `ExDatalog.Engine` — pluggable evaluation backend
5. `ExDatalog.Result` — structured result with relation access
Each step can be invoked individually:
{:ok, validated} = ExDatalog.validate(program)
{:ok, ir} = ExDatalog.compile(program)
{:ok, result} = ExDatalog.evaluate(ir, [])
## Options
`query/2` and `evaluate/2` accept:
- `engine` — backend module (default: `ExDatalog.Engine.Naive`, available in Phase 4)
- `storage` — storage module (default: `ExDatalog.Storage.Map`, available in Phase 4)
- `max_iterations` — fixpoint iteration limit (default: 10_000)
- `timeout_ms` — wall-clock timeout in milliseconds (default: 30_000)
- `goal` — `{relation_name, pattern}` to filter results (default: `nil`)
- `explain` — enable provenance tracking (default: `false`, available in Phase 6)
"""
alias ExDatalog.{Program, Validator}
@doc """
Creates a new, empty Datalog program.
Delegates to `ExDatalog.Program.new/0`.
## Examples
iex> ExDatalog.new()
%ExDatalog.Program{relations: %{}, facts: [], rules: []}
iex> alias ExDatalog.Program
iex> ExDatalog.new() |> Program.add_relation("edge", [:atom, :atom])
%ExDatalog.Program{relations: %{"edge" => %{arity: 2, types: [:atom, :atom]}}, facts: [], rules: []}
"""
@spec new() :: Program.t()
defdelegate new(), to: Program
@doc """
Validates a program, returning structural and semantic errors.
Returns `{:ok, program}` if valid, `{:error, errors}` otherwise.
`errors` is a list of `ExDatalog.Validator.Error.t()`.
Structural checks (Phase 1):
- Relation references exist.
- Arities match declared schemas.
- Terms are valid.
Semantic checks (Phase 2):
- Variable safety and range restriction.
- Constraint binding and ordering.
- Stratified negation.
## Examples
iex> alias ExDatalog.Program
iex> program = Program.new() |> Program.add_relation("edge", [:atom, :atom])
iex> {:ok, validated} = ExDatalog.validate(program)
iex> is_struct(validated, ExDatalog.Program)
true
"""
@spec validate(Program.t()) :: {:ok, Program.t()} | {:error, [Validator.Error.t()]}
def validate(%Program{} = program) do
Validator.validate(program)
end
@doc """
Compiles a validated program to an engine-neutral IR.
Runs validation first. Returns `{:ok, %ExDatalog.IR{}}` or
`{:error, errors}`.
The IR is deterministic: the same program always produces the same IR.
Rules are sorted by `(stratum, relation_name, rule_id)`. Facts are sorted
by `(relation_name, values)`. Relations are sorted by name.
## Examples
iex> alias ExDatalog.{Program, Rule, Atom, Term}
iex> program =
...> Program.new()
...> |> Program.add_relation("edge", [:atom, :atom])
...> |> Program.add_relation("path", [:atom, :atom])
...> |> Program.add_rule(
...> Rule.new(
...> Atom.new("path", [Term.var("X"), Term.var("Y")]),
...> [{:positive, Atom.new("edge", [Term.var("X"), Term.var("Y")])}]
...> )
...> )
iex> {:ok, ir} = ExDatalog.compile(program)
iex> length(ir.rules) == 1 and length(ir.relations) == 2
true
true
"""
@spec compile(Program.t()) :: {:ok, ExDatalog.IR.t()} | {:error, [Validator.Error.t()]}
def compile(%Program{} = program) do
ExDatalog.Compiler.compile(program)
end
@doc """
Evaluates a compiled IR program against a backend engine.
Returns `{:ok, ExDatalog.Result.t()}` or `{:error, reason}`.
## Options
- `:engine` — backend module (default: `ExDatalog.Engine.Naive`)
- `:storage` — storage module (default: `ExDatalog.Storage.Map`)
- `:max_iterations` — fixpoint iteration limit (default: 10_000)
- `:timeout_ms` — wall-clock timeout in ms (default: 30_000)
"""
@spec evaluate(ExDatalog.IR.t(), keyword()) :: {:ok, ExDatalog.Result.t()} | {:error, term()}
def evaluate(%ExDatalog.IR{} = ir, opts \\ []) do
engine = Keyword.get(opts, :engine, ExDatalog.Engine.Naive)
engine.evaluate(ir, opts)
end
@doc """
One-shot: validate, compile, and evaluate a program.
Equivalent to `validate/1` → `compile/1` → `evaluate/2`.
Returns `{:ok, ExDatalog.Result.t()}` or `{:error, reason}`.
## Options
See `evaluate/2` for available options.
## Examples
iex> alias ExDatalog.{Program, Rule, Atom, Term}
iex> program =
...> Program.new()
...> |> Program.add_relation("parent", [:atom, :atom])
...> |> Program.add_fact("parent", [:alice, :bob])
iex> {:ok, result} = ExDatalog.query(program)
iex> ExDatalog.Result.size(result, "parent")
1
"""
@spec query(Program.t(), keyword()) ::
{:ok, ExDatalog.Result.t()} | {:error, [Validator.Error.t()] | term()}
def query(%Program{} = program, opts \\ []) do
with {:ok, validated} <- validate(program),
{:ok, ir} <- ExDatalog.Compiler.compile(validated) do
evaluate(ir, opts)
end
end
end