gdp-wired 0.7.0 → 0.9.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,16 +1,13 @@
1
1
  import type { Named, NamesOf, Proof, Prover } from '@gdp-ts/core';
2
- import { type Decision, type Fun, type Rule, type StepLaw, type Verdict } from './dsl.js';
2
+ import { type Decision, type Fun, type Rule, type StepLaw, Unproven } from './dsl.js';
3
3
  import { type Law, type State } from './machine.js';
4
4
  type AnyNamed = Named<any, any>;
5
5
  export type Mint<K extends string, A> = <S extends AnyNamed[]>(args: A, ...about: S) => Proof<K, NamesOf<S>> | null;
6
- export declare class Unproven extends Error {
7
- readonly verdict: Verdict;
8
- constructor(what: string, verdict: Verdict);
9
- }
6
+ export { Unproven };
10
7
  type AnyFun = Fun<any, any, any>;
11
8
  type Conditions<O> = {
12
- [k in keyof O]: O[k] extends boolean ? k : never;
13
- }[keyof O] & string;
9
+ [k in keyof O & string]: O[k] extends boolean ? k : O[k] extends Record<string, unknown> ? `${k}.${Conditions<O[k]>}` : never;
10
+ }[keyof O & string];
14
11
  type ArgsOf<F extends AnyFun> = Parameters<F['run']>[0];
15
12
  type ParamsOf<F extends AnyFun> = NonNullable<Parameters<F['run']>[1]>;
16
13
  export declare function guard<K extends string, F extends AnyFun>(prover: Prover<K>, fn: F, field: Conditions<ReturnType<F['run']>>, opts: {
@@ -32,4 +29,3 @@ export declare function gate<K extends string, R extends AnyRule>(prover: Prover
32
29
  params?: RuleParams<R>;
33
30
  state?: State;
34
31
  }): Gate<K, InputOf<R>>;
35
- export {};
package/dist/core/gdp.js CHANGED
@@ -1,17 +1,10 @@
1
- import { funOf, proveDecision, proveSteps } from "./dsl.js";
1
+ import { Unproven, funOf, proveDecision, proveSteps } from "./dsl.js";
2
2
  import { evalC, interpret } from "./machine.js";
3
3
  import { allowed, requestEnv } from "./watch.js";
4
- // A verdict that isn't a checked proof, as the error a guard or gate is refused with (the verdict on `.verdict`).
5
- export class Unproven extends Error {
6
- verdict;
7
- constructor(what, verdict) {
8
- const found = [verdict.goal && `goal ${verdict.goal}`, verdict.stuck, verdict.witness && `a counterexample: ${JSON.stringify(verdict.witness)}`, verdict.why].filter(Boolean).join('; ');
9
- super(`${what}: ${verdict.proven ? 'proven, but the kernel did not check the proof' : `not proven${found ? ` (${found})` : ''}`}`);
10
- this.verdict = verdict;
11
- }
12
- }
4
+ export { Unproven };
13
5
  const proven = (what, v) => { if (!(v.proven && v.checked))
14
6
  throw new Unproven(what, v); };
7
+ const atPath = (o, path) => path.split('.').reduce((x, f) => (typeof x === 'object' && x !== null ? x[f] : undefined), o);
15
8
  // A proof wherever `fn`'s result `field` holds. Every law in `laws` must have been stated about `fn` (fn.law or
16
9
  // fn.relate) and is proven now; the arguments it is later called with must meet the function's kinds and `assume`.
17
10
  // const access = fun('document access').inputs({...}).returns(env => ({permit: permit(env)}))
@@ -31,7 +24,7 @@ export function guard(prover, fn, field, opts) {
31
24
  const input = r.encodeInput(args);
32
25
  if (m.assume && !evalC(m.assume, { ...ps, ...input }, { scalars: {}, maps: {} }, m))
33
26
  return null;
34
- return fn.run(args, params)[field] === true ? prover.prove(...about) : null;
27
+ return atPath(fn.run(args, params), field) === true ? prover.prove(...about) : null;
35
28
  };
36
29
  }
37
30
  // A proof for each request whose step takes `flag`, the rule's laws (and step laws) proven now. One gate holds one
