gdp-wired 0.4.0 → 0.5.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
@@ -799,8 +892,9 @@ export function stepLawObligations(machine, laws, instances, sl) {
799
892
  const primes = [...state, ...maps].map(n => `${n}'`);
800
893
  const laters = Array.from({ length: k - 1 }, (_, i) => laterNames(m, i + 2));
801
894
  const later = laters.flatMap(l => Object.values(l.vars).map(e => e.name)), laterMaps = laters.flatMap(l => Object.values(l.maps));
895
+ const names = Array.from({ length: k - 1 }, (_, i) => state.map(n => stateName(n, i + 1))).flat();
802
896
  const taken = [...m.params, ...m.inputs, ...state, ...maps, ...carried];
803
- for (const p of [...primes, ...later, ...laterMaps])
897
+ for (const p of [...primes, ...later, ...laterMaps, ...names])
804
898
  if (taken.includes(p))
805
899
  throw new Error(`${m.name}: ${p} is a name of the machine and a name a step law reads`);
806
900
  for (const key of [sl.forall, ...(sl.keys ?? [])])
@@ -822,14 +916,30 @@ export function stepLawObligations(machine, laws, instances, sl) {
822
916
  if (j > 1)
823
917
  run = reading(run, laters[j - 2].vars, laters[j - 2].maps);
824
918
  run = step(holds(run, [...assumed(laws, instances), ...assume, ...(whens[j - 1] ? [inlineFlags(m, whens[j - 1])] : []), ...closing(m, instances)]), u);
919
+ if (j < k)
920
+ run = nameState(run, state, j);
825
921
  }
826
922
  return [goal(k === 1 ? `${sl.name} @step law` : `${sl.name} @${k} steps`, run, across(run, sl.then, state, maps, { inputs: {}, flags: flagsOf(m) }))];
827
923
  }
828
924
  // The laws' proof and every step law's tree: all must check.
829
925
  // `perStep`: a step law's own instances, where it has them (else `instances`).
926
+ // Each frame's hypotheses read no name a later step's request is read by: the condition the Lean twin's proof needs
927
+ // (Sets.lean's checkSKStep, `avoids (szone s sl)`; plain flags are inlined here, so their names never occur), checked
928
+ // here alike.
830
929
  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])));
930
+ return checkLaws(m, laws, proof.laws) && stepLaws.every(sl => {
931
+ const inst = proof.perStep?.[sl.name] ?? proof.instances, plain = m.history ? historyMachine(m).machine : m;
932
+ const zone = stepZone(plain, sl.steps ?? 1);
933
+ return [...assumed(laws, inst), ...assumed(plain.facts ?? [], inst), ...closing(plain, inst)].every(c => !mentions(c, zone))
934
+ && stepLawObligations(m, laws, inst, sl).every(g => proof.trees[g.name] !== undefined && check(g, proof.trees[g.name]));
935
+ });
832
936
  }
937
+ // The names a k-step law's run introduces past its first frame: each later step's inputs, quantified flags and their
938
+ // witnesses, and the state after each step but the last (stateName).
939
+ export const stepZone = (m, k) => [
940
+ ...Array.from({ length: k - 1 }, (_, i) => Object.values(laterNames(m, i + 2).vars).map(e => e.name)).flat(),
941
+ ...Array.from({ length: k - 1 }, (_, i) => Object.keys(m.state).map(n => stateName(n, i + 1))).flat(),
942
+ ];
833
943
  export function foldObligations(f, laws) {
834
944
  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
945
  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 };
@@ -1,6 +1,6 @@
1
1
  import { atMost, n, not, pointWrite } from "../core/machine.js";
2
2
  import { add, k, scale, sub } from "../core/solver.js";
3
- import { canon, historyMachine, foldObligations, leafConstraints, linear, livenessHelpful, livenessParts, livenessRanks, loopObligations, trueC, obligations, progressObligations, stepLawObligations, substC, withProducts } from "../kernel/kernel.js";
3
+ import { canon, columnTerm, stateName, termColumn, historyMachine, foldObligations, leafConstraints, linear, livenessHelpful, livenessParts, livenessRanks, loopObligations, trueC, obligations, progressObligations, stepLawObligations, substC, withProducts } from "../kernel/kernel.js";
4
4
  import { grow, proveBuilt, proveLaws, proveStepLaws } from "../kernel/prove.js";
