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 +12 -0
- package/package.json +1 -1
- package/src/iso.js +10 -6
- package/src/solver.js +85 -20
- package/src/term.js +7 -2
- package/test/run-regression.mjs +62 -0
- package/the-art-of-eyeprolog.md +166 -94
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
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
|
-
|
|
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
|
-
//
|
|
2037
|
-
//
|
|
2038
|
-
|
|
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
|
-
//
|
|
1208
|
-
//
|
|
1209
|
-
//
|
|
1210
|
-
|
|
1211
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
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 =
|
|
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
|
}
|
package/test/run-regression.mjs
CHANGED
|
@@ -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: () => {
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -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,
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
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
|
-
|
|
6105
|
-
|
|
6106
|
-
|
|
6107
|
-
|
|
6108
|
-
|
|
6109
|
-
|
|
6110
|
-
|
|
6111
|
-
|
|
6112
|
-
|
|
6113
|
-
|
|
6114
|
-
|
|
6115
|
-
|
|
6116
|
-
|
|
6117
|
-
|
|
6118
|
-
|
|
6119
|
-
|
|
6120
|
-
|
|
6121
|
-
|
|
6122
|
-
|
|
6123
|
-
|
|
6124
|
-
`library(
|
|
6125
|
-
|
|
6126
|
-
|
|
6127
|
-
|
|
6128
|
-
|
|
6129
|
-
|
|
6130
|
-
`library(
|
|
6131
|
-
|
|
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
|
|
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.
|
|
6142
|
-
the library installs `+\` as a priority-201 `xfx` operator; `\` and
|
|
6143
|
-
their existing ISO operator definitions. Parenthesize lower-priority
|
|
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
|
|
6155
|
-
|
|
6156
|
-
|
|
6157
|
-
`
|
|
6158
|
-
|
|
6159
|
-
|
|
6160
|
-
|
|
6161
|
-
|
|
6162
|
-
only when the interop table assigns it one canonical provider.
|
|
6163
|
-
|
|
6164
|
-
|
|
6165
|
-
|
|
6166
|
-
|
|
6167
|
-
|
|
6168
|
-
|
|
6169
|
-
|
|
6170
|
-
|
|
6171
|
-
|
|
6172
|
-
|
|
6173
|
-
|
|
6174
|
-
|
|
6175
|
-
|
|
6176
|
-
|
|
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
|
-
|
|
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.
|
|
6184
|
-
cumulative and two-dimensional scheduling constraints, and
|
|
6185
|
-
propagation that
|
|
6186
|
-
|
|
6187
|
-
`library(iso_ext)`
|
|
6188
|
-
|
|
6189
|
-
|
|
6190
|
-
|
|
6191
|
-
|
|
6192
|
-
|
|
6193
|
-
|
|
6194
|
-
|
|
6195
|
-
|
|
6196
|
-
|
|
6197
|
-
`uuid(+Seed0,-UUID,-Seed)` creates a version 4 UUID atom
|
|
6198
|
-
Passing the returned seed to the next call produces the next
|
|
6199
|
-
with the same integer seed reproduces the same sequence
|
|
6200
|
-
state replaces hidden host entropy and behaves identically in Node and
|
|
6201
|
-
browser playground.
|
|
6202
|
-
|
|
6203
|
-
On the command line, a program
|
|
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
|
|
6234
|
-
text, proper lists, indexes, dates, and aggregate generators
|
|
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
|
|