gdp-wired 0.18.0 → 0.20.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.
@@ -1,10 +1,11 @@
1
1
  import { atMost, n, not, pointWrite } from "../core/machine.js";
2
2
  import { add, k, scale, sub } from "../core/solver.js";
3
3
  import { boundOf, canon, factorsOf, isCell, isMonomial, columnTerm, stateName, termColumn, historyMachine, foldObligations, leafConstraints, loopLiveness, loopMachine, linear, livenessHelpful, livenessParts, livenessRanks, loopObligations, trueC, obligations, progressObligations, stepLawObligations, substC, witnessesOf, withProducts } from "../kernel/kernel.js";
4
- import { grow, minimal, proveBuilt, proveLaws, proveStepLaws } from "../kernel/prove.js";
4
+ import { grow, minimal, proveBuilt, proveLaws, proveStepLaws, withDeadline } from "../kernel/prove.js";
5
5
  import { refinement, refinementSides } from "../core/compose.js";
6
6
  import { leanSurface, leanTerms } from "./lean-terms.js";
7
7
  import { descentGoal, mainGoal, premiseGoal } from "../kernel/cyclic.js";
8
+ import { allowNode, certMs, closingInsts, reqTS, withWitness, foldNodes, certMode, quietly, uncoveredWhy, hasQuantified, kWhens, machineNodes, proveCyclic, routeOf, searchedStepNodes, skMachine, stepLawNodes, tBack, tSub } from "../core/cyclic.js";
8
9
  import { leanLibrary } from "./library.js";
9
10
  export { leanLibrary };
10
11
  // --- to Lean ------------------------------------------------------------------------------------------------------------
@@ -48,8 +49,34 @@ export function leaves(t, path, visit) {
48
49
  leaves(t.yes, new Map(path).set(id, { lin: t.split, holds: true }), visit);
49
50
  leaves(t.no, new Map(path).set(id, { lin: t.split, holds: false }), visit);
50
51
  }
51
- export function encode(ex, tamper = false, dropStep = true, globalVars, globalMaps) {
52
+ export function encode(ex, tamper = false, dropStep = true, globalVars, globalMaps, slim = false) {
52
53
  const { goal, tree } = ex;
54
+ // `slim`: the table only has the columns Lean reads. Each row a leaf uses has a hint (where Lean looks for it first),
55
+ // and Lean computes only the hinted hypotheses' linear forms (Kernel.lean's srcRows), so the columns are the
56
+ // variables, those hypotheses' choices and the cells they read, a hinted term column's, and every row's (the path's
57
+ // atoms, the leaf's rows, its squares and products). A leaf with a row of no hint (Lean then reads every
58
+ // hypothesis's rows) keeps the whole table. A premise or descent of a cyclic node reads one or two of its
59
+ // hypotheses, so its table is a few columns, not every choice of the node's facts.
60
+ // Each leaf with its path and rows, found once (the passes below and the tree's text read them in the same order).
61
+ const leafList = [];
62
+ leaves(tree, new Map(), (leaf, path) => { leafList.push({ leaf, path, base: leafConstraints(goal, path) }); });
63
+ const reads = new Set(), hintTerms = new Set();
64
+ let whole = !slim;
65
+ if (slim)
66
+ for (const { leaf, base } of leafList) {
67
+ const n0 = base.length;
68
+ for (const i of [...leaf.leaf.flatMap(st => (st.kind === 'input' && st.index < n0 ? [st.index] : [])), ...(leaf.products ?? []).flat().filter(i => i < n0)]) {
69
+ const src = base[i].src;
70
+ if (!src)
71
+ whole = true;
72
+ else if ('fact' in src)
73
+ reads.add(src.fact);
74
+ else if ('term' in src)
75
+ hintTerms.add(src.term);
76
+ }
77
+ }
78
+ const readHyps = whole ? goal.hyps : goal.hyps.filter((_, i) => reads.has(i));
79
+ const readTerms = whole ? [] : [...hintTerms].map(name => columnTerm(name));
53
80
  // Columns: every variable of the hypotheses, then every cell any leaf uses (and the cells inside their keys),
54
81
  // shallower cells first so a key reads only earlier columns.
55
82
  const vars = globalVars ?? [...new Set(goal.hyps.flatMap(varsOf))];
@@ -101,9 +128,15 @@ export function encode(ex, tamper = false, dropStep = true, globalVars, globalMa
101
128
  else if (typeof val === 'object' && val !== null)
102
129
  choices(val);
103
130
  };
104
- goal.hyps.forEach(choices);
105
- leaves(tree, new Map(), (leaf, path) => {
106
- for (const r of [...withProducts(leafConstraints(goal, path), leaf.products ?? [], leaf.squares), ...(leaf.squares ?? []).map(lin => ({ lin }))])
131
+ readHyps.forEach(choices);
132
+ readTerms.forEach(choices);
133
+ const readSubterms = [...readHyps, ...readTerms.map((t) => ({ op: '<=', a: t, b: t }))].flatMap(subterms).filter(e => e.op === 'prod' || e.op === 'get');
134
+ for (const { leaf, path, base } of leafList) {
135
+ const n0 = base.length;
136
+ const used = whole ? undefined : new Set([...leaf.leaf.flatMap(st => (st.kind === 'input' && st.index < n0 ? [st.index] : [])), ...(leaf.products ?? []).flat().filter(i => i < n0)]);
137
+ const rowsHere = whole ? withProducts(base, leaf.products ?? [], leaf.squares)
138
+ : [...base.filter((_, i) => used.has(i)), ...withProducts(base, leaf.products ?? [], leaf.squares).slice(n0), ...[...path.values()].map(a => ({ lin: a.lin }))];
139
+ for (const r of [...rowsHere, ...(leaf.squares ?? []).map(lin => ({ lin }))])
107
140
  for (const key of Object.keys(r.lin)) {
108
141
  if (isMonomial(key))
109
142
  addMono(key);
@@ -114,18 +147,17 @@ export function encode(ex, tamper = false, dropStep = true, globalVars, globalMa
114
147
  }
115
148
  // Each cell a hypothesis reads under the path, kept or not: two reads of one cell cancel in a row (T − T), yet
116
149
  // Lean's linear form of each still needs the cell's column to see the comparison decided.
117
- for (const h of goal.hyps)
118
- for (const e of subterms(h)) {
119
- if (e.op === 'prod')
120
- for (const key of Object.keys(linear(e, path) ?? {}))
121
- if (isMonomial(key))
122
- addMono(key);
123
- if (e.op === 'get')
124
- for (const key of Object.keys(linear(e, path) ?? {}))
125
- if (isCell(key))
126
- addCell(key);
127
- }
128
- });
150
+ for (const e of readSubterms) {
151
+ if (e.op === 'prod')
152
+ for (const key of Object.keys(linear(e, path) ?? {}))
153
+ if (isMonomial(key))
154
+ addMono(key);
155
+ if (e.op === 'get')
156
+ for (const key of Object.keys(linear(e, path) ?? {}))
157
+ if (isCell(key))
158
+ addCell(key);
159
+ }
160
+ }
129
161
  const depth = (name) => name.split('[').length;
130
162
  // Term columns (open choices, each standing for its term's value) before the cells, whose keys may read them; a
131
163
  // monomial with a cell among its factors after the cells.
@@ -172,13 +204,11 @@ export function encode(ex, tamper = false, dropStep = true, globalVars, globalMa
172
204
  : name.includes('[') ? `.cell ${maps.indexOf(cellMap(name))} ${row(cellKey(name))}`
173
205
  : name.startsWith('⟦') ? `.term (${e(columnTerm(name))})`
174
206
  : `.var ${vars.indexOf(name)}`));
175
- let leafNo = 0;
176
- const lean = (t, path) => {
177
- if (!('leaf' in t)) {
178
- const id = canon(t.split);
179
- return `.split ${row(t.split)}\n (${lean(t.yes, new Map(path).set(id, { lin: t.split, holds: true }))})\n (${lean(t.no, new Map(path).set(id, { lin: t.split, holds: false }))})`;
180
- }
181
- const base = leafConstraints(goal, path), prods = t.products ?? [], squares = t.squares ?? [];
207
+ let leafNo = 0, leafAt = 0;
208
+ const lean = (t) => {
209
+ if (!('leaf' in t))
210
+ return `.split ${row(t.split)}\n (${lean(t.yes)})\n (${lean(t.no)})`;
211
+ const { path, base } = leafList[leafAt++], prods = t.products ?? [], squares = t.squares ?? [];
182
212
  const rows = withProducts(base, prods, squares).map(r => r.lin), n0 = base.length;
183
213
  // Only the rows the derivation and the products read, renumbered (the squares follow them, then the products, in
184
214
  // order); cuts carry their quotient row (Cert's format).
@@ -225,12 +255,17 @@ export function encode(ex, tamper = false, dropStep = true, globalVars, globalMa
225
255
  if ('fact' in src)
226
256
  return `.fact ${src.fact}`;
227
257
  const j = 'pair' in src ? pairs.indexOf([...src.pair].sort().join('\n')) : col.get('nat' in src ? src.nat : src.term) ?? -1;
228
- return j < 0 ? '.any' : 'pair' in src ? `.eq ${j}` : 'nat' in src ? `.nat ${j}` : `.term ${j}`;
258
+ return j < 0 ? '.any' : 'pair' in src ? `.eq ${eqAt.get(j)}` : 'nat' in src ? `.nat ${j}` : `.term ${j}`;
229
259
  };
230
- const at2 = list(used.map(u => `(${row(rows[u])}, ${hint(base[u].src)})`)), rest = `${list(steps)} ${list(eqs)} ${list(prods.map(([i, j]) => `(${idx(i)}, ${idx(j)})`))}`;
260
+ // (slim: only the pairs a row's hint names, renumbered; Lean reads the rest only for a row of no hint, which a slim
261
+ // table has none of)
262
+ const pairAt = (src) => (src && 'pair' in src ? pairs.indexOf([...src.pair].sort().join('\n')) : -1);
263
+ const keepEqs = slim && !whole ? [...new Set(used.map(u => pairAt(base[u].src)).filter(j => j >= 0))] : eqs.map((_, j) => j);
264
+ const eqAt = new Map(keepEqs.map((j, i) => [j, i]));
265
+ const at2 = list(used.map(u => `(${row(rows[u])}, ${hint(base[u].src)})`)), rest = `${list(steps)} ${list(keepEqs.map(j => eqs[j]))} ${list(prods.map(([i, j]) => `(${idx(i)}, ${idx(j)})`))}`;
231
266
  return squares.length ? `.leafSq ${at2} ${list(squares.map(row))} ${rest}` : `.leafAt ${at2} ${rest}`;
232
267
  };
233
- return { tab: list(tab), hyps: list(goal.hyps.map(c)), tree: lean(tree, new Map()), cols, c, e };
268
+ return { tab: list(tab), tabCols: tab, hyps: list(goal.hyps.map(c)), tree: lean(tree), cols, c, e };
234
269
  }
235
270
  export function goalToLean(ex, i, tamper = false, dropStep = true) {
236
271
  const { tab, hyps, tree, cols } = encode(ex, tamper, dropStep);
@@ -381,30 +416,6 @@ theorem law${j} ${reach.binders} (h : ${reach.hyp}) : ${statement(law)} := by
381
416
  ${law.forall ? `exact fun k => (evalC_iff (upd ρ ${vars.indexOf(law.forall)} k) μ ${id}_l${j}.holds).mp (hl k)` : `exact (evalC_iff ρ μ ${id}_l${j}.holds).mp hl`}`}`)).join('\n')}
