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 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
- - [Conformance report](conformance-report.md) — generated executable conformance status and corpus summary
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 20 checksum-protected wall-clock benchmarks spanning recursion/indexing, constraints, tabling/WFS, DCGs, Eyelet, search, term I/O, attributes, and rewriting. 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. Details are in [*The Art of EyeProlog*](the-art-of-eyeprolog.md).
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).
@@ -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.31",
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
- "generate": "node tools/generate-library-autoload-index.mjs && node tools/generate-predicate-reference.mjs && node tools/extract-book-examples.mjs",
50
- "generate:autoload": "node tools/generate-library-autoload-index.mjs",
51
- "test:eyeprolog": "node test/run-all.mjs",
52
- "test:conformance": "node test/run-conformance.mjs",
53
- "test:iso-strict": "node test/run-iso-strict.mjs",
54
- "test:interop": "node test/run-interop.mjs",
55
- "test:openrulebench": "node test/run-openrulebench.mjs",
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:regression:api": "node test/run-regression.mjs api",
60
- "test:regression:core": "node test/run-regression.mjs regression",
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
- "test:benchmark": "node test/run-benchmark-tests.mjs",
73
- "generate:reference": "node tools/generate-predicate-reference.mjs"
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
@@ -453,6 +453,7 @@
453
453
  "bayes-diagnosis",
454
454
  "bayes-therapy",
455
455
  "beam-deflection",
456
+ "bench",
456
457
  "binomial-vandermonde",
457
458
  "blocks-world-planning",
458
459
  "bmi",
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 id = nextFreshId();
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 = selectReadyDeterministicBuiltin(goals, env, this.registry);
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.registry.get(goal.name, goal.arity) : null;
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.program.findGroup(goal.name, goal.arity, goal.module ?? 'user');
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 id = nextFreshId();
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. Cut-free, non-recursive library helpers need
1308
- // neither. Copying their full active stack at every recursive step made
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 ? [...active, invocation] : active;
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.registry.get(goal.name, goal.arity);
2489
+ const def = solver.lookupBuiltin(goal.name, goal.arity);
2336
2490
  if (def) break;
2337
- const group = solver.program.findGroup(goal.name, goal.arity, goal.module ?? 'user');
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 (created) {
720
- this.slots.delete(name);
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._activateBindings();
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._activateBindings();
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.mark();
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 seenVariables = new Set();
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
- if (seenVariables.has(name)) continue;
1021
- seenVariables.add(name);
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 = [[left, right]];
1061
+ const stack = [left, right];
1044
1062
  while (stack.length) {
1045
- let [a, b] = stack.pop();
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([a, b]);
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([a, b]); continue; }
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([a, b]); continue; }
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([a.args[i], b.args[i]]);
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