eyeprolog 1.2.22 → 1.2.23
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 +9 -4
- package/src/solver.js +5 -2
- package/src/term.js +6 -3
- package/test/run-regression.mjs +57 -0
- package/the-art-of-eyeprolog.md +7 -1
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,
|
|
@@ -1238,15 +1239,19 @@ function scalarBuckets(index, term) {
|
|
|
1238
1239
|
}
|
|
1239
1240
|
|
|
1240
1241
|
function argumentBucket(index, term) {
|
|
1241
|
-
return scalarBuckets(index, term).get(term.name) ?? null;
|
|
1242
|
+
return scalarBuckets(index, term).get(scalarBucketKey(term.type, term.name)) ?? null;
|
|
1242
1243
|
}
|
|
1243
1244
|
|
|
1244
1245
|
function addArgumentBucket(index, term, clause) {
|
|
1245
|
-
addClauseBucket(scalarBuckets(index, term), term.name, clause);
|
|
1246
|
+
addClauseBucket(scalarBuckets(index, term), scalarBucketKey(term.type, term.name), clause);
|
|
1246
1247
|
}
|
|
1247
1248
|
|
|
1248
1249
|
function scalarIndexKey(term) {
|
|
1249
|
-
return `${term.type}\u0000${term.name}`;
|
|
1250
|
+
return `${term.type}\u0000${scalarBucketKey(term.type, term.name)}`;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
function scalarBucketKey(type, name) {
|
|
1254
|
+
return type === 'number' ? numberValueKey(name) : name;
|
|
1250
1255
|
}
|
|
1251
1256
|
|
|
1252
1257
|
function addClauseBucket(buckets, key, clause) {
|
|
@@ -1276,7 +1281,7 @@ function indexCompactOne(index, type, name, clause, clauses = null, clausePositi
|
|
|
1276
1281
|
index.sawScalar = true;
|
|
1277
1282
|
if (clauses && clausePosition > 0) index.fallback = clauses.slice(0, clausePosition);
|
|
1278
1283
|
}
|
|
1279
|
-
addClauseBucket(compactScalarBuckets(index, type), name, clause);
|
|
1284
|
+
addClauseBucket(compactScalarBuckets(index, type), scalarBucketKey(type, name), clause);
|
|
1280
1285
|
} else if (index.sawScalar) {
|
|
1281
1286
|
index.fallback.push(clause);
|
|
1282
1287
|
}
|
package/src/solver.js
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
COMPOUND, Env, compound, copyResolved, deref, emptyList, flattenConjunction, freshTerm,
|
|
5
5
|
numberTerm, numberTextFromDouble, termIsGround, termToString, unify, variantTerms,
|
|
6
6
|
} from './term.js';
|
|
7
|
+
import { sameNumberValue } from './number-value.js';
|
|
7
8
|
import { PrologError, getStrictIsoRegistry } from './iso.js';
|
|
8
9
|
import { getEyePrologRegistry } from './standard-library.js';
|
|
9
10
|
import { selectClauseCandidates, selectClauseCandidatesForValues, selectGroundClauseCandidates } from './program.js';
|
|
@@ -1089,11 +1090,13 @@ function isScalarTerm(term) {
|
|
|
1089
1090
|
}
|
|
1090
1091
|
|
|
1091
1092
|
function sameScalarTerm(left, right) {
|
|
1092
|
-
return isScalarTerm(left) && isScalarTerm(right) && left.type === right.type &&
|
|
1093
|
+
return isScalarTerm(left) && isScalarTerm(right) && left.type === right.type &&
|
|
1094
|
+
(left.type === 'number' ? sameNumberValue(left.name, right.name) : left.name === right.name);
|
|
1093
1095
|
}
|
|
1094
1096
|
|
|
1095
1097
|
function sameGroundTerm(left, right) {
|
|
1096
|
-
if (left?.type !== right?.type
|
|
1098
|
+
if (left?.type !== right?.type) return false;
|
|
1099
|
+
if (left?.type === 'number' ? !sameNumberValue(left.name, right.name) : left?.name !== right?.name) return false;
|
|
1097
1100
|
const arity = left.args?.length ?? 0;
|
|
1098
1101
|
if (arity !== (right.args?.length ?? 0)) return false;
|
|
1099
1102
|
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: () => {
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -5777,7 +5777,13 @@ therefore follow `-` directly because `%` cannot continue a graphic token; an
|
|
|
5777
5777
|
adjacent bracketed comment in `-/**/1` remains a syntax error under the eager
|
|
5778
5778
|
token-consumer rule. Decimal fractions and decimal exponents are supported;
|
|
5779
5779
|
the apostrophe character code is written with a doubled apostrophe as `0'''`
|
|
5780
|
-
and has value 39
|
|
5780
|
+
and has value 39, while a literal space character code is `0' ` and has value
|
|
5781
|
+
32. Character-code constants consume one Unicode scalar, including a
|
|
5782
|
+
non-BMP character. Trailing layout, comments, and other material are rejected;
|
|
5783
|
+
bound integers are converted to their canonical decimal spelling, and
|
|
5784
|
+
non-finite values are rejected. Equivalent spellings of the same numeric type
|
|
5785
|
+
compare by value, preserving the standard conversion round trip, while integer
|
|
5786
|
+
and floating-point terms remain distinct. The
|
|
5781
5787
|
regression gate vendors all 74 numbered cases from Ulrich Neumerkel's contemporary
|
|
5782
5788
|
`number_chars/2` comparison, including the Cor.2 error-precedence cases;
|
|
5783
5789
|
`number_codes/2` shares the same numeric parser and has mirrored coverage for
|