gdp-wired 0.4.0 → 0.6.0

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.
@@ -35,11 +35,24 @@ function decide(lin, path) {
35
35
  const flipped = path.get(canon(add(k(1), scale(lin, -1))))?.holds;
36
36
  return flipped === undefined ? undefined : !flipped;
37
37
  }
38
- // A term's linear form under the path, or undefined if the path leaves one of its choices open.
39
- // Reads become cells, named by map and key: `M[key]`.
38
+ // A term's linear form under the path. Reads become cells, named by map and key: `M[key]`; a choice the path leaves
39
+ // open (∸, min, max, if) becomes a column of its own, `⟦n⟧`, standing for the term's value (see termRows).
40
+ // Remembered per path and term (a path is never changed once built; a new decision is a new path), so a term shared
41
+ // across hypotheses, or nested in a k-step run's later states, is read once per path.
42
+ const linearMemo = new WeakMap();
40
43
  export function linear(e, path) {
44
+ let memo = linearMemo.get(path);
45
+ if (!memo)
46
+ linearMemo.set(path, (memo = new WeakMap()));
47
+ if (memo.has(e))
48
+ return memo.get(e);
49
+ const out = linearOf(e, path);
50
+ memo.set(e, out);
51
+ return out;
52
+ }
53
+ function linearOf(e, path) {
41
54
  switch (e.op) {
42
- case 'var': return e.name === '1' || /[*[\],:]/.test(e.name) ? undefined : v(e.name); // those characters build cells and monomials
55
+ case 'var': return e.name === '1' || /[*[\],:⟦]/.test(e.name) ? undefined : v(e.name); // those characters build cells, monomials and term columns
43
56
  case 'const': return k(e.value);
44
57
  case 'mul': {
45
58
  const a = linear(e.a, path);
@@ -61,14 +74,14 @@ export function linear(e, path) {
61
74
  return undefined;
62
75
  const aLeB = decide(sub(a, b), path), bLeA = decide(sub(b, a), path);
63
76
  if (e.op === 'sub')
64
- return bLeA === true ? sub(a, b) : bLeA === false ? k(0) : undefined;
77
+ return bLeA === true ? sub(a, b) : bLeA === false ? k(0) : v(termColumn(e));
65
78
  if (aLeB === undefined)
66
- return undefined;
79
+ return v(termColumn(e));
67
80
  return (e.op === 'min') === aLeB ? a : b;
68
81
  }
69
82
  case 'if': {
70
83
  const c = truth(e.c, path);
71
- return c === undefined ? undefined : linear(c ? e.then : e.else, path);
84
+ return c === undefined ? v(termColumn(e)) : linear(c ? e.then : e.else, path);
72
85
  }
73
86
  case 'get': {
74
87
  const key = linear(e.key, path);
@@ -76,6 +89,61 @@ export function linear(e, path) {
76
89
  }
77
90
  }
78
91
  }
92
+ // --- term columns -------------------------------------------------------------------------------------------------------
93
+ // A choice the path leaves open is a column ⟦n⟧ for the n-th term met (one table per process: the name only has to be
94
+ // the same for the same term). A leaf reads it through rows that hold whichever way the choice goes (termRows), so a
95
+ // fact about the choice's value can be split on, and proven, before the choice is decided.
96
+ // Terms are hash-consed: a term's id comes from its operator, its fields and its parts' ids, remembered per object, so a
97
+ // deep term (a k-step run nests k steps) is read once, not once per lookup.
98
+ const termIds = new Map(), termTable = [], memo = new WeakMap();
99
+ function termId(t) {
100
+ const had = memo.get(t);
101
+ if (had !== undefined)
102
+ return had;
103
+ const parts = Object.entries(t).sort(([x], [y]) => (x < y ? -1 : x > y ? 1 : 0)).map(([key, val]) => (Array.isArray(val) ? `${key}=[${val.map(x => termId(x)).join(',')}]` : typeof val === 'object' && val !== null ? `${key}=#${termId(val)}` : `${key}=${JSON.stringify(val)}`));
104
+ const key = parts.join(';');
105
+ let id = termIds.get(key);
106
+ if (id === undefined) {
107
+ id = termTable.length;
108
+ termIds.set(key, id);
109
+ termTable.push(t);
110
+ }
111
+ memo.set(t, id);
112
+ return id;
113
+ }
114
+ export const termColumn = (e) => `⟦${termId(e)}⟧`;
115
+ export const columnTerm = (name) => { const m = /^⟦(\d+)⟧$/.exec(name); return m ? termTable[Number(m[1])] : undefined; };
116
+ // Whether the path decides a choice's top (an if's condition, which side of min, max or ∸).
117
+ export function decided(e, path) {
118
+ if (e.op === 'if')
119
+ return truth(e.c, path) !== undefined;
120
+ if (e.op !== 'sub' && e.op !== 'min' && e.op !== 'max')
121
+ return true;
122
+ const a = linear(e.a, path), b = linear(e.b, path);
123
+ return !!a && !!b && decide(e.op === 'sub' ? sub(b, a) : sub(a, b), path) !== undefined;
124
+ }
125
+ // The rows a term column brings: its bounds, true whichever way the choice goes (min(a, b) ≤ a and ≤ b; max(a, b) ≥ a
126
+ // and ≥ b; a ∸ b ≥ a − b and ≤ a), and, once the path decides it, its equality to the branch taken.
127
+ export function termRows(name, path) {
128
+ const e = columnTerm(name), col = v(name), out = [];
129
+ if (e.op === 'min' || e.op === 'max' || e.op === 'sub') {
130
+ const a = linear(e.a, path), b = linear(e.b, path);
131
+ if (a && b) {
132
+ if (e.op === 'min')
133
+ out.push(sub(col, a), sub(col, b));
134
+ if (e.op === 'max')
135
+ out.push(sub(a, col), sub(b, col));
136
+ if (e.op === 'sub')
137
+ out.push(sub(sub(a, b), col), sub(col, a));
138
+ }
139
+ }
140
+ if (decided(e, path)) {
141
+ const r = linear(e, path);
142
+ if (r)
143
+ out.push(sub(col, r), sub(r, col));
144
+ }
145
+ return out;
146
+ }
79
147
  // Products: multiply out, each monomial (variables joined by '*', sorted) one more natural variable. Forgetting how
80
148
  // monomials relate (x*x to x, say) only weakens what a leaf may use, so it stays sound; distributing is what proves
81
149
  // spend <= m·count over a node from the same law at its children. Map reads can't be factors.
@@ -83,8 +151,8 @@ function times(a, b) {
83
151
  const out = [];
84
152
  for (const [p, cp] of Object.entries(a))
85
153
  for (const [q, cq] of Object.entries(b)) {
86
- if (p.includes('[') || q.includes('['))
87
- return undefined;
154
+ if (p.includes('[') || q.includes('[') || p.includes('⟦') || q.includes('⟦'))
155
+ return undefined; // factors are variables (and monomials of them)
88
156
  const factors = [...(p === '1' ? [] : p.split('*')), ...(q === '1' ? [] : q.split('*'))].sort();
89
157
  out.push({ [factors.length ? factors.join('*') : '1']: cp * cq });
90
158
  }
@@ -172,6 +240,23 @@ export function leafConstraints(goal, path) {
172
240
  if (decide(d, path) === true && decide(scale(d, -1), path) === true)
173
241
  rows.push({ lin: sub(v(p.name), v(q.name)), label: '' }, { lin: sub(v(q.name), v(p.name)), label: '' });
174
242
  }
243
+ // Each term column's rows, and the columns those rows bring in turn.
244
+ const seen = new Set(), todo = [...names].filter(n => n.startsWith('⟦'));
245
+ while (todo.length) {
246
+ const name = todo.pop();
247
+ if (seen.has(name))
248
+ continue;
249
+ seen.add(name);
250
+ for (const lin of termRows(name, path)) {
251
+ rows.push({ lin, label: '' });
252
+ for (const key of Object.keys(lin))
253
+ if (key !== '1') {
254
+ names.add(key);
255
+ if (key.startsWith('⟦') && !seen.has(key))
256
+ todo.push(key);
257
+ }
258
+ }
259
+ }
175
260
  for (const name of names)
176
261
  rows.push({ lin: { [name]: -1 }, label: '' });
177
262
  return rows;
@@ -300,6 +385,14 @@ const step = (r, u) => {
300
385
  flags: at.flags,
301
386
  } };
302
387
  };
388
+ // The state after a step, named: each state variable a fresh name n~j equal to its term there, which later frames read,
389
+ // so a run of k steps is k small frames rather than a term nesting each state in the next (which grows as 3^k when a
390
+ // step reads its state thrice). The names are new to the goal; each equation holds of the run that took the steps.
391
+ const nameState = (r, state, j) => ({
392
+ hyps: [...r.hyps, ...state.map(n => ({ op: '=', a: { op: 'var', name: stateName(n, j) }, b: r.at.vars[n] ?? { op: 'var', name: n } }))],
393
+ at: { ...r.at, vars: { ...r.at.vars, ...Object.fromEntries(state.map(n => [n, { op: 'var', name: stateName(n, j) }])) } },
394
+ });
395
+ export const stateName = (n, j) => `${n}~${j}`;
303
396
  // The conclusion read at the run's end: plain names at the last frame.
304
397
  const atEnd = (r, c) => substC(c, r.at);
305
398
  // The conclusion read across the run: plain names at the first frame, primed names (s', and M'[k] as a read of the map
@@ -623,8 +716,6 @@ export function historyMachine(m) {
623
716
  const h = m.history;
624
717
  if (!h)
625
718
  return { machine: m, goals: [] };
626
- if (m.inputMaps?.length || quantified(m).length || Object.values(m.step.maps ?? {}).some(w => 'each' in w))
627
- throw new Error(`${m.name}: a machine with a history has no maps carried by requests, quantified flags or writes at every key`);
628
719
  const queries = Object.keys(h.queries), sums = Object.keys(h.sums), plainInputs = m.inputs.filter(i => !queries.includes(i));
629
720
  const fields = h.record.map(f => `r.${f}`);
630
721
  const taken = [...m.params, ...m.inputs, ...Object.keys(m.state), ...Object.keys(m.step.state ?? {})];
@@ -641,12 +732,15 @@ export function historyMachine(m) {
641
732
  if (h.prune && !(h.record.includes(h.prune.order) && natural(h.prune.after) && readsOnly(h.prune.after, m.params)))
642
733
  throw new Error('a history is pruned by a recorded input and a term over the params');
643
734
  const atRequest = { vars: Object.fromEntries(h.record.map(f => [`r.${f}`, { op: 'var', name: f }])), maps: {}, flags: {} };
644
- const relations = (h.relations ?? []).map(r => ({ op: 'or', xs: [{ op: 'not', x: r.when }, r.holds] }));
735
+ // The relations conjoined to the assumption as the Lean twin builds them (History.lean's HMachine.plain: assume ∧
736
+ // r₁ ∧ … ∧ true), so the two read the same assumption.
737
+ const TRUE = { op: '<=', a: { op: 'const', value: 0 }, b: { op: 'const', value: 0 } };
738
+ const relations = (h.relations ?? []).map(r => ({ op: 'or', xs: [{ op: 'not', x: r.when }, r.holds] })).reduceRight((acc, r) => ({ op: 'and', xs: [r, acc] }), TRUE);
645
739
  const machine = {
646
740
  ...m, history: undefined,
647
741
  state: { ...m.state, ...Object.fromEntries(sums.map(s => [s, { op: 'const', value: 0 }])) },
648
742
  step: { ...m.step, state: { ...m.step.state, ...Object.fromEntries(sums.map(s => [s, { op: 'add', a: { op: 'var', name: s }, b: { op: 'if', c: h.keep, then: substE(h.sums[s], atRequest), else: { op: 'const', value: 0 } } }])) } },
649
- ...(m.assume || relations.length ? { assume: { op: 'and', xs: [...(m.assume ? [m.assume] : []), ...relations] } } : {}),
743
+ assume: { op: 'and', xs: [m.assume ?? TRUE, relations] },
650
744
  };
651
745
  const perRecord = { vars: { ...h.sums, ...h.queries }, maps: {}, flags: {} };
652
746
  const goals = (h.relations ?? []).map((r, i) => ({ name: `relation ${i + 1} @record`, hyps: [...preOf(m), r.when, { op: 'not', x: substC(r.holds, perRecord) }] }));
@@ -688,10 +782,20 @@ export function obligations(m, laws, instances = {}) {
688
782
  export function checkLaws(m, laws, proof) {
689
783
  return obligations(m, laws, proof.instances).every(g => proof.trees[g.name] !== undefined && check(g, proof.trees[g.name]));
690
784
  }
785
+ // A loop is a machine: its state, a turn's inputs as a request, its body taken where `while` holds and the state kept
786
+ // where it fails. Its invariants are the machine's laws, and that it halts is a liveness: from anywhere, `while` fails
787
+ // later, every request helpful, the rank the loop's (Live.lean's Loop.machine and loop_halts_of: then no run of turns is
788
+ // infinite). `while` and the rank read the state and params, not the turn's input.
691
789
  // A rank may be a tuple, compared in order (lexicographically): the first part drops, or it stays and the rest drops.
692
790
  // Tuples of naturals can't drop forever either; nested loops need them (an outer count, then an inner one).
693
791
  // A rank reads the state and params only: one that reads the turn's input drops against this turn's input, but the next
694
792
  // turn measures it with a fresh one, so nothing need decrease (while x < w, x := x + 1, rank w ∸ x, w = x + 1 each turn).
793
+ export const loopMachine = (l) => ({
794
+ name: l.name, params: l.params, inputs: l.inputs, state: l.state,
795
+ step: { state: Object.fromEntries(Object.entries(l.body).map(([v, e]) => [v, { op: 'if', c: l.while, then: e, else: { op: 'var', name: v } }])) },
796
+ ...(l.pre ? { pre: l.pre } : {}), ...(l.assume ? { assume: l.assume } : {}),
797
+ });
798
+ export const loopLiveness = (l, rank) => ({ name: `${l.name} halts`, from: trueC, to: { op: 'not', x: l.while }, rank, request: {} });
695
799
  export function loopObligations(l, laws, rank) {
696
800
  const ranks = Array.isArray(rank) ? rank : [rank];
697
801
  const parts = [...Object.values(l.state), ...Object.values(l.body), ...ranks];
@@ -701,26 +805,17 @@ export function loopObligations(l, laws, rank) {
701
805
  if (!(name in l.state))
702
806
  throw new Error(`${l.name}'s body sets ${name}, which isn't in its state`);
703
807
  inScope(`${l.name}'s start`, Object.values(l.state), l.params);
704
- inScope(l.name, [...Object.values(l.body), l.while, ...(l.assume ? [l.assume] : [])], names);
808
+ inScope(l.name, [...Object.values(l.body), ...(l.assume ? [l.assume] : [])], names);
809
+ inScope(`${l.name}'s while`, [l.while], [...l.params, ...Object.keys(l.state)]);
705
810
  inScope(`${l.name}'s precondition`, l.pre ? [l.pre] : [], l.params);
706
811
  inScope(`${l.name}'s rank and laws`, [...ranks, ...laws.map(law => law.holds)], [...l.params, ...Object.keys(l.state)]);
707
812
  if (ranks.some(r => mentions({ op: '=', a: r, b: r }, l.inputs)))
708
813
  throw new Error('a rank reads the state and params, not the turn\'s input');
709
814
  if (!parts.every(natural) || !naturalC(l.while) || !laws.every(law => !law.forall && !law.keys?.length && naturalC(law.holds) && !mentions(law.holds, l.inputs)))
710
815
  throw new Error('outside the kernel');
711
- const pre = preOf(l);
712
- const start = holds(from({ vars: { ...l.state }, maps: {}, flags: {} }), pre);
713
- const turn = step(holds(from(), [...pre, ...(l.assume ? [l.assume] : []), l.while, ...laws.map(law => law.holds)]), { vars: l.body, maps: {} });
714
- return [
715
- ...laws.flatMap(law => [goal(`${law.name} @start`, start, atEnd(start, law.holds)), goal(`${law.name} @turn`, turn, atEnd(turn, law.holds))]),
716
- goal('rank drops', turn, drops(ranks, turn.at)),
717
- ];
816
+ const m = loopMachine(l), { stay, cover, helps } = livenessParts(m, loopLiveness(l, rank));
817
+ return [...obligations(m, laws, {}), ...[stay, cover, ...helps].map(sl => stepLawObligations(m, laws, {}, sl)[0])];
718
818
  }
719
- const drops = (ranks, turn) => {
720
- const [r, ...rest] = ranks;
721
- const after = substE(r, turn), less = { op: '<', a: after, b: r };
722
- return rest.length === 0 ? less : { op: 'or', xs: [less, { op: 'and', xs: [{ op: '=', a: after, b: r }, drops(rest, turn)] }] };
723
- };
724
819
  // --- progress: laws with "there is a request" ----------------------------------------------------------------------
725
820
  // "In every reachable state where `when` holds, some request makes `then` true of the step": proven by naming the
726
821
  // request (a term per input over the state and params) and proving it allowed (the machine's assumption holds of it)
@@ -799,8 +894,9 @@ export function stepLawObligations(machine, laws, instances, sl) {
799
894
  const primes = [...state, ...maps].map(n => `${n}'`);
800
895
  const laters = Array.from({ length: k - 1 }, (_, i) => laterNames(m, i + 2));
801
896
  const later = laters.flatMap(l => Object.values(l.vars).map(e => e.name)), laterMaps = laters.flatMap(l => Object.values(l.maps));
897
+ const names = Array.from({ length: k - 1 }, (_, i) => state.map(n => stateName(n, i + 1))).flat();
802
898
  const taken = [...m.params, ...m.inputs, ...state, ...maps, ...carried];
803
- for (const p of [...primes, ...later, ...laterMaps])
899
+ for (const p of [...primes, ...later, ...laterMaps, ...names])
804
900
  if (taken.includes(p))
805
901
  throw new Error(`${m.name}: ${p} is a name of the machine and a name a step law reads`);
806
902
  for (const key of [sl.forall, ...(sl.keys ?? [])])
@@ -822,14 +918,30 @@ export function stepLawObligations(machine, laws, instances, sl) {
822
918
  if (j > 1)
823
919
  run = reading(run, laters[j - 2].vars, laters[j - 2].maps);
824
920
  run = step(holds(run, [...assumed(laws, instances), ...assume, ...(whens[j - 1] ? [inlineFlags(m, whens[j - 1])] : []), ...closing(m, instances)]), u);
921
+ if (j < k)
922
+ run = nameState(run, state, j);
825
923
  }
826
924
  return [goal(k === 1 ? `${sl.name} @step law` : `${sl.name} @${k} steps`, run, across(run, sl.then, state, maps, { inputs: {}, flags: flagsOf(m) }))];
827
925
  }
828
926
  // The laws' proof and every step law's tree: all must check.
829
927
  // `perStep`: a step law's own instances, where it has them (else `instances`).
928
+ // Each frame's hypotheses read no name a later step's request is read by: the condition the Lean twin's proof needs
929
+ // (Sets.lean's checkSKStep, `avoids (szone s sl)`; plain flags are inlined here, so their names never occur), checked
930
+ // here alike.
830
931
  export function checkStepLaws(m, laws, stepLaws, proof) {
831
- return checkLaws(m, laws, proof.laws) && stepLaws.every(sl => stepLawObligations(m, laws, proof.perStep?.[sl.name] ?? proof.instances, sl).every(g => proof.trees[g.name] !== undefined && check(g, proof.trees[g.name])));
932
+ return checkLaws(m, laws, proof.laws) && stepLaws.every(sl => {
933
+ const inst = proof.perStep?.[sl.name] ?? proof.instances, plain = m.history ? historyMachine(m).machine : m;
934
+ const zone = stepZone(plain, sl.steps ?? 1);
935
+ return [...assumed(laws, inst), ...assumed(plain.facts ?? [], inst), ...closing(plain, inst)].every(c => !mentions(c, zone))
936
+ && stepLawObligations(m, laws, inst, sl).every(g => proof.trees[g.name] !== undefined && check(g, proof.trees[g.name]));
937
+ });
832
938
  }
939
+ // The names a k-step law's run introduces past its first frame: each later step's inputs, quantified flags and their
940
+ // witnesses, and the state after each step but the last (stateName).
941
+ export const stepZone = (m, k) => [
942
+ ...Array.from({ length: k - 1 }, (_, i) => Object.values(laterNames(m, i + 2).vars).map(e => e.name)).flat(),
943
+ ...Array.from({ length: k - 1 }, (_, i) => Object.keys(m.state).map(n => stateName(n, i + 1))).flat(),
944
+ ];
833
945
  export function foldObligations(f, laws) {
834
946
  if (![...Object.values(f.leaf), ...Object.values(f.node)].every(natural) || !laws.every(law => !law.forall && !law.keys?.length && naturalC(law.holds) && !mentions(law.holds, f.inputs)))
835
947
  throw new Error('outside the kernel');
@@ -1,5 +1,5 @@
1
- import { add, k, scale, solve, sub } from "../core/solver.js";
2
- import { assumed, canon, check, checkLaws, checkStepLaws, historyMachine, leafConstraints, linear, livenessHelpful, livenessParts, obligations, plainKey, progressObligations, stepLawObligations, trueC, truth, unreachable, withProducts } from "./kernel.js";
1
+ import { add, k, scale, solve, sub, v } from "../core/solver.js";
2
+ import { assumed, canon, columnTerm, decided, stepZone, check, checkLaws, checkStepLaws, historyMachine, leafConstraints, linear, livenessHelpful, livenessParts, obligations, plainKey, progressObligations, stepLawObligations, trueC, truth, unreachable, withProducts } from "./kernel.js";
3
3
  // The atoms the path leaves open, in evaluation order: deciding any of them makes progress.
4
4
  function openE(e, path, out) {
5
5
  switch (e.op) {
@@ -22,7 +22,7 @@ function openE(e, path, out) {
22
22
  case 'max': {
23
23
  openE(e.a, path, out), openE(e.b, path, out);
24
24
  const a = linear(e.a, path), b = linear(e.b, path);
25
- if (a && b && linear(e, path) === undefined)
25
+ if (a && b && !decided(e, path))
26
26
  out.push(e.op === 'sub' ? sub(b, a) : sub(a, b));
27
27
  return;
28
28
  }
@@ -116,12 +116,34 @@ function parse(text) {
116
116
  // The budget: 200,000 nodes and, by default, a second per hypothesis (at least 20 seconds) per goal; past either the goal
117
117
  // is left `unknown`. A goal with many instances (a law about two keys at its pairs) has that many more rows per leaf.
118
118
  export function grow(goal, path = new Map(), budget = { nodes: 0, until: performance.now() + Math.max(20_000, 1_000 * goal.hyps.length) }) {
119
+ // At the root: plainly first; where the cases multiply past a budget, through cuts. The plain budget halves with each
120
+ // two open choices (what a plain search's cases grow by), so a long run's steps reach the cuts early.
121
+ if (path.size === 0 && budget.nodes === 0) {
122
+ const open = new Set(leafConstraints(goal, path).flatMap(r => Object.keys(r.lin)).filter(n => n.startsWith('⟦') && columnTerm(n)?.op === 'if')).size;
123
+ const plain = growPlain(goal, path, { nodes: 0, until: budget.until, cap: Math.max(32, Math.round(4_000 / 2 ** (open / 2))) });
124
+ if ('tree' in plain || plain.stuck === 'sat' || plain.why === undefined)
125
+ return plain;
126
+ const cut = withCuts(goal, path, budget);
127
+ if (cut)
128
+ return cut;
129
+ }
130
+ return growPlain(goal, path, budget);
131
+ }
132
+ function growPlain(goal, path, budget) {
133
+ if (budget.cap !== undefined && budget.nodes >= budget.cap)
134
+ return { stuck: 'unknown', path, why: 'cap' };
119
135
  if (++budget.nodes > 200_000 || (budget.until !== undefined && performance.now() > budget.until))
120
136
  return { stuck: 'unknown', path, why: outOfBudget(goal, budget.nodes > 200_000 ? 'nodes' : 'time', path.size) };
121
- const rows = leafConstraints(goal, path);
137
+ // Rows that read a term column only once a cut has put one on the path; before, the rows a choice-free search uses
138
+ // (what it saw before term columns), renumbered to the whole leaf's when a leaf is found.
139
+ const full = leafConstraints(goal, path);
140
+ const columns = [...path.values()].some(a => Object.keys(a.lin).some(key => key.startsWith('⟦')));
141
+ const idx = full.flatMap((r, i) => (columns || !Object.keys(r.lin).some(key => key.startsWith('⟦')) ? [i] : []));
142
+ const rows = idx.map(i => full[i]), at = (i) => (i < rows.length ? idx[i] : full.length + i - rows.length);
143
+ const leafOf = (steps, products) => ({ leaf: steps.map(st => (st.kind === 'input' ? { ...st, index: at(st.index) } : st)), ...(products ? { products: products.map(([i, j]) => [at(i), at(j)]) } : {}) });
122
144
  const verdict = solve(rows);
123
145
  if (verdict.kind === 'refuted')
124
- return { tree: { leaf: verdict.steps }, nodes: budget.nodes };
146
+ return { tree: leafOf(verdict.steps), nodes: budget.nodes };
125
147
  const open = [];
126
148
  for (const h of goal.hyps)
127
149
  openH(h, path, open);
@@ -148,7 +170,7 @@ export function grow(goal, path = new Map(), budget = { nodes: 0, until: perform
148
170
  pairs.splice(300);
149
171
  const product = solve(withProducts(rows, pairs));
150
172
  if (product.kind === 'refuted')
151
- return { tree: { leaf: product.steps, products: pairs }, nodes: budget.nodes };
173
+ return { tree: leafOf(product.steps, pairs), nodes: budget.nodes };
152
174
  // Degree three: each row times each product of two variables (x·y >= 0 is the pair of -x <= 0 and -y <= 0).
153
175
  const degree = Math.max(...rows.flatMap(r => Object.keys(r.lin).map(key => (key === '1' ? 0 : key.split('*').length))));
154
176
  if (degree >= 3) {
@@ -159,7 +181,7 @@ export function grow(goal, path = new Map(), budget = { nodes: 0, until: perform
159
181
  cubes.push([rows.length + s, i]);
160
182
  const third = solve(withProducts(rows, [...sq, ...cubes]));
161
183
  if (third.kind === 'refuted')
162
- return { tree: { leaf: third.steps, products: [...sq, ...cubes] }, nodes: budget.nodes };
184
+ return { tree: leafOf(third.steps, [...sq, ...cubes]), nodes: budget.nodes };
163
185
  }
164
186
  const factors = [...new Set(rows.flatMap(r => Object.keys(r.lin).filter(key => key.includes('*')).flatMap(key => key.split('*'))))];
165
187
  const fresh = (lin) => !path.has(canon(lin)) && !path.has(canon(add(k(1), scale(lin, -1))));
@@ -174,10 +196,10 @@ export function grow(goal, path = new Map(), budget = { nodes: 0, until: perform
174
196
  if (!split)
175
197
  return { stuck: verdict.kind === 'sat' ? 'sat' : 'unknown', ...(verdict.kind === 'sat' ? { witness: verdict.witness } : {}), path };
176
198
  const id = canon(split);
177
- const yes = grow(goal, new Map(path).set(id, { lin: split, holds: true }), budget);
199
+ const yes = growPlain(goal, new Map(path).set(id, { lin: split, holds: true }), budget);
178
200
  if (!('tree' in yes))
179
201
  return yes;
180
- const no = grow(goal, new Map(path).set(id, { lin: split, holds: false }), budget);
202
+ const no = growPlain(goal, new Map(path).set(id, { lin: split, holds: false }), budget);
181
203
  if (!('tree' in no))
182
204
  return no;
183
205
  return { tree: { split, yes: yes.tree, no: no.tree }, nodes: budget.nodes };
@@ -189,6 +211,90 @@ function outOfBudget(goal, limit, depth) {
189
211
  `Each case a step decides multiplies the cases to refute, so ${steps > 1 ? `a law over ${steps} steps` : 'many interacting cases'} can outgrow it: ` +
190
212
  'state the law over fewer steps (a one-step law and an invariant often give the same), or split it into laws about fewer conditions';
191
213
  }
214
+ // Cuts: where each of many choices (a step's case, over a run of k steps) multiplies the cases, a fact about each
215
+ // choice's value that holds whichever way it goes is split on first: its failing side is refuted by deciding that
216
+ // choice alone, and the rest of the proof assumes it, so the tree is a chain of small refutations instead of every
217
+ // combination. For a term column t = if c then A else B the candidates are t ≤ A, t ≤ B, t ≥ A and t ≥ B, innermost
218
+ // choices first; a cut is only a split, so the kernel checks it like any other.
219
+ function withCuts(goal, path, budget) {
220
+ const rows = leafConstraints(goal, path);
221
+ const columns = [...new Set(rows.flatMap(r => Object.keys(r.lin)).filter(n => n.startsWith('⟦')))]
222
+ .map(n => ({ n, e: columnTerm(n) })).filter(x => x.e.op === 'if' && !decided(x.e, path))
223
+ .sort((a, b) => Number(a.n.slice(1, -1)) - Number(b.n.slice(1, -1))); // a term's parts get their ids before it: innermost first
224
+ const chain = [];
225
+ let at = new Map(path);
226
+ for (const { n, e } of columns) {
227
+ if (e.op !== 'if')
228
+ continue;
229
+ const a = linear(e.then, at), b = linear(e.else, at), focus = topAtoms(e.c, at);
230
+ if (!a || !b || !focus.length)
231
+ continue;
232
+ for (const lemma of [sub(v(n), a), sub(v(n), b), sub(a, v(n)), sub(b, v(n))]) {
233
+ const id = canon(lemma);
234
+ if (at.has(id) || Object.keys(lemma).every(key => key === '1'))
235
+ continue;
236
+ const no = growFocused(goal, new Map(at).set(id, { lin: lemma, holds: false }), focus, { nodes: 0 });
237
+ if ('tree' in no) {
238
+ chain.push({ split: lemma, no: no.tree });
239
+ at = new Map(at).set(id, { lin: lemma, holds: true });
240
+ }
241
+ }
242
+ }
243
+ if (!chain.length)
244
+ return undefined;
245
+ const rest = growPlain(goal, at, budget);
246
+ if (!('tree' in rest))
247
+ return rest;
248
+ return { tree: chain.reduceRight((acc, c) => ({ split: c.split, yes: acc, no: c.no }), rest.tree), nodes: budget.nodes };
249
+ }
250
+ // A condition's own comparisons, each a split atom, read with term columns for whatever choices are inside it.
251
+ function topAtoms(c, path) {
252
+ if (c.op === 'not')
253
+ return topAtoms(c.x, path);
254
+ if (c.op === 'and' || c.op === 'or')
255
+ return c.xs.flatMap(y => topAtoms(y, path));
256
+ if (c.op === 'flag' || c.op === 'all')
257
+ return [];
258
+ const a = linear(c.a, path), b = linear(c.b, path);
259
+ if (!a || !b)
260
+ return [];
261
+ const atoms = c.op === '<=' ? [sub(a, b)] : c.op === '<' ? [add(sub(a, b), k(1))] : [sub(a, b), sub(b, a)];
262
+ return atoms.filter(l => Object.keys(l).some(key => key !== '1') && !path.has(canon(l)) && !path.has(canon(add(k(1), scale(l, -1)))));
263
+ }
264
+ // A refutation that splits only on these atoms (a cut's failing side), within a small budget.
265
+ function growFocused(goal, path, focus, budget, near = focus) {
266
+ if (++budget.nodes > 64)
267
+ return { stuck: 'unknown', path };
268
+ const verdict = solveNear(leafConstraints(goal, path), [...near, ...[...path.values()].slice(-1).map(a => a.lin)]);
269
+ if (verdict.kind === 'refuted')
270
+ return { tree: { leaf: verdict.steps }, nodes: budget.nodes };
271
+ const split = focus.find(l => !path.has(canon(l)) && !path.has(canon(add(k(1), scale(l, -1)))));
272
+ if (!split)
273
+ return { stuck: verdict.kind === 'sat' ? 'sat' : 'unknown', path };
274
+ const id = canon(split);
275
+ const yes = growFocused(goal, new Map(path).set(id, { lin: split, holds: true }), focus, budget, near);
276
+ if (!('tree' in yes))
277
+ return yes;
278
+ const no = growFocused(goal, new Map(path).set(id, { lin: split, holds: false }), focus, budget, near);
279
+ if (!('tree' in no))
280
+ return no;
281
+ return { tree: { split, yes: yes.tree, no: no.tree }, nodes: budget.nodes };
282
+ }
283
+ // A refutation from the rows near some facts (the rows reachable from their variables in two hops, each row joining the
284
+ // variables it reads), its inputs renumbered to the whole leaf's. A cut about one step of a long run needs that step's
285
+ // rows, not the run's; a cut it misses is only a cut not made (the plain search still follows).
286
+ function solveNear(rows, seeds) {
287
+ const reach = new Set(seeds.flatMap(l => Object.keys(l).filter(key => key !== '1')));
288
+ for (let hop = 0; hop < 2; hop++)
289
+ for (const r of rows) {
290
+ const keys = Object.keys(r.lin).filter(key => key !== '1');
291
+ if (keys.some(key => reach.has(key)))
292
+ keys.forEach(key => reach.add(key));
293
+ }
294
+ const picked = rows.flatMap((r, i) => (Object.keys(r.lin).every(key => key === '1' || reach.has(key)) ? [i] : []));
295
+ const near = solve(picked.map(i => rows[i]));
296
+ return near.kind === 'refuted' ? { kind: 'refuted', steps: near.steps.map(st => (st.kind === 'input' ? { ...st, index: picked[st.index] } : st)) } : near;
297
+ }
192
298
  // Integer completeness for bounded systems: when elimination is inexact and finds no witness, halve a variable's range
193
299
  // (x <= mid or x >= mid + 1). Each half is a split like any other, so the kernel checks it the same way; a variable
194
300
  // pinned to one value becomes an equality, which the solver substitutes exactly.
@@ -370,6 +476,42 @@ function match(pattern, term, x) {
370
476
  const witnesses = (m) => Object.entries(m.step.flags ?? {}).flatMap(([f, c]) => (c.op === 'all' ? [{ op: 'var', name: `w?${f}` }] : []));
371
477
  // The keys each quantified flag is assumed at (instances under ?f).
372
478
  const flagInstances = (m, keys) => Object.fromEntries(Object.entries(m.step.flags ?? {}).flatMap(([f, c]) => (c.op === 'all' ? [[`?${f}`, keys]] : [])));
479
+ // The instances some goals need, for every entry point: the every-key laws and the quantified flags at `seed` (the keys
480
+ // the step and the laws read, when known before any goal is built), then twice round: the facts at every key the goals
481
+ // built so far read a function at, and, where the seed doesn't cover them (no seed, positions that shift, a law about
482
+ // several keys), the every-key laws and flags at every key they read a map at (a law about several keys at tuples
483
+ // relating `focus`, the goals' own keys).
484
+ function instancesFor(m, laws, build, opts = {}) {
485
+ let inst = opts.start ?? (opts.seed ? { ...Object.fromEntries(laws.filter(l => l.forall).map(l => [l.name, l.keys?.length ? [] : opts.seed])), ...flagInstances(m, opts.seed) } : {});
486
+ const reads = opts.reads ?? (!opts.seed || shifts(m) || laws.some(l => l.keys?.length));
487
+ for (let round = 0; round < 2; round++) {
488
+ if (m.facts?.length)
489
+ inst = withFacts(m, inst, fnKeys(m, build(inst)));
490
+ if (reads) {
491
+ const keys = mapKeys(build(inst));
492
+ inst = union(withReads(laws, inst, keys, opts.focus ?? []), flagInstances(m, keys));
493
+ }
494
+ }
495
+ return inst;
496
+ }
497
+ // Instances without the keys that read any of these names (a law about several keys loses each tuple that does).
498
+ const avoiding = (inst, names, laws) => {
499
+ const bad = (e) => names.some(n => JSON.stringify(e).includes(`"name":${JSON.stringify(n)}`));
500
+ return Object.fromEntries(Object.entries(inst).map(([n, keys]) => {
501
+ const arity = (laws.find(l => l.name === n)?.keys?.length ?? 0) + 1;
502
+ const tuples = Array.from({ length: Math.floor(keys.length / arity) }, (_, t) => keys.slice(t * arity, (t + 1) * arity));
503
+ return [n, tuples.filter(tuple => !tuple.some(bad)).flat()];
504
+ }));
505
+ };
506
+ // Two sets of instances together: each name at the keys of both.
507
+ const union = (a, b) => {
508
+ const out = { ...a };
509
+ for (const [n, keys] of Object.entries(b)) {
510
+ const seen = new Set((out[n] ?? []).map(e => JSON.stringify(e)));
511
+ out[n] = [...(out[n] ?? []), ...keys.filter(e => !seen.has(JSON.stringify(e)))];
512
+ }
513
+ return out;
514
+ };
373
515
  const someKeys = (c, positive = true) => c.op === 'all' ? [...(positive ? [] : [c]), ...someKeys(c.c, positive)] : c.op === 'not' ? someKeys(c.x, !positive) : c.op === 'and' || c.op === 'or' ? c.xs.flatMap(y => someKeys(y, positive)) : [];
374
516
  function atWitness(c, picks, positive = true) {
375
517
  switch (c.op) {
@@ -434,23 +576,14 @@ export function proveLaws(m, laws) {
434
576
  return { ...proveLawsAsGiven(m, out.items), laws: out.items };
435
577
  }
436
578
  function proveLawsAsGiven(m, laws) {
437
- const keys = [...readKeys(m, laws), ...witnesses(m)];
438
- const instances = {};
439
- for (const law of laws) {
440
- const at = [...keys, ...(law.forall ? [{ op: 'var', name: `${law.forall}#` }] : []), ...(law.keys ?? []).map((name) => ({ op: 'var', name }))];
441
- instances[law.name] = { ...Object.fromEntries(laws.filter(l => l.forall).map(l => [l.name, l.keys?.length ? [] : at])), ...flagInstances(m, at) };
442
- }
443
- // The facts about functions, at every key each law's goals read a function at (found from the goals, twice round).
444
- for (let round = 0; round < 2 && m.facts?.length; round++) {
445
- const goals = obligations(m, laws, instances);
446
- laws.forEach((law, i) => { instances[law.name] = withFacts(m, instances[law.name], fnKeys(m, goals.slice(2 * i, 2 * i + 2))); });
447
- }
448
- // Where positions shift (a write at every key reading its key at k ∸ 1 or k + 1) or a law is about two keys, the
449
- // every-key laws also at each key the goals read a map at, once round.
450
- if (shifts(m) || laws.some(l => l.keys?.length)) {
451
- const goals = obligations(m, laws, instances);
452
- laws.forEach((law, i) => { instances[law.name] = withReads(laws, instances[law.name], mapKeys(goals.slice(2 * i, 2 * i + 2)), [...(law.forall ? [`${law.forall}#`] : []), ...(law.keys ?? [])]); });
453
- }
579
+ // Each law's two goals (start and step) get their own instances, seeded with its own keys.
580
+ const keys = [...readKeys(m, laws), ...witnesses(m)], instances = {};
581
+ laws.forEach((law, i) => {
582
+ instances[law.name] = instancesFor(m, laws, inst => obligations(m, laws, { ...instances, [law.name]: inst }).slice(2 * i, 2 * i + 2), {
583
+ seed: [...keys, ...(law.forall ? [{ op: 'var', name: `${law.forall}#` }] : []), ...(law.keys ?? []).map((name) => ({ op: 'var', name }))],
584
+ focus: [...(law.forall ? [`${law.forall}#`] : []), ...(law.keys ?? [])],
585
+ });
586
+ });
454
587
  const trees = {};
455
588
  let nodes = 0, leaves = 0;
456
589
  for (const goal of obligations(m, laws, instances)) {
@@ -483,16 +616,19 @@ function proveStepLawsAsGiven(m, laws, stepLaws, base) {
483
616
  if (!base.proof)
484
617
  return { proven: false, nodes: base.nodes, leaves: base.leaves, checked: false };
485
618
  const plain = m.history ? historyMachine(m).machine : m;
486
- const keys = [...readKeys(plain, laws), ...witnesses(plain), ...stepLaws.flatMap(sl => [sl.then, ...(sl.when === undefined ? [] : Array.isArray(sl.when) ? sl.when : [sl.when])].flatMap(readKeysC)),
487
- ...stepLaws.flatMap(sl => [sl.forall, ...(sl.keys ?? [])].flatMap(key => (key ? [{ op: 'var', name: key }] : [])))];
488
- let instances = { ...Object.fromEntries(laws.filter(l => l.forall).map(l => [l.name, l.keys?.length ? [] : keys])), ...flagInstances(plain, keys) };
489
- for (let round = 0; round < 2 && plain.facts?.length; round++)
490
- instances = withFacts(plain, instances, fnKeys(plain, stepLaws.flatMap(sl => stepLawObligations(m, laws, instances, sl))));
491
- // Where positions shift or a law is about two keys, each step law gets its own instances, from its own goal.
619
+ // Each step law its own instances, from its own goal: the keys the step and the laws read, and its own (a k-step
620
+ // law's later frames read keys no other step law has); where positions shift or a law is about several keys, the
621
+ // keys its goal reads a map at too. A frame's hypotheses read no name the run introduces later (kernel.ts's
622
+ // stepZone), so a k-step law drops the keys that read one.
623
+ const shared = [...readKeys(plain, laws), ...witnesses(plain)], named = [...laws, ...(plain.facts ?? [])], reads = shifts(plain) || laws.some(l => l.keys?.length);
624
+ const own = (sl) => [...[sl.then, ...(sl.when === undefined ? [] : Array.isArray(sl.when) ? sl.when : [sl.when])].flatMap(readKeysC),
625
+ ...[sl.forall, ...(sl.keys ?? [])].flatMap(key => (key ? [{ op: 'var', name: key }] : []))];
492
626
  const perStep = {};
493
- if (shifts(plain) || laws.some(l => l.keys?.length))
494
- for (const sl of stepLaws)
495
- perStep[sl.name] = withReads(laws, instances, mapKeys(stepLawObligations(m, laws, instances, sl)), [sl.forall, ...(sl.keys ?? [])].flatMap(k => (k ? [k] : [])));
627
+ for (const sl of stepLaws) {
628
+ const zone = stepZone(plain, sl.steps ?? 1), keep = (inst) => avoiding(inst, zone, named);
629
+ perStep[sl.name] = keep(instancesFor(plain, laws, inst => stepLawObligations(m, laws, keep(inst), sl), { seed: [...shared, ...own(sl)], focus: [sl.forall, ...(sl.keys ?? [])].flatMap(k => (k ? [k] : [])), reads }));
630
+ }
631
+ const instances = perStep[stepLaws[0]?.name ?? ''] ?? {};
496
632
  const trees = {};
497
633
  let nodes = base.nodes, leaves = base.leaves;
498
634
  for (const sl of stepLaws)
@@ -514,10 +650,7 @@ export function proveProgress(m, laws, p) {
514
650
  if (!base.proven || !base.checked)
515
651
  return { proven: false, nodes: base.nodes, leaves: base.leaves, ...(base.failed ? { failed: base.failed } : {}), checked: false };
516
652
  const ls = base.laws ?? laws;
517
- let instances = { ...withReads(ls, {}, mapKeys(progressObligations(m, ls, {}, p)), []), ...flagInstances(m, mapKeys(progressObligations(m, ls, {}, p))) };
518
- instances = { ...instances, ...withReads(ls, instances, mapKeys(progressObligations(m, ls, instances, p)), []) };
519
- if (m.facts?.length)
520
- instances = withFacts(m, instances, fnKeys(m, progressObligations(m, ls, instances, p)));
653
+ const instances = instancesFor(m, ls, inst => progressObligations(m, ls, inst, p));
521
654
  const trees = {};
522
655
  let nodes = base.nodes, leaves = base.leaves;
523
656
  for (const goal of progressObligations(m, ls, instances, p)) {
@@ -614,21 +747,12 @@ export function concretize(goal, bound = 12) {
614
747
  // The facts a goal needs, as hypotheses: each fact about every key at the keys the goal reads a function at (and at
615
748
  // the keys those facts read, a second round), each plain fact as it is.
616
749
  export function factsFor(m, goal) {
617
- let instances = {};
618
- for (let round = 0; round < 2; round++)
619
- instances = withFacts(m, instances, fnKeys(m, [{ ...goal, hyps: [...assumed(m.facts ?? [], instances), ...goal.hyps] }]));
620
- return assumed(m.facts ?? [], instances);
750
+ return assumed(m.facts ?? [], instancesFor(m, [], inst => [{ ...goal, hyps: [...assumed(m.facts ?? [], inst), ...goal.hyps] }]));
621
751
  }
622
752
  // A goal built from its instances (an exporter's goal, shaped as its Lean twin builds it): the every-key laws, the
623
753
  // quantified flags and the facts assumed at every key it reads a map or function at, then a tree, checked.
624
754
  export function proveBuilt(m, laws, build) {
625
- let instances = {};
626
- for (let round = 0; round < 2; round++) {
627
- const g = build(instances), keys = mapKeys([g]);
628
- instances = { ...withReads(laws, instances, keys, []), ...flagInstances(m, keys) };
629
- if (m.facts?.length)
630
- instances = withFacts(m, instances, fnKeys(m, [build(instances)]));
631
- }
755
+ const instances = instancesFor(m, laws, inst => [build(inst)]);
632
756
  const goal = build(instances), out = grow(goal);
633
757
  if (!('tree' in out) || !check(goal, out.tree))
634
758
  return { proven: false };
@@ -59,7 +59,7 @@ export declare function mmachineToLean(id: string, m: Machine, laws: Law[], opts
59
59
  stepLaws?: StepLaw[];
60
60
  }): string;
61
61
  export declare const hasSets: (m: Machine) => boolean;
62
- export declare function smachineToLean(id: string, m: Machine, laws: Law[], opts?: {
62
+ export declare function smachineToLean(id: string, given: Machine, laws: Law[], opts?: {
63
63
  stepLaws?: StepLaw[];
64
64
  liveness?: Liveness[];
65
65
  tamper?: {
@@ -70,6 +70,14 @@ export declare function smachineToLean(id: string, m: Machine, laws: Law[], opts
70
70
  free?: string[];
71
71
  ghosts?: string[];
72
72
  after?: (ix: (name: string) => number) => string;
73
+ historyTampers?: {
74
+ what: string;
75
+ relations: {
76
+ when: C;
77
+ holds: C;
78
+ }[];
79
+ withRecordTrees: boolean;
80
+ }[];
73
81
  refined?: {
74
82
  impl: Machine;
75
83
  spec: Machine;
@@ -77,8 +85,15 @@ export declare function smachineToLean(id: string, m: Machine, laws: Law[], opts
77
85
  stutter: C;
78
86
  specPre: number;
79
87
  };
88
+ loop?: {
89
+ loop: Loop;
90
+ tamper?: {
91
+ what: string;
92
+ loop: Loop;
93
+ };
94
+ };
80
95
  }): string;
81
- export declare function hmachineToLean(id: string, pruned: Machine, laws: Law[], tampers?: {
96
+ export declare function hmachineToLean(id: string, m: Machine, laws: Law[], tampers?: {
82
97
  what: string;
83
98
  relations: {
84
99
  when: C;