1:- module(hash_consing,
    2          [ intern/2,               % ?Term, ?Id
    3            externalized/2,         % +TermWithIds, -External
    4            internalized/3,         % +Constructors, +External, -TermWithIds
    5            rewritten/1             % +Constructors
    6          ]).

hash_consing: hash-consed terms whose Ids record a configurable shape, and a term rewrite that makes a program use them

A GENERIC LIBRARY: it knows no constructor of any calculus and keeps no configuration but the templates files declare.

HOW TO USE IT. A file is written as the program that does not intern, loads this library and names the constructors to intern, each by a template:

:- module(steps, [step/2, built/2]).
:- use_module(library(hash_consing), []).
:- hash_consing:rewritten([apply(*, _), lambda(_), variable(_)]).

step(apply(lambda(Body), Argument), beta(Body, Argument)).

built(Function, Applied) :-
    Applied = apply(Function, variable(0)).

Files that hand such terms to each other declare the same templates, most simply by including one file that holds the directive. Code that did not go through the rewrite crosses the boundary explicitly:

?- hash_consing:internalized([apply(*, _), lambda(_), variable(_)],
                             apply(lambda(variable(0)), variable(1)), Id),
   steps:step(Id, Result),
   hash_consing:externalized(Result, External).
Id = '__hash_consed_apply/2'('lambda/1', <handle>),
External = beta(variable(0), variable(1)).

With the list [] the file loads unchanged, which is the program without interning. The test test/hash_consing_rewrite.sh prints what the rewrite makes of its fixtures, `test/hash_consing_fixtures/`, and is the place to read exact rewritten clauses.

`intern(?Term, ?Id)` IS ONE RELATION WITH TWO DIRECTIONS (user, 2026-09-17: a binary predicate that upserts forwards and looks up backwards). Term is a ground term whose rewritten subterms are Ids already. Called with Id bound it looks the term up (and with Term bound as well it is a check that inserts nothing); called with Id unbound, or bound to an Id pattern whose Handle is unbound, it inserts Term unless it is there and answers its Id; a value that is no Id matches nothing and the call fails, as a pattern that does not match fails without interning (user ruling, 2026-09-17: an uninterned instance at an Id position fails to unify rather than raising). The store is canonical (the same term, the same Id), idempotent and monotone (it only grows and backtracking does not shrink it), so an answer once given is never contradicted. An Id is abstract: only ==, intern/2 and the two boundary predicates look at it.

AN ID IS '__hash_consed_Name/Arity'(Handle) OR '__hash_consed_Name/Arity'(Shape, Handle). Name/Arity is the root constructor of the term and Handle the node handle of one trie, the store. The root is in the functor name so that the rewrite turns a head pattern apply(F, X) into an Id pattern that first argument indexing tells apart by constructor, and a value of another constructor, or no Id at all, fails at head unification without a lookup (probe, 2026-09-17: with one functor for every Id, 156 ms against 0.9 ms for 1000 calls over 2000 clauses). The prefix follows SWI-Prolog's convention for names a library synthesises (library(apply_macros)'s '__aux_maplist/N_...'); a leading $ is reserved for the system.

THE SHAPE is there for clauses told apart by an INNER constructor,

step(apply(lambda(Body), Argument)) :- ...
step(apply(closure_a(Capture), Argument)) :- ...

whose heads would otherwise be one Id pattern, every candidate clause paying a lookup before it fails. Shape is ONE ATOM that spells the constructors found below the root, as far as the constructor's template says (user, 2026-09-20: the Id keeps one layer, the shape may have several; the layers are joined into one atom, not kept as several shapes; how deep is configurable). The heads above become step('__hash_consed_apply/2'('lambda/1', Handle)) and step('__hash_consed_apply/2'('closure_a/1', Handle)), and the deep index SWI-Prolog builds on the first argument of the first argument tells them apart without a lookup (benchmark, 2026-09-20: 1000 calls over 2000 such clauses, 621 ms without the shape and 3 ms with it; upserts 46 percent slower; the store no larger).

THE ID STILL HAS ONE LAYER. The shape is not in the functor name, because a shallow pattern apply(F, X) has to match the Ids of every shape and the set of inner constructors is open across files; and the Id holds no Id of a subterm, because a truncation would give one term two spellings and split == and the table keys. A shape is a function of the content and the template, so the Handle determines it and the store stays canonical.

THE CONFIGURATION IS A TEMPLATE, one syntax for every constructor (user, 2026-09-20: one syntax, not three; the template names the constructor, so no indicator beside it; a depth of zero is another feature and is not offered). An element of the directive's list is the constructor applied to one of these at each argument position (a constructor of arity zero is its atom):

_                  nothing is recorded about the argument
*                  the root constructor of the argument is recorded
a nested template  the root constructor is recorded and, when it is the
                   template's constructor, the template goes on below
                   it; alternatives are joined by `;`

lambda(_) records the root only and its Ids are '__hash_consed_lambda/1'(Handle). apply(*, _) gives apply(lambda(B), X) the shape 'lambda/1'; with apply(apply(*, _), _), apply(apply(lambda(B), Y), X) has the shape 'apply/2(lambda/1)' and apply(lambda(B), X) still 'lambda/1'. An argument that is no Id is written -. The reading has a precedent in SWI-Prolog's mode-directed tabling, :- table path(_, _, min).