@@ -70,6 +70,34 @@ export type Machine = {
70
70
  export type Fn = {
71
71
  js: string;
72
72
  lean?: string;
73
+ recur?: Recurrence;
74
+ };
75
+ export type Recurrence = {
76
+ args: string[];
77
+ zero: E;
78
+ next: E;
79
+ lemmas: {
80
+ name: string;
81
+ holds: C;
82
+ }[];
83
+ between: {
84
+ name: string;
85
+ holds: C;
86
+ }[];
87
+ uses: Record<string, {
88
+ arity: number;
89
+ lean?: string;
90
+ }>;
91
+ proofs?: {
92
+ lemmas?: {
93
+ machine: Machine;
94
+ laws: Law[];
95
+ };
96
+ between?: {
97
+ machine: Machine;
98
+ laws: Law[];
99
+ };
100
+ };
73
101
  };
74
102
  export type History = {
75
103
  keep: C;
@@ -448,13 +448,50 @@ function fnKeys(m, goals) {
448
448
  // value that makes one of its reads one of these (by matching: a function of several arguments reads a paired key), and
449
449
  // at the keys themselves.
450
450
  const withFacts = (m, inst, keys) => ({ ...inst, ...Object.fromEntries((m.facts ?? []).filter(f => f.forall).map(f => {
451
- if (f.keys?.length)
452
- return [f.name, tuples(keys, f.keys.length + 1)];
451
+ if (f.keys?.length) {
452
+ // A fact about several keys reads a function of several arguments (at a paired key): each read matched against the
453
+ // keys the goals read binds some of the fact's keys; one match, or two that agree, binding every key is an instance.
454
+ const vars = [f.forall, ...f.keys], reads = fnKeys(m, [{ name: '', hyps: [f.holds] }]);
455
+ if (!reads.length)
456
+ return [f.name, tuples(keys, vars.length)];
457
+ const partial = reads.flatMap(pattern => keys.flatMap(key => { const b = matchMany(pattern, key, vars); return b ? [b] : []; }));
458
+ const whole = (b) => vars.every(x => x in b);
459
+ const agree = (a, b) => Object.entries(b).every(([x, e]) => !(x in a) || JSON.stringify(a[x]) === JSON.stringify(e));
460
+ const found = [...partial.filter(whole), ...partial.flatMap((a, i) => partial.slice(i + 1).filter(b => agree(a, b)).map(b => ({ ...a, ...b })).filter(whole))];
461
+ const had = inst[f.name] ?? [], out = [...had], seen = new Set();
462
+ for (let t = 0; t < had.length / vars.length; t++)
463
+ seen.add(JSON.stringify(had.slice(t * vars.length, (t + 1) * vars.length)));
464
+ for (const b of found) {
465
+ const tuple = vars.map(x => b[x]), id = JSON.stringify(tuple);
466
+ if (!seen.has(id)) {
467
+ seen.add(id);
468
+ out.push(...tuple);
469
+ }
470
+ }
471
+ return [f.name, out];
472
+ }
453
473
  const had = inst[f.name] ?? [], seen = new Set(had.map(e => JSON.stringify(e)));
454
474
  const reads = fnKeys(m, [{ name: '', hyps: [f.holds] }]);
455
475
  const at = [...keys, ...reads.flatMap(pattern => keys.flatMap(key => { const b = match(pattern, key, f.forall); return b ? [b] : []; }))];
456
476
  return [f.name, [...had, ...at.filter(e => { const id = JSON.stringify(e); return seen.has(id) ? false : (seen.add(id), true); })]];
457
477
  })) });
478
+ // The values of the names in xs that make pattern the term, for those it reads, if some do (structurally).
479
+ function matchMany(pattern, term, xs) {
480
+ const bound = {};
481
+ const go = (p, t) => {
482
+ if (p.op === 'var' && xs.includes(p.name)) {
483
+ if (p.name in bound)
484
+ return JSON.stringify(bound[p.name]) === JSON.stringify(t);
485
+ bound[p.name] = t;
486
+ return true;
487
+ }
488
+ if (p.op !== t.op)
489
+ return false;
490
+ const pe = Object.entries(p), te = t;
491
+ return pe.every(([k, val]) => k === 'op' || (typeof val === 'object' && val !== null ? (Array.isArray(val) ? Array.isArray(te[k]) && te[k].length === val.length && val.every((y, i) => go(y, te[k][i])) : go(val, te[k])) : val === te[k]));
492
+ };
493
+ return go(pattern, term) ? bound : undefined;
494
+ }
458
495
  // The value of x that makes pattern the term, if one does (structurally).
459
496
  function match(pattern, term, x) {
460
497
  let bound;
@@ -1,4 +1,4 @@
1
- import type { C, E, Law, Machine } from '../core/machine.js';
1
+ import type { C, E, Law, Machine, Recurrence } from '../core/machine.js';
2
2
  import { type Step } from '../core/solver.js';
3
3
  import { type Fold, type Goal, type Instances, type Liveness, type Loop, type Path, type StepLaw, type Tree } from '../kernel/kernel.js';
4
4
  import { leanLibrary } from './library.js';
@@ -92,7 +92,15 @@ export declare function smachineToLean(id: string, given: Machine, laws: Law[],
92
92
  loop: Loop;
93
93
  };
94
94
  };
95
+ noFacts?: boolean;
95
96
  }): string;
97
+ type Uses = Record<string, {
98
+ lean?: string;
99
+ recur?: Recurrence;
100
+ arity: number;
101
+ }>;
102
+ export declare function recurToLean(lean: string, r: Recurrence, uses?: Uses): string;
103
+ export declare const recurPairing = "theorem Recur.pair_inj {a b c d : Nat} (h : (a + b) * (a + b) + b = (c + d) * (c + d) + d) : a = c \u2227 b = d := by\n have sq : \u2200 s t : Nat, s < t \u2192 s * s + s < t * t := by\n intro s t hst\n have h1 : (s + 1) * (s + 1) \u2264 t * t := Nat.mul_le_mul hst hst\n have h2 : (s + 1) * (s + 1) = s * s + s + s + 1 := by simp only [Nat.add_mul, Nat.mul_add, Nat.mul_one, Nat.one_mul]; omega\n omega\n have hb : b \u2264 a + b := Nat.le_add_left b a\n have hd : d \u2264 c + d := Nat.le_add_left d c\n rcases Nat.lt_trichotomy (a + b) (c + d) with hlt | heq | hgt\n \u00B7 have := sq _ _ hlt; omega\n \u00B7 rw [heq] at h; omega\n \u00B7 have := sq _ _ hgt; omega\nnoncomputable def Recur.unpair (k : Nat) : Nat \u00D7 Nat :=\n Classical.epsilon fun p : Nat \u00D7 Nat => (p.1 + p.2) * (p.1 + p.2) + p.2 = k\ntheorem Recur.unpair_pair (a b : Nat) : Recur.unpair ((a + b) * (a + b) + b) = (a, b) := by\n have h := Classical.epsilon_spec (p := fun p : Nat \u00D7 Nat => (p.1 + p.2) * (p.1 + p.2) + p.2 = (a + b) * (a + b) + b) \u27E8(a, b), rfl\u27E9\n obtain \u27E8h1, h2\u27E9 := Recur.pair_inj h\n exact Prod.ext h1 h2";
96
104
  export declare function hmachineToLean(id: string, m: Machine, laws: Law[], tampers?: {
97
105
  what: string;
98
106
  relations: {
@@ -3,7 +3,7 @@ import { add, k, scale, sub } from "../core/solver.js";
3
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
- import { leanTerms } from "./lean-terms.js";
6
+ import { leanSurface, leanTerms } from "./lean-terms.js";
7
7
  import { leanLibrary } from "./library.js";
8
8
  export { leanLibrary };
9
9
  // --- to Lean ------------------------------------------------------------------------------------------------------------
@@ -474,8 +474,8 @@ const boundKeys = (m) => [...Object.values(m.step.flags ?? {}).flatMap(c => (c.o
474
474
  // `historyTampers`: the history with other relations, each refused.
475
475
  export function smachineToLean(id, given, laws, opts = {}) {
476
476
  const hm = given.history ? { ...given, history: { ...given.history, prune: undefined } } : undefined;
477
- const hplain = hm ? historyMachine(hm) : undefined, m = hplain ? hplain.machine : given;
478
- const stepLaws = opts.stepLaws ?? [], rf = opts.refined, gv = opts.ghosts ?? [];
477
+ const hplain = hm ? historyMachine(hm) : undefined, stepLaws = opts.stepLaws ?? [], rf = opts.refined, gv = opts.ghosts ?? [];
478
+ const m = hplain ? hplain.machine : given;
479
479
  const claims = [];
480
480
  const claimSteps = () => claims.flatMap(cl => cl.steps);
481
481
  // The machine's own step laws, one step or k (Sets.lean's skstep_sound).
@@ -497,6 +497,7 @@ def ${id}_step${j} : SKStep := ⟨⟨${list(Array.from({ length: kk }, (_, s2) =
497
497
  ${list(qinsts(inst))},
498
498
  ${list(finsts(inst))},
499
499
  ${list(pinsts(inst))},
500
+ ${list(pfinsts(inst))},
500
501
  [],
501
502
  ${list(Array.from({ length: kk - 1 }, (_, s2) => list(Object.keys(m.state).map(n2 => `(${ix(n2)}, ${ix(stateName(n2, s2 + 1))})`))))}⟩
502
503
  theorem ${id}_step${j}_holds : SKStepHolds ${id}_machine ${id}_step${j} :=
@@ -608,7 +609,15 @@ theorem ${id}_orig_laws : ∀ ρ M, SReach ${id}_ghosted.orig ρ M → ∀ e ∈
608
609
  const factsF = m.facts ?? [];
609
610
  const base = [m.pre ?? trueC,
610
611
  ...factsF.filter(f => !f.forall).map(f => f.holds),
611
- ...Object.entries(inst).flatMap(([name, keys]) => { const f = factsF.find(f2 => f2.name === name); return f?.forall ? keys.map(key => sub1(f.holds, { [f.forall]: key })) : []; }),
612
+ ...Object.entries(inst).flatMap(([name, keys]) => { const f = factsF.find(f2 => f2.name === name); return f?.forall && !f.keys?.length ? keys.map(key => sub1(f.holds, { [f.forall]: key })) : []; }),
613
+ // A fact about several keys at each tuple (kept flattened), as Lean reads it: the further keys, then the first.
614
+ ...Object.entries(inst).flatMap(([name, keys]) => {
615
+ const f = factsF.find(f2 => f2.name === name);
616
+ if (!f?.keys?.length)
617
+ return [];
618
+ const k = f.keys.length + 1;
619
+ return Array.from({ length: Math.floor(keys.length / k) }, (_, t) => sub1(sub1(f.holds, Object.fromEntries(f.keys.map((kn, i) => [kn, keys[t * k + 1 + i]]))), { [f.forall]: keys[t * k] }));
620
+ }),
612
621
  ...laws.filter(lw => !lw.forall).map(lw => lw.holds),
613
622
  ...Object.entries(inst).flatMap(([name, keys]) => { const lw = laws.find(l2 => l2.name === name); return lw?.forall && !lw.keys?.length ? keys.map(key => sub1(lw.holds, { [lw.forall]: key })) : []; }),
614
623
  ...Object.entries(inst).flatMap(([name, keys]) => {
@@ -658,7 +667,8 @@ theorem ${id}_orig_laws : ∀ ρ M, SReach ${id}_ghosted.orig ρ M → ∀ e ∈
658
667
  ${list(ws.flatMap(([name, w]) => ('each' in w ? [`(${mx(name)}, ${ix(w.each)}, ${e(w.value)})`] : [])))},
659
668
  ${list(fs.flatMap(([f, c2]) => (c2.op === 'all' ? [`(${ix(`?${f}`)}, ${ix(c2.key)}, ${c(c2.c)})`] : [])))},
660
669
  ${list(fs.flatMap(([f, c2]) => (c2.op === 'all' ? [`(${ix(`?${f}`)}, ${ix(`w?${f}`)})`] : [])))},
661
- ${list((mm.facts ?? []).map(l => `⟨${c(l.holds)}, ${l.forall ? `some ${ix(l.forall)}` : 'none'}⟩`))}⟩`;
670
+ ${list((mm.facts ?? []).filter(l => !l.keys?.length).map(l => `⟨${c(l.holds)}, ${l.forall ? `some ${ix(l.forall)}` : 'none'}⟩`))},
671
+ ${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))))})`))}⟩`;
662
672
  };
663
673
  // A loop as Live.lean's record: inputs, start, body, while, precondition, assumption.
664
674
  const loopLit = (lp) => `⟨${list(lp.inputs.map(name => String(ix(name))))},
@@ -700,12 +710,21 @@ ${readable(id, laws, vars, mapNames, { binders: '(ρ : Nat → Nat) (μ : Nat
700
710
  const n = laws[lawIx(name)].keys.length + 1;
701
711
  return Array.from({ length: Math.floor(keys.length / n) }, (_, t) => `(${lawIx(name)}, ${e(keys[t * n])}, ${list(keys.slice(t * n + 1, t * n + n).map(e))})`);
702
712
  });
703
- const finsts = (inst) => Object.entries(inst).flatMap(([name, keys]) => { const j = facts.findIndex(f => f.name === name); return j < 0 ? [] : keys.map(key => `(${j}, ${e(key)})`); });
713
+ // The facts about one key, and those about several (each instance a tuple, kept flattened: its first key, the rest).
714
+ const facts1 = facts.filter(f => !f.keys?.length), factsN = facts.filter(f => f.keys?.length);
715
+ const finsts = (inst) => Object.entries(inst).flatMap(([name, keys]) => { const j = facts1.findIndex(f => f.name === name); return j < 0 ? [] : keys.map(key => `(${j}, ${e(key)})`); });
716
+ const pfinsts = (inst) => Object.entries(inst).flatMap(([name, keys]) => {
717
+ const j = factsN.findIndex(f => f.name === name);
718
+ if (j < 0)
719
+ return [];
720
+ const n = factsN[j].keys.length + 1;
721
+ return Array.from({ length: Math.floor(keys.length / n) }, (_, t) => `(${j}, ${e(keys[t * n])}, ${list(keys.slice(t * n + 1, t * n + n).map(e))})`);
722
+ });
704
723
  const qinsts = (inst) => Object.entries(inst).filter(([name]) => name.startsWith('?')).flatMap(([f, keys]) => keys.map(key => `(${ix(f)}, ${e(key)})`));
705
724
  const entries = laws.map((law, j) => {
706
725
  const g0 = goalsL[2 * j], g1 = goalsL[2 * j + 1], a = enc(g0, lp.trees[g0.name]), b = enc(g1, lp.trees[g1.name]), inst = lp.instances[law.name] ?? {};
707
726
  return `def ${id}_l${j} : MLaw := ⟨${c(law.holds)}, ${law.forall ? `some ${ix(law.forall)}` : 'none'}⟩
708
- def ${id}_e${j} : SEntry := ⟨${id}_l${j}, ${ix(law.forall ? `${law.forall}#` : '#')}, ${list((law.keys ?? []).map(key => String(ix(key))))}, ${list(insts(inst))}, ${list(qinsts(inst))}, ${list(finsts(inst))}, ${list(pinsts(inst))},
727
+ def ${id}_e${j} : SEntry := ⟨${id}_l${j}, ${ix(law.forall ? `${law.forall}#` : '#')}, ${list((law.keys ?? []).map(key => String(ix(key))))}, ${list(insts(inst))}, ${list(qinsts(inst))}, ${list(finsts(inst))}, ${list(pinsts(inst))}, ${list(pfinsts(inst))},
709
728
  (${a.tab}, [], ${a.tree}),
710
729
  (${b.tab}, [], ${b.tree})⟩`;
711
730
  });
@@ -714,13 +733,13 @@ def ${id}_e${j} : SEntry := ⟨${id}_l${j}, ${ix(law.forall ? `${law.forall}#` :
714
733
  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))) } };
715
734
  const h = {
716
735
  step: sl => { const g = goalsS[allSteps.indexOf(sl)]; return { pr: enc(g, r.proof.trees[g.name]), inst: instOf(sl) }; },
717
- sproof: sl => { const { pr, inst } = h.step(sl); return `(${list(insts(inst))}, ${list(qinsts(inst))}, ${list(finsts(inst))}, ${list(pinsts(inst))}, (${pr.tab}, [], ${pr.tree}))`; },
736
+ sproof: sl => { const { pr, inst } = h.step(sl); return `(${list(insts(inst))}, ${list(qinsts(inst))}, ${list(finsts(inst))}, ${list(pinsts(inst))}, ${list(pfinsts(inst))}, (${pr.tab}, [], ${pr.tree}))`; },
718
737
  allow: a => {
719
738
  const f2 = allowed.get(a);
720
739
  if (!f2)
721
740
  return undefined;
722
741
  const ar = f2.out, pr = enc(ar.goal, ar.tree), inst = ar.instances;
723
- return `(${list(f2.wit.map(([inp, t]) => `(${ix(inp)}, ${e(t)})`))}, (${list(insts(inst))}, ${list(qinsts(inst))}, ${list(finsts(inst))}, ${list(pinsts(inst))}, (${pr.tab}, ${list(ar.goal.hyps.map(c))}, ${pr.tree})), ${list(ar.goal.zkeys.map(e))})`;
742
+ return `(${list(f2.wit.map(([inp, t]) => `(${ix(inp)}, ${e(t)})`))}, (${list(insts(inst))}, ${list(qinsts(inst))}, ${list(finsts(inst))}, ${list(pinsts(inst))}, ${list(pfinsts(inst))}, (${pr.tab}, ${list(ar.goal.hyps.map(c))}, ${pr.tree})), ${list(ar.goal.zkeys.map(e))})`;
724
743
  },
725
744
  };
726
745
  const records = claims.map(cl => cl.render(h)).filter(t => t.length);
@@ -741,24 +760,230 @@ def ${id}_proof : List SEntry := ${list(laws.map((_, j) => `${id}_e${j}`))}
741
760
  theorem ${id}_check : checkSMachine ${id}_machine ${id}_proof = true := by decide +kernel
742
761
  theorem ${id}_laws : ∀ ρ M, SReach ${id}_machine ρ M → ∀ e ∈ ${id}_proof, e.Holds ρ M :=
743
762
  smachine_sound ${id}_machine ${id}_proof ${id}_check
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 ? `
763
+ ${hm ? histBlock() : readable(id, laws, vars, mapNames, { binders: '(ρ : Nat → Nat) (μ : Nat → Nat → Nat)', hyp: `SReach ${id}_machine ρ μ`, call: `${id}_laws ρ μ h` })}${opts.noFacts ? '' : factsProof(id, m, mapNames, vars, opts.simp)}${opts.tamper ? `
745
764
  -- TAMPERED: ${opts.tamper.what}. Lean computes the check to false.
746
765
  def ${id}_tampered : SMachine := ${lit(opts.tamper.machine)}
747
766
  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)}` : ''}`;
748
767
  }
749
- // The facts, proven in Lean of the functions' Lean definitions, when every function has one: a run that starts with
750
- // those functions starts where SReach asks the facts to hold.
751
- function factsProof(id, m, maps, simp = []) {
768
+ // A function defined by a recurrence (core/dsl.ts recur): its Lean definition by the same recurrence, and its lemmas
769
+ // proven together by induction (each case by omega, its min, max and ifs split first), and its laws between two points
770
+ // by induction on the distance. A function of several arguments is `<name>_rec`, its further arguments first, and the
771
+ // function the machine reads is its key's (`<name>`, through Recur.unpair: the arguments paired as the builders pair
772
+ // them, (a + b)² + b, which Recur.pair_inj proves injective). The kernel proved the lemmas by the same inductions; Lean
773
+ // proves them again of the definition.
774
+ const recurArg = (a) => `«${a}»`;
775
+ // The arguments a key was paired from (the builders' argsKey: left-nested (k + a)² + a), if it was.
776
+ const unpairKey = (key, n) => {
777
+ if (n === 1)
778
+ return [key];
779
+ if (key.op !== 'add' || key.a.op !== 'prod' || key.a.a.op !== 'add')
780
+ return undefined;
781
+ const s = key.a.a, b = key.b;
782
+ if (JSON.stringify(key.a.b) !== JSON.stringify(s) || JSON.stringify(s.b) !== JSON.stringify(b))
783
+ return undefined;
784
+ const rest = unpairKey(s.a, n - 1);
785
+ return rest ? [...rest, b] : undefined;
786
+ };
787
+ // f at x (and the further arguments), as Lean reads it.
788
+ const recurCall = (lean, r, x, args = r.args.map(recurArg)) => (r.args.length ? `(${lean}_rec ${[...args, x].join(' ')})` : `(${lean} ${x})`);
789
+ // A read of a function the recurrence uses: a recurrence by its definition at the unpaired arguments, any other by its
790
+ // Lean term at the key.
791
+ const useRead = (uses, where) => (map, key, e) => {
792
+ const u = uses[map];
793
+ if (!u?.lean)
794
+ throw new Error(`${where}: ${map} has no Lean definition`);
795
+ if (u.recur) {
796
+ const args = unpairKey(key, u.arity);
797
+ if (!args)
798
+ throw new Error(`${where}: ${map} read at a key not paired by its arguments`);
799
+ return recurCall(u.lean, u.recur, e(args[0]), args.slice(1).map(e));
800
+ }
801
+ return `((${u.lean}) ${e(key)})`;
802
+ };
803
+ // The lemmas of the recurrences a term reads, at the keys it reads them, with the names as given.
804
+ const useLemmas = (uses, t, names, where) => {
805
+ const out = [];
806
+ const walk = (x) => {
807
+ if (!x || typeof x !== 'object')
808
+ return;
809
+ const o = x;
810
+ if (o.op === 'get' && o.map) {
811
+ const u = uses[o.map];
812
+ if (u?.recur?.lemmas.length) {
813
+ const args = unpairKey(o.key, u.arity), s = leanSurface(names, where, useRead(uses, where)).e;
814
+ out.push(`have := ${u.lean}_lemmas ${[...args.slice(1).map(s), s(args[0])].join(' ')}`);
815
+ }
816
+ }
817
+ for (const v of Object.values(o))
818
+ walk(v);
819
+ };
820
+ walk(t);
821
+ return [...new Set(out)];
822
+ };
823
+ // The kernel's own proof of a recurrence's lemmas (or of its laws between two points), for Lean to check: the machine
824
+ // it was proven by, exported with its trees (smachine_sound gives its laws at every reachable state), and the run of
825
+ // that machine that reaches x and f(x) (or y = x + d and f(y) from x and f(x)), so each law, read there, is the lemma.
826
+ // The proof needs no tactic beyond simp's arithmetic of the machine's terms: what the kernel proved (products of two
827
+ // terms among it) Lean checks as the kernel's own tree.
828
+ function recurKernel(lean, r, uses, which) {
829
+ const pr = r.proofs?.[which];
830
+ if (!pr)
831
+ return undefined;
832
+ const m = pr.machine, two = which === 'between', id = `${lean.replace(/[^A-Za-z0-9_]/g, '_')}_${two ? 'two' : 'ind'}`;
833
+ const vars = [...m.params, ...m.inputs, ...Object.keys(m.state)], col = (n) => { const j = vars.indexOf(n); if (j < 0)
834
+ throw new Error(`${id}: no column for ${n}`); return j; };
835
+ const fnNames = Object.keys(m.fns ?? {}), args = r.args.map(recurArg), argNames = Object.fromEntries(r.args.map(a => [a, recurArg(a)]));
836
+ const F = (x) => recurCall(lean, r, x);
837
+ const surface = leanSurface(argNames, lean, useRead(uses, lean));
838
+ const binders = [...r.args.map(a => `(${recurArg(a)} : Nat)`), ...(two ? ['(x : Nat)'] : [])].join(' '), ps = [...args, ...(two ? ['x'] : [])].join(' ');
839
+ const start = [...r.args.map(a => [a, recurArg(a)]), ...(m.params.includes('fx0') ? [['fx0', surface.e(r.zero)]] : []),
840
+ ...(two ? [['x', 'x'], ['fx', F('x')]] : [])];
841
+ const chain = (pairs, dflt, v) => pairs.reduceRight((acc, [c, e]) => `if ${v} = ${c} then ${e} else ${acc}`, dflt);
842
+ // What the run holds after n steps, column by column.
843
+ const at = (n) => two
844
+ ? [[col('y'), `x + ${n}`], [col('fy'), F(`(x + ${n})`)], [col('x'), 'x'], [col('fx'), F('x')], ...r.args.map(a => [col(a), recurArg(a)])]
845
+ : [[col('x'), n], [col('fx'), F(n)], ...r.args.map(a => [col(a), recurArg(a)])];
846
+ const hs = at('n').map((_, i) => `h${i}`);
847
+ const recurDefs = [r.args.length ? `${lean}_rec` : lean, ...Object.values(uses).flatMap(u => (u.recur ? (u.recur.args.length ? [`${u.lean}_rec`, `${u.lean}_key`] : [u.lean]) : []))];
848
+ const one = (m.facts ?? []).some(f => !f.keys?.length), several = (m.facts ?? []).some(f => f.keys?.length);
849
+ // A fact of the params at the start of the laws between: the lemma at x and f(x), its own conjunct.
850
+ const proj = (j, k) => (k === 1 ? '' : j < k - 1 ? `${'.2'.repeat(j)}.1` : '.2'.repeat(k - 1));
851
+ 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; };
852
+ const factsAt = factsProof(id, m, fnNames, vars, [`${id}_ρ`, `${id}_μ`, ...recurDefs], { binders, rho: `(${id}_ρ ${ps})`, mu: `${id}_μ`, cite });
853
+ const unfold = [`${id}_machine`, `${id}_μ`, 'applyS', 'assoc', 'lookup', 'evalE', 'evalC', ...recurDefs].join(', ');
854
+ const pre = `
855
+ ${smachineToLean(id, m, pr.laws, { noFacts: true })}
856
+ def ${id}_ρ ${binders} : Nat → Nat := fun i => ${chain(start.map(([n, e]) => [col(n), e]), '0', 'i')}
857
+ noncomputable def ${id}_μ : Nat → Nat → Nat := fun j => ${chain(fnNames.map((n, j) => [j, `(${m.fns[n].lean})`]), 'fun _ => 0', 'j')}${factsAt}
858
+ -- 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)'}.
859
+ theorem ${id}_run ${binders} : ∀ n, ∃ ρ, SReach ${id}_machine ρ ${id}_μ ∧ ${at('n').map(([c, e]) => `ρ ${c} = ${e}`).join(' ∧ ')} := by
860
+ intro n
861
+ induction n with
862
+ | zero =>
863
+ refine ⟨_, SReach.start (${id}_ρ ${ps}) ${id}_μ ?_ ?_ ?_ ?_, ${at('0').map(() => '?_').join(', ')}⟩
864
+ · intro p hp; simp [${id}_machine] at hp
865
+ · simp [${id}_machine, evalC, evalE]
866
+ · ${one ? `exact ${id}_facts_at ${ps}` : `intro l hl; simp [${id}_machine] at hl`}
867
+ · ${several ? `exact ${id}_pfacts_at ${ps}` : `intro p hp; simp [${id}_machine] at hp`}
868
+ all_goals simp [${id}_ρ, ${unfold}]
869
+ | succ n ih =>
870
+ obtain ⟨ρ, hr, ${hs.join(', ')}⟩ := ih
871
+ refine ⟨_, SReach.step ρ ${id}_μ (fun _ => 0) (fun _ _ => 0) hr ?_, ${at('0').map(() => '?_').join(', ')}⟩
872
+ · simp [sView, ${id}_machine, evalC, evalE]
873
+ all_goals (try rw [show x + (n + 1) = x + n + 1 from by omega])
874
+ all_goals simp [sNext, sView, decideFlags, decideQ, flagSub, withInputs, withCarried, ${hs.join(', ')}, ${unfold}]`;
875
+ // The lemmas (or the laws between, after the lemmas at y): each the machine's law read on the run.
876
+ const own = two ? r.between.map((_, j) => r.lemmas.length + j) : r.lemmas.map((_, j) => j);
877
+ 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`;
878
+ const proof = (from) => `(obtain ⟨ρ, hr, ${hs.join(', ')}⟩ := ${id}_run ${from}; exact ${own.length === 1 ? `(${each(own[0])})` : `⟨${own.map(j => `(${each(j)})`).join(', ')}⟩`})`;
879
+ return { pre, proof };
880
+ }
881
+ export function recurToLean(lean, r, uses = {}) {
882
+ const binders = r.args.map(a => `(${recurArg(a)} : Nat)`).join(' '), argNames = Object.fromEntries(r.args.map(a => [a, recurArg(a)]));
883
+ const self = r.args.length ? `${lean}_rec` : lean;
884
+ const read = useRead(uses, lean);
885
+ const surface = (names) => leanSurface({ ...argNames, ...names }, lean, read);
886
+ const text = JSON.stringify(r), unfold = [self, ...(text.includes('"op":"min"') ? ['Nat.min_def'] : []), ...(text.includes('"op":"max"') ? ['Nat.max_def'] : [])].join(', ');
887
+ const lemmaAt = (x) => r.lemmas.map(l => `(${surface({ x, fx: recurCall(lean, r, x) }).c(l.holds)})`).join(' ∧ ');
888
+ const close = (haves) => `${haves.map(h => `${h}; `).join('')}(repeat' split) <;> omega`;
889
+ const step = (n) => useLemmas(uses, r.next, { ...argNames, x: n, prev: recurCall(lean, r, n) }, lean);
890
+ // Each theorem by the kernel's proof (its machine and the run), or by induction with omega where that fails.
891
+ const kl = recurKernel(lean, r, uses, 'lemmas'), kb = recurKernel(lean, r, uses, 'between');
892
+ const lemmas = r.lemmas.length ? `${kl?.pre ?? ''}
893
+ -- ${r.lemmas.map(l => l.name).join('; ')}
894
+ theorem ${lean}_lemmas ${binders} (x : Nat) : ${lemmaAt('x')} := by
895
+ first
896
+ | ${kl ? kl.proof(`${[...r.args.map(recurArg), 'x'].join(' ')}`) : 'fail'}
897
+ | (induction x with
898
+ | zero => (try simp only [${unfold}]); ${close(useLemmas(uses, { op: 'and', xs: r.lemmas.map(l => l.holds) }, { ...argNames, x: '0', fx: recurCall(lean, r, '0') }, lean))}
899
+ | succ n ih => (try simp only [${unfold}] at ih ⊢); ${close([...step('n'), ...useLemmas(uses, { op: 'and', xs: r.lemmas.map(l => l.holds) }, { ...argNames, x: '(n + 1)', fx: recurCall(lean, r, '(n + 1)') }, lean)])})` : '';
900
+ const args = r.args.map(recurArg).join(' ');
901
+ const pairAt = (x, y) => r.between.map(l => `(${surface({ x, fx: recurCall(lean, r, x), y, fy: recurCall(lean, r, y) }).c(l.holds)})`).join(' ∧ ');
902
+ const between = r.between.length ? `${kb?.pre ?? ''}
903
+ -- ${r.between.map(l => l.name).join('; ')}, for every x <= y
904
+ theorem ${lean}_between_by ${binders} (x d : Nat) : ${pairAt('x', '(x + d)')} := by
905
+ first
906
+ | ${kb ? kb.proof(`${[...r.args.map(recurArg), 'x', 'd'].join(' ')}`) : 'fail'}
907
+ | (induction d with
908
+ | zero => simp only [Nat.add_zero]; (repeat' split) <;> omega
909
+ | succ n ih =>
910
+ rw [show x + (n + 1) = (x + n) + 1 from by omega]
911
+ ${r.lemmas.length ? `have := ${lean}_lemmas ${args} (x + n); ` : ''}(try simp only [${unfold}] at ih this ⊢); ${close(step('(x + n)'))})
912
+ theorem ${lean}_between ${binders} (x y : Nat) (h : x ≤ y) : ${pairAt('x', 'y')} := by
913
+ obtain ⟨d, rfl⟩ := Nat.exists_eq_add_of_le h
914
+ exact ${lean}_between_by ${args} x d` : '';
915
+ const zero = surface({}).e(r.zero), next = surface({ x: 'x', prev: recurCall(lean, r, 'x') }).e(r.next);
916
+ if (!r.args.length)
917
+ return `def ${lean} : Nat → Nat\n | 0 => ${zero}\n | x + 1 => ${next}${lemmas}${between}`;
918
+ // The key's function: the arguments unpaired, innermost first.
919
+ const u = (i) => (i === r.args.length ? '(Recur.unpair k)' : `(Recur.unpair ${u(i + 1)}.1)`);
920
+ const keyOf = (x) => r.args.map(recurArg).reduce((k, a) => `((${k} + ${a}) * (${k} + ${a}) + ${a})`, x);
921
+ return `def ${self} ${binders} : Nat → Nat
922
+ | 0 => ${zero}
923
+ | x + 1 => ${next}
924
+ noncomputable def ${lean} (k : Nat) : Nat := ${self} ${r.args.map((_, i) => `${u(i + 1)}.2`).join(' ')} ${u(1)}.1
925
+ theorem ${lean}_key ${binders} (x : Nat) : ${lean} ${keyOf('x')} = ${self} ${args} x := by
926
+ simp only [${lean}, Recur.unpair_pair]${lemmas}${between}`;
927
+ }
928
+ // The pairing the builders read a function of several arguments by, and Lean's inverse of it (choice over the pairs:
929
+ // Recur.pair_inj makes it the pair).
930
+ export const recurPairing = `theorem Recur.pair_inj {a b c d : Nat} (h : (a + b) * (a + b) + b = (c + d) * (c + d) + d) : a = c ∧ b = d := by
931
+ have sq : ∀ s t : Nat, s < t → s * s + s < t * t := by
932
+ intro s t hst
933
+ have h1 : (s + 1) * (s + 1) ≤ t * t := Nat.mul_le_mul hst hst
934
+ have h2 : (s + 1) * (s + 1) = s * s + s + s + 1 := by simp only [Nat.add_mul, Nat.mul_add, Nat.mul_one, Nat.one_mul]; omega
935
+ omega
936
+ have hb : b ≤ a + b := Nat.le_add_left b a
937
+ have hd : d ≤ c + d := Nat.le_add_left d c
938
+ rcases Nat.lt_trichotomy (a + b) (c + d) with hlt | heq | hgt
939
+ · have := sq _ _ hlt; omega
940
+ · rw [heq] at h; omega
941
+ · have := sq _ _ hgt; omega
942
+ noncomputable def Recur.unpair (k : Nat) : Nat × Nat :=
943
+ Classical.epsilon fun p : Nat × Nat => (p.1 + p.2) * (p.1 + p.2) + p.2 = k
944
+ theorem Recur.unpair_pair (a b : Nat) : Recur.unpair ((a + b) * (a + b) + b) = (a, b) := by
945
+ have h := Classical.epsilon_spec (p := fun p : Nat × Nat => (p.1 + p.2) * (p.1 + p.2) + p.2 = (a + b) * (a + b) + b) ⟨(a, b), rfl⟩
946
+ obtain ⟨h1, h2⟩ := Recur.pair_inj h
947
+ exact Prod.ext h1 h2`;
948
+ function factsProof(id, m, maps, vars, simp = [], at) {
752
949
  const fns = Object.entries(m.fns ?? {}), facts = m.facts ?? [];
753
- if (!facts.length || !fns.length || fns.some(([, f]) => !f.lean))
950
+ if (!facts.length || (!at && !fns.length) || fns.some(([, f]) => !f.lean))
754
951
  return '';
952
+ const recurs = fns.filter(([, f]) => f.recur);
953
+ simp = [...simp, ...recurs.flatMap(([, f]) => (f.recur.args.length ? [`${f.lean}_rec`, `${f.lean}_key`] : [f.lean]))];
954
+ // Each recurrence's key lemma rewrites the function at its key before anything else simplifies the key.
955
+ const keyed = recurs.filter(([, f]) => f.recur.args.length).map(([, f]) => `${f.lean}_key`);
956
+ const hs = fns.map((_, j) => `h${j}`);
957
+ // A fact's proof, its keys named: the first `v`, the further ones `w1`, `w2`...: the recurrence theorem it is an
958
+ // instance of cited first (so that it and the goal simplify alike), then the facts' terms read and omega.
959
+ const tactic = (fact) => {
960
+ const keys = fact.forall ? [fact.forall, ...(fact.keys ?? [])] : [], arg = keys.map((_, i) => (i === 0 ? 'v' : `w${i}`));
961
+ const owner = recurs.find(([n]) => fact.name.startsWith(`${n}: `)), what = owner ? fact.name.slice(owner[0].length + 2) : '';
962
+ const r = owner?.[1].recur, lean = owner?.[1].lean;
963
+ const cite = at?.cite?.(fact) ?? (!r || !keys.length ? undefined : r.lemmas.some(l => l.name === what) ? `${lean}_lemmas ${[...arg.slice(1), arg[0]].join(' ')}`
964
+ : r.between.some(l => l.name === what) ? `${lean}_between ${[...arg.slice(2), arg[0], arg[1]].join(' ')}` : undefined);
965
+ const intro = keys.length > 1 ? `simp only [holdsOver]; intro ${arg.slice(1).join(' ')}; ` : '';
966
+ return `${intro}simp only [MLaw.Holds]; (try intro v); ${cite ? `have hc := ${cite}; ` : ''}simp only [${['evalC', 'evalE', 'upd', ...hs].join(', ')}]${keyed.length ? `; (try simp only [${keyed.join(', ')}])` : ''}; (try simp [${[...hs, ...simp].join(', ')}]${cite ? ' at hc ⊢' : ''}); all_goals ${cite ? 'first | omega | (simpa using hc)' : 'omega'}`;
967
+ };
968
+ const one = facts.filter(f => !f.keys?.length), several = facts.filter(f => f.keys?.length);
969
+ const binders = at ? at.binders : `(ρ : Nat → Nat) (μ : Nat → Nat → Nat) ${fns.map(([n, f], j) => `(h${j} : μ ${maps.indexOf(n)} = ${f.lean})`).join(' ')}`;
970
+ const rho = at ? at.rho : 'ρ', mu = at ? at.mu : 'μ', name = at ? 'at' : 'hold';
971
+ // At one state and maps, each function's equation is the maps' own definition.
972
+ const given = at ? fns.map(([n, f], j) => `have h${j} : ${mu} ${maps.indexOf(n)} = (${f.lean}) := rfl\n `).join('') : '';
973
+ const cases = (fs, h) => `rcases ${h} with ${fs.map(() => 'rfl').join(' | ')}\n${fs.map(f => ` · ${tactic(f)}`).join('\n')}`;
755
974
  return `
756
- -- The facts hold of the functions' Lean definitions: ${fns.map(([n, f]) => `${n} := ${f.lean}`).join('; ')}.
757
- theorem ${id}_facts_hold (ρ : Nat → Nat) (μ : Nat → Nat → Nat) ${fns.map(([n, f], j) => `(h${j} : μ ${maps.indexOf(n)} = ${f.lean})`).join(' ')} :
758
- ∀ l ∈ ${id}_machine.facts, l.Holds ρ μ := by
759
- intro l hl
975
+ -- The facts hold of the functions' Lean definitions: ${fns.map(([n, f]) => `${n} := ${f.lean}`).join('; ')}.${one.length ? `
976
+ theorem ${id}_facts_${name} ${binders} :
977
+ ∀ l ∈ ${id}_machine.facts, l.Holds ${rho} ${mu} := by
978
+ ${given}intro l hl
760
979
  simp only [${id}_machine, List.mem_cons, List.not_mem_nil, or_false] at hl
761
- rcases hl with ${facts.map(() => 'rfl').join(' | ')} <;> (simp only [MLaw.Holds]; try intro v); all_goals (simp [evalC, evalE, upd, ${[...fns.map((_, j) => `h${j}`), ...simp].join(', ')}]; try omega)`;
980
+ ${cases(one, 'hl')}` : ''}${several.length ? `
981
+ -- The facts about several keys, at every value of their further keys.
982
+ theorem ${id}_pfacts_${name} ${binders} :
983
+ ${id}_machine.PFacts ${rho} ${mu} := by
984
+ ${given}intro p hp
985
+ simp only [${id}_machine, List.mem_cons, List.not_mem_nil, or_false] at hp
986
+ ${cases(several, 'hp')}` : ''}`;
762
987
  }
763
988
  // Machines with a history through Kern.hmachine_sound (History.lean): Lean builds the plain machine (sums as state,
764
989
  // relations in the assumption) and each relation's record goal; the trees and the instance keys are the kernel's.
@@ -777,6 +1002,25 @@ export function toLean(items, opts = {}) {
777
1002
  const ident = (id) => { const s = id.replace(/[^A-Za-z0-9_]/g, '_'); return /^[A-Za-z]/.test(s) ? s : `x_${s}`; };
778
1003
  const parts = [], theorems = [];
779
1004
  let goals = 0;
1005
+ // Each recurrence defined once, before the first item that reads it.
1006
+ const recurs = new Map();
1007
+ const recurDefs = (m) => Object.entries(m.fns ?? {}).filter(([, f]) => f.recur).map(([n, f]) => {
1008
+ // The functions it reads are the machine's <n>.<use>, defined before it (flatFns puts them first).
1009
+ const uses = Object.fromEntries(Object.entries(f.recur.uses).map(([u, d]) => { const g = m.fns?.[`${n}.${u}`]; return [u, { ...d, ...(g?.recur ? { recur: g.recur } : {}) }]; }));
1010
+ const text = recurToLean(f.lean, f.recur, uses), had = recurs.get(f.lean);
1011
+ if (had !== undefined) {
1012
+ if (had !== text)
1013
+ throw new Error(`${f.lean}: two recurrences of one name`);
1014
+ return '';
1015
+ }
1016
+ const pairing = f.recur.args.length && !recurs.has('') ? (recurs.set('', recurPairing), `${recurPairing}\n`) : '';
1017
+ recurs.set(f.lean, text);
1018
+ if (f.recur.lemmas.length)
1019
+ theorems.push(`${f.lean}_lemmas`);
1020
+ if (f.recur.between.length)
1021
+ theorems.push(`${f.lean}_between`);
1022
+ return `${pairing}${text}\n`;
1023
+ }).join('');
780
1024
  for (let item of items) {
781
1025
  const id = ident(item.id);
782
1026
  if ('machine' in item) {
@@ -791,7 +1035,7 @@ export function toLean(items, opts = {}) {
791
1035
  }
792
1036
  // One twin: every machine goes to Lean as a machine with sets (one with none, if it has none): its laws, step laws,
793
1037
  // liveness and ghosts in one export; a machine with a history as its plain machine, and its laws over histories.
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 } : {}),
1038
+ 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 } : {}),
795
1039
  ...(item.ghosts?.length ? { ghosts: item.ghosts } : {}), ...(item.lean?.simp ? { simp: item.lean.simp } : {}), ...(item.lean?.after ? { after: item.lean.after } : {}) })}`);
796
1040
  theorems.push(`${id}_laws`, ...(m.history ? [`${id}_hlaws`] : []), ...(item.stepLaws ?? []).map((_, j) => `${id}_step${j}_holds`), ...(item.liveness ?? []).map((_, j) => `${id}_live${j}_holds`));
797
1041
  if (item.lean?.after)
@@ -3,3 +3,7 @@ export declare function leanTerms(vars: readonly string[], maps: readonly string
3
3
  e: (t: E) => string;
4
4
  c: (t: C) => string;
5
5
  };
6
+ export declare function leanSurface(names: Record<string, string>, where: string, get?: (map: string, key: E, e: (t: E) => string) => string): {
7
+ e: (t: E) => string;
8
+ c: (t: C) => string;
9
+ };
@@ -42,3 +42,41 @@ export function leanTerms(vars, maps, where) {
42
42
  };
43
43
  return { e, c };
44
44
  }
45
+ // Terms and conditions as plain Lean over Nat (a recurrence's definition and its lemmas): each variable as the Lean
46
+ // expression `names` gives it.
47
+ export function leanSurface(names, where, get) {
48
+ const name = (n) => { const s = names[n]; if (s === undefined)
49
+ throw new Error(`${where}: no Lean expression for ${n}`); return s; };
50
+ const e = (t) => {
51
+ switch (t.op) {
52
+ case 'var': return name(t.name);
53
+ case 'const': return String(t.value);
54
+ case 'mul': return `(${t.k} * ${e(t.a)})`;
55
+ case 'add': return `(${e(t.a)} + ${e(t.b)})`;
56
+ case 'sub': return `(${e(t.a)} - ${e(t.b)})`;
57
+ case 'prod': return `(${e(t.a)} * ${e(t.b)})`;
58
+ case 'min':
59
+ case 'max': return `(${t.op} ${e(t.a)} ${e(t.b)})`;
60
+ case 'if': return `(if ${c(t.c)} then ${e(t.then)} else ${e(t.else)})`;
61
+ case 'get':
62
+ if (!get)
63
+ throw new Error(`${where}: reads ${t.map}, a function or a map`);
64
+ return get(t.map, t.key, e);
65
+ default: return unreachable(t);
66
+ }
67
+ };
68
+ const c = (t) => {
69
+ switch (t.op) {
70
+ case '<=': return `${e(t.a)} ≤ ${e(t.b)}`;
71
+ case '<': return `${e(t.a)} < ${e(t.b)}`;
72
+ case '=': return `${e(t.a)} = ${e(t.b)}`;
73
+ case 'not': return `¬(${c(t.x)})`;
74
+ case 'and':
75
+ case 'or': return t.xs.length === 0 ? (t.op === 'and' ? 'True' : 'False') : `(${t.xs.map(c).join(t.op === 'and' ? ' ∧ ' : ' ∨ ')})`;
76
+ case 'flag':
77
+ case 'all': throw new Error(`${where}: takes no flag or quantifier`);
78
+ default: return unreachable(t);
79
+ }
80
+ };
81
+ return { e, c };
82
+ }