eyeprolog 1.3.91 → 1.3.93

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.
@@ -250,3 +250,4 @@ npm run generate
250
250
  - [01-color.pl](chapter-40/01-color.pl) — Embedded quad tests
251
251
  - [02-program.pl](chapter-40/02-program.pl)
252
252
  - [03-program-2.pl](chapter-40/03-program-2.pl)
253
+ - [04-program-3.pl](chapter-40/04-program-3.pl)
@@ -1,5 +1,6 @@
1
1
  % From The Art of EyeProlog, Chapter 40.
2
- ?- read(X).
3
- inputs("1."), X = 1, unexpected.
4
- inputs("1."), peeks(" "), X = 1.
5
- inputs("1. "), peeks(" "), X = 1, unexpected.
2
+ ?- dif(X,Y), X = a.
3
+ true, unexpected.
4
+ X = a, unexpected.
5
+ X = a, maybe.
6
+ maybe, unexpected.
@@ -1,5 +1,5 @@
1
1
  % From The Art of EyeProlog, Chapter 40.
2
- inf :- inf, inf.
3
-
4
- ?- inf.
5
- loops.
2
+ ?- read(X).
3
+ inputs("1."), X = 1, unexpected.
4
+ inputs("1."), peeks(" "), X = 1.
5
+ inputs("1. "), peeks(" "), X = 1, unexpected.
@@ -0,0 +1,5 @@
1
+ % From The Art of EyeProlog, Chapter 40.
2
+ inf :- inf, inf.
3
+
4
+ ?- inf.
5
+ loops.
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.91",
6
+ "version": "1.3.93",
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
@@ -627,16 +627,30 @@ function validPredicateIndicator(term) {
627
627
  (term.args[1].type === NUMBER && isDecimalInteger(term.args[1].name) && BigInt(term.args[1].name) >= 0n));
628
628
  }
629
629
 
630
- 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) {
631
636
  const indicator = copyResolved(goal.args[0], env);
632
637
  if (indicator.type !== VAR && !validPredicateIndicator(indicator)) {
633
638
  throw new PrologError('type_error(predicate_indicator)', indicator);
634
639
  }
635
- 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];
636
646
  const next = env.clone();
637
647
  const candidate = compound('/', [atom(group.name), numberTerm(group.arity)]);
638
- 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
+ }
639
652
  }
653
+ state.pending = false;
640
654
  }
641
655
 
642
656
  function callableOrVariable(term, env) {
@@ -652,7 +666,12 @@ function clauseBodyTerm(body) {
652
666
  return result;
653
667
  }
654
668
 
655
- 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) {
656
675
  const head = deref(goal.args[0], env);
657
676
  if (head.type === VAR) throw new PrologError('instantiation_error');
658
677
  if (head.type !== ATOM && head.type !== COMPOUND) throw new PrologError('type_error(callable)', head);
@@ -667,12 +686,17 @@ function* clauseBuiltin({ solver, goal, env }) {
667
686
  }
668
687
  callableOrVariable(goal.args[1], env);
669
688
  if (!group) return;
670
- for (const clause of group.clauses) {
689
+ for (let index = 0; index < group.clauses.length; index++) {
690
+ const clause = group.clauses[index];
671
691
  const pair = compound('$clause', [clause.head, clauseBodyTerm(clause.body)]);
672
692
  const copied = freshCopy(pair, new Env());
673
693
  const next = env.clone();
674
- 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
+ }
675
698
  }
699
+ state.pending = false;
676
700
  }
677
701
 
678
702
  function clauseParts(term, env) {
@@ -845,21 +869,32 @@ function* abolishBuiltin({ solver, goal, env }) {
845
869
  yield env;
846
870
  }
847
871
 
848
- 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) {
849
878
  const flag = deref(goal.args[0], env);
850
879
  if (flag.type !== VAR && flag.type !== ATOM) throw new PrologError('type_error(atom)', flag);
851
880
  if (flag.type === ATOM && !solver.prologFlags.has(flag.name)) {
852
881
  throw new PrologError('domain_error(prolog_flag)', flag);
853
882
  }
854
- 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];
855
887
  // ISO 7.11.1.1: when bounded=false, max_integer and min_integer have no
856
888
  // current value and current_prolog_flag/2 must therefore not enumerate
857
889
  // them. The definitions remain registered so attempts to change these
