gdp-wired 0.12.6 → 0.13.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
@@ -60,6 +60,9 @@ All of these are proven automatically, and each proof is also a Lean theorem:
60
60
  - **Step laws**: what one step does ("a balance only goes down", "a grant adds one"), and laws over up to 64 steps in
61
61
  a row (`examples/steps.ts`).
62
62
  - **Histories**: sums and queries over a log of requests, like a sliding-window rate limit.
63
+ - **Statecharts**: `chart(name).states({...}).on(event, {from, to, when, set})`, with nested states, parallel regions,
64
+ a compound's history, guards and extended state, compiled to a rule; `check()` shows every state reached by a run (or
65
+ proves it unreachable), no state stuck and no event ambiguous, and its laws prove like any rule's (`examples/charts.ts`).
63
66
  - **Orderings at any distance**: "a refund never follows a release", through ghost state that watches the rule.
64
67
  - **Liveness**: "every ticket issued is served", in every run that keeps sending the requests that help, including under
65
68
  weak fairness (`examples/liveness.ts`).
@@ -316,7 +319,7 @@ const soon = retry.law('the next try is at most 64 after the last request', ({ne
316
319
  - **Running it**: `rule.interpreter()` is persistent (searches branch from any state); `rule.compiled()` is what an app
317
320
  ships: maps and the log updated in place, so a step costs the same however long the history. Both refuse params and
318
321
  inputs that aren't naturals below 2^53 and any value past 2^53 − 1, where the proofs over the naturals stop describing
319
- the run. The GPU searches refuse a space whose values could pass 2^32 − 1.
322
+ the run, and a request that fails the rule's `assume` (no proof covers the step it would take). The GPU searches refuse a space whose values could pass 2^32 − 1.
320
323
 
321
324
  ## Numbers
322
325
 
@@ -0,0 +1,86 @@
1
+ import { type Cond, type Kind, type Rule, type Term, type TermLike, type Verdict, type Choice } from './dsl.js';
2
+ import { type Law } from './machine.js';
3
+ import type { StepLaw } from '../kernel/kernel.js';
4
+ export type StateNode = {
5
+ states?: Record<string, StateNode>;
6
+ initial?: string;
7
+ history?: boolean;
8
+ final?: boolean;
9
+ };
10
+ type Obj = Record<string, any>;
11
+ type Env<P, I, X> = P & I & X & {
12
+ event: Choice<string>;
13
+ in: (path: string) => Cond;
14
+ last: (compound: string) => Choice<string>;
15
+ };
16
+ type After<X> = X & {
17
+ in: (path: string) => Cond;
18
+ last: (compound: string) => Choice<string>;
19
+ };
20
+ export type Transition<P, I, X> = {
21
+ from: string | string[];
22
+ to: string;
23
+ when?: (env: Env<P, I, X>) => Cond;
24
+ set?: (env: Env<P, I, X>) => Partial<{
25
+ [k in keyof X]: TermLike | Cond | X[k];
26
+ }>;
27
+ };
28
+ export declare class Chart<P extends Obj = {}, I extends Obj = {}, X extends Obj = {}> {
29
+ private readonly s;
30
+ constructor(s: {
31
+ name: string;
32
+ params: Obj;
33
+ inputs: Obj;
34
+ context: Obj;
35
+ regions: [string, StateNode][];
36
+ on: [string, Transition<P, I, X>][];
37
+ });
38
+ private with;
39
+ params<K extends Record<string, Kind<unknown>>>(kinds: K): Chart<P & {
40
+ [k in keyof K]: K[k] extends Kind<infer V> ? V : never;
41
+ }, I, X>;
42
+ inputs<K extends Record<string, Kind<unknown>>>(kinds: K): Chart<P, I & {
43
+ [k in keyof K]: K[k] extends Kind<infer V> ? V : never;
44
+ }, X>;
45
+ context<T extends Record<string, number | {
46
+ kind: Kind<unknown>;
47
+ init: unknown;
48
+ }>>(init: T): Chart<P, I, X & {
49
+ [k in keyof T]: T[k] extends {
50
+ kind: Kind<infer V>;
51
+ } ? V : Term;
52
+ }>;
53
+ states(tree: Record<string, StateNode>, opts?: {
54
+ initial?: string;
55
+ }): Chart<P, I, X>;
56
+ regions(regions: Record<string, StateNode>): Chart<P, I, X>;
57
+ on(event: string, t: Transition<P, I, X>): Chart<P, I, X>;
58
+ private built?;
59
+ private build;
60
+ get rule(): Rule<Obj, Obj, Obj, Obj, Obj, Obj, Obj, Obj>;
61
+ in(env: Obj, path: string): Cond;
62
+ last(env: Obj, compound: string): Choice<string>;
63
+ private wrap;
64
+ law(name: string, f: (env: After<P & X>) => Cond): Law;
65
+ stepLaw(name: string, then: (before: Env<P, I, X>, after: After<X>) => Cond, when?: (before: Env<P, I, X>) => Cond): StepLaw;
66
+ chartLaws(): Law[];
67
+ prove(laws?: Law[], stepLaws?: StepLaw[]): Verdict;
68
+ check(opts?: {
69
+ params?: Obj;
70
+ bound?: number;
71
+ maxStates?: number;
72
+ }): {
73
+ ok: boolean;
74
+ reached: Record<string, {
75
+ run: Obj[];
76
+ } | 'unreachable' | 'unknown'>;
77
+ stuck: string[];
78
+ ambiguous: {
79
+ event: string;
80
+ transitions: [number, number];
81
+ verdict: Verdict;
82
+ }[];
83
+ };
84
+ }
85
+ export declare const chart: (name: string) => Chart;
86
+ export {};
@@ -0,0 +1,241 @@
1
+ // Statecharts as rules: states (nested, each compound with its initial child and, if asked, the child it was last in),
2
+ // parallel regions, events with a payload, guarded transitions that set the chart's extended state, final states. A chart
3
+ // compiles to one plain rule, its step a choice among transitions, so everything proven about rules holds of charts and
4
+ // nothing here is trusted: the kernel proves the rule it builds.
5
+ //
6
+ // const order = chart('order').context({retries: 0}).states({
7
+ // new: {}, active: {states: {paid: {}, shipped: {}}, history: true}, refunded: {final: true}, delivered: {final: true},
8
+ // })
9
+ // .on('pay', {from: 'new', to: 'active'})
10
+ // .on('ship', {from: 'active.paid', to: 'active.shipped'})
11
+ // .on('refund', {from: 'active.paid', to: 'refunded'})
12
+ // .on('deliver', {from: 'active.shipped', to: 'delivered'})
13
+ // order.check() // every state reached (with the run that reaches it), none stuck, no event ambiguous
14
+ //
15
+ // Semantics, per region and per event: of the transitions whose source holds and whose guard holds, the one with the
16
+ // most specific source (an inner state over its parent) is taken, then the first declared; the regions react to the
17
+ // same event at once. A transition into a compound state enters its initial child, or the child it was last in when the
18
+ // compound keeps its history; `set` writes the extended state (a variable two regions both set takes the first region's).
19
+ import { all, any, oneOf, proveRule, proveSteps, rule, when } from "./dsl.js";
20
+ import { interpret, numbersOf } from "./machine.js";
21
+ import { allowed } from "./watch.js";
22
+ export class Chart {
23
+ s;
24
+ constructor(s) {
25
+ this.s = s;
26
+ }
27
+ with(more) { return new Chart({ ...this.s, ...more }); }
28
+ params(kinds) { return this.with({ params: { ...this.s.params, ...kinds } }); }
29
+ // The payload an event brings (every event brings the same fields; one reads those it needs).
30
+ inputs(kinds) { return this.with({ inputs: { ...this.s.inputs, ...kinds } }); }
31
+ // The extended state: a natural from a number, or any kind from `kind.from(init)`.
32
+ context(init) { return this.with({ context: { ...this.s.context, ...init } }); }
33
+ // One region's states (a chart of one region names its states plainly: 'active.paid').
34
+ states(tree, opts = {}) { return this.with({ regions: [['state', { states: tree, initial: opts.initial }]] }); }
35
+ // Parallel regions, each with its own states, reacting to the same events: paths start with the region ('payment.paid').
36
+ regions(regions) { return this.with({ regions: Object.entries(regions) }); }
37
+ on(event, t) { return this.with({ on: [...this.s.on, [event, t]] }); }
38
+ // --- the rule it compiles to -------------------------------------------------------------------------------------
39
+ built;
40
+ build() {
41
+ if (this.built)
42
+ return this.built;
43
+ const s = this.s, single = s.regions.length === 1 && s.regions[0][0] === 'state';
44
+ if (!s.regions.length)
45
+ throw new Error(`${s.name}: a chart needs states`);
46
+ const regions = s.regions.map(([name, root]) => {
47
+ const leaves = [], finals = new Set(), compounds = new Map();
48
+ const walk = (path, node) => {
49
+ const kids = Object.entries(node.states ?? {});
50
+ if (!kids.length) {
51
+ leaves.push(path);
52
+ if (node.final)
53
+ finals.add(path);
54
+ return;
55
+ }
56
+ const initial = node.initial ?? kids[0][0];
57
+ if (!node.states[initial])
58
+ throw new Error(`${s.name}: ${path || name}'s initial state ${initial} isn't one of its states`);
59
+ compounds.set(path, { initial, history: !!node.history });
60
+ for (const [k, kid] of kids) {
61
+ if (/[.*[\],:]/.test(k))
62
+ throw new Error(`${s.name}: the state name ${k} has a reserved character`);
63
+ walk(path ? `${path}.${k}` : k, kid);
64
+ }
65
+ };
66
+ walk('', root);
67
+ return { name, field: single ? 'state' : name, leaves, finals, compounds };
68
+ });
69
+ // A path as the region and the state within it.
70
+ const locate = (path) => {
71
+ if (single)
72
+ return [regions[0], path];
73
+ const [head, ...rest] = path.split('.'), r = regions.find(x => x.name === head);
74
+ if (!r)
75
+ throw new Error(`${s.name}: ${path} names no region (${regions.map(x => x.name).join(', ')})`);
76
+ return [r, rest.join('.')];
77
+ };
78
+ const under = (r, path) => {
79
+ const out = r.leaves.filter(l => path === '' || l === path || l.startsWith(`${path}.`));
80
+ if (!out.length)
81
+ throw new Error(`${s.name}: no state ${path} in ${r.name}`);
82
+ return out;
83
+ };
84
+ const events = [...new Set(s.on.map(([e]) => e))];
85
+ if (!events.length)
86
+ throw new Error(`${s.name}: a chart needs transitions`);
87
+ const placed = s.on.map(([event, t], order) => {
88
+ const froms = (Array.isArray(t.from) ? t.from : [t.from]).map(locate), [to] = locate(t.to);
89
+ if (froms.some(([r]) => r !== to))
90
+ throw new Error(`${s.name}: a transition on ${event} goes from one region to another (${t.to})`);
91
+ under(to, locate(t.to)[1]);
92
+ const sources = [...new Set(froms.flatMap(([r, p]) => under(r, p)))];
93
+ return { ...t, event, region: to.name, sources, depth: Math.min(...froms.map(([, p]) => (p === '' ? 0 : p.split('.').length))), order };
94
+ });
95
+ // Most specific source first, then the order declared: the first enabled in this order is taken.
96
+ placed.sort((a, b) => b.depth - a.depth || a.order - b.order);
97
+ const lastOf = (r, c) => `${r.field}.last${c ? `.${c}` : ''}`;
98
+ const state = {};
99
+ for (const r of regions) {
100
+ const kind = oneOf(...r.leaves), enter = (path) => { const c = r.compounds.get(path); return c ? enter(path ? `${path}.${c.initial}` : c.initial) : path; };
101
+ state[r.field] = kind.from(enter(''));
102
+ for (const [c, { history }] of r.compounds)
103
+ if (history && c)
104
+ state[lastOf(r, c)] = kind.from(enter(c));
105
+ }
106
+ for (const [k, v] of Object.entries(s.context))
107
+ state[k] = v;
108
+ const inState = (env) => (path) => { const [r, p] = locate(path); return any(...under(r, p).map(l => env[r.field].is(l))); };
109
+ const guardEnv = (env) => this.wrap(env);
110
+ // Entering a path: a leaf, or a compound's last child (with history) or its initial one.
111
+ const entered = (env, r, path) => {
112
+ const c = r.compounds.get(path), field = env[r.field];
113
+ if (!c)
114
+ return field.of(path);
115
+ if (c.history && path)
116
+ return env[lastOf(r, path)];
117
+ return entered(env, r, path ? `${path}.${c.initial}` : c.initial);
118
+ };
119
+ let built = rule(s.name).params(s.params).inputs({ event: oneOf(...events), ...s.inputs }).state(state);
120
+ built = built.flags((env) => Object.fromEntries(placed.map((t, i) => [`on${i}`, all(env.event.is(t.event), inState(env)(single ? '' : t.region), any(...t.sources.map(l => env[regions.find(r => r.name === t.region).field].is(l))), ...(t.when ? [t.when(guardEnv(env))] : []))])));
121
+ // A transition is taken when it is enabled and no transition before it in its region is.
122
+ built = built.flags((env) => Object.fromEntries(placed.map((t, i) => [`take${i}`, all(env[`on${i}`], ...placed.slice(0, i).flatMap((u, j) => (u.region === t.region && u.sources.some(l => t.sources.includes(l)) ? [env[`on${j}`].not()] : [])))])));
123
+ built = built.step((env) => {
124
+ const out = {};
125
+ for (const r of regions) {
126
+ // The transitions taken in a region exclude one another: a chain of `when`s, the first in order outermost.
127
+ out[r.field] = placed.map((t, i) => [t, i]).filter(([t]) => t.region === r.name).reverse()
128
+ .reduce((acc, [t, i]) => when(env[`take${i}`], entered(env, r, locate(t.to)[1]), acc), env[r.field]);
129
+ for (const [c, { history }] of r.compounds)
130
+ if (history && c)
131
+ out[lastOf(r, c)] = when(any(...under(r, c).map(l => out[r.field].is(l))), out[r.field], env[lastOf(r, c)]);
132
+ }
133
+ for (const k of Object.keys(s.context)) {
134
+ const sets = placed.map((t, i) => [t, i]).filter(([t]) => t.set).map(([t, i]) => [i, t.set(guardEnv(env))[k]]).filter(([, v]) => v !== undefined);
135
+ if (sets.length)
136
+ out[k] = sets.reverse().reduce((acc, [i, v]) => when(env[`take${i}`], v, acc), env[k]);
137
+ }
138
+ return out;
139
+ });
140
+ this.built = { rule: built, regions, placed, events };
141
+ return this.built;
142
+ }
143
+ // The rule the chart compiles to: its state is each region's state (`state`, or the region's name), each compound's
144
+ // last child where it keeps its history, and the context; its inputs the event and the payload.
145
+ get rule() { return this.build().rule; }
146
+ // Whether a region is in a state (a compound: any state within it), for laws: chart.in(env, 'active').
147
+ in(env, path) {
148
+ const { regions } = this.build(), single = regions.length === 1 && regions[0].name === 'state';
149
+ const [head, ...rest] = single ? ['state', ...path.split('.')] : path.split('.'), r = regions.find(x => x.name === head);
150
+ const p = rest.join('.'), leaves = r.leaves.filter(l => p === '' || l === p || l.startsWith(`${p}.`));
151
+ if (!leaves.length)
152
+ throw new Error(`${this.s.name}: no state ${path}`);
153
+ return any(...leaves.map(l => env[r.field].is(l)));
154
+ }
155
+ // A compound's history: the state it was last in (its initial one before it was ever left), for laws: chart.last(env, 'active').
156
+ last(env, compound) {
157
+ const { regions } = this.build(), single = regions.length === 1 && regions[0].name === 'state';
158
+ const [head, ...rest] = single ? ['state', ...compound.split('.')] : compound.split('.'), r = regions.find(x => x.name === head), c = rest.join('.');
159
+ if (!r?.compounds.get(c)?.history)
160
+ throw new Error(`${this.s.name}: ${compound} keeps no history`);
161
+ return env[`${r.field}.last.${c}`];
162
+ }
163
+ wrap(env) { return Object.assign(Object.create(null), env, { in: (p) => this.in(env, p), last: (c) => this.last(env, c) }); }
164
+ law(name, f) { return this.rule.law(name, (env) => f(this.wrap(env))); }
165
+ stepLaw(name, then, when) {
166
+ return this.rule.stepLaw(name, (b, a) => then(this.wrap(b), this.wrap(a)), when && ((b) => when(this.wrap(b))));
167
+ }
168
+ // What the chart keeps true by its construction: each compound's history is one of its own states. Proven beside the
169
+ // laws `prove` and `check` are given (as a rule's kind laws are), since a law about entering a compound may need it.
170
+ chartLaws() {
171
+ const { rule: r, regions } = this.build();
172
+ return regions.flatMap(reg => [...reg.compounds].filter(([c, { history }]) => history && c).map(([c]) => r.law(`${reg.name === 'state' ? '' : `${reg.name}.`}${c} remembers one of its own states`, (env) => any(...reg.leaves.filter(l => l.startsWith(`${c}.`)).map(l => env[`${reg.field}.last.${c}`].is(l))))));
173
+ }
174
+ prove(laws = [], stepLaws = []) { const given = [...this.chartLaws(), ...laws]; return stepLaws.length ? proveSteps(this.rule, given, stepLaws) : proveRule(this.rule, given); }
175
+ // --- the checks a chart's author expects -------------------------------------------------------------------------
176
+ // reached: each state, with the first run from the start that reaches it (a breadth-first walk of the compiled rule
177
+ // over every event and small payloads, `bound` the largest natural tried, under `params`), or `unreachable` where the
178
+ // prover proves no run reaches it, or `unknown`. stuck: states with no transition out that aren't final. ambiguous:
179
+ // two transitions of one region, as specific as each other, both enabled by one request (the first declared is taken;
180
+ // the check names the pair and a request that enables both), each pair proven apart otherwise.
181
+ check(opts = {}) {
182
+ const { rule: r, regions, placed, events } = this.build(), m = r.machine, run = interpret(m), bound = opts.bound ?? 2;
183
+ const single = regions.length === 1 && regions[0].name === 'state', label = (reg, l) => (single ? l : `${reg.name}.${l}`);
184
+ const params = opts.params ? r.encodeParams(opts.params) : Object.fromEntries(m.params.map(p => [p, bound]));
185
+ // Every request: each event, each payload variable over its range (a kind bounded by a small constant in full).
186
+ const tops = new Map();
187
+ const conj = (c) => (c === undefined ? [] : c.op === 'and' ? c.xs.flatMap(conj) : [c]);
188
+ for (const c of conj(m.assume))
189
+ if ((c.op === '<' || c.op === '<=') && c.a.op === 'var' && c.b.op === 'const')
190
+ tops.set(c.a.name, c.op === '<' ? c.b.value - 1 : c.b.value);
191
+ const requests = m.inputs.reduce((acc, n) => acc.flatMap(t => Array.from({ length: Math.min(tops.get(n) ?? bound, 15) + 1 }, (_, i) => ({ ...t, [n]: i }))), [{}]);
192
+ const key = (st) => JSON.stringify([st.scalars, Object.entries(st.maps).map(([n, c]) => [n, [...c]])]);
193
+ const start = run.init(params), seen = new Map([[key(start), { st: start }]]), queue = [key(start)];
194
+ const reachedAt = new Map();
195
+ const note = (k, st) => { for (const reg of regions) {
196
+ const l = reg.leaves[st.scalars[reg.field]];
197
+ if (!reachedAt.has(label(reg, l)))
198
+ reachedAt.set(label(reg, l), k);
199
+ } };
200
+ note(queue[0], start);
201
+ while (queue.length && seen.size < (opts.maxStates ?? 20_000)) {
202
+ const k = queue.shift(), st = seen.get(k).st;
203
+ for (const req of requests) {
204
+ if (!allowed(m, run, params, st, req))
205
+ continue;
206
+ const next = run.step(params, st, req), nk = key(next);
207
+ if (seen.has(nk))
208
+ continue;
209
+ seen.set(nk, { st: next, from: k, by: req });
210
+ queue.push(nk);
211
+ note(nk, next);
212
+ }
213
+ }
214
+ const runTo = (k) => { const out = []; for (let at = seen.get(k); at.by; at = seen.get(at.from))
215
+ out.unshift({ ...numbersOf(at.by), event: events[at.by.event] }); return out; };
216
+ const reached = {};
217
+ for (const reg of regions)
218
+ for (const l of reg.leaves) {
219
+ const name = label(reg, l), at = reachedAt.get(name);
220
+ if (at) {
221
+ reached[name] = { run: runTo(at) };
222
+ continue;
223
+ }
224
+ const never = r.law(`${name} is never reached`, (env) => env[reg.field].is(l).not());
225
+ const v = proveRule(r, [...this.chartLaws(), never]);
226
+ reached[name] = v.proven && v.checked ? 'unreachable' : 'unknown';
227
+ }
228
+ const stuck = regions.flatMap(reg => reg.leaves.filter(l => !reg.finals.has(l) && !placed.some(t => t.region === reg.name && t.sources.includes(l))).map(l => label(reg, l)));
229
+ const ambiguous = [];
230
+ placed.forEach((t, i) => placed.forEach((u, j) => {
231
+ if (j <= i || t.region !== u.region || t.event !== u.event || t.depth !== u.depth || !t.sources.some(l => u.sources.includes(l)))
232
+ return;
233
+ const apart = r.stepLaw(`transitions ${t.order} and ${u.order} on ${t.event} are never both enabled`, (env) => all(env[`on${i}`], env[`on${j}`]).not());
234
+ const v = proveSteps(r, this.chartLaws(), [apart]);
235
+ if (!(v.proven && v.checked))
236
+ ambiguous.push({ event: t.event, transitions: [t.order, u.order], verdict: v });
237
+ }));
238
+ return { ok: !stuck.length && !ambiguous.length && Object.values(reached).every(x => x !== 'unknown'), reached, stuck, ambiguous };
239
+ }
240
+ }
241
+ export const chart = (name) => new Chart({ name, params: {}, inputs: {}, context: {}, regions: [], on: [] });
@@ -197,10 +197,10 @@ export declare class VariantKind<C extends Cases, T extends string = 'tag'> exte
197
197
  make(write: VariantWrite<C, T>): Variant<C, T>;
198
198
  store(name: string, value: unknown): Record<string, TermLike>;
199
199
  decode(name: string, vars: Record<string, number>): {
200
- [x: string]: (keyof C & string) | Raw;
200
+ [x: string]: Raw | (keyof C & string);
201
201
  };
202
202
  empty(): {
203
- [x: string]: (keyof C & string) | Raw;
203
+ [x: string]: Raw | (keyof C & string);
204
204
  };
205
205
  encode(name: string, raw: Raw): {
206
206
  [x: string]: number;
@@ -209,7 +209,7 @@ export declare class VariantKind<C extends Cases, T extends string = 'tag'> exte
209
209
  [x: string]: number;
210
210
  };
211
211
  sample(rand: Rand, bound: number): {
212
- [x: string]: (keyof C & string) | Raw;
212
+ [x: string]: Raw | (keyof C & string);
213
213
  };
214
214
  }
215
215
  export declare function variant<const C extends Cases>(cases: C): VariantKind<C>;
@@ -874,3 +874,4 @@ export declare class RoseFold<P extends Obj = {}, I extends Obj = {}, S extends
874
874
  rand?: Rand;
875
875
  }): Agreement;
