gdp-wired 0.0.0-stage → 0.2.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tom X Nguyen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,238 @@
1
- # Temporary Holding Version
1
+ <img src="https://raw.githubusercontent.com/monotykamary/gdp-wired/main/static/banner-ink.svg" alt="gdp-wired" width="100%" />
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ # gdp-wired
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.
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.
11
+
12
+ ```sh
13
+ npm install gdp-wired # or bun add gdp-wired; plain JavaScript with types, Node 20+, Bun or a browser
14
+ ```
15
+
16
+ ```ts
17
+ import {min, nat, proveRule, rule, when} from 'gdp-wired'
18
+
19
+ // A request of cost c is admitted while spend s is under budget b; each request is capped at m.
20
+ const budget = rule('budget')
21
+ .params({b: nat, m: nat})
22
+ .inputs({c: nat})
23
+ .state({s: 0})
24
+ .step(({b, m, c, s}) => ({s: when(s.lt(b), s.plus(min(c, m)), s)}))
25
+ const bounded = budget.law('spend stays within b + m', ({s, b, m}) => s.atMost(b.plus(m)))
26
+
27
+ proveRule(budget, [bounded]) // {proven: true, checked: true}
28
+ ```
29
+
30
+ Names are typed: `({s, bb}) => …` or a law that reads the request `c` is a TypeScript error, not a runtime surprise.
31
+ Forget the cap (`s.plus(c)`) and the kernel refuses the proof; a search from the start turns it into a real run:
32
+ `{params: {b: 1, m: 0}, trace: [{c: 2}]}`. `bun examples/budget.ts` runs all of it, including the generated JavaScript,
33
+ and `examples/domains/` holds 92 rules from nine domains (finance, access control, public services, operations,
34
+ protocols, commerce, supply chains, games, education), each checked three ways.
35
+
36
+ ## Scope: where it replaces Bend, and where it doesn't
37
+
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:
41
+
42
+ | It expresses and proves | It does not (today) |
43
+ | --- | --- |
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"), and k-step laws about k steps in a row ("two grants on one day add two", up to 16 steps) | Orderings over any number of steps ("a refund never follows a release", at any distance, needs a state flag), beyond progress |
46
+ | 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), each built as a plain machine so every law above applies | Refinement where the spec needs more than one step per step of the implementation |
47
+ | 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") |
48
+ | 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 |
49
+ | Decisions over facts: flags, one-of choices, optional facts, bounded naturals (`match` covers every case) | Datatypes beyond those encodings; division, modulo, reals |
50
+ | 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 |
51
+ | 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) |
52
+
53
+ Keep Bend where the specification is about **lists or sets of names** (resource scopes, capability subsets, normalization
54
+ that must refine its input), where a proof needs **induction over data structures**, or where the shipped code must be
55
+ the compiled proved source of an arbitrary algorithm. Use gdp-wired where the rule is a budget, a cap, a window, a
56
+ workflow or a policy decision: it proves those without proof files, finds counterexample runs when they're false, and
57
+ checks its own kernel in Lean.
58
+
59
+ For a sense of the split: in one real codebase with 98 Bend laws and about 1,600 lines of hand proofs, the policy
60
+ decisions, the compare-and-swap storage plans and the state/provider plans fit gdp-wired as it is (with names interned
61
+ to numbers), and gdp-wired can prove more about storage than its Bend proofs state (no revision reuse across keys); the
62
+ 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
63
+ sets, stay in Bend. `compare/` puts three rules side by side in gdp-wired, Bend and plain Lean, with line counts and
64
+ check times.
65
+
66
+ ## What's trusted
67
+
68
+ | Part | Trusted? | What it does |
69
+ | --- | --- | --- |
70
+ | `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 |
71
+ | `kernel/prove.ts` | No | Grows the trees. If it's wrong, the kernel rejects the tree |
72
+ | `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`), axioms `propext`, `Classical.choice`, `Quot.sound` only. The exporter sends the kernel's real trees; tampered ones compute to false |
73
+ | `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 |
74
+ | `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 |
75
+
76
+ How the untrusted parts are held to the trusted ones, automatically:
77
+
78
+ - **Types**: the whole repo type-checks strictly (`noImplicitReturns`, `noFallthroughCasesInSwitch`,
79
+ `noUncheckedIndexedAccess`), and a type-aware lint denies warnings; every switch over term and condition operations is
80
+ exhaustive and ends in `unreachable()`, so a new operation fails to compile wherever it's missing.
81
+ - **Conformance** (`core/conformance.test.ts`): random terms over every operation through every consumer (interpreter,
82
+ generated JS, the kernel on fixed values, the GPU search's u32 twin, the old symbolic prover) against the reference
83
+ evaluator, and the same cases through Lean's own `evalE` (`lean-kernel/conformance.ts`).
84
+ - **Examples** (`examples/`): every law proven by the kernel and re-checked by Lean, every rule run at random through
85
+ both runtimes, and every broken variant broken by a real run (a refused proof alone could be the prover's weakness).
86
+ - **Tampering and mutation** (`kernel/tamper.test.ts`, `kernel/refusals.test.ts`, `kernel/mutants.ts`): real proofs
87
+ broken structurally or offered for weakened goals that have a model must be rejected, every refusal the kernel makes is
88
+ exercised, and every one-operator mutation of `kernel.ts` and `check.ts` is run against the tests to find what they
89
+ don't notice: 239 of 261 mutants are caught, and the 22 left are equivalent (`kernel/MUTANTS.md`).
90
+ - **Fuzz**: the kernel never accepts a refutation of something with a model (`kernel/kernel.test.ts`), and laws proven
91
+ over histories hold on every run (`kernel/history.test.ts`).
92
+
93
+ ## Writing laws
94
+
95
+ The builders (`core/dsl.ts`) hand out typed values for every name you declare; everything compiles to the plain machine
96
+ the kernel checks, so nothing in them is trusted.
97
+
98
+ ```ts
99
+ import {bool, count, forall, maybe, nat, oneOf, proveSteps, relation, rule, yes} from 'gdp-wired'
100
+
101
+ // Maps and flags: a grant is decided once per step; each map is written once; a map law holds at every key.
102
+ const caps = rule('caps')
103
+ .params({d: nat})
104
+ .inputs({acct: nat, day: nat, ok: bool})
105
+ .maps({Acct: 0, Day: 0})
106
+ .flags(({ok, acct, day, d, Acct, Day}) => ({granted: ok.and(Acct.at(acct).lt(1), Day.at(day).lt(d))}))
107
+ .step(({acct, day, granted, Acct, Day}) => ({Acct: Acct.bump(acct, granted), Day: Day.bump(day, granted)}))
108
+ const dayCap = caps.law('day cap', ({Day, d}) => forall(k => Day.at(k).atMost(d)))
109
+
110
+ // Step laws: about one step, for every request the rule allows; `after` reads the state and maps after it.
111
+ const onlyUp = caps.stepLaw('no count goes down', ({Day}, after) => forall(k => Day.at(k).atMost(after.Day.at(k))))
112
+ 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)
113
+ // k-step laws: about k steps in a row; `requests` holds each step's request, `when` narrows the j-th step's.
114
+ 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)
115
+ proveSteps(caps, [dayCap], [onlyUp, once, twice]) // {proven: true, checked: true}
116
+
117
+ // Histories: a log of requests, a sum over it (state), a query over it at the request's time (an input the runtime
118
+ // computes), and a relation between them proven one record at a time.
119
+ const window = rule('window limit')
120
+ .params({w: nat, c: nat, now: nat})
121
+ .inputs({forced: bool, t: nat})
122
+ .log('forced', 't')
123
+ .sums({ours: (r, {now, w}) => count(r.forced.not().and(r.t.atMost(now), now.lt(r.t.plus(w))))})
124
+ .queries({seen: (r, {t, w}) => count(t.lt(r.t.plus(w)))})
125
+ .flags(({forced, seen, c}) => ({admitted: forced.not().and(seen.plus(1).atMost(c))}))
126
+ .keep(({forced, admitted}) => forced.or(admitted))
127
+ .relations(({t, now, ours, seen}) => [relation(ours.atMost(seen), t.atMost(now))])
128
+ .prune('t', ({w}) => w) // requests in time order: records older than the window are dropped
129
+
130
+ // Decisions over facts: optional facts and choices, every case covered.
131
+ const trial = rule('trial').inputs({plan: oneOf('free', 'pro'), spent: maybe(nat), allowance: maybe(nat)})
132
+ const failsClosed = trial.decide('a missing fact means no trial', ({spent}) => spent.isNone,
133
+ ({plan, spent, allowance}) => plan.match({free: () => spent.match({none: () => yes, some: s => allowance.match({none: () => yes, some: a => s.atLeast(a)})}), pro: () => yes}))
134
+ ```
135
+
136
+ - **Kinds**: `nat`, `upTo(k)`, `bool`, `oneOf(...)`, `maybe(kind)`; their assumptions (a bit is 0 or 1, a choice is in
137
+ range) go to the kernel by themselves, and `encodeInput`/`encodeParams` turn runtime values into the numbers the
138
+ machine reads.
139
+ - **Arithmetic** over the naturals: `plus`, `minus` (truncated), `times(k)`, `mul` (a product of two terms), `min`,
140
+ `max`, `when(cond, a, b)`, `count(cond)`; conditions `lt`, `atMost`, `eq`, `and`, `or`, `not`, `implies`.
141
+ - **Loops** (`loop(...)`): state, `while`, `body`, a `rank` (or a tuple of ranks) that must drop each turn.
142
+ - **Composition** (`gdp-wired/compose`): `product(a, b)`, `interleave(a, b)` and `refinement(impl, spec, {relation, witness})`
143
+ build plain machines from machines (each side's state named `a_x`, `b_x`). A refinement's laws are the relation, proven
144
+ 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.
146
+ - **Running it**: `rule.interpreter()` is persistent (searches branch from any state); `rule.compiled()` is what an app
147
+ ships: maps and the log updated in place, so a step costs the same however long the history. Both refuse params and
148
+ inputs that aren't naturals below 2^53 and any value past 2^53 − 1, where the proofs over the naturals stop describing
149
+ the run. The GPU searches refuse a space whose values could pass 2^32 − 1.
150
+
151
+ ## Numbers
152
+
153
+ On Modal CPUs (2 vCPU), generated JavaScript, the answers checked against the interpreter:
154
+
155
+ | Machine | 1k | 10k | 100k | 1M |
156
+ | --- | --- | --- | --- | --- |
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 |
159
+
160
+ Pruned, the window limit holds about 35 records whatever the event count; unpruned, each request scans every record kept
161
+ (76 ms at 10k events). The interpreter copies its maps each step (it's the reference, and searches branch from it):
162
+ 1.6 s for 10k requests of the caps rule.
163
+
164
+ A brute-force enumeration of 2^31 worlds (`enum/`, `perf/bend-enum/`), the same predicate as Bend programs and as a
165
+ Handwired word program: on an M2, Bend takes 985 ms sequential, 221 ms parallel and 153 ms on Metal (whole process,
166
+ about 130 ms of it device setup); ours takes 33 ms warm on WebGPU, with about 115 ms of device setup first. On an NVIDIA
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).
169
+
170
+ ## Compared with Bend and Lean
171
+
172
+ `compare/` writes three rules (a budget, per-key caps, a sliding-window limit) in gdp-wired, in Bend 2 and in plain Lean
173
+ 4, each checked by its own tool on Modal, each also handed the same bug to reject. From `compare/README.md`:
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`.
184
+
185
+ ## Layout
186
+
187
+ | Path | What |
188
+ | --- | --- |
189
+ | `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
+ | `kernel/` | The trusted kernel and the untrusted prover, with soundness fuzz (`kernel.test.ts`, `history.test.ts`) |
191
+ | `lean/`, `lean-kernel/` | Lean: the derivation checker `Cert.lean`, and the kernel's twin with its exported trees |
192
+ | `search/`, `hw/`, `modal-gpu/` | Counterexample search on the GPU: a hand-written WGSL kernel, and the same through the Handwired fork's word stages |
193
+ | `synth/` | Invariants and ranks found on the GPU, proven by the kernel |
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` |
198
+ | `compare/` | The same three rules in gdp-wired, Bend and plain Lean, checked and measured |
199
+ | `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 |
200
+ | `modal/` | Everything above as Modal jobs: `suite.json` is the regression suite |
201
+ | `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
+ | `vendor/gdp-ts` | [rauchg/gdp-ts](https://github.com/rauchg/gdp-ts) |
203
+
204
+ ## Running
205
+
206
+ The npm package is the library: the builders (`gdp-wired`), the runtimes (`gdp-wired/machine`), composition
207
+ (`gdp-wired/compose`), the kernel
208
+ (`gdp-wired/kernel`), the prover (`gdp-wired/prove`), the derivation checker and solver (`gdp-wired/check`,
209
+ `gdp-wired/solver`). Everything else here (examples, comparisons, the Lean twin, the GPU searches) runs from a clone and
210
+ needs [Bun](https://bun.sh); Lean 4.34.0 for `lean/` and `lean-kernel/`; a WebGPU runtime (Chrome, or Dawn's node
211
+ bindings) for the GPU parts. Releases: bump `version` in `package.json` and push to main;
212
+ `.github/workflows/publish.yml` publishes it with npm provenance.
213
+
214
+ ```sh
215
+ git clone --recursive https://github.com/monotykamary/gdp-wired && cd gdp-wired
216
+ bun examples/budget.ts
217
+ bun test core/compile.test.ts core/runtime.test.ts kernel/history.test.ts
218
+ cd kernel && bun run-kernel.ts && bun test kernel.test.ts
219
+ ```
220
+
221
+ The whole suite (110 jobs, about 15 minutes of wall time) runs on [Modal](https://modal.com):
222
+
223
+ ```sh
224
+ modal run modal/run.py --jobs modal/suite.json --out runs/modal/suite && sh modal/reap.sh
225
+ ```
226
+
227
+ Each directory's README has its own runs and results.
228
+
229
+ ## Credits
230
+
231
+ [gdp-ts](https://github.com/rauchg/gdp-ts) by Guillermo Rauch (Ghosts of Departed Proofs for TypeScript) is where the
232
+ audit path starts; [Handwired](https://github.com/chenglou/handwired) by Cheng Lou is the circuit and GPU runtime this
233
+ builds on, through a fork; [Bend](https://github.com/bendlang/bend) is the language the laws were first written in,
234
+ and the yardstick for the benchmarks.
235
+
236
+ ## License
237
+
238
+ MIT
@@ -0,0 +1,3 @@
1
+ import type { Constraint, Step } from './solver.js';
2
+ export declare function checkRefutation(constraints: Constraint[], steps: Step[]): boolean;
3
+ export declare function checkWitness(constraints: Constraint[], witness: Record<string, number>): boolean;
@@ -0,0 +1,31 @@
1
+ const vars = (lin) => Object.keys(lin).filter(key => key !== '1');
2
+ export function checkRefutation(constraints, steps) {
3
+ const rows = [];
4
+ for (const [i, s] of steps.entries()) {
5
+ if (s.kind === 'input') {
6
+ if (!(s.index >= 0 && s.index < constraints.length))
7
+ return false;
8
+ rows.push(constraints[s.index].lin);
9
+ }
10
+ else if (s.kind === 'combine') {
11
+ if (!(s.a < i && s.b < i && Number.isInteger(s.ma) && Number.isInteger(s.mb) && s.ma > 0 && s.mb > 0))
12
+ return false;
13
+ const out = {};
14
+ for (const [lin, m] of [[rows[s.a], s.ma], [rows[s.b], s.mb]])
15
+ for (const [key, c] of Object.entries(lin))
16
+ out[key] = (out[key] ?? 0) + m * c;
17
+ rows.push(out);
18
+ }
19
+ else {
20
+ const row = rows[s.of];
21
+ if (!(row !== undefined && s.of < i && Number.isInteger(s.g) && s.g > 0 && vars(row).every(key => row[key] % s.g === 0)))
22
+ return false;
23
+ rows.push({ ...Object.fromEntries(vars(row).map(key => [key, row[key] / s.g])), 1: Math.ceil((row['1'] ?? 0) / s.g) });
24
+ }
25
+ }
26
+ const last = rows.at(-1);
27
+ return last !== undefined && vars(last).every(key => last[key] === 0) && (last['1'] ?? 0) > 0;
28
+ }
29
+ export function checkWitness(constraints, witness) {
30
+ return constraints.every(c => Object.entries(c.lin).reduce((sum, [key, a]) => sum + a * (key === '1' ? 1 : (witness[key] ?? NaN)), 0) <= 0);
31
+ }
@@ -0,0 +1,24 @@
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
+ name?: string;
19
+ }): {
20
+ machine: Machine;
21
+ laws: Law[];
22
+ stepLaws: StepLaw[];
23
+ };
24
+ export declare function agree(a: Machine): Law[];
@@ -0,0 +1,121 @@
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})
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)
9
+ // Each side's state, maps and flags are renamed a_x and b_x; params and inputs keep their names.
10
+ import { all, any, bit, not, same, unreachable, x } from "./machine.js";
11
+ function rename(e, prefix, m, inputs) {
12
+ switch (e.op) {
13
+ case 'var': return m.params.includes(e.name) ? e : m.inputs.includes(e.name) ? (inputs[e.name] ?? e) : x(`${prefix}${e.name}`);
14
+ case 'const': return e;
15
+ case 'mul': return { ...e, a: rename(e.a, prefix, m, inputs) };
16
+ 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) };
18
+ case 'add':
19
+ case 'sub':
20
+ case 'min':
21
+ case 'max':
22
+ case 'prod': return { ...e, a: rename(e.a, prefix, m, inputs), b: rename(e.b, prefix, m, inputs) };
23
+ default: return unreachable(e);
24
+ }
25
+ }
26
+ function renameC(c, prefix, m, inputs) {
27
+ if (c.op === 'flag')
28
+ return { op: 'flag', name: `${prefix}${c.name}` };
29
+ if (c.op === 'not')
30
+ return { op: 'not', x: renameC(c.x, prefix, m, inputs) };
31
+ if (c.op === 'and' || c.op === 'or')
32
+ return { op: c.op, xs: c.xs.map(y => renameC(y, prefix, m, inputs)) };
33
+ return { ...c, a: rename(c.a, prefix, m, inputs), b: rename(c.b, prefix, m, inputs) };
34
+ }
35
+ // A condition (or law) of one side, read in the composed machine.
36
+ export const side = (m, prefix, c) => renameC(c, prefix, m, {});
37
+ // One side renamed; `stays` (if given) is when its state doesn't move: each update and write keeps the old value then.
38
+ function renamed(m, p, inputs, stays) {
39
+ if (m.history)
40
+ throw new Error(`${m.name}: a machine with a history is composed as its plain machine (historyMachine)`);
41
+ const keep = (now, old) => (stays ? { op: 'if', c: stays, then: old, else: now } : now);
42
+ return {
43
+ state: Object.fromEntries(Object.entries(m.state).map(([n, e]) => [`${p}${n}`, rename(e, p, m, {})])),
44
+ maps: Object.fromEntries(Object.entries(m.maps ?? {}).map(([n, v]) => [`${p}${n}`, v])),
45
+ step: {
46
+ flags: Object.fromEntries(Object.entries(m.step.flags ?? {}).map(([n, c]) => [`${p}${n}`, renameC(c, p, m, inputs)])),
47
+ state: Object.fromEntries(Object.entries(m.step.state ?? {}).map(([n, e]) => [`${p}${n}`, keep(rename(e, p, m, inputs), x(`${p}${n}`))])),
48
+ maps: Object.fromEntries(Object.entries(m.step.maps ?? {}).map(([n, w]) => {
49
+ const key = rename(w.key, p, m, inputs);
50
+ return [`${p}${n}`, { key, value: keep(rename(w.value, p, m, inputs), { op: 'get', map: `${p}${n}`, key }) }];
51
+ })),
52
+ },
53
+ assume: m.assume ? renameC(m.assume, p, m, inputs) : undefined,
54
+ };
55
+ }
56
+ const join = (a, b) => ({
57
+ state: { ...a.state, ...b.state }, maps: { ...a.maps, ...b.maps },
58
+ step: { flags: { ...a.step.flags, ...b.step.flags }, state: { ...a.step.state, ...b.step.state }, maps: { ...a.step.maps, ...b.step.maps } },
59
+ });
60
+ const conj = (cs) => { const xs = cs.filter((c) => c !== undefined); return xs.length ? all(...xs) : undefined; };
61
+ // a and b stepped by the same requests. `bInputs` rewrites what b sees (other tenants' requests as no-ops, say).
62
+ export function product(a, b, opts = {}) {
63
+ const A = renamed(a, 'a_', {}), B = renamed(b, 'b_', opts.bInputs ?? {});
64
+ const assume = conj([A.assume, B.assume, opts.assume]), pre = conj([a.pre, b.pre]);
65
+ return {
66
+ name: opts.name ?? `${a.name} | ${b.name}`,
67
+ params: [...new Set([...a.params, ...b.params, ...(opts.params ?? [])])],
68
+ inputs: [...new Set([...a.inputs, ...b.inputs, ...(opts.inputs ?? [])])],
69
+ ...join(A, B),
70
+ ...(assume ? { assume } : {}),
71
+ ...(pre ? { pre } : {}),
72
+ };
73
+ }
74
+ // 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
75
+ // side's assumption is about its own turns only.
76
+ export function interleave(a, b, opts = {}) {
77
+ const turn = opts.turn ?? 'turn';
78
+ if ([...a.params, ...a.inputs, ...b.params, ...b.inputs].includes(turn))
79
+ throw new Error(`interleave: ${turn} is already a name of ${a.name} or ${b.name}`);
80
+ const isA = same(x(turn), { op: 'const', value: 0 }), isB = same(x(turn), { op: 'const', value: 1 });
81
+ const A = renamed(a, 'a_', {}, isB), B = renamed(b, 'b_', {}, isA);
82
+ const pre = conj([a.pre, b.pre]);
83
+ return {
84
+ name: opts.name ?? `${a.name} || ${b.name}`,
85
+ params: [...new Set([...a.params, ...b.params])],
86
+ inputs: [...new Set([...a.inputs, ...b.inputs, turn])],
87
+ ...join(A, B),
88
+ assume: all(bit(turn), ...(A.assume ? [any(not(isA), A.assume)] : []), ...(B.assume ? [any(not(isB), B.assume)] : [])),
89
+ ...(pre ? { pre } : {}),
90
+ };
91
+ }
92
+ // impl refines spec. The machine is impl and spec side by side, the spec sent, at each of impl's requests, the request
93
+ // `witness` names (a term per spec input over impl's request, the params and both states, a_ and b_ names). The spec's
94
+ // assumption is not assumed there: the step law `allowed` proves the spec allows every request it is sent. `relation`
95
+ // (conditions over both states) holds at the start and after every step: the laws, proven by induction. With the
96
+ // precondition: impl's is assumed, and the spec's, if any, is a law (it holds of every impl's params).
97
+ // So every run of impl is matched, step by step, by a run of the spec that its own rules allow, related by `relation`.
98
+ export function refinement(impl, spec, opts) {
99
+ if (!spec.inputs.every(i => i in opts.witness))
100
+ throw new Error(`refinement: name every input of ${spec.name} (${spec.inputs.join(', ')})`);
101
+ const A = renamed(impl, 'a_', {}), B = renamed(spec, 'b_', opts.witness);
102
+ const allowedBy = B.assume;
103
+ const machine = {
104
+ name: opts.name ?? `${impl.name} refines ${spec.name}`,
105
+ params: [...new Set([...impl.params, ...spec.params])],
106
+ inputs: impl.inputs,
107
+ ...join(A, B),
108
+ ...(A.assume ? { assume: A.assume } : {}),
109
+ ...(impl.pre ? { pre: impl.pre } : {}),
110
+ };
111
+ const laws = [...opts.relation, ...(spec.pre ? [{ name: `${spec.name}'s precondition`, holds: spec.pre }] : [])];
112
+ const stepLaws = allowedBy ? [{ name: `${spec.name} allows the request it is sent`, then: allowedBy }] : [];
113
+ return { machine, laws, stepLaws };
114
+ }
115
+ // "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
+ export function agree(a) {
117
+ return [
118
+ ...Object.keys(a.state).map(n => ({ name: `a.${n} = b.${n}`, holds: same(x(`a_${n}`), x(`b_${n}`)) })),
119
+ ...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
+ ];
121
+ }