eyeprolog 1.3.11 → 1.3.13

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
@@ -107,6 +107,18 @@ keeps both long-running filters such as `\+ phrase((..., Pattern), Sequence)`
107
107
  and repeated same-input probes bounded without turning memory safety into a
108
108
  per-call cache-eviction cost.
109
109
 
110
+ Finite-tree unification also accepts an internal proven-nonoccurrence hint: when
111
+ the solver can prove that a variable cannot occur in the value it is about to
112
+ receive, that binding skips an otherwise redundant occurs traversal. The main
113
+ source-level case is conservative first use in a freshly renamed clause, inspired
114
+ by the local-variable optimization used by WAM-family systems; native construction
115
+ paths such as relational `length/2` reuse the same unifier mechanism. Repeated
116
+ variables, variables seen earlier in the clause, and ordinary public unification
117
+ remain fully occurs-checked. This is a proof local to one binding, not a WAM-style
118
+ local/global variable stack. `phrase/2` likewise supplies its fixed `[]`
119
+ remainder directly to the grammar; `phrase/3` retains its delayed final output
120
+ unification for steadfastness.
121
+
110
122
  Recursion through negation is explicit. EyeProlog provides `tnot/1` for
111
123
  well-founded negation over finite, range-restricted, function-free Datalog
112
124
  components. Ordinary `\+/1` remains negation-as-failure and is not silently
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.3.11",
6
+ "version": "1.3.13",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
package/src/iso.js CHANGED
@@ -181,7 +181,8 @@ export const eyePrologLibraryBuiltins = {
181
181
 
182
182
  function* unification({ goal, env }) {
183
183
  const next = env.clone();
184
- if (unify(goal.args[0], goal.args[1], next)) yield next;
184
+ const knownNonoccurringVariables = goal._knownNonoccurringVariables ?? null;
185
+ if (unify(goal.args[0], goal.args[1], next, { knownNonoccurringVariables })) yield next;
185
186
  }
186
187
  function* unificationWithOccursCheck({ goal, env }) {
187
188
  const next = env.clone();
@@ -2033,14 +2034,17 @@ function* phraseBuiltin({ solver, goal, env }) {
2033
2034
  throw new PrologError('type_error(list)', deref(requestedOutput, env));
2034
2035
  }
2035
2036
 
2036
- // Delay the final output unification to keep phrase/3 steadfast in its
2037
- // third argument, as required by the Part 3 execution model.
2038
- const finalOutput = variable(`\u0000phrase:${++isoFresh}`);
2037
+ // phrase/2 fixes the remainder to [] from the outset. phrase/3 keeps a
2038
+ // private output variable and delays the final unification so its third
2039
+ // argument remains steadfast as required by the Part 3 execution model.
2040
+ const finalOutput = goal.arity === 2
2041
+ ? requestedOutput
2042
+ : variable(`\u0000phrase:${++isoFresh}`);
2039
2043
  const expanded = expandDcgBody(grammarBody, input, finalOutput, {
2040
2044
  env,
2041
2045
  module: goal.module ?? grammarBody.module ?? 'user',
2042
2046
  });
2043
- const finish = compound('=', [finalOutput, requestedOutput]);
2047
+ const finish = goal.arity === 2 ? null : compound('=', [finalOutput, requestedOutput]);
2044
2048
  // Recursive DCGs are automatically tabled in normal mode. Keep tables in a
2045
2049
  // phrase-local scope keyed by the whole invocation. Repeatedly testing the
2046
2050
  // same grammar/input (issue #48) reuses its completed table, while switching
@@ -2064,7 +2068,7 @@ function* phraseBuiltin({ solver, goal, env }) {
2064
2068
  skipListTailTabling: !repeatedInvocation,
2065
2069
  });
2066
2070
  try {
2067
- yield* child.solve([expanded, finish], env, 0);
2071
+ yield* child.solve(finish == null ? [expanded] : [expanded, finish], env, 0);
2068
2072
  } finally {
2069
2073
  solver.absorbStatsFrom(child);
2070
2074
  solver.trimInnerTableScope('phrase');
package/src/solver.js CHANGED
@@ -799,9 +799,12 @@ export class Solver {
799
799
  const freshVariables = new Map();
800
800
  const freshHead = freshTerm(clause.head, id, freshVariables);
801
801
  const freshBody = clause.body.map((term) => freshTerm(term, id, freshVariables));
802
+ const localFreshPlan = clauseLocalFreshPlan(clause);
803
+ const headLocalFresh = freshVariableSet(localFreshPlan.head, freshVariables);
804
+ attachBodyLocalFreshVariables(freshBody, localFreshPlan.body, freshVariables);
802
805
  const next = env.clone();
803
806
  this.stats.unify_calls++;
804
- if (!unify(goal, freshHead, next)) continue;
807
+ if (!unify(goal, freshHead, next, { knownNonoccurringVariables: headLocalFresh })) continue;
805
808
  if (freshBody.length === 0) {
806
809
  yield* this.solve(rest, next, depth + 1);
807
810
  } else if (!groupNeedsActiveFrame(group)) {
@@ -1013,9 +1016,12 @@ function pushUserGoalUncachedFrames(stack, solver, group, goal, rest, env, depth
1013
1016
  const freshVariables = new Map();
1014
1017
  const freshHead = freshTerm(clause.head, id, freshVariables);
1015
1018
  const freshBody = clause.body.map((term) => freshTerm(term, id, freshVariables));
1019
+ const localFreshPlan = clauseLocalFreshPlan(clause);
1020
+ const headLocalFresh = freshVariableSet(localFreshPlan.head, freshVariables);
1021
+ attachBodyLocalFreshVariables(freshBody, localFreshPlan.body, freshVariables);
1016
1022
  const next = env.clone();
1017
1023
  solver.stats.unify_calls++;
1018
- if (!unify(goal, freshHead, next)) continue;
1024
+ if (!unify(goal, freshHead, next, { knownNonoccurringVariables: headLocalFresh })) continue;
1019
1025
  if (freshBody.length === 0) {
1020
1026
  frames.push({
1021
1027
  kind: 'goals',
@@ -1038,6 +1044,53 @@ function pushUserGoalUncachedFrames(stack, solver, group, goal, rest, env, depth
1038
1044
  for (let i = frames.length - 1; i >= 0; i--) stack.push(frames[i]);
1039
1045
  }
1040
1046
 
1047
+ function clauseLocalFreshPlan(clause) {
1048
+ if (clause._localFreshPlan != null) return clause._localFreshPlan;
1049
+ const seen = new Set();
1050
+ const planForTerm = (term) => {
1051
+ const counts = new Map();
1052
+ const stack = [term];
1053
+ while (stack.length > 0) {
1054
+ const current = stack.pop();
1055
+ if (current?.type === VAR) {
1056
+ counts.set(current.name, (counts.get(current.name) ?? 0) + 1);
1057
+ continue;
1058
+ }
1059
+ if (current?.type !== COMPOUND) continue;
1060
+ for (let index = 0; index < current.arity; index++) stack.push(current.args[index]);
1061
+ }
1062
+ const local = [];
1063
+ for (const [name, count] of counts) {
1064
+ if (!seen.has(name) && count === 1) local.push(name);
1065
+ seen.add(name);
1066
+ }
1067
+ return local;
1068
+ };
1069
+ const head = planForTerm(clause.head);
1070
+ const body = clause.body.map(planForTerm);
1071
+ return clause._localFreshPlan = { head, body };
1072
+ }
1073
+
1074
+ function freshVariableSet(names, freshVariables) {
1075
+ if (names.length === 0) return null;
1076
+ const fresh = new Set();
1077
+ for (const name of names) {
1078
+ const term = freshVariables.get(name);
1079
+ if (term != null) fresh.add(term.name);
1080
+ }
1081
+ return fresh.size === 0 ? null : fresh;
1082
+ }
1083
+
1084
+ function attachBodyLocalFreshVariables(freshBody, plan, freshVariables) {
1085
+ for (let index = 0; index < freshBody.length; index++) {
1086
+ 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;
1090
+ }
1091
+ }
1092
+ }
1093
+
1041
1094
  function groupNeedsActiveFrame(group) {
1042
1095
  // A direct recursive call that consumes the tail of a matched list cannot
1043
1096
  // revisit an earlier finite-tree call. It needs neither a cycle guard nor an
@@ -1204,12 +1257,11 @@ function* fixedLengthSolutions(solver, list, length, env) {
1204
1257
  const suffix = compactVariableList(remaining, `__length${id}_`);
1205
1258
  const next = env.clone();
1206
1259
  solver.stats.unify_calls++;
1207
- // cursor is dereferenced and the compact skeleton contains only freshly
1208
- // generated variables, so this binding cannot create a cycle. Binding it
1209
- // directly avoids traversing and expanding the new skeleton for an occurs
1210
- // check whose result is known by construction.
1211
- next.bind(cursor.name, suffix);
1212
- yield next;
1260
+ // The compact skeleton contains only freshly generated variables, so the
1261
+ // dereferenced tail variable is known not to occur in it. Reuse the same
1262
+ // proven-nonoccurrence path as source-level first-use unification.
1263
+ const knownNonoccurringVariables = new Set([cursor.name]);
1264
+ if (unify(cursor, suffix, next, { knownNonoccurringVariables })) yield next;
1213
1265
  }
1214
1266
 
1215
1267
  function* generatedLengthSolutions(solver, list, length, env) {
@@ -1243,15 +1295,13 @@ function* generatedLengthSolutions(solver, list, length, env) {
1243
1295
 
1244
1296
  const id = nextFreshId();
1245
1297
  let suffix = emptyList();
1298
+ // Every generated suffix is built from fresh variables and therefore cannot
1299
+ // contain the caller's dereferenced tail variable. Share the general
1300
+ // proven-nonoccurrence unification path instead of bypassing unify() here.
1301
+ const knownNonoccurringVariables = new Set([cursor.name]);
1246
1302
  for (let extra = 0n; ; extra++) {
1247
1303
  const next = env.clone();
1248
- // cursor is a dereferenced plain variable and suffix is made only from
1249
- // freshly generated variables, so this binding cannot create a cycle.
1250
- // Binding directly avoids an O(extra) occurs-check over the complete
1251
- // growing suffix for every answer; without this, unbounded length/2
1252
- // generation becomes quadratic and can spend hours before reaching the
1253
- // normal memory resource guard (issue #49).
1254
- next.bind(cursor.name, suffix);
1304
+ if (!unify(cursor, suffix, next, { knownNonoccurringVariables })) return;
1255
1305
  const answer = bindGeneratedLength(solver, length, count + extra, next);
1256
1306
  if (answer != null) yield answer;
1257
1307
  suffix = cons(variable(`__length${id}_${extra}`), suffix);
@@ -1900,10 +1950,14 @@ function compactIndexBucket(index, type, name) {
1900
1950
 
1901
1951
  function tryPushCompactBinaryChainFrames(stack, solver, group, goal, rest, env, depth, active) {
1902
1952
  if (active.length !== 0 || goal.type !== COMPOUND || goal.arity !== 2) return false;
1903
- const resolved = copyResolved(goal, env);
1904
- const first = resolved.args[0];
1905
- let secondType = resolved.args[1]?.type;
1906
- let secondName = resolved.args[1]?.name;
1953
+ // This fast path only accepts scalar arguments. Dereference those arguments
1954
+ // directly instead of deep-copying the whole goal before discovering that a
1955
+ // list/compound argument is ineligible. Besides avoiding wasted work, this
1956
+ // keeps deep DCG state lists away from recursive copyResolved().
1957
+ const first = derefForLocal(goal.args[0], env);
1958
+ const second = derefForLocal(goal.args[1], env);
1959
+ let secondType = second?.type;
1960
+ let secondName = second?.name;
1907
1961
  if (!isScalarTerm(first) || !['atom', 'string', 'number'].includes(secondType)) return false;
1908
1962
 
1909
1963
  const index = group.argIndexes[1];
@@ -2014,10 +2068,17 @@ function tryPushGroundChainFrames(stack, solver, group, goal, rest, env, depth,
2014
2068
  // has exactly one matching clause and a single ground body goal; otherwise the
2015
2069
  // normal clause path below remains authoritative.
2016
2070
  if (!termIsGround(goal, env)) return false;
2071
+ // The chain matcher below only propagates variables bound to flat scalar
2072
+ // head arguments (or literal scalars). Keep this optimization on that flat
2073
+ // domain instead of recursively copying an arbitrary ground compound just
2074
+ // to discover later that a rule shape is unsupported. Deep list arguments
2075
+ // such as DCG state therefore fall straight through to normal resolution.
2076
+ const resolvedArgs = goal.args.map((arg) => derefForLocal(arg, env));
2077
+ if (!resolvedArgs.every(isScalarTerm)) return false;
2017
2078
 
2018
2079
  const baseEnv = env;
2019
2080
  let currentGroup = group;
2020
- let currentGoal = copyResolved(goal, env);
2081
+ let currentGoal = compound(goal.name, resolvedArgs);
2021
2082
  let currentDepth = depth;
2022
2083
  const currentEnv = new Env();
2023
2084
  const seen = new Set();
@@ -2328,6 +2389,10 @@ function selectReadyDeterministicBuiltin(goals, env, registry) {
2328
2389
  for (let i = 0; i < goals.length; i++) {
2329
2390
  const goal = goals[i];
2330
2391
  if (goal?.kind === 'releaseActive' || goal?.kind === 'memoStore') return 0;
2392
+ // A first-use proof is derived from source goal order. Do not move a later
2393
+ // deterministic builtin across that equality: doing so could touch one of
2394
+ // its proven-fresh variables before the checked binding executes.
2395
+ if (goal?._knownNonoccurringVariables != null) return 0;
2331
2396
  if (goal.type !== COMPOUND && goal.type !== 'atom') continue;
2332
2397
  const def = registry.get(goal.name, goal.arity);
2333
2398
  if (!def?.deterministic || typeof def.ready !== 'function') continue;
package/src/term.js CHANGED
@@ -271,6 +271,11 @@ export function unify(left, right, env, options = {}) {
271
271
  // unification: a variable cannot be bound to a term containing itself.
272
272
  // Bindings are written into the supplied Env.
273
273
  const occursCheckHandler = options.occursCheck === 'fail' ? null : env?._occursCheckHandler;
274
+ // Callers may provide a proof that selected variables cannot occur in the
275
+ // term they are about to receive. Source-level first-use analysis and a few
276
+ // construction fast paths share this internal proof; ordinary unification
277
+ // remains fully occurs-checked.
278
+ const knownNonoccurringVariables = options.knownNonoccurringVariables ?? null;
274
279
  const stack = [[left, right]];
275
280
  while (stack.length) {
276
281
  let [a, b] = stack.pop();
@@ -286,7 +291,7 @@ export function unify(left, right, env, options = {}) {
286
291
  continue;
287
292
  }
288
293
  if (a.type === VAR) {
289
- if (occurs(a.name, b, env)) {
294
+ if (!knownNonoccurringVariables?.has(a.name) && occurs(a.name, b, env)) {
290
295
  occursCheckHandler?.(a, b, env);
291
296
  return false;
292
297
  }
@@ -295,7 +300,7 @@ export function unify(left, right, env, options = {}) {
295
300
  continue;
296
301
  }
297
302
  if (b.type === VAR) {
298
- if (occurs(b.name, a, env)) {
303
+ if (!knownNonoccurringVariables?.has(b.name) && occurs(b.name, a, env)) {
299
304
  occursCheckHandler?.(b, a, env);
300
305
  return false;
301
306
  }
@@ -1317,6 +1317,68 @@ c4 ?- call((!;1)).
1317
1317
  assertEqual(result.stderr, '', 'stderr');
1318
1318
  },
1319
1319
  },
1320
+ {
1321
+ name: 'proven-nonoccurrence first-use shortcut preserves finite-tree occurs checking',
1322
+ run: () => {
1323
+ const program = Program.parse(`
1324
+ first_use_cycle :- X = f(Y), Y = g(X).
1325
+ repeated_cycle :- X = f(X).
1326
+ first_use_ok(T) :- X = f(Y), Y = a, T = X.
1327
+ `);
1328
+ const solver = new Solver(program);
1329
+ const solveCount = (text) => {
1330
+ const goal = parseGoalText(text, {
1331
+ doubleQuotes: 'chars',
1332
+ operatorDefinitions: [...program.operators.values()],
1333
+ });
1334
+ let count = 0;
1335
+ for (const _ of solver.solve([goal], new Env(), 0)) count++;
1336
+ return count;
1337
+ };
1338
+ assertEqual(solveCount('first_use_cycle'), 0, 'cycle across later first-use binding');
1339
+ assertEqual(solveCount('repeated_cycle'), 0, 'same-goal repeated variable still checks occurs');
1340
+ assertEqual(solveCount('first_use_ok(f(a))'), 1, 'acyclic first-use bindings still succeed');
1341
+ },
1342
+ },
1343
+ {
1344
+ name: 'phrase/2 fixes the final remainder before running the grammar',
1345
+ run: () => {
1346
+ const program = Program.parse('probe(_, Out) :- var(Out).\n');
1347
+ const solver = new Solver(program);
1348
+ const goal = parseGoalText('phrase(probe, [])', {
1349
+ doubleQuotes: 'chars',
1350
+ operatorDefinitions: [...program.operators.values()],
1351
+ });
1352
+ let count = 0;
1353
+ for (const _ of solver.solve([goal], new Env(), 0)) count++;
1354
+ assertEqual(count, 0, 'phrase/2 exposes [] rather than a temporary output variable');
1355
+ },
1356
+ },
1357
+ {
1358
+ name: 'deep tail-consuming DCG avoids quadratic occurs scans and recursive ground-goal copying',
1359
+ run: () => {
1360
+ const engineUrl = new URL('../src/index.js', import.meta.url).href;
1361
+ const script = `
1362
+ import { Program, Solver, Env, atom, compound, listFromItems } from ${JSON.stringify(engineUrl)};
1363
+ const program = Program.parse(${JSON.stringify('s --> [].\ns --> [x], s.\n')});
1364
+ const input = listFromItems(Array.from({ length: 2500 }, () => atom('x')));
1365
+ const solver = new Solver(program, { solutionLimit: 1, maxMemoryBytes: Infinity });
1366
+ const goal = compound('phrase', [atom('s'), input]);
1367
+ let count = 0;
1368
+ for (const _ of solver.solve([goal], new Env(), 0)) { count++; break; }
1369
+ if (count !== 1) throw new Error('deep DCG did not succeed');
1370
+ process.stdout.write('ok');
1371
+ `;
1372
+ const result = spawnSync(process.execPath, [
1373
+ '--input-type=module',
1374
+ '--eval',
1375
+ script,
1376
+ ], { cwd: packageRoot, encoding: 'utf8', timeout: 10000 });
1377
+ if (result.error) throw result.error;
1378
+ assertEqual(result.status, 0, `deep DCG child status; stderr=${result.stderr}`);
1379
+ assertEqual(result.stdout, 'ok', 'deep DCG result');
1380
+ },
1381
+ },
1320
1382
  {
1321
1383
  name: 'REPL enumerates and stops answers like the Scryer top level',
1322
1384
  run: () => {
@@ -1900,15 +1900,37 @@ unification, another list predicate, or answer readback inspects it. This is a
1900
1900
  storage optimization, not a distinct Prolog term or list semantics. Embedders
1901
1901
  that inspect the JavaScript term model can recognize this representation with
1902
1902
  `CompactListTerm`, `isCompactList`, and `compactListLength`, or construct one
1903
- with `compactVariableList`. For open-ended `length(List, N)` generation, the
1904
- bundled path binds each fresh generated spine directly instead of re-running an
1905
- occurs-check over the whole growing list. It also reserves recovery headroom
1906
- proportional to the retained spine, so a finite heap limit is raised inside the
1907
- `length/2` search as a catchable `resource_error(memory)` rather than allowing
1908
- an outer solver frame to encounter the limit first. The ordinary clauses remain
1909
- the authoritative module definition and are used unchanged by the ISO-only
1910
- registry and whenever delays or finite-domain constraints require their normal
1911
- wake-up points.
1903
+ with `compactVariableList`. For open-ended `length(List, N)` generation, each generated spine is known not
1904
+ to contain the caller's dereferenced tail variable. The bundled path passes that
1905
+ proof through the normal unifier, sharing the same proven-nonoccurrence mechanism
1906
+ as first-use clause variables instead of maintaining a predicate-specific raw
1907
+ binding shortcut. It still reserves recovery headroom proportional to the
1908
+ retained spine, so a finite heap limit is raised inside the `length/2` search as
1909
+ a catchable `resource_error(memory)` rather than allowing an outer solver frame
1910
+ to encounter the limit first.
1911
+
1912
+ The same proven-nonoccurrence mechanism has a conservative source-level form for freshly renamed
1913
+ clauses. A singleton variable in the clause head, or a variable that has not
1914
+ appeared in the head or any earlier body goal and occurs exactly once in a
1915
+ direct `=/2` goal, cannot already be a subterm of the value it is about to
1916
+ receive. EyeProlog marks only that binding as locally fresh and skips its occurs
1917
+ traversal. A repeated variable such as the `X` in `X = f(X)`, a variable already
1918
+ seen earlier in the clause, and `unify_with_occurs_check/2` all keep the normal
1919
+ finite-tree check. The solver also treats such a first-use equality as a
1920
+ source-order barrier for its deterministic-goal scheduling, so the freshness
1921
+ proof cannot be invalidated by moving a later goal ahead of it. This recovers
1922
+ much of the classic WAM-family "local variable" optimization for DCG tail
1923
+ variables without introducing a WAM local/global stack distinction into the
1924
+ JavaScript term model.
1925
+
1926
+ For grammar execution, `phrase/2` passes its fixed final remainder `[]` directly
1927
+ into the expanded grammar. Besides matching the two-argument contract, this
1928
+ avoids repeatedly trying an empty production against a temporary output
1929
+ variable. `phrase/3` still uses a private final-output variable and delays its
1930
+ last unification, preserving the existing steadfast treatment of its explicit
1931
+ third argument. The ordinary `length/2` clauses remain the authoritative module
1932
+ definition and are used unchanged by the ISO-only registry and whenever delays
1933
+ or finite-domain constraints require their normal wake-up points.
1912
1934
 
1913
1935
  ### Implementation boundary
1914
1936
 
@@ -6101,47 +6123,74 @@ between solution branches.
6101
6123
 
6102
6124
  #### Interoperability profile and conservative autoloading
6103
6125
 
6104
- The full EyeProlog library is larger than the deliberately conservative
6105
- EyeProlog/Trealla/Scryer source-interoperability profile. The two concepts are
6106
- kept separate: a predicate can be implemented as ordinary portable Prolog and
6107
- still have an API that is not shared by the other systems.
6108
-
6109
- `library(lists)` is the first aligned common module. Its interop surface includes
6110
- `member/2`, `memberchk/2`, `select/3`, `append/2-3`, `last/2`,
6111
- `same_length/2`, `nth0/3-4`, `nth1/3-4`, `reverse/2`, `length/2`,
6112
- `maplist/2-8`, `foldl/4-6`, `sum_list/2`, and `list_to_set/2`. Its `length/2`
6113
- is fully relational: when both arguments are variables, `length(Xs, N)`
6114
- enumerates `Xs = [], N = 0`, then one-element lists with `N = 1`, and so on.
6115
- This generator mode is important for portable stress and enumeration programs.
6116
- EyeProlog-only helpers such as `set_nth0/4`, `take/3`, `drop/3`, and `slice/4`
6117
- remain available for compatibility but are outside this profile.
6118
-
6119
- `library(iso_ext)` is also recognized as a common interop module name.
6120
- EyeProlog exports `call_nth/2` there, matching Scryer's explicit import
6121
- organization while still permitting unqualified autoloaded source. The aligned
6122
- `library(lists)` and `library(iso_ext)` export sets are intentionally disjoint,
6123
- so a source file may import both without an accidental predicate collision.
6124
- `library(prologue)` is retained as an EyeProlog compatibility umbrella and may
6125
- overlap these modules; use selective imports when legacy code combines it with
6126
- the aligned libraries.
6127
-
6128
- `library(lambda)` is an explicitly imported Scryer-aligned module for
6129
- higher-order programming. Its implementation is adapted from Ulrich Neumerkel's
6130
- `library(lambda)` as distributed by Scryer Prolog, retaining the upstream
6131
- copyright and redistribution notice. The public syntax is:
6126
+ EyeProlog keeps four related concepts separate:
6127
+
6128
+ | Layer | Meaning |
6129
+ | --- | --- |
6130
+ | **ISO core** | The documented ISO predicate profile built into the processor. No EyeProlog library import is involved. |
6131
+ | **EyeProlog library surface** | Every module and exported predicate listed in the catalog above. Programs normally access these with `use_module/1-2`. |
6132
+ | **Interoperability profile** | A deliberately smaller set of library names and predicate interfaces that EyeProlog intends to keep source-compatible with Trealla and Scryer where practical. |
6133
+ | **Autoload map** | An even smaller convenience table that gives selected unqualified predicates one canonical EyeProlog provider. |
6134
+
6135
+ These layers answer different questions. A predicate may be implemented entirely
6136
+ as ordinary Prolog and still be outside the cross-processor interoperability
6137
+ profile; conversely, an interoperable predicate may be backed by a private host
6138
+ adapter. In this section, **portable** refers to source portability between
6139
+ Prolog systems, not merely to the language in which a predicate happens to be
6140
+ implemented.
6141
+
6142
+ The current interoperability profile recognizes these library roles:
6143
+
6144
+ | Library | Role in the interoperability profile |
6145
+ | --- | --- |
6146
+ | `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. |
6148
+ | `library(lambda)` | Scryer-aligned higher-order notation. It is imported explicitly because loading it also installs the `+\` operator. |
6149
+ | `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
+
6151
+ Other modules from the catalog, including `library(clpz)`, `library(strings)`,
6152
+ `library(random)`, and `library(uuid)`, remain normal EyeProlog libraries. They
6153
+ can be imported explicitly, but their module names and full APIs are not thereby
6154
+ claimed as part of the current conservative Trealla/Scryer profile.
6155
+
6156
+ For `library(lists)`, the current interop predicate set is `member/2`,
6157
+ `memberchk/2`, `select/3`, `append/2-3`, `last/2`, `same_length/2`,
6158
+ `nth0/3-4`, `nth1/3-4`, `reverse/2`, `length/2`, `maplist/2-8`,
6159
+ `foldl/4-6`, `sum_list/2`, and `list_to_set/2`. Other exports from the same
6160
+ module, such as `min_list/2`, `max_list/2`, `set_nth0/4`, `take/3`, `drop/3`,
6161
+ and `slice/4`, remain available to EyeProlog programs but lie outside this
6162
+ conservative cross-engine subset.
6163
+
6164
+ `length/2` remains fully relational. With both arguments variable,
6165
+ `length(Xs, N)` enumerates `Xs = [], N = 0`, then one-element lists with
6166
+ `N = 1`, and so on. Open-ended generation uses the normal memory guard with
6167
+ recovery headroom so finite-heap exhaustion remains a catchable
6168
+ `resource_error(memory)`.
6169
+
6170
+ `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
6174
+ `library(iso_ext)` are kept disjoint, so both modules can be imported together
6175
+ without an accidental collision. `library(prologue)` remains a compatibility
6176
+ umbrella and overlaps them; use selective imports when legacy code combines it
6177
+ with the aligned modules.
6178
+
6179
+ `library(lambda)` follows Scryer's higher-order notation, adapted from Ulrich
6180
+ Neumerkel's permissively licensed implementation. Its public syntax is:
6132
6181
 
6133
6182
  ```text
6134
6183
  \X1^X2^...^XN^Goal
6135
6184
  Free+\X1^X2^...^XN^Goal
6136
6185
  ```
6137
6186
 
6138
- The first form has no explicitly shared free variables. Before each invocation
6139
- EyeProlog copies the closure term, so local variables are fresh on successive
6187
+ The first form has no explicitly shared free variables. Before each invocation,
6188
+ EyeProlog copies the closure term so local variables are fresh on successive
6140
6189
  `maplist/2-8`, `foldl/4-6`, or direct `call/N` uses. In the second form, the
6141
- variables contained in `Free` remain shared with the surrounding goal. Importing
6142
- the library installs `+\` as a priority-201 `xfx` operator; `\` and `^` use
6143
- their existing ISO operator definitions. Parenthesize lower-priority goal
6144
- operators after `^`, for example `\X^(X > 3)`.
6190
+ variables contained in `Free` remain shared with the surrounding goal.
6191
+ Importing the library installs `+\` as a priority-201 `xfx` operator; `\` and
6192
+ `^` use their existing ISO operator definitions. Parenthesize lower-priority
6193
+ goal operators after `^`, for example `\X^(X > 3)`.
6145
6194
 
6146
6195
  A continuation lambda may leave arguments for a later call:
6147
6196
 
@@ -6151,56 +6200,78 @@ f(x, y).
6151
6200
  answer(A, B) :- call(\X^f(X), A, B).
6152
6201
  ```
6153
6202
 
6154
- This is equivalent to supplying both arguments directly. A lambda that is
6155
- called with too few parameters raises `existence_error(lambda_parameter, ...)`,
6156
- matching the diagnostic intent of the Scryer library. EyeProlog uses its ISO
6157
- `copy_term/2` implementation for the fresh-copy step; in EyeProlog this gives
6158
- the natural-copy behavior needed by the library without requiring a separate
6159
- `copy_term_nat/2` predicate.
6160
-
6161
- Normal EyeProlog execution can autoload an otherwise undefined unqualified call
6162
- only when the interop table assigns it one canonical provider. Thus `member/2`
6163
- autoloads from `library(lists)`, `call_nth/2` from `library(iso_ext)`, and
6164
- `between/3` from the internal `library(prologue)` implementation. Autoloading is
6165
- disabled by `--no-autoload`, by the JavaScript option `autoload: false`, and
6166
- always by `--iso-strict`.
6167
-
6168
- `-w` / `--warnings` reports explicit non-profile library dependencies and calls
6169
- to non-profile predicates from common libraries. `--portable` turns those
6170
- portability diagnostics into a failing command, making the profile suitable for
6171
- continuous integration. `npm run test:interop` executes the same Sudoku source
6172
- under EyeProlog, Trealla, and Scryer when those commands are installed; the
6173
- repository's interoperability workflow installs them and runs that check.
6174
-
6175
- `library(clpz)` follows the Trealla and Scryer convention for constraint logic
6176
- programming over integers. Its first implementation step provides finite
6203
+ This is equivalent to supplying both arguments directly. A lambda called with
6204
+ too few parameters raises `existence_error(lambda_parameter, ...)`. EyeProlog
6205
+ uses its ISO `copy_term/2` implementation for the fresh-copy step and does not
6206
+ require a separate `copy_term_nat/2` predicate.
6207
+
6208
+ 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:
6212
+
6213
+ | Predicate | Canonical autoload provider |
6214
+ | --- | --- |
6215
+ | `member/2` | `library(lists)` |
6216
+ | `call_nth/2` | `library(iso_ext)` |
6217
+ | `between/3` | `library(prologue)` |
6218
+
6219
+ Predicates outside that table require an explicit import even when EyeProlog
6220
+ provides them. Explicit imports therefore remain the clearest way to state
6221
+ library dependencies:
6222
+
6223
+ ```text
6224
+ :- use_module(library(lists)).
6225
+ :- use_module(library(iso_ext), [call_nth/2]).
6226
+ ```
6227
+
6228
+ Use `--no-autoload`, or the JavaScript option `autoload: false`, when every
6229
+ library dependency should be explicit. `--iso-strict` always disables EyeProlog
6230
+ library autoloading.
6231
+
6232
+ `-w` / `--warnings` reports explicit dependencies on non-profile libraries and
6233
+ calls to non-profile predicates from otherwise common modules. `--portable`
6234
+ turns those diagnostics into a failing run, making the conservative profile
6235
+ suitable for continuous integration. `npm run test:interop` executes the same
6236
+ portable Sudoku source under EyeProlog, Trealla, and Scryer when those commands
6237
+ are installed; the repository interoperability workflow installs them and runs
6238
+ that check.
6239
+
6240
+ #### Library notes beyond the interoperability profile
6241
+
6242
+ The catalog above is authoritative for the complete EyeProlog library surface.
6243
+ The following notes describe useful parts of that surface without extending the
6244
+ cross-engine claims made above.
6245
+
6246
+ `library(clpz)` follows the familiar Trealla and Scryer convention for
6247
+ constraint logic programming over integers. EyeProlog currently provides finite
6177
6248
  interval and union domains, arithmetic and reified constraints, backtrackable
6178
- labeling with `ff`, `up`, and `down`, global distinctness, linear sums and
6179
- scalar products, chains, elements, value counting, extensional tuple tables,
6249
+ labeling with `ff`, `up`, and `down`, global distinctness, linear sums and scalar
6250
+ products, chains, elements, value counting, extensional tuple tables,
6180
6251
  lexicographic chains, serialized schedules, global cardinality with costs,
6181
6252
  Hamiltonian circuits, three-way comparison, and domain reflection. Constraints
6182
6253
  are kept in the logical environment, so failed alternatives cannot leak domains
6183
- into later branches. The full Trealla library also contains automata,
6184
- cumulative and two-dimensional scheduling constraints, and unbounded-domain
6185
- propagation that are not yet exported here.
6186
-
6187
- `library(iso_ext)` collects extensions commonly found in mature Prolog
6188
- systems. `call_nth/2` exposes the ordinal number of each solution and supports
6189
- the explicit import used by Scryer-style source; `forall/2` checks an action for
6190
- every solution of a condition; `cfor/3` enumerates an inclusive evaluated
6191
- integer range; `succ/2` relates adjacent nonnegative integers; `findall/4`
6192
- collects into a difference list; and `variant/2` recognizes terms equal up to
6193
- variable renaming. Its portable `countall/2` counts solutions without exposing
6194
- a template. `countall/2` is not exported by `library(lists)`, avoiding a
6195
- full-module import collision between the two libraries.
6196
-
6197
- `uuid(+Seed0,-UUID,-Seed)` creates a version 4 UUID atom using `random/3`.
6198
- Passing the returned seed to the next call produces the next UUID; restarting
6199
- with the same integer seed reproduces the same sequence exactly. This explicit
6200
- state replaces hidden host entropy and behaves identically in Node and the
6201
- browser playground.
6202
-
6203
- On the command line, a program imports the modules it uses:
6254
+ into later branches. Trealla's larger library also contains facilities such as
6255
+ automata, cumulative and two-dimensional scheduling constraints, and
6256
+ unbounded-domain propagation that EyeProlog does not currently export.
6257
+
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
6261
+ condition; `cfor/3` enumerates an inclusive evaluated integer range; `succ/2`
6262
+ relates adjacent nonnegative integers; `findall/4` collects into a difference
6263
+ list; and `variant/2` recognizes terms equal up to variable renaming.
6264
+ `countall/2` counts solutions without exposing a template. These exports do not
6265
+ all belong to the conservative interop subset merely because they share the
6266
+ `iso_ext` module.
6267
+
6268
+ `uuid(+Seed0,-UUID,-Seed)` from `library(uuid)` creates a version 4 UUID atom
6269
+ using `random/3`. Passing the returned seed to the next call produces the next
6270
+ UUID; restarting with the same integer seed reproduces the same sequence. This
6271
+ explicit state replaces hidden host entropy and behaves identically in Node and
6272
+ the browser playground.
6273
+
6274
+ On the command line, a program can state its library dependencies explicitly:
6204
6275
 
6205
6276
  ```sh
6206
6277
  printf '%s\n' ':- use_module(library(lists)).' 'answer(X) :- member(X, [ready]).' > program.pl
@@ -6208,12 +6279,13 @@ eyeprolog --goal 'answer(X)' program.pl
6208
6279
  eyeprolog -p program.pl # add proof output
6209
6280
  ```
6210
6281
 
6211
- JavaScript uses the same registry by default:
6282
+ JavaScript uses the same normal EyeProlog library registry by default:
6212
6283
 
6213
6284
  ```js
6214
6285
  import { run } from 'eyeprolog';
6215
6286
 
6216
6287
  const source = `
6288
+ :- use_module(library(lists)).
6217
6289
  answer(Whole) :- append([red, green], [blue], Whole).
6218
6290
  `;
6219
6291
 
@@ -6221,7 +6293,7 @@ const result = run(source, { goal: 'answer(X)' });
6221
6293
  console.log(result.stdout);
6222
6294
  ```
6223
6295
 
6224
- The mode notation below is descriptive:
6296
+ The mode notation used in the reference tables below is descriptive:
6225
6297
 
6226
6298
  - `+` means the argument must already have the required input shape;
6227
6299
  - `-` means the predicate produces that argument;
@@ -6230,9 +6302,9 @@ The mode notation below is descriptive:
6230
6302
  Most EyeProlog library predicates are projections or filters. When an input is
6231
6303
  unbound, malformed, outside its domain, or incompatible with the requested
6232
6304
  output, they normally **fail** rather than raising the ISO errors described in
6233
- the errors section above. They do not invent open-ended domains. Bind arithmetic operands, source
6234
- text, proper lists, indexes, dates, and aggregate generators before calling
6235
- the corresponding predicate.
6305
+ the errors section above. They do not invent open-ended domains. Bind arithmetic
6306
+ operands, source text, proper lists, indexes, dates, and aggregate generators
6307
+ before calling the corresponding predicate.
6236
6308
 
6237
6309
  #### Portable numeric, comparison, and date relations
6238
6310