gdp-wired 0.6.0 → 0.8.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
@@ -84,6 +84,16 @@ export const max = (a, b) => new Term({ op: 'max', a: termOf(a), b: termOf(b) },
84
84
  export function when(c, then, otherwise) {
85
85
  if (then instanceof Cond && otherwise instanceof Cond)
86
86
  return choose(c, then, otherwise);
87
+ if (then instanceof Variant && otherwise instanceof Variant) {
88
+ if (then.kind.names.join() !== otherwise.kind.names.join())
89
+ throw new Error('when: variants of other cases');
90
+ return new Variant(when(c, then.tag, otherwise.tag), Object.fromEntries(then.kind.names.map(k => [k, when(c, then.cases[k], otherwise.cases[k])])), then.kind);
91
+ }
92
+ if (isRecord(then) && isRecord(otherwise)) {
93
+ if (Object.keys(then).sort().join() !== Object.keys(otherwise).sort().join())
94
+ throw new Error('when: records of other fields');
95
+ return Object.fromEntries(Object.keys(then).map(f => [f, when(c, then[f], otherwise[f])]));
96
+ }
87
97
  if (then instanceof Choice && otherwise instanceof Choice)
88
98
  return new Choice(when(c, then.n, otherwise.n), then.values);
89
99
  if (then instanceof Str && otherwise instanceof Str)
@@ -105,12 +115,49 @@ function pickNumbers(c, then, otherwise) {
105
115
  }
106
116
  // A condition by cases: `then` where c holds, `otherwise` elsewhere.
107
117
  export const choose = (c, then, otherwise) => any(all(c, then), all(c.not(), otherwise));
118
+ // A record's value: a plain object of its fields' values (not a term, a condition or another value of a kind).
119
+ const isRecord = (x) => typeof x === 'object' && x !== null && Object.getPrototypeOf(x) === Object.prototype;
120
+ // The same value, part by part: numbers equal, conditions both or neither, choices and strings alike, an optional value
121
+ // missing in both or present in both and the same, a variant in the same case with the same fields, a record field by
122
+ // field.
123
+ export function same(a, b) {
124
+ if (a instanceof Cond && b instanceof Cond)
125
+ return a.iff(b);
126
+ if (a instanceof Choice && b instanceof Choice)
127
+ return a.n.eq(b.n);
128
+ if (a instanceof Str && b instanceof Str)
129
+ return a.eq(b);
130
+ if (a instanceof Decimal)
131
+ return a.eq(b);
132
+ if (b instanceof Decimal)
133
+ return b.eq(a);
134
+ if (a instanceof Int)
135
+ return a.eq(b);
136
+ if (b instanceof Int)
137
+ return b.eq(a);
138
+ if (a instanceof Maybe && b instanceof Maybe)
139
+ return all(a.present.eq(b.present), a.isNone.or(same(a.inner, b.inner)));
140
+ if (a instanceof Variant && b instanceof Variant) {
141
+ if (a.kind.names.join() !== b.kind.names.join())
142
+ throw new Error('same: variants of other cases');
143
+ return all(a.tag.eq(b.tag), ...a.kind.names.filter(k => Object.keys(a.cases[k]).length).map(k => a.is(k).implies(same(a.cases[k], b.cases[k]))));
144
+ }
145
+ if (isRecord(a) && isRecord(b)) {
146
+ if (Object.keys(a).sort().join() !== Object.keys(b).sort().join())
147
+ throw new Error('same: records of other fields');
148
+ return all(...Object.keys(a).map(f => same(a[f], b[f])));
149
+ }
150
+ if ((a instanceof Term || typeof a === 'number') && (b instanceof Term || typeof b === 'number'))
151
+ return (typeof a === 'number' ? lit(a) : a).eq(b);
152
+ throw new Error('same: two values of one kind');
153
+ }
108
154
  // 1 where c holds, else 0: what a sum over a history adds per record.
109
155
  export const count = (c) => when(c, 1, 0);
110
156
  export const sum = (first, ...rest) => rest.reduce((acc, t) => acc.plus(t), typeof first === 'number' ? lit(first) : first);
111
157
  const named = (at, n) => (typeof at === 'string' ? at + n : at(n));
112
158
  const primed = (n) => `${n}'`;
113
159
  export class Kind {
160
+ value(name, at = '') { return this.build(name, part => v(named(at, part))); }
114
161
  // A runtime value of the kind (what encode takes), for comparing a rule with the code it describes (compare).
115
162
  sample(_rand, _bound) { throw new Error('this kind draws no runtime values'); }
116
163
  // A state variable of this kind starting at a runtime value, or at a value over the params: `.state({open: bool.from(true)})`.
@@ -235,7 +282,7 @@ export class Decimal {
235
282
  }
236
283
  const asNat = (name, raw) => {
237
284
  if (typeof raw !== 'number' || !Number.isSafeInteger(raw) || raw < 0)
238
- throw new RangeError(`${name} must be a natural, not ${String(raw)}`);
285
+ throw new RangeError(`${name} must be a natural, not ${shown(raw)}`);
239
286
  return raw;
240
287
  };
241
288
  class NatKind extends Kind {
@@ -246,7 +293,7 @@ class NatKind extends Kind {
246
293
  }
247
294
  vars(name) { return [name]; }
248
295
  assume(name) { return this.maxValue === undefined ? [] : [v(name).atMost(this.maxValue)]; }
249
- value(name, at = '') { return v(named(at, name)); }
296
+ build(name, get) { return get(name); }
250
297
  store(name, value) { return { [name]: value }; }
251
298
  decode(name, vars) { return vars[name]; }
252
299
  empty() { return 0; }
@@ -273,7 +320,7 @@ export class Bool extends Cond {
273
320
  class BoolKind extends Kind {
274
321
  vars(name) { return [name]; }
275
322
  assume(name) { return [v(name).atMost(1)]; }
276
- value(name, at = '') { return new Bool(v(named(at, name))); }
323
+ build(name, get) { return new Bool(get(name)); }
277
324
  store(name, value) { return { [name]: typeof value === 'boolean' ? (value ? 1 : 0) : count(value) }; }
278
325
  decode(name, vars) { return vars[name] === 1; }
279
326
  empty() { return false; }
@@ -282,7 +329,7 @@ class BoolKind extends Kind {
282
329
  return { [name]: raw ? 1 : 0 };
283
330
  if (raw === 0 || raw === 1)
284
331
  return { [name]: raw };
285
- throw new RangeError(`${name} must be a boolean, not ${String(raw)}`);
332
+ throw new RangeError(`${name} must be a boolean, not ${shown(raw)}`);
286
333
  }
287
334
  random(name, rand) { return { [name]: rand(2) }; }
288
335
  sample(rand) { return rand(2) === 1; }
@@ -322,14 +369,14 @@ class OneOfKind extends Kind {
322
369
  }
323
370
  vars(name) { return [name]; }
324
371
  assume(name) { return [v(name).lt(this.values.length)]; }
325
- value(name, at = '') { return new Choice(v(named(at, name)), this.values); }
372
+ build(name, get) { return new Choice(get(name), this.values); }
326
373
  store(name, value) { return { [name]: value instanceof Choice ? value.n : this.encode(name, value)[name] }; }
327
374
  decode(name, vars) { return this.values[vars[name]]; }
328
375
  empty() { return this.values[0]; }
329
376
  encode(name, raw) {
330
377
  const i = this.values.indexOf(raw);
331
378
  if (i < 0)
332
- throw new RangeError(`${name} is one of ${this.values.join(', ')}, not ${String(raw)}`);
379
+ throw new RangeError(`${name} is one of ${this.values.join(', ')}, not ${shown(raw)}`);
333
380
  return { [name]: i };
334
381
  }
335
382
  random(name, rand) { return { [name]: rand(this.values.length) }; }
@@ -366,7 +413,7 @@ class MaybeKind extends Kind {
366
413
  has(name) { return `${name}$some`; }
367
414
  vars(name) { return [this.has(name), ...this.of.vars(name)]; }
368
415
  assume(name) { return [v(this.has(name)).atMost(1), ...this.of.assume(name)]; }
369
- value(name, at = '') { return new Maybe(v(named(at, this.has(name))), this.of.value(name, at)); }
416
+ build(name, get) { return new Maybe(get(this.has(name)), this.of.build(name, get)); }
370
417
  // null for none, a Maybe as it is, any other value as some.
371
418
  store(name, value) {
372
419
  if (value === null || value === undefined)
@@ -388,6 +435,158 @@ class MaybeKind extends Kind {
388
435
  sample(rand, bound) { return rand(4) === 0 ? null : this.of.sample(rand, bound); }
389
436
  }
390
437
  export const maybe = (of) => new MaybeKind(of);
438
+ const fieldName = (name, f) => `${name}.${f}`;
439
+ const plainFields = (what, names) => { for (const f of names)
440
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(f))
441
+ throw new Error(`${what}: ${f} is not a plain field name`); };
442
+ const rawObject = (name, raw, what) => {
443
+ if (raw === null || typeof raw !== 'object' || Array.isArray(raw))
444
+ throw new RangeError(`${name} must be ${what}, not ${String(raw)}`);
445
+ return raw;
446
+ };
447
+ // Runtime values alike: objects field by field.
448
+ const sameRaw = (a, b) => {
449
+ if (a === b)
450
+ return true;
451
+ if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null)
452
+ return false;
453
+ const ka = Object.keys(a), kb = Object.keys(b);
454
+ return ka.length === kb.length && ka.every(k => sameRaw(a[k], b[k]));
455
+ };
456
+ const shown = (x) => (typeof x === 'object' && x !== null ? JSON.stringify(x) : String(x));
457
+ // r.<field> is a history record's field to the runtimes.
458
+ const composite = (name) => { if (name === 'r')
459
+ throw new Error('r names a history record; give the record or variant another name'); };
460
+ const merged = (parts) => Object.assign({}, ...parts);
461
+ class RecordKind extends Kind {
462
+ fields;
463
+ names;
464
+ constructor(fields) {
465
+ super();
466
+ this.fields = fields;
467
+ this.names = Object.keys(fields);
468
+ if (this.names.length === 0)
469
+ throw new Error('a record has a field');
470
+ plainFields('record', this.names);
471
+ }
472
+ each(f) { return this.names.map(n => f(n, this.fields[n])); }
473
+ vars(name) { composite(name); return this.each((f, k) => k.vars(fieldName(name, f))).flat(); }
474
+ assume(name) { return this.each((f, k) => k.assume(fieldName(name, f))).flat(); }
475
+ build(name, get) { return Object.fromEntries(this.each((f, k) => [f, k.build(fieldName(name, f), get)])); }
476
+ store(name, value) {
477
+ const o = value;
478
+ if (o === null || typeof o !== 'object')
479
+ throw new Error(`${name}: a record is written as an object of its fields`);
480
+ for (const f of Object.keys(o))
481
+ if (!(f in this.fields))
482
+ throw new Error(`${name}: the record has no field ${f}`);
483
+ return Object.assign({}, ...this.each((f, k) => { if (!(f in o))
484
+ throw new Error(`${name}: the write leaves out the field ${f}`); return k.store(fieldName(name, f), o[f]); }));
485
+ }
486
+ decode(name, vars) { return Object.fromEntries(this.each((f, k) => [f, k.decode(fieldName(name, f), vars)])); }
487
+ empty() { return Object.fromEntries(this.each((f, k) => [f, k.empty()])); }
488
+ encode(name, raw) {
489
+ const o = rawObject(name, raw, `an object with the fields ${this.names.join(', ')}`);
490
+ for (const f of Object.keys(o))
491
+ if (!(f in this.fields))
492
+ throw new RangeError(`${name} has no field ${f}`);
493
+ return merged(this.each((f, k) => k.encode(fieldName(name, f), o[f])));
494
+ }
495
+ random(name, rand, bound) { return merged(this.each((f, k) => k.random(fieldName(name, f), rand, bound))); }
496
+ sample(rand, bound) { return Object.fromEntries(this.each((f, k) => [f, k.sample(rand, bound)])); }
497
+ }
498
+ export const record = (fields) => new RecordKind(fields);
499
+ export class Variant {
500
+ tag;
501
+ cases;
502
+ kind;
503
+ constructor(tag, cases, kind) {
504
+ this.tag = tag;
505
+ this.cases = cases;
506
+ this.kind = kind;
507
+ }
508
+ is(k) { return this.tag.eq(this.kind.index(k)); }
509
+ match(cases) {
510
+ const results = this.kind.names.map(k => [k, cases[k](this.cases[k])]);
511
+ if (results.every(([, r]) => r instanceof Cond))
512
+ return any(...results.map(([k, r]) => all(this.is(k), r)));
513
+ // The last case is what's left: the kind's assumption says the index is below the number of cases.
514
+ return results.slice(0, -1).reduceRight((acc, [k, r]) => when(this.is(k), r, acc), results.at(-1)[1]);
515
+ }
516
+ }
517
+ export class VariantKind extends Kind {
518
+ cases;
519
+ names;
520
+ tagKey = 'tag';
521
+ constructor(cases) {
522
+ super();
523
+ this.cases = cases;
524
+ this.names = Object.keys(cases);
525
+ if (this.names.length === 0)
526
+ throw new Error('a variant has a case');
527
+ plainFields('variant', this.names);
528
+ for (const k of this.names) {
529
+ plainFields(`variant case ${k}`, Object.keys(cases[k]));
530
+ if ('tag' in cases[k])
531
+ throw new Error(`variant case ${k}: tag names the case, not a field`);
532
+ }
533
+ }
534
+ index(k) {
535
+ const i = this.names.indexOf(k);
536
+ if (i < 0)
537
+ throw new Error(`${k} is not a case of ${this.names.join(', ')}`);
538
+ return i;
539
+ }
540
+ payload(k) { return new RecordKind(this.cases[k]); }
541
+ filled(k) { return Object.keys(this.cases[k]).length > 0; }
542
+ at(name, k) { return fieldName(name, k); }
543
+ vars(name) { composite(name); return [name, ...this.names.filter(k => this.filled(k)).flatMap(k => this.payload(k).vars(this.at(name, k)))]; }
544
+ assume(name) { return [v(name).lt(this.names.length), ...this.names.filter(k => this.filled(k)).flatMap(k => this.payload(k).assume(this.at(name, k)))]; }
545
+ build(name, get) {
546
+ return new Variant(get(name), Object.fromEntries(this.names.map(k => [k, this.filled(k) ? this.payload(k).build(this.at(name, k), get) : {}])), this);
547
+ }
548
+ // The value of a case with these fields (terms, values or runtime values), to compare, match or pass to `when`:
549
+ // when(held, At.make({tag: 'holding', amount, since: now}), at)
550
+ make(write) {
551
+ const parts = this.store('$', write);
552
+ return this.build('$', part => { const t = parts[part]; return typeof t === 'number' ? lit(t) : t; });
553
+ }
554
+ // A Variant of this kind as it is, or {tag: case, ...fields} (fields as values or runtime values): the other cases'
555
+ // fields are written empty.
556
+ store(name, value) {
557
+ if (value instanceof Variant) {
558
+ if (value.kind.names.join() !== this.names.join())
559
+ throw new Error(`${name}: a variant of other cases is written`);
560
+ return { [name]: value.tag, ...Object.assign({}, ...this.names.filter(k => this.filled(k)).map(k => this.payload(k).store(this.at(name, k), value.cases[k]))) };
561
+ }
562
+ const o = value, k = o?.[this.tagKey];
563
+ if (typeof k !== 'string')
564
+ throw new Error(`${name}: a variant is written as {${this.tagKey}: case, ...fields}`);
565
+ const { [this.tagKey]: _, ...fields } = o;
566
+ return { [name]: this.index(k), ...Object.assign({}, ...this.names.filter(c => this.filled(c)).map(c => this.payload(c).store(this.at(name, c), c === k ? fields : this.payload(c).empty()))) };
567
+ }
568
+ decode(name, vars) {
569
+ const k = this.names[vars[name]];
570
+ return { [this.tagKey]: k, ...(this.filled(k) ? this.payload(k).decode(this.at(name, k), vars) : {}) };
571
+ }
572
+ empty() { const k = this.names[0]; return { [this.tagKey]: k, ...(this.filled(k) ? this.payload(k).empty() : {}) }; }
573
+ encode(name, raw) {
574
+ const o = rawObject(name, raw, `an object whose ${this.tagKey} is one of ${this.names.join(', ')}`), k = o[this.tagKey];
575
+ if (typeof k !== 'string' || !this.names.includes(k))
576
+ throw new RangeError(`${name}.${this.tagKey} is one of ${this.names.join(', ')}, not ${shown(k)}`);
577
+ const { [this.tagKey]: _, ...fields } = o;
578
+ if (!this.filled(k) && Object.keys(fields).length)
579
+ throw new RangeError(`${name}: the case ${k} has no fields`);
580
+ const zeros = (c) => Object.fromEntries(this.payload(c).vars(this.at(name, c)).map(n => [n, 0]));
581
+ return { [name]: this.index(k), ...merged(this.names.filter(c => this.filled(c)).map(c => (c === k ? this.payload(c).encode(this.at(name, c), fields) : zeros(c)))) };
582
+ }
583
+ random(name, rand, bound) {
584
+ const i = rand(this.names.length);
585
+ return { [name]: i, ...merged(this.names.filter(c => this.filled(c)).map(c => this.payload(c).random(this.at(name, c), rand, bound))) };
586
+ }
587
+ sample(rand, bound) { const k = this.names[rand(this.names.length)]; return { [this.tagKey]: k, ...(this.filled(k) ? this.payload(k).sample(rand, bound) : {}) }; }
588
+ }
589
+ export const variant = (cases) => new VariantKind(cases);
391
590
  // A string, encoded as its interned name, its length and one bit per pattern the kind declares (`like`, Cedar's glob:
392
591
  // `*` any run of characters, `\*` a star): all computed from the string itself when the input is encoded, so the bits
393
592
  // are the string's, not an answer someone supplies. Names come from one table per process, the empty string 0; a law
@@ -480,7 +679,7 @@ class StrKind extends Kind {
480
679
  return [v(name).eq(0).iff(v(`${name}$len`).eq(0)), ...(this.maxLength === undefined ? [] : [v(`${name}$len`).atMost(this.maxLength)]),
481
680
  ...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
681
  }
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)))]))); }
682
+ build(name, get) { return new Str(get(name), get(`${name}$len`), new Map(this.patterns.map((p, i) => [p, get(this.bit(name, i))]))); }
484
683
  store(name, value) {
485
684
  if (typeof value === 'string')
486
685
  return { [name]: literal(value), [`${name}$len`]: value.length, ...Object.fromEntries(this.patterns.map((p, i) => [this.bit(name, i), globMatch(p, value) ? 1 : 0])) };
@@ -493,7 +692,7 @@ class StrKind extends Kind {
493
692
  empty() { return ''; }
494
693
  encode(name, raw) {
495
694
  if (typeof raw !== 'string')
496
- throw new RangeError(`${name} must be a string, not ${String(raw)}`);
695
+ throw new RangeError(`${name} must be a string, not ${shown(raw)}`);
497
696
  if (this.maxLength !== undefined && raw.length > this.maxLength)
498
697
  throw new RangeError(`${name} is at most ${this.maxLength} characters`);
499
698
  return { [name]: intern(raw), [`${name}$len`]: raw.length, ...Object.fromEntries(this.patterns.map((p, i) => [this.bit(name, i), globMatch(p, raw) ? 1 : 0])) };
@@ -513,13 +712,13 @@ export const str = (opts = {}) => new StrKind(opts.like ?? [], opts.maxLength);
513
712
  class IntKind extends Kind {
514
713
  vars(name) { return [name, `${name}$neg`]; }
515
714
  assume() { return []; }
516
- value(name, at = '') { return new Int(v(named(at, name)), v(named(at, `${name}$neg`))); }
715
+ build(name, get) { return new Int(get(name), get(`${name}$neg`)); }
517
716
  store(name, value) { const i = normal(Int.of(value)); return { [name]: i.pos, [`${name}$neg`]: i.neg }; }
518
717
  decode(name, vars) { return vars[name] - vars[`${name}$neg`]; }
519
718
  empty() { return 0; }
520
719
  encode(name, raw) {
521
720
  if (typeof raw !== 'number' || !Number.isSafeInteger(raw))
522
- throw new RangeError(`${name} must be an integer, not ${String(raw)}`);
721
+ throw new RangeError(`${name} must be an integer, not ${shown(raw)}`);
523
722
  return { [name]: Math.max(raw, 0), [`${name}$neg`]: Math.max(-raw, 0) };
524
723
  }
525
724
  random(name, rand, bound) { const x = rand(2 * bound + 1) - bound; return { [name]: Math.max(x, 0), [`${name}$neg`]: Math.max(-x, 0) }; }
@@ -535,7 +734,7 @@ class DecimalKind extends Kind {
535
734
  }
536
735
  vars(name) { return [name, `${name}$neg`]; }
537
736
  assume() { return []; }
538
- value(name, at = '') { return new Decimal(new Int(v(named(at, name)), v(named(at, `${name}$neg`))), this.places); }
737
+ build(name, get) { return new Decimal(new Int(get(name), get(`${name}$neg`)), this.places); }
539
738
  store(name, value) {
540
739
  const i = normal(typeof value === 'string' ? Int.of(scaledOf(value, this.places)) : new Decimal(Int.of(0), this.places).lift(value));
541
740
  return { [name]: i.pos, [`${name}$neg`]: i.neg };
@@ -544,7 +743,7 @@ class DecimalKind extends Kind {
544
743
  empty() { return 0; }
545
744
  encode(name, raw) {
546
745
  if (typeof raw !== 'number' && typeof raw !== 'string')
547
- throw new RangeError(`${name} must be a decimal, not ${String(raw)}`);
746
+ throw new RangeError(`${name} must be a decimal, not ${shown(raw)}`);
548
747
  const x = scaledOf(raw, this.places, `a decimal (${name})`);
549
748
  return { [name]: Math.max(x, 0), [`${name}$neg`]: Math.max(-x, 0) };
550
749
  }
@@ -624,6 +823,64 @@ export class ListRef {
624
823
  increasing() { return forall((i, j) => i.lt(j).and(j.lt(this.len)).implies(this.at(i).lt(this.at(j)))); }
625
824
  }
626
825
  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));
826
+ const isPartsWrite = (u) => typeof u === 'object' && u !== null && 'parts' in u;
827
+ // The parts of a value written at a cell, by the names of the maps they go to.
828
+ const partsOf = (kind, name, value) => kind.store(name, value);
829
+ // A map whose cells hold values of a kind (a record, a variant, a choice...): one map per variable of the kind
830
+ // (<name>.<field>, and the case's index as <name> for a variant), every cell starting at the kind's value given by
831
+ // `from`. `at(k)` is the value at k, read as the kind's value is; `set` and `each` write whole values.
832
+ // .maps({Acct: record({limit: nat, blocked: bool}).from({limit: 0, blocked: false})})
833
+ // .step(({who, Acct}) => ({Acct: Acct.set(who, {limit: 10, blocked: false})}))
834
+ export class CellsRef {
835
+ name;
836
+ kind;
837
+ map;
838
+ constructor(name, kind, map) {
839
+ this.name = name;
840
+ this.kind = kind;
841
+ this.map = map;
842
+ }
843
+ at(key) { return this.kind.build(this.name, part => this.map(part).at(key)); }
844
+ set(key, value) {
845
+ const parts = partsOf(this.kind, this.name, value);
846
+ return { parts: Object.fromEntries(this.kind.vars(this.name).map(part => [part, this.map(part).set(key, parts[part])])) };
847
+ }
848
+ // Every key at once (the key read only as a map's key).
849
+ each(f) {
850
+ return { parts: Object.fromEntries(this.kind.vars(this.name).map(part => [part, this.map(part).each(k => partsOf(this.kind, this.name, f(k))[part])])) };
851
+ }
852
+ }
853
+ export class ItemsRef {
854
+ name;
855
+ kind;
856
+ len;
857
+ list;
858
+ constructor(name, kind, len, list) {
859
+ this.name = name;
860
+ this.kind = kind;
861
+ this.len = len;
862
+ this.list = list;
863
+ }
864
+ get names() { return this.kind.vars(this.name); }
865
+ at(i) { return this.kind.build(this.name, part => this.list(part).at(i)); }
866
+ // x at some position.
867
+ has(x) { return everyKey(k => k.lt(this.len).implies(same(this.at(k), x).not())).not(); }
868
+ op(when, len, f) { return { when, len, parts: Object.fromEntries(this.names.map(part => [part, f(this.list(part), part)])) }; }
869
+ push(x, when = yes) { const p = partsOf(this.kind, this.name, x); return this.op(when, this.len.plus(1), (l, part) => l.push(p[part], when)); }
870
+ pop(when = yes) { return this.op(when, this.len.minus(1), l => l.pop(when)); }
871
+ set(i, x, when = yes) { const p = partsOf(this.kind, this.name, x); return this.op(when, this.len, (l, part) => l.set(i, p[part], when)); }
872
+ insertAt(i, x, when = yes) { const p = partsOf(this.kind, this.name, x); return this.op(when, this.len.plus(1), (l, part) => l.insertAt(i, p[part], when)); }
873
+ removeAt(i, when = yes) { return this.op(when, this.len.minus(1), l => l.removeAt(i, when)); }
874
+ apply(...ops) {
875
+ const parts = {};
876
+ for (const part of this.names)
877
+ parts[part] = this.list(part).apply(...ops.map(op => op.parts[part]))[part];
878
+ return { [this.name]: { parts }, [`${this.name}_len`]: ops.reduceRight((acc, op) => when_(op.when, op.len, acc), this.len) };
879
+ }
880
+ // Laws: nothing past the end; the items in strictly increasing order of `by`.
881
+ within() { return forall(k => this.len.atMost(k).implies(all(...this.names.map(part => this.list(part).at(k).eq(0))))); }
882
+ increasing(by) { return forall((i, j) => i.lt(j).and(j.lt(this.len)).implies(by(this.at(i)).lt(by(this.at(j))))); }
883
+ }
627
884
  // A law about every key of a map: proven at a fresh key, assumed at the keys a step reads.
628
885
  // With two parameters or more, about every two keys or more: forall((i, j) => i.lt(j).implies(...)).
629
886
  export function forall(f) { return { forall: f }; }
@@ -827,7 +1084,131 @@ export const modBy = (c) => {
827
1084
  [`r(x + ${c}) = r(x)`]: forall(x => f(x.plus(c)).eq(f(x))),
828
1085
  }) });
829
1086
  };
