gdp-wired 0.1.0 → 0.3.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 CHANGED
@@ -2,12 +2,13 @@
2
2
 
3
3
  # gdp-wired
4
4
 
5
- Laws about stateful rules (budgets, caps, rate limits, access policies), proven in TypeScript by a small trusted
6
- kernel, with an untrusted prover that writes the proofs, an optional Lean twin that checks the kernel itself, and GPU
7
- searches that find counterexamples or the invariants a proof needs.
5
+ An automatic prover for stateful rules: budgets, caps, rate limits, workflows, access policies. Write the rule and the
6
+ laws it must keep; gdp-wired proves them with no proof to write, shows a real run that breaks them when they're false
7
+ (searched on the GPU), checks every proof again in Lean, and ships the rule as JavaScript.
8
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.
9
+ Proofs are checked by a small trusted kernel; the prover that writes them is untrusted, and the kernel's Lean twin is
10
+ proven sound. On the three rules in [`compare/`](compare/README.md), the proof is a one- or two-line call and checks in
11
+ 25–44 ms, against 74–117 lines of hand proof and 137–161 ms in Bend, and 8–30 lines and 488–571 ms in Lean.
11
12
 
12
13
  ```sh
13
14
  npm install gdp-wired # or bun add gdp-wired; plain JavaScript with types, Node 20+, Bun or a browser
@@ -33,44 +34,65 @@ Forget the cap (`s.plus(c)`) and the kernel refuses the proof; a search from the
33
34
  and `examples/domains/` holds 92 rules from nine domains (finance, access control, public services, operations,
34
35
  protocols, commerce, supply chains, games, education), each checked three ways.
35
36
 
36
- ## Scope: where it replaces Bend, and where it doesn't
37
+ ## Scope: what it proves by itself, and where the wall is
37
38
 
38
- gdp-wired is meant to replace Bend where it matters in practice, not to be a language: laws about **stateful rules whose
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:
39
+ gdp-wired is an automatic prover, not a language: laws about **stateful rules** (counters, totals, maps, sets and lists
40
+ of names, logs summed over time, steps in a row, runs that go on forever), proven without a proof to write, with the
41
+ rule shipped as code. With Lean beside it, it says what Bend says; where a proof needs more than its kernel decides, the
42
+ proof is Lean's, as a fact about a function the rule reads:
41
43
 
42
44
  | It expresses and proves | It does not (today) |
43
45
  | --- | --- |
44
- | Step machines over naturals: scalars, maps from naturals to naturals (one write per map per step), named step conditions, preconditions on params, assumptions per request | Lists, sets or strings as values: membership, subsets, equality of lists of names |
45
- | Invariants of the state and params, laws about every key of a map; step laws relating one step's before and after (`s'`, `M'[k]`: "only increases", "stays retired") | Laws about runs of two or more steps ("a refund never follows a release" needs a state flag), or orderings beyond progress |
46
- | Laws about one key of a map at a time, or about scalars | Totals across a map's keys ("the sum of every client's holdings equals in-use") |
47
- | Histories: sums and queries over a log of requests, relations between them proven one record at a time, pruning | Unbounded recursion over data, quantifiers over sequences other than a machine's own history |
48
- | Decisions over facts: flags, one-of choices, optional facts, bounded naturals (`match` covers every case) | Datatypes beyond those encodings; division, modulo, reals |
49
- | Loops that halt by a rank or a tuple of ranks; folds over binary trees; progress laws ("some request makes this happen") | Arbitrary algorithms: there is no general recursion to prove things about |
50
- | Arithmetic: `+`, truncated `−`, `×` by a constant, `min`, `max`, `if`, products of two terms (proven by the kernel, less well by the prover) | Generated code for decisions alone (`compileJs` emits machines; a predicate emitter is planned) |
51
-
52
- Keep Bend where the specification is about **lists or sets of names** (resource scopes, capability subsets, normalization
53
- that must refine its input), where a proof needs **induction over data structures**, or where the shipped code must be
54
- the compiled proved source of an arbitrary algorithm. Use gdp-wired where the rule is a budget, a cap, a window, a
55
- workflow or a policy decision: it proves those without proof files, finds counterexample runs when they're false, and
56
- checks its own kernel in Lean.
46
+ | Step machines over naturals: scalars, maps from naturals to naturals (a write at one key, or at every key, per map per step), named step conditions, preconditions on params, assumptions per request | Strings, and lists as values (a list in a map's cell, lists of lists) |
47
+ | Lists of names (`.lists(...)`): positions and a length, `push`, `pop`, `set`, and `insertAt`/`removeAt` that keep the order (positions shift: a write at every key reading its key at k ∸ 1 or k + 1, which the runtimes decide on breakpoints); laws about every two positions or more (`forall((i, j) => ...)`: "strictly increasing", so no name twice; `forall((i, j, k) => ...)`: "any three cells repeat a value"), and step laws that an insert puts its name in, keeps every name, and a removal takes it out (`examples/lists.ts`, a scope kept in normal form) | |
48
+ | Sets of names, as maps of 0/1 cells over names interned to numbers: a request carrying whole sets (`.carries`), conditions about every key (`everyKey`, `has`, `subsetOf`, `disjointFrom`, `sameAs`) anywhere in a step (each named as a flag of its own) and inside laws (pulled out as the law's key), the key read alone or plus or less a term and compared (`k < n`, `M[k + 1]`), writes at every key in one step (`each`); laws, step laws and k-step laws over them, each also a Lean theorem; a set's size over a universe of names (`within(n)`, `sizeBelow(n)`: the sum of its cells, exact for names interned below n; `examples/sizes.ts`) | A set's size over unbounded names (Lean's maps have no finite support, so no sum over every key) |
49
+ | Some key (`someKey`): "something was added, so some name is in", proven through a witness, a term naming the key, that the prover finds among the state, the params, the law's key and the request (or the one the law names) | A witness no term of the state names (which a ghost can keep: the last key added, say) |
50
+ | Functions from outside the rule (`fn`, and `divBy`, `modBy` for division and remainder by a constant): read like maps, known to the kernel only by their facts, which every goal may assume; the Lean export states the facts as the hypothesis they are, and proves them in Lean where the function has a Lean definition (`divBy` and `modBy` by `omega`); the GPU search and the GPU invariant synthesis tabulate each function over the keys they can read it at | Facts proven by gdp-wired (they are Lean's to prove) |
51
+ | Invariants of the state and params, laws about every key of a map; step laws relating one step's before and after (`s'`, `M'[k]`: "only increases", "stays retired"), and k-step laws about k steps in a row ("two grants on one day add two", up to 16 steps); orderings at any distance through ghost state ("a refund never follows a release": `ghost` with `ever`, `count`); liveness under fairness (`leadsTo`: "every ticket issued is served", in every run that sends each helpful request infinitely often, by a rank that drops: one natural or several compared in order; several helpful requests, each in the region where it helps; a request naming some inputs and leaving the rest, and the sets it carries, free), for machines with maps, with sets, and with a history (its queries free, within its relations); a Lean theorem over runs, and under weak fairness too (each helpful request taken whenever it stays allowed, proven allowed in its region: its free inputs at any value, or at a witness the exporter finds among 0, the state and the params, a history's query among them; the sets it carries empty) | |
52
+ | Composition: rules side by side (`product`), taking turns (`interleave`), and one rule refining another (`refinement`: a forward simulation, the spec allowing every request it is sent, steps the spec doesn't see as `stutter`, and `steps(spec, k)` where the spec takes several steps for one of the implementation's: a transfer at once refines a withdrawal then a deposit), each built as a plain machine so every law above applies; what a refinement means is a Lean theorem (`refines`: every reachable state of the implementation beside a reachable state of the spec, related by every law), for machines with maps and flags and for machines with sets (`srefines`: the sets a request carries shared by name, writes at every key, flags at every key) | Refinement where the number of spec steps depends on the state (beyond a bound k with skips) |
53
+ | Laws about one key of a map at a time, or about scalars; totals across a map's keys through a ghost total ("in use equals what the clients hold") | Sums across a map's keys other than through a ghost total |
54
+ | Histories: sums and queries over a log of requests, relations between them proven one record at a time, pruning | Quantifiers over sequences other than a machine's own history |
55
+ | Decisions over facts: flags, one-of choices, optional facts, bounded naturals (`match` covers every case) | Datatypes beyond those encodings (except as a function's arguments: a list as a number a Lean function decodes); reals |
56
+ | Loops that halt by a rank or a tuple of ranks; folds over binary trees; progress laws ("some request makes this happen"); recursion defined in Lean, entered as a function with its facts | Induction the prover would have to invent (the wall below) |
57
+ | Arithmetic: `+`, truncated `−`, `×` by a constant, `min`, `max`, `if`, products of two terms (proven by the kernel, less well by the prover), division and remainder by a constant (`divBy`, `modBy`) | Generated code for decisions alone (`compileJs` emits machines; a predicate emitter is planned) |
58
+
59
+ **The wall.** What stays outside is one of two things, and neither is a gap the next construct closes:
60
+
61
+ - **Induction the prover would have to invent.** A property of a recursive structure you define (a tree, every prefix
62
+ of a list of lists) whose proof needs a lemma nobody stated. No automatic prover proves all of these: arithmetic with
63
+ induction is undecidable. Here the proof is written in Lean, and gdp-wired uses it: define the function in Lean,
64
+ prove its facts there, and give the rule `fn(arity, js, {facts, lean})`. The steps, the state, the maps and the laws
65
+ around it stay automatic, and the Lean export takes the facts as the hypothesis they are.
66
+ - **An arbitrary algorithm shipped as its proved source.** gdp-wired ships the rule as JavaScript and runs a function's
67
+ own JavaScript beside its Lean definition (held to it by tests, not by a compiler). Bend compiles the proved source
68
+ itself.
69
+
70
+ The engineering list is closed. Each other "does not" above has a route already: a ghost (a witness no term names, a
71
+ sum across a map's keys), a bound with skips (spec steps that vary), an encoding (strings, datatypes as numbers a Lean
72
+ function decodes), or Lean itself (a function you define and prove there). Use gdp-wired where the rule is a budget, a cap, a
73
+ window, a workflow, a policy decision, an authority over sets of names or a list kept in normal form
74
+ (`examples/sets.ts`, `examples/lists.ts`): it proves those without proof files, finds counterexample runs when they're
75
+ false, and checks its own kernel in Lean. Keep Bend (or Lean) where the proof is the induction above.
57
76
 
58
77
  For a sense of the split: in one real codebase with 98 Bend laws and about 1,600 lines of hand proofs, the policy
59
78
  decisions, the compare-and-swap storage plans and the state/provider plans fit gdp-wired as it is (with names interned
60
79
  to numbers), and gdp-wired can prove more about storage than its Bend proofs state (no revision reuse across keys); the
61
- lifecycle machine's before/after laws are step laws (as in `release lifecycle` in `examples/`); the resource and authority checks, built on lists of names as
62
- sets, stay in Bend. `compare/` puts three rules side by side in gdp-wired, Bend and plain Lean, with line counts and
80
+ lifecycle machine's before/after laws are step laws (as in `release lifecycle` in `examples/`); the authority checks,
81
+ built on lists of names used as sets, fit as sets of names (all ten of its authority laws proven automatically, and the
82
+ rule agrees with the compiled Bend on all 7,654 outcomes over four names); the resource checks, which normalize scopes
83
+ that are lists of paths (lists of lists of names) and compare their prefixes, are induction over nested lists: the wall,
84
+ so they stay in Bend, or go to Lean as functions with proven facts. `compare/` puts three rules side by side in gdp-wired, Bend and plain Lean, with line counts and
63
85
  check times.
64
86
 
65
87
  ## What's trusted
66
88
 
67
89
  | Part | Trusted? | What it does |
68
90
  | --- | --- | --- |
69
- | `kernel/kernel.ts`, `core/check.ts` | **Yes** | Turns a machine and its laws into goals (the induction rules: step machines with maps and flags, step laws, loops with ranks, progress, folds over trees, histories) and checks a proof tree for each: splits on linear atoms, a Fourier–Motzkin derivation at each leaf. Refuses names a machine doesn't declare |
91
+ | `kernel/kernel.ts`, `core/check.ts` | **Yes** | Turns a machine and its laws into goals and checks a proof tree for each: splits on linear atoms, a Fourier–Motzkin derivation at each leaf. Every rule (step machines with maps and flags, step laws and k-step laws, loops with ranks, progress, folds over trees) is a few moves of one small algebra of runs: conditions holding where a run is, a step taken from there, a conclusion read at the end or across the run. Histories become plain machines first. Refuses names a machine doesn't declare |
70
92
  | `kernel/prove.ts` | No | Grows the trees. If it's wrong, the kernel rejects the tree |
71
- | `lean-kernel/` | Optional | The kernel's twin in Lean 4.34.0, each rule proven sound (`Kern.sound`, `machine_sound`, `mmachine_sound`, `fmmachine_sound`, `loop_sound`, `fold_sound`, `progress_sound`, `steplaw_sound`, `hmachine_sound`), axioms `propext`, `Classical.choice`, `Quot.sound` only. The exporter sends the kernel's real trees; tampered ones compute to false |
72
- | `search/`, `synth/`, `hw/` | No | GPU work: counterexample search over every short history, and invariant/rank synthesis. Every hit is replayed by the interpreter or proven by the kernel before it counts |
73
- | `core/dsl.ts`, the runtimes | No | The builders compile to the kernel's plain values; the interpreter and generated JavaScript are held to the reference evaluator by the conformance test |
93
+ | `lean-kernel/` | Optional | The kernel's twin in Lean 4.34.0, each rule proven sound (`Kern.sound`, `machine_sound`, `mmachine_sound`, `fmmachine_sound`, `loop_sound`, `fold_sound`, `progress_sound`, `steplaw_sound`, `kstep_sound`, `hmachine_sound`, `smachine_sound` with functions' facts and laws about several keys, `skstep_sound`, `leads_sound`, `leads_wf_sound` and `sleads_sound` for liveness over runs), what a ghost means (`ghost_laws`, `ghost_run`, `ever_meaning`) and what a refinement means (`refines`, `srefines`), axioms `propext`, `Classical.choice`, `Quot.sound` only. The exporter sends the kernel's real trees; tampered ones compute to false |
94
+ | `search/`, `synth/`, `hw/` | No | GPU work: counterexample search over every short history (for laws, step laws and k-step laws, machines with a log of requests too), and invariant/rank synthesis. Every hit is replayed by the interpreter or proven by the kernel before it counts |
95
+ | `core/dsl.ts`, `core/compose.ts`, the runtimes | No | The builders and combinators compile to the kernel's plain values; the interpreter and generated JavaScript are held to the reference evaluator by the conformance test, and the combinators to runs of their parts |
74
96
 
75
97
  How the untrusted parts are held to the trusted ones, automatically:
76
98
 
@@ -82,10 +104,13 @@ How the untrusted parts are held to the trusted ones, automatically:
82
104
  evaluator, and the same cases through Lean's own `evalE` (`lean-kernel/conformance.ts`).
83
105
  - **Examples** (`examples/`): every law proven by the kernel and re-checked by Lean, every rule run at random through
84
106
  both runtimes, and every broken variant broken by a real run (a refused proof alone could be the prover's weakness).
107
+ - **One description, three readings**: a law or step law is proven by the kernel, read on one run by `core/watch.ts` (the
108
+ examples' runs, every replay), and searched for a breaking run by the Handwired search; `hw/check-steps.ts` holds the
109
+ search to the watcher on whole spaces, and all three to each other on random machines and laws.
85
110
  - **Tampering and mutation** (`kernel/tamper.test.ts`, `kernel/refusals.test.ts`, `kernel/mutants.ts`): real proofs
86
111
  broken structurally or offered for weakened goals that have a model must be rejected, every refusal the kernel makes is
87
112
  exercised, and every one-operator mutation of `kernel.ts` and `check.ts` is run against the tests to find what they
88
- don't notice: 222 of 241 mutants are caught, and the 19 left are equivalent (`kernel/MUTANTS.md`).
113
+ don't notice: 239 of 261 mutants are caught, and the 22 left are equivalent (`kernel/MUTANTS.md`).
89
114
  - **Fuzz**: the kernel never accepts a refutation of something with a model (`kernel/kernel.test.ts`), and laws proven
90
115
  over histories hold on every run (`kernel/history.test.ts`).
91
116
 
@@ -109,7 +134,9 @@ const dayCap = caps.law('day cap', ({Day, d}) => forall(k => Day.at(k).atMost(d)
109
134
  // Step laws: about one step, for every request the rule allows; `after` reads the state and maps after it.
110
135
  const onlyUp = caps.stepLaw('no count goes down', ({Day}, after) => forall(k => Day.at(k).atMost(after.Day.at(k))))
111
136
  const once = caps.stepLaw('a grant adds one to its day', ({day, Day}, after) => after.Day.at(day).eq(Day.at(day).plus(1)), ({granted}) => granted)
112
- proveSteps(caps, [dayCap], [onlyUp, once]) // {proven: true, checked: true}
137
+ // k-step laws: about k steps in a row; `requests` holds each step's request, `when` narrows the j-th step's.
138
+ const twice = caps.stepsLaw('two grants on one day add two', 2, ({day, Day}, after, [, next]) => next!.day.eq(day).implies(after.Day.at(day).eq(Day.at(day).plus(2))), ({granted}) => granted)
139
+ proveSteps(caps, [dayCap], [onlyUp, once, twice]) // {proven: true, checked: true}
113
140
 
114
141
  // Histories: a log of requests, a sum over it (state), a query over it at the request's time (an input the runtime
115
142
  // computes), and a relation between them proven one record at a time.
@@ -135,7 +162,30 @@ const failsClosed = trial.decide('a missing fact means no trial', ({spent}) => s
135
162
  machine reads.
136
163
  - **Arithmetic** over the naturals: `plus`, `minus` (truncated), `times(k)`, `mul` (a product of two terms), `min`,
137
164
  `max`, `when(cond, a, b)`, `count(cond)`; conditions `lt`, `atMost`, `eq`, `and`, `or`, `not`, `implies`.
165
+ - **Sets and quantifiers**: `.carries(...)` for sets a request brings, `everyKey(k => ...)` (and `has`, `subsetOf`,
166
+ `disjointFrom`, `sameAs`) anywhere in a step or inside a law, `M.each(k => ...)` to write every key, and
167
+ `someKey(k => ..., witness?)` for "some key where", proven through a witness the prover finds or the one given.
168
+ - **Functions** (`.fns({perMinute: divBy(60)})`, or `fn(arity, js, {facts, lean})` for your own): read like params that
169
+ take arguments, `perMinute(t)`; the kernel knows each only by its facts (`.facts(...)`, and a library function's
170
+ own), and the Lean export takes them as the hypothesis they are, proving them where the function has a Lean
171
+ definition. A function defined and proven in Lean is how a rule reaches past what the kernel decides: `examples/functions.ts`.
172
+ - **Liveness** (`rule.leadsTo(name, {from, to, rank, helpful})`, `proveLeadsTo`): whenever `from` holds, `to` holds later in
173
+ every run that takes each helpful request infinitely often; proven by a rank no step raises while waiting and each
174
+ helpful request lowers in its region. `rank` may return several terms (compared in order) and `helpful` may be a list
175
+ of `{when, request}`; a request names only the inputs it fixes (`examples/liveness.ts`: batches, a history with a
176
+ query, a machine with sets).
138
177
  - **Loops** (`loop(...)`): state, `while`, `body`, a `rank` (or a tuple of ranks) that must drop each turn.
178
+ - **Lean** (`gdp-wired/lean`): `toLean([{id, machine, laws, stepLaws}, ...])` writes the proofs as a Lean file, and
179
+ `leanProject(file)` a Lake project with the twin, so `lake build` checks them. Each law comes as the twin's theorem and
180
+ as a plain statement over named variables, e.g. `theorem budget.law0 (h : FMReach budget_machine ρ μ) : ρ «s» ≤
181
+ (ρ «b» + ρ «m»)`, for a Lean development to cite.
182
+ - **Composition** (`gdp-wired/compose`): `product(a, b)`, `interleave(a, b)`, `refinement(impl, spec, {relation, witness, stutter})`,
183
+ `steps(spec, k)` (k spec steps as one, each skippable) and `ghost(m, {name: ever(...) | count(...) | total(m, map, name)})`
184
+ build plain machines from machines (each side's state named `a_x`, `b_x`). A refinement's laws are the relation, proven
185
+ by induction, and its step law says the spec allows the request `witness` sends it at each of the implementation's:
186
+ `examples/composition.ts` proves a meter that counts down refines one that counts up, and refuses two broken ones;
187
+ a two-phase meter refines it with stuttering; an escrow never refunds after a release (at any distance); and the
188
+ connection pool's in-use count is the total of every client's holdings.
139
189
  - **Running it**: `rule.interpreter()` is persistent (searches branch from any state); `rule.compiled()` is what an app
140
190
  ships: maps and the log updated in place, so a step costs the same however long the history. Both refuse params and
141
191
  inputs that aren't naturals below 2^53 and any value past 2^53 − 1, where the proofs over the naturals stop describing
@@ -179,15 +229,15 @@ its Lean twin in 1.35 s. What each proves, what each ships at runtime, and where
179
229
 
180
230
  | Path | What |
181
231
  | --- | --- |
182
- | `core/` | Machines, the interpreter and JS compiler, the linear solver and its checker, Houdini invariants, models, the first Lean export |
232
+ | `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 |
183
233
  | `kernel/` | The trusted kernel and the untrusted prover, with soundness fuzz (`kernel.test.ts`, `history.test.ts`) |
