eyeprolog 1.3.13 → 1.3.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
@@ -201,8 +201,8 @@ noun --> [world] | [prolog].
201
201
  %% goal: phrase(sentence, Words)
202
202
  ```
203
203
 
204
- EyeProlog also adds 126 public library predicate indicators to its 129-entry ISO
205
- profile. **87 are implemented entirely as ordinary Prolog clauses** in focused
204
+ EyeProlog also adds 128 public library predicate indicators to its 129-entry ISO
205
+ profile. **88 are implemented entirely as ordinary Prolog clauses** in focused
206
206
  modules under `src/lib/`; the remaining control predicates and finite-domain
207
207
  `library(clpz)` kernel use backtrackable host support.
208
208
  They are ISO/IEC 13211-2 modules loaded explicitly by purpose, such as
@@ -225,8 +225,13 @@ headroom, so an exhausted finite heap is reported as a catchable
225
225
  `resource_error(memory)` instead of degenerating into quadratic list checks.
226
226
 
227
227
  `library(iso_ext)` is also accepted as a common interop module name.
228
- EyeProlog exports `call_nth/2` there, so Scryer-style source can explicitly use
229
- `:- use_module(library(iso_ext)).`; unqualified source may still autoload it.
228
+ EyeProlog exports `call_nth/2`, `time/1`, and the DCG helper `...//0` there.
229
+ The latter describes an arbitrary number of input elements and supports the
230
+ nonterminal hand-off benchmark discussed in issue #49. These common predicates
231
+ may be imported explicitly, while source/CLI/API dependency loading can resolve
232
+ their unqualified forms conservatively. For Trealla-style interactive timing,
233
+ `time/1` is also available directly in the normal EyeProlog runtime; strict ISO
234
+ mode does not expose it.
230
235
  The aligned `library(lists)` and `library(iso_ext)` exports are kept disjoint so
231
236
  they can be imported together without an accidental import conflict. EyeProlog's
232
237
  legacy `library(prologue)` remains a compatibility umbrella and should be
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.3.13",
6
+ "version": "1.3.14",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
package/src/dcg.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // Grammar rules are lowered to ordinary clauses during program preparation;
3
3
  // phrase/2-3 use the same body expansion for dynamically supplied grammars.
4
4
  import {
5
- ATOM, COMPOUND, VAR, Env, atom, compound, deref, emptyList,
5
+ ATOM, COMPOUND, VAR, Env, atom, compactListLength, compound, deref, emptyList,
6
6
  flattenConjunction, variable,
7
7
  } from './term.js';
8
8
  import { PrologError } from './iso.js';
