gdp-wired 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,12 +2,13 @@
2
2
 
3
3
  # gdp-wired
4
4
 
5
- Laws about stateful rules (budgets, caps, rate limits, access policies), proven in TypeScript by a small trusted
6
- kernel, with an untrusted prover that writes the proofs, an optional Lean twin that checks the kernel itself, and GPU
7
- searches that find counterexamples or the invariants a proof needs.
5
+ An automatic prover for stateful rules: budgets, caps, rate limits, workflows, access policies. Write the rule and the
6
+ laws it must keep; gdp-wired proves them with no proof to write, shows a real run that breaks them when they're false
7
+ (searched on the GPU), checks every proof again in Lean, and ships the rule as JavaScript.
8
8
 
9
- You write the rule once with typed builders and state what must always hold. The library proves it by induction (or
10
- shows a real run that breaks it), and the same rule runs as an interpreter or ships as generated JavaScript.
9
+ Proofs are checked by a small trusted kernel; the prover that writes them is untrusted, and the kernel's Lean twin is
10
+ proven sound. On the three rules in [`compare/`](compare/README.md), the proof is a one- or two-line call and checks in
11
+ 25–44 ms, against 74–117 lines of hand proof and 137–161 ms in Bend, and 8–30 lines and 488–571 ms in Lean.
11
12
 
