eyeprolog 1.3.37 → 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 +22 -3
- package/examples/book/chapter-37/01-write_event.pl +8 -5
- package/package.json +1 -1
- package/src/ARCHITECTURE.md +7 -2
- package/src/cleanup.js +339 -0
- package/src/cli.js +5 -1
- package/src/index.js +5 -0
- package/src/standard-library.js +2 -0
- package/test/run-all.mjs +2 -0
- package/test/run-cleanup.mjs +135 -0
- package/test/run-regression.mjs +5 -1
- package/the-art-of-eyeprolog.md +55 -12
- package/why-eyeprolog.md +18 -5
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).
|
|
@@ -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
package/src/ARCHITECTURE.md
CHANGED
|
@@ -11,7 +11,10 @@ higher-level frontends.
|
|
|
11
11
|
`program-indexing.js`. Static recursion/Datalog/WFS classification lives in
|
|
12
12
|
`program-analysis.js`; compact clauses and candidate indexes live in
|
|
13
13
|
`program-indexing.js`.
|
|
14
|
-
3. **Execution** — `solver.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.
|
|
15
18
|
4. **Language services** — `iso.js`, `iso-arithmetic.js`, `dcg.js`,
|
|
16
19
|
`standard-library.js`, and `src/lib/`.
|
|
17
20
|
5. **Frontends/tools** — `execute.js`, `repl.js`, `cli.js`, `quads.js`,
|
|
@@ -30,7 +33,9 @@ report processor errors without importing `iso.js` and creating an
|
|
|
30
33
|
|
|
31
34
|
The JavaScript runtime stays flat directly under `src/`; the existing `src/lib/`
|
|
32
35
|
contains Prolog library sources rather than JavaScript runtime modules. The architecture
|
|
33
|
-
test rejects JavaScript import cycles under `src/`.
|
|
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.
|
|
34
39
|
|
|
35
40
|
## Performance rule
|
|
36
41
|
|
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/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;
|
package/src/standard-library.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// source-level interop autoloader declared below.
|
|
5
5
|
import { PrologError, createDefaultRegistry, eyePrologLibraryBuiltins } from './iso.js';
|
|
6
6
|
import { clpzBuiltins } from './clpz.js';
|
|
7
|
+
import { registerCleanupBuiltins } from './cleanup.js';
|
|
7
8
|
import { fs, isNode, memoryStatistics } from './platform.js';
|
|
8
9
|
import { ATOM, VAR, atom, deref, numberTerm, unify } from './term.js';
|
|
9
10
|
|
|
@@ -181,6 +182,7 @@ export function createEyePrologRegistry() {
|
|
|
181
182
|
registry.add('statistics', 0, statisticsBuiltin, { deterministic: true });
|
|
182
183
|
registry.add('statistics', 2, statisticsValueBuiltin);
|
|
183
184
|
registry.add('tnot', 1, tabledNegationBuiltin, { deterministic: true });
|
|
185
|
+
registerCleanupBuiltins(registry);
|
|
184
186
|
eyePrologLibraryBuiltins.register(registry);
|
|
185
187
|
clpzBuiltins.register(registry);
|
|
186
188
|
registry.eyePrologLibrary = true;
|
package/test/run-all.mjs
CHANGED
|
@@ -12,6 +12,7 @@ import { runBookExamples } from './run-book-examples.mjs';
|
|
|
12
12
|
import { runWg17 } from './run-wg17.mjs';
|
|
13
13
|
import { runOpenRuleBenchChecks } from './run-openrulebench.mjs';
|
|
14
14
|
import { runArchitecture } from './run-architecture.mjs';
|
|
15
|
+
import { runCleanup } from './run-cleanup.mjs';
|
|
15
16
|
|
|
16
17
|
await runStandalone(async (reporter) => {
|
|
17
18
|
runConformance(reporter);
|
|
@@ -19,6 +20,7 @@ await runStandalone(async (reporter) => {
|
|
|
19
20
|
runWg17(reporter);
|
|
20
21
|
runOpenRuleBenchChecks(reporter);
|
|
21
22
|
runArchitecture(reporter);
|
|
23
|
+
runCleanup(reporter);
|
|
22
24
|
runRegression(reporter);
|
|
23
25
|
await runPlayground(reporter);
|
|
24
26
|
runExamples(reporter);
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Regression coverage for call_cleanup/2 and setup_call_cleanup/3.
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import {
|
|
7
|
+
assertEqual,
|
|
8
|
+
assertIncludes,
|
|
9
|
+
assertNotIncludes,
|
|
10
|
+
isMainModule,
|
|
11
|
+
runStandalone,
|
|
12
|
+
} from './test-style.mjs';
|
|
13
|
+
|
|
14
|
+
const testRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)));
|
|
15
|
+
const packageRoot = path.resolve(testRoot, '..');
|
|
16
|
+
const bin = path.join(packageRoot, 'bin', 'eyeprolog.js');
|
|
17
|
+
|
|
18
|
+
export function runCleanup(reporter) {
|
|
19
|
+
reporter.section('Cleanup control');
|
|
20
|
+
|
|
21
|
+
reporter.test('call_cleanup/2 deterministic success has no leftover choicepoint', () => {
|
|
22
|
+
const result = runRepl('call_cleanup(true,true).\nhalt.\n');
|
|
23
|
+
assertEqual(result.status, 0, 'status');
|
|
24
|
+
assertIncludes(result.stdout, 'true.', 'stdout');
|
|
25
|
+
assertNotIncludes(result.stdout, '\n;', 'stdout');
|
|
26
|
+
assertEqual(result.stderr, '', 'stderr');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
reporter.test('call_cleanup/2 runs cleanup when the user stops enumeration', () => {
|
|
30
|
+
const result = runRepl('call_cleanup((X=one;X=two),write(issue48_cleanup)).\n.\nhalt.\n');
|
|
31
|
+
assertEqual(result.status, 0, 'status');
|
|
32
|
+
assertIncludes(result.stdout, 'X = one', 'first answer');
|
|
33
|
+
assertIncludes(result.stdout, 'issue48_cleanup', 'cleanup output');
|
|
34
|
+
assertNotIncludes(result.stdout, 'X = two', 'unrequested answer');
|
|
35
|
+
assertEqual(count(result.stdout, 'issue48_cleanup'), 1, 'cleanup count');
|
|
36
|
+
assertEqual(result.stderr, '', 'stderr');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
reporter.test('call_cleanup/2 runs before continuation after cut and sees current bindings', () => {
|
|
40
|
+
const result = runRepl(
|
|
41
|
+
'call_cleanup((X=one;X=two),assertz(issue48_saved(X))),!,issue48_saved(Y).\nhalt.\n',
|
|
42
|
+
);
|
|
43
|
+
assertEqual(result.status, 0, 'status');
|
|
44
|
+
assertIncludes(result.stdout, 'X = one, Y = one.', 'cut cleanup answer');
|
|
45
|
+
assertNotIncludes(result.stdout, 'false.', 'stdout');
|
|
46
|
+
assertEqual(result.stderr, '', 'stderr');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
reporter.test('call_cleanup/2 preserves protected exception over cleanup exception', () => {
|
|
50
|
+
const result = runRepl('catch(call_cleanup(throw(original),throw(cleanup)),E,true).\nhalt.\n');
|
|
51
|
+
assertEqual(result.status, 0, 'status');
|
|
52
|
+
assertIncludes(result.stdout, 'E = original.', 'caught exception');
|
|
53
|
+
assertNotIncludes(result.stdout, 'E = cleanup', 'exception priority');
|
|
54
|
+
assertEqual(result.stderr, '', 'stderr');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
reporter.test('exception unwind removes Goal bindings before Cleanup', () => {
|
|
58
|
+
const result = runRepl(
|
|
59
|
+
'catch((setup_call_cleanup(true,(G=bound;G=other),(var(G)->write(cleanup_unbound);write(cleanup_bound))),throw(cont)),E,true).\nhalt.\n',
|
|
60
|
+
);
|
|
61
|
+
assertEqual(result.status, 0, 'status');
|
|
62
|
+
assertIncludes(result.stdout, 'cleanup_unbound', 'cleanup binding state');
|
|
63
|
+
assertNotIncludes(result.stdout, 'cleanup_bound', 'unwound Goal binding');
|
|
64
|
+
assertIncludes(result.stdout, 'E = cont.', 'continuation exception');
|
|
65
|
+
assertEqual(result.stderr, '', 'stderr');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
reporter.test('nested call_cleanup/2 cleanups run inside-out on cut', () => {
|
|
69
|
+
const result = runRepl(
|
|
70
|
+
'call_cleanup(call_cleanup((X=one;X=two),write(inner_cleanup)),write(outer_cleanup)),!,true.\nhalt.\n',
|
|
71
|
+
);
|
|
72
|
+
assertEqual(result.status, 0, 'status');
|
|
73
|
+
const inner = result.stdout.indexOf('inner_cleanup');
|
|
74
|
+
const outer = result.stdout.indexOf('outer_cleanup');
|
|
75
|
+
if (inner < 0 || outer < 0 || inner >= outer) {
|
|
76
|
+
throw new Error(`cleanup order mismatch\nstdout: ${JSON.stringify(result.stdout)}`);
|
|
77
|
+
}
|
|
78
|
+
assertEqual(count(result.stdout, 'inner_cleanup'), 1, 'inner cleanup count');
|
|
79
|
+
assertEqual(count(result.stdout, 'outer_cleanup'), 1, 'outer cleanup count');
|
|
80
|
+
assertEqual(result.stderr, '', 'stderr');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
reporter.test('setup_call_cleanup/3 calls Setup once and ignores cleanup failure', () => {
|
|
84
|
+
const result = runRepl('setup_call_cleanup((X=one;X=two),true,fail).\nhalt.\n');
|
|
85
|
+
assertEqual(result.status, 0, 'status');
|
|
86
|
+
assertIncludes(result.stdout, 'X = one.', 'setup result');
|
|
87
|
+
assertNotIncludes(result.stdout, 'X = two', 'second setup solution');
|
|
88
|
+
assertNotIncludes(result.stdout, '\n;', 'leftover choicepoint');
|
|
89
|
+
assertEqual(result.stderr, '', 'stderr');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
reporter.test('setup_call_cleanup/3 does not install cleanup when Setup fails', () => {
|
|
93
|
+
const result = runRepl('setup_call_cleanup(fail,true,write(should_not_run)).\nhalt.\n');
|
|
94
|
+
assertEqual(result.status, 0, 'status');
|
|
95
|
+
assertIncludes(result.stdout, 'false.', 'failed setup');
|
|
96
|
+
assertNotIncludes(result.stdout, 'should_not_run', 'cleanup output');
|
|
97
|
+
assertEqual(result.stderr, '', 'stderr');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
reporter.test('setup_call_cleanup/3 validates Cleanup after successful Setup', () => {
|
|
101
|
+
const result = runRepl('catch(setup_call_cleanup(true,true,_),E,true).\nhalt.\n');
|
|
102
|
+
assertEqual(result.status, 0, 'status');
|
|
103
|
+
assertIncludes(result.stdout, 'instantiation_error', 'cleanup validation error');
|
|
104
|
+
assertEqual(result.stderr, '', 'stderr');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
reporter.test('cleanup predicates remain outside strict ISO core', () => {
|
|
108
|
+
const result = runRepl('catch(call_cleanup(true,true),E,true).\nhalt.\n', ['--iso-strict']);
|
|
109
|
+
assertEqual(result.status, 0, 'status');
|
|
110
|
+
assertIncludes(result.stdout, 'existence_error(procedure', 'strict ISO error');
|
|
111
|
+
assertIncludes(result.stdout, 'call_cleanup', 'strict ISO predicate');
|
|
112
|
+
assertEqual(result.stderr, '', 'stderr');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
reporter.sectionTotal('cleanup');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function runRepl(input, args = []) {
|
|
119
|
+
const result = spawnSync(process.execPath, [bin, ...args], {
|
|
120
|
+
cwd: packageRoot,
|
|
121
|
+
input,
|
|
122
|
+
encoding: 'utf8',
|
|
123
|
+
timeout: 10000,
|
|
124
|
+
});
|
|
125
|
+
if (result.error) throw result.error;
|
|
126
|
+
return result;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function count(text, needle) {
|
|
130
|
+
return String(text).split(needle).length - 1;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (isMainModule(import.meta.url)) {
|
|
134
|
+
await runStandalone((reporter) => runCleanup(reporter));
|
|
135
|
+
}
|
package/test/run-regression.mjs
CHANGED
|
@@ -4471,7 +4471,7 @@ answer(ok) :-
|
|
|
4471
4471
|
assertEqual(Boolean(registry.get('is', 2)), true, 'ISO is/2 exists');
|
|
4472
4472
|
assertEqual(Boolean(registry.get('append', 3)), false, 'append/3 is not ISO core');
|
|
4473
4473
|
assertEqual(library.eyePrologLibrary, true, 'complete registry marker');
|
|
4474
|
-
assertEqual(library.defs.size,
|
|
4474
|
+
assertEqual(library.defs.size, 160, 'EyeProlog registry contains ISO definitions, cleanup controls, observability extensions, WFS tnot/1, and private library adapters');
|
|
4475
4475
|
assertEqual(Boolean(registry.get('phrase', 2)), true, 'Part 3 phrase/2 exists');
|
|
4476
4476
|
assertEqual(Boolean(registry.get('phrase', 3)), true, 'Part 3 phrase/3 exists');
|
|
4477
4477
|
assertEqual(registry.get('statistics', 0), null, 'statistics/0 is absent from the ISO registry');
|
|
@@ -4482,6 +4482,10 @@ answer(ok) :-
|
|
|
4482
4482
|
assertEqual(Boolean(library.get('tnot', 1)), true, 'tnot/1 is an EyeProlog WFS extension');
|
|
4483
4483
|
assertEqual(registry.get('time', 1), null, 'time/1 is absent from the ISO registry');
|
|
4484
4484
|
assertEqual(Boolean(library.get('time', 1)), true, 'time/1 is an EyeProlog timing extension');
|
|
4485
|
+
assertEqual(registry.get('call_cleanup', 2), null, 'call_cleanup/2 is absent from the ISO registry');
|
|
4486
|
+
assertEqual(Boolean(library.get('call_cleanup', 2)), true, 'call_cleanup/2 is an EyeProlog cleanup control');
|
|
4487
|
+
assertEqual(registry.get('setup_call_cleanup', 3), null, 'setup_call_cleanup/3 is absent from the ISO registry');
|
|
4488
|
+
assertEqual(Boolean(library.get('setup_call_cleanup', 3)), true, 'setup_call_cleanup/3 is an EyeProlog cleanup control');
|
|
4485
4489
|
assertEqual(registeredNativeEyePrologLibraryNames().length, 41, 'public native EyeProlog builtin count');
|
|
4486
4490
|
assertEqual(eyePrologPortableLibraryIndicators.length, 87, 'portable Prolog library count');
|
|
4487
4491
|
assertEqual(eyePrologInteropLibraryIndicators.length, 29, 'cross-implementation interop profile count');
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -1947,6 +1947,13 @@ implements the shared ISO Part 3 grammar-rule and dynamic-body expansion
|
|
|
1947
1947
|
without depending back on the ISO registry. This keeps the low-level syntax and
|
|
1948
1948
|
error layers acyclic while preserving the existing `src/iso.js` exports.
|
|
1949
1949
|
|
|
1950
|
+
`src/cleanup.js` is an execution-layer sibling of the solver. It installs
|
|
1951
|
+
lifecycle-aware closing of protected builtin iterators from the supported API
|
|
1952
|
+
and CLI entry paths and registers `call_cleanup/2` and
|
|
1953
|
+
`setup_call_cleanup/3` for the normal EyeProlog profile. The standard-library
|
|
1954
|
+
layer does not import the solver back through this module, preserving the
|
|
1955
|
+
acyclic source graph.
|
|
1956
|
+
|
|
1950
1957
|
Program preparation follows the same pattern. `src/program.js` remains the
|
|
1951
1958
|
`Program` facade and source/module loader. Static recursion, Datalog, WFS, and
|
|
1952
1959
|
negation-stratification analysis is isolated in `src/program-analysis.js`,
|
|
@@ -5167,6 +5174,18 @@ for example malformed input or an unavailable required resource. ISO
|
|
|
5167
5174
|
instantiation, type, domain, permission, representation, and evaluation errors
|
|
5168
5175
|
follow this same exception path.
|
|
5169
5176
|
|
|
5177
|
+
Normal EyeProlog also provides `call_cleanup(Goal, Cleanup)` and
|
|
5178
|
+
`setup_call_cleanup(Setup, Goal, Cleanup)`. Cleanup is run exactly once when the
|
|
5179
|
+
protected search completes deterministically, is exhausted, is cut or otherwise
|
|
5180
|
+
pruned, top-level answer enumeration is abandoned, or an exception unwinds the
|
|
5181
|
+
search. `setup_call_cleanup/3` runs Setup once and installs Cleanup only after
|
|
5182
|
+
Setup succeeds. On cut or ordinary pruning Cleanup sees the current Goal
|
|
5183
|
+
bindings; on exception unwind the Goal bindings have been removed and Cleanup
|
|
5184
|
+
sees the Setup environment. Cleanup failure is ignored, and an exception already
|
|
5185
|
+
being propagated takes precedence over a cleanup exception. Nested cleanups run
|
|
5186
|
+
inside-out. These two controls are EyeProlog extensions and are absent from
|
|
5187
|
+
`--iso-strict`.
|
|
5188
|
+
|
|
5170
5189
|
Collection also makes search boundaries explicit. `findall/3` returns one list
|
|
5171
5190
|
and existentially closes variables that occur only in its goal. `bagof/3`
|
|
5172
5191
|
instead creates a group for each binding of a free variable and fails when
|
|
@@ -5372,13 +5391,21 @@ application code.
|
|
|
5372
5391
|
|
|
5373
5392
|
```eyeprolog
|
|
5374
5393
|
write_event(Path, Event) :-
|
|
5375
|
-
|
|
5376
|
-
|
|
5377
|
-
|
|
5378
|
-
|
|
5379
|
-
|
|
5394
|
+
setup_call_cleanup(
|
|
5395
|
+
open(Path, write, Stream, [type(text)]),
|
|
5396
|
+
( write_canonical(Stream, Event),
|
|
5397
|
+
put_char(Stream, '.'),
|
|
5398
|
+
nl(Stream)
|
|
5399
|
+
),
|
|
5400
|
+
close(Stream)
|
|
5401
|
+
).
|
|
5380
5402
|
```
|
|
5381
5403
|
|
|
5404
|
+
In normal mode, `setup_call_cleanup/3` is the preferred lifecycle boundary for
|
|
5405
|
+
resources such as streams: `close(Stream)` still runs if the protected work
|
|
5406
|
+
fails, throws, is cut, or its remaining alternatives are abandoned. Strict ISO
|
|
5407
|
+
mode does not provide this EyeProlog extension.
|
|
5408
|
+
|
|
5382
5409
|
The period is essential when another Prolog processor will read the result as
|
|
5383
5410
|
a term. `write/1-2` uses readable conventional syntax, `writeq/1-2` quotes
|
|
5384
5411
|
where needed, and `write_canonical/1-2` exposes canonical structure. Dotted
|
|
@@ -5586,8 +5613,10 @@ negation.
|
|
|
5586
5613
|
|
|
5587
5614
|
EyeProlog supports cut, operator declarations, dynamic database updates, grouped
|
|
5588
5615
|
solutions, exceptions, flags, initialization and inclusion directives, and
|
|
5589
|
-
standard stream and term I/O.
|
|
5590
|
-
|
|
5616
|
+
standard stream and term I/O. Normal mode additionally provides lifecycle-aware
|
|
5617
|
+
`call_cleanup/2` and `setup_call_cleanup/3`; these cleanup controls are
|
|
5618
|
+
EyeProlog extensions and are excluded by `--iso-strict`. ISO Part 2 modules and
|
|
5619
|
+
Part 3 definite clause grammars complement this Part 1 core.
|
|
5591
5620
|
|
|
5592
5621
|
### ISO Part 2 modules
|
|
5593
5622
|
|
|
@@ -5838,8 +5867,7 @@ collision because other Prolog systems commonly reject it while loading.
|
|
|
5838
5867
|
`;/2` recognizes an `->/2` term on its left and implements the ISO
|
|
5839
5868
|
if-then-else commitment described above. Cuts and committed conditions are
|
|
5840
5869
|
operational controls; use ordinary relations when all alternatives should
|
|
5841
|
-
remain observable.
|
|
5842
|
-
|
|
5870
|
+
remain observable.
|
|
5843
5871
|
### Definite clause grammar processing
|
|
5844
5872
|
|
|
5845
5873
|
| Predicate and principal call | Behavior |
|
|
@@ -6142,6 +6170,10 @@ The JavaScript `ioOptions.input` and `ioOptions.write` hooks connect standard
|
|
|
6142
6170
|
streams to an embedder. File-backed streams use synchronous lifecycle semantics
|
|
6143
6171
|
so side effects occur in Prolog execution order.
|
|
6144
6172
|
|
|
6173
|
+
### Normal-mode cleanup controls
|
|
6174
|
+
|
|
6175
|
+
`call_cleanup/2` and `setup_call_cleanup/3` are normal EyeProlog runtime extensions rather than members of the isolated ISO builtin registry. They protect a goal across deterministic completion, exhaustion, cut, top-level abandonment, and exception unwinding, running Cleanup exactly once. `setup_call_cleanup/3` runs Setup once and installs Cleanup only after Setup succeeds. Nested cleanups run inside-out, and strict ISO mode does not provide either predicate.
|
|
6176
|
+
|
|
6145
6177
|
### The EyeProlog library
|
|
6146
6178
|
|
|
6147
6179
|
EyeProlog exposes **128 library predicate indicators** in addition to the 129
|
|
@@ -6665,8 +6697,10 @@ the answer-control help. Enumeration is demand-driven: after an answer is
|
|
|
6665
6697
|
found, the top level does not pull a successor merely to discover whether the
|
|
6666
6698
|
current answer is the last one. Search for a later answer, including any side
|
|
6667
6699
|
effects reached on that path, starts only after an answer-control command asks
|
|
6668
|
-
to continue.
|
|
6669
|
-
|
|
6700
|
+
to continue. Stopping enumeration closes any active `call_cleanup/2` or
|
|
6701
|
+
`setup_call_cleanup/3` protection exactly once; detecting that a choicepoint
|
|
6702
|
+
remains does not execute that next branch. If an unresolved alternative
|
|
6703
|
+
ultimately has no solution, asking for it may therefore finish with `false.`. In scripted non-TTY input, a new
|
|
6670
6704
|
query line implicitly stops the preceding answer enumeration without consuming
|
|
6671
6705
|
the new query; explicit `;`, `n`, Space, `a`, or `f` still requests more
|
|
6672
6706
|
answers. Once the top-level reader has accepted a complete query, the following
|
|
@@ -7510,7 +7544,8 @@ Corrigenda 1–3. Corrigendum 2 additions—including `subsumes_term/2`,
|
|
|
7510
7544
|
`acyclic_term/1`, `sort/2`, `keysort/2`, `term_variables/2`, `retractall/1`,
|
|
7511
7545
|
and `call/2-8`—remain part of that strict baseline. Part 2 modules, Part 3 DCG
|
|
7512
7546
|
expansion/`phrase/2-3`, quads, EyeProlog libraries, the `occurs_check` flag,
|
|
7513
|
-
|
|
7547
|
+
automatic tabling, `call_cleanup/2`, and `setup_call_cleanup/3` are outside
|
|
7548
|
+
that Part 1 strict surface.
|
|
7514
7549
|
|
|
7515
7550
|
This release establishes the strict-mode mechanism required for the ongoing
|
|
7516
7551
|
conformance audit; it does **not** yet claim that every processor requirement
|
|
@@ -7739,6 +7774,14 @@ decidable by structural equality.
|
|
|
7739
7774
|
|
|
7740
7775
|
**Clause.** A fact or rule terminated by a period.
|
|
7741
7776
|
|
|
7777
|
+
**Choicepoint.** A remaining search alternative that may produce another
|
|
7778
|
+
answer if the caller asks the solver to continue. EyeProlog's top level reports
|
|
7779
|
+
choicepoint availability without speculatively executing the next alternative.
|
|
7780
|
+
|
|
7781
|
+
**Cleanup.** A protected finalization goal installed by normal-mode
|
|
7782
|
+
`call_cleanup/2` or `setup_call_cleanup/3`. It runs exactly once when the
|
|
7783
|
+
protected search ends, is pruned or abandoned, or unwinds through an exception.
|
|
7784
|
+
|
|
7742
7785
|
**Closed-world assumption.** The decision to treat failure to derive a
|
|
7743
7786
|
sufficiently scoped claim as evidence for its absence. EyeProlog's `\+/1` performs
|
|
7744
7787
|
negation as failure; the modeler is responsible for justifying the scope.
|
package/why-eyeprolog.md
CHANGED
|
@@ -36,6 +36,7 @@ keeps a narrow architecture:
|
|
|
36
36
|
- the ISO built-in registry;
|
|
37
37
|
- lean portable ISO Part 2 library modules;
|
|
38
38
|
- ISO Part 3 definite clause grammars and `phrase/2-3`;
|
|
39
|
+
- lifecycle-aware `call_cleanup/2` and `setup_call_cleanup/3` in normal mode;
|
|
39
40
|
- optional proof explanations; and
|
|
40
41
|
- the same implementation in Node.js and the browser.
|
|
41
42
|
|
|
@@ -48,11 +49,13 @@ their ISO definitions directly.
|
|
|
48
49
|
The implementation follows the same compactness rule. JavaScript runtime
|
|
49
50
|
modules stay flat under `src/`: `program.js` and `iso.js` remain stable facade
|
|
50
51
|
modules, while static program analysis, clause indexing, arithmetic evaluation,
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
processor error types, and cleanup lifecycle handling are factored into focused
|
|
53
|
+
sibling files. `cleanup.js` closes protected builtin iterators when search is
|
|
54
|
+
committed, abandoned, or unwound without making `solver.js` depend on the
|
|
55
|
+
language registry. The execution fast paths remain direct code in `solver.js`;
|
|
56
|
+
source cleanup is not allowed to add dispatch or abstraction overhead merely to
|
|
57
|
+
make that file smaller. `src/ARCHITECTURE.md` records these boundaries and an
|
|
58
|
+
automated test rejects JavaScript import cycles.
|
|
56
59
|
|
|
57
60
|
|
|
58
61
|
## Why keep well-founded negation explicit?
|
|
@@ -77,6 +80,16 @@ checked `examples/dcg-expression-language.pl` program shows the declarative side
|
|
|
77
80
|
of that design: one grammar builds precedence-aware syntax trees and another
|
|
78
81
|
generates minimally parenthesized token sequences back from them.
|
|
79
82
|
|
|
83
|
+
## Why cleanup follows search lifecycle?
|
|
84
|
+
|
|
85
|
+
A Prolog resource lifetime is tied to search, not just to ordinary function
|
|
86
|
+
return. A protected goal can finish, fail, be cut, be abandoned at the top
|
|
87
|
+
level while alternatives remain, or unwind through an exception. Normal-mode
|
|
88
|
+
`call_cleanup/2` and `setup_call_cleanup/3` make those exits explicit and run
|
|
89
|
+
Cleanup exactly once. This also preserves demand-driven answer interaction: the
|
|
90
|
+
top level need not execute a successor merely to decide whether a choicepoint
|
|
91
|
+
exists. Strict ISO mode leaves these predicates out.
|
|
92
|
+
|
|
80
93
|
## Why proofs?
|
|
81
94
|
|
|
82
95
|
An answer says that a goal succeeded. A proof records one successful route
|