gdp-wired 0.12.2 → 0.12.4

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.
package/README.md CHANGED
@@ -53,8 +53,8 @@ All of these are proven automatically, and each proof is also a Lean theorem:
53
53
  - **Maps, sets and lists of names**: laws at every key ("no day passes its cap"), sets a request carries, conditions
54
54
  about every key or every two keys ("a batch is taken only when its values increase"), lists kept in order
55
55
  (`examples/sets.ts`, `examples/lists.ts`, `examples/pairs.ts`).
56
- - **Step laws**: what one step does ("a balance only goes down", "a grant adds one"), and laws over several steps in a
57
- row.
56
+ - **Step laws**: what one step does ("a balance only goes down", "a grant adds one"), and laws over up to 64 steps in
57
+ a row (`examples/steps.ts`).
58
58
  - **Histories**: sums and queries over a log of requests, like a sliding-window rate limit.
59
59
  - **Orderings at any distance**: "a refund never follows a release", through ghost state that watches the rule.
60
60
  - **Liveness**: "every ticket issued is served", in every run that keeps sending the requests that help, including under
@@ -230,8 +230,10 @@ const soon = retry.law('the next try is at most 64 after the last request', ({ne
230
230
  carried map of its running values whose equations every request is assumed to meet (the encoding computes them from the
231
231
  array, and a request that doesn't meet them isn't allowed), read as `Lines.fold.total`; `Lines.every`, `some`, `has`,
232
232
  `increasing(by)`, `at(i)`. A lemma about the folds (`({taxed, total}) => taxed.atMost(total)`) is proven by induction
233
- over the items when the rule is built, then assumed at every prefix. A fold's start and step, and a lemma, may read the
234
- params (`{from: ({fee}) => fee, step: (acc, line, {ceil}) => acc.plus(min(line.amount, ceil))}`); a request is then
233
+ over the items when the rule is built, then assumed at every prefix; a decision (`decide`) assumes the equations and
234
+ lemmas too, at the list's end and wherever it reads the list. A lemma reads the folds, the prefix's length and the
235
+ params, not the items: a fact about every item is a fold's (a count of the items that meet it). A fold's start and
236
+ step, and a lemma, may read the params (`{from: ({fee}) => fee, step: (acc, line, {ceil}) => acc.plus(min(line.amount, ceil))}`); a request is then
235
237
  encoded with them, `rule.encodeInput(values, params)` (`examples/invoices.ts`).
236
238
  - **Folds over trees** (`fold(name).params(...).inputs(...).leaf(env => ({...})).node((env, left, right) => ({...}))`,
237
239
  `proveFold`): a tree as a value (`{leaf: inputs}` or `{node: inputs, left, right}`), each leaf's values from its inputs
@@ -336,6 +338,10 @@ or two lines and check in 25–44 ms, against 74–117 lines of hand proof and 1
336
338
  The proved Bend and Lean rules keep lists and scan them; gdp-wired runs maps and a log (unpruned in this window rule).
337
339
  Lean, compiled natively, wins where the state stays small. Details in [`docs/comparison.md`](docs/comparison.md).
338
340
 
341
+ Proving and checking: a law over 64 steps proves in about 5 s, and Lean checks its theorem in 47 s (6 s by
342
+ `native_decide`); a law over 32 steps proves in about a second. What changed since the first release, and by how much:
343
+ [`CHANGELOG.md`](CHANGELOG.md).
344
+
339
345
  ## Layout
340
346
 
341
347
  | Path | What |
@@ -343,7 +349,7 @@ Lean, compiled natively, wins where the state stays small. Details in [`docs/com
343
349
  | `core/` | Machines, the interpreter and JS compiler, the builders, composition (`compose.ts`), the linear solver and its checker, Houdini invariants, models, the first Lean export |
344
350
  | `kernel/` | The trusted kernel and the untrusted prover, with soundness fuzz (`kernel.test.ts`, `history.test.ts`) |
345
351
  | `lean-kernel/` | The kernel's Lean twin, its soundness proofs, and the exported trees |
346
- | `examples/` | The builders at work: `domains/` (187 rules across 21 domains, each with laws and broken variants), `harness.ts` (the triple check), `readme.ts`, `budget.ts`, `gdp.ts` (gdp-ts's example, its checks proven), `datatypes.ts` (records and variants), `lemmas.ts` (recurrences and their lemmas), `invoices.ts` (lists a request carries), `folds.ts` (folds over trees), `trees.ts` (trees and lists of lists in the state), `composition.ts`, `sets.ts`, `quantifiers.ts`, `functions.ts`, `lists.ts`, `sizes.ts`, `liveness.ts`, `steps.ts` (laws over up to 16 steps), and `jev-fabric/` (a Bend project's 59 proven laws, ported) |
352
+ | `examples/` | The builders at work: `domains/` (187 rules across 21 domains, each with laws and broken variants), `harness.ts` (the triple check), `readme.ts`, `budget.ts`, `gdp.ts` (gdp-ts's example, its checks proven), `datatypes.ts` (records and variants), `lemmas.ts` (recurrences and their lemmas), `invoices.ts` (lists a request carries), `folds.ts` (folds over trees), `trees.ts` (trees and lists of lists in the state), `composition.ts`, `sets.ts`, `quantifiers.ts`, `functions.ts`, `lists.ts`, `sizes.ts`, `liveness.ts`, `steps.ts` (laws over up to 64 steps), and `jev-fabric/` (a Bend project's 59 proven laws, ported) |
347
353
  | `docs/` | The comparison with Bend and Lean, and the full scope |
348
354
  | `compare/` | The same three rules in gdp-wired, Bend and plain Lean, checked and measured |
349
355
  | `research/` | Everything around the library ([`research/README.md`](research/README.md)): the GPU searches (`search/`, `hw/`, `modal-gpu/`), invariant and rank synthesis (`synth/`), relational laws and guard synthesis (`next/`), bit-vector laws (`sat/`), the enumeration and benchmarks (`enum/`, `gpu/`, `perf/`), the first Lean checker (`lean/`) and the first prototype, the audit path (`audit/`) |
@@ -371,7 +377,7 @@ bun test core/compile.test.ts core/runtime.test.ts kernel/history.test.ts
371
377
  cd kernel && bun run-kernel.ts && bun test kernel.test.ts
372
378
  ```
373
379
 
374
- The whole suite (122 jobs, about 20 minutes of wall time) runs on [Modal](https://modal.com):
380
+ The whole suite (159 jobs, about 6 minutes of wall time and 22 minutes of job time) runs on [Modal](https://modal.com):
375
381
 
376
382
  ```sh
377
383
  modal run modal/run.py --jobs modal/suite.json --out runs/modal/suite && sh modal/reap.sh
@@ -333,6 +333,10 @@ export declare class CarriedList<V, Fd extends string = never> {
333
333
  has(x: V): Cond;
334
334
  increasing(by: (item: V) => Term): Cond;
335
335
  equations(): Cond[];
336
+ equationsAt(keys: Term[]): Cond[];
337
+ private starts;
338
+ private get keyed();
339
+ private atKey;
336
340
  private get readsParams();
337
341
  private p;
338
342
  private running;
package/dist/core/dsl.js CHANGED
@@ -875,6 +875,43 @@ export class CellsRef {
875
875
  }
876
876
  }
877
877
  const fromOf = (d, p) => (typeof d.from === 'number' ? d.from : d.from(p));
878
+ // The keys a condition reads any of these maps at, as terms.
879
+ function keysRead(cs, maps) {
880
+ const out = [];
881
+ const e = (t) => {
882
+ if (t.op === 'get') {
883
+ if (maps.includes(t.map))
884
+ out.push(new Term(t.key));
885
+ e(t.key);
886
+ }
887
+ else if (t.op === 'if') {
888
+ c(t.c);
889
+ e(t.then);
890
+ e(t.else);
891
+ }
892
+ else if (t.op === 'mul')
893
+ e(t.a);
894
+ else if (t.op === 'add' || t.op === 'sub' || t.op === 'min' || t.op === 'max' || t.op === 'prod') {
895
+ e(t.a);
896
+ e(t.b);
897
+ }
898
+ };
899
+ const c = (x) => {
900
+ if (x.op === 'not')
901
+ c(x.x);
902
+ else if (x.op === 'and' || x.op === 'or')
903
+ x.xs.forEach(c);
904
+ else if (x.op === 'all')
905
+ c(x.c);
906
+ else if (x.op !== 'flag') {
907
+ e(x.a);
908
+ e(x.b);
909
+ }
910
+ };
911
+ cs.forEach(c);
912
+ return out;
913
+ }
914
+ const uniqueTerms = (ts) => [...new Map(ts.map(t => [JSON.stringify(t.e), t])).values()];
878
915
  export class CarriedList {
879
916
  name;
880
917
  kind;
@@ -908,15 +945,16 @@ export class CarriedList {
908
945
  increasing(by) { return everyKey(k => k.plus(1).lt(this.len).implies(by(this.at(k)).lt(by(this.at(k.plus(1)))))); }
909
946
  // What every request is assumed to meet: each fold's start, and at every key one condition (the item's own laws and
910
947
  // each fold's step below the length, each lemma up to it), so the prover closes one quantifier, not one per part.
911
- equations() {
948
+ equations() { return [...this.starts(), ...(this.keyed ? [everyKey(k => this.atKey(k))] : [])]; }
949
+ // The same at the keys given, without the quantifier: what a decision assumes of the list (Rule.decide).
950
+ equationsAt(keys) { return [...this.starts(), ...(this.keyed ? keys.map(k => this.atKey(k)) : [])]; }
951
+ starts() { return Object.entries(this.folds).map(([f, d]) => this.map(`${this.name}$${f}`).at(0).eq(fromOf(d, this.p()))); }
952
+ get keyed() { return this.kind.invariant(this.kind.value(this.name)).length + Object.keys(this.folds).length + Object.keys(this.lemmas).length > 0; }
953
+ atKey(k) {
912
954
  const folds = Object.entries(this.folds), lemmas = Object.values(this.lemmas);
913
- const at = (k) => {
914
- const x = this.at(k), steps = folds.map(([f, d]) => this.map(`${this.name}$${f}`).at(k.plus(1)).eq(d.step(this.map(`${this.name}$${f}`).at(k), x, this.p())));
915
- const below = [...this.kind.invariant(x), ...steps];
916
- return all(...(below.length ? [k.lt(this.len).implies(all(...below))] : []), ...(lemmas.length ? [k.atMost(this.len).implies(all(...lemmas.map(l => l(this.running(k), k, this.p()))))] : []));
917
- };
918
- const any = this.kind.invariant(this.kind.value(this.name)).length + folds.length + lemmas.length > 0;
919
- return [...folds.map(([f, d]) => this.map(`${this.name}$${f}`).at(0).eq(fromOf(d, this.p()))), ...(any ? [everyKey(at)] : [])];
955
+ const x = this.at(k), steps = folds.map(([f, d]) => this.map(`${this.name}$${f}`).at(k.plus(1)).eq(d.step(this.map(`${this.name}$${f}`).at(k), x, this.p())));
956
+ const below = [...this.kind.invariant(x), ...steps];
957
+ return all(...(below.length ? [k.lt(this.len).implies(all(...below))] : []), ...(lemmas.length ? [k.atMost(this.len).implies(all(...lemmas.map(l => l(this.running(k), k, this.p()))))] : []));
920
958
  }
921
959
  // Whether a fold reads the params (its start a function, or its step taking them).
922
960
  get readsParams() { return Object.values(this.folds).some(d => typeof d.from === 'function' || d.step.length > 2); }
@@ -1704,11 +1742,15 @@ export class Rule {
1704
1742
  return typeof spec.helpful === 'function' ? { ...base, request: req(spec.helpful) } : { ...base, helpful: spec.helpful.map(h => ({ ...(h.when ? { when: h.when(env).c } : {}), request: req(h.request) })) };
1705
1743
  }
1706
1744
  // A decision: whenever the params and the request meet `given`, `then` holds. Decisions don't read the state; the
1707
- // functions' facts are assumed at the arguments the decision reads them at.
1745
+ // functions' facts are assumed at the arguments the decision reads them at, and a carried list's equations (each
1746
+ // fold's start, its step and its lemmas) at the list's end, the key before it and every key the decision reads it at.
1708
1747
  decide(name, given, then) {
1709
1748
  const env = this.env({ flags: false, state: false, queries: false });
1710
1749
  const g = given(env), hyps = [...this.kindAssumptions(), ...this.spec.pre.map(f => f(env).c), ...this.spec.assume.map(f => f(env).c), ...(Array.isArray(g) ? g : [g]).map(c => c.c)];
1711
- const t = then(env).c, goal = { name, hyps: [...hyps, { op: 'not', x: t }] };
1750
+ const t = then(env).c;
1751
+ const lists = (this.spec.lists2 ?? []).map(([n]) => env[n]);
1752
+ const listed = lists.flatMap(l => l.equationsAt(uniqueTerms([l.len, l.len.minus(1), ...keysRead([...hyps, t], l.maps)])).map(c => c.c));
1753
+ const goal = { name, hyps: [...listed, ...hyps, { op: 'not', x: t }] };
1712
1754
  const facts = this.spec.fns?.length || this.spec.facts?.length ? factsFor(this.machine, goal) : [];
1713
1755
  return withSource({ name, rule: this, given: hyps, then: t, goal: { name, hyps: [...facts, ...goal.hyps] } }, { decision: { given: given, then: then, own: Array.isArray(g) ? g.length : 1 } });
1714
1756
  }
@@ -2349,7 +2391,9 @@ export function crossCheckDecision(d, opts = {}) {
2349
2391
  let checked = 0;
2350
2392
  for (let i = 0; i < (opts.trials ?? 500); i++) {
2351
2393
  const ps = r.randomParams(rand, bound), input = r.randomInput(rand, bound, ps), values = { ...ps, ...numbersOf(input) };
2352
- const st = { scalars: {}, maps: Object.fromEntries(Object.entries(input).filter((e) => typeof e[1] !== 'number').map(([n, cells]) => [n, new Map(cells)])) };
2394
+ // a map a request carries reads 0 past its cells (a list's encoding keeps no 0 cell)
2395
+ const carried = Object.entries(input).filter((e) => typeof e[1] !== 'number');
2396
+ const st = { scalars: {}, maps: Object.fromEntries(carried.map(([n, cells]) => [n, new Map(cells)])), defaults: Object.fromEntries(carried.map(([n]) => [n, 0])) };
2353
2397
  const keys = exactKeys(keysOf(bound, st), values, st);
2354
2398
  const sc = { vars: values, maps: Object.fromEntries(Object.entries(st.maps).map(([n, cells]) => [n, { cells, dflt: 0 }])), keys, flags: {} };
2355
2399
  const [g, t] = inScope(sc, () => { const env = r['env']({ flags: false, state: false, queries: false }), g = src.given(env); return [all(...(Array.isArray(g) ? g : [g])).ok, src.then(env).ok]; });
@@ -364,6 +364,30 @@ export function interpret(m) {
364
364
  // The same machine as JavaScript source: what an app ships. A step reads the state it was given (every query, flag,
365
365
  // scalar, key and value first), then writes the maps and the records in place, so it costs the same however large they
366
366
  // grow; the state passed in is consumed. fork copies a state to branch from.
367
+ // The runtime's helpers: a compiled module declares the ones it calls (and the ones those call), no others.
368
+ const RUNTIME = [
369
+ ['$n', `function $n(v, what) { if (typeof v !== 'number' || !Number.isSafeInteger(v) || v < 0) throw new RangeError(what + ' must be a natural below 2^53, not ' + v); return v; }`],
370
+ ['$x', `function $x(v) { if (v > 9007199254740991) throw new RangeError('a value passed 2^53 - 1, past which the proofs over the naturals no longer describe the run'); return v; }`],
371
+ ['$m', `function $m(v, what) { if (!(v instanceof Map)) throw new RangeError(what + ' must be a Map of naturals'); for (const [key, x] of v) { $n(key, 'a key of ' + what); $n(x, 'a cell of ' + what); } return v; }`],
372
+ ['$keys', `function $keys(maps, names) { const keys = new Set(); let past = 0; for (const c of Object.values(maps)) for (const key of c.keys()) past = Math.max(past, key + 1); for (const n of names) for (const key of maps[n].keys()) keys.add(key); return [...keys, past]; }`],
373
+ ['$breaks', `function $breaks(maps, reads, cmps) { const p = new Set([0]); for (const [n, d, minus] of reads) { for (const s of maps[n].keys()) { if (minus) p.add(s + d); else if (s >= d) p.add(s - d); } if (minus) p.add(d); } for (const [v, d, minus] of cmps) { if (minus) { p.add(v + d); p.add(d); } else if (v >= d) p.add(v - d); } const keys = new Set(); for (const q of p) for (let i = -1; i <= 2; i++) if (q + i >= 0) keys.add(q + i); const sorted = [...keys].sort((a, b) => a - b); sorted.push(sorted[sorted.length - 1] + 1); return sorted; }`],
374
+ ['$spread', `function $spread(keys, n) { const out = new Set(); for (const k of keys) for (let i = 0; i < n; i++) out.add(k + i); return [...out].sort((a, b) => a - b); }`],
375
+ ['$dense', `function $dense(keys, f) { const top = keys[keys.length - 1]; if (top > 1000000) throw new RangeError('a write at every key reads its key in arithmetic past a million keys (' + top + ')'); const d = f(top), cells = new Map(); for (let k = 0; k < top; k++) { const v = f(k); if (v !== d) cells.set(k, v); } return {cells, d}; }`],
376
+ ['$every', `function $every(keys, f) { const past = keys[keys.length - 1], d = f(past), cells = new Map(); for (let i = 0; i < keys.length - 1; i++) { const v = f(keys[i]); if (v !== d) cells.set(keys[i], v); } return {cells, d}; }`],
377
+ ];
378
+ function runtimeFor(code) {
379
+ const used = new Set();
380
+ for (let grew = true; grew;) {
381
+ grew = false;
382
+ const text = code + RUNTIME.filter(([name]) => used.has(name)).map(([, src]) => src).join('');
383
+ for (const [name] of RUNTIME)
384
+ if (!used.has(name) && text.includes(`${name}(`)) {
385
+ used.add(name);
386
+ grew = true;
387
+ }
388
+ }
389
+ return RUNTIME.filter(([name]) => used.has(name)).map(([, src]) => `${src}\n`).join('');
390
+ }
367
391
  export function compileJs(m) {
368
392
  const { h, plain, queries, carried } = runnable(m);
369
393
  const id = (name) => JSON.stringify(name);
@@ -451,16 +475,8 @@ export function compileJs(m) {
451
475
  'if (head > 1024 && 2 * head > records.length) { records.splice(0, head); head = 0; }', `return {scalars, maps: state.maps, history: records, head${h.prune ? ', last: order' : ''}};`);
452
476
  else
453
477
  lines.push('return Object.keys(dft).length ? {scalars, maps: state.maps, defaults: dft} : {scalars, maps: state.maps};');
454
- return `// Generated from the machine ${m.name}. step consumes its state: keep the one it returns (fork to branch).
455
- function $n(v, what) { if (typeof v !== 'number' || !Number.isSafeInteger(v) || v < 0) throw new RangeError(what + ' must be a natural below 2^53, not ' + v); return v; }
456
- function $x(v) { if (v > 9007199254740991) throw new RangeError('a value passed 2^53 - 1, past which the proofs over the naturals no longer describe the run'); return v; }
457
- function $m(v, what) { if (!(v instanceof Map)) throw new RangeError(what + ' must be a Map of naturals'); for (const [key, x] of v) { $n(key, 'a key of ' + what); $n(x, 'a cell of ' + what); } return v; }
458
- function $keys(maps, names) { const keys = new Set(); let past = 0; for (const c of Object.values(maps)) for (const key of c.keys()) past = Math.max(past, key + 1); for (const n of names) for (const key of maps[n].keys()) keys.add(key); return [...keys, past]; }
459
- ${fns.map((name, i) => `const fn${i} = (${m.fns[name].js});\n${m.fns[name].call ? `const fnc${i} = (${m.fns[name].call});\n` : ''}`).join('')}function $breaks(maps, reads, cmps) { const p = new Set([0]); for (const [n, d, minus] of reads) { for (const s of maps[n].keys()) { if (minus) p.add(s + d); else if (s >= d) p.add(s - d); } if (minus) p.add(d); } for (const [v, d, minus] of cmps) { if (minus) { p.add(v + d); p.add(d); } else if (v >= d) p.add(v - d); } const keys = new Set(); for (const q of p) for (let i = -1; i <= 2; i++) if (q + i >= 0) keys.add(q + i); const sorted = [...keys].sort((a, b) => a - b); sorted.push(sorted[sorted.length - 1] + 1); return sorted; }
460
- function $spread(keys, n) { const out = new Set(); for (const k of keys) for (let i = 0; i < n; i++) out.add(k + i); return [...out].sort((a, b) => a - b); }
461
- function $dense(keys, f) { const top = keys[keys.length - 1]; if (top > 1000000) throw new RangeError('a write at every key reads its key in arithmetic past a million keys (' + top + ')'); const d = f(top), cells = new Map(); for (let k = 0; k < top; k++) { const v = f(k); if (v !== d) cells.set(k, v); } return {cells, d}; }
462
- function $every(keys, f) { const past = keys[keys.length - 1], d = f(past), cells = new Map(); for (let i = 0; i < keys.length - 1; i++) { const v = f(keys[i]); if (v !== d) cells.set(keys[i], v); } return {cells, d}; }
463
- export function init(params) {
478
+ const fnDefs = fns.map((name, i) => `const fn${i} = (${m.fns[name].js});\n${m.fns[name].call ? `const fnc${i} = (${m.fns[name].call});\n` : ''}`).join('');
479
+ const code = `export function init(params) {
464
480
  const env = ${env(false)}, s = {};
465
481
  return {scalars: {${Object.entries(plain.state).map(([name, e]) => `${id(name)}: ${ex(e)}`).join(', ')}}, maps: {${Object.keys(plain.maps ?? {}).map(name => `${id(name)}: new Map()`).join(', ')}}${h ? ', history: [], head: 0' : ''}};
466
482
  }
@@ -471,6 +487,8 @@ export function step(params, state, input) {
471
487
  ${lines.join('\n ')}
472
488
  }
473
489
  `;
490
+ return `// Generated from the machine ${m.name}. step consumes its state: keep the one it returns (fork to branch).
491
+ ${runtimeFor(fnDefs + code)}${fnDefs}${code}`;
474
492
  }
475
493
  // Generated code as a function. Every caller passes source this library generated from its own terms, or a function's
476
494
  // `js` the rule's author wrote (code, as the rule itself is), never data; the lint's implied-eval rule is waived here
@@ -1163,12 +1163,14 @@ export function stepLawObligations(machine, laws, instances, sl) {
1163
1163
  // `perStep`: a step law's own instances, where it has them (else `instances`).
1164
1164
  // Each frame's hypotheses read no name a later step's request is read by: the condition the Lean twin's proof needs
1165
1165
  // (Sets.lean's checkSKStep, `avoids (szone s sl)`; plain flags are inlined here, so their names never occur), checked
1166
- // here alike.
1166
+ // here alike. A quantified flag's keys are free of it: where the flag is 1 its condition holds at every key, so at a
1167
+ // key a later step names too (and the flag, its witness and its condition read none).
1168
+ const lawsOnly = (inst) => Object.fromEntries(Object.entries(inst).filter(([n]) => !n.startsWith('?')));
1167
1169
  export function checkStepLaws(m, laws, stepLaws, proof) {
1168
1170
  return checkLaws(m, laws, proof.laws) && stepLaws.every(sl => {
1169
1171
  const inst = proof.perStep?.[sl.name] ?? proof.instances, plain = m.history ? historyMachine(m).machine : m;
1170
1172
  const zone = stepZone(plain, sl.steps ?? 1);
1171
- return [...assumed(laws, inst), ...assumed(plain.facts ?? [], inst), ...closing(plain, inst)].every(c => !mentions(c, zone))
1173
+ return [...assumed(laws, inst), ...assumed(plain.facts ?? [], inst), ...closing(plain, lawsOnly(inst))].every(c => !mentions(c, zone))
1172
1174
  && stepLawObligations(m, laws, inst, sl).every(g => proof.trees[g.name] !== undefined && check(g, proof.trees[g.name]));
1173
1175
  });
1174
1176
  }
@@ -665,6 +665,17 @@ const flagInstances = (m, keys, focus = []) => Object.fromEntries(Object.entries
665
665
  const n = boundOf(c).length;
666
666
  return [[`?${f}`, n === 1 ? keys : focus.length ? related(keys, focus, n).flat() : tuples(keys, n)]];
667
667
  }));
668
+ // A k-step law whose steps keep a condition (a flag that holds at one step and is read again at the next) fails at a
669
+ // later step's witness only where the condition held before: so, per quantified flag over one key, the instances at the
670
+ // law's own keys and that flag's witness at every step (a later step's witness is a key an earlier frame may name, as a
671
+ // flag holds at every key: kernel.ts's checkStepLaws). Few instances, where the shared ones are many and mostly idle.
672
+ const keptAcross = (m, laws, inst, own, k) => k < 2 ? [] : Object.entries(m.step.flags ?? {}).flatMap(([f, c]) => {
673
+ if (c.op !== 'all' || c.keys?.length)
674
+ return [];
675
+ const ws = Array.from({ length: k }, (_, i) => ({ op: 'var', name: i ? `w?${f}@${i + 1}` : `w?${f}` })), keys = [...own, ...ws.slice(0, 1)];
676
+ const keep = new Set(keys.map(e => JSON.stringify(e)));
677
+ return [Object.fromEntries(Object.entries(inst).map(([n, ks]) => [n, arity(m, laws, n) === 1 ? [...ks.filter(e => keep.has(JSON.stringify(e))), ...(n === `?${f}` ? ws.slice(1) : [])] : ks]))];
678
+ });
668
679
  const pairFlags = (m) => Object.values(m.step.flags ?? {}).some(c => c.op === 'all' && !!c.keys?.length);
669
680
  // How many keys each instance of a name takes: a law's, or a quantified flag's (?f).
670
681
  const arity = (m, laws, n) => {
@@ -677,7 +688,8 @@ const arity = (m, laws, n) => {
677
688
  // several keys), the every-key laws and flags at every key they read a map at (a law about several keys at tuples
678
689
  // relating `focus`, the goals' own keys).
679
690
  function instancesFor(m, laws, build, opts = {}) {
680
- let inst = opts.start ?? (opts.seed ? { ...Object.fromEntries(laws.filter(l => l.forall).map(l => [l.name, l.keys?.length ? [] : opts.seed])), ...flagInstances(m, opts.seed, opts.focus) } : {});
691
+ const seed = opts.seed && [...new Map(opts.seed.map(e => [JSON.stringify(e), e])).values()];
692
+ let inst = opts.start ?? (seed ? { ...Object.fromEntries(laws.filter(l => l.forall).map(l => [l.name, l.keys?.length ? [] : seed])), ...flagInstances(m, seed, opts.focus) } : {});
681
693
  const reads = opts.reads ?? (!opts.seed || shifts(m) || laws.some(l => l.keys?.length) || pairFlags(m));
682
694
  for (let round = 0; round < 2; round++) {
683
695
  if (m.facts?.length)
@@ -703,7 +715,7 @@ const union = (a, b, m) => {
703
715
  for (const [n, keys] of Object.entries(b)) {
704
716
  const k = arity(m, [], n), groups = (ks) => Array.from({ length: Math.floor(ks.length / k) }, (_, t) => ks.slice(t * k, (t + 1) * k));
705
717
  const seen = new Set(groups(out[n] ?? []).map(g => JSON.stringify(g)));
706
- out[n] = [...(out[n] ?? []), ...groups(keys).filter(g => !seen.has(JSON.stringify(g))).flat()];
718
+ out[n] = [...(out[n] ?? []), ...groups(keys).filter(g => !seen.has(JSON.stringify(g)) && !!seen.add(JSON.stringify(g))).flat()];
707
719
  }
708
720
  return out;
709
721
  };
@@ -814,7 +826,8 @@ function proveStepLawsAsGiven(m, laws, stepLaws, base) {
814
826
  // Each step law its own instances, from its own goal: the keys the step and the laws read, and its own (a k-step
815
827
  // law's later frames read keys no other step law has); where positions shift or a law is about several keys, the
816
828
  // keys its goal reads a map at too. A frame's hypotheses read no name the run introduces later (kernel.ts's
817
- // stepZone), so a k-step law drops the keys that read one.
829
+ // stepZone), so a k-step law drops the keys that read one; but a flag that holds at every key holds at a later step's
830
+ // key too: where these instances don't prove it, each flag at its own witness at every step is tried (keptAcross).
818
831
  const shared = [...readKeys(plain, laws), ...witnesses(plain)], named = [...laws, ...(plain.facts ?? [])], reads = shifts(plain) || laws.some(l => l.keys?.length) || pairFlags(plain);
819
832
  const own = (sl) => [...[sl.then, ...(sl.when === undefined ? [] : Array.isArray(sl.when) ? sl.when : [sl.when])].flatMap(readKeysC),
820
833
  ...[sl.forall, ...(sl.keys ?? [])].flatMap(key => (key ? [{ op: 'var', name: key }] : []))];
@@ -828,7 +841,17 @@ function proveStepLawsAsGiven(m, laws, stepLaws, base) {
828
841
  let nodes = base.nodes, leaves = base.leaves;
829
842
  for (const sl of stepLaws)
830
843
  for (const goal of stepLawObligations(m, laws, perStep[sl.name] ?? instances, sl)) {
831
- const out = grow(goal);
844
+ let out = grow(goal);
845
+ // A k-step law that reads a condition kept across its steps: each flag at its own witnesses, at every step.
846
+ if (!('tree' in out))
847
+ for (const kept of keptAcross(plain, laws, perStep[sl.name] ?? instances, own(sl), sl.steps ?? 1)) {
848
+ const again = grow(stepLawObligations(m, laws, kept, sl)[0]);
849
+ if ('tree' in again) {
850
+ out = again;
851
+ perStep[sl.name] = kept;
852
+ break;
853
+ }
854
+ }
832
855
  if (!('tree' in out))
833
856
  return { proven: false, nodes, leaves, failed: { goal: goal.name, stuck: out.stuck, ...(out.witness ? { witness: out.witness } : {}), ...(out.why ? { why: out.why } : {}) }, checked: false };
834
857
  trees[goal.name] = out.tree;
@@ -145,6 +145,7 @@ export type LeanItem = {
145
145
  };
146
146
  export declare function toLean(items: LeanItem[], opts?: {
147
147
  title?: string;
148
+ native?: boolean;
148
149
  }): string;
149
150
  export declare function leanProject(file: string, opts?: {
150
151
  name?: string;
@@ -214,11 +214,11 @@ def ${name}_tree : Tree :=
214
214
  return tamper
215
215
  ? `-- TAMPERED: ${ex.name}${dropStep ? ", the first leaf's derivation without its last step" : ''}. Lean computes the check to false.
216
216
  ${body}
217
- example : checkGoal ${name}_goal ${name}_tree = false := by decide +kernel`
217
+ example : checkGoal ${name}_goal ${name}_tree = false := by ${checkBy}`
218
218
  : `-- ${ex.name} (${cols.length} columns)
219
219
  ${body}
220
220
  theorem ${name} : ∀ env maps, ¬ ∀ c ∈ ${name}_goal.hyps, evalC env maps c = true :=
221
- sound ${name}_goal ${name}_tree (by decide +kernel)`;
221
+ sound ${name}_goal ${name}_tree (by ${checkBy})`;
222
222
  }
223
223
  // Whole machines through Kern.machine_sound: Lean builds the goals from the machine; the trees are the kernel's.
224
224
  export function machineToLean(id, m, laws, corollary) {
@@ -255,15 +255,17 @@ export function machineToLean(id, m, laws, corollary) {
255
255
  def ${id}_machine : FMachine := ${lit(m)}
256
256
  def ${id}_proof : MachineProof := ${list(proof)}
257
257
  theorem ${id}_laws : ∀ maps ρ, FReach ${id}_machine maps ρ → ∀ l ∈ ${id}_proof.map (·.1), evalC ρ maps l = true :=
258
- fmachine_sound ${id}_machine ${id}_proof (by decide +kernel)${corollary ? `\n${corollary}` : ''}`;
258
+ fmachine_sound ${id}_machine ${id}_proof (by ${checkBy})${corollary ? `\n${corollary}` : ''}`;
259
259
  }
260
260
  return `-- ${m.name}: ${laws.map(l => l.name).join('; ')} (variables ${vars.map((v2, j) => `${j} ${v2}`).join(', ')})
261
261
  def ${id}_machine : Machine := ${machine}
262
262
  def ${id}_proof : MachineProof := ${list(proof)}
263
263
  theorem ${id}_laws : ∀ maps ρ, Reach ${id}_machine maps ρ → ∀ l ∈ ${id}_proof.map (·.1), evalC ρ maps l = true :=
264
- machine_sound ${id}_machine ${id}_proof (by decide +kernel)${corollary ? `\n${corollary}` : ''}`;
264
+ machine_sound ${id}_machine ${id}_proof (by ${checkBy})${corollary ? `\n${corollary}` : ''}`;
265
265
  }
266
266
  export const TRUE = '.le (.const 0) (.const 0)';
267
+ // The tactic each check is closed by: `decide +kernel`, or `native_decide` for toLean's `native`.
268
+ let checkBy = 'decide +kernel';
267
269
  export const grown = (g) => { const o = grow(g); if (!('tree' in o))
268
270
  throw new Error(`${g.name}: stuck`); return o.tree; };
269
271
  // Loops through Kern.loop_halts_of: the loop's machine (Live.lean's Loop.machine) with its invariants as laws, and its
@@ -295,7 +297,7 @@ def ${id}_fold : Fold := ${fold}
295
297
  def ${id}_proof : FoldProof := ${list(proof)}
296
298
  theorem ${id}_laws : ∀ maps ρ, evalC ρ maps ${id}_fold.pre = true → ∀ t, Allowed ${id}_fold ρ maps t →
297
299
  ∀ l ∈ ${id}_proof.map (·.1), evalC (foldT ${id}_fold ρ maps t) maps l = true :=
298
- fold_sound ${id}_fold ${id}_proof (by decide +kernel)${extra}`;
300
+ fold_sound ${id}_fold ${id}_proof (by ${checkBy})${extra}`;
299
301
  }
300
302
  // Machines with maps (and flags) through Kern.fmmachine_sound: Lean inlines the flags, then builds the start and step
301
303
  // goals (read-over-write, ∀-laws at the proof's keys and at a skolem); the trees and the instance keys are the kernel's.
@@ -433,10 +435,10 @@ export function mmachineToLean(id, m, laws, opts = {}) {
433
435
  return `-- Progress: ${pc.name}
434
436
  def ${pc.id} : Progress := ${record(pc.when)}
435
437
  theorem ${pc.id}_holds : ProgressHolds ${id}_machine ${pc.id} :=
436
- progress_sound ${id}_machine ${id}_proof ${pc.id} ${id}_check (by decide +kernel)${pc.tamper ? `
438
+ progress_sound ${id}_machine ${id}_proof ${pc.id} ${id}_check (by ${checkBy})${pc.tamper ? `
437
439
  -- TAMPERED: ${pc.tamper.what}: the proof above offered for it. Lean computes the check to false.
438
440
  def ${pc.id}_tampered : Progress := ${record(pc.tamper.when)}
439
- example : checkProgress ${id}_machine ${id}_proof ${pc.id}_tampered = false := by decide +kernel` : ''}`;
441
+ example : checkProgress ${id}_machine ${id}_proof ${pc.id}_tampered = false := by ${checkBy}` : ''}`;
440
442
  });
441
443
  // Step laws through Kern.steplaw_sound: `when` and `then` sent with the flags inlined (as kernel.ts's goal has them);
442
444
  // a law over several steps goes through smachineToLean.
@@ -461,20 +463,20 @@ def ${id}_step${j} : StepLaw := ⟨${when ? any0.c(inlineFlags(when)) : TRUE},
461
463
  ${list(insts)},
462
464
  (${pr.tab}, ${pr.tree})⟩
463
465
  theorem ${id}_step${j}_holds : StepLawHolds ${id}_machine ${id}_step${j} :=
464
- steplaw_sound ${id}_machine ${id}_proof ${id}_step${j} ${id}_check (by decide +kernel)`;
466
+ steplaw_sound ${id}_machine ${id}_proof ${id}_step${j} ${id}_check (by ${checkBy})`;
465
467
  });
466
468
  const machineDef = `def ${id}_machine : FMMachine := ${lit(m)}`;
467
469
  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(', ')})
468
470
  ${machineDef}
469
471
  ${entries.join('\n')}
470
472
  def ${id}_proof : List MEntry := ${list(laws.map((_, j) => `${id}_e${j}`))}
471
- theorem ${id}_check : checkFMMachine ${id}_machine ${id}_proof = true := by decide +kernel
473
+ theorem ${id}_check : checkFMMachine ${id}_machine ${id}_proof = true := by ${checkBy}
472
474
  theorem ${id}_laws : ∀ ρ M, FMReach ${id}_machine ρ M → ∀ e ∈ ${id}_proof, e.law.Holds ρ M :=
473
475
  fmmachine_sound ${id}_machine ${id}_proof ${id}_check
474
476
  ${readable(id, laws, vars, mapNames, { binders: '(ρ : Nat → Nat) (μ : Nat → Nat → Nat)', hyp: `FMReach ${id}_machine ρ μ`, call: `${id}_laws ρ μ h` })}${opts.corollary ? `\n${opts.corollary}` : ''}${opts.tamper ? `
475
477
  -- TAMPERED: ${opts.tamper.what}. Lean computes the check to false.
476
478
  def ${id}_tampered : FMMachine := ${lit(opts.tamper.machine)}
477
- example : checkFMMachine ${id}_tampered ${id}_proof = false := by decide +kernel` : ''}${progress.length ? `\n\n${progress.join('\n\n')}` : ''}${stepRecords.length ? `\n\n${stepRecords.join('\n\n')}` : ''}`;
479
+ example : checkFMMachine ${id}_tampered ${id}_proof = false := by ${checkBy}` : ''}${progress.length ? `\n\n${progress.join('\n\n')}` : ''}${stepRecords.length ? `\n\n${stepRecords.join('\n\n')}` : ''}`;
478
480
  }
479
481
  // Machines with sets through Kern.smachine_sound and Kern.skstep_sound (Sets.lean): maps a request carries, flags that
480
482
  // hold at every key (each its 0/1 variable ?f, closed at the proof's keys and at a witness w?f) and writes at every
@@ -523,7 +525,7 @@ def ${id}_step${j} : SKStep := ⟨⟨${list(Array.from({ length: kk }, (_, s2) =
523
525
  ${pr.hyps},
524
526
  ${list(Array.from({ length: kk - 1 }, (_, s2) => list(Object.keys(m.state).map(n2 => `(${ix(n2)}, ${ix(stateName(n2, s2 + 1))})`))))}⟩
525
527
  theorem ${id}_step${j}_holds : SKStepHolds ${id}_machine ${id}_step${j} :=
526
- skstep_sound ${id}_machine ${id}_proof ${id}_step${j} ${id}_check (by decide +kernel)`;
528
+ skstep_sound ${id}_machine ${id}_proof ${id}_step${j} ${id}_check (by ${checkBy})`;
527
529
  } }));
528
530
  // A refinement (Refine.lean's SRefined): its step law, that the spec stutters or allows the request it is sent; srefines
529
531
  // says what the whole means.
@@ -531,11 +533,11 @@ theorem ${id}_step${j}_holds : SKStepHolds ${id}_machine ${id}_step${j} :=
531
533
  claims.push({ steps: [stepLaws[0]], allows: [], render: h => {
532
534
  const primesL = list(Object.keys(m.state).map(s2 => `(${ix(s2)}, ${ix(`${s2}'`)})`)), primeMapsL = list(Object.keys(m.maps ?? {}).map(mp => `(${mx(mp)}, ${mx(`${mp}'`)})`));
533
535
  const proofL = h.sproof(stepLaws[0]);
534
- return `theorem ${id}_allowed : checkSKStep ${id}_machine ${id}_proof (srStep ${id}_refined ${primesL} ${primeMapsL} ${proofL}) = true := by decide +kernel
536
+ return `theorem ${id}_allowed : checkSKStep ${id}_machine ${id}_proof (srStep ${id}_refined ${primesL} ${primeMapsL} ${proofL}) = true := by ${checkBy}
535
537
  theorem ${id}_refines : ∀ ρ M, SReach ${id}_refined.impl ρ M → ∃ ρc Mc σ Ms, SReach ${id}_refined.full ρc Mc ∧ Agree ${id}_refined.SV ρc ρ
536
538
  ∧ (∀ mp, mp ∉ ${id}_refined.SM → Mc mp = M mp) ∧ SReach ${id}_refined.spec σ Ms ∧ Agree ${id}_refined.X σ ρc
537
539
  ∧ (∀ mp, mp ∉ ${id}_refined.IM → mp ∉ ${id}_refined.spec.carried → Ms mp = Mc mp) ∧ ∀ e ∈ ${id}_proof, e.Holds ρc Mc :=
538
- srefines ${id}_refined (by decide +kernel) (by decide +kernel) ${id}_proof ${id}_check ${rf.specPre} ⟨rfl, rfl⟩ ${primesL} ${primeMapsL} ${proofL} ${id}_allowed (by decide +kernel) (by decide +kernel) (by decide +kernel)`;
540
+ srefines ${id}_refined (by ${checkBy}) (by ${checkBy}) ${id}_proof ${id}_check ${rf.specPre} ⟨rfl, rfl⟩ ${primesL} ${primeMapsL} ${proofL} ${id}_allowed (by ${checkBy}) (by ${checkBy}) (by ${checkBy})`;
539
541
  } });
540
542
  // Liveness (Live.lean's sleads_sound): its step laws (stay, cover, one per helpful request), and where each helpful
541
543
  // request is proven allowed in its region, the same under weak fairness (sleads_wf_sound).
@@ -546,7 +548,7 @@ theorem ${id}_refines : ∀ ρ M, SReach ${id}_refined.impl ρ M → ∃ ρc Mc
546
548
  const al = allows.map(h.allow);
547
549
  const wf = al.every(a => a !== undefined) ? `
548
550
  def ${lid}_ap : List (List (Nat × E) × SProof × List E) := ${list(al)}
549
- theorem ${lid}_allows : sliveAllowsOk ${id}_machine ${id}_proof ${lid} ${lid}_ap = true := by decide +kernel
551
+ theorem ${lid}_allows : sliveAllowsOk ${id}_machine ${id}_proof ${lid} ${lid}_ap = true := by ${checkBy}
550
552
  /-- ${l.name}, under weak fairness: along every run that takes each helpful request whenever, from some point on, it
551
553
  stays allowed, \`from\` is followed by \`to\`. -/
552
554
  theorem ${lid}_holds_wf (run : SRun ${id}_machine)
@@ -560,10 +562,10 @@ def ${lid} : Live := ${liveLit(l, { e, c }, ix, list(Object.keys(m.state).map(s2
560
562
  def ${lid}_sp : SProof := ${h.sproof(stay)}
561
563
  def ${lid}_cp : SProof := ${h.sproof(cover)}
562
564
  def ${lid}_hp : List SProof := ${list(helps.map(h.sproof))}
563
- theorem ${lid}_ok : sliveOk ${id}_machine ${lid} = true := by decide +kernel
564
- theorem ${lid}_stay : checkSKStep ${id}_machine ${id}_proof (sliveStep ${lid} ${lid}.pend (liveStayC ${lid}) ${lid}_sp) = true := by decide +kernel
565
- theorem ${lid}_cover : checkSKStep ${id}_machine ${id}_proof (sliveStep ${lid} ${lid}.pend (liveCoverC ${lid}) ${lid}_cp) = true := by decide +kernel
566
- theorem ${lid}_helps : sliveHelpsOk ${id}_machine ${id}_proof ${lid} ${lid}_hp = true := by decide +kernel
565
+ theorem ${lid}_ok : sliveOk ${id}_machine ${lid} = true := by ${checkBy}
566
+ theorem ${lid}_stay : checkSKStep ${id}_machine ${id}_proof (sliveStep ${lid} ${lid}.pend (liveStayC ${lid}) ${lid}_sp) = true := by ${checkBy}
567
+ theorem ${lid}_cover : checkSKStep ${id}_machine ${id}_proof (sliveStep ${lid} ${lid}.pend (liveCoverC ${lid}) ${lid}_cp) = true := by ${checkBy}
568
+ theorem ${lid}_helps : sliveHelpsOk ${id}_machine ${id}_proof ${lid} ${lid}_hp = true := by ${checkBy}
567
569
  /-- ${l.name}: along every run that takes each helpful request infinitely often, \`from\` is followed by \`to\`. -/
568
570
  theorem ${lid}_holds (run : SRun ${id}_machine)
569
571
  (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) :
@@ -577,13 +579,13 @@ theorem ${lid}_holds (run : SRun ${id}_machine)
577
579
  claims.push({ steps: [], allows: [], render: () => `/-- ${lp0.loop.name} halts: for params meeting its precondition, no run of turns goes on forever. -/
578
580
  theorem ${id}_halts : ∀ maps ρ0, evalC ρ0 maps ${id}_loop.pre = true →
579
581
  ¬ ∃ f : Nat → Nat → Nat, f 0 = applyS ${id}_loop.init ρ0 maps ∧ ∀ n, Turn ${id}_loop maps (f n) (f (n + 1)) :=
580
- 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 ? `
582
+ loop_halts_of ${id}_loop ${id}_live0 rfl rfl (by intro h hh; simp only [${id}_live0, List.mem_singleton] at hh; subst hh; rfl) (by ${checkBy}) ${id}_live0_holds${lp0.tamper ? `
581
583
  -- TAMPERED: ${lp0.tamper.what}. Lean computes the checks to false.
582
584
  def ${id}_tampered : SMachine := Loop.machine ${loopLit(lp0.tamper.loop)}
583
585
  example : (checkSMachine ${id}_tampered ${id}_proof && sliveOk ${id}_tampered ${id}_live0
584
586
  && checkSKStep ${id}_tampered ${id}_proof (sliveStep ${id}_live0 ${id}_live0.pend (liveStayC ${id}_live0) ${id}_live0_sp)
585
587
  && checkSKStep ${id}_tampered ${id}_proof (sliveStep ${id}_live0 ${id}_live0.pend (liveCoverC ${id}_live0) ${id}_live0_cp)
586
- && sliveHelpsOk ${id}_tampered ${id}_proof ${id}_live0 ${id}_live0_hp) = false := by decide +kernel` : ''}` });
588
+ && sliveHelpsOk ${id}_tampered ${id}_proof ${id}_live0 ${id}_live0_hp) = false := by ${checkBy}` : ''}` });
587
589
  // Ghosts (Ghost.lean's sghost_laws): the laws that read no ghost hold of the machine without its ghosts.
588
590
  if (gv.length)
589
591
  claims.push({ steps: [], allows: [], render: () => `-- The laws that read no ghost hold of the machine without its ghosts.
@@ -724,9 +726,9 @@ theorem ${id}_orig_laws : ∀ ρ M, SReach ${id}_ghosted.orig ρ M → ∀ e ∈
724
726
  throw new Error(`${id}: ${g.name} not proven`); const x2 = enc(g, out.tree); return `(${x2.tab}, ${x2.tree})`; });
725
727
  const tampered = (opts.historyTampers ?? []).map((t, j) => `-- TAMPERED: ${t.what}. Lean computes the check to false.
726
728
  def ${id}_tampered${j} : HMachine := ${hlit(t.relations)}
727
- example : checkHMachine ${id}_tampered${j} ${id}_proof ${t.withRecordTrees ? `${id}_records` : '[]'} = false := by decide +kernel`);
729
+ example : checkHMachine ${id}_tampered${j} ${id}_proof ${t.withRecordTrees ? `${id}_records` : '[]'} = false := by ${checkBy}`);
728
730
  return `def ${id}_records : List (List VDef × Tree) := ${list(records)}
729
- theorem ${id}_hcheck : checkHMachine ${id}_hist ${id}_proof ${id}_records = true := by decide +kernel
731
+ theorem ${id}_hcheck : checkHMachine ${id}_hist ${id}_proof ${id}_records = true := by ${checkBy}
730
732
  theorem ${id}_hlaws : ∀ ρ M hist, HReach ${id}_hist ρ M hist → ∀ e ∈ ${id}_proof, e.Holds ρ M :=
731
733
  hmachine_sound ${id}_hist ${id}_proof ${id}_records ${id}_hcheck
732
734
  ${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')}` : ''}`;
@@ -784,18 +786,18 @@ ${rf ? `def ${id}_refined : SRefined := ⟨${lit(rf.impl)},
784
786
  def ${id}_machine : SMachine := ${id}_refined.full` : gv.length ? `def ${id}_ghosted : SGhosted := ⟨${lit(orig)},
785
787
  ${list(gv.map(v2 => `(${ix(v2)}, ${e(m.state[v2])}, ${e(m.step.state?.[v2] ?? { op: 'var', name: v2 })})`))}⟩
786
788
  def ${id}_machine : SMachine := ${id}_ghosted.full
787
- theorem ${id}_ghostOk : sghostOk ${id}_ghosted = true := by decide +kernel` : hm ? `def ${id}_hist : HMachine := ${hlit(hm.history.relations ?? [])}
789
+ theorem ${id}_ghostOk : sghostOk ${id}_ghosted = true := by ${checkBy}` : hm ? `def ${id}_hist : HMachine := ${hlit(hm.history.relations ?? [])}
788
790
  def ${id}_machine : SMachine := ${id}_hist.plain` : lp0 ? `def ${id}_loop : Loop := ${loopLit(lp0.loop)}
789
791
  def ${id}_machine : SMachine := ${id}_loop.machine` : `def ${id}_machine : SMachine := ${lit(m)}`}
790
792
  ${entries.join('\n')}
791
793
  def ${id}_proof : List SEntry := ${list(laws.map((_, j) => `${id}_e${j}`))}
792
- theorem ${id}_check : checkSMachine ${id}_machine ${id}_proof = true := by decide +kernel
794
+ theorem ${id}_check : checkSMachine ${id}_machine ${id}_proof = true := by ${checkBy}
793
795
  theorem ${id}_laws : ∀ ρ M, SReach ${id}_machine ρ M → ∀ e ∈ ${id}_proof, e.Holds ρ M :=
794
796
  smachine_sound ${id}_machine ${id}_proof ${id}_check
795
797
  ${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 ? `
796
798
  -- TAMPERED: ${opts.tamper.what}. Lean computes the check to false.
797
799
  def ${id}_tampered : SMachine := ${lit(opts.tamper.machine)}
798
- 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)}` : ''}`;
800
+ example : checkSMachine ${id}_tampered ${id}_proof = false := by ${checkBy}` : ''}${records.length ? `\n\n${records.join('\n\n')}` : ''}${opts.after ? `\n\n${opts.after(ix)}` : ''}`;
799
801
  }
800
802
  // A function defined by a recurrence (core/dsl.ts recur): its Lean definition by the same recurrence, and its lemmas
801
803
  // proven together by induction (each case by omega, its min, max and ifs split first), and its laws between two points
@@ -1030,7 +1032,23 @@ export function hmachineToLean(id, m, laws, tampers = [], corollary) {
1030
1032
  // over named variables); step laws with `<id>_stepN_holds` (on the plain machine, `<id>s`, for a machine with a history);
1031
1033
  // loops with `<id>_halts`; decisions with `<id>`. A machine with sets has its step laws as `<id>_stepN_holds`.
1032
1034
  // Throws if the kernel can't prove one.
1035
+ //
1036
+ // `native`: each check by `native_decide` (compiled Lean, several times faster on a large certificate), not by
1037
+ // `decide +kernel` (Lean's kernel alone). Every such theorem then also rests on an axiom of its own that `#print axioms`
1038
+ // names (`…_native.native_decide.ax_…`): it trusts Lean's compiler, its runtime and the C compiler. Defaults to off, or
1039
+ // to GDP_LEAN_NATIVE=1 (the suite's quick runs); a release's proofs stay on Lean's kernel and its core axioms.
1033
1040
  export function toLean(items, opts = {}) {
1041
+ const outer = checkBy;
1042
+ const env = globalThis.process?.env;
1043
+ checkBy = (opts.native ?? env?.GDP_LEAN_NATIVE === '1') ? 'native_decide' : 'decide +kernel';
1044
+ try {
1045
+ return toLeanWith(items, opts);
1046
+ }
1047
+ finally {
1048
+ checkBy = outer;
1049
+ }
1050
+ }
1051
+ function toLeanWith(items, opts) {
1034
1052
  const ident = (id) => { const s = id.replace(/[^A-Za-z0-9_]/g, '_'); return /^[A-Za-z]/.test(s) ? s : `x_${s}`; };
1035
1053
  const parts = [], theorems = [];
1036
1054
  let goals = 0;