eyeprolog 1.3.12 → 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 +16 -10
- package/package.json +1 -1
- package/src/dcg.js +6 -1
- package/src/iso.js +60 -18
- package/src/lib/iso_ext.pl +17 -3
- package/src/repl.js +43 -12
- package/src/solver.js +170 -20
- package/src/standard-library.js +7 -2
- package/src/term.js +58 -10
- package/test/run-regression.mjs +117 -7
- package/the-art-of-eyeprolog.md +166 -102
package/README.md
CHANGED
|
@@ -107,13 +107,14 @@ keeps both long-running filters such as `\+ phrase((..., Pattern), Sequence)`
|
|
|
107
107
|
and repeated same-input probes bounded without turning memory safety into a
|
|
108
108
|
per-call cache-eviction cost.
|
|
109
109
|
|
|
110
|
-
Finite-tree unification also
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
110
|
+
Finite-tree unification also accepts an internal proven-nonoccurrence hint: when
|
|
111
|
+
the solver can prove that a variable cannot occur in the value it is about to
|
|
112
|
+
receive, that binding skips an otherwise redundant occurs traversal. The main
|
|
113
|
+
source-level case is conservative first use in a freshly renamed clause, inspired
|
|
114
|
+
by the local-variable optimization used by WAM-family systems; native construction
|
|
115
|
+
paths such as relational `length/2` reuse the same unifier mechanism. Repeated
|
|
115
116
|
variables, variables seen earlier in the clause, and ordinary public unification
|
|
116
|
-
remain fully occurs-checked. This is a
|
|
117
|
+
remain fully occurs-checked. This is a proof local to one binding, not a WAM-style
|
|
117
118
|
local/global variable stack. `phrase/2` likewise supplies its fixed `[]`
|
|
118
119
|
remainder directly to the grammar; `phrase/3` retains its delayed final output
|
|
119
120
|
unification for steadfastness.
|
|
@@ -200,8 +201,8 @@ noun --> [world] | [prolog].
|
|
|
200
201
|
%% goal: phrase(sentence, Words)
|
|
201
202
|
```
|
|
202
203
|
|
|
203
|
-
EyeProlog also adds
|
|
204
|
-
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
|
|
205
206
|
modules under `src/lib/`; the remaining control predicates and finite-domain
|
|
206
207
|
`library(clpz)` kernel use backtrackable host support.
|
|
207
208
|
They are ISO/IEC 13211-2 modules loaded explicitly by purpose, such as
|
|
@@ -224,8 +225,13 @@ headroom, so an exhausted finite heap is reported as a catchable
|
|
|
224
225
|
`resource_error(memory)` instead of degenerating into quadratic list checks.
|
|
225
226
|
|
|
226
227
|
`library(iso_ext)` is also accepted as a common interop module name.
|
|
227
|
-
EyeProlog exports `call_nth/2`
|
|
228
|
-
|
|
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.
|
|
229
235
|
The aligned `library(lists)` and `library(iso_ext)` exports are kept disjoint so
|
|
230
236
|
they can be imported together without an accidental import conflict. EyeProlog's
|
|
231
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,13 +176,15 @@ 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
|
|
|
182
184
|
function* unification({ goal, env }) {
|
|
183
185
|
const next = env.clone();
|
|
184
|
-
const
|
|
185
|
-
if (unify(goal.args[0], goal.args[1], next, {
|
|
186
|
+
const knownNonoccurringVariables = goal._knownNonoccurringVariables ?? null;
|
|
187
|
+
if (unify(goal.args[0], goal.args[1], next, { knownNonoccurringVariables })) yield next;
|
|
186
188
|
}
|
|
187
189
|
function* unificationWithOccursCheck({ goal, env }) {
|
|
188
190
|
const next = env.clone();
|
|
@@ -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
|
}
|
|
@@ -804,7 +836,7 @@ export class Solver {
|
|
|
804
836
|
attachBodyLocalFreshVariables(freshBody, localFreshPlan.body, freshVariables);
|
|
805
837
|
const next = env.clone();
|
|
806
838
|
this.stats.unify_calls++;
|
|
807
|
-
if (!unify(goal, freshHead, next, {
|
|
839
|
+
if (!unify(goal, freshHead, next, { knownNonoccurringVariables: headLocalFresh })) continue;
|
|
808
840
|
if (freshBody.length === 0) {
|
|
809
841
|
yield* this.solve(rest, next, depth + 1);
|
|
810
842
|
} else if (!groupNeedsActiveFrame(group)) {
|
|
@@ -1021,7 +1053,7 @@ function pushUserGoalUncachedFrames(stack, solver, group, goal, rest, env, depth
|
|
|
1021
1053
|
attachBodyLocalFreshVariables(freshBody, localFreshPlan.body, freshVariables);
|
|
1022
1054
|
const next = env.clone();
|
|
1023
1055
|
solver.stats.unify_calls++;
|
|
1024
|
-
if (!unify(goal, freshHead, next, {
|
|
1056
|
+
if (!unify(goal, freshHead, next, { knownNonoccurringVariables: headLocalFresh })) continue;
|
|
1025
1057
|
if (freshBody.length === 0) {
|
|
1026
1058
|
frames.push({
|
|
1027
1059
|
kind: 'goals',
|
|
@@ -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++) {
|
|
1086
|
-
const
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1118
|
+
const goal = freshBody[index];
|
|
1119
|
+
const knownNonoccurringVariables = freshVariableSet(plan[index] ?? [], freshVariables);
|
|
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 ||
|
|
@@ -1257,12 +1410,11 @@ function* fixedLengthSolutions(solver, list, length, env) {
|
|
|
1257
1410
|
const suffix = compactVariableList(remaining, `__length${id}_`);
|
|
1258
1411
|
const next = env.clone();
|
|
1259
1412
|
solver.stats.unify_calls++;
|
|
1260
|
-
//
|
|
1261
|
-
//
|
|
1262
|
-
//
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
yield next;
|
|
1413
|
+
// The compact skeleton contains only freshly generated variables, so the
|
|
1414
|
+
// dereferenced tail variable is known not to occur in it. Reuse the same
|
|
1415
|
+
// proven-nonoccurrence path as source-level first-use unification.
|
|
1416
|
+
const knownNonoccurringVariables = new Set([cursor.name]);
|
|
1417
|
+
if (unify(cursor, suffix, next, { knownNonoccurringVariables })) yield next;
|
|
1266
1418
|
}
|
|
1267
1419
|
|
|
1268
1420
|
function* generatedLengthSolutions(solver, list, length, env) {
|
|
@@ -1296,15 +1448,13 @@ function* generatedLengthSolutions(solver, list, length, env) {
|
|
|
1296
1448
|
|
|
1297
1449
|
const id = nextFreshId();
|
|
1298
1450
|
let suffix = emptyList();
|
|
1451
|
+
// Every generated suffix is built from fresh variables and therefore cannot
|
|
1452
|
+
// contain the caller's dereferenced tail variable. Share the general
|
|
1453
|
+
// proven-nonoccurrence unification path instead of bypassing unify() here.
|
|
1454
|
+
const knownNonoccurringVariables = new Set([cursor.name]);
|
|
1299
1455
|
for (let extra = 0n; ; extra++) {
|
|
1300
1456
|
const next = env.clone();
|
|
1301
|
-
|
|
1302
|
-
// freshly generated variables, so this binding cannot create a cycle.
|
|
1303
|
-
// Binding directly avoids an O(extra) occurs-check over the complete
|
|
1304
|
-
// growing suffix for every answer; without this, unbounded length/2
|
|
1305
|
-
// generation becomes quadratic and can spend hours before reaching the
|
|
1306
|
-
// normal memory resource guard (issue #49).
|
|
1307
|
-
next.bind(cursor.name, suffix);
|
|
1457
|
+
if (!unify(cursor, suffix, next, { knownNonoccurringVariables })) return;
|
|
1308
1458
|
const answer = bindGeneratedLength(solver, length, count + extra, next);
|
|
1309
1459
|
if (answer != null) yield answer;
|
|
1310
1460
|
suffix = cons(variable(`__length${id}_${extra}`), suffix);
|
|
@@ -2395,7 +2545,7 @@ function selectReadyDeterministicBuiltin(goals, env, registry) {
|
|
|
2395
2545
|
// A first-use proof is derived from source goal order. Do not move a later
|
|
2396
2546
|
// deterministic builtin across that equality: doing so could touch one of
|
|
2397
2547
|
// its proven-fresh variables before the checked binding executes.
|
|
2398
|
-
if (goal?.
|
|
2548
|
+
if (goal?._knownNonoccurringVariables != null) return 0;
|
|
2399
2549
|
if (goal.type !== COMPOUND && goal.type !== 'atom') continue;
|
|
2400
2550
|
const def = registry.get(goal.name, goal.arity);
|
|
2401
2551
|
if (!def?.deterministic || typeof def.ready !== 'function') continue;
|
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
|