THE SHAPE IS A FUNCTION OF THE TERM AND OF ITS OWN TEMPLATE, and of nothing else: at a * the root is read off the functor name of the argument's Id, with no lookup; each nested template costs an upsert one trie_term/2 on the argument's Id. The spelling of a shape is remembered in a trie keyed by its structure, so an atom is built once per structure; the atoms are finitely many, bounded by the templates and the constructors.

AT COMPILE TIME a pattern that determines everything its template records gets the atom written into the head; a pattern that determines only part of it gets an unbound shape, which is correct (the constructors of the pattern are still checked by the lookups of the body) and unindexed. A nested template reads the argument's own arguments off the occurrence of that argument in the same clause, or off the store when its Id was baked. A partial pattern is NOT expanded into one clause per completion: a file loaded later brings new shapes, which the expanded clauses would silently miss. So the deeper a template, the fewer patterns determine it.

ONE CONFIGURATION PER CONSTRUCTOR, PER PROCESS: a second declaration of a constructor with another template raises while the file loads, because two shapes of one term would not be == (agent design decision, 2026-09-20, not objected to by the user; between files the library checks nothing else, by the user's ruling of 2026-09-17: which files list which constructors is the user's to arrange, for instance with one included file).

THE STORE IS ONE TRIE, used through SWI-Prolog's trie API directly (user, 2026-09-17, after a benchmark of four candidate stores: "use the trie"). Its key is the term itself, constructor included (a trie shares a functor node among all its keys), and its value is the key's own handle, written back with trie_update/3 right after the insertion, because inserting an existing key with a different value raises instead of failing. It lives outside the table space and trie_property(Trie, value_count(Count)) counts it.

externalized/2 AND internalized/3 ARE THE BOUNDARY: the first replaces every Id by its term, recursively, for printing, for writing a file and for a content address; the second interns, bottom up, the instances of the constructors whose templates it is given, for a term that did not come through the rewrite (a toplevel query, read_term/2, a module that did not opt in, a term built by =..).

:- hash_consing:rewritten(Templates). OPTS A FILE IN (user ruling, 2026-09-17: opt-in per file, the constructors passed to the directive, no setting). Every clause read after it in that file, included files counted, has each occurrence of a listed constructor turned into an Id pattern and a call of intern/2, the direction of each call decided at run time, so the file is written as the program that does not intern and needs no mode declaration. Several directives in one file add up. An empty list rewrites nothing. The library checks no consistency between files: files that pass one constructor to each other must both list it, which the user arranges, for instance with one included file (user ruling, 2026-09-17).

THE REWRITE OF A CLAUSE:

head       fast path when the Handle of every top-level head occurrence
           is bound: look them up top down, then run the body, which
           keeps the last call; otherwise look up the bound ones top
           down, insert the ground ones bottom up, run the body, insert
           the rest bottom up (an instantiation error when a term is
           still not ground)
body goal  when the variables of its terms are bound: insert bottom up
           and call; otherwise insert the ground ones, call, and then
           settle every occurrence top down (a lookup when the call
           bound the Id, an insertion when it made the term ground, an
           instantiation error when neither)
ground     an occurrence that is ground in the source is interned while
           the file loads and its Id is written into the clause

Control constructs (`,`, ;, ->, *->, \+, call/1, forall/2, the goal of findall/3 and of catch/3, a module-qualified goal) are rewritten inside. A listed constructor in the template or the result of findall/3, in the catcher of catch/3, or in a DCG rule raises while the file loads. Directives are not rewritten.

TWO RULES FOR A FILE THAT OPTS IN. (1) An instance of a listed constructor is settled, ground or its Id bound, by the end of the goal it is handed to; a term handed over half built and filled in later raises an instantiation error. (2) Reflection sees Ids: =.., functor/3, arg/3, write/1, variant_sha1/2, term_hash/2 and ordering whose result depends on the order see '__hash_consed_Name/Arity'(Handle). Code that reads structure calls externalized/2 first, and code that builds an instance with them calls internalized/3 after; an instance left uninterned fails to unify at every rewritten position. Using an Id as an opaque key, whose result does not depend on the order (an assoc key, a sort to remove duplicates), is fine.

THIS MODULE REFLECTS ON SOURCE CLAUSES with =.. while a file loads, a mechanical boundary; nothing here chooses behaviour at run time by reflection, and no closure is handed to another module.

FIXME: trie_term/2 on an integer that is no node of the store crashes the process with a segmentation fault (measured 2026-09-14 on trie_term(42, _)); a forged '__hash_consed_apply/2'(42) reaches it. Only a bug makes such a value. Candidate fix: none in the trie API; the assertz backends of the benchmark raise instead, at the costs recorded there.

