Branch: development (switch to
stable),
SWI-Prolog Changelog from version 10.1.12 to 10.1.13
[Aug 7 2026]
- FIXED: MacOS: shell/1 left Prolog without usable standard streams The
child of shell/1 calls
adopt_ctty() when the streams it is handed
are not the controlling terminal of our session -- the pty of an
epilog window. It then is the session leader of a session that owns
a terminal, and on BSD systems, MacOS among them, the kernel revokes
such a terminal when its session leader exits. Revoking detaches
every descriptor on it from the device, in this process as well: the
descriptors stay open, but reading and writing them fails with EIO.
Prolog died on the first message it printed after the child was gone,
reporting only "Cannot write to user_error".
[Aug 6 2026]
- ENHANCED: apropos/2: use the Unicode ellipsis for truncated summaries
Only if the encoding of the message stream can represent it.
- ADDED: apropos/2: limit and offset options and clickable matches If
the terminal supports hyperlinks (Epilog), the matches of apropos/1
and apropos/2 are now links that run help/1 on the match and
the line telling there are more matches links to the next page.
XPCE class members are displayed using the arrow notation,
e.g.
dialog_item->open.
- ADDED: help/1: make the manual hyperlinks in the output work Clicking
a manual reference in an Epilog terminal now quits the pager and runs
help/1 on the linked object. The links are resolved to a `man:` IRI
holding the documentation object while the page is rendered, using
the PlDoc link rewriting that already serves the documentation browser.
- DOC: Update documentation for XPCE's threading model
- FIXED: ansi_hyperlink/3 to take registered iri schemes as non-url.
is_absolute_file_name/1 also considers registered iri schemes as an
absolute path. We only want to exclude Windows drive specifications.
[Aug 5 2026]
- ENHANCED: ansi_hyperlink/3: more general detection of non-file links.
Now considers
[a-z]+: a link, unless it is an absolute file name
(e.g., C:... on Windows).
- ADDED: html_text/2: pass hyperlinks through.
- ADDED: ansi_format/3,4: support
href(URL) to create an OSC 8 escape.
- FIXED: Windows: quitting the pager of help/1 left the screen cleared
A pager on Windows takes a screen buffer of its own from the console
API, and the console swaps back to the previous one when it exits:
that is what puts the terminal back as it was. A pseudo console
does not carry those calls -- its alternate screen is the DEC private
mode and nothing else -- so an Epilog window was left with whatever
the pager had drawn on it. with_pager/1 now asks for the alternate
screen itself.
- DOC: Windows: document the pseudo console stream functions
Swinpseudoconsole() and friends are how a foreign library puts a
child process on the terminal behind a stream. Documented in the
foreign streams section rather than in the comments of SWI-Stream.h.
- ADDED: Windows: export the pseudo console attributes
to foreign libraries Swinpseudoconsole_attributes() and
Swinfree_pseudoconsole_attributes() build and free the process
attribute list that puts a child on the pseudo console of an
Epilog window. System() had this private; process_create/3 lives
in packages/clib and needs the same, and reaching a window from a
foreign library is what the Swin*
pseudoconsole() calls are for.
- FIXED: file_autoload_directives/3: Handle XPCE text_buffer correctly.
Used @(Ref) rather than object/1.
- FIXED: help/1: find pager on OSes that use extensions for executables.
[Aug 4 2026]
- ADDED: Windows: run the child of shell/1 on the terminal's own console
An Epilog window has no console: it talks to its Prolog thread over
two pipes, and a child handed those gets no terminal -- nothing echoes
what it reads, it writes through the console API rather than as VT,
it sees no window size and ^C does not reach it.
- FIXED: Windows: shell/0 blocked, shell/1 wrote nowhere and neither
waited The child was created with CREATE_NO_WINDOW and no inherited
handles, so it got a hidden console of its own: shell/0 ran a shell
reading from a keyboard buffer nobody fills, and shell/1 wrote
where nobody looks. A console process now shares its console with
the child, and hands it the calling thread's streams, as System()
in pl-os.c does on POSIX. The flag is what a GUI process needs --
there a console child pops up a window of its own -- so it is kept
when we have no console to share.
- FIXED: shell/0 on Windows: %comspec% before the posix_shell flag $SHELL
and the posix_shell flag name POSIX paths, which a Windows box may
well resolve without being able to execute them, so an interactive
shell went looking for /bin/sh. %comspec% now comes first there.
Set the
shell flag to pick another one.
[Aug 3 2026]
- FIXED: a process started from a Prolog thread had SIGINT blocked
Only the main thread processes SIGINT, so every other thread
blocks it. A thread that forks handed the child that mask, and
exec() keeps the mask where it resets the handlers, so ^C did not
reach a process started by shell/1 or process_create/3 from a thread.
PL_cleanup_fork() now clears the mask, which is where the other traces
of Prolog that must not leak into the new process are removed.
- FIXED: let a Windows console act on the escape sequences itself The
standard streams of a Windows console are wrapped by an emulation
of our own, which parses escape sequences and acts on them through
the console API. It acts on SGR. Everything else -- cursor motion,
erase display, erase line, save and restore -- sits behind #if 0,
calling into the console that swipl-win used to have. The parser
consumed those sequences all the same, so a program that cleared the
screen with ESC [ 2 J found that nothing had happened and nothing
had been written either:
[Aug 2 2026]
- FIXED: no warning when restoring the state of a terminal that is gone
A terminal can disappear while we hold it: closing the master end of a
pty makes every operation on the slave fail with EIO. SetTtyState()
warned about that, but there is nothing left to restore and nothing
the user can do about it.
- ENHANCED: shell/1 adopts the terminal it was handed System() already
gives the child the calling thread's streams, and a thread need not
run on the terminal the process was started from: an Epilog window
gives its Prolog thread a pty of its own. Inheriting the fd is not
enough there. With no session owning that pty it has no foreground
process group, so resizing the window raised no SIGWINCH in the child
and ^C sent it no SIGINT.
[Aug 3 2026]
- FIXED: is/2:
2^2147483648 and larger on Windows. Completes
8733c1a4f8efa2733e341641e64dfda8c0656205
- ADDED: cls/0: CLear Screen.
- FIXED:
2^2147483648 and larger on Windows.
[Aug 2 2026]
- CLEANUP: html_text: measure table cells without rendering them twice
Column widths were established by formatting every cell a second time
with
tty(false) and counting characters, because ANSI attributes
would otherwise be counted as text. write_size/4 reports display
columns and ignores escape sequences, so the cells are measured as
they are printed.
- CLEANUP: ansi_term: drop keep_line_pos/2 The stream layer no
longer counts escape sequences as columns, so saving and restoring
line_position/2 around every sequence written is redundant.
set_stream/2 keeps the line_position property; it now has no callers
in the system but remains useful for text whose width the stream
cannot know.
- DOC: display columns and ANSI escape sequences Co-Authored-By:
Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session:
https://claude.ai/code/session_01LmQHEtHoVQ2NH2Zzusvepa
- ENHANCED: format/2 column stops count display columns ~t, ~|, ~+ and
~N now use the stream layer's Supdatepos() rather than a private copy
of the same rule. Escape sequences are skipped, a tab advances to
the next multiple of 8 and non-printable characters take no columns,
so format/2 and line_position/2 always agree.
- ENHANCED: line_position/2 skips ANSI escape sequences Escape sequences
are not visible, so they no longer advance the column. This makes
format/2 column stops, ~N and any code using line_position/2 work
on coloured output without help. character_count/2 and byte_count/2
keep counting every character.
- FIXED: line_position/2 for tabs and non-printable characters A tab
moved the column to the next multiple of 8 minus 2 and every other
non-printable character decremented it. Both stem from `linepos +=
PL_wcwidth(c)`, where PL_wcwidth() returns -1 for control characters.
- TEST: Make AGC margin tests work in single threaded version.
[Jul 28 2026]
- DOC: say where PL_register_blob_type() belongs It was described as
something you never need to call, which is true of the registration
itself but leaves the type invisible until its first blob exists.
Call it from
install() so that loading the library is enough for
blob_type_property/2 to see the type and set_blob_type/2 to give it
a collection budget, and so that its place in the standard order of
terms does not depend on which type happens to create a blob first.
- FIXED: register the remaining built-in blob types at startup A blob
type is registered when its first blob is created, so zipper, undo,
closure, thread and locale did not exist until something used them.
Two consequences: blob_type_property/2 cannot report a type before it
is used, and set_blob_type/2 cannot give it a collection budget, which
is backwards for a setting whose purpose is to bound accumulation.
- ADDED: blob_type_property/2 and set_blob_type/2 Replaces the
provisional set_blob_gc_margin/2 with the pair every other property in
the system uses, stream_property/2 and set_stream/2. It has somewhere
to grow: agc_close_streams is a global flag that only ever affects
the stream blob, and is really a per type policy.
- DOC: describe the per blob type atom-GC budget Documents gc_margin and
set_blob_gc_margin/2, and what `len' means for a PL_BLOB_NOCOPY blob
now that it also decides how much the blob counts towards that budget.
- FIXED: do not dump the target of a PL_BLOB_NOCOPY blob
writeBlob(),
the writer used for a binary blob whose type provides no write(), hex
dumps `length' bytes starting at `name'. For a PL_BLOB_NOCOPY blob
`name' is the foreign object and `length' describes what it retains
rather than how much of it we may read, so this walked off the object.
It was harmless while every such type passed sizeof(void*), but
a type that reports a real size would print megabytes of somebody
else's memory. Print the handle instead.
- ENHANCED: let a blob type declare its own atom-GC budget Atom-GC is
requested after agc_margin atoms have become candidates. An atom is
about fifty bytes, but a blob atom is the same fifty bytes whether
it wraps a point or a ten megabyte memory file, a compiled regex,
an answer trie or an SSL context, so the count says nothing about
what is being kept alive. Two hundred atoms of a hundred kilobytes
each hold twenty megabytes and never come close to the margin.
- FIXED: consider atom-GC when references are dropped, not only when
created
considerAGC() had one call site: lookupBlob(), every 128th
new atom. The counter it tests, GD->atoms.unregistered, grows
in unregister_atom(), which never tested it. Dropping the last
reference to a large number of atoms therefore reclaimed nothing
until something else happened to allocate atoms. Retracting 30,000
clauses that hold the only reference to an atom each and then running
clause-GC reclaimed 0 atoms; it now reclaims all of them.
[Jul 27 2026]
- FIXED: WASM: read a JavaScript object reference back using
blob(resolve) JavaScript objects are now written as <js>(Id,Class)
rather than <js_Class>(Id). The name between the brackets must be
the name of the blob type for the reader to find the object back,
so the blob type is renamed from `js_object' to `js'.
- TEST: read_term/3
blob(resolve) matching on the blob address A zipper
writes the address of the blob data and is thus found on the address;
a stream writes the address of its handle and takes the fallback.
Either way the text must match exactly.
- DOC: describe how read_term/3
blob(resolve) finds the blob
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session:
https://claude.ai/code/session_017dv8qG8JrSfiHguZaFbLAW
- ENHANCED: read_term/3
blob(resolve) finds the blob on its address
Most blob write() functions print the blob data using `%p'. If the
first argument of the text is that address we no longer need to
write every blob of this type to find the one that produced the text.
For 20,000 live regex blobs this makes resolving one 150 times faster.
It also avoids taking a lock the write() may need, e.g., the Python
GIL for library(janus).
- ENHANCED: Sfprintf() %As accepts the precision as string length
Allows writing text that is not 0-terminated, e.g., the pattern of
a library(pcre) blob.
- ADDED: SfprintfX() %As to emit a string as a quoted atom A blob
write() function must emit its arguments as valid Prolog syntax for
read_term/2,3 to be able to read them back. %As does that for a name,
file name, pattern, etc. without creating a Prolog term.
- DOC: Merge all read predicates into one description. Follows the write
predicates: read/1,2 and read_term/2,3 now share the description of
read_term/3, the most general form. read_clause/3 moved below the
option list as it refers to it.
[Jul 26 2026]
- DOC: read/1 and write/1 no longer use the C stack Section "Other
Limits" claimed they may crash the system on deeply nested terms,
which no longer holds now both use an explicit stack.
- CLEANUP: rename O_C_STACK_GUARDED to O_ALTSIGSTACK Nothing is guarded
since the
setjmp() context was removed; what the flag enables is
handling SIGSEGV on an alternative signal stack. Also renames
initGuardCStack() to initAltSignalStack().
- CLEANUP: remove the unused C-stack overflow guard Nothing uses
C_STACK_OVERFLOW_GUARDED since the reader and the writer no longer
recurse on the C stack, so the
setjmp() context and the sig_critical
flag in LD->signal are dead as well. What remains of O_C_STACK_GUARDED
is running SIGSEGV on an alternative signal stack: an overflow leaves
no C stack to run the handler on, and this way sigCrashHandler()
can still print the crash report. alt_segv_handler() did nothing
else and is replaced by sigCrashHandler() itself.
- ENHANCED: shrink the writer's resume frame Keep the sorted order of
the dict pairs on the heap rather than in the frame: the inline array
made every frame 64 bytes larger, while dicts are a small minority
of the frames pushed. Writing deeply structured terms is now within
1-3% of the recursive writer and writing dicts is about 10% faster,
as
pl_for_dict()'s callback and its 2Kb stack buffer are gone.
- TEST: deep term nesting for write/1 Write 20,000 levels of compounds,
lists, list tails, braces, operators (prefix, infix and block), dicts
and a mix of these, in the operator, the canonical and the dotted
list notation, and read the result back. The old recursive writer
ran out of C stack at about 18,000 levels natively and 2,000 on WASM.
- CLEANUP: drop C_STACK_OVERFLOW_GUARDED around
writeTerm()
writeTopTerm() was the last user of the SIGSEGV catching guard,
which is redundant now the writer no longer recurses on the C stack.
The sigaltstack machinery itself stays: unify, compare and friends
do still recurse.
- ENHANCED:
writeTerm() iterative via segstack Fold the recursion
between writeTerm(), writeTerm2(), writeList() and writeDictPair()
into a state machine over a segstack of wframe resume frames.
Writing a term used to cost about 450 bytes of C stack per level of
nesting, limiting the depth to about 18,000 levels natively and 2,000
on WASM; it now nests on the heap.
- DOC: refer to the split read_<construct>_*() functions
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session:
https://claude.ai/code/session_017dv8qG8JrSfiHguZaFbLAW
- TEST: deep term nesting for read/1 Read 20,000 levels of compounds,
lists, list tails, braces, parentheses, operators, dicts and a mix of
these, as well as a syntax error at that depth. The old recursive
parser ran out of C stack at about 8,000 levels natively and 1,000
on WASM. Restores the unconditional
termtest(1000), which had to be
reduced to 500 on WASM for the latter reason.
- CLEANUP: drop C_STACK_OVERFLOW_GUARDED around
complex_term() The
parser no longer recurses on the C stack, so the SIGSEGV catching
guard in read_term() is redundant. Note that it never worked on WASM,
which has neither sigaltstack() nor C-stack overflow detection.
- ENHANCED:
complex_term() iterative via segstack Fold the recursion
between complex_term(), simple_term() and the read_<construct>()
functions into a state machine over a segstack of pframe resume
frames. Reading a term used to cost about 1Kb of C stack per level
of nesting, limiting the depth to a few 10,000 levels natively and
about 1,000 on WASM; it now nests on the heap.
- TEST: Reduced term_to_atom-2 on WASM, where the C-stack is too small.
- FIXED: use GCD dispatch semaphores on macOS instead of named
POSIX semaphores macOS does not implement unnamed POSIX semaphores
(
sem_init() is a deprecated ENOSYS stub), so this code used a named
semaphore opened with a fixed name "pl" via sem_open()/sem_unlink().
That name lives in a single machine-global namespace, which has two
failure modes when many short-lived Prolog processes run concurrently:
- BUILD: ship prebuilt desktop/swipl.icns instead of generating it
at build time iconutil (used by desktop/make_icns.sh) delegates
to the IconServices system daemon, which refuses callers running
under sandbox-exec and is unavailable in many CI environments,
breaking the macOS icon build there. Commit the generated icon as a
source asset and copy it into the build tree with copy_if_different.
Regeneration from swipl-256.png is now the opt-in
regenerate_icns
target (macOS, outside a sandbox), documented in desktop/README.md.
- DOC:
blob(Mode) read option, blobs in the standard order of terms
Documents the read_term/2,3 blob(Mode) option, why `error' is the
default, the cost of blob(resolve) and the constraint that the
notation is unavailable where `<' is a prefix operator. Notes that
blob/2 reports the type a dead blob stands for while current_blob/2
reports `unavailable', documents blob_released/1, and states the
requirement that a blob write() callback emit valid Prolog syntax
between the brackets.
- TEST: read_term/2,3 option
blob(Mode) and blob_released/1 39 tests over
four suites: blob(dead) (round-trip, type profile, blob/2 reporting the
recorded type, accessors rejecting it, compare/3 agreeing with ==/2,
Prolog-syntax arguments), blob_released/1, blob(resolve) (finding a
live stream and a clause reference, falling back for a stale address)
and the syntax itself (the default remains a syntax error, forms like
<(a,b), <>(a) and f(a<b, c>d) keep their meaning, and a prefix `<'
operator keeps <a>(f) reading as >(<(a),f)).
- ADDED: blob_released/1 and a safe representation for released blobs
PL_free_blob() sets the blob's data to NULL but left its type in
place, so write/1 still dispatched to the type's
write() callback
and compare/3 to its compare(), both of which dereference the object
that is gone. This crashed for any blob type that does not check
PL_blob_data() for NULL. Such a blob now writes as <Type>(freed)
and is ordered by handle, and blob_released/1 tests for the state.
- ENHANCED: Toplevel accepts blobs written as <type>(...) The
interactive toplevel reads with
blob(resolve), so a blob copied from
earlier output can be pasted back into a goal. A form whose object
no longer exists reads as a dead blob rather than a syntax error,
which keeps the rest of the goal usable.
- ADDED: read_term/2,3 option
blob(Mode) to read <type>(...) blob syntax
Non-text blobs write as <type>(Arg, ...), which the reader rejected, so
a single blob made the whole enclosing term unparsable: stack traces,
~q log files and toplevel copy/paste all suffer. The reader now
claims this notation, but only when asked: blob(error) is the default
and unchanged, blob(dead) yields a blob without a foreign object,
and blob(resolve) additionally looks for a live blob that writes as
this text.
- MODIFIED: Sfprintf() %p now prints a canonical 0x<lowercase-hex>
pointer The platform %p is neither portable nor readable as a Prolog
number: glibc prints NULL as "(nil)" and the MSVC runtime prints
uppercase hex without the 0x prefix. Blob
write() callbacks print
their handle with %p, and that output must be acceptable to the reader.
Rendering the pointer ourselves fixes all ~250 %p uses in one place.
- TEST: Pin the current reader behaviour for the `<type>(...)` blob
syntax Nine cases covering the forms the reader must keep rejecting
(`<stream>(0x…)` and friends) and the neighbouring forms that do
parse today and must keep their meaning (
<(a,b), <>(a), f(a<b,
c>d)). This guards the upcoming reserved blob token, which is only
active when read_term/2,3 is given a non-default blob(Mode).
- CLEANUP: Turn reader token types into an enum The reader's token types
were
#defines named T_FUNCTOR, T_STRING, etc. These collide
with the VM instruction names of the same spelling, which pl-read.c
sees through pl-incl.h; the macros silently redefined them for
this file. They are now an enum token_type with a TK_ prefix,
which removes the collision and gives the switch on token types
compiler exhaustiveness checking.
[Jul 24 2026]
- DOC: Settle modes for foldl/4-7 and scanl/4-7 This invalidates PR #820.
- DOC: PR #770 Improve docs on determinism, \+/1 and type tests.
This merges PR #770 after several edits.
[Jul 19 2026]
- CLEANUP: library(apply): removed the indexing helper predicates
Predicates such as maplist/3 called a helper with the list as first
argument to get clause indexing. This is no longer needed as clause
indexing considers the first argument for which some clause has
a non-variable.
Package archive
[Jul 28 2026]
- ENHANCED: register the blob type when the library is loaded A blob
type is otherwise registered only when its first blob is created, so
blob_type_property/2 cannot report it and set_blob_type/2 cannot give
it a collection budget until the library has already been used, which
is backwards for a setting whose purpose is to bound accumulation.
Package clib
[Aug 5 2026]
- FIXED: Windows: process_create/3 gave its child no terminal An Epilog
window has no console: it talks to its Prolog thread over two pipes,
and a child handed those has nothing that echoes, no window size and
no ^C. with_pager/1 in library(help) therefore hung on Windows --
the pager sat waiting for a key from a console nobody types on.
[Aug 3 2026]
- FIXED: process_create/3 adopts the terminal it was handed A thread
need not run on the terminal the process was started from: an Epilog
window gives its Prolog thread a pty of its own, and process_create/3
hands that pty to the child. Inheriting the fd is not enough there.
With no session owning that pty it has no foreground process group,
so resizing the window raised no SIGWINCH in the child and ^C sent
it no SIGINT.
[Jul 28 2026]
- ENHANCED: register the blob type when the library is loaded A blob
type is otherwise registered only when its first blob is created, so
blob_type_property/2 cannot report it and set_blob_type/2 cannot give
it a collection budget until the library has already been used, which
is backwards for a setting whose purpose is to bound accumulation.
Package cpp
[Jul 26 2026]
- TEST: a released blob writes as <Type>(freed) Its own
write() is no
longer called, as that would dereference the object that is gone,
so the freed blob prints using the registered type name rather than
the mangled C++ one. Also exercises blob_released/1.
Package libedit
[Aug 7 2026]
- FIXED: MacOS: reconnect the terminal after a child took it down
el_wrap/4 hands libedit duplicates of the streams' descriptors, as
libedit owns and closes what it is given. On BSD systems, MacOS
among them, the kernel revokes a controlling terminal when its
session leader exits -- a child of shell/1 on the pty of an epilog
window -- which detaches every descriptor on it from the device,
in this process as well. Prolog reconnects the descriptors of its
own streams, but our duplicates it cannot see, so reading the next
line failed with EIO for good and the window filled with "I/O error
in read on stream user_input".
[Aug 4 2026]
- BUILD: build
library(win_console) only with LIBEDIT_DEBUG It drives
a Windows console and reads its screen buffer back, which is of no
use outside a test of what swipl.exe draws there. It now shares the
option that gates libedit's refresh trace, LIBEDIT_TRACE renamed to
LIBEDIT_DEBUG for covering both.
- FIXED: Windows: an abandoned pipe read wrote into a dead stack frame
pipe_read_or_msg() returned with the overlapped ReadFile() still queued
on three paths: WM_QUIT, a Prolog exception from PL_handle_signals(),
and any other wait result. The kernel owns the OVERLAPPED and
the buffer being read into, and both are locals -- the OVERLAPPED
of pipe_read_or_msg(), the buffer of read_char(). When the next
character arrived it was written into a stack frame that had long gone,
and the event it signalled had been closed and possibly reused.
[Aug 3 2026]
- FIXED: build against a stock libedit again Swrite_libedit() takes the
input line off the screen with EL_ERASELINE, which is an addition of
our copy of libedit. The binding still builds against the one the
system provides -- SYSTEM_LIBEDIT, never on Windows -- and there the
constant does not exist, so it did not compile at all.
- FIXED: put the input line back after output on a console Having
taken the input line off the screen and let the output through,
Swrite_libedit() puts the line back once the output has ended its line.
It decided that from the last byte written, which a write handler
is in no position to read: it is handed the encoded text, and the
encoding of a Windows console stream is wchar. writeln/1 arrives
there as "hello\r\n" in UTF-16, whose last byte is the zero half of
the newline, so the line looked unfinished and stayed off the screen
-- the output overwrote the prompt and no new prompt appeared until
a key was pressed.
- DEBUG: trace where foreign output is put on the screen Swrite_libedit()
decides whether output from elsewhere may take the input line down,
and whether the line goes back up afterwards. Nothing said which way
it went, and the two are hard to tell apart from the screen alone:
a line that was never taken down and one that was never put back both
leave the output where the input should be.
- FIXED: keep a thread's output out of the line being edited on a console
Swrite_libedit() takes the input line off the screen, writes, and puts
the line back only when the write comes from a thread other than the
one waiting for a key. It reads ctx->reader to tell those apart, and
only
do_read() ever set it -- which is compiled on Unix alone. On a
Windows console nothing looked like it was reading, so a background
thread's output went straight to the caret, into the middle of the
line the user was editing, and stayed there until the next keystroke.
- ADDED:
library(win_console): drive a Windows console and read
it back Runs a program on a Windows console and reports what
is on that console, cell by cell, out of the screen buffer with
ReadConsoleOutputW(). It exists so that what swipl.exe draws on a
console can be checked against the console itself: every other way
we have of testing the line editor puts a terminal of our own on the
other end, which can only show that libedit does what we think it does.
[Aug 2 2026]
- ENHANCED: allow ANSI escape sequences in the prompt
set_prolog_flag(toplevel_prompt, '\e[1m?- \e[0m') now gives a bold
prompt that is measured correctly; before, the escape bytes counted as
columns and every redraw, wrap and cursor motion was off by that many.
[Jul 31 2026]
- FIXED: keep output from other threads out of the input line A write
that arrives while the line editor owns the screen went to wherever
the caret was -- into the middle of the line being edited -- and
stayed there until the next keystroke redrew the line below it.
Take the input line off the screen first, let the output through,
and paint the line back below it, at once rather than on the next key.
[Aug 1 2026]
- FIXED: Epilog on Windows: keep libedit's terminal size in step An
Epilog window raises no SIGWINCH: resizing it only updates the size
on the stream, and
el_init_handles() runs terminal_init() before
EL_GETSZFN and EL_CLIENTDATA are set, so libedit never learned the
window size at all -- it took whatever the console fallback produced,
which for a window that has no console is uninitialised. Editing an
input line that wraps then paints on the wrong rows.
[Jul 21 2026]
- ENHANCED: use the Epilog terminal font for libedit column widths
When libedit drives an Epilog (xpce) terminal (el_flags&EPILOG)
the visible width of a glyph is decided by the terminal font, not by
the static Unicode tables PL_wcwidth() reads. Symbol and emoji code
points that PL_wcwidth() reports as width 1 are routinely drawn two
cells wide by an emoji-presenting font, which made libedit's cursor
arithmetic drift against what Epilog paints.
Package pcre
[Jul 28 2026]
- ENHANCED: register the blob type when the library is loaded A blob
type is otherwise registered only when its first blob is created, so
blob_type_property/2 cannot report it and set_blob_type/2 cannot give
it a collection budget until the library has already been used, which
is backwards for a setting whose purpose is to bound accumulation.
[Jul 27 2026]
- CLEANUP: write the pattern using the %As of SfprintfX() Avoids creating
a term on a foreign frame to write the pattern as a quoted atom.
A blob
write() function may be called while the atom table is scanned,
e.g., by read_term/3 using the blob(resolve) option.
[Jul 26 2026]
- FIXED: write a regex blob as valid Prolog syntax The pattern was
written unquoted between slashes, so a pattern holding a bracket
or a quote produced output that could not be read back at all,
e.g. <regex>(0x55c1e0, /
a(b)c/). It is now written as a quoted atom,
which makes the blob acceptable to read_term/2,3 using blob(dead).
Package pldoc
[Aug 6 2026]
- ADDED: xpce_object_label/2: xpce arrow notation for a class member
Turns e.g.
xpce(point,get,x) into point<-x. Extracted from the PlDoc
hooks that already used this notation so apropos/1 can use it too.
- ADDED: man_page//2:
link_scheme(Scheme) option Writes the manual
references as Scheme:Object IRIs instead of links into the PlDoc
server, removing the references that do not resolve to a documentation
object. This allows help/1 to create clickable terminal output
without depending on the PlDoc HTTP handlers.
- ADDED: pldoc_href_object/2, man_object_uri/2 and man_uri_object/2 These
map a PlDoc HREF onto the documentation object it refers to and convert
such an object to and from a `man:` IRI. The latter is used to embed
manual hyperlinks into terminal output using OSC8 escape sequences.
Package semweb
[Jul 28 2026]
- ENHANCED: register the blob type when the library is loaded A blob
type is otherwise registered only when its first blob is created, so
blob_type_property/2 cannot report it and set_blob_type/2 cannot give
it a collection budget until the library has already been used, which
is backwards for a setting whose purpose is to bound accumulation.
Package ssl
[Jul 28 2026]
- ENHANCED: register the blob type when the library is loaded A blob
type is otherwise registered only when its first blob is created, so
blob_type_property/2 cannot report it and set_blob_type/2 cannot give
it a collection budget until the library has already been used, which
is backwards for a setting whose purpose is to bound accumulation.
Package swipy
[Jul 28 2026]
- ENHANCED: register the blob type when the library is loaded A blob
type is otherwise registered only when its first blob is created, so
blob_type_property/2 cannot report it and set_blob_type/2 cannot give
it a collection budget until the library has already been used, which
is backwards for a setting whose purpose is to bound accumulation.
[Jul 27 2026]
- FIXED: object references now read back using read_term/3
blob(resolve)
Python object references are written as <py>(Ref,Class) rather than
<py_Class>(Ref). The name between the brackets must be the name of
the blob type for the reader to find the object back, which is what
makes pasting an object reference into a toplevel goal work. This also
makes blob/2 report the same type for a live and a dead reference.
Package tipc
[Jul 28 2026]
- ENHANCED: register the blob type when the library is loaded A blob
type is otherwise registered only when its first blob is created, so
blob_type_property/2 cannot report it and set_blob_type/2 cannot give
it a collection budget until the library has already been used, which
is backwards for a setting whose purpose is to bound accumulation.
Package xpce
[Aug 7 2026]
- TEST: terminal: read the echo client's bytes in the C locale BSD
`cat -v', which is what MacOS has, escapes per character rather than
per byte: in a UTF-8 locale it passes the 0xC2 of a two-byte sequence
through as itself and escapes only the second byte, so a mouse report
encoded as UTF-8 (DEC private mode 1005) came back as `ÂM-^E' where
the test expected `M-BM-^E'. What these tests are about is the bytes
the terminal sent, so ask for the byte-wise reading.
- FIXED: MacOS: terminal died when a child of shell/1 left it A process
that makes the pty its controlling terminal -- a child of shell/1 or
process_create/3 -- is the session leader of a session that owns a
terminal, and on BSD systems, MacOS among them, the kernel revokes
such a terminal when its session leader exits. Every descriptor on
the client side is detached from the device, ours as well as those
the Prolog thread runs on, and the master sees end of file. We took
that for the client closing the connection and took the pty down for
good; Prolog, left without usable streams, died on the first message
it printed.
- FIXED: release a window from the window system when deleted
from its frame Closing one terminal of a split Epilog window and
moving the mouse printed "
ws_window_frame_position(...) failed":
the event dispatching still had the deleted window as the window
holding the pointer. ->uncreate is a no-op if the window is not on a
created frame, and frame ->delete cleared <-frame before sending it.
Uncreate the window while it is still a member. This also releases
its Cairo surface and texture.
- FIXED: epilog: focus the new terminal after splitting a window With
click-to-focus the new terminal only got the keys after clicking it.
- FIXED: make the keyboard focus follow the click rather than the
pointer Moving the pointer over a window made it the frame's input
window, so the caret and thus the keyboard focus followed the mouse.
Class frame now has a class variable focus_follows_mouse (default
@off): only a click in a window makes it the frame's ->keyboard_focus.
Setting it to @on restores the old behaviour.
- FIXED: send keyboard events to the window holding the input focus After
splitting an Epilog window the caret moved to the selected terminal,
but typing went to the original one. A frame without an explicit
->keyboard_focus routed keys to the first member window that has a
<-keyboard_focus, while the caret follows ->input_window, which is
set when the pointer enters a window. Make <-keyboard_focus consult
->input_window before scanning the members, so both agree.
[Aug 6 2026]
- ADDED: prolog_terminal <-thread Provides the Prolog thread that is
connected to an Epilog terminal. Used by library(help) to find out
whether the page a clicked hyperlink appears in is still being shown
by a pager.
- ADDED:
library(epilog): epilog:tty_link_hook/2 to hook clicking links,
This replaces tty_link_hook/1, which was present only one day.
Our aim is to make manual page links clickable and show them in the
same terminal. So, we need the terminal.
[Aug 5 2026]
- ADDED:
library(epilog): tty_link_hook/1 to hook clicking links.
- FIXED: terminal: hand the function keys to a client that owns it F5
and F6 are epilog's debugger keys, which meant `mc', `vim' and every
other full screen program could never be given them. They now go the
way ^C does: while a process group of another session owns the pty,
the function keys are that program's.
- FIXED: terminal: pass the function keys on to the client ->typed knew
the cursor keys and a handful of named ones and dropped everything
else, so F1..F12 never reached the program running on the terminal.
They are now sent as xterm spells them, as are Shift, Alt and Control
held with a function or cursor key.
- TEST: terminal: wait for the echo client to turn echo off The tests
that read back what the terminal sent its client start `stty -echo;
cat -v' and begin typing as soon as the shell owns the pty, which is
before its `stty' has run. A key pressed in that window came back
twice: once echoed by the line discipline and once from the client.
The client now announces itself after `stty', and start_echo_client/1
waits for that.
- FIXED: "Natural" scrolling direction We negated the amount when
flipped was indicated, but SDL3 already did that.
- FIXED: terminal: cls brought the oldest lines of the session back
Erasing the display with no saved lines starts the ring over at slot 0,
where the oldest lines still sit, and the redraw paints a whole window
from `window_start' without stopping at `last'. cls/0 gets there every
time: its ED 3 drops the scroll back, so the ED 2 that follows takes
this branch and the banner reappeared with the next prompt written
over its first characters. Emptying the buffer now empties its lines.
- FIXED: Windows: the caret went to the top of the screen after a client
The reset that ends a console session sent the normal screen back
first and reset the scrolling region after it. DECSTBM homes the
caret, so whatever the screen restored was undone a sequence later
and the next prompt appeared in the top left rather than where the
client found it. Come back to the normal screen last of all.
- FIXED: Windows: a console session left the terminal as its client had
it A real console owns the screen buffer the application draws on:
when the application goes away the buffer dies with it and the one
underneath comes back, which is why `less' leaves a Windows console
as it found it without sending anything. A pseudo console does
not do that for us, and once it is closed nobody is left to send
the sequences either, so the terminal now sends them to itself:
normal screen, caret, attributes, scrolling region, cursor keys,
mouse reports and bracketed paste.
- FIXED: terminal: a stray rmcup blanked the window for good Leaving an
alternate screen that was never entered erased the display and then
found nothing saved to restore, so the window stayed empty with nothing
that could bring it back. Entering it twice was as bad: the second
smcup saved the alternate screen over the normal one. Both come out
of a Windows pseudo console around a full screen application such as
`less'. Ignore either, as xterm does.
- TEST: terminal: the Windows console description gained capabilities
Mirror win_ncurses.c, which now answers the keys (arrows, Home, End),
the parameterised motion and insert, home and the bell. Under this
profile the suite therefore drives libedit with the escape sequences
the console reports rather than with the editor bindings it fell
back on.
- BUILD: keep the xpce copy of SWI-Stream.h in step It shadows the
core header through the same include guard, so the pseudo console
declarations added there must appear here as well.
- TEST: terminal: cover the 1005 (UTF-8) mouse encoding It only differs
from the default past the 96th column, where a coordinate stops
fitting in a byte, so the test widens the terminal to 120 columns
and clicks there.
- ADDED: terminal: mouse reporting (DEC private modes 9/1000/1002/1003)
An application that asks for the mouse now gets it, encoded as it asked
for with 1006 (SGR), 1015 (urxvt), 1005 (UTF-8) or the default three
bytes. This is what `emacs -nw', `less --mouse', vim, mc and tmux
read, and the wheel is a button (64 and 65) like any other to them.
As in xterm, shift is the user's way out: a shifted event is never
reported, so selecting, pasting and scrolling back keep working,
and mouse reports take the wheel before alternate scroll does.
- ADDED: terminal: alternate scroll (DEC private mode 1007) While the
alternate screen is up and no application asked for mouse reports,
the wheel is sent to the application as cursor keys, three lines to
the notch. That is what makes the wheel scroll `less', `man' and
other full screen applications in xterm and VTE, which have this on
by default; so do we. Shift keeps the wheel on this side.
- FIXED: terminal: wheel scrolled the scroll back on the alternate screen
The lines the alternate screen replaced belong to the normal screen:
a full screen application owns the window until it gives them back,
so scrolling them into view showed the text under the application.
The scroll bar offered a bubble for them as well; it now fills the bar.
[Aug 4 2026]
- FIXED: terminal tests: wait for the terminal to settle before using
it A terminal was handed to a unit as soon as its prompt appeared, but
the prompt is drawn while the rest of the banner is still on its way.
With the units in one process that went unnoticed; with several running
at once the rest could arrive after a test had painted the screen,
and terminal_screen:delete_lines found its first row overwritten by
the prompt.
- TEST: terminal: a process per unit, run in parallel and only when
asked The suite drives a terminal and reads the screen back, so
it spends its time waiting rather than computing: a minute against
the four backends, where the rest of the xpce tests together take
five seconds. It is off now unless XPCE_TERMINAL_TESTS is set,
and worth setting when touching the terminal, libedit or shell/1 --
several of the bugs in those were caught by nothing else.
- TEST: terminal: a process started by the Prolog thread runs on
the window shell/1 hands the child the terminal its caller runs on.
The new terminal_child_on_terminal unit checks that from the outside:
the child writes to the screen, reads what is typed at the window,
echoes it, reports its exit status, and leaves the terminal usable
afterwards. It says nothing about how any of that is arranged, so
it runs on both platforms, and every Windows attempt at this failed
in a way it would have caught.
- ADDED: terminal: let a process run on this terminal's pseudo console
->launch puts a process on the terminal through a pseudo console
over the window's own pipes. The Prolog thread running on the
terminal needs the same thing for shell/1, but cannot use ->launch: it
creates the process itself and has to wait for it. The client streams
therefore answer SIO_[GET|REL]WINPSEUDOCONSOLE, and Swinpseudoconsole()
in the core asks them. See System() in src/pl-nt.c.
[Aug 3 2026]
- DOC: terminal_image: <-foreground_process and ->interrupt
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VnmviTFXmNbwtckwLhxHvP
- FIXED: terminal: <-foreground_process on a terminal that is gone
->unlink leaves ti->data at NULL, so the getter must check before it
looks at the pty.
- TEST: terminal: control keys reach a process running in the terminal
Adds a terminal_control_keys unit: ^C interrupts a child, ^X reaches
it as input, and a child that turned signal generation off gets ^C as
input too. Presses the keys with the new term_press/2, which goes
through ->typed and thus past the key bindings; term_send/2 puts
bytes on the pty and would not see them.
- FIXED: terminal: pass control characters to a process running in it
The window handled ^C, ^X, ^V and ^Y itself, so a program started
with shell/1 or process_create/3 never saw them. While a process
group of another session owns the pty, control characters now go
to the client and the tty line discipline decides what they mean:
it raises SIGINT for a program that wants signals and delivers the
character to one that does not. Ctrl+Shift and Meta bindings are
unaffected, and with no such process the window keeps its own bindings.
- FIXED: terminal: a CSI private marker is not an SGR parameter `CSI >
4 ; m' (XTMODKEYS, which vim sends on every start) ran as SGR 4 and
underlined everything after it. Keep the marker that opened the
sequence and let `m', `c' and `S' see it; `CSI > c' now gets the
secondary device attributes rather than the primary ones.
- ENHANCED: epilog: claim xterm-256color The terminal implements the
string capabilities of that description and the colours it adds:
ANSI 16, the 256 colour cube and 24 bit RGB. As `xterm' it told
applications it had eight colours, so nothing used the palette it
can actually draw.
- FIXED: terminal: do not report unknown sequences on the screen A
terminal ignores what it does not know quietly; the stream belongs to
the application, and writing our own notes into it corrupts the screen
it is painting. The reports are still there under
debugpce(term).
Also act on DECSCNM (`CSI ?5h'), which is terminfo's `flash'.
- FIXED: terminal: erased rows stayed on the screen Erasing the display,
or from the caret down, moved the end of the buffer up but left the
rows themselves in the ring. Both the painter and <-row walk the ring,
so the erased text stayed visible until something wrote over it.
- ADDED: terminal: autowrap, REP and the reset sequences DECAWM (`CSI
?7h/l') lets a program hold the caret in the last column rather than
wrap, REP (`CSI Ps b') repeats the last character written, and DECSTR
(`CSI !p') and RIS (`ESC c') put the terminal back to its defaults.
DECSTR is part of terminfo's is2, so it runs whenever a full screen
application starts.
- ADDED: terminal: tab stops (ESC H, CSI Ps g, CSI Ps I and CSI Ps Z)
A tab used to run to the next multiple of eight cells, which is not
the same as the next multiple of eight columns once a line holds wide
or combining characters. Keep the stops in a map that HTS and TBC
edit and move over them in columns.
- ADDED: terminal: scroll up and down (CSI Ps S and CSI Ps T) Terminfo's
indn/rin. Scrolling the whole window up pushes its top lines into
the scroll back as a line feed does; inside a scrolling region,
or on the alternate screen, they are dropped.
- ADDED: terminal: ESC 7, ESC 8, ESC D and ESC E DECSC/DECRC are
what terminfo's sc/rc use; they save and restore the attributes and
character sets along with the position, where `CSI s' and `CSI u'
save the position alone. IND and NEL used to end up in the "unknown
escape" branch.
- FIXED: terminal: erase sequences (EL, ED and ECH) `CSI Ps K' ignored
its parameter, so erasing to the start of the line erased to the end
of it instead. Implement EL 1 and 2, ED 1 (erase above the caret)
and ECH (`CSI Ps X'), which blanks columns in place.
- FIXED: terminal: honour the scrolling region (CSI Ps ; Ps r, DECSTBM)
Deleting a line in emacs pulled up everything below it, including the
mode line and the echo area: emacs bounds the delete with a scrolling
region, which the terminal parsed and ignored. IL, DL, the reverse
index and the line feed at the bottom of the region now stay inside it.
The region covers the whole window by default and a resize resets it.
- ENHANCED: terminal: insert and delete lines (CSI Ps L and CSI Ps M)
Implement the ANSI IL and DL sequences. Both move the caret to the
first column and work on the region from the caret to the bottom of
the window; the terminal has no scrolling region of its own (CSI r).
The reverse index (ESC M) and the scroll of a saved screen are the
same operation and now share its implementation.
- TEST: terminal: a program clearing the screen clears it Covers what
the Windows console emulation quietly ate: ESC [ 2 J written by the
program, rather than by the line editor, which asks the console to
act on its own sequences and so was unaffected.
- FIXED: terminal: cursor motion stops at the edge of the line Moving
the cursor right past the last column wrapped it to the next line,
and moving left from column 0 took it to the line above. Only writing
a character wraps: ECMA-48 has CUF not pass the last column, and a
terminal without `bw' does not take a backspace in column 0 to the
line above.
- FIXED: terminal: ignore NUL and DEL rather than printing them Any
character the terminal had no meaning for was written to the screen,
NUL among them. A terminal description that asks for padding -- vt100
puts $<3> on el and $<50> on clear -- has tputs send that padding
as NUL bytes, and at 38400 baud $<3> is twelve of them, so every
clear-to-end- of-line walked the caret twelve columns to the right.
Everything drawn after it landed in the wrong place.
- FIXED: terminal: the suite's own hook made the console thread spin
A terminal's Prolog thread reports one io_error when the terminal is
taken away from under it, and then exits: read_query_line/2 rethrows
and the top level answers with
thread_exit(io_error). Silencing that
message with a message_hook/3 that succeeds stops it doing so, and the
thread reads, fails and reports for as long as the process lives --
90,000 times and a whole core in the three seconds after a terminal
was destroyed.
- TEST: terminal: read the screen back from a real Windows console
Adds the `console' backend, which puts swipl.exe on a Windows console
through
library(win_console) and reads conhost's own screen buffer.
It is the only backend whose screen is not one of ours, so it is
the only one that shows what a user would see rather than what our
terminal makes of what libedit wrote.
- TEST: terminal: run the suite as more terminal descriptions see it Two
more runs against a child. TERM=linux has ich1, which sends libedit
down its insert-mode path rather than its insert-character one,
and so covers the terminal's IRM support.
- FIXED: terminal: implement insert mode (IRM) The terminal acted on CSI
? <n> h and CSI ? <n> l, the DEC private modes, but silently dropped
the plain CSI <n> h and CSI <n> l. Mode 4 is IRM, so a client that
opened room for a character with \e[4h instead of \e[<n>@ had that
character overwrite the line rather than push it right, losing one
character per row.
- TEST: terminal: run the suite on a terminal without xenl Adds a run
against a child on TERM=ansi. That description has am but not xenl,
the shape win_ncurses.c reports on Windows, so it is where the Windows
console class of redisplay bug can be caught without a Windows console.
- TEST: terminal: run the suite from ctest Registers the terminal suite
twice: against the epilog terminal, and against a child process on
that terminal's pty. Until now it was in no CMakeLists at all and
had to be started by hand.
- TEST: terminal: run the suite against a child process on the pty Adds
the
child(Profile) backend: a child `swipl' started with shell/1
from the epilog thread, so it runs on the terminal's pty with TERM
taken from Profile. That is the only way to reach libedit's ordinary
termcap paths -- the epilog backend always runs it with the EPILOG
flag, so everything a non-epilog terminal does was untested.
- TEST: terminal: drive the suite through a backend layer The suite only
ever needs three primitives -- send a keystroke, read the cursor, read
a row -- so it can run against more than one terminal. Route every
test through term_send/2, term_cursor/3 and term_row/3 and select
the terminal with test_terminal/1, so that the same assertions can
be aimed at a line editor other than the in-process epilog one.
- ADDED: terminal_image <-columns and <-rows Report the size of
the terminal in character units. The pixel size only yields this
after dividing by the font's cell width and subtracting the margin,
i.e. after duplicating
rlc_resize_pixel_units() outside the terminal.
[Aug 2 2026]
- TEST: xpce: cover the term description of a Prolog
method's object answer Co-Authored-By: Claude Opus 5
(1M context) <noreply@anthropic.com> Claude-Session:
https://claude.ai/code/session_01LmQHEtHoVQ2NH2Zzusvepa
- FIXED: xpce: unify the object a Prolog method returns with a term
description
get(Object, Selector, size(W,H)) decomposes the returned
object when the method is implemented in XPCE, but failed when it
is implemented in Prolog: that path unified the answer term as-is
and only fell back to unifyObject() when the answer was @Reference.
An anonymous reference is a blob rather than @Integer since the
object references were turned into blobs, so the fallback was no
longer reached and the thread monitor silently failed to open:
<-axis_length returns a size that ->thread_diagram reads as size(H,V).
[Aug 1 2026]
- TEST: terminal: clicking in the input line Synthesises clicks and
drags: the caret follows a click by the distance clicked, on a wrapped
row as well, a click outside the input line leaves it alone, and a
drag selects without moving it.
- ADDED: click in the input line to move the caret there The line
being edited belongs to the client, so the terminal cannot place the
caret itself. It can ask, though, and the request every line editor
understands is cursor-left and cursor-right: count the grapheme
clusters between the caret and the click and send that many.
- TEST: terminal: resizing, rewrapping and the caret Covers the Epilog
terminal's behaviour across a resize: editing a wrapped input line,
shrinking to a width the input fills exactly, a wrapped row whose
last column is a space, a key pressed before the resize has reached
libedit, walking the caret over a wrapped line and widening afterwards,
and a selection made before the resize.
[Jul 31 2026]
- TEST: terminal: output from another thread while typing Asserts that
the text appears on its own row and the input line is painted below
it, without waiting for a keystroke.
[Aug 1 2026]
- FIXED: moving the caret down no longer breaks a wrapped line
rlc_caret_down() cleared softreturn on every line it landed on, so a
client walking down its input with newlines -- libedit going to the
end of a wrapped line with ^E, or stepping from one display row to the
next -- turned each continuation into a hard line break. Rewrapping on
the next resize then reflowed the pieces on their own, and the repaint
that followed left parts of the old layout on the screen.
- FIXED: rewrapping on resize carries the caret and the selection
rlc_resize() rebuilds the ring of lines, but the caret and the
selection are indices into it, so afterwards they named different
text: the caret no longer marked the end of the input, and libedit --
which rewinds relative to it to repaint -- started a row off and left
a copy of a row on the screen. A selection made before the resize
covered something else.
- FIXED: selections that span the ring's wrap-around The lines live in
a ring, but two places ordered them by their bare indices: once it
has wrapped, a line further down the screen has the smaller index.
rlc_read_from_window() then took a region covering both ends of the
buffer for invalid and read nothing, and SelLT() -- which decides
whether a drag extends the selection forwards or backwards -- took
a drag downwards for a drag upwards. Selecting the whole contents
of a scrolled terminal, by select-all or by dragging from the first
line to the last, therefore stopped at the wrap.
- TEST: xpce: cover object references and weak hash tables
tests/test_object_reference.pl covers the representation, identity,
conversion, lifetime and the released state. The lifetime tests
assert what this work is for: 1000 objects created and dropped leave
the answer stack exactly as they found it, an object reachable only
from a Prolog variable stays alive across a garbage collection,
and one that is not reachable at all does not. A table created with
refer=none leaves its entries collectable while a normal one keeps
them alive, and lookup works either way.
- DOC: xpce: document handle-based object references Rewrites the places
that described an anonymous reference as @Integer and updates the
examples that printed one. The User's Guide chapters affected are
Getting started, the Prolog interface, memory management, debugging
and the glossary, plus the xpce-client manual page.
- FIXED: xpce: an address does not name an object at the XPCE level
either @pce<-object_from_reference still took an integer and turned
it into a pointer, guarded only by a range check and the object
magic, so it named whatever had since been created at that address.
object<-convert reached the same code by parsing text of the form
"@35435623". Both now accept names only.
- FIXED: xpce: the inspector and instance browser monitor objects without
owning them Both tools watch objects they must not keep alive, so both
stored the reference rather than the object: the inspector in its three
associations, the instance browser in the dict_item's object slot.
That worked only because nothing was ever reclaimed; it also meant
the reference could go stale, and <-reference was broken outright
for anonymous objects, since get(_, _, @Var) has no anonymous form.
- ENHANCED: xpce: an object reference is its own xref source identifier
library(pce_prolog_xref) reduced a buffer to the integer inside its @/1
reference because "the cross-referencer runs faster if the reference is
an indexable term". An anonymous reference is now an opaque handle,
which indexes exactly as an atom does, so the detour buys nothing and
costs the guarantee: the integer can go stale while the handle cannot.
[Jul 30 2026]
- FIXED: demo: read the text of a text_item selection, not its reference
text_item <-selection answers an XPCE string object, and item_string/2
fell through to formatting it with ~w before parsing the result as a
Prolog list. That wrote the object reference rather than the text,
so Generate reported
[Aug 1 2026]
- FIXED: xpce: atomic/1 no longer distinguishes a value from an object
An anonymous object reference used to be @Integer, a compound term,
so code could use atomic/1 to mean "a name or number rather than
an object". A blob is atomic, so those tests now accept objects.
- FIXED: xpce: adjust Prolog code that took object references
apart An anonymous reference is a blob, so Obj = @Ref no longer
decomposes one. Adds object_reference/2, object_from_reference/2
and is_object_reference/1 over the XPCE <-object_reference and
@pce<-object_from_reference methods, and uses them everywhere the
reference was reached through the @/1 term.
- ENHANCED: xpce: print an anonymous reference as the host writes
it
pp(), and with it the %O of the string formatter, still wrote an
anonymous object as @<integer>. That form no longer exists on the host
side and can no longer be read back, so it was misleading wherever it
surfaced: the visual hierarchy's status line, the inspector's value
labels and clipboard, show_slots/1 and every XPCE error message.
- ADDED: xpce: represent object references as <pce>(Addr,Class) blobs
An anonymous reference is now the blob <pce>(0xADDR,Class) rather
than the term @Integer, where the integer was the object's address.
Prolog holding a reference now means XPCE holds a reference, so the
answer stack no longer has to pin everything that escapes to Prolog:
rewindAnswerStack() is passed NIL instead of the result at both host
entry points. Objects reachable only from Prolog are reclaimed by the
atom garbage collector. Creating 10,000 points and dropping them used
to leave 10,000 cells on the answer stack forever; it now leaves none.
Named references remain @Name.
- ADDED: xpce: hash_table ->initialise takes the reference mode A hash
table already knows whether its keys and values create references
(<-refer), and the kernel uses non-referring tables for the object
registries behind <-attribute, the method caches and @display's placed
table. Until now that could only be set from C, so Prolog could not
build a table that monitors objects without keeping them alive.
- ADDED: xpce: host API to hold object references on behalf of the
host A host reference is an ordinary object reference held for the
host language. It makes the object non-virgin, so it survives
the rewind of the answer stack, and it makes
noRefsObj() false,
so ->free() only marks the object F_FREED and defers the unalloc:
a host reference can never dangle.
- BUILD: xpce: run the tests against SDL's dummy video driver XPCE
needs a display, so the bom, selection and image test sets were
skipped wherever the build host has no window server, which is the
normal case for CI. SDL's dummy driver provides a headless display,
and all eighteen test sets pass against it.
[Jul 28 2026]
- FIXED: xpce: clear F_ANSWER for every cell dropped from the answer
stack _
rewindAnswerStack() cleared F_ANSWER only on the branch that
also frees the object. An object that acquired a reference while it
was on the answer stack survives the rewind, but its cell is dropped,
leaving the flag set with no cell to match. isVirginObj() is then
false forever, so freeableObj() can never reclaim the object once
the last reference disappears.
- TEST: xpce: determine display availability by asking XPCE
display_present/0 tested $DISPLAY, which is an X11 leftover: since
XPCE moved to SDL that variable says nothing about whether a display
can actually be opened. XQuartz sets $DISPLAY on macOS sessions that
have no window server access, so the bom, selection and image test
sets ran anyway and crashed the suite, masking the ten test sets that
follow them.
[Jul 29 2026]
- TEST: terminal: deleting a wide character in front of NFD text
Regression test for the Windows-only bug where backspacing a two-column
character left of NFD content also removed the cluster behind it.
libedit sent the two columns as two CSI P sequences; we delete whole
grapheme clusters, so the second one took a cluster of its own.
- FIXED: Epilog terminal <-cwidth: do not measure non-characters
Surrogates and Unicode non-characters have no glyph, so measuring
one returns the width of the font's "missing glyph" box -- typically
two cells. libedit on Windows, where wchar_t is 16 bits, asks about
both halves of a surrogate pair and about its MB_FILL_CHAR sentinel
(0xFFFF), and then counted an emoji as four columns instead of two.
Fail instead, which leaves the caller with PL_wcwidth().
- TEST: terminal: editing at the right margin stays on the input
row Regression test for the Windows-only Epilog console bug where
replacing the last character of a line that fills the row exactly
drew the new character on the row above the line being edited,
after which the display walked up the screen.
[Jul 24 2026]
- ENHANCED: Navigator: browse all files and filter the tree on file name
The Navigator can now show all files of a directory in addition to the
loaded files and the Prolog source files, switch the content mode of
the whole tree at once and incrementally filter the tree on a file-name
pattern. Browsing is scoped to the seed directories (the loaded files,
the library search path and the working directory) and the expanded
directories are preserved across mode switches and filtering.
[Jul 19 2026]
- FIXED: Navigator: include non-terminals in the module export list
The cross referencer only matched Name/Arity in the module header,
so exported non-terminals (Name//Arity) were missing when expanding
the module node. Their definitions are found as grammar rules.
[Jul 21 2026]
- ADDED: terminal_image <-cwidth: columns occupied by a code point
Expose
terminal_char_cells(), the classification the terminal renderer
itself uses, as terminal_image <-cwidth(Code) -> Int. Clients that
must predict our layout now have a single point of truth instead of
a second copy that can drift.
Package yaml
[Jul 28 2026]
- ENHANCED: register the blob type when the library is loaded A blob
type is otherwise registered only when its first blob is created, so
blob_type_property/2 cannot report it and set_blob_type/2 cannot give
it a collection budget until the library has already been used, which
is backwards for a setting whose purpose is to bound accumulation.