eyeprolog 1.6.26 → 1.6.28

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/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.6.26",
6
+ "version": "1.6.28",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
@@ -21,7 +21,10 @@ higher-level frontends.
21
21
  `datalog-common.js` owns what the two Datalog evaluators share: scalar row
22
22
  keying and the per-argument candidate-bucket narrowing both index their
23
23
  relations with. Their fixpoint algorithms and relation storage stay in their
24
- own modules.
24
+ own modules. Positive Datalog compiles clause variables to numeric slots
25
+ and joins through array bindings; WFS retains its persistent Map bindings.
26
+ Candidate selection has a direct entry point for each binding representation
27
+ so their inner loops do not dispatch through a binding-store abstraction.
25
28
  `cleanup.js` owns lifecycle-aware disposal of protected builtin
26
29
  iterators and registers the normal-profile cleanup controls without making
27
30
  `solver.js` depend back on the language registry.
@@ -91,3 +91,17 @@ export function selectCandidateIndexes(indexes, args, bindings) {
91
91
  }
92
92
  return selected;
93
93
  }
94
+
95
+ // Positive Datalog compiles clause variables to numeric slots. Keep its hot
96
+ // path direct rather than introducing a binding-store dispatch in each lookup.
97
+ export function selectCandidateIndexesForSlots(indexes, args, bindings) {
98
+ let selected = null;
99
+ for (let i = 0; i < args.length; i++) {
100
+ const value = args[i].type === VAR ? bindings[args[i].slot] : args[i];
101
+ if (value == null) continue;
102
+ const bucket = indexes[i].get(scalarKey(value)) ?? EMPTY_ARRAY;
103
+ if (selected == null || bucket.length < selected.length) selected = bucket;
104
+ if (selected.length === 0) break;
105
+ }
106
+ return selected;
107
+ }
package/src/datalog.js CHANGED
@@ -14,7 +14,7 @@ import {
14
14
  predicateKey,
15
15
  sameScalar,
16
16
  scalarKey,
17
- selectCandidateIndexes,
17
+ selectCandidateIndexesForSlots,
18
18
  } from './datalog-common.js';
19
19
 
