eyelang 1.3.7 → 1.3.9
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 +3 -1
- package/examples/socket-age.pl +4 -4
- package/examples/socket-family.pl +4 -4
- package/package.json +1 -1
- package/playground.html +12 -0
- package/src/cli.js +4 -4
- package/src/parser.js +130 -1
- package/src/program.js +33 -27
- package/test/run-all.js +0 -0
- package/test/run-conformance.js +0 -0
- package/test/run-examples.js +0 -0
- package/test/run-regression.js +53 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# eyelang
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/eyelang)
|
|
4
4
|
[](https://doi.org/10.5281/zenodo.20342331)
|
|
5
5
|
|
|
6
6
|
eyelang is a small rule engine for Prolog-style Horn clauses over ordinary terms, lists, arithmetic, strings, and finite search. The command-line executable is `eyelang`.
|
|
@@ -458,6 +458,8 @@ The repository includes examples for recursion, graph reachability, finite searc
|
|
|
458
458
|
| [`service-impact.pl`](https://github.com/eyereasoner/eyelang/blob/main/examples/service-impact.pl) | Analyzes service impact over cyclic dependencies. | [`output/service-impact.pl`](https://github.com/eyereasoner/eyelang/blob/main/examples/output/service-impact.pl) |
|
|
459
459
|
| [`sieve.pl`](https://github.com/eyereasoner/eyelang/blob/main/examples/sieve.pl) | Enumerates primes with a sieve-style program. | [`output/sieve.pl`](https://github.com/eyereasoner/eyelang/blob/main/examples/output/sieve.pl) |
|
|
460
460
|
| [`skolem-functions.pl`](https://github.com/eyereasoner/eyelang/blob/main/examples/skolem-functions.pl) | Generates deterministic functional terms. | [`output/skolem-functions.pl`](https://github.com/eyereasoner/eyelang/blob/main/examples/output/skolem-functions.pl) |
|
|
461
|
+
| [`socket-age.pl`](https://github.com/eyereasoner/eyelang/blob/main/examples/socket-age.pl) | Shows socket-declared age reasoning inputs and plugs. | [`output/socket-age.pl`](https://github.com/eyereasoner/eyelang/blob/main/examples/output/socket-age.pl) |
|
|
462
|
+
| [`socket-family.pl`](https://github.com/eyereasoner/eyelang/blob/main/examples/socket-family.pl) | Shows socket-declared family-source inputs and ancestry rules. | [`output/socket-family.pl`](https://github.com/eyereasoner/eyelang/blob/main/examples/output/socket-family.pl) |
|
|
461
463
|
| [`socrates.pl`](https://github.com/eyereasoner/eyelang/blob/main/examples/socrates.pl) | Derives that Socrates is mortal. | [`output/socrates.pl`](https://github.com/eyereasoner/eyelang/blob/main/examples/output/socrates.pl) |
|
|
462
464
|
| [`statistics-summary.pl`](https://github.com/eyereasoner/eyelang/blob/main/examples/statistics-summary.pl) | Computes population statistics for a sample. | [`output/statistics-summary.pl`](https://github.com/eyereasoner/eyelang/blob/main/examples/output/statistics-summary.pl) |
|
|
463
465
|
| [`sudoku.pl`](https://github.com/eyereasoner/eyelang/blob/main/examples/sudoku.pl) | Solves generic 9x9 Sudoku strings through the sudoku/2 builtin. | [`output/sudoku.pl`](https://github.com/eyereasoner/eyelang/blob/main/examples/output/sudoku.pl) |
|
package/examples/socket-age.pl
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
%
|
|
1
|
+
% socket-age.pl
|
|
2
2
|
%
|
|
3
|
-
% A small runnable
|
|
3
|
+
% A small runnable eyelang Socket example for age reasoning.
|
|
4
4
|
%
|
|
5
|
-
% The socket facts are ordinary
|
|
5
|
+
% The socket facts are ordinary eyelang data. They document the semantic
|
|
6
6
|
% openings that this rule module expects:
|
|
7
7
|
%
|
|
8
8
|
% - a patient registry that provides birthDay/2
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
% The plug facts say which concrete providers are connected.
|
|
13
13
|
%
|
|
14
14
|
% Run:
|
|
15
|
-
%
|
|
15
|
+
% eyelang socket-age.pl
|
|
16
16
|
|
|
17
17
|
materialize(ageAbove, 2).
|
|
18
18
|
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
%
|
|
1
|
+
% socket-family.pl
|
|
2
2
|
%
|
|
3
|
-
% A small runnable
|
|
3
|
+
% A small runnable eyelang Socket example.
|
|
4
4
|
%
|
|
5
|
-
% The socket facts below are ordinary
|
|
5
|
+
% The socket facts below are ordinary eyelang data. They document the
|
|
6
6
|
% semantic opening: this reasoning module expects a provider for parent/2.
|
|
7
7
|
% The plug fact says which provider is connected.
|
|
8
8
|
%
|
|
9
9
|
% Run:
|
|
10
|
-
%
|
|
10
|
+
% eyelang socket-family.pl
|
|
11
11
|
|
|
12
12
|
materialize(ancestor, 2).
|
|
13
13
|
|
package/package.json
CHANGED
package/playground.html
CHANGED
|
@@ -438,10 +438,12 @@
|
|
|
438
438
|
"ackermann",
|
|
439
439
|
"age",
|
|
440
440
|
"aliases-and-namespaces",
|
|
441
|
+
"alignment-demo",
|
|
441
442
|
"allen-interval-calculus",
|
|
442
443
|
"ancestor",
|
|
443
444
|
"animal",
|
|
444
445
|
"annotation",
|
|
446
|
+
"backward",
|
|
445
447
|
"basic-monadic",
|
|
446
448
|
"bayes-diagnosis",
|
|
447
449
|
"bayes-therapy",
|
|
@@ -452,12 +454,14 @@
|
|
|
452
454
|
"buck-converter-design",
|
|
453
455
|
"cache-performance",
|
|
454
456
|
"canary-release",
|
|
457
|
+
"cat-koko",
|
|
455
458
|
"clinical-trial-screening",
|
|
456
459
|
"collatz-1000",
|
|
457
460
|
"combinatorics-findall-sort",
|
|
458
461
|
"competitive-enzyme-kinetics",
|
|
459
462
|
"complex",
|
|
460
463
|
"complex-matrix-stability",
|
|
464
|
+
"composition-of-injective-functions-is-injective",
|
|
461
465
|
"context-association",
|
|
462
466
|
"control-system",
|
|
463
467
|
"cryptarithmetic-send-more-money",
|
|
@@ -479,13 +483,16 @@
|
|
|
479
483
|
"dijkstra-findall-sort",
|
|
480
484
|
"dijkstra-risk-path",
|
|
481
485
|
"dining-philosophers",
|
|
486
|
+
"dog",
|
|
482
487
|
"drone-corridor-planner",
|
|
483
488
|
"easter-computus",
|
|
484
489
|
"electrical-rc-filter",
|
|
485
490
|
"epidemic-policy",
|
|
491
|
+
"equivalence-classes-overlap-implies-same-class",
|
|
486
492
|
"eulerian-path",
|
|
487
493
|
"ev-range-worlds",
|
|
488
494
|
"exact-cover-sudoku",
|
|
495
|
+
"existential-rule",
|
|
489
496
|
"exoplanet-validation-worlds",
|
|
490
497
|
"expression-eval",
|
|
491
498
|
"family-cousins",
|
|
@@ -500,9 +507,12 @@
|
|
|
500
507
|
"gd-step-certified",
|
|
501
508
|
"gdpr-compliance",
|
|
502
509
|
"goldbach-1000",
|
|
510
|
+
"good-cobbler",
|
|
503
511
|
"gps",
|
|
504
512
|
"graph-reachability",
|
|
505
513
|
"gray-code-counter",
|
|
514
|
+
"greatest-lower-bound-uniqueness",
|
|
515
|
+
"group-inverse-uniqueness",
|
|
506
516
|
"hamiltonian-cycle",
|
|
507
517
|
"hamiltonian-path",
|
|
508
518
|
"hamming-code",
|
|
@@ -542,6 +552,8 @@
|
|
|
542
552
|
"service-impact",
|
|
543
553
|
"sieve",
|
|
544
554
|
"skolem-functions",
|
|
555
|
+
"socket-age",
|
|
556
|
+
"socket-family",
|
|
545
557
|
"socrates",
|
|
546
558
|
"statistics-summary",
|
|
547
559
|
"sudoku",
|
package/src/cli.js
CHANGED
|
@@ -39,7 +39,7 @@ export async function main(argv) {
|
|
|
39
39
|
return;
|
|
40
40
|
} else if (!endOptions && arg === '--stats') {
|
|
41
41
|
options.stats = true;
|
|
42
|
-
} else if (!endOptions && arg === '--no-why') {
|
|
42
|
+
} else if (!endOptions && (arg === '--no-why' || arg === '-n')) {
|
|
43
43
|
options.why = false;
|
|
44
44
|
} else if (!endOptions && arg === '--query') {
|
|
45
45
|
if (i + 1 >= argv.length) throw new Error('--query requires an argument');
|
|
@@ -77,7 +77,7 @@ export async function main(argv) {
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
const program = Program.parseSources(sourceParts);
|
|
80
|
+
const program = Program.parseSources(sourceParts, { sourceMetadata: options.why, markRecursive: options.why });
|
|
81
81
|
|
|
82
82
|
if (options.query != null) runQuery(program, options.query, options);
|
|
83
83
|
else runDefault(program, options);
|
|
@@ -142,10 +142,10 @@ Input:
|
|
|
142
142
|
|
|
143
143
|
Options:
|
|
144
144
|
-h, --help Show this help text and exit.
|
|
145
|
-
-
|
|
145
|
+
-n, --no-why Suppress why/2 explanation facts; print answers only.
|
|
146
146
|
--query GOAL Run GOAL as a query instead of materializing output predicates.
|
|
147
147
|
--stats Print solver statistics to stderr after execution.
|
|
148
|
-
|
|
148
|
+
-v, --version Show the package version and exit.
|
|
149
149
|
-- Stop option parsing; following arguments are treated as files.
|
|
150
150
|
`);
|
|
151
151
|
}
|
package/src/parser.js
CHANGED
|
@@ -36,6 +36,7 @@ class Parser {
|
|
|
36
36
|
this.pos = 0;
|
|
37
37
|
this.line = 1;
|
|
38
38
|
this.anonymous = 0;
|
|
39
|
+
this.sourceMetadata = options.sourceMetadata !== false;
|
|
39
40
|
this.token = this.nextToken();
|
|
40
41
|
}
|
|
41
42
|
peek(offset = 0) {
|
|
@@ -256,16 +257,144 @@ class Parser {
|
|
|
256
257
|
}
|
|
257
258
|
this.expect(TOK.DOT, '.');
|
|
258
259
|
this.advance();
|
|
259
|
-
|
|
260
|
+
const clause = { head, body };
|
|
261
|
+
if (this.sourceMetadata) clause.source = { filename: this.filename, line, clause: clauses.length + 1 };
|
|
262
|
+
clauses.push(clause);
|
|
260
263
|
}
|
|
261
264
|
return clauses;
|
|
262
265
|
}
|
|
263
266
|
}
|
|
264
267
|
|
|
268
|
+
|
|
265
269
|
export function parseClauses(source, options = {}) {
|
|
270
|
+
if (options.sourceMetadata === false) {
|
|
271
|
+
const clauses = parseClausesFastNoSource(source);
|
|
272
|
+
if (clauses) return clauses;
|
|
273
|
+
}
|
|
266
274
|
return new Parser(source, options).parseProgram();
|
|
267
275
|
}
|
|
268
276
|
|
|
277
|
+
function isSimpleName(text) {
|
|
278
|
+
if (!text) return false;
|
|
279
|
+
const first = text.charCodeAt(0);
|
|
280
|
+
if (!(first >= 97 && first <= 122)) return false;
|
|
281
|
+
for (let i = 1; i < text.length; i++) {
|
|
282
|
+
const code = text.charCodeAt(i);
|
|
283
|
+
if (!(code === 95 || (code >= 48 && code <= 57) || (code >= 65 && code <= 90) || (code >= 97 && code <= 122))) return false;
|
|
284
|
+
}
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const SIMPLE_NUMBER = /^-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?$/;
|
|
289
|
+
const SIMPLE_ARG_FORBIDDEN = /[\s()[\]|"']/;
|
|
290
|
+
|
|
291
|
+
function parseClausesFastNoSource(source) {
|
|
292
|
+
source = String(source ?? '');
|
|
293
|
+
const atomCache = new Map();
|
|
294
|
+
const numberCache = new Map();
|
|
295
|
+
const stringCache = new Map();
|
|
296
|
+
const clauses = [];
|
|
297
|
+
let anonymous = 0;
|
|
298
|
+
let chunk = '';
|
|
299
|
+
|
|
300
|
+
const cached = (cache, key, create) => {
|
|
301
|
+
const existing = cache.get(key);
|
|
302
|
+
if (existing) return existing;
|
|
303
|
+
const value = create(key);
|
|
304
|
+
cache.set(key, value);
|
|
305
|
+
return value;
|
|
306
|
+
};
|
|
307
|
+
const scalarOrVariable = (text, variables) => {
|
|
308
|
+
text = text.trim();
|
|
309
|
+
if (!text) throw new Error('empty simple term');
|
|
310
|
+
if (text === '_') return variable(`__anon${anonymous++}`);
|
|
311
|
+
if (isVariableStart(text)) {
|
|
312
|
+
const existing = variables.get(text);
|
|
313
|
+
if (existing) return existing;
|
|
314
|
+
const value = variable(text);
|
|
315
|
+
variables.set(text, value);
|
|
316
|
+
return value;
|
|
317
|
+
}
|
|
318
|
+
if (SIMPLE_NUMBER.test(text)) return cached(numberCache, text, numberTerm);
|
|
319
|
+
if (text[0] === '"' && text.endsWith('"')) return cached(stringCache, text.slice(1, -1), stringTerm);
|
|
320
|
+
return cached(atomCache, text, atom);
|
|
321
|
+
};
|
|
322
|
+
const parseBinaryCompound = (text, variables) => {
|
|
323
|
+
text = text.trim();
|
|
324
|
+
const open = text.indexOf('(');
|
|
325
|
+
if (open <= 0 || text[text.length - 1] !== ')') return null;
|
|
326
|
+
const name = text.slice(0, open).trim();
|
|
327
|
+
if (!isSimpleName(name)) return null;
|
|
328
|
+
const inner = text.slice(open + 1, -1);
|
|
329
|
+
if (inner.includes('(') || inner.includes(')') || inner.includes('[') || inner.includes(']') || inner.includes('|') || inner.includes('"') || inner.includes("'")) return null;
|
|
330
|
+
const comma = inner.indexOf(',');
|
|
331
|
+
if (comma < 0 || inner.indexOf(',', comma + 1) >= 0) return null;
|
|
332
|
+
const left = inner.slice(0, comma).trim();
|
|
333
|
+
const right = inner.slice(comma + 1).trim();
|
|
334
|
+
if (!left || !right || SIMPLE_ARG_FORBIDDEN.test(left) || SIMPLE_ARG_FORBIDDEN.test(right)) return null;
|
|
335
|
+
return compound(name, [scalarOrVariable(left, variables), scalarOrVariable(right, variables)]);
|
|
336
|
+
};
|
|
337
|
+
const parseSimple = (text) => {
|
|
338
|
+
if (!text.endsWith('.') || text.includes('\n')) return null;
|
|
339
|
+
text = text.slice(0, -1);
|
|
340
|
+
const variables = new Map();
|
|
341
|
+
const rule = text.indexOf(':-');
|
|
342
|
+
if (rule < 0) {
|
|
343
|
+
const head = parseBinaryCompound(text, variables);
|
|
344
|
+
return head ? { head, body: [] } : null;
|
|
345
|
+
}
|
|
346
|
+
const head = parseBinaryCompound(text.slice(0, rule), variables);
|
|
347
|
+
const bodyGoal = parseBinaryCompound(text.slice(rule + 2), variables);
|
|
348
|
+
return head && bodyGoal ? { head, body: [bodyGoal] } : null;
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
const flush = () => {
|
|
352
|
+
const text = chunk.trim();
|
|
353
|
+
chunk = '';
|
|
354
|
+
if (!text) return true;
|
|
355
|
+
const simple = parseSimple(text);
|
|
356
|
+
if (simple) {
|
|
357
|
+
clauses.push(simple);
|
|
358
|
+
return true;
|
|
359
|
+
}
|
|
360
|
+
try {
|
|
361
|
+
const parsed = new Parser(text, { sourceMetadata: false }).parseProgram();
|
|
362
|
+
clauses.push(...parsed);
|
|
363
|
+
return true;
|
|
364
|
+
} catch (_) {
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
let lineStart = 0;
|
|
370
|
+
while (lineStart <= source.length) {
|
|
371
|
+
let lineEnd = source.indexOf('\n', lineStart);
|
|
372
|
+
if (lineEnd < 0) lineEnd = source.length;
|
|
373
|
+
let line = source.slice(lineStart, lineEnd);
|
|
374
|
+
if (line.endsWith('\r')) line = line.slice(0, -1);
|
|
375
|
+
const trimmed = line.trim();
|
|
376
|
+
if (trimmed && !trimmed.startsWith('%')) {
|
|
377
|
+
if (!chunk && trimmed.endsWith('.')) {
|
|
378
|
+
const simple = parseSimple(trimmed);
|
|
379
|
+
if (simple) clauses.push(simple);
|
|
380
|
+
else {
|
|
381
|
+
chunk = line + '\n';
|
|
382
|
+
if (!flush()) return null;
|
|
383
|
+
}
|
|
384
|
+
} else {
|
|
385
|
+
chunk += line + '\n';
|
|
386
|
+
if (trimmed.endsWith('.')) {
|
|
387
|
+
if (!flush()) return null;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if (lineEnd === source.length) break;
|
|
392
|
+
lineStart = lineEnd + 1;
|
|
393
|
+
}
|
|
394
|
+
if (chunk.trim() && !flush()) return null;
|
|
395
|
+
return clauses;
|
|
396
|
+
}
|
|
397
|
+
|
|
269
398
|
export function parseProgramText(source) {
|
|
270
399
|
return parseClauses(source);
|
|
271
400
|
}
|
package/src/program.js
CHANGED
|
@@ -1,27 +1,33 @@
|
|
|
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
|
-
import { ATOM, COMPOUND, Env, compound, deref, flattenConjunction, isScalar,
|
|
3
|
+
import { ATOM, COMPOUND, Env, compound, deref, flattenConjunction, isScalar, termToString } from './term.js';
|
|
4
4
|
import { parseClauses } from './parser.js';
|
|
5
5
|
|
|
6
6
|
export class Program {
|
|
7
|
-
constructor(clauses = []) {
|
|
8
|
-
this.clauses = clauses
|
|
7
|
+
constructor(clauses = [], options = {}) {
|
|
8
|
+
this.clauses = clauses;
|
|
9
9
|
this.groups = new Map();
|
|
10
|
-
|
|
11
|
-
this.
|
|
10
|
+
this.materializedGroups = new Set();
|
|
11
|
+
this.hasMaterialize = false;
|
|
12
|
+
for (let index = 0; index < this.clauses.length; index++) {
|
|
13
|
+
const clause = this.clauses[index];
|
|
14
|
+
clause.index = index;
|
|
15
|
+
this.indexClause(clause);
|
|
16
|
+
}
|
|
17
|
+
this.applyDeclarations(options);
|
|
12
18
|
}
|
|
13
19
|
static parse(source, options = {}) {
|
|
14
|
-
return new Program(parseClauses(source, options));
|
|
20
|
+
return new Program(parseClauses(source, options), options);
|
|
15
21
|
}
|
|
16
|
-
static parseSources(sources = []) {
|
|
22
|
+
static parseSources(sources = [], options = {}) {
|
|
17
23
|
const clauses = [];
|
|
18
24
|
for (const source of sources) {
|
|
19
25
|
const parsed = typeof source === 'string'
|
|
20
|
-
? parseClauses(source)
|
|
21
|
-
: parseClauses(source?.text ?? source?.source ?? '', { filename: source?.filename ?? '<input>' });
|
|
26
|
+
? parseClauses(source, options)
|
|
27
|
+
: parseClauses(source?.text ?? source?.source ?? '', { ...options, filename: source?.filename ?? '<input>' });
|
|
22
28
|
for (const clause of parsed) clauses.push(clause);
|
|
23
29
|
}
|
|
24
|
-
return new Program(clauses);
|
|
30
|
+
return new Program(clauses, options);
|
|
25
31
|
}
|
|
26
32
|
makeGroup(name, arity) {
|
|
27
33
|
// A group corresponds to one predicate indicator, for example edge/3.
|
|
@@ -61,22 +67,27 @@ export class Program {
|
|
|
61
67
|
findGroup(name, arity) {
|
|
62
68
|
return this.groups.get(`${name}/${arity}`) ?? null;
|
|
63
69
|
}
|
|
64
|
-
applyDeclarations() {
|
|
70
|
+
applyDeclarations(options = {}) {
|
|
65
71
|
for (const clause of this.clauses) {
|
|
66
72
|
const h = clause.head;
|
|
67
|
-
if (clause.body.length
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
+
if (clause.body.length !== 0 || h.type !== COMPOUND || h.arity !== 2) continue;
|
|
74
|
+
const [name, arity] = h.args;
|
|
75
|
+
if (name.type !== ATOM || arity.type !== 'number') continue;
|
|
76
|
+
const key = `${name.name}/${Number(arity.name)}`;
|
|
77
|
+
if (h.name === 'memoize') {
|
|
78
|
+
const group = this.groups.get(key);
|
|
79
|
+
if (group) group.memoized = true;
|
|
80
|
+
} else if (h.name === 'materialize') {
|
|
81
|
+
this.hasMaterialize = true;
|
|
82
|
+
this.materializedGroups.add(key);
|
|
73
83
|
}
|
|
74
84
|
}
|
|
75
|
-
this.markRecursivePredicates();
|
|
85
|
+
if (options.markRecursive !== false) this.markRecursivePredicates();
|
|
76
86
|
}
|
|
77
87
|
markRecursivePredicates() {
|
|
78
|
-
// Recursion is a group-level hint
|
|
79
|
-
//
|
|
88
|
+
// Recursion is a group-level diagnostic hint. It is computed from predicate
|
|
89
|
+
// dependencies rather than from individual clauses when callers explicitly ask
|
|
90
|
+
// for it.
|
|
80
91
|
const groups = [...this.groups.values()];
|
|
81
92
|
const indexByGroup = new Map(groups.map((group, i) => [group, i]));
|
|
82
93
|
const deps = groups.map(() => new Set());
|
|
@@ -109,15 +120,10 @@ export class Program {
|
|
|
109
120
|
}
|
|
110
121
|
}
|
|
111
122
|
hasMaterializeDeclarations() {
|
|
112
|
-
return this.
|
|
123
|
+
return this.hasMaterialize;
|
|
113
124
|
}
|
|
114
125
|
groupIsMaterialized(group) {
|
|
115
|
-
return this.
|
|
116
|
-
const h = clause.head;
|
|
117
|
-
if (clause.body.length !== 0 || h.type !== COMPOUND || h.name !== 'materialize' || h.arity !== 2) return false;
|
|
118
|
-
const [name, arity] = h.args;
|
|
119
|
-
return name.type === ATOM && arity.type === 'number' && name.name === group.name && String(group.arity) === arity.name;
|
|
120
|
-
});
|
|
126
|
+
return this.materializedGroups.has(`${group.name}/${group.arity}`);
|
|
121
127
|
}
|
|
122
128
|
groupHasRule(group) {
|
|
123
129
|
return group.clauses.some((clause) => clause.body.length > 0);
|
package/test/run-all.js
CHANGED
|
File without changes
|
package/test/run-conformance.js
CHANGED
|
File without changes
|
package/test/run-examples.js
CHANGED
|
File without changes
|
package/test/run-regression.js
CHANGED
|
@@ -185,6 +185,16 @@ why(
|
|
|
185
185
|
assertEqual(result.stderr, '', 'stderr');
|
|
186
186
|
},
|
|
187
187
|
},
|
|
188
|
+
{
|
|
189
|
+
name: 'README and playground cover every bundled example',
|
|
190
|
+
run: () => {
|
|
191
|
+
const examples = listExampleNames();
|
|
192
|
+
const readmeExamples = readmeCatalogExampleNames();
|
|
193
|
+
const playgroundExamples = playgroundExampleNames();
|
|
194
|
+
assertEqual(readmeExamples.join('\n'), examples.join('\n'), 'README example catalog');
|
|
195
|
+
assertEqual(playgroundExamples.join('\n'), examples.join('\n'), 'playground examples');
|
|
196
|
+
},
|
|
197
|
+
},
|
|
188
198
|
{
|
|
189
199
|
name: 'stdin input is accepted',
|
|
190
200
|
run: () => {
|
|
@@ -206,6 +216,15 @@ why(
|
|
|
206
216
|
assertEqual(result.stderr, '', 'stderr');
|
|
207
217
|
},
|
|
208
218
|
},
|
|
219
|
+
{
|
|
220
|
+
name: '-n suppresses query explanations',
|
|
221
|
+
run: () => {
|
|
222
|
+
const result = runCli(['-n', '--query', 'p(X)', '-'], { input: 'p(a).\np(b).\n' });
|
|
223
|
+
assertEqual(result.status, 0, 'exit status');
|
|
224
|
+
assertEqual(result.stdout, 'p(a).\np(b).\n', 'stdout');
|
|
225
|
+
assertEqual(result.stderr, '', 'stderr');
|
|
226
|
+
},
|
|
227
|
+
},
|
|
209
228
|
{
|
|
210
229
|
name: '--no-why suppresses materialization explanations',
|
|
211
230
|
run: () => {
|
|
@@ -460,6 +479,40 @@ function runWhyLoose({ program, query }) {
|
|
|
460
479
|
return result;
|
|
461
480
|
}
|
|
462
481
|
|
|
482
|
+
function listExampleNames() {
|
|
483
|
+
return fs.readdirSync(path.join(root, 'examples'))
|
|
484
|
+
.filter((name) => name.endsWith('.pl'))
|
|
485
|
+
.map((name) => name.slice(0, -3))
|
|
486
|
+
.sort();
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function readmeCatalogExampleNames() {
|
|
490
|
+
const readme = fs.readFileSync(path.join(root, 'README.md'), 'utf8');
|
|
491
|
+
const section = between(readme, '## Example catalog', '## Golden outputs, tests, and conformance');
|
|
492
|
+
return [...section.matchAll(/examples\/([A-Za-z0-9_-]+)\.pl/g)]
|
|
493
|
+
.map((match) => match[1])
|
|
494
|
+
.filter((name, index, names) => names.indexOf(name) === index)
|
|
495
|
+
.sort();
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function playgroundExampleNames() {
|
|
499
|
+
const html = fs.readFileSync(path.join(root, 'playground.html'), 'utf8');
|
|
500
|
+
const match = html.match(/const EXAMPLES = \[(.*?)\];/s);
|
|
501
|
+
if (match == null) throw new Error('playground EXAMPLES array not found');
|
|
502
|
+
return [...match[1].matchAll(/"([^"]+)"/g)]
|
|
503
|
+
.map((match) => match[1])
|
|
504
|
+
.sort();
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function between(text, startMarker, endMarker) {
|
|
508
|
+
const start = text.indexOf(startMarker);
|
|
509
|
+
if (start === -1) throw new Error(`${startMarker} not found`);
|
|
510
|
+
const contentStart = start + startMarker.length;
|
|
511
|
+
const end = text.indexOf(endMarker, contentStart);
|
|
512
|
+
if (end === -1) throw new Error(`${endMarker} not found`);
|
|
513
|
+
return text.slice(contentStart, end);
|
|
514
|
+
}
|
|
515
|
+
|
|
463
516
|
function runCli(args, options = {}) {
|
|
464
517
|
return spawnSync(process.execPath, [bin, ...args], {
|
|
465
518
|
cwd: root,
|