gdp-wired 0.14.2 → 0.16.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
@@ -69,6 +69,14 @@ All of these are proven automatically, and each proof is also a Lean theorem:
69
69
  machine runs as, with each guard and action it names given again in gdp-wired's terms, and `conforms(chart, machine,
70
70
  createActor)` drives XState's own actor and the compiled rule with the same random events, comparing every state and
71
71
  context; what a chart can't run the same way is refused when built, with the reason (`examples/xstate.ts`).
72
+ - **What nobody stated, looked for when the prover alone stops**: where `proveRule` or `proveSteps` doesn't prove the laws
73
+ and no run breaks them, it looks for lemmas about the rule's recurrences and lists (`discover`) and then invariants
74
+ (`inferLaws`), within 10 s (`{search: {ms}}`; `{search: false}`, the prover alone); the verdict's `found` holds what
75
+ proved them, as laws and lemmas with the source to paste.
76
+ - **Invariants nobody wrote** (`gdp-wired/infer`): `inferLaws(rule, laws)` proves a law that holds but isn't inductive
77
+ alone, with the laws it finds (bounds, sums and differences over the state, the same where a flag or a choice holds
78
+ one value, and laws about every key of a map), each proven by the kernel with yours and handed back as the
79
+ `r.law(...)` call that states it; a false law comes back with the run that breaks it (`examples/infer.ts`).
72
80
  - **Orderings at any distance**: "a refund never follows a release", through ghost state that watches the rule.
73
81
  - **Liveness**: "every ticket issued is served", in every run that keeps sending the requests that help, including under
74
82
  weak fairness (`examples/liveness.ts`).
@@ -78,6 +86,10 @@ All of these are proven automatically, and each proof is also a Lean theorem:
78
86
  - **Lemmas you state**: a function defined by a recurrence (`recur`), of one argument or several, reading other
79
87
  functions, and lemmas about it proven by induction, at one point or between two, which every rule that reads it may
80
88
  then use: a backoff delay is at most 64 at every attempt and never shrinks (`examples/lemmas.ts`).
89
+ - **Lemmas nobody stated** (`gdp-wired/discover`): `discover(rule, laws, stepLaws)` finds what a recurrence or the folds
90
+ of a list a request carries need, from their values (bounds, linear relations, a closed form of degree 2 at most, a
91
+ law between two points), proves it by the same induction and keeps only what the laws need; `discoverFold(fold,
92
+ laws)` does the same for a fold over a tree. What it finds comes back as source to paste (`examples/discover.ts`).
81
93
  - **Records and variants**: inputs, state, results, map cells and list items made of named fields, or one of several
82
94
  cases each with fields of its own (a payment by card or in cash, a seat free, held or sold), every case covered
83
95
  (`examples/datatypes.ts`).
@@ -90,13 +102,13 @@ All of these are proven automatically, and each proof is also a Lean theorem:
90
102
  secret never changes the answer". `examples/jev-fabric/` ports a Bend project's policy core and proves all 59 laws its
91
103
  Bend proofs state.
92
104
 
93
- When a law is false, the GPU search finds a real run that breaks it. When a law needs an invariant nobody wrote, the GPU
94
- synthesizer finds one and the kernel proves it.
105
+ When a law is false, the GPU search finds a real run that breaks it. When a law needs an invariant nobody wrote,
106
+ `inferLaws` finds one on the CPU (and the GPU synthesizer, at scale, in `research/synth/`) and the kernel proves it.
95
107
 
96
108
  **What it leaves to Lean or Bend.** Two things, and neither is a gap the next feature closes: proofs the prover would
97
- have to invent (induction over a recursive structure whose proof needs a lemma nobody stated, and arithmetic with
98
- products of unknowns beyond multiplying two known facts; integer arithmetic with induction or with products is
99
- undecidable; a lemma you state about a recurrence is proven, but a recursive datatype as a value is Lean's), and shipping an arbitrary algorithm as its proved source (gdp-wired ships the rule as JavaScript; Bend
109
+ have to invent (induction over a recursive structure whose proof needs a lemma nobody stated and `discover` doesn't find, and arithmetic with
110
+ products of unknowns beyond what products of facts and sums of squares give; integer arithmetic with induction or
111
+ with products is undecidable; a lemma you state about a recurrence is proven, but a recursive datatype as a value is Lean's), and shipping an arbitrary algorithm as its proved source (gdp-wired ships the rule as JavaScript; Bend
100
112
  compiles the proved source itself). The full list of what is and isn't in scope is in
101
113
  [`docs/comparison.md`](docs/comparison.md#gdp-wireds-scope-in-detail).
102
114
 
@@ -238,6 +250,13 @@ const soon = retry.law('the next try is at most 64 after the last request', ({ne
238
250
  `Acct.set(k, value)`, `Acct.each(k => value)`) and `.items({Queue: Job})` (`push`, `pop`, `set`, `insertAt`,
239
251
  `removeAt`, `apply`, `has`, `within`, `increasing(by)`), one map per variable of the kind; `rule.decode` reads them back
240
252
  as objects and arrays.
253
+ - **Sequences a request carries** (`.carriesSeq('Path')`, and tables of them, `.carriesSeqs('Paths')`): a sequence of
254
+ naturals as positions and a length, `Path.at(i)`, `Path.len`, a table's row `Paths.row(r)`; `A.prefixOf(B)`,
255
+ `startsWith`, `equals`, `extends(B, k)`, `has(x)`, each a condition about every position below a length, so laws about
256
+ prefixes of paths need no induction or lemma. The request gives an array of naturals (interned names), a table an
257
+ array of them. `everyBelow(n, k => ...)` checks bookkeeping a request carries row by row (where each grant went). A
258
+ condition about every row of a table can't compare the rows' sequences (that is two keys at once); compare a row at a
259
+ named row, a witness, instead (`examples/scopes.ts`: pi-fabric's path scopes, covering, attenuation and normalization).
241
260
  - **Lists a request carries** (`.carriesList('Lines', Line, folds, lemmas)`): the items one carried map per variable of
242
261
  the kind, the length `Lines.len`, each fold (`{total: {from: 0, step: (acc, line) => acc.plus(line.amount)}}`) a
243
262
  carried map of its running values whose equations every request is assumed to meet (the encoding computes them from the
@@ -276,8 +295,12 @@ const soon = retry.law('the next try is at most 64 after the last request', ({ne
276
295
  declared pattern (Cedar's glob), all computed from the string itself when the input is encoded. `s.like(pattern)`,
277
296
  `s.eq('admin')` (which carries the literal's length and pattern bits, so laws reason from either), `s.isEmpty`,
278
297
  `s.length`; sets of strings are carried maps keyed by `s.id`.
279
- - **Arithmetic** over the naturals: `plus`, `minus` (truncated), `times(k)`, `mul` (a product of two terms), `min`,
280
- `max`, `when(cond, a, b)`, `count(cond)`; conditions `lt`, `atMost`, `eq`, `and`, `or`, `not`, `implies`.
298
+ - **Arithmetic** over the naturals: `plus`, `minus` (truncated), `times(k)`, `mul` (a product of two terms, a map's
299
+ cell among them: `Used.at(k).atMost(per.mul(Seats.at(k)))`), `min`, `max`, `when(cond, a, b)`, `count(cond)`;
300
+ conditions `lt`, `atMost`, `eq`, `and`, `or`, `not`, `implies`. Where a law with products isn't linear in its
301
+ monomials, the prover multiplies facts together and adds squares, as Lean's `nlinarith` does: (x − y)² for each
302
+ product x·y the law reads, its factors as the step leaves them, and for each monomial split in two halves
303
+ (Cauchy–Schwarz's (ad − bc)²), times each factor past degree two (`research/frontier/nonlinear.ts`).
281
304
  - **Sets and quantifiers**: `.carries(...)` for sets a request brings, `everyKey(k => ...)` (and `has`, `subsetOf`,
282
305
  `disjointFrom`, `sameAs`) anywhere in a step or inside a law, `everyKey((i, j) => ...)` in a step for every two keys
283
306
  (each read on its own, compared with the other plainly: `i.lt(j)`), `M.each(k => ...)` to write every key, and
@@ -378,12 +401,13 @@ The npm package is built from `core/`, `kernel/` and `lean-kernel/` alone; nothi
378
401
  ## Running
379
402
 
380
403
  The npm package is the library: the builders (`gdp-wired`), the runtimes (`gdp-wired/machine`), composition
381
- (`gdp-wired/compose`), XState machines as charts (`gdp-wired/xstate`), the Lean export (`gdp-wired/lean`), the kernel
382
- (`gdp-wired/kernel`), the prover (`gdp-wired/prove`), the derivation checker and solver (`gdp-wired/check`,
383
- `gdp-wired/solver`). Everything else here (examples, comparisons, the Lean twin, the GPU searches) runs from a clone and
384
- needs [Bun](https://bun.sh); Lean 4.34.0 for `lean/` and `lean-kernel/`; a WebGPU runtime (Chrome, or Dawn's node
385
- bindings) for the GPU parts. Releases: bump `version` in `package.json` and push to main;
386
- `.github/workflows/publish.yml` publishes it with npm provenance.
404
+ (`gdp-wired/compose`), XState machines as charts (`gdp-wired/xstate`), lemmas nobody stated (`gdp-wired/discover`), invariant
405
+ inference (`gdp-wired/infer`), the
406
+ Lean export (`gdp-wired/lean`), the kernel (`gdp-wired/kernel`), the prover (`gdp-wired/prove`), the derivation checker
407
+ and solver (`gdp-wired/check`, `gdp-wired/solver`). Everything else here (examples, comparisons, the Lean twin, the GPU
408
+ searches) runs from a clone and needs [Bun](https://bun.sh); Lean 4.34.0 for `lean/` and `lean-kernel/`; a WebGPU
409
+ runtime (Chrome, or Dawn's node bindings) for the GPU parts. Releases: bump `version` in `package.json` and push to
410
+ main; `.github/workflows/publish.yml` publishes it with npm provenance.
387
411
 
388
412
  ```sh
