gdp-wired 0.13.2 → 0.14.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 +10 -4
- package/dist/core/chart.d.ts +1 -0
- package/dist/core/chart.js +5 -5
- package/dist/core/dsl.d.ts +2 -2
- package/dist/core/dsl.js +3 -2
- package/dist/core/xstate.d.ts +66 -0
- package/dist/core/xstate.js +218 -0
- package/package.json +8 -3
package/README.md
CHANGED
|
@@ -60,9 +60,15 @@ 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
|
|
64
|
-
a compound's history, guards and extended state, compiled to a rule;
|
|
65
|
-
proves it unreachable), no state stuck and no event ambiguous, and its
|
|
63
|
+
- **Statecharts**: `chart(name).states({...}).on(event, {from, to, when, set})`, with states nested to any depth,
|
|
64
|
+
parallel regions (at the top of the chart), a compound's history (deep), guards and extended state, compiled to a rule;
|
|
65
|
+
`check()` shows every state reached by a run (or proves it unreachable), no state stuck and no event ambiguous, and its
|
|
66
|
+
laws prove like any rule's (`examples/charts.ts`). Not yet: parallel regions inside a state, entry and exit actions,
|
|
67
|
+
eventless and delayed transitions.
|
|
68
|
+
- **XState machines** (`gdp-wired/xstate`): `fromXState(machine, {guards, actions})` builds the chart an XState v5
|
|
69
|
+
machine runs as, with each guard and action it names given again in gdp-wired's terms, and `conforms(chart, machine,
|
|
70
|
+
createActor)` drives XState's own actor and the compiled rule with the same random events, comparing every state and
|
|
71
|
+
context; what a chart can't run the same way is refused when built, with the reason (`examples/xstate.ts`).
|
|
66
72
|
- **Orderings at any distance**: "a refund never follows a release", through ghost state that watches the rule.
|
|
67
73
|
- **Liveness**: "every ticket issued is served", in every run that keeps sending the requests that help, including under
|
|
68
74
|
weak fairness (`examples/liveness.ts`).
|
|
@@ -372,7 +378,7 @@ The npm package is built from `core/`, `kernel/` and `lean-kernel/` alone; nothi
|
|
|
372
378
|
## Running
|
|
373
379
|
|
|
374
380
|
The npm package is the library: the builders (`gdp-wired`), the runtimes (`gdp-wired/machine`), composition
|
|
375
|
-
(`gdp-wired/compose`), the Lean export (`gdp-wired/lean`), the kernel
|
|
381
|
+
(`gdp-wired/compose`), XState machines as charts (`gdp-wired/xstate`), the Lean export (`gdp-wired/lean`), the kernel
|
|
376
382
|
(`gdp-wired/kernel`), the prover (`gdp-wired/prove`), the derivation checker and solver (`gdp-wired/check`,
|
|
377
383
|
`gdp-wired/solver`). Everything else here (examples, comparisons, the Lean twin, the GPU searches) runs from a clone and
|
|
378
384
|
needs [Bun](https://bun.sh); Lean 4.34.0 for `lean/` and `lean-kernel/`; a WebGPU runtime (Chrome, or Dawn's node
|
package/dist/core/chart.d.ts
CHANGED
|
@@ -20,6 +20,7 @@ type After<X> = X & {
|
|
|
20
20
|
export type Transition<P, I, X> = {
|
|
21
21
|
from: string | string[];
|
|
22
22
|
to?: string;
|
|
23
|
+
fresh?: boolean;
|
|
23
24
|
when?: (env: Env<P, I, X>) => Cond;
|
|
24
25
|
set?: (env: Env<P, I, X>) => Partial<{
|
|
25
26
|
[k in keyof X]: X[k] extends Choice<infer K> ? K | X[k] : TermLike | Cond | X[k];
|
package/dist/core/chart.js
CHANGED
|
@@ -119,14 +119,14 @@ export class Chart {
|
|
|
119
119
|
for (const [k, v] of Object.entries({ ...s.context, ...s.outputs }))
|
|
120
120
|
state[k] = v;
|
|
121
121
|
const guardEnv = (env) => this.wrap(env);
|
|
122
|
-
// Entering a path: a leaf, or a compound's last child (with history) or its initial one.
|
|
123
|
-
const entered = (env, r, path) => {
|
|
122
|
+
// Entering a path: a leaf, or a compound's last child (with history, unless fresh) or its initial one.
|
|
123
|
+
const entered = (env, r, path, fresh = false) => {
|
|
124
124
|
const c = r.compounds.get(path), field = env[r.field];
|
|
125
125
|
if (!c)
|
|
126
126
|
return field.of(path);
|
|
127
|
-
if (c.history && path)
|
|
127
|
+
if (c.history && path && !fresh)
|
|
128
128
|
return env[lastOf(r, path)];
|
|
129
|
-
return entered(env, r, path ? `${path}.${c.initial}` : c.initial);
|
|
129
|
+
return entered(env, r, path ? `${path}.${c.initial}` : c.initial, fresh);
|
|
130
130
|
};
|
|
131
131
|
let built = rule(s.name).params(s.params).inputs({ event: oneOf(...events), ...s.inputs }).state(state);
|
|
132
132
|
// A set of a region's states as ranges of their indices (n <= 2, 3 <= n <= 4): a few comparisons, where one test per
|
|
@@ -153,7 +153,7 @@ export class Chart {
|
|
|
153
153
|
// The transitions taken in a region exclude one another, so those entering one state share a `when` (any of
|
|
154
154
|
// them taken), and an internal one needs none (the state stays).
|
|
155
155
|
out[r.field] = grouped(placed.map((t, i) => [t, i]).filter(([t]) => t.region === r.name && t.to !== undefined)
|
|
156
|
-
.map(([t, i]) => ({ i, region: r.name, value: entered(env, r, locate(t.to)[1]) })), env, env[r.field]);
|
|
156
|
+
.map(([t, i]) => ({ i, region: r.name, value: entered(env, r, locate(t.to)[1], t.fresh) })), env, env[r.field]);
|
|
157
157
|
for (const [c, { history }] of r.compounds)
|
|
158
158
|
if (history && c)
|
|
159
159
|
out[lastOf(r, c)] = when(any(...under(r, c).map(l => out[r.field].is(l))), out[r.field], env[lastOf(r, c)]);
|
package/dist/core/dsl.d.ts
CHANGED
|
@@ -132,8 +132,8 @@ export declare class Choice<K extends string> {
|
|
|
132
132
|
is(k: K): Cond;
|
|
133
133
|
of(k: K): Choice<K>;
|
|
134
134
|
get index(): Term;
|
|
135
|
-
eq(o: K | Choice<K> | number): Cond;
|
|
136
|
-
ne(o: K | Choice<K> | number): Cond;
|
|
135
|
+
eq(o: K | Choice<K> | number | Term): Cond;
|
|
136
|
+
ne(o: K | Choice<K> | number | Term): Cond;
|
|
137
137
|
private indexOf;
|
|
138
138
|
match(cases: {
|
|
139
139
|
[k in K]: () => Cond;
|
package/dist/core/dsl.js
CHANGED
|
@@ -358,8 +358,9 @@ export class Choice {
|
|
|
358
358
|
of(k) { return new Choice(lit(this.indexOf(k)), this.values); }
|
|
359
359
|
// Its index among the values, in the order declared (oneOf('new', 'paid'): new is 0), as a term.
|
|
360
360
|
get index() { return this.n; }
|
|
361
|
-
// The same value as a name, another choice of the kind, or an index: status.eq('paid'),
|
|
362
|
-
eq(
|
|
361
|
+
// The same value as a name, another choice of the kind, or an index (a number or a natural term): status.eq('paid'),
|
|
362
|
+
// a.phase.eq(b.phase), phase.eq(3), phase.eq(when(c, 2, phase.index)).
|
|
363
|
+
eq(o) { return typeof o === 'string' ? this.is(o) : o instanceof Choice ? this.n.eq(o.n) : this.n.eq(o); }
|
|
363
364
|
ne(o) { return this.eq(o).not(); }
|
|
364
365
|
indexOf(k) {
|
|
365
366
|
const i = this.values.indexOf(k);
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { type Kind, type Cond } from './dsl.js';
|
|
2
|
+
import { type Chart } from './chart.js';
|
|
3
|
+
type Obj = Record<string, any>;
|
|
4
|
+
type XAction = string | {
|
|
5
|
+
type: string;
|
|
6
|
+
params?: unknown;
|
|
7
|
+
} | ((...args: never[]) => unknown);
|
|
8
|
+
type XTransition = string | {
|
|
9
|
+
target?: string | string[];
|
|
10
|
+
guard?: unknown;
|
|
11
|
+
actions?: XAction | XAction[];
|
|
12
|
+
reenter?: boolean;
|
|
13
|
+
description?: string;
|
|
14
|
+
};
|
|
15
|
+
export type XNode = {
|
|
16
|
+
id?: string;
|
|
17
|
+
type?: 'atomic' | 'compound' | 'parallel' | 'final' | 'history';
|
|
18
|
+
history?: 'shallow' | 'deep';
|
|
19
|
+
initial?: string;
|
|
20
|
+
target?: string;
|
|
21
|
+
states?: Record<string, XNode>;
|
|
22
|
+
on?: Record<string, XTransition | XTransition[] | undefined>;
|
|
23
|
+
entry?: XAction | XAction[];
|
|
24
|
+
exit?: XAction | XAction[];
|
|
25
|
+
always?: unknown;
|
|
26
|
+
after?: unknown;
|
|
27
|
+
invoke?: unknown;
|
|
28
|
+
onDone?: unknown;
|
|
29
|
+
context?: unknown;
|
|
30
|
+
[k: string]: unknown;
|
|
31
|
+
};
|
|
32
|
+
export type Guard = (env: Obj, params?: unknown) => Cond;
|
|
33
|
+
export type Action = ((env: Obj, params?: unknown) => Obj) | null;
|
|
34
|
+
export type Bridge = {
|
|
35
|
+
params?: Record<string, Kind<unknown>>;
|
|
36
|
+
inputs?: Record<string, Kind<unknown>>;
|
|
37
|
+
context?: Record<string, number | {
|
|
38
|
+
kind: Kind<unknown>;
|
|
39
|
+
init: unknown;
|
|
40
|
+
}>;
|
|
41
|
+
guards?: Record<string, Guard>;
|
|
42
|
+
actions?: Record<string, Action>;
|
|
43
|
+
events?: string[];
|
|
44
|
+
};
|
|
45
|
+
export type MachineLike = object;
|
|
46
|
+
export declare function fromXState(machine: MachineLike, opts?: Bridge): Chart<Obj, Obj, Obj>;
|
|
47
|
+
export type XActor = {
|
|
48
|
+
send: (e: never) => void;
|
|
49
|
+
getSnapshot: () => {
|
|
50
|
+
value: unknown;
|
|
51
|
+
context?: unknown;
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
export declare function conforms(c: Chart<Obj, Obj, Obj>, machine: MachineLike, createActor: (machine: never) => {
|
|
55
|
+
start: () => XActor;
|
|
56
|
+
}, opts?: {
|
|
57
|
+
runs?: number;
|
|
58
|
+
depth?: number;
|
|
59
|
+
seed?: number;
|
|
60
|
+
params?: Obj;
|
|
61
|
+
payload?: (event: string, rand: (n: number) => number) => Obj;
|
|
62
|
+
}): {
|
|
63
|
+
checked: number;
|
|
64
|
+
failure?: string;
|
|
65
|
+
};
|
|
66
|
+
export {};
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
// XState machines as charts (core/chart.ts): `fromXState(machine, {guards, actions})` reads an XState v5 machine's
|
|
2
|
+
// config (states, nesting, a parallel root, history nodes, final states, transitions with named guards and actions) and
|
|
3
|
+
// builds the chart that runs the same way, so its laws are proven of the machine you ship. XState's guards and actions
|
|
4
|
+
// are JavaScript, so each one the machine names is given again in gdp-wired's terms: a guard as a condition, an action
|
|
5
|
+
// as the context it writes (or null, for one that writes none). `conforms(chart, machine, createActor)` then drives the
|
|
6
|
+
// real XState actor and the compiled rule with the same random events and compares every state and context, so a
|
|
7
|
+
// guard or an action given wrongly shows up as a run where the two part.
|
|
8
|
+
//
|
|
9
|
+
// const machine = setup({guards: {canRetry: ...}, actions: {count: assign(...)}}).createMachine({...})
|
|
10
|
+
// const c = fromXState(machine, {
|
|
11
|
+
// guards: {canRetry: ({retries}) => retries.lt(2)},
|
|
12
|
+
// actions: {count: ({retries}) => ({retries: retries.plus(1)}), notify: null},
|
|
13
|
+
// })
|
|
14
|
+
// c.check(); c.prove([c.law('retries stay within 2', ({retries}) => retries.atMost(2))])
|
|
15
|
+
// conforms(c, machine, createActor) // {checked, failure?}
|
|
16
|
+
//
|
|
17
|
+
// What a chart can't run the same way is refused when the chart is built, with the reason: a parallel state below the
|
|
18
|
+
// root, eventless (`always`) and delayed (`after`) transitions, invoked actors and done events, wildcard events, several
|
|
19
|
+
// targets at once, inline guards and actions, entry and exit actions that write the context, two regions writing the
|
|
20
|
+
// context on one event (XState runs their actions one after another; a chart's regions write in one step), and a
|
|
21
|
+
// shallow history over nested states.
|
|
22
|
+
import { Choice, bool, int } from "./dsl.js";
|
|
23
|
+
import { chart } from "./chart.js";
|
|
24
|
+
const list = (x) => (x === undefined ? [] : Array.isArray(x) ? x : [x]);
|
|
25
|
+
const configOf = (machine) => ('config' in machine && machine.config && typeof machine.config === 'object' ? machine.config : machine);
|
|
26
|
+
export function fromXState(machine, opts = {}) {
|
|
27
|
+
const root = configOf(machine), name = root.id ?? 'machine', parallel = root.type === 'parallel';
|
|
28
|
+
const refuse = (where, what) => { throw new Error(`${name}: ${where || 'the machine'}: ${what}`); };
|
|
29
|
+
const infos = [], ids = new Map();
|
|
30
|
+
const walk = (node, segs, parent) => {
|
|
31
|
+
const info = { node, segs, parent }, where = segs.join('.');
|
|
32
|
+
infos.push(info);
|
|
33
|
+
ids.set(node.id ?? (segs.length ? `${name}.${where}` : name), info);
|
|
34
|
+
for (const k of ['always', 'after', 'invoke', 'onDone'])
|
|
35
|
+
if (node[k] !== undefined)
|
|
36
|
+
refuse(where, `\`${k}\` has no chart counterpart`);
|
|
37
|
+
if (node.type === 'parallel' && segs.length)
|
|
38
|
+
refuse(where, 'a parallel state below the root (a chart runs regions only at the top)');
|
|
39
|
+
for (const [k, kid] of Object.entries(node.states ?? {}))
|
|
40
|
+
walk(kid, [...segs, k], info);
|
|
41
|
+
const hist = Object.entries(node.states ?? {}).filter(([, kid]) => kid.type === 'history');
|
|
42
|
+
if (hist.length) {
|
|
43
|
+
const deep = hist.every(([, h]) => h.history === 'deep'), kids = Object.entries(node.states).filter(([, kid]) => kid.type !== 'history');
|
|
44
|
+
if (!deep && kids.some(([, kid]) => Object.values(kid.states ?? {}).some(g => g.type !== 'history')))
|
|
45
|
+
refuse(where, 'a shallow history over nested states (a chart keeps the innermost state)');
|
|
46
|
+
for (const [k, h] of hist)
|
|
47
|
+
if (h.target !== undefined && h.target !== (node.initial ?? kids[0]?.[0]))
|
|
48
|
+
refuse(`${where}.${k}`, 'a history with its own default target');
|
|
49
|
+
info.history = { deep };
|
|
50
|
+
}
|
|
51
|
+
return info;
|
|
52
|
+
};
|
|
53
|
+
walk(root, []);
|
|
54
|
+
const pathOf = (i) => i.segs.join('.');
|
|
55
|
+
const descend = (from, segs, t) => segs.reduce((at, seg) => {
|
|
56
|
+
const kid = infos.find(x => x.parent === at && x.segs.at(-1) === seg);
|
|
57
|
+
return kid ?? refuse(pathOf(from), `no state ${t}`);
|
|
58
|
+
}, from);
|
|
59
|
+
// A target as XState reads it: #id(.path), .child(.path) from the state itself, or a sibling(.path).
|
|
60
|
+
const resolve = (from, t) => {
|
|
61
|
+
let at;
|
|
62
|
+
if (t.startsWith('#')) {
|
|
63
|
+
const parts = t.slice(1).split('.');
|
|
64
|
+
let k = parts.length;
|
|
65
|
+
while (k > 0 && !ids.has(parts.slice(0, k).join('.')))
|
|
66
|
+
k--;
|
|
67
|
+
if (!k)
|
|
68
|
+
refuse(pathOf(from), `no state with the id in ${t}`);
|
|
69
|
+
at = descend(ids.get(parts.slice(0, k).join('.')), parts.slice(k), t);
|
|
70
|
+
}
|
|
71
|
+
else if (t.startsWith('.'))
|
|
72
|
+
at = descend(from, t.slice(1).split('.'), t);
|
|
73
|
+
else
|
|
74
|
+
at = descend(from.parent ?? from, t.split('.'), t);
|
|
75
|
+
return at.node.type === 'history' ? { to: at.parent, viaHistory: true } : { to: at, viaHistory: false };
|
|
76
|
+
};
|
|
77
|
+
// Guards and actions, named and given again.
|
|
78
|
+
const guardOf = (where, g) => {
|
|
79
|
+
if (g === undefined)
|
|
80
|
+
return undefined;
|
|
81
|
+
const [n, params] = typeof g === 'string' ? [g, undefined] : typeof g === 'object' && g && typeof g.type === 'string' ? [g.type, g.params] : [undefined, undefined];
|
|
82
|
+
if (n === undefined)
|
|
83
|
+
return refuse(where, `an inline guard${typeof g === 'function' && g.type ? ` (${g.type})` : ''}: name it in setup({guards}) and give its condition in guards`);
|
|
84
|
+
const f = opts.guards?.[n] ?? refuse(where, `the guard ${n}: give its condition in guards`);
|
|
85
|
+
return env => f(env, params);
|
|
86
|
+
};
|
|
87
|
+
const actionsOf = (where, as) => as.flatMap(a => {
|
|
88
|
+
const [n, params] = typeof a === 'string' ? [a, undefined] : typeof a === 'object' && a && typeof a.type === 'string' ? [a.type, a.params] : [undefined, undefined];
|
|
89
|
+
if (n === undefined)
|
|
90
|
+
return refuse(where, `an inline action${typeof a === 'function' && a.type ? ` (${a.type})` : ''}: name it in setup({actions}) and give the context it writes in actions`);
|
|
91
|
+
if (!opts.actions || !(n in opts.actions))
|
|
92
|
+
return refuse(where, `the action ${n}: give the context it writes in actions (null if it writes none)`);
|
|
93
|
+
const f = opts.actions[n];
|
|
94
|
+
return f ? [(env) => f(env, params)] : [];
|
|
95
|
+
});
|
|
96
|
+
for (const i of infos)
|
|
97
|
+
for (const k of ['entry', 'exit'])
|
|
98
|
+
if (actionsOf(pathOf(i), list(i.node[k])).length)
|
|
99
|
+
refuse(pathOf(i), `${k} actions that write the context (a chart writes the context on transitions only)`);
|
|
100
|
+
// Several actions run one after another, each reading the context the one before wrote.
|
|
101
|
+
const composed = (fs) => (fs.length ? (env) => {
|
|
102
|
+
let cur = env;
|
|
103
|
+
const out = {};
|
|
104
|
+
for (const f of fs) {
|
|
105
|
+
const o = f(cur);
|
|
106
|
+
Object.assign(out, o);
|
|
107
|
+
cur = Object.assign(Object.create(null), cur, Object.fromEntries(Object.entries(o).map(([k, v]) => [k, typeof v === 'string' && cur[k] instanceof Choice ? cur[k].of(v) : v])));
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
} : undefined);
|
|
111
|
+
// The states, as a chart's.
|
|
112
|
+
const nodeOf = (i) => {
|
|
113
|
+
const kids = infos.filter(x => x.parent === i && x.node.type !== 'history');
|
|
114
|
+
if (!kids.length)
|
|
115
|
+
return i.node.type === 'final' ? { final: true } : {};
|
|
116
|
+
return { states: Object.fromEntries(kids.map(k => [k.segs.at(-1), nodeOf(k)])), ...(i.node.initial ? { initial: i.node.initial } : {}), ...(i.history ? { history: true } : {}) };
|
|
117
|
+
};
|
|
118
|
+
const top = infos.filter(x => x.parent === infos[0]);
|
|
119
|
+
let c = chart(name).params((opts.params ?? {})).inputs((opts.inputs ?? {})).context(contextOf(root, opts.context, refuse));
|
|
120
|
+
if (parallel) {
|
|
121
|
+
for (const r of top)
|
|
122
|
+
if (!infos.some(x => x.parent === r && x.node.type !== 'history'))
|
|
123
|
+
refuse(pathOf(r), 'a region with no states of its own');
|
|
124
|
+
c = c.regions(Object.fromEntries(top.map(r => [r.segs[0], nodeOf(r)])));
|
|
125
|
+
}
|
|
126
|
+
else
|
|
127
|
+
c = c.states(nodeOf(infos[0]).states ?? refuse('', 'no states'), root.initial ? { initial: root.initial } : {});
|
|
128
|
+
if (opts.events)
|
|
129
|
+
c = c.events(...opts.events);
|
|
130
|
+
// The transitions, in document order (the chart takes the most specific source first, then the first declared, as
|
|
131
|
+
// XState does).
|
|
132
|
+
const writers = new Map();
|
|
133
|
+
for (const i of infos)
|
|
134
|
+
for (const [event, spec] of Object.entries(i.node.on ?? {})) {
|
|
135
|
+
const where = `${pathOf(i) || 'the root'} on ${event}`;
|
|
136
|
+
if (event.includes('*'))
|
|
137
|
+
refuse(where, 'a wildcard event');
|
|
138
|
+
if (spec === undefined)
|
|
139
|
+
refuse(where, 'a forbidden event (an undefined transition)');
|
|
140
|
+
for (const t of list(spec)) {
|
|
141
|
+
const o = typeof t === 'string' ? { target: t } : t, targets = list(o.target);
|
|
142
|
+
if (targets.length > 1)
|
|
143
|
+
refuse(where, 'several targets at once');
|
|
144
|
+
const to = targets[0] === undefined ? undefined : resolve(i, targets[0]);
|
|
145
|
+
const set = composed(actionsOf(where, list(o.actions))), when = guardOf(where, o.guard);
|
|
146
|
+
// Where it applies: its state; for the root, every top state but a final one (XState stops there), or in a
|
|
147
|
+
// parallel machine the region its target is in (the first region, for one with no target).
|
|
148
|
+
const from = i.segs.length ? pathOf(i) : parallel ? (to ? to.to.segs[0] : top[0].segs[0]) : top.filter(x => x.node.type !== 'final' && x.node.type !== 'history').map(pathOf);
|
|
149
|
+
if (set) {
|
|
150
|
+
const region = parallel ? String(list(from)[0]).split('.')[0] : 'state';
|
|
151
|
+
writers.set(event, (writers.get(event) ?? new Set()).add(region));
|
|
152
|
+
}
|
|
153
|
+
c = c.on(event, { from, ...(to ? { to: pathOf(to.to), fresh: !to.viaHistory } : {}), ...(when ? { when } : {}), ...(set ? { set: set } : {}) });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
for (const [event, regions] of writers)
|
|
157
|
+
if (regions.size > 1)
|
|
158
|
+
refuse(`on ${event}`, `regions ${[...regions].join(' and ')} both write the context (XState runs their actions one after another)`);
|
|
159
|
+
return c;
|
|
160
|
+
}
|
|
161
|
+
function contextOf(root, given, refuse) {
|
|
162
|
+
const out = {};
|
|
163
|
+
if (root.context !== undefined && typeof root.context !== 'object') {
|
|
164
|
+
if (!given)
|
|
165
|
+
refuse('', 'a context computed by a function: give its kinds in context');
|
|
166
|
+
return { ...given };
|
|
167
|
+
}
|
|
168
|
+
for (const [k, v] of Object.entries((root.context ?? {}))) {
|
|
169
|
+
if (given && k in given)
|
|
170
|
+
continue;
|
|
171
|
+
if (typeof v === 'number' && Number.isSafeInteger(v))
|
|
172
|
+
out[k] = v >= 0 ? v : int.from(v);
|
|
173
|
+
else if (typeof v === 'boolean')
|
|
174
|
+
out[k] = bool.from(v);
|
|
175
|
+
else
|
|
176
|
+
refuse('', `the context's ${k} (${JSON.stringify(v)}): give its kind in context`);
|
|
177
|
+
}
|
|
178
|
+
return { ...out, ...given };
|
|
179
|
+
}
|
|
180
|
+
const canonical = (v) => (v && typeof v === 'object' ? `{${Object.keys(v).sort().map(k => `${k}:${canonical(v[k])}`).join(',')}}` : JSON.stringify(v));
|
|
181
|
+
export function conforms(c, machine, createActor, opts = {}) {
|
|
182
|
+
const root = configOf(machine), events = [];
|
|
183
|
+
const walk = (n) => { for (const e of Object.keys(n.on ?? {}))
|
|
184
|
+
if (!events.includes(e))
|
|
185
|
+
events.push(e); Object.values(n.states ?? {}).forEach(walk); };
|
|
186
|
+
walk(root);
|
|
187
|
+
const r = c.rule, g = r.compiled(), ps = r.encodeParams((opts.params ?? {}));
|
|
188
|
+
// dsl.ts's generator: the high bits of a linear congruence (the low ones repeat with a short period)
|
|
189
|
+
let x = opts.seed ?? 1;
|
|
190
|
+
const rand = (k) => { x = (x * 1103515245 + 12345) % 2 ** 31; return Math.floor(x / 2 ** 31 * k); };
|
|
191
|
+
const valueOf = (d) => {
|
|
192
|
+
const nest = (path) => path.split('.').reduceRight((acc, seg) => (acc === undefined ? seg : { [seg]: acc }), undefined);
|
|
193
|
+
if (root.type === 'parallel')
|
|
194
|
+
return Object.fromEntries(Object.keys(root.states ?? {}).filter(k => root.states[k].type !== 'history').map(k => [k, nest(String(d[k]))]));
|
|
195
|
+
return nest(String(d.state));
|
|
196
|
+
};
|
|
197
|
+
const contextKeys = Object.keys((root.context && typeof root.context === 'object' ? root.context : {}));
|
|
198
|
+
let checked = 0;
|
|
199
|
+
for (let run = 0; run < (opts.runs ?? 100); run++) {
|
|
200
|
+
const actor = createActor(machine).start();
|
|
201
|
+
let st = g.init(ps);
|
|
202
|
+
const sent = [];
|
|
203
|
+
for (let d = 0; d < (opts.depth ?? 12); d++) {
|
|
204
|
+
const type = events[rand(events.length)], payload = opts.payload?.(type, rand) ?? {};
|
|
205
|
+
sent.push(Object.keys(payload).length ? `${type}${JSON.stringify(payload)}` : type);
|
|
206
|
+
actor.send({ type, ...payload });
|
|
207
|
+
st = g.step(ps, st, r.encodeInput({ event: type, ...payload }));
|
|
208
|
+
const snap = actor.getSnapshot(), d2 = r.decode(st), want = valueOf(d2);
|
|
209
|
+
if (canonical(snap.value) !== canonical(want))
|
|
210
|
+
return { checked, failure: `after ${sent.join(', ')}: XState is in ${JSON.stringify(snap.value)}, the chart in ${JSON.stringify(want)}` };
|
|
211
|
+
for (const k of contextKeys)
|
|
212
|
+
if (JSON.stringify(snap.context?.[k]) !== JSON.stringify(d2[k]))
|
|
213
|
+
return { checked, failure: `after ${sent.join(', ')}: XState's ${k} is ${JSON.stringify(snap.context?.[k])}, the chart's ${JSON.stringify(d2[k])}` };
|
|
214
|
+
checked++;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return { checked };
|
|
218
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdp-wired",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.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",
|
|
@@ -52,6 +52,10 @@
|
|
|
52
52
|
"types": "./dist/kernel/prove.d.ts",
|
|
53
53
|
"default": "./dist/kernel/prove.js"
|
|
54
54
|
},
|
|
55
|
+
"./xstate": {
|
|
56
|
+
"types": "./dist/core/xstate.d.ts",
|
|
57
|
+
"default": "./dist/core/xstate.js"
|
|
58
|
+
},
|
|
55
59
|
"./check": {
|
|
56
60
|
"types": "./dist/core/check.d.ts",
|
|
57
61
|
"default": "./dist/core/check.js"
|
|
@@ -89,7 +93,7 @@
|
|
|
89
93
|
"build": "bun scripts/build.ts",
|
|
90
94
|
"prepublishOnly": "bun scripts/build.ts",
|
|
91
95
|
"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 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"
|
|
96
|
+
"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 xstate.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/xstate.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
97
|
},
|
|
94
98
|
"devDependencies": {
|
|
95
99
|
"@gdp-ts/core": "0.1.0",
|
|
@@ -97,6 +101,7 @@
|
|
|
97
101
|
"@webgpu/types": "0.1.66",
|
|
98
102
|
"oxlint": "1.88.0",
|
|
99
103
|
"oxlint-tsgolint": "7.0.2004",
|
|
100
|
-
"typescript": "5.9.3"
|
|
104
|
+
"typescript": "5.9.3",
|
|
105
|
+
"xstate": "5.33.2"
|
|
101
106
|
}
|
|
102
107
|
}
|