gdp-wired 0.18.0 → 0.19.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, cyclicMachine, foldNodes, hasQuantified, kWhens, machineNodes, proveCyclic, qflagNodes, 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 ------------------------------------------------------------------------------------------------------------
@@ -381,30 +382,6 @@ theorem law${j} ${reach.binders} (h : ${reach.hyp}) : ${statement(law)} := by
381
382
  ${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
383
  end ${id}`;
383
384
  }
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
385
  // A liveness as Live.lean's record: from, to, the rank's parts, each helpful request with its region, the primes.
409
386
  function liveLit(l, t, ix, primes, primeMaps) {
410
387
  return `⟨${t.c(l.from)}, ${t.c(l.to)}, ${list(livenessRanks(l).map(t.e))},
@@ -543,6 +520,30 @@ export function machineLit(mm, t, ix, mx) {
543
520
  ${list((mm.facts ?? []).filter(l => !l.keys?.length).map(l => `⟨${c(l.holds)}, ${l.forall ? `some ${ix(l.forall)}` : 'none'}⟩`))},
544
521
  ${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
522
  }
523
+ // A request's extended names: its inputs, each flag that holds at every key and its witnesses (Sets.lean's extVars, in
524
+ // the order the usual export lists a later step's names in).
525
+ const extOf = (m) => [...m.inputs, ...Object.entries(m.step.flags ?? {}).flatMap(([f, c]) => (c.op === 'all' ? [`?${f}`, ...witnessesOf(f, c)] : []))];
526
+ // The names a machine's export numbers first, before those only its goals read: the machine's own (params, inputs, state,
527
+ // flags, the keys of its laws and facts, the keys its writes and quantified flags bind) and its claims' (each step law's
528
+ // key, the state after the step, each later request's names, the state between steps, a history's record). The usual
529
+ // export and the cyclic one (cycMachineToLean) number them alike, so that a machine, a law's statement and a step law are
530
+ // the same Lean text in both.
531
+ function machineNames(m, laws, allSteps, stepLaws, opts) {
532
+ const flags = Object.entries(m.step.flags ?? {}), quantified = flags.filter(([, c]) => c.op === 'all').map(([f]) => f);
533
+ const qwits = (f) => witnessesOf(f, m.step.flags[f]);
534
+ const carried = m.inputMaps ?? [], keysOf = [...new Set([...laws, ...(m.facts ?? [])].flatMap(l => [l.forall, ...(l.keys ?? [])].filter((k) => !!k)))];
535
+ const k = Math.max(1, ...allSteps.map(sl => sl.steps ?? 1));
536
+ const ext = [...m.inputs, ...quantified.flatMap(f => [`?${f}`, ...qwits(f)])];
537
+ const later = Array.from({ length: k - 1 }, (_, j) => ext.map(v => `${v}@${j + 2}`)).flat();
538
+ const laterMaps = Array.from({ length: k - 1 }, (_, j) => carried.map(mp => `${mp}@${j + 2}`)).flat();
539
+ const primes = allSteps.length ? Object.keys(m.state).map(s2 => `${s2}'`) : [];
540
+ const vars = [...new Set([...m.params, ...m.inputs, ...Object.keys(m.state), ...keysOf, ...keysOf.map(k2 => `${k2}#`), '#', ...flags.map(([f]) => `?${f}`),
541
+ ...quantified.flatMap(qwits), ...boundKeys(m), ...(opts.tamper ? boundKeys(opts.tamper.machine) : []), ...stepLaws.flatMap(sl => (sl.forall ? [sl.forall] : [])), ...(opts.free ?? []),
542
+ ...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}`)])];
543
+ const baseMaps = [...new Set([...Object.keys(m.maps ?? {}), ...Object.keys(m.step.maps ?? {}), ...Object.keys(m.fns ?? {})])];
544
+ const maps = [...new Set([...baseMaps, ...carried, ...(allSteps.length ? Object.keys(m.maps ?? {}).map(mp => `${mp}'`) : []), ...laterMaps])];
545
+ return { vars, maps };
546
+ }
546
547
  export function smachineToLean(id, given, laws, opts = {}) {
547
548
  const hm = given.history ? { ...given, history: { ...given.history, prune: undefined } } : undefined;
548
549
  const hplain = hm ? historyMachine(hm) : undefined, stepLaws = opts.stepLaws ?? [], rf = opts.refined, gv = opts.ghosts ?? [];
@@ -645,12 +646,8 @@ theorem ${id}_orig_laws : ∀ ρ M, SReach ${id}_ghosted.orig ρ M → ∀ e ∈
645
646
  const lp = r.proof.laws, si = r.proof.instances;
646
647
  const flags = Object.entries(m.step.flags ?? {}), quantified = flags.filter(([, c]) => c.op === 'all').map(([f]) => f);
647
648
  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));
649
+ const carried = m.inputMaps ?? [], facts = m.facts ?? [];
650
650
  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
651
  const instOf = (sl) => r.proof.perStep?.[sl.name] ?? si;
655
652
  const goalsL = obligations(m, laws, lp.instances), goalsS = allSteps.map(sl => stepLawObligations(m, laws, instOf(sl), sl)[0]);
656
653
  // Weak fairness over sets: each helpful request allowed in its region carrying empty sets (Live.lean's sAllowGoal,
@@ -716,13 +713,9 @@ theorem ${id}_orig_laws : ∀ ρ M, SReach ${id}_ghosted.orig ρ M → ∀ e ∈
716
713
  return r2.proven ? r2 : undefined;
717
714
  })]));
718
715
  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])])];