858
890
  // non-changeable flags still receive the normal flag error handling.
859
- if (definition.value == null) continue;
860
891
  const next = env.clone();
861
- 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
+ }
862
896
  }
897
+ state.pending = false;
863
898
  }
864
899
 
865
900
  function* setPrologFlagBuiltin({ solver, goal, env }) {
@@ -946,7 +981,12 @@ function* opBuiltin({ solver, goal, env }) {
946
981
  yield env;
947
982
  }
948
983
 
949
- 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) {
950
990
  const priority = deref(goal.args[0], env);
951
991
  const specifier = deref(goal.args[1], env);
952
992
  const name = deref(goal.args[2], env);
@@ -962,12 +1002,21 @@ function* currentOpBuiltin({ solver, goal, env }) {
962
1002
  throw new PrologError('domain_error(operator_specifier)', specifier);
963
1003
  }
964
1004
  if (name.type !== VAR && name.type !== ATOM) throw new PrologError('type_error(atom)', name);
965
- 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];
966
1011
  const next = env.clone();
967
1012
  if (unify(goal.args[0], numberTerm(definition.priority), next) &&
968
1013
  unify(goal.args[1], atom(definition.specifier), next) &&
969
- 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
+ }
970
1018
  }
1019
+ state.pending = false;
971
1020
  }
972
1021
 
973
1022
  function conversionCharacter(term, env, current = false, solver = null) {
@@ -998,13 +1047,25 @@ function* charConversionBuiltin({ solver, goal, env }) {
998
1047
  else solver.charConversions.set(input.name, output.name);
999
1048
  yield env;
1000
1049
  }
1001
- 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) {
1002
1056
  const input = conversionCharacter(goal.args[0], env, true, solver);
1003
1057
  const output = conversionCharacter(goal.args[1], env, true, solver);
1004
- 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];
1005
1062
  const next = env.clone();
1006
- 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
+ }
1007
1067
  }
1068
+ state.pending = false;
1008
1069
  }
1009
1070
  function* haltBuiltin({ goal, env }) {
1010
1071
  const code = goal.arity === 0 ? 0n : requireInteger(goal.args[0], env);
@@ -1293,7 +1354,12 @@ function isStreamPropertyPattern(value) {
1293
1354
  ].includes(value.name);
1294
1355
  }
1295
1356
 
1296
- 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) {
1297
1363
  const reference = deref(goal.args[0], env);
1298
1364
  const propertyPattern = deref(goal.args[1], env);
1299
1365
  let fixedStreamId = null;
@@ -1311,12 +1377,24 @@ function* streamPropertyBuiltin({ solver, goal, env }) {
1311
1377
  const streams = reference.type === VAR
1312
1378
  ? [...solver.io.streams.values()]
1313
1379
  : [solver.io.resolve(fixedStreamId)].filter(Boolean);
1380
+ const candidates = [];
1314
1381
  for (const stream of streams) {
1315
1382
  for (const property of streamProperties(stream)) {
1316
- const next = env.clone();
1317
- 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
+ }
1387
+ }
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;
1318
1395
  }
1319
1396
  }
1397
+ state.pending = false;
1320
1398
  }
1321
1399
 
