gdp-wired 0.11.0 → 0.12.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.
@@ -332,6 +332,9 @@ export function facts(c, path, positive = true) {
332
332
  }
333
333
  // What a leaf's derivation must refute.
334
334
  export function leafConstraints(goal, path) { return leafRows(goal, path, h => facts(h, path)); }
335
+ // The same rows, for the prover's search: each hypothesis' facts as they were at an earlier path of the goal's where
336
+ // every decision they read was the same (facts read the path only through those decisions), and found afresh, with
337
+ // what they read recorded, otherwise. The kernel's check never uses it: it rebuilds every row as leafConstraints does.
335
338
  const factsSeen = new WeakMap();
336
339
  export function leafConstraintsReusing(goal, path) {
337
340
  let byHyp = factsSeen.get(goal);
@@ -340,26 +343,56 @@ export function leafConstraintsReusing(goal, path) {
340
343
  return leafRows(goal, path, h => {
341
344
  let seen = byHyp.get(h);
342
345
  if (!seen)
343
- byHyp.set(h, (seen = []));
346
+ byHyp.set(h, (seen = {}));
344
347
  return reusing(seen, path, p => facts(h, p));
345
348
  });
346
349
  }
350
+ // Past `reuseKeep` values a trie starts afresh: a long search would otherwise keep every value it ever found.
351
+ const reuseKeep = 16;
352
+ function remember(seen, reads, out) {
353
+ if ((seen.size = (seen.size ?? 0) + 1) > reuseKeep) {
354
+ seen.root = undefined;
355
+ seen.size = 1;
356
+ }
357
+ const leaf = { reads, out };
358
+ let node = seen.root, parent, via;
359
+ while (node && 'key' in node) {
360
+ // A value that read otherwise than the trie says (none does: the decisions are deterministic) is not kept.
361
+ if (reads[node.at]?.[0] !== node.key)
362
+ return;
363
+ parent = node;
364
+ via = reads[node.at][1];
365
+ node = node.next.get(via);
366
+ }
367
+ const put = (n) => { if (parent)
368
+ parent.next.set(via, n);
369
+ else
370
+ seen.root = n; };
371
+ if (!node) {
372
+ put(leaf);
373
+ return;
374
+ }
375
+ const i = node.reads.findIndex((d, j) => reads[j]?.[0] !== d[0] || reads[j][1] !== d[1]);
376
+ if (i < 0 || reads[i]?.[0] !== node.reads[i][0])
377
+ return;
378
+ put({ at: i, key: node.reads[i][0], next: new Map([[node.reads[i][1], node], [reads[i][1], leaf]]) });
379
+ }
347
380
  export function reusing(seen, path, f) {
348
- const hit = seen.find(e => e.reads.every(([key, holds]) => path.get(key)?.holds === holds));
349
- if (hit) {
381
+ let node = seen.root;
382
+ while (node && 'key' in node)
383
+ node = node.next.get(path.get(node.key)?.holds);
384
+ if (node && node.reads.every(([key, holds]) => path.get(key)?.holds === holds)) {
350
385
  if (recording)
351
- for (const [key, holds] of hit.reads)
386
+ for (const [key, holds] of node.reads)
352
387
  recording.set(key, holds);
353
- return hit.out;
388
+ return node.out;
354
389
  }
355
390
  const outer = recording, mode = reuse;
356
391
  recording = new Map();
357
392
  reuse = true;
358
393
  try {
359
394
  const out = f(new Map(path));
360
- seen.unshift({ reads: [...recording], out });
361
- if (seen.length > 16)
362
- seen.pop();
395
+ remember(seen, [...recording], out);
363
396
  return out;
364
397
  }
365
398
  finally {
@@ -377,12 +410,12 @@ export function reusing(seen, path, f) {
377
410
  let reuse = false;
378
411
  const linearSeen = new WeakMap(), comparisonSeen = new WeakMap(), truthSeen = new WeakMap();
379
412
  const seenOf = (m, k) => { let s = m.get(k); if (!s)
380
- m.set(k, (s = [])); return s; };
413
+ m.set(k, (s = {})); return s; };
381
414
  function leafRows(goal, path, factsOf) {
382
- const rows = [...path.values()].map(a => atomOf(a.lin, a.holds));
383
- for (const h of goal.hyps)
415
+ const rows = [...path.values()].map((a, i) => ({ ...atomOf(a.lin, a.holds), src: { atom: i } }));
416
+ for (const [i, h] of goal.hyps.entries())
384
417
  for (const lin of factsOf(h))
385
- rows.push({ lin, label: '' });
418
+ rows.push({ lin, label: '', src: { fact: i } });
386
419
  const names = new Set(rows.flatMap(r => Object.keys(r.lin).filter(key => key !== '1')));
387
420
  // Two reads of one map whose keys the path proves equal are equal.
388
421
  const cells = [...names].filter(name => name.includes('[')).map(name => ({ name, map: name.slice(0, name.indexOf('[')), key: keyOf(name) }));
@@ -392,7 +425,7 @@ function leafRows(goal, path, factsOf) {
392
425
  continue;
393
426
  const d = sub(p.key, q.key);
394
427
  if (decide(d, path) === true && decide(scale(d, -1), path) === true)
395
- rows.push({ lin: sub(v(p.name), v(q.name)), label: '' }, { lin: sub(v(q.name), v(p.name)), label: '' });
428
+ rows.push({ lin: sub(v(p.name), v(q.name)), label: '', src: { pair: [p.name, q.name] } }, { lin: sub(v(q.name), v(p.name)), label: '', src: { pair: [p.name, q.name] } });
396
429
  }
397
430
  // Each term column's rows, and the columns those rows bring in turn.
398
431
  const seen = new Set(), todo = [...names].filter(n => n.startsWith('⟦'));
@@ -402,7 +435,7 @@ function leafRows(goal, path, factsOf) {
402
435
  continue;
403
436
  seen.add(name);
404
437
  for (const lin of termRows(name, path)) {
405
- rows.push({ lin, label: '' });
438
+ rows.push({ lin, label: '', src: { term: name } });
406
439
  for (const key of Object.keys(lin))
407
440
  if (key !== '1') {
408
441
  names.add(key);
@@ -412,7 +445,7 @@ function leafRows(goal, path, factsOf) {
412
445
  }
413
446
  }
414
447
  for (const name of names)
415
- rows.push({ lin: { [name]: -1 }, label: '' });
448
+ rows.push({ lin: { [name]: -1 }, label: '', src: { nat: name } });
416
449
  return rows;
417
450
  }
418
451
  const keyOf = (cell) => {
@@ -504,8 +537,8 @@ export function substC(c, s) {
504
537
  if (c.op === 'not')
505
538
  return { op: 'not', x: substC(c.x, s) };
506
539
  if (c.op === 'all') {
507
- const { [c.key]: _bound, ...vars } = s.vars;
508
- return { op: 'all', key: c.key, c: substC(c.c, { ...s, vars }) };
540
+ const keys = boundOf(c), vars = Object.fromEntries(Object.entries(s.vars).filter(([n]) => !keys.includes(n)));
541
+ return { ...c, c: substC(c.c, { ...s, vars }) };
509
542
  }
510
543
  if (c.op === 'and' || c.op === 'or')
511
544
  return { op: c.op, xs: c.xs.map(y => substC(y, s)) };
@@ -567,6 +600,29 @@ const flagsOf = (m) => {
567
600
  return out;
568
601
  };
569
602
  const quantified = (m) => Object.entries(m.step.flags ?? {}).filter((e) => e[1].op === 'all');
603
+ // The keys a quantifier binds: its key, and a quantified flag's further keys (a condition at every tuple of keys).
604
+ export const boundOf = (c) => [c.key, ...(c.keys ?? [])];
605
+ // A quantified flag's witnesses, one per key: w?f, then w2?f, w3?f.
606
+ export const witnessesOf = (f, q) => boundOf(q).map((_, i) => (i ? `w${i + 1}?${f}` : `w?${f}`));
607
+ // A comparison between two of a flag's keys, plainly (i < j, i ≤ j, i = j).
608
+ const crossing = (c, keys) => (c.op === '<' || c.op === '<=' || c.op === '=') && c.a.op === 'var' && c.b.op === 'var' && c.a.name !== c.b.name && keys.includes(c.a.name) && keys.includes(c.b.name);
609
+ // A condition over several keys with each comparison between two of them set aside (as 0 ≤ 0): what reads each key on
610
+ // its own. (Kept per condition and keys: the runtimes ask at every step.)
611
+ const crossCache = new WeakMap();
612
+ export const crossFree = (c, keys) => {
613
+ const id = keys.join(','), hit = crossCache.get(c)?.get(id);
614
+ if (hit)
615
+ return hit;
616
+ const zero = { op: 'const', value: 0 };
617
+ const e = (t) => (t.op === 'if' ? { ...t, c: crossFree(t.c, keys), then: e(t.then), else: e(t.else) } : t.op === 'get' ? { ...t, key: e(t.key) } : t.op === 'mul' ? { ...t, a: e(t.a) }
618
+ : t.op === 'add' || t.op === 'sub' || t.op === 'min' || t.op === 'max' || t.op === 'prod' ? { ...t, a: e(t.a), b: e(t.b) } : t);
619
+ const out = crossing(c, keys) ? { op: '<=', a: zero, b: zero } : c.op === 'not' ? { op: 'not', x: crossFree(c.x, keys) } : c.op === 'and' || c.op === 'or' ? { op: c.op, xs: c.xs.map(y => crossFree(y, keys)) }
620
+ : c.op === 'all' ? { ...c, c: crossFree(c.c, keys) } : c.op === 'flag' ? c : { ...c, a: e(c.a), b: e(c.b) };
621
+ if (!crossCache.has(c))
622
+ crossCache.set(c, new Map());
623
+ crossCache.get(c).set(id, out);
624
+ return out;
625
+ };
570
626
  // A machine's step as an update, its flags inlined.
571
627
  const updateOf = (m) => {
572
628
  const flags = { vars: {}, maps: {}, flags: flagsOf(m) };
@@ -579,15 +635,17 @@ const inlineFlags = (m, c) => substC(c, { vars: {}, maps: {}, flags: flagsOf(m)
579
635
  // A quantified flag ?f, closed: it is 0 or 1; where it is 1, its condition holds at each key the proof names (instances
580
636
  // under ?f); where it is 0, the condition fails at a fresh witness w?f. A real step decides ?f by the condition, and if
581
637
  // that fails some key fails it, so these hold of every real step: a goal that refutes them refutes the machine.
638
+ // Over several keys, the instances are tuples (flattened, as Instances keeps them) and there is a witness per key.
582
639
  const closing = (m, instances) => quantified(m).flatMap(([f, q]) => {
583
- const v = { op: 'var', name: `?${f}` }, zero = { op: 'const', value: 0 }, one = { op: 'const', value: 1 };
584
- const body = (key) => substC(q.c, { vars: { [q.key]: key }, maps: {}, flags: flagsOf(m) });
585
- return [{ op: '<=', a: v, b: one }, ...(instances[`?${f}`] ?? []).map((key) => ({ op: 'or', xs: [{ op: '=', a: v, b: zero }, body(key)] })),
586
- { op: 'or', xs: [{ op: '=', a: v, b: one }, { op: 'not', x: body({ op: 'var', name: `w?${f}` }) }] }];
640
+ const v = { op: 'var', name: `?${f}` }, zero = { op: 'const', value: 0 }, one = { op: 'const', value: 1 }, names = boundOf(q), n = names.length;
641
+ const body = (keys) => substC(q.c, { vars: Object.fromEntries(names.map((key, i) => [key, keys[i]])), maps: {}, flags: flagsOf(m) });
642
+ const inst = instances[`?${f}`] ?? [];
643
+ return [{ op: '<=', a: v, b: one }, ...Array.from({ length: Math.floor(inst.length / n) }, (_, t) => ({ op: 'or', xs: [{ op: '=', a: v, b: zero }, body(inst.slice(t * n, (t + 1) * n))] })),
644
+ { op: 'or', xs: [{ op: '=', a: v, b: one }, { op: 'not', x: body(witnessesOf(f, q).map((name) => ({ op: 'var', name }))) }] }];
587
645
  });
588
646
  // The names a later step (j >= 2) reads its request by: inputs, quantified flags and their witnesses, and carried maps.
589
647
  const laterNames = (m, j) => ({
590
- vars: Object.fromEntries([...m.inputs, ...quantified(m).flatMap(([f]) => [`?${f}`, `w?${f}`])].map(name => [name, { op: 'var', name: `${name}@${j}` }])),
648
+ vars: Object.fromEntries([...m.inputs, ...quantified(m).flatMap(([f, q]) => [`?${f}`, ...witnessesOf(f, q)])].map(name => [name, { op: 'var', name: `${name}@${j}` }])),
591
649
  maps: Object.fromEntries((m.inputMaps ?? []).map(map => [map, `${map}@${j}`])),
592
650
  });
593
651
  // The laws assumed where the run is: each plain law, and each every-key law at the keys the proof names.
@@ -619,7 +677,8 @@ function reads(t, out = { vars: new Set(), maps: new Set(), flags: new Set() })
619
677
  break;
620
678
  case 'all': {
621
679
  const inner = reads(t.c);
622
- inner.vars.delete(t.key);
680
+ for (const key of boundOf(t))
681
+ inner.vars.delete(key);
623
682
  for (const k of ['vars', 'maps', 'flags'])
624
683
  for (const name of inner[k])
625
684
  out[k].add(name);
@@ -709,9 +768,14 @@ export function scope(m, laws = []) {
709
768
  const c = m.step.flags[f], what = `${m.name}'s flag ${f}`;
710
769
  // A quantified flag's body reads no flag (the twin decides quantified flags before the plain ones).
711
770
  if (c.op === 'all') {
712
- bound(what, c.key, c.c);
771
+ const keys = boundOf(c), body = crossFree(c.c, keys);
772
+ distinct(what, keys);
773
+ for (const key of keys) {
774
+ bound(what, key, body);
775
+ apart(what, body, key, keys);
776
+ }
713
777
  unquantified(what, c.c);
714
- inScope(what, [c.c], [...names, c.key], readable, []);
778
+ inScope(what, [c.c], [...names, ...keys], readable, []);
715
779
  }
716
780
  else {
717
781
  unquantified(what, c);
@@ -825,6 +889,20 @@ function keyOnly(what, t, key, fns = []) {
825
889
  };
826
890
  walk(t);
827
891
  }
892
+ // Over several keys, each read apart from the others: no offset of its key terms, and no value it is compared with, reads
893
+ // another (the comparisons between two of them, plainly, are set aside before: crossFree).
894
+ function apart(what, t, key, keys) {
895
+ const others = keys.filter(k => k !== key), other = (e) => others.some(k => readsKey(e, k));
896
+ const walk = (u) => {
897
+ if ((u.op === 'get' && keyTerm(u.key, key) && other(u.key))
898
+ || ((u.op === '<' || u.op === '<=' || u.op === '=') && ((keyTerm(u.a, key) && (other(u.a) || other(u.b))) || (keyTerm(u.b, key) && (other(u.a) || other(u.b))))))
899
+ throw new Error(`${what}: the keys ${keys.join(', ')} are each read on their own, and compared with one another only plainly (i < j, i <= j, i = j)`);
900
+ for (const x of Object.values(u))
901
+ if (x && typeof x === 'object')
902
+ (Array.isArray(x) ? x : [x]).forEach(y => walk(y));
903
+ };
904
+ walk(t);
905
+ }
828
906
  // Whether the key is read only as a map's key, plainly (what the GPU searches decide: every cell, and one past them).
829
907
  export const plainKey = (t, key) => {
830
908
  const walk = (u) => {
@@ -1042,7 +1120,7 @@ export function stepLawObligations(machine, laws, instances, sl) {
1042
1120
  const m = machine.history ? historyMachine(machine).machine : machine;
1043
1121
  scope(m, laws);
1044
1122
  const k = sl.steps ?? 1;
1045
- // Past 64 the cost is the bound (proving grows about as k³: 64 steps in about 90 s; Lean checks 32 in about 13 minutes).
1123
+ // Past 64 the cost is the bound (proving grows about as k³: 64 steps in about 17 s on Modal; Lean checks 32 in about 25 s, 64 in about 3 minutes).
1046
1124
  if (!Number.isInteger(k) || k < 1 || k > 64)
1047
1125
  throw new Error(`step law ${sl.name}: steps is a whole number from 1 to 64`);
1048
1126
  const whens = sl.when === undefined ? [] : Array.isArray(sl.when) ? sl.when : [sl.when];
@@ -1,5 +1,5 @@
1
1
  import { add, k, refuteFast, scale, solve, sub, v } from "../core/solver.js";
2
- import { assumed, canon, canonFlip, columnTerm, decided, stepZone, check, checkLaws, checkStepLaws, historyMachine, leafConstraints, leafConstraintsReusing, reusing, linear, livenessHelpful, livenessParts, obligations, plainKey, progressObligations, stepLawObligations, trueC, truth, unreachable, withProducts } from "./kernel.js";
2
+ import { assumed, boundOf, canon, canonFlip, columnTerm, decided, stepZone, check, checkLaws, checkStepLaws, historyMachine, leafConstraints, leafConstraintsReusing, reusing, linear, livenessHelpful, livenessParts, obligations, plainKey, progressObligations, stepLawObligations, trueC, truth, unreachable, witnessesOf, 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) {
@@ -183,11 +183,17 @@ function growPlain(goal, path, budget) {
183
183
  if (!byHyp)
184
184
  openSeen.set(goal, (byHyp = new Map()));
185
185
  const openOf = (h) => { let seen = byHyp.get(h); if (!seen)
186
- byHyp.set(h, (seen = [])); return reusing(seen, path, p => { const out = []; openH(h, p, out); return out; }); };
186
+ byHyp.set(h, (seen = {})); return reusing(seen, path, p => { const out = []; openH(h, p, out); return out; }); };
187
+ // Then the hypotheses about only the names the conclusion reads (an assumption about another key, a witness, can
188
+ // wait: a refutation seldom needs it), then the rest, each group in the goal's order.
187
189
  if (last >= 0)
188
190
  open.push(...openOf(hyps[last]));
191
+ const near = nearOf(goal);
189
192
  for (const [i, h] of hyps.entries())
190
- if (i !== last)
193
+ if (i !== last && near[i])
194
+ open.push(...openOf(h));
195
+ for (const [i, h] of hyps.entries())
196
+ if (i !== last && !near[i])
191
197
  open.push(...openOf(h));
192
198
  open.push(...openCells(rows, path));
193
199
  const next = open.find(lin => Object.keys(lin).some(key => key !== '1') && !path.has(canon(lin)) && !path.has(canonFlip(lin)));
@@ -308,6 +314,17 @@ function lift(goal, tree, from, to) {
308
314
  };
309
315
  return go(tree, from, to);
310
316
  }
317
+ // Whether each hypothesis reads only names the goal's conclusion (its last hypothesis) reads.
318
+ const nearSeen = new WeakMap();
319
+ const namesOf = (t) => (t === undefined ? [] : [...JSON.stringify(t).matchAll(/"(?:name|map)":"([^"]+)"/g)].map(m => m[1]));
320
+ function nearOf(goal) {
321
+ let out = nearSeen.get(goal);
322
+ if (!out) {
323
+ const theirs = new Set(namesOf(goal.hyps.at(-1)));
324
+ nearSeen.set(goal, (out = goal.hyps.map(h => namesOf(h).every(n => theirs.has(n)))));
325
+ }
326
+ return out;
327
+ }
311
328
  // Each hypothesis' open atoms, as at an earlier path that decided the same atoms it reads (kernel.ts's reusing).
312
329
  const openSeen = new WeakMap();
313
330
  // What ran out, for a goal left unknown: its size, how deep the case splits went, and the two shapes that usually cause it.
@@ -446,7 +463,7 @@ function fractional(point, path) {
446
463
  // A key bound by a quantified flag or a write at every key isn't one: those reads are at every key already.
447
464
  function readKeys(m, laws) {
448
465
  const out = new Map();
449
- const bound = new Set([...Object.values(m.step.flags ?? {}).flatMap(c => (c.op === 'all' ? [c.key] : [])), ...Object.values(m.step.maps ?? {}).flatMap(w => ('each' in w ? [w.each] : []))]);
466
+ const bound = new Set([...Object.values(m.step.flags ?? {}).flatMap(c => (c.op === 'all' ? boundOf(c) : [])), ...Object.values(m.step.maps ?? {}).flatMap(w => ('each' in w ? [w.each] : []))]);
450
467
  const free = (e) => !JSON.stringify(e).match(/"name":"([^"]+)"/g)?.some(n => bound.has(n.slice(8, -1)));
451
468
  const inE = (e) => {
452
469
  if (e.op === 'get') {
@@ -503,30 +520,35 @@ function mapKeys(goals) {
503
520
  const withReads = (laws, inst, keys, focus) => ({ ...inst, ...Object.fromEntries(laws.filter(l => l.forall).map(l => {
504
521
  const had = inst[l.name] ?? [];
505
522
  if (l.keys?.length) {
506
- const n = l.keys.length + 1;
507
- const reads = (e, x) => x !== undefined && JSON.stringify(e).includes(`"name":${JSON.stringify(x)}`);
508
- const own = (i) => { const f = focus[i] ?? focus[0]; return keys.filter(e => reads(e, f) && focus.every(g => g === f || !reads(e, g))); };
509
- const R = keys.filter(e => !focus.some(f => reads(e, f)));
510
- const out = new Map();
523
+ const n = l.keys.length + 1, out = new Map();
511
524
  for (let i = 0; i + n <= had.length; i += n)
512
525
  out.set(JSON.stringify(had.slice(i, i + n)), had.slice(i, i + n));
513
- const go = (i, tuple, rest) => {
514
- if (i === n) {
515
- out.set(JSON.stringify(tuple), tuple);
516
- return;
517
- }
518
- for (const e of own(i))
519
- go(i + 1, [...tuple, e], rest);
520
- if (!rest)
521
- for (const e of R)
522
- go(i + 1, [...tuple, e], true);
523
- };
524
- go(0, [], false);
526
+ for (const tuple of related(keys, focus, n))
527
+ out.set(JSON.stringify(tuple), tuple);
525
528
  return [l.name, [...out.values()].flat()];
526
529
  }
527
530
  const seen = new Set(had.map(e => JSON.stringify(e)));
528
531
  return [l.name, [...had, ...keys.filter(e => { const id = JSON.stringify(e); return seen.has(id) ? false : (seen.add(id), true); })]];
529
532
  })) });
533
+ // The n-tuples of the keys that relate the focus (withReads).
534
+ const related = (keys, focus, n) => {
535
+ const reads = (e, x) => x !== undefined && JSON.stringify(e).includes(`"name":${JSON.stringify(x)}`);
536
+ const own = (i) => { const f = focus[i] ?? focus[0]; return keys.filter(e => reads(e, f) && focus.every(g => g === f || !reads(e, g))); };
537
+ const R = keys.filter(e => !focus.some(f => reads(e, f))), out = [];
538
+ const go = (i, tuple, rest) => {
539
+ if (i === n) {
540
+ out.push(tuple);
541
+ return;
542
+ }
543
+ for (const e of own(i))
544
+ go(i + 1, [...tuple, e], rest);
545
+ if (!rest)
546
+ for (const e of R)
547
+ go(i + 1, [...tuple, e], true);
548
+ };
549
+ go(0, [], false);
550
+ return out;
551
+ };
530
552
  // A law about every n keys at every n-tuple of the keys (flattened, as Instances keeps tuples).
531
553
  const tuples = (keys, n) => (n === 0 ? [] : n === 1 ? keys : keys.flatMap(a => { const rest = tuples(keys, n - 1); return Array.from({ length: rest.length / (n - 1) }, (_, t) => [a, ...rest.slice(t * (n - 1), (t + 1) * (n - 1))]).flat(); }));
532
554
  // The keys some goals read a function at.
@@ -615,43 +637,56 @@ function match(pattern, term, x) {
615
637
  };
616
638
  return go(pattern, term) ? bound : undefined;
617
639
  }
618
- // Each quantified flag's witness w?f: a key where its condition fails, when it does; every-key facts are wanted there too.
619
- const witnesses = (m) => Object.entries(m.step.flags ?? {}).flatMap(([f, c]) => (c.op === 'all' ? [{ op: 'var', name: `w?${f}` }] : []));
620
- // The keys each quantified flag is assumed at (instances under ?f).
621
- const flagInstances = (m, keys) => Object.fromEntries(Object.entries(m.step.flags ?? {}).flatMap(([f, c]) => (c.op === 'all' ? [[`?${f}`, keys]] : [])));
640
+ // Each quantified flag's witness w?f (one per key, over several): a key where its condition fails, when it does;
641
+ // every-key facts are wanted there too.
642
+ const witnesses = (m) => Object.entries(m.step.flags ?? {}).flatMap(([f, c]) => (c.op === 'all' ? witnessesOf(f, c).map((name) => ({ op: 'var', name })) : []));
643
+ // The keys each quantified flag is assumed at (instances under ?f): over several keys, the tuples that relate the
644
+ // goals' own keys as a law about several keys takes them (every tuple, without them).
645
+ const flagInstances = (m, keys, focus = []) => Object.fromEntries(Object.entries(m.step.flags ?? {}).flatMap(([f, c]) => {
646
+ if (c.op !== 'all')
647
+ return [];
648
+ const n = boundOf(c).length;
649
+ return [[`?${f}`, n === 1 ? keys : focus.length ? related(keys, focus, n).flat() : tuples(keys, n)]];
650
+ }));
651
+ const pairFlags = (m) => Object.values(m.step.flags ?? {}).some(c => c.op === 'all' && !!c.keys?.length);
652
+ // How many keys each instance of a name takes: a law's, or a quantified flag's (?f).
653
+ const arity = (m, laws, n) => {
654
+ const q = n.startsWith('?') ? m?.step.flags?.[n.slice(1)] : undefined;
655
+ return q?.op === 'all' ? boundOf(q).length : (laws.find(l => l.name === n)?.keys?.length ?? 0) + 1;
656
+ };
622
657
  // The instances some goals need, for every entry point: the every-key laws and the quantified flags at `seed` (the keys
623
658
  // the step and the laws read, when known before any goal is built), then twice round: the facts at every key the goals
624
659
  // built so far read a function at, and, where the seed doesn't cover them (no seed, positions that shift, a law about
625
660
  // several keys), the every-key laws and flags at every key they read a map at (a law about several keys at tuples
626
661
  // relating `focus`, the goals' own keys).
627
662
  function instancesFor(m, laws, build, opts = {}) {
628
- 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) } : {});
629
- const reads = opts.reads ?? (!opts.seed || shifts(m) || laws.some(l => l.keys?.length));
663
+ 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, opts.focus) } : {});
664
+ const reads = opts.reads ?? (!opts.seed || shifts(m) || laws.some(l => l.keys?.length) || pairFlags(m));
630
665
  for (let round = 0; round < 2; round++) {
631
666
  if (m.facts?.length)
632
667
  inst = withFacts(m, inst, fnKeys(m, build(inst)));
633
668
  if (reads) {
634
669
  const keys = mapKeys(build(inst));
635
- inst = union(withReads(laws, inst, keys, opts.focus ?? []), flagInstances(m, keys));
670
+ inst = union(withReads(laws, inst, keys, opts.focus ?? []), flagInstances(m, keys, opts.focus), m);
636
671
  }
637
672
  }
638
673
  return inst;
639
674
  }
640
675
  // Instances without the keys that read any of these names (a law about several keys loses each tuple that does).
641
- const avoiding = (inst, names, laws) => {
676
+ const avoiding = (inst, names, laws, m) => {
642
677
  const bad = (e) => names.some(n => JSON.stringify(e).includes(`"name":${JSON.stringify(n)}`));
643
678
  return Object.fromEntries(Object.entries(inst).map(([n, keys]) => {
644
- const arity = (laws.find(l => l.name === n)?.keys?.length ?? 0) + 1;
645
- const tuples = Array.from({ length: Math.floor(keys.length / arity) }, (_, t) => keys.slice(t * arity, (t + 1) * arity));
646
- return [n, tuples.filter(tuple => !tuple.some(bad)).flat()];
679
+ const a = arity(m, laws, n), groups = Array.from({ length: Math.floor(keys.length / a) }, (_, t) => keys.slice(t * a, (t + 1) * a));
680
+ return [n, groups.filter(tuple => !tuple.some(bad)).flat()];
647
681
  }));
648
682
  };
649
- // Two sets of instances together: each name at the keys of both.
650
- const union = (a, b) => {
683
+ // Two sets of instances together: each name at the keys (or tuples) of both.
684
+ const union = (a, b, m) => {
651
685
  const out = { ...a };
652
686
  for (const [n, keys] of Object.entries(b)) {
653
- const seen = new Set((out[n] ?? []).map(e => JSON.stringify(e)));
654
- out[n] = [...(out[n] ?? []), ...keys.filter(e => !seen.has(JSON.stringify(e)))];
687
+ const k = arity(m, [], n), groups = (ks) => Array.from({ length: Math.floor(ks.length / k) }, (_, t) => ks.slice(t * k, (t + 1) * k));
688
+ const seen = new Set(groups(out[n] ?? []).map(g => JSON.stringify(g)));
689
+ out[n] = [...(out[n] ?? []), ...groups(keys).filter(g => !seen.has(JSON.stringify(g))).flat()];
655
690
  }
656
691
  return out;
657
692
  };
@@ -763,12 +798,12 @@ function proveStepLawsAsGiven(m, laws, stepLaws, base) {
763
798
  // law's later frames read keys no other step law has); where positions shift or a law is about several keys, the
764
799
  // keys its goal reads a map at too. A frame's hypotheses read no name the run introduces later (kernel.ts's
765
800
  // stepZone), so a k-step law drops the keys that read one.
766
- const shared = [...readKeys(plain, laws), ...witnesses(plain)], named = [...laws, ...(plain.facts ?? [])], reads = shifts(plain) || laws.some(l => l.keys?.length);
801
+ const shared = [...readKeys(plain, laws), ...witnesses(plain)], named = [...laws, ...(plain.facts ?? [])], reads = shifts(plain) || laws.some(l => l.keys?.length) || pairFlags(plain);
767
802
  const own = (sl) => [...[sl.then, ...(sl.when === undefined ? [] : Array.isArray(sl.when) ? sl.when : [sl.when])].flatMap(readKeysC),
768
803
  ...[sl.forall, ...(sl.keys ?? [])].flatMap(key => (key ? [{ op: 'var', name: key }] : []))];
769
804
  const perStep = {};
770
805
  for (const sl of stepLaws) {
771
- const zone = stepZone(plain, sl.steps ?? 1), keep = (inst) => avoiding(inst, zone, named);
806
+ const zone = stepZone(plain, sl.steps ?? 1), keep = (inst) => avoiding(inst, zone, named, plain);
772
807
  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 }));
773
808
  }
774
809
  const instances = perStep[stepLaws[0]?.name ?? ''] ?? {};
@@ -1,6 +1,6 @@
1
1
  import { atMost, n, not, pointWrite } from "../core/machine.js";
2
2
  import { add, k, scale, sub } from "../core/solver.js";
3
- import { canon, columnTerm, stateName, termColumn, historyMachine, foldObligations, leafConstraints, loopLiveness, loopMachine, linear, livenessHelpful, livenessParts, livenessRanks, loopObligations, trueC, obligations, progressObligations, stepLawObligations, substC, withProducts } from "../kernel/kernel.js";
3
+ import { boundOf, canon, columnTerm, stateName, termColumn, historyMachine, foldObligations, leafConstraints, loopLiveness, loopMachine, linear, livenessHelpful, livenessParts, livenessRanks, loopObligations, trueC, obligations, progressObligations, stepLawObligations, substC, witnessesOf, withProducts } from "../kernel/kernel.js";
4
4
  import { grow, proveBuilt, proveLaws, proveStepLaws } from "../kernel/prove.js";
5
5
  import { refinement, refinementSides } from "../core/compose.js";
6
6
  import { leanSurface, leanTerms } from "./lean-terms.js";
@@ -137,7 +137,9 @@ export function encode(ex, tamper = false, dropStep = true, globalVars, globalMa
137
137
  coefs.pop();
138
138
  return coefs;
139
139
  };
140
- const row = (lin) => `⟨${list(dense(lin).map(int))}, ${int(lin['1'] ?? 0)}⟩`;
140
+ // Rows by their nonzero coefficients (Kernel.lean's sp builds the dense row): most of a wide row is zeros.
141
+ const sparse = (coefs) => list(coefs.flatMap((x, j) => (x === 0 ? [] : [`(${j}, ${int(x)})`])));
142
+ const row = (lin) => `(sp ${sparse(dense(lin))} ${int(lin['1'] ?? 0)})`;
141
143
  const { e, c } = leanTerms(vars, maps, ex.name);
142
144
  const tab = cols.map(name => (name.includes('[') ? `.cell ${maps.indexOf(cellMap(name))} ${row(cellKey(name))}`
143
145
  : name.startsWith('⟦') ? `.term (${e(columnTerm(name))})`
@@ -171,22 +173,35 @@ export function encode(ex, tamper = false, dropStep = true, globalVars, globalMa
171
173
  const q = Object.fromEntries(Object.entries(from).filter(([key]) => key !== '1').map(([key, v]) => [key, v / s.g]));
172
174
  const cc = Math.ceil((from['1'] ?? 0) / s.g);
173
175
  replay.push({ ...q, 1: cc });
174
- return `.cut ${s.of} ${int(s.g)} ${list(dense(q).map(int))} ${int(cc)}`;
176
+ return `.cut ${s.of} ${int(s.g)} (sparseL 0 ${sparse(dense(q))}) ${int(cc)}`;
175
177
  });
176
178
  if (tamper && dropStep && leafNo++ === 0)
177
179
  steps = steps.slice(0, -1); // the first leaf loses its last step
178
180
  // Cell pairs the path proves equal (Lean re-checks each).
179
- const eqs = [];
181
+ const eqs = [], pairs = [];
180
182
  const cellCols = cols.map((name, j) => ({ name, j })).filter(c2 => c2.name.includes('['));
181
183
  for (const [a, p] of cellCols.entries())
182
184
  for (const q of cellCols.slice(a + 1)) {
183
185
  if (cellMap(p.name) !== cellMap(q.name))
184
186
  continue;
185
187
  const d = sub(cellKey(p.name), cellKey(q.name));
186
- if (decide(d, path) === true && decide(scale(d, -1), path) === true)
188
+ if (decide(d, path) === true && decide(scale(d, -1), path) === true) {
187
189
  eqs.push(`(${p.j}, ${q.j})`);
190
+ pairs.push([p.name, q.name].sort().join('\n'));
191
+ }
188
192
  }
189
- return `.leaf ${list(used.map(u => row(rows[u])))} ${list(steps)} ${list(eqs)} ${list(prods.map(([i, j]) => `(${idx(i)}, ${idx(j)})`))}`;
193
+ // Each row with where Lean finds it (a hint: missed, Lean looks among all of the leaf's rows).
194
+ const hint = (src) => {
195
+ if (!src)
196
+ return '.any';
197
+ if ('atom' in src)
198
+ return `.atom ${path.size - 1 - src.atom}`; // Lean's path has the latest decision first
199
+ if ('fact' in src)
200
+ return `.fact ${src.fact}`;
201
+ const j = 'pair' in src ? pairs.indexOf([...src.pair].sort().join('\n')) : col.get('nat' in src ? src.nat : src.term) ?? -1;
202
+ return j < 0 ? '.any' : 'pair' in src ? `.eq ${j}` : 'nat' in src ? `.nat ${j}` : `.term ${j}`;
203
+ };
204
+ return `.leafAt ${list(used.map(u => `(${row(rows[u])}, ${hint(base[u].src)})`))} ${list(steps)} ${list(eqs)} ${list(prods.map(([i, j]) => `(${idx(i)}, ${idx(j)})`))}`;
190
205
  };
191
206
  return { tab: list(tab), hyps: list(goal.hyps.map(c)), tree: lean(tree, new Map()), cols, c, e };
192
207
  }
@@ -474,7 +489,7 @@ const flagsOfM = (m) => {
474
489
  // Sets, or functions with facts: both go through Sets.lean.
475
490
  export const hasSets = (m) => !!m.inputMaps?.length || Object.values(m.step.flags ?? {}).some(c => c.op === 'all') || Object.values(m.step.maps ?? {}).some(w => 'each' in w)
476
491
  || !!Object.keys(m.fns ?? {}).length || !!m.facts?.length;
477
- const boundKeys = (m) => [...Object.values(m.step.flags ?? {}).flatMap(c => (c.op === 'all' ? [c.key] : [])), ...Object.values(m.step.maps ?? {}).flatMap(w => ('each' in w ? [w.each] : []))];
492
+ const boundKeys = (m) => [...Object.values(m.step.flags ?? {}).flatMap(c => (c.op === 'all' ? boundOf(c) : [])), ...Object.values(m.step.maps ?? {}).flatMap(w => ('each' in w ? [w.each] : []))];
478
493
  // A machine with a history goes as its plain machine (kernel.ts's historyMachine: sums as state, queries as inputs,
479
494
  // the relations assumed), built in Lean from the machine and its history (History.lean's HMachine.plain), so every claim
480
495
  // below is about it; with the relations' record goals, its laws hold over histories too (hmachine_sound).
@@ -505,7 +520,7 @@ def ${id}_step${j} : SKStep := ⟨⟨${list(Array.from({ length: kk }, (_, s2) =
505
520
  ${list(finsts(inst))},
506
521
  ${list(pinsts(inst))},
507
522
  ${list(pfinsts(inst))},
508
- [],
523
+ ${pr.hyps},
509
524
  ${list(Array.from({ length: kk - 1 }, (_, s2) => list(Object.keys(m.state).map(n2 => `(${ix(n2)}, ${ix(stateName(n2, s2 + 1))})`))))}⟩
510
525
  theorem ${id}_step${j}_holds : SKStepHolds ${id}_machine ${id}_step${j} :=
511
526
  skstep_sound ${id}_machine ${id}_proof ${id}_step${j} ${id}_check (by decide +kernel)`;
@@ -580,9 +595,10 @@ theorem ${id}_orig_laws : ∀ ρ M, SReach ${id}_ghosted.orig ρ M → ∀ e ∈
580
595
  throw new Error(`${id}: not proven`);
581
596
  const lp = r.proof.laws, si = r.proof.instances;
582
597
  const flags = Object.entries(m.step.flags ?? {}), quantified = flags.filter(([, c]) => c.op === 'all').map(([f]) => f);
598
+ const qwits = (f) => witnessesOf(f, m.step.flags[f]);
583
599
  const carried = m.inputMaps ?? [], facts = m.facts ?? [], keysOf = [...new Set([...laws, ...facts].flatMap(l => [l.forall, ...(l.keys ?? [])].filter((k) => !!k)))];
584
600
  const k = Math.max(1, ...allSteps.map(sl => sl.steps ?? 1));
585
- const ext = [...m.inputs, ...quantified.flatMap(f => [`?${f}`, `w?${f}`])];
601
+ const ext = [...m.inputs, ...quantified.flatMap(f => [`?${f}`, ...qwits(f)])];
586
602
  const later = Array.from({ length: k - 1 }, (_, j) => ext.map(v => `${v}@${j + 2}`)).flat();
587
603
  const laterMaps = Array.from({ length: k - 1 }, (_, j) => carried.map(mp => `${mp}@${j + 2}`)).flat();
588
604
  const primes = allSteps.length ? Object.keys(m.state).map(s2 => `${s2}'`) : [];
@@ -635,9 +651,14 @@ theorem ${id}_orig_laws : ∀ ρ M, SReach ${id}_ghosted.orig ρ M → ∀ e ∈
635
651
  return Array.from({ length: Math.floor(keys.length / k) }, (_, t) => sub1(sub1(lw.holds, Object.fromEntries(lw.keys.map((kn, i) => [kn, keys[t * k + 1 + i]]))), { [lw.forall]: keys[t * k] }));
636
652
  }),
637
653
  w,
638
- ...allFlags.flatMap(([f, fc]) => (fc.op === 'all' ? [{ op: '<=', a: { op: 'var', name: `?${f}` }, b: zero1(1) },
639
- ...(inst[`?${f}`] ?? []).map((key) => ({ op: 'or', xs: [eqC({ op: 'var', name: `?${f}` }, zero1(0)), sub1(fc.c, { [fc.key]: key })] })),
640
- { op: 'or', xs: [eqC({ op: 'var', name: `?${f}` }, zero1(1)), { op: 'not', x: sub1(fc.c, { [fc.key]: { op: 'var', name: `w?${f}` } }) }] }] : []))];
654
+ ...allFlags.flatMap(([f, fc]) => {
655
+ if (fc.op !== 'all')
656
+ return [];
657
+ const names = boundOf(fc), n = names.length, at = (ts) => sub1(fc.c, Object.fromEntries(names.map((key, i) => [key, ts[i]]))), qi = inst[`?${f}`] ?? [];
658
+ return [{ op: '<=', a: { op: 'var', name: `?${f}` }, b: zero1(1) },
659
+ ...Array.from({ length: Math.floor(qi.length / n) }, (_, t) => ({ op: 'or', xs: [eqC({ op: 'var', name: `?${f}` }, zero1(0)), at(qi.slice(t * n, (t + 1) * n))] })),
660
+ { op: 'or', xs: [eqC({ op: 'var', name: `?${f}` }, zero1(1)), { op: 'not', x: at(qwits(f).map((name) => ({ op: 'var', name }))) }] }];
661
+ })];
641
662
  const last = { op: 'not', x: sInline(m.assume ?? trueC) }, zkeys = carriedKeys([...base, last]);
642
663
  return { name: 'allowed', hyps: [...base, ...(m.inputMaps ?? []).flatMap(mp => zkeys.map(t => eqC({ op: 'get', map: mp, key: t }, zero1(0)))), last], zkeys };
643
664
  };
@@ -647,7 +668,7 @@ theorem ${id}_orig_laws : ∀ ρ M, SReach ${id}_ghosted.orig ρ M → ∀ e ∈
647
668
  })]));
648
669
  const allowGoals = [...allowed.values()].flatMap(a => (a ? [a.out.goal] : []));
649
670
  const vars = [...new Set([...m.params, ...m.inputs, ...Object.keys(m.state), ...keysOf, ...keysOf.map(k2 => `${k2}#`), '#', ...flags.map(([f]) => `?${f}`),
650
- ...quantified.map(f => `w?${f}`), ...boundKeys(m), ...(opts.tamper ? boundKeys(opts.tamper.machine) : []), ...stepLaws.flatMap(sl => (sl.forall ? [sl.forall] : [])), ...(opts.free ?? []),
671
+ ...quantified.flatMap(qwits), ...boundKeys(m), ...(opts.tamper ? boundKeys(opts.tamper.machine) : []), ...stepLaws.flatMap(sl => (sl.forall ? [sl.forall] : [])), ...(opts.free ?? []),
651
672
  ...primes, ...later, ...Array.from({ length: k - 1 }, (_, j) => Object.keys(m.state).map(n2 => stateName(n2, j + 1))).flat(),
652
673
  ...(hm ? hm.history.record.map(f => `r.${f}`) : []), ...[...goalsL, ...goalsS, ...allowGoals, ...(hplain?.goals ?? [])].flatMap(g => g.hyps.flatMap(varsOf))])];
653
674
  const baseMaps = [...new Set([...Object.keys(m.maps ?? {}), ...Object.keys(m.step.maps ?? {}), ...Object.keys(m.fns ?? {})])];
@@ -672,8 +693,8 @@ theorem ${id}_orig_laws : ∀ ρ M, SReach ${id}_ghosted.orig ρ M → ∀ e ∈
672
693
  ${list(fs.flatMap(([f, c2]) => (c2.op === 'all' ? [] : [`(${ix(`?${f}`)}, ${c(fl[f])})`])))}⟩,
673
694
  ${list((mm.inputMaps ?? []).map(mp => String(mx(mp))))},
674
695
  ${list(ws.flatMap(([name, w]) => ('each' in w ? [`(${mx(name)}, ${ix(w.each)}, ${e(w.value)})`] : [])))},
675
- ${list(fs.flatMap(([f, c2]) => (c2.op === 'all' ? [`(${ix(`?${f}`)}, ${ix(c2.key)}, ${c(c2.c)})`] : [])))},
676
- ${list(fs.flatMap(([f, c2]) => (c2.op === 'all' ? [`(${ix(`?${f}`)}, ${ix(`w?${f}`)})`] : [])))},
696
+ ${list(fs.flatMap(([f, c2]) => (c2.op === 'all' ? [`(${ix(`?${f}`)}, ${list(boundOf(c2).map(key => String(ix(key))))}, ${c(c2.c)})`] : [])))},
697
+ ${list(fs.flatMap(([f, c2]) => (c2.op === 'all' ? [`(${ix(`?${f}`)}, ${list(witnessesOf(f, c2).map(w => String(ix(w))))})`] : [])))},
677
698
  ${list((mm.facts ?? []).filter(l => !l.keys?.length).map(l => `⟨${c(l.holds)}, ${l.forall ? `some ${ix(l.forall)}` : 'none'}⟩`))},
678
699
  ${list((mm.facts ?? []).filter(l => l.keys?.length).map(l => `(⟨${c(l.holds)}, some ${ix(l.forall)}⟩, ${list(l.keys.map(k2 => String(ix(k2))))})`))}⟩`;
679
700
  };
@@ -727,20 +748,24 @@ ${readable(id, laws, vars, mapNames, { binders: '(ρ : Nat → Nat) (μ : Nat
727
748
  const n = factsN[j].keys.length + 1;
728
749
  return Array.from({ length: Math.floor(keys.length / n) }, (_, t) => `(${j}, ${e(keys[t * n])}, ${list(keys.slice(t * n + 1, t * n + n).map(e))})`);
729
750
  });
730
- const qinsts = (inst) => Object.entries(inst).filter(([name]) => name.startsWith('?')).flatMap(([f, keys]) => keys.map(key => `(${ix(f)}, ${e(key)})`));
751
+ // Each quantified flag at its tuples (one key each, over one key).
752
+ const qinsts = (inst) => Object.entries(inst).filter(([name]) => name.startsWith('?')).flatMap(([f, keys]) => {
753
+ const q = m.step.flags?.[f.slice(1)], n = q?.op === 'all' ? boundOf(q).length : 1;
754
+ return Array.from({ length: Math.floor(keys.length / n) }, (_, t) => `(${ix(f)}, ${list(keys.slice(t * n, (t + 1) * n).map(e))})`);
755
+ });
731
756
  const entries = laws.map((law, j) => {
732
757
  const g0 = goalsL[2 * j], g1 = goalsL[2 * j + 1], a = enc(g0, lp.trees[g0.name]), b = enc(g1, lp.trees[g1.name]), inst = lp.instances[law.name] ?? {};
733
758
  return `def ${id}_l${j} : MLaw := ⟨${c(law.holds)}, ${law.forall ? `some ${ix(law.forall)}` : 'none'}⟩
734
759
  def ${id}_e${j} : SEntry := ⟨${id}_l${j}, ${ix(law.forall ? `${law.forall}#` : '#')}, ${list((law.keys ?? []).map(key => String(ix(key))))}, ${list(insts(inst))}, ${list(qinsts(inst))}, ${list(finsts(inst))}, ${list(pinsts(inst))}, ${list(pfinsts(inst))},
735
- (${a.tab}, [], ${a.tree}),
736
- (${b.tab}, [], ${b.tree})⟩`;
760
+ (${a.tab}, ${a.hyps}, ${a.tree}),
761
+ (${b.tab}, ${b.hyps}, ${b.tree})⟩`;
737
762
  });
738
763
  // A ghosted machine (compose.ts's ghost): the original and its ghosts, the machine Lean checks built from them
739
764
  // (Ghost.lean), so the laws that read no ghost are laws of the original.
740
765
  const orig = { ...m, state: Object.fromEntries(Object.entries(m.state).filter(([n2]) => !gv.includes(n2))), step: { ...m.step, state: Object.fromEntries(Object.entries(m.step.state ?? {}).filter(([n2]) => !gv.includes(n2))) } };
741
766
  const h = {
742
767
  step: sl => { const g = goalsS[allSteps.indexOf(sl)]; return { pr: enc(g, r.proof.trees[g.name]), inst: instOf(sl) }; },
743
- sproof: sl => { const { pr, inst } = h.step(sl); return `(${list(insts(inst))}, ${list(qinsts(inst))}, ${list(finsts(inst))}, ${list(pinsts(inst))}, ${list(pfinsts(inst))}, (${pr.tab}, [], ${pr.tree}))`; },
768
+ sproof: sl => { const { pr, inst } = h.step(sl); return `(${list(insts(inst))}, ${list(qinsts(inst))}, ${list(finsts(inst))}, ${list(pinsts(inst))}, ${list(pfinsts(inst))}, (${pr.tab}, ${pr.hyps}, ${pr.tree}))`; },
744
769
  allow: a => {
745
770
  const f2 = allowed.get(a);
746
771
  if (!f2)