gdp-wired 0.3.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/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 { 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) => (typeof t === 'number' ? { op: 'const', value: naturalConst(t) } : t.e);
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
- constructor(e) {
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
- constructor(c) {
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 const when = (c, then, otherwise) => new Term({ op: 'if', c: c.c, then: termOf(then), else: termOf(otherwise) });
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,32 +354,179 @@ 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
+ };
208
503
  let boundCounter = 0;
209
504
  // A condition at every key: only as a whole flag (`flags: ({...}) => ({covered: everyKey(k => ...)})`), the key read
210
505
  // 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 }); };
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)); };
212
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
213
508
  // key, given here or found by the prover among a few candidates (kernel/prove.ts).
214
509
  export const someKey = (f, witness) => {
215
510
  const key = `s$${++boundCounter}`;
216
- return new Cond({ op: 'not', x: { op: 'all', key, c: f(v(key)).not().c, ...(witness === undefined ? {} : { witness: termOf(witness) }) } });
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);
217
513
  };
218
514
  // A map from naturals to naturals, every cell starting at `init`. One write per map per step.
219
515
  export class MapRef {
220
516
  name;
221
517
  init;
518
+ cells;
222
519
  constructor(name, init) {
223
520
  this.name = name;
224
521
  this.init = init;
522
+ this.cells = scope?.maps[name];
225
523
  }
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) }; }
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) } }; }
228
526
  // key's cell plus `by` where `c` holds (the counter every cap keeps).
229
527
  bump(key, c = yes, by = 1) { return this.set(key, when(c, this.at(key).plus(by), this.at(key))); }
230
528
  // 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))) }; }
529
+ each(f) { const each = `k$${++boundCounter}`; return { map: this.name, each, value: termOf(f(v(each))), concrete: { at: f } }; }
232
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.
233
531
  has(key) { return this.at(key).atLeast(1); }
234
532
  subsetOf(other) { return everyKey(k => this.at(k).atMost(other.at(k))); }
@@ -242,11 +540,13 @@ export class MapRef {
242
540
  export class ListRef {
243
541
  name;
244
542
  len;
543
+ cells;
245
544
  constructor(name, len) {
246
545
  this.name = name;
247
546
  this.len = len;
547
+ this.cells = scope?.maps[name];
248
548
  }
249
- at(i) { return new Term({ op: 'get', map: this.name, key: termOf(i) }); }
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); }
250
550
  // Conditions: x at some position; each position below the next.
251
551
  has(x) { return everyKey(k => k.lt(this.len).implies(this.at(k).ne(x))).not(); }
252
552
  sorted() { return everyKey(k => k.plus(1).lt(this.len).implies(this.at(k).lt(this.at(k.plus(1))))); }
@@ -258,16 +558,16 @@ export class ListRef {
258
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) }; }
259
559
  // The updates for the step: the first operation whose condition holds, else the list as it was.
260
560
  apply(...ops) {
261
- const each = `k$${++boundCounter}`, k = v(each);
262
- const at = ops.reduceRight((acc, op) => when_(op.when, op.at(k), acc), this.at(k));
561
+ const each = `k$${++boundCounter}`;
562
+ const at = (k) => ops.reduceRight((acc, op) => when_(op.when, op.at(k), acc), this.at(k));
263
563
  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 };
564
+ return { [this.name]: { map: this.name, each, value: at(v(each)).e, concrete: { at } }, [`${this.name}_len`]: len };
265
565
  }
266
566
  // Laws: nothing past the end; positions strictly increasing (so no name twice), as one law about every two keys.
267
567
  within() { return forall(k => this.len.atMost(k).implies(this.at(k).eq(0))); }
268
568
  increasing() { return forall((i, j) => i.lt(j).and(j.lt(this.len)).implies(this.at(i).lt(this.at(j)))); }
269
569
  }
270
- const when_ = (c, then, otherwise) => new Term({ op: 'if', c: c.c, then: termOf(then), else: termOf(otherwise) });
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));
271
571
  // A law about every key of a map: proven at a fresh key, assumed at the keys a step reads.
272
572
  // With two parameters or more, about every two keys or more: forall((i, j) => i.lt(j).implies(...)).
273
573
  export function forall(f) { return { forall: f }; }
@@ -491,9 +791,22 @@ const fnSource = (name, d) => {
491
791
  const fnTerm = (name, d) => (...args) => {
492
792
  if (args.length !== d.arity)
493
793
  throw new Error(`function ${name} takes ${d.arity} argument${d.arity === 1 ? '' : 's'}`);
494
- return new Term({ op: 'get', map: name, key: argsKey(args.map(termOf)) });
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;
495
806
  };
496
807
  let keyCounter = 0;
808
+ const sources = new WeakMap();
809
+ const withSource = (x, src) => (sources.set(x, src), x);
497
810
  // A k-step law's later requests are built over marked names (j:name, which no rule can declare: ':' is reserved); the
498
811
  // kernel reads them as name@j.
499
812
  const markedName = (name) => { const m = /^(\d+):(.*)$/.exec(name); return m ? `${m[2]}@${m[1]}` : name; };
@@ -528,6 +841,24 @@ export class Rule {
528
841
  return this.with({ state: [...this.spec.state, ...Object.entries(init)] });
529
842
  }
530
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
+ }
531
862
  // Lists, each empty at the start: a map of positions and its length <name>_len (ListRef).
532
863
  lists(...names) {
533
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] });
@@ -571,7 +902,7 @@ export class Rule {
571
902
  // (the kernel proves it adds nothing to any query).
572
903
  prune(order, after) { return this.with({ prune: { order, after: after } }); }
573
904
  // 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 }))); }
905
+ law(name, f) { return withSource(this.lawOf(name, f(this.env({ flags: false, inputs: false, queries: false }))), { law: f }); }
575
906
  lawOf(name, out) {
576
907
  const key = `key$${++keyCounter}`;
577
908
  if (out instanceof Cond) {
@@ -595,14 +926,18 @@ export class Rule {
595
926
  after[n] = new MapRef(`${n}'`, init);
596
927
  for (const n of this.spec.lists ?? [])
597
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
+ }
598
933
  const out = then(env, after), w = when ? { when: when(env).c } : {};
599
- const key = `key$${++keyCounter}`;
934
+ const key = `key$${++keyCounter}`, src = { step: { then: then, ...(when ? { when: when } : {}) } };
600
935
  if (out instanceof Cond) {
601
936
  const p = prenex(out.c, `step law ${name}`, key);
602
- return p ? { name, forall: key, then: p, ...w } : { name, then: out.c, ...w };
937
+ return withSource(p ? { name, forall: key, then: p, ...w } : { name, then: out.c, ...w }, src);
603
938
  }
604
939
  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 };
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);
606
941
  }
607
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
608
943
  // state, maps and sums after the last, with each step's request and queries (requests[0] is the first's); `when(before,
@@ -619,6 +954,10 @@ export class Rule {
619
954
  after[n] = new MapRef(`${n}'`, init);
620
955
  for (const n of this.spec.lists ?? [])
621
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
+ }
622
961
  // The j-th request's values over marked names, renamed to the kernel's input@j once built.
623
962
  const mark = (j) => `${j}:`;
624
963
  const requests = Array.from({ length: k }, (_, j) => {
@@ -655,12 +994,24 @@ export class Rule {
655
994
  const base = { name, from: spec.from(env).c, to: spec.to(env).c, rank };
656
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) })) };
657
996
  }
658
- // 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.
659
999
  decide(name, given, then) {
660
1000
  const env = this.env({ flags: false, state: false, queries: false });
661
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)];
662
- const t = then(env).c;
663
- return { name, rule: this, given: hyps, then: t, goal: { name, hyps: [...hyps, { op: 'not', x: t }] } };
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)));
664
1015
  }
665
1016
  // --- what it compiles to ------------------------------------------------------------------------------------------
