gdp-wired 0.16.0 → 0.18.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 +10 -2
- package/dist/core/cycles.d.ts +73 -0
- package/dist/core/cycles.js +899 -0
- package/dist/core/cyclic.d.ts +67 -0
- package/dist/core/cyclic.js +740 -0
- package/dist/core/data.d.ts +258 -0
- package/dist/core/data.js +1858 -0
- package/dist/core/discover.js +7 -2
- package/dist/core/dsl.d.ts +7 -1
- package/dist/core/dsl.js +39 -6
- package/dist/core/infer.js +5 -1
- package/dist/kernel/cyclic.d.ts +45 -0
- package/dist/kernel/cyclic.js +54 -0
- package/dist/kernel/prove.d.ts +15 -0
- package/dist/kernel/prove.js +485 -5
- package/dist/lean-kernel/emit.d.ts +73 -0
- package/dist/lean-kernel/emit.js +250 -26
- package/dist/lean-kernel/library.js +3 -0
- package/package.json +14 -2
package/dist/kernel/prove.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { add, k, refuteFast, scale, solve, sub, v } from "../core/solver.js";
|
|
2
|
-
import {
|
|
2
|
+
import { checkRefutation } from "../core/check.js";
|
|
3
|
+
import { crossCyclic, cyclicMachine, cyclicOn, machineNodes, proveCyclic } from "../core/cyclic.js";
|
|
4
|
+
import { assumed, atomOf, boundOf, canon, canonFlip, columnTerm, decided, factorsOf, isCell, isMonomial, stepZone, check, checkLaws, checkStepLaws, historyMachine, leafConstraints, leafConstraintsReusing, reusing, linear, livenessHelpful, livenessParts, obligations, plainKey, progressObligations, stepLawObligations, trueC, truth, unreachable, witnessesOf, withProducts } from "./kernel.js";
|
|
3
5
|
// The atoms the path leaves open, in evaluation order: deciding any of them makes progress.
|
|
4
6
|
function openE(e, path, out) {
|
|
5
7
|
switch (e.op) {
|
|
@@ -76,7 +78,10 @@ function openH(c, path, out, positive = true) {
|
|
|
76
78
|
if (out.length === before && !positive && c.op === '=' && truth(c, path) === undefined)
|
|
77
79
|
openC(c, path, out);
|
|
78
80
|
}
|
|
79
|
-
// Pairs of reads of one map whose keys the path doesn't yet compare.
|
|
81
|
+
// Pairs of reads of one map whose keys the path doesn't yet compare. With `pairedKeys.skip` (core/data.ts), reads at
|
|
82
|
+
// keys a pairing builds ((a + b)² + b, keys whose difference has monomials) are left uncompared: that encoding states
|
|
83
|
+
// their congruence as facts, and comparing their keys only brings products into leaves its linear rows decide.
|
|
84
|
+
export const pairedKeys = { skip: false };
|
|
80
85
|
function openCells(rows, path) {
|
|
81
86
|
const names = [...new Set(rows.flatMap(r => Object.keys(r.lin)))].filter(isCell);
|
|
82
87
|
const out = [];
|
|
@@ -86,7 +91,27 @@ function openCells(rows, path) {
|
|
|
86
91
|
if (p.slice(0, p.indexOf('[')) !== q.slice(0, q.indexOf('[')))
|
|
87
92
|
continue;
|
|
88
93
|
const d = sub(parse(keyText(p)), parse(keyText(q)));
|
|
94
|
+
if (pairedKeys.skip && Object.keys(d).some(isMonomial))
|
|
95
|
+
continue;
|
|
89
96
|
const dec = (lin) => path.get(canon(lin))?.holds ?? (path.has(canonFlip(lin)) ? !path.get(canonFlip(lin)).holds : undefined);
|
|
97
|
+
// Two cells of a run's map (core/cyclic.ts: time u and key k at u + k·Ω) at keys k·Ω and k'·Ω: k = k' first, a
|
|
98
|
+
// linear atom; the keys' own difference only where k = k' (its other side then refuted by (k − k')·Ω = 0, a product).
|
|
99
|
+
const q0 = cellProducts.on ? quotient(d) : undefined;
|
|
100
|
+
if (q0) {
|
|
101
|
+
const qa = dec(q0), qb = dec(scale(q0, -1));
|
|
102
|
+
if (qa === undefined) {
|
|
103
|
+
out.push(q0);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (qa === true && qb === undefined) {
|
|
107
|
+
out.push(scale(q0, -1));
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (!(qa && qb))
|
|
111
|
+
continue;
|
|
112
|
+
cellDiffs.set(canon(d), q0);
|
|
113
|
+
cellDiffs.set(canon(scale(d, -1)), scale(q0, -1));
|
|
114
|
+
}
|
|
90
115
|
const a = dec(d), b = dec(scale(d, -1));
|
|
91
116
|
if (a === undefined)
|
|
92
117
|
out.push(d);
|
|
@@ -95,6 +120,24 @@ function openCells(rows, path) {
|
|
|
95
120
|
}
|
|
96
121
|
return out;
|
|
97
122
|
}
|
|
123
|
+
// A key difference q·F over monomials that share one factor F (Ω), with no other part: q, else undefined.
|
|
124
|
+
const quotient = (d) => {
|
|
125
|
+
const keys = Object.keys(d);
|
|
126
|
+
if (!keys.length || keys.some(key => key === '1' || !isMonomial(key)))
|
|
127
|
+
return undefined;
|
|
128
|
+
const shared = factorsOf(keys[0]).find(f => keys.every(key => factorsOf(key).includes(f)));
|
|
129
|
+
if (!shared)
|
|
130
|
+
return undefined;
|
|
131
|
+
const out = {};
|
|
132
|
+
for (const key of keys) {
|
|
133
|
+
const fs = [...factorsOf(key)];
|
|
134
|
+
fs.splice(fs.indexOf(shared), 1);
|
|
135
|
+
out[fs.join('*')] = (out[fs.join('*')] ?? 0) + d[key];
|
|
136
|
+
}
|
|
137
|
+
return out;
|
|
138
|
+
};
|
|
139
|
+
// The cells' key differences split at their quotient's equality, by canonical text: their quotient.
|
|
140
|
+
const cellDiffs = new Map();
|
|
98
141
|
function parse(text) {
|
|
99
142
|
const out = {};
|
|
100
143
|
let depth = 0, start = 0;
|
|
@@ -138,10 +181,45 @@ const solveP = (rows, maxRows) => {
|
|
|
138
181
|
return out;
|
|
139
182
|
};
|
|
140
183
|
// PROVE_TRACE=1 logs each goal grown from its root (hypotheses, nodes, time, outcome) to stderr.
|
|
184
|
+
// PROVE_DUMP: a hook research/perf/prof sets to read each root goal.
|
|
185
|
+
export let proveDump;
|
|
186
|
+
export const setProveDump = (f) => { proveDump = f; };
|
|
141
187
|
const proveTrace = !!globalThis.process?.env?.PROVE_TRACE;
|
|
188
|
+
// `why`, when the budget ran out: which limit, at what size, and what usually helps.
|
|
189
|
+
// Set while core/cyclic.ts grows its goals.
|
|
190
|
+
export const cellProducts = { on: false };
|
|
142
191
|
// The budget: 200,000 nodes and, by default, a second per hypothesis (at least 20 seconds) per goal; past either the goal
|
|
143
192
|
// is left `unknown`. A goal with many instances (a law about two keys at its pairs) has that many more rows per leaf.
|
|
193
|
+
// A deadline every goal grown inside `withDeadline` keeps, beside its own budget: a search that proves many things (core/
|
|
194
|
+
// infer.ts, core/discover.ts, the search proveRule falls back on) bounds them all; a goal past it is left `unknown`.
|
|
195
|
+
let deadline = Infinity;
|
|
196
|
+
export function withDeadline(until, f) {
|
|
197
|
+
const outer = deadline;
|
|
198
|
+
deadline = Math.min(outer, until);
|
|
199
|
+
try {
|
|
200
|
+
return f();
|
|
201
|
+
}
|
|
202
|
+
finally {
|
|
203
|
+
deadline = outer;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
// `units: false`: the goal's own order, no units first (a caller after a model, not a proof: a run guided by the
|
|
207
|
+
// prover's witnesses, say, whose models units would steer).
|
|
144
208
|
export function grow(goal, path = new Map(), budget = { nodes: 0, until: performance.now() + Math.max(20_000, 1_000 * goal.hyps.length) }) {
|
|
209
|
+
if (deadline < (budget.until ?? Infinity))
|
|
210
|
+
budget.until = deadline;
|
|
211
|
+
if (budget.units === false && unitsNow) {
|
|
212
|
+
unitsNow = false;
|
|
213
|
+
try {
|
|
214
|
+
return grow(goal, path, budget);
|
|
215
|
+
}
|
|
216
|
+
finally {
|
|
217
|
+
unitsNow = true;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (proveDump && !path.size && !budget.nodes) {
|
|
221
|
+
proveDump(goal);
|
|
222
|
+
}
|
|
145
223
|
if (proveTrace && !path.size && !budget.nodes) {
|
|
146
224
|
const t = performance.now(), out = growRoot(goal, path, budget);
|
|
147
225
|
console.error(`[grow] ${goal.name}: ${goal.hyps.length} hyps, ${budget.nodes} nodes, ${Math.round(performance.now() - t)} ms, ${'tree' in out ? 'proven' : `${out.stuck}${out.why ? ` (${out.why})` : ''}`}`);
|
|
@@ -157,6 +235,18 @@ export function grow(goal, path = new Map(), budget = { nodes: 0, until: perform
|
|
|
157
235
|
function growRoot(goal, path, budget) {
|
|
158
236
|
// At the root: plainly first; where the cases multiply past a budget, through cuts. The plain budget halves with each
|
|
159
237
|
// two open choices (what a plain search's cases grow by), so a long run's steps reach the cuts early.
|
|
238
|
+
if (path.size === 0 && budget.nodes === 0 && unitFirst && unitsNow) {
|
|
239
|
+
// A goal that a few cases settle needs no units (finding them costs a Farkas check per atom); the rest get them.
|
|
240
|
+
unitsNow = false;
|
|
241
|
+
try {
|
|
242
|
+
const quick = growPlain(goal, path, { nodes: 0, until: budget.until, cap: 64 });
|
|
243
|
+
if ('tree' in quick || quick.stuck === 'sat' || quick.why === undefined)
|
|
244
|
+
return quick;
|
|
245
|
+
}
|
|
246
|
+
finally {
|
|
247
|
+
unitsNow = true;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
160
250
|
if (path.size === 0 && budget.nodes === 0) {
|
|
161
251
|
const open = new Set(leafConstraints(goal, path).flatMap(r => Object.keys(r.lin)).filter(n => n.startsWith('⟦') && columnTerm(n)?.op === 'if')).size;
|
|
162
252
|
const plain = growPlain(goal, path, { nodes: 0, until: budget.until, cap: Math.max(32, Math.round(4_000 / 2 ** (open / 2))) });
|
|
@@ -177,7 +267,7 @@ function growPlain(goal, path, budget) {
|
|
|
177
267
|
return { stuck: 'unknown', path, why: outOfBudget(goal, budget.nodes > 200_000 ? 'nodes' : 'time', path.size) };
|
|
178
268
|
// Rows that read a term column only once a cut has put one on the path; before, the rows a choice-free search uses
|
|
179
269
|
// (what it saw before term columns), renumbered to the whole leaf's when a leaf is found.
|
|
180
|
-
const full = timed(() =>
|
|
270
|
+
const full = timed(() => rowsAt(goal, path), ms => { profile.rowsMs += ms; });
|
|
181
271
|
const columns = [...path.values()].some(a => Object.keys(a.lin).some(key => key.startsWith('⟦')));
|
|
182
272
|
const idx = full.flatMap((r, i) => (columns || !Object.keys(r.lin).some(key => key.startsWith('⟦')) ? [i] : []));
|
|
183
273
|
const rows = idx.map(i => full[i]), at = (i) => (i < rows.length ? idx[i] : full.length + i - rows.length);
|
|
@@ -196,6 +286,8 @@ function growPlain(goal, path, budget) {
|
|
|
196
286
|
byHyp.set(h, (seen = {})); return reusing(seen, path, p => { const out = []; openH(h, p, out); return out; }); };
|
|
197
287
|
// Then the hypotheses about only the names the conclusion reads (an assumption about another key, a witness, can
|
|
198
288
|
// wait: a refutation seldom needs it), then the rest, each group in the goal's order.
|
|
289
|
+
if (unitFirst && unitsNow)
|
|
290
|
+
open.push(...unitsOf(goal));
|
|
199
291
|
if (last >= 0)
|
|
200
292
|
open.push(...openOf(hyps[last]));
|
|
201
293
|
const near = nearOf(goal);
|
|
@@ -228,6 +320,21 @@ function growPlain(goal, path, budget) {
|
|
|
228
320
|
return { tree: { leaf: fast.steps }, nodes: budget.nodes };
|
|
229
321
|
}
|
|
230
322
|
let split = next ?? (verdict.kind === 'unknown' ? bisect(rows, path) ?? fractional(verdict.point, path) : undefined);
|
|
323
|
+
// A cell difference's failing side (k = k' on the path, the keys apart): (k − k')·Ω = 0 refutes it, a product of each
|
|
324
|
+
// of the path's two atoms k − k' <= 0 and k' − k <= 0 with Ω >= 0.
|
|
325
|
+
if (cellProducts.on && path.size) {
|
|
326
|
+
const [id, last] = [...path].at(-1), q0 = !last.holds ? cellDiffs.get(id) : undefined;
|
|
327
|
+
if (q0) {
|
|
328
|
+
const at = (lin) => rows.findIndex(r => canon(r.lin) === canon(lin));
|
|
329
|
+
const d = last.lin, f = factorsOf(Object.keys(d)[0]).find(x => !Object.keys(q0).includes(x) && Object.keys(d).every(key => factorsOf(key).includes(x)));
|
|
330
|
+
const e1 = at(q0), e2 = at(scale(q0, -1)), sign = f === undefined ? -1 : at({ [f]: -1 });
|
|
331
|
+
if (e1 >= 0 && e2 >= 0 && sign >= 0) {
|
|
332
|
+
const pairs = [[e1, sign], [e2, sign]], out = solve(withProducts(rows, pairs));
|
|
333
|
+
if (out.kind === 'refuted')
|
|
334
|
+
return { tree: leafOf(out.steps, pairs), nodes: budget.nodes };
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
231
338
|
// Products: with monomials about, try every pair of rows multiplied; failing that, split a factor at zero.
|
|
232
339
|
const monomials = rows.some(r => Object.keys(r.lin).some(isMonomial));
|
|
233
340
|
// A witness that multiplies out (each monomial's value is its factors' product) is a real model: nothing to refute.
|
|
@@ -237,6 +344,30 @@ function growPlain(goal, path, budget) {
|
|
|
237
344
|
if (real)
|
|
238
345
|
return { stuck: 'sat', witness: w, path };
|
|
239
346
|
}
|
|
347
|
+
// An equation about a factor (x' = x + 1, a run's next state as a cell) times each factor's sign: e·f = 0 for every
|
|
348
|
+
// factor f, so a monomial of x' is one of x (x'·x' = (x + 1)²). Whatever the number of rows: there are few such pairs.
|
|
349
|
+
// Only for cyclic proofs (core/cyclic.ts, whose runs keep each next state as a cell): the usual rules substitute it.
|
|
350
|
+
if (!split && monomials && cellProducts.on) {
|
|
351
|
+
const factorSet = new Set(rows.flatMap(r => Object.keys(r.lin).filter(isMonomial).flatMap(factorsOf)));
|
|
352
|
+
const byCanon = new Map(rows.map((r, i) => [canon(r.lin), i]));
|
|
353
|
+
const eqs = rows.flatMap((r, i) => (Object.keys(r.lin).some(key => key !== '1' && factorSet.has(key)) && byCanon.has(canon(scale(r.lin, -1))) ? [i] : []));
|
|
354
|
+
const signs = rows.flatMap((r, i) => { const keys = Object.keys(r.lin); return keys.length === 1 && factorSet.has(keys[0]) && r.lin[keys[0]] < 0 ? [i] : []; });
|
|
355
|
+
const pairs = eqs.flatMap(i => signs.map(j => [i, j])).slice(0, 200);
|
|
356
|
+
if (pairs.length) {
|
|
357
|
+
const sub = solve(withProducts(rows, pairs));
|
|
358
|
+
if (sub.kind === 'refuted')
|
|
359
|
+
return { tree: leafOf(sub.steps, pairs), nodes: budget.nodes };
|
|
360
|
+
// Then every linear row about a factor (n >= x: (n − x)·d >= 0) times each sign, past the forty rows the pairs
|
|
361
|
+
// below are kept to.
|
|
362
|
+
const about = rows.flatMap((r, i) => (!eqs.includes(i) && Object.keys(r.lin).some(key => key !== '1' && factorSet.has(key)) && !Object.keys(r.lin).some(isMonomial) ? [i] : []));
|
|
363
|
+
const more = [...pairs, ...about.flatMap(i => signs.map(j => [i, j]))].slice(0, 400);
|
|
364
|
+
if (more.length > pairs.length) {
|
|
365
|
+
const ineq = solve(withProducts(rows, more));
|
|
366
|
+
if (ineq.kind === 'refuted')
|
|
367
|
+
return { tree: leafOf(ineq.steps, more), nodes: budget.nodes };
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
240
371
|
if (!split && monomials && rows.length <= 40) {
|
|
241
372
|
// Only rows about the factors (no other map cells), each pair once: elimination over hundreds of products blows up.
|
|
242
373
|
const factorSet = new Set(rows.flatMap(r => Object.keys(r.lin).filter(isMonomial).flatMap(factorsOf)));
|
|
@@ -426,6 +557,24 @@ function squareForms(rows) {
|
|
|
426
557
|
// the decision's own row, and any row only it makes, are not there), every split kept. The derivations are the same
|
|
427
558
|
// arithmetic over the same rows, and the kernel checks the whole tree in the end. Undefined at the first leaf that
|
|
428
559
|
// needs the decision.
|
|
560
|
+
// A leaf's rows, by its path's decisions in order (what they are a function of): a lift reads every leaf of a subtree
|
|
561
|
+
// at its own path, found when it was grown, and at the path without the split, which the next lift up reads again.
|
|
562
|
+
const rowsSeen = new WeakMap();
|
|
563
|
+
function rowsAt(goal, path) {
|
|
564
|
+
let byPath = rowsSeen.get(goal);
|
|
565
|
+
if (!byPath)
|
|
566
|
+
rowsSeen.set(goal, (byPath = new Map()));
|
|
567
|
+
let id = '';
|
|
568
|
+
for (const [key, a] of path)
|
|
569
|
+
id += `${a.holds ? '+' : '-'}${key}|`;
|
|
570
|
+
let rows = byPath.get(id);
|
|
571
|
+
if (!rows) {
|
|
572
|
+
if (byPath.size >= 4096)
|
|
573
|
+
byPath.clear();
|
|
574
|
+
byPath.set(id, (rows = leafConstraintsReusing(goal, path)));
|
|
575
|
+
}
|
|
576
|
+
return rows;
|
|
577
|
+
}
|
|
429
578
|
function lift(goal, tree, from, to) {
|
|
430
579
|
const own = to.size; // the decision's row: the last of `from`'s path rows, below every later split's
|
|
431
580
|
const reads = (t) => ('leaf' in t ? t.leaf.some(st => st.kind === 'input' && st.index === own) || (t.products ?? []).some(([i, j]) => i === own || j === own) : reads(t.yes) || reads(t.no));
|
|
@@ -440,7 +589,7 @@ function lift(goal, tree, from, to) {
|
|
|
440
589
|
const no = yes && go(t.no, new Map(a).set(sid, { lin: t.split, holds: false }), new Map(b).set(sid, { lin: t.split, holds: false }));
|
|
441
590
|
return yes && no ? { split: t.split, yes, no } : undefined;
|
|
442
591
|
}
|
|
443
|
-
const ra =
|
|
592
|
+
const ra = rowsAt(goal, a), rb = rowsAt(goal, b), at = new Map();
|
|
444
593
|
rb.forEach((r, i) => { const key = canon(r.lin); if (!at.has(key))
|
|
445
594
|
at.set(key, i); });
|
|
446
595
|
const map = (i) => (i < ra.length ? at.get(canon(ra[i].lin)) : undefined);
|
|
@@ -466,6 +615,86 @@ function lift(goal, tree, from, to) {
|
|
|
466
615
|
};
|
|
467
616
|
return go(tree, from, to);
|
|
468
617
|
}
|
|
618
|
+
// A tree for a weaker goal (some hypotheses left out, some conjuncts of one left out) made a tree for the whole: at any
|
|
619
|
+
// path a leaf's rows are among the whole goal's (each hypothesis' facts are a hypothesis' own, the cells, terms and
|
|
620
|
+
// naturals they read a subset), so each input is renumbered to the same row there, a square or product past the rows
|
|
621
|
+
// moved by the difference. Where a dropped conjunct makes the whole hypothesis false under the path, its row 1 <= 0
|
|
622
|
+
// refutes the leaf alone; a leaf neither maps is grown again on the whole goal.
|
|
623
|
+
// `opaque`: variables the weaker goal has for terms of the whole (a step's value, read the same in two hypotheses), each
|
|
624
|
+
// read as the term's linear form at the whole goal's path; a split the whole's path already decides is followed, not
|
|
625
|
+
// made again.
|
|
626
|
+
export function widen(part, whole, tree, opaque = {}, pp = new Map(), wp = pp) {
|
|
627
|
+
const names = Object.keys(opaque);
|
|
628
|
+
const rename = (lin) => {
|
|
629
|
+
if (!names.length)
|
|
630
|
+
return lin;
|
|
631
|
+
let out = {};
|
|
632
|
+
for (const [key, c] of Object.entries(lin)) {
|
|
633
|
+
const e = opaque[key];
|
|
634
|
+
if (e) {
|
|
635
|
+
const l = linear(e, wp);
|
|
636
|
+
if (!l)
|
|
637
|
+
return undefined;
|
|
638
|
+
out = add(out, scale(l, c));
|
|
639
|
+
}
|
|
640
|
+
else if (names.some(n => key.includes(n)))
|
|
641
|
+
return undefined;
|
|
642
|
+
else
|
|
643
|
+
out = add(out, { [key]: c });
|
|
644
|
+
}
|
|
645
|
+
return out;
|
|
646
|
+
};
|
|
647
|
+
if (!('leaf' in tree)) {
|
|
648
|
+
const id = canon(tree.split), wl = rename(tree.split);
|
|
649
|
+
if (!wl) {
|
|
650
|
+
const o = grow(whole, wp);
|
|
651
|
+
return 'tree' in o ? o.tree : undefined;
|
|
652
|
+
}
|
|
653
|
+
const wid = canon(wl), known = Object.keys(wl).every(key => key === '1') ? (wl['1'] ?? 0) <= 0 : wp.has(wid) ? wp.get(wid).holds : wp.has(canonFlip(wl)) ? !wp.get(canonFlip(wl)).holds : undefined;
|
|
654
|
+
if (known !== undefined)
|
|
655
|
+
return widen(part, whole, known ? tree.yes : tree.no, opaque, new Map(pp).set(id, { lin: tree.split, holds: known }), wp);
|
|
656
|
+
const yes = widen(part, whole, tree.yes, opaque, new Map(pp).set(id, { lin: tree.split, holds: true }), new Map(wp).set(wid, { lin: wl, holds: true }));
|
|
657
|
+
const no = yes && widen(part, whole, tree.no, opaque, new Map(pp).set(id, { lin: tree.split, holds: false }), new Map(wp).set(wid, { lin: wl, holds: false }));
|
|
658
|
+
return yes && no ? { split: wl, yes, no } : undefined;
|
|
659
|
+
}
|
|
660
|
+
const rp = rowsAt(part, pp), rw = rowsAt(whole, wp), at = new Map();
|
|
661
|
+
rw.forEach((r, i) => { const key = canon(r.lin); if (!at.has(key))
|
|
662
|
+
at.set(key, i); });
|
|
663
|
+
const map = (i) => { if (i >= rp.length)
|
|
664
|
+
return i - rp.length + rw.length; const l = rename(rp[i].lin); return l && at.get(canon(l)); };
|
|
665
|
+
const leaf = [], products = [], squares = (tree.squares ?? []).map(rename);
|
|
666
|
+
let ok = squares.every(Boolean);
|
|
667
|
+
for (const st of ok ? tree.leaf : []) {
|
|
668
|
+
if (st.kind !== 'input') {
|
|
669
|
+
leaf.push(st);
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
672
|
+
const j = map(st.index);
|
|
673
|
+
if (j === undefined) {
|
|
674
|
+
ok = false;
|
|
675
|
+
break;
|
|
676
|
+
}
|
|
677
|
+
leaf.push({ ...st, index: j });
|
|
678
|
+
}
|
|
679
|
+
for (const [i, j] of ok ? tree.products ?? [] : []) {
|
|
680
|
+
const pi = map(i), pj = map(j);
|
|
681
|
+
if (pi === undefined || pj === undefined) {
|
|
682
|
+
ok = false;
|
|
683
|
+
break;
|
|
684
|
+
}
|
|
685
|
+
products.push([pi, pj]);
|
|
686
|
+
}
|
|
687
|
+
// (renamed, a derivation through products or cuts is replayed: a cut's divisor may not divide the terms' coefficients)
|
|
688
|
+
if (ok && names.length && !checkRefutation(withProducts(rw, products, squares), leaf))
|
|
689
|
+
ok = false;
|
|
690
|
+
if (ok)
|
|
691
|
+
return { leaf, ...(tree.products ? { products } : {}), ...(tree.squares ? { squares: squares } : {}) };
|
|
692
|
+
const absurd = rw.findIndex(r => Object.keys(r.lin).every(key => key === '1') && (r.lin['1'] ?? 0) > 0);
|
|
693
|
+
if (absurd >= 0)
|
|
694
|
+
return { leaf: [{ kind: 'input', index: absurd }] };
|
|
695
|
+
const out = grow(whole, wp);
|
|
696
|
+
return 'tree' in out ? out.tree : undefined;
|
|
697
|
+
}
|
|
469
698
|
// Whether each hypothesis reads only names the goal's conclusion (its last hypothesis) reads.
|
|
470
699
|
const nearSeen = new WeakMap();
|
|
471
700
|
const namesOf = (t) => (t === undefined ? [] : [...JSON.stringify(t).matchAll(/"(?:name|map)":"([^"]+)"/g)].map(m => m[1]));
|
|
@@ -477,6 +706,51 @@ function nearOf(goal) {
|
|
|
477
706
|
}
|
|
478
707
|
return out;
|
|
479
708
|
}
|
|
709
|
+
// Units first: an atom some hypothesis splits on whose value the root's rows already force (a guard `?f = 0` where
|
|
710
|
+
// `?f = 1` holds, `k < len` where it is given). Split at the top, its forced side refutes at once; split late, under
|
|
711
|
+
// every other case, it is split again in each.
|
|
712
|
+
// GDP_UNITS=0 leaves them in the goal's order (for comparing).
|
|
713
|
+
const unitFirst = globalThis.process?.env?.GDP_UNITS !== '0';
|
|
714
|
+
let unitsNow = true;
|
|
715
|
+
const unitSeen = new WeakMap();
|
|
716
|
+
function unitsOf(goal) {
|
|
717
|
+
let out = unitSeen.get(goal);
|
|
718
|
+
if (out)
|
|
719
|
+
return out;
|
|
720
|
+
// Forced by the root's rows without a solver: a row the atom is (or is the negation of), or the bounds the rows give
|
|
721
|
+
// single variables (x <= c, x >= c) deciding it whichever way its variables go.
|
|
722
|
+
const root = new Map(), rows = leafConstraints(goal, root), seen = new Set(), given = new Set();
|
|
723
|
+
const lo = new Map(), hi = new Map();
|
|
724
|
+
for (const r of rows) {
|
|
725
|
+
given.add(canon(r.lin));
|
|
726
|
+
const vs = Object.keys(r.lin).filter(key => key !== '1');
|
|
727
|
+
if (vs.length !== 1)
|
|
728
|
+
continue;
|
|
729
|
+
const x = vs[0], c = r.lin[x], k = r.lin['1'] ?? 0, bound = -k / c;
|
|
730
|
+
if (c > 0)
|
|
731
|
+
hi.set(x, Math.min(hi.get(x) ?? Infinity, Math.floor(bound)));
|
|
732
|
+
else
|
|
733
|
+
lo.set(x, Math.max(lo.get(x) ?? -Infinity, Math.ceil(bound)));
|
|
734
|
+
}
|
|
735
|
+
const range = (lin) => Object.entries(lin).reduce(([a, b], [x, c]) => x === '1' ? [a + c, b + c]
|
|
736
|
+
: c > 0 ? [a + c * (lo.get(x) ?? -Infinity), b + c * (hi.get(x) ?? Infinity)] : [a + c * (hi.get(x) ?? Infinity), b + c * (lo.get(x) ?? -Infinity)], [0, 0]);
|
|
737
|
+
out = [];
|
|
738
|
+
for (const h of goal.hyps.slice(0, -1)) {
|
|
739
|
+
const atoms = [];
|
|
740
|
+
openH(h, root, atoms);
|
|
741
|
+
for (const lin of atoms) {
|
|
742
|
+
const id = canon(lin);
|
|
743
|
+
if (seen.has(id) || seen.has(canonFlip(lin)) || !Object.keys(lin).some(key => key !== '1'))
|
|
744
|
+
continue;
|
|
745
|
+
seen.add(id);
|
|
746
|
+
const [a, b] = range(lin);
|
|
747
|
+
if (given.has(id) || given.has(canon(atomOf(lin, false).lin)) || b <= 0 || a >= 1)
|
|
748
|
+
out.push(lin);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
unitSeen.set(goal, out);
|
|
752
|
+
return out;
|
|
753
|
+
}
|
|
480
754
|
// Each hypothesis' open atoms, as at an earlier path that decided the same atoms it reads (kernel.ts's reusing).
|
|
481
755
|
const openSeen = new WeakMap();
|
|
482
756
|
// What ran out, for a goal left unknown: its size, how deep the case splits went, and the two shapes that usually cause it.
|
|
@@ -847,6 +1121,134 @@ function instancesFor(m, laws, build, opts = {}) {
|
|
|
847
1121
|
}
|
|
848
1122
|
return inst;
|
|
849
1123
|
}
|
|
1124
|
+
// Instances worth their rows: a quantified flag or a law about every key, at a key where the goals read a map the way
|
|
1125
|
+
// its condition reads one at its own key (M[k] read as M[t], M[k + b] as M[t + b]). An instance anywhere else gives each
|
|
1126
|
+
// leaf rows about cells nothing else mentions, and a split for each (a flag over grant numbers at a path's positions,
|
|
1127
|
+
// say); a tuple, where each of its keys is so read. Found as a fixpoint from the goals with none of these instances, as
|
|
1128
|
+
// an instance's rows read cells that make others worth it; a name whose condition reads no map at its keys, and the
|
|
1129
|
+
// focus keys, are kept as they are.
|
|
1130
|
+
// Untrusted: fewer instances still make a checked proof, and where they don't prove a goal the full set is tried.
|
|
1131
|
+
const triggers = (body, key) => {
|
|
1132
|
+
const out = [], mentions = (e) => JSON.stringify(e).includes(`"name":${JSON.stringify(key)}`);
|
|
1133
|
+
const inE = (e) => { if (e.op === 'get') {
|
|
1134
|
+
if (mentions(e.key))
|
|
1135
|
+
out.push({ map: e.map, at: e.key });
|
|
1136
|
+
inE(e.key);
|
|
1137
|
+
}
|
|
1138
|
+
else if (e.op === 'if')
|
|
1139
|
+
(inC(e.c), inE(e.then), inE(e.else));
|
|
1140
|
+
else if (e.op === 'mul')
|
|
1141
|
+
inE(e.a);
|
|
1142
|
+
else if ('a' in e)
|
|
1143
|
+
(inE(e.a), inE(e.b)); };
|
|
1144
|
+
const inC = (c) => (c.op === 'flag' ? undefined : c.op === 'all' ? inC(c.c) : c.op === 'not' ? inC(c.x) : c.op === 'and' || c.op === 'or' ? c.xs.forEach(inC) : (inE(c.a), inE(c.b)));
|
|
1145
|
+
inC(body);
|
|
1146
|
+
return out;
|
|
1147
|
+
};
|
|
1148
|
+
const substKey = (e, key, by) => JSON.parse(JSON.stringify(e).replaceAll(`{"op":"var","name":${JSON.stringify(key)}}`, JSON.stringify(by)));
|
|
1149
|
+
// GDP_TRIGGER=0 keeps every instance (for comparing).
|
|
1150
|
+
const triggerOn = globalThis.process?.env?.GDP_TRIGGER !== '0';
|
|
1151
|
+
function triggered(m, laws, inst, build, focus = []) {
|
|
1152
|
+
if (!triggerOn)
|
|
1153
|
+
return inst;
|
|
1154
|
+
// Per name: its bound keys, and for each the triggers that read it alone (a key with none takes any value).
|
|
1155
|
+
const bodies = new Map();
|
|
1156
|
+
for (const [n, ks] of Object.entries(inst)) {
|
|
1157
|
+
if (!ks.length)
|
|
1158
|
+
continue;
|
|
1159
|
+
const q = n.startsWith('?') ? m.step.flags?.[n.slice(1)] : undefined, l = q ? undefined : laws.find(x => x.name === n);
|
|
1160
|
+
const body = q?.op === 'all' ? q.c : l?.forall ? l.holds : undefined, keys = q?.op === 'all' ? boundOf(q) : l?.forall ? [l.forall, ...(l.keys ?? [])] : [];
|
|
1161
|
+
if (!body)
|
|
1162
|
+
continue;
|
|
1163
|
+
const per = keys.map(key => triggers(body, key).filter(t => keys.every(o => o === key || !JSON.stringify(t.at).includes(`"name":${JSON.stringify(o)}`))));
|
|
1164
|
+
if (per.some(t => t.length))
|
|
1165
|
+
bodies.set(n, { keys, per });
|
|
1166
|
+
}
|
|
1167
|
+
if (!bodies.size)
|
|
1168
|
+
return inst;
|
|
1169
|
+
const tuplesOf = (n, ks) => { const a = bodies.get(n).keys.length; return Array.from({ length: Math.floor(ks.length / a) }, (_, t) => ks.slice(t * a, (t + 1) * a)); };
|
|
1170
|
+
const kept = (tuple) => tuple.some(e => e.op === 'var' && focus.includes(e.name));
|
|
1171
|
+
const out = Object.fromEntries(Object.entries(inst).map(([n, ks]) => [n, bodies.has(n) ? tuplesOf(n, ks).filter(kept).flat() : ks]));
|
|
1172
|
+
for (let round = 0; round < 8; round++) {
|
|
1173
|
+
const reads = new Set();
|
|
1174
|
+
const inE = (e) => { if (e.op === 'get') {
|
|
1175
|
+
reads.add(`${e.map}|${JSON.stringify(e.key)}`);
|
|
1176
|
+
inE(e.key);
|
|
1177
|
+
}
|
|
1178
|
+
else if (e.op === 'if')
|
|
1179
|
+
(inC(e.c), inE(e.then), inE(e.else));
|
|
1180
|
+
else if (e.op === 'mul')
|
|
1181
|
+
inE(e.a);
|
|
1182
|
+
else if ('a' in e)
|
|
1183
|
+
(inE(e.a), inE(e.b)); };
|
|
1184
|
+
const inC = (c) => (c.op === 'flag' ? undefined : c.op === 'all' ? inC(c.c) : c.op === 'not' ? inC(c.x) : c.op === 'and' || c.op === 'or' ? c.xs.forEach(inC) : (inE(c.a), inE(c.b)));
|
|
1185
|
+
for (const g of build(out))
|
|
1186
|
+
g.hyps.forEach(inC);
|
|
1187
|
+
let grew = false;
|
|
1188
|
+
for (const [n, { keys, per }] of bodies) {
|
|
1189
|
+
const have = new Set(tuplesOf(n, out[n]).map(t => JSON.stringify(t)));
|
|
1190
|
+
for (const tuple of tuplesOf(n, inst[n])) {
|
|
1191
|
+
const id = JSON.stringify(tuple);
|
|
1192
|
+
if (have.has(id) || !keys.every((key, i) => !per[i].length || per[i].some(({ map, at }) => reads.has(`${map}|${JSON.stringify(substKey(at, key, tuple[i]))}`))))
|
|
1193
|
+
continue;
|
|
1194
|
+
out[n].push(...tuple);
|
|
1195
|
+
have.add(id);
|
|
1196
|
+
grew = true;
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
if (!grew)
|
|
1200
|
+
break;
|
|
1201
|
+
}
|
|
1202
|
+
return out;
|
|
1203
|
+
}
|
|
1204
|
+
// For a certificate others check again (the Lean export sets `minimal.on`): the instances of quantified flags a proof
|
|
1205
|
+
// found that its leaves refute with, the proof grown again with those alone. The checked goal is then built from far
|
|
1206
|
+
// fewer hypotheses (a law about pairs of items with 900 instance hypotheses and 30 leaves using a few of them, say).
|
|
1207
|
+
export const minimal = { on: false };
|
|
1208
|
+
// The hypotheses a tree's leaves read: each leaf's input rows, and the rows its products multiply.
|
|
1209
|
+
export function usedHyps(goal, tree, path = new Map(), out = new Set()) {
|
|
1210
|
+
if ('leaf' in tree) {
|
|
1211
|
+
const rows = rowsAt(goal, path), n = rows.length, sq = tree.squares?.length ?? 0;
|
|
1212
|
+
const mark = (i) => {
|
|
1213
|
+
if (i < n) {
|
|
1214
|
+
const s = rows[i].src;
|
|
1215
|
+
if (s && 'fact' in s)
|
|
1216
|
+
out.add(s.fact);
|
|
1217
|
+
}
|
|
1218
|
+
else if (i >= n + sq) {
|
|
1219
|
+
const pq = tree.products?.[i - n - sq];
|
|
1220
|
+
if (pq) {
|
|
1221
|
+
mark(pq[0]);
|
|
1222
|
+
mark(pq[1]);
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
};
|
|
1226
|
+
for (const st of tree.leaf)
|
|
1227
|
+
if (st.kind === 'input')
|
|
1228
|
+
mark(st.index);
|
|
1229
|
+
return out;
|
|
1230
|
+
}
|
|
1231
|
+
const id = canon(tree.split);
|
|
1232
|
+
usedHyps(goal, tree.yes, new Map(path).set(id, { lin: tree.split, holds: true }), out);
|
|
1233
|
+
return usedHyps(goal, tree.no, new Map(path).set(id, { lin: tree.split, holds: false }), out);
|
|
1234
|
+
}
|
|
1235
|
+
// The tuples of each quantified flag whose hypotheses `used` names: a flag's instance hypotheses follow its ?f <= 1 in
|
|
1236
|
+
// the goal, in the instances' order (kernel.ts's closing).
|
|
1237
|
+
function usedInstances(m, goal, inst, used, focus) {
|
|
1238
|
+
const out = { ...inst };
|
|
1239
|
+
for (const [f, q] of Object.entries(m.step.flags ?? {})) {
|
|
1240
|
+
if (q.op !== 'all' || !inst[`?${f}`]?.length)
|
|
1241
|
+
continue;
|
|
1242
|
+
const at = goal.hyps.findIndex(h => h.op === '<=' && h.a.op === 'var' && h.a.name === `?${f}` && h.b.op === 'const' && h.b.value === 1);
|
|
1243
|
+
if (at < 0)
|
|
1244
|
+
continue;
|
|
1245
|
+
const a = boundOf(q).length, ks = inst[`?${f}`];
|
|
1246
|
+
out[`?${f}`] = Array.from({ length: Math.floor(ks.length / a) }, (_, t) => ks.slice(t * a, (t + 1) * a))
|
|
1247
|
+
.filter((tuple, t) => used.has(at + 1 + t) || tuple.some(e => e.op === 'var' && focus.includes(e.name))).flat();
|
|
1248
|
+
}
|
|
1249
|
+
return out;
|
|
1250
|
+
}
|
|
1251
|
+
const sameInstances = (a, b) => Object.keys(b).every(n => (a[n]?.length ?? 0) === b[n].length);
|
|
850
1252
|
// Instances without the keys that read any of these names (a law about several keys loses each tuple that does).
|
|
851
1253
|
const avoiding = (inst, names, laws, m) => {
|
|
852
1254
|
const bad = (e) => names.some(n => JSON.stringify(e).includes(`"name":${JSON.stringify(n)}`));
|
|
@@ -941,6 +1343,9 @@ export function proveLaw(m, laws, i) {
|
|
|
941
1343
|
seed: [...keys, ...(law.forall ? [{ op: 'var', name: `${law.forall}#` }] : []), ...(law.keys ?? []).map((name) => ({ op: 'var', name }))],
|
|
942
1344
|
focus: [...(law.forall ? [`${law.forall}#`] : []), ...(law.keys ?? [])],
|
|
943
1345
|
});
|
|
1346
|
+
const few = triggered(m, laws, inst, i2 => obligations(m, laws, { [law.name]: i2 }, i), law.forall ? [`${law.forall}#`] : []);
|
|
1347
|
+
if (!sameInstances(few, inst) && obligations(m, laws, { [law.name]: few }, i).every(goal => 'tree' in grow(goal)))
|
|
1348
|
+
return { proven: true };
|
|
944
1349
|
for (const goal of obligations(m, laws, { [law.name]: inst }, i)) {
|
|
945
1350
|
const out = grow(goal);
|
|
946
1351
|
if (!('tree' in out))
|
|
@@ -957,6 +1362,64 @@ function proveLawsAsGiven(m, laws) {
|
|
|
957
1362
|
focus: [...(law.forall ? [`${law.forall}#`] : []), ...(law.keys ?? [])],
|
|
958
1363
|
});
|
|
959
1364
|
});
|
|
1365
|
+
const t0 = performance.now(), out = proveThinned(m, laws, { ...instances }), usualMs = performance.now() - t0;
|
|
1366
|
+
if (cyclicOn()) {
|
|
1367
|
+
if (cyclicMachine(m, laws))
|
|
1368
|
+
crossCyclic('machine', m.name, out.proven && out.checked, () => { const { facts, specs } = machineNodes(m, laws, instances); return proveCyclic(facts, specs); }, usualMs);
|
|
1369
|
+
else
|
|
1370
|
+
console.error(`cyclic: ${JSON.stringify({ kind: 'machine', name: m.name, usual: out.proven && out.checked, uncovered: true })}`);
|
|
1371
|
+
}
|
|
1372
|
+
return out;
|
|
1373
|
+
}
|
|
1374
|
+
// Each law with its instances thinned (`triggered`) where that proves it, else with all of them.
|
|
1375
|
+
function proveThinned(m, laws, instances) {
|
|
1376
|
+
const trees = {}, fulls = { ...instances };
|
|
1377
|
+
let nodes = 0, leaves = 0;
|
|
1378
|
+
for (const [i, law] of laws.entries()) {
|
|
1379
|
+
const full = instances[law.name], few = triggered(m, laws, full, inst => obligations(m, laws, { ...instances, [law.name]: inst }, i), law.forall ? [`${law.forall}#`] : []);
|
|
1380
|
+
for (const inst of sameInstances(few, full) ? [full] : [few, full]) {
|
|
1381
|
+
instances[law.name] = inst;
|
|
1382
|
+
const done = {};
|
|
1383
|
+
let failed, n = 0, lv = 0;
|
|
1384
|
+
for (const goal of obligations(m, laws, instances, i)) {
|
|
1385
|
+
const out = grow(goal);
|
|
1386
|
+
if (!('tree' in out)) {
|
|
1387
|
+
failed = { goal: goal.name, stuck: out.stuck, ...(out.witness ? { witness: out.witness } : {}), ...(out.why ? { why: out.why } : {}) };
|
|
1388
|
+
break;
|
|
1389
|
+
}
|
|
1390
|
+
done[goal.name] = out.tree;
|
|
1391
|
+
n += out.nodes;
|
|
1392
|
+
lv += countLeaves(out.tree);
|
|
1393
|
+
}
|
|
1394
|
+
if (failed && inst !== full)
|
|
1395
|
+
continue;
|
|
1396
|
+
if (failed)
|
|
1397
|
+
return { proven: false, nodes, leaves, failed, checked: false };
|
|
1398
|
+
Object.assign(trees, done);
|
|
1399
|
+
nodes += n;
|
|
1400
|
+
leaves += lv;
|
|
1401
|
+
break;
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
// The goals of no one law (a history's own), with the instances chosen.
|
|
1405
|
+
for (const goal of obligations(m, laws, instances)) {
|
|
1406
|
+
if (trees[goal.name])
|
|
1407
|
+
continue;
|
|
1408
|
+
const out = grow(goal);
|
|
1409
|
+
// Where thinned instances leave such a goal unproven, every goal again with the full ones.
|
|
1410
|
+
if (!('tree' in out) && laws.some(l => instances[l.name] !== fulls[l.name]))
|
|
1411
|
+
return proveWith(m, laws, fulls);
|
|
1412
|
+
if (!('tree' in out))
|
|
1413
|
+
return { proven: false, nodes, leaves, failed: { goal: goal.name, stuck: out.stuck, ...(out.witness ? { witness: out.witness } : {}), ...(out.why ? { why: out.why } : {}) }, checked: false };
|
|
1414
|
+
trees[goal.name] = out.tree;
|
|
1415
|
+
nodes += out.nodes;
|
|
1416
|
+
leaves += countLeaves(out.tree);
|
|
1417
|
+
}
|
|
1418
|
+
const proof = { instances, trees };
|
|
1419
|
+
return { proven: true, nodes, leaves, checked: checkLaws(m, laws, proof), proof };
|
|
1420
|
+
}
|
|
1421
|
+
// Every goal of the laws with these instances.
|
|
1422
|
+
function proveWith(m, laws, instances) {
|
|
960
1423
|
const trees = {};
|
|
961
1424
|
let nodes = 0, leaves = 0;
|
|
962
1425
|
for (const goal of obligations(m, laws, instances)) {
|
|
@@ -1007,7 +1470,12 @@ function proveStepLawsAsGiven(m, laws, stepLaws, base) {
|
|
|
1007
1470
|
let nodes = base.nodes, leaves = base.leaves;
|
|
1008
1471
|
for (const sl of stepLaws)
|
|
1009
1472
|
for (const goal of stepLawObligations(m, laws, perStep[sl.name] ?? instances, sl)) {
|
|
1010
|
-
|
|
1473
|
+
const full = perStep[sl.name] ?? instances, few = triggered(plain, laws, full, inst => stepLawObligations(m, laws, inst, sl), [sl.forall, ...(sl.keys ?? [])].flatMap(k => (k ? [k] : [])));
|
|
1474
|
+
let out = sameInstances(few, full) ? grow(goal) : grow(stepLawObligations(m, laws, few, sl).find(g => g.name === goal.name) ?? goal);
|
|
1475
|
+
if ('tree' in out && !sameInstances(few, full))
|
|
1476
|
+
perStep[sl.name] = few;
|
|
1477
|
+
else if (!sameInstances(few, full))
|
|
1478
|
+
out = grow(goal);
|
|
1011
1479
|
// A k-step law that reads a condition kept across its steps: each flag at its own witnesses, at every step.
|
|
1012
1480
|
if (!('tree' in out))
|
|
1013
1481
|
for (const kept of keptAcross(plain, laws, perStep[sl.name] ?? instances, own(sl), sl.steps ?? 1)) {
|
|
@@ -1020,6 +1488,18 @@ function proveStepLawsAsGiven(m, laws, stepLaws, base) {
|
|
|
1020
1488
|
}
|
|
1021
1489
|
if (!('tree' in out))
|
|
1022
1490
|
return { proven: false, nodes, leaves, failed: { goal: goal.name, stuck: out.stuck, ...(out.witness ? { witness: out.witness } : {}), ...(out.why ? { why: out.why } : {}) }, checked: false };
|
|
1491
|
+
if (minimal.on) {
|
|
1492
|
+
const had = perStep[sl.name] ?? instances, built = stepLawObligations(m, laws, had, sl).find(g => g.name === goal.name);
|
|
1493
|
+
if (built) {
|
|
1494
|
+
const focus = [sl.forall, ...(sl.keys ?? [])].flatMap(k => (k ? [k] : [])), thin = usedInstances(plain, built, had, usedHyps(built, out.tree), focus);
|
|
1495
|
+
const g2 = sameInstances(thin, had) ? undefined : stepLawObligations(m, laws, thin, sl).find(g => g.name === goal.name);
|
|
1496
|
+
const again = g2 && grow(g2);
|
|
1497
|
+
if (again && 'tree' in again) {
|
|
1498
|
+
out = again;
|
|
1499
|
+
perStep[sl.name] = thin;
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1023
1503
|
trees[goal.name] = out.tree;
|
|
1024
1504
|
nodes += out.nodes;
|
|
1025
1505
|
leaves += countLeaves(out.tree);
|