gdp-wired 0.12.3 → 0.12.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -53,8 +53,8 @@ All of these are proven automatically, and each proof is also a Lean theorem:
53
53
  - **Maps, sets and lists of names**: laws at every key ("no day passes its cap"), sets a request carries, conditions
54
54
  about every key or every two keys ("a batch is taken only when its values increase"), lists kept in order
55
55
  (`examples/sets.ts`, `examples/lists.ts`, `examples/pairs.ts`).
56
- - **Step laws**: what one step does ("a balance only goes down", "a grant adds one"), and laws over several steps in a
57
- row.
56
+ - **Step laws**: what one step does ("a balance only goes down", "a grant adds one"), and laws over up to 64 steps in
57
+ a row (`examples/steps.ts`).
58
58
  - **Histories**: sums and queries over a log of requests, like a sliding-window rate limit.
59
59
  - **Orderings at any distance**: "a refund never follows a release", through ghost state that watches the rule.
60
60
  - **Liveness**: "every ticket issued is served", in every run that keeps sending the requests that help, including under
@@ -230,8 +230,10 @@ const soon = retry.law('the next try is at most 64 after the last request', ({ne
230
230
  carried map of its running values whose equations every request is assumed to meet (the encoding computes them from the
231
231
  array, and a request that doesn't meet them isn't allowed), read as `Lines.fold.total`; `Lines.every`, `some`, `has`,
232
232
  `increasing(by)`, `at(i)`. A lemma about the folds (`({taxed, total}) => taxed.atMost(total)`) is proven by induction
233
- over the items when the rule is built, then assumed at every prefix. A fold's start and step, and a lemma, may read the
234
- params (`{from: ({fee}) => fee, step: (acc, line, {ceil}) => acc.plus(min(line.amount, ceil))}`); a request is then
233
+ over the items when the rule is built, then assumed at every prefix; a decision (`decide`) assumes the equations and
234
+ lemmas too, at the list's end and wherever it reads the list. A lemma reads the folds, the prefix's length and the
235
+ params, not the items: a fact about every item is a fold's (a count of the items that meet it). A fold's start and
236
+ step, and a lemma, may read the params (`{from: ({fee}) => fee, step: (acc, line, {ceil}) => acc.plus(min(line.amount, ceil))}`); a request is then
235
237
  encoded with them, `rule.encodeInput(values, params)` (`examples/invoices.ts`).
236
238
  - **Folds over trees** (`fold(name).params(...).inputs(...).leaf(env => ({...})).node((env, left, right) => ({...}))`,
237
239
  `proveFold`): a tree as a value (`{leaf: inputs}` or `{node: inputs, left, right}`), each leaf's values from its inputs
@@ -336,6 +338,10 @@ or two lines and check in 25–44 ms, against 74–117 lines of hand proof and 1
336
338
  The proved Bend and Lean rules keep lists and scan them; gdp-wired runs maps and a log (unpruned in this window rule).
337
339
  Lean, compiled natively, wins where the state stays small. Details in [`docs/comparison.md`](docs/comparison.md).
338
340
 
341
+ Proving and checking: a law over 64 steps proves in about 5 s, and Lean checks its theorem in 47 s (6 s by
342
+ `native_decide`); a law over 32 steps proves in about a second. What changed since the first release, and by how much:
343
+ [`CHANGELOG.md`](CHANGELOG.md).
344
+
339
345
  ## Layout
340
346
 
341
347
  | Path | What |
@@ -343,7 +349,7 @@ Lean, compiled natively, wins where the state stays small. Details in [`docs/com
343
349
  | `core/` | Machines, the interpreter and JS compiler, the builders, composition (`compose.ts`), the linear solver and its checker, Houdini invariants, models, the first Lean export |
344
350
  | `kernel/` | The trusted kernel and the untrusted prover, with soundness fuzz (`kernel.test.ts`, `history.test.ts`) |
345
351
  | `lean-kernel/` | The kernel's Lean twin, its soundness proofs, and the exported trees |
346
- | `examples/` | The builders at work: `domains/` (187 rules across 21 domains, each with laws and broken variants), `harness.ts` (the triple check), `readme.ts`, `budget.ts`, `gdp.ts` (gdp-ts's example, its checks proven), `datatypes.ts` (records and variants), `lemmas.ts` (recurrences and their lemmas), `invoices.ts` (lists a request carries), `folds.ts` (folds over trees), `trees.ts` (trees and lists of lists in the state), `composition.ts`, `sets.ts`, `quantifiers.ts`, `functions.ts`, `lists.ts`, `sizes.ts`, `liveness.ts`, `steps.ts` (laws over up to 16 steps), and `jev-fabric/` (a Bend project's 59 proven laws, ported) |
352
+ | `examples/` | The builders at work: `domains/` (187 rules across 21 domains, each with laws and broken variants), `harness.ts` (the triple check), `readme.ts`, `budget.ts`, `gdp.ts` (gdp-ts's example, its checks proven), `datatypes.ts` (records and variants), `lemmas.ts` (recurrences and their lemmas), `invoices.ts` (lists a request carries), `folds.ts` (folds over trees), `trees.ts` (trees and lists of lists in the state), `composition.ts`, `sets.ts`, `quantifiers.ts`, `functions.ts`, `lists.ts`, `sizes.ts`, `liveness.ts`, `steps.ts` (laws over up to 64 steps), and `jev-fabric/` (a Bend project's 59 proven laws, ported) |
347
353
  | `docs/` | The comparison with Bend and Lean, and the full scope |
348
354
  | `compare/` | The same three rules in gdp-wired, Bend and plain Lean, checked and measured |
349
355
  | `research/` | Everything around the library ([`research/README.md`](research/README.md)): the GPU searches (`search/`, `hw/`, `modal-gpu/`), invariant and rank synthesis (`synth/`), relational laws and guard synthesis (`next/`), bit-vector laws (`sat/`), the enumeration and benchmarks (`enum/`, `gpu/`, `perf/`), the first Lean checker (`lean/`) and the first prototype, the audit path (`audit/`) |
@@ -371,7 +377,7 @@ bun test core/compile.test.ts core/runtime.test.ts kernel/history.test.ts
371
377
  cd kernel && bun run-kernel.ts && bun test kernel.test.ts
372
378
  ```
373
379
 
374
- The whole suite (122 jobs, about 20 minutes of wall time) runs on [Modal](https://modal.com):
380
+ The whole suite (159 jobs, about 6 minutes of wall time and 22 minutes of job time) runs on [Modal](https://modal.com):
375
381
 
376
382
  ```sh
377
383
  modal run modal/run.py --jobs modal/suite.json --out runs/modal/suite && sh modal/reap.sh
@@ -333,6 +333,10 @@ export declare class CarriedList<V, Fd extends string = never> {
333
333
  has(x: V): Cond;
334
334
  increasing(by: (item: V) => Term): Cond;
335
335
  equations(): Cond[];
336
+ equationsAt(keys: Term[]): Cond[];
337
+ private starts;
338
+ private get keyed();
339
+ private atKey;
336
340
  private get readsParams();
337
341
  private p;
338
342
  private running;
package/dist/core/dsl.js CHANGED
@@ -875,6 +875,43 @@ export class CellsRef {
875
875
  }
876
876
  }
877
877
  const fromOf = (d, p) => (typeof d.from === 'number' ? d.from : d.from(p));
878
+ // The keys a condition reads any of these maps at, as terms.
879
+ function keysRead(cs, maps) {
880
+ const out = [];
881
+ const e = (t) => {
882
+ if (t.op === 'get') {
883
+ if (maps.includes(t.map))
884
+ out.push(new Term(t.key));
885
+ e(t.key);
886
+ }
887
+ else if (t.op === 'if') {
888
+ c(t.c);
889
+ e(t.then);
890
+ e(t.else);
891
+ }
892
+ else if (t.op === 'mul')
893
+ e(t.a);
894
+ else if (t.op === 'add' || t.op === 'sub' || t.op === 'min' || t.op === 'max' || t.op === 'prod') {
895
+ e(t.a);
896
+ e(t.b);
897
+ }
898
+ };
899
+ const c = (x) => {
900
+ if (x.op === 'not')
901
+ c(x.x);
902
+ else if (x.op === 'and' || x.op === 'or')
903
+ x.xs.forEach(c);
904
+ else if (x.op === 'all')
905
+ c(x.c);
906
+ else if (x.op !== 'flag') {
907
+ e(x.a);
908
+ e(x.b);
909
+ }
910
+ };
911
+ cs.forEach(c);
912
+ return out;
913
+ }
914
+ const uniqueTerms = (ts) => [...new Map(ts.map(t => [JSON.stringify(t.e), t])).values()];
878
915
  export class CarriedList {
879
916
  name;
880
917
  kind;
@@ -908,15 +945,16 @@ export class CarriedList {
908
945
  increasing(by) { return everyKey(k => k.plus(1).lt(this.len).implies(by(this.at(k)).lt(by(this.at(k.plus(1)))))); }
909
946
  // What every request is assumed to meet: each fold's start, and at every key one condition (the item's own laws and
910
947
  // each fold's step below the length, each lemma up to it), so the prover closes one quantifier, not one per part.
911
- equations() {
948
+ equations() { return [...this.starts(), ...(this.keyed ? [everyKey(k => this.atKey(k))] : [])]; }
949
+ // The same at the keys given, without the quantifier: what a decision assumes of the list (Rule.decide).
950
+ equationsAt(keys) { return [...this.starts(), ...(this.keyed ? keys.map(k => this.atKey(k)) : [])]; }
951
+ starts() { return Object.entries(this.folds).map(([f, d]) => this.map(`${this.name}$${f}`).at(0).eq(fromOf(d, this.p()))); }
952
+ get keyed() { return this.kind.invariant(this.kind.value(this.name)).length + Object.keys(this.folds).length + Object.keys(this.lemmas).length > 0; }
953
+ atKey(k) {
912
954
  const folds = Object.entries(this.folds), lemmas = Object.values(this.lemmas);
913
- const at = (k) => {
914
- const x = this.at(k), steps = folds.map(([f, d]) => this.map(`${this.name}$${f}`).at(k.plus(1)).eq(d.step(this.map(`${this.name}$${f}`).at(k), x, this.p())));
915
- const below = [...this.kind.invariant(x), ...steps];
916
- return all(...(below.length ? [k.lt(this.len).implies(all(...below))] : []), ...(lemmas.length ? [k.atMost(this.len).implies(all(...lemmas.map(l => l(this.running(k), k, this.p()))))] : []));
917
- };
918
- const any = this.kind.invariant(this.kind.value(this.name)).length + folds.length + lemmas.length > 0;
919
- return [...folds.map(([f, d]) => this.map(`${this.name}$${f}`).at(0).eq(fromOf(d, this.p()))), ...(any ? [everyKey(at)] : [])];
955
+ const x = this.at(k), steps = folds.map(([f, d]) => this.map(`${this.name}$${f}`).at(k.plus(1)).eq(d.step(this.map(`${this.name}$${f}`).at(k), x, this.p())));
956
+ const below = [...this.kind.invariant(x), ...steps];
957
+ return all(...(below.length ? [k.lt(this.len).implies(all(...below))] : []), ...(lemmas.length ? [k.atMost(this.len).implies(all(...lemmas.map(l => l(this.running(k), k, this.p()))))] : []));
920
958
  }
921
959
  // Whether a fold reads the params (its start a function, or its step taking them).
922
960
  get readsParams() { return Object.values(this.folds).some(d => typeof d.from === 'function' || d.step.length > 2); }
@@ -1704,11 +1742,15 @@ export class Rule {
1704
1742
  return typeof spec.helpful === 'function' ? { ...base, request: req(spec.helpful) } : { ...base, helpful: spec.helpful.map(h => ({ ...(h.when ? { when: h.when(env).c } : {}), request: req(h.request) })) };
1705
1743
  }
1706
1744
  // A decision: whenever the params and the request meet `given`, `then` holds. Decisions don't read the state; the
1707
- // functions' facts are assumed at the arguments the decision reads them at.
1745
+ // functions' facts are assumed at the arguments the decision reads them at, and a carried list's equations (each
1746
+ // fold's start, its step and its lemmas) at the list's end, the key before it and every key the decision reads it at.
1708
1747
  decide(name, given, then) {
1709
1748
  const env = this.env({ flags: false, state: false, queries: false });
1710
1749
  const g = given(env), hyps = [...this.kindAssumptions(), ...this.spec.pre.map(f => f(env).c), ...this.spec.assume.map(f => f(env).c), ...(Array.isArray(g) ? g : [g]).map(c => c.c)];
1711
- const t = then(env).c, goal = { name, hyps: [...hyps, { op: 'not', x: t }] };
1750
+ const t = then(env).c;
1751
+ const lists = (this.spec.lists2 ?? []).map(([n]) => env[n]);
1752
+ const listed = lists.flatMap(l => l.equationsAt(uniqueTerms([l.len, l.len.minus(1), ...keysRead([...hyps, t], l.maps)])).map(c => c.c));
1753
+ const goal = { name, hyps: [...listed, ...hyps, { op: 'not', x: t }] };
1712
1754
  const facts = this.spec.fns?.length || this.spec.facts?.length ? factsFor(this.machine, goal) : [];
1713
1755
  return withSource({ name, rule: this, given: hyps, then: t, goal: { name, hyps: [...facts, ...goal.hyps] } }, { decision: { given: given, then: then, own: Array.isArray(g) ? g.length : 1 } });
1714
1756
  }
@@ -2349,7 +2391,9 @@ export function crossCheckDecision(d, opts = {}) {
2349
2391
  let checked = 0;
2350
2392
  for (let i = 0; i < (opts.trials ?? 500); i++) {
2351
2393
  const ps = r.randomParams(rand, bound), input = r.randomInput(rand, bound, ps), values = { ...ps, ...numbersOf(input) };
2352
- const st = { scalars: {}, maps: Object.fromEntries(Object.entries(input).filter((e) => typeof e[1] !== 'number').map(([n, cells]) => [n, new Map(cells)])) };
2394
+ // a map a request carries reads 0 past its cells (a list's encoding keeps no 0 cell)
2395
+ const carried = Object.entries(input).filter((e) => typeof e[1] !== 'number');
2396
+ const st = { scalars: {}, maps: Object.fromEntries(carried.map(([n, cells]) => [n, new Map(cells)])), defaults: Object.fromEntries(carried.map(([n]) => [n, 0])) };
2353
2397
  const keys = exactKeys(keysOf(bound, st), values, st);
2354
2398
  const sc = { vars: values, maps: Object.fromEntries(Object.entries(st.maps).map(([n, cells]) => [n, { cells, dflt: 0 }])), keys, flags: {} };
2355
2399
  const [g, t] = inScope(sc, () => { const env = r['env']({ flags: false, state: false, queries: false }), g = src.given(env); return [all(...(Array.isArray(g) ? g : [g])).ok, src.then(env).ok]; });
@@ -364,6 +364,30 @@ export function interpret(m) {
364
364
  // The same machine as JavaScript source: what an app ships. A step reads the state it was given (every query, flag,
365
365
  // scalar, key and value first), then writes the maps and the records in place, so it costs the same however large they
366
366
  // grow; the state passed in is consumed. fork copies a state to branch from.
367
+ // The runtime's helpers: a compiled module declares the ones it calls (and the ones those call), no others.
368
+ const RUNTIME = [
369
+ ['$n', `function $n(v, what) { if (typeof v !== 'number' || !Number.isSafeInteger(v) || v < 0) throw new RangeError(what + ' must be a natural below 2^53, not ' + v); return v; }`],
370
+ ['$x', `function $x(v) { if (v > 9007199254740991) throw new RangeError('a value passed 2^53 - 1, past which the proofs over the naturals no longer describe the run'); return v; }`],
371
+ ['$m', `function $m(v, what) { if (!(v instanceof Map)) throw new RangeError(what + ' must be a Map of naturals'); for (const [key, x] of v) { $n(key, 'a key of ' + what); $n(x, 'a cell of ' + what); } return v; }`],
372
+ ['$keys', `function $keys(maps, names) { const keys = new Set(); let past = 0; for (const c of Object.values(maps)) for (const key of c.keys()) past = Math.max(past, key + 1); for (const n of names) for (const key of maps[n].keys()) keys.add(key); return [...keys, past]; }`],
373
+ ['$breaks', `function $breaks(maps, reads, cmps) { const p = new Set([0]); for (const [n, d, minus] of reads) { for (const s of maps[n].keys()) { if (minus) p.add(s + d); else if (s >= d) p.add(s - d); } if (minus) p.add(d); } for (const [v, d, minus] of cmps) { if (minus) { p.add(v + d); p.add(d); } else if (v >= d) p.add(v - d); } const keys = new Set(); for (const q of p) for (let i = -1; i <= 2; i++) if (q + i >= 0) keys.add(q + i); const sorted = [...keys].sort((a, b) => a - b); sorted.push(sorted[sorted.length - 1] + 1); return sorted; }`],
374
+ ['$spread', `function $spread(keys, n) { const out = new Set(); for (const k of keys) for (let i = 0; i < n; i++) out.add(k + i); return [...out].sort((a, b) => a - b); }`],
375
+ ['$dense', `function $dense(keys, f) { const top = keys[keys.length - 1]; if (top > 1000000) throw new RangeError('a write at every key reads its key in arithmetic past a million keys (' + top + ')'); const d = f(top), cells = new Map(); for (let k = 0; k < top; k++) { const v = f(k); if (v !== d) cells.set(k, v); } return {cells, d}; }`],
376
+ ['$every', `function $every(keys, f) { const past = keys[keys.length - 1], d = f(past), cells = new Map(); for (let i = 0; i < keys.length - 1; i++) { const v = f(keys[i]); if (v !== d) cells.set(keys[i], v); } return {cells, d}; }`],
377
+ ];
378
+ function runtimeFor(code) {
379
+ const used = new Set();
380
+ for (let grew = true; grew;) {
381
+ grew = false;
382
+ const text = code + RUNTIME.filter(([name]) => used.has(name)).map(([, src]) => src).join('');
383
+ for (const [name] of RUNTIME)
384
+ if (!used.has(name) && text.includes(`${name}(`)) {
385
+ used.add(name);
386
+ grew = true;
387
+ }
388
+ }
389
+ return RUNTIME.filter(([name]) => used.has(name)).map(([, src]) => `${src}\n`).join('');
390
+ }
367
391
  export function compileJs(m) {
368
392
  const { h, plain, queries, carried } = runnable(m);
369
393
  const id = (name) => JSON.stringify(name);
@@ -451,16 +475,8 @@ export function compileJs(m) {
451
475
  'if (head > 1024 && 2 * head > records.length) { records.splice(0, head); head = 0; }', `return {scalars, maps: state.maps, history: records, head${h.prune ? ', last: order' : ''}};`);
452
476
  else
453
477
  lines.push('return Object.keys(dft).length ? {scalars, maps: state.maps, defaults: dft} : {scalars, maps: state.maps};');
454
- return `// Generated from the machine ${m.name}. step consumes its state: keep the one it returns (fork to branch).
455
- function $n(v, what) { if (typeof v !== 'number' || !Number.isSafeInteger(v) || v < 0) throw new RangeError(what + ' must be a natural below 2^53, not ' + v); return v; }
456
- function $x(v) { if (v > 9007199254740991) throw new RangeError('a value passed 2^53 - 1, past which the proofs over the naturals no longer describe the run'); return v; }
457
- function $m(v, what) { if (!(v instanceof Map)) throw new RangeError(what + ' must be a Map of naturals'); for (const [key, x] of v) { $n(key, 'a key of ' + what); $n(x, 'a cell of ' + what); } return v; }
458
- function $keys(maps, names) { const keys = new Set(); let past = 0; for (const c of Object.values(maps)) for (const key of c.keys()) past = Math.max(past, key + 1); for (const n of names) for (const key of maps[n].keys()) keys.add(key); return [...keys, past]; }
459
- ${fns.map((name, i) => `const fn${i} = (${m.fns[name].js});\n${m.fns[name].call ? `const fnc${i} = (${m.fns[name].call});\n` : ''}`).join('')}function $breaks(maps, reads, cmps) { const p = new Set([0]); for (const [n, d, minus] of reads) { for (const s of maps[n].keys()) { if (minus) p.add(s + d); else if (s >= d) p.add(s - d); } if (minus) p.add(d); } for (const [v, d, minus] of cmps) { if (minus) { p.add(v + d); p.add(d); } else if (v >= d) p.add(v - d); } const keys = new Set(); for (const q of p) for (let i = -1; i <= 2; i++) if (q + i >= 0) keys.add(q + i); const sorted = [...keys].sort((a, b) => a - b); sorted.push(sorted[sorted.length - 1] + 1); return sorted; }
460
- function $spread(keys, n) { const out = new Set(); for (const k of keys) for (let i = 0; i < n; i++) out.add(k + i); return [...out].sort((a, b) => a - b); }
461
- function $dense(keys, f) { const top = keys[keys.length - 1]; if (top > 1000000) throw new RangeError('a write at every key reads its key in arithmetic past a million keys (' + top + ')'); const d = f(top), cells = new Map(); for (let k = 0; k < top; k++) { const v = f(k); if (v !== d) cells.set(k, v); } return {cells, d}; }
462
- function $every(keys, f) { const past = keys[keys.length - 1], d = f(past), cells = new Map(); for (let i = 0; i < keys.length - 1; i++) { const v = f(keys[i]); if (v !== d) cells.set(keys[i], v); } return {cells, d}; }
463
- export function init(params) {
478
+ const fnDefs = fns.map((name, i) => `const fn${i} = (${m.fns[name].js});\n${m.fns[name].call ? `const fnc${i} = (${m.fns[name].call});\n` : ''}`).join('');
479
+ const code = `export function init(params) {
464
480
  const env = ${env(false)}, s = {};
465
481
  return {scalars: {${Object.entries(plain.state).map(([name, e]) => `${id(name)}: ${ex(e)}`).join(', ')}}, maps: {${Object.keys(plain.maps ?? {}).map(name => `${id(name)}: new Map()`).join(', ')}}${h ? ', history: [], head: 0' : ''}};
466
482
  }
@@ -471,6 +487,8 @@ export function step(params, state, input) {
471
487
  ${lines.join('\n ')}
472
488
  }
473
489
  `;
490
+ return `// Generated from the machine ${m.name}. step consumes its state: keep the one it returns (fork to branch).
491
+ ${runtimeFor(fnDefs + code)}${fnDefs}${code}`;
474
492
  }
475
493
  // Generated code as a function. Every caller passes source this library generated from its own terms, or a function's
476
494
  // `js` the rule's author wrote (code, as the rule itself is), never data; the lint's implied-eval rule is waived here
@@ -1163,12 +1163,14 @@ export function stepLawObligations(machine, laws, instances, sl) {
1163
1163
  // `perStep`: a step law's own instances, where it has them (else `instances`).
1164
1164
  // Each frame's hypotheses read no name a later step's request is read by: the condition the Lean twin's proof needs
1165
1165
  // (Sets.lean's checkSKStep, `avoids (szone s sl)`; plain flags are inlined here, so their names never occur), checked
1166
- // here alike.
1166
+ // here alike. A quantified flag's keys are free of it: where the flag is 1 its condition holds at every key, so at a
1167
+ // key a later step names too (and the flag, its witness and its condition read none).
1168
+ const lawsOnly = (inst) => Object.fromEntries(Object.entries(inst).filter(([n]) => !n.startsWith('?')));
1167
1169
  export function checkStepLaws(m, laws, stepLaws, proof) {
1168
1170
  return checkLaws(m, laws, proof.laws) && stepLaws.every(sl => {
1169
1171
  const inst = proof.perStep?.[sl.name] ?? proof.instances, plain = m.history ? historyMachine(m).machine : m;
1170
1172
  const zone = stepZone(plain, sl.steps ?? 1);
1171
- return [...assumed(laws, inst), ...assumed(plain.facts ?? [], inst), ...closing(plain, inst)].every(c => !mentions(c, zone))
1173
+ return [...assumed(laws, inst), ...assumed(plain.facts ?? [], inst), ...closing(plain, lawsOnly(inst))].every(c => !mentions(c, zone))
1172
1174
  && stepLawObligations(m, laws, inst, sl).every(g => proof.trees[g.name] !== undefined && check(g, proof.trees[g.name]));
1173
1175
  });
1174
1176
  }
@@ -665,6 +665,17 @@ const flagInstances = (m, keys, focus = []) => Object.fromEntries(Object.entries
665
665
  const n = boundOf(c).length;
666
666
  return [[`?${f}`, n === 1 ? keys : focus.length ? related(keys, focus, n).flat() : tuples(keys, n)]];
667
667
  }));
668
+ // A k-step law whose steps keep a condition (a flag that holds at one step and is read again at the next) fails at a
669
+ // later step's witness only where the condition held before: so, per quantified flag over one key, the instances at the
670
+ // law's own keys and that flag's witness at every step (a later step's witness is a key an earlier frame may name, as a
671
+ // flag holds at every key: kernel.ts's checkStepLaws). Few instances, where the shared ones are many and mostly idle.
672
+ const keptAcross = (m, laws, inst, own, k) => k < 2 ? [] : Object.entries(m.step.flags ?? {}).flatMap(([f, c]) => {
673
+ if (c.op !== 'all' || c.keys?.length)
674
+ return [];
675
+ const ws = Array.from({ length: k }, (_, i) => ({ op: 'var', name: i ? `w?${f}@${i + 1}` : `w?${f}` })), keys = [...own, ...ws.slice(0, 1)];
676
+ const keep = new Set(keys.map(e => JSON.stringify(e)));
677
+ return [Object.fromEntries(Object.entries(inst).map(([n, ks]) => [n, arity(m, laws, n) === 1 ? [...ks.filter(e => keep.has(JSON.stringify(e))), ...(n === `?${f}` ? ws.slice(1) : [])] : ks]))];
678
+ });
668
679
  const pairFlags = (m) => Object.values(m.step.flags ?? {}).some(c => c.op === 'all' && !!c.keys?.length);
669
680
  // How many keys each instance of a name takes: a law's, or a quantified flag's (?f).
670
681
  const arity = (m, laws, n) => {
@@ -677,7 +688,8 @@ const arity = (m, laws, n) => {
677
688
  // several keys), the every-key laws and flags at every key they read a map at (a law about several keys at tuples
678
689
  // relating `focus`, the goals' own keys).
679
690
  function instancesFor(m, laws, build, opts = {}) {
680
- 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) } : {});
691
+ const seed = opts.seed && [...new Map(opts.seed.map(e => [JSON.stringify(e), e])).values()];
692
+ let inst = opts.start ?? (seed ? { ...Object.fromEntries(laws.filter(l => l.forall).map(l => [l.name, l.keys?.length ? [] : seed])), ...flagInstances(m, seed, opts.focus) } : {});
681
693
  const reads = opts.reads ?? (!opts.seed || shifts(m) || laws.some(l => l.keys?.length) || pairFlags(m));
682
694
  for (let round = 0; round < 2; round++) {
683
695
  if (m.facts?.length)
@@ -703,7 +715,7 @@ const union = (a, b, m) => {
703
715
  for (const [n, keys] of Object.entries(b)) {
704
716
  const k = arity(m, [], n), groups = (ks) => Array.from({ length: Math.floor(ks.length / k) }, (_, t) => ks.slice(t * k, (t + 1) * k));
705
717
  const seen = new Set(groups(out[n] ?? []).map(g => JSON.stringify(g)));
706
- out[n] = [...(out[n] ?? []), ...groups(keys).filter(g => !seen.has(JSON.stringify(g))).flat()];
718
+ out[n] = [...(out[n] ?? []), ...groups(keys).filter(g => !seen.has(JSON.stringify(g)) && !!seen.add(JSON.stringify(g))).flat()];
707
719
  }
708
720
  return out;
709
721
  };
@@ -814,7 +826,8 @@ function proveStepLawsAsGiven(m, laws, stepLaws, base) {
814
826
  // Each step law its own instances, from its own goal: the keys the step and the laws read, and its own (a k-step
815
827
  // law's later frames read keys no other step law has); where positions shift or a law is about several keys, the
816
828
  // keys its goal reads a map at too. A frame's hypotheses read no name the run introduces later (kernel.ts's
817
- // stepZone), so a k-step law drops the keys that read one.
829
+ // stepZone), so a k-step law drops the keys that read one; but a flag that holds at every key holds at a later step's
830
+ // key too: where these instances don't prove it, each flag at its own witness at every step is tried (keptAcross).
818
831
  const shared = [...readKeys(plain, laws), ...witnesses(plain)], named = [...laws, ...(plain.facts ?? [])], reads = shifts(plain) || laws.some(l => l.keys?.length) || pairFlags(plain);
819
832
  const own = (sl) => [...[sl.then, ...(sl.when === undefined ? [] : Array.isArray(sl.when) ? sl.when : [sl.when])].flatMap(readKeysC),
820
833
  ...[sl.forall, ...(sl.keys ?? [])].flatMap(key => (key ? [{ op: 'var', name: key }] : []))];
@@ -828,7 +841,17 @@ function proveStepLawsAsGiven(m, laws, stepLaws, base) {
828
841
  let nodes = base.nodes, leaves = base.leaves;
829
842
  for (const sl of stepLaws)
830
843
  for (const goal of stepLawObligations(m, laws, perStep[sl.name] ?? instances, sl)) {
831
- const out = grow(goal);
844
+ let out = grow(goal);
845
+ // A k-step law that reads a condition kept across its steps: each flag at its own witnesses, at every step.
846
+ if (!('tree' in out))
847
+ for (const kept of keptAcross(plain, laws, perStep[sl.name] ?? instances, own(sl), sl.steps ?? 1)) {
848
+ const again = grow(stepLawObligations(m, laws, kept, sl)[0]);
849
+ if ('tree' in again) {
850
+ out = again;
851
+ perStep[sl.name] = kept;
852
+ break;
853
+ }
854
+ }
832
855
  if (!('tree' in out))
833
856
  return { proven: false, nodes, leaves, failed: { goal: goal.name, stuck: out.stuck, ...(out.witness ? { witness: out.witness } : {}), ...(out.why ? { why: out.why } : {}) }, checked: false };
834
857
  trees[goal.name] = out.tree;