666
1017
  kindAssumptions(which = 'both') {
@@ -668,8 +1019,14 @@ export class Rule {
668
1019
  const is = which === 'params' ? [] : this.spec.inputs.flatMap(([n, k]) => k.assume(n));
669
1020
  return [...ps, ...is].map(c => c.c);
670
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
+ }
671
1027
  // The values handed to the builders' callbacks.
672
1028
  env(parts = {}) {
1029
+ this.distinct();
673
1030
  const out = {};
674
1031
  for (const [n, k] of this.spec.params)
675
1032
  out[n] = k.value(n);
@@ -690,16 +1047,41 @@ export class Rule {
690
1047
  out[n] = new ListRef(n, v(`${n}_len`));
691
1048
  for (const [n] of this.spec.sums)
692
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
+ }
693
1054
  }
694
1055
  if (parts.queries !== false)
695
1056
  for (const [n] of this.spec.queries)
696
1057
  out[n] = v(n);
697
1058
  if (parts.flags !== false)
698
1059
  for (const f of this.spec.flags)
699
- for (const n of Object.keys(f(out)))
700
- out[n] = new Cond({ op: 'flag', name: 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
+ }
701
1065
  return out;
702
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
+ }
703
1085
  get machine() {
704
1086
  const s = this.spec;
705
1087
  const params = s.params.flatMap(([n, k]) => k.vars(n)), queries = s.queries.map(([n]) => n);
@@ -709,9 +1091,11 @@ export class Rule {
709
1091
  const declared = {};
710
1092
  for (const f of s.flags)
711
1093
  for (const [n, c] of Object.entries(f(env))) {
1094
+ if (n in env && !(n in declared))
1095
+ throw new Error(`${s.name}: ${n} is a flag and ${this.whatIs(n)}`);
712
1096
  declared[n] = c.c;
713
- env[n] = new Cond({ op: 'flag', name: n });
714
- } // a name again: replaced in place
1097
+ env[n] = new Cond({ op: 'flag', name: n }); // a name again: replaced in place
1098
+ }
715
1099
  // Quantifiers anywhere, named as flags: each placed before the flag that reads it.
716
1100
  const hoist = new Hoister('all$'), flags = {};
717
1101
  const placed = () => { for (const [n, c] of Object.entries(hoist.flags))
@@ -723,7 +1107,16 @@ export class Rule {
723
1107
  if (!(n in flags))
724
1108
  flags[n] = out;
725
1109
  }
726
- const updates = s.step?.(env) ?? {};
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
+ }
727
1120
  const state = {}, maps = {};
728
1121
  for (const [n, u] of Object.entries(updates)) {
729
1122
  if (u === undefined)
@@ -775,6 +1168,191 @@ export class Rule {
775
1168
  }
776
1169
  return m;
777
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
+ }
778
1356
  // --- running it -----------------------------------------------------------------------------------------------------
779
1357
  // Runtime values (numbers, booleans, choice names, null for a missing value) to the variables the machine reads.
780
1358
  encodeParams(values) { return encode(this.spec.params, values); }
@@ -795,6 +1373,16 @@ export class Rule {
795
1373
  out[n] = new Map(Array.from({ length: bound + 1 }, (_, key) => [key, rand(2)]).filter(([, cell]) => cell === 1));
796
1374
  return out;
797
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
+ }
798
1386
  // The prune order's variable, if the history is pruned (a runtime must send it nondecreasing).
799
1387
  get orderedBy() { return this.spec.prune ? new Map(this.spec.inputs).get(this.spec.prune.order).vars(this.spec.prune.order)[0] : undefined; }
800
1388
  interpreter() { return interpret(this.machine); }
@@ -804,6 +1392,73 @@ export class Rule {
804
1392
  }
805
1393
  const encode = (kinds, values) => Object.assign({}, ...kinds.map(([n, k]) => k.encode(n, values[n])));
806
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));
807
1462
  // A loop runs `body` while `while` holds, each turn with fresh inputs; it halts if a rank over the state and params
808
1463
  // (or a tuple of them, compared in order) drops on every turn.
809
1464
  export class LoopRule {
@@ -862,22 +1517,48 @@ const goalsVerdict = (goals) => {
862
1517
  for (const g of goals) {
863
1518
  const out = grow(g);
864
1519
  if (!('tree' in out))
865
- 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 } : {}) };
866
1521
  trees[g.name] = out.tree;
867
1522
  }
868
1523
  return { proven: true, checked: goals.every(g => check(g, trees[g.name])), trees };
869
1524
  };
870
1525
  export function proveRule(r, laws) {
871
1526
  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 } : {}) } : {}) };
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 } : {}) } : {}) };
873
1528
  }
874
1529
  export function proveSteps(r, laws, stepLaws) {
875
1530
  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 } : {}) } : {}) };
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 } : {}) } : {}) };
877
1532
  }
878
1533
  export function proveLeadsTo(r, laws, l) {
879
1534
  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 } : {}) } : {}) };
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 } : {}) } : {}) };
881
1536
  }
882
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
+ }
883
1564
  export const proveLoop = (l, laws) => goalsVerdict(loopObligations(l.loop, laws, l.rankTerms));