gdp-wired 0.3.0 → 0.5.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 +109 -113
- package/dist/core/dsl.d.ts +210 -9
- package/dist/core/dsl.js +799 -78
- package/dist/core/watch.d.ts +14 -0
- package/dist/core/watch.js +78 -0
- package/dist/kernel/kernel.d.ts +7 -0
- package/dist/kernel/kernel.js +121 -11
- package/dist/kernel/prove.d.ts +3 -0
- package/dist/kernel/prove.js +190 -51
- package/dist/lean-kernel/emit.d.ts +2 -10
- package/dist/lean-kernel/emit.js +164 -201
- package/dist/lean-kernel/library.js +5 -5
- package/package.json +2 -2
package/dist/core/dsl.js
CHANGED
|
@@ -15,64 +15,224 @@
|
|
|
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 { compileFunction, 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, proveLiveness, 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 Cond && otherwise instanceof Cond)
|
|
86
|
+
return choose(c, then, otherwise);
|
|
87
|
+
if (then instanceof Choice && otherwise instanceof Choice)
|
|
88
|
+
return new Choice(when(c, then.n, otherwise.n), then.values);
|
|
89
|
+
if (then instanceof Str && otherwise instanceof Str)
|
|
90
|
+
return then.choose(c, otherwise);
|
|
91
|
+
if (then instanceof Maybe && otherwise instanceof Maybe)
|
|
92
|
+
return new Maybe(when(c, then.present, otherwise.present), when(c, then.inner, otherwise.inner));
|
|
93
|
+
return pickNumbers(c, then, otherwise);
|
|
94
|
+
}
|
|
95
|
+
function pickNumbers(c, then, otherwise) {
|
|
96
|
+
if (then instanceof Decimal || otherwise instanceof Decimal) {
|
|
97
|
+
const places = (then instanceof Decimal ? then : otherwise).places, d = new Decimal(Int.of(0), places);
|
|
98
|
+
return new Decimal(when(c, d.lift(then), d.lift(otherwise)), places);
|
|
99
|
+
}
|
|
100
|
+
if (then instanceof Int || otherwise instanceof Int || (typeof then === 'number' && then < 0) || (typeof otherwise === 'number' && otherwise < 0)) {
|
|
101
|
+
const a = Int.of(then), b = Int.of(otherwise);
|
|
102
|
+
return new Int(when(c, a.pos, b.pos), when(c, a.neg, b.neg));
|
|
103
|
+
}
|
|
104
|
+
return new Term({ op: 'if', c: c.c, then: termOf(then), else: termOf(otherwise) }, c.ok === undefined ? undefined : valOf(c.ok ? then : otherwise));
|
|
105
|
+
}
|
|
66
106
|
// A condition by cases: `then` where c holds, `otherwise` elsewhere.
|
|
67
107
|
export const choose = (c, then, otherwise) => any(all(c, then), all(c.not(), otherwise));
|
|
68
108
|
// 1 where c holds, else 0: what a sum over a history adds per record.
|
|
69
109
|
export const count = (c) => when(c, 1, 0);
|
|
70
110
|
export const sum = (first, ...rest) => rest.reduce((acc, t) => acc.plus(t), typeof first === 'number' ? lit(first) : first);
|
|
71
|
-
|
|
72
|
-
|
|
111
|
+
const named = (at, n) => (typeof at === 'string' ? at + n : at(n));
|
|
112
|
+
const primed = (n) => `${n}'`;
|
|
73
113
|
export class Kind {
|
|
114
|
+
// A runtime value of the kind (what encode takes), for comparing a rule with the code it describes (compare).
|
|
115
|
+
sample(_rand, _bound) { throw new Error('this kind draws no runtime values'); }
|
|
116
|
+
// A state variable of this kind starting at a runtime value, or at a value over the params: `.state({open: bool.from(true)})`.
|
|
117
|
+
from(init) { return { kind: this, init }; }
|
|
118
|
+
}
|
|
119
|
+
const v = (name) => new Term({ op: 'var', name }, scope?.vars[name]);
|
|
120
|
+
let scope;
|
|
121
|
+
const inScope = (s, f) => { const outer = scope; scope = s; try {
|
|
122
|
+
return f();
|
|
123
|
+
}
|
|
124
|
+
finally {
|
|
125
|
+
scope = outer;
|
|
126
|
+
} };
|
|
127
|
+
const keyTerm = (k) => new Term({ op: 'const', value: k }, k);
|
|
128
|
+
// Keys enough to decide a condition at every key as the interpreter does: these, the key past every map's cells, and
|
|
129
|
+
// every key up to the values in play (where a key compared with them changes).
|
|
130
|
+
const exactKeys = (keys, values, ...states) => {
|
|
131
|
+
let past = 0;
|
|
132
|
+
for (const st of states)
|
|
133
|
+
for (const cells of Object.values(st.maps))
|
|
134
|
+
for (const k of cells.keys())
|
|
135
|
+
past = Math.max(past, k + 1);
|
|
136
|
+
const top = Math.min(64, Math.max(0, ...Object.values(values).filter(x => x <= 64)) + 2);
|
|
137
|
+
return [...new Set([...keys, past, ...Array.from({ length: top + 1 }, (_, i) => i)])].sort((a, b) => a - b);
|
|
138
|
+
};
|
|
139
|
+
const everyOk = (ks, f) => {
|
|
140
|
+
if (!ks)
|
|
141
|
+
return undefined;
|
|
142
|
+
let out = true;
|
|
143
|
+
for (const k of ks) {
|
|
144
|
+
const o = f(k);
|
|
145
|
+
if (o === undefined)
|
|
146
|
+
return undefined;
|
|
147
|
+
if (!o)
|
|
148
|
+
out = false;
|
|
149
|
+
}
|
|
150
|
+
return out;
|
|
151
|
+
};
|
|
152
|
+
const isZero = (t) => t.e.op === 'const' && t.e.value === 0;
|
|
153
|
+
const add = (a, b) => (isZero(b) ? a : isZero(a) ? b : a.plus(b));
|
|
154
|
+
export class Int {
|
|
155
|
+
pos;
|
|
156
|
+
neg;
|
|
157
|
+
constructor(pos, neg) {
|
|
158
|
+
this.pos = pos;
|
|
159
|
+
this.neg = neg;
|
|
160
|
+
}
|
|
161
|
+
static of(x) {
|
|
162
|
+
if (x instanceof Int)
|
|
163
|
+
return x;
|
|
164
|
+
if (typeof x !== 'number')
|
|
165
|
+
return new Int(x, lit(0));
|
|
166
|
+
if (!Number.isSafeInteger(x))
|
|
167
|
+
throw new RangeError(`${x} is not an integer`);
|
|
168
|
+
return x >= 0 ? new Int(lit(x), lit(0)) : new Int(lit(0), lit(-x));
|
|
169
|
+
}
|
|
170
|
+
plus(b) { const o = Int.of(b); return new Int(add(this.pos, o.pos), add(this.neg, o.neg)); }
|
|
171
|
+
minus(b) { const o = Int.of(b); return new Int(add(this.pos, o.neg), add(this.neg, o.pos)); }
|
|
172
|
+
negate() { return new Int(this.neg, this.pos); }
|
|
173
|
+
times(k) {
|
|
174
|
+
if (!Number.isSafeInteger(k))
|
|
175
|
+
throw new RangeError(`${k} is not an integer`);
|
|
176
|
+
return k >= 0 ? new Int(this.pos.times(k), this.neg.times(k)) : new Int(this.neg.times(-k), this.pos.times(-k));
|
|
177
|
+
}
|
|
178
|
+
lt(b) { const o = Int.of(b); return add(this.pos, o.neg).lt(add(o.pos, this.neg)); }
|
|
179
|
+
atMost(b) { const o = Int.of(b); return add(this.pos, o.neg).atMost(add(o.pos, this.neg)); }
|
|
180
|
+
gt(b) { return Int.of(b).lt(this); }
|
|
181
|
+
atLeast(b) { return Int.of(b).atMost(this); }
|
|
182
|
+
eq(b) { const o = Int.of(b); return add(this.pos, o.neg).eq(add(o.pos, this.neg)); }
|
|
183
|
+
ne(b) { return this.eq(b).not(); }
|
|
184
|
+
min(b) { return when(this.atMost(b), this, b); }
|
|
185
|
+
max(b) { return when(this.atLeast(b), this, b); }
|
|
186
|
+
get isNegative() { return this.neg.gt(this.pos); }
|
|
187
|
+
// |value|, and the value where it is not negative (0 below).
|
|
188
|
+
get abs() { return this.pos.minus(this.neg).plus(this.neg.minus(this.pos)); }
|
|
189
|
+
get clamped() { return this.pos.minus(this.neg); }
|
|
190
|
+
}
|
|
191
|
+
// The same value with at most one part nonzero (what a state variable holds).
|
|
192
|
+
const normal = (i) => (isZero(i.neg) || isZero(i.pos) ? i : new Int(i.pos.minus(i.neg), i.neg.minus(i.pos)));
|
|
193
|
+
const scaledOf = (x, places, what = 'a decimal') => {
|
|
194
|
+
const text = typeof x === 'number' ? x.toFixed(places + 3) : x.trim();
|
|
195
|
+
const m = /^(-?)(\d+)(?:\.(\d+))?$/.exec(text);
|
|
196
|
+
if (!m)
|
|
197
|
+
throw new RangeError(`${String(x)} is not ${what}`);
|
|
198
|
+
const frac = (m[3] ?? '').replace(/0+$/, '');
|
|
199
|
+
if (frac.length > places)
|
|
200
|
+
throw new RangeError(`${String(x)} needs more than ${places} decimal places`);
|
|
201
|
+
const n = Number(m[2]) * 10 ** places + Number(frac.padEnd(places, '0') || '0');
|
|
202
|
+
if (!Number.isSafeInteger(n))
|
|
203
|
+
throw new RangeError(`${String(x)} is too large`);
|
|
204
|
+
return m[1] && n !== 0 ? -n : n;
|
|
205
|
+
};
|
|
206
|
+
export class Decimal {
|
|
207
|
+
int;
|
|
208
|
+
places;
|
|
209
|
+
constructor(int, places) {
|
|
210
|
+
this.int = int;
|
|
211
|
+
this.places = places;
|
|
212
|
+
}
|
|
213
|
+
lift(b) {
|
|
214
|
+
if (b instanceof Decimal) {
|
|
215
|
+
if (b.places !== this.places)
|
|
216
|
+
throw new Error(`decimals with ${this.places} and ${b.places} places don't mix`);
|
|
217
|
+
return b.int;
|
|
218
|
+
}
|
|
219
|
+
return typeof b === 'number' ? Int.of(scaledOf(b, this.places)) : Int.of(b).times(10 ** this.places);
|
|
220
|
+
}
|
|
221
|
+
with(i) { return new Decimal(i, this.places); }
|
|
222
|
+
plus(b) { return this.with(this.int.plus(this.lift(b))); }
|
|
223
|
+
minus(b) { return this.with(this.int.minus(this.lift(b))); }
|
|
224
|
+
negate() { return this.with(this.int.negate()); }
|
|
225
|
+
times(k) { return this.with(this.int.times(k)); }
|
|
226
|
+
lt(b) { return this.int.lt(this.lift(b)); }
|
|
227
|
+
atMost(b) { return this.int.atMost(this.lift(b)); }
|
|
228
|
+
gt(b) { return this.int.gt(this.lift(b)); }
|
|
229
|
+
atLeast(b) { return this.int.atLeast(this.lift(b)); }
|
|
230
|
+
eq(b) { return this.int.eq(this.lift(b)); }
|
|
231
|
+
ne(b) { return this.int.ne(this.lift(b)); }
|
|
232
|
+
min(b) { return this.with(this.int.min(this.lift(b))); }
|
|
233
|
+
max(b) { return this.with(this.int.max(this.lift(b))); }
|
|
234
|
+
get isNegative() { return this.int.isNegative; }
|
|
74
235
|
}
|
|
75
|
-
const v = (name) => new Term({ op: 'var', name });
|
|
76
236
|
const asNat = (name, raw) => {
|
|
77
237
|
if (typeof raw !== 'number' || !Number.isSafeInteger(raw) || raw < 0)
|
|
78
238
|
throw new RangeError(`${name} must be a natural, not ${String(raw)}`);
|
|
@@ -86,7 +246,10 @@ class NatKind extends Kind {
|
|
|
86
246
|
}
|
|
87
247
|
vars(name) { return [name]; }
|
|
88
248
|
assume(name) { return this.maxValue === undefined ? [] : [v(name).atMost(this.maxValue)]; }
|
|
89
|
-
value(name,
|
|
249
|
+
value(name, at = '') { return v(named(at, name)); }
|
|
250
|
+
store(name, value) { return { [name]: value }; }
|
|
251
|
+
decode(name, vars) { return vars[name]; }
|
|
252
|
+
empty() { return 0; }
|
|
90
253
|
encode(name, raw) {
|
|
91
254
|
const n = asNat(name, raw);
|
|
92
255
|
if (this.maxValue !== undefined && n > this.maxValue)
|
|
@@ -94,6 +257,7 @@ class NatKind extends Kind {
|
|
|
94
257
|
return { [name]: n };
|
|
95
258
|
}
|
|
96
259
|
random(name, rand, bound) { return { [name]: rand((this.maxValue ?? bound) + 1) }; }
|
|
260
|
+
sample(rand, bound) { return this.maxValue !== undefined && rand(8) === 0 ? this.maxValue : rand((this.maxValue ?? bound) + 1); }
|
|
97
261
|
}
|
|
98
262
|
export const nat = new NatKind();
|
|
99
263
|
// A natural known to be at most `maxValue` (a percentage, a floor number, a weekday).
|
|
@@ -102,14 +266,17 @@ export const upTo = (maxValue) => new NatKind(naturalConst(maxValue));
|
|
|
102
266
|
export class Bool extends Cond {
|
|
103
267
|
n;
|
|
104
268
|
constructor(n) {
|
|
105
|
-
super({ op: '=', a: n.e, b: { op: 'const', value: 1 } });
|
|
269
|
+
super({ op: '=', a: n.e, b: { op: 'const', value: 1 } }, n.val === undefined ? undefined : n.val === 1);
|
|
106
270
|
this.n = n;
|
|
107
271
|
}
|
|
108
272
|
}
|
|
109
273
|
class BoolKind extends Kind {
|
|
110
274
|
vars(name) { return [name]; }
|
|
111
275
|
assume(name) { return [v(name).atMost(1)]; }
|
|
112
|
-
value(name,
|
|
276
|
+
value(name, at = '') { return new Bool(v(named(at, name))); }
|
|
277
|
+
store(name, value) { return { [name]: typeof value === 'boolean' ? (value ? 1 : 0) : count(value) }; }
|
|
278
|
+
decode(name, vars) { return vars[name] === 1; }
|
|
279
|
+
empty() { return false; }
|
|
113
280
|
encode(name, raw) {
|
|
114
281
|
if (typeof raw === 'boolean')
|
|
115
282
|
return { [name]: raw ? 1 : 0 };
|
|
@@ -118,6 +285,7 @@ class BoolKind extends Kind {
|
|
|
118
285
|
throw new RangeError(`${name} must be a boolean, not ${String(raw)}`);
|
|
119
286
|
}
|
|
120
287
|
random(name, rand) { return { [name]: rand(2) }; }
|
|
288
|
+
sample(rand) { return rand(2) === 1; }
|
|
121
289
|
}
|
|
122
290
|
export const bool = new BoolKind();
|
|
123
291
|
// One of a few named values, encoded as its index. `match` takes every case (TypeScript checks none is missing).
|
|
@@ -154,7 +322,10 @@ class OneOfKind extends Kind {
|
|
|
154
322
|
}
|
|
155
323
|
vars(name) { return [name]; }
|
|
156
324
|
assume(name) { return [v(name).lt(this.values.length)]; }
|
|
157
|
-
value(name,
|
|
325
|
+
value(name, at = '') { return new Choice(v(named(at, name)), this.values); }
|
|
326
|
+
store(name, value) { return { [name]: value instanceof Choice ? value.n : this.encode(name, value)[name] }; }
|
|
327
|
+
decode(name, vars) { return this.values[vars[name]]; }
|
|
328
|
+
empty() { return this.values[0]; }
|
|
158
329
|
encode(name, raw) {
|
|
159
330
|
const i = this.values.indexOf(raw);
|
|
160
331
|
if (i < 0)
|
|
@@ -162,6 +333,7 @@ class OneOfKind extends Kind {
|
|
|
162
333
|
return { [name]: i };
|
|
163
334
|
}
|
|
164
335
|
random(name, rand) { return { [name]: rand(this.values.length) }; }
|
|
336
|
+
sample(rand) { return this.values[rand(this.values.length)]; }
|
|
165
337
|
}
|
|
166
338
|
export const oneOf = (...values) => new OneOfKind(values);
|
|
167
339
|
// A value that may be missing, encoded as a presence bit and the value. The value can only be read in `some`, so no
|
|
@@ -194,7 +366,17 @@ class MaybeKind extends Kind {
|
|
|
194
366
|
has(name) { return `${name}$some`; }
|
|
195
367
|
vars(name) { return [this.has(name), ...this.of.vars(name)]; }
|
|
196
368
|
assume(name) { return [v(this.has(name)).atMost(1), ...this.of.assume(name)]; }
|
|
197
|
-
value(name,
|
|
369
|
+
value(name, at = '') { return new Maybe(v(named(at, this.has(name))), this.of.value(name, at)); }
|
|
370
|
+
// null for none, a Maybe as it is, any other value as some.
|
|
371
|
+
store(name, value) {
|
|
372
|
+
if (value === null || value === undefined)
|
|
373
|
+
return { [this.has(name)]: 0, ...this.of.store(name, this.of.empty()) };
|
|
374
|
+
if (value instanceof Maybe)
|
|
375
|
+
return { [this.has(name)]: value.present, ...this.of.store(name, value.inner) };
|
|
376
|
+
return { [this.has(name)]: 1, ...this.of.store(name, value) };
|
|
377
|
+
}
|
|
378
|
+
decode(name, vars) { return vars[this.has(name)] === 1 ? this.of.decode(name, vars) : null; }
|
|
379
|
+
empty() { return null; }
|
|
198
380
|
encode(name, raw) {
|
|
199
381
|
if (raw === null || raw === undefined)
|
|
200
382
|
return { [this.has(name)]: 0, ...Object.fromEntries(this.of.vars(name).map(n => [n, 0])) };
|
|
@@ -203,32 +385,204 @@ class MaybeKind extends Kind {
|
|
|
203
385
|
random(name, rand, bound) {
|
|
204
386
|
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
387
|
}
|
|
388
|
+
sample(rand, bound) { return rand(4) === 0 ? null : this.of.sample(rand, bound); }
|
|
206
389
|
}
|
|
207
390
|
export const maybe = (of) => new MaybeKind(of);
|
|
391
|
+
// A string, encoded as its interned name, its length and one bit per pattern the kind declares (`like`, Cedar's glob:
|
|
392
|
+
// `*` any run of characters, `\*` a star): all computed from the string itself when the input is encoded, so the bits
|
|
393
|
+
// are the string's, not an answer someone supplies. Names come from one table per process, the empty string 0; a law
|
|
394
|
+
// compares strings by name. Equal strings have equal lengths and pattern bits, and `eq` says so, with a literal or with
|
|
395
|
+
// another string, so a law may reason from either. (Names are this process's: state that keeps them across processes
|
|
396
|
+
// keeps `interned()` with them, and `intern.load` restores it before any rule is built.)
|
|
397
|
+
const internTable = new Map([['', 0]]);
|
|
398
|
+
// The strings rules and laws name as literals (what random draws favor).
|
|
399
|
+
const literals = new Set(['']);
|
|
400
|
+
const literal = (text) => (literals.add(text), intern(text));
|
|
401
|
+
export const intern = Object.assign((text) => {
|
|
402
|
+
let id = internTable.get(text);
|
|
403
|
+
if (id === undefined)
|
|
404
|
+
internTable.set(text, (id = internTable.size));
|
|
405
|
+
return id;
|
|
406
|
+
}, { load(entries) { for (const [text, id] of entries) {
|
|
407
|
+
if (internTable.has(text) && internTable.get(text) !== id)
|
|
408
|
+
throw new Error(`"${text}" is interned as ${internTable.get(text)} already`);
|
|
409
|
+
internTable.set(text, id);
|
|
410
|
+
} } });
|
|
411
|
+
export const interned = () => [...internTable];
|
|
412
|
+
export const globMatch = (pattern, text) => {
|
|
413
|
+
const parts = [[]];
|
|
414
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
415
|
+
if (pattern[i] === '\\' && pattern[i + 1] === '*') {
|
|
416
|
+
parts.at(-1).push('*');
|
|
417
|
+
i++;
|
|
418
|
+
}
|
|
419
|
+
else if (pattern[i] === '*')
|
|
420
|
+
parts.push([]);
|
|
421
|
+
else
|
|
422
|
+
parts.at(-1).push(pattern[i]);
|
|
423
|
+
}
|
|
424
|
+
const lits = parts.map(p => p.join(''));
|
|
425
|
+
if (lits.length === 1)
|
|
426
|
+
return text === lits[0];
|
|
427
|
+
if (!text.startsWith(lits[0]) || !text.endsWith(lits.at(-1)) || text.length < lits[0].length + lits.at(-1).length)
|
|
428
|
+
return false;
|
|
429
|
+
let at = lits[0].length;
|
|
430
|
+
const end = text.length - lits.at(-1).length;
|
|
431
|
+
for (const lit of lits.slice(1, -1)) {
|
|
432
|
+
const i = text.indexOf(lit, at);
|
|
433
|
+
if (i < 0 || i + lit.length > end)
|
|
434
|
+
return false;
|
|
435
|
+
at = i + lit.length;
|
|
436
|
+
}
|
|
437
|
+
return true;
|
|
438
|
+
};
|
|
439
|
+
export class Str {
|
|
440
|
+
id;
|
|
441
|
+
length;
|
|
442
|
+
bits;
|
|
443
|
+
constructor(id, length, bits) {
|
|
444
|
+
this.id = id;
|
|
445
|
+
this.length = length;
|
|
446
|
+
this.bits = bits;
|
|
447
|
+
}
|
|
448
|
+
get patterns() { return [...this.bits.keys()]; }
|
|
449
|
+
// This string where c holds, else `other` (the patterns both declare).
|
|
450
|
+
choose(c, other) { return new Str(when(c, this.id, other.id), when(c, this.length, other.length), new Map([...this.bits].filter(([p]) => other.bits.has(p)).map(([p, b]) => [p, when(c, b, other.bits.get(p))]))); }
|
|
451
|
+
// The string matches `pattern`, one the kind declared.
|
|
452
|
+
like(pattern) {
|
|
453
|
+
const b = this.bits.get(pattern);
|
|
454
|
+
if (!b)
|
|
455
|
+
throw new Error(`like("${pattern}"): declare the pattern on the kind (str({like: [...]})); declared: ${[...this.bits.keys()].join(', ') || 'none'}`);
|
|
456
|
+
return b.eq(1);
|
|
457
|
+
}
|
|
458
|
+
get isEmpty() { return this.id.eq(0); }
|
|
459
|
+
// A declared pattern's bit (0 or 1).
|
|
460
|
+
bit(pattern) { const b = this.bits.get(pattern); if (!b)
|
|
461
|
+
throw new Error(`no pattern "${pattern}" on this string`); return b; }
|
|
462
|
+
eq(b) {
|
|
463
|
+
if (typeof b === 'string')
|
|
464
|
+
return all(this.id.eq(literal(b)), this.length.eq(b.length), ...[...this.bits].map(([p, bit]) => bit.eq(globMatch(p, b) ? 1 : 0)));
|
|
465
|
+
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))));
|
|
466
|
+
}
|
|
467
|
+
ne(b) { return typeof b === 'string' ? this.id.ne(literal(b)) : this.id.ne(b.id); }
|
|
468
|
+
}
|
|
469
|
+
class StrKind extends Kind {
|
|
470
|
+
patterns;
|
|
471
|
+
maxLength;
|
|
472
|
+
constructor(patterns, maxLength) {
|
|
473
|
+
super();
|
|
474
|
+
this.patterns = patterns;
|
|
475
|
+
this.maxLength = maxLength;
|
|
476
|
+
}
|
|
477
|
+
bit(name, i) { return `${name}$like${i}`; }
|
|
478
|
+
vars(name) { return [name, `${name}$len`, ...this.patterns.map((_, i) => this.bit(name, i))]; }
|
|
479
|
+
assume(name) {
|
|
480
|
+
return [v(name).eq(0).iff(v(`${name}$len`).eq(0)), ...(this.maxLength === undefined ? [] : [v(`${name}$len`).atMost(this.maxLength)]),
|
|
481
|
+
...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('\\*', '*'))))))];
|
|
482
|
+
}
|
|
483
|
+
value(name, at = '') { return new Str(v(named(at, name)), v(named(at, `${name}$len`)), new Map(this.patterns.map((p, i) => [p, v(named(at, this.bit(name, i)))]))); }
|
|
484
|
+
store(name, value) {
|
|
485
|
+
if (typeof value === 'string')
|
|
486
|
+
return { [name]: literal(value), [`${name}$len`]: value.length, ...Object.fromEntries(this.patterns.map((p, i) => [this.bit(name, i), globMatch(p, value) ? 1 : 0])) };
|
|
487
|
+
const t = value;
|
|
488
|
+
return { [name]: t.id, [`${name}$len`]: t.length, ...Object.fromEntries(this.patterns.map((p, i) => [this.bit(name, i), t.bit(p)])) };
|
|
489
|
+
}
|
|
490
|
+
decode(name, vars) { const id = vars[name]; for (const [text, n] of internTable)
|
|
491
|
+
if (n === id)
|
|
492
|
+
return text; throw new Error(`${name}: no string is interned as ${id}`); }
|
|
493
|
+
empty() { return ''; }
|
|
494
|
+
encode(name, raw) {
|
|
495
|
+
if (typeof raw !== 'string')
|
|
496
|
+
throw new RangeError(`${name} must be a string, not ${String(raw)}`);
|
|
497
|
+
if (this.maxLength !== undefined && raw.length > this.maxLength)
|
|
498
|
+
throw new RangeError(`${name} is at most ${this.maxLength} characters`);
|
|
499
|
+
return { [name]: intern(raw), [`${name}$len`]: raw.length, ...Object.fromEntries(this.patterns.map((p, i) => [this.bit(name, i), globMatch(p, raw) ? 1 : 0])) };
|
|
500
|
+
}
|
|
501
|
+
// Strings drawn so each pattern matches some of them (the patterns with their stars filled), every string named
|
|
502
|
+
// as a literal (what the rule and its laws compare with), and short strings over their characters.
|
|
503
|
+
random(name, rand, bound) { return this.encode(name, this.sample(rand, bound)); }
|
|
504
|
+
sample(rand, bound) {
|
|
505
|
+
const chars = [...new Set(['a', 'b', ...Array.from(this.patterns.join('').replaceAll('*', ''))])];
|
|
506
|
+
const pool = [...this.patterns.flatMap(p => [p.replaceAll('*', ''), p.replaceAll('*', chars[rand(chars.length)])]), ...literals];
|
|
507
|
+
const text = rand(2) === 0 ? pool[rand(pool.length)] : Array.from({ length: rand(Math.min(bound, 4) + 1) }, () => chars[rand(chars.length)]).join('');
|
|
508
|
+
return this.maxLength === undefined ? text : text.slice(0, this.maxLength);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
export const str = (opts = {}) => new StrKind(opts.like ?? [], opts.maxLength);
|
|
512
|
+
// A signed integer, encoded as two naturals (the value and `<name>$neg`): at most one nonzero when encoded.
|
|
513
|
+
class IntKind extends Kind {
|
|
514
|
+
vars(name) { return [name, `${name}$neg`]; }
|
|
515
|
+
assume() { return []; }
|
|
516
|
+
value(name, at = '') { return new Int(v(named(at, name)), v(named(at, `${name}$neg`))); }
|
|
517
|
+
store(name, value) { const i = normal(Int.of(value)); return { [name]: i.pos, [`${name}$neg`]: i.neg }; }
|
|
518
|
+
decode(name, vars) { return vars[name] - vars[`${name}$neg`]; }
|
|
519
|
+
empty() { return 0; }
|
|
520
|
+
encode(name, raw) {
|
|
521
|
+
if (typeof raw !== 'number' || !Number.isSafeInteger(raw))
|
|
522
|
+
throw new RangeError(`${name} must be an integer, not ${String(raw)}`);
|
|
523
|
+
return { [name]: Math.max(raw, 0), [`${name}$neg`]: Math.max(-raw, 0) };
|
|
524
|
+
}
|
|
525
|
+
random(name, rand, bound) { const x = rand(2 * bound + 1) - bound; return { [name]: Math.max(x, 0), [`${name}$neg`]: Math.max(-x, 0) }; }
|
|
526
|
+
sample(rand, bound) { return rand(2 * bound + 1) - bound; }
|
|
527
|
+
}
|
|
528
|
+
export const int = new IntKind();
|
|
529
|
+
// A decimal with `places` places, encoded as its integer count of 10^-places: given as a number or a string ("12.50").
|
|
530
|
+
class DecimalKind extends Kind {
|
|
531
|
+
places;
|
|
532
|
+
constructor(places) {
|
|
533
|
+
super();
|
|
534
|
+
this.places = places;
|
|
535
|
+
}
|
|
536
|
+
vars(name) { return [name, `${name}$neg`]; }
|
|
537
|
+
assume() { return []; }
|
|
538
|
+
value(name, at = '') { return new Decimal(new Int(v(named(at, name)), v(named(at, `${name}$neg`))), this.places); }
|
|
539
|
+
store(name, value) {
|
|
540
|
+
const i = normal(typeof value === 'string' ? Int.of(scaledOf(value, this.places)) : new Decimal(Int.of(0), this.places).lift(value));
|
|
541
|
+
return { [name]: i.pos, [`${name}$neg`]: i.neg };
|
|
542
|
+
}
|
|
543
|
+
decode(name, vars) { return (vars[name] - vars[`${name}$neg`]) / 10 ** this.places; }
|
|
544
|
+
empty() { return 0; }
|
|
545
|
+
encode(name, raw) {
|
|
546
|
+
if (typeof raw !== 'number' && typeof raw !== 'string')
|
|
547
|
+
throw new RangeError(`${name} must be a decimal, not ${String(raw)}`);
|
|
548
|
+
const x = scaledOf(raw, this.places, `a decimal (${name})`);
|
|
549
|
+
return { [name]: Math.max(x, 0), [`${name}$neg`]: Math.max(-x, 0) };
|
|
550
|
+
}
|
|
551
|
+
random(name, rand, bound) { return new IntKind().random(name, rand, bound); }
|
|
552
|
+
sample(rand, bound) { return (rand(2 * bound * 10 ** this.places + 1) - bound * 10 ** this.places) / 10 ** this.places; }
|
|
553
|
+
}
|
|
554
|
+
export const decimal = (places) => {
|
|
555
|
+
if (!Number.isInteger(places) || places < 0 || places > 8)
|
|
556
|
+
throw new RangeError('a decimal has 0 to 8 places');
|
|
557
|
+
return new DecimalKind(places);
|
|
558
|
+
};
|
|
208
559
|
let boundCounter = 0;
|
|
209
560
|
// A condition at every key: only as a whole flag (`flags: ({...}) => ({covered: everyKey(k => ...)})`), the key read
|
|
210
561
|
// only as a map's key. The kernel closes it (instances where it holds, a witness where it fails).
|
|
211
|
-
export const everyKey = (f) => { const key = `k$${++boundCounter}`; return new Cond({ op: 'all', key, c: f(v(key)).c }); };
|
|
562
|
+
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)); };
|
|
212
563
|
// 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
|
|
213
564
|
// key, given here or found by the prover among a few candidates (kernel/prove.ts).
|
|
214
565
|
export const someKey = (f, witness) => {
|
|
215
566
|
const key = `s$${++boundCounter}`;
|
|
216
|
-
|
|
567
|
+
const none = everyOk(scope?.keys, k => f(keyTerm(k)).not().ok);
|
|
568
|
+
return new Cond({ op: 'not', x: { op: 'all', key, c: f(v(key)).not().c, ...(witness === undefined ? {} : { witness: termOf(witness) }) } }, none === undefined ? undefined : !none);
|
|
217
569
|
};
|
|
218
570
|
// A map from naturals to naturals, every cell starting at `init`. One write per map per step.
|
|
219
571
|
export class MapRef {
|
|
220
572
|
name;
|
|
221
573
|
init;
|
|
574
|
+
cells;
|
|
222
575
|
constructor(name, init) {
|
|
223
576
|
this.name = name;
|
|
224
577
|
this.init = init;
|
|
578
|
+
this.cells = scope?.maps[name];
|
|
225
579
|
}
|
|
226
|
-
at(key) { return new Term({ op: 'get', map: this.name, key: termOf(key) }); }
|
|
227
|
-
set(key, value) { return { map: this.name, key: termOf(key), value: termOf(value) }; }
|
|
580
|
+
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); }
|
|
581
|
+
set(key, value) { return { map: this.name, key: termOf(key), value: termOf(value), concrete: { key: valOf(key), value: valOf(value) } }; }
|
|
228
582
|
// key's cell plus `by` where `c` holds (the counter every cap keeps).
|
|
229
583
|
bump(key, c = yes, by = 1) { return this.set(key, when(c, this.at(key).plus(by), this.at(key))); }
|
|
230
584
|
// Every key at once: the value at each key, the key read only as a map's key (Other.at(k)).
|
|
231
|
-
each(f) { const each = `k$${++boundCounter}`; return { map: this.name, each, value: termOf(f(v(each))) }; }
|
|
585
|
+
each(f) { const each = `k$${++boundCounter}`; return { map: this.name, each, value: termOf(f(v(each))), concrete: { at: f } }; }
|
|
232
586
|
// As a set (cells 0 or 1): a member, and, as whole flags, every member of this one in another, none shared, the same.
|
|
233
587
|
has(key) { return this.at(key).atLeast(1); }
|
|
234
588
|
subsetOf(other) { return everyKey(k => this.at(k).atMost(other.at(k))); }
|
|
@@ -242,11 +596,13 @@ export class MapRef {
|
|
|
242
596
|
export class ListRef {
|
|
243
597
|
name;
|
|
244
598
|
len;
|
|
599
|
+
cells;
|
|
245
600
|
constructor(name, len) {
|
|
246
601
|
this.name = name;
|
|
247
602
|
this.len = len;
|
|
603
|
+
this.cells = scope?.maps[name];
|
|
248
604
|
}
|
|
249
|
-
at(i) { return new Term({ op: 'get', map: this.name, key: termOf(i) }); }
|
|
605
|
+
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); }
|
|
250
606
|
// Conditions: x at some position; each position below the next.
|
|
251
607
|
has(x) { return everyKey(k => k.lt(this.len).implies(this.at(k).ne(x))).not(); }
|
|
252
608
|
sorted() { return everyKey(k => k.plus(1).lt(this.len).implies(this.at(k).lt(this.at(k.plus(1))))); }
|
|
@@ -258,16 +614,16 @@ export class ListRef {
|
|
|
258
614
|
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) }; }
|
|
259
615
|
// The updates for the step: the first operation whose condition holds, else the list as it was.
|
|
260
616
|
apply(...ops) {
|
|
261
|
-
const each = `k$${++boundCounter}
|
|
262
|
-
const at = ops.reduceRight((acc, op) => when_(op.when, op.at(k), acc), this.at(k));
|
|
617
|
+
const each = `k$${++boundCounter}`;
|
|
618
|
+
const at = (k) => ops.reduceRight((acc, op) => when_(op.when, op.at(k), acc), this.at(k));
|
|
263
619
|
const len = ops.reduceRight((acc, op) => when_(op.when, op.len, acc), this.len);
|
|
264
|
-
return { [this.name]: { map: this.name, each, value: at.e }, [`${this.name}_len`]: len };
|
|
620
|
+
return { [this.name]: { map: this.name, each, value: at(v(each)).e, concrete: { at } }, [`${this.name}_len`]: len };
|
|
265
621
|
}
|
|
266
622
|
// Laws: nothing past the end; positions strictly increasing (so no name twice), as one law about every two keys.
|
|
267
623
|
within() { return forall(k => this.len.atMost(k).implies(this.at(k).eq(0))); }
|
|
268
624
|
increasing() { return forall((i, j) => i.lt(j).and(j.lt(this.len)).implies(this.at(i).lt(this.at(j)))); }
|
|
269
625
|
}
|
|
270
|
-
const when_ = (c, then, otherwise) => new Term({ op: 'if', c: c.c, then: termOf(then), else: termOf(otherwise) });
|
|
626
|
+
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));
|
|
271
627
|
// A law about every key of a map: proven at a fresh key, assumed at the keys a step reads.
|
|
272
628
|
// With two parameters or more, about every two keys or more: forall((i, j) => i.lt(j).implies(...)).
|
|
273
629
|
export function forall(f) { return { forall: f }; }
|
|
@@ -491,9 +847,22 @@ const fnSource = (name, d) => {
|
|
|
491
847
|
const fnTerm = (name, d) => (...args) => {
|
|
492
848
|
if (args.length !== d.arity)
|
|
493
849
|
throw new Error(`function ${name} takes ${d.arity} argument${d.arity === 1 ? '' : 's'}`);
|
|
494
|
-
|
|
850
|
+
const vals = known(args.map(valOf));
|
|
851
|
+
return new Term({ op: 'get', map: name, key: argsKey(args.map(termOf)) }, vals && fnValueOf(d)(...vals));
|
|
852
|
+
};
|
|
853
|
+
// A function's JavaScript on its arguments themselves (not through the pairing the kernel reads them by).
|
|
854
|
+
const fnValues = new WeakMap();
|
|
855
|
+
const fnValueOf = (d) => {
|
|
856
|
+
let f = fnValues.get(d);
|
|
857
|
+
if (!f) {
|
|
858
|
+
f = typeof d.js === 'function' ? d.js : compileFunction([], `return (${d.js})`)();
|
|
859
|
+
fnValues.set(d, f);
|
|
860
|
+
}
|
|
861
|
+
return f;
|
|
495
862
|
};
|
|
496
863
|
let keyCounter = 0;
|
|
864
|
+
const sources = new WeakMap();
|
|
865
|
+
const withSource = (x, src) => (sources.set(x, src), x);
|
|
497
866
|
// A k-step law's later requests are built over marked names (j:name, which no rule can declare: ':' is reserved); the
|
|
498
867
|
// kernel reads them as name@j.
|
|
499
868
|
const markedName = (name) => { const m = /^(\d+):(.*)$/.exec(name); return m ? `${m[2]}@${m[1]}` : name; };
|
|
@@ -523,11 +892,40 @@ export class Rule {
|
|
|
523
892
|
get name() { return this.spec.name; }
|
|
524
893
|
params(kinds) { return this.with({ params: [...this.spec.params, ...Object.entries(kinds)] }); }
|
|
525
894
|
inputs(kinds) { return this.with({ inputs: [...this.spec.inputs, ...Object.entries(kinds)] }); }
|
|
526
|
-
// Each variable starts at a natural or at a term over the params
|
|
895
|
+
// Each variable starts at a natural or at a term over the params, or is declared by a kind and starts where `from`
|
|
896
|
+
// says: `.state({balance: int.from(-5), status: oneOf('open', 'closed').from('open'), note: maybe(nat).from(null)})`.
|
|
527
897
|
state(init) {
|
|
528
|
-
|
|
898
|
+
const entries = [], kinded = [];
|
|
899
|
+
for (const [n, x] of Object.entries(init)) {
|
|
900
|
+
if (typeof x === 'number' || typeof x === 'function') {
|
|
901
|
+
entries.push([n, x]);
|
|
902
|
+
continue;
|
|
903
|
+
}
|
|
904
|
+
const { kind, init: start } = x;
|
|
905
|
+
kinded.push([n, kind]);
|
|
906
|
+
if (typeof start === 'function') {
|
|
907
|
+
const parts = (p) => kind.store(n, start(p));
|
|
908
|
+
for (const part of kind.vars(n))
|
|
909
|
+
entries.push([part, p => parts(p)[part]]);
|
|
910
|
+
}
|
|
911
|
+
else {
|
|
912
|
+
const enc = kind.encode(n, start);
|
|
913
|
+
for (const part of kind.vars(n))
|
|
914
|
+
entries.push([part, enc[part]]);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
return this.with({ state: [...this.spec.state, ...entries], kinded: [...(this.spec.kinded ?? []), ...kinded] });
|
|
529
918
|
}
|
|
530
919
|
maps(init) { return this.with({ maps: [...this.spec.maps, ...Object.entries(init)] }); }
|
|
920
|
+
// Integers (Int), each starting at an integer or at one over the params; kept as two naturals, <name> and <name>$neg.
|
|
921
|
+
ints(init) {
|
|
922
|
+
return this.state(Object.fromEntries(Object.entries(init).map(([n, x]) => [n, int.from(x)])));
|
|
923
|
+
}
|
|
924
|
+
// Decimals with `places` places (Decimal), each starting at a number, a string ("12.50") or one over the params.
|
|
925
|
+
decimals(places, init) {
|
|
926
|
+
const kind = decimal(places);
|
|
927
|
+
return this.state(Object.fromEntries(Object.entries(init).map(([n, x]) => [n, kind.from(x)])));
|
|
928
|
+
}
|
|
531
929
|
// Lists, each empty at the start: a map of positions and its length <name>_len (ListRef).
|
|
532
930
|
lists(...names) {
|
|
533
931
|
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] });
|
|
@@ -571,7 +969,7 @@ export class Rule {
|
|
|
571
969
|
// (the kernel proves it adds nothing to any query).
|
|
572
970
|
prune(order, after) { return this.with({ prune: { order, after: after } }); }
|
|
573
971
|
// A law about the params, the state, the maps and the sums (not a request): `forall` for every key of a map.
|
|
574
|
-
law(name, f) { return this.lawOf(name, f(this.env({ flags: false, inputs: false, queries: false }))); }
|
|
972
|
+
law(name, f) { return withSource(this.lawOf(name, f(this.env({ flags: false, inputs: false, queries: false }))), { law: f }); }
|
|
575
973
|
lawOf(name, out) {
|
|
576
974
|
const key = `key$${++keyCounter}`;
|
|
577
975
|
if (out instanceof Cond) {
|
|
@@ -586,23 +984,15 @@ export class Rule {
|
|
|
586
984
|
// counter.stepLaw('never goes back', ({used}, after) => used.atMost(after.used))
|
|
587
985
|
stepLaw(name, then, when) {
|
|
588
986
|
const env = this.env();
|
|
589
|
-
const after =
|
|
590
|
-
for (const [n] of this.spec.state)
|
|
591
|
-
after[n] = v(`${n}'`);
|
|
592
|
-
for (const [n] of this.spec.sums)
|
|
593
|
-
after[n] = v(`${n}'`);
|
|
594
|
-
for (const [n, init] of this.spec.maps)
|
|
595
|
-
after[n] = new MapRef(`${n}'`, init);
|
|
596
|
-
for (const n of this.spec.lists ?? [])
|
|
597
|
-
after[n] = new ListRef(`${n}'`, v(`${n}_len'`));
|
|
987
|
+
const after = this.afterEnv();
|
|
598
988
|
const out = then(env, after), w = when ? { when: when(env).c } : {};
|
|
599
|
-
const key = `key$${++keyCounter}
|
|
989
|
+
const key = `key$${++keyCounter}`, src = { step: { then: then, ...(when ? { when: when } : {}) } };
|
|
600
990
|
if (out instanceof Cond) {
|
|
601
991
|
const p = prenex(out.c, `step law ${name}`, key);
|
|
602
|
-
return p ? { name, forall: key, then: p, ...w } : { name, then: out.c, ...w };
|
|
992
|
+
return withSource(p ? { name, forall: key, then: p, ...w } : { name, then: out.c, ...w }, src);
|
|
603
993
|
}
|
|
604
994
|
const [k1, more] = keysFor(out), c = out.forall(v(k1), ...more.map(v)).c;
|
|
605
|
-
return more.length ? { name, forall: k1, keys: more, then: c, ...w } : { name, forall: k1, then: prenex(c, `step law ${name}`, k1) ?? c, ...w };
|
|
995
|
+
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);
|
|
606
996
|
}
|
|
607
997
|
// 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
|
|
608
998
|
// state, maps and sums after the last, with each step's request and queries (requests[0] is the first's); `when(before,
|
|
@@ -610,15 +1000,7 @@ export class Rule {
|
|
|
610
1000
|
// meter.stepsLaw('two requests spend at most 2m', 2, ({spent, m}, after) => after.spent.atMost(spent.plus(m).plus(m)))
|
|
611
1001
|
stepsLaw(name, k, then, when) {
|
|
612
1002
|
const env = this.env();
|
|
613
|
-
const after =
|
|
614
|
-
for (const [n] of this.spec.state)
|
|
615
|
-
after[n] = v(`${n}'`);
|
|
616
|
-
for (const [n] of this.spec.sums)
|
|
617
|
-
after[n] = v(`${n}'`);
|
|
618
|
-
for (const [n, init] of this.spec.maps)
|
|
619
|
-
after[n] = new MapRef(`${n}'`, init);
|
|
620
|
-
for (const n of this.spec.lists ?? [])
|
|
621
|
-
after[n] = new ListRef(`${n}'`, v(`${n}_len'`));
|
|
1003
|
+
const after = this.afterEnv();
|
|
622
1004
|
// The j-th request's values over marked names, renamed to the kernel's input@j once built.
|
|
623
1005
|
const mark = (j) => `${j}:`;
|
|
624
1006
|
const requests = Array.from({ length: k }, (_, j) => {
|
|
@@ -655,12 +1037,24 @@ export class Rule {
|
|
|
655
1037
|
const base = { name, from: spec.from(env).c, to: spec.to(env).c, rank };
|
|
656
1038
|
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) })) };
|
|
657
1039
|
}
|
|
658
|
-
// A decision: whenever the params and the request meet `given`, `then` holds. Decisions don't read the state
|
|
1040
|
+
// A decision: whenever the params and the request meet `given`, `then` holds. Decisions don't read the state; the
|
|
1041
|
+
// functions' facts are assumed at the arguments the decision reads them at.
|
|
659
1042
|
decide(name, given, then) {
|
|
660
1043
|
const env = this.env({ flags: false, state: false, queries: false });
|
|
661
1044
|
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)];
|
|
662
|
-
const t = then(env).c;
|
|
663
|
-
|
|
1045
|
+
const t = then(env).c, goal = { name, hyps: [...hyps, { op: 'not', x: t }] };
|
|
1046
|
+
const facts = this.spec.fns?.length || this.spec.facts?.length ? factsFor(this.machine, goal) : [];
|
|
1047
|
+
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 } });
|
|
1048
|
+
}
|
|
1049
|
+
// A decision about two requests under the same params (noninterference, say: requests that differ only in a secret
|
|
1050
|
+
// are decided alike). `given` and `then` read both; the second request's inputs are named b.<input> in a witness.
|
|
1051
|
+
// r.relate('the secret is irrelevant', (a, b) => a.user.eq(b.user), (a, b) => a.allowed.iff(b.allowed))
|
|
1052
|
+
relate(name, given, then) {
|
|
1053
|
+
const s = this.spec, b = (n) => `b.${n}`, own = [...s.inputs.map(([n]) => n), ...(s.carried ?? [])];
|
|
1054
|
+
const second = (env) => ({ ...env, ...Object.fromEntries(own.map(n => [n, env[b(n)]])) });
|
|
1055
|
+
const pair = new Rule({ ...s, inputs: [...s.inputs, ...s.inputs.map(([n, k]) => [b(n), k])], carried: [...(s.carried ?? []), ...(s.carried ?? []).map(b)],
|
|
1056
|
+
assume: [...s.assume, ...s.assume.map(f => (env) => f(second(env)))], flags: [], step: undefined, record: undefined, sums: [], queries: [], keep: undefined, relations: undefined, prune: undefined });
|
|
1057
|
+
return pair.decide(name, env => given(env, second(env)), env => then(env, second(env)));
|
|
664
1058
|
}
|
|
665
1059
|
// --- what it compiles to ------------------------------------------------------------------------------------------
|
|
666
1060
|
kindAssumptions(which = 'both') {
|
|
@@ -668,8 +1062,27 @@ export class Rule {
|
|
|
668
1062
|
const is = which === 'params' ? [] : this.spec.inputs.flatMap(([n, k]) => k.assume(n));
|
|
669
1063
|
return [...ps, ...is].map(c => c.c);
|
|
670
1064
|
}
|
|
1065
|
+
// The state after a step, as a step law reads it: every state variable, sum, map and list, primed.
|
|
1066
|
+
afterEnv() {
|
|
1067
|
+
const after = {};
|
|
1068
|
+
for (const [n] of this.spec.state)
|
|
1069
|
+
after[n] = v(`${n}'`);
|
|
1070
|
+
for (const [n] of this.spec.sums)
|
|
1071
|
+
after[n] = v(`${n}'`);
|
|
1072
|
+
for (const [n, init] of this.spec.maps)
|
|
1073
|
+
after[n] = new MapRef(`${n}'`, init);
|
|
1074
|
+
for (const n of this.spec.lists ?? [])
|
|
1075
|
+
after[n] = new ListRef(`${n}'`, v(`${n}_len'`));
|
|
1076
|
+
for (const [n, k] of this.spec.kinded ?? []) {
|
|
1077
|
+
for (const part of k.vars(n))
|
|
1078
|
+
delete after[part];
|
|
1079
|
+
after[n] = k.value(n, primed);
|
|
1080
|
+
}
|
|
1081
|
+
return after;
|
|
1082
|
+
}
|
|
671
1083
|
// The values handed to the builders' callbacks.
|
|
672
1084
|
env(parts = {}) {
|
|
1085
|
+
this.distinct();
|
|
673
1086
|
const out = {};
|
|
674
1087
|
for (const [n, k] of this.spec.params)
|
|
675
1088
|
out[n] = k.value(n);
|
|
@@ -690,16 +1103,42 @@ export class Rule {
|
|
|
690
1103
|
out[n] = new ListRef(n, v(`${n}_len`));
|
|
691
1104
|
for (const [n] of this.spec.sums)
|
|
692
1105
|
out[n] = v(n);
|
|
1106
|
+
for (const [n, k] of this.spec.kinded ?? []) {
|
|
1107
|
+
for (const part of k.vars(n))
|
|
1108
|
+
delete out[part];
|
|
1109
|
+
out[n] = k.value(n);
|
|
1110
|
+
}
|
|
693
1111
|
}
|
|
694
1112
|
if (parts.queries !== false)
|
|
695
1113
|
for (const [n] of this.spec.queries)
|
|
696
1114
|
out[n] = v(n);
|
|
697
1115
|
if (parts.flags !== false)
|
|
698
1116
|
for (const f of this.spec.flags)
|
|
699
|
-
for (const n of Object.keys(f(out)))
|
|
700
|
-
out[n]
|
|
1117
|
+
for (const n of Object.keys(f(out))) {
|
|
1118
|
+
if (n in out && !(out[n] instanceof Cond && out[n].c.op === 'flag'))
|
|
1119
|
+
throw new Error(`${this.spec.name}: ${n} is a flag and ${this.whatIs(n)}`);
|
|
1120
|
+
out[n] = new Cond({ op: 'flag', name: n }, scope?.flags[n]);
|
|
1121
|
+
}
|
|
701
1122
|
return out;
|
|
702
1123
|
}
|
|
1124
|
+
// Every name the rule declares is declared once (a flag named again replaces the earlier one, so flags are checked as
|
|
1125
|
+
// they are defined): an input and a state variable sharing a name would hand the callbacks only one of them.
|
|
1126
|
+
names() {
|
|
1127
|
+
const s = this.spec;
|
|
1128
|
+
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']),
|
|
1129
|
+
...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']),
|
|
1130
|
+
];
|
|
1131
|
+
}
|
|
1132
|
+
whatIs(n) { return this.names().find(([m]) => m === n)?.[1] ?? 'something else'; }
|
|
1133
|
+
distinct() {
|
|
1134
|
+
const seen = new Map();
|
|
1135
|
+
for (const [n, what] of this.names()) {
|
|
1136
|
+
const had = seen.get(n);
|
|
1137
|
+
if (had)
|
|
1138
|
+
throw new Error(`${this.spec.name}: ${n} is ${had} and ${had === what ? 'declared again' : what}; give each a name of its own`);
|
|
1139
|
+
seen.set(n, what);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
703
1142
|
get machine() {
|
|
704
1143
|
const s = this.spec;
|
|
705
1144
|
const params = s.params.flatMap(([n, k]) => k.vars(n)), queries = s.queries.map(([n]) => n);
|
|
@@ -709,9 +1148,11 @@ export class Rule {
|
|
|
709
1148
|
const declared = {};
|
|
710
1149
|
for (const f of s.flags)
|
|
711
1150
|
for (const [n, c] of Object.entries(f(env))) {
|
|
1151
|
+
if (n in env && !(n in declared))
|
|
1152
|
+
throw new Error(`${s.name}: ${n} is a flag and ${this.whatIs(n)}`);
|
|
712
1153
|
declared[n] = c.c;
|
|
713
|
-
env[n] = new Cond({ op: 'flag', name: n });
|
|
714
|
-
}
|
|
1154
|
+
env[n] = new Cond({ op: 'flag', name: n }); // a name again: replaced in place
|
|
1155
|
+
}
|
|
715
1156
|
// Quantifiers anywhere, named as flags: each placed before the flag that reads it.
|
|
716
1157
|
const hoist = new Hoister('all$'), flags = {};
|
|
717
1158
|
const placed = () => { for (const [n, c] of Object.entries(hoist.flags))
|
|
@@ -723,7 +1164,15 @@ export class Rule {
|
|
|
723
1164
|
if (!(n in flags))
|
|
724
1165
|
flags[n] = out;
|
|
725
1166
|
}
|
|
726
|
-
const updates = s.step?.(env)
|
|
1167
|
+
const updates = { ...s.step?.(env) };
|
|
1168
|
+
// A kinded variable's update, as its variables' terms.
|
|
1169
|
+
for (const [n, k] of s.kinded ?? []) {
|
|
1170
|
+
const u = updates[n];
|
|
1171
|
+
if (u === undefined)
|
|
1172
|
+
continue;
|
|
1173
|
+
delete updates[n];
|
|
1174
|
+
Object.assign(updates, k.store(n, u));
|
|
1175
|
+
}
|
|
727
1176
|
const state = {}, maps = {};
|
|
728
1177
|
for (const [n, u] of Object.entries(updates)) {
|
|
729
1178
|
if (u === undefined)
|
|
@@ -775,6 +1224,174 @@ export class Rule {
|
|
|
775
1224
|
}
|
|
776
1225
|
return m;
|
|
777
1226
|
}
|
|
1227
|
+
// --- the builders checked ------------------------------------------------------------------------------------------
|
|
1228
|
+
// The builders against what they built, on concrete runs: random params and requests by their kinds (`pre` and
|
|
1229
|
+
// `assume` met), and at every state each law's callback, each step law's, every flag and the step itself run again
|
|
1230
|
+
// with every value known, so each answer is the builders' own arithmetic (`val`, `ok`), against the expressions the
|
|
1231
|
+
// interpreter evaluates (core/watch.ts's verdicts, over the same keys). A law that says something other than its
|
|
1232
|
+
// callback, or a step that writes something other than it means, is reported with the state it shows at.
|
|
1233
|
+
crossCheck(laws, stepLaws = [], opts = {}) {
|
|
1234
|
+
const s = this.spec, m = this.machine, run = interpret(m), bound = opts.bound ?? 4, rand = opts.rand ?? seeded(7);
|
|
1235
|
+
let checked = 0;
|
|
1236
|
+
const at = (what, params, st, input) => `${what}: params ${JSON.stringify(params)}, state ${JSON.stringify(this.decode(st))}${input ? `, request ${JSON.stringify(numbersOf(input))}` : ''}`;
|
|
1237
|
+
const mapsOf = (st) => Object.fromEntries(Object.entries(st.maps).map(([n, cells]) => [n, { cells, dflt: st.defaults?.[n] ?? m.maps?.[n] ?? 0 }]));
|
|
1238
|
+
for (let trial = 0; trial < (opts.trials ?? 100); trial++) {
|
|
1239
|
+
const params = this.randomParams(rand, bound);
|
|
1240
|
+
if (m.pre && !evalC(m.pre, params, { scalars: {}, maps: {} }, m))
|
|
1241
|
+
continue;
|
|
1242
|
+
let st = run.init(params);
|
|
1243
|
+
for (let d = 0, tries = 0; d <= (opts.depth ?? 8) && tries < 200; tries++) {
|
|
1244
|
+
// The laws at this state.
|
|
1245
|
+
const keys = keysOf(bound, st), lawScope = { vars: { ...params, ...st.scalars }, maps: mapsOf(st), keys, flags: {} };
|
|
1246
|
+
for (const law of laws) {
|
|
1247
|
+
const f = sources.get(law)?.law;
|
|
1248
|
+
if (!f)
|
|
1249
|
+
continue;
|
|
1250
|
+
const ok = inScope(lawScope, () => this.verdict(f(this.env({ flags: false, inputs: false, queries: false })), keys));
|
|
1251
|
+
if (ok === undefined)
|
|
1252
|
+
continue;
|
|
1253
|
+
checked++;
|
|
1254
|
+
if (ok !== watch(m, run, law, params, bound).start(st))
|
|
1255
|
+
return { checked, failure: at(`law "${law.name}": its callback says ${ok}, the law proven says ${!ok}`, params, st) };
|
|
1256
|
+
}
|
|
1257
|
+
if (d === (opts.depth ?? 8))
|
|
1258
|
+
break;
|
|
1259
|
+
const input = this.randomInput(rand, bound);
|
|
1260
|
+
if (s.prune || !allowed(m, run, params, st, input))
|
|
1261
|
+
continue;
|
|
1262
|
+
d++;
|
|
1263
|
+
const next = run.step(params, st, input);
|
|
1264
|
+
if (!s.record) {
|
|
1265
|
+
const failure = this.crossStep(params, st, input, next, stepLaws, bound, () => checked++);
|
|
1266
|
+
if (failure)
|
|
1267
|
+
return { checked, failure: at(failure, params, st, input) };
|
|
1268
|
+
}
|
|
1269
|
+
st = next;
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
return { checked };
|
|
1273
|
+
}
|
|
1274
|
+
// A condition's value, or a law about every key's at every tuple of these keys (undefined when not every value is known).
|
|
1275
|
+
verdict(out, keys) {
|
|
1276
|
+
if (out instanceof Cond)
|
|
1277
|
+
return out.ok;
|
|
1278
|
+
const go = (i, picked) => (i === out.forall.length ? out.forall(...picked).ok : everyOk(keys, k => go(i + 1, [...picked, keyTerm(k)])));
|
|
1279
|
+
return go(0, []);
|
|
1280
|
+
}
|
|
1281
|
+
// One step: the flags, the updates and the step laws, each against what the interpreter computed.
|
|
1282
|
+
crossStep(params, st, input, next, stepLaws, bound, count) {
|
|
1283
|
+
const s = this.spec, m = this.machine, run = interpret(m), seen = carriedView(m, st, input), req = requestEnv(m, run, params, st, input);
|
|
1284
|
+
const keys = keysOf(bound, seen, next), vars = { ...params, ...st.scalars, ...req };
|
|
1285
|
+
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: {} };
|
|
1286
|
+
// Flags: every callback in order, each name's last definition winning, until the values settle (a name defined
|
|
1287
|
+
// again replaces the first in place, so the flags between read the new one).
|
|
1288
|
+
const names = Object.keys(m.step.flags ?? {});
|
|
1289
|
+
for (let pass = 0; pass <= s.flags.length; pass++) {
|
|
1290
|
+
const values = {};
|
|
1291
|
+
inScope(sc, () => {
|
|
1292
|
+
const env = this.env({ flags: false });
|
|
1293
|
+
for (const f of s.flags)
|
|
1294
|
+
for (const [n, c] of Object.entries(f(env))) {
|
|
1295
|
+
env[n] = new Cond({ op: 'flag', name: n }, sc.flags[n]);
|
|
1296
|
+
if (c.ok !== undefined)
|
|
1297
|
+
values[n] = c.ok;
|
|
1298
|
+
}
|
|
1299
|
+
});
|
|
1300
|
+
const settled = Object.keys(values).every(n => sc.flags[n] === values[n]);
|
|
1301
|
+
sc.flags = values;
|
|
1302
|
+
if (settled)
|
|
1303
|
+
break;
|
|
1304
|
+
}
|
|
1305
|
+
for (const n of names) {
|
|
1306
|
+
if (sc.flags[n] === undefined || n.startsWith('all$'))
|
|
1307
|
+
continue;
|
|
1308
|
+
count();
|
|
1309
|
+
if (sc.flags[n] !== (req[`?${n}`] === 1))
|
|
1310
|
+
return `flag ${n}: its callback says ${sc.flags[n]}, the step decides ${req[`?${n}`] === 1}`;
|
|
1311
|
+
}
|
|
1312
|
+
// The updates: every state variable as its value is written (a kinded one through its kind's `store`, as the
|
|
1313
|
+
// machine is built) or kept, and each map write.
|
|
1314
|
+
const updates = inScope(sc, () => s.step?.(this.env()) ?? {});
|
|
1315
|
+
const written = { ...updates };
|
|
1316
|
+
inScope(sc, () => { for (const [n, k] of s.kinded ?? [])
|
|
1317
|
+
if (updates[n] !== undefined) {
|
|
1318
|
+
delete written[n];
|
|
1319
|
+
Object.assign(written, k.store(n, updates[n]));
|
|
1320
|
+
} });
|
|
1321
|
+
for (const n of Object.keys(st.scalars)) {
|
|
1322
|
+
const u = written[n], got = next.scalars[n], want = u === undefined ? st.scalars[n] : valOf(u);
|
|
1323
|
+
if (want === undefined)
|
|
1324
|
+
continue;
|
|
1325
|
+
count();
|
|
1326
|
+
if (want !== got)
|
|
1327
|
+
return `the step writes ${n} = ${want} by its callback, ${got} by the machine`;
|
|
1328
|
+
}
|
|
1329
|
+
for (const [n] of s.maps) {
|
|
1330
|
+
const u = updates[n], cell = (k) => next.maps[n]?.get(k) ?? next.defaults?.[n] ?? m.maps[n];
|
|
1331
|
+
if (!u?.concrete)
|
|
1332
|
+
continue;
|
|
1333
|
+
if ('at' in u.concrete) {
|
|
1334
|
+
const f = u.concrete.at;
|
|
1335
|
+
for (const k of sc.keys) {
|
|
1336
|
+
const want = inScope(sc, () => valOf(f(keyTerm(k))));
|
|
1337
|
+
if (want === undefined)
|
|
1338
|
+
continue;
|
|
1339
|
+
count();
|
|
1340
|
+
if (want !== cell(k))
|
|
1341
|
+
return `the step writes ${n}[${k}] = ${want} by its callback, ${cell(k)} by the machine`;
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
else if (u.concrete.key !== undefined && u.concrete.value !== undefined) {
|
|
1345
|
+
count();
|
|
1346
|
+
if (u.concrete.value !== cell(u.concrete.key))
|
|
1347
|
+
return `the step writes ${n}[${u.concrete.key}] = ${u.concrete.value} by its callback, ${cell(u.concrete.key)} by the machine`;
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
// Step laws (one step each): `when` and `then` on the state before and after.
|
|
1351
|
+
for (const sl of stepLaws) {
|
|
1352
|
+
const src = sources.get(sl)?.step;
|
|
1353
|
+
if (!src || (sl.steps ?? 1) > 1)
|
|
1354
|
+
continue;
|
|
1355
|
+
const after = { ...sc, keys, vars: { ...sc.vars, ...Object.fromEntries(Object.entries(next.scalars).map(([n, x]) => [`${n}'`, x])) },
|
|
1356
|
+
maps: { ...sc.maps, ...Object.fromEntries(Object.entries(next.maps).map(([n, cells]) => [`${n}'`, { cells, dflt: next.defaults?.[n] ?? m.maps?.[n] ?? 0 }])) } };
|
|
1357
|
+
const ok = inScope(after, () => {
|
|
1358
|
+
const env = this.env(), afterEnv = this.afterEnv();
|
|
1359
|
+
const w = src.when ? src.when(env).ok : true;
|
|
1360
|
+
return w === undefined ? undefined : w ? this.verdict(src.then(env, afterEnv), keys) : true;
|
|
1361
|
+
});
|
|
1362
|
+
if (ok === undefined)
|
|
1363
|
+
continue;
|
|
1364
|
+
count();
|
|
1365
|
+
if (ok !== watch(m, run, sl, params, bound).step(st, input, next))
|
|
1366
|
+
return `step law "${sl.name}": its callback says ${ok}, the law proven says ${!ok}`;
|
|
1367
|
+
}
|
|
1368
|
+
return undefined;
|
|
1369
|
+
}
|
|
1370
|
+
// Against the code it describes, step by step: `reference.init(params)` and `reference.step(params, state, request)`,
|
|
1371
|
+
// with requests drawn by their kinds (or `draw`), must hold the same values as the compiled rule (decoded: integers and
|
|
1372
|
+
// decimals as numbers) for every field the reference's state has, after every step of `trials` runs of `depth`.
|
|
1373
|
+
compare(reference, opts = {}) {
|
|
1374
|
+
const s = this.spec, rand = opts.rand ?? seeded(5), bound = opts.bound ?? 100, gen = this.compiled();
|
|
1375
|
+
let checked = 0;
|
|
1376
|
+
const sample = (kinds) => Object.fromEntries(kinds.map(([n, k]) => [n, k.sample(rand, bound)]));
|
|
1377
|
+
for (let trial = 0; trial < (opts.trials ?? 200); trial++) {
|
|
1378
|
+
const params = (opts.params?.(rand) ?? sample(s.params)), ps = this.encodeParams(params);
|
|
1379
|
+
let ours = gen.init(ps), theirs = reference.init(params);
|
|
1380
|
+
const edges = trial === 0 ? opts.edges ?? [] : [];
|
|
1381
|
+
for (let d = 0; d < (opts.depth ?? 10) + edges.length; d++) {
|
|
1382
|
+
const request = edges[d] ?? opts.draw?.(rand) ?? sample(s.inputs);
|
|
1383
|
+
ours = gen.step(ps, ours, this.encodeInput(request));
|
|
1384
|
+
theirs = reference.step(params, theirs, request);
|
|
1385
|
+
const got = this.decode(ours);
|
|
1386
|
+
for (const [field, want] of Object.entries(theirs)) {
|
|
1387
|
+
checked++;
|
|
1388
|
+
if (got[field] !== want)
|
|
1389
|
+
return { checked, failure: `${s.name}, params ${JSON.stringify(params)}, after ${JSON.stringify(request)}: ${field} is ${got[field]} here, ${want} in the code it describes` };
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
return { checked };
|
|
1394
|
+
}
|
|
778
1395
|
// --- running it -----------------------------------------------------------------------------------------------------
|
|
779
1396
|
// Runtime values (numbers, booleans, choice names, null for a missing value) to the variables the machine reads.
|
|
780
1397
|
encodeParams(values) { return encode(this.spec.params, values); }
|
|
@@ -795,6 +1412,17 @@ export class Rule {
|
|
|
795
1412
|
out[n] = new Map(Array.from({ length: bound + 1 }, (_, key) => [key, rand(2)]).filter(([, cell]) => cell === 1));
|
|
796
1413
|
return out;
|
|
797
1414
|
}
|
|
1415
|
+
// The state's scalars as runtime values: each kinded variable as its kind decodes it (an integer as one signed
|
|
1416
|
+
// number, a decimal as its number, a choice by name, a string as itself, none as null).
|
|
1417
|
+
decode(st) {
|
|
1418
|
+
const out = { ...st.scalars };
|
|
1419
|
+
for (const [n, k] of this.spec.kinded ?? []) {
|
|
1420
|
+
for (const part of k.vars(n))
|
|
1421
|
+
delete out[part];
|
|
1422
|
+
out[n] = k.decode(n, st.scalars);
|
|
1423
|
+
}
|
|
1424
|
+
return out;
|
|
1425
|
+
}
|
|
798
1426
|
// The prune order's variable, if the history is pruned (a runtime must send it nondecreasing).
|
|
799
1427
|
get orderedBy() { return this.spec.prune ? new Map(this.spec.inputs).get(this.spec.prune.order).vars(this.spec.prune.order)[0] : undefined; }
|
|
800
1428
|
interpreter() { return interpret(this.machine); }
|
|
@@ -804,6 +1432,73 @@ export class Rule {
|
|
|
804
1432
|
}
|
|
805
1433
|
const encode = (kinds, values) => Object.assign({}, ...kinds.map(([n, k]) => k.encode(n, values[n])));
|
|
806
1434
|
export const rule = (name) => new Rule({ name, params: [], inputs: [], state: [], maps: [], flags: [], pre: [], assume: [], sums: [], queries: [] });
|
|
1435
|
+
// The kind a result field is kept in.
|
|
1436
|
+
const kindOf = (t) => (t instanceof Cond ? bool : t instanceof Int ? int : t instanceof Decimal ? decimal(t.places) : t instanceof Choice ? oneOf(...t.values) : t instanceof Str ? str({ like: t.patterns }) : nat);
|
|
1437
|
+
export class Fun {
|
|
1438
|
+
r;
|
|
1439
|
+
out;
|
|
1440
|
+
constructor(r, out) {
|
|
1441
|
+
this.r = r;
|
|
1442
|
+
this.out = out;
|
|
1443
|
+
}
|
|
1444
|
+
get name() { return this.r.name; }
|
|
1445
|
+
params(kinds) { return new Fun(this.r.params(kinds), this.out); }
|
|
1446
|
+
inputs(kinds) { return new Fun(this.r.inputs(kinds), this.out); }
|
|
1447
|
+
fns(defs) { return new Fun(this.r.fns(defs), this.out); }
|
|
1448
|
+
facts(f) { return new Fun(this.r.facts(f), this.out); }
|
|
1449
|
+
pre(f) { return new Fun(this.r.pre(f), this.out); }
|
|
1450
|
+
assume(f) { return new Fun(this.r.assume(f), this.out); }
|
|
1451
|
+
// The result (replaces an earlier one, so a variant is one call away).
|
|
1452
|
+
returns(f) { return new Fun(this.r, f); }
|
|
1453
|
+
result(env) {
|
|
1454
|
+
if (!this.out)
|
|
1455
|
+
throw new Error(`${this.name}: no result (returns)`);
|
|
1456
|
+
return Object.fromEntries(Object.entries(this.out(env)).map(([k, t]) => [k, typeof t === 'number' ? lit(t) : t]));
|
|
1457
|
+
}
|
|
1458
|
+
// Whenever the arguments (and the result) meet `given`, the result meets `then`.
|
|
1459
|
+
law(name, given, then) {
|
|
1460
|
+
return this.r.decide(name, env => given(env, this.result(env)), env => then(this.result(env), env));
|
|
1461
|
+
}
|
|
1462
|
+
// A law about two calls under the same params: `then(outA, outB, a, b)` given `given(a, b)` (noninterference, say).
|
|
1463
|
+
relate(name, given, then) {
|
|
1464
|
+
return this.r.relate(name, given, (a, b) => then(this.result(a), this.result(b), a, b));
|
|
1465
|
+
}
|
|
1466
|
+
// Against the code it describes: `reference` called with the same arguments (drawn by their kinds, or by `draw`, the
|
|
1467
|
+
// `edges` first) must return the same value for every field it returns (a field it leaves out is not compared). The
|
|
1468
|
+
// check for an encoding: that the terms here are what the real function computes, on its real inputs.
|
|
1469
|
+
// cap.compare(({left, local}) => ({out: realCap(left, local)}), {edges: [{left: 2 ** 32 + 5, local: 10}]})
|
|
1470
|
+
compare(reference, opts = {}) {
|
|
1471
|
+
const rand = opts.rand ?? seeded(5), bound = opts.bound ?? 1000, params = (opts.params ?? {}), kinds = this.r['spec'].inputs;
|
|
1472
|
+
let checked = 0;
|
|
1473
|
+
const draws = [...(opts.edges ?? []), ...Array.from({ length: opts.trials ?? 2000 }, () => opts.draw?.(rand) ?? Object.fromEntries(kinds.map(([n, k]) => [n, k.sample(rand, bound)])))];
|
|
1474
|
+
for (const values of draws) {
|
|
1475
|
+
const ours = this.run(values, params), theirs = reference(values, params);
|
|
1476
|
+
for (const [field, want] of Object.entries(theirs)) {
|
|
1477
|
+
checked++;
|
|
1478
|
+
if (ours[field] !== want)
|
|
1479
|
+
return { checked, failure: `${this.name} at ${JSON.stringify(values)}: ${field} is ${String(ours[field])} here, ${String(want)} in the code it describes` };
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
return { checked };
|
|
1483
|
+
}
|
|
1484
|
+
// The one-step rule it compiles to: the result's fields are its state (named out.<field>, apart from the arguments).
|
|
1485
|
+
get rule() {
|
|
1486
|
+
const fields = Object.entries(this.result(this.r['env']({ flags: false, state: false, queries: false })));
|
|
1487
|
+
const name = (k) => `out.${k}`;
|
|
1488
|
+
return this.r.state(Object.fromEntries(fields.map(([k, t]) => { const kind = kindOf(t); return [name(k), kind.from(kind.empty())]; })))
|
|
1489
|
+
.step(env => Object.fromEntries(Object.entries(this.result(env)).map(([k, t]) => [name(k), t])));
|
|
1490
|
+
}
|
|
1491
|
+
compiledRule;
|
|
1492
|
+
// The result at these arguments (and params), from the compiled rule: numbers, and booleans for the conditions.
|
|
1493
|
+
run(values, params = {}) {
|
|
1494
|
+
this.compiledRule ??= (rule => ({ rule, gen: rule.compiled() }))(this.rule);
|
|
1495
|
+
const { rule, gen } = this.compiledRule, ps = rule.encodeParams(params);
|
|
1496
|
+
const kinds = Object.entries(this.result(this.r['env']({ flags: false, state: false, queries: false })));
|
|
1497
|
+
const st = rule.decode(gen.step(ps, gen.init(ps), rule.encodeInput(values)));
|
|
1498
|
+
return Object.fromEntries(kinds.map(([k]) => [k, st[`out.${k}`]]));
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
export const fun = (name) => new Fun(rule(name));
|
|
807
1502
|
// A loop runs `body` while `while` holds, each turn with fresh inputs; it halts if a rank over the state and params
|
|
808
1503
|
// (or a tuple of them, compared in order) drops on every turn.
|
|
809
1504
|
export class LoopRule {
|
|
@@ -862,22 +1557,48 @@ const goalsVerdict = (goals) => {
|
|
|
862
1557
|
for (const g of goals) {
|
|
863
1558
|
const out = grow(g);
|
|
864
1559
|
if (!('tree' in out))
|
|
865
|
-
return { proven: false, checked: false, goal: g.name, stuck: out.stuck, ...(out.witness ? { witness: out.witness } : {}) };
|
|
1560
|
+
return { proven: false, checked: false, goal: g.name, stuck: out.stuck, ...(out.witness ? { witness: out.witness } : {}), ...(out.why ? { why: out.why } : {}) };
|
|
866
1561
|
trees[g.name] = out.tree;
|
|
867
1562
|
}
|
|
868
1563
|
return { proven: true, checked: goals.every(g => check(g, trees[g.name])), trees };
|
|
869
1564
|
};
|
|
870
1565
|
export function proveRule(r, laws) {
|
|
871
1566
|
const out = proveLaws(r.machine, laws);
|
|
872
|
-
return { proven: out.proven, checked: out.checked, ...(out.failed ? { goal: out.failed.goal, stuck: out.failed.stuck, ...(out.failed.witness ? { witness: out.failed.witness } : {}) } : {}) };
|
|
1567
|
+
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 } : {}) } : {}) };
|
|
873
1568
|
}
|
|
874
1569
|
export function proveSteps(r, laws, stepLaws) {
|
|
875
1570
|
const out = proveStepLaws(r.machine, laws, stepLaws);
|
|
876
|
-
return { proven: out.proven, checked: out.checked, ...(out.failed ? { goal: out.failed.goal, stuck: out.failed.stuck, ...(out.failed.witness ? { witness: out.failed.witness } : {}) } : {}) };
|
|
1571
|
+
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 } : {}) } : {}) };
|
|
877
1572
|
}
|
|
878
1573
|
export function proveLeadsTo(r, laws, l) {
|
|
879
1574
|
const out = proveLiveness(r.machine, laws, l);
|
|
880
|
-
return { proven: out.proven, checked: out.checked, ...(out.failed ? { goal: out.failed.goal, stuck: out.failed.stuck, ...(out.failed.witness ? { witness: out.failed.witness } : {}) } : {}) };
|
|
1575
|
+
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 } : {}) } : {}) };
|
|
881
1576
|
}
|
|
882
1577
|
export const proveDecision = (d) => goalsVerdict([d.goal]);
|
|
1578
|
+
const seeded = (seed) => { let x = seed; return k => { x = (x * 1103515245 + 12345) % 2 ** 31; return Math.floor(x / 2 ** 31 * k); }; };
|
|
1579
|
+
// A decision's callbacks against its condition, on random params and requests: `given` and `then` each run with every
|
|
1580
|
+
// value known, against what the interpreter evaluates of the decision built from them.
|
|
1581
|
+
export function crossCheckDecision(d, opts = {}) {
|
|
1582
|
+
const src = sources.get(d)?.decision;
|
|
1583
|
+
if (!src)
|
|
1584
|
+
return { checked: 0 };
|
|
1585
|
+
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);
|
|
1586
|
+
let checked = 0;
|
|
1587
|
+
for (let i = 0; i < (opts.trials ?? 500); i++) {
|
|
1588
|
+
const input = r.randomInput(rand, bound), values = { ...r.randomParams(rand, bound), ...numbersOf(input) };
|
|
1589
|
+
const st = { scalars: {}, maps: Object.fromEntries(Object.entries(input).filter((e) => typeof e[1] !== 'number').map(([n, cells]) => [n, new Map(cells)])) };
|
|
1590
|
+
const keys = exactKeys(keysOf(bound, st), values, st);
|
|
1591
|
+
const sc = { vars: values, maps: Object.fromEntries(Object.entries(st.maps).map(([n, cells]) => [n, { cells, dflt: 0 }])), keys, flags: {} };
|
|
1592
|
+
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]; });
|
|
1593
|
+
for (const [what, ok, c] of [['given', g, own], ['then', t, [d.then]]]) {
|
|
1594
|
+
if (ok === undefined)
|
|
1595
|
+
continue;
|
|
1596
|
+
checked++;
|
|
1597
|
+
const want = c.every(x => evalC(x, values, st, m));
|
|
1598
|
+
if (ok !== want)
|
|
1599
|
+
return { checked, failure: `decision "${d.name}": its ${what} callback says ${ok}, the decision proven says ${want}, at ${JSON.stringify(values)}` };
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
return { checked };
|
|
1603
|
+
}
|
|
883
1604
|
export const proveLoop = (l, laws) => goalsVerdict(loopObligations(l.loop, laws, l.rankTerms));
|