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 CHANGED
@@ -91,10 +91,10 @@ noun --> [world] | [prolog].
91
91
  %% goal: phrase(sentence, Words)
92
92
  ```
93
93
 
94
- EyeProlog also adds 57 public library predicate indicators to its 129-entry ISO
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`; the
97
- `call_nth/2` module clause uses one private host adapter to number solutions.
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
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.1.13",
6
+ "version": "1.1.14",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
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
- // call_nth/2 is a Prologue library predicate with control behavior that cannot
168
- // be expressed portably as ordinary Prolog clauses. Keep the public wrapper
169
- // in library(prologue) and expose only this private adapter to the host registry.
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');
@@ -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
- for (const alternative of alternatives) {
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 alternatives) {
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, options);
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' || item.name === 'waits' || item.name === 'other_answer_sequence') {
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 { solutions, error, tailOutput, inputPosition };
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 || this.solutionsSeen >= this.solutionLimit) break;
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 || this.solutionsSeen >= this.solutionLimit) return;
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;
@@ -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
+ ; ... .
@@ -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 reports annotations that cannot be checked safely',
417
+ name: 'runQuads recognizes bounded nontermination descriptions',
396
418
  run: () => {
397
419
  const result = publicApi.runQuads(`?- repeat, fail.\n loops.\n`);
398
- assertEqual(result.results[0].kind, 'unsupported', 'quad result');
399
- assertIncludes(result.stdout, 'quads: UNSUPPORTED <input>:1', 'unsupported report');
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, 130, 'EyeProlog registry contains ISO definitions and one private library adapter');
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, 1, 'public native EyeProlog builtin count');
1420
+ assertEqual(registeredNativeEyePrologLibraryNames().length, 2, 'public native EyeProlog builtin count');
1399
1421
  assertEqual(eyePrologPortableLibraryIndicators.length, 56, 'portable Prolog library count');
1400
- assertEqual(eyePrologNativeLibraryIndicators.length, 1, 'native host library count');
1401
- assertEqual(eyePrologNativeLibraryIndicators.join(','), 'call_nth/2', 'control predicate requiring host support');
1402
- assertEqual(eyePrologLibraryIndicators.length, 57, 'complete EyeProlog library surface');
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');
@@ -5761,13 +5761,13 @@ so side effects occur in Prolog execution order.
5761
5761
 
5762
5762
  ### The EyeProlog library
5763
5763
 
5764
- EyeProlog exposes **57 library predicate indicators** in addition to the 129
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` clause delegates its solution
5768
- numbering to one private host adapter. The resulting normal EyeProlog language
5769
- surface is therefore **186 public predicate indicators**. Internally, the
5770
- runtime registry contains the 129 ISO definitions plus that private adapter;
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` quads and finite examples retained as offline
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. The
6185
- nontermination and advanced stream annotations `loops`, `peeks/1`, and `waits`,
6186
- and the unordered `other_answer_sequence` annotation, are not executed by the
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