gdp-wired 0.12.7 → 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 +3 -0
- package/dist/core/chart.d.ts +86 -0
- package/dist/core/chart.js +241 -0
- package/dist/core/dsl.d.ts +4 -3
- package/dist/core/dsl.js +2 -0
- package/package.json +2 -2
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`).
|
|
@@ -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: [] });
|
package/dist/core/dsl.d.ts
CHANGED
|
@@ -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)
|
|
200
|
+
[x: string]: Raw | (keyof C & string);
|
|
201
201
|
};
|
|
202
202
|
empty(): {
|
|
203
|
-
[x: string]: (keyof C & string)
|
|
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)
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdp-wired",
|
|
3
|
-
"version": "0.
|
|
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",
|