876
876
  }
877
+ export { Chart, chart, type StateNode, type Transition } from './chart.js';
package/dist/core/dsl.js CHANGED
@@ -2126,7 +2126,7 @@ export class Rule {
2126
2126
  // with requests drawn by their kinds (or `draw`), must hold the same values as the compiled rule (decoded: integers and
2127
2127
  // decimals as numbers) for every field the reference's state has, after every step of `trials` runs of `depth`.
2128
2128
  compare(reference, opts = {}) {
2129
- const s = this.spec, rand = opts.rand ?? seeded(5), bound = opts.bound ?? 100, gen = this.compiled();
2129
+ const s = this.spec, rand = opts.rand ?? seeded(5), bound = opts.bound ?? 100, gen = this.compiled(), run = interpret(this.machine);
2130
2130
  let checked = 0;
2131
2131
  const sample = (kinds) => Object.fromEntries(kinds.map(([n, k]) => [n, k.sample(rand, bound)]));
2132
2132
  for (let trial = 0; trial < (opts.trials ?? 200); trial++) {
@@ -2134,8 +2134,11 @@ export class Rule {
2134
2134
  let ours = gen.init(ps), theirs = reference.init(params);
2135
2135
  const edges = trial === 0 ? opts.edges ?? [] : [];
2136
2136
  for (let d = 0; d < (opts.depth ?? 10) + edges.length; d++) {
2137
- const request = edges[d] ?? opts.draw?.(rand) ?? sample(s.inputs);
2138
- ours = gen.step(ps, ours, this.encodeInput(request, params));
2137
+ const request = edges[d] ?? opts.draw?.(rand) ?? sample(s.inputs), encoded = this.encodeInput(request, params);
2138
+ // A request the rule's assumption refuses is no step of the rule: the runtimes throw, so it isn't compared.
2139
+ if (!allowed(this.machine, run, ps, ours, encoded))
2140
+ continue;
2141
+ ours = gen.step(ps, ours, encoded);
2139
2142
  theirs = reference.step(params, theirs, request);
2140
2143
  const got = this.decode(ours);
2141
2144
  for (const [field, want] of Object.entries(theirs)) {
@@ -2739,3 +2742,5 @@ export class RoseFold {
2739
2742
  return { checked };
2740
2743
  }
2741
2744
  }
2745
+ // Statecharts (core/chart.ts): states nested and in parallel regions, compiled to a rule.
2746
+ export { Chart, chart } from "./chart.js";
@@ -272,6 +272,9 @@ export function carriedView(m, st, input, rename = map => map) {
272
272
  }
273
273
  // A request's numbers (its inputs), without the maps it carries.
274
274
  export const numbersOf = (input) => Object.fromEntries(Object.entries(input).filter((e) => typeof e[1] === 'number'));
275
+ // A request the rule's assumption refuses: the proofs say nothing about the step it would take, so neither runtime takes
276
+ // it (a caller that may send one checks first, as gdp-wired/gdp's gate does).
277
+ const refused = (m) => `${m.name}: the request fails the rule's assumption, so no proof covers the step it would take`;
275
278
  // The interpreter: persistent (a step leaves the state it was given as it was), so searches can branch from any state.
276
279
  export function interpret(m) {
277
280
  const { h, plain, carried } = runnable(m);
@@ -340,6 +343,8 @@ export function interpret(m) {
340
343
  }
341
344
  for (const [name, c] of Object.entries(plain.step.flags ?? {}))
342
345
  env[`?${name}`] = evalC(c, env, st, plain) ? 1 : 0;
346
+ if (m.assume && !evalC(m.assume, env, st, plain))
347
+ throw new RangeError(refused(m));
343
348
  const scalars = { ...st.scalars };
344
349
  for (const [name, e] of Object.entries(plain.step.state ?? {}))
345
350
  scalars[name] = evalE(e, env, st, plain);
@@ -371,6 +376,8 @@ const RUNTIME = [
371
376
  ['$m', `function $m(v, what) { if (!(v instanceof Map)) throw new RangeError(what + ' must be a Map of naturals'); for (const [key, x] of v) { $n(key, 'a key of ' + what); $n(x, 'a cell of ' + what); } return v; }`],
372
377
  ['$keys', `function $keys(maps, names) { const keys = new Set(); let past = 0; for (const c of Object.values(maps)) for (const key of c.keys()) past = Math.max(past, key + 1); for (const n of names) for (const key of maps[n].keys()) keys.add(key); return [...keys, past]; }`],
373
378
  ['$breaks', `function $breaks(maps, reads, cmps) { const p = new Set([0]); for (const [n, d, minus] of reads) { for (const s of maps[n].keys()) { if (minus) p.add(s + d); else if (s >= d) p.add(s - d); } if (minus) p.add(d); } for (const [v, d, minus] of cmps) { if (minus) { p.add(v + d); p.add(d); } else if (v >= d) p.add(v - d); } const keys = new Set(); for (const q of p) for (let i = -1; i <= 2; i++) if (q + i >= 0) keys.add(q + i); const sorted = [...keys].sort((a, b) => a - b); sorted.push(sorted[sorted.length - 1] + 1); return sorted; }`],
379
+ // A condition at every two keys that holds wherever x(k) differs from y(k1): only the pairs whose values match are read.
380
+ ['$join', `function $join(ks, x, y, f) { const by = new Map(); for (const k1 of ks) { const v = y(k1), l = by.get(v); if (l) l.push(k1); else by.set(v, [k1]); } return ks.every(k => { const l = by.get(x(k)); return l === undefined || l.every(k1 => f(k, k1)); }); }`],
374
381
  ['$spread', `function $spread(keys, n) { const out = new Set(); for (const k of keys) for (let i = 0; i < n; i++) out.add(k + i); return [...out].sort((a, b) => a - b); }`],
375
382
  ['$dense', `function $dense(keys, f) { const top = keys[keys.length - 1]; if (top > 1000000) throw new RangeError('a write at every key reads its key in arithmetic past a million keys (' + top + ')'); const d = f(top), cells = new Map(); for (let k = 0; k < top; k++) { const v = f(k); if (v !== d) cells.set(k, v); } return {cells, d}; }`],
376
383
  ['$every', `function $every(keys, f) { const past = keys[keys.length - 1], d = f(past), cells = new Map(); for (let i = 0; i < keys.length - 1; i++) { const v = f(keys[i]); if (v !== d) cells.set(keys[i], v); } return {cells, d}; }`],
@@ -388,6 +395,22 @@ function runtimeFor(code) {
388
395
  }
389
396
  return RUNTIME.filter(([name]) => used.has(name)).map(([, src]) => `${src}\n`).join('');
390
397
  }
398
+ // A condition over two keys, k and k1, that holds wherever a term of k alone differs from a term of k1 alone: an
399
+ // implication (a not-and, or an or with one) whose premises include x(k) = y(k1). The runtime then reads only the pairs
400
+ // whose values match ($join), the rest holding by the premise: rows plus matches, not rows squared.
401
+ function joinOf(c, k, k1) {
402
+ const reads = (t, key) => JSON.stringify(t).includes(`"op":"var","name":${JSON.stringify(key)}`);
403
+ const premises = c.op === 'not' && c.x.op === 'and' ? c.x.xs : c.op === 'or' ? c.xs.flatMap(x => (x.op === 'not' ? (x.x.op === 'and' ? x.x.xs : [x.x]) : [])) : [];
404
+ for (const p of premises) {
405
+ if (p.op !== '=')
406
+ continue;
407
+ if (reads(p.a, k) && !reads(p.a, k1) && reads(p.b, k1) && !reads(p.b, k))
408
+ return { x: p.a, y: p.b };
409
+ if (reads(p.b, k) && !reads(p.b, k1) && reads(p.a, k1) && !reads(p.a, k))
410
+ return { x: p.b, y: p.a };
411
+ }
412
+ return undefined;
413
+ }
391
414
  export function compileJs(m) {
392
415
  const { h, plain, queries, carried } = runnable(m);
393
416
  const id = (name) => JSON.stringify(name);
@@ -427,6 +450,15 @@ export function compileJs(m) {
427
450
  bound = { ...bound, ...Object.fromEntries(names.map((n, i) => [n, ks[i]])) };
428
451
  const body = cx(c.c);
429
452
  bound = outer;
453
+ if (names.length === 2) {
454
+ const j = joinOf(c.c, names[0], names[1]);
455
+ if (j) {
456
+ bound = { ...bound, [names[0]]: 'k', [names[1]]: 'k1' };
457
+ const x = ex(j.x), y = ex(j.y);
458
+ bound = outer;
459
+ return `$join(${keys}, k => ${x}, k1 => ${y}, (k, k1) => ${body})`;
460
+ }
461
+ }
430
462
  return names.length === 1 ? `${keys}.every(k => ${body})` : `(ks => ${ks.map(k => `ks.every(${k} => `).join('')}${body}${')'.repeat(ks.length)})(${keys})`;
431
463
  }
432
464
  }
@@ -465,7 +497,9 @@ export function compileJs(m) {
465
497
  }
466
498
  lines.push(`let ${queries.map((_, i) => `q${i} = 0`).join(', ')};`, `for (let i = head; i < records.length; i++) { const r = records[i]; ${queries.map((q, i) => `q${i} = $x(q${i} + ${ex(h.queries[q])});`).join(' ')} }`, ...queries.map((q, i) => `env[${id(q)}] = q${i};`));
467
499
  }
468
- lines.push(...flags.map((name, i) => `const flag${i} = ${cx(plain.step.flags[name])};`), `const scalars = {...s, ${Object.entries(plain.step.state ?? {}).map(([name, e]) => `${id(name)}: ${ex(e)}`).join(', ')}};`, ...writes.map(([, w], i) => ('each' in w ? `const write${i} = ${every(w)};` : `const key${i} = ${ex(w.key)}, value${i} = ${ex(w.value)};`)));
500
+ lines.push(...flags.map((name, i) => `const flag${i} = ${cx(plain.step.flags[name])};`),
501
+ // The rule's own assumption, as `allowed` (core/watch.ts) reads it (a history's machine assumes more: its relations).
502
+ ...(m.assume ? [`if (!${cx(m.assume)}) throw new RangeError(${id(refused(m))});`] : []), `const scalars = {...s, ${Object.entries(plain.step.state ?? {}).map(([name, e]) => `${id(name)}: ${ex(e)}`).join(', ')}};`, ...writes.map(([, w], i) => ('each' in w ? `const write${i} = ${every(w)};` : `const key${i} = ${ex(w.key)}, value${i} = ${ex(w.value)};`)));
469
503
  if (h)
470
504
  lines.push(`const keep = ${cx(h.keep)};`);
471
505
  lines.push(...writes.map(([name, w], i) => ('each' in w ? `state.maps[${id(name)}] = write${i}.cells; dft[${id(name)}] = write${i}.d;` : `state.maps[${id(name)}].set(key${i}, value${i});`)));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdp-wired",
3
- "version": "0.12.6",
3
+ "version": "0.13.0",
4
4
  "description": "An automatic prover for stateful rules (budgets, caps, windows, workflows, policies): laws proven with no proof to write, checked again in Lean, counterexamples found on the GPU, the rule shipped as code",
5
5
  "keywords": [
6
6
  "verification",
@@ -89,7 +89,7 @@
89
89
  "build": "bun scripts/build.ts",
90
90
  "prepublishOnly": "bun scripts/build.ts",
91
91
  "smoke": "sh scripts/smoke.sh",
92
- "test": "bun scripts/gen-lean-library.ts --check && cd core && bun test solver.test.ts dsl.test.ts conformance.test.ts compose.test.ts quantifiers.test.ts functions.test.ts keyterms.test.ts lists.test.ts liveness.test.ts sizes.test.ts values.test.ts builders.test.ts gdp.test.ts datatypes.test.ts lemmas.test.ts invoices.test.ts folds.test.ts trees.test.ts pairs.test.ts && cd ../kernel && bun test tamper.test.ts refusals.test.ts steplaw.test.ts sets.test.ts && cd ../examples && bun test examples.test.ts && cd .. && bun examples/readme.ts && bun examples/guide.ts && bun examples/composition.ts && bun examples/sets.ts && bun examples/quantifiers.ts && bun examples/functions.ts && bun examples/datatypes.ts && bun examples/lemmas.ts && bun examples/invoices.ts && bun examples/folds.ts && bun examples/trees.ts && bun examples/pairs.ts && bun examples/lists.ts && bun examples/sizes.ts && bun examples/liveness.ts && bun examples/budget.ts && bun examples/gdp.ts && bun examples/steps.ts && bun examples/jev-fabric/run.ts"
92
+ "test": "bun scripts/gen-lean-library.ts --check && cd core && bun test solver.test.ts dsl.test.ts conformance.test.ts compose.test.ts quantifiers.test.ts functions.test.ts keyterms.test.ts lists.test.ts liveness.test.ts sizes.test.ts values.test.ts builders.test.ts gdp.test.ts datatypes.test.ts lemmas.test.ts invoices.test.ts folds.test.ts trees.test.ts pairs.test.ts chart.test.ts && cd ../kernel && bun test tamper.test.ts refusals.test.ts steplaw.test.ts sets.test.ts && cd ../examples && bun test examples.test.ts && cd .. && bun examples/readme.ts && bun examples/guide.ts && bun examples/composition.ts && bun examples/sets.ts && bun examples/quantifiers.ts && bun examples/functions.ts && bun examples/datatypes.ts && bun examples/lemmas.ts && bun examples/invoices.ts && bun examples/folds.ts && bun examples/trees.ts && bun examples/pairs.ts && bun examples/groups.ts && bun examples/charts.ts && bun examples/lists.ts && bun examples/sizes.ts && bun examples/liveness.ts && bun examples/budget.ts && bun examples/gdp.ts && bun examples/steps.ts && bun examples/jev-fabric/run.ts"
93
93
  },
94
94
  "devDependencies": {
95
95
  "@gdp-ts/core": "0.1.0",