eyeprolog 1.5.34 → 1.5.36
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 +24 -15
- package/src/program-analysis.js +37 -2
- package/src/program.js +6 -3
- package/src/solver.js +14 -12
- package/src/term.js +33 -16
- package/src/wfs.js +8 -2
- package/src/write.js +20 -14
- package/test/run-neumerkel-tests.mjs +5 -2
- package/test/run-regression.mjs +13 -9
package/package.json
CHANGED
package/src/parser.js
CHANGED
|
@@ -238,6 +238,12 @@ export function createParserOperatorState(definitions = [], includeDefaults = tr
|
|
|
238
238
|
}
|
|
239
239
|
return state;
|
|
240
240
|
}
|
|
241
|
+
// Pre-compiled character-class regexes used in the lexer hot path.
|
|
242
|
+
const RE_OCTAL_DIGIT = /^[0-7]$/;
|
|
243
|
+
const RE_HEX_DIGIT = /^[0-9A-Fa-f]$/;
|
|
244
|
+
const RE_DECIMAL_DIGIT = /^[0-9]$/;
|
|
245
|
+
const RE_BINARY_DIGIT = /^[01]$/;
|
|
246
|
+
|
|
241
247
|
|
|
242
248
|
class Parser {
|
|
243
249
|
constructor(source, options = {}) {
|
|
@@ -480,7 +486,7 @@ class Parser {
|
|
|
480
486
|
|
|
481
487
|
if (escaped === 'x') {
|
|
482
488
|
let digits = '';
|
|
483
|
-
while (
|
|
489
|
+
while (RE_HEX_DIGIT.test(peekChar())) digits += takeChar();
|
|
484
490
|
if (!digits || takeChar() !== '\\') throw new Error(`parse line ${line}: bad hexadecimal escape`);
|
|
485
491
|
const code = Number.parseInt(digits, 16);
|
|
486
492
|
if (code > 0x10ffff || (code >= 0xd800 && code <= 0xdfff)) {
|
|
@@ -489,9 +495,9 @@ class Parser {
|
|
|
489
495
|
if (this.strictIso && !isStrictIsoPcsCodePoint(code)) throw new CharacterRepresentationError();
|
|
490
496
|
return String.fromCodePoint(code);
|
|
491
497
|
}
|
|
492
|
-
if (
|
|
498
|
+
if (RE_OCTAL_DIGIT.test(escaped)) {
|
|
493
499
|
let digits = escaped;
|
|
494
|
-
while (
|
|
500
|
+
while (RE_OCTAL_DIGIT.test(peekChar())) digits += takeChar();
|
|
495
501
|
if (takeChar() !== '\\') throw new Error(`parse line ${line}: bad octal escape`);
|
|
496
502
|
const code = Number.parseInt(digits, 8);
|
|
497
503
|
if (code > 0x10ffff || (code >= 0xd800 && code <= 0xdfff)) {
|
|
@@ -503,7 +509,7 @@ class Parser {
|
|
|
503
509
|
// A backslash followed by a decimal digit is numeric-escape syntax, but
|
|
504
510
|
// ISO octal digits are limited to 0..7. Do not reinterpret \8 or \9 as
|
|
505
511
|
// implementation-specific one-character escapes.
|
|
506
|
-
if (
|
|
512
|
+
if (RE_DECIMAL_DIGIT.test(escaped)) throw new Error(`parse line ${line}: bad octal escape`);
|
|
507
513
|
|
|
508
514
|
// The only remaining ISO meta escapes are the four meta characters from
|
|
509
515
|
// 6.5.5. Forms such as \c, \d, \e, \u or \. are not quoted
|
|
@@ -652,9 +658,9 @@ class Parser {
|
|
|
652
658
|
return { type: TOK.NUMBER, text: String(negative ? -code : code), line };
|
|
653
659
|
}
|
|
654
660
|
const radixKind = this.peek() === '0' ? this.peek(1) : '';
|
|
655
|
-
const radixHasDigit = radixKind === 'b' ?
|
|
656
|
-
: radixKind === 'o' ?
|
|
657
|
-
: radixKind === 'x' ?
|
|
661
|
+
const radixHasDigit = radixKind === 'b' ? RE_BINARY_DIGIT.test(this.peek(2))
|
|
662
|
+
: radixKind === 'o' ? RE_OCTAL_DIGIT.test(this.peek(2))
|
|
663
|
+
: radixKind === 'x' ? RE_HEX_DIGIT.test(this.peek(2))
|
|
658
664
|
: false;
|
|
659
665
|
if (radixHasDigit) {
|
|
660
666
|
this.take();
|
|
@@ -811,11 +817,14 @@ class Parser {
|
|
|
811
817
|
let left = this.parsePrefixTerm(minPrecedence, allowBar, allowOperatorAtom);
|
|
812
818
|
const leftIsBareOperatorAtom = initialWasCurrentOperator &&
|
|
813
819
|
left.type === ATOM && left.name === initialOperatorName;
|
|
814
|
-
// Neumerkel syntax #379
|
|
815
|
-
//
|
|
816
|
-
//
|
|
817
|
-
//
|
|
818
|
-
|
|
820
|
+
// Neumerkel syntax #379: the DCG rule operator `-->` is processor-defined
|
|
821
|
+
// and not an ISO predicate-indicator name. A bare `-->` (not parenthesized)
|
|
822
|
+
// followed by `/` would be an invalid strict-ISO predicate indicator.
|
|
823
|
+
// However, `(-->)/2` is valid: the parentheses make `-->` an ordinary atom
|
|
824
|
+
// argument, and ISO 7.10.3 does not restrict which atoms may appear as the
|
|
825
|
+
// name in a Name/Arity indicator — only the write-term rules govern spelling.
|
|
826
|
+
// Only reject the bare-operator form so (-->)/2 succeeds per #379.
|
|
827
|
+
if (this.strictIso && leftIsBareOperatorAtom && left.name === '-->' &&
|
|
819
828
|
this.operatorTokenName() === '/') {
|
|
820
829
|
throw new Error(`parse line ${this.token.line}: operator atom --> is not permitted in a strict ISO predicate indicator`);
|
|
821
830
|
}
|
|
@@ -1813,14 +1822,14 @@ export function parseNumberTokenText(text, options = {}) {
|
|
|
1813
1822
|
value = controls[escaped];
|
|
1814
1823
|
} else if (escaped === 'x') {
|
|
1815
1824
|
let digits = '';
|
|
1816
|
-
while (
|
|
1825
|
+
while (RE_HEX_DIGIT.test(source[position] ?? '')) digits += source[position++];
|
|
1817
1826
|
if (!digits || source[position++] !== '\\') throw invalidNumberTokenError;
|
|
1818
1827
|
const code = Number.parseInt(digits, 16);
|
|
1819
1828
|
if (code > 0x10ffff || (code >= 0xd800 && code <= 0xdfff)) throw invalidNumberTokenError;
|
|
1820
1829
|
value = String.fromCodePoint(code);
|
|
1821
|
-
} else if (
|
|
1830
|
+
} else if (RE_OCTAL_DIGIT.test(escaped)) {
|
|
1822
1831
|
let digits = escaped;
|
|
1823
|
-
while (
|
|
1832
|
+
while (RE_OCTAL_DIGIT.test(source[position] ?? '')) digits += source[position++];
|
|
1824
1833
|
if (source[position++] !== '\\') throw invalidNumberTokenError;
|
|
1825
1834
|
const code = Number.parseInt(digits, 8);
|
|
1826
1835
|
if (code > 0x10ffff || (code >= 0xd800 && code <= 0xdfff)) throw invalidNumberTokenError;
|
package/src/program-analysis.js
CHANGED
|
@@ -8,7 +8,11 @@ import {
|
|
|
8
8
|
|
|
9
9
|
export function componentHasNegativeEdge(start, deps, negativeEdges) {
|
|
10
10
|
const forward = reachableIndexes(start, deps);
|
|
11
|
-
|
|
11
|
+
// A node is in the same SCC as `start` iff it can also reach `start`.
|
|
12
|
+
// Rather than calling reachableIndexes() per-node (O(n^2)), compute the
|
|
13
|
+
// reverse-reachability set from `start` over the transposed graph once.
|
|
14
|
+
const backward = reachableIndexesTransposed(start, deps, forward);
|
|
15
|
+
const component = new Set([...forward].filter((index) => backward.has(index)));
|
|
12
16
|
return negativeEdges.some(([from, to]) => component.has(from) && component.has(to));
|
|
13
17
|
}
|
|
14
18
|
|
|
@@ -27,7 +31,8 @@ export function clauseIsDirectRecursive(clause, group) {
|
|
|
27
31
|
|
|
28
32
|
export function componentHasCut(start, deps, groups) {
|
|
29
33
|
const forward = reachableIndexes(start, deps);
|
|
30
|
-
const
|
|
34
|
+
const backward = reachableIndexesTransposed(start, deps, forward);
|
|
35
|
+
const component = [...forward].filter((index) => backward.has(index));
|
|
31
36
|
return component.some((index) => {
|
|
32
37
|
const group = groups[index];
|
|
33
38
|
const directRecursive = group.clauses.some((clause) => clauseIsDirectRecursive(clause, group));
|
|
@@ -50,6 +55,36 @@ export function reachableIndexes(start, deps) {
|
|
|
50
55
|
return seen;
|
|
51
56
|
}
|
|
52
57
|
|
|
58
|
+
// Returns the set of nodes in `candidates` that can reach `target` by
|
|
59
|
+
// traversing `deps` in reverse. This is equivalent to asking which nodes
|
|
60
|
+
// in the forward-reachable set from `target` also have `target` in their
|
|
61
|
+
// own forward-reachable set, but computed in a single BFS over the
|
|
62
|
+
// transposed graph rather than one BFS per candidate node.
|
|
63
|
+
function reachableIndexesTransposed(target, deps, candidates) {
|
|
64
|
+
// Build a transposed adjacency list restricted to the candidate set.
|
|
65
|
+
const reverse = new Map();
|
|
66
|
+
for (const from of candidates) {
|
|
67
|
+
if (!reverse.has(from)) reverse.set(from, []);
|
|
68
|
+
const edges = deps[from];
|
|
69
|
+
if (edges == null) continue;
|
|
70
|
+
for (const to of edges) {
|
|
71
|
+
if (!candidates.has(to)) continue;
|
|
72
|
+
let bucket = reverse.get(to);
|
|
73
|
+
if (bucket == null) { bucket = []; reverse.set(to, bucket); }
|
|
74
|
+
bucket.push(from);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const seen = new Set();
|
|
78
|
+
const stack = [target];
|
|
79
|
+
while (stack.length) {
|
|
80
|
+
const current = stack.pop();
|
|
81
|
+
if (seen.has(current)) continue;
|
|
82
|
+
seen.add(current);
|
|
83
|
+
for (const prev of reverse.get(current) ?? []) if (!seen.has(prev)) stack.push(prev);
|
|
84
|
+
}
|
|
85
|
+
return seen;
|
|
86
|
+
}
|
|
87
|
+
|
|
53
88
|
|
|
54
89
|
export function isFiniteDatalogArgument(term) {
|
|
55
90
|
return term?.type === VAR || term?.type === ATOM || term?.type === 'string' || term?.type === 'number';
|
package/src/program.js
CHANGED
|
@@ -29,9 +29,12 @@ import {
|
|
|
29
29
|
} from './program-indexing.js';
|
|
30
30
|
export { selectClauseCandidates, selectClauseCandidatesForValues, selectGroundClauseCandidates } from './program-indexing.js';
|
|
31
31
|
import {
|
|
32
|
+
|
|
32
33
|
componentHasNegativeEdge, componentHasCut, reachableIndexes, datalogDependencyClauseCount, isFiniteDatalogGroup, isRangeRestrictedFiniteDatalogGroup, isFiniteWfsDatalogGroup, inferStructuralInputPositions, hasStrictListTailRecursion, hasLinearNumericRecursion, isPiAccumulator, isPortableBetweenGenerator, directGoalDependencyKey, collectGoalDependencies, stronglyConnectedComponents, computeNegationStrata,
|
|
33
34
|
} from './program-analysis.js';
|
|
34
35
|
|
|
36
|
+
const RE_DECIMAL_INT = /^\d+$/;
|
|
37
|
+
|
|
35
38
|
const DEFER_PROGRAM_BUILD = Symbol('deferProgramBuild');
|
|
36
39
|
const FAST_PARSE_ABORT = Symbol('fastParseAbort');
|
|
37
40
|
const PROGRAM_BUILD_BATCH_SIZE = 16384;
|
|
@@ -303,7 +306,7 @@ export class Program {
|
|
|
303
306
|
const positions = [];
|
|
304
307
|
for (let index = 0; index < template.args.length; index++) {
|
|
305
308
|
const spec = template.args[index];
|
|
306
|
-
if ((spec.type === 'number' &&
|
|
309
|
+
if ((spec.type === 'number' && RE_DECIMAL_INT.test(spec.name)) ||
|
|
307
310
|
(spec.type === ATOM && spec.name === ':')) positions.push(index);
|
|
308
311
|
}
|
|
309
312
|
const definitions = this.moduleMetaPredicates.get(module) ?? new Map();
|
|
@@ -1653,7 +1656,7 @@ function moduleExportIndicators(term) {
|
|
|
1653
1656
|
if (item.type === COMPOUND && item.name === 'op' && item.arity === 3) {
|
|
1654
1657
|
const [priority, specifier, names] = item.args;
|
|
1655
1658
|
const operatorNames = names.type === ATOM ? [names] : properListItems(names, new Env());
|
|
1656
|
-
if (priority.type !== NUMBER ||
|
|
1659
|
+
if (priority.type !== NUMBER || !RE_DECIMAL_INT.test(priority.name) || Number(priority.name) > 1200 ||
|
|
1657
1660
|
specifier.type !== ATOM || !['fx', 'fy', 'xf', 'yf', 'xfx', 'xfy', 'yfx'].includes(specifier.name) ||
|
|
1658
1661
|
operatorNames == null || operatorNames.some((name) => name.type !== ATOM)) return null;
|
|
1659
1662
|
continue;
|
|
@@ -1832,7 +1835,7 @@ function staticProcedureModificationError(name, arity) {
|
|
|
1832
1835
|
|
|
1833
1836
|
function predicateIndicator(name, arity) {
|
|
1834
1837
|
if (name?.type !== ATOM || arity?.type !== 'number') return null;
|
|
1835
|
-
if (
|
|
1838
|
+
if (!RE_DECIMAL_INT.test(arity.name)) return null;
|
|
1836
1839
|
const arityNumber = Number(arity.name);
|
|
1837
1840
|
return { name: name.name, arity: arityNumber, key: `${name.name}/${arityNumber}` };
|
|
1838
1841
|
}
|
package/src/solver.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Most semantic decisions still flow through unification; optimizations only select candidates earlier.
|
|
3
3
|
import {
|
|
4
4
|
ATOM, COMPOUND, NUMBER, STRING, VAR, Env, Term, compactListLength, compactVariableList, compound, cons, copyResolved, deref, emptyList,
|
|
5
|
-
flattenConjunction, freshTerm, isCons, isDecimalInteger, isEmptyList,
|
|
5
|
+
flattenConjunction, freshTerm, isCons, isDecimalInteger, isEmptyList, isScalar,
|
|
6
6
|
numberTerm, numberTextFromDouble, properListItems, termIsGround, termToString, unify, variable, variantTerms,
|
|
7
7
|
} from './term.js';
|
|
8
8
|
import { numberValueKey, sameNumberValue } from './number-value.js';
|
|
@@ -2692,11 +2692,14 @@ function matchScalarFact(goal, head, env) {
|
|
|
2692
2692
|
|
|
2693
2693
|
function derefScalarMatch(term, env, names, values) {
|
|
2694
2694
|
let current = term;
|
|
2695
|
-
|
|
2695
|
+
const seen = new Set();
|
|
2696
|
+
while (current?.type === 'var') {
|
|
2697
|
+
if (seen.has(current.name)) break;
|
|
2698
|
+
seen.add(current.name);
|
|
2696
2699
|
const localIndex = names.indexOf(current.name);
|
|
2697
|
-
if (localIndex >= 0) current = values[localIndex];
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
+
if (localIndex >= 0) { current = values[localIndex]; continue; }
|
|
2701
|
+
if (env.has(current.name)) { current = env.get(current.name); continue; }
|
|
2702
|
+
break;
|
|
2700
2703
|
}
|
|
2701
2704
|
return current;
|
|
2702
2705
|
}
|
|
@@ -3092,12 +3095,11 @@ function matchGroundBinaryClause(goal, clause) {
|
|
|
3092
3095
|
return { nextGoal: compound(bodyGoal.name, bodyArgs) };
|
|
3093
3096
|
}
|
|
3094
3097
|
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
}
|
|
3098
|
+
// isScalar is imported from term.js; use it as the canonical scalar test.
|
|
3099
|
+
const isScalarTerm = isScalar;
|
|
3098
3100
|
|
|
3099
3101
|
function sameScalarTerm(left, right) {
|
|
3100
|
-
return
|
|
3102
|
+
return isScalar(left) && isScalar(right) && left.type === right.type &&
|
|
3101
3103
|
(left.type === 'number' ? sameNumberValue(left.name, right.name) : left.name === right.name);
|
|
3102
3104
|
}
|
|
3103
3105
|
|
|
@@ -3131,9 +3133,9 @@ function sameResolvedGroundTerm(left, right, env) {
|
|
|
3131
3133
|
|
|
3132
3134
|
function groundChainKey(term) {
|
|
3133
3135
|
if (term?.type === COMPOUND) {
|
|
3134
|
-
|
|
3135
|
-
for (let i = 0; i < term.arity; i++)
|
|
3136
|
-
return
|
|
3136
|
+
const parts = [`${term.name}/${term.arity}`];
|
|
3137
|
+
for (let i = 0; i < term.arity; i++) parts.push(groundChainKey(term.args[i]));
|
|
3138
|
+
return parts.join('');
|
|
3137
3139
|
}
|
|
3138
3140
|
return `${term?.type ?? ''}:${term?.name ?? ''}`;
|
|
3139
3141
|
}
|
package/src/term.js
CHANGED
|
@@ -72,7 +72,7 @@ export class CompactListTerm {
|
|
|
72
72
|
mayContainVariable(name, env = null) {
|
|
73
73
|
if (String(name).startsWith(this._variablePrefix)) {
|
|
74
74
|
const indexText = String(name).slice(this._variablePrefix.length);
|
|
75
|
-
if (
|
|
75
|
+
if (RE_DIGIT_STR.test(indexText)) {
|
|
76
76
|
const index = BigInt(indexText);
|
|
77
77
|
if (index >= this._offset && index < this._offset + this._compactLength) return true;
|
|
78
78
|
}
|
|
@@ -1268,17 +1268,22 @@ export function copyResolved(term, env) {
|
|
|
1268
1268
|
}
|
|
1269
1269
|
|
|
1270
1270
|
export function termIsGround(term, env = new Env()) {
|
|
1271
|
+
// Defer the cycle-guard Set until we encounter a compound term, since
|
|
1272
|
+
// the overwhelming majority of ground checks are on acyclic clause data.
|
|
1271
1273
|
const pending = [term];
|
|
1272
|
-
|
|
1274
|
+
let seen = null;
|
|
1273
1275
|
while (pending.length > 0) {
|
|
1274
1276
|
const resolved = deref(pending.pop(), env);
|
|
1275
1277
|
if (resolved.type === VAR) return false;
|
|
1278
|
+
const arity = resolved.args.length;
|
|
1279
|
+
if (arity === 0) continue;
|
|
1280
|
+
if (seen == null) seen = new Set();
|
|
1276
1281
|
if (seen.has(resolved)) continue;
|
|
1277
1282
|
seen.add(resolved);
|
|
1278
1283
|
// Visit leftmost arguments first. Lists and other recursive structures
|
|
1279
1284
|
// commonly carry their first unbound variable there, allowing a
|
|
1280
1285
|
// non-ground check to finish without walking the complete tail.
|
|
1281
|
-
for (let index =
|
|
1286
|
+
for (let index = arity - 1; index >= 0; index--) {
|
|
1282
1287
|
pending.push(resolved.args[index]);
|
|
1283
1288
|
}
|
|
1284
1289
|
}
|
|
@@ -1287,11 +1292,18 @@ export function termIsGround(term, env = new Env()) {
|
|
|
1287
1292
|
|
|
1288
1293
|
const graphicAtomChars = new Set('!#$&*+-/<=>@^~\\'.split(''));
|
|
1289
1294
|
|
|
1295
|
+
const RE_LOWER_IDENT = /^[a-z][A-Za-z0-9_]*$/;
|
|
1296
|
+
const RE_UPPER_IDENT = /^(?:_|[A-Z_][A-Za-z0-9_]*)$/;
|
|
1297
|
+
const RE_LEGACY_VAR = /^\?(?:[A-Za-z_][A-Za-z0-9_]*)?$/;
|
|
1298
|
+
const RE_FLOAT = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/;
|
|
1299
|
+
const RE_DIGIT_STR = /^\d+$/;
|
|
1300
|
+
const RE_SANITIZE_VAR = /[^A-Za-z0-9_]/g;
|
|
1301
|
+
const RE_UPPER_START = /^[A-Z_]/;
|
|
1290
1302
|
function atomNeedsQuotes(name) {
|
|
1291
1303
|
if (!name) return true;
|
|
1292
1304
|
if (name === '[]' || name === '{}') return false;
|
|
1293
1305
|
if (name === '\\+' || name === '+' || name === '-' || name === '\\') return true;
|
|
1294
|
-
if (
|
|
1306
|
+
if (RE_LOWER_IDENT.test(name)) return false;
|
|
1295
1307
|
for (const ch of name) if (!graphicAtomChars.has(ch)) return true;
|
|
1296
1308
|
return false;
|
|
1297
1309
|
}
|
|
@@ -1322,11 +1334,11 @@ function legacyVariableToIso(name) {
|
|
|
1322
1334
|
|
|
1323
1335
|
function writeVariable(name) {
|
|
1324
1336
|
name = String(name ?? '');
|
|
1325
|
-
if (
|
|
1326
|
-
if (
|
|
1327
|
-
const sanitized = name.replace(
|
|
1337
|
+
if (RE_LEGACY_VAR.test(name)) return legacyVariableToIso(name);
|
|
1338
|
+
if (RE_UPPER_IDENT.test(name)) return name;
|
|
1339
|
+
const sanitized = name.replace(RE_SANITIZE_VAR, '_');
|
|
1328
1340
|
if (!sanitized) return '_';
|
|
1329
|
-
return
|
|
1341
|
+
return RE_UPPER_START.test(sanitized) ? sanitized : `_${sanitized}`;
|
|
1330
1342
|
}
|
|
1331
1343
|
|
|
1332
1344
|
function writeString(value, quoteStrings) {
|
|
@@ -1363,7 +1375,7 @@ function quotedListSplice(term, env, doubleQuotes) {
|
|
|
1363
1375
|
if (item.type !== ATOM || Array.from(item.name).length !== 1) return null;
|
|
1364
1376
|
characters.push(item.name);
|
|
1365
1377
|
} else {
|
|
1366
|
-
if (item.type !== NUMBER ||
|
|
1378
|
+
if (item.type !== NUMBER || !RE_DIGIT_STR.test(item.name)) return null;
|
|
1367
1379
|
const code = BigInt(item.name);
|
|
1368
1380
|
if (code < 0n || code > 0x10ffffn || (code >= 0xd800n && code <= 0xdfffn)) return null;
|
|
1369
1381
|
characters.push(String.fromCodePoint(Number(code)));
|
|
@@ -1558,12 +1570,16 @@ function variableRank(name, ranks) {
|
|
|
1558
1570
|
return rank;
|
|
1559
1571
|
}
|
|
1560
1572
|
|
|
1573
|
+
// ISO standard order: variables < numbers < atoms < strings < compound.
|
|
1574
|
+
// Defined once to avoid allocating a fresh object literal on every comparison.
|
|
1575
|
+
const TYPE_ORDER = { [VAR]: 0, [NUMBER]: 1, [ATOM]: 2, [STRING]: 3, [COMPOUND]: 4 };
|
|
1576
|
+
const EMPTY_ENV = new Env();
|
|
1577
|
+
|
|
1561
1578
|
function compareTermsWithRanks(left, right, variableRanks) {
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
const
|
|
1566
|
-
const rr = rank(right);
|
|
1579
|
+
left = deref(left, EMPTY_ENV);
|
|
1580
|
+
right = deref(right, EMPTY_ENV);
|
|
1581
|
+
const lr = TYPE_ORDER[left.type] ?? 0;
|
|
1582
|
+
const rr = TYPE_ORDER[right.type] ?? 0;
|
|
1567
1583
|
if (lr !== rr) return lr < rr ? -1 : 1;
|
|
1568
1584
|
if (left.type === NUMBER) {
|
|
1569
1585
|
const leftInteger = isDecimalInteger(left.name);
|
|
@@ -1587,8 +1603,9 @@ function compareTermsWithRanks(left, right, variableRanks) {
|
|
|
1587
1603
|
return 0;
|
|
1588
1604
|
}
|
|
1589
1605
|
|
|
1606
|
+
const RE_DECIMAL_INTEGER = /^-?\d+$/;
|
|
1590
1607
|
export function isDecimalInteger(text) {
|
|
1591
|
-
return
|
|
1608
|
+
return RE_DECIMAL_INTEGER.test(text ?? '');
|
|
1592
1609
|
}
|
|
1593
1610
|
|
|
1594
1611
|
export function compareIntegerText(left, right) {
|
|
@@ -1599,7 +1616,7 @@ export function compareIntegerText(left, right) {
|
|
|
1599
1616
|
|
|
1600
1617
|
export function parseFiniteNumber(text) {
|
|
1601
1618
|
if (text == null || text === '') return null;
|
|
1602
|
-
if (
|
|
1619
|
+
if (!RE_FLOAT.test(text)) return null;
|
|
1603
1620
|
const n = Number(text);
|
|
1604
1621
|
return Number.isFinite(n) ? n : null;
|
|
1605
1622
|
}
|
package/src/wfs.js
CHANGED
|
@@ -21,9 +21,15 @@ import {
|
|
|
21
21
|
resolvePatternTerm,
|
|
22
22
|
} from './datalog-common.js';
|
|
23
23
|
|
|
24
|
+
const _wfsScalarKeyCache = new WeakMap();
|
|
24
25
|
function scalarKey(term) {
|
|
25
|
-
|
|
26
|
-
return
|
|
26
|
+
const cached = _wfsScalarKeyCache.get(term);
|
|
27
|
+
if (cached != null) return cached;
|
|
28
|
+
const key = term.type === 'number'
|
|
29
|
+
? `number\u0000${numberValueKey(term.name)}`
|
|
30
|
+
: `${term.type}\u0000${term.name}`;
|
|
31
|
+
_wfsScalarKeyCache.set(term, key);
|
|
32
|
+
return key;
|
|
27
33
|
}
|
|
28
34
|
|
|
29
35
|
function sameScalar(left, right) {
|
package/src/write.js
CHANGED
|
@@ -5,8 +5,14 @@ import {
|
|
|
5
5
|
} from './term.js';
|
|
6
6
|
|
|
7
7
|
const graphicAtomCharacters = new Set('!#$&*+-./<=>?@^~\\'.split(''));
|
|
8
|
-
const dottedGraphicAtomCharacters = graphicAtomCharacters;
|
|
9
8
|
const compactInfixOperators = new Set([':', '..']);
|
|
9
|
+
const RE_LOWER_WORD = /^[a-z][A-Za-z0-9_]*$/;
|
|
10
|
+
const RE_ALNUM_IDENT = /^[A-Za-z0-9_]$/;
|
|
11
|
+
const RE_DIGITS_ONLY = /^\d+$/;
|
|
12
|
+
const RE_LEGACY_VAR_W = /^\?(?:[A-Za-z_][A-Za-z0-9_]*)?$/;
|
|
13
|
+
const RE_UPPER_IDENT_W = /^(?:_|[A-Z_][A-Za-z0-9_]*)$/;
|
|
14
|
+
const RE_SANITIZE_W = /[^A-Za-z0-9_]/g;
|
|
15
|
+
const RE_UPPER_START_W = /^[A-Z_]/;
|
|
10
16
|
|
|
11
17
|
function quotedControlEscape(ch) {
|
|
12
18
|
if (ch === '\x00') return '\\0\\';
|
|
@@ -39,7 +45,7 @@ function atomNeedsQuotes(name) {
|
|
|
39
45
|
// be read as a bracketed comment and therefore still requires quoting.
|
|
40
46
|
if (name === '.') return true;
|
|
41
47
|
if (name.startsWith('/*')) return true;
|
|
42
|
-
if (
|
|
48
|
+
if (RE_LOWER_WORD.test(name)) return false;
|
|
43
49
|
for (const ch of name) if (!graphicAtomCharacters.has(ch)) return true;
|
|
44
50
|
return false;
|
|
45
51
|
}
|
|
@@ -60,7 +66,7 @@ function writeAtom(name) {
|
|
|
60
66
|
|
|
61
67
|
function isDottedGraphicAtom(name) {
|
|
62
68
|
return name.includes('.') && [...name].some((ch) => ch !== '.') && !name.startsWith('/*') &&
|
|
63
|
-
[...name].every((ch) =>
|
|
69
|
+
[...name].every((ch) => graphicAtomCharacters.has(ch));
|
|
64
70
|
}
|
|
65
71
|
|
|
66
72
|
function compactBoundaryNeedsSpace(left, right) {
|
|
@@ -81,12 +87,12 @@ function compactBoundaryNeedsSpace(left, right) {
|
|
|
81
87
|
// neighbouring identifier/number token. Most predefined word operators
|
|
82
88
|
// are handled explicitly below; this also protects quoted/custom cases that
|
|
83
89
|
// render without punctuation.
|
|
84
|
-
if (
|
|
90
|
+
if (RE_ALNUM_IDENT.test(a) && RE_ALNUM_IDENT.test(b)) return true;
|
|
85
91
|
return false;
|
|
86
92
|
}
|
|
87
93
|
|
|
88
94
|
function isWordOperatorToken(token) {
|
|
89
|
-
return
|
|
95
|
+
return RE_LOWER_WORD.test(token);
|
|
90
96
|
}
|
|
91
97
|
|
|
92
98
|
function compactPrefixOperator(token, argument) {
|
|
@@ -100,7 +106,7 @@ function compactPrefixOperator(token, argument) {
|
|
|
100
106
|
}
|
|
101
107
|
|
|
102
108
|
function quotedOperatorAfterNumericNeedsSpace(left, token) {
|
|
103
|
-
if (!token.startsWith("'") ||
|
|
109
|
+
if (!token.startsWith("'") || !RE_DIGITS_ONLY.test(left)) return false;
|
|
104
110
|
const value = Number(left);
|
|
105
111
|
// `0'X` starts character-code notation and bases 2..36 start based-number
|
|
106
112
|
// notation. Base 1 and values above 36 do not, so they need no layout
|
|
@@ -136,11 +142,11 @@ function legacyVariableToIso(name) {
|
|
|
136
142
|
|
|
137
143
|
function writeVariable(name) {
|
|
138
144
|
name = String(name ?? '');
|
|
139
|
-
if (
|
|
140
|
-
if (
|
|
141
|
-
const sanitized = name.replace(
|
|
145
|
+
if (RE_LEGACY_VAR_W.test(name)) return legacyVariableToIso(name);
|
|
146
|
+
if (RE_UPPER_IDENT_W.test(name)) return name;
|
|
147
|
+
const sanitized = name.replace(RE_SANITIZE_W, '_');
|
|
142
148
|
if (!sanitized) return '_';
|
|
143
|
-
return
|
|
149
|
+
return RE_UPPER_START_W.test(sanitized) ? sanitized : `_${sanitized}`;
|
|
144
150
|
}
|
|
145
151
|
|
|
146
152
|
function writeString(value) {
|
|
@@ -158,7 +164,7 @@ function quotedListCharacter(item, doubleQuotes) {
|
|
|
158
164
|
return item.name;
|
|
159
165
|
}
|
|
160
166
|
if (doubleQuotes === 'codes') {
|
|
161
|
-
if (item.type !== NUMBER ||
|
|
167
|
+
if (item.type !== NUMBER || !RE_DIGITS_ONLY.test(item.name)) return null;
|
|
162
168
|
const code = BigInt(item.name);
|
|
163
169
|
if (code < 0n || code > 0x10ffffn || (code >= 0xd800n && code <= 0xdfffn)) return null;
|
|
164
170
|
return String.fromCodePoint(Number(code));
|
|
@@ -198,7 +204,7 @@ function operatorName(name) {
|
|
|
198
204
|
// output must use the unquoted `|` token (WG17 #181/#290).
|
|
199
205
|
if (name === '|') return '|';
|
|
200
206
|
if (name === '.' || name.startsWith('/*')) return quoteAtom(name);
|
|
201
|
-
if (
|
|
207
|
+
if (RE_LOWER_WORD.test(name)) return name;
|
|
202
208
|
if (/^[!#$&*+\-./<=>?@^~\\;:]+$/.test(name)) return name;
|
|
203
209
|
return quoteAtom(name);
|
|
204
210
|
}
|
|
@@ -335,7 +341,7 @@ function format(term, env, options, table, maxPriority = 1200, context = 'term')
|
|
|
335
341
|
|
|
336
342
|
if (options.numbervars && resolved.type === COMPOUND && resolved.name === '$VAR' && resolved.arity === 1) {
|
|
337
343
|
const index = deref(resolved.args[0], env);
|
|
338
|
-
if (index.type === NUMBER &&
|
|
344
|
+
if (index.type === NUMBER && RE_DIGITS_ONLY.test(index.name)) {
|
|
339
345
|
const name = writeNumberedVariable(Number(index.name));
|
|
340
346
|
if (name != null) return name;
|
|
341
347
|
}
|
|
@@ -405,7 +411,7 @@ function format(term, env, options, table, maxPriority = 1200, context = 'term')
|
|
|
405
411
|
const childNumbervar = options.numbervars && child.type === COMPOUND &&
|
|
406
412
|
child.name === '$VAR' && child.arity === 1 && (() => {
|
|
407
413
|
const index = deref(child.args[0], env);
|
|
408
|
-
return index.type === NUMBER &&
|
|
414
|
+
return index.type === NUMBER && RE_DIGITS_ONLY.test(index.name) &&
|
|
409
415
|
writeNumberedVariable(Number(index.name)) != null;
|
|
410
416
|
})();
|
|
411
417
|
const childUsesSpecialNotation = isCons(child) ||
|
|
@@ -59,13 +59,16 @@ export function runNeumerkelHarnessTests(reporter = new TestReporter()) {
|
|
|
59
59
|
}
|
|
60
60
|
});
|
|
61
61
|
|
|
62
|
-
reporter.test('latest strict syntax
|
|
62
|
+
reporter.test('latest strict syntax allows Neumerkel #379 parenthesized --> atom', () => {
|
|
63
|
+
// Upstream Codex expectation for #379: writeq((-->)/2). -> (-->)/2
|
|
64
|
+
// (-->)/2 is valid: parentheses make --> an ordinary atom; only bare
|
|
65
|
+
// --> followed by / in a predicate indicator is processor-defined syntax.
|
|
63
66
|
const item = {
|
|
64
67
|
id: 379,
|
|
65
68
|
query: 'writeq((-->)/2).',
|
|
66
69
|
input: 'writeq((-->)/2).',
|
|
67
70
|
readCount: 1,
|
|
68
|
-
expected: '
|
|
71
|
+
expected: '(-->)/2',
|
|
69
72
|
};
|
|
70
73
|
const actual = executeWg17Item(item);
|
|
71
74
|
if (!matchesUpstreamExpectation(item.expected, actual, item)) {
|
package/test/run-regression.mjs
CHANGED
|
@@ -1956,8 +1956,10 @@ c4 ?- call((!;1)).
|
|
|
1956
1956
|
},
|
|
1957
1957
|
},
|
|
1958
1958
|
{
|
|
1959
|
-
name: 'ISO predicate indicators require parentheses around operator atoms',
|
|
1959
|
+
name: 'ISO predicate indicators require parentheses around bare operator atoms',
|
|
1960
1960
|
run: () => {
|
|
1961
|
+
// Bare --> /2 (without parens) is still rejected in all modes because
|
|
1962
|
+
// --> is an infix/prefix operator atom that cannot appear as a bare operand.
|
|
1961
1963
|
let caught = null;
|
|
1962
1964
|
try {
|
|
1963
1965
|
parseGoalText('writeq(--> /2)');
|
|
@@ -1966,20 +1968,22 @@ c4 ?- call((!;1)).
|
|
|
1966
1968
|
}
|
|
1967
1969
|
if (!caught) throw new Error('bare operator predicate indicator unexpectedly parsed');
|
|
1968
1970
|
assertIncludes(caught.message, 'operator atom', 'bare indicator syntax rejection');
|
|
1971
|
+
// (-->)/2 with parentheses is legal in both normal and strict ISO modes.
|
|
1972
|
+
// Neumerkel #379: upstream Codex expectation is (-->)/2 -> (-->)/2 (success).
|
|
1973
|
+
// Parentheses make --> an ordinary atom argument; ISO 7.10.3 does not
|
|
1974
|
+
// restrict which atoms may appear as the name in a Name/Arity indicator.
|
|
1969
1975
|
parseGoalText('writeq((-->)/2)');
|
|
1970
1976
|
assertEqual(
|
|
1971
1977
|
run('', { goal: 'writeq((-->)/2)' }).stdout,
|
|
1972
1978
|
'(-->)/2writeq((-->) / 2).\n',
|
|
1973
1979
|
'parenthesized operator indicator stays legal outside strict ISO',
|
|
1974
1980
|
);
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
if (!caught) throw new Error('Neumerkel #379 strict indicator unexpectedly parsed');
|
|
1982
|
-
assertIncludes(caught.message, 'strict ISO predicate indicator', 'Neumerkel #379 strict syntax rejection');
|
|
1981
|
+
parseGoalText('writeq((-->)/2)', { isoStrict: true });
|
|
1982
|
+
assertEqual(
|
|
1983
|
+
run('', { isoStrict: true, goal: 'writeq((-->)/2)' }).stdout,
|
|
1984
|
+
'(-->)/2writeq((-->) / 2).\n',
|
|
1985
|
+
'Neumerkel #379: parenthesized --> is legal in strict ISO',
|
|
1986
|
+
);
|
|
1983
1987
|
},
|
|
1984
1988
|
},
|
|
1985
1989
|
{
|