eyeprolog 1.3.85 → 1.3.86
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 +125 -24
- package/src/scryer-compat.js +2 -2
- package/src/solver.js +79 -66
- package/test/run-regression.mjs +62 -12
- package/the-art-of-eyeprolog.md +26 -10
package/package.json
CHANGED
package/src/iso.js
CHANGED
|
@@ -137,8 +137,10 @@ export const isoBuiltins = {
|
|
|
137
137
|
registry.add('number_chars', 2, numberCharsBuiltin, { deterministic: true });
|
|
138
138
|
registry.add('number_codes', 2, numberCodesBuiltin, { deterministic: true });
|
|
139
139
|
|
|
140
|
-
registry.add('call', 1, callBuiltin);
|
|
141
|
-
for (let arity = 2; arity <= 8; arity++)
|
|
140
|
+
registry.add('call', 1, callBuiltin, { expandGoal: expandCallGoal });
|
|
141
|
+
for (let arity = 2; arity <= 8; arity++) {
|
|
142
|
+
registry.add('call', arity, callClosureBuiltin, { expandGoal: expandCallClosureGoal });
|
|
143
|
+
}
|
|
142
144
|
registry.add('catch', 3, catchBuiltin);
|
|
143
145
|
registry.add('throw', 1, throwBuiltin, { deterministic: true });
|
|
144
146
|
registry.add('\\+', 1, negationBuiltin, { deterministic: true });
|
|
@@ -166,7 +168,7 @@ export const eyePrologLibraryBuiltins = {
|
|
|
166
168
|
register(registry) {
|
|
167
169
|
registry.add('eyeprolog__call_nth', 2, callNthBuiltin, { eyePrologLibrary: true });
|
|
168
170
|
registry.add('eyeprolog__call_residue_vars', 2, callResidueVarsBuiltin, { eyePrologLibrary: true });
|
|
169
|
-
registry.add('eyeprolog__countall', 2, countAllBuiltin, { eyePrologLibrary: true });
|
|
171
|
+
registry.add('eyeprolog__countall', 2, countAllBuiltin, { deterministic: true, eyePrologLibrary: true });
|
|
170
172
|
registry.add('eyeprolog__freeze', 2, freezeBuiltin, { eyePrologLibrary: true });
|
|
171
173
|
registry.add('dif', 2, difBuiltin, { deterministic: true, eyePrologLibrary: true });
|
|
172
174
|
registry.add('time', 1, timeBuiltin, { eyePrologLibrary: true });
|
|
@@ -712,27 +714,43 @@ function assertBuiltin(atStart) {
|
|
|
712
714
|
};
|
|
713
715
|
}
|
|
714
716
|
|
|
715
|
-
function
|
|
717
|
+
function retractBuiltin(context) {
|
|
718
|
+
const state = { pending: true };
|
|
719
|
+
const iterator = retractSolutions(context, state);
|
|
720
|
+
iterator.hasPendingAlternatives = () => state.pending;
|
|
721
|
+
return iterator;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
function* retractSolutions({ solver, goal, env }, state) {
|
|
716
725
|
const parts = clauseParts(goal.args[0], env);
|
|
717
726
|
requireClauseHead(parts.head);
|
|
718
727
|
const group = solver.program.findGroup(parts.head.name, parts.head.arity, parts.head.module ?? goal.module ?? 'user');
|
|
719
728
|
if (isProcessorStaticProcedure(solver, parts.head) || (group && !group.dynamic)) {
|
|
720
729
|
throw new PrologError('permission_error(modify, static_procedure)', procedureIndicator(parts.head));
|
|
721
730
|
}
|
|
722
|
-
if (!group)
|
|
731
|
+
if (!group) {
|
|
732
|
+
state.pending = false;
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
723
735
|
// ISO logical update view: this call keeps the clauses that were visible
|
|
724
736
|
// when it began. A later retract/1 may erase one of those clauses from the
|
|
725
737
|
// live procedure, but must not invalidate this call's pending alternatives.
|
|
726
|
-
const
|
|
727
|
-
for (const clause of
|
|
738
|
+
const matches = [];
|
|
739
|
+
for (const clause of [...group.clauses]) {
|
|
728
740
|
const copied = freshCopy(compound('$clause', [clause.head, clauseBodyTerm(clause.body)]), new Env());
|
|
729
741
|
const next = env.clone();
|
|
730
742
|
if (!unify(parts.head, copied.args[0], next)) continue;
|
|
731
743
|
if (parts.rule && !unify(parts.body, copied.args[1], next)) continue;
|
|
732
744
|
if (!parts.rule && !(copied.args[1].type === ATOM && copied.args[1].name === 'true')) continue;
|
|
745
|
+
matches.push({ clause, next });
|
|
746
|
+
}
|
|
747
|
+
for (let index = 0; index < matches.length; index++) {
|
|
748
|
+
const { clause, next } = matches[index];
|
|
733
749
|
solver.program.removeDynamicClause(group, clause);
|
|
750
|
+
state.pending = index + 1 < matches.length;
|
|
734
751
|
yield next;
|
|
735
752
|
}
|
|
753
|
+
state.pending = false;
|
|
736
754
|
}
|
|
737
755
|
|
|
738
756
|
function* retractAllBuiltin({ solver, goal, env }) {
|
|
@@ -2413,6 +2431,11 @@ function callable(term, env) {
|
|
|
2413
2431
|
validateControlCallable(term, term, env);
|
|
2414
2432
|
return term;
|
|
2415
2433
|
}
|
|
2434
|
+
|
|
2435
|
+
function withPendingState(iterator, state) {
|
|
2436
|
+
iterator.hasPendingAlternatives = () => state.pending;
|
|
2437
|
+
return iterator;
|
|
2438
|
+
}
|
|
2416
2439
|
function validateControlCallable(term, culprit, env) {
|
|
2417
2440
|
// Only control constructs need their nested goals validated at meta-call
|
|
2418
2441
|
// entry. Walk them iteratively and dereference each nested goal lazily so
|
|
@@ -2438,9 +2461,14 @@ function validateControlCallable(term, culprit, env) {
|
|
|
2438
2461
|
}
|
|
2439
2462
|
}
|
|
2440
2463
|
}
|
|
2441
|
-
function
|
|
2464
|
+
function expandCallGoal({ goal, env }) {
|
|
2442
2465
|
const invoked = callable(goal.args[0], env);
|
|
2443
2466
|
if (invoked.module == null) invoked.module = goal.module ?? 'user';
|
|
2467
|
+
return invoked;
|
|
2468
|
+
}
|
|
2469
|
+
function* callBuiltin(context) {
|
|
2470
|
+
const { solver, env } = context;
|
|
2471
|
+
const invoked = expandCallGoal(context);
|
|
2444
2472
|
const child = solver.cloneForInnerGoal();
|
|
2445
2473
|
try {
|
|
2446
2474
|
yield* child.solve([invoked], env, 0);
|
|
@@ -2448,7 +2476,7 @@ function* callBuiltin({ solver, goal, env }) {
|
|
|
2448
2476
|
solver.absorbStatsFrom(child);
|
|
2449
2477
|
}
|
|
2450
2478
|
}
|
|
2451
|
-
function
|
|
2479
|
+
function expandCallClosureGoal({ goal, env }) {
|
|
2452
2480
|
const closure = callable(goal.args[0], env);
|
|
2453
2481
|
const existing = closure.type === COMPOUND ? closure.args : [];
|
|
2454
2482
|
const extra = goal.args.slice(1);
|
|
@@ -2457,6 +2485,11 @@ function* callClosureBuiltin({ solver, goal, env }) {
|
|
|
2457
2485
|
}
|
|
2458
2486
|
const invoked = compound(closure.name, [...existing, ...extra]);
|
|
2459
2487
|
invoked.module = closure.module ?? goal.module ?? 'user';
|
|
2488
|
+
return invoked;
|
|
2489
|
+
}
|
|
2490
|
+
function* callClosureBuiltin(context) {
|
|
2491
|
+
const { solver, env } = context;
|
|
2492
|
+
const invoked = expandCallClosureGoal(context);
|
|
2460
2493
|
const child = solver.cloneForInnerGoal();
|
|
2461
2494
|
try {
|
|
2462
2495
|
yield* child.solve([invoked], env, 0);
|
|
@@ -2509,7 +2542,12 @@ function writeElapsedTime(solver, startedAt, inferences) {
|
|
|
2509
2542
|
);
|
|
2510
2543
|
}
|
|
2511
2544
|
|
|
2512
|
-
function
|
|
2545
|
+
function timeBuiltin(context) {
|
|
2546
|
+
const state = { pending: true };
|
|
2547
|
+
return withPendingState(timeSolutions(context, state), state);
|
|
2548
|
+
}
|
|
2549
|
+
|
|
2550
|
+
function* timeSolutions({ solver, goal, env }, state) {
|
|
2513
2551
|
const invoked = callable(goal.args[0], env);
|
|
2514
2552
|
const child = solver.cloneForInnerGoal();
|
|
2515
2553
|
let startedAt = monotonicMilliseconds();
|
|
@@ -2519,12 +2557,15 @@ function* timeBuiltin({ solver, goal, env }) {
|
|
|
2519
2557
|
for (const answerEnv of child.solve([invoked], env, 0)) {
|
|
2520
2558
|
writeElapsedTime(solver, startedAt, child.inferenceObservation.value - startedInferences);
|
|
2521
2559
|
yieldedAny = true;
|
|
2560
|
+
state.pending = child.hasPendingAlternatives();
|
|
2522
2561
|
yield answerEnv;
|
|
2562
|
+
if (!state.pending) return;
|
|
2523
2563
|
// A resumed time/1 measures only the work required to reach the next
|
|
2524
2564
|
// answer, so nondeterministic calls get one timing line per solution.
|
|
2525
2565
|
startedAt = monotonicMilliseconds();
|
|
2526
2566
|
startedInferences = child.inferenceObservation.value;
|
|
2527
2567
|
}
|
|
2568
|
+
state.pending = false;
|
|
2528
2569
|
// A call that fails without producing an answer still reports the work.
|
|
2529
2570
|
if (!yieldedAny) writeElapsedTime(solver, startedAt, child.inferenceObservation.value - startedInferences);
|
|
2530
2571
|
} finally {
|
|
@@ -2532,10 +2573,18 @@ function* timeBuiltin({ solver, goal, env }) {
|
|
|
2532
2573
|
}
|
|
2533
2574
|
}
|
|
2534
2575
|
|
|
2535
|
-
function
|
|
2576
|
+
function callNthBuiltin(context) {
|
|
2577
|
+
const state = { pending: true };
|
|
2578
|
+
return withPendingState(callNthSolutions(context, state), state);
|
|
2579
|
+
}
|
|
2580
|
+
|
|
2581
|
+
function* callNthSolutions({ solver, goal, env }, state) {
|
|
2536
2582
|
const requestedTerm = deref(goal.args[1], env);
|
|
2537
2583
|
// Zero is the one Nth value that fails before Goal is inspected.
|
|
2538
|
-
if (requestedTerm.type === NUMBER && isDecimalInteger(requestedTerm.name) && BigInt(requestedTerm.name) === 0n)
|
|
2584
|
+
if (requestedTerm.type === NUMBER && isDecimalInteger(requestedTerm.name) && BigInt(requestedTerm.name) === 0n) {
|
|
2585
|
+
state.pending = false;
|
|
2586
|
+
return;
|
|
2587
|
+
}
|
|
2539
2588
|
|
|
2540
2589
|
const invoked = callable(goal.args[0], env);
|
|
2541
2590
|
let requested = null;
|
|
@@ -2554,9 +2603,13 @@ function* callNthBuiltin({ solver, goal, env }) {
|
|
|
2554
2603
|
nth++;
|
|
2555
2604
|
if (requested != null && nth < requested) continue;
|
|
2556
2605
|
const next = answerEnv.clone();
|
|
2557
|
-
if (unify(goal.args[1], numberTerm(nth.toString()), next))
|
|
2558
|
-
|
|
2606
|
+
if (unify(goal.args[1], numberTerm(nth.toString()), next)) {
|
|
2607
|
+
state.pending = requested == null && child.hasPendingAlternatives();
|
|
2608
|
+
yield next;
|
|
2609
|
+
if (requested != null || !state.pending) return;
|
|
2610
|
+
}
|
|
2559
2611
|
}
|
|
2612
|
+
state.pending = false;
|
|
2560
2613
|
} finally {
|
|
2561
2614
|
solver.absorbStatsFrom(child);
|
|
2562
2615
|
}
|
|
@@ -2648,12 +2701,22 @@ function callResidueVarsBuiltin({ solver, goal, env }) {
|
|
|
2648
2701
|
return iterator;
|
|
2649
2702
|
}
|
|
2650
2703
|
|
|
2651
|
-
function
|
|
2704
|
+
function freezeBuiltin(context) {
|
|
2705
|
+
const state = { pending: true };
|
|
2706
|
+
return withPendingState(freezeSolutions(context, state), state);
|
|
2707
|
+
}
|
|
2708
|
+
|
|
2709
|
+
function* freezeSolutions({ solver, goal, env }, state) {
|
|
2652
2710
|
const watched = deref(goal.args[0], env);
|
|
2653
2711
|
if (watched.type !== VAR) {
|
|
2654
2712
|
const child = solver.cloneForInnerGoal();
|
|
2655
2713
|
try {
|
|
2656
|
-
|
|
2714
|
+
for (const answerEnv of child.solve([callable(goal.args[1], env)], env, 0)) {
|
|
2715
|
+
state.pending = child.hasPendingAlternatives();
|
|
2716
|
+
yield answerEnv;
|
|
2717
|
+
if (!state.pending) return;
|
|
2718
|
+
}
|
|
2719
|
+
state.pending = false;
|
|
2657
2720
|
} finally {
|
|
2658
2721
|
solver.absorbStatsFrom(child);
|
|
2659
2722
|
}
|
|
@@ -2661,10 +2724,16 @@ function* freezeBuiltin({ solver, goal, env }) {
|
|
|
2661
2724
|
}
|
|
2662
2725
|
const next = env.clone();
|
|
2663
2726
|
next.delay(watched.name, goal.args[1], goal.module ?? 'user');
|
|
2727
|
+
state.pending = false;
|
|
2664
2728
|
yield next;
|
|
2665
2729
|
}
|
|
2666
2730
|
|
|
2667
|
-
function
|
|
2731
|
+
function phraseBuiltin(context) {
|
|
2732
|
+
const state = { pending: true };
|
|
2733
|
+
return withPendingState(phraseSolutions(context, state), state);
|
|
2734
|
+
}
|
|
2735
|
+
|
|
2736
|
+
function* phraseSolutions({ solver, goal, env }, state) {
|
|
2668
2737
|
const grammarBody0 = deref(goal.args[0], env);
|
|
2669
2738
|
if (grammarBody0.type === VAR) throw new PrologError('instantiation_error');
|
|
2670
2739
|
if (grammarBody0.type !== ATOM && grammarBody0.type !== COMPOUND) {
|
|
@@ -2721,7 +2790,12 @@ function* phraseBuiltin({ solver, goal, env }) {
|
|
|
2721
2790
|
skipListTailTabling: !repeatedInvocation,
|
|
2722
2791
|
});
|
|
2723
2792
|
try {
|
|
2724
|
-
|
|
2793
|
+
for (const answerEnv of child.solve(finish == null ? [expanded] : [expanded, finish], env, 0)) {
|
|
2794
|
+
state.pending = child.hasPendingAlternatives();
|
|
2795
|
+
yield answerEnv;
|
|
2796
|
+
if (!state.pending) return;
|
|
2797
|
+
}
|
|
2798
|
+
state.pending = false;
|
|
2725
2799
|
} finally {
|
|
2726
2800
|
solver.absorbStatsFrom(child);
|
|
2727
2801
|
solver.trimInnerTableScope('phrase');
|
|
@@ -2781,7 +2855,12 @@ function prologErrorBall(error) {
|
|
|
2781
2855
|
if (hasDefaultGroundErrorShape(error) || termIsGround(term)) return term;
|
|
2782
2856
|
return freshCopy(term, new Env());
|
|
2783
2857
|
}
|
|
2784
|
-
function
|
|
2858
|
+
function catchBuiltin(context) {
|
|
2859
|
+
const state = { pending: true };
|
|
2860
|
+
return withPendingState(catchSolutions(context, state), state);
|
|
2861
|
+
}
|
|
2862
|
+
|
|
2863
|
+
function* catchSolutions({ solver, goal, env }, state) {
|
|
2785
2864
|
let child = null;
|
|
2786
2865
|
try {
|
|
2787
2866
|
// Corrigendum 2 removed catch/3's own callability errors so that errors
|
|
@@ -2798,12 +2877,19 @@ function* catchBuiltin({ solver, goal, env }) {
|
|
|
2798
2877
|
if (result.done) solver.stats.deterministic_builtin_failures++;
|
|
2799
2878
|
else {
|
|
2800
2879
|
solver.stats.deterministic_builtin_successes++;
|
|
2880
|
+
state.pending = false;
|
|
2801
2881
|
yield result.value;
|
|
2802
2882
|
}
|
|
2883
|
+
state.pending = false;
|
|
2803
2884
|
return;
|
|
2804
2885
|
}
|
|
2805
2886
|
child = solver.cloneForInnerGoal();
|
|
2806
|
-
|
|
2887
|
+
for (const answerEnv of child.solve([invoked], env.clone(), 0)) {
|
|
2888
|
+
state.pending = child.hasPendingAlternatives();
|
|
2889
|
+
yield answerEnv;
|
|
2890
|
+
if (!state.pending) return;
|
|
2891
|
+
}
|
|
2892
|
+
state.pending = false;
|
|
2807
2893
|
} catch (error) {
|
|
2808
2894
|
const ball = error instanceof ThrownTerm
|
|
2809
2895
|
? error.term
|
|
@@ -2814,12 +2900,18 @@ function* catchBuiltin({ solver, goal, env }) {
|
|
|
2814
2900
|
const recovered = env.clone();
|
|
2815
2901
|
if (!unify(goal.args[1], ball, recovered)) throw error;
|
|
2816
2902
|
const recovery = callable(goal.args[2], recovered);
|
|
2817
|
-
if (recovery.type === ATOM && (recovery.name === 'false' || recovery.name === 'fail'))
|
|
2903
|
+
if (recovery.type === ATOM && (recovery.name === 'false' || recovery.name === 'fail')) {
|
|
2904
|
+
state.pending = false;
|
|
2905
|
+
return;
|
|
2906
|
+
}
|
|
2818
2907
|
if (recovery.type === ATOM && recovery.name === 'true') {
|
|
2908
|
+
state.pending = false;
|
|
2819
2909
|
yield recovered;
|
|
2820
2910
|
return;
|
|
2821
2911
|
}
|
|
2822
|
-
yield* solver
|
|
2912
|
+
yield* solveControlBranch(solver, recovery, recovered,
|
|
2913
|
+
(pending) => { state.pending = pending; });
|
|
2914
|
+
state.pending = false;
|
|
2823
2915
|
} finally {
|
|
2824
2916
|
if (child != null) solver.absorbStatsFrom(child);
|
|
2825
2917
|
}
|
|
@@ -2917,11 +3009,19 @@ function* disjunctionSolutions({ solver, goal, env }, state) {
|
|
|
2917
3009
|
(pending) => { state.pending = pending; });
|
|
2918
3010
|
state.pending = false;
|
|
2919
3011
|
}
|
|
2920
|
-
function
|
|
3012
|
+
function ifThenBuiltin(context) {
|
|
3013
|
+
const state = { pending: true };
|
|
3014
|
+
return withPendingState(ifThenSolutions(context, state), state);
|
|
3015
|
+
}
|
|
3016
|
+
|
|
3017
|
+
function* ifThenSolutions({ solver, goal, env }, state) {
|
|
2921
3018
|
for (const conditionEnv of solver.cloneForInnerGoal(1).solve([callable(goal.args[0], env)], env.clone(), 0)) {
|
|
2922
|
-
yield* solveControlBranch(solver, goal.args[1], conditionEnv
|
|
3019
|
+
yield* solveControlBranch(solver, goal.args[1], conditionEnv,
|
|
3020
|
+
(pending) => { state.pending = pending; });
|
|
3021
|
+
state.pending = false;
|
|
2923
3022
|
return;
|
|
2924
3023
|
}
|
|
3024
|
+
state.pending = false;
|
|
2925
3025
|
}
|
|
2926
3026
|
|
|
2927
3027
|
export { arithmeticValueTerm, evaluateArithmetic, compareArithmeticValues } from './iso-arithmetic.js';
|
|
@@ -2940,6 +3040,7 @@ export class BuiltinRegistry {
|
|
|
2940
3040
|
handler,
|
|
2941
3041
|
deterministic: options.deterministic ?? false,
|
|
2942
3042
|
deterministicWhen: options.deterministicWhen ?? null,
|
|
3043
|
+
expandGoal: options.expandGoal ?? null,
|
|
2943
3044
|
ready: options.ready ?? null,
|
|
2944
3045
|
fallbackWhenNotReady: options.fallbackWhenNotReady ?? false,
|
|
2945
3046
|
shouldUse: options.shouldUse ?? null,
|
package/src/scryer-compat.js
CHANGED
|
@@ -207,9 +207,9 @@ function* termCharsBuiltin({ solver, goal, env }) {
|
|
|
207
207
|
|
|
208
208
|
export const scryerCompatibilityBuiltins = {
|
|
209
209
|
register(registry) {
|
|
210
|
-
registry.add('eyeprolog__bb_get', 2, bbGetBuiltin, { eyePrologLibrary: true });
|
|
210
|
+
registry.add('eyeprolog__bb_get', 2, bbGetBuiltin, { deterministic: true, eyePrologLibrary: true });
|
|
211
211
|
registry.add('eyeprolog__bb_b_put', 2, bbPutBuiltin, { deterministic: true, eyePrologLibrary: true });
|
|
212
|
-
registry.add('eyeprolog__bb_global_get', 2, bbGlobalGetBuiltin, { eyePrologLibrary: true });
|
|
212
|
+
registry.add('eyeprolog__bb_global_get', 2, bbGlobalGetBuiltin, { deterministic: true, eyePrologLibrary: true });
|
|
213
213
|
registry.add('eyeprolog__bb_global_put', 2, bbGlobalPutBuiltin, { deterministic: true, eyePrologLibrary: true });
|
|
214
214
|
registry.add('eyeprolog__char_type', 2, charTypeBuiltin, { eyePrologLibrary: true });
|
|
215
215
|
registry.add('eyeprolog__current_time', 1, currentTimeBuiltin, { deterministic: true, eyePrologLibrary: true });
|
package/src/solver.js
CHANGED
|
@@ -614,7 +614,21 @@ export class Solver {
|
|
|
614
614
|
|
|
615
615
|
const def = callable ? this.registry.get(goal.name, goal.arity) : null;
|
|
616
616
|
this.active = active;
|
|
617
|
-
|
|
617
|
+
const builtinReady = def && builtinIsReadyOrAuthoritative(def, this, goal, env);
|
|
618
|
+
if (builtinReady && typeof def.expandGoal === 'function') {
|
|
619
|
+
// Meta-calls execute in this continuation instead of hiding their
|
|
620
|
+
// search behind a host-generator frame. Their real clause, control,
|
|
621
|
+
// and builtin frames therefore determine whether another answer is
|
|
622
|
+
// pending exactly as they do for a direct call. A fresh active marker
|
|
623
|
+
// keeps cut opaque at the call/N boundary.
|
|
624
|
+
const expanded = def.expandGoal({ solver: this, goal, env });
|
|
625
|
+
const invocation = { goal, env };
|
|
626
|
+
active = [...active, invocation];
|
|
627
|
+
goals = [expanded, { kind: 'releaseActive' }, ...rest];
|
|
628
|
+
depth++;
|
|
629
|
+
continue;
|
|
630
|
+
}
|
|
631
|
+
if (builtinReady) {
|
|
618
632
|
const deterministic = def.deterministic ||
|
|
619
633
|
def.deterministicWhen?.({ solver: this, goal, env }) === true;
|
|
620
634
|
const iterator = def.handler({ solver: this, goal, env });
|
|
@@ -1414,10 +1428,7 @@ function bundledBetweenIterator(solver, group, goal, env) {
|
|
|
1414
1428
|
// the generated value in the caller repeatedly revisits all earlier frames.
|
|
1415
1429
|
// Enumerate the canonical bundled relation directly while leaving user
|
|
1416
1430
|
// definitions and non-EyeProlog registries on the ordinary Prolog path.
|
|
1417
|
-
|
|
1418
|
-
const iterator = bundledBetweenSolutions(solver, goal, env, state);
|
|
1419
|
-
iterator.hasPendingAlternatives = () => state.pending;
|
|
1420
|
-
return iterator;
|
|
1431
|
+
return bundledBetweenSolutions(solver, goal, env);
|
|
1421
1432
|
}
|
|
1422
1433
|
|
|
1423
1434
|
function requireBetweenInteger(term, env) {
|
|
@@ -1429,7 +1440,7 @@ function requireBetweenInteger(term, env) {
|
|
|
1429
1440
|
return BigInt(value.name);
|
|
1430
1441
|
}
|
|
1431
1442
|
|
|
1432
|
-
function* bundledBetweenSolutions(solver, goal, env
|
|
1443
|
+
function* bundledBetweenSolutions(solver, goal, env) {
|
|
1433
1444
|
const lower = requireBetweenInteger(goal.args[0], env);
|
|
1434
1445
|
const upper = requireBetweenInteger(goal.args[1], env);
|
|
1435
1446
|
const requested = deref(goal.args[2], env);
|
|
@@ -1439,23 +1450,15 @@ function* bundledBetweenSolutions(solver, goal, env, state) {
|
|
|
1439
1450
|
throw new PrologError('type_error(integer)', requested);
|
|
1440
1451
|
}
|
|
1441
1452
|
const value = BigInt(requested.name);
|
|
1442
|
-
if (value >= lower && value <= upper)
|
|
1443
|
-
state.pending = false;
|
|
1444
|
-
yield env;
|
|
1445
|
-
}
|
|
1446
|
-
state.pending = false;
|
|
1453
|
+
if (value >= lower && value <= upper) yield env;
|
|
1447
1454
|
return;
|
|
1448
1455
|
}
|
|
1449
1456
|
|
|
1450
1457
|
for (let value = lower; value <= upper; value++) {
|
|
1451
1458
|
const next = env.clone();
|
|
1452
1459
|
solver.stats.unify_calls++;
|
|
1453
|
-
if (unify(goal.args[2], numberTerm(value), next))
|
|
1454
|
-
state.pending = value < upper;
|
|
1455
|
-
yield next;
|
|
1456
|
-
}
|
|
1460
|
+
if (unify(goal.args[2], numberTerm(value), next)) yield next;
|
|
1457
1461
|
}
|
|
1458
|
-
state.pending = false;
|
|
1459
1462
|
}
|
|
1460
1463
|
|
|
1461
1464
|
function bundledMemberIterator(solver, group, goal, env) {
|
|
@@ -1472,50 +1475,18 @@ function bundledMemberIterator(solver, group, goal, env) {
|
|
|
1472
1475
|
let cursor = deref(goal.args[1], env);
|
|
1473
1476
|
while (isCons(cursor)) cursor = deref(cursor.args[1], env);
|
|
1474
1477
|
if (!isEmptyList(cursor)) return null;
|
|
1475
|
-
|
|
1476
|
-
const iterator = bundledMemberSolutions(solver, goal, env, state);
|
|
1477
|
-
iterator.hasPendingAlternatives = () => state.pending;
|
|
1478
|
-
return iterator;
|
|
1478
|
+
return bundledMemberSolutions(solver, goal, env);
|
|
1479
1479
|
}
|
|
1480
1480
|
|
|
1481
|
-
function* bundledMemberSolutions(solver, goal, env
|
|
1481
|
+
function* bundledMemberSolutions(solver, goal, env) {
|
|
1482
1482
|
let cursor = deref(goal.args[1], env);
|
|
1483
1483
|
while (isCons(cursor)) {
|
|
1484
1484
|
const item = cursor.args[0];
|
|
1485
1485
|
cursor = deref(cursor.args[1], env);
|
|
1486
1486
|
const next = env.clone();
|
|
1487
1487
|
solver.stats.unify_calls++;
|
|
1488
|
-
if (unify(goal.args[0], item, next))
|
|
1489
|
-
state.pending = bundledMemberMayMatch(goal.args[0], cursor, env);
|
|
1490
|
-
yield next;
|
|
1491
|
-
}
|
|
1492
|
-
}
|
|
1493
|
-
state.pending = false;
|
|
1494
|
-
}
|
|
1495
|
-
|
|
1496
|
-
function bundledMemberMayMatch(target, cursor, env) {
|
|
1497
|
-
while (isCons(cursor)) {
|
|
1498
|
-
if (!termsCannotUnify(target, cursor.args[0], env)) return true;
|
|
1499
|
-
cursor = deref(cursor.args[1], env);
|
|
1500
|
-
}
|
|
1501
|
-
return false;
|
|
1502
|
-
}
|
|
1503
|
-
|
|
1504
|
-
function termsCannotUnify(left, right, env) {
|
|
1505
|
-
const pending = [[left, right]];
|
|
1506
|
-
while (pending.length > 0) {
|
|
1507
|
-
const [leftItem, rightItem] = pending.pop();
|
|
1508
|
-
const actualLeft = derefForLocal(leftItem, env);
|
|
1509
|
-
const actualRight = derefForLocal(rightItem, env);
|
|
1510
|
-
if (actualLeft.type === VAR || actualRight.type === VAR) continue;
|
|
1511
|
-
if (actualLeft.type !== actualRight.type || actualLeft.name !== actualRight.name ||
|
|
1512
|
-
actualLeft.arity !== actualRight.arity) return true;
|
|
1513
|
-
if (actualLeft.type !== COMPOUND) continue;
|
|
1514
|
-
for (let index = 0; index < actualLeft.arity; index++) {
|
|
1515
|
-
pending.push([actualLeft.args[index], actualRight.args[index]]);
|
|
1516
|
-
}
|
|
1488
|
+
if (unify(goal.args[0], item, next)) yield next;
|
|
1517
1489
|
}
|
|
1518
|
-
return false;
|
|
1519
1490
|
}
|
|
1520
1491
|
|
|
1521
1492
|
function bundledEllipsisPlan(solver, group, goal, rest, env) {
|
|
@@ -1630,13 +1601,7 @@ function bundledLengthIterator(solver, group, goal, env) {
|
|
|
1630
1601
|
if (length.type === VAR && cursor.name === length.name) return null;
|
|
1631
1602
|
}
|
|
1632
1603
|
}
|
|
1633
|
-
|
|
1634
|
-
// A supplied length selects at most one list, and a closed supplied list has
|
|
1635
|
-
// exactly one length. Only an open list paired with an output variable can
|
|
1636
|
-
// produce another answer after the first one.
|
|
1637
|
-
const hasOpenTail = cursor.type === VAR;
|
|
1638
|
-
iterator.hasPendingAlternatives = () => length.type === VAR && hasOpenTail;
|
|
1639
|
-
return iterator;
|
|
1604
|
+
return bundledLengthSolutions(solver, goal, env);
|
|
1640
1605
|
}
|
|
1641
1606
|
|
|
1642
1607
|
function* bundledLengthSolutions(solver, goal, env) {
|
|
@@ -2210,13 +2175,7 @@ function pushStreamingScalarFactRun(stack, solver, runGoals, groups, rest, env,
|
|
|
2210
2175
|
const iterator = scalarFactRunSolutions(solver, runGoals, groups, env, depth, active);
|
|
2211
2176
|
const first = iterator.next();
|
|
2212
2177
|
if (first.done) return;
|
|
2213
|
-
stack.
|
|
2214
|
-
kind: 'resumeBuiltin',
|
|
2215
|
-
iterator,
|
|
2216
|
-
goals: rest,
|
|
2217
|
-
depth: depth + runGoals.length,
|
|
2218
|
-
active,
|
|
2219
|
-
});
|
|
2178
|
+
pushResumeBuiltinFrame(stack, iterator, rest, depth + runGoals.length, active);
|
|
2220
2179
|
stack.push({
|
|
2221
2180
|
kind: 'goals',
|
|
2222
2181
|
goals: rest,
|
|
@@ -2854,10 +2813,64 @@ function builtinIsReadyOrAuthoritative(def, solver, goal, env) {
|
|
|
2854
2813
|
|
|
2855
2814
|
function iteratorMayHavePendingAlternatives(iterator) {
|
|
2856
2815
|
const predicate = iterator?.hasPendingAlternatives;
|
|
2857
|
-
return typeof predicate
|
|
2816
|
+
return typeof predicate === 'function' && predicate.call(iterator) !== false;
|
|
2817
|
+
}
|
|
2818
|
+
|
|
2819
|
+
function lookaheadChoiceIterator(iterator) {
|
|
2820
|
+
let buffered = null;
|
|
2821
|
+
let bufferedReady = false;
|
|
2822
|
+
let closed = false;
|
|
2823
|
+
|
|
2824
|
+
const fill = () => {
|
|
2825
|
+
if (closed || bufferedReady) return;
|
|
2826
|
+
buffered = iterator.next();
|
|
2827
|
+
bufferedReady = true;
|
|
2828
|
+
if (buffered.done) closed = true;
|
|
2829
|
+
};
|
|
2830
|
+
|
|
2831
|
+
return {
|
|
2832
|
+
next() {
|
|
2833
|
+
fill();
|
|
2834
|
+
if (buffered?.done !== false) return buffered ?? { done: true, value: undefined };
|
|
2835
|
+
const result = buffered;
|
|
2836
|
+
buffered = null;
|
|
2837
|
+
bufferedReady = false;
|
|
2838
|
+
// Ordinary relational iterators are advanced by one answer so their
|
|
2839
|
+
// exhausted state is known before this answer reaches the caller.
|
|
2840
|
+
fill();
|
|
2841
|
+
return result;
|
|
2842
|
+
},
|
|
2843
|
+
return(value) {
|
|
2844
|
+
if (!closed) iterator.return?.();
|
|
2845
|
+
closed = true;
|
|
2846
|
+
buffered = { done: true, value };
|
|
2847
|
+
bufferedReady = true;
|
|
2848
|
+
return buffered;
|
|
2849
|
+
},
|
|
2850
|
+
throw(error) {
|
|
2851
|
+
closed = true;
|
|
2852
|
+
if (typeof iterator.throw === 'function') return iterator.throw(error);
|
|
2853
|
+
throw error;
|
|
2854
|
+
},
|
|
2855
|
+
prepareClose(reason) {
|
|
2856
|
+
iterator.prepareClose?.(reason);
|
|
2857
|
+
},
|
|
2858
|
+
hasPendingAlternatives() {
|
|
2859
|
+
fill();
|
|
2860
|
+
return buffered?.done === false;
|
|
2861
|
+
},
|
|
2862
|
+
[Symbol.iterator]() { return this; },
|
|
2863
|
+
};
|
|
2864
|
+
}
|
|
2865
|
+
|
|
2866
|
+
function choicepointAwareIterator(iterator) {
|
|
2867
|
+
return typeof iterator?.hasPendingAlternatives === 'function'
|
|
2868
|
+
? iterator
|
|
2869
|
+
: lookaheadChoiceIterator(iterator);
|
|
2858
2870
|
}
|
|
2859
2871
|
|
|
2860
2872
|
function pushResumeBuiltinFrame(stack, iterator, goals, depth, active) {
|
|
2873
|
+
iterator = choicepointAwareIterator(iterator);
|
|
2861
2874
|
if (!iteratorMayHavePendingAlternatives(iterator)) {
|
|
2862
2875
|
// Finish the suspended generator now so its finally blocks release child
|
|
2863
2876
|
// solvers, aggregate statistics, and perform predicate-local cleanup.
|
package/test/run-regression.mjs
CHANGED
|
@@ -1999,11 +1999,13 @@ c4 ?- call((!;1)).
|
|
|
1999
1999
|
},
|
|
2000
2000
|
},
|
|
2001
2001
|
{
|
|
2002
|
-
name: 'predicates discard exhausted choicepoints
|
|
2002
|
+
name: 'direct and meta-called predicates discard exhausted choicepoints (issue #74 comment 5427164365)',
|
|
2003
2003
|
run: () => {
|
|
2004
2004
|
const result = runCli([], {
|
|
2005
2005
|
input:
|
|
2006
2006
|
'X=1;X=2.\n;\n' +
|
|
2007
|
+
'call(X=1).\n' +
|
|
2008
|
+
'call(=(X),1).\n' +
|
|
2007
2009
|
'setof(X,true,Xs).\n' +
|
|
2008
2010
|
'findall(t,false,Xs).\n' +
|
|
2009
2011
|
'use_module(library(lists)).\n' +
|
|
@@ -2015,6 +2017,8 @@ c4 ?- call((!;1)).
|
|
|
2015
2017
|
assertEqual(result.stdout,
|
|
2016
2018
|
'?- X = 1\n' +
|
|
2017
2019
|
'; X = 2.\n' +
|
|
2020
|
+
'?- X = 1.\n' +
|
|
2021
|
+
'?- X = 1.\n' +
|
|
2018
2022
|
'?- Xs = [_A].\n' +
|
|
2019
2023
|
'?- Xs = [].\n' +
|
|
2020
2024
|
'?- true.\n' +
|
|
@@ -2044,6 +2048,16 @@ c4 ?- call((!;1)).
|
|
|
2044
2048
|
};
|
|
2045
2049
|
|
|
2046
2050
|
finalAnswerHasNoFrames('', 'X=1;X=2', 2);
|
|
2051
|
+
finalAnswerHasNoFrames('', 'call(X=1)', 1);
|
|
2052
|
+
finalAnswerHasNoFrames('', 'call(=(X),1)', 1);
|
|
2053
|
+
finalAnswerHasNoFrames('', 'atom_concat(a,X,ab)', 1);
|
|
2054
|
+
finalAnswerHasNoFrames('', 'sub_atom(ab,0,2,0,S)', 1);
|
|
2055
|
+
finalAnswerHasNoFrames('', 'current_prolog_flag(double_quotes,_Value)', 1);
|
|
2056
|
+
finalAnswerHasNoFrames('', "current_op(700,xfx,'=')", 1);
|
|
2057
|
+
finalAnswerHasNoFrames(':- dynamic(p/1).\np(a).', 'clause(p(X),true)', 1);
|
|
2058
|
+
finalAnswerHasNoFrames(':- dynamic(p/1).\np(a).', 'retract(p(X))', 1);
|
|
2059
|
+
finalAnswerHasNoFrames('', 'catch(true,_,fail)', 1);
|
|
2060
|
+
finalAnswerHasNoFrames('', 'phrase([],[])', 1);
|
|
2047
2061
|
finalAnswerHasNoFrames('', 'setof(X,true,Xs)', 1);
|
|
2048
2062
|
finalAnswerHasNoFrames('', 'findall(t,false,Xs)', 1);
|
|
2049
2063
|
finalAnswerHasNoFrames(':- use_module(library(lists)).', 'member(a,"ba")', 1, 'lists');
|
|
@@ -2056,6 +2070,38 @@ c4 ?- call((!;1)).
|
|
|
2056
2070
|
assertEqual(groupedSolutions.next().done, false, 'first grouped setof/3 answer');
|
|
2057
2071
|
assertEqual(grouped.hasPendingAlternatives(), true, 'genuine grouped setof/3 choicepoint');
|
|
2058
2072
|
groupedSolutions.return();
|
|
2073
|
+
|
|
2074
|
+
const called = new Solver(Program.parse(''), { registry: getEyePrologRegistry() });
|
|
2075
|
+
const calledSolutions = called.solve([parseGoalText('call((X=1;X=2))')], new Env(), 0);
|
|
2076
|
+
assertEqual(calledSolutions.next().done, false, 'called disjunction first answer');
|
|
2077
|
+
assertEqual(called.hasPendingAlternatives(), true, 'called disjunction retains its real alternative');
|
|
2078
|
+
assertEqual(calledSolutions.next().done, false, 'called disjunction second answer');
|
|
2079
|
+
assertEqual(called.hasPendingAlternatives(), false, 'called disjunction removes its exhausted alternative');
|
|
2080
|
+
calledSolutions.return();
|
|
2081
|
+
|
|
2082
|
+
const customRegistry = new BuiltinRegistry();
|
|
2083
|
+
customRegistry.add('single', 1, function* ({ goal, env }) {
|
|
2084
|
+
const next = env.clone();
|
|
2085
|
+
if (unify(goal.args[0], atom('only'), next)) yield next;
|
|
2086
|
+
});
|
|
2087
|
+
const custom = new Solver(Program.parse(''), { registry: customRegistry });
|
|
2088
|
+
const customSolutions = custom.solve([parseGoalText('single(X)')], new Env(), 0);
|
|
2089
|
+
assertEqual(customSolutions.next().done, false, 'custom relational iterator answer');
|
|
2090
|
+
assertEqual(custom.hasPendingAlternatives(), false,
|
|
2091
|
+
'unannotated relational iterator uses the universal lookahead protocol');
|
|
2092
|
+
customSolutions.return();
|
|
2093
|
+
|
|
2094
|
+
const dynamicProgram = Program.parse(':- dynamic(p/1).\np(a).\np(b).\n');
|
|
2095
|
+
const dynamicSolver = new Solver(dynamicProgram, { registry: getEyePrologRegistry() });
|
|
2096
|
+
const dynamicSolutions = dynamicSolver.solve([parseGoalText('catch(retract(p(X)),_,fail)')], new Env(), 0);
|
|
2097
|
+
assertEqual(dynamicSolutions.next().done, false, 'stateful protected iterator first answer');
|
|
2098
|
+
assertEqual(dynamicProgram.findGroup('p', 1).clauses.length, 1,
|
|
2099
|
+
'lookahead does not perform the next retraction');
|
|
2100
|
+
assertEqual(dynamicSolver.hasPendingAlternatives(), true,
|
|
2101
|
+
'stateful protected iterator retains its real second retraction');
|
|
2102
|
+
dynamicSolutions.return();
|
|
2103
|
+
assertEqual(dynamicProgram.findGroup('p', 1).clauses.length, 1,
|
|
2104
|
+
'closing stateful iterator leaves the unrequested clause intact');
|
|
2059
2105
|
},
|
|
2060
2106
|
},
|
|
2061
2107
|
{
|
|
@@ -5373,21 +5419,13 @@ answer(ok) :-
|
|
|
5373
5419
|
import { Program, Solver, Env, deref, variable, parseGoalText, getEyePrologRegistry } from ${JSON.stringify(engineUrl)};
|
|
5374
5420
|
import { usedHeapSize } from ${JSON.stringify(platformUrl)};
|
|
5375
5421
|
const program = Program.parse(${JSON.stringify(programText)}, { sourceMetadata: false });
|
|
5376
|
-
|
|
5377
|
-
// of relying on V8's version-dependent old-generation promotion.
|
|
5378
|
-
const solver = new Solver(program, {
|
|
5379
|
-
registry: getEyePrologRegistry(),
|
|
5380
|
-
maxMemoryBytes: usedHeapSize() + 20 * 1024 * 1024,
|
|
5381
|
-
});
|
|
5382
|
-
const execute = (text) => {
|
|
5383
|
-
const goal = parseGoalText(text, { operatorDefinitions: [...program.operators.values()] });
|
|
5384
|
-
return [...solver.solve([goal], new Env(), 0)];
|
|
5385
|
-
};
|
|
5422
|
+
const registry = getEyePrologRegistry();
|
|
5386
5423
|
const reported = parseGoalText(${JSON.stringify(reportedGoal)}, {
|
|
5387
5424
|
operatorDefinitions: [...program.operators.values()],
|
|
5388
5425
|
});
|
|
5426
|
+
const reportedSolver = new Solver(program, { registry, maxMemoryBytes: Infinity });
|
|
5389
5427
|
let reportedAnswers = 0;
|
|
5390
|
-
for (const answer of
|
|
5428
|
+
for (const answer of reportedSolver.solve([reported], new Env(), 0)) {
|
|
5391
5429
|
reportedAnswers++;
|
|
5392
5430
|
if (reportedAnswers !== 14) continue;
|
|
5393
5431
|
if (deref(variable('I'), answer).name !== '13' || deref(variable('N'), answer).name !== '8192') {
|
|
@@ -5396,6 +5434,18 @@ answer(ok) :-
|
|
|
5396
5434
|
break;
|
|
5397
5435
|
}
|
|
5398
5436
|
if (reportedAnswers !== 14) throw new Error('reported query produced too few answers');
|
|
5437
|
+
|
|
5438
|
+
// Exercise EyeProlog's recovery window at a stable threshold relative
|
|
5439
|
+
// to the heap after the preliminary reproduction. Keeping that probe
|
|
5440
|
+
// unlimited prevents host GC timing from exhausting the test early.
|
|
5441
|
+
const solver = new Solver(program, {
|
|
5442
|
+
registry,
|
|
5443
|
+
maxMemoryBytes: usedHeapSize() + 8 * 1024 * 1024,
|
|
5444
|
+
});
|
|
5445
|
+
const execute = (text) => {
|
|
5446
|
+
const goal = parseGoalText(text, { operatorDefinitions: [...program.operators.values()] });
|
|
5447
|
+
return [...solver.solve([goal], new Env(), 0)];
|
|
5448
|
+
};
|
|
5399
5449
|
let caught = null;
|
|
5400
5450
|
try { execute('length(L,32768),l(L)'); } catch (error) { caught = error; }
|
|
5401
5451
|
if (caught?.formal !== 'resource_error(memory)') throw caught ?? new Error('no resource error');
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -2017,7 +2017,12 @@ answer(X) :- host_status(service, X).
|
|
|
2017
2017
|
```
|
|
2018
2018
|
|
|
2019
2019
|
Only mark a built-in deterministic when it can produce at most one environment
|
|
2020
|
-
for a call.
|
|
2020
|
+
for a call. An unmarked resumable handler is treated as an effect-free relation:
|
|
2021
|
+
the solver may buffer one answer to determine its exact pending state. A
|
|
2022
|
+
stateful or meta-control handler must instead return an iterator with a
|
|
2023
|
+
`hasPendingAlternatives()` method, updated before each yield, so the solver can
|
|
2024
|
+
remain demand-driven without advancing it speculatively. A mode-sensitive
|
|
2025
|
+
extension can additionally provide `ready`,
|
|
2021
2026
|
`fallbackWhenNotReady`, and `shouldUse` metadata. This metadata affects
|
|
2022
2027
|
dispatch and safe early filtering, so it belongs to the extension's contract.
|
|
2023
2028
|
|
|
@@ -5136,8 +5141,12 @@ at a named boundary.
|
|
|
5136
5141
|
## 34. Control, exceptions, and grouped solutions
|
|
5137
5142
|
|
|
5138
5143
|
The control predicates accept goals as arguments. `call/1` invokes a callable
|
|
5139
|
-
term, `
|
|
5140
|
-
|
|
5144
|
+
term, and `call/2-8` appends arguments to a callable closure. The expanded goal
|
|
5145
|
+
runs in the current search continuation, so a direct goal and its meta-called
|
|
5146
|
+
form expose the same remaining alternatives; the meta-call still establishes
|
|
5147
|
+
its own cut boundary. `once/1` keeps its first solution, and `!/0` commits
|
|
5148
|
+
within the clause that contains it. If-then-else commits to the first successful
|
|
5149
|
+
condition:
|
|
5141
5150
|
|
|
5142
5151
|
<figure>
|
|
5143
5152
|
<img src="book-assets/iso-control-board.svg" alt="A goal passes through choice and exception recovery before finite solutions enter findall, bagof, and setof collectors.">
|
|
@@ -6831,11 +6840,15 @@ When another answer exists in an interactive terminal, press `;`, Space, or
|
|
|
6831
6840
|
enumeration, `a` enumerates all remaining answers, and `f` advances to the
|
|
6832
6841
|
next five-answer boundary (5, 10, 15, ... displayed leaf answers), regardless
|
|
6833
6842
|
of how many answers were stepped through individually beforehand. `h` displays
|
|
6834
|
-
the answer-control help. Enumeration is demand-driven:
|
|
6835
|
-
found, the top level does not
|
|
6836
|
-
|
|
6837
|
-
|
|
6838
|
-
|
|
6843
|
+
the answer-control help. Enumeration of goal continuations is demand-driven:
|
|
6844
|
+
after an answer is found, the top level does not execute a later program branch
|
|
6845
|
+
or side effect merely to discover whether the current answer is the last one.
|
|
6846
|
+
The solver's uniform choicepoint protocol uses explicit clause and control
|
|
6847
|
+
frames, permits a one-answer buffer only for effect-free host relations, and
|
|
6848
|
+
requires stateful or meta-control iterators to report their pending state
|
|
6849
|
+
directly. Search that can perform an effect starts only after an answer-control
|
|
6850
|
+
command asks to continue. Stopping enumeration closes any active
|
|
6851
|
+
`call_cleanup/2` or
|
|
6839
6852
|
`setup_call_cleanup/3` protection exactly once; detecting that a choicepoint
|
|
6840
6853
|
remains does not execute that next branch. If an unresolved alternative
|
|
6841
6854
|
ultimately has no solution, asking for it may therefore finish with `false.`. In scripted non-TTY input, a new
|
|
@@ -7995,8 +8008,11 @@ decidable by structural equality.
|
|
|
7995
8008
|
**Clause.** A fact or rule terminated by a period.
|
|
7996
8009
|
|
|
7997
8010
|
**Choicepoint.** A remaining search alternative that may produce another
|
|
7998
|
-
answer if the caller asks the solver to continue.
|
|
7999
|
-
|
|
8011
|
+
answer if the caller asks the solver to continue. Every resumable engine
|
|
8012
|
+
iterator follows the same pending-alternative protocol. Ordinary effect-free
|
|
8013
|
+
host relations may buffer one answer to prove exhaustion; stateful and
|
|
8014
|
+
meta-control iterators report their state without executing an unrequested
|
|
8015
|
+
effect or program branch.
|
|
8000
8016
|
|
|
8001
8017
|
**Cleanup.** A protected finalization goal installed by normal-mode
|
|
8002
8018
|
`call_cleanup/2` or `setup_call_cleanup/3`. It runs exactly once when the
|