20
20
  class DatalogRelation {
@@ -61,7 +61,7 @@ class DatalogRelation {
61
61
  const tuple = new Array(args.length);
62
62
  const parts = new Array(args.length);
63
63
  for (let i = 0; i < args.length; i++) {
64
- const value = args[i].type === VAR ? bindings.get(args[i].name) : args[i];
64
+ const value = args[i].type === VAR ? bindings[args[i].slot] : args[i];
65
65
  if (value == null || value.type === VAR) return null;
66
66
  tuple[i] = value;
67
67
  parts[i] = scalarKey(value);
@@ -87,7 +87,7 @@ class DatalogRelation {
87
87
  }
88
88
 
89
89
  candidateIndexes(args, bindings) {
90
- return selectCandidateIndexes(this.indexes, args, bindings);
90
+ return selectCandidateIndexesForSlots(this.indexes, args, bindings);
91
91
  }
92
92
  }
93
93
 
@@ -106,11 +106,21 @@ function compileProgram(program, rootGroup) {
106
106
  for (const group of groups) {
107
107
  const headKey = predicateKey(group.module, group.name, group.arity);
108
108
  for (const clause of group.clauses) {
109
- const headArgs = clause.head.args ?? EMPTY_ARRAY;
110
109
  if (clause.body.length === 0) continue;
111
110
  const body = clause.body.map((goal) => directLiteral(goal, group.module));
112
111
  if (body.some((literal) => literal == null)) continue;
113
- const rule = { headKey, headArgs, body };
112
+ // Variable names matter only within this source clause. Compile them to
113
+ // array slots once so each join can bind and undo without Map lookups.
114
+ const variables = new Map();
115
+ const compileArgument = (arg) => {
116
+ if (arg.type !== VAR) return arg;
117
+ let slot = variables.get(arg.name);
118
+ if (slot == null) variables.set(arg.name, slot = variables.size);
119
+ return { type: VAR, slot };
120
+ };
121
+ const headArgs = (clause.head.args ?? EMPTY_ARRAY).map(compileArgument);
122
+ for (const literal of body) literal.args = literal.args.map(compileArgument);
123
+ const rule = { headKey, headArgs, body, variableCount: variables.size };
114
124
  rules.push(rule);
115
125
  for (let index = 0; index < body.length; index++) {
116
126
  const key = body[index].key;
@@ -130,20 +140,20 @@ function matchTupleMutable(args, tuple, bindings) {
130
140
  for (let i = 0; i < args.length; i++) {
131
141
  const pattern = args[i];
132
142
  if (pattern.type === VAR) {
133
- const current = bindings.get(pattern.name);
143
+ const current = bindings[pattern.slot];
134
144
  if (current != null) {
135
145
  if (!sameScalar(current, tuple[i])) {
136
- for (let j = added.length - 1; j >= 0; j--) bindings.delete(added[j]);
146
+ undoBindings(bindings, added);
137
147
  return null;
138
148
  }
139
149
  } else {
140
- bindings.set(pattern.name, tuple[i]);
141
- added.push(pattern.name);
150
+ bindings[pattern.slot] = tuple[i];
151
+ added.push(pattern.slot);
142
152
  }
143
153
  continue;
144
154
  }
145
155
  if (!sameScalar(pattern, tuple[i])) {
146
- for (let j = added.length - 1; j >= 0; j--) bindings.delete(added[j]);
156
+ undoBindings(bindings, added);
147
157
  return null;
148
158
  }
149
159
  }
@@ -151,7 +161,7 @@ function matchTupleMutable(args, tuple, bindings) {
151
161
  }
152
162
 
153
163
  function undoBindings(bindings, added) {
154
- for (let i = added.length - 1; i >= 0; i--) bindings.delete(added[i]);
164
+ for (let i = added.length - 1; i >= 0; i--) bindings[added[i]] = undefined;
155
165
  }
156
166
 
157
167
 
@@ -234,7 +244,7 @@ export function evaluatePositiveDatalog(program, rootGroup) {
234
244
  const triggerEntries = compiled.triggers.get(event.key) ?? EMPTY_ARRAY;
235
245
  for (const { rule, literalIndex } of triggerEntries) {
236
246
  const fixed = rule.body[literalIndex];
237
- const bindings = new Map();
247
+ const bindings = new Array(rule.variableCount);
238
248
  if (!matchTupleMutable(fixed.args, event.tuple, bindings)) continue;
239
249
  const remaining = [];
240
250
  for (let i = 0; i < rule.body.length; i++) if (i !== literalIndex) remaining.push(i);
package/src/explain.js CHANGED
@@ -647,7 +647,18 @@ function originalVariableName(name) {
647
647
  function resolveForProof(term, env) {
648
648
  const resolved = deref(term, env);
649
649
  if (resolved.type === VAR) return new Term(VAR, originalVariableName(resolved.name), []);
650
- return new Term(resolved.type, resolved.name, resolved.args.map((arg) => resolveForProof(arg, env)));
650
+ // Runtime terms are immutable. A ground subtree already is a proof snapshot,
651
+ // so copy only the ancestors of arguments changed by variable resolution.
652
+ // Allocate the argument array on the first changed child, keeping ground
653
+ // lists allocation-free even when many proof steps share their suffixes.
654
+ let args = null;
655
+ for (let index = 0; index < resolved.args.length; index++) {
656
+ const child = resolveForProof(resolved.args[index], env);
657
+ if (args == null && child !== resolved.args[index]) args = resolved.args.slice();
658
+ if (args != null) args[index] = child;
659
+ }
660
+ if (args == null && resolved.module == null) return resolved;
661
+ return new Term(resolved.type, resolved.name, args ?? resolved.args.slice());
651
662
  }
652
663
 
653
664
  function collectClauseSubstitutions(clause, freshHead, freshBody) {
@@ -10,6 +10,7 @@ import { compareTerms } from '../../src/term.js';
10
10
  import { checkProofDocument, verdict } from '../../src/check-proof.js';
11
11
  import { formatTermForWrite } from '../../src/write.js';
12
12
  import { selectClauseCandidates } from '../../src/program.js';
13
+ import { evaluatePositiveDatalog, relationForDatalogGroup } from '../../src/datalog.js';
13
14
  import { assertEqual, assertIncludes, assertNotIncludes } from '../test-style.mjs';
14
15
  import { goalsFromSource } from '../goal-metadata.mjs';
15
16
  import {
@@ -770,6 +771,44 @@ export function whiteBoxCases() {
770
771
  assertEqual(ground.stats.datalog_evaluations, 0, 'ground query keeps the ordinary indexed chain path');
771
772
  },
772
773
  },
774
+ {
775
+ name: 'Datalog joins preserve clause-local variables and repeated arguments',
776
+ run: () => {
777
+ const program = Program.parse(`
778
+ link(a,b,red).
779
+ link(b,c,red).
780
+ link(c,d,red).
781
+ link(x,x,red).
782
+ link(a,b,blue).
783
+ link(c,c,blue).
784
+ link(a,c,blue).
785
+ reach(X,Y,C) :- link(X,Y,C).
786
+ reach(X,Y,C) :- link(X,Z,C), reach(Z,Y,C).
787
+ back(Y,X) :- reach(X,Y,red).
788
+ back(X,Y) :- reach(X,Y,blue).
789
+ diagonal(X) :- reach(X,X,C).
790
+ marked(tag,X) :- reach(X,X,red).
791
+ wide(X,marker,Y) :- reach(X,Y,red).
792
+ ready :- diagonal(x).
793
+ summary :- ready, back(X,Y), marked(tag,Z), wide(X,marker,Y).
794
+ `);
795
+ const model = evaluatePositiveDatalog(program, program.findGroup('summary', 0));
796
+ const checkRows = (name, arity, expected) => {
797
+ const relation = relationForDatalogGroup(model, program.findGroup(name, arity));
798
+ const actual = relation.rows.map((row) => row.map((value) => value.name).join(':')).sort();
799
+ assertEqual(actual.join('|'), expected.sort().join('|'), `${name}/${arity} rows`);
800
+ };
801
+ checkRows('back', 2, ['b:a', 'c:b', 'd:c', 'x:x', 'c:a', 'd:a', 'd:b', 'a:b', 'c:c', 'a:c']);
802
+ checkRows('diagonal', 1, ['c', 'x']);
803
+ checkRows('marked', 2, ['tag:x']);
804
+ checkRows('wide', 3, [
805
+ 'a:marker:b', 'b:marker:c', 'c:marker:d', 'x:marker:x',
806
+ 'a:marker:c', 'a:marker:d', 'b:marker:d',
807
+ ]);
808
+ checkRows('ready', 0, ['']);
809
+ checkRows('summary', 0, ['']);
810
+ },
811
+ },
773
812
  {
774
813
  name: 'compact finite Datalog planning preserves lazy clause terms',
775
814
  run: () => {