eyeprolog 1.5.28 → 1.5.30

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.28",
6
+ "version": "1.5.30",
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++;
@@ -810,15 +848,15 @@ export class Solver {
810
848
  if (!entry.computing) {
811
849
  if (!this.tableCoordinator) {
812
850
  this.tableCoordinator = { entry, cycleSeen: false, entries: new Set([entry]) };
813
- scheduleTableFixpointRound(stack, this, { entry, group, goal, rest, env, depth, active });
851
+ scheduleTableFixpointRound(stack, this, { entry, group, goal, rest, env, depth, active, mapKey });
814
852
  } else {
815
853
  entry.computing = true;
816
854
  stack.push({ kind: 'completeMemo', entry, revision: this.programRevision });
817
- pushUserGoalUncachedFrames(stack, this, group, goal, [{ kind: 'memoStore', entry, goal, revision: this.programRevision }, ...rest], env, depth, active);
855
+ pushUserGoalUncachedFrames(stack, this, group, goal, [{ kind: 'memoStore', entry, goal, revision: this.programRevision }, ...rest], env, depth, active, mapKey);
818
856
  }
819
857
  break;
820
858
  }
821
- if (this.tableCoordinator && (usingBroadTable || activeVariantIn(goal, env, active))) {
859
+ if (this.tableCoordinator && (usingBroadTable || activeTableKeyIn(mapKey, active))) {
822
860
  this.tableCoordinator.cycleSeen = true;
823
861
  }
824
862
  pushMemoAnswerFrames(stack, entry, goal, rest, env, depth, active, this);
@@ -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();
@@ -1214,6 +1252,7 @@ function scheduleTableFixpointRound(stack, solver, frame) {
1214
1252
  frame.env,
1215
1253
  frame.depth,
1216
1254
  frame.active,
1255
+ frame.mapKey,
1217
1256
  );
1218
1257
  }
1219
1258
 
@@ -1231,34 +1270,33 @@ function pushMemoAnswerFrames(stack, entry, goal, rest, env, depth, active, solv
1231
1270
  selected = { bucket, fallback, length: candidateLength };
1232
1271
  }
1233
1272
  }
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 });
1273
+ const pushReplay = (answerIndex) => {
1274
+ stack.push({
1275
+ kind: 'unifyArgsBranch',
1276
+ goal,
1277
+ args: entry.answers[answerIndex],
1278
+ freshNonGround: true,
1279
+ goals: rest,
1280
+ env,
1281
+ depth: depth + 1,
1282
+ active,
1283
+ });
1246
1284
  };
1247
1285
  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]);
1286
+ for (let i = selected.fallback.length - 1; i >= 0; i--) pushReplay(selected.fallback[i]);
1287
+ for (let i = selected.bucket.length - 1; i >= 0; i--) pushReplay(selected.bucket[i]);
1250
1288
  return;
1251
1289
  }
1252
- for (let answerIndex = entry.answers.length - 1; answerIndex >= 0; answerIndex--) replay(answerIndex);
1290
+ for (let answerIndex = entry.answers.length - 1; answerIndex >= 0; answerIndex--) pushReplay(answerIndex);
1253
1291
  }
1254
1292
 
