gdp-wired 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  import { atMost, n, not, pointWrite } from "../core/machine.js";
2
2
  import { add, k, scale, sub } from "../core/solver.js";
3
- import { canon, historyMachine, foldObligations, leafConstraints, linear, livenessHelpful, livenessParts, livenessRanks, loopObligations, trueC, obligations, progressObligations, stepLawObligations, substC, withProducts } from "../kernel/kernel.js";
3
+ import { canon, columnTerm, stateName, termColumn, historyMachine, foldObligations, leafConstraints, loopLiveness, loopMachine, linear, livenessHelpful, livenessParts, livenessRanks, loopObligations, trueC, obligations, progressObligations, stepLawObligations, substC, withProducts } from "../kernel/kernel.js";
4
4
  import { grow, proveBuilt, proveLaws, proveStepLaws } from "../kernel/prove.js";
5
5
  import { refinement, refinementSides } from "../core/compose.js";
6
6
  import { leanTerms } from "./lean-terms.js";
@@ -52,11 +52,13 @@ export function encode(ex, tamper = false, dropStep = true, globalVars, globalMa
52
52
  // Columns: every variable of the hypotheses, then every cell any leaf uses (and the cells inside their keys),
53
53
  // shallower cells first so a key reads only earlier columns.
54
54
  const vars = globalVars ?? [...new Set(goal.hyps.flatMap(varsOf))];
55
- const cells = new Set();
55
+ const cells = new Set(), terms = new Set();
56
56
  const addCell = (name) => { if (cells.has(name))
57
57
  return; cells.add(name); for (const key of Object.keys(cellKey(name)))
58
58
  if (key.includes('['))
59
- addCell(key); };
59
+ addCell(key);
60
+ else if (key.startsWith('⟦'))
61
+ terms.add(key); };
60
62
  // Monomials: every one a leaf's rows or products use, or that a product term has under the leaf's path.
61
63
  const monos = new Set();
