gdp-wired 0.19.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -16,8 +16,9 @@ export declare function leaves(t: Tree, path: Path, visit: (t: Tree & {
16
16
  products?: [number, number][];
17
17
  squares?: Lin[];
18
18
  }, path: Path) => void): void;
19
- export declare function encode(ex: Exported, tamper?: boolean, dropStep?: boolean, globalVars?: string[], globalMaps?: string[]): {
19
+ export declare function encode(ex: Exported, tamper?: boolean, dropStep?: boolean, globalVars?: string[], globalMaps?: string[], slim?: boolean): {
20
20
  tab: string;
21
+ tabCols: string[];
21
22
  hyps: string;
22
23
  tree: string;
23
24
  cols: string[];
@@ -5,7 +5,7 @@ import { grow, minimal, proveBuilt, proveLaws, proveStepLaws, withDeadline } fro
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
+ import { allowNode, certMs, closingInsts, reqTS, withWitness, foldNodes, certMode, quietly, uncoveredWhy, hasQuantified, kWhens, machineNodes, proveCyclic, routeOf, searchedStepNodes, skMachine, stepLawNodes, tBack, tSub } from "../core/cyclic.js";
9
9
  import { leanLibrary } from "./library.js";
10
10
  export { leanLibrary };
11
11
  // --- to Lean ------------------------------------------------------------------------------------------------------------
@@ -49,8 +49,34 @@ export function leaves(t, path, visit) {
49
49
  leaves(t.yes, new Map(path).set(id, { lin: t.split, holds: true }), visit);
50
50
  leaves(t.no, new Map(path).set(id, { lin: t.split, holds: false }), visit);
51
51
  }
52
- export function encode(ex, tamper = false, dropStep = true, globalVars, globalMaps) {
52
+ export function encode(ex, tamper = false, dropStep = true, globalVars, globalMaps, slim = false) {
53
53
  const { goal, tree } = ex;
54
+ // `slim`: the table only has the columns Lean reads. Each row a leaf uses has a hint (where Lean looks for it first),
55
+ // and Lean computes only the hinted hypotheses' linear forms (Kernel.lean's srcRows), so the columns are the
56
+ // variables, those hypotheses' choices and the cells they read, a hinted term column's, and every row's (the path's
57
+ // atoms, the leaf's rows, its squares and products). A leaf with a row of no hint (Lean then reads every
58
+ // hypothesis's rows) keeps the whole table. A premise or descent of a cyclic node reads one or two of its
59
+ // hypotheses, so its table is a few columns, not every choice of the node's facts.
60
+ // Each leaf with its path and rows, found once (the passes below and the tree's text read them in the same order).
61
+ const leafList = [];
62
+ leaves(tree, new Map(), (leaf, path) => { leafList.push({ leaf, path, base: leafConstraints(goal, path) }); });
63
+ const reads = new Set(), hintTerms = new Set();
64
+ let whole = !slim;
65
+ if (slim)
66
+ for (const { leaf, base } of leafList) {
67
+ const n0 = base.length;
68
+ for (const i of [...leaf.leaf.flatMap(st => (st.kind === 'input' && st.index < n0 ? [st.index] : [])), ...(leaf.products ?? []).flat().filter(i => i < n0)]) {
69
+ const src = base[i].src;
70
+ if (!src)
71
+ whole = true;
72
+ else if ('fact' in src)
73
+ reads.add(src.fact);
74
+ else if ('term' in src)
75
+ hintTerms.add(src.term);
76
+ }
77
+ }
78
+ const readHyps = whole ? goal.hyps : goal.hyps.filter((_, i) => reads.has(i));
79
+ const readTerms = whole ? [] : [...hintTerms].map(name => columnTerm(name));
54
80
  // Columns: every variable of the hypotheses, then every cell any leaf uses (and the cells inside their keys),
55
81
  // shallower cells first so a key reads only earlier columns.
56
82
  const vars = globalVars ?? [...new Set(goal.hyps.flatMap(varsOf))];
@@ -102,9 +128,15 @@ export function encode(ex, tamper = false, dropStep = true, globalVars, globalMa
102
128
  else if (typeof val === 'object' && val !== null)
103
129
  choices(val);
104
130
  };
105
- goal.hyps.forEach(choices);
106
- leaves(tree, new Map(), (leaf, path) => {
107
- for (const r of [...withProducts(leafConstraints(goal, path), leaf.products ?? [], leaf.squares), ...(leaf.squares ?? []).map(lin => ({ lin }))])
131
+ readHyps.forEach(choices);
132
+ readTerms.forEach(choices);
133
+ const readSubterms = [...readHyps, ...readTerms.map((t) => ({ op: '<=', a: t, b: t }))].flatMap(subterms).filter(e => e.op === 'prod' || e.op === 'get');
134
+ for (const { leaf, path, base } of leafList) {
135
+ const n0 = base.length;
136
+ const used = whole ? undefined : new Set([...leaf.leaf.flatMap(st => (st.kind === 'input' && st.index < n0 ? [st.index] : [])), ...(leaf.products ?? []).flat().filter(i => i < n0)]);
137
+ const rowsHere = whole ? withProducts(base, leaf.products ?? [], leaf.squares)
138
+ : [...base.filter((_, i) => used.has(i)), ...withProducts(base, leaf.products ?? [], leaf.squares).slice(n0), ...[...path.values()].map(a => ({ lin: a.lin }))];
139
+ for (const r of [...rowsHere, ...(leaf.squares ?? []).map(lin => ({ lin }))])
108
140
  for (const key of Object.keys(r.lin)) {
109
141
  if (isMonomial(key))
110
142
  addMono(key);
@@ -115,18 +147,17 @@ export function encode(ex, tamper = false, dropStep = true, globalVars, globalMa
115
147
  }
116
148
  // Each cell a hypothesis reads under the path, kept or not: two reads of one cell cancel in a row (T − T), yet
117
149
  // Lean's linear form of each still needs the cell's column to see the comparison decided.
118
- for (const h of goal.hyps)
119
- for (const e of subterms(h)) {
120
- if (e.op === 'prod')
121
- for (const key of Object.keys(linear(e, path) ?? {}))
122
- if (isMonomial(key))
123
- addMono(key);
124
- if (e.op === 'get')
125
- for (const key of Object.keys(linear(e, path) ?? {}))
126
- if (isCell(key))
127
- addCell(key);
128
- }
129
- });
150
+ for (const e of readSubterms) {
151
+ if (e.op === 'prod')
152
+ for (const key of Object.keys(linear(e, path) ?? {}))
153
+ if (isMonomial(key))
154
+ addMono(key);
155
+ if (e.op === 'get')
156
+ for (const key of Object.keys(linear(e, path) ?? {}))
157
+ if (isCell(key))
158
+ addCell(key);
159
+ }
160
+ }
130
161
  const depth = (name) => name.split('[').length;
131
162
  // Term columns (open choices, each standing for its term's value) before the cells, whose keys may read them; a
132
163
  // monomial with a cell among its factors after the cells.
@@ -173,13 +204,11 @@ export function encode(ex, tamper = false, dropStep = true, globalVars, globalMa
173
204
  : name.includes('[') ? `.cell ${maps.indexOf(cellMap(name))} ${row(cellKey(name))}`
174
205
  : name.startsWith('⟦') ? `.term (${e(columnTerm(name))})`
175
206
  : `.var ${vars.indexOf(name)}`));
176
- let leafNo = 0;
177
- const lean = (t, path) => {
178
- if (!('leaf' in t)) {
179
- const id = canon(t.split);
180
- return `.split ${row(t.split)}\n (${lean(t.yes, new Map(path).set(id, { lin: t.split, holds: true }))})\n (${lean(t.no, new Map(path).set(id, { lin: t.split, holds: false }))})`;
181
- }
182
- const base = leafConstraints(goal, path), prods = t.products ?? [], squares = t.squares ?? [];
207
+ let leafNo = 0, leafAt = 0;
208
+ const lean = (t) => {
209
+ if (!('leaf' in t))
210
+ return `.split ${row(t.split)}\n (${lean(t.yes)})\n (${lean(t.no)})`;
211
+ const { path, base } = leafList[leafAt++], prods = t.products ?? [], squares = t.squares ?? [];
183
212
  const rows = withProducts(base, prods, squares).map(r => r.lin), n0 = base.length;
184
213
  // Only the rows the derivation and the products read, renumbered (the squares follow them, then the products, in
185
214
  // order); cuts carry their quotient row (Cert's format).
@@ -226,12 +255,17 @@ export function encode(ex, tamper = false, dropStep = true, globalVars, globalMa
226
255
  if ('fact' in src)
227
256
  return `.fact ${src.fact}`;
228
257
  const j = 'pair' in src ? pairs.indexOf([...src.pair].sort().join('\n')) : col.get('nat' in src ? src.nat : src.term) ?? -1;
229
- return j < 0 ? '.any' : 'pair' in src ? `.eq ${j}` : 'nat' in src ? `.nat ${j}` : `.term ${j}`;
258
+ return j < 0 ? '.any' : 'pair' in src ? `.eq ${eqAt.get(j)}` : 'nat' in src ? `.nat ${j}` : `.term ${j}`;
230
259
  };
231
- const at2 = list(used.map(u => `(${row(rows[u])}, ${hint(base[u].src)})`)), rest = `${list(steps)} ${list(eqs)} ${list(prods.map(([i, j]) => `(${idx(i)}, ${idx(j)})`))}`;
260
+ // (slim: only the pairs a row's hint names, renumbered; Lean reads the rest only for a row of no hint, which a slim
261
+ // table has none of)
262
+ const pairAt = (src) => (src && 'pair' in src ? pairs.indexOf([...src.pair].sort().join('\n')) : -1);
263
+ const keepEqs = slim && !whole ? [...new Set(used.map(u => pairAt(base[u].src)).filter(j => j >= 0))] : eqs.map((_, j) => j);
264
+ const eqAt = new Map(keepEqs.map((j, i) => [j, i]));
265
+ const at2 = list(used.map(u => `(${row(rows[u])}, ${hint(base[u].src)})`)), rest = `${list(steps)} ${list(keepEqs.map(j => eqs[j]))} ${list(prods.map(([i, j]) => `(${idx(i)}, ${idx(j)})`))}`;
232
266
  return squares.length ? `.leafSq ${at2} ${list(squares.map(row))} ${rest}` : `.leafAt ${at2} ${rest}`;
233
267
  };
234
- return { tab: list(tab), hyps: list(goal.hyps.map(c)), tree: lean(tree, new Map()), cols, c, e };
268
+ return { tab: list(tab), tabCols: tab, hyps: list(goal.hyps.map(c)), tree: lean(tree), cols, c, e };
235
269
  }
236
270
  export function goalToLean(ex, i, tamper = false, dropStep = true) {
237
271
  const { tab, hyps, tree, cols } = encode(ex, tamper, dropStep);
@@ -640,7 +674,7 @@ example : (checkSMachine ${id}_tampered ${id}_proof && sliveOk ${id}_tampered ${
640
674
  theorem ${id}_orig_laws : ∀ ρ M, SReach ${id}_ghosted.orig ρ M → ∀ e ∈ ${id}_proof, avoids ${id}_ghosted.vars e.law.holds = true → e.Holds ρ M :=
641
675
  sghost_laws ${id}_ghosted ${id}_ghostOk ${id}_proof ${id}_check` });
642
676
  const allSteps = claimSteps();
643
- const r = proveStepLaws(m, laws, allSteps);
677
+ const r = proveStepLawsOnce(m, laws, allSteps);
644
678
  if (!r.proven || !r.proof)
645
679
  throw new Error(`${id}: not proven`);
646
680
  const lp = r.proof.laws, si = r.proof.instances;
@@ -830,18 +864,31 @@ example : checkSMachine ${id}_tampered ${id}_proof = false := by ${checkBy}` : '
830
864
  // liveness's), carried to the machine by Bridge.lean's cyc_machine_sound and BridgeRules.lean's cyc_kstep_sound,
831
865
  // sleads_of_holds (liveness, loops), hreach_plain (histories) and cyc_ghost_holds (ghosts), and each theorem stated as the
832
866
  // 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).
867
+ // `<id>.lawN`, `<id>_stepN_holds`, `<id>_liveN_holds`, `<id>_halts`; a machine with flags that hold at every key through
868
+ // its sk machine (BridgeRules.lean's q bridges). What the cyclic route doesn't cover goes the usual way: a law that reads
869
+ // a map at a key read from a map, and whatever no certificate is found for within the deadline (`certReport`).
870
+ // The usual proof of a machine's laws and step laws, made once per toLean: an item exported the usual way after its
871
+ // cyclic certificate wasn't found (cycMachineProof) reads the proof that search started from, not one made again. It
872
+ // is made with no cyclic certificate of its own (quietly): the export makes the certificate it exports from the usual
873
+ // trees (cycMachineProof), and the usual export reads the trees, so a certificate made here is never read.
874
+ const usualMade = new Map();
875
+ function proveStepLawsOnce(m, laws, stepLaws) {
876
+ const key = JSON.stringify([m, laws, stepLaws]), had = usualMade.get(key);
877
+ if (had)
878
+ return had;
879
+ const r = quietly(() => proveStepLaws(m, laws, stepLaws));
880
+ usualMade.set(key, r);
881
+ return r;
882
+ }
836
883
  const envOf = () => globalThis.process?.env;
837
- export const cyclicCerts = () => envOf()?.GDP_CERT !== 'usual';
884
+ export const cyclicCerts = certMode;
838
885
  let certsOn = false, cycTexts = 0;
839
886
  // What the last toLean exported from cyclic certificates, and what went the usual way (and why).
840
887
  export const certReport = { cyclic: [], usual: [], translatedSteps: [] };
841
888
  // Each item's cyclic certificate is made within one deadline (GDP_CERT_MS, 10 s by default), every goal it grows (the
842
889
  // translation's leaves grown again, the searched nodes) bounded by it: past it, the item goes the usual way.
843
890
  function cycMachineProof(m, laws, allSteps, allows = []) {
844
- const r = proveStepLaws(m, laws, allSteps);
891
+ const r = proveStepLawsOnce(m, laws, allSteps);
845
892
  if (!r.proven || !r.proof)
846
893
  return { why: 'not proven the usual way' };
847
894
  const until = performance.now() + certMs();
@@ -850,13 +897,14 @@ function cycMachineProof(m, laws, allSteps, allows = []) {
850
897
  function cycMachineProofBy(m, laws, allSteps, allows, r, until) {
851
898
  if (!r.proof)
852
899
  return { why: 'not proven the usual way' };
900
+ const tStart = performance.now();
853
901
  const lp = r.proof.laws, instOf = (sl) => r.proof.perStep?.[sl.name] ?? r.proof.instances;
854
902
  // A machine with flags that hold at every key: its sk machine's nodes, the flags' closing facts read where each node
855
903
  // reads the run.
856
904
  const q = hasQuantified(m), mm = skMachine(m);
857
905
  const L = laws.length, mine = (facts, lawNodes) => searchedStepNodes(m, laws, allSteps.map(sl => ({ law: sl, instances: instOf(sl) })), facts, lawNodes);
858
906
  const tries = [];
859
- if (!q && routeOf() !== 'search' && allSteps.length) {
907
+ if (routeOf() !== 'search' && allSteps.length) {
860
908
  const usualSteps = allSteps.map(sl => { const inst = instOf(sl), g = stepLawObligations(m, laws, inst, sl)[0]; return { sl, inst, goal: g, tree: r.proof.trees[g.name] }; });
861
909
  const tr = usualSteps.every(u => u.tree) ? machineNodes(m, laws, lp.instances, { trees: lp.trees, steps: usualSteps }) : undefined;
862
910
  if (tr && !tr.why && tr.specs.length === 3 * L + 2 * allSteps.length) {
@@ -870,15 +918,18 @@ function cycMachineProofBy(m, laws, allSteps, allows, r, until) {
870
918
  tries.push({ facts: tr.facts, specs: tr.specs, lawNodes: tr.specs, stepAt: allSteps.map((_, j) => 3 * L + 2 * j + 1), translated: true });
871
919
  }
872
920
  }
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)) {
921
+ for (const base of [...(routeOf() !== 'search' ? [machineNodes(m, laws, lp.instances, { trees: lp.trees })] : []), machineNodes(m, laws, lp.instances)].filter(b => !b.why)) {
875
922
  tries.push({ facts: base.facts, specs: [...base.specs, ...mine(base.facts, base.specs)], lawNodes: base.specs, stepAt: allSteps.map((_, j) => base.specs.length + j) });
876
923
  }
877
924
  let last = 'no nodes';
925
+ const t00 = tStart;
878
926
  for (const { facts, specs: specs0, lawNodes, stepAt, translated } of tries) {
879
927
  if (performance.now() > until)
880
928
  return { why: `past the certificate's deadline (${certMs()} ms)` };
929
+ const tTry = performance.now();
881
930
  let specs = specs0, v = proveCyclic(facts, specs);
931
+ if (envOf()?.GDP_LEAN_TIMES === '1')
932
+ console.error(`lean try ${m.name}: ${translated ? 'translated' : 'laws + searched steps'}, ${specs.length} nodes, ${v.proven && v.checked ? 'proven' : 'not'}, ${Math.round(performance.now() - tTry)} ms (${Math.round(tTry - t00)} ms building before)`);
882
933
  if (!(v.proven && v.checked)) {
883
934
  last = performance.now() > until ? `past the certificate's deadline (${certMs()} ms)` : v.proven ? `not checked (${v.why})` : `${v.node}: ${v.goal}: ${v.stuck}`;
884
935
  continue;
@@ -918,8 +969,9 @@ export function cycMachineToLean(id, given, laws, opts = {}) {
918
969
  const hm = given.history ? { ...given, history: { ...given.history, prune: undefined } } : undefined;
919
970
  const hplain = hm ? historyMachine(hm) : undefined, m = hplain ? hplain.machine : given;
920
971
  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' };
972
+ const why = uncoveredWhy(m, laws);
973
+ if (why)
974
+ return { why };
923
975
  // With flags that hold at every key, the sk machine's encoding and BridgeRules.lean's q bridges.
924
976
  const mq = skMachine(m), Q = hasQuantified(m) ? 'Q' : '', q = hasQuantified(m) ? 'q' : '';
925
977
  const lives = (opts.liveness ?? []).map(l => ({ l, ...livenessParts(m, l) }));
@@ -948,6 +1000,15 @@ export function cycMachineToLean(id, given, laws, opts = {}) {
948
1000
  const state = Object.keys(m.state), carried = m.inputMaps ?? [];
949
1001
  const primesL = () => list(state.map(s2 => `(${ix(s2)}, ${ix(`${s2}'`)})`)), primeMapsL = () => list(Object.keys(m.maps ?? {}).map(mp => `(${mx(mp)}, ${mx(`${mp}'`)})`));
950
1002
  const node = (i) => `${id}_node${i}`;
1003
+ // What every law's and step law's check reads alike, proven once: the exported facts among the encoding's, and (with
1004
+ // no flag at every key) the encoding's shape and terms at no keys, which a law's at its keys implies. Each check then
1005
+ // reads those as true (simp only, unfolding the check), not computed again per law.
1006
+ const sharedOk = [`${id}_factsIn`, ...(Q ? [] : [`${id}_shape`, `${id}_terms`])].join(', ');
1007
+ const okLemmas = `theorem ${id}_factsIn : cyc${Q}FactsIn ${id}_machine ${id}_enc ${id}_facts = true := by ${checkBy}${Q ? '' : `
1008
+ theorem ${id}_shape : cycShapeOk ${id}_machine ${id}_enc [] = true := by ${checkBy}
1009
+ theorem ${id}_terms : cycTermsOk ${id}_machine ${id}_enc [] = true := by ${checkBy}`}`;
1010
+ const mOk = `by simp only [cyc${Q}MOk, ${sharedOk}]; ${checkBy}`, kOk = `by simp only [cyc${Q}KOk, cyc${Q}KOkAt, ${sharedOk}]; ${checkBy}`;
1011
+ const aOk = `by simp only [cyc${Q}AOk, ${sharedOk}]; ${checkBy}`;
951
1012
  const kstep = (sk, i) => `cyc_${q}kstep_sound ${id}_machine ${id}_enc ${id}_facts ${id}_nodes ${id}_check ${sk} ${node(i)} ${node(i)}_mem`;
952
1013
  // The machine as the usual export defines it: with a history its plain machine, with ghosts the ghosted one, a loop's.
953
1014
  const orig = { ...m, state: Object.fromEntries(Object.entries(m.state).filter(([n2]) => !gv.includes(n2))), step: { ...m.step, state: Object.fromEntries(Object.entries(m.step.state ?? {}).filter(([n2]) => !gv.includes(n2))) } };
@@ -985,7 +1046,7 @@ def ${id}_machine : SMachine := ${id}_loop.machine` : `def ${id}_machine : SMach
985
1046
  const lawBlocks = laws.map((l, j) => `-- ${l.name}
986
1047
  def ${id}_law${j} : C := ${c(l.holds)}
987
1048
  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}
1049
+ theorem ${id}_law${j}_ok : cyc${Q}MOk ${id}_machine ${id}_enc ${id}_facts ${id}_law${j} ${keysL(l)} ${node(j)} = true := ${mOk}
989
1050
  theorem ${id}_law${j}_holds : ∀ ρ M, SReach ${id}_machine ρ M → ∀ ρ' : Nat → Nat, (∀ i, i ∉ ${keysL(l)} → ρ' i = ρ i) → evalC ρ' M ${id}_law${j} = true :=
990
1051
  cyc_${q}machine_sound ${id}_machine ${id}_enc ${id}_facts ${id}_nodes ${id}_check ${id}_law${j} ${keysL(l)} ${node(j)} ${node(j)}_mem ${id}_law${j}_ok`).join('\n');
991
1052
  const keys2 = (l) => list((l.keys ?? []).map(k2 => String(ix(k2))));
@@ -1026,7 +1087,7 @@ def ${id}_step${j} : SKStep := ⟨⟨${list(kWhens(mq, sl).map(c))},
1026
1087
  ${list(mren)},
1027
1088
  [], [], [], [], [],
1028
1089
  ${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}
1090
+ theorem ${id}_step${j}_ok : cyc${Q}KOk ${id}_machine ${id}_enc ${id}_facts ${id}_step${j} ${node(stepAt[j])} = true := ${kOk}
1030
1091
  theorem ${id}_step${j}_holds : SKStepHolds ${id}_machine ${id}_step${j} :=
1031
1092
  ${kstep(`${id}_step${j}`, stepAt[j])} ${id}_step${j}_ok`;
1032
1093
  });
@@ -1044,7 +1105,7 @@ theorem ${lid}_allows : ∀ h ∈ ${lid}.helps, ∃ wit : List (Nat × E), disti
1044
1105
  intro h hh
1045
1106
  simp only [${lid}, List.mem_cons, List.not_mem_nil, or_false] at hh
1046
1107
  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 ')}
1108
+ ${al.map((a, i) => `· exact ⟨${lid}_wit${i}, by ${checkBy}, by ${checkBy}, cyc_${q}allow_sound ${id}_machine ${id}_enc ${id}_facts ${id}_nodes ${id}_check _ ${node(a.node)} ${node(a.node)}_mem (${aOk})⟩`).join('\n ')}
1048
1109
  /-- ${l.name}, under weak fairness: along every run that takes each helpful request whenever, from some point on, it
1049
1110
  stays allowed, \`from\` is followed by \`to\`. -/
1050
1111
  theorem ${lid}_holds_wf (run : SRun ${id}_machine)
@@ -1056,13 +1117,13 @@ theorem ${lid}_holds_wf (run : SRun ${id}_machine)
1056
1117
  return `-- Liveness: ${l.name}
1057
1118
  def ${lid} : Live := ${liveLit(l, { e, c }, ix, primesL(), primeMapsL())}
1058
1119
  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})
1120
+ theorem ${lid}_stay : SKStepHolds ${id}_machine (liveK ${lid} ${lid}.pend (liveStayC ${lid})) := ${kstep('_', s0)} (${kOk})
1121
+ theorem ${lid}_cover : SKStepHolds ${id}_machine (liveK ${lid} ${lid}.pend (liveCoverC ${lid})) := ${kstep('_', c0)} (${kOk})
1061
1122
  theorem ${lid}_helps : ∀ h ∈ ${lid}.helps, SKStepHolds ${id}_machine (liveK ${lid} (liveHelpWhen ${lid} h) (liveHelpThen ${lid})) := by
1062
1123
  intro h hh
1063
1124
  simp only [${lid}, List.mem_cons, List.not_mem_nil, or_false] at hh
1064
1125
  rcases hh with ${helps.map(() => 'rfl').join(' | ')}
1065
- ${helps.map((_, i) => `· exact ${kstep('_', hAt(i))} (by ${checkBy})`).join('\n ')}
1126
+ ${helps.map((_, i) => `· exact ${kstep('_', hAt(i))} (${kOk})`).join('\n ')}
1066
1127
  /-- ${l.name}: along every run that takes each helpful request infinitely often, \`from\` is followed by \`to\`. -/
1067
1128
  theorem ${lid}_holds (run : SRun ${id}_machine)
1068
1129
  (fair : ∀ h ∈ ${lid}.helps, ∀ n, ∃ m, n ≤ m ∧ ∀ p ∈ h.2, (run.req m).1 p.1 = evalE (run.st m).1 (run.st m).2 p.2) :
@@ -1079,7 +1140,7 @@ theorem ${id}_orig_laws : ∀ ρ M, SReach ${id}_ghosted.orig ρ M → ∀ e ∈
1079
1140
  ${laws.length ? `${oneOf('he')}
1080
1141
  rcases he with ${laws.map(() => 'rfl').join(' | ')}
1081
1142
  ${laws.map((l, j) => `· exact cyc_machine_holds (cyc_ghost_holds ${id}_ghosted ${id}_ghostOk ${id}_law${j} ${keysL(l)} hav ${id}_law${j}_holds ρ M hr)`).join('\n ')}` : `simp [${id}_mlaws] at he`}` : '';
1082
- const refineBlock = rf ? `theorem ${id}_allowed : SKStepHolds ${id}_machine (srStep ${id}_refined ${primesL()} ${primeMapsL()} noSProof) := ${kstep('_', stepAt[0])} (by ${checkBy})
1143
+ const refineBlock = rf ? `theorem ${id}_allowed : SKStepHolds ${id}_machine (srStep ${id}_refined ${primesL()} ${primeMapsL()} noSProof) := ${kstep('_', stepAt[0])} (${kOk})
1083
1144
  theorem ${id}_refines : ∀ ρ M, SReach ${id}_refined.impl ρ M → ∃ ρc Mc σ Ms, SReach ${id}_refined.full ρc Mc ∧ Agree ${id}_refined.SV ρc ρ
1084
1145
  ∧ (∀ mp, mp ∉ ${id}_refined.SM → Mc mp = M mp) ∧ SReach ${id}_refined.spec σ Ms ∧ Agree ${id}_refined.X σ ρc
1085
1146
  ∧ (∀ mp, mp ∉ ${id}_refined.IM → mp ∉ ${id}_refined.spec.carried → Ms mp = Mc mp) ∧ ∀ e ∈ ${id}_mlaws, holdsOver e.1 Mc e.2 ρc :=
@@ -1093,6 +1154,7 @@ theorem ${id}_refines : ∀ ρ M, SReach ${id}_refined.impl ρ M → ∃ ρc Mc
1093
1154
  ${machineDef}
1094
1155
  def ${id}_enc : CycEnc := ${enc}
1095
1156
  ${cycProofText(id, p, [], vars, maps)}
1157
+ ${okLemmas}
1096
1158
  ${lawBlocks}
1097
1159
  ${lawsThm}
1098
1160
  ${hm ? histBlock() : readable(id, laws, vars, maps, { binders: '(ρ : Nat → Nat) (μ : Nat → Nat → Nat)', hyp: `SReach ${id}_machine ρ μ`, call: '',
@@ -1395,6 +1457,7 @@ export function toLean(items, opts = {}) {
1395
1457
  certReport.usual = [];
1396
1458
  certReport.translatedSteps = [];
1397
1459
  cycTexts = 0;
1460
+ usualMade.clear();
1398
1461
  // The proofs found with only the instances their leaves use: the same laws, from fewer hypotheses for Lean to build.
1399
1462
  const wasMinimal = minimal.on;
1400
1463
  minimal.on = true;
@@ -1431,13 +1494,13 @@ function toLeanWith(items, opts) {
1431
1494
  return `${pairing}${text}\n`;
1432
1495
  }).join('');
1433
1496
  for (let item of items) {
1434
- const id = ident(item.id);
1497
+ const id = ident(item.id), tItem = performance.now();
1435
1498
  if ('machine' in item) {
1436
1499
  const m = item.machine;
1437
1500
  // Laws with some key go in as the prover proved them: with its witness in place (the stronger law).
1438
1501
  const someKey = (c) => JSON.stringify(c).includes('"op":"all"');
1439
1502
  if ([...item.laws.map(l => l.holds), ...(item.stepLaws ?? []).map(sl => sl.then)].some(someKey)) {
1440
- const r = proveStepLaws(m, item.laws, item.stepLaws ?? []);
1503
+ const r = proveStepLawsOnce(m, item.laws, item.stepLaws ?? []);
1441
1504
  if (!r.proven)
1442
1505
  throw new Error(`${item.id}: not proven (${r.failed?.stuck ?? ''})`);
1443
1506
  item = { ...item, laws: r.laws ?? item.laws, ...(item.stepLaws ? { stepLaws: r.stepLaws ?? item.stepLaws } : {}) };
@@ -1506,6 +1569,8 @@ function toLeanWith(items, opts) {
1506
1569
  parts.push(goalToLean({ name: item.id, goal: item.goal, tree: grown(item.goal) }, n, false).replace(new RegExp(`\\bt${n}(_goal|_tree)?\\b`, 'g'), (_, suffix) => `${id}${suffix ?? ''}`));
1507
1570
  theorems.push(id);
1508
1571
  }
1572
+ if (envOf()?.GDP_LEAN_TIMES === '1')
1573
+ console.error(`lean item ${item.id}: ${Math.round(performance.now() - tItem)} ms, ${parts.at(-1)?.length ?? 0} bytes`);
1509
1574
  }
1510
1575
  // The file the items need: Live.lean for a liveness or a loop, Sets.lean (which imports Steps.lean) for any machine.
1511
1576
  return `import History
@@ -1558,10 +1623,27 @@ ${cycProofText(id, p, tampered, vars, maps)}${bridge ? `\n${cycBridgeLean(id, p,
1558
1623
  // node's membership, the check and cyclic_sound's theorem; each tampered proof's check computed to false.
1559
1624
  function cycProofText(id, p, tampered, vars, maps) {
1560
1625
  const { e, c } = leanTerms(vars, maps, id);
1626
+ // (data, not code: no compiled code for Lean to make, unless native_decide runs it)
1627
+ const d = checkBy === 'native_decide' ? 'def' : 'noncomputable def';
1561
1628
  const ix = (name) => vars.indexOf(name);
1562
1629
  const facts = list(p.facts.map(f => `⟨${list(f.keys.map(key => String(ix(key))))}, ${c(f.holds)}⟩`));
1563
1630
  // A tampered proof keeps the proof's tables and trees (made for the proof's own goals) under its changed nodes.
1564
- const enc = (g, tree) => { const x = encode({ name: `${id}: ${g.name}`, goal: g, tree }, false, true, vars, maps); return `(${x.tab}, ${x.tree})`; };
1631
+ // Each table once (`<id>_tabN`): a node's goals (its own, each use's premises and descent) share its hypotheses, so
1632
+ // most of their tables are the same columns, and Lean elaborates each list once instead of once per goal.
1633
+ // So too each term column's term (an open choice the tables share) and each goal's table and tree where two goals
1634
+ // have the same (a premise read at the same terms by two nodes).
1635
+ const shared = (prefix) => {
1636
+ const seen = new Map();
1637
+ return { seen, name: (text) => { let name = seen.get(text); if (!name)
1638
+ seen.set(text, (name = `${id}_${prefix}${seen.size}`)); return name; } };
1639
+ };
1640
+ const terms = shared('term'), tabs = shared('tab'), goals = shared('goal');
1641
+ const enc = (g, tree) => {
1642
+ const x = encode({ name: `${id}: ${g.name}`, goal: g, tree }, false, true, vars, maps, true);
1643
+ const tab = list(x.tabCols.map(col => (col.startsWith('.term (') && col.length > 48 ? `.term ${terms.name(col.slice(6))}` : col)));
1644
+ const pair = `(${tabs.name(tab)}, ${x.tree})`;
1645
+ return pair.length > 200 ? goals.name(pair) : pair;
1646
+ };
1565
1647
  const nodeLits = (q) => q.nodes.map((n, i) => {
1566
1648
  const o = p.nodes[i];
1567
1649
  const uses = n.uses.map((u, j) => {
@@ -1571,19 +1653,23 @@ function cycProofText(id, p, tampered, vars, maps) {
1571
1653
  return { hyps: list(n.hyps.map(c)), concl: c(n.concl), rest: `${list(n.rank.map(e))}, ${list(n.insts.map(t => `(${t.fact}, ${list(t.at.map(e))})`))},\n ${list(uses)},\n ${enc(mainGoal(p.facts, p.nodes, o), o.main)}` };
1572
1654
  });
1573
1655
  const nodes = (q) => list(nodeLits(q).map(x => `⟨${x.hyps}, ${x.concl}, ${x.rest}⟩`));
1574
- return `def ${id}_facts : List Fact := ${facts}
1575
- ${nodeLits(p).map((x, i) => `def ${id}_hyps${i} : List C := ${x.hyps}
1576
- def ${id}_concl${i} : C := ${x.concl}
1577
- def ${id}_node${i} : CNode := ⟨${id}_hyps${i}, ${id}_concl${i}, ${x.rest}⟩
1656
+ const own = nodeLits(p), bad = tampered.map(t => nodes(t.proof));
1657
+ return `${d} ${id}_facts : List Fact := ${facts}
1658
+ ${[...terms.seen].map(([t, name]) => `${d} ${name} : E := ${t}`).join('\n')}
1659
+ ${[...tabs.seen].map(([tab, name]) => `${d} ${name} : List VDef := ${tab}`).join('\n')}
1660
+ ${[...goals.seen].map(([g, name]) => `${d} ${name} : List VDef × Tree := ${g}`).join('\n')}
1661
+ ${own.map((x, i) => `${d} ${id}_hyps${i} : List C := ${x.hyps}
1662
+ ${d} ${id}_concl${i} : C := ${x.concl}
1663
+ ${d} ${id}_node${i} : CNode := ⟨${id}_hyps${i}, ${id}_concl${i}, ${x.rest}⟩
1578
1664
  theorem ${id}_node${i}_hyps : ${id}_node${i}.hyps = ${id}_hyps${i} := rfl
1579
1665
  theorem ${id}_node${i}_concl : ${id}_node${i}.concl = ${id}_concl${i} := rfl`).join('\n')}
1580
- def ${id}_nodes : List CNode := ${list(p.nodes.map((_, i) => `${id}_node${i}`))}
1666
+ ${d} ${id}_nodes : List CNode := ${list(p.nodes.map((_, i) => `${id}_node${i}`))}
1581
1667
  ${p.nodes.map((_, i) => `theorem ${id}_node${i}_mem : ${id}_node${i} ∈ ${id}_nodes := by simp only [${id}_nodes, List.mem_cons, true_or, or_true]`).join('\n')}
1582
1668
  theorem ${id}_check : checkCyclic ${id}_facts ${id}_nodes = true := by ${checkBy}
1583
1669
  theorem ${id}_sound : ∀ (maps : Nat → Nat → Nat) env, FactsHold ${id}_facts env maps → ∀ n ∈ ${id}_nodes, (∀ c ∈ n.hyps, evalC env maps c = true) → evalC env maps n.concl = true :=
1584
1670
  cyclic_sound ${id}_facts ${id}_nodes ${id}_check
1585
1671
  ${tampered.map((t, j) => `-- TAMPERED: ${t.what}. Lean computes the check to false.
1586
- def ${id}_tampered${j} : List CNode := ${nodes(t.proof)}
1672
+ ${d} ${id}_tampered${j} : List CNode := ${bad[j]}
1587
1673
  example : checkCyclic ${id}_facts ${id}_tampered${j} = false := by ${checkBy}`).join('\n')}`;
1588
1674
  }
1589
1675
  function cycScope(all, bridge, extra = []) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdp-wired",
3
- "version": "0.19.0",
3
+ "version": "0.20.0",
4
4
  "description": "An automatic prover for stateful rules (budgets, caps, windows, workflows, policies): laws proven with no proof to write, checked again in Lean, counterexamples found on the GPU, the rule shipped as code",
5
5
  "keywords": [
6
6
  "verification",