gdp-wired 0.1.0 → 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/README.md CHANGED
@@ -42,7 +42,8 @@ code. If the checker side finds new frontiers the scope may grow; until then:
42
42
  | It expresses and proves | It does not (today) |
43
43
  | --- | --- |
44
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 |
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 |
46
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") |
47
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 |
48
49
  | Decisions over facts: flags, one-of choices, optional facts, bounded naturals (`match` covers every case) | Datatypes beyond those encodings; division, modulo, reals |
@@ -66,11 +67,11 @@ check times.
66
67
 
67
68
  | Part | Trusted? | What it does |
68
69
  | --- | --- | --- |
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 |
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 |
70
71
  | `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
+ | `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 |
72
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 |
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 |
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 |
74
75
 
75
76
  How the untrusted parts are held to the trusted ones, automatically:
76
77
 
@@ -85,7 +86,7 @@ How the untrusted parts are held to the trusted ones, automatically:
85
86
  - **Tampering and mutation** (`kernel/tamper.test.ts`, `kernel/refusals.test.ts`, `kernel/mutants.ts`): real proofs
86
87
  broken structurally or offered for weakened goals that have a model must be rejected, every refusal the kernel makes is
87
88
  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`).
89
+ don't notice: 239 of 261 mutants are caught, and the 22 left are equivalent (`kernel/MUTANTS.md`).
89
90
  - **Fuzz**: the kernel never accepts a refutation of something with a model (`kernel/kernel.test.ts`), and laws proven
90
91
  over histories hold on every run (`kernel/history.test.ts`).
91
92
 
@@ -109,7 +110,9 @@ 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.
110
111
  const onlyUp = caps.stepLaw('no count goes down', ({Day}, after) => forall(k => Day.at(k).atMost(after.Day.at(k))))
111
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)
112
- proveSteps(caps, [dayCap], [onlyUp, once]) // {proven: true, checked: true}
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}
113
116
 
114
117
  // Histories: a log of requests, a sum over it (state), a query over it at the request's time (an input the runtime
115
118
  // computes), and a relation between them proven one record at a time.
@@ -136,6 +139,10 @@ const failsClosed = trial.decide('a missing fact means no trial', ({spent}) => s
136
139
  - **Arithmetic** over the naturals: `plus`, `minus` (truncated), `times(k)`, `mul` (a product of two terms), `min`,
137
140
  `max`, `when(cond, a, b)`, `count(cond)`; conditions `lt`, `atMost`, `eq`, `and`, `or`, `not`, `implies`.
138
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.
139
146
  - **Running it**: `rule.interpreter()` is persistent (searches branch from any state); `rule.compiled()` is what an app
140
147
  ships: maps and the log updated in place, so a step costs the same however long the history. Both refuse params and
141
148
  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 +186,15 @@ its Lean twin in 1.35 s. What each proves, what each ships at runtime, and where
179
186
 
180
187
  | Path | What |
181
188
  | --- | --- |
182
- | `core/` | Machines, the interpreter and JS compiler, the linear solver and its checker, Houdini invariants, models, the first Lean export |
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 |
183
190
  | `kernel/` | The trusted kernel and the untrusted prover, with soundness fuzz (`kernel.test.ts`, `history.test.ts`) |
184
191
  | `lean/`, `lean-kernel/` | Lean: the derivation checker `Cert.lean`, and the kernel's twin with its exported trees |
185
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 |
186
193
  | `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 |
194
+ | `next/` | Guards synthesized from laws, relational laws (migrations, tenant isolation, through `core/compose.ts`), a ReBAC audit |
188
195
  | `sat/` | Bit-vector laws: bit-blasted, solved, LRAT-checked in TypeScript, optionally by Lean |
