eyeprolog 1.2.22 → 1.2.24
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/package.json +1 -1
- package/src/iso.js +20 -18
- package/src/number-value.js +28 -0
- package/src/parser.js +28 -0
- package/src/program.js +45 -15
- package/src/solver.js +173 -7
- package/src/term.js +6 -3
- package/test/run-regression.mjs +106 -3
- package/the-art-of-eyeprolog.md +20 -3
package/package.json
CHANGED
package/src/iso.js
CHANGED
|
@@ -5,7 +5,8 @@ import {
|
|
|
5
5
|
isDecimalInteger, listFromItems, numberTerm, numberTextFromDouble,
|
|
6
6
|
properListItems, termIsGround, termToString, unify, variable, variantTerms,
|
|
7
7
|
} from './term.js';
|
|
8
|
-
import {
|
|
8
|
+
import { sameNumberValue } from './number-value.js';
|
|
9
|
+
import { createParserOperatorState, parseClauses, parseGoalText, parseNumberTokenText } from './parser.js';
|
|
9
10
|
import { formatTermForWrite } from './write.js';
|
|
10
11
|
import { emptyTerminalSequence, expandDcgBody, isListOrPartialList, validateDcgEmbeddedGoals } from './dcg.js';
|
|
11
12
|
|
|
@@ -220,7 +221,8 @@ function subsumesTerm(general, specific, env) {
|
|
|
220
221
|
}
|
|
221
222
|
continue;
|
|
222
223
|
}
|
|
223
|
-
if (left.type !== right.type || left.
|
|
224
|
+
if (left.type !== right.type || left.arity !== right.arity) return false;
|
|
225
|
+
if (left.type === NUMBER ? !sameNumberValue(left.name, right.name) : left.name !== right.name) return false;
|
|
224
226
|
for (let i = left.arity - 1; i >= 0; i--) pending.push([left.args[i], right.args[i]]);
|
|
225
227
|
}
|
|
226
228
|
return true;
|
|
@@ -239,7 +241,8 @@ function* nonIdentity({ goal, env }) {
|
|
|
239
241
|
function identical(left, right, env) {
|
|
240
242
|
left = deref(left, env);
|
|
241
243
|
right = deref(right, env);
|
|
242
|
-
if (left.type !== right.type || left.
|
|
244
|
+
if (left.type !== right.type || left.arity !== right.arity) return false;
|
|
245
|
+
if (left.type === NUMBER ? !sameNumberValue(left.name, right.name) : left.name !== right.name) return false;
|
|
243
246
|
if (left.type === VAR) return left.name === right.name;
|
|
244
247
|
for (let i = 0; i < left.arity; i++) if (!identical(left.args[i], right.args[i], env)) return false;
|
|
245
248
|
return true;
|
|
@@ -1549,9 +1552,7 @@ function quotedNumberSign(text, start) {
|
|
|
1549
1552
|
}
|
|
1550
1553
|
|
|
1551
1554
|
function parseIsoNumber(text) {
|
|
1552
|
-
|
|
1553
|
-
// with the numeric token itself rather than trailing layout text.
|
|
1554
|
-
if (text.length === 0 || /[\u0009-\u000d\u0020]$/.test(text)) return null;
|
|
1555
|
+
if (text.length === 0) return null;
|
|
1555
1556
|
let position = skipNumberLayout(text, 0);
|
|
1556
1557
|
let sign = '';
|
|
1557
1558
|
|
|
@@ -1587,10 +1588,7 @@ function parseIsoNumber(text) {
|
|
|
1587
1588
|
// ISO floating-point syntax requires a decimal fraction before an exponent.
|
|
1588
1589
|
if (/^-?\d+[eE][+-]?\d+$/.test(numericText)) return null;
|
|
1589
1590
|
try {
|
|
1590
|
-
const
|
|
1591
|
-
if (parsed.type !== COMPOUND || parsed.name !== 'number_chars_value' ||
|
|
1592
|
-
parsed.arity !== 1 || parsed.args[0].type !== NUMBER) return null;
|
|
1593
|
-
const value = parsed.args[0];
|
|
1591
|
+
const value = parseNumberTokenText(numericText);
|
|
1594
1592
|
if (isDecimalInteger(value.name)) return numberTerm(BigInt(value.name).toString());
|
|
1595
1593
|
const finite = Number(value.name);
|
|
1596
1594
|
if (!Number.isFinite(finite)) return null;
|
|
@@ -1601,14 +1599,18 @@ function parseIsoNumber(text) {
|
|
|
1601
1599
|
}
|
|
1602
1600
|
|
|
1603
1601
|
function sameNumber(left, right) {
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1602
|
+
return sameNumberValue(left.name, right.name);
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
function canonicalNumberText(value) {
|
|
1606
|
+
if (isDecimalInteger(value.name)) return BigInt(value.name).toString();
|
|
1607
|
+
const finite = Number(value.name);
|
|
1608
|
+
if (Number.isFinite(finite) && /^-?\d+\.\d+(?:[eE][+-]?\d+)?$/.test(value.name)) {
|
|
1609
|
+
return value.name;
|
|
1608
1610
|
}
|
|
1609
|
-
const
|
|
1610
|
-
|
|
1611
|
-
return
|
|
1611
|
+
const text = numberTextFromDouble(finite);
|
|
1612
|
+
if (text == null) throw new PrologError('representation_error(max_float)');
|
|
1613
|
+
return text;
|
|
1612
1614
|
}
|
|
1613
1615
|
|
|
1614
1616
|
function numberListText(list, env, kind, valueIsBound) {
|
|
@@ -1650,7 +1652,7 @@ function numberListBuiltin(kind) {
|
|
|
1650
1652
|
if (sameNumber(value, parsed)) yield next;
|
|
1651
1653
|
return;
|
|
1652
1654
|
}
|
|
1653
|
-
const items = characters(value
|
|
1655
|
+
const items = characters(canonicalNumberText(value)).map((ch) =>
|
|
1654
1656
|
kind === 'chars' ? atom(ch) : numberTerm(ch.codePointAt(0)));
|
|
1655
1657
|
if (unify(goal.args[1], listFromItems(items), next)) yield next;
|
|
1656
1658
|
return;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Numeric value identity shared by term semantics and scalar indexes. Keep
|
|
2
|
+
// integer and float terms distinct while ignoring insignificant spelling
|
|
3
|
+
// differences within either ISO numeric type.
|
|
4
|
+
const decimalInteger = (text) => /^-?\d+$/.test(text ?? '');
|
|
5
|
+
|
|
6
|
+
const finiteFloat = (text) => {
|
|
7
|
+
if (!/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/.test(text ?? '')) return null;
|
|
8
|
+
const value = Number(text);
|
|
9
|
+
return Number.isFinite(value) ? value : null;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function sameNumberValue(left, right) {
|
|
13
|
+
const leftInteger = decimalInteger(left);
|
|
14
|
+
const rightInteger = decimalInteger(right);
|
|
15
|
+
if (leftInteger || rightInteger) {
|
|
16
|
+
return leftInteger && rightInteger && BigInt(left) === BigInt(right);
|
|
17
|
+
}
|
|
18
|
+
const leftValue = finiteFloat(left);
|
|
19
|
+
const rightValue = finiteFloat(right);
|
|
20
|
+
return leftValue != null && rightValue != null && leftValue === rightValue;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function numberValueKey(text) {
|
|
24
|
+
if (decimalInteger(text)) return `integer:${BigInt(text)}`;
|
|
25
|
+
const value = finiteFloat(text);
|
|
26
|
+
if (value == null) return `invalid:${text}`;
|
|
27
|
+
return `float:${Object.is(value, -0) ? 0 : value}`;
|
|
28
|
+
}
|
package/src/parser.js
CHANGED
|
@@ -433,6 +433,18 @@ class Parser {
|
|
|
433
433
|
this.take();
|
|
434
434
|
this.take();
|
|
435
435
|
let value = this.take();
|
|
436
|
+
if (value) {
|
|
437
|
+
const firstCode = value.charCodeAt(0);
|
|
438
|
+
if (firstCode >= 0xd800 && firstCode <= 0xdbff) {
|
|
439
|
+
const secondCode = this.peek().charCodeAt(0);
|
|
440
|
+
if (secondCode < 0xdc00 || secondCode > 0xdfff) {
|
|
441
|
+
throw new Error(`parse line ${line}: bad character code constant`);
|
|
442
|
+
}
|
|
443
|
+
value += this.take();
|
|
444
|
+
} else if (firstCode >= 0xdc00 && firstCode <= 0xdfff) {
|
|
445
|
+
throw new Error(`parse line ${line}: bad character code constant`);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
436
448
|
if (!value || (value !== ' ' && isWhitespaceCode(value.charCodeAt(0)))) {
|
|
437
449
|
throw new Error(`parse line ${line}: bad character code constant`);
|
|
438
450
|
}
|
|
@@ -1294,6 +1306,22 @@ export function parseProgramText(source, options = {}) {
|
|
|
1294
1306
|
return parseClauses(source, options);
|
|
1295
1307
|
}
|
|
1296
1308
|
|
|
1309
|
+
export function parseNumberTokenText(text) {
|
|
1310
|
+
const source = String(text ?? '');
|
|
1311
|
+
const parser = new Parser(source, {
|
|
1312
|
+
includeDefaultOperators: false,
|
|
1313
|
+
sourceMetadata: false,
|
|
1314
|
+
});
|
|
1315
|
+
// Inspect the tokenizer position before requesting another token: advancing
|
|
1316
|
+
// would skip trailing layout and make `3 ` or `3/**/` look complete. A
|
|
1317
|
+
// literal space consumed by the character-code token `0' ` is already part
|
|
1318
|
+
// of parser.pos and is therefore correctly accepted.
|
|
1319
|
+
if (parser.token.type !== TOK.NUMBER || parser.pos !== source.length) {
|
|
1320
|
+
throw new Error('not exactly one number token');
|
|
1321
|
+
}
|
|
1322
|
+
return numberTerm(parser.token.text);
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1297
1325
|
export function parseGoalText(text, options = {}) {
|
|
1298
1326
|
const clauses = parseClauses(`zz_goal((${text})).`, options);
|
|
1299
1327
|
const head = clauses[0]?.head;
|
package/src/program.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Program representation and clause indexing.
|
|
2
2
|
// Indexes are deliberately conservative: they speed up common scalar arguments but never replace unification as the final check.
|
|
3
3
|
import { ATOM, COMPOUND, VAR, Env, atom, compound, deref, flattenConjunction, isScalar, numberTerm, properListItems, termToString, variable } from './term.js';
|
|
4
|
+
import { numberValueKey } from './number-value.js';
|
|
4
5
|
import { formatTermForWrite } from './write.js';
|
|
5
6
|
import {
|
|
6
7
|
ISO_OPERATOR_DEFINITIONS,
|
|
@@ -158,6 +159,9 @@ export class Program {
|
|
|
158
159
|
scalarFactsOnly: true,
|
|
159
160
|
dynamic: this.dynamicPredicates.has(modulePredicateKey(module, name, arity)),
|
|
160
161
|
negationStratum: null,
|
|
162
|
+
hasCut: false,
|
|
163
|
+
cutReachable: null,
|
|
164
|
+
bundledLibrary: false,
|
|
161
165
|
};
|
|
162
166
|
return group;
|
|
163
167
|
}
|
|
@@ -185,6 +189,7 @@ export class Program {
|
|
|
185
189
|
group.rejectedDemandIndexes.clear();
|
|
186
190
|
}
|
|
187
191
|
group.clauses.push(clause);
|
|
192
|
+
if (clauseHasCut(clause)) group.hasCut = true;
|
|
188
193
|
const clausePosition = group.clauses.length - 1;
|
|
189
194
|
for (let i = 0; i < head.arity; i++) indexOne(group.argIndexes[i], head.args[i], clause, group.clauses, clausePosition);
|
|
190
195
|
}
|
|
@@ -291,6 +296,7 @@ export class Program {
|
|
|
291
296
|
const groups = [...this.groups.values()];
|
|
292
297
|
const indexByGroup = new Map(groups.map((group, i) => [group, i]));
|
|
293
298
|
const deps = groups.map(() => new Set());
|
|
299
|
+
const cutDeps = groups.map(() => new Set());
|
|
294
300
|
const negativeEdges = [];
|
|
295
301
|
for (const group of groups) {
|
|
296
302
|
const groupIndex = indexByGroup.get(group);
|
|
@@ -298,11 +304,18 @@ export class Program {
|
|
|
298
304
|
if (isCompactBinaryClause(clause)) {
|
|
299
305
|
if (clause.bodyName != null) {
|
|
300
306
|
const dep = this.findGroup(clause.bodyName, 2, group.module);
|
|
301
|
-
if (dep)
|
|
307
|
+
if (dep) {
|
|
308
|
+
deps[groupIndex].add(indexByGroup.get(dep));
|
|
309
|
+
cutDeps[groupIndex].add(indexByGroup.get(dep));
|
|
310
|
+
}
|
|
302
311
|
}
|
|
303
312
|
continue;
|
|
304
313
|
}
|
|
305
314
|
for (const goal of clause.body) {
|
|
315
|
+
for (const dependency of collectGoalDependencies(goal, false, true)) {
|
|
316
|
+
const dep = this.findGroup(dependency.name, dependency.arity, dependency.module ?? group.module);
|
|
317
|
+
if (dep) cutDeps[groupIndex].add(indexByGroup.get(dep));
|
|
318
|
+
}
|
|
306
319
|
const directKey = directGoalDependencyKey(goal);
|
|
307
320
|
if (directKey) {
|
|
308
321
|
const dep = this.findGroup(goal.name, goal.arity, goal.module ?? group.module);
|
|
@@ -324,6 +337,8 @@ export class Program {
|
|
|
324
337
|
const start = indexByGroup.get(group);
|
|
325
338
|
const standardLibraryModule = group.module !== 'user' &&
|
|
326
339
|
this.modules.get(group.module)?.filename?.startsWith('src/lib/');
|
|
340
|
+
group.bundledLibrary = standardLibraryModule === true;
|
|
341
|
+
group.cutReachable = [...reachableIndexes(start, cutDeps)].some((index) => groups[index].hasCut);
|
|
327
342
|
const seen = new Set();
|
|
328
343
|
const stack = [start];
|
|
329
344
|
let recursive = false;
|
|
@@ -532,6 +547,7 @@ class ProgramBuilder {
|
|
|
532
547
|
group.clauses.push(clause);
|
|
533
548
|
clause.groundHead = termHasNoVariables(head);
|
|
534
549
|
clause.scalarHead = head.type === COMPOUND && head.args.every(isScalar);
|
|
550
|
+
if (clauseHasCut(clause)) group.hasCut = true;
|
|
535
551
|
if (clause.body.length !== 0 || !clause.scalarHead) group.scalarFactsOnly = false;
|
|
536
552
|
for (let i = 0; i < head.arity; i++) {
|
|
537
553
|
indexOne(group.argIndexes[i], head.args[i], clause, group.clauses, clausePosition);
|
|
@@ -1113,33 +1129,41 @@ function directGoalDependencyKey(goal) {
|
|
|
1113
1129
|
return `${goal.name}/${goal.arity}`;
|
|
1114
1130
|
}
|
|
1115
1131
|
|
|
1116
|
-
function collectGoalDependencies(goal, negated) {
|
|
1132
|
+
function collectGoalDependencies(goal, negated, traverseConditionals = false) {
|
|
1117
1133
|
if (goal.type === ATOM) return [{ key: `${goal.name}/0`, name: goal.name, arity: 0, module: goal.module, negative: negated }];
|
|
1118
1134
|
if (goal.type !== COMPOUND) return [];
|
|
1119
1135
|
if (goal.name === ',' && goal.arity === 2) {
|
|
1120
1136
|
return [
|
|
1121
|
-
...collectGoalDependencies(goal.args[0], negated),
|
|
1122
|
-
...collectGoalDependencies(goal.args[1], negated),
|
|
1137
|
+
...collectGoalDependencies(goal.args[0], negated, traverseConditionals),
|
|
1138
|
+
...collectGoalDependencies(goal.args[1], negated, traverseConditionals),
|
|
1139
|
+
];
|
|
1140
|
+
}
|
|
1141
|
+
if (traverseConditionals && (goal.name === ';' || goal.name === '->') && goal.arity === 2) {
|
|
1142
|
+
return [
|
|
1143
|
+
...collectGoalDependencies(goal.args[0], negated, true),
|
|
1144
|
+
...collectGoalDependencies(goal.args[1], negated, true),
|
|
1123
1145
|
];
|
|
1124
1146
|
}
|
|
1125
1147
|
if ((goal.name === '\\+' || goal.name === 'not') && goal.arity === 1) {
|
|
1126
|
-
return collectGoalDependencies(goal.args[0], !negated);
|
|
1148
|
+
return collectGoalDependencies(goal.args[0], !negated, traverseConditionals);
|
|
1127
1149
|
}
|
|
1128
1150
|
if (goal.name === 'once' && goal.arity === 1) {
|
|
1129
|
-
return collectGoalDependencies(goal.args[0], negated);
|
|
1151
|
+
return collectGoalDependencies(goal.args[0], negated, traverseConditionals);
|
|
1130
1152
|
}
|
|
1131
1153
|
if (goal.name === 'forall' && goal.arity === 2) {
|
|
1132
1154
|
return [
|
|
1133
|
-
...collectGoalDependencies(goal.args[0], negated),
|
|
1134
|
-
...collectGoalDependencies(goal.args[1], negated),
|
|
1155
|
+
...collectGoalDependencies(goal.args[0], negated, traverseConditionals),
|
|
1156
|
+
...collectGoalDependencies(goal.args[1], negated, traverseConditionals),
|
|
1135
1157
|
];
|
|
1136
1158
|
}
|
|
1137
1159
|
if ((goal.name === 'findall' || goal.name === 'sumall') && goal.arity === 3) {
|
|
1138
|
-
return collectGoalDependencies(goal.args[1], negated);
|
|
1160
|
+
return collectGoalDependencies(goal.args[1], negated, traverseConditionals);
|
|
1161
|
+
}
|
|
1162
|
+
if (goal.name === 'countall' && goal.arity === 2) {
|
|
1163
|
+
return collectGoalDependencies(goal.args[0], negated, traverseConditionals);
|
|
1139
1164
|
}
|
|
1140
|
-
if (goal.name === 'countall' && goal.arity === 2) return collectGoalDependencies(goal.args[0], negated);
|
|
1141
1165
|
if ((goal.name === 'aggregate_min' || goal.name === 'aggregate_max') && goal.arity === 5) {
|
|
1142
|
-
return collectGoalDependencies(goal.args[2], negated);
|
|
1166
|
+
return collectGoalDependencies(goal.args[2], negated, traverseConditionals);
|
|
1143
1167
|
}
|
|
1144
1168
|
return [{ key: `${goal.name}/${goal.arity}`, name: goal.name, arity: goal.arity, module: goal.module, negative: negated }];
|
|
1145
1169
|
}
|
|
@@ -1238,15 +1262,19 @@ function scalarBuckets(index, term) {
|
|
|
1238
1262
|
}
|
|
1239
1263
|
|
|
1240
1264
|
function argumentBucket(index, term) {
|
|
1241
|
-
return scalarBuckets(index, term).get(term.name) ?? null;
|
|
1265
|
+
return scalarBuckets(index, term).get(scalarBucketKey(term.type, term.name)) ?? null;
|
|
1242
1266
|
}
|
|
1243
1267
|
|
|
1244
1268
|
function addArgumentBucket(index, term, clause) {
|
|
1245
|
-
addClauseBucket(scalarBuckets(index, term), term.name, clause);
|
|
1269
|
+
addClauseBucket(scalarBuckets(index, term), scalarBucketKey(term.type, term.name), clause);
|
|
1246
1270
|
}
|
|
1247
1271
|
|
|
1248
1272
|
function scalarIndexKey(term) {
|
|
1249
|
-
return `${term.type}\u0000${term.name}`;
|
|
1273
|
+
return `${term.type}\u0000${scalarBucketKey(term.type, term.name)}`;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
function scalarBucketKey(type, name) {
|
|
1277
|
+
return type === 'number' ? numberValueKey(name) : name;
|
|
1250
1278
|
}
|
|
1251
1279
|
|
|
1252
1280
|
function addClauseBucket(buckets, key, clause) {
|
|
@@ -1276,7 +1304,7 @@ function indexCompactOne(index, type, name, clause, clauses = null, clausePositi
|
|
|
1276
1304
|
index.sawScalar = true;
|
|
1277
1305
|
if (clauses && clausePosition > 0) index.fallback = clauses.slice(0, clausePosition);
|
|
1278
1306
|
}
|
|
1279
|
-
addClauseBucket(compactScalarBuckets(index, type), name, clause);
|
|
1307
|
+
addClauseBucket(compactScalarBuckets(index, type), scalarBucketKey(type, name), clause);
|
|
1280
1308
|
} else if (index.sawScalar) {
|
|
1281
1309
|
index.fallback.push(clause);
|
|
1282
1310
|
}
|
|
@@ -1303,6 +1331,7 @@ function rebuildGroupIndexes(group) {
|
|
|
1303
1331
|
group.demandIndexes.clear();
|
|
1304
1332
|
group.rejectedDemandIndexes.clear();
|
|
1305
1333
|
group.scalarFactsOnly = true;
|
|
1334
|
+
group.hasCut = false;
|
|
1306
1335
|
for (let clausePosition = 0; clausePosition < group.clauses.length; clausePosition++) {
|
|
1307
1336
|
const clause = group.clauses[clausePosition];
|
|
1308
1337
|
if (isCompactBinaryClause(clause)) {
|
|
@@ -1315,6 +1344,7 @@ function rebuildGroupIndexes(group) {
|
|
|
1315
1344
|
}
|
|
1316
1345
|
clause.groundHead = termHasNoVariables(clause.head);
|
|
1317
1346
|
clause.scalarHead = clause.head.type === COMPOUND && clause.head.args.every(isScalar);
|
|
1347
|
+
if (clauseHasCut(clause)) group.hasCut = true;
|
|
1318
1348
|
if (clause.body.length !== 0 || !clause.scalarHead) group.scalarFactsOnly = false;
|
|
1319
1349
|
for (let i = 0; i < group.arity; i++) indexOne(group.argIndexes[i], clause.head.args[i], clause, group.clauses, clausePosition);
|
|
1320
1350
|
}
|
package/src/solver.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
// Depth-first EyeProlog solver with builtin dispatch, memoization, and guarded recursion handling.
|
|
2
2
|
// Most semantic decisions still flow through unification; optimizations only select candidates earlier.
|
|
3
3
|
import {
|
|
4
|
-
COMPOUND, Env, compound, copyResolved, deref, emptyList,
|
|
5
|
-
|
|
4
|
+
COMPOUND, NUMBER, VAR, Env, compound, cons, copyResolved, deref, emptyList,
|
|
5
|
+
flattenConjunction, freshTerm, isCons, isDecimalInteger, isEmptyList,
|
|
6
|
+
numberTerm, numberTextFromDouble, termIsGround, termToString, unify, variable, variantTerms,
|
|
6
7
|
} from './term.js';
|
|
8
|
+
import { sameNumberValue } from './number-value.js';
|
|
7
9
|
import { PrologError, getStrictIsoRegistry } from './iso.js';
|
|
8
10
|
import { getEyePrologRegistry } from './standard-library.js';
|
|
9
11
|
import { selectClauseCandidates, selectClauseCandidatesForValues, selectGroundClauseCandidates } from './program.js';
|
|
@@ -366,6 +368,23 @@ export class Solver {
|
|
|
366
368
|
}
|
|
367
369
|
qualifyMetaArguments(goal, group);
|
|
368
370
|
|
|
371
|
+
const lengthIterator = prologueLengthIterator(this, group, goal, env);
|
|
372
|
+
if (lengthIterator != null) {
|
|
373
|
+
const firstResult = lengthIterator.next();
|
|
374
|
+
if (firstResult.done) break;
|
|
375
|
+
stack.push({
|
|
376
|
+
kind: 'resumeBuiltin',
|
|
377
|
+
iterator: lengthIterator,
|
|
378
|
+
goals: rest,
|
|
379
|
+
depth: depth + 1,
|
|
380
|
+
active,
|
|
381
|
+
});
|
|
382
|
+
goals = rest;
|
|
383
|
+
env = firstResult.value;
|
|
384
|
+
depth++;
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
|
|
369
388
|
if (group.tabled) {
|
|
370
389
|
const key = memoKey(goal, env, group);
|
|
371
390
|
if (key.hasBound) {
|
|
@@ -417,8 +436,8 @@ export class Solver {
|
|
|
417
436
|
return activeVariantIn(goal, env, this.active);
|
|
418
437
|
}
|
|
419
438
|
|
|
420
|
-
checkMemoryLimit() {
|
|
421
|
-
if (this.inferences < this.nextMemoryCheck) return;
|
|
439
|
+
checkMemoryLimit(force = false) {
|
|
440
|
+
if (!force && this.inferences < this.nextMemoryCheck) return;
|
|
422
441
|
this.nextMemoryCheck = this.inferences + 256;
|
|
423
442
|
if (!Number.isFinite(this.maxMemoryBytes)) return;
|
|
424
443
|
const used = usedHeapSize();
|
|
@@ -476,6 +495,12 @@ export class Solver {
|
|
|
476
495
|
if (!unify(goal, freshHead, next)) continue;
|
|
477
496
|
if (freshBody.length === 0) {
|
|
478
497
|
yield* this.solve(rest, next, depth + 1);
|
|
498
|
+
} else if (!groupNeedsActiveFrame(group)) {
|
|
499
|
+
for (const bodyEnv of this.solve(freshBody, next, depth + 1)) {
|
|
500
|
+
if (this.solutionsSeen > 0) this.solutionsSeen--;
|
|
501
|
+
yield* this.solve(rest, bodyEnv, depth + 1);
|
|
502
|
+
if (this.solutionsSeen >= this.solutionLimit) break;
|
|
503
|
+
}
|
|
479
504
|
} else {
|
|
480
505
|
yield* this.solveRuleBodyThenRest(goal, env, freshBody, rest, next, depth);
|
|
481
506
|
}
|
|
@@ -612,7 +637,11 @@ function pushUserGoalUncachedFrames(stack, solver, group, goal, rest, env, depth
|
|
|
612
637
|
const candidates = selectClauseCandidates(group, goal, env);
|
|
613
638
|
const frames = [];
|
|
614
639
|
const invocation = { goal, env };
|
|
615
|
-
|
|
640
|
+
// Active frames serve two purposes: they delimit cut and detect variants in
|
|
641
|
+
// recursive user predicates. Cut-free, non-recursive library helpers need
|
|
642
|
+
// neither. Copying their full active stack at every recursive step made
|
|
643
|
+
// otherwise linear relations such as length/2 retain O(depth^2) references.
|
|
644
|
+
const guarded = groupNeedsActiveFrame(group);
|
|
616
645
|
const release = guarded ? [{ kind: 'releaseActive' }] : [];
|
|
617
646
|
const nextActive = guarded ? [...active, invocation] : active;
|
|
618
647
|
for (const pass of [candidates.primary, candidates.fallback]) {
|
|
@@ -662,6 +691,141 @@ function pushUserGoalUncachedFrames(stack, solver, group, goal, rest, env, depth
|
|
|
662
691
|
for (let i = frames.length - 1; i >= 0; i--) stack.push(frames[i]);
|
|
663
692
|
}
|
|
664
693
|
|
|
694
|
+
function groupNeedsActiveFrame(group) {
|
|
695
|
+
// User code may observe the surrounding control context through later cuts,
|
|
696
|
+
// so only apply this planning shortcut to the fixed bundled-library graph.
|
|
697
|
+
if (group.bundledLibrary !== true) return true;
|
|
698
|
+
// A frame is also required above a cut-bearing callee. The disjunction
|
|
699
|
+
// builtin uses the caller marker to distinguish a callee-local cut from a
|
|
700
|
+
// cut in its own branch. null means dependency analysis was intentionally
|
|
701
|
+
// disabled (strict mode or a newly mutated group), so remain conservative.
|
|
702
|
+
return group.cutReachable !== false || (group.recursive && !group.linearNumeric);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function prologueLengthIterator(solver, group, goal, env) {
|
|
706
|
+
if (solver.registry.eyePrologLibrary !== true ||
|
|
707
|
+
group.module !== 'prologue' || group.name !== 'length' || group.arity !== 2 ||
|
|
708
|
+
group.bundledLibrary !== true || group.clauses.length !== 2) {
|
|
709
|
+
return null;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// Delayed and constrained variables need the ordinary solver's wake-up
|
|
713
|
+
// points. The fast path is deliberately limited to plain finite-tree terms.
|
|
714
|
+
if (env._clpz != null) return null;
|
|
715
|
+
const length = deref(goal.args[1], env);
|
|
716
|
+
if (length.type === VAR && env._delays?.has(length.name)) return null;
|
|
717
|
+
|
|
718
|
+
let cursor = deref(goal.args[0], env);
|
|
719
|
+
while (isCons(cursor)) {
|
|
720
|
+
cursor = deref(cursor.args[1], env);
|
|
721
|
+
}
|
|
722
|
+
if (cursor.type === VAR) {
|
|
723
|
+
if (env._delays?.has(cursor.name)) return null;
|
|
724
|
+
if (length.type === VAR && cursor.name === length.name) return null;
|
|
725
|
+
}
|
|
726
|
+
return prologueLengthSolutions(solver, goal, env);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function* prologueLengthSolutions(solver, goal, env) {
|
|
730
|
+
const requestedLength = deref(goal.args[1], env);
|
|
731
|
+
if (requestedLength.type !== VAR) {
|
|
732
|
+
if (requestedLength.type !== NUMBER || !isDecimalInteger(requestedLength.name)) {
|
|
733
|
+
throw new PrologError('type_error(integer)', requestedLength);
|
|
734
|
+
}
|
|
735
|
+
const length = BigInt(requestedLength.name);
|
|
736
|
+
if (length < 0n) throw new PrologError('domain_error(not_less_than_zero)', requestedLength);
|
|
737
|
+
yield* fixedLengthSolutions(solver, goal.args[0], length, env);
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
yield* generatedLengthSolutions(solver, goal.args[0], goal.args[1], env);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function* fixedLengthSolutions(solver, list, length, env) {
|
|
745
|
+
let cursor = deref(list, env);
|
|
746
|
+
let remaining = length;
|
|
747
|
+
let steps = 0n;
|
|
748
|
+
while (isCons(cursor)) {
|
|
749
|
+
if (remaining === 0n) return;
|
|
750
|
+
remaining--;
|
|
751
|
+
cursor = deref(cursor.args[1], env);
|
|
752
|
+
lengthAllocationCheckpoint(solver, ++steps);
|
|
753
|
+
}
|
|
754
|
+
if (isEmptyList(cursor)) {
|
|
755
|
+
if (remaining === 0n) yield env;
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
if (cursor.type !== VAR) return;
|
|
759
|
+
|
|
760
|
+
// A source-level anonymous variable occurs nowhere else, so materializing
|
|
761
|
+
// its list cannot affect any subsequent goal or answer substitution.
|
|
762
|
+
if (isAnonymousVariable(cursor)) {
|
|
763
|
+
yield env;
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
const id = nextFreshId();
|
|
768
|
+
let suffix = emptyList();
|
|
769
|
+
for (let index = 0n; index < remaining; index++) {
|
|
770
|
+
suffix = cons(variable(`__length${id}_${index}`), suffix);
|
|
771
|
+
lengthAllocationCheckpoint(solver, ++steps);
|
|
772
|
+
}
|
|
773
|
+
const next = env.clone();
|
|
774
|
+
solver.stats.unify_calls++;
|
|
775
|
+
if (unify(cursor, suffix, next)) yield next;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function* generatedLengthSolutions(solver, list, length, env) {
|
|
779
|
+
let cursor = deref(list, env);
|
|
780
|
+
let count = 0n;
|
|
781
|
+
let steps = 0n;
|
|
782
|
+
while (isCons(cursor)) {
|
|
783
|
+
count++;
|
|
784
|
+
cursor = deref(cursor.args[1], env);
|
|
785
|
+
lengthAllocationCheckpoint(solver, ++steps);
|
|
786
|
+
}
|
|
787
|
+
if (isEmptyList(cursor)) {
|
|
788
|
+
const next = bindGeneratedLength(solver, length, count, env);
|
|
789
|
+
if (next != null) yield next;
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
if (cursor.type !== VAR) return;
|
|
793
|
+
|
|
794
|
+
if (isAnonymousVariable(cursor)) {
|
|
795
|
+
for (let value = count; ; value++) {
|
|
796
|
+
const next = bindGeneratedLength(solver, length, value, env);
|
|
797
|
+
if (next != null) yield next;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
const id = nextFreshId();
|
|
802
|
+
let suffix = emptyList();
|
|
803
|
+
for (let extra = 0n; ; extra++) {
|
|
804
|
+
const next = env.clone();
|
|
805
|
+
solver.stats.unify_calls++;
|
|
806
|
+
if (unify(cursor, suffix, next)) {
|
|
807
|
+
const answer = bindGeneratedLength(solver, length, count + extra, next);
|
|
808
|
+
if (answer != null) yield answer;
|
|
809
|
+
}
|
|
810
|
+
suffix = cons(variable(`__length${id}_${extra}`), suffix);
|
|
811
|
+
lengthAllocationCheckpoint(solver, ++steps);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
function bindGeneratedLength(solver, length, value, env) {
|
|
816
|
+
const next = env.clone();
|
|
817
|
+
solver.stats.unify_calls++;
|
|
818
|
+
return unify(length, numberTerm(value), next) ? next : null;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function isAnonymousVariable(term) {
|
|
822
|
+
return term.type === VAR && term.name.startsWith('__anon');
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
function lengthAllocationCheckpoint(solver, steps) {
|
|
826
|
+
if ((steps & 255n) === 0n) solver.checkMemoryLimit(true);
|
|
827
|
+
}
|
|
828
|
+
|
|
665
829
|
function pushFastPiFrames(stack, goal, rest, env, depth, active) {
|
|
666
830
|
const values = goal.args.map((arg) => deref(arg, env));
|
|
667
831
|
if ([0, 1, 2, 4].some((index) => values[index].type !== 'number')) return false;
|
|
@@ -1089,11 +1253,13 @@ function isScalarTerm(term) {
|
|
|
1089
1253
|
}
|
|
1090
1254
|
|
|
1091
1255
|
function sameScalarTerm(left, right) {
|
|
1092
|
-
return isScalarTerm(left) && isScalarTerm(right) && left.type === right.type &&
|
|
1256
|
+
return isScalarTerm(left) && isScalarTerm(right) && left.type === right.type &&
|
|
1257
|
+
(left.type === 'number' ? sameNumberValue(left.name, right.name) : left.name === right.name);
|
|
1093
1258
|
}
|
|
1094
1259
|
|
|
1095
1260
|
function sameGroundTerm(left, right) {
|
|
1096
|
-
if (left?.type !== right?.type
|
|
1261
|
+
if (left?.type !== right?.type) return false;
|
|
1262
|
+
if (left?.type === 'number' ? !sameNumberValue(left.name, right.name) : left?.name !== right?.name) return false;
|
|
1097
1263
|
const arity = left.args?.length ?? 0;
|
|
1098
1264
|
if (arity !== (right.args?.length ?? 0)) return false;
|
|
1099
1265
|
for (let i = 0; i < arity; i++) if (!sameGroundTerm(left.args[i], right.args[i])) return false;
|
package/src/term.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// Term model, environments, unification, readback, and ordering helpers.
|
|
2
|
-
//
|
|
2
|
+
// Keep dependencies minimal because nearly every other module imports this file.
|
|
3
|
+
import { sameNumberValue } from './number-value.js';
|
|
4
|
+
|
|
3
5
|
export const VAR = 'var';
|
|
4
6
|
export const ATOM = 'atom';
|
|
5
7
|
export const STRING = 'string';
|
|
@@ -247,7 +249,7 @@ export function unify(left, right, env, options = {}) {
|
|
|
247
249
|
}
|
|
248
250
|
|
|
249
251
|
if (isScalar(a)) {
|
|
250
|
-
if (a.name !== b.name) return false;
|
|
252
|
+
if (a.type === NUMBER ? !sameNumberValue(a.name, b.name) : a.name !== b.name) return false;
|
|
251
253
|
continue;
|
|
252
254
|
}
|
|
253
255
|
|
|
@@ -497,7 +499,8 @@ export function variantTerms(left, leftEnv, right, rightEnv, pairs = new Map(),
|
|
|
497
499
|
reverse.set(right.name, left.name);
|
|
498
500
|
return true;
|
|
499
501
|
}
|
|
500
|
-
if (left.type !== right.type || left.
|
|
502
|
+
if (left.type !== right.type || left.arity !== right.arity) return false;
|
|
503
|
+
if (left.type === NUMBER ? !sameNumberValue(left.name, right.name) : left.name !== right.name) return false;
|
|
501
504
|
for (let i = 0; i < left.arity; i++) {
|
|
502
505
|
if (!variantTerms(left.args[i], leftEnv, right.args[i], rightEnv, pairs, reverse)) return false;
|
|
503
506
|
}
|
package/test/run-regression.mjs
CHANGED
|
@@ -567,6 +567,63 @@ c4 ?- call((!;1)).
|
|
|
567
567
|
}
|
|
568
568
|
},
|
|
569
569
|
},
|
|
570
|
+
{
|
|
571
|
+
name: 'number conversion accepts ISO space and Unicode character-code constants exactly',
|
|
572
|
+
run: () => {
|
|
573
|
+
const source = String.raw`
|
|
574
|
+
?- N = 0' .
|
|
575
|
+
N = 32.
|
|
576
|
+
?- number_chars(N,"0' ").
|
|
577
|
+
N = 32.
|
|
578
|
+
?- number_codes(N,[48,39,32]).
|
|
579
|
+
N = 32.
|
|
580
|
+
?- N = 0'😀.
|
|
581
|
+
N = 128512.
|
|
582
|
+
?- number_chars(N,"0'😀").
|
|
583
|
+
N = 128512.
|
|
584
|
+
?- number_codes(N,[48,39,128512]).
|
|
585
|
+
N = 128512.
|
|
586
|
+
?- number_codes(N,[48,39,34]).
|
|
587
|
+
N = 34.
|
|
588
|
+
?- number_codes(N,[48,39,96]).
|
|
589
|
+
N = 96.
|
|
590
|
+
?- number_codes(N,[48,39,92,92]).
|
|
591
|
+
N = 92.
|
|
592
|
+
?- number_chars(01,Chars).
|
|
593
|
+
Chars = "1".
|
|
594
|
+
?- number_codes(01,Codes).
|
|
595
|
+
Codes = [49].
|
|
596
|
+
?- 1.2 = 1.20.
|
|
597
|
+
true.
|
|
598
|
+
?- 1.2 == 1.20.
|
|
599
|
+
true.
|
|
600
|
+
?- 1 = 1.0.
|
|
601
|
+
false.
|
|
602
|
+
?- number_chars(1.20,C), number_chars(Y,C), 1.20 == Y.
|
|
603
|
+
C = "1.20", Y = 1.2.
|
|
604
|
+
`;
|
|
605
|
+
const result = publicApi.runQuads(source);
|
|
606
|
+
assertEqual(result.total, 15, 'quad total');
|
|
607
|
+
assertEqual(result.passed, 15, 'quad passed');
|
|
608
|
+
assertEqual(result.stdout, 'quads: 15 run, 15 passed, 0 failed.\n', 'quad report');
|
|
609
|
+
|
|
610
|
+
for (const goal of [
|
|
611
|
+
'number_chars(N,"3/**/")',
|
|
612
|
+
'number_codes(N,[51,47,42,42,47])',
|
|
613
|
+
'number_chars(N,"0\' ")',
|
|
614
|
+
'number_codes(N,[48,39,32,32])',
|
|
615
|
+
]) {
|
|
616
|
+
let caught = null;
|
|
617
|
+
try {
|
|
618
|
+
publicApi.run('', { goal });
|
|
619
|
+
} catch (error) {
|
|
620
|
+
caught = error;
|
|
621
|
+
}
|
|
622
|
+
if (caught == null) throw new Error(`${goal} should reject trailing layout`);
|
|
623
|
+
assertIncludes(String(caught?.message ?? caught), 'syntax_error(number)', goal);
|
|
624
|
+
}
|
|
625
|
+
},
|
|
626
|
+
},
|
|
570
627
|
{
|
|
571
628
|
name: 'number conversion rejects parenthesized numeric terms',
|
|
572
629
|
run: () => {
|
|
@@ -624,6 +681,21 @@ c4 ?- call((!;1)).
|
|
|
624
681
|
assertEqual(result.stderr, '', 'quad stderr');
|
|
625
682
|
},
|
|
626
683
|
},
|
|
684
|
+
{
|
|
685
|
+
name: 'REPL advances anonymous Prologue length checks through I = 28',
|
|
686
|
+
run: () => {
|
|
687
|
+
const result = runCli([], {
|
|
688
|
+
input:
|
|
689
|
+
'use_module(library(prologue)).\n' +
|
|
690
|
+
'length(_,I),I>9,N is 2^I,\\+ \\+ length(_,N).\n' +
|
|
691
|
+
'f\nf\nf\n;\n;\n;\n;\n\nhalt.\n',
|
|
692
|
+
});
|
|
693
|
+
assertEqual(result.status, 0, 'exit status');
|
|
694
|
+
assertIncludes(result.stdout, '; I = 28, N = 268435456\n; ... .\n?- ', 'large anonymous length answer');
|
|
695
|
+
assertNotIncludes(result.stdout, 'resource_error(memory)', 'stdout');
|
|
696
|
+
assertEqual(result.stderr, '', 'stderr');
|
|
697
|
+
},
|
|
698
|
+
},
|
|
627
699
|
{
|
|
628
700
|
name: 'Prologue freeze wakes delayed goals with their bindings',
|
|
629
701
|
run: () => {
|
|
@@ -2415,14 +2487,14 @@ open(X) :- candidate(X), \\+ closed(X).
|
|
|
2415
2487
|
},
|
|
2416
2488
|
},
|
|
2417
2489
|
{
|
|
2418
|
-
name: 'list allocation heap pressure becomes resource_error(memory)',
|
|
2490
|
+
name: 'named list allocation heap pressure becomes resource_error(memory)',
|
|
2419
2491
|
run: () => {
|
|
2420
2492
|
const engineUrl = new URL('../src/index.js', import.meta.url).href;
|
|
2421
2493
|
const programText = ':- use_module(library(prologue)).\n';
|
|
2422
|
-
const goalText = '
|
|
2494
|
+
const goalText = '\\+ \\+ length(List, 1000000)';
|
|
2423
2495
|
const script = `
|
|
2424
2496
|
import { Program, Solver, Env, parseGoalText, getEyePrologRegistry } from ${JSON.stringify(engineUrl)};
|
|
2425
|
-
const program = Program.parse(${JSON.stringify(programText)});
|
|
2497
|
+
const program = Program.parse(${JSON.stringify(programText)}, { sourceMetadata: false });
|
|
2426
2498
|
const solver = new Solver(program, { registry: getEyePrologRegistry() });
|
|
2427
2499
|
const goal = parseGoalText(${JSON.stringify(goalText)}, {
|
|
2428
2500
|
operatorDefinitions: [...program.operators.values()],
|
|
@@ -2443,6 +2515,37 @@ open(X) :- candidate(X), \\+ closed(X).
|
|
|
2443
2515
|
assertEqual(result.stdout, 'resource_error(memory)', 'heap pressure resource error');
|
|
2444
2516
|
},
|
|
2445
2517
|
},
|
|
2518
|
+
{
|
|
2519
|
+
name: 'anonymous Prologue length checks avoid materializing discarded lists',
|
|
2520
|
+
run: () => {
|
|
2521
|
+
const engineUrl = new URL('../src/index.js', import.meta.url).href;
|
|
2522
|
+
const programText = ':- use_module(library(prologue)).\n';
|
|
2523
|
+
const goalText = 'length(_, I), I > 9, N is 2^I, \\+ \\+ length(_, N)';
|
|
2524
|
+
const script = `
|
|
2525
|
+
import { Program, Solver, Env, parseGoalText, getEyePrologRegistry } from ${JSON.stringify(engineUrl)};
|
|
2526
|
+
const program = Program.parse(${JSON.stringify(programText)}, { sourceMetadata: false });
|
|
2527
|
+
const solver = new Solver(program, {
|
|
2528
|
+
registry: getEyePrologRegistry(),
|
|
2529
|
+
solutionLimit: 19,
|
|
2530
|
+
});
|
|
2531
|
+
const goal = parseGoalText(${JSON.stringify(goalText)}, {
|
|
2532
|
+
operatorDefinitions: [...program.operators.values()],
|
|
2533
|
+
});
|
|
2534
|
+
let answers = 0;
|
|
2535
|
+
for (const _ of solver.solve([goal], new Env(), 0)) answers++;
|
|
2536
|
+
process.stdout.write(String(answers));
|
|
2537
|
+
`;
|
|
2538
|
+
const result = spawnSync(process.execPath, [
|
|
2539
|
+
'--max-old-space-size=64',
|
|
2540
|
+
'--input-type=module',
|
|
2541
|
+
'--eval',
|
|
2542
|
+
script,
|
|
2543
|
+
], { cwd: packageRoot, encoding: 'utf8', timeout: 30000 });
|
|
2544
|
+
if (result.error) throw result.error;
|
|
2545
|
+
assertEqual(result.status, 0, `bounded-heap child status; stderr=${result.stderr}`);
|
|
2546
|
+
assertEqual(result.stdout, '19', 'answers through I = 28');
|
|
2547
|
+
},
|
|
2548
|
+
},
|
|
2446
2549
|
{
|
|
2447
2550
|
name: 'solver honors solution limits',
|
|
2448
2551
|
run: () => {
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -1843,6 +1843,17 @@ ISO 13211-1 leaves the resource atom implementation dependent. EyeProlog uses
|
|
|
1843
1843
|
`finite_memory` spelling for the distinct convention where no finite amount of
|
|
1844
1844
|
memory could complete the computation.
|
|
1845
1845
|
|
|
1846
|
+
The iterative solver keeps active-call frames only where they are semantically
|
|
1847
|
+
needed for cut scope or recursive variant guards. Bundled-library helpers whose
|
|
1848
|
+
callable dependency region is cut-free and which need no recursive variant
|
|
1849
|
+
guard therefore do not copy a growing active-call sequence at every step.
|
|
1850
|
+
Under the normal EyeProlog registry, the bundled Prologue `length/2` also has a
|
|
1851
|
+
scoped iterative execution path: named lists are counted or constructed without
|
|
1852
|
+
recursive interpreter frames, and an anonymous list is not materialized because
|
|
1853
|
+
its binding cannot be observed. The ordinary clauses remain the authoritative
|
|
1854
|
+
module definition and are used unchanged by the ISO-only registry and whenever
|
|
1855
|
+
delays or finite-domain constraints require their normal wake-up points.
|
|
1856
|
+
|
|
1846
1857
|
### Implementation boundary
|
|
1847
1858
|
|
|
1848
1859
|
The source layout mirrors the language boundary. `src/iso.js` contains the
|
|
@@ -5777,7 +5788,13 @@ therefore follow `-` directly because `%` cannot continue a graphic token; an
|
|
|
5777
5788
|
adjacent bracketed comment in `-/**/1` remains a syntax error under the eager
|
|
5778
5789
|
token-consumer rule. Decimal fractions and decimal exponents are supported;
|
|
5779
5790
|
the apostrophe character code is written with a doubled apostrophe as `0'''`
|
|
5780
|
-
and has value 39
|
|
5791
|
+
and has value 39, while a literal space character code is `0' ` and has value
|
|
5792
|
+
32. Character-code constants consume one Unicode scalar, including a
|
|
5793
|
+
non-BMP character. Trailing layout, comments, and other material are rejected;
|
|
5794
|
+
bound integers are converted to their canonical decimal spelling, and
|
|
5795
|
+
non-finite values are rejected. Equivalent spellings of the same numeric type
|
|
5796
|
+
compare by value, preserving the standard conversion round trip, while integer
|
|
5797
|
+
and floating-point terms remain distinct. The
|
|
5781
5798
|
regression gate vendors all 74 numbered cases from Ulrich Neumerkel's contemporary
|
|
5782
5799
|
`number_chars/2` comparison, including the Cor.2 error-precedence cases;
|
|
5783
5800
|
`number_codes/2` shares the same numeric parser and has mirrored coverage for
|
|
@@ -5912,8 +5929,8 @@ so side effects occur in Prolog execution order.
|
|
|
5912
5929
|
### The EyeProlog library
|
|
5913
5930
|
|
|
5914
5931
|
EyeProlog exposes **99 library predicate indicators** in addition to the 129
|
|
5915
|
-
indicators in its isolated ISO profile. **60 are
|
|
5916
|
-
|
|
5932
|
+
indicators in its isolated ISO profile. **60 are defined as ordinary Prolog
|
|
5933
|
+
clauses** in focused modules under `src/lib/`. The remaining 39
|
|
5917
5934
|
are public wrappers around backtrackable host support: Prologue `call_nth/2` and
|
|
5918
5935
|
`freeze/2`, plus the 37-predicate finite-domain `library(clpz)` kernel. The
|
|
5919
5936
|
resulting normal EyeProlog language surface is therefore **228 public predicate
|