gdp-wired 0.3.0 → 0.5.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.
@@ -1,5 +1,5 @@
1
- import { add, k, scale, solve, sub } from "../core/solver.js";
2
- import { canon, check, checkLaws, checkStepLaws, historyMachine, leafConstraints, linear, livenessHelpful, livenessParts, obligations, plainKey, progressObligations, stepLawObligations, trueC, truth, unreachable, withProducts } from "./kernel.js";
1
+ import { add, k, scale, solve, sub, v } from "../core/solver.js";
2
+ import { assumed, canon, columnTerm, decided, stepZone, check, checkLaws, checkStepLaws, historyMachine, leafConstraints, linear, livenessHelpful, livenessParts, obligations, plainKey, progressObligations, stepLawObligations, trueC, truth, unreachable, withProducts } from "./kernel.js";
3
3
  // The atoms the path leaves open, in evaluation order: deciding any of them makes progress.
4
4
  function openE(e, path, out) {
5
5
  switch (e.op) {
@@ -22,7 +22,7 @@ function openE(e, path, out) {
22
22
  case 'max': {
23
23
  openE(e.a, path, out), openE(e.b, path, out);
24
24
  const a = linear(e.a, path), b = linear(e.b, path);
25
- if (a && b && linear(e, path) === undefined)
25
+ if (a && b && !decided(e, path))
26
26
  out.push(e.op === 'sub' ? sub(b, a) : sub(a, b));
27
27
  return;
28
28
  }
@@ -116,12 +116,34 @@ function parse(text) {
116
116
  // The budget: 200,000 nodes and, by default, a second per hypothesis (at least 20 seconds) per goal; past either the goal
117
117
  // is left `unknown`. A goal with many instances (a law about two keys at its pairs) has that many more rows per leaf.
118
118
  export function grow(goal, path = new Map(), budget = { nodes: 0, until: performance.now() + Math.max(20_000, 1_000 * goal.hyps.length) }) {
119
+ // At the root: plainly first; where the cases multiply past a budget, through cuts. The plain budget halves with each
120
+ // two open choices (what a plain search's cases grow by), so a long run's steps reach the cuts early.
121
+ if (path.size === 0 && budget.nodes === 0) {
122
+ const open = new Set(leafConstraints(goal, path).flatMap(r => Object.keys(r.lin)).filter(n => n.startsWith('⟦') && columnTerm(n)?.op === 'if')).size;
123
+ const plain = growPlain(goal, path, { nodes: 0, until: budget.until, cap: Math.max(32, Math.round(4_000 / 2 ** (open / 2))) });
124
+ if ('tree' in plain || plain.stuck === 'sat' || plain.why === undefined)
125
+ return plain;
126
+ const cut = withCuts(goal, path, budget);
127
+ if (cut)
128
+ return cut;
129
+ }
130
+ return growPlain(goal, path, budget);
131
+ }
132
+ function growPlain(goal, path, budget) {
133
+ if (budget.cap !== undefined && budget.nodes >= budget.cap)
134
+ return { stuck: 'unknown', path, why: 'cap' };
119
135
  if (++budget.nodes > 200_000 || (budget.until !== undefined && performance.now() > budget.until))
120
- return { stuck: 'unknown', path };
121
- const rows = leafConstraints(goal, path);
136
+ return { stuck: 'unknown', path, why: outOfBudget(goal, budget.nodes > 200_000 ? 'nodes' : 'time', path.size) };
137
+ // Rows that read a term column only once a cut has put one on the path; before, the rows a choice-free search uses
138
+ // (what it saw before term columns), renumbered to the whole leaf's when a leaf is found.
139
+ const full = leafConstraints(goal, path);
140
+ const columns = [...path.values()].some(a => Object.keys(a.lin).some(key => key.startsWith('⟦')));
141
+ const idx = full.flatMap((r, i) => (columns || !Object.keys(r.lin).some(key => key.startsWith('⟦')) ? [i] : []));
142
+ const rows = idx.map(i => full[i]), at = (i) => (i < rows.length ? idx[i] : full.length + i - rows.length);
143
+ const leafOf = (steps, products) => ({ leaf: steps.map(st => (st.kind === 'input' ? { ...st, index: at(st.index) } : st)), ...(products ? { products: products.map(([i, j]) => [at(i), at(j)]) } : {}) });
122
144
  const verdict = solve(rows);
123
145
  if (verdict.kind === 'refuted')
124
- return { tree: { leaf: verdict.steps }, nodes: budget.nodes };
146
+ return { tree: leafOf(verdict.steps), nodes: budget.nodes };
125
147
  const open = [];
126
148
  for (const h of goal.hyps)
127
149
  openH(h, path, open);
@@ -148,7 +170,7 @@ export function grow(goal, path = new Map(), budget = { nodes: 0, until: perform
148
170
  pairs.splice(300);
149
171
  const product = solve(withProducts(rows, pairs));
150
172
  if (product.kind === 'refuted')
151
- return { tree: { leaf: product.steps, products: pairs }, nodes: budget.nodes };
173
+ return { tree: leafOf(product.steps, pairs), nodes: budget.nodes };
152
174
  // Degree three: each row times each product of two variables (x·y >= 0 is the pair of -x <= 0 and -y <= 0).
153
175
  const degree = Math.max(...rows.flatMap(r => Object.keys(r.lin).map(key => (key === '1' ? 0 : key.split('*').length))));
154
176
  if (degree >= 3) {
@@ -159,7 +181,7 @@ export function grow(goal, path = new Map(), budget = { nodes: 0, until: perform
159
181
  cubes.push([rows.length + s, i]);
160
182
  const third = solve(withProducts(rows, [...sq, ...cubes]));
161
183
  if (third.kind === 'refuted')
162
- return { tree: { leaf: third.steps, products: [...sq, ...cubes] }, nodes: budget.nodes };
184
+ return { tree: leafOf(third.steps, [...sq, ...cubes]), nodes: budget.nodes };
163
185
  }
164
186
  const factors = [...new Set(rows.flatMap(r => Object.keys(r.lin).filter(key => key.includes('*')).flatMap(key => key.split('*'))))];
165
187
  const fresh = (lin) => !path.has(canon(lin)) && !path.has(canon(add(k(1), scale(lin, -1))));
@@ -174,14 +196,105 @@ export function grow(goal, path = new Map(), budget = { nodes: 0, until: perform
174
196
  if (!split)
175
197
  return { stuck: verdict.kind === 'sat' ? 'sat' : 'unknown', ...(verdict.kind === 'sat' ? { witness: verdict.witness } : {}), path };
176
198
  const id = canon(split);
177
- const yes = grow(goal, new Map(path).set(id, { lin: split, holds: true }), budget);
199
+ const yes = growPlain(goal, new Map(path).set(id, { lin: split, holds: true }), budget);
200
+ if (!('tree' in yes))
201
+ return yes;
202
+ const no = growPlain(goal, new Map(path).set(id, { lin: split, holds: false }), budget);
203
+ if (!('tree' in no))
204
+ return no;
205
+ return { tree: { split, yes: yes.tree, no: no.tree }, nodes: budget.nodes };
206
+ }
207
+ // What ran out, for a goal left unknown: its size, how deep the case splits went, and the two shapes that usually cause it.
208
+ function outOfBudget(goal, limit, depth) {
209
+ const steps = Number(/@(\d+) steps/.exec(goal.name)?.[1] ?? 1);
210
+ return `the prover's ${limit === 'nodes' ? '200,000-node' : 'time'} budget ran out at ${goal.hyps.length} hypotheses, ${depth} case splits deep. ` +
211
+ `Each case a step decides multiplies the cases to refute, so ${steps > 1 ? `a law over ${steps} steps` : 'many interacting cases'} can outgrow it: ` +
212
+ 'state the law over fewer steps (a one-step law and an invariant often give the same), or split it into laws about fewer conditions';
213
+ }
214
+ // Cuts: where each of many choices (a step's case, over a run of k steps) multiplies the cases, a fact about each
215
+ // choice's value that holds whichever way it goes is split on first: its failing side is refuted by deciding that
216
+ // choice alone, and the rest of the proof assumes it, so the tree is a chain of small refutations instead of every
217
+ // combination. For a term column t = if c then A else B the candidates are t ≤ A, t ≤ B, t ≥ A and t ≥ B, innermost
218
+ // choices first; a cut is only a split, so the kernel checks it like any other.
219
+ function withCuts(goal, path, budget) {
220
+ const rows = leafConstraints(goal, path);
221
+ const columns = [...new Set(rows.flatMap(r => Object.keys(r.lin)).filter(n => n.startsWith('⟦')))]
222
+ .map(n => ({ n, e: columnTerm(n) })).filter(x => x.e.op === 'if' && !decided(x.e, path))
223
+ .sort((a, b) => Number(a.n.slice(1, -1)) - Number(b.n.slice(1, -1))); // a term's parts get their ids before it: innermost first
224
+ const chain = [];
225
+ let at = new Map(path);
226
+ for (const { n, e } of columns) {
227
+ if (e.op !== 'if')
228
+ continue;
229
+ const a = linear(e.then, at), b = linear(e.else, at), focus = topAtoms(e.c, at);
230
+ if (!a || !b || !focus.length)
231
+ continue;
232
+ for (const lemma of [sub(v(n), a), sub(v(n), b), sub(a, v(n)), sub(b, v(n))]) {
233
+ const id = canon(lemma);
234
+ if (at.has(id) || Object.keys(lemma).every(key => key === '1'))
235
+ continue;
236
+ const no = growFocused(goal, new Map(at).set(id, { lin: lemma, holds: false }), focus, { nodes: 0 });
237
+ if ('tree' in no) {
238
+ chain.push({ split: lemma, no: no.tree });
239
+ at = new Map(at).set(id, { lin: lemma, holds: true });
240
+ }
241
+ }
242
+ }
243
+ if (!chain.length)
244
+ return undefined;
245
+ const rest = growPlain(goal, at, budget);
246
+ if (!('tree' in rest))
247
+ return rest;
248
+ return { tree: chain.reduceRight((acc, c) => ({ split: c.split, yes: acc, no: c.no }), rest.tree), nodes: budget.nodes };
249
+ }
250
+ // A condition's own comparisons, each a split atom, read with term columns for whatever choices are inside it.
251
+ function topAtoms(c, path) {
252
+ if (c.op === 'not')
253
+ return topAtoms(c.x, path);
254
+ if (c.op === 'and' || c.op === 'or')
255
+ return c.xs.flatMap(y => topAtoms(y, path));
256
+ if (c.op === 'flag' || c.op === 'all')
257
+ return [];
258
+ const a = linear(c.a, path), b = linear(c.b, path);
259
+ if (!a || !b)
260
+ return [];
261
+ const atoms = c.op === '<=' ? [sub(a, b)] : c.op === '<' ? [add(sub(a, b), k(1))] : [sub(a, b), sub(b, a)];
262
+ return atoms.filter(l => Object.keys(l).some(key => key !== '1') && !path.has(canon(l)) && !path.has(canon(add(k(1), scale(l, -1)))));
263
+ }
264
+ // A refutation that splits only on these atoms (a cut's failing side), within a small budget.
265
+ function growFocused(goal, path, focus, budget, near = focus) {
266
+ if (++budget.nodes > 64)
267
+ return { stuck: 'unknown', path };
268
+ const verdict = solveNear(leafConstraints(goal, path), [...near, ...[...path.values()].slice(-1).map(a => a.lin)]);
269
+ if (verdict.kind === 'refuted')
270
+ return { tree: { leaf: verdict.steps }, nodes: budget.nodes };
271
+ const split = focus.find(l => !path.has(canon(l)) && !path.has(canon(add(k(1), scale(l, -1)))));
272
+ if (!split)
273
+ return { stuck: verdict.kind === 'sat' ? 'sat' : 'unknown', path };
274
+ const id = canon(split);
275
+ const yes = growFocused(goal, new Map(path).set(id, { lin: split, holds: true }), focus, budget, near);
178
276
  if (!('tree' in yes))
179
277
  return yes;
180
- const no = grow(goal, new Map(path).set(id, { lin: split, holds: false }), budget);
278
+ const no = growFocused(goal, new Map(path).set(id, { lin: split, holds: false }), focus, budget, near);
181
279
  if (!('tree' in no))
182
280
  return no;
183
281
  return { tree: { split, yes: yes.tree, no: no.tree }, nodes: budget.nodes };
184
282
  }
283
+ // A refutation from the rows near some facts (the rows reachable from their variables in two hops, each row joining the
284
+ // variables it reads), its inputs renumbered to the whole leaf's. A cut about one step of a long run needs that step's
285
+ // rows, not the run's; a cut it misses is only a cut not made (the plain search still follows).
286
+ function solveNear(rows, seeds) {
287
+ const reach = new Set(seeds.flatMap(l => Object.keys(l).filter(key => key !== '1')));
288
+ for (let hop = 0; hop < 2; hop++)
289
+ for (const r of rows) {
290
+ const keys = Object.keys(r.lin).filter(key => key !== '1');
291
+ if (keys.some(key => reach.has(key)))
292
+ keys.forEach(key => reach.add(key));
293
+ }
294
+ const picked = rows.flatMap((r, i) => (Object.keys(r.lin).every(key => key === '1' || reach.has(key)) ? [i] : []));
295
+ const near = solve(picked.map(i => rows[i]));
296
+ return near.kind === 'refuted' ? { kind: 'refuted', steps: near.steps.map(st => (st.kind === 'input' ? { ...st, index: picked[st.index] } : st)) } : near;
297
+ }
185
298
  // Integer completeness for bounded systems: when elimination is inexact and finds no witness, halve a variable's range
186
299
  // (x <= mid or x >= mid + 1). Each half is a split like any other, so the kernel checks it the same way; a variable
187
300
  // pinned to one value becomes an equality, which the solver substitutes exactly.
@@ -363,6 +476,42 @@ function match(pattern, term, x) {
363
476
  const witnesses = (m) => Object.entries(m.step.flags ?? {}).flatMap(([f, c]) => (c.op === 'all' ? [{ op: 'var', name: `w?${f}` }] : []));
364
477
  // The keys each quantified flag is assumed at (instances under ?f).
365
478
  const flagInstances = (m, keys) => Object.fromEntries(Object.entries(m.step.flags ?? {}).flatMap(([f, c]) => (c.op === 'all' ? [[`?${f}`, keys]] : [])));
479
+ // The instances some goals need, for every entry point: the every-key laws and the quantified flags at `seed` (the keys
480
+ // the step and the laws read, when known before any goal is built), then twice round: the facts at every key the goals
481
+ // built so far read a function at, and, where the seed doesn't cover them (no seed, positions that shift, a law about
482
+ // several keys), the every-key laws and flags at every key they read a map at (a law about several keys at tuples
483
+ // relating `focus`, the goals' own keys).
484
+ function instancesFor(m, laws, build, opts = {}) {
485
+ let inst = opts.start ?? (opts.seed ? { ...Object.fromEntries(laws.filter(l => l.forall).map(l => [l.name, l.keys?.length ? [] : opts.seed])), ...flagInstances(m, opts.seed) } : {});
486
+ const reads = opts.reads ?? (!opts.seed || shifts(m) || laws.some(l => l.keys?.length));
487
+ for (let round = 0; round < 2; round++) {
488
+ if (m.facts?.length)
489
+ inst = withFacts(m, inst, fnKeys(m, build(inst)));
490
+ if (reads) {
491
+ const keys = mapKeys(build(inst));
492
+ inst = union(withReads(laws, inst, keys, opts.focus ?? []), flagInstances(m, keys));
493
+ }
494
+ }
495
+ return inst;
496
+ }
497
+ // Instances without the keys that read any of these names (a law about several keys loses each tuple that does).
498
+ const avoiding = (inst, names, laws) => {
499
+ const bad = (e) => names.some(n => JSON.stringify(e).includes(`"name":${JSON.stringify(n)}`));
500
+ return Object.fromEntries(Object.entries(inst).map(([n, keys]) => {
501
+ const arity = (laws.find(l => l.name === n)?.keys?.length ?? 0) + 1;
502
+ const tuples = Array.from({ length: Math.floor(keys.length / arity) }, (_, t) => keys.slice(t * arity, (t + 1) * arity));
503
+ return [n, tuples.filter(tuple => !tuple.some(bad)).flat()];
504
+ }));
505
+ };
506
+ // Two sets of instances together: each name at the keys of both.
507
+ const union = (a, b) => {
508
+ const out = { ...a };
509
+ for (const [n, keys] of Object.entries(b)) {
510
+ const seen = new Set((out[n] ?? []).map(e => JSON.stringify(e)));
511
+ out[n] = [...(out[n] ?? []), ...keys.filter(e => !seen.has(JSON.stringify(e)))];
512
+ }
513
+ return out;
514
+ };
366
515
  const someKeys = (c, positive = true) => c.op === 'all' ? [...(positive ? [] : [c]), ...someKeys(c.c, positive)] : c.op === 'not' ? someKeys(c.x, !positive) : c.op === 'and' || c.op === 'or' ? c.xs.flatMap(y => someKeys(y, positive)) : [];
367
516
  function atWitness(c, picks, positive = true) {
368
517
  switch (c.op) {
@@ -427,29 +576,20 @@ export function proveLaws(m, laws) {
427
576
  return { ...proveLawsAsGiven(m, out.items), laws: out.items };
428
577
  }
429
578
  function proveLawsAsGiven(m, laws) {
430
- const keys = [...readKeys(m, laws), ...witnesses(m)];
431
- const instances = {};
432
- for (const law of laws) {
433
- const at = [...keys, ...(law.forall ? [{ op: 'var', name: `${law.forall}#` }] : []), ...(law.keys ?? []).map((name) => ({ op: 'var', name }))];
434
- instances[law.name] = { ...Object.fromEntries(laws.filter(l => l.forall).map(l => [l.name, l.keys?.length ? [] : at])), ...flagInstances(m, at) };
435
- }
436
- // The facts about functions, at every key each law's goals read a function at (found from the goals, twice round).
437
- for (let round = 0; round < 2 && m.facts?.length; round++) {
438
- const goals = obligations(m, laws, instances);
439
- laws.forEach((law, i) => { instances[law.name] = withFacts(m, instances[law.name], fnKeys(m, goals.slice(2 * i, 2 * i + 2))); });
440
- }
441
- // Where positions shift (a write at every key reading its key at k ∸ 1 or k + 1) or a law is about two keys, the
442
- // every-key laws also at each key the goals read a map at, once round.
443
- if (shifts(m) || laws.some(l => l.keys?.length)) {
444
- const goals = obligations(m, laws, instances);
445
- laws.forEach((law, i) => { instances[law.name] = withReads(laws, instances[law.name], mapKeys(goals.slice(2 * i, 2 * i + 2)), [...(law.forall ? [`${law.forall}#`] : []), ...(law.keys ?? [])]); });
446
- }
579
+ // Each law's two goals (start and step) get their own instances, seeded with its own keys.
580
+ const keys = [...readKeys(m, laws), ...witnesses(m)], instances = {};
581
+ laws.forEach((law, i) => {
582
+ instances[law.name] = instancesFor(m, laws, inst => obligations(m, laws, { ...instances, [law.name]: inst }).slice(2 * i, 2 * i + 2), {
583
+ seed: [...keys, ...(law.forall ? [{ op: 'var', name: `${law.forall}#` }] : []), ...(law.keys ?? []).map((name) => ({ op: 'var', name }))],
584
+ focus: [...(law.forall ? [`${law.forall}#`] : []), ...(law.keys ?? [])],
585
+ });
586
+ });
447
587
  const trees = {};
448
588
  let nodes = 0, leaves = 0;
449
589
  for (const goal of obligations(m, laws, instances)) {
450
590
  const out = grow(goal);
451
591
  if (!('tree' in out))
452
- return { proven: false, nodes, leaves, failed: { goal: goal.name, stuck: out.stuck, ...(out.witness ? { witness: out.witness } : {}) }, checked: false };
592
+ return { proven: false, nodes, leaves, failed: { goal: goal.name, stuck: out.stuck, ...(out.witness ? { witness: out.witness } : {}), ...(out.why ? { why: out.why } : {}) }, checked: false };
453
593
  trees[goal.name] = out.tree;
454
594
  nodes += out.nodes;
455
595
  leaves += countLeaves(out.tree);
@@ -476,23 +616,26 @@ function proveStepLawsAsGiven(m, laws, stepLaws, base) {
476
616
  if (!base.proof)
477
617
  return { proven: false, nodes: base.nodes, leaves: base.leaves, checked: false };
478
618
  const plain = m.history ? historyMachine(m).machine : m;
479
- const keys = [...readKeys(plain, laws), ...witnesses(plain), ...stepLaws.flatMap(sl => [sl.then, ...(sl.when === undefined ? [] : Array.isArray(sl.when) ? sl.when : [sl.when])].flatMap(readKeysC)),
480
- ...stepLaws.flatMap(sl => [sl.forall, ...(sl.keys ?? [])].flatMap(key => (key ? [{ op: 'var', name: key }] : [])))];
481
- let instances = { ...Object.fromEntries(laws.filter(l => l.forall).map(l => [l.name, l.keys?.length ? [] : keys])), ...flagInstances(plain, keys) };
482
- for (let round = 0; round < 2 && plain.facts?.length; round++)
483
- instances = withFacts(plain, instances, fnKeys(plain, stepLaws.flatMap(sl => stepLawObligations(m, laws, instances, sl))));
484
- // Where positions shift or a law is about two keys, each step law gets its own instances, from its own goal.
619
+ // Each step law its own instances, from its own goal: the keys the step and the laws read, and its own (a k-step
620
+ // law's later frames read keys no other step law has); where positions shift or a law is about several keys, the
621
+ // keys its goal reads a map at too. A frame's hypotheses read no name the run introduces later (kernel.ts's
622
+ // stepZone), so a k-step law drops the keys that read one.
623
+ const shared = [...readKeys(plain, laws), ...witnesses(plain)], named = [...laws, ...(plain.facts ?? [])], reads = shifts(plain) || laws.some(l => l.keys?.length);
624
+ const own = (sl) => [...[sl.then, ...(sl.when === undefined ? [] : Array.isArray(sl.when) ? sl.when : [sl.when])].flatMap(readKeysC),
625
+ ...[sl.forall, ...(sl.keys ?? [])].flatMap(key => (key ? [{ op: 'var', name: key }] : []))];
485
626
  const perStep = {};
486
- if (shifts(plain) || laws.some(l => l.keys?.length))
487
- for (const sl of stepLaws)
488
- perStep[sl.name] = withReads(laws, instances, mapKeys(stepLawObligations(m, laws, instances, sl)), [sl.forall, ...(sl.keys ?? [])].flatMap(k => (k ? [k] : [])));
627
+ for (const sl of stepLaws) {
628
+ const zone = stepZone(plain, sl.steps ?? 1), keep = (inst) => avoiding(inst, zone, named);
629
+ perStep[sl.name] = keep(instancesFor(plain, laws, inst => stepLawObligations(m, laws, keep(inst), sl), { seed: [...shared, ...own(sl)], focus: [sl.forall, ...(sl.keys ?? [])].flatMap(k => (k ? [k] : [])), reads }));
630
+ }
631
+ const instances = perStep[stepLaws[0]?.name ?? ''] ?? {};
489
632
  const trees = {};
490
633
  let nodes = base.nodes, leaves = base.leaves;
491
634
  for (const sl of stepLaws)
492
635
  for (const goal of stepLawObligations(m, laws, perStep[sl.name] ?? instances, sl)) {
493
636
  const out = grow(goal);
494
637
  if (!('tree' in out))
495
- return { proven: false, nodes, leaves, failed: { goal: goal.name, stuck: out.stuck, ...(out.witness ? { witness: out.witness } : {}) }, checked: false };
638
+ return { proven: false, nodes, leaves, failed: { goal: goal.name, stuck: out.stuck, ...(out.witness ? { witness: out.witness } : {}), ...(out.why ? { why: out.why } : {}) }, checked: false };
496
639
  trees[goal.name] = out.tree;
497
640
  nodes += out.nodes;
498
641
  leaves += countLeaves(out.tree);
@@ -507,16 +650,13 @@ export function proveProgress(m, laws, p) {
507
650
  if (!base.proven || !base.checked)
508
651
  return { proven: false, nodes: base.nodes, leaves: base.leaves, ...(base.failed ? { failed: base.failed } : {}), checked: false };
509
652
  const ls = base.laws ?? laws;
510
- let instances = { ...withReads(ls, {}, mapKeys(progressObligations(m, ls, {}, p)), []), ...flagInstances(m, mapKeys(progressObligations(m, ls, {}, p))) };
511
- instances = { ...instances, ...withReads(ls, instances, mapKeys(progressObligations(m, ls, instances, p)), []) };
512
- if (m.facts?.length)
513
- instances = withFacts(m, instances, fnKeys(m, progressObligations(m, ls, instances, p)));
653
+ const instances = instancesFor(m, ls, inst => progressObligations(m, ls, inst, p));
514
654
  const trees = {};
515
655
  let nodes = base.nodes, leaves = base.leaves;
516
656
  for (const goal of progressObligations(m, ls, instances, p)) {
517
657
  const out = grow(goal);
518
658
  if (!('tree' in out))
519
- return { proven: false, nodes, leaves, failed: { goal: goal.name, stuck: out.stuck, ...(out.witness ? { witness: out.witness } : {}) }, checked: false };
659
+ return { proven: false, nodes, leaves, failed: { goal: goal.name, stuck: out.stuck, ...(out.witness ? { witness: out.witness } : {}), ...(out.why ? { why: out.why } : {}) }, checked: false };
520
660
  trees[goal.name] = out.tree;
521
661
  nodes += out.nodes;
522
662
  leaves += countLeaves(out.tree);
@@ -562,7 +702,7 @@ function readKeysC(c) {
562
702
  export const countLeaves = (t) => ('leaf' in t ? 1 : countLeaves(t.yes) + countLeaves(t.no));
563
703
  // A real counterexample for a goal the prover couldn't close (with products, its witness may be abstract): the free
564
704
  // variables over 0..bound, every hypothesis evaluated with true arithmetic. Goals that read maps are left to the
565
- // machine searches (search/), which run whole histories.
705
+ // machine searches (research/search/), which run whole histories.
566
706
  export function concretize(goal, bound = 12) {
567
707
  const names = new Set();
568
708
  let reads = false;
@@ -604,16 +744,15 @@ export function concretize(goal, bound = 12) {
604
744
  }
605
745
  return undefined;
606
746
  }
747
+ // The facts a goal needs, as hypotheses: each fact about every key at the keys the goal reads a function at (and at
748
+ // the keys those facts read, a second round), each plain fact as it is.
749
+ export function factsFor(m, goal) {
750
+ return assumed(m.facts ?? [], instancesFor(m, [], inst => [{ ...goal, hyps: [...assumed(m.facts ?? [], inst), ...goal.hyps] }]));
751
+ }
607
752
  // A goal built from its instances (an exporter's goal, shaped as its Lean twin builds it): the every-key laws, the
608
753
  // quantified flags and the facts assumed at every key it reads a map or function at, then a tree, checked.
609
754
  export function proveBuilt(m, laws, build) {
610
- let instances = {};
611
- for (let round = 0; round < 2; round++) {
612
- const g = build(instances), keys = mapKeys([g]);
613
- instances = { ...withReads(laws, instances, keys, []), ...flagInstances(m, keys) };
614
- if (m.facts?.length)
615
- instances = withFacts(m, instances, fnKeys(m, [build(instances)]));
616
- }
755
+ const instances = instancesFor(m, laws, inst => [build(inst)]);
617
756
  const goal = build(instances), out = grow(goal);
618
757
  if (!('tree' in out) || !check(goal, out.tree))
619
758
  return { proven: false };
@@ -57,16 +57,6 @@ export declare function mmachineToLean(id: string, m: Machine, laws: Law[], opts
57
57
  progress?: ProgressCase[];
58
58
  free?: string[];
59
59
  stepLaws?: StepLaw[];
60
- liveness?: Liveness[];
61
- ghosts?: string[];
62
- after?: (ix: (name: string) => number) => string;
63
- refined?: {
64
- impl: Machine;
65
- spec: Machine;
66
- wit: Record<string, E>;
67
- stutter: C;
68
- specPre: number;
69
- };
70
60
  }): string;
71
61
  export declare const hasSets: (m: Machine) => boolean;
72
62
  export declare function smachineToLean(id: string, m: Machine, laws: Law[], opts?: {
@@ -78,6 +68,8 @@ export declare function smachineToLean(id: string, m: Machine, laws: Law[], opts
78
68
  };
79
69
  simp?: string[];
80
70
  free?: string[];
71
+ ghosts?: string[];
72
+ after?: (ix: (name: string) => number) => string;
81
73
  refined?: {
82
74
  impl: Machine;
83
75
  spec: Machine;