eyeprolog 1.3.13 → 1.3.14
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 +9 -4
- package/package.json +1 -1
- package/src/dcg.js +6 -1
- package/src/iso.js +58 -16
- package/src/lib/iso_ext.pl +17 -3
- package/src/repl.js +43 -12
- package/src/solver.js +156 -3
- package/src/standard-library.js +7 -2
- package/src/term.js +53 -6
- package/test/run-regression.mjs +116 -6
- package/the-art-of-eyeprolog.md +31 -19
package/README.md
CHANGED
|
@@ -201,8 +201,8 @@ noun --> [world] | [prolog].
|
|
|
201
201
|
%% goal: phrase(sentence, Words)
|
|
202
202
|
```
|
|
203
203
|
|
|
204
|
-
EyeProlog also adds
|
|
205
|
-
profile. **
|
|
204
|
+
EyeProlog also adds 128 public library predicate indicators to its 129-entry ISO
|
|
205
|
+
profile. **88 are implemented entirely as ordinary Prolog clauses** in focused
|
|
206
206
|
modules under `src/lib/`; the remaining control predicates and finite-domain
|
|
207
207
|
`library(clpz)` kernel use backtrackable host support.
|
|
208
208
|
They are ISO/IEC 13211-2 modules loaded explicitly by purpose, such as
|
|
@@ -225,8 +225,13 @@ headroom, so an exhausted finite heap is reported as a catchable
|
|
|
225
225
|
`resource_error(memory)` instead of degenerating into quadratic list checks.
|
|
226
226
|
|
|
227
227
|
`library(iso_ext)` is also accepted as a common interop module name.
|
|
228
|
-
EyeProlog exports `call_nth/2`
|
|
229
|
-
|
|
228
|
+
EyeProlog exports `call_nth/2`, `time/1`, and the DCG helper `...//0` there.
|
|
229
|
+
The latter describes an arbitrary number of input elements and supports the
|
|
230
|
+
nonterminal hand-off benchmark discussed in issue #49. These common predicates
|
|
231
|
+
may be imported explicitly, while source/CLI/API dependency loading can resolve
|
|
232
|
+
their unqualified forms conservatively. For Trealla-style interactive timing,
|
|
233
|
+
`time/1` is also available directly in the normal EyeProlog runtime; strict ISO
|
|
234
|
+
mode does not expose it.
|
|
230
235
|
The aligned `library(lists)` and `library(iso_ext)` exports are kept disjoint so
|
|
231
236
|
they can be imported together without an accidental import conflict. EyeProlog's
|
|
232
237
|
legacy `library(prologue)` remains a compatibility umbrella and should be
|
package/package.json
CHANGED
package/src/dcg.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Grammar rules are lowered to ordinary clauses during program preparation;
|
|
3
3
|
// phrase/2-3 use the same body expansion for dynamically supplied grammars.
|
|
4
4
|
import {
|
|
5
|
-
ATOM, COMPOUND, VAR, Env, atom, compound, deref, emptyList,
|
|
5
|
+
ATOM, COMPOUND, VAR, Env, atom, compactListLength, compound, deref, emptyList,
|
|
6
6
|
flattenConjunction, variable,
|
|
7
7
|
} from './term.js';
|
|
8
8
|
import { PrologError } from './iso.js';
|
|
@@ -280,6 +280,11 @@ export function expandDcgRuleClause(clause, defaultModule = 'user') {
|
|
|
280
280
|
export function isListOrPartialList(term, env) {
|
|
281
281
|
const seen = new Set();
|
|
282
282
|
let cursor = deref(term, env);
|
|
283
|
+
// A compact list skeleton has a fixed proper spine by construction. Its
|
|
284
|
+
// elements may later receive bindings, but those bindings cannot change the
|
|
285
|
+
// list tail shape, so phrase/2-3 need not expand the entire lazy spine merely
|
|
286
|
+
// to validate it as a list.
|
|
287
|
+
if (compactListLength(cursor) != null) return true;
|
|
283
288
|
while (cursor.type === COMPOUND && cursor.name === '.' && cursor.arity === 2) {
|
|
284
289
|
if (seen.has(cursor)) return false;
|
|
285
290
|
seen.add(cursor);
|
package/src/iso.js
CHANGED
|
@@ -176,6 +176,8 @@ export const eyePrologLibraryBuiltins = {
|
|
|
176
176
|
registry.add('eyeprolog__call_nth', 2, callNthBuiltin, { eyePrologLibrary: true });
|
|
177
177
|
registry.add('eyeprolog__countall', 2, countAllBuiltin, { eyePrologLibrary: true });
|
|
178
178
|
registry.add('eyeprolog__freeze', 2, freezeBuiltin, { eyePrologLibrary: true });
|
|
179
|
+
registry.add('time', 1, timeBuiltin, { eyePrologLibrary: true });
|
|
180
|
+
registry.add('eyeprolog__time', 1, timeBuiltin, { eyePrologLibrary: true });
|
|
179
181
|
},
|
|
180
182
|
};
|
|
181
183
|
|
|
@@ -1899,29 +1901,31 @@ const bagofBuiltin = allSolutionsBuiltin(false);
|
|
|
1899
1901
|
const setofBuiltin = allSolutionsBuiltin(true);
|
|
1900
1902
|
|
|
1901
1903
|
function callable(term, env) {
|
|
1902
|
-
term =
|
|
1904
|
+
term = deref(term, env);
|
|
1903
1905
|
if (term.type === VAR) throw new PrologError('instantiation_error');
|
|
1904
1906
|
if (term.type !== ATOM && term.type !== COMPOUND) throw new PrologError('type_error(callable)', term);
|
|
1905
|
-
validateControlCallable(term, term);
|
|
1907
|
+
validateControlCallable(term, term, env);
|
|
1906
1908
|
return term;
|
|
1907
1909
|
}
|
|
1908
|
-
function validateControlCallable(term, culprit) {
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1910
|
+
function validateControlCallable(term, culprit, env) {
|
|
1911
|
+
// Only control constructs need their nested goals validated at meta-call
|
|
1912
|
+
// entry. Walk them iteratively and dereference each nested goal lazily so
|
|
1913
|
+
// passing a callable that contains a very deep data term (for example
|
|
1914
|
+
// phrase(a, List) with an 8k-cell List) never consumes the JavaScript stack.
|
|
1915
|
+
const pending = [term];
|
|
1916
|
+
while (pending.length > 0) {
|
|
1917
|
+
const current = deref(pending.pop(), env);
|
|
1918
|
+
if (current.type !== COMPOUND || ![',', ';', '->'].includes(current.name) || current.arity !== 2) continue;
|
|
1919
|
+
for (let index = current.arity - 1; index >= 0; index--) {
|
|
1920
|
+
const argument = deref(current.args[index], env);
|
|
1921
|
+
if (argument.type === VAR) throw new PrologError('instantiation_error');
|
|
1922
|
+
if (argument.type !== ATOM && argument.type !== COMPOUND) {
|
|
1923
|
+
throw new PrologError('type_error(callable)', culprit);
|
|
1924
|
+
}
|
|
1925
|
+
pending.push(argument);
|
|
1914
1926
|
}
|
|
1915
|
-
validateControlCallable(argument, culprit);
|
|
1916
1927
|
}
|
|
1917
1928
|
}
|
|
1918
|
-
function resolveCallable(term, env) {
|
|
1919
|
-
const resolved = deref(term, env);
|
|
1920
|
-
if (resolved.type !== COMPOUND) return resolved;
|
|
1921
|
-
const callable = compound(resolved.name, resolved.args.map((arg) => resolveCallable(arg, env)));
|
|
1922
|
-
if (resolved.module != null) callable.module = resolved.module;
|
|
1923
|
-
return callable;
|
|
1924
|
-
}
|
|
1925
1929
|
function* callBuiltin({ solver, goal, env }) {
|
|
1926
1930
|
const child = solver.cloneForInnerGoal();
|
|
1927
1931
|
try {
|
|
@@ -1972,6 +1976,44 @@ function* countAllBuiltin({ solver, goal, env }) {
|
|
|
1972
1976
|
if (unify(goal.args[1], numberTerm(count), next)) yield next;
|
|
1973
1977
|
}
|
|
1974
1978
|
|
|
1979
|
+
function monotonicMilliseconds() {
|
|
1980
|
+
return globalThis.performance?.now?.() ?? Date.now();
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
function writeElapsedTime(solver, startedAt, inferences) {
|
|
1984
|
+
const stream = solver.io.resolve(solver.io.currentOutput);
|
|
1985
|
+
if (stream?.type !== 'text') throw new PrologError('permission_error(output, binary_stream)');
|
|
1986
|
+
const elapsedSeconds = Math.max(0, monotonicMilliseconds() - startedAt) / 1000;
|
|
1987
|
+
const mlips = elapsedSeconds > 0 ? inferences / elapsedSeconds / 1_000_000 : 0;
|
|
1988
|
+
solver.io.writeUnit(
|
|
1989
|
+
stream,
|
|
1990
|
+
`% Time elapsed ${elapsedSeconds.toFixed(3)}s, ${inferences} Inferences, ${mlips.toFixed(3)} MLips\n`,
|
|
1991
|
+
);
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
function* timeBuiltin({ solver, goal, env }) {
|
|
1995
|
+
const invoked = callable(goal.args[0], env);
|
|
1996
|
+
const child = solver.cloneForInnerGoal();
|
|
1997
|
+
let startedAt = monotonicMilliseconds();
|
|
1998
|
+
let startedInferences = child.inferenceObservation.value;
|
|
1999
|
+
let yieldedAny = false;
|
|
2000
|
+
try {
|
|
2001
|
+
for (const answerEnv of child.solve([invoked], env, 0)) {
|
|
2002
|
+
writeElapsedTime(solver, startedAt, child.inferenceObservation.value - startedInferences);
|
|
2003
|
+
yieldedAny = true;
|
|
2004
|
+
yield answerEnv;
|
|
2005
|
+
// A resumed time/1 measures only the work required to reach the next
|
|
2006
|
+
// answer, so nondeterministic calls get one timing line per solution.
|
|
2007
|
+
startedAt = monotonicMilliseconds();
|
|
2008
|
+
startedInferences = child.inferenceObservation.value;
|
|
2009
|
+
}
|
|
2010
|
+
// A call that fails without producing an answer still reports the work.
|
|
2011
|
+
if (!yieldedAny) writeElapsedTime(solver, startedAt, child.inferenceObservation.value - startedInferences);
|
|
2012
|
+
} finally {
|
|
2013
|
+
solver.absorbStatsFrom(child);
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
|
|
1975
2017
|
function* callNthBuiltin({ solver, goal, env }) {
|
|
1976
2018
|
const requestedTerm = deref(goal.args[1], env);
|
|
1977
2019
|
// Zero is the one Nth value that fails before Goal is inspected.
|
package/src/lib/iso_ext.pl
CHANGED
|
@@ -7,17 +7,21 @@
|
|
|
7
7
|
succ/2,
|
|
8
8
|
cfor/3,
|
|
9
9
|
findall/4,
|
|
10
|
-
variant/2
|
|
10
|
+
variant/2,
|
|
11
|
+
time/1,
|
|
12
|
+
'...'/2
|
|
11
13
|
]).
|
|
12
14
|
|
|
13
15
|
:- meta_predicate(call_nth(0, '?')).
|
|
14
16
|
:- meta_predicate(countall(0, '?')).
|
|
15
17
|
:- meta_predicate(forall(0, 0)).
|
|
16
18
|
:- meta_predicate(findall('?', 0, '?', '?')).
|
|
19
|
+
:- meta_predicate(time(0)).
|
|
17
20
|
|
|
18
21
|
% The organization and predicate contracts follow library(iso_ext) in
|
|
19
|
-
% Trealla.
|
|
20
|
-
%
|
|
22
|
+
% Trealla. Most definitions use only EyeProlog's ISO profile. time/1 is the
|
|
23
|
+
% deliberate exception: its private adapter supplies monotonic host timing and
|
|
24
|
+
% writes the measurement while the public wrapper keeps normal meta semantics.
|
|
21
25
|
|
|
22
26
|
call_nth(Goal, Nth) :- eyeprolog__call_nth(Goal, Nth).
|
|
23
27
|
|
|
@@ -59,6 +63,16 @@ variant(X, Y) :-
|
|
|
59
63
|
subsumes_term(CopyX, CopyY),
|
|
60
64
|
subsumes_term(CopyY, CopyX).
|
|
61
65
|
|
|
66
|
+
% Trealla-compatible timing wrapper. The private adapter measures the callable
|
|
67
|
+
% while this Prolog wrapper supplies normal module/meta-predicate semantics.
|
|
68
|
+
time(Goal) :- eyeprolog__time(Goal).
|
|
69
|
+
|
|
70
|
+
% Trealla/Scryer DCG helper: describes an arbitrary number of elements.
|
|
71
|
+
% EyeProlog's two-clause form has the same finite-input relation without the
|
|
72
|
+
% Trealla-specific empty-input cut guard, so recursive calls stay cut-free.
|
|
73
|
+
'...' --> [].
|
|
74
|
+
'...' --> [_], '...' .
|
|
75
|
+
|
|
62
76
|
iso_ext__call_all([]).
|
|
63
77
|
iso_ext__call_all([Goal|Goals]) :-
|
|
64
78
|
call(Goal),
|
package/src/repl.js
CHANGED
|
@@ -526,6 +526,7 @@ async function readSource(designation) {
|
|
|
526
526
|
async function solveQuery(engine, state, goal, reader, output) {
|
|
527
527
|
const variables = queryVariables(goal);
|
|
528
528
|
const solver = state.solver;
|
|
529
|
+
const demandDriven = containsTimedGoal(goal);
|
|
529
530
|
solver.solutionsSeen = 0;
|
|
530
531
|
const solutions = solver.solve([goal], new engine.Env(), 0);
|
|
531
532
|
let current = pullSolution(solver, solutions, reader);
|
|
@@ -544,11 +545,12 @@ async function solveQuery(engine, state, goal, reader, output) {
|
|
|
544
545
|
let firstAnswer = true;
|
|
545
546
|
let formattingAfterAdvance = false;
|
|
546
547
|
while (!current.result.done) {
|
|
547
|
-
|
|
548
|
-
//
|
|
549
|
-
//
|
|
550
|
-
//
|
|
551
|
-
//
|
|
548
|
+
// Ordinary queries keep the existing eager look-ahead so deterministic
|
|
549
|
+
// answers can end with a full stop without showing an unnecessary answer
|
|
550
|
+
// prompt. `time/1` is different: running a future solution changes what is
|
|
551
|
+
// being measured and can retain a very large current substitution. Timed
|
|
552
|
+
// queries therefore advance only after the user asks for another answer.
|
|
553
|
+
const next = demandDriven ? null : pullSolution(solver, solutions, reader);
|
|
552
554
|
if (formattingAfterAdvance) output.write(' ');
|
|
553
555
|
formattingAfterAdvance = false;
|
|
554
556
|
output.write(current.output);
|
|
@@ -556,10 +558,8 @@ async function solveQuery(engine, state, goal, reader, output) {
|
|
|
556
558
|
output.write(`${firstAnswer ? ' ' : ''}${answer}`);
|
|
557
559
|
answersShown++;
|
|
558
560
|
firstAnswer = false;
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
// scanner would absorb it into that token. Insert layout so the printed
|
|
562
|
-
// answer remains valid Prolog text (issue #44).
|
|
561
|
+
|
|
562
|
+
if (demandDriven ? !solver.hasPendingAlternatives() : (!next.error && next.result.done)) {
|
|
563
563
|
output.write(`${continuesGraphicToken(answer, answer.length) ? ' ' : ''}.\n`);
|
|
564
564
|
return null;
|
|
565
565
|
}
|
|
@@ -584,9 +584,6 @@ async function solveQuery(engine, state, goal, reader, output) {
|
|
|
584
584
|
break;
|
|
585
585
|
}
|
|
586
586
|
if (control === 'f') {
|
|
587
|
-
// `f` groups leaf answers in blocks of five, rather than merely
|
|
588
|
-
// adding five more answers after whatever the user has already
|
|
589
|
-
// inspected. Stop for control again at the next 5-answer boundary.
|
|
590
587
|
const remainder = answersShown % 5;
|
|
591
588
|
const answersToBoundary = remainder === 0 ? 5 : 5 - remainder;
|
|
592
589
|
automatic = answersToBoundary - 1;
|
|
@@ -606,6 +603,29 @@ async function solveQuery(engine, state, goal, reader, output) {
|
|
|
606
603
|
formattingAfterAdvance = true;
|
|
607
604
|
}
|
|
608
605
|
|
|
606
|
+
if (demandDriven) {
|
|
607
|
+
// The displayed timed answer is no longer needed. Drop it before
|
|
608
|
+
// resuming search so a large list substitution does not remain live only
|
|
609
|
+
// because the top level is looking for its successor.
|
|
610
|
+
current = null;
|
|
611
|
+
const requested = pullSolution(solver, solutions, reader);
|
|
612
|
+
if (requested.error) {
|
|
613
|
+
if (formattingAfterAdvance) output.write(' ');
|
|
614
|
+
formattingAfterAdvance = false;
|
|
615
|
+
output.write(requested.output);
|
|
616
|
+
if (requested.error?.name === 'HaltSignal') return { halted: true, code: requested.error.code };
|
|
617
|
+
throw requested.error;
|
|
618
|
+
}
|
|
619
|
+
if (requested.result.done) {
|
|
620
|
+
if (formattingAfterAdvance) output.write(' ');
|
|
621
|
+
formattingAfterAdvance = false;
|
|
622
|
+
output.write(`${requested.output}false.\n`);
|
|
623
|
+
return null;
|
|
624
|
+
}
|
|
625
|
+
current = requested;
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
|
|
609
629
|
if (next.error) {
|
|
610
630
|
if (formattingAfterAdvance) output.write(' ');
|
|
611
631
|
formattingAfterAdvance = false;
|
|
@@ -618,6 +638,17 @@ async function solveQuery(engine, state, goal, reader, output) {
|
|
|
618
638
|
return null;
|
|
619
639
|
}
|
|
620
640
|
|
|
641
|
+
function containsTimedGoal(goal) {
|
|
642
|
+
const stack = [goal];
|
|
643
|
+
while (stack.length !== 0) {
|
|
644
|
+
const term = stack.pop();
|
|
645
|
+
if (term?.type !== 'compound') continue;
|
|
646
|
+
if (term.name === 'time' && term.arity === 1) return true;
|
|
647
|
+
for (let index = term.args.length - 1; index >= 0; index--) stack.push(term.args[index]);
|
|
648
|
+
}
|
|
649
|
+
return false;
|
|
650
|
+
}
|
|
651
|
+
|
|
621
652
|
function pullSolution(solver, solutions, reader) {
|
|
622
653
|
const stream = solver.io.resolve('user_output');
|
|
623
654
|
const originalWrite = stream?.write;
|
package/src/solver.js
CHANGED
|
@@ -58,6 +58,10 @@ export class Solver {
|
|
|
58
58
|
this.depthLimitExceeded = false;
|
|
59
59
|
this.maxInferences = options.maxInferences ?? Infinity;
|
|
60
60
|
this.inferences = 0;
|
|
61
|
+
// Shared only for observability: nested meta-call solvers contribute to the
|
|
62
|
+
// same measurement counter without changing each solver's local inference
|
|
63
|
+
// limit accounting. time/1 snapshots this counter around the measured goal.
|
|
64
|
+
this.inferenceObservation = options.inferenceObservation ?? { value: 0 };
|
|
61
65
|
this.inferenceLimitExceeded = false;
|
|
62
66
|
this.maxMemoryBytes = options.maxMemoryBytes ?? softHeapLimit();
|
|
63
67
|
this.memoryRecovery = options.memoryRecovery ?? {
|
|
@@ -158,6 +162,7 @@ export class Solver {
|
|
|
158
162
|
charConversions: this.charConversions,
|
|
159
163
|
io: this.io,
|
|
160
164
|
innerTableScopes: this.innerTableScopes,
|
|
165
|
+
inferenceObservation: this.inferenceObservation,
|
|
161
166
|
skipListTailTabling: options.skipListTailTabling ?? this.skipListTailTabling,
|
|
162
167
|
});
|
|
163
168
|
if (options.tableScope != null) {
|
|
@@ -282,6 +287,7 @@ export class Solver {
|
|
|
282
287
|
this.solveStacks.push(stack);
|
|
283
288
|
while (stack.length) {
|
|
284
289
|
this.inferences++;
|
|
290
|
+
this.inferenceObservation.value++;
|
|
285
291
|
this.checkMemoryLimit();
|
|
286
292
|
if (this.inferences > this.maxInferences) {
|
|
287
293
|
this.inferenceLimitExceeded = true;
|
|
@@ -334,6 +340,7 @@ export class Solver {
|
|
|
334
340
|
|
|
335
341
|
while (true) {
|
|
336
342
|
this.inferences++;
|
|
343
|
+
this.inferenceObservation.value++;
|
|
337
344
|
this.checkMemoryLimit();
|
|
338
345
|
if (this.inferences > this.maxInferences) {
|
|
339
346
|
this.inferenceLimitExceeded = true;
|
|
@@ -388,6 +395,7 @@ export class Solver {
|
|
|
388
395
|
const selectedIndex = selectReadyDeterministicBuiltin(goals, env, this.registry);
|
|
389
396
|
const goal = deref(goals[selectedIndex], env);
|
|
390
397
|
const rest = selectedIndex === 0 ? goals.slice(1) : [...goals.slice(0, selectedIndex), ...goals.slice(selectedIndex + 1)];
|
|
398
|
+
prepareLocalVariablesForGoal(goal, env);
|
|
391
399
|
if (goal.type === 'atom' && goal.name === '!' && goal.arity === 0) {
|
|
392
400
|
const marker = active[active.length - 1] ?? null;
|
|
393
401
|
if (marker) marker.cutEpoch = (marker.cutEpoch ?? 0) + 1;
|
|
@@ -539,6 +547,23 @@ export class Solver {
|
|
|
539
547
|
continue;
|
|
540
548
|
}
|
|
541
549
|
|
|
550
|
+
const ellipsisPlan = bundledEllipsisPlan(this, group, goal, rest, env);
|
|
551
|
+
if (ellipsisPlan != null) {
|
|
552
|
+
const firstResult = ellipsisPlan.iterator.next();
|
|
553
|
+
if (firstResult.done) break;
|
|
554
|
+
stack.push({
|
|
555
|
+
kind: 'resumeBuiltin',
|
|
556
|
+
iterator: ellipsisPlan.iterator,
|
|
557
|
+
goals: ellipsisPlan.rest,
|
|
558
|
+
depth: depth + 1,
|
|
559
|
+
active,
|
|
560
|
+
});
|
|
561
|
+
goals = ellipsisPlan.rest;
|
|
562
|
+
env = firstResult.value;
|
|
563
|
+
depth++;
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
|
|
542
567
|
if (group.tabled && !(this.skipListTailTabling && group.listTailRecursive)) {
|
|
543
568
|
const key = memoKey(goal, env, group);
|
|
544
569
|
if (key.hasBound) {
|
|
@@ -612,6 +637,13 @@ export class Solver {
|
|
|
612
637
|
}
|
|
613
638
|
}
|
|
614
639
|
|
|
640
|
+
hasPendingAlternatives() {
|
|
641
|
+
// When solve() is suspended at an answer, active solve stacks contain only
|
|
642
|
+
// unexplored work. The timed REPL path uses this without speculatively
|
|
643
|
+
// pulling the next answer.
|
|
644
|
+
return this.solveStacks.some((stack) => stack.length !== 0);
|
|
645
|
+
}
|
|
646
|
+
|
|
615
647
|
fastCountGoal(goal, env) {
|
|
616
648
|
return fastCountPureGoal(this, goal, env);
|
|
617
649
|
}
|
|
@@ -1083,12 +1115,47 @@ function freshVariableSet(names, freshVariables) {
|
|
|
1083
1115
|
|
|
1084
1116
|
function attachBodyLocalFreshVariables(freshBody, plan, freshVariables) {
|
|
1085
1117
|
for (let index = 0; index < freshBody.length; index++) {
|
|
1118
|
+
const goal = freshBody[index];
|
|
1086
1119
|
const knownNonoccurringVariables = freshVariableSet(plan[index] ?? [], freshVariables);
|
|
1087
|
-
if (knownNonoccurringVariables
|
|
1088
|
-
|
|
1089
|
-
|
|
1120
|
+
if (knownNonoccurringVariables == null || goal?.type !== COMPOUND) continue;
|
|
1121
|
+
if (goal.name === '=' && goal.arity === 2) {
|
|
1122
|
+
goal._knownNonoccurringVariables = knownNonoccurringVariables;
|
|
1123
|
+
continue;
|
|
1124
|
+
}
|
|
1125
|
+
// Only a compiler-generated DCG state first handed as a complete argument
|
|
1126
|
+
// to another callable receives a longer-lived local marker. Equality-created
|
|
1127
|
+
// sequence variables keep the existing one-unification proof instead.
|
|
1128
|
+
const localFirstUseVariables = new Set();
|
|
1129
|
+
for (const argument of goal.args) {
|
|
1130
|
+
if (argument?.type === VAR && argument.name.startsWith('\u0000dcg') &&
|
|
1131
|
+
knownNonoccurringVariables.has(argument.name)) {
|
|
1132
|
+
localFirstUseVariables.add(argument.name);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
if (localFirstUseVariables.size !== 0) goal._localFirstUseVariables = localFirstUseVariables;
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
|
|
1140
|
+
function prepareLocalVariablesForGoal(goal, env) {
|
|
1141
|
+
// A DCG local is globalized if later source places it inside a structure.
|
|
1142
|
+
// Inspect the small fresh goal syntax rather than dereferencing large data.
|
|
1143
|
+
if (env.hasLocalVariables() && goal?.type === COMPOUND) {
|
|
1144
|
+
const pending = [];
|
|
1145
|
+
for (const argument of goal.args) if (argument?.type === COMPOUND) pending.push(argument);
|
|
1146
|
+
while (pending.length !== 0) {
|
|
1147
|
+
const current = pending.pop();
|
|
1148
|
+
for (const argument of current.args ?? []) {
|
|
1149
|
+
if (argument?.type === VAR) {
|
|
1150
|
+
const root = derefForLocal(argument, env);
|
|
1151
|
+
if (root.type === VAR && env.isLocalVariable(root.name)) env.demoteLocalVariable(root.name);
|
|
1152
|
+
} else if (argument?.type === COMPOUND) {
|
|
1153
|
+
pending.push(argument);
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1090
1156
|
}
|
|
1091
1157
|
}
|
|
1158
|
+
env.markLocalVariables(goal?._localFirstUseVariables ?? null);
|
|
1092
1159
|
}
|
|
1093
1160
|
|
|
1094
1161
|
function groupNeedsActiveFrame(group) {
|
|
@@ -1176,6 +1243,92 @@ function* bundledMemberSolutions(solver, goal, env) {
|
|
|
1176
1243
|
}
|
|
1177
1244
|
}
|
|
1178
1245
|
|
|
1246
|
+
function bundledEllipsisPlan(solver, group, goal, rest, env) {
|
|
1247
|
+
if (solver.registry.eyePrologLibrary !== true ||
|
|
1248
|
+
group.module !== 'iso_ext' || group.name !== '...' || group.arity !== 2 ||
|
|
1249
|
+
group.bundledLibrary !== true || group.clauses.length !== 2) {
|
|
1250
|
+
return null;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
// length/2 and other native constructors can leave a known finite list as a
|
|
1254
|
+
// compact spine. The ordinary ...//0 relation simply enumerates every
|
|
1255
|
+
// suffix of such a list; doing that directly avoids clause freshening and
|
|
1256
|
+
// recursive solver depth for every consumed element. Non-compact and open
|
|
1257
|
+
// list cases retain the ordinary Prolog definition.
|
|
1258
|
+
const input = deref(goal.args[0], env);
|
|
1259
|
+
if (compactListLength(input) == null) return null;
|
|
1260
|
+
|
|
1261
|
+
// A following binary identity relation is just a zero-width DCG hand-off.
|
|
1262
|
+
// Fuse it by asking .../2 for that relation's required output directly.
|
|
1263
|
+
// This is a structural optimization: any one-clause p(X,Y):-X=Y (or p(X,X).)
|
|
1264
|
+
// qualifies, not just a predicate named epsilon/2.
|
|
1265
|
+
if (rest.length > 0) {
|
|
1266
|
+
const continuation = deref(rest[0], env);
|
|
1267
|
+
if (continuation?.type === COMPOUND && continuation.arity === 2) {
|
|
1268
|
+
const continuationGroup = solver.program.findGroup(
|
|
1269
|
+
continuation.name, continuation.arity, continuation.module ?? goal.module ?? 'user',
|
|
1270
|
+
);
|
|
1271
|
+
if (isBinaryIdentityGroup(continuationGroup)) {
|
|
1272
|
+
const output = deref(goal.args[1], env);
|
|
1273
|
+
const continuationInput = deref(continuation.args[0], env);
|
|
1274
|
+
if (output.type === VAR && continuationInput.type === VAR && output.name === continuationInput.name &&
|
|
1275
|
+
output.name.startsWith('\u0000dcg')) {
|
|
1276
|
+
return {
|
|
1277
|
+
iterator: bundledEllipsisSolutions(solver, continuation.args[1], input, env),
|
|
1278
|
+
rest: rest.slice(1),
|
|
1279
|
+
};
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
return { iterator: bundledEllipsisSolutions(solver, goal.args[1], input, env), rest };
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
function isBinaryIdentityGroup(group) {
|
|
1289
|
+
if (group == null || group.arity !== 2 || group.clauses.length !== 1 || group.hasCut === true) return false;
|
|
1290
|
+
const clause = group.clauses[0];
|
|
1291
|
+
const left = clause.head?.args?.[0];
|
|
1292
|
+
const right = clause.head?.args?.[1];
|
|
1293
|
+
if (left?.type !== VAR || right?.type !== VAR) return false;
|
|
1294
|
+
if (clause.body.length === 0) return left.name === right.name;
|
|
1295
|
+
if (clause.body.length !== 1) return false;
|
|
1296
|
+
const equality = clause.body[0];
|
|
1297
|
+
if (equality?.type !== COMPOUND || equality.name !== '=' || equality.arity !== 2) return false;
|
|
1298
|
+
const a = equality.args[0];
|
|
1299
|
+
const b = equality.args[1];
|
|
1300
|
+
if (a?.type !== VAR || b?.type !== VAR) return false;
|
|
1301
|
+
return (a.name === left.name && b.name === right.name) ||
|
|
1302
|
+
(a.name === right.name && b.name === left.name);
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
function* bundledEllipsisSolutions(solver, output, input, env) {
|
|
1306
|
+
let cursor = input;
|
|
1307
|
+
const requestedOutput = deref(output, env);
|
|
1308
|
+
|
|
1309
|
+
// A fixed empty remainder is the important DCG scanner case. Still walk the
|
|
1310
|
+
// actual compact spine so this remains a real tail-consumption benchmark,
|
|
1311
|
+
// but avoid allocating a speculative environment for every suffix that is
|
|
1312
|
+
// structurally incapable of matching [].
|
|
1313
|
+
if (isEmptyList(requestedOutput)) {
|
|
1314
|
+
while (!isEmptyList(cursor)) {
|
|
1315
|
+
if (!isCons(cursor)) return;
|
|
1316
|
+
cursor = deref(cursor.args[1], env);
|
|
1317
|
+
}
|
|
1318
|
+
yield env;
|
|
1319
|
+
return;
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
while (true) {
|
|
1323
|
+
const next = env.clone();
|
|
1324
|
+
solver.stats.unify_calls++;
|
|
1325
|
+
if (unify(output, cursor, next)) yield next;
|
|
1326
|
+
if (isEmptyList(cursor)) return;
|
|
1327
|
+
if (!isCons(cursor)) return;
|
|
1328
|
+
cursor = deref(cursor.args[1], env);
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1179
1332
|
function bundledLengthIterator(solver, group, goal, env) {
|
|
1180
1333
|
if (solver.registry.eyePrologLibrary !== true ||
|
|
1181
1334
|
!['lists', 'prologue'].includes(group.module) || group.name !== 'length' || group.arity !== 2 ||
|
package/src/standard-library.js
CHANGED
|
@@ -47,7 +47,7 @@ function libraryUrl(filename) {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
export const eyePrologNativeLibraryIndicators = Object.freeze([
|
|
50
|
-
'call_nth/2', 'freeze/2', 'countall/2',
|
|
50
|
+
'call_nth/2', 'freeze/2', 'countall/2', 'time/1',
|
|
51
51
|
'#>/2', '#</2', '#>=/2', '#=</2', '#=/2', '#\\=/2', '#\\/1',
|
|
52
52
|
'#<==>/2', '#==>/2', '#<==/2', '#\\//2', '#\\/2', '#/\\/2',
|
|
53
53
|
'in/2', 'ins/2', 'all_different/1', 'all_distinct/1', 'nvalue/2', 'sum/3',
|
|
@@ -68,7 +68,7 @@ export const eyePrologPortableLibraryIndicators = Object.freeze([
|
|
|
68
68
|
'nth0/3', 'nth0/4', 'nth1/3', 'nth1/4', 'set_nth0/4', 'take/3', 'drop/3', 'slice/4', 'reverse/2',
|
|
69
69
|
'length/2', 'sum_list/2', 'min_list/2', 'max_list/2', 'list_to_set/2',
|
|
70
70
|
'succ/2', 'foldl/4', 'foldl/5', 'foldl/6',
|
|
71
|
-
'forall/2', 'cfor/3', 'findall/4', 'variant/2', 'uuid/3',
|
|
71
|
+
'forall/2', 'cfor/3', 'findall/4', 'variant/2', '.../2', 'uuid/3',
|
|
72
72
|
'^/3', '^/4', '^/5', '^/6', '^/7', '^/8', '^/9', '^/10',
|
|
73
73
|
'\\/1', '\\/2', '\\/3', '\\/4', '\\/5', '\\/6', '\\/7', '\\/8',
|
|
74
74
|
'+\\/2', '+\\/3', '+\\/4', '+\\/5', '+\\/6', '+\\/7', '+\\/8', '+\\/9',
|
|
@@ -117,6 +117,11 @@ export const eyePrologInteropAutoload = Object.freeze({
|
|
|
117
117
|
// it from library(iso_ext), matching the explicit Scryer import while still
|
|
118
118
|
// allowing Trealla-style unqualified source to use the same autoload entry.
|
|
119
119
|
'call_nth/2': 'iso_ext',
|
|
120
|
+
// Trealla exposes time/1 as a meta timing predicate and library(iso_ext)
|
|
121
|
+
// supplies ...//0. Autoload both so UWN's DCG hand-off benchmark runs
|
|
122
|
+
// unchanged while their implementations remain outside the ISO core.
|
|
123
|
+
'time/1': 'iso_ext',
|
|
124
|
+
'.../2': 'iso_ext',
|
|
120
125
|
// Trealla and Scryer expose between/3 without an EyeProlog-style
|
|
121
126
|
// library(prologue) dependency. EyeProlog keeps its implementation in the
|
|
122
127
|
// Prologue module but autoloads it so portable source need not name that
|
package/src/term.js
CHANGED
|
@@ -99,6 +99,7 @@ export class Env {
|
|
|
99
99
|
this._delays = null;
|
|
100
100
|
this._clpz = null;
|
|
101
101
|
this._occursCheckHandler = null;
|
|
102
|
+
this._localVariables = null;
|
|
102
103
|
}
|
|
103
104
|
clone() {
|
|
104
105
|
// Most speculative environments are either rejected without a binding or
|
|
@@ -111,12 +112,43 @@ export class Env {
|
|
|
111
112
|
clone._delays = this._delays;
|
|
112
113
|
clone._clpz = this._clpz;
|
|
113
114
|
clone._occursCheckHandler = this._occursCheckHandler;
|
|
115
|
+
clone._localVariables = this._localVariables;
|
|
114
116
|
return clone;
|
|
115
117
|
}
|
|
116
118
|
setOccursCheckHandler(handler) {
|
|
117
119
|
this._occursCheckHandler = typeof handler === 'function' ? handler : null;
|
|
118
120
|
return this;
|
|
119
121
|
}
|
|
122
|
+
hasLocalVariables() {
|
|
123
|
+
return this._localVariables != null && this._localVariables.size !== 0;
|
|
124
|
+
}
|
|
125
|
+
isLocalVariable(name) {
|
|
126
|
+
return this._localVariables?.has(name) === true;
|
|
127
|
+
}
|
|
128
|
+
markLocalVariables(names) {
|
|
129
|
+
if (names == null || names.size === 0) return;
|
|
130
|
+
let next = this._localVariables;
|
|
131
|
+
for (const name of names) {
|
|
132
|
+
const root = deref(variable(name), this);
|
|
133
|
+
if (root.type !== VAR || next?.has(root.name)) continue;
|
|
134
|
+
if (next === this._localVariables) next = new Set(this._localVariables ?? []);
|
|
135
|
+
next.add(root.name);
|
|
136
|
+
}
|
|
137
|
+
this._localVariables = next;
|
|
138
|
+
}
|
|
139
|
+
demoteLocalVariable(name) {
|
|
140
|
+
const root = deref(variable(name), this);
|
|
141
|
+
if (root.type !== VAR || this._localVariables?.has(root.name) !== true) return;
|
|
142
|
+
const next = new Set(this._localVariables);
|
|
143
|
+
next.delete(root.name);
|
|
144
|
+
this._localVariables = next.size === 0 ? null : next;
|
|
145
|
+
}
|
|
146
|
+
forgetLocalVariable(name) {
|
|
147
|
+
if (this._localVariables?.has(name) !== true) return;
|
|
148
|
+
const next = new Set(this._localVariables);
|
|
149
|
+
next.delete(name);
|
|
150
|
+
this._localVariables = next.size === 0 ? null : next;
|
|
151
|
+
}
|
|
120
152
|
has(name) {
|
|
121
153
|
return this.get(name) !== undefined;
|
|
122
154
|
}
|
|
@@ -212,6 +244,9 @@ export function deref(term, env) {
|
|
|
212
244
|
let current = term;
|
|
213
245
|
let seen = null;
|
|
214
246
|
while (current?.type === VAR) {
|
|
247
|
+
// A live compiler-proven DCG local is the current unbound representative.
|
|
248
|
+
// No older Env layer can contain a binding for it.
|
|
249
|
+
if (env?.isLocalVariable?.(current.name) === true) break;
|
|
215
250
|
const next = env?.get(current.name);
|
|
216
251
|
if (next === undefined) break;
|
|
217
252
|
if (seen?.has(current.name)) break;
|
|
@@ -284,28 +319,40 @@ export function unify(left, right, env, options = {}) {
|
|
|
284
319
|
|
|
285
320
|
if (a.type === VAR && b.type === VAR && a.name === b.name) continue;
|
|
286
321
|
if (a.type === VAR && b.type === VAR) {
|
|
287
|
-
//
|
|
288
|
-
//
|
|
289
|
-
|
|
290
|
-
env
|
|
322
|
+
// For a compiler-generated DCG state handed directly to another
|
|
323
|
+
// nonterminal, keep the local caller variable as representative. Ordinary
|
|
324
|
+
// aliases retain the established direction and observable conventions.
|
|
325
|
+
const aLocalDcg = env?.isLocalVariable(a.name) === true &&
|
|
326
|
+
a.name.startsWith('\u0000dcg') && b.name.startsWith('\u0000dcg');
|
|
327
|
+
if (aLocalDcg) {
|
|
328
|
+
markCompactVariableBound(b);
|
|
329
|
+
env.bind(b.name, a);
|
|
330
|
+
} else {
|
|
331
|
+
markCompactVariableBound(a);
|
|
332
|
+
env.bind(a.name, b);
|
|
333
|
+
}
|
|
291
334
|
continue;
|
|
292
335
|
}
|
|
293
336
|
if (a.type === VAR) {
|
|
294
|
-
|
|
337
|
+
const aLocal = env?.isLocalVariable(a.name) === true;
|
|
338
|
+
if (!aLocal && !knownNonoccurringVariables?.has(a.name) && occurs(a.name, b, env)) {
|
|
295
339
|
occursCheckHandler?.(a, b, env);
|
|
296
340
|
return false;
|
|
297
341
|
}
|
|
298
342
|
markCompactVariableBound(a);
|
|
299
343
|
env.bind(a.name, b);
|
|
344
|
+
if (aLocal) env.forgetLocalVariable(a.name);
|
|
300
345
|
continue;
|
|
301
346
|
}
|
|
302
347
|
if (b.type === VAR) {
|
|
303
|
-
|
|
348
|
+
const bLocal = env?.isLocalVariable(b.name) === true;
|
|
349
|
+
if (!bLocal && !knownNonoccurringVariables?.has(b.name) && occurs(b.name, a, env)) {
|
|
304
350
|
occursCheckHandler?.(b, a, env);
|
|
305
351
|
return false;
|
|
306
352
|
}
|
|
307
353
|
markCompactVariableBound(b);
|
|
308
354
|
env.bind(b.name, a);
|
|
355
|
+
if (bLocal) env.forgetLocalVariable(b.name);
|
|
309
356
|
continue;
|
|
310
357
|
}
|
|
311
358
|
|
package/test/run-regression.mjs
CHANGED
|
@@ -1324,6 +1324,9 @@ c4 ?- call((!;1)).
|
|
|
1324
1324
|
first_use_cycle :- X = f(Y), Y = g(X).
|
|
1325
1325
|
repeated_cycle :- X = f(X).
|
|
1326
1326
|
first_use_ok(T) :- X = f(Y), Y = a, T = X.
|
|
1327
|
+
pass(_).
|
|
1328
|
+
handed_off_cycle :- pass(X), Y = f(X), X = g(Y).
|
|
1329
|
+
handed_off_alias_cycle :- pass(X), Y = X, X = f(Y).
|
|
1327
1330
|
`);
|
|
1328
1331
|
const solver = new Solver(program);
|
|
1329
1332
|
const solveCount = (text) => {
|
|
@@ -1338,6 +1341,8 @@ c4 ?- call((!;1)).
|
|
|
1338
1341
|
assertEqual(solveCount('first_use_cycle'), 0, 'cycle across later first-use binding');
|
|
1339
1342
|
assertEqual(solveCount('repeated_cycle'), 0, 'same-goal repeated variable still checks occurs');
|
|
1340
1343
|
assertEqual(solveCount('first_use_ok(f(a))'), 1, 'acyclic first-use bindings still succeed');
|
|
1344
|
+
assertEqual(solveCount('handed_off_cycle'), 0, 'nested use globalizes a handed-off local before a cycle');
|
|
1345
|
+
assertEqual(solveCount('handed_off_alias_cycle'), 0, 'aliasing does not hide a later cycle');
|
|
1341
1346
|
},
|
|
1342
1347
|
},
|
|
1343
1348
|
{
|
|
@@ -1379,6 +1384,106 @@ c4 ?- call((!;1)).
|
|
|
1379
1384
|
assertEqual(result.stdout, 'ok', 'deep DCG result');
|
|
1380
1385
|
},
|
|
1381
1386
|
},
|
|
1387
|
+
{
|
|
1388
|
+
name: 'Trealla-style DCG hand-off autoloads time/1 and ...//0 without quadratic occurs checks (issue #49)',
|
|
1389
|
+
run: () => {
|
|
1390
|
+
const engineUrl = new URL('../src/index.js', import.meta.url).href;
|
|
1391
|
+
const script = `
|
|
1392
|
+
import { run } from ${JSON.stringify(engineUrl)};
|
|
1393
|
+
const source = ${JSON.stringify('a --> ..., epsilon.\nepsilon --> [].\n')};
|
|
1394
|
+
const result = run(source, {
|
|
1395
|
+
goal: ${JSON.stringify('length(_,E), E>12, N is 2^E, \\+ \\+ (length(L,N), time(phrase(a,L)))')},
|
|
1396
|
+
solutionLimit: 1,
|
|
1397
|
+
});
|
|
1398
|
+
if (!result.stdout.startsWith('% Time elapsed ') || !result.stdout.endsWith('s\\n')) {
|
|
1399
|
+
throw new Error('unexpected time/1 output: ' + JSON.stringify(result.stdout));
|
|
1400
|
+
}
|
|
1401
|
+
process.stdout.write('ok');
|
|
1402
|
+
`;
|
|
1403
|
+
const result = spawnSync(process.execPath, ['--input-type=module', '--eval', script], {
|
|
1404
|
+
cwd: packageRoot,
|
|
1405
|
+
encoding: 'utf8',
|
|
1406
|
+
timeout: 10000,
|
|
1407
|
+
});
|
|
1408
|
+
if (result.error) throw result.error;
|
|
1409
|
+
assertEqual(result.status, 0, `DCG hand-off child status; stderr=${result.stderr}`);
|
|
1410
|
+
assertEqual(result.stdout, 'ok', 'DCG hand-off benchmark result');
|
|
1411
|
+
},
|
|
1412
|
+
},
|
|
1413
|
+
{
|
|
1414
|
+
name: 'Trealla-style DCG hand-off reaches 65536 cells without the solver depth ceiling',
|
|
1415
|
+
run: () => {
|
|
1416
|
+
const engineUrl = new URL('../src/index.js', import.meta.url).href;
|
|
1417
|
+
const script = `
|
|
1418
|
+
import { run } from ${JSON.stringify(engineUrl)};
|
|
1419
|
+
const source = ${JSON.stringify(':- set_prolog_flag(occurs_check, true).\na --> ..., epsilon.\nepsilon --> [].\n')};
|
|
1420
|
+
const result = run(source, {
|
|
1421
|
+
goal: ${JSON.stringify('\\+ \\+ (length(L,65536), time(phrase(a,L)))')},
|
|
1422
|
+
solutionLimit: 1,
|
|
1423
|
+
});
|
|
1424
|
+
if (!result.stdout.startsWith('% Time elapsed ')) {
|
|
1425
|
+
throw new Error('65536-cell hand-off did not succeed: ' + JSON.stringify(result.stdout));
|
|
1426
|
+
}
|
|
1427
|
+
process.stdout.write('ok');
|
|
1428
|
+
`;
|
|
1429
|
+
const result = spawnSync(process.execPath, ['--input-type=module', '--eval', script], {
|
|
1430
|
+
cwd: packageRoot,
|
|
1431
|
+
encoding: 'utf8',
|
|
1432
|
+
timeout: 5000,
|
|
1433
|
+
});
|
|
1434
|
+
if (result.error) throw result.error;
|
|
1435
|
+
assertEqual(result.status, 0, `65536-cell hand-off status; stderr=${result.stderr}`);
|
|
1436
|
+
assertEqual(result.stdout, 'ok', '65536-cell hand-off result');
|
|
1437
|
+
},
|
|
1438
|
+
},
|
|
1439
|
+
{
|
|
1440
|
+
name: 'REPL applies conservative autoloading to interactive time/1 and consulted ...//0',
|
|
1441
|
+
run: () => {
|
|
1442
|
+
const filename = path.join(tmp, `issue49-handoff-${tmpCounter++}.pl`);
|
|
1443
|
+
fs.writeFileSync(filename, 'a --> ..., epsilon.\nepsilon --> [].\n');
|
|
1444
|
+
const result = runCli([], {
|
|
1445
|
+
input:
|
|
1446
|
+
`[${sourceAtom(filename)}].\n` +
|
|
1447
|
+
'use_module(library(lists)).\n' +
|
|
1448
|
+
'length(_,E),E>12,N is 2^E,\\+ \\+ (length(L,N),time(phrase(a,L))).\n' +
|
|
1449
|
+
'\n' +
|
|
1450
|
+
'halt.\n',
|
|
1451
|
+
timeout: 10000,
|
|
1452
|
+
});
|
|
1453
|
+
if (result.error) throw result.error;
|
|
1454
|
+
assertEqual(result.status, 0, `REPL hand-off status; stderr=${result.stderr}`);
|
|
1455
|
+
assertIncludes(result.stdout, '% Time elapsed ', 'REPL time/1 output');
|
|
1456
|
+
assertEqual((result.stdout.match(/% Time elapsed /g) ?? []).length, 1,
|
|
1457
|
+
'REPL timed query does not prefetch an unrequested next answer');
|
|
1458
|
+
assertIncludes(result.stdout, 'E = 13, N = 8192', 'REPL first benchmark answer');
|
|
1459
|
+
assertNotIncludes(result.stdout, 'existence_error(procedure', 'REPL autoload errors');
|
|
1460
|
+
assertEqual(result.stderr, '', 'REPL hand-off stderr');
|
|
1461
|
+
},
|
|
1462
|
+
},
|
|
1463
|
+
{
|
|
1464
|
+
name: 'REPL Trealla hand-off benchmark reaches E=16 on demand without OOM fallthrough',
|
|
1465
|
+
run: () => {
|
|
1466
|
+
const filename = path.join(tmp, `issue49-handoff-deep-${tmpCounter++}.pl`);
|
|
1467
|
+
fs.writeFileSync(filename, ':- set_prolog_flag(occurs_check, true).\na --> ..., epsilon.\nepsilon --> [].\n');
|
|
1468
|
+
const result = runCli([], {
|
|
1469
|
+
input:
|
|
1470
|
+
`[${sourceAtom(filename)}].\n` +
|
|
1471
|
+
'use_module(library(lists)).\n' +
|
|
1472
|
+
'length(_,E),E>12,N is 2^E,\\+ \\+ (length(L,N),time(phrase(a,L))).\n' +
|
|
1473
|
+
';\n;\n;\n\n' +
|
|
1474
|
+
'halt.\n',
|
|
1475
|
+
timeout: 5000,
|
|
1476
|
+
});
|
|
1477
|
+
if (result.error) throw result.error;
|
|
1478
|
+
assertEqual(result.status, 0, `deep REPL hand-off status; stderr=${result.stderr}`);
|
|
1479
|
+
for (const [e, n] of [[13, 8192], [14, 16384], [15, 32768], [16, 65536]]) {
|
|
1480
|
+
assertIncludes(result.stdout, `E = ${e}, N = ${n}`, `REPL hand-off E=${e}`);
|
|
1481
|
+
}
|
|
1482
|
+
assertEqual((result.stdout.match(/% Time elapsed /g) ?? []).length, 4,
|
|
1483
|
+
'exactly four requested timed answers');
|
|
1484
|
+
assertEqual(result.stderr, '', 'deep REPL hand-off stderr');
|
|
1485
|
+
},
|
|
1486
|
+
},
|
|
1382
1487
|
{
|
|
1383
1488
|
name: 'REPL enumerates and stops answers like the Scryer top level',
|
|
1384
1489
|
run: () => {
|
|
@@ -3916,7 +4021,7 @@ answer(ok) :-
|
|
|
3916
4021
|
assertEqual(Boolean(registry.get('is', 2)), true, 'ISO is/2 exists');
|
|
3917
4022
|
assertEqual(Boolean(registry.get('append', 3)), false, 'append/3 is not ISO core');
|
|
3918
4023
|
assertEqual(library.eyePrologLibrary, true, 'complete registry marker');
|
|
3919
|
-
assertEqual(library.defs.size,
|
|
4024
|
+
assertEqual(library.defs.size, 158, 'EyeProlog registry contains ISO definitions, observability extensions, WFS tnot/1, and private library adapters');
|
|
3920
4025
|
assertEqual(Boolean(registry.get('phrase', 2)), true, 'Part 3 phrase/2 exists');
|
|
3921
4026
|
assertEqual(Boolean(registry.get('phrase', 3)), true, 'Part 3 phrase/3 exists');
|
|
3922
4027
|
assertEqual(registry.get('statistics', 0), null, 'statistics/0 is absent from the ISO registry');
|
|
@@ -3925,17 +4030,21 @@ answer(ok) :-
|
|
|
3925
4030
|
assertEqual(Boolean(library.get('statistics', 2)), true, 'statistics/2 is an EyeProlog observability extension');
|
|
3926
4031
|
assertEqual(registry.get('tnot', 1), null, 'tnot/1 is absent from the ISO registry');
|
|
3927
4032
|
assertEqual(Boolean(library.get('tnot', 1)), true, 'tnot/1 is an EyeProlog WFS extension');
|
|
3928
|
-
assertEqual(
|
|
3929
|
-
assertEqual(
|
|
3930
|
-
assertEqual(
|
|
4033
|
+
assertEqual(registry.get('time', 1), null, 'time/1 is absent from the ISO registry');
|
|
4034
|
+
assertEqual(Boolean(library.get('time', 1)), true, 'time/1 is an EyeProlog timing extension');
|
|
4035
|
+
assertEqual(registeredNativeEyePrologLibraryNames().length, 41, 'public native EyeProlog builtin count');
|
|
4036
|
+
assertEqual(eyePrologPortableLibraryIndicators.length, 87, 'portable Prolog library count');
|
|
4037
|
+
assertEqual(eyePrologInteropLibraryIndicators.length, 29, 'cross-implementation interop profile count');
|
|
3931
4038
|
assertEqual(eyePrologInteropLibraryModules.join(','), 'lists,iso_ext,lambda', 'common explicit library module profile');
|
|
3932
4039
|
assertEqual(eyePrologInteropAutoload['member/2'], 'lists', 'member/2 canonical autoload');
|
|
3933
4040
|
assertEqual(eyePrologInteropAutoload['between/3'], 'prologue', 'between/3 canonical internal autoload');
|
|
3934
4041
|
assertEqual(eyePrologInteropAutoload['call_nth/2'], 'iso_ext', 'call_nth/2 canonical interop autoload');
|
|
4042
|
+
assertEqual(eyePrologInteropAutoload['time/1'], 'iso_ext', 'time/1 canonical interop autoload');
|
|
4043
|
+
assertEqual(eyePrologInteropAutoload['.../2'], 'iso_ext', '.../2 canonical interop autoload');
|
|
3935
4044
|
assertEqual(eyePrologInteropAutoload['set_nth0/4'] ?? null, null, 'EyeProlog-only set_nth0/4 is not autoloadable');
|
|
3936
|
-
assertEqual(eyePrologNativeLibraryIndicators.length,
|
|
4045
|
+
assertEqual(eyePrologNativeLibraryIndicators.length, 41, 'native host library count');
|
|
3937
4046
|
assertEqual(eyePrologNativeLibraryIndicators.slice(0, 2).join(','), 'call_nth/2,freeze/2', 'control predicates requiring host support');
|
|
3938
|
-
assertEqual(eyePrologLibraryIndicators.length,
|
|
4047
|
+
assertEqual(eyePrologLibraryIndicators.length, 128, 'complete EyeProlog library surface');
|
|
3939
4048
|
assertEqual(registry.get('eyeprolog__call_nth', 2), null, 'private call_nth adapter is absent from ISO registry');
|
|
3940
4049
|
assertEqual(Boolean(library.get('eyeprolog__call_nth', 2)), true, 'private call_nth adapter is registered for EyeProlog');
|
|
3941
4050
|
assertEqual(library.get('eyeprolog__call_nth', 2)?.eyePrologLibrary, true, 'private adapter is marked as library support');
|
|
@@ -3943,6 +4052,7 @@ answer(ok) :-
|
|
|
3943
4052
|
assertEqual(Boolean(library.get('eyeprolog__freeze', 2)), true, 'private freeze adapter is registered for EyeProlog');
|
|
3944
4053
|
assertEqual(registry.get('eyeprolog__countall', 2), null, 'private countall adapter is absent from ISO registry');
|
|
3945
4054
|
assertEqual(Boolean(library.get('eyeprolog__countall', 2)), true, 'private countall adapter is registered for EyeProlog');
|
|
4055
|
+
assertEqual(Boolean(library.get('eyeprolog__time', 1)), true, 'private time adapter is registered for EyeProlog');
|
|
3946
4056
|
assertEqual(Boolean(library.get('eyeprolog__clpz_labeling', 2)), true, 'private CLP(Z) labeling adapter is registered');
|
|
3947
4057
|
assertEqual(Boolean(library.get('eyeprolog__clpz_global_cardinality', 3)), true, 'private CLP(Z) cardinality adapter is registered');
|
|
3948
4058
|
assertEqual(library.get('between', 3), null, 'between/3 remains portable Prolog');
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -6067,14 +6067,16 @@ so side effects occur in Prolog execution order.
|
|
|
6067
6067
|
|
|
6068
6068
|
### The EyeProlog library
|
|
6069
6069
|
|
|
6070
|
-
EyeProlog exposes **
|
|
6071
|
-
indicators in its isolated ISO profile. **
|
|
6072
|
-
clauses** in focused modules under `src/lib/`. The remaining
|
|
6073
|
-
|
|
6074
|
-
|
|
6075
|
-
resulting normal EyeProlog language surface is therefore
|
|
6076
|
-
|
|
6077
|
-
|
|
6070
|
+
EyeProlog exposes **128 library predicate indicators** in addition to the 129
|
|
6071
|
+
indicators in its isolated ISO profile. **88 are defined entirely as ordinary
|
|
6072
|
+
Prolog clauses** in focused modules under `src/lib/`. The remaining public
|
|
6073
|
+
relations use small private host adapters where control, constraints, or host
|
|
6074
|
+
observability cannot be expressed by ordinary clauses alone; `time/1` is one
|
|
6075
|
+
such relation. The resulting normal EyeProlog language surface is therefore
|
|
6076
|
+
**257 public predicate indicators**. Most library relations remain module source
|
|
6077
|
+
clauses over private adapters. `time/1` is additionally registered directly in
|
|
6078
|
+
the normal EyeProlog runtime so Trealla-style timing works at the interactive
|
|
6079
|
+
top level without an import; it is absent from the strict ISO registry.
|
|
6078
6080
|
|
|
6079
6081
|
The sources are `src/lib/aggregate.pl`, `src/lib/clpz.pl`, `src/lib/comparison.pl`,
|
|
6080
6082
|
`src/lib/dates.pl`, `src/lib/iso_ext.pl`, `src/lib/lambda.pl`,
|
|
@@ -6110,7 +6112,7 @@ between solution branches.
|
|
|
6110
6112
|
| `library(clpz)` | `#>/2`, `#</2`, `#>=/2`, `#=</2`, `#=/2`, `#\=/2`, `#\/1`, `#<==>/2`, `#==>/2`, `#<==/2`, `#\//2`, `#\/2`, `#/\/2`, `in/2`, `ins/2`, `all_different/1`, `all_distinct/1`, `nvalue/2`, `sum/3`, `scalar_product/4`, `tuples_in/2`, `labeling/2`, `label/1`, `indomain/1`, `lex_chain/1`, `serialized/2`, `global_cardinality/2`, `global_cardinality/3`, `circuit/1`, `chain/2`, `element/3`, `zcompare/3`, `fd_var/1`, `fd_inf/2`, `fd_sup/2`, `fd_size/2`, `fd_dom/2` |
|
|
6111
6113
|
| `library(comparison)` | `lt/2`, `gt/2`, `le/2`, `ge/2` |
|
|
6112
6114
|
| `library(dates)` | `difference/3` |
|
|
6113
|
-
| `library(iso_ext)` | `call_nth/2`, `countall/2`, `forall/2`, `succ/2`, `cfor/3`, `findall/4`, `variant/2` |
|
|
6115
|
+
| `library(iso_ext)` | `call_nth/2`, `countall/2`, `forall/2`, `succ/2`, `cfor/3`, `findall/4`, `variant/2`, `time/1`, `.../2` |
|
|
6114
6116
|
| `library(lambda)` | `^/3`, `^/4`, `^/5`, `^/6`, `^/7`, `^/8`, `^/9`, `^/10`, `\/1`, `\/2`, `\/3`, `\/4`, `\/5`, `\/6`, `\/7`, `\/8`, `+\/2`, `+\/3`, `+\/4`, `+\/5`, `+\/6`, `+\/7`, `+\/8`, `+\/9` |
|
|
6115
6117
|
| `library(lists)` | `member/2`, `memberchk/2`, `select/3`, `append/2`, `append/3`, `last/2`, `same_length/2`, `nth0/3`, `nth0/4`, `nth1/3`, `nth1/4`, `reverse/2`, `length/2`, `maplist/2`, `maplist/3`, `maplist/4`, `maplist/5`, `maplist/6`, `maplist/7`, `maplist/8`, `foldl/4`, `foldl/5`, `foldl/6`, `sum_list/2`, `min_list/2`, `max_list/2`, `list_to_set/2`, `set_nth0/4`, `take/3`, `drop/3`, `slice/4` |
|
|
6116
6118
|
| `library(primes)` | `smallest_divisor_from/3` |
|
|
@@ -6144,7 +6146,7 @@ The current interoperability profile recognizes these library roles:
|
|
|
6144
6146
|
| Library | Role in the interoperability profile |
|
|
6145
6147
|
| --- | --- |
|
|
6146
6148
|
| `library(lists)` | Common list module. A conservative subset of its exports is in the shared predicate profile. |
|
|
6147
|
-
| `library(iso_ext)` | Common extension-module name. `call_nth/2`
|
|
6149
|
+
| `library(iso_ext)` | Common extension-module name. `call_nth/2`, `time/1`, and the `...//0` arbitrary-sequence helper are conservatively autoloaded for cross-engine source. |
|
|
6148
6150
|
| `library(lambda)` | Scryer-aligned higher-order notation. It is imported explicitly because loading it also installs the `+\` operator. |
|
|
6149
6151
|
| `library(prologue)` | EyeProlog compatibility module, not a common interop library name. `between/3` is nevertheless autoloaded from it so portable source need not name this EyeProlog-specific provider. |
|
|
6150
6152
|
|
|
@@ -6168,9 +6170,14 @@ recovery headroom so finite-heap exhaustion remains a catchable
|
|
|
6168
6170
|
`resource_error(memory)`.
|
|
6169
6171
|
|
|
6170
6172
|
`library(iso_ext)` is a common interop module name, but only part of its
|
|
6171
|
-
EyeProlog API belongs to the shared profile. `call_nth/2`
|
|
6172
|
-
|
|
6173
|
-
|
|
6173
|
+
EyeProlog API belongs to the shared profile. `call_nth/2`, `time/1`, and
|
|
6174
|
+
`.../2` are mapped there. `time/1` measures each solution of a meta-call and
|
|
6175
|
+
prints elapsed time, EyeProlog inference count, and MLips in Trealla-style form,
|
|
6176
|
+
for example `% Time elapsed 0.832s, 65551 Inferences, 0.079 MLips`; `...//0`
|
|
6177
|
+
describes an arbitrary number of input elements. Together they let the
|
|
6178
|
+
Trealla/Scryer DCG hand-off benchmark run in EyeProlog without source changes
|
|
6179
|
+
(assuming the usual list library is already imported in an interactive
|
|
6180
|
+
session). The interop exports of `library(lists)` and
|
|
6174
6181
|
`library(iso_ext)` are kept disjoint, so both modules can be imported together
|
|
6175
6182
|
without an accidental collision. `library(prologue)` remains a compatibility
|
|
6176
6183
|
umbrella and overlaps them; use selective imports when legacy code combines it
|
|
@@ -6206,14 +6213,19 @@ uses its ISO `copy_term/2` implementation for the fresh-copy step and does not
|
|
|
6206
6213
|
require a separate `copy_term_nat/2` predicate.
|
|
6207
6214
|
|
|
6208
6215
|
Autoloading is a convenience layered on top of the interoperability profile; it
|
|
6209
|
-
is not a general search through all EyeProlog libraries.
|
|
6210
|
-
|
|
6211
|
-
only when the interop table assigns it one canonical
|
|
6216
|
+
is not a general search through all EyeProlog libraries. When a source program
|
|
6217
|
+
or an explicit CLI/API goal is built, an otherwise undefined **unqualified**
|
|
6218
|
+
predicate may be autoloaded only when the interop table assigns it one canonical
|
|
6219
|
+
provider. The interactive top level keeps ordinary library imports explicit;
|
|
6220
|
+
`time/1` is available there because it is also a normal EyeProlog runtime
|
|
6221
|
+
extension. For example, the canonical build-time providers are:
|
|
6212
6222
|
|
|
6213
6223
|
| Predicate | Canonical autoload provider |
|
|
6214
6224
|
| --- | --- |
|
|
6215
6225
|
| `member/2` | `library(lists)` |
|
|
6216
6226
|
| `call_nth/2` | `library(iso_ext)` |
|
|
6227
|
+
| `time/1` | `library(iso_ext)` |
|
|
6228
|
+
| `.../2` | `library(iso_ext)` |
|
|
6217
6229
|
| `between/3` | `library(prologue)` |
|
|
6218
6230
|
|
|
6219
6231
|
Predicates outside that table require an explicit import even when EyeProlog
|
|
@@ -6255,9 +6267,9 @@ into later branches. Trealla's larger library also contains facilities such as
|
|
|
6255
6267
|
automata, cumulative and two-dimensional scheduling constraints, and
|
|
6256
6268
|
unbounded-domain propagation that EyeProlog does not currently export.
|
|
6257
6269
|
|
|
6258
|
-
|
|
6259
|
-
EyeProlog's extension relations `countall/2`,
|
|
6260
|
-
`findall/4`, and `variant/2`. `forall/2` checks an action for every solution of a
|
|
6270
|
+
Alongside its interop entries `call_nth/2`, `time/1`, and `.../2`,
|
|
6271
|
+
`library(iso_ext)` also exports EyeProlog's extension relations `countall/2`,
|
|
6272
|
+
`forall/2`, `succ/2`, `cfor/3`, `findall/4`, and `variant/2`. `forall/2` checks an action for every solution of a
|
|
6261
6273
|
condition; `cfor/3` enumerates an inclusive evaluated integer range; `succ/2`
|
|
6262
6274
|
relates adjacent nonnegative integers; `findall/4` collects into a difference
|
|
6263
6275
|
list; and `variant/2` recognizes terms equal up to variable renaming.
|