382
417
  end ${id}`;
383
418
  }
384
- // Live.lean's reqC: each input named by its term, two parts per and.
385
- const reqTS = (req) => (req.length ? { op: 'and', xs: [{ op: '=', a: { op: 'var', name: req[0][0] }, b: req[0][1] }, reqTS(req.slice(1))] } : { op: '<=', a: { op: 'const', value: 0 }, b: { op: 'const', value: 0 } });
386
- // Witnesses for a helpful request's free inputs, for its allowed goal: all free first, then each free input fixed to 0, a
387
- // state variable or a param (a history's query, say, which its relations bound), the first combination that proves.
388
- function withWitness(m, request, attempt) {
389
- const free = m.inputs.filter(i => !(i in request));
390
- const cands = [undefined, { op: 'const', value: 0 }, ...Object.keys(m.state).map(v => ({ op: 'var', name: v })), ...m.params.map(v => ({ op: 'var', name: v }))];
391
- const combos = [[]];
392
- if (free.length <= 2) {
393
- const go = (i, acc) => { if (i === free.length) {
394
- if (acc.length)
395
- combos.push(acc);
396
- return;
397
- } for (const c of cands)
398
- go(i + 1, c ? [...acc, [free[i], c]] : acc); };
399
- go(0, []);
400
- }
401
- for (const wit of combos) {
402
- const out = attempt(wit);
403
- if (out !== undefined)
404
- return { wit, out };
405
- }
406
- return undefined;
407
- }
408
419
  // A liveness as Live.lean's record: from, to, the rank's parts, each helpful request with its region, the primes.
409
420
  function liveLit(l, t, ix, primes, primeMaps) {
410
421
  return `⟨${t.c(l.from)}, ${t.c(l.to)}, ${list(livenessRanks(l).map(t.e))},
@@ -543,6 +554,30 @@ export function machineLit(mm, t, ix, mx) {
543
554
  ${list((mm.facts ?? []).filter(l => !l.keys?.length).map(l => `⟨${c(l.holds)}, ${l.forall ? `some ${ix(l.forall)}` : 'none'}⟩`))},
544
555
  ${list((mm.facts ?? []).filter(l => l.keys?.length).map(l => `(⟨${c(l.holds)}, some ${ix(l.forall)}⟩, ${list(l.keys.map(k2 => String(ix(k2))))})`))}⟩`;
545
556
  }
557
+ // A request's extended names: its inputs, each flag that holds at every key and its witnesses (Sets.lean's extVars, in
558
+ // the order the usual export lists a later step's names in).
559
+ const extOf = (m) => [...m.inputs, ...Object.entries(m.step.flags ?? {}).flatMap(([f, c]) => (c.op === 'all' ? [`?${f}`, ...witnessesOf(f, c)] : []))];
560
+ // The names a machine's export numbers first, before those only its goals read: the machine's own (params, inputs, state,
561
+ // flags, the keys of its laws and facts, the keys its writes and quantified flags bind) and its claims' (each step law's
562
+ // key, the state after the step, each later request's names, the state between steps, a history's record). The usual
563
+ // export and the cyclic one (cycMachineToLean) number them alike, so that a machine, a law's statement and a step law are
564
+ // the same Lean text in both.
565
+ function machineNames(m, laws, allSteps, stepLaws, opts) {
566
+ const flags = Object.entries(m.step.flags ?? {}), quantified = flags.filter(([, c]) => c.op === 'all').map(([f]) => f);
567
+ const qwits = (f) => witnessesOf(f, m.step.flags[f]);
568
+ const carried = m.inputMaps ?? [], keysOf = [...new Set([...laws, ...(m.facts ?? [])].flatMap(l => [l.forall, ...(l.keys ?? [])].filter((k) => !!k)))];
569
+ const k = Math.max(1, ...allSteps.map(sl => sl.steps ?? 1));
570
+ const ext = [...m.inputs, ...quantified.flatMap(f => [`?${f}`, ...qwits(f)])];
571
+ const later = Array.from({ length: k - 1 }, (_, j) => ext.map(v => `${v}@${j + 2}`)).flat();
572
+ const laterMaps = Array.from({ length: k - 1 }, (_, j) => carried.map(mp => `${mp}@${j + 2}`)).flat();
573
+ const primes = allSteps.length ? Object.keys(m.state).map(s2 => `${s2}'`) : [];
574
+ const vars = [...new Set([...m.params, ...m.inputs, ...Object.keys(m.state), ...keysOf, ...keysOf.map(k2 => `${k2}#`), '#', ...flags.map(([f]) => `?${f}`),
575
+ ...quantified.flatMap(qwits), ...boundKeys(m), ...(opts.tamper ? boundKeys(opts.tamper.machine) : []), ...stepLaws.flatMap(sl => (sl.forall ? [sl.forall] : [])), ...(opts.free ?? []),
576
+ ...primes, ...later, ...Array.from({ length: k - 1 }, (_, j) => Object.keys(m.state).map(n2 => stateName(n2, j + 1))).flat(), ...(opts.record ?? []).map(f => `r.${f}`)])];
577
+ const baseMaps = [...new Set([...Object.keys(m.maps ?? {}), ...Object.keys(m.step.maps ?? {}), ...Object.keys(m.fns ?? {})])];
578
+ const maps = [...new Set([...baseMaps, ...carried, ...(allSteps.length ? Object.keys(m.maps ?? {}).map(mp => `${mp}'`) : []), ...laterMaps])];
579
+ return { vars, maps };
580
+ }
546
581
  export function smachineToLean(id, given, laws, opts = {}) {
547
582
  const hm = given.history ? { ...given, history: { ...given.history, prune: undefined } } : undefined;
548
583
  const hplain = hm ? historyMachine(hm) : undefined, stepLaws = opts.stepLaws ?? [], rf = opts.refined, gv = opts.ghosts ?? [];
@@ -639,18 +674,14 @@ example : (checkSMachine ${id}_tampered ${id}_proof && sliveOk ${id}_tampered ${
639
674
  theorem ${id}_orig_laws : ∀ ρ M, SReach ${id}_ghosted.orig ρ M → ∀ e ∈ ${id}_proof, avoids ${id}_ghosted.vars e.law.holds = true → e.Holds ρ M :=
640
675
  sghost_laws ${id}_ghosted ${id}_ghostOk ${id}_proof ${id}_check` });
641
676
  const allSteps = claimSteps();
642
- const r = proveStepLaws(m, laws, allSteps);
677
+ const r = proveStepLawsOnce(m, laws, allSteps);
643
678
  if (!r.proven || !r.proof)
644
679
  throw new Error(`${id}: not proven`);
645
680
  const lp = r.proof.laws, si = r.proof.instances;
646
681
  const flags = Object.entries(m.step.flags ?? {}), quantified = flags.filter(([, c]) => c.op === 'all').map(([f]) => f);
647
682
  const qwits = (f) => witnessesOf(f, m.step.flags[f]);
648
- const carried = m.inputMaps ?? [], facts = m.facts ?? [], keysOf = [...new Set([...laws, ...facts].flatMap(l => [l.forall, ...(l.keys ?? [])].filter((k) => !!k)))];
649
- const k = Math.max(1, ...allSteps.map(sl => sl.steps ?? 1));
683
+ const carried = m.inputMaps ?? [], facts = m.facts ?? [];
650
684
  const ext = [...m.inputs, ...quantified.flatMap(f => [`?${f}`, ...qwits(f)])];
651
- const later = Array.from({ length: k - 1 }, (_, j) => ext.map(v => `${v}@${j + 2}`)).flat();
652
- const laterMaps = Array.from({ length: k - 1 }, (_, j) => carried.map(mp => `${mp}@${j + 2}`)).flat();
653
- const primes = allSteps.length ? Object.keys(m.state).map(s2 => `${s2}'`) : [];
654
685
  const instOf = (sl) => r.proof.perStep?.[sl.name] ?? si;
655
686
  const goalsL = obligations(m, laws, lp.instances), goalsS = allSteps.map(sl => stepLawObligations(m, laws, instOf(sl), sl)[0]);
656
687
  // Weak fairness over sets: each helpful request allowed in its region carrying empty sets (Live.lean's sAllowGoal,
@@ -716,13 +747,9 @@ theorem ${id}_orig_laws : ∀ ρ M, SReach ${id}_ghosted.orig ρ M → ∀ e ∈
716
747
  return r2.proven ? r2 : undefined;
717
748
  })]));
718
749
  const allowGoals = [...allowed.values()].flatMap(a => (a ? [a.out.goal] : []));
