gdp-wired 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -6
- package/dist/core/dsl.d.ts +1 -0
- package/dist/core/dsl.js +7 -2
- package/dist/core/gdp.d.ts +35 -0
- package/dist/core/gdp.js +66 -0
- package/dist/kernel/kernel.d.ts +2 -0
- package/dist/kernel/kernel.js +19 -17
- package/dist/lean-kernel/emit.d.ts +17 -2
- package/dist/lean-kernel/emit.js +78 -123
- package/dist/lean-kernel/library.js +3 -3
- package/package.json +18 -4
package/README.md
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
<
|
|
1
|
+
<picture>
|
|
2
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/monotykamary/gdp-wired/main/static/banner-dark.svg">
|
|
3
|
+
<img src="https://raw.githubusercontent.com/monotykamary/gdp-wired/main/static/banner-light.svg" alt="gdp-wired: an automatic prover for stateful rules" width="100%">
|
|
4
|
+
</picture>
|
|
2
5
|
|
|
3
6
|
# gdp-wired
|
|
4
7
|
|
|
@@ -27,8 +30,10 @@ proveRule(budget, [bounded]) // {proven: true, checked: true}
|
|
|
27
30
|
Names are typed: `({s, bb}) => …` or a law that reads the request `c` is a TypeScript error, not a runtime surprise.
|
|
28
31
|
Forget the cap (`s.plus(c)`) and the kernel refuses the proof; a search from the start turns it into a real run:
|
|
29
32
|
`{params: {b: 1, m: 0}, trace: [{c: 2}]}`. `bun examples/budget.ts` runs all of it, including the generated JavaScript,
|
|
30
|
-
and `examples/domains/` holds
|
|
31
|
-
protocols, commerce, supply chains, games, education
|
|
33
|
+
and `examples/domains/` holds 187 rules from 21 domains (finance, access control, public services, operations,
|
|
34
|
+
protocols, commerce, supply chains, games, education, healthcare, energy, networking, AI agents, cloud, ad tech,
|
|
35
|
+
governance, insurance, bookings, safety and IoT, mobility, media), each checked three ways: the kernel proves every law
|
|
36
|
+
and refutes every broken variant, random runs of both runtimes agree, and Lean checks the trees.
|
|
32
37
|
|
|
33
38
|
## Where it fits
|
|
34
39
|
|
|
@@ -101,6 +106,39 @@ The untrusted parts are held to the trusted ones by the test suite:
|
|
|
101
106
|
tests, and the few that survive are equivalent (`kernel/MUTANTS.md`).
|
|
102
107
|
- **Fuzz**: the kernel never accepts a refutation of something that has a model.
|
|
103
108
|
|
|
109
|
+
## With gdp-ts
|
|
110
|
+
|
|
111
|
+
[gdp-ts](https://github.com/rauchg/gdp-ts) makes every call site present a proof that a check ran on exactly these
|
|
112
|
+
values, and trusts the module that performs the check. `gdp-wired/gdp` proves that check: the trusted module hands its
|
|
113
|
+
private prover to a `guard` (a function's condition) or a `gate` (a rule's flag), which proves the laws when the module
|
|
114
|
+
loads, refuses to load if one fails, and mints a proof only where the proven condition holds. gdp-ts then holds every
|
|
115
|
+
call site to it: the policy is proven, every caller is held to it, and Lean checks the proof again.
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
// proofs/can-set-protection.ts (trusted: the only module that can mint this proof)
|
|
119
|
+
import {defineProof, type Named, type Proof} from '@gdp-ts/core'
|
|
120
|
+
import {fun, no, oneOf} from 'gdp-wired'
|
|
121
|
+
import {guard} from 'gdp-wired/gdp'
|
|
122
|
+
|
|
123
|
+
const canSet = fun('can set password protection')
|
|
124
|
+
.inputs({role: oneOf('owner', 'member', 'viewer', 'none'), plan: oneOf('free', 'pro', 'enterprise')})
|
|
125
|
+
.returns(({role, plan}) => ({allow: role.is('owner').or(role.is('member')).and(plan.is('free').not())}))
|
|
126
|
+
const laws = [canSet.law('the free plan never has protection', ({plan}, out) => [plan.is('free'), out.allow], () => no)]
|
|
127
|
+
|
|
128
|
+
const CanSetProtection = defineProof('CanSetProtection') // not exported
|
|
129
|
+
export interface CanSetProtection<U, P> extends Proof<'CanSetProtection', [U, P]> {}
|
|
130
|
+
const mint = guard(CanSetProtection, canSet, 'allow', {laws}) // proven here, or the module doesn't load
|
|
131
|
+
|
|
132
|
+
export async function canSetProtection<U, P>(user: Named<U, UserId>, project: Named<P, ProjectId>): Promise<CanSetProtection<U, P> | null> {
|
|
133
|
+
return mint({role: await db.role(user.value, project.value), plan: await db.plan(project.value)}, user, project)
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
A `gate` does the same for a rule with state: each request takes the rule's step, and a proof is minted when the step
|
|
138
|
+
takes the flag (a one-time token redeemed, a sign-in while the account is unlocked, an email within the hourly cap).
|
|
139
|
+
Requests outside what was proven (a failed `assume`, a history read out of order) mint nothing and leave the state as it
|
|
140
|
+
was. `examples/gdp.ts` runs gdp-ts's own example this way; `@gdp-ts/core` is an optional peer dependency.
|
|
141
|
+
|
|
104
142
|
## Writing laws
|
|
105
143
|
|
|
106
144
|
The builders (`core/dsl.ts`) hand out typed values for every name you declare; everything compiles to the plain machine
|
|
@@ -179,7 +217,8 @@ const failsClosed = trial.decide('a missing fact means no trial', ({spent}) => s
|
|
|
179
217
|
- **Two runs** (`r.relate(name, (a, b) => given, (a, b) => then)`, or `f.relate(...)` with both results): a decision
|
|
180
218
|
about two requests under the same params, for noninterference ("requests that differ only in a secret are decided
|
|
181
219
|
alike"); the second request's inputs show as `b.<input>` in a counterexample.
|
|
182
|
-
- **Loops** (`loop(...)`): state, `while`, `body`, a `rank` (or a tuple of ranks) that must drop each turn.
|
|
220
|
+
- **Loops** (`loop(...)`): state, `while`, `body`, a `rank` (or a tuple of ranks) that must drop each turn. A loop is a
|
|
221
|
+
machine (its body taken while `while` holds) and its halting a liveness of it, so Lean checks it with the machine twin.
|
|
183
222
|
- **Lean** (`gdp-wired/lean`): `toLean([{id, machine, laws, stepLaws}, ...])` writes the proofs as a Lean file, and
|
|
184
223
|
`leanProject(file)` a Lake project with the twin, so `lake build` checks them. Each law comes as the twin's theorem and
|
|
185
224
|
as a plain statement over named variables, e.g. `theorem budget.law0 (h : SReach budget_machine ρ μ) : ρ «s» ≤
|
|
@@ -229,7 +268,7 @@ Lean, compiled natively, wins where the state stays small. Details in [`docs/com
|
|
|
229
268
|
| `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 |
|
|
230
269
|
| `kernel/` | The trusted kernel and the untrusted prover, with soundness fuzz (`kernel.test.ts`, `history.test.ts`) |
|
|
231
270
|
| `lean-kernel/` | The kernel's Lean twin, its soundness proofs, and the exported trees |
|
|
232
|
-
| `examples/` | The builders at work: `domains/` (
|
|
271
|
+
| `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), `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) |
|
|
233
272
|
| `docs/` | The comparison with Bend and Lean, and the full scope |
|
|
234
273
|
| `compare/` | The same three rules in gdp-wired, Bend and plain Lean, checked and measured |
|
|
235
274
|
| `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/`) |
|
|
@@ -265,10 +304,24 @@ modal run modal/run.py --jobs modal/suite.json --out runs/modal/suite && sh moda
|
|
|
265
304
|
|
|
266
305
|
Each directory's README has its own runs and results.
|
|
267
306
|
|
|
307
|
+
## The skill
|
|
308
|
+
|
|
309
|
+
`npx skills add monotykamary/gdp-wired` installs a skill that guides coding agents (and people) through writing a rule
|
|
310
|
+
the way the code does it, stating its laws, reading a refused proof and handing a claim to Lean:
|
|
311
|
+
|
|
312
|
+
- [**Workflow**](skills/gdp-wired/SKILL.md#workflow): the checklist for adding or changing a rule whose invariants matter.
|
|
313
|
+
- [**Writing rules**](skills/gdp-wired/references/writing-rules.md): kinds, maps, step and k-step laws, histories,
|
|
314
|
+
liveness, decisions, two-run laws, loops.
|
|
315
|
+
- [**Reading verdicts**](skills/gdp-wired/references/reading-verdicts.md): a bug or a missing invariant, and what to
|
|
316
|
+
change.
|
|
317
|
+
- [**Shipping**](skills/gdp-wired/references/shipping.md): running the proven rule, and holding existing code to it.
|
|
318
|
+
- [**The Lean handoff**](skills/gdp-wired/references/lean-handoff.md): exporting the proofs, and functions proven in Lean.
|
|
319
|
+
- [**Where it fits**](skills/gdp-wired/references/where-it-fits.md): gdp-wired, Lean or Bend.
|
|
320
|
+
|
|
268
321
|
## Credits
|
|
269
322
|
|
|
270
323
|
[gdp-ts](https://github.com/rauchg/gdp-ts) by Guillermo Rauch (Ghosts of Departed Proofs for TypeScript) is where the
|
|
271
|
-
audit path starts; [Handwired](https://github.com/chenglou/handwired) by Cheng Lou is the circuit and GPU runtime this
|
|
324
|
+
audit path starts, and what `gdp-wired/gdp` mints proofs for; [Handwired](https://github.com/chenglou/handwired) by Cheng Lou is the circuit and GPU runtime this
|
|
272
325
|
builds on, through a fork; [Bend](https://github.com/bendlang/bend) is the language the laws were first written in,
|
|
273
326
|
and the yardstick for the benchmarks.
|
|
274
327
|
|
package/dist/core/dsl.d.ts
CHANGED
|
@@ -469,6 +469,7 @@ export declare class Fun<P extends Obj = {}, I extends Obj = {}, O extends Obj =
|
|
|
469
469
|
}): Value<O>;
|
|
470
470
|
}
|
|
471
471
|
export declare const fun: (name: string) => Fun;
|
|
472
|
+
export declare const funOf: (d: Decision) => Fun<Obj, Obj, Obj> | undefined;
|
|
472
473
|
export type Agreement = {
|
|
473
474
|
checked: number;
|
|
474
475
|
failure?: string;
|
package/dist/core/dsl.js
CHANGED
|
@@ -1457,11 +1457,11 @@ export class Fun {
|
|
|
1457
1457
|
}
|
|
1458
1458
|
// Whenever the arguments (and the result) meet `given`, the result meets `then`.
|
|
1459
1459
|
law(name, given, then) {
|
|
1460
|
-
return this.r.decide(name, env => given(env, this.result(env)), env => then(this.result(env), env));
|
|
1460
|
+
return lawOfFun(this, this.r.decide(name, env => given(env, this.result(env)), env => then(this.result(env), env)));
|
|
1461
1461
|
}
|
|
1462
1462
|
// A law about two calls under the same params: `then(outA, outB, a, b)` given `given(a, b)` (noninterference, say).
|
|
1463
1463
|
relate(name, given, then) {
|
|
1464
|
-
return this.r.relate(name, given, (a, b) => then(this.result(a), this.result(b), a, b));
|
|
1464
|
+
return lawOfFun(this, this.r.relate(name, given, (a, b) => then(this.result(a), this.result(b), a, b)));
|
|
1465
1465
|
}
|
|
1466
1466
|
// Against the code it describes: `reference` called with the same arguments (drawn by their kinds, or by `draw`, the
|
|
1467
1467
|
// `edges` first) must return the same value for every field it returns (a field it leaves out is not compared). The
|
|
@@ -1499,6 +1499,11 @@ export class Fun {
|
|
|
1499
1499
|
}
|
|
1500
1500
|
}
|
|
1501
1501
|
export const fun = (name) => new Fun(rule(name));
|
|
1502
|
+
// The function a law was stated about. Variants of one function share its arguments, so a law's terms alone don't say
|
|
1503
|
+
// which result it is about; a guard (gdp.ts) takes only laws stated about the function it runs.
|
|
1504
|
+
const lawsOfFuns = new WeakMap();
|
|
1505
|
+
const lawOfFun = (f, d) => { lawsOfFuns.set(d, f); return d; };
|
|
1506
|
+
export const funOf = (d) => lawsOfFuns.get(d);
|
|
1502
1507
|
// A loop runs `body` while `while` holds, each turn with fresh inputs; it halts if a rank over the state and params
|
|
1503
1508
|
// (or a tuple of them, compared in order) drops on every turn.
|
|
1504
1509
|
export class LoopRule {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Named, NamesOf, Proof, Prover } from '@gdp-ts/core';
|
|
2
|
+
import { type Decision, type Fun, type Rule, type StepLaw, type Verdict } from './dsl.js';
|
|
3
|
+
import { type Law, type State } from './machine.js';
|
|
4
|
+
type AnyNamed = Named<any, any>;
|
|
5
|
+
export type Mint<K extends string, A> = <S extends AnyNamed[]>(args: A, ...about: S) => Proof<K, NamesOf<S>> | null;
|
|
6
|
+
export declare class Unproven extends Error {
|
|
7
|
+
readonly verdict: Verdict;
|
|
8
|
+
constructor(what: string, verdict: Verdict);
|
|
9
|
+
}
|
|
10
|
+
type AnyFun = Fun<any, any, any>;
|
|
11
|
+
type Conditions<O> = {
|
|
12
|
+
[k in keyof O]: O[k] extends boolean ? k : never;
|
|
13
|
+
}[keyof O] & string;
|
|
14
|
+
type ArgsOf<F extends AnyFun> = Parameters<F['run']>[0];
|
|
15
|
+
type ParamsOf<F extends AnyFun> = NonNullable<Parameters<F['run']>[1]>;
|
|
16
|
+
export declare function guard<K extends string, F extends AnyFun>(prover: Prover<K>, fn: F, field: Conditions<ReturnType<F['run']>>, opts: {
|
|
17
|
+
laws: Decision[];
|
|
18
|
+
params?: ParamsOf<F>;
|
|
19
|
+
}): Mint<K, ArgsOf<F>>;
|
|
20
|
+
type AnyRule = Rule<any, any, any, any, any, any, any, any>;
|
|
21
|
+
type FlagOf<R> = R extends Rule<any, any, any, any, infer F, any, any, any> ? keyof F & string : never;
|
|
22
|
+
type InputOf<R extends AnyRule> = Parameters<R['encodeInput']>[0];
|
|
23
|
+
type RuleParams<R extends AnyRule> = Parameters<R['encodeParams']>[0];
|
|
24
|
+
export type Gate<K extends string, I> = {
|
|
25
|
+
request: Mint<K, I>;
|
|
26
|
+
readonly state: State;
|
|
27
|
+
values(): Record<string, unknown>;
|
|
28
|
+
};
|
|
29
|
+
export declare function gate<K extends string, R extends AnyRule>(prover: Prover<K>, r: R, flag: FlagOf<R>, opts: {
|
|
30
|
+
laws: Law[];
|
|
31
|
+
stepLaws?: StepLaw[];
|
|
32
|
+
params?: RuleParams<R>;
|
|
33
|
+
state?: State;
|
|
34
|
+
}): Gate<K, InputOf<R>>;
|
|
35
|
+
export {};
|
package/dist/core/gdp.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { funOf, proveDecision, proveSteps } from "./dsl.js";
|
|
2
|
+
import { evalC, interpret } from "./machine.js";
|
|
3
|
+
import { allowed, requestEnv } from "./watch.js";
|
|
4
|
+
// A verdict that isn't a checked proof, as the error a guard or gate is refused with (the verdict on `.verdict`).
|
|
5
|
+
export class Unproven extends Error {
|
|
6
|
+
verdict;
|
|
7
|
+
constructor(what, verdict) {
|
|
8
|
+
const found = [verdict.goal && `goal ${verdict.goal}`, verdict.stuck, verdict.witness && `a counterexample: ${JSON.stringify(verdict.witness)}`, verdict.why].filter(Boolean).join('; ');
|
|
9
|
+
super(`${what}: ${verdict.proven ? 'proven, but the kernel did not check the proof' : `not proven${found ? ` (${found})` : ''}`}`);
|
|
10
|
+
this.verdict = verdict;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
const proven = (what, v) => { if (!(v.proven && v.checked))
|
|
14
|
+
throw new Unproven(what, v); };
|
|
15
|
+
// A proof wherever `fn`'s result `field` holds. Every law in `laws` must have been stated about `fn` (fn.law or
|
|
16
|
+
// fn.relate) and is proven now; the arguments it is later called with must meet the function's kinds and `assume`.
|
|
17
|
+
// const access = fun('document access').inputs({...}).returns(env => ({permit: permit(env)}))
|
|
18
|
+
// const mint = guard(CanRead, access, 'permit', {laws: [access.law('...', ...)]})
|
|
19
|
+
export function guard(prover, fn, field, opts) {
|
|
20
|
+
if (opts.laws.length === 0)
|
|
21
|
+
throw new Error(`${fn.name}: a guard needs a law to prove`);
|
|
22
|
+
for (const d of opts.laws) {
|
|
23
|
+
if (funOf(d) !== fn)
|
|
24
|
+
throw new Error(`${fn.name}: "${d.name}" was not stated about this function (state it with ${fn.name}.law)`);
|
|
25
|
+
proven(`${fn.name}: "${d.name}"`, proveDecision(d));
|
|
26
|
+
}
|
|
27
|
+
const r = fn.rule, m = r.machine, params = (opts.params ?? {}), ps = r.encodeParams(params);
|
|
28
|
+
if (m.pre && !evalC(m.pre, ps, { scalars: {}, maps: {} }, m))
|
|
29
|
+
throw new Error(`${fn.name}: the params ${JSON.stringify(params)} don't meet its precondition`);
|
|
30
|
+
return (args, ...about) => {
|
|
31
|
+
const input = r.encodeInput(args);
|
|
32
|
+
if (m.assume && !evalC(m.assume, { ...ps, ...input }, { scalars: {}, maps: {} }, m))
|
|
33
|
+
return null;
|
|
34
|
+
return fn.run(args, params)[field] === true ? prover.prove(...about) : null;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
// A proof for each request whose step takes `flag`, the rule's laws (and step laws) proven now. One gate holds one
|
|
38
|
+
// run's state: keep one per state it guards (per process, or loaded and saved around each request in a transaction).
|
|
39
|
+
// const tokens = rule('one-time tokens').inputs({...}).maps({...}).flags(env => ({redeem: ...})).step(...)
|
|
40
|
+
// const redeem = gate(Redeemed, tokens, 'redeem', {laws: tokenLaws})
|
|
41
|
+
// redeem.request({op: 'redeem', token: 7}, user, token) // a Redeemed proof the first time, null after
|
|
42
|
+
export function gate(prover, r, flag, opts) {
|
|
43
|
+
const m = r.machine;
|
|
44
|
+
if (!(flag in (m.step.flags ?? {})))
|
|
45
|
+
throw new Error(`${m.name}: no flag ${flag}`);
|
|
46
|
+
if (opts.laws.length + (opts.stepLaws?.length ?? 0) === 0)
|
|
47
|
+
throw new Error(`${m.name}: a gate needs a law to prove`);
|
|
48
|
+
proven(m.name, proveSteps(r, opts.laws, opts.stepLaws ?? []));
|
|
49
|
+
const params = (opts.params ?? {}), ps = r.encodeParams(params);
|
|
50
|
+
if (m.pre && !evalC(m.pre, ps, { scalars: {}, maps: {} }, m))
|
|
51
|
+
throw new Error(`${m.name}: the params ${JSON.stringify(params)} don't meet its precondition`);
|
|
52
|
+
const run = interpret(m), compiled = r.compiled();
|
|
53
|
+
let st = opts.state ?? compiled.init(ps);
|
|
54
|
+
return {
|
|
55
|
+
request: (args, ...about) => {
|
|
56
|
+
const input = r.encodeInput(args);
|
|
57
|
+
if (!allowed(m, run, ps, st, input))
|
|
58
|
+
return null;
|
|
59
|
+
const granted = requestEnv(m, run, ps, st, input)[`?${flag}`] === 1;
|
|
60
|
+
st = compiled.step(ps, st, input);
|
|
61
|
+
return granted ? prover.prove(...about) : null;
|
|
62
|
+
},
|
|
63
|
+
get state() { return st; },
|
|
64
|
+
values: () => r.decode(st),
|
|
65
|
+
};
|
|
66
|
+
}
|
package/dist/kernel/kernel.d.ts
CHANGED
|
@@ -61,6 +61,8 @@ export type Loop = {
|
|
|
61
61
|
pre?: C;
|
|
62
62
|
assume?: C;
|
|
63
63
|
};
|
|
64
|
+
export declare const loopMachine: (l: Loop) => Machine;
|
|
65
|
+
export declare const loopLiveness: (l: Loop, rank: E | E[]) => Liveness;
|
|
64
66
|
export declare function loopObligations(l: Loop, laws: Law[], rank: E | E[]): Goal[];
|
|
65
67
|
export declare function progressObligations(m: Machine, laws: Law[], instances: Instances, p: {
|
|
66
68
|
name: string;
|
package/dist/kernel/kernel.js
CHANGED
|
@@ -716,8 +716,6 @@ export function historyMachine(m) {
|
|
|
716
716
|
const h = m.history;
|
|
717
717
|
if (!h)
|
|
718
718
|
return { machine: m, goals: [] };
|
|
719
|
-
if (m.inputMaps?.length || quantified(m).length || Object.values(m.step.maps ?? {}).some(w => 'each' in w))
|
|
720
|
-
throw new Error(`${m.name}: a machine with a history has no maps carried by requests, quantified flags or writes at every key`);
|
|
721
719
|
const queries = Object.keys(h.queries), sums = Object.keys(h.sums), plainInputs = m.inputs.filter(i => !queries.includes(i));
|
|
722
720
|
const fields = h.record.map(f => `r.${f}`);
|
|
723
721
|
const taken = [...m.params, ...m.inputs, ...Object.keys(m.state), ...Object.keys(m.step.state ?? {})];
|
|
@@ -734,12 +732,15 @@ export function historyMachine(m) {
|
|
|
734
732
|
if (h.prune && !(h.record.includes(h.prune.order) && natural(h.prune.after) && readsOnly(h.prune.after, m.params)))
|
|
735
733
|
throw new Error('a history is pruned by a recorded input and a term over the params');
|
|
736
734
|
const atRequest = { vars: Object.fromEntries(h.record.map(f => [`r.${f}`, { op: 'var', name: f }])), maps: {}, flags: {} };
|
|
737
|
-
|
|
735
|
+
// The relations conjoined to the assumption as the Lean twin builds them (History.lean's HMachine.plain: assume ∧
|
|
736
|
+
// r₁ ∧ … ∧ true), so the two read the same assumption.
|
|
737
|
+
const TRUE = { op: '<=', a: { op: 'const', value: 0 }, b: { op: 'const', value: 0 } };
|
|
738
|
+
const relations = (h.relations ?? []).map(r => ({ op: 'or', xs: [{ op: 'not', x: r.when }, r.holds] })).reduceRight((acc, r) => ({ op: 'and', xs: [r, acc] }), TRUE);
|
|
738
739
|
const machine = {
|
|
739
740
|
...m, history: undefined,
|
|
740
741
|
state: { ...m.state, ...Object.fromEntries(sums.map(s => [s, { op: 'const', value: 0 }])) },
|
|
741
742
|
step: { ...m.step, state: { ...m.step.state, ...Object.fromEntries(sums.map(s => [s, { op: 'add', a: { op: 'var', name: s }, b: { op: 'if', c: h.keep, then: substE(h.sums[s], atRequest), else: { op: 'const', value: 0 } } }])) } },
|
|
742
|
-
|
|
743
|
+
assume: { op: 'and', xs: [m.assume ?? TRUE, relations] },
|
|
743
744
|
};
|
|
744
745
|
const perRecord = { vars: { ...h.sums, ...h.queries }, maps: {}, flags: {} };
|
|
745
746
|
const goals = (h.relations ?? []).map((r, i) => ({ name: `relation ${i + 1} @record`, hyps: [...preOf(m), r.when, { op: 'not', x: substC(r.holds, perRecord) }] }));
|
|
@@ -781,10 +782,20 @@ export function obligations(m, laws, instances = {}) {
|
|
|
781
782
|
export function checkLaws(m, laws, proof) {
|
|
782
783
|
return obligations(m, laws, proof.instances).every(g => proof.trees[g.name] !== undefined && check(g, proof.trees[g.name]));
|
|
783
784
|
}
|
|
785
|
+
// A loop is a machine: its state, a turn's inputs as a request, its body taken where `while` holds and the state kept
|
|
786
|
+
// where it fails. Its invariants are the machine's laws, and that it halts is a liveness: from anywhere, `while` fails
|
|
787
|
+
// later, every request helpful, the rank the loop's (Live.lean's Loop.machine and loop_halts_of: then no run of turns is
|
|
788
|
+
// infinite). `while` and the rank read the state and params, not the turn's input.
|
|
784
789
|
// A rank may be a tuple, compared in order (lexicographically): the first part drops, or it stays and the rest drops.
|
|
785
790
|
// Tuples of naturals can't drop forever either; nested loops need them (an outer count, then an inner one).
|
|
786
791
|
// A rank reads the state and params only: one that reads the turn's input drops against this turn's input, but the next
|
|
787
792
|
// turn measures it with a fresh one, so nothing need decrease (while x < w, x := x + 1, rank w ∸ x, w = x + 1 each turn).
|
|
793
|
+
export const loopMachine = (l) => ({
|
|
794
|
+
name: l.name, params: l.params, inputs: l.inputs, state: l.state,
|
|
795
|
+
step: { state: Object.fromEntries(Object.entries(l.body).map(([v, e]) => [v, { op: 'if', c: l.while, then: e, else: { op: 'var', name: v } }])) },
|
|
796
|
+
...(l.pre ? { pre: l.pre } : {}), ...(l.assume ? { assume: l.assume } : {}),
|
|
797
|
+
});
|
|
798
|
+
export const loopLiveness = (l, rank) => ({ name: `${l.name} halts`, from: trueC, to: { op: 'not', x: l.while }, rank, request: {} });
|
|
788
799
|
export function loopObligations(l, laws, rank) {
|
|
789
800
|
const ranks = Array.isArray(rank) ? rank : [rank];
|
|
790
801
|
const parts = [...Object.values(l.state), ...Object.values(l.body), ...ranks];
|
|
@@ -794,26 +805,17 @@ export function loopObligations(l, laws, rank) {
|
|
|
794
805
|
if (!(name in l.state))
|
|
795
806
|
throw new Error(`${l.name}'s body sets ${name}, which isn't in its state`);
|
|
796
807
|
inScope(`${l.name}'s start`, Object.values(l.state), l.params);
|
|
797
|
-
inScope(l.name, [...Object.values(l.body),
|
|
808
|
+
inScope(l.name, [...Object.values(l.body), ...(l.assume ? [l.assume] : [])], names);
|
|
809
|
+
inScope(`${l.name}'s while`, [l.while], [...l.params, ...Object.keys(l.state)]);
|
|
798
810
|
inScope(`${l.name}'s precondition`, l.pre ? [l.pre] : [], l.params);
|
|
799
811
|
inScope(`${l.name}'s rank and laws`, [...ranks, ...laws.map(law => law.holds)], [...l.params, ...Object.keys(l.state)]);
|
|
800
812
|
if (ranks.some(r => mentions({ op: '=', a: r, b: r }, l.inputs)))
|
|
801
813
|
throw new Error('a rank reads the state and params, not the turn\'s input');
|
|
802
814
|
if (!parts.every(natural) || !naturalC(l.while) || !laws.every(law => !law.forall && !law.keys?.length && naturalC(law.holds) && !mentions(law.holds, l.inputs)))
|
|
803
815
|
throw new Error('outside the kernel');
|
|
804
|
-
const
|
|
805
|
-
|
|
806
|
-
const turn = step(holds(from(), [...pre, ...(l.assume ? [l.assume] : []), l.while, ...laws.map(law => law.holds)]), { vars: l.body, maps: {} });
|
|
807
|
-
return [
|
|
808
|
-
...laws.flatMap(law => [goal(`${law.name} @start`, start, atEnd(start, law.holds)), goal(`${law.name} @turn`, turn, atEnd(turn, law.holds))]),
|
|
809
|
-
goal('rank drops', turn, drops(ranks, turn.at)),
|
|
810
|
-
];
|
|
816
|
+
const m = loopMachine(l), { stay, cover, helps } = livenessParts(m, loopLiveness(l, rank));
|
|
817
|
+
return [...obligations(m, laws, {}), ...[stay, cover, ...helps].map(sl => stepLawObligations(m, laws, {}, sl)[0])];
|
|
811
818
|
}
|
|
812
|
-
const drops = (ranks, turn) => {
|
|
813
|
-
const [r, ...rest] = ranks;
|
|
814
|
-
const after = substE(r, turn), less = { op: '<', a: after, b: r };
|
|
815
|
-
return rest.length === 0 ? less : { op: 'or', xs: [less, { op: 'and', xs: [{ op: '=', a: after, b: r }, drops(rest, turn)] }] };
|
|
816
|
-
};
|
|
817
819
|
// --- progress: laws with "there is a request" ----------------------------------------------------------------------
|
|
818
820
|
// "In every reachable state where `when` holds, some request makes `then` true of the step": proven by naming the
|
|
819
821
|
// request (a term per input over the state and params) and proving it allowed (the machine's assumption holds of it)
|
|
@@ -59,7 +59,7 @@ export declare function mmachineToLean(id: string, m: Machine, laws: Law[], opts
|
|
|
59
59
|
stepLaws?: StepLaw[];
|
|
60
60
|
}): string;
|
|
61
61
|
export declare const hasSets: (m: Machine) => boolean;
|
|
62
|
-
export declare function smachineToLean(id: string,
|
|
62
|
+
export declare function smachineToLean(id: string, given: Machine, laws: Law[], opts?: {
|
|
63
63
|
stepLaws?: StepLaw[];
|
|
64
64
|
liveness?: Liveness[];
|
|
65
65
|
tamper?: {
|
|
@@ -70,6 +70,14 @@ export declare function smachineToLean(id: string, m: Machine, laws: Law[], opts
|
|
|
70
70
|
free?: string[];
|
|
71
71
|
ghosts?: string[];
|
|
72
72
|
after?: (ix: (name: string) => number) => string;
|
|
73
|
+
historyTampers?: {
|
|
74
|
+
what: string;
|
|
75
|
+
relations: {
|
|
76
|
+
when: C;
|
|
77
|
+
holds: C;
|
|
78
|
+
}[];
|
|
79
|
+
withRecordTrees: boolean;
|
|
80
|
+
}[];
|
|
73
81
|
refined?: {
|
|
74
82
|
impl: Machine;
|
|
75
83
|
spec: Machine;
|
|
@@ -77,8 +85,15 @@ export declare function smachineToLean(id: string, m: Machine, laws: Law[], opts
|
|
|
77
85
|
stutter: C;
|
|
78
86
|
specPre: number;
|
|
79
87
|
};
|
|
88
|
+
loop?: {
|
|
89
|
+
loop: Loop;
|
|
90
|
+
tamper?: {
|
|
91
|
+
what: string;
|
|
92
|
+
loop: Loop;
|
|
93
|
+
};
|
|
94
|
+
};
|
|
80
95
|
}): string;
|
|
81
|
-
export declare function hmachineToLean(id: string,
|
|
96
|
+
export declare function hmachineToLean(id: string, m: Machine, laws: Law[], tampers?: {
|
|
82
97
|
what: string;
|
|
83
98
|
relations: {
|
|
84
99
|
when: C;
|