189
196
  | `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` |
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` |
191
198
  | `compare/` | The same three rules in gdp-wired, Bend and plain Lean, checked and measured |
192
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 |
193
200
  | `modal/` | Everything above as Modal jobs: `suite.json` is the regression suite |
@@ -196,7 +203,8 @@ its Lean twin in 1.35 s. What each proves, what each ships at runtime, and where
196
203
 
197
204
  ## Running
198
205
 
199
- The npm package is the library: the builders (`gdp-wired`), the runtimes (`gdp-wired/machine`), the kernel
206
+ The npm package is the library: the builders (`gdp-wired`), the runtimes (`gdp-wired/machine`), composition
207
+ (`gdp-wired/compose`), the kernel
200
208
  (`gdp-wired/kernel`), the prover (`gdp-wired/prove`), the derivation checker and solver (`gdp-wired/check`,
201
209
  `gdp-wired/solver`). Everything else here (examples, comparisons, the Lean twin, the GPU searches) runs from a clone and
202
210
  needs [Bun](https://bun.sh); Lean 4.34.0 for `lean/` and `lean-kernel/`; a WebGPU runtime (Chrome, or Dawn's node
@@ -210,7 +218,7 @@ bun test core/compile.test.ts core/runtime.test.ts kernel/history.test.ts
210
218
  cd kernel && bun run-kernel.ts && bun test kernel.test.ts
211
219
  ```
212
220
 