FIXME: on 2026-09-14, within tabled compiles near a full stack, trie_lookup/3 failed silently on a key of the store and left the resource error pending. Here such a failure turns an upsert into an insertion of an existing key, which raises, or a lookup into a failure, which reads as no answer rather than resource_error. A direct probe at 1m, 4m and 32m stacks on 2026-09-17 raised resource_error(stack) every time and did not reproduce it.

FIXME: the order of two Ids depends on the insertion history. Nothing checks that no code in an opted-in file reads that order; it is a rule of the file, stated above.

FIXME: an Id written into a clause while the file loads is a handle of this process. A file must be loaded from source, never qcompiled nor saved in a state, and the store lives in one thread's global variable.

FIXME: a file that uses a constructor another file interns, without listing it, passes its instances uninterned; the rewritten positions of the other file then fail to unify, silently. The library cannot see it.

  229:- use_module(library(error), [must_be/2]).  230:- use_module(library(lists), [reverse/2, append/3]).  231
  232:- initialization(stores_created).
global variables of the loading thread.
  237stores_created :-
  238    trie_new(Store),
  239    nb_setval(hash_consing_store, Store),
  240    trie_new(Spellings),
  241    nb_setval(hash_consing_spellings, Spellings).
  242
  243%   ---- the relation ----
  247intern(Term, Id) :-
  248    nonvar(Id),
  249    !,
  250    id_parts(Id, _, _, Handle),
  251    (   nonvar(Handle)
  252    ->  trie_term(Handle, Stored),
  253        Term = Stored
  254    ;   upserted(Term, Id)
  255    ).
  256intern(Term, Id) :-
  257    upserted(Term, Id).
  261upserted(Term, Id) :-
  262    (   ground(Term)
  263    ->  true
  264    ;   throw(error(instantiation_error, context(hash_consing:intern/2, Term-Id)))
  265    ),
  266    nb_getval(hash_consing_store, Trie),
  267    (   trie_lookup(Trie, Term, Handle)
  268    ->  true
  269    ;   trie_insert(Trie, Term, pending, Handle),
  270        trie_update(Trie, Term, Handle)
  271    ),
  272    functor(Term, Name, Arity),
  273    configuration_of(Name, Arity, Configuration),
  274    id_of(Configuration, Term, Name, Arity, Handle, Id).
  275
  276id_of(root_only, _, Name, Arity, Handle, Id) :-
  277    id_name(Name, Arity, IdName),
  278    compound_name_arguments(Id, IdName, [Handle]).
  279id_of(shaped(Records), Term, Name, Arity, Handle, Id) :-
  280    id_name(Name, Arity, IdName),
  281    (   term_shape(Term, Records, [], Shape)
  282    ->  true
  283    ;   throw(error(instantiation_error, context(hash_consing:intern/2, Term)))
  284    ),
  285    compound_name_arguments(Id, IdName, [Shape, Handle]).
pattern; Shape is none for a constructor without a shape. Fails on any other value.
  291id_parts(Value, IdName, Shape, Handle) :-
  292    compound(Value),
  293    compound_name_arity(Value, IdName, Arity),
  294    sub_atom(IdName, 0, _, _, '__hash_consed_'),
  295    id_arguments(Arity, Value, Shape, Handle).
  296
  297id_arguments(1, Value, none, Handle) :-
  298    arg(1, Value, Handle).
  299id_arguments(2, Value, Shape, Handle) :-
  300    arg(1, Value, Shape),
  301    arg(2, Value, Handle).
as it is configured, its Handle and, if it has one, its shape unbound.
  306id_pattern(Name, Arity, Id, Handle) :-
  307    id_name(Name, Arity, IdName),
  308    configuration_of(Name, Arity, Configuration),
  309    (   Configuration == root_only
  310    ->  compound_name_arguments(Id, IdName, [Handle])
  311    ;   compound_name_arguments(Id, IdName, [_, Handle])
  312    ).
constructor Name/Arity, remembered once made, in both directions.
  317:- dynamic known_id_name/3.             % known_id_name(Name, Arity, IdName)
  318:- dynamic id_constructor/3.            % id_constructor(IdName, Name, Arity)
  319
  320id_name(Name, Arity, IdName) :-
  321    (   known_id_name(Name, Arity, Known)
  322    ->  IdName = Known
  323    ;   format(atom(Made), '__hash_consed_~w/~d', [Name, Arity]),
  324        assertz(known_id_name(Name, Arity, Made)),
  325        assertz(id_constructor(Made, Name, Arity)),
  326        IdName = Made
  327    ).
  328
  329%   ---- the configuration of a constructor ----
  330
  331:- dynamic constructor_configuration/3. % constructor_configuration(Name, Arity, Configuration)
shaped(Records) with one record per argument, ignored or root(Templates), Templates a list of below(Name/Arity, Records) for the nested templates; a constructor met without a declaration is registered as root_only.
  339configuration_of(Name, Arity, Configuration) :-
  340    (   constructor_configuration(Name, Arity, Known)
  341    ->  Configuration = Known
  342    ;   assertz(constructor_configuration(Name, Arity, root_only)),
  343        Configuration = root_only
  344    ).
