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,526 @@
1
+ // Typed builders for machines, decisions and loops: names are declared once and come back as typed values, so a
2
+ // misspelled name, a law that reads a request, or a step that sets an undeclared variable is a TypeScript error, and
3
+ // the input kinds the kernel would otherwise see as hand encodings (a flag, an optional fact, a choice among values)
4
+ // are declared as kinds. Everything here compiles to the plain values kernel.ts and the runtimes take (Machine, Law,
5
+ // Goal, Loop), so nothing in this file is trusted: the kernel checks what it is given, and its scope check refuses
6
+ // whatever the types here let through.
7
+ //
8
+ // const budget = rule('budget')
9
+ // .params({b: nat, m: nat})
10
+ // .inputs({c: nat})
11
+ // .state({s: 0})
12
+ // .step(({b, m, c, s}) => ({s: when(s.lt(b), s.plus(min(c, m)), s)}))
13
+ // const bounded = budget.law('spend stays within b + m', ({s, b, m}) => s.atMost(b.plus(m)))
14
+ // proveRule(budget, [bounded])
15
+ //
16
+ // A step returns its updates as one object: every right-hand side reads the state before the step, as the kernel and
17
+ // both runtimes do.
18
+ import { interpret, loadJs } from "./machine.js";
19
+ import { check, loopObligations } from "../kernel/kernel.js";
20
+ import { grow, proveLaws, proveStepLaws } from "../kernel/prove.js";
21
+ // --- terms and conditions -------------------------------------------------------------------------------------------
22
+ const naturalConst = (v) => {
23
+ if (!Number.isSafeInteger(v) || v < 0)
24
+ throw new RangeError(`${v} is not a natural`);
25
+ return v;
26
+ };
27
+ export const termOf = (t) => (typeof t === 'number' ? { op: 'const', value: naturalConst(t) } : t.e);
28
+ // A natural-valued term. `minus` is truncated (a ∸ b), `times` multiplies by a constant, `mul` by another term (a
29
+ // product: the kernel proves with it, the GPU searches don't take it).
30
+ export class Term {
31
+ e;
32
+ constructor(e) {
33
+ this.e = e;
34
+ }
35
+ plus(b) { return new Term({ op: 'add', a: this.e, b: termOf(b) }); }
36
+ minus(b) { return new Term({ op: 'sub', a: this.e, b: termOf(b) }); }
37
+ times(k) { return new Term({ op: 'mul', k: naturalConst(k), a: this.e }); }
38
+ mul(b) { return new Term({ op: 'prod', a: this.e, b: termOf(b) }); }
39
+ lt(b) { return new Cond({ op: '<', a: this.e, b: termOf(b) }); }
40
+ atMost(b) { return new Cond({ op: '<=', a: this.e, b: termOf(b) }); }
41
+ gt(b) { return new Cond({ op: '<', a: termOf(b), b: this.e }); }
42
+ atLeast(b) { return new Cond({ op: '<=', a: termOf(b), b: this.e }); }
43
+ eq(b) { return new Cond({ op: '=', a: this.e, b: termOf(b) }); }
44
+ ne(b) { return this.eq(b).not(); }
45
+ }
46
+ export class Cond {
47
+ c;
48
+ constructor(c) {
49
+ this.c = c;
50
+ }
51
+ and(...xs) { return all(this, ...xs); }
52
+ or(...xs) { return any(this, ...xs); }
53
+ not() { return new Cond({ op: 'not', x: this.c }); }
54
+ implies(b) { return any(this.not(), b); }
55
+ iff(b) { return all(this.implies(b), b.implies(this)); }
56
+ }
57
+ export const lit = (v) => new Term(termOf(v));
58
+ export const yes = new Cond({ op: '<=', a: { op: 'const', value: 0 }, b: { op: 'const', value: 0 } });
59
+ export const no = new Cond({ op: '<', a: { op: 'const', value: 0 }, b: { op: 'const', value: 0 } });
60
+ export const all = (...cs) => (cs.length === 0 ? yes : cs.length === 1 ? cs[0] : new Cond({ op: 'and', xs: cs.map(c => c.c) }));
61
+ export const any = (...cs) => (cs.length === 0 ? no : cs.length === 1 ? cs[0] : new Cond({ op: 'or', xs: cs.map(c => c.c) }));
62
+ export const not = (c) => c.not();
63
+ export const min = (a, b) => new Term({ op: 'min', a: termOf(a), b: termOf(b) });
64
+ export const max = (a, b) => new Term({ op: 'max', a: termOf(a), b: termOf(b) });
65
+ export const when = (c, then, otherwise) => new Term({ op: 'if', c: c.c, then: termOf(then), else: termOf(otherwise) });
66
+ // A condition by cases: `then` where c holds, `otherwise` elsewhere.
67
+ export const choose = (c, then, otherwise) => any(all(c, then), all(c.not(), otherwise));
68
+ // 1 where c holds, else 0: what a sum over a history adds per record.
69
+ export const count = (c) => when(c, 1, 0);
70
+ export const sum = (first, ...rest) => rest.reduce((acc, t) => acc.plus(t), typeof first === 'number' ? lit(first) : first);
71
+ // A kind names the variables a value is encoded in, what is assumed of them, the value the builders hand out, and how
72
+ // a runtime value is encoded and drawn at random (the examples' harness draws inputs this way).
73
+ export class Kind {
74
+ }
75
+ const v = (name) => new Term({ op: 'var', name });
76
+ const asNat = (name, raw) => {
77
+ if (typeof raw !== 'number' || !Number.isSafeInteger(raw) || raw < 0)
78
+ throw new RangeError(`${name} must be a natural, not ${String(raw)}`);
79
+ return raw;
80
+ };
81
+ class NatKind extends Kind {
82
+ maxValue;
83
+ constructor(maxValue) {
84
+ super();
85
+ this.maxValue = maxValue;
86
+ }
87
+ vars(name) { return [name]; }
88
+ assume(name) { return this.maxValue === undefined ? [] : [v(name).atMost(this.maxValue)]; }
89
+ value(name, prefix = '') { return v(prefix + name); }
90
+ encode(name, raw) {
91
+ const n = asNat(name, raw);
92
+ if (this.maxValue !== undefined && n > this.maxValue)
93
+ throw new RangeError(`${name} is at most ${this.maxValue}, not ${n}`);
94
+ return { [name]: n };
95
+ }
96
+ random(name, rand, bound) { return { [name]: rand((this.maxValue ?? bound) + 1) }; }
97
+ }
98
+ export const nat = new NatKind();
99
+ // A natural known to be at most `maxValue` (a percentage, a floor number, a weekday).
100
+ export const upTo = (maxValue) => new NatKind(naturalConst(maxValue));
101
+ // A yes/no value, encoded as 0 or 1. It is a condition; `.n` is its 0/1 term (to add or count it).
102
+ export class Bool extends Cond {
103
+ n;
104
+ constructor(n) {
105
+ super({ op: '=', a: n.e, b: { op: 'const', value: 1 } });
106
+ this.n = n;
107
+ }
108
+ }
109
+ class BoolKind extends Kind {
110
+ vars(name) { return [name]; }
111
+ assume(name) { return [v(name).atMost(1)]; }
112
+ value(name, prefix = '') { return new Bool(v(prefix + name)); }
113
+ encode(name, raw) {
114
+ if (typeof raw === 'boolean')
115
+ return { [name]: raw ? 1 : 0 };
116
+ if (raw === 0 || raw === 1)
117
+ return { [name]: raw };
118
+ throw new RangeError(`${name} must be a boolean, not ${String(raw)}`);
119
+ }
120
+ random(name, rand) { return { [name]: rand(2) }; }
121
+ }
122
+ export const bool = new BoolKind();
123
+ // One of a few named values, encoded as its index. `match` takes every case (TypeScript checks none is missing).
124
+ export class Choice {
125
+ n;
126
+ values;
127
+ constructor(n, values) {
128
+ this.n = n;
129
+ this.values = values;
130
+ }
131
+ is(k) {
132
+ const i = this.values.indexOf(k);
133
+ if (i < 0)
134
+ throw new Error(`${k} is not one of ${this.values.join(', ')}`);
135
+ return this.n.eq(i);
136
+ }
137
+ match(cases) {
138
+ const results = this.values.map(k => [k, cases[k]()]);
139
+ if (results.every(([, r]) => r instanceof Cond))
140
+ return any(...results.map(([k, r]) => all(this.is(k), r)));
141
+ if (results.some(([, r]) => r instanceof Cond))
142
+ throw new Error('every case of a match is a condition, or every case is a term');
143
+ // The last case is what's left: the kind's assumption says the index is below the number of values.
144
+ return results.slice(0, -1).reduceRight((acc, [k, r]) => when(this.is(k), r, acc), typeof results.at(-1)[1] === 'number' ? lit(results.at(-1)[1]) : results.at(-1)[1]);
145
+ }
146
+ }
147
+ class OneOfKind extends Kind {
148
+ values;
149
+ constructor(values) {
150
+ super();
151
+ this.values = values;
152
+ if (values.length === 0 || new Set(values).size !== values.length)
153
+ throw new Error('oneOf takes distinct values');
154
+ }
155
+ vars(name) { return [name]; }
156
+ assume(name) { return [v(name).lt(this.values.length)]; }
157
+ value(name, prefix = '') { return new Choice(v(prefix + name), this.values); }
158
+ encode(name, raw) {
159
+ const i = this.values.indexOf(raw);
160
+ if (i < 0)
161
+ throw new RangeError(`${name} is one of ${this.values.join(', ')}, not ${String(raw)}`);
162
+ return { [name]: i };
163
+ }
164
+ random(name, rand) { return { [name]: rand(this.values.length) }; }
165
+ }
166
+ export const oneOf = (...values) => new OneOfKind(values);
167
+ // A value that may be missing, encoded as a presence bit and the value. The value can only be read in `some`, so no
168
+ // law or step can read a missing value; the encoding leaves it unconstrained when missing, so nothing proven depends
169
+ // on it.
170
+ export class Maybe {
171
+ present;
172
+ inner;
173
+ constructor(present, inner) {
174
+ this.present = present;
175
+ this.inner = inner;
176
+ }
177
+ get isSome() { return this.present.eq(1); }
178
+ get isNone() { return this.present.eq(0); }
179
+ match(cases) {
180
+ const none = cases.none(), some = cases.some(this.inner);
181
+ if (none instanceof Cond && some instanceof Cond)
182
+ return choose(this.isSome, some, none);
183
+ if (none instanceof Cond || some instanceof Cond)
184
+ throw new Error('both cases of a match are conditions, or both are terms');
185
+ return when(this.isSome, some, none);
186
+ }
187
+ }
188
+ class MaybeKind extends Kind {
189
+ of;
190
+ constructor(of) {
191
+ super();
192
+ this.of = of;
193
+ }
194
+ has(name) { return `${name}$some`; }
195
+ vars(name) { return [this.has(name), ...this.of.vars(name)]; }
196
+ assume(name) { return [v(this.has(name)).atMost(1), ...this.of.assume(name)]; }
197
+ value(name, prefix = '') { return new Maybe(v(prefix + this.has(name)), this.of.value(name, prefix)); }
198
+ encode(name, raw) {
199
+ if (raw === null || raw === undefined)
200
+ return { [this.has(name)]: 0, ...Object.fromEntries(this.of.vars(name).map(n => [n, 0])) };
201
+ return { [this.has(name)]: 1, ...this.of.encode(name, raw) };
202
+ }
203
+ random(name, rand, bound) {
204
+ return rand(4) === 0 ? { [this.has(name)]: 0, ...Object.fromEntries(this.of.vars(name).map(n => [n, rand(bound + 1)])) } : { [this.has(name)]: 1, ...this.of.random(name, rand, bound) };
205
+ }
206
+ }
207
+ export const maybe = (of) => new MaybeKind(of);
208
+ // A map from naturals to naturals, every cell starting at `init`. One write per map per step.
209
+ export class MapRef {
210
+ name;
211
+ init;
212
+ constructor(name, init) {
213
+ this.name = name;
214
+ this.init = init;
215
+ }
216
+ at(key) { return new Term({ op: 'get', map: this.name, key: termOf(key) }); }
217
+ set(key, value) { return { map: this.name, key: termOf(key), value: termOf(value) }; }
218
+ // key's cell plus `by` where `c` holds (the counter every cap keeps).
219
+ bump(key, c = yes, by = 1) { return this.set(key, when(c, this.at(key).plus(by), this.at(key))); }
220
+ }
221
+ // A law about every key of a map: proven at a fresh key, assumed at the keys a step reads.
222
+ export const forall = (f) => ({ forall: f });
223
+ // For histories: `holds` (a <= b, homogeneous over sums and queries) wherever `when` (params and plain inputs) holds.
224
+ export const relation = (holds, whenever = yes) => ({ when: whenever, holds });
225
+ let keyCounter = 0;
226
+ // A k-step law's later requests are built over marked names (j:name, which no rule can declare: ':' is reserved); the
227
+ // kernel reads them as name@j.
228
+ const markedName = (name) => { const m = /^(\d+):(.*)$/.exec(name); return m ? `${m[2]}@${m[1]}` : name; };
229
+ const markedE = (e) => {
230
+ switch (e.op) {
231
+ case 'var': return { op: 'var', name: markedName(e.name) };
232
+ case 'const': return e;
233
+ case 'mul': return { ...e, a: markedE(e.a) };
234
+ case 'if': return { op: 'if', c: marked(e.c), then: markedE(e.then), else: markedE(e.else) };
235
+ case 'get': return { op: 'get', map: e.map, key: markedE(e.key) };
236
+ case 'add':
237
+ case 'sub':
238
+ case 'min':
239
+ case 'max':
240
+ case 'prod': return { ...e, a: markedE(e.a), b: markedE(e.b) };
241
+ }
242
+ };
243
+ const marked = (c) => c.op === 'flag' ? c : c.op === 'not' ? { op: 'not', x: marked(c.x) } : c.op === 'and' || c.op === 'or' ? { op: c.op, xs: c.xs.map(marked) } : { ...c, a: markedE(c.a), b: markedE(c.b) };
244
+ // P params, I inputs, S state, M maps, F flags, R a history's record, Su its sums, Q its queries: each a record of the
245
+ // values the builders hand out, grown by each call.
246
+ export class Rule {
247
+ spec;
248
+ constructor(spec) {
249
+ this.spec = spec;
250
+ }
251
+ with(more) { return new Rule({ ...this.spec, ...more }); }
252
+ get name() { return this.spec.name; }
253
+ params(kinds) { return this.with({ params: [...this.spec.params, ...Object.entries(kinds)] }); }
254
+ inputs(kinds) { return this.with({ inputs: [...this.spec.inputs, ...Object.entries(kinds)] }); }
255
+ // Each variable starts at a natural or at a term over the params.
256
+ state(init) {
257
+ return this.with({ state: [...this.spec.state, ...Object.entries(init)] });
258
+ }
259
+ maps(init) { return this.with({ maps: [...this.spec.maps, ...Object.entries(init)] }); }
260
+ // Named conditions of the step, decided once per step before it; each call's flags may read the earlier calls'. A flag
261
+ // named again replaces the earlier definition in place (what a variant needs: one call, the step unchanged); the new
262
+ // definition may read only the flags defined before the original's place, as the kernel checks.
263
+ flags(f) {
264
+ return this.with({ flags: [...this.spec.flags, f] });
265
+ }
266
+ // The step (replaces an earlier one, so a variant is one call away): its updates read the state before it.
267
+ step(f) {
268
+ return this.with({ step: f });
269
+ }
270
+ // A precondition on the params: the laws are claimed only for params that meet it.
271
+ pre(f) { return this.with({ pre: [...this.spec.pre, f] }); }
272
+ // What every request satisfies (with the input kinds' own assumptions): it holds at each step, not at the start.
273
+ assume(f) { return this.with({ assume: [...this.spec.assume, f] }); }
274
+ // A history: the requests the step keeps, each recorded with these inputs.
275
+ log(...record) { return this.with({ record }); }
276
+ // State equal to the sum over the history of a term of the record and the params.
277
+ sums(sums) {
278
+ return this.with({ sums: [...this.spec.sums, ...Object.entries(sums)] });
279
+ }
280
+ // An input the runtime computes: the sum over the history before the request of a term of the record, the params and
281
+ // the request's other inputs.
282
+ queries(queries) {
283
+ return this.with({ queries: [...this.spec.queries, ...Object.entries(queries)] });
284
+ }
285
+ keep(f) { return this.with({ keep: f }); }
286
+ relations(f) { return this.with({ relations: f }); }
287
+ // Requests come in nondecreasing `order`; a record whose order plus `after` is at most the request's is dropped
288
+ // (the kernel proves it adds nothing to any query).
289
+ prune(order, after) { return this.with({ prune: { order, after: after } }); }
290
+ // A law about the params, the state, the maps and the sums (not a request): `forall` for every key of a map.
291
+ law(name, f) {
292
+ const out = f(this.env({ flags: false, inputs: false, queries: false }));
293
+ if (out instanceof Cond)
294
+ return { name, holds: out.c };
295
+ const key = `key$${++keyCounter}`;
296
+ return { name, forall: key, holds: out.forall(v(key)).c };
297
+ }
298
+ // A law about one step, for every request the rule allows: `then(before, after)` relates what the step reads (params,
299
+ // the request, state, maps, flags, sums, queries) to the state, maps and sums after it. `when` narrows the requests.
300
+ // counter.stepLaw('never goes back', ({used}, after) => used.atMost(after.used))
301
+ stepLaw(name, then, when) {
302
+ const env = this.env();
303
+ const after = {};
304
+ for (const [n] of this.spec.state)
305
+ after[n] = v(`${n}'`);
306
+ for (const [n] of this.spec.sums)
307
+ after[n] = v(`${n}'`);
308
+ for (const [n, init] of this.spec.maps)
309
+ after[n] = new MapRef(`${n}'`, init);
310
+ const out = then(env, after), w = when ? { when: when(env).c } : {};
311
+ if (out instanceof Cond)
312
+ return { name, then: out.c, ...w };
313
+ const key = `key$${++keyCounter}`;
314
+ return { name, forall: key, then: out.forall(v(key)).c, ...w };
315
+ }
316
+ // A law about k steps in a row (k from 2 to 16): `then(first, last, requests)` relates what the first step reads to the
317
+ // state, maps and sums after the last, with each step's request and queries (requests[0] is the first's); `when(before,
318
+ // j)` narrows the j-th step's request (j from 0), read at the state that step starts from.
319
+ // meter.stepsLaw('two requests spend at most 2m', 2, ({spent, m}, after) => after.spent.atMost(spent.plus(m).plus(m)))
320
+ stepsLaw(name, k, then, when) {
321
+ const env = this.env();
322
+ const after = {};
323
+ for (const [n] of this.spec.state)
324
+ after[n] = v(`${n}'`);
325
+ for (const [n] of this.spec.sums)
326
+ after[n] = v(`${n}'`);
327
+ for (const [n, init] of this.spec.maps)
328
+ after[n] = new MapRef(`${n}'`, init);
329
+ // The j-th request's values over marked names, renamed to the kernel's input@j once built.
330
+ const mark = (j) => `${j}:`;
331
+ const requests = Array.from({ length: k }, (_, j) => {
332
+ if (j === 0)
333
+ return Object.fromEntries([...this.spec.inputs.map(([n]) => n), ...this.spec.queries.map(([n]) => n)].map(n => [n, env[n]]));
334
+ return Object.fromEntries([...this.spec.inputs.map(([n, kind]) => [n, kind.value(n, mark(j + 1))]), ...this.spec.queries.map(([n]) => [n, v(`${mark(j + 1)}${n}`)])]);
335
+ });
336
+ const out = then(env, after, requests);
337
+ const whens = when ? Array.from({ length: k }, (_, j) => when(env, j)) : [];
338
+ while (whens.length && whens.at(-1) === undefined)
339
+ whens.pop();
340
+ const w = whens.length ? { when: whens.map(c => (c ?? lit(0).atMost(0)).c) } : {};
341
+ if (out instanceof Cond)
342
+ return { name, steps: k, then: marked(out.c), ...w };
343
+ const key = `key$${++keyCounter}`;
344
+ return { name, steps: k, forall: key, then: marked(out.forall(v(key)).c), ...w };
345
+ }
346
+ // A decision: whenever the params and the request meet `given`, `then` holds. Decisions don't read the state.
347
+ decide(name, given, then) {
348
+ const env = this.env({ flags: false, state: false, queries: false });
349
+ const g = given(env), hyps = [...this.kindAssumptions(), ...this.spec.pre.map(f => f(env).c), ...this.spec.assume.map(f => f(env).c), ...(Array.isArray(g) ? g : [g]).map(c => c.c)];
350
+ const t = then(env).c;
351
+ return { name, rule: this, given: hyps, then: t, goal: { name, hyps: [...hyps, { op: 'not', x: t }] } };
352
+ }
353
+ // --- what it compiles to ------------------------------------------------------------------------------------------
354
+ kindAssumptions(which = 'both') {
355
+ const ps = which === 'inputs' ? [] : this.spec.params.flatMap(([n, k]) => k.assume(n));
356
+ const is = which === 'params' ? [] : this.spec.inputs.flatMap(([n, k]) => k.assume(n));
357
+ return [...ps, ...is].map(c => c.c);
358
+ }
359
+ // The values handed to the builders' callbacks.
360
+ env(parts = {}) {
361
+ const out = {};
362
+ for (const [n, k] of this.spec.params)
363
+ out[n] = k.value(n);
364
+ if (parts.inputs !== false)
365
+ for (const [n, k] of this.spec.inputs)
366
+ out[n] = k.value(n);
367
+ if (parts.state !== false) {
368
+ for (const [n] of this.spec.state)
369
+ out[n] = v(n);
370
+ for (const [n, init] of this.spec.maps)
371
+ out[n] = new MapRef(n, init);
372
+ for (const [n] of this.spec.sums)
373
+ out[n] = v(n);
374
+ }
375
+ if (parts.queries !== false)
376
+ for (const [n] of this.spec.queries)
377
+ out[n] = v(n);
378
+ if (parts.flags !== false)
379
+ for (const f of this.spec.flags)
380
+ for (const n of Object.keys(f(out)))
381
+ out[n] = new Cond({ op: 'flag', name: n });
382
+ return out;
383
+ }
384
+ get machine() {
385
+ const s = this.spec;
386
+ const params = s.params.flatMap(([n, k]) => k.vars(n)), queries = s.queries.map(([n]) => n);
387
+ const inputs = [...s.inputs.flatMap(([n, k]) => k.vars(n)), ...queries];
388
+ const pEnv = this.env({ inputs: false, state: false, flags: false, queries: false });
389
+ const env = this.env({ flags: false });
390
+ const flags = {};
391
+ for (const f of s.flags)
392
+ for (const [n, c] of Object.entries(f(env))) {
393
+ flags[n] = c.c;
394
+ env[n] = new Cond({ op: 'flag', name: n });
395
+ } // a name again: replaced in place
396
+ const updates = s.step?.(env) ?? {};
397
+ const state = {}, maps = {};
398
+ for (const [n, u] of Object.entries(updates)) {
399
+ if (u === undefined)
400
+ continue;
401
+ if (typeof u === 'object' && 'map' in u) {
402
+ if (u.map !== n)
403
+ throw new Error(`${s.name}: the step writes map ${u.map} under the name ${n}`);
404
+ maps[n] = { key: u.key, value: u.value };
405
+ }
406
+ else
407
+ state[n] = termOf(u);
408
+ }
409
+ const pre = [...this.kindAssumptions('params'), ...s.pre.map(f => f(pEnv).c)];
410
+ const assume = [...this.kindAssumptions('inputs'), ...s.assume.map(f => f(env).c)];
411
+ const m = {
412
+ name: s.name, params, inputs,
413
+ state: Object.fromEntries(s.state.map(([n, init]) => [n, typeof init === 'number' ? termOf(init) : termOf(init(pEnv))])),
414
+ ...(s.maps.length ? { maps: Object.fromEntries(s.maps) } : {}),
415
+ step: { ...(Object.keys(flags).length ? { flags } : {}), ...(Object.keys(state).length ? { state } : {}), ...(Object.keys(maps).length ? { maps } : {}) },
416
+ ...(pre.length ? { pre: pre.length === 1 ? pre[0] : { op: 'and', xs: pre } } : {}),
417
+ ...(assume.length ? { assume: assume.length === 1 ? assume[0] : { op: 'and', xs: assume } } : {}),
418
+ };
419
+ if (s.record) {
420
+ const kinds = new Map(s.inputs);
421
+ const record = Object.fromEntries(s.record.map(n => [n, kinds.get(n).value(n, 'r.')]));
422
+ const plain = this.env({ state: false, flags: false, queries: false });
423
+ const rv = Object.fromEntries(Object.entries(env).filter(([n]) => queries.includes(n) || s.sums.some(([sn]) => sn === n) || n in plain));
424
+ const pruneOrder = s.prune ? kinds.get(s.prune.order).vars(s.prune.order) : undefined;
425
+ if (pruneOrder && pruneOrder.length !== 1)
426
+ throw new Error(`${s.name}: a history is pruned by a single-variable input`);
427
+ m.history = {
428
+ keep: (s.keep?.(env) ?? yes).c,
429
+ record: s.record.flatMap(n => kinds.get(n).vars(n)),
430
+ sums: Object.fromEntries(s.sums.map(([n, f]) => [n, termOf(f(record, pEnv))])),
431
+ queries: Object.fromEntries(s.queries.map(([n, f]) => [n, termOf(f(record, plain))])),
432
+ ...(s.relations ? { relations: s.relations(rv).map(r => ({ when: r.when.c, holds: r.holds.c })) } : {}),
433
+ ...(s.prune ? { prune: { order: pruneOrder[0], after: termOf(s.prune.after(pEnv)) } } : {}),
434
+ };
435
+ }
436
+ return m;
437
+ }
438
+ // --- running it -----------------------------------------------------------------------------------------------------
439
+ // Runtime values (numbers, booleans, choice names, null for a missing value) to the variables the machine reads.
440
+ encodeParams(values) { return encode(this.spec.params, values); }
441
+ encodeInput(values) { return encode(this.spec.inputs, values); }
442
+ // Random params and inputs that meet their kinds (not the user's `pre` or `assume`, which a caller checks).
443
+ randomParams(rand, bound) { return Object.assign({}, ...this.spec.params.map(([n, k]) => k.random(n, rand, bound))); }
444
+ randomInput(rand, bound) { return Object.assign({}, ...this.spec.inputs.map(([n, k]) => k.random(n, rand, bound))); }
445
+ // The prune order's variable, if the history is pruned (a runtime must send it nondecreasing).
446
+ get orderedBy() { return this.spec.prune ? new Map(this.spec.inputs).get(this.spec.prune.order).vars(this.spec.prune.order)[0] : undefined; }
447
+ interpreter() { return interpret(this.machine); }
448
+ compiled() {
449
+ return loadJs(this.machine);
450
+ }
451
+ }
452
+ const encode = (kinds, values) => Object.assign({}, ...kinds.map(([n, k]) => k.encode(n, values[n])));
453
+ export const rule = (name) => new Rule({ name, params: [], inputs: [], state: [], maps: [], flags: [], pre: [], assume: [], sums: [], queries: [] });
454
+ // A loop runs `body` while `while` holds, each turn with fresh inputs; it halts if a rank over the state and params
455
+ // (or a tuple of them, compared in order) drops on every turn.
456
+ export class LoopRule {
457
+ spec;
458
+ constructor(spec) {
459
+ this.spec = spec;
460
+ }
461
+ with(more) { return new LoopRule({ ...this.spec, ...more }); }
462
+ params(kinds) { return this.with({ params: [...this.spec.params, ...Object.entries(kinds)] }); }
463
+ inputs(kinds) { return this.with({ inputs: [...this.spec.inputs, ...Object.entries(kinds)] }); }
464
+ state(init) { return this.with({ state: [...this.spec.state, ...Object.entries(init)] }); }
465
+ while(f) { return this.with({ while: f }); }
466
+ body(f) { return this.with({ body: f }); }
467
+ pre(f) { return this.with({ pre: [...this.spec.pre, f] }); }
468
+ assume(f) { return this.with({ assume: [...this.spec.assume, f] }); }
469
+ rank(f) { return this.with({ rank: f }); }
470
+ law(name, f) { return { name, holds: f(this.env(false)).c }; }
471
+ env(inputs = true) {
472
+ const out = {};
473
+ for (const [n, k] of this.spec.params)
474
+ out[n] = k.value(n);
475
+ if (inputs)
476
+ for (const [n, k] of this.spec.inputs)
477
+ out[n] = k.value(n);
478
+ for (const [n] of this.spec.state)
479
+ out[n] = v(n);
480
+ return out;
481
+ }
482
+ get loop() {
483
+ const s = this.spec, env = this.env(), pEnv = Object.fromEntries(s.params.map(([n, k]) => [n, k.value(n)]));
484
+ if (!s.while || !s.body)
485
+ throw new Error(`${s.name}: a loop needs while and body`);
486
+ const pre = [...s.params.flatMap(([n, k]) => k.assume(n)).map(c => c.c), ...s.pre.map(f => f(pEnv).c)];
487
+ const assume = [...s.inputs.flatMap(([n, k]) => k.assume(n)).map(c => c.c), ...s.assume.map(f => f(env).c)];
488
+ return {
489
+ name: s.name, params: s.params.flatMap(([n, k]) => k.vars(n)), inputs: s.inputs.flatMap(([n, k]) => k.vars(n)),
490
+ state: Object.fromEntries(s.state.map(([n, init]) => [n, typeof init === 'number' ? termOf(init) : termOf(init(pEnv))])),
491
+ while: s.while(env).c,
492
+ body: Object.fromEntries(Object.entries(s.body(env)).filter(([, t]) => t !== undefined).map(([n, t]) => [n, termOf(t)])),
493
+ ...(pre.length ? { pre: pre.length === 1 ? pre[0] : { op: 'and', xs: pre } } : {}),
494
+ ...(assume.length ? { assume: assume.length === 1 ? assume[0] : { op: 'and', xs: assume } } : {}),
495
+ };
496
+ }
497
+ get rankTerms() {
498
+ if (!this.spec.rank)
499
+ throw new Error(`${this.spec.name}: no rank`);
500
+ const r = this.spec.rank(this.env(false));
501
+ return (Array.isArray(r) ? r : [r]).map(termOf);
502
+ }
503
+ randomParams(rand, bound) { return Object.assign({}, ...this.spec.params.map(([n, k]) => k.random(n, rand, bound))); }
504
+ randomInput(rand, bound) { return Object.assign({}, ...this.spec.inputs.map(([n, k]) => k.random(n, rand, bound))); }
505
+ }
506
+ export const loop = (name) => new LoopRule({ name, params: [], inputs: [], state: [], pre: [], assume: [] });
507
+ const goalsVerdict = (goals) => {
508
+ const trees = {};
509
+ for (const g of goals) {
510
+ const out = grow(g);
511
+ if (!('tree' in out))
512
+ return { proven: false, checked: false, goal: g.name, stuck: out.stuck, ...(out.witness ? { witness: out.witness } : {}) };
513
+ trees[g.name] = out.tree;
514
+ }
515
+ return { proven: true, checked: goals.every(g => check(g, trees[g.name])), trees };
516
+ };
517
+ export function proveRule(r, laws) {
518
+ const out = proveLaws(r.machine, laws);
519
+ return { proven: out.proven, checked: out.checked, ...(out.failed ? { goal: out.failed.goal, stuck: out.failed.stuck, ...(out.failed.witness ? { witness: out.failed.witness } : {}) } : {}) };
520
+ }
521
+ export function proveSteps(r, laws, stepLaws) {
522
+ const out = proveStepLaws(r.machine, laws, stepLaws);
523
+ return { proven: out.proven, checked: out.checked, ...(out.failed ? { goal: out.failed.goal, stuck: out.failed.stuck, ...(out.failed.witness ? { witness: out.failed.witness } : {}) } : {}) };
524
+ }
525
+ export const proveDecision = (d) => goalsVerdict([d.goal]);
526
+ export const proveLoop = (l, laws) => goalsVerdict(loopObligations(l.loop, laws, l.rankTerms));
@@ -0,0 +1,145 @@
1
+ import { type Constraint, type Verdict } from './solver.js';
2
+ export type E = {
3
+ op: 'var';
4
+ name: string;
5
+ } | {
6
+ op: 'const';
7
+ value: number;
8
+ } | {
9
+ op: 'add' | 'sub' | 'min' | 'max';
10
+ a: E;
11
+ b: E;
12
+ } | {
13
+ op: 'mul';
14
+ k: number;
15
+ a: E;
16
+ } | {
17
+ op: 'prod';
18
+ a: E;
19
+ b: E;
20
+ } | {
21
+ op: 'if';
22
+ c: C;
23
+ then: E;
24
+ else: E;
25
+ } | {
26
+ op: 'get';
27
+ map: string;
28
+ key: E;
29
+ };
30
+ export type C = {
31
+ op: '<' | '<=' | '=';
32
+ a: E;
33
+ b: E;
34
+ } | {
35
+ op: 'and';
36
+ xs: C[];
37
+ } | {
38
+ op: 'or';
39
+ xs: C[];
40
+ } | {
41
+ op: 'not';
42
+ x: C;
43
+ } | {
44
+ op: 'flag';
45
+ name: string;
46
+ };
47
+ export type Machine = {
48
+ name: string;
49
+ params: string[];
50
+ inputs: string[];
51
+ state: Record<string, E>;
52
+ maps?: Record<string, number>;
53
+ step: {
54
+ flags?: Record<string, C>;
55
+ state?: Record<string, E>;
56
+ maps?: Record<string, {
57
+ key: E;
58
+ value: E;
59
+ }>;
60
+ };
61
+ pre?: C;
62
+ assume?: C;
63
+ history?: History;
64
+ };
65
+ export type History = {
66
+ keep: C;
67
+ record: string[];
68
+ sums: Record<string, E>;
69
+ queries: Record<string, E>;
70
+ relations?: {
71
+ when: C;
72
+ holds: C;
73
+ }[];
74
+ prune?: {
75
+ order: string;
76
+ after: E;
77
+ };
78
+ };
79
+ export type Law = {
80
+ name: string;
81
+ holds: C;
82
+ forall?: string;
83
+ };
84
+ export { unreachable } from '../kernel/kernel.js';
85
+ export declare const x: (name: string) => E;
86
+ export declare const n: (value: number) => E;
87
+ export declare const plus: (a: E, b: E) => E;
88
+ export declare const minus: (a: E, b: E) => E;
89
+ export declare const times: (factor: number, a: E) => E;
90
+ export declare const min: (a: E, b: E) => E;
91
+ export declare const max: (a: E, b: E) => E;
92
+ export declare const prod: (a: E, b: E) => E;
93
+ export declare const ite: (c: C, then: E, otherwise: E) => E;
94
+ export declare const get: (map: string, key: E) => E;
95
+ export declare const less: (a: E, b: E) => C;
96
+ export declare const atMost: (a: E, b: E) => C;
97
+ export declare const same: (a: E, b: E) => C;
98
+ export declare const all: (...xs: C[]) => C;
99
+ export declare const any: (...xs: C[]) => C;
100
+ export declare const not: (c: C) => C;
101
+ export declare const bit: (name: string) => C;
102
+ export type State = {
103
+ scalars: Record<string, number>;
104
+ maps: Record<string, Map<number, number>>;
105
+ history?: Env[];
106
+ last?: number;
107
+ };
108
+ type Env = Record<string, number>;
109
+ export declare const naturalIn: (v: unknown, what: string) => number;
110
+ export declare function evalE(e: E, env: Env, st: State, m: Machine): number;
111
+ export declare function evalC(c: C, env: Env, st: State, m: Machine): boolean;
112
+ export declare function interpret(m: Machine): {
113
+ init: (params: Env) => State;
114
+ queries: (params: Env, st: State, input: Env) => Env;
115
+ step: (params: Env, st: State, input: Env) => State;
116
+ };
117
+ export declare function compileJs(m: Machine): string;
118
+ export declare function compileFunction<F>(params: string[], body: string): F;
119
+ export type Compiled = {
120
+ init: (params: object) => State & {
121
+ head?: number;
122
+ };
123
+ fork: (st: State) => State;
124
+ step: (params: object, st: State, input: object) => State & {
125
+ head?: number;
126
+ };
127
+ };
128
+ export declare const loadJs: (m: Machine) => Compiled;
129
+ export type Obligation = {
130
+ phase: 'start' | 'step';
131
+ law: string;
132
+ constraints: Constraint[];
133
+ verdict: Verdict;
134
+ };
135
+ export type Proof = {
136
+ law: Law;
137
+ proven: boolean;
138
+ obligations: Obligation[];
139
+ };
140
+ export declare function prove(m: Machine, goal: Law, invariants?: Law[]): Proof;
141
+ export declare function search(m: Machine, law: Law, depth?: number, bound?: number): {
142
+ params: Env;
143
+ trace: Env[];
144
+ } | undefined;
145
+ export declare function conditionCases(c: C, truth: boolean): Constraint[][];