eyeprolog 1.3.77 → 1.3.78
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/iso.js +43 -10
- package/src/solver.js +70 -19
- package/test/run-regression.mjs +35 -0
package/package.json
CHANGED
package/src/iso.js
CHANGED
|
@@ -65,9 +65,9 @@ export const isoBuiltins = {
|
|
|
65
65
|
registry.add('=..', 2, univBuiltin, { deterministic: true });
|
|
66
66
|
registry.add('copy_term', 2, copyTermBuiltin, { deterministic: true });
|
|
67
67
|
registry.add('term_variables', 2, termVariablesBuiltin, { deterministic: true });
|
|
68
|
-
registry.add('findall', 3, findallBuiltin);
|
|
69
|
-
registry.add('bagof', 3, bagofBuiltin);
|
|
70
|
-
registry.add('setof', 3, setofBuiltin);
|
|
68
|
+
registry.add('findall', 3, findallBuiltin, { deterministic: true });
|
|
69
|
+
registry.add('bagof', 3, bagofBuiltin, { deterministicWhen: allSolutionsHasAtMostOneGroup });
|
|
70
|
+
registry.add('setof', 3, setofBuiltin, { deterministicWhen: allSolutionsHasAtMostOneGroup });
|
|
71
71
|
registry.add('clause', 2, clauseBuiltin, {
|
|
72
72
|
shouldUse: ({ solver }) => solver.program.findGroup('clause', 2) == null,
|
|
73
73
|
});
|
|
@@ -2367,6 +2367,16 @@ function allSolutionsBuiltin(asSet) {
|
|
|
2367
2367
|
const bagofBuiltin = allSolutionsBuiltin(false);
|
|
2368
2368
|
const setofBuiltin = allSolutionsBuiltin(true);
|
|
2369
2369
|
|
|
2370
|
+
function allSolutionsHasAtMostOneGroup({ goal, env }) {
|
|
2371
|
+
try {
|
|
2372
|
+
const { iterated, quantified } = bagGoalParts(goal.args[1], env);
|
|
2373
|
+
return freeVariables(iterated, goal.args[0], quantified, env).length === 0;
|
|
2374
|
+
} catch (_) {
|
|
2375
|
+
// Leave error selection and reporting to the builtin itself.
|
|
2376
|
+
return false;
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
|
|
2370
2380
|
function callable(term, env) {
|
|
2371
2381
|
term = deref(term, env);
|
|
2372
2382
|
if (term.type === VAR) throw new PrologError('instantiation_error');
|
|
@@ -2823,35 +2833,57 @@ function* negationBuiltin({ solver, goal, env }) {
|
|
|
2823
2833
|
for (const _ of solver.cloneForInnerGoal(1).solve([invoked], env.clone(), 0)) return;
|
|
2824
2834
|
yield env;
|
|
2825
2835
|
}
|
|
2826
|
-
function* solveControlBranch(solver, goal, env) {
|
|
2836
|
+
function* solveControlBranch(solver, goal, env, observePending = null) {
|
|
2837
|
+
const existingStacks = new Set(solver.solveStacks);
|
|
2827
2838
|
for (const answer of solver.solve([callable(goal, env)], env, 0)) {
|
|
2828
2839
|
// A branch answer is internal to its enclosing control construct. The
|
|
2829
2840
|
// surrounding solve will count the completed control goal after the
|
|
2830
2841
|
// builtin yields it. Leaving both counts in place makes a bounded search
|
|
2831
2842
|
// such as once/1 or negation stop before it can observe the branch answer.
|
|
2832
2843
|
if (solver.solutionsSeen > 0) solver.solutionsSeen--;
|
|
2844
|
+
observePending?.(solver.hasPendingAlternatives(existingStacks));
|
|
2833
2845
|
yield answer;
|
|
2834
2846
|
}
|
|
2835
2847
|
}
|
|
2836
|
-
function
|
|
2848
|
+
function disjunctionBuiltin(context) {
|
|
2849
|
+
const state = { pending: true };
|
|
2850
|
+
const iterator = disjunctionSolutions(context, state);
|
|
2851
|
+
iterator.hasPendingAlternatives = () => state.pending;
|
|
2852
|
+
return iterator;
|
|
2853
|
+
}
|
|
2854
|
+
function* disjunctionSolutions({ solver, goal, env }, state) {
|
|
2837
2855
|
const left = deref(goal.args[0], env);
|
|
2838
2856
|
if (left.type === COMPOUND && left.name === '->' && left.arity === 2) {
|
|
2839
2857
|
for (const conditionEnv of solver.cloneForInnerGoal(1).solve([callable(left.args[0], env)], env.clone(), 0)) {
|
|
2840
|
-
yield* solveControlBranch(solver, left.args[1], conditionEnv
|
|
2858
|
+
yield* solveControlBranch(solver, left.args[1], conditionEnv,
|
|
2859
|
+
(pending) => { state.pending = pending; });
|
|
2860
|
+
state.pending = false;
|
|
2841
2861
|
return;
|
|
2842
2862
|
}
|
|
2843
|
-
yield* solveControlBranch(solver, goal.args[1], env.clone()
|
|
2863
|
+
yield* solveControlBranch(solver, goal.args[1], env.clone(),
|
|
2864
|
+
(pending) => { state.pending = pending; });
|
|
2865
|
+
state.pending = false;
|
|
2844
2866
|
return;
|
|
2845
2867
|
}
|
|
2846
2868
|
const marker = solver.active[solver.active.length - 1] ?? null;
|
|
2847
2869
|
const markerCutEpoch = marker?.cutEpoch ?? 0;
|
|
2848
2870
|
const solverCutEpoch = solver.cutEpoch;
|
|
2849
|
-
yield* solveControlBranch(solver, goal.args[0], env.clone())
|
|
2871
|
+
yield* solveControlBranch(solver, goal.args[0], env.clone(), (pending) => {
|
|
2872
|
+
const cutThisScope = marker == null
|
|
2873
|
+
? solver.cutEpoch !== solverCutEpoch
|
|
2874
|
+
: (marker.cutEpoch ?? 0) !== markerCutEpoch;
|
|
2875
|
+
state.pending = pending || !cutThisScope;
|
|
2876
|
+
});
|
|
2850
2877
|
const cutThisScope = marker == null
|
|
2851
2878
|
? solver.cutEpoch !== solverCutEpoch
|
|
2852
2879
|
: (marker.cutEpoch ?? 0) !== markerCutEpoch;
|
|
2853
|
-
if (cutThisScope)
|
|
2854
|
-
|
|
2880
|
+
if (cutThisScope) {
|
|
2881
|
+
state.pending = false;
|
|
2882
|
+
return;
|
|
2883
|
+
}
|
|
2884
|
+
yield* solveControlBranch(solver, goal.args[1], env.clone(),
|
|
2885
|
+
(pending) => { state.pending = pending; });
|
|
2886
|
+
state.pending = false;
|
|
2855
2887
|
}
|
|
2856
2888
|
function* ifThenBuiltin({ solver, goal, env }) {
|
|
2857
2889
|
for (const conditionEnv of solver.cloneForInnerGoal(1).solve([callable(goal.args[0], env)], env.clone(), 0)) {
|
|
@@ -2875,6 +2907,7 @@ export class BuiltinRegistry {
|
|
|
2875
2907
|
arity,
|
|
2876
2908
|
handler,
|
|
2877
2909
|
deterministic: options.deterministic ?? false,
|
|
2910
|
+
deterministicWhen: options.deterministicWhen ?? null,
|
|
2878
2911
|
ready: options.ready ?? null,
|
|
2879
2912
|
fallbackWhenNotReady: options.fallbackWhenNotReady ?? false,
|
|
2880
2913
|
shouldUse: options.shouldUse ?? null,
|
package/src/solver.js
CHANGED
|
@@ -607,14 +607,16 @@ export class Solver {
|
|
|
607
607
|
const def = callable ? this.registry.get(goal.name, goal.arity) : null;
|
|
608
608
|
this.active = active;
|
|
609
609
|
if (def && builtinIsReadyOrAuthoritative(def, this, goal, env)) {
|
|
610
|
+
const deterministic = def.deterministic ||
|
|
611
|
+
def.deterministicWhen?.({ solver: this, goal, env }) === true;
|
|
610
612
|
const iterator = def.handler({ solver: this, goal, env });
|
|
611
613
|
const firstResult = iterator.next();
|
|
612
|
-
if (
|
|
614
|
+
if (deterministic) {
|
|
613
615
|
if (!firstResult.done) this.stats.deterministic_builtin_successes++;
|
|
614
616
|
else this.stats.deterministic_builtin_failures++;
|
|
615
617
|
}
|
|
616
618
|
if (firstResult.done) break;
|
|
617
|
-
if (!
|
|
619
|
+
if (!deterministic) {
|
|
618
620
|
stack.push({
|
|
619
621
|
kind: 'resumeBuiltin',
|
|
620
622
|
iterator,
|
|
@@ -819,11 +821,22 @@ export class Solver {
|
|
|
819
821
|
}
|
|
820
822
|
}
|
|
821
823
|
|
|
822
|
-
hasPendingAlternatives() {
|
|
823
|
-
// When solve() is suspended at an answer, active solve stacks contain
|
|
824
|
-
// unexplored work
|
|
825
|
-
//
|
|
826
|
-
|
|
824
|
+
hasPendingAlternatives(excludedStacks = null) {
|
|
825
|
+
// When solve() is suspended at an answer, active solve stacks contain
|
|
826
|
+
// unexplored work or an iterator frame that can prove it just yielded its
|
|
827
|
+
// final answer. The demand-driven REPL uses this without speculatively
|
|
828
|
+
// pulling the next answer. Control builtins can exclude their caller's
|
|
829
|
+
// pre-existing stacks when inspecting a nested branch.
|
|
830
|
+
return this.solveStacks.some((stack) => !excludedStacks?.has(stack) && stack.some((frame) => {
|
|
831
|
+
if (frame.kind === 'resumeBuiltin' &&
|
|
832
|
+
typeof frame.iterator?.hasPendingAlternatives === 'function') {
|
|
833
|
+
return frame.iterator.hasPendingAlternatives();
|
|
834
|
+
}
|
|
835
|
+
if (frame.kind === 'userClause' && headCannotMatch(frame.goal, frame.clause.head, frame.env)) {
|
|
836
|
+
return false;
|
|
837
|
+
}
|
|
838
|
+
return true;
|
|
839
|
+
}));
|
|
827
840
|
}
|
|
828
841
|
|
|
829
842
|
fastCountGoal(goal, env) {
|
|
@@ -1440,7 +1453,10 @@ function bundledBetweenIterator(solver, group, goal, env) {
|
|
|
1440
1453
|
// the generated value in the caller repeatedly revisits all earlier frames.
|
|
1441
1454
|
// Enumerate the canonical bundled relation directly while leaving user
|
|
1442
1455
|
// definitions and non-EyeProlog registries on the ordinary Prolog path.
|
|
1443
|
-
|
|
1456
|
+
const state = { pending: true };
|
|
1457
|
+
const iterator = bundledBetweenSolutions(solver, goal, env, state);
|
|
1458
|
+
iterator.hasPendingAlternatives = () => state.pending;
|
|
1459
|
+
return iterator;
|
|
1444
1460
|
}
|
|
1445
1461
|
|
|
1446
1462
|
function requireBetweenInteger(term, env) {
|
|
@@ -1452,7 +1468,7 @@ function requireBetweenInteger(term, env) {
|
|
|
1452
1468
|
return BigInt(value.name);
|
|
1453
1469
|
}
|
|
1454
1470
|
|
|
1455
|
-
function* bundledBetweenSolutions(solver, goal, env) {
|
|
1471
|
+
function* bundledBetweenSolutions(solver, goal, env, state) {
|
|
1456
1472
|
const lower = requireBetweenInteger(goal.args[0], env);
|
|
1457
1473
|
const upper = requireBetweenInteger(goal.args[1], env);
|
|
1458
1474
|
const requested = deref(goal.args[2], env);
|
|
@@ -1462,15 +1478,23 @@ function* bundledBetweenSolutions(solver, goal, env) {
|
|
|
1462
1478
|
throw new PrologError('type_error(integer)', requested);
|
|
1463
1479
|
}
|
|
1464
1480
|
const value = BigInt(requested.name);
|
|
1465
|
-
if (value >= lower && value <= upper)
|
|
1481
|
+
if (value >= lower && value <= upper) {
|
|
1482
|
+
state.pending = false;
|
|
1483
|
+
yield env;
|
|
1484
|
+
}
|
|
1485
|
+
state.pending = false;
|
|
1466
1486
|
return;
|
|
1467
1487
|
}
|
|
1468
1488
|
|
|
1469
1489
|
for (let value = lower; value <= upper; value++) {
|
|
1470
1490
|
const next = env.clone();
|
|
1471
1491
|
solver.stats.unify_calls++;
|
|
1472
|
-
if (unify(goal.args[2], numberTerm(value), next))
|
|
1492
|
+
if (unify(goal.args[2], numberTerm(value), next)) {
|
|
1493
|
+
state.pending = value < upper;
|
|
1494
|
+
yield next;
|
|
1495
|
+
}
|
|
1473
1496
|
}
|
|
1497
|
+
state.pending = false;
|
|
1474
1498
|
}
|
|
1475
1499
|
|
|
1476
1500
|
function bundledMemberIterator(solver, group, goal, env) {
|
|
@@ -1487,17 +1511,33 @@ function bundledMemberIterator(solver, group, goal, env) {
|
|
|
1487
1511
|
let cursor = deref(goal.args[1], env);
|
|
1488
1512
|
while (isCons(cursor)) cursor = deref(cursor.args[1], env);
|
|
1489
1513
|
if (!isEmptyList(cursor)) return null;
|
|
1490
|
-
|
|
1514
|
+
const state = { pending: true };
|
|
1515
|
+
const iterator = bundledMemberSolutions(solver, goal, env, state);
|
|
1516
|
+
iterator.hasPendingAlternatives = () => state.pending;
|
|
1517
|
+
return iterator;
|
|
1491
1518
|
}
|
|
1492
1519
|
|
|
1493
|
-
function* bundledMemberSolutions(solver, goal, env) {
|
|
1520
|
+
function* bundledMemberSolutions(solver, goal, env, state) {
|
|
1494
1521
|
let cursor = deref(goal.args[1], env);
|
|
1495
1522
|
while (isCons(cursor)) {
|
|
1523
|
+
const item = cursor.args[0];
|
|
1524
|
+
cursor = deref(cursor.args[1], env);
|
|
1496
1525
|
const next = env.clone();
|
|
1497
1526
|
solver.stats.unify_calls++;
|
|
1498
|
-
if (unify(goal.args[0],
|
|
1527
|
+
if (unify(goal.args[0], item, next)) {
|
|
1528
|
+
state.pending = bundledMemberMayMatch(goal.args[0], cursor, env);
|
|
1529
|
+
yield next;
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
state.pending = false;
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
function bundledMemberMayMatch(target, cursor, env) {
|
|
1536
|
+
while (isCons(cursor)) {
|
|
1537
|
+
if (!goalHeadTermsCannotMatch(target, cursor.args[0], env)) return true;
|
|
1499
1538
|
cursor = deref(cursor.args[1], env);
|
|
1500
1539
|
}
|
|
1540
|
+
return false;
|
|
1501
1541
|
}
|
|
1502
1542
|
|
|
1503
1543
|
function bundledEllipsisPlan(solver, group, goal, rest, env) {
|
|
@@ -2849,11 +2889,22 @@ function headCannotMatch(goal, head, env) {
|
|
|
2849
2889
|
if (goal.type !== COMPOUND || head.type !== COMPOUND) return false;
|
|
2850
2890
|
if (goal.name !== head.name || goal.arity !== head.arity) return true;
|
|
2851
2891
|
for (let i = 0; i < goal.arity; i++) {
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2892
|
+
if (goalHeadTermsCannotMatch(goal.args[i], head.args[i], env)) return true;
|
|
2893
|
+
}
|
|
2894
|
+
return false;
|
|
2895
|
+
}
|
|
2896
|
+
|
|
2897
|
+
function goalHeadTermsCannotMatch(goalTerm, headTerm, env) {
|
|
2898
|
+
const pending = [[goalTerm, headTerm]];
|
|
2899
|
+
while (pending.length > 0) {
|
|
2900
|
+
const [goalItem, headItem] = pending.pop();
|
|
2901
|
+
const actual = derefForLocal(goalItem, env);
|
|
2902
|
+
if (actual.type === VAR || headItem.type === VAR) continue;
|
|
2903
|
+
if (actual.type !== headItem.type || actual.name !== headItem.name || actual.arity !== headItem.arity) return true;
|
|
2904
|
+
if (actual.type !== COMPOUND) continue;
|
|
2905
|
+
for (let index = 0; index < actual.arity; index++) {
|
|
2906
|
+
pending.push([actual.args[index], headItem.args[index]]);
|
|
2907
|
+
}
|
|
2857
2908
|
}
|
|
2858
2909
|
return false;
|
|
2859
2910
|
}
|
package/test/run-regression.mjs
CHANGED
|
@@ -1826,6 +1826,41 @@ c4 ?- call((!;1)).
|
|
|
1826
1826
|
assertEqual(result.stderr, '', 'stderr');
|
|
1827
1827
|
},
|
|
1828
1828
|
},
|
|
1829
|
+
{
|
|
1830
|
+
name: 'REPL omits exhausted choicepoints without probing future branches (issue #74)',
|
|
1831
|
+
run: () => {
|
|
1832
|
+
const result = runCli([], {
|
|
1833
|
+
input:
|
|
1834
|
+
'X=1;X=2.\n;\n' +
|
|
1835
|
+
'setof(X,true,Xs).\n' +
|
|
1836
|
+
'findall(t,false,Xs).\n' +
|
|
1837
|
+
'use_module(library(lists)).\n' +
|
|
1838
|
+
'member(a,"ba").\n' +
|
|
1839
|
+
'append("a",Xs,Xs0).\n' +
|
|
1840
|
+
'halt.\n',
|
|
1841
|
+
});
|
|
1842
|
+
assertEqual(result.status, 0, 'exit status');
|
|
1843
|
+
assertEqual(result.stdout,
|
|
1844
|
+
'?- X = 1\n' +
|
|
1845
|
+
'; X = 2.\n' +
|
|
1846
|
+
'?- Xs = [_A].\n' +
|
|
1847
|
+
'?- Xs = [].\n' +
|
|
1848
|
+
'?- true.\n' +
|
|
1849
|
+
'?- true.\n' +
|
|
1850
|
+
'?- Xs0 = [a | Xs].\n' +
|
|
1851
|
+
'?- ',
|
|
1852
|
+
'stdout');
|
|
1853
|
+
assertEqual(result.stderr, '', 'stderr');
|
|
1854
|
+
|
|
1855
|
+
const grouped = new Solver(Program.parse(''));
|
|
1856
|
+
const groupedSolutions = grouped.solve([
|
|
1857
|
+
parseGoalText('setof(X,(Y=1,X=a;Y=2,X=b),Xs)'),
|
|
1858
|
+
], new Env(), 0);
|
|
1859
|
+
assertEqual(groupedSolutions.next().done, false, 'first grouped setof/3 answer');
|
|
1860
|
+
assertEqual(grouped.hasPendingAlternatives(), true, 'genuine grouped setof/3 choicepoint');
|
|
1861
|
+
groupedSolutions.return();
|
|
1862
|
+
},
|
|
1863
|
+
},
|
|
1829
1864
|
{
|
|
1830
1865
|
name: 'REPL hides aliases to fresh throw variables while preserving query aliases',
|
|
1831
1866
|
run: () => {
|