184
234
  | `lean/`, `lean-kernel/` | Lean: the derivation checker `Cert.lean`, and the kernel's twin with its exported trees |
185
235
  | `search/`, `hw/`, `modal-gpu/` | Counterexample search on the GPU: a hand-written WGSL kernel, and the same through the Handwired fork's word stages |
186
236
  | `synth/` | Invariants and ranks found on the GPU, proven by the kernel |
187
- | `next/` | Guards synthesized from laws, relational laws (migrations, tenant isolation), a ReBAC audit |
237
+ | `next/` | Guards synthesized from laws, relational laws (migrations, tenant isolation, through `core/compose.ts`), a ReBAC audit |
188
238
  | `sat/` | Bit-vector laws: bit-blasted, solved, LRAT-checked in TypeScript, optionally by Lean |
189
239
  | `enum/`, `perf/` | The enumeration, and the benchmarks |
190
- | `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` |
240
+ | `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` |
191
241
  | `compare/` | The same three rules in gdp-wired, Bend and plain Lean, checked and measured |
192
242
  | `spec.ts`, `gdp.ts`, `circuit.ts`, `laws.ts` | The audit path: gdp-ts predicates written once, as trusted provers per request and as a Handwired circuit that decides every (user, project) pair at once |
193
243
  | `modal/` | Everything above as Modal jobs: `suite.json` is the regression suite |