389
413
  git clone --recursive https://github.com/monotykamary/gdp-wired && cd gdp-wired
@@ -256,9 +256,9 @@ export class Chart {
256
256
  missed.push({ name, law: r.law(`${name} is never reached`, (env) => env[reg.field].is(l).not()) });
257
257
  }
258
258
  const proven = (v) => v.proven && v.checked;
259
- const allMissed = missed.length > 0 && proven(proveRule(r, [...this.chartLaws(), ...missed.map(x => x.law)]));
259
+ const allMissed = missed.length > 0 && proven(proveRule(r, [...this.chartLaws(), ...missed.map(x => x.law)], { search: false }));
260
260
  for (const x of missed)
261
- reached[x.name] = allMissed || proven(proveRule(r, [...this.chartLaws(), x.law])) ? 'unreachable' : 'unknown';
261
+ reached[x.name] = allMissed || proven(proveRule(r, [...this.chartLaws(), x.law], { search: false })) ? 'unreachable' : 'unknown';
262
262
  // A way out is a transition to a state: an internal one stays where it is.
263
263
  const stuck = regions.flatMap(reg => reg.leaves.filter(l => !reg.finals.has(l) && !placed.some(t => t.to !== undefined && t.region === reg.name && t.sources.includes(l))).map(l => label(reg, l)));
264
264
  // Pairs as specific as each other, on one event, from a state they share: proven never both enabled, all at once,
@@ -270,9 +270,9 @@ export class Chart {
270
270
  pairs.push({ t, u, law: r.stepLaw(`transitions ${t.order} and ${u.order} on ${t.event} are never both enabled`, (env) => all(env[`on${i}`], env[`on${j}`]).not()) });
271
271
  }));
272
272
  const ambiguous = [];
