1:- module(hash_consing,
2 [ intern/2, % ?Term, ?Id
3 represented/2, % ?Layer, ?Representation
4 is_id/1, % +Value
5 externalized/2, % +TermWithIds, -External
6 internalized/3, % +Templates, +External, -TermWithIds
7 declared/1, % +Templates
8 rewritten/1, % +Templates
9 rewritten/2, % +Templates, +Options
10 store_property/1, % ?Property
11 store_growth_bounded/1 % +Bytes
12 ]).
341:- use_module(library(error), [must_be/2]). 342:- use_module(library(lists), [reverse/2, member/2]). 343 344:- initialization(stores_created).
351:- dynamic process_stores/2. 352 353stores_created :- 354 ( process_stores(_, _) 355 -> true 356 ; trie_new(Store), 357 trie_new(Spellings), 358 assertz(process_stores(Store, Spellings)) 359 ).
ids(Count), the number of terms interned;
nodes(Count), the number of nodes of its trie; bytes(Bytes), the
memory the trie takes. The spellings of the shapes are not counted.367store_property(ids(Count)) :- 368 process_stores(Store, _), 369 trie_property(Store, value_count(Count)). 370store_property(nodes(Count)) :- 371 process_stores(Store, _), 372 trie_property(Store, node_count(Count)). 373store_property(bytes(Bytes)) :- 374 process_stores(Store, _), 375 trie_property(Store, size(Bytes)). 376 377% ---- the relation ----
383intern(Term, Id) :- 384 nonvar(Id), 385 !, 386 id_parts(Id, _, _, Handle), 387 ( nonvar(Handle) 388 -> trie_term(Handle, Stored), 389 Term = Stored 390 ; upserted(Term, Id) 391 ). 392intern(Term, Id) :- 393 upserted(Term, Id).
400upserted(Term, Id) :- 401 process_stores(Trie, _), 402 trie_lookup(Trie, Term, Handle), 403 Handle \== pending, 404 !, 405 stored_id(Term, Handle, Id). 406upserted(Term, Id) :- 407 ( ground(Term) 408 -> true 409 ; throw(error(instantiation_error, context(hash_consing:intern/2, Term-Id))) 410 ), 411 functor(Term, Name, Arity), 412 templates_known(Name, Arity, hash_consing:intern/2, Templates, Kind), 413 ( Kind == root_only 414 -> true 415 ; first_template_recorded(Templates, Term, Structure) 416 ), 417 process_stores(Trie, _), 418 ( trie_lookup(Trie, Term, Found), 419 Found \== pending 420 -> Handle = Found 421 ; with_mutex(hash_consing_store, handle_inserted(Trie, Term, Handle)) 422 ), 423 id_built(Name, Arity, Kind, Structure, Handle, Id). 424 425% stored_id(+Term, +Handle, ?Id): the Id of a term found in the store 426% with its handle written back. A stored term is ground and was declared, 427% and the first template that matched it when it was inserted still does, 428% so only the shape is computed again. 429 430stored_id(Term, Handle, Id) :- 431 functor(Term, Name, Arity), 432 constructor_templates(Name, Arity, Templates, Kind), 433 ( Kind == root_only 434 -> known_id_name(Name, Arity, IdName), 435 compound_name_arguments(Id, IdName, [Handle]) 436 ; first_template_recorded(Templates, Term, Structure), 437 id_built(Name, Arity, Kind, Structure, Handle, Id) 438 ). 439 440% handle_inserted(+Trie, +Term, -Handle): Term looked up again and, when it 441% is still absent, inserted, under the mutex `hash_consing_store`. Two 442% threads that share the store and both miss Term would otherwise both 443% insert it, and the second trie_insert/4 raises 444% `permission_error(modify, trie_key, Term)` (measured 2026-09-30: eight 445% threads interning the same 20000 terms raised in 5 of 5 runs). A lookup 446% that finds Term with its handle written back takes no mutex; a value 447% still `pending`, written by an insertion in progress in another thread or 448% left by an interrupted one, is settled here, under the mutex, so that only 449% the thread holding it writes to the store. 450 451handle_inserted(Trie, Term, Handle) :- 452 ( trie_lookup(Trie, Term, Found) 453 -> settled_handle(Trie, Term, Found, Handle) 454 ; store_within_limit(Trie), 455 thread_growth_within_budget(Trie), 456 trie_property(Trie, node_count(NodesBefore)), 457 sig_atomic(inserted(Trie, Term, Handle)), 458 thread_growth_counted(Trie, NodesBefore) 459 ).
resource_error(hash_consing_store) when the Prolog flag
hash_consing_store_limit is a number of bytes and the store takes more.
Measuring the bytes walks the whole trie (20 ms at 200,000 terms,
measured 2026-10-01), its node count does not, so the bytes are
estimated from the node count and the bytes per node of the last
measurement, and measured again only when the estimate passes the limit
or the nodes have doubled since; only a measurement raises.471:- create_prolog_flag(hash_consing_store_limit, infinite, [type(term), keep(true)]). 472 473store_within_limit(Trie) :- 474 current_prolog_flag(hash_consing_store_limit, Limit), 475 ( Limit == infinite 476 -> true 477 ; must_be(positive_integer, Limit), 478 trie_property(Trie, node_count(Nodes)), 479 bytes_per_node(Trie, Nodes, MeasuredNodes, MeasuredBytes), 480 ( Nodes * MeasuredBytes =< Limit * MeasuredNodes 481 -> true 482 ; trie_property(Trie, size(Bytes)), 483 flag(hash_consing_measured_nodes, _, Nodes), 484 flag(hash_consing_measured_bytes, _, Bytes), 485 ( Bytes > Limit 486 -> format(atom(Measured), 'the store takes ~D bytes, the limit is ~D', [Bytes, Limit]), 487 throw(error(resource_error(hash_consing_store), 488 context(hash_consing:intern/2, Measured))) 489 ; true 490 ) 491 ) 492 ). 493 494% bytes_per_node(+Trie, +Nodes, -MeasuredNodes, -MeasuredBytes): the nodes 495% and bytes of the last measurement of the store, measured again when 496% there is none or the nodes have doubled since; their ratio estimates the 497% bytes of a node. 498 499bytes_per_node(Trie, Nodes, MeasuredNodes, MeasuredBytes) :- 500 flag(hash_consing_measured_nodes, KnownNodes, KnownNodes), 501 flag(hash_consing_measured_bytes, KnownBytes, KnownBytes), 502 ( KnownNodes > 0, 503 Nodes < 2 * KnownNodes 504 -> MeasuredNodes = KnownNodes, 505 MeasuredBytes = KnownBytes 506 ; trie_property(Trie, size(Bytes)), 507 MeasuredNodes is max(Nodes, 1), 508 MeasuredBytes = Bytes, 509 flag(hash_consing_measured_nodes, _, MeasuredNodes), 510 flag(hash_consing_measured_bytes, _, MeasuredBytes) 511 ).
infinite
lifts the bound. Once the nodes this thread inserted since the call take
more than Bytes, its next insertion raises
error(resource_error(hash_consing_store), context(hash_consing:intern/2,
Message)), Message stating the bytes added and the budget, the error
the flag hash_consing_store_limit raises. A term already in the store
costs nothing: interning it again adds no node, is not counted, and is
answered even past the budget. Only this thread's own insertions
count, measured as the nodes each one adds under the store's mutex, so
threads that share the store do not charge each other, and what the
store held before the call does not count. A thread created later inherits the bound with a count
of its own from zero, as it inherits stack_limit and table_space.
Bytes are estimated from the nodes as for the flag
hash_consing_store_limit. The bound lives in the Prolog flag
hash_consing_growth_budget.531:- create_prolog_flag(hash_consing_growth_budget, infinite, [type(term), keep(true)]). 532 533store_growth_bounded(Bytes) :- 534 ( Bytes == infinite 535 -> true 536 ; must_be(positive_integer, Bytes) 537 ), 538 set_prolog_flag(hash_consing_growth_budget, Bytes), 539 nb_setval(hash_consing_growth_added, 0). 540 541% thread_growth_within_budget(+Trie): the nodes this thread inserted since 542% its budget was set take no more than the budget. 543 544thread_growth_within_budget(Trie) :- 545 current_prolog_flag(hash_consing_growth_budget, Bytes), 546 ( Bytes == infinite 547 -> true 548 ; thread_growth_added(Added), 549 trie_property(Trie, node_count(Nodes)), 550 bytes_per_node(Trie, Nodes, MeasuredNodes, MeasuredBytes), 551 ( Added * MeasuredBytes =< Bytes * MeasuredNodes 552 -> true 553 ; Spent is Added * MeasuredBytes // MeasuredNodes, 554 format(atom(Measured), 555 'this thread grew the store by about ~D bytes, its budget is ~D', 556 [Spent, Bytes]), 557 throw(error(resource_error(hash_consing_store), 558 context(hash_consing:intern/2, Measured))) 559 ) 560 ). 561 562% thread_growth_added(-Added): the nodes this thread inserted since its 563% budget was set; zero in a thread that inherited the budget and has not 564% inserted yet. 565 566thread_growth_added(Added) :- 567 ( nb_current(hash_consing_growth_added, Added) 568 -> true 569 ; Added = 0 570 ). 571 572% thread_growth_counted(+Trie, +NodesBefore): the nodes the insertion just 573% made, the store's nodes now less NodesBefore, added to this thread's 574% count when it has a budget. Called under the store's mutex, right after 575% the insertion, so no other thread's nodes are counted. 576 577thread_growth_counted(Trie, NodesBefore) :- 578 current_prolog_flag(hash_consing_growth_budget, Bytes), 579 ( Bytes == infinite 580 -> true 581 ; thread_growth_added(AddedBefore), 582 trie_property(Trie, node_count(Nodes)), 583 Added is AddedBefore + Nodes - NodesBefore, 584 nb_setval(hash_consing_growth_added, Added) 585 ). 586 587% inserted(+Trie, +Term, -Handle): the insertion and the write-back of the 588% key's own handle as its value, run under `sig_atomic/1` by 589% handle_inserted/3, so 590% that a signal (a `call_with_time_limit/2` expiring) waits until both are 591% done. `call_with_inference_limit/3` is no signal: its limit can still 592% fall between the two and leave the value `pending` (measured 2026-09-27: 593% interning a fresh two-argument term under a limit of 9 inferences left it 594% `pending`, and the next lookup handed `pending` to `trie_term/2`, which 595% raised `type_error(address, pending)`). settled_handle/4 repairs such a 596% key at its next lookup. 597 598inserted(Trie, Term, Handle) :- 599 trie_insert(Trie, Term, pending, Handle), 600 trie_update(Trie, Term, Handle). 601 602% settled_handle(+Trie, +Term, +Found, -Handle): the handle of Term's node, 603% Found when the value was written back, and otherwise, when an 604% interrupted insertion left `pending`, the node's own handle, read with 605% `'$trie_gen_node'/3` (SWI-Prolog's generator of a trie's keys with their 606% nodes, here with the key bound) and written back. The repair may itself 607% be interrupted between its two steps; the key then stays `pending` and 608% is repaired at its next lookup. 609 610settled_handle(Trie, Term, pending, Handle) :- 611 !, 612 '$trie_gen_node'(Trie, Term, Handle), 613 trie_update(Trie, Term, Handle). 614settled_handle(_, _, Handle, Handle).
Given an Id of the constructor, Layer is its stored term, or, when the handle is unbound, Layer is interned into it and must be ground. Given a plain term of the constructor, it is Layer, and no template may match any completion of it: it is then not the representation of a term that must be interned. Given nothing, Layer is represented as far as what is bound of it decides: its Id when it is ground and a template matches it, an Id pattern whose handle is unbound when every completion must be interned, Layer itself when none may be, and an instantiation error when what is bound does not decide. Any other value fails.
632represented(Layer, Representation) :- 633 ( var(Layer) 634 -> throw(error(instantiation_error, 635 context(hash_consing:represented/2, Layer-Representation))) 636 ; true 637 ), 638 functor(Layer, Name, Arity), 639 templates_known(Name, Arity, hash_consing:represented/2, Templates, Kind), 640 id_name(Name, Arity, IdName), 641 ( var(Representation) 642 -> representation_made(Templates, Kind, Name, Arity, Layer, Representation) 643 ; id_parts(Representation, IdName, _, Handle) 644 -> ( nonvar(Handle) 645 -> trie_term(Handle, Stored), 646 Layer = Stored 647 ; ground(Layer) 648 -> upserted(Layer, Representation) 649 ; throw(error(instantiation_error, context(hash_consing:represented/2, Layer))) 650 ) 651 ; functor(Representation, Name, Arity) 652 -> Layer = Representation, 653 value_classified(Templates, Layer, Class, _), 654 Class == must_not_intern 655 ). 656 657representation_made(_, _, _, _, Layer, Representation) :- 658 ground(Layer), 659 !, 660 ( upserted(Layer, Id) 661 -> Representation = Id 662 ; Representation = Layer 663 ). 664representation_made(Templates, Kind, Name, Arity, Layer, Representation) :- 665 value_classified(Templates, Layer, Class, Structure), 666 ( Class == must_intern 667 -> ( ground(Layer) 668 -> upserted(Layer, Representation) 669 ; id_built(Name, Arity, Kind, Structure, _, Representation) 670 ) 671 ; Class == must_not_intern 672 -> Representation = Layer 673 ; throw(error(instantiation_error, context(hash_consing:represented/2, Layer))) 674 ).
682represented_settled(Layer, Representation) :-
683 represented(Layer, Representation),
684 ( id_parts(Representation, _, _, Handle),
685 var(Handle)
686 -> throw(error(instantiation_error, context(hash_consing:represented/2, Layer)))
687 ; true
688 ).term_weight.pl calls it once per node it weighs.
697is_id(Value) :-
698 compound(Value),
699 compound_name_arity(Value, IdName, Arity),
700 id_constructor(IdName, _, _),
701 arg(Arity, Value, Handle),
702 nonvar(Handle).none for a constructor without a shape. Fails on
any other value.712id_parts(Value, IdName, Shape, Handle) :- 713 compound(Value), 714 compound_name_arity(Value, IdName, Arity), 715 id_constructor(IdName, _, _), 716 id_arguments(Arity, Value, Shape, Handle). 717 718id_arguments(1, Value, none, Handle) :- 719 arg(1, Value, Handle). 720id_arguments(2, Value, Shape, Handle) :- 721 arg(1, Value, Shape), 722 arg(2, Value, Handle).
root_only, an unbound shape when Structure is
unknown, and otherwise the spelling of Structure.
730id_built(Name, Arity, Kind, Structure, Handle, Id) :-
731 id_name(Name, Arity, IdName),
732 ( Kind == root_only
733 -> compound_name_arguments(Id, IdName, [Handle])
734 ; Structure == unknown
735 -> compound_name_arguments(Id, IdName, [_, Handle])
736 ; structure_spelled(Structure, Shape),
737 compound_name_arguments(Id, IdName, [Shape, Handle])
738 ).745:- dynamic known_id_name/3. % known_id_name(Name, Arity, IdName) 746:- dynamic id_constructor/3. % id_constructor(IdName, Name, Arity) 747 748id_name(Name, Arity, IdName) :- 749 ( known_id_name(Name, Arity, Known) 750 -> IdName = Known 751 ; with_mutex(hash_consing_templates, id_name_made(Name, Arity, IdName)) 752 ). 753 754% id_name_made(+Name, +Arity, -IdName): the name looked up again under the 755% mutex `hash_consing_templates`, and made when it is still unknown. 756% id_constructor/3 is asserted before known_id_name/3, so a thread that 757% finds the name without the mutex also finds the fact id_parts/4 reads. 758 759id_name_made(Name, Arity, IdName) :- 760 ( known_id_name(Name, Arity, Known) 761 -> IdName = Known 762 ; format(atom(Made), '__hash_consed_~w/~d', [Name, Arity]), 763 assertz(id_constructor(Made, Name, Arity)), 764 assertz(known_id_name(Name, Arity, Made)), 765 IdName = Made 766 ). 767 768% ---- the templates of a constructor ---- 769 770% A TEMPLATE IS KEPT PARSED, `template(Name/Arity, Arguments)` with one of 771% `ignored` (for `_`), `root` (for `*`) or a nested template per argument, 772% ground, so that two declarations of a constructor compare with `==`. 773 774:- dynamic constructor_templates/4. % constructor_templates(Name, Arity, Templates, Kind)
781declared(Templates) :-
782 templates_declared(Templates, hash_consing:declared/1, _).root_only or shaped;
raises when the constructor has none.
789templates_known(Name, Arity, Context, Templates, Kind) :-
790 ( constructor_templates(Name, Arity, Templates, Kind)
791 -> true
792 ; throw(error(existence_error(templates, Name/Arity), context(Context, _)))
793 ).Name/Arity of the groups. Raises, registering nothing, on a malformed or
unreachable template and on a constructor already declared with another
list.803templates_declared(Templates, Context, Constructors) :- 804 must_be(list, Templates), 805 templates_parsed(Templates, Context, Parsed), 806 parsed_constructors(Parsed, [], Reversed), 807 reverse(Reversed, Constructors), 808 constructors_groups(Constructors, Parsed, Groups), 809 with_mutex(hash_consing_templates, groups_declared(Groups, Context)). 810 811% groups_declared(+Groups, +Context): the groups checked against the lists 812% already declared, then registered, as one step under the mutex 813% `hash_consing_templates`. Two threads declaring a constructor at once 814% would otherwise both find it undeclared and both register it (measured 815% 2026-09-30: eight threads declaring the same 2000 constructors left 3924 816% registrations and 3631 Id names, and is_id/1 answered once for each Id 817% name of its constructor, since id_parts/4 reads them). 818 819groups_declared(Groups, Context) :- 820 groups_checked(Groups, Context), 821 groups_registered(Groups). 822 823parsed_constructors([], Constructors, Constructors). 824parsed_constructors([_-template(Constructor, _) | Parsed], Seen, Constructors) :- 825 ( memberchk(Constructor, Seen) 826 -> parsed_constructors(Parsed, Seen, Constructors) 827 ; parsed_constructors(Parsed, [Constructor | Seen], Constructors) 828 ). 829 830% A GROUP is `group(Name/Arity, Written, Templates)`: the templates of one 831% constructor as written and as parsed, in the order of the list. 832 833constructors_groups([], _, []). 834constructors_groups([Constructor | Constructors], Parsed, [group(Constructor, Written, Templates) | Groups]) :- 835 constructor_templates_parsed(Parsed, Constructor, Written, Templates), 836 constructors_groups(Constructors, Parsed, Groups). 837 838constructor_templates_parsed([], _, [], []). 839constructor_templates_parsed([Template-Parsed | Pairs], Constructor, Written, Templates) :- 840 ( Parsed = template(Constructor, _) 841 -> Written = [Template | WrittenRest], 842 Templates = [Parsed | TemplatesRest] 843 ; Written = WrittenRest, 844 Templates = TemplatesRest 845 ), 846 constructor_templates_parsed(Pairs, Constructor, WrittenRest, TemplatesRest). 847 848groups_checked([], _). 849groups_checked([group(Name/Arity, Written, Templates) | Groups], Context) :- 850 templates_reachable(Written, Templates, [], Context), 851 ( constructor_templates(Name, Arity, Known, _), 852 Known \== Templates 853 -> templates_written(Known, KnownWritten), 854 throw(error(permission_error(reconfigure, interned_constructor, Name/Arity), 855 context(Context, configured(KnownWritten)-declared(Written)))) 856 ; true 857 ), 858 groups_checked(Groups, Context).
864templates_reachable([], [], _, _). 865templates_reachable([Template | Written], [Parsed | Templates], Earlier, Context) :- 866 ( member(EarlierTemplate, Earlier), 867 template_covers(EarlierTemplate, Parsed) 868 -> throw(error(domain_error(reachable_template, Template), context(Context, _))) 869 ; templates_reachable(Written, Templates, [Parsed | Earlier], Context) 870 ).
876template_covers(template(Constructor, Coverings), template(Constructor, Covereds)) :- 877 arguments_cover(Coverings, Covereds). 878 879arguments_cover([], []). 880arguments_cover([Covering | Coverings], [Covered | Covereds]) :- 881 argument_covers(Covering, Covered), 882 arguments_cover(Coverings, Covereds). 883 884argument_covers(ignored, _). 885argument_covers(root, _). 886argument_covers(template(Constructor, Coverings), template(Constructor, Covereds)) :- 887 arguments_cover(Coverings, Covereds). 888 889groups_registered([]). 890groups_registered([group(Name/Arity, _, Templates) | Groups]) :- 891 ( constructor_templates(Name, Arity, _, _) 892 -> true 893 ; templates_kind(Templates, Kind), 894 id_name(Name, Arity, _), 895 assertz(constructor_templates(Name, Arity, Templates, Kind)) 896 ), 897 groups_registered(Groups). 898 899% A constructor whose one template records nothing has Ids without a 900% shape; any other list has a template that records something. 901 902templates_kind([template(_, Arguments)], root_only) :- 903 all_ignored(Arguments), 904 !. 905templates_kind(_, shaped). 906 907all_ignored([]). 908all_ignored([ignored | Arguments]) :- 909 all_ignored(Arguments). 910 911compound_name_arguments_or_atom(Term, Name, Arguments) :- 912 ( atom(Term) 913 -> Name = Term, 914 Arguments = [] 915 ; compound_name_arguments(Term, Name, Arguments) 916 ).
Template-Parsed.922templates_parsed([], _, []). 923templates_parsed([Template | Templates], Context, [Template-Parsed | Pairs]) :- 924 template_parsed(Template, Context, Parsed), 925 templates_parsed(Templates, Context, Pairs). 926 927template_parsed(Template, Context, _) :- 928 var(Template), 929 !, 930 throw(error(instantiation_error, context(Context, _))). 931template_parsed(Template, Context, template(Name/Arity, Arguments)) :- 932 callable(Template), 933 Template \== (*), 934 !, 935 compound_name_arguments_or_atom(Template, Name, TemplateArguments), 936 length(TemplateArguments, Arity), 937 arguments_parsed(TemplateArguments, Context, Arguments). 938template_parsed(Template, Context, _) :- 939 throw(error(type_error(constructor_template, Template), context(Context, _))). 940 941arguments_parsed([], _, []). 942arguments_parsed([Argument | Arguments], Context, [Parsed | Parseds]) :- 943 argument_parsed(Argument, Context, Parsed), 944 arguments_parsed(Arguments, Context, Parseds). 945 946argument_parsed(Argument, _, ignored) :- 947 var(Argument), 948 !. 949argument_parsed(*, _, root) :- 950 !. 951argument_parsed(Argument, Context, Parsed) :- 952 template_parsed(Argument, Context, Parsed).
958templates_written([], []). 959templates_written([Template | Templates], [Written | Writtens]) :- 960 template_written(Template, Written), 961 templates_written(Templates, Writtens). 962 963template_written(template(Name/_, Arguments), Written) :- 964 arguments_written(Arguments, WrittenArguments), 965 ( WrittenArguments == [] 966 -> Written = Name 967 ; compound_name_arguments(Written, Name, WrittenArguments) 968 ). 969 970arguments_written([], []). 971arguments_written([Argument | Arguments], [Written | Writtens]) :- 972 argument_written(Argument, Written), 973 arguments_written(Arguments, Writtens). 974 975argument_written(ignored, _). 976argument_written(root, *). 977argument_written(template(Constructor, Arguments), Written) :- 978 template_written(template(Constructor, Arguments), Written). 979 980% ---- shapes ---- 981 982% A STRUCTURE is what a shape atom spells: a list, one description per 983% recorded argument, `other` for an argument that stands for a number or a 984% string and `constructor(Name/Arity, Inner)` otherwise, Inner being `none` 985% where nothing is recorded below and the list of the descriptions below 986% otherwise. While a value leaves a recorded root open its description is 987% `unknown`, and a structure holding one is not spelled. 988% 989% THE SAME COMPARISON serves the rewrite, on the source pattern of an 990% occurrence, and the store, on a term whose arguments are represented: a 991% value's root and arguments are read off a plain term or a pattern as they 992% are and off an Id through the store, and a variable, or the arguments of 993% an Id pattern whose handle is unbound, are open.
must_intern when a template matches every term Value stands
for, must_not_intern when no template matches any, undetermined
otherwise. Structure is what the first template that matches some such
term records, when that template matches them all and every root it
records is known; unknown otherwise.1003value_classified(Templates, Value, Class, Structure) :- 1004 templates_relations(Templates, Value, Relations), 1005 relations_class(Relations, Class), 1006 relations_structure(Relations, Structure). 1007 1008templates_relations([], _, []). 1009templates_relations([Template | Templates], Value, [Relation-Descriptions | Relations]) :- 1010 template_relation(Template, Value, Relation, Descriptions), 1011 templates_relations(Templates, Value, Relations). 1012 1013relations_class(Relations, must_intern) :- 1014 memberchk(instance-_, Relations), 1015 !. 1016relations_class(Relations, must_not_intern) :- 1017 \+ memberchk(overlap-_, Relations), 1018 !. 1019relations_class(_, undetermined). 1020 1021relations_structure([], unknown). 1022relations_structure([Relation-Descriptions | Relations], Structure) :- 1023 ( Relation == disjoint 1024 -> relations_structure(Relations, Structure) 1025 ; Relation == instance, 1026 descriptions_known(Descriptions) 1027 -> Structure = Descriptions 1028 ; Structure = unknown 1029 ). 1030 1031descriptions_known([]). 1032descriptions_known([Description | Descriptions]) :- 1033 description_known(Description), 1034 descriptions_known(Descriptions). 1035 1036description_known(other). 1037description_known(constructor(_, Inner)) :- 1038 ( Inner == none 1039 -> true 1040 ; is_list(Inner), 1041 descriptions_known(Inner) 1042 ).
1049first_template_recorded([Template | Templates], Term, Structure) :-
1050 template_relation(Template, Term, Relation, Descriptions),
1051 ( Relation == instance
1052 -> Structure = Descriptions
1053 ; first_template_recorded(Templates, Term, Structure)
1054 ).instance),
none does (disjoint) or some do (overlap), and what the template
records on them. Value's own root is the template's constructor.1062template_relation(template(_, TemplateArguments), Value, Relation, Descriptions) :- 1063 compound_name_arguments_or_atom(Value, _, Arguments), 1064 arguments_relation(TemplateArguments, Arguments, instance, Relation, [], Reversed), 1065 reverse(Reversed, Descriptions). 1066 1067arguments_relation([], [], Relation, Relation, Descriptions, Descriptions). 1068arguments_relation([TemplateArgument | TemplateArguments], [Argument | Arguments], 1069 Relation0, Relation, Descriptions0, Descriptions) :- 1070 argument_relation(TemplateArgument, Argument, ArgumentRelation, Descriptions0, Descriptions1), 1071 relation_combined(Relation0, ArgumentRelation, Relation1), 1072 arguments_relation(TemplateArguments, Arguments, Relation1, Relation, Descriptions1, Descriptions). 1073 1074argument_relation(ignored, _, instance, Descriptions, Descriptions). 1075argument_relation(root, Argument, instance, Descriptions, [Description | Descriptions]) :- 1076 value_root(Argument, Root), 1077 root_description(Root, Description). 1078argument_relation(template(Name/Arity, TemplateArguments), Argument, Relation, 1079 Descriptions, [Description | Descriptions]) :- 1080 value_view(Argument, View), 1081 nested_relation(View, Name, Arity, TemplateArguments, Relation, Description). 1082 1083nested_relation(open, Name, Arity, _, overlap, constructor(Name/Arity, unknown)). 1084nested_relation(other, Name, Arity, _, disjoint, constructor(Name/Arity, unknown)). 1085nested_relation(layer(ViewName/ViewArity, Arguments), Name, Arity, TemplateArguments, 1086 Relation, constructor(Name/Arity, Inner)) :- 1087 ( ViewName/ViewArity == Name/Arity 1088 -> arguments_relation(TemplateArguments, Arguments, instance, Relation, [], Reversed), 1089 reverse(Reversed, Descriptions), 1090 ( Descriptions == [] 1091 -> Inner = none 1092 ; Inner = Descriptions 1093 ) 1094 ; Relation = disjoint, 1095 Inner = unknown 1096 ). 1097 1098relation_combined(disjoint, _, disjoint) :- 1099 !. 1100relation_combined(_, disjoint, disjoint) :- 1101 !. 1102relation_combined(instance, instance, instance) :- 1103 !. 1104relation_combined(_, _, overlap). 1105 1106root_description(open, unknown). 1107root_description(other, other). 1108root_description(Name/Arity, constructor(Name/Arity, none)).
Name/Arity, read off
an Id's functor name without a lookup; open for a variable, other for
a number or a string.1116value_root(Value, open) :- 1117 var(Value), 1118 !. 1119value_root(Value, Name/Arity) :- 1120 id_parts(Value, IdName, _, _), 1121 !, 1122 id_constructor(IdName, Name, Arity). 1123value_root(Value, Name/Arity) :- 1124 compound(Value), 1125 !, 1126 compound_name_arity(Value, Name, Arity). 1127value_root(Value, Value/0) :- 1128 atom(Value), 1129 !. 1130value_root(_, other).
layer(Name/Arity, Arguments), the root and the arguments of the term
Value stands for, an Id's read from the store and open while its handle is
unbound; open for a variable; other for a number or a string.1138value_view(Value, open) :- 1139 var(Value), 1140 !. 1141value_view(Value, layer(Name/Arity, Arguments)) :- 1142 id_parts(Value, IdName, _, Handle), 1143 !, 1144 id_constructor(IdName, Name, Arity), 1145 ( nonvar(Handle) 1146 -> trie_term(Handle, Stored), 1147 compound_name_arguments_or_atom(Stored, _, Arguments) 1148 ; length(Arguments, Arity) 1149 ). 1150value_view(Value, layer(Name/Arity, Arguments)) :- 1151 compound(Value), 1152 !, 1153 compound_name_arguments(Value, Name, Arguments), 1154 length(Arguments, Arity). 1155value_view(Value, layer(Value/0, [])) :- 1156 atom(Value), 1157 !. 1158value_view(_, other).
1165structure_spelled(Structure, Atom) :- 1166 process_stores(_, Spellings), 1167 ( trie_lookup(Spellings, Structure, Known) 1168 -> Atom = Known 1169 ; with_mutex(hash_consing_spellings, spelling_inserted(Spellings, Structure, Atom)) 1170 ). 1171 1172% spelling_inserted(+Spellings, +Structure, -Atom): the spelling looked up 1173% again and, when it is still absent, made and inserted, under the mutex 1174% `hash_consing_spellings`. Two threads that both miss a structure would 1175% otherwise both insert it, and the second trie_insert/3 fails, which 1176% fails its interning (measured 2026-09-30: eight threads spelling the 1177% same 2000 shapes). 1178 1179spelling_inserted(Spellings, Structure, Atom) :- 1180 ( trie_lookup(Spellings, Structure, Known) 1181 -> Atom = Known 1182 ; structure_atom(Structure, Atom), 1183 trie_insert(Spellings, Structure, Atom) 1184 ).
- for other, Name/Arity
and, in parentheses, what is below.1191structure_atom(Structure, Atom) :- 1192 descriptions_texts(Structure, Texts), 1193 atomic_list_concat(Texts, ',', Atom). 1194 1195descriptions_texts([], []). 1196descriptions_texts([Description | Descriptions], [Text | Texts]) :- 1197 description_text(Description, Text), 1198 descriptions_texts(Descriptions, Texts). 1199 1200description_text(other, '-'). 1201description_text(constructor(Name/Arity, none), Text) :- 1202 !, 1203 format(atom(Text), '~w/~d', [Name, Arity]). 1204description_text(constructor(Name/Arity, Inner), Text) :- 1205 structure_atom(Inner, InnerText), 1206 format(atom(Text), '~w/~d(~w)', [Name, Arity, InnerText]). 1207 1208% ---- the boundary ----
1214externalized(Term, External) :- 1215 var(Term), 1216 !, 1217 External = Term. 1218externalized(Term, External) :- 1219 id_parts(Term, _, _, Handle), 1220 !, 1221 ( nonvar(Handle) 1222 -> trie_term(Handle, Stored), 1223 externalized(Stored, External) 1224 ; throw(error(instantiation_error, context(hash_consing:externalized/2, Term))) 1225 ). 1226externalized(Term, External) :- 1227 compound(Term), 1228 !, 1229 compound_name_arguments(Term, Name, Arguments), 1230 arguments_externalized(Arguments, ExternalArguments), 1231 compound_name_arguments(External, Name, ExternalArguments). 1232externalized(Term, Term). 1233 1234arguments_externalized([], []). 1235arguments_externalized([Argument | Arguments], [External | Externals]) :- 1236 externalized(Argument, External), 1237 arguments_externalized(Arguments, Externals).
1245internalized(Templates, External, TermWithIds) :- 1246 templates_declared(Templates, hash_consing:internalized/3, Constructors), 1247 term_internalized(External, Constructors, TermWithIds). 1248 1249term_internalized(Term, _, Internal) :- 1250 var(Term), 1251 !, 1252 Internal = Term. 1253term_internalized(Term, _, Internal) :- 1254 id_parts(Term, _, _, _), 1255 !, 1256 Internal = Term. 1257term_internalized(Term, Indicators, Internal) :- 1258 compound(Term), 1259 !, 1260 compound_name_arguments(Term, Name, Arguments), 1261 arguments_internalized(Arguments, Indicators, InternalArguments), 1262 compound_name_arguments(Rebuilt, Name, InternalArguments), 1263 compound_name_arity(Term, Name, Arity), 1264 instance_internalized(Name/Arity, Indicators, Rebuilt, Internal). 1265term_internalized(Term, Indicators, Internal) :- 1266 atom(Term), 1267 !, 1268 instance_internalized(Term/0, Indicators, Term, Internal). 1269term_internalized(Term, _, Term). 1270 1271arguments_internalized([], _, []). 1272arguments_internalized([Argument | Arguments], Indicators, [Internal | Internals]) :- 1273 term_internalized(Argument, Indicators, Internal), 1274 arguments_internalized(Arguments, Indicators, Internals). 1275 1276% An instance whose templates were given is represented; one that is not 1277% ground stays plain when no completion of it can match, and raises 1278% otherwise, as intern/2 does on a term that is not ground. 1279 1280instance_internalized(Name/Arity, Constructors, Layer, Internal) :- 1281 ( memberchk(Name/Arity, Constructors) 1282 -> ( ground(Layer) 1283 -> ( upserted(Layer, Id) 1284 -> Internal = Id 1285 ; Internal = Layer 1286 ) 1287 ; constructor_templates(Name, Arity, Templates, _), 1288 value_classified(Templates, Layer, Class, _), 1289 ( Class == must_not_intern 1290 -> Internal = Layer 1291 ; throw(error(instantiation_error, context(hash_consing:internalized/3, Layer))) 1292 ) 1293 ) 1294 ; Internal = Layer 1295 ). 1296 1297% ---- the directive ----
rewritten(Templates, []).layer at the argument positions that hold a LAYER and _
elsewhere. In every head and body goal of the file with that name
and arity, the term at a layer position keeps its own constructor as
written, a plain term, and only its arguments are rewritten, as
represented/2 reads its first argument. This is how a
file that opts in calls a predicate of a file that does not, one
that hands out or takes the plain layer of a listed constructor.1318:- dynamic registered_constructor/3. % registered_constructor(SourceFile, Name, Arity) 1319:- dynamic registered_layer_positions/4. % registered_layer_positions(SourceFile, Name, Arity, Positions) 1320 1321rewritten(Templates) :- 1322 rewritten_with_options(Templates, [], hash_consing:rewritten/1). 1323 1324rewritten(Templates, Options) :- 1325 rewritten_with_options(Templates, Options, hash_consing:rewritten/2). 1326 1327rewritten_with_options(Templates, Options, Context) :- 1328 must_be(list, Templates), 1329 must_be(list, Options), 1330 options_checked(Options, Context, Layered), 1331 ( prolog_load_context(source, File) 1332 -> true 1333 ; throw(error(context_error(nodirective, Context), _)) 1334 ), 1335 templates_declared(Templates, Context, Constructors), 1336 constructors_registered(Constructors, File), 1337 layered_registered(Layered, File, Context). 1338 1339options_checked([], _, []). 1340options_checked([Option | Options], Context, Layered) :- 1341 ( Option = layer_arguments(Skeletons), 1342 is_list(Skeletons) 1343 -> skeletons_read(Skeletons, Context, Layered, Rest), 1344 options_checked(Options, Context, Rest) 1345 ; throw(error(domain_error(rewritten_option, Option), context(Context, _))) 1346 ). 1347 1348% skeletons_read(+Skeletons, +Context, -Layered, ?Rest): each skeleton 1349% read as `layered(Name, Arity, Positions)`, its layer positions, in front 1350% of Rest. 1351 1352skeletons_read([], _, Rest, Rest). 1353skeletons_read([Skeleton | Skeletons], Context, [layered(Name, Arity, Positions) | Layered], Rest) :- 1354 ( compound(Skeleton), 1355 compound_name_arguments(Skeleton, Name, Arguments), 1356 layer_markers(Arguments, 1, Positions), 1357 Positions \== [] 1358 -> length(Arguments, Arity) 1359 ; throw(error(domain_error(layer_skeleton, Skeleton), context(Context, _))) 1360 ), 1361 skeletons_read(Skeletons, Context, Layered, Rest). 1362 1363% layered_registered(+Layered, +File, +Context): the layer positions 1364% recorded for File; a second skeleton of the same name and arity in one 1365% file must give the same positions. 1366 1367layered_registered([], _, _). 1368layered_registered([layered(Name, Arity, Positions) | Layered], File, Context) :- 1369 ( registered_layer_positions(File, Name, Arity, Known) 1370 -> ( Known == Positions 1371 -> true 1372 ; throw(error(permission_error(reconfigure, layer_arguments, Name/Arity), 1373 context(Context, _))) 1374 ) 1375 ; assertz(registered_layer_positions(File, Name, Arity, Positions)) 1376 ), 1377 layered_registered(Layered, File, Context). 1378 1379% layer_markers(+Arguments, +Position, -Positions): the positions, from 1380% Position on, of the arguments that are the atom `layer`; any other 1381% argument must be a variable. 1382 1383layer_markers([], _, []). 1384layer_markers([Argument | Arguments], Position, Positions) :- 1385 Next is Position + 1, 1386 ( Argument == layer 1387 -> Positions = [Position | Rest] 1388 ; var(Argument) 1389 -> Positions = Rest 1390 ), 1391 layer_markers(Arguments, Next, Rest). 1392 1393constructors_registered([], _). 1394constructors_registered([Name/Arity | Constructors], File) :- 1395 ( registered_constructor(File, Name, Arity) 1396 -> true 1397 ; assertz(registered_constructor(File, Name, Arity)) 1398 ), 1399 constructors_registered(Constructors, File). 1400 1401% ---- the rewrite ----
1408source_rewritten(Source, _) :- 1409 var(Source), 1410 !, 1411 fail. 1412source_rewritten(end_of_file, _) :- 1413 !, 1414 prolog_load_context(source, File), 1415 prolog_load_context(file, File), 1416 retractall(registered_constructor(File, _, _)), 1417 retractall(registered_layer_positions(File, _, _, _)), 1418 fail. 1419source_rewritten((:- _), _) :- 1420 !, 1421 fail. 1422source_rewritten((?- _), _) :- 1423 !, 1424 fail. 1425source_rewritten(Source, Rewritten) :- 1426 prolog_load_context(source, File), 1427 registered_constructor(File, _, _), 1428 !, 1429 clause_rewritten(Source, File, Candidate), 1430 Candidate \=@= Source, 1431 Rewritten = Candidate. 1432 1433clause_rewritten((Head --> Body), File, (Head --> Body)) :- 1434 !, 1435 listed_absent((Head --> Body), File, dcg_rule). 1436clause_rewritten((Head :- Body), File, Rewritten) :- 1437 !, 1438 rule_rewritten(Head, Body, File, Rewritten). 1439clause_rewritten(Head, File, Rewritten) :- 1440 rule_rewritten(Head, true, File, Rewritten).
1446rule_rewritten(Qualifier:Head0, Body0, File, (Qualifier:Head :- Body)) :- 1447 !, 1448 plain_rule_rewritten(Head0, Body0, File, Head, Body). 1449rule_rewritten(Head0, Body0, File, (Head :- Body)) :- 1450 plain_rule_rewritten(Head0, Body0, File, Head, Body). 1451 1452plain_rule_rewritten(Head0, Body0, File, Head, Body) :- 1453 file_layer_positions(Head0, File, Positions), 1454 arguments_of_abstracted(Head0, Positions, File, Head, Occurrences), 1455 term_singletons(Head0-Body0, Singletons), 1456 body_rewritten(Body0, rewriting(File, Singletons), RewrittenBody), 1457 head_scheduled(Occurrences, Head-RewrittenBody, RewrittenBody, Body).
occurrence(Class, Layer, Representation, Handle): Class id for an
occurrence that must be interned, its Representation an Id pattern and
Handle that pattern's handle, and undetermined(IdName) for an
undetermined one, its Representation and its Handle the fresh variable
written where it stood. An occurrence that must not be interned is its
layer, written in place, and is not listed.1472arguments_of_abstracted(Callable0, LayerPositions, File, Callable, Occurrences) :- 1473 compound(Callable0), 1474 !, 1475 compound_name_arguments(Callable0, Name, Arguments0), 1476 positioned_arguments_abstracted(Arguments0, 1, LayerPositions, File, Arguments, [], Reversed), 1477 compound_name_arguments(Callable, Name, Arguments), 1478 reverse(Reversed, BottomUp), 1479 occurrences_baked(BottomUp, Occurrences). 1480arguments_of_abstracted(Callable, _, _, Callable, []). 1481 1482% file_layer_positions(+Callable, +File, -Positions): the layer positions 1483% File registered for Callable's name and arity (rewritten/2), none when 1484% it registered none. 1485 1486file_layer_positions(Callable, File, Positions) :- 1487 ( compound(Callable), 1488 compound_name_arity(Callable, Name, Arity), 1489 registered_layer_positions(File, Name, Arity, Registered) 1490 -> Positions = Registered 1491 ; Positions = [] 1492 ). 1493 1494% positioned_arguments_abstracted(+Arguments0, +Position, +LayerPositions, 1495% +File, -Arguments, +Reversed0, -Reversed): arguments_abstracted/5, the 1496% argument at a layer position kept as a plain layer: its own 1497% constructor as written, its arguments abstracted. 1498 1499positioned_arguments_abstracted([], _, _, _, [], Reversed, Reversed). 1500positioned_arguments_abstracted([Argument0 | Arguments0], Position, LayerPositions, File, 1501 [Argument | Arguments], Reversed0, Reversed) :- 1502 ( memberchk(Position, LayerPositions) 1503 -> layer_abstracted(Argument0, File, Argument, Reversed0, Reversed1) 1504 ; abstracted(Argument0, File, Argument, Reversed0, Reversed1) 1505 ), 1506 Next is Position + 1, 1507 positioned_arguments_abstracted(Arguments0, Next, LayerPositions, File, 1508 Arguments, Reversed1, Reversed). 1509 1510layer_abstracted(Layer0, File, Layer, Reversed0, Reversed) :- 1511 ( compound(Layer0) 1512 -> compound_name_arguments(Layer0, Name, Arguments0), 1513 arguments_abstracted(Arguments0, File, Arguments, Reversed0, Reversed), 1514 compound_name_arguments(Layer, Name, Arguments) 1515 ; Layer = Layer0, 1516 Reversed = Reversed0 1517 ). 1518 1519arguments_abstracted([], _, [], Reversed, Reversed). 1520arguments_abstracted([Argument0 | Arguments0], File, [Argument | Arguments], Reversed0, Reversed) :- 1521 abstracted(Argument0, File, Argument, Reversed0, Reversed1), 1522 arguments_abstracted(Arguments0, File, Arguments, Reversed1, Reversed).
1530abstracted(Term, _, Term, Reversed, Reversed) :- 1531 var(Term), 1532 !. 1533abstracted(Term0, File, Term, Reversed0, Reversed) :- 1534 compound(Term0), 1535 !, 1536 compound_name_arguments(Term0, Name, Arguments0), 1537 arguments_abstracted(Arguments0, File, Arguments, Reversed0, Reversed1), 1538 compound_name_arguments(Layer, Name, Arguments), 1539 compound_name_arity(Term0, Name, Arity), 1540 occurrence_abstracted(Name, Arity, Term0, Layer, File, Term, Reversed1, Reversed). 1541abstracted(Term0, File, Term, Reversed0, Reversed) :- 1542 atom(Term0), 1543 !, 1544 occurrence_abstracted(Term0, 0, Term0, Term0, File, Term, Reversed0, Reversed). 1545abstracted(Term, _, Term, Reversed, Reversed).
1552occurrence_abstracted(Name, Arity, Source, Layer, File, Representation, Reversed0, Reversed) :- 1553 ( registered_constructor(File, Name, Arity) 1554 -> constructor_templates(Name, Arity, Templates, Kind), 1555 value_classified(Templates, Source, Class, Structure), 1556 occurrence_represented(Class, Name, Arity, Kind, Structure, Layer, Representation, 1557 Reversed0, Reversed) 1558 ; Representation = Layer, 1559 Reversed = Reversed0 1560 ). 1561 1562occurrence_represented(must_intern, Name, Arity, Kind, Structure, Layer, Id, 1563 Reversed, [occurrence(id, Layer, Id, Handle) | Reversed]) :- 1564 id_built(Name, Arity, Kind, Structure, Handle, Id). 1565occurrence_represented(must_not_intern, _, _, _, _, Layer, Layer, Reversed, Reversed). 1566occurrence_represented(undetermined, Name, Arity, _, _, Layer, Variable, 1567 Reversed, [occurrence(undetermined(IdName), Layer, Variable, Variable) | Reversed]) :- 1568 id_name(Name, Arity, IdName).
1576occurrences_baked([], []). 1577occurrences_baked([Occurrence | Occurrences], Remaining) :- 1578 Occurrence = occurrence(Class, Layer, Representation, _), 1579 ( ground(Layer) 1580 -> occurrence_represented_now(Class, Layer, Representation), 1581 occurrences_baked(Occurrences, Remaining) 1582 ; Remaining = [Occurrence | Rest], 1583 occurrences_baked(Occurrences, Rest) 1584 ). 1585 1586occurrence_represented_now(id, Layer, Id) :- 1587 intern(Layer, Id). 1588occurrence_represented_now(undetermined(_), Layer, Variable) :- 1589 represented(Layer, Variable).
1597head_scheduled([], _, Body, Body) :- 1598 !. 1599head_scheduled(BottomUp, Clause, Body0, (Condition -> Fast ; General)) :- 1600 reverse(BottomUp, TopDown), 1601 top_level_occurrences(BottomUp, BottomUp, TopLevel), 1602 occurrences_given(TopLevel, Condition), 1603 occurrences_read(TopDown, Clause-BottomUp, Read), 1604 occurrences_goal(Read, related, Lookups), 1605 occurrences_goal(TopDown, related_if_given, Settled), 1606 occurrences_goal(BottomUp, represented_if_ground, Inserted), 1607 occurrences_goal(BottomUp, settled_unless_given, Finished), 1608 conjoined([Lookups, Body0], Fast), 1609 conjoined([Settled, Inserted, Body0, Finished], General).
true left out.1615conjoined(Goals, Conjunction) :- 1616 goals_kept(Goals, Kept), 1617 kept_conjoined(Kept, Conjunction). 1618 1619goals_kept([], []). 1620goals_kept([Goal | Goals], Kept) :- 1621 ( Goal == true 1622 -> Kept = Rest 1623 ; Kept = [Goal | Rest] 1624 ), 1625 goals_kept(Goals, Rest). 1626 1627kept_conjoined([], true). 1628kept_conjoined([Goal], Goal) :- 1629 !. 1630kept_conjoined([Goal | Goals], (Goal, Conjunction)) :- 1631 kept_conjoined(Goals, Conjunction). 1632 1633% occurrences_read(+Occurrences, +Clause, -Read): the occurrences whose 1634% layer the clause reads. A given Id's layer is looked up only to bind the 1635% layer's arguments; an Id occurrence whose layer is its constructor over 1636% variables that occur nowhere else in Clause (the head, the body and the 1637% layers of the listed occurrences) binds nothing anyone reads, and is left 1638% out of the fast path. Its Id pattern in the head already selects the 1639% constructor and the shape. Every other occurrence is kept. 1640 1641occurrences_read(Occurrences, Clause, Read) :- 1642 variables_occurring_more_than_once(Clause, Shared), 1643 occurrences_kept_when_read(Occurrences, Shared, Read). 1644 1645occurrences_kept_when_read([], _, []). 1646occurrences_kept_when_read([Occurrence | Occurrences], Shared, Read) :- 1647 ( Occurrence = occurrence(id, Layer, _, _), 1648 layer_arguments(Layer, Arguments), 1649 unread_arguments(Arguments, Shared) 1650 -> Read = Rest 1651 ; Read = [Occurrence | Rest] 1652 ), 1653 occurrences_kept_when_read(Occurrences, Shared, Rest). 1654 1655layer_arguments(Layer, Arguments) :- 1656 ( atom(Layer) 1657 -> Arguments = [] 1658 ; compound_name_arguments(Layer, _, Arguments) 1659 ). 1660 1661unread_arguments([], _). 1662unread_arguments([Argument | Arguments], Shared) :- 1663 var(Argument), 1664 \+ variable_member(Argument, Shared), 1665 unread_arguments(Arguments, Shared). 1666 1667% variables_occurring_more_than_once(+Term, -Shared): the variables of Term 1668% that occur in it more than once. 1669 1670variables_occurring_more_than_once(Term, Shared) :- 1671 term_singletons(Term, Singletons), 1672 term_variables(Term, Variables), 1673 variables_not_in(Variables, Singletons, Shared). 1674 1675variables_not_in([], _, []). 1676variables_not_in([Variable | Variables], Excluded, Kept) :- 1677 ( variable_member(Variable, Excluded) 1678 -> Kept = Rest 1679 ; Kept = [Variable | Rest] 1680 ), 1681 variables_not_in(Variables, Excluded, Rest). 1682 1683% An occurrence is at the top when its handle is in the layer of no other 1684% listed occurrence; the layer of an occurrence that must not be interned 1685% is written in place and does not hide what it holds. 1686 1687top_level_occurrences([], _, []). 1688top_level_occurrences([Occurrence | Occurrences], All, TopLevel) :- 1689 Occurrence = occurrence(_, _, _, Handle), 1690 ( nested_in_another(Handle, All) 1691 -> TopLevel = Rest 1692 ; TopLevel = [Occurrence | Rest] 1693 ), 1694 top_level_occurrences(Occurrences, All, Rest). 1695 1696nested_in_another(Handle, [occurrence(_, Layer, _, _) | Occurrences]) :- 1697 ( term_variables(Layer, Variables), 1698 variable_member(Handle, Variables) 1699 -> true 1700 ; nested_in_another(Handle, Occurrences) 1701 ). 1702 1703variable_member(Variable, [Candidate | Candidates]) :- 1704 ( Variable == Candidate 1705 -> true 1706 ; variable_member(Variable, Candidates) 1707 ).
1714occurrences_given([Occurrence], Condition) :- 1715 !, 1716 occurrence_given(Occurrence, Condition). 1717occurrences_given([Occurrence | Occurrences], (Condition, Conditions)) :- 1718 occurrence_given(Occurrence, Condition), 1719 occurrences_given(Occurrences, Conditions). 1720 1721% An Id pattern is given when its handle is bound; an undetermined 1722% occurrence when its variable is bound and not to an Id pattern of its 1723% constructor whose handle is unbound. The Ids of a constructor whose 1724% interning can be undetermined always have a shape. 1725 1726occurrence_given(occurrence(id, _, _, Handle), nonvar(Handle)). 1727occurrence_given(occurrence(undetermined(IdName), _, Variable, _), 1728 (nonvar(Variable), (Variable = Id -> nonvar(Handle) ; true))) :- 1729 compound_name_arguments(Id, IdName, [_, Handle]).
1735occurrences_goal([], _, true). 1736occurrences_goal([Occurrence], Step, Goal) :- 1737 !, 1738 occurrence_goal(Step, Occurrence, Goal). 1739occurrences_goal([Occurrence | Occurrences], Step, (Goal, Goals)) :- 1740 occurrence_goal(Step, Occurrence, Goal), 1741 occurrences_goal(Occurrences, Step, Goals). 1742 1743% The steps: `related` relates an occurrence to its layer; `related_if_given` 1744% does so when it is given; `represented_if_ground` represents it when it is 1745% not given and its layer is ground; `settled_unless_given` represents it at 1746% the end of a head, where it must be settled; `represented_before_call` 1747% interns a ground one and decides an undetermined one before a goal is 1748% called. 1749 1750occurrence_goal(related, occurrence(id, Layer, Id, _), 1751 hash_consing:intern(Layer, Id)). 1752occurrence_goal(related, occurrence(undetermined(_), Layer, Variable, _), 1753 hash_consing:represented(Layer, Variable)). 1754occurrence_goal(related_if_given, occurrence(id, Layer, Id, Handle), 1755 (nonvar(Handle) -> hash_consing:intern(Layer, Id) ; true)). 1756occurrence_goal(related_if_given, Occurrence, 1757 (Given -> hash_consing:represented(Layer, Variable) ; true)) :- 1758 Occurrence = occurrence(undetermined(_), Layer, Variable, _), 1759 occurrence_given(Occurrence, Given). 1760occurrence_goal(represented_if_ground, occurrence(id, Layer, Id, Handle), 1761 ((var(Handle), ground(Layer)) -> hash_consing:intern(Layer, Id) ; true)). 1762occurrence_goal(represented_if_ground, Occurrence, 1763 ((\+ Given, ground(Layer)) -> hash_consing:represented(Layer, Variable) ; true)) :- 1764 Occurrence = occurrence(undetermined(_), Layer, Variable, _), 1765 occurrence_given(Occurrence, Given). 1766occurrence_goal(settled_unless_given, occurrence(id, Layer, Id, Handle), 1767 (var(Handle) -> hash_consing:intern(Layer, Id) ; true)). 1768occurrence_goal(settled_unless_given, Occurrence, 1769 (Given -> true ; hash_consing:represented_settled(Layer, Variable))) :- 1770 Occurrence = occurrence(undetermined(_), Layer, Variable, _), 1771 occurrence_given(Occurrence, Given). 1772occurrence_goal(represented_before_call, occurrence(id, Layer, Id, _), 1773 (ground(Layer) -> hash_consing:intern(Layer, Id) ; true)). 1774occurrence_goal(represented_before_call, occurrence(undetermined(_), Layer, Variable, _), 1775 hash_consing:represented(Layer, Variable)).
rewriting(File, Singletons), Singletons the variables that
occur once in the source clause.
The control constructs rewritten inside, every other goal by goal_rewritten/3.
1785body_rewritten(Goal, _, Goal) :- 1786 var(Goal), 1787 !. 1788body_rewritten((Left0, Right0), Context, (Left, Right)) :- 1789 !, 1790 body_rewritten(Left0, Context, Left), 1791 body_rewritten(Right0, Context, Right). 1792body_rewritten((Left0 ; Right0), Context, (Left ; Right)) :- 1793 !, 1794 body_rewritten(Left0, Context, Left), 1795 body_rewritten(Right0, Context, Right). 1796body_rewritten((Condition0 -> Then0), Context, (Condition -> Then)) :- 1797 !, 1798 body_rewritten(Condition0, Context, Condition), 1799 body_rewritten(Then0, Context, Then). 1800body_rewritten((Condition0 *-> Then0), Context, (Condition *-> Then)) :- 1801 !, 1802 body_rewritten(Condition0, Context, Condition), 1803 body_rewritten(Then0, Context, Then). 1804body_rewritten(\+ Goal0, Context, \+ Goal) :- 1805 !, 1806 body_rewritten(Goal0, Context, Goal). 1807body_rewritten(call(Goal0), Context, call(Goal)) :- 1808 !, 1809 body_rewritten(Goal0, Context, Goal). 1810body_rewritten(forall(Condition0, Action0), Context, forall(Condition, Action)) :- 1811 !, 1812 body_rewritten(Condition0, Context, Condition), 1813 body_rewritten(Action0, Context, Action). 1814body_rewritten(findall(Template, Goal0, Result), Context, findall(Template, Goal, Result)) :- 1815 !, 1816 context_file(Context, File), 1817 listed_absent(Template-Result, File, findall_template_or_result), 1818 body_rewritten(Goal0, Context, Goal). 1819body_rewritten(catch(Goal0, Catcher, Recovery0), Context, catch(Goal, Catcher, Recovery)) :- 1820 !, 1821 context_file(Context, File), 1822 listed_absent(Catcher, File, catch_catcher), 1823 body_rewritten(Goal0, Context, Goal), 1824 body_rewritten(Recovery0, Context, Recovery). 1825body_rewritten(hash_consing:Goal0, Context, hash_consing:Goal) :- 1826 templates_first(Goal0), 1827 !, 1828 templates_kept(Goal0, Context, Goal). 1829body_rewritten(hash_consing:Goal0, Context, hash_consing:Goal) :- 1830 layer_first(Goal0), 1831 !, 1832 goal_rewritten(Goal0, [1], Context, Goal). 1833body_rewritten(Qualifier:Goal0, Context, Qualifier:Goal) :- 1834 !, 1835 body_rewritten(Goal0, Context, Goal). 1836body_rewritten(Goal0, Context, Goal) :- 1837 templates_first(Goal0), 1838 prolog_load_context(module, Module), 1839 predicate_property(Module:Goal0, imported_from(hash_consing)), 1840 !, 1841 templates_kept(Goal0, Context, Goal). 1842body_rewritten(Goal0, Context, Goal) :- 1843 layer_first(Goal0), 1844 prolog_load_context(module, Module), 1845 predicate_property(Module:Goal0, imported_from(hash_consing)), 1846 !, 1847 goal_rewritten(Goal0, [1], Context, Goal). 1848body_rewritten(Goal0, Context, Goal) :- 1849 context_file(Context, File), 1850 file_layer_positions(Goal0, File, Positions), 1851 goal_rewritten(Goal0, Positions, Context, Goal). 1852 1853% templates_first(+Goal): Goal is a predicate of this library whose first 1854% argument is a list of templates. The templates are data the library 1855% reads, not terms to intern, so a call of it in an opted-in file keeps 1856% them as written (templates_kept/3), and only its other arguments are 1857% rewritten. 1858 1859templates_first(internalized(_, _, _)). 1860templates_first(declared(_)). 1861 1862templates_kept(Goal0, Context, Goal) :- 1863 compound_name_arguments(Goal0, Name, [Templates | Arguments]), 1864 compound_name_arguments(StandIn, Name, [Placeholder | Arguments]), 1865 goal_rewritten(StandIn, [], Context, Goal), 1866 Placeholder = Templates. 1867 1868% layer_first(+Goal): Goal is a predicate of this library whose first 1869% argument is a layer, kept as a plain layer in an opted-in file. 1870 1871layer_first(represented(_, _)). 1872 1873context_file(rewriting(File, _), File).
1886goal_rewritten(Goal0, LayerPositions, rewriting(File, Singletons), Goal) :- 1887 arguments_of_abstracted(Goal0, LayerPositions, File, Called, BottomUp), 1888 occurrences_partitioned(BottomUp, Singletons, Read, Unread), 1889 goal_scheduled(Read, Called, Scheduled), 1890 unread_checks(Unread, Checks), 1891 conjoined([Scheduled | Checks], Goal). 1892 1893occurrences_partitioned([], _, [], []). 1894occurrences_partitioned([Occurrence | Occurrences], Singletons, Read, Unread) :- 1895 ( Occurrence = occurrence(id, Layer, _, _), 1896 layer_arguments(Layer, Arguments), 1897 Arguments \== [], 1898 singleton_arguments(Arguments, Singletons) 1899 -> Unread = [Occurrence | UnreadRest], 1900 Read = ReadRest 1901 ; Read = [Occurrence | ReadRest], 1902 Unread = UnreadRest 1903 ), 1904 occurrences_partitioned(Occurrences, Singletons, ReadRest, UnreadRest). 1905 1906singleton_arguments([], _). 1907singleton_arguments([Argument | Arguments], Singletons) :- 1908 var(Argument), 1909 variable_member(Argument, Singletons), 1910 singleton_arguments(Arguments, Singletons). 1911 1912unread_checks([], []). 1913unread_checks([occurrence(id, Layer, Id, Handle) | Occurrences], 1914 [(nonvar(Handle) -> true ; hash_consing:intern(Layer, Id)) | Checks]) :- 1915 unread_checks(Occurrences, Checks). 1916 1917goal_scheduled([], Called, Called) :- 1918 !. 1919goal_scheduled(BottomUp, Called, (ground(Variables) -> Fast ; General)) :- 1920 reverse(BottomUp, TopDown), 1921 terms_variables(BottomUp, Variables), 1922 occurrences_goal(BottomUp, related, Inserts), 1923 occurrences_goal(BottomUp, represented_before_call, Settled), 1924 occurrences_goal(TopDown, related, Finished), 1925 conjoined([Inserts, Called], Fast), 1926 conjoined([Settled, Called, Finished], General).
1932terms_variables(Occurrences, Variables) :- 1933 occurrences_terms(Occurrences, Terms), 1934 term_variables(Terms, All), 1935 occurrences_handles(Occurrences, Handles), 1936 variables_without(All, Handles, Variables). 1937 1938occurrences_terms([], []). 1939occurrences_terms([occurrence(_, Layer, _, _) | Occurrences], [Layer | Terms]) :- 1940 occurrences_terms(Occurrences, Terms). 1941 1942occurrences_handles([], []). 1943occurrences_handles([occurrence(_, _, _, Handle) | Occurrences], [Handle | Handles]) :- 1944 occurrences_handles(Occurrences, Handles). 1945 1946variables_without([], _, []). 1947variables_without([Variable | Variables], Excluded, Kept) :- 1948 ( variable_member(Variable, Excluded) 1949 -> Kept = Rest 1950 ; Kept = [Variable | Rest] 1951 ), 1952 variables_without(Variables, Excluded, Rest).
1959listed_absent(Term, File, Place) :- 1960 ( listed_occurs(Term, File) 1961 -> throw(error(domain_error(term_without_rewritten_constructor, Term), 1962 context(hash_consing:rewritten/1, Place))) 1963 ; true 1964 ). 1965 1966listed_occurs(Term, File) :- 1967 compound(Term), 1968 !, 1969 compound_name_arity(Term, Name, Arity), 1970 ( registered_constructor(File, Name, Arity) 1971 -> true 1972 ; arg(_, Term, Argument), 1973 listed_occurs(Argument, File) 1974 ). 1975listed_occurs(Term, File) :- 1976 atom(Term), 1977 registered_constructor(File, Term, 0). 1978 1979% ---- the hook, last, so that this file's own clauses are read before it 1980% is active ---- 1981 1982:- multifile user:term_expansion/2. 1983:- dynamic user:term_expansion/2. 1984 1985userterm_expansion(Source, Rewritten) :- 1986 hash_consing:source_rewritten(Source, Rewritten). 1987 1988% The message of a constructor with no templates names the two ways to 1989% give it some. 1990 1991:- multifile prolog:error_message//1. 1992 1993prologerror_message(resource_error(hash_consing_store)) --> 1994 [ 'The hash_consing store passed the limit set by the flag'-[], nl, 1995 'hash_consing_store_limit, or this thread passed the growth budget'-[], nl, 1996 'set by hash_consing:store_growth_bounded/1; the store keeps every'-[], nl, 1997 'term interned so far.'-[] 1998 ]. 1999prologerror_message(existence_error(templates, Name/Arity)) --> 2000 [ 'No templates for the constructor ~q in this process.'-[Name/Arity], nl, 2001 'Declare them with hash_consing:declared/1, or list them in the'-[], nl, 2002 'hash_consing:rewritten/1 directive of the files that use them.'-[] 2003 ]
hash_consing: hash-consed terms selected by templates, whose Ids record a 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 that files and declared/1 declare.
HOW TO USE IT. A file is written as the program that does not intern, loads this library and names, by templates, the terms to intern:
:- module(steps, [step/2, built/2]). :- use_module(library(hash_consing), []). :- hash_consing:rewritten([apply(lambda(_), _), lambda(_), variable(_)]). step(apply(lambda(Body), Argument), beta(Body, Argument)). built(Function, Applied) :- Applied = apply(Function, variable(0)).A term is interned when a template of its constructor matches it (TEMPLATES ARE PATTERNS, below); here an application is interned when its first argument is a lambda, and any other application stays a plain compound whose arguments are represented in turn. 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(_), _), 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 testtest/hash_consing.pltloads the fixtures in `test/hash_consing_fixtures/` and compares what the rewrite makes of them with the snapshots in `test/hash_consing_snapshots/`, 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 subterms are represented 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. It fails, inserting nothing, when no template of Term's constructor matches Term (user, 2026-09-26: a term that matches no template is not interned, even when its constructor has templates), and raises
existence_error(templates, Name/Arity)when the constructor has no template in this process, which is most likely a missing declaration (user, 2026-09-26, approving the change). 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, represented/2, is_id/1 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 patternapply(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,
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 term'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). Under the templates
apply(lambda(_), _)andapply(closure(_), _)the heads above becomestep('__hash_consed_apply/2'('lambda/1', Handle))andstep('__hash_consed_apply/2'('closure/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 of the constructor's templates, so the Handle determines it and the store stays canonical.TEMPLATES ARE PATTERNS, in one syntax for every constructor (user, 2026-09-20: one syntax, not three; user, 2026-09-26: hash_consing is based on patterns, not on constructors). An element of a list of templates is a constructor applied to one of these at each argument position (a constructor of arity zero is its atom):
_ any argument; nothing is recorded * any argument; its root constructor is recorded a nested template an argument whose root constructor is the template's and whose arguments match the template's arguments; its root is recorded, and what the template records below it*records the root constructor of the term the argument stands for,Name/Arity, whether that term is interned or not, and-for a number or a string. There is no alternative inside a template, and;is an ordinary constructor: the alternatives of a constructor are several templates in the list, because Prolog's;is a disjunction whose two sides can both hold, and templates are not predicates (user, 2026-09-26).A CONSTRUCTOR'S TEMPLATES ARE TRIED IN ORDER, the order of the list, and the first that matches a term is the term's template; the templates after it are not looked at, and a term has one template or none (user, 2026-09-26: the parts where templates overlap use the first matching template; matching several templates at once has no meaning). The shape is what that template records, spelled as the recorded descriptions joined by commas,
''when it records nothing. Underapply(apply(*, _), _)thenapply(*, _), the termapply(apply(lambda(B), Y), X)has the shape'apply/2(lambda/1)'andapply(lambda(B), X)the shape'lambda/1'; underapply(apply(*, _), _)alone the second is not interned. A template that one earlier template always takes first raises while it is declared, so a constructor whose templates record nothing has the one template `Name(_, ..., _)`, and its Ids have no shape; the Ids of any other constructor have one. Two templates that record different positions can spell the same atom for different terms, which only keeps the index from telling their clauses apart.MATCHING AND RECORDING READ THE TERM, not how its subterms are represented: an argument that is an Id is read through the store and an argument that is a plain compound as it is, and both give the same root and arguments. At a
*the root of an Id is read off its functor name, with no lookup; a nested template costs one trie_term/2 on an argument that is an 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.ONE LIST OF TEMPLATES PER CONSTRUCTOR, PER PROCESS: the first declaration of a constructor, by rewritten/1, declared/1 or internalized/3, fixes its list, and another declaration with other templates or another order raises
permission_error(reconfigure, interned_constructor, Name/Arity), because whether a term is interned and what its shape is must agree wherever it is handed (agent design decision, 2026-09-20, for one template, and 2026-09-26 for a list, in plans the user approved; 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). A constructor is never declared by being met.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.A TERM THAT IS NOT INTERNED STAYS AS IT IS (user, 2026-09-26): a plain compound of its constructor, whose arguments are represented in turn. The REPRESENTATION of a term is its Id when a template of its constructor matches it, and the term itself, with represented arguments, otherwise.
THE BOUNDARY: externalized/2 replaces every Id by its term, recursively, for printing, for writing a file and for a content address. internalized/3 declares the templates it is given and represents, bottom up, the instances of their constructors, 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
=..). declared/1 only declares templates, for a program whose constructors are made at run time. is_id/1 tells an Id from any other value. `represented(?Layer, ?Representation)` relates one layer, a constructor applied to represented arguments, to its representation.A LAYER POSITION holds a layer, not a representation: in a file that opts in, the term written there keeps its own constructor as a plain term and only its arguments are rewritten. The first argument of represented/2 is one; rewritten/2's option
layer_argumentsnames more, in the heads and goals of the file, for a predicate of a module that does not opt in and hands out or takes the plain layer of a listed constructor (a reader of one layer). A variable there stays a variable.:- 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). It declares the templates as declared/1 does, and every clause read after it in that file, included files counted, has each occurrence of a constructor of the list rewritten as below, 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, each declaring whole lists. 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 (user ruling, 2026-09-17). SHARE ONE DIRECTIVE BY include/1: keep the directive in one file that every file handing these terms to another includes; an included directive opts in the file that includes it. A file left out fails silently (the FIXME below).AN OCCURRENCE IS CLASSIFIED while the file loads, by comparing its source pattern with each template of its constructor: every instance of the pattern matches the template, none does, or some do.
must intern a template matches every instance: an Id pattern and a call of `intern/2`; the shape is written in when the first template that some instance matches matches them all and each root it records is in the pattern must not intern no template matches any instance: the layer itself, written where the occurrence stood undetermined otherwise: a fresh variable where the occurrence stood, and a call of `represented/2`A pattern whose variables are all filled with an atom no template names is taken by a template only when that template has _ or
*there, and such a template takes every instance, so no occurrence is missed as must intern. The other two tests treat each occurrence of a variable apart, so a pattern with a repeated variable can be classified undetermined, or left without its shape, although its instances decide it; that is correct and unindexed. An undetermined occurrence at the top of a head costs its clause the first argument indexing of that argument: the clause is a candidate for every call, so a call that a clause before it answers leaves a choice point on it (measured 2026-09-26: with such a clause first in its predicate, calls on an Id, a plain term and the other constructors' Ids were deterministic; with it last, each of them left a choice point). Such a clause is not split into an Id version and a plain version: called with that argument unbound, both would run the body, and a cut in the first would prune the second.THE REWRITE OF A CLAUSE:
head fast path when every top-level occurrence is given (the handle of an Id pattern bound; the variable of an undetermined occurrence bound, and not to an Id pattern whose handle is unbound): relate them to their layers top down, then run the body, which keeps the last call; otherwise relate the given ones top down, represent the ground ones bottom up, run the body, represent the rest bottom up (an instantiation error when a term that must be interned is still not ground, or an undetermined one is still undecided) body goal when the variables of its layers are bound: represent bottom up and call; otherwise intern the ground ones and decide each undetermined one (its Id, an Id pattern whose handle is unbound, or the layer itself; an instantiation error when still undecided), 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). An Id occurrence over variables that occur once in the clause is unread: its Id pattern alone is passed, and after the call only its handle is checked ground an occurrence that is ground in the source is represented while the file loads, and its Id or its layer is written into the clauseControl 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 whose interning is undetermined is decided when it is handed to a goal: what is bound of it settles whether a template matches every completion or none does, or the call raises an instantiation error; and every instance is settled, ground or its Id bound, by the end of the goal it is handed to. The first half is a DOWNGRADE of the rule before 0.2.0, which asked only for the second: a term that matches no template stays as it is (user, 2026-09-26), and a callee could not otherwise tell whether to receive an Id or a plain term. (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); the standard order of two Ids depends on the history of insertions into the store, not on their terms. Code that reads structure calls externalized/2 first, and code that builds an instance with them calls internalized/3 after; an instance that must be interned and is 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.THE STORE BELONGS TO THE PROCESS. The store and the spellings of the shapes are two tries made when this module is first loaded and kept in the fact process_stores/2, so every thread and every engine interns into the same store and reads the same Ids, with nothing to install. Reloading the module keeps them. A lookup that finds its key with its handle written back takes no lock; every write to the store, a value still
pendingsettled included, is made under a mutex. The Prolog flaghash_consing_store_limit,infiniteby default, bounds the bytes of the store: an insertion that would grow it past the limit raisesresource_error(hash_consing_store), the way the stack and table limits do, and the store stays as it was. store_growth_bounded/1 bounds instead what one thread adds to the store from the call on, the way the stack and table limits bound one thread, so a thread that shares the store with others, or finds it already full of what earlier work interned, is charged with its own insertions only. A declaration, an insertion into the store and a new spelling each take a mutex (hash_consing_templates,hash_consing_store,hash_consing_spellings) and look again under it before they write. store_property/1 reads the size of the store.AN ID IS NEVER FREED NOR REUSED. The store only grows: an Id stays valid, and stands for the same term, until the process ends. Two Ids are
==exactly when the terms they stand for are==, so an Id is a key of its term.AN INTERNED TERM IS A VALUE, NOT A CELL. intern/2 stores a copy of the term as it is at the call: a later setarg/3 or nb_setarg/3 on the term that was interned, or on a layer read back from an Id, changes that copy only, never the store, and interning the old value again answers the same Id. So a mutable cell (a thunk, a memory cell updated in place) must stay outside the store; interning it freezes its value at that moment. An Id itself must never be mutated: an Id whose handle is set to another Id's handle is that other Id, and one set to any other integer crashes the process (the FIXME below on trie_term/2).
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: 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.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 does not check it; one directive shared by include/1 (above) avoids it.