gdp-wired 0.13.0 → 0.13.1

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.
@@ -1,4 +1,4 @@
1
- import { type Cond, type Kind, type Rule, type Term, type TermLike, type Verdict, type Choice } from './dsl.js';
1
+ import { type Cond, type Kind, type Rule, type Term, type TermLike, type Verdict, Choice } from './dsl.js';
2
2
  import { type Law } from './machine.js';
3
3
  import type { StepLaw } from '../kernel/kernel.js';
4
4
  export type StateNode = {
@@ -19,10 +19,10 @@ type After<X> = X & {
19
19
  };
20
20
  export type Transition<P, I, X> = {
21
21
  from: string | string[];
22
- to: string;
22
+ to?: string;
23
23
  when?: (env: Env<P, I, X>) => Cond;
24
24
  set?: (env: Env<P, I, X>) => Partial<{
25
- [k in keyof X]: TermLike | Cond | X[k];
25
+ [k in keyof X]: X[k] extends Choice<infer K> ? K | X[k] : TermLike | Cond | X[k];
26
26
  }>;
27
27
  };
28
28
  export declare class Chart<P extends Obj = {}, I extends Obj = {}, X extends Obj = {}> {
@@ -32,8 +32,11 @@ export declare class Chart<P extends Obj = {}, I extends Obj = {}, X extends Obj
32
32
  params: Obj;
33
33
  inputs: Obj;
34
34
  context: Obj;
35
+ outputs: Obj;
35
36
  regions: [string, StateNode][];
36
37
  on: [string, Transition<P, I, X>][];
38
+ field?: string;
39
+ events?: string[];
37
40
  });
38
41
  private with;
39
42
  params<K extends Record<string, Kind<unknown>>>(kinds: K): Chart<P & {
@@ -50,8 +53,18 @@ export declare class Chart<P extends Obj = {}, I extends Obj = {}, X extends Obj
50
53
  kind: Kind<infer V>;
51
54
  } ? V : Term;
52
55
  }>;
56
+ outputs<T extends Record<string, number | {
57
+ kind: Kind<unknown>;
58
+ init: unknown;
59
+ }>>(init: T): Chart<P, I, X & {
60
+ [k in keyof T]: T[k] extends {
61
+ kind: Kind<infer V>;
62
+ } ? V : Term;
63
+ }>;
64
+ events(...names: string[]): Chart<P, I, X>;
53
65
  states(tree: Record<string, StateNode>, opts?: {
54
66
  initial?: string;
67
+ field?: string;
55
68
  }): Chart<P, I, X>;
56
69
  regions(regions: Record<string, StateNode>): Chart<P, I, X>;
57
70
  on(event: string, t: Transition<P, I, X>): Chart<P, I, X>;
@@ -16,7 +16,7 @@
16
16
  // most specific source (an inner state over its parent) is taken, then the first declared; the regions react to the
17
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
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";
19
+ import { Choice, all, any, no, oneOf, proveRule, proveSteps, rule, when, yes } from "./dsl.js";
20
20
  import { interpret, numbersOf } from "./machine.js";
21
21
  import { allowed } from "./watch.js";
22
22
  export class Chart {
@@ -30,8 +30,15 @@ export class Chart {
30
30
  inputs(kinds) { return this.with({ inputs: { ...this.s.inputs, ...kinds } }); }
31
31
  // The extended state: a natural from a number, or any kind from `kind.from(init)`.
32
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 }]] }); }
33
+ // What each step decides, read after it (a command, a verdict): every output starts each step at its default, and a
34
+ // transition taken may set it, so a step where none fires reads the default, never the last step's value. A default
35
+ // is a number, a choice (`oneOf(...).from('denied')`) or a yes/no (`bool.from(false)`).
36
+ outputs(init) { return this.with({ outputs: { ...this.s.outputs, ...init } }); }
37
+ // The events, in the order they are encoded (by default, the order they are first used in `on`).
38
+ events(...names) { return this.with({ events: names }); }
39
+ // One region's states (a chart of one region names its states plainly: 'active.paid'); `field` names the state
40
+ // variable the rule keeps them in (`state` by default).
41
+ states(tree, opts = {}) { return this.with({ regions: [['state', { states: tree, initial: opts.initial }]], field: opts.field }); }
35
42
  // Parallel regions, each with its own states, reacting to the same events: paths start with the region ('payment.paid').
36
43
  regions(regions) { return this.with({ regions: Object.entries(regions) }); }
37
44
  on(event, t) { return this.with({ on: [...this.s.on, [event, t]] }); }
@@ -64,7 +71,7 @@ export class Chart {
64
71
  }
65
72
  };
66
73
  walk('', root);
67
- return { name, field: single ? 'state' : name, leaves, finals, compounds };
74
+ return { name, field: single ? s.field ?? 'state' : name, leaves, finals, compounds };
68
75
  });
69
76
  // A path as the region and the state within it.