830
- const pairE = (a, b) => ({ op: 'if', c: { op: '<', a, b }, then: { op: 'add', a: { op: 'prod', a: b, b }, b: a }, else: { op: 'add', a: { op: 'add', a: { op: 'prod', a, b: a }, b: a }, b } });
1087
+ const reservedInRecur = ['x', 'fx', 'y', 'fy', 'prev', 'x0'];
1088
+ export const recur = (name, def, lemmas = {}, between = {}) => {
1089
+ const plain = /^[A-Za-z][A-Za-z0-9_]*$/;
1090
+ if (!plain.test(name))
1091
+ throw new Error(`recur: ${name} is not a plain name (it names the Lean definition)`);
1092
+ const args = [...(def.args ?? [])], uses = Object.entries(def.fns ?? {});
1093
+ for (const n of [...args, ...uses.map(([u]) => u)])
1094
+ if (!plain.test(n) || reservedInRecur.includes(n) || n === name)
1095
+ throw new Error(`recur ${name}: ${n} can't name an argument or a function here (${reservedInRecur.join(', ')} and ${name} are taken)`);
1096
+ distinctNames(`recur ${name}`, [...args, ...uses.map(([u]) => u)]);
1097
+ if (args.length > 2 && Object.keys(between).length)
1098
+ throw new Error(`recur ${name}: a law between two points takes at most two further arguments`);
1099
+ if (args.length > 3)
1100
+ throw new Error(`recur ${name}: at most three further arguments`);
1101
+ const named = (n) => new Term({ op: 'var', name: n });
1102
+ const argsAt = (ts) => Object.fromEntries(args.map((a, i) => [a, ts[i]]));
1103
+ const usesAt = (at) => Object.fromEntries(uses.map(([u, d]) => [u, fnTerm(at(u), d)]));
1104
+ const envAt = (ts, at = u => u) => ({ ...argsAt(ts), ...usesAt(at) });
1105
+ const zeroAt = (ts) => (typeof def.zero === 'function' ? def.zero(argsAt(ts)) : def.zero);
1106
+ // The recurrence over named variables (x, prev, the arguments; lemmas over x and fx; laws between over x, fx, y, fy),
1107
+ // the functions it reads by their own names: what the runtimes and the Lean export are made from.
1108
+ const own = args.map(named);
1109
+ const zero = termOf(zeroAt(own)), next = termOf(def.next(named('x'), named('prev'), envAt(own)));
1110
+ const only = (what, t, names, maps) => {
1111
+ if (JSON.stringify(t).includes('"op":"all"') || JSON.stringify(t).includes('"op":"flag"'))
1112
+ throw new Error(`recur ${name}: ${what} takes no quantifier`);
1113
+ for (const n of namesIn(t))
1114
+ if (!names.includes(n))
1115
+ throw new Error(`recur ${name}: ${what} reads ${n}`);
1116
+ for (const m of [...JSON.stringify(t).matchAll(/"op":"get","map":"([^"]+)"/g)].map(x => x[1]))
1117
+ if (!maps.includes(m))
1118
+ throw new Error(`recur ${name}: ${what} reads a function or a map it isn't given (${m})`);
1119
+ };
1120
+ only('zero', zero, args, []);
1121
+ only('next', next, ['x', 'prev', ...args], uses.map(([u]) => u));
1122
+ const stated = Object.entries(lemmas).map(([n, f]) => { const holds = f(named('x'), named('fx'), envAt(own)).c; only(`lemma "${n}"`, holds, ['x', 'fx', ...args], uses.map(([u]) => u)); return { name: n, holds }; });
1123
+ const pairs = Object.entries(between).map(([n, f]) => { const holds = f(named('x'), named('fx'), named('y'), named('fy'), envAt(own)).c; only(`law between "${n}"`, holds, ['x', 'fx', 'y', 'fy', ...args], uses.map(([u]) => u)); return { name: n, holds }; });
1124
+ const argKinds = Object.fromEntries(args.map(a => [a, nat]));
1125
+ const failed = (what, v) => { if (!(v.proven && v.checked))
1126
+ throw new Unproven(`${name}'s ${what}`, v); };
1127
+ // The lemmas: x counting up from 0 and fx its value, each lemma a law of that machine.
1128
+ let verdict = { proven: true, checked: true };
1129
+ if (stated.length) {
1130
+ const r = rule(`${name}, by induction`).params(argKinds).fns(def.fns ?? {}).inputs({}).state({ x: 0, fx: ((p) => zeroAt(args.map(a => p[a]))) })
1131
+ .step((env) => ({ x: env.x.plus(1), fx: def.next(env.x, env.fx, envAt(args.map(a => env[a]))) }));
1132
+ verdict = proveRule(r, Object.entries(lemmas).map(([n, f]) => r.law(n, (env) => f(env.x, env.fx, envAt(args.map(a => env[a]))))));
1133
+ failed('lemmas', verdict);
1134
+ }
1135
+ const js = recurJs(name, args, zero, next, uses);
1136
+ const recurrence = { args, zero, next, lemmas: stated, between: pairs, uses: Object.fromEntries(uses.map(([u, d]) => [u, { arity: d.arity, ...(d.lean ? { lean: d.lean } : {}) }])) };
1137
+ // A law about every n keys (forall counts them by the callback's own parameters).
1138
+ const keysOf = (n) => (f) => {
1139
+ if (n === 0)
1140
+ return f();
1141
+ const by = [(a) => f(a), (a, b) => f(a, b), (a, b, c) => f(a, b, c), (a, b, c, d) => f(a, b, c, d)][n - 1];
1142
+ return forall(by);
1143
+ };
1144
+ const facts = (withBetween) => (f, at = {}) => {
1145
+ const env = (ts) => ({ ...argsAt(ts), ...Object.fromEntries(uses.map(([u, d]) => [u, at[u] ?? fnTerm(u, d)])) });
1146
+ return {
1147
+ [`${name}(0)`]: keysOf(args.length)((...as) => f(0, ...as).eq(zeroAt(as))),
1148
+ [`${name}(x + 1)`]: keysOf(1 + args.length)((k, ...as) => f(k.plus(1), ...as).eq(def.next(k, f(k, ...as), env(as)))),
1149
+ ...Object.fromEntries(Object.entries(lemmas).map(([n, l]) => [n, keysOf(1 + args.length)((k, ...as) => l(k, f(k, ...as), env(as)))])),
1150
+ ...(withBetween ? Object.fromEntries(Object.entries(between).map(([n, l]) => [n, keysOf(2 + args.length)((k, j, ...as) => k.atMost(j).implies(l(k, f(k, ...as), j, f(j, ...as), env(as))))])) : {}),
1151
+ };
1152
+ };
1153
+ const base = { arity: 1 + args.length, js, lean: `Recur.${name}`, recur: recurrence, ...(uses.length ? { uses: def.fns } : {}) };
1154
+ // The laws between two points: y counting up from x, fy from f(x) (a param, given as f(x) by a fact), each law a law
1155
+ // of that machine beside "fy is f(y)", which lets the lemmas speak at y.
1156
+ if (pairs.length) {
1157
+ const self = { ...base, facts: facts(false) };
1158
+ const r = rule(`${name}, between`).params({ ...argKinds, x: nat, fx: nat }).fns({ [name]: self }).inputs({})
1159
+ .facts(((p) => ({ [`fx is ${name}(x)`]: p.fx.eq(p[name](p.x, ...args.map(a => p[a]))) })))
1160
+ .state({ y: ((p) => p.x), fy: ((p) => p.fx) })
1161
+ .step((env) => ({ y: env.y.plus(1), fy: def.next(env.y, env.fy, envAt(args.map(a => env[a]), u => `${name}.${u}`)) }));
1162
+ const at = (env) => args.map(a => env[a]);
1163
+ const laws = [r.law(`fy is ${name}(y)`, (env) => env.fy.eq(env[name](env.y, ...at(env)))),
1164
+ ...Object.entries(between).map(([n, l]) => r.law(n, (env) => l(env.x, env.fx, env.y, env.fy, envAt(at(env), u => `${name}.${u}`))))];
1165
+ const v = proveRule(r, laws);
1166
+ failed('laws between two points', v);
1167
+ verdict = { proven: true, checked: verdict.checked && v.checked };
1168
+ }
1169
+ return { ...base, verdict, facts: facts(true) };
1170
+ };
1171
+ const distinctNames = (what, names) => { if (new Set(names).size !== names.length)
1172
+ throw new Error(`${what}: a name given twice`); };
1173
+ // A recurrence as JavaScript: a function of its arguments, the functions it reads inlined (each as a function of its key,
1174
+ // as the runtimes call it).
1175
+ const recurJs = (name, args, zero, next, uses) => {
1176
+ const fns = new Map(uses.map(([u], i) => [u, `$f${i}`]));
1177
+ const js = (e) => {
1178
+ switch (e.op) {
1179
+ case 'var': return e.name;
1180
+ case 'const': return String(e.value);
1181
+ case 'add': return `(${js(e.a)} + ${js(e.b)})`;
1182
+ case 'sub': return `Math.max(0, ${js(e.a)} - ${js(e.b)})`;
1183
+ case 'mul': return `(${e.k} * ${js(e.a)})`;
1184
+ case 'prod': return `(${js(e.a)} * ${js(e.b)})`;
1185
+ case 'min': return `Math.min(${js(e.a)}, ${js(e.b)})`;
1186
+ case 'max': return `Math.max(${js(e.a)}, ${js(e.b)})`;
1187
+ case 'if': return `(${jc(e.c)} ? ${js(e.then)} : ${js(e.else)})`;
1188
+ case 'get': return `${fns.get(e.map)}(${js(e.key)})`;
1189
+ }
1190
+ };
1191
+ const jc = (c) => {
1192
+ switch (c.op) {
1193
+ case '<': return `(${js(c.a)} < ${js(c.b)})`;
1194
+ case '<=': return `(${js(c.a)} <= ${js(c.b)})`;
1195
+ case '=': return `(${js(c.a)} === ${js(c.b)})`;
1196
+ case 'and': return c.xs.length ? `(${c.xs.map(jc).join(' && ')})` : 'true';
1197
+ case 'or': return c.xs.length ? `(${c.xs.map(jc).join(' || ')})` : 'false';
1198
+ case 'not': return `(!${jc(c.x)})`;
1199
+ case 'flag':
1200
+ case 'all': throw new Error('a recurrence takes no flag or quantifier');
1201
+ }
1202
+ };
1203
+ const inlined = uses.map(([u, d], i) => `const $f${i} = (${fnSource(u, d)}); `).join('');
1204
+ return `(x, ${args.map(a => `${a}, `).join('')}) => { ${inlined}let prev = ${js(zero)}; for (let x0 = 0; x0 < x; x0++) { prev = ((x, prev) => ${js(next)})(x0, prev); if (!Number.isSafeInteger(prev)) throw new RangeError(${JSON.stringify(`${name} passes the safe integers`)}); } return prev; }`;
1205
+ };
1206
+ // Functions with the functions they read, those first (named <function>.<use>), each with the terms its facts read them by.
1207
+ const flatFns = (defs) => defs.flatMap(([n, d]) => {
1208
+ const uses = Object.entries(d.uses ?? {}).map(([u, ud]) => [`${n}.${u}`, ud]);
1209
+ return [...flatFns(uses), [n, d, Object.fromEntries(uses.map(([un, ud]) => [un.slice(n.length + 1), fnTerm(un, ud)]))]];
1210
+ });
1211
+ const pairE = (a, b) => { const s = { op: 'add', a, b }; return { op: 'add', a: { op: 'prod', a: s, b: s }, b }; };
831
1212
  const argsKey = (args) => args.slice(1).reduce(pairE, args[0]);
832
1213
  // A function's JavaScript as one function of its key: its arguments unpaired (left-nested, as argsKey pairs them).
833
1214
  const fnSource = (name, d) => {
@@ -842,7 +1223,7 @@ const fnSource = (name, d) => {
842
1223
  }
843
1224
  if (d.arity === 1)
844
1225
  return src;
845
- return `k => { const u = z => { const s = Math.floor(Math.sqrt(z)); return z - s * s < s ? [z - s * s, s] : [s, z - s * s - s]; }; const a = []; let z = k; for (let i = 1; i < ${d.arity}; i++) { const [p, q] = u(z); a.unshift(q); z = p; } a.unshift(z); return (${src})(...a); }`;
1226
+ return `k => { const u = z => { let s = Math.floor(Math.sqrt(z)); while (s * s > z) s--; while ((s + 1) * (s + 1) <= z) s++; return [s - (z - s * s), z - s * s]; }; const a = []; let z = k; for (let i = 1; i < ${d.arity}; i++) { const [p, q] = u(z); a.unshift(q); z = p; } a.unshift(z); return (${src})(...a); }`;
846
1227
  };
847
1228
  const fnTerm = (name, d) => (...args) => {
848
1229
  if (args.length !== d.arity)
@@ -916,7 +1297,24 @@ export class Rule {
916
1297
  }
917
1298
  return this.with({ state: [...this.spec.state, ...entries], kinded: [...(this.spec.kinded ?? []), ...kinded] });
918
1299
  }
919
- maps(init) { return this.with({ maps: [...this.spec.maps, ...Object.entries(init)] }); }
1300
+ // A map of naturals starts every cell at a natural; a map of a kind at the kind's value: `record({...}).from({...})`.
1301
+ maps(init) {
1302
+ const maps = [], cells = [];
1303
+ for (const [n, x] of Object.entries(init)) {
1304
+ if (typeof x === 'number') {
1305
+ maps.push([n, x]);
1306
+ continue;
1307
+ }
1308
+ const { kind, init: start } = x;
1309
+ if (typeof start === 'function')
1310
+ throw new Error(`map ${n}: every cell starts at one value (not a term over the params)`);
1311
+ const enc = kind.encode(n, start);
1312
+ cells.push([n, kind]);
1313
+ for (const part of kind.vars(n))
1314
+ maps.push([part, enc[part]]);
1315
+ }
1316
+ return this.with({ maps: [...this.spec.maps, ...maps], cells: [...(this.spec.cells ?? []), ...cells] });
1317
+ }
920
1318
  // Integers (Int), each starting at an integer or at one over the params; kept as two naturals, <name> and <name>$neg.
921
1319
  ints(init) {
922
1320
  return this.state(Object.fromEntries(Object.entries(init).map(([n, x]) => [n, int.from(x)])));
@@ -930,6 +1328,13 @@ export class Rule {
930
1328
  lists(...names) {
931
1329
  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] });
932
1330
  }
