eyelang 1.3.3 → 1.3.4

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 CHANGED
@@ -4,7 +4,7 @@
4
4
  [![DOI](https://img.shields.io/badge/DOI-10.5281%2Fzenodo.20761726-blue.svg)](https://doi.org/10.5281/zenodo.20761726)
5
5
 
6
6
  Eyelang is a small logic programming language for rules, goals, answers, and proofs.
7
- Its source syntax is Prolog-like Horn-clause syntax with a few deliberate eyelang choices, such as `?x` variables for N3/SPARQL-style readability, explicit `table(path, 2).` declarations for tabled predicates, and advisory `mode/3` declarations for host tooling.
7
+ Its source syntax is Prolog-like Horn-clause syntax with deliberate eyelang choices, including `?x` variables for N3/SPARQL-style readability, explicit `table(path, 2).` declarations for tabled predicates, advisory `mode/3` declarations for host tooling, and stratified-negation diagnostics for portable `not/1` usage.
8
8
  It grew out of logic-language experiments in the EYE/N3 reasoning tradition, but is packaged here as its own project.
9
9
 
10
10
  ## Install and run
package/docs/guide.md CHANGED
@@ -577,6 +577,8 @@ 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.
581
+
580
582
  ## Implementation limits
581
583
 
582
584
  Eyelang is intentionally smaller than ISO Prolog. It has no operators, zero-arity compound syntax, cut, modules, dynamic database updates, DCGs, or complete ISO library. Arity-zero data is always written and read back as an atom, such as `nil`, never `nil()`. Negation is negation-as-failure through `not/1`. Search is goal-directed and expected to be finite for the selected output goals. Output explanations are non-normative proof printouts and do not change answer semantics.
@@ -353,6 +353,26 @@ Arithmetic and string built-ins do not introduce a separate semantic universe. T
353
353
 
354
354
  Negation-as-failure `not(?goal)` is especially operational: it succeeds when the current goal-directed search finds no solution for `?goal`. It is not classical negation and should not be read as adding negative facts to the Herbrand model. Programs using negation SHOULD keep the negated goal sufficiently ground and finite.
355
355
 
356
+ ### 8.5 Stratified negation
357
+
358
+ Portable programs using user-defined predicates under `not/1` SHOULD be **stratified**. A program is stratified when no predicate depends negatively on itself, either directly or through a cycle of other predicate dependencies. In a stratified program, predicates can be assigned strata so that positive dependencies stay in the same or a lower stratum and negative dependencies point strictly to a lower stratum.
359
+
360
+ For example, this is stratified because `open/1` depends negatively on `closed/1`, but `closed/1` does not depend back on `open/1`:
361
+
362
+ ```eyelang
363
+ closed(?x) :- blocked(?x).
364
+ open(?x) :- candidate(?x), not(closed(?x)).
365
+ ```
366
+
367
+ This is not stratified because `p/1` and `q/1` form a cycle that contains a negative dependency:
368
+
369
+ ```eyelang
370
+ p(?x) :- q(?x).
371
+ q(?x) :- not(p(?x)).
372
+ ```
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()`.
375
+
356
376
  ## 9. Standard built-in predicates
357
377
 
358
378
  This section specifies the **standard built-ins** of the Eyelang language. An implementation that claims support for this standard built-in profile MUST implement the predicates in this section with the meanings described here.
@@ -494,7 +514,7 @@ The first goal can yield `holds((name(alice, "Alice"), knows(alice, bob)), name(
494
514
 
495
515
  | Built-in | Meaning |
496
516
  |---|---|
497
- | `not(?goal)` | Negation as failure. Succeeds when `?goal` has no solution. |
517
+ | `not(?goal)` | Negation as failure. Succeeds when `?goal` has no solution. Portable user-defined negation should be stratified. |
498
518
  | `once(?goal)` | Succeeds with at most the first solution of `?goal`. |
499
519
  | `forall(?generator, ?test)` | Succeeds when every solution of `?generator` also satisfies `?test`; succeeds vacuously when `?generator` has no solutions. |
500
520
 
package/index.d.ts CHANGED
@@ -11,6 +11,7 @@ export interface EyelangRunOptions {
11
11
  registry?: BuiltinRegistry;
12
12
  sourceMetadata?: boolean;
13
13
  markRecursive?: boolean;
14
+ strictNegation?: boolean;
14
15
  [key: string]: unknown;
15
16
  }
16
17
 
@@ -43,6 +44,7 @@ export interface EyelangPredicateGroup {
43
44
  mode: string[] | null;
44
45
  determinism: 'det' | 'semidet' | null;
45
46
  recursive: boolean;
47
+ negationStratum: number | null;
46
48
  }
47
49
 
48
50
  export type EyelangTerm = Term | { type: string; name: string; args?: EyelangTerm[]; arity?: number };
@@ -70,6 +72,9 @@ export class Program {
70
72
  groups: Map<string, EyelangPredicateGroup>;
71
73
  materializedGroups: Set<string>;
72
74
  hasMaterialize: boolean;
75
+ negationDependencies: Array<{ from: string; to: string; negative: boolean }>;
76
+ negationStratificationErrors: Array<{ from: string; to: string }>;
77
+ stratifiedNegation: boolean;
73
78
  static parse(source: string, options?: EyelangRunOptions): Program;
74
79
  static parseSources(sources?: Array<string | EyelangSourcePart>, options?: EyelangRunOptions): Program;
75
80
  makeGroup(name: string, arity: number): EyelangPredicateGroup;
@@ -77,6 +82,9 @@ export class Program {
77
82
  findGroup(name: string, arity: number): EyelangPredicateGroup | null;
78
83
  applyDeclarations(options?: EyelangRunOptions): void;
79
84
  markRecursivePredicates(): void;
85
+ analyzeNegationStratification(): Array<{ from: string; to: string }>;
86
+ assertStratifiedNegation(): true;
87
+ isStratifiedNegation(): boolean;
80
88
  hasMaterializeDeclarations(): boolean;
81
89
  groupIsMaterialized(group: EyelangPredicateGroup): boolean;
82
90
  groupHasRule(group: EyelangPredicateGroup): boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyelang",
3
- "version": "1.3.3",
3
+ "version": "1.3.4",
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
@@ -43,6 +43,7 @@ export class Program {
43
43
  mode: null,
44
44
  determinism: null,
45
45
  recursive: false,
46
+ negationStratum: null,
46
47
  };
47
48
  if (arity > 2) {
48
49
  for (let left = 0; left < arity; left++) {
@@ -100,6 +101,8 @@ export class Program {
100
101
  }
101
102
  }
102
103
  if (options.markRecursive !== false) this.markRecursivePredicates();
104
+ this.analyzeNegationStratification();
105
+ if (options.strictNegation === true) this.assertStratifiedNegation();
103
106
  }
104
107
  markRecursivePredicates() {
105
108
  // Recursion is a group-level diagnostic hint. It is computed from predicate
@@ -136,6 +139,74 @@ export class Program {
136
139
  group.recursive = recursive;
137
140
  }
138
141
  }
142
+
143
+ analyzeNegationStratification() {
144
+ // Stratified negation is a portability diagnostic. A program is stratified
145
+ // when no predicate depends negatively on itself, directly or indirectly.
146
+ const groups = [...this.groups.values()];
147
+ const groupKeys = new Map(groups.map((group) => [group, `${group.name}/${group.arity}`]));
148
+ const groupByKey = new Map(groups.map((group) => [`${group.name}/${group.arity}`, group]));
149
+ const indexByKey = new Map(groups.map((group, i) => [`${group.name}/${group.arity}`, i]));
150
+ const edges = [];
151
+
152
+ for (const group of groups) {
153
+ const from = groupKeys.get(group);
154
+ for (const clause of group.clauses) {
155
+ for (const goal of clause.body) {
156
+ for (const dep of collectGoalDependencies(goal, false)) {
157
+ if (!groupByKey.has(dep.key)) continue;
158
+ edges.push({ from, to: dep.key, negative: dep.negative });
159
+ }
160
+ }
161
+ }
162
+ }
163
+
164
+ const adjacency = groups.map(() => []);
165
+ for (const edge of edges) {
166
+ const from = indexByKey.get(edge.from);
167
+ const to = indexByKey.get(edge.to);
168
+ if (from == null || to == null) continue;
169
+ adjacency[from].push(to);
170
+ }
171
+
172
+ const sccs = stronglyConnectedComponents(adjacency);
173
+ const componentByIndex = new Map();
174
+ for (let component = 0; component < sccs.length; component++) {
175
+ for (const index of sccs[component]) componentByIndex.set(index, component);
176
+ }
177
+
178
+ const violations = [];
179
+ const seen = new Set();
180
+ for (const edge of edges) {
181
+ if (!edge.negative) continue;
182
+ const from = indexByKey.get(edge.from);
183
+ const to = indexByKey.get(edge.to);
184
+ if (from == null || to == null) continue;
185
+ if (componentByIndex.get(from) !== componentByIndex.get(to)) continue;
186
+ const key = `${edge.from}->${edge.to}`;
187
+ if (seen.has(key)) continue;
188
+ seen.add(key);
189
+ violations.push({ from: edge.from, to: edge.to });
190
+ }
191
+
192
+ const strata = computeNegationStrata(groups, edges, indexByKey);
193
+ for (const group of groups) group.negationStratum = strata.get(groupKeys.get(group)) ?? null;
194
+
195
+ this.negationDependencies = edges;
196
+ this.negationStratificationErrors = violations;
197
+ this.stratifiedNegation = violations.length === 0;
198
+ return violations;
199
+ }
200
+ assertStratifiedNegation() {
201
+ const violations = this.negationStratificationErrors ?? this.analyzeNegationStratification();
202
+ if (violations.length === 0) return true;
203
+ const details = violations.map((edge) => `${edge.from} depends negatively on ${edge.to}`).join('; ');
204
+ throw new Error(`unstratified negation: ${details}`);
205
+ }
206
+ isStratifiedNegation() {
207
+ return this.stratifiedNegation !== false;
208
+ }
209
+
139
210
  hasMaterializeDeclarations() {
140
211
  return this.hasMaterialize;
141
212
  }
@@ -177,6 +248,92 @@ export class Program {
177
248
  }
178
249
 
179
250
 
251
+
252
+ function collectGoalDependencies(goal, negated) {
253
+ if (goal.type !== COMPOUND) return [];
254
+ if (goal.name === ',' && goal.arity === 2) {
255
+ return [
256
+ ...collectGoalDependencies(goal.args[0], negated),
257
+ ...collectGoalDependencies(goal.args[1], negated),
258
+ ];
259
+ }
260
+ if (goal.name === 'not' && goal.arity === 1) {
261
+ return collectGoalDependencies(goal.args[0], !negated);
262
+ }
263
+ if (goal.name === 'once' && goal.arity === 1) {
264
+ return collectGoalDependencies(goal.args[0], negated);
265
+ }
266
+ if (goal.name === 'forall' && goal.arity === 2) {
267
+ return [
268
+ ...collectGoalDependencies(goal.args[0], negated),
269
+ ...collectGoalDependencies(goal.args[1], negated),
270
+ ];
271
+ }
272
+ return [{ key: `${goal.name}/${goal.arity}`, negative: negated }];
273
+ }
274
+
275
+ function stronglyConnectedComponents(adjacency) {
276
+ let index = 0;
277
+ const stack = [];
278
+ const onStack = new Set();
279
+ const indexes = new Map();
280
+ const lowlinks = new Map();
281
+ const components = [];
282
+
283
+ function visit(v) {
284
+ indexes.set(v, index);
285
+ lowlinks.set(v, index);
286
+ index++;
287
+ stack.push(v);
288
+ onStack.add(v);
289
+
290
+ for (const w of adjacency[v]) {
291
+ if (!indexes.has(w)) {
292
+ visit(w);
293
+ lowlinks.set(v, Math.min(lowlinks.get(v), lowlinks.get(w)));
294
+ } else if (onStack.has(w)) {
295
+ lowlinks.set(v, Math.min(lowlinks.get(v), indexes.get(w)));
296
+ }
297
+ }
298
+
299
+ if (lowlinks.get(v) === indexes.get(v)) {
300
+ const component = [];
301
+ while (true) {
302
+ const w = stack.pop();
303
+ onStack.delete(w);
304
+ component.push(w);
305
+ if (w === v) break;
306
+ }
307
+ components.push(component);
308
+ }
309
+ }
310
+
311
+ for (let v = 0; v < adjacency.length; v++) {
312
+ if (!indexes.has(v)) visit(v);
313
+ }
314
+ return components;
315
+ }
316
+
317
+ function computeNegationStrata(groups, edges, indexByKey) {
318
+ const strata = new Map(groups.map((group) => [`${group.name}/${group.arity}`, 0]));
319
+ if (groups.length === 0) return strata;
320
+
321
+ for (let pass = 0; pass < groups.length; pass++) {
322
+ let changed = false;
323
+ for (const edge of edges) {
324
+ if (!indexByKey.has(edge.from) || !indexByKey.has(edge.to)) continue;
325
+ const fromStratum = strata.get(edge.from) ?? 0;
326
+ const required = (strata.get(edge.to) ?? 0) + (edge.negative ? 1 : 0);
327
+ if (fromStratum < required) {
328
+ strata.set(edge.from, required);
329
+ changed = true;
330
+ }
331
+ }
332
+ if (!changed) return strata;
333
+ }
334
+ return new Map(groups.map((group) => [`${group.name}/${group.arity}`, null]));
335
+ }
336
+
180
337
  function declarationIndicator(name, arity) {
181
338
  if (name?.type !== ATOM || arity?.type !== 'number') return null;
182
339
  if (!/^\d+$/.test(arity.name)) return null;
@@ -396,6 +396,49 @@ function apiCases() {
396
396
  assertEqual(group.arity, 2, 'group arity');
397
397
  },
398
398
  },
399
+ {
400
+ name: 'program reports stratified negation metadata',
401
+ run: () => {
402
+ const program = Program.parse(`
403
+ materialize(open, 1).
404
+ candidate(a).
405
+ blocked(b).
406
+ closed(?x) :- blocked(?x).
407
+ open(?x) :- candidate(?x), not(closed(?x)).
408
+ `);
409
+ assertEqual(program.isStratifiedNegation(), true, 'stratified negation');
410
+ assertEqual(program.negationStratificationErrors.length, 0, 'stratification errors');
411
+ assertEqual(program.findGroup('closed', 1).negationStratum, 0, 'closed stratum');
412
+ assertEqual(program.findGroup('open', 1).negationStratum, 1, 'open stratum');
413
+ },
414
+ },
415
+ {
416
+ name: 'program detects unstratified negation cycles',
417
+ run: () => {
418
+ const program = Program.parse('p(?x) :- q(?x).\nq(?x) :- not(p(?x)).\n');
419
+ assertEqual(program.isStratifiedNegation(), false, 'unstratified negation');
420
+ assertEqual(program.negationStratificationErrors.length, 1, 'stratification error count');
421
+ assertEqual(program.negationStratificationErrors[0].from, 'q/1', 'error source');
422
+ assertEqual(program.negationStratificationErrors[0].to, 'p/1', 'error target');
423
+ let threw = false;
424
+ try { program.assertStratifiedNegation(); } catch (err) {
425
+ threw = true;
426
+ assertIncludes(err.message, 'unstratified negation', 'error message');
427
+ }
428
+ assertEqual(threw, true, 'assertion throws');
429
+ },
430
+ },
431
+ {
432
+ name: 'strictNegation option rejects unstratified programs',
433
+ run: () => {
434
+ let threw = false;
435
+ try { Program.parse('p(?x) :- not(p(?x)).\n', { strictNegation: true }); } catch (err) {
436
+ threw = true;
437
+ assertIncludes(err.message, 'p/1 depends negatively on p/1', 'error message');
438
+ }
439
+ assertEqual(threw, true, 'strict negation throws');
440
+ },
441
+ },
399
442
  {
400
443
  name: 'program and solver public classes',
401
444
  run: () => {