gdp-wired 0.13.1 → 0.13.2

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, Choice } from './dsl.js';
1
+ import { type Kind, type Rule, type TermLike, type Verdict, Choice, Cond, Term } 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 = {
@@ -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 { Choice, all, any, no, oneOf, proveRule, proveSteps, rule, when, yes } from "./dsl.js";
19
+ import { Choice, Cond, Term, 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 {
@@ -118,7 +118,6 @@ export class Chart {
118
118
  }
119
119
  for (const [k, v] of Object.entries({ ...s.context, ...s.outputs }))
120
120
  state[k] = v;
121
- const inState = (env) => (path) => { const [r, p] = locate(path); return any(...under(r, p).map(l => env[r.field].is(l))); };
122
121
  const guardEnv = (env) => this.wrap(env);
123
122
  // Entering a path: a leaf, or a compound's last child (with history) or its initial one.
124
123
  const entered = (env, r, path) => {
@@ -130,25 +129,41 @@ export class Chart {
130
129
  return entered(env, r, path ? `${path}.${c.initial}` : c.initial);
131
130
  };
132
131
  let built = rule(s.name).params(s.params).inputs({ event: oneOf(...events), ...s.inputs }).state(state);
133
- 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))] : []))])));
134
- // A transition is taken when it is enabled and no transition before it in its region is.
135
- 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()] : [])))])));
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
133
+ // state would be a disjunction the prover splits on; the whole region is no condition at all.
134
+ const among = (env, r, leaves) => {
135
+ const n = env[r.field].index, ix = leaves.map(l => r.leaves.indexOf(l)).sort((a, b) => a - b), top = r.leaves.length - 1, ranges = [];
136
+ for (const i of ix) {
137
+ const last = ranges.at(-1);
138
+ if (last && i === last[1] + 1)
139
+ last[1] = i;
140
+ else
141
+ ranges.push([i, i]);
142
+ }
143
+ return any(...ranges.map(([a, b]) => (a === 0 && b === top ? yes : a === b ? n.eq(a) : a === 0 ? n.atMost(b) : b === top ? n.atLeast(a) : n.atLeast(a).and(n.atMost(b)))));
144
+ };
145
+ const regionOf = (t) => regions.find(r => r.name === t.region);
146
+ built = built.flags((env) => Object.fromEntries(placed.map((t, i) => [`on${i}`, all(env.event.is(t.event), among(env, regionOf(t), t.sources), ...(t.when ? [t.when(guardEnv(env))] : []))])));
147
+ // A transition is taken when it is enabled and no transition before it in its region, on its event, from a state it
148
+ // shares, is (transitions on other events are never enabled together).
149
+ 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.event === t.event && u.sources.some(l => t.sources.includes(l)) ? [env[`on${j}`].not()] : [])))])));
136
150
  built = built.step((env) => {
137
151
  const out = {};
138
152
  for (const r of regions) {
139
- // The transitions taken in a region exclude one another: a chain of `when`s, the first in order outermost.
140
- out[r.field] = placed.map((t, i) => [t, i]).filter(([t]) => t.region === r.name).reverse()
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]);
153
+ // The transitions taken in a region exclude one another, so those entering one state share a `when` (any of
154
+ // them taken), and an internal one needs none (the state stays).
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]);
142
157
  for (const [c, { history }] of r.compounds)
143
158
  if (history && c)
144
159
  out[lastOf(r, c)] = when(any(...under(r, c).map(l => out[r.field].is(l))), out[r.field], env[lastOf(r, c)]);
145
160
  }
146
161
  // Context keeps its value where no transition taken sets it; an output falls back to its default.
147
162
  for (const k of [...Object.keys(s.context), ...Object.keys(s.outputs)]) {
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);
163
+ const sets = placed.map((t, i) => [t, i]).filter(([t]) => t.set).map(([t, i]) => ({ i, region: t.region, value: t.set(guardEnv(env))[k] })).filter(x => x.value !== undefined);
149
164
  const base = k in s.outputs ? defaultOf(env[k], s.outputs[k], k) : env[k];
150
165
  if (sets.length || k in s.outputs)
151
- out[k] = sets.reverse().reduce((acc, [i, v]) => when(env[`take${i}`], v, acc), base);
166
+ out[k] = grouped(sets.map(x => ({ ...x, value: typeof x.value === 'string' ? env[k].of(x.value) : x.value })), env, base);
152
167
  }
153
168
  return out;
154
169
  });
@@ -213,7 +228,9 @@ export class Chart {
213
228
  reachedAt.set(label(reg, l), k);
214
229
  } };
215
230
  note(queue[0], start);
