Releases: nerima-lisp/cl-prolog
Release list
v1.1.0
Added
- A coverage report.
nix build .#coverage(orsbcl --script run-coverage.lispoutside Nix) runs the regression suite under SB-COVER
and writes an HTML report instrumentingcl-prologandcl-prolog/weave
only, not thecl-weaveharness driving them. Nix embeds its runner in
flake.nixwithpkgs.writeText;run-coverage.lispremains the local
SBCL entry point.checks.coveragebuilds the Nix report in CI; it asserts
the report exists, not a coverage percentage. See
Testing. benchmarks/performance.lispgains three benchmark groups matching the
optimizations below: indexed substitution, ground-vs-nonground tabled
answer replay, and the left-recursion cache across cold/warm/revision-
changed rulebases.
Changed
- Fixed-builtin goal dispatch (
%goal-solver) uses a nestedeq(predicate)
->eql(arity) hash-table pair instead of one table keyed by a consed
(predicate . arity)underequal, avoiding a cons and anequalhash on
every builtin lookup — the hottest path in goal dispatch. - Left-recursion detection caches its per-revision, per-module SCC index and
its recursive predicate/arity membership check through nestedeq/eql
hash tables instead of anequal-keyed table over a freshly consed
(rulebase revision module)list, and now reads the rulebase's own
persistent analysis table directly instead of duplicating it into the
per-query table session. - Clause freshening (
%freshen-clause,%freshen-term) and indexed-
substitution's copy map use a small inline-array store that only escalates
to a hash table past 12 entries, avoiding a hash-table allocation for the
common case of a clause with few variables. - Unification's cyclic-pair scratch space stores a nested
eqhash table of
"already seen" right-hand terms per left-hand term instead of a plain list
checked withmember, turning an O(n) scan per remembered pair into an
O(1) lookup. - Tabled answers now record whether they contain unbound variables; replaying
a ground answer to a consumer skips variant instantiation entirely, since
there is nothing to rename. - Predicate first-argument indexing builds its per-key candidate lists with
one linear merge over the original clause order instead of rescanning
every clause once per distinct first-argument key, removing an
O(clauses × distinct keys) reconstruction from%make-predicate-descriptor. - Query-variable collection uses an
eqhash-table membership check instead
ofpushnew, avoiding an O(n) rescan per occurrence, and the query-solving
loop now collects a query's variables once before the search begins
instead of once per solution. format/2's column directives (~t,~|,~+) track the pending
segment's length and fill count incrementally instead of resumming the
pending output on every column-fill request, and literal text runs batch
throughsubseqinstead of appending one character at a time.predsort/3's merge sort splits its private list destructively via
nthcdrinstead of allocating twosubseqcopies per recursion level; the
caller copies the substituted input list once up front so the destructive
split cannot alias shared structure.- The lexer's tokenizer tracks open-delimiter depth with an explicit counter
instead of recomputing it from the delimiter stack's length on every
open/close paren, and its near-duplicate quoted-atom/string scanners are
unified into one macro. paredit-clibumpedv0.8.0->v1.0.0.- CI: the
nix-setupcomposite action no longer receivesCACHIX_AUTH_TOKEN
on pull-request runs, so a fork's workflow changes cannot exfiltrate the
write-capable Cachix credential; PR runs still read from the cache. - CI: the release workflow's
.asd-version andCHANGELOG.md-section
extraction moved fromsed/awkto Perl, which behaves identically on the
GNU and BSD tool variants the supported runners carry (the priorsed
pattern relied on a GNU-only escape and silently produced no match under
macOS's BSDsed).
Fixed
atom_number/2and thenumber_string-family conversions parse the full
ISO number-token grammar — including the0x/0o/0bradix and0'c
character-code notations — by delegating to the same reader the tokenizer
uses, instead of a separate hand-rolled decimal-only parser that rejected
those notations.
v1.0.1
Fixed
- A malformed Lisp-shape rule no longer terminates the query with a Lisp
error. Whenasserta/1,assertz/1orretract/1received the Lisp clause
shape(:- HEAD . BODY-GOALS)with a head that is not callable — say
(assertz (:- 42 (color a red)))— the branch that builds the ISO
type_error(callable, Culprit)named an unbound variable as the culprit.
Instead of the standard error, the query died with an SBCL
UNBOUND-VARIABLE, which nocatch/3and noprolog-type-errorhandler can
intercept. The culprit is now the offending head, and the same input raises
prolog-type-erroras ISO 13211-1 8.9.1.3 requires. - A rule whose head is a bare atom is now accepted in both spellings of a
clause.assertz((warm :- color(a, red))), the:-/2 term Prolog source
reads, normalizes its head through the same path a list head takes, so it
assertswarm/0. The Lisp shape(assertz (:- warm (color a red)))tested
the raw head instead and rejected it, even though the documentation describes
the two shapes as asserting a rule either way. Both now store the identical
clause, andclause/2reports the same body for each. - An uninstantiated head in the Lisp clause shape raises an
instantiation_error.(assertz (:- ?head (color a red)))reached the same
broken culprit expression; it now signalsprolog-instantiation-error,
matching how a bareassertz(X)is already handled.
The :-/2 branch that reads a rule from Prolog source text was never affected;
these three defects were confined to the engine's internal Lisp clause shape,
which is why the ISO conformance suite did not reach them. tests/builtin- dynamic-database.lisp now covers all three, asserting the specific condition
class — a bare "signals something" expectation accepts the Lisp UNBOUND-VARIABLE
just as readily as the ISO error and would not have caught the original defect.
v1.0.0
First stable release: the exported surface is now considered stable.
This release fixes the ISO 13211-1 conformance defects a first systematic audit
of the standard's syntax and builtin error contracts turned up — reachable from
ordinary Prolog source text, and each now covered by
tests/iso-conformance.lisp, which states its cases as the standard does.
Fixed
- atoms are now identified by their text, as ISO 13211-1 6.4.2 requires.
Quoting an atom used to produce a different atom —hello == 'hello'was
false — because an unquoted name was interned upcased while a quoted one kept
its spelling. The two encodings now live in different packages
(cl-prolog.verbatim-atomsholds any text containing an upper-case letter),
which makes the text/symbol mapping a bijection, and=/2,==/2,
compare/3andsort/2all decide identity on text. Consequences, each a
behavior change:writeq/1andprint/1no longer lose case:'FooBar'printed as
foobar, soterm_to_atom/2could not round-trip a mixed-case atomatom_codes/2,atom_chars/2,char_code/2,atom_string/2,
upcase_atom/2,downcase_atom/2,char_type/2andformat/2's~a
and~sreport the atom's text rather than its upcased symbol name —
atom_codes(abc, X)gave[65,66,67]andformat("~a", [hello])
printedHELLO- the standard order of terms ranks atoms by their characters (ISO 7.2.3),
so'B' @< aholds; it previously compared upcased symbol names, then
home packages, then creation order, which also letX == Ybe false for
a pairX = Yunified []is the atom of text"[]":[] == '[]'andatom_length([], 2)char_conversion/2andopen/3receive the character and pathname their
argument spells, not an upcased one
New exportedprolog-atomandprolog-atom-textname an atom by its text
from Lisp, which is now the only way to write one whose text is not lower
case; see Semantics.
- an atom that is an operator can now be written as a term, per ISO 6.3.3.1
(as an argument) and 6.3.4.3 (bracketed).functor(T, +, 2),
T =.. [+, 1, 2],X = (-),atom_length(-, 1),current_op(P, T, +),
compare(<, A, B)andsort(0, @<, L, S)all raised a syntax error, which
madeop/3,sort/4,predsort/3andcompare/3unusable from Prolog
source. An operator with no operand after it now reads as its atom; an
operator used unbracketed as another operator's left operand still does not,
as ISO requires the brackets there - a run of graphic characters is now one token, per ISO 6.4.2, and an
undeclared one is an atom. The tokenizer matched the longest declared
operator instead, so:- op(700, xfx, ===).— declaring an operator that by
definition does not exist yet — split===into==and=and failed to
parse assertz/1,asserta/1,retract/1andretractall/1now convert a
:-/2 term to a clause, per ISO 7.6.1. Only the Lisp-level
(:- HEAD . BODY-GOALS)shape was recognized, soassertz((h :- Body))from
Prolog source stored the whole:-/2 term as a fact head and lefth
undefined; runtime rule assertion was broken outright.clause/2returns the
body in the shape source text spells it, and a fact matches
retract((h :- true))- an atom is now quoted only when it cannot read back bare, per ISO 6.4.2.
writeq/1,print/1,~qandwrite_canonical/1quoted every atom that was
not a plain atom name, so a graphic token printed as'+','=..','@<',
'\+'and the solo chars as'!',';'— all of which are name tokens
needing no quotes. Still quoted, each because the reader forces it:,and
|(they would read back as separators),{}(not yet read as an atom), a
lone.(the end token per ISO 6.4.8), and a compound's functor unless it is
a plain name (this reader does not yet require(to follow a functor with no
layout, so a bare+(1,2)would read as the prefix operator applied to
(1,2), andwrite_canonical/1output has to stay re-readable) - the lexer now reads the numeric notations of ISO 6.4.4: the character-code
constant0'c(including0'',0'\nand0'\x41\) and the radix constants
0x,0o,0b. None of them parsed, soX is 0'a— the way a character code
is written — was a syntax error, and so was reading a term containing one from
a stream, since the source splitter also mistook0''s quote for the start of
a quoted atom - escape sequences in a quoted token are now decoded, per ISO 6.4.2.1. A
\passed the following character through verbatim instead, so'a\nb'held
the letternrather than a newline.\a \b \e \f \n \r \t \v \0, the radix
forms\xHH\and\OOO\,\\ \' \" \``, and the`-newline continuation are
all honored, in'...'and"..."alike - the bitwise operators of ISO 6.3.4.4 table 7 are now declared:
/\,\/,
xorat 500 yfx,<<and>>at 400 yfx, and prefix\at 200 fy. Their
evaluable functors already existed, soX is 1 << 3andX is 12 /\ 10were
computable but unwritable asserta/1andassertz/1no longer accept an unbound argument: they
stored a clause whose head was a fresh variable, which no call could ever
match, instead of raising the instantiation_error ISO 8.9.1.3 requires. A
rule with a variable head is rejected the same waycompare/3validates a bound Order argument (ISO 8.4.2.3): a non-atom is a
type_error(atom, _)and an atom other than<,=,>a
domain_error(order, _), where both silently failedclause/2rejects a non-callable Body withtype_error(callable, _)(ISO
8.8.1.3) instead of failing, andcurrent_input/1andcurrent_output/1
reject a non-stream argument withdomain_error(stream, _)(ISO 8.11.1.3),
which used to fail silently and hide a misspelled aliasnumber_chars/2andnumber_codes/2raisesyntax_error/1for text that
spells no number, as ISO 8.16.7.3 and 8.16.8.3 require, rather than
domain_error(number_text, _); theprolog-syntax-errorcondition is now
exported.read_term_from_atom/3validates its options through the same
read-option checkerread_term/2,3uses, so an unsupported option is a
domain_error(read_option, _)instead of being accepted and ignoredatom_number/2still fails rather than raising on unparseable text, and now
decides that before running its continuation, so an error raised by a later
goal is no longer mistaken for its own and swallowed- a bare
{}reads as the atom of that name (ISO 6.3.6) (If -> Then)without an else branch now works. ISO 7.8.7 makes if-then a
control construct in its own right, which fails when the condition fails; only
the if-then-else form had a solver, so the common( Cond -> Then )raised
existence_error(procedure, (->)/2).(If *-> Then)likewise- arithmetic now follows ISO 9.x where it had drifted:
**is 9.3.1's float
power (2 ** 3is8.0) while^is 9.3.10's integer-preserving one (2 ^ 3
is8), raising an integer to a negative integer power is
type_error(float, Base), and dividing two integers exactly stays integral
(4 / 2was2.0). Float overflow and underflow reach Prolog as
evaluation_error(float_overflow)/(underflow)instead of escaping as a
host condition - stream and I/O error contracts corrected across ISO 8.11-8.14: a
designator that could never name a stream isdomain_error(stream_or_alias)
rather thanexistence_error;open/3,4reports a non-atom mode as
type_error(atom);get_char/2andpeek_char/2reject a bound non-character
argument withtype_error(in_character)where they silently failed;
get_byte/put_byteusetype_error(in_byte)/type_error(byte)and check
the argument before the stream, and the stream's permission error names the
stream's actual type as the culprit (text_stream) rather than the required
one;write_term/2,3reports an unsupported option in thewrite_option
domain; andcurrent_char_conversion/2rejects a non-character argument char_code/2raisesrepresentation_error(character_code)for an integer that
names no character, per ISO 8.16.6.3, rather than a domain error; the
prolog-representation-errorcondition is new and exportedcurrent_prolog_flag/2raisesdomain_error(prolog_flag, F)for an atom that
names no flag (ISO 8.17.2.3) instead of failing- one operator name now means one operator in the operator table, whichever
symbol spells it, so redefining a standard operator throughop/3replaces
its entry instead of adding a second invisible one at a different priority numbervars/3uses the ISO 8.14.2 functor'$VAR', whose text is upper
case; the distinct lower-case atom'$var'is no longer written as a
variable name- CI: the SBCL check timeout now sends a follow-up
SIGKILL, since SBCL can
defer a bareSIGTERMpast a tight compiled loop;nix flake check's step
now carries its own budget narrower than the job's, so a hang reports an
attributable timeout instead of an ambiguous job cancellation - CI:
packages.defaultandapps.test(the two entry points README.md
documents) are now actually built bynix flake check, not merely
evaluated —apps.testin particular exercises a genuinely different code
path (cl-weave's own dynamic-space sizing) that was previously untested - CI: tag releases now require a green
nix flake checkon the tagged commit
before publishing, closing a gap where a redmaincould still be tagged
and released; the release workflow's:versionextraction is now anchored
the same wayflake.nix's already was, so a comment merely mentioning
:versioncan no longer shadow the real field - CI: a missing
CACHIX_CACHErepository variable now emits a build warning
instead...
v0.8.0
Added
library(assoc):empty_assoc/1,put_assoc/4,get_assoc/3,
del_assoc/4,list_to_assoc/2,assoc_to_list/2,assoc_to_keys/2,
assoc_to_values/2— association maps keyed by the standard order of terms
(builtins/assoc.lisp)library(pairs):pairs_keys_values/3(bidirectional),pairs_keys/2,
pairs_values/2(builtins/pairs.lisp)plus/3, the integer relationA + B =:= Cusable in any single-unknown
mode- a SWI-compatible string term type (represented as a Common Lisp string):
thedouble_quotesflag gains astringvalue and is now honored by the
reader ("..."produces codes/chars/atom/string per the flag);string/1
andatomic/1classify strings; the standard order of terms places String
between Atom and Compound;==/compareuse content equality; and the term
writer prints strings raw forwriteand as escaped"..."for
writeq/write_canonical - string builtins (
builtins/string.lisp):string_length/2,
string_concat/3(relational),atom_string/2,string_to_atom/2,
number_string/2,string_chars/2,string_codes/2,term_string/2,
text_concat/3,sub_string/5,split_string/4, plus the shared%text-of
text-coercion helper - an
occurs_checkProlog flag (truedefault,false,error):=/2
consults it —falseallows a cyclic binding,errorraises when a cycle
would form;unify_with_occurs_check/2always checks; clause-resolution
unification stays occurs-checked for safety - regression suites
builtin-stringandbuiltin-occurs-check - character classification
char_type/2andcode_type/2(simple types plus
thedigit(W),to_lower/1,to_upper/1,upper/1,lower/1,code/1
parametric types) and case foldingupcase_atom/2,downcase_atom/2
(builtins/char-type.lisp) term_to_atom/2(bidirectional term/text conversion) and
read_term_from_atom/3, routing parser failures through catchable ISO
syntax_error/resource_error(builtins/term-io.lisp)sort/4(key-and-order sort with@</@=</@>/@>=, key 0 = whole term),
predsort/3(comparison-predicate sort dropping=duplicates), and
aggregate_all/3(count,count/1,sum,max,min,bag,set)
(builtins/collection.lisp)- evaluable arithmetic functions
gcd/2,atan2/2,msb/1,lsb/1,
popcount/1, and the constante(builtins/arithmetic.lisp) - regression suites
builtin-char-typeandbuiltin-term-io, plussort/4,
predsort/3,aggregate_all/3, and arithmetic-function coverage library(lists)predicates:sum_list/2(with thesumlist/2alias),
max_list/2,min_list/2,numlist/3,list_to_set/2,subtract/3,
intersection/3,union/3, andpermutation/2(builtins/list-extra.lisp)library(apply)meta-predicates driven through the CPS prover:
maplist/2and up (variadic in the number of lists),foldl/4..6,
include/3,exclude/3, andpartition/4(builtins/apply.lisp); element
goals are cut-opaque likecall/1, and the filter predicates test
provability only and do not leak their goal's bindings- formatted output:
format/1,2,3supporting the~w ~p ~q ~a ~s ~d ~D ~f ~e ~g ~r ~R ~c ~n ~~directives, the~`cfill character,~*
argument-supplied counts, and column control (~tfill,~|absolute
column,~+relative column), plustab/1,2andprint/1,2
(builtins/format.lisp); format strings may be atoms, code lists, character
lists, or Common Lisp strings.~pquotes likeprint/1, and~e/~guse
the C/Prolog exponent convention *max-prolog-builtin-output-length*: a configurable, exported bound on the
characters or list elements a single builtin call may materialize, so
formatfill/repeat/newline runs,tab/1, andnumlist/3ranges raise a
catchableresource_errorinstead of exhausting memory on hostile input- regression suites
builtin-list-extraandbuiltin-formatcovering the new
predicates, their error contracts, and resource bounds
Security
- malformed
formatdirectives, out-of-range~rradices, invalid~c
character codes, and too-few-argument errors now raise catchable ISO errors
instead of escaping to the host as uncatchable Common Lisp conditions numlist/3,tab/1, andformat's repeat/fill/newline counts are bounded
against*max-prolog-builtin-output-length*, closing unbounded-allocation
denial-of-service vectors reachable from untrusted querieschar_type/2,code_type/2,sort/4, andaggregate_all/3guard their
compound arguments with a proper-list check before measuring length, so a
partial list (e.g.char_type(a, [x|y])) raises a catchable ISO error
instead of an uncatchable hosttype_error- the
<<left-shift arithmetic result size is now bounded against
*max-prolog-arithmetic-result-bits*(like**/^), so1 << hugeraises
a catchableresource_errorinstead of allocating an unbounded bignum (>>
only shrinks the result and needs no bound)
Changed
- the term writer is now cycle-safe: it marks each cons on the current path and
emits...on revisiting one, so writing a cyclic term (which the
occurs_check=falseflag lets a user build) terminates instead of recursing
forever; acyclic shared subterms still print in full list_to_set/2deduplicates in O(n log n) via an index-tagged standard-order
sort instead of the previous O(n^2) linear scan, preserving first-occurrence
orderaggregate_all/3evaluates thesum/max/mintemplate as an arithmetic
expression (sosum(X*2)works), matching SWIpredsort/3fails (rather than raising) when its comparison predicate has no
proof, matching SWI; its merge step is iterative to keep the control stack
O(log n) on large listschar_type/2/code_type/2graphicnow denotes the Prolog symbol-char
class (distinct fromgraph)
v0.7.0
Highlights
Added
- SLG tabling engine (
table-variant.lisp,tabling.lisp): variant-check keys, per-query table sessions, answer tables, and fixpoint iteration, threaded through the proof-state continuation so builtin-dispatched goals inherit the active tabling context - Benchmarks harness (
benchmarks/): in-process micro-benchmarks and a cross-engine comparison script against SWI-Prolog, Trealla, and Scryer Prolog on a shared, checksum-verified workload - CI matrix covering
x86_64-linuxandaarch64-linux
Correctness / ISO conformance
get_code/peek_codetrack end-of-stream identically toget_char/peek_charper ISO 8.11.3/8.11.4; stream alias validation shared across IO builtins- Goal-dispatch and DCG expansion hardened; finite-domain constraint handling refined
Performance
- Environment indexing in unification uses a bounded overlay over an immutable base table instead of a full rehash on every binding, avoiding an O(n) rebuild per variable binding
Refactor
- Parser, term/atom builtins, and source-loader split into focused modules (lexer/grammar layers, term-inspect/compare/construct, atom-ops/text-conversion/atom-number-conversion, source-io/directives/registry/rollback) with no behavior change
- Regression suite reorganized into thematic files with a shared query helper lifted into
tests/support/core.lisp
Documentation
- Architecture, API reference, builtin goals, querying, semantics, development, and troubleshooting guides synced with the module split and the new tabling and benchmarking surfaces
Full changelog: https://github.com/nerima-lisp/cl-prolog/blob/main/CHANGELOG.md
v0.6.0
Highlights
Correctness
- Parsed finite-domain ranges finally work:
X in 1..5was rejected by the finite-domain store (only the Lisp-shaped infix range form worked); both shapes are now accepted - Quoted
?-prefixed atoms such as'?x'are real atoms instead of being misread as logic variables - ISO-conformant goal dispatch:
instantiation_errorfor variable goals,type_errorfor non-callable or improper goals - Closing the selected
current_input/current_outputstream resets the selection touser_input/user_output member/2/append/3terminate on cyclic lists; cyclicconsult/load_filessource lists raise a resource error;.inside{...}no longer splits a clause early
Resource hardening
- New exported parser resource limits (source size, tokens, depth, lexeme lengths, interned-symbol budget) with a
prolog-parser-resource-errorcondition surfaced as catchable ISOresource_error/1 - Untrusted input no longer permanently interns symbols (syntax errors, missing paths, stream handles, operator specifiers, arithmetic keys); exponentiation is bounded
Performance
- Hash-indexed unification environments; iterative, cycle-safe substitution
- O(1)
assertzand tabled-answer deduplication; SCC-based left-recursion detection - O(n log n)
bagof/setofgrouping; augmenting-pathall_different
Documentation
- Install/run instructions match reality (ASDF workflow; not on Quicklisp), complete exported-symbol API reference, accurate
phrase/unifycontracts, explicit Linux-only flake outputs, concrete security-reporting and code-of-conduct procedures
Full changelog: https://github.com/takeokunn/cl-prolog/blob/main/CHANGELOG.md
v0.5.0
What's Changed
- feat: add cl-weave query test helpers by @takeokunn in #1
- Harden Prolog runtime semantics and use Linux-only CI by @takeokunn in #2
New Contributors
- @takeokunn made their first contribution in #1
Full Changelog: v0.4.1...v0.5.0
v0.4.1
Patch release: internal refactoring and build hardening on top of v0.4.0.
Changed
- the stream read/write builtins (
read/read_term,write-family,nl,flush_output, character/byte I/O,at_end_of_stream) moved fromio.lispinto the newio-streams.lisp, defined through a shared%define-io-dual-builtinmacro that derives the current-stream and explicit-stream variants from one specification; the macro validates its clause plist at macroexpansion time so a malformed definition fails the build instead of compiling a builtin that silently always fails %io-optionsand%io-read-optionsshare one option-list parser (%io-parse-option-list) instead of two divergent copiesquery-prologandquery-prolog-firstreuse the solution-mapping core ofmap-prolog-solutionsinstead of re-decoding their options through the public entry pointdeftest-table/deftest-unificationexpand each table spec into its own named cl-weave case, so a failing spec is reported individually
Added
- a regression test that
read_term/2rejects unsupported read options with an ISO domain error
See CHANGELOG.md for the full history, including the v0.4.0 notes (ISO built-in completion and the cl-weave test migration).