eyeprolog 1.3.33 → 1.3.35
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/package.json +1 -1
- package/src/iso.js +7 -1
- package/src/quads.js +4 -1
- package/src/repl.js +55 -48
- package/src/solver.js +35 -8
- package/test/run-regression.mjs +65 -3
- package/the-art-of-eyeprolog.md +16 -5
package/package.json
CHANGED
package/src/iso.js
CHANGED
|
@@ -1939,7 +1939,13 @@ function validateControlCallable(term, culprit, env) {
|
|
|
1939
1939
|
if (current.type !== COMPOUND || ![',', ';', '->'].includes(current.name) || current.arity !== 2) continue;
|
|
1940
1940
|
for (let index = current.arity - 1; index >= 0; index--) {
|
|
1941
1941
|
const argument = deref(current.args[index], env);
|
|
1942
|
-
|
|
1942
|
+
// A variable nested in a control construct is not an error at call/1
|
|
1943
|
+
// entry: an earlier goal may instantiate it before execution reaches
|
|
1944
|
+
// that position. If it is still unbound when selected, the solver then
|
|
1945
|
+
// raises instantiation_error at that point, after any preceding effects.
|
|
1946
|
+
// Non-variable non-callables are different: ISO call/1 validates those
|
|
1947
|
+
// eagerly and reports the whole control term as the culprit.
|
|
1948
|
+
if (argument.type === VAR) continue;
|
|
1943
1949
|
if (argument.type !== ATOM && argument.type !== COMPOUND) {
|
|
1944
1950
|
throw new PrologError('type_error(callable)', culprit);
|
|
1945
1951
|
}
|
package/src/quads.js
CHANGED
|
@@ -247,7 +247,10 @@ function executeQuery(program, query, input, maxSolutions, options) {
|
|
|
247
247
|
error,
|
|
248
248
|
tailOutput,
|
|
249
249
|
inputPosition,
|
|
250
|
-
|
|
250
|
+
// Accept direct structural evidence from EyeProlog's recursion guard as
|
|
251
|
+
// well as bounded-search evidence. A detected active-variant cycle is
|
|
252
|
+
// stronger evidence than merely reaching a timeout/inference ceiling.
|
|
253
|
+
loops: solver.recursionCycleDetected || solver.depthLimitExceeded || solver.inferenceLimitExceeded,
|
|
251
254
|
};
|
|
252
255
|
}
|
|
253
256
|
|
package/src/repl.js
CHANGED
|
@@ -18,6 +18,8 @@ RETURN or ".": stop enumeration
|
|
|
18
18
|
"p": print terms with depth limit
|
|
19
19
|
`;
|
|
20
20
|
|
|
21
|
+
const SCRIPTED_NEXT_QUERY = Symbol('scripted-next-query');
|
|
22
|
+
|
|
21
23
|
export async function runRepl(engine, options = {}) {
|
|
22
24
|
const input = options.input ?? process.stdin;
|
|
23
25
|
const output = options.output ?? process.stdout;
|
|
@@ -102,6 +104,7 @@ class LineReader {
|
|
|
102
104
|
this.output = output;
|
|
103
105
|
this.terminal = Boolean(input.isTTY && output.isTTY && typeof input.setRawMode === 'function');
|
|
104
106
|
this.history = [];
|
|
107
|
+
this.pendingLines = [];
|
|
105
108
|
this.currentPrompt = '?- ';
|
|
106
109
|
this.open();
|
|
107
110
|
}
|
|
@@ -119,16 +122,30 @@ class LineReader {
|
|
|
119
122
|
this.lines = this.readline[Symbol.asyncIterator]();
|
|
120
123
|
}
|
|
121
124
|
|
|
125
|
+
async nextLine() {
|
|
126
|
+
if (this.pendingLines.length > 0) return { done: false, value: this.pendingLines.shift() };
|
|
127
|
+
return this.lines.next();
|
|
128
|
+
}
|
|
129
|
+
|
|
122
130
|
async read(prompt) {
|
|
123
131
|
this.currentPrompt = prompt;
|
|
124
132
|
this.readline.setPrompt(prompt);
|
|
125
133
|
this.output.write(prompt);
|
|
126
|
-
const result = await this.
|
|
134
|
+
const result = await this.nextLine();
|
|
127
135
|
return result.done ? null : result.value;
|
|
128
136
|
}
|
|
129
137
|
|
|
130
138
|
async readControl(prompt) {
|
|
131
|
-
if (!this.terminal)
|
|
139
|
+
if (!this.terminal) {
|
|
140
|
+
const result = await this.nextLine();
|
|
141
|
+
if (result.done) return null;
|
|
142
|
+
if (!isScriptedAnswerControl(result.value)) {
|
|
143
|
+
this.pendingLines.unshift(result.value);
|
|
144
|
+
return SCRIPTED_NEXT_QUERY;
|
|
145
|
+
}
|
|
146
|
+
this.output.write(prompt);
|
|
147
|
+
return result.value;
|
|
148
|
+
}
|
|
132
149
|
this.output.write(prompt);
|
|
133
150
|
this.history = [...this.readline.history];
|
|
134
151
|
this.currentPrompt = '?- ';
|
|
@@ -250,6 +267,12 @@ class LineReader {
|
|
|
250
267
|
}
|
|
251
268
|
}
|
|
252
269
|
|
|
270
|
+
function isScriptedAnswerControl(line) {
|
|
271
|
+
if (line == null || line === '' || line === '\r' || line === '\n' || line === ' ') return true;
|
|
272
|
+
const control = line.trim();
|
|
273
|
+
return control.startsWith('.') || [';', 'n', 'a', 'f', 'w', 'p', 'h'].includes(control);
|
|
274
|
+
}
|
|
275
|
+
|
|
253
276
|
function runWithTerminalSignals(reader, operation) {
|
|
254
277
|
const suspended = reader.suspendForComputation();
|
|
255
278
|
try {
|
|
@@ -541,7 +564,6 @@ async function readSource(designation) {
|
|
|
541
564
|
async function solveQuery(engine, state, goal, reader, output) {
|
|
542
565
|
const variables = queryVariables(goal);
|
|
543
566
|
const solver = state.solver;
|
|
544
|
-
const demandDriven = containsTimedGoal(goal);
|
|
545
567
|
solver.solutionsSeen = 0;
|
|
546
568
|
const solutions = solver.solve([goal], new engine.Env(), 0);
|
|
547
569
|
let current = pullSolution(solver, solutions, reader);
|
|
@@ -560,12 +582,11 @@ async function solveQuery(engine, state, goal, reader, output) {
|
|
|
560
582
|
let firstAnswer = true;
|
|
561
583
|
let formattingAfterAdvance = false;
|
|
562
584
|
while (!current.result.done) {
|
|
563
|
-
//
|
|
564
|
-
//
|
|
565
|
-
//
|
|
566
|
-
//
|
|
567
|
-
//
|
|
568
|
-
const next = demandDriven ? null : pullSolution(solver, solutions, reader);
|
|
585
|
+
// Enumeration is demand-driven: never execute search for a future answer
|
|
586
|
+
// merely to decide how to punctuate the current one. That search may have
|
|
587
|
+
// side effects, and it belongs only to an explicit request for another
|
|
588
|
+
// answer. A scripted non-TTY session may start its next query directly;
|
|
589
|
+
// LineReader treats that as an implicit stop without consuming the query.
|
|
569
590
|
if (formattingAfterAdvance) output.write(' ');
|
|
570
591
|
formattingAfterAdvance = false;
|
|
571
592
|
output.write(current.output);
|
|
@@ -574,7 +595,11 @@ async function solveQuery(engine, state, goal, reader, output) {
|
|
|
574
595
|
answersShown++;
|
|
575
596
|
firstAnswer = false;
|
|
576
597
|
|
|
577
|
-
if (
|
|
598
|
+
if (!solver.hasPendingAlternatives()) {
|
|
599
|
+
// The solver is suspended at the yielded answer even though no work is
|
|
600
|
+
// left. Close the generator to run its cleanup/finally blocks without
|
|
601
|
+
// advancing search or executing future side effects.
|
|
602
|
+
if (typeof solutions.return === 'function') solutions.return();
|
|
578
603
|
output.write(`${continuesGraphicToken(answer, answer.length) ? ' ' : ''}.\n`);
|
|
579
604
|
return null;
|
|
580
605
|
}
|
|
@@ -586,6 +611,11 @@ async function solveQuery(engine, state, goal, reader, output) {
|
|
|
586
611
|
} else {
|
|
587
612
|
while (true) {
|
|
588
613
|
const controlLine = await reader.readControl('\n;');
|
|
614
|
+
if (controlLine === SCRIPTED_NEXT_QUERY) {
|
|
615
|
+
if (typeof solutions.return === 'function') solutions.return();
|
|
616
|
+
output.write(`${continuesGraphicToken(answer, answer.length) ? ' ' : ''}.\n`);
|
|
617
|
+
return null;
|
|
618
|
+
}
|
|
589
619
|
if (controlLine == null || controlLine === '' || controlLine === '\r' || controlLine === '\n' ||
|
|
590
620
|
controlLine.trimStart().startsWith('.')) {
|
|
591
621
|
if (typeof solutions.return === 'function') solutions.return();
|
|
@@ -618,52 +648,29 @@ async function solveQuery(engine, state, goal, reader, output) {
|
|
|
618
648
|
formattingAfterAdvance = true;
|
|
619
649
|
}
|
|
620
650
|
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
if (
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
throw requested.error;
|
|
633
|
-
}
|
|
634
|
-
if (requested.result.done) {
|
|
635
|
-
if (formattingAfterAdvance) output.write(' ');
|
|
636
|
-
formattingAfterAdvance = false;
|
|
637
|
-
output.write(`${requested.output}false.\n`);
|
|
638
|
-
return null;
|
|
639
|
-
}
|
|
640
|
-
current = requested;
|
|
641
|
-
continue;
|
|
651
|
+
// Drop the displayed substitution before resuming search. The next search
|
|
652
|
+
// step, including any side effects, happens only after the user asked for
|
|
653
|
+
// another answer (or selected automatic enumeration).
|
|
654
|
+
current = null;
|
|
655
|
+
const requested = pullSolution(solver, solutions, reader);
|
|
656
|
+
if (requested.error) {
|
|
657
|
+
if (formattingAfterAdvance) output.write(' ');
|
|
658
|
+
formattingAfterAdvance = false;
|
|
659
|
+
output.write(requested.output);
|
|
660
|
+
if (requested.error?.name === 'HaltSignal') return { halted: true, code: requested.error.code };
|
|
661
|
+
throw requested.error;
|
|
642
662
|
}
|
|
643
|
-
|
|
644
|
-
if (next.error) {
|
|
663
|
+
if (requested.result.done) {
|
|
645
664
|
if (formattingAfterAdvance) output.write(' ');
|
|
646
665
|
formattingAfterAdvance = false;
|
|
647
|
-
output.write(
|
|
648
|
-
|
|
649
|
-
throw next.error;
|
|
666
|
+
output.write(`${requested.output}false.\n`);
|
|
667
|
+
return null;
|
|
650
668
|
}
|
|
651
|
-
current =
|
|
669
|
+
current = requested;
|
|
652
670
|
}
|
|
653
671
|
return null;
|
|
654
672
|
}
|
|
655
673
|
|
|
656
|
-
function containsTimedGoal(goal) {
|
|
657
|
-
const stack = [goal];
|
|
658
|
-
while (stack.length !== 0) {
|
|
659
|
-
const term = stack.pop();
|
|
660
|
-
if (term?.type !== 'compound') continue;
|
|
661
|
-
if (term.name === 'time' && term.arity === 1) return true;
|
|
662
|
-
for (let index = term.args.length - 1; index >= 0; index--) stack.push(term.args[index]);
|
|
663
|
-
}
|
|
664
|
-
return false;
|
|
665
|
-
}
|
|
666
|
-
|
|
667
674
|
function pullSolution(solver, solutions, reader) {
|
|
668
675
|
const stream = solver.io.resolve('user_output');
|
|
669
676
|
const originalWrite = stream?.write;
|
package/src/solver.js
CHANGED
|
@@ -63,6 +63,10 @@ export class Solver {
|
|
|
63
63
|
// limit accounting. time/1 snapshots this counter around the measured goal.
|
|
64
64
|
this.inferenceObservation = options.inferenceObservation ?? { value: 0 };
|
|
65
65
|
this.inferenceLimitExceeded = false;
|
|
66
|
+
// Set when the normal-profile recursion guard detects re-entry of an
|
|
67
|
+
// already active variant on the current search path. Quad `loops` checks
|
|
68
|
+
// use this structural evidence in addition to bounded resource probes.
|
|
69
|
+
this.recursionCycleDetected = false;
|
|
66
70
|
this.maxMemoryBytes = options.maxMemoryBytes ?? softHeapLimit();
|
|
67
71
|
this.memoryRecovery = options.memoryRecovery ?? {
|
|
68
72
|
active: false,
|
|
@@ -261,6 +265,7 @@ export class Solver {
|
|
|
261
265
|
if (!child || child === this || !child.stats) return;
|
|
262
266
|
this.depthLimitExceeded ||= child.depthLimitExceeded;
|
|
263
267
|
this.inferenceLimitExceeded ||= child.inferenceLimitExceeded;
|
|
268
|
+
this.recursionCycleDetected ||= child.recursionCycleDetected;
|
|
264
269
|
for (const [key, value] of Object.entries(child.stats)) {
|
|
265
270
|
if (key === 'max_depth' || key === 'max_goal_count') {
|
|
266
271
|
this.stats[key] = Math.max(this.stats[key] ?? 0, value ?? 0);
|
|
@@ -674,7 +679,7 @@ export class Solver {
|
|
|
674
679
|
|
|
675
680
|
hasPendingAlternatives() {
|
|
676
681
|
// When solve() is suspended at an answer, active solve stacks contain only
|
|
677
|
-
// unexplored work. The
|
|
682
|
+
// unexplored work. The demand-driven REPL uses this without speculatively
|
|
678
683
|
// pulling the next answer.
|
|
679
684
|
return this.solveStacks.some((stack) => stack.length !== 0);
|
|
680
685
|
}
|
|
@@ -845,7 +850,10 @@ export class Solver {
|
|
|
845
850
|
}
|
|
846
851
|
|
|
847
852
|
*solveUserGoalUncached(group, goal, rest, env, depth) {
|
|
848
|
-
if (group.recursive && !group.cutRecursive && !group.linearNumeric && this.activeVariant(goal, env))
|
|
853
|
+
if (group.recursive && !group.cutRecursive && !group.linearNumeric && this.activeVariant(goal, env)) {
|
|
854
|
+
this.recursionCycleDetected = true;
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
849
857
|
// Program indexes provide candidate clauses, but every candidate is still
|
|
850
858
|
// freshened and unified below. The index is a performance hint, not a
|
|
851
859
|
// semantic shortcut.
|
|
@@ -1047,7 +1055,10 @@ function pushMemoAnswerFrames(stack, entry, goal, rest, env, depth, active, solv
|
|
|
1047
1055
|
}
|
|
1048
1056
|
|
|
1049
1057
|
function pushUserGoalUncachedFrames(stack, solver, group, goal, rest, env, depth, active) {
|
|
1050
|
-
if (group.recursive && !group.cutRecursive && !group.linearNumeric && activeVariantIn(goal, env, active))
|
|
1058
|
+
if (group.recursive && !group.cutRecursive && !group.linearNumeric && activeVariantIn(goal, env, active)) {
|
|
1059
|
+
solver.recursionCycleDetected = true;
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1051
1062
|
if (group.fastPi && pushFastPiFrames(stack, goal, rest, env, depth, active)) return;
|
|
1052
1063
|
if (tryPushGroundScalarRuleFrame(stack, solver, group, goal, rest, env, depth, active)) return;
|
|
1053
1064
|
if (tryPushGroundChainFrames(stack, solver, group, goal, rest, env, depth, active)) return;
|
|
@@ -1981,7 +1992,10 @@ function tryPushScalarFactRunFrames(stack, solver, goals, env, depth, active) {
|
|
|
1981
1992
|
}
|
|
1982
1993
|
|
|
1983
1994
|
const goal = runGoals[state.index];
|
|
1984
|
-
if (activeMightContain(goal, active) && activeVariantIn(goal, envWithLocal(env, state.names, state.values), active))
|
|
1995
|
+
if (activeMightContain(goal, active) && activeVariantIn(goal, envWithLocal(env, state.names, state.values), active)) {
|
|
1996
|
+
solver.recursionCycleDetected = true;
|
|
1997
|
+
continue;
|
|
1998
|
+
}
|
|
1985
1999
|
solver.stats.solve_one_goal_calls++;
|
|
1986
2000
|
const candidates = selectScalarFactCandidates(groups[state.index], goal, env, state.names, state.values);
|
|
1987
2001
|
const nextStates = [];
|
|
@@ -2035,7 +2049,10 @@ function* scalarFactRunSolutions(solver, goals, groups, env, depth, active) {
|
|
|
2035
2049
|
}
|
|
2036
2050
|
|
|
2037
2051
|
const goal = goals[state.index];
|
|
2038
|
-
if (activeMightContain(goal, active) && activeVariantIn(goal, envWithLocal(env, state.names, state.values), active))
|
|
2052
|
+
if (activeMightContain(goal, active) && activeVariantIn(goal, envWithLocal(env, state.names, state.values), active)) {
|
|
2053
|
+
solver.recursionCycleDetected = true;
|
|
2054
|
+
continue;
|
|
2055
|
+
}
|
|
2039
2056
|
solver.stats.solve_one_goal_calls++;
|
|
2040
2057
|
const candidates = selectScalarFactCandidates(groups[state.index], goal, env, state.names, state.values);
|
|
2041
2058
|
const nextStates = [];
|
|
@@ -2205,7 +2222,11 @@ function tryPushCompactBinaryChainFrames(stack, solver, group, goal, rest, env,
|
|
|
2205
2222
|
if (solver.solutionsSeen >= solver.solutionLimit) return true;
|
|
2206
2223
|
solver.stats.max_depth = Math.max(solver.stats.max_depth, currentDepth);
|
|
2207
2224
|
const seenSet = seen[secondType];
|
|
2208
|
-
if (!seenSet
|
|
2225
|
+
if (!seenSet) return true;
|
|
2226
|
+
if (seenSet.has(secondName)) {
|
|
2227
|
+
solver.recursionCycleDetected = true;
|
|
2228
|
+
return true;
|
|
2229
|
+
}
|
|
2209
2230
|
if (cache[secondType].has(secondName)) {
|
|
2210
2231
|
rememberCompactChainSuccess(cache, seen);
|
|
2211
2232
|
stack.push({ kind: 'goals', goals: rest, env, depth: depth + 1, active });
|
|
@@ -2326,8 +2347,14 @@ function tryPushGroundChainFrames(stack, solver, group, goal, rest, env, depth,
|
|
|
2326
2347
|
if (solver.solutionsSeen >= solver.solutionLimit) return true;
|
|
2327
2348
|
solver.stats.max_depth = Math.max(solver.stats.max_depth, currentDepth);
|
|
2328
2349
|
const key = groundChainKey(currentGoal);
|
|
2329
|
-
if (seen.has(key))
|
|
2330
|
-
|
|
2350
|
+
if (seen.has(key)) {
|
|
2351
|
+
solver.recursionCycleDetected = true;
|
|
2352
|
+
return true;
|
|
2353
|
+
}
|
|
2354
|
+
if (activeVariantIn(currentGoal, currentEnv, active)) {
|
|
2355
|
+
solver.recursionCycleDetected = true;
|
|
2356
|
+
return true;
|
|
2357
|
+
}
|
|
2331
2358
|
if (solver.groundChainSuccess.has(key)) {
|
|
2332
2359
|
rememberGroundChainSuccess(solver, seen);
|
|
2333
2360
|
stack.push({ kind: 'goals', goals: rest, env: baseEnv, depth: depth + 1, active });
|
package/test/run-regression.mjs
CHANGED
|
@@ -1317,6 +1317,22 @@ c4 ?- call((!;1)).
|
|
|
1317
1317
|
assertEqual(result.stdout, 'quads: 1 run, 1 passed, 0 failed.\n', 'quad report');
|
|
1318
1318
|
},
|
|
1319
1319
|
},
|
|
1320
|
+
{
|
|
1321
|
+
name: 'runQuads preserves output before a delayed call/1 instantiation error (issue #57)',
|
|
1322
|
+
run: () => {
|
|
1323
|
+
const result = publicApi.runQuads(`16, "7.8.3.4#9"\n?- call((write(3), X)).\n outputs("3"), instantiation_error.\n`);
|
|
1324
|
+
assertEqual(result.passed, 1, 'quad passed');
|
|
1325
|
+
assertEqual(result.stdout, 'quads: 1 run, 1 passed, 0 failed.\n', 'quad report');
|
|
1326
|
+
},
|
|
1327
|
+
},
|
|
1328
|
+
{
|
|
1329
|
+
name: 'runQuads recognizes recursion-guard cycle evidence as loops (issue #58)',
|
|
1330
|
+
run: () => {
|
|
1331
|
+
const result = publicApi.runQuads(`inf :- inf, inf.\n\n23\n?- inf.\n loops.\n`);
|
|
1332
|
+
assertEqual(result.passed, 1, 'quad passed');
|
|
1333
|
+
assertEqual(result.stdout, 'quads: 1 run, 1 passed, 0 failed.\n', 'quad report');
|
|
1334
|
+
},
|
|
1335
|
+
},
|
|
1320
1336
|
{
|
|
1321
1337
|
name: '--quads runs embedded tests and reports failures through exit status',
|
|
1322
1338
|
run: () => {
|
|
@@ -1659,6 +1675,52 @@ c4 ?- call((!;1)).
|
|
|
1659
1675
|
assertEqual(result.stderr, '', 'stderr');
|
|
1660
1676
|
},
|
|
1661
1677
|
},
|
|
1678
|
+
{
|
|
1679
|
+
name: 'REPL does not precompute an unrequested future alternative (issue #48)',
|
|
1680
|
+
run: () => {
|
|
1681
|
+
const result = runCli([], {
|
|
1682
|
+
input: '(X = first; (repeat, fail)).\nhalt.\n',
|
|
1683
|
+
timeout: 2000,
|
|
1684
|
+
});
|
|
1685
|
+
if (result.error) throw result.error;
|
|
1686
|
+
assertEqual(result.status, 0, 'exit status');
|
|
1687
|
+
assertEqual(result.stdout, '?- X = first.\n?- ', 'first answer is immediate');
|
|
1688
|
+
assertEqual(result.stderr, '', 'stderr');
|
|
1689
|
+
},
|
|
1690
|
+
},
|
|
1691
|
+
{
|
|
1692
|
+
name: 'REPL executes future side effects only after another answer is requested (issue #48)',
|
|
1693
|
+
run: () => {
|
|
1694
|
+
const stopped = runCli([], {
|
|
1695
|
+
input:
|
|
1696
|
+
'(X = first; (assertz(issue48_seen), X = second)).\n' +
|
|
1697
|
+
'current_predicate(issue48_seen/0).\n' +
|
|
1698
|
+
'halt.\n',
|
|
1699
|
+
});
|
|
1700
|
+
assertEqual(stopped.status, 0, 'stopped status');
|
|
1701
|
+
assertEqual(
|
|
1702
|
+
stopped.stdout,
|
|
1703
|
+
'?- X = first.\n?- false.\n?- ',
|
|
1704
|
+
'unrequested branch has no side effect',
|
|
1705
|
+
);
|
|
1706
|
+
|
|
1707
|
+
const advanced = runCli([], {
|
|
1708
|
+
input:
|
|
1709
|
+
'(X = first; (assertz(issue48_seen), X = second)).\n' +
|
|
1710
|
+
';\n' +
|
|
1711
|
+
'current_predicate(issue48_seen/0).\n' +
|
|
1712
|
+
'halt.\n',
|
|
1713
|
+
});
|
|
1714
|
+
assertEqual(advanced.status, 0, 'advanced status');
|
|
1715
|
+
assertEqual(
|
|
1716
|
+
advanced.stdout,
|
|
1717
|
+
'?- X = first\n; X = second.\n?- true.\n?- ',
|
|
1718
|
+
'requested branch performs its side effect',
|
|
1719
|
+
);
|
|
1720
|
+
assertEqual(stopped.stderr, '', 'stopped stderr');
|
|
1721
|
+
assertEqual(advanced.stderr, '', 'advanced stderr');
|
|
1722
|
+
},
|
|
1723
|
+
},
|
|
1662
1724
|
{
|
|
1663
1725
|
name: 'REPL bindings use argument syntax for operator atoms',
|
|
1664
1726
|
run: () => {
|
|
@@ -1691,7 +1753,7 @@ c4 ?- call((!;1)).
|
|
|
1691
1753
|
child.stdout.on('data', (text) => {
|
|
1692
1754
|
stdout += text;
|
|
1693
1755
|
if (stdout.endsWith('?- ')) sawQueryComputingPrompt = true;
|
|
1694
|
-
if (stdout.
|
|
1756
|
+
if (stdout.includes('\\n; ')) sawComputingPrompt = true;
|
|
1695
1757
|
});
|
|
1696
1758
|
child.stderr.on('data', (text) => { stderr += text; });
|
|
1697
1759
|
|
|
@@ -1712,10 +1774,10 @@ c4 ?- call((!;1)).
|
|
|
1712
1774
|
await waitFor(() => sawQueryComputingPrompt, 'query computing prompt');
|
|
1713
1775
|
await waitFor(() => stdout.endsWith(' false.\\n?- '), 'query result');
|
|
1714
1776
|
child.stdin.write('(N = 0; N = 1; (call_nth(repeat, 100000), N = 2)).\\n');
|
|
1715
|
-
await waitFor(() => stdout.endsWith(' N = 0
|
|
1777
|
+
await waitFor(() => stdout.endsWith(' N = 0'), 'first answer');
|
|
1716
1778
|
child.stdin.write(';\\n');
|
|
1717
1779
|
await waitFor(() => sawComputingPrompt, 'computing prompt');
|
|
1718
|
-
await waitFor(() => stdout.endsWith('; N = 1
|
|
1780
|
+
await waitFor(() => stdout.endsWith('; N = 1'), 'formatted answer');
|
|
1719
1781
|
child.stdin.write('\\n');
|
|
1720
1782
|
await waitFor(() => stdout.endsWith(' ... .\\n?- '), 'stopped enumeration');
|
|
1721
1783
|
child.stdin.write('halt.\\n');
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -6645,8 +6645,16 @@ When another answer exists in an interactive terminal, press `;`, Space, or
|
|
|
6645
6645
|
enumeration, `a` enumerates all remaining answers, and `f` advances to the
|
|
6646
6646
|
next five-answer boundary (5, 10, 15, ... displayed leaf answers), regardless
|
|
6647
6647
|
of how many answers were stepped through individually beforehand. `h` displays
|
|
6648
|
-
the answer-control help.
|
|
6649
|
-
|
|
6648
|
+
the answer-control help. Enumeration is demand-driven: after an answer is
|
|
6649
|
+
found, the top level does not pull a successor merely to discover whether the
|
|
6650
|
+
current answer is the last one. Search for a later answer, including any side
|
|
6651
|
+
effects reached on that path, starts only after an answer-control command asks
|
|
6652
|
+
to continue. If an unresolved alternative ultimately has no solution, asking
|
|
6653
|
+
for it may therefore finish with `false.`. In scripted non-TTY input, a new
|
|
6654
|
+
query line implicitly stops the preceding answer enumeration without consuming
|
|
6655
|
+
the new query; explicit `;`, `n`, Space, `a`, or `f` still requests more
|
|
6656
|
+
answers. Once the top-level reader has accepted a complete query, the following
|
|
6657
|
+
line begins with two spaces to mark active execution; a
|
|
6650
6658
|
third space appears when its result is ready for formatting. The answer prompt
|
|
6651
6659
|
is `;` with no trailing space while it waits for input; after an advance
|
|
6652
6660
|
command, one space marks active search and a second marks an answer ready for
|
|
@@ -6831,9 +6839,12 @@ variable in the renamed exception term. `...` and `ad_infinitum` accept further
|
|
|
6831
6839
|
indented descriptions after one query are independent checks: each re-runs the
|
|
6832
6840
|
query, each is counted in the `quads:` summary, and a failing description does
|
|
6833
6841
|
not suppress later descriptions for that query. `inputs/1` supplies and checks
|
|
6834
|
-
consumed characters; `outputs/1` checks emitted
|
|
6835
|
-
|
|
6836
|
-
|
|
6842
|
+
consumed characters; `outputs/1` checks characters emitted while reaching
|
|
6843
|
+
the described answer or error, including output produced before a later
|
|
6844
|
+
exception. `sto` marks an answer description that this finite-tree
|
|
6845
|
+
implementation skips. `loops` accepts direct active-variant cycle evidence from
|
|
6846
|
+
EyeProlog's normal recursion guard, with bounded depth/inference exhaustion as a
|
|
6847
|
+
fallback for loops that have no such structural witness. The advanced stream
|
|
6837
6848
|
annotations `peeks/1` and `waits`, and the unordered `other_answer_sequence`
|
|
6838
6849
|
annotation, are not executed by the current runner.
|
|
6839
6850
|
|