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 +12 -6
- package/dist/core/dsl.d.ts +4 -0
- package/dist/core/dsl.js +55 -11
- package/dist/core/machine.js +28 -10
- package/dist/kernel/kernel.js +4 -2
- package/dist/kernel/prove.js +27 -4
- package/dist/lean-kernel/emit.d.ts +1 -0
- package/dist/lean-kernel/emit.js +43 -25
- package/dist/lean-kernel/library.js +1 -1
- package/package.json +1 -1
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
|
|
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
|
|
234
|
-
|
|
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
|
|
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 (
|
|
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
|
package/dist/core/dsl.d.ts
CHANGED
|
@@ -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
|
|
914
|
-
|
|
915
|
-
|
|
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
|
|
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
|
-
|
|
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]; });
|
package/dist/core/machine.js
CHANGED
|
@@ -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
|
-
|
|
455
|
-
|
|
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
|
package/dist/kernel/kernel.js
CHANGED
|
@@ -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
|
}
|
package/dist/kernel/prove.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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;
|
package/dist/lean-kernel/emit.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
564
|
-
theorem ${lid}_stay : checkSKStep ${id}_machine ${id}_proof (sliveStep ${lid} ${lid}.pend (liveStayC ${lid}) ${lid}_sp) = true := by
|
|
565
|
-
theorem ${lid}_cover : checkSKStep ${id}_machine ${id}_proof (sliveStep ${lid} ${lid}.pend (liveCoverC ${lid}) ${lid}_cp) = true := by
|
|
566
|
-
theorem ${lid}_helps : sliveHelpsOk ${id}_machine ${id}_proof ${lid} ${lid}_hp = true := by
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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;
|