62
64
  const subterms = (c) => {
@@ -77,11 +79,29 @@ export function encode(ex, tamper = false, dropStep = true, globalVars, globalMa
77
79
  wc(c);
78
80
  return out2;
79
81
  };
82
+ // Every choice in the hypotheses has its column, whether or not a leaf's rows keep it (a column can cancel, T − T,
83
+ // yet Lean's linear form of T still needs it): each distinct subterm once, as a run's states are shared.
84
+ const seenTerms = new WeakSet();
85
+ const choices = (t) => {
86
+ if (seenTerms.has(t))
87
+ return;
88
+ seenTerms.add(t);
89
+ if (t.op === 'sub' || t.op === 'min' || t.op === 'max' || t.op === 'if')
90
+ terms.add(termColumn(t));
91
+ for (const val of Object.values(t))
92
+ if (Array.isArray(val))
93
+ val.forEach(x => choices(x));
94
+ else if (typeof val === 'object' && val !== null)
95
+ choices(val);
96
+ };
97
+ goal.hyps.forEach(choices);
80
98
  leaves(tree, new Map(), (leaf, path) => {
81
99
  for (const r of withProducts(leafConstraints(goal, path), leaf.products ?? []))
82
100
  for (const key of Object.keys(r.lin)) {
83
101
  if (key.includes('['))
84
102
  addCell(key);
103
+ else if (key.startsWith('⟦'))
104
+ terms.add(key);
85
105
  else if (key.includes('*'))
86
106
  monos.add(key);
87
107
  }
@@ -93,7 +113,8 @@ export function encode(ex, tamper = false, dropStep = true, globalVars, globalMa
93
113
  monos.add(key);
94
114
  });
95
115
  const depth = (name) => name.split('[').length;
96
- const cols = [...vars, ...[...monos].sort((a, b) => a.split('*').length - b.split('*').length), ...[...cells].sort((a, b) => depth(a) - depth(b))];
116
+ // Term columns (open choices, each standing for its term's value) before the cells, whose keys may read them.
117
+ const cols = [...vars, ...[...monos].sort((a, b) => a.split('*').length - b.split('*').length), ...[...terms].sort((a, b) => Number(a.slice(1, -1)) - Number(b.slice(1, -1))), ...[...cells].sort((a, b) => depth(a) - depth(b))];
97
118
  const col = new Map(cols.map((name, j) => [name, j]));
98
119
  const maps = globalMaps ?? [...new Set([...[...cells].map(cellMap), ...goal.hyps.flatMap(h => [...JSON.stringify(h).matchAll(/"op":"get","map":"([^"]+)"/g)].map(m => m[1]))])];
99
120
  const dense = (lin) => {
@@ -110,11 +131,12 @@ export function encode(ex, tamper = false, dropStep = true, globalVars, globalMa
110
131
  return coefs;
111
132
  };
112
133
  const row = (lin) => `⟨${list(dense(lin).map(int))}, ${int(lin['1'] ?? 0)}⟩`;
113
- const tab = cols.map(name => (name.includes('[') ? `.cell ${maps.indexOf(cellMap(name))} ${row(cellKey(name))}`
114
- : name.includes('*') ? `.mono ${list(name.split('*').map(f => { const j = vars.indexOf(f); if (j < 0)
115
- throw new Error(`${ex.name}: factor ${f}`); return j; }).sort((a, b) => a - b).map(String))}`
116
- : `.var ${vars.indexOf(name)}`));
117
134
  const { e, c } = leanTerms(vars, maps, ex.name);
135
+ const tab = cols.map(name => (name.includes('[') ? `.cell ${maps.indexOf(cellMap(name))} ${row(cellKey(name))}`
136
+ : name.startsWith('⟦') ? `.term (${e(columnTerm(name))})`
137
+ : name.includes('*') ? `.mono ${list(name.split('*').map(f => { const j = vars.indexOf(f); if (j < 0)
138
+ throw new Error(`${ex.name}: factor ${f}`); return j; }).sort((a, b) => a - b).map(String))}`
139
+ : `.var ${vars.indexOf(name)}`));
118
140
  let leafNo = 0;
119
141
  const lean = (t, path) => {
120
142
  if (!('leaf' in t)) {
@@ -222,36 +244,11 @@ theorem ${id}_laws : ∀ maps ρ, Reach ${id}_machine maps ρ → ∀ l ∈ ${id
222
244
  export const TRUE = '.le (.const 0) (.const 0)';
223
245
  export const grown = (g) => { const o = grow(g); if (!('tree' in o))
224
246
  throw new Error(`${g.name}: stuck`); return o.tree; };
225
- // Loops through Kern.loop_sound: Lean builds the invariant and rank goals from the loop; the trees are the kernel's.
247
+ // Loops through Kern.loop_halts_of: the loop's machine (Live.lean's Loop.machine) with its invariants as laws, and its
248
+ // halting as a liveness (kernel.ts's loopMachine, loopLiveness); the trees are the kernel's.
226
249
  export function loopToLean(id, l, laws, rank, tamper) {
227
- const vars = [...l.params, ...l.inputs, ...Object.keys(l.state)];
228
- const gs = loopObligations(l, laws, rank);
229
- const enc = (g, tree = grown(g)) => encode({ name: `${id}: ${g.name}`, goal: g, tree }, false, true, vars);
230
- const any0 = enc(gs[0], { leaf: [] });
231
- const ix = (name) => vars.indexOf(name);
232
- const lit = (lp) => `⟨${list(lp.inputs.map(name => String(ix(name))))},
233
- ${list(Object.entries(lp.state).map(([name, e]) => `(${ix(name)}, ${any0.e(e)})`))},
234
- ${list(Object.entries(lp.body).map(([name, e]) => `(${ix(name)}, ${any0.e(e)})`))},
235
- ${any0.c(lp.while)},
236
- ${lp.pre ? any0.c(lp.pre) : TRUE},
237
- ${lp.assume ? any0.c(lp.assume) : TRUE}⟩`;
238
- const proof = laws.map((law, j) => {
239
- const s2 = enc(gs[2 * j]), t = enc(gs[2 * j + 1]);
240
- return `(${any0.c(law.holds)},\n (${s2.tab}, ${s2.tree}),\n (${t.tab}, ${t.tree}))`;
241
- });
242
- const r = enc(gs.at(-1));
243
- const ranks = Array.isArray(rank) ? rank : [rank];
244
- return `-- ${l.name}: halts with ranks ${JSON.stringify(ranks.length)}${laws.length ? `, invariants ${laws.map(law => law.name).join('; ')}` : ''} (variables ${vars.map((v2, j) => `${j} ${v2}`).join(', ')})
245
- def ${id}_loop : Loop := ${lit(l)}
246
- def ${id}_proof : LoopProof := ⟨${list(proof)},
247
- ${list(ranks.map(any0.e))},
248
- (${r.tab}, ${r.tree})⟩
249
- theorem ${id}_halts : ∀ maps ρ0, evalC ρ0 maps ${id}_loop.pre = true →
250
- ¬ ∃ f : Nat → Nat → Nat, f 0 = applyS ${id}_loop.init ρ0 maps ∧ ∀ n, Turn ${id}_loop maps (f n) (f (n + 1)) :=
251
- loop_sound ${id}_loop ${id}_proof (by decide +kernel)${tamper ? `
252
- -- TAMPERED: ${tamper.what}. Lean computes the check to false.
253
- def ${id}_tampered : Loop := ${lit(tamper.loop)}
254
- example : checkLoop ${id}_tampered ${id}_proof = false := by decide +kernel` : ''}`;
250
+ loopObligations(l, laws, rank);
251
+ return smachineToLean(id, loopMachine(l), laws, { liveness: [loopLiveness(l, rank)], loop: { loop: l, ...(tamper ? { tamper } : {}) } });
255
252
  }
256
253
  // Folds through Kern.fold_sound: Lean builds the leaf and node goals; the trees are the kernel's.
257
254
  export function foldToLean(id, f, laws, extra = '') {
@@ -419,7 +416,8 @@ theorem ${pc.id}_holds : ProgressHolds ${id}_machine ${pc.id} :=
419
416
  def ${pc.id}_tampered : Progress := ${record(pc.tamper.when)}
420
417
  example : checkProgress ${id}_machine ${id}_proof ${pc.id}_tampered = false := by decide +kernel` : ''}`;
421
418
  });
422
- // Step laws through Kern.steplaw_sound: `when` and `then` sent with the flags inlined (as kernel.ts's goal has them).
419
+ // Step laws through Kern.steplaw_sound: `when` and `then` sent with the flags inlined (as kernel.ts's goal has them);
420
+ // a law over several steps goes through smachineToLean.
423
421
  const sp = opts.stepLaws?.length ? proveStepLaws(m, laws, opts.stepLaws) : undefined;
424
422
  if (opts.stepLaws?.length && (!sp?.proven || !sp.proof))
425
423
  throw new Error(`${id}: step laws not proven`);
@@ -430,9 +428,11 @@ example : checkProgress ${id}_machine ${id}_proof ${pc.id}_tampered = false := b
430
428
  const insts = laws.flatMap((l, li) => (l.forall ? (sp.proof.instances[l.name] ?? []).map(key => `(${li}, ${any0.e(key)})`) : []));
431
429
  const primes = list(Object.keys(m.state).map(s2 => `(${ix(s2)}, ${ix(`${s2}'`)})`)), primeMaps = list(baseMaps.map(mp => `(${mapNames.indexOf(mp)}, ${mapNames.indexOf(`${mp}'`)})`));
432
430
  const k = sl.steps ?? 1;
433
- if (k === 1 && !Array.isArray(sl.when))
434
- return `-- Step law: ${sl.name}
435
- def ${id}_step${j} : StepLaw := ⟨${sl.when ? any0.c(inlineFlags(sl.when)) : TRUE},
431
+ const when = Array.isArray(sl.when) ? sl.when[0] : sl.when;
432
+ if (k !== 1)
433
+ throw new Error(`${id}: step law ${sl.name}: a law over several steps goes through smachineToLean (Sets.lean's skstep_sound)`);
434
+ return `-- Step law: ${sl.name}
435
+ def ${id}_step${j} : StepLaw := ⟨${when ? any0.c(inlineFlags(when)) : TRUE},
436
436
  ${any0.c(inlineFlags(sl.then))},
437
437
  ${primes},
438
438
  ${primeMaps},
@@ -440,19 +440,6 @@ def ${id}_step${j} : StepLaw := ⟨${sl.when ? any0.c(inlineFlags(sl.when)) : TR
440
440
  (${pr.tab}, ${pr.tree})⟩
441
441
  theorem ${id}_step${j}_holds : StepLawHolds ${id}_machine ${id}_step${j} :=
442
442
  steplaw_sound ${id}_machine ${id}_proof ${id}_step${j} ${id}_check (by decide +kernel)`;
443
- // Through Kern.kstep_sound (Steps.lean): one `when` per step (0 <= 0 where none), the j-th request's inputs renamed.
444
- const whens = sl.when === undefined ? [] : Array.isArray(sl.when) ? sl.when : [sl.when];
445
- const ren = Array.from({ length: k }, (_, s2) => (s2 === 0 ? '[]' : list(m.inputs.map(i => `(${ix(i)}, ${ix(`${i}@${s2 + 1}`)})`))));
446
- return `-- Step law over ${k} steps: ${sl.name}
447
- def ${id}_step${j} : KStep := ⟨${list(Array.from({ length: k }, (_, s2) => (whens[s2] ? any0.c(inlineFlags(whens[s2])) : TRUE)))},
448
- ${any0.c(inlineFlags(sl.then))},
449
- ${list(ren)},
450
- ${primes},
451
- ${primeMaps},
452
- ${list(insts)},
453
- (${pr.tab}, ${pr.tree})⟩
454
- theorem ${id}_step${j}_holds : KStepHolds ${id}_machine ${id}_step${j} :=
455
- kstep_sound ${id}_machine ${id}_proof ${id}_step${j} ${id}_check (by decide +kernel)`;
456
443
  });
457
444
  const machineDef = `def ${id}_machine : FMMachine := ${lit(m)}`;
458
445
  return `-- ${m.name}: ${laws.map(l => l.name).join('; ')}${flagNames.length ? `, with flags ${flagNames.join(', ')}` : ''} (variables ${vars.map((v2, j) => `${j} ${v2}`).join(', ')}; maps ${mapNames.map((v2, j) => `${j} ${v2}`).join(', ')})
@@ -481,7 +468,13 @@ const flagsOfM = (m) => {
481
468
  export const hasSets = (m) => !!m.inputMaps?.length || Object.values(m.step.flags ?? {}).some(c => c.op === 'all') || Object.values(m.step.maps ?? {}).some(w => 'each' in w)
482
469
  || !!Object.keys(m.fns ?? {}).length || !!m.facts?.length;
483
470
  const boundKeys = (m) => [...Object.values(m.step.flags ?? {}).flatMap(c => (c.op === 'all' ? [c.key] : [])), ...Object.values(m.step.maps ?? {}).flatMap(w => ('each' in w ? [w.each] : []))];
484
- export function smachineToLean(id, m, laws, opts = {}) {
471
+ // A machine with a history goes as its plain machine (kernel.ts's historyMachine: sums as state, queries as inputs,
472
+ // the relations assumed), built in Lean from the machine and its history (History.lean's HMachine.plain), so every claim
473
+ // below is about it; with the relations' record goals, its laws hold over histories too (hmachine_sound).
474
+ // `historyTampers`: the history with other relations, each refused.
475
+ export function smachineToLean(id, given, laws, opts = {}) {
476
+ const hm = given.history ? { ...given, history: { ...given.history, prune: undefined } } : undefined;
477
+ const hplain = hm ? historyMachine(hm) : undefined, m = hplain ? hplain.machine : given;
485
478
  const stepLaws = opts.stepLaws ?? [], rf = opts.refined, gv = opts.ghosts ?? [];
486
479
  const claims = [];
487
480
  const claimSteps = () => claims.flatMap(cl => cl.steps);
@@ -504,7 +497,8 @@ def ${id}_step${j} : SKStep := ⟨⟨${list(Array.from({ length: kk }, (_, s2) =
504
497
  ${list(qinsts(inst))},
505
498
  ${list(finsts(inst))},
506
499
  ${list(pinsts(inst))},
507
- []⟩
500
+ [],
501
+ ${list(Array.from({ length: kk - 1 }, (_, s2) => list(Object.keys(m.state).map(n2 => `(${ix(n2)}, ${ix(stateName(n2, s2 + 1))})`))))}⟩
508
502
  theorem ${id}_step${j}_holds : SKStepHolds ${id}_machine ${id}_step${j} :=
509
503
  skstep_sound ${id}_machine ${id}_proof ${id}_step${j} ${id}_check (by decide +kernel)`;
510
504
  } }));
@@ -554,6 +548,19 @@ theorem ${lid}_holds (run : SRun ${id}_machine)
554
548
  sleads_sound ${id}_machine ${id}_proof ${lid} _ _ _ ${id}_check ${lid}_ok ${lid}_stay ${lid}_cover ${lid}_helps run fair${wf}`;
555
549
  } });
556
550
  }
551
+ // A loop (Live.lean's loop_halts_of): its machine is the loop's, and its liveness says the loop halts.
552
+ const lp0 = opts.loop;
553
+ if (lp0)
554
+ claims.push({ steps: [], allows: [], render: () => `/-- ${lp0.loop.name} halts: for params meeting its precondition, no run of turns goes on forever. -/
555
+ theorem ${id}_halts : ∀ maps ρ0, evalC ρ0 maps ${id}_loop.pre = true →
556
+ ¬ ∃ f : Nat → Nat → Nat, f 0 = applyS ${id}_loop.init ρ0 maps ∧ ∀ n, Turn ${id}_loop maps (f n) (f (n + 1)) :=
557
+ 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 decide +kernel) ${id}_live0_holds${lp0.tamper ? `
558
+ -- TAMPERED: ${lp0.tamper.what}. Lean computes the checks to false.
559
+ def ${id}_tampered : SMachine := Loop.machine ${loopLit(lp0.tamper.loop)}
560
+ example : (checkSMachine ${id}_tampered ${id}_proof && sliveOk ${id}_tampered ${id}_live0
561
+ && checkSKStep ${id}_tampered ${id}_proof (sliveStep ${id}_live0 ${id}_live0.pend (liveStayC ${id}_live0) ${id}_live0_sp)
562
+ && checkSKStep ${id}_tampered ${id}_proof (sliveStep ${id}_live0 ${id}_live0.pend (liveCoverC ${id}_live0) ${id}_live0_cp)
563
+ && sliveHelpsOk ${id}_tampered ${id}_proof ${id}_live0 ${id}_live0_hp) = false := by decide +kernel` : ''}` });
557
564
  // Ghosts (Ghost.lean's sghost_laws): the laws that read no ghost hold of the machine without its ghosts.
558
565
  if (gv.length)
559
566
  claims.push({ steps: [], allows: [], render: () => `-- The laws that read no ghost hold of the machine without its ghosts.
@@ -625,7 +632,8 @@ theorem ${id}_orig_laws : ∀ ρ M, SReach ${id}_ghosted.orig ρ M → ∀ e ∈
625
632
  const allowGoals = [...allowed.values()].flatMap(a => (a ? [a.out.goal] : []));
626
633
  const vars = [...new Set([...m.params, ...m.inputs, ...Object.keys(m.state), ...keysOf, ...keysOf.map(k2 => `${k2}#`), '#', ...flags.map(([f]) => `?${f}`),
627
634
  ...quantified.map(f => `w?${f}`), ...boundKeys(m), ...(opts.tamper ? boundKeys(opts.tamper.machine) : []), ...stepLaws.flatMap(sl => (sl.forall ? [sl.forall] : [])), ...(opts.free ?? []),
628
- ...primes, ...later, ...[...goalsL, ...goalsS, ...allowGoals].flatMap(g => g.hyps.flatMap(varsOf))])];
635
+ ...primes, ...later, ...Array.from({ length: k - 1 }, (_, j) => Object.keys(m.state).map(n2 => stateName(n2, j + 1))).flat(),
636
+ ...(hm ? hm.history.record.map(f => `r.${f}`) : []), ...[...goalsL, ...goalsS, ...allowGoals, ...(hplain?.goals ?? [])].flatMap(g => g.hyps.flatMap(varsOf))])];
629
637
  const baseMaps = [...new Set([...Object.keys(m.maps ?? {}), ...Object.keys(m.step.maps ?? {}), ...Object.keys(m.fns ?? {})])];
