gdp-wired 0.8.0 → 0.10.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 +30 -9
- package/dist/core/dsl.d.ts +117 -27
- package/dist/core/dsl.js +315 -36
- package/dist/core/gdp.d.ts +2 -2
- package/dist/core/gdp.js +2 -1
- package/dist/core/machine.d.ts +10 -0
- package/dist/lean-kernel/emit.d.ts +5 -0
- package/dist/lean-kernel/emit.js +134 -84
- package/dist/lean-kernel/library.js +5 -5
- package/package.json +2 -2
package/dist/core/dsl.js
CHANGED
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
//
|
|
16
16
|
// A step returns its updates as one object: every right-hand side reads the state before the step, as the kernel and
|
|
17
17
|
// both runtimes do.
|
|
18
|
-
import { carriedView, compileFunction, evalC, interpret, loadJs, numbersOf } from "./machine.js";
|
|
18
|
+
import { carriedView, compileFunction, evalC, evalE, interpret, loadJs, numbersOf } from "./machine.js";
|
|
19
19
|
import { allowed, keysOf, requestEnv, watch } from "./watch.js";
|
|
20
|
-
import { check, loopObligations } from "../kernel/kernel.js";
|
|
20
|
+
import { check, foldObligations, loopObligations } from "../kernel/kernel.js";
|
|
21
21
|
import { factsFor, grow, proveLaws, proveLiveness, proveStepLaws } from "../kernel/prove.js";
|
|
22
22
|
// --- terms and conditions -------------------------------------------------------------------------------------------
|
|
23
23
|
const naturalConst = (v) => {
|
|
@@ -158,6 +158,10 @@ const named = (at, n) => (typeof at === 'string' ? at + n : at(n));
|
|
|
158
158
|
const primed = (n) => `${n}'`;
|
|
159
159
|
export class Kind {
|
|
160
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 []; }
|
|
161
165
|
// A runtime value of the kind (what encode takes), for comparing a rule with the code it describes (compare).
|
|
162
166
|
sample(_rand, _bound) { throw new Error('this kind draws no runtime values'); }
|
|
163
167
|
// A state variable of this kind starting at a runtime value, or at a value over the params: `.state({open: bool.from(true)})`.
|
|
@@ -321,6 +325,7 @@ class BoolKind extends Kind {
|
|
|
321
325
|
vars(name) { return [name]; }
|
|
322
326
|
assume(name) { return [v(name).atMost(1)]; }
|
|
323
327
|
build(name, get) { return new Bool(get(name)); }
|
|
328
|
+
invariant(b) { return [b.n.atMost(1)]; }
|
|
324
329
|
store(name, value) { return { [name]: typeof value === 'boolean' ? (value ? 1 : 0) : count(value) }; }
|
|
325
330
|
decode(name, vars) { return vars[name] === 1; }
|
|
326
331
|
empty() { return false; }
|
|
@@ -370,6 +375,7 @@ class OneOfKind extends Kind {
|
|
|
370
375
|
vars(name) { return [name]; }
|
|
371
376
|
assume(name) { return [v(name).lt(this.values.length)]; }
|
|
372
377
|
build(name, get) { return new Choice(get(name), this.values); }
|
|
378
|
+
invariant(c) { return [c.n.lt(this.values.length)]; }
|
|
373
379
|
store(name, value) { return { [name]: value instanceof Choice ? value.n : this.encode(name, value)[name] }; }
|
|
374
380
|
decode(name, vars) { return this.values[vars[name]]; }
|
|
375
381
|
empty() { return this.values[0]; }
|
|
@@ -414,6 +420,7 @@ class MaybeKind extends Kind {
|
|
|
414
420
|
vars(name) { return [this.has(name), ...this.of.vars(name)]; }
|
|
415
421
|
assume(name) { return [v(this.has(name)).atMost(1), ...this.of.assume(name)]; }
|
|
416
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)]; }
|
|
417
424
|
// null for none, a Maybe as it is, any other value as some.
|
|
418
425
|
store(name, value) {
|
|
419
426
|
if (value === null || value === undefined)
|
|
@@ -435,6 +442,8 @@ class MaybeKind extends Kind {
|
|
|
435
442
|
sample(rand, bound) { return rand(4) === 0 ? null : this.of.sample(rand, bound); }
|
|
436
443
|
}
|
|
437
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);
|
|
438
447
|
const fieldName = (name, f) => `${name}.${f}`;
|
|
439
448
|
const plainFields = (what, names) => { for (const f of names)
|
|
440
449
|
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(f))
|
|
@@ -473,6 +482,7 @@ class RecordKind extends Kind {
|
|
|
473
482
|
vars(name) { composite(name); return this.each((f, k) => k.vars(fieldName(name, f))).flat(); }
|
|
474
483
|
assume(name) { return this.each((f, k) => k.assume(fieldName(name, f))).flat(); }
|
|
475
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(); }
|
|
476
486
|
store(name, value) {
|
|
477
487
|
const o = value;
|
|
478
488
|
if (o === null || typeof o !== 'object')
|
|
@@ -516,19 +526,20 @@ export class Variant {
|
|
|
516
526
|
}
|
|
517
527
|
export class VariantKind extends Kind {
|
|
518
528
|
cases;
|
|
529
|
+
tagKey;
|
|
519
530
|
names;
|
|
520
|
-
tagKey
|
|
521
|
-
constructor(cases) {
|
|
531
|
+
constructor(cases, tagKey) {
|
|
522
532
|
super();
|
|
523
533
|
this.cases = cases;
|
|
534
|
+
this.tagKey = tagKey;
|
|
524
535
|
this.names = Object.keys(cases);
|
|
525
536
|
if (this.names.length === 0)
|
|
526
537
|
throw new Error('a variant has a case');
|
|
527
538
|
plainFields('variant', this.names);
|
|
528
539
|
for (const k of this.names) {
|
|
529
540
|
plainFields(`variant case ${k}`, Object.keys(cases[k]));
|
|
530
|
-
if (
|
|
531
|
-
throw new Error(`variant case ${k}:
|
|
541
|
+
if (tagKey in cases[k])
|
|
542
|
+
throw new Error(`variant case ${k}: ${tagKey} names the case, not a field`);
|
|
532
543
|
}
|
|
533
544
|
}
|
|
534
545
|
index(k) {
|
|
@@ -547,6 +558,7 @@ export class VariantKind extends Kind {
|
|
|
547
558
|
}
|
|
548
559
|
// The value of a case with these fields (terms, values or runtime values), to compare, match or pass to `when`:
|
|
549
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]))]; }
|
|
550
562
|
make(write) {
|
|
551
563
|
const parts = this.store('$', write);
|
|
552
564
|
return this.build('$', part => { const t = parts[part]; return typeof t === 'number' ? lit(t) : t; });
|
|
@@ -586,7 +598,12 @@ export class VariantKind extends Kind {
|
|
|
586
598
|
}
|
|
587
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) : {}) }; }
|
|
588
600
|
}
|
|
589
|
-
|
|
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
|
+
}
|
|
590
607
|
// A string, encoded as its interned name, its length and one bit per pattern the kind declares (`like`, Cedar's glob:
|
|
591
608
|
// `*` any run of characters, `\*` a star): all computed from the string itself when the input is encoded, so the bits
|
|
592
609
|
// are the string's, not an answer someone supplies. Names come from one table per process, the empty string 0; a law
|
|
@@ -850,6 +867,89 @@ export class CellsRef {
|
|
|
850
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])])) };
|
|
851
868
|
}
|
|
852
869
|
}
|
|
870
|
+
export class CarriedList {
|
|
871
|
+
name;
|
|
872
|
+
kind;
|
|
873
|
+
len;
|
|
874
|
+
folds;
|
|
875
|
+
map;
|
|
876
|
+
lemmas;
|
|
877
|
+
fold;
|
|
878
|
+
constructor(name, kind, len, folds, map, lemmas = {}) {
|
|
879
|
+
this.name = name;
|
|
880
|
+
this.kind = kind;
|
|
881
|
+
this.len = len;
|
|
882
|
+
this.folds = folds;
|
|
883
|
+
this.map = map;
|
|
884
|
+
this.lemmas = lemmas;
|
|
885
|
+
this.fold = Object.fromEntries(Object.keys(folds).map(f => [f, map(`${name}$${f}`).at(len)]));
|
|
886
|
+
}
|
|
887
|
+
at(i) { return this.kind.build(this.name, part => this.map(part).at(i)); }
|
|
888
|
+
// The running value of a fold after the first i items.
|
|
889
|
+
after(fold, i) { return this.map(`${this.name}$${fold}`).at(i); }
|
|
890
|
+
every(f) { return everyKey(k => k.lt(this.len).implies(f(this.at(k), k))); }
|
|
891
|
+
some(f) { return this.every((x, i) => f(x, i).not()).not(); }
|
|
892
|
+
has(x) { return this.some(y => same(y, x)); }
|
|
893
|
+
// Each item's `by` below the next one's (so no two alike).
|
|
894
|
+
increasing(by) { return everyKey(k => k.plus(1).lt(this.len).implies(by(this.at(k)).lt(by(this.at(k.plus(1)))))); }
|
|
895
|
+
// What every request is assumed to meet: each fold's start, and at every key one condition (the item's own laws and
|
|
896
|
+
// each fold's step below the length, each lemma up to it), so the prover closes one quantifier, not one per part.
|
|
897
|
+
equations() {
|
|
898
|
+
const folds = Object.entries(this.folds), lemmas = Object.values(this.lemmas);
|
|
899
|
+
const at = (k) => {
|
|
900
|
+
const x = this.at(k), steps = folds.map(([f, d]) => this.map(`${this.name}$${f}`).at(k.plus(1)).eq(d.step(this.map(`${this.name}$${f}`).at(k), x)));
|
|
901
|
+
const below = [...this.kind.invariant(x), ...steps];
|
|
902
|
+
return all(...(below.length ? [k.lt(this.len).implies(all(...below))] : []), ...(lemmas.length ? [k.atMost(this.len).implies(all(...lemmas.map(l => l(this.running(k), k))))] : []));
|
|
903
|
+
};
|
|
904
|
+
const any = this.kind.invariant(this.kind.value(this.name)).length + folds.length + lemmas.length > 0;
|
|
905
|
+
return [...folds.map(([f, d]) => this.map(`${this.name}$${f}`).at(0).eq(d.from)), ...(any ? [everyKey(at)] : [])];
|
|
906
|
+
}
|
|
907
|
+
running(k) { return Object.fromEntries(Object.keys(this.folds).map(f => [f, this.after(f, k)])); }
|
|
908
|
+
// The lemmas, proven: the folds as a machine counting i up from 0 over items that are functions of i known only by
|
|
909
|
+
// the kind's own laws, each lemma a law of it (so it holds after every prefix of every list).
|
|
910
|
+
prove() {
|
|
911
|
+
if (!Object.keys(this.lemmas).length)
|
|
912
|
+
return { proven: true, checked: true };
|
|
913
|
+
const parts = this.kind.vars(this.name), fname = (p) => `item$${p.replaceAll('.', '$')}`;
|
|
914
|
+
const item = (i) => this.kind.build(this.name, p => new Term({ op: 'get', map: fname(p), key: i.e }));
|
|
915
|
+
// Every item is a value of the kind (its own laws, as a fact of the first part's function).
|
|
916
|
+
const kindFact = this.kind.invariant(this.kind.value(this.name)).length ? { 'every item is a value of its kind': forall(k => all(...this.kind.invariant(item(k)))) } : {};
|
|
917
|
+
const fns = Object.fromEntries(parts.map((p, j) => [fname(p), fn(1, 'x => 0', j === 0 ? { facts: () => kindFact } : {})]));
|
|
918
|
+
const folds = Object.entries(this.folds);
|
|
919
|
+
const r = rule(`${this.name}'s folds, by induction`).fns(fns).inputs({}).state({ i: 0, ...Object.fromEntries(folds.map(([f, d]) => [`acc$${f}`, d.from])) })
|
|
920
|
+
.step((env) => ({ i: env.i.plus(1), ...Object.fromEntries(folds.map(([f, d]) => [`acc$${f}`, d.step(env[`acc$${f}`], item(env.i))])) }));
|
|
921
|
+
const accs = (env) => Object.fromEntries(folds.map(([f]) => [f, env[`acc$${f}`]]));
|
|
922
|
+
return proveRule(r, Object.entries(this.lemmas).map(([n, l]) => r.law(n, (env) => l(accs(env), env.i))));
|
|
923
|
+
}
|
|
924
|
+
// Every carried map it is kept in: the items' parts, then each fold's.
|
|
925
|
+
get maps() { return [...this.kind.vars(this.name), ...Object.keys(this.folds).map(f => `${this.name}$${f}`)]; }
|
|
926
|
+
// A request's list as those maps and its length: each fold computed by its step, as the builders compute it.
|
|
927
|
+
encode(raw) {
|
|
928
|
+
if (!Array.isArray(raw))
|
|
929
|
+
throw new RangeError(`${this.name} must be a list, not ${shown(raw)}`);
|
|
930
|
+
const items = raw.map(x => this.kind.encode(this.name, x));
|
|
931
|
+
const maps = Object.fromEntries(this.maps.map(m => [m, new Map()]));
|
|
932
|
+
items.forEach((enc, i) => { for (const [part, x] of Object.entries(enc))
|
|
933
|
+
if (x !== 0)
|
|
934
|
+
maps[part].set(i, x); });
|
|
935
|
+
for (const [f, d] of Object.entries(this.folds)) {
|
|
936
|
+
const acc = maps[`${this.name}$${f}`];
|
|
937
|
+
let value = d.from;
|
|
938
|
+
if (value !== 0)
|
|
939
|
+
acc.set(0, value);
|
|
940
|
+
items.forEach((enc, i) => {
|
|
941
|
+
const item = this.kind.build(this.name, part => new Term({ op: 'const', value: enc[part] }, enc[part]));
|
|
942
|
+
const next = valOf(d.step(new Term({ op: 'const', value }, value), item));
|
|
943
|
+
if (next === undefined || !Number.isSafeInteger(next) || next < 0)
|
|
944
|
+
throw new RangeError(`${this.name}'s fold ${f} reads more than its items`);
|
|
945
|
+
value = next;
|
|
946
|
+
if (value !== 0)
|
|
947
|
+
acc.set(i + 1, value);
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
return { maps, len: items.length };
|
|
951
|
+
}
|
|
952
|
+
}
|
|
853
953
|
export class ItemsRef {
|
|
854
954
|
name;
|
|
855
955
|
kind;
|
|
@@ -1084,7 +1184,7 @@ export const modBy = (c) => {
|
|
|
1084
1184
|
[`r(x + ${c}) = r(x)`]: forall(x => f(x.plus(c)).eq(f(x))),
|
|
1085
1185
|
}) });
|
|
1086
1186
|
};
|
|
1087
|
-
const reservedInRecur = ['x', 'fx', 'y', 'fy', 'prev', 'x0'];
|
|
1187
|
+
const reservedInRecur = ['x', 'fx', 'y', 'fy', 'prev', 'x0', 'fx0'];
|
|
1088
1188
|
export const recur = (name, def, lemmas = {}, between = {}) => {
|
|
1089
1189
|
const plain = /^[A-Za-z][A-Za-z0-9_]*$/;
|
|
1090
1190
|
if (!plain.test(name))
|
|
@@ -1094,15 +1194,13 @@ export const recur = (name, def, lemmas = {}, between = {}) => {
|
|
|
1094
1194
|
if (!plain.test(n) || reservedInRecur.includes(n) || n === name)
|
|
1095
1195
|
throw new Error(`recur ${name}: ${n} can't name an argument or a function here (${reservedInRecur.join(', ')} and ${name} are taken)`);
|
|
1096
1196
|
distinctNames(`recur ${name}`, [...args, ...uses.map(([u]) => u)]);
|
|
1097
|
-
if (args.length >
|
|
1098
|
-
throw new Error(`recur ${name}:
|
|
1099
|
-
if (args.length > 3)
|
|
1100
|
-
throw new Error(`recur ${name}: at most three further arguments`);
|
|
1197
|
+
if (args.length > 4)
|
|
1198
|
+
throw new Error(`recur ${name}: at most four further arguments`);
|
|
1101
1199
|
const named = (n) => new Term({ op: 'var', name: n });
|
|
1102
1200
|
const argsAt = (ts) => Object.fromEntries(args.map((a, i) => [a, ts[i]]));
|
|
1103
1201
|
const usesAt = (at) => Object.fromEntries(uses.map(([u, d]) => [u, fnTerm(at(u), d)]));
|
|
1104
1202
|
const envAt = (ts, at = u => u) => ({ ...argsAt(ts), ...usesAt(at) });
|
|
1105
|
-
const zeroAt = (ts) => (typeof def.zero === 'function' ? def.zero(
|
|
1203
|
+
const zeroAt = (ts, at = u => u) => (typeof def.zero === 'function' ? def.zero(envAt(ts, at)) : def.zero);
|
|
1106
1204
|
// The recurrence over named variables (x, prev, the arguments; lemmas over x and fx; laws between over x, fx, y, fy),
|
|
1107
1205
|
// the functions it reads by their own names: what the runtimes and the Lean export are made from.
|
|
1108
1206
|
const own = args.map(named);
|
|
@@ -1117,7 +1215,7 @@ export const recur = (name, def, lemmas = {}, between = {}) => {
|
|
|
1117
1215
|
if (!maps.includes(m))
|
|
1118
1216
|
throw new Error(`recur ${name}: ${what} reads a function or a map it isn't given (${m})`);
|
|
1119
1217
|
};
|
|
1120
|
-
only('zero', zero, args, []);
|
|
1218
|
+
only('zero', zero, args, uses.map(([u]) => u));
|
|
1121
1219
|
only('next', next, ['x', 'prev', ...args], uses.map(([u]) => u));
|
|
1122
1220
|
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
1221
|
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 }; });
|
|
@@ -1126,44 +1224,55 @@ export const recur = (name, def, lemmas = {}, between = {}) => {
|
|
|
1126
1224
|
throw new Unproven(`${name}'s ${what}`, v); };
|
|
1127
1225
|
// The lemmas: x counting up from 0 and fx its value, each lemma a law of that machine.
|
|
1128
1226
|
let verdict = { proven: true, checked: true };
|
|
1227
|
+
const proofs = {};
|
|
1129
1228
|
if (stated.length) {
|
|
1130
|
-
|
|
1229
|
+
// A start that reads a function is a param given as that start by a fact (a start reads the params alone).
|
|
1230
|
+
const zeroReads = JSON.stringify(zero).includes('"op":"get"');
|
|
1231
|
+
let r0 = rule(`${name}, by induction`).params({ ...argKinds, ...(zeroReads ? { fx0: nat } : {}) }).fns(def.fns ?? {}).inputs({});
|
|
1232
|
+
if (zeroReads)
|
|
1233
|
+
r0 = r0.facts((p) => ({ [`fx0 is ${name}(0)`]: p.fx0.eq(zeroAt(args.map(a => p[a]))) }));
|
|
1234
|
+
const r = r0.state({ x: 0, fx: ((p) => (zeroReads ? p.fx0 : zeroAt(args.map(a => p[a])))) })
|
|
1131
1235
|
.step((env) => ({ x: env.x.plus(1), fx: def.next(env.x, env.fx, envAt(args.map(a => env[a]))) }));
|
|
1132
|
-
|
|
1236
|
+
const laws = Object.entries(lemmas).map(([n, f]) => r.law(n, (env) => f(env.x, env.fx, envAt(args.map(a => env[a])))));
|
|
1237
|
+
verdict = proveRule(r, laws);
|
|
1133
1238
|
failed('lemmas', verdict);
|
|
1239
|
+
proofs.lemmas = { machine: r.machine, laws };
|
|
1134
1240
|
}
|
|
1135
1241
|
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 } : {}) }])) };
|
|
1242
|
+
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 };
|
|
1137
1243
|
// A law about every n keys (forall counts them by the callback's own parameters).
|
|
1138
1244
|
const keysOf = (n) => (f) => {
|
|
1139
1245
|
if (n === 0)
|
|
1140
1246
|
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)
|
|
1247
|
+
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),
|
|
1248
|
+
(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];
|
|
1142
1249
|
return forall(by);
|
|
1143
1250
|
};
|
|
1144
1251
|
const facts = (withBetween) => (f, at = {}) => {
|
|
1145
1252
|
const env = (ts) => ({ ...argsAt(ts), ...Object.fromEntries(uses.map(([u, d]) => [u, at[u] ?? fnTerm(u, d)])) });
|
|
1146
1253
|
return {
|
|
1147
|
-
[`${name}(0)`]: keysOf(args.length)((...as) => f(0, ...as).eq(
|
|
1254
|
+
[`${name}(0)`]: keysOf(args.length)((...as) => f(0, ...as).eq(typeof def.zero === 'function' ? def.zero(env(as)) : def.zero)),
|
|
1148
1255
|
[`${name}(x + 1)`]: keysOf(1 + args.length)((k, ...as) => f(k.plus(1), ...as).eq(def.next(k, f(k, ...as), env(as)))),
|
|
1149
1256
|
...Object.fromEntries(Object.entries(lemmas).map(([n, l]) => [n, keysOf(1 + args.length)((k, ...as) => l(k, f(k, ...as), env(as)))])),
|
|
1150
1257
|
...(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
1258
|
};
|
|
1152
1259
|
};
|
|
1153
1260
|
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
|
|
1155
|
-
// of
|
|
1261
|
+
// 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)
|
|
1262
|
+
// does: they are given of it, as facts of the params), each law a law of that machine beside the lemmas at y. What
|
|
1263
|
+
// 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.
|
|
1156
1264
|
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
1265
|
const at = (env) => args.map(a => env[a]);
|
|
1163
|
-
|
|
1164
|
-
|
|
1266
|
+
let r0 = rule(`${name}, between`).params({ ...argKinds, x: nat, fx: nat }).fns(def.fns ?? {}).inputs({});
|
|
1267
|
+
if (stated.length)
|
|
1268
|
+
r0 = r0.facts((p) => Object.fromEntries(Object.entries(lemmas).map(([n, l]) => [`${n} at x`, l(p.x, p.fx, envAt(at(p)))])));
|
|
1269
|
+
const r = r0.state({ y: ((p) => p.x), fy: ((p) => p.fx) })
|
|
1270
|
+
.step((env) => ({ y: env.y.plus(1), fy: def.next(env.y, env.fy, envAt(at(env))) }));
|
|
1271
|
+
const laws = [...Object.entries(lemmas).map(([n, l]) => r.law(`${n} at y`, (env) => l(env.y, env.fy, envAt(at(env))))),
|
|
1272
|
+
...Object.entries(between).map(([n, l]) => r.law(n, (env) => l(env.x, env.fx, env.y, env.fy, envAt(at(env)))))];
|
|
1165
1273
|
const v = proveRule(r, laws);
|
|
1166
1274
|
failed('laws between two points', v);
|
|
1275
|
+
proofs.between = { machine: r.machine, laws };
|
|
1167
1276
|
verdict = { proven: true, checked: verdict.checked && v.checked };
|
|
1168
1277
|
}
|
|
1169
1278
|
return { ...base, verdict, facts: facts(true) };
|
|
@@ -1201,7 +1310,9 @@ const recurJs = (name, args, zero, next, uses) => {
|
|
|
1201
1310
|
}
|
|
1202
1311
|
};
|
|
1203
1312
|
const inlined = uses.map(([u, d], i) => `const $f${i} = (${fnSource(u, d)}); `).join('');
|
|
1204
|
-
|
|
1313
|
+
// The last value computed at each tuple of further arguments is kept, so reading f at x after f at a smaller x takes
|
|
1314
|
+
// only the steps between (a rule that reads f at a count that grows costs one step a request).
|
|
1315
|
+
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; }; })()`;
|
|
1205
1316
|
};
|
|
1206
1317
|
// Functions with the functions they read, those first (named <function>.<use>), each with the terms its facts read them by.
|
|
1207
1318
|
const flatFns = (defs) => defs.flatMap(([n, d]) => {
|
|
@@ -1223,7 +1334,7 @@ const fnSource = (name, d) => {
|
|
|
1223
1334
|
}
|
|
1224
1335
|
if (d.arity === 1)
|
|
1225
1336
|
return src;
|
|
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
|
|
1337
|
+
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); }; })()`;
|
|
1227
1338
|
};
|
|
1228
1339
|
const fnTerm = (name, d) => (...args) => {
|
|
1229
1340
|
if (args.length !== d.arity)
|
|
@@ -1343,6 +1454,27 @@ export class Rule {
|
|
|
1343
1454
|
facts(f) { return this.with({ facts: [...(this.spec.facts ?? []), f] }); }
|
|
1344
1455
|
// Maps each request carries whole (a set of names, say: cells 0 or 1), every cell past them 0; read like any map.
|
|
1345
1456
|
carries(...names) { return this.with({ carried: [...(this.spec.carried ?? []), ...names] }); }
|
|
1457
|
+
// A list of values of a kind each request carries, with folds over it and lemmas about the folds (CarriedList):
|
|
1458
|
+
// .carriesList('Lines', record({amount: nat}), {total: {from: 0, step: (acc, line) => acc.plus(line.amount)}})
|
|
1459
|
+
carriesList(name, of, folds = {}, lemmas = {}) {
|
|
1460
|
+
const entries = [[name, of, folds, lemmas]];
|
|
1461
|
+
for (const [n, , fs] of entries)
|
|
1462
|
+
for (const f of Object.keys(fs))
|
|
1463
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(f))
|
|
1464
|
+
throw new Error(`${n}: ${f} is not a plain fold name`);
|
|
1465
|
+
// The folds' lemmas, proven now (a lemma that isn't throws, with the verdict).
|
|
1466
|
+
for (const [n, k, fs, ls] of entries) {
|
|
1467
|
+
const v2 = new CarriedList(n, k, v(`${n}_len`), fs, part => new MapRef(part, 0), ls).prove();
|
|
1468
|
+
if (!(v2.proven && v2.checked))
|
|
1469
|
+
throw new Unproven(`${n}'s fold lemmas`, v2);
|
|
1470
|
+
}
|
|
1471
|
+
for (const [n, , fs] of entries)
|
|
1472
|
+
for (const f of Object.keys(fs))
|
|
1473
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(f))
|
|
1474
|
+
throw new Error(`${n}: ${f} is not a plain fold name`);
|
|
1475
|
+
const maps = entries.flatMap(([n, k, fs]) => [...k.vars(n), ...Object.keys(fs).map(f => `${n}$${f}`)]);
|
|
1476
|
+
return this.with({ carried: [...(this.spec.carried ?? []), ...maps], inputs: [...this.spec.inputs, ...entries.map(([n]) => [`${n}_len`, nat])], lists2: [...(this.spec.lists2 ?? []), ...entries] });
|
|
1477
|
+
}
|
|
1346
1478
|
// Named conditions of the step, decided once per step before it; each call's flags may read the earlier calls'. A flag
|
|
1347
1479
|
// named again replaces the earlier definition in place (what a variant needs: one call, the step unchanged); the new
|
|
1348
1480
|
// definition may read only the flags defined before the original's place, as the kernel checks.
|
|
@@ -1384,6 +1516,22 @@ export class Rule {
|
|
|
1384
1516
|
const [k1, more] = keysFor(out), holds = out.forall(v(k1), ...more.map(v)).c;
|
|
1385
1517
|
return more.length ? { name, forall: k1, keys: more, holds } : { name, forall: k1, holds: prenex(holds, `law ${name}`, k1) ?? holds };
|
|
1386
1518
|
}
|
|
1519
|
+
// 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
|
|
1520
|
+
// a case in range, a presence bit 0 or 1), as laws: proveRule, proveSteps and proveLeadsTo prove them beside the laws
|
|
1521
|
+
// they are given, so a law may lean on "the hold is idle, holding or released" without stating it; give them to
|
|
1522
|
+
// toLean (or the kernel) with the rest.
|
|
1523
|
+
kindLaws() {
|
|
1524
|
+
const out = [];
|
|
1525
|
+
const of = (env, n) => env[n];
|
|
1526
|
+
for (const [n, k] of this.spec.kinded ?? [])
|
|
1527
|
+
if (k.invariant(k.value(n)).length)
|
|
1528
|
+
out.push(this.law(`${n} is a value of its kind`, env => all(...k.invariant(of(env, n)))));
|
|
1529
|
+
for (const [n, k] of [...this.spec.cells ?? [], ...this.spec.items ?? []])
|
|
1530
|
+
if (k.invariant(k.value(n)).length) {
|
|
1531
|
+
out.push(this.law(`every ${n} cell is a value of its kind`, env => forall(key => all(...k.invariant(of(env, n).at(key))))));
|
|
1532
|
+
}
|
|
1533
|
+
return out;
|
|
1534
|
+
}
|
|
1387
1535
|
// A law about one step, for every request the rule allows: `then(before, after)` relates what the step reads (params,
|
|
1388
1536
|
// the request, state, maps, flags, sums, queries) to the state, maps and sums after it. `when` narrows the requests.
|
|
1389
1537
|
// counter.stepLaw('never goes back', ({used}, after) => used.atMost(after.used))
|
|
@@ -1456,6 +1604,8 @@ export class Rule {
|
|
|
1456
1604
|
// r.relate('the secret is irrelevant', (a, b) => a.user.eq(b.user), (a, b) => a.allowed.iff(b.allowed))
|
|
1457
1605
|
relate(name, given, then) {
|
|
1458
1606
|
const s = this.spec, b = (n) => `b.${n}`, own = [...s.inputs.map(([n]) => n), ...(s.carried ?? [])];
|
|
1607
|
+
if (s.lists2?.length)
|
|
1608
|
+
throw new Error(`${s.name}: a decision about two requests takes no carried list`);
|
|
1459
1609
|
const second = (env) => ({ ...env, ...Object.fromEntries(own.map(n => [n, env[b(n)]])) });
|
|
1460
1610
|
const pair = new Rule({ ...s, inputs: [...s.inputs, ...s.inputs.map(([n, k]) => [b(n), k])], carried: [...(s.carried ?? []), ...(s.carried ?? []).map(b)],
|
|
1461
1611
|
assume: [...s.assume, ...s.assume.map(f => (env) => f(second(env)))], flags: [], step: undefined, record: undefined, sums: [], queries: [], keep: undefined, relations: undefined, prune: undefined });
|
|
@@ -1499,6 +1649,12 @@ export class Rule {
|
|
|
1499
1649
|
out[n] = k.value(n);
|
|
1500
1650
|
for (const n of this.spec.carried ?? [])
|
|
1501
1651
|
out[n] = new MapRef(n, 0);
|
|
1652
|
+
for (const [n, k, fs, ls] of this.spec.lists2 ?? []) {
|
|
1653
|
+
const l = new CarriedList(n, k, v(`${n}_len`), fs, part => new MapRef(part, 0), ls);
|
|
1654
|
+
for (const part of l.maps)
|
|
1655
|
+
delete out[part];
|
|
1656
|
+
out[n] = l;
|
|
1657
|
+
}
|
|
1502
1658
|
}
|
|
1503
1659
|
if (parts.state !== false) {
|
|
1504
1660
|
for (const [n] of this.spec.state)
|
|
@@ -1609,7 +1765,7 @@ export class Rule {
|
|
|
1609
1765
|
state[n] = hoist.term(termOf(u));
|
|
1610
1766
|
}
|
|
1611
1767
|
const pre = [...this.kindAssumptions('params'), ...s.pre.map(f => f(pEnv).c)];
|
|
1612
|
-
const assume = [...this.kindAssumptions('inputs'), ...s.assume.map(f => hoist.cond(f(env).c))];
|
|
1768
|
+
const assume = [...this.kindAssumptions('inputs'), ...(s.lists2 ?? []).flatMap(([n]) => env[n].equations().map(c => hoist.cond(c.c))), ...s.assume.map(f => hoist.cond(f(env).c))];
|
|
1613
1769
|
placed();
|
|
1614
1770
|
// Functions, and their facts: each function's own (named after it), then the rule's.
|
|
1615
1771
|
// The functions a function reads come first, each named <function>.<use>.
|
|
@@ -1821,9 +1977,16 @@ export class Rule {
|
|
|
1821
1977
|
// Runtime values (numbers, booleans, choice names, null for a missing value) to the variables the machine reads.
|
|
1822
1978
|
encodeParams(values) { return encode(this.spec.params, values); }
|
|
1823
1979
|
// A carried map is a Set of keys (each cell 1), an array of them, or a Map of key to natural.
|
|
1980
|
+
// A carried list is an array of its items' runtime values (its length and folds computed from them).
|
|
1981
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1824
1982
|
encodeInput(values) {
|
|
1825
|
-
const
|
|
1983
|
+
const lists = Object.fromEntries((this.spec.lists2 ?? []).map(([n, k, fs]) => [n, new CarriedList(n, k, v(`${n}_len`), fs, part => new MapRef(part, 0)).encode(values[n])]));
|
|
1984
|
+
const out = encode(this.spec.inputs, { ...values, ...Object.fromEntries(Object.entries(lists).map(([n, l]) => [`${n}_len`, l.len])) });
|
|
1985
|
+
for (const l of Object.values(lists))
|
|
1986
|
+
Object.assign(out, l.maps);
|
|
1826
1987
|
for (const n of this.spec.carried ?? []) {
|
|
1988
|
+
if (n in out)
|
|
1989
|
+
continue;
|
|
1827
1990
|
const given = values[n];
|
|
1828
1991
|
out[n] = given instanceof Map ? new Map(given) : new Map([...given].map(key => [key, 1]));
|
|
1829
1992
|
}
|
|
@@ -1835,6 +1998,11 @@ export class Rule {
|
|
|
1835
1998
|
const out = Object.assign({}, ...this.spec.inputs.map(([n, k]) => k.random(n, rand, bound)));
|
|
1836
1999
|
for (const n of this.spec.carried ?? [])
|
|
1837
2000
|
out[n] = new Map(Array.from({ length: bound + 1 }, (_, key) => [key, rand(2)]).filter(([, cell]) => cell === 1));
|
|
2001
|
+
// A carried list: items drawn by their kind, its length and folds as the encoding computes them.
|
|
2002
|
+
for (const [n, k, fs] of this.spec.lists2 ?? []) {
|
|
2003
|
+
const l = new CarriedList(n, k, v(`${n}_len`), fs, part => new MapRef(part, 0)).encode(Array.from({ length: rand(bound + 1) }, () => k.sample(rand, bound)));
|
|
2004
|
+
Object.assign(out, l.maps, { [`${n}_len`]: l.len });
|
|
2005
|
+
}
|
|
1838
2006
|
return out;
|
|
1839
2007
|
}
|
|
1840
2008
|
// The state's scalars as runtime values: each kinded variable as its kind decodes it (an integer as one signed
|
|
@@ -1876,7 +2044,7 @@ const encode = (kinds, values) => Object.assign({}, ...kinds.map(([n, k]) => k.e
|
|
|
1876
2044
|
export const rule = (name) => new Rule({ name, params: [], inputs: [], state: [], maps: [], flags: [], pre: [], assume: [], sums: [], queries: [] });
|
|
1877
2045
|
// The kind a result field is kept in.
|
|
1878
2046
|
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);
|
|
2047
|
+
: 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);
|
|
1880
2048
|
export class Fun {
|
|
1881
2049
|
r;
|
|
1882
2050
|
out;
|
|
@@ -2019,16 +2187,18 @@ const goalsVerdict = (goals) => {
|
|
|
2019
2187
|
}
|
|
2020
2188
|
return { proven: true, checked: goals.every(g => check(g, trees[g.name])), trees };
|
|
2021
2189
|
};
|
|
2190
|
+
// The kinds' laws first, those the caller didn't pass already (by name).
|
|
2191
|
+
const withKinds = (r, laws) => [...r.kindLaws().filter(k => !laws.some(l => l.name === k.name)), ...laws];
|
|
2022
2192
|
export function proveRule(r, laws) {
|
|
2023
|
-
const out = proveLaws(r.machine, laws);
|
|
2193
|
+
const out = proveLaws(r.machine, withKinds(r, laws));
|
|
2024
2194
|
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 } : {}) } : {}) };
|
|
2025
2195
|
}
|
|
2026
2196
|
export function proveSteps(r, laws, stepLaws) {
|
|
2027
|
-
const out = proveStepLaws(r.machine, laws, stepLaws);
|
|
2197
|
+
const out = proveStepLaws(r.machine, withKinds(r, laws), stepLaws);
|
|
2028
2198
|
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 } : {}) } : {}) };
|
|
2029
2199
|
}
|
|
2030
2200
|
export function proveLeadsTo(r, laws, l) {
|
|
2031
|
-
const out = proveLiveness(r.machine, laws, l);
|
|
2201
|
+
const out = proveLiveness(r.machine, withKinds(r, laws), l);
|
|
2032
2202
|
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 } : {}) } : {}) };
|
|
2033
2203
|
}
|
|
2034
2204
|
export const proveDecision = (d) => goalsVerdict([d.goal]);
|
|
@@ -2059,3 +2229,112 @@ export function crossCheckDecision(d, opts = {}) {
|
|
|
2059
2229
|
return { checked };
|
|
2060
2230
|
}
|
|
2061
2231
|
export const proveLoop = (l, laws) => goalsVerdict(loopObligations(l.loop, laws, l.rankTerms));
|
|
2232
|
+
export class FoldRule {
|
|
2233
|
+
spec;
|
|
2234
|
+
constructor(spec) {
|
|
2235
|
+
this.spec = spec;
|
|
2236
|
+
}
|
|
2237
|
+
with(more) { return new FoldRule({ ...this.spec, ...more }); }
|
|
2238
|
+
get name() { return this.spec.name; }
|
|
2239
|
+
params(kinds) { return this.with({ params: [...this.spec.params, ...Object.entries(kinds)] }); }
|
|
2240
|
+
inputs(kinds) { return this.with({ inputs: [...this.spec.inputs, ...Object.entries(kinds)] }); }
|
|
2241
|
+
pre(f) { return this.with({ pre: [...this.spec.pre, f] }); }
|
|
2242
|
+
// What every leaf's and node's inputs satisfy (with their kinds' own assumptions).
|
|
2243
|
+
assume(f) { return this.with({ assume: [...this.spec.assume, f] }); }
|
|
2244
|
+
leaf(f) { return this.with({ leaf: f }); }
|
|
2245
|
+
node(f) { return this.with({ node: f }); }
|
|
2246
|
+
env(inputs = true) {
|
|
2247
|
+
const out = {};
|
|
2248
|
+
for (const [n, k] of this.spec.params)
|
|
2249
|
+
out[n] = k.value(n);
|
|
2250
|
+
if (inputs)
|
|
2251
|
+
for (const [n, k] of this.spec.inputs)
|
|
2252
|
+
out[n] = k.value(n);
|
|
2253
|
+
return out;
|
|
2254
|
+
}
|
|
2255
|
+
names() {
|
|
2256
|
+
if (!this.spec.leaf)
|
|
2257
|
+
throw new Error(`${this.spec.name}: a fold needs a leaf`);
|
|
2258
|
+
return Object.keys(this.spec.leaf(this.env()));
|
|
2259
|
+
}
|
|
2260
|
+
side(prefix) { return Object.fromEntries(this.names().map(n => [n, v(`${prefix}${n}`)])); }
|
|
2261
|
+
// A law about the values (and the params): it reads no input.
|
|
2262
|
+
law(name, f) { return { name, holds: f({ ...this.env(false), ...this.side('') }).c }; }
|
|
2263
|
+
get fold() {
|
|
2264
|
+
const s = this.spec;
|
|
2265
|
+
if (!s.leaf || !s.node)
|
|
2266
|
+
throw new Error(`${s.name}: a fold needs a leaf and a node`);
|
|
2267
|
+
const env = this.env(), pEnv = this.env(false);
|
|
2268
|
+
const pre = [...s.params.flatMap(([n, k]) => k.assume(n)).map(c => c.c), ...s.pre.map(f => f(pEnv).c)];
|
|
2269
|
+
const assume = [...s.inputs.flatMap(([n, k]) => k.assume(n)).map(c => c.c), ...s.assume.map(f => f(env).c)];
|
|
2270
|
+
const terms = (r) => Object.fromEntries(Object.entries(r).map(([n, t]) => [n, termOf(t)]));
|
|
2271
|
+
const leaf = terms(s.leaf(env)), node = terms(s.node(env, this.side('l_'), this.side('r_')));
|
|
2272
|
+
return { name: s.name, params: s.params.flatMap(([n, k]) => k.vars(n)), inputs: s.inputs.flatMap(([n, k]) => k.vars(n)), leaf, node,
|
|
2273
|
+
...(pre.length ? { pre: pre.length === 1 ? pre[0] : { op: 'and', xs: pre } } : {}), ...(assume.length ? { assume: assume.length === 1 ? assume[0] : { op: 'and', xs: assume } } : {}) };
|
|
2274
|
+
}
|
|
2275
|
+
// The values of a tree, by the terms that were proven (each leaf's and node's inputs as their kinds encode them; one
|
|
2276
|
+
// that doesn't meet `assume` is refused, as the laws are claimed only of trees that meet it).
|
|
2277
|
+
run(tree, params = {}) {
|
|
2278
|
+
const f = this.fold, ps = encode(this.spec.params, params), none = { name: f.name, params: [], inputs: [], state: {}, step: {} };
|
|
2279
|
+
const st = { scalars: {}, maps: {} };
|
|
2280
|
+
if (f.pre && !evalC(f.pre, ps, st, none))
|
|
2281
|
+
throw new RangeError(`${f.name}: the params don't meet its precondition`);
|
|
2282
|
+
const go = (t) => {
|
|
2283
|
+
const own = 'leaf' in t ? t.leaf : t.node, env = { ...ps, ...encode(this.spec.inputs, own) };
|
|
2284
|
+
if (f.assume && !evalC(f.assume, env, st, none))
|
|
2285
|
+
throw new RangeError(`${f.name}: ${JSON.stringify(own)} doesn't meet its assumption`);
|
|
2286
|
+
if ('leaf' in t)
|
|
2287
|
+
return Object.fromEntries(Object.entries(f.leaf).map(([n, e]) => [n, evalE(e, env, st, none)]));
|
|
2288
|
+
const l = go(t.left), r = go(t.right);
|
|
2289
|
+
const both = { ...env, ...Object.fromEntries(Object.entries(l).map(([n, x]) => [`l_${n}`, x])), ...Object.fromEntries(Object.entries(r).map(([n, x]) => [`r_${n}`, x])) };
|
|
2290
|
+
return Object.fromEntries(Object.entries(f.node).map(([n, e]) => [n, evalE(e, both, st, none)]));
|
|
2291
|
+
};
|
|
2292
|
+
return go(tree);
|
|
2293
|
+
}
|
|
2294
|
+
// The builders against what they built, on random trees: each value by the callbacks' own arithmetic against the
|
|
2295
|
+
// terms the kernel proved (as run evaluates them), and each law's callback against its condition.
|
|
2296
|
+
crossCheck(laws = [], opts = {}) {
|
|
2297
|
+
const s = this.spec, rand = opts.rand ?? seeded(13), bound = opts.bound ?? 6, f = this.fold;
|
|
2298
|
+
let checked = 0;
|
|
2299
|
+
const sample = (kinds) => Object.fromEntries(kinds.map(([n, k]) => [n, k.sample(rand, bound)]));
|
|
2300
|
+
const tree = (d) => (d === 0 || rand(3) === 0 ? { leaf: sample(s.inputs) } : { node: sample(s.inputs), left: tree(d - 1), right: tree(d - 1) });
|
|
2301
|
+
const known = (vals) => Object.fromEntries(Object.entries(vals).map(([n, x]) => [n, new Term({ op: 'const', value: x }, x)]));
|
|
2302
|
+
for (let t = 0; t < (opts.trials ?? 200); t++) {
|
|
2303
|
+
const params = sample(s.params), ps = encode(s.params, params);
|
|
2304
|
+
const pv = known(ps), pEnv = Object.fromEntries(s.params.map(([n, k]) => [n, k.build(n, part => pv[part])]));
|
|
2305
|
+
const builder = (tr) => {
|
|
2306
|
+
const own = 'leaf' in tr ? tr.leaf : tr.node, iv = known(encode(s.inputs, own)), env = { ...pEnv, ...Object.fromEntries(s.inputs.map(([n, k]) => [n, k.build(n, part => iv[part])])) };
|
|
2307
|
+
const out = 'leaf' in tr ? s.leaf(env) : (() => { const l = builder(tr.left), r = builder(tr.right); return l && r ? s.node(env, known(l), known(r)) : undefined; })();
|
|
2308
|
+
if (!out)
|
|
2309
|
+
return undefined;
|
|
2310
|
+
const vals = Object.entries(out).map(([n, x]) => [n, valOf(x)]);
|
|
2311
|
+
return vals.every(([, x]) => x !== undefined) ? Object.fromEntries(vals) : undefined;
|
|
2312
|
+
};
|
|
2313
|
+
const tr = tree(opts.depth ?? 3);
|
|
2314
|
+
let ours;
|
|
2315
|
+
try {
|
|
2316
|
+
ours = this.run(tr, params);
|
|
2317
|
+
}
|
|
2318
|
+
catch {
|
|
2319
|
+
continue;
|
|
2320
|
+
}
|
|
2321
|
+
const theirs = builder(tr);
|
|
2322
|
+
if (!theirs)
|
|
2323
|
+
continue;
|
|
2324
|
+
for (const [n, x] of Object.entries(theirs)) {
|
|
2325
|
+
checked++;
|
|
2326
|
+
if (ours[n] !== x)
|
|
2327
|
+
return { checked, failure: `${f.name}: ${n} is ${x} by its callbacks, ${ours[n]} by the terms proven, for ${JSON.stringify(tr)}` };
|
|
2328
|
+
}
|
|
2329
|
+
for (const law of laws) {
|
|
2330
|
+
const ok = evalC(law.holds, { ...ps, ...ours }, { scalars: {}, maps: {} }, { name: f.name, params: [], inputs: [], state: {}, step: {} });
|
|
2331
|
+
checked++;
|
|
2332
|
+
if (!ok)
|
|
2333
|
+
return { checked, failure: `${f.name}: law ${law.name} fails for ${JSON.stringify(tr)} with params ${JSON.stringify(params)}` };
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
return { checked };
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
export const fold = (name) => new FoldRule({ name, params: [], inputs: [], pre: [], assume: [] });
|
|
2340
|
+
export const proveFold = (f, laws) => goalsVerdict(foldObligations(f.fold, laws));
|
package/dist/core/gdp.d.ts
CHANGED
|
@@ -6,8 +6,8 @@ export type Mint<K extends string, A> = <S extends AnyNamed[]>(args: A, ...about
|
|
|
6
6
|
export { Unproven };
|
|
7
7
|
type AnyFun = Fun<any, any, any>;
|
|
8
8
|
type Conditions<O> = {
|
|
9
|
-
[k in keyof O]: O[k] extends boolean ? k : never;
|
|
10
|
-
}[keyof O
|
|
9
|
+
[k in keyof O & string]: O[k] extends boolean ? k : O[k] extends Record<string, unknown> ? `${k}.${Conditions<O[k]>}` : never;
|
|
10
|
+
}[keyof O & string];
|
|
11
11
|
type ArgsOf<F extends AnyFun> = Parameters<F['run']>[0];
|
|
12
12
|
type ParamsOf<F extends AnyFun> = NonNullable<Parameters<F['run']>[1]>;
|
|
13
13
|
export declare function guard<K extends string, F extends AnyFun>(prover: Prover<K>, fn: F, field: Conditions<ReturnType<F['run']>>, opts: {
|
package/dist/core/gdp.js
CHANGED
|
@@ -4,6 +4,7 @@ import { allowed, requestEnv } from "./watch.js";
|
|
|
4
4
|
export { Unproven };
|
|
5
5
|
const proven = (what, v) => { if (!(v.proven && v.checked))
|
|
6
6
|
throw new Unproven(what, v); };
|
|
7
|
+
const atPath = (o, path) => path.split('.').reduce((x, f) => (typeof x === 'object' && x !== null ? x[f] : undefined), o);
|
|
7
8
|
// A proof wherever `fn`'s result `field` holds. Every law in `laws` must have been stated about `fn` (fn.law or
|
|
8
9
|
// fn.relate) and is proven now; the arguments it is later called with must meet the function's kinds and `assume`.
|
|
9
10
|
// const access = fun('document access').inputs({...}).returns(env => ({permit: permit(env)}))
|
|
@@ -23,7 +24,7 @@ export function guard(prover, fn, field, opts) {
|
|
|
23
24
|
const input = r.encodeInput(args);
|
|
24
25
|
if (m.assume && !evalC(m.assume, { ...ps, ...input }, { scalars: {}, maps: {} }, m))
|
|
25
26
|
return null;
|
|
26
|
-
return fn.run(args, params)
|
|
27
|
+
return atPath(fn.run(args, params), field) === true ? prover.prove(...about) : null;
|
|
27
28
|
};
|
|
28
29
|
}
|
|
29
30
|
// A proof for each request whose step takes `flag`, the rule's laws (and step laws) proven now. One gate holds one
|
package/dist/core/machine.d.ts
CHANGED
|
@@ -88,6 +88,16 @@ export type Recurrence = {
|
|
|
88
88
|
arity: number;
|
|
89
89
|
lean?: string;
|
|
90
90
|
}>;
|
|
91
|
+
proofs?: {
|
|
92
|
+
lemmas?: {
|
|
93
|
+
machine: Machine;
|
|
94
|
+
laws: Law[];
|
|
95
|
+
};
|
|
96
|
+
between?: {
|
|
97
|
+
machine: Machine;
|
|
98
|
+
laws: Law[];
|
|
99
|
+
};
|
|
100
|
+
};
|
|
91
101
|
};
|
|
92
102
|
export type History = {
|
|
93
103
|
keep: C;
|