Aletheia
New to Prolog or logic programming? This README assumes some familiarity with the vocabulary (facts, rules, unification, backtracking, cut...) since it's written for evaluating Aletheia as a dependency. guides/language/TUTORIAL.md explains every one of those terms from scratch, with plain-English walkthroughs and no assumed background at all — start there instead if any of the paragraph below is unfamiliar.
Aletheia is an embedded Prolog dialect for Elixir — Clojure is to
Common Lisp as Aletheia is to Prolog. Classic Prolog surface syntax
(:-, ,/;/->, =, double-quoted strings, DCG's -->, a
runtime-mutable op/3 operator table — existing Prolog knowledge
transfers directly, no bespoke syntax to learn) on top of the BEAM, via
a reader built on Ichor's parsing
substrate and Episteme, the
standalone resolution engine and clause database (unification,
SLD-resolution, backtracking, a genuine clause-scoped cut) this package
is a syntax front-end for.
In plain English: this program stores four facts about who's a
parent of whom, then a rule — grandparent(X, Z) holds whenever
some Y exists with X a parent of YandY a parent of Z.
Asking grandparent(tom, Who) doesn't specify Who at all; Aletheia
backtracks through every value that makes the rule true and hands
back all of them, not just the first.
{:ok, db} = Aletheia.consult_string("""
parent(tom, bob).
parent(tom, liz).
parent(bob, ann).
parent(bob, pat).
grandparent(X, Z) :- parent(X, Y), parent(Y, Z).
""")
Aletheia.query("grandparent(tom, Who)", db)
#=> {:ok, [%{"Who" => :ann}, %{"Who" => :pat}]}
See guides/language/TUTORIAL.md for a full walkthrough of the language, or guides/language/ALETHEIA_CHEATSHEET.md if you already know Prolog and just want the syntax at a glance. For embedding Aletheia into an Elixir application specifically, see guides/TUTORIAL.md instead.
Why embed a Prolog dialect in Elixir?
Some problems are naturally relational and searchy — "given these
facts and rules, find every X that satisfies these constraints,
backtracking through alternatives automatically" — a rules engine, an
expert-system-style diagnosis/eligibility check, a permissions/access-
control policy expressed as facts instead of nested conditionals, a
small parser or configuration DSL (Aletheia ships real
DCG support for exactly that), or a
relational query over in-memory facts that would otherwise be a tangle
of nested Enum.filter/Enum.flat_map calls hand-rolling what
unification and backtracking already do for free. Prolog's own
strength — a real, load-bearing distinction between facts (what's
true), rules (what follows from what), and goals (what you're
asking), searched exhaustively via backtracking rather than evaluated
top-to-bottom like ordinary code — is exactly the shape those problems
already have; forcing them into imperative control flow is usually the
harder path, not the easier one.
Aletheia specifically (rather than reaching for
Episteme directly) is for when you
want that as real, classic Prolog syntax you can write, read, and
consult from .alp source files — existing Prolog books/tutorials/
muscle memory transfer directly, and op/3 lets you extend the grammar
itself at read time, the same way real Prolog does.
Components
Aletheia source (.alp)
│
▼
Aletheia.Reader ──── lib/aletheia/reader/grammar.aether (an Aether
│ grammar), generated ahead of time via
│ `mix ichor.gen` into grammar_generated.ex,
│ with Ichor.Toolkit.Pratt wired in via @native
│ for op/3-driven operator-precedence parsing
▼
Episteme.Term IR ──── Var/Compound structs + native lists/strings,
│ implementing Ichor.Backtrack.Term
▼
Episteme.Database ──── clause storage, populated by consult
│
▼
Episteme.Engine ──── SLD-resolution over Ichor.Backtrack.Tree/
│ Bindings, plus Episteme's own cut-barrier
│ mechanism and builtin predicate dispatch
▼
solutions
Everything below Aletheia.Reader in that diagram — the term IR,
clause database, resolution engine, cut, and builtin predicates —
lives in Episteme, a separate
package with no dependency on Aletheia's reader or concrete syntax at
all. This package (Aletheia) is the syntax front-end: the reader plus
a REPL.
- Reader (
Aletheia.Reader,Aletheia.Reader.Actions,Aletheia.Reader.Pratt) — parses source into clause forms ({:fact, _}/{:rule, _, _}/{:dcg, _, _}/{:directive, _}), one top-level clause at a time, threading the operator tableop/3directives mutate from each into the next.Aletheia.Reader.Prattis the@native(...)callback doing the actual precedence-climbing;Aletheia.Reader.ControlSyntaxbridges the reader's own ISO-punctuation term shapes (,/2,;/2,=/2, ...) to the plain-English namesEpisteme.Enginedispatches on. - Term IR (
Episteme.Term) — atoms/numbers/strings are plain Elixir atoms/integers-floats/binaries, lists are native Elixir lists (decomposing as ISO's own./2 cons functor so unification can recurse into them), variables and compounds get dedicated structs. - Database (
Episteme.Database) — clause storage, indexed by{name, arity}, pluggable backend (in-memory ETS by default, DETS for on-disk persistence). - Engine (
Episteme.Engine) — the resolution engine: clause selection, subgoal sequencing, control constructs (and/or/if_then/cut/not/call/N/once/ignore/phrase) overIchor.Backtrack, plus a real clause-scoped cut (see the reference) andEpisteme.Dcg(DCG rule/body translation). - Builtins (
Episteme.Builtins.*) — arithmetic, exceptions, the full list-predicate family (including higher-orderinclude/exclude/foldl/maplist), aggregation (findall/bagof/setof), the dynamic database (assert/retract/dynamic/abolish/clause), the atom/number/string conversion family, and I/O (write/format/...). - REPL (
Aletheia.Repl) — a consult + interactive query loop, genuinely lazy (pulls one solution at a time viaquery_lazy/2+next_solution/2, never forcing more of a search than you actually ask to see via;).
Aletheia depends on ichor_runtime
directly (Ichor.Toolkit.Pratt, used by the reader itself), on
ichor only at dev time (mix ichor.gen, ahead-of-time grammar codegen — see
lib/aletheia/reader/grammar_generated.ex,
never ships in a release build), and on
episteme for everything past
parsing (which in turn depends on ichor_runtime for
Ichor.Backtrack/Ichor.Toolkit.TermWalk).
Installation
Not yet published to Hex. For now, add it as a path or git dependency alongside a checkout of this repository:
def deps do
[
{:aletheia, path: "../aletheia"}
]
end
Aletheia's own mix.exs depends on
Episteme as a regular Hex
dependency ({:episteme, "~> 0.2"}) — no sibling checkout needed.
Documentation
Generated API docs — module and function reference, rebuilt from
mainon every push.guides/TUTORIAL.md — embedding Aletheia into an Elixir application, step by step: adding the dependency, consulting a program, querying it (eagerly, once, or lazily), errors at the host boundary, the REPL, and a worked example. Start here if you're new to Aletheia and already know Prolog.
guides/EXAMPLES.md — concrete, complete embedding examples (incremental consulting, multi-tenant database isolation, error handling, paginating a large search).
CASE_STUDY.md — a single larger, real-world worked example: a structured-log auditor combining a DCG grammar, the dynamic database, and grouped aggregation, written as one real
.alpprogram, with the intent, solution, and result spelled out end to end.guides/CHEATSHEET.md — quick reference for that same embedding API.
CHANGELOG.md — release history.
LICENSE — MIT.
Language tutorial and Language reference — everything about the Aletheia language itself (the Prolog dialect), independent of how it's embedded. Start here if you're new to Prolog.
Development
mix deps.get
mix precommit
mix precommit runs the whole gate in one shot — format, compile --warnings-as-errors, credo --strict, sobelow --skip, test,
dialyzer — the same checks CI runs, in fast-to-slow order. After
editing lib/aletheia/reader/grammar.aether, regenerate the checked-in
parser with MIX_ENV=dev mix gen.grammar before running the suite
again.
See CHANGELOG.md for release history.
License
MIT — see LICENSE.