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 +86 -35
- package/dist/core/compose.d.ts +38 -0
- package/dist/core/compose.js +245 -0
- package/dist/core/dsl.d.ts +85 -13
- package/dist/core/dsl.js +424 -19
- package/dist/core/machine.d.ts +34 -6
- package/dist/core/machine.js +194 -22
- package/dist/kernel/kernel.d.ts +31 -3
- package/dist/kernel/kernel.js +364 -87
- package/dist/kernel/prove.d.ts +33 -9
- package/dist/kernel/prove.js +298 -24
- package/dist/lean-kernel/emit.d.ts +132 -0
- package/dist/lean-kernel/emit.js +936 -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 +15 -4
package/README.md
CHANGED
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
# gdp-wired
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
10
|
-
|
|
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:
|
|
37
|
+
## Scope: what it proves by itself, and where the wall is
|
|
37
38
|
|
|
38
|
-
gdp-wired is
|
|
39
|
-
|
|
40
|
-
code.
|
|
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 (
|
|
45
|
-
|
|
|
46
|
-
|
|
|
47
|
-
|
|
|
48
|
-
|
|
|
49
|
-
|
|
|
50
|
-
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
|
62
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
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`),
|
|
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 (
|
|
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
|
+
}
|