Skip to content

hash_consing 0.2.0 - #2

Merged
Atry merged 2 commits into
mainfrom
release-0.2.0
Oct 1, 2026
Merged

Atry merged 2 commits into
mainfrom
release-0.2.0

Conversation

@Atry

@Atry Atry commented Sep 26, 2026 •

Copy link
Copy Markdown
Owner

Release 0.2.0. Merging this pull request publishes it: CI tags V0.2.0 and registers the version with the SWI-Prolog pack server.

Templates are patterns

What is interned is now chosen by patterns, not by constructors.

  • A constructor may have several templates. They are tried in the order of the list, the first that matches a term decides its shape, and a term that none matches is not interned: it stays a plain compound whose arguments are represented in turn.
  • A nested template requires its constructor; * records the root constructor of any argument; ; is an ordinary constructor, no longer an alternative.
  • One list of templates per constructor and process: declaring a constructor again with another list raises, and so does a template that an earlier one always takes first.
  • The rewrite classifies each occurrence while a file loads: it must be interned (an Id pattern and intern/2, the shape written in when the pattern decides it), it must not be (the plain layer, written in place), or it is undetermined (a fresh variable and represented/2). A clause is never split, so every clause keeps the meaning it has without interning.
  • New predicates: declared/1 declares templates at run time, is_id/1 tells an Id from any other value, and represented/2 relates a layer to its representation.
  • id_parts/4 recognises an Id by looking up the Id functors this process made instead of scanning the name's prefix; alternating the old and the new library three times each on the same benchmark, a call on a plain compound or an Id took about a tenth less.
  • upserted/2 inserts under sig_atomic/1, so that a time limit expiring between the insertion and the write-back of the handle no longer leaves the key pending.

Interrupted insertions and threads

  • An inference limit is no signal, so sig_atomic/1 does not hold it off: a limit falling between the insertion and the write-back still left the key pending, and the next lookup raised type_error(address, pending). settled_handle/4 now repairs such a key at its next lookup.
  • Threads may share the store: a thread that sets the library's two global variables to the loading thread's tries declares, interns and looks up alongside the others. Three check-then-write steps raced before, each shown by a probe with eight threads:
    • a declaration: duplicate registrations, and is_id/1 answered twice;
    • an insertion: the second trie_insert/4 raised permission_error(modify, trie_key, Term);
    • a new spelling of a shape: the second trie_insert/3 failed, and so did its interning.
  • Each of these steps now takes a mutex and looks again before it writes. A lookup that finds its key takes no lock. Interning 200000 new terms in one thread took about 19 percent longer after the change, measured under a heavy load; lookups did not change.

Incompatible changes

The README lists them for 0.1.0 users: the meaning of a nested template; ;; a term no template matches is no longer interned and intern/2 fails on it; intern/2 raises existence_error(templates, Name/Arity) on a constructor without templates instead of declaring it on first use; a rewritten body goal raises an instantiation error when an occurrence whose interning is undetermined is still undecided when the goal is called; * spells the root of any compound or atom instead of -.

Tests

The shell-driven baseline diff is replaced by plunit, test/hash_consing.plt, 65 tests in 10 units, with the rewritten clauses of each fixture compared to test/hash_consing_snapshots/. A warning or an error printed during the run fails it. Run from the root of the pack:

swipl -p library=prolog --stack-limit=32m --table-space=32m -g run_tests -t halt test/hash_consing.plt

Locally and on CI all 65 pass. The unit interrupted_intern interrupts interning by every inference limit from 1 to 300 and by millisecond time limits; without settled_handle/4 it fails at the limit 25. The unit concurrency runs eight threads at once; before the mutexes, each of its three tests failed in 5 of 5 runs. The 30 queries of the old test keep their outcomes, and of the old fixtures only spines renders differently, where a single-layer application its nested template does not match is now a plain compound.

🤖 Generated with Claude Code

What is interned is now chosen by patterns: a constructor may have several
templates, tried in order, the first that matches a term deciding its
shape, and a term that none matches stays a plain compound. A nested
template requires its constructor, and `;` is no longer an alternative. New
predicates declared/1, is_id/1 and represented/2; intern/2 raises
existence_error on a constructor without templates. The README lists the
changes that break 0.1.0 users.

The tests moved from a shell-driven baseline diff to plunit:
test/hash_consing.plt, 60 tests, with snapshots in
test/hash_consing_snapshots/. upserted/2 now inserts under sig_atomic/1, so
that a time limit expiring between the insertion and the write-back of the
handle no longer leaves the key pending.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…g, and make declarations, insertions and spellings safe across threads
@Atry
Atry merged commit d6b6232 into main Oct 1, 2026
3 checks passed
@Atry
Atry deleted the release-0.2.0 branch October 1, 2026 01:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant