gdp-wired 0.6.0 → 0.8.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.
@@ -0,0 +1,31 @@
1
+ import type { Named, NamesOf, Proof, Prover } from '@gdp-ts/core';
2
+ import { type Decision, type Fun, type Rule, type StepLaw, Unproven } from './dsl.js';
3
+ import { type Law, type State } from './machine.js';
4
+ type AnyNamed = Named<any, any>;
5
+ export type Mint<K extends string, A> = <S extends AnyNamed[]>(args: A, ...about: S) => Proof<K, NamesOf<S>> | null;
6
+ export { Unproven };
7
+ type AnyFun = Fun<any, any, any>;
8
+ type Conditions<O> = {
9
+ [k in keyof O]: O[k] extends boolean ? k : never;
10
+ }[keyof O] & string;
11
+ type ArgsOf<F extends AnyFun> = Parameters<F['run']>[0];
12
+ type ParamsOf<F extends AnyFun> = NonNullable<Parameters<F['run']>[1]>;
13
+ export declare function guard<K extends string, F extends AnyFun>(prover: Prover<K>, fn: F, field: Conditions<ReturnType<F['run']>>, opts: {
14
+ laws: Decision[];
15
+ params?: ParamsOf<F>;
16
+ }): Mint<K, ArgsOf<F>>;
17
+ type AnyRule = Rule<any, any, any, any, any, any, any, any>;
18
+ type FlagOf<R> = R extends Rule<any, any, any, any, infer F, any, any, any> ? keyof F & string : never;
19
+ type InputOf<R extends AnyRule> = Parameters<R['encodeInput']>[0];
20
+ type RuleParams<R extends AnyRule> = Parameters<R['encodeParams']>[0];
21
+ export type Gate<K extends string, I> = {
22
+ request: Mint<K, I>;
23
+ readonly state: State;
24
+ values(): Record<string, unknown>;
25
+ };
26
+ export declare function gate<K extends string, R extends AnyRule>(prover: Prover<K>, r: R, flag: FlagOf<R>, opts: {
27
+ laws: Law[];
28
+ stepLaws?: StepLaw[];
29
+ params?: RuleParams<R>;
30
+ state?: State;
31
+ }): Gate<K, InputOf<R>>;
@@ -0,0 +1,58 @@
1
+ import { Unproven, funOf, proveDecision, proveSteps } from "./dsl.js";
2
+ import { evalC, interpret } from "./machine.js";
3
+ import { allowed, requestEnv } from "./watch.js";
4
+ export { Unproven };
5
+ const proven = (what, v) => { if (!(v.proven && v.checked))
6
+ throw new Unproven(what, v); };
7
+ // A proof wherever `fn`'s result `field` holds. Every law in `laws` must have been stated about `fn` (fn.law or
8
+ // fn.relate) and is proven now; the arguments it is later called with must meet the function's kinds and `assume`.
9
+ // const access = fun('document access').inputs({...}).returns(env => ({permit: permit(env)}))
10
+ // const mint = guard(CanRead, access, 'permit', {laws: [access.law('...', ...)]})
11
+ export function guard(prover, fn, field, opts) {
12
+ if (opts.laws.length === 0)
13
+ throw new Error(`${fn.name}: a guard needs a law to prove`);
14
+ for (const d of opts.laws) {
15
+ if (funOf(d) !== fn)
16
+ throw new Error(`${fn.name}: "${d.name}" was not stated about this function (state it with ${fn.name}.law)`);
17
+ proven(`${fn.name}: "${d.name}"`, proveDecision(d));
18
+ }
19
+ const r = fn.rule, m = r.machine, params = (opts.params ?? {}), ps = r.encodeParams(params);
20
+ if (m.pre && !evalC(m.pre, ps, { scalars: {}, maps: {} }, m))
21
+ throw new Error(`${fn.name}: the params ${JSON.stringify(params)} don't meet its precondition`);
22
+ return (args, ...about) => {
23
+ const input = r.encodeInput(args);
24
+ if (m.assume && !evalC(m.assume, { ...ps, ...input }, { scalars: {}, maps: {} }, m))
25
+ return null;
26
+ return fn.run(args, params)[field] === true ? prover.prove(...about) : null;
27
+ };
28
+ }
29
+ // A proof for each request whose step takes `flag`, the rule's laws (and step laws) proven now. One gate holds one
30
+ // run's state: keep one per state it guards (per process, or loaded and saved around each request in a transaction).
31
+ // const tokens = rule('one-time tokens').inputs({...}).maps({...}).flags(env => ({redeem: ...})).step(...)
32
+ // const redeem = gate(Redeemed, tokens, 'redeem', {laws: tokenLaws})
33
+ // redeem.request({op: 'redeem', token: 7}, user, token) // a Redeemed proof the first time, null after
34
+ export function gate(prover, r, flag, opts) {
35
+ const m = r.machine;
36
+ if (!(flag in (m.step.flags ?? {})))
37
+ throw new Error(`${m.name}: no flag ${flag}`);
38
+ if (opts.laws.length + (opts.stepLaws?.length ?? 0) === 0)
39
+ throw new Error(`${m.name}: a gate needs a law to prove`);
40
+ proven(m.name, proveSteps(r, opts.laws, opts.stepLaws ?? []));
41
+ const params = (opts.params ?? {}), ps = r.encodeParams(params);
42
+ if (m.pre && !evalC(m.pre, ps, { scalars: {}, maps: {} }, m))
43
+ throw new Error(`${m.name}: the params ${JSON.stringify(params)} don't meet its precondition`);
44
+ const run = interpret(m), compiled = r.compiled();
45
+ let st = opts.state ?? compiled.init(ps);
46
+ return {
47
+ request: (args, ...about) => {
48
+ const input = r.encodeInput(args);
49
+ if (!allowed(m, run, ps, st, input))
50
+ return null;
51
+ const granted = requestEnv(m, run, ps, st, input)[`?${flag}`] === 1;
52
+ st = compiled.step(ps, st, input);
53
+ return granted ? prover.prove(...about) : null;
54
+ },
55
+ get state() { return st; },
56
+ values: () => r.decode(st),
57
+ };
58
+ }
@@ -70,6 +70,24 @@ export type Machine = {
70
70
  export type Fn = {
71
71
  js: string;
72
72
  lean?: string;
73
+ recur?: Recurrence;
74
+ };
75
+ export type Recurrence = {
76
+ args: string[];
77
+ zero: E;
78
+ next: E;
79
+ lemmas: {
80
+ name: string;
81
+ holds: C;
82
+ }[];
83
+ between: {
84
+ name: string;
85
+ holds: C;
86
+ }[];
87
+ uses: Record<string, {
88
+ arity: number;
89
+ lean?: string;
90
+ }>;
73
91
  };
74
92
  export type History = {
75
93
  keep: C;
@@ -448,13 +448,50 @@ function fnKeys(m, goals) {
448
448
  // value that makes one of its reads one of these (by matching: a function of several arguments reads a paired key), and
449
449
  // at the keys themselves.
450
450
  const withFacts = (m, inst, keys) => ({ ...inst, ...Object.fromEntries((m.facts ?? []).filter(f => f.forall).map(f => {
451
- if (f.keys?.length)
452
- return [f.name, tuples(keys, f.keys.length + 1)];
451
+ if (f.keys?.length) {
452
+ // A fact about several keys reads a function of several arguments (at a paired key): each read matched against the
453
+ // keys the goals read binds some of the fact's keys; one match, or two that agree, binding every key is an instance.
454
+ const vars = [f.forall, ...f.keys], reads = fnKeys(m, [{ name: '', hyps: [f.holds] }]);
455
+ if (!reads.length)
456
+ return [f.name, tuples(keys, vars.length)];
457
+ const partial = reads.flatMap(pattern => keys.flatMap(key => { const b = matchMany(pattern, key, vars); return b ? [b] : []; }));
458
+ const whole = (b) => vars.every(x => x in b);
459
+ const agree = (a, b) => Object.entries(b).every(([x, e]) => !(x in a) || JSON.stringify(a[x]) === JSON.stringify(e));
460
+ const found = [...partial.filter(whole), ...partial.flatMap((a, i) => partial.slice(i + 1).filter(b => agree(a, b)).map(b => ({ ...a, ...b })).filter(whole))];
461
+ const had = inst[f.name] ?? [], out = [...had], seen = new Set();
462
+ for (let t = 0; t < had.length / vars.length; t++)
463
+ seen.add(JSON.stringify(had.slice(t * vars.length, (t + 1) * vars.length)));
464
+ for (const b of found) {
465
+ const tuple = vars.map(x => b[x]), id = JSON.stringify(tuple);
466
+ if (!seen.has(id)) {
467
+ seen.add(id);
468
+ out.push(...tuple);
469
+ }
470
+ }
471
+ return [f.name, out];
472
+ }
453
473
  const had = inst[f.name] ?? [], seen = new Set(had.map(e => JSON.stringify(e)));
454
474
  const reads = fnKeys(m, [{ name: '', hyps: [f.holds] }]);
455
475
  const at = [...keys, ...reads.flatMap(pattern => keys.flatMap(key => { const b = match(pattern, key, f.forall); return b ? [b] : []; }))];
456
476
  return [f.name, [...had, ...at.filter(e => { const id = JSON.stringify(e); return seen.has(id) ? false : (seen.add(id), true); })]];
457
477
  })) });
478
+ // The values of the names in xs that make pattern the term, for those it reads, if some do (structurally).
479
+ function matchMany(pattern, term, xs) {
480
+ const bound = {};
481
+ const go = (p, t) => {
482
+ if (p.op === 'var' && xs.includes(p.name)) {
483
+ if (p.name in bound)
484
+ return JSON.stringify(bound[p.name]) === JSON.stringify(t);
485
+ bound[p.name] = t;
486
+ return true;
487
+ }
488
+ if (p.op !== t.op)
489
+ return false;
490
+ const pe = Object.entries(p), te = t;
491
+ return pe.every(([k, val]) => k === 'op' || (typeof val === 'object' && val !== null ? (Array.isArray(val) ? Array.isArray(te[k]) && te[k].length === val.length && val.every((y, i) => go(y, te[k][i])) : go(val, te[k])) : val === te[k]));
492
+ };
493
+ return go(pattern, term) ? bound : undefined;
494
+ }
458
495
  // The value of x that makes pattern the term, if one does (structurally).
459
496
  function match(pattern, term, x) {
460
497
  let bound;
@@ -1,4 +1,4 @@
1
- import type { C, E, Law, Machine } from '../core/machine.js';
1
+ import type { C, E, Law, Machine, Recurrence } from '../core/machine.js';
2
2
  import { type Step } from '../core/solver.js';
3
3
  import { type Fold, type Goal, type Instances, type Liveness, type Loop, type Path, type StepLaw, type Tree } from '../kernel/kernel.js';
4
4
  import { leanLibrary } from './library.js';
@@ -93,6 +93,13 @@ export declare function smachineToLean(id: string, given: Machine, laws: Law[],
93
93
  };
94
94
  };
95
95
  }): string;
96
+ type Uses = Record<string, {
97
+ lean?: string;
98
+ recur?: Recurrence;
99
+ arity: number;
100
+ }>;
101
+ export declare function recurToLean(lean: string, r: Recurrence, uses?: Uses): string;
102
+ export declare const recurPairing = "theorem Recur.pair_inj {a b c d : Nat} (h : (a + b) * (a + b) + b = (c + d) * (c + d) + d) : a = c \u2227 b = d := by\n have sq : \u2200 s t : Nat, s < t \u2192 s * s + s < t * t := by\n intro s t hst\n have h1 : (s + 1) * (s + 1) \u2264 t * t := Nat.mul_le_mul hst hst\n have h2 : (s + 1) * (s + 1) = s * s + s + s + 1 := by simp only [Nat.add_mul, Nat.mul_add, Nat.mul_one, Nat.one_mul]; omega\n omega\n have hb : b \u2264 a + b := Nat.le_add_left b a\n have hd : d \u2264 c + d := Nat.le_add_left d c\n rcases Nat.lt_trichotomy (a + b) (c + d) with hlt | heq | hgt\n \u00B7 have := sq _ _ hlt; omega\n \u00B7 rw [heq] at h; omega\n \u00B7 have := sq _ _ hgt; omega\nnoncomputable def Recur.unpair (k : Nat) : Nat \u00D7 Nat :=\n Classical.epsilon fun p : Nat \u00D7 Nat => (p.1 + p.2) * (p.1 + p.2) + p.2 = k\ntheorem Recur.unpair_pair (a b : Nat) : Recur.unpair ((a + b) * (a + b) + b) = (a, b) := by\n have h := Classical.epsilon_spec (p := fun p : Nat \u00D7 Nat => (p.1 + p.2) * (p.1 + p.2) + p.2 = (a + b) * (a + b) + b) \u27E8(a, b), rfl\u27E9\n obtain \u27E8h1, h2\u27E9 := Recur.pair_inj h\n exact Prod.ext h1 h2";
96
103
  export declare function hmachineToLean(id: string, m: Machine, laws: Law[], tampers?: {
97
104
  what: string;
98
105
  relations: {
@@ -3,7 +3,7 @@ import { add, k, scale, sub } from "../core/solver.js";
3
3
  import { canon, columnTerm, stateName, termColumn, historyMachine, foldObligations, leafConstraints, loopLiveness, loopMachine, linear, livenessHelpful, livenessParts, livenessRanks, loopObligations, trueC, obligations, progressObligations, stepLawObligations, substC, withProducts } from "../kernel/kernel.js";
4
4
  import { grow, proveBuilt, proveLaws, proveStepLaws } from "../kernel/prove.js";
5
5
  import { refinement, refinementSides } from "../core/compose.js";
6
- import { leanTerms } from "./lean-terms.js";
6
+ import { leanSurface, leanTerms } from "./lean-terms.js";
7
7
  import { leanLibrary } from "./library.js";
8
8
  export { leanLibrary };
9
9
  // --- to Lean ------------------------------------------------------------------------------------------------------------
@@ -474,8 +474,8 @@ const boundKeys = (m) => [...Object.values(m.step.flags ?? {}).flatMap(c => (c.o
474
474
  // `historyTampers`: the history with other relations, each refused.
475
475
  export function smachineToLean(id, given, laws, opts = {}) {
476
476
  const hm = given.history ? { ...given, history: { ...given.history, prune: undefined } } : undefined;
477
- const hplain = hm ? historyMachine(hm) : undefined, m = hplain ? hplain.machine : given;
478
- const stepLaws = opts.stepLaws ?? [], rf = opts.refined, gv = opts.ghosts ?? [];
477
+ const hplain = hm ? historyMachine(hm) : undefined, stepLaws = opts.stepLaws ?? [], rf = opts.refined, gv = opts.ghosts ?? [];
478
+ const { machine: m, origins } = specializeFacts(hplain ? hplain.machine : given, laws, stepLaws);
479
479
  const claims = [];
480
480
  const claimSteps = () => claims.flatMap(cl => cl.steps);
481
481
  // The machine's own step laws, one step or k (Sets.lean's skstep_sound).
@@ -741,24 +741,203 @@ def ${id}_proof : List SEntry := ${list(laws.map((_, j) => `${id}_e${j}`))}
741
741
  theorem ${id}_check : checkSMachine ${id}_machine ${id}_proof = true := by decide +kernel
742
742
  theorem ${id}_laws : ∀ ρ M, SReach ${id}_machine ρ M → ∀ e ∈ ${id}_proof, e.Holds ρ M :=
743
743
  smachine_sound ${id}_machine ${id}_proof ${id}_check
744
- ${hm ? histBlock() : readable(id, laws, vars, mapNames, { binders: '(ρ : Nat → Nat) (μ : Nat → Nat → Nat)', hyp: `SReach ${id}_machine ρ μ`, call: `${id}_laws ρ μ h` })}${factsProof(id, m, mapNames, opts.simp)}${opts.tamper ? `
744
+ ${hm ? histBlock() : readable(id, laws, vars, mapNames, { binders: '(ρ : Nat → Nat) (μ : Nat → Nat → Nat)', hyp: `SReach ${id}_machine ρ μ`, call: `${id}_laws ρ μ h` })}${factsProof(id, m, mapNames, vars, opts.simp, origins)}${opts.tamper ? `
745
745
  -- TAMPERED: ${opts.tamper.what}. Lean computes the check to false.
746
746
  def ${id}_tampered : SMachine := ${lit(opts.tamper.machine)}
747
747
  example : checkSMachine ${id}_tampered ${id}_proof = false := by decide +kernel` : ''}${records.length ? `\n\n${records.join('\n\n')}` : ''}${opts.after ? `\n\n${opts.after(ix)}` : ''}`;
748
748
  }
749
+ function specializeFacts(m, laws, stepLaws) {
750
+ const origins = new Map(), facts = m.facts ?? [];
751
+ if (!facts.some(f => f.keys?.length))
752
+ return { machine: m, origins };
753
+ const r = proveStepLaws(m, laws, stepLaws);
754
+ if (!r.proven || !r.proof)
755
+ throw new Error(`${m.name}: not proven (${r.failed?.stuck ?? ''})`);
756
+ const all = [...Object.values(r.proof.laws.instances), r.proof.instances, ...Object.values(r.proof.perStep ?? {})];
757
+ const overParams = (t) => varsOf({ op: '=', a: t, b: t }).every(x => m.params.includes(x)) && !JSON.stringify(t).includes('"op":"get"');
758
+ const out = [];
759
+ for (const f of facts) {
760
+ if (!f.keys?.length) {
761
+ out.push(f);
762
+ continue;
763
+ }
764
+ const vars = [f.forall, ...f.keys], seen = new Set();
765
+ for (const inst of all) {
766
+ const keys = inst[f.name] ?? [];
767
+ for (let t = 0; t + vars.length <= keys.length; t += vars.length) {
768
+ const tuple = keys.slice(t, t + vars.length), open = tuple.map((e2, i) => [e2, i]).filter(([e2]) => !overParams(e2)).map(([, i]) => i);
769
+ // A tuple with two keys over the state is left out (the prover offers more instances than a proof uses): if the
770
+ // proof needs it, proving the machine again without it fails, and says so.
771
+ if (open.length > 1)
772
+ continue;
773
+ const left = open[0] ?? 0, at = Object.fromEntries(vars.flatMap((x, i) => (i === left ? [] : [[x, tuple[i]]])));
774
+ const id = JSON.stringify([left, at]);
775
+ if (seen.has(id))
776
+ continue;
777
+ seen.add(id);
778
+ const name = `${f.name} #${seen.size}`;
779
+ out.push({ name, forall: vars[left], holds: substC(f.holds, { vars: at, maps: {}, flags: {} }) });
780
+ origins.set(name, { from: f, at });
781
+ }
782
+ }
783
+ }
784
+ return { machine: { ...m, facts: out }, origins };
785
+ }
786
+ // A function defined by a recurrence (core/dsl.ts recur): its Lean definition by the same recurrence, and its lemmas
787
+ // proven together by induction (each case by omega, its min, max and ifs split first), and its laws between two points
788
+ // by induction on the distance. A function of several arguments is `<name>_rec`, its further arguments first, and the
789
+ // function the machine reads is its key's (`<name>`, through Recur.unpair: the arguments paired as the builders pair
790
+ // them, (a + b)² + b, which Recur.pair_inj proves injective). The kernel proved the lemmas by the same inductions; Lean
791
+ // proves them again of the definition.
792
+ const recurArg = (a) => `«${a}»`;
793
+ // The arguments a key was paired from (the builders' argsKey: left-nested (k + a)² + a), if it was.
794
+ const unpairKey = (key, n) => {
795
+ if (n === 1)
796
+ return [key];
797
+ if (key.op !== 'add' || key.a.op !== 'prod' || key.a.a.op !== 'add')
798
+ return undefined;
799
+ const s = key.a.a, b = key.b;
800
+ if (JSON.stringify(key.a.b) !== JSON.stringify(s) || JSON.stringify(s.b) !== JSON.stringify(b))
801
+ return undefined;
802
+ const rest = unpairKey(s.a, n - 1);
803
+ return rest ? [...rest, b] : undefined;
804
+ };
805
+ // f at x (and the further arguments), as Lean reads it.
806
+ const recurCall = (lean, r, x, args = r.args.map(recurArg)) => (r.args.length ? `(${lean}_rec ${[...args, x].join(' ')})` : `(${lean} ${x})`);
807
+ // A read of a function the recurrence uses: a recurrence by its definition at the unpaired arguments, any other by its
808
+ // Lean term at the key.
809
+ const useRead = (uses, where) => (map, key, e) => {
810
+ const u = uses[map];
811
+ if (!u?.lean)
812
+ throw new Error(`${where}: ${map} has no Lean definition`);
813
+ if (u.recur) {
814
+ const args = unpairKey(key, u.arity);
815
+ if (!args)
816
+ throw new Error(`${where}: ${map} read at a key not paired by its arguments`);
817
+ return recurCall(u.lean, u.recur, e(args[0]), args.slice(1).map(e));
818
+ }
819
+ return `((${u.lean}) ${e(key)})`;
820
+ };
821
+ // The lemmas of the recurrences a term reads, at the keys it reads them, with the names as given.
822
+ const useLemmas = (uses, t, names, where) => {
823
+ const out = [];
824
+ const walk = (x) => {
825
+ if (!x || typeof x !== 'object')
826
+ return;
827
+ const o = x;
828
+ if (o.op === 'get' && o.map) {
829
+ const u = uses[o.map];
830
+ if (u?.recur?.lemmas.length) {
831
+ const args = unpairKey(o.key, u.arity), s = leanSurface(names, where, useRead(uses, where)).e;
832
+ out.push(`have := ${u.lean}_lemmas ${[...args.slice(1).map(s), s(args[0])].join(' ')}`);
833
+ }
834
+ }
835
+ for (const v of Object.values(o))
836
+ walk(v);
837
+ };
838
+ walk(t);
839
+ return [...new Set(out)];
840
+ };
841
+ export function recurToLean(lean, r, uses = {}) {
842
+ const binders = r.args.map(a => `(${recurArg(a)} : Nat)`).join(' '), argNames = Object.fromEntries(r.args.map(a => [a, recurArg(a)]));
843
+ const self = r.args.length ? `${lean}_rec` : lean;
844
+ const read = useRead(uses, lean);
845
+ const surface = (names) => leanSurface({ ...argNames, ...names }, lean, read);
846
+ const text = JSON.stringify(r), unfold = [self, ...(text.includes('"op":"min"') ? ['Nat.min_def'] : []), ...(text.includes('"op":"max"') ? ['Nat.max_def'] : [])].join(', ');
847
+ const lemmaAt = (x) => r.lemmas.map(l => `(${surface({ x, fx: recurCall(lean, r, x) }).c(l.holds)})`).join(' ∧ ');
848
+ const close = (haves) => `${haves.map(h => `${h}; `).join('')}(repeat' split) <;> omega`;
849
+ const step = (n) => useLemmas(uses, r.next, { ...argNames, x: n, prev: recurCall(lean, r, n) }, lean);
850
+ const lemmas = r.lemmas.length ? `
851
+ -- ${r.lemmas.map(l => l.name).join('; ')}
852
+ theorem ${lean}_lemmas ${binders} (x : Nat) : ${lemmaAt('x')} := by
853
+ induction x with
854
+ | zero => (try simp only [${unfold}]); ${close(useLemmas(uses, { op: 'and', xs: r.lemmas.map(l => l.holds) }, { ...argNames, x: '0', fx: recurCall(lean, r, '0') }, lean))}
855
+ | succ n ih => (try simp only [${unfold}] at ih ⊢); ${close([...step('n'), ...useLemmas(uses, { op: 'and', xs: r.lemmas.map(l => l.holds) }, { ...argNames, x: '(n + 1)', fx: recurCall(lean, r, '(n + 1)') }, lean)])}` : '';
856
+ const args = r.args.map(recurArg).join(' ');
857
+ const pairAt = (x, y) => r.between.map(l => `(${surface({ x, fx: recurCall(lean, r, x), y, fy: recurCall(lean, r, y) }).c(l.holds)})`).join(' ∧ ');
858
+ const between = r.between.length ? `
859
+ -- ${r.between.map(l => l.name).join('; ')}, for every x <= y
860
+ theorem ${lean}_between_by ${binders} (x d : Nat) : ${pairAt('x', '(x + d)')} := by
861
+ induction d with
862
+ | zero => simp only [Nat.add_zero]; (repeat' split) <;> omega
863
+ | succ n ih =>
864
+ rw [show x + (n + 1) = (x + n) + 1 from by omega]
865
+ ${r.lemmas.length ? `have := ${lean}_lemmas ${args} (x + n); ` : ''}(try simp only [${unfold}] at ih this ⊢); ${close(step('(x + n)'))}
866
+ theorem ${lean}_between ${binders} (x y : Nat) (h : x ≤ y) : ${pairAt('x', 'y')} := by
867
+ obtain ⟨d, rfl⟩ := Nat.exists_eq_add_of_le h
868
+ exact ${lean}_between_by ${args} x d` : '';
869
+ const zero = surface({}).e(r.zero), next = surface({ x: 'x', prev: recurCall(lean, r, 'x') }).e(r.next);
870
+ if (!r.args.length)
871
+ return `def ${lean} : Nat → Nat\n | 0 => ${zero}\n | x + 1 => ${next}${lemmas}${between}`;
872
+ // The key's function: the arguments unpaired, innermost first.
873
+ const u = (i) => (i === r.args.length ? '(Recur.unpair k)' : `(Recur.unpair ${u(i + 1)}.1)`);
874
+ const keyOf = (x) => r.args.map(recurArg).reduce((k, a) => `((${k} + ${a}) * (${k} + ${a}) + ${a})`, x);
875
+ return `def ${self} ${binders} : Nat → Nat
876
+ | 0 => ${zero}
877
+ | x + 1 => ${next}
878
+ noncomputable def ${lean} (k : Nat) : Nat := ${self} ${r.args.map((_, i) => `${u(i + 1)}.2`).join(' ')} ${u(1)}.1
879
+ theorem ${lean}_key ${binders} (x : Nat) : ${lean} ${keyOf('x')} = ${self} ${args} x := by
880
+ simp only [${lean}, Recur.unpair_pair]${lemmas}${between}`;
881
+ }
882
+ // The pairing the builders read a function of several arguments by, and Lean's inverse of it (choice over the pairs:
883
+ // Recur.pair_inj makes it the pair).
884
+ export const recurPairing = `theorem Recur.pair_inj {a b c d : Nat} (h : (a + b) * (a + b) + b = (c + d) * (c + d) + d) : a = c ∧ b = d := by
885
+ have sq : ∀ s t : Nat, s < t → s * s + s < t * t := by
886
+ intro s t hst
887
+ have h1 : (s + 1) * (s + 1) ≤ t * t := Nat.mul_le_mul hst hst
888
+ have h2 : (s + 1) * (s + 1) = s * s + s + s + 1 := by simp only [Nat.add_mul, Nat.mul_add, Nat.mul_one, Nat.one_mul]; omega
889
+ omega
890
+ have hb : b ≤ a + b := Nat.le_add_left b a
891
+ have hd : d ≤ c + d := Nat.le_add_left d c
892
+ rcases Nat.lt_trichotomy (a + b) (c + d) with hlt | heq | hgt
893
+ · have := sq _ _ hlt; omega
894
+ · rw [heq] at h; omega
895
+ · have := sq _ _ hgt; omega
896
+ noncomputable def Recur.unpair (k : Nat) : Nat × Nat :=
897
+ Classical.epsilon fun p : Nat × Nat => (p.1 + p.2) * (p.1 + p.2) + p.2 = k
898
+ theorem Recur.unpair_pair (a b : Nat) : Recur.unpair ((a + b) * (a + b) + b) = (a, b) := by
899
+ have h := Classical.epsilon_spec (p := fun p : Nat × Nat => (p.1 + p.2) * (p.1 + p.2) + p.2 = (a + b) * (a + b) + b) ⟨(a, b), rfl⟩
900
+ obtain ⟨h1, h2⟩ := Recur.pair_inj h
901
+ exact Prod.ext h1 h2`;
749
902
  // The facts, proven in Lean of the functions' Lean definitions, when every function has one: a run that starts with
750
- // those functions starts where SReach asks the facts to hold.
751
- function factsProof(id, m, maps, simp = []) {
903
+ // those functions starts where SReach asks the facts to hold. A recurrence's equations unfold its definition (a function
904
+ // of several arguments read at its key first); its lemmas and laws between two points are its theorems at the keys.
905
+ function factsProof(id, m, maps, vars, simp = [], origins = new Map()) {
752
906
  const fns = Object.entries(m.fns ?? {}), facts = m.facts ?? [];
753
907
  if (!facts.length || !fns.length || fns.some(([, f]) => !f.lean))
754
908
  return '';
909
+ const recurs = fns.filter(([, f]) => f.recur);
910
+ simp = [...simp, ...recurs.flatMap(([, f]) => (f.recur.args.length ? [`${f.lean}_rec`, `${f.lean}_key`] : [f.lean]))];
911
+ const at = leanSurface(Object.fromEntries(vars.map((x, j) => [x, `(ρ ${j})`])), `${id}'s facts`);
912
+ // Each recurrence's key lemma rewrites the function at its key before anything else simplifies the key.
913
+ const keyed = recurs.filter(([, f]) => f.recur.args.length).map(([, f]) => `${f.lean}_key`);
914
+ const tactic = (fact) => {
915
+ // The theorem a fact is an instance of, cited first so that both it and the goal are simplified alike.
916
+ const cite = () => {
917
+ const o = origins.get(fact.name), general = o?.from ?? fact;
918
+ const owner = recurs.find(([n]) => general.name.startsWith(`${n}: `)), what = owner ? general.name.slice(owner[0].length + 2) : '';
919
+ if (!owner || !general.forall)
920
+ return undefined;
921
+ const [, f] = owner, r = f.recur, keys = [general.forall, ...(general.keys ?? [])];
922
+ // Each key: the one left (v), or its term, read at the columns.
923
+ const arg = keys.map(x => (x === fact.forall ? 'v' : `(${at.e(o?.at[x] ?? { op: 'var', name: x })})`));
924
+ if (r.lemmas.some(l => l.name === what))
925
+ return `${f.lean}_lemmas ${[...arg.slice(1), arg[0]].join(' ')}`;
926
+ if (r.between.some(l => l.name === what))
927
+ return `${f.lean}_between ${[...arg.slice(2), arg[0], arg[1]].join(' ')}`;
928
+ return undefined;
929
+ };
930
+ const c = cite(), hs = [...fns.map((_, j) => `h${j}`)];
931
+ return `simp only [MLaw.Holds]; (try intro v); ${c ? `have hc := ${c}; ` : ''}simp only [evalC, evalE, upd, ${hs.join(', ')}]${keyed.length ? `; (try simp only [${keyed.join(', ')}])` : ''}; (try simp [${[...hs, ...simp].join(', ')}]${c ? ' at hc ⊢' : ''}); all_goals omega`;
932
+ };
755
933
  return `
756
934
  -- The facts hold of the functions' Lean definitions: ${fns.map(([n, f]) => `${n} := ${f.lean}`).join('; ')}.
757
935
  theorem ${id}_facts_hold (ρ : Nat → Nat) (μ : Nat → Nat → Nat) ${fns.map(([n, f], j) => `(h${j} : μ ${maps.indexOf(n)} = ${f.lean})`).join(' ')} :
758
936
  ∀ l ∈ ${id}_machine.facts, l.Holds ρ μ := by
759
937
  intro l hl
760
938
  simp only [${id}_machine, List.mem_cons, List.not_mem_nil, or_false] at hl
761
- rcases hl with ${facts.map(() => 'rfl').join(' | ')} <;> (simp only [MLaw.Holds]; try intro v); all_goals (simp [evalC, evalE, upd, ${[...fns.map((_, j) => `h${j}`), ...simp].join(', ')}]; try omega)`;
939
+ rcases hl with ${facts.map(() => 'rfl').join(' | ')}
940
+ ${facts.map(f => ` · ${tactic(f)}`).join('\n')}`;
762
941
  }
763
942
  // Machines with a history through Kern.hmachine_sound (History.lean): Lean builds the plain machine (sums as state,
764
943
  // relations in the assumption) and each relation's record goal; the trees and the instance keys are the kernel's.
@@ -777,6 +956,25 @@ export function toLean(items, opts = {}) {
777
956
  const ident = (id) => { const s = id.replace(/[^A-Za-z0-9_]/g, '_'); return /^[A-Za-z]/.test(s) ? s : `x_${s}`; };
778
957
  const parts = [], theorems = [];
779
958
  let goals = 0;
959
+ // Each recurrence defined once, before the first item that reads it.
960
+ const recurs = new Map();
961
+ const recurDefs = (m) => Object.entries(m.fns ?? {}).filter(([, f]) => f.recur).map(([n, f]) => {
962
+ // The functions it reads are the machine's <n>.<use>, defined before it (flatFns puts them first).
963
+ const uses = Object.fromEntries(Object.entries(f.recur.uses).map(([u, d]) => { const g = m.fns?.[`${n}.${u}`]; return [u, { ...d, ...(g?.recur ? { recur: g.recur } : {}) }]; }));
964
+ const text = recurToLean(f.lean, f.recur, uses), had = recurs.get(f.lean);
965
+ if (had !== undefined) {
966
+ if (had !== text)
967
+ throw new Error(`${f.lean}: two recurrences of one name`);
968
+ return '';
969
+ }
970
+ const pairing = f.recur.args.length && !recurs.has('') ? (recurs.set('', recurPairing), `${recurPairing}\n`) : '';
971
+ recurs.set(f.lean, text);
972
+ if (f.recur.lemmas.length)
973
+ theorems.push(`${f.lean}_lemmas`);
974
+ if (f.recur.between.length)
975
+ theorems.push(`${f.lean}_between`);
976
+ return `${pairing}${text}\n`;
977
+ }).join('');
780
978
  for (let item of items) {
781
979
  const id = ident(item.id);
782
980
  if ('machine' in item) {
@@ -791,7 +989,7 @@ export function toLean(items, opts = {}) {
791
989
  }
792
990
  // One twin: every machine goes to Lean as a machine with sets (one with none, if it has none): its laws, step laws,
793
991
  // liveness and ghosts in one export; a machine with a history as its plain machine, and its laws over histories.
794
- parts.push(`-- ${item.id}\n${item.lean?.defs ? `${item.lean.defs}\n` : ''}${smachineToLean(id, m, item.laws, { stepLaws: item.stepLaws, ...(item.liveness?.length ? { liveness: item.liveness } : {}),
992
+ parts.push(`-- ${item.id}\n${recurDefs(m)}${item.lean?.defs ? `${item.lean.defs}\n` : ''}${smachineToLean(id, m, item.laws, { stepLaws: item.stepLaws, ...(item.liveness?.length ? { liveness: item.liveness } : {}),
795
993
  ...(item.ghosts?.length ? { ghosts: item.ghosts } : {}), ...(item.lean?.simp ? { simp: item.lean.simp } : {}), ...(item.lean?.after ? { after: item.lean.after } : {}) })}`);
796
994
  theorems.push(`${id}_laws`, ...(m.history ? [`${id}_hlaws`] : []), ...(item.stepLaws ?? []).map((_, j) => `${id}_step${j}_holds`), ...(item.liveness ?? []).map((_, j) => `${id}_live${j}_holds`));
797
995
  if (item.lean?.after)
@@ -3,3 +3,7 @@ export declare function leanTerms(vars: readonly string[], maps: readonly string
3
3
  e: (t: E) => string;
4
4
  c: (t: C) => string;
5
5
  };
6
+ export declare function leanSurface(names: Record<string, string>, where: string, get?: (map: string, key: E, e: (t: E) => string) => string): {
7
+ e: (t: E) => string;
8
+ c: (t: C) => string;
9
+ };
@@ -42,3 +42,41 @@ export function leanTerms(vars, maps, where) {
42
42
  };
43
43
  return { e, c };
44
44
  }
45
+ // Terms and conditions as plain Lean over Nat (a recurrence's definition and its lemmas): each variable as the Lean
46
+ // expression `names` gives it.
47
+ export function leanSurface(names, where, get) {
48
+ const name = (n) => { const s = names[n]; if (s === undefined)
49
+ throw new Error(`${where}: no Lean expression for ${n}`); return s; };
50
+ const e = (t) => {
51
+ switch (t.op) {
52
+ case 'var': return name(t.name);
53
+ case 'const': return String(t.value);
54
+ case 'mul': return `(${t.k} * ${e(t.a)})`;
55
+ case 'add': return `(${e(t.a)} + ${e(t.b)})`;
56
+ case 'sub': return `(${e(t.a)} - ${e(t.b)})`;
57
+ case 'prod': return `(${e(t.a)} * ${e(t.b)})`;
58
+ case 'min':
59
+ case 'max': return `(${t.op} ${e(t.a)} ${e(t.b)})`;
60
+ case 'if': return `(if ${c(t.c)} then ${e(t.then)} else ${e(t.else)})`;
61
+ case 'get':
62
+ if (!get)
63
+ throw new Error(`${where}: reads ${t.map}, a function or a map`);
64
+ return get(t.map, t.key, e);
65
+ default: return unreachable(t);
66
+ }
67
+ };
68
+ const c = (t) => {
69
+ switch (t.op) {
70
+ case '<=': return `${e(t.a)} ≤ ${e(t.b)}`;
71
+ case '<': return `${e(t.a)} < ${e(t.b)}`;
72
+ case '=': return `${e(t.a)} = ${e(t.b)}`;
73
+ case 'not': return `¬(${c(t.x)})`;
74
+ case 'and':
75
+ case 'or': return t.xs.length === 0 ? (t.op === 'and' ? 'True' : 'False') : `(${t.xs.map(c).join(t.op === 'and' ? ' ∧ ' : ' ∨ ')})`;
76
+ case 'flag':
77
+ case 'all': throw new Error(`${where}: takes no flag or quantifier`);
78
+ default: return unreachable(t);
79
+ }
80
+ };
81
+ return { e, c };
82
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdp-wired",
3
- "version": "0.6.0",
3
+ "version": "0.8.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",
@@ -9,6 +9,7 @@
9
9
  "state-machine",
10
10
  "rate-limiting",
11
11
  "policy",
12
+ "gdp-ts",
12
13
  "lean",
13
14
  "automatic-prover",
14
15
  "theorem-prover",
@@ -59,6 +60,10 @@
59
60
  "types": "./dist/core/solver.d.ts",
60
61
  "default": "./dist/core/solver.js"
61
62
  },
63
+ "./gdp": {
64
+ "types": "./dist/core/gdp.d.ts",
65
+ "default": "./dist/core/gdp.js"
66
+ },
62
67
  "./package.json": "./package.json"
63
68
  },
64
69
  "files": [
@@ -69,6 +74,14 @@
69
74
  "engines": {
70
75
  "node": ">=20"
71
76
  },
77
+ "peerDependencies": {
78
+ "@gdp-ts/core": ">=0.1.0"
79
+ },
80
+ "peerDependenciesMeta": {
81
+ "@gdp-ts/core": {
82
+ "optional": true
83
+ }
84
+ },
72
85
  "scripts": {
73
86
  "typecheck": "tsc -p .",
74
87
  "lint": "oxlint --type-aware --deny-warnings --disable-nested-config",
@@ -76,13 +89,14 @@
76
89
  "build": "bun scripts/build.ts",
77
90
  "prepublishOnly": "bun scripts/build.ts",
78
91
  "smoke": "sh scripts/smoke.sh",
79
- "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 && 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/lists.ts && bun examples/sizes.ts && bun examples/liveness.ts && bun examples/budget.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 && 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"
80
93
  },
81
94
  "devDependencies": {
95
+ "@gdp-ts/core": "0.1.0",
82
96
  "@types/bun": "1.3.1",
83
- "typescript": "5.9.3",
84
97
  "@webgpu/types": "0.1.66",
85
98
  "oxlint": "1.88.0",
86
- "oxlint-tsgolint": "7.0.2004"
99
+ "oxlint-tsgolint": "7.0.2004",
100
+ "typescript": "5.9.3"
87
101
  }
88
102
  }