cl-prolog
cl-prolog is a small, dependency-free Prolog engine for Common Lisp, built
around three ideas:
- macro-first rule definition — clauses are data, macros own the syntax
- CPS proof search — the engine emits solutions through continuations; callers choose streaming or collection
- data / logic separation — rulebases are plain structs the engine walks
The public package is cl-prolog.
Quick start
(require :asdf)
(asdf:load-asd (truename "cl-prolog.asd")) ; run from the repository root
(asdf:load-system :cl-prolog)
(in-package #:cl-prolog)
(define-rulebase *family*
((parent tom bob))
((parent bob alice))
((ancestor ?x ?y) (parent ?x ?y))
((ancestor ?x ?y) (parent ?x ?z) (ancestor ?z ?y)))
(query-prolog *family* '(ancestor tom ?who))
;; => (((?WHO . BOB)) ((?WHO . ALICE)))
Facts are one-element clauses; rules are a head followed by body goals.
Logic variables are ?-prefixed symbols.
Where to go next
- Querying — the query entry points and their contracts
- Builtin goals — unification, control, collection, database, arithmetic, and list goals
- Rule DSL — rulebase construction,
:whenguards, foreign predicates - DCG — grammar rules and combinators
- Semantics — clause order, cut, depth bounds, occurs check
- API reference — the complete symbol index
- Architecture — module layout and the CPS engine
- Development — testing, examples, and design constraints
- Troubleshooting — common surprises and fixes
- Release checklist — the evidence bar for shipping
- Repository documentation — changelog, contributing, policy
Install
cl-prolog is not currently distributed by Quicklisp. Clone the repository and either load its ASDF definition directly or place the checkout in a directory configured in your ASDF source registry.
git clone https://github.com/takeokunn/cl-prolog.git
cd cl-prolog
sbcl --non-interactive \
--eval '(require :asdf)' \
--eval '(asdf:load-asd (truename "cl-prolog.asd"))' \
--eval '(asdf:load-system :cl-prolog)'
To run the cl-weave regression suite through the Linux-only Nix app:
nix run github:takeokunn/cl-prolog
Querying
(query-prolog rb '(ancestor tom ?who)) ; all solutions
(query-prolog rb '(ancestor tom ?who) :limit 2) ; bounded search
(query-prolog-first rb '(ancestor ?x bob)) ; first solution or NIL
(prolog-succeeds-p rb '(ancestor tom eve)) ; boolean, stops at first proof
;; streaming: the function is called as each solution is proven
(map-prolog-solutions
(lambda (solution) (format t "~&=> ~S~%" solution))
rb '(ancestor tom ?who))
with-prolog-query binds variables from the first solution; prolog-match
dispatches like cond over queries.
Entry points
map-prolog-solutions— the primitive. Calls a function once per solution as it is proven (streaming CPS). Keywords::max-depth,:environment,:project,:limit.query-prolog— collect solutions into a list. Same keywords.query-prolog-first— first solution ornil(searches with:limit 1).prolog-succeeds-p— boolean; stops at the first proof.solution-binding— look one variable up in a solution alist.
Conventions
- a solution is an alist of query-variable bindings; ground success is
nil, so “one ground proof” is(nil)and failure is() :project nilreturns raw proof environments instead:max-depthoptionally bounds user-rule resolution (the default isNIL, meaning unbounded); exhaustion signalsprolog-depth-limit-exceeded;0disables rule expansion entirely, facts still match:limitmust benilor a positive integer
Builtin Goals
The list below is a representative, task-oriented overview, not an exhaustive list. The authoritative list of builtin symbols exported to Lisp callers is the API Reference. Parsed Prolog source also uses implemented goal names that need not be exported as Common Lisp package symbols.
- Unification:
(= a b),(\= a b), and(unifiable a b ?unifier)unify two terms, require that they cannot unify, or return a unifier without binding either input. - Control:
!,(not g),(and g...), and(or g...)provide cut, negation as failure, conjunction, and disjunction. - Meta-call:
(call g),(once g),(ignore g),(not g),(forall c a), and(repeat)invoke goals, control their solutions, or generate repeated proofs. - Collection:
(findall t g ?bag),(bagof t g ?bag), and(setof t g ?set)collect templates.bagofgroups free variables, whilesetofalso removes duplicates and sorts. - Dynamic database:
(asserta c),(assertz c),(retract c),(abolish (p / n)), and(clause h b)inspect or mutate clauses in the rulebase passed to the query. - Arithmetic:
(is ?x expr),(=:= a b),(=\= a b),(< a b),(=< a b),(> a b), and(>= a b)evaluate arithmetic expressions and compare their numeric values. - Lists:
(member ?x list),(append ?a ?b ?c),(reverse ?a ?b), and(length ?l ?n)define relations over proper and partially instantiated lists. - Finite domains:
(#= a b),(#\= a b),(#< a b),(#=< a b),(#> a b),(#>= a b),(in x range), and(indomain x)constrain integers and enumerate finite-domain solutions. - Lisp guard:
(:when fn ?x...)calls a Lisp predicate with solved values.
Arithmetic expressions use prefix Lisp-shaped terms. Binary operators are +,
-, *, /, //, div, rem, mod, min, max, **, and ^. Unary
operators are +, -, abs, sign, truncate, round, ceiling, floor,
and sqrt.
Collection and dynamic-database goals can be used like any other query:
(query-prolog *family* '(findall ?child (parent tom ?child) ?children))
;; => (((?CHILDREN BOB)))
(query-prolog *family* '(assertz (parent tom eve)))
(query-prolog *family* '(parent tom ?child))
;; includes EVE
(query-prolog (make-rulebase) '(is ?total (+ 20 (* 2 11))))
;; => (((?TOTAL . 42)))
Relational list length
length/2 can check a proper list, bind its length, complete a partial list to
a requested non-negative length, or generate lists of increasing length when
both arguments are variables. A non-integer length raises type_error(integer, ...); a negative length raises domain_error(not_less_than_zero, ...).
Cyclic lists fail rather than making the solver loop.
Meta-call validation
Goals executed by call/1, once/1, ignore/1, negation, forall/2,
conditionals, catch/3, and cleanup predicates must be callable after applying
the current bindings. Invalid goals raise the corresponding ISO instantiation
or callable type error. Validation happens when that goal is actually reached,
so an unreachable branch is not inspected eagerly.
Finite-domain equality
#=/2 binds an unconstrained variable when the other operand is an integer;
the binding is visible to ordinary unification and later goals. Comparing a
variable with itself succeeds for #=, #=<, and #>=, and fails for #\=,
#<, and #>.
Term and stream I/O
read_term/3 supports variable_names/1 and singletons/1. The latter
returns Name=Variable entries for named variables that occur exactly once;
the anonymous variable _ is omitted.
For input streams, stream_property/2 reports end_of_stream(not),
end_of_stream(at), or end_of_stream(past). Peeking at EOF establishes
at; an attempted consuming read advances it to past. Repositioning a
stream resets the state to not.
Structurally unusable goals
A non-function :when guard, or a goal that is not a symbol or symbol-headed
list, signals invalid-goal-error (invalid-goal-error-goal returns the
offending goal). Calling a known name at an unsupported arity denotes a
different, undefined procedure and signals the ISO existence_error instead.
halt/0 and halt/1 raise the prolog-halt condition (readers:
prolog-halt-code); it is deliberately not a prolog-exception, so
catch/3 never intercepts it and the embedding application decides how to
exit.
Prolog numbers are integers and floating-point values. Common Lisp ratios and complex values are not accepted by numeric or atomic term predicates and do not receive numeric term ordering.
Extending the builtin set
(define-foreign-predicate (twice input output)
(rulebase environment depth emit)
(declare (ignore rulebase depth))
(let ((value (logic-substitute input environment)))
(when (numberp value)
(multiple-value-bind (extended ok) (unify output (* 2 value) environment)
(when ok (funcall emit extended))))))
define-foreign-predicateis the public extension API and registers one exact predicate name/arity pair- the predicate argument list has fixed arity; lambda-list keywords such as
&restare not supported rulebase,environment, anddepthexpose the current solver contextemitmust be called once per solution with its resulting environment; call it zero times to fail and do not collect result lists
The internal builtin-definition machinery is not exported. See Rule DSL for additional foreign-predicate examples.
Rule DSL
For explicit rulebase composition, define-rulebase creates a named rulebase
and extend-rulebase returns a new rulebase with additional clauses, leaving
the base object unchanged:
(define-rulebase *base*
((color apple red)))
(defparameter *extended*
(extend-rulebase *base*
((color lime green))))
(query-prolog *extended* '(color ?fruit ?color))
In the prolog / def-rule DSL you write guards as expressions —
(:when (> ?n 10)) — and the macro compiles them to closures. The engine
never evaluates user expressions at runtime.
Available forms
prolog— build a rulebase from clausesdefine-rulebase—defparameter+prologextend-rulebase— functional extension; the new clauses shadow the basedef-rule— define a reusable clause-producing formwith-prolog-query— bind variables from the first solutionprolog-match—condover queries
Clause shape: ((pred args...)) is a fact, (head goal...) is a rule.
Foreign predicates
Extend the goal set with define-foreign-predicate. Foreign predicates use
the engine’s CPS solution protocol and dispatch by exact name and arity:
(define-foreign-predicate (choose output) (rulebase environment depth emit)
(declare (ignore rulebase depth))
(dolist (value '(left right))
(multiple-value-bind (extended ok) (unify output value environment)
(when ok (funcall emit extended)))))
DCG
(defparameter *grammar*
(make-rulebase
:clauses
(list (def-dcg-rule noun (terminal :noun))
(def-dcg-rule verb (terminal :verb))
(def-dcg-rule sentence
(dcg-star noun)
(verb)
(brace (= 1 1)))))) ; Lisp guard, like (:when ...)
(phrase *grammar* 'sentence '(:noun :noun :verb))
;; => NIL, T (remainder, matched-p)
(phrase-all *grammar* 'sentence '(:noun :noun :verb))
;; => (NIL)
Combinators: dcg-alt, dcg-opt, dcg-star, dcg-plus,
dcg-error-recovery, plus token matchers dcg-token-match and
dcg-token-match-value.
Reference
def-dcg-rule— compile a grammar body into a rule with two stream arguments; body elements:(terminal KIND...),(brace EXPR), non-terminal calls, and combinator formsphrase—(phrase rulebase rule-name input)returns(values remainder matched-p)for the first parsephrase-all—(phrase-all rulebase rule-name input)returns the remainders of every parse- combinators (usable as goals):
dcg-alt,dcg-opt,dcg-star,dcg-plus,dcg-error-recovery - token matchers:
(dcg-token-match kind input rest),(dcg-token-match-value kind value input rest)
Tokens are bare kind symbols or (kind . value) conses.
dcg-error-recovery skips ahead to the next token whose kind is in
*dcg-sync-tokens* (internal parameter: :t-rparen, :t-semi, :t-eof).
dcg-star refuses to repeat a rule that consumed no input, so nullable
rules terminate.
Semantics
- Clause order: facts are always tried before rules; within each group, definition order is preserved.
- Cut prunes the running clause’s remaining choice points and the predicate’s remaining rule clauses.
- Optional depth bound: rule resolution is unbounded by default. Set
:max-depthto a non-negative integer to bound user-rule resolution; exhaustion signalsprolog-depth-limit-exceededrather than masquerading as logical failure. - Occurs check is always on; unification never introduces cyclic terms. Host-provided cyclic cons structures are nevertheless handled safely: unification compares them coinductively, substitution preserves cycles and sharing, and variable collection terminates.
Modules
Unqualified calls first resolve predicates in the current module and then its
imports. current_predicate/1 follows the same visible-predicate view, including
imported predicates. In a qualified call Module:Goal, Module is resolved
through the current bindings and must become an atom naming an existing module;
violations raise catchable ISO instantiation, type, or existence errors.
Proof-search semantics
- registered builtins and foreign predicates are authoritative for their predicate indicator; otherwise facts precede rules in definition order
- rule variables (and variables inside facts) are freshly renamed per use
- an explicit depth bound decrements per user-rule resolution, so left recursion terminates with the solutions found within the bound
See Architecture for how cut and guards are implemented in the CPS engine.
API Reference
cl-prolog exposes one public package: cl-prolog. This page lists every
symbol exported by that package. Anything not listed here is internal.
See Querying, Builtin goals, Rule DSL, and DCG for narrative explanations and examples.
Data
- Clause representation:
clause,clause-p,clause-head,clause-body,make-clause - Rulebase representation:
rulebase,rulebase-p,rulebase-visible-clauses,make-rulebase,copy-rulebase,rulebase-extend,rulebase-insert-clause!
A clause with an empty body is a fact. Rulebases are explicit values; the
library has no global rulebase. Prefer prolog and extend-rulebase for
declarative construction. rulebase-insert-clause! and the dynamic database
goals provide intentional mutation with logical-update semantics.
copy-rulebase is useful when dynamic updates must not affect a reusable
base. It copies stored terms and mutable registries while allowing immutable
metadata to be shared.
The exported symbol clause names the clause type/accessor API and is also
the Lisp representation of the exported clause/2 builtin goal.
Unification
logic-var-p— true for?-prefixed non-keyword symbolsfresh-logic-variable— return a fresh symbol satisfyinglogic-var-punify—(unify left right &optional environment)returns(values extended-environment t)on success and(values nil nil)on failure; the occurs check is always enabledlogic-substitute— apply an environment to a term while preserving dotted structure
Environments are persistent association lists. unify extends rather than
mutates an environment, so older environments remain valid choice points.
Proof Search and Depth
*max-prolog-depth*— default maximum user-rule depth;nilmeans unboundeddefine-foreign-predicate— register one exact predicate name/arity pair using the solver’s zero-to-manyemitcontinuation protocol
define-foreign-predicate is the supported extension surface. Its form is:
(define-foreign-predicate (name argument...)
(rulebase environment depth emit)
body...)
The argument list has fixed arity. Call emit once for each solution
environment and do not collect solutions in the predicate. The builtin
registry and its defining machinery are internal implementation details.
Queries
map-prolog-solutionsquery-prologquery-prolog-firstprolog-succeeds-psolution-binding
The query functions accept an explicit rulebase and query. The mapping and
list-returning APIs support :max-depth, :environment, :project, and
:limit; prolog-succeeds-p supports :max-depth. See
Querying for contracts and result shapes.
Parser, Writer, and Loader
- Readers and parser:
read-prolog-term,read-prolog-clause,parse-prolog - Writer:
write-prolog-term,prolog-term-string - Source loading:
consult-prolog,ensure-prolog-loaded - Loader goal symbols:
consult,ensure_loaded,load_files
read-prolog-term and read-prolog-clause accept a string or stream and an
optional operator table. parse-prolog parses Prolog source into clause data.
write-prolog-term writes to its optional stream, while
prolog-term-string returns a string.
consult-prolog validates a source before replacing its registered clauses.
ensure-prolog-loaded loads a pathname only when that source is not already
registered. The goal symbols correspond to consult/1, ensure_loaded/1,
and load_files/1 in parsed or Lisp-shaped queries.
Rule DSL
prologdefine-rulebaseextend-rulebasedef-rulewith-prolog-queryprolog-match
See Rule DSL for macro forms and examples.
Builtin Goal Symbols
These exported symbols form the public Lisp package surface for builtin goals. The same names can be written in parsed Prolog syntax with their supported arities. See Builtin goals for behavior-oriented guidance.
- Control and meta-call:
!,call,call_nth,call_with_depth_limit,once,setup_call_cleanup,call_cleanup,forall,if-then-else,soft-if-then-else,catch,throw,unify_with_occurs_check,repeat,true,fail,false,\+ - Collection and sorting:
findall,bagof,setof,sort,msort,keysort - Dynamic database and reflection:
asserta,assert,assertz,retract,retractall,current_predicate,predicate_property,abolish,clause - Unification and term construction:
\=,..,=.. - Arithmetic evaluation and comparison:
is,=:=,=\=,<,=<,>,>= - Finite domains:
in,ins,#=,#\=,#<,#=<,#>,#>=,all_different,labeling,indomain - Type and term tests:
var,nonvar,atom,atomic,number,integer,float,compound,callable,ground,acyclic_term,cyclic_term - Standard term order and comparison:
==,\==,@<,@=<,@>,@>=,compare,unifiable - Term inspection and copying:
term_variables,functor,arg,copy_term,numbervars
DCG
- Grammar definition and execution:
def-dcg-rule,phrase,phrase-all - Combinators:
dcg-alt,dcg-opt,dcg-star,dcg-plus,dcg-error-recovery - Token matchers:
dcg-token-match,dcg-token-match-value
DCG tokens are either a bare token-kind symbol or a (kind . value) cons.
dcg-token-match matches the kind and returns the remaining input;
dcg-token-match-value matches both kind and value. See DCG.
Conditions
- Depth configuration:
invalid-max-depth-errorand readerinvalid-max-depth-error-value - Depth exhaustion:
prolog-depth-limit-exceededand readerprolog-depth-limit-exceeded-goal - Structurally invalid goals:
invalid-goal-errorand readerinvalid-goal-error-goal - Prolog throws:
prolog-exceptionand readerprolog-exception-term - Runtime hierarchy:
prolog-runtime-error,prolog-instantiation-error,prolog-type-error,prolog-domain-error,prolog-permission-error,prolog-existence-error,prolog-evaluation-error,prolog-resource-error - Process-level halt:
prolog-haltand readerprolog-halt-code - Arithmetic diagnostics:
arithmetic-evaluation-errorand readersarithmetic-error-expression,arithmetic-error-reason
prolog-halt is not a prolog-exception, so catch/3 does not intercept it.
The embedding application decides how to translate the condition into process
termination.
Script Entry Points
nix run .— run the cl-weave-backed ASDF regression suite on Linuxasdf:load-system :cl-prolog/examples— load runnable examples
Architecture
Design Direction
cl-prolog is a relational programming library embedded in Common Lisp. Its
main design choices are:
- macro-first authoring: macros own syntax and produce runtime data
- continuation-passing proof search: solutions stream through continuations
- explicit rulebase and query state rather than a process-global database
- one ordered clause representation for facts and rules
- a narrow exported package surface with internal solver machinery
It implements a focused Prolog runtime rather than attempting to mirror every facility of a standalone ISO Prolog system.
ASDF Load Order
cl-prolog.asd is serial. The production system loads these components in
this exact dependency order:
package.lispoperator-table.lispmodule-system.lispdata.lispunification.lispparser.lispterm-writer.lispengine.lispio-context.lispprover.lispbuiltins/core.lispbuiltins/control.lispbuiltins/collection.lispbuiltins/dynamic.lispbuiltins/arithmetic.lispbuiltins/list.lispbuiltins/atom.lispbuiltins/operator.lispbuiltins/io.lispbuiltins/io-streams.lispbuiltins/io-code.lispfd-store.lispbuiltins/fd.lispbuiltin-term.lispdcg-runtime.lispquery.lispsource-loader.lispdsl-compiler.lispdsl.lispdcg.lisp
The important boundaries are:
data.lispowns clauses, the logical-update rulebase, indexes, and mutable registriesengine.lispowns conditions plus builtin and foreign-predicate registriesprover.lispowns normalization, proof state, dispatch, clause resolution, cut barriers, depth accounting, and tablingquery.lispturns the continuation protocol into the public mapping and result APIssource-loader.lisp,dsl*.lisp, anddcg*.lispare separate front ends that produce or consume the same clause and query representation
Unified Clause Store
Facts and rules use the same clause structure. A fact is a clause whose body
is empty. The rulebase stores clauses in one definition-ordered sequence and a
predicate index; it does not search a separate fact collection before a rule
collection.
Stored entries carry their module, source identity, and born/died revisions. A predicate call takes the visible entries for its logical-update snapshot and tries them in definition order. Each selected clause is freshened before unification, so variables do not leak between uses.
Proof-State Prover
The prover streams solutions in continuation-passing style. Its internal
proof-state carries:
- the explicit rulebase
- the current persistent binding environment
- remaining user-rule depth
- the active module
- the table session
- the current cut tag
State transitions construct updated proof states rather than replacing the
rulebase with hidden global state. Goal dispatch first recognizes registered
builtin or foreign solvers; otherwise it resolves the goal against the visible
user clauses. Foreign predicates are keyed by exact name and arity and use the
same zero-to-many emit continuation contract as internal solvers.
map-prolog-solutions exposes this streaming model. Convenience query APIs
fold or stop that stream instead of requiring the prover to accumulate all
answers.
Cut
Cut uses dynamically scoped catch/throw tags:
- each user-predicate invocation establishes a fresh cut barrier
!emits the current state and throws to that invocation’s tag- the throw abandons remaining alternatives for the invocation
- opaque meta-calls establish their own barrier
- transparent control constructs deliberately reuse the caller’s barrier
This keeps a rule-body cut local to the predicate invocation while preserving the intended transparency of control constructs.
Depth and tabling
Depth decreases when proof enters a user rule, not for every builtin or
unification step. nil means unbounded search. Declared tabled predicates and
detected left recursion can use a per-query table session; depth-limited or
active finite-domain searches bypass that tabling path where required.
Guards
(:when expression) guards are compiled by the DSL macros into
(:when function variable...) goals. At runtime the solver substitutes bound
values and calls the function. Relational rule data therefore does not require
runtime eval.
Explicit Dynamic Mutation
There is no process-global rulebase. prolog constructs one, extend-rulebase
derives another, and every query receives its rulebase explicitly.
The default authoring style is immutable, but mutation is a supported and visible operation:
asserta/1,assert/1, andassertz/1insert into the supplied rulebaseretract/1,retractall/1, andabolish/1retire matching entriesconsult-prologtransactionally replaces the clauses registered for a source after validationrulebase-insert-clause!exposes insertion to Lisp callers
Born/died revisions preserve logical-update behavior: a running predicate
invocation continues over its snapshot even when a dynamic goal changes the
database. Later invocations observe the newer revision. Callers that require
isolation can use copy-rulebase before mutation.
Macro-First Surface
(prolog
((parent tom bob))
((score bob 42))
((rich ?x) (score ?x ?n) (:when (> ?n 10))))
The DSL expands into clause construction, including precompiled guard closures. Parsed Prolog source, Lisp DSL forms, dynamic goals, and DCGs all converge on the same runtime terms and rulebase.
Verification Layers
nix run .— run the cl-weave-backed ASDF regression behavior on Linuxnix flake check— verify packaging and clean-source behavior
When architecture changes, update the narrowest affected verification layer first.
Development
Environment
The flake defines outputs for x86_64-linux and aarch64-linux only. On
Linux, enter the reproducible development environment with:
nix develop # sbcl, cl-weave, paredit-cli, nixpkgs-fmt, mdbook
On Darwin and other platforms, the flake does not expose a development shell, package, check, or app. Load the local checkout with ASDF instead and use CI as the authoritative Linux Nix verification path.
Examples
sbcl --non-interactive \
--eval '(require :asdf)' \
--eval '(asdf:load-asd (truename "cl-prolog.asd"))' \
--eval '(asdf:load-system :cl-prolog/examples)'
Run this from the repository root. Loading cl-prolog/examples loads the
library first and then executes all three example files. The example files are
not standalone scripts, so invoking them directly with sbcl --script does
not load the cl-prolog package.
Testing
The cl-prolog/tests ASDF system depends on
cl-weave and runs the complete
regression suite, including isolated table cases, per-query cases, fixtures,
and generated relational properties. Nix provides the self-contained runner:
nix run .
All flake outputs, including the packaged Nix app and checks, are supported on
Linux only. On Darwin, use the ASDF workflow for library development and rely
on CI for the Linux nix flake check verification path.
Pass any cl-weave CLI options after --; for example, to produce a JSON
result:
nix run . -- --reporter json --output cl-prolog-weave-results.json
The full Linux Nix verification suite is:
nix flake check
This also runs checks.paredit-lint, a structural parse gate over every
tracked .lisp/.asd file, and checks.documentation, which builds the
mdBook site and fails if it does not produce a valid index.html.
Query test helpers
Load the cl-prolog/weave ASDF system to use the public query test helpers:
(asdf:load-system :cl-prolog/weave)
deftest-queries creates an independent cl-weave
case and a fresh rulebase for every query. A leading case label is optional;
without one, the printed query is used.
(cl-prolog/weave:deftest-queries family-queries ((make-family-rulebase))
("keeps proof order" (parent alice ?child) :ordered
(((?child . bob)) ((?child . carol))))
((parent alice ?child) :set
(((?child . carol)) ((?child . bob))))
((parent alice ?child) :first ((?child . bob)))
((parent alice bob) :succeeds)
((parent bob alice) :fails)
((parent alice bob) :signals cl-prolog:invalid-max-depth-error
:max-depth :invalid))
:ordered, :set, and :first take an expected value. :set ignores only
the order of complete solutions; it still compares the structure within each
solution with equal. :signals optionally takes a condition type. Query
options follow the expected value or assertion kind.
Use assert-query inside an existing cl-weave case when a table is not needed:
(cl-weave:it "finds Alice's first child"
(cl-prolog/weave:assert-query (make-family-rulebase)
(parent alice ?child) :first ((?child . bob))))
Documentation
nix build .#docs # rendered site in ./result
mdbook serve docs # live-reloading preview from the dev shell
The Nix build is Linux-only. On other systems with mdBook installed, use
mdbook build docs or mdbook serve docs directly.
Design Constraints
- no runtime dependencies, SBCL-tested, ANSI-leaning core
- a single canonical public API surface
See Release checklist for the evidence bar a change must clear before shipping.
Troubleshooting
query-prolog returned (nil)
That means the query succeeded and projected no variables.
(query-prolog rulebase '(parent tom bob))
;; => (nil)
Success with variables returns binding alists. Failure returns nil.
query-prolog-first returned nil
That means no proof succeeded.
If you need the first successful binding only, this is the right entry point.
If you need every solution, use query-prolog.
solution-binding returned nil
The variable may be unbound in that solution, or the variable may not exist in the projected environment.
Inspect the whole solution first:
(first (query-prolog rulebase '(ancestor tom ?who)))
unify returned nil, nil
unify returns two values.
- first value: extended environment on success, or
nilon failure - second value: success flag
Failure is (values nil nil). Ground success is (values nil t), so callers
must inspect the second value rather than treating a nil environment as
failure.
:max-depth 0 rejected a derived rule
That is expected. Depth limits apply to user-rule expansion, and exhaustion
signals prolog-depth-limit-exceeded so an incomplete search is never
reported as logical failure.
0: facts and built-ins only1: one rule expansion- larger values: deeper derived proofs
A foreign predicate did not run
define-foreign-predicate dispatches by exact name and arity. Check both
parts of the predicate indicator. Builtins remain authoritative when names
overlap.
phrase returned nil
This has two meanings depending on the grammar:
- full token consumption on success
- no parse when the caller expected a non-
nilremainder
Use phrase-all when you need to inspect every successful remainder stream.
Nix checks fail only in a clean source tree
Check whether the file is tracked in git. The release checks intentionally validate the tracked tree, not only the working directory.
Script entry point cannot find the repository root
Confirm:
- you are running the script with
sbcl --script - you are invoking it from inside the repository checkout
- the script has not been copied away from the tree it expects to load
Direct smoke:
nix run .
The packaged Nix runner is Linux-only. On Darwin, skip this smoke step and use ASDF directly for local library work.
What To Include In A Bug Report
- exact query or macro form
- expected result
- actual result
- SBCL version
- output of
nix run .when reproducing on Linux - output of
nix flake check --print-build-logs
Release Checklist
This is the minimum evidence bar for calling a revision releasable.
Documentation Review
Confirm that these files still describe the current code:
README.mddocs/src/api-reference.mddocs/src/architecture.mddocs/src/troubleshooting.mdCHANGELOG.mdCONTRIBUTING.mdCODE_OF_CONDUCT.mdSECURITY.mdSUPPORT.md
If the public surface changed, update the docs in the same change.
Verification Commands
Run:
nix run . # on Linux
nix flake check
What Must Be Green
- ASDF/cl-weave regression suite
- Nix packaging check when Nix is part of the release process
- the mdBook documentation build (
checks.documentation)
Refuse To Ship When
Do not ship when:
- public API changed without matching documentation updates
- examples no longer execute
- release docs or policy files are missing from the tracked tree
- tests pass only because regression coverage was silently removed