gdp-wired 0.12.5 → 0.12.7
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 +8 -4
- package/dist/core/dsl.d.ts +17 -25
- package/dist/core/dsl.js +67 -5
- package/dist/core/machine.js +47 -4
- package/dist/kernel/prove.js +7 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -13,6 +13,9 @@ the laws it must keep. gdp-wired proves them with no proof to write, shows a rea
|
|
|
13
13
|
npm install gdp-wired # or bun add gdp-wired; plain JavaScript with types, Node 20+, Bun or a browser
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
+
New to formal verification? [The guide](https://gdpwired.dev/guide) takes a rule from ordinary TypeScript to a proof in
|
|
17
|
+
seven steps, with recipes for limits, workflows, permissions and orders with line items.
|
|
18
|
+
|
|
16
19
|
```ts
|
|
17
20
|
import {min, nat, proveRule, rule, when} from 'gdp-wired'
|
|
18
21
|
|
|
@@ -52,7 +55,8 @@ All of these are proven automatically, and each proof is also a Lean theorem:
|
|
|
52
55
|
- **Invariants**: a law that holds after every sequence of requests ("spend stays within the budget").
|
|
53
56
|
- **Maps, sets and lists of names**: laws at every key ("no day passes its cap"), sets a request carries, conditions
|
|
54
57
|
about every key or every two keys ("a batch is taken only when its values increase"), lists kept in order
|
|
55
|
-
(`examples/sets.ts`, `examples/lists.ts`, `examples/pairs.ts`)
|
|
58
|
+
(`examples/sets.ts`, `examples/lists.ts`, `examples/pairs.ts`), and two groups of items with a set each, decided at
|
|
59
|
+
every pair at once (`examples/groups.ts`).
|
|
56
60
|
- **Step laws**: what one step does ("a balance only goes down", "a grant adds one"), and laws over up to 64 steps in
|
|
57
61
|
a row (`examples/steps.ts`).
|
|
58
62
|
- **Histories**: sums and queries over a log of requests, like a sliding-window rate limit.
|
|
@@ -312,7 +316,7 @@ const soon = retry.law('the next try is at most 64 after the last request', ({ne
|
|
|
312
316
|
- **Running it**: `rule.interpreter()` is persistent (searches branch from any state); `rule.compiled()` is what an app
|
|
313
317
|
ships: maps and the log updated in place, so a step costs the same however long the history. Both refuse params and
|
|
314
318
|
inputs that aren't naturals below 2^53 and any value past 2^53 − 1, where the proofs over the naturals stop describing
|
|
315
|
-
the run. The GPU searches refuse a space whose values could pass 2^32 − 1.
|
|
319
|
+
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.
|
|
316
320
|
|
|
317
321
|
## Numbers
|
|
318
322
|
|
|
@@ -351,7 +355,7 @@ Proving and checking: a law over 64 steps proves in about 5 s, and Lean checks i
|
|
|
351
355
|
| `core/` | Machines, the interpreter and JS compiler, the builders, composition (`compose.ts`), the linear solver and its checker, Houdini invariants, models, the first Lean export |
|
|
352
356
|
| `kernel/` | The trusted kernel and the untrusted prover, with soundness fuzz (`kernel.test.ts`, `history.test.ts`) |
|
|
353
357
|
| `lean-kernel/` | The kernel's Lean twin, its soundness proofs, and the exported trees |
|
|
354
|
-
| `examples/` | The builders at work: `domains/` (187 rules across 21 domains, each with laws and broken variants), `harness.ts` (the triple check), `readme.ts`, `budget.ts`, `gdp.ts` (gdp-ts's example, its checks proven), `datatypes.ts` (records and variants), `lemmas.ts` (recurrences and their lemmas), `invoices.ts` (lists a request carries), `folds.ts` (folds over trees), `trees.ts` (trees and lists of lists in the state), `composition.ts`, `sets.ts`, `quantifiers.ts`, `functions.ts`, `lists.ts`, `sizes.ts`, `liveness.ts`, `steps.ts` (laws over up to 64 steps), and `jev-fabric/` (a Bend project's 59 proven laws, ported) |
|
|
358
|
+
| `examples/` | The builders at work: `domains/` (187 rules across 21 domains, each with laws and broken variants), `harness.ts` (the triple check), `readme.ts`, `guide.ts` (the guide's code), `groups.ts` (two groups decided at every pair), `budget.ts`, `gdp.ts` (gdp-ts's example, its checks proven), `datatypes.ts` (records and variants), `lemmas.ts` (recurrences and their lemmas), `invoices.ts` (lists a request carries), `folds.ts` (folds over trees), `trees.ts` (trees and lists of lists in the state), `composition.ts`, `sets.ts`, `quantifiers.ts`, `functions.ts`, `lists.ts`, `sizes.ts`, `liveness.ts`, `steps.ts` (laws over up to 64 steps), and `jev-fabric/` (a Bend project's 59 proven laws, ported) |
|
|
355
359
|
| `docs/` | The comparison with Bend and Lean, and the full scope |
|
|
356
360
|
| `compare/` | The same three rules in gdp-wired, Bend and plain Lean, checked and measured |
|
|
357
361
|
| `research/` | Everything around the library ([`research/README.md`](research/README.md)): the GPU searches (`search/`, `hw/`, `modal-gpu/`), invariant and rank synthesis (`synth/`), relational laws and guard synthesis (`next/`), bit-vector laws (`sat/`), the enumeration and benchmarks (`enum/`, `gpu/`, `perf/`), the first Lean checker (`lean/`) and the first prototype, the audit path (`audit/`) |
|
|
@@ -379,7 +383,7 @@ bun test core/compile.test.ts core/runtime.test.ts kernel/history.test.ts
|
|
|
379
383
|
cd kernel && bun run-kernel.ts && bun test kernel.test.ts
|
|
380
384
|
```
|
|
381
385
|
|
|
382
|
-
The whole suite (
|
|
386
|
+
The whole suite (163 jobs, about 10 minutes of wall time and 46 minutes of job time) runs on [Modal](https://modal.com):
|
|
383
387
|
|
|
384
388
|
```sh
|
|
385
389
|
modal run modal/run.py --jobs modal/suite.json --out runs/modal/suite && sh modal/reap.sh
|
package/dist/core/dsl.d.ts
CHANGED
|
@@ -39,7 +39,8 @@ export declare function when(c: Cond, then: TermLike, otherwise: TermLike): Term
|
|
|
39
39
|
export declare function when(c: Cond, then: Decimal, otherwise: Decimal | number): Decimal;
|
|
40
40
|
export declare function when(c: Cond, then: Decimal | number, otherwise: Decimal): Decimal;
|
|
41
41
|
export declare function when(c: Cond, then: IntLike, otherwise: IntLike): Int;
|
|
42
|
-
export declare function when<K extends string>(c: Cond, then: Choice<K>, otherwise: Choice<K>): Choice<K>;
|
|
42
|
+
export declare function when<K extends string>(c: Cond, then: Choice<K> | NoInfer<K>, otherwise: Choice<K>): Choice<K>;
|
|
43
|
+
export declare function when<K extends string>(c: Cond, then: Choice<K>, otherwise: Choice<K> | NoInfer<K>): Choice<K>;
|
|
43
44
|
export declare function when(c: Cond, then: Str, otherwise: Str): Str;
|
|
44
45
|
export declare function when<V>(c: Cond, then: Maybe<V>, otherwise: Maybe<V>): Maybe<V>;
|
|
45
46
|
export declare function when(c: Cond, then: Cond, otherwise: Cond): Cond;
|
|
@@ -129,6 +130,8 @@ export declare class Choice<K extends string> {
|
|
|
129
130
|
readonly values: readonly K[];
|
|
130
131
|
constructor(n: Term, values: readonly K[]);
|
|
131
132
|
is(k: K): Cond;
|
|
133
|
+
of(k: K): Choice<K>;
|
|
134
|
+
private index;
|
|
132
135
|
match(cases: {
|
|
133
136
|
[k in K]: () => Cond;
|
|
134
137
|
}): Cond;
|
|
@@ -574,20 +577,10 @@ export declare class Rule<P extends Obj = {}, I extends Obj = {}, S extends Obj
|
|
|
574
577
|
private verdict;
|
|
575
578
|
private crossStep;
|
|
576
579
|
compare<St extends Record<string, Raw>>(reference: {
|
|
577
|
-
init
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
[k in keyof P]: Raw;
|
|
582
|
-
}, st: St, request: {
|
|
583
|
-
[k in keyof I]: Raw;
|
|
584
|
-
}) => St;
|
|
585
|
-
}, opts?: Draws<{
|
|
586
|
-
[k in keyof I]: Raw;
|
|
587
|
-
}> & {
|
|
588
|
-
params?: (rand: Rand) => {
|
|
589
|
-
[k in keyof P]: Raw;
|
|
590
|
-
};
|
|
580
|
+
init(params: Value<P>): St;
|
|
581
|
+
step(params: Value<P>, st: St, request: RawInputs<I>): St;
|
|
582
|
+
}, opts?: Draws<RawInputs<I>> & {
|
|
583
|
+
params?: (rand: Rand) => Value<P>;
|
|
591
584
|
depth?: number;
|
|
592
585
|
}): Agreement;
|
|
593
586
|
encodeParams(values: {
|
|
@@ -662,6 +655,9 @@ type CaseRaw<C extends Cases, T extends string = 'tag'> = {
|
|
|
662
655
|
type Value<T> = {
|
|
663
656
|
[k in keyof T]: ValueOf<T[k]>;
|
|
664
657
|
};
|
|
658
|
+
type RawInputs<T> = {
|
|
659
|
+
[k in keyof T]: T[k] extends MapRef ? ReadonlySet<number> | readonly number[] | ReadonlyMap<number, number> : T[k] extends CarriedList<infer V, infer _F> ? readonly ValueOf<V>[] : ValueOf<T[k]>;
|
|
660
|
+
};
|
|
665
661
|
type Field = Term | Cond | Int | Decimal | Choice<string> | Str | Variant<any, any> | Maybe<any> | {
|
|
666
662
|
[f: string]: Field | number;
|
|
667
663
|
};
|
|
@@ -682,16 +678,8 @@ export declare class Fun<P extends Obj = {}, I extends Obj = {}, O extends Obj =
|
|
|
682
678
|
private result;
|
|
683
679
|
law(name: string, given: (env: P & I, out: O) => Cond | Cond[], then: (out: O, env: P & I) => Cond): Decision;
|
|
684
680
|
relate(name: string, given: (a: P & I, b: P & I) => Cond | Cond[], then: (outA: O, outB: O, a: P & I, b: P & I) => Cond): Decision;
|
|
685
|
-
compare(reference: (values: {
|
|
686
|
-
|
|
687
|
-
}, params: {
|
|
688
|
-
[k in keyof P]: Raw;
|
|
689
|
-
}) => Partial<Value<O>>, opts?: Draws<{
|
|
690
|
-
[k in keyof I]: Raw;
|
|
691
|
-
}> & {
|
|
692
|
-
params?: {
|
|
693
|
-
[k in keyof P]: Raw;
|
|
694
|
-
};
|
|
681
|
+
compare(reference: (values: Value<I>, params: Value<P>) => Partial<Value<O>>, opts?: Draws<Value<I>> & {
|
|
682
|
+
params?: Value<P>;
|
|
695
683
|
}): Agreement;
|
|
696
684
|
get rule(): Rule<P, I>;
|
|
697
685
|
private compiledRule?;
|
|
@@ -756,6 +744,10 @@ export type Verdict = {
|
|
|
756
744
|
witness?: Record<string, number>;
|
|
757
745
|
why?: string;
|
|
758
746
|
trees?: Record<string, Tree>;
|
|
747
|
+
run?: {
|
|
748
|
+
params: Record<string, number>;
|
|
749
|
+
trace: Record<string, unknown>[];
|
|
750
|
+
};
|
|
759
751
|
};
|
|
760
752
|
export declare class Unproven extends Error {
|
|
761
753
|
readonly verdict: Verdict;
|
package/dist/core/dsl.js
CHANGED
|
@@ -94,6 +94,11 @@ export function when(c, then, otherwise) {
|
|
|
94
94
|
throw new Error('when: records of other fields');
|
|
95
95
|
return Object.fromEntries(Object.keys(then).map(f => [f, when(c, then[f], otherwise[f])]));
|
|
96
96
|
}
|
|
97
|
+
// A choice's name beside a choice is that choice's value: when(closing, 'closed', status).
|
|
98
|
+
if (then instanceof Choice && typeof otherwise === 'string')
|
|
99
|
+
return when(c, then, then.of(otherwise));
|
|
100
|
+
if (typeof then === 'string' && otherwise instanceof Choice)
|
|
101
|
+
return when(c, otherwise.of(then), otherwise);
|
|
97
102
|
if (then instanceof Choice && otherwise instanceof Choice)
|
|
98
103
|
return new Choice(when(c, then.n, otherwise.n), then.values);
|
|
99
104
|
if (then instanceof Str && otherwise instanceof Str)
|
|
@@ -348,11 +353,14 @@ export class Choice {
|
|
|
348
353
|
this.n = n;
|
|
349
354
|
this.values = values;
|
|
350
355
|
}
|
|
351
|
-
is(k) {
|
|
356
|
+
is(k) { return this.n.eq(this.index(k)); }
|
|
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) {
|
|
352
360
|
const i = this.values.indexOf(k);
|
|
353
361
|
if (i < 0)
|
|
354
362
|
throw new Error(`${k} is not one of ${this.values.join(', ')}`);
|
|
355
|
-
return
|
|
363
|
+
return i;
|
|
356
364
|
}
|
|
357
365
|
match(cases) {
|
|
358
366
|
const results = this.values.map(k => [k, cases[k]()]);
|
|
@@ -2118,7 +2126,7 @@ export class Rule {
|
|
|
2118
2126
|
// with requests drawn by their kinds (or `draw`), must hold the same values as the compiled rule (decoded: integers and
|
|
2119
2127
|
// decimals as numbers) for every field the reference's state has, after every step of `trials` runs of `depth`.
|
|
2120
2128
|
compare(reference, opts = {}) {
|
|
2121
|
-
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);
|
|
2122
2130
|
let checked = 0;
|
|
2123
2131
|
const sample = (kinds) => Object.fromEntries(kinds.map(([n, k]) => [n, k.sample(rand, bound)]));
|
|
2124
2132
|
for (let trial = 0; trial < (opts.trials ?? 200); trial++) {
|
|
@@ -2126,8 +2134,11 @@ export class Rule {
|
|
|
2126
2134
|
let ours = gen.init(ps), theirs = reference.init(params);
|
|
2127
2135
|
const edges = trial === 0 ? opts.edges ?? [] : [];
|
|
2128
2136
|
for (let d = 0; d < (opts.depth ?? 10) + edges.length; d++) {
|
|
2129
|
-
const request = edges[d] ?? opts.draw?.(rand) ?? sample(s.inputs);
|
|
2130
|
-
|
|
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);
|
|
2131
2142
|
theirs = reference.step(params, theirs, request);
|
|
2132
2143
|
const got = this.decode(ours);
|
|
2133
2144
|
for (const [field, want] of Object.entries(theirs)) {
|
|
@@ -2377,11 +2388,62 @@ const goalsVerdict = (goals) => {
|
|
|
2377
2388
|
};
|
|
2378
2389
|
// The kinds' laws first, those the caller didn't pass already (by name).
|
|
2379
2390
|
const withKinds = (r, laws) => [...r.kindLaws().filter(k => !laws.some(l => l.name === k.name)), ...laws];
|
|
2391
|
+
// A real run from the start that breaks a law or a step law, from a few random runs (each request drawn by its kinds,
|
|
2392
|
+
// those the rule's assumption refuses skipped): a false law is refused at once, with the run, where the symbolic search
|
|
2393
|
+
// would have to decide every case of its goal to reach one. Nothing here is trusted; a run it finds is a run the
|
|
2394
|
+
// interpreter took. A rule that reads functions is skipped: the kernel knows them only by their facts (a recurrence's
|
|
2395
|
+
// induction machine reads the function it proves things about), so a run computing them proves nothing about the goal.
|
|
2396
|
+
const refuteBudget = { trials: 60, depth: 6, bound: 3, ms: 150 };
|
|
2397
|
+
function refuteByRuns(r, laws, stepLaws = []) {
|
|
2398
|
+
const m = r.machine;
|
|
2399
|
+
if (Object.keys(m.fns ?? {}).length || m.facts?.length)
|
|
2400
|
+
return undefined;
|
|
2401
|
+
const run = interpret(m), rand = seeded(13), until = performance.now() + refuteBudget.ms, bound = refuteBudget.bound;
|
|
2402
|
+
const shown = (input) => Object.fromEntries(Object.entries(input).map(([n, x]) => [n, typeof x === 'number' ? x : Object.fromEntries(x)]));
|
|
2403
|
+
try {
|
|
2404
|
+
for (let trial = 0; trial < refuteBudget.trials && performance.now() < until; trial++) {
|
|
2405
|
+
const params = r.randomParams(rand, bound);
|
|
2406
|
+
if (m.pre && !evalC(m.pre, params, { scalars: {}, maps: {} }, m))
|
|
2407
|
+
continue;
|
|
2408
|
+
const watching = [...laws, ...stepLaws].map(w => ({ w, on: watch(m, run, w, params, bound) }));
|
|
2409
|
+
let st = run.init(params);
|
|
2410
|
+
const trace = [];
|
|
2411
|
+
// The witness: the params, the state the breaking step starts from (or the start) and its request's own values.
|
|
2412
|
+
const broken = (w, at, before, input) => ({ goal: `${w.name} ${at}`, witness: { ...params, ...before.scalars, ...(input ? numbersOf(input) : {}) }, run: { params, trace: trace.map(shown) } });
|
|
2413
|
+
for (const { w, on } of watching)
|
|
2414
|
+
if (!('then' in w) && !on.start(st))
|
|
2415
|
+
return broken(w, '@start', st);
|
|
2416
|
+
for (let d = 0, tries = 0; d < refuteBudget.depth && tries < 40; tries++) {
|
|
2417
|
+
const input = r.randomInput(rand, bound, params);
|
|
2418
|
+
if (!allowed(m, run, params, st, input))
|
|
2419
|
+
continue;
|
|
2420
|
+
d++;
|
|
2421
|
+
const next = run.step(params, st, input);
|
|
2422
|
+
trace.push(input);
|
|
2423
|
+
for (const { w, on } of watching)
|
|
2424
|
+
if (!on.step(st, input, next))
|
|
2425
|
+
return broken(w, !('then' in w) ? '@step' : (w.steps ?? 1) > 1 ? `@${w.steps} steps` : '@step law', st, input);
|
|
2426
|
+
st = next;
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
catch {
|
|
2431
|
+
return undefined;
|
|
2432
|
+
}
|
|
2433
|
+
return undefined;
|
|
2434
|
+
}
|
|
2435
|
+
const refutedVerdict = (out) => ({ proven: false, checked: false, goal: out.goal, stuck: 'sat', witness: out.witness, run: out.run });
|
|
2380
2436
|
export function proveRule(r, laws) {
|
|
2437
|
+
const refuted = refuteByRuns(r, laws);
|
|
2438
|
+
if (refuted)
|
|
2439
|
+
return refutedVerdict(refuted);
|
|
2381
2440
|
const out = proveLaws(r.machine, withKinds(r, laws));
|
|
2382
2441
|
return { proven: out.proven, checked: out.checked, ...(out.failed ? { goal: out.failed.goal, stuck: out.failed.stuck, ...(out.failed.witness ? { witness: out.failed.witness } : {}), ...(out.failed.why ? { why: out.failed.why } : {}) } : {}) };
|
|
2383
2442
|
}
|
|
2384
2443
|
export function proveSteps(r, laws, stepLaws) {
|
|
2444
|
+
const refuted = refuteByRuns(r, laws, stepLaws);
|
|
2445
|
+
if (refuted)
|
|
2446
|
+
return refutedVerdict(refuted);
|
|
2385
2447
|
const out = proveStepLaws(r.machine, withKinds(r, laws), stepLaws);
|
|
2386
2448
|
return { proven: out.proven, checked: out.checked, ...(out.failed ? { goal: out.failed.goal, stuck: out.failed.stuck, ...(out.failed.witness ? { witness: out.failed.witness } : {}), ...(out.failed.why ? { why: out.failed.why } : {}) } : {}) };
|
|
2387
2449
|
}
|
package/dist/core/machine.js
CHANGED
|
@@ -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])};`),
|
|
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});`)));
|
|
@@ -727,15 +761,24 @@ function renameKey(lin, from, to) {
|
|
|
727
761
|
}
|
|
728
762
|
// --- real counterexamples ----------------------------------------------------------------------------------------------
|
|
729
763
|
// Every sequence of up to `depth` requests, with params and inputs within 0..bound, from the start: the first that breaks
|
|
730
|
-
// the law, checked with the interpreter. A
|
|
731
|
-
//
|
|
764
|
+
// the law, checked with the interpreter. A variable `pre` or `assume` bounds by a constant of at most 15 (a choice, a
|
|
765
|
+
// yes/no, a small upTo) takes every value it may instead. A law that fails here is false; one that only fails `prove` may
|
|
766
|
+
// just need a stronger invariant.
|
|
732
767
|
export function search(m, law, depth = 3, bound = 2) {
|
|
733
768
|
const run = interpret(m);
|
|
734
769
|
const holds = (params, st) => {
|
|
735
770
|
const keys = law.forall ? [...new Set([0, ...Object.values(st.maps).flatMap(cells => [...cells.keys()])])] : [0];
|
|
736
771
|
return keys.every(key => evalC(law.holds, { ...params, ...(law.forall ? { [law.forall]: key } : {}) }, st, m));
|
|
737
772
|
};
|
|
738
|
-
const
|
|
773
|
+
const tops = new Map();
|
|
774
|
+
const conj = (c) => (c === undefined ? [] : c.op === 'and' ? c.xs.flatMap(conj) : [c]);
|
|
775
|
+
for (const c of [...conj(m.pre), ...conj(m.assume)])
|
|
776
|
+
if ((c.op === '<' || c.op === '<=') && c.a.op === 'var' && c.b.op === 'const') {
|
|
777
|
+
const top = c.op === '<' ? c.b.value - 1 : c.b.value;
|
|
778
|
+
if (top >= 0 && top <= 15)
|
|
779
|
+
tops.set(c.a.name, Math.min(top, tops.get(c.a.name) ?? top));
|
|
780
|
+
}
|
|
781
|
+
const tuples = (names) => names.reduce((acc, name) => acc.flatMap(t => Array.from({ length: (tops.get(name) ?? bound) + 1 }, (_, i) => ({ ...t, [name]: i }))), [{}]);
|
|
739
782
|
const assumed = (env, st) => m.assume === undefined || evalC(m.assume, env, st, m);
|
|
740
783
|
const meetsPre = (params) => m.pre === undefined || evalC(m.pre, params, { scalars: {}, maps: {} }, m);
|
|
741
784
|
for (const params of tuples(m.params)) {
|
package/dist/kernel/prove.js
CHANGED
|
@@ -137,9 +137,16 @@ const solveP = (rows, maxRows) => {
|
|
|
137
137
|
s.rows += rows.length;
|
|
138
138
|
return out;
|
|
139
139
|
};
|
|
140
|
+
// PROVE_TRACE=1 logs each goal grown from its root (hypotheses, nodes, time, outcome) to stderr.
|
|
141
|
+
const proveTrace = !!globalThis.process?.env?.PROVE_TRACE;
|
|
140
142
|
// The budget: 200,000 nodes and, by default, a second per hypothesis (at least 20 seconds) per goal; past either the goal
|
|
141
143
|
// is left `unknown`. A goal with many instances (a law about two keys at its pairs) has that many more rows per leaf.
|
|
142
144
|
export function grow(goal, path = new Map(), budget = { nodes: 0, until: performance.now() + Math.max(20_000, 1_000 * goal.hyps.length) }) {
|
|
145
|
+
if (proveTrace && !path.size && !budget.nodes) {
|
|
146
|
+
const t = performance.now(), out = growRoot(goal, path, budget);
|
|
147
|
+
console.error(`[grow] ${goal.name}: ${goal.hyps.length} hyps, ${budget.nodes} nodes, ${Math.round(performance.now() - t)} ms, ${'tree' in out ? 'proven' : `${out.stuck}${out.why ? ` (${out.why})` : ''}`}`);
|
|
148
|
+
return out;
|
|
149
|
+
}
|
|
143
150
|
if (!profile.on || path.size || budget.nodes)
|
|
144
151
|
return growRoot(goal, path, budget);
|
|
145
152
|
const t = performance.now(), s0 = profile.solves, m0 = profile.solveMs, r0 = profile.rowsMs, n0 = profile.nodes;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdp-wired",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.7",
|
|
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/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 && 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"
|
|
93
93
|
},
|
|
94
94
|
"devDependencies": {
|
|
95
95
|
"@gdp-ts/core": "0.1.0",
|