gdp-wired 0.2.0 → 0.4.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 +128 -92
- package/dist/core/compose.d.ts +15 -1
- package/dist/core/compose.js +132 -8
- package/dist/core/dsl.d.ts +272 -17
- package/dist/core/dsl.js +1098 -60
- package/dist/core/machine.d.ts +34 -6
- package/dist/core/machine.js +194 -22
- package/dist/core/watch.d.ts +14 -0
- package/dist/core/watch.js +78 -0
- package/dist/kernel/kernel.d.ts +30 -2
- package/dist/kernel/kernel.js +306 -48
- package/dist/kernel/prove.d.ts +36 -9
- package/dist/kernel/prove.js +317 -28
- package/dist/lean-kernel/emit.d.ts +124 -0
- package/dist/lean-kernel/emit.js +875 -0
- package/dist/lean-kernel/lean-terms.d.ts +5 -0
- package/dist/lean-kernel/lean-terms.js +44 -0
- package/dist/lean-kernel/library.d.ts +1 -0
- package/dist/lean-kernel/library.js +13 -0
- package/package.json +11 -4
package/README.md
CHANGED
|
@@ -2,12 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
# gdp-wired
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
You write the rule once with typed builders and state what must always hold. The library proves it by induction (or
|
|
10
|
-
shows a real run that breaks it), and the same rule runs as an interpreter or ships as generated JavaScript.
|
|
5
|
+
An automatic prover for stateful rules: budgets, caps, rate limits, workflows, access policies. You write the rule and
|
|
6
|
+
the laws it must keep. gdp-wired proves them with no proof to write, shows a real run that breaks a law when it's false
|
|
7
|
+
(found by searching on the GPU), has every proof checked again by Lean, and ships the rule as JavaScript.
|
|
11
8
|
|
|
12
9
|
```sh
|
|
13
10
|
npm install gdp-wired # or bun add gdp-wired; plain JavaScript with types, Node 20+, Bun or a browser
|
|
@@ -33,62 +30,76 @@ Forget the cap (`s.plus(c)`) and the kernel refuses the proof; a search from the
|
|
|
33
30
|
and `examples/domains/` holds 92 rules from nine domains (finance, access control, public services, operations,
|
|
34
31
|
protocols, commerce, supply chains, games, education), each checked three ways.
|
|
35
32
|
|
|
36
|
-
##
|
|
33
|
+
## Where it fits
|
|
37
34
|
|
|
38
|
-
gdp-wired
|
|
39
|
-
state is counters, totals, maps of counters and logs summed over time**, proven automatically, with the rule shipped as
|
|
40
|
-
code. If the checker side finds new frontiers the scope may grow; until then:
|
|
35
|
+
<img src="https://raw.githubusercontent.com/monotykamary/gdp-wired/main/static/venn.svg" alt="What Lean, Bend and gdp-wired each cover, and where they overlap" width="640" />
|
|
41
36
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
37
|
+
Lean proves anything you write the proof for. Bend runs programs that carry their proofs. gdp-wired proves one kind of
|
|
38
|
+
claim, laws about stateful rules, with no proof to write, and hands every proof to Lean as a theorem. Where a proof needs
|
|
39
|
+
more than it decides, the proof is Lean's: a function you define and prove there, which the rule then uses.
|
|
40
|
+
[`docs/comparison.md`](docs/comparison.md) goes through each region, compares the three side by side, and measures them
|
|
41
|
+
on the same rules.
|
|
42
|
+
|
|
43
|
+
## What it proves
|
|
44
|
+
|
|
45
|
+
All of these are proven automatically, and each proof is also a Lean theorem:
|
|
46
|
+
|
|
47
|
+
- **Invariants**: a law that holds after every sequence of requests ("spend stays within the budget").
|
|
48
|
+
- **Maps, sets and lists of names**: laws at every key ("no day passes its cap"), sets a request carries, conditions
|
|
49
|
+
about every key, lists kept in order (`examples/sets.ts`, `examples/lists.ts`).
|
|
50
|
+
- **Step laws**: what one step does ("a balance only goes down", "a grant adds one"), and laws over several steps in a
|
|
51
|
+
row.
|
|
52
|
+
- **Histories**: sums and queries over a log of requests, like a sliding-window rate limit.
|
|
53
|
+
- **Orderings at any distance**: "a refund never follows a release", through ghost state that watches the rule.
|
|
54
|
+
- **Liveness**: "every ticket issued is served", in every run that keeps sending the requests that help, including under
|
|
55
|
+
weak fairness (`examples/liveness.ts`).
|
|
56
|
+
- **Refinement**: an implementation behaves like a simpler spec, step for step (`examples/composition.ts`).
|
|
57
|
+
- **Functions**: division, remainder, and your own functions, known by their facts; a function defined in Lean brings
|
|
58
|
+
facts proven there (`examples/functions.ts`).
|
|
59
|
+
- **Pure functions and two-run laws**: laws about what a function returns, and laws comparing two calls, like "the
|
|
60
|
+
secret never changes the answer". `examples/jev-fabric/` ports a Bend project's policy core and proves all 59 laws its
|
|
61
|
+
Bend proofs state.
|
|
62
|
+
|
|
63
|
+
When a law is false, the GPU search finds a real run that breaks it. When a law needs an invariant nobody wrote, the GPU
|
|
64
|
+
synthesizer finds one and the kernel proves it.
|
|
65
|
+
|
|
66
|
+
**What it leaves to Lean or Bend.** Two things, and neither is a gap the next feature closes: proofs the prover would
|
|
67
|
+
have to invent (induction over a recursive structure whose proof needs a lemma nobody stated, and arithmetic with
|
|
68
|
+
products of unknowns beyond multiplying two known facts; integer arithmetic with induction or with products is
|
|
69
|
+
undecidable), and shipping an arbitrary algorithm as its proved source (gdp-wired ships the rule as JavaScript; Bend
|
|
70
|
+
compiles the proved source itself). The full list of what is and isn't in scope is in
|
|
71
|
+
[`docs/comparison.md`](docs/comparison.md#gdp-wireds-scope-in-detail).
|
|
72
|
+
|
|
73
|
+
## Why you can trust a proof
|
|
74
|
+
|
|
75
|
+
A small kernel checks every proof; the prover that finds them is untrusted, and the kernel's twin in Lean is proven
|
|
76
|
+
sound.
|
|
67
77
|
|
|
68
78
|
| Part | Trusted? | What it does |
|
|
69
79
|
| --- | --- | --- |
|
|
70
|
-
| `kernel/kernel.ts`, `core/check.ts` | **Yes** | Turns a
|
|
71
|
-
| `kernel/prove.ts` | No |
|
|
72
|
-
| `lean-kernel/` | Optional | The kernel's twin in Lean 4.34.0,
|
|
73
|
-
| `search/`, `synth/`, `hw/` | No | GPU
|
|
74
|
-
| `core/dsl.ts`, `core/compose.ts`, the runtimes | No | The builders and combinators compile to the kernel's plain values; the
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
- **
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
- **
|
|
85
|
-
|
|
86
|
-
- **
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
- **
|
|
91
|
-
|
|
80
|
+
| `kernel/kernel.ts`, `core/check.ts` | **Yes** | Turns a rule and its laws into goals, and checks a proof tree for each: case splits on linear facts, then a linear refutation at each leaf |
|
|
81
|
+
| `kernel/prove.ts` | No | Finds the proof trees. If it's wrong, the kernel rejects the tree |
|
|
82
|
+
| `lean-kernel/` | Optional | The kernel's twin in Lean 4.34.0, every rule proven sound there with the standard axioms only. The exporter sends the kernel's real trees; tampered ones compute to false |
|
|
83
|
+
| `research/search/`, `research/synth/`, `research/hw/` | No | The GPU searches for breaking runs, and invariant and rank synthesis. Every hit is replayed or proven before it counts |
|
|
84
|
+
| `core/dsl.ts`, `core/compose.ts`, the runtimes | No | The builders and combinators compile to the kernel's plain values; the runtimes are held to a reference evaluator |
|
|
85
|
+
|
|
86
|
+
The untrusted parts are held to the trusted ones by the test suite:
|
|
87
|
+
|
|
88
|
+
- **Builders**: every law, step law, flag, update and decision in the examples is run again on concrete runs, its
|
|
89
|
+
callback's answer computed by the builders' own arithmetic (each term carries its value beside its expression) against
|
|
90
|
+
what the interpreter makes of the expression that was proven (`rule.crossCheck`, `crossCheckDecision`). A builder that
|
|
91
|
+
writes a different expression than it means is caught, even where the kernel would still prove the law it wrote.
|
|
92
|
+
- **Encodings**: `fun.compare(reference)` and `rule.compare({init, step})` run the rule against the code it describes,
|
|
93
|
+
on drawn and edge inputs (strings, decimals and choices as themselves), field by field.
|
|
94
|
+
- **Types**: strict TypeScript and a lint that denies warnings; every switch over operations is exhaustive, so a new
|
|
95
|
+
operation fails to compile wherever it's missing.
|
|
96
|
+
- **Conformance**: random terms through every evaluator (interpreter, generated JavaScript, the kernel, the GPU search,
|
|
97
|
+
Lean's own evaluator) against the reference one.
|
|
98
|
+
- **Examples**: every law proven and re-checked in Lean, every rule run at random, every broken variant broken by a real
|
|
99
|
+
run.
|
|
100
|
+
- **Tampering and mutation**: broken proofs must be rejected; every one-operator change to the kernel is run against the
|
|
101
|
+
tests, and the few that survive are equivalent (`kernel/MUTANTS.md`).
|
|
102
|
+
- **Fuzz**: the kernel never accepts a refutation of something that has a model.
|
|
92
103
|
|
|
93
104
|
## Writing laws
|
|
94
105
|
|
|
@@ -133,16 +144,50 @@ const failsClosed = trial.decide('a missing fact means no trial', ({spent}) => s
|
|
|
133
144
|
({plan, spent, allowance}) => plan.match({free: () => spent.match({none: () => yes, some: s => allowance.match({none: () => yes, some: a => s.atLeast(a)})}), pro: () => yes}))
|
|
134
145
|
```
|
|
135
146
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
147
|
+
|
|
148
|
+
- **Kinds**: `nat`, `upTo(k)`, `bool`, `oneOf(...)`, `maybe(kind)`, `int`, `decimal(places)`, `str({like, maxLength})`;
|
|
149
|
+
their assumptions (a bit is 0 or 1, a choice is in range) go to the kernel by themselves, and
|
|
150
|
+
`encodeInput`/`encodeParams` turn runtime values into the numbers the machine reads.
|
|
151
|
+
- **Integers and decimals**: `int` and `decimal(places)` inputs, `.ints({...})` and `.decimals(places, {...})` state;
|
|
152
|
+
`plus`, `minus`, `negate`, `times(k)` (k may be negative), `min`, `max`, `abs`, `isNegative`, comparisons with numbers
|
|
153
|
+
(a decimal's literals are scaled: `price.atMost(19.99)`). Each is two naturals underneath, so the kernel and the Lean
|
|
154
|
+
twin see nothing new; `rule.decode(state)` reads them back as numbers.
|
|
155
|
+
- **Strings**: `str({like: ['*.internal'], maxLength: 64})`. A string is its interned name, its length and a bit per
|
|
156
|
+
declared pattern (Cedar's glob), all computed from the string itself when the input is encoded. `s.like(pattern)`,
|
|
157
|
+
`s.eq('admin')` (which carries the literal's length and pattern bits, so laws reason from either), `s.isEmpty`,
|
|
158
|
+
`s.length`; sets of strings are carried maps keyed by `s.id`.
|
|
139
159
|
- **Arithmetic** over the naturals: `plus`, `minus` (truncated), `times(k)`, `mul` (a product of two terms), `min`,
|
|
140
160
|
`max`, `when(cond, a, b)`, `count(cond)`; conditions `lt`, `atMost`, `eq`, `and`, `or`, `not`, `implies`.
|
|
161
|
+
- **Sets and quantifiers**: `.carries(...)` for sets a request brings, `everyKey(k => ...)` (and `has`, `subsetOf`,
|
|
162
|
+
`disjointFrom`, `sameAs`) anywhere in a step or inside a law, `M.each(k => ...)` to write every key, and
|
|
163
|
+
`someKey(k => ..., witness?)` for "some key where", proven through a witness the prover finds or the one given.
|
|
164
|
+
- **Functions** (`.fns({perMinute: divBy(60)})`, or `fn(arity, js, {facts, lean})` for your own): read like params that
|
|
165
|
+
take arguments, `perMinute(t)`; the kernel knows each only by its facts (`.facts(...)`, and a library function's
|
|
166
|
+
own), and the Lean export takes them as the hypothesis they are, proving them where the function has a Lean
|
|
167
|
+
definition. A function defined and proven in Lean is how a rule reaches past what the kernel decides: `examples/functions.ts`.
|
|
168
|
+
- **Liveness** (`rule.leadsTo(name, {from, to, rank, helpful})`, `proveLeadsTo`): whenever `from` holds, `to` holds later in
|
|
169
|
+
every run that takes each helpful request infinitely often; proven by a rank no step raises while waiting and each
|
|
170
|
+
helpful request lowers in its region. `rank` may return several terms (compared in order) and `helpful` may be a list
|
|
171
|
+
of `{when, request}`; a request names only the inputs it fixes (`examples/liveness.ts`: batches, a history with a
|
|
172
|
+
query, a machine with sets).
|
|
173
|
+
- **Pure functions** (`fun(name).inputs(...).returns(env => ({...}))`): each field of the result a term or a condition;
|
|
174
|
+
`f.law(name, given, then)` is a decision about the terms it returns and `f.run(values)` runs those terms compiled. A
|
|
175
|
+
decision assumes the functions' facts where it reads them.
|
|
176
|
+
- **Two runs** (`r.relate(name, (a, b) => given, (a, b) => then)`, or `f.relate(...)` with both results): a decision
|
|
177
|
+
about two requests under the same params, for noninterference ("requests that differ only in a secret are decided
|
|
178
|
+
alike"); the second request's inputs show as `b.<input>` in a counterexample.
|
|
141
179
|
- **Loops** (`loop(...)`): state, `while`, `body`, a `rank` (or a tuple of ranks) that must drop each turn.
|
|
142
|
-
- **
|
|
180
|
+
- **Lean** (`gdp-wired/lean`): `toLean([{id, machine, laws, stepLaws}, ...])` writes the proofs as a Lean file, and
|
|
181
|
+
`leanProject(file)` a Lake project with the twin, so `lake build` checks them. Each law comes as the twin's theorem and
|
|
182
|
+
as a plain statement over named variables, e.g. `theorem budget.law0 (h : SReach budget_machine ρ μ) : ρ «s» ≤
|
|
183
|
+
(ρ «b» + ρ «m»)`, for a Lean development to cite.
|
|
184
|
+
- **Composition** (`gdp-wired/compose`): `product(a, b)`, `interleave(a, b)`, `refinement(impl, spec, {relation, witness, stutter})`,
|
|
185
|
+
`steps(spec, k)` (k spec steps as one, each skippable) and `ghost(m, {name: ever(...) | count(...) | total(m, map, name)})`
|
|
143
186
|
build plain machines from machines (each side's state named `a_x`, `b_x`). A refinement's laws are the relation, proven
|
|
144
187
|
by induction, and its step law says the spec allows the request `witness` sends it at each of the implementation's:
|
|
145
|
-
`examples/composition.ts` proves a meter that counts down refines one that counts up, and refuses two broken ones
|
|
188
|
+
`examples/composition.ts` proves a meter that counts down refines one that counts up, and refuses two broken ones;
|
|
189
|
+
a two-phase meter refines it with stuttering; an escrow never refunds after a release (at any distance); and the
|
|
190
|
+
connection pool's in-use count is the total of every client's holdings.
|
|
146
191
|
- **Running it**: `rule.interpreter()` is persistent (searches branch from any state); `rule.compiled()` is what an app
|
|
147
192
|
ships: maps and the log updated in place, so a step costs the same however long the history. Both refuse params and
|
|
148
193
|
inputs that aren't naturals below 2^53 and any value past 2^53 − 1, where the proofs over the naturals stop describing
|
|
@@ -154,33 +199,25 @@ On Modal CPUs (2 vCPU), generated JavaScript, the answers checked against the in
|
|
|
154
199
|
|
|
155
200
|
| Machine | 1k | 10k | 100k | 1M |
|
|
156
201
|
| --- | --- | --- | --- | --- |
|
|
157
|
-
| Per-key caps (`perf/grant-scale.ts`), requests | 0.3 ms | 2.5 ms | 29 ms | 267 ms |
|
|
158
|
-
| Window limit, pruned (`perf/window-scale.ts`), events | 0.8 ms | 3.1 ms | 16 ms | 136 ms |
|
|
202
|
+
| Per-key caps (`research/perf/grant-scale.ts`), requests | 0.3 ms | 2.5 ms | 29 ms | 267 ms |
|
|
203
|
+
| Window limit, pruned (`research/perf/window-scale.ts`), events | 0.8 ms | 3.1 ms | 16 ms | 136 ms |
|
|
159
204
|
|
|
160
205
|
Pruned, the window limit holds about 35 records whatever the event count; unpruned, each request scans every record kept
|
|
161
206
|
(76 ms at 10k events). The interpreter copies its maps each step (it's the reference, and searches branch from it):
|
|
162
207
|
1.6 s for 10k requests of the caps rule.
|
|
163
208
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
L4 ours takes 5 ms warm; Bend's CUDA build compiled, but its GPU runs hung in our tests (its CPU builds ran: 109 ms
|
|
168
|
-
parallel).
|
|
209
|
+
Measured against Bend and Lean on the same three rules ([`compare/`](compare/README.md)), gdp-wired's proofs are one
|
|
210
|
+
or two lines and check in 25–44 ms, against 74–117 lines of hand proof and 137–161 ms in Bend and 8–30 lines and
|
|
211
|
+
488–571 ms in Lean. Running each tool's proved code over 100,000 requests (whole command, Modal, 2 CPUs):
|
|
169
212
|
|
|
170
|
-
|
|
213
|
+
| | budget | caps | window |
|
|
214
|
+
| --- | --- | --- | --- |
|
|
215
|
+
| gdp-wired | 44 ms | 59 ms | 2.6 s |
|
|
216
|
+
| Bend | 92 s | 75 s | over 300 s |
|
|
217
|
+
| Lean | 8 ms | 205 ms | 8.7 s |
|
|
171
218
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
| | Rule lines | Law lines | Proof lines | Check (median, whole command) |
|
|
176
|
-
| --- | --- | --- | --- | --- |
|
|
177
|
-
| gdp-wired | 5–7 | 1–4 | 1–2 (`proveRule`) | 25–44 ms |
|
|
178
|
-
| Bend 2 | 39–98 (25–50 of them proof evidence in the rule) | 5–26 | 74–117 | 137–161 ms (`--verdict` 167–178 ms) |
|
|
179
|
-
| Lean 4 (by hand) | 4–15 | 1–4 | 8–30 | 488–571 ms |
|
|
180
|
-
|
|
181
|
-
Most of each time is the tool's startup (17, 134 and 453 ms). gdp-wired's kernel trees for the same rules re-check in
|
|
182
|
-
its Lean twin in 1.35 s. What each proves, what each ships at runtime, and where each is the better tool are in
|
|
183
|
-
`compare/README.md`.
|
|
219
|
+
The proved Bend and Lean rules keep lists and scan them; gdp-wired runs maps and a log (unpruned in this window rule).
|
|
220
|
+
Lean, compiled natively, wins where the state stays small. Details in [`docs/comparison.md`](docs/comparison.md).
|
|
184
221
|
|
|
185
222
|
## Layout
|
|
186
223
|
|
|
@@ -188,23 +225,22 @@ its Lean twin in 1.35 s. What each proves, what each ships at runtime, and where
|
|
|
188
225
|
| --- | --- |
|
|
189
226
|
| `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 |
|
|
190
227
|
| `kernel/` | The trusted kernel and the untrusted prover, with soundness fuzz (`kernel.test.ts`, `history.test.ts`) |
|
|
191
|
-
| `lean
|
|
192
|
-
| `
|
|
193
|
-
| `
|
|
194
|
-
| `next/` | Guards synthesized from laws, relational laws (migrations, tenant isolation, through `core/compose.ts`), a ReBAC audit |
|
|
195
|
-
| `sat/` | Bit-vector laws: bit-blasted, solved, LRAT-checked in TypeScript, optionally by Lean |
|
|
196
|
-
| `enum/`, `perf/` | The enumeration, and the benchmarks |
|
|
197
|
-
| `examples/` | The builders at work: `domains/` (92 rules across nine domains, each with laws and broken variants), `harness.ts` (the triple check), `readme.ts`, `budget.ts`, `composition.ts` |
|
|
228
|
+
| `lean-kernel/` | The kernel's Lean twin, its soundness proofs, and the exported trees |
|
|
229
|
+
| `examples/` | The builders at work: `domains/` (92 rules across nine domains, each with laws and broken variants), `harness.ts` (the triple check), `readme.ts`, `budget.ts`, `composition.ts`, `sets.ts`, `quantifiers.ts`, `functions.ts`, `lists.ts`, `sizes.ts`, `liveness.ts`, and `jev-fabric/` (a Bend project's 59 proven laws, ported) |
|
|
230
|
+
| `docs/` | The comparison with Bend and Lean, and the full scope |
|
|
198
231
|
| `compare/` | The same three rules in gdp-wired, Bend and plain Lean, checked and measured |
|
|
199
|
-
| `
|
|
232
|
+
| `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/`) |
|
|
200
233
|
| `modal/` | Everything above as Modal jobs: `suite.json` is the regression suite |
|
|
234
|
+
| `scripts/` | The package build, the smoke test of the packed tarball, the Lean library generator, the diagram |
|
|
201
235
|
| `vendor/handwired` | [monotykamary/handwired](https://github.com/monotykamary/handwired), branch `u32-words`: Cheng Lou's Handwired with exact u32 word circuits and word stages in loop programs |
|
|
202
236
|
| `vendor/gdp-ts` | [rauchg/gdp-ts](https://github.com/rauchg/gdp-ts) |
|
|
203
237
|
|
|
238
|
+
The npm package is built from `core/`, `kernel/` and `lean-kernel/` alone; nothing in it imports `research/`.
|
|
239
|
+
|
|
204
240
|
## Running
|
|
205
241
|
|
|
206
242
|
The npm package is the library: the builders (`gdp-wired`), the runtimes (`gdp-wired/machine`), composition
|
|
207
|
-
(`gdp-wired/compose`), the kernel
|
|
243
|
+
(`gdp-wired/compose`), the Lean export (`gdp-wired/lean`), the kernel
|
|
208
244
|
(`gdp-wired/kernel`), the prover (`gdp-wired/prove`), the derivation checker and solver (`gdp-wired/check`,
|
|
209
245
|
`gdp-wired/solver`). Everything else here (examples, comparisons, the Lean twin, the GPU searches) runs from a clone and
|
|
210
246
|
needs [Bun](https://bun.sh); Lean 4.34.0 for `lean/` and `lean-kernel/`; a WebGPU runtime (Chrome, or Dawn's node
|
|
@@ -218,7 +254,7 @@ bun test core/compile.test.ts core/runtime.test.ts kernel/history.test.ts
|
|
|
218
254
|
cd kernel && bun run-kernel.ts && bun test kernel.test.ts
|
|
219
255
|
```
|
|
220
256
|
|
|
221
|
-
The whole suite (
|
|
257
|
+
The whole suite (122 jobs, about 20 minutes of wall time) runs on [Modal](https://modal.com):
|
|
222
258
|
|
|
223
259
|
```sh
|
|
224
260
|
modal run modal/run.py --jobs modal/suite.json --out runs/modal/suite && sh modal/reap.sh
|
package/dist/core/compose.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type C, type E, type Law, type Machine } from './machine.js';
|
|
2
|
-
import type
|
|
2
|
+
import { type StepLaw } from '../kernel/kernel.js';
|
|
3
3
|
export declare const side: (m: Machine, prefix: "a_" | "b_", c: C) => C;
|
|
4
4
|
export declare function product(a: Machine, b: Machine, opts?: {
|
|
5
5
|
name?: string;
|
|
@@ -15,10 +15,24 @@ export declare function interleave(a: Machine, b: Machine, opts?: {
|
|
|
15
15
|
export declare function refinement(impl: Machine, spec: Machine, opts: {
|
|
16
16
|
relation: Law[];
|
|
17
17
|
witness: Record<string, E>;
|
|
18
|
+
stutter?: C;
|
|
18
19
|
name?: string;
|
|
19
20
|
}): {
|
|
20
21
|
machine: Machine;
|
|
21
22
|
laws: Law[];
|
|
22
23
|
stepLaws: StepLaw[];
|
|
23
24
|
};
|
|
25
|
+
export declare function refinementSides(impl: Machine, spec: Machine): {
|
|
26
|
+
impl: Machine;
|
|
27
|
+
spec: Machine;
|
|
28
|
+
};
|
|
24
29
|
export declare function agree(a: Machine): Law[];
|
|
30
|
+
export declare function steps(m: Machine, k: number): Machine;
|
|
31
|
+
export type Ghost = {
|
|
32
|
+
start: E;
|
|
33
|
+
step: E;
|
|
34
|
+
};
|
|
35
|
+
export declare function ghost(m: Machine, vars: Record<string, Ghost>): Machine;
|
|
36
|
+
export declare const ever: (name: string, cond: C) => Ghost;
|
|
37
|
+
export declare const count: (name: string, cond: C) => Ghost;
|
|
38
|
+
export declare function total(m: Machine, map: string, name: string): Ghost;
|
package/dist/core/compose.js
CHANGED
|
@@ -3,18 +3,24 @@
|
|
|
3
3
|
// is trusted: a proof is about the machine built, and the combinators are tested against runs of their parts.
|
|
4
4
|
// product(a, b) both step on every request (their params and inputs shared by name)
|
|
5
5
|
// interleave(a, b) one of them steps on each request, `turn` picking which
|
|
6
|
-
// refinement(impl, spec, {relation, witness})
|
|
6
|
+
// refinement(impl, spec, {relation, witness, stutter})
|
|
7
7
|
// impl refines spec: the spec, sent the request `witness` names for each of impl's, always allows it,
|
|
8
|
-
// and `relation` holds of the two states at every step (a forward simulation)
|
|
8
|
+
// and `relation` holds of the two states at every step (a forward simulation); where `stutter`
|
|
9
|
+
// holds, impl's step is one the spec doesn't see, and the spec stays
|
|
10
|
+
// ghost(m, vars) state that watches the machine without changing it: what happened before ("ever", "count") and
|
|
11
|
+
// totals across a map's keys ("total"), for laws about whole histories
|
|
12
|
+
// steps(m, k) k steps of m as one: the spec of a refinement that takes several steps for one of impl's
|
|
9
13
|
// Each side's state, maps and flags are renamed a_x and b_x; params and inputs keep their names.
|
|
10
14
|
import { all, any, bit, not, same, unreachable, x } from "./machine.js";
|
|
15
|
+
import { substC, substE } from "../kernel/kernel.js";
|
|
16
|
+
import { nameQuantifiers } from "./dsl.js";
|
|
11
17
|
function rename(e, prefix, m, inputs) {
|
|
12
18
|
switch (e.op) {
|
|
13
19
|
case 'var': return m.params.includes(e.name) ? e : m.inputs.includes(e.name) ? (inputs[e.name] ?? e) : x(`${prefix}${e.name}`);
|
|
14
20
|
case 'const': return e;
|
|
15
21
|
case 'mul': return { ...e, a: rename(e.a, prefix, m, inputs) };
|
|
16
22
|
case 'if': return { op: 'if', c: renameC(e.c, prefix, m, inputs), then: rename(e.then, prefix, m, inputs), else: rename(e.else, prefix, m, inputs) };
|
|
17
|
-
case 'get': return { op: 'get', map: `${prefix}${e.map}`, key: rename(e.key, prefix, m, inputs) };
|
|
23
|
+
case 'get': return { op: 'get', map: m.inputMaps?.includes(e.map) ? e.map : `${prefix}${e.map}`, key: rename(e.key, prefix, m, inputs) };
|
|
18
24
|
case 'add':
|
|
19
25
|
case 'sub':
|
|
20
26
|
case 'min':
|
|
@@ -30,6 +36,8 @@ function renameC(c, prefix, m, inputs) {
|
|
|
30
36
|
return { op: 'not', x: renameC(c.x, prefix, m, inputs) };
|
|
31
37
|
if (c.op === 'and' || c.op === 'or')
|
|
32
38
|
return { op: c.op, xs: c.xs.map(y => renameC(y, prefix, m, inputs)) };
|
|
39
|
+
if (c.op === 'all')
|
|
40
|
+
return { op: 'all', key: `${prefix}${c.key}`, c: renameC(c.c, prefix, m, inputs) }; // the key renamed with its side
|
|
33
41
|
return { ...c, a: rename(c.a, prefix, m, inputs), b: rename(c.b, prefix, m, inputs) };
|
|
34
42
|
}
|
|
35
43
|
// A condition (or law) of one side, read in the composed machine.
|
|
@@ -46,6 +54,8 @@ function renamed(m, p, inputs, stays) {
|
|
|
46
54
|
flags: Object.fromEntries(Object.entries(m.step.flags ?? {}).map(([n, c]) => [`${p}${n}`, renameC(c, p, m, inputs)])),
|
|
47
55
|
state: Object.fromEntries(Object.entries(m.step.state ?? {}).map(([n, e]) => [`${p}${n}`, keep(rename(e, p, m, inputs), x(`${p}${n}`))])),
|
|
48
56
|
maps: Object.fromEntries(Object.entries(m.step.maps ?? {}).map(([n, w]) => {
|
|
57
|
+
if ('each' in w)
|
|
58
|
+
return [`${p}${n}`, { each: `${p}${w.each}`, value: keep(rename(w.value, p, m, inputs), { op: 'get', map: `${p}${n}`, key: x(`${p}${w.each}`) }) }];
|
|
49
59
|
const key = rename(w.key, p, m, inputs);
|
|
50
60
|
return [`${p}${n}`, { key, value: keep(rename(w.value, p, m, inputs), { op: 'get', map: `${p}${n}`, key }) }];
|
|
51
61
|
})),
|
|
@@ -57,6 +67,8 @@ const join = (a, b) => ({
|
|
|
57
67
|
state: { ...a.state, ...b.state }, maps: { ...a.maps, ...b.maps },
|
|
58
68
|
step: { flags: { ...a.step.flags, ...b.step.flags }, state: { ...a.step.state, ...b.step.state }, maps: { ...a.step.maps, ...b.step.maps } },
|
|
59
69
|
});
|
|
70
|
+
// The maps requests carry: shared by name, as inputs are.
|
|
71
|
+
const carriedBy = (...ms) => { const names = [...new Set(ms.flatMap(m => m.inputMaps ?? []))]; return names.length ? { inputMaps: names } : {}; };
|
|
60
72
|
const conj = (cs) => { const xs = cs.filter((c) => c !== undefined); return xs.length ? all(...xs) : undefined; };
|
|
61
73
|
// a and b stepped by the same requests. `bInputs` rewrites what b sees (other tenants' requests as no-ops, say).
|
|
62
74
|
export function product(a, b, opts = {}) {
|
|
@@ -66,7 +78,7 @@ export function product(a, b, opts = {}) {
|
|
|
66
78
|
name: opts.name ?? `${a.name} | ${b.name}`,
|
|
67
79
|
params: [...new Set([...a.params, ...b.params, ...(opts.params ?? [])])],
|
|
68
80
|
inputs: [...new Set([...a.inputs, ...b.inputs, ...(opts.inputs ?? [])])],
|
|
69
|
-
...join(A, B),
|
|
81
|
+
...join(A, B), ...carriedBy(a, b),
|
|
70
82
|
...(assume ? { assume } : {}),
|
|
71
83
|
...(pre ? { pre } : {}),
|
|
72
84
|
};
|
|
@@ -84,7 +96,7 @@ export function interleave(a, b, opts = {}) {
|
|
|
84
96
|
name: opts.name ?? `${a.name} || ${b.name}`,
|
|
85
97
|
params: [...new Set([...a.params, ...b.params])],
|
|
86
98
|
inputs: [...new Set([...a.inputs, ...b.inputs, turn])],
|
|
87
|
-
...join(A, B),
|
|
99
|
+
...join(A, B), ...carriedBy(a, b),
|
|
88
100
|
assume: all(bit(turn), ...(A.assume ? [any(not(isA), A.assume)] : []), ...(B.assume ? [any(not(isB), B.assume)] : [])),
|
|
89
101
|
...(pre ? { pre } : {}),
|
|
90
102
|
};
|
|
@@ -98,13 +110,16 @@ export function interleave(a, b, opts = {}) {
|
|
|
98
110
|
export function refinement(impl, spec, opts) {
|
|
99
111
|
if (!spec.inputs.every(i => i in opts.witness))
|
|
100
112
|
throw new Error(`refinement: name every input of ${spec.name} (${spec.inputs.join(', ')})`);
|
|
101
|
-
|
|
102
|
-
|
|
113
|
+
if ((spec.inputMaps ?? []).some(mp => !(impl.inputMaps ?? []).includes(mp)))
|
|
114
|
+
throw new Error(`refinement: a map ${spec.name}'s requests carry is one ${impl.name}'s carry too (shared by name)`);
|
|
115
|
+
const A = renamed(impl, 'a_', {}), B = renamed(spec, 'b_', opts.witness, opts.stutter);
|
|
116
|
+
// A step the spec doesn't see needs no request it allows.
|
|
117
|
+
const allowedBy = B.assume && opts.stutter ? any(opts.stutter, B.assume) : B.assume;
|
|
103
118
|
const machine = {
|
|
104
119
|
name: opts.name ?? `${impl.name} refines ${spec.name}`,
|
|
105
120
|
params: [...new Set([...impl.params, ...spec.params])],
|
|
106
121
|
inputs: impl.inputs,
|
|
107
|
-
...join(A, B),
|
|
122
|
+
...join(A, B), ...carriedBy(impl, spec),
|
|
108
123
|
...(A.assume ? { assume: A.assume } : {}),
|
|
109
124
|
...(impl.pre ? { pre: impl.pre } : {}),
|
|
110
125
|
};
|
|
@@ -112,6 +127,16 @@ export function refinement(impl, spec, opts) {
|
|
|
112
127
|
const stepLaws = allowedBy ? [{ name: `${spec.name} allows the request it is sent`, then: allowedBy }] : [];
|
|
113
128
|
return { machine, laws, stepLaws };
|
|
114
129
|
}
|
|
130
|
+
// A refinement's two sides as machines in its composite's names (for lean-kernel/Refine.lean, which builds the composite
|
|
131
|
+
// from them): the implementation's parts named a_x, the spec's b_x, the spec's inputs b_<input> (which the composite
|
|
132
|
+
// never reads: the witness stands for them there).
|
|
133
|
+
export function refinementSides(impl, spec) {
|
|
134
|
+
const named = (m, p, inputs, inputNames) => {
|
|
135
|
+
const R = renamed(m, p, inputs);
|
|
136
|
+
return { name: m.name, params: m.params, inputs: inputNames, state: R.state, maps: R.maps, step: R.step, ...(m.inputMaps?.length ? { inputMaps: m.inputMaps } : {}), ...(R.assume ? { assume: R.assume } : {}), ...(m.pre ? { pre: m.pre } : {}) };
|
|
137
|
+
};
|
|
138
|
+
return { impl: named(impl, 'a_', {}, impl.inputs), spec: named(spec, 'b_', Object.fromEntries(spec.inputs.map(i => [i, x(`b_${i}`)])), spec.inputs.map(i => `b_${i}`)) };
|
|
139
|
+
}
|
|
115
140
|
// "a and b agree" in a product of a machine with another of its shape: every scalar equal, every map equal at every key.
|
|
116
141
|
export function agree(a) {
|
|
117
142
|
return [
|
|
@@ -119,3 +144,102 @@ export function agree(a) {
|
|
|
119
144
|
...Object.keys(a.maps ?? {}).map(n => ({ name: `a.${n}[k] = b.${n}[k]`, holds: same({ op: 'get', map: `a_${n}`, key: x('k') }, { op: 'get', map: `b_${n}`, key: x('k') }), forall: 'k' })),
|
|
120
145
|
];
|
|
121
146
|
}
|
|
147
|
+
// --- several steps as one --------------------------------------------------------------------------------------------
|
|
148
|
+
// k steps of m in a row as one step: the j-th request read as <input>@s<j> (j from 1), each step skipped where its input
|
|
149
|
+
// skip@s<j> is 1. Each step reads the state the one before it left (its flags decided there, every map read over the
|
|
150
|
+
// writes before), so several writes to one map become one write at every key, its key compared with theirs (which the
|
|
151
|
+
// runtimes decide on breakpoints). Its assumption: each step not skipped is one m allows. As the spec of a refinement,
|
|
152
|
+
// the witness names each step's request and when it is skipped.
|
|
153
|
+
export function steps(m, k) {
|
|
154
|
+
if (!Number.isInteger(k) || k < 1)
|
|
155
|
+
throw new Error('steps: k is a whole number at least 1');
|
|
156
|
+
if (m.history || m.inputMaps?.length)
|
|
157
|
+
throw new Error(`steps: ${m.name} has a history or maps a request carries (not taken here)`);
|
|
158
|
+
const at = (name, j) => `${name}@s${j}`;
|
|
159
|
+
let vars = Object.fromEntries(Object.keys(m.state).map(n => [n, x(n)]));
|
|
160
|
+
let maps = Object.fromEntries(Object.keys(m.maps ?? {}).map(map => [map, (key) => ({ op: 'get', map, key })]));
|
|
161
|
+
const allowed = [], written = new Set();
|
|
162
|
+
for (let j = 1; j <= k; j++) {
|
|
163
|
+
const skip = same(x(at('skip', j)), { op: 'const', value: 1 });
|
|
164
|
+
const sub = { vars: { ...vars, ...Object.fromEntries(m.inputs.map(i => [i, x(at(i, j))])) }, maps, flags: {} };
|
|
165
|
+
for (const [f, c] of Object.entries(m.step.flags ?? {}))
|
|
166
|
+
sub.flags[f] = substC(c, sub);
|
|
167
|
+
if (m.assume)
|
|
168
|
+
allowed.push(any(skip, substC(m.assume, sub)));
|
|
169
|
+
const kept = (now, old) => ({ op: 'if', c: skip, then: old, else: now });
|
|
170
|
+
const nextVars = { ...vars };
|
|
171
|
+
for (const [n, e] of Object.entries(m.step.state ?? {}))
|
|
172
|
+
nextVars[n] = kept(substE(e, sub), vars[n]);
|
|
173
|
+
const nextMaps = { ...maps };
|
|
174
|
+
for (const [map, w] of Object.entries(m.step.maps ?? {})) {
|
|
175
|
+
written.add(map);
|
|
176
|
+
const old = maps[map];
|
|
177
|
+
if ('each' in w)
|
|
178
|
+
nextMaps[map] = (key) => kept(substE(w.value, { ...sub, vars: { ...sub.vars, [w.each]: key } }), old(key));
|
|
179
|
+
else {
|
|
180
|
+
const wk = substE(w.key, sub), wv = substE(w.value, sub);
|
|
181
|
+
nextMaps[map] = (key) => kept({ op: 'if', c: same(key, wk), then: wv, else: old(key) }, old(key));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
vars = nextVars;
|
|
185
|
+
maps = nextMaps;
|
|
186
|
+
}
|
|
187
|
+
const each = 'k$steps';
|
|
188
|
+
return nameQuantifiers({
|
|
189
|
+
name: `${m.name}, ${k} steps at once`, params: m.params,
|
|
190
|
+
inputs: Array.from({ length: k }, (_, i) => [...m.inputs.map(n => at(n, i + 1)), at('skip', i + 1)]).flat(),
|
|
191
|
+
state: m.state, ...(m.maps ? { maps: m.maps } : {}),
|
|
192
|
+
step: {
|
|
193
|
+
state: Object.fromEntries(Object.keys(m.step.state ?? {}).map(n => [n, vars[n]])),
|
|
194
|
+
...(written.size ? { maps: Object.fromEntries([...written].map(map => [map, { each, value: maps[map](x(each)) }])) } : {}),
|
|
195
|
+
},
|
|
196
|
+
assume: all(...Array.from({ length: k }, (_, i) => bit(at('skip', i + 1))), ...allowed),
|
|
197
|
+
...(m.pre ? { pre: m.pre } : {}),
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
export function ghost(m, vars) {
|
|
201
|
+
const taken = [...m.params, ...m.inputs, ...Object.keys(m.state), ...Object.keys(m.maps ?? {}), ...Object.keys(m.step.flags ?? {})];
|
|
202
|
+
for (const name of Object.keys(vars))
|
|
203
|
+
if (taken.includes(name))
|
|
204
|
+
throw new Error(`ghost: ${name} is already a name of ${m.name}`);
|
|
205
|
+
if (m.history)
|
|
206
|
+
throw new Error(`${m.name}: ghosts go on a machine without a history (its sums are ghosts of that kind already)`);
|
|
207
|
+
return {
|
|
208
|
+
...m,
|
|
209
|
+
state: { ...m.state, ...Object.fromEntries(Object.entries(vars).map(([n, g]) => [n, g.start])) },
|
|
210
|
+
step: { ...m.step, state: { ...m.step.state, ...Object.fromEntries(Object.entries(vars).map(([n, g]) => [n, g.step])) } },
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
// Whether `cond` has held at some step so far (read where the step reads: the state before, the request, the flags).
|
|
214
|
+
export const ever = (name, cond) => ({ start: { op: 'const', value: 0 }, step: { op: 'if', c: cond, then: { op: 'const', value: 1 }, else: x(name) } });
|
|
215
|
+
// How many steps so far `cond` held at.
|
|
216
|
+
export const count = (name, cond) => ({ start: { op: 'const', value: 0 }, step: { op: 'add', a: x(name), b: { op: 'if', c: cond, then: { op: 'const', value: 1 }, else: { op: 'const', value: 0 } } } });
|
|
217
|
+
// The total of a map over every key: the map must start at 0 everywhere (so finitely many keys are nonzero), and each
|
|
218
|
+
// step writes one key, so the total moves by the value written less the value it replaces. That difference is pushed
|
|
219
|
+
// through the written term where its shape gives it (old + d adds d; old ∸ d takes min(d, old); an if moves each way
|
|
220
|
+
// by its branch's), so a proof needs no fact about the total beyond the law it is about; any other term falls back to
|
|
221
|
+
// total ∸ old + new (exact, since the total is at least any one cell).
|
|
222
|
+
export function total(m, map, name) {
|
|
223
|
+
if ((m.maps ?? {})[map] !== 0)
|
|
224
|
+
throw new Error(`total: ${map} must start at 0 at every key`);
|
|
225
|
+
const w = m.step.maps?.[map];
|
|
226
|
+
if (!w)
|
|
227
|
+
return { start: { op: 'const', value: 0 }, step: x(name) };
|
|
228
|
+
if ('each' in w)
|
|
229
|
+
throw new Error(`total: ${map} is written at every key at once, which no one step's difference describes`);
|
|
230
|
+
const old = { op: 'get', map, key: w.key }, t = x(name), same = (a) => JSON.stringify(a) === JSON.stringify(old);
|
|
231
|
+
const moved = (e) => {
|
|
232
|
+
if (same(e))
|
|
233
|
+
return t;
|
|
234
|
+
if (e.op === 'add' && same(e.a))
|
|
235
|
+
return { op: 'add', a: t, b: e.b };
|
|
236
|
+
if (e.op === 'add' && same(e.b))
|
|
237
|
+
return { op: 'add', a: t, b: e.a };
|
|
238
|
+
if (e.op === 'sub' && same(e.a))
|
|
239
|
+
return { op: 'sub', a: t, b: { op: 'min', a: e.b, b: old } };
|
|
240
|
+
if (e.op === 'if')
|
|
241
|
+
return { op: 'if', c: e.c, then: moved(e.then), else: moved(e.else) };
|
|
242
|
+
return { op: 'add', a: { op: 'sub', a: t, b: old }, b: e };
|
|
243
|
+
};
|
|
244
|
+
return { start: { op: 'const', value: 0 }, step: moved(w.value) };
|
|
245
|
+
}
|