eyeprolog 1.3.36 → 1.3.38
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/README.md +28 -3
- package/examples/book/chapter-37/01-write_event.pl +8 -5
- package/package.json +3 -2
- package/src/ARCHITECTURE.md +49 -0
- package/src/cleanup.js +339 -0
- package/src/cli.js +5 -1
- package/src/dcg.js +1 -1
- package/src/errors.js +23 -0
- package/src/index.js +5 -0
- package/src/iso-arithmetic.js +166 -0
- package/src/iso.js +4 -176
- package/src/program-analysis.js +515 -0
- package/src/program-indexing.js +378 -0
- package/src/program.js +8 -873
- package/src/standard-library.js +2 -0
- package/test/conformance/ISO-IMPLEMENTATION-DEFINED.md +8 -8
- package/test/run-all.mjs +4 -0
- package/test/run-architecture.mjs +82 -0
- package/test/run-cleanup.mjs +135 -0
- package/test/run-regression.mjs +5 -1
- package/the-art-of-eyeprolog.md +84 -25
- package/why-eyeprolog.md +22 -0
package/README.md
CHANGED
|
@@ -149,6 +149,24 @@ statistics(wfs_undefined_answers, UndefinedObservations).
|
|
|
149
149
|
Both automatic tabling and `tnot/1` are EyeProlog extensions. Strict ISO mode
|
|
150
150
|
disables automatic tabling and does not provide `tnot/1`.
|
|
151
151
|
|
|
152
|
+
## Cleanup-aware control
|
|
153
|
+
|
|
154
|
+
Normal mode provides `call_cleanup/2` and `setup_call_cleanup/3` for resource
|
|
155
|
+
lifetimes that follow Prolog search. Cleanup runs exactly once when the protected
|
|
156
|
+
goal completes deterministically, is exhausted, is cut or otherwise pruned, the
|
|
157
|
+
top level stops answer enumeration, or an exception unwinds the search.
|
|
158
|
+
`setup_call_cleanup/3` runs Setup once and installs Cleanup only after Setup
|
|
159
|
+
succeeds. On ordinary pruning Cleanup sees the current goal bindings; during
|
|
160
|
+
exception unwinding bindings made by the protected goal have already been
|
|
161
|
+
unwound. Cleanup failure is ignored, and an exception already being propagated
|
|
162
|
+
takes precedence over a cleanup exception. Nested cleanups run inside-out.
|
|
163
|
+
|
|
164
|
+
These predicates are EyeProlog normal-mode extensions and are absent from
|
|
165
|
+
`--iso-strict`. Their implementation is lifecycle-aware: the interactive top
|
|
166
|
+
level can report a remaining choicepoint without speculatively requesting the
|
|
167
|
+
next solution, while abandoning that choicepoint still closes protected
|
|
168
|
+
resources.
|
|
169
|
+
|
|
152
170
|
## OpenRuleBench portable profile
|
|
153
171
|
|
|
154
172
|
The `openrulebench/` directory contains a deterministic four-engine adaptation
|
|
@@ -176,9 +194,10 @@ eyeprolog --iso-strict --goal 'p(X)' program.pl
|
|
|
176
194
|
|
|
177
195
|
The equivalent JavaScript option is `isoStrict: true`. Strict mode rejects
|
|
178
196
|
EyeProlog language extensions, Part 2 module directives, and Part 3 grammar-rule
|
|
179
|
-
expansion/`phrase/2-3`; it also removes the EyeProlog `occurs_check` flag
|
|
180
|
-
|
|
181
|
-
|
|
197
|
+
expansion/`phrase/2-3`; it also removes the EyeProlog `occurs_check` flag,
|
|
198
|
+
`call_cleanup/2`, and `setup_call_cleanup/3`, and disables automatic tabling.
|
|
199
|
+
Normal mode is unchanged and continues to support modules, DCGs, quads,
|
|
200
|
+
libraries, proofs, cleanup-aware control, and the other documented extensions.
|
|
182
201
|
|
|
183
202
|
The auditable processor-requirement checklist lives in
|
|
184
203
|
[`test/conformance/ISO-COMPLIANCE.md`](test/conformance/ISO-COMPLIANCE.md).
|
|
@@ -312,4 +331,10 @@ The GitHub test workflow runs the complete suite and an npm package dry-run on
|
|
|
312
331
|
both the minimum supported Node.js 18 release line and Node.js 24. Publishing
|
|
313
332
|
repeats those release checks before uploading the package.
|
|
314
333
|
|
|
334
|
+
The runtime JavaScript modules stay flat under `src/`; the existing `src/lib/`
|
|
335
|
+
directory contains the portable Prolog library modules. See
|
|
336
|
+
[`src/ARCHITECTURE.md`](src/ARCHITECTURE.md) for the source-layer boundaries,
|
|
337
|
+
facade modules, dependency rule, and the requirement that architectural cleanup
|
|
338
|
+
must preserve the existing solver hot paths and benchmark performance.
|
|
339
|
+
|
|
315
340
|
EyeProlog is released under the [MIT License](LICENSE.md).
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
% From The Art of EyeProlog, Chapter 37.
|
|
2
2
|
write_event(Path, Event) :-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
setup_call_cleanup(
|
|
4
|
+
open(Path, write, Stream, [type(text)]),
|
|
5
|
+
( write_canonical(Stream, Event),
|
|
6
|
+
put_char(Stream, '.'),
|
|
7
|
+
nl(Stream)
|
|
8
|
+
),
|
|
9
|
+
close(Stream)
|
|
10
|
+
).
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "1.3.
|
|
6
|
+
"version": "1.3.38",
|
|
7
7
|
"description": "EyeProlog turns facts and rules into answers and proofs.",
|
|
8
8
|
"type": "module",
|
|
9
9
|
"main": "./index.js",
|
|
@@ -64,6 +64,7 @@
|
|
|
64
64
|
"report:wg17": "node tools/report-wg17-syntax-coverage.mjs",
|
|
65
65
|
"report:wg17-syntax": "node tools/report-wg17-syntax-coverage.mjs",
|
|
66
66
|
"preversion": "npm test && node test/run-conformance-report.mjs conformance-report.md",
|
|
67
|
-
"postversion": "git push origin HEAD --follow-tags"
|
|
67
|
+
"postversion": "git push origin HEAD --follow-tags",
|
|
68
|
+
"test:architecture": "node test/run-architecture.mjs"
|
|
68
69
|
}
|
|
69
70
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# EyeProlog source architecture
|
|
2
|
+
|
|
3
|
+
The runtime is intentionally layered so semantic modules do not depend back on
|
|
4
|
+
higher-level frontends.
|
|
5
|
+
|
|
6
|
+
## Layers
|
|
7
|
+
|
|
8
|
+
1. **Kernel representation and syntax** — `term.js`, `number-value.js`,
|
|
9
|
+
`syntax-scan.js`, `parser.js`, `write.js`, `errors.js`.
|
|
10
|
+
2. **Program preparation** — `program.js` plus `program-analysis.js` and
|
|
11
|
+
`program-indexing.js`. Static recursion/Datalog/WFS classification lives in
|
|
12
|
+
`program-analysis.js`; compact clauses and candidate indexes live in
|
|
13
|
+
`program-indexing.js`.
|
|
14
|
+
3. **Execution** — `solver.js`, `cleanup.js`, `io.js`, `datalog.js`, `wfs.js`,
|
|
15
|
+
`clpz.js`. `cleanup.js` owns lifecycle-aware disposal of protected builtin
|
|
16
|
+
iterators and registers the normal-profile cleanup controls without making
|
|
17
|
+
`solver.js` depend back on the language registry.
|
|
18
|
+
4. **Language services** — `iso.js`, `iso-arithmetic.js`, `dcg.js`,
|
|
19
|
+
`standard-library.js`, and `src/lib/`.
|
|
20
|
+
5. **Frontends/tools** — `execute.js`, `repl.js`, `cli.js`, `quads.js`,
|
|
21
|
+
`explain.js`, and the playground worker.
|
|
22
|
+
|
|
23
|
+
`iso.js` and `program.js` remain facade modules for their existing exports, so
|
|
24
|
+
this refactor does not change the public JavaScript API.
|
|
25
|
+
|
|
26
|
+
## Dependency rule
|
|
27
|
+
|
|
28
|
+
Dependencies should point down or sideways within a layer, never back from a
|
|
29
|
+
kernel component into the ISO registry or a frontend. In particular,
|
|
30
|
+
`errors.js` owns `PrologError` and `HaltSignal`; DCG expansion can therefore
|
|
31
|
+
report processor errors without importing `iso.js` and creating an
|
|
32
|
+
`iso.js <-> dcg.js` cycle.
|
|
33
|
+
|
|
34
|
+
The JavaScript runtime stays flat directly under `src/`; the existing `src/lib/`
|
|
35
|
+
contains Prolog library sources rather than JavaScript runtime modules. The architecture
|
|
36
|
+
test rejects JavaScript import cycles under `src/`. Cleanup lifecycle hooks are
|
|
37
|
+
installed from the public API and CLI entry paths; `standard-library.js` only
|
|
38
|
+
registers the predicates, so the execution layer remains acyclic.
|
|
39
|
+
|
|
40
|
+
## Performance rule
|
|
41
|
+
|
|
42
|
+
Architecture changes must not add runtime strategy objects, callbacks, or
|
|
43
|
+
extra dispatch in solver hot paths. Existing scalar/indexed solver paths stay
|
|
44
|
+
as direct function calls. Candidate indexing is separated physically but
|
|
45
|
+
retains the same data structures and selection functions.
|
|
46
|
+
|
|
47
|
+
Large solver fast paths deliberately remain co-located in `solver.js` until a
|
|
48
|
+
split can demonstrate benchmark parity. A cleaner file layout is not worth a
|
|
49
|
+
runtime regression.
|
package/src/cleanup.js
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
// call_cleanup/2 and setup_call_cleanup/3 plus solver lifecycle support.
|
|
2
|
+
//
|
|
3
|
+
// EyeProlog's solver is demand-driven: a yielded builtin remains represented by
|
|
4
|
+
// a resumeBuiltin frame until its next answer is requested. Cleanup predicates
|
|
5
|
+
// therefore need two pieces of host support:
|
|
6
|
+
// * discarded resumeBuiltin iterators must be closed when search is pruned;
|
|
7
|
+
// * a cleanup iterator can report that its protected goal has no alternatives,
|
|
8
|
+
// so the REPL does not need speculative look-ahead to suppress a choicepoint.
|
|
9
|
+
import { PrologError } from './errors.js';
|
|
10
|
+
import { deref } from './term.js';
|
|
11
|
+
|
|
12
|
+
const cleanupLifecycleInstalled = Symbol('eyeprolog.cleanupLifecycleInstalled');
|
|
13
|
+
const cleanupLifecycleState = Symbol('eyeprolog.cleanupLifecycleState');
|
|
14
|
+
const searchStackPatched = Symbol('eyeprolog.cleanupSearchStackPatched');
|
|
15
|
+
|
|
16
|
+
export function installCleanupLifecycle(Solver) {
|
|
17
|
+
const prototype = Solver.prototype;
|
|
18
|
+
if (prototype[cleanupLifecycleInstalled]) return;
|
|
19
|
+
Object.defineProperty(prototype, cleanupLifecycleInstalled, { value: true });
|
|
20
|
+
|
|
21
|
+
const originalSolve = prototype.solve;
|
|
22
|
+
prototype.solve = function* cleanupAwareSolve(...args) {
|
|
23
|
+
const state = ensureLifecycleState(this);
|
|
24
|
+
const owner = {};
|
|
25
|
+
const iterator = originalSolve.apply(this, args);
|
|
26
|
+
let completed = false;
|
|
27
|
+
let thrown = null;
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
while (true) {
|
|
31
|
+
const result = withOwner(state, owner, () => iterator.next());
|
|
32
|
+
if (result.done) {
|
|
33
|
+
completed = true;
|
|
34
|
+
return result.value;
|
|
35
|
+
}
|
|
36
|
+
yield result.value;
|
|
37
|
+
}
|
|
38
|
+
} catch (error) {
|
|
39
|
+
thrown = error;
|
|
40
|
+
throw error;
|
|
41
|
+
} finally {
|
|
42
|
+
// If the consumer prunes this solve() while it is suspended at an answer,
|
|
43
|
+
// close pending builtin iterators before the original solver removes its
|
|
44
|
+
// explicit search stack. Cleanup exceptions are observable on an ordinary
|
|
45
|
+
// cut/return, but never replace an exception already propagating outward.
|
|
46
|
+
if (!completed) {
|
|
47
|
+
let closeError = null;
|
|
48
|
+
try {
|
|
49
|
+
closeOwnedSearchStacks(this, state, owner, thrown != null);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
closeError = error;
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
withOwner(state, owner, () => iterator.return?.());
|
|
55
|
+
} catch (error) {
|
|
56
|
+
if (closeError == null) closeError = error;
|
|
57
|
+
}
|
|
58
|
+
if (thrown == null && closeError != null) throw closeError;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
prototype.hasPendingAlternatives = function cleanupAwarePendingAlternatives() {
|
|
64
|
+
return this.solveStacks.some((stack) => stack.some(frameHasPendingAlternative));
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function registerCleanupBuiltins(registry) {
|
|
69
|
+
registry.add('call_cleanup', 2, callCleanupBuiltin);
|
|
70
|
+
registry.add('setup_call_cleanup', 3, setupCallCleanupBuiltin);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function ensureLifecycleState(solver) {
|
|
74
|
+
if (solver[cleanupLifecycleState] != null) return solver[cleanupLifecycleState];
|
|
75
|
+
const state = {
|
|
76
|
+
owner: null,
|
|
77
|
+
stackOwners: new WeakMap(),
|
|
78
|
+
};
|
|
79
|
+
Object.defineProperty(solver, cleanupLifecycleState, { value: state });
|
|
80
|
+
|
|
81
|
+
const solveStacks = solver.solveStacks;
|
|
82
|
+
const originalPush = solveStacks.push;
|
|
83
|
+
const originalSplice = solveStacks.splice;
|
|
84
|
+
|
|
85
|
+
solveStacks.push = function cleanupAwarePush(...stacks) {
|
|
86
|
+
for (const stack of stacks) {
|
|
87
|
+
state.stackOwners.set(stack, state.owner);
|
|
88
|
+
patchSearchStack(stack);
|
|
89
|
+
}
|
|
90
|
+
return originalPush.apply(this, stacks);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
solveStacks.splice = function cleanupAwareSplice(start, deleteCount, ...items) {
|
|
94
|
+
const removed = originalSplice.call(this, start, deleteCount, ...items);
|
|
95
|
+
// This path is used by Solver.solve()'s finally block. If the solve is
|
|
96
|
+
// unwinding an exception, a cleanup exception must not replace it. Normal
|
|
97
|
+
// consumer pruning closes frames earlier in cleanupAwareSolve(), where
|
|
98
|
+
// cleanup errors are allowed to propagate.
|
|
99
|
+
for (let index = removed.length - 1; index >= 0; index--) {
|
|
100
|
+
closeSearchStack(removed[index], true);
|
|
101
|
+
}
|
|
102
|
+
return removed;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
return state;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function withOwner(state, owner, operation) {
|
|
109
|
+
const previous = state.owner;
|
|
110
|
+
state.owner = owner;
|
|
111
|
+
try {
|
|
112
|
+
return operation();
|
|
113
|
+
} finally {
|
|
114
|
+
state.owner = previous;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function patchSearchStack(stack) {
|
|
119
|
+
if (stack[searchStackPatched]) return;
|
|
120
|
+
Object.defineProperty(stack, searchStackPatched, { value: true });
|
|
121
|
+
const originalSplice = stack.splice;
|
|
122
|
+
stack.splice = function cleanupAwareSearchSplice(start, deleteCount, ...items) {
|
|
123
|
+
const removed = originalSplice.call(this, start, deleteCount, ...items);
|
|
124
|
+
// A cut prunes explicit search frames synchronously. Closing a discarded
|
|
125
|
+
// builtin iterator here gives call_cleanup/2 its required cut semantics and
|
|
126
|
+
// also lets existing builtin finally blocks release their child solvers.
|
|
127
|
+
closeFrames(removed, false);
|
|
128
|
+
return removed;
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function closeOwnedSearchStacks(solver, state, owner, suppressErrors) {
|
|
133
|
+
const stacks = solver.solveStacks
|
|
134
|
+
.filter((stack) => state.stackOwners.get(stack) === owner)
|
|
135
|
+
.slice()
|
|
136
|
+
.reverse();
|
|
137
|
+
let firstError = null;
|
|
138
|
+
for (const stack of stacks) {
|
|
139
|
+
try {
|
|
140
|
+
closeSearchStack(stack, suppressErrors);
|
|
141
|
+
} catch (error) {
|
|
142
|
+
if (firstError == null) firstError = error;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (!suppressErrors && firstError != null) throw firstError;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function closeSearchStack(stack, suppressErrors) {
|
|
149
|
+
if (stack == null || stack.length === 0) return;
|
|
150
|
+
// Use Array.prototype directly: calling the patched splice would always use
|
|
151
|
+
// cut semantics (propagating errors) even while an exception is unwinding.
|
|
152
|
+
const removed = Array.prototype.splice.call(stack, 0, stack.length);
|
|
153
|
+
closeFrames(removed, suppressErrors);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function closeFrames(frames, suppressErrors) {
|
|
157
|
+
let firstError = null;
|
|
158
|
+
for (let index = frames.length - 1; index >= 0; index--) {
|
|
159
|
+
const frame = frames[index];
|
|
160
|
+
if (frame?.kind !== 'resumeBuiltin' || typeof frame.iterator?.return !== 'function') continue;
|
|
161
|
+
try {
|
|
162
|
+
frame.iterator.prepareClose?.({ unwinding: suppressErrors });
|
|
163
|
+
frame.iterator.return();
|
|
164
|
+
} catch (error) {
|
|
165
|
+
if (firstError == null) firstError = error;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (!suppressErrors && firstError != null) throw firstError;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function frameHasPendingAlternative(frame) {
|
|
172
|
+
if (frame?.kind !== 'resumeBuiltin') return true;
|
|
173
|
+
const predicate = frame.iterator?.hasPendingAlternatives;
|
|
174
|
+
if (typeof predicate !== 'function') return true;
|
|
175
|
+
return predicate.call(frame.iterator);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function callableTerm(term, env) {
|
|
179
|
+
const value = deref(term, env);
|
|
180
|
+
if (value.type === 'var') throw new PrologError('instantiation_error');
|
|
181
|
+
if (value.type !== 'atom' && value.type !== 'compound') {
|
|
182
|
+
throw new PrologError('type_error(callable)', value);
|
|
183
|
+
}
|
|
184
|
+
return value;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function firstSetupSolution(solver, setup, env) {
|
|
188
|
+
const child = solver.cloneForInnerGoal(1);
|
|
189
|
+
const iterator = child.solve([callableTerm(setup, env)], env.clone(), 0);
|
|
190
|
+
try {
|
|
191
|
+
const result = iterator.next();
|
|
192
|
+
return result.done ? null : result.value;
|
|
193
|
+
} finally {
|
|
194
|
+
try {
|
|
195
|
+
iterator.return?.();
|
|
196
|
+
} finally {
|
|
197
|
+
solver.absorbStatsFrom(child);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function callCleanupBuiltin({ solver, goal, env }) {
|
|
203
|
+
const cleanup = callableTerm(goal.args[1], env);
|
|
204
|
+
return cleanupProtectedIterator(solver, goal.args[0], cleanup, env);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function setupCallCleanupBuiltin({ solver, goal, env }) {
|
|
208
|
+
let protectedIterator = null;
|
|
209
|
+
let pending = true;
|
|
210
|
+
const iterator = (function* setupThenProtected() {
|
|
211
|
+
const setupEnv = firstSetupSolution(solver, goal.args[0], env);
|
|
212
|
+
if (setupEnv == null) {
|
|
213
|
+
pending = false;
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
// The cleanup is validated after Setup succeeds and before Goal starts, as
|
|
217
|
+
// in the WG17 cleanup proposal. A variable bound by Setup may therefore be
|
|
218
|
+
// used as Cleanup.
|
|
219
|
+
const cleanup = callableTerm(goal.args[2], setupEnv);
|
|
220
|
+
protectedIterator = cleanupProtectedIterator(solver, goal.args[1], cleanup, setupEnv);
|
|
221
|
+
try {
|
|
222
|
+
while (true) {
|
|
223
|
+
const result = protectedIterator.next();
|
|
224
|
+
pending = protectedIterator.hasPendingAlternatives();
|
|
225
|
+
if (result.done) {
|
|
226
|
+
pending = false;
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
yield result.value;
|
|
230
|
+
if (!pending) return;
|
|
231
|
+
}
|
|
232
|
+
} finally {
|
|
233
|
+
protectedIterator.return?.();
|
|
234
|
+
}
|
|
235
|
+
})();
|
|
236
|
+
iterator.hasPendingAlternatives = () => pending;
|
|
237
|
+
iterator.prepareClose = (reason) => protectedIterator?.prepareClose?.(reason);
|
|
238
|
+
return iterator;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function cleanupProtectedIterator(solver, protectedGoal, cleanup, initialEnv) {
|
|
242
|
+
const child = solver.cloneForInnerGoal();
|
|
243
|
+
let childIterator = null;
|
|
244
|
+
let cleanupEnv = initialEnv;
|
|
245
|
+
let pending = true;
|
|
246
|
+
let cleaned = false;
|
|
247
|
+
let bodyError = null;
|
|
248
|
+
let unwindToSetupBindings = false;
|
|
249
|
+
|
|
250
|
+
const performCleanup = () => {
|
|
251
|
+
if (cleaned) return;
|
|
252
|
+
// Mark first so a throwing Cleanup is still considered executed exactly once.
|
|
253
|
+
cleaned = true;
|
|
254
|
+
runCleanupOnce(solver, cleanup, cleanupEnv);
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const iterator = (function* protectedWithCleanup() {
|
|
258
|
+
try {
|
|
259
|
+
childIterator = child.solve([callableTerm(protectedGoal, initialEnv)], initialEnv.clone(), 0);
|
|
260
|
+
while (true) {
|
|
261
|
+
const result = childIterator.next();
|
|
262
|
+
if (result.done) {
|
|
263
|
+
pending = false;
|
|
264
|
+
// At the latest cleanup moment, Goal bindings have been undone.
|
|
265
|
+
cleanupEnv = initialEnv;
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
cleanupEnv = result.value;
|
|
270
|
+
pending = child.hasPendingAlternatives();
|
|
271
|
+
if (!pending) {
|
|
272
|
+
// A deterministic final answer is known from the solver's explicit
|
|
273
|
+
// pending-frame state. Finalize the protected iterator and cleanup now,
|
|
274
|
+
// before yielding the answer, without computing a speculative answer.
|
|
275
|
+
childIterator.return?.();
|
|
276
|
+
childIterator = null;
|
|
277
|
+
performCleanup();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
yield result.value;
|
|
281
|
+
if (!pending) return;
|
|
282
|
+
}
|
|
283
|
+
} catch (error) {
|
|
284
|
+
bodyError = error;
|
|
285
|
+
// Exceptions from Goal unwind its bindings before Cleanup is called.
|
|
286
|
+
cleanupEnv = initialEnv;
|
|
287
|
+
throw error;
|
|
288
|
+
} finally {
|
|
289
|
+
let childCloseError = null;
|
|
290
|
+
if (childIterator != null) {
|
|
291
|
+
try {
|
|
292
|
+
childIterator.return?.();
|
|
293
|
+
} catch (error) {
|
|
294
|
+
childCloseError = error;
|
|
295
|
+
}
|
|
296
|
+
childIterator = null;
|
|
297
|
+
}
|
|
298
|
+
solver.absorbStatsFrom(child);
|
|
299
|
+
|
|
300
|
+
if (unwindToSetupBindings) cleanupEnv = initialEnv;
|
|
301
|
+
if (!cleaned) {
|
|
302
|
+
try {
|
|
303
|
+
performCleanup();
|
|
304
|
+
} catch (cleanupError) {
|
|
305
|
+
// When the protected goal (or an inner cleanup) already raised, it has
|
|
306
|
+
// priority over this cleanup exception. An exception from an outer
|
|
307
|
+
// continuation is preserved by cleanup-aware stack teardown too.
|
|
308
|
+
if (bodyError == null && childCloseError == null) throw cleanupError;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (bodyError == null && childCloseError != null) throw childCloseError;
|
|
312
|
+
}
|
|
313
|
+
})();
|
|
314
|
+
|
|
315
|
+
iterator.hasPendingAlternatives = () => pending;
|
|
316
|
+
iterator.prepareClose = ({ unwinding } = {}) => {
|
|
317
|
+
unwindToSetupBindings = unwinding === true;
|
|
318
|
+
};
|
|
319
|
+
return iterator;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function runCleanupOnce(solver, cleanup, env) {
|
|
323
|
+
const child = solver.cloneForInnerGoal(1);
|
|
324
|
+
const iterator = child.solve([cleanup], env.clone(), 0);
|
|
325
|
+
let result;
|
|
326
|
+
try {
|
|
327
|
+
result = iterator.next();
|
|
328
|
+
} finally {
|
|
329
|
+
try {
|
|
330
|
+
iterator.return?.();
|
|
331
|
+
} finally {
|
|
332
|
+
solver.absorbStatsFrom(child);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
// Cleanup is semidet only as a control operation: failure is ignored, and
|
|
336
|
+
// bindings made solely by Cleanup are intentionally not added to an answer
|
|
337
|
+
// already produced by Goal.
|
|
338
|
+
return !result.done;
|
|
339
|
+
}
|
package/src/cli.js
CHANGED
|
@@ -174,7 +174,7 @@ export async function main(argv) {
|
|
|
174
174
|
|
|
175
175
|
async function loadEngine() {
|
|
176
176
|
if (engineModule == null) {
|
|
177
|
-
const [term, parser, program, solver, iso, library, write, quads, execute] = await Promise.all([
|
|
177
|
+
const [term, parser, program, solver, iso, library, write, quads, execute, cleanup] = await Promise.all([
|
|
178
178
|
import('./term.js'),
|
|
179
179
|
import('./parser.js'),
|
|
180
180
|
import('./program.js'),
|
|
@@ -184,7 +184,11 @@ async function loadEngine() {
|
|
|
184
184
|
import('./write.js'),
|
|
185
185
|
import('./quads.js'),
|
|
186
186
|
import('./execute.js'),
|
|
187
|
+
import('./cleanup.js'),
|
|
187
188
|
]);
|
|
189
|
+
// CLI loading is an entry-point layer above solver.js and the standard
|
|
190
|
+
// registry, so lifecycle installation stays acyclic.
|
|
191
|
+
cleanup.installCleanupLifecycle(solver.Solver);
|
|
188
192
|
engineModule = { ...term, ...parser, ...program, ...solver, ...iso, ...library, ...write, ...quads, ...execute };
|
|
189
193
|
}
|
|
190
194
|
return engineModule;
|
package/src/dcg.js
CHANGED
package/src/errors.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Runtime control and ISO processor error types shared across subsystems.
|
|
2
|
+
// Keep these independent of the ISO builtin registry so syntax, DCG, program,
|
|
3
|
+
// and solver layers can report Prolog errors without importing the whole ISO
|
|
4
|
+
// implementation (and without creating semantic-layer import cycles).
|
|
5
|
+
import { termToString } from './term.js';
|
|
6
|
+
|
|
7
|
+
export class PrologError extends Error {
|
|
8
|
+
constructor(formal, culprit = null) {
|
|
9
|
+
const detail = culprit == null ? formal : `${formal}, ${termToString(culprit)}`;
|
|
10
|
+
super(`error(${detail})`);
|
|
11
|
+
this.name = 'PrologError';
|
|
12
|
+
this.formal = formal;
|
|
13
|
+
this.culprit = culprit;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class HaltSignal extends Error {
|
|
18
|
+
constructor(code = 0) {
|
|
19
|
+
super(`halt(${code})`);
|
|
20
|
+
this.name = 'HaltSignal';
|
|
21
|
+
this.code = code;
|
|
22
|
+
}
|
|
23
|
+
}
|
package/src/index.js
CHANGED
|
@@ -27,6 +27,7 @@ export {
|
|
|
27
27
|
export { StreamManager } from './io.js';
|
|
28
28
|
export { runQuads } from './quads.js';
|
|
29
29
|
|
|
30
|
+
import { installCleanupLifecycle } from './cleanup.js';
|
|
30
31
|
import { Program, autoloadProgramGoals } from './program.js';
|
|
31
32
|
import { Solver } from './solver.js';
|
|
32
33
|
import { whyNoProof, whyProof } from './explain.js';
|
|
@@ -34,6 +35,10 @@ import { getStrictIsoRegistry } from './iso.js';
|
|
|
34
35
|
import { getEyePrologRegistry } from './standard-library.js';
|
|
35
36
|
import { executeGoals, normalizeGoals } from './execute.js';
|
|
36
37
|
|
|
38
|
+
// The public API is an entry point above the solver/registry layers, so it can
|
|
39
|
+
// install pruning-aware iterator disposal without introducing an import cycle.
|
|
40
|
+
installCleanupLifecycle(Solver);
|
|
41
|
+
|
|
37
42
|
export function run(source, options = {}) {
|
|
38
43
|
const includeWhy = options.proof === true || options.why === true || options.explain === true;
|
|
39
44
|
const requestedStrictIso = options.isoStrict === true;
|