This repository was archived by the owner on Sep 20, 2026. It is now read-only.
Repository navigation
Expand file tree
/
Copy pathext_points.pl
More file actions
2392 lines (2273 loc) · 129 KB
/
Copy pathext_points.pl
File metadata and controls
2392 lines (2273 loc) · 129 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
% Guarantees: metta_with_trailed/3 is published as a host_service
% [source: engine/ext_points.pl:kind/2; commit=40b71fc99571872ca5fc85cdaf7902b467166539].
% Guarantees: a `:- seam:context_reader(Head, Key, Shape)` declaration defines
% the reader and compiles every resolving call to its nb_current/2 read, at
% the inferences of the dynamic fact the reader replaced; a malformed shape
% or key refuses at load [tested: trailed_scopes:every_declared_reader_is_compiled_to_its_read,
% trailed_scopes:a_call_site_carries_the_read_rather_than_a_call,
% trailed_scopes:an_inactive_reader_costs_what_the_asserted_guard_cost,
% trailed_scopes:a_malformed_reader_declaration_refuses_at_load; commit=3ff7688a605c1f0de0e021f66f3075353476a992].
%
% Purpose: declare each engine extension seam, its direction and its cut
% semantics, and publish the predicates extensions and host bindings may call.
% Guarantees: namespace registration is a published host service distinct
% from value species [tested: run_tests(space_registration); commit=a8e3fc42306377adf7cae0a331f3d92fbf190304].
% Guarantees: context_reader/4 defines a scoped reader and compiles resolving
% calls directly to its read; malformed declarations refuse at load
% [tested: reference_scopes; commit=9b0a084e534ddf7dd67980ad84c27c8279b877f1].
% Guarantees: metta_transaction/2 publishes the result-aware transaction service
% [tested: classes_transaction_results; commit=9b0a084e534ddf7dd67980ad84c27c8279b877f1].
% Guarantees: grounded_length/2 lets the value's owner answer a length query
% independently of its structural view [tested:
% refinements:a_length_provider_does_not_read_structure; commit=9b0a084e534ddf7dd67980ad84c27c8279b877f1].
% Guarantees: allocation, release and held-goal context hooks let lib_thread
% own scope lifetimes across host engines [tested: lib_thread_scope;
% commit=c6e1198c490a824b96f6fc6e1c0622a542917024].
% Assumes: space_releasing/1 owners tolerate repeated preparation across the
% preliminary clear, final release and retries [tested: release_preparation,
% lib_thread_cancellation; commit=0891c522503ca9856fb654f306364f4ae9736b22].
% Guarantees:
% - extensions share the engine's console rendering and IEEE exception
% recovery [tested: engine_modules:every_declared_service_is_exported_to_the_host,
% lib_string_surface:template_uses_engine_rendering_and_host_grammar,
% lib_vector_surface:nonfinite_reductions; commit=b7866b4d874879ff0cb212eb1c6af60dddaa39c6].
% - metta_apply_algebra_operation/5 exposes the native carrier operation
% semantics to host bindings [tested:
% test_visibility_operations_share_the_native_carrier; commit=90ba93eb8f6e98ebfefc55416859bf13de6a8427].
% - evaluation context and ordered-match demand are published engine
% services [tested: run_tests(evaluation_context); commit=54cb2eee69c42c1ae685643cbe2578f8d617a265].
% - metta_import_record/2 and metta_unimport/2 expose one source lifecycle
% to libraries [tested: lib_import_lifecycle; commit=4f2d6c0f8eb293b73f8dde30a1c84e24834f7393].
% - libraries can distinguish an author's annotated effect from inferred
% operation metadata [tested: run_tests(metta_arrow_products); commit=bbb512316280110a747e31c26adfc31e8c5104be].
% - host query carriers enter the engine-owned algebra scope, read its
% effective or explicitly selected carrier and identity, and compose
% operation-answer weights only through declared host services
% [tested: a_host_binding_calls_only_published_surface,
% test_the_host_service_scoreboard_matches_the_tree,
% test_two_annotated_operation_calls_multiply_all_four_joint_weights;
% commit=2e627a593413191cda3170f2eb716835f7f62543].
% - atom events raised inside an observation frame are retained in write
% order, merged into an enclosing frame on nested commit, published only
% after the outer commit, and discarded on rollback [tested:
% test_events_publish_only_after_transaction_commit,
% test_rollback_and_outer_rollback_discard_every_buffered_event,
% test_speculative_execution_discards_its_event_segment; commit=39092863ae34184a9f955f185ff57c1ff177ec40].
% - a committed segment announces its boundary once, after every one of its
% atom events has been dispatched, carrying the sorted space names it
% touched; an unscoped write is a segment of one and a discarded frame
% announces nothing [tested:
% test_a_transaction_delivers_one_progress_after_its_deltas,
% test_a_discarded_segment_announces_no_boundary; commit=0de0dc08d2fc77bee9dd132c41f1de23cda1e6c2].
% - deferred commit callbacks run after earlier committed events even when a
% subscriber fails, while rollback runs every paired discard callback even
% when an earlier discard raises [tested:
% test_a_failed_launch_watcher_does_not_strand_committed_async_work,
% test_a_rolled_back_async_launch_never_starts_or_lands,
% every_deferred_discard_runs_before_the_first_error_is_rethrown;
% commit=39092863ae34184a9f955f185ff57c1ff177ec40].
% - reader-token registration is an engine-owned host service, while token
% construction is claimed by the host that owns the registered callable;
% mapping introspection is an ordinary extension service [tested:
% test_a_registered_token_class_parses_like_a_shipped_one,
% every_seam_declares_one_kind,
% every_seam_kind_matches_its_direction; commit=2c741dda928a30d0ce1c7e1fcf0b263b4d1bb97b].
% - every handler seam lives in THIS module, so an extension writes
% seam:atom_added/2 and the module carries the namespace the metta_on_
% prefix used to carry [tested: test_every_seam_is_reached_under_its_module;
% commit=dd407a40f623b16eda0bb51a74458f7dd3760e21].
% - automatic-cache graph, source-boundary, policy, explanation and support
% seams each declare their event/declaration/service direction explicitly
% [tested: every_seam_kind_matches_its_direction,
% test_a_doubly_branching_recursion_is_tabled_automatically_and_a_tail_recursion_is_not;
% commit=9e7d5dc2cad810940e5386d52636ac6946df279d].
% - host repeatability asks one engine-owned effect-classification service
% rather than reaching the walk's private queue predicates [tested:
% a_host_binding_calls_only_published_surface,
% test_the_host_service_scoreboard_matches_the_tree; commit=6917bef7ca902671999eafcae3a7a86db8f69723].
% - declaring a seam is priced the same whether or not its defining file has
% loaded, so the boot sweep costs the size of the table rather than a
% library-index search per row [tested:
% a_missing_definition_is_priced_like_a_present_one]
% [measured 2026-09-06: engine/bench.pl bench_run(boot) 543,929 to
% 240,641, and thirty added kind/2 rows 124,185 to 1,755;
% commit=8ec7de241ef3cdd2753f24a97c86e9e9c7240b06].
% - finite algebra equality is a host-owned decision with an explicit false answer
% [tested: test_finite_tensor_semiring_checks_every_law; commit=074dc0a88b1605c54824de677d586b6f60998bcf].
% - a consumer that MIRRORS a catalog row hears every write to its head,
% including a removal whose head was left unbound, and hears nothing for a
% head it did not ask about [tested: run_tests(catalog_watch);
% commit=c26b6a4d28ef8fb50742440feed2c0578ebb0f58]
% - watching one head costs 1 inference on a '&metta' write and nothing on a
% write to any other space [measured 2026-09-08: 36.02 to 37.02 inferences
% per '&metta' write, 27.02 either way per '&self' write and 375.07 either
% way per equation;
% command=python extensions/python/benchmarks/probes/bound_row_cost.py --write;
% fixture=300 writes per arm against a control checkout at
% 9006528e04dfcc6bf3c7f43cd77a7816ad0223d7; commit=c26b6a4d28ef8fb50742440feed2c0578ebb0f58]
% Open Obligations:
% To Do: None
% Hacks: None
% Future Enhancements: None
%The module IS the namespace, which is why the names below are short. Every
%handler seam used to wear a prefix that did a module's job: metta_on_ for the
%events, metta_foreign_ for the space-provider protocol, metta_grounded_ for
%the grounded-value protocol, metta_host_ for the host's. The prefix was the
%only namespace there was, and being a convention it could not refuse
%anything: two libraries could declare the same seam name and corrupt each
%other by import order, and nothing said which module a handler belonged to.
%
%Now an extension writes
%
% :- multifile seam:atom_added/2.
% seam:atom_added(Space, Atom) :- ...
%
%which is SWI's own hook shape, the one prolog:message//1 has always used
%[source: SWI-Prolog 10.1 Reference Manual, section 4.10 and library(error)'s
%error:has_type/2]. The old spellings are GONE rather than aliased: an alias
%tier would be a second name for one thing, which the tree's ladder refuses,
%and compatibility against our own Prolog surface is not a constraint.
%
%Nothing here is imported into the engine's module. engine/metta.pl loads this
%file with an empty import list, so `seam:` is not optional and cannot decay
%back into a bare name that happens to resolve. The export list below is
%therefore the DECLARED surface rather than what anyone can reach, which is
%what the layering lane and the published-surface walk both ask for.
:- module(seam,
[ % The extension-point table itself, and what it decides.
kind/2,
clauses_from/2,
every_clause_runs/1,
publish/1,
publish_declared/0,
seam_home/2,
% Events: the engine tells, every handler runs.
atom_added/2,
atom_removed/2,
space_created/1,
space_releasing/1,
space_released/1,
space_access/1,
space_dependency/2,
host_engine_created/1,
host_engine_released/1,
catalog_row_changed/2,
segment_committed/1,
cache_policy_changed/1,
forget_derived/0,
function_call_graph_changed/2,
function_changed/1,
function_clauses_changed/1,
function_removed/1,
source_program_compiled/0,
backend_selftest/0,
% The atom-write wrappers those events ride on.
enable_atom_hook/1,
disable_atom_hook/1,
atom_hook_clause/2,
atom_hook_changed/3,
sync_atom_hook/1,
write_door_module/2,
% Post-commit observation frames and provider publication.
observation_begin/0,
observation_commit/0,
observation_discard/0,
observation_defer/2,
observation_frames/1,
observe/3,
% Declarations: fact tables the engine reads as data.
transaction_constraint/1,
extension_builtin/2,
builtin_type_declaration/2,
context_reader/4,
context_events/3,
engine_context/1,
engine_emitted/1,
foreign_capability/2,
grounded_extra_type/2,
automatic_cache_explanation/3,
interposed_dispatch/4,
pure_operation/1,
seeded_operation/1,
route_cap/4,
% Ownership: the first handler that succeeds claims the request.
custom_match/2,
dispatch_call/4,
effect_operation_name/3,
form_rewriter/1,
matchable_value/1,
pattern_modifier/3,
% The foreign-space provider protocol.
foreign_add/2,
foreign_add_many/2,
foreign_atoms/2,
foreign_token/3,
foreign_add_token/3,
foreign_remove_token/3,
foreign_clear/1,
foreign_erring/5,
foreign_match/3,
foreign_plan/5,
foreign_participant/3,
foreign_pushdown/3,
foreign_refuse/2,
foreign_remove/3,
foreign_space/1,
% The grounded-value protocol.
grounded_applicable/1,
grounded_apply/4,
grounded_algebra_equal/3,
grounded_algebra_type/3,
grounded_class_type/2,
grounded_length/2,
grounded_numeric/1,
grounded_numeric_operation/3,
grounded_structure/2,
grounded_text/2,
grounded_type_names/2,
% The host protocol's handler half; its service half is the
% engine's own predicates, reached under the subsystem that
% defines them.
host_add_hooks_idle/2,
atom_hook_ref_idle/2,
host_transport_failure/1,
host_error_reason/2,
compiled_source/1,
host_import/1,
host_object/1,
host_reader_token_construct/3,
host_remove_hooks_idle/2
]).
% Assumes: metta_engine:goal_expansion/2 is visible while clauses compile.
% Set the base before the clauses and their engine-dependent directives.
% [source: https://github.com/SWI-Prolog/swipl-devel/blob/fc7ef84b949378b729052c3ade79c90ce5416abb/boot/expand.pl#L239; commit=ede2ac57e213a0d4502c6bbbca6227f97015b720]
:- set_module(base(metta_engine)).
% Every listener this file registers goes through the engine's one door, which
% registers once, takes no name and holds no mutex while SWI takes the
% channel's event-list lock (engine/host_listeners.pl).
:- use_module(host_listeners, [metta_listen/2]).
%%%% What kind of seam each extension point is %%%%
%
%Every seam below is declared multifile and then given a KIND on the line
%after it. The kind is the load-bearing fact about a seam, because a cut means
%opposite things in the three of them, and it lived in this comment until a
%checker had to restate it by hand. A restated list drifts, and this one had:
%the prose named five event hooks, omitting backend_selftest/0 and
%wrongly including dispatch_call/4, and both were contradicted by their
%own call sites. So it is data now, and the prose derives from it.
%
%EVENT: run for an effect and the answer discarded, forall(Hook, true). Every
%handler runs.
%
%OWNERSHIP: consulted for an answer, and the first handler that succeeds
%CLAIMS the request; the caller takes it with ->/2 or once/1, and a provider
%declines by failing.
%
%DECLARATION: a fact table the engine reads as data rather than calls for an
%effect. Every clause has to stay readable for the same reason an event
%handler has to stay reachable.
%
%SERVICE: the other direction. The three kinds above are all HANDLER seams,
%where an extension writes the clauses and the engine calls them; a service is
%a predicate the ENGINE defines and an extension is allowed to CALL. A foreign
%space backend needs one: it speaks text over a wire, so it has to turn a term
%into text and back, and before this kind existed it reached into
%engine/parser.pl to do it. SQLite publishes the same half of its own contract
%and for the same reason, handing an extension an sqlite3_api_routines table
%of the host functions it may call so that an extension never links against
%internals [source: https://www.sqlite.org/vtab.html and loadext.html]. Naming
%the surface is what makes "reaches past the seam" a question a checker can
%answer, and two extensions had already answered it wrongly: morkspaces.pl and
%extensions/python/metta/_binding/shim.pl each wrapped metta_unwritable_symbol/2 under a private
%name of its own, which is what an undeclared dependency looks like from the
%outside [measured 2026-08-17].
%
%The cut rule follows from the kind rather than being a second list to keep.
%In an OWNERSHIP seam a clause guarded by a test that establishes "this
%request is mine" may cut freely, and lib/lib_redis/lib_redis.pl does:
%redis_space_conn(Space, _) fails for a space redis does not own, so a later
%provider's clauses are untouched, and the cut is a real optimisation there.
%In an EVENT or DECLARATION seam every clause must stay reachable, so a cut
%prunes that predicate's remaining clauses and silently disables every handler
%loaded after it. lib/lib_tabling/lib_tabling.pl cut after metta_tabling_declared, a
%GLOBAL CONDITION rather than an ownership test: nothing about it says this
%handler is the one that should answer. With tabling declared, duals.pl's
%invalidation handler (asserted last, so ordered last) never ran and
%(not-provable (pq 2)) answered True and False at once.
%
%Write ( Condition -> Action ; true ) in an event handler, which keeps the
%guard's cost and prunes nothing. A cut is transparent through ,/2, ;/2 and
%the THEN branch of ->/2 and *->/2, and opaque everywhere else, including the
%CONDITION of ->/2, where the manual's own worked example is
%`t3 :- (a, !, b -> c ; d)` pruning a/0 and not t3/0
%[source: SWI-Prolog manual, !/0, scope-of-the-cut table]. So a checker that
%flags a cut in a condition is flagging correct code.
%
%Two checks enforce it and neither subsumes the other. A source scan reads
%every clause in the tree, including one a directive asserts, and a runtime
%scan reads clause/3 after the libraries have loaded, which is the only way to
%see a handler that Python installs or one whose body is built at run time
%[tested: tests/prolog/static_checks.pl, no_cut_in_an_event_hook and
%no_cut_in_a_live_hook_clause].
%
%kind/2 is itself multifile, so a library that introduces a seam of
%its own declares its kind beside it and gets the same gate. Every seam has
%exactly one kind and that is checked rather than trusted
%[tested: every_seam_declares_one_kind], so a seam added without one fails the
%gate instead of going quietly unchecked.
:- multifile kind/2.
%It is a seam itself, so it carries its own kind, and being a declaration it
%is covered by the cut check like any other.
kind(kind/2, declaration).
%The names the engine writes into compiled bodies and therefore binds into
%every space's module. Declared here because it is a seam in both directions:
%an extension that teaches the engine to emit a goal of its own names it, and
%the engine reads the whole table when it protects a space's module. Its
%clauses live in engine/translator.pl, beside the translation rules that emit
%them.
:- multifile engine_emitted/1.
kind(engine_emitted/1, declaration).
%Libraries contribute builtin arrows without replacing the engine's table.
%It is a declaration seam, so every contributed clause remains reachable
%[tested: test_a_library_types_its_own_blob_without_destroying_the_table;
%commit=65d5fff90323fb92e2415f9fe93c477d5c67f10e].
:- multifile builtin_type_declaration/2.
kind(builtin_type_declaration/2, declaration).
%Pattern modifiers are expression lists claimed by shape. The engine replaces
%the modifier position with a fresh variable and runs the owner's guard after
%matching, so an extension can add a structural view without teaching the
%store a new term kind. The lifting walk is a host service because a binding
%that constructs patterns must apply the same semantics as compiled match.
%[tested: test_a_path_reaches_into_a_handle_without_converting_it;
%commit=b54ecaaa1224eabb90f808275003cd9abeef8065].
:- multifile pattern_modifier/3.
kind(pattern_modifier/3, ownership).
%Who writes a seam's clauses. This is the primitive the cut rule derives from,
%rather than the cut rule naming kinds directly: that rule is about a handler
%an extension contributed staying reachable, so it can only bite where an
%extension contributes the clauses. A service's clauses are the engine's own
%and cut freely, as swrite/2 does; reading the rule off the kind list alone
%would have called every one of them an offender.
clauses_from(event, extension).
clauses_from(ownership, extension).
clauses_from(declaration, extension).
clauses_from(service, engine).
clauses_from(host_service, engine).
%A seam whose clauses must all stay reachable: contributed by an extension,
%and not an ownership seam where the first success is meant to claim the
%request. Derived, so adding a kind does not mean editing a second list.
every_clause_runs(Seam) :-
kind(Seam, Kind),
clauses_from(Kind, extension),
Kind \== ownership.
%A handler seam is multifile because an extension adds clauses to it. A
%service is not, because an extension calling it must not be able to redefine
%it, and multifile is exactly the permission to try. The two directions are
%checked apart for that reason [tested: every_seam_kind_matches_its_direction].
%CALL DISPATCH: a handler is offered every compiled call site and either
%CLAIMS it, by binding Goal to something the engine runs instead, or fails and
%the ordinary call proceeds. Failing is the shipped default and the whole of
%what a handler must do to opt out.
%
%It was named metta_memoized_dispatch_call/4, for the first library that used
%it, and the name was the problem: nothing suggested it was the general
%dispatch seam, so nothing reached for it to do anything else. lib_memo binds
%Goal to a cache lookup, and it is still the only handler in the tree. This is
%Trino's applyX / Optional.empty() shape, and it was here before that reading.
%
%A function name alone does not identify a function, because a named space
%compiles its equations into a module of its own, so a handler that keeps
%state per function reads current_metta_module/1 to learn which module the
%call site is in. It reads it rather than being passed it because this hook is
%consulted on every compiled call site.
:- multifile dispatch_call/4.
kind(dispatch_call/4, ownership).
%Function-change hooks, run once per compiled equation. Dynamic for the same
%reason the atom hooks below are: a handler needed only once a feature is used
%should cost nothing until then, so it is installed when that feature first
%runs rather than when its file loads. A resident handler clause costs four
%inferences on EVERY compiled equation [measured 2026-08-15: engine/duals.pl's
%invalidation handler, 4001 on source-load's thousand equations].
:- multifile function_changed/1.
kind(function_changed/1, event).
%DROP every answer a library derived earlier and would serve again.
%
% forget_derived
%
%A library that answers a call from something it computed before -- a memo, a
%table, a materialised view -- holds state no digest can see, and until this
%seam a caller had no way to ask for it back. Replaying a recorded run is that
%caller: a recording pins the atoms with a digest and the draws with a seed,
%and this is the third thing the re-run has to start from. Without it a replay
%of a memoised head is a different execution with the same answers -- measured
%2026-09-07, `!(fib 6)` recording 22 events and replaying 2 in the engine that
%recorded it, every call after the first answered from the cache.
%
%An EVENT, so every handler runs: the derived answers of one program can be
%held by several libraries and dropping one library's is not dropping the
%state. It is total rather than per space or per function, which is the same
%argument the tracer's teardown makes: a reset that is sometimes partial is a
%silent divergence, and the caller asking for it wants the engine as cold as
%it can be made.
:- multifile forget_derived/0.
kind(forget_derived/0, event).
%The compiled half of the change story, run once per compiled equation AFTER
%its clause and provenance are in place. function_changed above is the
%DEFINITION event: it fires when an equation arrives whether or not the engine
%has translated it yet, which under deferred translation can be well before
%any clause exists. A handler that acts on the compiled predicate, wrapping it
%the way the tracer does, listens here instead, because at definition time
%there may be nothing to wrap and at materialisation time nothing else fires.
:- multifile function_clauses_changed/1.
kind(function_clauses_changed/1, event).
:- multifile function_call_graph_changed/2.
kind(function_call_graph_changed/2, event).
:- multifile function_removed/1.
kind(function_removed/1, event).
:- multifile cache_policy_changed/1.
kind(cache_policy_changed/1, event).
:- multifile source_program_compiled/0.
kind(source_program_compiled/0, event).
%A deferred function's clauses now stand AND nothing is mid-translation.
%function_call_graph_changed/2 above fires while the function is still inside
%its own compilation guard, so a handler that RECOMPILES on the news cannot
%act on it there: it would recompile the predicate its caller is in the middle
%of building. This is the settle point after that, fired once per
%materialisation rather than once per equation, and it is where the automatic
%cache decides -- early enough to reach the first call, which is the whole
%reason the decision cannot wait for the source's flush.
:- multifile deferred_translation_settled/0.
kind(deferred_translation_settled/0, event).
:- dynamic function_changed/1.
:- dynamic function_clauses_changed/1.
:- dynamic function_call_graph_changed/2.
:- dynamic function_removed/1.
:- dynamic cache_policy_changed/1.
:- dynamic source_program_compiled/0.
:- dynamic deferred_translation_settled/0.
%Automatic caching decisions are extension-owned declarations. The core's
%explain door enumerates them, while lib_memo owns the state and reasons.
:- multifile automatic_cache_explanation/3.
kind(automatic_cache_explanation/3, declaration).
%Space writes: every 'add-atom'/3, 'remove-atom'/3 and 'subtract-atom'/3 runs
%these hooks with the space and the term, after the write. A standing query, a subscription,
%an index or a mirror hangs off them; with no handlers nothing changes.
%A removal hook fires only when something was actually removed, once PER
%OCCURRENCE, and it carries the occurrence that left rather than the term the
%caller asked about. `(remove-atom &s (p $x))` drains every atom matching
%`(p $x)` (engine/spaces/foreign.pl, remove_matching_atoms/2), and it drains
%them by reading them first and then removing each by name, so what reaches
%the hook is a ground atom the space held and a handler no longer has to
%re-read the space to find out which. It carried the caller's pattern and
%fired once until 2026-08-30, when removal stopped being multiset
%subtraction, which 'subtract-atom'/3 carries as its own head now: it fires
%this hook exactly once, for the single occurrence it took.
%extensions/python/metta/structures.py's LiveView is the worked
%instance [tested: test_liveview_mirrors_the_space].
:- multifile atom_added/2.
kind(atom_added/2, event).
:- multifile atom_removed/2.
kind(atom_removed/2, event).
% Lifetime events are independent of atom writes and transaction observation.
% Creation fires once at allocation. Release preparation joins dependants
% before either clearing phase and may repeat on retry. Its owners must be
% idempotent. Final retirement follows successful storage teardown; inside a
% transaction it waits for the outer native outcome and the captured foreign
% participants, and an aborted retirement never fires it.
:- multifile space_created/1, space_releasing/1, space_released/1, space_access/1.
kind(space_created/1, event).
kind(space_releasing/1, event).
kind(space_released/1, event).
kind(space_access/1, event).
% A closure captured on the caller and applied around a held engine's goal.
% Every declared context composes; no host duplicates the context stack.
:- multifile engine_context/1.
kind(engine_context/1, declaration).
:- multifile host_engine_created/1, host_engine_released/1.
kind(host_engine_created/1, event).
kind(host_engine_released/1, event).
% Spaces that must remain live while a dependent space is returned from a scope.
:- multifile space_dependency/2.
kind(space_dependency/2, declaration).
:- dynamic atom_added/2.
:- dynamic atom_removed/2.
%One catalog head's rows landing or leaving '&metta', for a consumer that
%MIRRORS a row it reads on a hot path. Event is added or removed and Row is
%the row as a list; a removal by pattern leaves positions unbound and is
%announced once per watched head, because over-announcing refreshes a mirror
%for nothing while under-announcing leaves one wrong.
%
%It exists beside the two hooks above rather than inside them because their
%price is the wrong shape for this. A single atom_added/2 clause wraps the
%write door for EVERY space, which cost 16 inferences on every '&self' write
%and 33 on every '&metta' one, measured against the same tree with no handler
%[measured 2026-09-08: 43.02 against 27.02 and 70.03 against 37.02;
%command=python extensions/python/benchmarks/probes/bound_row_cost.py --subscription;
%fixture=200 writes per arm, both arms in one process]. This one is
%read off the catalog's own note funnel, which only '&metta' writes reach,
%and it is guarded by watch_catalog_rows/1, so a head nobody watches costs one
%indexed lookup that fails.
%
%The shape is PostgreSQL's: the catalog is authoritative, pg_settings is a
%view of it, and an assign hook updates the fast copy at the write rather than
%making every reader consult the catalog [source: PostgreSQL documentation,
%20.1 Setting Parameters, and src/backend/utils/misc/guc.c's assign_hook].
%extensions/python/metta/_catalog/bounds.py is the worked instance: it mirrors the
%`(limit <name> <value>)` bounds it reads once per cursor, where reading them
%through a crossing cost 21 inferences and 3.2 microseconds per cursor
%[tested: test_a_bound_read_after_the_first_costs_no_crossing].
:- multifile catalog_row_changed/2.
kind(catalog_row_changed/2, event).
:- dynamic catalog_row_changed/2.
%The END of one committed segment, with the sorted list of space names its
%events touched. The two hooks above say WHAT changed, one call per atom; this
%one says THAT IS ALL, once per commit, after every one of those calls has
%returned. An unscoped write is a segment of one; a transaction is a segment of
%its whole ordered diff; a rolled-back or speculative one has no segment at all,
%because it has no committed events.
%
%A consumer that maintains a derived answer needs the boundary and not only the
%events. The diff of a transaction is already applied and committed when its
%FIRST event is delivered, so a handler that recomputes per event recomputes N
%times over one unchanging state and keeps the first answer; recomputing at the
%boundary is the same answer for one recomputation. This is Materialize's
%SUBSCRIBE progress row, which carries a timestamp and no data and whose whole
%content is "there are no more updates for either timestamp 2 or 3"
%[source: https://materialize.com/docs/sql/subscribe/, the PROGRESS option].
%extensions/python/metta/structures.py's Live is the worked instance: its
%`heads` and `tabled` strategies mark themselves stale per event and re-answer
%here, and its `progress` delta is this hook crossing
%[tested: test_a_transaction_delivers_one_progress_after_its_deltas].
%
%The list is computed only when a handler exists, so a tree with none pays one
%clause lookup per commit.
:- multifile segment_committed/1.
kind(segment_committed/1, event).
:- dynamic segment_committed/1.
%Foreign spaces: a host runtime may declare a space whose atoms live outside
%the Prolog database, in a database, a dataframe, a service. match/4,
%'add-atom'/3, 'remove-atom'/3 and 'get-atoms'/2 consult these hooks first
%for a declared name; with no declarations nothing changes.
%
%THE NAME AN ATOMIC SPACE CARRIES BEGINS WITH '&'. That is the engine's rule
%and not this seam's invention: metta_space_name/1 refuses any other spelling
%at the door that CREATES a space, metta_require_space_name/2 refuses it at
%new-space and inherits, the Python seat's register_provider refuses it before
%it reaches here, and neither wire codec can decode a space name without it.
%metta_space_operand/1 therefore tests the prefix before probing either
%registry, which is what makes it cheap on the matcher, get-metatype, the
%three type-candidate resolvers, operation admission, the translator and the
%codec. A clause here naming an atom without the prefix is answered NO by all
%of them, quietly, so tests/prolog/static_checks.pl scans the live database
%and refuses one by name. A PARAMETRIC space is named by a nonempty ground
%list instead and carries no prefix; the guard is on the atom case alone.
:- multifile foreign_space/1.
kind(foreign_space/1, ownership).
%THE OWNERSHIP-GUARD PROTOCOL, which every clause of the five capability
%hooks below obeys and which the conformance kit now depends on: a hook
%clause takes the space as its FIRST argument and its body's LEADING goal is
%the ownership test that decides whether this provider serves that space
%(mork_owns_space/1, redis_space_conn/7, metta_py_foreign/1 are the three
%shipped spellings). The guard is a pure lookup, so anything may call it to
%ask "does this provider serve this space" without performing the operation.
%
%What made this worth writing down: a check that asked whether the hook
%PREDICATE had clauses answered yes for every provider as soon as ANY
%provider implemented it, so a declaration with nothing behind it stopped
%being caught the day MORK gained its own clear hook. The predicate having
%clauses is a receipt; a clause whose guard admits THIS space is the payload.
%Both tests below run with a rival provider present, which is what makes
%them able to tell the two questions apart
%[tested: conformance_catches_a_capability_with_no_hook,
%readying_refuses_a_declared_capability_with_no_hook_clauses;
%commit=938744d2c4d718ea78358825b3079df7c20c9b16].
:- multifile foreign_match/3.
%A declared error mode's stream: like foreign_match/3, with the
%mode enforced on the provider's own host, where its exceptions are
%native. Item is `answer` (the pattern is bound), kept(ErrorAtom), or
%`end` from an adapter that must mark exhaustion. Only adapters whose
%host exceptions cannot cross as Prolog exceptions implement this; a
%Prolog-hosted provider needs none, the engine's catch handles it.
:- multifile foreign_erring/5.
% A pure ownership lookup returns the selected registration's ground Identity
% and a module-qualified Capture closure. Calling Capture with one additional
% argument binds transaction(Begin, Commit, Rollback), three qualified goals
% retaining that provider. Capture runs once, outside locks, before Begin at
% the first write for Space/Identity. Completion never resolves Space again.
% A replacement registration has another identity, even under the same name.
% [source: engine/metta/space_hooks.pl:metta_enlist_foreign/1; commit=05fae56ad5b23baa140cb4e6454cb7b304c06f4f]
:- multifile foreign_participant/3.
kind(foreign_match/3, ownership).
kind(foreign_erring/5, ownership).
kind(foreign_participant/3, ownership).
%Custom matching for grounded values, Hyperon's CustomMatch: a host value
%may carry its own matching logic, consulted by metta_match_atoms/2 when
%that value meets a non-variable operand inside `unify`. The hook
%enumerates one solution per binding set, binding the other operand's
%variables; failure means no match. Variables always bind the value
%whole without consulting it, and values with no owner fall through to
%ground equality, so with no declarations nothing changes.
:- multifile matchable_value/1.
:- multifile custom_match/2.
kind(matchable_value/1, ownership).
kind(custom_match/2, ownership).
:- multifile foreign_add/2.
kind(foreign_add/2, ownership).
%A provider's own BATCH crossing, optional. The atoms arrive as a list and the
%provider stores them however it likes; one without this clause gets a
%foreign_add/2 per atom, which is what every provider written before it
%gets. The hooks are the provider's, exactly as they are for its per-atom add.
%
%A batch is a TRANSPORT optimisation and never a semantic one, so the engine
%routes only atoms whose add is a store and nothing more through here. That is
%not advice to the provider, it is enforced upstream: an equation or a type
%declaration in the list drops the whole batch to 'add-atom'/3 per atom.
:- multifile foreign_add_many/2.
kind(foreign_add_many/2, ownership).
:- multifile foreign_remove/3.
kind(foreign_remove/3, ownership).
:- multifile foreign_atoms/2.
kind(foreign_atoms/2, ownership).
% One stable occurrence identity and its candidate atom. A provider may return
% extra candidates; the seat unifies each against the offered pattern.
:- multifile foreign_token/3.
kind(foreign_token/3, ownership).
% Optional exact mutation. add-token stores one Atom and returns its fresh
% portable token. remove-token consumes only Token and returns true iff it
% existed. Both follow the provider's existing transaction and hook contract.
% [tested: reference_providers; commit=90ba93eb8f6e98ebfefc55416859bf13de6a8427]
:- multifile foreign_add_token/3, foreign_remove_token/3.
kind(foreign_add_token/3, ownership).
kind(foreign_remove_token/3, ownership).
%Clear was the sixth of these all along and was declared nowhere: it lived in
%extensions/python/metta/_binding/shim.pl, so a Prolog provider that implemented clear, as
%lib/lib_redis/lib_redis.pl does, was reachable only when Python was in the process.
:- multifile foreign_clear/1.
kind(foreign_clear/1, ownership).
%What the caller will do with a match. Options is a list; the only option
%today is limit(N), meaning the caller stops after N answers. It is `[]` when
%there is nothing to say, which is most calls.
%
%It is ADVISORY, and that is what makes it sound. A provider may
%over-approximate, so N candidates are not N answers, and a provider that
%truncated at N without knowing which of its candidates unify would
%under-answer, which is the one thing the contract forbids. So honour it only
%when you can tell an exact match from a candidate, and ignore it otherwise:
%the engine bounds the answers itself either way, and this changes only how
%much work the BACKEND does before the first one.
%
%Two levers a reader might expect here are already in place and need no
%option. The bound parts of a pattern reach a provider as ground atoms,
%including the bindings an enclosing join has made, so the second pattern of
%a join arrives as (other a0 $_) rather than (other $_ $_). And the engine
%stops pulling as soon as it has enough: a limit of 3 over a provider holding
%a thousand atoms pulls four [measured 2026-08-16].
%
%ONE hook, with the options always passed. There was a /2 beside this and the
%engine chose between them with `clause(foreign_match(_,_,_), _)`, which
%asks whether ANY provider anywhere declared the bounded form. The Python shim
%declares it unconditionally, so with Python in the process that guard was true
%for every space, and a Prolog-only provider writing /2 had the /3 form called
%instead: the shim's clause failed on its own ownership check and the whole
%match answered nothing. Reproduced as `unbounded: 3, bounded: 0`
%[measured 2026-08-16]. A provider that has nothing to do with the options
%ignores the argument, which costs it one underscore and cannot go wrong.
%How much a provider's own filtering is worth, per PATTERN. Class is exact or
%inexact, and a space with no clause is inexact, which is the answer every
%provider written before this gets for free.
%
% exact every candidate you yield for this pattern unifies with it, so N
% candidates are N answers and limit(N) is a requirement you may
% truncate to
% inexact you reduce what you produce, and some of it may not match; the
% engine re-unifies and limit(N) stays advice you must not truncate to
%
%This is Apache DataFusion's TableProviderFilterPushDown, whose Exact rung
%says it in the same words: "Your source guarantees that no output rows will
%have a false value for this predicate. Because the filter is fully evaluated
%at the source, DataFusion will not add a FilterExec for it", against Inexact,
%"Your source has the ability to reduce the data produced, but the output may
%still include rows that do not satisfy the predicate"
%[source: Apache DataFusion, Custom Table Providers].
%
%PER PATTERN, not per provider, which is the part worth copying. A backend is
%usually exact on equality against an indexed column and inexact on everything
%else, and one flag for the whole provider would force it to claim the weaker
%answer everywhere. The clause takes the pattern, so it can say which is which.
%
%DataFusion's third rung, Unsupported, is deliberately absent. It exists there
%because the planner decides whether to SEND a filter at all; here the pattern
%is the only thing a provider is given, so there is nothing to withhold, and a
%provider that ignores it is inexact in the only sense the engine acts on.
%
%What the engine does with exact: it stops pulling at N instead of at N+1,
%since it no longer needs the extra candidate to learn the bound is met. What
%it does NOT do is skip unification, which is not a filter here but the step
%that binds the pattern's variables, so an exact claim cannot make an answer
%wrong. It can only make a wrong claim cost answers, which is why
%check_space_provider tests it against the provider's own output
%[tested: a_bounded_match_carries_its_options,
%a_bound_is_withheld_from_an_unclaimed_pattern,
%test_a_false_exact_claim_is_caught].
:- multifile foreign_pushdown/3.
kind(foreign_pushdown/3, ownership).
%The routing voice of a third-party declaration kind. Consulted after the
%declared fidelity or the provider's own method proposes a route class,
%and every loaded advisor may only DEMOTE: the effective class is the most
%conservative voice, refuse below inexact below exact, so advisors compose
%order-independently and none can widen a claim its author never made.
%route_cap(Space, Pattern, Cap, Why): Cap is exact (no objection),
%inexact (candidates must be re-unified, the pushdown of the caller's
%bound is withheld) or refuse (this route must not serve now, loud at the
%match and naming Why). An advisor typically reads its own kind's atoms
%from '&metta', often through metta_shape_route/5, which is what lets a
%freshness or cost kind change routing with no kernel edit
%[tested: a_route_cap_demotes_and_refuses_through_the_published_seam].
%Declared metadata steering the router is the oldest optimizer discipline
%there is: semantic query optimization transforms evaluation by declared
%integrity constraints [source: Chakravarthy, Grant and Minker, ACM TODS
%1990], and a FRESHNESS vocabulary gating routes runs in production as
%Oracle's QUERY_REWRITE_INTEGRITY, whose stale_tolerated mode alone lets
%a stale materialized view keep serving rewrites [source:
%https://docs.oracle.com/en/database/oracle/oracle-database/23/dwhsg/basic-query-rewrite-materialized-views.html].
:- multifile route_cap/4.
:- dynamic route_cap/4.
kind(route_cap/4, declaration).
%A conjunction, offered WHOLE before the engine splits it. Succeed to claim
%some of it, binding Goal to a goal that enumerates bindings for Claimed; fail
%to decline, and the engine plans it exactly as it does today.
%
% foreign_plan(Space, Patterns, Claimed, Rest, Goal)
%
%This is the seam that makes a backend's own join reachable. Without it every
%conjunction is split one pattern at a time and re-dispatched per outer row,
%which is a nested-loop plan, and a nested-loop plan cannot reach the AGM bound
%however fast the provider is: for the triangle R(x,y), S(y,z), T(z,x) with each
%relation of size N the bound is N^1.5 and "it is not possible to achieve a
%running time of O(N^3/2) using only join plans" [source: Ngo/Re/Rudra and the
%worst-case-optimal join literature]. So this is not a tuning knob; it is the
%difference between a provider being allowed to be asymptotically better and
%not being allowed to.
%
%Four properties, each with a precedent elsewhere in this file:
%
% - DECLINING IS THE DEFAULT. A provider with no clause gets today's behaviour
% exactly, the same safe default the capability vocabulary has.
% - A PARTIAL CLAIM IS LEGAL. Claimed plus Rest lets a backend take the two
% patterns it owns and leave the third, so the seam is not all-or-nothing.
% They must PARTITION Patterns: dropping a conjunct answers more rows than
% the query asks for and the engine refuses it, because nothing downstream
% would catch it.
% - THE STRATEGY IS INVISIBLE. Leapfrog, a hash join, a SQL SELECT, a vector
% index: the engine sees a goal. It supports none of them and therefore all.
% - THE CLAIM IS EXACT, and this one is the exception to the seam's usual rule.
% Elsewhere a provider may over-approximate because the engine re-unifies
% each candidate, which is cheap. There is no cheap re-check for a join: the
% only way to verify a row is to run the join. So claiming means answering
% exactly, a provider that cannot must decline, and check_space_provider
% verifies the claim against the engine's own split rather than trusting it.
%
%The caller's options are not passed. The engine still bounds the answers, so
%this costs work in the backend and never an answer, and a limit could not be
%honoured usefully anyway while a provider answers a whole batch at a time.
:- multifile foreign_plan/5.
kind(foreign_plan/5, ownership).
%What a provider answers. Failure alone cannot say: foreign_match/3 is a
%legitimate enumerator, so "no clause" and "no atoms match" look identical
%from the engine, and clause/2 cannot stand in either, because every provider
%in this tree writes ONE clause with a variable space and an ownership guard
%in the body, which unifies with any space at all.
%
%So it is declared, the way extensions/python/metta/foreign/__init__.py derives it from the narrow
%protocols a provider implements. The capabilities are add, remove, match,
%enumerate, clear, PLAN and RULES.
%
%`rules` is the odd one and it is the one that matters most. It says the
%space's atoms include EQUATIONS, which in MeTTa is the difference between a
%data source and a place a program lives. The provider stores one the way it
%stores any atom and the ENGINE compiles it, so a foreign rule is the same
%compiled clause a native one is; nothing in the provider knows what an
%equation is. A space without the declaration is refused an equation at
%add-atom rather than storing one that can never fire. A space that declares
%NOTHING is taken to provide everything, which is what every provider written
%before this assumed.
%
%Two things follow from a declaration, and the first is the one that matters:
%a space that enumerates but does not match now has its enumeration FILTERED
%here for a bound pattern, instead of answering nothing. The Python half has
%always said enumeration is enough ("An Enumerable provider need not implement
%Matcher"), and the Prolog half quietly required both. The second is that an
%operation a space does not provide raises with the space and the operation
%named, rather than failing into "there is nothing there".
:- multifile foreign_capability/2.
kind(foreign_capability/2, declaration).
%What a context's change events promise, for a provider that owns a FAMILY
%of space names rather than one name it could write an atom about.
%
% context_events(Space, Delivery, Order)
%
%Delivery is at-most-once, at-least-once or per-write-exactly and Order is
%ordered or unordered, the catalog's own `delivery` and `event-order`
%vocabularies. The per-space door is the ordinary declaration atom,
%(events <ctx> <delivery> <order>) in '&metta', which is what
%Space.events(delivery, order) and a Python provider's registration write; this is
%the same answer for a provider like MORK, whose spaces are every name
%beginning &mork, so there is no one name to write the atom about. The two
%doors are read by one question, metta_event_capability/3, exactly as a
%Prolog provider's foreign_capability/2 clauses and the Python
%bridge's registered facts are read by one foreign_provides/2.
%
%Declaring nothing means no events, which is the safe answer and the one
%every provider written before this gets: a subscription on the space is
%refused naming the missing capability rather than served and silently
%missing writes [P12.14].
:- multifile context_events/3.
kind(context_events/3, declaration).
%Why a space says no, in the provider's own words. The engine refuses a
%capability a space does not declare, and "does not implement add" reads
%differently from "declines this add request"; a provider with a reason raises
%it here and the engine's generic permission_error is what a provider without
%one gets. It is expected to THROW rather than answer
%[tested: test_a_provider_states_its_own_refusal].
:- multifile foreign_refuse/2.
kind(foreign_refuse/2, ownership).
%An exception that must never be recovered from. A caught abort, limit, alarm
%or interrupt is a stopped program pretending it succeeded, and the engine's
%recovery catches all consult this: control_exception(Ball) true means the
%ball is rethrown rather than handled.
%
%A library that introduces its own cancellation or budget signal adds a
%clause and every recovery site in the engine respects it, which is the only
%way it could: a signal the engine has never heard of is swallowed by the
%first recovery catch it meets, and the failure is silent. This is
%KeyboardInterrupt living outside Exception, given a seam.
%
%library(exceptions) says the same thing more directly, and was measured
%rather than assumed. Written its way a recovery site is
%catch(Goal, \+ metta_control_signal, _, Recover), the negated type its
%is_exception/3 already supports, and it is behaviour-identical: ten balls,
%errors and non-errors, control and ordinary, recovered or escaped the same
%way both ways. It costs too much. catch/4 puts a freeze/2 on the ball at
%CALL time, so the price is paid whether or not anything throws: 20,000
%quiet calls went 140,002 to 240,002 inferences, 1.71x, and 20,000 throwing
%ones 240,003 to 1,340,002, 5.58x [measured 2026-08-16]. The recovery catch
%wraps every candidate the translator tries, so the quiet number is the one
%that decides it [source: ai-swi-library-review.md, entry 2].
%The multifile declaration for it is in engine/metta.pl, not here, because
%control_exception/1 is also an engine_emitted/1 name: the translator writes it
%into compiled bodies and protect_engine_emitted/1 imports it into every
%space's module from the ENGINE's module, which it can only do if that is
%where it lives. It is the one seam whose home is the engine core rather than
%this module, and seam_home/2 below is what lets the publication machinery say
%so instead of assuming.
kind(control_exception/1, declaration).
%Whether a HOST's own atom hooks are idle for a space, the host's clause of
%it: the shim answers for the Python side, and with no host loaded the seam
%has no clause and the engine's own no-handlers test already answered. The
%engine hands the host the full handler CENSUS as clause references, so a
%host clause matches the census against the one reference it installed and
%never consults engine internals to answer: a host is asked about ITS hooks,
%with the facts it needs in the question. engine/spaces.pl asks them; they are
%declared here because every seam is.
:- multifile host_add_hooks_idle/2.
kind(host_add_hooks_idle/2, ownership).
%Whether ONE added-atom hook is idle for ONE space, answered by whoever
%installed that hook. The census seam above asks a host about the whole
%reference list at once, which works while every hook belongs to a host and
%breaks the moment the ENGINE installs one of its own: the bridge hook is a
%single clause with an unbound Space, because any space might carry a
%reaction, so its head cannot say which spaces it watches and no host can
%speak for it either. The census then had two references where the host
%clause matches one, answered "not idle" for EVERY space, and the batched
%program-atom door fell back to the per-atom one -- 30,274 inferences to
%4,496,299 for a forty-equation fast-cache restore, 149x, from one reaction
%on an unrelated space [measured 2026-09-04].
%
%A hook that knows its own table answers from it. Refs nobody claims idle
%are what the host census is asked about, so the existing clause keeps
%matching the list it was written for.
:- multifile atom_hook_ref_idle/2.
kind(atom_hook_ref_idle/2, ownership).
%Whether an error term is a host's transport dying, and how a host's own
%error renders as a MeTTa (Error ...) reason. Both are the host's to answer
%for its own exception shapes, so both are ownership. They used to be
%user-module hooks spelled metta_host_transport_failure/1 and
%metta_host_error_reason/2, declared multifile only by the Python bridge, so
%every seatless process -- the WebAssembly host, the pure kernel -- paid an
%existence_error where "no" was the answer; and the spelling carried the
%metta_host_ namespace this module exists to replace. The engine declares
%them HERE now, the module carries the namespace, and a hook with no clauses
%is a question every host declined, which fails cleanly into the
%message-system rendering at the call site (engine/metta/space_hooks.pl).
:- multifile host_transport_failure/1.
kind(host_transport_failure/1, ownership).
:- multifile host_error_reason/2.
kind(host_error_reason/2, ownership).
:- multifile host_remove_hooks_idle/2.
kind(host_remove_hooks_idle/2, ownership).
%An APPLICABLE GROUNDED ATOM. MeTTa's own definition of a Grounded atom is
%that it "may contain any binary object, for example operation (including deep
%neural networks), collection or value" [source: metta-lang.dev/docs/learn,
%Atom kinds and types], and an operation is a thing you call. The engine leaves
%a grounded head unevaluated unless a handler claims it, so `((py-atom
%numpy.absolute) -5)` answered itself and a callable held in a MeTTa variable
%was not a callable at all.
%
% grounded_apply(Value, Args, Out)
%
%Value is the grounded atom in head position and Args are the arguments as the
%engine has them. Succeed to claim it and bind Out; fail and the expression
%stays unreduced, which is what every value that is not an operation should do.
%
%Nothing in the engine knows what makes a value applicable, which is the point:
%a Python bridge claims Python callables, and a bridge for something else
%claims its own. It is consulted only for a head that is neither a function
%name nor a partial application, so an ordinary call never reaches it.
%
%grounded_apply(Obj, Positional, KeywordPairs, Out): Positional are the
%finished argument values, KeywordPairs the `(name value)` pairs of a
%`(Kwargs ...)` written LAST at the call site, `[]` when none was written.
%The translator decides that from the source, so a `(Kwargs ...)` that
%arrives in a value is the data it is and never becomes control
%[tested: test_grounded_applications_read_keywords_only_where_written].
:- multifile grounded_apply/4.
kind(grounded_apply/4, ownership).
%Whether a value is an operation at all, asked WITHOUT applying it. `bind!`
%needs to know before there are any arguments: a name bound to a callable is
%callable by that name, and a name bound to 5 is not.
:- multifile grounded_applicable/1.
kind(grounded_applicable/1, ownership).
% A grounded provider decides exact value equality for finite algebra carriers.
% Succeed with true or false to claim the pair; failure leaves native equality.
% False must not fall through to blob identity or structural matching [tested:
% test_finite_tensor_semiring_checks_every_law; commit=074dc0a88b1605c54824de677d586b6f60998bcf].
:- multifile grounded_algebra_equal/3.
kind(grounded_algebra_equal/3, ownership).
% A host carrier predicate receives the value in its own faithful atom reading.
% The first owner returns true or false; a refusal cannot fall through into a
% different host representation [tested: test_carrier_preserves_text_and_symbol_types;
% commit=074dc0a88b1605c54824de677d586b6f60998bcf].
:- multifile grounded_algebra_type/3.
kind(grounded_algebra_type/3, ownership).
%A grounded host value may participate in the language's numeric operations
%without becoming a Prolog number. Admission and execution stay one provider
%protocol: the owner recognizes its numeric objects, then evaluates with that
%host's operator dispatch so reflected methods and result types are retained
%[tested: test_numpy_numeric_family_keeps_python_result_types and