eyeprolog 1.5.66 → 1.5.68

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.5.66",
6
+ "version": "1.5.68",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
package/src/lib/si.pl CHANGED
@@ -70,12 +70,7 @@ si__condition((A;B)) :- si__condition(A), si__condition(B).
70
70
  % f(a) vs f(Y) and X vs 1 are genuinely undecided.
71
71
  compare_si(Order, A, B) :-
72
72
  si__require_order(Order),
73
- ( A == B
74
- -> Order = (=)
75
- ; si__order_decided(A, B)
76
- -> compare(Order, A, B)
77
- ; throw(error(instantiation_error, [predicate-compare_si/3]))
78
- ).
73
+ si__compare(Order, [A-B]).
79
74
 
80
75
  si__require_order(Order) :- var(Order), !.
81
76
  si__require_order(Order) :-
@@ -86,25 +81,38 @@ si__require_order(Order) :-
86
81
  ; throw(error(type_error(atom, Order), [predicate-compare_si/3]))
87
82
  ).
88
83
 
89
- % Both arguments are known to be non-identical here.
90
- si__order_decided(A, B) :- ( var(A) ; var(B) ), !, fail.
91
- si__order_decided(A, B) :-
84
+ % Keep pending argument pairs in a work list. Recursing inside an if-then-else
85
+ % condition retains a child Solver per list cell; testing whole-tail identity
86
+ % at each step also repeatedly scans the same suffix (issue #105).
87
+ si__compare(=, []).
88
+ si__compare(Order, [A-B|Pairs]) :-
89
+ si__compare_pair(A, B, Comparison, Pairs, Next),
90
+ si__compare_next(Comparison, Order, Next).
91
+
92
+ si__compare_next(=, Order, Pairs) :- si__compare(Order, Pairs).
93
+ si__compare_next(<, <, _).
94
+ si__compare_next(>, >, _).
95
+
96
+ si__compare_pair(A, B, =, Pairs, Pairs) :-
97
+ ( var(A) ; var(B) ), !,
98
+ ( A == B -> true
99
+ ; throw(error(instantiation_error, [predicate-compare_si/3])) ).
100
+ si__compare_pair([A|As], [B|Bs], =, Pairs, [A-B,As-Bs|Pairs]) :- !.
101
+ si__compare_pair(A, B, Comparison, Pairs, Next) :-
92
102
  compound(A), compound(B), !,
93
103
  functor(A, NameA, ArityA),
94
104
  functor(B, NameB, ArityB),
95
- ( ArityA =\= ArityB -> true
96
- ; NameA \== NameB -> true
105
+ ( ArityA =\= ArityB -> compare(Comparison, ArityA, ArityB), Next = Pairs
106
+ ; NameA \== NameB -> compare(Comparison, NameA, NameB), Next = Pairs
97
107
  ; A =.. [_|ArgsA],
98
108
  B =.. [_|ArgsB],
99
- si__args_decided(ArgsA, ArgsB)
100
- ).
101
- % Otherwise both are non-variables and at least one is atomic, so either the
102
- % type order or the values themselves settle it and no instantiation can
103
- % change the outcome.
104
- si__order_decided(_, _).
105
-
106
- si__args_decided([A|As], [B|Bs]) :-
107
- ( A == B
108
- -> si__args_decided(As, Bs)
109
- ; si__order_decided(A, B)
109
+ Comparison = (=),
110
+ si__prepend_pairs(ArgsA, ArgsB, Next, Pairs)
110
111
  ).
112
+ si__compare_pair(A, _, >, Pairs, Pairs) :- compound(A), !.
113
+ si__compare_pair(_, B, <, Pairs, Pairs) :- compound(B), !.
114
+ si__compare_pair(A, B, Comparison, Pairs, Pairs) :- compare(Comparison, A, B).
115
+
116
+ si__prepend_pairs([], [], Pairs, Pairs).
117
+ si__prepend_pairs([A|As], [B|Bs], [A-B|Pairs], Tail) :-
118
+ si__prepend_pairs(As, Bs, Pairs, Tail).
package/src/program.js CHANGED
@@ -320,7 +320,7 @@ export class Program {
320
320
  // Numeric closure modes are a widely implemented compatibility
321
321
  // extension. Keep their existing hidden lexical qualification while
322
322
  // reserving explicit Module:Goal wrapping for ISO Part 2 ':' modes.
323
- modes.push({ index, kind: 'closure' });
323
+ modes.push({ index, kind: 'closure', extraArguments: Number(spec.name) });
324
324
  }
325
325
  }
326
326
  const definitions = this.moduleMetaPredicates.get(module) ?? new Map();
@@ -1179,6 +1179,12 @@ function collectAutoloadGoalDependencies(goal, out = []) {
1179
1179
  return out;
1180
1180
  }
1181
1181
  if (goal?.type !== COMPOUND) return out;
1182
+ if (goal.name === ':' && goal.arity === 2 && goal.args[0]?.type === ATOM) {
1183
+ const start = out.length;
1184
+ collectAutoloadGoalDependencies(goal.args[1], out);
1185
+ for (let i = start; i < out.length; i++) out[i].module ??= goal.args[0].name;
1186
+ return out;
1187
+ }
1182
1188
  if (goal.name === ',' && goal.arity === 2) {
1183
1189
  collectAutoloadGoalDependencies(goal.args[0], out);
1184
1190
  collectAutoloadGoalDependencies(goal.args[1], out);
@@ -1198,7 +1204,7 @@ function collectAutoloadGoalDependencies(goal, out = []) {
1198
1204
  return out;
1199
1205
  }
1200
1206
 
1201
- out.push({ key: `${goal.name}/${goal.arity}`, name: goal.name, arity: goal.arity, module: goal.module });
1207
+ out.push({ key: `${goal.name}/${goal.arity}`, name: goal.name, arity: goal.arity, module: goal.module, goal });
1202
1208
 
1203
1209
  if (goal.name === 'forall' && goal.arity === 2) {
1204
1210
  collectAutoloadGoalDependencies(goal.args[0], out);
@@ -1219,12 +1225,50 @@ function collectAutoloadGoalDependencies(goal, out = []) {
1219
1225
  collectAutoloadGoalDependencies(goal.args[2], out);
1220
1226
  } else if ((goal.name === 'call_cleanup' || goal.name === 'setup_call_cleanup') && (goal.arity === 2 || goal.arity === 3)) {
1221
1227
  for (const arg of goal.args) collectAutoloadGoalDependencies(arg, out);
1222
- } else if ((goal.name === 'call' || goal.name === 'time') && goal.arity === 1) {
1223
- collectAutoloadGoalDependencies(goal.args[0], out);
1228
+ } else if (goal.name === 'call' && goal.arity >= 1) {
1229
+ collectAutoloadClosureDependencies(goal.args[0], goal.arity - 1, out, goal.module);
1224
1230
  }
1225
1231
  return out;
1226
1232
  }
1227
1233
 
1234
+ function collectAutoloadClosureDependencies(closure, extraArguments, out, module) {
1235
+ if (closure?.type === COMPOUND && closure.name === ':' && closure.arity === 2 && closure.args[0]?.type === ATOM) {
1236
+ collectAutoloadClosureDependencies(closure.args[1], extraArguments, out, closure.args[0].name);
1237
+ return;
1238
+ }
1239
+ if (closure?.type !== ATOM && closure?.type !== COMPOUND) return;
1240
+ const start = out.length;
1241
+ if (extraArguments === 0) {
1242
+ collectAutoloadGoalDependencies(closure, out);
1243
+ } else {
1244
+ const arity = closure.arity + extraArguments;
1245
+ // Extra closure arguments are unknown here. Record the final indicator
1246
+ // without allocating placeholder terms or treating data arguments as goals.
1247
+ out.push({ key: `${closure.name}/${arity}`, name: closure.name, arity, module: closure.module, goal: closure });
1248
+ }
1249
+ for (let i = start; i < out.length; i++) out[i].module ??= module;
1250
+ }
1251
+
1252
+ function expandAutoloadMetaDependencies(program, dependencies, module = 'user') {
1253
+ const seen = new WeakMap();
1254
+ for (let i = 0; i < dependencies.length; i++) {
1255
+ const dependency = dependencies[i];
1256
+ if (dependency.goal == null) continue;
1257
+ const caller = dependency.module ?? module;
1258
+ const visitKey = `${caller}\u0000${dependency.key}`;
1259
+ const visits = seen.get(dependency.goal) ?? new Set();
1260
+ if (visits.has(visitKey)) continue;
1261
+ visits.add(visitKey);
1262
+ seen.set(dependency.goal, visits);
1263
+ const group = program.findGroup(dependency.name, dependency.arity, caller);
1264
+ for (const mode of group?.metaArgumentModes ?? []) {
1265
+ if (mode.kind !== 'closure' || !Number.isSafeInteger(mode.extraArguments) || mode.extraArguments < 0) continue;
1266
+ collectAutoloadClosureDependencies(dependency.goal?.args[mode.index], mode.extraArguments, dependencies, caller);
1267
+ }
1268
+ }
1269
+ return dependencies;
1270
+ }
1271
+
1228
1272
  function groupAutoloadDependencies(group) {
1229
1273
  const dependencies = [];
1230
1274
  for (const clause of group.clauses) {
@@ -1288,7 +1332,7 @@ function libraryAutoloadRequests(program, extraGoals = []) {
1288
1332
  const requests = new Map();
1289
1333
  for (const group of program.groups.values()) {
1290
1334
  if (bundledLibraryModule(program, group.module)) continue;
1291
- for (const dependency of groupAutoloadDependencies(group)) {
1335
+ for (const dependency of expandAutoloadMetaDependencies(program, groupAutoloadDependencies(group), group.module)) {
1292
1336
  const targetModule = dependency.module ?? group.module;
1293
1337
  if (procedureResolvedBeforeAutoload(program, dependency, targetModule)) continue;
1294
1338
  const library = autoloadLibraryFor(dependency);
@@ -1304,7 +1348,7 @@ function libraryAutoloadRequests(program, extraGoals = []) {
1304
1348
  }
1305
1349
  }
1306
1350
  for (const goal of program.initializations) {
1307
- for (const dependency of collectAutoloadGoalDependencies(goal)) {
1351
+ for (const dependency of expandAutoloadMetaDependencies(program, collectAutoloadGoalDependencies(goal))) {
1308
1352
  const targetModule = dependency.module ?? 'user';
1309
1353
  if (procedureResolvedBeforeAutoload(program, dependency, targetModule)) continue;
1310
1354
  const library = autoloadLibraryFor(dependency);
@@ -1319,7 +1363,7 @@ function libraryAutoloadRequests(program, extraGoals = []) {
1319
1363
  });
1320
1364
  }
1321
1365
  }
1322
- for (const dependency of extraGoalDependencies(extraGoals)) {
1366
+ for (const dependency of expandAutoloadMetaDependencies(program, extraGoalDependencies(extraGoals))) {
1323
1367
  const targetModule = dependency.module ?? 'user';
1324
1368
  if (procedureResolvedBeforeAutoload(program, dependency, targetModule)) continue;
1325
1369
  const library = autoloadLibraryFor(dependency);
package/src/solver.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // Depth-first EyeProlog solver with builtin dispatch, memoization, and guarded recursion handling.
2
2
  // Most semantic decisions still flow through unification; optimizations only select candidates earlier.
3
3
  import {
4
- ATOM, COMPOUND, NUMBER, STRING, VAR, Env, Term, atom, compactListLength, compactVariableList, compound, cons, copyResolved, deref, emptyList,
4
+ ATOM, COMPOUND, NUMBER, STRING, VAR, Env, Term, atom, compareTerms, compactListLength, compactVariableList, compound, cons, copyResolved, deref, emptyList,
5
5
  flattenConjunction, freshTerm, isCons, isDecimalInteger, isEmptyList, isScalar,
6
6
  numberTerm, numberTextFromDouble, properListItems, termIsGround, termToString, unify, variable, variantTerms,
7
7
  } from './term.js';
@@ -838,6 +838,16 @@ export class Solver {
838
838
  continue;
839
839
  }
840
840
 
841
+ const comparisonIterator = bundledCompareSiIterator(this, group, goal, env);
842
+ if (comparisonIterator != null) {
843
+ const firstResult = comparisonIterator.next();
844
+ if (firstResult.done) break;
845
+ goals = rest;
846
+ env = firstResult.value;
847
+ depth++;
848
+ continue;
849
+ }
850
+
841
851
  const lengthIterator = bundledLengthIterator(this, group, goal, env);
842
852
  if (lengthIterator != null) {
843
853
  const firstResult = lengthIterator.next();
@@ -1778,6 +1788,60 @@ function pushWfsAnswerFrames(stack, model, group, goal, rest, env, depth, active
1778
1788
  }
1779
1789
  }
1780
1790
 
1791
+ // Like the bundled length/member specializations, this keeps the portable
1792
+ // Prolog definition available to other registries and user definitions. A
1793
+ // direct walk avoids allocating fresh clause variables and doing occurs checks
1794
+ // over the remaining list suffix on each comparison step (issue #105).
1795
+ function bundledCompareSiIterator(solver, group, goal, env) {
1796
+ if (solver.registry.eyePrologLibrary !== true || group.bundledLibrary !== true ||
1797
+ group.module !== 'si' || group.name !== 'compare_si' || group.arity !== 3 ||
1798
+ group.tabled || group.clauses.length !== 1) return null;
1799
+ return bundledCompareSiSolutions(solver, goal, env);
1800
+ }
1801
+
1802
+ const compareSiErrorContext = {};
1803
+ function* bundledCompareSiSolutions(solver, goal, env) {
1804
+ try {
1805
+ const order = deref(goal.args[0], env);
1806
+ if (order.type !== VAR) {
1807
+ if (order.type !== ATOM) throw new PrologError('type_error(atom)', order);
1808
+ if (!['<', '=', '>'].includes(order.name)) throw new PrologError('domain_error(order)', order);
1809
+ }
1810
+ const pending = [goal.args[1], goal.args[2]];
1811
+ let comparison = 0;
1812
+ while (pending.length !== 0) {
1813
+ const right = deref(pending.pop(), env);
1814
+ const left = deref(pending.pop(), env);
1815
+ if (left === right) continue;
1816
+ if (left.type === VAR || right.type === VAR) {
1817
+ if (left.type === VAR && right.type === VAR && left.name === right.name) continue;
1818
+ throw new PrologError('instantiation_error');
1819
+ }
1820
+ if (left.type === COMPOUND && right.type === COMPOUND) {
1821
+ if (left.arity !== right.arity) {
1822
+ comparison = left.arity < right.arity ? -1 : 1;
1823
+ } else if (left.name !== right.name) {
1824
+ comparison = compareTerms(atom(left.name), atom(right.name));
1825
+ } else {
1826
+ // Push right-to-left so the first differing argument decides the
1827
+ // result. Later variables must not cause premature errors.
1828
+ for (let i = left.arity - 1; i >= 0; i--) pending.push(left.args[i], right.args[i]);
1829
+ continue;
1830
+ }
1831
+ } else {
1832
+ // At least one operand is atomic: type/value order is already fixed.
1833
+ comparison = compareTerms(left, right);
1834
+ }
1835
+ if (comparison !== 0) break;
1836
+ }
1837
+ const next = env.clone();
1838
+ solver.stats.unify_calls++;
1839
+ if (unify(goal.args[0], atom(comparison < 0 ? '<' : comparison > 0 ? '>' : '='), next)) yield next;
1840
+ } catch (error) {
1841
+ throw attachBuiltinErrorContext(error, compareSiErrorContext, goal);
1842
+ }
1843
+ }
1844
+
1781
1845
  function bundledBetweenIterator(solver, group, goal, env) {
1782
1846
  if (solver.registry.eyePrologLibrary !== true ||
1783
1847
  group.module !== 'between' || group.name !== 'between' || group.arity !== 3 ||
package/src/term.js CHANGED
@@ -1586,29 +1586,34 @@ const TYPE_ORDER = { [VAR]: 0, [NUMBER]: 1, [ATOM]: 2, [STRING]: 3, [COMPOUND]:
1586
1586
  const EMPTY_ENV = new Env();
1587
1587
 
1588
1588
  function compareTermsWithRanks(left, right, variableRanks) {
1589
- left = deref(left, EMPTY_ENV);
1590
- right = deref(right, EMPTY_ENV);
1591
- const lr = TYPE_ORDER[left.type] ?? 0;
1592
- const rr = TYPE_ORDER[right.type] ?? 0;
1593
- if (lr !== rr) return lr < rr ? -1 : 1;
1594
- if (left.type === NUMBER) {
1595
- const leftInteger = isDecimalInteger(left.name);
1596
- const rightInteger = isDecimalInteger(right.name);
1597
- if (leftInteger !== rightInteger) return leftInteger ? 1 : -1;
1598
- return compareNumberText(left.name, right.name);
1599
- }
1600
- if (left.type === VAR) {
1601
- if (left.name === right.name) return 0;
1602
- const leftOrder = variableRank(left.name, variableRanks);
1603
- const rightOrder = variableRank(right.name, variableRanks);
1604
- return leftOrder < rightOrder ? -1 : 1;
1605
- }
1606
- if (left.type === ATOM || left.type === STRING) return compareCharacterText(left.name, right.name);
1607
- if (left.arity !== right.arity) return left.arity < right.arity ? -1 : 1;
1608
- if (left.name !== right.name) return compareCharacterText(left.name, right.name);
1609
- for (let i = 0; i < left.arity; i++) {
1610
- const cmp = compareTermsWithRanks(left.args[i], right.args[i], variableRanks);
1611
- if (cmp) return cmp;
1589
+ // Standard compare/3 is used alongside compare_si/3 in issue #105. Walk
1590
+ // argument pairs explicitly so long lists do not exhaust the host stack.
1591
+ const pending = [left, right];
1592
+ while (pending.length !== 0) {
1593
+ right = deref(pending.pop(), EMPTY_ENV);
1594
+ left = deref(pending.pop(), EMPTY_ENV);
1595
+ const lr = TYPE_ORDER[left.type] ?? 0;
1596
+ const rr = TYPE_ORDER[right.type] ?? 0;
1597
+ if (lr !== rr) return lr < rr ? -1 : 1;
1598
+ if (left.type === NUMBER) {
1599
+ const leftInteger = isDecimalInteger(left.name);
1600
+ const rightInteger = isDecimalInteger(right.name);
1601
+ if (leftInteger !== rightInteger) return leftInteger ? 1 : -1;
1602
+ const cmp = compareNumberText(left.name, right.name);
1603
+ if (cmp) return cmp;
1604
+ } else if (left.type === VAR) {
1605
+ if (left.name === right.name) continue;
1606
+ const leftOrder = variableRank(left.name, variableRanks);
1607
+ const rightOrder = variableRank(right.name, variableRanks);
1608
+ return leftOrder < rightOrder ? -1 : 1;
1609
+ } else if (left.type === ATOM || left.type === STRING) {
1610
+ const cmp = compareCharacterText(left.name, right.name);
1611
+ if (cmp) return cmp;
1612
+ } else {
1613
+ if (left.arity !== right.arity) return left.arity < right.arity ? -1 : 1;
1614
+ if (left.name !== right.name) return compareCharacterText(left.name, right.name);
1615
+ for (let i = left.arity - 1; i >= 0; i--) pending.push(left.args[i], right.args[i]);
1616
+ }
1612
1617
  }
1613
1618
  return 0;
1614
1619
  }
@@ -28,6 +28,142 @@ import {
28
28
 
29
29
  export function regressionCases() {
30
30
  return [
31
+ {
32
+ name: 'autoload follows declared meta-predicates and closure arities (issue #105)',
33
+ run: () => {
34
+ const wrappers = ':- meta_predicate(mytime(0)).\nmytime(G) :- time(G).\n' +
35
+ ':- meta_predicate(apply_one(1,?)).\napply_one(G,X) :- call(G,X).\n';
36
+ for (const goal of ['mytime(mytime(compare_si(<,a,b)))',
37
+ 'apply_one(compare_si(<,a),b)', 'call(compare_si(<),a,b)', 'time(compare_si(<,a,b))']) {
38
+ const result = runEyeProlog(wrappers, { goals: [goal] });
39
+ assertIncludes(result.stdout, goal.startsWith('apply_one') ? 'apply_one' : goal.split('(')[0], goal);
40
+ }
41
+ const initialized = runEyeProlog(wrappers + ':- initialization((mytime(compare_si(<,a,b)),write(ok))).', { goals: [] });
42
+ assertIncludes(initialized.stdout, 'ok', 'initialization meta-goal');
43
+ // A predicate with the same name as a library wrapper can take data.
44
+ const data = Program.parse('time(_). answer :- time(compare_si(O,a,b)).');
45
+ assertEqual(data.autoloadedPredicates.some(({ indicator }) => indicator === 'compare_si/3'), false, 'data argument is not a goal');
46
+ },
47
+ },
48
+ {
49
+ name: 'imported user meta-wrappers autoload caller goals on the first REPL invocation',
50
+ run: () => {
51
+ const file = path.join(temp.dir, 'mytime.pl');
52
+ fs.writeFileSync(file, ':- module(timing_wrapper,[mytime/1]).\n:- meta_predicate(mytime(0)).\nmytime(G) :- time(G).\n');
53
+ const source = `:- use_module(${sourceAtom(file)}).\nanswer(ok) :- mytime(compare_si(<,a,b)).\n`;
54
+ assertIncludes(runEyeProlog(source, { goals: ['answer(X)'] }).stdout, 'answer(ok)', 'imported wrapper');
55
+ const repl = runCli([], { input: `use_module(${sourceAtom(file)}).\nmytime(compare_si(O,a,b)).\n.\nhalt.\n` });
56
+ assertEqual(repl.status, 0, repl.stderr);
57
+ assertIncludes(repl.stdout, 'O = (<)', 'first meta-call');
58
+ assertNotIncludes(repl.stdout + repl.stderr, 'existence_error', 'meta autoload');
59
+ },
60
+ },
61
+ {
62
+ name: 'bundled compare_si/3 matches the portable definition and respects user definitions',
63
+ run: () => {
64
+ const terms = ['X', 'Y', '1', '1.0', '-2', 'a', 'b', '[]', '[X,a]', '[X|Y]',
65
+ 'f(X)', 'f(Y)', 'g(X)', 'f(X,a)', 'f(X,b)', 'f(g(X),a)'];
66
+ const clauses = [];
67
+ for (const left of terms) for (const right of terms) {
68
+ clauses.push(`answer(${clauses.length},R) :- catch((compare_si(O,${left},${right})->R=O;R=failed),error(E,C),R=error(E,C)).`);
69
+ }
70
+ const source = clauses.join('\n');
71
+ const optimized = runEyeProlog(source, { goals: ['answer(I,R)'] });
72
+ const portable = runEyeProlog(source, { goals: ['answer(I,R)'], registry: createDefaultRegistry() });
73
+ assertEqual(optimized.stdout, portable.stdout, 'all scalar, variable, compound and list comparisons');
74
+ const custom = runEyeProlog('compare_si(custom,_,_).', { goals: ['compare_si(O,a,b)'] });
75
+ assertIncludes(custom.stdout, 'compare_si(custom, a, b)', 'user definition');
76
+ },
77
+ },
78
+ {
79
+ name: 'compare/3 and compare_si/3 compare large lists and deep terms with a bounded host stack',
80
+ run: () => {
81
+ // A separate process bounds regressions in both memory and time. Build
82
+ // the operands directly so this measures comparison, not append/3.
83
+ const script = `
84
+ import {Program,Solver,Env,getEyePrologRegistry,variable,atom,compound,listFromItems,termToString} from './src/index.js';
85
+ const program = Program.parse(':- use_module(library(si)).');
86
+ const solver = new Solver(program,{registry:getEyePrologRegistry()});
87
+ function check(a,b,expected) {
88
+ for(const name of ['compare','compare_si']) {
89
+ const order=variable('Order');
90
+ const answers=[...solver.solve([compound(name,[order,a,b])],new Env(),0)];
91
+ if (answers.length!==1 || termToString(order,answers[0])!==expected) throw new Error(name+': wrong order');
92
+ }
93
+ }
94
+ for (const n of [16384,65536]) {
95
+ const prefix=Array.from({length:n},(_,i)=>variable('X'+i));
96
+ const a=listFromItems([...prefix,atom('a')]);
97
+ const b=listFromItems([...prefix,atom('b')]);
98
+ check(a,b,'<'); check(b,a,'>');
99
+ check(a,listFromItems([...prefix,atom('a')]),'=');
100
+ }
101
+ let a=atom('a'), b=atom('b');
102
+ for(let i=0;i<16384;i++) {a=compound('f',[a]);b=compound('f',[b]);}
103
+ check(a,b,'<');
104
+ console.log('ok');
105
+ `;
106
+ const result = spawnSync(process.execPath, ['--max-old-space-size=128', '--stack-size=256', '--input-type=module', '--eval', script], {
107
+ cwd: packageRoot, encoding: 'utf8', timeout: 15000,
108
+ });
109
+ if (result.error) throw result.error;
110
+ assertEqual(result.status, 0, `large comparison: ${result.stderr}`);
111
+ assertIncludes(result.stdout, 'ok', 'large comparison completes');
112
+ },
113
+ },
114
+ {
115
+ name: 'REPL prints the complete 8192-cell timed comparison answer (issue #105)',
116
+ run: () => {
117
+ const result = runCli([], { input:
118
+ 'length(_,I),I=13,N is 2^I,length(P,N),append(P,[1],L1),append(P,[2],L2),' +
119
+ 'time(compare(R,L1,L2)),time(compare_si(S,L1,L2)).\n.\nhalt.\n',
120
+ timeout: 60000,
121
+ });
122
+ if (result.error) throw result.error;
123
+ assertEqual(result.status, 0, result.stderr);
124
+ assertNotIncludes(result.stdout + result.stderr, 'Maximum call stack', 'host stack');
125
+ assertIncludes(result.stdout, 'I = 13, N = 8192', 'large answer');
126
+ assertIncludes(result.stdout, 'R = (<), S = (<)', 'both comparison results');
127
+ },
128
+ },
129
+ {
130
+ name: 'timed compare_si/3 backtracks over growing prefixes without exhausting the host stack (issue #105)',
131
+ run: () => {
132
+ // Bound the reported generator so the regression exhausts every
133
+ // answer, including sizes beyond the original 256-cell failure.
134
+ const result = runCli(['-'], { input:
135
+ '%% goal: answer(I,R,S)\n' +
136
+ 'answer(I,R,S) :- length(_,I), (I =< 9 -> true ; !, fail), ' +
137
+ 'N is 2^I, length(P,N), append(P,[1],L1), append(P,[2],L2), ' +
138
+ 'time(compare(R,L1,L2)), time(compare_si(S,L1,L2)).\n',
139
+ timeout: 20000,
140
+ });
141
+ assertEqual(result.status, 0, `exit status; error=${result.error}; stderr=${result.stderr}`);
142
+ for (let i = 0; i <= 9; i++) {
143
+ assertIncludes(result.stdout, `answer(${i}, <, <).`, 'comparison result');
144
+ }
145
+ assertNotIncludes(result.stderr + result.stdout, 'Maximum call stack', 'host stack');
146
+ },
147
+ },
148
+ {
149
+ name: 'compare_si/3 work list preserves argument priority and instantiation errors',
150
+ run: () => {
151
+ const goals = [
152
+ 'compare_si(=,f(X,g(Y)),f(X,g(Y))),var(X),var(Y)',
153
+ 'compare_si(<,f(g(X),a),f(g(X),b)),var(X)',
154
+ 'compare_si(>,f(g(b),a),f(g(a),z))',
155
+ 'compare_si(<,[a|X],[b|Y]),var(X),var(Y)',
156
+ 'compare_si(>,f(X),a),var(X)',
157
+ 'compare_si(<,a,f(X)),var(X)',
158
+ '\\+ compare_si(>,f(g(X),a),f(g(X),b))',
159
+ 'catch((compare_si(_,f(g(X),a),f(g(Y),b)),fail),error(instantiation_error,[predicate-compare_si/3]),true),var(X),var(Y)',
160
+ ];
161
+ for (const goal of goals) {
162
+ const result = runEyeProlog(`answer(ok) :- ${goal}.`, { goals: ['answer(X)'] });
163
+ assertIncludes(result.stdout, 'answer(ok)', goal);
164
+ }
165
+ },
166
+ },
31
167
  {
32
168
  name: 'compare_si/3 validates Order before term instantiation (issue #100)',
33
169
  run: () => {