@@ -196,7 +246,8 @@ its Lean twin in 1.35 s. What each proves, what each ships at runtime, and where
196
246
 
197
247
  ## Running
198
248
 
199
- The npm package is the library: the builders (`gdp-wired`), the runtimes (`gdp-wired/machine`), the kernel
249
+ The npm package is the library: the builders (`gdp-wired`), the runtimes (`gdp-wired/machine`), composition
250
+ (`gdp-wired/compose`), the Lean export (`gdp-wired/lean`), the kernel
200
251
  (`gdp-wired/kernel`), the prover (`gdp-wired/prove`), the derivation checker and solver (`gdp-wired/check`,
201
252
  `gdp-wired/solver`). Everything else here (examples, comparisons, the Lean twin, the GPU searches) runs from a clone and
202
253
  needs [Bun](https://bun.sh); Lean 4.34.0 for `lean/` and `lean-kernel/`; a WebGPU runtime (Chrome, or Dawn's node
@@ -210,7 +261,7 @@ bun test core/compile.test.ts core/runtime.test.ts kernel/history.test.ts
210
261
  cd kernel && bun run-kernel.ts && bun test kernel.test.ts
211
262
  ```
212
263
 
213
- The whole suite (107 jobs, about 15 minutes of wall time) runs on [Modal](https://modal.com):
264
+ The whole suite (122 jobs, about 20 minutes of wall time) runs on [Modal](https://modal.com):
214
265
 
215
266
  ```sh
216
267
  modal run modal/run.py --jobs modal/suite.json --out runs/modal/suite && sh modal/reap.sh
@@ -0,0 +1,38 @@
1
+ import { type C, type E, type Law, type Machine } from './machine.js';
2
+ import { type StepLaw } from '../kernel/kernel.js';
3
+ export declare const side: (m: Machine, prefix: "a_" | "b_", c: C) => C;
4
+ export declare function product(a: Machine, b: Machine, opts?: {
5
+ name?: string;
6
+ params?: string[];
7
+ inputs?: string[];
8
+ bInputs?: Record<string, E>;
9
+ assume?: C;
10
+ }): Machine;
11
+ export declare function interleave(a: Machine, b: Machine, opts?: {
12
+ name?: string;
13
+ turn?: string;
14
+ }): Machine;
15
+ export declare function refinement(impl: Machine, spec: Machine, opts: {
16
+ relation: Law[];
17
+ witness: Record<string, E>;
18
+ stutter?: C;
19
+ name?: string;
20
+ }): {
21
+ machine: Machine;
22
+ laws: Law[];
23
+ stepLaws: StepLaw[];
24
+ };
25
+ export declare function refinementSides(impl: Machine, spec: Machine): {
26
+ impl: Machine;
27
+ spec: Machine;
28
+ };
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;
@@ -0,0 +1,245 @@
1
+ // Composition: machines built from machines. Each combinator returns a plain Machine, so every capability (laws by
2
+ // induction, step laws, k-step laws, progress, the runtimes, the Lean twin) applies to what it builds, and nothing here
3
+ // is trusted: a proof is about the machine built, and the combinators are tested against runs of their parts.
4
+ // product(a, b) both step on every request (their params and inputs shared by name)
5
+ // interleave(a, b) one of them steps on each request, `turn` picking which
6
+ // refinement(impl, spec, {relation, witness, stutter})
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); 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
13
+ // Each side's state, maps and flags are renamed a_x and b_x; params and inputs keep their names.
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";
17
+ function rename(e, prefix, m, inputs) {
18
+ switch (e.op) {
19
+ case 'var': return m.params.includes(e.name) ? e : m.inputs.includes(e.name) ? (inputs[e.name] ?? e) : x(`${prefix}${e.name}`);
20
+ case 'const': return e;
21
+ case 'mul': return { ...e, a: rename(e.a, prefix, m, inputs) };
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) };
23
+ case 'get': return { op: 'get', map: m.inputMaps?.includes(e.map) ? e.map : `${prefix}${e.map}`, key: rename(e.key, prefix, m, inputs) };
24
+ case 'add':
25
+ case 'sub':
26
+ case 'min':
27
+ case 'max':
28
+ case 'prod': return { ...e, a: rename(e.a, prefix, m, inputs), b: rename(e.b, prefix, m, inputs) };
29
+ default: return unreachable(e);
30
+ }
31
+ }
32
+ function renameC(c, prefix, m, inputs) {
33
+ if (c.op === 'flag')
34
+ return { op: 'flag', name: `${prefix}${c.name}` };
35
+ if (c.op === 'not')
36
+ return { op: 'not', x: renameC(c.x, prefix, m, inputs) };
37
+ if (c.op === 'and' || c.op === 'or')
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
41
+ return { ...c, a: rename(c.a, prefix, m, inputs), b: rename(c.b, prefix, m, inputs) };
42
+ }
43
+ // A condition (or law) of one side, read in the composed machine.
44
+ export const side = (m, prefix, c) => renameC(c, prefix, m, {});
45
+ // One side renamed; `stays` (if given) is when its state doesn't move: each update and write keeps the old value then.
46
+ function renamed(m, p, inputs, stays) {
47
+ if (m.history)
48
+ throw new Error(`${m.name}: a machine with a history is composed as its plain machine (historyMachine)`);
49
+ const keep = (now, old) => (stays ? { op: 'if', c: stays, then: old, else: now } : now);
50
+ return {
51
+ state: Object.fromEntries(Object.entries(m.state).map(([n, e]) => [`${p}${n}`, rename(e, p, m, {})])),
52
+ maps: Object.fromEntries(Object.entries(m.maps ?? {}).map(([n, v]) => [`${p}${n}`, v])),
53
+ step: {
54
+ flags: Object.fromEntries(Object.entries(m.step.flags ?? {}).map(([n, c]) => [`${p}${n}`, renameC(c, p, m, inputs)])),
55
+ state: Object.fromEntries(Object.entries(m.step.state ?? {}).map(([n, e]) => [`${p}${n}`, keep(rename(e, p, m, inputs), x(`${p}${n}`))])),
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}`) }) }];
59
+ const key = rename(w.key, p, m, inputs);
60
+ return [`${p}${n}`, { key, value: keep(rename(w.value, p, m, inputs), { op: 'get', map: `${p}${n}`, key }) }];
61
+ })),
62
+ },
63
+ assume: m.assume ? renameC(m.assume, p, m, inputs) : undefined,
64
+ };
65
+ }
66
+ const join = (a, b) => ({
67
+ state: { ...a.state, ...b.state }, maps: { ...a.maps, ...b.maps },
68
+ step: { flags: { ...a.step.flags, ...b.step.flags }, state: { ...a.step.state, ...b.step.state }, maps: { ...a.step.maps, ...b.step.maps } },
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 } : {}; };
72
+ const conj = (cs) => { const xs = cs.filter((c) => c !== undefined); return xs.length ? all(...xs) : undefined; };
73
+ // a and b stepped by the same requests. `bInputs` rewrites what b sees (other tenants' requests as no-ops, say).
74
+ export function product(a, b, opts = {}) {
75
+ const A = renamed(a, 'a_', {}), B = renamed(b, 'b_', opts.bInputs ?? {});
76
+ const assume = conj([A.assume, B.assume, opts.assume]), pre = conj([a.pre, b.pre]);
77
+ return {
78
+ name: opts.name ?? `${a.name} | ${b.name}`,
79
+ params: [...new Set([...a.params, ...b.params, ...(opts.params ?? [])])],
80
+ inputs: [...new Set([...a.inputs, ...b.inputs, ...(opts.inputs ?? [])])],
81
+ ...join(A, B), ...carriedBy(a, b),
82
+ ...(assume ? { assume } : {}),
83
+ ...(pre ? { pre } : {}),
84
+ };
85
+ }
86
+ // One of a and b steps on each request: a when turn = 0, b when turn = 1; the other keeps its state and maps. Each
87
+ // side's assumption is about its own turns only.
88
+ export function interleave(a, b, opts = {}) {
89
+ const turn = opts.turn ?? 'turn';
90
+ if ([...a.params, ...a.inputs, ...b.params, ...b.inputs].includes(turn))
91
+ throw new Error(`interleave: ${turn} is already a name of ${a.name} or ${b.name}`);
92
+ const isA = same(x(turn), { op: 'const', value: 0 }), isB = same(x(turn), { op: 'const', value: 1 });
93
+ const A = renamed(a, 'a_', {}, isB), B = renamed(b, 'b_', {}, isA);
94
+ const pre = conj([a.pre, b.pre]);
95
+ return {
96
+ name: opts.name ?? `${a.name} || ${b.name}`,
97
+ params: [...new Set([...a.params, ...b.params])],
98
+ inputs: [...new Set([...a.inputs, ...b.inputs, turn])],
99
+ ...join(A, B), ...carriedBy(a, b),
100
+ assume: all(bit(turn), ...(A.assume ? [any(not(isA), A.assume)] : []), ...(B.assume ? [any(not(isB), B.assume)] : [])),
101
+ ...(pre ? { pre } : {}),
102
+ };
103
+ }
104
+ // impl refines spec. The machine is impl and spec side by side, the spec sent, at each of impl's requests, the request
105
+ // `witness` names (a term per spec input over impl's request, the params and both states, a_ and b_ names). The spec's
106
+ // assumption is not assumed there: the step law `allowed` proves the spec allows every request it is sent. `relation`
107
+ // (conditions over both states) holds at the start and after every step: the laws, proven by induction. With the
108
+ // precondition: impl's is assumed, and the spec's, if any, is a law (it holds of every impl's params).
109
+ // So every run of impl is matched, step by step, by a run of the spec that its own rules allow, related by `relation`.
110
+ export function refinement(impl, spec, opts) {
111
+ if (!spec.inputs.every(i => i in opts.witness))
112
+ throw new Error(`refinement: name every input of ${spec.name} (${spec.inputs.join(', ')})`);
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;
118
+ const machine = {
119
+ name: opts.name ?? `${impl.name} refines ${spec.name}`,
120
+ params: [...new Set([...impl.params, ...spec.params])],
121
+ inputs: impl.inputs,
122
+ ...join(A, B), ...carriedBy(impl, spec),
123
+ ...(A.assume ? { assume: A.assume } : {}),
124
+ ...(impl.pre ? { pre: impl.pre } : {}),
125
+ };
126
+ const laws = [...opts.relation, ...(spec.pre ? [{ name: `${spec.name}'s precondition`, holds: spec.pre }] : [])];
127
+ const stepLaws = allowedBy ? [{ name: `${spec.name} allows the request it is sent`, then: allowedBy }] : [];
128
+ return { machine, laws, stepLaws };
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
+ }
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.
141
+ export function agree(a) {
142
+ return [
143
+ ...Object.keys(a.state).map(n => ({ name: `a.${n} = b.${n}`, holds: same(x(`a_${n}`), x(`b_${n}`)) })),
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' })),
145
+ ];
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
+ }