1322
1400
  function inputStreamFor(solver, goal, env) {
@@ -1919,7 +1997,12 @@ function* atomLengthBuiltin({ goal, env }) {
1919
1997
  if (unify(goal.args[1], numberTerm(characters(value.name).length), next)) yield next;
1920
1998
  }
1921
1999
 
1922
- 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) {
1923
2006
  const first = deref(goal.args[0], env);
1924
2007
  const second = deref(goal.args[1], env);
1925
2008
  const whole = deref(goal.args[2], env);
@@ -1942,11 +2025,16 @@ function* atomConcatBuiltin({ goal, env }) {
1942
2025
  } else if (second.type === ATOM && whole.type === ATOM && whole.name.endsWith(second.name)) {
1943
2026
  candidates.push([whole.name.slice(0, whole.name.length - second.name.length), second.name, whole.name]);
1944
2027
  }
1945
- for (const [a, b, c] of candidates) {
2028
+ for (let index = 0; index < candidates.length; index++) {
2029
+ const [a, b, c] = candidates[index];
1946
2030
  const next = env.clone();
1947
2031
  if (unify(goal.args[0], atom(a), next) && unify(goal.args[1], atom(b), next) &&
1948
- 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
+ }
1949
2036
  }
2037
+ state.pending = false;
1950
2038
  }
1951
2039
 
1952
2040
  function optionalInteger(term, env) {
@@ -1956,7 +2044,12 @@ function optionalInteger(term, env) {
1956
2044
  return BigInt(value.name);
1957
2045
  }
1958
2046
 
1959
- 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) {
1960
2053
  const source = deref(goal.args[0], env);
1961
2054
  if (source.type === VAR) throw new PrologError('instantiation_error');
1962
2055
  if (source.type !== ATOM) throw new PrologError('type_error(atom)', source);
@@ -1977,11 +2070,34 @@ function* subAtomBuiltin({ goal, env }) {
1977
2070
  throw new PrologError('domain_error(not_less_than_zero)', deref(goal.args[3], env));
1978
2071
  }
1979
2072
  const chars = characters(source.name);
1980
- for (let start = 0; start <= chars.length; start++) {
1981
- if (before != null && before !== BigInt(start)) continue;
1982
- 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];
1983
2100
  const remaining = chars.length - start - size;
1984
- if (length != null && length !== BigInt(size)) continue;
1985
2101
  if (after != null && after !== BigInt(remaining)) continue;
1986
2102
  const text = chars.slice(start, start + size).join('');
1987
2103
  if (sub.type === ATOM && sub.name !== text) continue;
@@ -1989,9 +2105,13 @@ function* subAtomBuiltin({ goal, env }) {
1989
2105
  if (unify(goal.args[1], numberTerm(start), next) &&
1990
2106
  unify(goal.args[2], numberTerm(size), next) &&
1991
2107
  unify(goal.args[3], numberTerm(remaining), next) &&
1992
- 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
+ }
1993
2112
  }
1994
2113
  }
2114
+ state.pending = false;
1995
2115
  }
1996
2116
 
1997
2117
  function listElements(term, env) {
@@ -2406,42 +2526,52 @@ function sortedUnique(items) {
2406
2526
  }
2407
2527
 
2408
2528
  function allSolutionsBuiltin(asSet) {
2409
- return function* ({ solver, goal, env }) {
2410
- // EyeProlog validates the iterated goal before Instances. ISO 7.12 makes
2411
- // simultaneous-error selection implementation dependent; keep this stable as
2412
- // a processor choice rather than describing it as mandatory table ordering.
2413
- const { iterated, quantified } = bagGoalParts(goal.args[1], env);
2414
- assertListOrPartial(goal.args[2], env);
2415
- const free = freeVariables(iterated, goal.args[0], quantified, env);
2416
- const collector = solver.cloneForInnerGoal(10000000);
2417
- const groups = [];
2418
- for (const answerEnv of collector.solve([iterated], env.clone(), 0)) {
2419
- const copied = freshCopy(compound('$bag', [
2420
- compound('$witness', free),
2421
- goal.args[0],
2422
- ]), answerEnv);
2423
- let group = groups.find((candidate) => sameWitness(candidate.witness, copied.args[0]));
2424
- if (!group) {
2425
- group = { witness: copied.args[0], templates: [] };
2426
- groups.push(group);
2427
- group.templates.push(copied.args[1]);
2428
- } else {
2429
- const alignment = new Env();
2430
- unify(copied.args[0], group.witness, alignment);
2431
- group.templates.push(copyResolved(copied.args[1], alignment));
2432
- }
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));
2433
2558
  }
2434
- solver.absorbStatsFrom(collector);
2435
- for (const group of groups) {
2436
- const next = env.clone();
2437
- let matches = true;
2438
- for (let i = 0; i < free.length; i++) {
2439
- if (!unify(free[i], group.witness.args[i], next)) { matches = false; break; }
2440
- }
2441
- const templates = asSet ? sortedUnique(group.templates) : group.templates;
2442
- 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; }
2443
2567
  }
2444
- };
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;
2445
2575
  }
2446
2576
  const bagofBuiltin = allSolutionsBuiltin(false);
2447
2577
  const setofBuiltin = allSolutionsBuiltin(true);
@@ -2964,8 +3094,12 @@ function* onceBuiltin({ solver, goal, env }) {
2964
3094
  }
2965
3095
  solver.absorbStatsFrom(child);
2966
3096
  }