12
13
  ```sh
13
14
  npm install gdp-wired # or bun add gdp-wired; plain JavaScript with types, Node 20+, Bun or a browser
@@ -33,34 +34,54 @@ Forget the cap (`s.plus(c)`) and the kernel refuses the proof; a search from the
33
34
  and `examples/domains/` holds 92 rules from nine domains (finance, access control, public services, operations,
34
35
  protocols, commerce, supply chains, games, education), each checked three ways.
35
36
 
36
- ## Scope: where it replaces Bend, and where it doesn't
37
+ ## Scope: what it proves by itself, and where the wall is
37
38
 
38
- gdp-wired is meant to replace Bend where it matters in practice, not to be a language: laws about **stateful rules whose
39
- state is counters, totals, maps of counters and logs summed over time**, proven automatically, with the rule shipped as
40
- code. If the checker side finds new frontiers the scope may grow; until then:
39
+ gdp-wired is an automatic prover, not a language: laws about **stateful rules** (counters, totals, maps, sets and lists
40
+ of names, logs summed over time, steps in a row, runs that go on forever), proven without a proof to write, with the
41
+ rule shipped as code. With Lean beside it, it says what Bend says; where a proof needs more than its kernel decides, the
42
+ proof is Lean's, as a fact about a function the rule reads:
41
43
 
42
44
  | It expresses and proves | It does not (today) |
43
45
  | --- | --- |
44
- | Step machines over naturals: scalars, maps from naturals to naturals (one write per map per step), named step conditions, preconditions on params, assumptions per request | Lists, sets or strings as values: membership, subsets, equality of lists of names |
45
- | Invariants of the state and params, laws about every key of a map; step laws relating one step's before and after (`s'`, `M'[k]`: "only increases", "stays retired"), and k-step laws about k steps in a row ("two grants on one day add two", up to 16 steps) | Orderings over any number of steps ("a refund never follows a release", at any distance, needs a state flag), beyond progress |
46
- | Composition: rules side by side (`product`), taking turns (`interleave`), and one rule refining another (`refinement`: a forward simulation, the spec allowing every request it is sent), each built as a plain machine so every law above applies | Refinement where the spec needs more than one step per step of the implementation |
47
- | Laws about one key of a map at a time, or about scalars | Totals across a map's keys ("the sum of every client's holdings equals in-use") |
48
- | Histories: sums and queries over a log of requests, relations between them proven one record at a time, pruning | Unbounded recursion over data, quantifiers over sequences other than a machine's own history |
49
- | Decisions over facts: flags, one-of choices, optional facts, bounded naturals (`match` covers every case) | Datatypes beyond those encodings; division, modulo, reals |
50
- | Loops that halt by a rank or a tuple of ranks; folds over binary trees; progress laws ("some request makes this happen") | Arbitrary algorithms: there is no general recursion to prove things about |
51
- | Arithmetic: `+`, truncated `−`, `×` by a constant, `min`, `max`, `if`, products of two terms (proven by the kernel, less well by the prover) | Generated code for decisions alone (`compileJs` emits machines; a predicate emitter is planned) |
52
-
53
- Keep Bend where the specification is about **lists or sets of names** (resource scopes, capability subsets, normalization
54
- that must refine its input), where a proof needs **induction over data structures**, or where the shipped code must be
55
- the compiled proved source of an arbitrary algorithm. Use gdp-wired where the rule is a budget, a cap, a window, a
56
- workflow or a policy decision: it proves those without proof files, finds counterexample runs when they're false, and
57
- checks its own kernel in Lean.
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.
58
76
 
59
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
60
78
  decisions, the compare-and-swap storage plans and the state/provider plans fit gdp-wired as it is (with names interned
61
79
  to numbers), and gdp-wired can prove more about storage than its Bend proofs state (no revision reuse across keys); the
62
- lifecycle machine's before/after laws are step laws (as in `release lifecycle` in `examples/`); the resource and authority checks, built on lists of names as
63
- sets, stay in Bend. `compare/` puts three rules side by side in gdp-wired, Bend and plain Lean, with line counts and
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
64
85
  check times.
65
86
 
66
87
  ## What's trusted
@@ -69,8 +90,8 @@ check times.
69
90
  | --- | --- | --- |
70
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 |
71
92
  | `kernel/prove.ts` | No | Grows the trees. If it's wrong, the kernel rejects the tree |
72
- | `lean-kernel/` | Optional | The kernel's twin in Lean 4.34.0, each rule proven sound (`Kern.sound`, `machine_sound`, `mmachine_sound`, `fmmachine_sound`, `loop_sound`, `fold_sound`, `progress_sound`, `steplaw_sound`, `kstep_sound`, `hmachine_sound`), axioms `propext`, `Classical.choice`, `Quot.sound` only. The exporter sends the kernel's real trees; tampered ones compute to false |
73
- | `search/`, `synth/`, `hw/` | No | GPU work: counterexample search over every short history, and invariant/rank synthesis. Every hit is replayed by the interpreter or proven by the kernel before it counts |
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 |
74
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 |
75
96
 
76
97
  How the untrusted parts are held to the trusted ones, automatically:
@@ -83,6 +104,9 @@ How the untrusted parts are held to the trusted ones, automatically:
83
104
  evaluator, and the same cases through Lean's own `evalE` (`lean-kernel/conformance.ts`).
84
105
  - **Examples** (`examples/`): every law proven by the kernel and re-checked by Lean, every rule run at random through
85
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.
86
110
  - **Tampering and mutation** (`kernel/tamper.test.ts`, `kernel/refusals.test.ts`, `kernel/mutants.ts`): real proofs
87
111
  broken structurally or offered for weakened goals that have a model must be rejected, every refusal the kernel makes is
88
112
  exercised, and every one-operator mutation of `kernel.ts` and `check.ts` is run against the tests to find what they
@@ -138,11 +162,30 @@ const failsClosed = trial.decide('a missing fact means no trial', ({spent}) => s
138
162
  machine reads.
139
163
  - **Arithmetic** over the naturals: `plus`, `minus` (truncated), `times(k)`, `mul` (a product of two terms), `min`,
140
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).
141
177
  - **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})`
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)})`
143
184
  build plain machines from machines (each side's state named `a_x`, `b_x`). A refinement's laws are the relation, proven
144
185
  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.
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.
146
189
  - **Running it**: `rule.interpreter()` is persistent (searches branch from any state); `rule.compiled()` is what an app
147
190
  ships: maps and the log updated in place, so a step costs the same however long the history. Both refuse params and
148
191
  inputs that aren't naturals below 2^53 and any value past 2^53 − 1, where the proofs over the naturals stop describing
@@ -194,7 +237,7 @@ its Lean twin in 1.35 s. What each proves, what each ships at runtime, and where
194
237
  | `next/` | Guards synthesized from laws, relational laws (migrations, tenant isolation, through `core/compose.ts`), a ReBAC audit |
195
238
  | `sat/` | Bit-vector laws: bit-blasted, solved, LRAT-checked in TypeScript, optionally by Lean |
196
239
  | `enum/`, `perf/` | The enumeration, and the benchmarks |
197
- | `examples/` | The builders at work: `domains/` (92 rules across nine domains, each with laws and broken variants), `harness.ts` (the triple check), `readme.ts`, `budget.ts`, `composition.ts` |
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` |
198
241
  | `compare/` | The same three rules in gdp-wired, Bend and plain Lean, checked and measured |
199
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 |
200
243
  | `modal/` | Everything above as Modal jobs: `suite.json` is the regression suite |
