gdp-wired 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -6
- package/dist/core/dsl.d.ts +164 -13
- package/dist/core/dsl.js +525 -27
- package/dist/core/gdp.d.ts +4 -8
- package/dist/core/gdp.js +4 -11
- package/dist/core/machine.d.ts +28 -0
- package/dist/kernel/prove.js +39 -2
- package/dist/lean-kernel/emit.d.ts +9 -1
- package/dist/lean-kernel/emit.js +264 -20
- package/dist/lean-kernel/lean-terms.d.ts +4 -0
- package/dist/lean-kernel/lean-terms.js +38 -0
- package/dist/lean-kernel/library.js +5 -5
- package/package.json +2 -2
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,53 @@ 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))); }
|
|
161
|
+
// What every value a step writes keeps true of its variables (a flag is 0 or 1, a choice or a case in range, a
|
|
162
|
+
// presence bit 0 or 1), unlike a bound such as upTo's, which a step may pass: the laws a rule's state and cells of the
|
|
163
|
+
// kind are proven to keep (Rule.kindLaws).
|
|
164
|
+
invariant(_value) { return []; }
|
|
114
165
|
// A runtime value of the kind (what encode takes), for comparing a rule with the code it describes (compare).
|
|
115
166
|
sample(_rand, _bound) { throw new Error('this kind draws no runtime values'); }
|
|
116
167
|
// 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 +286,7 @@ export class Decimal {
|
|
|
235
286
|
}
|
|
236
287
|
const asNat = (name, raw) => {
|
|
237
288
|
if (typeof raw !== 'number' || !Number.isSafeInteger(raw) || raw < 0)
|
|
238
|
-
throw new RangeError(`${name} must be a natural, not ${
|
|
289
|
+
throw new RangeError(`${name} must be a natural, not ${shown(raw)}`);
|
|
239
290
|
return raw;
|
|
240
291
|
};
|
|
241
292
|
class NatKind extends Kind {
|
|
@@ -246,7 +297,7 @@ class NatKind extends Kind {
|
|
|
246
297
|
}
|
|
247
298
|
vars(name) { return [name]; }
|
|
248
299
|
assume(name) { return this.maxValue === undefined ? [] : [v(name).atMost(this.maxValue)]; }
|
|
249
|
-
|
|
300
|
+
build(name, get) { return get(name); }
|
|
250
301
|
store(name, value) { return { [name]: value }; }
|
|
251
302
|
decode(name, vars) { return vars[name]; }
|
|
252
303
|
empty() { return 0; }
|
|
@@ -273,7 +324,8 @@ export class Bool extends Cond {
|
|
|
273
324
|
class BoolKind extends Kind {
|
|
274
325
|
vars(name) { return [name]; }
|
|
275
326
|
assume(name) { return [v(name).atMost(1)]; }
|
|
276
|
-
|
|
327
|
+
build(name, get) { return new Bool(get(name)); }
|
|
328
|
+
invariant(b) { return [b.n.atMost(1)]; }
|
|
277
329
|
store(name, value) { return { [name]: typeof value === 'boolean' ? (value ? 1 : 0) : count(value) }; }
|
|
278
330
|
decode(name, vars) { return vars[name] === 1; }
|
|
279
331
|
empty() { return false; }
|
|
@@ -282,7 +334,7 @@ class BoolKind extends Kind {
|
|
|
282
334
|
return { [name]: raw ? 1 : 0 };
|
|
283
335
|
if (raw === 0 || raw === 1)
|
|
284
336
|
return { [name]: raw };
|
|
285
|
-
throw new RangeError(`${name} must be a boolean, not ${
|
|
337
|
+
throw new RangeError(`${name} must be a boolean, not ${shown(raw)}`);
|
|
286
338
|
}
|
|
287
339
|
random(name, rand) { return { [name]: rand(2) }; }
|
|
288
340
|
sample(rand) { return rand(2) === 1; }
|
|
@@ -322,14 +374,15 @@ class OneOfKind extends Kind {
|
|
|
322
374
|
}
|
|
323
375
|
vars(name) { return [name]; }
|
|
324
376
|
assume(name) { return [v(name).lt(this.values.length)]; }
|
|
325
|
-
|
|
377
|
+
build(name, get) { return new Choice(get(name), this.values); }
|
|
378
|
+
invariant(c) { return [c.n.lt(this.values.length)]; }
|
|
326
379
|
store(name, value) { return { [name]: value instanceof Choice ? value.n : this.encode(name, value)[name] }; }
|
|
327
380
|
decode(name, vars) { return this.values[vars[name]]; }
|
|
328
381
|
empty() { return this.values[0]; }
|
|
329
382
|
encode(name, raw) {
|
|
330
383
|
const i = this.values.indexOf(raw);
|
|
331
384
|
if (i < 0)
|
|
332
|
-
throw new RangeError(`${name} is one of ${this.values.join(', ')}, not ${
|
|
385
|
+
throw new RangeError(`${name} is one of ${this.values.join(', ')}, not ${shown(raw)}`);
|
|
333
386
|
return { [name]: i };
|
|
334
387
|
}
|
|
335
388
|
random(name, rand) { return { [name]: rand(this.values.length) }; }
|
|
@@ -366,7 +419,8 @@ class MaybeKind extends Kind {
|
|
|
366
419
|
has(name) { return `${name}$some`; }
|
|
367
420
|
vars(name) { return [this.has(name), ...this.of.vars(name)]; }
|
|
368
421
|
assume(name) { return [v(this.has(name)).atMost(1), ...this.of.assume(name)]; }
|
|
369
|
-
|
|
422
|
+
build(name, get) { return new Maybe(get(this.has(name)), this.of.build(name, get)); }
|
|
423
|
+
invariant(m) { return [m.present.atMost(1), ...this.of.invariant(m.inner)]; }
|
|
370
424
|
// null for none, a Maybe as it is, any other value as some.
|
|
371
425
|
store(name, value) {
|
|
372
426
|
if (value === null || value === undefined)
|
|
@@ -388,6 +442,168 @@ class MaybeKind extends Kind {
|
|
|
388
442
|
sample(rand, bound) { return rand(4) === 0 ? null : this.of.sample(rand, bound); }
|
|
389
443
|
}
|
|
390
444
|
export const maybe = (of) => new MaybeKind(of);
|
|
445
|
+
// A value present where c holds and missing elsewhere (a function's result that may be missing, say).
|
|
446
|
+
export const optional = (c, value) => new Maybe(count(c), value);
|
|
447
|
+
const fieldName = (name, f) => `${name}.${f}`;
|
|
448
|
+
const plainFields = (what, names) => { for (const f of names)
|
|
449
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(f))
|
|
450
|
+
throw new Error(`${what}: ${f} is not a plain field name`); };
|
|
451
|
+
const rawObject = (name, raw, what) => {
|
|
452
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw))
|
|
453
|
+
throw new RangeError(`${name} must be ${what}, not ${String(raw)}`);
|
|
454
|
+
return raw;
|
|
455
|
+
};
|
|
456
|
+
// Runtime values alike: objects field by field.
|
|
457
|
+
const sameRaw = (a, b) => {
|
|
458
|
+
if (a === b)
|
|
459
|
+
return true;
|
|
460
|
+
if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null)
|
|
461
|
+
return false;
|
|
462
|
+
const ka = Object.keys(a), kb = Object.keys(b);
|
|
463
|
+
return ka.length === kb.length && ka.every(k => sameRaw(a[k], b[k]));
|
|
464
|
+
};
|
|
465
|
+
const shown = (x) => (typeof x === 'object' && x !== null ? JSON.stringify(x) : String(x));
|
|
466
|
+
// r.<field> is a history record's field to the runtimes.
|
|
467
|
+
const composite = (name) => { if (name === 'r')
|
|
468
|
+
throw new Error('r names a history record; give the record or variant another name'); };
|
|
469
|
+
const merged = (parts) => Object.assign({}, ...parts);
|
|
470
|
+
class RecordKind extends Kind {
|
|
471
|
+
fields;
|
|
472
|
+
names;
|
|
473
|
+
constructor(fields) {
|
|
474
|
+
super();
|
|
475
|
+
this.fields = fields;
|
|
476
|
+
this.names = Object.keys(fields);
|
|
477
|
+
if (this.names.length === 0)
|
|
478
|
+
throw new Error('a record has a field');
|
|
479
|
+
plainFields('record', this.names);
|
|
480
|
+
}
|
|
481
|
+
each(f) { return this.names.map(n => f(n, this.fields[n])); }
|
|
482
|
+
vars(name) { composite(name); return this.each((f, k) => k.vars(fieldName(name, f))).flat(); }
|
|
483
|
+
assume(name) { return this.each((f, k) => k.assume(fieldName(name, f))).flat(); }
|
|
484
|
+
build(name, get) { return Object.fromEntries(this.each((f, k) => [f, k.build(fieldName(name, f), get)])); }
|
|
485
|
+
invariant(r) { return this.each((f, k) => k.invariant(r[f])).flat(); }
|
|
486
|
+
store(name, value) {
|
|
487
|
+
const o = value;
|
|
488
|
+
if (o === null || typeof o !== 'object')
|
|
489
|
+
throw new Error(`${name}: a record is written as an object of its fields`);
|
|
490
|
+
for (const f of Object.keys(o))
|
|
491
|
+
if (!(f in this.fields))
|
|
492
|
+
throw new Error(`${name}: the record has no field ${f}`);
|
|
493
|
+
return Object.assign({}, ...this.each((f, k) => { if (!(f in o))
|
|
494
|
+
throw new Error(`${name}: the write leaves out the field ${f}`); return k.store(fieldName(name, f), o[f]); }));
|
|
495
|
+
}
|
|
496
|
+
decode(name, vars) { return Object.fromEntries(this.each((f, k) => [f, k.decode(fieldName(name, f), vars)])); }
|
|
497
|
+
empty() { return Object.fromEntries(this.each((f, k) => [f, k.empty()])); }
|
|
498
|
+
encode(name, raw) {
|
|
499
|
+
const o = rawObject(name, raw, `an object with the fields ${this.names.join(', ')}`);
|
|
500
|
+
for (const f of Object.keys(o))
|
|
501
|
+
if (!(f in this.fields))
|
|
502
|
+
throw new RangeError(`${name} has no field ${f}`);
|
|
503
|
+
return merged(this.each((f, k) => k.encode(fieldName(name, f), o[f])));
|
|
504
|
+
}
|
|
505
|
+
random(name, rand, bound) { return merged(this.each((f, k) => k.random(fieldName(name, f), rand, bound))); }
|
|
506
|
+
sample(rand, bound) { return Object.fromEntries(this.each((f, k) => [f, k.sample(rand, bound)])); }
|
|
507
|
+
}
|
|
508
|
+
export const record = (fields) => new RecordKind(fields);
|
|
509
|
+
export class Variant {
|
|
510
|
+
tag;
|
|
511
|
+
cases;
|
|
512
|
+
kind;
|
|
513
|
+
constructor(tag, cases, kind) {
|
|
514
|
+
this.tag = tag;
|
|
515
|
+
this.cases = cases;
|
|
516
|
+
this.kind = kind;
|
|
517
|
+
}
|
|
518
|
+
is(k) { return this.tag.eq(this.kind.index(k)); }
|
|
519
|
+
match(cases) {
|
|
520
|
+
const results = this.kind.names.map(k => [k, cases[k](this.cases[k])]);
|
|
521
|
+
if (results.every(([, r]) => r instanceof Cond))
|
|
522
|
+
return any(...results.map(([k, r]) => all(this.is(k), r)));
|
|
523
|
+
// The last case is what's left: the kind's assumption says the index is below the number of cases.
|
|
524
|
+
return results.slice(0, -1).reduceRight((acc, [k, r]) => when(this.is(k), r, acc), results.at(-1)[1]);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
export class VariantKind extends Kind {
|
|
528
|
+
cases;
|
|
529
|
+
tagKey;
|
|
530
|
+
names;
|
|
531
|
+
constructor(cases, tagKey) {
|
|
532
|
+
super();
|
|
533
|
+
this.cases = cases;
|
|
534
|
+
this.tagKey = tagKey;
|
|
535
|
+
this.names = Object.keys(cases);
|
|
536
|
+
if (this.names.length === 0)
|
|
537
|
+
throw new Error('a variant has a case');
|
|
538
|
+
plainFields('variant', this.names);
|
|
539
|
+
for (const k of this.names) {
|
|
540
|
+
plainFields(`variant case ${k}`, Object.keys(cases[k]));
|
|
541
|
+
if (tagKey in cases[k])
|
|
542
|
+
throw new Error(`variant case ${k}: ${tagKey} names the case, not a field`);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
index(k) {
|
|
546
|
+
const i = this.names.indexOf(k);
|
|
547
|
+
if (i < 0)
|
|
548
|
+
throw new Error(`${k} is not a case of ${this.names.join(', ')}`);
|
|
549
|
+
return i;
|
|
550
|
+
}
|
|
551
|
+
payload(k) { return new RecordKind(this.cases[k]); }
|
|
552
|
+
filled(k) { return Object.keys(this.cases[k]).length > 0; }
|
|
553
|
+
at(name, k) { return fieldName(name, k); }
|
|
554
|
+
vars(name) { composite(name); return [name, ...this.names.filter(k => this.filled(k)).flatMap(k => this.payload(k).vars(this.at(name, k)))]; }
|
|
555
|
+
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)))]; }
|
|
556
|
+
build(name, get) {
|
|
557
|
+
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);
|
|
558
|
+
}
|
|
559
|
+
// The value of a case with these fields (terms, values or runtime values), to compare, match or pass to `when`:
|
|
560
|
+
// when(held, At.make({tag: 'holding', amount, since: now}), at)
|
|
561
|
+
invariant(x) { return [x.tag.lt(this.names.length), ...this.names.filter(k => this.filled(k)).flatMap(k => this.payload(k).invariant(x.cases[k]))]; }
|
|
562
|
+
make(write) {
|
|
563
|
+
const parts = this.store('$', write);
|
|
564
|
+
return this.build('$', part => { const t = parts[part]; return typeof t === 'number' ? lit(t) : t; });
|
|
565
|
+
}
|
|
566
|
+
// A Variant of this kind as it is, or {tag: case, ...fields} (fields as values or runtime values): the other cases'
|
|
567
|
+
// fields are written empty.
|
|
568
|
+
store(name, value) {
|
|
569
|
+
if (value instanceof Variant) {
|
|
570
|
+
if (value.kind.names.join() !== this.names.join())
|
|
571
|
+
throw new Error(`${name}: a variant of other cases is written`);
|
|
572
|
+
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]))) };
|
|
573
|
+
}
|
|
574
|
+
const o = value, k = o?.[this.tagKey];
|
|
575
|
+
if (typeof k !== 'string')
|
|
576
|
+
throw new Error(`${name}: a variant is written as {${this.tagKey}: case, ...fields}`);
|
|
577
|
+
const { [this.tagKey]: _, ...fields } = o;
|
|
578
|
+
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()))) };
|
|
579
|
+
}
|
|
580
|
+
decode(name, vars) {
|
|
581
|
+
const k = this.names[vars[name]];
|
|
582
|
+
return { [this.tagKey]: k, ...(this.filled(k) ? this.payload(k).decode(this.at(name, k), vars) : {}) };
|
|
583
|
+
}
|
|
584
|
+
empty() { const k = this.names[0]; return { [this.tagKey]: k, ...(this.filled(k) ? this.payload(k).empty() : {}) }; }
|
|
585
|
+
encode(name, raw) {
|
|
586
|
+
const o = rawObject(name, raw, `an object whose ${this.tagKey} is one of ${this.names.join(', ')}`), k = o[this.tagKey];
|
|
587
|
+
if (typeof k !== 'string' || !this.names.includes(k))
|
|
588
|
+
throw new RangeError(`${name}.${this.tagKey} is one of ${this.names.join(', ')}, not ${shown(k)}`);
|
|
589
|
+
const { [this.tagKey]: _, ...fields } = o;
|
|
590
|
+
if (!this.filled(k) && Object.keys(fields).length)
|
|
591
|
+
throw new RangeError(`${name}: the case ${k} has no fields`);
|
|
592
|
+
const zeros = (c) => Object.fromEntries(this.payload(c).vars(this.at(name, c)).map(n => [n, 0]));
|
|
593
|
+
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)))) };
|
|
594
|
+
}
|
|
595
|
+
random(name, rand, bound) {
|
|
596
|
+
const i = rand(this.names.length);
|
|
597
|
+
return { [name]: i, ...merged(this.names.filter(c => this.filled(c)).map(c => this.payload(c).random(this.at(name, c), rand, bound))) };
|
|
598
|
+
}
|
|
599
|
+
sample(rand, bound) { const k = this.names[rand(this.names.length)]; return { [this.tagKey]: k, ...(this.filled(k) ? this.payload(k).sample(rand, bound) : {}) }; }
|
|
600
|
+
}
|
|
601
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
602
|
+
export function variant(cases, opts = {}) {
|
|
603
|
+
if (opts.tag !== undefined && !/^[A-Za-z_][A-Za-z0-9_]*$/.test(opts.tag))
|
|
604
|
+
throw new Error(`variant: ${opts.tag} is not a plain key`);
|
|
605
|
+
return new VariantKind(cases, opts.tag ?? 'tag');
|
|
606
|
+
}
|
|
391
607
|
// A string, encoded as its interned name, its length and one bit per pattern the kind declares (`like`, Cedar's glob:
|
|
392
608
|
// `*` any run of characters, `\*` a star): all computed from the string itself when the input is encoded, so the bits
|
|
393
609
|
// are the string's, not an answer someone supplies. Names come from one table per process, the empty string 0; a law
|
|
@@ -480,7 +696,7 @@ class StrKind extends Kind {
|
|
|
480
696
|
return [v(name).eq(0).iff(v(`${name}$len`).eq(0)), ...(this.maxLength === undefined ? [] : [v(`${name}$len`).atMost(this.maxLength)]),
|
|
481
697
|
...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
698
|
}
|
|
483
|
-
|
|
699
|
+
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
700
|
store(name, value) {
|
|
485
701
|
if (typeof value === 'string')
|
|
486
702
|
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 +709,7 @@ class StrKind extends Kind {
|
|
|
493
709
|
empty() { return ''; }
|
|
494
710
|
encode(name, raw) {
|
|
495
711
|
if (typeof raw !== 'string')
|
|
496
|
-
throw new RangeError(`${name} must be a string, not ${
|
|
712
|
+
throw new RangeError(`${name} must be a string, not ${shown(raw)}`);
|
|
497
713
|
if (this.maxLength !== undefined && raw.length > this.maxLength)
|
|
498
714
|
throw new RangeError(`${name} is at most ${this.maxLength} characters`);
|
|
499
715
|
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 +729,13 @@ export const str = (opts = {}) => new StrKind(opts.like ?? [], opts.maxLength);
|
|
|
513
729
|
class IntKind extends Kind {
|
|
514
730
|
vars(name) { return [name, `${name}$neg`]; }
|
|
515
731
|
assume() { return []; }
|
|
516
|
-
|
|
732
|
+
build(name, get) { return new Int(get(name), get(`${name}$neg`)); }
|
|
517
733
|
store(name, value) { const i = normal(Int.of(value)); return { [name]: i.pos, [`${name}$neg`]: i.neg }; }
|
|
518
734
|
decode(name, vars) { return vars[name] - vars[`${name}$neg`]; }
|
|
519
735
|
empty() { return 0; }
|
|
520
736
|
encode(name, raw) {
|
|
521
737
|
if (typeof raw !== 'number' || !Number.isSafeInteger(raw))
|
|
522
|
-
throw new RangeError(`${name} must be an integer, not ${
|
|
738
|
+
throw new RangeError(`${name} must be an integer, not ${shown(raw)}`);
|
|
523
739
|
return { [name]: Math.max(raw, 0), [`${name}$neg`]: Math.max(-raw, 0) };
|
|
524
740
|
}
|
|
525
741
|
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 +751,7 @@ class DecimalKind extends Kind {
|
|
|
535
751
|
}
|
|
536
752
|
vars(name) { return [name, `${name}$neg`]; }
|
|
537
753
|
assume() { return []; }
|
|
538
|
-
|
|
754
|
+
build(name, get) { return new Decimal(new Int(get(name), get(`${name}$neg`)), this.places); }
|
|
539
755
|
store(name, value) {
|
|
540
756
|
const i = normal(typeof value === 'string' ? Int.of(scaledOf(value, this.places)) : new Decimal(Int.of(0), this.places).lift(value));
|
|
541
757
|
return { [name]: i.pos, [`${name}$neg`]: i.neg };
|
|
@@ -544,7 +760,7 @@ class DecimalKind extends Kind {
|
|
|
544
760
|
empty() { return 0; }
|
|
545
761
|
encode(name, raw) {
|
|
546
762
|
if (typeof raw !== 'number' && typeof raw !== 'string')
|
|
547
|
-
throw new RangeError(`${name} must be a decimal, not ${
|
|
763
|
+
throw new RangeError(`${name} must be a decimal, not ${shown(raw)}`);
|
|
548
764
|
const x = scaledOf(raw, this.places, `a decimal (${name})`);
|
|
549
765
|
return { [name]: Math.max(x, 0), [`${name}$neg`]: Math.max(-x, 0) };
|
|
550
766
|
}
|
|
@@ -624,6 +840,64 @@ export class ListRef {
|
|
|
624
840
|
increasing() { return forall((i, j) => i.lt(j).and(j.lt(this.len)).implies(this.at(i).lt(this.at(j)))); }
|
|
625
841
|
}
|
|
626
842
|
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));
|
|
843
|
+
const isPartsWrite = (u) => typeof u === 'object' && u !== null && 'parts' in u;
|
|
844
|
+
// The parts of a value written at a cell, by the names of the maps they go to.
|
|
845
|
+
const partsOf = (kind, name, value) => kind.store(name, value);
|
|
846
|
+
// A map whose cells hold values of a kind (a record, a variant, a choice...): one map per variable of the kind
|
|
847
|
+
// (<name>.<field>, and the case's index as <name> for a variant), every cell starting at the kind's value given by
|
|
848
|
+
// `from`. `at(k)` is the value at k, read as the kind's value is; `set` and `each` write whole values.
|
|
849
|
+
// .maps({Acct: record({limit: nat, blocked: bool}).from({limit: 0, blocked: false})})
|
|
850
|
+
// .step(({who, Acct}) => ({Acct: Acct.set(who, {limit: 10, blocked: false})}))
|
|
851
|
+
export class CellsRef {
|
|
852
|
+
name;
|
|
853
|
+
kind;
|
|
854
|
+
map;
|
|
855
|
+
constructor(name, kind, map) {
|
|
856
|
+
this.name = name;
|
|
857
|
+
this.kind = kind;
|
|
858
|
+
this.map = map;
|
|
859
|
+
}
|
|
860
|
+
at(key) { return this.kind.build(this.name, part => this.map(part).at(key)); }
|
|
861
|
+
set(key, value) {
|
|
862
|
+
const parts = partsOf(this.kind, this.name, value);
|
|
863
|
+
return { parts: Object.fromEntries(this.kind.vars(this.name).map(part => [part, this.map(part).set(key, parts[part])])) };
|
|
864
|
+
}
|
|
865
|
+
// Every key at once (the key read only as a map's key).
|
|
866
|
+
each(f) {
|
|
867
|
+
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])])) };
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
export class ItemsRef {
|
|
871
|
+
name;
|
|
872
|
+
kind;
|
|
873
|
+
len;
|
|
874
|
+
list;
|
|
875
|
+
constructor(name, kind, len, list) {
|
|
876
|
+
this.name = name;
|
|
877
|
+
this.kind = kind;
|
|
878
|
+
this.len = len;
|
|
879
|
+
this.list = list;
|
|
880
|
+
}
|
|
881
|
+
get names() { return this.kind.vars(this.name); }
|
|
882
|
+
at(i) { return this.kind.build(this.name, part => this.list(part).at(i)); }
|
|
883
|
+
// x at some position.
|
|
884
|
+
has(x) { return everyKey(k => k.lt(this.len).implies(same(this.at(k), x).not())).not(); }
|
|
885
|
+
op(when, len, f) { return { when, len, parts: Object.fromEntries(this.names.map(part => [part, f(this.list(part), part)])) }; }
|
|
886
|
+
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)); }
|
|
887
|
+
pop(when = yes) { return this.op(when, this.len.minus(1), l => l.pop(when)); }
|
|
888
|
+
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)); }
|
|
889
|
+
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)); }
|
|
890
|
+
removeAt(i, when = yes) { return this.op(when, this.len.minus(1), l => l.removeAt(i, when)); }
|
|
891
|
+
apply(...ops) {
|
|
892
|
+
const parts = {};
|
|
893
|
+
for (const part of this.names)
|
|
894
|
+
parts[part] = this.list(part).apply(...ops.map(op => op.parts[part]))[part];
|
|
895
|
+
return { [this.name]: { parts }, [`${this.name}_len`]: ops.reduceRight((acc, op) => when_(op.when, op.len, acc), this.len) };
|
|
896
|
+
}
|
|
897
|
+
// Laws: nothing past the end; the items in strictly increasing order of `by`.
|
|
898
|
+
within() { return forall(k => this.len.atMost(k).implies(all(...this.names.map(part => this.list(part).at(k).eq(0))))); }
|
|
899
|
+
increasing(by) { return forall((i, j) => i.lt(j).and(j.lt(this.len)).implies(by(this.at(i)).lt(by(this.at(j))))); }
|
|
900
|
+
}
|
|
627
901
|
// A law about every key of a map: proven at a fresh key, assumed at the keys a step reads.
|
|
628
902
|
// With two parameters or more, about every two keys or more: forall((i, j) => i.lt(j).implies(...)).
|
|
629
903
|
export function forall(f) { return { forall: f }; }
|
|
@@ -827,7 +1101,142 @@ export const modBy = (c) => {
|
|
|
827
1101
|
[`r(x + ${c}) = r(x)`]: forall(x => f(x.plus(c)).eq(f(x))),
|
|
828
1102
|
}) });
|
|
829
1103
|
};
|
|
830
|
-
const
|
|
1104
|
+
const reservedInRecur = ['x', 'fx', 'y', 'fy', 'prev', 'x0', 'fx0'];
|
|
1105
|
+
export const recur = (name, def, lemmas = {}, between = {}) => {
|
|
1106
|
+
const plain = /^[A-Za-z][A-Za-z0-9_]*$/;
|
|
1107
|
+
if (!plain.test(name))
|
|
1108
|
+
throw new Error(`recur: ${name} is not a plain name (it names the Lean definition)`);
|
|
1109
|
+
const args = [...(def.args ?? [])], uses = Object.entries(def.fns ?? {});
|
|
1110
|
+
for (const n of [...args, ...uses.map(([u]) => u)])
|
|
1111
|
+
if (!plain.test(n) || reservedInRecur.includes(n) || n === name)
|
|
1112
|
+
throw new Error(`recur ${name}: ${n} can't name an argument or a function here (${reservedInRecur.join(', ')} and ${name} are taken)`);
|
|
1113
|
+
distinctNames(`recur ${name}`, [...args, ...uses.map(([u]) => u)]);
|
|
1114
|
+
if (args.length > 4)
|
|
1115
|
+
throw new Error(`recur ${name}: at most four further arguments`);
|
|
1116
|
+
const named = (n) => new Term({ op: 'var', name: n });
|
|
1117
|
+
const argsAt = (ts) => Object.fromEntries(args.map((a, i) => [a, ts[i]]));
|
|
1118
|
+
const usesAt = (at) => Object.fromEntries(uses.map(([u, d]) => [u, fnTerm(at(u), d)]));
|
|
1119
|
+
const envAt = (ts, at = u => u) => ({ ...argsAt(ts), ...usesAt(at) });
|
|
1120
|
+
const zeroAt = (ts, at = u => u) => (typeof def.zero === 'function' ? def.zero(envAt(ts, at)) : def.zero);
|
|
1121
|
+
// The recurrence over named variables (x, prev, the arguments; lemmas over x and fx; laws between over x, fx, y, fy),
|
|
1122
|
+
// the functions it reads by their own names: what the runtimes and the Lean export are made from.
|
|
1123
|
+
const own = args.map(named);
|
|
1124
|
+
const zero = termOf(zeroAt(own)), next = termOf(def.next(named('x'), named('prev'), envAt(own)));
|
|
1125
|
+
const only = (what, t, names, maps) => {
|
|
1126
|
+
if (JSON.stringify(t).includes('"op":"all"') || JSON.stringify(t).includes('"op":"flag"'))
|
|
1127
|
+
throw new Error(`recur ${name}: ${what} takes no quantifier`);
|
|
1128
|
+
for (const n of namesIn(t))
|
|
1129
|
+
if (!names.includes(n))
|
|
1130
|
+
throw new Error(`recur ${name}: ${what} reads ${n}`);
|
|
1131
|
+
for (const m of [...JSON.stringify(t).matchAll(/"op":"get","map":"([^"]+)"/g)].map(x => x[1]))
|
|
1132
|
+
if (!maps.includes(m))
|
|
1133
|
+
throw new Error(`recur ${name}: ${what} reads a function or a map it isn't given (${m})`);
|
|
1134
|
+
};
|
|
1135
|
+
only('zero', zero, args, uses.map(([u]) => u));
|
|
1136
|
+
only('next', next, ['x', 'prev', ...args], uses.map(([u]) => u));
|
|
1137
|
+
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 }; });
|
|
1138
|
+
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 }; });
|
|
1139
|
+
const argKinds = Object.fromEntries(args.map(a => [a, nat]));
|
|
1140
|
+
const failed = (what, v) => { if (!(v.proven && v.checked))
|
|
1141
|
+
throw new Unproven(`${name}'s ${what}`, v); };
|
|
1142
|
+
// The lemmas: x counting up from 0 and fx its value, each lemma a law of that machine.
|
|
1143
|
+
let verdict = { proven: true, checked: true };
|
|
1144
|
+
const proofs = {};
|
|
1145
|
+
if (stated.length) {
|
|
1146
|
+
// A start that reads a function is a param given as that start by a fact (a start reads the params alone).
|
|
1147
|
+
const zeroReads = JSON.stringify(zero).includes('"op":"get"');
|
|
1148
|
+
let r0 = rule(`${name}, by induction`).params({ ...argKinds, ...(zeroReads ? { fx0: nat } : {}) }).fns(def.fns ?? {}).inputs({});
|
|
1149
|
+
if (zeroReads)
|
|
1150
|
+
r0 = r0.facts((p) => ({ [`fx0 is ${name}(0)`]: p.fx0.eq(zeroAt(args.map(a => p[a]))) }));
|
|
1151
|
+
const r = r0.state({ x: 0, fx: ((p) => (zeroReads ? p.fx0 : zeroAt(args.map(a => p[a])))) })
|
|
1152
|
+
.step((env) => ({ x: env.x.plus(1), fx: def.next(env.x, env.fx, envAt(args.map(a => env[a]))) }));
|
|
1153
|
+
const laws = Object.entries(lemmas).map(([n, f]) => r.law(n, (env) => f(env.x, env.fx, envAt(args.map(a => env[a])))));
|
|
1154
|
+
verdict = proveRule(r, laws);
|
|
1155
|
+
failed('lemmas', verdict);
|
|
1156
|
+
proofs.lemmas = { machine: r.machine, laws };
|
|
1157
|
+
}
|
|
1158
|
+
const js = recurJs(name, args, zero, next, uses);
|
|
1159
|
+
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 } : {}) }])), proofs };
|
|
1160
|
+
// A law about every n keys (forall counts them by the callback's own parameters).
|
|
1161
|
+
const keysOf = (n) => (f) => {
|
|
1162
|
+
if (n === 0)
|
|
1163
|
+
return f();
|
|
1164
|
+
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),
|
|
1165
|
+
(a, b, c, d, e) => f(a, b, c, d, e), (a, b, c, d, e, g) => f(a, b, c, d, e, g)][n - 1];
|
|
1166
|
+
return forall(by);
|
|
1167
|
+
};
|
|
1168
|
+
const facts = (withBetween) => (f, at = {}) => {
|
|
1169
|
+
const env = (ts) => ({ ...argsAt(ts), ...Object.fromEntries(uses.map(([u, d]) => [u, at[u] ?? fnTerm(u, d)])) });
|
|
1170
|
+
return {
|
|
1171
|
+
[`${name}(0)`]: keysOf(args.length)((...as) => f(0, ...as).eq(typeof def.zero === 'function' ? def.zero(env(as)) : def.zero)),
|
|
1172
|
+
[`${name}(x + 1)`]: keysOf(1 + args.length)((k, ...as) => f(k.plus(1), ...as).eq(def.next(k, f(k, ...as), env(as)))),
|
|
1173
|
+
...Object.fromEntries(Object.entries(lemmas).map(([n, l]) => [n, keysOf(1 + args.length)((k, ...as) => l(k, f(k, ...as), env(as)))])),
|
|
1174
|
+
...(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))))])) : {}),
|
|
1175
|
+
};
|
|
1176
|
+
};
|
|
1177
|
+
const base = { arity: 1 + args.length, js, lean: `Recur.${name}`, recur: recurrence, ...(uses.length ? { uses: def.fns } : {}) };
|
|
1178
|
+
// The laws between two points: y counting up from x and fy from fx, a value at x that meets the lemmas (as f(x)
|
|
1179
|
+
// does: they are given of it, as facts of the params), each law a law of that machine beside the lemmas at y. What
|
|
1180
|
+
// holds there for every such fx holds for f(x), and fy is f(y) by the recurrence; the machine reads no f at all.
|
|
1181
|
+
if (pairs.length) {
|
|
1182
|
+
const at = (env) => args.map(a => env[a]);
|
|
1183
|
+
let r0 = rule(`${name}, between`).params({ ...argKinds, x: nat, fx: nat }).fns(def.fns ?? {}).inputs({});
|
|
1184
|
+
if (stated.length)
|
|
1185
|
+
r0 = r0.facts((p) => Object.fromEntries(Object.entries(lemmas).map(([n, l]) => [`${n} at x`, l(p.x, p.fx, envAt(at(p)))])));
|
|
1186
|
+
const r = r0.state({ y: ((p) => p.x), fy: ((p) => p.fx) })
|
|
1187
|
+
.step((env) => ({ y: env.y.plus(1), fy: def.next(env.y, env.fy, envAt(at(env))) }));
|
|
1188
|
+
const laws = [...Object.entries(lemmas).map(([n, l]) => r.law(`${n} at y`, (env) => l(env.y, env.fy, envAt(at(env))))),
|
|
1189
|
+
...Object.entries(between).map(([n, l]) => r.law(n, (env) => l(env.x, env.fx, env.y, env.fy, envAt(at(env)))))];
|
|
1190
|
+
const v = proveRule(r, laws);
|
|
1191
|
+
failed('laws between two points', v);
|
|
1192
|
+
proofs.between = { machine: r.machine, laws };
|
|
1193
|
+
verdict = { proven: true, checked: verdict.checked && v.checked };
|
|
1194
|
+
}
|
|
1195
|
+
return { ...base, verdict, facts: facts(true) };
|
|
1196
|
+
};
|
|
1197
|
+
const distinctNames = (what, names) => { if (new Set(names).size !== names.length)
|
|
1198
|
+
throw new Error(`${what}: a name given twice`); };
|
|
1199
|
+
// A recurrence as JavaScript: a function of its arguments, the functions it reads inlined (each as a function of its key,
|
|
1200
|
+
// as the runtimes call it).
|
|
1201
|
+
const recurJs = (name, args, zero, next, uses) => {
|
|
1202
|
+
const fns = new Map(uses.map(([u], i) => [u, `$f${i}`]));
|
|
1203
|
+
const js = (e) => {
|
|
1204
|
+
switch (e.op) {
|
|
1205
|
+
case 'var': return e.name;
|
|
1206
|
+
case 'const': return String(e.value);
|
|
1207
|
+
case 'add': return `(${js(e.a)} + ${js(e.b)})`;
|
|
1208
|
+
case 'sub': return `Math.max(0, ${js(e.a)} - ${js(e.b)})`;
|
|
1209
|
+
case 'mul': return `(${e.k} * ${js(e.a)})`;
|
|
1210
|
+
case 'prod': return `(${js(e.a)} * ${js(e.b)})`;
|
|
1211
|
+
case 'min': return `Math.min(${js(e.a)}, ${js(e.b)})`;
|
|
1212
|
+
case 'max': return `Math.max(${js(e.a)}, ${js(e.b)})`;
|
|
1213
|
+
case 'if': return `(${jc(e.c)} ? ${js(e.then)} : ${js(e.else)})`;
|
|
1214
|
+
case 'get': return `${fns.get(e.map)}(${js(e.key)})`;
|
|
1215
|
+
}
|
|
1216
|
+
};
|
|
1217
|
+
const jc = (c) => {
|
|
1218
|
+
switch (c.op) {
|
|
1219
|
+
case '<': return `(${js(c.a)} < ${js(c.b)})`;
|
|
1220
|
+
case '<=': return `(${js(c.a)} <= ${js(c.b)})`;
|
|
1221
|
+
case '=': return `(${js(c.a)} === ${js(c.b)})`;
|
|
1222
|
+
case 'and': return c.xs.length ? `(${c.xs.map(jc).join(' && ')})` : 'true';
|
|
1223
|
+
case 'or': return c.xs.length ? `(${c.xs.map(jc).join(' || ')})` : 'false';
|
|
1224
|
+
case 'not': return `(!${jc(c.x)})`;
|
|
1225
|
+
case 'flag':
|
|
1226
|
+
case 'all': throw new Error('a recurrence takes no flag or quantifier');
|
|
1227
|
+
}
|
|
1228
|
+
};
|
|
1229
|
+
const inlined = uses.map(([u, d], i) => `const $f${i} = (${fnSource(u, d)}); `).join('');
|
|
1230
|
+
// The last value computed at each tuple of further arguments is kept, so reading f at x after f at a smaller x takes
|
|
1231
|
+
// only the steps between (a rule that reads f at a count that grows costs one step a request).
|
|
1232
|
+
return `(() => { ${inlined}const $memo = new Map(); return (x, ${args.map(a => `${a}, `).join('')}) => { const $k = ${args.length ? `[${args.join(', ')}].join()` : "''"}; let $at = $memo.get($k); if (!$at || $at[0] > x) $at = [0, ${js(zero)}]; let x0 = $at[0], prev = $at[1]; for (; x0 < x; x0++) { prev = ((x, prev) => ${js(next)})(x0, prev); if (!Number.isSafeInteger(prev)) throw new RangeError(${JSON.stringify(`${name} passes the safe integers`)}); } if ($memo.size > 4096) $memo.clear(); $memo.set($k, [x, prev]); return prev; }; })()`;
|
|
1233
|
+
};
|
|
1234
|
+
// Functions with the functions they read, those first (named <function>.<use>), each with the terms its facts read them by.
|
|
1235
|
+
const flatFns = (defs) => defs.flatMap(([n, d]) => {
|
|
1236
|
+
const uses = Object.entries(d.uses ?? {}).map(([u, ud]) => [`${n}.${u}`, ud]);
|
|
1237
|
+
return [...flatFns(uses), [n, d, Object.fromEntries(uses.map(([un, ud]) => [un.slice(n.length + 1), fnTerm(un, ud)]))]];
|
|
1238
|
+
});
|
|
1239
|
+
const pairE = (a, b) => { const s = { op: 'add', a, b }; return { op: 'add', a: { op: 'prod', a: s, b: s }, b }; };
|
|
831
1240
|
const argsKey = (args) => args.slice(1).reduce(pairE, args[0]);
|
|
832
1241
|
// A function's JavaScript as one function of its key: its arguments unpaired (left-nested, as argsKey pairs them).
|
|
833
1242
|
const fnSource = (name, d) => {
|
|
@@ -842,7 +1251,7 @@ const fnSource = (name, d) => {
|
|
|
842
1251
|
}
|
|
843
1252
|
if (d.arity === 1)
|
|
844
1253
|
return src;
|
|
845
|
-
return `k => { const u = z => {
|
|
1254
|
+
return `(() => { const $fn = (${src}); 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 $fn(...a); }; })()`;
|
|
846
1255
|
};
|
|
847
1256
|
const fnTerm = (name, d) => (...args) => {
|
|
848
1257
|
if (args.length !== d.arity)
|
|
@@ -916,7 +1325,24 @@ export class Rule {
|
|
|
916
1325
|
}
|
|
917
1326
|
return this.with({ state: [...this.spec.state, ...entries], kinded: [...(this.spec.kinded ?? []), ...kinded] });
|
|
918
1327
|
}
|
|
919
|
-
|
|
1328
|
+
// A map of naturals starts every cell at a natural; a map of a kind at the kind's value: `record({...}).from({...})`.
|
|
1329
|
+
maps(init) {
|
|
1330
|
+
const maps = [], cells = [];
|
|
1331
|
+
for (const [n, x] of Object.entries(init)) {
|
|
1332
|
+
if (typeof x === 'number') {
|
|
1333
|
+
maps.push([n, x]);
|
|
1334
|
+
continue;
|
|
1335
|
+
}
|
|
1336
|
+
const { kind, init: start } = x;
|
|
1337
|
+
if (typeof start === 'function')
|
|
1338
|
+
throw new Error(`map ${n}: every cell starts at one value (not a term over the params)`);
|
|
1339
|
+
const enc = kind.encode(n, start);
|
|
1340
|
+
cells.push([n, kind]);
|
|
1341
|
+
for (const part of kind.vars(n))
|
|
1342
|
+
maps.push([part, enc[part]]);
|
|
1343
|
+
}
|
|
1344
|
+
return this.with({ maps: [...this.spec.maps, ...maps], cells: [...(this.spec.cells ?? []), ...cells] });
|
|
1345
|
+
}
|
|
920
1346
|
// Integers (Int), each starting at an integer or at one over the params; kept as two naturals, <name> and <name>$neg.
|
|
921
1347
|
ints(init) {
|
|
922
1348
|
return this.state(Object.fromEntries(Object.entries(init).map(([n, x]) => [n, int.from(x)])));
|
|
@@ -930,6 +1356,13 @@ export class Rule {
|
|
|
930
1356
|
lists(...names) {
|
|
931
1357
|
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
1358
|
}
|
|
1359
|
+
// Lists whose items are values of a kind, each empty at the start: one list per variable of the kind, of one length
|
|
1360
|
+
// <name>_len (ItemsRef).
|
|
1361
|
+
items(kinds) {
|
|
1362
|
+
const entries = Object.entries(kinds);
|
|
1363
|
+
return this.with({ maps: [...this.spec.maps, ...entries.flatMap(([n, k]) => k.vars(n).map(part => [part, 0]))],
|
|
1364
|
+
state: [...this.spec.state, ...entries.map(([n]) => [`${n}_len`, 0])], items: [...(this.spec.items ?? []), ...entries] });
|
|
1365
|
+
}
|
|
933
1366
|
// Functions defined outside the rule (fn, divBy, modBy), read like params that take arguments: f(x).
|
|
934
1367
|
fns(defs) {
|
|
935
1368
|
return this.with({ fns: [...(this.spec.fns ?? []), ...Object.entries(defs)] });
|
|
@@ -979,6 +1412,22 @@ export class Rule {
|
|
|
979
1412
|
const [k1, more] = keysFor(out), holds = out.forall(v(k1), ...more.map(v)).c;
|
|
980
1413
|
return more.length ? { name, forall: k1, keys: more, holds } : { name, forall: k1, holds: prenex(holds, `law ${name}`, k1) ?? holds };
|
|
981
1414
|
}
|
|
1415
|
+
// What the kinds keep true of the state and of the maps and lists of a kind (Kind.invariant: a flag 0 or 1, a choice or
|
|
1416
|
+
// a case in range, a presence bit 0 or 1), as laws: proveRule, proveSteps and proveLeadsTo prove them beside the laws
|
|
1417
|
+
// they are given, so a law may lean on "the hold is idle, holding or released" without stating it; give them to
|
|
1418
|
+
// toLean (or the kernel) with the rest.
|
|
1419
|
+
kindLaws() {
|
|
1420
|
+
const out = [];
|
|
1421
|
+
const of = (env, n) => env[n];
|
|
1422
|
+
for (const [n, k] of this.spec.kinded ?? [])
|
|
1423
|
+
if (k.invariant(k.value(n)).length)
|
|
1424
|
+
out.push(this.law(`${n} is a value of its kind`, env => all(...k.invariant(of(env, n)))));
|
|
1425
|
+
for (const [n, k] of [...this.spec.cells ?? [], ...this.spec.items ?? []])
|
|
1426
|
+
if (k.invariant(k.value(n)).length) {
|
|
1427
|
+
out.push(this.law(`every ${n} cell is a value of its kind`, env => forall(key => all(...k.invariant(of(env, n).at(key))))));
|
|
1428
|
+
}
|
|
1429
|
+
return out;
|
|
1430
|
+
}
|
|
982
1431
|
// A law about one step, for every request the rule allows: `then(before, after)` relates what the step reads (params,
|
|
983
1432
|
// the request, state, maps, flags, sums, queries) to the state, maps and sums after it. `when` narrows the requests.
|
|
984
1433
|
// counter.stepLaw('never goes back', ({used}, after) => used.atMost(after.used))
|
|
@@ -1073,6 +1522,7 @@ export class Rule {
|
|
|
1073
1522
|
after[n] = new MapRef(`${n}'`, init);
|
|
1074
1523
|
for (const n of this.spec.lists ?? [])
|
|
1075
1524
|
after[n] = new ListRef(`${n}'`, v(`${n}_len'`));
|
|
1525
|
+
this.composites(after, part => `${part}'`, n => v(`${n}_len'`));
|
|
1076
1526
|
for (const [n, k] of this.spec.kinded ?? []) {
|
|
1077
1527
|
for (const part of k.vars(n))
|
|
1078
1528
|
delete after[part];
|
|
@@ -1101,6 +1551,7 @@ export class Rule {
|
|
|
1101
1551
|
out[n] = new MapRef(n, init);
|
|
1102
1552
|
for (const n of this.spec.lists ?? [])
|
|
1103
1553
|
out[n] = new ListRef(n, v(`${n}_len`));
|
|
1554
|
+
this.composites(out, part => part, n => v(`${n}_len`));
|
|
1104
1555
|
for (const [n] of this.spec.sums)
|
|
1105
1556
|
out[n] = v(n);
|
|
1106
1557
|
for (const [n, k] of this.spec.kinded ?? []) {
|
|
@@ -1121,12 +1572,27 @@ export class Rule {
|
|
|
1121
1572
|
}
|
|
1122
1573
|
return out;
|
|
1123
1574
|
}
|
|
1575
|
+
// Maps and lists of a kind in place of their parts' maps (and a list's length), each part's map named by `at`.
|
|
1576
|
+
composites(out, at, len) {
|
|
1577
|
+
const init = new Map(this.spec.maps);
|
|
1578
|
+
for (const [n, k] of this.spec.cells ?? []) {
|
|
1579
|
+
for (const part of k.vars(n))
|
|
1580
|
+
delete out[part];
|
|
1581
|
+
out[n] = new CellsRef(n, k, part => new MapRef(at(part), init.get(part)));
|
|
1582
|
+
}
|
|
1583
|
+
for (const [n, k] of this.spec.items ?? []) {
|
|
1584
|
+
for (const part of k.vars(n))
|
|
1585
|
+
delete out[part];
|
|
1586
|
+
out[n] = new ItemsRef(n, k, len(n), part => new ListRef(at(part), len(n)));
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1124
1589
|
// Every name the rule declares is declared once (a flag named again replaces the earlier one, so flags are checked as
|
|
1125
1590
|
// they are defined): an input and a state variable sharing a name would hand the callbacks only one of them.
|
|
1126
1591
|
names() {
|
|
1127
1592
|
const s = this.spec;
|
|
1128
1593
|
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
1594
|
...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']),
|
|
1595
|
+
...[...(s.cells ?? []), ...(s.items ?? [])].filter(([n, k]) => !k.vars(n).includes(n)).map(([n]) => [n, 'a map of a kind']),
|
|
1130
1596
|
];
|
|
1131
1597
|
}
|
|
1132
1598
|
whatIs(n) { return this.names().find(([m]) => m === n)?.[1] ?? 'something else'; }
|
|
@@ -1173,6 +1639,7 @@ export class Rule {
|
|
|
1173
1639
|
delete updates[n];
|
|
1174
1640
|
Object.assign(updates, k.store(n, u));
|
|
1175
1641
|
}
|
|
1642
|
+
expandParts(updates);
|
|
1176
1643
|
const state = {}, maps = {};
|
|
1177
1644
|
for (const [n, u] of Object.entries(updates)) {
|
|
1178
1645
|
if (u === undefined)
|
|
@@ -1189,9 +1656,11 @@ export class Rule {
|
|
|
1189
1656
|
const assume = [...this.kindAssumptions('inputs'), ...s.assume.map(f => hoist.cond(f(env).c))];
|
|
1190
1657
|
placed();
|
|
1191
1658
|
// Functions, and their facts: each function's own (named after it), then the rule's.
|
|
1192
|
-
|
|
1659
|
+
// The functions a function reads come first, each named <function>.<use>.
|
|
1660
|
+
const flat = flatFns(s.fns ?? []);
|
|
1661
|
+
const fns = Object.fromEntries(flat.map(([n, d]) => [n, { js: fnSource(n, d), ...(d.lean ? { lean: d.lean } : {}), ...(d.recur ? { recur: d.recur } : {}) }]));
|
|
1193
1662
|
const facts = [
|
|
1194
|
-
...
|
|
1663
|
+
...flat.flatMap(([n, d, uses]) => Object.entries(d.facts?.(fnTerm(n, d), uses) ?? {}).map(([name, f]) => this.lawOf(`${n}: ${name}`, f))),
|
|
1195
1664
|
...(s.facts ?? []).flatMap(f => Object.entries(f(pEnv)).map(([name, out]) => this.lawOf(name, out))),
|
|
1196
1665
|
];
|
|
1197
1666
|
const m = {
|
|
@@ -1311,7 +1780,7 @@ export class Rule {
|
|
|
1311
1780
|
}
|
|
1312
1781
|
// The updates: every state variable as its value is written (a kinded one through its kind's `store`, as the
|
|
1313
1782
|
// machine is built) or kept, and each map write.
|
|
1314
|
-
const updates = inScope(sc, () => s.step?.(this.env()) ?? {});
|
|
1783
|
+
const updates = inScope(sc, () => expandParts(s.step?.(this.env()) ?? {}));
|
|
1315
1784
|
const written = { ...updates };
|
|
1316
1785
|
inScope(sc, () => { for (const [n, k] of s.kinded ?? [])
|
|
1317
1786
|
if (updates[n] !== undefined) {
|
|
@@ -1385,8 +1854,8 @@ export class Rule {
|
|
|
1385
1854
|
const got = this.decode(ours);
|
|
1386
1855
|
for (const [field, want] of Object.entries(theirs)) {
|
|
1387
1856
|
checked++;
|
|
1388
|
-
if (got[field]
|
|
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` };
|
|
1857
|
+
if (!sameRaw(got[field], want))
|
|
1858
|
+
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
1859
|
}
|
|
1391
1860
|
}
|
|
1392
1861
|
}
|
|
@@ -1421,6 +1890,14 @@ export class Rule {
|
|
|
1421
1890
|
delete out[part];
|
|
1422
1891
|
out[n] = k.decode(n, st.scalars);
|
|
1423
1892
|
}
|
|
1893
|
+
// 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.
|
|
1894
|
+
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)])));
|
|
1895
|
+
for (const [n, k] of this.spec.cells ?? []) {
|
|
1896
|
+
const keys = [...new Set(k.vars(n).flatMap(part => [...(st.maps[part]?.keys() ?? [])]))].sort((a, b) => a - b);
|
|
1897
|
+
out[n] = Object.fromEntries(keys.map(key => [key, cell(n, k, key)]));
|
|
1898
|
+
}
|
|
1899
|
+
for (const [n, k] of this.spec.items ?? [])
|
|
1900
|
+
out[n] = Array.from({ length: st.scalars[`${n}_len`] }, (_, i) => cell(n, k, i));
|
|
1424
1901
|
return out;
|
|
1425
1902
|
}
|
|
1426
1903
|
// The prune order's variable, if the history is pruned (a runtime must send it nondecreasing).
|
|
@@ -1430,10 +1907,20 @@ export class Rule {
|
|
|
1430
1907
|
return loadJs(this.machine);
|
|
1431
1908
|
}
|
|
1432
1909
|
}
|
|
1910
|
+
// A step's writes to maps and lists of a kind, as the writes to their parts' maps.
|
|
1911
|
+
const expandParts = (updates) => {
|
|
1912
|
+
for (const [n, u] of Object.entries(updates))
|
|
1913
|
+
if (isPartsWrite(u)) {
|
|
1914
|
+
delete updates[n];
|
|
1915
|
+
Object.assign(updates, u.parts);
|
|
1916
|
+
}
|
|
1917
|
+
return updates;
|
|
1918
|
+
};
|
|
1433
1919
|
const encode = (kinds, values) => Object.assign({}, ...kinds.map(([n, k]) => k.encode(n, values[n])));
|
|
1434
1920
|
export const rule = (name) => new Rule({ name, params: [], inputs: [], state: [], maps: [], flags: [], pre: [], assume: [], sums: [], queries: [] });
|
|
1435
1921
|
// 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 })
|
|
1922
|
+
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 })
|
|
1923
|
+
: t instanceof Variant ? t.kind : t instanceof Maybe ? maybe(kindOf(t.inner)) : isRecord(t) ? record(Object.fromEntries(Object.entries(t).map(([f, x]) => [f, kindOf(x)]))) : nat);
|
|
1437
1924
|
export class Fun {
|
|
1438
1925
|
r;
|
|
1439
1926
|
out;
|
|
@@ -1475,8 +1962,8 @@ export class Fun {
|
|
|
1475
1962
|
const ours = this.run(values, params), theirs = reference(values, params);
|
|
1476
1963
|
for (const [field, want] of Object.entries(theirs)) {
|
|
1477
1964
|
checked++;
|
|
1478
|
-
if (ours[field]
|
|
1479
|
-
return { checked, failure: `${this.name} at ${JSON.stringify(values)}: ${field} is ${
|
|
1965
|
+
if (!sameRaw(ours[field], want))
|
|
1966
|
+
return { checked, failure: `${this.name} at ${JSON.stringify(values)}: ${field} is ${shown(ours[field])} here, ${shown(want)} in the code it describes` };
|
|
1480
1967
|
}
|
|
1481
1968
|
}
|
|
1482
1969
|
return { checked };
|
|
@@ -1557,6 +2044,15 @@ export class LoopRule {
|
|
|
1557
2044
|
randomInput(rand, bound) { return Object.assign({}, ...this.spec.inputs.map(([n, k]) => k.random(n, rand, bound))); }
|
|
1558
2045
|
}
|
|
1559
2046
|
export const loop = (name) => new LoopRule({ name, params: [], inputs: [], state: [], pre: [], assume: [] });
|
|
2047
|
+
// 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`).
|
|
2048
|
+
export class Unproven extends Error {
|
|
2049
|
+
verdict;
|
|
2050
|
+
constructor(what, verdict) {
|
|
2051
|
+
const found = [verdict.goal && `goal ${verdict.goal}`, verdict.stuck, verdict.witness && `a counterexample: ${JSON.stringify(verdict.witness)}`, verdict.why].filter(Boolean).join('; ');
|
|
2052
|
+
super(`${what}: ${verdict.proven ? 'proven, but the kernel did not check the proof' : `not proven${found ? ` (${found})` : ''}`}`);
|
|
2053
|
+
this.verdict = verdict;
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
1560
2056
|
const goalsVerdict = (goals) => {
|
|
1561
2057
|
const trees = {};
|
|
1562
2058
|
for (const g of goals) {
|
|
@@ -1567,16 +2063,18 @@ const goalsVerdict = (goals) => {
|
|
|
1567
2063
|
}
|
|
1568
2064
|
return { proven: true, checked: goals.every(g => check(g, trees[g.name])), trees };
|
|
1569
2065
|
};
|
|
2066
|
+
// The kinds' laws first, those the caller didn't pass already (by name).
|
|
2067
|
+
const withKinds = (r, laws) => [...r.kindLaws().filter(k => !laws.some(l => l.name === k.name)), ...laws];
|
|
1570
2068
|
export function proveRule(r, laws) {
|
|
1571
|
-
const out = proveLaws(r.machine, laws);
|
|
2069
|
+
const out = proveLaws(r.machine, withKinds(r, laws));
|
|
1572
2070
|
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 } : {}) } : {}) };
|
|
1573
2071
|
}
|
|
1574
2072
|
export function proveSteps(r, laws, stepLaws) {
|
|
1575
|
-
const out = proveStepLaws(r.machine, laws, stepLaws);
|
|
2073
|
+
const out = proveStepLaws(r.machine, withKinds(r, laws), stepLaws);
|
|
1576
2074
|
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 } : {}) } : {}) };
|
|
1577
2075
|
}
|
|
1578
2076
|
export function proveLeadsTo(r, laws, l) {
|
|
1579
|
-
const out = proveLiveness(r.machine, laws, l);
|
|
2077
|
+
const out = proveLiveness(r.machine, withKinds(r, laws), l);
|
|
1580
2078
|
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 } : {}) } : {}) };
|
|
1581
2079
|
}
|
|
1582
2080
|
export const proveDecision = (d) => goalsVerdict([d.goal]);
|