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 +19 -11
- package/dist/core/compose.d.ts +24 -0
- package/dist/core/compose.js +121 -0
- package/dist/core/dsl.d.ts +1 -0
- package/dist/core/dsl.js +48 -0
- package/dist/kernel/kernel.d.ts +2 -1
- package/dist/kernel/kernel.js +83 -64
- package/dist/kernel/prove.js +1 -1
- package/package.json +6 -2
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")
|
|
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
|
|
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:
|
|
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
|
-
|
|
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`),
|
|
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 (
|
|
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
|
+
}
|
package/dist/core/dsl.d.ts
CHANGED
|
@@ -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 });
|
package/dist/kernel/kernel.d.ts
CHANGED
|
@@ -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: {
|
package/dist/kernel/kernel.js
CHANGED
|
@@ -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
|
-
|
|
433
|
-
const
|
|
434
|
-
const
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
509
|
-
const
|
|
510
|
-
|
|
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 ? [
|
|
520
|
-
|
|
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
|
|
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
|
-
|
|
536
|
-
inScope(`step law ${sl.name}'s when`, [
|
|
537
|
-
if (!naturalC(sl.then) || (
|
|
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
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
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
|
-
|
|
571
|
-
|
|
572
|
-
|
|
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
|
}
|
package/dist/kernel/prove.js
CHANGED
|
@@ -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 => [
|
|
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.
|
|
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",
|