eyeprolog 1.6.3 → 1.6.4
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/cleanup.js +7 -0
- package/src/iso.js +17 -1
- package/src/solver.js +88 -15
- package/test/regression/cases-api.mjs +51 -5
- package/test/regression/cases-regression.mjs +13 -0
package/package.json
CHANGED
package/src/cleanup.js
CHANGED
|
@@ -163,6 +163,13 @@ function closeSearchStack(stack, suppressErrors) {
|
|
|
163
163
|
closeFrames(removed, suppressErrors);
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
+
// A caught exception discards only the protected goal's alternatives. Cleanup
|
|
167
|
+
// must run before recovery, without replacing the exception being unwound.
|
|
168
|
+
export function unwindSearchStack(stack, start) {
|
|
169
|
+
const removed = Array.prototype.splice.call(stack, start, stack.length - start);
|
|
170
|
+
closeFrames(removed, true);
|
|
171
|
+
}
|
|
172
|
+
|
|
166
173
|
function closeFrames(frames, suppressErrors) {
|
|
167
174
|
let firstError = null;
|
|
168
175
|
for (let index = frames.length - 1; index >= 0; index--) {
|
package/src/iso.js
CHANGED
|
@@ -141,7 +141,7 @@ const isoBuiltins = {
|
|
|
141
141
|
for (let arity = 2; arity <= 8; arity++) {
|
|
142
142
|
registry.add('call', arity, callClosureBuiltin, { expandGoal: expandCallClosureGoal });
|
|
143
143
|
}
|
|
144
|
-
registry.add('catch', 3, catchBuiltin);
|
|
144
|
+
registry.add('catch', 3, catchBuiltin, { catchControl });
|
|
145
145
|
registry.add('throw', 1, throwBuiltin, { deterministic: true });
|
|
146
146
|
registry.add('\\+', 1, negationBuiltin, { deterministic: true });
|
|
147
147
|
registry.add('once', 1, onceBuiltin, { deterministic: true });
|
|
@@ -3106,6 +3106,21 @@ function prologErrorBall(error) {
|
|
|
3106
3106
|
}
|
|
3107
3107
|
const catchBuiltin = pendingBuiltin(catchSolutions, true);
|
|
3108
3108
|
|
|
3109
|
+
// The solver runs protected goals on its explicit search stack. Keep ball
|
|
3110
|
+
// conversion and callable validation here, shared with the builtin semantics.
|
|
3111
|
+
const catchControl = {
|
|
3112
|
+
prepare: ({ goal, env }) => callable(goal.args[0], env),
|
|
3113
|
+
recover({ goal, env, error }) {
|
|
3114
|
+
const ball = error instanceof ThrownTerm
|
|
3115
|
+
? error.term
|
|
3116
|
+
: error instanceof PrologError ? prologErrorBall(error) : null;
|
|
3117
|
+
if (ball == null) return null;
|
|
3118
|
+
const recovered = env.clone();
|
|
3119
|
+
if (!unify(goal.args[1], ball, recovered)) return null;
|
|
3120
|
+
return { goal: callable(goal.args[2], recovered), env: recovered };
|
|
3121
|
+
},
|
|
3122
|
+
};
|
|
3123
|
+
|
|
3109
3124
|
function* catchSolutions({ solver, goal, env }, state) {
|
|
3110
3125
|
let child = null;
|
|
3111
3126
|
try {
|
|
@@ -3297,6 +3312,7 @@ export class BuiltinRegistry {
|
|
|
3297
3312
|
deterministic: options.deterministic ?? false,
|
|
3298
3313
|
deterministicWhen: options.deterministicWhen ?? null,
|
|
3299
3314
|
expandGoal: options.expandGoal ?? null,
|
|
3315
|
+
catchControl: options.catchControl ?? null,
|
|
3300
3316
|
ready: options.ready ?? null,
|
|
3301
3317
|
fallbackWhenNotReady: options.fallbackWhenNotReady ?? false,
|
|
3302
3318
|
shouldUse: options.shouldUse ?? null,
|
package/src/solver.js
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
} from './term.js';
|
|
8
8
|
import { numberValueKey, sameNumberValue } from './number-value.js';
|
|
9
9
|
import { attachBuiltinErrorContext } from './errors.js';
|
|
10
|
+
import { unwindSearchStack } from './cleanup.js';
|
|
10
11
|
import { PrologError, getStrictIsoRegistry } from './iso.js';
|
|
11
12
|
import { getEyePrologRegistry } from './standard-library.js';
|
|
12
13
|
import { selectClauseCandidates, selectClauseCandidatesForValues, selectGroundClauseCandidates } from './program-indexing.js';
|
|
@@ -462,7 +463,11 @@ export class Solver {
|
|
|
462
463
|
const stack = [{ kind: 'goals', goals, env, depth, active: savedActive.slice() }];
|
|
463
464
|
registeredStack = stack;
|
|
464
465
|
this.solveStacks.push(stack);
|
|
466
|
+
let active = savedActive;
|
|
465
467
|
while (stack.length) {
|
|
468
|
+
const frame = stack.pop();
|
|
469
|
+
active = frame.active ?? active;
|
|
470
|
+
try {
|
|
466
471
|
this.inferences++;
|
|
467
472
|
this.inferenceObservation.value++;
|
|
468
473
|
this.checkMemoryLimit();
|
|
@@ -470,7 +475,6 @@ export class Solver {
|
|
|
470
475
|
this.inferenceLimitExceeded = true;
|
|
471
476
|
break;
|
|
472
477
|
}
|
|
473
|
-
const frame = stack.pop();
|
|
474
478
|
this.syncProgramRevision();
|
|
475
479
|
if (frame.kind === 'resumeBuiltin') {
|
|
476
480
|
if (this.solutionsSeen >= this.solutionLimit) continue;
|
|
@@ -623,7 +627,7 @@ export class Solver {
|
|
|
623
627
|
env.setOccursCheckHandler(this.occursCheckHandler);
|
|
624
628
|
env.setAttributeHookRunner(this.attributeHookRunner);
|
|
625
629
|
depth = frame.depth;
|
|
626
|
-
|
|
630
|
+
active = frame.active;
|
|
627
631
|
|
|
628
632
|
while (true) {
|
|
629
633
|
this.inferences++;
|
|
@@ -673,6 +677,12 @@ export class Solver {
|
|
|
673
677
|
}
|
|
674
678
|
|
|
675
679
|
const first = goals[0];
|
|
680
|
+
if (first?.kind === 'exitCatch') {
|
|
681
|
+
active = first.active;
|
|
682
|
+
depth = first.depth;
|
|
683
|
+
goals = first.goals;
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
676
686
|
if (first?.kind === 'continueGoals') {
|
|
677
687
|
if (first.releaseActive) active = active.slice(0, -1);
|
|
678
688
|
depth = first.depth;
|
|
@@ -749,6 +759,27 @@ export class Solver {
|
|
|
749
759
|
const def = callable ? this.lookupBuiltin(goal.name, goal.arity) : null;
|
|
750
760
|
this.active = active;
|
|
751
761
|
const builtinReady = def && builtinIsReadyOrAuthoritative(def, this, goal, env);
|
|
762
|
+
if (builtinReady && def.catchControl != null) {
|
|
763
|
+
// Backtracking frames retain this handler in their active path;
|
|
764
|
+
// exitCatch removes it before executing the caller's continuation.
|
|
765
|
+
// Nesting protected goals therefore needs no recursive solve() call.
|
|
766
|
+
const handler = {
|
|
767
|
+
goal, env: env.clone(), definition: def,
|
|
768
|
+
parentActive: active, goals: rest, depth,
|
|
769
|
+
searchStack: stack, stackDepth: stack.length,
|
|
770
|
+
};
|
|
771
|
+
active = [...active, handler];
|
|
772
|
+
let invoked;
|
|
773
|
+
try {
|
|
774
|
+
invoked = def.catchControl.prepare({ goal, env });
|
|
775
|
+
} catch (error) {
|
|
776
|
+
throw attachBuiltinErrorContext(error, def, goal);
|
|
777
|
+
}
|
|
778
|
+
goals = [invoked, { kind: 'exitCatch', active: handler.parentActive, goals: rest, depth }];
|
|
779
|
+
env = env.clone();
|
|
780
|
+
depth++;
|
|
781
|
+
continue;
|
|
782
|
+
}
|
|
752
783
|
if (builtinReady && typeof def.expandGoal === 'function') {
|
|
753
784
|
// Meta-calls execute in this continuation instead of hiding their
|
|
754
785
|
// search behind a host-generator frame. Their real clause, control,
|
|
@@ -968,20 +999,43 @@ export class Solver {
|
|
|
968
999
|
pushUserGoalUncachedFrames(stack, this, group, goal, rest, env, depth, active);
|
|
969
1000
|
break;
|
|
970
1001
|
}
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
1002
|
+
} catch (caught) {
|
|
1003
|
+
let error = normalizeSolverResourceError(this, caught);
|
|
1004
|
+
let recovered = false;
|
|
1005
|
+
for (let index = active.length - 1; index >= 0; index--) {
|
|
1006
|
+
const handler = active[index];
|
|
1007
|
+
// Reentrant control builtins inherit active cut markers, but must
|
|
1008
|
+
// propagate exceptions back to the solve() that owns the handler.
|
|
1009
|
+
if (handler.searchStack !== stack) continue;
|
|
1010
|
+
unwindSearchStack(stack, handler.stackDepth);
|
|
1011
|
+
active = handler.parentActive;
|
|
1012
|
+
let recovery;
|
|
1013
|
+
try {
|
|
1014
|
+
recovery = handler.definition.catchControl.recover({ ...handler, error });
|
|
1015
|
+
} catch (replacement) {
|
|
1016
|
+
error = attachBuiltinErrorContext(replacement, handler.definition, handler.goal);
|
|
1017
|
+
index = active.length;
|
|
1018
|
+
continue;
|
|
1019
|
+
}
|
|
1020
|
+
if (recovery == null) {
|
|
1021
|
+
index = active.length;
|
|
1022
|
+
continue;
|
|
1023
|
+
}
|
|
1024
|
+
// Recovery is outside this handler and has its own opaque cut scope.
|
|
1025
|
+
const invocation = { goal: recovery.goal, env: recovery.env._snapshotForSolverRead() };
|
|
1026
|
+
stack.push({
|
|
1027
|
+
kind: 'goals',
|
|
1028
|
+
goals: [recovery.goal, { kind: 'exitCatch', active, goals: handler.goals, depth: handler.depth }],
|
|
1029
|
+
env: recovery.env, depth: handler.depth + 1, active: [...active, invocation],
|
|
1030
|
+
});
|
|
1031
|
+
recovered = true;
|
|
1032
|
+
break;
|
|
982
1033
|
}
|
|
1034
|
+
if (!recovered) throw error;
|
|
983
1035
|
}
|
|
984
|
-
|
|
1036
|
+
}
|
|
1037
|
+
} catch (error) {
|
|
1038
|
+
throw normalizeSolverResourceError(this, error);
|
|
985
1039
|
} finally {
|
|
986
1040
|
const stackIndex = this.solveStacks.indexOf(registeredStack);
|
|
987
1041
|
if (stackIndex >= 0) this.solveStacks.splice(stackIndex, 1);
|
|
@@ -1152,6 +1206,20 @@ export class Solver {
|
|
|
1152
1206
|
|
|
1153
1207
|
}
|
|
1154
1208
|
|
|
1209
|
+
function normalizeSolverResourceError(solver, error) {
|
|
1210
|
+
const normalized = normalizeHostResourceError(error);
|
|
1211
|
+
if (normalized instanceof PrologError && normalized.formal === 'resource_error(memory)') {
|
|
1212
|
+
// Leave bounded breathing room for recovery after abandoned query terms
|
|
1213
|
+
// become garbage, including when a handler in this search catches the error.
|
|
1214
|
+
if (!solver.memoryRecovery.active) {
|
|
1215
|
+
solver.memoryRecovery.active = true;
|
|
1216
|
+
solver.memoryRecovery.reservationBytes = 1024 * 1024;
|
|
1217
|
+
solver.memoryRecovery.checks = 16;
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
return normalized;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1155
1223
|
function normalizeHostResourceError(error) {
|
|
1156
1224
|
if (error?.name !== 'RangeError') return error;
|
|
1157
1225
|
const message = String(error?.message ?? '');
|
|
@@ -1160,9 +1228,14 @@ function normalizeHostResourceError(error) {
|
|
|
1160
1228
|
// with the resource atom implementation dependent. A finite host capacity
|
|
1161
1229
|
// ceiling is reported as `memory`; reserve `finite_memory` for the separate
|
|
1162
1230
|
// convention where no finite amount of memory can complete the computation.
|
|
1163
|
-
|
|
1231
|
+
// Do not run a regular expression here: V8 can overflow while compiling it
|
|
1232
|
+
// on an already exhausted stack, masking the original error (issue #117).
|
|
1233
|
+
if (message === 'Map maximum size exceeded' || message === 'Set maximum size exceeded') {
|
|
1164
1234
|
return new PrologError('resource_error(memory)');
|
|
1165
1235
|
}
|
|
1236
|
+
if (message === 'Maximum call stack size exceeded') {
|
|
1237
|
+
return new PrologError('resource_error(stack)');
|
|
1238
|
+
}
|
|
1166
1239
|
return error;
|
|
1167
1240
|
}
|
|
1168
1241
|
|
|
@@ -1080,11 +1080,12 @@ answer(ok) :-
|
|
|
1080
1080
|
},
|
|
1081
1081
|
},
|
|
1082
1082
|
{
|
|
1083
|
-
name: 'host
|
|
1083
|
+
name: 'host capacity and stack errors become Prolog resource errors',
|
|
1084
1084
|
run: () => {
|
|
1085
|
-
for (const [predicate, message] of [
|
|
1086
|
-
['exhaust_map', 'Map maximum size exceeded'],
|
|
1087
|
-
['exhaust_set', 'Set maximum size exceeded'],
|
|
1085
|
+
for (const [predicate, message, resource] of [
|
|
1086
|
+
['exhaust_map', 'Map maximum size exceeded', 'memory'],
|
|
1087
|
+
['exhaust_set', 'Set maximum size exceeded', 'memory'],
|
|
1088
|
+
['exhaust_stack', 'Maximum call stack size exceeded', 'stack'],
|
|
1088
1089
|
]) {
|
|
1089
1090
|
const registry = new BuiltinRegistry();
|
|
1090
1091
|
registry.add(predicate, 0, function* () {
|
|
@@ -1099,10 +1100,55 @@ answer(ok) :-
|
|
|
1099
1100
|
caught = error;
|
|
1100
1101
|
}
|
|
1101
1102
|
assertEqual(caught?.name, 'PrologError', `${predicate} normalized error type`);
|
|
1102
|
-
assertEqual(caught?.formal,
|
|
1103
|
+
assertEqual(caught?.formal, `resource_error(${resource})`, `${predicate} normalized resource error`);
|
|
1103
1104
|
}
|
|
1104
1105
|
},
|
|
1105
1106
|
},
|
|
1107
|
+
{
|
|
1108
|
+
name: 'unrelated host RangeErrors retain their identity',
|
|
1109
|
+
run: () => {
|
|
1110
|
+
const original = new RangeError('unrelated host failure');
|
|
1111
|
+
const registry = new BuiltinRegistry();
|
|
1112
|
+
registry.add('host_failure', 0, function* () { throw original; });
|
|
1113
|
+
const solver = new Solver(Program.parse(''), { registry });
|
|
1114
|
+
let caught;
|
|
1115
|
+
try {
|
|
1116
|
+
[...solver.solve([parseGoalText('host_failure')], new Env(), 0)];
|
|
1117
|
+
} catch (error) {
|
|
1118
|
+
caught = error;
|
|
1119
|
+
}
|
|
1120
|
+
assertEqual(caught, original, 'unrecognized host error');
|
|
1121
|
+
},
|
|
1122
|
+
},
|
|
1123
|
+
{
|
|
1124
|
+
name: 'nested catch succeeds on cold and warm runs with a small host stack (issue #117)',
|
|
1125
|
+
run: () => {
|
|
1126
|
+
const engineUrl = new URL('../src/index.js', testDirUrl).href;
|
|
1127
|
+
const script = `
|
|
1128
|
+
import { Program, Solver, Env, parseGoalText, getEyePrologRegistry } from ${JSON.stringify(engineUrl)};
|
|
1129
|
+
const program = Program.parse('deep(0,G) :- !, G. deep(N,G) :- M is N-1, catch(deep(M,G),-,true).');
|
|
1130
|
+
const solver = new Solver(program, { registry: getEyePrologRegistry() });
|
|
1131
|
+
const solve = (text) => [...solver.solve([parseGoalText(text)], new Env(), 0)];
|
|
1132
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
1133
|
+
for (const depth of [550, 1500]) {
|
|
1134
|
+
if (solve('deep(' + depth + ',true)').length !== 1) {
|
|
1135
|
+
throw new Error('nested catch failed');
|
|
1136
|
+
}
|
|
1137
|
+
if (solve('catch(deep(' + depth + ',throw(ball)),ball,true)').length !== 1) {
|
|
1138
|
+
throw new Error('nested catch did not unwind to the matching handler');
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
process.stdout.write('passed');
|
|
1143
|
+
`;
|
|
1144
|
+
const result = spawnSync(process.execPath, [
|
|
1145
|
+
'--stack-size=256', '--input-type=module', '--eval', script,
|
|
1146
|
+
], { cwd: packageRoot, encoding: 'utf8', timeout: 10000 });
|
|
1147
|
+
if (result.error) throw result.error;
|
|
1148
|
+
assertEqual(result.status, 0, `issue #117 child status; stderr=${result.stderr}`);
|
|
1149
|
+
assertEqual(result.stdout, 'passed', 'issue #117 repeated deep catch execution');
|
|
1150
|
+
},
|
|
1151
|
+
},
|
|
1106
1152
|
{
|
|
1107
1153
|
name: 'unbounded length/2 reaches a catchable memory resource error (issue #49)',
|
|
1108
1154
|
run: () => {
|
|
@@ -37,6 +37,19 @@ import {
|
|
|
37
37
|
|
|
38
38
|
export function regressionCases() {
|
|
39
39
|
return [
|
|
40
|
+
{
|
|
41
|
+
name: 'REPL repeated nested catch succeeds from the first query (issue #117)',
|
|
42
|
+
run: () => {
|
|
43
|
+
const file = path.join(temp.dir, 'deep-catch-117.pl');
|
|
44
|
+
fs.writeFileSync(file, 'deep(0,G) :- !, G.\ndeep(N,G) :- M is N-1, catch(deep(M,G),-,true).\n');
|
|
45
|
+
const result = runCli([], { input:
|
|
46
|
+
`[${sourceAtom(file)}].\ndeep(550,true).\ndeep(550,true).\ndeep(550,true).\nhalt.\n`,
|
|
47
|
+
});
|
|
48
|
+
assertEqual(result.status, 0, result.stderr);
|
|
49
|
+
assertNotIncludes(result.stdout + result.stderr, 'error(', 'all repeated queries succeed');
|
|
50
|
+
assertEqual((result.stdout.match(/\btrue\./g) ?? []).length, 4, 'consult and three successful queries');
|
|
51
|
+
},
|
|
52
|
+
},
|
|
40
53
|
{
|
|
41
54
|
name: 'close/1 accepts a $stream/1 term rebuilt via functor/3 and arg/3 that is structurally == to the real handle (issue #109)',
|
|
42
55
|
run: () => {
|