eyeprolog 1.5.35 → 1.5.37
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/README.md +1 -1
- package/examples/bulk-stream-write.pl +42 -0
- package/examples/output/bulk-stream-write.pl +1 -0
- package/package.json +1 -1
- package/playground.html +1 -0
- package/src/explain.js +6 -1
- package/src/io.js +15 -0
- package/src/parser.js +16 -10
- package/src/program-analysis.js +3 -1
- package/src/program.js +6 -3
- package/src/term.js +33 -16
- package/src/wfs.js +8 -2
- package/src/write.js +19 -12
- package/test/bench/benchmarks.json +9 -27
- package/test/run-benchmark-tests.mjs +3 -3
- package/the-art-of-eyeprolog.md +14 -13
package/README.md
CHANGED
|
@@ -86,7 +86,7 @@ The checked [Symbiotic Knowledge Graphs example](examples/symbiotic-knowledge-gr
|
|
|
86
86
|
The same RDF → Prolog → RDF boundary is exercised by five additional checked scenarios: [cross-organization data sharing](https://eyereasoner.github.io/eyeprolog/examples/deck/cross-organization-data-sharing), [explainable EV-depot configuration](https://eyereasoner.github.io/eyeprolog/examples/deck/explainable-ev-depot-configuration), [operational incident response](https://eyereasoner.github.io/eyeprolog/examples/deck/operational-incident-response), [software supply-chain vulnerability response](https://eyereasoner.github.io/eyeprolog/examples/deck/sbom-vulnerability-response), and a [scientific evidence graph](https://eyereasoner.github.io/eyeprolog/examples/deck/scientific-evidence-graph). Together they cover policy decisions, reversible configuration reasoning, dependency-graph diagnosis, transitive SBOM exposure, and evidence aggregation with explicit disagreement.
|
|
87
87
|
|
|
88
88
|
## Benchmarks
|
|
89
|
-
EyeProlog has
|
|
89
|
+
EyeProlog has 19 checksum-protected wall-clock benchmarks spanning recursion/indexing, constraints, tabling/WFS, DCGs, Eyelet, search, term I/O, attributes, rewriting, and the classic Prolog naive-reverse workload. Short workloads are adaptively batched before timing so millisecond-scale noise is not mistaken for a regression. Run `npm run benchmark`; create a machine-local comparison point with `npm run benchmark:baseline`; use `npm run test:benchmark` for harness checks. The checked [`examples/bench.pl`](examples/bench.pl) preserves the classic Quintus 1984 `nrev/2` workload on a 30-element list. For a comparable LIPS number, run `npm run benchmark:lips`: it executes the classic failure-driven `dobench/1` and `dodummy/1` loops in Prolog, subtracts dummy-loop CPU time, and applies the historical 496 procedure calls per reversal. The generic benchmark table still shows a quick wall-clock LIPS estimate for `classic-nrev`, but `benchmark:lips` is the canonical engine-speed measurement. LIPS is a historical basic-engine-speed indicator, not a whole-system performance score. Details are in [*The Art of EyeProlog*](the-art-of-eyeprolog.md).
|
|
90
90
|
For the project policy on post-ISO-standard and WG17 compatibility features such as digit separators, see [ISO/WG17 compatibility extensions](test/conformance/ISO-WG17-EXTENSIONS.md).
|
|
91
91
|
## Development
|
|
92
92
|
```sh
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
% Bulk character-by-character writes to a non-console text stream.
|
|
2
|
+
%
|
|
3
|
+
% put_char/2 writes one character at a time to an open file stream, and each
|
|
4
|
+
% write logically appends at the current stream position (ISO 7.10.2.8). This
|
|
5
|
+
% is the common pattern for hand-rolled serializers that emit one character or
|
|
6
|
+
% code point per call rather than a single bulk write/1 call. The example
|
|
7
|
+
% writes a repeating a-z run to a temporary file, closes it, then reopens and
|
|
8
|
+
% reads the file back one character at a time with get_char/2 to confirm every
|
|
9
|
+
% character round-trips. The path is under /tmp so the source tree is
|
|
10
|
+
% unchanged.
|
|
11
|
+
|
|
12
|
+
%% goal: bulk_write_result(X0, X1)
|
|
13
|
+
|
|
14
|
+
bulk_write_chars(_, 0) :- !.
|
|
15
|
+
bulk_write_chars(Stream, N) :-
|
|
16
|
+
N > 0,
|
|
17
|
+
Code is 0'a + (N mod 26),
|
|
18
|
+
char_code(Char, Code),
|
|
19
|
+
put_char(Stream, Char),
|
|
20
|
+
N1 is N - 1,
|
|
21
|
+
bulk_write_chars(Stream, N1).
|
|
22
|
+
|
|
23
|
+
count_chars(Stream, Acc, Count) :-
|
|
24
|
+
get_char(Stream, Char),
|
|
25
|
+
count_chars_step(Char, Stream, Acc, Count).
|
|
26
|
+
|
|
27
|
+
count_chars_step(end_of_file, _, Count, Count) :- !.
|
|
28
|
+
count_chars_step(_, Stream, Acc, Count) :-
|
|
29
|
+
Acc1 is Acc + 1,
|
|
30
|
+
count_chars(Stream, Acc1, Count).
|
|
31
|
+
|
|
32
|
+
bulk_write_path('/tmp/eyeprolog-bulk-stream-write-example.txt').
|
|
33
|
+
|
|
34
|
+
bulk_write_result(Requested, Counted) :-
|
|
35
|
+
Requested = 5000,
|
|
36
|
+
bulk_write_path(Path),
|
|
37
|
+
open(Path, write, Out, [type(text)]),
|
|
38
|
+
bulk_write_chars(Out, Requested),
|
|
39
|
+
close(Out),
|
|
40
|
+
open(Path, read, In, [type(text)]),
|
|
41
|
+
count_chars(In, 0, Counted),
|
|
42
|
+
close(In).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
bulk_write_result(5000, 5000).
|
package/package.json
CHANGED
package/playground.html
CHANGED
package/src/explain.js
CHANGED
|
@@ -218,7 +218,12 @@ function builtinIsUsedForGoal(def, solver, goal, env) {
|
|
|
218
218
|
function selectReadyDeterministicBuiltin(goals, env, registry) {
|
|
219
219
|
for (let i = 0; i < goals.length; i++) {
|
|
220
220
|
const goal = goals[i];
|
|
221
|
-
|
|
221
|
+
// Match solver.js's goal-type check: a 0-arity builtin (ATOM) is just as
|
|
222
|
+
// eligible for this fast path as a COMPOUND one. No bundled builtin
|
|
223
|
+
// currently pairs arity 0 with a custom ready() gate, so this has no
|
|
224
|
+
// observable effect today, but it keeps the proof-explanation path from
|
|
225
|
+
// silently diverging from actual execution if one is added later.
|
|
226
|
+
if (goal.type !== COMPOUND && goal.type !== ATOM) continue;
|
|
222
227
|
const def = registry.get(goal.name, goal.arity);
|
|
223
228
|
if (!def?.deterministic || typeof def.ready !== 'function') continue;
|
|
224
229
|
if (typeof def.shouldUse === 'function') continue;
|
package/src/io.js
CHANGED
|
@@ -186,6 +186,21 @@ export class StreamManager {
|
|
|
186
186
|
}
|
|
187
187
|
const text = String(value);
|
|
188
188
|
const content = String(stream.content);
|
|
189
|
+
// The overwhelming majority of text-stream writes append at the current
|
|
190
|
+
// end of the sink: bulk write/1 calls and repeated put_char/put_code
|
|
191
|
+
// loops alike. Handling that case as a plain concatenation, separate
|
|
192
|
+
// from the general reposition rebuild below, keeps the two ISO-distinct
|
|
193
|
+
// cases self-documenting instead of folding them into one three-part
|
|
194
|
+
// slice expression that always pays for the overwrite case's shape.
|
|
195
|
+
// This split is for clarity, not raw throughput: measured end to end,
|
|
196
|
+
// the general expression already performs comparably for this pattern.
|
|
197
|
+
if (stream.position === content.length) {
|
|
198
|
+
stream.content = content + text;
|
|
199
|
+
stream.position += text.length;
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
// 7.10.2.8: output after repositioning overwrites the existing sink
|
|
203
|
+
// contents at the selected stream position rather than always appending.
|
|
189
204
|
const end = Math.min(content.length, stream.position + text.length);
|
|
190
205
|
stream.content = `${content.slice(0, stream.position)}${text}${content.slice(end)}`;
|
|
191
206
|
stream.position += text.length;
|
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();
|
|
@@ -1816,14 +1822,14 @@ export function parseNumberTokenText(text, options = {}) {
|
|
|
1816
1822
|
value = controls[escaped];
|
|
1817
1823
|
} else if (escaped === 'x') {
|
|
1818
1824
|
let digits = '';
|
|
1819
|
-
while (
|
|
1825
|
+
while (RE_HEX_DIGIT.test(source[position] ?? '')) digits += source[position++];
|
|
1820
1826
|
if (!digits || source[position++] !== '\\') throw invalidNumberTokenError;
|
|
1821
1827
|
const code = Number.parseInt(digits, 16);
|
|
1822
1828
|
if (code > 0x10ffff || (code >= 0xd800 && code <= 0xdfff)) throw invalidNumberTokenError;
|
|
1823
1829
|
value = String.fromCodePoint(code);
|
|
1824
|
-
} else if (
|
|
1830
|
+
} else if (RE_OCTAL_DIGIT.test(escaped)) {
|
|
1825
1831
|
let digits = escaped;
|
|
1826
|
-
while (
|
|
1832
|
+
while (RE_OCTAL_DIGIT.test(source[position] ?? '')) digits += source[position++];
|
|
1827
1833
|
if (source[position++] !== '\\') throw invalidNumberTokenError;
|
|
1828
1834
|
const code = Number.parseInt(digits, 8);
|
|
1829
1835
|
if (code > 0x10ffff || (code >= 0xd800 && code <= 0xdfff)) throw invalidNumberTokenError;
|
package/src/program-analysis.js
CHANGED
|
@@ -65,7 +65,9 @@ function reachableIndexesTransposed(target, deps, candidates) {
|
|
|
65
65
|
const reverse = new Map();
|
|
66
66
|
for (const from of candidates) {
|
|
67
67
|
if (!reverse.has(from)) reverse.set(from, []);
|
|
68
|
-
|
|
68
|
+
const edges = deps[from];
|
|
69
|
+
if (edges == null) continue;
|
|
70
|
+
for (const to of edges) {
|
|
69
71
|
if (!candidates.has(to)) continue;
|
|
70
72
|
let bucket = reverse.get(to);
|
|
71
73
|
if (bucket == null) { bucket = []; reverse.set(to, bucket); }
|
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/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
|
@@ -6,6 +6,13 @@ import {
|
|
|
6
6
|
|
|
7
7
|
const graphicAtomCharacters = new Set('!#$&*+-./<=>?@^~\\'.split(''));
|
|
8
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_]/;
|
|
9
16
|
|
|
10
17
|
function quotedControlEscape(ch) {
|
|
11
18
|
if (ch === '\x00') return '\\0\\';
|
|
@@ -38,7 +45,7 @@ function atomNeedsQuotes(name) {
|
|
|
38
45
|
// be read as a bracketed comment and therefore still requires quoting.
|
|
39
46
|
if (name === '.') return true;
|
|
40
47
|
if (name.startsWith('/*')) return true;
|
|
41
|
-
if (
|
|
48
|
+
if (RE_LOWER_WORD.test(name)) return false;
|
|
42
49
|
for (const ch of name) if (!graphicAtomCharacters.has(ch)) return true;
|
|
43
50
|
return false;
|
|
44
51
|
}
|
|
@@ -80,12 +87,12 @@ function compactBoundaryNeedsSpace(left, right) {
|
|
|
80
87
|
// neighbouring identifier/number token. Most predefined word operators
|
|
81
88
|
// are handled explicitly below; this also protects quoted/custom cases that
|
|
82
89
|
// render without punctuation.
|
|
83
|
-
if (
|
|
90
|
+
if (RE_ALNUM_IDENT.test(a) && RE_ALNUM_IDENT.test(b)) return true;
|
|
84
91
|
return false;
|
|
85
92
|
}
|
|
86
93
|
|
|
87
94
|
function isWordOperatorToken(token) {
|
|
88
|
-
return
|
|
95
|
+
return RE_LOWER_WORD.test(token);
|
|
89
96
|
}
|
|
90
97
|
|
|
91
98
|
function compactPrefixOperator(token, argument) {
|
|
@@ -99,7 +106,7 @@ function compactPrefixOperator(token, argument) {
|
|
|
99
106
|
}
|
|
100
107
|
|
|
101
108
|
function quotedOperatorAfterNumericNeedsSpace(left, token) {
|
|
102
|
-
if (!token.startsWith("'") ||
|
|
109
|
+
if (!token.startsWith("'") || !RE_DIGITS_ONLY.test(left)) return false;
|
|
103
110
|
const value = Number(left);
|
|
104
111
|
// `0'X` starts character-code notation and bases 2..36 start based-number
|
|
105
112
|
// notation. Base 1 and values above 36 do not, so they need no layout
|
|
@@ -135,11 +142,11 @@ function legacyVariableToIso(name) {
|
|
|
135
142
|
|
|
136
143
|
function writeVariable(name) {
|
|
137
144
|
name = String(name ?? '');
|
|
138
|
-
if (
|
|
139
|
-
if (
|
|
140
|
-
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, '_');
|
|
141
148
|
if (!sanitized) return '_';
|
|
142
|
-
return
|
|
149
|
+
return RE_UPPER_START_W.test(sanitized) ? sanitized : `_${sanitized}`;
|
|
143
150
|
}
|
|
144
151
|
|
|
145
152
|
function writeString(value) {
|
|
@@ -157,7 +164,7 @@ function quotedListCharacter(item, doubleQuotes) {
|
|
|
157
164
|
return item.name;
|
|
158
165
|
}
|
|
159
166
|
if (doubleQuotes === 'codes') {
|
|
160
|
-
if (item.type !== NUMBER ||
|
|
167
|
+
if (item.type !== NUMBER || !RE_DIGITS_ONLY.test(item.name)) return null;
|
|
161
168
|
const code = BigInt(item.name);
|
|
162
169
|
if (code < 0n || code > 0x10ffffn || (code >= 0xd800n && code <= 0xdfffn)) return null;
|
|
163
170
|
return String.fromCodePoint(Number(code));
|
|
@@ -197,7 +204,7 @@ function operatorName(name) {
|
|
|
197
204
|
// output must use the unquoted `|` token (WG17 #181/#290).
|
|
198
205
|
if (name === '|') return '|';
|
|
199
206
|
if (name === '.' || name.startsWith('/*')) return quoteAtom(name);
|
|
200
|
-
if (
|
|
207
|
+
if (RE_LOWER_WORD.test(name)) return name;
|
|
201
208
|
if (/^[!#$&*+\-./<=>?@^~\\;:]+$/.test(name)) return name;
|
|
202
209
|
return quoteAtom(name);
|
|
203
210
|
}
|
|
@@ -334,7 +341,7 @@ function format(term, env, options, table, maxPriority = 1200, context = 'term')
|
|
|
334
341
|
|
|
335
342
|
if (options.numbervars && resolved.type === COMPOUND && resolved.name === '$VAR' && resolved.arity === 1) {
|
|
336
343
|
const index = deref(resolved.args[0], env);
|
|
337
|
-
if (index.type === NUMBER &&
|
|
344
|
+
if (index.type === NUMBER && RE_DIGITS_ONLY.test(index.name)) {
|
|
338
345
|
const name = writeNumberedVariable(Number(index.name));
|
|
339
346
|
if (name != null) return name;
|
|
340
347
|
}
|
|
@@ -404,7 +411,7 @@ function format(term, env, options, table, maxPriority = 1200, context = 'term')
|
|
|
404
411
|
const childNumbervar = options.numbervars && child.type === COMPOUND &&
|
|
405
412
|
child.name === '$VAR' && child.arity === 1 && (() => {
|
|
406
413
|
const index = deref(child.args[0], env);
|
|
407
|
-
return index.type === NUMBER &&
|
|
414
|
+
return index.type === NUMBER && RE_DIGITS_ONLY.test(index.name) &&
|
|
408
415
|
writeNumberedVariable(Number(index.name)) != null;
|
|
409
416
|
})();
|
|
410
417
|
const childUsesSpecialNotation = isCons(child) ||
|
|
@@ -72,15 +72,6 @@
|
|
|
72
72
|
],
|
|
73
73
|
"expectedSha256": "b4a153234f3daf1e2ba6c26843349a1bc1919ebd9f9e98559835eb5ef915716b"
|
|
74
74
|
},
|
|
75
|
-
{
|
|
76
|
-
"name": "dcg-command",
|
|
77
|
-
"group": "dcg",
|
|
78
|
-
"file": "examples/dcg-command-parser.pl",
|
|
79
|
-
"goals": [
|
|
80
|
-
"dcg_example(X0, X1)"
|
|
81
|
-
],
|
|
82
|
-
"expectedSha256": "6f640fb0d327eb9139d3568cf7a86ad55a52afdbb0208ca35a70419be2bf1b9c"
|
|
83
|
-
},
|
|
84
75
|
{
|
|
85
76
|
"name": "dcg-expression",
|
|
86
77
|
"group": "dcg",
|
|
@@ -106,24 +97,6 @@
|
|
|
106
97
|
],
|
|
107
98
|
"expectedSha256": "a7c33872b86aa7877b9c38ec41c447fc96a328d40f512428993d662f28a097ad"
|
|
108
99
|
},
|
|
109
|
-
{
|
|
110
|
-
"name": "clpz-register-allocation",
|
|
111
|
-
"group": "clpz",
|
|
112
|
-
"file": "examples/register-allocation.pl",
|
|
113
|
-
"goals": [
|
|
114
|
-
"registerAnswer(X0, X1)"
|
|
115
|
-
],
|
|
116
|
-
"expectedSha256": "1ed0b4a90deff2908463774d0025210d58b89366efb5d82b94ff5b51daca046b"
|
|
117
|
-
},
|
|
118
|
-
{
|
|
119
|
-
"name": "clpb-feature-model",
|
|
120
|
-
"group": "clpb",
|
|
121
|
-
"file": "examples/clpb-feature-model.pl",
|
|
122
|
-
"goals": [
|
|
123
|
-
"feature_plan(X0)"
|
|
124
|
-
],
|
|
125
|
-
"expectedSha256": "47abfd8a87efb595ffb11c6c8cbda3cb5855e95124bc574e3cae387c12318f33"
|
|
126
|
-
},
|
|
127
100
|
{
|
|
128
101
|
"name": "attributed-variables",
|
|
129
102
|
"group": "attributes",
|
|
@@ -190,5 +163,14 @@
|
|
|
190
163
|
"type_answer(X0, X1)"
|
|
191
164
|
],
|
|
192
165
|
"expectedSha256": "acad9ddb995d9ddef091e17cc91659d36ed7f9014d6714fd40100d85a53ae203"
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
"name": "bulk-stream-write",
|
|
169
|
+
"group": "term-io",
|
|
170
|
+
"file": "examples/bulk-stream-write.pl",
|
|
171
|
+
"goals": [
|
|
172
|
+
"bulk_write_result(X0, X1)"
|
|
173
|
+
],
|
|
174
|
+
"expectedSha256": "8471da130a4a3a92dd31ee6cf285b1e129886177d7be367d51650a48b8920324"
|
|
193
175
|
}
|
|
194
176
|
]
|
|
@@ -42,7 +42,7 @@ function runWorker(item) {
|
|
|
42
42
|
]);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
ok(Array.isArray(manifest) && manifest.length ===
|
|
45
|
+
ok(Array.isArray(manifest) && manifest.length === 19, 'benchmark manifest should contain exactly 19 representative workloads');
|
|
46
46
|
ok(new Set(manifest.map((item) => item.name)).size === manifest.length, 'benchmark names should be unique');
|
|
47
47
|
const classicNrev = manifest.find((item) => item.name === 'classic-nrev');
|
|
48
48
|
ok(classicNrev?.logicalInferences === 496, 'classic nrev should retain the traditional 496-call LIPS accounting');
|
|
@@ -61,7 +61,7 @@ for (const item of manifest) {
|
|
|
61
61
|
|
|
62
62
|
const adaptive = await spawnJson([
|
|
63
63
|
path.join(root, 'test', 'benchmark.mjs'),
|
|
64
|
-
'--filter', 'dcg-
|
|
64
|
+
'--filter', 'dcg-expression',
|
|
65
65
|
'--runs', '1',
|
|
66
66
|
'--warmup', '0',
|
|
67
67
|
'--target-ms', '50',
|
|
@@ -69,7 +69,7 @@ const adaptive = await spawnJson([
|
|
|
69
69
|
]);
|
|
70
70
|
ok(adaptive.results.length === 1, 'adaptive benchmark smoke test should select one workload');
|
|
71
71
|
ok(adaptive.results[0].batchSize > 1, 'adaptive benchmark smoke test should batch a short workload');
|
|
72
|
-
ok(adaptive.results[0].sha256 === manifest.find((item) => item.name === 'dcg-
|
|
72
|
+
ok(adaptive.results[0].sha256 === manifest.find((item) => item.name === 'dcg-expression').expectedSha256,
|
|
73
73
|
'adaptive batching should preserve the semantic checksum');
|
|
74
74
|
|
|
75
75
|
const nrev = await spawnJson([
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -5867,9 +5867,9 @@ and reports the portable ISO `type_error(list)` error term.
|
|
|
5867
5867
|
#### A bidirectional expression grammar
|
|
5868
5868
|
|
|
5869
5869
|
DCGs become more useful when the grammar produces a structured term rather than
|
|
5870
|
-
merely accepting a token list.
|
|
5870
|
+
merely accepting a token list. The checked
|
|
5871
5871
|
[`dcg-expression-language.pl`](https://github.com/eyereasoner/eyeprolog/blob/main/examples/dcg-expression-language.pl)
|
|
5872
|
-
example implements a small arithmetic language in both directions.
|
|
5872
|
+
example implements a small arithmetic language in both directions. Its parser
|
|
5873
5873
|
respects precedence and left associativity while constructing an abstract syntax
|
|
5874
5874
|
tree:
|
|
5875
5875
|
|
|
@@ -5887,17 +5887,17 @@ additive_tail(AST, AST) --> [].
|
|
|
5887
5887
|
|
|
5888
5888
|
The accumulator removes left recursion without moving parsing into JavaScript.
|
|
5889
5889
|
A second DCG walks the AST in the other direction and emits only the parentheses
|
|
5890
|
-
needed to preserve its structure.
|
|
5890
|
+
needed to preserve its structure. The example therefore exercises parsing,
|
|
5891
5891
|
semantic actions, nonterminal-to-nonterminal state hand-off, generation,
|
|
5892
5892
|
backtracking, `phrase/3` remainder handling, and AST-to-token-to-AST
|
|
5893
|
-
round-tripping.
|
|
5893
|
+
round-tripping. The checked answers are in
|
|
5894
5894
|
[`examples/output/dcg-expression-language.pl`](https://github.com/eyereasoner/eyeprolog/blob/main/examples/output/dcg-expression-language.pl).
|
|
5895
5895
|
|
|
5896
5896
|
#### Deep sequence hand-off
|
|
5897
5897
|
|
|
5898
5898
|
`library(iso_ext)` provides the common `... //0` helper, which describes an
|
|
5899
|
-
arbitrary number of input elements.
|
|
5900
|
-
useful interoperability and stress-test relation.
|
|
5899
|
+
arbitrary number of input elements. It is not part of ISO Part 3, but it is a
|
|
5900
|
+
useful interoperability and stress-test relation. A compact hand-off test is:
|
|
5901
5901
|
|
|
5902
5902
|
```text
|
|
5903
5903
|
a --> ..., epsilon.
|
|
@@ -5905,11 +5905,11 @@ epsilon --> [].
|
|
|
5905
5905
|
```
|
|
5906
5906
|
|
|
5907
5907
|
Here the remaining sequence is repeatedly passed from `... //0` to another
|
|
5908
|
-
nonterminal.
|
|
5908
|
+
nonterminal. For a finite compact list, EyeProlog can scan the arbitrary
|
|
5909
5909
|
sequence iteratively instead of consuming one ordinary solver depth level per
|
|
5910
|
-
list cell.
|
|
5910
|
+
list cell. If the continuation is structurally proven to be a zero-width
|
|
5911
5911
|
identity grammar such as `epsilon//0`, the hand-off can be continued without
|
|
5912
|
-
constructing a fresh general clause-resolution frame at every suffix.
|
|
5912
|
+
constructing a fresh general clause-resolution frame at every suffix. The list
|
|
5913
5913
|
spine is still traversed; this is a control/allocation optimization rather than
|
|
5914
5914
|
an O(1) semantic shortcut.
|
|
5915
5915
|
|
|
@@ -6062,7 +6062,8 @@ contains 129 name/arity entries across 100 names.
|
|
|
6062
6062
|
`;/2` recognizes an `->/2` term on its left and implements the ISO
|
|
6063
6063
|
if-then-else commitment described above. Cuts and committed conditions are
|
|
6064
6064
|
operational controls; use ordinary relations when all alternatives should
|
|
6065
|
-
remain observable.
|
|
6065
|
+
remain observable.
|
|
6066
|
+
|
|
6066
6067
|
#### Definite clause grammar processing
|
|
6067
6068
|
|
|
6068
6069
|
- **`phrase(+Body,?Sequence)`** — Parses or generates `Sequence` with a Part 3 grammar body and requires complete consumption.
|
|
@@ -9743,7 +9744,7 @@ Review questions:
|
|
|
9743
9744
|
</figure>
|
|
9744
9745
|
|
|
9745
9746
|
The [examples directory](https://github.com/eyereasoner/eyeprolog/tree/main/examples/) is the book's executable companion. The
|
|
9746
|
-
top-level directory contains **
|
|
9747
|
+
top-level directory contains **226 self-contained runnable programs**. Every
|
|
9747
9748
|
source program has an exact answer file under
|
|
9748
9749
|
[examples/output](https://github.com/eyereasoner/eyeprolog/tree/main/examples/output/), and **61 selected programs** have a checked
|
|
9749
9750
|
explanation under [examples/proof](https://github.com/eyereasoner/eyeprolog/tree/main/examples/proof/). The thematic lists link every top-level program and open the program
|
|
@@ -10176,7 +10177,7 @@ npm run benchmark:baseline
|
|
|
10176
10177
|
npm run benchmark:lips
|
|
10177
10178
|
```
|
|
10178
10179
|
|
|
10179
|
-
The benchmark suite contains
|
|
10180
|
+
The benchmark suite contains 19 representative workloads and stores their
|
|
10180
10181
|
semantic output digests in the repository, while wall-clock baselines remain
|
|
10181
10182
|
machine-local under `.benchmarks/` because absolute timings are machine-specific.
|
|
10182
10183
|
Each benchmark runs in its own fresh Node worker. Inside that worker, one untimed
|
|
@@ -10368,7 +10369,7 @@ specifications.
|
|
|
10368
10369
|
|
|
10369
10370
|
- Kurt Gödel,
|
|
10370
10371
|
[“Über formal unentscheidbare Sätze der *Principia Mathematica* und
|
|
10371
|
-
verwandter Systeme I”](https://doi.org/10.1007/
|
|
10372
|
+
verwandter Systeme I”](https://doi.org/10.1007/BF01700692),
|
|
10372
10373
|
*Monatshefte für Mathematik und Physik* 38, 1931, pp. 173–198. The
|
|
10373
10374
|
incompleteness theorems establish intrinsic limits for sufficiently
|
|
10374
10375
|
expressive effectively axiomatized formal systems. Chapter 30 treats such
|