configuration of the constructor in this process; another raises.
  349configuration_registered(Name, Arity, Configuration) :-
  350    (   constructor_configuration(Name, Arity, Known)
  351    ->  (   Known == Configuration
  352        ->  true
  353        ;   throw(error(permission_error(reconfigure, interned_constructor, Name/Arity),
  354                        context(hash_consing:rewritten/1,
  355                                configured(Known)-declared(Configuration))))
  356        )
  357    ;   assertz(constructor_configuration(Name, Arity, Configuration))
  358    ).
constructor list, a template, in the form the library keeps: ground, so that two declarations of one constructor compare with ==.
  364specification(Template, _, _, _) :-
  365    var(Template),
  366    !,
  367    throw(error(instantiation_error, context(hash_consing:rewritten/1, _))).
  368specification(Template, Name, Arity, Configuration) :-
  369    callable(Template),
  370    Template \== (*),
  371    Template \= (_ ; _),
  372    !,
  373    compound_name_arguments_or_atom(Template, Name, Arguments),
  374    length(Arguments, Arity),
  375    arguments_records(Arguments, Records),
  376    (   all_ignored(Records)
  377    ->  Configuration = root_only
  378    ;   Configuration = shaped(Records)
  379    ).
  380specification(Template, _, _, _) :-
  381    throw(error(type_error(constructor_template, Template), _)).
  382
  383compound_name_arguments_or_atom(Term, Name, Arguments) :-
  384    (   atom(Term)
  385    ->  Name = Term,
  386        Arguments = []
  387    ;   compound_name_arguments(Term, Name, Arguments)
  388    ).
  389
  390arguments_records([], []).
  391arguments_records([Argument | Arguments], [Record | Records]) :-
  392    argument_record(Argument, Record),
  393    arguments_records(Arguments, Records).
  394
  395argument_record(Argument, ignored) :-
  396    var(Argument),
  397    !.
  398argument_record(*, root([])) :-
  399    !.
  400argument_record(Alternatives, root(Templates)) :-
  401    alternatives_list(Alternatives, List),
  402    templates_below(List, Templates).
  403
  404alternatives_list(Alternatives, List) :-
  405    (   nonvar(Alternatives),
  406        Alternatives = (Left ; Right)
  407    ->  alternatives_list(Left, LeftList),
  408        alternatives_list(Right, RightList),
  409        append(LeftList, RightList, List)
  410    ;   List = [Alternatives]
  411    ).
  412
  413%   A nested template whose own arguments are all ignored records nothing
  414%   below the root, which the root already says, and is left out.
  415templates_below([], []).
  416templates_below([Template | Templates], Belows) :-
  417    specification(Template, Name, Arity, Configuration),
  418    (   Configuration = shaped(Records)
  419    ->  Belows = [below(Name/Arity, Records) | Rest]
  420    ;   Belows = Rest
  421    ),
  422    templates_below(Templates, Rest),
  423    (   memberchk(below(Name/Arity, _), Rest)
  424    ->  throw(error(domain_error(one_template_per_constructor, Template), _))
  425    ;   true
  426    ).
  427
  428all_ignored([]).
  429all_ignored([ignored | Records]) :-
  430    all_ignored(Records).
  431
  432%   ---- shapes ----
  433
  434%   A STRUCTURE is what a shape atom spells: a list, one description per
  435%   recorded argument, `other` for an argument that is no Id and
  436%   `constructor(Name/Arity, Structure)` otherwise, the inner structure being
  437%   `none` where no nested template goes on.
under its template. Fails when the term does not determine it: a recorded argument is an unbound variable, or a nested template needs the arguments of an Id pattern that is neither among Occurrences, the occurrence(Term, Id, Handle) of the clause being rewritten, nor stored. A ground Term always determines its shape.
  446term_shape(Term, Records, Occurrences, Atom) :-
  447    compound_name_arguments_or_atom(Term, _, Arguments),
  448    arguments_descriptions(Records, Arguments, Occurrences, Structure),
  449    nb_getval(hash_consing_spellings, Spellings),
  450    (   trie_lookup(Spellings, Structure, Known)
  451    ->  Atom = Known
  452    ;   structure_atom(Structure, Atom),
  453        trie_insert(Spellings, Structure, Atom)
  454    ).
  455
  456arguments_descriptions([], [], _, []).
  457arguments_descriptions([ignored | Records], [_ | Arguments], Occurrences, Descriptions) :-
  458    !,
  459    arguments_descriptions(Records, Arguments, Occurrences, Descriptions).
  460arguments_descriptions([root(Templates) | Records], [Argument | Arguments], Occurrences,
  461                       [Description | Descriptions]) :-
  462    argument_description(Argument, Templates, Occurrences, Description),
  463    arguments_descriptions(Records, Arguments, Occurrences, Descriptions).
  464
  465argument_description(Argument, _, _, _) :-
  466    var(Argument),
  467    !,
  468    fail.
  469argument_description(Argument, Templates, Occurrences, constructor(Name/Arity, Inner)) :-
  470    id_parts(Argument, IdName, _, _),
  471    !,
  472    id_constructor(IdName, Name, Arity),
  473    (   memberchk(below(Name/Arity, Records), Templates)
  474    ->  id_term(Argument, Occurrences, Below),
  475        compound_name_arguments_or_atom(Below, _, BelowArguments),
  476        arguments_descriptions(Records, BelowArguments, Occurrences, Inner)
  477    ;   Inner = none
  478    ).
  479argument_description(_, _, _, other).