216
- while (queue.length && seen.size < (opts.maxStates ?? 20_000)) {
231
+ // Breadth first, so each state's run is a shortest one; done once every state has one.
232
+ const states = regions.reduce((n, reg) => n + reg.leaves.length, 0);
233
+ while (queue.length && seen.size < (opts.maxStates ?? 20_000) && reachedAt.size < states) {
217
234
  const k = queue.shift(), st = seen.get(k).st;
218
235
  for (const req of requests) {
219
236
  if (!allowed(m, run, params, st, req))
@@ -228,32 +245,61 @@ export class Chart {
228
245
  }
229
246
  const runTo = (k) => { const out = []; for (let at = seen.get(k); at.by; at = seen.get(at.from))
230
247
  out.unshift({ ...numbersOf(at.by), event: events[at.by.event] }); return out; };
231
- const reached = {};
248
+ // States no run reached: proven unreachable together (one may lean on another), else each on its own.
249
+ const reached = {}, missed = [];
232
250
  for (const reg of regions)
233
251
  for (const l of reg.leaves) {
234
252
  const name = label(reg, l), at = reachedAt.get(name);
235
- if (at) {
253
+ if (at)
236
254
  reached[name] = { run: runTo(at) };
237
- continue;
238
- }
239
- const never = r.law(`${name} is never reached`, (env) => env[reg.field].is(l).not());
240
- const v = proveRule(r, [...this.chartLaws(), never]);
241
- reached[name] = v.proven && v.checked ? 'unreachable' : 'unknown';
255
+ else
256
+ missed.push({ name, law: r.law(`${name} is never reached`, (env) => env[reg.field].is(l).not()) });
242
257
  }
258
+ const proven = (v) => v.proven && v.checked;
259
+ const allMissed = missed.length > 0 && proven(proveRule(r, [...this.chartLaws(), ...missed.map(x => x.law)]));
260
+ for (const x of missed)
261
+ reached[x.name] = allMissed || proven(proveRule(r, [...this.chartLaws(), x.law])) ? 'unreachable' : 'unknown';
243
262
  // A way out is a transition to a state: an internal one stays where it is.
244
263
  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)));
245
- const ambiguous = [];
264
+ // Pairs as specific as each other, on one event, from a state they share: proven never both enabled, all at once,
265
+ // and only where that fails each on its own (to name the pair and the request).
266
+ const pairs = [];
246
267
  placed.forEach((t, i) => placed.forEach((u, j) => {
247
268
  if (j <= i || t.region !== u.region || t.event !== u.event || t.depth !== u.depth || !t.sources.some(l => u.sources.includes(l)))
248
269
  return;
249
- 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());
250
- const v = proveSteps(r, this.chartLaws(), [apart]);
251
- if (!(v.proven && v.checked))
252
- ambiguous.push({ event: t.event, transitions: [t.order, u.order], verdict: v });
270
+ pairs.push({ t, u, law: r.stepLaw(`transitions ${t.order} and ${u.order} on ${t.event} are never both enabled`, (env) => all(env[`on${i}`], env[`on${j}`]).not()) });
253
271
  }));
272
+ const ambiguous = [];
273
+ if (pairs.length && !proven(proveSteps(r, this.chartLaws(), pairs.map(x => x.law))))
274
+ for (const { t, u, law } of pairs) {
275
+ const v = proveSteps(r, this.chartLaws(), [law]);
276
+ if (!proven(v))
277
+ ambiguous.push({ event: t.event, transitions: [t.order, u.order], verdict: v });
278
+ }
254
279
  return { ok: !stuck.length && !ambiguous.length && Object.values(reached).every(x => x !== 'unknown'), reached, stuck, ambiguous };
255
280
  }
256
281
  }
282
+ // The value a step writes: of the transitions taken, the first in order that writes one. Transitions of one region exclude
283
+ // one another, so consecutive ones of a region writing the same value share one `when` (any of them taken); the order
284
+ // between regions is kept.
285
+ function grouped(writes, env, base) {
286
+ const keyOf = (v) => JSON.stringify(v instanceof Choice ? v.index.e : v instanceof Cond ? v.c : v instanceof Term ? v.e : v);
287
+ const groups = [];
288
+ for (const w of writes) {
289
+ const key = keyOf(w.value);
290
+ let g;
291
+ for (let j = groups.length - 1; j >= 0 && groups[j].region === w.region; j--)
292
+ if (groups[j].key === key) {
293
+ g = groups[j];
294
+ break;
295
+ }
296
+ if (g)
297
+ g.takes.push(env[`take${w.i}`]);
298
+ else
299
+ groups.push({ region: w.region, key, value: w.value, takes: [env[`take${w.i}`]] });
300
+ }
301
+ return groups.reduceRight((acc, g) => when(any(...g.takes), g.value, acc), base);
302
+ }
257
303
  export const chart = (name) => new Chart({ name, params: {}, inputs: {}, context: {}, outputs: {}, regions: [], on: [] });
