eyeprolog 1.5.67 → 1.5.69
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/program.js +51 -7
- package/src/solver.js +65 -1
- package/src/term.js +28 -23
- package/test/regression/cases-regression.mjs +102 -0
package/package.json
CHANGED
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 (
|
|
1223
|
-
|
|
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
|
-
|
|
1590
|
-
|
|
1591
|
-
const
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
const
|
|
1596
|
-
const
|
|
1597
|
-
if (
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
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,108 @@ 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
|
+
// Keep the large variable-list answer that overflowed REPL printing,
|
|
118
|
+
// but prepend the differing elements to avoid expensive append/3 setup.
|
|
119
|
+
// The adjacent cases cover long shared-prefix comparison and append/3
|
|
120
|
+
// backtracking separately.
|
|
121
|
+
const result = runCli([], { input:
|
|
122
|
+
'length(_,I),I=13,N is 2^I,length(P,N),L1=[1|P],L2=[2|P],' +
|
|
123
|
+
'time(compare(R,L1,L2)),time(compare_si(S,L1,L2)).\n.\nhalt.\n',
|
|
124
|
+
timeout: 60000,
|
|
125
|
+
});
|
|
126
|
+
if (result.error) throw result.error;
|
|
127
|
+
assertEqual(result.status, 0, result.stderr);
|
|
128
|
+
assertNotIncludes(result.stdout + result.stderr, 'Maximum call stack', 'host stack');
|
|
129
|
+
assertIncludes(result.stdout, 'I = 13, N = 8192', 'large answer');
|
|
130
|
+
assertIncludes(result.stdout, 'R = (<), S = (<)', 'both comparison results');
|
|
131
|
+
},
|
|
132
|
+
},
|
|
31
133
|
{
|
|
32
134
|
name: 'timed compare_si/3 backtracks over growing prefixes without exhausting the host stack (issue #105)',
|
|
33
135
|
run: () => {
|