Handle is bound; the occurrence's, when it is a pattern of the clause being rewritten; fails otherwise.
  485id_term(Id, Occurrences, Term) :-
  486    id_parts(Id, _, _, Handle),
  487    (   nonvar(Handle)
  488    ->  trie_term(Handle, Term)
  489    ;   occurrence_term(Occurrences, Id, Term)
  490    ).
  491
  492occurrence_term([occurrence(Candidate, CandidateId, _) | Occurrences], Id, Term) :-
  493    (   CandidateId == Id
  494    ->  Term = Candidate
  495    ;   occurrence_term(Occurrences, Id, Term)
  496    ).
commas, - for other, Name/Arity and, in parentheses, what is below.
  501structure_atom(Structure, Atom) :-
  502    descriptions_texts(Structure, Texts),
  503    atomic_list_concat(Texts, ',', Atom).
  504
  505descriptions_texts([], []).
  506descriptions_texts([Description | Descriptions], [Text | Texts]) :-
  507    description_text(Description, Text),
  508    descriptions_texts(Descriptions, Texts).
  509
  510description_text(other, '-').
  511description_text(constructor(Name/Arity, none), Text) :-
  512    !,
  513    format(atom(Text), '~w/~d', [Name, Arity]).
  514description_text(constructor(Name/Arity, Inner), Text) :-
  515    structure_atom(Inner, InnerText),
  516    format(atom(Text), '~w/~d(~w)', [Name, Arity, InnerText]).
  517
  518%   ---- the boundary ----
recursively.
  523externalized(Term, External) :-
  524    var(Term),
  525    !,
  526    External = Term.
  527externalized(Term, External) :-
  528    id_parts(Term, _, _, Handle),
  529    !,
  530    (   nonvar(Handle)
  531    ->  trie_term(Handle, Stored),
  532        externalized(Stored, External)
  533    ;   throw(error(instantiation_error, context(hash_consing:externalized/2, Term)))
  534    ).
  535externalized(Term, External) :-
  536    compound(Term),
  537    !,
  538    compound_name_arguments(Term, Name, Arguments),
  539    arguments_externalized(Arguments, ExternalArguments),
  540    compound_name_arguments(External, Name, ExternalArguments).
  541externalized(Term, Term).
  542
  543arguments_externalized([], []).
  544arguments_externalized([Argument | Arguments], [External | Externals]) :-
  545    externalized(Argument, External),
  546    arguments_externalized(Arguments, Externals).
constructor in Constructors interned, bottom up; an Id is kept as it is. The list's elements are templates, as in the directive, and register the same configurations.
  553internalized(Constructors, External, TermWithIds) :-
  554    must_be(list, Constructors),
  555    specifications_indicators(Constructors, Indicators),
  556    term_internalized(External, Indicators, TermWithIds).
  557
  558specifications_indicators([], []).
  559specifications_indicators([Element | Elements], [Name/Arity | Indicators]) :-
  560    specification(Element, Name, Arity, Configuration),
  561    configuration_registered(Name, Arity, Configuration),
  562    specifications_indicators(Elements, Indicators).
  563
  564term_internalized(Term, _, Internal) :-
  565    var(Term),
  566    !,
  567    Internal = Term.
  568term_internalized(Term, _, Internal) :-
  569    id_parts(Term, _, _, _),
  570    !,
  571    Internal = Term.
  572term_internalized(Term, Indicators, Internal) :-
  573    compound(Term),
  574    !,
  575    compound_name_arguments(Term, Name, Arguments),
  576    arguments_internalized(Arguments, Indicators, InternalArguments),
  577    compound_name_arguments(Rebuilt, Name, InternalArguments),
  578    compound_name_arity(Term, Name, Arity),
  579    instance_internalized(Name/Arity, Indicators, Rebuilt, Internal).
  580term_internalized(Term, Indicators, Internal) :-
  581    atom(Term),
  582    !,
  583    instance_internalized(Term/0, Indicators, Term, Internal).
  584term_internalized(Term, _, Term).
  585
  586arguments_internalized([], _, []).
  587arguments_internalized([Argument | Arguments], Indicators, [Internal | Internals]) :-
  588    term_internalized(Argument, Indicators, Internal),
  589    arguments_internalized(Arguments, Indicators, Internals).
  590
  591instance_internalized(Indicator, Indicators, Term, Internal) :-
  592    (   memberchk(Indicator, Indicators)
  593    ->  intern(Term, Internal)
  594    ;   Internal = Term
  595    ).
  596
  597%   ---- the directive ----
