All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]
[0.2.0] - 2026-08-05
Added
- CASE_STUDY.md: a single larger, real-world worked
example (a structured-log auditor -- parses logfmt-style lines with
a DCG grammar, stores them in the dynamic database, and flags
brute-force/credential-stuffing patterns via grouped
setof/3aggregation andformat/2reporting), with intent, solution, and result spelled out end to end, alongside the many small examples in EXAMPLES.md. - DCG (Definite Clause Grammar) support, closing out v0.5: a new
Episteme.Dcgmodule (translate_rule/2,translate_body/3) translates aHead --> Bodyrule into an ordinary clause, threading an incoming/outgoing difference-list pair through every nonterminal call. A practical subset of real DCG translation -- terminals,{Goal}(ISO's own{}/1 embedded-goal shape),cut,and/2,or/2,if_then/2,if_then_else/3,not/1, a bare nonterminal, and a variable body (phrase(Var, S0, S)) -- deliberately not all of it (nocall//Npushback).phrase/2,3(Episteme.Engine) runs a DCG body as an ordinary goal;dcg_translate/2is the readable-->/2alias for introspection; a new{:dcg, head, body}Episteme.Database.consult_forms/2form and-->/2-shapedassert/1(same as:-/2is already special-cased) both translate and store. See REFERENCE.md. format/1andformat/2, closing out v0.4 entirely. A practical subset of ISO/SWI's own directive set (~w/~p/~q-- all the same thing here,Term.to_text/1--~a,~d,~s,~i,~n,~~), deliberately not all of it (no column/radix directives, no numeric-prefixed~Nd).Argsis normally a proper list, one element consumed per directive left to right; a single non-listArgsis treated as[Args], matching SWI's own leniency. Lives inEpisteme.Builtins.Ioalongsidewrite/1and friends, reusingEpisteme.Builtins.Strings.require_atomic_text/2for the format string itself. See REFERENCE.md.- A real string type -- a genuinely new atomic term class, backed by
plain Elixir binaries with no wrapper struct (the same
"raw host-language value" pattern atoms/integers/floats already use),
not ISO's own code-list default.
"abc"now unifies with"abc"by exact byte equality and never with[97,98,99]orabc-- both structurally distinct from a string. Standard order of terms gained a fourth class between atom and compound (Var < Number < Atom < String < Compound), and a newstring/1type check joinsatom/1/atomic/1(the latter now also true for strings). Alongside it, the v0.4 atom/number/string conversion family, in a newEpisteme.Builtins.Strings:atom_codes/2,atom_chars/2,atom_length/2,atom_concat/3,sub_atom/5,char_code/2,number_codes/2,number_chars/2,upcase_atom/2,downcase_atom/2,atomic_list_concat/2,3(classic ISO/de-facto, working with atoms), and their SWI-style real- string counterpartsatom_string/2,string_to_atom/2,string_concat/3,string_chars/2,string_codes/2,string_length/2,number_string/2,split_string/4.atom_concat/3,string_concat/3, andsub_atom/5all enumerate every matching decomposition on backtracking (the same "generate every candidate, let unify filter it" shapeappend/3already uses over lists) rather than needing separate deterministic/nondeterministic code paths. See REFERENCE.md. bagof/3andsetof/3, completing the v0.3 aggregation family alongside the existingfindall/3. Unlikefindall/3, both fail outright (notList = []) when the goal has no solutions, and group solutions by the goal's own free variables (every variable it mentions that isn't inTemplateand isn't existentially quantified viaVar^Goal), backtracking one solution per distinct group.setof/3additionally sorts and dedups each group (and orders the groups themselves) by standard order of terms. Built on two newEpisteme.Termfunctions,term_variables/1(real Prolog'sterm_variables/2, not itself exposed as a goal yet) andsort_by_order/1/sort_unique/1(the latter two also now shared bymsort/2/sort/2, replacing what used to be private duplicate logic inEpisteme.Builtins.Lists). See REFERENCE.md.- Published API docs to GitHub Pages at
joetjen.github.io/episteme,
rebuilt on every push to
mainvia.github/workflows/docs.yml. Referenced frommix.exs(docs: [homepage_url: ...],package: [links: %{"Docs" => ...}]) and linked from the README. - Rounds out the dynamic-database family:
abolish/1(unlikeretractall/1, undefines the predicate too -- a later call raisesexistence_erroragain) anddynamic/1(declares a predicate defined with zero clauses, so a call fails instead of raisingexistence_error, andassert/retractwork on it immediately; accepts a singleName/Arityor a list of them). Both build on two newEpisteme.Databasefunctions,abolish/3anddeclare_dynamic/3, and a newEpisteme.Database.Backendcallback (abolish/3, implemented in bothBackends.EtsandBackends.Dets). Alsoclause/2: enumerates every stored clause forHead's own{name, arity}whose head unifies with it, one per backtrack. See REFERENCE.md. - The rest of the v0.2 list-predicate family:
msort/2,sort/2,permutation/2,sum_list/2,max_list/2,min_list/2, andlist_to_set/2(pure operations, inEpisteme.Builtins.Lists, all built onTerm.compare_order/2) alongsideinclude/3,exclude/3,foldl/4(plus/5//6for 2–3 lists in lockstep), andmaplist/2..N(higher-order -- these call aGoalper element via the same mechanism ascall/N, so they live directly inEpisteme.Engine, next tocall/Nitself, not in a Builtins module). See REFERENCE.md. - Standard order of terms:
order_less/2,order_greater/2,order_less_or_equal/2,order_greater_or_equal/2(real Prolog@<,@>,@=<,@>=), andcompare/3(already a plain word in ISO, so it keeps its name). A total order over every term (Var < Number < Atom < Compound), not just numbers — see REFERENCE.md for the full within-class ordering rules (float-before-int on equal value, alphabetical atoms, arity-then-name-then-args for compounds).Episteme.Term.compare_order/2is the underlying whole-term operation, alongsidestructurally_equal?/2andresolve_deep/2. - Term construction and inspection:
functor/3,arg/3,univ/2(real Prolog=..), andground/1(alongside the existing type checks).functor/3anduniv/2both work in either direction — decomposing an already-bound term, or building a fresh one from a name/arity or[Name | Args]list. See REFERENCE.md. - Closes out the v0.2 predicate catalog:
ignore/1(likeonce/1, but never fails — succeeds with bindings unchanged if the goal has no solution; equivalent to(call(Goal) -> true ; true)) andunify_with_occurs_check/2(likeunify/2, but rejects a variable binding to a compound term that already contains it, instead of silently building an infinite term — built onIchor.Backtrack.Bindings.unify_occurs_check/4, already present inichor_runtimebut not previously wired up as a callable goal). See REFERENCE.md and REFERENCE.md. Alsoexistence_error/2, exposed as a directly-callable goal alongsidetype_error/2/domain_error/2/instantiation_error/1-- previously only ever thrown internally (on an undefined-predicate call) -- and the last five evaluable functors:sin/1,cos/1,tan/1,exp/1,log/1(natural log), alongside the existingsqrt/1inEpisteme.Builtins.Arithmetic. This closes out the full v0.2 predicate catalog.
[0.1.0] - 2026-08-04
Added
- Initial extraction from Aletheia: term representation, clause database, SLD-resolution engine (with a genuine clause-scoped cut), and the arithmetic/exceptions/lists/io builtin predicates, split out into their own package so they're usable without Aletheia's reader/syntax layer.
- A dynamic database:
assert/1,asserta/1,assertz/1,retract/1,retractall/1. Effects persist across separateEpisteme.query/2calls against the sameDatabase.t(), not just within one. - Solution aggregation:
findall/3,forall/2. between/3inEpisteme.Builtins.Arithmetic.- Pluggable clause storage via the new
Episteme.Database.Backendbehaviour:Episteme.Database.Backends.Ets(the default, in-memory, indexed by{name, arity}) andEpisteme.Database.Backends.Dets(the same shape, persisted to disk).Database.new/1takes a:backendoption;Database.close/1andDatabase.sync/1are new. - TUTORIAL.md (a from-scratch walkthrough), REFERENCE.md (every control construct, comparison, type check, arithmetic feature, and exception/database/list/I/O predicate, in full detail with a verified example each), EXAMPLES.md (complete, verified-runnable programs), CHEATSHEET.md (a one-page predicate/function reference), and CONTRIBUTING.md.
Changed
- Breaking: control constructs and comparison operators are now
spelled as plain English words instead of ISO Prolog's punctuation
operators, since Episteme has no reader of its own for that
punctuation to be conventional syntax against:
,→and/2,;→or/2,!→cut,\+→not/1(Cond -> Then ; Else)→if_then_else/3(new, explicit 3-arity goal, replacing the old ";wrapping a->" nested-pattern special case);(Cond -> Then)→if_then/2=→unify/2,\=→not_unify/2,==→equal/2,\==→not_equal/2=:=→numeric_equal/2,=\=→numeric_not_equal/2,=<→less_or_equal/2,>=→greater_or_equal/2+,-,*,/,<,>are unchanged — ordinary math notation, not Prolog-specific punctuation, and arithmetic expression functors (+,-,*,/,//,mod,rem,**,^,abs,sign,min,max,sqrt) are entirely unaffected, since those were never goals to begin with. No aliases: the old symbol-based names are gone, not deprecated.
- Breaking:
Database.t()now wraps a mutable backend resource (an ETS table by default) rather than being a plain immutable struct —assert/retractneed a database whose mutations are visible to every holder of the same value.add_clause/2,add_fact/2,clauses_for/3,defined?/3,indicator/1, andconsult_forms/2keep their existing signatures and behavior;Database.new/0still works exactly as before (now sugar forDatabase.new([])). - The
precommitalias now runssobelow --skip, honoring the# sobelow_skip [...]justification comment onEpisteme.query_once/2(a documented low-confidence false positive) instead of only silencing it by happenstance ofsobelow's own default exit code. ichor_runtimerequirement bumped from~> 0.1.0to~> 0.2, resolving to0.2.0. That release's breaking change (raw_capturesbecoming an ordered list of pairs instead of a map) is confined toIchor.Actions/Ichor.Capture/the grammar runtime, none of which Episteme touches — it only usesIchor.Backtrack.*andIchor.Toolkit.TermWalk, both unaffected, so this is a no-op for Episteme's own behavior.
Fixed
Episteme.query/2(andquery_once/2/query_lazy/2) no longer crashes with a rawFunctionClauseErrorwhen a partial list (one ending in a variable or other non-list, non-[]tail, e.g.[1 | X]) appears anywhere in the top-level goal — the internal walk that finds which variables to report in the answer usedEnum.reduce/3, which only works on proper (nil-terminated) lists.length/2with an unbound list and a bound count built one fresh variable and duplicated the same one that many times instead of generating independent fresh variables, so e.g.and(length(L, 3), unify(L, [a, b, c]))wrongly failed (the shared variable couldn't unify with two different values at once).- The package
descriptioninmix.exsexceeded Hex's 300-character limit, somix hex.build(and thusmix hex.publish, and the CI job that runshex.buildto verify the package assembles) failed outright. Trimmed to fit while keeping the same content.