@@ -204,7 +247,7 @@ its Lean twin in 1.35 s. What each proves, what each ships at runtime, and where
204
247
  ## Running
205
248
 
206
249
  The npm package is the library: the builders (`gdp-wired`), the runtimes (`gdp-wired/machine`), composition
207
- (`gdp-wired/compose`), the kernel
250
+ (`gdp-wired/compose`), the Lean export (`gdp-wired/lean`), the kernel
208
251
  (`gdp-wired/kernel`), the prover (`gdp-wired/prove`), the derivation checker and solver (`gdp-wired/check`,
209
252
  `gdp-wired/solver`). Everything else here (examples, comparisons, the Lean twin, the GPU searches) runs from a clone and
210
253
  needs [Bun](https://bun.sh); Lean 4.34.0 for `lean/` and `lean-kernel/`; a WebGPU runtime (Chrome, or Dawn's node
@@ -218,7 +261,7 @@ bun test core/compile.test.ts core/runtime.test.ts kernel/history.test.ts
218
261
  cd kernel && bun run-kernel.ts && bun test kernel.test.ts
219
262
  ```
220
263
 
221
- The whole suite (110 jobs, about 15 minutes of wall time) runs on [Modal](https://modal.com):
264
+ The whole suite (122 jobs, about 20 minutes of wall time) runs on [Modal](https://modal.com):
222
265
 
223
266
  ```sh
224
267
  modal run modal/run.py --jobs modal/suite.json --out runs/modal/suite && sh modal/reap.sh
@@ -1,5 +1,5 @@
1
1
  import { type C, type E, type Law, type Machine } from './machine.js';
2
- import type { StepLaw } from '../kernel/kernel.js';
2
+ import { type StepLaw } from '../kernel/kernel.js';
3
3
  export declare const side: (m: Machine, prefix: "a_" | "b_", c: C) => C;
4
4
  export declare function product(a: Machine, b: Machine, opts?: {
5
5
  name?: string;
@@ -15,10 +15,24 @@ export declare function interleave(a: Machine, b: Machine, opts?: {
15
15
  export declare function refinement(impl: Machine, spec: Machine, opts: {
16
16
  relation: Law[];
17
17
  witness: Record<string, E>;
18
+ stutter?: C;
18
19
  name?: string;
19
20
  }): {
20
21
  machine: Machine;
21
22
  laws: Law[];
22
23
  stepLaws: StepLaw[];
23
24
  };
25
+ export declare function refinementSides(impl: Machine, spec: Machine): {
26
+ impl: Machine;
27
+ spec: Machine;
28
+ };
24
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;
@@ -3,18 +3,24 @@
3
3
  // is trusted: a proof is about the machine built, and the combinators are tested against runs of their parts.
4
4
  // product(a, b) both step on every request (their params and inputs shared by name)
5
5
  // interleave(a, b) one of them steps on each request, `turn` picking which
6
- // refinement(impl, spec, {relation, witness})
6
+ // refinement(impl, spec, {relation, witness, stutter})
7
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)
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
9
13
  // Each side's state, maps and flags are renamed a_x and b_x; params and inputs keep their names.
10
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";
11
17
  function rename(e, prefix, m, inputs) {
12
18
  switch (e.op) {
13
19
  case 'var': return m.params.includes(e.name) ? e : m.inputs.includes(e.name) ? (inputs[e.name] ?? e) : x(`${prefix}${e.name}`);
14
20
  case 'const': return e;
15
21
  case 'mul': return { ...e, a: rename(e.a, prefix, m, inputs) };
16
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) };
17
- case 'get': return { op: 'get', map: `${prefix}${e.map}`, key: rename(e.key, 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) };
18
24
  case 'add':
19
25
  case 'sub':
20
26
  case 'min':
@@ -30,6 +36,8 @@ function renameC(c, prefix, m, inputs) {
30
36
  return { op: 'not', x: renameC(c.x, prefix, m, inputs) };
31
37
  if (c.op === 'and' || c.op === 'or')
32
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
33
41
  return { ...c, a: rename(c.a, prefix, m, inputs), b: rename(c.b, prefix, m, inputs) };
34
42
  }
35
43
  // A condition (or law) of one side, read in the composed machine.
@@ -46,6 +54,8 @@ function renamed(m, p, inputs, stays) {
46
54
  flags: Object.fromEntries(Object.entries(m.step.flags ?? {}).map(([n, c]) => [`${p}${n}`, renameC(c, p, m, inputs)])),
47
55
  state: Object.fromEntries(Object.entries(m.step.state ?? {}).map(([n, e]) => [`${p}${n}`, keep(rename(e, p, m, inputs), x(`${p}${n}`))])),
48
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}`) }) }];
49
59
  const key = rename(w.key, p, m, inputs);
50
60
  return [`${p}${n}`, { key, value: keep(rename(w.value, p, m, inputs), { op: 'get', map: `${p}${n}`, key }) }];
51
61
  })),
@@ -57,6 +67,8 @@ const join = (a, b) => ({
57
67
  state: { ...a.state, ...b.state }, maps: { ...a.maps, ...b.maps },
58
68
  step: { flags: { ...a.step.flags, ...b.step.flags }, state: { ...a.step.state, ...b.step.state }, maps: { ...a.step.maps, ...b.step.maps } },
59
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 } : {}; };
60
72
  const conj = (cs) => { const xs = cs.filter((c) => c !== undefined); return xs.length ? all(...xs) : undefined; };
61
73
  // a and b stepped by the same requests. `bInputs` rewrites what b sees (other tenants' requests as no-ops, say).
62
74
  export function product(a, b, opts = {}) {
@@ -66,7 +78,7 @@ export function product(a, b, opts = {}) {
66
78
  name: opts.name ?? `${a.name} | ${b.name}`,
67
79
  params: [...new Set([...a.params, ...b.params, ...(opts.params ?? [])])],
68
80
  inputs: [...new Set([...a.inputs, ...b.inputs, ...(opts.inputs ?? [])])],
69
- ...join(A, B),
81
+ ...join(A, B), ...carriedBy(a, b),
70
82
  ...(assume ? { assume } : {}),
71
83
  ...(pre ? { pre } : {}),
72
84
  };
@@ -84,7 +96,7 @@ export function interleave(a, b, opts = {}) {
84
96
  name: opts.name ?? `${a.name} || ${b.name}`,
85
97
  params: [...new Set([...a.params, ...b.params])],
86
98
  inputs: [...new Set([...a.inputs, ...b.inputs, turn])],
87
- ...join(A, B),
99
+ ...join(A, B), ...carriedBy(a, b),
88
100
  assume: all(bit(turn), ...(A.assume ? [any(not(isA), A.assume)] : []), ...(B.assume ? [any(not(isB), B.assume)] : [])),
89
101
  ...(pre ? { pre } : {}),
90
102
  };
@@ -98,13 +110,16 @@ export function interleave(a, b, opts = {}) {
98
110
  export function refinement(impl, spec, opts) {
99
111
  if (!spec.inputs.every(i => i in opts.witness))
100
112
  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;
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;
103
118
  const machine = {
104
119
  name: opts.name ?? `${impl.name} refines ${spec.name}`,
105
120
  params: [...new Set([...impl.params, ...spec.params])],
106
121
  inputs: impl.inputs,
107
- ...join(A, B),
122
+ ...join(A, B), ...carriedBy(impl, spec),
108
123
  ...(A.assume ? { assume: A.assume } : {}),
109
124
  ...(impl.pre ? { pre: impl.pre } : {}),
110
125
  };
@@ -112,6 +127,16 @@ export function refinement(impl, spec, opts) {
112
127
  const stepLaws = allowedBy ? [{ name: `${spec.name} allows the request it is sent`, then: allowedBy }] : [];
113
128
  return { machine, laws, stepLaws };
114
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
+ }
115
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.
116
141
  export function agree(a) {
117
142
  return [
@@ -119,3 +144,102 @@ export function agree(a) {
119
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' })),
120
145
  ];
121
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
+ }
@@ -1,5 +1,5 @@
1
- import { type C, type E, type Law, type Machine, type State } from './machine.js';
2
- import { type Goal, type Loop, type StepLaw, type Tree } from '../kernel/kernel.js';
1
+ import { type C, type E, type Law, type Machine, type Request, type State } from './machine.js';
2
+ import { type Goal, type Liveness, type Loop, type StepLaw, type Tree } from '../kernel/kernel.js';
3
3
  export type TermLike = Term | number;
4
4
  export declare const termOf: (t: TermLike) => E;
5
5
  export declare class Term {
@@ -87,7 +87,13 @@ export type MapWrite = {
87
87
  readonly map: string;
88
88
  readonly key: E;
89
89
  readonly value: E;
90
+ } | {
91
+ readonly map: string;
92
+ readonly each: string;
93
+ readonly value: E;
90
94
  };
95
+ export declare const everyKey: (f: (key: Term) => Cond) => Cond;
96
+ export declare const someKey: (f: (key: Term) => Cond, witness?: TermLike) => Cond;
91
97
  export declare class MapRef {
92
98
  readonly name: string;
93
99
  readonly init: number;
@@ -95,16 +101,57 @@ export declare class MapRef {
95
101
  at(key: TermLike): Term;
96
102
  set(key: TermLike, value: TermLike): MapWrite;
97
103
  bump(key: TermLike, c?: Cond, by?: TermLike): MapWrite;
104
+ each(f: (key: Term) => TermLike): MapWrite;
105
+ has(key: TermLike): Cond;
106
+ subsetOf(other: MapRef): Cond;
107
+ disjointFrom(other: MapRef): Cond;
108
+ sameAs(other: MapRef): Cond;
109
+ within(n: number): Cond;
110
+ sizeBelow(n: number): Term;
111
+ }
112
+ export type ListOp = {
113
+ readonly when: Cond;
114
+ readonly at: (k: Term) => TermLike;
115
+ readonly len: TermLike;
116
+ };
117
+ export declare class ListRef {
118
+ readonly name: string;
119
+ readonly len: Term;
120
+ constructor(name: string, len: Term);
121
+ at(i: TermLike): Term;
122
+ has(x: TermLike): Cond;
123
+ sorted(): Cond;
124
+ push(x: TermLike, when?: Cond): ListOp;
125
+ pop(when?: Cond): ListOp;
126
+ set(i: TermLike, x: TermLike, when?: Cond): ListOp;
127
+ insertAt(i: TermLike, x: TermLike, when?: Cond): ListOp;
128
+ removeAt(i: TermLike, when?: Cond): ListOp;
129
+ apply(...ops: ListOp[]): Record<string, TermLike | MapWrite>;
130
+ within(): Forall;
131
+ increasing(): Forall;
98
132
  }
99
133
  export type Forall = {
100
- readonly forall: (key: Term) => Cond;
134
+ readonly forall: (key: Term, ...more: Term[]) => Cond;
101
135
  };
102
- export declare const forall: (f: (key: Term) => Cond) => Forall;
136
+ export declare function forall(f: (key: Term, key2: Term, key3: Term, key4: Term) => Cond): Forall;
103
137
  export type Relation = {
104
138
  readonly when: Cond;
105
139
  readonly holds: Cond;
106
140
  };
107
141
  export declare const relation: (holds: Cond, whenever?: Cond) => Relation;
142
+ export declare function nameQuantifiers(m: Machine): Machine;
143
+ export type FnDef = {
144
+ arity: number;
145
+ js: string | ((...args: number[]) => number);
146
+ lean?: string;
147
+ facts?: (f: (...args: TermLike[]) => Term) => Record<string, Cond | Forall>;
148
+ };
149
+ export declare const fn: (arity: number, js: FnDef["js"], opts?: {
150
+ lean?: string;
151
+ facts?: FnDef["facts"];
152
+ }) => FnDef;
153
+ export declare const divBy: (c: number) => FnDef;
154
+ export declare const modBy: (c: number) => FnDef;
108
155
  type Kinds = Record<string, Kind<unknown>>;
109
156
  type ValuesOf<K extends Kinds> = {
110
157
  [n in keyof K]: K[n] extends Kind<infer V> ? V : never;
@@ -121,6 +168,10 @@ type Spec = {
121
168
  inputs: [string, Kind<unknown>][];
122
169
  state: [string, number | ((p: never) => TermLike)][];
123
170
  maps: [string, number][];
171
+ lists?: string[];
172
+ carried?: string[];
173
+ fns?: [string, FnDef][];
174
+ facts?: ((env: never) => Record<string, Cond | Forall>)[];
124
175
  flags: ((env: never) => Record<string, Cond>)[];
125
176
  step?: (env: never) => Record<string, TermLike | MapWrite | undefined>;
126
177
  pre: ((p: never) => Cond)[];
@@ -148,6 +199,18 @@ export declare class Rule<P extends Obj = {}, I extends Obj = {}, S extends Obj
148
199
  maps<T extends Record<string, number>>(init: T): Rule<P, I, S, M & {
149
200
  [k in keyof T]: MapRef;
150
201
  }, F, R, Su, Q>;
202
+ lists<const N extends string>(...names: N[]): Rule<P, I, S & {
203
+ [k in N as `${k}_len`]: Term;
204
+ }, M & {
205
+ [k in N]: ListRef;
206
+ }, F, R, Su, Q>;
207
+ fns<T extends Record<string, FnDef>>(defs: T): Rule<P & {
208
+ [k in keyof T]: (...args: TermLike[]) => Term;
209
+ }, I, S, M, F, R, Su, Q>;
210
+ facts(f: (p: P) => Record<string, Cond | Forall>): Rule<P, I, S, M, F, R, Su, Q>;
211
+ carries<const N extends string>(...names: N[]): Rule<P, I & {
212
+ [k in N]: MapRef;
213
+ }, S, M, F, R, Su, Q>;
151
214
  flags<T extends Record<string, Cond>>(f: (env: P & I & S & M & F & Su & Q) => T): Rule<P, I, S, M, F & {
152
215
  [k in keyof T]: Cond;
153
216
  }, R, Su, Q>;
@@ -165,8 +228,18 @@ export declare class Rule<P extends Obj = {}, I extends Obj = {}, S extends Obj
165
228
  relations(f: (env: P & I & Su & Q) => Relation[]): Rule<P, I, S, M, F, R, Su, Q>;
166
229
  prune(order: keyof R & string, after: (p: P) => TermLike): Rule<P, I, S, M, F, R, Su, Q>;
167
230
  law(name: string, f: (env: P & S & M & Su) => Cond | Forall): Law;
231
+ private lawOf;
168
232
  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
233
  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;
234
+ leadsTo(name: string, spec: {
235
+ from: (env: P & S & M & Su) => Cond;
236
+ to: (env: P & S & M & Su) => Cond;
237
+ rank: (env: P & S & M & Su) => TermLike | TermLike[];
238
+ helpful: ((env: P & S & M & Su) => Record<string, TermLike>) | {
239
+ when?: (env: P & S & M & Su) => Cond;
240
+ request: (env: P & S & M & Su) => Record<string, TermLike>;
241
+ }[];
242
+ }): Liveness;
170
243
  decide(name: string, given: (env: P & I) => Cond | Cond[], then: (env: P & I) => Cond): Decision;
171
244
  private kindAssumptions;
172
245
  private env;
@@ -175,10 +248,10 @@ export declare class Rule<P extends Obj = {}, I extends Obj = {}, S extends Obj
175
248
  [k in keyof P]: Raw;
176
249
  }): Record<string, number>;
177
250
  encodeInput(values: {
178
- [k in keyof I]: Raw;
179
- }): Record<string, number>;
251
+ [k in keyof I]: I[k] extends MapRef ? ReadonlySet<number> | readonly number[] | ReadonlyMap<number, number> : Raw;
252
+ }): Request;
180
253
  randomParams(rand: Rand, bound: number): Record<string, number>;
181
- randomInput(rand: Rand, bound: number): Record<string, number>;
254
+ randomInput(rand: Rand, bound: number): Request;
182
255
  get orderedBy(): string | undefined;
183
256
  interpreter(): {
184
257
  init: (params: {
@@ -186,16 +259,12 @@ export declare class Rule<P extends Obj = {}, I extends Obj = {}, S extends Obj
186
259
  }) => State;
187
260
  queries: (params: {
188
261
  [x: string]: number;
189
- }, st: State, input: {
190
- [x: string]: number;
191
- }) => {
262
+ }, st: State, input: Request) => {
192
263
  [x: string]: number;
193
264
  };
194
265
  step: (params: {
195
266
  [x: string]: number;
196
- }, st: State, input: {
197
- [x: string]: number;
198
- }) => State;
267
+ }, given: State, input: Request) => State;
199
268
  };
200
269
  compiled(): {
201
270
  init: (params: object) => State & {
@@ -264,5 +333,7 @@ export type Verdict = {
264
333
  export declare function proveRule(r: Rule<Obj, Obj, Obj, Obj, Obj, Obj, Obj, Obj>, laws: Law[]): Verdict;
265
334
  export declare function proveSteps(r: Rule<Obj, Obj, Obj, Obj, Obj, Obj, Obj, Obj>, laws: Law[], stepLaws: StepLaw[]): Verdict;
266
335
  export type { StepLaw };
336
+ export type { Liveness };
337
+ export declare function proveLeadsTo(r: Rule<Obj, Obj, Obj, Obj, Obj, Obj, Obj, Obj>, laws: Law[], l: Liveness): Verdict;
267
338
  export declare const proveDecision: (d: Decision) => Verdict;
268
339
  export declare const proveLoop: (l: LoopRule<Obj, Obj, Obj>, laws: Law[]) => Verdict;