gdp-wired 0.16.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
@@ -120,6 +120,7 @@ sound.
120
120
  | Part | Trusted? | What it does |
121
121
  | --- | --- | --- |
122
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`) |
123
124
  | `kernel/prove.ts` | No | Finds the proof trees. If it's wrong, the kernel rejects the tree |
124
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 |
125
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 |
@@ -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
+ }
@@ -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); }; };
@@ -260,7 +261,10 @@ function houdiniList(r, part, cands, params) {
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
263
  // `first`: the laws' verdict on r alone, when the caller has it.
263
- export function discover(r, laws, stepLaws = [], first = proveSteps(r, laws, stepLaws, { search: false })) {
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);
@@ -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: [] };
package/dist/core/dsl.js CHANGED
@@ -18,7 +18,8 @@
18
18
  import { carriedView, compileFunction, evalC, evalE, interpret, loadJs, numbersOf, pairedArgs } from "./machine.js";
19
19
  import { allowed, keysOf, requestEnv, watch } from "./watch.js";
20
20
  import { check, foldObligations, loopObligations } from "../kernel/kernel.js";
21
- import { factsFor, grow, proveLaws, proveLiveness, proveStepLaws } from "../kernel/prove.js";
21
+ import { factsFor, grow, proveLaws, proveLiveness, proveStepLaws, withDeadline } from "../kernel/prove.js";
22
+ import { crossCyclic, cyclicOn, foldNodes, proveCyclic, quietly, recurNodes } from "./cyclic.js";
22
23
  import { discover } from "./discover.js";
23
24
  import { inferLaws } from "./infer.js";
24
25
  // --- terms and conditions -------------------------------------------------------------------------------------------
@@ -1399,6 +1400,10 @@ export const recur = (name, def, lemmas = {}, between = {}) => {
1399
1400
  const stated = Object.entries(lemmas).map(([n, f]) => { const holds = f(named('x'), named('fx'), envAt(own)).c; only(`lemma "${n}"`, holds, ['x', 'fx', ...args], uses.map(([u]) => u)); return { name: n, holds }; });
1400
1401
  const pairs = Object.entries(between).map(([n, f]) => { const holds = f(named('x'), named('fx'), named('y'), named('fy'), envAt(own)).c; only(`law between "${n}"`, holds, ['x', 'fx', 'y', 'fy', ...args], uses.map(([u]) => u)); return { name: n, holds }; });
1401
1402
  const argKinds = Object.fromEntries(args.map(a => [a, nat]));
1403
+ // The recurrence as a cyclic proof's instance (core/cyclic.ts, GDP_CYCLIC=1): the facts of the functions it reads, from
1404
+ // a rule that reads them.
1405
+ const recurCase = (between) => ({ name, args, zero, next, lemmas: stated, between,
1406
+ facts: uses.length ? rule(`${name}'s functions`).fns(def.fns ?? {}).inputs({}).state({ z: 0 }).step((env) => ({ z: env.z })).machine.facts ?? [] : [] });
1402
1407
  const failed = (what, v) => { if (!(v.proven && v.checked))
1403
1408
  throw new Unproven(`${name}'s ${what}`, v); };
1404
1409
  // The lemmas: x counting up from 0 and fx its value, each lemma a law of that machine.
@@ -1413,7 +1418,11 @@ export const recur = (name, def, lemmas = {}, between = {}) => {
1413
1418
  const r = r0.state({ x: 0, fx: ((p) => (zeroReads ? p.fx0 : zeroAt(args.map(a => p[a])))) })
1414
1419
  .step((env) => ({ x: env.x.plus(1), fx: def.next(env.x, env.fx, envAt(args.map(a => env[a]))) }));
1415
1420
  const laws = Object.entries(lemmas).map(([n, f]) => r.law(n, (env) => f(env.x, env.fx, envAt(args.map(a => env[a])))));
1416
- verdict = proveRule(r, laws, { search: false });
1421
+ // (Its machine's proof timed alone: the recurrence is compared below as its own instance.)
1422
+ const t0 = performance.now();
1423
+ verdict = quietly(() => proveRule(r, laws, { search: false }));
1424
+ if (cyclicOn())
1425
+ crossCyclic('recurrence', name, verdict.proven && verdict.checked, () => { const { facts, specs } = recurNodes(recurCase([])); return proveCyclic(facts, specs); }, performance.now() - t0);
1417
1426
  failed('lemmas', verdict);
1418
1427
  proofs.lemmas = { machine: r.machine, laws };
1419
1428
  }