constructors whose templates are in the list; several calls add up.
  602:- dynamic registered_constructor/3.     % registered_constructor(SourceFile, Name, Arity)
  603
  604rewritten(Constructors) :-
  605    must_be(list, Constructors),
  606    (   prolog_load_context(source, File)
  607    ->  true
  608    ;   throw(error(context_error(nodirective, hash_consing:rewritten/1), _))
  609    ),
  610    constructors_registered(Constructors, File).
  611
  612constructors_registered([], _).
  613constructors_registered([Element | Elements], File) :-
  614    specification(Element, Name, Arity, Configuration),
  615    configuration_registered(Name, Arity, Configuration),
  616    id_name(Name, Arity, _),
  617    (   registered_constructor(File, Name, Arity)
  618    ->  true
  619    ;   assertz(registered_constructor(File, Name, Arity))
  620    ),
  621    constructors_registered(Elements, File).
  622
  623%   ---- the rewrite ----
file that opted in; fails, leaving the term to SWI-Prolog, otherwise.
  628source_rewritten(Source, _) :-
  629    var(Source),
  630    !,
  631    fail.
  632source_rewritten(end_of_file, _) :-
  633    !,
  634    prolog_load_context(source, File),
  635    prolog_load_context(file, File),
  636    retractall(registered_constructor(File, _, _)),
  637    fail.
  638source_rewritten((:- _), _) :-
  639    !,
  640    fail.
  641source_rewritten((?- _), _) :-
  642    !,
  643    fail.
  644source_rewritten(Source, Rewritten) :-
  645    prolog_load_context(source, File),
  646    registered_constructor(File, _, _),
  647    !,
  648    clause_rewritten(Source, File, Candidate),
  649    Candidate \=@= Source,
  650    Rewritten = Candidate.
  651
  652clause_rewritten((Head --> Body), File, (Head --> Body)) :-
  653    !,
  654    listed_absent((Head --> Body), File, dcg_rule).
  655clause_rewritten((Head :- Body), File, Rewritten) :-
  656    !,
  657    rule_rewritten(Head, Body, File, Rewritten).
  658clause_rewritten(Head, File, Rewritten) :-
  659    rule_rewritten(Head, true, File, Rewritten).
occurrences scheduled around the rewritten body.
  664rule_rewritten(Qualifier:Head0, Body0, File, (Qualifier:Head :- Body)) :-
  665    !,
  666    plain_rule_rewritten(Head0, Body0, File, Head, Body).
  667rule_rewritten(Head0, Body0, File, (Head :- Body)) :-
  668    plain_rule_rewritten(Head0, Body0, File, Head, Body).
  669
  670plain_rule_rewritten(Head0, Body0, File, Head, Body) :-
  671    arguments_of_abstracted(Head0, File, Head, Occurrences),
  672    body_rewritten(Body0, File, RewrittenBody),
  673    head_scheduled(Occurrences, RewrittenBody, Body).
arguments of a head or a goal abstracted (its own functor is a predicate and is left alone), the ground occurrences interned now, and the others listed bottom up as occurrence(Term, Id, Handle).
  680arguments_of_abstracted(Callable0, File, Callable, Occurrences) :-
  681    compound(Callable0),
  682    !,
  683    compound_name_arguments(Callable0, Name, Arguments0),
  684    arguments_abstracted(Arguments0, File, Arguments, [], Reversed),
  685    compound_name_arguments(Callable, Name, Arguments),
  686    reverse(Reversed, BottomUp),
  687    occurrences_baked(BottomUp, Occurrences),
  688    shapes_determined(Occurrences).
  689arguments_of_abstracted(Callable, _, Callable, []).
  690
  691arguments_abstracted([], _, [], Reversed, Reversed).
  692arguments_abstracted([Argument0 | Arguments0], File, [Argument | Arguments], Reversed0, Reversed) :-
  693    abstracted(Argument0, File, Argument, Reversed0, Reversed1),
  694    arguments_abstracted(Arguments0, File, Arguments, Reversed1, Reversed).
of a listed constructor replaced by an Id pattern, its subterms first, the occurrences prepended as they are met.
  700abstracted(Term, _, Term, Reversed, Reversed) :-
  701    var(Term),
  702    !.
  703abstracted(Term0, File, Term, Reversed0, Reversed) :-
  704    compound(Term0),
  705    !,
  706    compound_name_arguments(Term0, Name, Arguments0),
  707    arguments_abstracted(Arguments0, File, Arguments, Reversed0, Reversed1),
  708    compound_name_arguments(Layer, Name, Arguments),
  709    compound_name_arity(Term0, Name, Arity),
  710    occurrence_abstracted(Name, Arity, Layer, File, Term, Reversed1, Reversed).
  711abstracted(Term0, File, Term, Reversed0, Reversed) :-
  712    atom(Term0),
  713    !,
  714    occurrence_abstracted(Term0, 0, Term0, File, Term, Reversed0, Reversed).
  715abstracted(Term, _, Term, Reversed, Reversed).
  716
  717occurrence_abstracted(Name, Arity, Layer, File, Term, Reversed0, Reversed) :-
  718    (   registered_constructor(File, Name, Arity)
  719    ->  id_pattern(Name, Arity, Term, Handle),
  720        Reversed = [occurrence(Layer, Term, Handle) | Reversed0]
  721    ;   Term = Layer,
  722        Reversed = Reversed0
  723    ).
