eyelang 1.3.8 → 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/examples/socket-age.pl +4 -4
- package/examples/socket-family.pl +4 -4
- package/package.json +1 -1
- 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 +9 -0
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/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
|
@@ -216,6 +216,15 @@ why(
|
|
|
216
216
|
assertEqual(result.stderr, '', 'stderr');
|
|
217
217
|
},
|
|
218
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
|
+
},
|
|
219
228
|
{
|
|
220
229
|
name: '--no-why suppresses materialization explanations',
|
|
221
230
|
run: () => {
|