213
- The whole suite (107 jobs, about 15 minutes of wall time) runs on [Modal](https://modal.com):
221
+ The whole suite (110 jobs, about 15 minutes of wall time) runs on [Modal](https://modal.com):
214
222
 
215
223
  ```sh
216
224
  modal run modal/run.py --jobs modal/suite.json --out runs/modal/suite && sh modal/reap.sh
@@ -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
+ }
@@ -166,6 +166,7 @@ export declare class Rule<P extends Obj = {}, I extends Obj = {}, S extends Obj
166
166
  prune(order: keyof R & string, after: (p: P) => TermLike): Rule<P, I, S, M, F, R, Su, Q>;
167
167
  law(name: string, f: (env: P & S & M & Su) => Cond | Forall): Law;
168
168
  stepLaw(name: string, then: (before: P & I & S & M & F & Su & Q, after: S & M & Su) => Cond | Forall, when?: (before: P & I & S & M & F & Su & Q) => Cond): StepLaw;
169
+ stepsLaw(name: string, k: number, then: (first: P & I & S & M & F & Su & Q, last: S & M & Su, requests: (I & Q)[]) => Cond | Forall, when?: (before: P & I & S & M & F & Su & Q, j: number) => Cond | undefined): StepLaw;
169
170
  decide(name: string, given: (env: P & I) => Cond | Cond[], then: (env: P & I) => Cond): Decision;
170
171
  private kindAssumptions;
171
172
  private env;
package/dist/core/dsl.js CHANGED
@@ -223,6 +223,24 @@ export const forall = (f) => ({ forall: f });
223
223
  // For histories: `holds` (a <= b, homogeneous over sums and queries) wherever `when` (params and plain inputs) holds.
224
224
  export const relation = (holds, whenever = yes) => ({ when: whenever, holds });
225
225
  let keyCounter = 0;
226
+ // A k-step law's later requests are built over marked names (j:name, which no rule can declare: ':' is reserved); the
227
+ // kernel reads them as name@j.
228
+ const markedName = (name) => { const m = /^(\d+):(.*)$/.exec(name); return m ? `${m[2]}@${m[1]}` : name; };
229
+ const markedE = (e) => {
230
+ switch (e.op) {
231
+ case 'var': return { op: 'var', name: markedName(e.name) };
232
+ case 'const': return e;
233
+ case 'mul': return { ...e, a: markedE(e.a) };
234
+ case 'if': return { op: 'if', c: marked(e.c), then: markedE(e.then), else: markedE(e.else) };
235
+ case 'get': return { op: 'get', map: e.map, key: markedE(e.key) };
236
+ case 'add':
237
+ case 'sub':
238
+ case 'min':
239
+ case 'max':
240
+ case 'prod': return { ...e, a: markedE(e.a), b: markedE(e.b) };
241
+ }
242
+ };
243
+ const marked = (c) => c.op === 'flag' ? c : c.op === 'not' ? { op: 'not', x: marked(c.x) } : c.op === 'and' || c.op === 'or' ? { op: c.op, xs: c.xs.map(marked) } : { ...c, a: markedE(c.a), b: markedE(c.b) };
226
244
  // P params, I inputs, S state, M maps, F flags, R a history's record, Su its sums, Q its queries: each a record of the
227
245
  // values the builders hand out, grown by each call.
228
246
  export class Rule {
@@ -295,6 +313,36 @@ export class Rule {
295
313
  const key = `key$${++keyCounter}`;
296
314
  return { name, forall: key, then: out.forall(v(key)).c, ...w };
297
315
  }
316
+ // A law about k steps in a row (k from 2 to 16): `then(first, last, requests)` relates what the first step reads to the
317
+ // state, maps and sums after the last, with each step's request and queries (requests[0] is the first's); `when(before,
318
+ // j)` narrows the j-th step's request (j from 0), read at the state that step starts from.
319
+ // meter.stepsLaw('two requests spend at most 2m', 2, ({spent, m}, after) => after.spent.atMost(spent.plus(m).plus(m)))
320
+ stepsLaw(name, k, then, when) {
321
+ const env = this.env();
322
+ const after = {};
323
+ for (const [n] of this.spec.state)
324
+ after[n] = v(`${n}'`);
325
+ for (const [n] of this.spec.sums)
326
+ after[n] = v(`${n}'`);
327
+ for (const [n, init] of this.spec.maps)
328
+ after[n] = new MapRef(`${n}'`, init);
329
+ // The j-th request's values over marked names, renamed to the kernel's input@j once built.
330
+ const mark = (j) => `${j}:`;
331
+ const requests = Array.from({ length: k }, (_, j) => {
332
+ if (j === 0)
333
+ return Object.fromEntries([...this.spec.inputs.map(([n]) => n), ...this.spec.queries.map(([n]) => n)].map(n => [n, env[n]]));
334
+ return Object.fromEntries([...this.spec.inputs.map(([n, kind]) => [n, kind.value(n, mark(j + 1))]), ...this.spec.queries.map(([n]) => [n, v(`${mark(j + 1)}${n}`)])]);
335
+ });
336
+ const out = then(env, after, requests);
337
+ const whens = when ? Array.from({ length: k }, (_, j) => when(env, j)) : [];
338
+ while (whens.length && whens.at(-1) === undefined)
339
+ whens.pop();
340
+ const w = whens.length ? { when: whens.map(c => (c ?? lit(0).atMost(0)).c) } : {};
341
+ if (out instanceof Cond)
342
+ return { name, steps: k, then: marked(out.c), ...w };
343
+ const key = `key$${++keyCounter}`;
344
+ return { name, steps: k, forall: key, then: marked(out.forall(v(key)).c), ...w };
345
+ }
298
346
  // A decision: whenever the params and the request meet `given`, `then` holds. Decisions don't read the state.
299
347
  decide(name, given, then) {
300
348
  const env = this.env({ flags: false, state: false, queries: false });
@@ -61,9 +61,10 @@ export declare function progressObligations(m: Machine, laws: Law[], instances:
61
61
  }): Goal[];
62
62
  export type StepLaw = {
63
63
  name: string;
64
- when?: C;
64
+ when?: C | C[];
65
65
  then: C;
66
66
  forall?: string;
67
+ steps?: number;
67
68
  };
68
69
  export declare function stepLawObligations(machine: Machine, laws: Law[], instances: Instances, sl: StepLaw): Goal[];
69
70
  export declare function checkStepLaws(m: Machine, laws: Law[], stepLaws: StepLaw[], proof: {
@@ -274,6 +274,45 @@ const preOf = (o) => {
274
274
  throw new Error('a precondition reads only the params');
275
275
  return o.pre ? [o.pre] : [];
276
276
  };
277
+ const identity = { vars: {}, maps: {}, flags: {} };
278
+ const from = (at = identity) => ({ hyps: [], at });
279
+ // Conditions that hold where the run is.
280
+ const holds = (r, cs) => ({ hyps: [...r.hyps, ...cs.map(c => substC(c, r.at))], at: r.at });
281
+ // Names read where the run is: inputs named for this step (a request's terms, or fresh names for a later step).
282
+ const reading = (r, vars) => ({ hyps: r.hyps, at: { ...r.at, vars: { ...r.at.vars, ...vars } } });
283
+ // One step: each name the update sets is its term read where the run is; a map read after it is the written value at
284
+ // the written key, and what the map held before elsewhere.
285
+ const step = (r, u) => {
286
+ const at = r.at, before = (map, key) => (at.maps[map] ? at.maps[map](key) : { op: 'get', map, key });
287
+ return { hyps: r.hyps, at: {
288
+ vars: { ...at.vars, ...Object.fromEntries(Object.entries(u.vars).map(([n, e]) => [n, substE(e, at)])) },
289
+ maps: { ...at.maps, ...Object.fromEntries(Object.entries(u.maps).map(([map, w]) => [map, (key) => ({ op: 'if', c: { op: '=', a: key, b: substE(w.key, at) }, then: substE(w.value, at), else: before(map, key) })])) },
290
+ flags: at.flags,
291
+ } };
292
+ };
293
+ // The conclusion read at the run's end: plain names at the last frame.
294
+ const atEnd = (r, c) => substC(c, r.at);
295
+ // The conclusion read across the run: plain names at the first frame, primed names (s', and M'[k] as a read of the map
296
+ // named M') at the last; the first step's inputs as it read them, and its flags, decided where it was taken.
297
+ const across = (r, c, state, maps, first) => substC(c, {
298
+ vars: { ...first.inputs, ...Object.fromEntries(state.map(n => [`${n}'`, r.at.vars[n] ?? { op: 'var', name: n }])) },
299
+ maps: Object.fromEntries(maps.map(map => [`${map}'`, (key) => (r.at.maps[map] ? r.at.maps[map](key) : { op: 'get', map, key })])),
300
+ flags: first.flags,
301
+ });
302
+ const goal = (name, r, conclusion) => ({ name, hyps: [...r.hyps, { op: 'not', x: conclusion }] });
303
+ // A machine's step as an update, its flags inlined (they are decided before the step, over the frame it starts from).
304
+ const updateOf = (m) => {
305
+ const flags = { vars: {}, maps: {}, flags: m.step.flags ?? {} };
306
+ return {
307
+ vars: Object.fromEntries(Object.entries(m.step.state ?? {}).map(([n, e]) => [n, substE(e, flags)])),
308
+ maps: Object.fromEntries(Object.entries(m.step.maps ?? {}).map(([map, w]) => [map, { key: substE(w.key, flags), value: substE(w.value, flags) }])),
309
+ };
310
+ };
311
+ const inlineFlags = (m, c) => substC(c, { vars: {}, maps: {}, flags: m.step.flags ?? {} });
312
+ // The laws assumed where the run is: each plain law, and each every-key law at the keys the proof names.
313
+ const assumed = (laws, instances) => laws.flatMap(l => (l.forall ? (instances[l.name] ?? []).map(key => substC(l.holds, { vars: { [l.forall]: key }, maps: {}, flags: {} })) : [l.holds]));
314
+ // The start: the state's start values over the params, every map cell its constant.
315
+ const startOf = (m) => ({ vars: { ...m.state }, maps: Object.fromEntries(Object.entries(m.maps ?? {}).map(([map, c]) => [map, () => ({ op: 'const', value: c })])), flags: {} });
277
316
  function reads(t, out = { vars: new Set(), maps: new Set(), flags: new Set() }) {
278
317
  switch (t.op) {
279
318
  case 'var':
@@ -429,28 +468,15 @@ export function obligations(m, laws, instances = {}) {
429
468
  && Object.values(m.step.flags ?? {}).every(naturalC) && laws.every(l => naturalC(l.holds)) && Object.values(m.maps ?? {}).every(c => Number.isInteger(c) && c >= 0);
430
469
  if (!ok || laws.some(l => mentions(l.holds, m.inputs)))
431
470
  throw new Error('a negative constant or factor, or a law about a request, is outside the kernel');
432
- const fresh = (law) => `${law.forall}#`;
433
- const atKey = (law, key) => substC(law.holds, { vars: { [law.forall]: key }, maps: {}, flags: {} });
434
- const goal = (law) => (law.forall ? atKey(law, { op: 'var', name: fresh(law) }) : law.holds);
435
- const pre = preOf(m);
436
- // At the start: the precondition, and no request.
437
- const start = { vars: Object.fromEntries(Object.entries(m.state)), maps: Object.fromEntries(Object.entries(m.maps ?? {}).map(([map, c]) => [map, () => ({ op: 'const', value: c })])), flags: {} };
438
- // One step: the state after, as terms over the state before; a map read after the step is the written value at the
439
- // written key, and the old cell elsewhere.
440
- const flags = { vars: {}, maps: {}, flags: m.step.flags ?? {} };
441
- const inline = (c) => substC(c, flags), inlineE = (e) => substE(e, flags);
442
- const step = {
443
- vars: Object.fromEntries(Object.entries(m.step.state ?? {}).map(([name, e]) => [name, inlineE(e)])),
444
- flags: {},
445
- maps: Object.fromEntries(Object.entries(m.step.maps ?? {}).map(([map, w]) => [map, (key) => ({ op: 'if', c: { op: '=', a: key, b: inlineE(w.key) }, then: inlineE(w.value), else: { op: 'get', map, key } })])),
446
- };
447
- const out = [];
448
- for (const law of laws) {
449
- out.push({ name: `${law.name} @start`, hyps: [...pre, { op: 'not', x: substC(goal(law), start) }] });
450
- const before = laws.flatMap(l => (l.forall ? (instances[law.name]?.[l.name] ?? []).map(key => atKey(l, key)) : [l.holds]));
451
- out.push({ name: `${law.name} @step`, hyps: [...pre, ...(m.assume ? [inline(m.assume)] : []), ...before, { op: 'not', x: substC(goal(law), step) }] });
452
- }
453
- return out;
471
+ // A law that holds at every key is proven at a fresh one.
472
+ const proven = (law) => (law.forall ? substC(law.holds, { vars: { [law.forall]: { op: 'var', name: `${law.forall}#` } }, maps: {}, flags: {} }) : law.holds);
473
+ const pre = preOf(m), u = updateOf(m), assume = m.assume ? [inlineFlags(m, m.assume)] : [];
474
+ return laws.flatMap(law => {
475
+ // At the start: the precondition, and no request. One step: from a state where the laws hold, a request allowed.
476
+ const start = holds(from(startOf(m)), pre);
477
+ const turn = step(holds(from(), [...pre, ...assume, ...assumed(laws, instances[law.name] ?? {})]), u);
478
+ return [goal(`${law.name} @start`, start, atEnd(start, proven(law))), goal(`${law.name} @step`, turn, atEnd(turn, proven(law)))];
479
+ });
454
480
  }
455
481
  // The whole set at once: every goal has a tree, and every tree checks.
456
482
  export function checkLaws(m, laws, proof) {
@@ -476,15 +502,12 @@ export function loopObligations(l, laws, rank) {
476
502
  throw new Error('a rank reads the state and params, not the turn\'s input');
477
503
  if (!parts.every(natural) || !naturalC(l.while) || !laws.every(law => !law.forall && naturalC(law.holds) && !mentions(law.holds, l.inputs)))
478
504
  throw new Error('outside the kernel');
479
- const start = { vars: l.state, maps: {}, flags: {} }, turn = { vars: l.body, maps: {}, flags: {} };
480
505
  const pre = preOf(l);
481
- const during = [...pre, ...(l.assume ? [l.assume] : []), l.while, ...laws.map(law => law.holds)];
506
+ const start = holds(from({ vars: { ...l.state }, maps: {}, flags: {} }), pre);
507
+ const turn = step(holds(from(), [...pre, ...(l.assume ? [l.assume] : []), l.while, ...laws.map(law => law.holds)]), { vars: l.body, maps: {} });
482
508
  return [
483
- ...laws.flatMap(law => [
484
- { name: `${law.name} @start`, hyps: [...pre, { op: 'not', x: substC(law.holds, start) }] },
485
- { name: `${law.name} @turn`, hyps: [...during, { op: 'not', x: substC(law.holds, turn) }] },
486
- ]),
487
- { name: 'rank drops', hyps: [...during, { op: 'not', x: drops(ranks, turn) }] },
509
+ ...laws.flatMap(law => [goal(`${law.name} @start`, start, atEnd(start, law.holds)), goal(`${law.name} @turn`, turn, atEnd(turn, law.holds))]),
510
+ goal('rank drops', turn, drops(ranks, turn.at)),
488
511
  ];
489
512
  }
490
513
  const drops = (ranks, turn) => {
@@ -505,49 +528,45 @@ export function progressObligations(m, laws, instances, p) {
505
528
  throw new Error('a request is named from the state and params');
506
529
  const req = { vars: p.request, maps: {}, flags: {} };
507
530
  const flags = Object.fromEntries(Object.entries(m.step.flags ?? {}).map(([f, c]) => [f, substC(c, req)]));
508
- const now = (e) => substE(substE(e, { vars: {}, maps: {}, flags: m.step.flags ?? {} }), req);
509
- const after = {
510
- vars: { ...p.request, ...Object.fromEntries(Object.keys(m.state).map(n => [`${n}'`, { op: 'var', name: n }])), ...Object.fromEntries(Object.entries(m.step.state ?? {}).map(([n, e]) => [`${n}'`, now(e)])) },
511
- maps: {
512
- ...Object.fromEntries(Object.keys(m.maps ?? {}).map(map => [`${map}'`, (key) => ({ op: 'get', map, key })])),
513
- ...Object.fromEntries(Object.entries(m.step.maps ?? {}).map(([map, w]) => [`${map}'`, (key) => ({ op: 'if', c: { op: '=', a: key, b: now(w.key) }, then: now(w.value), else: { op: 'get', map, key } })])),
514
- },
515
- flags,
516
- };
517
- const before = [...preOf(m), ...laws.flatMap(l => (l.forall ? (instances[l.name] ?? []).map(key => substC(l.holds, { vars: { [l.forall]: key }, maps: {}, flags: {} })) : [l.holds])), p.when];
531
+ // The run: a state where the laws and `when` hold, then the step the named request causes.
532
+ const before = holds(from(), [...preOf(m), ...assumed(laws, instances), p.when]);
533
+ const caused = step(reading(before, p.request), updateOf(m));
518
534
  return [
519
- ...(m.assume ? [{ name: `${p.name}: the request is allowed`, hyps: [...before, { op: 'not', x: substC(m.assume, req) }] }] : []),
520
- { name: `${p.name}: the step it causes`, hyps: [...before, { op: 'not', x: substC(p.then, after) }] },
535
+ ...(m.assume ? [goal(`${p.name}: the request is allowed`, before, substC(m.assume, req))] : []),
536
+ goal(`${p.name}: the step it causes`, caused, across(caused, p.then, Object.keys(m.state), Object.keys(m.maps ?? {}), { inputs: p.request, flags })),
521
537
  ];
522
538
  }
523
539
  export function stepLawObligations(machine, laws, instances, sl) {
524
540
  const m = machine.history ? historyMachine(machine).machine : machine;
525
541
  scope(m, laws);
542
+ const k = sl.steps ?? 1;
543
+ if (!Number.isInteger(k) || k < 1 || k > 16)
544
+ throw new Error(`step law ${sl.name}: steps is a whole number from 1 to 16`);
545
+ const whens = sl.when === undefined ? [] : Array.isArray(sl.when) ? sl.when : [sl.when];
546
+ if (whens.length > k)
547
+ throw new Error(`step law ${sl.name}: one when per step at most`);
526
548
  const state = Object.keys(m.state), maps = Object.keys(m.maps ?? {}), flagNames = Object.keys(m.step.flags ?? {});
527
549
  const primes = [...state, ...maps].map(n => `${n}'`);
550
+ const later = Array.from({ length: k - 1 }, (_, i) => m.inputs.map(input => `${input}@${i + 2}`)).flat();
528
551
  const taken = [...m.params, ...m.inputs, ...state, ...maps];
529
- for (const p of primes)
552
+ for (const p of [...primes, ...later])
530
553
  if (taken.includes(p))
531
- throw new Error(`${m.name}: ${p} is a name of the machine and a primed name of a step law`);
532
- if (sl.forall && [...taken, ...primes].includes(sl.forall))
554
+ throw new Error(`${m.name}: ${p} is a name of the machine and a name a step law reads`);
555
+ if (sl.forall && [...taken, ...primes, ...later].includes(sl.forall))
533
556
  throw new Error(`step law ${sl.name}'s key ${sl.forall} is already a name of ${m.name}`);
534
- inScope(`step law ${sl.name}`, [sl.then], [...taken, ...state.map(n => `${n}'`), ...(sl.forall ? [sl.forall] : [])], [...maps, ...maps.map(n => `${n}'`)], flagNames);
535
- if (sl.when)
536
- inScope(`step law ${sl.name}'s when`, [sl.when], taken, maps, flagNames);
537
- if (!naturalC(sl.then) || (sl.when && !naturalC(sl.when)))
557
+ inScope(`step law ${sl.name}`, [sl.then], [...taken, ...state.map(n => `${n}'`), ...later, ...(sl.forall ? [sl.forall] : [])], [...maps, ...maps.map(n => `${n}'`)], flagNames);
558
+ for (const w of whens)
559
+ inScope(`step law ${sl.name}'s when`, [w], taken, maps, flagNames);
560
+ if (!naturalC(sl.then) || !whens.every(naturalC))
538
561
  throw new Error(`step law ${sl.name}: a negative constant or factor is outside the kernel`);
539
- const flags = { vars: {}, maps: {}, flags: m.step.flags ?? {} };
540
- const now = (e) => substE(e, flags), nowC = (c) => substC(c, flags);
541
- const after = {
542
- vars: Object.fromEntries(state.map(n => [`${n}'`, now(m.step.state?.[n] ?? { op: 'var', name: n })])),
543
- maps: Object.fromEntries(maps.map(map => {
544
- const w = m.step.maps?.[map];
545
- return [`${map}'`, (key) => (w ? { op: 'if', c: { op: '=', a: key, b: now(w.key) }, then: now(w.value), else: { op: 'get', map, key } } : { op: 'get', map, key })];
546
- })),
547
- flags: m.step.flags ?? {},
548
- };
549
- const before = laws.flatMap(l => (l.forall ? (instances[l.name] ?? []).map(key => substC(l.holds, { vars: { [l.forall]: key }, maps: {}, flags: {} })) : [l.holds]));
550
- return [{ name: `${sl.name} @step law`, hyps: [...preOf(m), ...before, ...(m.assume ? [nowC(m.assume)] : []), ...(sl.when ? [nowC(sl.when)] : []), { op: 'not', x: substC(sl.then, after) }] }];
562
+ const u = updateOf(m), assume = m.assume ? [inlineFlags(m, m.assume)] : [];
563
+ let run = holds(from(), preOf(m));
564
+ for (let j = 1; j <= k; j++) {
565
+ if (j > 1)
566
+ run = reading(run, Object.fromEntries(m.inputs.map(input => [input, { op: 'var', name: `${input}@${j}` }])));
567
+ run = step(holds(run, [...assumed(laws, instances), ...assume, ...(whens[j - 1] ? [inlineFlags(m, whens[j - 1])] : [])]), u);
568
+ }
569
+ return [goal(k === 1 ? `${sl.name} @step law` : `${sl.name} @${k} steps`, run, across(run, sl.then, state, maps, { inputs: {}, flags: m.step.flags ?? {} }))];
551
570
  }
552
571
  // The laws' proof and every step law's tree: all must check.
553
572
  export function checkStepLaws(m, laws, stepLaws, proof) {
@@ -566,9 +585,9 @@ export function foldObligations(f, laws) {
566
585
  inScope(`${f.name}'s laws`, laws.map(law => law.holds), [...f.params, ...names]);
567
586
  const side = (p) => ({ vars: Object.fromEntries(names.map(n => [n, { op: 'var', name: `${p}${n}` }])), maps: {}, flags: {} });
568
587
  // A fold's inputs are each leaf's and node's own, so its assumption holds at every leaf and node it reads them at.
588
+ // A leaf is a start from its inputs; a node, a step from the frame of its two children, the laws holding at each.
569
589
  const assume = [...preOf(f), ...(f.assume ? [f.assume] : [])];
570
- return laws.flatMap(law => [
571
- { name: `${law.name} @leaf`, hyps: [...assume, { op: 'not', x: substC(law.holds, { vars: f.leaf, maps: {}, flags: {} }) }] },
572
- { name: `${law.name} @node`, hyps: [...assume, ...laws.flatMap(h => [substC(h.holds, side('l_')), substC(h.holds, side('r_'))]), { op: 'not', x: substC(law.holds, { vars: f.node, maps: {}, flags: {} }) }] },
573
- ]);
590
+ const leaf = holds(from({ vars: { ...f.leaf }, maps: {}, flags: {} }), assume);
591
+ const node = step(holds(from(), [...assume, ...laws.flatMap(h => [substC(h.holds, side('l_')), substC(h.holds, side('r_'))])]), { vars: f.node, maps: {} });
592
+ return laws.flatMap(law => [goal(`${law.name} @leaf`, leaf, atEnd(leaf, law.holds)), goal(`${law.name} @node`, node, atEnd(node, law.holds))]);
574
593
  }
@@ -269,7 +269,7 @@ export function proveStepLaws(m, laws, stepLaws) {
269
269
  if (!base.proven || !base.proof)
270
270
  return { proven: false, nodes: base.nodes, leaves: base.leaves, ...(base.failed ? { failed: base.failed } : {}), checked: false };
271
271
  const plain = m.history ? historyMachine(m).machine : m;
272
- const keys = [...readKeys(plain, laws), ...stepLaws.flatMap(sl => [...readKeysC(sl.then), ...(sl.when ? readKeysC(sl.when) : [])])];
272
+ const keys = [...readKeys(plain, laws), ...stepLaws.flatMap(sl => [sl.then, ...(sl.when === undefined ? [] : Array.isArray(sl.when) ? sl.when : [sl.when])].flatMap(readKeysC))];
273
273
  const instances = Object.fromEntries(laws.filter(l => l.forall).map(l => [l.name, keys]));
274
274
  const trees = {};
275
275
  let nodes = base.nodes, leaves = base.leaves;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdp-wired",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Laws about stateful rules (budgets, caps, windows, workflows, policy decisions), proven automatically by a small checked kernel, with the rule shipped as code",
5
5
  "keywords": [
6
6
  "verification",
@@ -32,6 +32,10 @@
32
32
  "types": "./dist/core/machine.d.ts",
33
33
  "default": "./dist/core/machine.js"
34
34
  },
35
+ "./compose": {
36
+ "types": "./dist/core/compose.d.ts",
37
+ "default": "./dist/core/compose.js"
38
+ },
35
39
  "./kernel": {
36
40
  "types": "./dist/kernel/kernel.d.ts",
37
41
  "default": "./dist/kernel/kernel.js"
@@ -65,7 +69,7 @@
65
69
  "build": "bun scripts/build.ts",
66
70
  "prepublishOnly": "bun scripts/build.ts",
67
71
  "smoke": "sh scripts/smoke.sh",
68
- "test": "cd core && bun test solver.test.ts dsl.test.ts conformance.test.ts && cd ../kernel && bun test tamper.test.ts refusals.test.ts steplaw.test.ts && cd ../examples && bun test examples.test.ts && cd .. && bun examples/readme.ts && bun examples/budget.ts"
72
+ "test": "cd core && bun test solver.test.ts dsl.test.ts conformance.test.ts compose.test.ts && cd ../kernel && bun test tamper.test.ts refusals.test.ts steplaw.test.ts && cd ../examples && bun test examples.test.ts && cd .. && bun examples/readme.ts && bun examples/composition.ts && bun examples/budget.ts"
69
73
  },
70
74
  "devDependencies": {
71
75
  "@types/bun": "1.3.1",