1255
- function pushUserGoalUncachedFrames(stack, solver, group, goal, rest, env, depth, active) {
1293
+ function pushUserGoalUncachedFrames(stack, solver, group, goal, rest, env, depth, active, tableMapKey = null) {
1256
1294
  if (group.fastPi && pushFastPiFrames(stack, goal, rest, env, depth, active)) return;
1257
1295
  if (tryPushGroundScalarRuleFrame(stack, solver, group, goal, rest, env, depth, active)) return;
1258
1296
  if (tryPushGroundChainFrames(stack, solver, group, goal, rest, env, depth, active)) return;
1259
1297
  const candidates = selectClauseCandidates(group, goal, env);
1260
1298
  const frames = [];
1261
- const invocation = { goal, env };
1299
+ const invocation = { goal, env: env._snapshotForSolverRead(), tableMapKey };
1262
1300
  // Active frames serve two purposes: they delimit cut and detect variants in
1263
1301
  // recursive user predicates. Cut-free, non-recursive library helpers need
1264
1302
  // neither. Copying their full active stack at every recursive step made
@@ -1535,18 +1573,20 @@ function pushWfsAnswerFrames(stack, model, group, goal, rest, env, depth, active
1535
1573
  if (!relation) return;
1536
1574
  for (let rowIndex = relation.rows.length - 1; rowIndex >= 0; rowIndex--) {
1537
1575
  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
1576
  if (!lower?.has(row)) {
1546
1577
  solver.stats.wfs_undefined_answers++;
1547
1578
  continue;
1548
1579
  }
1549
- stack.push({ kind: 'goals', goals: rest, env: next, depth: depth + 1, active });
1580
+ stack.push({
1581
+ kind: 'unifyArgsBranch',
1582
+ goal,
1583
+ args: row,
1584
+ freshNonGround: false,
1585
+ goals: rest,
1586
+ env,
1587
+ depth: depth + 1,
1588
+ active,
1589
+ });
1550
1590
  }
1551
1591
  }
1552
1592
 
@@ -2030,14 +2070,8 @@ function termReferencesResolvedVariable(term, variableName, env) {
2030
2070
  }
2031
2071
 
2032
2072
  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
- }
2073
+ for (const [name, value] of env?._bindingEntries?.() ?? []) {
2074
+ if (name !== variableName && termReferencesResolvedVariable(value, variableName, env)) return true;
2041
2075
  }
2042
2076
  return false;
2043
2077
  }
@@ -2315,10 +2349,15 @@ function tryPushScalarFactRunFrames(stack, solver, goals, env, depth, active) {
2315
2349
  const state = localStack.pop();
2316
2350
  solver.stats.max_depth = Math.max(solver.stats.max_depth, state.depth);
2317
2351
  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 });
2352
+ frames.push({
2353
+ kind: 'bindBranch',
2354
+ names: state.names,
2355
+ values: state.values,
2356
+ goals: rest,
2357
+ env,
2358
+ depth: state.depth,
2359
+ active,
2360
+ });
2322
2361
  if (frames.length > frameLimit) {
2323
2362
  // Do not repeat the old bug of abandoning the optimization and then
2324
2363
  // re-solving through the generic engine. Restart this rare oversized
@@ -2977,6 +3016,13 @@ function rememberMemoAnswer(entry, goal, env) {
2977
3016
  }
2978
3017
  }
2979
3018
 
3019
+ function activeTableKeyIn(mapKey, active) {
3020
+ for (let index = active.length - 1; index >= 0; index--) {
3021
+ if (active[index]?.tableMapKey === mapKey) return true;
3022
+ }
3023
+ return false;
3024
+ }
3025
+
2980
3026
  function activeVariantIn(goal, env, active) {
2981
3027
  if (active.length === 0) return false;
2982
3028
  let goalShape = null;
package/src/term.js CHANGED
@@ -167,6 +167,15 @@ export class Env {
167
167
  this._attributeHookRunner = typeof handler === 'function' ? handler : null;
168
168
  return this;
169
169
  }
170
+ _asSolverEnv() {
171
+ return SolverEnv.from(this);
172
+ }
173
+ _isSolverEnv() {
174
+ return false;
175
+ }
176
+ _snapshotForSolverRead() {
177
+ return this.clone();
178
+ }
170
179
  adopt(other) {
171
180
  if (!(other instanceof Env)) throw new TypeError('Env.adopt expects Env');
172
181
  this._state = other._state;
@@ -333,6 +342,22 @@ export class Env {
333
342
  }
334
343
  return undefined;
335
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
+ }
336
361
  compactForDeepContinuation(segmentLimit = 16) {
337
362
  if ((this._state.segmentCount ?? 0) < segmentLimit) return false;
338
363
  const flattened = new Map();
@@ -648,6 +673,227 @@ export class Env {
648
673
  }
649
674
  }
650
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
+
651
897
  export function deref(term, env) {
652
898
  // Follow variable bindings until a non-variable term is reached. The seen set
653
899
  // protects readback from accidental cycles in partially constructed terms.