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 +37 -13
- package/dist/core/chart.js +4 -4
- package/dist/core/discover.d.ts +27 -0
- package/dist/core/discover.js +491 -0
- package/dist/core/dsl.d.ts +67 -4
- package/dist/core/dsl.js +157 -17
- package/dist/core/infer.d.ts +29 -0
- package/dist/core/infer.js +681 -0
- package/dist/kernel/kernel.d.ts +5 -1
- package/dist/kernel/kernel.js +44 -12
- package/dist/kernel/prove.d.ts +8 -0
- package/dist/kernel/prove.js +173 -15
- package/dist/lean-kernel/emit.d.ts +2 -1
- package/dist/lean-kernel/emit.js +37 -23
- package/dist/lean-kernel/library.js +1 -1
- package/package.json +10 -2
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,
|
|
94
|
-
|
|
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
|
|
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
|
|
280
|
-
`max`, `when(cond, a, b)`, `count(cond)`;
|
|
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`),
|
|
382
|
-
|
|
383
|
-
`gdp-wired/
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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
|
package/dist/core/chart.js
CHANGED
|
@@ -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()];
|