whose term is ground is interned while the file loads, which binds its Id in the clause; bottom up, so a parent of baked occurrences may become ground in turn.
  730occurrences_baked([], []).
  731occurrences_baked([occurrence(Term, Id, Handle) | Occurrences], Remaining) :-
  732    (   ground(Term)
  733    ->  intern(Term, Id),
  734        occurrences_baked(Occurrences, Remaining)
  735    ;   Remaining = [occurrence(Term, Id, Handle) | Rest],
  736        occurrences_baked(Occurrences, Rest)
  737    ).
occurrence of a shaped constructor whose pattern determines the whole configured shape gets the shape atom written into its Id pattern, bottom up so that a determined inner occurrence can determine an outer one; an occurrence that determines only part of it keeps an unbound shape, which matches every shape, and its constructors are still checked by the lookups of the body.
  747shapes_determined(Occurrences) :-
  748    shapes_determined(Occurrences, Occurrences).
  749
  750shapes_determined([], _).
  751shapes_determined([occurrence(Term, Id, _) | Rest], Occurrences) :-
  752    (   id_parts(Id, _, Shape, _),
  753        var(Shape),
  754        compound_name_arguments_or_atom(Term, Name, Arguments),
  755        length(Arguments, Arity),
  756        constructor_configuration(Name, Arity, shaped(Records)),
  757        term_shape(Term, Records, Occurrences, Atom)
  758    ->  Shape = Atom
  759    ;   true
  760    ),
  761    shapes_determined(Rest, Occurrences).
OF A CLAUSE (above).
  766head_scheduled([], Body, Body) :-
  767    !.
  768head_scheduled(BottomUp, Body0, (Condition -> Fast ; General)) :-
  769    reverse(BottomUp, TopDown),
  770    top_level_occurrences(BottomUp, BottomUp, TopLevel),
  771    handles_bound(TopLevel, Condition),
  772    occurrences_goal(TopDown, intern, Lookups),
  773    occurrences_goal(TopDown, intern_if_bound, Settled),
  774    occurrences_goal(BottomUp, intern_if_unbound_and_ground, Inserted),
  775    occurrences_goal(BottomUp, intern_if_unbound, Finished),
  776    conjoined([Lookups, Body0], Fast),
  777    conjoined([Settled, Inserted, Body0, Finished], General).
  781conjoined(Goals, Conjunction) :-
  782    goals_kept(Goals, Kept),
  783    kept_conjoined(Kept, Conjunction).
  784
  785goals_kept([], []).
  786goals_kept([Goal | Goals], Kept) :-
  787    (   Goal == true
  788    ->  Kept = Rest
  789    ;   Kept = [Goal | Rest]
  790    ),
  791    goals_kept(Goals, Rest).
  792
  793kept_conjoined([], true).
  794kept_conjoined([Goal], Goal) :-
  795    !.
  796kept_conjoined([Goal | Goals], (Goal, Conjunction)) :-
  797    kept_conjoined(Goals, Conjunction).
  798
  799top_level_occurrences([], _, []).
  800top_level_occurrences([Occurrence | Occurrences], All, TopLevel) :-
  801    Occurrence = occurrence(_, _, Handle),
  802    (   nested_in_another(Handle, All)
  803    ->  TopLevel = Rest
  804    ;   TopLevel = [Occurrence | Rest]
  805    ),
  806    top_level_occurrences(Occurrences, All, Rest).
  807
  808nested_in_another(Handle, [occurrence(Term, _, _) | Occurrences]) :-
  809    (   term_variables(Term, Variables),
  810        variable_member(Handle, Variables)
  811    ->  true
  812    ;   nested_in_another(Handle, Occurrences)
  813    ).
  814
  815variable_member(Variable, [Candidate | Candidates]) :-
  816    (   Variable == Candidate
  817    ->  true
  818    ;   variable_member(Variable, Candidates)
  819    ).
  820
  821handles_bound([occurrence(_, _, Handle)], nonvar(Handle)) :-
  822    !.
  823handles_bound([occurrence(_, _, Handle) | Occurrences], (nonvar(Handle), Condition)) :-
  824    handles_bound(Occurrences, Condition).
over the occurrences, in the order given.
  829occurrences_goal([], _, true).
  830occurrences_goal([Occurrence], Step, Goal) :-
  831    !,
  832    occurrence_goal(Step, Occurrence, Goal).
  833occurrences_goal([Occurrence | Occurrences], Step, (Goal, Goals)) :-
  834    occurrence_goal(Step, Occurrence, Goal),
  835    occurrences_goal(Occurrences, Step, Goals).
  836
  837occurrence_goal(intern, occurrence(Term, Id, _),
  838                hash_consing:intern(Term, Id)).
  839occurrence_goal(intern_if_bound, occurrence(Term, Id, Handle),
  840                (nonvar(Handle) -> hash_consing:intern(Term, Id) ; true)).
  841occurrence_goal(intern_if_unbound, occurrence(Term, Id, Handle),
  842                (var(Handle) -> hash_consing:intern(Term, Id) ; true)).
  843occurrence_goal(intern_if_unbound_and_ground, occurrence(Term, Id, Handle),
  844                ((var(Handle), ground(Term)) -> hash_consing:intern(Term, Id) ; true)).
  845occurrence_goal(intern_if_ground, occurrence(Term, Id, _),
  846                (ground(Term) -> hash_consing:intern(Term, Id) ; true)).
