gdp-wired 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +128 -92
- package/dist/core/compose.d.ts +15 -1
- package/dist/core/compose.js +132 -8
- package/dist/core/dsl.d.ts +272 -17
- package/dist/core/dsl.js +1098 -60
- package/dist/core/machine.d.ts +34 -6
- package/dist/core/machine.js +194 -22
- package/dist/core/watch.d.ts +14 -0
- package/dist/core/watch.js +78 -0
- package/dist/kernel/kernel.d.ts +30 -2
- package/dist/kernel/kernel.js +306 -48
- package/dist/kernel/prove.d.ts +36 -9
- package/dist/kernel/prove.js +317 -28
- package/dist/lean-kernel/emit.d.ts +124 -0
- package/dist/lean-kernel/emit.js +875 -0
- package/dist/lean-kernel/lean-terms.d.ts +5 -0
- package/dist/lean-kernel/lean-terms.js +44 -0
- package/dist/lean-kernel/library.d.ts +1 -0
- package/dist/lean-kernel/library.js +13 -0
- package/package.json +11 -4
package/dist/core/dsl.js
CHANGED
|
@@ -15,54 +15,83 @@
|
|
|
15
15
|
//
|
|
16
16
|
// A step returns its updates as one object: every right-hand side reads the state before the step, as the kernel and
|
|
17
17
|
// both runtimes do.
|
|
18
|
-
import { interpret, loadJs } from "./machine.js";
|
|
18
|
+
import { carriedView, compileFunction, evalC, interpret, loadJs, numbersOf } from "./machine.js";
|
|
19
|
+
import { allowed, keysOf, requestEnv, watch } from "./watch.js";
|
|
19
20
|
import { check, loopObligations } from "../kernel/kernel.js";
|
|
20
|
-
import { grow, proveLaws, proveStepLaws } from "../kernel/prove.js";
|
|
21
|
+
import { factsFor, grow, proveLaws, proveLiveness, proveStepLaws } from "../kernel/prove.js";
|
|
21
22
|
// --- terms and conditions -------------------------------------------------------------------------------------------
|
|
22
23
|
const naturalConst = (v) => {
|
|
23
24
|
if (!Number.isSafeInteger(v) || v < 0)
|
|
24
25
|
throw new RangeError(`${v} is not a natural`);
|
|
25
26
|
return v;
|
|
26
27
|
};
|
|
27
|
-
export const termOf = (t) =>
|
|
28
|
+
export const termOf = (t) => {
|
|
29
|
+
if (typeof t === 'number')
|
|
30
|
+
return { op: 'const', value: naturalConst(t) };
|
|
31
|
+
if (!(t instanceof Term))
|
|
32
|
+
throw new TypeError('a natural is expected here; an integer or a decimal compares from its own side (x.atMost(n))');
|
|
33
|
+
return t.e;
|
|
34
|
+
};
|
|
28
35
|
// A natural-valued term. `minus` is truncated (a ∸ b), `times` multiplies by a constant, `mul` by another term (a
|
|
29
36
|
// product: the kernel proves with it, the GPU searches don't take it).
|
|
37
|
+
//
|
|
38
|
+
// Each term and condition may also carry its value (`val`, `ok`), computed beside its expression by plain JavaScript
|
|
39
|
+
// when every value it reads is known: what `crossCheck` compares with the expression the interpreter evaluates, so a
|
|
40
|
+
// builder that writes a different expression than it means is caught on concrete runs.
|
|
30
41
|
export class Term {
|
|
31
42
|
e;
|
|
32
|
-
|
|
43
|
+
val;
|
|
44
|
+
constructor(e, val) {
|
|
33
45
|
this.e = e;
|
|
46
|
+
this.val = val;
|
|
34
47
|
}
|
|
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) }); }
|
|
48
|
+
plus(b) { return new Term({ op: 'add', a: this.e, b: termOf(b) }, both(this.val, valOf(b), (x, y) => x + y)); }
|
|
49
|
+
minus(b) { return new Term({ op: 'sub', a: this.e, b: termOf(b) }, both(this.val, valOf(b), (x, y) => Math.max(0, x - y))); }
|
|
50
|
+
times(k) { return new Term({ op: 'mul', k: naturalConst(k), a: this.e }, both(this.val, k, (x, y) => x * y)); }
|
|
51
|
+
mul(b) { return new Term({ op: 'prod', a: this.e, b: termOf(b) }, both(this.val, valOf(b), (x, y) => x * y)); }
|
|
52
|
+
lt(b) { return new Cond({ op: '<', a: this.e, b: termOf(b) }, both(this.val, valOf(b), (x, y) => x < y)); }
|
|
53
|
+
atMost(b) { return new Cond({ op: '<=', a: this.e, b: termOf(b) }, both(this.val, valOf(b), (x, y) => x <= y)); }
|
|
54
|
+
gt(b) { return new Cond({ op: '<', a: termOf(b), b: this.e }, both(this.val, valOf(b), (x, y) => x > y)); }
|
|
55
|
+
atLeast(b) { return new Cond({ op: '<=', a: termOf(b), b: this.e }, both(this.val, valOf(b), (x, y) => x >= y)); }
|
|
56
|
+
eq(b) { return new Cond({ op: '=', a: this.e, b: termOf(b) }, both(this.val, valOf(b), (x, y) => x === y)); }
|
|
44
57
|
ne(b) { return this.eq(b).not(); }
|
|
45
58
|
}
|
|
59
|
+
const valOf = (t) => (typeof t === 'number' ? t : t instanceof Term ? t.val : undefined);
|
|
60
|
+
const both = (a, b, f) => (a === undefined || b === undefined ? undefined : f(a, b));
|
|
61
|
+
// Every value known, or undefined.
|
|
62
|
+
const known = (xs) => (xs.every(x => x !== undefined) ? xs : undefined);
|
|
46
63
|
export class Cond {
|
|
47
64
|
c;
|
|
48
|
-
|
|
65
|
+
ok;
|
|
66
|
+
constructor(c, ok) {
|
|
49
67
|
this.c = c;
|
|
68
|
+
this.ok = ok;
|
|
50
69
|
}
|
|
51
70
|
and(...xs) { return all(this, ...xs); }
|
|
52
71
|
or(...xs) { return any(this, ...xs); }
|
|
53
|
-
not() { return new Cond({ op: 'not', x: this.c }); }
|
|
72
|
+
not() { return new Cond({ op: 'not', x: this.c }, this.ok === undefined ? undefined : !this.ok); }
|
|
54
73
|
implies(b) { return any(this.not(), b); }
|
|
55
74
|
iff(b) { return all(this.implies(b), b.implies(this)); }
|
|
56
75
|
}
|
|
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) }));
|
|
76
|
+
export const lit = (v) => new Term(termOf(v), v);
|
|
77
|
+
export const yes = new Cond({ op: '<=', a: { op: 'const', value: 0 }, b: { op: 'const', value: 0 } }, true);
|
|
78
|
+
export const no = new Cond({ op: '<', a: { op: 'const', value: 0 }, b: { op: 'const', value: 0 } }, false);
|
|
79
|
+
export const all = (...cs) => (cs.length === 0 ? yes : cs.length === 1 ? cs[0] : new Cond({ op: 'and', xs: cs.map(c => c.c) }, known(cs.map(c => c.ok))?.every(x => x)));
|
|
80
|
+
export const any = (...cs) => (cs.length === 0 ? no : cs.length === 1 ? cs[0] : new Cond({ op: 'or', xs: cs.map(c => c.c) }, known(cs.map(c => c.ok))?.some(x => x)));
|
|
62
81
|
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
|
|
82
|
+
export const min = (a, b) => new Term({ op: 'min', a: termOf(a), b: termOf(b) }, both(valOf(a), valOf(b), Math.min));
|
|
83
|
+
export const max = (a, b) => new Term({ op: 'max', a: termOf(a), b: termOf(b) }, both(valOf(a), valOf(b), Math.max));
|
|
84
|
+
export function when(c, then, otherwise) {
|
|
85
|
+
if (then instanceof Decimal || otherwise instanceof Decimal) {
|
|
86
|
+
const places = (then instanceof Decimal ? then : otherwise).places, d = new Decimal(Int.of(0), places);
|
|
87
|
+
return new Decimal(when(c, d.lift(then), d.lift(otherwise)), places);
|
|
88
|
+
}
|
|
89
|
+
if (then instanceof Int || otherwise instanceof Int || (typeof then === 'number' && then < 0) || (typeof otherwise === 'number' && otherwise < 0)) {
|
|
90
|
+
const a = Int.of(then), b = Int.of(otherwise);
|
|
91
|
+
return new Int(when(c, a.pos, b.pos), when(c, a.neg, b.neg));
|
|
92
|
+
}
|
|
93
|
+
return new Term({ op: 'if', c: c.c, then: termOf(then), else: termOf(otherwise) }, c.ok === undefined ? undefined : valOf(c.ok ? then : otherwise));
|
|
94
|
+
}
|
|
66
95
|
// A condition by cases: `then` where c holds, `otherwise` elsewhere.
|
|
67
96
|
export const choose = (c, then, otherwise) => any(all(c, then), all(c.not(), otherwise));
|
|
68
97
|
// 1 where c holds, else 0: what a sum over a history adds per record.
|
|
@@ -71,8 +100,127 @@ export const sum = (first, ...rest) => rest.reduce((acc, t) => acc.plus(t), type
|
|
|
71
100
|
// A kind names the variables a value is encoded in, what is assumed of them, the value the builders hand out, and how
|
|
72
101
|
// a runtime value is encoded and drawn at random (the examples' harness draws inputs this way).
|
|
73
102
|
export class Kind {
|
|
103
|
+
// A runtime value of the kind (what encode takes), for comparing a rule with the code it describes (compare).
|
|
104
|
+
sample(_rand, _bound) { throw new Error('this kind draws no runtime values'); }
|
|
105
|
+
}
|
|
106
|
+
const v = (name) => new Term({ op: 'var', name }, scope?.vars[name]);
|
|
107
|
+
let scope;
|
|
108
|
+
const inScope = (s, f) => { const outer = scope; scope = s; try {
|
|
109
|
+
return f();
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
scope = outer;
|
|
113
|
+
} };
|
|
114
|
+
const keyTerm = (k) => new Term({ op: 'const', value: k }, k);
|
|
115
|
+
// Keys enough to decide a condition at every key as the interpreter does: these, the key past every map's cells, and
|
|
116
|
+
// every key up to the values in play (where a key compared with them changes).
|
|
117
|
+
const exactKeys = (keys, values, ...states) => {
|
|
118
|
+
let past = 0;
|
|
119
|
+
for (const st of states)
|
|
120
|
+
for (const cells of Object.values(st.maps))
|
|
121
|
+
for (const k of cells.keys())
|
|
122
|
+
past = Math.max(past, k + 1);
|
|
123
|
+
const top = Math.min(64, Math.max(0, ...Object.values(values).filter(x => x <= 64)) + 2);
|
|
124
|
+
return [...new Set([...keys, past, ...Array.from({ length: top + 1 }, (_, i) => i)])].sort((a, b) => a - b);
|
|
125
|
+
};
|
|
126
|
+
const everyOk = (ks, f) => {
|
|
127
|
+
if (!ks)
|
|
128
|
+
return undefined;
|
|
129
|
+
let out = true;
|
|
130
|
+
for (const k of ks) {
|
|
131
|
+
const o = f(k);
|
|
132
|
+
if (o === undefined)
|
|
133
|
+
return undefined;
|
|
134
|
+
if (!o)
|
|
135
|
+
out = false;
|
|
136
|
+
}
|
|
137
|
+
return out;
|
|
138
|
+
};
|
|
139
|
+
const isZero = (t) => t.e.op === 'const' && t.e.value === 0;
|
|
140
|
+
const add = (a, b) => (isZero(b) ? a : isZero(a) ? b : a.plus(b));
|
|
141
|
+
export class Int {
|
|
142
|
+
pos;
|
|
143
|
+
neg;
|
|
144
|
+
constructor(pos, neg) {
|
|
145
|
+
this.pos = pos;
|
|
146
|
+
this.neg = neg;
|
|
147
|
+
}
|
|
148
|
+
static of(x) {
|
|
149
|
+
if (x instanceof Int)
|
|
150
|
+
return x;
|
|
151
|
+
if (typeof x !== 'number')
|
|
152
|
+
return new Int(x, lit(0));
|
|
153
|
+
if (!Number.isSafeInteger(x))
|
|
154
|
+
throw new RangeError(`${x} is not an integer`);
|
|
155
|
+
return x >= 0 ? new Int(lit(x), lit(0)) : new Int(lit(0), lit(-x));
|
|
156
|
+
}
|
|
157
|
+
plus(b) { const o = Int.of(b); return new Int(add(this.pos, o.pos), add(this.neg, o.neg)); }
|
|
158
|
+
minus(b) { const o = Int.of(b); return new Int(add(this.pos, o.neg), add(this.neg, o.pos)); }
|
|
159
|
+
negate() { return new Int(this.neg, this.pos); }
|
|
160
|
+
times(k) {
|
|
161
|
+
if (!Number.isSafeInteger(k))
|
|
162
|
+
throw new RangeError(`${k} is not an integer`);
|
|
163
|
+
return k >= 0 ? new Int(this.pos.times(k), this.neg.times(k)) : new Int(this.neg.times(-k), this.pos.times(-k));
|
|
164
|
+
}
|
|
165
|
+
lt(b) { const o = Int.of(b); return add(this.pos, o.neg).lt(add(o.pos, this.neg)); }
|
|
166
|
+
atMost(b) { const o = Int.of(b); return add(this.pos, o.neg).atMost(add(o.pos, this.neg)); }
|
|
167
|
+
gt(b) { return Int.of(b).lt(this); }
|
|
168
|
+
atLeast(b) { return Int.of(b).atMost(this); }
|
|
169
|
+
eq(b) { const o = Int.of(b); return add(this.pos, o.neg).eq(add(o.pos, this.neg)); }
|
|
170
|
+
ne(b) { return this.eq(b).not(); }
|
|
171
|
+
min(b) { return when(this.atMost(b), this, b); }
|
|
172
|
+
max(b) { return when(this.atLeast(b), this, b); }
|
|
173
|
+
get isNegative() { return this.neg.gt(this.pos); }
|
|
174
|
+
// |value|, and the value where it is not negative (0 below).
|
|
175
|
+
get abs() { return this.pos.minus(this.neg).plus(this.neg.minus(this.pos)); }
|
|
176
|
+
get clamped() { return this.pos.minus(this.neg); }
|
|
177
|
+
}
|
|
178
|
+
// The same value with at most one part nonzero (what a state variable holds).
|
|
179
|
+
const normal = (i) => (isZero(i.neg) || isZero(i.pos) ? i : new Int(i.pos.minus(i.neg), i.neg.minus(i.pos)));
|
|
180
|
+
const termValue = (t) => (t.e.op === 'const' ? t.e.value : 0);
|
|
181
|
+
const scaledOf = (x, places, what = 'a decimal') => {
|
|
182
|
+
const text = typeof x === 'number' ? x.toFixed(places + 3) : x.trim();
|
|
183
|
+
const m = /^(-?)(\d+)(?:\.(\d+))?$/.exec(text);
|
|
184
|
+
if (!m)
|
|
185
|
+
throw new RangeError(`${String(x)} is not ${what}`);
|
|
186
|
+
const frac = (m[3] ?? '').replace(/0+$/, '');
|
|
187
|
+
if (frac.length > places)
|
|
188
|
+
throw new RangeError(`${String(x)} needs more than ${places} decimal places`);
|
|
189
|
+
const n = Number(m[2]) * 10 ** places + Number(frac.padEnd(places, '0') || '0');
|
|
190
|
+
if (!Number.isSafeInteger(n))
|
|
191
|
+
throw new RangeError(`${String(x)} is too large`);
|
|
192
|
+
return m[1] && n !== 0 ? -n : n;
|
|
193
|
+
};
|
|
194
|
+
export class Decimal {
|
|
195
|
+
int;
|
|
196
|
+
places;
|
|
197
|
+
constructor(int, places) {
|
|
198
|
+
this.int = int;
|
|
199
|
+
this.places = places;
|
|
200
|
+
}
|
|
201
|
+
lift(b) {
|
|
202
|
+
if (b instanceof Decimal) {
|
|
203
|
+
if (b.places !== this.places)
|
|
204
|
+
throw new Error(`decimals with ${this.places} and ${b.places} places don't mix`);
|
|
205
|
+
return b.int;
|
|
206
|
+
}
|
|
207
|
+
return typeof b === 'number' ? Int.of(scaledOf(b, this.places)) : Int.of(b).times(10 ** this.places);
|
|
208
|
+
}
|
|
209
|
+
with(i) { return new Decimal(i, this.places); }
|
|
210
|
+
plus(b) { return this.with(this.int.plus(this.lift(b))); }
|
|
211
|
+
minus(b) { return this.with(this.int.minus(this.lift(b))); }
|
|
212
|
+
negate() { return this.with(this.int.negate()); }
|
|
213
|
+
times(k) { return this.with(this.int.times(k)); }
|
|
214
|
+
lt(b) { return this.int.lt(this.lift(b)); }
|
|
215
|
+
atMost(b) { return this.int.atMost(this.lift(b)); }
|
|
216
|
+
gt(b) { return this.int.gt(this.lift(b)); }
|
|
217
|
+
atLeast(b) { return this.int.atLeast(this.lift(b)); }
|
|
218
|
+
eq(b) { return this.int.eq(this.lift(b)); }
|
|
219
|
+
ne(b) { return this.int.ne(this.lift(b)); }
|
|
220
|
+
min(b) { return this.with(this.int.min(this.lift(b))); }
|
|
221
|
+
max(b) { return this.with(this.int.max(this.lift(b))); }
|
|
222
|
+
get isNegative() { return this.int.isNegative; }
|
|
74
223
|
}
|
|
75
|
-
const v = (name) => new Term({ op: 'var', name });
|
|
76
224
|
const asNat = (name, raw) => {
|
|
77
225
|
if (typeof raw !== 'number' || !Number.isSafeInteger(raw) || raw < 0)
|
|
78
226
|
throw new RangeError(`${name} must be a natural, not ${String(raw)}`);
|
|
@@ -94,6 +242,7 @@ class NatKind extends Kind {
|
|
|
94
242
|
return { [name]: n };
|
|
95
243
|
}
|
|
96
244
|
random(name, rand, bound) { return { [name]: rand((this.maxValue ?? bound) + 1) }; }
|
|
245
|
+
sample(rand, bound) { return this.maxValue !== undefined && rand(8) === 0 ? this.maxValue : rand((this.maxValue ?? bound) + 1); }
|
|
97
246
|
}
|
|
98
247
|
export const nat = new NatKind();
|
|
99
248
|
// A natural known to be at most `maxValue` (a percentage, a floor number, a weekday).
|
|
@@ -102,7 +251,7 @@ export const upTo = (maxValue) => new NatKind(naturalConst(maxValue));
|
|
|
102
251
|
export class Bool extends Cond {
|
|
103
252
|
n;
|
|
104
253
|
constructor(n) {
|
|
105
|
-
super({ op: '=', a: n.e, b: { op: 'const', value: 1 } });
|
|
254
|
+
super({ op: '=', a: n.e, b: { op: 'const', value: 1 } }, n.val === undefined ? undefined : n.val === 1);
|
|
106
255
|
this.n = n;
|
|
107
256
|
}
|
|
108
257
|
}
|
|
@@ -118,6 +267,7 @@ class BoolKind extends Kind {
|
|
|
118
267
|
throw new RangeError(`${name} must be a boolean, not ${String(raw)}`);
|
|
119
268
|
}
|
|
120
269
|
random(name, rand) { return { [name]: rand(2) }; }
|
|
270
|
+
sample(rand) { return rand(2) === 1; }
|
|
121
271
|
}
|
|
122
272
|
export const bool = new BoolKind();
|
|
123
273
|
// One of a few named values, encoded as its index. `match` takes every case (TypeScript checks none is missing).
|
|
@@ -162,6 +312,7 @@ class OneOfKind extends Kind {
|
|
|
162
312
|
return { [name]: i };
|
|
163
313
|
}
|
|
164
314
|
random(name, rand) { return { [name]: rand(this.values.length) }; }
|
|
315
|
+
sample(rand) { return this.values[rand(this.values.length)]; }
|
|
165
316
|
}
|
|
166
317
|
export const oneOf = (...values) => new OneOfKind(values);
|
|
167
318
|
// A value that may be missing, encoded as a presence bit and the value. The value can only be read in `some`, so no
|
|
@@ -203,26 +354,459 @@ class MaybeKind extends Kind {
|
|
|
203
354
|
random(name, rand, bound) {
|
|
204
355
|
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
356
|
}
|
|
357
|
+
sample(rand, bound) { return rand(4) === 0 ? null : this.of.sample(rand, bound); }
|
|
206
358
|
}
|
|
207
359
|
export const maybe = (of) => new MaybeKind(of);
|
|
360
|
+
// A string, encoded as its interned name, its length and one bit per pattern the kind declares (`like`, Cedar's glob:
|
|
361
|
+
// `*` any run of characters, `\*` a star): all computed from the string itself when the input is encoded, so the bits
|
|
362
|
+
// are the string's, not an answer someone supplies. Names come from one table per process, the empty string 0; a law
|
|
363
|
+
// compares strings by name. Equal strings have equal lengths and pattern bits, and `eq` says so, with a literal or with
|
|
364
|
+
// another string, so a law may reason from either. (Names are this process's: state that keeps them across processes
|
|
365
|
+
// keeps `interned()` with them, and `intern.load` restores it before any rule is built.)
|
|
366
|
+
const internTable = new Map([['', 0]]);
|
|
367
|
+
// The strings rules and laws name as literals (what random draws favor).
|
|
368
|
+
const literals = new Set(['']);
|
|
369
|
+
const literal = (text) => (literals.add(text), intern(text));
|
|
370
|
+
export const intern = Object.assign((text) => {
|
|
371
|
+
let id = internTable.get(text);
|
|
372
|
+
if (id === undefined)
|
|
373
|
+
internTable.set(text, (id = internTable.size));
|
|
374
|
+
return id;
|
|
375
|
+
}, { load(entries) { for (const [text, id] of entries) {
|
|
376
|
+
if (internTable.has(text) && internTable.get(text) !== id)
|
|
377
|
+
throw new Error(`"${text}" is interned as ${internTable.get(text)} already`);
|
|
378
|
+
internTable.set(text, id);
|
|
379
|
+
} } });
|
|
380
|
+
export const interned = () => [...internTable];
|
|
381
|
+
export const globMatch = (pattern, text) => {
|
|
382
|
+
const parts = [[]];
|
|
383
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
384
|
+
if (pattern[i] === '\\' && pattern[i + 1] === '*') {
|
|
385
|
+
parts.at(-1).push('*');
|
|
386
|
+
i++;
|
|
387
|
+
}
|
|
388
|
+
else if (pattern[i] === '*')
|
|
389
|
+
parts.push([]);
|
|
390
|
+
else
|
|
391
|
+
parts.at(-1).push(pattern[i]);
|
|
392
|
+
}
|
|
393
|
+
const lits = parts.map(p => p.join(''));
|
|
394
|
+
if (lits.length === 1)
|
|
395
|
+
return text === lits[0];
|
|
396
|
+
if (!text.startsWith(lits[0]) || !text.endsWith(lits.at(-1)) || text.length < lits[0].length + lits.at(-1).length)
|
|
397
|
+
return false;
|
|
398
|
+
let at = lits[0].length;
|
|
399
|
+
const end = text.length - lits.at(-1).length;
|
|
400
|
+
for (const lit of lits.slice(1, -1)) {
|
|
401
|
+
const i = text.indexOf(lit, at);
|
|
402
|
+
if (i < 0 || i + lit.length > end)
|
|
403
|
+
return false;
|
|
404
|
+
at = i + lit.length;
|
|
405
|
+
}
|
|
406
|
+
return true;
|
|
407
|
+
};
|
|
408
|
+
export class Str {
|
|
409
|
+
id;
|
|
410
|
+
length;
|
|
411
|
+
bits;
|
|
412
|
+
constructor(id, length, bits) {
|
|
413
|
+
this.id = id;
|
|
414
|
+
this.length = length;
|
|
415
|
+
this.bits = bits;
|
|
416
|
+
}
|
|
417
|
+
// The string matches `pattern`, one the kind declared.
|
|
418
|
+
like(pattern) {
|
|
419
|
+
const b = this.bits.get(pattern);
|
|
420
|
+
if (!b)
|
|
421
|
+
throw new Error(`like("${pattern}"): declare the pattern on the kind (str({like: [...]})); declared: ${[...this.bits.keys()].join(', ') || 'none'}`);
|
|
422
|
+
return b.eq(1);
|
|
423
|
+
}
|
|
424
|
+
get isEmpty() { return this.id.eq(0); }
|
|
425
|
+
eq(b) {
|
|
426
|
+
if (typeof b === 'string')
|
|
427
|
+
return all(this.id.eq(literal(b)), this.length.eq(b.length), ...[...this.bits].map(([p, bit]) => bit.eq(globMatch(p, b) ? 1 : 0)));
|
|
428
|
+
return all(this.id.eq(b.id), this.length.eq(b.length), ...[...this.bits].filter(([p]) => b.bits.has(p)).map(([p, bit]) => bit.eq(b.bits.get(p))));
|
|
429
|
+
}
|
|
430
|
+
ne(b) { return typeof b === 'string' ? this.id.ne(literal(b)) : this.id.ne(b.id); }
|
|
431
|
+
}
|
|
432
|
+
class StrKind extends Kind {
|
|
433
|
+
patterns;
|
|
434
|
+
maxLength;
|
|
435
|
+
constructor(patterns, maxLength) {
|
|
436
|
+
super();
|
|
437
|
+
this.patterns = patterns;
|
|
438
|
+
this.maxLength = maxLength;
|
|
439
|
+
}
|
|
440
|
+
bit(name, i) { return `${name}$like${i}`; }
|
|
441
|
+
vars(name) { return [name, `${name}$len`, ...this.patterns.map((_, i) => this.bit(name, i))]; }
|
|
442
|
+
assume(name) {
|
|
443
|
+
return [v(name).eq(0).iff(v(`${name}$len`).eq(0)), ...(this.maxLength === undefined ? [] : [v(`${name}$len`).atMost(this.maxLength)]),
|
|
444
|
+
...this.patterns.map((p, i) => (p.replaceAll('\\*', '').includes('*') ? v(this.bit(name, i)).atMost(1) : v(this.bit(name, i)).eq(1).iff(v(name).eq(literal(p.replaceAll('\\*', '*'))))))];
|
|
445
|
+
}
|
|
446
|
+
value(name, prefix = '') { return new Str(v(prefix + name), v(`${prefix}${name}$len`), new Map(this.patterns.map((p, i) => [p, v(prefix + this.bit(name, i))]))); }
|
|
447
|
+
encode(name, raw) {
|
|
448
|
+
if (typeof raw !== 'string')
|
|
449
|
+
throw new RangeError(`${name} must be a string, not ${String(raw)}`);
|
|
450
|
+
if (this.maxLength !== undefined && raw.length > this.maxLength)
|
|
451
|
+
throw new RangeError(`${name} is at most ${this.maxLength} characters`);
|
|
452
|
+
return { [name]: intern(raw), [`${name}$len`]: raw.length, ...Object.fromEntries(this.patterns.map((p, i) => [this.bit(name, i), globMatch(p, raw) ? 1 : 0])) };
|
|
453
|
+
}
|
|
454
|
+
// Strings drawn so each pattern matches some of them (the patterns with their stars filled), every string named
|
|
455
|
+
// as a literal (what the rule and its laws compare with), and short strings over their characters.
|
|
456
|
+
random(name, rand, bound) { return this.encode(name, this.sample(rand, bound)); }
|
|
457
|
+
sample(rand, bound) {
|
|
458
|
+
const chars = [...new Set(['a', 'b', ...Array.from(this.patterns.join('').replaceAll('*', ''))])];
|
|
459
|
+
const pool = [...this.patterns.flatMap(p => [p.replaceAll('*', ''), p.replaceAll('*', chars[rand(chars.length)])]), ...literals];
|
|
460
|
+
const text = rand(2) === 0 ? pool[rand(pool.length)] : Array.from({ length: rand(Math.min(bound, 4) + 1) }, () => chars[rand(chars.length)]).join('');
|
|
461
|
+
return this.maxLength === undefined ? text : text.slice(0, this.maxLength);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
export const str = (opts = {}) => new StrKind(opts.like ?? [], opts.maxLength);
|
|
465
|
+
// A signed integer, encoded as two naturals (the value and `<name>$neg`): at most one nonzero when encoded.
|
|
466
|
+
class IntKind extends Kind {
|
|
467
|
+
vars(name) { return [name, `${name}$neg`]; }
|
|
468
|
+
assume() { return []; }
|
|
469
|
+
value(name, prefix = '') { return new Int(v(prefix + name), v(`${prefix}${name}$neg`)); }
|
|
470
|
+
encode(name, raw) {
|
|
471
|
+
if (typeof raw !== 'number' || !Number.isSafeInteger(raw))
|
|
472
|
+
throw new RangeError(`${name} must be an integer, not ${String(raw)}`);
|
|
473
|
+
return { [name]: Math.max(raw, 0), [`${name}$neg`]: Math.max(-raw, 0) };
|
|
474
|
+
}
|
|
475
|
+
random(name, rand, bound) { const x = rand(2 * bound + 1) - bound; return { [name]: Math.max(x, 0), [`${name}$neg`]: Math.max(-x, 0) }; }
|
|
476
|
+
sample(rand, bound) { return rand(2 * bound + 1) - bound; }
|
|
477
|
+
}
|
|
478
|
+
export const int = new IntKind();
|
|
479
|
+
// A decimal with `places` places, encoded as its integer count of 10^-places: given as a number or a string ("12.50").
|
|
480
|
+
class DecimalKind extends Kind {
|
|
481
|
+
places;
|
|
482
|
+
constructor(places) {
|
|
483
|
+
super();
|
|
484
|
+
this.places = places;
|
|
485
|
+
}
|
|
486
|
+
vars(name) { return [name, `${name}$neg`]; }
|
|
487
|
+
assume() { return []; }
|
|
488
|
+
value(name, prefix = '') { return new Decimal(new Int(v(prefix + name), v(`${prefix}${name}$neg`)), this.places); }
|
|
489
|
+
encode(name, raw) {
|
|
490
|
+
if (typeof raw !== 'number' && typeof raw !== 'string')
|
|
491
|
+
throw new RangeError(`${name} must be a decimal, not ${String(raw)}`);
|
|
492
|
+
const x = scaledOf(raw, this.places, `a decimal (${name})`);
|
|
493
|
+
return { [name]: Math.max(x, 0), [`${name}$neg`]: Math.max(-x, 0) };
|
|
494
|
+
}
|
|
495
|
+
random(name, rand, bound) { return new IntKind().random(name, rand, bound); }
|
|
496
|
+
sample(rand, bound) { return (rand(2 * bound * 10 ** this.places + 1) - bound * 10 ** this.places) / 10 ** this.places; }
|
|
497
|
+
}
|
|
498
|
+
export const decimal = (places) => {
|
|
499
|
+
if (!Number.isInteger(places) || places < 0 || places > 8)
|
|
500
|
+
throw new RangeError('a decimal has 0 to 8 places');
|
|
501
|
+
return new DecimalKind(places);
|
|
502
|
+
};
|
|
503
|
+
let boundCounter = 0;
|
|
504
|
+
// A condition at every key: only as a whole flag (`flags: ({...}) => ({covered: everyKey(k => ...)})`), the key read
|
|
505
|
+
// only as a map's key. The kernel closes it (instances where it holds, a witness where it fails).
|
|
506
|
+
export const everyKey = (f) => { const key = `k$${++boundCounter}`; return new Cond({ op: 'all', key, c: f(v(key)).c }, everyOk(scope?.keys, k => f(keyTerm(k)).ok)); };
|
|
507
|
+
// Some key where f holds, in a law or a step law's conclusion (¬∀k. ¬f(k)): proven through a witness, a term naming the
|
|
508
|
+
// key, given here or found by the prover among a few candidates (kernel/prove.ts).
|
|
509
|
+
export const someKey = (f, witness) => {
|
|
510
|
+
const key = `s$${++boundCounter}`;
|
|
511
|
+
const none = everyOk(scope?.keys, k => f(keyTerm(k)).not().ok);
|
|
512
|
+
return new Cond({ op: 'not', x: { op: 'all', key, c: f(v(key)).not().c, ...(witness === undefined ? {} : { witness: termOf(witness) }) } }, none === undefined ? undefined : !none);
|
|
513
|
+
};
|
|
208
514
|
// A map from naturals to naturals, every cell starting at `init`. One write per map per step.
|
|
209
515
|
export class MapRef {
|
|
210
516
|
name;
|
|
211
517
|
init;
|
|
518
|
+
cells;
|
|
212
519
|
constructor(name, init) {
|
|
213
520
|
this.name = name;
|
|
214
521
|
this.init = init;
|
|
522
|
+
this.cells = scope?.maps[name];
|
|
215
523
|
}
|
|
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) }; }
|
|
524
|
+
at(key) { const k = valOf(key); return new Term({ op: 'get', map: this.name, key: termOf(key) }, this.cells && k !== undefined ? this.cells.cells.get(k) ?? this.cells.dflt : undefined); }
|
|
525
|
+
set(key, value) { return { map: this.name, key: termOf(key), value: termOf(value), concrete: { key: valOf(key), value: valOf(value) } }; }
|
|
218
526
|
// key's cell plus `by` where `c` holds (the counter every cap keeps).
|
|
219
527
|
bump(key, c = yes, by = 1) { return this.set(key, when(c, this.at(key).plus(by), this.at(key))); }
|
|
528
|
+
// Every key at once: the value at each key, the key read only as a map's key (Other.at(k)).
|
|
529
|
+
each(f) { const each = `k$${++boundCounter}`; return { map: this.name, each, value: termOf(f(v(each))), concrete: { at: f } }; }
|
|
530
|
+
// As a set (cells 0 or 1): a member, and, as whole flags, every member of this one in another, none shared, the same.
|
|
531
|
+
has(key) { return this.at(key).atLeast(1); }
|
|
532
|
+
subsetOf(other) { return everyKey(k => this.at(k).atMost(other.at(k))); }
|
|
533
|
+
disjointFrom(other) { return everyKey(k => this.at(k).plus(other.at(k)).atMost(1)); }
|
|
534
|
+
sameAs(other) { return everyKey(k => this.at(k).eq(other.at(k))); }
|
|
535
|
+
// Over a universe of n names (keys 0 .. n − 1): every key past it holds 0 (a law, or a whole flag for a set a request
|
|
536
|
+
// carries), and the set's size, the sum of its cells there (exact, given `within`).
|
|
537
|
+
within(n) { naturalConst(n); return everyKey(k => k.atLeast(n).implies(this.at(k).eq(0))); }
|
|
538
|
+
sizeBelow(n) { naturalConst(n); return sum(0, ...Array.from({ length: n }, (_, key) => this.at(key))); }
|
|
539
|
+
}
|
|
540
|
+
export class ListRef {
|
|
541
|
+
name;
|
|
542
|
+
len;
|
|
543
|
+
cells;
|
|
544
|
+
constructor(name, len) {
|
|
545
|
+
this.name = name;
|
|
546
|
+
this.len = len;
|
|
547
|
+
this.cells = scope?.maps[name];
|
|
548
|
+
}
|
|
549
|
+
at(i) { const k = valOf(i); return new Term({ op: 'get', map: this.name, key: termOf(i) }, this.cells && k !== undefined ? this.cells.cells.get(k) ?? this.cells.dflt : undefined); }
|
|
550
|
+
// Conditions: x at some position; each position below the next.
|
|
551
|
+
has(x) { return everyKey(k => k.lt(this.len).implies(this.at(k).ne(x))).not(); }
|
|
552
|
+
sorted() { return everyKey(k => k.plus(1).lt(this.len).implies(this.at(k).lt(this.at(k.plus(1))))); }
|
|
553
|
+
// Operations.
|
|
554
|
+
push(x, when = yes) { return { when, at: k => when_(k.eq(this.len), x, this.at(k)), len: this.len.plus(1) }; }
|
|
555
|
+
pop(when = yes) { return { when, at: k => when_(k.plus(1).eq(this.len), 0, this.at(k)), len: this.len.minus(1) }; }
|
|
556
|
+
set(i, x, when = yes) { return { when, at: k => when_(k.eq(i), x, this.at(k)), len: this.len }; }
|
|
557
|
+
insertAt(i, x, when = yes) { return { when, at: k => when_(k.lt(i), this.at(k), when_(k.eq(i), x, this.at(k.minus(1)))), len: this.len.plus(1) }; }
|
|
558
|
+
removeAt(i, when = yes) { return { when, at: k => when_(k.lt(i), this.at(k), this.at(k.plus(1))), len: this.len.minus(1) }; }
|
|
559
|
+
// The updates for the step: the first operation whose condition holds, else the list as it was.
|
|
560
|
+
apply(...ops) {
|
|
561
|
+
const each = `k$${++boundCounter}`;
|
|
562
|
+
const at = (k) => ops.reduceRight((acc, op) => when_(op.when, op.at(k), acc), this.at(k));
|
|
563
|
+
const len = ops.reduceRight((acc, op) => when_(op.when, op.len, acc), this.len);
|
|
564
|
+
return { [this.name]: { map: this.name, each, value: at(v(each)).e, concrete: { at } }, [`${this.name}_len`]: len };
|
|
565
|
+
}
|
|
566
|
+
// Laws: nothing past the end; positions strictly increasing (so no name twice), as one law about every two keys.
|
|
567
|
+
within() { return forall(k => this.len.atMost(k).implies(this.at(k).eq(0))); }
|
|
568
|
+
increasing() { return forall((i, j) => i.lt(j).and(j.lt(this.len)).implies(this.at(i).lt(this.at(j)))); }
|
|
220
569
|
}
|
|
570
|
+
const when_ = (c, then, otherwise) => new Term({ op: 'if', c: c.c, then: termOf(then), else: termOf(otherwise) }, c.ok === undefined ? undefined : valOf(c.ok ? then : otherwise));
|
|
221
571
|
// A law about every key of a map: proven at a fresh key, assumed at the keys a step reads.
|
|
222
|
-
|
|
572
|
+
// With two parameters or more, about every two keys or more: forall((i, j) => i.lt(j).implies(...)).
|
|
573
|
+
export function forall(f) { return { forall: f }; }
|
|
574
|
+
// The keys a law's callback names: one, or two when it takes two.
|
|
575
|
+
const keysFor = (out) => [`key$${++keyCounter}`, Array.from({ length: Math.max(0, out.forall.length - 1) }, () => `key$${++keyCounter}`)];
|
|
223
576
|
// For histories: `holds` (a <= b, homogeneous over sums and queries) wherever `when` (params and plain inputs) holds.
|
|
224
577
|
export const relation = (holds, whenever = yes) => ({ when: whenever, holds });
|
|
578
|
+
// --- quantifiers anywhere ---------------------------------------------------------------------------------------------
|
|
579
|
+
// The kernel takes a condition at every key as a whole flag only (decided once per step, closed both ways). Written
|
|
580
|
+
// anywhere else in a step (inside another flag, a term's if, a write, the assumption), it is named here as a flag of its
|
|
581
|
+
// own, so every part downstream sees only whole flags. A per-step truth inside such a condition (another quantifier,
|
|
582
|
+
// or a flag) is split out by cases: ∀k. φ(k, q) is (q ∧ ∀k. φ(k, yes)) ∨ (¬q ∧ ∀k. φ(k, no)). A quantifier that reads a
|
|
583
|
+
// key bound outside it is two keys at once, which no flag is.
|
|
584
|
+
const namesIn = (t) => [...JSON.stringify(t).matchAll(/"op":"var","name":"([^"]+)"/g)].map(m => m[1]);
|
|
585
|
+
function swapC(c, from, to) {
|
|
586
|
+
if (c === from)
|
|
587
|
+
return to;
|
|
588
|
+
switch (c.op) {
|
|
589
|
+
case 'not': return { op: 'not', x: swapC(c.x, from, to) };
|
|
590
|
+
case 'and':
|
|
591
|
+
case 'or': return { op: c.op, xs: c.xs.map(y => swapC(y, from, to)) };
|
|
592
|
+
case 'all': return { op: 'all', key: c.key, c: swapC(c.c, from, to) };
|
|
593
|
+
case 'flag': return c;
|
|
594
|
+
case '<':
|
|
595
|
+
case '<=':
|
|
596
|
+
case '=': return { ...c, a: swapE(c.a, from, to), b: swapE(c.b, from, to) };
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
function swapE(e, from, to) {
|
|
600
|
+
switch (e.op) {
|
|
601
|
+
case 'var':
|
|
602
|
+
case 'const': return e;
|
|
603
|
+
case 'mul': return { ...e, a: swapE(e.a, from, to) };
|
|
604
|
+
case 'if': return { op: 'if', c: swapC(e.c, from, to), then: swapE(e.then, from, to), else: swapE(e.else, from, to) };
|
|
605
|
+
case 'get': return { ...e, key: swapE(e.key, from, to) };
|
|
606
|
+
case 'add':
|
|
607
|
+
case 'sub':
|
|
608
|
+
case 'min':
|
|
609
|
+
case 'max':
|
|
610
|
+
case 'prod': return { ...e, a: swapE(e.a, from, to), b: swapE(e.b, from, to) };
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
function perStep(c) {
|
|
614
|
+
switch (c.op) {
|
|
615
|
+
case 'flag':
|
|
616
|
+
case 'all': return c;
|
|
617
|
+
case 'not': return perStep(c.x);
|
|
618
|
+
case 'and':
|
|
619
|
+
case 'or':
|
|
620
|
+
for (const y of c.xs) {
|
|
621
|
+
const p = perStep(y);
|
|
622
|
+
if (p)
|
|
623
|
+
return p;
|
|
624
|
+
}
|
|
625
|
+
return undefined;
|
|
626
|
+
case '<':
|
|
627
|
+
case '<=':
|
|
628
|
+
case '=': return perStepE(c.a) ?? perStepE(c.b);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
function perStepE(e) {
|
|
632
|
+
switch (e.op) {
|
|
633
|
+
case 'var':
|
|
634
|
+
case 'const': return undefined;
|
|
635
|
+
case 'mul': return perStepE(e.a);
|
|
636
|
+
case 'if': return perStep(e.c) ?? perStepE(e.then) ?? perStepE(e.else);
|
|
637
|
+
case 'get': return perStepE(e.key);
|
|
638
|
+
case 'add':
|
|
639
|
+
case 'sub':
|
|
640
|
+
case 'min':
|
|
641
|
+
case 'max':
|
|
642
|
+
case 'prod': return perStepE(e.a) ?? perStepE(e.b);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
class Hoister {
|
|
646
|
+
prefix;
|
|
647
|
+
flags = {};
|
|
648
|
+
constructor(prefix) {
|
|
649
|
+
this.prefix = prefix;
|
|
650
|
+
}
|
|
651
|
+
n = 0;
|
|
652
|
+
// c with every quantifier inside it named as a flag; `bound` the keys bound outside c.
|
|
653
|
+
cond(c, bound = []) {
|
|
654
|
+
switch (c.op) {
|
|
655
|
+
case 'all': return this.quantifier(c, bound);
|
|
656
|
+
case 'flag': return c;
|
|
657
|
+
case 'not': return { op: 'not', x: this.cond(c.x, bound) };
|
|
658
|
+
case 'and':
|
|
659
|
+
case 'or': return { op: c.op, xs: c.xs.map(y => this.cond(y, bound)) };
|
|
660
|
+
case '<':
|
|
661
|
+
case '<=':
|
|
662
|
+
case '=': return { ...c, a: this.term(c.a, bound), b: this.term(c.b, bound) };
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
term(e, bound = []) {
|
|
666
|
+
switch (e.op) {
|
|
667
|
+
case 'var':
|
|
668
|
+
case 'const': return e;
|
|
669
|
+
case 'mul': return { ...e, a: this.term(e.a, bound) };
|
|
670
|
+
case 'if': return { op: 'if', c: this.cond(e.c, bound), then: this.term(e.then, bound), else: this.term(e.else, bound) };
|
|
671
|
+
case 'get': return { ...e, key: this.term(e.key, bound) };
|
|
672
|
+
case 'add':
|
|
673
|
+
case 'sub':
|
|
674
|
+
case 'min':
|
|
675
|
+
case 'max':
|
|
676
|
+
case 'prod': return { ...e, a: this.term(e.a, bound), b: this.term(e.b, bound) };
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
// A quantifier as a flag (or, with per-step truths inside, a condition over flags).
|
|
680
|
+
quantifier(q, bound, name) {
|
|
681
|
+
const outer = bound.filter(key => namesIn(q).includes(key));
|
|
682
|
+
if (outer.length)
|
|
683
|
+
throw new Error(`a condition at every key reads ${outer[0]}, a key bound outside it: two keys at once are outside the kernel`);
|
|
684
|
+
const inner = perStep(q.c);
|
|
685
|
+
if (!inner) {
|
|
686
|
+
const f = name ?? `${this.prefix}${++this.n}`;
|
|
687
|
+
this.flags[f] = { op: 'all', key: q.key, c: q.c };
|
|
688
|
+
return { op: 'flag', name: f };
|
|
689
|
+
}
|
|
690
|
+
const truth = inner.op === 'flag' ? inner : this.quantifier(inner, [...bound, q.key]);
|
|
691
|
+
const split = (by) => this.quantifier({ op: 'all', key: q.key, c: swapC(q.c, inner, by) }, bound);
|
|
692
|
+
return { op: 'or', xs: [{ op: 'and', xs: [truth, split(yes.c)] }, { op: 'and', xs: [{ op: 'not', x: truth }, split(no.c)] }] };
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
// A machine built by hand (or by a combinator) with quantifiers inside its conditions, terms or writes: each named as a
|
|
696
|
+
// flag of its own, as the builders do.
|
|
697
|
+
export function nameQuantifiers(m) {
|
|
698
|
+
const hoist = new Hoister('all$'), flags = {};
|
|
699
|
+
const placed = () => { for (const [n, c] of Object.entries(hoist.flags))
|
|
700
|
+
if (!(n in flags))
|
|
701
|
+
flags[n] = c; };
|
|
702
|
+
for (const [n, c] of Object.entries(m.step.flags ?? {})) {
|
|
703
|
+
const out = c.op === 'all' ? hoist.quantifier(c, [], n) : hoist.cond(c);
|
|
704
|
+
placed();
|
|
705
|
+
if (!(n in flags))
|
|
706
|
+
flags[n] = out;
|
|
707
|
+
}
|
|
708
|
+
const state = Object.fromEntries(Object.entries(m.step.state ?? {}).map(([n, e]) => [n, hoist.term(e)]));
|
|
709
|
+
const maps = Object.fromEntries(Object.entries(m.step.maps ?? {}).map(([n, w]) => [n, 'each' in w ? { each: w.each, value: hoist.term(w.value, [w.each]) } : { key: hoist.term(w.key), value: hoist.term(w.value) }]));
|
|
710
|
+
const assume = m.assume ? hoist.cond(m.assume) : undefined;
|
|
711
|
+
placed();
|
|
712
|
+
return { ...m, step: { ...(Object.keys(flags).length ? { flags } : {}), ...(m.step.state ? { state } : {}), ...(m.step.maps ? { maps } : {}) }, ...(assume ? { assume } : {}) };
|
|
713
|
+
}
|
|
714
|
+
// A law or a step law's conclusion with quantifiers inside, each pulled out as the law's one key: ∀k. A ∧ ∀k. B is
|
|
715
|
+
// ∀k. A ∧ B, and p ∨ ∀k. A is ∀k. p ∨ A when p doesn't read the key. One under an or beside a part that reads the key
|
|
716
|
+
// is two keys at once. A quantifier under a not is some key (someKey), left for the prover's witness.
|
|
717
|
+
const hasForall = (c, positive = true) => c.op === 'all' ? positive || hasForall(c.c, positive) : c.op === 'not' ? hasForall(c.x, !positive) : c.op === 'and' || c.op === 'or' ? c.xs.some(y => hasForall(y, positive)) : false;
|
|
718
|
+
const hasAll = (c) => JSON.stringify(c).includes('"op":"all"');
|
|
719
|
+
function prenex(c, what, key) {
|
|
720
|
+
let found = false;
|
|
721
|
+
const walk = (d, positive) => {
|
|
722
|
+
switch (d.op) {
|
|
723
|
+
case 'all':
|
|
724
|
+
if (!positive)
|
|
725
|
+
return d;
|
|
726
|
+
found = true;
|
|
727
|
+
return walk(substKey(d.c, d.key, key), positive);
|
|
728
|
+
case 'not': return { op: 'not', x: walk(d.x, !positive) };
|
|
729
|
+
case 'and':
|
|
730
|
+
case 'or': {
|
|
731
|
+
if ((d.op === 'or') === positive && d.xs.some(y => hasForall(y, positive)) && d.xs.filter(y => hasForall(y, positive) || namesIn(y).includes(key)).length > 1)
|
|
732
|
+
throw new Error(`${what}: a condition at every key in an or beside another part that reads a key is two keys at once`);
|
|
733
|
+
return { op: d.op, xs: d.xs.map(y => walk(y, positive)) };
|
|
734
|
+
}
|
|
735
|
+
case 'flag': return d;
|
|
736
|
+
case '<':
|
|
737
|
+
case '<=':
|
|
738
|
+
case '=':
|
|
739
|
+
if (hasAll(d))
|
|
740
|
+
throw new Error(`${what}: a condition at every key inside a term is a flag of the rule`);
|
|
741
|
+
return d;
|
|
742
|
+
}
|
|
743
|
+
};
|
|
744
|
+
const out = walk(c, true);
|
|
745
|
+
return found ? out : undefined;
|
|
746
|
+
}
|
|
747
|
+
const substKey = (c, from, to) => JSON.parse(JSON.stringify(c).replaceAll(`"op":"var","name":"${from}"`, `"op":"var","name":"${to}"`));
|
|
748
|
+
export const fn = (arity, js, opts = {}) => {
|
|
749
|
+
if (!Number.isInteger(arity) || arity < 1)
|
|
750
|
+
throw new Error('a function takes one argument or more');
|
|
751
|
+
return { arity, js, ...opts };
|
|
752
|
+
};
|
|
753
|
+
// x / c and x mod c, rounded down, for a constant c >= 1: each with its two facts (c·q <= x < c·q + c), which Lean's omega
|
|
754
|
+
// proves of `x / c`.
|
|
755
|
+
export const divBy = (c) => {
|
|
756
|
+
if (!Number.isSafeInteger(c) || c < 1)
|
|
757
|
+
throw new RangeError('divBy takes a whole number at least 1');
|
|
758
|
+
return fn(1, `x => Math.floor(x / ${c})`, { lean: `fun x => x / ${c}`, facts: f => ({
|
|
759
|
+
[`${c}·q(x) <= x`]: forall(x => f(x).times(c).atMost(x)),
|
|
760
|
+
[`x < ${c}·q(x) + ${c}`]: forall(x => x.lt(f(x).times(c).plus(c))),
|
|
761
|
+
}) });
|
|
762
|
+
};
|
|
763
|
+
export const modBy = (c) => {
|
|
764
|
+
if (!Number.isSafeInteger(c) || c < 1)
|
|
765
|
+
throw new RangeError('modBy takes a whole number at least 1');
|
|
766
|
+
// x mod c is below c, and x less it is a multiple of c: x ∸ r = c·q for the q that divBy(c) gives; as facts about r
|
|
767
|
+
// alone, r <= x, r < c, and r(x + c) = r(x).
|
|
768
|
+
return fn(1, `x => x % ${c}`, { lean: `fun x => x % ${c}`, facts: f => ({
|
|
769
|
+
[`r(x) < ${c}`]: forall(x => f(x).lt(c)),
|
|
770
|
+
[`r(x) <= x`]: forall(x => f(x).atMost(x)),
|
|
771
|
+
[`r(x + ${c}) = r(x)`]: forall(x => f(x.plus(c)).eq(f(x))),
|
|
772
|
+
}) });
|
|
773
|
+
};
|
|
774
|
+
const pairE = (a, b) => ({ op: 'if', c: { op: '<', a, b }, then: { op: 'add', a: { op: 'prod', a: b, b }, b: a }, else: { op: 'add', a: { op: 'add', a: { op: 'prod', a, b: a }, b: a }, b } });
|
|
775
|
+
const argsKey = (args) => args.slice(1).reduce(pairE, args[0]);
|
|
776
|
+
// A function's JavaScript as one function of its key: its arguments unpaired (left-nested, as argsKey pairs them).
|
|
777
|
+
const fnSource = (name, d) => {
|
|
778
|
+
const src = typeof d.js === 'string' ? d.js : String(d.js);
|
|
779
|
+
let f;
|
|
780
|
+
try {
|
|
781
|
+
f = compileFunction([], `return (${src})`)();
|
|
782
|
+
f(...Array.from({ length: d.arity }, () => 0));
|
|
783
|
+
}
|
|
784
|
+
catch (e) {
|
|
785
|
+
throw new Error(`function ${name}: its js must be a function of its arguments that reads nothing else (${e.message})`);
|
|
786
|
+
}
|
|
787
|
+
if (d.arity === 1)
|
|
788
|
+
return src;
|
|
789
|
+
return `k => { const u = z => { const s = Math.floor(Math.sqrt(z)); return z - s * s < s ? [z - s * s, s] : [s, z - s * s - s]; }; const a = []; let z = k; for (let i = 1; i < ${d.arity}; i++) { const [p, q] = u(z); a.unshift(q); z = p; } a.unshift(z); return (${src})(...a); }`;
|
|
790
|
+
};
|
|
791
|
+
const fnTerm = (name, d) => (...args) => {
|
|
792
|
+
if (args.length !== d.arity)
|
|
793
|
+
throw new Error(`function ${name} takes ${d.arity} argument${d.arity === 1 ? '' : 's'}`);
|
|
794
|
+
const vals = known(args.map(valOf));
|
|
795
|
+
return new Term({ op: 'get', map: name, key: argsKey(args.map(termOf)) }, vals && fnValueOf(d)(...vals));
|
|
796
|
+
};
|
|
797
|
+
// A function's JavaScript on its arguments themselves (not through the pairing the kernel reads them by).
|
|
798
|
+
const fnValues = new WeakMap();
|
|
799
|
+
const fnValueOf = (d) => {
|
|
800
|
+
let f = fnValues.get(d);
|
|
801
|
+
if (!f) {
|
|
802
|
+
f = typeof d.js === 'function' ? d.js : compileFunction([], `return (${d.js})`)();
|
|
803
|
+
fnValues.set(d, f);
|
|
804
|
+
}
|
|
805
|
+
return f;
|
|
806
|
+
};
|
|
225
807
|
let keyCounter = 0;
|
|
808
|
+
const sources = new WeakMap();
|
|
809
|
+
const withSource = (x, src) => (sources.set(x, src), x);
|
|
226
810
|
// A k-step law's later requests are built over marked names (j:name, which no rule can declare: ':' is reserved); the
|
|
227
811
|
// kernel reads them as name@j.
|
|
228
812
|
const markedName = (name) => { const m = /^(\d+):(.*)$/.exec(name); return m ? `${m[2]}@${m[1]}` : name; };
|
|
@@ -240,7 +824,7 @@ const markedE = (e) => {
|
|
|
240
824
|
case 'prod': return { ...e, a: markedE(e.a), b: markedE(e.b) };
|
|
241
825
|
}
|
|
242
826
|
};
|
|
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) };
|
|
827
|
+
const marked = (c) => c.op === 'flag' ? c : c.op === 'all' ? { op: 'all', key: c.key, c: marked(c.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
828
|
// 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
829
|
// values the builders hand out, grown by each call.
|
|
246
830
|
export class Rule {
|
|
@@ -257,6 +841,36 @@ export class Rule {
|
|
|
257
841
|
return this.with({ state: [...this.spec.state, ...Object.entries(init)] });
|
|
258
842
|
}
|
|
259
843
|
maps(init) { return this.with({ maps: [...this.spec.maps, ...Object.entries(init)] }); }
|
|
844
|
+
// Integers (Int), each starting at an integer or at one over the params; kept as two naturals, <name> and <name>$neg.
|
|
845
|
+
ints(init) { return this.signedState(init, undefined); }
|
|
846
|
+
// Decimals with `places` places (Decimal), each starting at a number, a string ("12.50") or one over the params.
|
|
847
|
+
decimals(places, init) {
|
|
848
|
+
decimal(places);
|
|
849
|
+
return this.signedState(init, places);
|
|
850
|
+
}
|
|
851
|
+
signedState(init, places) {
|
|
852
|
+
const parts = (n, x) => {
|
|
853
|
+
if (typeof x !== 'function') {
|
|
854
|
+
const i = places === undefined ? Int.of(x) : Int.of(scaledOf(x, places));
|
|
855
|
+
return [[n, termValue(i.pos)], [`${n}$neg`, termValue(i.neg)]];
|
|
856
|
+
}
|
|
857
|
+
const at = (p) => normal(places === undefined ? Int.of(x(p)) : new Decimal(Int.of(0), places).lift(x(p)));
|
|
858
|
+
return [[n, p => at(p).pos], [`${n}$neg`, p => at(p).neg]];
|
|
859
|
+
};
|
|
860
|
+
return this.with({ state: [...this.spec.state, ...Object.entries(init).flatMap(([n, x]) => parts(n, x))], signed: [...(this.spec.signed ?? []), ...Object.keys(init).map(n => [n, places])] });
|
|
861
|
+
}
|
|
862
|
+
// Lists, each empty at the start: a map of positions and its length <name>_len (ListRef).
|
|
863
|
+
lists(...names) {
|
|
864
|
+
return this.with({ maps: [...this.spec.maps, ...names.map(n => [n, 0])], state: [...this.spec.state, ...names.map(n => [`${n}_len`, 0])], lists: [...(this.spec.lists ?? []), ...names] });
|
|
865
|
+
}
|
|
866
|
+
// Functions defined outside the rule (fn, divBy, modBy), read like params that take arguments: f(x).
|
|
867
|
+
fns(defs) {
|
|
868
|
+
return this.with({ fns: [...(this.spec.fns ?? []), ...Object.entries(defs)] });
|
|
869
|
+
}
|
|
870
|
+
// What is given about the functions (with their own facts): laws over the params and functions, each by name.
|
|
871
|
+
facts(f) { return this.with({ facts: [...(this.spec.facts ?? []), f] }); }
|
|
872
|
+
// Maps each request carries whole (a set of names, say: cells 0 or 1), every cell past them 0; read like any map.
|
|
873
|
+
carries(...names) { return this.with({ carried: [...(this.spec.carried ?? []), ...names] }); }
|
|
260
874
|
// Named conditions of the step, decided once per step before it; each call's flags may read the earlier calls'. A flag
|
|
261
875
|
// named again replaces the earlier definition in place (what a variant needs: one call, the step unchanged); the new
|
|
262
876
|
// definition may read only the flags defined before the original's place, as the kernel checks.
|
|
@@ -288,12 +902,15 @@ export class Rule {
|
|
|
288
902
|
// (the kernel proves it adds nothing to any query).
|
|
289
903
|
prune(order, after) { return this.with({ prune: { order, after: after } }); }
|
|
290
904
|
// 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
|
-
|
|
293
|
-
if (out instanceof Cond)
|
|
294
|
-
return { name, holds: out.c };
|
|
905
|
+
law(name, f) { return withSource(this.lawOf(name, f(this.env({ flags: false, inputs: false, queries: false }))), { law: f }); }
|
|
906
|
+
lawOf(name, out) {
|
|
295
907
|
const key = `key$${++keyCounter}`;
|
|
296
|
-
|
|
908
|
+
if (out instanceof Cond) {
|
|
909
|
+
const p = prenex(out.c, `law ${name}`, key);
|
|
910
|
+
return p ? { name, forall: key, holds: p } : { name, holds: out.c };
|
|
911
|
+
}
|
|
912
|
+
const [k1, more] = keysFor(out), holds = out.forall(v(k1), ...more.map(v)).c;
|
|
913
|
+
return more.length ? { name, forall: k1, keys: more, holds } : { name, forall: k1, holds: prenex(holds, `law ${name}`, k1) ?? holds };
|
|
297
914
|
}
|
|
298
915
|
// A law about one step, for every request the rule allows: `then(before, after)` relates what the step reads (params,
|
|
299
916
|
// the request, state, maps, flags, sums, queries) to the state, maps and sums after it. `when` narrows the requests.
|
|
@@ -307,11 +924,20 @@ export class Rule {
|
|
|
307
924
|
after[n] = v(`${n}'`);
|
|
308
925
|
for (const [n, init] of this.spec.maps)
|
|
309
926
|
after[n] = new MapRef(`${n}'`, init);
|
|
927
|
+
for (const n of this.spec.lists ?? [])
|
|
928
|
+
after[n] = new ListRef(`${n}'`, v(`${n}_len'`));
|
|
929
|
+
for (const [n, places] of this.spec.signed ?? []) {
|
|
930
|
+
after[n] = this.signedValue(n, places, `${n}'`, `${n}$neg'`);
|
|
931
|
+
delete after[`${n}$neg`];
|
|
932
|
+
}
|
|
310
933
|
const out = then(env, after), w = when ? { when: when(env).c } : {};
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
934
|
+
const key = `key$${++keyCounter}`, src = { step: { then: then, ...(when ? { when: when } : {}) } };
|
|
935
|
+
if (out instanceof Cond) {
|
|
936
|
+
const p = prenex(out.c, `step law ${name}`, key);
|
|
937
|
+
return withSource(p ? { name, forall: key, then: p, ...w } : { name, then: out.c, ...w }, src);
|
|
938
|
+
}
|
|
939
|
+
const [k1, more] = keysFor(out), c = out.forall(v(k1), ...more.map(v)).c;
|
|
940
|
+
return withSource(more.length ? { name, forall: k1, keys: more, then: c, ...w } : { name, forall: k1, then: prenex(c, `step law ${name}`, k1) ?? c, ...w }, src);
|
|
315
941
|
}
|
|
316
942
|
// 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
943
|
// state, maps and sums after the last, with each step's request and queries (requests[0] is the first's); `when(before,
|
|
@@ -326,6 +952,12 @@ export class Rule {
|
|
|
326
952
|
after[n] = v(`${n}'`);
|
|
327
953
|
for (const [n, init] of this.spec.maps)
|
|
328
954
|
after[n] = new MapRef(`${n}'`, init);
|
|
955
|
+
for (const n of this.spec.lists ?? [])
|
|
956
|
+
after[n] = new ListRef(`${n}'`, v(`${n}_len'`));
|
|
957
|
+
for (const [n, places] of this.spec.signed ?? []) {
|
|
958
|
+
after[n] = this.signedValue(n, places, `${n}'`, `${n}$neg'`);
|
|
959
|
+
delete after[`${n}$neg`];
|
|
960
|
+
}
|
|
329
961
|
// The j-th request's values over marked names, renamed to the kernel's input@j once built.
|
|
330
962
|
const mark = (j) => `${j}:`;
|
|
331
963
|
const requests = Array.from({ length: k }, (_, j) => {
|
|
@@ -338,17 +970,48 @@ export class Rule {
|
|
|
338
970
|
while (whens.length && whens.at(-1) === undefined)
|
|
339
971
|
whens.pop();
|
|
340
972
|
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
973
|
const key = `key$${++keyCounter}`;
|
|
344
|
-
|
|
974
|
+
if (out instanceof Cond) {
|
|
975
|
+
const p = prenex(out.c, `step law ${name}`, key);
|
|
976
|
+
return p ? { name, steps: k, forall: key, then: marked(p), ...w } : { name, steps: k, then: marked(out.c), ...w };
|
|
977
|
+
}
|
|
978
|
+
const [k1, more] = keysFor(out), c = out.forall(v(k1), ...more.map(v)).c;
|
|
979
|
+
return more.length ? { name, steps: k, forall: k1, keys: more, then: marked(c), ...w } : { name, steps: k, forall: k1, then: marked(prenex(c, `step law ${name}`, k1) ?? c), ...w };
|
|
980
|
+
}
|
|
981
|
+
// "Whenever `from` holds, `to` holds later", in every run that takes each helpful request infinitely often: proven by
|
|
982
|
+
// a rank (a natural over the state and params, or several compared in order) that no step raises while `from` holds
|
|
983
|
+
// and `to` doesn't, except by reaching `to`, and that each helpful request lowers (or it reaches `to`). A helpful
|
|
984
|
+
// request names some inputs by terms over the state and params, the rest left free; with several, each comes with the
|
|
985
|
+
// region where it helps (`when`), some region always holds, and a step that keeps the rank keeps its region.
|
|
986
|
+
// queue.leadsTo('every ticket is served', {from: ({t, next, served}) => t.lt(next).and(served.atMost(t)), to: ({t, served}) => served.gt(t),
|
|
987
|
+
// rank: ({t, served}) => t.plus(1).minus(served), helpful: () => ({op: 1})})
|
|
988
|
+
// batches.leadsTo('every batch is done', {from: () => yes, to: ({waiting, items}) => waiting.eq(0).and(items.eq(0)),
|
|
989
|
+
// rank: ({waiting, items}) => [waiting, items], helpful: [{when: ({items}) => items.gt(0), request: () => ({op: 1})}, {when: ({items}) => items.eq(0), request: () => ({op: 0})}]})
|
|
990
|
+
leadsTo(name, spec) {
|
|
991
|
+
const env = this.env({ flags: false, inputs: false, queries: false });
|
|
992
|
+
const req = (f) => Object.fromEntries(Object.entries(f(env)).map(([n, t]) => [n, termOf(t)]));
|
|
993
|
+
const r = spec.rank(env), rank = Array.isArray(r) ? r.map(termOf) : termOf(r);
|
|
994
|
+
const base = { name, from: spec.from(env).c, to: spec.to(env).c, rank };
|
|
995
|
+
return typeof spec.helpful === 'function' ? { ...base, request: req(spec.helpful) } : { ...base, helpful: spec.helpful.map(h => ({ ...(h.when ? { when: h.when(env).c } : {}), request: req(h.request) })) };
|
|
345
996
|
}
|
|
346
|
-
// A decision: whenever the params and the request meet `given`, `then` holds. Decisions don't read the state
|
|
997
|
+
// A decision: whenever the params and the request meet `given`, `then` holds. Decisions don't read the state; the
|
|
998
|
+
// functions' facts are assumed at the arguments the decision reads them at.
|
|
347
999
|
decide(name, given, then) {
|
|
348
1000
|
const env = this.env({ flags: false, state: false, queries: false });
|
|
349
1001
|
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
|
-
|
|
1002
|
+
const t = then(env).c, goal = { name, hyps: [...hyps, { op: 'not', x: t }] };
|
|
1003
|
+
const facts = this.spec.fns?.length || this.spec.facts?.length ? factsFor(this.machine, goal) : [];
|
|
1004
|
+
return withSource({ name, rule: this, given: hyps, then: t, goal: { name, hyps: [...facts, ...goal.hyps] } }, { decision: { given: given, then: then, own: Array.isArray(g) ? g.length : 1 } });
|
|
1005
|
+
}
|
|
1006
|
+
// A decision about two requests under the same params (noninterference, say: requests that differ only in a secret
|
|
1007
|
+
// are decided alike). `given` and `then` read both; the second request's inputs are named b.<input> in a witness.
|
|
1008
|
+
// r.relate('the secret is irrelevant', (a, b) => a.user.eq(b.user), (a, b) => a.allowed.iff(b.allowed))
|
|
1009
|
+
relate(name, given, then) {
|
|
1010
|
+
const s = this.spec, b = (n) => `b.${n}`, own = [...s.inputs.map(([n]) => n), ...(s.carried ?? [])];
|
|
1011
|
+
const second = (env) => ({ ...env, ...Object.fromEntries(own.map(n => [n, env[b(n)]])) });
|
|
1012
|
+
const pair = new Rule({ ...s, inputs: [...s.inputs, ...s.inputs.map(([n, k]) => [b(n), k])], carried: [...(s.carried ?? []), ...(s.carried ?? []).map(b)],
|
|
1013
|
+
assume: [...s.assume, ...s.assume.map(f => (env) => f(second(env)))], flags: [], step: undefined, record: undefined, sums: [], queries: [], keep: undefined, relations: undefined, prune: undefined });
|
|
1014
|
+
return pair.decide(name, env => given(env, second(env)), env => then(env, second(env)));
|
|
352
1015
|
}
|
|
353
1016
|
// --- what it compiles to ------------------------------------------------------------------------------------------
|
|
354
1017
|
kindAssumptions(which = 'both') {
|
|
@@ -356,44 +1019,104 @@ export class Rule {
|
|
|
356
1019
|
const is = which === 'params' ? [] : this.spec.inputs.flatMap(([n, k]) => k.assume(n));
|
|
357
1020
|
return [...ps, ...is].map(c => c.c);
|
|
358
1021
|
}
|
|
1022
|
+
// A signed state variable's value over these names of its two parts.
|
|
1023
|
+
signedValue(n, places, pos, neg) {
|
|
1024
|
+
const i = new Int(v(pos), v(neg));
|
|
1025
|
+
return places === undefined ? i : new Decimal(i, places);
|
|
1026
|
+
}
|
|
359
1027
|
// The values handed to the builders' callbacks.
|
|
360
1028
|
env(parts = {}) {
|
|
1029
|
+
this.distinct();
|
|
361
1030
|
const out = {};
|
|
362
1031
|
for (const [n, k] of this.spec.params)
|
|
363
1032
|
out[n] = k.value(n);
|
|
364
|
-
|
|
1033
|
+
for (const [n, d] of this.spec.fns ?? [])
|
|
1034
|
+
out[n] = fnTerm(n, d);
|
|
1035
|
+
if (parts.inputs !== false) {
|
|
365
1036
|
for (const [n, k] of this.spec.inputs)
|
|
366
1037
|
out[n] = k.value(n);
|
|
1038
|
+
for (const n of this.spec.carried ?? [])
|
|
1039
|
+
out[n] = new MapRef(n, 0);
|
|
1040
|
+
}
|
|
367
1041
|
if (parts.state !== false) {
|
|
368
1042
|
for (const [n] of this.spec.state)
|
|
369
1043
|
out[n] = v(n);
|
|
370
1044
|
for (const [n, init] of this.spec.maps)
|
|
371
1045
|
out[n] = new MapRef(n, init);
|
|
1046
|
+
for (const n of this.spec.lists ?? [])
|
|
1047
|
+
out[n] = new ListRef(n, v(`${n}_len`));
|
|
372
1048
|
for (const [n] of this.spec.sums)
|
|
373
1049
|
out[n] = v(n);
|
|
1050
|
+
for (const [n, places] of this.spec.signed ?? []) {
|
|
1051
|
+
out[n] = this.signedValue(n, places, n, `${n}$neg`);
|
|
1052
|
+
delete out[`${n}$neg`];
|
|
1053
|
+
}
|
|
374
1054
|
}
|
|
375
1055
|
if (parts.queries !== false)
|
|
376
1056
|
for (const [n] of this.spec.queries)
|
|
377
1057
|
out[n] = v(n);
|
|
378
1058
|
if (parts.flags !== false)
|
|
379
1059
|
for (const f of this.spec.flags)
|
|
380
|
-
for (const n of Object.keys(f(out)))
|
|
381
|
-
out[n]
|
|
1060
|
+
for (const n of Object.keys(f(out))) {
|
|
1061
|
+
if (n in out && !(out[n] instanceof Cond && out[n].c.op === 'flag'))
|
|
1062
|
+
throw new Error(`${this.spec.name}: ${n} is a flag and ${this.whatIs(n)}`);
|
|
1063
|
+
out[n] = new Cond({ op: 'flag', name: n }, scope?.flags[n]);
|
|
1064
|
+
}
|
|
382
1065
|
return out;
|
|
383
1066
|
}
|
|
1067
|
+
// Every name the rule declares is declared once (a flag named again replaces the earlier one, so flags are checked as
|
|
1068
|
+
// they are defined): an input and a state variable sharing a name would hand the callbacks only one of them.
|
|
1069
|
+
names() {
|
|
1070
|
+
const s = this.spec;
|
|
1071
|
+
return [...s.params.map(([n]) => [n, 'a param']), ...(s.fns ?? []).map(([n]) => [n, 'a function']), ...s.inputs.map(([n]) => [n, 'an input']), ...(s.carried ?? []).map(n => [n, 'a carried map']),
|
|
1072
|
+
...s.state.map(([n]) => [n, 'a state variable']), ...s.maps.map(([n]) => [n, 'a map']), ...s.sums.map(([n]) => [n, 'a sum']), ...s.queries.map(([n]) => [n, 'a query']),
|
|
1073
|
+
];
|
|
1074
|
+
}
|
|
1075
|
+
whatIs(n) { return this.names().find(([m]) => m === n)?.[1] ?? 'something else'; }
|
|
1076
|
+
distinct() {
|
|
1077
|
+
const seen = new Map();
|
|
1078
|
+
for (const [n, what] of this.names()) {
|
|
1079
|
+
const had = seen.get(n);
|
|
1080
|
+
if (had)
|
|
1081
|
+
throw new Error(`${this.spec.name}: ${n} is ${had} and ${had === what ? 'declared again' : what}; give each a name of its own`);
|
|
1082
|
+
seen.set(n, what);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
384
1085
|
get machine() {
|
|
385
1086
|
const s = this.spec;
|
|
386
1087
|
const params = s.params.flatMap(([n, k]) => k.vars(n)), queries = s.queries.map(([n]) => n);
|
|
387
1088
|
const inputs = [...s.inputs.flatMap(([n, k]) => k.vars(n)), ...queries];
|
|
388
1089
|
const pEnv = this.env({ inputs: false, state: false, flags: false, queries: false });
|
|
389
1090
|
const env = this.env({ flags: false });
|
|
390
|
-
const
|
|
1091
|
+
const declared = {};
|
|
391
1092
|
for (const f of s.flags)
|
|
392
1093
|
for (const [n, c] of Object.entries(f(env))) {
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
1094
|
+
if (n in env && !(n in declared))
|
|
1095
|
+
throw new Error(`${s.name}: ${n} is a flag and ${this.whatIs(n)}`);
|
|
1096
|
+
declared[n] = c.c;
|
|
1097
|
+
env[n] = new Cond({ op: 'flag', name: n }); // a name again: replaced in place
|
|
1098
|
+
}
|
|
1099
|
+
// Quantifiers anywhere, named as flags: each placed before the flag that reads it.
|
|
1100
|
+
const hoist = new Hoister('all$'), flags = {};
|
|
1101
|
+
const placed = () => { for (const [n, c] of Object.entries(hoist.flags))
|
|
1102
|
+
if (!(n in flags))
|
|
1103
|
+
flags[n] = c; };
|
|
1104
|
+
for (const [n, c] of Object.entries(declared)) {
|
|
1105
|
+
const out = c.op === 'all' ? hoist.quantifier(c, [], n) : hoist.cond(c);
|
|
1106
|
+
placed();
|
|
1107
|
+
if (!(n in flags))
|
|
1108
|
+
flags[n] = out;
|
|
1109
|
+
}
|
|
1110
|
+
const updates = { ...s.step?.(env) };
|
|
1111
|
+
// A signed variable's update, as its two parts, normalized.
|
|
1112
|
+
for (const [n, places] of s.signed ?? []) {
|
|
1113
|
+
const u = updates[n];
|
|
1114
|
+
if (u === undefined)
|
|
1115
|
+
continue;
|
|
1116
|
+
const i = normal(places === undefined ? Int.of(u) : new Decimal(Int.of(0), places).lift(u));
|
|
1117
|
+
updates[n] = i.pos;
|
|
1118
|
+
updates[`${n}$neg`] = i.neg;
|
|
1119
|
+
}
|
|
397
1120
|
const state = {}, maps = {};
|
|
398
1121
|
for (const [n, u] of Object.entries(updates)) {
|
|
399
1122
|
if (u === undefined)
|
|
@@ -401,20 +1124,30 @@ export class Rule {
|
|
|
401
1124
|
if (typeof u === 'object' && 'map' in u) {
|
|
402
1125
|
if (u.map !== n)
|
|
403
1126
|
throw new Error(`${s.name}: the step writes map ${u.map} under the name ${n}`);
|
|
404
|
-
maps[n] = { key: u.key, value: u.value };
|
|
1127
|
+
maps[n] = 'each' in u ? { each: u.each, value: hoist.term(u.value, [u.each]) } : { key: hoist.term(u.key), value: hoist.term(u.value) };
|
|
405
1128
|
}
|
|
406
1129
|
else
|
|
407
|
-
state[n] = termOf(u);
|
|
1130
|
+
state[n] = hoist.term(termOf(u));
|
|
408
1131
|
}
|
|
409
1132
|
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)];
|
|
1133
|
+
const assume = [...this.kindAssumptions('inputs'), ...s.assume.map(f => hoist.cond(f(env).c))];
|
|
1134
|
+
placed();
|
|
1135
|
+
// Functions, and their facts: each function's own (named after it), then the rule's.
|
|
1136
|
+
const fns = Object.fromEntries((s.fns ?? []).map(([n, d]) => [n, { js: fnSource(n, d), ...(d.lean ? { lean: d.lean } : {}) }]));
|
|
1137
|
+
const facts = [
|
|
1138
|
+
...(s.fns ?? []).flatMap(([n, d]) => Object.entries(d.facts?.(fnTerm(n, d)) ?? {}).map(([name, f]) => this.lawOf(`${n}: ${name}`, f))),
|
|
1139
|
+
...(s.facts ?? []).flatMap(f => Object.entries(f(pEnv)).map(([name, out]) => this.lawOf(name, out))),
|
|
1140
|
+
];
|
|
411
1141
|
const m = {
|
|
412
1142
|
name: s.name, params, inputs,
|
|
413
1143
|
state: Object.fromEntries(s.state.map(([n, init]) => [n, typeof init === 'number' ? termOf(init) : termOf(init(pEnv))])),
|
|
414
1144
|
...(s.maps.length ? { maps: Object.fromEntries(s.maps) } : {}),
|
|
1145
|
+
...(s.carried?.length ? { inputMaps: s.carried } : {}),
|
|
415
1146
|
step: { ...(Object.keys(flags).length ? { flags } : {}), ...(Object.keys(state).length ? { state } : {}), ...(Object.keys(maps).length ? { maps } : {}) },
|
|
416
1147
|
...(pre.length ? { pre: pre.length === 1 ? pre[0] : { op: 'and', xs: pre } } : {}),
|
|
417
1148
|
...(assume.length ? { assume: assume.length === 1 ? assume[0] : { op: 'and', xs: assume } } : {}),
|
|
1149
|
+
...(Object.keys(fns).length ? { fns } : {}),
|
|
1150
|
+
...(facts.length ? { facts } : {}),
|
|
418
1151
|
};
|
|
419
1152
|
if (s.record) {
|
|
420
1153
|
const kinds = new Map(s.inputs);
|
|
@@ -435,13 +1168,221 @@ export class Rule {
|
|
|
435
1168
|
}
|
|
436
1169
|
return m;
|
|
437
1170
|
}
|
|
1171
|
+
// --- the builders checked ------------------------------------------------------------------------------------------
|
|
1172
|
+
// The builders against what they built, on concrete runs: random params and requests by their kinds (`pre` and
|
|
1173
|
+
// `assume` met), and at every state each law's callback, each step law's, every flag and the step itself run again
|
|
1174
|
+
// with every value known, so each answer is the builders' own arithmetic (`val`, `ok`), against the expressions the
|
|
1175
|
+
// interpreter evaluates (core/watch.ts's verdicts, over the same keys). A law that says something other than its
|
|
1176
|
+
// callback, or a step that writes something other than it means, is reported with the state it shows at.
|
|
1177
|
+
crossCheck(laws, stepLaws = [], opts = {}) {
|
|
1178
|
+
const s = this.spec, m = this.machine, run = interpret(m), bound = opts.bound ?? 4, rand = opts.rand ?? seeded(7);
|
|
1179
|
+
let checked = 0;
|
|
1180
|
+
const at = (what, params, st, input) => `${what}: params ${JSON.stringify(params)}, state ${JSON.stringify(this.decode(st))}${input ? `, request ${JSON.stringify(numbersOf(input))}` : ''}`;
|
|
1181
|
+
const mapsOf = (st) => Object.fromEntries(Object.entries(st.maps).map(([n, cells]) => [n, { cells, dflt: st.defaults?.[n] ?? m.maps?.[n] ?? 0 }]));
|
|
1182
|
+
for (let trial = 0; trial < (opts.trials ?? 100); trial++) {
|
|
1183
|
+
const params = this.randomParams(rand, bound);
|
|
1184
|
+
if (m.pre && !evalC(m.pre, params, { scalars: {}, maps: {} }, m))
|
|
1185
|
+
continue;
|
|
1186
|
+
let st = run.init(params);
|
|
1187
|
+
for (let d = 0, tries = 0; d <= (opts.depth ?? 8) && tries < 200; tries++) {
|
|
1188
|
+
// The laws at this state.
|
|
1189
|
+
const keys = keysOf(bound, st), lawScope = { vars: { ...params, ...st.scalars }, maps: mapsOf(st), keys, flags: {} };
|
|
1190
|
+
for (const law of laws) {
|
|
1191
|
+
const f = sources.get(law)?.law;
|
|
1192
|
+
if (!f)
|
|
1193
|
+
continue;
|
|
1194
|
+
const ok = inScope(lawScope, () => this.verdict(f(this.env({ flags: false, inputs: false, queries: false })), keys));
|
|
1195
|
+
if (ok === undefined)
|
|
1196
|
+
continue;
|
|
1197
|
+
checked++;
|
|
1198
|
+
if (ok !== watch(m, run, law, params, bound).start(st))
|
|
1199
|
+
return { checked, failure: at(`law "${law.name}": its callback says ${ok}, the law proven says ${!ok}`, params, st) };
|
|
1200
|
+
}
|
|
1201
|
+
if (d === (opts.depth ?? 8))
|
|
1202
|
+
break;
|
|
1203
|
+
const input = this.randomInput(rand, bound);
|
|
1204
|
+
if (s.prune || !allowed(m, run, params, st, input))
|
|
1205
|
+
continue;
|
|
1206
|
+
d++;
|
|
1207
|
+
const next = run.step(params, st, input);
|
|
1208
|
+
if (!s.record) {
|
|
1209
|
+
const failure = this.crossStep(params, st, input, next, stepLaws, bound, () => checked++);
|
|
1210
|
+
if (failure)
|
|
1211
|
+
return { checked, failure: at(failure, params, st, input) };
|
|
1212
|
+
}
|
|
1213
|
+
st = next;
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
return { checked };
|
|
1217
|
+
}
|
|
1218
|
+
// A condition's value, or a law about every key's at every tuple of these keys (undefined when not every value is known).
|
|
1219
|
+
verdict(out, keys) {
|
|
1220
|
+
if (out instanceof Cond)
|
|
1221
|
+
return out.ok;
|
|
1222
|
+
const go = (i, picked) => (i === out.forall.length ? out.forall(...picked).ok : everyOk(keys, k => go(i + 1, [...picked, keyTerm(k)])));
|
|
1223
|
+
return go(0, []);
|
|
1224
|
+
}
|
|
1225
|
+
// One step: the flags, the updates and the step laws, each against what the interpreter computed.
|
|
1226
|
+
crossStep(params, st, input, next, stepLaws, bound, count) {
|
|
1227
|
+
const s = this.spec, m = this.machine, run = interpret(m), seen = carriedView(m, st, input), req = requestEnv(m, run, params, st, input);
|
|
1228
|
+
const keys = keysOf(bound, seen, next), vars = { ...params, ...st.scalars, ...req };
|
|
1229
|
+
const sc = { vars, maps: Object.fromEntries(Object.entries(seen.maps).map(([n, cells]) => [n, { cells, dflt: seen.defaults?.[n] ?? m.maps?.[n] ?? 0 }])), keys: exactKeys(keys, vars, seen, next), flags: {} };
|
|
1230
|
+
// Flags: every callback in order, each name's last definition winning, until the values settle (a name defined
|
|
1231
|
+
// again replaces the first in place, so the flags between read the new one).
|
|
1232
|
+
const names = Object.keys(m.step.flags ?? {});
|
|
1233
|
+
for (let pass = 0; pass <= s.flags.length; pass++) {
|
|
1234
|
+
const values = {};
|
|
1235
|
+
inScope(sc, () => {
|
|
1236
|
+
const env = this.env({ flags: false });
|
|
1237
|
+
for (const f of s.flags)
|
|
1238
|
+
for (const [n, c] of Object.entries(f(env))) {
|
|
1239
|
+
env[n] = new Cond({ op: 'flag', name: n }, sc.flags[n]);
|
|
1240
|
+
if (c.ok !== undefined)
|
|
1241
|
+
values[n] = c.ok;
|
|
1242
|
+
}
|
|
1243
|
+
});
|
|
1244
|
+
const settled = Object.keys(values).every(n => sc.flags[n] === values[n]);
|
|
1245
|
+
sc.flags = values;
|
|
1246
|
+
if (settled)
|
|
1247
|
+
break;
|
|
1248
|
+
}
|
|
1249
|
+
for (const n of names) {
|
|
1250
|
+
if (sc.flags[n] === undefined || n.startsWith('all$'))
|
|
1251
|
+
continue;
|
|
1252
|
+
count();
|
|
1253
|
+
if (sc.flags[n] !== (req[`?${n}`] === 1))
|
|
1254
|
+
return `flag ${n}: its callback says ${sc.flags[n]}, the step decides ${req[`?${n}`] === 1}`;
|
|
1255
|
+
}
|
|
1256
|
+
// The updates: each written value (an integer's or a decimal's as its count), every scalar not written kept, each map write.
|
|
1257
|
+
const updates = inScope(sc, () => s.step?.(this.env()) ?? {});
|
|
1258
|
+
const signed = new Map(s.signed ?? []);
|
|
1259
|
+
for (const n of Object.keys(st.scalars)) {
|
|
1260
|
+
if (n.endsWith('$neg') && signed.has(n.slice(0, -4)))
|
|
1261
|
+
continue;
|
|
1262
|
+
const u = updates[n], got = signed.has(n) ? next.scalars[n] - next.scalars[`${n}$neg`] : next.scalars[n];
|
|
1263
|
+
let want;
|
|
1264
|
+
if (u === undefined)
|
|
1265
|
+
want = signed.has(n) ? st.scalars[n] - st.scalars[`${n}$neg`] : st.scalars[n];
|
|
1266
|
+
else if (signed.has(n)) {
|
|
1267
|
+
const i = signed.get(n) === undefined ? Int.of(u) : new Decimal(Int.of(0), signed.get(n)).lift(u);
|
|
1268
|
+
want = both(i.pos.val, i.neg.val, (a, b) => a - b);
|
|
1269
|
+
}
|
|
1270
|
+
else
|
|
1271
|
+
want = valOf(u);
|
|
1272
|
+
if (want === undefined)
|
|
1273
|
+
continue;
|
|
1274
|
+
count();
|
|
1275
|
+
if (want !== got)
|
|
1276
|
+
return `the step writes ${n} = ${want} by its callback, ${got} by the machine`;
|
|
1277
|
+
}
|
|
1278
|
+
for (const [n] of s.maps) {
|
|
1279
|
+
const u = updates[n], cell = (k) => next.maps[n]?.get(k) ?? next.defaults?.[n] ?? m.maps[n];
|
|
1280
|
+
if (!u?.concrete)
|
|
1281
|
+
continue;
|
|
1282
|
+
if ('at' in u.concrete) {
|
|
1283
|
+
const f = u.concrete.at;
|
|
1284
|
+
for (const k of sc.keys) {
|
|
1285
|
+
const want = inScope(sc, () => valOf(f(keyTerm(k))));
|
|
1286
|
+
if (want === undefined)
|
|
1287
|
+
continue;
|
|
1288
|
+
count();
|
|
1289
|
+
if (want !== cell(k))
|
|
1290
|
+
return `the step writes ${n}[${k}] = ${want} by its callback, ${cell(k)} by the machine`;
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
else if (u.concrete.key !== undefined && u.concrete.value !== undefined) {
|
|
1294
|
+
count();
|
|
1295
|
+
if (u.concrete.value !== cell(u.concrete.key))
|
|
1296
|
+
return `the step writes ${n}[${u.concrete.key}] = ${u.concrete.value} by its callback, ${cell(u.concrete.key)} by the machine`;
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
// Step laws (one step each): `when` and `then` on the state before and after.
|
|
1300
|
+
for (const sl of stepLaws) {
|
|
1301
|
+
const src = sources.get(sl)?.step;
|
|
1302
|
+
if (!src || (sl.steps ?? 1) > 1)
|
|
1303
|
+
continue;
|
|
1304
|
+
const after = { ...sc, keys, vars: { ...sc.vars, ...Object.fromEntries(Object.entries(next.scalars).map(([n, x]) => [`${n}'`, x])) },
|
|
1305
|
+
maps: { ...sc.maps, ...Object.fromEntries(Object.entries(next.maps).map(([n, cells]) => [`${n}'`, { cells, dflt: next.defaults?.[n] ?? m.maps?.[n] ?? 0 }])) } };
|
|
1306
|
+
const ok = inScope(after, () => {
|
|
1307
|
+
const env = this.env(), afterEnv = {};
|
|
1308
|
+
for (const [n] of s.state)
|
|
1309
|
+
afterEnv[n] = v(`${n}'`);
|
|
1310
|
+
for (const [n] of s.sums)
|
|
1311
|
+
afterEnv[n] = v(`${n}'`);
|
|
1312
|
+
for (const [n, init] of s.maps)
|
|
1313
|
+
afterEnv[n] = new MapRef(`${n}'`, init);
|
|
1314
|
+
for (const n of s.lists ?? [])
|
|
1315
|
+
afterEnv[n] = new ListRef(`${n}'`, v(`${n}_len'`));
|
|
1316
|
+
for (const [n, places] of s.signed ?? []) {
|
|
1317
|
+
afterEnv[n] = this.signedValue(n, places, `${n}'`, `${n}$neg'`);
|
|
1318
|
+
delete afterEnv[`${n}$neg`];
|
|
1319
|
+
}
|
|
1320
|
+
const w = src.when ? src.when(env).ok : true;
|
|
1321
|
+
return w === undefined ? undefined : w ? this.verdict(src.then(env, afterEnv), keys) : true;
|
|
1322
|
+
});
|
|
1323
|
+
if (ok === undefined)
|
|
1324
|
+
continue;
|
|
1325
|
+
count();
|
|
1326
|
+
if (ok !== watch(m, run, sl, params, bound).step(st, input, next))
|
|
1327
|
+
return `step law "${sl.name}": its callback says ${ok}, the law proven says ${!ok}`;
|
|
1328
|
+
}
|
|
1329
|
+
return undefined;
|
|
1330
|
+
}
|
|
1331
|
+
// Against the code it describes, step by step: `reference.init(params)` and `reference.step(params, state, request)`,
|
|
1332
|
+
// with requests drawn by their kinds (or `draw`), must hold the same values as the compiled rule (decoded: integers and
|
|
1333
|
+
// decimals as numbers) for every field the reference's state has, after every step of `trials` runs of `depth`.
|
|
1334
|
+
compare(reference, opts = {}) {
|
|
1335
|
+
const s = this.spec, rand = opts.rand ?? seeded(5), bound = opts.bound ?? 100, gen = this.compiled();
|
|
1336
|
+
let checked = 0;
|
|
1337
|
+
const sample = (kinds) => Object.fromEntries(kinds.map(([n, k]) => [n, k.sample(rand, bound)]));
|
|
1338
|
+
for (let trial = 0; trial < (opts.trials ?? 200); trial++) {
|
|
1339
|
+
const params = (opts.params?.(rand) ?? sample(s.params)), ps = this.encodeParams(params);
|
|
1340
|
+
let ours = gen.init(ps), theirs = reference.init(params);
|
|
1341
|
+
const edges = trial === 0 ? opts.edges ?? [] : [];
|
|
1342
|
+
for (let d = 0; d < (opts.depth ?? 10) + edges.length; d++) {
|
|
1343
|
+
const request = edges[d] ?? opts.draw?.(rand) ?? sample(s.inputs);
|
|
1344
|
+
ours = gen.step(ps, ours, this.encodeInput(request));
|
|
1345
|
+
theirs = reference.step(params, theirs, request);
|
|
1346
|
+
const got = this.decode(ours);
|
|
1347
|
+
for (const [field, want] of Object.entries(theirs)) {
|
|
1348
|
+
checked++;
|
|
1349
|
+
if (got[field] !== want)
|
|
1350
|
+
return { checked, failure: `${s.name}, params ${JSON.stringify(params)}, after ${JSON.stringify(request)}: ${field} is ${got[field]} here, ${want} in the code it describes` };
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
return { checked };
|
|
1355
|
+
}
|
|
438
1356
|
// --- running it -----------------------------------------------------------------------------------------------------
|
|
439
1357
|
// Runtime values (numbers, booleans, choice names, null for a missing value) to the variables the machine reads.
|
|
440
1358
|
encodeParams(values) { return encode(this.spec.params, values); }
|
|
441
|
-
|
|
1359
|
+
// A carried map is a Set of keys (each cell 1), an array of them, or a Map of key to natural.
|
|
1360
|
+
encodeInput(values) {
|
|
1361
|
+
const out = encode(this.spec.inputs, values);
|
|
1362
|
+
for (const n of this.spec.carried ?? []) {
|
|
1363
|
+
const given = values[n];
|
|
1364
|
+
out[n] = given instanceof Map ? new Map(given) : new Map([...given].map(key => [key, 1]));
|
|
1365
|
+
}
|
|
1366
|
+
return out;
|
|
1367
|
+
}
|
|
442
1368
|
// Random params and inputs that meet their kinds (not the user's `pre` or `assume`, which a caller checks).
|
|
443
1369
|
randomParams(rand, bound) { return Object.assign({}, ...this.spec.params.map(([n, k]) => k.random(n, rand, bound))); }
|
|
444
|
-
randomInput(rand, bound) {
|
|
1370
|
+
randomInput(rand, bound) {
|
|
1371
|
+
const out = Object.assign({}, ...this.spec.inputs.map(([n, k]) => k.random(n, rand, bound)));
|
|
1372
|
+
for (const n of this.spec.carried ?? [])
|
|
1373
|
+
out[n] = new Map(Array.from({ length: bound + 1 }, (_, key) => [key, rand(2)]).filter(([, cell]) => cell === 1));
|
|
1374
|
+
return out;
|
|
1375
|
+
}
|
|
1376
|
+
// The state's scalars as values: an integer's two parts as one signed number, a decimal as its number.
|
|
1377
|
+
decode(st) {
|
|
1378
|
+
const out = { ...st.scalars };
|
|
1379
|
+
for (const [n, places] of this.spec.signed ?? []) {
|
|
1380
|
+
const x = (st.scalars[n] ?? 0) - (st.scalars[`${n}$neg`] ?? 0);
|
|
1381
|
+
out[n] = places === undefined ? x : x / 10 ** places;
|
|
1382
|
+
delete out[`${n}$neg`];
|
|
1383
|
+
}
|
|
1384
|
+
return out;
|
|
1385
|
+
}
|
|
445
1386
|
// The prune order's variable, if the history is pruned (a runtime must send it nondecreasing).
|
|
446
1387
|
get orderedBy() { return this.spec.prune ? new Map(this.spec.inputs).get(this.spec.prune.order).vars(this.spec.prune.order)[0] : undefined; }
|
|
447
1388
|
interpreter() { return interpret(this.machine); }
|
|
@@ -451,6 +1392,73 @@ export class Rule {
|
|
|
451
1392
|
}
|
|
452
1393
|
const encode = (kinds, values) => Object.assign({}, ...kinds.map(([n, k]) => k.encode(n, values[n])));
|
|
453
1394
|
export const rule = (name) => new Rule({ name, params: [], inputs: [], state: [], maps: [], flags: [], pre: [], assume: [], sums: [], queries: [] });
|
|
1395
|
+
export class Fun {
|
|
1396
|
+
r;
|
|
1397
|
+
out;
|
|
1398
|
+
constructor(r, out) {
|
|
1399
|
+
this.r = r;
|
|
1400
|
+
this.out = out;
|
|
1401
|
+
}
|
|
1402
|
+
get name() { return this.r.name; }
|
|
1403
|
+
params(kinds) { return new Fun(this.r.params(kinds), this.out); }
|
|
1404
|
+
inputs(kinds) { return new Fun(this.r.inputs(kinds), this.out); }
|
|
1405
|
+
fns(defs) { return new Fun(this.r.fns(defs), this.out); }
|
|
1406
|
+
facts(f) { return new Fun(this.r.facts(f), this.out); }
|
|
1407
|
+
pre(f) { return new Fun(this.r.pre(f), this.out); }
|
|
1408
|
+
assume(f) { return new Fun(this.r.assume(f), this.out); }
|
|
1409
|
+
// The result (replaces an earlier one, so a variant is one call away).
|
|
1410
|
+
returns(f) { return new Fun(this.r, f); }
|
|
1411
|
+
result(env) {
|
|
1412
|
+
if (!this.out)
|
|
1413
|
+
throw new Error(`${this.name}: no result (returns)`);
|
|
1414
|
+
return Object.fromEntries(Object.entries(this.out(env)).map(([k, t]) => [k, t instanceof Cond || t instanceof Int || t instanceof Decimal ? t : new Term(termOf(t))]));
|
|
1415
|
+
}
|
|
1416
|
+
// Whenever the arguments (and the result) meet `given`, the result meets `then`.
|
|
1417
|
+
law(name, given, then) {
|
|
1418
|
+
return this.r.decide(name, env => given(env, this.result(env)), env => then(this.result(env), env));
|
|
1419
|
+
}
|
|
1420
|
+
// A law about two calls under the same params: `then(outA, outB, a, b)` given `given(a, b)` (noninterference, say).
|
|
1421
|
+
relate(name, given, then) {
|
|
1422
|
+
return this.r.relate(name, given, (a, b) => then(this.result(a), this.result(b), a, b));
|
|
1423
|
+
}
|
|
1424
|
+
// Against the code it describes: `reference` called with the same arguments (drawn by their kinds, or by `draw`, the
|
|
1425
|
+
// `edges` first) must return the same value for every field it returns (a field it leaves out is not compared). The
|
|
1426
|
+
// check for an encoding: that the terms here are what the real function computes, on its real inputs.
|
|
1427
|
+
// cap.compare(({left, local}) => ({out: realCap(left, local)}), {edges: [{left: 2 ** 32 + 5, local: 10}]})
|
|
1428
|
+
compare(reference, opts = {}) {
|
|
1429
|
+
const rand = opts.rand ?? seeded(5), bound = opts.bound ?? 1000, params = (opts.params ?? {}), kinds = this.r['spec'].inputs;
|
|
1430
|
+
let checked = 0;
|
|
1431
|
+
const draws = [...(opts.edges ?? []), ...Array.from({ length: opts.trials ?? 2000 }, () => opts.draw?.(rand) ?? Object.fromEntries(kinds.map(([n, k]) => [n, k.sample(rand, bound)])))];
|
|
1432
|
+
for (const values of draws) {
|
|
1433
|
+
const ours = this.run(values, params), theirs = reference(values, params);
|
|
1434
|
+
for (const [field, want] of Object.entries(theirs)) {
|
|
1435
|
+
checked++;
|
|
1436
|
+
if (ours[field] !== want)
|
|
1437
|
+
return { checked, failure: `${this.name} at ${JSON.stringify(values)}: ${field} is ${String(ours[field])} here, ${String(want)} in the code it describes` };
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
return { checked };
|
|
1441
|
+
}
|
|
1442
|
+
// The one-step rule it compiles to: the result's fields are its state (named out.<field>, apart from the arguments).
|
|
1443
|
+
get rule() {
|
|
1444
|
+
const fields = Object.entries(this.result(this.r['env']({ flags: false, state: false, queries: false })));
|
|
1445
|
+
const name = (k) => `out.${k}`, of = (pick) => Object.fromEntries(fields.filter(([, t]) => pick(t)).map(([k]) => [name(k), 0]));
|
|
1446
|
+
let r = this.r.state(of(t => t instanceof Term || t instanceof Cond)).ints(of(t => t instanceof Int));
|
|
1447
|
+
for (const places of new Set(fields.flatMap(([, t]) => (t instanceof Decimal ? [t.places] : []))))
|
|
1448
|
+
r = r.decimals(places, of(t => t instanceof Decimal && t.places === places));
|
|
1449
|
+
return r.step(env => Object.fromEntries(Object.entries(this.result(env)).map(([k, t]) => [name(k), t instanceof Cond ? count(t) : t])));
|
|
1450
|
+
}
|
|
1451
|
+
compiledRule;
|
|
1452
|
+
// The result at these arguments (and params), from the compiled rule: numbers, and booleans for the conditions.
|
|
1453
|
+
run(values, params = {}) {
|
|
1454
|
+
this.compiledRule ??= (rule => ({ rule, gen: rule.compiled() }))(this.rule);
|
|
1455
|
+
const { rule, gen } = this.compiledRule, ps = rule.encodeParams(params);
|
|
1456
|
+
const kinds = Object.entries(this.result(this.r['env']({ flags: false, state: false, queries: false })));
|
|
1457
|
+
const st = rule.decode(gen.step(ps, gen.init(ps), rule.encodeInput(values)));
|
|
1458
|
+
return Object.fromEntries(kinds.map(([k, t]) => [k, t instanceof Cond ? st[`out.${k}`] === 1 : st[`out.${k}`]]));
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
export const fun = (name) => new Fun(rule(name));
|
|
454
1462
|
// A loop runs `body` while `while` holds, each turn with fresh inputs; it halts if a rank over the state and params
|
|
455
1463
|
// (or a tuple of them, compared in order) drops on every turn.
|
|
456
1464
|
export class LoopRule {
|
|
@@ -509,18 +1517,48 @@ const goalsVerdict = (goals) => {
|
|
|
509
1517
|
for (const g of goals) {
|
|
510
1518
|
const out = grow(g);
|
|
511
1519
|
if (!('tree' in out))
|
|
512
|
-
return { proven: false, checked: false, goal: g.name, stuck: out.stuck, ...(out.witness ? { witness: out.witness } : {}) };
|
|
1520
|
+
return { proven: false, checked: false, goal: g.name, stuck: out.stuck, ...(out.witness ? { witness: out.witness } : {}), ...(out.why ? { why: out.why } : {}) };
|
|
513
1521
|
trees[g.name] = out.tree;
|
|
514
1522
|
}
|
|
515
1523
|
return { proven: true, checked: goals.every(g => check(g, trees[g.name])), trees };
|
|
516
1524
|
};
|
|
517
1525
|
export function proveRule(r, laws) {
|
|
518
1526
|
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 } : {}) } : {}) };
|
|
1527
|
+
return { proven: out.proven, checked: out.checked, ...(out.failed ? { goal: out.failed.goal, stuck: out.failed.stuck, ...(out.failed.witness ? { witness: out.failed.witness } : {}), ...(out.failed.why ? { why: out.failed.why } : {}) } : {}) };
|
|
520
1528
|
}
|
|
521
1529
|
export function proveSteps(r, laws, stepLaws) {
|
|
522
1530
|
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 } : {}) } : {}) };
|
|
1531
|
+
return { proven: out.proven, checked: out.checked, ...(out.failed ? { goal: out.failed.goal, stuck: out.failed.stuck, ...(out.failed.witness ? { witness: out.failed.witness } : {}), ...(out.failed.why ? { why: out.failed.why } : {}) } : {}) };
|
|
1532
|
+
}
|
|
1533
|
+
export function proveLeadsTo(r, laws, l) {
|
|
1534
|
+
const out = proveLiveness(r.machine, laws, l);
|
|
1535
|
+
return { proven: out.proven, checked: out.checked, ...(out.failed ? { goal: out.failed.goal, stuck: out.failed.stuck, ...(out.failed.witness ? { witness: out.failed.witness } : {}), ...(out.failed.why ? { why: out.failed.why } : {}) } : {}) };
|
|
524
1536
|
}
|
|
525
1537
|
export const proveDecision = (d) => goalsVerdict([d.goal]);
|
|
1538
|
+
const seeded = (seed) => { let x = seed; return k => { x = (x * 1103515245 + 12345) % 2 ** 31; return Math.floor(x / 2 ** 31 * k); }; };
|
|
1539
|
+
// A decision's callbacks against its condition, on random params and requests: `given` and `then` each run with every
|
|
1540
|
+
// value known, against what the interpreter evaluates of the decision built from them.
|
|
1541
|
+
export function crossCheckDecision(d, opts = {}) {
|
|
1542
|
+
const src = sources.get(d)?.decision;
|
|
1543
|
+
if (!src)
|
|
1544
|
+
return { checked: 0 };
|
|
1545
|
+
const r = d.rule, m = r.machine, bound = opts.bound ?? 4, rand = opts.rand ?? seeded(11), own = d.given.slice(d.given.length - src.own);
|
|
1546
|
+
let checked = 0;
|
|
1547
|
+
for (let i = 0; i < (opts.trials ?? 500); i++) {
|
|
1548
|
+
const input = r.randomInput(rand, bound), values = { ...r.randomParams(rand, bound), ...numbersOf(input) };
|
|
1549
|
+
const st = { scalars: {}, maps: Object.fromEntries(Object.entries(input).filter((e) => typeof e[1] !== 'number').map(([n, cells]) => [n, new Map(cells)])) };
|
|
1550
|
+
const keys = exactKeys(keysOf(bound, st), values, st);
|
|
1551
|
+
const sc = { vars: values, maps: Object.fromEntries(Object.entries(st.maps).map(([n, cells]) => [n, { cells, dflt: 0 }])), keys, flags: {} };
|
|
1552
|
+
const [g, t] = inScope(sc, () => { const env = r['env']({ flags: false, state: false, queries: false }), g = src.given(env); return [all(...(Array.isArray(g) ? g : [g])).ok, src.then(env).ok]; });
|
|
1553
|
+
for (const [what, ok, c] of [['given', g, own], ['then', t, [d.then]]]) {
|
|
1554
|
+
if (ok === undefined)
|
|
1555
|
+
continue;
|
|
1556
|
+
checked++;
|
|
1557
|
+
const want = c.every(x => evalC(x, values, st, m));
|
|
1558
|
+
if (ok !== want)
|
|
1559
|
+
return { checked, failure: `decision "${d.name}": its ${what} callback says ${ok}, the decision proven says ${want}, at ${JSON.stringify(values)}` };
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
return { checked };
|
|
1563
|
+
}
|
|
526
1564
|
export const proveLoop = (l, laws) => goalsVerdict(loopObligations(l.loop, laws, l.rankTerms));
|