eyeprolog 1.1.13 → 1.1.14
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 +3 -3
- package/index.d.ts +5 -0
- package/package.json +1 -1
- package/src/cli.js +10 -0
- package/src/iso.js +20 -3
- package/src/lib/prologue.pl +4 -0
- package/src/quads.js +28 -6
- package/src/solver.js +38 -2
- package/src/standard-library.js +1 -1
- package/src/term.js +20 -0
- package/test/fixtures/length_quad.pl +94 -0
- package/test/run-regression.mjs +32 -8
- package/the-art-of-eyeprolog.md +20 -13
package/README.md
CHANGED
|
@@ -91,10 +91,10 @@ noun --> [world] | [prolog].
|
|
|
91
91
|
%% goal: phrase(sentence, Words)
|
|
92
92
|
```
|
|
93
93
|
|
|
94
|
-
EyeProlog also adds
|
|
94
|
+
EyeProlog also adds 58 public library predicate indicators to its 129-entry ISO
|
|
95
95
|
profile. **56 are implemented entirely as ordinary Prolog clauses** in
|
|
96
|
-
`src/lib/eyeprolog.pl`, `src/lib/lists.pl`, and `src/lib/prologue.pl`;
|
|
97
|
-
`call_nth/2`
|
|
96
|
+
`src/lib/eyeprolog.pl`, `src/lib/lists.pl`, and `src/lib/prologue.pl`;
|
|
97
|
+
`call_nth/2` and `freeze/2` use private host adapters for their control behavior.
|
|
98
98
|
They are ISO/IEC 13211-2 modules,
|
|
99
99
|
loaded explicitly with `use_module(library(eyeprolog))`,
|
|
100
100
|
`use_module(library(lists))`, or `use_module(library(prologue))`. Portable text
|
package/index.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export interface EyePrologRunOptions {
|
|
|
11
11
|
why?: boolean;
|
|
12
12
|
explain?: boolean;
|
|
13
13
|
maxDepth?: number;
|
|
14
|
+
maxInferences?: number;
|
|
14
15
|
solutionLimit?: number;
|
|
15
16
|
registry?: BuiltinRegistry;
|
|
16
17
|
sourceMetadata?: boolean;
|
|
@@ -161,6 +162,10 @@ export class Solver {
|
|
|
161
162
|
program: Program;
|
|
162
163
|
registry: BuiltinRegistry;
|
|
163
164
|
maxDepth: number;
|
|
165
|
+
depthLimitExceeded: boolean;
|
|
166
|
+
maxInferences: number;
|
|
167
|
+
inferences: number;
|
|
168
|
+
inferenceLimitExceeded: boolean;
|
|
164
169
|
solutionLimit: number;
|
|
165
170
|
solutionsSeen: number;
|
|
166
171
|
active: unknown[];
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -110,6 +110,16 @@ export async function main(argv) {
|
|
|
110
110
|
for (const source of sourceParts) options.goals.push(...goalsFromSource(source.text));
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
// The ISO Prolog working-example quad files assume the Prologue predicates
|
|
114
|
+
// are available as system predicates and therefore contain no use_module/1
|
|
115
|
+
// directive. Import their portable EyeProlog counterparts in quad mode.
|
|
116
|
+
if (options.quads) {
|
|
117
|
+
sourceParts.unshift({
|
|
118
|
+
text: ':- use_module(library(prologue)).\n',
|
|
119
|
+
filename: '<quad-prelude>',
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
113
123
|
const engine = await loadEngine();
|
|
114
124
|
let program = engine.Program.parseSources(sourceParts, { sourceMetadata: options.proof });
|
|
115
125
|
|
package/src/iso.js
CHANGED
|
@@ -164,12 +164,13 @@ export const isoBuiltins = {
|
|
|
164
164
|
}
|
|
165
165
|
};
|
|
166
166
|
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
//
|
|
167
|
+
// These Prologue library predicates have control behavior that cannot be
|
|
168
|
+
// expressed portably as ordinary Prolog clauses. Keep their public wrappers in
|
|
169
|
+
// library(prologue) and expose only private adapters to the host registry.
|
|
170
170
|
export const eyePrologLibraryBuiltins = {
|
|
171
171
|
register(registry) {
|
|
172
172
|
registry.add('eyeprolog__call_nth', 2, callNthBuiltin, { eyePrologLibrary: true });
|
|
173
|
+
registry.add('eyeprolog__freeze', 2, freezeBuiltin, { eyePrologLibrary: true });
|
|
173
174
|
},
|
|
174
175
|
};
|
|
175
176
|
|
|
@@ -1727,6 +1728,22 @@ function* callNthBuiltin({ solver, goal, env }) {
|
|
|
1727
1728
|
}
|
|
1728
1729
|
}
|
|
1729
1730
|
|
|
1731
|
+
function* freezeBuiltin({ solver, goal, env }) {
|
|
1732
|
+
const watched = deref(goal.args[0], env);
|
|
1733
|
+
if (watched.type !== VAR) {
|
|
1734
|
+
const child = solver.cloneForInnerGoal();
|
|
1735
|
+
try {
|
|
1736
|
+
yield* child.solve([callable(goal.args[1], env)], env, 0);
|
|
1737
|
+
} finally {
|
|
1738
|
+
solver.absorbStatsFrom(child);
|
|
1739
|
+
}
|
|
1740
|
+
return;
|
|
1741
|
+
}
|
|
1742
|
+
const next = env.clone();
|
|
1743
|
+
next.delay(watched.name, goal.args[1], goal.module ?? 'user');
|
|
1744
|
+
yield next;
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1730
1747
|
function* phraseBuiltin({ solver, goal, env }) {
|
|
1731
1748
|
const grammarBody = deref(goal.args[0], env);
|
|
1732
1749
|
if (grammarBody.type === VAR) throw new PrologError('instantiation_error');
|
package/src/lib/prologue.pl
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
nth1/3,
|
|
20
20
|
nth1/4,
|
|
21
21
|
call_nth/2,
|
|
22
|
+
freeze/2,
|
|
22
23
|
foldl/4,
|
|
23
24
|
foldl/5,
|
|
24
25
|
foldl/6,
|
|
@@ -33,6 +34,7 @@
|
|
|
33
34
|
:- meta_predicate(maplist(6, '?', '?', '?', '?', '?', '?')).
|
|
34
35
|
:- meta_predicate(maplist(7, '?', '?', '?', '?', '?', '?', '?')).
|
|
35
36
|
:- meta_predicate(call_nth(0, '?')).
|
|
37
|
+
:- meta_predicate(freeze('?', 0)).
|
|
36
38
|
:- meta_predicate(foldl(3, '?', '?', '?')).
|
|
37
39
|
:- meta_predicate(foldl(4, '?', '?', '?', '?')).
|
|
38
40
|
:- meta_predicate(foldl(5, '?', '?', '?', '?', '?')).
|
|
@@ -159,6 +161,8 @@ nth1(N, List, Elem, Rest) :-
|
|
|
159
161
|
|
|
160
162
|
call_nth(Goal, Nth) :- eyeprolog__call_nth(Goal, Nth).
|
|
161
163
|
|
|
164
|
+
freeze(Var, Goal) :- eyeprolog__freeze(Var, Goal).
|
|
165
|
+
|
|
162
166
|
foldl(_, [], Acc, Acc).
|
|
163
167
|
foldl(Closure, [A|As], Acc0, Acc) :-
|
|
164
168
|
call(Closure, A, Acc0, Acc1),
|
package/src/quads.js
CHANGED
|
@@ -56,12 +56,16 @@ function checkQuad(program, quad, options) {
|
|
|
56
56
|
|
|
57
57
|
function checkDescription(program, quad, description, options) {
|
|
58
58
|
const alternatives = splitOperator(description, '|');
|
|
59
|
-
|
|
59
|
+
// Probe an explicitly accepted nontermination outcome before alternatives
|
|
60
|
+
// that would run the same query without a bound.
|
|
61
|
+
const ordered = [...alternatives].sort((left, right) =>
|
|
62
|
+
Number(alternativeDescribesLoop(right)) - Number(alternativeDescribesLoop(left)));
|
|
63
|
+
for (const alternative of ordered) {
|
|
60
64
|
const malformed = malformedAlternative(quad.query, alternative);
|
|
61
65
|
if (malformed != null) return { ok: false, kind: 'malformed', expected: malformed };
|
|
62
66
|
}
|
|
63
67
|
let unsupported = null;
|
|
64
|
-
for (const alternative of
|
|
68
|
+
for (const alternative of ordered) {
|
|
65
69
|
const checked = checkAlternative(program, quad, alternative, options);
|
|
66
70
|
if (checked.ok) return checked;
|
|
67
71
|
if (checked.kind === 'unsupported') unsupported ??= checked;
|
|
@@ -86,7 +90,10 @@ function checkAlternative(program, quad, alternative, options) {
|
|
|
86
90
|
const maxSolutions = inputSpecs.length > 0
|
|
87
91
|
? 1
|
|
88
92
|
: moreAt < 0 ? describedCount + 1 : Math.max(describedCount, 1);
|
|
89
|
-
const actual = executeQuery(program, quad.query, input, maxSolutions,
|
|
93
|
+
const actual = executeQuery(program, quad.query, input, maxSolutions, {
|
|
94
|
+
...options,
|
|
95
|
+
detectLoops: leaves.some((leaf) => leaf.loops),
|
|
96
|
+
});
|
|
90
97
|
|
|
91
98
|
if (inputSpecs.length > 0) {
|
|
92
99
|
const leaf = leaves[0];
|
|
@@ -137,6 +144,7 @@ function describeLeaf(term) {
|
|
|
137
144
|
sto: false,
|
|
138
145
|
false: false,
|
|
139
146
|
truth: false,
|
|
147
|
+
loops: false,
|
|
140
148
|
error: null,
|
|
141
149
|
input: null,
|
|
142
150
|
output: null,
|
|
@@ -149,7 +157,8 @@ function describeLeaf(term) {
|
|
|
149
157
|
if (item.name === 'unexpected' || item.name === 'inattendue') leaf.unexpected = true;
|
|
150
158
|
else if (item.name === '...' || item.name === 'ad_infinitum') leaf.more = true;
|
|
151
159
|
else if (item.name === 'sto') leaf.sto = true;
|
|
152
|
-
else if (item.name === 'loops'
|
|
160
|
+
else if (item.name === 'loops') leaf.loops = true;
|
|
161
|
+
else if (item.name === 'waits' || item.name === 'other_answer_sequence') {
|
|
153
162
|
leaf.unsupported ??= item;
|
|
154
163
|
} else if (item.name === 'false') leaf.false = true;
|
|
155
164
|
else if (item.name === 'true') leaf.truth = true;
|
|
@@ -181,7 +190,7 @@ function describeLeaf(term) {
|
|
|
181
190
|
if (isErrorDescription(item)) leaf.error = item;
|
|
182
191
|
else leaf.malformed ??= item;
|
|
183
192
|
}
|
|
184
|
-
leaf.hasExpectation = leaf.bindings.length > 0 || leaf.truth || leaf.false || leaf.error != null || leaf.output != null;
|
|
193
|
+
leaf.hasExpectation = leaf.bindings.length > 0 || leaf.truth || leaf.false || leaf.loops || leaf.error != null || leaf.output != null;
|
|
185
194
|
if (!leaf.hasExpectation && !leaf.more && !leaf.sto && leaf.unsupported == null) leaf.malformed ??= term;
|
|
186
195
|
if ([leaf.false, leaf.truth, leaf.error != null].filter(Boolean).length > 1) leaf.malformed ??= term;
|
|
187
196
|
return leaf;
|
|
@@ -192,6 +201,8 @@ function executeQuery(program, query, input, maxSolutions, options) {
|
|
|
192
201
|
const solver = new Solver(program, {
|
|
193
202
|
...options,
|
|
194
203
|
registry: options.registry ?? getEyePrologRegistry(),
|
|
204
|
+
maxDepth: options.detectLoops ? (options.loopMaxDepth ?? 1000) : options.maxDepth,
|
|
205
|
+
maxInferences: options.detectLoops ? (options.loopMaxInferences ?? 10000) : options.maxInferences,
|
|
195
206
|
// The solver's counter also observes completed nested searches (for
|
|
196
207
|
// example each arm of a DCG disjunction). Bound the public iterator here
|
|
197
208
|
// instead of letting those internal completions consume the quad's answer
|
|
@@ -225,10 +236,17 @@ function executeQuery(program, query, input, maxSolutions, options) {
|
|
|
225
236
|
error = { term: errorTerm(caught), output: pendingOutput };
|
|
226
237
|
}
|
|
227
238
|
const inputPosition = solver.io.resolve('user_input')?.position ?? 0;
|
|
228
|
-
return {
|
|
239
|
+
return {
|
|
240
|
+
solutions,
|
|
241
|
+
error,
|
|
242
|
+
tailOutput,
|
|
243
|
+
inputPosition,
|
|
244
|
+
loops: solver.depthLimitExceeded || solver.inferenceLimitExceeded,
|
|
245
|
+
};
|
|
229
246
|
}
|
|
230
247
|
|
|
231
248
|
function matchLeaf(query, leaf, actual, position) {
|
|
249
|
+
if (leaf.loops) return actual.loops;
|
|
232
250
|
if (leaf.false) {
|
|
233
251
|
return position >= actual.solutions.length && actual.error == null && outputMatches(leaf.output, actual.tailOutput);
|
|
234
252
|
}
|
|
@@ -241,6 +259,10 @@ function matchLeaf(query, leaf, actual, position) {
|
|
|
241
259
|
return substitutionMatches(query, leaf.bindings, solution.env);
|
|
242
260
|
}
|
|
243
261
|
|
|
262
|
+
function alternativeDescribesLoop(alternative) {
|
|
263
|
+
return splitOperator(alternative, ';').some((term) => describeLeaf(term).loops);
|
|
264
|
+
}
|
|
265
|
+
|
|
244
266
|
function substitutionMatches(query, bindings, actualEnv) {
|
|
245
267
|
const queryVariables = namedVariables(query);
|
|
246
268
|
const queryNames = new Set(queryVariables.map((variable) => variable.name));
|
package/src/solver.js
CHANGED
|
@@ -29,6 +29,10 @@ export class Solver {
|
|
|
29
29
|
this.mutableProgram = program.mutable === true;
|
|
30
30
|
this.programRevision = this.program.revision ?? 0;
|
|
31
31
|
this.maxDepth = options.maxDepth ?? 100000;
|
|
32
|
+
this.depthLimitExceeded = false;
|
|
33
|
+
this.maxInferences = options.maxInferences ?? Infinity;
|
|
34
|
+
this.inferences = 0;
|
|
35
|
+
this.inferenceLimitExceeded = false;
|
|
32
36
|
this.solutionLimit = options.solutionLimit ?? 10000000;
|
|
33
37
|
this.solutionsSeen = 0;
|
|
34
38
|
this.prologFlags = options.prologFlags ?? defaultPrologFlags(this.registry?.eyePrologLibrary ? 'fail' : 'error');
|
|
@@ -83,6 +87,7 @@ export class Solver {
|
|
|
83
87
|
const solver = new Solver(this.program, {
|
|
84
88
|
registry: this.registry,
|
|
85
89
|
maxDepth: this.maxDepth,
|
|
90
|
+
maxInferences: this.maxInferences,
|
|
86
91
|
solutionLimit,
|
|
87
92
|
prologFlags: this.prologFlags,
|
|
88
93
|
charConversions: this.charConversions,
|
|
@@ -110,6 +115,8 @@ export class Solver {
|
|
|
110
115
|
|
|
111
116
|
absorbStatsFrom(child) {
|
|
112
117
|
if (!child || child === this || !child.stats) return;
|
|
118
|
+
this.depthLimitExceeded ||= child.depthLimitExceeded;
|
|
119
|
+
this.inferenceLimitExceeded ||= child.inferenceLimitExceeded;
|
|
113
120
|
for (const [key, value] of Object.entries(child.stats)) {
|
|
114
121
|
if (key === 'max_depth' || key === 'max_goal_count') {
|
|
115
122
|
this.stats[key] = Math.max(this.stats[key] ?? 0, value ?? 0);
|
|
@@ -140,6 +147,11 @@ export class Solver {
|
|
|
140
147
|
registeredStack = stack;
|
|
141
148
|
this.solveStacks.push(stack);
|
|
142
149
|
while (stack.length) {
|
|
150
|
+
this.inferences++;
|
|
151
|
+
if (this.inferences > this.maxInferences) {
|
|
152
|
+
this.inferenceLimitExceeded = true;
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
143
155
|
const frame = stack.pop();
|
|
144
156
|
this.syncProgramRevision();
|
|
145
157
|
if (frame.kind === 'resumeBuiltin') {
|
|
@@ -185,11 +197,31 @@ export class Solver {
|
|
|
185
197
|
let active = frame.active;
|
|
186
198
|
|
|
187
199
|
while (true) {
|
|
200
|
+
this.inferences++;
|
|
201
|
+
if (this.inferences > this.maxInferences) {
|
|
202
|
+
this.inferenceLimitExceeded = true;
|
|
203
|
+
stack.length = 0;
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
188
206
|
this.syncProgramRevision();
|
|
189
207
|
this.stats.solve_goals_calls++;
|
|
190
208
|
this.stats.max_depth = Math.max(this.stats.max_depth, depth);
|
|
191
209
|
this.stats.max_goal_count = Math.max(this.stats.max_goal_count, goals.length);
|
|
192
|
-
if (depth > this.maxDepth
|
|
210
|
+
if (depth > this.maxDepth) {
|
|
211
|
+
this.depthLimitExceeded = true;
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
if (this.solutionsSeen >= this.solutionLimit) break;
|
|
215
|
+
|
|
216
|
+
const readyDelays = env.takeReadyDelays();
|
|
217
|
+
if (readyDelays.length > 0) {
|
|
218
|
+
const awakened = readyDelays.map(({ goal, module }) => {
|
|
219
|
+
const delayed = copyResolved(goal, env);
|
|
220
|
+
qualifyTerm(delayed, module);
|
|
221
|
+
return delayed;
|
|
222
|
+
});
|
|
223
|
+
goals = [...awakened, ...goals];
|
|
224
|
+
}
|
|
193
225
|
|
|
194
226
|
if (goals.length === 0) {
|
|
195
227
|
this.solutionsSeen++;
|
|
@@ -348,7 +380,11 @@ export class Solver {
|
|
|
348
380
|
|
|
349
381
|
*solveUserGoal(goal, rest, env, depth) {
|
|
350
382
|
this.stats.solve_one_goal_calls++;
|
|
351
|
-
if (depth > this.maxDepth
|
|
383
|
+
if (depth > this.maxDepth) {
|
|
384
|
+
this.depthLimitExceeded = true;
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
if (this.solutionsSeen >= this.solutionLimit) return;
|
|
352
388
|
if (goal.type !== COMPOUND && goal.type !== 'atom') return;
|
|
353
389
|
const group = this.program.findGroup(goal.name, goal.arity, goal.module ?? 'user');
|
|
354
390
|
if (!group) return;
|
package/src/standard-library.js
CHANGED
|
@@ -35,7 +35,7 @@ function libraryUrl(filename) {
|
|
|
35
35
|
return url;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
export const eyePrologNativeLibraryIndicators = Object.freeze(['call_nth/2']);
|
|
38
|
+
export const eyePrologNativeLibraryIndicators = Object.freeze(['call_nth/2', 'freeze/2']);
|
|
39
39
|
export const eyePrologPortableLibraryIndicators = Object.freeze([
|
|
40
40
|
'uuid/3', 'difference/3', 'maplist/2', 'maplist/3', 'maplist/4', 'maplist/5',
|
|
41
41
|
'maplist/6', 'maplist/7', 'maplist/8', 'lt/2', 'gt/2', 'le/2', 'ge/2',
|
package/src/term.js
CHANGED
|
@@ -45,6 +45,7 @@ export class Env {
|
|
|
45
45
|
cacheValue: undefined,
|
|
46
46
|
cache: null,
|
|
47
47
|
};
|
|
48
|
+
this._delays = null;
|
|
48
49
|
}
|
|
49
50
|
clone() {
|
|
50
51
|
// Most speculative environments are either rejected without a binding or
|
|
@@ -54,6 +55,7 @@ export class Env {
|
|
|
54
55
|
// occasionally flattened.
|
|
55
56
|
const clone = Object.create(Env.prototype);
|
|
56
57
|
clone._state = this._state;
|
|
58
|
+
clone._delays = this._delays;
|
|
57
59
|
return clone;
|
|
58
60
|
}
|
|
59
61
|
has(name) {
|
|
@@ -125,6 +127,24 @@ export class Env {
|
|
|
125
127
|
cache: null,
|
|
126
128
|
};
|
|
127
129
|
}
|
|
130
|
+
delay(name, goal, module = 'user') {
|
|
131
|
+
const delays = new Map(this._delays ?? []);
|
|
132
|
+
delays.set(name, [...(delays.get(name) ?? []), { goal, module }]);
|
|
133
|
+
this._delays = delays;
|
|
134
|
+
}
|
|
135
|
+
takeReadyDelays() {
|
|
136
|
+
if (this._delays == null || this._delays.size === 0) return [];
|
|
137
|
+
const ready = [];
|
|
138
|
+
let remaining = this._delays;
|
|
139
|
+
for (const [name, delays] of this._delays) {
|
|
140
|
+
if (deref(variable(name), this).type === VAR) continue;
|
|
141
|
+
if (remaining === this._delays) remaining = new Map(this._delays);
|
|
142
|
+
remaining.delete(name);
|
|
143
|
+
ready.push(...delays);
|
|
144
|
+
}
|
|
145
|
+
if (ready.length > 0) this._delays = remaining;
|
|
146
|
+
return ready;
|
|
147
|
+
}
|
|
128
148
|
}
|
|
129
149
|
|
|
130
150
|
export function deref(term, env) {
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
% ids refer to http://www.complang.tuwien.ac.at/ulrich/iso-prolog/length#1
|
|
2
|
+
|
|
3
|
+
a1 ?- atom_length(A,N).
|
|
4
|
+
instantiation_error.
|
|
5
|
+
a2 ?- atom_length(a,a).
|
|
6
|
+
type_error(integer,a).
|
|
7
|
+
a3 ?- atom_length(a,1.1).
|
|
8
|
+
type_error(integer,1.1).
|
|
9
|
+
a4 ?- atom_length(a,-1).
|
|
10
|
+
domain_error(not_less_than_zero,-1).
|
|
11
|
+
a5 ?- atom_length(1,N).
|
|
12
|
+
type_error(atom,1).
|
|
13
|
+
1 ?- length(L,N).
|
|
14
|
+
L = [], N = 0
|
|
15
|
+
; L = [_A], N = 1
|
|
16
|
+
; L = [_A,_B], N = 2
|
|
17
|
+
; ... .
|
|
18
|
+
2 ?- length(L,0).
|
|
19
|
+
L = [].
|
|
20
|
+
3 ?- length([_|L],0).
|
|
21
|
+
false.
|
|
22
|
+
4 ?- length(2,0).
|
|
23
|
+
false.
|
|
24
|
+
5 ?- length([_|2],0).
|
|
25
|
+
false.
|
|
26
|
+
6 ?- length([_|2],N).
|
|
27
|
+
false.
|
|
28
|
+
7 ?- length([_|2],2).
|
|
29
|
+
false.
|
|
30
|
+
8 ?- length(L,-1).
|
|
31
|
+
domain_error(not_less_than_zero,-1).
|
|
32
|
+
9 ?- length([],-1).
|
|
33
|
+
domain_error(not_less_than_zero,-1).
|
|
34
|
+
10 ?- length(a,-1).
|
|
35
|
+
domain_error(not_less_than_zero,-1).
|
|
36
|
+
11 ?- length([],-0.1).
|
|
37
|
+
type_error(integer,-0.1).
|
|
38
|
+
12 ?- length(L,-0.1).
|
|
39
|
+
type_error(integer,-0.1).
|
|
40
|
+
13 ?- length([a],1.0).
|
|
41
|
+
type_error(integer,1.0).
|
|
42
|
+
14 ?- length(L,1.0).
|
|
43
|
+
type_error(integer,1.0).
|
|
44
|
+
15 ?- length(L,1.1).
|
|
45
|
+
type_error(integer,1.1).
|
|
46
|
+
16 ?- length(L,1.0e99).
|
|
47
|
+
type_error(integer,1.0e99).
|
|
48
|
+
17 ?- N is 2^52, length([], N).
|
|
49
|
+
false.
|
|
50
|
+
18 ?- length([],0+0).
|
|
51
|
+
type_error(integer,0+0).
|
|
52
|
+
19 ?- length([],-_).
|
|
53
|
+
type_error(integer,-_).
|
|
54
|
+
20 ?- length([a],-_).
|
|
55
|
+
type_error(integer,-_).
|
|
56
|
+
21 ?- length([a,b|X],X).
|
|
57
|
+
resource_error(finite_memory)
|
|
58
|
+
| resource_error(...)
|
|
59
|
+
| loops.
|
|
60
|
+
22 ?- length(L,L).
|
|
61
|
+
resource_error(finite_memory)
|
|
62
|
+
| resource_error(...)
|
|
63
|
+
| loops.
|
|
64
|
+
23 ?- L = [_|_], length(L,L).
|
|
65
|
+
type_error(integer,[_|_]).
|
|
66
|
+
24 ?- L = [_], length(L,L).
|
|
67
|
+
type_error(integer,[_]).
|
|
68
|
+
25 ?- L = [1], length(L,L).
|
|
69
|
+
type_error(integer,[1]).
|
|
70
|
+
26 ?- L = [a|L], length(L,N).
|
|
71
|
+
sto, false % current_prolog_flag(occurs_check, true)
|
|
72
|
+
| sto, resource_error(finite_memory)
|
|
73
|
+
| sto, resource_error(...)
|
|
74
|
+
| sto, loops
|
|
75
|
+
| sto, L = [a,a], N = 2
|
|
76
|
+
; L = [a,a,_A], N = 3
|
|
77
|
+
; ... . % dag representation, literal substitution, Tau
|
|
78
|
+
27 ?- L = [a|L], length(L,0).
|
|
79
|
+
sto, false.
|
|
80
|
+
28 ?- L = [a|L], length(L,7).
|
|
81
|
+
sto, false
|
|
82
|
+
| sto, L = [a,a,_A,_B,_C,_D,_E]. % tau
|
|
83
|
+
29 ?- freeze(L,L=[]), length(L,L).
|
|
84
|
+
false.
|
|
85
|
+
30 ?- freeze(L,L=[_|L]), length(L,N).
|
|
86
|
+
sto, loops
|
|
87
|
+
| sto, resource_error(...).
|
|
88
|
+
31 ?- freeze(L,L=[_|L]), N is 2^64, length(L,N).
|
|
89
|
+
sto, false.
|
|
90
|
+
32 ?- length([a,b|L], N).
|
|
91
|
+
L = [], N = 2
|
|
92
|
+
; L = [_A], N = 3
|
|
93
|
+
; L = [_A,_B], N = 4
|
|
94
|
+
; ... .
|
package/test/run-regression.mjs
CHANGED
|
@@ -366,6 +366,28 @@ c4 ?- call((!;1)).
|
|
|
366
366
|
assertEqual(result.stdout, 'quads: 13 run, 13 passed, 0 failed.\n', 'quad report');
|
|
367
367
|
},
|
|
368
368
|
},
|
|
369
|
+
{
|
|
370
|
+
name: 'CLI passes the complete authoritative length quad corpus',
|
|
371
|
+
run: () => {
|
|
372
|
+
const filename = path.join(testRoot, 'fixtures', 'length_quad.pl');
|
|
373
|
+
const source = fs.readFileSync(filename, 'utf8');
|
|
374
|
+
assertEqual(Program.parse(source).quads.length, 37, 'vendored quad total');
|
|
375
|
+
const result = runCli(['-q', filename]);
|
|
376
|
+
assertEqual(result.status, 0, 'quad exit status');
|
|
377
|
+
assertEqual(result.stdout, 'quads: 37 run, 37 passed, 0 failed.\n', 'quad report');
|
|
378
|
+
assertEqual(result.stderr, '', 'quad stderr');
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
name: 'Prologue freeze wakes delayed goals with their bindings',
|
|
383
|
+
run: () => {
|
|
384
|
+
const result = runEyeProlog(
|
|
385
|
+
':- use_module(library(prologue)).\nwake(X, Y) :- freeze(X, Y = awake), X = ready.\n',
|
|
386
|
+
{ goal: 'wake(X, Y)' },
|
|
387
|
+
);
|
|
388
|
+
assertEqual(result.stdout, 'wake(ready, awake).\n', 'freeze answer');
|
|
389
|
+
},
|
|
390
|
+
},
|
|
369
391
|
{
|
|
370
392
|
name: 'runQuads covers the remaining finite Prologue examples and arities',
|
|
371
393
|
run: () => {
|
|
@@ -392,11 +414,11 @@ c4 ?- call((!;1)).
|
|
|
392
414
|
},
|
|
393
415
|
},
|
|
394
416
|
{
|
|
395
|
-
name: 'runQuads
|
|
417
|
+
name: 'runQuads recognizes bounded nontermination descriptions',
|
|
396
418
|
run: () => {
|
|
397
419
|
const result = publicApi.runQuads(`?- repeat, fail.\n loops.\n`);
|
|
398
|
-
assertEqual(result.
|
|
399
|
-
|
|
420
|
+
assertEqual(result.passed, 1, 'quad passed');
|
|
421
|
+
assertEqual(result.stdout, 'quads: 1 run, 1 passed, 0 failed.\n', 'quad report');
|
|
400
422
|
},
|
|
401
423
|
},
|
|
402
424
|
{
|
|
@@ -1392,17 +1414,19 @@ open(X) :- candidate(X), \\+ closed(X).
|
|
|
1392
1414
|
assertEqual(Boolean(registry.get('is', 2)), true, 'ISO is/2 exists');
|
|
1393
1415
|
assertEqual(Boolean(registry.get('append', 3)), false, 'append/3 is not ISO core');
|
|
1394
1416
|
assertEqual(library.eyePrologLibrary, true, 'complete registry marker');
|
|
1395
|
-
assertEqual(library.defs.size,
|
|
1417
|
+
assertEqual(library.defs.size, 131, 'EyeProlog registry contains ISO definitions and two private library adapters');
|
|
1396
1418
|
assertEqual(Boolean(registry.get('phrase', 2)), true, 'Part 3 phrase/2 exists');
|
|
1397
1419
|
assertEqual(Boolean(registry.get('phrase', 3)), true, 'Part 3 phrase/3 exists');
|
|
1398
|
-
assertEqual(registeredNativeEyePrologLibraryNames().length,
|
|
1420
|
+
assertEqual(registeredNativeEyePrologLibraryNames().length, 2, 'public native EyeProlog builtin count');
|
|
1399
1421
|
assertEqual(eyePrologPortableLibraryIndicators.length, 56, 'portable Prolog library count');
|
|
1400
|
-
assertEqual(eyePrologNativeLibraryIndicators.length,
|
|
1401
|
-
assertEqual(eyePrologNativeLibraryIndicators.join(','), 'call_nth/2', 'control
|
|
1402
|
-
assertEqual(eyePrologLibraryIndicators.length,
|
|
1422
|
+
assertEqual(eyePrologNativeLibraryIndicators.length, 2, 'native host library count');
|
|
1423
|
+
assertEqual(eyePrologNativeLibraryIndicators.join(','), 'call_nth/2,freeze/2', 'control predicates requiring host support');
|
|
1424
|
+
assertEqual(eyePrologLibraryIndicators.length, 58, 'complete EyeProlog library surface');
|
|
1403
1425
|
assertEqual(registry.get('eyeprolog__call_nth', 2), null, 'private call_nth adapter is absent from ISO registry');
|
|
1404
1426
|
assertEqual(Boolean(library.get('eyeprolog__call_nth', 2)), true, 'private call_nth adapter is registered for EyeProlog');
|
|
1405
1427
|
assertEqual(library.get('eyeprolog__call_nth', 2)?.eyePrologLibrary, true, 'private adapter is marked as library support');
|
|
1428
|
+
assertEqual(registry.get('eyeprolog__freeze', 2), null, 'private freeze adapter is absent from ISO registry');
|
|
1429
|
+
assertEqual(Boolean(library.get('eyeprolog__freeze', 2)), true, 'private freeze adapter is registered for EyeProlog');
|
|
1406
1430
|
assertEqual(library.get('between', 3), null, 'between/3 remains portable Prolog');
|
|
1407
1431
|
assertEqual(library.get('smallest_divisor_from', 3), null, 'smallest_divisor_from/3 remains portable Prolog');
|
|
1408
1432
|
assertEqual(library.get('random', 3), null, 'random/3 remains portable Prolog');
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -5761,13 +5761,13 @@ so side effects occur in Prolog execution order.
|
|
|
5761
5761
|
|
|
5762
5762
|
### The EyeProlog library
|
|
5763
5763
|
|
|
5764
|
-
EyeProlog exposes **
|
|
5764
|
+
EyeProlog exposes **58 library predicate indicators** in addition to the 129
|
|
5765
5765
|
indicators in its isolated ISO profile. **56 are implemented entirely as
|
|
5766
5766
|
ordinary Prolog clauses** across `src/lib/eyeprolog.pl`, `src/lib/lists.pl`, and
|
|
5767
|
-
`src/lib/prologue.pl`. The Prologue `call_nth/2`
|
|
5768
|
-
|
|
5769
|
-
surface is therefore **
|
|
5770
|
-
runtime registry contains the 129 ISO definitions plus
|
|
5767
|
+
`src/lib/prologue.pl`. The Prologue `call_nth/2` and `freeze/2` clauses delegate
|
|
5768
|
+
their control behavior to private host adapters. The resulting normal EyeProlog
|
|
5769
|
+
language surface is therefore **187 public predicate indicators**. Internally, the
|
|
5770
|
+
runtime registry contains the 129 ISO definitions plus those two private adapters;
|
|
5771
5771
|
the remaining EyeProlog relations are module source clauses.
|
|
5772
5772
|
|
|
5773
5773
|
The three Prolog files declare `eyeprolog`, `lists`, and `prologue` with
|
|
@@ -5776,8 +5776,8 @@ The three Prolog files declare `eyeprolog`, `lists`, and `prologue` with
|
|
|
5776
5776
|
`use_module(library(prologue))`; `use_module/2` can select a smaller import
|
|
5777
5777
|
list. The last module implements p.p.1 through p.p.11 of the
|
|
5778
5778
|
[working-draft Prologue](https://www.complang.tuwien.ac.at/ulrich/iso-prolog/prologue),
|
|
5779
|
-
with the published `call_nth/2
|
|
5780
|
-
regressions. `src/standard-library.js` registers the module sources and private
|
|
5779
|
+
with the published Prologue, `call_nth/2`, and `length/2` quads retained as
|
|
5780
|
+
offline regressions. `src/standard-library.js` registers the module sources and private
|
|
5781
5781
|
control adapter for Node and browser resolution, and never adds clauses
|
|
5782
5782
|
implicitly.
|
|
5783
5783
|
The isolated ISO-only registry remains
|
|
@@ -5785,13 +5785,18 @@ available through `createDefaultRegistry()` and `getDefaultRegistry()` for
|
|
|
5785
5785
|
conformance work and advanced embedders. Module-local predicate identity keeps
|
|
5786
5786
|
private helpers and same-named predicates in different modules separate.
|
|
5787
5787
|
|
|
5788
|
+
`freeze(?Term,:Goal)` runs `Goal` immediately when `Term` is already nonvariable;
|
|
5789
|
+
otherwise it delays the goal until `Term` becomes nonvariable. Suspensions are
|
|
5790
|
+
kept in the logical environment, so bindings and backtracking remain isolated
|
|
5791
|
+
between solution branches.
|
|
5792
|
+
|
|
5788
5793
|
<!-- eyeprolog-library-catalog:start -->
|
|
5789
5794
|
|
|
5790
5795
|
| Module | Exported predicate indicators |
|
|
5791
5796
|
| --- | --- |
|
|
5792
5797
|
| `library(lists)` | `maplist/3`, `append/3`, `member/2`, `select/3`, `last/2`, `nth0/3`, `nth1/3`, `reverse/2`, `length/2`, `sum_list/2`, `min_list/2`, `max_list/2`, `list_to_set/2`, `countall/2` |
|
|
5793
5798
|
| `library(eyeprolog)` | `uuid/3`, `difference/3`, `lt/2`, `le/2`, `gt/2`, `ge/2`, `between/3`, `smallest_divisor_from/3`, `random/3`, `matches/3`, `split/3`, `replace/4`, `lowercase/2`, `uppercase/2`, `trim/2`, `number_string/2`, `atom_string/2`, `term_string/2`, `string_concat/3`, `contains/2`, `matches/2`, `join/3`, `substring/4`, `set_nth0/4`, `take/3`, `drop/3`, `slice/4`, `sumall/3`, `aggregate_min/5`, `aggregate_max/5` |
|
|
5794
|
-
| `library(prologue)` | `member/2`, `append/3`, `length/2`, `between/3`, `select/3`, `succ/2`, `maplist/2`, `maplist/3`, `maplist/4`, `maplist/5`, `maplist/6`, `maplist/7`, `maplist/8`, `nth0/3`, `nth0/4`, `nth1/3`, `nth1/4`, `call_nth/2`, `foldl/4`, `foldl/5`, `foldl/6`, `countall/2` |
|
|
5799
|
+
| `library(prologue)` | `member/2`, `append/3`, `length/2`, `between/3`, `select/3`, `succ/2`, `maplist/2`, `maplist/3`, `maplist/4`, `maplist/5`, `maplist/6`, `maplist/7`, `maplist/8`, `nth0/3`, `nth0/4`, `nth1/3`, `nth1/4`, `call_nth/2`, `freeze/2`, `foldl/4`, `foldl/5`, `foldl/6`, `countall/2` |
|
|
5795
5800
|
|
|
5796
5801
|
<!-- eyeprolog-library-catalog:end -->
|
|
5797
5802
|
|
|
@@ -6169,7 +6174,9 @@ Run all quads in a file with `eyeprolog --quads file.pl` or `eyeprolog -q
|
|
|
6169
6174
|
file.pl`. A label such as `colors` is optional. Loading the file normally only
|
|
6170
6175
|
records its quads; it does not execute them or add their queries and answers as
|
|
6171
6176
|
program clauses. A quad run prints a summary and exits with status `1` when any
|
|
6172
|
-
description fails.
|
|
6177
|
+
description fails. Quad mode imports `library(prologue)` as a compatibility
|
|
6178
|
+
prelude because the ISO Prolog working-example files use those predicates as
|
|
6179
|
+
system predicates without an explicit module directive.
|
|
6173
6180
|
|
|
6174
6181
|
Unless the source explicitly selects another `unknown` flag, quad execution
|
|
6175
6182
|
uses `unknown=error`, so an undefined predicate is reported rather than being
|
|
@@ -6181,10 +6188,10 @@ and the `unexpected` annotation for an answer that must not occur (`inattendue`
|
|
|
6181
6188
|
is its synonym). `...` and `ad_infinitum` accept further answers. Multiple
|
|
6182
6189
|
indented descriptions after one query must all hold. `inputs/1` supplies and
|
|
6183
6190
|
checks consumed characters; `outputs/1` checks emitted characters. `sto` marks
|
|
6184
|
-
an answer description that this finite-tree implementation skips.
|
|
6185
|
-
|
|
6186
|
-
and the unordered `other_answer_sequence`
|
|
6187
|
-
current runner.
|
|
6191
|
+
an answer description that this finite-tree implementation skips. `loops` is
|
|
6192
|
+
checked with a deterministic solver-depth budget. The advanced stream
|
|
6193
|
+
annotations `peeks/1` and `waits`, and the unordered `other_answer_sequence`
|
|
6194
|
+
annotation, are not executed by the current runner.
|
|
6188
6195
|
|
|
6189
6196
|
The JavaScript API exposes the same operation without process I/O:
|
|
6190
6197
|
|