eyelang 1.3.4 → 1.3.5

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/docs/guide.md CHANGED
@@ -577,7 +577,7 @@ semidet(edge, 2).
577
577
 
578
578
  For large programs, keep helper predicates selective, bind arguments early, document intended calling patterns with `mode/3` when helpful, and declare focused output predicates with `materialize/2` when default output would otherwise solve broad helper goals.
579
579
 
580
- When using `not/1` over user-defined predicates, keep the dependency graph stratified: negative dependencies should not participate in recursion. The JavaScript API exposes `program.stratifiedNegation`, `program.negationStratificationErrors`, and `program.assertStratifiedNegation()` so host tools can warn or reject programs that rely on unstratified negation.
580
+ When using `not/1` over user-defined predicates, keep the dependency graph stratified: negative dependencies should not participate in recursion. The JavaScript API exposes `program.stratifiedNegation`, `program.negationStratificationErrors`, and `program.assertStratifiedNegation()` so host tools can warn or reject programs that rely on unstratified negation. The diagnostic is lazy by default; use `{ analyzeNegation: true }` to compute it during parsing or `{ strictNegation: true }` to compute and reject unstratified programs.
581
581
 
582
582
  ## Implementation limits
583
583
 
@@ -371,7 +371,7 @@ p(?x) :- q(?x).
371
371
  q(?x) :- not(p(?x)).
372
372
  ```
373
373
 
374
- The JavaScript implementation records stratification metadata on `Program` instances: `stratifiedNegation`, `negationStratificationErrors`, `negationDependencies`, and per-group `negationStratum`. Embedders that want to reject non-portable negation can parse with `{ strictNegation: true }` or call `program.assertStratifiedNegation()`.
374
+ The JavaScript implementation records stratification metadata on `Program` instances: `stratifiedNegation`, `negationStratificationErrors`, `negationDependencies`, and per-group `negationStratum`. This diagnostic is computed lazily when one of those properties or helper methods is first read, or eagerly when parsing with `{ analyzeNegation: true }` or `{ strictNegation: true }`. Embedders that want to reject non-portable negation can parse with `{ strictNegation: true }` or call `program.assertStratifiedNegation()`.
375
375
 
376
376
  ## 9. Standard built-in predicates
377
377
 
package/index.d.ts CHANGED
@@ -12,6 +12,7 @@ export interface EyelangRunOptions {
12
12
  sourceMetadata?: boolean;
13
13
  markRecursive?: boolean;
14
14
  strictNegation?: boolean;
15
+ analyzeNegation?: boolean;
15
16
  [key: string]: unknown;
16
17
  }
17
18
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyelang",
3
- "version": "1.3.4",
3
+ "version": "1.3.5",
4
4
  "description": "A small Prolog-like logic programming language for rules, goals, answers, and proofs.",
5
5
  "type": "module",
6
6
  "main": "./index.js",
package/src/program.js CHANGED
@@ -14,6 +14,7 @@ export class Program {
14
14
  clause.index = index;
15
15
  this.indexClause(clause);
16
16
  }
17
+ this._negationAnalysis = null;
17
18
  this.applyDeclarations(options);
18
19
  }
19
20
  static parse(source, options = {}) {
@@ -101,7 +102,7 @@ export class Program {
101
102
  }
102
103
  }
103
104
  if (options.markRecursive !== false) this.markRecursivePredicates();
104
- this.analyzeNegationStratification();
105
+ if (options.analyzeNegation === true || options.strictNegation === true) this.analyzeNegationStratification();
105
106
  if (options.strictNegation === true) this.assertStratifiedNegation();
106
107
  }
107
108
  markRecursivePredicates() {
@@ -192,19 +193,34 @@ export class Program {
192
193
  const strata = computeNegationStrata(groups, edges, indexByKey);
193
194
  for (const group of groups) group.negationStratum = strata.get(groupKeys.get(group)) ?? null;
194
195
 
195
- this.negationDependencies = edges;
196
- this.negationStratificationErrors = violations;
197
- this.stratifiedNegation = violations.length === 0;
196
+ this._negationAnalysis = {
197
+ dependencies: edges,
198
+ errors: violations,
199
+ stratified: violations.length === 0,
200
+ };
198
201
  return violations;
199
202
  }
203
+ ensureNegationStratification() {
204
+ if (!this._negationAnalysis) this.analyzeNegationStratification();
205
+ return this._negationAnalysis;
206
+ }
207
+ get negationDependencies() {
208
+ return this.ensureNegationStratification().dependencies;
209
+ }
210
+ get negationStratificationErrors() {
211
+ return this.ensureNegationStratification().errors;
212
+ }
213
+ get stratifiedNegation() {
214
+ return this.ensureNegationStratification().stratified;
215
+ }
200
216
  assertStratifiedNegation() {
201
- const violations = this.negationStratificationErrors ?? this.analyzeNegationStratification();
217
+ const violations = this.ensureNegationStratification().errors;
202
218
  if (violations.length === 0) return true;
203
219
  const details = violations.map((edge) => `${edge.from} depends negatively on ${edge.to}`).join('; ');
204
220
  throw new Error(`unstratified negation: ${details}`);
205
221
  }
206
222
  isStratifiedNegation() {
207
- return this.stratifiedNegation !== false;
223
+ return this.ensureNegationStratification().stratified;
208
224
  }
209
225
 
210
226
  hasMaterializeDeclarations() {
@@ -396,6 +396,23 @@ function apiCases() {
396
396
  assertEqual(group.arity, 2, 'group arity');
397
397
  },
398
398
  },
399
+ {
400
+ name: 'program keeps negation diagnostics lazy by default',
401
+ run: () => {
402
+ const program = Program.parse('p(a).\nq(?x) :- not(p(?x)).\n');
403
+ assertEqual(program._negationAnalysis, null, 'analysis starts lazy');
404
+ assertEqual(program.negationDependencies.length, 1, 'dependency count');
405
+ assertEqual(program._negationAnalysis !== null, true, 'analysis computed on demand');
406
+ },
407
+ },
408
+ {
409
+ name: 'analyzeNegation option computes diagnostics eagerly',
410
+ run: () => {
411
+ const program = Program.parse('p(a).\nq(?x) :- not(p(?x)).\n', { analyzeNegation: true });
412
+ assertEqual(program._negationAnalysis !== null, true, 'analysis computed eagerly');
413
+ assertEqual(program.stratifiedNegation, true, 'stratified negation');
414
+ },
415
+ },
399
416
  {
400
417
  name: 'program reports stratified negation metadata',
401
418
  run: () => {