gdp-wired 0.15.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -69,6 +69,10 @@ All of these are proven automatically, and each proof is also a Lean theorem:
69
69
  machine runs as, with each guard and action it names given again in gdp-wired's terms, and `conforms(chart, machine,
70
70
  createActor)` drives XState's own actor and the compiled rule with the same random events, comparing every state and
71
71
  context; what a chart can't run the same way is refused when built, with the reason (`examples/xstate.ts`).
72
+ - **What nobody stated, looked for when the prover alone stops**: where `proveRule` or `proveSteps` doesn't prove the laws
73
+ and no run breaks them, it looks for lemmas about the rule's recurrences and lists (`discover`) and then invariants
74
+ (`inferLaws`), within 10 s (`{search: {ms}}`; `{search: false}`, the prover alone); the verdict's `found` holds what
75
+ proved them, as laws and lemmas with the source to paste.
72
76
  - **Invariants nobody wrote** (`gdp-wired/infer`): `inferLaws(rule, laws)` proves a law that holds but isn't inductive
73
77
  alone, with the laws it finds (bounds, sums and differences over the state, the same where a flag or a choice holds
74
78
  one value, and laws about every key of a map), each proven by the kernel with yours and handed back as the
@@ -116,6 +120,7 @@ sound.
116
120
  | Part | Trusted? | What it does |
117
121
  | --- | --- | --- |
118
122
  | `kernel/kernel.ts`, `core/check.ts` | **Yes** | Turns a rule and its laws into goals, and checks a proof tree for each: case splits on linear facts, then a linear refutation at each leaf |
123
+ | `kernel/cyclic.ts` | **Yes** | Cyclic proofs: builds each node's goals (its own, and for each node it uses, that node's hypotheses and that the rank drops) and checks each tree with kernel.ts's check. Its twin is `lean-kernel/Cyclic.lean` (`cyclic_sound`) |
119
124
  | `kernel/prove.ts` | No | Finds the proof trees. If it's wrong, the kernel rejects the tree |
120
125
  | `lean-kernel/` | Optional | The kernel's twin in Lean 4.34.0, every rule proven sound there with the standard axioms only. The exporter sends the kernel's real trees; tampered ones compute to false |
121
126
  | `research/search/`, `research/synth/`, `research/hw/` | No | The GPU searches for breaking runs, and invariant and rank synthesis. Every hit is replayed or proven before it counts |
@@ -246,6 +251,13 @@ const soon = retry.law('the next try is at most 64 after the last request', ({ne
246
251
  `Acct.set(k, value)`, `Acct.each(k => value)`) and `.items({Queue: Job})` (`push`, `pop`, `set`, `insertAt`,
247
252
  `removeAt`, `apply`, `has`, `within`, `increasing(by)`), one map per variable of the kind; `rule.decode` reads them back
248
253
  as objects and arrays.
254
+ - **Sequences a request carries** (`.carriesSeq('Path')`, and tables of them, `.carriesSeqs('Paths')`): a sequence of
255
+ naturals as positions and a length, `Path.at(i)`, `Path.len`, a table's row `Paths.row(r)`; `A.prefixOf(B)`,
256
+ `startsWith`, `equals`, `extends(B, k)`, `has(x)`, each a condition about every position below a length, so laws about
257
+ prefixes of paths need no induction or lemma. The request gives an array of naturals (interned names), a table an
258
+ array of them. `everyBelow(n, k => ...)` checks bookkeeping a request carries row by row (where each grant went). A
259
+ condition about every row of a table can't compare the rows' sequences (that is two keys at once); compare a row at a
260
+ named row, a witness, instead (`examples/scopes.ts`: pi-fabric's path scopes, covering, attenuation and normalization).
249
261
  - **Lists a request carries** (`.carriesList('Lines', Line, folds, lemmas)`): the items one carried map per variable of
250
262
  the kind, the length `Lines.len`, each fold (`{total: {from: 0, step: (acc, line) => acc.plus(line.amount)}}`) a
251
263
  carried map of its running values whose equations every request is assumed to meet (the encoding computes them from the
@@ -256,9 +256,9 @@ export class Chart {
256
256
  missed.push({ name, law: r.law(`${name} is never reached`, (env) => env[reg.field].is(l).not()) });
257
257
  }
258
258
  const proven = (v) => v.proven && v.checked;
259
- const allMissed = missed.length > 0 && proven(proveRule(r, [...this.chartLaws(), ...missed.map(x => x.law)]));
259
+ const allMissed = missed.length > 0 && proven(proveRule(r, [...this.chartLaws(), ...missed.map(x => x.law)], { search: false }));
260
260
  for (const x of missed)
261
- reached[x.name] = allMissed || proven(proveRule(r, [...this.chartLaws(), x.law])) ? 'unreachable' : 'unknown';
261
+ reached[x.name] = allMissed || proven(proveRule(r, [...this.chartLaws(), x.law], { search: false })) ? 'unreachable' : 'unknown';
262
262
  // A way out is a transition to a state: an internal one stays where it is.
263
263
  const stuck = regions.flatMap(reg => reg.leaves.filter(l => !reg.finals.has(l) && !placed.some(t => t.to !== undefined && t.region === reg.name && t.sources.includes(l))).map(l => label(reg, l)));
264
264
  // Pairs as specific as each other, on one event, from a state they share: proven never both enabled, all at once,
@@ -270,9 +270,9 @@ export class Chart {
270
270
  pairs.push({ t, u, law: r.stepLaw(`transitions ${t.order} and ${u.order} on ${t.event} are never both enabled`, (env) => all(env[`on${i}`], env[`on${j}`]).not()) });
271
271
  }));
272
272
  const ambiguous = [];
273
- if (pairs.length && !proven(proveSteps(r, this.chartLaws(), pairs.map(x => x.law))))
273
+ if (pairs.length && !proven(proveSteps(r, this.chartLaws(), pairs.map(x => x.law), { search: false })))
274
274
  for (const { t, u, law } of pairs) {
275
- const v = proveSteps(r, this.chartLaws(), [law]);
275
+ const v = proveSteps(r, this.chartLaws(), [law], { search: false });
276
276
  if (!proven(v))
277
277
  ambiguous.push({ event: t.event, transitions: [t.order, u.order], verdict: v });
278
278
  }
@@ -0,0 +1,51 @@
1
+ import type { C, E, Law, Machine } from './machine.js';
2
+ import type { Fold, Instances } from '../kernel/kernel.js';
3
+ import { type CNode, type Cyclic, type Fact, type Use } from '../kernel/cyclic.js';
4
+ export type NodeSpec = Omit<CNode, 'uses' | 'main'> & {
5
+ uses: Omit<Use, 'premises' | 'descent'>[];
6
+ };
7
+ export type CyclicVerdict = {
8
+ proven: true;
9
+ checked: boolean;
10
+ proof: Cyclic;
11
+ why?: string;
12
+ } | {
13
+ proven: false;
14
+ checked: false;
15
+ node: string;
16
+ goal: string;
17
+ stuck: string;
18
+ };
19
+ export declare const pair: (u: E, key: E) => E;
20
+ export declare function proveCyclic(facts: Fact[], specs: NodeSpec[]): CyclicVerdict;
21
+ export type RecurCase = {
22
+ name: string;
23
+ args: string[];
24
+ zero: E;
25
+ next: E;
26
+ lemmas: {
27
+ name: string;
28
+ holds: C;
29
+ }[];
30
+ between: {
31
+ name: string;
32
+ holds: C;
33
+ }[];
34
+ facts: Law[];
35
+ };
36
+ export declare function recurNodes(r: RecurCase): {
37
+ facts: Fact[];
38
+ specs: NodeSpec[];
39
+ };
40
+ export declare const cyclicMachine: (m: Machine, laws?: Law[]) => boolean;
41
+ export declare function machineNodes(m: Machine, laws: Law[], instances: Record<string, Instances>): {
42
+ facts: Fact[];
43
+ specs: NodeSpec[];
44
+ };
45
+ export declare function foldNodes(f: Fold, laws: Law[]): {
46
+ facts: Fact[];
47
+ specs: NodeSpec[];
48
+ };
49
+ export declare const cyclicOn: () => boolean;
50
+ export declare const quietly: <T>(f: () => T) => T;
51
+ export declare function crossCyclic(kind: string, name: string, usual: boolean, run: () => CyclicVerdict, usualMs?: number): void;
@@ -0,0 +1,295 @@
1
+ import { substC, substE } from "../kernel/kernel.js";
2
+ import { descentGoal, mainGoal, premiseGoal, refuseCyclic } from "../kernel/cyclic.js";
3
+ import { cellProducts, grow } from "../kernel/prove.js";
4
+ const v = (name) => ({ op: 'var', name });
5
+ const k = (value) => ({ op: 'const', value });
6
+ const add = (a, b) => ({ op: 'add', a, b });
7
+ const less1 = (a) => ({ op: 'sub', a, b: k(1) });
8
+ const get = (map, key) => ({ op: 'get', map, key });
9
+ const le = (a, b) => ({ op: '<=', a, b });
10
+ const eq = (a, b) => ({ op: '=', a, b });
11
+ const and = (...xs) => (xs.length === 1 ? xs[0] : { op: 'and', xs });
12
+ const implies = (a, b) => ({ op: 'or', xs: [{ op: 'not', x: a }, b] });
13
+ const yes = le(k(0), k(0));
14
+ const vars = (s) => ({ vars: s, maps: {}, flags: {} });
15
+ // A map's cell at time u and key k, in the run's one map over both: u + k·Ω, where Ω is above the run's length T, so
16
+ // that no two (u, k) with u <= T meet (u < Ω). Linear in u, and two keys' cells differ by (k − k')·Ω.
17
+ const omega = v('O$');
18
+ export const pair = (u, key) => add(u, { op: 'prod', a: key, b: omega });
19
+ // Every goal grown, then the whole checked by the kernel.
20
+ export function proveCyclic(facts, specs) {
21
+ const nodes = [];
22
+ const grown = (g, node) => {
23
+ const was = cellProducts.on;
24
+ cellProducts.on = true;
25
+ let o;
26
+ try {
27
+ o = grow(g);
28
+ }
29
+ finally {
30
+ cellProducts.on = was;
31
+ }
32
+ return 'tree' in o ? o.tree : { proven: false, checked: false, node, goal: g.name, stuck: o.stuck };
33
+ };
34
+ for (const n of specs) {
35
+ const main = grown(mainGoal(facts, specs, n), n.name);
36
+ if ('proven' in main)
37
+ return main;
38
+ const uses = [];
39
+ for (const u of n.uses) {
40
+ const m = specs[u.node];
41
+ if (!m)
42
+ return { proven: false, checked: false, node: n.name, goal: `use of node ${u.node}`, stuck: 'no such node' };
43
+ const premises = [];
44
+ for (const [i, c] of m.hyps.entries()) {
45
+ const t = grown(premiseGoal(facts, n, u, c, i), n.name);
46
+ if ('proven' in t)
47
+ return t;
48
+ premises.push(t);
49
+ }
50
+ const descent = grown(descentGoal(facts, n, u, m), n.name);
51
+ if ('proven' in descent)
52
+ return descent;
53
+ uses.push({ ...u, premises, descent });
54
+ }
55
+ nodes.push({ ...n, uses, main });
56
+ }
57
+ const proof = { facts, nodes }, why = refuseCyclic(proof);
58
+ return { proven: true, checked: why === undefined, proof, ...(why ? { why } : {}) };
59
+ }
60
+ // The facts of a machine's functions (laws about every key of them), as cyclic facts.
61
+ const factOf = (l) => ({ name: l.name, keys: [...(l.forall ? [l.forall] : []), ...(l.keys ?? [])], holds: l.holds });
62
+ export function recurNodes(r) {
63
+ const f = `${r.name}$`, x = v('x'), y = v('y'), kk = v('k$');
64
+ const facts = [
65
+ { name: `${r.name}(0)`, keys: [], holds: eq(get(f, k(0)), r.zero) },
66
+ { name: `${r.name}(x + 1)`, keys: ['k$'], holds: eq(get(f, add(kk, k(1))), substE(r.next, vars({ x: kk, prev: get(f, kk) }))) },
67
+ ...r.facts.map(factOf),
68
+ ];
69
+ const at = (t) => vars({ x: t, fx: get(f, t) });
70
+ const n = r.lemmas.length;
71
+ const lemmaUses = (to, guard) => r.lemmas.map((_, j) => ({ node: j, subst: { x: to }, guard }));
72
+ const specs = r.lemmas.map(l => ({
73
+ name: `${r.name}: ${l.name}`, hyps: [], concl: substC(l.holds, at(x)), rank: [k(0), x],
74
+ insts: [{ fact: 0, at: [] }, { fact: 1, at: [less1(x)] }], uses: lemmaUses(less1(x), le(k(1), x)),
75
+ }));
76
+ // A law between two points: for every x <= y, by induction on y from x, the lemmas at x, at y and at y − 1.
77
+ for (const b of r.between)
78
+ specs.push({
79
+ name: `${r.name}: ${b.name}`, hyps: [], rank: [k(1), y],
80
+ concl: implies(le(x, y), substC(b.holds, vars({ x, fx: get(f, x), y, fy: get(f, y) }))),
81
+ insts: [{ fact: 1, at: [less1(y)] }],
82
+ uses: [...r.between.map((_, j) => ({ node: n + j, subst: { y: less1(y) }, guard: le(add(x, k(1)), y) })), ...lemmaUses(x, yes), ...lemmaUses(y, yes), ...lemmaUses(less1(y), le(k(1), y))],
83
+ });
84
+ return { facts, specs: withReads(facts, specs) };
85
+ }
86
+ // 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
87
+ // alone) at each key f is read at, twice over (a fact's own reads give more keys).
88
+ function withReads(facts, specs) {
89
+ const simple = facts.flatMap((f, i) => {
90
+ if (f.keys.length !== 1)
91
+ return [];
92
+ const reads = [...JSON.stringify(f.holds).matchAll(/"op":"get","map":"([^"]+)","key":\{"op":"var","name":"([^"]+)"\}/g)].filter(m => m[2] === f.keys[0]);
93
+ return reads.map(m => ({ fact: i, map: m[1] }));
94
+ });
95
+ if (!simple.length)
96
+ return specs;
97
+ const readsIn = (t, map) => { const out = []; const walk = (x) => { if (x && typeof x === 'object') {
98
+ const o = x;
99
+ if (o.op === 'get' && o.map === map)
100
+ out.push(o.key);
101
+ for (const val of Object.values(o))
102
+ walk(val);
103
+ } }; walk(t); return out; };
104
+ return specs.map(n => {
105
+ const insts = [...n.insts], seen = new Set(insts.map(p => JSON.stringify(p)));
106
+ for (let round = 0; round < 2; round++) {
107
+ const goal = [n.hyps, n.concl, insts.map(p => substC(facts[p.fact].holds, vars(Object.fromEntries(facts[p.fact].keys.map((key, i) => [key, p.at[i]]))))), n.uses.map(u => substC(specs[u.node].concl, vars(u.subst)))];
108
+ for (const s of simple)
109
+ for (const key of readsIn(goal, s.map)) {
110
+ const p = { fact: s.fact, at: [key] }, id = JSON.stringify(p);
111
+ if (!seen.has(id)) {
112
+ seen.add(id);
113
+ insts.push(p);
114
+ }
115
+ }
116
+ }
117
+ return { ...n, insts };
118
+ });
119
+ }
120
+ // --- machines -------------------------------------------------------------------------------------------------------
121
+ // The machines this instance covers: no history, no flag holding at every key, and no law that reads a map at a key
122
+ // 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
123
+ // the prover would have to prove equal through products (`machineNodes` proves those laws far slower than the usual
124
+ // rule, past its budget: research/cyclic/probe.ts).
125
+ export const cyclicMachine = (m, laws = []) => {
126
+ const maps = Object.keys(m.maps ?? {});
127
+ const nested = (t) => { let found = false; const walk = (x, inKey) => { if (found || !x || typeof x !== 'object')
128
+ return; const o = x; if (o.op === 'get' && inKey && maps.includes(o.map)) {
129
+ found = true;
130
+ return;
131
+ } if (o.op === 'get') {
132
+ walk(o.key, true);
133
+ return;
134
+ } for (const val of Object.values(o))
135
+ walk(val, inKey); }; walk(t, false); return found; };
136
+ return !m.history && !Object.values(m.step.flags ?? {}).some(c => c.op === 'all') && !laws.some(l => nested(l.holds));
137
+ };
138
+ // The run as maps over time: names at time u read as their maps there.
139
+ function trace(m) {
140
+ const maps = [...Object.keys(m.maps ?? {}), ...(m.inputMaps ?? [])];
141
+ const at = (u) => ({
142
+ vars: Object.fromEntries([...Object.keys(m.state), ...m.inputs].map(n => [n, get(`${n}@`, u)])),
143
+ maps: Object.fromEntries(maps.map(map => [map, (key) => get(`${map}@`, pair(u, key))])), flags: {},
144
+ });
145
+ return { at };
146
+ }
147
+ // Every map read at `from` (the same term) read at `to`.
148
+ const readsAt = (t, from, to) => {
149
+ const id = JSON.stringify(from);
150
+ const walk = (x) => {
151
+ if (Array.isArray(x))
152
+ return x.map(walk);
153
+ if (!x || typeof x !== 'object')
154
+ return x;
155
+ const o = x;
156
+ if (o.op === 'get' && JSON.stringify(o.key) === id)
157
+ return { ...o, key: to };
158
+ return Object.fromEntries(Object.entries(o).map(([key, val]) => [key, walk(val)]));
159
+ };
160
+ return walk(t);
161
+ };
162
+ // A machine's flags, each its condition over the frame before the step (through the flags before it).
163
+ const flagsOf = (m) => {
164
+ const out = {};
165
+ for (const [f, c] of Object.entries(m.step.flags ?? {}))
166
+ out[f] = substC(c, { vars: {}, maps: {}, flags: out });
167
+ return out;
168
+ };
169
+ export function machineNodes(m, laws, instances) {
170
+ const { at } = trace(m), flags = flagsOf(m), inline = (t) => substC(t, { vars: {}, maps: {}, flags });
171
+ const t = v('t$'), T = v('T$'), u = v('u$'), key = v('k$');
172
+ const inlineE = (e) => substE(e, { vars: {}, maps: {}, flags });
173
+ const now = (c, when) => substC(c, at(when)), nowE = (e, when) => substE(inlineE(e), at(when));
174
+ const state = Object.keys(m.state), mapNames = Object.keys(m.maps ?? {});
175
+ // The start and a step, each stated at the time u it gives (a start at u <= 0, a step at 1 <= u <= T from u − 1), so a
176
+ // node reads them at its own time t: the cells they give are the cells it reads, with no key to prove equal.
177
+ const start = le(u, k(0)), stepped = and(le(k(1), u), le(u, T)), prev = less1(u);
178
+ const facts = [
179
+ { name: `${m.name}: the start`, keys: ['u$'], holds: implies(start, and(yes, ...state.map(s => eq(get(`${s}@`, u), m.state[s])))) },
180
+ { name: `${m.name}: a step`, keys: ['u$'], holds: implies(stepped, and(yes, ...(m.assume ? [now(inline(m.assume), prev)] : []), ...state.map(s => eq(get(`${s}@`, u), nowE(m.step.state?.[s] ?? v(s), prev))))) },
181
+ ...mapNames.flatMap((map) => {
182
+ const w = m.step.maps?.[map], cell = (when) => get(`${map}@`, pair(when, key));
183
+ // A write at one key: where the key is the write's, the value with every read at the write's key read at the key
184
+ // instead (the same cell there), so the cells the step gives are at the key the node reads.
185
+ const next = !w ? cell(prev) : 'each' in w ? substE(substE(w.value, { vars: { [w.each]: key }, maps: {}, flags }), at(prev))
186
+ : { op: 'if', c: eq(key, nowE(w.key, prev)), then: substE(readsAt(inlineE(w.value), inlineE(w.key), key), at(prev)), else: cell(prev) };
187
+ return [
188
+ { name: `${m.name}: ${map} at the start`, keys: ['u$', 'k$'], holds: implies(start, eq(cell(u), k(m.maps[map]))) },
189
+ { name: `${m.name}: ${map} at a step`, keys: ['u$', 'k$'], holds: implies(stepped, eq(cell(u), next)) },
190
+ { name: `${m.name}: ${map}'s cells are naturals`, keys: ['u$', 'k$'], holds: le(k(0), cell(u)) },
191
+ ];
192
+ }),
193
+ ...(m.facts ?? []).map(factOf),
194
+ // That a state variable's value at a time is a natural holds of every model: read at the node's time, it makes the
195
+ // value a column of its own (a value read only inside a product would be none, and no two reads of it equal).
196
+ ...state.map((s) => ({ name: `${m.name}: ${s} is a natural`, keys: ['u$'], holds: le(k(0), get(`${s}@`, u)) })),
197
+ { name: `${m.name}: params are naturals`, keys: [], holds: and(yes, ...m.params.map(x => le(k(0), v(x)))) },
198
+ ];
199
+ const fi = (name) => facts.findIndex(f => f.name === name);
200
+ const keysOf = (l) => [...(l.forall ? [l.forall] : []), ...(l.keys ?? [])];
201
+ // A map read at the node's time t at key e: that map's start, step and natural facts at (t, e).
202
+ const mapInsts = (c) => {
203
+ const out = [], seen = new Set();
204
+ const walk = (x) => {
205
+ if (!x || typeof x !== 'object')
206
+ return;
207
+ const o = x;
208
+ if (o.op === 'get' && typeof o.map === 'string' && o.map.endsWith('@') && mapNames.includes(o.map.slice(0, -1))) {
209
+ const kk = o.key;
210
+ if (kk.op === 'add' && JSON.stringify(kk.a) === JSON.stringify(t) && kk.b.op === 'prod') {
211
+ const read = kk.b.a, id = JSON.stringify(read), map = o.map.slice(0, -1);
212
+ if (!seen.has(`${map}:${id}`)) {
213
+ seen.add(`${map}:${id}`);
214
+ out.push(...['at the start', 'at a step', '\'s cells are naturals'].map(what => ({ fact: fi(`${m.name}: ${map}${what.startsWith('\'') ? what : ` ${what}`}`), at: [t, read] })));
215
+ }
216
+ }
217
+ }
218
+ for (const val of Object.values(o))
219
+ walk(val);
220
+ };
221
+ walk(c);
222
+ return out;
223
+ };
224
+ const specs = laws.map(l => {
225
+ const inst = instances[l.name] ?? {};
226
+ // Terms over the frame before the step (the machine's proof proves a law about every key at a fresh key k#: here
227
+ // the node's own k).
228
+ const fresh = Object.fromEntries(laws.flatMap(x => (x.forall ? [[`${x.forall}#`, v(x.forall)]] : [])));
229
+ const pre = at(less1(t)), before = (e) => substE(substE(e, vars(fresh)), pre);
230
+ // The laws at the step before, at the keys the machine's proof assumes them at.
231
+ const uses = laws.flatMap((l2, j) => {
232
+ const ks = keysOf(l2);
233
+ if (!ks.length)
234
+ return [{ node: j, subst: { 't$': less1(t) }, guard: le(k(1), t) }];
235
+ const flat = inst[l2.name] ?? [];
236
+ return Array.from({ length: Math.floor(flat.length / ks.length) }, (_, i) => ({ node: j, subst: { 't$': less1(t), ...Object.fromEntries(ks.map((kn, q) => [kn, before(flat[i * ks.length + q])])) }, guard: le(k(1), t) }));
237
+ });
238
+ // The functions' facts at the keys the proof names, over the frame before the step (at t = 0, the start's).
239
+ const factInsts = (m.facts ?? []).flatMap(f => {
240
+ const ks = keysOf(f), flat = inst[f.name] ?? [];
241
+ if (!ks.length)
242
+ return [{ fact: fi(f.name), at: [] }];
243
+ return Array.from({ length: Math.floor(flat.length / ks.length) }, (_, i) => ({ fact: fi(f.name), at: flat.slice(i * ks.length, (i + 1) * ks.length).map(before) }));
244
+ });
245
+ const concl = substC(l.holds, at(t));
246
+ const useConcls = uses.map(us => substC(substC(laws[us.node].holds, at(t)), vars(us.subst)));
247
+ const base = mapInsts([concl, useConcls]);
248
+ return {
249
+ name: `${m.name}: ${l.name}`, hyps: [...(m.pre ? [m.pre] : []), le(t, T), ...(mapNames.length ? [{ op: '<', a: T, b: omega }] : [])], concl, rank: [t],
250
+ insts: [{ fact: 0, at: [t] }, { fact: 1, at: [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,
251
+ };
252
+ });
253
+ return { facts, specs };
254
+ }
255
+ // --- folds over trees -----------------------------------------------------------------------------------------------
256
+ // 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,
257
+ // or a node (Kind[n] = 1) with children Left[n], Right[n] numbered below n, its values from theirs and its inputs.
258
+ export function foldNodes(f, laws) {
259
+ const n = v('n$'), N = v('N$'), names = Object.keys(f.leaf);
260
+ const at = (i) => vars(Object.fromEntries([...names, ...f.inputs].map(x => [x, get(`${x}@`, i)])));
261
+ const kind = (i) => get('Kind@', i), left = (i) => get('Left@', i), right = (i) => get('Right@', i);
262
+ const side = (child, p) => Object.fromEntries(names.map(x => [`${p}${x}`, get(`${x}@`, child)]));
263
+ const nodeAt = (i) => vars({ ...Object.fromEntries(f.inputs.map(x => [x, get(`${x}@`, i)])), ...side(left(i), 'l_'), ...side(right(i), 'r_') });
264
+ const assume = (i) => (f.assume ? [substC(f.assume, at(i))] : []);
265
+ const facts = [{ name: `${f.name}: the table`, keys: ['n$'], holds: implies({ op: '<', a: n, b: N }, and(le(kind(n), k(1)), implies(eq(kind(n), k(0)), and(yes, ...assume(n), ...names.map(x => eq(get(`${x}@`, n), substE(f.leaf[x], at(n)))))), implies(eq(kind(n), k(1)), and({ op: '<', a: left(n), b: n }, { op: '<', a: right(n), b: n }, ...assume(n), ...names.map(x => eq(get(`${x}@`, n), substE(f.node[x], nodeAt(n)))))))) },
266
+ // Each value a natural (true of every model), so a value read only inside a product is a column of its own.
267
+ { name: `${f.name}: values are naturals`, keys: ['n$'], holds: and(yes, ...names.map(x => le(k(0), get(`${x}@`, n)))) },
268
+ { name: `${f.name}: params are naturals`, keys: [], holds: and(yes, ...f.params.map(x => le(k(0), v(x)))) }];
269
+ const specs = laws.map(l => ({
270
+ name: `${f.name}: ${l.name}`, hyps: [...(f.pre ? [f.pre] : []), { op: '<', a: n, b: N }], concl: substC(l.holds, at(n)), rank: [n],
271
+ insts: [{ fact: 0, at: [n] }, ...[n, left(n), right(n)].map(i => ({ fact: 1, at: [i] })), { fact: 2, at: [] }],
272
+ uses: laws.flatMap((_, j) => [left, right].map(c => ({ node: j, subst: { 'n$': c(n) }, guard: eq(kind(n), k(1)) }))),
273
+ }));
274
+ return { facts, specs };
275
+ }
276
+ // --- the cyclic route beside the usual one (GDP_CYCLIC=1) -------------------------------------------------------------
277
+ // With GDP_CYCLIC=1, every proof the instances above cover is made again as a cyclic proof, and a verdict that differs
278
+ // throws: the suite run so shows the rules are instances. Each comparison is printed to stderr as a line `cyclic: {...}`.
279
+ const env = () => globalThis.process?.env;
280
+ let quiet = 0;
281
+ export const cyclicOn = () => quiet === 0 && env()?.GDP_CYCLIC === '1';
282
+ // A search's many trial proofs (core/discover.ts, core/infer.ts) aren't compared: what it finds is proven again.
283
+ export const quietly = (f) => { quiet++; try {
284
+ return f();
285
+ }
286
+ finally {
287
+ quiet--;
288
+ } };
289
+ export function crossCyclic(kind, name, usual, run, usualMs) {
290
+ const t0 = performance.now(), out = run(), ms = Math.round(performance.now() - t0);
291
+ const cyclic = out.proven && out.checked;
292
+ console.error(`cyclic: ${JSON.stringify({ kind, name, usual, cyclic, ms, ...(usualMs === undefined ? {} : { usualMs: Math.round(usualMs) }), ...(out.proven ? { nodes: out.proof.nodes.length, ...(out.why ? { why: out.why } : {}) } : { goal: out.goal, stuck: out.stuck }) })}`);
293
+ if (cyclic !== usual)
294
+ 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})`}`);
295
+ }
@@ -12,7 +12,7 @@ export type Discovery = {
12
12
  rule: AnyRule;
13
13
  found: Record<string, Found>;
14
14
  };
