eyeprolog 1.5.100 → 1.5.102
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/parser.js +49 -2
- package/src/program.js +36 -1
- package/test/regression/cases-regression.mjs +55 -0
package/package.json
CHANGED
package/src/parser.js
CHANGED
|
@@ -203,6 +203,21 @@ const CLPZ_OPERATOR_DEFINITIONS = [
|
|
|
203
203
|
[450, 'xfx', '..'],
|
|
204
204
|
];
|
|
205
205
|
|
|
206
|
+
// Names of bundled library(Name) modules whose own operators are pre-seeded
|
|
207
|
+
// into the live parser's operator table the moment `:- use_module(library(
|
|
208
|
+
// Name))` is parsed (applyImportedLibraryOperators below), rather than only
|
|
209
|
+
// once that module's own `:- op(...)` directives are themselves parsed. A
|
|
210
|
+
// library's clauses are parsed and processed in one pass (see
|
|
211
|
+
// loadSourceIntoBuilder in program.js), so without this a file that both
|
|
212
|
+
// imports one of these libraries and uses its custom operator syntax later
|
|
213
|
+
// in the very same source text would fail to parse. Any bundled library
|
|
214
|
+
// whose own source declares `:- op(...)` must be listed here, or it cannot
|
|
215
|
+
// safely be served from the prepared-clause cache in program.js: a cache
|
|
216
|
+
// hit replays already-parsed clause objects directly and never re-runs a
|
|
217
|
+
// live Parser over that library's text, so a cached op/3 directive from an
|
|
218
|
+
// unlisted library would never reach a live parser's operator table.
|
|
219
|
+
export const PRESEEDED_LIBRARY_OPERATOR_NAMES = new Set(['clpz', 'atts', 'tabling', 'debug']);
|
|
220
|
+
|
|
206
221
|
function operatorStrength(priority) {
|
|
207
222
|
return 1201 - priority;
|
|
208
223
|
}
|
|
@@ -421,6 +436,7 @@ class Parser {
|
|
|
421
436
|
const designation = directive.args[0];
|
|
422
437
|
if (designation?.type !== COMPOUND || designation.name !== 'library' || designation.arity !== 1 ||
|
|
423
438
|
designation.args[0]?.type !== ATOM) return;
|
|
439
|
+
if (!PRESEEDED_LIBRARY_OPERATOR_NAMES.has(designation.args[0].name)) return;
|
|
424
440
|
if (designation.args[0].name === 'clpz') {
|
|
425
441
|
for (const [priority, specifier, name] of CLPZ_OPERATOR_DEFINITIONS) {
|
|
426
442
|
this.defineOperator(priority, specifier, name);
|
|
@@ -1428,6 +1444,28 @@ function parseClausesFastNoSource(source, emit = null, emitBinary = null, option
|
|
|
1428
1444
|
return value;
|
|
1429
1445
|
};
|
|
1430
1446
|
|
|
1447
|
+
// SIMPLE_NUMBER-matched text is already lexically valid Prolog number
|
|
1448
|
+
// syntax, and this fast path deliberately leaves a numeric literal's
|
|
1449
|
+
// spelling otherwise untouched (test/conformance/cases/arithmetic/
|
|
1450
|
+
// 024_numeric_literal_readback.pl: decimal and exponent literals retain
|
|
1451
|
+
// their lexical form here, so e.g. 6.02e23 must not become 6.02e+23).
|
|
1452
|
+
// Two things still cannot be left as raw matched text, though: an integer
|
|
1453
|
+
// is never left non-canonical anywhere else in the language (a leading
|
|
1454
|
+
// zero, or the "-0" from issue #114, must collapse the same way the
|
|
1455
|
+
// scanner's own adjacent-minus BigInt construction already does), and a
|
|
1456
|
+
// float whose value is exactly zero must never print with a stray minus --
|
|
1457
|
+
// this implementation deliberately never produces -0.0 (term.js
|
|
1458
|
+
// numberTextFromDouble) -- without reformatting any non-zero float.
|
|
1459
|
+
const canonicalFastNumberText = (text) => {
|
|
1460
|
+
if (text.includes('.') || text.includes('e') || text.includes('E')) {
|
|
1461
|
+
return (text.charCodeAt(0) === 45 && Number(text) === 0) ? text.slice(1) : text;
|
|
1462
|
+
}
|
|
1463
|
+
const first = text.charCodeAt(0);
|
|
1464
|
+
if (first !== 45 && (text.length === 1 || first !== 48)) return text;
|
|
1465
|
+
return BigInt(text).toString();
|
|
1466
|
+
};
|
|
1467
|
+
const fastNumberTerm = (text) => numberTerm(canonicalFastNumberText(text));
|
|
1468
|
+
|
|
1431
1469
|
const isFastScalarToken = (text) => SIMPLE_VARIABLE.test(text) || SIMPLE_ATOM.test(text) || GRAPHIC_ATOM.test(text) || SIMPLE_NUMBER.test(text);
|
|
1432
1470
|
const scalarOrVariableFast = (text) => {
|
|
1433
1471
|
if (!text || !isFastScalarToken(text)) throw new Error('bad simple term');
|
|
@@ -1440,7 +1478,7 @@ function parseClausesFastNoSource(source, emit = null, emitBinary = null, option
|
|
|
1440
1478
|
variableCache.set(text, value);
|
|
1441
1479
|
return value;
|
|
1442
1480
|
}
|
|
1443
|
-
if ((first === 45 || isDigitCode(first)) && SIMPLE_NUMBER.test(text)) return cached(numberCache, text,
|
|
1481
|
+
if ((first === 45 || isDigitCode(first)) && SIMPLE_NUMBER.test(text)) return cached(numberCache, text, fastNumberTerm);
|
|
1444
1482
|
return atom(text);
|
|
1445
1483
|
};
|
|
1446
1484
|
|
|
@@ -1506,7 +1544,10 @@ function parseClausesFastNoSource(source, emit = null, emitBinary = null, option
|
|
|
1506
1544
|
return term;
|
|
1507
1545
|
}
|
|
1508
1546
|
if (kind === 'atom') return atom(value);
|
|
1509
|
-
|
|
1547
|
+
// See canonicalFastNumberText above: "-0" and "007" must still come out
|
|
1548
|
+
// the same as everywhere else in the language (issue #114), without
|
|
1549
|
+
// reformatting a non-zero float's retained lexical spelling.
|
|
1550
|
+
if (simpleNumberInRange(text, start, end)) return cached(numberCache, value, fastNumberTerm);
|
|
1510
1551
|
return null;
|
|
1511
1552
|
};
|
|
1512
1553
|
|
|
@@ -1522,6 +1563,12 @@ function parseClausesFastNoSource(source, emit = null, emitBinary = null, option
|
|
|
1522
1563
|
if (type == null) return false;
|
|
1523
1564
|
let name = text.slice(start, end);
|
|
1524
1565
|
if (type === 'var' && name === '_') name = `__anon${anonymous++}`;
|
|
1566
|
+
// This range becomes a CompactBinaryClause argument, materialized lazily
|
|
1567
|
+
// as numberTerm(name) (program-indexing.js) and used as-is for indexing.
|
|
1568
|
+
// Canonicalize the stored text up front for the same reason as the two
|
|
1569
|
+
// callers above -- otherwise a fact such as bar(-0, 1) or bar(007, 1)
|
|
1570
|
+
// keeps its raw, non-canonical spelling forever (issue #114).
|
|
1571
|
+
if (type === 'number') name = canonicalFastNumberText(name);
|
|
1525
1572
|
if (slot === 0) {
|
|
1526
1573
|
out.arg0Type = type;
|
|
1527
1574
|
out.arg0Name = name;
|
package/src/program.js
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
ISO_OPERATOR_DEFINITIONS,
|
|
7
7
|
PART2_OPERATOR_DEFINITIONS,
|
|
8
8
|
PART3_OPERATOR_DEFINITIONS,
|
|
9
|
+
PRESEEDED_LIBRARY_OPERATOR_NAMES,
|
|
9
10
|
QUAD_OPERATOR_DEFINITIONS,
|
|
10
11
|
createParserOperatorState,
|
|
11
12
|
parseClauses,
|
|
@@ -108,9 +109,43 @@ function hasDirectCutTailRecursion(group) {
|
|
|
108
109
|
return sawRecursive;
|
|
109
110
|
}
|
|
110
111
|
|
|
112
|
+
// library(clpz) alone pulls in about a dozen further bundled libraries
|
|
113
|
+
// (assoc, pairs, between, lists, atts, iso_ext, dcgs, terms, error, si,
|
|
114
|
+
// freeze, arithmetic, debug, format) via its own use_module/1-2 directives,
|
|
115
|
+
// each recursively loaded and prepared (parsed, DCG- and goal-expanded,
|
|
116
|
+
// dependency-analyzed) from scratch on every single program that needs
|
|
117
|
+
// clpz. Matching against every bundled filename, not just clpz.pl's own,
|
|
118
|
+
// lets each of those dependencies -- and every other bundled library used
|
|
119
|
+
// this way -- be prepared once per process and reused, the same as clpz.pl
|
|
120
|
+
// itself already was.
|
|
121
|
+
//
|
|
122
|
+
// A cache hit replays already-prepared clause objects directly (see below)
|
|
123
|
+
// and never re-runs a live Parser over that library's own source text. A
|
|
124
|
+
// library whose own source declares `:- op(...)` relies on exactly that
|
|
125
|
+
// live parse to install its operators into the importing file's operator
|
|
126
|
+
// table before the rest of that file is parsed (loadSourceIntoBuilder
|
|
127
|
+
// processes one whole source's clauses only after fully parsing it), so
|
|
128
|
+
// serving such a library from this cache would silently stop a sibling
|
|
129
|
+
// source term, later in the very same file as its use_module/1 directive,
|
|
130
|
+
// from parsing with that library's operator syntax. Only a library the
|
|
131
|
+
// parser separately pre-seeds regardless of caching
|
|
132
|
+
// (PRESEEDED_LIBRARY_OPERATOR_NAMES) is safe to include despite declaring
|
|
133
|
+
// its own operators.
|
|
134
|
+
const OWN_OPERATOR_DIRECTIVE = /:-(?:\s|\/\*[\s\S]*?\*\/)*op\s*\(/;
|
|
135
|
+
const bundledLibraryFilenames = new Set(
|
|
136
|
+
Array.from(standardLibrarySources.entries())
|
|
137
|
+
.filter(([name, entry]) => PRESEEDED_LIBRARY_OPERATOR_NAMES.has(name) || !OWN_OPERATOR_DIRECTIVE.test(entry.source))
|
|
138
|
+
.map(([, entry]) => entry.filename),
|
|
139
|
+
);
|
|
140
|
+
|
|
111
141
|
function preparedBundledLibraryCacheKey(program, options) {
|
|
112
142
|
const filename = String(options.filename ?? '');
|
|
113
|
-
if (
|
|
143
|
+
if (!bundledLibraryFilenames.has(filename)) return null;
|
|
144
|
+
// Strict ISO Part 1 skips the normal-profile clause preparation
|
|
145
|
+
// (expandClauseGoals, normalizeQualifiedClauseHead, DCG expansion) that
|
|
146
|
+
// this cache stores the result of, so a strict-mode load must never be
|
|
147
|
+
// served from -- or poison -- a normal-mode cache entry for the same file.
|
|
148
|
+
if (program.strictIso) return null;
|
|
114
149
|
// A user hook deliberately defined before use_module/1 is allowed to
|
|
115
150
|
// transform bundled source. Such a program needs its own ordinary expansion.
|
|
116
151
|
if (program.groups.has(modulePredicateKey('user', 'term_expansion', 2)) ||
|
|
@@ -5892,5 +5892,60 @@ answer(Result) :- countdown(2048, Result), Result = 2048.
|
|
|
5892
5892
|
}
|
|
5893
5893
|
},
|
|
5894
5894
|
},
|
|
5895
|
+
{
|
|
5896
|
+
// https://github.com/eyereasoner/eyeprolog/issues/114#issuecomment-5663598188
|
|
5897
|
+
name: 'the fast compact-clause loader canonicalizes numeric literals the same way the general parser does (issue #114 follow-up)',
|
|
5898
|
+
run: () => {
|
|
5899
|
+
// parseClausesFastNoSource (src/parser.js) has its own, separate
|
|
5900
|
+
// number-token handling for facts and rules that match its compact
|
|
5901
|
+
// two-argument shape -- exactly the kind of second code path that
|
|
5902
|
+
// reintroduces the #114 bug class. It used to build every numeric
|
|
5903
|
+
// argument straight from the raw regex-matched text, so a fact like
|
|
5904
|
+
// bar(-0, 1) or bar(007, 1) kept that raw spelling forever instead of
|
|
5905
|
+
// canonicalizing like every other numeric literal in the language.
|
|
5906
|
+
for (const [program, goal, expected] of [
|
|
5907
|
+
["bar(-0, 1).\n", 'bar(A, B), write_canonical(A), nl', '0'],
|
|
5908
|
+
["bar(-0.0, 1).\n", 'bar(A, B), write_canonical(A), nl', '0.0'],
|
|
5909
|
+
["bar(007, 1).\n", 'bar(A, B), write_canonical(A), nl', '7'],
|
|
5910
|
+
['point(1, -0).\n', 'point(A, B), write_canonical(B), nl', '0'],
|
|
5911
|
+
]) {
|
|
5912
|
+
const stdout = run(program, { goal }).stdout;
|
|
5913
|
+
assertEqual(stdout.split('\n')[0], expected, `${program.trim()}: ${goal}`);
|
|
5914
|
+
}
|
|
5915
|
+
// The fast loader also deliberately retains a numeric literal's exact
|
|
5916
|
+
// lexical spelling otherwise (test/conformance/cases/arithmetic/
|
|
5917
|
+
// 024_numeric_literal_readback.pl): the zero-canonicalization above
|
|
5918
|
+
// must not turn into a general reformatting pass that rewrites a
|
|
5919
|
+
// non-zero float's notation.
|
|
5920
|
+
assertEqual(
|
|
5921
|
+
run('raw(a, 6.02e23).\n', { goal: 'raw(_, B), write_canonical(B), nl' }).stdout.split('\n')[0],
|
|
5922
|
+
'6.02e23',
|
|
5923
|
+
'non-zero exponent notation is preserved, not reformatted to 6.02e+23',
|
|
5924
|
+
);
|
|
5925
|
+
assertEqual(
|
|
5926
|
+
run('raw(a, -1.0e-3).\n', { goal: 'raw(_, B), write_canonical(B), nl' }).stdout.split('\n')[0],
|
|
5927
|
+
'-1.0e-3',
|
|
5928
|
+
'non-zero negative exponent notation is preserved, not reformatted to -0.001',
|
|
5929
|
+
);
|
|
5930
|
+
},
|
|
5931
|
+
},
|
|
5932
|
+
{
|
|
5933
|
+
// https://github.com/eyereasoner/eyeprolog/issues/114#issuecomment-5663598188
|
|
5934
|
+
name: 'a genuine IEEE-754 negative zero produced by arithmetic (not just parsed from text) still prints as 0.0',
|
|
5935
|
+
run: () => {
|
|
5936
|
+
// Every fix above canonicalizes -0.0 where it is spelled out in
|
|
5937
|
+
// source text. EyeProlog's floats are ordinary JS doubles, though,
|
|
5938
|
+
// and JS arithmetic itself produces a real, distinct -0.0 bit
|
|
5939
|
+
// pattern for operations such as a negative number times zero --
|
|
5940
|
+
// this is genuine IEEE-754 negative zero, not a text-parsing
|
|
5941
|
+
// artifact, and 13211-1 has nothing to say about it either way. It
|
|
5942
|
+
// must still come out normalized to plain 0.0 on the way to a term,
|
|
5943
|
+
// the same as every syntactic spelling of negative zero does.
|
|
5944
|
+
for (const goal of ['X is -1.0 * 0.0', 'X is 0.0 * -1.0', 'X is -(1.0 - 1.0)']) {
|
|
5945
|
+
const stdout = run('', { goal: `${goal}, write_canonical(X), nl` }).stdout;
|
|
5946
|
+
assertEqual(stdout.split('\n')[0], '0.0', `${goal}: internal -0.0 normalizes to 0.0`);
|
|
5947
|
+
}
|
|
5948
|
+
},
|
|
5949
|
+
},
|
|
5895
5950
|
];
|
|
5896
5951
|
}
|