eyeprolog 1.3.65 → 1.3.66
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 +18 -1
- package/examples/dif-constraints.pl +19 -0
- package/examples/output/dif-constraints.pl +2 -0
- package/package.json +1 -1
- package/playground.html +1 -0
- package/src/ARCHITECTURE.md +7 -0
- package/src/iso.js +45 -6
- package/src/repl.js +13 -0
- package/src/solver.js +4 -1
- package/src/standard-library.js +1 -1
- package/src/term.js +56 -0
- package/test/run-regression.mjs +42 -5
- package/the-art-of-eyeprolog.md +4 -3
package/README.md
CHANGED
|
@@ -130,6 +130,23 @@ local/global variable stack. `phrase/2` likewise supplies its fixed `[]`
|
|
|
130
130
|
remainder directly to the grammar; `phrase/3` retains its delayed final output
|
|
131
131
|
unification for steadfastness.
|
|
132
132
|
|
|
133
|
+
### Delayed disequality with `dif/2`
|
|
134
|
+
|
|
135
|
+
The normal EyeProlog runtime provides `dif/2` as a conforming extension;
|
|
136
|
+
`--iso-strict` remains limited to ISO/IEC 13211-1 and does not expose it.
|
|
137
|
+
Unlike `\=/2`, which only tests whether two terms are non-unifiable *now*,
|
|
138
|
+
`dif/2` can succeed with a residual disequality constraint and reject a later
|
|
139
|
+
binding that would make the terms identical. For example,
|
|
140
|
+
`dif(X, Y), X = Y` fails, while `dif(X-Y, 1-2), X = Y, Y = 1` succeeds once
|
|
141
|
+
the structural constraint becomes provably satisfied.
|
|
142
|
+
|
|
143
|
+
The implementation uses backtracking-safe annotated variables in `Env`. A
|
|
144
|
+
pending constraint is indexed by every currently unbound variable it mentions;
|
|
145
|
+
variable aliasing and later bindings reindex the annotation, entailed constraints
|
|
146
|
+
are removed, and violated constraints make unification fail. The interactive
|
|
147
|
+
top level includes pending `dif/2` goals in residual answers. See
|
|
148
|
+
[`examples/dif-constraints.pl`](examples/dif-constraints.pl).
|
|
149
|
+
|
|
133
150
|
Recursion through negation is explicit. EyeProlog provides `tnot/1` for
|
|
134
151
|
well-founded negation over finite, range-restricted, function-free Datalog
|
|
135
152
|
components. Ordinary `\+/1` remains negation-as-failure and is not silently
|
|
@@ -258,7 +275,7 @@ be continued without rebuilding a full clause-resolution frame for every
|
|
|
258
275
|
suffix. Open and remainder-producing uses still retain the ordinary DCG
|
|
259
276
|
solutions and backtracking behavior.
|
|
260
277
|
|
|
261
|
-
EyeProlog also adds
|
|
278
|
+
EyeProlog also adds 129 public library predicate indicators to its 129-entry ISO
|
|
262
279
|
profile. **88 are implemented entirely as ordinary Prolog clauses** in focused
|
|
263
280
|
modules under `src/lib/`; the remaining control predicates and finite-domain
|
|
264
281
|
`library(clpz)` kernel use backtrackable host support.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
% Delayed disequality constraints with dif/2.
|
|
2
|
+
%
|
|
3
|
+
% Unlike \=/2, dif/2 records a constraint when its arguments are still
|
|
4
|
+
% unifiable and rechecks that constraint after later bindings.
|
|
5
|
+
|
|
6
|
+
%% goal: allowed_pair(X, Y)
|
|
7
|
+
%% goal: specialization(X, Y)
|
|
8
|
+
|
|
9
|
+
allowed_pair(X, Y) :-
|
|
10
|
+
dif(X, Y),
|
|
11
|
+
X = left,
|
|
12
|
+
Y = right.
|
|
13
|
+
|
|
14
|
+
% The structural constraint remains pending after X=Y. Binding both to 1
|
|
15
|
+
% then proves X-Y and 1-2 different, so the constraint is discharged.
|
|
16
|
+
specialization(X, Y) :-
|
|
17
|
+
dif(X-Y, 1-2),
|
|
18
|
+
X = Y,
|
|
19
|
+
Y = 1.
|
package/package.json
CHANGED
package/playground.html
CHANGED
package/src/ARCHITECTURE.md
CHANGED
|
@@ -31,6 +31,13 @@ kernel component into the ISO registry or a frontend. In particular,
|
|
|
31
31
|
report processor errors without importing `iso.js` and creating an
|
|
32
32
|
`iso.js <-> dcg.js` cycle.
|
|
33
33
|
|
|
34
|
+
`term.js` also owns the generic annotated-variable storage carried by `Env`.
|
|
35
|
+
Annotations are persistent across `Env.clone()` and therefore backtrack with the
|
|
36
|
+
substitution. Language services may attach immutable constraint descriptors,
|
|
37
|
+
but the unconstrained unification hot path only performs a null check; descriptor
|
|
38
|
+
validation and reindexing run only in environments that actually contain
|
|
39
|
+
annotations. `dif/2` is the first user of this mechanism.
|
|
40
|
+
|
|
34
41
|
The JavaScript runtime stays flat directly under `src/`; the existing `src/lib/`
|
|
35
42
|
contains Prolog library sources rather than JavaScript runtime modules. The architecture
|
|
36
43
|
test rejects JavaScript import cycles under `src/`. Cleanup lifecycle hooks are
|
package/src/iso.js
CHANGED
|
@@ -167,6 +167,7 @@ export const eyePrologLibraryBuiltins = {
|
|
|
167
167
|
registry.add('eyeprolog__call_nth', 2, callNthBuiltin, { eyePrologLibrary: true });
|
|
168
168
|
registry.add('eyeprolog__countall', 2, countAllBuiltin, { eyePrologLibrary: true });
|
|
169
169
|
registry.add('eyeprolog__freeze', 2, freezeBuiltin, { eyePrologLibrary: true });
|
|
170
|
+
registry.add('dif', 2, difBuiltin, { deterministic: true, eyePrologLibrary: true });
|
|
170
171
|
registry.add('time', 1, timeBuiltin, { eyePrologLibrary: true });
|
|
171
172
|
registry.add('eyeprolog__time', 1, timeBuiltin, { eyePrologLibrary: true });
|
|
172
173
|
},
|
|
@@ -189,6 +190,43 @@ function* nonUnification({ goal, env }) {
|
|
|
189
190
|
if (!unify(goal.args[0], goal.args[1], env.clone())) yield env;
|
|
190
191
|
}
|
|
191
192
|
|
|
193
|
+
function difConstraint(left, right) {
|
|
194
|
+
return Object.freeze({
|
|
195
|
+
kind: 'dif',
|
|
196
|
+
left,
|
|
197
|
+
right,
|
|
198
|
+
variables(env) {
|
|
199
|
+
return new Set([...termVariableNames(left, env), ...termVariableNames(right, env)]);
|
|
200
|
+
},
|
|
201
|
+
status(env) {
|
|
202
|
+
if (identical(left, right, env)) return 'violated';
|
|
203
|
+
const probe = env.clone();
|
|
204
|
+
return unify(left, right, probe, { skipVariableConstraints: true }) ? 'pending' : 'entailed';
|
|
205
|
+
},
|
|
206
|
+
residualGoal() {
|
|
207
|
+
return compound('dif', [left, right]);
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function* difBuiltin({ goal, env }) {
|
|
213
|
+
const left = goal.args[0];
|
|
214
|
+
const right = goal.args[1];
|
|
215
|
+
if (identical(left, right, env)) return;
|
|
216
|
+
|
|
217
|
+
// In EyeProlog's finite-tree model, terms that cannot unify are already
|
|
218
|
+
// provably different and need no residual constraint. Otherwise attach a
|
|
219
|
+
// disequality constraint to every currently unbound variable it mentions.
|
|
220
|
+
const probe = env.clone();
|
|
221
|
+
if (!unify(left, right, probe, { skipVariableConstraints: true })) {
|
|
222
|
+
yield env;
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const next = env.clone();
|
|
226
|
+
next.addVariableConstraint(difConstraint(left, right));
|
|
227
|
+
yield next;
|
|
228
|
+
}
|
|
229
|
+
|
|
192
230
|
function termVariableNames(term, env, names = new Set(), seen = new Set()) {
|
|
193
231
|
term = deref(term, env);
|
|
194
232
|
if (term.type === VAR) {
|
|
@@ -1646,7 +1684,7 @@ function writeOptionBoolean(value, env, option) {
|
|
|
1646
1684
|
value = deref(value, env);
|
|
1647
1685
|
if (value.type === VAR) throw new PrologError('instantiation_error');
|
|
1648
1686
|
if (value.type !== ATOM || !['true', 'false'].includes(value.name)) {
|
|
1649
|
-
throw new PrologError('domain_error(write_option)', option);
|
|
1687
|
+
throw new PrologError('domain_error(write_option)', copyResolved(option, env));
|
|
1650
1688
|
}
|
|
1651
1689
|
return value.name === 'true';
|
|
1652
1690
|
}
|
|
@@ -1657,19 +1695,20 @@ function writeVariableNames(value, env, option) {
|
|
|
1657
1695
|
const items = properListItems(value, env);
|
|
1658
1696
|
if (items == null) {
|
|
1659
1697
|
if (isPartialList(value, env)) throw new PrologError('instantiation_error');
|
|
1660
|
-
throw new PrologError('domain_error(write_option)', option);
|
|
1698
|
+
throw new PrologError('domain_error(write_option)', copyResolved(option, env));
|
|
1661
1699
|
}
|
|
1662
1700
|
const names = new Map();
|
|
1663
1701
|
for (const item of items) {
|
|
1664
1702
|
const pair = deref(item, env);
|
|
1703
|
+
if (pair.type === VAR) throw new PrologError('instantiation_error');
|
|
1665
1704
|
if (pair.type !== COMPOUND || pair.name !== '=' || pair.arity !== 2) {
|
|
1666
|
-
throw new PrologError('domain_error(write_option)', option);
|
|
1705
|
+
throw new PrologError('domain_error(write_option)', copyResolved(option, env));
|
|
1667
1706
|
}
|
|
1668
1707
|
const name = deref(pair.args[0], env);
|
|
1669
1708
|
const target = deref(pair.args[1], env);
|
|
1670
1709
|
if (name.type === VAR) throw new PrologError('instantiation_error');
|
|
1671
1710
|
if (name.type !== ATOM) {
|
|
1672
|
-
throw new PrologError('domain_error(write_option)', option);
|
|
1711
|
+
throw new PrologError('domain_error(write_option)', copyResolved(option, env));
|
|
1673
1712
|
}
|
|
1674
1713
|
// Corrigendum 3 permits any term on the right. Only variables can name a
|
|
1675
1714
|
// variable being written; retain the leftmost applicable entry.
|
|
@@ -1682,7 +1721,7 @@ function termWriteOptionsFromItems(options, env, mode = 'write_term', solver = n
|
|
|
1682
1721
|
const result = defaultTermWriteOptions(mode);
|
|
1683
1722
|
for (const option of options) {
|
|
1684
1723
|
if (option.type !== COMPOUND || option.arity !== 1) {
|
|
1685
|
-
throw new PrologError('domain_error(write_option)', option);
|
|
1724
|
+
throw new PrologError('domain_error(write_option)', copyResolved(option, env));
|
|
1686
1725
|
}
|
|
1687
1726
|
if (option.name === 'quoted') result.quoted = writeOptionBoolean(option.args[0], env, option);
|
|
1688
1727
|
else if (option.name === 'ignore_ops') result.ignoreOps = writeOptionBoolean(option.args[0], env, option);
|
|
@@ -1691,7 +1730,7 @@ function termWriteOptionsFromItems(options, env, mode = 'write_term', solver = n
|
|
|
1691
1730
|
else if (option.name === 'double_quotes' && !solver?.isoStrict) {
|
|
1692
1731
|
result.doubleQuotes = writeOptionBoolean(option.args[0], env, option);
|
|
1693
1732
|
} else {
|
|
1694
|
-
throw new PrologError('domain_error(write_option)', option);
|
|
1733
|
+
throw new PrologError('domain_error(write_option)', copyResolved(option, env));
|
|
1695
1734
|
}
|
|
1696
1735
|
}
|
|
1697
1736
|
return result;
|
package/src/repl.js
CHANGED
|
@@ -740,6 +740,19 @@ function formatAnswer(engine, state, variables, env) {
|
|
|
740
740
|
doubleQuotes: state.solver.prologFlags.get('double_quotes')?.value?.name ?? 'chars',
|
|
741
741
|
})}`);
|
|
742
742
|
}
|
|
743
|
+
for (const constraint of env.variableConstraints?.() ?? []) {
|
|
744
|
+
const residual = constraint.residualGoal?.(env);
|
|
745
|
+
if (residual == null) continue;
|
|
746
|
+
collectUnboundVariables(engine, residual, env, names, () => `_${letterName(generated++)}`);
|
|
747
|
+
bindings.push(engine.formatTermForWrite(residual, env, {
|
|
748
|
+
quoted: true,
|
|
749
|
+
operators,
|
|
750
|
+
variableNames: names,
|
|
751
|
+
operatorAtomsAsArgs: true,
|
|
752
|
+
dottedGraphicAtoms: true,
|
|
753
|
+
doubleQuotes: state.solver.prologFlags.get('double_quotes')?.value?.name ?? 'chars',
|
|
754
|
+
}));
|
|
755
|
+
}
|
|
743
756
|
return bindings.length === 0 ? 'true' : bindings.join(', ');
|
|
744
757
|
}
|
|
745
758
|
|
package/src/solver.js
CHANGED
|
@@ -1971,7 +1971,7 @@ function fastCountBranch(branch, env) {
|
|
|
1971
1971
|
|
|
1972
1972
|
function fastCountPureGoal(solver, goal, env) {
|
|
1973
1973
|
if (solver.isoStrict || solver.solutionLimit !== Infinity || solver.maxInferences !== Infinity ||
|
|
1974
|
-
env._clpz != null || (env._delays != null && env._delays.size !== 0)) return null;
|
|
1974
|
+
env._clpz != null || env._variableConstraints != null || (env._delays != null && env._delays.size !== 0)) return null;
|
|
1975
1975
|
const budget = { remaining: 8192 };
|
|
1976
1976
|
const branches = expandPureCountGoal(solver, goal, goal.module ?? 'user', new Set(), budget);
|
|
1977
1977
|
if (branches == null) return null;
|
|
@@ -2015,6 +2015,7 @@ function tryPushScalarFactRunFrames(stack, solver, goals, env, depth, active) {
|
|
|
2015
2015
|
if (state.index === runLength) {
|
|
2016
2016
|
const next = env.clone();
|
|
2017
2017
|
for (let i = 0; i < state.names.length; i++) next.bind(state.names[i], state.values[i]);
|
|
2018
|
+
if (next._variableConstraints != null && !next.validateVariableConstraints()) continue;
|
|
2018
2019
|
frames.push({ kind: 'goals', goals: rest, env: next, depth: state.depth, active });
|
|
2019
2020
|
if (frames.length > frameLimit) {
|
|
2020
2021
|
// Do not repeat the old bug of abandoning the optimization and then
|
|
@@ -2079,6 +2080,7 @@ function* scalarFactRunSolutions(solver, goals, groups, env, depth, active) {
|
|
|
2079
2080
|
if (state.index === goals.length) {
|
|
2080
2081
|
const next = env.clone();
|
|
2081
2082
|
for (let i = 0; i < state.names.length; i++) next.bind(state.names[i], state.values[i]);
|
|
2083
|
+
if (next._variableConstraints != null && !next.validateVariableConstraints()) continue;
|
|
2082
2084
|
yield next;
|
|
2083
2085
|
continue;
|
|
2084
2086
|
}
|
|
@@ -2183,6 +2185,7 @@ function matchScalarFact(goal, head, env) {
|
|
|
2183
2185
|
|
|
2184
2186
|
const next = env.clone();
|
|
2185
2187
|
for (let i = 0; i < names.length; i++) next.bind(names[i], values[i]);
|
|
2188
|
+
if (next._variableConstraints != null && !next.validateVariableConstraints()) return null;
|
|
2186
2189
|
return next;
|
|
2187
2190
|
}
|
|
2188
2191
|
|
package/src/standard-library.js
CHANGED
|
@@ -48,7 +48,7 @@ function libraryUrl(filename) {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
export const eyePrologNativeLibraryIndicators = Object.freeze([
|
|
51
|
-
'call_nth/2', 'freeze/2', 'countall/2', 'time/1',
|
|
51
|
+
'call_nth/2', 'freeze/2', 'dif/2', 'countall/2', 'time/1',
|
|
52
52
|
'#>/2', '#</2', '#>=/2', '#=</2', '#=/2', '#\\=/2', '#\\/1',
|
|
53
53
|
'#<==>/2', '#==>/2', '#<==/2', '#\\//2', '#\\/2', '#/\\/2',
|
|
54
54
|
'in/2', 'ins/2', 'all_different/1', 'all_distinct/1', 'nvalue/2', 'sum/3',
|
package/src/term.js
CHANGED
|
@@ -97,6 +97,11 @@ export class Env {
|
|
|
97
97
|
cache: null,
|
|
98
98
|
};
|
|
99
99
|
this._delays = null;
|
|
100
|
+
// Backtracking-safe attributed-variable constraints. Constraints are
|
|
101
|
+
// immutable descriptors shared by cloned environments; the Set/Map index
|
|
102
|
+
// is copied only when a branch adds, removes, or reindexes a constraint.
|
|
103
|
+
this._variableConstraints = null;
|
|
104
|
+
this._variableAnnotations = null;
|
|
100
105
|
this._clpz = null;
|
|
101
106
|
this._occursCheckHandler = null;
|
|
102
107
|
this._localVariables = null;
|
|
@@ -110,6 +115,8 @@ export class Env {
|
|
|
110
115
|
const clone = Object.create(Env.prototype);
|
|
111
116
|
clone._state = this._state;
|
|
112
117
|
clone._delays = this._delays;
|
|
118
|
+
clone._variableConstraints = this._variableConstraints;
|
|
119
|
+
clone._variableAnnotations = this._variableAnnotations;
|
|
113
120
|
clone._clpz = this._clpz;
|
|
114
121
|
clone._occursCheckHandler = this._occursCheckHandler;
|
|
115
122
|
clone._localVariables = this._localVariables;
|
|
@@ -236,6 +243,54 @@ export class Env {
|
|
|
236
243
|
if (ready.length > 0) this._delays = remaining;
|
|
237
244
|
return ready;
|
|
238
245
|
}
|
|
246
|
+
addVariableConstraint(constraint) {
|
|
247
|
+
if (constraint == null || typeof constraint.variables !== 'function' ||
|
|
248
|
+
typeof constraint.status !== 'function') {
|
|
249
|
+
throw new TypeError('variable constraint requires variables(env) and status(env)');
|
|
250
|
+
}
|
|
251
|
+
const constraints = new Set(this._variableConstraints ?? []);
|
|
252
|
+
constraints.add(constraint);
|
|
253
|
+
this._variableConstraints = constraints;
|
|
254
|
+
this._reindexVariableConstraints();
|
|
255
|
+
}
|
|
256
|
+
variableConstraints(kind = null) {
|
|
257
|
+
const constraints = [...(this._variableConstraints ?? [])];
|
|
258
|
+
return kind == null ? constraints : constraints.filter((constraint) => constraint.kind === kind);
|
|
259
|
+
}
|
|
260
|
+
variableAnnotations(name) {
|
|
261
|
+
const root = deref(variable(name), this);
|
|
262
|
+
if (root.type !== VAR) return [];
|
|
263
|
+
return [...(this._variableAnnotations?.get(root.name) ?? [])];
|
|
264
|
+
}
|
|
265
|
+
validateVariableConstraints() {
|
|
266
|
+
if (this._variableConstraints == null || this._variableConstraints.size === 0) return true;
|
|
267
|
+
const pending = new Set();
|
|
268
|
+
for (const constraint of this._variableConstraints) {
|
|
269
|
+
const status = constraint.status(this);
|
|
270
|
+
if (status === 'violated') return false;
|
|
271
|
+
if (status !== 'entailed') pending.add(constraint);
|
|
272
|
+
}
|
|
273
|
+
this._variableConstraints = pending.size === 0 ? null : pending;
|
|
274
|
+
this._reindexVariableConstraints();
|
|
275
|
+
return true;
|
|
276
|
+
}
|
|
277
|
+
_reindexVariableConstraints() {
|
|
278
|
+
if (this._variableConstraints == null || this._variableConstraints.size === 0) {
|
|
279
|
+
this._variableAnnotations = null;
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
const annotations = new Map();
|
|
283
|
+
for (const constraint of this._variableConstraints) {
|
|
284
|
+
for (const name of constraint.variables(this)) {
|
|
285
|
+
const root = deref(variable(name), this);
|
|
286
|
+
if (root.type !== VAR) continue;
|
|
287
|
+
const set = annotations.get(root.name) ?? new Set();
|
|
288
|
+
set.add(constraint);
|
|
289
|
+
annotations.set(root.name, set);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
this._variableAnnotations = annotations.size === 0 ? null : annotations;
|
|
293
|
+
}
|
|
239
294
|
}
|
|
240
295
|
|
|
241
296
|
export function deref(term, env) {
|
|
@@ -373,6 +428,7 @@ export function unify(left, right, env, options = {}) {
|
|
|
373
428
|
|
|
374
429
|
return false;
|
|
375
430
|
}
|
|
431
|
+
if (options.skipVariableConstraints !== true && env?._variableConstraints != null && !env.validateVariableConstraints()) return false;
|
|
376
432
|
return true;
|
|
377
433
|
}
|
|
378
434
|
|
package/test/run-regression.mjs
CHANGED
|
@@ -110,6 +110,41 @@ export function runRegression(reporter = new TestReporter(), requestedSection =
|
|
|
110
110
|
|
|
111
111
|
function regressionCases() {
|
|
112
112
|
return [
|
|
113
|
+
{
|
|
114
|
+
name: 'dif/2 keeps a delayed disequality across later unification (issue #68)',
|
|
115
|
+
run: () => {
|
|
116
|
+
assertEqual(run('', { goal: 'dif(1,Y),Y=1' }).stats.completed_goal_lists, 0,
|
|
117
|
+
'binding a constrained variable to the forbidden value fails');
|
|
118
|
+
assertEqual(run('', { goal: 'dif(1,Y),Y=2' }).stats.completed_goal_lists, 1,
|
|
119
|
+
'binding a constrained variable to a different value succeeds');
|
|
120
|
+
assertEqual(run('', { goal: 'dif(X,Y),X=Y' }).stats.completed_goal_lists, 0,
|
|
121
|
+
'aliasing two constrained variables fails');
|
|
122
|
+
assertEqual(run('', { goal: 'dif(X-Y,1-2),X=Y,Y=1' }).stats.completed_goal_lists, 1,
|
|
123
|
+
'a specialization that makes the terms non-unifiable discharges the constraint');
|
|
124
|
+
assertEqual(run('', { goal: 'dif(X-Y,1-2),X=Y,X=2' }).stats.completed_goal_lists, 1,
|
|
125
|
+
'a second specialization discharges the same structural constraint');
|
|
126
|
+
assertEqual(run('p(a).', { goal: 'dif(X,a),p(X)' }).stats.completed_goal_lists, 0,
|
|
127
|
+
'scalar-fact fast matching still validates annotated variables');
|
|
128
|
+
assertEqual(run('p(b).', { goal: 'dif(X,a),p(X)' }).stats.completed_goal_lists, 1,
|
|
129
|
+
'scalar-fact fast matching can discharge an annotated disequality');
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
name: 'write_term variable_names/1 errors preserve the instantiated option culprit (issue #69)',
|
|
134
|
+
run: () => {
|
|
135
|
+
const scalar = run('', {
|
|
136
|
+
isoStrict: true,
|
|
137
|
+
goal: "catch((VN=1,write_term(T,[variable_names(VN)])),error(E,_),writeq(E))",
|
|
138
|
+
});
|
|
139
|
+
assertEqual(scalar.stdout, 'domain_error(write_option,variable_names(1))', 'scalar culprit');
|
|
140
|
+
|
|
141
|
+
const list = run('', {
|
|
142
|
+
isoStrict: true,
|
|
143
|
+
goal: "catch((VN=[[]],write_term(T,[variable_names(VN)])),error(E,_),writeq(E))",
|
|
144
|
+
});
|
|
145
|
+
assertEqual(list.stdout, 'domain_error(write_option,variable_names([[]]))', 'list culprit');
|
|
146
|
+
},
|
|
147
|
+
},
|
|
113
148
|
{
|
|
114
149
|
name: 'large source scanning avoids quadratic full-stop lookback',
|
|
115
150
|
run: () => {
|
|
@@ -4694,7 +4729,7 @@ answer(ok) :-
|
|
|
4694
4729
|
assertEqual(Boolean(registry.get('is', 2)), true, 'ISO is/2 exists');
|
|
4695
4730
|
assertEqual(Boolean(registry.get('append', 3)), false, 'append/3 is not ISO core');
|
|
4696
4731
|
assertEqual(library.eyePrologLibrary, true, 'complete registry marker');
|
|
4697
|
-
assertEqual(library.defs.size,
|
|
4732
|
+
assertEqual(library.defs.size, 161, 'EyeProlog registry contains ISO definitions, cleanup controls, observability extensions, WFS tnot/1, and private library adapters');
|
|
4698
4733
|
assertEqual(Boolean(registry.get('phrase', 2)), true, 'Part 3 phrase/2 exists');
|
|
4699
4734
|
assertEqual(Boolean(registry.get('phrase', 3)), true, 'Part 3 phrase/3 exists');
|
|
4700
4735
|
assertEqual(registry.get('statistics', 0), null, 'statistics/0 is absent from the ISO registry');
|
|
@@ -4705,11 +4740,13 @@ answer(ok) :-
|
|
|
4705
4740
|
assertEqual(Boolean(library.get('tnot', 1)), true, 'tnot/1 is an EyeProlog WFS extension');
|
|
4706
4741
|
assertEqual(registry.get('time', 1), null, 'time/1 is absent from the ISO registry');
|
|
4707
4742
|
assertEqual(Boolean(library.get('time', 1)), true, 'time/1 is an EyeProlog timing extension');
|
|
4743
|
+
assertEqual(registry.get('dif', 2), null, 'dif/2 is absent from the strict ISO core registry');
|
|
4744
|
+
assertEqual(Boolean(library.get('dif', 2)), true, 'dif/2 is an EyeProlog attributed-variable constraint extension');
|
|
4708
4745
|
assertEqual(registry.get('call_cleanup', 2), null, 'call_cleanup/2 is absent from the ISO registry');
|
|
4709
4746
|
assertEqual(Boolean(library.get('call_cleanup', 2)), true, 'call_cleanup/2 is an EyeProlog cleanup control');
|
|
4710
4747
|
assertEqual(registry.get('setup_call_cleanup', 3), null, 'setup_call_cleanup/3 is absent from the ISO registry');
|
|
4711
4748
|
assertEqual(Boolean(library.get('setup_call_cleanup', 3)), true, 'setup_call_cleanup/3 is an EyeProlog cleanup control');
|
|
4712
|
-
assertEqual(registeredNativeEyePrologLibraryNames().length,
|
|
4749
|
+
assertEqual(registeredNativeEyePrologLibraryNames().length, 42, 'public native EyeProlog builtin count');
|
|
4713
4750
|
assertEqual(eyePrologPortableLibraryIndicators.length, 87, 'portable Prolog library count');
|
|
4714
4751
|
assertEqual(eyePrologInteropLibraryIndicators.length, 29, 'cross-implementation interop profile count');
|
|
4715
4752
|
assertEqual(eyePrologInteropLibraryModules.join(','), 'lists,iso_ext,lambda', 'common explicit library module profile');
|
|
@@ -4719,9 +4756,9 @@ answer(ok) :-
|
|
|
4719
4756
|
assertEqual(eyePrologInteropAutoload['time/1'], 'iso_ext', 'time/1 canonical interop autoload');
|
|
4720
4757
|
assertEqual(eyePrologInteropAutoload['.../2'], 'iso_ext', '.../2 canonical interop autoload');
|
|
4721
4758
|
assertEqual(eyePrologInteropAutoload['set_nth0/4'] ?? null, null, 'EyeProlog-only set_nth0/4 is not autoloadable');
|
|
4722
|
-
assertEqual(eyePrologNativeLibraryIndicators.length,
|
|
4723
|
-
assertEqual(eyePrologNativeLibraryIndicators.slice(0,
|
|
4724
|
-
assertEqual(eyePrologLibraryIndicators.length,
|
|
4759
|
+
assertEqual(eyePrologNativeLibraryIndicators.length, 42, 'native host library count');
|
|
4760
|
+
assertEqual(eyePrologNativeLibraryIndicators.slice(0, 3).join(','), 'call_nth/2,freeze/2,dif/2', 'control and constraint predicates requiring host support');
|
|
4761
|
+
assertEqual(eyePrologLibraryIndicators.length, 129, 'complete EyeProlog library surface');
|
|
4725
4762
|
assertEqual(registry.get('eyeprolog__call_nth', 2), null, 'private call_nth adapter is absent from ISO registry');
|
|
4726
4763
|
assertEqual(Boolean(library.get('eyeprolog__call_nth', 2)), true, 'private call_nth adapter is registered for EyeProlog');
|
|
4727
4764
|
assertEqual(library.get('eyeprolog__call_nth', 2)?.eyePrologLibrary, true, 'private adapter is marked as library support');
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -6211,13 +6211,13 @@ so side effects occur in Prolog execution order.
|
|
|
6211
6211
|
|
|
6212
6212
|
### The EyeProlog library
|
|
6213
6213
|
|
|
6214
|
-
EyeProlog exposes **
|
|
6214
|
+
EyeProlog exposes **129 library predicate indicators** in addition to the 129
|
|
6215
6215
|
indicators in its isolated ISO profile. **88 are defined entirely as ordinary
|
|
6216
6216
|
Prolog clauses** in focused modules under `src/lib/`. The remaining public
|
|
6217
6217
|
relations use small private host adapters where control, constraints, or host
|
|
6218
6218
|
observability cannot be expressed by ordinary clauses alone; `time/1` is one
|
|
6219
6219
|
such relation. The resulting normal EyeProlog language surface is therefore
|
|
6220
|
-
**
|
|
6220
|
+
**258 public predicate indicators**. Most library relations remain module source
|
|
6221
6221
|
clauses over private adapters. `time/1` is additionally registered directly in
|
|
6222
6222
|
the normal EyeProlog runtime so Trealla-style timing works at the interactive
|
|
6223
6223
|
top level without an import; it is absent from the strict ISO registry.
|
|
@@ -6257,6 +6257,7 @@ between solution branches.
|
|
|
6257
6257
|
| `library(comparison)` | `lt/2`, `gt/2`, `le/2`, `ge/2` |
|
|
6258
6258
|
| `library(dates)` | `difference/3` |
|
|
6259
6259
|
| `library(iso_ext)` | `call_nth/2`, `countall/2`, `forall/2`, `succ/2`, `cfor/3`, `findall/4`, `variant/2`, `time/1`, `.../2` |
|
|
6260
|
+
| Normal runtime extension | `dif/2` |
|
|
6260
6261
|
| `library(lambda)` | `^/3`, `^/4`, `^/5`, `^/6`, `^/7`, `^/8`, `^/9`, `^/10`, `\/1`, `\/2`, `\/3`, `\/4`, `\/5`, `\/6`, `\/7`, `\/8`, `+\/2`, `+\/3`, `+\/4`, `+\/5`, `+\/6`, `+\/7`, `+\/8`, `+\/9` |
|
|
6261
6262
|
| `library(lists)` | `member/2`, `memberchk/2`, `select/3`, `append/2`, `append/3`, `last/2`, `same_length/2`, `nth0/3`, `nth0/4`, `nth1/3`, `nth1/4`, `reverse/2`, `length/2`, `maplist/2`, `maplist/3`, `maplist/4`, `maplist/5`, `maplist/6`, `maplist/7`, `maplist/8`, `foldl/4`, `foldl/5`, `foldl/6`, `sum_list/2`, `min_list/2`, `max_list/2`, `list_to_set/2`, `set_nth0/4`, `take/3`, `drop/3`, `slice/4` |
|
|
6262
6263
|
| `library(primes)` | `smallest_divisor_from/3` |
|
|
@@ -7130,7 +7131,7 @@ Review questions:
|
|
|
7130
7131
|
</figure>
|
|
7131
7132
|
|
|
7132
7133
|
The [examples directory](https://github.com/eyereasoner/eyeprolog/tree/main/examples/) is the book's executable companion. The
|
|
7133
|
-
top-level directory contains **
|
|
7134
|
+
top-level directory contains **212 self-contained runnable programs**. Every
|
|
7134
7135
|
source program has an exact answer file under
|
|
7135
7136
|
[examples/output](https://github.com/eyereasoner/eyeprolog/tree/main/examples/output/), and **61 selected programs** have a checked
|
|
7136
7137
|
explanation under [examples/proof](https://github.com/eyereasoner/eyeprolog/tree/main/examples/proof/). The thematic tables below link every top-level program and open the program
|