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.
@@ -1,8 +1,8 @@
1
- % eyelog-socket-age.pl
1
+ % socket-age.pl
2
2
  %
3
- % A small runnable eyelog Socket example for age reasoning.
3
+ % A small runnable eyelang Socket example for age reasoning.
4
4
  %
5
- % The socket facts are ordinary eyelog data. They document the semantic
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
- % eyelog eyelog-socket-age.pl
15
+ % eyelang socket-age.pl
16
16
 
17
17
  materialize(ageAbove, 2).
18
18
 
@@ -1,13 +1,13 @@
1
- % eyelog-socket-family.pl
1
+ % socket-family.pl
2
2
  %
3
- % A small runnable eyelog Socket example.
3
+ % A small runnable eyelang Socket example.
4
4
  %
5
- % The socket facts below are ordinary eyelog data. They document the
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
- % eyelog eyelog-socket-family.pl
10
+ % eyelang socket-family.pl
11
11
 
12
12
  materialize(ancestor, 2).
13
13
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyelang",
3
- "version": "1.3.8",
3
+ "version": "1.3.9",
4
4
  "type": "module",
5
5
  "description": "A small rule engine for Prolog-style Horn clauses",
6
6
  "keywords": [
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
- -v, --version Show the package version and exit.
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
- --no-why Suppress why/2 explanation facts; print answers only.
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
- clauses.push({ head, body, source: { filename: this.filename, line, clause: clauses.length + 1 } });
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, termIsGround, termToString } from './term.js';
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.map((clause, index) => ({ ...clause, index, headGround: termIsGround(clause.head) }));
7
+ constructor(clauses = [], options = {}) {
8
+ this.clauses = clauses;
9
9
  this.groups = new Map();
10
- for (const clause of this.clauses) this.indexClause(clause);
11
- this.applyDeclarations();
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 === 0 && h.type === COMPOUND && h.name === 'memoize' && h.arity === 2) {
68
- const [name, arity] = h.args;
69
- if (name.type === ATOM && arity.type === 'number') {
70
- const group = this.findGroup(name.name, Number(arity.name));
71
- if (group) group.memoized = true;
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 used by the solver and diagnostics. It is
79
- // computed from predicate dependencies rather than from individual clauses.
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.clauses.some((clause) => clause.body.length === 0 && clause.head.type === COMPOUND && clause.head.name === 'materialize' && clause.head.arity === 2);
123
+ return this.hasMaterialize;
113
124
  }
114
125
  groupIsMaterialized(group) {
115
- return this.clauses.some((clause) => {
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
File without changes
File without changes
@@ -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: () => {