eyeprolog 1.5.31 → 1.5.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/conformance-report.md +6 -0
- package/examples/bench.pl +57 -0
- package/examples/output/bench.pl +1 -0
- package/package.json +22 -20
- package/playground.html +1 -0
- package/src/solver.js +181 -27
- package/src/term.js +39 -20
- package/test/bench/benchmarks.json +10 -0
- package/test/benchmark.mjs +18 -1
- package/test/conformance/ISO-COMPLIANCE.md +19 -6
- package/test/conformance/ISO-WG17-EXTENSIONS.md +68 -0
- package/test/conformance/NEUMERKEL-LATEST.md +34 -0
- package/test/conformance/NEUMERKEL-LIVE.md +63 -0
- package/test/conformance/README.md +22 -40
- package/test/conformance/WG17-SYNTAX-STATUS.md +1 -1
- package/test/lips-benchmark.mjs +93 -0
- package/test/neumerkel.mjs +470 -0
- package/test/run-all.mjs +6 -0
- package/test/run-benchmark-tests.mjs +27 -1
- package/test/run-conformance-all.mjs +16 -0
- package/test/run-conformance-report.mjs +13 -1
- package/test/run-conformance.mjs +1 -1
- package/test/run-neumerkel-tests.mjs +80 -0
- package/test/run-neumerkel.mjs +95 -0
- package/test/run-regression.mjs +16 -3
- package/the-art-of-eyeprolog.md +13 -3
- package/tools/report-wg17-syntax-coverage.mjs +2 -2
- package/tools/upgrade-wg17.mjs +3 -3
package/README.md
CHANGED
|
@@ -74,9 +74,9 @@ All 33 bundled modules that overlap Scryer's current `src/lib/` surface cover Sc
|
|
|
74
74
|
- [Symbiotic Knowledge Graphs](https://eyereasoner.github.io/eyeprolog/examples/deck/symbiotic-knowledge-graphs) — RDF ↔ Prolog heatwave-response demo for human/AI/KG co-evolution
|
|
75
75
|
- [rdf-prolog-roundtrip](https://github.com/eyereasoner/rdf-prolog-roundtrip) — standalone RDF 1.2 ↔ ISO Prolog bridge used by the RDF examples
|
|
76
76
|
- [ISO conformance audit](test/conformance/ISO-COMPLIANCE.md) — supported Part 1 profile
|
|
77
|
-
- [
|
|
77
|
+
- [Latest Neumerkel conformity](test/conformance/NEUMERKEL-LATEST.md) — tracked result from the current live upstream inventory
|
|
78
|
+
- [Conformance report](conformance-report.md) — generated executable conformance status and local corpus summary
|
|
78
79
|
- [OpenRuleBench](openrulebench/README.md) — portable benchmark profile
|
|
79
|
-
|
|
80
80
|
## RDF, Prolog, and symbiotic knowledge graphs
|
|
81
81
|
|
|
82
82
|
EyeProlog can sit behind an RDF knowledge graph without inventing a private graph representation. [`rdf-prolog-roundtrip`](https://github.com/eyereasoner/rdf-prolog-roundtrip) converts RDF 1.2 datasets to ordinary `rdf(Subject, Predicate, Object, Graph)` facts, EyeProlog applies portable rules, and ground `rdf/4` results can be converted back to RDF.
|
|
@@ -86,14 +86,14 @@ The checked [Symbiotic Knowledge Graphs example](examples/symbiotic-knowledge-gr
|
|
|
86
86
|
The same RDF → Prolog → RDF boundary is exercised by five additional checked scenarios: [cross-organization data sharing](https://eyereasoner.github.io/eyeprolog/examples/deck/cross-organization-data-sharing), [explainable EV-depot configuration](https://eyereasoner.github.io/eyeprolog/examples/deck/explainable-ev-depot-configuration), [operational incident response](https://eyereasoner.github.io/eyeprolog/examples/deck/operational-incident-response), [software supply-chain vulnerability response](https://eyereasoner.github.io/eyeprolog/examples/deck/sbom-vulnerability-response), and a [scientific evidence graph](https://eyereasoner.github.io/eyeprolog/examples/deck/scientific-evidence-graph). Together they cover policy decisions, reversible configuration reasoning, dependency-graph diagnosis, transitive SBOM exposure, and evidence aggregation with explicit disagreement.
|
|
87
87
|
|
|
88
88
|
## Benchmarks
|
|
89
|
-
EyeProlog has
|
|
89
|
+
EyeProlog has 21 checksum-protected wall-clock benchmarks spanning recursion/indexing, constraints, tabling/WFS, DCGs, Eyelet, search, term I/O, attributes, rewriting, and the classic Prolog naive-reverse workload. Short workloads are adaptively batched before timing so millisecond-scale noise is not mistaken for a regression. Run `npm run benchmark`; create a machine-local comparison point with `npm run benchmark:baseline`; use `npm run test:benchmark` for harness checks. The checked [`examples/bench.pl`](examples/bench.pl) preserves the classic Quintus 1984 `nrev/2` workload on a 30-element list. For a comparable LIPS number, run `npm run benchmark:lips`: it executes the classic failure-driven `dobench/1` and `dodummy/1` loops in Prolog, subtracts dummy-loop CPU time, and applies the historical 496 procedure calls per reversal. The generic benchmark table still shows a quick wall-clock LIPS estimate for `classic-nrev`, but `benchmark:lips` is the canonical engine-speed measurement. LIPS is a historical basic-engine-speed indicator, not a whole-system performance score. Details are in [*The Art of EyeProlog*](the-art-of-eyeprolog.md).
|
|
90
|
+
For the project policy on post-ISO-standard and WG17 compatibility features such as digit separators, see [ISO/WG17 compatibility extensions](test/conformance/ISO-WG17-EXTENSIONS.md).
|
|
90
91
|
## Development
|
|
91
|
-
|
|
92
92
|
```sh
|
|
93
93
|
git clone https://github.com/eyereasoner/eyeprolog.git
|
|
94
94
|
cd eyeprolog
|
|
95
95
|
npm install
|
|
96
96
|
npm test
|
|
97
97
|
```
|
|
98
|
-
|
|
98
|
+
`npm test` is the release gate and fetches the latest seven Neumerkel conformity sources before the local gates; use `npm run test:offline` for a network-free pass, `npm run test:conformance` for all conformance layers, `npm run test:neumerkel` for live upstream only, and `npm run test:neumerkel:cached` only to reproduce the last fetch. Upstream counts are discovered dynamically and verified against the tracked [latest Neumerkel report](test/conformance/NEUMERKEL-LATEST.md). Exact bytes/hashes stay under Git-ignored `.cache/neumerkel/`; refresh the tracked report with `npm run conformance:update:neumerkel` when upstream changes. Benchmarks remain `npm run benchmark` and `npm run benchmark:lips`.
|
|
99
99
|
EyeProlog is released under the [MIT License](LICENSE.md).
|
package/conformance-report.md
CHANGED
|
@@ -4,6 +4,12 @@ This report combines an executable external conformance gate with the file-based
|
|
|
4
4
|
conformance corpus under `test/conformance/`. The executable result is measured
|
|
5
5
|
when this report is generated; it is not inferred from fixture counts.
|
|
6
6
|
|
|
7
|
+
## Latest Neumerkel evidence
|
|
8
|
+
|
|
9
|
+
See the tracked [latest Neumerkel conformity report](test/conformance/NEUMERKEL-LATEST.md).
|
|
10
|
+
The live release gate fetches all seven TU Wien sources again and verifies that tracked
|
|
11
|
+
report still matches the discovered upstream inventory.
|
|
12
|
+
|
|
7
13
|
## Executable conformance status
|
|
8
14
|
|
|
9
15
|
| Gate | Passed | Total | Status |
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
% Classic Prolog "naive reverse" benchmark (Quintus, 1984), adapted as a
|
|
2
|
+
% deterministic EyeProlog example. The traditional workload reverses a
|
|
3
|
+
% 30-element list with nrev/2 and concatenate/3. It takes 496 Prolog
|
|
4
|
+
% procedure calls per reversal in the original LIPS accounting.
|
|
5
|
+
%
|
|
6
|
+
% `npm run benchmark:lips` uses the classic dobench/dodummy control subtraction
|
|
7
|
+
% with Node's process CPU clock, avoiding a dependency on a Prolog-specific
|
|
8
|
+
% statistics/2 clock while preserving the benchmark methodology. See README.md.
|
|
9
|
+
%
|
|
10
|
+
% Source/history:
|
|
11
|
+
% https://gerrit.googlesource.com/prolog-cafe/+/73b5ce4f5fbef086a22de3292a53e1ffe2947fab/examples/benchmarks/src/bench.pl
|
|
12
|
+
|
|
13
|
+
%% goal: bench_result(X0)
|
|
14
|
+
|
|
15
|
+
nrev([], []).
|
|
16
|
+
nrev([X|Rest], Ans) :-
|
|
17
|
+
nrev(Rest, L),
|
|
18
|
+
concatenate(L, [X], Ans).
|
|
19
|
+
|
|
20
|
+
concatenate([], L, L).
|
|
21
|
+
concatenate([X|L1], L2, [X|L3]) :-
|
|
22
|
+
concatenate(L1, L2, L3).
|
|
23
|
+
|
|
24
|
+
data([1,2,3,4,5,6,7,8,9,10,
|
|
25
|
+
11,12,13,14,15,16,17,18,19,20,
|
|
26
|
+
21,22,23,24,25,26,27,28,29,30]).
|
|
27
|
+
|
|
28
|
+
bench_result(Reversed) :-
|
|
29
|
+
data(List),
|
|
30
|
+
nrev(List, Reversed).
|
|
31
|
+
|
|
32
|
+
% Supporting predicates from the classic benchmark harness. The JavaScript
|
|
33
|
+
% benchmark driver times dodummy/1 and dobench/1 separately and subtracts the
|
|
34
|
+
% dummy time, matching the Quintus methodology without requiring a particular
|
|
35
|
+
% Prolog statistics/2 clock implementation.
|
|
36
|
+
|
|
37
|
+
dobench(Count) :-
|
|
38
|
+
data(List),
|
|
39
|
+
bench_repeat(Count),
|
|
40
|
+
nrev(List, _),
|
|
41
|
+
fail.
|
|
42
|
+
dobench(_).
|
|
43
|
+
|
|
44
|
+
dodummy(Count) :-
|
|
45
|
+
data(List),
|
|
46
|
+
bench_repeat(Count),
|
|
47
|
+
dummy(List, _),
|
|
48
|
+
fail.
|
|
49
|
+
dodummy(_).
|
|
50
|
+
|
|
51
|
+
dummy(_, _).
|
|
52
|
+
|
|
53
|
+
bench_repeat(_N).
|
|
54
|
+
bench_repeat(N) :-
|
|
55
|
+
N > 1,
|
|
56
|
+
N1 is N - 1,
|
|
57
|
+
bench_repeat(N1).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
bench_result([30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1]).
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "1.5.
|
|
6
|
+
"version": "1.5.33",
|
|
7
7
|
"description": "EyeProlog turns facts and rules into answers and proofs.",
|
|
8
8
|
"type": "module",
|
|
9
9
|
"main": "./index.js",
|
|
@@ -46,30 +46,32 @@
|
|
|
46
46
|
},
|
|
47
47
|
"scripts": {
|
|
48
48
|
"test": "node test/run-all.mjs",
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"test:
|
|
52
|
-
"test:
|
|
53
|
-
"test:
|
|
54
|
-
"test:
|
|
55
|
-
"test:
|
|
49
|
+
"test:offline": "node test/run-all.mjs --offline",
|
|
50
|
+
"test:conformance": "node test/run-conformance-all.mjs",
|
|
51
|
+
"test:conformance:offline": "node test/run-conformance-all.mjs --offline",
|
|
52
|
+
"test:neumerkel": "node test/run-neumerkel.mjs",
|
|
53
|
+
"test:neumerkel:cached": "node test/run-neumerkel.mjs --cached --no-verify-report",
|
|
54
|
+
"test:neumerkel:harness": "node test/run-neumerkel-tests.mjs",
|
|
55
|
+
"test:iso": "node test/run-iso-strict.mjs",
|
|
56
56
|
"test:wg17": "node test/run-wg17.mjs",
|
|
57
|
-
"test:examples": "node test/run-examples.mjs",
|
|
58
57
|
"test:regression": "node test/run-regression.mjs",
|
|
59
|
-
"test:
|
|
60
|
-
"test:
|
|
61
|
-
"test:regression:docs": "node test/run-regression.mjs docs",
|
|
62
|
-
"test:regression:white-box": "node test/run-regression.mjs white-box",
|
|
58
|
+
"test:examples": "node test/run-examples.mjs",
|
|
59
|
+
"test:interop": "node test/run-interop.mjs",
|
|
63
60
|
"test:playground": "node test/run-playground.mjs",
|
|
64
|
-
"wg17:upgrade": "node tools/upgrade-wg17.mjs",
|
|
65
|
-
"report:wg17": "node tools/report-wg17-syntax-coverage.mjs",
|
|
66
|
-
"report:wg17-syntax": "node tools/report-wg17-syntax-coverage.mjs",
|
|
67
|
-
"preversion": "npm test && node test/run-conformance-report.mjs conformance-report.md",
|
|
68
|
-
"postversion": "git push origin HEAD --follow-tags",
|
|
69
61
|
"test:architecture": "node test/run-architecture.mjs",
|
|
62
|
+
"test:openrulebench": "node test/run-openrulebench.mjs",
|
|
63
|
+
"test:benchmark": "node test/run-benchmark-tests.mjs",
|
|
70
64
|
"benchmark": "node test/benchmark.mjs",
|
|
65
|
+
"benchmark:lips": "node test/lips-benchmark.mjs",
|
|
71
66
|
"benchmark:baseline": "node test/benchmark.mjs --save .benchmarks/baseline.json",
|
|
72
|
-
"
|
|
73
|
-
"
|
|
67
|
+
"conformance:update": "npm run conformance:update:wg17 && npm run conformance:update:neumerkel",
|
|
68
|
+
"conformance:report": "node test/run-conformance-report.mjs conformance-report.md",
|
|
69
|
+
"generate": "node tools/generate-library-autoload-index.mjs && node tools/generate-predicate-reference.mjs && node tools/extract-book-examples.mjs",
|
|
70
|
+
"generate:autoload": "node tools/generate-library-autoload-index.mjs",
|
|
71
|
+
"generate:reference": "node tools/generate-predicate-reference.mjs",
|
|
72
|
+
"preversion": "npm test && npm run conformance:report",
|
|
73
|
+
"postversion": "git push origin HEAD --follow-tags",
|
|
74
|
+
"conformance:update:wg17": "node tools/upgrade-wg17.mjs",
|
|
75
|
+
"conformance:update:neumerkel": "node test/run-neumerkel.mjs --update-report"
|
|
74
76
|
}
|
|
75
77
|
}
|
package/playground.html
CHANGED
package/src/solver.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Depth-first EyeProlog solver with builtin dispatch, memoization, and guarded recursion handling.
|
|
2
2
|
// Most semantic decisions still flow through unification; optimizations only select candidates earlier.
|
|
3
3
|
import {
|
|
4
|
-
ATOM, COMPOUND, NUMBER, STRING, VAR, Env, compactListLength, compactVariableList, compound, cons, copyResolved, deref, emptyList,
|
|
4
|
+
ATOM, COMPOUND, NUMBER, STRING, VAR, Env, Term, compactListLength, compactVariableList, compound, cons, copyResolved, deref, emptyList,
|
|
5
5
|
flattenConjunction, freshTerm, isCons, isDecimalInteger, isEmptyList,
|
|
6
6
|
numberTerm, numberTextFromDouble, properListItems, termIsGround, termToString, unify, variable, variantTerms,
|
|
7
7
|
} from './term.js';
|
|
@@ -18,6 +18,7 @@ import { evaluatePositiveDatalog, relationForDatalogGroup, datalogCandidateIndex
|
|
|
18
18
|
let freshCounter = 0;
|
|
19
19
|
const DEFAULT_INNER_TABLE_SCOPE_LIMIT = 1024;
|
|
20
20
|
const GOAL_CONTINUATION_THRESHOLD = 64;
|
|
21
|
+
const NO_CALL_TARGET = Symbol('no-call-target');
|
|
21
22
|
// Conservative live-storage estimate for one generated length/2 list cell
|
|
22
23
|
// (cons object, argument vector, fresh variable, and its generated name).
|
|
23
24
|
const GENERATED_LENGTH_CELL_RESERVE_BYTES = 256;
|
|
@@ -90,6 +91,9 @@ export class Solver {
|
|
|
90
91
|
// implementation-specific predicates. Use the Part 1 + corrigenda
|
|
91
92
|
// registry even when an embedder supplied the normal EyeProlog registry.
|
|
92
93
|
this.registry = this.isoStrict ? getStrictIsoRegistry() : (options.registry ?? getEyePrologRegistry());
|
|
94
|
+
this.userGroupCache = Object.create(null);
|
|
95
|
+
this.moduleGroupCache = new Map();
|
|
96
|
+
this.builtinCache = Object.create(null);
|
|
93
97
|
this.mutableProgram = program.mutable === true;
|
|
94
98
|
this.programRevision = this.program.revision ?? 0;
|
|
95
99
|
// Normal Prolog execution must not silently acquire a semantic depth
|
|
@@ -295,6 +299,37 @@ export class Solver {
|
|
|
295
299
|
}
|
|
296
300
|
}
|
|
297
301
|
|
|
302
|
+
lookupGroup(name, arity, module = 'user') {
|
|
303
|
+
if (module === 'user') {
|
|
304
|
+
let slots = this.userGroupCache[name];
|
|
305
|
+
if (slots === undefined) this.userGroupCache[name] = slots = [];
|
|
306
|
+
const cached = slots[arity];
|
|
307
|
+
if (cached !== undefined) return cached === NO_CALL_TARGET ? null : cached;
|
|
308
|
+
const group = this.program.findGroup(name, arity, module);
|
|
309
|
+
slots[arity] = group ?? NO_CALL_TARGET;
|
|
310
|
+
return group;
|
|
311
|
+
}
|
|
312
|
+
let byName = this.moduleGroupCache.get(module);
|
|
313
|
+
if (byName == null) this.moduleGroupCache.set(module, byName = new Map());
|
|
314
|
+
let byArity = byName.get(name);
|
|
315
|
+
if (byArity == null) byName.set(name, byArity = []);
|
|
316
|
+
const cached = byArity[arity];
|
|
317
|
+
if (cached !== undefined) return cached === NO_CALL_TARGET ? null : cached;
|
|
318
|
+
const group = this.program.findGroup(name, arity, module);
|
|
319
|
+
byArity[arity] = group ?? NO_CALL_TARGET;
|
|
320
|
+
return group;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
lookupBuiltin(name, arity) {
|
|
324
|
+
let slots = this.builtinCache[name];
|
|
325
|
+
if (slots === undefined) this.builtinCache[name] = slots = [];
|
|
326
|
+
const cached = slots[arity];
|
|
327
|
+
if (cached !== undefined) return cached === NO_CALL_TARGET ? null : cached;
|
|
328
|
+
const def = this.registry.get(name, arity);
|
|
329
|
+
slots[arity] = def ?? NO_CALL_TARGET;
|
|
330
|
+
return def;
|
|
331
|
+
}
|
|
332
|
+
|
|
298
333
|
syncProgramRevision() {
|
|
299
334
|
if (!this.mutableProgram) {
|
|
300
335
|
if (this.program.mutable !== true) return;
|
|
@@ -303,6 +338,9 @@ export class Solver {
|
|
|
303
338
|
const revision = this.program.revision ?? 0;
|
|
304
339
|
if (revision === this.programRevision) return;
|
|
305
340
|
this.programRevision = revision;
|
|
341
|
+
this.userGroupCache = Object.create(null);
|
|
342
|
+
this.moduleGroupCache.clear();
|
|
343
|
+
this.builtinCache = Object.create(null);
|
|
306
344
|
this.memo.clear();
|
|
307
345
|
this.subsumptiveMemo.clear();
|
|
308
346
|
this.wfsModels.clear();
|
|
@@ -512,13 +550,7 @@ export class Solver {
|
|
|
512
550
|
});
|
|
513
551
|
continue;
|
|
514
552
|
}
|
|
515
|
-
const
|
|
516
|
-
const freshVariables = new Map();
|
|
517
|
-
const freshHead = freshTerm(clause.head, id, freshVariables);
|
|
518
|
-
const freshBody = clause.body.map((term) => freshTerm(term, id, freshVariables));
|
|
519
|
-
const localFreshPlan = clauseLocalFreshPlan(clause);
|
|
520
|
-
const headLocalFresh = freshVariableSet(localFreshPlan.head, freshVariables);
|
|
521
|
-
attachBodyLocalFreshVariables(freshBody, localFreshPlan.body, freshVariables);
|
|
553
|
+
const { freshHead, freshBody, headLocalFresh } = instantiateClause(clause, nextFreshId());
|
|
522
554
|
next = clauseEnv.clone();
|
|
523
555
|
this.stats.unify_calls++;
|
|
524
556
|
if (!unify(clauseGoal, freshHead, next, { knownNonoccurringVariables: headLocalFresh })) continue;
|
|
@@ -654,7 +686,7 @@ export class Solver {
|
|
|
654
686
|
// EyeProlog normally solves left-to-right, but ready deterministic builtins can
|
|
655
687
|
// be run early as pure filters. Stop at internal sentinels so rule-body
|
|
656
688
|
// active guards are released before the caller's remaining goals are seen.
|
|
657
|
-
const selectedIndex =
|
|
689
|
+
const selectedIndex = 0;
|
|
658
690
|
const goal = deref(goals[selectedIndex], env);
|
|
659
691
|
const rest = selectedIndex === 0 ? goals.slice(1) : [...goals.slice(0, selectedIndex), ...goals.slice(selectedIndex + 1)];
|
|
660
692
|
prepareLocalVariablesForGoal(goal, env);
|
|
@@ -706,7 +738,7 @@ export class Solver {
|
|
|
706
738
|
}
|
|
707
739
|
}
|
|
708
740
|
|
|
709
|
-
const def = callable ? this.
|
|
741
|
+
const def = callable ? this.lookupBuiltin(goal.name, goal.arity) : null;
|
|
710
742
|
this.active = active;
|
|
711
743
|
const builtinReady = def && builtinIsReadyOrAuthoritative(def, this, goal, env);
|
|
712
744
|
if (builtinReady && typeof def.expandGoal === 'function') {
|
|
@@ -740,7 +772,7 @@ export class Solver {
|
|
|
740
772
|
}
|
|
741
773
|
|
|
742
774
|
this.stats.solve_one_goal_calls++;
|
|
743
|
-
const group = this.
|
|
775
|
+
const group = this.lookupGroup(goal.name, goal.arity, goal.module ?? 'user');
|
|
744
776
|
if (!group) {
|
|
745
777
|
if (goal.name === '-->' && goal.arity === 2) {
|
|
746
778
|
throw new PrologError(
|
|
@@ -870,6 +902,35 @@ export class Solver {
|
|
|
870
902
|
}
|
|
871
903
|
|
|
872
904
|
if (!group.tabled && tryPushScalarFactRunFrames(stack, this, [goal, ...rest], env, depth, active)) break;
|
|
905
|
+
// When indexing leaves exactly one cut-free clause, enter it directly
|
|
906
|
+
// instead of allocating a resumable clause-choice frame. This keeps
|
|
907
|
+
// ordinary deterministic recursion on the interpreter's tight loop.
|
|
908
|
+
if (!group.tabled && groupNeedsActiveFrame(group) === false) {
|
|
909
|
+
const candidates = selectClauseCandidates(group, goal, env);
|
|
910
|
+
const candidateCount = clauseCandidateLength(candidates.primary) +
|
|
911
|
+
clauseCandidateLength(candidates.fallback);
|
|
912
|
+
if (candidateCount === 1) {
|
|
913
|
+
const clause = clauseCandidateLength(candidates.primary) === 1
|
|
914
|
+
? clauseCandidateAt(candidates.primary, 0)
|
|
915
|
+
: clauseCandidateAt(candidates.fallback, 0);
|
|
916
|
+
const { freshHead, freshBody, headLocalFresh } = instantiateClause(clause, nextFreshId());
|
|
917
|
+
const next = env.clone();
|
|
918
|
+
this.stats.unify_calls++;
|
|
919
|
+
if (!unify(goal, freshHead, next, { knownNonoccurringVariables: headLocalFresh })) break;
|
|
920
|
+
if (freshBody.length === 0) {
|
|
921
|
+
goals = rest;
|
|
922
|
+
} else if (rest.length >= GOAL_CONTINUATION_THRESHOLD ||
|
|
923
|
+
rest.some((pending) => pending?.kind === 'continueGoals')) {
|
|
924
|
+
next.compactForDeepContinuation?.();
|
|
925
|
+
goals = [...freshBody, { kind: 'continueGoals', goals: rest, depth, releaseActive: false }];
|
|
926
|
+
} else {
|
|
927
|
+
goals = [...freshBody, ...rest];
|
|
928
|
+
}
|
|
929
|
+
env = next;
|
|
930
|
+
depth++;
|
|
931
|
+
continue;
|
|
932
|
+
}
|
|
933
|
+
}
|
|
873
934
|
pushUserGoalUncachedFrames(stack, this, group, goal, rest, env, depth, active);
|
|
874
935
|
break;
|
|
875
936
|
}
|
|
@@ -1110,13 +1171,7 @@ export class Solver {
|
|
|
1110
1171
|
continue;
|
|
1111
1172
|
}
|
|
1112
1173
|
if (headCannotMatch(goal, clause.head, env)) continue;
|
|
1113
|
-
const
|
|
1114
|
-
const freshVariables = new Map();
|
|
1115
|
-
const freshHead = freshTerm(clause.head, id, freshVariables);
|
|
1116
|
-
const freshBody = clause.body.map((term) => freshTerm(term, id, freshVariables));
|
|
1117
|
-
const localFreshPlan = clauseLocalFreshPlan(clause);
|
|
1118
|
-
const headLocalFresh = freshVariableSet(localFreshPlan.head, freshVariables);
|
|
1119
|
-
attachBodyLocalFreshVariables(freshBody, localFreshPlan.body, freshVariables);
|
|
1174
|
+
const { freshHead, freshBody, headLocalFresh } = instantiateClause(clause, nextFreshId());
|
|
1120
1175
|
const next = env.clone();
|
|
1121
1176
|
this.stats.unify_calls++;
|
|
1122
1177
|
if (!unify(goal, freshHead, next, { knownNonoccurringVariables: headLocalFresh })) continue;
|
|
@@ -1301,16 +1356,15 @@ function pushUserGoalUncachedFrames(stack, solver, group, goal, rest, env, depth
|
|
|
1301
1356
|
if (tryPushGroundScalarRuleFrame(stack, solver, group, goal, rest, env, depth, active)) return;
|
|
1302
1357
|
if (tryPushGroundChainFrames(stack, solver, group, goal, rest, env, depth, active)) return;
|
|
1303
1358
|
const candidates = selectClauseCandidates(group, goal, env);
|
|
1359
|
+
const candidateCount = clauseCandidateLength(candidates.primary) + clauseCandidateLength(candidates.fallback);
|
|
1304
1360
|
const frames = [];
|
|
1305
|
-
const invocation = { goal, env: env._snapshotForSolverRead(), tableMapKey };
|
|
1306
1361
|
// Active frames serve two purposes: they delimit cut and detect variants in
|
|
1307
|
-
// recursive user predicates.
|
|
1308
|
-
//
|
|
1309
|
-
// otherwise linear relations such as length/2 retain O(depth^2) references.
|
|
1362
|
+
// recursive user predicates. Delay the invocation snapshot until a reachable
|
|
1363
|
+
// cut/guard actually needs one; ordinary cut-free recursion avoids it.
|
|
1310
1364
|
for (const pass of [candidates.primary, candidates.fallback]) {
|
|
1311
1365
|
for (let candidateIndex = 0; candidateIndex < clauseCandidateLength(pass); candidateIndex++) {
|
|
1312
1366
|
const clause = clauseCandidateAt(pass, candidateIndex);
|
|
1313
|
-
if (headCannotMatch(goal, clause.head, env)) continue;
|
|
1367
|
+
if (candidateCount > 1 && headCannotMatch(goal, clause.head, env)) continue;
|
|
1314
1368
|
// Do not speculatively unify alternative heads. With attributed variables,
|
|
1315
1369
|
// head unification can run verify_attributes/3, queue wakeups, fail, or
|
|
1316
1370
|
// throw. Those effects are observable and must occur only if Prolog search
|
|
@@ -1331,10 +1385,12 @@ function pushUserGoalUncachedFrames(stack, solver, group, goal, rest, env, depth
|
|
|
1331
1385
|
// recursive clause; do not copy the whole active path at every such layer.
|
|
1332
1386
|
const traditionalDepthFirst = !group.tabled;
|
|
1333
1387
|
const guarded = traditionalDepthFirst
|
|
1334
|
-
? frames.some((frame) => clauseHasCutForTailRelease(frame.clause))
|
|
1388
|
+
? (group.hasCut === true && frames.some((frame) => clauseHasCutForTailRelease(frame.clause)))
|
|
1335
1389
|
: groupNeedsActiveFrame(group);
|
|
1336
1390
|
const release = guarded ? [{ kind: 'releaseActive' }] : [];
|
|
1337
|
-
const nextActive = guarded
|
|
1391
|
+
const nextActive = guarded
|
|
1392
|
+
? [...active, { goal, env: env._snapshotForSolverRead(), tableMapKey }]
|
|
1393
|
+
: active;
|
|
1338
1394
|
for (const frame of frames) {
|
|
1339
1395
|
frame.active = nextActive;
|
|
1340
1396
|
frame.release = release;
|
|
@@ -1347,6 +1403,104 @@ function pushUserGoalUncachedFrames(stack, solver, group, goal, rest, env, depth
|
|
|
1347
1403
|
}
|
|
1348
1404
|
|
|
1349
1405
|
|
|
1406
|
+
function instantiatePlanNode(node, variables) {
|
|
1407
|
+
if (node.variableSlot != null) return variables[node.variableSlot];
|
|
1408
|
+
const args = new Array(node.args.length);
|
|
1409
|
+
for (let index = 0; index < args.length; index++) {
|
|
1410
|
+
args[index] = instantiatePlanNode(node.args[index], variables);
|
|
1411
|
+
}
|
|
1412
|
+
const term = new Term(node.type, node.name, args);
|
|
1413
|
+
if (node.module != null) term.module = node.module;
|
|
1414
|
+
return term;
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
function freshSetForVariableSlots(slots, variables) {
|
|
1418
|
+
if (slots.length === 0) return null;
|
|
1419
|
+
const names = new Array(slots.length);
|
|
1420
|
+
for (let index = 0; index < slots.length; index++) names[index] = variables[slots[index]].name;
|
|
1421
|
+
return names.length <= 4 ? new SmallFreshVariableSet(names) : new Set(names);
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
function instantiateClause(clause, id) {
|
|
1425
|
+
let plan = clause._instantiationPlan;
|
|
1426
|
+
if (plan == null) {
|
|
1427
|
+
const variableSlots = new Map();
|
|
1428
|
+
const variableNames = [];
|
|
1429
|
+
const compile = (term) => {
|
|
1430
|
+
if (term.type === VAR) {
|
|
1431
|
+
let slot = variableSlots.get(term.name);
|
|
1432
|
+
if (slot == null) {
|
|
1433
|
+
slot = variableNames.length;
|
|
1434
|
+
variableSlots.set(term.name, slot);
|
|
1435
|
+
variableNames.push(term.name);
|
|
1436
|
+
}
|
|
1437
|
+
return { variableSlot: slot };
|
|
1438
|
+
}
|
|
1439
|
+
return {
|
|
1440
|
+
type: term.type,
|
|
1441
|
+
name: term.name,
|
|
1442
|
+
module: term.module,
|
|
1443
|
+
args: term.args.map(compile),
|
|
1444
|
+
};
|
|
1445
|
+
};
|
|
1446
|
+
const localFreshPlan = clauseLocalFreshPlan(clause);
|
|
1447
|
+
const head = compile(clause.head);
|
|
1448
|
+
const body = clause.body.map(compile);
|
|
1449
|
+
const slotsFor = (names) => names.map((name) => variableSlots.get(name)).filter((slot) => slot != null);
|
|
1450
|
+
const bodyProofIndexes = [];
|
|
1451
|
+
for (let index = 0; index < clause.body.length; index++) {
|
|
1452
|
+
if (goalUsesFirstUseProof(clause.body[index])) bodyProofIndexes.push(index);
|
|
1453
|
+
}
|
|
1454
|
+
plan = clause._instantiationPlan = {
|
|
1455
|
+
head,
|
|
1456
|
+
body,
|
|
1457
|
+
variableNames,
|
|
1458
|
+
headLocalSlots: slotsFor(localFreshPlan.head),
|
|
1459
|
+
bodyLocalSlots: localFreshPlan.body.map(slotsFor),
|
|
1460
|
+
bodyProofIndexes,
|
|
1461
|
+
};
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
const variables = new Array(plan.variableNames.length);
|
|
1465
|
+
for (let index = 0; index < variables.length; index++) {
|
|
1466
|
+
variables[index] = variable(`${plan.variableNames[index]}#${id}`);
|
|
1467
|
+
}
|
|
1468
|
+
const freshHead = instantiatePlanNode(plan.head, variables);
|
|
1469
|
+
const freshBody = new Array(plan.body.length);
|
|
1470
|
+
for (let index = 0; index < freshBody.length; index++) {
|
|
1471
|
+
freshBody[index] = instantiatePlanNode(plan.body[index], variables);
|
|
1472
|
+
}
|
|
1473
|
+
const headLocalFresh = freshSetForVariableSlots(plan.headLocalSlots, variables);
|
|
1474
|
+
for (let proofIndex = 0; proofIndex < plan.bodyProofIndexes.length; proofIndex++) {
|
|
1475
|
+
const index = plan.bodyProofIndexes[proofIndex];
|
|
1476
|
+
const goal = freshBody[index];
|
|
1477
|
+
const knownNonoccurringVariables = freshSetForVariableSlots(plan.bodyLocalSlots[index] ?? [], variables);
|
|
1478
|
+
if (knownNonoccurringVariables == null || goal?.type !== COMPOUND) continue;
|
|
1479
|
+
attachKnownNonoccurringVariables(goal, knownNonoccurringVariables);
|
|
1480
|
+
}
|
|
1481
|
+
return { freshHead, freshBody, headLocalFresh };
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
function attachKnownNonoccurringVariables(term, knownNonoccurringVariables) {
|
|
1485
|
+
if (term?.type !== COMPOUND) return;
|
|
1486
|
+
if (term.name === '=' && term.arity === 2) {
|
|
1487
|
+
term._knownNonoccurringVariables = knownNonoccurringVariables;
|
|
1488
|
+
} else if ((term.name === 'get_atts' && term.arity === 2)
|
|
1489
|
+
|| (term.name === 'get_attr' && term.arity === 3)
|
|
1490
|
+
|| (term.name === '$get_attr_list' && term.arity === 2)
|
|
1491
|
+
|| (term.name === '$get_from_attr_list' && term.arity === 3)) {
|
|
1492
|
+
term._firstUseVariables = knownNonoccurringVariables;
|
|
1493
|
+
}
|
|
1494
|
+
if ((term.name === ',' || term.name === ';' || term.name === '->') && term.arity === 2) {
|
|
1495
|
+
attachKnownNonoccurringVariables(term.args[0], knownNonoccurringVariables);
|
|
1496
|
+
attachKnownNonoccurringVariables(term.args[1], knownNonoccurringVariables);
|
|
1497
|
+
} else if (term.name === '\\+' && term.arity === 1) {
|
|
1498
|
+
attachKnownNonoccurringVariables(term.args[0], knownNonoccurringVariables);
|
|
1499
|
+
} else if (term.name === ':' && term.arity === 2) {
|
|
1500
|
+
attachKnownNonoccurringVariables(term.args[1], knownNonoccurringVariables);
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1350
1504
|
function clauseLocalFreshPlan(clause) {
|
|
1351
1505
|
if (clause._localFreshPlan != null) return clause._localFreshPlan;
|
|
1352
1506
|
const seen = new Set();
|
|
@@ -2332,9 +2486,9 @@ function tryPushScalarFactRunFrames(stack, solver, goals, env, depth, active) {
|
|
|
2332
2486
|
const goal = goals[runLength];
|
|
2333
2487
|
if (!goal || goal.kind === 'releaseActive' || goal.kind === 'memoStore' || goal.kind === 'continueGoals') break;
|
|
2334
2488
|
if (goal.type !== COMPOUND) break;
|
|
2335
|
-
const def = solver.
|
|
2489
|
+
const def = solver.lookupBuiltin(goal.name, goal.arity);
|
|
2336
2490
|
if (def) break;
|
|
2337
|
-
const group = solver.
|
|
2491
|
+
const group = solver.lookupGroup(goal.name, goal.arity, goal.module ?? 'user');
|
|
2338
2492
|
if (!group || group.tabled || !group.scalarFactsOnly) break;
|
|
2339
2493
|
groups.push(group);
|
|
2340
2494
|
runLength++;
|
package/src/term.js
CHANGED
|
@@ -682,12 +682,10 @@ class SolverBindingStore {
|
|
|
682
682
|
this.baseValues = [];
|
|
683
683
|
this.slotLastTrail = [];
|
|
684
684
|
this.freeSlots = [];
|
|
685
|
-
this.trailNames = [];
|
|
686
685
|
this.trailSlots = [];
|
|
687
686
|
this.trailOldValues = [];
|
|
688
687
|
this.trailNewValues = [];
|
|
689
688
|
this.trailPreviousForSlot = [];
|
|
690
|
-
this.trailCreated = [];
|
|
691
689
|
if (bindings != null) {
|
|
692
690
|
for (const [name, value] of bindings) {
|
|
693
691
|
const slot = this.names.length;
|
|
@@ -709,15 +707,13 @@ class SolverBindingStore {
|
|
|
709
707
|
throw new Error('stale solver binding mark');
|
|
710
708
|
}
|
|
711
709
|
while (this.trailSlots.length > mark) {
|
|
712
|
-
const created = this.trailCreated.pop();
|
|
713
710
|
const previousForSlot = this.trailPreviousForSlot.pop();
|
|
714
711
|
this.trailNewValues.pop();
|
|
715
712
|
const oldValue = this.trailOldValues.pop();
|
|
716
713
|
const slot = this.trailSlots.pop();
|
|
717
|
-
const name = this.trailNames.pop();
|
|
718
714
|
this.slotLastTrail[slot] = previousForSlot;
|
|
719
|
-
if (
|
|
720
|
-
this.slots.delete(
|
|
715
|
+
if (oldValue === ENV_UNBOUND) {
|
|
716
|
+
this.slots.delete(this.names[slot]);
|
|
721
717
|
this.values[slot] = ENV_UNBOUND;
|
|
722
718
|
this.baseValues[slot] = ENV_UNBOUND;
|
|
723
719
|
this.names[slot] = null;
|
|
@@ -740,12 +736,10 @@ class SolverBindingStore {
|
|
|
740
736
|
this.slotLastTrail[slot] = -1;
|
|
741
737
|
}
|
|
742
738
|
const entry = this.trailSlots.length;
|
|
743
|
-
this.trailNames.push(name);
|
|
744
739
|
this.trailSlots.push(slot);
|
|
745
740
|
this.trailOldValues.push(this.values[slot]);
|
|
746
741
|
this.trailNewValues.push(value);
|
|
747
742
|
this.trailPreviousForSlot.push(this.slotLastTrail[slot] ?? -1);
|
|
748
|
-
this.trailCreated.push(created);
|
|
749
743
|
this.slotLastTrail[slot] = entry;
|
|
750
744
|
this.values[slot] = value;
|
|
751
745
|
}
|
|
@@ -862,7 +856,9 @@ class SolverEnv extends Env {
|
|
|
862
856
|
}
|
|
863
857
|
|
|
864
858
|
get(name) {
|
|
865
|
-
this.
|
|
859
|
+
if (this._solverBindingMark !== this._solverBindings.trailSlots.length) {
|
|
860
|
+
this._solverBindings.rollback(this._solverBindingMark);
|
|
861
|
+
}
|
|
866
862
|
const slot = this._solverBindings.slots.get(name);
|
|
867
863
|
if (slot === undefined) return undefined;
|
|
868
864
|
const value = this._solverBindings.values[slot];
|
|
@@ -878,9 +874,11 @@ class SolverEnv extends Env {
|
|
|
878
874
|
}
|
|
879
875
|
|
|
880
876
|
bind(name, term) {
|
|
881
|
-
this.
|
|
877
|
+
if (this._solverBindingMark !== this._solverBindings.trailSlots.length) {
|
|
878
|
+
this._solverBindings.rollback(this._solverBindingMark);
|
|
879
|
+
}
|
|
882
880
|
this._solverBindings.bind(name, term);
|
|
883
|
-
this._solverBindingMark = this._solverBindings.
|
|
881
|
+
this._solverBindingMark = this._solverBindings.trailSlots.length;
|
|
884
882
|
}
|
|
885
883
|
|
|
886
884
|
compactForDeepContinuation() {
|
|
@@ -933,6 +931,21 @@ function structuralVariableNames(term) {
|
|
|
933
931
|
if (term?.type === VAR) return [term.name];
|
|
934
932
|
if (term?.type !== COMPOUND) return EMPTY_ARGS;
|
|
935
933
|
|
|
934
|
+
if (!isCompactList(term) && term.arity === 2) {
|
|
935
|
+
const left = term.args[0];
|
|
936
|
+
const right = term.args[1];
|
|
937
|
+
const leftSimple = left?.type === VAR || isScalar(left);
|
|
938
|
+
const rightSimple = right?.type === VAR || isScalar(right);
|
|
939
|
+
if (leftSimple && rightSimple) {
|
|
940
|
+
if (left?.type === VAR) {
|
|
941
|
+
if (right?.type === VAR) return left.name === right.name ? [left.name] : [left.name, right.name];
|
|
942
|
+
return [left.name];
|
|
943
|
+
}
|
|
944
|
+
if (right?.type === VAR) return [right.name];
|
|
945
|
+
return EMPTY_ARGS;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
936
949
|
const cached = structuralVariableCache.get(term);
|
|
937
950
|
if (cached !== undefined) return cached;
|
|
938
951
|
|
|
@@ -1013,12 +1026,17 @@ function occurs(variableName, term, env) {
|
|
|
1013
1026
|
}
|
|
1014
1027
|
|
|
1015
1028
|
const pending = initial.slice();
|
|
1016
|
-
const
|
|
1029
|
+
const seenSmall = [];
|
|
1030
|
+
let seenLarge = null;
|
|
1017
1031
|
for (let index = 0; index < pending.length; index++) {
|
|
1018
1032
|
const name = pending[index];
|
|
1019
1033
|
if (name === variableName) return true;
|
|
1020
|
-
|
|
1021
|
-
|
|
1034
|
+
let alreadySeen = false;
|
|
1035
|
+
if (seenLarge != null) alreadySeen = seenLarge.has(name);
|
|
1036
|
+
else for (let j = 0; j < seenSmall.length; j++) { if (seenSmall[j] === name) { alreadySeen = true; break; } }
|
|
1037
|
+
if (alreadySeen) continue;
|
|
1038
|
+
if (seenLarge != null) seenLarge.add(name);
|
|
1039
|
+
else { seenSmall.push(name); if (seenSmall.length === 8) seenLarge = new Set(seenSmall); }
|
|
1022
1040
|
const binding = env?.get(name);
|
|
1023
1041
|
if (binding === undefined) continue;
|
|
1024
1042
|
const names = structuralVariableNames(binding);
|
|
@@ -1040,9 +1058,10 @@ export function unify(left, right, env, options = {}) {
|
|
|
1040
1058
|
// construction fast paths share this internal proof; ordinary unification
|
|
1041
1059
|
// remains fully occurs-checked.
|
|
1042
1060
|
const knownNonoccurringVariables = options.knownNonoccurringVariables ?? null;
|
|
1043
|
-
const stack = [
|
|
1061
|
+
const stack = [left, right];
|
|
1044
1062
|
while (stack.length) {
|
|
1045
|
-
let
|
|
1063
|
+
let b = stack.pop();
|
|
1064
|
+
let a = stack.pop();
|
|
1046
1065
|
a = deref(a, env);
|
|
1047
1066
|
b = deref(b, env);
|
|
1048
1067
|
|
|
@@ -1053,7 +1072,7 @@ export function unify(left, right, env, options = {}) {
|
|
|
1053
1072
|
a = deref(a, env);
|
|
1054
1073
|
b = deref(b, env);
|
|
1055
1074
|
if (a.type !== VAR || b.type !== VAR || a.name === b.name) {
|
|
1056
|
-
stack.push(
|
|
1075
|
+
stack.push(a, b);
|
|
1057
1076
|
continue;
|
|
1058
1077
|
}
|
|
1059
1078
|
}
|
|
@@ -1083,7 +1102,7 @@ export function unify(left, right, env, options = {}) {
|
|
|
1083
1102
|
if (!env.preparePrologAttributeUnification(a, b)) return false;
|
|
1084
1103
|
a = deref(a, env);
|
|
1085
1104
|
b = deref(b, env);
|
|
1086
|
-
if (a.type !== VAR) { stack.push(
|
|
1105
|
+
if (a.type !== VAR) { stack.push(a, b); continue; }
|
|
1087
1106
|
}
|
|
1088
1107
|
markCompactVariableBound(a, b, env);
|
|
1089
1108
|
env?.dropPrologAttributes?.(a.name);
|
|
@@ -1101,7 +1120,7 @@ export function unify(left, right, env, options = {}) {
|
|
|
1101
1120
|
if (!env.preparePrologAttributeUnification(b, a)) return false;
|
|
1102
1121
|
b = deref(b, env);
|
|
1103
1122
|
a = deref(a, env);
|
|
1104
|
-
if (b.type !== VAR) { stack.push(
|
|
1123
|
+
if (b.type !== VAR) { stack.push(a, b); continue; }
|
|
1105
1124
|
}
|
|
1106
1125
|
markCompactVariableBound(b, a, env);
|
|
1107
1126
|
env?.dropPrologAttributes?.(b.name);
|
|
@@ -1121,7 +1140,7 @@ export function unify(left, right, env, options = {}) {
|
|
|
1121
1140
|
|
|
1122
1141
|
if (a.type === COMPOUND) {
|
|
1123
1142
|
if (a.name !== b.name || a.arity !== b.arity) return false;
|
|
1124
|
-
for (let i = a.arity - 1; i >= 0; i--) stack.push(
|
|
1143
|
+
for (let i = a.arity - 1; i >= 0; i--) stack.push(a.args[i], b.args[i]);
|
|
1125
1144
|
continue;
|
|
1126
1145
|
}
|
|
1127
1146
|
|