eyeprolog 1.6.28 → 1.6.29
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/index.d.ts +1 -0
- package/package.json +1 -1
- package/src/ARCHITECTURE.md +14 -0
- package/src/explain.js +20 -4
- package/src/iso.js +17 -3
- package/src/solver.js +26 -15
- package/src/term.js +84 -41
- package/test/regression/cases-white-box.mjs +62 -1
package/index.d.ts
CHANGED
|
@@ -214,6 +214,7 @@ export type BuiltinHandler =
|
|
|
214
214
|
export class BuiltinRegistry {
|
|
215
215
|
constructor();
|
|
216
216
|
defs: Map<string, BuiltinDefinition>;
|
|
217
|
+
byName: Map<string, Map<number, BuiltinDefinition>>;
|
|
217
218
|
eyePrologLibrary?: boolean;
|
|
218
219
|
add(name: string, arity: number, handler: BuiltinHandler, options?: Partial<BuiltinDefinition>): this;
|
|
219
220
|
get(name: string, arity: number): BuiltinDefinition | null;
|
package/package.json
CHANGED
package/src/ARCHITECTURE.md
CHANGED
|
@@ -39,6 +39,10 @@ Verification walks the supplied certificate and checks source-clause steps again
|
|
|
39
39
|
the parsed `Program`; it does not call the solver to rediscover the proof.
|
|
40
40
|
Built-in and abstract library nodes are explicit trust boundaries, while expanded
|
|
41
41
|
proofs expose bundled Prolog-library clauses as ordinary source steps.
|
|
42
|
+
Proof nodes share the runtime terms they resolve. Runtime terms are structurally
|
|
43
|
+
immutable, so a ground subtree already is the snapshot a proof needs, and
|
|
44
|
+
`resolveForProof` copies only the ancestors of an argument that variable
|
|
45
|
+
resolution actually changed. Nothing anywhere may rewrite a term in place.
|
|
42
46
|
|
|
43
47
|
`iso.js` and `program.js` remain facade modules for their existing exports, so
|
|
44
48
|
this refactor does not change the public JavaScript API.
|
|
@@ -116,6 +120,16 @@ construct native propagation/search plans while their Prolog modules retain
|
|
|
116
120
|
validation, residual constraints, and portable fallbacks. A cleaner file layout
|
|
117
121
|
is not worth a runtime regression, so such splits must retain benchmark parity.
|
|
118
122
|
|
|
123
|
+
Hot paths follow one allocation discipline: the state a walk or a comparison
|
|
124
|
+
needs only for its hard case -- a cycle guard, a traversal stack, a variable
|
|
125
|
+
pairing -- is allocated when that case actually appears rather than on entry.
|
|
126
|
+
`deref`, `variantTerms`, and `goalHeadTermsCannotMatch` therefore decide the
|
|
127
|
+
shallow, acyclic, scalar-argument case, which is the overwhelming majority of
|
|
128
|
+
their calls, without allocating anything. A key a table or an index derives
|
|
129
|
+
from a term is built once into one fragment array for the whole term: a key
|
|
130
|
+
composed from a key per subterm makes recording one answer quadratic in that
|
|
131
|
+
answer's own size.
|
|
132
|
+
|
|
119
133
|
Performance claims use `npm test`'s own elapsed time across the full corpus,
|
|
120
134
|
not predicate, inference, or host-call counts as a substitute for elapsed
|
|
121
135
|
time, and not a separate wall-clock benchmark harness.
|
package/src/explain.js
CHANGED
|
@@ -131,12 +131,15 @@ function* proveGoalAll(program, goal, env, depth, maxDepth, registry, active, de
|
|
|
131
131
|
const id = nextFreshId();
|
|
132
132
|
const freshVariables = new Map();
|
|
133
133
|
const freshHead = freshTerm(clause.head, id, freshVariables);
|
|
134
|
-
const freshBody = clause.body.map((term) => freshTerm(term, id, freshVariables));
|
|
135
134
|
const next = env.clone();
|
|
136
135
|
if (!unify(goal, freshHead, next)) continue;
|
|
137
136
|
|
|
137
|
+
// Rename the body only once the head has matched. The shared variable
|
|
138
|
+
// map carries the head's renamings, so a body renamed now is the same
|
|
139
|
+
// body renaming as before -- a clause rejected on its head simply no
|
|
140
|
+
// longer pays for one.
|
|
141
|
+
const freshBody = clause.body.map((term) => freshTerm(term, id, freshVariables));
|
|
138
142
|
const substitutions = collectClauseSubstitutions(clause, freshHead, freshBody);
|
|
139
|
-
const bindings = resolvedSubstitutions(substitutions, next);
|
|
140
143
|
|
|
141
144
|
if (freshBody.length === 0) {
|
|
142
145
|
yield {
|
|
@@ -146,7 +149,10 @@ function* proveGoalAll(program, goal, env, depth, maxDepth, registry, active, de
|
|
|
146
149
|
method: sourceMethod(clause, 'fact'),
|
|
147
150
|
sourceHead: clause.head,
|
|
148
151
|
sourceBody: [],
|
|
149
|
-
|
|
152
|
+
// A fact binds everything it is going to bind at head unification.
|
|
153
|
+
// A rule's substitutions are only final once its body is proved,
|
|
154
|
+
// so that branch resolves them against the proving environment.
|
|
155
|
+
bindings: resolvedSubstitutions(substitutions, next),
|
|
150
156
|
children: [],
|
|
151
157
|
},
|
|
152
158
|
};
|
|
@@ -254,7 +260,17 @@ function builtinChildren(program, goal, env, depth, maxDepth, registry, active,
|
|
|
254
260
|
}
|
|
255
261
|
|
|
256
262
|
function activeVariant(goal, env, active) {
|
|
257
|
-
|
|
263
|
+
// Only a goal for the same predicate can be a variant of this one, so reject
|
|
264
|
+
// the rest on their functor instead of on a structural comparison. Scan from
|
|
265
|
+
// the innermost entry outwards: a replay that is about to cycle repeats its
|
|
266
|
+
// nearest ancestor, not the goal the whole replay started from.
|
|
267
|
+
for (let index = active.length - 1; index >= 0; index--) {
|
|
268
|
+
const entry = active[index];
|
|
269
|
+
const candidate = entry.goal;
|
|
270
|
+
if (candidate.type !== goal.type || candidate.name !== goal.name || candidate.arity !== goal.arity) continue;
|
|
271
|
+
if (variantTerms(goal, env, candidate, entry.env)) return true;
|
|
272
|
+
}
|
|
273
|
+
return false;
|
|
258
274
|
}
|
|
259
275
|
|
|
260
276
|
function sourceMethod(clause, kind) {
|
package/src/iso.js
CHANGED
|
@@ -3302,10 +3302,15 @@ import { isBuiltin, arithmeticComparison } from './iso-arithmetic.js';
|
|
|
3302
3302
|
export class BuiltinRegistry {
|
|
3303
3303
|
constructor() {
|
|
3304
3304
|
this.defs = new Map();
|
|
3305
|
+
// `defs` stays keyed by the printable predicate indicator because callers
|
|
3306
|
+
// enumerate the catalog from it. Lookups, though, happen at least once per
|
|
3307
|
+
// goal on the solver's hot path, so they go through a name/arity index
|
|
3308
|
+
// rather than formatting -- and allocating -- an indicator string each time.
|
|
3309
|
+
this.byName = new Map();
|
|
3305
3310
|
}
|
|
3306
3311
|
|
|
3307
3312
|
add(name, arity, handler, options = {}) {
|
|
3308
|
-
|
|
3313
|
+
const def = {
|
|
3309
3314
|
name,
|
|
3310
3315
|
arity,
|
|
3311
3316
|
handler,
|
|
@@ -3317,16 +3322,25 @@ export class BuiltinRegistry {
|
|
|
3317
3322
|
fallbackWhenNotReady: options.fallbackWhenNotReady ?? false,
|
|
3318
3323
|
shouldUse: options.shouldUse ?? null,
|
|
3319
3324
|
eyePrologLibrary: options.eyePrologLibrary ?? false,
|
|
3320
|
-
}
|
|
3325
|
+
};
|
|
3326
|
+
this.defs.set(`${name}/${arity}`, def);
|
|
3327
|
+
let byArity = this.byName.get(name);
|
|
3328
|
+
if (byArity == null) this.byName.set(name, byArity = new Map());
|
|
3329
|
+
byArity.set(arity, def);
|
|
3321
3330
|
return this;
|
|
3322
3331
|
}
|
|
3323
3332
|
|
|
3324
3333
|
get(name, arity) {
|
|
3325
|
-
return this.
|
|
3334
|
+
return this.byName.get(name)?.get(arity) ?? null;
|
|
3326
3335
|
}
|
|
3327
3336
|
|
|
3328
3337
|
remove(name, arity) {
|
|
3329
3338
|
this.defs.delete(`${name}/${arity}`);
|
|
3339
|
+
const byArity = this.byName.get(name);
|
|
3340
|
+
if (byArity != null) {
|
|
3341
|
+
byArity.delete(arity);
|
|
3342
|
+
if (byArity.size === 0) this.byName.delete(name);
|
|
3343
|
+
}
|
|
3330
3344
|
return this;
|
|
3331
3345
|
}
|
|
3332
3346
|
}
|
package/src/solver.js
CHANGED
|
@@ -3223,13 +3223,13 @@ function rememberGroundChainSuccess(solver, seen) {
|
|
|
3223
3223
|
|
|
3224
3224
|
function rememberMemoAnswer(entry, goal, env) {
|
|
3225
3225
|
const variables = new Map();
|
|
3226
|
-
const
|
|
3227
|
-
const answerArgs = goal.args.
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
}
|
|
3232
|
-
const key =
|
|
3226
|
+
const keyParts = [];
|
|
3227
|
+
const answerArgs = new Array(goal.args.length);
|
|
3228
|
+
for (let position = 0; position < goal.args.length; position++) {
|
|
3229
|
+
if (position > 0) keyParts.push('\x1f');
|
|
3230
|
+
answerArgs[position] = copyResolvedWithKey(goal.args[position], env, variables, keyParts);
|
|
3231
|
+
}
|
|
3232
|
+
const key = keyParts.join('');
|
|
3233
3233
|
if (entry.answerKeys.has(key)) return;
|
|
3234
3234
|
entry.answerKeys.add(key);
|
|
3235
3235
|
const answerIndex = entry.answers.length;
|
|
@@ -3373,7 +3373,13 @@ function canonicalTermKey(term, env, variables) {
|
|
|
3373
3373
|
return key.join('');
|
|
3374
3374
|
}
|
|
3375
3375
|
|
|
3376
|
-
|
|
3376
|
+
// Copies one answer argument and appends its variant key to `keyParts`.
|
|
3377
|
+
// Writing fragments into one shared array keeps a tabled answer linear in its
|
|
3378
|
+
// own size: returning a key per subterm instead made every enclosing term
|
|
3379
|
+
// rebuild the key of everything below it, so a predicate tabling long lists --
|
|
3380
|
+
// a Collatz trajectory, a transitive closure path -- paid quadratic string
|
|
3381
|
+
// building for each answer it recorded.
|
|
3382
|
+
function copyResolvedWithKey(term, env, variables, keyParts) {
|
|
3377
3383
|
const value = derefForLocal(term, env);
|
|
3378
3384
|
if (value.type === 'var') {
|
|
3379
3385
|
let id = variables.get(value.name);
|
|
@@ -3381,19 +3387,24 @@ function copyResolvedWithKey(term, env, variables) {
|
|
|
3381
3387
|
id = variables.size;
|
|
3382
3388
|
variables.set(value.name, id);
|
|
3383
3389
|
}
|
|
3384
|
-
|
|
3390
|
+
keyParts.push('var:', id);
|
|
3391
|
+
return termModuleCache.variable(value.name);
|
|
3385
3392
|
}
|
|
3386
3393
|
if (!value.args?.length) {
|
|
3387
3394
|
// Atomic terms are immutable in the solver. Share them across table
|
|
3388
3395
|
// answers instead of allocating a fresh host object for every cell of a
|
|
3389
3396
|
// large closure such as tc/2.
|
|
3390
|
-
|
|
3397
|
+
keyParts.push(value.type, ':', value.name);
|
|
3398
|
+
return value;
|
|
3391
3399
|
}
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3400
|
+
keyParts.push(value.type, ':', value.name, '(');
|
|
3401
|
+
const children = new Array(value.args.length);
|
|
3402
|
+
for (let index = 0; index < value.args.length; index++) {
|
|
3403
|
+
if (index > 0) keyParts.push(',');
|
|
3404
|
+
children[index] = copyResolvedWithKey(value.args[index], env, variables, keyParts);
|
|
3405
|
+
}
|
|
3406
|
+
keyParts.push(')');
|
|
3407
|
+
return termModuleCache.compound(value.name, children);
|
|
3397
3408
|
}
|
|
3398
3409
|
|
|
3399
3410
|
// Avoid circular import surprises in older Node loaders.
|
package/src/term.js
CHANGED
|
@@ -19,6 +19,10 @@ const ATTRIBUTED_ENV_FLATTEN_DEPTH = 256;
|
|
|
19
19
|
// binding is just as safe to cache as a present one. Negative lookups are
|
|
20
20
|
// especially common while tabling and constraint code probes fresh variables.
|
|
21
21
|
const ENV_UNBOUND = Symbol('environment-unbound');
|
|
22
|
+
// Dereferencing walks a binding chain. Chains are one or two hops in practice
|
|
23
|
+
// and cyclic only when a term was built incorrectly, so the cycle guard starts
|
|
24
|
+
// recording visited names only after a chain is long enough to be suspect.
|
|
25
|
+
const DEREF_CYCLE_GUARD_HOPS = 16;
|
|
22
26
|
// Runtime terms are structurally immutable: environments hold bindings beside
|
|
23
27
|
// them rather than rewriting their argument arrays. Cache only the syntactic
|
|
24
28
|
// variable names; binding reachability is still checked against each Env.
|
|
@@ -885,20 +889,34 @@ class SolverEnv extends Env {
|
|
|
885
889
|
}
|
|
886
890
|
|
|
887
891
|
export function deref(term, env) {
|
|
888
|
-
// Follow variable bindings until a non-variable term is reached.
|
|
889
|
-
//
|
|
892
|
+
// Follow variable bindings until a non-variable term is reached. This is the
|
|
893
|
+
// most frequently executed function in the runtime, so nothing it needs only
|
|
894
|
+
// occasionally may be paid for on every call: most terms are not variables
|
|
895
|
+
// at all, most environments declare no DCG locals, and almost every chain is
|
|
896
|
+
// one or two hops long.
|
|
897
|
+
if (term?.type !== VAR || env == null) return term;
|
|
898
|
+
// A live compiler-proven DCG local is the current unbound representative.
|
|
899
|
+
// No older Env layer can contain a binding for it. Both Env representations
|
|
900
|
+
// answer that question from this one set, so read it once per call instead
|
|
901
|
+
// of dispatching a method call per hop.
|
|
902
|
+
const locals = env._localVariables;
|
|
890
903
|
let current = term;
|
|
891
904
|
let seen = null;
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
const next = env?.get(current.name);
|
|
905
|
+
let hops = 0;
|
|
906
|
+
do {
|
|
907
|
+
if (locals != null && locals.has(current.name)) break;
|
|
908
|
+
const next = env.get(current.name);
|
|
897
909
|
if (next === undefined) break;
|
|
898
|
-
|
|
899
|
-
|
|
910
|
+
// The seen set protects readback from accidental cycles in partially
|
|
911
|
+
// constructed terms. A cycle still terminates the walk once the guard
|
|
912
|
+
// starts recording, so ordinary chains never allocate it.
|
|
913
|
+
if (++hops > DEREF_CYCLE_GUARD_HOPS) {
|
|
914
|
+
if (seen == null) seen = new Set();
|
|
915
|
+
if (seen.has(current.name)) break;
|
|
916
|
+
seen.add(current.name);
|
|
917
|
+
}
|
|
900
918
|
current = next;
|
|
901
|
-
}
|
|
919
|
+
} while (current?.type === VAR);
|
|
902
920
|
return current;
|
|
903
921
|
}
|
|
904
922
|
|
|
@@ -1519,40 +1537,65 @@ export function termSignature(term) {
|
|
|
1519
1537
|
return term?.type === COMPOUND ? `${term.name}/${term.arity}` : null;
|
|
1520
1538
|
}
|
|
1521
1539
|
|
|
1522
|
-
export function variantTerms(left, leftEnv, right, rightEnv, pairs =
|
|
1523
|
-
// Variant checks sit on the recursive-call hot path
|
|
1524
|
-
//
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1540
|
+
export function variantTerms(left, leftEnv, right, rightEnv, pairs = null, reverse = null) {
|
|
1541
|
+
// Variant checks sit on the recursive-call hot path, where the usual answer
|
|
1542
|
+
// is "not a variant" and the usual reason is one mismatched scalar argument.
|
|
1543
|
+
// Settle scalar and variable arguments as they are derefed and defer only
|
|
1544
|
+
// compound ones to an explicit work stack, which long lists still need so
|
|
1545
|
+
// they do not consume the JavaScript call stack. The stack, the variable
|
|
1546
|
+
// pairing, and the cycle guard are therefore allocated only once something
|
|
1547
|
+
// actually needs them, which a rejected comparison usually never does.
|
|
1548
|
+
let leftStack = null;
|
|
1549
|
+
let rightStack = null;
|
|
1550
|
+
let seen = null;
|
|
1551
|
+
let current = deref(left, leftEnv);
|
|
1552
|
+
let other = deref(right, rightEnv);
|
|
1553
|
+
for (;;) {
|
|
1554
|
+
if (current.type === VAR || other.type === VAR) {
|
|
1555
|
+
if (current.type !== VAR || other.type !== VAR) return false;
|
|
1556
|
+
pairs ??= new Map();
|
|
1557
|
+
reverse ??= new Map();
|
|
1558
|
+
const forward = pairs.get(current.name);
|
|
1559
|
+
const backward = reverse.get(other.name);
|
|
1560
|
+
if (forward !== undefined || backward !== undefined) {
|
|
1561
|
+
if (forward !== other.name || backward !== current.name) return false;
|
|
1562
|
+
} else {
|
|
1563
|
+
pairs.set(current.name, other.name);
|
|
1564
|
+
reverse.set(other.name, current.name);
|
|
1565
|
+
}
|
|
1566
|
+
} else if (current.type !== other.type || current.arity !== other.arity) {
|
|
1567
|
+
return false;
|
|
1568
|
+
} else if (current.type === NUMBER ? !sameNumberValue(current.name, other.name) : current.name !== other.name) {
|
|
1569
|
+
return false;
|
|
1570
|
+
} else if (current.type === COMPOUND && seen?.get(current)?.has(other) !== true) {
|
|
1571
|
+
let recorded = false;
|
|
1572
|
+
for (let i = 0; i < current.arity; i++) {
|
|
1573
|
+
const leftArg = deref(current.args[i], leftEnv);
|
|
1574
|
+
const rightArg = deref(other.args[i], rightEnv);
|
|
1575
|
+
if (leftArg.type !== COMPOUND && rightArg.type !== COMPOUND
|
|
1576
|
+
&& leftArg.type !== VAR && rightArg.type !== VAR) {
|
|
1577
|
+
if (leftArg.type !== rightArg.type) return false;
|
|
1578
|
+
if (leftArg.type === NUMBER
|
|
1579
|
+
? !sameNumberValue(leftArg.name, rightArg.name)
|
|
1580
|
+
: leftArg.name !== rightArg.name) return false;
|
|
1581
|
+
continue;
|
|
1582
|
+
}
|
|
1583
|
+
if (!recorded) {
|
|
1584
|
+
recorded = true;
|
|
1585
|
+
let rights = (seen ??= new WeakMap()).get(current);
|
|
1586
|
+
if (rights == null) seen.set(current, rights = new WeakSet());
|
|
1587
|
+
rights.add(other);
|
|
1588
|
+
leftStack ??= [];
|
|
1589
|
+
rightStack ??= [];
|
|
1590
|
+
}
|
|
1591
|
+
leftStack.push(leftArg);
|
|
1592
|
+
rightStack.push(rightArg);
|
|
1536
1593
|
}
|
|
1537
|
-
pairs.set(left.name, right.name);
|
|
1538
|
-
reverse.set(right.name, left.name);
|
|
1539
|
-
continue;
|
|
1540
|
-
}
|
|
1541
|
-
|
|
1542
|
-
if (left.type !== right.type || left.arity !== right.arity) return false;
|
|
1543
|
-
if (left.type === NUMBER ? !sameNumberValue(left.name, right.name) : left.name !== right.name) return false;
|
|
1544
|
-
if (left.type !== COMPOUND) continue;
|
|
1545
|
-
|
|
1546
|
-
let rights = seen.get(left);
|
|
1547
|
-
if (rights?.has(right)) continue;
|
|
1548
|
-
if (rights == null) {
|
|
1549
|
-
rights = new WeakSet();
|
|
1550
|
-
seen.set(left, rights);
|
|
1551
1594
|
}
|
|
1552
|
-
|
|
1553
|
-
|
|
1595
|
+
if (leftStack == null || leftStack.length === 0) return true;
|
|
1596
|
+
current = leftStack.pop();
|
|
1597
|
+
other = rightStack.pop();
|
|
1554
1598
|
}
|
|
1555
|
-
return true;
|
|
1556
1599
|
}
|
|
1557
1600
|
|
|
1558
1601
|
|
|
@@ -6,7 +6,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
6
6
|
import * as publicApi from '../../src/index.js';
|
|
7
7
|
import { Env, Program, atom, compound, copyResolved, flattenConjunction, listFromItems, numberTerm, parseProgramText, properListItems, stringTerm, termIsGround, termToString, unify, variable, variantTerms } from '../../src/index.js';
|
|
8
8
|
import { ISO_OPERATOR_DEFINITIONS, parseGoalText, parseNumberTokenText } from '../../src/parser.js';
|
|
9
|
-
import { compareTerms } from '../../src/term.js';
|
|
9
|
+
import { compareTerms, deref } from '../../src/term.js';
|
|
10
10
|
import { checkProofDocument, verdict } from '../../src/check-proof.js';
|
|
11
11
|
import { formatTermForWrite } from '../../src/write.js';
|
|
12
12
|
import { selectClauseCandidates } from '../../src/program.js';
|
|
@@ -1065,5 +1065,66 @@ path(X, Z) :- edge(X, Y), path(Y, Z).
|
|
|
1065
1065
|
assertIncludes(result.stdout, 'collatzTrajectory(1, [1]).\n', 'stdout');
|
|
1066
1066
|
},
|
|
1067
1067
|
},
|
|
1068
|
+
{
|
|
1069
|
+
// The hot paths below settle the ordinary case without allocating the
|
|
1070
|
+
// traversal state the hard case needs. These cases pin the hard cases:
|
|
1071
|
+
// the state has to appear exactly when the comparison stops being
|
|
1072
|
+
// shallow, acyclic, and decidable on a scalar.
|
|
1073
|
+
name: 'variantTerms separates renaming from sharing and still rejects on scalars',
|
|
1074
|
+
run: () => {
|
|
1075
|
+
const goal = parseGoalText('step(1, X, point(Y, X))');
|
|
1076
|
+
const renamed = parseGoalText('step(1, A, point(B, A))');
|
|
1077
|
+
const shared = parseGoalText('step(1, A, point(B, B))');
|
|
1078
|
+
const otherScalar = parseGoalText('step(2, A, point(B, A))');
|
|
1079
|
+
assertEqual(variantTerms(goal, new Env(), renamed, new Env()), true, 'a consistent renaming is a variant');
|
|
1080
|
+
assertEqual(variantTerms(goal, new Env(), shared, new Env()), false, 'sharing a variable is not a renaming');
|
|
1081
|
+
assertEqual(variantTerms(goal, new Env(), otherScalar, new Env()), false, 'a differing scalar argument rejects');
|
|
1082
|
+
assertEqual(
|
|
1083
|
+
variantTerms(parseGoalText('step(1.0, A)'), new Env(), parseGoalText('step(1, A)'), new Env()),
|
|
1084
|
+
false,
|
|
1085
|
+
'a float and an integer argument stay distinct',
|
|
1086
|
+
);
|
|
1087
|
+
},
|
|
1088
|
+
},
|
|
1089
|
+
{
|
|
1090
|
+
name: 'variantTerms and deref terminate on cyclic bindings',
|
|
1091
|
+
run: () => {
|
|
1092
|
+
const leftEnv = new Env();
|
|
1093
|
+
leftEnv.bind('X', compound('f', [variable('X')]));
|
|
1094
|
+
const rightEnv = new Env();
|
|
1095
|
+
rightEnv.bind('Y', compound('f', [variable('Y')]));
|
|
1096
|
+
assertEqual(variantTerms(variable('X'), leftEnv, variable('Y'), rightEnv), true, 'cyclic terms of one shape are variants');
|
|
1097
|
+
|
|
1098
|
+
const chain = new Env();
|
|
1099
|
+
for (let index = 0; index < 40; index++) chain.bind(`V${index}`, variable(`V${index + 1}`));
|
|
1100
|
+
chain.bind('V40', atom('done'));
|
|
1101
|
+
assertEqual(deref(variable('V0'), chain).name, 'done', 'a forty-hop chain resolves to its value');
|
|
1102
|
+
|
|
1103
|
+
const cyclicChain = new Env();
|
|
1104
|
+
cyclicChain.bind('A', variable('B'));
|
|
1105
|
+
cyclicChain.bind('B', variable('A'));
|
|
1106
|
+
assertEqual(deref(variable('A'), cyclicChain).type, 'var', 'a cyclic chain stops at a variable');
|
|
1107
|
+
},
|
|
1108
|
+
},
|
|
1109
|
+
{
|
|
1110
|
+
name: 'tabled answers separate on a deep list element and still merge on a repeated derivation',
|
|
1111
|
+
run: () => {
|
|
1112
|
+
const program = Program.parse(`
|
|
1113
|
+
:- table suffixed/2.
|
|
1114
|
+
:- table duplicated/1.
|
|
1115
|
+
mark(a).
|
|
1116
|
+
mark(b).
|
|
1117
|
+
chain(0, [E], E).
|
|
1118
|
+
chain(N, [x|T], E) :- N > 0, M is N - 1, chain(M, T, E).
|
|
1119
|
+
suffixed(E, L) :- mark(E), chain(40, L, E).
|
|
1120
|
+
duplicated(L) :- chain(40, L, a).
|
|
1121
|
+
duplicated(L) :- chain(40, L, a).
|
|
1122
|
+
`);
|
|
1123
|
+
const distinct = run(program, { goal: 'suffixed(E, L)' });
|
|
1124
|
+
assertEqual(distinct.stdout.trim().split('\n').length, 2, 'answers differing only past forty cells stay distinct');
|
|
1125
|
+
const merged = run(program, { goal: 'duplicated(L)' });
|
|
1126
|
+
assertEqual(merged.stdout.trim().split('\n').length, 1, 'one answer derived twice is tabled once');
|
|
1127
|
+
},
|
|
1128
|
+
},
|
|
1068
1129
|
];
|
|
1069
1130
|
}
|