eyeprolog 1.5.27 → 1.5.29

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.5.27",
6
+ "version": "1.5.29",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
package/src/iso.js CHANGED
@@ -860,7 +860,7 @@ function* retractSolutions({ solver, goal, env }, state) {
860
860
  if (!unify(parts.head, copied.args[0], next)) continue;
861
861
  if (parts.rule && !unify(parts.body, copied.args[1], next)) continue;
862
862
  if (!parts.rule && !(copied.args[1].type === ATOM && copied.args[1].name === 'true')) continue;
863
- matches.push({ clause, next });
863
+ matches.push({ clause, next: next._toPersistentEnv?.() ?? next });
864
864
  }
865
865
  for (let index = 0; index < matches.length; index++) {
866
866
  const { clause, next } = matches[index];
package/src/solver.js CHANGED
@@ -392,6 +392,8 @@ export class Solver {
392
392
 
393
393
  *solve(goals, env = new Env(), depth = 0) {
394
394
  if (!Array.isArray(goals)) goals = [goals];
395
+ const ownsSolverBindings = env._isSolverEnv?.() !== true;
396
+ if (ownsSolverBindings) env = env._asSolverEnv();
395
397
  env.setOccursCheckHandler(this.occursCheckHandler);
396
398
  env.setAttributeHookRunner(this.attributeHookRunner);
397
399
  if (this.isoStrict) rejectStrictIsoStringTerms(goals, env);
@@ -437,6 +439,42 @@ export class Solver {
437
439
  });
438
440
  continue;
439
441
  }
442
+ if (frame.kind === 'unifyArgsBranch') {
443
+ if (this.solutionsSeen >= this.solutionLimit) continue;
444
+ const next = frame.env.clone();
445
+ const sourceArgs = frame.freshNonGround && !frame.args.every((arg) => termIsGround(arg))
446
+ ? freshTerm(compound('$branch_args', frame.args), nextFreshId()).args
447
+ : frame.args;
448
+ let ok = true;
449
+ for (let i = 0; i < frame.goal.arity; i++) {
450
+ this.stats.unify_calls++;
451
+ if (!unify(frame.goal.args[i], sourceArgs[i], next)) { ok = false; break; }
452
+ }
453
+ if (ok) {
454
+ stack.push({
455
+ kind: 'goals',
456
+ goals: frame.goals,
457
+ env: next,
458
+ depth: frame.depth,
459
+ active: frame.active,
460
+ });
461
+ }
462
+ continue;
463
+ }
464
+ if (frame.kind === 'bindBranch') {
465
+ if (this.solutionsSeen >= this.solutionLimit) continue;
466
+ const next = frame.env.clone();
467
+ for (let i = 0; i < frame.names.length; i++) next.bind(frame.names[i], frame.values[i]);
468
+ if (next._variableConstraints != null && !next.validateVariableConstraints()) continue;
469
+ stack.push({
470
+ kind: 'goals',
471
+ goals: frame.goals,
472
+ env: next,
473
+ depth: frame.depth,
474
+ active: frame.active,
475
+ });
476
+ continue;
477
+ }
440
478
  if (frame.kind === 'userClause') {
441
479
  if (this.solutionsSeen >= this.solutionLimit) continue;
442
480
  const { clause, goal: clauseGoal, rest: clauseRest, env: clauseEnv, depth: clauseDepth,
@@ -585,7 +623,7 @@ export class Solver {
585
623
  this.solutionsSeen++;
586
624
  this.stats.completed_goal_lists++;
587
625
  this.active = active;
588
- yield env;
626
+ yield ownsSolverBindings && env._isSolverEnv?.() === true ? env._toPersistentEnv() : env;
589
627
  break;
590
628
  }
591
629
 
@@ -673,7 +711,7 @@ export class Solver {
673
711
  // pending exactly as they do for a direct call. A fresh active marker
674
712
  // keeps cut opaque at the call/N boundary.
675
713
  const expanded = def.expandGoal({ solver: this, goal, env });
676
- const invocation = { goal, env };
714
+ const invocation = { goal, env: env._snapshotForSolverRead() };
677
715
  active = [...active, invocation];
678
716
  goals = [expanded, { kind: 'releaseActive' }, ...rest];
679
717
  depth++;
@@ -1096,12 +1134,12 @@ export class Solver {
1096
1134
  // expansion of the current rule body, but it must be released before
1097
1135
  // the caller's remaining goals are solved. Keeping the goal active
1098
1136
  // through rest goals over-prunes valid transitive/recursive derivations.
1099
- this.active.push({ goal, env: goalEnv });
1137
+ this.active.push({ goal, env: goalEnv._snapshotForSolverRead() });
1100
1138
  for (const bodyEnv of this.solve(body, env, depth + 1)) {
1101
1139
  if (this.solutionsSeen > 0) this.solutionsSeen--;
1102
1140
  this.active.pop();
1103
1141
  yield* this.solve(rest, bodyEnv, depth + 1);
1104
- this.active.push({ goal, env: goalEnv });
1142
+ this.active.push({ goal, env: goalEnv._snapshotForSolverRead() });
1105
1143
  if (this.solutionsSeen >= this.solutionLimit) break;
1106
1144
  }
1107
1145
  this.active.pop();
@@ -1231,25 +1269,24 @@ function pushMemoAnswerFrames(stack, entry, goal, rest, env, depth, active, solv
1231
1269
  selected = { bucket, fallback, length: candidateLength };
1232
1270
  }
1233
1271
  }
1234
- const replay = (answerIndex) => {
1235
- const storedArgs = entry.answers[answerIndex];
1236
- const answerArgs = storedArgs.every((arg) => termIsGround(arg))
1237
- ? storedArgs
1238
- : freshTerm(compound('$memo_answer', storedArgs), nextFreshId()).args;
1239
- const next = env.clone();
1240
- let ok = true;
1241
- for (let i = 0; i < goal.arity; i++) {
1242
- solver.stats.unify_calls++;
1243
- if (!unify(goal.args[i], answerArgs[i], next)) { ok = false; break; }
1244
- }
1245
- if (ok) stack.push({ kind: 'goals', goals: rest, env: next, depth: depth + 1, active });
1272
+ const pushReplay = (answerIndex) => {
1273
+ stack.push({
1274
+ kind: 'unifyArgsBranch',
1275
+ goal,
1276
+ args: entry.answers[answerIndex],
1277
+ freshNonGround: true,
1278
+ goals: rest,
1279
+ env,
1280
+ depth: depth + 1,
1281
+ active,
1282
+ });
1246
1283
  };
1247
1284
  if (selected != null) {
1248
- for (let i = selected.fallback.length - 1; i >= 0; i--) replay(selected.fallback[i]);
1249
- for (let i = selected.bucket.length - 1; i >= 0; i--) replay(selected.bucket[i]);
1285
+ for (let i = selected.fallback.length - 1; i >= 0; i--) pushReplay(selected.fallback[i]);
1286
+ for (let i = selected.bucket.length - 1; i >= 0; i--) pushReplay(selected.bucket[i]);
1250
1287
  return;
1251
1288
  }
1252
- for (let answerIndex = entry.answers.length - 1; answerIndex >= 0; answerIndex--) replay(answerIndex);
1289
+ for (let answerIndex = entry.answers.length - 1; answerIndex >= 0; answerIndex--) pushReplay(answerIndex);
1253
1290
  }
1254
1291
 
1255
1292
  function pushUserGoalUncachedFrames(stack, solver, group, goal, rest, env, depth, active) {
@@ -1258,7 +1295,7 @@ function pushUserGoalUncachedFrames(stack, solver, group, goal, rest, env, depth
1258
1295
  if (tryPushGroundChainFrames(stack, solver, group, goal, rest, env, depth, active)) return;
1259
1296
  const candidates = selectClauseCandidates(group, goal, env);
1260
1297
  const frames = [];
1261
- const invocation = { goal, env };
1298
+ const invocation = { goal, env: env._snapshotForSolverRead() };
1262
1299
  // Active frames serve two purposes: they delimit cut and detect variants in
1263
1300
  // recursive user predicates. Cut-free, non-recursive library helpers need
1264
1301
  // neither. Copying their full active stack at every recursive step made
@@ -1535,18 +1572,20 @@ function pushWfsAnswerFrames(stack, model, group, goal, rest, env, depth, active
1535
1572
  if (!relation) return;
1536
1573
  for (let rowIndex = relation.rows.length - 1; rowIndex >= 0; rowIndex--) {
1537
1574
  const row = relation.rows[rowIndex];
1538
- const next = env.clone();
1539
- let ok = true;
1540
- for (let i = 0; i < goal.arity; i++) {
1541
- solver.stats.unify_calls++;
1542
- if (!unify(goal.args[i], row[i], next)) { ok = false; break; }
1543
- }
1544
- if (!ok) continue;
1545
1575
  if (!lower?.has(row)) {
1546
1576
  solver.stats.wfs_undefined_answers++;
1547
1577
  continue;
1548
1578
  }
1549
- stack.push({ kind: 'goals', goals: rest, env: next, depth: depth + 1, active });
1579
+ stack.push({
1580
+ kind: 'unifyArgsBranch',
1581
+ goal,
1582
+ args: row,
1583
+ freshNonGround: false,
1584
+ goals: rest,
1585
+ env,
1586
+ depth: depth + 1,
1587
+ active,
1588
+ });
1550
1589
  }
1551
1590
  }
1552
1591
 
@@ -2030,14 +2069,8 @@ function termReferencesResolvedVariable(term, variableName, env) {
2030
2069
  }
2031
2070
 
2032
2071
  function envHasObservableAliasTo(variableName, env) {
2033
- for (let state = env?._state; state != null; state = state.parent) {
2034
- if (state.bindingName != null && state.bindingName !== variableName &&
2035
- termReferencesResolvedVariable(state.bindingValue, variableName, env)) return true;
2036
- if (state.bindings) {
2037
- for (const [name, value] of state.bindings) {
2038
- if (name !== variableName && termReferencesResolvedVariable(value, variableName, env)) return true;
2039
- }
2040
- }
2072
+ for (const [name, value] of env?._bindingEntries?.() ?? []) {
2073
+ if (name !== variableName && termReferencesResolvedVariable(value, variableName, env)) return true;
2041
2074
  }
2042
2075
  return false;
2043
2076
  }
@@ -2315,10 +2348,15 @@ function tryPushScalarFactRunFrames(stack, solver, goals, env, depth, active) {
2315
2348
  const state = localStack.pop();
2316
2349
  solver.stats.max_depth = Math.max(solver.stats.max_depth, state.depth);
2317
2350
  if (state.index === runLength) {
2318
- const next = env.clone();
2319
- for (let i = 0; i < state.names.length; i++) next.bind(state.names[i], state.values[i]);
2320
- if (next._variableConstraints != null && !next.validateVariableConstraints()) continue;
2321
- frames.push({ kind: 'goals', goals: rest, env: next, depth: state.depth, active });
2351
+ frames.push({
2352
+ kind: 'bindBranch',
2353
+ names: state.names,
2354
+ values: state.values,
2355
+ goals: rest,
2356
+ env,
2357
+ depth: state.depth,
2358
+ active,
2359
+ });
2322
2360
  if (frames.length > frameLimit) {
2323
2361
  // Do not repeat the old bug of abandoning the optimization and then
2324
2362
  // re-solving through the generic engine. Restart this rare oversized
package/src/term.js CHANGED
@@ -15,6 +15,10 @@ const EMPTY_ARGS = Object.freeze([]);
15
15
  // propagation uses 256 because its repeated negative lookups dominate sooner.
16
16
  const ENV_FLATTEN_DEPTH = 256;
17
17
  const ATTRIBUTED_ENV_FLATTEN_DEPTH = 256;
18
+ // Environment states are immutable once published to a branch, so an absent
19
+ // binding is just as safe to cache as a present one. Negative lookups are
20
+ // especially common while tabling and constraint code probes fresh variables.
21
+ const ENV_UNBOUND = Symbol('environment-unbound');
18
22
  // Runtime terms are structurally immutable: environments hold bindings beside
19
23
  // them rather than rewriting their argument arrays. Cache only the syntactic
20
24
  // variable names; binding reachability is still checked against each Env.
@@ -163,6 +167,15 @@ export class Env {
163
167
  this._attributeHookRunner = typeof handler === 'function' ? handler : null;
164
168
  return this;
165
169
  }
170
+ _asSolverEnv() {
171
+ return SolverEnv.from(this);
172
+ }
173
+ _isSolverEnv() {
174
+ return false;
175
+ }
176
+ _snapshotForSolverRead() {
177
+ return this.clone();
178
+ }
166
179
  adopt(other) {
167
180
  if (!(other instanceof Env)) throw new TypeError('Env.adopt expects Env');
168
181
  this._state = other._state;
@@ -292,9 +305,11 @@ export class Env {
292
305
  }
293
306
  get(name) {
294
307
  const root = this._state;
295
- if (root.cacheName === name) return root.cacheValue;
308
+ if (root.cacheName === name) {
309
+ return root.cacheValue === ENV_UNBOUND ? undefined : root.cacheValue;
310
+ }
296
311
  const cached = root.cache?.get(name);
297
- if (cached !== undefined) return cached;
312
+ if (cached !== undefined) return cached === ENV_UNBOUND ? undefined : cached;
298
313
  for (let state = root; state != null; state = state.parent) {
299
314
  let value;
300
315
  let found = false;
@@ -317,8 +332,32 @@ export class Env {
317
332
  return value;
318
333
  }
319
334
  }
335
+ if (root.depth >= 4) {
336
+ if (root.cacheName == null) {
337
+ root.cacheName = name;
338
+ root.cacheValue = ENV_UNBOUND;
339
+ } else {
340
+ (root.cache ??= new Map([[root.cacheName, root.cacheValue]])).set(name, ENV_UNBOUND);
341
+ }
342
+ }
320
343
  return undefined;
321
344
  }
345
+ *_bindingEntries() {
346
+ const seen = new Set();
347
+ for (let state = this._state; state != null; state = state.parent) {
348
+ if (state.bindingName != null && !seen.has(state.bindingName)) {
349
+ seen.add(state.bindingName);
350
+ yield [state.bindingName, state.bindingValue];
351
+ }
352
+ if (state.bindings) {
353
+ for (const [name, value] of state.bindings) {
354
+ if (seen.has(name)) continue;
355
+ seen.add(name);
356
+ yield [name, value];
357
+ }
358
+ }
359
+ }
360
+ }
322
361
  compactForDeepContinuation(segmentLimit = 16) {
323
362
  if ((this._state.segmentCount ?? 0) < segmentLimit) return false;
324
363
  const flattened = new Map();
@@ -634,6 +673,227 @@ export class Env {
634
673
  }
635
674
  }
636
675
 
676
+
677
+ class SolverBindingStore {
678
+ constructor(bindings) {
679
+ this.slots = new Map();
680
+ this.names = [];
681
+ this.values = [];
682
+ this.baseValues = [];
683
+ this.slotLastTrail = [];
684
+ this.freeSlots = [];
685
+ this.trailNames = [];
686
+ this.trailSlots = [];
687
+ this.trailOldValues = [];
688
+ this.trailNewValues = [];
689
+ this.trailPreviousForSlot = [];
690
+ this.trailCreated = [];
691
+ if (bindings != null) {
692
+ for (const [name, value] of bindings) {
693
+ const slot = this.names.length;
694
+ this.slots.set(name, slot);
695
+ this.names.push(name);
696
+ this.values.push(value);
697
+ this.baseValues.push(value);
698
+ this.slotLastTrail.push(-1);
699
+ }
700
+ }
701
+ }
702
+
703
+ mark() {
704
+ return this.trailSlots.length;
705
+ }
706
+
707
+ rollback(mark) {
708
+ if (mark > this.trailSlots.length) {
709
+ throw new Error('stale solver binding mark');
710
+ }
711
+ while (this.trailSlots.length > mark) {
712
+ const created = this.trailCreated.pop();
713
+ const previousForSlot = this.trailPreviousForSlot.pop();
714
+ this.trailNewValues.pop();
715
+ const oldValue = this.trailOldValues.pop();
716
+ const slot = this.trailSlots.pop();
717
+ const name = this.trailNames.pop();
718
+ this.slotLastTrail[slot] = previousForSlot;
719
+ if (created) {
720
+ this.slots.delete(name);
721
+ this.values[slot] = ENV_UNBOUND;
722
+ this.baseValues[slot] = ENV_UNBOUND;
723
+ this.names[slot] = null;
724
+ this.freeSlots.push(slot);
725
+ } else {
726
+ this.values[slot] = oldValue;
727
+ }
728
+ }
729
+ }
730
+
731
+ bind(name, value) {
732
+ let slot = this.slots.get(name);
733
+ const created = slot === undefined;
734
+ if (created) {
735
+ slot = this.freeSlots.length === 0 ? this.values.length : this.freeSlots.pop();
736
+ this.slots.set(name, slot);
737
+ this.names[slot] = name;
738
+ this.values[slot] = ENV_UNBOUND;
739
+ this.baseValues[slot] = ENV_UNBOUND;
740
+ this.slotLastTrail[slot] = -1;
741
+ }
742
+ const entry = this.trailSlots.length;
743
+ this.trailNames.push(name);
744
+ this.trailSlots.push(slot);
745
+ this.trailOldValues.push(this.values[slot]);
746
+ this.trailNewValues.push(value);
747
+ this.trailPreviousForSlot.push(this.slotLastTrail[slot] ?? -1);
748
+ this.trailCreated.push(created);
749
+ this.slotLastTrail[slot] = entry;
750
+ this.values[slot] = value;
751
+ }
752
+
753
+ getAtMark(name, mark) {
754
+ if (mark > this.trailSlots.length) throw new Error('stale solver binding mark');
755
+ const slot = this.slots.get(name);
756
+ if (slot === undefined) return undefined;
757
+ let entry = this.slotLastTrail[slot] ?? -1;
758
+ while (entry >= mark) entry = this.trailPreviousForSlot[entry] ?? -1;
759
+ const value = entry >= 0 ? this.trailNewValues[entry] : this.baseValues[slot];
760
+ return value === ENV_UNBOUND ? undefined : value;
761
+ }
762
+ }
763
+
764
+ function copyEnvBranchState(source, target) {
765
+ target._delays = source._delays;
766
+ target._variableConstraints = source._variableConstraints;
767
+ target._variableAnnotations = source._variableAnnotations;
768
+ target._prologAttributes = source._prologAttributes;
769
+ target._attributeHookRunner = source._attributeHookRunner;
770
+ target._pendingAttributeGoals = source._pendingAttributeGoals;
771
+ target._backtrackableBlackboard = source._backtrackableBlackboard;
772
+ target._occursCheckHandler = source._occursCheckHandler;
773
+ target._localVariables = source._localVariables;
774
+ target._compactVariableOrigins = source._compactVariableOrigins;
775
+ target._compactVariableRisks = source._compactVariableRisks;
776
+ return target;
777
+ }
778
+
779
+ class SolverEnvReadView {
780
+ constructor(source) {
781
+ this._solverBindings = source._solverBindings;
782
+ this._solverBindingMark = source._solverBindingMark;
783
+ this._localVariables = source._localVariables;
784
+ }
785
+ get(name) {
786
+ return this._solverBindings.getAtMark(name, this._solverBindingMark);
787
+ }
788
+ has(name) {
789
+ return this.get(name) !== undefined;
790
+ }
791
+ isLocalVariable(name) {
792
+ return this._localVariables?.has(name) === true;
793
+ }
794
+ }
795
+
796
+ // SolverEnv replaces persistent binding layers with one solver-owned mutable
797
+ // slot store and a linear trail. Clones are just trail marks plus the existing
798
+ // copy-on-write logical side stores. The solver explores these marks in depth-
799
+ // first order, so rollback can discard unreachable branch history immediately.
800
+ // Public Env remains persistent and is used for stable API snapshots.
801
+ class SolverEnv extends Env {
802
+ constructor() {
803
+ super();
804
+ this._solverBindings = new SolverBindingStore();
805
+ this._solverBindingMark = 0;
806
+ this._state = null;
807
+ }
808
+
809
+ static from(env) {
810
+ if (env instanceof SolverEnv) return env;
811
+ const solverEnv = Object.create(SolverEnv.prototype);
812
+ solverEnv._state = null;
813
+ solverEnv._solverBindings = new SolverBindingStore(env?._bindingEntries?.() ?? []);
814
+ solverEnv._solverBindingMark = 0;
815
+ copyEnvBranchState(env ?? new Env(), solverEnv);
816
+ return solverEnv;
817
+ }
818
+
819
+ _asSolverEnv() {
820
+ return this;
821
+ }
822
+
823
+ _isSolverEnv() {
824
+ return true;
825
+ }
826
+
827
+ _snapshotForSolverRead() {
828
+ return new SolverEnvReadView(this);
829
+ }
830
+
831
+ _activateBindings() {
832
+ if (this._solverBindingMark !== this._solverBindings.trailSlots.length) {
833
+ this._solverBindings.rollback(this._solverBindingMark);
834
+ }
835
+ }
836
+
837
+ clone() {
838
+ this._activateBindings();
839
+ const clone = Object.create(SolverEnv.prototype);
840
+ clone._state = null;
841
+ clone._solverBindings = this._solverBindings;
842
+ clone._solverBindingMark = this._solverBindingMark;
843
+ return copyEnvBranchState(this, clone);
844
+ }
845
+
846
+ adopt(other) {
847
+ if (!(other instanceof Env)) throw new TypeError('Env.adopt expects Env');
848
+ if (other instanceof SolverEnv && other._solverBindings === this._solverBindings) {
849
+ other._activateBindings();
850
+ this._solverBindingMark = other._solverBindingMark;
851
+ } else {
852
+ const replacement = SolverEnv.from(other);
853
+ this._solverBindings = replacement._solverBindings;
854
+ this._solverBindingMark = replacement._solverBindingMark;
855
+ }
856
+ copyEnvBranchState(other, this);
857
+ return this;
858
+ }
859
+
860
+ has(name) {
861
+ return this.get(name) !== undefined;
862
+ }
863
+
864
+ get(name) {
865
+ this._activateBindings();
866
+ const slot = this._solverBindings.slots.get(name);
867
+ if (slot === undefined) return undefined;
868
+ const value = this._solverBindings.values[slot];
869
+ return value === ENV_UNBOUND ? undefined : value;
870
+ }
871
+
872
+ *_bindingEntries() {
873
+ this._activateBindings();
874
+ for (const [name, slot] of this._solverBindings.slots) {
875
+ const value = this._solverBindings.values[slot];
876
+ if (value !== ENV_UNBOUND) yield [name, value];
877
+ }
878
+ }
879
+
880
+ bind(name, term) {
881
+ this._activateBindings();
882
+ this._solverBindings.bind(name, term);
883
+ this._solverBindingMark = this._solverBindings.mark();
884
+ }
885
+
886
+ compactForDeepContinuation() {
887
+ return false;
888
+ }
889
+
890
+ _toPersistentEnv() {
891
+ const stable = new Env(this._bindingEntries());
892
+ copyEnvBranchState(this, stable);
893
+ return stable;
894
+ }
895
+ }
896
+
637
897
  export function deref(term, env) {
638
898
  // Follow variable bindings until a non-variable term is reached. The seen set
639
899
  // protects readback from accidental cycles in partially constructed terms.
@@ -7224,11 +7224,15 @@ function whiteBoxCases() {
7224
7224
  assertEqual(env.get('V31').name, '31', 'binding before flatten');
7225
7225
  assertEqual(env.get('V39').name, '39', 'latest binding');
7226
7226
  assertEqual(env.has('missing'), false, 'missing binding');
7227
+ assertEqual(env.has('missing'), false, 'cached missing binding');
7227
7228
 
7228
7229
  const clone = env.clone();
7229
7230
  clone.bind('OnlyClone', atom('yes'));
7231
+ clone.bind('missing', atom('now_present'));
7230
7232
  assertEqual(clone.get('OnlyClone').name, 'yes', 'clone write');
7233
+ assertEqual(clone.get('missing').name, 'now_present', 'clone write supersedes cached miss');
7231
7234
  assertEqual(env.has('OnlyClone'), false, 'clone remains isolated');
7235
+ assertEqual(env.has('missing'), false, 'parent retains cached miss');
7232
7236
  },
7233
7237
  },
7234
7238
  {