eyeprolog 1.3.90 → 1.3.92

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/index.d.ts CHANGED
@@ -182,7 +182,12 @@ export interface BuiltinDefinition {
182
182
  eyePrologLibrary: boolean;
183
183
  }
184
184
 
185
- export type BuiltinHandler = (context: { solver: Solver; goal: EyePrologTerm; env: Env }) => Iterable<Env>;
185
+ export interface BuiltinIterator extends IterableIterator<Env> {
186
+ hasPendingAlternatives?: () => boolean;
187
+ }
188
+
189
+ export type BuiltinHandler =
190
+ (context: { solver: Solver; goal: EyePrologTerm; env: Env }) => BuiltinIterator;
186
191
 
187
192
  export class BuiltinRegistry {
188
193
  constructor();
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.3.90",
6
+ "version": "1.3.92",
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
@@ -224,12 +224,40 @@ function difConstraint(left, right) {
224
224
  })) return true;
225
225
  return identical(left, right, probe);
226
226
  },
227
- residualGoal() {
228
- return compound('dif', [left, right]);
227
+ residualGoal(env) {
228
+ const [projectedLeft, projectedRight] = projectDifSubterms(left, right, env);
229
+ return compound('dif', [projectedLeft, projectedRight]);
229
230
  },
230
231
  });
231
232
  }
232
233
 
234
+ function projectDifSubterms(left, right, env) {
235
+ left = deref(left, env);
236
+ right = deref(right, env);
237
+ while (left.type === COMPOUND && right.type === COMPOUND &&
238
+ left.name === right.name && left.arity === right.arity) {
239
+ let projectedIndex = -1;
240
+ for (let index = 0; index < left.arity; index++) {
241
+ if (identical(left.args[index], right.args[index], env)) continue;
242
+ const probe = env.clone();
243
+ if (unify(left.args[index], right.args[index], probe, {
244
+ skipVariableConstraints: true,
245
+ skipAttributeHooks: true,
246
+ }) && identical(left, right, probe)) {
247
+ projectedIndex = index;
248
+ break;
249
+ }
250
+ }
251
+ // If no single aligned subterm equality entails equality of the complete
252
+ // compounds, their disequality is a genuine disjunction. Keep it whole
253
+ // instead of inventing auxiliary terms or separate stronger constraints.
254
+ if (projectedIndex === -1) return [left, right];
255
+ left = deref(left.args[projectedIndex], env);
256
+ right = deref(right.args[projectedIndex], env);
257
+ }
258
+ return [left, right];
259
+ }
260
+
233
261
  function* difBuiltin({ goal, env }) {
234
262
  const left = goal.args[0];
235
263
  const right = goal.args[1];
@@ -599,16 +627,30 @@ function validPredicateIndicator(term) {
599
627
  (term.args[1].type === NUMBER && isDecimalInteger(term.args[1].name) && BigInt(term.args[1].name) >= 0n));
600
628
  }
601
629
 