@@ -1450,7 +1459,9 @@ export const recur = (name, def, lemmas = {}, between = {}) => {
1450
1459
  .step((env) => ({ y: env.y.plus(1), fy: def.next(env.y, env.fy, envAt(at(env))) }));
1451
1460
  const laws = [...Object.entries(lemmas).map(([n, l]) => r.law(`${n} at y`, (env) => l(env.y, env.fy, envAt(at(env))))),
1452
1461
  ...Object.entries(between).map(([n, l]) => r.law(n, (env) => l(env.x, env.fx, env.y, env.fy, envAt(at(env)))))];
1453
- const v = proveRule(r, laws, { search: false });
1462
+ const t0 = performance.now(), v = quietly(() => proveRule(r, laws, { search: false })), usualMs = performance.now() - t0;
1463
+ if (cyclicOn())
1464
+ crossCyclic('recurrence, between', name, v.proven && v.checked, () => { const { facts, specs } = recurNodes(recurCase(pairs)); return proveCyclic(facts, specs); }, usualMs);
1454
1465
  failed('laws between two points', v);
1455
1466
  proofs.between = { machine: r.machine, laws };
1456
1467
  verdict = { proven: true, checked: verdict.checked && v.checked };
@@ -2607,7 +2618,7 @@ function searched(r, laws, stepLaws, v, opts) {
2607
2618
  let rule = r, lemmas;
2608
2619
  const { fns, lists } = r.parts;
2609
2620
  if (lists.length || fns.some(([, d]) => recurOf(d))) {
2610
- const d = discover(r, laws, stepLaws, v);
2621
+ const d = withDeadline(until, () => discover(r, laws, stepLaws, v));
2611
2622
  if (d.verdict.proven && d.verdict.checked)
2612
2623
  return { ...d.verdict, found: { lemmas: d.found } };
2613
2624
  if (Object.keys(d.found).length)
@@ -2771,7 +2782,12 @@ export class FoldRule {
2771
2782
  }
2772
2783
  export const fold = (name) => new FoldRule({ name, params: [], inputs: [], pre: [], assume: [] });
2773
2784
  // A fold's laws proven over every tree (a rose fold's own laws, its children's lemmas, with them).
2774
- export const proveFold = (f, laws) => goalsVerdict(foldObligations(f.fold, [...(f.lemmas ?? []), ...laws]));
2785
+ export const proveFold = (f, laws) => {
2786
+ const all = [...(f.lemmas ?? []), ...laws], t0 = performance.now(), v = goalsVerdict(foldObligations(f.fold, all)), usualMs = performance.now() - t0;
2787
+ if (cyclicOn())
2788
+ crossCyclic('fold', f.fold.name, v.proven && v.checked, () => { const { facts, specs } = foldNodes(f.fold, all); return proveCyclic(facts, specs); }, usualMs);
2789
+ return v;
2790
+ };
2775
2791
  const asTerm = (t) => (t instanceof Term ? t : lit(t));
2776
2792
  // A fold over trees whose nodes have any number of children, as the kernel's binary fold (so the kernel and Lean's
2777
2793
  // fold_sound check it as they check any fold): a node's children become a chain of glue nodes, each folding one child
@@ -13,10 +13,11 @@
13
13
  // its witness is a counterexample to induction, so the candidate that rules it out (and the most of those seen) is
14
14
  // added; a chosen candidate that nothing supports, or that makes a proof slow, is dropped; the set proven as a whole
15
15
  // is the answer, after each chosen candidate the proof doesn't need is taken out again
16
+ import { quietly } from "./cyclic.js";
16
17
  import { any, atMost, evalC, get, interpret, n, not, plus, same, times, x } from "./machine.js";
17
18
  import { proveRule, proveSteps } from "./dsl.js";
18
19
  import { allowed, watch } from "./watch.js";
19
- import { proveLaw, proveLaws, proveStepLaws } from "../kernel/prove.js";
20
+ import { proveLaw, proveLaws, proveStepLaws, withDeadline } from "../kernel/prove.js";
20
21
  const KEY = 'key$found', AT = '@', K = '#';
21
22
  const seeded = (seed) => { let s = seed; return k => { s = (s * 1103515245 + 12345) % 2 ** 31; return Math.floor(s / 2 ** 31 * k); }; };
22
23
  const isRule = (t) => typeof t.kindsOfState === 'function';
@@ -106,6 +107,9 @@ const keyValue = (cell, w) => {
106
107
  return value;
107
108
  };
108
109
  export function inferLaws(target, laws, stepLaws = [], opts = {}) {
110
+ return quietly(() => withDeadline(performance.now() + (opts.ms ?? 20000), () => inferLawsSearch(target, laws, stepLaws, opts)));
111
+ }
112
+ function inferLawsSearch(target, laws, stepLaws = [], opts = {}) {
109
113
  const t0 = performance.now(), until = t0 + (opts.ms ?? 20000), rule = isRule(target) ? target : undefined;
110
114
  const m = rule ? rule.machine : target;
111
115
  let proofs = 0;
@@ -0,0 +1,45 @@
1
+ import type { C, E } from '../core/machine.js';
2
+ import { type Goal, type Tree } from './kernel.js';
3
+ export type Fact = {
4
+ name: string;
5
+ keys: string[];
6
+ holds: C;
7
+ };
8
+ export type Use = {
9
+ node: number;
10
+ subst: Record<string, E>;
11
+ guard: C;
12
+ premises: Tree[];
13
+ descent: Tree;
14
+ };
15
+ export type CNode = {
16
+ name: string;
17
+ hyps: C[];
18
+ concl: C;
19
+ rank: E[];
20
+ insts: {
21
+ fact: number;
22
+ at: E[];
23
+ }[];
24
+ uses: Use[];
25
+ main: Tree;
26
+ };
27
+ export type Cyclic = {
28
+ facts: Fact[];
29
+ nodes: CNode[];
30
+ };
31
+ export declare const falseC: C;
32
+ export declare const lexLt: (as: E[], bs: E[]) => C;
33
+ export declare const instHyps: (facts: Fact[], insts: CNode["insts"]) => C[];
34
+ export declare const baseHyps: (facts: Fact[], n: Pick<CNode, "hyps" | "insts">) => C[];
35
+ export declare const useHyps: (nodes: Pick<CNode, "concl">[], uses: Pick<Use, "node" | "subst" | "guard">[]) => C[];
36
+ type Shape = Pick<CNode, 'name' | 'hyps' | 'concl' | 'rank' | 'insts'> & {
37
+ uses: Pick<Use, 'node' | 'subst' | 'guard'>[];
38
+ };
39
+ export declare const mainGoal: (facts: Fact[], nodes: Shape[], n: Shape) => Goal;
40
+ export declare const premiseGoal: (facts: Fact[], n: Shape, u: Shape["uses"][number], c: C, i: number) => Goal;
41
+ export declare const descentGoal: (facts: Fact[], n: Shape, u: Shape["uses"][number], m: Shape) => Goal;
42
+ export declare const factsAvoid: (facts: Fact[], subst: Record<string, E>) => boolean;
43
+ export declare function refuseCyclic(p: Cyclic): string | undefined;
44
+ export declare const checkCyclic: (p: Cyclic) => boolean;
45
+ export {};
@@ -0,0 +1,54 @@
1
+ import { check, substC, substE } from "./kernel.js";
2
+ const zero = { op: 'const', value: 0 };
3
+ export const falseC = { op: '<', a: zero, b: zero };
4
+ // as below bs in order: the first place they differ, as's is smaller (Lean's lexLtC, built the same way).
5
+ export const lexLt = (as, bs) => (as.length && bs.length
6
+ ? { op: 'or', xs: [{ op: '<', a: as[0], b: bs[0] }, { op: 'and', xs: [{ op: '=', a: as[0], b: bs[0] }, lexLt(as.slice(1), bs.slice(1))] }] }
7
+ : falseC);
8
+ const sub = (s) => ({ vars: s, maps: {}, flags: {} });
9
+ // A fact at the terms a node names: its keys paired with the terms (the shorter list decides, as Lean's zip does).
10
+ const keyed = (keys, at) => Object.fromEntries(keys.slice(0, at.length).map((key, i) => [key, at[i]]));
11
+ export const instHyps = (facts, insts) => insts.flatMap(p => { const f = facts[p.fact]; return f ? [substC(f.holds, sub(keyed(f.keys, p.at)))] : []; });
12
+ export const baseHyps = (facts, n) => [...n.hyps, ...instHyps(facts, n.insts)];
13
+ export const useHyps = (nodes, uses) => uses.flatMap(u => {
14
+ const m = nodes[u.node];
15
+ return m ? [{ op: 'or', xs: [{ op: 'not', x: u.guard }, substC(m.concl, sub(u.subst))] }] : [];
16
+ });
17
+ export const mainGoal = (facts, nodes, n) => ({ name: `${n.name}`, hyps: [...baseHyps(facts, n), ...useHyps(nodes, n.uses), { op: 'not', x: n.concl }] });
18
+ export const premiseGoal = (facts, n, u, c, i) => ({ name: `${n.name}: use of node ${u.node}, its hypothesis ${i}`, hyps: [...baseHyps(facts, n), u.guard, { op: 'not', x: substC(c, sub(u.subst)) }] });
19
+ export const descentGoal = (facts, n, u, m) => ({ name: `${n.name}: use of node ${u.node}, its rank below`, hyps: [...baseHyps(facts, n), u.guard, { op: 'not', x: lexLt(m.rank.map(e => substE(e, sub(u.subst))), n.rank) }] });
20
+ const occurs = (name, t) => JSON.stringify(t).includes(`"op":"var","name":${JSON.stringify(name)}`);
21
+ // The language Lean's twin has: no flag, no quantifier.
22
+ const plain = (t) => !/"op":"(?:flag|all)"/.test(JSON.stringify(t));
23
+ export const factsAvoid = (facts, subst) => Object.keys(subst).every(v => facts.every(f => f.keys.includes(v) || !occurs(v, f.holds)));
24
+ // Why a cyclic proof is refused, or undefined when it checks.
25
+ export function refuseCyclic(p) {
26
+ const { facts, nodes } = p;
27
+ if (!plain(facts) || !plain(nodes.map(n => [n.hyps, n.concl, n.rank, n.insts, n.uses.map(u => [u.subst, u.guard])])))
28
+ return 'a flag or a quantifier (no cyclic proof reads one)';
29
+ for (const f of facts)
30
+ if (new Set(f.keys).size !== f.keys.length)
31
+ return `fact ${f.name}: a key twice`;
32
+ for (const n of nodes) {
33
+ if (!check(mainGoal(facts, nodes, n), n.main))
34
+ return `${n.name}: its goal`;
35
+ for (const [j, u] of n.uses.entries()) {
36
+ const m = nodes[u.node];
37
+ if (!m)
38
+ return `${n.name}: use ${j} names no node`;
39
+ if (m.rank.length !== n.rank.length)
40
+ return `${n.name}: use ${j} compares ranks of different lengths`;
41
+ if (!factsAvoid(facts, u.subst))
42
+ return `${n.name}: use ${j} assigns a variable a fact reads`;
43
+ if (m.hyps.length !== u.premises.length)
44
+ return `${n.name}: use ${j} has ${u.premises.length} premise trees for ${m.hyps.length} hypotheses`;
45
+ for (const [i, c] of m.hyps.entries())
46
+ if (!check(premiseGoal(facts, n, u, c, i), u.premises[i]))
47
+ return `${n.name}: use ${j}, hypothesis ${i} of ${m.name}`;
48
+ if (!check(descentGoal(facts, n, u, m), u.descent))
49
+ return `${n.name}: use ${j} doesn't lower the rank`;
50
+ }
51
+ }
52
+ return undefined;
53
+ }
54
+ export const checkCyclic = (p) => refuseCyclic(p) === undefined;
@@ -21,6 +21,11 @@ export declare const solveKinds: Record<string, {
21
21
  ms: number;
22
22
  rows: number;
23
23
  }>;
24
+ export declare let proveDump: ((goal: Goal) => void) | undefined;
25
+ export declare const setProveDump: (f: ((goal: Goal) => void) | undefined) => void;
26
+ export declare const cellProducts: {
27
+ on: boolean;
28
+ };
24
29
  export type Outcome = {
25
30
  tree: Tree;
26
31
  nodes: number;
@@ -30,10 +35,15 @@ export type Outcome = {
30
35
  path: Path;
31
36
  why?: string;
32
37
  };
38
+ export declare function withDeadline<T>(until: number, f: () => T): T;
33
39
  export declare function grow(goal: Goal, path?: Path, budget?: {
34
40
  nodes: number;
35
41
  until?: number;
42
+ units?: boolean;
36
43
  }): Outcome;
44
+ export declare const minimal: {
45
+ on: boolean;
46
+ };
37
47
  export type Report = {
38
48
  proven: boolean;
39
49
  nodes: number;