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 +6 -1
- package/package.json +1 -1
- package/src/iso.js +228 -66
- package/src/scryer-compat.js +22 -12
- package/src/solver.js +100 -82
- package/src/standard-library.js +15 -3
- package/src/term.js +135 -13
- package/test/run-regression.mjs +109 -16
- package/the-art-of-eyeprolog.md +10 -9
package/index.d.ts
CHANGED
|
@@ -182,7 +182,12 @@ export interface BuiltinDefinition {
|
|
|
182
182
|
eyePrologLibrary: boolean;
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
-
export
|
|
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
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
|
-
|
|
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
|
|
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
|
-
|
|
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))
|
|
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
|
|
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 (
|
|
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))
|
|
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
|
|
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
|
-
|
|
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))
|
|
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
|
|
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
|
-
|
|
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))
|
|
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
|
|
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
|
-
|
|
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))
|
|
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
|
|
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
|
-
|
|
1289
|
-
|
|
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
|
|
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 (
|
|
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))
|
|
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
|
|
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
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
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))
|
|
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
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
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
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
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
|
|
2940
|
-
|
|
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);
|
package/src/scryer-compat.js
CHANGED
|
@@ -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
|
|
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
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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))
|
|
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
|
|
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
|
|
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))
|
|
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
|
-
|
|
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
|
-
|
|
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))
|
|
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
|
-
//
|
|
1472
|
-
//
|
|
1473
|
-
//
|
|
1474
|
-
//
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
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))
|
|
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:
|
|
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:
|
|
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))
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
function
|
|
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.
|
package/src/standard-library.js
CHANGED
|
@@ -274,7 +274,14 @@ function* statisticsBuiltin({ solver, env }) {
|
|
|
274
274
|
yield env;
|
|
275
275
|
}
|
|
276
276
|
|
|
277
|
-
function
|
|
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 (
|
|
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))
|
|
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
|
-
//
|
|
77
|
-
// an
|
|
78
|
-
//
|
|
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
|
-
|
|
778
|
+
transferCompactVariableOrigins(b, a, env);
|
|
697
779
|
env?.finalizePrologAttributeAlias?.(b.name, a.name);
|
|
698
780
|
env.bind(b.name, a);
|
|
699
781
|
} else {
|
|
700
|
-
|
|
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
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
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
|
|
package/test/run-regression.mjs
CHANGED
|
@@ -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
|
|
344
|
-
'?- A = [[]], B = [], dif([
|
|
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
|
|
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(),
|
|
2294
|
-
'unannotated
|
|
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
|
-
'
|
|
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: '
|
|
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])
|
|
2317
|
-
|
|
2318
|
-
'
|
|
2319
|
-
'
|
|
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"
|
|
2326
|
-
'
|
|
2327
|
-
'?-
|
|
2328
|
-
'?- L = "i".\n' +
|
|
2354
|
+
'?- L = "i", Det = preparing\n' +
|
|
2355
|
+
'; L = "i".\n' +
|
|
2356
|
+
'?- Det = yes.\n' +
|
|
2329
2357
|
'?- ',
|
|
2330
|
-
'
|
|
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
|
{
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -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
|
|
2021
|
-
the solver
|
|
2022
|
-
|
|
2023
|
-
`hasPendingAlternatives()
|
|
2024
|
-
|
|
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.
|
|
8019
|
-
|
|
8020
|
-
|
|
8021
|
-
|
|
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
|