716
+ const named = machineNames(m, laws, allSteps, stepLaws, { ...opts, ...(hm ? { record: hm.history.record } : {}) });
717
+ const vars = [...new Set([...named.vars, ...[...goalsL, ...goalsS, ...allowGoals, ...(hplain?.goals ?? [])].flatMap(g => g.hyps.flatMap(varsOf))])];
718
+ const mapNames = [...new Set([...named.maps, ...[...JSON.stringify([...goalsL, ...goalsS, ...allowGoals]).matchAll(/"op":"get","map":"([^"]+)"/g)].map(x2 => x2[1])])];
726
719
  const enc = (g, tree) => encode({ name: `${id}: ${g.name}`, goal: g, tree }, false, true, vars, mapNames);
727
720
  const { e, c } = leanTerms(vars, mapNames, id);
728
721
  const ix = (name) => { const j = vars.indexOf(name); if (j < 0)
@@ -830,6 +823,325 @@ ${hm ? histBlock() : readable(id, laws, vars, mapNames, { binders: '(ρ : Nat
830
823
  def ${id}_tampered : SMachine := ${lit(opts.tamper.machine)}
831
824
  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
825
  }
826
+ // --- the cyclic certificate's export (GDP_CERT=cyclic) -------------------------------------------------------------
827
+ // By default, or with GDP_CERT=cyclic (core/cyclic.ts's certOn: a covered proof's certificate is its cyclic proof; or toLean's
828
+ // `cyclic`), toLean exports each machine the cyclic route covers from one cyclic
829
+ // proof (kernel/cyclic.ts) of its laws and of every step law its claims bring (its own step laws and k-step laws, a
830
+ // liveness's), carried to the machine by Bridge.lean's cyc_machine_sound and BridgeRules.lean's cyc_kstep_sound,
831
+ // sleads_of_holds (liveness, loops), hreach_plain (histories) and cyc_ghost_holds (ghosts), and each theorem stated as the
832
+ // usual export states it: the same machine, laws, step laws and liveness (machineNames numbers them alike), the same
833
+ // `<id>.lawN`, `<id>_stepN_holds`, `<id>_liveN_holds`, `<id>_halts`. What the cyclic route doesn't cover goes the usual
834
+ // way: a flag that holds at every key, a law that reads a map at a key read from a map, a refinement, and a liveness's
835
+ // weak fairness (its helpful requests proven allowed: a claim about requests the run doesn't take).
836
+ const envOf = () => globalThis.process?.env;
837
+ export const cyclicCerts = () => envOf()?.GDP_CERT !== 'usual';
838
+ let certsOn = false, cycTexts = 0;
839
+ // What the last toLean exported from cyclic certificates, and what went the usual way (and why).
840
+ export const certReport = { cyclic: [], usual: [], translatedSteps: [] };
841
+ // Each item's cyclic certificate is made within one deadline (GDP_CERT_MS, 10 s by default), every goal it grows (the
842
+ // translation's leaves grown again, the searched nodes) bounded by it: past it, the item goes the usual way.
843
+ function cycMachineProof(m, laws, allSteps, allows = []) {
844
+ const r = proveStepLaws(m, laws, allSteps);
845
+ if (!r.proven || !r.proof)
846
+ return { why: 'not proven the usual way' };
847
+ const until = performance.now() + certMs();
848
+ return withDeadline(until, () => cycMachineProofBy(m, laws, allSteps, allows, r, until));
849
+ }
850
+ function cycMachineProofBy(m, laws, allSteps, allows, r, until) {
851
+ if (!r.proof)
852
+ return { why: 'not proven the usual way' };
853
+ const lp = r.proof.laws, instOf = (sl) => r.proof.perStep?.[sl.name] ?? r.proof.instances;
854
+ // A machine with flags that hold at every key: its sk machine's nodes, the flags' closing facts read where each node
855
+ // reads the run.
856
+ const q = hasQuantified(m), mm = skMachine(m);
857
+ const L = laws.length, mine = (facts, lawNodes) => searchedStepNodes(m, laws, allSteps.map(sl => ({ law: sl, instances: instOf(sl) })), facts, lawNodes);
858
+ const tries = [];
859
+ if (!q && routeOf() !== 'search' && allSteps.length) {
860
+ 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] }; });
861
+ const tr = usualSteps.every(u => u.tree) ? machineNodes(m, laws, lp.instances, { trees: lp.trees, steps: usualSteps }) : undefined;
862
+ if (tr && !tr.why && tr.specs.length === 3 * L + 2 * allSteps.length) {
863
+ // (either time form cycKOk reads: one step back at a time, or t − n at once)
864
+ 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));
865
+ const fits = allSteps.every((_, j) => forms.some(own => {
866
+ const a = tr.specs[3 * L + 2 * j + 1], b = own[j], hs = new Set(b.hyps.map(id));
867
+ return id(a.concl) === id(b.concl) && a.hyps.every(h => hs.has(id(h)));
868
+ }));
869
+ if (fits)
870
+ tries.push({ facts: tr.facts, specs: tr.specs, lawNodes: tr.specs, stepAt: allSteps.map((_, j) => 3 * L + 2 * j + 1), translated: true });
871
+ }
872
+ }
873
+ for (const base of (q ? [qflagNodes(m, laws, lp.instances)]
874
+ : [...(routeOf() !== 'search' ? [machineNodes(m, laws, lp.instances, { trees: lp.trees })] : []), machineNodes(m, laws, lp.instances)]).filter(b => !b.why)) {
875
+ tries.push({ facts: base.facts, specs: [...base.specs, ...mine(base.facts, base.specs)], lawNodes: base.specs, stepAt: allSteps.map((_, j) => base.specs.length + j) });
876
+ }
877
+ let last = 'no nodes';
878
+ for (const { facts, specs: specs0, lawNodes, stepAt, translated } of tries) {
879
+ if (performance.now() > until)
880
+ return { why: `past the certificate's deadline (${certMs()} ms)` };
881
+ let specs = specs0, v = proveCyclic(facts, specs);
882
+ if (!(v.proven && v.checked)) {
883
+ last = performance.now() > until ? `past the certificate's deadline (${certMs()} ms)` : v.proven ? `not checked (${v.why})` : `${v.node}: ${v.goal}: ${v.stuck}`;
884
+ continue;
885
+ }
886
+ const allowAt = allows.map((group) => {
887
+ if ((lawNodes[0]?.rank.length ?? 2) < 2)
888
+ return undefined;
889
+ const before = specs, out = [];
890
+ for (const a of group) {
891
+ const found = withWitness(m, a.request, wit => {
892
+ if (performance.now() > until)
893
+ return undefined;
894
+ 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) : []);
895
+ const v2 = proveCyclic(facts, [...specs, spec]);
896
+ return v2.proven && v2.checked ? { spec, v2 } : undefined;
897
+ });
898
+ if (!found) {
899
+ specs = before;
900
+ return undefined;
901
+ }
902
+ out.push({ node: specs.length, wit: found.wit });
903
+ specs = [...specs, found.out.spec];
904
+ v = found.out.v2;
905
+ }
906
+ return out;
907
+ });
908
+ if (v.proven && v.checked) {
909
+ if (translated)
910
+ certReport.translatedSteps.push(m.name);
911
+ return { p: v.proof, stepAt, allowAt };
912
+ }
913
+ }
914
+ return { why: last };
915
+ }
916
+ // A machine (with a history, ghosts, a liveness or as a loop's) from one cyclic proof, or why it can't be.
917
+ export function cycMachineToLean(id, given, laws, opts = {}) {
918
+ const hm = given.history ? { ...given, history: { ...given.history, prune: undefined } } : undefined;
919
+ const hplain = hm ? historyMachine(hm) : undefined, m = hplain ? hplain.machine : given;
920
+ const stepLaws = opts.stepLaws ?? [], gv = opts.ghosts ?? [], lp0 = opts.loop;
921
+ if (!cyclicMachine(skMachine(m), laws))
922
+ return { why: 'a law reading a map at a key read from a map' };
923
+ // With flags that hold at every key, the sk machine's encoding and BridgeRules.lean's q bridges.
924
+ const mq = skMachine(m), Q = hasQuantified(m) ? 'Q' : '', q = hasQuantified(m) ? 'q' : '';
925
+ const lives = (opts.liveness ?? []).map(l => ({ l, ...livenessParts(m, l) }));
926
+ // A refinement's step law: the spec stutters or allows the request it is sent, its `then` Lean's SRefined.allowed (the
927
+ // spec's assumption at the witness, the composite's flags inlined).
928
+ const rf = opts.refined;
929
+ 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: {},
930
+ flags: Object.fromEntries(Object.keys(m.step.flags ?? {}).map(f => [f, { op: 'flag', name: f }])) })] } };
931
+ const allSteps = [...(rfStep ? [rfStep] : stepLaws), ...lives.flatMap(x => [x.stay, x.cover, ...x.helps])];
932
+ const made = cycMachineProof(m, laws, allSteps, lives.map(x => x.helps.map((hs, i) => ({ when: hs.when, request: livenessHelpful(x.l)[i].request }))));
933
+ if ('why' in made)
934
+ return made;
935
+ const { p, stepAt, allowAt } = made;
936
+ const named = machineNames(m, laws, allSteps, stepLaws, { ...(opts.free ? { free: opts.free } : {}), ...(hm ? { record: hm.history.record } : {}) });
937
+ const sc = cycScope([p], { machine: mq, laws });
938
+ const vars = [...new Set([...named.vars, ...sc.vars])], maps = [...new Set([...named.maps, ...sc.maps])];
939
+ const { e, c } = leanTerms(vars, maps, id);
940
+ const ix = (name) => { const j = vars.indexOf(name); if (j < 0)
941
+ throw new Error(`${id}: no variable for ${name}`); return j; };
942
+ const mx = (name) => { const j = maps.indexOf(name); if (j < 0)
943
+ throw new Error(`${id}: no map for ${name}`); return j; };
944
+ const lit = (mm) => machineLit(mm, { e, c }, ix, mx);
945
+ const inline = (c2) => substC(c2, { vars: {}, maps: {}, flags: flagsOfM(m) });
946
+ const keysOf = (l) => [...(l.forall ? [l.forall] : []), ...(l.keys ?? [])];
947
+ const keysL = (l) => list(keysOf(l).map(k2 => String(ix(k2))));
948
+ const state = Object.keys(m.state), carried = m.inputMaps ?? [];
949
+ const primesL = () => list(state.map(s2 => `(${ix(s2)}, ${ix(`${s2}'`)})`)), primeMapsL = () => list(Object.keys(m.maps ?? {}).map(mp => `(${mx(mp)}, ${mx(`${mp}'`)})`));
950
+ const node = (i) => `${id}_node${i}`;
951
+ const kstep = (sk, i) => `cyc_${q}kstep_sound ${id}_machine ${id}_enc ${id}_facts ${id}_nodes ${id}_check ${sk} ${node(i)} ${node(i)}_mem`;
952
+ // The machine as the usual export defines it: with a history its plain machine, with ghosts the ghosted one, a loop's.
953
+ 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))) } };
954
+ const hlit = () => {
955
+ const h = hm.history, base = { ...hm, history: undefined };
956
+ return `⟨${lit(base)},
957
+ ${c(h.keep)},
958
+ ${list(h.record.map(f => String(ix(f))))},
959
+ ${list(h.record.map(f => String(ix(`r.${f}`))))},
960
+ ${list(Object.entries(h.sums).map(([name, g]) => `(${ix(name)}, ${e(g)})`))},
961
+ ${list(Object.entries(h.queries).map(([name, g]) => `(${ix(name)}, ${e(g)})`))},
962
+ ${list((h.relations ?? []).map(rel => { if (rel.holds.op !== '<=')
963
+ throw new Error(`${id}: a relation is a <= b`); return `(${c(rel.when)}, ${e(rel.holds.a)}, ${e(rel.holds.b)})`; }))}⟩`;
964
+ };
965
+ const loopLit = (lp) => `⟨${list(lp.inputs.map(name => String(ix(name))))},
966
+ ${list(Object.entries(lp.state).map(([name, e2]) => `(${ix(name)}, ${e(e2)})`))},
967
+ ${list(Object.entries(lp.body).map(([name, e2]) => `(${ix(name)}, ${e(e2)})`))},
968
+ ${c(lp.while)},
969
+ ${lp.pre ? c(lp.pre) : TRUE},
970
+ ${lp.assume ? c(lp.assume) : TRUE}⟩`;
971
+ const machineDef = rf ? `def ${id}_refined : SRefined := ⟨${lit(rf.impl)},
972
+ ${lit(rf.spec)},
973
+ ${list(Object.entries(rf.wit).map(([inp, t2]) => `(${ix(`b_${inp}`)}, ${e(t2)})`))},
974
+ ${c(rf.stutter)}⟩
975
+ def ${id}_machine : SMachine := ${id}_refined.full` : gv.length ? `def ${id}_ghosted : SGhosted := ⟨${lit(orig)},
976
+ ${list(gv.map(v2 => `(${ix(v2)}, ${e(m.state[v2])}, ${e(m.step.state?.[v2] ?? { op: 'var', name: v2 })})`))}⟩
977
+ def ${id}_machine : SMachine := ${id}_ghosted.full
978
+ theorem ${id}_ghostOk : sghostOk ${id}_ghosted = true := by ${checkBy}` : hm ? `def ${id}_hist : HMachine := ${hlit()}
979
+ def ${id}_machine : SMachine := ${id}_hist.plain` : lp0 ? `def ${id}_loop : Loop := ${loopLit(lp0.loop)}
980
+ def ${id}_machine : SMachine := ${id}_loop.machine` : `def ${id}_machine : SMachine := ${lit(m)}`;
981
+ const enc = `⟨${['u$', 'k$', 'T$', 'O$', 't$'].map(ix).join(', ')},
982
+ ${list([...state, ...mq.inputs].map(x => `(${ix(x)}, ${mx(`${x}@`)})`))},
983
+ ${list([...Object.keys(m.maps ?? {}), ...carried].map(x => `(${mx(x)}, ${mx(`${x}@`)})`))}⟩`;
984
+ // Each law: Bridge.lean's check and theorem, then all of them as the usual export's `<id>_laws` states them.
985
+ const lawBlocks = laws.map((l, j) => `-- ${l.name}
986
+ def ${id}_law${j} : C := ${c(l.holds)}
987
+ def ${id}_l${j} : MLaw := ⟨${id}_law${j}, ${l.forall ? `some ${ix(l.forall)}` : 'none'}⟩
988
+ theorem ${id}_law${j}_ok : cyc${Q}MOk ${id}_machine ${id}_enc ${id}_facts ${id}_law${j} ${keysL(l)} ${node(j)} = true := by ${checkBy}
989
+ theorem ${id}_law${j}_holds : ∀ ρ M, SReach ${id}_machine ρ M → ∀ ρ' : Nat → Nat, (∀ i, i ∉ ${keysL(l)} → ρ' i = ρ i) → evalC ρ' M ${id}_law${j} = true :=
990
+ 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');
991
+ const keys2 = (l) => list((l.keys ?? []).map(k2 => String(ix(k2))));
992
+ const oneOf = (hyp) => `simp only [${id}_mlaws, List.mem_cons, List.not_mem_nil, or_false] at ${hyp}`;
993
+ const lawsThm = `def ${id}_mlaws : List (MLaw × List Nat) := ${list(laws.map((l, j) => `(${id}_l${j}, ${keys2(l)})`))}
994
+ theorem ${id}_laws : ∀ ρ M, SReach ${id}_machine ρ M → ∀ e ∈ ${id}_mlaws, holdsOver e.1 M e.2 ρ := by
995
+ intro ρ M h e he
996
+ ${laws.length ? `${oneOf('he')}
997
+ rcases he with ${laws.map(() => 'rfl').join(' | ')}
998
+ ${laws.map((_, j) => `· exact cyc_machine_holds (${id}_law${j}_holds ρ M h)`).join('\n ')}` : `simp [${id}_mlaws] at he`}`;
999
+ // A history's record goals (leaf goals, as the usual export has them), and its laws over histories.
1000
+ const histBlock = () => {
1001
+ const records = hplain.goals.map(g => { const out = grow(g); if (!('tree' in out))
1002
+ 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})`; });
1003
+ const reach = (hyp) => `(hreach_plain ${id}_hist ${id}_records ${id}_hcheck ρ μ hist ${hyp})`;
1004
+ return `def ${id}_records : List (List VDef × Tree) := ${list(records)}
1005
+ theorem ${id}_hcheck : checkHMachine ${id}_hist [] ${id}_records = true := by ${checkBy}
1006
+ theorem ${id}_hlaws : ∀ ρ M hist, HReach ${id}_hist ρ M hist → ∀ e ∈ ${id}_mlaws, holdsOver e.1 M e.2 ρ :=
1007
+ fun ρ M hist h => ${id}_laws ρ M (hreach_plain ${id}_hist ${id}_records ${id}_hcheck ρ M hist h)
1008
+ ${readable(id, laws, vars, maps, { binders: '(ρ : Nat → Nat) (μ : Nat → Nat → Nat) (hist : List (Nat → Nat))', hyp: `HReach ${id}_hist ρ μ hist`, call: '',
1009
+ have: j => `have hl : holdsOver ${id}_l${j} μ ${keys2(laws[j])} ρ := cyc_machine_holds (${id}_law${j}_holds ρ μ ${reach('h')})` })}`;
1010
+ };
1011
+ // The machine's own step laws, one step or k (BridgeRules.lean's cyc_kstep_sound): the SKStep as the usual export
1012
+ // writes it, with no certificate in it.
1013
+ const stepBlocks = (rf ? [] : stepLaws).map((sl, j) => {
1014
+ const kk = sl.steps ?? 1;
1015
+ const ren = Array.from({ length: kk }, (_, s2) => (s2 === 0 ? '[]' : list(extOf(m).map(v2 => `(${ix(v2)}, ${ix(`${v2}@${s2 + 1}`)})`))));
1016
+ const mren = Array.from({ length: kk }, (_, s2) => (s2 === 0 ? '[]' : list(carried.map(mp => `(${mx(mp)}, ${mx(`${mp}@${s2 + 1}`)})`))));
1017
+ const names = Array.from({ length: kk - 1 }, (_, s2) => list(state.map(n2 => `(${ix(n2)}, ${ix(stateName(n2, s2 + 1))})`)));
1018
+ return `-- Step law${kk > 1 ? ` over ${kk} steps` : ''}: ${sl.name}
1019
+ def ${id}_step${j} : SKStep := ⟨⟨${list(kWhens(mq, sl).map(c))},
1020
+ ${c(inline(sl.then))},
1021
+ ${list(ren)},
1022
+ ${primesL()},
1023
+ ${primeMapsL()},
1024
+ [],
1025
+ ([], .leaf [] [] [] [])⟩,
1026
+ ${list(mren)},
1027
+ [], [], [], [], [],
1028
+ ${list(names)}⟩
1029
+ theorem ${id}_step${j}_ok : cyc${Q}KOk ${id}_machine ${id}_enc ${id}_facts ${id}_step${j} ${node(stepAt[j])} = true := by ${checkBy}
1030
+ theorem ${id}_step${j}_holds : SKStepHolds ${id}_machine ${id}_step${j} :=
1031
+ ${kstep(`${id}_step${j}`, stepAt[j])} ${id}_step${j}_ok`;
1032
+ });
1033
+ // Each liveness from its step laws (BridgeRules.lean's sleads_of_holds), as the usual export states it.
1034
+ let at = stepLaws.length;
1035
+ const liveBlocks = lives.map(({ l, helps }, j) => {
1036
+ const lid = `${id}_live${j}`, at0 = at, s0 = stepAt[at0], c0 = stepAt[at0 + 1], hAt = (i) => stepAt[at0 + 2 + i];
1037
+ at += 2 + helps.length;
1038
+ // Under weak fairness, where each helpful request is proven allowed in its region (BridgeRules.lean's cyc_allow_sound).
1039
+ const al = allowAt[j], wfp = al ? `
1040
+ ${al.map((a, i) => `def ${lid}_wit${i} : List (Nat × E) := ${list(a.wit.map(([inp, t2]) => `(${ix(inp)}, ${e(t2)})`))}`).join('\n')}
1041
+ theorem ${lid}_allows : ∀ h ∈ ${lid}.helps, ∃ wit : List (Nat × E), distinct ((h.2 ++ wit).map (·.1)) = true
1042
+ ∧ (∀ 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)
1043
+ ∧ AllowHolds ${id}_machine (.and (liveHelpWhen ${lid} h) (reqC wit)) := by
1044
+ intro h hh
1045
+ simp only [${lid}, List.mem_cons, List.not_mem_nil, or_false] at hh
1046
+ rcases hh with ${helps.map(() => 'rfl').join(' | ')}
1047
+ ${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 (by ${checkBy})⟩`).join('\n ')}
1048
+ /-- ${l.name}, under weak fairness: along every run that takes each helpful request whenever, from some point on, it
1049
+ stays allowed, \`from\` is followed by \`to\`. -/
1050
+ theorem ${lid}_holds_wf (run : SRun ${id}_machine)
1051
+ (fair : ∀ h ∈ ${lid}.helps, ∀ n, (∀ m, n ≤ m → ∃ ι κ, (∀ p ∈ h.2, ι p.1 = evalE (run.st m).1 (run.st m).2 p.2)
1052
+ ∧ 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) →
1053
+ ∃ m, n ≤ m ∧ ∀ p ∈ h.2, (run.req m).1 p.1 = evalE (run.st m).1 (run.st m).2 p.2) :
1054
+ ∀ 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 :=
1055
+ sleads_wf_of_holds ${id}_machine ${lid} ${lid}_ok ${lid}_stay ${lid}_cover ${lid}_helps ${lid}_allows run fair` : '';
1056
+ return `-- Liveness: ${l.name}
1057
+ def ${lid} : Live := ${liveLit(l, { e, c }, ix, primesL(), primeMapsL())}
1058
+ theorem ${lid}_ok : sliveOk ${id}_machine ${lid} = true := by ${checkBy}
1059
+ theorem ${lid}_stay : SKStepHolds ${id}_machine (liveK ${lid} ${lid}.pend (liveStayC ${lid})) := ${kstep('_', s0)} (by ${checkBy})
1060
+ theorem ${lid}_cover : SKStepHolds ${id}_machine (liveK ${lid} ${lid}.pend (liveCoverC ${lid})) := ${kstep('_', c0)} (by ${checkBy})
1061
+ theorem ${lid}_helps : ∀ h ∈ ${lid}.helps, SKStepHolds ${id}_machine (liveK ${lid} (liveHelpWhen ${lid} h) (liveHelpThen ${lid})) := by
1062
+ intro h hh
1063
+ simp only [${lid}, List.mem_cons, List.not_mem_nil, or_false] at hh
1064
+ rcases hh with ${helps.map(() => 'rfl').join(' | ')}
1065
+ ${helps.map((_, i) => `· exact ${kstep('_', hAt(i))} (by ${checkBy})`).join('\n ')}
1066
+ /-- ${l.name}: along every run that takes each helpful request infinitely often, \`from\` is followed by \`to\`. -/
1067
+ theorem ${lid}_holds (run : SRun ${id}_machine)
1068
+ (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) :
1069
+ ∀ 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 :=
1070
+ sleads_of_holds ${id}_machine ${lid} ${lid}_ok ${lid}_stay ${lid}_cover ${lid}_helps run fair${wfp}`;
1071
+ });
1072
+ const loopBlock = lp0 ? `/-- ${lp0.loop.name} halts: for params meeting its precondition, no run of turns goes on forever. -/
1073
+ theorem ${id}_halts : ∀ maps ρ0, evalC ρ0 maps ${id}_loop.pre = true →
1074
+ ¬ ∃ f : Nat → Nat → Nat, f 0 = applyS ${id}_loop.init ρ0 maps ∧ ∀ n, Turn ${id}_loop maps (f n) (f (n + 1)) :=
1075
+ 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` : '';
1076
+ const ghostBlock = gv.length ? `-- The laws that read no ghost hold of the machine without its ghosts.
1077
+ 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
1078
+ intro ρ M hr e he hav
1079
+ ${laws.length ? `${oneOf('he')}
1080
+ rcases he with ${laws.map(() => 'rfl').join(' | ')}
1081
+ ${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`}` : '';
1082
+ const refineBlock = rf ? `theorem ${id}_allowed : SKStepHolds ${id}_machine (srStep ${id}_refined ${primesL()} ${primeMapsL()} noSProof) := ${kstep('_', stepAt[0])} (by ${checkBy})
1083
+ theorem ${id}_refines : ∀ ρ M, SReach ${id}_refined.impl ρ M → ∃ ρc Mc σ Ms, SReach ${id}_refined.full ρc Mc ∧ Agree ${id}_refined.SV ρc ρ
1084
+ ∧ (∀ mp, mp ∉ ${id}_refined.SM → Mc mp = M mp) ∧ SReach ${id}_refined.spec σ Ms ∧ Agree ${id}_refined.X σ ρc
1085
+ ∧ (∀ mp, mp ∉ ${id}_refined.IM → mp ∉ ${id}_refined.spec.carried → Ms mp = Mc mp) ∧ ∀ e ∈ ${id}_mlaws, holdsOver e.1 Mc e.2 ρc :=
1086
+ srefines_of_holds ${id}_refined (by ${checkBy}) (by ${checkBy}) _ ${id}_laws
1087
+ (fun ρ M h => by have hp := ${id}_law${rf.specPre}_holds ρ M h ρ (fun _ _ => rfl); exact hp)
1088
+ ${primesL()} ${primeMapsL()} noSProof ${id}_allowed (by ${checkBy}) (by ${checkBy}) (by ${checkBy})` : '';
1089
+ const records = [...stepBlocks, ...(refineBlock ? [refineBlock] : []), ...liveBlocks, ...(loopBlock ? [loopBlock] : []), ...(ghostBlock ? [ghostBlock] : [])];
1090
+ cycTexts++;
1091
+ return { text: `-- ${m.name}: ${laws.map(l => l.name).join('; ')}, as one cyclic proof (its laws' nodes, then ${allSteps.length} step laws')
1092
+ -- (variables ${vars.map((v2, j) => `${j} ${v2}`).join(', ')}; maps ${maps.map((v2, j) => `${j} ${v2}`).join(', ')})
1093
+ ${machineDef}
1094
+ def ${id}_enc : CycEnc := ${enc}
1095
+ ${cycProofText(id, p, [], vars, maps)}
1096
+ ${lawBlocks}
1097
+ ${lawsThm}
1098
+ ${hm ? histBlock() : readable(id, laws, vars, maps, { binders: '(ρ : Nat → Nat) (μ : Nat → Nat → Nat)', hyp: `SReach ${id}_machine ρ μ`, call: '',
1099
+ 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)}` : ''}` };
1100
+ }
1101
+ // A fold from its cyclic proof (Bridge.lean's cyc_fold_sound): the fold numbered as foldToLean numbers it, each law's
1102
+ // theorem over trees, and all of them as the usual export's `<id>_laws` states them.
1103
+ function cycFoldToLean(id, f, laws) {
1104
+ const gs = foldObligations(f, laws), trees = {};
1105
+ for (const g of gs) {
1106
+ const o = grow(g);
1107
+ if (!('tree' in o))
1108
+ return { why: `${g.name}: not proven the usual way` };
1109
+ trees[g.name] = o.tree;
1110
+ }
1111
+ let p, why = 'no nodes';
1112
+ const until = performance.now() + certMs();
1113
+ for (const base of withDeadline(until, () => [...(routeOf() !== 'search' ? [foldNodes(f, laws, trees)] : []), foldNodes(f, laws)].filter(b => !b.why))) {
1114
+ if (performance.now() > until) {
1115
+ why = `past the certificate's deadline (${certMs()} ms)`;
1116
+ break;
1117
+ }
1118
+ const v = withDeadline(until, () => proveCyclic(base.facts, base.specs));
1119
+ if (v.proven && v.checked) {
1120
+ p = v.proof;
1121
+ break;
1122
+ }
1123
+ why = v.proven ? `not checked (${v.why})` : `${v.node}: ${v.goal}: ${v.stuck}`;
1124
+ }
1125
+ if (!p)
1126
+ return { why };
1127
+ const st = Object.keys(f.leaf), v = (name) => ({ op: 'var', name });
1128
+ const sc = cycScope([p], undefined, [...[...f.params, ...f.inputs, ...st, ...st.map(x => `l_${x}`), ...st.map(x => `r_${x}`), 'n$', 'N$'].map(v),
1129
+ ...Object.values(f.leaf), ...Object.values(f.node), ...laws.map((l) => ({ op: 'if', c: l.holds, then: v('n$'), else: v('n$') })),
1130
+ ...['Kind', 'Left', 'Right', ...st, ...f.inputs].map((x) => ({ op: 'get', map: `${x}@`, key: v('n$') }))]);
1131
+ const vars = [...new Set([...f.params, ...f.inputs, ...st, ...st.map(n2 => `l_${n2}`), ...st.map(n2 => `r_${n2}`), ...sc.vars])], maps = sc.maps;
1132
+ const t = leanTerms(vars, maps, id);
1133
+ cycTexts++;
1134
+ 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(', ')})
1135
+ ${cycProofText(id, p, [], vars, maps)}
1136
+ ${cycFoldLean(id, f, laws, vars, maps, t)}
1137
+ def ${id}_flaws : List C := ${list(laws.map((_, j) => `${id}_law${j}`))}
1138
+ theorem ${id}_laws : ∀ maps ρ, evalC ρ maps ${id}_fold.pre = true → ∀ t, Allowed ${id}_fold ρ maps t →
1139
+ ∀ l ∈ ${id}_flaws, evalC (foldT ${id}_fold ρ maps t) maps l = true := by
1140
+ intro maps ρ hp t ht l hl
1141
+ simp only [${id}_flaws, List.mem_cons, List.not_mem_nil, or_false] at hl
1142
+ rcases hl with ${laws.map(() => 'rfl').join(' | ')}
1143
+ ${laws.map((_, j) => `· exact ${id}_law${j}_holds maps ρ hp t ht`).join('\n ')}` };
1144
+ }
833
1145
  // A function defined by a recurrence (core/dsl.ts recur): its Lean definition by the same recurrence, and its lemmas
834
1146
  // proven together by induction (each case by omega, its min, max and ifs split first), and its laws between two points
835
1147
  // by induction on the distance. A function of several arguments is `<name>_rec`, its further arguments first, and the
@@ -916,8 +1228,10 @@ function recurKernel(lean, r, uses, which) {
916
1228
  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
1229
  const factsAt = factsProof(id, m, fnNames, vars, [`${id}_ρ`, `${id}_μ`, ...recurDefs], { binders, rho: `(${id}_ρ ${ps})`, mu: `${id}_μ`, cite });
918
1230
  const unfold = [`${id}_machine`, `${id}_μ`, 'applyS', 'assoc', 'lookup', 'evalE', 'evalC', ...recurDefs].join(', ');
1231
+ // (with cyclic certificates, the machine's laws from its cyclic proof, where it has one)
1232
+ const cyc = certsOn ? cycMachineToLean(id, m, pr.laws, { noFacts: true }) : undefined;
919
1233
  const pre = `
