gdp-wired 0.18.0 → 0.19.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,6 +1,7 @@
1
- import { substC, substE } from "../kernel/kernel.js";
1
+ import { assumed, boundOf, foldObligations, obligations, substC, substE, witnessesOf } from "../kernel/kernel.js";
2
2
  import { baseHyps, descentGoal, instHyps, mainGoal, premiseGoal, refuseCyclic } from "../kernel/cyclic.js";
3
3
  import { cellProducts, grow, profile, usedHyps, widen } from "../kernel/prove.js";
4
+ import { carry, decide, readsFirst, renamer, translate, tstats } from "./translate.js";
4
5
  const v = (name) => ({ op: 'var', name });
5
6
  const k = (value) => ({ op: 'const', value });
6
7
  const add = (a, b) => ({ op: 'add', a, b });
@@ -21,7 +22,7 @@ export const pair = (u, key) => add(u, { op: 'prod', a: key, b: omega });
21
22
  // descent, the kernel's check): ms, goals, hypotheses, solver and row ms.
22
23
  export const cyclicProf = {};
23
24
  const profOn = () => env()?.GDP_CYCLIC_PROF === '1';
24
- function timedKind(kind, hyps, f) {
25
+ export function timedKind(kind, hyps, f) {
25
26
  if (!profOn())
26
27
  return f();
27
28
  const was = profile.on, s0 = profile.solveMs, r0 = profile.rowsMs, t0 = performance.now();
@@ -141,17 +142,24 @@ export function slice(g, whole, hypsOnly = false) {
141
142
  }
142
143
  return { name: g.name, hyps, from };
143
144
  }
145
+ const withProducts = (f) => { const was = cellProducts.on; cellProducts.on = true; try {
146
+ return f();
147
+ }
148
+ finally {
149
+ cellProducts.on = was;
150
+ } };
144
151
  let sliceNow = false;
145
152
  const sliceOn = () => sliceNow && env()?.GDP_CYCLIC_SLICE !== '0';
146
153
  const afterOn = () => env()?.GDP_CYCLIC_AFTER !== '0';
147
154
  const stepNodesOn = () => env()?.GDP_CYCLIC_STEP !== '0';
148
- // Trees already grown, by their goal (a node met again: a recurrence's lemmas in its laws between, a machine proven again),
149
- // with the facts and uses a node's own goal kept.
150
- const grownBefore = new Map();
155
+ const searched = new Map();
156
+ export const translatedCache = new Map();
151
157
  // `order`: the order the nodes are grown in, by default those with a weak variant first (a variant that uses a weak node
152
158
  // is passed over).
153
- export function proveCyclic(facts, specs, order = [...specs.keys()].sort((i, j) => Number(!specs[i].variants?.some(x => x.weak)) - Number(!specs[j].variants?.some(x => x.weak)))) {
154
- const nodes = [], shapes = [...specs], weak = new Set();
159
+ // `made`: the first nodes already made (a proof of the same facts whose nodes these specs begin with), taken as they are.
160
+ export function proveCyclic(facts, specs, order0, grownBefore = searched, st = tstats(), made = []) {
161
+ const order = (order0 ?? [...specs.keys()].sort((i, j) => Number(!specs[i].variants?.some(x => x.weak)) - Number(!specs[j].variants?.some(x => x.weak)))).filter(i => i >= made.length);
162
+ const nodes = [...made], shapes = [...specs], weak = new Set();
155
163
  let later, refuted = false;
156
164
  // A goal's tree, and the hypotheses its leaves read (of the whole goal, a weaker goal's read through where they came from).
157
165
  const grown = (g, node, own) => {
@@ -179,12 +187,6 @@ export function proveCyclic(facts, specs, order = [...specs.keys()].sort((i, j)
179
187
  cellProducts.on = was;
180
188
  }
181
189
  };
182
- const withProducts = (f) => { const was = cellProducts.on; cellProducts.on = true; try {
183
- return f();
184
- }
185
- finally {
186
- cellProducts.on = was;
187
- } };
188
190
  // The tree made the goal's own (a weaker goal's widened), or grown on the goal itself where that fails.
189
191
  const own = (r, g) => (r.from === g ? r.tree : timedKind('widen', 0, () => widen(r.from, g, r.tree, opaqueOf.get(r.from))));
190
192
  const whole = (g, node, mine) => {
@@ -222,15 +224,23 @@ export function proveCyclic(facts, specs, order = [...specs.keys()].sort((i, j)
222
224
  };
223
225
  // A node's goal grown; then the node keeps only the facts and uses its tree reads (the fewer, the fewer hypotheses its
224
226
  // goal has for the kernel to read at every leaf, and the fewer uses to prove below); then each use's goals.
225
- const attempt = ({ opaque, variants: _v, weak: _w, slice: sl, ...n0 }) => {
227
+ const attempt = ({ opaque, variants: _v, weak: _w, slice: sl, main: given0, carry: made, ...n0 }) => {
226
228
  hidden = opaque ?? [];
227
229
  sliceNow = !!sl;
228
230
  // A goal's own hypotheses: the node's, and those after its facts and uses.
229
231
  const n1 = { ...n0, insts: n0.insts.filter(p => facts[p.fact]) };
232
+ const given = given0 ?? (made ? timedKind('carry', 0, () => withProducts(() => carry(mainGoal(facts, shapes, n1), n1.insts.length, n1.uses.length, st, performance.now() + 2_000))) : undefined);
233
+ if (made && !given0) {
234
+ if (given)
235
+ st.carried++;
236
+ else
237
+ st.uncarried++;
238
+ }
230
239
  const base = baseHyps(facts, n1).length, mine = (tail) => (i) => i < n1.hyps.length || i >= base + tail;
231
- const g = mainGoal(facts, shapes, n1), key = JSON.stringify(g.hyps), before = grownBefore.get(key);
232
- let n = n1, main, src;
233
- if (before) {
240
+ const g = given ? undefined : mainGoal(facts, shapes, n1), key = g ? JSON.stringify(g.hyps) : '', before = g && grownBefore.get(key);
241
+ let n = n1, main = given, src;
242
+ if (!g) { /* the tree given */ }
243
+ else if (before) {
234
244
  n = { ...n1, insts: before.insts?.map(i => n1.insts[i]) ?? n1.insts, uses: before.uses?.map(j => n1.uses[j]) ?? n1.uses };
235
245
  main = before.tree;
236
246
  src = { tree: main, from: mainGoal(facts, shapes, n) };
@@ -333,7 +343,9 @@ export function proveCyclic(facts, specs, order = [...specs.keys()].sort((i, j)
333
343
  const sliced = (specs) => specs.map(sp => ({ ...sp, slice: true, ...(sp.variants ? { variants: sp.variants.map(x => ({ ...x, slice: true })) } : {}) }));
334
344
  // The facts of a machine's functions (laws about every key of them), as cyclic facts.
335
345
  const factOf = (l) => ({ name: l.name, keys: [...(l.forall ? [l.forall] : []), ...(l.keys ?? [])], holds: l.holds });
336
- export function recurNodes(r) {
346
+ // `usual`: the usual proofs of the lemmas (the recurrence run as a machine from x = 0) and of the laws between (y from x),
347
+ // translated (`translatedRecur` below), or no nodes with why not.
348
+ export function recurNodes(r, usual, st = tstats()) {
337
349
  const f = `${r.name}$`, x = v('x'), y = v('y'), kk = v('k$');
338
350
  const facts = [
339
351
  { name: `${r.name}(0)`, keys: [], holds: eq(get(f, k(0)), r.zero) },
@@ -379,6 +391,8 @@ export function recurNodes(r) {
379
391
  variants: [{ ...sp, rank: [...sp.rank, k(1)] }] };
380
392
  return a;
381
393
  });
394
+ if (usual)
395
+ return { facts, ...translatedRecur() };
382
396
  const all = withReads(facts, [...own, ...lemmaAfter, ...betweenAfter]);
383
397
  for (const [i, a] of all.slice(0, n + b).entries()) {
384
398
  const o = shared(facts, a, all[n + b + i].concl);
@@ -386,6 +400,106 @@ export function recurNodes(r) {
386
400
  all[i] = { ...a, opaque: o };
387
401
  }
388
402
  return { facts, specs: sliced(all.map(sp => (sp.variants ? { ...sp, variants: withReads(facts, sp.variants, all) } : sp))) };
403
+ // The usual proofs translated, as a machine's are: each lemma and law between three nodes. At x (at y), as above, its
404
+ // goal grown: from f(0) or the lemma at the start (x <= 0), or after a step to x (1 <= x). After a step, rank (0, x, 0):
405
+ // the usual step goal with x as x − 1 and fx as f(x − 1), the lemmas at x − 1 its uses; a law between likewise at y − 1
406
+ // (rank (1, y, 0)), fx as f(x), its uses the lemmas at x (the usual machine's facts) and at y − 1 and the laws between
407
+ // at y − 1 (each antecedent x <= y − 1 split first). At the start, rank (0, 0, 0) and (1, 0, 0): the usual start goal,
408
+ // fx0 as f(0) (a start that reads a function), fx as f(x).
409
+ function translatedRecur() {
410
+ const lemmaAfterT = [], betweenAfterT = [], lemmaStart = [], betweenStart = [];
411
+ const made = [], N = 2 * n + 2 * b;
412
+ const factIx = (name) => facts.findIndex((q, i) => i >= 2 && q.name === name);
413
+ // A usual goal's hypotheses: the machine's facts at the instances, its precondition and assumption, the laws assumed.
414
+ const layout = (u, i) => {
415
+ const g = obligations(u.m, u.laws, u.proof.instances, i), inst = u.proof.instances[u.laws[i].name] ?? {};
416
+ const fs = (u.m.facts ?? []).flatMap(q => { const ks = [...(q.forall ? [q.forall] : []), ...(q.keys ?? [])], flat = inst[q.name] ?? []; return ks.length ? Array.from({ length: Math.floor(flat.length / ks.length) }, (_, j) => ({ fact: q, at: flat.slice(j * ks.length, (j + 1) * ks.length) })) : [{ fact: q, at: [] }]; });
417
+ const np = u.m.pre ? 1 : 0, na = u.m.assume ? 1 : 0;
418
+ return { g0: g[0], g1: g[1], t0: u.proof.trees[g[0].name], t1: u.proof.trees[g[1].name], fs, np, na, ok: g[0].hyps.length === fs.length + np + 1 && g[1].hyps.length === fs.length + np + na + u.laws.length + 1 };
419
+ };
420
+ const last = (g) => g.hyps.at(-1).x;
421
+ if (n) {
422
+ const u = usual.lemmas;
423
+ if (!u || u.laws.map(l => l.name).join('\0') !== r.lemmas.map(l => l.name).join('\0') || u.laws.some(l => l.forall))
424
+ return { specs: [], why: 'lemmas: no usual proof of these lemmas' };
425
+ const sigma = vars({ x: less1(x), fx: get(f, less1(x)) }), sigma0 = vars({ fx0: get(f, k(0)) }), ren = renamer({ s: sigma }), ren0 = renamer({ s: sigma0 });
426
+ for (const [i] of r.lemmas.entries()) {
427
+ const lay = layout(u, i);
428
+ if (!lay.ok || !lay.t0 || !lay.t1)
429
+ return { specs: [], why: `${r.lemmas[i].name}: a usual goal of another shape` };
430
+ const used1 = usedHyps(lay.g1, lay.t1), used0 = usedHyps(lay.g0, lay.t0), insts1 = [], insts0 = [];
431
+ for (const [q, p] of lay.fs.entries()) {
432
+ const zeroFact = p.fact.name.startsWith('fx0 is '), ix = zeroFact ? 0 : factIx(p.fact.name);
433
+ for (const [used, out, s] of [[used1, insts1, sigma], [used0, insts0, sigma0]]) {
434
+ if (!used.has(q))
435
+ continue;
436
+ if (ix < 0 || (zeroFact && s === sigma))
437
+ return { specs: [], why: `${p.fact.name}: no fact of the recurrence's` };
438
+ out.push({ fact: ix, at: p.at.map(e => substE(e, s)) });
439
+ }
440
+ }
441
+ const pre = u.m.pre ? [substC(u.m.pre, sigma)] : [];
442
+ lemmaAfterT.push({ name: `${r.name}: ${r.lemmas[i].name}, after a step`, hyps: [le(k(1), x), ...pre, ...(u.m.assume ? [substC(u.m.assume, sigma)] : [])], concl: substC(last(lay.g1), sigma), rank: [k(0), x, k(0)],
443
+ insts: insts1, uses: lemmaUses(less1(x), yes).filter((_, j) => used1.has(lay.fs.length + lay.np + lay.na + j)) });
444
+ lemmaStart.push({ name: `${r.name}: ${r.lemmas[i].name}, at the start`, hyps: u.m.pre ? [u.m.pre] : [], concl: substC(last(lay.g0), sigma0), rank: [k(0), k(0), k(0)], insts: insts0, uses: [] });
445
+ made.push({ g: lay.g1, tree: lay.t1, node: n + b + i, ren }, { g: lay.g0, tree: lay.t0, node: N + i, ren: ren0 });
446
+ }
447
+ }
448
+ if (b) {
449
+ const u = usual.between, lemmaAt = (name) => r.lemmas.findIndex(l => `${l.name} at x` === name);
450
+ if (!u || u.laws.slice(n).map(l => l.name).join('\0') !== r.between.map(l => l.name).join('\0') || u.laws.some(l => l.forall))
451
+ return { specs: [], why: 'between: no usual proof of these laws' };
452
+ const sigma = vars({ y: less1(y), fy: get(f, less1(y)), fx: get(f, x) }), sigma0 = vars({ fx: get(f, x) }), ren = renamer({ s: sigma }), ren0 = renamer({ s: sigma0 });
453
+ for (const [i] of r.between.entries()) {
454
+ const lay = layout(u, n + i);
455
+ if (!lay.ok || !lay.t0 || !lay.t1)
456
+ return { specs: [], why: `${r.between[i].name}: a usual goal of another shape` };
457
+ const used1 = usedHyps(lay.g1, lay.t1), used0 = usedHyps(lay.g0, lay.t0);
458
+ const parts = (used, s, after) => {
459
+ const insts = [], uses = [];
460
+ for (const [q, p] of lay.fs.entries()) {
461
+ if (!used.has(q))
462
+ continue;
463
+ const j = lemmaAt(p.fact.name), ix = factIx(p.fact.name);
464
+ if (j >= 0)
465
+ uses.push({ node: j, subst: { x }, guard: yes });
466
+ else if (ix >= 0)
467
+ insts.push({ fact: ix, at: p.at.map(e => substE(e, s)) });
468
+ else
469
+ return undefined;
470
+ }
471
+ if (after)
472
+ for (const [j] of u.laws.entries())
473
+ if (used.has(lay.fs.length + lay.np + lay.na + j))
474
+ uses.push(j < n ? { node: j, subst: { x: less1(y) }, guard: yes } : { node: j, subst: { y: less1(y) }, guard: yes });
475
+ return { insts, uses };
476
+ };
477
+ const p1 = parts(used1, sigma, true), p0 = parts(used0, sigma0, false);
478
+ if (!p1 || !p0)
479
+ return { specs: [], why: `${r.between[i].name}: a fact of no recurrence's` };
480
+ const pre = u.m.pre ? [substC(u.m.pre, sigma)] : [];
481
+ betweenAfterT.push({ name: `${r.name}: ${r.between[i].name}, after a step`, hyps: [le(add(x, k(1)), y), ...pre, ...(u.m.assume ? [substC(u.m.assume, sigma)] : [])], concl: substC(last(lay.g1), sigma), rank: [k(1), y, k(0)], ...p1 });
482
+ betweenStart.push({ name: `${r.name}: ${r.between[i].name}, at the start`, hyps: u.m.pre ? [u.m.pre] : [], concl: substC(last(lay.g0), sigma0), rank: [k(1), k(0), k(0)], ...p0 });
483
+ made.push({ g: lay.g1, tree: lay.t1, node: 2 * n + b + i, ren, decide: p1.uses.flatMap(us => (us.node >= n && us.node < n + b ? [le(x, less1(y))] : [])) }, { g: lay.g0, tree: lay.t0, node: N + n + i, ren: ren0 });
484
+ }
485
+ }
486
+ const carried = own.map((sp, i) => ({ ...sp, variants: undefined, carry: true, uses: [...sp.uses, i < n ? { node: N + i, subst: {}, guard: le(x, k(0)) } : { node: N + i, subst: {}, guard: le(y, x) }] }));
487
+ const rest = [...lemmaAfterT, ...betweenAfterT, ...lemmaStart, ...betweenStart], all = [...withReads(facts, carried, [...carried, ...rest]), ...rest];
488
+ for (const [i, a] of all.slice(0, n + b).entries()) {
489
+ const o = [...new Set([...shared(facts, a, all[n + b + i].concl), ...shared(facts, a, all[N + i].concl)].map(e => JSON.stringify(e)))].map(e => JSON.parse(e));
490
+ if (o.length)
491
+ all[i] = { ...a, opaque: o };
492
+ }
493
+ const until = performance.now() + 10_000;
494
+ for (const m of made) {
495
+ const whole = mainGoal(facts, all, all[m.node]);
496
+ const tree = timedKind('translate', 0, () => withProducts(() => decide(whole, m.decide ?? [], wp => translate(m.g, whole, m.tree, m.ren, st, until, new Map(), wp), st, until)));
497
+ if (!tree)
498
+ return { specs: [], why: `${all[m.node].name}: not translated` };
499
+ all[m.node] = { ...all[m.node], main: tree };
500
+ }
501
+ return { specs: sliced(all) };
502
+ }
389
503
  }
390
504
  // Function facts at the keys a node's goal reads them at: each fact about every key of a function read f[k] (the key
391
505
  // alone) at each key f is read at, twice over (a fact's own reads give more keys).
@@ -426,7 +540,9 @@ function withReads(facts, specs, nodes = specs) {
426
540
  // read from a map of the state (a table of nodes: a node's fold at its child), whose cells a run's maps put at keys
427
541
  // the prover would have to prove equal through products (`machineNodes` proves those laws far slower than the usual
428
542
  // rule, past its budget: research/cyclic/probe.ts).
429
- export const cyclicMachine = (m, laws = []) => {
543
+ export const cyclicMachine = (m, laws = []) => uncoveredWhy(m, laws) === undefined;
544
+ // Why a machine's laws are no instance's (undefined where they are one).
545
+ export const uncoveredWhy = (m, laws = []) => {
430
546
  const maps = Object.keys(m.maps ?? {});
431
547
  const nested = (t) => { let found = false; const walk = (x, inKey) => { if (found || !x || typeof x !== 'object')
432
548
  return; const o = x; if (o.op === 'get' && inKey && maps.includes(o.map)) {
@@ -437,7 +553,7 @@ export const cyclicMachine = (m, laws = []) => {
437
553
  return;
438
554
  } for (const val of Object.values(o))
439
555
  walk(val, inKey); }; walk(t, false); return found; };
440
- return !m.history && !Object.values(m.step.flags ?? {}).some(c => c.op === 'all') && !laws.some(l => nested(l.holds));
556
+ return m.history ? 'a history' : Object.values(m.step.flags ?? {}).some(c => c.op === 'all') ? 'a flag at every key' : laws.some(l => nested(l.holds)) ? 'a law reading a map at a key read from a map' : undefined;
441
557
  };
442
558
  // The run as maps over time: names at time u read as their maps there.
443
559
  function trace(m) {
@@ -470,7 +586,8 @@ const flagsOf = (m) => {
470
586
  out[f] = substC(c, { vars: {}, maps: {}, flags: out });
471
587
  return out;
472
588
  };
473
- export function machineNodes(m, laws, instances) {
589
+ // `lawsMade`: the laws' nodes are made already (their trees not translated again).
590
+ export function machineNodes(m, laws, instances, usual, st = tstats()) {
474
591
  const { at } = trace(m), flags = flagsOf(m), inline = (t) => substC(t, { vars: {}, maps: {}, flags });
475
592
  const t = v('t$'), T = v('T$'), u = v('u$'), key = v('k$');
476
593
  const inlineE = (e) => substE(e, { vars: {}, maps: {}, flags });
@@ -635,7 +752,161 @@ export function machineNodes(m, laws, instances) {
635
752
  insts: [...run(t), ...factInsts, ...base, ...state.map(st => ({ fact: fi(`${m.name}: ${st} is a natural`), at: [t] })), { fact: fi(`${m.name}: params are naturals`), at: [] }], uses,
636
753
  };
637
754
  });
755
+ if (usual)
756
+ return { facts, ...translatedNodes() };
638
757
  return { facts, specs: sliced(stepNodesOn() ? stepNodes(specs) : specs) };
758
+ // The usual proof translated (core/translate.ts): each law three nodes. At t, rank (t, 1), as above (Bridge.lean's
759
+ // cycMOk reads it), its goal grown: it carries the law at the start or after t's step to the cells at t through the
760
+ // start's and the step's equations. After a step from t − 1, rank (t, 0): the usual step goal over the run's cells at
761
+ // t − 1 (each hypothesis of it read so: the functions' facts at the keys the usual proof names, the precondition, the
762
+ // request's assumption, every law at t − 1 at those keys, each a use guarded by 0 <= 0, its descent from 1 <= t), its
763
+ // tree the usual one's renamed. At the start, rank (0, 0): the usual start goal (the law at the start's values), the
764
+ // law's fresh key k# its own k, its tree the usual one's. Only the facts and uses the usual tree reads are kept.
765
+ function translatedNodes() {
766
+ const L = laws.length, pre = at(less1(t)), one = le(k(1), t), omegaHyp = mapNames.length ? [{ op: '<', a: T, b: omega }] : [];
767
+ const fresh = Object.fromEntries(laws.flatMap(x => (x.forall ? [[`${x.forall}#`, v(x.forall)]] : [])));
768
+ const sigma = { vars: { ...pre.vars, ...fresh }, maps: pre.maps, flags: {} };
769
+ const ren = renamer({ s: sigma, cells: Object.fromEntries([...mapNames, ...(m.inputMaps ?? [])].map(map => [map, { to: `${map}@`, at: less1(t) }])) }), ren0 = renamer({ s: vars(fresh) });
770
+ const fnInsts = (inst, frame) => (m.facts ?? []).flatMap(f => {
771
+ const ks = keysOf(f), flat = inst[f.name] ?? [], fact = fi(f.name);
772
+ if (!ks.length)
773
+ return [{ fact, at: [] }];
774
+ return Array.from({ length: Math.floor(flat.length / ks.length) }, (_, i) => ({ fact, at: flat.slice(i * ks.length, (i + 1) * ks.length).map(e => substE(substE(e, vars(fresh)), frame)) }));
775
+ });
776
+ const goals = new Map(obligations(m, laws, instances).map(g => [g.name, g]));
777
+ // The state the step gives over the frame before it, a write's reads at its key read at the key read instead (as the
778
+ // map's step fact has them): the usual step goal's conclusion but for those reads, which the translation reads at
779
+ // the key read where the usual path has the two keys equal (two cells over time then split apart: `close`).
780
+ const upd = { flags: {}, vars: Object.fromEntries(state.map(st2 => [st2, inlineE(m.step.state?.[st2] ?? v(st2))])), maps: Object.fromEntries(mapNames.map(map => [map, (kk) => {
781
+ const w = m.step.maps?.[map];
782
+ return !w ? get(map, kk) : 'each' in w ? substE(inlineE(w.value), vars({ [w.each]: kk })) : { op: 'if', c: eq(kk, inlineE(w.key)), then: readsAt(inlineE(w.value), inlineE(w.key), kk), else: get(map, kk) };
783
+ }])) };
784
+ // A step law's condition with its primed names read as given, its flags inlined (over the frame before the step).
785
+ const primedOf = (c, s0) => substC(c, { ...s0, flags });
786
+ const nows = [], afters = [], starts = [], made = [];
787
+ // A write's key and the keys the usual conclusion reads its map at (each a condition if(K = W, ...)).
788
+ const wkeys = new Set(mapNames.flatMap(map => { const w = m.step.maps?.[map]; return w && !('each' in w) ? [JSON.stringify(inlineE(w.key))] : []; }));
789
+ const readsOf = (c) => {
790
+ const out = [], seen = new Set();
791
+ const walk = (x) => {
792
+ if (!x || typeof x !== 'object')
793
+ return;
794
+ const o = x;
795
+ if (o.op === 'if' && o.c.op === '=' && wkeys.has(JSON.stringify(o.c.b))) {
796
+ const id = JSON.stringify([o.c.a, o.c.b]);
797
+ if (!seen.has(id)) {
798
+ seen.add(id);
799
+ out.push({ k: o.c.a, w: o.c.b });
800
+ }
801
+ }
802
+ for (const val of Object.values(o))
803
+ walk(val);
804
+ };
805
+ walk(c);
806
+ return out;
807
+ };
808
+ for (const [i, l] of laws.entries()) {
809
+ const g0 = goals.get(`${l.name} @start`), g1 = goals.get(`${l.name} @step`), t0 = g0 && usual.trees[g0.name], t1 = g1 && usual.trees[g1.name];
810
+ if (!g0 || !g1 || !t0 || !t1)
811
+ return { specs: [], why: `${l.name}: no usual tree` };
812
+ const inst = instances[l.name] ?? {}, nf = assumed(m.facts ?? [], inst).length, np = m.pre ? 1 : 0, na = m.assume ? 1 : 0;
813
+ const uses = laws.flatMap((l2, j) => {
814
+ const ks = keysOf(l2);
815
+ if (!ks.length)
816
+ return [{ node: j, subst: { 't$': less1(t) }, guard: yes }];
817
+ const flat = inst[l2.name] ?? [];
818
+ return Array.from({ length: Math.floor(flat.length / ks.length) }, (_, q) => ({ node: j, subst: { 't$': less1(t), ...Object.fromEntries(ks.map((kn, r) => [kn, substE(flat[q * ks.length + r], sigma)])) }, guard: yes }));
819
+ });
820
+ const fn1 = fnInsts(inst, pre), fn0 = fnInsts(inst, vars({}));
821
+ if (g1.hyps.length !== nf + np + na + uses.length + 1 || g0.hyps.length !== nf + np + 1 || fn1.length !== nf)
822
+ return { specs: [], why: `${l.name}: a usual goal of another shape` };
823
+ const used1 = usedHyps(g1, t1), used0 = usedHyps(g0, t0), last = (g) => g.hyps.at(-1).x;
824
+ const concl = substC(l.holds, at(t));
825
+ afters.push({ name: `${m.name}: ${l.name}, after a step`, hyps: [...(m.pre ? [m.pre] : []), one, le(t, T), ...omegaHyp, ...(m.assume ? [now(inline(m.assume), less1(t))] : [])],
826
+ concl: substC(substC(substC(l.holds, vars(fresh)), upd), pre), rank: [t, k(0)], insts: [...fn1.filter((_, q) => used1.has(q))], uses: uses.filter((_, j) => used1.has(nf + np + na + j)) });
827
+ starts.push({ name: `${m.name}: ${l.name}, at the start`, hyps: m.pre ? [m.pre] : [], concl: substC(last(g0), vars(fresh)), rank: [k(0), k(0)], insts: [...fn0.filter((_, q) => used0.has(q))], uses: [] });
828
+ nows.push({ name: `${m.name}: ${l.name}`, hyps: [...(m.pre ? [m.pre] : []), le(t, T), ...omegaHyp], concl, rank: [t, k(1)], insts: [...run(t), ...mapInsts(concl)],
829
+ uses: [{ node: L + i, subst: {}, guard: one }, { node: 2 * L + i, subst: {}, guard: le(t, k(0)) }], carry: true });
830
+ made.push({ g: g1, tree: t1, node: L + i, ren, decide: [one], reads: readsOf(last(g1)) }, { g: g0, tree: t0, node: 2 * L + i, ren: ren0 });
831
+ }
832
+ // One-step step laws (`usual.steps`), each two nodes after the laws' (core/cyclic.ts's header has their shape): after a
833
+ // step from t − 1, rank (t, 0), the usual step law's goal over the run's cells at t − 1 (its `when` and the request's
834
+ // assumption hypotheses, the laws at t − 1 at its keys uses), the state after it the step's terms; and the step law
835
+ // itself, rank (t, 2), over the cells at t − 1 and at t (a primed name at t), carried from the first by the step's
836
+ // facts at t, its hypotheses the precondition, 1 <= t <= T, T < Ω and `when` at t − 1.
837
+ const stepSpecs = [];
838
+ for (const [q, u] of (usual.steps ?? []).entries()) {
839
+ const { sl, inst, goal: g, tree } = u, whens = sl.when === undefined ? [] : Array.isArray(sl.when) ? sl.when : [sl.when], K = sl.steps ?? 1;
840
+ // (k steps: a machine with no map, the state after each step a cell at its time; one step: any the instances cover)
841
+ if (K > 1 && (mapNames.length || m.inputMaps?.length))
842
+ return { specs: [], why: `${sl.name}: steps over a map` };
843
+ // The times the steps start from (t − k, ..., t − 1), and the frames: the state before step j its cells at t − k + j − 1
844
+ // (n~j the state after step j, at t − k + j), its request read there (i@j).
845
+ const time = (back) => (back === 0 ? t : { op: 'sub', a: t, b: k(back) });
846
+ const frame = { vars: { ...at(time(K)).vars, ...Object.fromEntries(Array.from({ length: K }, (_, j) => j + 1).flatMap(j => [
847
+ ...(j >= 2 ? m.inputs.map(i => [`${i}@${j}`, get(`${i}@`, time(K - j + 1))]) : []),
848
+ ...(j < K ? state.map(n => [`${n}~${j}`, get(`${n}@`, time(K - j))]) : []),
849
+ ])) }, maps: pre.maps, flags: {} };
850
+ const sig = K === 1 ? sigma : frame, renK = K === 1 ? ren : renamer({ s: frame });
851
+ // (the usual goal's hypotheses: the precondition, the facts at the instances, then each step's laws, assumption and
852
+ // `when`, and after each step but the last its state's equations)
853
+ const nf = assumed(m.facts ?? [], inst).length, np = m.pre ? 1 : 0, na = m.assume ? 1 : 0, nl = assumed(laws, inst).length;
854
+ const lawsAt = (back) => laws.flatMap((l2, j) => {
855
+ const ks = keysOf(l2), tb = time(back), ab = at(tb);
856
+ if (!ks.length)
857
+ return [{ node: j, subst: { 't$': tb }, guard: yes }];
858
+ const flat = inst[l2.name] ?? [];
859
+ return Array.from({ length: Math.floor(flat.length / ks.length) }, (_, r) => ({ node: j, subst: { 't$': tb, ...Object.fromEntries(ks.map((kn, c) => [kn, substE(flat[r * ks.length + c], ab)])) }, guard: yes }));
860
+ });
861
+ const perStep = Array.from({ length: K }, (_, j) => lawsAt(K - j)), fn1 = fnInsts(inst, sig);
862
+ // Each step's block: its laws, its assumption, its when, then (but after the last) its state's equations.
863
+ const blocks = [];
864
+ let pos = np + nf;
865
+ for (let j = 0; j < K; j++) {
866
+ blocks.push(pos);
867
+ pos += nl + na + (whens[j] ? 1 : 0) + (j < K - 1 ? state.length : 0);
868
+ }
869
+ if (g.hyps.length !== pos + 1 || fn1.length !== nf || perStep.some(us => us.length !== nl))
870
+ return { specs: [], why: `${sl.name}: a usual goal of another shape` };
871
+ const used = usedHyps(g, tree), last = g.hyps.at(-1).x;
872
+ const assumes = Array.from({ length: K }, (_, j) => (m.assume ? [now(inline(m.assume), time(K - j))] : [])).flat();
873
+ const when = whens.map((w, j) => now(inline(w), time(K - j)));
874
+ const bounds = [...(m.pre ? [m.pre] : []), le(k(K), t), le(t, T), ...omegaHyp];
875
+ const after = K === 1 ? substC(primedOf(sl.then, { vars: Object.fromEntries(state.map(st2 => [`${st2}'`, inlineE(m.step.state?.[st2] ?? v(st2))])), maps: Object.fromEntries(mapNames.map(map => [`${map}'`, upd.maps[map]])), flags: {} }), pre) : substC(last, frame);
876
+ const later = Object.fromEntries(Array.from({ length: K - 1 }, (_, j) => j + 2).flatMap(j => m.inputs.map(i => [`${i}@${j}`, get(`${i}@`, time(K - j + 1))])));
877
+ const cells = substC(primedOf(sl.then, { vars: { ...later, ...Object.fromEntries(state.map(st2 => [`${st2}'`, get(`${st2}@`, t)])) }, maps: Object.fromEntries(mapNames.map(map => [`${map}'`, (kk) => get(`${map}@`, pair(t, kk))])), flags: {} }), at(time(K)));
878
+ const a = 3 * L + 2 * q, steps = Array.from({ length: K - 1 }, (_, j) => ({ fact: 1, at: [time(K - j - 1)] }));
879
+ // (the state after each step but the last its equations, hypotheses here as in the usual goal: the step's facts at
880
+ // those times give them where the step law is, `steps` below)
881
+ const eqs = blocks.slice(0, K - 1).flatMap((b0, j) => g.hyps.slice(b0 + nl + na + (whens[j] ? 1 : 0), b0 + nl + na + (whens[j] ? 1 : 0) + state.length).map(h => substC(h, frame)));
882
+ stepSpecs.push({ name: `${m.name}: ${sl.name}, after ${K === 1 ? 'a step' : `${K} steps`}`, hyps: [...bounds, ...assumes, ...when, ...eqs], concl: after, rank: [t, k(0)],
883
+ insts: fn1.filter((_, r) => used.has(np + r)), uses: perStep.flatMap((us, j) => us.filter((_, r) => used.has(blocks[j] + r))) });
884
+ stepSpecs.push({ name: `${m.name}: ${sl.name}`, hyps: [...bounds, ...when], concl: cells, rank: [t, k(2)],
885
+ insts: [...run(t), ...steps, ...mapInsts(cells)], uses: [{ node: a, subst: {}, guard: yes }], carry: true });
886
+ // (each time decided at least the steps before it first: t ∸ j is then t − j)
887
+ made.push({ g, tree, node: a, ren: renK, decide: Array.from({ length: K }, (_, j) => le(k(j + 1), t)), reads: K === 1 ? readsOf(last) : [] });
888
+ }
889
+ const all = [...nows, ...afters, ...starts, ...stepSpecs];
890
+ for (const [i, a] of nows.entries()) {
891
+ const o = [...new Set([...shared(facts, a, afters[i].concl), ...shared(facts, a, starts[i].concl)].map(e => JSON.stringify(e)))].map(e => JSON.parse(e));
892
+ if (o.length)
893
+ a.opaque = o;
894
+ }
895
+ // (a translation is bounded by a minute: a step law over 32 steps has a goal of 32 frames at every leaf)
896
+ const until = performance.now() + 60_000;
897
+ for (const x of made) {
898
+ if (usual.lawsMade && x.node < 3 * L)
899
+ continue;
900
+ // (after a step, 1 <= t decided first: t ∸ 1 is then t − 1, and a cell read at a key over the cells at t − 1 has a
901
+ // linear form)
902
+ const whole = mainGoal(facts, all, all[x.node]);
903
+ const tree = timedKind('translate', 0, () => withProducts(() => decide(whole, x.decide ?? [], wp => readsFirst(whole, x.reads ?? [], x.ren, (p, swaps) => translate(x.g, whole, x.tree, x.ren, st, until, new Map(), p, swaps), st, until, wp), st, until)));
904
+ if (!tree)
905
+ return { specs: [], why: `${all[x.node].name}: not translated` };
906
+ all[x.node] = { ...all[x.node], main: tree };
907
+ }
908
+ return { specs: sliced(all) };
909
+ }
639
910
  }
640
911
  // The terms (not a read, a variable or a constant) of a conclusion that a node's facts read too, largest first; none
641
912
  // inside a product (a product of a term's linear form is no linear form).
@@ -676,10 +947,224 @@ function shared(facts, a, concl) {
676
947
  walk(concl);
677
948
  return out.sort((x, y) => JSON.stringify(y).length - JSON.stringify(x).length);
678
949
  }
950
+ // --- step laws and k-step laws over the run -----------------------------------------------------------------------
951
+ // A k-step law (or a step law, k = 1) as a node over the machine's run (BridgeRules.lean's cycKOk, cyc_kstep_sound
952
+ // carries it to the law): the run's last k steps up to time t, each time one step back from the next (t − 1, t − 1 − 1,
953
+ // …), as a step's fact reads the time before it, so the cells a step gives are those the next one reads. The first state
954
+ // at t − k, the j-th request at t − (k − j), the state after the last at t.
955
+ export const tBack = (n) => (n === 0 ? v('t$') : less1(tBack(n - 1)));
956
+ // The same times read t − n at once (BridgeRules.lean's tSub; core/translate.ts's step laws over several steps).
957
+ export const tSub = (n) => (n === 0 ? v('t$') : { op: 'sub', a: v('t$'), b: k(n) });
958
+ // `then` over the run (BridgeRules.lean's kσ and kRd): the primed names at t, each later request's inputs (input@j) and
959
+ // the maps it carries (map@j) at its time, the plain names at t − k.
960
+ export function kStepAt(m, k, time = tBack) {
961
+ const { at } = trace(m), first = at(time(k)), last = at(time(0));
962
+ const vars = { ...first.vars }, maps = { ...first.maps };
963
+ for (const x of Object.keys(m.state))
964
+ vars[`${x}'`] = last.vars[x];
965
+ for (const map of Object.keys(m.maps ?? {}))
966
+ maps[`${map}'`] = last.maps[map];
967
+ for (let j = 1; j < k; j++) {
968
+ const a = at(time(k - j));
969
+ for (const i of m.inputs)
970
+ vars[`${i}@${j + 1}`] = a.vars[i];
971
+ for (const c of m.inputMaps ?? [])
972
+ maps[`${c}@${j + 1}`] = a.maps[c];
973
+ }
974
+ return { vars, maps, flags: {} };
975
+ }
976
+ // A k-step law's `when`s, one per step (none: 0 <= 0), its flags inlined, as Lean's SKStep has them.
977
+ export const kWhens = (m, sl) => {
978
+ const ws = sl.when === undefined ? [] : Array.isArray(sl.when) ? sl.when : [sl.when], flags = flagsOf(m);
979
+ return Array.from({ length: sl.steps ?? 1 }, (_, j) => (ws[j] ? substC(ws[j], { vars: {}, maps: {}, flags }) : yes));
980
+ };
981
+ // The nodes of the step laws, after the laws' (`laws` the machine's, `lawNodes` their nodes, the first L at time t, as
982
+ // machineNodes makes them). Each node: the precondition, k <= t <= T, T < Ω and each step's `when` at its time;
983
+ // the run's facts at the times it reads (each step's, each map cell's step back from where it is read, the functions'
984
+ // facts at the keys the usual proof names), every law at each state but the last (at the keys it names), and `then`.
985
+ export function stepLawNodes(m, laws, steps, facts, lawNodes, time = tBack) {
986
+ const rank = lawNodes[0]?.rank.length ?? 2;
987
+ const { at } = trace(m), flags = flagsOf(m), inline = (c) => substC(c, { vars: {}, maps: {}, flags });
988
+ const t = v('t$'), T = v('T$'), mapNames = Object.keys(m.maps ?? {}), traced = [...mapNames, ...(m.inputMaps ?? [])], state = Object.keys(m.state);
989
+ const fi = (name) => facts.findIndex(f => f.name === name);
990
+ const keysOf = (l) => [...(l.forall ? [l.forall] : []), ...(l.keys ?? [])];
991
+ const tuples = (inst, l) => { const ks = keysOf(l), flat = inst[l.name] ?? []; return ks.length ? Array.from({ length: Math.floor(flat.length / ks.length) }, (_, i) => flat.slice(i * ks.length, (i + 1) * ks.length)) : [[]]; };
992
+ const specs = steps.map(({ law: sl, instances: inst }) => {
993
+ const K = sl.steps ?? 1, times = Array.from({ length: K + 1 }, (_, j) => time(K - j));
994
+ const hyps = [...(m.pre ? [m.pre] : []), le(k(K), t), le(t, T), ...(traced.length ? [{ op: '<', a: T, b: omega }] : []),
995
+ ...kWhens(m, sl).map((w, j) => substC(w, at(times[j])))];
996
+ const concl = substC(inline(sl.then), kStepAt(m, K, time));
997
+ // Every law at each state but the last, at the keys the proof names there.
998
+ const uses = times.slice(0, K).flatMap(u => laws.flatMap((l, i) => tuples(inst, l).map(ts => ({
999
+ node: i, subst: { 't$': u, ...Object.fromEntries(keysOf(l).map((kn, q) => [kn, substE(ts[q], at(u))])) }, guard: yes
1000
+ }))));
1001
+ const insts = [
1002
+ ...times.slice(1).map(u => ({ fact: fi(`${m.name}: a step`), at: [u] })),
1003
+ ...times.flatMap(u => state.map(st => ({ fact: fi(`${m.name}: ${st} is a natural`), at: [u] }))),
1004
+ { fact: fi(`${m.name}: params are naturals`), at: [] },
1005
+ ...(m.facts ?? []).flatMap(f => tuples(inst, f).map(ts => ({ fact: fi(f.name), at: ts.map(e => substE(e, at(times[0]))) }))),
1006
+ ];
1007
+ // Each map cell the node reads at a time after the first: that map's step there, back to the time before, and so on.
1008
+ const seen = new Set(), ix = new Map(times.map((u, j) => [JSON.stringify(u), j]));
1009
+ const cellFacts = (x) => {
1010
+ if (!x || typeof x !== 'object')
1011
+ return;
1012
+ if (Array.isArray(x)) {
1013
+ x.forEach(cellFacts);
1014
+ return;
1015
+ }
1016
+ const o = x;
1017
+ if (o.op === 'get' && typeof o.map === 'string' && o.map.endsWith('@') && traced.includes(o.map.slice(0, -1))) {
1018
+ const key = o.key;
1019
+ if (key.op === 'add' && key.b.op === 'prod' && JSON.stringify(key.b.b) === JSON.stringify(omega)) {
1020
+ const j = ix.get(JSON.stringify(key.a)), map = o.map.slice(0, -1), id = `${map}:${JSON.stringify(key)}`, u = key.a, cell = key.b.a;
1021
+ if (j !== undefined && !seen.has(id) && mapNames.includes(map)) {
1022
+ seen.add(id);
1023
+ const more = [...(j > 0 ? ['at a step'] : []), '\'s cells are naturals'].map(what => ({ fact: fi(`${m.name}: ${map}${what.startsWith('\'') ? what : ` ${what}`}`), at: [u, cell] }));
1024
+ insts.push(...more);
1025
+ for (const p of more)
1026
+ cellFacts(substC(facts[p.fact].holds, vars({ 'u$': p.at[0], 'k$': p.at[1] })));
1027
+ }
1028
+ }
1029
+ }
1030
+ for (const val of Object.values(o))
1031
+ cellFacts(val);
1032
+ };
1033
+ cellFacts([hyps, concl, uses.map(us => substC(laws[us.node].holds, { ...at(us.subst['t$']), vars: { ...at(us.subst['t$']).vars, ...Object.fromEntries(Object.entries(us.subst).filter(([n]) => n !== 't$')) } }))]);
1034
+ for (const p of [...insts])
1035
+ if (p.fact === fi(`${m.name}: a step`))
1036
+ cellFacts(substC(facts[p.fact].holds, vars({ 'u$': p.at[0] })));
1037
+ return { name: `${m.name}: ${sl.name}`, hyps, concl, rank: rank === 1 ? [t] : [t, ...Array.from({ length: rank - 1 }, () => k(2))], insts: insts.filter(p => p.fact >= 0), uses };
1038
+ });
1039
+ return sliced(withReads(facts, specs, [...lawNodes, ...specs]));
1040
+ }
1041
+ // A request allowed (BridgeRules.lean's cycAOk, cyc_allow_sound carries it to the claim): from every reachable state,
1042
+ // every request meeting `w` (a helpful request's region and terms), carrying empty sets, is one the machine allows. A node
1043
+ // at time t, the request the run takes at t the claim's (no step reads it): the precondition, t <= T, T < Ω, `w` at t,
1044
+ // each carried map's cell the assumption reads at t 0, every law at t (rank (t, 2) above theirs), the run's facts at t;
1045
+ // its conclusion the assumption at t, flags inlined.
1046
+ export function allowNode(m, laws, inst, w, facts, lawNodes, name, extra = []) {
1047
+ const { at } = trace(m), flags = flagsOf(m), t = v('t$'), T = v('T$'), mapNames = Object.keys(m.maps ?? {}), carried = m.inputMaps ?? [];
1048
+ const traced = [...mapNames, ...carried], state = Object.keys(m.state), now = at(t);
1049
+ const fi = (n) => facts.findIndex(f => f.name === n);
1050
+ const keysOf = (l) => [...(l.forall ? [l.forall] : []), ...(l.keys ?? [])];
1051
+ const tuples = (l) => { const ks = keysOf(l), flat = inst[l.name] ?? []; return ks.length ? Array.from({ length: Math.floor(flat.length / ks.length) }, (_, i) => flat.slice(i * ks.length, (i + 1) * ks.length)) : [[]]; };
1052
+ const concl = substC(substC(m.assume ?? yes, { vars: {}, maps: {}, flags }), now);
1053
+ // Each carried cell the conclusion and the facts given at t read there (a flag's body at its witness, say): 0 (the
1054
+ // request carries empty sets).
1055
+ const zeros = [], seen = new Set();
1056
+ const walk = (x) => {
1057
+ if (!x || typeof x !== 'object')
1058
+ return;
1059
+ const o = x;
1060
+ if (o.op === 'get' && typeof o.map === 'string' && o.map.endsWith('@') && carried.includes(o.map.slice(0, -1))) {
1061
+ const id = JSON.stringify(o);
1062
+ if (!seen.has(id)) {
1063
+ seen.add(id);
1064
+ zeros.push(eq(o, k(0)));
1065
+ }
1066
+ }
1067
+ for (const val of Object.values(o))
1068
+ walk(val);
1069
+ };
1070
+ walk([concl, instHyps(facts, extra)]);
1071
+ const hyps = [...(m.pre ? [m.pre] : []), le(t, T), ...(traced.length ? [{ op: '<', a: T, b: omega }] : []), substC(w, now), ...zeros];
1072
+ const uses = laws.flatMap((l, i) => tuples(l).map(ts => ({ node: i, subst: Object.fromEntries(keysOf(l).map((kn, q) => [kn, substE(ts[q], now)])), guard: yes })));
1073
+ const insts = [{ fact: fi(`${m.name}: the start`), at: [t] }, { fact: fi(`${m.name}: a step`), at: [t] },
1074
+ ...state.map(st => ({ fact: fi(`${m.name}: ${st} is a natural`), at: [t] })), { fact: fi(`${m.name}: params are naturals`), at: [] },
1075
+ ...(m.facts ?? []).flatMap(f => tuples(f).map(ts => ({ fact: fi(f.name), at: ts.map(e => substE(e, now)) }))), ...extra];
1076
+ // Each map cell read at t: its start, its step and that it is a natural there.
1077
+ const cells = new Set();
1078
+ const cellWalk = (x) => {
1079
+ if (!x || typeof x !== 'object')
1080
+ return;
1081
+ const o = x;
1082
+ if (o.op === 'get' && typeof o.map === 'string' && o.map.endsWith('@') && mapNames.includes(o.map.slice(0, -1))) {
1083
+ const key = o.key;
1084
+ if (key.op === 'add' && JSON.stringify(key.a) === JSON.stringify(t) && key.b.op === 'prod') {
1085
+ const map = o.map.slice(0, -1), cell = key.b.a, id = `${map}:${JSON.stringify(cell)}`;
1086
+ if (!cells.has(id)) {
1087
+ cells.add(id);
1088
+ insts.push(...['at the start', 'at a step', '\'s cells are naturals'].map(what => ({ fact: fi(`${m.name}: ${map}${what.startsWith('\'') ? what : ` ${what}`}`), at: [t, cell] })));
1089
+ }
1090
+ }
1091
+ }
1092
+ for (const val of Object.values(o))
1093
+ cellWalk(val);
1094
+ };
1095
+ cellWalk([hyps, concl, uses.map(us => substC(laws[us.node].holds, { ...now, vars: { ...now.vars, ...us.subst } }))]);
1096
+ const rank = lawNodes[0]?.rank.length ?? 2;
1097
+ const spec = { name, hyps, concl, rank: [t, ...Array.from({ length: rank - 1 }, () => k(2))], insts: insts.filter(p => p.fact >= 0), uses };
1098
+ return sliced(withReads(facts, [spec], [...lawNodes, spec]))[0];
1099
+ }
1100
+ // --- flags that hold at every key ------------------------------------------------------------------------------------
1101
+ // A machine with flags that hold at every key, read as its sk machine (BridgeRules.lean's SMachine.sk): each such flag
1102
+ // ?f and its witnesses inputs a request names, a read of the flag `?f = 1`; the closing conditions facts of the run
1103
+ // (`qflagFacts`). Untrusted: BridgeRules.lean's cycQMOk checks the facts against the machine.
1104
+ const quantifiedOf = (m) => Object.entries(m.step.flags ?? {}).filter((e) => e[1].op === 'all');
1105
+ export const hasQuantified = (m) => quantifiedOf(m).length > 0;
1106
+ export function skMachine(m) {
1107
+ const q = quantifiedOf(m);
1108
+ if (!q.length)
1109
+ return m;
1110
+ const flags = Object.fromEntries(Object.entries(m.step.flags ?? {}).map(([f, c]) => [f, c.op === 'all' ? eq(v(`?${f}`), k(1)) : c]));
1111
+ return { ...m, inputs: [...m.inputs, ...q.map(([f]) => `?${f}`), ...q.flatMap(([f, c]) => witnessesOf(f, c))], step: { ...m.step, flags } };
1112
+ }
1113
+ // Each flag's closing conditions at each time u <= T (BridgeRules.lean's cycQFacts): 0 or 1; 0, or its body at every
1114
+ // value of its keys; 1, or its body failing at the witness.
1115
+ export function qflagFacts(m) {
1116
+ const { at } = trace(skMachine(m)), u = v('u$'), T = v('T$');
1117
+ const upTo = (c) => implies(le(u, T), substC(c, at(u)));
1118
+ return quantifiedOf(m).flatMap(([f, q]) => {
1119
+ const fv = v(`?${f}`), keys = boundOf(q), wits = witnessesOf(f, q);
1120
+ return [
1121
+ { name: `${m.name}: ?${f} is 0 or 1`, keys: ['u$'], holds: upTo(le(fv, k(1))) },
1122
+ { name: `${m.name}: ?${f} is 0 or its body holds`, keys: ['u$', ...keys], holds: upTo({ op: 'or', xs: [eq(fv, k(0)), q.c] }) },
1123
+ { name: `${m.name}: ?${f} is 1 or its body fails at the witness`, keys: ['u$'],
1124
+ holds: upTo({ op: 'or', xs: [eq(fv, k(1)), { op: 'not', x: substC(q.c, vars(Object.fromEntries(keys.map((key, i) => [key, v(wits[i])])))) }] }) },
1125
+ ];
1126
+ });
1127
+ }
1128
+ // The closing facts at the times a node reads the run (each flag's at its time, its body at the keys the usual proof
1129
+ // closes it at, read there).
1130
+ export function closingInsts(m, facts, insts, times, laws = []) {
1131
+ // (a law's fresh key k#, which the usual proof's keys may read, the node's own k)
1132
+ const fresh = vars(Object.fromEntries(laws.flatMap(x => (x.forall ? [[`${x.forall}#`, v(x.forall)]] : []))));
1133
+ const { at } = trace(skMachine(m)), fi = (n) => facts.findIndex(f => f.name === n), out = [], seen = new Set();
1134
+ const push = (p) => { const id = JSON.stringify(p); if (p.fact >= 0 && !seen.has(id)) {
1135
+ seen.add(id);
1136
+ out.push(p);
1137
+ } };
1138
+ for (const [f, q] of quantifiedOf(m)) {
1139
+ const n = boundOf(q).length;
1140
+ for (const u of times) {
1141
+ push({ fact: fi(`${m.name}: ?${f} is 0 or 1`), at: [u] });
1142
+ push({ fact: fi(`${m.name}: ?${f} is 1 or its body fails at the witness`), at: [u] });
1143
+ for (const inst of insts) {
1144
+ const flat = inst[`?${f}`] ?? [];
1145
+ for (let i = 0; i + n <= flat.length; i += n)
1146
+ push({ fact: fi(`${m.name}: ?${f} is 0 or its body holds`), at: [u, ...flat.slice(i, i + n).map(e => substE(substE(e, fresh), at(u)))] });
1147
+ }
1148
+ }
1149
+ }
1150
+ return out;
1151
+ }
1152
+ // The laws' nodes of a machine with such flags: its sk machine's (searched), with the closing facts, each node reading
1153
+ // them where the run's step before it and its own time are.
1154
+ export function qflagNodes(m, laws, instances) {
1155
+ const base = machineNodes(skMachine(m), laws, instances);
1156
+ if (base.why)
1157
+ return base;
1158
+ const facts = [...base.facts, ...qflagFacts(m)], t = v('t$');
1159
+ const close = closingInsts(m, facts, Object.values(instances), [t, less1(t)], laws);
1160
+ const add = (sp) => ({ ...sp, insts: [...sp.insts, ...close], ...(sp.variants ? { variants: sp.variants.map(add) } : {}) });
1161
+ return { facts, specs: base.specs.map(add) };
1162
+ }
679
1163
  // --- folds over trees -----------------------------------------------------------------------------------------------
680
1164
  // The tree as a table of nodes below its size N: a node n is a leaf (Kind[n] = 0) whose values come from its inputs,
681
1165
  // or a node (Kind[n] = 1) with children Left[n], Right[n] numbered below n, its values from theirs and its inputs.
682
- export function foldNodes(f, laws) {
1166
+ // `usual`: the usual proof's trees (kernel.ts's foldObligations), translated (`translatedFold` below), or no nodes with why not.
1167
+ export function foldNodes(f, laws, usual, st = tstats()) {
683
1168
  const n = v('n$'), N = v('N$'), names = Object.keys(f.leaf);
684
1169
  const at = (i) => vars(Object.fromEntries([...names, ...f.inputs].map(x => [x, get(`${x}@`, i)])));
685
1170
  const kind = (i) => get('Kind@', i), left = (i) => get('Left@', i), right = (i) => get('Right@', i);
@@ -713,7 +1198,42 @@ export function foldNodes(f, laws) {
713
1198
  const o = shared(facts, a, fromChildren[i].concl);
714
1199
  return o.length ? { ...a, opaque: o } : a;
715
1200
  });
1201
+ if (usual)
1202
+ return { facts, ...translatedFold() };
716
1203
  return { facts, specs: sliced([...atNode, ...fromChildren]) };
1204
+ // The usual proof translated, as a machine's is: each law three nodes. At a node, as above, its goal grown: from the
1205
+ // law at a leaf (Kind[n] = 0) or from its children (Kind[n] = 1). From its children, rank (n, 0): the usual node goal
1206
+ // over the table (an input as its cell at n, l_x and r_x as x at Left[n] and Right[n]), the laws at both children its
1207
+ // uses (each child below n a hypothesis, which the table at n gives where the node is used), the assumption at n
1208
+ // too. At a leaf, rank (n, 0): the usual leaf goal at n.
1209
+ function translatedFold() {
1210
+ const goals = new Map(foldObligations(f, laws).map(g => [g.name, g])), sigma = nodeAt(n), sigma0 = at(n), ren = renamer({ s: sigma }), ren0 = renamer({ s: sigma0 });
1211
+ const na = (f.pre ? 1 : 0) + (f.assume ? 1 : 0), last = (g) => g.hyps.at(-1).x;
1212
+ const kids = [], leaves = [], made = [];
1213
+ for (const [i, l] of laws.entries()) {
1214
+ const g0 = goals.get(`${l.name} @leaf`), g1 = goals.get(`${l.name} @node`), t0 = g0 && usual[g0.name], t1 = g1 && usual[g1.name];
1215
+ if (!g0 || !g1 || !t0 || !t1 || g0.hyps.length !== na + 1 || g1.hyps.length !== na + 2 * L + 1)
1216
+ return { specs: [], why: `${l.name}: no usual tree, or one of another shape` };
1217
+ const used1 = usedHyps(g1, t1);
1218
+ kids.push({ name: `${f.name}: ${l.name}, from its children`, hyps: [...hyps, inner, { op: '<', a: left(n), b: n }, { op: '<', a: right(n), b: n }, ...(f.assume ? [substC(f.assume, sigma0)] : [])], concl: substC(last(g1), sigma), rank: [n, k(0)], insts: [],
1219
+ uses: laws.flatMap((_, j) => [left, right].map(c => ({ node: j, subst: { 'n$': c(n) }, guard: yes }))).filter((_, q) => used1.has(na + q)) });
1220
+ leaves.push({ name: `${f.name}: ${l.name}, at a leaf`, hyps: [...hyps, eq(kind(n), k(0)), ...(f.assume ? [substC(f.assume, sigma0)] : [])], concl: substC(last(g0), sigma0), rank: [n, k(0)], insts: [], uses: [] });
1221
+ made.push({ g: g1, tree: t1, node: L + i, ren }, { g: g0, tree: t0, node: 2 * L + i, ren: ren0 });
1222
+ }
1223
+ const carried = atNode.map((a, i) => {
1224
+ const b = { ...a, variants: undefined, carry: true, uses: [...a.uses, { node: 2 * L + i, subst: {}, guard: eq(kind(n), k(0)) }] };
1225
+ const o = [...new Set([...shared(facts, b, kids[i].concl), ...shared(facts, b, leaves[i].concl)].map(e => JSON.stringify(e)))].map(e => JSON.parse(e));
1226
+ return { ...b, opaque: o.length ? o : undefined };
1227
+ });
1228
+ const all = [...carried, ...kids, ...leaves], until = performance.now() + 10_000;
1229
+ for (const m of made) {
1230
+ const tree = timedKind('translate', 0, () => withProducts(() => translate(m.g, mainGoal(facts, all, all[m.node]), m.tree, m.ren, st, until)));
1231
+ if (!tree)
1232
+ return { specs: [], why: `${all[m.node].name}: not translated` };
1233
+ all[m.node] = { ...all[m.node], main: tree };
1234
+ }
1235
+ return { specs: sliced(all) };
1236
+ }
717
1237
  }
718
1238
  // --- the cyclic route beside the usual one (GDP_CYCLIC=1) -------------------------------------------------------------
719
1239
  // With GDP_CYCLIC=1, every proof the instances above cover is made again as a cyclic proof, and a verdict that differs
@@ -721,6 +1241,10 @@ export function foldNodes(f, laws) {
721
1241
  const env = () => globalThis.process?.env;
722
1242
  let quiet = 0;
723
1243
  export const cyclicOn = () => quiet === 0 && env()?.GDP_CYCLIC === '1';
1244
+ // The `cyclic: {...}` lines research/cyclic/ratios.ts reads, printed only with GDP_CYCLIC=1 (the cross-check) or
1245
+ // GDP_CYCLIC_LOG=1 (modal/run.py sets it): a user's proofs print nothing.
1246
+ export const cycLog = (o) => { if (env()?.GDP_CYCLIC === '1' || env()?.GDP_CYCLIC_LOG === '1')
1247
+ console.error(`cyclic: ${JSON.stringify(o)}`); };
724
1248
  // A search's many trial proofs (core/discover.ts, core/infer.ts) aren't compared: what it finds is proven again.
725
1249
  export const quietly = (f) => { quiet++; try {
726
1250
  return f();
@@ -728,13 +1252,130 @@ export const quietly = (f) => { quiet++; try {
728
1252
  finally {
729
1253
  quiet--;
730
1254
  } };
731
- export function crossCyclic(kind, name, usual, run, usualMs) {
1255
+ // A translated proof by what it was made from (the machine, its laws and instances; a recurrence's or fold's case): a
1256
+ // proof made again (a rule proven twice, as the suite does) is the one made and checked before (the same proof: the
1257
+ // kernel's check is a function of it, and nothing changes a proof once made). The key's time is GDP_CYCLIC_PROF's `key`.
1258
+ const proofsMade = new Map();
1259
+ // `base`: the key of a proof made before whose nodes these begin with (a machine's laws, for its step laws), reused where
1260
+ // its facts are these.
1261
+ export function translatedProof(key, build, st, base) {
1262
+ const k0 = timedKind('key', 0, key), had = proofsMade.get(k0);
1263
+ if (had) {
1264
+ st.cached++;
1265
+ return { verdict: { proven: true, checked: true, proof: had } };
1266
+ }
1267
+ const b = base && proofsMade.get(timedKind('key', 0, base)), { facts, specs, why } = build(!!b);
1268
+ if (why)
1269
+ return { why };
1270
+ const made = b && b.nodes.length <= specs.length && JSON.stringify(b.facts) === JSON.stringify(facts) ? b.nodes : [];
1271
+ if (b && !made.length)
1272
+ return translatedProof(key, () => build(false), st);
1273
+ const v = proveCyclic(facts, specs, undefined, translatedCache, st, made);
1274
+ if (v.proven && v.checked) {
1275
+ if (proofsMade.size >= 1024)
1276
+ proofsMade.clear();
1277
+ proofsMade.set(k0, v.proof);
1278
+ }
1279
+ return { verdict: v };
1280
+ }
1281
+ export const routeOf = () => env()?.GDP_CYCLIC_ROUTE ?? 'translate';
1282
+ const none = () => ({ proven: false, checked: false, node: '', goal: 'no instance searched', stuck: 'unknown' });
1283
+ export function crossCyclic(kind, name, usual, run0, usualMs, translated) {
1284
+ const run = run0 ?? none;
732
1285
  for (const key of Object.keys(cyclicProf))
733
1286
  delete cyclicProf[key];
734
- const t0 = performance.now(), out = run(), ms = Math.round(performance.now() - t0);
735
- const cyclic = out.proven && out.checked;
1287
+ const route = routeOf(), st = tstats(), t0 = performance.now();
1288
+ let out, how = 'searched', why;
1289
+ if (route !== 'search' && usual && translated) {
1290
+ const r = translated(st);
1291
+ if (r.verdict?.proven && r.verdict.checked) {
1292
+ out = r.verdict;
1293
+ how = 'translated';
1294
+ }
1295
+ else {
1296
+ how = 'fallback';
1297
+ why = r.why ?? (r.verdict && !r.verdict.proven ? `${r.verdict.goal}: ${r.verdict.stuck}` : r.verdict?.why);
1298
+ }
1299
+ }
1300
+ out ??= run();
1301
+ const ms = Math.round(performance.now() - t0), cyclic = out.proven && out.checked;
736
1302
  const prof = profOn() ? { prof: Object.fromEntries(Object.entries(cyclicProf).map(([key, p]) => [key, Object.fromEntries(Object.entries(p).map(([f, x]) => [f, Math.round(x)]))])) } : {};
737
- console.error(`cyclic: ${JSON.stringify({ kind, name, usual, cyclic, ms, ...(usualMs === undefined ? {} : { usualMs: Math.round(usualMs) }), ...prof, ...(out.proven ? { nodes: out.proof.nodes.length, ...(out.why ? { why: out.why } : {}) } : { goal: out.goal, stuck: out.stuck }) })}`);
738
- if (cyclic !== usual && env()?.GDP_CYCLIC_LAX !== '1')
1303
+ let search;
1304
+ if (route === 'both' && run0) {
1305
+ const s0 = performance.now(), o = run0();
1306
+ search = { searchMs: Math.round(performance.now() - s0), searchCyclic: o.proven && o.checked };
1307
+ }
1308
+ cycLog({ kind, name, usual, cyclic, ms, ...(usualMs === undefined ? {} : { usualMs: Math.round(usualMs) }), route: how, ...(how !== 'searched' ? st : {}), ...(why ? { fellBack: why } : {}), ...search, ...prof, ...(out.proven ? { nodes: out.proof.nodes.length, ...(out.why ? { why: out.why } : {}) } : { goal: out.goal, stuck: out.stuck }) });
1309
+ const lax = env()?.GDP_CYCLIC_LAX === '1';
1310
+ if (cyclic !== usual && !lax)
739
1311
  throw new Error(`cyclic route: ${kind} ${name} is ${cyclic ? 'proven' : 'not proven'} as a cyclic proof, ${usual ? 'proven' : 'not proven'} the usual way${out.proven ? (out.why ? ` (${out.why})` : '') : ` (${out.goal}: ${out.stuck})`}`);
1312
+ if (search && search.searchCyclic !== usual && !lax)
1313
+ throw new Error(`cyclic route: ${kind} ${name} is ${search.searchCyclic ? 'proven' : 'not proven'} as a cyclic proof searched, ${usual ? 'proven' : 'not proven'} the usual way`);
1314
+ }
1315
+ // The certificate of every proof the instances above cover is the cyclic one (GDP_CERT=usual: the usual trees, as
1316
+ // before 0.19.0), made from the usual proof (translated, searched where that fails) and checked by kernel/cyclic.ts; the
1317
+ // usual trees are then the translation's input, not checked again (kernel/prove.ts, core/dsl.ts). Each logs a line
1318
+ // `cyclic: {..., cert: true}` (cycLog).
1319
+ export const certOn = () => quiet === 0 && env()?.GDP_CERT !== 'usual';
1320
+ // With GDP_CERT=cyclic, what no translation covers (flags that hold at every key, k-step laws over a map) gets a searched
1321
+ // certificate too where GDP_CERT_SEARCH=1, each within GDP_CERT_MS (10 s by default), and keeps the usual one where none
1322
+ // is found then. Off by default: a search costs seconds a proof.
1323
+ export const certSearch = () => env()?.GDP_CERT_SEARCH === '1';
1324
+ export const certMs = () => Number(env()?.GDP_CERT_MS ?? 10_000);
1325
+ export function certCyclic(kind, name, translated, run0, usualMs) {
1326
+ const run = run0 ?? none;
1327
+ const st = tstats(), t0 = performance.now();
1328
+ let out, how = 'searched', why;
1329
+ if (translated) {
1330
+ const r = translated(st);
1331
+ if (r.verdict?.proven && r.verdict.checked) {
1332
+ out = r.verdict;
1333
+ how = 'translated';
1334
+ }
1335
+ else {
1336
+ how = 'fallback';
1337
+ why = r.why ?? (r.verdict && !r.verdict.proven ? `${r.verdict.goal}: ${r.verdict.stuck}` : r.verdict?.why);
1338
+ }
1339
+ }
1340
+ out ??= run();
1341
+ const checked = out.proven && out.checked;
1342
+ cycLog({ kind, name, cert: true, usual: true, cyclic: checked, ms: Math.round(performance.now() - t0), ...(usualMs === undefined ? {} : { usualMs: Math.round(usualMs) }), route: how, ...(how !== 'searched' ? st : {}), ...(why ? { fellBack: why } : {}) });
1343
+ return { checked, ...(out.proven ? { proof: out.proof } : {}) };
1344
+ }
1345
+ // The step laws' searched nodes after the laws' (stepLawNodes over the sk machine), with the flags' closing facts read
1346
+ // where each node reads the run on a machine with flags that hold at every key: what GDP_CERT=cyclic's search and the
1347
+ // Lean export's (lean-kernel/emit.ts) both try.
1348
+ export function searchedStepNodes(m, laws, steps, facts, lawNodes) {
1349
+ const q = hasQuantified(m);
1350
+ return stepLawNodes(skMachine(m), laws, steps, facts, lawNodes).map((sp, i) => {
1351
+ if (!q)
1352
+ return sp;
1353
+ const more = closingInsts(m, facts, [steps[i].instances], Array.from({ length: (steps[i].law.steps ?? 1) + 1 }, (_, j) => tBack(j)));
1354
+ const add = (x) => ({ ...x, insts: [...x.insts, ...more], ...(x.variants ? { variants: x.variants.map(add) } : {}) });
1355
+ return add(sp);
1356
+ });
1357
+ }
1358
+ // Live.lean's reqC: each input named by its term, two parts per and.
1359
+ export const reqTS = (req) => (req.length ? { op: 'and', xs: [{ op: '=', a: { op: 'var', name: req[0][0] }, b: req[0][1] }, reqTS(req.slice(1))] } : { op: '<=', a: { op: 'const', value: 0 }, b: { op: 'const', value: 0 } });
1360
+ // Witnesses for a helpful request's free inputs, for its allowed goal: all free first, then each free input fixed to 0, a
1361
+ // state variable or a param (a history's query, say, which its relations bound), the first combination that proves.
1362
+ export function withWitness(m, request, attempt) {
1363
+ const free = m.inputs.filter(i => !(i in request));
1364
+ const cands = [undefined, { op: 'const', value: 0 }, ...Object.keys(m.state).map(v => ({ op: 'var', name: v })), ...m.params.map(v => ({ op: 'var', name: v }))];
1365
+ const combos = [[]];
1366
+ if (free.length <= 2) {
1367
+ const go = (i, acc) => { if (i === free.length) {
1368
+ if (acc.length)
1369
+ combos.push(acc);
1370
+ return;
1371
+ } for (const c of cands)
1372
+ go(i + 1, c ? [...acc, [free[i], c]] : acc); };
1373
+ go(0, []);
1374
+ }
1375
+ for (const wit of combos) {
1376
+ const out = attempt(wit);
1377
+ if (out !== undefined)
1378
+ return { wit, out };
1379
+ }
1380
+ return undefined;
740
1381
  }