gdp-wired 0.0.0-stage → 0.2.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.
@@ -0,0 +1,593 @@
1
+ // The kernel: the only part of a machine proof that is trusted, besides check.ts. Provers (prove.ts, or anything else)
2
+ // are untrusted; they hand the kernel a proof tree and the kernel either accepts it or doesn't.
3
+ //
4
+ // A goal is a list of hypotheses over naturals (quantifier-free: +, ∸, × by a constant, min, max, if, map reads) that
5
+ // can't all hold. A proof is a tree of splits on linear atoms (lin <= 0, or lin >= 1), with a derivation at each leaf.
6
+ // At a leaf, the kernel takes the path's atoms, every linear fact the hypotheses give under the path (a comparison whose
7
+ // ifs, mins and truncations the path decides; the parts of a conjunction; the one disjunct left when the path refutes
8
+ // the others), each variable >= 0, and, for two reads of one map at keys the path proves equal, their equality; the
9
+ // leaf's derivation must refute them (check.ts). Splits are trivially exhaustive, so a tree whose leaves all check
10
+ // means the hypotheses never hold together.
11
+ //
12
+ // Machines: `obligations` turns a machine and a set of laws into goals, the induction rule spelled out: each law holds
13
+ // at the start, and one step from a state where every law in the set holds keeps it. A law that holds at every key is
14
+ // assumed at whatever keys the proof names (instantiating ∀ is sound at any term) and is proven at a fresh key. The
15
+ // whole set is checked, so nothing is assumed that isn't proven.
16
+ //
17
+ // Two kinds of assumption: `pre`, about the params alone, is a precondition (the law is claimed only for params that
18
+ // meet it), so it holds at the start and at every step; `assume`, about each request, holds only at a step, since no
19
+ // request has come at the start.
20
+ import { checkRefutation } from "../core/check.js";
21
+ import { add, k, scale, sub, v } from "../core/solver.js";
22
+ // An exhaustive switch's default: TypeScript proves it unreachable, and if it is reached anyway (a value from outside
23
+ // the types), it fails loudly instead of returning undefined.
24
+ export function unreachable(value) { throw new Error(`unhandled case: ${JSON.stringify(value)}`); }
25
+ export const canon = (lin) => Object.keys(lin).sort().map(key => `${key}:${lin[key]}`).join(',');
26
+ export const atomOf = (lin, holds) => ({ lin: holds ? lin : add(k(1), scale(lin, -1)), label: '' });
27
+ // Is lin <= 0 decided by the path? A constant atom decides itself.
28
+ // Over the integers lin <= 0 fails exactly when 1 - lin <= 0 holds, so either atom decides the other.
29
+ function decide(lin, path) {
30
+ if (Object.keys(lin).every(key => key === '1'))
31
+ return (lin['1'] ?? 0) <= 0;
32
+ const direct = path.get(canon(lin))?.holds;
33
+ if (direct !== undefined)
34
+ return direct;
35
+ const flipped = path.get(canon(add(k(1), scale(lin, -1))))?.holds;
36
+ return flipped === undefined ? undefined : !flipped;
37
+ }
38
+ // A term's linear form under the path, or undefined if the path leaves one of its choices open.
39
+ // Reads become cells, named by map and key: `M[key]`.
40
+ export function linear(e, path) {
41
+ switch (e.op) {
42
+ case 'var': return e.name === '1' || /[*[\],:]/.test(e.name) ? undefined : v(e.name); // those characters build cells and monomials
43
+ case 'const': return k(e.value);
44
+ case 'mul': {
45
+ const a = linear(e.a, path);
46
+ return a && scale(a, e.k);
47
+ }
48
+ case 'prod': {
49
+ const a = linear(e.a, path), b = linear(e.b, path);
50
+ return a && b && times(a, b);
51
+ }
52
+ case 'add': {
53
+ const a = linear(e.a, path), b = linear(e.b, path);
54
+ return a && b && add(a, b);
55
+ }
56
+ case 'sub':
57
+ case 'min':
58
+ case 'max': {
59
+ const a = linear(e.a, path), b = linear(e.b, path);
60
+ if (!a || !b)
61
+ return undefined;
62
+ const aLeB = decide(sub(a, b), path), bLeA = decide(sub(b, a), path);
63
+ if (e.op === 'sub')
64
+ return bLeA === true ? sub(a, b) : bLeA === false ? k(0) : undefined;
65
+ if (aLeB === undefined)
66
+ return undefined;
67
+ return (e.op === 'min') === aLeB ? a : b;
68
+ }
69
+ case 'if': {
70
+ const c = truth(e.c, path);
71
+ return c === undefined ? undefined : linear(c ? e.then : e.else, path);
72
+ }
73
+ case 'get': {
74
+ const key = linear(e.key, path);
75
+ return key && v(`${e.map}[${canon(key)}]`);
76
+ }
77
+ }
78
+ }
79
+ // Products: multiply out, each monomial (variables joined by '*', sorted) one more natural variable. Forgetting how
80
+ // monomials relate (x*x to x, say) only weakens what a leaf may use, so it stays sound; distributing is what proves
81
+ // spend <= m·count over a node from the same law at its children. Map reads can't be factors.
82
+ function times(a, b) {
83
+ const out = [];
84
+ for (const [p, cp] of Object.entries(a))
85
+ for (const [q, cq] of Object.entries(b)) {
86
+ if (p.includes('[') || q.includes('['))
87
+ return undefined;
88
+ const factors = [...(p === '1' ? [] : p.split('*')), ...(q === '1' ? [] : q.split('*'))].sort();
89
+ out.push({ [factors.length ? factors.join('*') : '1']: cp * cq });
90
+ }
91
+ return add(...out);
92
+ }
93
+ // The atoms (lin <= 0 each) that make a comparison true, and those that make it false (one of which holds).
94
+ function comparison(c, path) {
95
+ const a = linear(c.a, path), b = linear(c.b, path);
96
+ if (!a || !b)
97
+ return undefined;
98
+ const ab = sub(a, b), ba = sub(b, a);
99
+ if (c.op === '<=')
100
+ return { when: [ab], unless: [[add(ba, k(1))]] };
101
+ if (c.op === '<')
102
+ return { when: [add(ab, k(1))], unless: [[ba]] };
103
+ return { when: [ab, ba], unless: [[add(ab, k(1))], [add(ba, k(1))]] };
104
+ }
105
+ // Kleene truth under the path: true, false, or undefined when open.
106
+ export function truth(c, path) {
107
+ switch (c.op) {
108
+ case 'flag': throw new Error('flags are inlined before the kernel sees them');
109
+ case 'not': {
110
+ const t = truth(c.x, path);
111
+ return t === undefined ? undefined : !t;
112
+ }
113
+ case 'and':
114
+ case 'or': {
115
+ const ts = c.xs.map(y => truth(y, path)), dominant = c.op === 'or';
116
+ return ts.includes(dominant) ? dominant : ts.every(t => t === !dominant) ? !dominant : undefined;
117
+ }
118
+ case '<':
119
+ case '<=':
120
+ case '=': {
121
+ const cmp = comparison(c, path);
122
+ if (!cmp)
123
+ return undefined;
124
+ const ts = cmp.when.map(lin => decide(lin, path));
125
+ return ts.includes(false) ? false : ts.every(t => t === true) ? true : undefined;
126
+ }
127
+ default: return unreachable(c);
128
+ }
129
+ }
130
+ // The linear facts a hypothesis gives under the path (each lin <= 0).
131
+ export function facts(c, path, positive = true) {
132
+ switch (c.op) {
133
+ case 'flag': throw new Error('flags are inlined before the kernel sees them');
134
+ case 'not': return facts(c.x, path, !positive);
135
+ case 'and':
136
+ case 'or': {
137
+ if ((c.op === 'and') === positive)
138
+ return c.xs.flatMap(y => facts(y, path, positive));
139
+ const open = c.xs.filter(y => truth(y, path) !== !positive);
140
+ return open.length === 0 ? [k(1)] : open.length === 1 ? facts(open[0], path, positive) : [];
141
+ }
142
+ case '<':
143
+ case '<=':
144
+ case '=': {
145
+ const cmp = comparison(c, path);
146
+ if (!cmp)
147
+ return [];
148
+ if (positive)
149
+ return cmp.when;
150
+ const open = cmp.unless.filter(atoms => !atoms.some(lin => decide(lin, path) === false));
151
+ return open.length === 0 ? [k(1)] : open.length === 1 ? open[0] : [];
152
+ }
153
+ default: return unreachable(c);
154
+ }
155
+ }
156
+ // What a leaf's derivation must refute.
157
+ export function leafConstraints(goal, path) {
158
+ const rows = [...path.values()].map(a => atomOf(a.lin, a.holds));
159
+ for (const h of goal.hyps)
160
+ for (const lin of facts(h, path))
161
+ rows.push({ lin, label: '' });
162
+ const names = new Set(rows.flatMap(r => Object.keys(r.lin).filter(key => key !== '1')));
163
+ // Two reads of one map whose keys the path proves equal are equal.
164
+ const cells = [...names].filter(name => name.includes('[')).map(name => ({ name, map: name.slice(0, name.indexOf('[')), key: keyOf(name) }));
165
+ for (const [i, p] of cells.entries())
166
+ for (const q of cells.slice(i + 1)) {
167
+ if (p.map !== q.map)
168
+ continue;
169
+ const d = sub(p.key, q.key);
170
+ if (decide(d, path) === true && decide(scale(d, -1), path) === true)
171
+ rows.push({ lin: sub(v(p.name), v(q.name)), label: '' }, { lin: sub(v(q.name), v(p.name)), label: '' });
172
+ }
173
+ for (const name of names)
174
+ rows.push({ lin: { [name]: -1 }, label: '' });
175
+ return rows;
176
+ }
177
+ const keyOf = (cell) => {
178
+ const inner = cell.slice(cell.indexOf('[') + 1, -1);
179
+ const out = {};
180
+ // Keys are canonical texts `name:coef` joined by ',' at the outer level; a name may itself be a cell with brackets.
181
+ let depth = 0, start = 0;
182
+ const parts = [];
183
+ for (let i = 0; i <= inner.length; i++) {
184
+ const ch = inner[i];
185
+ if (ch === '[')
186
+ depth++;
187
+ else if (ch === ']')
188
+ depth--;
189
+ else if ((ch === ',' && depth === 0) || i === inner.length) {
190
+ if (i > start)
191
+ parts.push(inner.slice(start, i));
192
+ start = i + 1;
193
+ }
194
+ }
195
+ for (const part of parts) {
196
+ const at = part.lastIndexOf(':');
197
+ out[part.slice(0, at)] = Number(part.slice(at + 1));
198
+ }
199
+ return out;
200
+ };
201
+ // A leaf may also use the product of two of its rows: p <= 0 and q <= 0 give p·q >= 0, as nlinarith does. A pair may
202
+ // name an earlier product row too (index past the leaf's rows), which is a row of the same kind, so degrees go higher.
203
+ export function withProducts(rows, pairs) {
204
+ const out = [...rows];
205
+ for (const [i, j] of pairs) {
206
+ const p = out[i], q = out[j], pq = p && q ? times(p.lin, q.lin) : undefined;
207
+ out.push(pq ? { lin: scale(pq, -1), label: '' } : { lin: {}, label: '' });
208
+ }
209
+ return out;
210
+ }
211
+ export function check(goal, tree, path = new Map()) {
212
+ if ('leaf' in tree)
213
+ return checkRefutation(withProducts(leafConstraints(goal, path), tree.products ?? []), tree.leaf);
214
+ const id = canon(tree.split);
215
+ if (path.has(id) || Object.keys(tree.split).every(key => key === '1'))
216
+ return false;
217
+ const yes = new Map(path).set(id, { lin: tree.split, holds: true }), no = new Map(path).set(id, { lin: tree.split, holds: false });
218
+ return check(goal, tree.yes, yes) && check(goal, tree.no, no);
219
+ }
220
+ // --- machines ----------------------------------------------------------------------------------------------------------
221
+ // Every constant and factor is a natural, so every value a machine computes is one (∸ truncates).
222
+ function natural(e) {
223
+ switch (e.op) {
224
+ case 'var': return true;
225
+ case 'const': return Number.isInteger(e.value) && e.value >= 0;
226
+ case 'mul': return Number.isInteger(e.k) && e.k >= 0 && natural(e.a);
227
+ case 'prod': return natural(e.a) && natural(e.b);
228
+ case 'if': return naturalC(e.c) && natural(e.then) && natural(e.else);
229
+ case 'get': return natural(e.key);
230
+ case 'add':
231
+ case 'sub':
232
+ case 'min':
233
+ case 'max': return natural(e.a) && natural(e.b);
234
+ default: return unreachable(e);
235
+ }
236
+ }
237
+ const naturalC = (c) => c.op === 'flag' ? true : c.op === 'not' ? naturalC(c.x) : c.op === 'and' || c.op === 'or' ? c.xs.every(naturalC) : natural(c.a) && natural(c.b);
238
+ function substE(e, s) {
239
+ switch (e.op) {
240
+ case 'var': return s.vars[e.name] ?? e;
241
+ case 'const': return e;
242
+ case 'mul': return { ...e, a: substE(e.a, s) };
243
+ case 'prod': return { ...e, a: substE(e.a, s), b: substE(e.b, s) };
244
+ case 'if': return { op: 'if', c: substC(e.c, s), then: substE(e.then, s), else: substE(e.else, s) };
245
+ case 'get': {
246
+ const key = substE(e.key, s);
247
+ return s.maps[e.map] ? s.maps[e.map](key) : { op: 'get', map: e.map, key };
248
+ }
249
+ case 'add':
250
+ case 'sub':
251
+ case 'min':
252
+ case 'max': return { ...e, a: substE(e.a, s), b: substE(e.b, s) };
253
+ default: return unreachable(e);
254
+ }
255
+ }
256
+ export function substC(c, s) {
257
+ if (c.op === 'flag') {
258
+ const f = s.flags[c.name];
259
+ if (!f)
260
+ throw new Error(`unknown flag ${c.name}`);
261
+ return f;
262
+ }
263
+ if (c.op === 'not')
264
+ return { op: 'not', x: substC(c.x, s) };
265
+ if (c.op === 'and' || c.op === 'or')
266
+ return { op: c.op, xs: c.xs.map(y => substC(y, s)) };
267
+ return { ...c, a: substE(c.a, s), b: substE(c.b, s) };
268
+ }
269
+ const mentions = (c, names) => JSON.stringify(c).match(/"name":"([^"]+)"/g)?.some(m => names.includes(m.slice(8, -1))) ?? false;
270
+ // A precondition reads the params and nothing else: no state, no request, no map.
271
+ const aboutParams = (c, params) => !JSON.stringify(c).includes('"op":"get"') && (JSON.stringify(c).match(/"name":"([^"]+)"/g) ?? []).every(m => params.includes(m.slice(8, -1)));
272
+ const preOf = (o) => {
273
+ if (o.pre && !(aboutParams(o.pre, o.params) && naturalC(o.pre)))
274
+ throw new Error('a precondition reads only the params');
275
+ return o.pre ? [o.pre] : [];
276
+ };
277
+ const identity = { vars: {}, maps: {}, flags: {} };
278
+ const from = (at = identity) => ({ hyps: [], at });
279
+ // Conditions that hold where the run is.
280
+ const holds = (r, cs) => ({ hyps: [...r.hyps, ...cs.map(c => substC(c, r.at))], at: r.at });
281
+ // Names read where the run is: inputs named for this step (a request's terms, or fresh names for a later step).
282
+ const reading = (r, vars) => ({ hyps: r.hyps, at: { ...r.at, vars: { ...r.at.vars, ...vars } } });
283
+ // One step: each name the update sets is its term read where the run is; a map read after it is the written value at
284
+ // the written key, and what the map held before elsewhere.
285
+ const step = (r, u) => {
286
+ const at = r.at, before = (map, key) => (at.maps[map] ? at.maps[map](key) : { op: 'get', map, key });
287
+ return { hyps: r.hyps, at: {
288
+ vars: { ...at.vars, ...Object.fromEntries(Object.entries(u.vars).map(([n, e]) => [n, substE(e, at)])) },
289
+ maps: { ...at.maps, ...Object.fromEntries(Object.entries(u.maps).map(([map, w]) => [map, (key) => ({ op: 'if', c: { op: '=', a: key, b: substE(w.key, at) }, then: substE(w.value, at), else: before(map, key) })])) },
290
+ flags: at.flags,
291
+ } };
292
+ };
293
+ // The conclusion read at the run's end: plain names at the last frame.
294
+ const atEnd = (r, c) => substC(c, r.at);
295
+ // The conclusion read across the run: plain names at the first frame, primed names (s', and M'[k] as a read of the map
296
+ // named M') at the last; the first step's inputs as it read them, and its flags, decided where it was taken.
297
+ const across = (r, c, state, maps, first) => substC(c, {
298
+ vars: { ...first.inputs, ...Object.fromEntries(state.map(n => [`${n}'`, r.at.vars[n] ?? { op: 'var', name: n }])) },
299
+ maps: Object.fromEntries(maps.map(map => [`${map}'`, (key) => (r.at.maps[map] ? r.at.maps[map](key) : { op: 'get', map, key })])),
300
+ flags: first.flags,
301
+ });
302
+ const goal = (name, r, conclusion) => ({ name, hyps: [...r.hyps, { op: 'not', x: conclusion }] });
303
+ // A machine's step as an update, its flags inlined (they are decided before the step, over the frame it starts from).
304
+ const updateOf = (m) => {
305
+ const flags = { vars: {}, maps: {}, flags: m.step.flags ?? {} };
306
+ return {
307
+ vars: Object.fromEntries(Object.entries(m.step.state ?? {}).map(([n, e]) => [n, substE(e, flags)])),
308
+ maps: Object.fromEntries(Object.entries(m.step.maps ?? {}).map(([map, w]) => [map, { key: substE(w.key, flags), value: substE(w.value, flags) }])),
309
+ };
310
+ };
311
+ const inlineFlags = (m, c) => substC(c, { vars: {}, maps: {}, flags: m.step.flags ?? {} });
312
+ // The laws assumed where the run is: each plain law, and each every-key law at the keys the proof names.
313
+ const assumed = (laws, instances) => laws.flatMap(l => (l.forall ? (instances[l.name] ?? []).map(key => substC(l.holds, { vars: { [l.forall]: key }, maps: {}, flags: {} })) : [l.holds]));
314
+ // The start: the state's start values over the params, every map cell its constant.
315
+ const startOf = (m) => ({ vars: { ...m.state }, maps: Object.fromEntries(Object.entries(m.maps ?? {}).map(([map, c]) => [map, () => ({ op: 'const', value: c })])), flags: {} });
316
+ function reads(t, out = { vars: new Set(), maps: new Set(), flags: new Set() }) {
317
+ switch (t.op) {
318
+ case 'var':
319
+ out.vars.add(t.name);
320
+ break;
321
+ case 'const': break;
322
+ case 'get':
323
+ out.maps.add(t.map);
324
+ reads(t.key, out);
325
+ break;
326
+ case 'flag':
327
+ out.flags.add(t.name);
328
+ break;
329
+ case 'not':
330
+ reads(t.x, out);
331
+ break;
332
+ case 'and':
333
+ case 'or':
334
+ for (const y of t.xs)
335
+ reads(y, out);
336
+ break;
337
+ case 'mul':
338
+ reads(t.a, out);
339
+ break;
340
+ case 'if':
341
+ reads(t.c, out);
342
+ reads(t.then, out);
343
+ reads(t.else, out);
344
+ break;
345
+ case 'add':
346
+ case 'sub':
347
+ case 'min':
348
+ case 'max':
349
+ case 'prod':
350
+ case '<':
351
+ case '<=':
352
+ case '=':
353
+ reads(t.a, out);
354
+ reads(t.b, out);
355
+ break;
356
+ default: unreachable(t);
357
+ }
358
+ return out;
359
+ }
360
+ function inScope(what, ts, vars, maps = [], flags = []) {
361
+ for (const t of ts) {
362
+ const r = reads(t);
363
+ for (const name of r.vars)
364
+ if (!vars.includes(name))
365
+ throw new Error(`${what} reads ${name}, which it doesn't declare`);
366
+ for (const map of r.maps)
367
+ if (!maps.includes(map))
368
+ throw new Error(`${what} reads the map ${map}, which it doesn't declare`);
369
+ for (const flag of r.flags)
370
+ if (!flags.includes(flag))
371
+ throw new Error(`${what} reads the flag ${flag}, which it doesn't declare`);
372
+ }
373
+ }
374
+ // A name the kernel's linear forms would read as something else: '1' is the constant, and '*', '[', ']', ',' and ':'
375
+ // build monomials and cells. Such a name could never be proven about; refused instead, with its name.
376
+ const plainName = (what, name) => { if (name === '1' || /[*[\],:]/.test(name))
377
+ throw new Error(`${what}: the name ${name} is reserved (1, or * [ ] , :)`); };
378
+ const distinct = (what, names) => { const seen = new Set(); for (const name of names) {
379
+ if (seen.has(name))
380
+ throw new Error(`${what} declares ${name} twice`);
381
+ seen.add(name);
382
+ } };
383
+ // A machine (without its history: historyMachine checks that and scopes the plain machine it makes) and its laws. The
384
+ // step reads params, inputs and the state before it; a flag reads the flags declared before it; a start value reads the
385
+ // params; a law reads the params, the state, the maps and its own key.
386
+ export function scope(m, laws = []) {
387
+ const state = Object.keys(m.state), maps = Object.keys(m.maps ?? {}), flags = Object.keys(m.step.flags ?? {});
388
+ const names = [...m.params, ...m.inputs, ...state];
389
+ distinct(m.name, [...names, ...maps]);
390
+ for (const name of [...names, ...maps, ...flags])
391
+ plainName(m.name, name);
392
+ for (const name of Object.keys(m.step.state ?? {}))
393
+ if (!state.includes(name))
394
+ throw new Error(`${m.name}'s step sets ${name}, which isn't in its state`);
395
+ for (const map of Object.keys(m.step.maps ?? {}))
396
+ if (!maps.includes(map))
397
+ throw new Error(`${m.name}'s step writes the map ${map}, which it doesn't declare`);
398
+ inScope(`${m.name}'s start`, Object.values(m.state), m.params);
399
+ Object.values(m.step.flags ?? {}).forEach((c, i) => inScope(`${m.name}'s flag ${flags[i]}`, [c], names, maps, flags.slice(0, i)));
400
+ inScope(`${m.name}'s step`, [...Object.values(m.step.state ?? {}), ...Object.values(m.step.maps ?? {}).flatMap(w => [w.key, w.value])], names, maps, flags);
401
+ if (m.assume)
402
+ inScope(`${m.name}'s assumption`, [m.assume], names, maps, flags);
403
+ if (m.pre)
404
+ inScope(`${m.name}'s precondition`, [m.pre], m.params);
405
+ distinct(`${m.name}'s laws`, laws.map(l => l.name));
406
+ for (const law of laws) {
407
+ if (law.forall && [...names, ...maps].includes(law.forall))
408
+ throw new Error(`law ${law.name}'s key ${law.forall} is already a name of ${m.name}`);
409
+ inScope(`law ${law.name}`, [law.holds], [...m.params, ...state, ...(law.forall ? [law.forall] : [])], maps);
410
+ }
411
+ }
412
+ // --- histories ---------------------------------------------------------------------------------------------------------
413
+ // A term or condition that reads only these names, and no map.
414
+ const readsOnly = (t, names) => !JSON.stringify(t).includes('"op":"get"') && !JSON.stringify(t).includes('"op":"flag"') && (JSON.stringify(t).match(/"name":"([^"]+)"/g) ?? []).every(m => names.includes(m.slice(8, -1)));
415
+ const homogeneous = (e, names) => e.op === 'var' ? names.includes(e.name) : e.op === 'add' ? homogeneous(e.a, names) && homogeneous(e.b, names) : e.op === 'mul' ? Number.isInteger(e.k) && e.k >= 0 && homogeneous(e.a, names) : false;
416
+ // A machine with a history as a plain one: each sum a state variable from 0, plus its term at the request whenever the
417
+ // step keeps the request; each query an input; each relation part of the step's assumption. Sound because, at every
418
+ // step, a sum is the sum of its term over the history so far and a query the sum of its term over the same records:
419
+ // a relation that holds record by record (the goals returned, one per relation, over a record's fields as free
420
+ // naturals) holds of the sums, since a and b are linear without a constant and `when` doesn't read the record.
421
+ export function historyMachine(m) {
422
+ const h = m.history;
423
+ if (!h)
424
+ return { machine: m, goals: [] };
425
+ const queries = Object.keys(h.queries), sums = Object.keys(h.sums), plainInputs = m.inputs.filter(i => !queries.includes(i));
426
+ const fields = h.record.map(f => `r.${f}`);
427
+ const taken = [...m.params, ...m.inputs, ...Object.keys(m.state), ...Object.keys(m.step.state ?? {})];
428
+ // r.<field> names a record's field and nothing else: a param, input or state variable of that name would be read as
429
+ // the field in the per-record goal and as itself in `pre` and `when`.
430
+ const ok = [...taken, ...sums].every(name => !name.startsWith('r.'))
431
+ && queries.every(q => m.inputs.includes(q)) && h.record.every(f => plainInputs.includes(f)) && sums.every(s => !taken.includes(s))
432
+ && Object.values(h.sums).every(g => natural(g) && readsOnly(g, [...fields, ...m.params]))
433
+ && Object.values(h.queries).every(g => natural(g) && readsOnly(g, [...fields, ...m.params, ...plainInputs])) && naturalC(h.keep)
434
+ && (h.relations ?? []).every(r => naturalC(r.when) && readsOnly(r.when, [...m.params, ...plainInputs]) && r.holds.op === '<=' && homogeneous(r.holds.a, [...sums, ...queries]) && homogeneous(r.holds.b, [...sums, ...queries]));
435
+ if (!ok)
436
+ throw new Error('a history: sums read the record and params, queries also the request\'s other inputs, relations are homogeneous a <= b under a condition on the params and the request');
437
+ inScope(`${m.name}'s keep`, [h.keep], [...m.params, ...m.inputs, ...Object.keys(m.state), ...sums], Object.keys(m.maps ?? {}), Object.keys(m.step.flags ?? {}));
438
+ if (h.prune && !(h.record.includes(h.prune.order) && natural(h.prune.after) && readsOnly(h.prune.after, m.params)))
439
+ throw new Error('a history is pruned by a recorded input and a term over the params');
440
+ const atRequest = { vars: Object.fromEntries(h.record.map(f => [`r.${f}`, { op: 'var', name: f }])), maps: {}, flags: {} };
441
+ const relations = (h.relations ?? []).map(r => ({ op: 'or', xs: [{ op: 'not', x: r.when }, r.holds] }));
442
+ const machine = {
443
+ ...m, history: undefined,
444
+ state: { ...m.state, ...Object.fromEntries(sums.map(s => [s, { op: 'const', value: 0 }])) },
445
+ step: { ...m.step, state: { ...m.step.state, ...Object.fromEntries(sums.map(s => [s, { op: 'add', a: { op: 'var', name: s }, b: { op: 'if', c: h.keep, then: substE(h.sums[s], atRequest), else: { op: 'const', value: 0 } } }])) } },
446
+ ...(m.assume || relations.length ? { assume: { op: 'and', xs: [...(m.assume ? [m.assume] : []), ...relations] } } : {}),
447
+ };
448
+ const perRecord = { vars: { ...h.sums, ...h.queries }, maps: {}, flags: {} };
449
+ const goals = (h.relations ?? []).map((r, i) => ({ name: `relation ${i + 1} @record`, hyps: [...preOf(m), r.when, { op: 'not', x: substC(r.holds, perRecord) }] }));
450
+ // Pruning: a record whose order plus `after` is at most the request's adds nothing to any query. With requests in
451
+ // nondecreasing order (the runtimes refuse one that goes back), such a record adds nothing to any later query either,
452
+ // so dropping it changes no query: these goals are about the runtimes, the laws are proven over the whole history.
453
+ if (h.prune) {
454
+ const { order, after } = h.prune;
455
+ const gone = { op: '<=', a: { op: 'add', a: { op: 'var', name: `r.${order}` }, b: after }, b: { op: 'var', name: order } };
456
+ for (const [q, g] of Object.entries(h.queries))
457
+ goals.push({ name: `prune @query ${q}`, hyps: [...preOf(m), gone, { op: 'not', x: { op: '<=', a: g, b: { op: 'const', value: 0 } } }] });
458
+ }
459
+ return { machine, goals };
460
+ }
461
+ export function obligations(m, laws, instances = {}) {
462
+ if (m.history) {
463
+ const { machine, goals } = historyMachine(m);
464
+ return [...obligations(machine, laws, instances), ...goals];
465
+ }
466
+ scope(m, laws);
467
+ const ok = [...Object.values(m.state), ...Object.values(m.step.state ?? {}), ...Object.values(m.step.maps ?? {}).flatMap(w => [w.key, w.value])].every(natural)
468
+ && Object.values(m.step.flags ?? {}).every(naturalC) && laws.every(l => naturalC(l.holds)) && Object.values(m.maps ?? {}).every(c => Number.isInteger(c) && c >= 0);
469
+ if (!ok || laws.some(l => mentions(l.holds, m.inputs)))
470
+ throw new Error('a negative constant or factor, or a law about a request, is outside the kernel');
471
+ // A law that holds at every key is proven at a fresh one.
472
+ const proven = (law) => (law.forall ? substC(law.holds, { vars: { [law.forall]: { op: 'var', name: `${law.forall}#` } }, maps: {}, flags: {} }) : law.holds);
473
+ const pre = preOf(m), u = updateOf(m), assume = m.assume ? [inlineFlags(m, m.assume)] : [];
474
+ return laws.flatMap(law => {
475
+ // At the start: the precondition, and no request. One step: from a state where the laws hold, a request allowed.
476
+ const start = holds(from(startOf(m)), pre);
477
+ const turn = step(holds(from(), [...pre, ...assume, ...assumed(laws, instances[law.name] ?? {})]), u);
478
+ return [goal(`${law.name} @start`, start, atEnd(start, proven(law))), goal(`${law.name} @step`, turn, atEnd(turn, proven(law)))];
479
+ });
480
+ }
481
+ // The whole set at once: every goal has a tree, and every tree checks.
482
+ export function checkLaws(m, laws, proof) {
483
+ return obligations(m, laws, proof.instances).every(g => proof.trees[g.name] !== undefined && check(g, proof.trees[g.name]));
484
+ }
485
+ // A rank may be a tuple, compared in order (lexicographically): the first part drops, or it stays and the rest drops.
486
+ // Tuples of naturals can't drop forever either; nested loops need them (an outer count, then an inner one).
487
+ // A rank reads the state and params only: one that reads the turn's input drops against this turn's input, but the next
488
+ // turn measures it with a fresh one, so nothing need decrease (while x < w, x := x + 1, rank w ∸ x, w = x + 1 each turn).
489
+ export function loopObligations(l, laws, rank) {
490
+ const ranks = Array.isArray(rank) ? rank : [rank];
491
+ const parts = [...Object.values(l.state), ...Object.values(l.body), ...ranks];
492
+ const names = [...l.params, ...l.inputs, ...Object.keys(l.state)];
493
+ distinct(l.name, names);
494
+ for (const name of Object.keys(l.body))
495
+ if (!(name in l.state))
496
+ throw new Error(`${l.name}'s body sets ${name}, which isn't in its state`);
497
+ inScope(`${l.name}'s start`, Object.values(l.state), l.params);
498
+ inScope(l.name, [...Object.values(l.body), l.while, ...(l.assume ? [l.assume] : [])], names);
499
+ inScope(`${l.name}'s precondition`, l.pre ? [l.pre] : [], l.params);
500
+ inScope(`${l.name}'s rank and laws`, [...ranks, ...laws.map(law => law.holds)], [...l.params, ...Object.keys(l.state)]);
501
+ if (ranks.some(r => mentions({ op: '=', a: r, b: r }, l.inputs)))
502
+ throw new Error('a rank reads the state and params, not the turn\'s input');
503
+ if (!parts.every(natural) || !naturalC(l.while) || !laws.every(law => !law.forall && naturalC(law.holds) && !mentions(law.holds, l.inputs)))
504
+ throw new Error('outside the kernel');
505
+ const pre = preOf(l);
506
+ const start = holds(from({ vars: { ...l.state }, maps: {}, flags: {} }), pre);
507
+ const turn = step(holds(from(), [...pre, ...(l.assume ? [l.assume] : []), l.while, ...laws.map(law => law.holds)]), { vars: l.body, maps: {} });
508
+ return [
509
+ ...laws.flatMap(law => [goal(`${law.name} @start`, start, atEnd(start, law.holds)), goal(`${law.name} @turn`, turn, atEnd(turn, law.holds))]),
510
+ goal('rank drops', turn, drops(ranks, turn.at)),
511
+ ];
512
+ }
513
+ const drops = (ranks, turn) => {
514
+ const [r, ...rest] = ranks;
515
+ const after = substE(r, turn), less = { op: '<', a: after, b: r };
516
+ return rest.length === 0 ? less : { op: 'or', xs: [less, { op: 'and', xs: [{ op: '=', a: after, b: r }, drops(rest, turn)] }] };
517
+ };
518
+ // --- progress: laws with "there is a request" ----------------------------------------------------------------------
519
+ // "In every reachable state where `when` holds, some request makes `then` true of the step": proven by naming the
520
+ // request (a term per input over the state and params) and proving it allowed (the machine's assumption holds of it)
521
+ // and that the step it causes makes `then` hold. `then` reads the state before the step by plain names and after it by
522
+ // primed names (s', and M'[k] as a read of the map named M'). The laws assumed must be proven inductive on their own.
523
+ export function progressObligations(m, laws, instances, p) {
524
+ if (!m.inputs.every(i => i in p.request) || !Object.values(p.request).every(natural) || mentions(p.when, m.inputs))
525
+ throw new Error('outside the kernel');
526
+ for (const e of Object.values(p.request))
527
+ if (mentions({ op: '=', a: e, b: e }, m.inputs))
528
+ throw new Error('a request is named from the state and params');
529
+ const req = { vars: p.request, maps: {}, flags: {} };
530
+ const flags = Object.fromEntries(Object.entries(m.step.flags ?? {}).map(([f, c]) => [f, substC(c, req)]));
531
+ // The run: a state where the laws and `when` hold, then the step the named request causes.
532
+ const before = holds(from(), [...preOf(m), ...assumed(laws, instances), p.when]);
533
+ const caused = step(reading(before, p.request), updateOf(m));
534
+ return [
535
+ ...(m.assume ? [goal(`${p.name}: the request is allowed`, before, substC(m.assume, req))] : []),
536
+ goal(`${p.name}: the step it causes`, caused, across(caused, p.then, Object.keys(m.state), Object.keys(m.maps ?? {}), { inputs: p.request, flags })),
537
+ ];
538
+ }
539
+ export function stepLawObligations(machine, laws, instances, sl) {
540
+ const m = machine.history ? historyMachine(machine).machine : machine;
541
+ scope(m, laws);
542
+ const k = sl.steps ?? 1;
543
+ if (!Number.isInteger(k) || k < 1 || k > 16)
544
+ throw new Error(`step law ${sl.name}: steps is a whole number from 1 to 16`);
545
+ const whens = sl.when === undefined ? [] : Array.isArray(sl.when) ? sl.when : [sl.when];
546
+ if (whens.length > k)
547
+ throw new Error(`step law ${sl.name}: one when per step at most`);
548
+ const state = Object.keys(m.state), maps = Object.keys(m.maps ?? {}), flagNames = Object.keys(m.step.flags ?? {});
549
+ const primes = [...state, ...maps].map(n => `${n}'`);
550
+ const later = Array.from({ length: k - 1 }, (_, i) => m.inputs.map(input => `${input}@${i + 2}`)).flat();
551
+ const taken = [...m.params, ...m.inputs, ...state, ...maps];
552
+ for (const p of [...primes, ...later])
553
+ if (taken.includes(p))
554
+ throw new Error(`${m.name}: ${p} is a name of the machine and a name a step law reads`);
555
+ if (sl.forall && [...taken, ...primes, ...later].includes(sl.forall))
556
+ throw new Error(`step law ${sl.name}'s key ${sl.forall} is already a name of ${m.name}`);
557
+ inScope(`step law ${sl.name}`, [sl.then], [...taken, ...state.map(n => `${n}'`), ...later, ...(sl.forall ? [sl.forall] : [])], [...maps, ...maps.map(n => `${n}'`)], flagNames);
558
+ for (const w of whens)
559
+ inScope(`step law ${sl.name}'s when`, [w], taken, maps, flagNames);
560
+ if (!naturalC(sl.then) || !whens.every(naturalC))
561
+ throw new Error(`step law ${sl.name}: a negative constant or factor is outside the kernel`);
562
+ const u = updateOf(m), assume = m.assume ? [inlineFlags(m, m.assume)] : [];
563
+ let run = holds(from(), preOf(m));
564
+ for (let j = 1; j <= k; j++) {
565
+ if (j > 1)
566
+ run = reading(run, Object.fromEntries(m.inputs.map(input => [input, { op: 'var', name: `${input}@${j}` }])));
567
+ run = step(holds(run, [...assumed(laws, instances), ...assume, ...(whens[j - 1] ? [inlineFlags(m, whens[j - 1])] : [])]), u);
568
+ }
569
+ return [goal(k === 1 ? `${sl.name} @step law` : `${sl.name} @${k} steps`, run, across(run, sl.then, state, maps, { inputs: {}, flags: m.step.flags ?? {} }))];
570
+ }
571
+ // The laws' proof and every step law's tree: all must check.
572
+ export function checkStepLaws(m, laws, stepLaws, proof) {
573
+ return checkLaws(m, laws, proof.laws) && stepLaws.every(sl => stepLawObligations(m, laws, proof.instances, sl).every(g => proof.trees[g.name] !== undefined && check(g, proof.trees[g.name])));
574
+ }
575
+ export function foldObligations(f, laws) {
576
+ if (![...Object.values(f.leaf), ...Object.values(f.node)].every(natural) || !laws.every(law => !law.forall && naturalC(law.holds) && !mentions(law.holds, f.inputs)))
577
+ throw new Error('outside the kernel');
578
+ const names = Object.keys(f.leaf);
579
+ distinct(f.name, [...f.params, ...f.inputs, ...names]);
580
+ if (Object.keys(f.node).sort().join() !== [...names].sort().join())
581
+ throw new Error(`${f.name}'s leaf and node set the same names`);
582
+ inScope(`${f.name}'s leaf`, [...Object.values(f.leaf), ...(f.assume ? [f.assume] : [])], [...f.params, ...f.inputs]);
583
+ inScope(`${f.name}'s node`, Object.values(f.node), [...f.params, ...f.inputs, ...names.flatMap(n => [`l_${n}`, `r_${n}`])]);
584
+ inScope(`${f.name}'s precondition`, f.pre ? [f.pre] : [], f.params);
585
+ inScope(`${f.name}'s laws`, laws.map(law => law.holds), [...f.params, ...names]);
586
+ const side = (p) => ({ vars: Object.fromEntries(names.map(n => [n, { op: 'var', name: `${p}${n}` }])), maps: {}, flags: {} });
587
+ // A fold's inputs are each leaf's and node's own, so its assumption holds at every leaf and node it reads them at.
588
+ // A leaf is a start from its inputs; a node, a step from the frame of its two children, the laws holding at each.
589
+ const assume = [...preOf(f), ...(f.assume ? [f.assume] : [])];
590
+ const leaf = holds(from({ vars: { ...f.leaf }, maps: {}, flags: {} }), assume);
591
+ const node = step(holds(from(), [...assume, ...laws.flatMap(h => [substC(h.holds, side('l_')), substC(h.holds, side('r_'))])]), { vars: f.node, maps: {} });
592
+ return laws.flatMap(law => [goal(`${law.name} @leaf`, leaf, atEnd(leaf, law.holds)), goal(`${law.name} @node`, node, atEnd(node, law.holds))]);
593
+ }
@@ -0,0 +1,43 @@
1
+ import type { Law, Machine } from '../core/machine.js';
2
+ import { type Goal, type Instances, type Path, type StepLaw, type Tree } from './kernel.js';
3
+ export type Outcome = {
4
+ tree: Tree;
5
+ nodes: number;
6
+ } | {
7
+ stuck: 'sat' | 'unknown';
8
+ witness?: Record<string, number>;
9
+ path: Path;
10
+ };
11
+ export declare function grow(goal: Goal, path?: Path, budget?: {
12
+ nodes: number;
13
+ until?: number;
14
+ }): Outcome;
15
+ export type Report = {
16
+ proven: boolean;
17
+ nodes: number;
18
+ leaves: number;
19
+ failed?: {
20
+ goal: string;
21
+ stuck: string;
22
+ witness?: Record<string, number>;
23
+ };
24
+ checked: boolean;
25
+ };
26
+ export declare function proveLaws(m: Machine, laws: Law[]): Report & {
27
+ proof?: {
28
+ instances: Record<string, Instances>;
29
+ trees: Record<string, Tree>;
30
+ };
31
+ };
32
+ export declare function proveStepLaws(m: Machine, laws: Law[], stepLaws: StepLaw[]): Report & {
33
+ proof?: {
34
+ laws: {
35
+ instances: Record<string, Instances>;
36
+ trees: Record<string, Tree>;
37
+ };
38
+ instances: Instances;
39
+ trees: Record<string, Tree>;
40
+ };
41
+ };
42
+ export declare const countLeaves: (t: Tree) => number;
43
+ export declare function concretize(goal: Goal, bound?: number): Record<string, number> | undefined;