1331
+ // Lists whose items are values of a kind, each empty at the start: one list per variable of the kind, of one length
1332
+ // <name>_len (ItemsRef).
1333
+ items(kinds) {
1334
+ const entries = Object.entries(kinds);
1335
+ return this.with({ maps: [...this.spec.maps, ...entries.flatMap(([n, k]) => k.vars(n).map(part => [part, 0]))],
1336
+ state: [...this.spec.state, ...entries.map(([n]) => [`${n}_len`, 0])], items: [...(this.spec.items ?? []), ...entries] });
1337
+ }
933
1338
  // Functions defined outside the rule (fn, divBy, modBy), read like params that take arguments: f(x).
934
1339
  fns(defs) {
935
1340
  return this.with({ fns: [...(this.spec.fns ?? []), ...Object.entries(defs)] });
@@ -1073,6 +1478,7 @@ export class Rule {
1073
1478
  after[n] = new MapRef(`${n}'`, init);
1074
1479
  for (const n of this.spec.lists ?? [])
1075
1480
  after[n] = new ListRef(`${n}'`, v(`${n}_len'`));
1481
+ this.composites(after, part => `${part}'`, n => v(`${n}_len'`));
1076
1482
  for (const [n, k] of this.spec.kinded ?? []) {
1077
1483
  for (const part of k.vars(n))
1078
1484
  delete after[part];
@@ -1101,6 +1507,7 @@ export class Rule {
1101
1507
  out[n] = new MapRef(n, init);
1102
1508
  for (const n of this.spec.lists ?? [])
1103
1509
  out[n] = new ListRef(n, v(`${n}_len`));
1510
+ this.composites(out, part => part, n => v(`${n}_len`));
1104
1511
  for (const [n] of this.spec.sums)
1105
1512
  out[n] = v(n);
1106
1513
  for (const [n, k] of this.spec.kinded ?? []) {
@@ -1121,12 +1528,27 @@ export class Rule {
1121
1528
  }
1122
1529
  return out;
1123
1530
  }
1531
+ // Maps and lists of a kind in place of their parts' maps (and a list's length), each part's map named by `at`.
1532
+ composites(out, at, len) {
1533
+ const init = new Map(this.spec.maps);
1534
+ for (const [n, k] of this.spec.cells ?? []) {
1535
+ for (const part of k.vars(n))
1536
+ delete out[part];
1537
+ out[n] = new CellsRef(n, k, part => new MapRef(at(part), init.get(part)));
1538
+ }
1539
+ for (const [n, k] of this.spec.items ?? []) {
1540
+ for (const part of k.vars(n))
1541
+ delete out[part];
1542
+ out[n] = new ItemsRef(n, k, len(n), part => new ListRef(at(part), len(n)));
1543
+ }
1544
+ }
1124
1545
  // Every name the rule declares is declared once (a flag named again replaces the earlier one, so flags are checked as
1125
1546
  // they are defined): an input and a state variable sharing a name would hand the callbacks only one of them.
1126
1547
  names() {
1127
1548
  const s = this.spec;
1128
1549
  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
1550
  ...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']),
1551
+ ...[...(s.cells ?? []), ...(s.items ?? [])].filter(([n, k]) => !k.vars(n).includes(n)).map(([n]) => [n, 'a map of a kind']),
1130
1552
  ];
1131
1553
  }
1132
1554
  whatIs(n) { return this.names().find(([m]) => m === n)?.[1] ?? 'something else'; }
@@ -1173,6 +1595,7 @@ export class Rule {
1173
1595
  delete updates[n];
1174
1596
  Object.assign(updates, k.store(n, u));
1175
1597
  }
1598
+ expandParts(updates);
1176
1599
  const state = {}, maps = {};
1177
1600
  for (const [n, u] of Object.entries(updates)) {
1178
1601
  if (u === undefined)
@@ -1189,9 +1612,11 @@ export class Rule {
1189
1612
  const assume = [...this.kindAssumptions('inputs'), ...s.assume.map(f => hoist.cond(f(env).c))];
1190
1613
  placed();
1191
1614
  // Functions, and their facts: each function's own (named after it), then the rule's.
1192
- const fns = Object.fromEntries((s.fns ?? []).map(([n, d]) => [n, { js: fnSource(n, d), ...(d.lean ? { lean: d.lean } : {}) }]));
1615
+ // The functions a function reads come first, each named <function>.<use>.
1616
+ const flat = flatFns(s.fns ?? []);
1617
+ const fns = Object.fromEntries(flat.map(([n, d]) => [n, { js: fnSource(n, d), ...(d.lean ? { lean: d.lean } : {}), ...(d.recur ? { recur: d.recur } : {}) }]));
1193
1618
  const facts = [
1194
- ...(s.fns ?? []).flatMap(([n, d]) => Object.entries(d.facts?.(fnTerm(n, d)) ?? {}).map(([name, f]) => this.lawOf(`${n}: ${name}`, f))),
1619
+ ...flat.flatMap(([n, d, uses]) => Object.entries(d.facts?.(fnTerm(n, d), uses) ?? {}).map(([name, f]) => this.lawOf(`${n}: ${name}`, f))),
1195
1620
  ...(s.facts ?? []).flatMap(f => Object.entries(f(pEnv)).map(([name, out]) => this.lawOf(name, out))),
1196
1621
  ];
1197
1622
  const m = {
@@ -1311,7 +1736,7 @@ export class Rule {
1311
1736
  }
1312
1737
  // The updates: every state variable as its value is written (a kinded one through its kind's `store`, as the
1313
1738
  // machine is built) or kept, and each map write.
1314
- const updates = inScope(sc, () => s.step?.(this.env()) ?? {});
1739
+ const updates = inScope(sc, () => expandParts(s.step?.(this.env()) ?? {}));
1315
1740
  const written = { ...updates };
1316
1741
  inScope(sc, () => { for (const [n, k] of s.kinded ?? [])
1317
1742
  if (updates[n] !== undefined) {
@@ -1385,8 +1810,8 @@ export class Rule {
1385
1810
  const got = this.decode(ours);
1386
1811
  for (const [field, want] of Object.entries(theirs)) {
1387
1812
  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` };
1813
+ if (!sameRaw(got[field], want))
1814
+ return { checked, failure: `${s.name}, params ${JSON.stringify(params)}, after ${JSON.stringify(request)}: ${field} is ${shown(got[field])} here, ${shown(want)} in the code it describes` };
1390
1815
  }
1391
1816
  }
1392
1817
  }
@@ -1421,6 +1846,14 @@ export class Rule {
1421
1846
  delete out[part];
1422
1847
  out[n] = k.decode(n, st.scalars);
1423
1848
  }
1849
+ // A map of a kind: the keys any of its parts holds a cell at, each with its value; a list of a kind: its items.
1850
+ const cell = (n, k, key) => k.decode(n, Object.fromEntries(k.vars(n).map(part => [part, st.maps[part]?.get(key) ?? st.defaults?.[part] ?? new Map(this.spec.maps).get(part)])));
1851
+ for (const [n, k] of this.spec.cells ?? []) {
1852
+ const keys = [...new Set(k.vars(n).flatMap(part => [...(st.maps[part]?.keys() ?? [])]))].sort((a, b) => a - b);
1853
+ out[n] = Object.fromEntries(keys.map(key => [key, cell(n, k, key)]));
1854
+ }
1855
+ for (const [n, k] of this.spec.items ?? [])
1856
+ out[n] = Array.from({ length: st.scalars[`${n}_len`] }, (_, i) => cell(n, k, i));
1424
1857
  return out;
1425
1858
  }
1426
1859
  // The prune order's variable, if the history is pruned (a runtime must send it nondecreasing).
@@ -1430,10 +1863,20 @@ export class Rule {
1430
1863
  return loadJs(this.machine);
1431
1864
  }
1432
1865
  }
1866
+ // A step's writes to maps and lists of a kind, as the writes to their parts' maps.
1867
+ const expandParts = (updates) => {
1868
+ for (const [n, u] of Object.entries(updates))
1869
+ if (isPartsWrite(u)) {
1870
+ delete updates[n];
1871
+ Object.assign(updates, u.parts);
1872
+ }
1873
+ return updates;
1874
+ };
1433
1875
  const encode = (kinds, values) => Object.assign({}, ...kinds.map(([n, k]) => k.encode(n, values[n])));
1434
1876
  export const rule = (name) => new Rule({ name, params: [], inputs: [], state: [], maps: [], flags: [], pre: [], assume: [], sums: [], queries: [] });
1435
1877
  // 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);
1878
+ 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 })
1879
+ : t instanceof Variant ? t.kind : isRecord(t) ? record(Object.fromEntries(Object.entries(t).map(([f, x]) => [f, kindOf(x)]))) : nat);
1437
1880
  export class Fun {
1438
1881
  r;
1439
1882
  out;
@@ -1457,11 +1900,11 @@ export class Fun {
1457
1900
  }
1458
1901
  // Whenever the arguments (and the result) meet `given`, the result meets `then`.
1459
1902
  law(name, given, then) {
1460
- return this.r.decide(name, env => given(env, this.result(env)), env => then(this.result(env), env));
1903
+ return lawOfFun(this, this.r.decide(name, env => given(env, this.result(env)), env => then(this.result(env), env)));
1461
1904
  }
1462
1905
  // A law about two calls under the same params: `then(outA, outB, a, b)` given `given(a, b)` (noninterference, say).
1463
1906
  relate(name, given, then) {
1464
- return this.r.relate(name, given, (a, b) => then(this.result(a), this.result(b), a, b));
1907
+ return lawOfFun(this, this.r.relate(name, given, (a, b) => then(this.result(a), this.result(b), a, b)));
1465
1908
  }
1466
1909
  // Against the code it describes: `reference` called with the same arguments (drawn by their kinds, or by `draw`, the
1467
1910
  // `edges` first) must return the same value for every field it returns (a field it leaves out is not compared). The
@@ -1475,8 +1918,8 @@ export class Fun {
1475
1918
  const ours = this.run(values, params), theirs = reference(values, params);
1476
1919
  for (const [field, want] of Object.entries(theirs)) {
1477
1920
  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` };
1921
+ if (!sameRaw(ours[field], want))
1922
+ return { checked, failure: `${this.name} at ${JSON.stringify(values)}: ${field} is ${shown(ours[field])} here, ${shown(want)} in the code it describes` };
1480
1923
  }
1481
1924
  }
1482
1925
  return { checked };
@@ -1499,6 +1942,11 @@ export class Fun {
1499
1942
  }
1500
1943
  }
1501
1944
  export const fun = (name) => new Fun(rule(name));
1945
+ // The function a law was stated about. Variants of one function share its arguments, so a law's terms alone don't say
1946
+ // which result it is about; a guard (gdp.ts) takes only laws stated about the function it runs.
1947
+ const lawsOfFuns = new WeakMap();
1948
+ const lawOfFun = (f, d) => { lawsOfFuns.set(d, f); return d; };
1949
+ export const funOf = (d) => lawsOfFuns.get(d);
1502
1950
  // A loop runs `body` while `while` holds, each turn with fresh inputs; it halts if a rank over the state and params
1503
1951
  // (or a tuple of them, compared in order) drops on every turn.
1504
1952
  export class LoopRule {
@@ -1552,6 +2000,15 @@ export class LoopRule {
1552
2000
  randomInput(rand, bound) { return Object.assign({}, ...this.spec.inputs.map(([n, k]) => k.random(n, rand, bound))); }
1553
2001
  }
1554
2002
  export const loop = (name) => new LoopRule({ name, params: [], inputs: [], state: [], pre: [], assume: [] });
2003
+ // A verdict that isn't a checked proof, as the error a lemma, a guard or a gate is refused with (the verdict on `.verdict`).
2004
+ export class Unproven extends Error {
2005
+ verdict;
2006
+ constructor(what, verdict) {
2007
+ const found = [verdict.goal && `goal ${verdict.goal}`, verdict.stuck, verdict.witness && `a counterexample: ${JSON.stringify(verdict.witness)}`, verdict.why].filter(Boolean).join('; ');
2008
+ super(`${what}: ${verdict.proven ? 'proven, but the kernel did not check the proof' : `not proven${found ? ` (${found})` : ''}`}`);
2009
+ this.verdict = verdict;
2010
+ }
2011
+ }
1555
2012
  const goalsVerdict = (goals) => {
1556
2013
  const trees = {};
1557
2014
  for (const g of goals) {