5
5
  import { refinement, refinementSides } from "../core/compose.js";
6
6
  import { leanTerms } from "./lean-terms.js";
@@ -52,11 +52,13 @@ export function encode(ex, tamper = false, dropStep = true, globalVars, globalMa
52
52
  // Columns: every variable of the hypotheses, then every cell any leaf uses (and the cells inside their keys),
53
53
  // shallower cells first so a key reads only earlier columns.
54
54
  const vars = globalVars ?? [...new Set(goal.hyps.flatMap(varsOf))];
55
- const cells = new Set();
55
+ const cells = new Set(), terms = new Set();
56
56
  const addCell = (name) => { if (cells.has(name))
57
57
  return; cells.add(name); for (const key of Object.keys(cellKey(name)))
58
58
  if (key.includes('['))
59
- addCell(key); };
59
+ addCell(key);
60
+ else if (key.startsWith('⟦'))
61
+ terms.add(key); };
60
62
  // Monomials: every one a leaf's rows or products use, or that a product term has under the leaf's path.
61
63
  const monos = new Set();
62
64
  const subterms = (c) => {
@@ -77,11 +79,29 @@ export function encode(ex, tamper = false, dropStep = true, globalVars, globalMa
77
79
  wc(c);
78
80
  return out2;
79
81
  };
82
+ // Every choice in the hypotheses has its column, whether or not a leaf's rows keep it (a column can cancel, T − T,
83
+ // yet Lean's linear form of T still needs it): each distinct subterm once, as a run's states are shared.
84
+ const seenTerms = new WeakSet();
85
+ const choices = (t) => {
86
+ if (seenTerms.has(t))
87
+ return;
88
+ seenTerms.add(t);
89
+ if (t.op === 'sub' || t.op === 'min' || t.op === 'max' || t.op === 'if')
90
+ terms.add(termColumn(t));
91
+ for (const val of Object.values(t))
92
+ if (Array.isArray(val))
93
+ val.forEach(x => choices(x));
94
+ else if (typeof val === 'object' && val !== null)
95
+ choices(val);
96
+ };
97
+ goal.hyps.forEach(choices);
80
98
  leaves(tree, new Map(), (leaf, path) => {
81
99
  for (const r of withProducts(leafConstraints(goal, path), leaf.products ?? []))
82
100
  for (const key of Object.keys(r.lin)) {
83
101
  if (key.includes('['))
84
102
  addCell(key);
103
+ else if (key.startsWith('⟦'))
104
+ terms.add(key);
85
105
  else if (key.includes('*'))
86
106
  monos.add(key);
87
107
  }
@@ -93,7 +113,8 @@ export function encode(ex, tamper = false, dropStep = true, globalVars, globalMa
93
113
  monos.add(key);
94
114
  });
95
115
  const depth = (name) => name.split('[').length;
96
- const cols = [...vars, ...[...monos].sort((a, b) => a.split('*').length - b.split('*').length), ...[...cells].sort((a, b) => depth(a) - depth(b))];
116
+ // Term columns (open choices, each standing for its term's value) before the cells, whose keys may read them.
117
+ const cols = [...vars, ...[...monos].sort((a, b) => a.split('*').length - b.split('*').length), ...[...terms].sort((a, b) => Number(a.slice(1, -1)) - Number(b.slice(1, -1))), ...[...cells].sort((a, b) => depth(a) - depth(b))];
97
118
  const col = new Map(cols.map((name, j) => [name, j]));
98
119
  const maps = globalMaps ?? [...new Set([...[...cells].map(cellMap), ...goal.hyps.flatMap(h => [...JSON.stringify(h).matchAll(/"op":"get","map":"([^"]+)"/g)].map(m => m[1]))])];
99
120
  const dense = (lin) => {
@@ -110,11 +131,12 @@ export function encode(ex, tamper = false, dropStep = true, globalVars, globalMa
110
131
  return coefs;
111
132
  };
112
133
  const row = (lin) => `⟨${list(dense(lin).map(int))}, ${int(lin['1'] ?? 0)}⟩`;
113
- const tab = cols.map(name => (name.includes('[') ? `.cell ${maps.indexOf(cellMap(name))} ${row(cellKey(name))}`
114
- : name.includes('*') ? `.mono ${list(name.split('*').map(f => { const j = vars.indexOf(f); if (j < 0)
115
- throw new Error(`${ex.name}: factor ${f}`); return j; }).sort((a, b) => a - b).map(String))}`
116
- : `.var ${vars.indexOf(name)}`));
117
134
  const { e, c } = leanTerms(vars, maps, ex.name);
135
+ const tab = cols.map(name => (name.includes('[') ? `.cell ${maps.indexOf(cellMap(name))} ${row(cellKey(name))}`
136
+ : name.startsWith('⟦') ? `.term (${e(columnTerm(name))})`
137
+ : name.includes('*') ? `.mono ${list(name.split('*').map(f => { const j = vars.indexOf(f); if (j < 0)
138
+ throw new Error(`${ex.name}: factor ${f}`); return j; }).sort((a, b) => a - b).map(String))}`
139
+ : `.var ${vars.indexOf(name)}`));
118
140
  let leafNo = 0;
119
141
  const lean = (t, path) => {
120
142
  if (!('leaf' in t)) {
@@ -504,7 +526,8 @@ def ${id}_step${j} : SKStep := ⟨⟨${list(Array.from({ length: kk }, (_, s2) =
504
526
  ${list(qinsts(inst))},
505
527
  ${list(finsts(inst))},
506
528
  ${list(pinsts(inst))},
507
- []⟩
529
+ [],
530
+ ${list(Array.from({ length: kk - 1 }, (_, s2) => list(Object.keys(m.state).map(n2 => `(${ix(n2)}, ${ix(stateName(n2, s2 + 1))})`))))}⟩
508
531
  theorem ${id}_step${j}_holds : SKStepHolds ${id}_machine ${id}_step${j} :=
509
532
  skstep_sound ${id}_machine ${id}_proof ${id}_step${j} ${id}_check (by decide +kernel)`;
510
533
  } }));
@@ -625,7 +648,8 @@ theorem ${id}_orig_laws : ∀ ρ M, SReach ${id}_ghosted.orig ρ M → ∀ e ∈
625
648
  const allowGoals = [...allowed.values()].flatMap(a => (a ? [a.out.goal] : []));
626
649
  const vars = [...new Set([...m.params, ...m.inputs, ...Object.keys(m.state), ...keysOf, ...keysOf.map(k2 => `${k2}#`), '#', ...flags.map(([f]) => `?${f}`),
627
650
  ...quantified.map(f => `w?${f}`), ...boundKeys(m), ...(opts.tamper ? boundKeys(opts.tamper.machine) : []), ...stepLaws.flatMap(sl => (sl.forall ? [sl.forall] : [])), ...(opts.free ?? []),
628
- ...primes, ...later, ...[...goalsL, ...goalsS, ...allowGoals].flatMap(g => g.hyps.flatMap(varsOf))])];
651
+ ...primes, ...later, ...Array.from({ length: k - 1 }, (_, j) => Object.keys(m.state).map(n2 => stateName(n2, j + 1))).flat(),
652
+ ...[...goalsL, ...goalsS, ...allowGoals].flatMap(g => g.hyps.flatMap(varsOf))])];
629
653
  const baseMaps = [...new Set([...Object.keys(m.maps ?? {}), ...Object.keys(m.step.maps ?? {}), ...Object.keys(m.fns ?? {})])];
630
654
  const mapNames = [...new Set([...baseMaps, ...carried, ...(allSteps.length ? Object.keys(m.maps ?? {}).map(mp => `${mp}'`) : []), ...laterMaps,
631
655
  ...[...JSON.stringify([...goalsL, ...goalsS, ...allowGoals]).matchAll(/"op":"get","map":"([^"]+)"/g)].map(x2 => x2[1])])];