@@ -280,6 +280,11 @@ export function expandDcgRuleClause(clause, defaultModule = 'user') {
280
280
  export function isListOrPartialList(term, env) {
281
281
  const seen = new Set();
282
282
  let cursor = deref(term, env);
283
+ // A compact list skeleton has a fixed proper spine by construction. Its
284
+ // elements may later receive bindings, but those bindings cannot change the
285
+ // list tail shape, so phrase/2-3 need not expand the entire lazy spine merely
286
+ // to validate it as a list.
287
+ if (compactListLength(cursor) != null) return true;
283
288
  while (cursor.type === COMPOUND && cursor.name === '.' && cursor.arity === 2) {
284
289
  if (seen.has(cursor)) return false;
285
290
  seen.add(cursor);
package/src/iso.js CHANGED
@@ -176,6 +176,8 @@ export const eyePrologLibraryBuiltins = {
176
176
  registry.add('eyeprolog__call_nth', 2, callNthBuiltin, { eyePrologLibrary: true });
177
177
  registry.add('eyeprolog__countall', 2, countAllBuiltin, { eyePrologLibrary: true });
178
178
  registry.add('eyeprolog__freeze', 2, freezeBuiltin, { eyePrologLibrary: true });
179
+ registry.add('time', 1, timeBuiltin, { eyePrologLibrary: true });
180
+ registry.add('eyeprolog__time', 1, timeBuiltin, { eyePrologLibrary: true });
179
181
  },
180
182
  };
181
183
 
@@ -1899,29 +1901,31 @@ const bagofBuiltin = allSolutionsBuiltin(false);
1899
1901
  const setofBuiltin = allSolutionsBuiltin(true);
1900
1902
 
1901
1903
  function callable(term, env) {
1902
- term = resolveCallable(term, env);
1904
+ term = deref(term, env);
1903
1905
  if (term.type === VAR) throw new PrologError('instantiation_error');
1904
1906
  if (term.type !== ATOM && term.type !== COMPOUND) throw new PrologError('type_error(callable)', term);
1905
- validateControlCallable(term, term);
1907
+ validateControlCallable(term, term, env);
1906
1908
  return term;
1907
1909
  }
1908
- function validateControlCallable(term, culprit) {
1909
- if (term.type !== COMPOUND || ![',', ';', '->'].includes(term.name) || term.arity !== 2) return;
1910
- for (const argument of term.args) {
1911
- if (argument.type === VAR) throw new PrologError('instantiation_error');
1912
- if (argument.type !== ATOM && argument.type !== COMPOUND) {
1913
- throw new PrologError('type_error(callable)', culprit);
1910
+ function validateControlCallable(term, culprit, env) {
1911
+ // Only control constructs need their nested goals validated at meta-call
1912
+ // entry. Walk them iteratively and dereference each nested goal lazily so
1913
+ // passing a callable that contains a very deep data term (for example
1914
+ // phrase(a, List) with an 8k-cell List) never consumes the JavaScript stack.
1915
+ const pending = [term];
1916
+ while (pending.length > 0) {
1917
+ const current = deref(pending.pop(), env);
1918
+ if (current.type !== COMPOUND || ![',', ';', '->'].includes(current.name) || current.arity !== 2) continue;
1919
+ for (let index = current.arity - 1; index >= 0; index--) {
1920
+ const argument = deref(current.args[index], env);
1921
+ if (argument.type === VAR) throw new PrologError('instantiation_error');
1922
+ if (argument.type !== ATOM && argument.type !== COMPOUND) {
1923
+ throw new PrologError('type_error(callable)', culprit);
1924
+ }
1925
+ pending.push(argument);
1914
1926
  }
1915
- validateControlCallable(argument, culprit);
1916
1927
  }
1917
1928
  }
1918
- function resolveCallable(term, env) {
1919
- const resolved = deref(term, env);
1920
- if (resolved.type !== COMPOUND) return resolved;
1921
- const callable = compound(resolved.name, resolved.args.map((arg) => resolveCallable(arg, env)));
1922
- if (resolved.module != null) callable.module = resolved.module;
1923
- return callable;
1924
- }
1925
1929
  function* callBuiltin({ solver, goal, env }) {
1926
1930
  const child = solver.cloneForInnerGoal();
1927
1931
  try {
@@ -1972,6 +1976,44 @@ function* countAllBuiltin({ solver, goal, env }) {
1972
1976
  if (unify(goal.args[1], numberTerm(count), next)) yield next;
1973
1977
  }
1974
1978
 
1979
+ function monotonicMilliseconds() {
1980
+ return globalThis.performance?.now?.() ?? Date.now();
1981
+ }
1982
+
1983
+ function writeElapsedTime(solver, startedAt, inferences) {
1984
+ const stream = solver.io.resolve(solver.io.currentOutput);
1985
+ if (stream?.type !== 'text') throw new PrologError('permission_error(output, binary_stream)');
1986
+ const elapsedSeconds = Math.max(0, monotonicMilliseconds() - startedAt) / 1000;
1987
+ const mlips = elapsedSeconds > 0 ? inferences / elapsedSeconds / 1_000_000 : 0;
1988
+ solver.io.writeUnit(
1989
+ stream,
1990
+ `% Time elapsed ${elapsedSeconds.toFixed(3)}s, ${inferences} Inferences, ${mlips.toFixed(3)} MLips\n`,
1991
+ );
1992
+ }
1993
+
1994
+ function* timeBuiltin({ solver, goal, env }) {
1995
+ const invoked = callable(goal.args[0], env);
1996
+ const child = solver.cloneForInnerGoal();
1997
+ let startedAt = monotonicMilliseconds();
1998
+ let startedInferences = child.inferenceObservation.value;
1999
+ let yieldedAny = false;
2000
+ try {
2001
+ for (const answerEnv of child.solve([invoked], env, 0)) {
2002
+ writeElapsedTime(solver, startedAt, child.inferenceObservation.value - startedInferences);
2003
+ yieldedAny = true;
2004
+ yield answerEnv;
2005
+ // A resumed time/1 measures only the work required to reach the next
2006
+ // answer, so nondeterministic calls get one timing line per solution.
2007
+ startedAt = monotonicMilliseconds();
2008
+ startedInferences = child.inferenceObservation.value;
2009
+ }
2010
+ // A call that fails without producing an answer still reports the work.
2011
+ if (!yieldedAny) writeElapsedTime(solver, startedAt, child.inferenceObservation.value - startedInferences);
2012
+ } finally {
2013
+ solver.absorbStatsFrom(child);
2014
+ }
2015
+ }
2016
+
1975
2017
  function* callNthBuiltin({ solver, goal, env }) {
1976
2018
  const requestedTerm = deref(goal.args[1], env);
1977
2019
  // Zero is the one Nth value that fails before Goal is inspected.
@@ -7,17 +7,21 @@
7
7
  succ/2,
8
8
  cfor/3,
9
9
  findall/4,
10
- variant/2
10
+ variant/2,
11
+ time/1,
12
+ '...'/2
11
13
  ]).
12
14
 
13
15
  :- meta_predicate(call_nth(0, '?')).
14
16
  :- meta_predicate(countall(0, '?')).
15
17
  :- meta_predicate(forall(0, 0)).
16
18
  :- meta_predicate(findall('?', 0, '?', '?')).
19
+ :- meta_predicate(time(0)).
17
20
 
18
21
  % The organization and predicate contracts follow library(iso_ext) in
19
- % Trealla. These definitions use only EyeProlog's ISO profile; extensions that
20
- % require runtime cleanup, choice-point, alarm, or timeout hooks are omitted.
22
+ % Trealla. Most definitions use only EyeProlog's ISO profile. time/1 is the
23
+ % deliberate exception: its private adapter supplies monotonic host timing and
24
+ % writes the measurement while the public wrapper keeps normal meta semantics.
21
25
 
22
26
  call_nth(Goal, Nth) :- eyeprolog__call_nth(Goal, Nth).
23
27
 
@@ -59,6 +63,16 @@ variant(X, Y) :-
59
63
  subsumes_term(CopyX, CopyY),
60
64
  subsumes_term(CopyY, CopyX).
61
65
 
66
+ % Trealla-compatible timing wrapper. The private adapter measures the callable
67
+ % while this Prolog wrapper supplies normal module/meta-predicate semantics.
68
+ time(Goal) :- eyeprolog__time(Goal).
69
+
70
+ % Trealla/Scryer DCG helper: describes an arbitrary number of elements.
71
+ % EyeProlog's two-clause form has the same finite-input relation without the
72
+ % Trealla-specific empty-input cut guard, so recursive calls stay cut-free.
73
+ '...' --> [].
74
+ '...' --> [_], '...' .
75
+
62
76
  iso_ext__call_all([]).
63
77
  iso_ext__call_all([Goal|Goals]) :-
64
78
  call(Goal),
package/src/repl.js CHANGED
@@ -526,6 +526,7 @@ async function readSource(designation) {
526
526
  async function solveQuery(engine, state, goal, reader, output) {
527
527
  const variables = queryVariables(goal);
528
528
  const solver = state.solver;
529
+ const demandDriven = containsTimedGoal(goal);
529
530
  solver.solutionsSeen = 0;
530
531
  const solutions = solver.solve([goal], new engine.Env(), 0);
531
532
  let current = pullSolution(solver, solutions, reader);
@@ -544,11 +545,12 @@ async function solveQuery(engine, state, goal, reader, output) {
544
545
  let firstAnswer = true;
545
546
  let formattingAfterAdvance = false;
546
547
  while (!current.result.done) {
547
- const next = pullSolution(solver, solutions, reader);
548
- // The control prompt has no trailing space while it waits for input. The
549
- // first space appears as soon as the user requests another solution and
550
- // remains visible while pullSolution() computes; the second appears only
551
- // when the requested leaf answer is ready to format.
548
+ // Ordinary queries keep the existing eager look-ahead so deterministic
549
+ // answers can end with a full stop without showing an unnecessary answer
550
+ // prompt. `time/1` is different: running a future solution changes what is
551
+ // being measured and can retain a very large current substitution. Timed
552
+ // queries therefore advance only after the user asks for another answer.
553
+ const next = demandDriven ? null : pullSolution(solver, solutions, reader);
552
554
  if (formattingAfterAdvance) output.write(' ');
553
555
  formattingAfterAdvance = false;
554
556
  output.write(current.output);
@@ -556,10 +558,8 @@ async function solveQuery(engine, state, goal, reader, output) {
556
558
  output.write(`${firstAnswer ? ' ' : ''}${answer}`);
557
559
  answersShown++;
558
560
  firstAnswer = false;
559
- if (!next.error && next.result.done) {
560
- // A terminal full stop cannot immediately follow a graphic token: the
561
- // scanner would absorb it into that token. Insert layout so the printed
562
- // answer remains valid Prolog text (issue #44).
561
+
562
+ if (demandDriven ? !solver.hasPendingAlternatives() : (!next.error && next.result.done)) {
563
563
  output.write(`${continuesGraphicToken(answer, answer.length) ? ' ' : ''}.\n`);
564
564
  return null;
565
565
  }
@@ -584,9 +584,6 @@ async function solveQuery(engine, state, goal, reader, output) {
584
584
  break;
585
585
  }
586
586
  if (control === 'f') {
587
- // `f` groups leaf answers in blocks of five, rather than merely
588
- // adding five more answers after whatever the user has already
589
- // inspected. Stop for control again at the next 5-answer boundary.
590
587
  const remainder = answersShown % 5;
591
588
  const answersToBoundary = remainder === 0 ? 5 : 5 - remainder;
592
589
  automatic = answersToBoundary - 1;
@@ -606,6 +603,29 @@ async function solveQuery(engine, state, goal, reader, output) {
606
603
  formattingAfterAdvance = true;
607
604
  }
608
605
 
606
+ if (demandDriven) {
607
+ // The displayed timed answer is no longer needed. Drop it before
608
+ // resuming search so a large list substitution does not remain live only
609
+ // because the top level is looking for its successor.
610
+ current = null;
611
+ const requested = pullSolution(solver, solutions, reader);
612
+ if (requested.error) {
613
+ if (formattingAfterAdvance) output.write(' ');
614
+ formattingAfterAdvance = false;
615
+ output.write(requested.output);
616
+ if (requested.error?.name === 'HaltSignal') return { halted: true, code: requested.error.code };
617
+ throw requested.error;
618
+ }
619
+ if (requested.result.done) {
620
+ if (formattingAfterAdvance) output.write(' ');
621
+ formattingAfterAdvance = false;
622
+ output.write(`${requested.output}false.\n`);
623
+ return null;
624
+ }
625
+ current = requested;
626
+ continue;
627
+ }
628
+
609
629
  if (next.error) {
610
630
  if (formattingAfterAdvance) output.write(' ');
611
631
  formattingAfterAdvance = false;
@@ -618,6 +638,17 @@ async function solveQuery(engine, state, goal, reader, output) {
618
638
  return null;
619
639
  }
620
640
 
641
+ function containsTimedGoal(goal) {
642
+ const stack = [goal];
643
+ while (stack.length !== 0) {
644
+ const term = stack.pop();
645
+ if (term?.type !== 'compound') continue;
646
+ if (term.name === 'time' && term.arity === 1) return true;
647
+ for (let index = term.args.length - 1; index >= 0; index--) stack.push(term.args[index]);
648
+ }
649
+ return false;
650
+ }
651
+
621
652
  function pullSolution(solver, solutions, reader) {
622
653
  const stream = solver.io.resolve('user_output');
623
654
  const originalWrite = stream?.write;
package/src/solver.js CHANGED
@@ -58,6 +58,10 @@ export class Solver {
58
58
  this.depthLimitExceeded = false;
59
59
  this.maxInferences = options.maxInferences ?? Infinity;
60
60
  this.inferences = 0;
61
+ // Shared only for observability: nested meta-call solvers contribute to the
62
+ // same measurement counter without changing each solver's local inference
63
+ // limit accounting. time/1 snapshots this counter around the measured goal.
64
+ this.inferenceObservation = options.inferenceObservation ?? { value: 0 };
61
65
  this.inferenceLimitExceeded = false;
62
66
  this.maxMemoryBytes = options.maxMemoryBytes ?? softHeapLimit();
63
67
  this.memoryRecovery = options.memoryRecovery ?? {
@@ -158,6 +162,7 @@ export class Solver {
158
162
  charConversions: this.charConversions,
159
163
  io: this.io,
160
164
  innerTableScopes: this.innerTableScopes,
165
+ inferenceObservation: this.inferenceObservation,
161
166
  skipListTailTabling: options.skipListTailTabling ?? this.skipListTailTabling,
162
167
  });
163
168
  if (options.tableScope != null) {
@@ -282,6 +287,7 @@ export class Solver {
282
287
  this.solveStacks.push(stack);
283
288
  while (stack.length) {
284
289
  this.inferences++;
290
+ this.inferenceObservation.value++;
285
291
  this.checkMemoryLimit();
286
292
  if (this.inferences > this.maxInferences) {
287
293
  this.inferenceLimitExceeded = true;
@@ -334,6 +340,7 @@ export class Solver {
334
340
 
335
341
  while (true) {
336
342
  this.inferences++;
343
+ this.inferenceObservation.value++;
337
344
  this.checkMemoryLimit();
338
345
  if (this.inferences > this.maxInferences) {
339
346
  this.inferenceLimitExceeded = true;
@@ -388,6 +395,7 @@ export class Solver {
388
395
  const selectedIndex = selectReadyDeterministicBuiltin(goals, env, this.registry);
389
396
  const goal = deref(goals[selectedIndex], env);
390
397
  const rest = selectedIndex === 0 ? goals.slice(1) : [...goals.slice(0, selectedIndex), ...goals.slice(selectedIndex + 1)];
398
+ prepareLocalVariablesForGoal(goal, env);
391
399
  if (goal.type === 'atom' && goal.name === '!' && goal.arity === 0) {
392
400
  const marker = active[active.length - 1] ?? null;
393
401
  if (marker) marker.cutEpoch = (marker.cutEpoch ?? 0) + 1;
@@ -539,6 +547,23 @@ export class Solver {
539
547
  continue;
540
548
  }
541
549
 
550
+ const ellipsisPlan = bundledEllipsisPlan(this, group, goal, rest, env);
551
+ if (ellipsisPlan != null) {
552
+ const firstResult = ellipsisPlan.iterator.next();
553
+ if (firstResult.done) break;
554
+ stack.push({
555
+ kind: 'resumeBuiltin',
556
+ iterator: ellipsisPlan.iterator,
557
+ goals: ellipsisPlan.rest,
558
+ depth: depth + 1,
559
+ active,
560
+ });
561
+ goals = ellipsisPlan.rest;
562
+ env = firstResult.value;
563
+ depth++;
564
+ continue;
565
+ }
566
+
542
567
  if (group.tabled && !(this.skipListTailTabling && group.listTailRecursive)) {
543
568
  const key = memoKey(goal, env, group);
544
569
  if (key.hasBound) {
@@ -612,6 +637,13 @@ export class Solver {
612
637
  }
613
638
  }
614
639
 
640
+ hasPendingAlternatives() {
641
+ // When solve() is suspended at an answer, active solve stacks contain only
642
+ // unexplored work. The timed REPL path uses this without speculatively
643
+ // pulling the next answer.
644
+ return this.solveStacks.some((stack) => stack.length !== 0);
645
+ }
646
+
615
647
  fastCountGoal(goal, env) {
616
648
  return fastCountPureGoal(this, goal, env);
617
649
  }
@@ -1083,12 +1115,47 @@ function freshVariableSet(names, freshVariables) {
1083
1115
 
1084
1116
  function attachBodyLocalFreshVariables(freshBody, plan, freshVariables) {
1085
1117
  for (let index = 0; index < freshBody.length; index++) {
1118
+ const goal = freshBody[index];
1086
1119
  const knownNonoccurringVariables = freshVariableSet(plan[index] ?? [], freshVariables);
1087
- if (knownNonoccurringVariables != null && freshBody[index]?.type === COMPOUND &&
1088
- freshBody[index].name === '=' && freshBody[index].arity === 2) {
1089
- freshBody[index]._knownNonoccurringVariables = knownNonoccurringVariables;
1120
+ if (knownNonoccurringVariables == null || goal?.type !== COMPOUND) continue;
1121
+ if (goal.name === '=' && goal.arity === 2) {
1122
+ goal._knownNonoccurringVariables = knownNonoccurringVariables;
1123
+ continue;
1124
+ }
1125
+ // Only a compiler-generated DCG state first handed as a complete argument
1126
+ // to another callable receives a longer-lived local marker. Equality-created
1127
+ // sequence variables keep the existing one-unification proof instead.
1128
+ const localFirstUseVariables = new Set();
1129
+ for (const argument of goal.args) {
1130
+ if (argument?.type === VAR && argument.name.startsWith('\u0000dcg') &&
1131
+ knownNonoccurringVariables.has(argument.name)) {
1132
+ localFirstUseVariables.add(argument.name);
1133
+ }
1134
+ }
1135
+ if (localFirstUseVariables.size !== 0) goal._localFirstUseVariables = localFirstUseVariables;
1136
+ }
1137
+ }
1138
+
1139
+
1140
+ function prepareLocalVariablesForGoal(goal, env) {
1141
+ // A DCG local is globalized if later source places it inside a structure.
1142
+ // Inspect the small fresh goal syntax rather than dereferencing large data.
1143
+ if (env.hasLocalVariables() && goal?.type === COMPOUND) {
1144
+ const pending = [];
1145
+ for (const argument of goal.args) if (argument?.type === COMPOUND) pending.push(argument);
1146
+ while (pending.length !== 0) {
1147
+ const current = pending.pop();
1148
+ for (const argument of current.args ?? []) {
1149
+ if (argument?.type === VAR) {
1150
+ const root = derefForLocal(argument, env);
1151
+ if (root.type === VAR && env.isLocalVariable(root.name)) env.demoteLocalVariable(root.name);
1152
+ } else if (argument?.type === COMPOUND) {
1153
+ pending.push(argument);
1154
+ }
1155
+ }
1090
1156
  }
1091
1157
  }
1158
+ env.markLocalVariables(goal?._localFirstUseVariables ?? null);
1092
1159
  }
1093
1160
 
1094
1161
  function groupNeedsActiveFrame(group) {
@@ -1176,6 +1243,92 @@ function* bundledMemberSolutions(solver, goal, env) {
1176
1243
  }
1177
1244
  }
1178
1245
 
1246
+ function bundledEllipsisPlan(solver, group, goal, rest, env) {
1247
+ if (solver.registry.eyePrologLibrary !== true ||
1248
+ group.module !== 'iso_ext' || group.name !== '...' || group.arity !== 2 ||
1249
+ group.bundledLibrary !== true || group.clauses.length !== 2) {
1250
+ return null;
1251
+ }
1252
+
1253
+ // length/2 and other native constructors can leave a known finite list as a
1254
+ // compact spine. The ordinary ...//0 relation simply enumerates every
1255
+ // suffix of such a list; doing that directly avoids clause freshening and
1256
+ // recursive solver depth for every consumed element. Non-compact and open
1257
+ // list cases retain the ordinary Prolog definition.
1258
+ const input = deref(goal.args[0], env);
1259
+ if (compactListLength(input) == null) return null;
1260
+
1261
+ // A following binary identity relation is just a zero-width DCG hand-off.
1262
+ // Fuse it by asking .../2 for that relation's required output directly.
1263
+ // This is a structural optimization: any one-clause p(X,Y):-X=Y (or p(X,X).)
1264
+ // qualifies, not just a predicate named epsilon/2.
1265
+ if (rest.length > 0) {
1266
+ const continuation = deref(rest[0], env);
1267
+ if (continuation?.type === COMPOUND && continuation.arity === 2) {
1268
+ const continuationGroup = solver.program.findGroup(
1269
+ continuation.name, continuation.arity, continuation.module ?? goal.module ?? 'user',
1270
+ );
1271
+ if (isBinaryIdentityGroup(continuationGroup)) {
1272
+ const output = deref(goal.args[1], env);
1273
+ const continuationInput = deref(continuation.args[0], env);
1274
+ if (output.type === VAR && continuationInput.type === VAR && output.name === continuationInput.name &&
1275
+ output.name.startsWith('\u0000dcg')) {
1276
+ return {
1277
+ iterator: bundledEllipsisSolutions(solver, continuation.args[1], input, env),
1278
+ rest: rest.slice(1),
1279
+ };
1280
+ }
1281
+ }
1282
+ }
1283
+ }
1284
+
1285
+ return { iterator: bundledEllipsisSolutions(solver, goal.args[1], input, env), rest };
1286
+ }
1287
+
1288
+ function isBinaryIdentityGroup(group) {
1289
+ if (group == null || group.arity !== 2 || group.clauses.length !== 1 || group.hasCut === true) return false;
1290
+ const clause = group.clauses[0];
1291
+ const left = clause.head?.args?.[0];
1292
+ const right = clause.head?.args?.[1];
1293
+ if (left?.type !== VAR || right?.type !== VAR) return false;
1294
+ if (clause.body.length === 0) return left.name === right.name;
1295
+ if (clause.body.length !== 1) return false;
1296
+ const equality = clause.body[0];
1297
+ if (equality?.type !== COMPOUND || equality.name !== '=' || equality.arity !== 2) return false;
1298
+ const a = equality.args[0];
1299
+ const b = equality.args[1];
1300
+ if (a?.type !== VAR || b?.type !== VAR) return false;
1301
+ return (a.name === left.name && b.name === right.name) ||
1302
+ (a.name === right.name && b.name === left.name);
1303
+ }
1304
+
1305
+ function* bundledEllipsisSolutions(solver, output, input, env) {
1306
+ let cursor = input;
1307
+ const requestedOutput = deref(output, env);
1308
+
1309
+ // A fixed empty remainder is the important DCG scanner case. Still walk the
1310
+ // actual compact spine so this remains a real tail-consumption benchmark,
1311
+ // but avoid allocating a speculative environment for every suffix that is
1312
+ // structurally incapable of matching [].
1313
+ if (isEmptyList(requestedOutput)) {
1314
+ while (!isEmptyList(cursor)) {
1315
+ if (!isCons(cursor)) return;
1316
+ cursor = deref(cursor.args[1], env);
1317
+ }
1318
+ yield env;
1319
+ return;
1320
+ }
1321
+
1322
+ while (true) {
1323
+ const next = env.clone();
1324
+ solver.stats.unify_calls++;
1325
+ if (unify(output, cursor, next)) yield next;
1326
+ if (isEmptyList(cursor)) return;
1327
+ if (!isCons(cursor)) return;
1328
+ cursor = deref(cursor.args[1], env);
1329
+ }
1330
+ }
1331
+
1179
1332
  function bundledLengthIterator(solver, group, goal, env) {
1180
1333
  if (solver.registry.eyePrologLibrary !== true ||
1181
1334
  !['lists', 'prologue'].includes(group.module) || group.name !== 'length' || group.arity !== 2 ||
@@ -47,7 +47,7 @@ function libraryUrl(filename) {
47
47
  }
48
48
 
49
49
  export const eyePrologNativeLibraryIndicators = Object.freeze([
50
- 'call_nth/2', 'freeze/2', 'countall/2',
50
+ 'call_nth/2', 'freeze/2', 'countall/2', 'time/1',
51
51
  '#>/2', '#</2', '#>=/2', '#=</2', '#=/2', '#\\=/2', '#\\/1',
52
52
  '#<==>/2', '#==>/2', '#<==/2', '#\\//2', '#\\/2', '#/\\/2',
53
53
  'in/2', 'ins/2', 'all_different/1', 'all_distinct/1', 'nvalue/2', 'sum/3',
@@ -68,7 +68,7 @@ export const eyePrologPortableLibraryIndicators = Object.freeze([
68
68
  'nth0/3', 'nth0/4', 'nth1/3', 'nth1/4', 'set_nth0/4', 'take/3', 'drop/3', 'slice/4', 'reverse/2',
69
69
  'length/2', 'sum_list/2', 'min_list/2', 'max_list/2', 'list_to_set/2',
70
70
  'succ/2', 'foldl/4', 'foldl/5', 'foldl/6',
71
- 'forall/2', 'cfor/3', 'findall/4', 'variant/2', 'uuid/3',
71
+ 'forall/2', 'cfor/3', 'findall/4', 'variant/2', '.../2', 'uuid/3',
72
72
  '^/3', '^/4', '^/5', '^/6', '^/7', '^/8', '^/9', '^/10',
73
73
  '\\/1', '\\/2', '\\/3', '\\/4', '\\/5', '\\/6', '\\/7', '\\/8',
74
74
  '+\\/2', '+\\/3', '+\\/4', '+\\/5', '+\\/6', '+\\/7', '+\\/8', '+\\/9',
@@ -117,6 +117,11 @@ export const eyePrologInteropAutoload = Object.freeze({
117
117
  // it from library(iso_ext), matching the explicit Scryer import while still
118
118
  // allowing Trealla-style unqualified source to use the same autoload entry.
119
119
  'call_nth/2': 'iso_ext',
120
+ // Trealla exposes time/1 as a meta timing predicate and library(iso_ext)
121
+ // supplies ...//0. Autoload both so UWN's DCG hand-off benchmark runs
122
+ // unchanged while their implementations remain outside the ISO core.
123
+ 'time/1': 'iso_ext',
124
+ '.../2': 'iso_ext',
120
125
  // Trealla and Scryer expose between/3 without an EyeProlog-style
121
126
  // library(prologue) dependency. EyeProlog keeps its implementation in the
122
127
  // Prologue module but autoloads it so portable source need not name that
package/src/term.js CHANGED
@@ -99,6 +99,7 @@ export class Env {
99
99
  this._delays = null;
100
100
  this._clpz = null;
101
101
  this._occursCheckHandler = null;
102
+ this._localVariables = null;
102
103
  }
103
104
  clone() {
104
105
  // Most speculative environments are either rejected without a binding or
@@ -111,12 +112,43 @@ export class Env {
111
112
  clone._delays = this._delays;
112
113
  clone._clpz = this._clpz;
113
114
  clone._occursCheckHandler = this._occursCheckHandler;
115
+ clone._localVariables = this._localVariables;
114
116
  return clone;
115
117
  }
116
118
  setOccursCheckHandler(handler) {
117
119
  this._occursCheckHandler = typeof handler === 'function' ? handler : null;
118
120
  return this;
119
121
  }
122
+ hasLocalVariables() {
123
+ return this._localVariables != null && this._localVariables.size !== 0;
124
+ }
125
+ isLocalVariable(name) {
126
+ return this._localVariables?.has(name) === true;
127
+ }
128
+ markLocalVariables(names) {
129
+ if (names == null || names.size === 0) return;
130
+ let next = this._localVariables;
131
+ for (const name of names) {
132
+ const root = deref(variable(name), this);
133
+ if (root.type !== VAR || next?.has(root.name)) continue;
134
+ if (next === this._localVariables) next = new Set(this._localVariables ?? []);
135
+ next.add(root.name);
136
+ }
137
+ this._localVariables = next;
138
+ }
139
+ demoteLocalVariable(name) {
140
+ const root = deref(variable(name), this);
141
+ if (root.type !== VAR || this._localVariables?.has(root.name) !== true) return;
142
+ const next = new Set(this._localVariables);
143
+ next.delete(root.name);
144
+ this._localVariables = next.size === 0 ? null : next;
145
+ }
146
+ forgetLocalVariable(name) {
147
+ if (this._localVariables?.has(name) !== true) return;
148
+ const next = new Set(this._localVariables);
149
+ next.delete(name);
150
+ this._localVariables = next.size === 0 ? null : next;
151
+ }
120
152
  has(name) {
121
153
  return this.get(name) !== undefined;
122
154
  }
@@ -212,6 +244,9 @@ export function deref(term, env) {
212
244
  let current = term;
213
245
  let seen = null;
214
246
  while (current?.type === VAR) {
247
+ // A live compiler-proven DCG local is the current unbound representative.
248
+ // No older Env layer can contain a binding for it.
249
+ if (env?.isLocalVariable?.(current.name) === true) break;
215
250
  const next = env?.get(current.name);
216
251
  if (next === undefined) break;
217
252
  if (seen?.has(current.name)) break;
@@ -284,28 +319,40 @@ export function unify(left, right, env, options = {}) {
284
319
 
285
320
  if (a.type === VAR && b.type === VAR && a.name === b.name) continue;
286
321
  if (a.type === VAR && b.type === VAR) {
287
- // Both variables are already dereferenced and unbound, so linking them
288
- // cannot create a cycle and needs no occurs-check traversal.
289
- markCompactVariableBound(a);
290
- env.bind(a.name, b);
322
+ // For a compiler-generated DCG state handed directly to another
323
+ // nonterminal, keep the local caller variable as representative. Ordinary
324
+ // aliases retain the established direction and observable conventions.
325
+ const aLocalDcg = env?.isLocalVariable(a.name) === true &&
326
+ a.name.startsWith('\u0000dcg') && b.name.startsWith('\u0000dcg');
327
+ if (aLocalDcg) {
328
+ markCompactVariableBound(b);
329
+ env.bind(b.name, a);
330
+ } else {
331
+ markCompactVariableBound(a);
332
+ env.bind(a.name, b);
333
+ }
291
334
  continue;
292
335
  }
293
336
  if (a.type === VAR) {
294
- if (!knownNonoccurringVariables?.has(a.name) && occurs(a.name, b, env)) {
337
+ const aLocal = env?.isLocalVariable(a.name) === true;
338
+ if (!aLocal && !knownNonoccurringVariables?.has(a.name) && occurs(a.name, b, env)) {
295
339
  occursCheckHandler?.(a, b, env);
296
340
  return false;
297
341
  }
298
342
  markCompactVariableBound(a);
299
343
  env.bind(a.name, b);
344
+ if (aLocal) env.forgetLocalVariable(a.name);
300
345
  continue;
301
346
  }
302
347
  if (b.type === VAR) {
303
- if (!knownNonoccurringVariables?.has(b.name) && occurs(b.name, a, env)) {
348
+ const bLocal = env?.isLocalVariable(b.name) === true;
349
+ if (!bLocal && !knownNonoccurringVariables?.has(b.name) && occurs(b.name, a, env)) {
304
350
  occursCheckHandler?.(b, a, env);
305
351
  return false;
306
352
  }
307
353
  markCompactVariableBound(b);
308
354
  env.bind(b.name, a);
355
+ if (bLocal) env.forgetLocalVariable(b.name);
309
356
  continue;
310
357
  }
311
358
 
@@ -1324,6 +1324,9 @@ c4 ?- call((!;1)).
1324
1324
  first_use_cycle :- X = f(Y), Y = g(X).
1325
1325
  repeated_cycle :- X = f(X).
1326
1326
  first_use_ok(T) :- X = f(Y), Y = a, T = X.
1327
+ pass(_).
1328
+ handed_off_cycle :- pass(X), Y = f(X), X = g(Y).
1329
+ handed_off_alias_cycle :- pass(X), Y = X, X = f(Y).
1327
1330
  `);
1328
1331
  const solver = new Solver(program);
1329
1332
  const solveCount = (text) => {
@@ -1338,6 +1341,8 @@ c4 ?- call((!;1)).
1338
1341
  assertEqual(solveCount('first_use_cycle'), 0, 'cycle across later first-use binding');
1339
1342
  assertEqual(solveCount('repeated_cycle'), 0, 'same-goal repeated variable still checks occurs');
1340
1343
  assertEqual(solveCount('first_use_ok(f(a))'), 1, 'acyclic first-use bindings still succeed');
1344
+ assertEqual(solveCount('handed_off_cycle'), 0, 'nested use globalizes a handed-off local before a cycle');
1345
+ assertEqual(solveCount('handed_off_alias_cycle'), 0, 'aliasing does not hide a later cycle');
1341
1346
  },
1342
1347
  },
1343
1348
  {
@@ -1379,6 +1384,106 @@ c4 ?- call((!;1)).
1379
1384
  assertEqual(result.stdout, 'ok', 'deep DCG result');
1380
1385
  },
1381
1386
  },
1387
+ {
1388
+ name: 'Trealla-style DCG hand-off autoloads time/1 and ...//0 without quadratic occurs checks (issue #49)',
1389
+ run: () => {
1390
+ const engineUrl = new URL('../src/index.js', import.meta.url).href;
1391
+ const script = `
1392
+ import { run } from ${JSON.stringify(engineUrl)};
1393
+ const source = ${JSON.stringify('a --> ..., epsilon.\nepsilon --> [].\n')};
1394
+ const result = run(source, {
1395
+ goal: ${JSON.stringify('length(_,E), E>12, N is 2^E, \\+ \\+ (length(L,N), time(phrase(a,L)))')},
1396
+ solutionLimit: 1,
1397
+ });
1398
+ if (!result.stdout.startsWith('% Time elapsed ') || !result.stdout.endsWith('s\\n')) {
1399
+ throw new Error('unexpected time/1 output: ' + JSON.stringify(result.stdout));
1400
+ }
1401
+ process.stdout.write('ok');
1402
+ `;
1403
+ const result = spawnSync(process.execPath, ['--input-type=module', '--eval', script], {
1404
+ cwd: packageRoot,
1405
+ encoding: 'utf8',
1406
+ timeout: 10000,
1407
+ });
1408
+ if (result.error) throw result.error;
1409
+ assertEqual(result.status, 0, `DCG hand-off child status; stderr=${result.stderr}`);
1410
+ assertEqual(result.stdout, 'ok', 'DCG hand-off benchmark result');
1411
+ },
1412
+ },
1413
+ {
1414
+ name: 'Trealla-style DCG hand-off reaches 65536 cells without the solver depth ceiling',
1415
+ run: () => {
1416
+ const engineUrl = new URL('../src/index.js', import.meta.url).href;
1417
+ const script = `
1418
+ import { run } from ${JSON.stringify(engineUrl)};
1419
+ const source = ${JSON.stringify(':- set_prolog_flag(occurs_check, true).\na --> ..., epsilon.\nepsilon --> [].\n')};
1420
+ const result = run(source, {
1421
+ goal: ${JSON.stringify('\\+ \\+ (length(L,65536), time(phrase(a,L)))')},
1422
+ solutionLimit: 1,
1423
+ });
1424
+ if (!result.stdout.startsWith('% Time elapsed ')) {
1425
+ throw new Error('65536-cell hand-off did not succeed: ' + JSON.stringify(result.stdout));
1426
+ }
1427
+ process.stdout.write('ok');
1428
+ `;
1429
+ const result = spawnSync(process.execPath, ['--input-type=module', '--eval', script], {
1430
+ cwd: packageRoot,
1431
+ encoding: 'utf8',
1432
+ timeout: 5000,
1433
+ });
1434
+ if (result.error) throw result.error;
1435
+ assertEqual(result.status, 0, `65536-cell hand-off status; stderr=${result.stderr}`);
1436
+ assertEqual(result.stdout, 'ok', '65536-cell hand-off result');
1437
+ },
1438
+ },
1439
+ {
1440
+ name: 'REPL applies conservative autoloading to interactive time/1 and consulted ...//0',
1441
+ run: () => {
1442
+ const filename = path.join(tmp, `issue49-handoff-${tmpCounter++}.pl`);
1443
+ fs.writeFileSync(filename, 'a --> ..., epsilon.\nepsilon --> [].\n');
1444
+ const result = runCli([], {
1445
+ input:
1446
+ `[${sourceAtom(filename)}].\n` +
1447
+ 'use_module(library(lists)).\n' +
1448
+ 'length(_,E),E>12,N is 2^E,\\+ \\+ (length(L,N),time(phrase(a,L))).\n' +
1449
+ '\n' +
1450
+ 'halt.\n',
1451
+ timeout: 10000,
1452
+ });
1453
+ if (result.error) throw result.error;
1454
+ assertEqual(result.status, 0, `REPL hand-off status; stderr=${result.stderr}`);
1455
+ assertIncludes(result.stdout, '% Time elapsed ', 'REPL time/1 output');
1456
+ assertEqual((result.stdout.match(/% Time elapsed /g) ?? []).length, 1,
1457
+ 'REPL timed query does not prefetch an unrequested next answer');
1458
+ assertIncludes(result.stdout, 'E = 13, N = 8192', 'REPL first benchmark answer');
1459
+ assertNotIncludes(result.stdout, 'existence_error(procedure', 'REPL autoload errors');
1460
+ assertEqual(result.stderr, '', 'REPL hand-off stderr');
1461
+ },
1462
+ },
1463
+ {
1464
+ name: 'REPL Trealla hand-off benchmark reaches E=16 on demand without OOM fallthrough',
1465
+ run: () => {
1466
+ const filename = path.join(tmp, `issue49-handoff-deep-${tmpCounter++}.pl`);
1467
+ fs.writeFileSync(filename, ':- set_prolog_flag(occurs_check, true).\na --> ..., epsilon.\nepsilon --> [].\n');
1468
+ const result = runCli([], {
1469
+ input:
1470
+ `[${sourceAtom(filename)}].\n` +
1471
+ 'use_module(library(lists)).\n' +
1472
+ 'length(_,E),E>12,N is 2^E,\\+ \\+ (length(L,N),time(phrase(a,L))).\n' +
1473
+ ';\n;\n;\n\n' +
1474
+ 'halt.\n',
1475
+ timeout: 5000,
1476
+ });
1477
+ if (result.error) throw result.error;
1478
+ assertEqual(result.status, 0, `deep REPL hand-off status; stderr=${result.stderr}`);
1479
+ for (const [e, n] of [[13, 8192], [14, 16384], [15, 32768], [16, 65536]]) {
1480
+ assertIncludes(result.stdout, `E = ${e}, N = ${n}`, `REPL hand-off E=${e}`);
1481
+ }
1482
+ assertEqual((result.stdout.match(/% Time elapsed /g) ?? []).length, 4,
1483
+ 'exactly four requested timed answers');
1484
+ assertEqual(result.stderr, '', 'deep REPL hand-off stderr');
1485
+ },
1486
+ },
1382
1487
  {
1383
1488
  name: 'REPL enumerates and stops answers like the Scryer top level',
1384
1489
  run: () => {
@@ -3916,7 +4021,7 @@ answer(ok) :-
3916
4021
  assertEqual(Boolean(registry.get('is', 2)), true, 'ISO is/2 exists');
3917
4022
  assertEqual(Boolean(registry.get('append', 3)), false, 'append/3 is not ISO core');
3918
4023
  assertEqual(library.eyePrologLibrary, true, 'complete registry marker');
3919
- assertEqual(library.defs.size, 156, 'EyeProlog registry contains ISO definitions, observability extensions, WFS tnot/1, and private library adapters');
4024
+ assertEqual(library.defs.size, 158, 'EyeProlog registry contains ISO definitions, observability extensions, WFS tnot/1, and private library adapters');
3920
4025
  assertEqual(Boolean(registry.get('phrase', 2)), true, 'Part 3 phrase/2 exists');
3921
4026
  assertEqual(Boolean(registry.get('phrase', 3)), true, 'Part 3 phrase/3 exists');
3922
4027
  assertEqual(registry.get('statistics', 0), null, 'statistics/0 is absent from the ISO registry');
@@ -3925,17 +4030,21 @@ answer(ok) :-
3925
4030
  assertEqual(Boolean(library.get('statistics', 2)), true, 'statistics/2 is an EyeProlog observability extension');
3926
4031
  assertEqual(registry.get('tnot', 1), null, 'tnot/1 is absent from the ISO registry');
3927
4032
  assertEqual(Boolean(library.get('tnot', 1)), true, 'tnot/1 is an EyeProlog WFS extension');
3928
- assertEqual(registeredNativeEyePrologLibraryNames().length, 40, 'public native EyeProlog builtin count');
3929
- assertEqual(eyePrologPortableLibraryIndicators.length, 86, 'portable Prolog library count');
3930
- assertEqual(eyePrologInteropLibraryIndicators.length, 27, 'cross-implementation interop profile count');
4033
+ assertEqual(registry.get('time', 1), null, 'time/1 is absent from the ISO registry');
4034
+ assertEqual(Boolean(library.get('time', 1)), true, 'time/1 is an EyeProlog timing extension');
4035
+ assertEqual(registeredNativeEyePrologLibraryNames().length, 41, 'public native EyeProlog builtin count');
4036
+ assertEqual(eyePrologPortableLibraryIndicators.length, 87, 'portable Prolog library count');
4037
+ assertEqual(eyePrologInteropLibraryIndicators.length, 29, 'cross-implementation interop profile count');
3931
4038
  assertEqual(eyePrologInteropLibraryModules.join(','), 'lists,iso_ext,lambda', 'common explicit library module profile');
3932
4039
  assertEqual(eyePrologInteropAutoload['member/2'], 'lists', 'member/2 canonical autoload');
3933
4040
  assertEqual(eyePrologInteropAutoload['between/3'], 'prologue', 'between/3 canonical internal autoload');
3934
4041
  assertEqual(eyePrologInteropAutoload['call_nth/2'], 'iso_ext', 'call_nth/2 canonical interop autoload');
4042
+ assertEqual(eyePrologInteropAutoload['time/1'], 'iso_ext', 'time/1 canonical interop autoload');
4043
+ assertEqual(eyePrologInteropAutoload['.../2'], 'iso_ext', '.../2 canonical interop autoload');
3935
4044
  assertEqual(eyePrologInteropAutoload['set_nth0/4'] ?? null, null, 'EyeProlog-only set_nth0/4 is not autoloadable');
3936
- assertEqual(eyePrologNativeLibraryIndicators.length, 40, 'native host library count');
4045
+ assertEqual(eyePrologNativeLibraryIndicators.length, 41, 'native host library count');
3937
4046
  assertEqual(eyePrologNativeLibraryIndicators.slice(0, 2).join(','), 'call_nth/2,freeze/2', 'control predicates requiring host support');
3938
- assertEqual(eyePrologLibraryIndicators.length, 126, 'complete EyeProlog library surface');
4047
+ assertEqual(eyePrologLibraryIndicators.length, 128, 'complete EyeProlog library surface');
3939
4048
  assertEqual(registry.get('eyeprolog__call_nth', 2), null, 'private call_nth adapter is absent from ISO registry');
3940
4049
  assertEqual(Boolean(library.get('eyeprolog__call_nth', 2)), true, 'private call_nth adapter is registered for EyeProlog');
3941
4050
  assertEqual(library.get('eyeprolog__call_nth', 2)?.eyePrologLibrary, true, 'private adapter is marked as library support');
@@ -3943,6 +4052,7 @@ answer(ok) :-
3943
4052
  assertEqual(Boolean(library.get('eyeprolog__freeze', 2)), true, 'private freeze adapter is registered for EyeProlog');
3944
4053
  assertEqual(registry.get('eyeprolog__countall', 2), null, 'private countall adapter is absent from ISO registry');
3945
4054
  assertEqual(Boolean(library.get('eyeprolog__countall', 2)), true, 'private countall adapter is registered for EyeProlog');
4055
+ assertEqual(Boolean(library.get('eyeprolog__time', 1)), true, 'private time adapter is registered for EyeProlog');
3946
4056
  assertEqual(Boolean(library.get('eyeprolog__clpz_labeling', 2)), true, 'private CLP(Z) labeling adapter is registered');
3947
4057
  assertEqual(Boolean(library.get('eyeprolog__clpz_global_cardinality', 3)), true, 'private CLP(Z) cardinality adapter is registered');
3948
4058
  assertEqual(library.get('between', 3), null, 'between/3 remains portable Prolog');
@@ -6067,14 +6067,16 @@ so side effects occur in Prolog execution order.
6067
6067
 
6068
6068
  ### The EyeProlog library
6069
6069
 
6070
- EyeProlog exposes **126 library predicate indicators** in addition to the 129
6071
- indicators in its isolated ISO profile. **87 are defined as ordinary Prolog
6072
- clauses** in focused modules under `src/lib/`. The remaining 39
6073
- are public wrappers around backtrackable host support: Prologue `call_nth/2` and
6074
- `freeze/2`, plus the 37-predicate finite-domain `library(clpz)` kernel. The
6075
- resulting normal EyeProlog language surface is therefore **255 public predicate
6076
- indicators**. Internally, the runtime registry contains the 129 ISO definitions
6077
- plus 23 private library adapters; public relations remain module source clauses.
6070
+ EyeProlog exposes **128 library predicate indicators** in addition to the 129
6071
+ indicators in its isolated ISO profile. **88 are defined entirely as ordinary
6072
+ Prolog clauses** in focused modules under `src/lib/`. The remaining public
6073
+ relations use small private host adapters where control, constraints, or host
6074
+ observability cannot be expressed by ordinary clauses alone; `time/1` is one
6075
+ such relation. The resulting normal EyeProlog language surface is therefore
6076
+ **257 public predicate indicators**. Most library relations remain module source
6077
+ clauses over private adapters. `time/1` is additionally registered directly in
6078
+ the normal EyeProlog runtime so Trealla-style timing works at the interactive
6079
+ top level without an import; it is absent from the strict ISO registry.
6078
6080
 
6079
6081
  The sources are `src/lib/aggregate.pl`, `src/lib/clpz.pl`, `src/lib/comparison.pl`,
6080
6082
  `src/lib/dates.pl`, `src/lib/iso_ext.pl`, `src/lib/lambda.pl`,
@@ -6110,7 +6112,7 @@ between solution branches.
6110
6112
  | `library(clpz)` | `#>/2`, `#</2`, `#>=/2`, `#=</2`, `#=/2`, `#\=/2`, `#\/1`, `#<==>/2`, `#==>/2`, `#<==/2`, `#\//2`, `#\/2`, `#/\/2`, `in/2`, `ins/2`, `all_different/1`, `all_distinct/1`, `nvalue/2`, `sum/3`, `scalar_product/4`, `tuples_in/2`, `labeling/2`, `label/1`, `indomain/1`, `lex_chain/1`, `serialized/2`, `global_cardinality/2`, `global_cardinality/3`, `circuit/1`, `chain/2`, `element/3`, `zcompare/3`, `fd_var/1`, `fd_inf/2`, `fd_sup/2`, `fd_size/2`, `fd_dom/2` |
6111
6113
  | `library(comparison)` | `lt/2`, `gt/2`, `le/2`, `ge/2` |
6112
6114
  | `library(dates)` | `difference/3` |
6113
- | `library(iso_ext)` | `call_nth/2`, `countall/2`, `forall/2`, `succ/2`, `cfor/3`, `findall/4`, `variant/2` |
6115
+ | `library(iso_ext)` | `call_nth/2`, `countall/2`, `forall/2`, `succ/2`, `cfor/3`, `findall/4`, `variant/2`, `time/1`, `.../2` |
6114
6116
  | `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` |
6115
6117
  | `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` |
6116
6118
  | `library(primes)` | `smallest_divisor_from/3` |
@@ -6144,7 +6146,7 @@ The current interoperability profile recognizes these library roles:
6144
6146
  | Library | Role in the interoperability profile |
6145
6147
  | --- | --- |
6146
6148
  | `library(lists)` | Common list module. A conservative subset of its exports is in the shared predicate profile. |
6147
- | `library(iso_ext)` | Common extension-module name. `call_nth/2` is currently its autoloaded cross-engine predicate. |
6149
+ | `library(iso_ext)` | Common extension-module name. `call_nth/2`, `time/1`, and the `...//0` arbitrary-sequence helper are conservatively autoloaded for cross-engine source. |
6148
6150
  | `library(lambda)` | Scryer-aligned higher-order notation. It is imported explicitly because loading it also installs the `+\` operator. |
6149
6151
  | `library(prologue)` | EyeProlog compatibility module, not a common interop library name. `between/3` is nevertheless autoloaded from it so portable source need not name this EyeProlog-specific provider. |
6150
6152
 
@@ -6168,9 +6170,14 @@ recovery headroom so finite-heap exhaustion remains a catchable
6168
6170
  `resource_error(memory)`.
6169
6171
 
6170
6172
  `library(iso_ext)` is a common interop module name, but only part of its
6171
- EyeProlog API belongs to the shared profile. `call_nth/2` is mapped there to
6172
- match Scryer's explicit import organization while also permitting portable
6173
- unqualified source to autoload it. The interop exports of `library(lists)` and
6173
+ EyeProlog API belongs to the shared profile. `call_nth/2`, `time/1`, and
6174
+ `.../2` are mapped there. `time/1` measures each solution of a meta-call and
6175
+ prints elapsed time, EyeProlog inference count, and MLips in Trealla-style form,
6176
+ for example `% Time elapsed 0.832s, 65551 Inferences, 0.079 MLips`; `...//0`
6177
+ describes an arbitrary number of input elements. Together they let the
6178
+ Trealla/Scryer DCG hand-off benchmark run in EyeProlog without source changes
6179
+ (assuming the usual list library is already imported in an interactive
6180
+ session). The interop exports of `library(lists)` and
6174
6181
  `library(iso_ext)` are kept disjoint, so both modules can be imported together
6175
6182
  without an accidental collision. `library(prologue)` remains a compatibility
6176
6183
  umbrella and overlaps them; use selective imports when legacy code combines it
@@ -6206,14 +6213,19 @@ uses its ISO `copy_term/2` implementation for the fresh-copy step and does not
6206
6213
  require a separate `copy_term_nat/2` predicate.
6207
6214
 
6208
6215
  Autoloading is a convenience layered on top of the interoperability profile; it
6209
- is not a general search through all EyeProlog libraries. During normal
6210
- execution, an otherwise undefined **unqualified** predicate may be autoloaded
6211
- only when the interop table assigns it one canonical provider. For example:
6216
+ is not a general search through all EyeProlog libraries. When a source program
6217
+ or an explicit CLI/API goal is built, an otherwise undefined **unqualified**
6218
+ predicate may be autoloaded only when the interop table assigns it one canonical
6219
+ provider. The interactive top level keeps ordinary library imports explicit;
6220
+ `time/1` is available there because it is also a normal EyeProlog runtime
6221
+ extension. For example, the canonical build-time providers are:
6212
6222
 
6213
6223
  | Predicate | Canonical autoload provider |
6214
6224
  | --- | --- |
6215
6225
  | `member/2` | `library(lists)` |
6216
6226
  | `call_nth/2` | `library(iso_ext)` |
6227
+ | `time/1` | `library(iso_ext)` |
6228
+ | `.../2` | `library(iso_ext)` |
6217
6229
  | `between/3` | `library(prologue)` |
6218
6230
 
6219
6231
  Predicates outside that table require an explicit import even when EyeProlog
@@ -6255,9 +6267,9 @@ into later branches. Trealla's larger library also contains facilities such as
6255
6267
  automata, cumulative and two-dimensional scheduling constraints, and
6256
6268
  unbounded-domain propagation that EyeProlog does not currently export.
6257
6269
 
6258
- Beyond its interop entry for `call_nth/2`, `library(iso_ext)` also exports
6259
- EyeProlog's extension relations `countall/2`, `forall/2`, `succ/2`, `cfor/3`,
6260
- `findall/4`, and `variant/2`. `forall/2` checks an action for every solution of a
6270
+ Alongside its interop entries `call_nth/2`, `time/1`, and `.../2`,
6271
+ `library(iso_ext)` also exports EyeProlog's extension relations `countall/2`,
6272
+ `forall/2`, `succ/2`, `cfor/3`, `findall/4`, and `variant/2`. `forall/2` checks an action for every solution of a
6261
6273
  condition; `cfor/3` enumerates an inclusive evaluated integer range; `succ/2`
6262
6274
  relates adjacent nonnegative integers; `findall/4` collects into a difference
6263
6275
  list; and `variant/2` recognizes terms equal up to variable renaming.