eyeprolog 1.5.31 → 1.5.32
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 +2 -2
- package/examples/bench.pl +57 -0
- package/examples/output/bench.pl +1 -0
- package/package.json +3 -2
- 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 +1 -1
- package/test/conformance/ISO-WG17-EXTENSIONS.md +68 -0
- package/test/lips-benchmark.mjs +93 -0
- package/test/run-benchmark-tests.mjs +27 -1
- package/test/run-regression.mjs +8 -0
- package/the-art-of-eyeprolog.md +12 -2
package/README.md
CHANGED
|
@@ -86,9 +86,9 @@ 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
|
|
@@ -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.32",
|
|
7
7
|
"description": "EyeProlog turns facts and rules into answers and proofs.",
|
|
8
8
|
"type": "module",
|
|
9
9
|
"main": "./index.js",
|
|
@@ -70,6 +70,7 @@
|
|
|
70
70
|
"benchmark": "node test/benchmark.mjs",
|
|
71
71
|
"benchmark:baseline": "node test/benchmark.mjs --save .benchmarks/baseline.json",
|
|
72
72
|
"test:benchmark": "node test/run-benchmark-tests.mjs",
|
|
73
|
-
"generate:reference": "node tools/generate-predicate-reference.mjs"
|
|
73
|
+
"generate:reference": "node tools/generate-predicate-reference.mjs",
|
|
74
|
+
"benchmark:lips": "node test/lips-benchmark.mjs"
|
|
74
75
|
}
|
|
75
76
|
}
|
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
|
|
|
@@ -8,6 +8,16 @@
|
|
|
8
8
|
],
|
|
9
9
|
"expectedSha256": "a0aa24fcbc87c394a4da789ac861b8e18cabad5e0d73b25ddc02df5dd4376c1b"
|
|
10
10
|
},
|
|
11
|
+
{
|
|
12
|
+
"name": "classic-nrev",
|
|
13
|
+
"group": "recursion/lips",
|
|
14
|
+
"file": "examples/bench.pl",
|
|
15
|
+
"goals": [
|
|
16
|
+
"bench_result(X0)"
|
|
17
|
+
],
|
|
18
|
+
"expectedSha256": "8045dc0730e4996a7b01fb2348311fb175ed53f4c49d14f82e01a3d470602255",
|
|
19
|
+
"logicalInferences": 496
|
|
20
|
+
},
|
|
11
21
|
{
|
|
12
22
|
"name": "clpz-sudoku",
|
|
13
23
|
"group": "clpz",
|
package/test/benchmark.mjs
CHANGED
|
@@ -69,6 +69,9 @@ for (const item of manifest) {
|
|
|
69
69
|
if (!/^[0-9a-f]{64}$/.test(item.expectedSha256)) {
|
|
70
70
|
throw new Error(`invalid expectedSha256 for benchmark ${item.name}`);
|
|
71
71
|
}
|
|
72
|
+
if (item.logicalInferences != null && (!Number.isInteger(item.logicalInferences) || item.logicalInferences <= 0)) {
|
|
73
|
+
throw new Error(`invalid logicalInferences for benchmark ${item.name}`);
|
|
74
|
+
}
|
|
72
75
|
}
|
|
73
76
|
|
|
74
77
|
const selected = manifest.filter((item) => {
|
|
@@ -115,6 +118,16 @@ function formatMs(value) {
|
|
|
115
118
|
return `${value.toFixed(1)} ms`;
|
|
116
119
|
}
|
|
117
120
|
|
|
121
|
+
function lipsFor(logicalInferences, medianMs) {
|
|
122
|
+
if (!Number.isFinite(logicalInferences) || !Number.isFinite(medianMs) || medianMs <= 0) return null;
|
|
123
|
+
return logicalInferences * 1000 / medianMs;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function formatLips(value) {
|
|
127
|
+
if (value == null) return '—';
|
|
128
|
+
return Math.round(value).toLocaleString('en-US');
|
|
129
|
+
}
|
|
130
|
+
|
|
118
131
|
function changePercent(medianMs, baselineMs) {
|
|
119
132
|
if (baselineMs == null || baselineMs === 0) return null;
|
|
120
133
|
return ((medianMs - baselineMs) / baselineMs) * 100;
|
|
@@ -213,6 +226,8 @@ for (const item of selected) {
|
|
|
213
226
|
answerLines: worker.answerLines,
|
|
214
227
|
outputBytes: worker.outputBytes,
|
|
215
228
|
sha256: worker.digest,
|
|
229
|
+
logicalInferences: item.logicalInferences ?? null,
|
|
230
|
+
lips: lipsFor(item.logicalInferences, medianMs),
|
|
216
231
|
baselineMs: baselineItem?.medianMs ?? null,
|
|
217
232
|
changePercent: changePercent(medianMs, baselineItem?.medianMs ?? null),
|
|
218
233
|
});
|
|
@@ -250,10 +265,11 @@ if (options.json) {
|
|
|
250
265
|
results,
|
|
251
266
|
}, null, 2)}\n`);
|
|
252
267
|
} else {
|
|
253
|
-
const headers = ['Benchmark', 'Median/op', 'Range/op', 'Batch', 'Baseline', 'Change', 'Answers'];
|
|
268
|
+
const headers = ['Benchmark', 'Median/op', 'LIPS', 'Range/op', 'Batch', 'Baseline', 'Change', 'Answers'];
|
|
254
269
|
const rows = results.map((item) => [
|
|
255
270
|
item.name,
|
|
256
271
|
formatMs(item.medianMs),
|
|
272
|
+
formatLips(item.lips),
|
|
257
273
|
`${formatMs(item.minMs)}–${formatMs(item.maxMs)}`,
|
|
258
274
|
String(item.batchSize),
|
|
259
275
|
formatMs(item.baselineMs),
|
|
@@ -283,6 +299,7 @@ if (options.json) {
|
|
|
283
299
|
if (baselinePath == null && !baselineWarning) process.stdout.write('No timing baseline found; run npm run benchmark:baseline to create .benchmarks/baseline.json.\n');
|
|
284
300
|
if (options.save != null) process.stdout.write(`Saved timing baseline: ${path.relative(root, options.save)}\n`);
|
|
285
301
|
process.stdout.write('Change compares the current median/op directly with the saved baseline median/op; the measured range is shown separately.\n');
|
|
302
|
+
process.stdout.write('The classic-nrev LIPS column is a quick wall-clock estimate. Use npm run benchmark:lips for the Quintus-style dummy-subtracted CPU measurement.\n');
|
|
286
303
|
if (summary.comparable > 0) {
|
|
287
304
|
process.stdout.write('Suite score is the geometric mean of current/baseline ratios, so every benchmark has equal relative weight; Time-weighted total compares summed medians and is dominated by longer workloads.\n');
|
|
288
305
|
}
|
|
@@ -254,7 +254,7 @@ not interpret the following as core-language features:
|
|
|
254
254
|
- Part 3 grammar-rule expansion and `phrase/2-3`;
|
|
255
255
|
- explicit tabling, EyeProlog well-founded evaluation via `tnot/1` and
|
|
256
256
|
`wfs_truth/2`, WFS runtime statistics, and recursive numeric execution shortcuts;
|
|
257
|
-
- normal-profile integer digit separators and Trealla-compatible
|
|
257
|
+
- normal-profile integer digit separators (the accepted WG17 single-underscore/layout form documented at <https://www.complang.tuwien.ac.at/ulrich/iso-prolog/digit_separators>) and Trealla-compatible
|
|
258
258
|
`"text"||Tail` double-quoted-list splicing;
|
|
259
259
|
- the JavaScript API `stringTerm(Text)` additional term type (normal mode only).
|
|
260
260
|
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# ISO/WG17 compatibility extensions
|
|
2
|
+
|
|
3
|
+
EyeProlog separates its strict ISO core from carefully selected compatibility
|
|
4
|
+
extensions. `isoStrict: true` targets ISO/IEC 13211-1:1995 plus Corrigenda 1-3;
|
|
5
|
+
newer or non-core features belong in the normal profile until they are part of
|
|
6
|
+
the claimed strict baseline.
|
|
7
|
+
|
|
8
|
+
## Selection rule
|
|
9
|
+
|
|
10
|
+
A proposed extension is a good candidate when it has all of these properties:
|
|
11
|
+
|
|
12
|
+
1. it solves a concrete portability or usability problem;
|
|
13
|
+
2. its syntax/semantics are precise enough to test independently;
|
|
14
|
+
3. it has clear WG17 momentum, a published ISO technical specification, or
|
|
15
|
+
established interoperable implementation practice; and
|
|
16
|
+
4. it does not silently change strict ISO behavior.
|
|
17
|
+
|
|
18
|
+
This intentionally excludes adopting every discussion item on the ISO Prolog
|
|
19
|
+
working-notes pages. Draft/editorial proposals can change, and controversial
|
|
20
|
+
changes are more valuable as tracked tests/documentation than as premature
|
|
21
|
+
language behavior.
|
|
22
|
+
|
|
23
|
+
## Implemented high-value items
|
|
24
|
+
|
|
25
|
+
- **Digit separators (normal profile).** Decimal, binary, octal, and hexadecimal
|
|
26
|
+
integer constants accept a single underscore between digit groups. Layout,
|
|
27
|
+
line comments, and block comments may follow the underscore before the next
|
|
28
|
+
digit, matching the accepted WG17 options described at
|
|
29
|
+
<https://www.complang.tuwien.ac.at/ulrich/iso-prolog/digit_separators>.
|
|
30
|
+
Consecutive/trailing separators and separators inside floats/exponents are
|
|
31
|
+
rejected. Strict ISO mode rejects the extension.
|
|
32
|
+
- **Corrigenda 1-3.** The strict suite explicitly covers the published
|
|
33
|
+
corrections, including `variable_names/1` write behavior.
|
|
34
|
+
- **Cleanup.** `setup_call_cleanup/3` and `call_cleanup/2` are available in the
|
|
35
|
+
normal compatibility/library surface and are regression tested.
|
|
36
|
+
- **DCGs.** EyeProlog supports grammar-rule expansion and `phrase/2-3` in the
|
|
37
|
+
normal profile, aligned with ISO/IEC TS 13211-3:2025. These remain outside
|
|
38
|
+
the Part 1 strict-core claim.
|
|
39
|
+
- **Prologue/library portability.** The normal profile includes a portable
|
|
40
|
+
standard-library surface and autoloading rather than treating those
|
|
41
|
+
predicates as Part 1 core built-ins.
|
|
42
|
+
|
|
43
|
+
## What should be considered next?
|
|
44
|
+
|
|
45
|
+
The best next work is evidence-driven rather than feature-count driven:
|
|
46
|
+
|
|
47
|
+
- keep running the public WG17 syntax/conformance corpora and close concrete
|
|
48
|
+
interoperability gaps they expose;
|
|
49
|
+
- prefer published specifications (for example additional fully testable parts
|
|
50
|
+
of the DCG TS) over draft syntax experiments;
|
|
51
|
+
- evaluate Prolog-prologue predicates when they materially improve source
|
|
52
|
+
portability and can be implemented with compatible errors/modes;
|
|
53
|
+
- keep module conformance as a separate Part 2 effort instead of mixing it into
|
|
54
|
+
the Part 1 strict claim; and
|
|
55
|
+
- track draft STC/WG17 proposals in tests/status documents until their intended
|
|
56
|
+
behavior is stable enough to adopt explicitly.
|
|
57
|
+
|
|
58
|
+
In particular, the digit-separator page currently leaves float separator forms
|
|
59
|
+
such as `1_1.25`, `11.2_5`, and `1.0e1_0` unresolved. EyeProlog therefore does
|
|
60
|
+
**not** generalize integer separators into float syntax.
|
|
61
|
+
|
|
62
|
+
## Regression expectations
|
|
63
|
+
|
|
64
|
+
Every accepted compatibility extension should have tests for positive syntax,
|
|
65
|
+
negative/boundary syntax, stream or conversion entry points where applicable,
|
|
66
|
+
and an explicit strict-mode rejection when the feature is not in the strict
|
|
67
|
+
baseline. This keeps the normal profile useful without weakening the meaning of
|
|
68
|
+
`isoStrict: true`.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import process from 'node:process';
|
|
4
|
+
import { performance } from 'node:perf_hooks';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { Program, parseGoalText, run } from '../index.js';
|
|
7
|
+
|
|
8
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
9
|
+
|
|
10
|
+
function integer(value, name, minimum) {
|
|
11
|
+
const n = Number(value);
|
|
12
|
+
if (!Number.isInteger(n) || n < minimum) throw new Error(`${name} must be an integer >= ${minimum}`);
|
|
13
|
+
return n;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const options = { count: 50, runs: 5, warmup: 2, json: false };
|
|
17
|
+
for (let i = 2; i < process.argv.length; i++) {
|
|
18
|
+
const arg = process.argv[i];
|
|
19
|
+
if (arg === '--count') options.count = integer(process.argv[++i], '--count', 1);
|
|
20
|
+
else if (arg === '--runs') options.runs = integer(process.argv[++i], '--runs', 1);
|
|
21
|
+
else if (arg === '--warmup') options.warmup = integer(process.argv[++i], '--warmup', 0);
|
|
22
|
+
else if (arg === '--json') options.json = true;
|
|
23
|
+
else if (arg === '-h' || arg === '--help') {
|
|
24
|
+
process.stdout.write('Usage: node test/lips-benchmark.mjs [--count N] [--runs N] [--warmup N] [--json]\n');
|
|
25
|
+
process.exit(0);
|
|
26
|
+
} else throw new Error(`unknown option: ${arg}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const source = await fs.readFile(path.join(root, 'examples', 'bench.pl'), 'utf8');
|
|
30
|
+
const program = Program.parse(source);
|
|
31
|
+
const dobench = parseGoalText(`dobench(${options.count})`);
|
|
32
|
+
const dodummy = parseGoalText(`dodummy(${options.count})`);
|
|
33
|
+
|
|
34
|
+
function execute(goal) {
|
|
35
|
+
const cpuStart = process.cpuUsage();
|
|
36
|
+
const wallStart = performance.now();
|
|
37
|
+
const result = run(program, { goals: [goal] });
|
|
38
|
+
const wallMs = performance.now() - wallStart;
|
|
39
|
+
const cpu = process.cpuUsage(cpuStart);
|
|
40
|
+
if (result.haltCode != null) throw new Error(`benchmark halted with code ${result.haltCode}`);
|
|
41
|
+
if (!result.stdout) throw new Error('benchmark goal produced no success answer');
|
|
42
|
+
return { cpuMs: (cpu.user + cpu.system) / 1000, wallMs };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
for (let i = 0; i < options.warmup; i++) {
|
|
46
|
+
execute(dodummy);
|
|
47
|
+
execute(dobench);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const samples = [];
|
|
51
|
+
for (let i = 0; i < options.runs; i++) {
|
|
52
|
+
const dummy = execute(dodummy);
|
|
53
|
+
const bench = execute(dobench);
|
|
54
|
+
const netCpuMs = bench.cpuMs - dummy.cpuMs;
|
|
55
|
+
const netWallMs = bench.wallMs - dummy.wallMs;
|
|
56
|
+
if (netCpuMs <= 0) throw new Error(`non-positive control-subtracted CPU time (${netCpuMs.toFixed(3)} ms); increase --count`);
|
|
57
|
+
samples.push({
|
|
58
|
+
dummyCpuMs: dummy.cpuMs,
|
|
59
|
+
benchCpuMs: bench.cpuMs,
|
|
60
|
+
netCpuMs,
|
|
61
|
+
netWallMs,
|
|
62
|
+
lips: 496 * options.count * 1000 / netCpuMs,
|
|
63
|
+
wallLips: netWallMs > 0 ? 496 * options.count * 1000 / netWallMs : null,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function median(values) {
|
|
68
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
69
|
+
const middle = Math.floor(sorted.length / 2);
|
|
70
|
+
return sorted.length % 2 ? sorted[middle] : (sorted[middle - 1] + sorted[middle]) / 2;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const result = {
|
|
74
|
+
methodology: 'Quintus 1984 naive reverse; dobench minus dodummy; 496 calls/reversal',
|
|
75
|
+
count: options.count,
|
|
76
|
+
runs: options.runs,
|
|
77
|
+
warmup: options.warmup,
|
|
78
|
+
lips: median(samples.map((sample) => sample.lips)),
|
|
79
|
+
wallLips: median(samples.map((sample) => sample.wallLips).filter(Number.isFinite)),
|
|
80
|
+
netCpuMs: median(samples.map((sample) => sample.netCpuMs)),
|
|
81
|
+
samples,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
if (options.json) {
|
|
85
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
86
|
+
} else {
|
|
87
|
+
process.stdout.write(`EyeProlog classic nrev: ${Math.round(result.lips).toLocaleString('en-US')} LIPS\n`);
|
|
88
|
+
process.stdout.write(` ${options.count} reversals/run, ${options.runs} measured runs, ${options.warmup} warmups\n`);
|
|
89
|
+
process.stdout.write(` median control-subtracted CPU: ${result.netCpuMs.toFixed(1)} ms\n`);
|
|
90
|
+
if (Number.isFinite(result.wallLips)) {
|
|
91
|
+
process.stdout.write(` wall-clock cross-check: ${Math.round(result.wallLips).toLocaleString('en-US')} LIPS\n`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -42,8 +42,10 @@ function runWorker(item) {
|
|
|
42
42
|
]);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
ok(Array.isArray(manifest) && manifest.length ===
|
|
45
|
+
ok(Array.isArray(manifest) && manifest.length === 21, 'benchmark manifest should contain exactly 21 representative workloads');
|
|
46
46
|
ok(new Set(manifest.map((item) => item.name)).size === manifest.length, 'benchmark names should be unique');
|
|
47
|
+
const classicNrev = manifest.find((item) => item.name === 'classic-nrev');
|
|
48
|
+
ok(classicNrev?.logicalInferences === 496, 'classic nrev should retain the traditional 496-call LIPS accounting');
|
|
47
49
|
|
|
48
50
|
for (const item of manifest) {
|
|
49
51
|
ok(/^[0-9a-f]{64}$/.test(item.expectedSha256), `${item.name} should have a committed semantic checksum`);
|
|
@@ -70,4 +72,28 @@ ok(adaptive.results[0].batchSize > 1, 'adaptive benchmark smoke test should batc
|
|
|
70
72
|
ok(adaptive.results[0].sha256 === manifest.find((item) => item.name === 'dcg-command').expectedSha256,
|
|
71
73
|
'adaptive batching should preserve the semantic checksum');
|
|
72
74
|
|
|
75
|
+
const nrev = await spawnJson([
|
|
76
|
+
path.join(root, 'test', 'benchmark.mjs'),
|
|
77
|
+
'--filter', 'classic-nrev',
|
|
78
|
+
'--runs', '1',
|
|
79
|
+
'--warmup', '0',
|
|
80
|
+
'--target-ms', '0',
|
|
81
|
+
'--json',
|
|
82
|
+
]);
|
|
83
|
+
ok(nrev.results.length === 1, 'classic nrev benchmark should be selectable');
|
|
84
|
+
ok(nrev.results[0].logicalInferences === 496, 'classic nrev JSON should expose its logical-inference count');
|
|
85
|
+
ok(Number.isFinite(nrev.results[0].lips) && nrev.results[0].lips > 0, 'classic nrev JSON should report positive LIPS');
|
|
86
|
+
|
|
87
|
+
const classicLips = await spawnJson([
|
|
88
|
+
path.join(root, 'test', 'lips-benchmark.mjs'),
|
|
89
|
+
'--count', '20',
|
|
90
|
+
'--runs', '1',
|
|
91
|
+
'--warmup', '1',
|
|
92
|
+
'--json',
|
|
93
|
+
]);
|
|
94
|
+
ok(classicLips.count === 20, 'classic LIPS harness should honor the requested reversal count');
|
|
95
|
+
ok(classicLips.methodology.includes('496 calls/reversal'), 'classic LIPS harness should identify the historical accounting');
|
|
96
|
+
ok(Number.isFinite(classicLips.lips) && classicLips.lips > 0, 'classic LIPS harness should report positive CPU LIPS');
|
|
97
|
+
ok(Number.isFinite(classicLips.netCpuMs) && classicLips.netCpuMs > 0, 'classic LIPS harness should subtract positive CPU time');
|
|
98
|
+
|
|
73
99
|
process.stdout.write(`Benchmark harness tests: ${passed}/${passed} passed.\n`);
|
package/test/run-regression.mjs
CHANGED
|
@@ -7330,11 +7330,19 @@ function whiteBoxCases() {
|
|
|
7330
7330
|
'program radix literal');
|
|
7331
7331
|
assertEqual(parseNumberTokenText('1_ /* group */ 000').name, '1000',
|
|
7332
7332
|
'number-token conversion with layout');
|
|
7333
|
+
assertEqual(parseNumberTokenText('0b1010_0101').name, '165', 'binary number-token conversion');
|
|
7334
|
+
assertEqual(parseNumberTokenText('0o7_% group\n7').name, '63', 'octal number-token conversion with line comment');
|
|
7335
|
+
assertEqual(parseNumberTokenText('0xCA_/* group */FE').name, '51966', 'hex number-token conversion with block comment');
|
|
7333
7336
|
assertEqual(
|
|
7334
7337
|
run('', { goal: 'number_chars(N,"1_000")' }).stdout,
|
|
7335
7338
|
'number_chars(1000, "1_000").\n',
|
|
7336
7339
|
'normal number_chars input',
|
|
7337
7340
|
);
|
|
7341
|
+
assertEqual(
|
|
7342
|
+
run('', { goal: 'number_codes(N,[48,120,67,65,95,70,69])' }).stdout,
|
|
7343
|
+
'number_codes(51966, [48, 120, 67, 65, 95, 70, 69]).\n',
|
|
7344
|
+
'normal number_codes radix input',
|
|
7345
|
+
);
|
|
7338
7346
|
assertEqual(
|
|
7339
7347
|
run('', { goal: 'read_term(N, [])', ioOptions: { input: '1_ /* group */ 000. ' } }).stdout,
|
|
7340
7348
|
'read_term(1000, []).\n',
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -9743,7 +9743,7 @@ Review questions:
|
|
|
9743
9743
|
</figure>
|
|
9744
9744
|
|
|
9745
9745
|
The [examples directory](https://github.com/eyereasoner/eyeprolog/tree/main/examples/) is the book's executable companion. The
|
|
9746
|
-
top-level directory contains **
|
|
9746
|
+
top-level directory contains **225 self-contained runnable programs**. Every
|
|
9747
9747
|
source program has an exact answer file under
|
|
9748
9748
|
[examples/output](https://github.com/eyereasoner/eyeprolog/tree/main/examples/output/), and **61 selected programs** have a checked
|
|
9749
9749
|
explanation under [examples/proof](https://github.com/eyereasoner/eyeprolog/tree/main/examples/proof/). The thematic lists link every top-level program and open the program
|
|
@@ -10173,9 +10173,10 @@ and fast:
|
|
|
10173
10173
|
```sh
|
|
10174
10174
|
npm run benchmark
|
|
10175
10175
|
npm run benchmark:baseline
|
|
10176
|
+
npm run benchmark:lips
|
|
10176
10177
|
```
|
|
10177
10178
|
|
|
10178
|
-
The benchmark suite contains
|
|
10179
|
+
The benchmark suite contains 21 representative workloads and stores their
|
|
10179
10180
|
semantic output digests in the repository, while wall-clock baselines remain
|
|
10180
10181
|
machine-local under `.benchmarks/` because absolute timings are machine-specific.
|
|
10181
10182
|
Each benchmark runs in its own fresh Node worker. Inside that worker, one untimed
|
|
@@ -10184,6 +10185,15 @@ with independent `run()` calls until a batch is roughly 400 ms long. After one
|
|
|
10184
10185
|
warm-up batch, five measured batches are reported as milliseconds per workload
|
|
10185
10186
|
execution. Naturally long workloads keep a batch size of one.
|
|
10186
10187
|
|
|
10188
|
+
The classic `examples/bench.pl` workload also has a dedicated LIPS harness.
|
|
10189
|
+
`npm run benchmark:lips` follows the 1984 Quintus method more closely than the
|
|
10190
|
+
generic runner: `dobench/1` and `dodummy/1` execute failure-driven loops inside
|
|
10191
|
+
Prolog, the dummy CPU time is subtracted, and the remaining time is converted
|
|
10192
|
+
using 496 procedure calls for one reversal of the 30-element list. Node's
|
|
10193
|
+
process CPU clock is used for the primary figure, with wall-clock LIPS printed
|
|
10194
|
+
as a cross-check. Because LIPS measures a deliberately small recursive kernel,
|
|
10195
|
+
it is useful for engine tuning but is not a complete application benchmark.
|
|
10196
|
+
|
|
10187
10197
|
The report shows the median, per-operation range, chosen batch size, saved
|
|
10188
10198
|
baseline median, and the percentage change between the current median and baseline
|
|
10189
10199
|
median. The range remains visible as context, but it does not suppress or reinterpret
|