Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

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 or nil (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 nil returns raw proof environments instead
  • :max-depth optionally bounds user-rule resolution (the default is NIL, meaning unbounded); exhaustion signals prolog-depth-limit-exceeded; 0 disables rule expansion entirely, facts still match
  • :limit must be nil or 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. bagof groups free variables, while setof also 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-predicate is the public extension API and registers one exact predicate name/arity pair
  • the predicate argument list has fixed arity; lambda-list keywords such as &rest are not supported
  • rulebase, environment, and depth expose the current solver context
  • emit must 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 clauses
  • define-rulebasedefparameter + prolog
  • extend-rulebase — functional extension; the new clauses shadow the base
  • def-rule — define a reusable clause-producing form
  • with-prolog-query — bind variables from the first solution
  • prolog-matchcond over 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 forms
  • phrase(phrase rulebase rule-name input) returns (values remainder matched-p) for the first parse
  • phrase-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-depth to a non-negative integer to bound user-rule resolution; exhaustion signals prolog-depth-limit-exceeded rather 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 symbols
  • fresh-logic-variable — return a fresh symbol satisfying logic-var-p
  • unify(unify left right &optional environment) returns (values extended-environment t) on success and (values nil nil) on failure; the occurs check is always enabled
  • logic-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; nil means unbounded
  • define-foreign-predicate — register one exact predicate name/arity pair using the solver’s zero-to-many emit continuation 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-solutions
  • query-prolog
  • query-prolog-first
  • prolog-succeeds-p
  • solution-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

  • prolog
  • define-rulebase
  • extend-rulebase
  • def-rule
  • with-prolog-query
  • prolog-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-error and reader invalid-max-depth-error-value
  • Depth exhaustion: prolog-depth-limit-exceeded and reader prolog-depth-limit-exceeded-goal
  • Structurally invalid goals: invalid-goal-error and reader invalid-goal-error-goal
  • Prolog throws: prolog-exception and reader prolog-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-halt and reader prolog-halt-code
  • Arithmetic diagnostics: arithmetic-evaluation-error and readers arithmetic-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 Linux
  • asdf: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:

  1. package.lisp
  2. operator-table.lisp
  3. module-system.lisp
  4. data.lisp
  5. unification.lisp
  6. parser.lisp
  7. term-writer.lisp
  8. engine.lisp
  9. io-context.lisp
  10. prover.lisp
  11. builtins/core.lisp
  12. builtins/control.lisp
  13. builtins/collection.lisp
  14. builtins/dynamic.lisp
  15. builtins/arithmetic.lisp
  16. builtins/list.lisp
  17. builtins/atom.lisp
  18. builtins/operator.lisp
  19. builtins/io.lisp
  20. builtins/io-streams.lisp
  21. builtins/io-code.lisp
  22. fd-store.lisp
  23. builtins/fd.lisp
  24. builtin-term.lisp
  25. dcg-runtime.lisp
  26. query.lisp
  27. source-loader.lisp
  28. dsl-compiler.lisp
  29. dsl.lisp
  30. dcg.lisp

The important boundaries are:

  • data.lisp owns clauses, the logical-update rulebase, indexes, and mutable registries
  • engine.lisp owns conditions plus builtin and foreign-predicate registries
  • prover.lisp owns normalization, proof state, dispatch, clause resolution, cut barriers, depth accounting, and tabling
  • query.lisp turns the continuation protocol into the public mapping and result APIs
  • source-loader.lisp, dsl*.lisp, and dcg*.lisp are 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, and assertz/1 insert into the supplied rulebase
  • retract/1, retractall/1, and abolish/1 retire matching entries
  • consult-prolog transactionally replaces the clauses registered for a source after validation
  • rulebase-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

  1. nix run . — run the cl-weave-backed ASDF regression behavior on Linux
  2. nix 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 nil on 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 only
  • 1: 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-nil remainder

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.md
  • docs/src/api-reference.md
  • docs/src/architecture.md
  • docs/src/troubleshooting.md
  • CHANGELOG.md
  • CONTRIBUTING.md
  • CODE_OF_CONDUCT.md
  • SECURITY.md
  • SUPPORT.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

Repository documentation