920
- ${smachineToLean(id, m, pr.laws, { noFacts: true })}
1234
+ ${cyc && 'text' in cyc ? cyc.text : smachineToLean(id, m, pr.laws, { noFacts: true })}
921
1235
  def ${id}_ρ ${binders} : Nat → Nat := fun i => ${chain(start.map(([n, e]) => [col(n), e]), '0', 'i')}
922
1236
  noncomputable def ${id}_μ : Nat → Nat → Nat := fun j => ${chain(fnNames.map((n, j) => [j, `(${m.fns[n].lean})`]), 'fun _ => 0', 'j')}${factsAt}
923
1237
  -- 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 +1253,9 @@ theorem ${id}_run ${binders} : ∀ n, ∃ ρ, SReach ${id}_machine ρ ${id}_μ
939
1253
  all_goals simp [sNext, sView, decideFlags, decideQ, flagSub, withInputs, withCarried, ${hs.join(', ')}, ${unfold}]`;
940
1254
  // The lemmas (or the laws between, after the lemmas at y): each the machine's law read on the run.
941
1255
  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`;
1256
+ const each = (j) => (cyc && 'text' in cyc
1257
+ ? `by have h := ${id}_law${j}_holds ρ ${id}_μ hr ρ (fun _ _ => rfl); simp [${id}_law${j}, evalC, evalE, ${hs.join(', ')}] at h; simpa using h`
1258
+ : `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
1259
  const proof = (from) => `(obtain ⟨ρ, hr, ${hs.join(', ')}⟩ := ${id}_run ${from}; exact ${own.length === 1 ? `(${each(own[0])})` : `⟨${own.map(j => `(${each(j)})`).join(', ')}⟩`})`;
944
1260
  return { pre, proof };
945
1261
  }
@@ -1068,10 +1384,17 @@ export function hmachineToLean(id, m, laws, tampers = [], corollary) {
1068
1384
  // `decide +kernel` (Lean's kernel alone). Every such theorem then also rests on an axiom of its own that `#print axioms`
1069
1385
  // names (`…_native.native_decide.ax_…`): it trusts Lean's compiler, its runtime and the C compiler. Defaults to off, or
1070
1386
  // to GDP_LEAN_NATIVE=1 (the suite's quick runs); a release's proofs stay on Lean's kernel and its core axioms.
1387
+ // `cyclic`: each machine, loop and fold the cyclic route covers from cyclic certificates (see cycMachineToLean); defaults
1388
+ // to GDP_CERT=cyclic. `certReport` then says which items went which way.
1071
1389
  export function toLean(items, opts = {}) {
1072
- const outer = checkBy;
1390
+ const outer = checkBy, outerCerts = certsOn;
1073
1391
  const env = globalThis.process?.env;
1074
1392
  checkBy = (opts.native ?? env?.GDP_LEAN_NATIVE === '1') ? 'native_decide' : 'decide +kernel';
1393
+ certsOn = opts.cyclic ?? cyclicCerts();
1394
+ certReport.cyclic = [];
1395
+ certReport.usual = [];
1396
+ certReport.translatedSteps = [];
1397
+ cycTexts = 0;
1075
1398
  // The proofs found with only the instances their leaves use: the same laws, from fewer hypotheses for Lean to build.
1076
1399
  const wasMinimal = minimal.on;
1077
1400
  minimal.on = true;
@@ -1080,6 +1403,7 @@ export function toLean(items, opts = {}) {
1080
1403
  }
1081
1404
  finally {
1082
1405
  checkBy = outer;
1406
+ certsOn = outerCerts;
1083
1407
  minimal.on = wasMinimal;
1084
1408
  }
1085
1409
  }
@@ -1120,8 +1444,16 @@ function toLeanWith(items, opts) {
1120
1444
  }
1121
1445
  // One twin: every machine goes to Lean as a machine with sets (one with none, if it has none): its laws, step laws,
1122
1446
  // 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 } : {}) })}`);
1447
+ // With cyclic certificates, from one cyclic proof where the cyclic route covers it.
1448
+ const mopts = { stepLaws: item.stepLaws, ...(item.liveness?.length ? { liveness: item.liveness } : {}),
1449
+ ...(item.ghosts?.length ? { ghosts: item.ghosts } : {}), ...(item.lean?.simp ? { simp: item.lean.simp } : {}), ...(item.lean?.after ? { after: item.lean.after } : {}) };
1450
+ const defs = `${recurDefs(m)}${item.lean?.defs ? `${item.lean.defs}\n` : ''}`;
1451
+ const cyc = certsOn ? cycMachineToLean(id, m, item.laws, mopts) : undefined;
1452
+ if (cyc && 'text' in cyc)
1453
+ certReport.cyclic.push(item.id);
1454
+ else if (cyc)
1455
+ certReport.usual.push({ id: item.id, why: cyc.why });
1456
+ parts.push(`-- ${item.id}\n${defs}${cyc && 'text' in cyc ? cyc.text : smachineToLean(id, m, item.laws, mopts)}`);
1125
1457
  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
1458
  if (item.lean?.after)
1127
1459
  theorems.push(...[...item.lean.after(() => 0).matchAll(/^theorem (\S+)/gm)].map(x2 => x2[1]));
@@ -1133,11 +1465,24 @@ function toLeanWith(items, opts) {
1133
1465
  const laws = [...relation, { name: `${spec.name}'s precondition`, holds: spec.pre ?? atMost(n(0), n(0)) }];
1134
1466
  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
1467
  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 })}`);
1468
+ // With cyclic certificates: the composite's laws and its step law (Lean's SRefined.allowed, built here as Lean builds
1469
+ // it) from one cyclic proof, through BridgeRules.lean's srefines_of_holds.
1470
+ const cyc = certsOn ? cycMachineToLean(id, R.machine, laws, { free, refined: { ...refined, step: sl } }) : undefined;
1471
+ if (cyc && 'text' in cyc)
1472
+ certReport.cyclic.push(item.id);
1473
+ else if (cyc)
1474
+ certReport.usual.push({ id: item.id, why: cyc.why });
1475
+ parts.push(`-- ${item.id}\n${cyc && 'text' in cyc ? cyc.text : smachineToLean(id, R.machine, laws, { stepLaws: [sl], free, refined })}`);
1137
1476
  theorems.push(`${id}_laws`, `${id}_refines`);
1138
1477
  }
1139
1478
  else if ('loop' in item) {
1140
- parts.push(`-- ${item.id}\n${loopToLean(id, item.loop, item.laws ?? [], item.rank)}`);
1479
+ loopObligations(item.loop, item.laws ?? [], item.rank);
1480
+ const cyc = certsOn ? cycMachineToLean(id, loopMachine(item.loop), item.laws ?? [], { liveness: [loopLiveness(item.loop, item.rank)], loop: { loop: item.loop } }) : undefined;
1481
+ if (cyc && 'text' in cyc)
1482
+ certReport.cyclic.push(item.id);
1483
+ else if (cyc)
1484
+ certReport.usual.push({ id: item.id, why: cyc.why });
1485
+ parts.push(`-- ${item.id}\n${cyc && 'text' in cyc ? cyc.text : loopToLean(id, item.loop, item.laws ?? [], item.rank)}`);
1141
1486
  theorems.push(`${id}_halts`);
1142
1487
  }
1143
1488
  else if ('cyclic' in item) {
@@ -1147,7 +1492,12 @@ function toLeanWith(items, opts) {
1147
1492
  ...(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
1493
  }
1149
1494
  else if ('fold' in item) {
1150
- parts.push(`-- ${item.id}\n${foldToLean(id, item.fold, item.laws)}`);
1495
+ const cyc = certsOn ? cycFoldToLean(id, item.fold, item.laws) : undefined;
1496
+ if (cyc && 'text' in cyc)
1497
+ certReport.cyclic.push(item.id);
1498
+ else if (cyc)
1499
+ certReport.usual.push({ id: item.id, why: cyc.why });
1500
+ parts.push(`-- ${item.id}\n${cyc && 'text' in cyc ? cyc.text : foldToLean(id, item.fold, item.laws)}`);
1151
1501
  theorems.push(`${id}_laws`);
1152
1502
  }
1153
1503
  else {
@@ -1159,7 +1509,7 @@ function toLeanWith(items, opts) {
1159
1509
  }
1160
1510
  // The file the items need: Live.lean for a liveness or a loop, Sets.lean (which imports Steps.lean) for any machine.
1161
1511
  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'}
1512
+ ${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
1513
  open Kern Cert
1164
1514
  set_option maxRecDepth 100000
1165
1515
  set_option maxHeartbeats 0
@@ -1200,6 +1550,14 @@ export function cyclicToLean(id, p, tampered = [], after, bridge, recur, fold) {
1200
1550
  ...(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
1551
  ...(fold ? ['Kind', 'Left', 'Right', ...fnames, ...fold.fold.inputs].map((x) => ({ op: 'get', map: `${x}@`, key: v('n$') })) : [])]);
1202
1552
  const { e, c } = leanTerms(vars, maps, id);
1553
+ const ix = (name) => vars.indexOf(name), mx = (map) => maps.indexOf(map);
1554
+ return `-- ${p.nodes.map(n => n.name).join('; ')} (variables ${vars.map((name, j) => `${j} ${name}`).join(', ')}; maps ${maps.map((name, j) => `${j} ${name}`).join(', ')})
1555
+ ${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)}` : ''}`;
1556
+ }
1557
+ // A cyclic proof as Lean: its facts and nodes (each goal's table and tree, Lean building the goals from the nodes), each
1558
+ // node's membership, the check and cyclic_sound's theorem; each tampered proof's check computed to false.
1559
+ function cycProofText(id, p, tampered, vars, maps) {
1560
+ const { e, c } = leanTerms(vars, maps, id);
1203
1561
  const ix = (name) => vars.indexOf(name);
1204
1562
  const facts = list(p.facts.map(f => `⟨${list(f.keys.map(key => String(ix(key))))}, ${c(f.holds)}⟩`));
1205
1563
  // A tampered proof keeps the proof's tables and trees (made for the proof's own goals) under its changed nodes.
@@ -1213,9 +1571,7 @@ export function cyclicToLean(id, p, tampered = [], after, bridge, recur, fold) {
1213
1571
  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
1572
  });
1215
1573
  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}
1574
+ return `def ${id}_facts : List Fact := ${facts}
1219
1575
  ${nodeLits(p).map((x, i) => `def ${id}_hyps${i} : List C := ${x.hyps}
1220
1576
  def ${id}_concl${i} : C := ${x.concl}
1221
1577
  def ${id}_node${i} : CNode := ⟨${id}_hyps${i}, ${id}_concl${i}, ${x.rest}⟩
@@ -1228,7 +1584,7 @@ theorem ${id}_sound : ∀ (maps : Nat → Nat → Nat) env, FactsHold ${id}_fact
1228
1584
  cyclic_sound ${id}_facts ${id}_nodes ${id}_check
1229
1585
  ${tampered.map((t, j) => `-- TAMPERED: ${t.what}. Lean computes the check to false.
1230
1586
  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)}` : ''}`;
1587
+ example : checkCyclic ${id}_facts ${id}_tampered${j} = false := by ${checkBy}`).join('\n')}`;
1232
1588
  }
1233
1589
  function cycScope(all, bridge, extra = []) {
1234
1590
  const names = (t) => [...JSON.stringify(t).matchAll(/"op":"var","name":"([^"]+)"/g)].map(m => m[1]);