inside, every other goal by goal_rewritten/3.
  851body_rewritten(Goal, _, Goal) :-
  852    var(Goal),
  853    !.
  854body_rewritten((Left0, Right0), File, (Left, Right)) :-
  855    !,
  856    body_rewritten(Left0, File, Left),
  857    body_rewritten(Right0, File, Right).
  858body_rewritten((Left0 ; Right0), File, (Left ; Right)) :-
  859    !,
  860    body_rewritten(Left0, File, Left),
  861    body_rewritten(Right0, File, Right).
  862body_rewritten((Condition0 -> Then0), File, (Condition -> Then)) :-
  863    !,
  864    body_rewritten(Condition0, File, Condition),
  865    body_rewritten(Then0, File, Then).
  866body_rewritten((Condition0 *-> Then0), File, (Condition *-> Then)) :-
  867    !,
  868    body_rewritten(Condition0, File, Condition),
  869    body_rewritten(Then0, File, Then).
  870body_rewritten(\+ Goal0, File, \+ Goal) :-
  871    !,
  872    body_rewritten(Goal0, File, Goal).
  873body_rewritten(call(Goal0), File, call(Goal)) :-
  874    !,
  875    body_rewritten(Goal0, File, Goal).
  876body_rewritten(forall(Condition0, Action0), File, forall(Condition, Action)) :-
  877    !,
  878    body_rewritten(Condition0, File, Condition),
  879    body_rewritten(Action0, File, Action).
  880body_rewritten(findall(Template, Goal0, Result), File, findall(Template, Goal, Result)) :-
  881    !,
  882    listed_absent(Template-Result, File, findall_template_or_result),
  883    body_rewritten(Goal0, File, Goal).
  884body_rewritten(catch(Goal0, Catcher, Recovery0), File, catch(Goal, Catcher, Recovery)) :-
  885    !,
  886    listed_absent(Catcher, File, catch_catcher),
  887    body_rewritten(Goal0, File, Goal),
  888    body_rewritten(Recovery0, File, Recovery).
  889body_rewritten(Qualifier:Goal0, File, Qualifier:Goal) :-
  890    !,
  891    body_rewritten(Goal0, File, Goal).
  892body_rewritten(Goal0, File, Goal) :-
  893    goal_rewritten(Goal0, File, Goal).
OF A CLAUSE (above).
  898goal_rewritten(Goal0, File, Goal) :-
  899    arguments_of_abstracted(Goal0, File, Called, BottomUp),
  900    goal_scheduled(BottomUp, Called, Goal).
  901
  902goal_scheduled([], Called, Called) :-
  903    !.
  904goal_scheduled(BottomUp, Called, (ground(Variables) -> Fast ; General)) :-
  905    reverse(BottomUp, TopDown),
  906    terms_variables(BottomUp, Variables),
  907    occurrences_goal(BottomUp, intern, Inserts),
  908    occurrences_goal(BottomUp, intern_if_ground, Settled),
  909    occurrences_goal(TopDown, intern, Finished),
  910    conjoined([Inserts, Called], Fast),
  911    conjoined([Settled, Called, Finished], General).
occurrences' terms other than their own Handles.
  916terms_variables(Occurrences, Variables) :-
  917    occurrences_terms(Occurrences, Terms),
  918    term_variables(Terms, All),
  919    occurrences_handles(Occurrences, Handles),
  920    variables_without(All, Handles, Variables).
  921
  922occurrences_terms([], []).
  923occurrences_terms([occurrence(Term, _, _) | Occurrences], [Term | Terms]) :-
  924    occurrences_terms(Occurrences, Terms).
  925
  926occurrences_handles([], []).
  927occurrences_handles([occurrence(_, _, Handle) | Occurrences], [Handle | Handles]) :-
  928    occurrences_handles(Occurrences, Handles).
  929
  930variables_without([], _, []).
  931variables_without([Variable | Variables], Excluded, Kept) :-
  932    (   variable_member(Variable, Excluded)
  933    ->  Kept = Rest
  934    ;   Kept = [Variable | Rest]
  935    ),
  936    variables_without(Variables, Excluded, Rest).
Term, a place the rewrite does not schedule; raises otherwise.
  941listed_absent(Term, File, Place) :-
  942    abstracted(Term, File, _, [], Occurrences),
  943    (   Occurrences == []
  944    ->  true
  945    ;   throw(error(domain_error(term_without_rewritten_constructor, Term),
  946                    context(hash_consing:rewritten/1, Place)))
  947    ).
  948
  949%   ---- the hook, last, so that this file's own clauses are read before it
  950%   is active ----
  951
  952:- multifile user:term_expansion/2.  953:- dynamic user:term_expansion/2.  954
  955user:term_expansion(Source, Rewritten) :-
  956    hash_consing:source_rewritten(Source, Rewritten)