630
638
  const mapNames = [...new Set([...baseMaps, ...carried, ...(allSteps.length ? Object.keys(m.maps ?? {}).map(mp => `${mp}'`) : []), ...laterMaps,
631
639
  ...[...JSON.stringify([...goalsL, ...goalsS, ...allowGoals]).matchAll(/"op":"get","map":"([^"]+)"/g)].map(x2 => x2[1])])];
@@ -651,6 +659,39 @@ theorem ${id}_orig_laws : ∀ ρ M, SReach ${id}_ghosted.orig ρ M → ∀ e ∈
651
659
  ${list(fs.flatMap(([f, c2]) => (c2.op === 'all' ? [`(${ix(`?${f}`)}, ${ix(c2.key)}, ${c(c2.c)})`] : [])))},
652
660
  ${list(fs.flatMap(([f, c2]) => (c2.op === 'all' ? [`(${ix(`?${f}`)}, ${ix(`w?${f}`)})`] : [])))},
653
661
  ${list((mm.facts ?? []).map(l => `⟨${c(l.holds)}, ${l.forall ? `some ${ix(l.forall)}` : 'none'}⟩`))}⟩`;
662
+ };
663
+ // A loop as Live.lean's record: inputs, start, body, while, precondition, assumption.
664
+ const loopLit = (lp) => `⟨${list(lp.inputs.map(name => String(ix(name))))},
665
+ ${list(Object.entries(lp.state).map(([name, e2]) => `(${ix(name)}, ${e(e2)})`))},
666
+ ${list(Object.entries(lp.body).map(([name, e2]) => `(${ix(name)}, ${e(e2)})`))},
667
+ ${c(lp.while)},
668
+ ${lp.pre ? c(lp.pre) : TRUE},
669
+ ${lp.assume ? c(lp.assume) : TRUE}⟩`;
670
+ // The history: the machine without it (its state without the sums, its assumption without the relations), what a
671
+ // record keeps and the sums, queries and relations over it.
672
+ const hlit = (rels) => {
673
+ const h = hm.history, base = { ...hm, history: undefined };
674
+ return `⟨${lit(base)},
675
+ ${c(h.keep)},
676
+ ${list(h.record.map(f => String(ix(f))))},
677
+ ${list(h.record.map(f => String(ix(`r.${f}`))))},
678
+ ${list(Object.entries(h.sums).map(([name, g]) => `(${ix(name)}, ${e(g)})`))},
679
+ ${list(Object.entries(h.queries).map(([name, g]) => `(${ix(name)}, ${e(g)})`))},
680
+ ${list(rels.map(rel => { if (rel.holds.op !== '<=')
681
+ throw new Error(`${id}: a relation is a <= b`); return `(${c(rel.when)}, ${e(rel.holds.a)}, ${e(rel.holds.b)})`; }))}⟩`;
682
+ };
683
+ // Each relation's record goal and its tree, the history's check, and the laws over histories.
684
+ const histBlock = () => {
685
+ const records = hplain.goals.map(g => { const out = grow(g); if (!('tree' in out))
686
+ throw new Error(`${id}: ${g.name} not proven`); const x2 = enc(g, out.tree); return `(${x2.tab}, ${x2.tree})`; });
687
+ const tampered = (opts.historyTampers ?? []).map((t, j) => `-- TAMPERED: ${t.what}. Lean computes the check to false.
688
+ def ${id}_tampered${j} : HMachine := ${hlit(t.relations)}
689
+ example : checkHMachine ${id}_tampered${j} ${id}_proof ${t.withRecordTrees ? `${id}_records` : '[]'} = false := by decide +kernel`);
690
+ return `def ${id}_records : List (List VDef × Tree) := ${list(records)}
691
+ theorem ${id}_hcheck : checkHMachine ${id}_hist ${id}_proof ${id}_records = true := by decide +kernel
692
+ theorem ${id}_hlaws : ∀ ρ M hist, HReach ${id}_hist ρ M hist → ∀ e ∈ ${id}_proof, e.Holds ρ M :=
693
+ hmachine_sound ${id}_hist ${id}_proof ${id}_records ${id}_hcheck
694
+ ${readable(id, laws, vars, mapNames, { binders: '(ρ : Nat → Nat) (μ : Nat → Nat → Nat) (hist : List (Nat → Nat))', hyp: `HReach ${id}_hist ρ μ hist`, call: `${id}_hlaws ρ μ hist h` })}${tampered.length ? `\n${tampered.join('\n')}` : ''}`;
654
695
  };
655
696
  const lawIx = (name) => laws.findIndex(l => l.name === name);
656
697
  const insts = (inst) => Object.entries(inst).filter(([name]) => lawIx(name) >= 0 && !laws[lawIx(name)].keys?.length).flatMap(([name, keys]) => keys.map(key => `(${lawIx(name)}, ${e(key)})`));
@@ -692,13 +733,15 @@ ${rf ? `def ${id}_refined : SRefined := ⟨${lit(rf.impl)},
692
733
  def ${id}_machine : SMachine := ${id}_refined.full` : gv.length ? `def ${id}_ghosted : SGhosted := ⟨${lit(orig)},
693
734
  ${list(gv.map(v2 => `(${ix(v2)}, ${e(m.state[v2])}, ${e(m.step.state?.[v2] ?? { op: 'var', name: v2 })})`))}⟩
694
735
  def ${id}_machine : SMachine := ${id}_ghosted.full
695
- theorem ${id}_ghostOk : sghostOk ${id}_ghosted = true := by decide +kernel` : `def ${id}_machine : SMachine := ${lit(m)}`}
736
+ theorem ${id}_ghostOk : sghostOk ${id}_ghosted = true := by decide +kernel` : hm ? `def ${id}_hist : HMachine := ${hlit(hm.history.relations ?? [])}
737
+ def ${id}_machine : SMachine := ${id}_hist.plain` : lp0 ? `def ${id}_loop : Loop := ${loopLit(lp0.loop)}
738
+ def ${id}_machine : SMachine := ${id}_loop.machine` : `def ${id}_machine : SMachine := ${lit(m)}`}
696
739
  ${entries.join('\n')}
697
740
  def ${id}_proof : List SEntry := ${list(laws.map((_, j) => `${id}_e${j}`))}
698
741
  theorem ${id}_check : checkSMachine ${id}_machine ${id}_proof = true := by decide +kernel
699
742
  theorem ${id}_laws : ∀ ρ M, SReach ${id}_machine ρ M → ∀ e ∈ ${id}_proof, e.Holds ρ M :=
700
743
  smachine_sound ${id}_machine ${id}_proof ${id}_check
701
- ${readable(id, laws, vars, mapNames, { binders: '(ρ : Nat → Nat) (μ : Nat → Nat → Nat)', hyp: `SReach ${id}_machine ρ μ`, call: `${id}_laws ρ μ h` })}${factsProof(id, m, mapNames, opts.simp)}${opts.tamper ? `
744
+ ${hm ? histBlock() : readable(id, laws, vars, mapNames, { binders: '(ρ : Nat → Nat) (μ : Nat → Nat → Nat)', hyp: `SReach ${id}_machine ρ μ`, call: `${id}_laws ρ μ h` })}${factsProof(id, m, mapNames, opts.simp)}${opts.tamper ? `
702
745
  -- TAMPERED: ${opts.tamper.what}. Lean computes the check to false.
703
746
  def ${id}_tampered : SMachine := ${lit(opts.tamper.machine)}
704
747
  example : checkSMachine ${id}_tampered ${id}_proof = false := by decide +kernel` : ''}${records.length ? `\n\n${records.join('\n\n')}` : ''}${opts.after ? `\n\n${opts.after(ix)}` : ''}`;
@@ -721,61 +764,9 @@ theorem ${id}_facts_hold (ρ : Nat → Nat) (μ : Nat → Nat → Nat) ${fns.map
721
764
  // relations in the assumption) and each relation's record goal; the trees and the instance keys are the kernel's.
722
765
  // Pruning is left out: its goals are about the runtimes (dropping a record changes no query), the laws and relations
723
766
  // are proven over the whole history either way, and Lean's HMachine has no pruning; the TypeScript kernel checks them.
724
- export function hmachineToLean(id, pruned, laws, tampers = [], corollary) {
725
- const m = { ...pruned, history: { ...pruned.history, prune: undefined } };
726
- const h = m.history;
727
- const r = proveLaws(m, laws);
728
- if (!r.proven || !r.proof)
729
- throw new Error(`${id}: not proven`);
730
- const keysOf = [...new Set(laws.flatMap(l => (l.forall ? [l.forall] : [])))];
731
- const flagNames = Object.keys(m.step.flags ?? {});
732
- const sums = Object.keys(h.sums), fields = h.record.map(f => `r.${f}`);
733
- const vars = [...m.params, ...m.inputs, ...Object.keys(m.state), ...sums, ...keysOf, ...keysOf.map(k2 => `${k2}#`), '#', ...flagNames.map(f => `?${f}`), ...fields];
734
- const mapNames = [...new Set([...Object.keys(m.maps ?? {}), ...Object.keys(m.step.maps ?? {}), ...[...JSON.stringify(laws).matchAll(/"op":"get","map":"([^"]+)"/g)].map(x2 => x2[1])])];
735
- const gs = obligations(m, laws, r.proof.instances);
736
- const enc = (g, tree = r.proof.trees[g.name]) => encode({ name: `${id}: ${g.name}`, goal: g, tree }, false, true, vars, mapNames);
737
- const any0 = enc(gs[0], { leaf: [] });
738
- const ix = (name) => { const j = vars.indexOf(name); if (j < 0)
739
- throw new Error(`${id}: no variable ${name}`); return j; };
740
- const rels = (rs) => list(rs.map(rel => {
741
- if (rel.holds.op !== '<=')
742
- throw new Error(`${id}: a relation is a <= b`);
743
- return `(${any0.c(rel.when)}, ${any0.e(rel.holds.a)}, ${any0.e(rel.holds.b)})`;
744
- }));
745
- const lit = (rs) => `⟨⟨⟨${list(m.inputs.map(name => String(ix(name))))},
746
- ${list(Object.entries(m.state).map(([name, e]) => `(${ix(name)}, ${any0.e(e)})`))},
747
- ${list(Object.entries(m.maps ?? {}).map(([name, c2]) => `(${mapNames.indexOf(name)}, ${c2})`))},
748
- ${list(Object.entries(m.step.state ?? {}).map(([name, e]) => `(${ix(name)}, ${any0.e(e)})`))},
749
- ${list(Object.entries(m.step.maps ?? {}).map(([name, w]) => { const pw = pointWrite(w, 'Lean (for now)'); return `(${mapNames.indexOf(name)}, ${any0.e(pw.key)}, ${any0.e(pw.value)})`; }))},
750
- ${m.pre ? any0.c(m.pre) : TRUE},
751
- ${m.assume ? any0.c(m.assume) : TRUE}⟩,
752
- ${list(flagNames.map(f => `(${ix(`?${f}`)}, ${any0.c(m.step.flags[f])})`))}⟩,
753
- ${any0.c(h.keep)},
754
- ${list(h.record.map(f => String(ix(f))))},
755
- ${list(fields.map(f => String(ix(f))))},
756
- ${list(Object.entries(h.sums).map(([name, g]) => `(${ix(name)}, ${any0.e(g)})`))},
757
- ${list(Object.entries(h.queries).map(([name, g]) => `(${ix(name)}, ${any0.e(g)})`))},
758
- ${rels(rs)}⟩`;
759
- const entries = laws.map((law, j) => {
760
- const a = enc(gs[2 * j]), b = enc(gs[2 * j + 1]);
761
- const insts = Object.entries(r.proof.instances[law.name] ?? {}).flatMap(([lname, keys]) => keys.map(key => `(${laws.findIndex(l => l.name === lname)}, ${any0.e(key)})`));
762
- return `def ${id}_l${j} : MLaw := ⟨${any0.c(law.holds)}, ${law.forall ? `some ${ix(law.forall)}` : 'none'}⟩\ndef ${id}_e${j} : MEntry := ⟨${id}_l${j}, ${ix(law.forall ? `${law.forall}#` : '#')}, ${list(insts)},\n (${a.tab}, ${a.tree}),\n (${b.tab}, ${b.tree})⟩`;
763
- });
764
- const records = gs.slice(2 * laws.length).map(g => { const x2 = enc(g); return `(${x2.tab}, ${x2.tree})`; });
765
- const tamperText = tampers.map((t, j) => `-- TAMPERED: ${t.what}. Lean computes the check to false.
766
- def ${id}_tampered${j} : HMachine := ${lit(t.relations)}
767
- example : checkHMachine ${id}_tampered${j} ${id}_proof ${t.withRecordTrees ? `${id}_records` : '[]'} = false := by decide +kernel`);
768
- return `-- ${m.name}: ${laws.map(l => l.name).join('; ')}, with a history (sums ${sums.join(', ')}; queries ${Object.keys(h.queries).join(', ')}; ${(h.relations ?? []).length} relation(s))
769
- -- (variables ${vars.map((v2, j) => `${j} ${v2}`).join(', ')}; maps ${mapNames.map((v2, j) => `${j} ${v2}`).join(', ')})
770
- def ${id}_machine : HMachine := ${lit(h.relations ?? [])}
771
- ${entries.join('\n')}
772
- def ${id}_proof : List MEntry := ${list(laws.map((_, j) => `${id}_e${j}`))}
773
- def ${id}_records : List (List VDef × Tree) := ${list(records)}
774
- theorem ${id}_check : checkHMachine ${id}_machine ${id}_proof ${id}_records = true := by decide +kernel
775
- theorem ${id}_laws : ∀ ρ M hist, HReach ${id}_machine ρ M hist → ∀ e ∈ ${id}_proof, e.law.Holds ρ M :=
776
- hmachine_sound ${id}_machine ${id}_proof ${id}_records ${id}_check
777
- ${readable(id, laws, vars, mapNames, { binders: '(ρ : Nat → Nat) (μ : Nat → Nat → Nat) (hist : List (Nat → Nat))', hyp: `HReach ${id}_machine ρ μ hist`, call: `${id}_laws ρ μ hist h` })}${corollary ? `\n${corollary(ix)}` : ''}
778
- ${tamperText.join('\n')}`;
767
+ // A machine with a history: its laws over histories (History.lean's hmachine_sound), through smachineToLean.
768
+ export function hmachineToLean(id, m, laws, tampers = [], corollary) {
769
+ return smachineToLean(id, m, laws, { historyTampers: tampers, ...(corollary ? { after: corollary } : {}) });
779
770
  }
780
771
  // A Lean file proving each item, to compile against the twin (`leanLibrary`, or `leanProject` for a ready Lake project).
781
772
  // Each machine's laws come with `<id>_laws` (the twin's theorem) and `<id>.lawN` (the law as a plain Lean statement
@@ -798,23 +789,11 @@ export function toLean(items, opts = {}) {
798
789
  throw new Error(`${item.id}: not proven (${r.failed?.stuck ?? ''})`);
799
790
  item = { ...item, laws: r.laws ?? item.laws, ...(item.stepLaws ? { stepLaws: r.stepLaws ?? item.stepLaws } : {}) };
800
791
  }
801
- if (m.history && (m.fns || m.facts))
802
- throw new Error(`${item.id}: functions on a machine with a history aren't exported to Lean yet`);
803
- // One twin: a machine without a history goes to Lean as a machine with sets (one with none, if it has none): its laws,
804
- // step laws, liveness and ghosts in one export. A history's laws go through History.lean, and its step laws and
805
- // liveness through its plain machine (its sums as state, its queries as inputs).
806
- if (m.history) {
807
- parts.push(`-- ${item.id}\n${hmachineToLean(id, m, item.laws)}`);
808
- theorems.push(`${id}_laws`);
809
- if (item.stepLaws?.length || item.liveness?.length) {
810
- parts.push(`-- ${item.id}: step laws and liveness, on its plain machine\n${smachineToLean(`${id}s`, historyMachine(m).machine, item.laws, { stepLaws: item.stepLaws, ...(item.liveness?.length ? { liveness: item.liveness } : {}) })}`);
811
- theorems.push(...(item.stepLaws ?? []).map((_, j) => `${id}s_step${j}_holds`), ...(item.liveness ?? []).map((_, j) => `${id}s_live${j}_holds`));
812
- }
813
- continue;
814
- }
792
+ // One twin: every machine goes to Lean as a machine with sets (one with none, if it has none): its laws, step laws,
793
+ // liveness and ghosts in one export; a machine with a history as its plain machine, and its laws over histories.
815
794
  parts.push(`-- ${item.id}\n${item.lean?.defs ? `${item.lean.defs}\n` : ''}${smachineToLean(id, m, item.laws, { stepLaws: item.stepLaws, ...(item.liveness?.length ? { liveness: item.liveness } : {}),
816
795
  ...(item.ghosts?.length ? { ghosts: item.ghosts } : {}), ...(item.lean?.simp ? { simp: item.lean.simp } : {}), ...(item.lean?.after ? { after: item.lean.after } : {}) })}`);
817
- theorems.push(`${id}_laws`, ...(item.stepLaws ?? []).map((_, j) => `${id}_step${j}_holds`), ...(item.liveness ?? []).map((_, j) => `${id}_live${j}_holds`));
796
+ theorems.push(`${id}_laws`, ...(m.history ? [`${id}_hlaws`] : []), ...(item.stepLaws ?? []).map((_, j) => `${id}_step${j}_holds`), ...(item.liveness ?? []).map((_, j) => `${id}_live${j}_holds`));
818
797
  if (item.lean?.after)
819
798
  theorems.push(...[...item.lean.after(() => 0).matchAll(/^theorem (\S+)/gm)].map(x2 => x2[1]));
820
799
  }
@@ -839,9 +818,9 @@ export function toLean(items, opts = {}) {
839
818
  theorems.push(id);
840
819
  }
841
820
  }
842
- // Sets.lean (which imports Steps.lean) only when a machine has sets.
821
+ // The file the items need: Live.lean for a liveness or a loop, Sets.lean (which imports Steps.lean) for any machine.
843
822
  return `import History
844
- 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) ? 'Live' : items.some(item => 'machine' in item) ? 'Sets' : 'Steps'}
823
+ 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'}
845
824
  open Kern Cert
846
825
  set_option maxRecDepth 100000
847
826
  set_option maxHeartbeats 0