2967
- function* repeatBuiltin({ env }) {
2968
- 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;
2969
3103
  }
2970
3104
  function* negationBuiltin({ solver, goal, env }) {
2971
3105
  const invoked = callable(goal.args[0], env);
package/src/quads.js CHANGED
@@ -1,6 +1,8 @@
1
1
  // Embedded quad tests: a query followed by one or more answer descriptions.
2
2
  // The syntax follows the portable "queries using answer descriptions" format
3
3
  // used by Trealla and the ISO Prolog working examples linked from issue #1.
4
+ // A `maybe` annotation denotes a successful answer with residual constraints;
5
+ // it does not relax the ordinary answer-substitution comparison.
4
6
  import {
5
7
  ATOM, COMPOUND, VAR, Env, atom, compound, copyResolved, deref,
6
8
  flattenConjunction, listFromItems, properListItems, termIsGround,
@@ -203,6 +205,7 @@ function describeLeaf(term) {
203
205
  sto: false,
204
206
  false: false,
205
207
  truth: false,
208
+ maybe: false,
206
209
  loops: false,
207
210
  waits: false,
208
211
  error: null,
@@ -218,6 +221,7 @@ function describeLeaf(term) {
218
221
  if (item.name === 'unexpected' || item.name === 'inattendue') leaf.unexpected = true;
219
222
  else if (item.name === '...' || item.name === 'ad_infinitum') leaf.more = true;
220
223
  else if (item.name === 'sto') leaf.sto = true;
224
+ else if (item.name === 'maybe') leaf.maybe = true;
221
225
  else if (item.name === 'loops') leaf.loops = true;
222
226
  else if (item.name === 'waits') leaf.waits = true;
223
227
  else if (item.name === 'other_answer_sequence') {
@@ -253,10 +257,11 @@ function describeLeaf(term) {
253
257
  if (isErrorDescription(item)) leaf.error = item;
254
258
  else leaf.malformed ??= item;
255
259
  }
256
- leaf.hasExpectation = leaf.bindings.length > 0 || leaf.truth || leaf.false || leaf.loops || leaf.waits ||
260
+ leaf.hasExpectation = leaf.bindings.length > 0 || leaf.truth || leaf.false || leaf.maybe || leaf.loops || leaf.waits ||
257
261
  leaf.error != null || leaf.output != null;
258
262
  if (!leaf.hasExpectation && !leaf.more && !leaf.sto && leaf.unsupported == null) leaf.malformed ??= term;
259
263
  if ([leaf.false, leaf.truth, leaf.error != null].filter(Boolean).length > 1) leaf.malformed ??= term;
264
+ if (leaf.maybe && (leaf.false || leaf.loops || leaf.waits || leaf.error != null)) leaf.malformed ??= term;
260
265
  return leaf;
261
266
  }
262
267
 
@@ -386,9 +391,26 @@ function matchLeaf(program, query, leaf, actual, position) {
386
391
  }
387
392
  const solution = actual.solutions[position];
388
393
  if (!solution || !outputMatches(program, leaf.output, solution.output)) return false;
394
+ // Portable answer descriptions distinguish plain success from success with
395
+ // residual constraints. `maybe` requires at least one pending residue, while
396
+ // its absence requires an unconstrained answer. In either case the stated
397
+ // substitutions remain exact and are checked below.
398
+ if (leaf.maybe !== hasPendingConstraints(solution.env)) return false;
389
399
  return substitutionMatches(query, leaf.bindings, solution.env);
390
400
  }
391
401
 
402
+ function hasPendingConstraints(env) {
403
+ for (const constraint of env.variableConstraints?.() ?? []) {
404
+ if (constraint.status?.(env) === 'pending') return true;
405
+ }
406
+ // Prolog attributed variables and delayed goals are the other residual-state
407
+ // mechanisms exposed by call_residue_vars/2. Treat either as pending residue
408
+ // even when a library does not provide a printable projection goal.
409
+ if ((env.attributedVariableNames?.() ?? []).length > 0) return true;
410
+ if ((env.delayedVariableNames?.() ?? []).length > 0) return true;
411
+ return false;
412
+ }
413
+
392
414
  function descriptionDeclaresSto(description) {
393
415
  return splitOperator(description, '|').some((alternative) =>
394
416
  splitOperator(alternative, ';').some((term) => describeLeaf(term).sto));
@@ -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',