602
- function* currentPredicateBuiltin({ solver, goal, env }) {
630
+ function currentPredicateBuiltin(context) {
631
+ const state = { pending: false };
632
+ return withPendingState(currentPredicateSolutions(context, state), state);
633
+ }
634
+
635
+ function* currentPredicateSolutions({ solver, goal, env }, state) {
603
636
  const indicator = copyResolved(goal.args[0], env);
604
637
  if (indicator.type !== VAR && !validPredicateIndicator(indicator)) {
605
638
  throw new PrologError('type_error(predicate_indicator)', indicator);
606
639
  }
607
- for (const group of solver.program.groups.values()) {
640
+ const groups = [...solver.program.groups.values()].filter((group) =>
641
+ indicator.type === VAR ||
642
+ (indicator.args[0].type === VAR || indicator.args[0].name === group.name) &&
643
+ (indicator.args[1].type === VAR || BigInt(indicator.args[1].name) === BigInt(group.arity)));
644
+ for (let index = 0; index < groups.length; index++) {
645
+ const group = groups[index];
608
646
  const next = env.clone();
609
647
  const candidate = compound('/', [atom(group.name), numberTerm(group.arity)]);
610
- if (unify(goal.args[0], candidate, next)) yield next;
648
+ if (unify(goal.args[0], candidate, next)) {
649
+ state.pending = index + 1 < groups.length;
650
+ yield next;
651
+ }
611
652
  }
653
+ state.pending = false;
612
654
  }
613
655
 
614
656
  function callableOrVariable(term, env) {
@@ -624,7 +666,12 @@ function clauseBodyTerm(body) {
624
666
  return result;
625
667
  }
626
668
 
627
- function* clauseBuiltin({ solver, goal, env }) {
669
+ function clauseBuiltin(context) {
670
+ const state = { pending: false };
671
+ return withPendingState(clauseSolutions(context, state), state);
672
+ }
673
+
674
+ function* clauseSolutions({ solver, goal, env }, state) {
628
675
  const head = deref(goal.args[0], env);
629
676
  if (head.type === VAR) throw new PrologError('instantiation_error');
630
677
  if (head.type !== ATOM && head.type !== COMPOUND) throw new PrologError('type_error(callable)', head);
@@ -639,12 +686,17 @@ function* clauseBuiltin({ solver, goal, env }) {
639
686
  }
640
687
  callableOrVariable(goal.args[1], env);
641
688
  if (!group) return;
642
- for (const clause of group.clauses) {
689
+ for (let index = 0; index < group.clauses.length; index++) {
690
+ const clause = group.clauses[index];
643
691
  const pair = compound('$clause', [clause.head, clauseBodyTerm(clause.body)]);
644
692
  const copied = freshCopy(pair, new Env());
645
693
  const next = env.clone();
646
- if (unify(goal.args[0], copied.args[0], next) && unify(goal.args[1], copied.args[1], next)) yield next;
694
+ if (unify(goal.args[0], copied.args[0], next) && unify(goal.args[1], copied.args[1], next)) {
695
+ state.pending = index + 1 < group.clauses.length;
696
+ yield next;
697
+ }
647
698
  }
699
+ state.pending = false;
648
700
  }
649
701
 
650
702
  function clauseParts(term, env) {
@@ -817,21 +869,32 @@ function* abolishBuiltin({ solver, goal, env }) {
817
869
  yield env;
818
870
  }
819
871
 
820
- function* currentPrologFlagBuiltin({ solver, goal, env }) {
872
+ function currentPrologFlagBuiltin(context) {
873
+ const state = { pending: false };
874
+ return withPendingState(currentPrologFlagSolutions(context, state), state);
875
+ }
876
+
877
+ function* currentPrologFlagSolutions({ solver, goal, env }, state) {
821
878
  const flag = deref(goal.args[0], env);
822
879
  if (flag.type !== VAR && flag.type !== ATOM) throw new PrologError('type_error(atom)', flag);
823
880
  if (flag.type === ATOM && !solver.prologFlags.has(flag.name)) {
824
881
  throw new PrologError('domain_error(prolog_flag)', flag);
825
882
  }
826
- for (const [name, definition] of solver.prologFlags) {
883
+ const definitions = [...solver.prologFlags]
884
+ .filter(([name, definition]) => definition.value != null && (flag.type === VAR || flag.name === name));
885
+ for (let index = 0; index < definitions.length; index++) {
886
+ const [name, definition] = definitions[index];
827
887
  // ISO 7.11.1.1: when bounded=false, max_integer and min_integer have no
828
888
  // current value and current_prolog_flag/2 must therefore not enumerate
829
889
  // them. The definitions remain registered so attempts to change these
830
890
  // non-changeable flags still receive the normal flag error handling.
831
- if (definition.value == null) continue;
832
891
  const next = env.clone();
833
- if (unify(goal.args[0], atom(name), next) && unify(goal.args[1], definition.value, next)) yield next;
892
+ if (unify(goal.args[0], atom(name), next) && unify(goal.args[1], definition.value, next)) {
893
+ state.pending = index + 1 < definitions.length;
894
+ yield next;
895
+ }
834
896
  }
897
+ state.pending = false;
835
898
  }
836
899
 
837
900
  function* setPrologFlagBuiltin({ solver, goal, env }) {
@@ -918,7 +981,12 @@ function* opBuiltin({ solver, goal, env }) {
918
981
  yield env;
919
982
  }
920
983
 
921
- function* currentOpBuiltin({ solver, goal, env }) {
984
+ function currentOpBuiltin(context) {
985
+ const state = { pending: false };
986
+ return withPendingState(currentOpSolutions(context, state), state);
987
+ }
988
+
989
+ function* currentOpSolutions({ solver, goal, env }, state) {
922
990
  const priority = deref(goal.args[0], env);
923
991
  const specifier = deref(goal.args[1], env);
924
992
  const name = deref(goal.args[2], env);
@@ -934,12 +1002,21 @@ function* currentOpBuiltin({ solver, goal, env }) {
934
1002
  throw new PrologError('domain_error(operator_specifier)', specifier);
935
1003
  }
936
1004
  if (name.type !== VAR && name.type !== ATOM) throw new PrologError('type_error(atom)', name);
937
- for (const definition of solver.program.operators.values()) {
1005
+ const definitions = [...solver.program.operators.values()].filter((definition) =>
1006
+ (priority.type === VAR || BigInt(priority.name) === BigInt(definition.priority)) &&
1007
+ (specifier.type === VAR || specifier.name === definition.specifier) &&
1008
+ (name.type === VAR || name.name === definition.name));
1009
+ for (let index = 0; index < definitions.length; index++) {
1010
+ const definition = definitions[index];
938
1011
  const next = env.clone();
939
1012
  if (unify(goal.args[0], numberTerm(definition.priority), next) &&
940
1013
  unify(goal.args[1], atom(definition.specifier), next) &&
941
- unify(goal.args[2], atom(definition.name), next)) yield next;
1014
+ unify(goal.args[2], atom(definition.name), next)) {
1015
+ state.pending = index + 1 < definitions.length;
1016
+ yield next;
1017
+ }
942
1018
  }
1019
+ state.pending = false;
943
1020
  }
944
1021
 
945
1022
  function conversionCharacter(term, env, current = false, solver = null) {
@@ -970,13 +1047,25 @@ function* charConversionBuiltin({ solver, goal, env }) {
970
1047
  else solver.charConversions.set(input.name, output.name);
971
1048
  yield env;
972
1049
  }
973
- function* currentCharConversionBuiltin({ solver, goal, env }) {
1050
+ function currentCharConversionBuiltin(context) {
1051
+ const state = { pending: false };
1052
+ return withPendingState(currentCharConversionSolutions(context, state), state);
1053
+ }
1054
+
1055
+ function* currentCharConversionSolutions({ solver, goal, env }, state) {
974
1056
  const input = conversionCharacter(goal.args[0], env, true, solver);
975
1057
  const output = conversionCharacter(goal.args[1], env, true, solver);
976
- for (const [from, to] of [...solver.charConversions]) {
1058
+ const conversions = [...solver.charConversions].filter(([from, to]) =>
1059
+ (input.type === VAR || input.name === from) && (output.type === VAR || output.name === to));
1060
+ for (let index = 0; index < conversions.length; index++) {
1061
+ const [from, to] = conversions[index];
977
1062
  const next = env.clone();
978
- if (unify(input, atom(from), next) && unify(output, atom(to), next)) yield next;
1063
+ if (unify(input, atom(from), next) && unify(output, atom(to), next)) {
1064
+ state.pending = index + 1 < conversions.length;
1065
+ yield next;
1066
+ }
979
1067
  }
1068
+ state.pending = false;
980
1069
  }
981
1070
  function* haltBuiltin({ goal, env }) {
982
1071
  const code = goal.arity === 0 ? 0n : requireInteger(goal.args[0], env);
@@ -1265,7 +1354,12 @@ function isStreamPropertyPattern(value) {
1265
1354
  ].includes(value.name);
1266
1355
  }
1267
1356
 
1268
- function* streamPropertyBuiltin({ solver, goal, env }) {
1357
+ function streamPropertyBuiltin(context) {
1358
+ const state = { pending: false };
1359
+ return withPendingState(streamPropertySolutions(context, state), state);
1360
+ }
1361
+
1362
+ function* streamPropertySolutions({ solver, goal, env }, state) {
1269
1363
  const reference = deref(goal.args[0], env);
1270
1364
  const propertyPattern = deref(goal.args[1], env);
1271
1365
  let fixedStreamId = null;
@@ -1283,12 +1377,24 @@ function* streamPropertyBuiltin({ solver, goal, env }) {
1283
1377
  const streams = reference.type === VAR
1284
1378
  ? [...solver.io.streams.values()]
1285
1379
  : [solver.io.resolve(fixedStreamId)].filter(Boolean);
1380
+ const candidates = [];
1286
1381
  for (const stream of streams) {
1287
1382
  for (const property of streamProperties(stream)) {
1288
- const next = env.clone();
1289
- if (unify(goal.args[0], streamHandle(stream.id), next) && unify(goal.args[1], property, next)) yield next;
1383
+ if (propertyPattern.type === VAR ||
1384
+ (property.type === propertyPattern.type && property.name === propertyPattern.name)) {
1385
+ candidates.push([stream, property]);
1386
+ }
1290
1387
  }
1291
1388
  }
1389
+ for (let index = 0; index < candidates.length; index++) {
1390
+ const [stream, property] = candidates[index];
1391
+ const next = env.clone();
1392
+ if (unify(goal.args[0], streamHandle(stream.id), next) && unify(goal.args[1], property, next)) {
1393
+ state.pending = index + 1 < candidates.length;
1394
+ yield next;
1395
+ }
1396
+ }
1397
+ state.pending = false;
1292
1398
  }
1293
1399
 
1294
1400
  function inputStreamFor(solver, goal, env) {
@@ -1891,7 +1997,12 @@ function* atomLengthBuiltin({ goal, env }) {
1891
1997
  if (unify(goal.args[1], numberTerm(characters(value.name).length), next)) yield next;
1892
1998
  }
1893
1999
 
1894
- function* atomConcatBuiltin({ goal, env }) {
2000
+ function atomConcatBuiltin(context) {
2001
+ const state = { pending: false };
2002
+ return withPendingState(atomConcatSolutions(context, state), state);
2003
+ }
2004
+
2005
+ function* atomConcatSolutions({ goal, env }, state) {
1895
2006
  const first = deref(goal.args[0], env);
1896
2007
  const second = deref(goal.args[1], env);
1897
2008
  const whole = deref(goal.args[2], env);
@@ -1914,11 +2025,16 @@ function* atomConcatBuiltin({ goal, env }) {
1914
2025
  } else if (second.type === ATOM && whole.type === ATOM && whole.name.endsWith(second.name)) {
1915
2026
  candidates.push([whole.name.slice(0, whole.name.length - second.name.length), second.name, whole.name]);
1916
2027
  }
1917
- for (const [a, b, c] of candidates) {
2028
+ for (let index = 0; index < candidates.length; index++) {
2029
+ const [a, b, c] = candidates[index];
1918
2030
  const next = env.clone();
1919
2031
  if (unify(goal.args[0], atom(a), next) && unify(goal.args[1], atom(b), next) &&
1920
- unify(goal.args[2], atom(c), next)) yield next;
2032
+ unify(goal.args[2], atom(c), next)) {
2033
+ state.pending = index + 1 < candidates.length;
2034
+ yield next;
2035
+ }
1921
2036
  }
2037
+ state.pending = false;
1922
2038
  }
1923
2039
 
1924
2040
  function optionalInteger(term, env) {
@@ -1928,7 +2044,12 @@ function optionalInteger(term, env) {
1928
2044
  return BigInt(value.name);
1929
2045
  }
1930
2046
 
1931
- function* subAtomBuiltin({ goal, env }) {
2047
+ function subAtomBuiltin(context) {
2048
+ const state = { pending: false };
2049
+ return withPendingState(subAtomSolutions(context, state), state);
2050
+ }
2051
+
2052
+ function* subAtomSolutions({ goal, env }, state) {
1932
2053
  const source = deref(goal.args[0], env);
1933
2054
  if (source.type === VAR) throw new PrologError('instantiation_error');
1934
2055
  if (source.type !== ATOM) throw new PrologError('type_error(atom)', source);
@@ -1949,11 +2070,34 @@ function* subAtomBuiltin({ goal, env }) {
1949
2070
  throw new PrologError('domain_error(not_less_than_zero)', deref(goal.args[3], env));
1950
2071
  }
1951
2072
  const chars = characters(source.name);
1952
- for (let start = 0; start <= chars.length; start++) {
1953
- if (before != null && before !== BigInt(start)) continue;
1954
- for (let size = 0; size <= chars.length - start; size++) {
2073
+ let starts;
2074
+ if (before != null) {
2075
+ starts = before <= BigInt(chars.length) ? [Number(before)] : [];
2076
+ } else if (length != null && after != null) {
2077
+ const onlyStart = BigInt(chars.length) - length - after;
2078
+ starts = onlyStart >= 0n ? [Number(onlyStart)] : [];
2079
+ } else {
2080
+ const maximumStart = length != null
2081
+ ? BigInt(chars.length) - length
2082
+ : after != null ? BigInt(chars.length) - after : BigInt(chars.length);
2083
+ starts = maximumStart < 0n
2084
+ ? []
2085
+ : Array.from({ length: Number(maximumStart) + 1 }, (_, index) => index);
2086
+ }
2087
+ for (let startIndex = 0; startIndex < starts.length; startIndex++) {
2088
+ const start = starts[startIndex];
2089
+ let sizes;
2090
+ if (length != null) {
2091
+ sizes = length <= BigInt(chars.length - start) ? [Number(length)] : [];
2092
+ } else if (after != null) {
2093
+ const onlySize = BigInt(chars.length - start) - after;
2094
+ sizes = onlySize >= 0n ? [Number(onlySize)] : [];
2095
+ } else {
2096
+ sizes = Array.from({ length: chars.length - start + 1 }, (_, index) => index);
2097
+ }
2098
+ for (let sizeIndex = 0; sizeIndex < sizes.length; sizeIndex++) {
2099
+ const size = sizes[sizeIndex];
1955
2100
  const remaining = chars.length - start - size;
1956
- if (length != null && length !== BigInt(size)) continue;
1957
2101
  if (after != null && after !== BigInt(remaining)) continue;
1958
2102
  const text = chars.slice(start, start + size).join('');
1959
2103
  if (sub.type === ATOM && sub.name !== text) continue;
@@ -1961,9 +2105,13 @@ function* subAtomBuiltin({ goal, env }) {
1961
2105
  if (unify(goal.args[1], numberTerm(start), next) &&
1962
2106
  unify(goal.args[2], numberTerm(size), next) &&
1963
2107
  unify(goal.args[3], numberTerm(remaining), next) &&
1964
- unify(goal.args[4], atom(text), next)) yield next;
2108
+ unify(goal.args[4], atom(text), next)) {
2109
+ state.pending = sizeIndex + 1 < sizes.length || startIndex + 1 < starts.length;
2110
+ yield next;
2111
+ }
1965
2112
  }
1966
2113
  }
2114
+ state.pending = false;
1967
2115
  }
1968
2116
 
1969
2117
  function listElements(term, env) {
@@ -2378,42 +2526,52 @@ function sortedUnique(items) {
2378
2526
  }
2379
2527
 
2380
2528
  function allSolutionsBuiltin(asSet) {
2381
- return function* ({ solver, goal, env }) {
2382
- // EyeProlog validates the iterated goal before Instances. ISO 7.12 makes
2383
- // simultaneous-error selection implementation dependent; keep this stable as
2384
- // a processor choice rather than describing it as mandatory table ordering.
2385
- const { iterated, quantified } = bagGoalParts(goal.args[1], env);
2386
- assertListOrPartial(goal.args[2], env);
2387
- const free = freeVariables(iterated, goal.args[0], quantified, env);
2388
- const collector = solver.cloneForInnerGoal(10000000);
2389
- const groups = [];
2390
- for (const answerEnv of collector.solve([iterated], env.clone(), 0)) {
2391
- const copied = freshCopy(compound('$bag', [
2392
- compound('$witness', free),
2393
- goal.args[0],
2394
- ]), answerEnv);
2395
- let group = groups.find((candidate) => sameWitness(candidate.witness, copied.args[0]));
2396
- if (!group) {
2397
- group = { witness: copied.args[0], templates: [] };
2398
- groups.push(group);
2399
- group.templates.push(copied.args[1]);
2400
- } else {
2401
- const alignment = new Env();
2402
- unify(copied.args[0], group.witness, alignment);
2403
- group.templates.push(copyResolved(copied.args[1], alignment));
2404
- }
2529
+ return function allSolutions(context) {
2530
+ const state = { pending: false };
2531
+ return withPendingState(allSolutionsGroups(context, asSet, state), state);
2532
+ };
2533
+ }
2534
+
2535
+ function* allSolutionsGroups({ solver, goal, env }, asSet, state) {
2536
+ // EyeProlog validates the iterated goal before Instances. ISO 7.12 makes
2537
+ // simultaneous-error selection implementation dependent; keep this stable as
2538
+ // a processor choice rather than describing it as mandatory table ordering.
2539
+ const { iterated, quantified } = bagGoalParts(goal.args[1], env);
2540
+ assertListOrPartial(goal.args[2], env);
2541
+ const free = freeVariables(iterated, goal.args[0], quantified, env);
2542
+ const collector = solver.cloneForInnerGoal(10000000);
2543
+ const groups = [];
2544
+ for (const answerEnv of collector.solve([iterated], env.clone(), 0)) {
2545
+ const copied = freshCopy(compound('$bag', [
2546
+ compound('$witness', free),
2547
+ goal.args[0],
2548
+ ]), answerEnv);
2549
+ let group = groups.find((candidate) => sameWitness(candidate.witness, copied.args[0]));
2550
+ if (!group) {
2551
+ group = { witness: copied.args[0], templates: [] };
2552
+ groups.push(group);
2553
+ group.templates.push(copied.args[1]);
2554
+ } else {
2555
+ const alignment = new Env();
2556
+ unify(copied.args[0], group.witness, alignment);
2557
+ group.templates.push(copyResolved(copied.args[1], alignment));
2405
2558
  }
2406
- solver.absorbStatsFrom(collector);
2407
- for (const group of groups) {
2408
- const next = env.clone();
2409
- let matches = true;
2410
- for (let i = 0; i < free.length; i++) {
2411
- if (!unify(free[i], group.witness.args[i], next)) { matches = false; break; }
2412
- }
2413
- const templates = asSet ? sortedUnique(group.templates) : group.templates;
2414
- if (matches && unify(goal.args[2], listFromItems(templates), next)) yield next;
2559
+ }
2560
+ solver.absorbStatsFrom(collector);
2561
+ for (let index = 0; index < groups.length; index++) {
2562
+ const group = groups[index];
2563
+ const next = env.clone();
2564
+ let matches = true;
2565
+ for (let i = 0; i < free.length; i++) {
2566
+ if (!unify(free[i], group.witness.args[i], next)) { matches = false; break; }
2415
2567
  }
2416
- };
2568
+ const templates = asSet ? sortedUnique(group.templates) : group.templates;
2569
+ if (matches && unify(goal.args[2], listFromItems(templates), next)) {
2570
+ state.pending = index + 1 < groups.length;
2571
+ yield next;
2572
+ }
2573
+ }
2574
+ state.pending = false;
2417
2575
  }
2418
2576
  const bagofBuiltin = allSolutionsBuiltin(false);
2419
2577
  const setofBuiltin = allSolutionsBuiltin(true);
@@ -2936,8 +3094,12 @@ function* onceBuiltin({ solver, goal, env }) {
2936
3094
  }
2937
3095
  solver.absorbStatsFrom(child);
2938
3096
  }
2939
- function* repeatBuiltin({ env }) {
2940
- while (true) yield env;
3097
+ function repeatBuiltin({ env }) {
3098
+ const iterator = (function* repeatSolutions() {
3099
+ while (true) yield env;
3100
+ })();
3101
+ iterator.hasPendingAlternatives = () => true;
3102
+ return iterator;
2941
3103
  }
2942
3104
  function* negationBuiltin({ solver, goal, env }) {
2943
3105
  const invoked = callable(goal.args[0], env);
@@ -103,7 +103,14 @@ function validCharType(term) {
103
103
  (term.type === COMPOUND && ['lower', 'upper'].includes(term.name) && term.arity === 1);
104
104
  }
105
105
 
106
- function* charTypeBuiltin({ goal, env }) {
106
+ function charTypeBuiltin(context) {
107
+ const state = { pending: false };
108
+ const iterator = charTypeSolutions(context, state);
109
+ iterator.hasPendingAlternatives = () => state.pending;
110
+ return iterator;
111
+ }
112
+
113
+ function* charTypeSolutions({ goal, env }, state) {
107
114
  const char = deref(goal.args[0], env);
108
115
  const type = deref(goal.args[1], env);
109
116
  if (char.type === VAR && type.type === VAR) throw new PrologError('instantiation_error');
@@ -112,20 +119,23 @@ function* charTypeBuiltin({ goal, env }) {
112
119
  }
113
120
  if (type.type !== VAR && !validCharType(type)) throw new PrologError('domain_error(char_type)', type);
114
121
 
115
- const chars = char.type === VAR
116
- ? (function* unicodeScalars() {
117
- for (let code = 0; code <= 0x10ffff; code++) {
118
- if (code >= 0xd800 && code <= 0xdfff) continue;
119
- yield String.fromCodePoint(code);
120
- }
121
- })()
122
- : [char.name];
123
- for (const ch of chars) {
124
- for (const candidate of charTypeCandidates(ch)) {
122
+ const firstCode = char.type === VAR ? 0 : char.name.codePointAt(0);
123
+ const lastCode = char.type === VAR ? 0x10ffff : firstCode;
124
+ for (let code = firstCode; code <= lastCode; code++) {
125
+ if (code >= 0xd800 && code <= 0xdfff) continue;
126
+ const ch = String.fromCodePoint(code);
127
+ const candidates = charTypeCandidates(ch).filter((candidate) =>
128
+ type.type === VAR || (candidate.type === type.type && candidate.name === type.name));
129
+ for (let index = 0; index < candidates.length; index++) {
130
+ const candidate = candidates[index];
125
131
  const next = env.clone();
126
- if (unify(goal.args[0], atom(ch), next) && unify(goal.args[1], candidate, next)) yield next;
132
+ if (unify(goal.args[0], atom(ch), next) && unify(goal.args[1], candidate, next)) {
133
+ state.pending = index + 1 < candidates.length || code < lastCode;
134
+ yield next;
135
+ }
127
136
  }
128
137
  }
138
+ state.pending = false;
129
139
  }
130
140
 
131
141
  const shortMonths = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
package/src/solver.js CHANGED
@@ -1371,7 +1371,14 @@ function groupNeedsActiveFrame(group) {
1371
1371
  return group.cutReachable !== false || (group.recursive && !group.linearNumeric);
1372
1372
  }
1373
1373
 
1374
- function* datalogAnswerSolutions(solver, relation, goal, env) {
1374
+ function datalogAnswerSolutions(solver, relation, goal, env) {
1375
+ const state = { pending: false };
1376
+ const iterator = datalogAnswerGenerator(solver, relation, goal, env, state);
1377
+ iterator.hasPendingAlternatives = () => state.pending;
1378
+ return iterator;
1379
+ }
1380
+
1381
+ function* datalogAnswerGenerator(solver, relation, goal, env, state) {
1375
1382
  if (!relation) return;
1376
1383
  const candidates = datalogCandidateIndexes(
1377
1384
  relation,
@@ -1380,20 +1387,26 @@ function* datalogAnswerSolutions(solver, relation, goal, env) {
1380
1387
  derefForLocal,
1381
1388
  memoAnswerScalarKey,
1382
1389
  );
1383
- const visit = function* (rowIndex) {
1390
+ const indexes = candidates == null
1391
+ ? { length: relation.rows.length, at: (index) => index }
1392
+ : { length: candidates.length, at: (index) => candidates[index] };
1393
+ for (let position = 0; position < indexes.length; position++) {
1394
+ const rowIndex = indexes.at(position);
1384
1395
  const row = relation.rows[rowIndex];
1385
1396
  const next = env.clone();
1397
+ let matches = true;
1386
1398
  for (let i = 0; i < goal.arity; i++) {
1387
1399
  solver.stats.unify_calls++;
1388
- if (!unify(goal.args[i], row[i], next)) return;
1400
+ if (!unify(goal.args[i], row[i], next)) {
1401
+ matches = false;
1402
+ break;
1403
+ }
1389
1404
  }
1405
+ if (!matches) continue;
1406
+ state.pending = position + 1 < indexes.length;
1390
1407
  yield next;
1391
- };
1392
- if (candidates == null) {
1393
- for (let rowIndex = 0; rowIndex < relation.rows.length; rowIndex++) yield* visit(rowIndex);
1394
- return;
1395
1408
  }
1396
- for (const rowIndex of candidates) yield* visit(rowIndex);
1409
+ state.pending = false;
1397
1410
  }
1398
1411
 
1399
1412
  function pushWfsAnswerFrames(stack, model, group, goal, rest, env, depth, active, solver) {
@@ -1428,7 +1441,10 @@ function bundledBetweenIterator(solver, group, goal, env) {
1428
1441
  // the generated value in the caller repeatedly revisits all earlier frames.
1429
1442
  // Enumerate the canonical bundled relation directly while leaving user
1430
1443
  // definitions and non-EyeProlog registries on the ordinary Prolog path.
1431
- return bundledBetweenSolutions(solver, goal, env);
1444
+ const state = { pending: false };
1445
+ const iterator = bundledBetweenSolutions(solver, goal, env, state);
1446
+ iterator.hasPendingAlternatives = () => state.pending;
1447
+ return iterator;
1432
1448
  }
1433
1449
 
1434
1450
  function requireBetweenInteger(term, env) {
@@ -1440,7 +1456,7 @@ function requireBetweenInteger(term, env) {
1440
1456
  return BigInt(value.name);
1441
1457
  }
1442
1458
 
1443
- function* bundledBetweenSolutions(solver, goal, env) {
1459
+ function* bundledBetweenSolutions(solver, goal, env, state) {
1444
1460
  const lower = requireBetweenInteger(goal.args[0], env);
1445
1461
  const upper = requireBetweenInteger(goal.args[1], env);
1446
1462
  const requested = deref(goal.args[2], env);
@@ -1457,8 +1473,12 @@ function* bundledBetweenSolutions(solver, goal, env) {
1457
1473
  for (let value = lower; value <= upper; value++) {
1458
1474
  const next = env.clone();
1459
1475
  solver.stats.unify_calls++;
1460
- if (unify(goal.args[2], numberTerm(value), next)) yield next;
1476
+ if (unify(goal.args[2], numberTerm(value), next)) {
1477
+ state.pending = value < upper;
1478
+ yield next;
1479
+ }
1461
1480
  }
1481
+ state.pending = false;
1462
1482
  }
1463
1483
 
1464
1484
  function bundledMemberIterator(solver, group, goal, env) {
@@ -1468,25 +1488,48 @@ function bundledMemberIterator(solver, group, goal, env) {
1468
1488
  return null;
1469
1489
  }
1470
1490
 
1471
- // Keep the relational/open-list cases in ordinary Prolog. When the second
1472
- // argument is already a finite list, however, the canonical two-clause
1473
- // member/2 definition is exactly a left-to-right scan and can avoid clause
1474
- // freshening and recursive solver frames for every element.
1475
- let cursor = deref(goal.args[1], env);
1476
- while (isCons(cursor)) cursor = deref(cursor.args[1], env);
1477
- if (!isEmptyList(cursor)) return null;
1478
- return bundledMemberSolutions(solver, goal, env);
1491
+ // Attributed and delayed list variables retain the ordinary clause path so
1492
+ // their hooks and wake-up boundaries remain source-defined. Plain proper,
1493
+ // partial, and improper lists can all follow the canonical two-clause
1494
+ // relation lazily without first traversing the spine.
1495
+ if (env._prologAttributes != null || env._delays != null) return null;
1496
+ const state = { pending: false };
1497
+ const iterator = bundledMemberSolutions(solver, goal, env, state);
1498
+ iterator.hasPendingAlternatives = () => state.pending;
1499
+ return iterator;
1479
1500
  }
1480
1501
 
1481
- function* bundledMemberSolutions(solver, goal, env) {
1502
+ function* bundledMemberSolutions(solver, goal, env, state) {
1482
1503
  let cursor = deref(goal.args[1], env);
1483
1504
  while (isCons(cursor)) {
1484
1505
  const item = cursor.args[0];
1485
1506
  cursor = deref(cursor.args[1], env);
1486
1507
  const next = env.clone();
1487
1508
  solver.stats.unify_calls++;
1488
- if (unify(goal.args[0], item, next)) yield next;
1509
+ if (unify(goal.args[0], item, next)) {
1510
+ // The tail itself is the pending search state. Do not inspect it for a
1511
+ // future successful unification: an untried list position remains a
1512
+ // choicepoint even when resuming it will eventually fail.
1513
+ state.pending = isCons(cursor) || cursor.type === VAR;
1514
+ yield next;
1515
+ }
1489
1516
  }
1517
+ if (cursor.type === VAR) {
1518
+ const openTail = cursor;
1519
+ const id = nextFreshId();
1520
+ let candidate = cons(goal.args[0], variable(`__member${id}_tail`));
1521
+ for (let before = 0n; ; before++) {
1522
+ const next = env.clone();
1523
+ solver.stats.unify_calls++;
1524
+ if (unify(openTail, candidate, next)) {
1525
+ state.pending = true;
1526
+ yield next;
1527
+ }
1528
+ candidate = cons(variable(`__member${id}_head_${before}`), candidate);
1529
+ generatedLengthAllocationCheckpoint(solver, before + 1n);
1530
+ }
1531
+ }
1532
+ state.pending = false;
1490
1533
  }
1491
1534
 
1492
1535
  function bundledEllipsisPlan(solver, group, goal, rest, env) {
@@ -1520,7 +1563,7 @@ function bundledEllipsisPlan(solver, group, goal, rest, env) {
1520
1563
  if (output.type === VAR && continuationInput.type === VAR && output.name === continuationInput.name &&
1521
1564
  output.name.startsWith('\u0000dcg')) {
1522
1565
  return {
1523
- iterator: bundledEllipsisSolutions(solver, continuation.args[1], input, env),
1566
+ iterator: bundledEllipsisIterator(solver, continuation.args[1], input, env),
1524
1567
  rest: rest.slice(1),
1525
1568
  };
1526
1569
  }
@@ -1528,7 +1571,14 @@ function bundledEllipsisPlan(solver, group, goal, rest, env) {
1528
1571
  }
1529
1572
  }
1530
1573
 
1531
- return { iterator: bundledEllipsisSolutions(solver, goal.args[1], input, env), rest };
1574
+ return { iterator: bundledEllipsisIterator(solver, goal.args[1], input, env), rest };
1575
+ }
1576
+
1577
+ function bundledEllipsisIterator(solver, output, input, env) {
1578
+ const state = { pending: false };
1579
+ const iterator = bundledEllipsisSolutions(solver, output, input, env, state);
1580
+ iterator.hasPendingAlternatives = () => state.pending;
1581
+ return iterator;
1532
1582
  }
1533
1583
 
1534
1584
  function isBinaryIdentityGroup(group) {
@@ -1548,7 +1598,7 @@ function isBinaryIdentityGroup(group) {
1548
1598
  (a.name === right.name && b.name === left.name);
1549
1599
  }
1550
1600
 
1551
- function* bundledEllipsisSolutions(solver, output, input, env) {
1601
+ function* bundledEllipsisSolutions(solver, output, input, env, state) {
1552
1602
  let cursor = input;
1553
1603
  const requestedOutput = deref(output, env);
1554
1604
 
@@ -1568,7 +1618,10 @@ function* bundledEllipsisSolutions(solver, output, input, env) {
1568
1618
  while (true) {
1569
1619
  const next = env.clone();
1570
1620
  solver.stats.unify_calls++;
1571
- if (unify(output, cursor, next)) yield next;
1621
+ if (unify(output, cursor, next)) {
1622
+ state.pending = isCons(cursor);
1623
+ yield next;
1624
+ }
1572
1625
  if (isEmptyList(cursor)) return;
1573
1626
  if (!isCons(cursor)) return;
1574
1627
  cursor = deref(cursor.args[1], env);
@@ -1601,7 +1654,14 @@ function bundledLengthIterator(solver, group, goal, env) {
1601
1654
  if (length.type === VAR && cursor.name === length.name) return null;
1602
1655
  }
1603
1656
  }
1604
- return bundledLengthSolutions(solver, goal, env);
1657
+ const state = {
1658
+ // length(OpenList, Length) has an unbounded succession of longer lists.
1659
+ // All other accepted fast-path modes are semidet.
1660
+ pending: length.type === VAR && cursor.type === VAR,
1661
+ };
1662
+ const iterator = bundledLengthSolutions(solver, goal, env);
1663
+ iterator.hasPendingAlternatives = () => state.pending;
1664
+ return iterator;
1605
1665
  }
1606
1666
 
1607
1667
  function* bundledLengthSolutions(solver, goal, env) {
@@ -2185,7 +2245,14 @@ function pushStreamingScalarFactRun(stack, solver, runGoals, groups, rest, env,
2185
2245
  });
2186
2246
  }
2187
2247
 
2188
- function* scalarFactRunSolutions(solver, goals, groups, env, depth, active) {
2248
+ function scalarFactRunSolutions(solver, goals, groups, env, depth, active) {
2249
+ const state = { pending: false };
2250
+ const iterator = scalarFactRunGenerator(solver, goals, groups, env, depth, active, state);
2251
+ iterator.hasPendingAlternatives = () => state.pending;
2252
+ return iterator;
2253
+ }
2254
+
2255
+ function* scalarFactRunGenerator(solver, goals, groups, env, depth, active, pendingState) {
2189
2256
  const localStack = [{ index: 0, names: [], values: [] }];
2190
2257
  while (localStack.length) {
2191
2258
  const state = localStack.pop();
@@ -2194,6 +2261,7 @@ function* scalarFactRunSolutions(solver, goals, groups, env, depth, active) {
2194
2261
  const next = env.clone();
2195
2262
  for (let i = 0; i < state.names.length; i++) next.bind(state.names[i], state.values[i]);
2196
2263
  if (next._variableConstraints != null && !next.validateVariableConstraints()) continue;
2264
+ pendingState.pending = localStack.length !== 0;
2197
2265
  yield next;
2198
2266
  continue;
2199
2267
  }
@@ -2218,6 +2286,7 @@ function* scalarFactRunSolutions(solver, goals, groups, env, depth, active) {
2218
2286
  for (let i = nextStates.length - 1; i >= 0; i--) localStack.push(nextStates[i]);
2219
2287
  if (solver.solutionsSeen >= solver.solutionLimit) return;
2220
2288
  }
2289
+ pendingState.pending = false;
2221
2290
  }
2222
2291
 
2223
2292
 
@@ -2813,64 +2882,13 @@ function builtinIsReadyOrAuthoritative(def, solver, goal, env) {
2813
2882
 
2814
2883
  function iteratorMayHavePendingAlternatives(iterator) {
2815
2884
  const predicate = iterator?.hasPendingAlternatives;
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);
2885
+ // An unannotated suspended iterator is itself an untried continuation.
2886
+ // Advancing it here would speculatively execute Prolog search (and possibly
2887
+ // effects) merely to discover whether a later answer succeeds.
2888
+ return typeof predicate !== 'function' || predicate.call(iterator) !== false;
2870
2889
  }
2871
2890
 
2872
2891
  function pushResumeBuiltinFrame(stack, iterator, goals, depth, active) {
2873
- iterator = choicepointAwareIterator(iterator);
2874
2892
  if (!iteratorMayHavePendingAlternatives(iterator)) {
2875
2893
  // Finish the suspended generator now so its finally blocks release child
2876
2894
  // solvers, aggregate statistics, and perform predicate-local cleanup.
@@ -274,7 +274,14 @@ function* statisticsBuiltin({ solver, env }) {
274
274
  yield env;
275
275
  }
276
276
 
277
- function* statisticsValueBuiltin({ solver, goal, env }) {
277
+ function statisticsValueBuiltin(context) {
278
+ const state = { pending: false };
279
+ const iterator = statisticsValueSolutions(context, state);
280
+ iterator.hasPendingAlternatives = () => state.pending;
281
+ return iterator;
282
+ }
283
+
284
+ function* statisticsValueSolutions({ solver, goal, env }, state) {
278
285
  const snapshot = runtimeStatistics(solver);
279
286
  const key = deref(goal.args[0], env);
280
287
  const entries = key.type === VAR
@@ -288,10 +295,15 @@ function* statisticsValueBuiltin({ solver, goal, env }) {
288
295
  throw new PrologError('domain_error(statistics_key)', key);
289
296
  }
290
297
 
291
- for (const [name, value] of entries) {
298
+ for (let index = 0; index < entries.length; index++) {
299
+ const [name, value] = entries[index];
292
300
  const next = env.clone();
293
- if (unify(goal.args[0], atom(name), next) && unify(goal.args[1], numberTerm(value), next)) yield next;
301
+ if (unify(goal.args[0], atom(name), next) && unify(goal.args[1], numberTerm(value), next)) {
302
+ state.pending = index + 1 < entries.length;
303
+ yield next;
304
+ }
294
305
  }
306
+ state.pending = false;
295
307
  }
296
308
 
297
309
  export function createEyePrologRegistry() {
package/src/term.js CHANGED
@@ -65,7 +65,7 @@ export class CompactListTerm {
65
65
  }
66
66
  return this._args;
67
67
  }
68
- mayContainVariable(name) {
68
+ mayContainVariable(name, env = null) {
69
69
  if (String(name).startsWith(this._variablePrefix)) {
70
70
  const indexText = String(name).slice(this._variablePrefix.length);
71
71
  if (/^\d+$/.test(indexText)) {
@@ -73,9 +73,12 @@ export class CompactListTerm {
73
73
  if (index >= this._offset && index < this._offset + this._compactLength) return true;
74
74
  }
75
75
  }
76
- // If no generated head in this suffix has ever participated in a binding,
77
- // an unrelated variable cannot occur below it. This remains conservative
78
- // across backtracking because the high-water mark is never rolled back.
76
+ // An Env keeps the exact backtrackable set of cells whose current values
77
+ // may reach an external variable. Without one, retain the shared
78
+ // high-water mark as a conservative public-API fallback.
79
+ if (env?.compactListMayReachExternalVariable != null) {
80
+ return env.compactListMayReachExternalVariable(this._compactState, this._offset);
81
+ }
79
82
  return this._compactState.maxPossiblyBoundIndex >= this._offset;
80
83
  }
81
84
  }
@@ -124,6 +127,11 @@ export class Env {
124
127
  this._backtrackableBlackboard = null;
125
128
  this._occursCheckHandler = null;
126
129
  this._localVariables = null;
130
+ // Origins carried by variables that temporarily represent compact-list
131
+ // cells. Indexed by logical variable name because callers may reconstruct
132
+ // an equivalent Term object while environments retain name identity.
133
+ this._compactVariableOrigins = null;
134
+ this._compactVariableRisks = null;
127
135
  }
128
136
  clone() {
129
137
  // Most speculative environments are either rejected without a binding or
@@ -142,6 +150,8 @@ export class Env {
142
150
  clone._backtrackableBlackboard = this._backtrackableBlackboard;
143
151
  clone._occursCheckHandler = this._occursCheckHandler;
144
152
  clone._localVariables = this._localVariables;
153
+ clone._compactVariableOrigins = this._compactVariableOrigins;
154
+ clone._compactVariableRisks = this._compactVariableRisks;
145
155
  return clone;
146
156
  }
147
157
  setOccursCheckHandler(handler) {
@@ -164,8 +174,80 @@ export class Env {
164
174
  // Execution callbacks belong to the Solver driving this Env, not to the
165
175
  // logical branch being adopted from an inner attribute-hook call.
166
176
  this._localVariables = other._localVariables;
177
+ this._compactVariableOrigins = other._compactVariableOrigins;
178
+ this._compactVariableRisks = other._compactVariableRisks;
167
179
  return this;
168
180
  }
181
+ compactVariableOrigins(name) {
182
+ return this._compactVariableOrigins?.get(name) ?? null;
183
+ }
184
+ transferCompactVariableOrigins(sourceName, targetName, origins) {
185
+ const next = new Map(this._compactVariableOrigins ?? []);
186
+ if (sourceName !== targetName) next.delete(sourceName);
187
+ next.set(targetName, origins);
188
+ this._compactVariableOrigins = next.size === 0 ? null : next;
189
+ }
190
+ removeCompactVariableOrigins(name) {
191
+ if (this._compactVariableOrigins?.has(name) !== true) return;
192
+ const next = new Map(this._compactVariableOrigins);
193
+ next.delete(name);
194
+ this._compactVariableOrigins = next.size === 0 ? null : next;
195
+ }
196
+ addCompactVariableRisks(origins) {
197
+ if (origins.length === 0) return;
198
+ const next = new Map(this._compactVariableRisks ?? []);
199
+ for (const { state, index } of origins) {
200
+ const current = next.get(state) ?? null;
201
+ let top = current?.top ?? null;
202
+ const removed = current?.removed ?? null;
203
+ while (top != null && removed?.has(top)) top = top.parent;
204
+ // Immediate variable-to-variable handoffs carry the same origin. Avoid
205
+ // stacking duplicate risk entries along that common alias chain.
206
+ if (top?.index === index) continue;
207
+ next.set(state, { top: { index, parent: top }, removed });
208
+ }
209
+ this._compactVariableRisks = next;
210
+ }
211
+ removeCompactVariableRisks(origins) {
212
+ if (origins.length === 0 || this._compactVariableRisks == null) return;
213
+ const next = new Map(this._compactVariableRisks);
214
+ for (const { state, index } of origins) {
215
+ const current = next.get(state);
216
+ if (current == null) continue;
217
+ let top = current.top;
218
+ let removed = current.removed;
219
+ while (top != null && removed?.has(top)) top = top.parent;
220
+ if (top?.index === index) {
221
+ top = top.parent;
222
+ while (top != null && removed?.has(top)) top = top.parent;
223
+ } else {
224
+ // Out-of-order handoffs are uncommon, but remain exact: tombstone
225
+ // every active entry for this origin without rewriting the persistent
226
+ // stack shared by sibling environments.
227
+ let scan = top;
228
+ let changed = false;
229
+ while (scan != null) {
230
+ if (scan.index === index && removed?.has(scan) !== true) {
231
+ if (!changed) removed = new Set(removed ?? []);
232
+ removed.add(scan);
233
+ changed = true;
234
+ }
235
+ scan = scan.parent;
236
+ }
237
+ }
238
+ if (top == null) next.delete(state);
239
+ else next.set(state, { top, removed });
240
+ }
241
+ this._compactVariableRisks = next.size === 0 ? null : next;
242
+ }
243
+ compactListMayReachExternalVariable(state, offset) {
244
+ const current = this._compactVariableRisks?.get(state);
245
+ if (current == null) return false;
246
+ for (let entry = current.top; entry != null; entry = entry.parent) {
247
+ if (current.removed?.has(entry) !== true && entry.index >= offset) return true;
248
+ }
249
+ return false;
250
+ }
169
251
  getBacktrackableBlackboard(key) {
170
252
  return this._backtrackableBlackboard?.get(key);
171
253
  }
@@ -605,7 +687,7 @@ function occursUncached(variableName, term, env) {
605
687
  if (binding !== undefined) stack.push(binding);
606
688
  continue;
607
689
  }
608
- if (isCompactList(current) && !current.mayContainVariable(variableName)) continue;
690
+ if (isCompactList(current) && !current.mayContainVariable(variableName, env)) continue;
609
691
  if (current?.type !== COMPOUND || seenTerms.has(current)) continue;
610
692
  seenTerms.add(current);
611
693
  for (let i = 0; i < current.arity; i++) stack.push(current.args[i]);
@@ -693,11 +775,11 @@ export function unify(left, right, env, options = {}) {
693
775
  const aLocalDcg = env?.isLocalVariable(a.name) === true &&
694
776
  a.name.startsWith('\u0000dcg') && b.name.startsWith('\u0000dcg');
695
777
  if (aLocalDcg) {
696
- markCompactVariableBound(b);
778
+ transferCompactVariableOrigins(b, a, env);
697
779
  env?.finalizePrologAttributeAlias?.(b.name, a.name);
698
780
  env.bind(b.name, a);
699
781
  } else {
700
- markCompactVariableBound(a);
782
+ transferCompactVariableOrigins(a, b, env);
701
783
  env?.finalizePrologAttributeAlias?.(a.name, b.name);
702
784
  env.bind(a.name, b);
703
785
  }
@@ -715,7 +797,7 @@ export function unify(left, right, env, options = {}) {
715
797
  b = deref(b, env);
716
798
  if (a.type !== VAR) { stack.push([a, b]); continue; }
717
799
  }
718
- markCompactVariableBound(a);
800
+ markCompactVariableBound(a, b, env);
719
801
  env?.dropPrologAttributes?.(a.name);
720
802
  env.bind(a.name, b);
721
803
  if (aLocal) env.forgetLocalVariable(a.name);
@@ -733,7 +815,7 @@ export function unify(left, right, env, options = {}) {
733
815
  a = deref(a, env);
734
816
  if (b.type !== VAR) { stack.push([a, b]); continue; }
735
817
  }
736
- markCompactVariableBound(b);
818
+ markCompactVariableBound(b, a, env);
737
819
  env?.dropPrologAttributes?.(b.name);
738
820
  env.bind(b.name, a);
739
821
  if (bLocal) env.forgetLocalVariable(b.name);
@@ -761,10 +843,50 @@ export function unify(left, right, env, options = {}) {
761
843
  return true;
762
844
  }
763
845
 
764
- function markCompactVariableBound(term) {
765
- if (term?._compactState == null || term._compactIndex == null) return;
766
- if (term._compactIndex > term._compactState.maxPossiblyBoundIndex) {
767
- term._compactState.maxPossiblyBoundIndex = term._compactIndex;
846
+ function compactVariableOrigins(term, env) {
847
+ const remembered = term?.type === VAR ? env?.compactVariableOrigins(term.name) : null;
848
+ const origins = remembered == null ? [] : [...remembered];
849
+ if (term?._compactState != null && term._compactIndex != null &&
850
+ !origins.some(({ state, index }) => state === term._compactState && index === term._compactIndex)) {
851
+ origins.push({ state: term._compactState, index: term._compactIndex });
852
+ }
853
+ return origins;
854
+ }
855
+
856
+ function transferCompactVariableOrigins(source, target, env) {
857
+ const sourceOrigins = compactVariableOrigins(source, env);
858
+ if (sourceOrigins.length === 0 || target?.type !== VAR) return;
859
+ const targetOrigins = compactVariableOrigins(target, env);
860
+ for (const origin of sourceOrigins) {
861
+ if (!targetOrigins.some(({ state, index }) => state === origin.state && index === origin.index)) {
862
+ targetOrigins.push(origin);
863
+ }
864
+ }
865
+ env?.addCompactVariableRisks(sourceOrigins);
866
+ env?.transferCompactVariableOrigins(source.name, target.name, targetOrigins);
867
+ }
868
+
869
+ function markCompactVariableBound(term, value, env) {
870
+ const origins = compactVariableOrigins(term, env);
871
+ if (origins.length === 0) return;
872
+ // A compact cell commonly passes through a fresh clause variable before it
873
+ // receives its actual value. Carry the provenance along that alias. Only a
874
+ // non-scalar value can make an unrelated logical variable reachable from
875
+ // the compact list, so ground atoms and numbers need not poison the whole
876
+ // suffix for subsequent finite-tree occurs checks.
877
+ if (value?.type === VAR) {
878
+ transferCompactVariableOrigins(term, value, env);
879
+ return;
880
+ }
881
+ if (isScalar(value)) {
882
+ env?.removeCompactVariableRisks(origins);
883
+ env?.removeCompactVariableOrigins(term.name);
884
+ return;
885
+ }
886
+ env?.addCompactVariableRisks(origins);
887
+ env?.removeCompactVariableOrigins(term.name);
888
+ for (const { state, index } of origins) {
889
+ if (index > state.maxPossiblyBoundIndex) state.maxPossiblyBoundIndex = index;
768
890
  }
769
891
  }
770
892
 
@@ -29,6 +29,7 @@ import {
29
29
  eyePrologInteropLibraryIndicators,
30
30
  eyePrologInteropLibraryModules,
31
31
  atom,
32
+ compactVariableList,
32
33
  compound,
33
34
  listFromItems,
34
35
  numberTerm,
@@ -340,8 +341,8 @@ function regressionCases() {
340
341
  });
341
342
  assertEqual(residuals.status, 0, 'dif/2 residual display status');
342
343
  assertEqual(residuals.stdout,
343
- '?- C = z, D = z, dif(A-z, B-z).\n' +
344
- '?- A = [[]], B = [], dif([[]], [C]).\n' +
344
+ '?- C = z, D = z, dif(A, B).\n' +
345
+ '?- A = [[]], B = [], dif([], C).\n' +
345
346
  '?- X = Y.\n' +
346
347
  '?- ',
347
348
  'dif/2 residual constraints are retained and discharged');
@@ -374,6 +375,12 @@ function regressionCases() {
374
375
  'dif(X-Y,Z-Z),dif(X,Y).\n' +
375
376
  'dif(X,a),dif(X,b).\n' +
376
377
  'dif(X,a),dif(Y,a),X=Y.\n' +
378
+ 'dif(-X,-Y).\n' +
379
+ 'dif(X,Y),X+Y= -XA+ -YA.\n' +
380
+ 'dif(f(X,a),f(Y,a)).\n' +
381
+ 'dif(f(X,X),f(Y,Y)).\n' +
382
+ 'dif(f(X,Y),f(Y,X)).\n' +
383
+ 'dif(f(X,A),f(Y,B)).\n' +
377
384
  'halt.\n',
378
385
  });
379
386
  assertEqual(residuals.status, 0, 'dif/2 normalization display status');
@@ -383,8 +390,14 @@ function regressionCases() {
383
390
  '?- dif(X, Y).\n' +
384
391
  '?- dif(X, a), dif(X, b).\n' +
385
392
  '?- X = Y, dif(Y, a).\n' +
393
+ '?- dif(X, Y).\n' +
394
+ '?- X = -XA, Y = -YA, dif(XA, YA).\n' +
395
+ '?- dif(X, Y).\n' +
396
+ '?- dif(X, Y).\n' +
397
+ '?- dif(X, Y).\n' +
398
+ '?- dif(f(X, A), f(Y, B)).\n' +
386
399
  '?- ',
387
- 'dif/2 residuals are minimal independent of insertion order');
400
+ 'dif/2 residuals are minimal without auxiliary terms');
388
401
  assertEqual(residuals.stderr, '', 'dif/2 normalization display stderr');
389
402
 
390
403
  const pendingDifCount = (text) => {
@@ -2290,16 +2303,32 @@ c4 ?- call((!;1)).
2290
2303
  const custom = new Solver(Program.parse(''), { registry: customRegistry });
2291
2304
  const customSolutions = custom.solve([parseGoalText('single(X)')], new Env(), 0);
2292
2305
  assertEqual(customSolutions.next().done, false, 'custom relational iterator answer');
2293
- assertEqual(custom.hasPendingAlternatives(), false,
2294
- 'unannotated relational iterator uses the universal lookahead protocol');
2306
+ assertEqual(custom.hasPendingAlternatives(), true,
2307
+ 'an unannotated suspended iterator remains an untried continuation');
2295
2308
  customSolutions.return();
2296
2309
 
2310
+ const annotatedRegistry = new BuiltinRegistry();
2311
+ annotatedRegistry.add('single', 1, ({ goal, env }) => {
2312
+ const iterator = (function* singleSolution() {
2313
+ const next = env.clone();
2314
+ if (unify(goal.args[0], atom('only'), next)) yield next;
2315
+ })();
2316
+ iterator.hasPendingAlternatives = () => false;
2317
+ return iterator;
2318
+ });
2319
+ const annotated = new Solver(Program.parse(''), { registry: annotatedRegistry });
2320
+ const annotatedSolutions = annotated.solve([parseGoalText('single(X)')], new Env(), 0);
2321
+ assertEqual(annotatedSolutions.next().done, false, 'annotated relational iterator answer');
2322
+ assertEqual(annotated.hasPendingAlternatives(), false,
2323
+ 'an iterator may report exact exhaustion without being resumed');
2324
+ annotatedSolutions.return();
2325
+
2297
2326
  const dynamicProgram = Program.parse(':- dynamic(p/1).\np(a).\np(b).\n');
2298
2327
  const dynamicSolver = new Solver(dynamicProgram, { registry: getEyePrologRegistry() });
2299
2328
  const dynamicSolutions = dynamicSolver.solve([parseGoalText('catch(retract(p(X)),_,fail)')], new Env(), 0);
2300
2329
  assertEqual(dynamicSolutions.next().done, false, 'stateful protected iterator first answer');
2301
2330
  assertEqual(dynamicProgram.findGroup('p', 1).clauses.length, 1,
2302
- 'lookahead does not perform the next retraction');
2331
+ 'pending-state inspection does not perform the next retraction');
2303
2332
  assertEqual(dynamicSolver.hasPendingAlternatives(), true,
2304
2333
  'stateful protected iterator retains its real second retraction');
2305
2334
  dynamicSolutions.return();
@@ -2308,27 +2337,91 @@ c4 ?- call((!;1)).
2308
2337
  },
2309
2338
  },
2310
2339
  {
2311
- name: 'bound member tails do not invent predicate choicepoints (issue #74 follow-up)',
2340
+ name: 'member/2 retains untried positions without scanning them (issue #74 comment 5435449600)',
2312
2341
  run: () => {
2313
2342
  const result = runCli([], {
2314
2343
  input:
2315
2344
  'use_module(library(lists)).\n' +
2316
- 'L=[i],append(L,_,[i|L]).\n' +
2317
- 'L=[_],append(L,_,[i|L]).\n' +
2318
- 'L=[i],append(L,_,[i|L]),member(a,[a|L]).\n' +
2319
- 'L=[_],append(L,_,[i|L]),member(a,[a|L]).\n' +
2345
+ 'L=[i],append(L,_,[i|L]),' +
2346
+ '(Det=preparing;setup_call_cleanup(true,member(a,[a|L]),Det=yes)).\n' +
2347
+ ';\n' +
2348
+ 'setup_call_cleanup(true,member(a,[a]),Det=yes).\n' +
2320
2349
  'halt.\n',
2321
2350
  });
2322
2351
  assertEqual(result.status, 0, 'exit status');
2323
2352
  assertEqual(result.stdout,
2324
2353
  '?- true.\n' +
2325
- '?- L = "i".\n' +
2326
- '?- L = "i".\n' +
2327
- '?- L = "i".\n' +
2328
- '?- L = "i".\n' +
2354
+ '?- L = "i", Det = preparing\n' +
2355
+ '; L = "i".\n' +
2356
+ '?- Det = yes.\n' +
2329
2357
  '?- ',
2330
- 'ground and formerly variable list elements have identical determinism');
2358
+ 'cleanup observes the real list-position choicepoint');
2331
2359
  assertEqual(result.stderr, '', 'stderr');
2360
+
2361
+ let speculativeTailRead = false;
2362
+ const unreadTail = {
2363
+ type: 'compound',
2364
+ name: '.',
2365
+ get arity() { return 2; },
2366
+ get args() {
2367
+ speculativeTailRead = true;
2368
+ throw new Error('member/2 inspected an untried tail position');
2369
+ },
2370
+ };
2371
+ const memberGoal = compound('member', [atom('a'), compound('.', [atom('a'), unreadTail])]);
2372
+ memberGoal.module = 'lists';
2373
+ const solver = new Solver(Program.parse(':- use_module(library(lists)).'), {
2374
+ registry: getEyePrologRegistry(),
2375
+ });
2376
+ const solutions = solver.solve([memberGoal], new Env(), 0);
2377
+ assertEqual(solutions.next().done, false, 'member/2 first answer');
2378
+ assertEqual(speculativeTailRead, false, 'first answer does not inspect the tail');
2379
+ assertEqual(solver.hasPendingAlternatives(), true, 'unread tail is a genuine alternative');
2380
+ solutions.return();
2381
+ assertEqual(speculativeTailRead, false, 'discarding the choicepoint does not inspect it');
2382
+
2383
+ const compactAppendTail = compactVariableList(100000n, '__appendTail');
2384
+ const appendGoal = compound('append', [
2385
+ variable('Prefix'),
2386
+ compound('.', [atom('a'), variable('Suffix')]),
2387
+ compound('.', [atom('a'), compactAppendTail]),
2388
+ ]);
2389
+ appendGoal.module = 'lists';
2390
+ const appendSolver = new Solver(Program.parse(':- use_module(library(lists)).'), {
2391
+ registry: getEyePrologRegistry(),
2392
+ });
2393
+ const appendSolutions = appendSolver.solve([appendGoal], new Env(), 0);
2394
+ assertEqual(appendSolutions.next().done, false, 'append/3 first answer');
2395
+ assertEqual(compactAppendTail._args, null, 'append/3 first answer does not expand the tail');
2396
+ assertEqual(appendSolver.hasPendingAlternatives(), true, 'append/3 retains its recursive alternative');
2397
+ appendSolutions.return();
2398
+ assertEqual(compactAppendTail._args, null, 'discarding append/3 alternatives does not expand the tail');
2399
+
2400
+ const compact = compactVariableList(100000n, '__scalarCompact');
2401
+ const firstCell = compact.args[0];
2402
+ const env = new Env();
2403
+ assertEqual(unify(firstCell, variable('ClauseHead'), env), true,
2404
+ 'compact cell aliases a clause-head variable');
2405
+ assertEqual(unify(variable('ClauseHead'), atom('i'), env), true,
2406
+ 'the clause-head variable receives its scalar value');
2407
+
2408
+ // Recreate the unexpanded view over the same conservative provenance,
2409
+ // as a recursive compact-list tail would provide.
2410
+ compact._args = null;
2411
+ assertEqual(unify(variable('Unrelated'), compact, env), true,
2412
+ 'an unrelated variable unifies with the compact list');
2413
+ assertEqual(compact._args, null,
2414
+ 'the occurs check proves nonoccurrence without expanding scalar-bound cells');
2415
+
2416
+ const nonGroundCompact = compactVariableList(2n, '__nonGroundCompact');
2417
+ const nonGroundCell = nonGroundCompact.args[0];
2418
+ const cyclicEnv = new Env();
2419
+ assertEqual(unify(nonGroundCell, variable('Intermediate'), cyclicEnv), true,
2420
+ 'a second compact cell aliases a clause-head variable');
2421
+ assertEqual(unify(variable('Intermediate'), compound('f', [variable('Cycle')]), cyclicEnv), true,
2422
+ 'the aliased cell receives a non-ground value');
2423
+ assertEqual(unify(variable('Cycle'), nonGroundCompact, cyclicEnv), false,
2424
+ 'non-ground compact bindings still receive the complete finite-tree occurs check');
2332
2425
  },
2333
2426
  },
2334
2427
  {
@@ -2017,11 +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. 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
2020
+ for a call. An unmarked suspended iterator is conservatively an untried
2021
+ continuation: the solver never resumes it merely to discover whether a later
2022
+ answer will succeed. An iterator that knows its remaining search positions may
2023
+ provide `hasPendingAlternatives()`, updated before each yield, to remove its
2024
+ resume frame exactly when no position remains. This method reports pending
2025
+ search, not the existence of a future successful answer. A mode-sensitive
2025
2026
  extension can additionally provide `ready`,
2026
2027
  `fallbackWhenNotReady`, and `shouldUse` metadata. This metadata affects
2027
2028
  dispatch and safe early filtering, so it belongs to the extension's contract.
@@ -8015,10 +8016,10 @@ decidable by structural equality.
8015
8016
 
8016
8017
  **Choicepoint.** A remaining search alternative that may produce another
8017
8018
  answer if the caller asks the solver to continue. Every resumable engine
8018
- iterator follows the same pending-alternative protocol. Ordinary effect-free
8019
- host relations may buffer one answer to prove exhaustion; stateful and
8020
- meta-control iterators report their state without executing an unrequested
8021
- effect or program branch.
8019
+ iterator follows the same pending-alternative protocol. A suspended iterator
8020
+ is conservatively a choicepoint unless it reports that no search position
8021
+ remains; the engine never executes an unrequested effect or program branch to
8022
+ look for a later successful answer.
8022
8023
 
8023
8024
  **Cleanup.** A protected finalization goal installed by normal-mode
8024
8025
  `call_cleanup/2` or `setup_call_cleanup/3`. It runs exactly once when the