719
- const vars = [...new Set([...m.params, ...m.inputs, ...Object.keys(m.state), ...keysOf, ...keysOf.map(k2 => `${k2}#`), '#', ...flags.map(([f]) => `?${f}`),
720
- ...quantified.flatMap(qwits), ...boundKeys(m), ...(opts.tamper ? boundKeys(opts.tamper.machine) : []), ...stepLaws.flatMap(sl => (sl.forall ? [sl.forall] : [])), ...(opts.free ?? []),
721
- ...primes, ...later, ...Array.from({ length: k - 1 }, (_, j) => Object.keys(m.state).map(n2 => stateName(n2, j + 1))).flat(),
722
- ...(hm ? hm.history.record.map(f => `r.${f}`) : []), ...[...goalsL, ...goalsS, ...allowGoals, ...(hplain?.goals ?? [])].flatMap(g => g.hyps.flatMap(varsOf))])];
723
- const baseMaps = [...new Set([...Object.keys(m.maps ?? {}), ...Object.keys(m.step.maps ?? {}), ...Object.keys(m.fns ?? {})])];
724
- const mapNames = [...new Set([...baseMaps, ...carried, ...(allSteps.length ? Object.keys(m.maps ?? {}).map(mp => `${mp}'`) : []), ...laterMaps,
725
- ...[...JSON.stringify([...goalsL, ...goalsS, ...allowGoals]).matchAll(/"op":"get","map":"([^"]+)"/g)].map(x2 => x2[1])])];
750
+ const named = machineNames(m, laws, allSteps, stepLaws, { ...opts, ...(hm ? { record: hm.history.record } : {}) });
751
+ const vars = [...new Set([...named.vars, ...[...goalsL, ...goalsS, ...allowGoals, ...(hplain?.goals ?? [])].flatMap(g => g.hyps.flatMap(varsOf))])];
752
+ const mapNames = [...new Set([...named.maps, ...[...JSON.stringify([...goalsL, ...goalsS, ...allowGoals]).matchAll(/"op":"get","map":"([^"]+)"/g)].map(x2 => x2[1])])];
726
753
  const enc = (g, tree) => encode({ name: `${id}: ${g.name}`, goal: g, tree }, false, true, vars, mapNames);
727
754
  const { e, c } = leanTerms(vars, mapNames, id);
728
755
  const ix = (name) => { const j = vars.indexOf(name); if (j < 0)
@@ -830,6 +857,353 @@ ${hm ? histBlock() : readable(id, laws, vars, mapNames, { binders: '(ρ : Nat
830
857
  def ${id}_tampered : SMachine := ${lit(opts.tamper.machine)}
831
858
  example : checkSMachine ${id}_tampered ${id}_proof = false := by ${checkBy}` : ''}${records.length ? `\n\n${records.join('\n\n')}` : ''}${opts.after ? `\n\n${opts.after(ix)}` : ''}`;
832
859
  }
860
+ // --- the cyclic certificate's export (GDP_CERT=cyclic) -------------------------------------------------------------
861
+ // By default, or with GDP_CERT=cyclic (core/cyclic.ts's certOn: a covered proof's certificate is its cyclic proof; or toLean's
862
+ // `cyclic`), toLean exports each machine the cyclic route covers from one cyclic
863
+ // proof (kernel/cyclic.ts) of its laws and of every step law its claims bring (its own step laws and k-step laws, a
864
+ // liveness's), carried to the machine by Bridge.lean's cyc_machine_sound and BridgeRules.lean's cyc_kstep_sound,
865
+ // sleads_of_holds (liveness, loops), hreach_plain (histories) and cyc_ghost_holds (ghosts), and each theorem stated as the
866
+ // usual export states it: the same machine, laws, step laws and liveness (machineNames numbers them alike), the same
867
+ // `<id>.lawN`, `<id>_stepN_holds`, `<id>_liveN_holds`, `<id>_halts`; a machine with flags that hold at every key through
868
+ // its sk machine (BridgeRules.lean's q bridges). What the cyclic route doesn't cover goes the usual way: a law that reads
869
+ // a map at a key read from a map, and whatever no certificate is found for within the deadline (`certReport`).
870
+ // The usual proof of a machine's laws and step laws, made once per toLean: an item exported the usual way after its
871
+ // cyclic certificate wasn't found (cycMachineProof) reads the proof that search started from, not one made again. It
872
+ // is made with no cyclic certificate of its own (quietly): the export makes the certificate it exports from the usual
873
+ // trees (cycMachineProof), and the usual export reads the trees, so a certificate made here is never read.
874
+ const usualMade = new Map();
875
+ function proveStepLawsOnce(m, laws, stepLaws) {
876
+ const key = JSON.stringify([m, laws, stepLaws]), had = usualMade.get(key);
877
+ if (had)
878
+ return had;
879
+ const r = quietly(() => proveStepLaws(m, laws, stepLaws));
880
+ usualMade.set(key, r);
881
+ return r;
882
+ }
883
+ const envOf = () => globalThis.process?.env;
884
+ export const cyclicCerts = certMode;
885
+ let certsOn = false, cycTexts = 0;
886
+ // What the last toLean exported from cyclic certificates, and what went the usual way (and why).
887
+ export const certReport = { cyclic: [], usual: [], translatedSteps: [] };
888
+ // Each item's cyclic certificate is made within one deadline (GDP_CERT_MS, 10 s by default), every goal it grows (the
889
+ // translation's leaves grown again, the searched nodes) bounded by it: past it, the item goes the usual way.
890
+ function cycMachineProof(m, laws, allSteps, allows = []) {
891
+ const r = proveStepLawsOnce(m, laws, allSteps);
892
+ if (!r.proven || !r.proof)
893
+ return { why: 'not proven the usual way' };
894
+ const until = performance.now() + certMs();
895
+ return withDeadline(until, () => cycMachineProofBy(m, laws, allSteps, allows, r, until));
896
+ }
897
+ function cycMachineProofBy(m, laws, allSteps, allows, r, until) {
898
+ if (!r.proof)
899
+ return { why: 'not proven the usual way' };
900
+ const tStart = performance.now();
901
+ const lp = r.proof.laws, instOf = (sl) => r.proof.perStep?.[sl.name] ?? r.proof.instances;
902
+ // A machine with flags that hold at every key: its sk machine's nodes, the flags' closing facts read where each node
903
+ // reads the run.
904
+ const q = hasQuantified(m), mm = skMachine(m);
905
+ const L = laws.length, mine = (facts, lawNodes) => searchedStepNodes(m, laws, allSteps.map(sl => ({ law: sl, instances: instOf(sl) })), facts, lawNodes);
906
+ const tries = [];
907
+ if (routeOf() !== 'search' && allSteps.length) {
908
+ const usualSteps = allSteps.map(sl => { const inst = instOf(sl), g = stepLawObligations(m, laws, inst, sl)[0]; return { sl, inst, goal: g, tree: r.proof.trees[g.name] }; });
909
+ const tr = usualSteps.every(u => u.tree) ? machineNodes(m, laws, lp.instances, { trees: lp.trees, steps: usualSteps }) : undefined;
910
+ if (tr && !tr.why && tr.specs.length === 3 * L + 2 * allSteps.length) {
911
+ // (either time form cycKOk reads: one step back at a time, or t − n at once)
912
+ const id = (x) => JSON.stringify(x), forms = [tBack, tSub].map(time => stepLawNodes(mm, laws, allSteps.map(sl => ({ law: sl, instances: instOf(sl) })), tr.facts, tr.specs, time));
913
+ const fits = allSteps.every((_, j) => forms.some(own => {
914
+ const a = tr.specs[3 * L + 2 * j + 1], b = own[j], hs = new Set(b.hyps.map(id));
915
+ return id(a.concl) === id(b.concl) && a.hyps.every(h => hs.has(id(h)));
916
+ }));
917
+ if (fits)
918
+ tries.push({ facts: tr.facts, specs: tr.specs, lawNodes: tr.specs, stepAt: allSteps.map((_, j) => 3 * L + 2 * j + 1), translated: true });
919
+ }
920
+ }
921
+ for (const base of [...(routeOf() !== 'search' ? [machineNodes(m, laws, lp.instances, { trees: lp.trees })] : []), machineNodes(m, laws, lp.instances)].filter(b => !b.why)) {
922
+ tries.push({ facts: base.facts, specs: [...base.specs, ...mine(base.facts, base.specs)], lawNodes: base.specs, stepAt: allSteps.map((_, j) => base.specs.length + j) });
923
+ }
924
+ let last = 'no nodes';
925
+ const t00 = tStart;
926
+ for (const { facts, specs: specs0, lawNodes, stepAt, translated } of tries) {
927
+ if (performance.now() > until)
928
+ return { why: `past the certificate's deadline (${certMs()} ms)` };
929
+ const tTry = performance.now();
930
+ let specs = specs0, v = proveCyclic(facts, specs);
931
+ if (envOf()?.GDP_LEAN_TIMES === '1')
932
+ console.error(`lean try ${m.name}: ${translated ? 'translated' : 'laws + searched steps'}, ${specs.length} nodes, ${v.proven && v.checked ? 'proven' : 'not'}, ${Math.round(performance.now() - tTry)} ms (${Math.round(tTry - t00)} ms building before)`);
933
+ if (!(v.proven && v.checked)) {
934
+ last = performance.now() > until ? `past the certificate's deadline (${certMs()} ms)` : v.proven ? `not checked (${v.why})` : `${v.node}: ${v.goal}: ${v.stuck}`;
935
+ continue;
936
+ }
937
+ const allowAt = allows.map((group) => {
938
+ if ((lawNodes[0]?.rank.length ?? 2) < 2)
939
+ return undefined;
940
+ const before = specs, out = [];
941
+ for (const a of group) {
942
+ const found = withWitness(m, a.request, wit => {
943
+ if (performance.now() > until)
944
+ return undefined;
945
+ const spec = allowNode(mm, laws, r.proof.instances, { op: 'and', xs: [a.when, reqTS(wit)] }, facts, lawNodes, `${m.name}: allowed`, q ? closingInsts(m, facts, [r.proof.instances, ...Object.values(lp.instances)], [{ op: 'var', name: 't$' }], laws) : []);
946
+ const v2 = proveCyclic(facts, [...specs, spec]);
947
+ return v2.proven && v2.checked ? { spec, v2 } : undefined;
948
+ });
949
+ if (!found) {
950
+ specs = before;
951
+ return undefined;
952
+ }
953
+ out.push({ node: specs.length, wit: found.wit });
954
+ specs = [...specs, found.out.spec];
955
+ v = found.out.v2;
956
+ }
957
+ return out;
958
+ });
959
+ if (v.proven && v.checked) {
960
+ if (translated)
961
+ certReport.translatedSteps.push(m.name);
962
+ return { p: v.proof, stepAt, allowAt };
963
+ }
964
+ }
965
+ return { why: last };
966
+ }
967
+ // A machine (with a history, ghosts, a liveness or as a loop's) from one cyclic proof, or why it can't be.
968
+ export function cycMachineToLean(id, given, laws, opts = {}) {
969
+ const hm = given.history ? { ...given, history: { ...given.history, prune: undefined } } : undefined;
970
+ const hplain = hm ? historyMachine(hm) : undefined, m = hplain ? hplain.machine : given;
971
+ const stepLaws = opts.stepLaws ?? [], gv = opts.ghosts ?? [], lp0 = opts.loop;
972
+ const why = uncoveredWhy(m, laws);
973
+ if (why)
974
+ return { why };
975
+ // With flags that hold at every key, the sk machine's encoding and BridgeRules.lean's q bridges.
976
+ const mq = skMachine(m), Q = hasQuantified(m) ? 'Q' : '', q = hasQuantified(m) ? 'q' : '';
977
+ const lives = (opts.liveness ?? []).map(l => ({ l, ...livenessParts(m, l) }));
978
+ // A refinement's step law: the spec stutters or allows the request it is sent, its `then` Lean's SRefined.allowed (the
979
+ // spec's assumption at the witness, the composite's flags inlined).
980
+ const rf = opts.refined;
981
+ const rfStep = rf && { ...rf.step, when: undefined, then: { op: 'or', xs: [rf.stutter, substC(rf.spec.assume ?? trueC, { vars: Object.fromEntries(Object.entries(rf.wit).map(([inp, t2]) => [`b_${inp}`, t2])), maps: {},
982
+ flags: Object.fromEntries(Object.keys(m.step.flags ?? {}).map(f => [f, { op: 'flag', name: f }])) })] } };
983
+ const allSteps = [...(rfStep ? [rfStep] : stepLaws), ...lives.flatMap(x => [x.stay, x.cover, ...x.helps])];
984
+ const made = cycMachineProof(m, laws, allSteps, lives.map(x => x.helps.map((hs, i) => ({ when: hs.when, request: livenessHelpful(x.l)[i].request }))));
985
+ if ('why' in made)
986
+ return made;
987
+ const { p, stepAt, allowAt } = made;
988
+ const named = machineNames(m, laws, allSteps, stepLaws, { ...(opts.free ? { free: opts.free } : {}), ...(hm ? { record: hm.history.record } : {}) });
989
+ const sc = cycScope([p], { machine: mq, laws });
990
+ const vars = [...new Set([...named.vars, ...sc.vars])], maps = [...new Set([...named.maps, ...sc.maps])];
991
+ const { e, c } = leanTerms(vars, maps, id);
992
+ const ix = (name) => { const j = vars.indexOf(name); if (j < 0)
993
+ throw new Error(`${id}: no variable for ${name}`); return j; };
994
+ const mx = (name) => { const j = maps.indexOf(name); if (j < 0)
995
+ throw new Error(`${id}: no map for ${name}`); return j; };
996
+ const lit = (mm) => machineLit(mm, { e, c }, ix, mx);
997
+ const inline = (c2) => substC(c2, { vars: {}, maps: {}, flags: flagsOfM(m) });
998
+ const keysOf = (l) => [...(l.forall ? [l.forall] : []), ...(l.keys ?? [])];
999
+ const keysL = (l) => list(keysOf(l).map(k2 => String(ix(k2))));
1000
+ const state = Object.keys(m.state), carried = m.inputMaps ?? [];
1001
+ const primesL = () => list(state.map(s2 => `(${ix(s2)}, ${ix(`${s2}'`)})`)), primeMapsL = () => list(Object.keys(m.maps ?? {}).map(mp => `(${mx(mp)}, ${mx(`${mp}'`)})`));
1002
+ const node = (i) => `${id}_node${i}`;
1003
+ // What every law's and step law's check reads alike, proven once: the exported facts among the encoding's, and (with
1004
+ // no flag at every key) the encoding's shape and terms at no keys, which a law's at its keys implies. Each check then
1005
+ // reads those as true (simp only, unfolding the check), not computed again per law.
1006
+ const sharedOk = [`${id}_factsIn`, ...(Q ? [] : [`${id}_shape`, `${id}_terms`])].join(', ');
1007
+ const okLemmas = `theorem ${id}_factsIn : cyc${Q}FactsIn ${id}_machine ${id}_enc ${id}_facts = true := by ${checkBy}${Q ? '' : `
1008
+ theorem ${id}_shape : cycShapeOk ${id}_machine ${id}_enc [] = true := by ${checkBy}
1009
+ theorem ${id}_terms : cycTermsOk ${id}_machine ${id}_enc [] = true := by ${checkBy}`}`;
1010
+ const mOk = `by simp only [cyc${Q}MOk, ${sharedOk}]; ${checkBy}`, kOk = `by simp only [cyc${Q}KOk, cyc${Q}KOkAt, ${sharedOk}]; ${checkBy}`;
1011
+ const aOk = `by simp only [cyc${Q}AOk, ${sharedOk}]; ${checkBy}`;
1012
+ const kstep = (sk, i) => `cyc_${q}kstep_sound ${id}_machine ${id}_enc ${id}_facts ${id}_nodes ${id}_check ${sk} ${node(i)} ${node(i)}_mem`;
1013
+ // The machine as the usual export defines it: with a history its plain machine, with ghosts the ghosted one, a loop's.
1014
+ const orig = { ...m, state: Object.fromEntries(Object.entries(m.state).filter(([n2]) => !gv.includes(n2))), step: { ...m.step, state: Object.fromEntries(Object.entries(m.step.state ?? {}).filter(([n2]) => !gv.includes(n2))) } };
1015
+ const hlit = () => {
1016
+ const h = hm.history, base = { ...hm, history: undefined };
1017
+ return `⟨${lit(base)},
1018
+ ${c(h.keep)},
1019
+ ${list(h.record.map(f => String(ix(f))))},
1020
+ ${list(h.record.map(f => String(ix(`r.${f}`))))},
1021
+ ${list(Object.entries(h.sums).map(([name, g]) => `(${ix(name)}, ${e(g)})`))},
1022
+ ${list(Object.entries(h.queries).map(([name, g]) => `(${ix(name)}, ${e(g)})`))},
1023
+ ${list((h.relations ?? []).map(rel => { if (rel.holds.op !== '<=')
1024
+ throw new Error(`${id}: a relation is a <= b`); return `(${c(rel.when)}, ${e(rel.holds.a)}, ${e(rel.holds.b)})`; }))}⟩`;
1025
+ };
1026
+ const loopLit = (lp) => `⟨${list(lp.inputs.map(name => String(ix(name))))},
1027
+ ${list(Object.entries(lp.state).map(([name, e2]) => `(${ix(name)}, ${e(e2)})`))},
1028
+ ${list(Object.entries(lp.body).map(([name, e2]) => `(${ix(name)}, ${e(e2)})`))},
1029
+ ${c(lp.while)},
1030
+ ${lp.pre ? c(lp.pre) : TRUE},
1031
+ ${lp.assume ? c(lp.assume) : TRUE}⟩`;
1032
+ const machineDef = rf ? `def ${id}_refined : SRefined := ⟨${lit(rf.impl)},
1033
+ ${lit(rf.spec)},
1034
+ ${list(Object.entries(rf.wit).map(([inp, t2]) => `(${ix(`b_${inp}`)}, ${e(t2)})`))},
1035
+ ${c(rf.stutter)}⟩
1036
+ def ${id}_machine : SMachine := ${id}_refined.full` : gv.length ? `def ${id}_ghosted : SGhosted := ⟨${lit(orig)},
1037
+ ${list(gv.map(v2 => `(${ix(v2)}, ${e(m.state[v2])}, ${e(m.step.state?.[v2] ?? { op: 'var', name: v2 })})`))}⟩
1038
+ def ${id}_machine : SMachine := ${id}_ghosted.full
1039
+ theorem ${id}_ghostOk : sghostOk ${id}_ghosted = true := by ${checkBy}` : hm ? `def ${id}_hist : HMachine := ${hlit()}
1040
+ def ${id}_machine : SMachine := ${id}_hist.plain` : lp0 ? `def ${id}_loop : Loop := ${loopLit(lp0.loop)}
1041
+ def ${id}_machine : SMachine := ${id}_loop.machine` : `def ${id}_machine : SMachine := ${lit(m)}`;
1042
+ const enc = `⟨${['u$', 'k$', 'T$', 'O$', 't$'].map(ix).join(', ')},
1043
+ ${list([...state, ...mq.inputs].map(x => `(${ix(x)}, ${mx(`${x}@`)})`))},
1044
+ ${list([...Object.keys(m.maps ?? {}), ...carried].map(x => `(${mx(x)}, ${mx(`${x}@`)})`))}⟩`;
1045
+ // Each law: Bridge.lean's check and theorem, then all of them as the usual export's `<id>_laws` states them.
1046
+ const lawBlocks = laws.map((l, j) => `-- ${l.name}
1047
+ def ${id}_law${j} : C := ${c(l.holds)}
1048
+ def ${id}_l${j} : MLaw := ⟨${id}_law${j}, ${l.forall ? `some ${ix(l.forall)}` : 'none'}⟩
1049
+ theorem ${id}_law${j}_ok : cyc${Q}MOk ${id}_machine ${id}_enc ${id}_facts ${id}_law${j} ${keysL(l)} ${node(j)} = true := ${mOk}
1050
+ theorem ${id}_law${j}_holds : ∀ ρ M, SReach ${id}_machine ρ M → ∀ ρ' : Nat → Nat, (∀ i, i ∉ ${keysL(l)} → ρ' i = ρ i) → evalC ρ' M ${id}_law${j} = true :=
1051
+ cyc_${q}machine_sound ${id}_machine ${id}_enc ${id}_facts ${id}_nodes ${id}_check ${id}_law${j} ${keysL(l)} ${node(j)} ${node(j)}_mem ${id}_law${j}_ok`).join('\n');
1052
+ const keys2 = (l) => list((l.keys ?? []).map(k2 => String(ix(k2))));
1053
+ const oneOf = (hyp) => `simp only [${id}_mlaws, List.mem_cons, List.not_mem_nil, or_false] at ${hyp}`;
1054
+ const lawsThm = `def ${id}_mlaws : List (MLaw × List Nat) := ${list(laws.map((l, j) => `(${id}_l${j}, ${keys2(l)})`))}
1055
+ theorem ${id}_laws : ∀ ρ M, SReach ${id}_machine ρ M → ∀ e ∈ ${id}_mlaws, holdsOver e.1 M e.2 ρ := by
1056
+ intro ρ M h e he
1057
+ ${laws.length ? `${oneOf('he')}
1058
+ rcases he with ${laws.map(() => 'rfl').join(' | ')}
1059
+ ${laws.map((_, j) => `· exact cyc_machine_holds (${id}_law${j}_holds ρ M h)`).join('\n ')}` : `simp [${id}_mlaws] at he`}`;
1060
+ // A history's record goals (leaf goals, as the usual export has them), and its laws over histories.
1061
+ const histBlock = () => {
1062
+ const records = hplain.goals.map(g => { const out = grow(g); if (!('tree' in out))
1063
+ throw new Error(`${id}: ${g.name} not proven`); const x2 = encode({ name: `${id}: ${g.name}`, goal: g, tree: out.tree }, false, true, vars, maps); return `(${x2.tab}, ${x2.tree})`; });
1064
+ const reach = (hyp) => `(hreach_plain ${id}_hist ${id}_records ${id}_hcheck ρ μ hist ${hyp})`;
1065
+ return `def ${id}_records : List (List VDef × Tree) := ${list(records)}
1066
+ theorem ${id}_hcheck : checkHMachine ${id}_hist [] ${id}_records = true := by ${checkBy}
1067
+ theorem ${id}_hlaws : ∀ ρ M hist, HReach ${id}_hist ρ M hist → ∀ e ∈ ${id}_mlaws, holdsOver e.1 M e.2 ρ :=
1068
+ fun ρ M hist h => ${id}_laws ρ M (hreach_plain ${id}_hist ${id}_records ${id}_hcheck ρ M hist h)
1069
+ ${readable(id, laws, vars, maps, { binders: '(ρ : Nat → Nat) (μ : Nat → Nat → Nat) (hist : List (Nat → Nat))', hyp: `HReach ${id}_hist ρ μ hist`, call: '',
1070
+ have: j => `have hl : holdsOver ${id}_l${j} μ ${keys2(laws[j])} ρ := cyc_machine_holds (${id}_law${j}_holds ρ μ ${reach('h')})` })}`;
1071
+ };
1072
+ // The machine's own step laws, one step or k (BridgeRules.lean's cyc_kstep_sound): the SKStep as the usual export
1073
+ // writes it, with no certificate in it.
1074
+ const stepBlocks = (rf ? [] : stepLaws).map((sl, j) => {
1075
+ const kk = sl.steps ?? 1;
1076
+ const ren = Array.from({ length: kk }, (_, s2) => (s2 === 0 ? '[]' : list(extOf(m).map(v2 => `(${ix(v2)}, ${ix(`${v2}@${s2 + 1}`)})`))));
1077
+ const mren = Array.from({ length: kk }, (_, s2) => (s2 === 0 ? '[]' : list(carried.map(mp => `(${mx(mp)}, ${mx(`${mp}@${s2 + 1}`)})`))));
1078
+ const names = Array.from({ length: kk - 1 }, (_, s2) => list(state.map(n2 => `(${ix(n2)}, ${ix(stateName(n2, s2 + 1))})`)));
1079
+ return `-- Step law${kk > 1 ? ` over ${kk} steps` : ''}: ${sl.name}
1080
+ def ${id}_step${j} : SKStep := ⟨⟨${list(kWhens(mq, sl).map(c))},
1081
+ ${c(inline(sl.then))},
1082
+ ${list(ren)},
1083
+ ${primesL()},
1084
+ ${primeMapsL()},
1085
+ [],
1086
+ ([], .leaf [] [] [] [])⟩,
1087
+ ${list(mren)},
1088
+ [], [], [], [], [],
1089
+ ${list(names)}⟩
1090
+ theorem ${id}_step${j}_ok : cyc${Q}KOk ${id}_machine ${id}_enc ${id}_facts ${id}_step${j} ${node(stepAt[j])} = true := ${kOk}
1091
+ theorem ${id}_step${j}_holds : SKStepHolds ${id}_machine ${id}_step${j} :=
1092
+ ${kstep(`${id}_step${j}`, stepAt[j])} ${id}_step${j}_ok`;
1093
+ });
1094
+ // Each liveness from its step laws (BridgeRules.lean's sleads_of_holds), as the usual export states it.
1095
+ let at = stepLaws.length;
1096
+ const liveBlocks = lives.map(({ l, helps }, j) => {
1097
+ const lid = `${id}_live${j}`, at0 = at, s0 = stepAt[at0], c0 = stepAt[at0 + 1], hAt = (i) => stepAt[at0 + 2 + i];
1098
+ at += 2 + helps.length;
1099
+ // Under weak fairness, where each helpful request is proven allowed in its region (BridgeRules.lean's cyc_allow_sound).
1100
+ const al = allowAt[j], wfp = al ? `
1101
+ ${al.map((a, i) => `def ${lid}_wit${i} : List (Nat × E) := ${list(a.wit.map(([inp, t2]) => `(${ix(inp)}, ${e(t2)})`))}`).join('\n')}
1102
+ theorem ${lid}_allows : ∀ h ∈ ${lid}.helps, ∃ wit : List (Nat × E), distinct ((h.2 ++ wit).map (·.1)) = true
1103
+ ∧ (∀ p ∈ wit, (${id}_machine.f.m.inputs.contains p.1 = true ∧ avoids ${id}_machine.extVars (.eq p.2 p.2) = true) ∧ avoidsMaps ${id}_machine.carried (.eq p.2 p.2) = true)
1104
+ ∧ AllowHolds ${id}_machine (.and (liveHelpWhen ${lid} h) (reqC wit)) := by
1105
+ intro h hh
1106
+ simp only [${lid}, List.mem_cons, List.not_mem_nil, or_false] at hh
1107
+ rcases hh with ${helps.map(() => 'rfl').join(' | ')}
1108
+ ${al.map((a, i) => `· exact ⟨${lid}_wit${i}, by ${checkBy}, by ${checkBy}, cyc_${q}allow_sound ${id}_machine ${id}_enc ${id}_facts ${id}_nodes ${id}_check _ ${node(a.node)} ${node(a.node)}_mem (${aOk})⟩`).join('\n ')}
1109
+ /-- ${l.name}, under weak fairness: along every run that takes each helpful request whenever, from some point on, it
1110
+ stays allowed, \`from\` is followed by \`to\`. -/
1111
+ theorem ${lid}_holds_wf (run : SRun ${id}_machine)
1112
+ (fair : ∀ h ∈ ${lid}.helps, ∀ n, (∀ m, n ≤ m → ∃ ι κ, (∀ p ∈ h.2, ι p.1 = evalE (run.st m).1 (run.st m).2 p.2)
1113
+ ∧ evalC (sView ${id}_machine (run.st m).1 (run.st m).2 ι κ).1 (sView ${id}_machine (run.st m).1 (run.st m).2 ι κ).2 ${id}_machine.f.m.assume = true) →
1114
+ ∃ m, n ≤ m ∧ ∀ p ∈ h.2, (run.req m).1 p.1 = evalE (run.st m).1 (run.st m).2 p.2) :
1115
+ ∀ n, evalC (run.st n).1 (run.st n).2 ${lid}.from_ = true → ∃ m, n ≤ m ∧ evalC (run.st m).1 (run.st m).2 ${lid}.to = true :=
1116
+ sleads_wf_of_holds ${id}_machine ${lid} ${lid}_ok ${lid}_stay ${lid}_cover ${lid}_helps ${lid}_allows run fair` : '';
1117
+ return `-- Liveness: ${l.name}
1118
+ def ${lid} : Live := ${liveLit(l, { e, c }, ix, primesL(), primeMapsL())}
1119
+ theorem ${lid}_ok : sliveOk ${id}_machine ${lid} = true := by ${checkBy}
1120
+ theorem ${lid}_stay : SKStepHolds ${id}_machine (liveK ${lid} ${lid}.pend (liveStayC ${lid})) := ${kstep('_', s0)} (${kOk})
1121
+ theorem ${lid}_cover : SKStepHolds ${id}_machine (liveK ${lid} ${lid}.pend (liveCoverC ${lid})) := ${kstep('_', c0)} (${kOk})
1122
+ theorem ${lid}_helps : ∀ h ∈ ${lid}.helps, SKStepHolds ${id}_machine (liveK ${lid} (liveHelpWhen ${lid} h) (liveHelpThen ${lid})) := by
1123
+ intro h hh
1124
+ simp only [${lid}, List.mem_cons, List.not_mem_nil, or_false] at hh
1125
+ rcases hh with ${helps.map(() => 'rfl').join(' | ')}
1126
+ ${helps.map((_, i) => `· exact ${kstep('_', hAt(i))} (${kOk})`).join('\n ')}
1127
+ /-- ${l.name}: along every run that takes each helpful request infinitely often, \`from\` is followed by \`to\`. -/
1128
+ theorem ${lid}_holds (run : SRun ${id}_machine)
1129
+ (fair : ∀ h ∈ ${lid}.helps, ∀ n, ∃ m, n ≤ m ∧ ∀ p ∈ h.2, (run.req m).1 p.1 = evalE (run.st m).1 (run.st m).2 p.2) :
1130
+ ∀ n, evalC (run.st n).1 (run.st n).2 ${lid}.from_ = true → ∃ m, n ≤ m ∧ evalC (run.st m).1 (run.st m).2 ${lid}.to = true :=
1131
+ sleads_of_holds ${id}_machine ${lid} ${lid}_ok ${lid}_stay ${lid}_cover ${lid}_helps run fair${wfp}`;
1132
+ });
1133
+ const loopBlock = lp0 ? `/-- ${lp0.loop.name} halts: for params meeting its precondition, no run of turns goes on forever. -/
1134
+ theorem ${id}_halts : ∀ maps ρ0, evalC ρ0 maps ${id}_loop.pre = true →
1135
+ ¬ ∃ f : Nat → Nat → Nat, f 0 = applyS ${id}_loop.init ρ0 maps ∧ ∀ n, Turn ${id}_loop maps (f n) (f (n + 1)) :=
1136
+ loop_halts_of ${id}_loop ${id}_live0 rfl rfl (by intro h hh; simp only [${id}_live0, List.mem_singleton] at hh; subst hh; rfl) (by ${checkBy}) ${id}_live0_holds` : '';
1137
+ const ghostBlock = gv.length ? `-- The laws that read no ghost hold of the machine without its ghosts.
1138
+ theorem ${id}_orig_laws : ∀ ρ M, SReach ${id}_ghosted.orig ρ M → ∀ e ∈ ${id}_mlaws, avoids ${id}_ghosted.vars e.1.holds = true → holdsOver e.1 M e.2 ρ := by
1139
+ intro ρ M hr e he hav
1140
+ ${laws.length ? `${oneOf('he')}
1141
+ rcases he with ${laws.map(() => 'rfl').join(' | ')}
1142
+ ${laws.map((l, j) => `· exact cyc_machine_holds (cyc_ghost_holds ${id}_ghosted ${id}_ghostOk ${id}_law${j} ${keysL(l)} hav ${id}_law${j}_holds ρ M hr)`).join('\n ')}` : `simp [${id}_mlaws] at he`}` : '';
1143
+ const refineBlock = rf ? `theorem ${id}_allowed : SKStepHolds ${id}_machine (srStep ${id}_refined ${primesL()} ${primeMapsL()} noSProof) := ${kstep('_', stepAt[0])} (${kOk})
1144
+ theorem ${id}_refines : ∀ ρ M, SReach ${id}_refined.impl ρ M → ∃ ρc Mc σ Ms, SReach ${id}_refined.full ρc Mc ∧ Agree ${id}_refined.SV ρc ρ
1145
+ ∧ (∀ mp, mp ∉ ${id}_refined.SM → Mc mp = M mp) ∧ SReach ${id}_refined.spec σ Ms ∧ Agree ${id}_refined.X σ ρc
1146
+ ∧ (∀ mp, mp ∉ ${id}_refined.IM → mp ∉ ${id}_refined.spec.carried → Ms mp = Mc mp) ∧ ∀ e ∈ ${id}_mlaws, holdsOver e.1 Mc e.2 ρc :=
1147
+ srefines_of_holds ${id}_refined (by ${checkBy}) (by ${checkBy}) _ ${id}_laws
1148
+ (fun ρ M h => by have hp := ${id}_law${rf.specPre}_holds ρ M h ρ (fun _ _ => rfl); exact hp)
1149
+ ${primesL()} ${primeMapsL()} noSProof ${id}_allowed (by ${checkBy}) (by ${checkBy}) (by ${checkBy})` : '';
1150
+ const records = [...stepBlocks, ...(refineBlock ? [refineBlock] : []), ...liveBlocks, ...(loopBlock ? [loopBlock] : []), ...(ghostBlock ? [ghostBlock] : [])];
1151
+ cycTexts++;
1152
+ return { text: `-- ${m.name}: ${laws.map(l => l.name).join('; ')}, as one cyclic proof (its laws' nodes, then ${allSteps.length} step laws')
1153
+ -- (variables ${vars.map((v2, j) => `${j} ${v2}`).join(', ')}; maps ${maps.map((v2, j) => `${j} ${v2}`).join(', ')})
1154
+ ${machineDef}
1155
+ def ${id}_enc : CycEnc := ${enc}
1156
+ ${cycProofText(id, p, [], vars, maps)}
1157
+ ${okLemmas}
1158
+ ${lawBlocks}
1159
+ ${lawsThm}
1160
+ ${hm ? histBlock() : readable(id, laws, vars, maps, { binders: '(ρ : Nat → Nat) (μ : Nat → Nat → Nat)', hyp: `SReach ${id}_machine ρ μ`, call: '',
1161
+ have: j => `have hl : holdsOver ${id}_l${j} μ ${keys2(laws[j])} ρ := cyc_machine_holds (${id}_law${j}_holds ρ μ h)` })}${opts.noFacts ? '' : factsProof(id, m, maps, vars, opts.simp)}${records.length ? `\n\n${records.join('\n\n')}` : ''}${opts.after ? `\n\n${opts.after(ix)}` : ''}` };
1162
+ }
1163
+ // A fold from its cyclic proof (Bridge.lean's cyc_fold_sound): the fold numbered as foldToLean numbers it, each law's
1164
+ // theorem over trees, and all of them as the usual export's `<id>_laws` states them.
1165
+ function cycFoldToLean(id, f, laws) {
1166
+ const gs = foldObligations(f, laws), trees = {};
1167
+ for (const g of gs) {
1168
+ const o = grow(g);
1169
+ if (!('tree' in o))
1170
+ return { why: `${g.name}: not proven the usual way` };
1171
+ trees[g.name] = o.tree;
1172
+ }
1173
+ let p, why = 'no nodes';
1174
+ const until = performance.now() + certMs();
1175
+ for (const base of withDeadline(until, () => [...(routeOf() !== 'search' ? [foldNodes(f, laws, trees)] : []), foldNodes(f, laws)].filter(b => !b.why))) {
1176
+ if (performance.now() > until) {
1177
+ why = `past the certificate's deadline (${certMs()} ms)`;
1178
+ break;
1179
+ }
1180
+ const v = withDeadline(until, () => proveCyclic(base.facts, base.specs));
1181
+ if (v.proven && v.checked) {
1182
+ p = v.proof;
1183
+ break;
1184
+ }
1185
+ why = v.proven ? `not checked (${v.why})` : `${v.node}: ${v.goal}: ${v.stuck}`;
1186
+ }
1187
+ if (!p)
1188
+ return { why };
1189
+ const st = Object.keys(f.leaf), v = (name) => ({ op: 'var', name });
1190
+ const sc = cycScope([p], undefined, [...[...f.params, ...f.inputs, ...st, ...st.map(x => `l_${x}`), ...st.map(x => `r_${x}`), 'n$', 'N$'].map(v),
1191
+ ...Object.values(f.leaf), ...Object.values(f.node), ...laws.map((l) => ({ op: 'if', c: l.holds, then: v('n$'), else: v('n$') })),
1192
+ ...['Kind', 'Left', 'Right', ...st, ...f.inputs].map((x) => ({ op: 'get', map: `${x}@`, key: v('n$') }))]);
1193
+ const vars = [...new Set([...f.params, ...f.inputs, ...st, ...st.map(n2 => `l_${n2}`), ...st.map(n2 => `r_${n2}`), ...sc.vars])], maps = sc.maps;
1194
+ const t = leanTerms(vars, maps, id);
1195
+ cycTexts++;
1196
+ return { text: `-- ${f.name}: ${laws.map(law => law.name).join('; ')}, as one cyclic proof (variables ${vars.map((v2, j) => `${j} ${v2}`).join(', ')}; maps ${maps.map((v2, j) => `${j} ${v2}`).join(', ')})
1197
+ ${cycProofText(id, p, [], vars, maps)}
1198
+ ${cycFoldLean(id, f, laws, vars, maps, t)}
1199
+ def ${id}_flaws : List C := ${list(laws.map((_, j) => `${id}_law${j}`))}
1200
+ theorem ${id}_laws : ∀ maps ρ, evalC ρ maps ${id}_fold.pre = true → ∀ t, Allowed ${id}_fold ρ maps t →
1201
+ ∀ l ∈ ${id}_flaws, evalC (foldT ${id}_fold ρ maps t) maps l = true := by
1202
+ intro maps ρ hp t ht l hl
1203
+ simp only [${id}_flaws, List.mem_cons, List.not_mem_nil, or_false] at hl
1204
+ rcases hl with ${laws.map(() => 'rfl').join(' | ')}
1205
+ ${laws.map((_, j) => `· exact ${id}_law${j}_holds maps ρ hp t ht`).join('\n ')}` };
1206
+ }
833
1207
  // A function defined by a recurrence (core/dsl.ts recur): its Lean definition by the same recurrence, and its lemmas
834
1208
  // proven together by induction (each case by omega, its min, max and ifs split first), and its laws between two points
835
1209
  // by induction on the distance. A function of several arguments is `<name>_rec`, its further arguments first, and the
@@ -916,8 +1290,10 @@ function recurKernel(lean, r, uses, which) {
916
1290
  const cite = (f) => { const j = r.lemmas.findIndex(l => f.name === `${l.name} at x`); return two && j >= 0 ? `(${lean}_lemmas ${args.join(' ')} x)${proj(j, r.lemmas.length)}` : undefined; };
917
1291
  const factsAt = factsProof(id, m, fnNames, vars, [`${id}_ρ`, `${id}_μ`, ...recurDefs], { binders, rho: `(${id}_ρ ${ps})`, mu: `${id}_μ`, cite });
918
1292
  const unfold = [`${id}_machine`, `${id}_μ`, 'applyS', 'assoc', 'lookup', 'evalE', 'evalC', ...recurDefs].join(', ');
1293
+ // (with cyclic certificates, the machine's laws from its cyclic proof, where it has one)
1294
+ const cyc = certsOn ? cycMachineToLean(id, m, pr.laws, { noFacts: true }) : undefined;
919
1295
  const pre = `
920
- ${smachineToLean(id, m, pr.laws, { noFacts: true })}
1296
+ ${cyc && 'text' in cyc ? cyc.text : smachineToLean(id, m, pr.laws, { noFacts: true })}
921
1297
  def ${id}_ρ ${binders} : Nat → Nat := fun i => ${chain(start.map(([n, e]) => [col(n), e]), '0', 'i')}
922
1298
  noncomputable def ${id}_μ : Nat → Nat → Nat := fun j => ${chain(fnNames.map((n, j) => [j, `(${m.fns[n].lean})`]), 'fun _ => 0', 'j')}${factsAt}
923
1299
  -- The run of ${m.name} that ${two ? 'counts y up from x' : 'counts x up from 0'}: reachable, and holding ${two ? 'y = x + n and f(y)' : 'x = n and f(x)'}.
@@ -939,7 +1315,9 @@ theorem ${id}_run ${binders} : ∀ n, ∃ ρ, SReach ${id}_machine ρ ${id}_μ
939
1315
  all_goals simp [sNext, sView, decideFlags, decideQ, flagSub, withInputs, withCarried, ${hs.join(', ')}, ${unfold}]`;
940
1316
  // The lemmas (or the laws between, after the lemmas at y): each the machine's law read on the run.
941
1317
  const own = two ? r.between.map((_, j) => r.lemmas.length + j) : r.lemmas.map((_, j) => j);
942
- const each = (j) => `by have h := ${id}_laws ρ ${id}_μ hr ${id}_e${j} (by simp [${id}_proof]); simp [SEntry.Holds, holdsOver, ${id}_e${j}, ${id}_l${j}, MLaw.Holds, evalC, evalE, ${hs.join(', ')}] at h; simpa using h`;
1318
+ const each = (j) => (cyc && 'text' in cyc
1319
+ ? `by have h := ${id}_law${j}_holds ρ ${id}_μ hr ρ (fun _ _ => rfl); simp [${id}_law${j}, evalC, evalE, ${hs.join(', ')}] at h; simpa using h`
1320
+ : `by have h := ${id}_laws ρ ${id}_μ hr ${id}_e${j} (by simp [${id}_proof]); simp [SEntry.Holds, holdsOver, ${id}_e${j}, ${id}_l${j}, MLaw.Holds, evalC, evalE, ${hs.join(', ')}] at h; simpa using h`);
943
1321
  const proof = (from) => `(obtain ⟨ρ, hr, ${hs.join(', ')}⟩ := ${id}_run ${from}; exact ${own.length === 1 ? `(${each(own[0])})` : `⟨${own.map(j => `(${each(j)})`).join(', ')}⟩`})`;
944
1322
  return { pre, proof };
945
1323
  }
@@ -1068,10 +1446,18 @@ export function hmachineToLean(id, m, laws, tampers = [], corollary) {
1068
1446
  // `decide +kernel` (Lean's kernel alone). Every such theorem then also rests on an axiom of its own that `#print axioms`
1069
1447
  // names (`…_native.native_decide.ax_…`): it trusts Lean's compiler, its runtime and the C compiler. Defaults to off, or
1070
1448
  // to GDP_LEAN_NATIVE=1 (the suite's quick runs); a release's proofs stay on Lean's kernel and its core axioms.
1449
+ // `cyclic`: each machine, loop and fold the cyclic route covers from cyclic certificates (see cycMachineToLean); defaults
1450
+ // to GDP_CERT=cyclic. `certReport` then says which items went which way.
1071
1451
  export function toLean(items, opts = {}) {
1072
- const outer = checkBy;
1452
+ const outer = checkBy, outerCerts = certsOn;
1073
1453
  const env = globalThis.process?.env;
1074
1454
  checkBy = (opts.native ?? env?.GDP_LEAN_NATIVE === '1') ? 'native_decide' : 'decide +kernel';
1455
+ certsOn = opts.cyclic ?? cyclicCerts();
1456
+ certReport.cyclic = [];
1457
+ certReport.usual = [];
1458
+ certReport.translatedSteps = [];
1459
+ cycTexts = 0;
1460
+ usualMade.clear();
1075
1461
  // The proofs found with only the instances their leaves use: the same laws, from fewer hypotheses for Lean to build.
1076
1462
  const wasMinimal = minimal.on;
1077
1463
  minimal.on = true;
@@ -1080,6 +1466,7 @@ export function toLean(items, opts = {}) {
1080
1466
  }
1081
1467
  finally {
1082
1468
  checkBy = outer;
1469
+ certsOn = outerCerts;
1083
1470
  minimal.on = wasMinimal;
1084
1471
  }
1085
1472
  }
@@ -1107,21 +1494,29 @@ function toLeanWith(items, opts) {
1107
1494
  return `${pairing}${text}\n`;
1108
1495
  }).join('');
1109
1496
  for (let item of items) {
1110
- const id = ident(item.id);
1497
+ const id = ident(item.id), tItem = performance.now();
1111
1498
  if ('machine' in item) {
1112
1499
  const m = item.machine;
1113
1500
  // Laws with some key go in as the prover proved them: with its witness in place (the stronger law).
1114
1501
  const someKey = (c) => JSON.stringify(c).includes('"op":"all"');
1115
1502
  if ([...item.laws.map(l => l.holds), ...(item.stepLaws ?? []).map(sl => sl.then)].some(someKey)) {
1116
- const r = proveStepLaws(m, item.laws, item.stepLaws ?? []);
1503
+ const r = proveStepLawsOnce(m, item.laws, item.stepLaws ?? []);
1117
1504
  if (!r.proven)
1118
1505
  throw new Error(`${item.id}: not proven (${r.failed?.stuck ?? ''})`);
1119
1506
  item = { ...item, laws: r.laws ?? item.laws, ...(item.stepLaws ? { stepLaws: r.stepLaws ?? item.stepLaws } : {}) };
1120
1507
  }
1121
1508
  // One twin: every machine goes to Lean as a machine with sets (one with none, if it has none): its laws, step laws,
1122
1509
  // liveness and ghosts in one export; a machine with a history as its plain machine, and its laws over histories.
1123
- parts.push(`-- ${item.id}\n${recurDefs(m)}${item.lean?.defs ? `${item.lean.defs}\n` : ''}${smachineToLean(id, m, item.laws, { stepLaws: item.stepLaws, ...(item.liveness?.length ? { liveness: item.liveness } : {}),
1124
- ...(item.ghosts?.length ? { ghosts: item.ghosts } : {}), ...(item.lean?.simp ? { simp: item.lean.simp } : {}), ...(item.lean?.after ? { after: item.lean.after } : {}) })}`);
1510
+ // With cyclic certificates, from one cyclic proof where the cyclic route covers it.
1511
+ const mopts = { stepLaws: item.stepLaws, ...(item.liveness?.length ? { liveness: item.liveness } : {}),
1512
+ ...(item.ghosts?.length ? { ghosts: item.ghosts } : {}), ...(item.lean?.simp ? { simp: item.lean.simp } : {}), ...(item.lean?.after ? { after: item.lean.after } : {}) };
1513
+ const defs = `${recurDefs(m)}${item.lean?.defs ? `${item.lean.defs}\n` : ''}`;
1514
+ const cyc = certsOn ? cycMachineToLean(id, m, item.laws, mopts) : undefined;
1515
+ if (cyc && 'text' in cyc)
1516
+ certReport.cyclic.push(item.id);
1517
+ else if (cyc)
1518
+ certReport.usual.push({ id: item.id, why: cyc.why });
1519
+ parts.push(`-- ${item.id}\n${defs}${cyc && 'text' in cyc ? cyc.text : smachineToLean(id, m, item.laws, mopts)}`);
1125
1520
  theorems.push(`${id}_laws`, ...(m.history ? [`${id}_hlaws`] : []), ...(item.stepLaws ?? []).map((_, j) => `${id}_step${j}_holds`), ...(item.liveness ?? []).map((_, j) => `${id}_live${j}_holds`));
1126
1521
  if (item.lean?.after)
1127
1522
  theorems.push(...[...item.lean.after(() => 0).matchAll(/^theorem (\S+)/gm)].map(x2 => x2[1]));
@@ -1133,11 +1528,24 @@ function toLeanWith(items, opts) {
1133
1528
  const laws = [...relation, { name: `${spec.name}'s precondition`, holds: spec.pre ?? atMost(n(0), n(0)) }];
1134
1529
  const sl = R.stepLaws[0] ?? { name: `${spec.name} allows the request it is sent`, then: { op: 'or', xs: [stutter, atMost(n(0), n(0))] } };
1135
1530
  const refined = { impl: S.impl, spec: S.spec, wit: witness, stutter, specPre: relation.length }, free = spec.inputs.map(i => `b_${i}`);
1136
- parts.push(`-- ${item.id}\n${smachineToLean(id, R.machine, laws, { stepLaws: [sl], free, refined })}`);
1531
+ // With cyclic certificates: the composite's laws and its step law (Lean's SRefined.allowed, built here as Lean builds
1532
+ // it) from one cyclic proof, through BridgeRules.lean's srefines_of_holds.
1533
+ const cyc = certsOn ? cycMachineToLean(id, R.machine, laws, { free, refined: { ...refined, step: sl } }) : undefined;
1534
+ if (cyc && 'text' in cyc)
1535
+ certReport.cyclic.push(item.id);
1536
+ else if (cyc)
1537
+ certReport.usual.push({ id: item.id, why: cyc.why });
1538
+ parts.push(`-- ${item.id}\n${cyc && 'text' in cyc ? cyc.text : smachineToLean(id, R.machine, laws, { stepLaws: [sl], free, refined })}`);
1137
1539
  theorems.push(`${id}_laws`, `${id}_refines`);
1138
1540
  }
1139
1541
  else if ('loop' in item) {
1140
- parts.push(`-- ${item.id}\n${loopToLean(id, item.loop, item.laws ?? [], item.rank)}`);
1542
+ loopObligations(item.loop, item.laws ?? [], item.rank);
1543
+ const cyc = certsOn ? cycMachineToLean(id, loopMachine(item.loop), item.laws ?? [], { liveness: [loopLiveness(item.loop, item.rank)], loop: { loop: item.loop } }) : undefined;
1544
+ if (cyc && 'text' in cyc)
1545
+ certReport.cyclic.push(item.id);
1546
+ else if (cyc)
1547
+ certReport.usual.push({ id: item.id, why: cyc.why });
1548
+ parts.push(`-- ${item.id}\n${cyc && 'text' in cyc ? cyc.text : loopToLean(id, item.loop, item.laws ?? [], item.rank)}`);
1141
1549
  theorems.push(`${id}_halts`);
1142
1550
  }
1143
1551
  else if ('cyclic' in item) {
@@ -1147,7 +1555,12 @@ function toLeanWith(items, opts) {
1147
1555
  ...(item.after ? (ts => (item.data ? ts.slice(-1).map(t => `${id}.${t}`) : ts))([...item.after(() => 0, () => 0).matchAll(/^theorem (\S+)/gm)].map(x2 => x2[1])) : []));
1148
1556
  }
1149
1557
  else if ('fold' in item) {
1150
- parts.push(`-- ${item.id}\n${foldToLean(id, item.fold, item.laws)}`);
1558
+ const cyc = certsOn ? cycFoldToLean(id, item.fold, item.laws) : undefined;
1559
+ if (cyc && 'text' in cyc)
1560
+ certReport.cyclic.push(item.id);
1561
+ else if (cyc)
1562
+ certReport.usual.push({ id: item.id, why: cyc.why });
1563
+ parts.push(`-- ${item.id}\n${cyc && 'text' in cyc ? cyc.text : foldToLean(id, item.fold, item.laws)}`);
1151
1564
  theorems.push(`${id}_laws`);
1152
1565
  }
1153
1566
  else {
@@ -1156,10 +1569,12 @@ function toLeanWith(items, opts) {
1156
1569
  parts.push(goalToLean({ name: item.id, goal: item.goal, tree: grown(item.goal) }, n, false).replace(new RegExp(`\\bt${n}(_goal|_tree)?\\b`, 'g'), (_, suffix) => `${id}${suffix ?? ''}`));
1157
1570
  theorems.push(id);
1158
1571
  }
1572
+ if (envOf()?.GDP_LEAN_TIMES === '1')
1573
+ console.error(`lean item ${item.id}: ${Math.round(performance.now() - tItem)} ms, ${parts.at(-1)?.length ?? 0} bytes`);
1159
1574
  }
1160
1575
  // The file the items need: Live.lean for a liveness or a loop, Sets.lean (which imports Steps.lean) for any machine.
1161
1576
  return `import History
1162
- ${items.some(item => 'cyclic' in item) ? (items.some(item => 'cyclic' in item && (item.bridge || item.recur || item.fold)) ? 'import Bridge\n' : 'import Cyclic\n') : ''}${items.some(item => 'cyclic' in item && item.data) ? 'import Data\n' : ''}import ${items.some(item => 'refinement' in item) ? 'Refine' : items.some(item => 'machine' in item && item.ghosts?.length) ? 'Ghost' : items.some(item => ('machine' in item && item.liveness?.length) || 'loop' in item) ? 'Live' : items.some(item => 'machine' in item) ? 'Sets' : 'Steps'}
1577
+ ${cycTexts ? 'import BridgeRules\n' : ''}${items.some(item => 'cyclic' in item) ? (items.some(item => 'cyclic' in item && (item.bridge || item.recur || item.fold)) ? 'import Bridge\n' : 'import Cyclic\n') : ''}${items.some(item => 'cyclic' in item && item.data) ? 'import Data\n' : ''}import ${items.some(item => 'refinement' in item) ? 'Refine' : items.some(item => 'machine' in item && item.ghosts?.length) ? 'Ghost' : items.some(item => ('machine' in item && item.liveness?.length) || 'loop' in item) ? 'Live' : items.some(item => 'machine' in item) ? 'Sets' : 'Steps'}
1163
1578
  open Kern Cert
1164
1579
  set_option maxRecDepth 100000
1165
1580
  set_option maxHeartbeats 0
@@ -1200,10 +1615,35 @@ export function cyclicToLean(id, p, tampered = [], after, bridge, recur, fold) {
1200
1615
  ...(fold ? [...Object.values(fold.fold.leaf), ...Object.values(fold.fold.node), ...fold.laws.map((l) => ({ op: 'if', c: l.holds, then: v('n$'), else: v('n$') }))] : []),
1201
1616
  ...(fold ? ['Kind', 'Left', 'Right', ...fnames, ...fold.fold.inputs].map((x) => ({ op: 'get', map: `${x}@`, key: v('n$') })) : [])]);
1202
1617
  const { e, c } = leanTerms(vars, maps, id);
1618
+ const ix = (name) => vars.indexOf(name), mx = (map) => maps.indexOf(map);
1619
+ return `-- ${p.nodes.map(n => n.name).join('; ')} (variables ${vars.map((name, j) => `${j} ${name}`).join(', ')}; maps ${maps.map((name, j) => `${j} ${name}`).join(', ')})
1620
+ ${cycProofText(id, p, tampered, vars, maps)}${bridge ? `\n${cycBridgeLean(id, p, bridge, vars, maps, { e, c })}` : ''}${recur ? `\n${cycRecurLean(id, p, recur, vars, maps, { e, c })}` : ''}${fold ? `\n${cycFoldLean(id, fold.fold, fold.laws, vars, maps, { e, c })}` : ''}${after ? `\n${after(ix, mx)}` : ''}`;
1621
+ }
1622
+ // A cyclic proof as Lean: its facts and nodes (each goal's table and tree, Lean building the goals from the nodes), each
1623
+ // node's membership, the check and cyclic_sound's theorem; each tampered proof's check computed to false.
1624
+ function cycProofText(id, p, tampered, vars, maps) {
1625
+ const { e, c } = leanTerms(vars, maps, id);
1626
+ // (data, not code: no compiled code for Lean to make, unless native_decide runs it)
1627
+ const d = checkBy === 'native_decide' ? 'def' : 'noncomputable def';
1203
1628
  const ix = (name) => vars.indexOf(name);
1204
1629
  const facts = list(p.facts.map(f => `⟨${list(f.keys.map(key => String(ix(key))))}, ${c(f.holds)}⟩`));
1205
1630
  // A tampered proof keeps the proof's tables and trees (made for the proof's own goals) under its changed nodes.
1206
- const enc = (g, tree) => { const x = encode({ name: `${id}: ${g.name}`, goal: g, tree }, false, true, vars, maps); return `(${x.tab}, ${x.tree})`; };
1631
+ // Each table once (`<id>_tabN`): a node's goals (its own, each use's premises and descent) share its hypotheses, so
1632
+ // most of their tables are the same columns, and Lean elaborates each list once instead of once per goal.
1633
+ // So too each term column's term (an open choice the tables share) and each goal's table and tree where two goals
1634
+ // have the same (a premise read at the same terms by two nodes).
1635
+ const shared = (prefix) => {
1636
+ const seen = new Map();
1637
+ return { seen, name: (text) => { let name = seen.get(text); if (!name)
1638
+ seen.set(text, (name = `${id}_${prefix}${seen.size}`)); return name; } };
1639
+ };
1640
+ const terms = shared('term'), tabs = shared('tab'), goals = shared('goal');
1641
+ const enc = (g, tree) => {
1642
+ const x = encode({ name: `${id}: ${g.name}`, goal: g, tree }, false, true, vars, maps, true);
1643
+ const tab = list(x.tabCols.map(col => (col.startsWith('.term (') && col.length > 48 ? `.term ${terms.name(col.slice(6))}` : col)));
1644
+ const pair = `(${tabs.name(tab)}, ${x.tree})`;
1645
+ return pair.length > 200 ? goals.name(pair) : pair;
1646
+ };
1207
1647
  const nodeLits = (q) => q.nodes.map((n, i) => {
1208
1648
  const o = p.nodes[i];
1209
1649
  const uses = n.uses.map((u, j) => {
@@ -1213,22 +1653,24 @@ export function cyclicToLean(id, p, tampered = [], after, bridge, recur, fold) {
1213
1653
  return { hyps: list(n.hyps.map(c)), concl: c(n.concl), rest: `${list(n.rank.map(e))}, ${list(n.insts.map(t => `(${t.fact}, ${list(t.at.map(e))})`))},\n ${list(uses)},\n ${enc(mainGoal(p.facts, p.nodes, o), o.main)}` };
1214
1654
  });
1215
1655
  const nodes = (q) => list(nodeLits(q).map(x => `⟨${x.hyps}, ${x.concl}, ${x.rest}⟩`));
1216
- const mx = (map) => maps.indexOf(map);
1217
- return `-- ${p.nodes.map(n => n.name).join('; ')} (variables ${vars.map((name, j) => `${j} ${name}`).join(', ')}; maps ${maps.map((name, j) => `${j} ${name}`).join(', ')})
1218
- def ${id}_facts : List Fact := ${facts}
1219
- ${nodeLits(p).map((x, i) => `def ${id}_hyps${i} : List C := ${x.hyps}
1220
- def ${id}_concl${i} : C := ${x.concl}
1221
- def ${id}_node${i} : CNode := ⟨${id}_hyps${i}, ${id}_concl${i}, ${x.rest}⟩
1656
+ const own = nodeLits(p), bad = tampered.map(t => nodes(t.proof));
1657
+ return `${d} ${id}_facts : List Fact := ${facts}
1658
+ ${[...terms.seen].map(([t, name]) => `${d} ${name} : E := ${t}`).join('\n')}
1659
+ ${[...tabs.seen].map(([tab, name]) => `${d} ${name} : List VDef := ${tab}`).join('\n')}
1660
+ ${[...goals.seen].map(([g, name]) => `${d} ${name} : List VDef × Tree := ${g}`).join('\n')}
1661
+ ${own.map((x, i) => `${d} ${id}_hyps${i} : List C := ${x.hyps}
1662
+ ${d} ${id}_concl${i} : C := ${x.concl}
1663
+ ${d} ${id}_node${i} : CNode := ⟨${id}_hyps${i}, ${id}_concl${i}, ${x.rest}⟩
1222
1664
  theorem ${id}_node${i}_hyps : ${id}_node${i}.hyps = ${id}_hyps${i} := rfl
1223
1665
  theorem ${id}_node${i}_concl : ${id}_node${i}.concl = ${id}_concl${i} := rfl`).join('\n')}
1224
- def ${id}_nodes : List CNode := ${list(p.nodes.map((_, i) => `${id}_node${i}`))}
1666
+ ${d} ${id}_nodes : List CNode := ${list(p.nodes.map((_, i) => `${id}_node${i}`))}
1225
1667
  ${p.nodes.map((_, i) => `theorem ${id}_node${i}_mem : ${id}_node${i} ∈ ${id}_nodes := by simp only [${id}_nodes, List.mem_cons, true_or, or_true]`).join('\n')}
1226
1668
  theorem ${id}_check : checkCyclic ${id}_facts ${id}_nodes = true := by ${checkBy}
1227
1669
  theorem ${id}_sound : ∀ (maps : Nat → Nat → Nat) env, FactsHold ${id}_facts env maps → ∀ n ∈ ${id}_nodes, (∀ c ∈ n.hyps, evalC env maps c = true) → evalC env maps n.concl = true :=
1228
1670
  cyclic_sound ${id}_facts ${id}_nodes ${id}_check
1229
1671
  ${tampered.map((t, j) => `-- TAMPERED: ${t.what}. Lean computes the check to false.
1230
- def ${id}_tampered${j} : List CNode := ${nodes(t.proof)}
1231
- example : checkCyclic ${id}_facts ${id}_tampered${j} = false := by ${checkBy}`).join('\n')}${bridge ? `\n${cycBridgeLean(id, p, bridge, vars, maps, { e, c })}` : ''}${recur ? `\n${cycRecurLean(id, p, recur, vars, maps, { e, c })}` : ''}${fold ? `\n${cycFoldLean(id, fold.fold, fold.laws, vars, maps, { e, c })}` : ''}${after ? `\n${after(ix, mx)}` : ''}`;
1672
+ ${d} ${id}_tampered${j} : List CNode := ${bad[j]}
1673
+ example : checkCyclic ${id}_facts ${id}_tampered${j} = false := by ${checkBy}`).join('\n')}`;
1232
1674
  }
1233
1675
  function cycScope(all, bridge, extra = []) {
1234
1676
  const names = (t) => [...JSON.stringify(t).matchAll(/"op":"var","name":"([^"]+)"/g)].map(m => m[1]);