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 CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.3.85",
6
+ "version": "1.3.87",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
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++) registry.add('call', arity, callClosureBuiltin);
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* retractBuiltin({ solver, goal, env }) {
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) return;
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 candidates = [...group.clauses];
727
- for (const clause of candidates) {
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 = writeOptionSpacing(option.args[0], env, option) === 'minimal';
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* callBuiltin({ solver, goal, env }) {
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* callClosureBuiltin({ solver, goal, env }) {
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* timeBuiltin({ solver, goal, env }) {
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* callNthBuiltin({ solver, goal, env }) {
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) return;
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)) yield next;
2558
- if (requested != null) return;
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* freezeBuiltin({ solver, goal, env }) {
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
- yield* child.solve([callable(goal.args[1], env)], env, 0);
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* phraseBuiltin({ solver, goal, env }) {
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
- yield* child.solve(finish == null ? [expanded] : [expanded, finish], env, 0);
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* catchBuiltin({ solver, goal, env }) {
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
- yield* child.solve([invoked], env.clone(), 0);
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')) return;
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.solve([recovery], recovered, 0);
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* ifThenBuiltin({ solver, goal, env }) {
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,
@@ -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
- if (def && builtinIsReadyOrAuthoritative(def, this, goal, env)) {
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
- const state = { pending: true };
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, state) {
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
- const state = { pending: true };
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, state) {
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
- const iterator = bundledLengthSolutions(solver, goal, env);
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.push({
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 !== 'function' || predicate.call(iterator) !== false;
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(minimal|standard)`. `minimal` emits only lexically required separators; `standard` 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,...)`. |
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
@@ -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(minimal)])',
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(standard)])' }).stdout,
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
 
@@ -1999,11 +1999,13 @@ c4 ?- call((!;1)).
1999
1999
  },
2000
2000
  },
2001
2001
  {
2002
- name: 'predicates discard exhausted choicepoints before the REPL observes them (issue #74)',
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(minimal)]), put_char(\'|\'),',
4036
- ' write_term(1+1, [spacing(standard)]).',
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 ['true', 'false', '1', '0']) {
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
- // Exercise EyeProlog's recovery window at a stable threshold instead
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 solver.solve([reported], new Env(), 0)) {
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');
@@ -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. A mode-sensitive extension can additionally provide `ready`,
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, `once/1` keeps its first solution, and `!/0` commits within the clause
5140
- that contains it. If-then-else commits to the first successful condition:
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(minimal|standard)` option: `minimal` emits
5434
- only separators required to avoid lexical ambiguity, while `standard` adds
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(minimal)])` emits `1+1` and
5437
- `write_term(1+1,[spacing(standard)])` emits `1 + 1`. The REPL always follows
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(minimal or standard)*. |
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: after an answer is
6835
- found, the top level does not pull a successor merely to discover whether the
6836
- current answer is the last one. Search for a later answer, including any side
6837
- effects reached on that path, starts only after an answer-control command asks
6838
- to continue. Stopping enumeration closes any active `call_cleanup/2` or
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(minimal|standard)` as explicitly
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. EyeProlog's top level reports
7999
- choicepoint availability without speculatively executing the next alternative.
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