eyeprolog 1.3.85 → 1.3.87
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 +126 -34
- package/src/scryer-compat.js +2 -2
- package/src/solver.js +79 -66
- package/test/conformance/ISO-IMPLEMENTATION-DEFINED.md +1 -1
- package/test/run-iso-strict.mjs +2 -2
- package/test/run-regression.mjs +65 -15
- package/the-art-of-eyeprolog.md +32 -16
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 }) {
|
|
@@ -1738,15 +1756,6 @@ function writeOptionBoolean(value, env, option) {
|
|
|
1738
1756
|
return value.name === 'true';
|
|
1739
1757
|
}
|
|
1740
1758
|
|
|
1741
|
-
function writeOptionSpacing(value, env, option) {
|
|
1742
|
-
value = deref(value, env);
|
|
1743
|
-
if (value.type === VAR) throw new PrologError('instantiation_error');
|
|
1744
|
-
if (value.type !== ATOM || !['minimal', 'standard'].includes(value.name)) {
|
|
1745
|
-
throw new PrologError('domain_error(write_option)', copyResolved(option, env));
|
|
1746
|
-
}
|
|
1747
|
-
return value.name;
|
|
1748
|
-
}
|
|
1749
|
-
|
|
1750
1759
|
function writeVariableNames(value, env, option) {
|
|
1751
1760
|
value = deref(value, env);
|
|
1752
1761
|
if (value.type === VAR) throw new PrologError('instantiation_error');
|
|
@@ -1788,7 +1797,7 @@ function termWriteOptionsFromItems(options, env, mode = 'write_term', solver = n
|
|
|
1788
1797
|
else if (option.name === 'double_quotes' && !solver?.isoStrict) {
|
|
1789
1798
|
result.doubleQuotes = writeOptionBoolean(option.args[0], env, option);
|
|
1790
1799
|
} else if (option.name === 'spacing' && !solver?.isoStrict) {
|
|
1791
|
-
result.minimalOperatorSpacing =
|
|
1800
|
+
result.minimalOperatorSpacing = !writeOptionBoolean(option.args[0], env, option);
|
|
1792
1801
|
} else {
|
|
1793
1802
|
throw new PrologError('domain_error(write_option)', copyResolved(option, env));
|
|
1794
1803
|
}
|
|
@@ -2413,6 +2422,11 @@ function callable(term, env) {
|
|
|
2413
2422
|
validateControlCallable(term, term, env);
|
|
2414
2423
|
return term;
|
|
2415
2424
|
}
|
|
2425
|
+
|
|
2426
|
+
function withPendingState(iterator, state) {
|
|
2427
|
+
iterator.hasPendingAlternatives = () => state.pending;
|
|
2428
|
+
return iterator;
|
|
2429
|
+
}
|
|
2416
2430
|
function validateControlCallable(term, culprit, env) {
|
|
2417
2431
|
// Only control constructs need their nested goals validated at meta-call
|
|
2418
2432
|
// entry. Walk them iteratively and dereference each nested goal lazily so
|
|
@@ -2438,9 +2452,14 @@ function validateControlCallable(term, culprit, env) {
|
|
|
2438
2452
|
}
|
|
2439
2453
|
}
|
|
2440
2454
|
}
|
|
2441
|
-
function
|
|
2455
|
+
function expandCallGoal({ goal, env }) {
|
|
2442
2456
|
const invoked = callable(goal.args[0], env);
|
|
2443
2457
|
if (invoked.module == null) invoked.module = goal.module ?? 'user';
|
|
2458
|
+
return invoked;
|
|
2459
|
+
}
|
|
2460
|
+
function* callBuiltin(context) {
|
|
2461
|
+
const { solver, env } = context;
|
|
2462
|
+
const invoked = expandCallGoal(context);
|
|
2444
2463
|
const child = solver.cloneForInnerGoal();
|
|
2445
2464
|
try {
|
|
2446
2465
|
yield* child.solve([invoked], env, 0);
|
|
@@ -2448,7 +2467,7 @@ function* callBuiltin({ solver, goal, env }) {
|
|
|
2448
2467
|
solver.absorbStatsFrom(child);
|
|
2449
2468
|
}
|
|
2450
2469
|
}
|
|
2451
|
-
function
|
|
2470
|
+
function expandCallClosureGoal({ goal, env }) {
|
|
2452
2471
|
const closure = callable(goal.args[0], env);
|
|
2453
2472
|
const existing = closure.type === COMPOUND ? closure.args : [];
|
|
2454
2473
|
const extra = goal.args.slice(1);
|
|
@@ -2457,6 +2476,11 @@ function* callClosureBuiltin({ solver, goal, env }) {
|
|
|
2457
2476
|
}
|
|
2458
2477
|
const invoked = compound(closure.name, [...existing, ...extra]);
|
|
2459
2478
|
invoked.module = closure.module ?? goal.module ?? 'user';
|
|
2479
|
+
return invoked;
|
|
2480
|
+
}
|
|
2481
|
+
function* callClosureBuiltin(context) {
|
|
2482
|
+
const { solver, env } = context;
|
|
2483
|
+
const invoked = expandCallClosureGoal(context);
|
|
2460
2484
|
const child = solver.cloneForInnerGoal();
|
|
2461
2485
|
try {
|
|
2462
2486
|
yield* child.solve([invoked], env, 0);
|
|
@@ -2509,7 +2533,12 @@ function writeElapsedTime(solver, startedAt, inferences) {
|
|
|
2509
2533
|
);
|
|
2510
2534
|
}
|
|
2511
2535
|
|
|
2512
|
-
function
|
|
2536
|
+
function timeBuiltin(context) {
|
|
2537
|
+
const state = { pending: true };
|
|
2538
|
+
return withPendingState(timeSolutions(context, state), state);
|
|
2539
|
+
}
|
|
2540
|
+
|
|
2541
|
+
function* timeSolutions({ solver, goal, env }, state) {
|
|
2513
2542
|
const invoked = callable(goal.args[0], env);
|
|
2514
2543
|
const child = solver.cloneForInnerGoal();
|
|
2515
2544
|
let startedAt = monotonicMilliseconds();
|
|
@@ -2519,12 +2548,15 @@ function* timeBuiltin({ solver, goal, env }) {
|
|
|
2519
2548
|
for (const answerEnv of child.solve([invoked], env, 0)) {
|
|
2520
2549
|
writeElapsedTime(solver, startedAt, child.inferenceObservation.value - startedInferences);
|
|
2521
2550
|
yieldedAny = true;
|
|
2551
|
+
state.pending = child.hasPendingAlternatives();
|
|
2522
2552
|
yield answerEnv;
|
|
2553
|
+
if (!state.pending) return;
|
|
2523
2554
|
// A resumed time/1 measures only the work required to reach the next
|
|
2524
2555
|
// answer, so nondeterministic calls get one timing line per solution.
|
|
2525
2556
|
startedAt = monotonicMilliseconds();
|
|
2526
2557
|
startedInferences = child.inferenceObservation.value;
|
|
2527
2558
|
}
|
|
2559
|
+
state.pending = false;
|
|
2528
2560
|
// A call that fails without producing an answer still reports the work.
|
|
2529
2561
|
if (!yieldedAny) writeElapsedTime(solver, startedAt, child.inferenceObservation.value - startedInferences);
|
|
2530
2562
|
} finally {
|
|
@@ -2532,10 +2564,18 @@ function* timeBuiltin({ solver, goal, env }) {
|
|
|
2532
2564
|
}
|
|
2533
2565
|
}
|
|
2534
2566
|
|
|
2535
|
-
function
|
|
2567
|
+
function callNthBuiltin(context) {
|
|
2568
|
+
const state = { pending: true };
|
|
2569
|
+
return withPendingState(callNthSolutions(context, state), state);
|
|
2570
|
+
}
|
|
2571
|
+
|
|
2572
|
+
function* callNthSolutions({ solver, goal, env }, state) {
|
|
2536
2573
|
const requestedTerm = deref(goal.args[1], env);
|
|
2537
2574
|
// Zero is the one Nth value that fails before Goal is inspected.
|
|
2538
|
-
if (requestedTerm.type === NUMBER && isDecimalInteger(requestedTerm.name) && BigInt(requestedTerm.name) === 0n)
|
|
2575
|
+
if (requestedTerm.type === NUMBER && isDecimalInteger(requestedTerm.name) && BigInt(requestedTerm.name) === 0n) {
|
|
2576
|
+
state.pending = false;
|
|
2577
|
+
return;
|
|
2578
|
+
}
|
|
2539
2579
|
|
|
2540
2580
|
const invoked = callable(goal.args[0], env);
|
|
2541
2581
|
let requested = null;
|
|
@@ -2554,9 +2594,13 @@ function* callNthBuiltin({ solver, goal, env }) {
|
|
|
2554
2594
|
nth++;
|
|
2555
2595
|
if (requested != null && nth < requested) continue;
|
|
2556
2596
|
const next = answerEnv.clone();
|
|
2557
|
-
if (unify(goal.args[1], numberTerm(nth.toString()), next))
|
|
2558
|
-
|
|
2597
|
+
if (unify(goal.args[1], numberTerm(nth.toString()), next)) {
|
|
2598
|
+
state.pending = requested == null && child.hasPendingAlternatives();
|
|
2599
|
+
yield next;
|
|
2600
|
+
if (requested != null || !state.pending) return;
|
|
2601
|
+
}
|
|
2559
2602
|
}
|
|
2603
|
+
state.pending = false;
|
|
2560
2604
|
} finally {
|
|
2561
2605
|
solver.absorbStatsFrom(child);
|
|
2562
2606
|
}
|
|
@@ -2648,12 +2692,22 @@ function callResidueVarsBuiltin({ solver, goal, env }) {
|
|
|
2648
2692
|
return iterator;
|
|
2649
2693
|
}
|
|
2650
2694
|
|
|
2651
|
-
function
|
|
2695
|
+
function freezeBuiltin(context) {
|
|
2696
|
+
const state = { pending: true };
|
|
2697
|
+
return withPendingState(freezeSolutions(context, state), state);
|
|
2698
|
+
}
|
|
2699
|
+
|
|
2700
|
+
function* freezeSolutions({ solver, goal, env }, state) {
|
|
2652
2701
|
const watched = deref(goal.args[0], env);
|
|
2653
2702
|
if (watched.type !== VAR) {
|
|
2654
2703
|
const child = solver.cloneForInnerGoal();
|
|
2655
2704
|
try {
|
|
2656
|
-
|
|
2705
|
+
for (const answerEnv of child.solve([callable(goal.args[1], env)], env, 0)) {
|
|
2706
|
+
state.pending = child.hasPendingAlternatives();
|
|
2707
|
+
yield answerEnv;
|
|
2708
|
+
if (!state.pending) return;
|
|
2709
|
+
}
|
|
2710
|
+
state.pending = false;
|
|
2657
2711
|
} finally {
|
|
2658
2712
|
solver.absorbStatsFrom(child);
|
|
2659
2713
|
}
|
|
@@ -2661,10 +2715,16 @@ function* freezeBuiltin({ solver, goal, env }) {
|
|
|
2661
2715
|
}
|
|
2662
2716
|
const next = env.clone();
|
|
2663
2717
|
next.delay(watched.name, goal.args[1], goal.module ?? 'user');
|
|
2718
|
+
state.pending = false;
|
|
2664
2719
|
yield next;
|
|
2665
2720
|
}
|
|
2666
2721
|
|
|
2667
|
-
function
|
|
2722
|
+
function phraseBuiltin(context) {
|
|
2723
|
+
const state = { pending: true };
|
|
2724
|
+
return withPendingState(phraseSolutions(context, state), state);
|
|
2725
|
+
}
|
|
2726
|
+
|
|
2727
|
+
function* phraseSolutions({ solver, goal, env }, state) {
|
|
2668
2728
|
const grammarBody0 = deref(goal.args[0], env);
|
|
2669
2729
|
if (grammarBody0.type === VAR) throw new PrologError('instantiation_error');
|
|
2670
2730
|
if (grammarBody0.type !== ATOM && grammarBody0.type !== COMPOUND) {
|
|
@@ -2721,7 +2781,12 @@ function* phraseBuiltin({ solver, goal, env }) {
|
|
|
2721
2781
|
skipListTailTabling: !repeatedInvocation,
|
|
2722
2782
|
});
|
|
2723
2783
|
try {
|
|
2724
|
-
|
|
2784
|
+
for (const answerEnv of child.solve(finish == null ? [expanded] : [expanded, finish], env, 0)) {
|
|
2785
|
+
state.pending = child.hasPendingAlternatives();
|
|
2786
|
+
yield answerEnv;
|
|
2787
|
+
if (!state.pending) return;
|
|
2788
|
+
}
|
|
2789
|
+
state.pending = false;
|
|
2725
2790
|
} finally {
|
|
2726
2791
|
solver.absorbStatsFrom(child);
|
|
2727
2792
|
solver.trimInnerTableScope('phrase');
|
|
@@ -2781,7 +2846,12 @@ function prologErrorBall(error) {
|
|
|
2781
2846
|
if (hasDefaultGroundErrorShape(error) || termIsGround(term)) return term;
|
|
2782
2847
|
return freshCopy(term, new Env());
|
|
2783
2848
|
}
|
|
2784
|
-
function
|
|
2849
|
+
function catchBuiltin(context) {
|
|
2850
|
+
const state = { pending: true };
|
|
2851
|
+
return withPendingState(catchSolutions(context, state), state);
|
|
2852
|
+
}
|
|
2853
|
+
|
|
2854
|
+
function* catchSolutions({ solver, goal, env }, state) {
|
|
2785
2855
|
let child = null;
|
|
2786
2856
|
try {
|
|
2787
2857
|
// Corrigendum 2 removed catch/3's own callability errors so that errors
|
|
@@ -2798,12 +2868,19 @@ function* catchBuiltin({ solver, goal, env }) {
|
|
|
2798
2868
|
if (result.done) solver.stats.deterministic_builtin_failures++;
|
|
2799
2869
|
else {
|
|
2800
2870
|
solver.stats.deterministic_builtin_successes++;
|
|
2871
|
+
state.pending = false;
|
|
2801
2872
|
yield result.value;
|
|
2802
2873
|
}
|
|
2874
|
+
state.pending = false;
|
|
2803
2875
|
return;
|
|
2804
2876
|
}
|
|
2805
2877
|
child = solver.cloneForInnerGoal();
|
|
2806
|
-
|
|
2878
|
+
for (const answerEnv of child.solve([invoked], env.clone(), 0)) {
|
|
2879
|
+
state.pending = child.hasPendingAlternatives();
|
|
2880
|
+
yield answerEnv;
|
|
2881
|
+
if (!state.pending) return;
|
|
2882
|
+
}
|
|
2883
|
+
state.pending = false;
|
|
2807
2884
|
} catch (error) {
|
|
2808
2885
|
const ball = error instanceof ThrownTerm
|
|
2809
2886
|
? error.term
|
|
@@ -2814,12 +2891,18 @@ function* catchBuiltin({ solver, goal, env }) {
|
|
|
2814
2891
|
const recovered = env.clone();
|
|
2815
2892
|
if (!unify(goal.args[1], ball, recovered)) throw error;
|
|
2816
2893
|
const recovery = callable(goal.args[2], recovered);
|
|
2817
|
-
if (recovery.type === ATOM && (recovery.name === 'false' || recovery.name === 'fail'))
|
|
2894
|
+
if (recovery.type === ATOM && (recovery.name === 'false' || recovery.name === 'fail')) {
|
|
2895
|
+
state.pending = false;
|
|
2896
|
+
return;
|
|
2897
|
+
}
|
|
2818
2898
|
if (recovery.type === ATOM && recovery.name === 'true') {
|
|
2899
|
+
state.pending = false;
|
|
2819
2900
|
yield recovered;
|
|
2820
2901
|
return;
|
|
2821
2902
|
}
|
|
2822
|
-
yield* solver
|
|
2903
|
+
yield* solveControlBranch(solver, recovery, recovered,
|
|
2904
|
+
(pending) => { state.pending = pending; });
|
|
2905
|
+
state.pending = false;
|
|
2823
2906
|
} finally {
|
|
2824
2907
|
if (child != null) solver.absorbStatsFrom(child);
|
|
2825
2908
|
}
|
|
@@ -2917,11 +3000,19 @@ function* disjunctionSolutions({ solver, goal, env }, state) {
|
|
|
2917
3000
|
(pending) => { state.pending = pending; });
|
|
2918
3001
|
state.pending = false;
|
|
2919
3002
|
}
|
|
2920
|
-
function
|
|
3003
|
+
function ifThenBuiltin(context) {
|
|
3004
|
+
const state = { pending: true };
|
|
3005
|
+
return withPendingState(ifThenSolutions(context, state), state);
|
|
3006
|
+
}
|
|
3007
|
+
|
|
3008
|
+
function* ifThenSolutions({ solver, goal, env }, state) {
|
|
2921
3009
|
for (const conditionEnv of solver.cloneForInnerGoal(1).solve([callable(goal.args[0], env)], env.clone(), 0)) {
|
|
2922
|
-
yield* solveControlBranch(solver, goal.args[1], conditionEnv
|
|
3010
|
+
yield* solveControlBranch(solver, goal.args[1], conditionEnv,
|
|
3011
|
+
(pending) => { state.pending = pending; });
|
|
3012
|
+
state.pending = false;
|
|
2923
3013
|
return;
|
|
2924
3014
|
}
|
|
3015
|
+
state.pending = false;
|
|
2925
3016
|
}
|
|
2926
3017
|
|
|
2927
3018
|
export { arithmeticValueTerm, evaluateArithmetic, compareArithmeticValues } from './iso-arithmetic.js';
|
|
@@ -2940,6 +3031,7 @@ export class BuiltinRegistry {
|
|
|
2940
3031
|
handler,
|
|
2941
3032
|
deterministic: options.deterministic ?? false,
|
|
2942
3033
|
deterministicWhen: options.deterministicWhen ?? null,
|
|
3034
|
+
expandGoal: options.expandGoal ?? null,
|
|
2943
3035
|
ready: options.ready ?? null,
|
|
2944
3036
|
fallbackWhenNotReady: options.fallbackWhenNotReady ?? false,
|
|
2945
3037
|
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.
|
|
@@ -124,7 +124,7 @@ families; `--iso-strict` is intended to remove their Part 1 interpretation.
|
|
|
124
124
|
| 5.5.9 Built-in predicates | EyeProlog libraries, CLP(Z), statistics, Part 3 `phrase/2-3`, and interop autoloaded predicates | Strict registry contains only the Part 1 + Corrigenda core registry. |
|
|
125
125
|
| 5.5.10 Evaluable functors | Normal mode additionally accepts the EyeProlog evaluable atom `e`; the remaining arithmetic functors accepted by strict mode are the Part 1 + Corrigenda set. | Strict mode rejects `e/0` as non-evaluable and retains the Corrigendum arithmetic additions. **covered** — strict extension-boundary regression plus `src/iso-arithmetic.js`. |
|
|
126
126
|
| 5.5.11 Reserved atoms | None | None. |
|
|
127
|
-
| Cor.3 5.5.12 Options | Extra library/host options may exist outside core option lists | Normal mode additionally accepts the EyeProlog `write_term/2-3` options `double_quotes(true|false)` and `spacing(
|
|
127
|
+
| Cor.3 5.5.12 Options | Extra library/host options may exist outside core option lists | Normal mode additionally accepts the EyeProlog `write_term/2-3` options `double_quotes(true|false)` and `spacing(true|false)`. `spacing(false)` emits only lexically required separators; `spacing(true)` adds conventional layout around operators. Strict core excludes these implementation-specific extensions and accepts only the Part 1 plus Corrigendum 3 write-option surface; unknown extension options raise `domain_error(write_option,...)`. |
|
|
128
128
|
|
|
129
129
|
Normal mode provides documented module and DCG compatibility profiles whose
|
|
130
130
|
features overlap standardized Part 2 and Part 3 facilities. They are extensions
|
package/test/run-iso-strict.mjs
CHANGED
|
@@ -285,13 +285,13 @@ export function runIsoStrict(reporter = new TestReporter()) {
|
|
|
285
285
|
|
|
286
286
|
const strictSpacingError = capture(() => run('', {
|
|
287
287
|
isoStrict: true,
|
|
288
|
-
goal: 'write_term(1+1,[spacing(
|
|
288
|
+
goal: 'write_term(1+1,[spacing(false)])',
|
|
289
289
|
}));
|
|
290
290
|
equal(strictSpacingError.formal, 'domain_error(write_option)', 'strict spacing extension rejection');
|
|
291
291
|
|
|
292
292
|
const normal = run('', { goal: 'write_term("ab",[double_quotes(true)])' });
|
|
293
293
|
includes(normal.stdout, '"ab"', 'normal-profile extension remains available');
|
|
294
|
-
includes(run('', { goal: 'write_term(1+1,[spacing(
|
|
294
|
+
includes(run('', { goal: 'write_term(1+1,[spacing(true)])' }).stdout,
|
|
295
295
|
'1 + 1', 'normal-profile spacing extension remains available');
|
|
296
296
|
});
|
|
297
297
|
|
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
|
{
|
|
@@ -4032,8 +4078,8 @@ child.stdin.write(\`consult(${consultedAtom}).\\n\`);
|
|
|
4032
4078
|
run: () => {
|
|
4033
4079
|
const source = [
|
|
4034
4080
|
'emit :-',
|
|
4035
|
-
' write_term(1+1, [spacing(
|
|
4036
|
-
' write_term(1+1, [spacing(
|
|
4081
|
+
' write_term(1+1, [spacing(false)]), put_char(\'|\'),',
|
|
4082
|
+
' write_term(1+1, [spacing(true)]).',
|
|
4037
4083
|
'',
|
|
4038
4084
|
].join('\n');
|
|
4039
4085
|
assertEqual(run(source, { goal: 'emit' }).stdout, '1+1|1 + 1emit.\n',
|
|
@@ -4058,7 +4104,7 @@ child.stdin.write(\`consult(${consultedAtom}).\\n\`);
|
|
|
4058
4104
|
}
|
|
4059
4105
|
assertEqual(variableError?.formal, 'instantiation_error', 'variable spacing value');
|
|
4060
4106
|
|
|
4061
|
-
for (const value of ['
|
|
4107
|
+
for (const value of ['minimal', 'standard', '1', '0']) {
|
|
4062
4108
|
let domainError = null;
|
|
4063
4109
|
try {
|
|
4064
4110
|
run('', { goal: `write_term(1+1,[spacing(${value})])` });
|
|
@@ -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.">
|
|
@@ -5430,11 +5439,11 @@ still retained where adjacent graphic tokens would otherwise merge, as in
|
|
|
5430
5439
|
`double_quotes(true|false)`: `true` lets eligible character/code lists use the
|
|
5431
5440
|
current `double_quotes` representation, while strict ISO mode rejects this
|
|
5432
5441
|
implementation-specific write option. Normal mode also accepts the
|
|
5433
|
-
implementation-specific `spacing(
|
|
5434
|
-
only separators required to avoid lexical ambiguity, while `
|
|
5442
|
+
implementation-specific boolean `spacing(true|false)` option: `false` emits
|
|
5443
|
+
only separators required to avoid lexical ambiguity, while `true` adds
|
|
5435
5444
|
conventional layout around operators. For example,
|
|
5436
|
-
`write_term(1+1,[spacing(
|
|
5437
|
-
`write_term(1+1,[spacing(
|
|
5445
|
+
`write_term(1+1,[spacing(false)])` emits `1+1` and
|
|
5446
|
+
`write_term(1+1,[spacing(true)])` emits `1 + 1`. The REPL always follows
|
|
5438
5447
|
the minimal-separator rule, so `X=1*1` is displayed as `X = 1*1`, while a
|
|
5439
5448
|
separator is retained in `X = a+ -b` because the adjacent graphic tokens would
|
|
5440
5449
|
otherwise merge. Strict ISO mode rejects both extension options.
|
|
@@ -6130,7 +6139,7 @@ the beginning. A peek does not mark the stream as past-end.
|
|
|
6130
6139
|
| `write(+Term)`, `write(+Stream,+Term)` | Writes readable operator notation without quoting atoms merely because quoting would be required for reparsing. Number variables are enabled. |
|
|
6131
6140
|
| `writeq(+Term)`, `writeq(+Stream,+Term)` | Like `write`, but quotes atoms when required for unambiguous input syntax. |
|
|
6132
6141
|
| `write_canonical(+Term)`, `write_canonical(+Stream,+Term)` | Writes quoted canonical functor notation while ignoring operators and without interpreting *$VAR/1*. |
|
|
6133
|
-
| `write_term(+Term,+Options)`, `write_term(+Stream,+Term,+Options)` | Writes with *quoted(true or false)*, *ignore_ops(true or false)*, *numbervars(true or false)*, and *variable_names([Name=Variable,...])*. Normal mode also supports *double_quotes(true or false)* and *spacing(
|
|
6142
|
+
| `write_term(+Term,+Options)`, `write_term(+Stream,+Term,+Options)` | Writes with *quoted(true or false)*, *ignore_ops(true or false)*, *numbervars(true or false)*, and *variable_names([Name=Variable,...])*. Normal mode also supports *double_quotes(true or false)* and *spacing(true or false)*. |
|
|
6134
6143
|
|
|
6135
6144
|
Term input uses the program's current operator table and the same ISO quoted-character
|
|
6136
6145
|
syntax as source text, including backslash-terminated octal and hexadecimal
|
|
@@ -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
|
|
@@ -7783,7 +7796,7 @@ Notable implementation boundaries are:
|
|
|
7783
7796
|
- `write_term/2-3` implements the Part 1 plus Corrigendum 3 `quoted/1`,
|
|
7784
7797
|
`ignore_ops/1`, `numbervars/1`, and `variable_names/1` option surface,
|
|
7785
7798
|
including option validation and traversal rules; normal mode also offers
|
|
7786
|
-
`double_quotes(true|false)` and `spacing(
|
|
7799
|
+
`double_quotes(true|false)` and `spacing(true|false)` as explicitly
|
|
7787
7800
|
implementation-specific extensions, which strict mode rejects;
|
|
7788
7801
|
- unification consistently performs an occurs check, rejecting rational-tree
|
|
7789
7802
|
bindings accepted as extensions by some systems.
|
|
@@ -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
|