273
- if (pairs.length && !proven(proveSteps(r, this.chartLaws(), pairs.map(x => x.law))))
273
+ if (pairs.length && !proven(proveSteps(r, this.chartLaws(), pairs.map(x => x.law), { search: false })))
274
274
  for (const { t, u, law } of pairs) {
275
- const v = proveSteps(r, this.chartLaws(), [law]);
275
+ const v = proveSteps(r, this.chartLaws(), [law], { search: false });
276
276
  if (!proven(v))
277
277
  ambiguous.push({ event: t.event, transitions: [t.order, u.order], verdict: v });
278
278
  }
@@ -0,0 +1,27 @@
1
+ import { type Law } from './machine.js';
2
+ import { type StepLaw, type Verdict, Rule } from './dsl.js';
3
+ import { type Fold } from '../kernel/kernel.js';
4
+ type Obj = object;
5
+ type AnyRule = Rule<Obj, Obj, Obj, Obj, Obj, Obj, Obj, Obj>;
6
+ export type Found = {
7
+ lemmas?: string[];
8
+ between?: string[];
9
+ };
10
+ export type Discovery = {
11
+ verdict: Verdict;
12
+ rule: AnyRule;
13
+ found: Record<string, Found>;
14
+ };
15
+ export declare function discover(r: AnyRule, laws: Law[], stepLaws?: StepLaw[], first?: Verdict): Discovery;
16
+ type TreeFold = {
17
+ fold: Fold;
18
+ lemmas?: Law[];
19
+ name: string;
20
+ };
21
+ export type FoldDiscovery = {
22
+ verdict: Verdict;
23
+ lemmas: Law[];
24
+ found: string[];
25
+ };
26
+ export declare function discoverFold(f: TreeFold, laws: Law[]): FoldDiscovery;
27
+ export {};
@@ -0,0 +1,491 @@
1
+ // Lemmas nobody stated, found: when a rule's laws need a fact about a function defined by a recurrence (`recur`) or
2
+ // about the folds of a list a request carries (`carriesList`) that nobody wrote, `discover` looks for it. Candidates come
3
+ // from the values themselves: the function computed at many points, the folds over many random lists, and every
4
+ // statement of a few shapes that all of them meet (bounds, bounds linear in x, in the further arguments, in the other
5
+ // folds or in the params, a closed form fitted exactly to the values, a polynomial of degree 2 at most, and laws
6
+ // between two points: never shrinks, never grows faster than it did). Those are proven the way stated lemmas are, by
7
+ // induction, together (one that isn't inductive is dropped and the rest tried again: Houdini), and of what is proven
8
+ // only the lemmas the laws need are kept. Nothing here is trusted: what it finds is a lemma like any other, proven by
9
+ // the kernel when the function or the list is made again with it, and exported to Lean the same way.
10
+ // const out = discover(r, laws, stepLaws)
11
+ // out.verdict // the laws, proven of out.rule: r with its functions and lists made again with the lemmas
12
+ // out.found // per function or list, the lemmas found, as source to paste into recur(...) or carriesList(...)
13
+ import { any, atMost, compileFunction, evalC, evalE, n, plus, prod, same, times, x } from "./machine.js";
14
+ import { Unproven, lit, proveFold, proveSteps, recur, recurOf } from "./dsl.js";
15
+ const seeded = (seed) => { let s = seed; return (k) => { s = (s * 1103515245 + 12345) % 2 ** 31; return Math.floor(s / 2 ** 31 * k); }; };
16
+ const valueOf = (t) => (typeof t === 'number' ? t : t.val);
17
+ const evalLin = (l, at) => Object.entries(l).reduce((s, [a, c]) => s + c * (a === '1' ? 1 : at[a]), 0);
18
+ const holds = (c, at) => { const v = evalLin(c.lin, at); return c.rel === '=' ? v === 0 : v <= 0; };
19
+ // The statement as a condition over terms, and as source: the atoms with a positive coefficient on one side, the rest on
20
+ // the other (naturals: no subtraction).
21
+ const sides = (l) => {
22
+ const pos = [], neg = [];
23
+ for (const [a, c] of Object.entries(l))
24
+ if (c > 0)
25
+ pos.push([a, c]);
26
+ else if (c < 0)
27
+ neg.push([a, -c]);
28
+ return { pos, neg };
29
+ };
30
+ const termOfSide = (side, atoms) => {
31
+ const ts = side.map(([a, c]) => (a === '1' ? lit(c) : c === 1 ? atoms[a] : atoms[a].times(c)));
32
+ return ts.length ? ts.slice(1).reduce((acc, t) => acc.plus(t), ts[0]) : lit(0);
33
+ };
34
+ const condOf = (c, atoms) => {
35
+ const { pos, neg } = sides(c.lin), a = termOfSide(pos, atoms), b = termOfSide(neg, atoms);
36
+ return c.rel === '=' ? a.eq(b) : a.atMost(b);
37
+ };
38
+ const srcOfSide = (side, src) => {
39
+ const parts = side.filter(([a]) => a !== '1'), k = side.find(([a]) => a === '1')?.[1];
40
+ if (!parts.length)
41
+ return String(k ?? 0);
42
+ const one = ([a, c]) => (c === 1 ? src(a) : `${src(a)}.times(${c})`);
43
+ return [one(parts[0]), ...parts.slice(1).map(p => `.plus(${one(p)})`), ...(k ? [`.plus(${k})`] : [])].join('');
44
+ };
45
+ // The side that holds the value the lemma is about (f(x), or the fold) goes first: f.atLeast(1), not lit(1).atMost(f).
46
+ const flipped = (c, target) => { const { pos, neg } = sides(c.lin); return neg.some(([a]) => a === target) || (pos.every(([a]) => a === '1') && neg.some(([a]) => a !== '1')); };
47
+ const srcOf = (c, src, target) => {
48
+ const { pos, neg } = sides(c.lin);
49
+ if (flipped(c, target))
50
+ return `${srcOfSide(neg, src)}.${c.rel === '=' ? 'eq' : 'atLeast'}(${srcOfSide(pos, src)})`;
51
+ return `${srcOfSide(pos, src)}.${c.rel === '=' ? 'eq' : 'atMost'}(${srcOfSide(neg, src)})`;
52
+ };
53
+ // The name a found lemma goes by: the statement in plain arithmetic.
54
+ const nameOf = (c, show, target) => {
55
+ const { pos, neg } = sides(c.lin);
56
+ const side = (s) => (s.length ? [...s.filter(([a]) => a === target), ...s.filter(([a]) => a !== target)].map(([a, k]) => (a === '1' ? String(k) : k === 1 ? show(a) : `${k}·${show(a)}`)).join(' + ') : '0');
57
+ return flipped(c, target) ? `${side(neg)} ${c.rel === '=' ? '=' : '>='} ${side(pos)}` : `${side(pos)} ${c.rel === '=' ? '=' : '<='} ${side(neg)}`;
58
+ };
59
+ // Candidates: `target` against forms built from `bases` (each a scaled atom or nothing) and a fitted constant, kept when
60
+ // the samples hold them. An equality where the upper and lower constant meet.
61
+ function linearCands(target, forms, samples, show) {
62
+ const out = [];
63
+ for (const form of forms) {
64
+ const d = samples.map(s => s[target] - evalLin(form, s)), hi = Math.max(...d), lo = Math.min(...d);
65
+ const lin = (k, sign) => { const l = { [target]: sign }; for (const [a, c] of Object.entries(form))
66
+ l[a] = (l[a] ?? 0) - sign * c; if (k)
67
+ l['1'] = -sign * k; return l; };
68
+ const constOnly = Object.keys(form).length === 0;
69
+ if (hi === lo)
70
+ out.push({ name: '', rel: '=', lin: lin(hi, 1) });
71
+ else {
72
+ out.push({ name: '', rel: '<=', lin: lin(hi, 1) });
73
+ // f >= 0 says nothing; a lower bound against a form only where it is one.
74
+ if (!(constOnly && lo <= 0))
75
+ out.push({ name: '', rel: '<=', lin: lin(lo, -1) });
76
+ }
77
+ }
78
+ for (const c of out) {
79
+ c.name = nameOf(c, show, target);
80
+ c.target = target;
81
+ }
82
+ return dedupe(out.filter(c => samples.every(s => holds(c, s))));
83
+ }
84
+ const dedupe = (cs) => [...new Map(cs.map(c => [c.name, c])).values()];
85
+ // A polynomial in x of degree 2 at most, exact over the rationals, as d·f = a·x·x + b·x + c with whole numbers: fitted to
86
+ // three points, kept when every sample meets it.
87
+ function closedForm(samples, show) {
88
+ const at = new Map();
89
+ for (const s of samples) {
90
+ if (at.has(s.x) && at.get(s.x) !== s.f)
91
+ return [];
92
+ at.set(s.x, s.f);
93
+ }
94
+ if (![0, 1, 2].every(x => at.has(x)))
95
+ return [];
96
+ const [f0, f1, f2] = [at.get(0), at.get(1), at.get(2)];
97
+ // f = a x² + b x + c: c = f0, a = (f2 - 2 f1 + f0) / 2, b = f1 - f0 - a; times 2 to keep whole numbers.
98
+ const A = f2 - 2 * f1 + f0, B = 2 * (f1 - f0) - A, C = 2 * f0;
99
+ if (A === 0)
100
+ return []; // linear: the bounds find it
101
+ const g = [A, B, C, 2].reduce((p, q) => gcd(p, Math.abs(q)), 0);
102
+ const c = { name: '', rel: '=', lin: Object.fromEntries(Object.entries({ f: 2 / g, xx: -A / g, x: -B / g, 1: -C / g }).filter(([, k]) => k !== 0)) };
103
+ c.name = nameOf(c, show, 'f');
104
+ return samples.every(s => holds(c, s)) ? [c] : [];
105
+ }
106
+ // The law a goal is of (the kernel names each goal after its law: '<law> @step').
107
+ const lawOfGoal = (goal) => goal.replace(/ @(start|step|step law|\d+ steps|leaf|node)$/, '');
108
+ const isGoalOf = (goal) => (c) => { const law = lawOfGoal(goal); return law === c.name || law === `${c.name} at y`; };
109
+ const gcd = (a, b) => (b === 0 ? Math.abs(a) : gcd(b, a % b));
110
+ // --- recurrences ---------------------------------------------------------------------------------------------------
111
+ const showRecur = (a) => (a === 'f' ? 'f(x)' : a === 'g' ? 'f(y)' : a === 'xx' ? 'x·x' : a);
112
+ const srcRecur = (a) => (a === 'xx' ? 'x.mul(x)' : a);
113
+ // The function's values: x from 0 to 40 at a few values of each further argument (all of them for two at most).
114
+ function recurSamples(d, args) {
115
+ const f = (typeof d.js === 'function' ? d.js : compileFunction([], `return (${d.js})`)());
116
+ const vals = [0, 1, 2, 3, 5, 8, 13, 40];
117
+ const tuples = args.reduce((acc) => acc.flatMap(t => vals.map(v => [...t, v])), [[]]);
118
+ const out = [];
119
+ for (const t of tuples)
120
+ for (let x = 0; x <= 40; x++) {
121
+ let y;
122
+ try {
123
+ y = f(x, ...t);
124
+ }
125
+ catch (e) {
126
+ if (e instanceof RangeError)
127
+ break;
128
+ throw e;
129
+ }
130
+ out.push({ x, f: y, xx: x * x, ...Object.fromEntries(args.map((a, i) => [a, t[i]])) });
131
+ }
132
+ return out;
133
+ }
134
+ // Lemmas about every x and f(x), and laws between two points, that every value meets.
135
+ function recurCands(samples, args) {
136
+ const slopes = new Set();
137
+ const byArgs = new Map();
138
+ for (const s of samples) {
139
+ const k = args.map(a => s[a]).join();
140
+ byArgs.set(k, [...(byArgs.get(k) ?? []), s]);
141
+ }
142
+ for (const run of byArgs.values())
143
+ for (let i = 1; i < run.length; i++) {
144
+ const dd = run[i].f - run[i - 1].f;
145
+ if (dd > 0)
146
+ slopes.add(dd);
147
+ }
148
+ // The gentlest slopes and the steepest.
149
+ const sorted = [...slopes].sort((a, b) => a - b), picked = [...new Set([...sorted.slice(0, 3), ...sorted.slice(-1)])];
150
+ const xs = [{}, ...picked.map(s => ({ x: s }))];
151
+ const forms = xs.flatMap(fx => [fx, ...args.map(a => ({ ...fx, [a]: 1 }))]);
152
+ const lemmas = [...linearCands('f', forms, samples, showRecur), ...(args.length ? [] : closedForm(samples, showRecur))];
153
+ const pairs = [];
154
+ for (const run of byArgs.values())
155
+ for (const a of run)
156
+ for (const b of run)
157
+ if (a.x <= b.x)
158
+ pairs.push({ x: a.x, f: a.f, y: b.x, g: b.f });
159
+ const steepest = Math.max(0, ...slopes);
160
+ const between = [
161
+ { name: '', rel: '<=', lin: { f: 1, g: -1 } },
162
+ { name: '', rel: '<=', lin: { g: 1, f: -1 } },
163
+ ...(steepest ? [{ name: '', rel: '<=', lin: { g: 1, x: steepest, f: -1, y: -steepest } }] : []),
164
+ ].filter(c => pairs.every(p => holds(c, p)));
165
+ for (const c of between)
166
+ c.name = nameOf(c, showRecur, 'g');
167
+ return { lemmas, between };
168
+ }
169
+ const recurAtoms = (args, x, f, env, y, g) => ({ x, f, xx: x.mul(x), ...(y && g ? { y, g } : {}), ...Object.fromEntries(args.map(a => [a, env[a]])) });
170
+ const recurSrc = (c, args, between) => {
171
+ const reads = (a) => Object.keys(c.lin).includes(a) || (a === 'x' && 'xx' in c.lin);
172
+ const names = between ? ['x', 'f', 'y', 'g'] : ['x', 'f'];
173
+ let unused = 0;
174
+ const params = [...names.map(n => (reads(n) ? n : '_'.repeat(++unused))), ...(args.some(reads) ? [`{${args.filter(reads).join(', ')}}`] : [])];
175
+ while (params.length && /^_+$/.test(params.at(-1)))
176
+ params.pop();
177
+ return `${quoted(c.name)}: (${params.join(', ')}) => ${srcOf(c, srcRecur, between ? 'g' : 'f')}`;
178
+ };
179
+ // The function made again with the lemmas and laws between, those that aren't proven dropped one at a time.
180
+ function houdiniRecur(d, lemmas, between) {
181
+ const src = recurOf(d), args = [...(src.def.args ?? [])];
182
+ let ls = [...lemmas], bs = [...between];
183
+ for (let round = 0; round < lemmas.length + between.length + 1; round++) {
184
+ try {
185
+ const made = recur(src.name, src.def, { ...src.lemmas, ...Object.fromEntries(ls.map(c => [c.name, (x, f, env) => condOf(c, recurAtoms(args, x, f, env))])) }, { ...src.between, ...Object.fromEntries(bs.map(c => [c.name, (x, f, y, g, env) => condOf(c, recurAtoms(args, x, f, env, y, g))])) });
186
+ return { d: made, lemmas: ls, between: bs };
187
+ }
188
+ catch (e) {
189
+ if (!(e instanceof Unproven))
190
+ throw e;
191
+ const goal = e.verdict.goal ?? '', l = ls.find(isGoalOf(goal)), b = bs.find(isGoalOf(goal));
192
+ if (l)
193
+ ls = ls.filter(c => c !== l);
194
+ else if (b)
195
+ bs = bs.filter(c => c !== b);
196
+ else if (bs.length)
197
+ bs = []; // a lemma at y that isn't proven between the points: give up on the laws between
198
+ else
199
+ return undefined;
200
+ }
201
+ }
202
+ return undefined;
203
+ }
204
+ const natParams = (params) => params.filter(([n, k]) => { const vs = k.vars(n); return vs.length === 1 && vs[0] === n; }).map(([n]) => n);
205
+ function listSamples([name, kind, folds], params) {
206
+ const rand = seeded(7), out = [];
207
+ for (let trial = 0; trial < 300; trial++) {
208
+ const pv = Object.assign({}, ...params.map(([n, k]) => k.random(n, rand, 12)));
209
+ const p = Object.fromEntries(params.map(([n, k]) => [n, k.build(n, part => lit(pv[part]))]));
210
+ const acc = {};
211
+ for (const [f, d] of Object.entries(folds))
212
+ acc[f] = typeof d.from === 'number' ? d.from : valueOf(d.from(p));
213
+ const len = rand(9), at = (i) => ({ ...acc, i, ...pv });
214
+ out.push(at(0));
215
+ for (let i = 0; i < len; i++) {
216
+ const raw = kind.random(name, rand, 12), item = kind.build(name, part => lit(raw[part] ?? 0));
217
+ for (const [f, d] of Object.entries(folds))
218
+ acc[f] = valueOf(d.step(lit(acc[f]), item, p));
219
+ out.push(at(i + 1));
220
+ }
221
+ }
222
+ return out.filter(s => Object.values(s).every(v => v !== undefined && Number.isSafeInteger(v)));
223
+ }
224
+ function listCands(part, params, samples) {
225
+ const folds = Object.keys(part[2]), ps = natParams(params);
226
+ // The position reads only as itself (a list's lemmas hold at every position, a condition at every key, which reads its
227
+ // key alone or plus a constant).
228
+ const out = [];
229
+ for (const f of folds) {
230
+ const others = [...folds.filter(g => g !== f), ...ps];
231
+ const forms = [{}, { i: 1 }, ...others.map(g => ({ [g]: 1 })), ...others.flatMap((g, j) => others.slice(j + 1).map(h => ({ [g]: 1, [h]: 1 })))];
232
+ out.push(...linearCands(f, forms, samples, a => a));
233
+ }
234
+ return dedupe(out);
235
+ }
236
+ const listSrc = (c, folds, params, target) => {
237
+ const reads = (a) => a in c.lin;
238
+ const ps = [`{${folds.filter(reads).join(', ')}}`, ...(reads('i') || params.some(reads) ? [reads('i') ? 'i' : '_'] : []), ...(params.some(reads) ? [`{${params.filter(reads).join(', ')}}`] : [])];
239
+ return `${quoted(c.name)}: (${ps.join(', ')}) => ${srcOf(c, a => a, target)}`;
240
+ };
241
+ const listLemma = (c, params) => (acc, i, p) => condOf(c, { ...acc, i, ...Object.fromEntries(params.map(n => [n, p[n]])) });
242
+ function houdiniList(r, part, cands, params) {
243
+ let ls = [...cands];
244
+ for (let round = 0; round <= cands.length; round++) {
245
+ try {
246
+ return { rule: r.withParts({}, { [part[0]]: Object.fromEntries(ls.map(c => [c.name, listLemma(c, params)])) }), kept: ls };
247
+ }
248
+ catch (e) {
249
+ if (!(e instanceof Unproven))
250
+ throw e;
251
+ const goal = e.verdict.goal ?? '', l = ls.find(isGoalOf(goal));
252
+ if (!l)
253
+ return undefined;
254
+ ls = ls.filter(c => c !== l);
255
+ }
256
+ }
257
+ return undefined;
258
+ }
259
+ // --- the search ----------------------------------------------------------------------------------------------------
260
+ // The laws of r, and where they aren't proven as r states them, the lemmas found for its functions and lists: all that
261
+ // is proven first, then each dropped in turn while the laws stay proven without it.
262
+ // `first`: the laws' verdict on r alone, when the caller has it.
263
+ export function discover(r, laws, stepLaws = [], first = proveSteps(r, laws, stepLaws, { search: false })) {
264
+ if (first.proven && first.checked)
265
+ return { verdict: first, rule: r, found: {} };
266
+ const { params, fns, lists } = r.parts, ps = natParams(params);
267
+ // Per function and per list: what is proven, made into the rule.
268
+ const fnMade = {};
269
+ for (const [n, d] of fns) {
270
+ const src = recurOf(d);
271
+ if (!src)
272
+ continue;
273
+ const args = [...(src.def.args ?? [])], { lemmas, between } = recurCands(recurSamples(d, args), args);
274
+ const made = houdiniRecur(d, lemmas, between);
275
+ if (made && (made.lemmas.length || made.between.length))
276
+ fnMade[n] = made;
277
+ }
278
+ let rule = r.withParts(Object.fromEntries(Object.entries(fnMade).map(([n, m]) => [n, m.d])));
279
+ const listKept = {};
280
+ for (const part of lists) {
281
+ const out = houdiniList(rule, part, listCands(part, params, listSamples(part, params)), ps);
282
+ if (out && out.kept.length) {
283
+ rule = out.rule;
284
+ listKept[part[0]] = out.kept;
285
+ }
286
+ }
287
+ const verdict = proveSteps(rule, laws, stepLaws, { search: false });
288
+ const found = () => ({
289
+ ...Object.fromEntries(Object.entries(fnMade).map(([n, m]) => { const args = [...(recurOf(m.d).def.args ?? [])]; return [n, { ...(m.lemmas.length ? { lemmas: m.lemmas.map(c => recurSrc(c, args, false)) } : {}), ...(m.between.length ? { between: m.between.map(c => recurSrc(c, args, true)) } : {}) }]; })),
290
+ ...Object.fromEntries(Object.entries(listKept).map(([n, cs]) => [n, { lemmas: cs.map(c => listSrc(c, Object.keys(lists.find(l => l[0] === n)[2]), ps, c.target)) }])),
291
+ });
292
+ if (!(verdict.proven && verdict.checked))
293
+ return { verdict, rule, found: found() };
294
+ // Only what the laws need: each lemma dropped in turn (the function or list made again without it, which fails when
295
+ // another lemma leaned on it), kept where the laws aren't proven without it.
296
+ let best = { rule, verdict };
297
+ // A rule made again and the laws proven of it; or the lemma whose proof failed without the one dropped.
298
+ const tryRule = (next) => {
299
+ try {
300
+ const r2 = next(), v = proveSteps(r2, laws, stepLaws, { search: false });
301
+ return v.proven && v.checked ? { rule: r2, verdict: v } : undefined;
302
+ }
303
+ catch (e) {
304
+ if (e instanceof Unproven)
305
+ return { leaned: e.verdict.goal ?? '' };
306
+ throw e;
307
+ }
308
+ };
309
+ const original = Object.fromEntries(fns);
310
+ const fnRecur = (n, m) => {
311
+ const src = recurOf(original[n]), args = [...(src.def.args ?? [])];
312
+ return recur(src.name, src.def, { ...src.lemmas, ...Object.fromEntries(m.lemmas.map(c => [c.name, (x, f, env) => condOf(c, recurAtoms(args, x, f, env))])) }, { ...src.between, ...Object.fromEntries(m.between.map(c => [c.name, (x, f, y, g, env) => condOf(c, recurAtoms(args, x, f, env, y, g))])) });
313
+ };
314
+ const build = () => r.withParts(Object.fromEntries(Object.entries(fnMade).map(([n, m]) => [n, fnRecur(n, m)])), Object.fromEntries(Object.entries(listKept).map(([n, cs]) => [n, Object.fromEntries(cs.map(c => [c.name, listLemma(c, ps)]))])));
315
+ for (const [n, m] of Object.entries(fnMade)) {
316
+ // A lemma dropped takes with it those that leaned on it, each named by the proof that failed without it.
317
+ for (const kind of ['between', 'lemmas'])
318
+ for (const c of [...m[kind]]) {
319
+ if (!m[kind].includes(c))
320
+ continue;
321
+ const before = { lemmas: m.lemmas, between: m.between };
322
+ m[kind] = m[kind].filter(x => x !== c);
323
+ for (;;) {
324
+ const out = tryRule(build);
325
+ if (out && 'rule' in out) {
326
+ best = out;
327
+ break;
328
+ }
329
+ const d = out ? [...m.lemmas, ...m.between].find(isGoalOf(out.leaned)) : undefined;
330
+ if (!d) {
331
+ m.lemmas = before.lemmas;
332
+ m.between = before.between;
333
+ break;
334
+ }
335
+ m.lemmas = m.lemmas.filter(x => x !== d);
336
+ m.between = m.between.filter(x => x !== d);
337
+ }
338
+ }
339
+ if (!m.lemmas.length && !m.between.length)
340
+ delete fnMade[n];
341
+ }
342
+ for (const [n, cs] of Object.entries(listKept)) {
343
+ for (const c of [...cs]) {
344
+ if (!listKept[n].includes(c))
345
+ continue;
346
+ const before = listKept[n];
347
+ listKept[n] = listKept[n].filter(x => x !== c);
348
+ for (;;) {
349
+ const out = tryRule(build);
350
+ if (out && 'rule' in out) {
351
+ best = out;
352
+ break;
353
+ }
354
+ const d = out ? listKept[n].find(isGoalOf(out.leaned)) : undefined;
355
+ if (!d) {
356
+ listKept[n] = before;
357
+ break;
358
+ }
359
+ listKept[n] = listKept[n].filter(x => x !== d);
360
+ }
361
+ }
362
+ if (!listKept[n].length)
363
+ delete listKept[n];
364
+ }
365
+ const again = tryRule(build), last = again && 'rule' in again ? again : best;
366
+ return { verdict: last.verdict, rule: last.rule, found: found() };
367
+ }
368
+ function foldSamples(f) {
369
+ const rand = seeded(11), none = { name: f.name, params: [], inputs: [], state: {}, step: {} }, st = { scalars: {}, maps: {} };
370
+ const out = [];
371
+ const draw = (names, ok, env) => {
372
+ for (let tries = 0; tries < 50; tries++) {
373
+ const e = { ...env, ...Object.fromEntries(names.map(n => [n, rand(2) ? rand(2) : rand(13)])) };
374
+ if (!ok || evalC(ok, e, st, none))
375
+ return e;
376
+ }
377
+ return undefined;
378
+ };
379
+ for (let trial = 0; trial < 300; trial++) {
380
+ const ps = draw(f.params, f.pre, {});
381
+ if (!ps)
382
+ continue;
383
+ const tree = (depth) => {
384
+ const own = draw(f.inputs, f.assume, ps);
385
+ if (!own)
386
+ return undefined;
387
+ if (depth === 0 || rand(4) === 0)
388
+ return Object.fromEntries(Object.entries(f.leaf).map(([n, e]) => [n, evalE(e, own, st, none)]));
389
+ const l = tree(depth - 1), r = tree(depth - 1);
390
+ if (!l || !r)
391
+ return undefined;
392
+ const env = { ...own, ...Object.fromEntries(Object.entries(l).map(([n, x]) => [`l_${n}`, x])), ...Object.fromEntries(Object.entries(r).map(([n, x]) => [`r_${n}`, x])) };
393
+ const vals = Object.fromEntries(Object.entries(f.node).map(([n, e]) => [n, evalE(e, env, st, none)]));
394
+ out.push({ ...ps, ...vals });
395
+ return vals;
396
+ };
397
+ const top = tree(1 + rand(6));
398
+ if (top)
399
+ out.push({ ...ps, ...top });
400
+ }
401
+ return out.filter(s => Object.values(s).every(x => Number.isSafeInteger(x)));
402
+ }
403
+ const eOfSide = (side) => {
404
+ const es = side.map(([a, c]) => { const t = a === '1' ? n(c) : a.includes('*') ? prod(x(a.split('*')[0]), x(a.split('*')[1])) : x(a); return a === '1' || c === 1 ? t : times(c, t); });
405
+ return es.length ? es.slice(1).reduce((acc, t) => plus(acc, t), es[0]) : n(0);
406
+ };
407
+ const cOfCand = (c) => { const { pos, neg } = sides(c.lin), a = eOfSide(pos), b = eOfSide(neg); return c.rel === '=' ? same(a, b) : atMost(a, b); };
408
+ export function discoverFold(f, laws) {
409
+ const stated = f.lemmas ?? [], first = proveFold({ fold: f.fold, lemmas: stated }, laws);
410
+ if (first.proven && first.checked)
411
+ return { verdict: first, lemmas: [], found: [] };
412
+ const fold = f.fold, names = Object.keys(fold.leaf), rose = names.includes('kind$'), samples = foldSamples(fold);
413
+ // The values a law reads (a tree's own, or under .children the running values of the children), each group with the
414
+ // samples it is claimed of and the guard it is claimed under.
415
+ const groups = rose
416
+ ? [{ atoms: names.filter(n => !n.startsWith('kids$') && n !== 'kind$'), at: 0 }, { atoms: names.filter(n => n.startsWith('kids$')), at: 1 }]
417
+ : [{ atoms: names, at: undefined }];
418
+ const cands = [];
419
+ for (const g of groups) {
420
+ const here = samples.filter(s => g.at === undefined || s['kind$'] === g.at).map(s => ({ ...s, ...Object.fromEntries(g.atoms.flatMap(a => fold.params.map(p => [`${a}*${p}`, s[a] * s[p]]))) }));
421
+ if (!here.length)
422
+ continue;
423
+ for (const t of g.atoms) {
424
+ const others = [...g.atoms.filter(a => a !== t), ...fold.params];
425
+ const forms = [{}, ...others.map(o => ({ [o]: 1 })), ...others.flatMap((o, j) => others.slice(j + 1).map(q => ({ [o]: 1, [q]: 1 }))), ...g.atoms.filter(a => a !== t).flatMap(a => fold.params.map(p => ({ [`${a}*${p}`]: 1 })))];
426
+ const show = (a) => a.replace(/kids\$/g, '').replace('*', '·');
427
+ // A lemma about the children's running values goes by "the children's ...", apart from the laws about a tree's.
428
+ for (const c of linearCands(t, forms, here, show))
429
+ cands.push({ cand: g.at === 1 ? { ...c, name: `the children's ${c.name}` } : c, ...(g.at === undefined ? {} : { at: g.at }) });
430
+ }
431
+ }
432
+ const lawOf = ({ cand, at }) => ({ name: cand.name, holds: at === undefined ? cOfCand(cand) : any(at === 0 ? atMost(n(1), x('kind$')) : same(x('kind$'), n(0)), cOfCand(cand)) });
433
+ // Houdini: every candidate beside the laws; a candidate whose goal fails is dropped, until the rest are proven.
434
+ let kept = dedupeBy(cands, c => `${c.at}:${c.cand.name}`);
435
+ const prove = (ks) => proveFold({ fold, lemmas: [...stated, ...ks.map(lawOf)] }, laws);
436
+ let verdict = prove(kept);
437
+ for (let round = 0; round <= cands.length && !(verdict.proven && verdict.checked); round++) {
438
+ const bad = kept.find(c => isGoalOf(verdict.goal ?? '')(c.cand));
439
+ if (!bad)
440
+ break;
441
+ // The kernel's witness is a counterexample to induction: the leaf or node it names, from children that meet every
442
+ // law kept. Each candidate its values break goes with the one the goal named.
443
+ const broken = new Set([bad, ...brokenAt(fold, verdict, kept)]);
444
+ kept = kept.filter(c => !broken.has(c));
445
+ verdict = prove(kept);
446
+ }
447
+ if (!(verdict.proven && verdict.checked))
448
+ return { verdict, lemmas: [], found: [] };
449
+ // Only what the laws need.
450
+ for (const c of [...kept]) {
451
+ const v = prove(kept.filter(k => k !== c));
452
+ if (v.proven && v.checked) {
453
+ kept = kept.filter(k => k !== c);
454
+ verdict = v;
455
+ }
456
+ }
457
+ const src = ({ cand, at }) => {
458
+ const reads = Object.keys(cand.lin).flatMap(a => a.split('*')).filter(a => a !== '1'), own = reads.filter(a => !fold.params.includes(a)), ps = reads.filter(a => fold.params.includes(a));
459
+ const body = srcOf(cand, a => (a.includes('*') ? `${a.split('*')[0]}.mul(${a.split('*')[1]})` : a).replace(/kids\$/g, ''), cand.target);
460
+ if (at === 1)
461
+ return `${quoted(cand.name)}: ({${own.map(a => a.replace('kids$', '')).join(', ')}}${ps.length ? `, {${ps.join(', ')}}` : ''}) => ${body}`;
462
+ return `f.law(${quoted(cand.name)}, ({${[...own, ...ps].join(', ')}}) => ${body})`;
463
+ };
464
+ return { verdict, lemmas: kept.map(lawOf), found: kept.map(src) };
465
+ }
466
+ function brokenAt(fold, v, kept) {
467
+ const w = v.witness, where = /@(leaf|node)$/.exec(v.goal ?? '')?.[1];
468
+ // A witness over products (each a variable of its own to the linear refutation) isn't a point of the fold.
469
+ if (!w || !where || Object.keys(w).some(k => k.includes('*')))
470
+ return [];
471
+ const none = { name: fold.name, params: [], inputs: [], state: {}, step: {} }, st = { scalars: {}, maps: {} };
472
+ const withProducts = (c, at) => ({ ...at, ...Object.fromEntries(Object.keys(c.lin).filter(a => a.includes('*')).map(a => [a, at[a.split('*')[0]] * at[a.split('*')[1]]])) });
473
+ const meets = (c, at) => (c.at !== undefined && at['kind$'] !== c.at) || holds(c.cand, withProducts(c.cand, at));
474
+ try {
475
+ const env = { ...Object.fromEntries([...fold.params, ...fold.inputs, ...Object.keys(fold.leaf).flatMap(k => [`l_${k}`, `r_${k}`])].map(k => [k, 0])), ...w };
476
+ if ((fold.pre && !evalC(fold.pre, env, st, none)) || (fold.assume && !evalC(fold.assume, env, st, none)))
477
+ return [];
478
+ const side = (p) => ({ ...Object.fromEntries(fold.params.map(k => [k, env[k]])), ...Object.fromEntries(Object.keys(fold.leaf).map(k => [k, env[`${p}${k}`]])) });
479
+ // The children must meet what is kept, or the point isn't one the induction step starts from.
480
+ if (where === 'node' && !kept.every(c => meets(c, side('l_')) && meets(c, side('r_'))))
481
+ return [];
482
+ const vals = Object.fromEntries(Object.entries(where === 'leaf' ? fold.leaf : fold.node).map(([k, e]) => [k, evalE(e, env, st, none)]));
483
+ const at = { ...Object.fromEntries(fold.params.map(k => [k, env[k]])), ...vals };
484
+ return kept.filter(c => !meets(c, at));
485
+ }
486
+ catch {
487
+ return [];
488
+ }
489
+ }
490
+ const quoted = (name) => (name.includes("'") ? JSON.stringify(name) : `'${name}'`);
491
+ const dedupeBy = (xs, key) => [...new Map(xs.map(x => [key(x), x])).values()];