gdp-wired 0.9.0 → 0.10.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 CHANGED
@@ -67,6 +67,10 @@ All of these are proven automatically, and each proof is also a Lean theorem:
67
67
  - **Records and variants**: inputs, state, results, map cells and list items made of named fields, or one of several
68
68
  cases each with fields of its own (a payment by card or in cash, a seat free, held or sold), every case covered
69
69
  (`examples/datatypes.ts`).
70
+ - **Lists a request carries, and trees**: an invoice's lines, with folds over them (the total, the taxable part) and
71
+ lemmas about the folds proven by induction over the lines (`examples/invoices.ts`); a fold over a tree as a value (a
72
+ spend roll-up over an org chart, sign-offs over a document's sections), its laws proven by induction over the tree
73
+ (`examples/folds.ts`).
70
74
  - **Pure functions and two-run laws**: laws about what a function returns, and laws comparing two calls, like "the
71
75
  secret never changes the answer". `examples/jev-fabric/` ports a Bend project's policy core and proves all 59 laws its
72
76
  Bend proofs state.
@@ -218,7 +222,19 @@ const soon = retry.law('the next try is at most 64 after the last request', ({ne
218
222
  twin see nothing new. Maps and lists of them: `.maps({Acct: record({...}).from({...})})` (`Acct.at(k)`,
219
223
  `Acct.set(k, value)`, `Acct.each(k => value)`) and `.items({Queue: Job})` (`push`, `pop`, `set`, `insertAt`,
220
224
  `removeAt`, `apply`, `has`, `within`, `increasing(by)`), one map per variable of the kind; `rule.decode` reads them back
221
- as objects and arrays. Not as values: recursive types (a tree).
225
+ as objects and arrays.
226
+ - **Lists a request carries** (`.carriesList('Lines', Line, folds, lemmas)`): the items one carried map per variable of
227
+ the kind, the length `Lines.len`, each fold (`{total: {from: 0, step: (acc, line) => acc.plus(line.amount)}}`) a
228
+ carried map of its running values whose equations every request is assumed to meet (the encoding computes them from the
229
+ array, and a request that doesn't meet them isn't allowed), read as `Lines.fold.total`; `Lines.every`, `some`, `has`,
230
+ `increasing(by)`, `at(i)`. A lemma about the folds (`({taxed, total}) => taxed.atMost(total)`) is proven by induction
231
+ over the items when the rule is built, then assumed at every prefix (`examples/invoices.ts`).
232
+ - **Folds over trees** (`fold(name).params(...).inputs(...).leaf(env => ({...})).node((env, left, right) => ({...}))`,
233
+ `proveFold`): a tree as a value (`{leaf: inputs}` or `{node: inputs, left, right}`), each leaf's values from its inputs
234
+ and each node's from its own and its children's; a law about the values is proven by induction over the tree (the
235
+ kernel's leaf and node goals, Lean's `fold_sound`), `f.run(tree, params)` evaluates the terms proven and `f.crossCheck`
236
+ holds the builders to them (`examples/folds.ts`). A tree in a rule's state, and claims that need a lemma about the
237
+ tree's shape nobody stated, stay Lean's.
222
238
  - **State of any kind**: `.state({open: bool.from(true), status: oneOf('new', 'done').from('new'), owner: str().from(''), note: maybe(nat).from(null)})`;
223
239
  a step writes each kind's own values, `when(c, a, b)` chooses between any two values of a kind, and `rule.decode(state)`
224
240
  reads them back as runtime values.
@@ -313,7 +329,7 @@ Lean, compiled natively, wins where the state stays small. Details in [`docs/com
313
329
  | `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 |
314
330
  | `kernel/` | The trusted kernel and the untrusted prover, with soundness fuzz (`kernel.test.ts`, `history.test.ts`) |
315
331
  | `lean-kernel/` | The kernel's Lean twin, its soundness proofs, and the exported trees |
316
- | `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), `composition.ts`, `sets.ts`, `quantifiers.ts`, `functions.ts`, `lists.ts`, `sizes.ts`, `liveness.ts`, `steps.ts` (laws over up to 16 steps), and `jev-fabric/` (a Bend project's 59 proven laws, ported) |
332
+ | `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), `composition.ts`, `sets.ts`, `quantifiers.ts`, `functions.ts`, `lists.ts`, `sizes.ts`, `liveness.ts`, `steps.ts` (laws over up to 16 steps), and `jev-fabric/` (a Bend project's 59 proven laws, ported) |
317
333
  | `docs/` | The comparison with Bend and Lean, and the full scope |
318
334
  | `compare/` | The same three rules in gdp-wired, Bend and plain Lean, checked and measured |
319
335
  | `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/`) |
@@ -1,5 +1,5 @@
1
1
  import { type C, type E, type Law, type Recurrence, type Machine, type Request, type State } from './machine.js';
2
- import { type Goal, type Liveness, type Loop, type StepLaw, type Tree } from '../kernel/kernel.js';
2
+ import { type Fold, type Goal, type Liveness, type Loop, type StepLaw, type Tree } from '../kernel/kernel.js';
3
3
  export type TermLike = Term | number;
4
4
  export declare const termOf: (t: TermLike) => E;
5
5
  export declare class Term {
@@ -310,6 +310,35 @@ export type ItemsOp = {
310
310
  readonly parts: Record<string, ListOp>;
311
311
  readonly len: TermLike;
312
312
  };
313
+ export type ListFold<V> = {
314
+ from: number;
315
+ step: (acc: Term, item: V) => TermLike;
316
+ };
317
+ export type ListLemma = (acc: Record<string, Term>, i: Term) => Cond;
318
+ export declare class CarriedList<V, Fd extends string = never> {
319
+ readonly name: string;
320
+ readonly kind: Kind<V>;
321
+ readonly len: Term;
322
+ readonly folds: Record<Fd, ListFold<V>>;
323
+ private readonly map;
324
+ readonly lemmas: Record<string, ListLemma>;
325
+ readonly fold: Record<Fd, Term>;
326
+ constructor(name: string, kind: Kind<V>, len: Term, folds: Record<Fd, ListFold<V>>, map: (part: string) => MapRef, lemmas?: Record<string, ListLemma>);
327
+ at(i: TermLike): V;
328
+ after(fold: Fd, i: TermLike): Term;
329
+ every(f: (item: V, i: Term) => Cond): Cond;
330
+ some(f: (item: V, i: Term) => Cond): Cond;
331
+ has(x: V): Cond;
332
+ increasing(by: (item: V) => Term): Cond;
333
+ equations(): Cond[];
334
+ private running;
335
+ prove(): Verdict;
336
+ get maps(): string[];
337
+ encode(raw: Raw): {
338
+ maps: Record<string, Map<number, number>>;
339
+ len: number;
340
+ };
341
+ }
313
342
  export declare class ItemsRef<V> {
314
343
  readonly name: string;
315
344
  readonly kind: Kind<V>;
@@ -392,6 +421,7 @@ type Spec = {
392
421
  items?: [string, Kind<unknown>][];
393
422
  kinded?: [string, Kind<unknown>][];
394
423
  carried?: string[];
424
+ lists2?: [string, Kind<unknown>, Record<string, ListFold<unknown>>, Record<string, ListLemma>][];
395
425
  fns?: [string, FnDef][];
396
426
  facts?: ((env: never) => Record<string, Cond | Forall>)[];
397
427
  flags: ((env: never) => Record<string, Cond>)[];
@@ -444,6 +474,9 @@ export declare class Rule<P extends Obj = {}, I extends Obj = {}, S extends Obj
444
474
  carries<const N extends string>(...names: N[]): Rule<P, I & {
445
475
  [k in N]: MapRef;
446
476
  }, S, M, F, R, Su, Q>;
477
+ carriesList<const N extends string, V, Fd extends string = never>(name: N, of: Kind<V>, folds?: Record<Fd, ListFold<V>>, lemmas?: Record<string, (acc: Record<Fd, Term>, i: Term) => Cond>): Rule<P, I & {
478
+ [k in N]: CarriedList<V, Fd>;
479
+ }, S, M, F, R, Su, Q>;
447
480
  flags<T extends Record<string, Cond>>(f: (env: P & I & S & M & F & Su & Q) => T): Rule<P, I, S, M, F & {
448
481
  [k in keyof T]: Cond;
449
482
  }, R, Su, Q>;
@@ -516,7 +549,7 @@ export declare class Rule<P extends Obj = {}, I extends Obj = {}, S extends Obj
516
549
  [k in keyof P]: Raw;
517
550
  }): Record<string, number>;
518
551
  encodeInput(values: {
519
- [k in keyof I]: I[k] extends MapRef ? ReadonlySet<number> | readonly number[] | ReadonlyMap<number, number> : Raw;
552
+ [k in keyof I]: I[k] extends MapRef ? ReadonlySet<number> | readonly number[] | ReadonlyMap<number, number> : I[k] extends CarriedList<any, any> ? readonly Raw[] : Raw;
520
553
  }): Request;
521
554
  randomParams(rand: Rand, bound: number): Record<string, number>;
522
555
  randomInput(rand: Rand, bound: number): Request;
@@ -690,3 +723,55 @@ export declare function crossCheckDecision(d: Decision, opts?: {
690
723
  failure?: string;
691
724
  };
692
725
  export declare const proveLoop: (l: LoopRule<Obj, Obj, Obj>, laws: Law[]) => Verdict;
726
+ export type TreeValue<I> = {
727
+ leaf: I;
728
+ } | {
729
+ node: I;
730
+ left: TreeValue<I>;
731
+ right: TreeValue<I>;
732
+ };
733
+ type FoldSpec = {
734
+ name: string;
735
+ params: [string, Kind<unknown>][];
736
+ inputs: [string, Kind<unknown>][];
737
+ pre: ((p: never) => Cond)[];
738
+ assume: ((env: never) => Cond)[];
739
+ leaf?: (env: never) => Record<string, TermLike>;
740
+ node?: (env: never, left: never, right: never) => Record<string, TermLike>;
741
+ };
742
+ export declare class FoldRule<P extends Obj = {}, I extends Obj = {}, S extends Obj = {}> {
743
+ private readonly spec;
744
+ constructor(spec: FoldSpec);
745
+ private with;
746
+ get name(): string;
747
+ params<K extends Kinds>(kinds: K): FoldRule<P & ValuesOf<K>, I, S>;
748
+ inputs<K extends Kinds>(kinds: K): FoldRule<P, I & ValuesOf<K>, S>;
749
+ pre(f: (p: P) => Cond): FoldRule<P, I, S>;
750
+ assume(f: (env: P & I) => Cond): FoldRule<P, I, S>;
751
+ leaf<T extends Record<string, TermLike>>(f: (env: P & I) => T): FoldRule<P, I, {
752
+ [k in keyof T]: Term;
753
+ }>;
754
+ node(f: (env: P & I, left: S, right: S) => {
755
+ [k in keyof S]: TermLike;
756
+ }): FoldRule<P, I, S>;
757
+ private env;
758
+ private names;
759
+ private side;
760
+ law(name: string, f: (env: P & S) => Cond): Law;
761
+ get fold(): Fold;
762
+ run(tree: TreeValue<{
763
+ [k in keyof I]: Raw;
764
+ }>, params?: {
765
+ [k in keyof P]?: Raw;
766
+ }): {
767
+ [k in keyof S]: number;
768
+ };
769
+ crossCheck(laws?: Law[], opts?: {
770
+ trials?: number;
771
+ depth?: number;
772
+ bound?: number;
773
+ rand?: Rand;
774
+ }): Agreement;
775
+ }
776
+ export declare const fold: (name: string) => FoldRule;
777
+ export declare const proveFold: (f: FoldRule<Obj, Obj, Obj>, laws: Law[]) => Verdict;
package/dist/core/dsl.js CHANGED
@@ -15,9 +15,9 @@
15
15
  //
16
16
  // A step returns its updates as one object: every right-hand side reads the state before the step, as the kernel and
17
17
  // both runtimes do.
18
- import { carriedView, compileFunction, evalC, interpret, loadJs, numbersOf } from "./machine.js";
18
+ import { carriedView, compileFunction, evalC, evalE, interpret, loadJs, numbersOf } from "./machine.js";
19
19
  import { allowed, keysOf, requestEnv, watch } from "./watch.js";
20
- import { check, loopObligations } from "../kernel/kernel.js";
20
+ import { check, foldObligations, loopObligations } from "../kernel/kernel.js";
21
21
  import { factsFor, grow, proveLaws, proveLiveness, proveStepLaws } from "../kernel/prove.js";
22
22
  // --- terms and conditions -------------------------------------------------------------------------------------------
23
23
  const naturalConst = (v) => {
@@ -867,6 +867,89 @@ export class CellsRef {
867
867
  return { parts: Object.fromEntries(this.kind.vars(this.name).map(part => [part, this.map(part).each(k => partsOf(this.kind, this.name, f(k))[part])])) };
868
868
  }
869
869
  }
870
+ export class CarriedList {
871
+ name;
872
+ kind;
873
+ len;
874
+ folds;
875
+ map;
876
+ lemmas;
877
+ fold;
878
+ constructor(name, kind, len, folds, map, lemmas = {}) {
879
+ this.name = name;
880
+ this.kind = kind;
881
+ this.len = len;
882
+ this.folds = folds;
883
+ this.map = map;
884
+ this.lemmas = lemmas;
885
+ this.fold = Object.fromEntries(Object.keys(folds).map(f => [f, map(`${name}$${f}`).at(len)]));
886
+ }
887
+ at(i) { return this.kind.build(this.name, part => this.map(part).at(i)); }
888
+ // The running value of a fold after the first i items.
889
+ after(fold, i) { return this.map(`${this.name}$${fold}`).at(i); }
890
+ every(f) { return everyKey(k => k.lt(this.len).implies(f(this.at(k), k))); }
891
+ some(f) { return this.every((x, i) => f(x, i).not()).not(); }
892
+ has(x) { return this.some(y => same(y, x)); }
893
+ // Each item's `by` below the next one's (so no two alike).
894
+ increasing(by) { return everyKey(k => k.plus(1).lt(this.len).implies(by(this.at(k)).lt(by(this.at(k.plus(1)))))); }
895
+ // What every request is assumed to meet: each fold's start, and at every key one condition (the item's own laws and
896
+ // each fold's step below the length, each lemma up to it), so the prover closes one quantifier, not one per part.
897
+ equations() {
898
+ const folds = Object.entries(this.folds), lemmas = Object.values(this.lemmas);
899
+ const at = (k) => {
900
+ const x = this.at(k), steps = folds.map(([f, d]) => this.map(`${this.name}$${f}`).at(k.plus(1)).eq(d.step(this.map(`${this.name}$${f}`).at(k), x)));
901
+ const below = [...this.kind.invariant(x), ...steps];
902
+ return all(...(below.length ? [k.lt(this.len).implies(all(...below))] : []), ...(lemmas.length ? [k.atMost(this.len).implies(all(...lemmas.map(l => l(this.running(k), k))))] : []));
903
+ };
904
+ const any = this.kind.invariant(this.kind.value(this.name)).length + folds.length + lemmas.length > 0;
905
+ return [...folds.map(([f, d]) => this.map(`${this.name}$${f}`).at(0).eq(d.from)), ...(any ? [everyKey(at)] : [])];
906
+ }
907
+ running(k) { return Object.fromEntries(Object.keys(this.folds).map(f => [f, this.after(f, k)])); }
908
+ // The lemmas, proven: the folds as a machine counting i up from 0 over items that are functions of i known only by
909
+ // the kind's own laws, each lemma a law of it (so it holds after every prefix of every list).
910
+ prove() {
911
+ if (!Object.keys(this.lemmas).length)
912
+ return { proven: true, checked: true };
913
+ const parts = this.kind.vars(this.name), fname = (p) => `item$${p.replaceAll('.', '$')}`;
914
+ const item = (i) => this.kind.build(this.name, p => new Term({ op: 'get', map: fname(p), key: i.e }));
915
+ // Every item is a value of the kind (its own laws, as a fact of the first part's function).
916
+ const kindFact = this.kind.invariant(this.kind.value(this.name)).length ? { 'every item is a value of its kind': forall(k => all(...this.kind.invariant(item(k)))) } : {};
917
+ const fns = Object.fromEntries(parts.map((p, j) => [fname(p), fn(1, 'x => 0', j === 0 ? { facts: () => kindFact } : {})]));
918
+ const folds = Object.entries(this.folds);
919
+ const r = rule(`${this.name}'s folds, by induction`).fns(fns).inputs({}).state({ i: 0, ...Object.fromEntries(folds.map(([f, d]) => [`acc$${f}`, d.from])) })
920
+ .step((env) => ({ i: env.i.plus(1), ...Object.fromEntries(folds.map(([f, d]) => [`acc$${f}`, d.step(env[`acc$${f}`], item(env.i))])) }));
921
+ const accs = (env) => Object.fromEntries(folds.map(([f]) => [f, env[`acc$${f}`]]));
922
+ return proveRule(r, Object.entries(this.lemmas).map(([n, l]) => r.law(n, (env) => l(accs(env), env.i))));
923
+ }
924
+ // Every carried map it is kept in: the items' parts, then each fold's.
925
+ get maps() { return [...this.kind.vars(this.name), ...Object.keys(this.folds).map(f => `${this.name}$${f}`)]; }
926
+ // A request's list as those maps and its length: each fold computed by its step, as the builders compute it.
927
+ encode(raw) {
928
+ if (!Array.isArray(raw))
929
+ throw new RangeError(`${this.name} must be a list, not ${shown(raw)}`);
930
+ const items = raw.map(x => this.kind.encode(this.name, x));
931
+ const maps = Object.fromEntries(this.maps.map(m => [m, new Map()]));
932
+ items.forEach((enc, i) => { for (const [part, x] of Object.entries(enc))
933
+ if (x !== 0)
934
+ maps[part].set(i, x); });
935
+ for (const [f, d] of Object.entries(this.folds)) {
936
+ const acc = maps[`${this.name}$${f}`];
937
+ let value = d.from;
938
+ if (value !== 0)
939
+ acc.set(0, value);
940
+ items.forEach((enc, i) => {
941
+ const item = this.kind.build(this.name, part => new Term({ op: 'const', value: enc[part] }, enc[part]));
942
+ const next = valOf(d.step(new Term({ op: 'const', value }, value), item));
943
+ if (next === undefined || !Number.isSafeInteger(next) || next < 0)
944
+ throw new RangeError(`${this.name}'s fold ${f} reads more than its items`);
945
+ value = next;
946
+ if (value !== 0)
947
+ acc.set(i + 1, value);
948
+ });
949
+ }
950
+ return { maps, len: items.length };
951
+ }
952
+ }
870
953
  export class ItemsRef {
871
954
  name;
872
955
  kind;
@@ -1371,6 +1454,27 @@ export class Rule {
1371
1454
  facts(f) { return this.with({ facts: [...(this.spec.facts ?? []), f] }); }
1372
1455
  // Maps each request carries whole (a set of names, say: cells 0 or 1), every cell past them 0; read like any map.
1373
1456
  carries(...names) { return this.with({ carried: [...(this.spec.carried ?? []), ...names] }); }
1457
+ // A list of values of a kind each request carries, with folds over it and lemmas about the folds (CarriedList):
1458
+ // .carriesList('Lines', record({amount: nat}), {total: {from: 0, step: (acc, line) => acc.plus(line.amount)}})
1459
+ carriesList(name, of, folds = {}, lemmas = {}) {
1460
+ const entries = [[name, of, folds, lemmas]];
1461
+ for (const [n, , fs] of entries)
1462
+ for (const f of Object.keys(fs))
1463
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(f))
1464
+ throw new Error(`${n}: ${f} is not a plain fold name`);
1465
+ // The folds' lemmas, proven now (a lemma that isn't throws, with the verdict).
1466
+ for (const [n, k, fs, ls] of entries) {
1467
+ const v2 = new CarriedList(n, k, v(`${n}_len`), fs, part => new MapRef(part, 0), ls).prove();
1468
+ if (!(v2.proven && v2.checked))
1469
+ throw new Unproven(`${n}'s fold lemmas`, v2);
1470
+ }
1471
+ for (const [n, , fs] of entries)
1472
+ for (const f of Object.keys(fs))
1473
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(f))
1474
+ throw new Error(`${n}: ${f} is not a plain fold name`);
1475
+ const maps = entries.flatMap(([n, k, fs]) => [...k.vars(n), ...Object.keys(fs).map(f => `${n}$${f}`)]);
1476
+ return this.with({ carried: [...(this.spec.carried ?? []), ...maps], inputs: [...this.spec.inputs, ...entries.map(([n]) => [`${n}_len`, nat])], lists2: [...(this.spec.lists2 ?? []), ...entries] });
1477
+ }
1374
1478
  // Named conditions of the step, decided once per step before it; each call's flags may read the earlier calls'. A flag
1375
1479
  // named again replaces the earlier definition in place (what a variant needs: one call, the step unchanged); the new
1376
1480
  // definition may read only the flags defined before the original's place, as the kernel checks.
@@ -1500,6 +1604,8 @@ export class Rule {
1500
1604
  // r.relate('the secret is irrelevant', (a, b) => a.user.eq(b.user), (a, b) => a.allowed.iff(b.allowed))
1501
1605
  relate(name, given, then) {
1502
1606
  const s = this.spec, b = (n) => `b.${n}`, own = [...s.inputs.map(([n]) => n), ...(s.carried ?? [])];
1607
+ if (s.lists2?.length)
1608
+ throw new Error(`${s.name}: a decision about two requests takes no carried list`);
1503
1609
  const second = (env) => ({ ...env, ...Object.fromEntries(own.map(n => [n, env[b(n)]])) });
1504
1610
  const pair = new Rule({ ...s, inputs: [...s.inputs, ...s.inputs.map(([n, k]) => [b(n), k])], carried: [...(s.carried ?? []), ...(s.carried ?? []).map(b)],
1505
1611
  assume: [...s.assume, ...s.assume.map(f => (env) => f(second(env)))], flags: [], step: undefined, record: undefined, sums: [], queries: [], keep: undefined, relations: undefined, prune: undefined });
@@ -1543,6 +1649,12 @@ export class Rule {
1543
1649
  out[n] = k.value(n);
1544
1650
  for (const n of this.spec.carried ?? [])
1545
1651
  out[n] = new MapRef(n, 0);
1652
+ for (const [n, k, fs, ls] of this.spec.lists2 ?? []) {
1653
+ const l = new CarriedList(n, k, v(`${n}_len`), fs, part => new MapRef(part, 0), ls);
1654
+ for (const part of l.maps)
1655
+ delete out[part];
1656
+ out[n] = l;
1657
+ }
1546
1658
  }
1547
1659
  if (parts.state !== false) {
1548
1660
  for (const [n] of this.spec.state)
@@ -1653,7 +1765,7 @@ export class Rule {
1653
1765
  state[n] = hoist.term(termOf(u));
1654
1766
  }
1655
1767
  const pre = [...this.kindAssumptions('params'), ...s.pre.map(f => f(pEnv).c)];
1656
- const assume = [...this.kindAssumptions('inputs'), ...s.assume.map(f => hoist.cond(f(env).c))];
1768
+ const assume = [...this.kindAssumptions('inputs'), ...(s.lists2 ?? []).flatMap(([n]) => env[n].equations().map(c => hoist.cond(c.c))), ...s.assume.map(f => hoist.cond(f(env).c))];
1657
1769
  placed();
1658
1770
  // Functions, and their facts: each function's own (named after it), then the rule's.
1659
1771
  // The functions a function reads come first, each named <function>.<use>.
@@ -1865,9 +1977,16 @@ export class Rule {
1865
1977
  // Runtime values (numbers, booleans, choice names, null for a missing value) to the variables the machine reads.
1866
1978
  encodeParams(values) { return encode(this.spec.params, values); }
1867
1979
  // A carried map is a Set of keys (each cell 1), an array of them, or a Map of key to natural.
1980
+ // A carried list is an array of its items' runtime values (its length and folds computed from them).
1981
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1868
1982
  encodeInput(values) {
1869
- const out = encode(this.spec.inputs, values);
1983
+ const lists = Object.fromEntries((this.spec.lists2 ?? []).map(([n, k, fs]) => [n, new CarriedList(n, k, v(`${n}_len`), fs, part => new MapRef(part, 0)).encode(values[n])]));
1984
+ const out = encode(this.spec.inputs, { ...values, ...Object.fromEntries(Object.entries(lists).map(([n, l]) => [`${n}_len`, l.len])) });
1985
+ for (const l of Object.values(lists))
1986
+ Object.assign(out, l.maps);
1870
1987
  for (const n of this.spec.carried ?? []) {
1988
+ if (n in out)
1989
+ continue;
1871
1990
  const given = values[n];
1872
1991
  out[n] = given instanceof Map ? new Map(given) : new Map([...given].map(key => [key, 1]));
1873
1992
  }
@@ -1879,6 +1998,11 @@ export class Rule {
1879
1998
  const out = Object.assign({}, ...this.spec.inputs.map(([n, k]) => k.random(n, rand, bound)));
1880
1999
  for (const n of this.spec.carried ?? [])
1881
2000
  out[n] = new Map(Array.from({ length: bound + 1 }, (_, key) => [key, rand(2)]).filter(([, cell]) => cell === 1));
2001
+ // A carried list: items drawn by their kind, its length and folds as the encoding computes them.
2002
+ for (const [n, k, fs] of this.spec.lists2 ?? []) {
2003
+ const l = new CarriedList(n, k, v(`${n}_len`), fs, part => new MapRef(part, 0)).encode(Array.from({ length: rand(bound + 1) }, () => k.sample(rand, bound)));
2004
+ Object.assign(out, l.maps, { [`${n}_len`]: l.len });
2005
+ }
1882
2006
  return out;
1883
2007
  }
1884
2008
  // The state's scalars as runtime values: each kinded variable as its kind decodes it (an integer as one signed
@@ -2105,3 +2229,112 @@ export function crossCheckDecision(d, opts = {}) {
2105
2229
  return { checked };
2106
2230
  }
2107
2231
  export const proveLoop = (l, laws) => goalsVerdict(loopObligations(l.loop, laws, l.rankTerms));
2232
+ export class FoldRule {
2233
+ spec;
2234
+ constructor(spec) {
2235
+ this.spec = spec;
2236
+ }
2237
+ with(more) { return new FoldRule({ ...this.spec, ...more }); }
2238
+ get name() { return this.spec.name; }
2239
+ params(kinds) { return this.with({ params: [...this.spec.params, ...Object.entries(kinds)] }); }
2240
+ inputs(kinds) { return this.with({ inputs: [...this.spec.inputs, ...Object.entries(kinds)] }); }
2241
+ pre(f) { return this.with({ pre: [...this.spec.pre, f] }); }
2242
+ // What every leaf's and node's inputs satisfy (with their kinds' own assumptions).
2243
+ assume(f) { return this.with({ assume: [...this.spec.assume, f] }); }
2244
+ leaf(f) { return this.with({ leaf: f }); }
2245
+ node(f) { return this.with({ node: f }); }
2246
+ env(inputs = true) {
2247
+ const out = {};
2248
+ for (const [n, k] of this.spec.params)
2249
+ out[n] = k.value(n);
2250
+ if (inputs)
2251
+ for (const [n, k] of this.spec.inputs)
2252
+ out[n] = k.value(n);
2253
+ return out;
2254
+ }
2255
+ names() {
2256
+ if (!this.spec.leaf)
2257
+ throw new Error(`${this.spec.name}: a fold needs a leaf`);
2258
+ return Object.keys(this.spec.leaf(this.env()));
2259
+ }
2260
+ side(prefix) { return Object.fromEntries(this.names().map(n => [n, v(`${prefix}${n}`)])); }
2261
+ // A law about the values (and the params): it reads no input.
2262
+ law(name, f) { return { name, holds: f({ ...this.env(false), ...this.side('') }).c }; }
2263
+ get fold() {
2264
+ const s = this.spec;
2265
+ if (!s.leaf || !s.node)
2266
+ throw new Error(`${s.name}: a fold needs a leaf and a node`);
2267
+ const env = this.env(), pEnv = this.env(false);
2268
+ const pre = [...s.params.flatMap(([n, k]) => k.assume(n)).map(c => c.c), ...s.pre.map(f => f(pEnv).c)];
2269
+ const assume = [...s.inputs.flatMap(([n, k]) => k.assume(n)).map(c => c.c), ...s.assume.map(f => f(env).c)];
2270
+ const terms = (r) => Object.fromEntries(Object.entries(r).map(([n, t]) => [n, termOf(t)]));
2271
+ const leaf = terms(s.leaf(env)), node = terms(s.node(env, this.side('l_'), this.side('r_')));
2272
+ return { name: s.name, params: s.params.flatMap(([n, k]) => k.vars(n)), inputs: s.inputs.flatMap(([n, k]) => k.vars(n)), leaf, node,
2273
+ ...(pre.length ? { pre: pre.length === 1 ? pre[0] : { op: 'and', xs: pre } } : {}), ...(assume.length ? { assume: assume.length === 1 ? assume[0] : { op: 'and', xs: assume } } : {}) };
2274
+ }
2275
+ // The values of a tree, by the terms that were proven (each leaf's and node's inputs as their kinds encode them; one
2276
+ // that doesn't meet `assume` is refused, as the laws are claimed only of trees that meet it).
2277
+ run(tree, params = {}) {
2278
+ const f = this.fold, ps = encode(this.spec.params, params), none = { name: f.name, params: [], inputs: [], state: {}, step: {} };
2279
+ const st = { scalars: {}, maps: {} };
2280
+ if (f.pre && !evalC(f.pre, ps, st, none))
2281
+ throw new RangeError(`${f.name}: the params don't meet its precondition`);
2282
+ const go = (t) => {
2283
+ const own = 'leaf' in t ? t.leaf : t.node, env = { ...ps, ...encode(this.spec.inputs, own) };
2284
+ if (f.assume && !evalC(f.assume, env, st, none))
2285
+ throw new RangeError(`${f.name}: ${JSON.stringify(own)} doesn't meet its assumption`);
2286
+ if ('leaf' in t)
2287
+ return Object.fromEntries(Object.entries(f.leaf).map(([n, e]) => [n, evalE(e, env, st, none)]));
2288
+ const l = go(t.left), r = go(t.right);
2289
+ const both = { ...env, ...Object.fromEntries(Object.entries(l).map(([n, x]) => [`l_${n}`, x])), ...Object.fromEntries(Object.entries(r).map(([n, x]) => [`r_${n}`, x])) };
2290
+ return Object.fromEntries(Object.entries(f.node).map(([n, e]) => [n, evalE(e, both, st, none)]));
2291
+ };
2292
+ return go(tree);
2293
+ }
2294
+ // The builders against what they built, on random trees: each value by the callbacks' own arithmetic against the
2295
+ // terms the kernel proved (as run evaluates them), and each law's callback against its condition.
2296
+ crossCheck(laws = [], opts = {}) {
2297
+ const s = this.spec, rand = opts.rand ?? seeded(13), bound = opts.bound ?? 6, f = this.fold;
2298
+ let checked = 0;
2299
+ const sample = (kinds) => Object.fromEntries(kinds.map(([n, k]) => [n, k.sample(rand, bound)]));
2300
+ const tree = (d) => (d === 0 || rand(3) === 0 ? { leaf: sample(s.inputs) } : { node: sample(s.inputs), left: tree(d - 1), right: tree(d - 1) });
2301
+ const known = (vals) => Object.fromEntries(Object.entries(vals).map(([n, x]) => [n, new Term({ op: 'const', value: x }, x)]));
2302
+ for (let t = 0; t < (opts.trials ?? 200); t++) {
2303
+ const params = sample(s.params), ps = encode(s.params, params);
2304
+ const pv = known(ps), pEnv = Object.fromEntries(s.params.map(([n, k]) => [n, k.build(n, part => pv[part])]));
2305
+ const builder = (tr) => {
2306
+ const own = 'leaf' in tr ? tr.leaf : tr.node, iv = known(encode(s.inputs, own)), env = { ...pEnv, ...Object.fromEntries(s.inputs.map(([n, k]) => [n, k.build(n, part => iv[part])])) };
2307
+ const out = 'leaf' in tr ? s.leaf(env) : (() => { const l = builder(tr.left), r = builder(tr.right); return l && r ? s.node(env, known(l), known(r)) : undefined; })();
2308
+ if (!out)
2309
+ return undefined;
2310
+ const vals = Object.entries(out).map(([n, x]) => [n, valOf(x)]);
2311
+ return vals.every(([, x]) => x !== undefined) ? Object.fromEntries(vals) : undefined;
2312
+ };
2313
+ const tr = tree(opts.depth ?? 3);
2314
+ let ours;
2315
+ try {
2316
+ ours = this.run(tr, params);
2317
+ }
2318
+ catch {
2319
+ continue;
2320
+ }
2321
+ const theirs = builder(tr);
2322
+ if (!theirs)
2323
+ continue;
2324
+ for (const [n, x] of Object.entries(theirs)) {
2325
+ checked++;
2326
+ if (ours[n] !== x)
2327
+ return { checked, failure: `${f.name}: ${n} is ${x} by its callbacks, ${ours[n]} by the terms proven, for ${JSON.stringify(tr)}` };
2328
+ }
2329
+ for (const law of laws) {
2330
+ const ok = evalC(law.holds, { ...ps, ...ours }, { scalars: {}, maps: {} }, { name: f.name, params: [], inputs: [], state: {}, step: {} });
2331
+ checked++;
2332
+ if (!ok)
2333
+ return { checked, failure: `${f.name}: law ${law.name} fails for ${JSON.stringify(tr)} with params ${JSON.stringify(params)}` };
2334
+ }
2335
+ }
2336
+ return { checked };
2337
+ }
2338
+ }
2339
+ export const fold = (name) => new FoldRule({ name, params: [], inputs: [], pre: [], assume: [] });
2340
+ export const proveFold = (f, laws) => goalsVerdict(foldObligations(f.fold, laws));
@@ -135,6 +135,10 @@ export type LeanItem = {
135
135
  loop: Loop;
136
136
  rank: E | E[];
137
137
  laws?: Law[];
138
+ } | {
139
+ id: string;
140
+ fold: Fold;
141
+ laws: Law[];
138
142
  } | {
139
143
  id: string;
140
144
  goal: Goal;
@@ -1055,6 +1055,10 @@ export function toLean(items, opts = {}) {
1055
1055
  parts.push(`-- ${item.id}\n${loopToLean(id, item.loop, item.laws ?? [], item.rank)}`);
1056
1056
  theorems.push(`${id}_halts`);
1057
1057
  }
1058
+ else if ('fold' in item) {
1059
+ parts.push(`-- ${item.id}\n${foldToLean(id, item.fold, item.laws)}`);
1060
+ theorems.push(`${id}_laws`);
1061
+ }
1058
1062
  else {
1059
1063
  // goalToLean names its theorem t<i>; here it is the item's id.
1060
1064
  const n = goals++;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdp-wired",
3
- "version": "0.9.0",
3
+ "version": "0.10.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 && 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/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 && 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/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",