# Ann: SWI-Prolog 10.1.13

**URL:** <https://swi-prolog.discourse.group/t/ann-swi-prolog-10-1-13/9781>\
**Category:** Releases\
**Created:** [August 7, 2026, 12:34pm UTC](https://swi-prolog.discourse.group/t/ann-swi-prolog-10-1-13/9781 "2026-08-07T12:34:00Z")\
**Posts on this page:** 3\
**Page:** 1

<div class="post-metadata">

**Author:** ![jan](https://yyz2.discourse-cdn.com/free1/user_avatar/swi-prolog.discourse.group/jan/32/4_2.png) [@jan](https://swi-prolog.discourse.group/u/jan)\
**Post date:** [August 7, 2026, 12:34pm UTC](https://swi-prolog.discourse.group/t/ann-swi-prolog-10-1-13/9781/1 "2026-08-07T12:34:00Z")

</div>

Dear SWI-Prolog user,

SWI-Prolog 10.1.13 is ready for download. This comes with quite a  
few enhancements. Highlights:

- `2^2147483648` and higher can now be evaluated on Windows.

- read/1 and write/1 and friends have been converted from recursive C  
functions to iterative state engines. This allows for reading and  
writing much deeper nested terms and provides a consistent  
`resource_error(memory)` if there is insufficient memory to save the  
data requires to deal with nested terms. Before, some systems would  
generate `resource_error(c_stack)` (POSIX) while others (Windows, WASM)  
would crash uncontrolled.

- _Blobs_, representing atom and _foreign data_ have new support in  
read\_term/2. By default, they (still) raise a syntax error. Log  
data including blobs can use the option `blob(dead)` to read blobs  
as a blob, but disconnected from the original foreign data. The  
toplevel uses `blob(resolve)`, which scans the atom table for an  
existing blob that writes equivalent as the one we are trying to  
read. Note that this is slightly unsafe as the blob may have  
been subject to the _ABA problem_: the original blob was deleted  
but a new blob of the same type was allocated at the same address.

- AGC (Atom Garbage Collection) got a notion of the “weight” of an  
atom for triggering a collection. Weights can be defined by type  
and garbage collection can be triggered by type (though all types  
are collected regardless of the type that triggered AGC). The  
weight of normal atoms it their memory usage in bytes.

- Column tracking now deals with terminal escape sequences. This  
simplifies `library(ansi_term)` and allows for control sequences  
in format/3 while properly dealing with alignment. Note that this  
is consistent as handling double-width Unicode characters and  
Unicode combining marks was already implemented and the line  
positions is thus measured in columns rather than characters.  
ansi\_format/3 now also supports `href(URL)` to embed hyperlinks.

- Related, the prompt may now contain terminal escape sequences to  
achieve coloured, bold, etc. prompts.

- XPCE object references used `@Integer`, where _Integer_ was a slightly  
garbled address. Now XPCE uses _blobs_ of the form  
`<pce>(Address,Class)`. This makes XPCE’s memory management safe  
and relieves the user for explicitly protecting objects that would  
otherwise be garbage collected unintended.

- The swipl-win (Epilog) terminal and Libedit commandline editor have  
received many updates and a lot of new tests:

- Flipped (Apple _natural_) scrolling now works correctly.

- Windows console version (`swipl.exe`) now uses Windows decoding of  
terminal sequences if supported. Otherwise it falls back to the  
limited built-in decoding (basically only bold and the standard  
16 entries color palette).

These are a lot of changes and quite likely there will be some regression ☹  
Please report!

```
Enjoy --- Jan

```

# SWI-Prolog Changelog since version 10.1.12

- 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”.

- 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.

- 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.

- 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.

- 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:

- 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.

- FIXED: is/2: `2^2147483648` and larger on Windows. Completes  
8733c1a4f8efa2733e341641e64dfda8c0656205

- ADDED: cls/0: CLear Screen.

- FIXED: `2^2147483648` and larger on Windows.

- 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](mailto:noreply@anthropic.com) Claude-Session:  
[https://claude.ai/code/session\_01LmQHEtHoVQ2NH2Zzusvepa](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.

- 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.

- FIXED: WASM: read a JavaScript object reference back using blob(resolve)  
JavaScript objects are now written as (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](mailto:noreply@anthropic.com) Claude-Session:  
[https://claude.ai/code/session\_017dv8qG8JrSfiHguZaFbLAW](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.

- 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\_\_\*() functions  
Co-Authored-By: Claude Opus 5 [noreply@anthropic.com](mailto:noreply@anthropic.com) Claude-Session:  
[https://claude.ai/code/session\_017dv8qG8JrSfiHguZaFbLAW](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\_()  
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 (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 (freed) and is ordered by  
handle, and blob\_released/1 tests for the state.

 - ENHANCED: Toplevel accepts blobs written as (…) 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 (…) blob syntax  
Non-text blobs write as (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  
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 `#define`s 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.

 - 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.

 - 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

- 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

- 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.

- 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.

- 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

- TEST: a released blob writes as (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

- 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”.

- 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.

- 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.

- 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.

- 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.

- 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.

- 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

- 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.

- 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.

- 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. (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

- 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

- 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

- 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

- 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.

- FIXED: object references now read back using read\_term/3 blob(resolve)  
Python object references are written as (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

- 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

- 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.

- 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 epilog: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.

- ADDED: library(epilog): 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.

- 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.

- DOC: terminal\_image: \<-foreground\_process and -\>interrupt  
Co-Authored-By: Claude Opus 5 (1M context) [noreply@anthropic.com](mailto:noreply@anthropic.com)  
Claude-Session: [https://claude.ai/code/session\_01VnmviTFXmNbwtckwLhxHvP](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  
? h and CSI ? l, the DEC private modes, but silently dropped  
the plain CSI h and CSI l. Mode 4 is IRM, so a client that  
opened room for a character with \e[4h instead of \e[@ 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.

- TEST: xpce: cover the term description of a Prolog  
method’s object answer Co-Authored-By: Claude Opus 5  
(1M context) [noreply@anthropic.com](mailto:noreply@anthropic.com) Claude-Session:  
[https://claude.ai/code/session\_01LmQHEtHoVQ2NH2Zzusvepa](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).

- 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.

- 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.

- 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.

- 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

- 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 @. 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 (Addr,Class) blobs  
An anonymous reference is now the blob (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.

- 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.

- 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.

- 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.

- 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.

- 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

- 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.

---

<div class="post-metadata">

**Author:** ![kuniaki.mukai](https://avatars.discourse-cdn.com/v4/letter/k/c67d28/32.png) [@kuniaki.mukai](https://swi-prolog.discourse.group/u/kuniaki.mukai)\
**Post date:** [August 11, 2026, 1:24pm UTC](https://swi-prolog.discourse.group/t/ann-swi-prolog-10-1-13/9781/2 "2026-08-11T13:24:14Z")

</div>

Build failure in `packages/xpce/src/txt/terminal.c` on macOS (missing prerequisite headers / macro collision with `send`)

_(Note: Diagnostic and fix drafted with AI assistance.)_

**Environment:**

- macOS (Apple Silicon, ARM64)

- macOS SDK (Xcode toolchain)

- SWI-Prolog git `master` (`packages/xpce`)

**Problem:** Compiling `packages/xpce/src/txt/terminal.c` fails with:

1. Unknown type errors (`u_int`, `u_short`, `u_long`, `rusage_info_t`) because `<libproc.h>` is included without prerequisite system includes (`<sys/types.h>`, `<sys/resource.h>`).

2. Conflicting types error for `send()` / `sendPCE`: `<libproc.h>` pulls in `<sys/socket.h>` after `h/kernel.h` defines `#define send sendPCE`, which rewrites the standard BSD socket `send()` declaration into `sendPCE(...)`.

**Proposed Fix:** Include the prerequisite system headers at the top of `packages/xpce/src/txt/terminal.c` before `#include <h/kernel.h>`:

```prolog
#include <sys/types.h>
#include <sys/resource.h>
#include <sys/socket.h>
#ifdef HAVE_LIBPROC_H
#include <libproc.h>
#endif

```

---

<div class="post-metadata">

**Author:** ![jan](https://yyz2.discourse-cdn.com/free1/user_avatar/swi-prolog.discourse.group/jan/32/4_2.png) [@jan](https://swi-prolog.discourse.group/u/jan)\
**Post date:** [August 11, 2026, 2:13pm UTC](https://swi-prolog.discourse.group/t/ann-swi-prolog-10-1-13/9781/3 "2026-08-11T14:13:37Z")

</div>

> [@kuniaki.mukai](#):
>
> Build failure in `packages/xpce/src/txt/terminal.c` on macOS (missing prerequisite headers / macro collision with `send`)

Thanks. Claude created a bit simpler patch, but the idea is the same. Works again.