15
- export declare function discover(r: AnyRule, laws: Law[], stepLaws?: StepLaw[]): Discovery;
15
+ export declare function discover(r: AnyRule, laws: Law[], stepLaws?: StepLaw[], first?: Verdict): Discovery;
16
16
  type TreeFold = {
17
17
  fold: Fold;
18
18
  lemmas?: Law[];
@@ -10,6 +10,7 @@
10
10
  // const out = discover(r, laws, stepLaws)
11
11
  // out.verdict // the laws, proven of out.rule: r with its functions and lists made again with the lemmas
12
12
  // out.found // per function or list, the lemmas found, as source to paste into recur(...) or carriesList(...)
13
+ import { quietly } from "./cyclic.js";
13
14
  import { any, atMost, compileFunction, evalC, evalE, n, plus, prod, same, times, x } from "./machine.js";
14
15
  import { Unproven, lit, proveFold, proveSteps, recur, recurOf } from "./dsl.js";
15
16
  const seeded = (seed) => { let s = seed; return (k) => { s = (s * 1103515245 + 12345) % 2 ** 31; return Math.floor(s / 2 ** 31 * k); }; };
@@ -259,8 +260,11 @@ function houdiniList(r, part, cands, params) {
259
260
  // --- the search ----------------------------------------------------------------------------------------------------
260
261
  // The laws of r, and where they aren't proven as r states them, the lemmas found for its functions and lists: all that
261
262
  // is proven first, then each dropped in turn while the laws stay proven without it.
262
- export function discover(r, laws, stepLaws = []) {
263
- const first = proveSteps(r, laws, stepLaws);
263
+ // `first`: the laws' verdict on r alone, when the caller has it.
264
+ export function discover(r, laws, stepLaws = [], first) {
265
+ return quietly(() => discoverSearch(r, laws, stepLaws, first ?? proveSteps(r, laws, stepLaws, { search: false })));
266
+ }
267
+ function discoverSearch(r, laws, stepLaws, first) {
264
268
  if (first.proven && first.checked)
265
269
  return { verdict: first, rule: r, found: {} };
266
270
  const { params, fns, lists } = r.parts, ps = natParams(params);
@@ -284,7 +288,7 @@ export function discover(r, laws, stepLaws = []) {
284
288
  listKept[part[0]] = out.kept;
285
289
  }
286
290
  }
287
- const verdict = proveSteps(rule, laws, stepLaws);
291
+ const verdict = proveSteps(rule, laws, stepLaws, { search: false });
288
292
  const found = () => ({
289
293
  ...Object.fromEntries(Object.entries(fnMade).map(([n, m]) => { const args = [...(recurOf(m.d).def.args ?? [])]; return [n, { ...(m.lemmas.length ? { lemmas: m.lemmas.map(c => recurSrc(c, args, false)) } : {}), ...(m.between.length ? { between: m.between.map(c => recurSrc(c, args, true)) } : {}) }]; })),
290
294
  ...Object.fromEntries(Object.entries(listKept).map(([n, cs]) => [n, { lemmas: cs.map(c => listSrc(c, Object.keys(lists.find(l => l[0] === n)[2]), ps, c.target)) }])),
@@ -297,7 +301,7 @@ export function discover(r, laws, stepLaws = []) {
297
301
  // A rule made again and the laws proven of it; or the lemma whose proof failed without the one dropped.
298
302
  const tryRule = (next) => {
299
303
  try {
300
- const r2 = next(), v = proveSteps(r2, laws, stepLaws);
304
+ const r2 = next(), v = proveSteps(r2, laws, stepLaws, { search: false });
301
305
  return v.proven && v.checked ? { rule: r2, verdict: v } : undefined;
302
306
  }
303
307
  catch (e) {
@@ -405,7 +409,8 @@ const eOfSide = (side) => {
405
409
  return es.length ? es.slice(1).reduce((acc, t) => plus(acc, t), es[0]) : n(0);
406
410
  };
407
411
  const cOfCand = (c) => { const { pos, neg } = sides(c.lin), a = eOfSide(pos), b = eOfSide(neg); return c.rel === '=' ? same(a, b) : atMost(a, b); };
408
- export function discoverFold(f, laws) {
412
+ export function discoverFold(f, laws) { return quietly(() => discoverFoldSearch(f, laws)); }
413
+ function discoverFoldSearch(f, laws) {
409
414
  const stated = f.lemmas ?? [], first = proveFold({ fold: f.fold, lemmas: stated }, laws);
410
415
  if (first.proven && first.checked)
411
416
  return { verdict: first, lemmas: [], found: [] };
@@ -299,6 +299,26 @@ export declare class ListRef {
299
299
  within(): Forall;
300
300
  increasing(): Forall;
301
301
  }
302
+ export declare class SeqRef {
303
+ private readonly map;
304
+ readonly len: Term;
305
+ private readonly base?;
306
+ constructor(map: MapRef, len: Term, base?: Term | undefined);
307
+ at(i: TermLike): Term;
308
+ private agree;
309
+ prefixOf(o: SeqRef): Cond;
310
+ startsWith(o: SeqRef): Cond;
311
+ equals(o: SeqRef): Cond;
312
+ extends(o: SeqRef, by: TermLike): Cond;
313
+ has(x: TermLike): Cond;
314
+ }
315
+ export declare class SeqTable {
316
+ readonly name: string;
317
+ readonly rows: Term;
318
+ constructor(name: string, rows: Term);
319
+ row(r: TermLike): SeqRef;
320
+ }
321
+ export declare const everyBelow: (n: TermLike, f: (key: Term) => Cond) => Cond;
302
322
  export type PartsWrite = {
303
323
  readonly parts: Record<string, MapWrite>;
304
324
  };
@@ -473,6 +493,8 @@ type Spec = {
473
493
  nodes?: [string, Kind<unknown>, string[], Record<string, NodeFold<unknown, string>>][];
474
494
  kinded?: [string, Kind<unknown>][];
475
495
  carried?: string[];
496
+ seqs?: string[];
497
+ seqTables?: string[];
476
498
  lists2?: [string, Kind<unknown>, Record<string, ListFold<unknown>>, Record<string, ListLemma>][];
477
499
  fns?: [string, FnDef][];
478
500
  facts?: ((env: never) => Record<string, Cond | Forall>)[];
@@ -539,6 +561,12 @@ export declare class Rule<P extends Obj = {}, I extends Obj = {}, S extends Obj
539
561
  carries<const N extends string>(...names: N[]): Rule<P, I & {
540
562
  [k in N]: MapRef;
541
563
  }, S, M, F, R, Su, Q>;
564
+ carriesSeq<const N extends string>(...names: N[]): Rule<P, I & {
565
+ [k in N]: SeqRef;
566
+ }, S, M, F, R, Su, Q>;
567
+ carriesSeqs<const N extends string>(...names: N[]): Rule<P, I & {
568
+ [k in N]: SeqTable;
569
+ }, S, M, F, R, Su, Q>;
542
570
  carriesList<const N extends string, V, Fd extends string = never>(name: N, of: Kind<V>, folds?: Record<Fd, ListFold<V, P>>, lemmas?: Record<string, (acc: Record<Fd, Term>, i: Term, p: P) => Cond>): Rule<P, I & {
543
571
  [k in N]: CarriedList<V, Fd>;
544
572
  }, S, M, F, R, Su, Q>;
@@ -604,7 +632,7 @@ export declare class Rule<P extends Obj = {}, I extends Obj = {}, S extends Obj
604
632
  [k in keyof P]: Raw;
605
633
  }): Record<string, number>;
606
634
  encodeInput(values: {
607
- [k in keyof I]: I[k] extends MapRef ? ReadonlySet<number> | readonly number[] | ReadonlyMap<number, number> : I[k] extends CarriedList<any, any> ? readonly Raw[] : Raw;
635
+ [k in keyof I]: I[k] extends MapRef ? ReadonlySet<number> | readonly number[] | ReadonlyMap<number, number> : I[k] extends SeqRef ? readonly number[] : I[k] extends SeqTable ? readonly (readonly number[])[] : I[k] extends CarriedList<any, any> ? readonly Raw[] : Raw;
608
636
  }, params?: {
609
637
  [k in keyof P]: Raw;
610
638
  }): Request;
@@ -678,7 +706,7 @@ type Value<T> = {
678
706
  [k in keyof T]: ValueOf<T[k]>;
679
707
  };
680
708
  type RawInputs<T> = {
681
- [k in keyof T]: T[k] extends MapRef ? ReadonlySet<number> | readonly number[] | ReadonlyMap<number, number> : T[k] extends CarriedList<infer V, infer _F> ? readonly ValueOf<V>[] : ValueOf<T[k]>;
709
+ [k in keyof T]: T[k] extends MapRef ? ReadonlySet<number> | readonly number[] | ReadonlyMap<number, number> : T[k] extends SeqRef ? readonly number[] : T[k] extends SeqTable ? readonly (readonly number[])[] : T[k] extends CarriedList<infer V, infer _F> ? readonly ValueOf<V>[] : ValueOf<T[k]>;
682
710
  };
683
711
  type Field = Term | Cond | Int | Decimal | Choice<string> | Str | Variant<any, any> | Maybe<any> | {
684
712
  [f: string]: Field | number;
@@ -770,13 +798,30 @@ export type Verdict = {
770
798
  params: Record<string, number>;
771
799
  trace: Record<string, unknown>[];
772
800
  };
801
+ found?: Found;
802
+ };
803
+ export type Found = {
804
+ laws?: {
805
+ law: Law;
806
+ text: string;
807
+ source?: string;
808
+ }[];
809
+ lemmas?: Record<string, {
810
+ lemmas?: string[];
811
+ between?: string[];
812
+ }>;
813
+ };
814
+ export type ProveOptions = {
815
+ search?: boolean | {
816
+ ms?: number;
817
+ };
773
818
  };
774
819
  export declare class Unproven extends Error {
775
820
  readonly verdict: Verdict;
776
821
  constructor(what: string, verdict: Verdict);
777
822
  }
778
- export declare function proveRule(r: Rule<Obj, Obj, Obj, Obj, Obj, Obj, Obj, Obj>, laws: Law[]): Verdict;
779
- export declare function proveSteps(r: Rule<Obj, Obj, Obj, Obj, Obj, Obj, Obj, Obj>, laws: Law[], stepLaws: StepLaw[]): Verdict;
823
+ export declare function proveRule(r: Rule<Obj, Obj, Obj, Obj, Obj, Obj, Obj, Obj>, laws: Law[], opts?: ProveOptions): Verdict;
824
+ export declare function proveSteps(r: Rule<Obj, Obj, Obj, Obj, Obj, Obj, Obj, Obj>, laws: Law[], stepLaws: StepLaw[], opts?: ProveOptions): Verdict;
780
825
  export type { StepLaw };
781
826
  export type { Liveness };
782
827
  export declare function proveLeadsTo(r: Rule<Obj, Obj, Obj, Obj, Obj, Obj, Obj, Obj>, laws: Law[], l: Liveness): Verdict;