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.
- package/LICENSE +21 -0
- package/README.md +237 -2
- package/dist/core/check.d.ts +3 -0
- package/dist/core/check.js +31 -0
- package/dist/core/compose.d.ts +24 -0
- package/dist/core/compose.js +121 -0
- package/dist/core/dsl.d.ts +268 -0
- package/dist/core/dsl.js +526 -0
- package/dist/core/machine.d.ts +145 -0
- package/dist/core/machine.js +516 -0
- package/dist/core/solver.d.ts +41 -0
- package/dist/core/solver.js +182 -0
- package/dist/kernel/kernel.d.ts +88 -0
- package/dist/kernel/kernel.js +593 -0
- package/dist/kernel/prove.d.ts +43 -0
- package/dist/kernel/prove.js +347 -0
- package/package.json +79 -4
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
// Step machines: the one source for a stateful rule (a budget, caps, a ledger). From a Machine the library derives
|
|
2
|
+
// - interpret / compileJs: the runtime, as an interpreter and as generated JavaScript
|
|
3
|
+
// - prove: each law by induction (holds at the start, one step preserves it), with every case decided by solver.ts
|
|
4
|
+
// - search: real counterexample traces from the start, by bounded enumeration of short request sequences
|
|
5
|
+
// - strengthen (invariant.ts): candidate invariants dropped until the rest are inductive (Houdini)
|
|
6
|
+
// Arithmetic is linear: +, −, × by a constant, min, max, if. Maps are naturals indexed by naturals, every cell starting
|
|
7
|
+
// at the same value; a law about a map holds for every key x, and is proven at one arbitrary x (with the law assumed at
|
|
8
|
+
// every other key the step reads).
|
|
9
|
+
import { historyMachine, scope } from "../kernel/kernel.js";
|
|
10
|
+
import { add, eq, k, le, lt, nat, scale, show, solve, sub, v } from "./solver.js";
|
|
11
|
+
export { unreachable } from "../kernel/kernel.js";
|
|
12
|
+
// Builders.
|
|
13
|
+
export const x = (name) => ({ op: 'var', name });
|
|
14
|
+
export const n = (value) => ({ op: 'const', value });
|
|
15
|
+
export const plus = (a, b) => ({ op: 'add', a, b });
|
|
16
|
+
export const minus = (a, b) => ({ op: 'sub', a, b });
|
|
17
|
+
export const times = (factor, a) => ({ op: 'mul', k: factor, a });
|
|
18
|
+
export const min = (a, b) => ({ op: 'min', a, b });
|
|
19
|
+
export const max = (a, b) => ({ op: 'max', a, b });
|
|
20
|
+
export const prod = (a, b) => ({ op: 'prod', a, b });
|
|
21
|
+
export const ite = (c, then, otherwise) => ({ op: 'if', c, then, else: otherwise });
|
|
22
|
+
export const get = (map, key) => ({ op: 'get', map, key });
|
|
23
|
+
export const less = (a, b) => ({ op: '<', a, b });
|
|
24
|
+
export const atMost = (a, b) => ({ op: '<=', a, b });
|
|
25
|
+
export const same = (a, b) => ({ op: '=', a, b });
|
|
26
|
+
export const all = (...xs) => ({ op: 'and', xs });
|
|
27
|
+
export const any = (...xs) => ({ op: 'or', xs });
|
|
28
|
+
export const not = (c) => ({ op: 'not', x: c });
|
|
29
|
+
export const bit = (name) => any(same(x(name), n(0)), same(x(name), n(1)));
|
|
30
|
+
export const naturalIn = (v, what) => {
|
|
31
|
+
if (typeof v !== 'number' || !Number.isSafeInteger(v) || v < 0)
|
|
32
|
+
throw new RangeError(`${what} must be a natural below 2^53, not ${String(v)}`);
|
|
33
|
+
return v;
|
|
34
|
+
};
|
|
35
|
+
const exact = (v) => {
|
|
36
|
+
if (v > Number.MAX_SAFE_INTEGER)
|
|
37
|
+
throw new RangeError('a value passed 2^53 - 1, past which the proofs over the naturals no longer describe the run');
|
|
38
|
+
return v;
|
|
39
|
+
};
|
|
40
|
+
export function evalE(e, env, st, m) {
|
|
41
|
+
switch (e.op) {
|
|
42
|
+
case 'var': {
|
|
43
|
+
const v = env[e.name] ?? st.scalars[e.name];
|
|
44
|
+
if (v === undefined)
|
|
45
|
+
throw new Error(`no value for ${e.name}`);
|
|
46
|
+
return v;
|
|
47
|
+
}
|
|
48
|
+
case 'const': return e.value;
|
|
49
|
+
case 'add': return exact(evalE(e.a, env, st, m) + evalE(e.b, env, st, m));
|
|
50
|
+
case 'sub': return Math.max(0, evalE(e.a, env, st, m) - evalE(e.b, env, st, m));
|
|
51
|
+
case 'mul': return exact(e.k * evalE(e.a, env, st, m));
|
|
52
|
+
case 'prod': return exact(evalE(e.a, env, st, m) * evalE(e.b, env, st, m));
|
|
53
|
+
case 'min': return Math.min(evalE(e.a, env, st, m), evalE(e.b, env, st, m));
|
|
54
|
+
case 'max': return Math.max(evalE(e.a, env, st, m), evalE(e.b, env, st, m));
|
|
55
|
+
case 'if': return evalC(e.c, env, st, m) ? evalE(e.then, env, st, m) : evalE(e.else, env, st, m);
|
|
56
|
+
case 'get': return st.maps[e.map].get(evalE(e.key, env, st, m)) ?? m.maps[e.map];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export function evalC(c, env, st, m) {
|
|
60
|
+
switch (c.op) {
|
|
61
|
+
case '<': return evalE(c.a, env, st, m) < evalE(c.b, env, st, m);
|
|
62
|
+
case '<=': return evalE(c.a, env, st, m) <= evalE(c.b, env, st, m);
|
|
63
|
+
case '=': return evalE(c.a, env, st, m) === evalE(c.b, env, st, m);
|
|
64
|
+
case 'and': return c.xs.every(y => evalC(y, env, st, m));
|
|
65
|
+
case 'or': return c.xs.some(y => evalC(y, env, st, m));
|
|
66
|
+
case 'not': return !evalC(c.x, env, st, m);
|
|
67
|
+
case 'flag': return env[`?${c.name}`] === 1;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// The machine a runtime steps (with a history, kernel.ts's plain machine), its queries, and the inputs a request carries.
|
|
71
|
+
function runnable(m) {
|
|
72
|
+
const h = m.history;
|
|
73
|
+
if (!h)
|
|
74
|
+
scope(m);
|
|
75
|
+
const plain = h ? historyMachine(m).machine : m;
|
|
76
|
+
const queries = h ? Object.keys(h.queries) : [];
|
|
77
|
+
return { h, plain, queries, carried: m.inputs.filter(i => !queries.includes(i)) };
|
|
78
|
+
}
|
|
79
|
+
const empty = { scalars: {}, maps: {} };
|
|
80
|
+
// The interpreter: persistent (a step leaves the state it was given as it was), so searches can branch from any state.
|
|
81
|
+
export function interpret(m) {
|
|
82
|
+
const { h, plain, carried } = runnable(m);
|
|
83
|
+
const args = (params, input) => {
|
|
84
|
+
const env = {};
|
|
85
|
+
for (const p of m.params)
|
|
86
|
+
env[p] = naturalIn(params[p], `param ${p}`);
|
|
87
|
+
if (input)
|
|
88
|
+
for (const i of carried)
|
|
89
|
+
env[i] = naturalIn(input[i], `input ${i}`);
|
|
90
|
+
return env;
|
|
91
|
+
};
|
|
92
|
+
// The records a request's queries read: with pruning, those not yet spent at its order.
|
|
93
|
+
const live = (env, st) => {
|
|
94
|
+
const records = st.history;
|
|
95
|
+
if (!h.prune)
|
|
96
|
+
return records;
|
|
97
|
+
const { order, after } = h.prune, by = evalE(after, env, empty, m);
|
|
98
|
+
let first = 0;
|
|
99
|
+
while (first < records.length && exact(records[first][order] + by) <= env[order])
|
|
100
|
+
first++;
|
|
101
|
+
return first ? records.slice(first) : records;
|
|
102
|
+
};
|
|
103
|
+
const queriesOf = (env, records) => {
|
|
104
|
+
const out = {};
|
|
105
|
+
for (const [q, g] of Object.entries(h.queries)) {
|
|
106
|
+
let total = 0;
|
|
107
|
+
for (const r of records) {
|
|
108
|
+
for (const f of h.record)
|
|
109
|
+
env[`r.${f}`] = r[f];
|
|
110
|
+
total = exact(total + evalE(g, env, empty, m));
|
|
111
|
+
}
|
|
112
|
+
out[q] = total;
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
};
|
|
116
|
+
const ordered = (env, st) => {
|
|
117
|
+
if (h?.prune && st.last !== undefined && env[h.prune.order] < st.last)
|
|
118
|
+
throw new RangeError(`${m.name}: requests come in nondecreasing ${h.prune.order} (${env[h.prune.order]} after ${st.last})`);
|
|
119
|
+
};
|
|
120
|
+
return {
|
|
121
|
+
init: (params) => {
|
|
122
|
+
const env = args(params);
|
|
123
|
+
return {
|
|
124
|
+
scalars: Object.fromEntries(Object.entries(plain.state).map(([name, e]) => [name, evalE(e, env, empty, plain)])),
|
|
125
|
+
maps: Object.fromEntries(Object.keys(plain.maps ?? {}).map(name => [name, new Map()])),
|
|
126
|
+
...(h ? { history: [] } : {}),
|
|
127
|
+
};
|
|
128
|
+
},
|
|
129
|
+
// The queries a request would read (none without a history).
|
|
130
|
+
queries: (params, st, input) => {
|
|
131
|
+
if (!h)
|
|
132
|
+
return {};
|
|
133
|
+
const env = args(params, input);
|
|
134
|
+
ordered(env, st);
|
|
135
|
+
return queriesOf(env, live(env, st));
|
|
136
|
+
},
|
|
137
|
+
step: (params, st, input) => {
|
|
138
|
+
const env = args(params, input);
|
|
139
|
+
let records = [];
|
|
140
|
+
if (h) {
|
|
141
|
+
ordered(env, st);
|
|
142
|
+
records = live(env, st);
|
|
143
|
+
Object.assign(env, queriesOf(env, records));
|
|
144
|
+
}
|
|
145
|
+
for (const [name, c] of Object.entries(plain.step.flags ?? {}))
|
|
146
|
+
env[`?${name}`] = evalC(c, env, st, plain) ? 1 : 0;
|
|
147
|
+
const scalars = { ...st.scalars };
|
|
148
|
+
for (const [name, e] of Object.entries(plain.step.state ?? {}))
|
|
149
|
+
scalars[name] = evalE(e, env, st, plain);
|
|
150
|
+
const writes = Object.entries(plain.step.maps ?? {}).map(([name, { key, value }]) => [name, evalE(key, env, st, plain), evalE(value, env, st, plain)]);
|
|
151
|
+
const maps = Object.fromEntries(Object.entries(st.maps).map(([name, cells]) => [name, new Map(cells)]));
|
|
152
|
+
for (const [name, key, value] of writes)
|
|
153
|
+
maps[name].set(key, value);
|
|
154
|
+
if (!h)
|
|
155
|
+
return { scalars, maps };
|
|
156
|
+
const kept = evalC(h.keep, env, st, plain);
|
|
157
|
+
return { scalars, maps, history: kept ? [...records, Object.fromEntries(h.record.map(f => [f, env[f]]))] : records, ...(h.prune ? { last: env[h.prune.order] } : {}) };
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
// The same machine as JavaScript source: what an app ships. A step reads the state it was given (every query, flag,
|
|
162
|
+
// scalar, key and value first), then writes the maps and the records in place, so it costs the same however large they
|
|
163
|
+
// grow; the state passed in is consumed. fork copies a state to branch from.
|
|
164
|
+
export function compileJs(m) {
|
|
165
|
+
const { h, plain, queries, carried } = runnable(m);
|
|
166
|
+
const id = (name) => JSON.stringify(name);
|
|
167
|
+
const flags = Object.keys(plain.step.flags ?? {});
|
|
168
|
+
const ex = (e) => {
|
|
169
|
+
switch (e.op) {
|
|
170
|
+
case 'var': return e.name.startsWith('r.') ? `r[${id(e.name.slice(2))}]` : plain.params.includes(e.name) || plain.inputs.includes(e.name) ? `env[${id(e.name)}]` : `s[${id(e.name)}]`;
|
|
171
|
+
case 'const': return String(e.value);
|
|
172
|
+
case 'add': return `$x(${ex(e.a)} + ${ex(e.b)})`;
|
|
173
|
+
case 'sub': return `Math.max(0, ${ex(e.a)} - ${ex(e.b)})`;
|
|
174
|
+
case 'mul': return `$x(${e.k} * ${ex(e.a)})`;
|
|
175
|
+
case 'prod': return `$x(${ex(e.a)} * ${ex(e.b)})`;
|
|
176
|
+
case 'min': return `Math.min(${ex(e.a)}, ${ex(e.b)})`;
|
|
177
|
+
case 'max': return `Math.max(${ex(e.a)}, ${ex(e.b)})`;
|
|
178
|
+
case 'if': return `(${cx(e.c)} ? ${ex(e.then)} : ${ex(e.else)})`;
|
|
179
|
+
case 'get': return `(maps[${id(e.map)}].get(${ex(e.key)}) ?? ${plain.maps[e.map]})`;
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
const cx = (c) => {
|
|
183
|
+
switch (c.op) {
|
|
184
|
+
case '<': return `(${ex(c.a)} < ${ex(c.b)})`;
|
|
185
|
+
case '<=': return `(${ex(c.a)} <= ${ex(c.b)})`;
|
|
186
|
+
case '=': return `(${ex(c.a)} === ${ex(c.b)})`;
|
|
187
|
+
case 'and': return `(${c.xs.map(cx).join(' && ')})`;
|
|
188
|
+
case 'or': return `(${c.xs.map(cx).join(' || ')})`;
|
|
189
|
+
case 'not': return `(!${cx(c.x)})`;
|
|
190
|
+
case 'flag': return `flag${flags.indexOf(c.name)}`;
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
const writes = Object.entries(plain.step.maps ?? {});
|
|
194
|
+
const env = (input) => `{${[...m.params.map(p => `${id(p)}: $n(params[${id(p)}], ${id(`param ${p}`)})`), ...(input ? carried.map(i => `${id(i)}: $n(input[${id(i)}], ${id(`input ${i}`)})`) : [])].join(', ')}}`;
|
|
195
|
+
const lines = [`const env = ${env(true)}, s = state.scalars, maps = state.maps;`];
|
|
196
|
+
if (h) {
|
|
197
|
+
lines.push('const records = state.history;', 'let head = state.head;');
|
|
198
|
+
if (h.prune) {
|
|
199
|
+
const order = id(h.prune.order);
|
|
200
|
+
lines.push(`const order = env[${order}];`, `if (state.last !== undefined && order < state.last) throw new RangeError(${id(`${m.name}: requests come in nondecreasing ${h.prune.order}`)} + \` (\${order} after \${state.last})\`);`, `const after = ${ex(h.prune.after)};`, `while (head < records.length && $x(records[head][${order}] + after) <= order) head++;`);
|
|
201
|
+
}
|
|
202
|
+
lines.push(`let ${queries.map((_, i) => `q${i} = 0`).join(', ')};`, `for (let i = head; i < records.length; i++) { const r = records[i]; ${queries.map((q, i) => `q${i} = $x(q${i} + ${ex(h.queries[q])});`).join(' ')} }`, ...queries.map((q, i) => `env[${id(q)}] = q${i};`));
|
|
203
|
+
}
|
|
204
|
+
lines.push(...flags.map((name, i) => `const flag${i} = ${cx(plain.step.flags[name])};`), `const scalars = {...s, ${Object.entries(plain.step.state ?? {}).map(([name, e]) => `${id(name)}: ${ex(e)}`).join(', ')}};`, ...writes.map(([, { key, value }], i) => `const key${i} = ${ex(key)}, value${i} = ${ex(value)};`));
|
|
205
|
+
if (h)
|
|
206
|
+
lines.push(`const keep = ${cx(h.keep)};`);
|
|
207
|
+
lines.push(...writes.map(([name], i) => `maps[${id(name)}].set(key${i}, value${i});`));
|
|
208
|
+
if (h)
|
|
209
|
+
lines.push(`if (keep) records.push({${h.record.map(f => `${id(f)}: env[${id(f)}]`).join(', ')}});`,
|
|
210
|
+
// Drop the spent prefix now and then, not every step: amortized, each record is moved a bounded number of times.
|
|
211
|
+
'if (head > 1024 && 2 * head > records.length) { records.splice(0, head); head = 0; }', `return {scalars, maps, history: records, head${h.prune ? ', last: order' : ''}};`);
|
|
212
|
+
else
|
|
213
|
+
lines.push('return {scalars, maps};');
|
|
214
|
+
return `// Generated from the machine ${m.name}. step consumes its state: keep the one it returns (fork to branch).
|
|
215
|
+
function $n(v, what) { if (typeof v !== 'number' || !Number.isSafeInteger(v) || v < 0) throw new RangeError(what + ' must be a natural below 2^53, not ' + v); return v; }
|
|
216
|
+
function $x(v) { if (v > 9007199254740991) throw new RangeError('a value passed 2^53 - 1, past which the proofs over the naturals no longer describe the run'); return v; }
|
|
217
|
+
export function init(params) {
|
|
218
|
+
const env = ${env(false)}, s = {};
|
|
219
|
+
return {scalars: {${Object.entries(plain.state).map(([name, e]) => `${id(name)}: ${ex(e)}`).join(', ')}}, maps: {${Object.keys(plain.maps ?? {}).map(name => `${id(name)}: new Map()`).join(', ')}}${h ? ', history: [], head: 0' : ''}};
|
|
220
|
+
}
|
|
221
|
+
export function fork(state) {
|
|
222
|
+
return {scalars: {...state.scalars}, maps: Object.fromEntries(Object.entries(state.maps).map(([name, cells]) => [name, new Map(cells)]))${h ? ', history: state.history.slice(state.head), head: 0, last: state.last' : ''}};
|
|
223
|
+
}
|
|
224
|
+
export function step(params, state, input) {
|
|
225
|
+
${lines.join('\n ')}
|
|
226
|
+
}
|
|
227
|
+
`;
|
|
228
|
+
}
|
|
229
|
+
// Generated code as a function. Every caller passes source this library generated from its own terms, never from data,
|
|
230
|
+
// which is what makes compiling it safe; the lint's implied-eval rule is waived here and nowhere else.
|
|
231
|
+
export function compileFunction(params, body) {
|
|
232
|
+
// oxlint-disable-next-line typescript/no-implied-eval
|
|
233
|
+
return new Function(...params, body);
|
|
234
|
+
}
|
|
235
|
+
export const loadJs = (m) => compileFunction([], `${compileJs(m).replace(/^export function/gm, 'function')}\nreturn {init, fork, step}`)();
|
|
236
|
+
const cross = (as, f) => as.flatMap(a => f(a.value).map(b => ({ path: [...a.path, ...b.path], value: b.value })));
|
|
237
|
+
// Every way a key can differ from each of some focus keys.
|
|
238
|
+
const apartFrom = (key, keys) => keys.reduce((acc, f) => acc.flatMap(p => [[...p, ...lt(key, f)], [...p, ...lt(f, key)]]), [[]]);
|
|
239
|
+
function symE(e, s) {
|
|
240
|
+
switch (e.op) {
|
|
241
|
+
case 'var': return [{ path: [], value: s.names[e.name] ?? v(e.name) }];
|
|
242
|
+
case 'const': return [{ path: [], value: k(e.value) }];
|
|
243
|
+
case 'add': return cross(symE(e.a, s), a => symE(e.b, s).map(b => ({ path: b.path, value: add(a, b.value) })));
|
|
244
|
+
case 'mul': return symE(e.a, s).map(a => ({ path: a.path, value: scale(a.value, e.k) }));
|
|
245
|
+
case 'prod': throw new Error('a product of two terms: kernel/prove.ts proves with products, this prover does not');
|
|
246
|
+
case 'sub': // truncated at 0, as on naturals
|
|
247
|
+
return cross(symE(e.a, s), a => symE(e.b, s).flatMap(b => [
|
|
248
|
+
{ path: [...b.path, ...le(b.value, a)], value: sub(a, b.value) },
|
|
249
|
+
{ path: [...b.path, ...lt(a, b.value)], value: k(0) },
|
|
250
|
+
]));
|
|
251
|
+
case 'min':
|
|
252
|
+
case 'max':
|
|
253
|
+
return cross(symE(e.a, s), a => symE(e.b, s).flatMap(b => {
|
|
254
|
+
const aFirst = e.op === 'min' ? le(a, b.value) : le(b.value, a);
|
|
255
|
+
const bFirst = e.op === 'min' ? lt(b.value, a) : lt(a, b.value);
|
|
256
|
+
return [{ path: [...b.path, ...aFirst], value: a }, { path: [...b.path, ...bFirst], value: b.value }];
|
|
257
|
+
}));
|
|
258
|
+
case 'if':
|
|
259
|
+
return [...cross(symC(e.c, s, true), () => symE(e.then, s)), ...cross(symC(e.c, s, false), () => symE(e.else, s))];
|
|
260
|
+
case 'get':
|
|
261
|
+
return symE(e.key, s).flatMap(key => {
|
|
262
|
+
const focus = s.reads.focus.filter(f => f.map === e.map);
|
|
263
|
+
const ids = focus.map(f => `${show(key.value)}|${show(f.key)}`);
|
|
264
|
+
if (s.decided && ids.every(id => id in s.decided)) {
|
|
265
|
+
const hit = focus.find((_, i) => s.decided[ids[i]] === 'eq');
|
|
266
|
+
if (hit)
|
|
267
|
+
return [{ path: key.path, value: v(hit.cell) }];
|
|
268
|
+
const other = s.reads.other(e.map, key.value);
|
|
269
|
+
return (other.assume.length ? other.assume : [[]]).map(a => ({ path: [...key.path, ...a], value: v(other.cell) }));
|
|
270
|
+
}
|
|
271
|
+
const at = focus.map(f => ({ path: [...key.path, ...eq(key.value, f.key)], value: v(f.cell) }));
|
|
272
|
+
const other = s.reads.other(e.map, key.value);
|
|
273
|
+
const assumes = other.assume.length ? other.assume : [[]];
|
|
274
|
+
const elsewhere = apartFrom(key.value, focus.map(f => f.key)).flatMap(apart => assumes.map(a => ({ path: [...key.path, ...apart, ...a], value: v(other.cell) })));
|
|
275
|
+
return [...at, ...elsewhere];
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
// The cases in which c has the given truth value.
|
|
280
|
+
function symC(c, s, truth) {
|
|
281
|
+
switch (c.op) {
|
|
282
|
+
case '<':
|
|
283
|
+
case '<=':
|
|
284
|
+
case '=':
|
|
285
|
+
return cross(symE(c.a, s), a => symE(c.b, s).flatMap(b => {
|
|
286
|
+
const [p, q] = [a, b.value];
|
|
287
|
+
const holds = c.op === '<' ? (truth ? [lt(p, q)] : [le(q, p)])
|
|
288
|
+
: c.op === '<=' ? (truth ? [le(p, q)] : [lt(q, p)])
|
|
289
|
+
: truth ? [eq(p, q)] : [lt(p, q), lt(q, p)];
|
|
290
|
+
return holds.map(constraints => ({ path: [...b.path, ...constraints], value: null }));
|
|
291
|
+
}));
|
|
292
|
+
case 'not': return symC(c.x, s, !truth);
|
|
293
|
+
case 'flag': {
|
|
294
|
+
if (s.flags === undefined || !(c.name in s.flags))
|
|
295
|
+
throw new Error(`the flag ${c.name} is read outside its step`);
|
|
296
|
+
return s.flags[c.name] === truth ? [{ path: [], value: null }] : [];
|
|
297
|
+
}
|
|
298
|
+
case 'and':
|
|
299
|
+
case 'or': {
|
|
300
|
+
if ((c.op === 'and') === truth)
|
|
301
|
+
return c.xs.reduce((acc, y) => cross(acc, () => symC(y, s, truth)), [{ path: [], value: null }]);
|
|
302
|
+
return c.xs.flatMap(y => symC(y, s, truth));
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
// PROVE_DEBUG=1 logs the case counts to stderr; read through globalThis so the module also loads where there is no process.
|
|
307
|
+
const proveDebug = !!globalThis.process?.env?.PROVE_DEBUG;
|
|
308
|
+
const debug = (label, n) => { if (proveDebug)
|
|
309
|
+
console.error(`[prove] ${label}: ${n} (${Math.round(performance.now())} ms)`); };
|
|
310
|
+
// Drops the cases whose path is already impossible, so impossible branches don't multiply.
|
|
311
|
+
function feasible(cases) {
|
|
312
|
+
return cases.filter(c => {
|
|
313
|
+
const names = [...new Set(c.path.flatMap(x => Object.keys(x.lin).filter(key => key !== '1')))];
|
|
314
|
+
return solve([...nat(...names), ...c.path]).kind !== 'refuted';
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
// The map reads a condition makes, as (map, key expression).
|
|
318
|
+
function readsOf(c) {
|
|
319
|
+
const inE = (e) => e.op === 'get' ? [{ map: e.map, key: e.key }, ...inE(e.key)]
|
|
320
|
+
: e.op === 'if' ? [...readsOfC(e.c), ...inE(e.then), ...inE(e.else)] : 'a' in e ? [...inE(e.a), ...('b' in e ? inE(e.b) : [])] : [];
|
|
321
|
+
const readsOfC = (y) => y.op === 'flag' ? [] : y.op === 'not' ? readsOfC(y.x) : y.op === 'and' || y.op === 'or' ? y.xs.flatMap(readsOfC) : [...inE(y.a), ...inE(y.b)];
|
|
322
|
+
return readsOfC(c);
|
|
323
|
+
}
|
|
324
|
+
const keyLin = (e, names) => symE(e, { names, reads: noReads })[0].value;
|
|
325
|
+
const cellName = (map, key, when) => `${map}[${show(key)}]${when}`;
|
|
326
|
+
// Proves `goal` by induction, assuming `invariants` (which include goal) hold before the step. A law with `forall`
|
|
327
|
+
// holds at every key; it is proven at a fresh key, and assumed at every key the step reads.
|
|
328
|
+
export function prove(m, goal, invariants = [goal]) {
|
|
329
|
+
const obligations = [];
|
|
330
|
+
const naturals = [...m.params, ...m.inputs, ...Object.keys(m.state)];
|
|
331
|
+
const preCases = m.pre ? symC(m.pre, { names: {}, reads: noReads }, true) : [{ path: [], value: null }];
|
|
332
|
+
const stepAssume = m.pre && m.assume ? all(m.pre, m.assume) : m.pre ?? m.assume;
|
|
333
|
+
const assumeCases = stepAssume ? symC(stepAssume, { names: {}, reads: noReads }, true) : [{ path: [], value: null }];
|
|
334
|
+
const goalNames = goal.forall ? { [goal.forall]: v(`${goal.forall}*`) } : {};
|
|
335
|
+
const forallLaws = invariants.filter(law => law.forall !== undefined);
|
|
336
|
+
const scalarLaws = invariants.filter(law => law.forall === undefined);
|
|
337
|
+
// At the start: every scalar and every map cell is its initial value.
|
|
338
|
+
const startNames = { ...goalNames };
|
|
339
|
+
const startPath = [];
|
|
340
|
+
for (const [name, e] of Object.entries(m.state)) {
|
|
341
|
+
const [only] = symE(e, { names: {}, reads: noReads });
|
|
342
|
+
startPath.push(...only.path, ...eq(v(`${name}@0`), only.value));
|
|
343
|
+
startNames[name] = v(`${name}@0`);
|
|
344
|
+
}
|
|
345
|
+
const startReads = { focus: [], other: map => ({ cell: `${map}@0`, assume: [] }) };
|
|
346
|
+
for (const map of Object.keys(m.maps ?? {}))
|
|
347
|
+
startPath.push(...eq(v(`${map}@0`), k(m.maps[map])));
|
|
348
|
+
for (const a of preCases)
|
|
349
|
+
for (const g of symC(goal.holds, { names: startNames, reads: startReads }, false)) {
|
|
350
|
+
const constraints = [...nat(...m.params, ...Object.values(goalNames).flatMap(l => Object.keys(l))), ...a.path, ...startPath, ...g.path];
|
|
351
|
+
obligations.push({ phase: 'start', law: goal.name, constraints, verdict: solve(constraints) });
|
|
352
|
+
}
|
|
353
|
+
// The focus cells: every map read of the goal and of the scalar invariants, before and after the step.
|
|
354
|
+
const focusReads = [...readsOf(goal.holds).map(r => ({ ...r, names: goalNames })), ...scalarLaws.flatMap(l => readsOf(l.holds).map(r => ({ ...r, names: {} })))];
|
|
355
|
+
const focus = [];
|
|
356
|
+
for (const r of focusReads) {
|
|
357
|
+
const key = keyLin(r.key, r.names);
|
|
358
|
+
if (!focus.some(f => f.map === r.map && show(f.key) === show(key)))
|
|
359
|
+
focus.push({ map: r.map, key, cell: cellName(r.map, key, '') });
|
|
360
|
+
}
|
|
361
|
+
// The laws that hold at every key, at one key: the cells there are resolved by `cellAt`.
|
|
362
|
+
const lawsAt = (key, cellAt) => forallLaws.reduce((acc, law) => {
|
|
363
|
+
const cases = symC(law.holds, { names: { [law.forall]: key }, reads: { focus: [], other: map => ({ cell: cellAt(map), assume: [] }) } }, true);
|
|
364
|
+
return acc.flatMap(p => cases.map(c => [...p, ...c.path]));
|
|
365
|
+
}, [[]]);
|
|
366
|
+
const otherCell = (map, key) => cellName(map, key, '');
|
|
367
|
+
const preReads = { focus, other: (map, key) => ({ cell: otherCell(map, key), assume: lawsAt(key, mp => otherCell(mp, key)) }) };
|
|
368
|
+
const preSym = { names: { ...goalNames }, reads: preReads };
|
|
369
|
+
// Assumptions before the step: the scalar invariants, the every-key laws at each focus key, and focus cells of one
|
|
370
|
+
// map at equal keys being equal.
|
|
371
|
+
let assumptions = [{ path: [], value: null }];
|
|
372
|
+
for (const law of scalarLaws)
|
|
373
|
+
assumptions = feasible(cross(assumptions, () => symC(law.holds, preSym, true)));
|
|
374
|
+
for (const f of focus) {
|
|
375
|
+
const cases = lawsAt(f.key, mp => focus.find(g => g.map === mp && show(g.key) === show(f.key))?.cell ?? otherCell(mp, f.key));
|
|
376
|
+
assumptions = feasible(cross(assumptions, () => cases.map(path => ({ path, value: null }))));
|
|
377
|
+
}
|
|
378
|
+
for (const [i, f] of focus.entries())
|
|
379
|
+
for (const g of focus.slice(i + 1).filter(g => g.map === f.map)) {
|
|
380
|
+
assumptions = feasible(cross(assumptions, () => [
|
|
381
|
+
{ path: [...eq(f.key, g.key), ...eq(v(f.cell), v(g.cell))], value: null },
|
|
382
|
+
...apartFrom(f.key, [g.key]).map(path => ({ path, value: null })),
|
|
383
|
+
]));
|
|
384
|
+
}
|
|
385
|
+
// The step: each new scalar, and each focus cell after the step's write to its map.
|
|
386
|
+
debug('assume cases', assumeCases.length);
|
|
387
|
+
debug('assumptions', assumptions.length);
|
|
388
|
+
// Keys first: how each key the step reads or writes compares with each focus key of the same map, decided once.
|
|
389
|
+
const stepReads = [
|
|
390
|
+
...Object.values(m.step.flags ?? {}).flatMap(readsOf),
|
|
391
|
+
...Object.values(m.step.state ?? {}).flatMap(e => readsOf({ op: '=', a: e, b: e })),
|
|
392
|
+
...Object.entries(m.step.maps ?? {}).flatMap(([map, w]) => [{ map, key: w.key }, ...readsOf({ op: '=', a: w.value, b: w.value })]),
|
|
393
|
+
];
|
|
394
|
+
const pairs = new Map();
|
|
395
|
+
for (const r of stepReads) {
|
|
396
|
+
const key = keyLin(r.key, {});
|
|
397
|
+
for (const f of focus.filter(f => f.map === r.map))
|
|
398
|
+
pairs.set(`${show(key)}|${show(f.key)}`, { key, fkey: f.key });
|
|
399
|
+
}
|
|
400
|
+
// The request's assumptions and the invariants start every case, so contradictory choices are pruned early.
|
|
401
|
+
let decisions = feasible(cross(assumeCases, () => assumptions)).map(c => ({ path: c.path, value: {} }));
|
|
402
|
+
for (const [id, { key, fkey }] of pairs) {
|
|
403
|
+
decisions = feasible(cross(decisions, acc => show(key) === show(fkey) ? [{ path: [], value: { ...acc, [id]: 'eq' } }] : [
|
|
404
|
+
{ path: eq(key, fkey), value: { ...acc, [id]: 'eq' } },
|
|
405
|
+
{ path: lt(key, fkey), value: { ...acc, [id]: 'apart' } },
|
|
406
|
+
{ path: lt(fkey, key), value: { ...acc, [id]: 'apart' } },
|
|
407
|
+
]));
|
|
408
|
+
}
|
|
409
|
+
debug('key decisions', decisions.length);
|
|
410
|
+
// Flags next: each named condition true or false, once, then the rest of the step under that choice.
|
|
411
|
+
let flagCases = decisions.map(d => ({ path: d.path, value: { decided: d.value, flags: {} } }));
|
|
412
|
+
for (const [name, c] of Object.entries(m.step.flags ?? {})) {
|
|
413
|
+
flagCases = feasible(cross(flagCases, acc => [true, false].flatMap(truth => symC(c, { ...preSym, decided: acc.decided, flags: acc.flags }, truth).map(r => ({ path: r.path, value: { decided: acc.decided, flags: { ...acc.flags, [name]: truth } } })))));
|
|
414
|
+
}
|
|
415
|
+
debug('flag cases', flagCases.length);
|
|
416
|
+
const stepCases = [];
|
|
417
|
+
for (const fc of flagCases) {
|
|
418
|
+
const preSymF = { ...preSym, flags: fc.value.flags, decided: fc.value.decided };
|
|
419
|
+
let cases = [{ path: fc.path, value: {} }];
|
|
420
|
+
for (const [name, e] of Object.entries(m.step.state ?? {}))
|
|
421
|
+
cases = feasible(cross(cases, acc => symE(e, preSymF).map(r => ({ path: r.path, value: { ...acc, [name]: r.value } }))));
|
|
422
|
+
for (const f of focus) {
|
|
423
|
+
const write = m.step.maps?.[f.map];
|
|
424
|
+
cases = feasible(cross(cases, acc => {
|
|
425
|
+
if (write === undefined)
|
|
426
|
+
return [{ path: [], value: { ...acc, [`${f.cell}'`]: v(f.cell) } }];
|
|
427
|
+
return cross(symE(write.key, preSymF), key => {
|
|
428
|
+
const decided = preSymF.decided?.[`${show(key)}|${show(f.key)}`];
|
|
429
|
+
const written = () => symE(write.value, preSymF).map(val => ({ path: [...val.path, ...(decided ? [] : eq(key, f.key))], value: { ...acc, [`${f.cell}'`]: val.value } }));
|
|
430
|
+
const kept = (paths) => paths.map(apart => ({ path: apart, value: { ...acc, [`${f.cell}'`]: v(f.cell) } }));
|
|
431
|
+
return decided === 'eq' ? written() : decided === 'apart' ? kept([[]]) : [...written(), ...kept(apartFrom(key, [f.key]))];
|
|
432
|
+
});
|
|
433
|
+
}));
|
|
434
|
+
}
|
|
435
|
+
stepCases.push(...cases);
|
|
436
|
+
}
|
|
437
|
+
debug('step cases', stepCases.length);
|
|
438
|
+
for (const st of stepCases) {
|
|
439
|
+
const postNames = { ...goalNames, ...Object.fromEntries(Object.keys(m.state).map(name => [name, st.value[name] ?? v(name)])) };
|
|
440
|
+
const postFocus = focus.map(f => ({ ...f, cell: `${f.cell}'` }));
|
|
441
|
+
const negated = symC(goal.holds, { names: postNames, reads: { focus: postFocus, other: noReads.other } }, false);
|
|
442
|
+
for (const g of negated) {
|
|
443
|
+
// The goal reads the focus cells after the step; replace each by its value.
|
|
444
|
+
const path = g.path.map(c => ({ ...c, lin: postFocus.reduce((lin, f) => renameKey(lin, f.cell, st.value[f.cell]), c.lin) }));
|
|
445
|
+
const all = [...st.path, ...path];
|
|
446
|
+
const vars = [...new Set(all.flatMap(c => Object.keys(c.lin).filter(key => key !== '1')))];
|
|
447
|
+
const constraints = [...nat(...new Set([...naturals, ...vars])), ...all];
|
|
448
|
+
obligations.push({ phase: 'step', law: goal.name, constraints, verdict: solve(constraints) });
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return { law: goal, proven: obligations.every(o => o.verdict.kind === 'refuted'), obligations };
|
|
452
|
+
}
|
|
453
|
+
const noReads = { focus: [], other: map => ({ cell: `${map}[?]`, assume: [] }) };
|
|
454
|
+
function renameKey(lin, from, to) {
|
|
455
|
+
if (!(from in lin) || to === undefined)
|
|
456
|
+
return lin;
|
|
457
|
+
const { [from]: c, ...rest } = lin;
|
|
458
|
+
return add(rest, scale(to, c));
|
|
459
|
+
}
|
|
460
|
+
// --- real counterexamples ----------------------------------------------------------------------------------------------
|
|
461
|
+
// Every sequence of up to `depth` requests, with params and inputs within 0..bound, from the start: the first that breaks
|
|
462
|
+
// the law, checked with the interpreter. A law that fails here is false; one that only fails `prove` may just need a
|
|
463
|
+
// stronger invariant.
|
|
464
|
+
export function search(m, law, depth = 3, bound = 2) {
|
|
465
|
+
const run = interpret(m);
|
|
466
|
+
const holds = (params, st) => {
|
|
467
|
+
const keys = law.forall ? [...new Set([0, ...Object.values(st.maps).flatMap(cells => [...cells.keys()])])] : [0];
|
|
468
|
+
return keys.every(key => evalC(law.holds, { ...params, ...(law.forall ? { [law.forall]: key } : {}) }, st, m));
|
|
469
|
+
};
|
|
470
|
+
const tuples = (names) => names.reduce((acc, name) => acc.flatMap(t => Array.from({ length: bound + 1 }, (_, i) => ({ ...t, [name]: i }))), [{}]);
|
|
471
|
+
const assumed = (env, st) => m.assume === undefined || evalC(m.assume, env, st, m);
|
|
472
|
+
const meetsPre = (params) => m.pre === undefined || evalC(m.pre, params, { scalars: {}, maps: {} }, m);
|
|
473
|
+
for (const params of tuples(m.params)) {
|
|
474
|
+
const go = (st, trace) => {
|
|
475
|
+
if (!holds(params, st))
|
|
476
|
+
return trace;
|
|
477
|
+
if (trace.length === depth)
|
|
478
|
+
return undefined;
|
|
479
|
+
for (const input of tuples(m.inputs)) {
|
|
480
|
+
if (!assumed({ ...params, ...input }, st))
|
|
481
|
+
continue;
|
|
482
|
+
const found = go(run.step(params, st, input), [...trace, input]);
|
|
483
|
+
if (found)
|
|
484
|
+
return found;
|
|
485
|
+
}
|
|
486
|
+
return undefined;
|
|
487
|
+
};
|
|
488
|
+
void go;
|
|
489
|
+
}
|
|
490
|
+
// Shortest first: every depth for every params before the next depth.
|
|
491
|
+
for (let limit = 0; limit <= depth; limit++)
|
|
492
|
+
for (const params of tuples(m.params).filter(meetsPre)) {
|
|
493
|
+
const go = (st, trace) => {
|
|
494
|
+
if (!holds(params, st))
|
|
495
|
+
return trace;
|
|
496
|
+
if (trace.length === limit)
|
|
497
|
+
return undefined;
|
|
498
|
+
for (const input of tuples(m.inputs)) {
|
|
499
|
+
if (!assumed({ ...params, ...input }, st))
|
|
500
|
+
continue;
|
|
501
|
+
const found = go(run.step(params, st, input), [...trace, input]);
|
|
502
|
+
if (found)
|
|
503
|
+
return found;
|
|
504
|
+
}
|
|
505
|
+
return undefined;
|
|
506
|
+
};
|
|
507
|
+
const trace = go(run.init(params), []);
|
|
508
|
+
if (trace)
|
|
509
|
+
return { params, trace };
|
|
510
|
+
}
|
|
511
|
+
return undefined;
|
|
512
|
+
}
|
|
513
|
+
// The cases of linear constraints in which a condition over plain variables has the given truth value.
|
|
514
|
+
export function conditionCases(c, truth) {
|
|
515
|
+
return symC(c, { names: {}, reads: noReads }, truth).map(cs => cs.path);
|
|
516
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export type Lin = Record<string, number>;
|
|
2
|
+
export type Constraint = {
|
|
3
|
+
lin: Lin;
|
|
4
|
+
label: string;
|
|
5
|
+
};
|
|
6
|
+
export declare const v: (name: string) => Lin;
|
|
7
|
+
export declare const k: (value: number) => Lin;
|
|
8
|
+
export declare function add(...xs: Lin[]): Lin;
|
|
9
|
+
export declare const scale: (x: Lin, f: number) => Lin;
|
|
10
|
+
export declare const sub: (a: Lin, b: Lin) => Lin;
|
|
11
|
+
export declare function show(x: Lin): string;
|
|
12
|
+
export declare const le: (a: Lin, b: Lin, label?: string) => Constraint[];
|
|
13
|
+
export declare const lt: (a: Lin, b: Lin, label?: string) => Constraint[];
|
|
14
|
+
export declare const eq: (a: Lin, b: Lin, label?: string) => Constraint[];
|
|
15
|
+
export declare const nat: (...names: string[]) => Constraint[];
|
|
16
|
+
export declare const negate: (c: Constraint) => Constraint;
|
|
17
|
+
export type Step = {
|
|
18
|
+
kind: 'input';
|
|
19
|
+
index: number;
|
|
20
|
+
} | {
|
|
21
|
+
kind: 'combine';
|
|
22
|
+
a: number;
|
|
23
|
+
b: number;
|
|
24
|
+
ma: number;
|
|
25
|
+
mb: number;
|
|
26
|
+
} | {
|
|
27
|
+
kind: 'cut';
|
|
28
|
+
of: number;
|
|
29
|
+
g: number;
|
|
30
|
+
};
|
|
31
|
+
export type Verdict = {
|
|
32
|
+
kind: 'refuted';
|
|
33
|
+
steps: Step[];
|
|
34
|
+
} | {
|
|
35
|
+
kind: 'sat';
|
|
36
|
+
witness: Record<string, number>;
|
|
37
|
+
} | {
|
|
38
|
+
kind: 'unknown';
|
|
39
|
+
point?: Record<string, number>;
|
|
40
|
+
};
|
|
41
|
+
export declare function solve(constraints: Constraint[], maxRows?: number): Verdict;
|