258
304
  // An output's default as a value of its kind: a number, a choice's named value, or a yes/no.
259
305
  function defaultOf(current, init, name) {
@@ -131,7 +131,10 @@ export declare class Choice<K extends string> {
131
131
  constructor(n: Term, values: readonly K[]);
132
132
  is(k: K): Cond;
133
133
  of(k: K): Choice<K>;
134
- private index;
134
+ get index(): Term;
135
+ eq(o: K | Choice<K> | number): Cond;
136
+ ne(o: K | Choice<K> | number): Cond;
137
+ private indexOf;
135
138
  match(cases: {
136
139
  [k in K]: () => Cond;
137
140
  }): Cond;
package/dist/core/dsl.js CHANGED
@@ -353,10 +353,15 @@ export class Choice {
353
353
  this.n = n;
354
354
  this.values = values;
355
355
  }
356
- is(k) { return this.n.eq(this.index(k)); }
356
+ is(k) { return this.n.eq(this.indexOf(k)); }
357
357
  // The value named k, of this choice's kind (to write: when(c, status.of('closed'), status)).
358
- of(k) { return new Choice(lit(this.index(k)), this.values); }
359
- index(k) {
358
+ of(k) { return new Choice(lit(this.indexOf(k)), this.values); }
359
+ // Its index among the values, in the order declared (oneOf('new', 'paid'): new is 0), as a term.
360
+ get index() { return this.n; }
361
+ // The same value as a name, another choice of the kind, or an index: status.eq('paid'), a.phase.eq(b.phase), phase.eq(3).
362
+ eq(o) { return typeof o === 'string' ? this.is(o) : typeof o === 'number' ? this.n.eq(o) : this.n.eq(o.n); }
363
+ ne(o) { return this.eq(o).not(); }
364
+ indexOf(k) {
360
365
  const i = this.values.indexOf(k);
361
366
  if (i < 0)
362
367
  throw new Error(`${k} is not one of ${this.values.join(', ')}`);
@@ -2426,13 +2431,31 @@ function refuteByRuns(r, laws, stepLaws = []) {
2426
2431
  st = next;
2427
2432
  }
2428
2433
  }
2434
+ // A step law holds from every state the laws allow, reached or not: from random such states (each scalar and cell
2435
+ // small, kept when every law and kind law holds there), a step that breaks it is a counterexample too, without a run.
2436
+ const steps1 = stepLaws.filter(sl => (sl.steps ?? 1) === 1), given = withKinds(r, laws);
2437
+ for (let trial = 0; steps1.length && trial < refuteBudget.trials * 4 && performance.now() < until; trial++) {
2438
+ const params = r.randomParams(rand, bound);
2439
+ if (m.pre && !evalC(m.pre, params, { scalars: {}, maps: {} }, m))
2440
+ continue;
2441
+ const st = { scalars: Object.fromEntries(Object.keys(m.state).map(n => [n, rand(8)])), maps: Object.fromEntries(Object.keys(m.maps ?? {}).map(n => [n, new Map(Array.from({ length: rand(3) }, () => [rand(bound + 1), rand(bound + 1)]))])) };
2442
+ if (m.history || !given.every(l => watch(m, run, l, params, bound).start(st)))
2443
+ continue;
2444
+ const input = r.randomInput(rand, bound, params);
2445
+ if (!allowed(m, run, params, st, input))
2446
+ continue;
2447
+ const next = run.step(params, st, input);
2448
+ for (const sl of steps1)
2449
+ if (!watch(m, run, sl, params, bound).step(st, input, next))
2450
+ return { goal: `${sl.name} @step law`, witness: { ...params, ...st.scalars, ...numbersOf(input) } };
2451
+ }
2429
2452
  }
2430
2453
  catch {
2431
2454
  return undefined;
2432
2455
  }
2433
2456
  return undefined;
2434
2457
  }
2435
- const refutedVerdict = (out) => ({ proven: false, checked: false, goal: out.goal, stuck: 'sat', witness: out.witness, run: out.run });
2458
+ const refutedVerdict = (out) => ({ proven: false, checked: false, goal: out.goal, stuck: 'sat', witness: out.witness, ...(out.run ? { run: out.run } : {}) });
2436
2459
  export function proveRule(r, laws) {
2437
2460
  const refuted = refuteByRuns(r, laws);
2438
2461
  if (refuted)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdp-wired",
3
- "version": "0.13.1",
3
+ "version": "0.13.2",
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",