70
77
  const locate = (path) => {
@@ -81,14 +88,20 @@ export class Chart {
81
88
  throw new Error(`${s.name}: no state ${path} in ${r.name}`);
82
89
  return out;
83
90
  };
84
- const events = [...new Set(s.on.map(([e]) => e))];
91
+ const used = [...new Set(s.on.map(([e]) => e))], events = s.events ?? used;
92
+ for (const e of used)
93
+ if (!events.includes(e))
94
+ throw new Error(`${s.name}: the event ${e} isn't one of the events declared (${events.join(', ')})`);
95
+ if (new Set(events).size !== events.length)
96
+ throw new Error(`${s.name}: an event is declared twice`);
85
97
  if (!events.length)
86
98
  throw new Error(`${s.name}: a chart needs transitions`);
87
99
  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);
100
+ const froms = (Array.isArray(t.from) ? t.from : [t.from]).map(locate), [to] = t.to === undefined ? froms[0] : locate(t.to);
89
101
  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]);
102
+ throw new Error(`${s.name}: a transition on ${event} goes from one region to another (${t.to ?? [t.from].flat().join(", ")})`);
103
+ if (t.to !== undefined)
104
+ under(to, locate(t.to)[1]);
92
105
  const sources = [...new Set(froms.flatMap(([r, p]) => under(r, p)))];
93
106
  return { ...t, event, region: to.name, sources, depth: Math.min(...froms.map(([, p]) => (p === '' ? 0 : p.split('.').length))), order };
94
107
  });
@@ -103,7 +116,7 @@ export class Chart {
103
116
  if (history && c)
104
117
  state[lastOf(r, c)] = kind.from(enter(c));
105
118
  }
106
- for (const [k, v] of Object.entries(s.context))
119
+ for (const [k, v] of Object.entries({ ...s.context, ...s.outputs }))
107
120
  state[k] = v;
108
121
  const inState = (env) => (path) => { const [r, p] = locate(path); return any(...under(r, p).map(l => env[r.field].is(l))); };
109
122
  const guardEnv = (env) => this.wrap(env);
@@ -125,15 +138,17 @@ export class Chart {
125
138
  for (const r of regions) {
126
139
  // The transitions taken in a region exclude one another: a chain of `when`s, the first in order outermost.
127
140
  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]);
141
+ .reduce((acc, [t, i]) => when(env[`take${i}`], t.to === undefined ? env[r.field] : entered(env, r, locate(t.to)[1]), acc), env[r.field]);
129
142
  for (const [c, { history }] of r.compounds)
130
143
  if (history && c)
131
144
  out[lastOf(r, c)] = when(any(...under(r, c).map(l => out[r.field].is(l))), out[r.field], env[lastOf(r, c)]);
132
145
  }
133
- for (const k of Object.keys(s.context)) {
146
+ // Context keeps its value where no transition taken sets it; an output falls back to its default.
147
+ for (const k of [...Object.keys(s.context), ...Object.keys(s.outputs)]) {
134
148
  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]);
149
+ const base = k in s.outputs ? defaultOf(env[k], s.outputs[k], k) : env[k];
150
+ if (sets.length || k in s.outputs)
151
+ out[k] = sets.reverse().reduce((acc, [i, v]) => when(env[`take${i}`], v, acc), base);
137
152
  }
138
153
  return out;
139
154
  });
@@ -225,7 +240,8 @@ export class Chart {
225
240
  const v = proveRule(r, [...this.chartLaws(), never]);
226
241
  reached[name] = v.proven && v.checked ? 'unreachable' : 'unknown';
227
242
  }
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)));
243
+ // A way out is a transition to a state: an internal one stays where it is.
244
+ const stuck = regions.flatMap(reg => reg.leaves.filter(l => !reg.finals.has(l) && !placed.some(t => t.to !== undefined && t.region === reg.name && t.sources.includes(l))).map(l => label(reg, l)));
229
245
  const ambiguous = [];
230
246
  placed.forEach((t, i) => placed.forEach((u, j) => {
231
247
  if (j <= i || t.region !== u.region || t.event !== u.event || t.depth !== u.depth || !t.sources.some(l => u.sources.includes(l)))
@@ -238,4 +254,17 @@ export class Chart {
238
254
  return { ok: !stuck.length && !ambiguous.length && Object.values(reached).every(x => x !== 'unknown'), reached, stuck, ambiguous };
239
255
  }
240
256
  }
241
- export const chart = (name) => new Chart({ name, params: {}, inputs: {}, context: {}, regions: [], on: [] });
257
+ export const chart = (name) => new Chart({ name, params: {}, inputs: {}, context: {}, outputs: {}, regions: [], on: [] });
258
+ // An output's default as a value of its kind: a number, a choice's named value, or a yes/no.
259
+ function defaultOf(current, init, name) {
260
+ if (typeof init === 'number')
261
+ return init;
262
+ const { kind, init: v } = init;
263
+ if (current instanceof Choice)
264
+ return current.of(v);
265
+ if (typeof v === 'boolean')
266
+ return v ? yes : no;
267
+ if (typeof v === 'number')
268
+ return v;
269
+ throw new Error(`the output ${name}'s default is a number, a choice or a yes/no, not ${String(v)} of ${kind.constructor.name}`);
270
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdp-wired",
3
- "version": "0.13.0",
3
+ "version": "0.13.1",
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",