gdp-wired 0.9.0 → 0.10.1
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 +18 -2
- package/dist/core/dsl.d.ts +87 -2
- package/dist/core/dsl.js +237 -4
- package/dist/core/solver.d.ts +1 -0
- package/dist/core/solver.js +116 -5
- package/dist/kernel/kernel.d.ts +9 -1
- package/dist/kernel/kernel.js +168 -11
- package/dist/kernel/prove.d.ts +21 -0
- package/dist/kernel/prove.js +128 -22
- package/dist/lean-kernel/emit.d.ts +4 -0
- package/dist/lean-kernel/emit.js +12 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -67,6 +67,10 @@ All of these are proven automatically, and each proof is also a Lean theorem:
|
|
|
67
67
|
- **Records and variants**: inputs, state, results, map cells and list items made of named fields, or one of several
|
|
68
68
|
cases each with fields of its own (a payment by card or in cash, a seat free, held or sold), every case covered
|
|
69
69
|
(`examples/datatypes.ts`).
|
|
70
|
+
- **Lists a request carries, and trees**: an invoice's lines, with folds over them (the total, the taxable part) and
|
|
71
|
+
lemmas about the folds proven by induction over the lines (`examples/invoices.ts`); a fold over a tree as a value (a
|
|
72
|
+
spend roll-up over an org chart, sign-offs over a document's sections), its laws proven by induction over the tree
|
|
73
|
+
(`examples/folds.ts`).
|
|
70
74
|
- **Pure functions and two-run laws**: laws about what a function returns, and laws comparing two calls, like "the
|
|
71
75
|
secret never changes the answer". `examples/jev-fabric/` ports a Bend project's policy core and proves all 59 laws its
|
|
72
76
|
Bend proofs state.
|
|
@@ -218,7 +222,19 @@ const soon = retry.law('the next try is at most 64 after the last request', ({ne
|
|
|
218
222
|
twin see nothing new. Maps and lists of them: `.maps({Acct: record({...}).from({...})})` (`Acct.at(k)`,
|
|
219
223
|
`Acct.set(k, value)`, `Acct.each(k => value)`) and `.items({Queue: Job})` (`push`, `pop`, `set`, `insertAt`,
|
|
220
224
|
`removeAt`, `apply`, `has`, `within`, `increasing(by)`), one map per variable of the kind; `rule.decode` reads them back
|
|
221
|
-
as objects and arrays.
|
|
225
|
+
as objects and arrays.
|
|
226
|
+
- **Lists a request carries** (`.carriesList('Lines', Line, folds, lemmas)`): the items one carried map per variable of
|
|
227
|
+
the kind, the length `Lines.len`, each fold (`{total: {from: 0, step: (acc, line) => acc.plus(line.amount)}}`) a
|
|
228
|
+
carried map of its running values whose equations every request is assumed to meet (the encoding computes them from the
|
|
229
|
+
array, and a request that doesn't meet them isn't allowed), read as `Lines.fold.total`; `Lines.every`, `some`, `has`,
|
|
230
|
+
`increasing(by)`, `at(i)`. A lemma about the folds (`({taxed, total}) => taxed.atMost(total)`) is proven by induction
|
|
231
|
+
over the items when the rule is built, then assumed at every prefix (`examples/invoices.ts`).
|
|
232
|
+
- **Folds over trees** (`fold(name).params(...).inputs(...).leaf(env => ({...})).node((env, left, right) => ({...}))`,
|
|
233
|
+
`proveFold`): a tree as a value (`{leaf: inputs}` or `{node: inputs, left, right}`), each leaf's values from its inputs
|
|
234
|
+
and each node's from its own and its children's; a law about the values is proven by induction over the tree (the
|
|
235
|
+
kernel's leaf and node goals, Lean's `fold_sound`), `f.run(tree, params)` evaluates the terms proven and `f.crossCheck`
|
|
236
|
+
holds the builders to them (`examples/folds.ts`). A tree in a rule's state, and claims that need a lemma about the
|
|
237
|
+
tree's shape nobody stated, stay Lean's.
|
|
222
238
|
- **State of any kind**: `.state({open: bool.from(true), status: oneOf('new', 'done').from('new'), owner: str().from(''), note: maybe(nat).from(null)})`;
|
|
223
239
|
a step writes each kind's own values, `when(c, a, b)` chooses between any two values of a kind, and `rule.decode(state)`
|
|
224
240
|
reads them back as runtime values.
|
|
@@ -313,7 +329,7 @@ Lean, compiled natively, wins where the state stays small. Details in [`docs/com
|
|
|
313
329
|
| `core/` | Machines, the interpreter and JS compiler, the builders, composition (`compose.ts`), the linear solver and its checker, Houdini invariants, models, the first Lean export |
|
|
314
330
|
| `kernel/` | The trusted kernel and the untrusted prover, with soundness fuzz (`kernel.test.ts`, `history.test.ts`) |
|
|
315
331
|
| `lean-kernel/` | The kernel's Lean twin, its soundness proofs, and the exported trees |
|
|
316
|
-
| `examples/` | The builders at work: `domains/` (187 rules across 21 domains, each with laws and broken variants), `harness.ts` (the triple check), `readme.ts`, `budget.ts`, `gdp.ts` (gdp-ts's example, its checks proven), `datatypes.ts` (records and variants), `lemmas.ts` (recurrences and their lemmas), `composition.ts`, `sets.ts`, `quantifiers.ts`, `functions.ts`, `lists.ts`, `sizes.ts`, `liveness.ts`, `steps.ts` (laws over up to 16 steps), and `jev-fabric/` (a Bend project's 59 proven laws, ported) |
|
|
332
|
+
| `examples/` | The builders at work: `domains/` (187 rules across 21 domains, each with laws and broken variants), `harness.ts` (the triple check), `readme.ts`, `budget.ts`, `gdp.ts` (gdp-ts's example, its checks proven), `datatypes.ts` (records and variants), `lemmas.ts` (recurrences and their lemmas), `invoices.ts` (lists a request carries), `folds.ts` (folds over trees), `composition.ts`, `sets.ts`, `quantifiers.ts`, `functions.ts`, `lists.ts`, `sizes.ts`, `liveness.ts`, `steps.ts` (laws over up to 16 steps), and `jev-fabric/` (a Bend project's 59 proven laws, ported) |
|
|
317
333
|
| `docs/` | The comparison with Bend and Lean, and the full scope |
|
|
318
334
|
| `compare/` | The same three rules in gdp-wired, Bend and plain Lean, checked and measured |
|
|
319
335
|
| `research/` | Everything around the library ([`research/README.md`](research/README.md)): the GPU searches (`search/`, `hw/`, `modal-gpu/`), invariant and rank synthesis (`synth/`), relational laws and guard synthesis (`next/`), bit-vector laws (`sat/`), the enumeration and benchmarks (`enum/`, `gpu/`, `perf/`), the first Lean checker (`lean/`) and the first prototype, the audit path (`audit/`) |
|
package/dist/core/dsl.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type C, type E, type Law, type Recurrence, type Machine, type Request, type State } from './machine.js';
|
|
2
|
-
import { type Goal, type Liveness, type Loop, type StepLaw, type Tree } from '../kernel/kernel.js';
|
|
2
|
+
import { type Fold, type Goal, type Liveness, type Loop, type StepLaw, type Tree } from '../kernel/kernel.js';
|
|
3
3
|
export type TermLike = Term | number;
|
|
4
4
|
export declare const termOf: (t: TermLike) => E;
|
|
5
5
|
export declare class Term {
|
|
@@ -310,6 +310,35 @@ export type ItemsOp = {
|
|
|
310
310
|
readonly parts: Record<string, ListOp>;
|
|
311
311
|
readonly len: TermLike;
|
|
312
312
|
};
|
|
313
|
+
export type ListFold<V> = {
|
|
314
|
+
from: number;
|
|
315
|
+
step: (acc: Term, item: V) => TermLike;
|
|
316
|
+
};
|
|
317
|
+
export type ListLemma = (acc: Record<string, Term>, i: Term) => Cond;
|
|
318
|
+
export declare class CarriedList<V, Fd extends string = never> {
|
|
319
|
+
readonly name: string;
|
|
320
|
+
readonly kind: Kind<V>;
|
|
321
|
+
readonly len: Term;
|
|
322
|
+
readonly folds: Record<Fd, ListFold<V>>;
|
|
323
|
+
private readonly map;
|
|
324
|
+
readonly lemmas: Record<string, ListLemma>;
|
|
325
|
+
readonly fold: Record<Fd, Term>;
|
|
326
|
+
constructor(name: string, kind: Kind<V>, len: Term, folds: Record<Fd, ListFold<V>>, map: (part: string) => MapRef, lemmas?: Record<string, ListLemma>);
|
|
327
|
+
at(i: TermLike): V;
|
|
328
|
+
after(fold: Fd, i: TermLike): Term;
|
|
329
|
+
every(f: (item: V, i: Term) => Cond): Cond;
|
|
330
|
+
some(f: (item: V, i: Term) => Cond): Cond;
|
|
331
|
+
has(x: V): Cond;
|
|
332
|
+
increasing(by: (item: V) => Term): Cond;
|
|
333
|
+
equations(): Cond[];
|
|
334
|
+
private running;
|
|
335
|
+
prove(): Verdict;
|
|
336
|
+
get maps(): string[];
|
|
337
|
+
encode(raw: Raw): {
|
|
338
|
+
maps: Record<string, Map<number, number>>;
|
|
339
|
+
len: number;
|
|
340
|
+
};
|
|
341
|
+
}
|
|
313
342
|
export declare class ItemsRef<V> {
|
|
314
343
|
readonly name: string;
|
|
315
344
|
readonly kind: Kind<V>;
|
|
@@ -392,6 +421,7 @@ type Spec = {
|
|
|
392
421
|
items?: [string, Kind<unknown>][];
|
|
393
422
|
kinded?: [string, Kind<unknown>][];
|
|
394
423
|
carried?: string[];
|
|
424
|
+
lists2?: [string, Kind<unknown>, Record<string, ListFold<unknown>>, Record<string, ListLemma>][];
|
|
395
425
|
fns?: [string, FnDef][];
|
|
396
426
|
facts?: ((env: never) => Record<string, Cond | Forall>)[];
|
|
397
427
|
flags: ((env: never) => Record<string, Cond>)[];
|
|
@@ -444,6 +474,9 @@ export declare class Rule<P extends Obj = {}, I extends Obj = {}, S extends Obj
|
|
|
444
474
|
carries<const N extends string>(...names: N[]): Rule<P, I & {
|
|
445
475
|
[k in N]: MapRef;
|
|
446
476
|
}, S, M, F, R, Su, Q>;
|
|
477
|
+
carriesList<const N extends string, V, Fd extends string = never>(name: N, of: Kind<V>, folds?: Record<Fd, ListFold<V>>, lemmas?: Record<string, (acc: Record<Fd, Term>, i: Term) => Cond>): Rule<P, I & {
|
|
478
|
+
[k in N]: CarriedList<V, Fd>;
|
|
479
|
+
}, S, M, F, R, Su, Q>;
|
|
447
480
|
flags<T extends Record<string, Cond>>(f: (env: P & I & S & M & F & Su & Q) => T): Rule<P, I, S, M, F & {
|
|
448
481
|
[k in keyof T]: Cond;
|
|
449
482
|
}, R, Su, Q>;
|
|
@@ -516,7 +549,7 @@ export declare class Rule<P extends Obj = {}, I extends Obj = {}, S extends Obj
|
|
|
516
549
|
[k in keyof P]: Raw;
|
|
517
550
|
}): Record<string, number>;
|
|
518
551
|
encodeInput(values: {
|
|
519
|
-
[k in keyof I]: I[k] extends MapRef ? ReadonlySet<number> | readonly number[] | ReadonlyMap<number, number> : Raw;
|
|
552
|
+
[k in keyof I]: I[k] extends MapRef ? ReadonlySet<number> | readonly number[] | ReadonlyMap<number, number> : I[k] extends CarriedList<any, any> ? readonly Raw[] : Raw;
|
|
520
553
|
}): Request;
|
|
521
554
|
randomParams(rand: Rand, bound: number): Record<string, number>;
|
|
522
555
|
randomInput(rand: Rand, bound: number): Request;
|
|
@@ -690,3 +723,55 @@ export declare function crossCheckDecision(d: Decision, opts?: {
|
|
|
690
723
|
failure?: string;
|
|
691
724
|
};
|
|
692
725
|
export declare const proveLoop: (l: LoopRule<Obj, Obj, Obj>, laws: Law[]) => Verdict;
|
|
726
|
+
export type TreeValue<I> = {
|
|
727
|
+
leaf: I;
|
|
728
|
+
} | {
|
|
729
|
+
node: I;
|
|
730
|
+
left: TreeValue<I>;
|
|
731
|
+
right: TreeValue<I>;
|
|
732
|
+
};
|
|
733
|
+
type FoldSpec = {
|
|
734
|
+
name: string;
|
|
735
|
+
params: [string, Kind<unknown>][];
|
|
736
|
+
inputs: [string, Kind<unknown>][];
|
|
737
|
+
pre: ((p: never) => Cond)[];
|
|
738
|
+
assume: ((env: never) => Cond)[];
|
|
739
|
+
leaf?: (env: never) => Record<string, TermLike>;
|
|
740
|
+
node?: (env: never, left: never, right: never) => Record<string, TermLike>;
|
|
741
|
+
};
|
|
742
|
+
export declare class FoldRule<P extends Obj = {}, I extends Obj = {}, S extends Obj = {}> {
|
|
743
|
+
private readonly spec;
|
|
744
|
+
constructor(spec: FoldSpec);
|
|
745
|
+
private with;
|
|
746
|
+
get name(): string;
|
|
747
|
+
params<K extends Kinds>(kinds: K): FoldRule<P & ValuesOf<K>, I, S>;
|
|
748
|
+
inputs<K extends Kinds>(kinds: K): FoldRule<P, I & ValuesOf<K>, S>;
|
|
749
|
+
pre(f: (p: P) => Cond): FoldRule<P, I, S>;
|
|
750
|
+
assume(f: (env: P & I) => Cond): FoldRule<P, I, S>;
|
|
751
|
+
leaf<T extends Record<string, TermLike>>(f: (env: P & I) => T): FoldRule<P, I, {
|
|
752
|
+
[k in keyof T]: Term;
|
|
753
|
+
}>;
|
|
754
|
+
node(f: (env: P & I, left: S, right: S) => {
|
|
755
|
+
[k in keyof S]: TermLike;
|
|
756
|
+
}): FoldRule<P, I, S>;
|
|
757
|
+
private env;
|
|
758
|
+
private names;
|
|
759
|
+
private side;
|
|
760
|
+
law(name: string, f: (env: P & S) => Cond): Law;
|
|
761
|
+
get fold(): Fold;
|
|
762
|
+
run(tree: TreeValue<{
|
|
763
|
+
[k in keyof I]: Raw;
|
|
764
|
+
}>, params?: {
|
|
765
|
+
[k in keyof P]?: Raw;
|
|
766
|
+
}): {
|
|
767
|
+
[k in keyof S]: number;
|
|
768
|
+
};
|
|
769
|
+
crossCheck(laws?: Law[], opts?: {
|
|
770
|
+
trials?: number;
|
|
771
|
+
depth?: number;
|
|
772
|
+
bound?: number;
|
|
773
|
+
rand?: Rand;
|
|
774
|
+
}): Agreement;
|
|
775
|
+
}
|
|
776
|
+
export declare const fold: (name: string) => FoldRule;
|
|
777
|
+
export declare const proveFold: (f: FoldRule<Obj, Obj, Obj>, laws: Law[]) => Verdict;
|
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) => {
|
|
@@ -867,6 +867,89 @@ export class CellsRef {
|
|
|
867
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
868
|
}
|
|
869
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
|
+
}
|
|
870
953
|
export class ItemsRef {
|
|
871
954
|
name;
|
|
872
955
|
kind;
|
|
@@ -1371,6 +1454,27 @@ export class Rule {
|
|
|
1371
1454
|
facts(f) { return this.with({ facts: [...(this.spec.facts ?? []), f] }); }
|
|
1372
1455
|
// Maps each request carries whole (a set of names, say: cells 0 or 1), every cell past them 0; read like any map.
|
|
1373
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
|
+
}
|
|
1374
1478
|
// Named conditions of the step, decided once per step before it; each call's flags may read the earlier calls'. A flag
|
|
1375
1479
|
// named again replaces the earlier definition in place (what a variant needs: one call, the step unchanged); the new
|
|
1376
1480
|
// definition may read only the flags defined before the original's place, as the kernel checks.
|
|
@@ -1500,6 +1604,8 @@ export class Rule {
|
|
|
1500
1604
|
// r.relate('the secret is irrelevant', (a, b) => a.user.eq(b.user), (a, b) => a.allowed.iff(b.allowed))
|
|
1501
1605
|
relate(name, given, then) {
|
|
1502
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`);
|
|
1503
1609
|
const second = (env) => ({ ...env, ...Object.fromEntries(own.map(n => [n, env[b(n)]])) });
|
|
1504
1610
|
const pair = new Rule({ ...s, inputs: [...s.inputs, ...s.inputs.map(([n, k]) => [b(n), k])], carried: [...(s.carried ?? []), ...(s.carried ?? []).map(b)],
|
|
1505
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 });
|
|
@@ -1543,6 +1649,12 @@ export class Rule {
|
|
|
1543
1649
|
out[n] = k.value(n);
|
|
1544
1650
|
for (const n of this.spec.carried ?? [])
|
|
1545
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
|
+
}
|
|
1546
1658
|
}
|
|
1547
1659
|
if (parts.state !== false) {
|
|
1548
1660
|
for (const [n] of this.spec.state)
|
|
@@ -1653,7 +1765,7 @@ export class Rule {
|
|
|
1653
1765
|
state[n] = hoist.term(termOf(u));
|
|
1654
1766
|
}
|
|
1655
1767
|
const pre = [...this.kindAssumptions('params'), ...s.pre.map(f => f(pEnv).c)];
|
|
1656
|
-
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))];
|
|
1657
1769
|
placed();
|
|
1658
1770
|
// Functions, and their facts: each function's own (named after it), then the rule's.
|
|
1659
1771
|
// The functions a function reads come first, each named <function>.<use>.
|
|
@@ -1865,9 +1977,16 @@ export class Rule {
|
|
|
1865
1977
|
// Runtime values (numbers, booleans, choice names, null for a missing value) to the variables the machine reads.
|
|
1866
1978
|
encodeParams(values) { return encode(this.spec.params, values); }
|
|
1867
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
|
|
1868
1982
|
encodeInput(values) {
|
|
1869
|
-
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);
|
|
1870
1987
|
for (const n of this.spec.carried ?? []) {
|
|
1988
|
+
if (n in out)
|
|
1989
|
+
continue;
|
|
1871
1990
|
const given = values[n];
|
|
1872
1991
|
out[n] = given instanceof Map ? new Map(given) : new Map([...given].map(key => [key, 1]));
|
|
1873
1992
|
}
|
|
@@ -1879,6 +1998,11 @@ export class Rule {
|
|
|
1879
1998
|
const out = Object.assign({}, ...this.spec.inputs.map(([n, k]) => k.random(n, rand, bound)));
|
|
1880
1999
|
for (const n of this.spec.carried ?? [])
|
|
1881
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
|
+
}
|
|
1882
2006
|
return out;
|
|
1883
2007
|
}
|
|
1884
2008
|
// The state's scalars as runtime values: each kinded variable as its kind decodes it (an integer as one signed
|
|
@@ -2105,3 +2229,112 @@ export function crossCheckDecision(d, opts = {}) {
|
|
|
2105
2229
|
return { checked };
|
|
2106
2230
|
}
|
|
2107
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/solver.d.ts
CHANGED
package/dist/core/solver.js
CHANGED
|
@@ -8,18 +8,38 @@
|
|
|
8
8
|
// exact shadow). Then a rational solution of the rest extends to an integer x, so back-substitution finds a witness.
|
|
9
9
|
export const v = (name) => ({ [name]: 1 });
|
|
10
10
|
export const k = (value) => (value === 0 ? {} : { 1: value });
|
|
11
|
+
// (Plain loops, no intermediate arrays: these run millions of times in a proof. Each keeps the keys in the order they
|
|
12
|
+
// first appear, as a sum over the entries would.)
|
|
11
13
|
export function add(...xs) {
|
|
12
14
|
const out = {};
|
|
13
15
|
for (const x of xs)
|
|
14
|
-
for (const
|
|
15
|
-
out[key] = (out[key] ?? 0) +
|
|
16
|
-
for (const key
|
|
16
|
+
for (const key in x)
|
|
17
|
+
out[key] = (out[key] ?? 0) + x[key];
|
|
18
|
+
for (const key in out)
|
|
17
19
|
if (out[key] === 0)
|
|
18
20
|
delete out[key];
|
|
19
21
|
return out;
|
|
20
22
|
}
|
|
21
|
-
export const scale = (x, f) =>
|
|
22
|
-
|
|
23
|
+
export const scale = (x, f) => {
|
|
24
|
+
const out = {};
|
|
25
|
+
for (const key in x) {
|
|
26
|
+
const value = x[key] * f;
|
|
27
|
+
if (value !== 0)
|
|
28
|
+
out[key] = value;
|
|
29
|
+
}
|
|
30
|
+
return out;
|
|
31
|
+
};
|
|
32
|
+
export const sub = (a, b) => {
|
|
33
|
+
const out = {};
|
|
34
|
+
for (const key in a)
|
|
35
|
+
out[key] = a[key];
|
|
36
|
+
for (const key in b)
|
|
37
|
+
out[key] = (out[key] ?? 0) - b[key];
|
|
38
|
+
for (const key in out)
|
|
39
|
+
if (out[key] === 0)
|
|
40
|
+
delete out[key];
|
|
41
|
+
return out;
|
|
42
|
+
};
|
|
23
43
|
export function show(x) {
|
|
24
44
|
const parts = Object.entries(x).filter(([key]) => key !== '1').map(([key, c]) => (c === 1 ? key : c === -1 ? `-${key}` : `${c}${key}`));
|
|
25
45
|
if ((x['1'] ?? 0) !== 0 || parts.length === 0)
|
|
@@ -180,3 +200,94 @@ function prune(steps, last) {
|
|
|
180
200
|
return s.kind === 'combine' ? { ...s, a: at.get(s.a), b: at.get(s.b) } : s.kind === 'cut' ? { ...s, of: at.get(s.of) } : s;
|
|
181
201
|
});
|
|
182
202
|
}
|
|
203
|
+
// A refutation found fast, where there is one over the rationals: Farkas' lemma says rows a·x + c <= 0 (x free) have no
|
|
204
|
+
// rational solution exactly when some λ >= 0 has Σ λ·a = 0 and Σ λ·c > 0. A floating-point simplex looks for such λ
|
|
205
|
+
// (phase one on Σ λ·a = 0, Σ λ·c = 1), and elimination on the rows it uses alone (usually a handful) gives the
|
|
206
|
+
// derivation, renumbered to the whole system's rows. Floating point only picks the rows: the derivation is exact, and
|
|
207
|
+
// the checker sees nothing else. Undefined when no λ turns up (a system with a rational solution, or one the floats
|
|
208
|
+
// misjudge): the caller goes on as without it.
|
|
209
|
+
export function refuteFast(constraints, maxRows = 2_000) {
|
|
210
|
+
const m = constraints.length;
|
|
211
|
+
if (m === 0)
|
|
212
|
+
return undefined;
|
|
213
|
+
const names = [...new Set(constraints.flatMap(c => vars(c.lin)))], n = names.length;
|
|
214
|
+
if (m > maxRows || n > maxRows)
|
|
215
|
+
return undefined;
|
|
216
|
+
// Tableau rows: one per variable (Σ λ_i a_ij = 0) and the normalization (Σ λ_i c_i = 1); columns: the m λs, then one
|
|
217
|
+
// artificial per row, then the right-hand side.
|
|
218
|
+
const R = n + 1, cols = m + R;
|
|
219
|
+
const T = Array.from({ length: R }, () => new Float64Array(cols + 1));
|
|
220
|
+
const col = new Map(names.map((x, j) => [x, j]));
|
|
221
|
+
constraints.forEach((c, i) => {
|
|
222
|
+
for (const [key, a] of Object.entries(c.lin)) {
|
|
223
|
+
if (key === '1')
|
|
224
|
+
T[n][i] = a;
|
|
225
|
+
else
|
|
226
|
+
T[col.get(key)][i] = a;
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
T[n][cols] = 1;
|
|
230
|
+
for (let r = 0; r < R; r++)
|
|
231
|
+
T[r][m + r] = 1;
|
|
232
|
+
const basis = Array.from({ length: R }, (_, r) => m + r);
|
|
233
|
+
// Phase one: minimize the artificials' sum, as the reduced costs of the objective row.
|
|
234
|
+
const z = new Float64Array(cols + 1);
|
|
235
|
+
for (let r = 0; r < R; r++)
|
|
236
|
+
for (let j = 0; j <= cols; j++)
|
|
237
|
+
if (j < m || j === cols)
|
|
238
|
+
z[j] -= T[r][j];
|
|
239
|
+
const eps = 1e-9;
|
|
240
|
+
for (let iter = 0; iter < 50 * (R + m); iter++) {
|
|
241
|
+
// Bland's rule: the first column with a negative reduced cost enters (no cycling on degenerate systems).
|
|
242
|
+
let enter = -1;
|
|
243
|
+
for (let j = 0; j < cols; j++)
|
|
244
|
+
if (z[j] < -eps) {
|
|
245
|
+
enter = j;
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
if (enter < 0)
|
|
249
|
+
break;
|
|
250
|
+
let leave = -1, best = Infinity;
|
|
251
|
+
for (let r = 0; r < R; r++) {
|
|
252
|
+
const a = T[r][enter];
|
|
253
|
+
if (a > eps) {
|
|
254
|
+
const ratio = T[r][cols] / a;
|
|
255
|
+
if (ratio < best - eps || (Math.abs(ratio - best) <= eps && basis[r] < basis[leave])) {
|
|
256
|
+
best = ratio;
|
|
257
|
+
leave = r;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (leave < 0)
|
|
262
|
+
return undefined; // unbounded: cannot happen in phase one; give up rather than guess
|
|
263
|
+
const p = T[leave], pv = p[enter];
|
|
264
|
+
for (let j = 0; j <= cols; j++)
|
|
265
|
+
p[j] /= pv;
|
|
266
|
+
for (let r = 0; r < R; r++)
|
|
267
|
+
if (r !== leave) {
|
|
268
|
+
const f = T[r][enter];
|
|
269
|
+
if (f !== 0) {
|
|
270
|
+
const row = T[r];
|
|
271
|
+
for (let j = 0; j <= cols; j++)
|
|
272
|
+
row[j] -= f * p[j];
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
const f = z[enter];
|
|
276
|
+
if (f !== 0)
|
|
277
|
+
for (let j = 0; j <= cols; j++)
|
|
278
|
+
z[j] -= f * p[j];
|
|
279
|
+
basis[leave] = enter;
|
|
280
|
+
}
|
|
281
|
+
if (-z[cols] > 1e-7)
|
|
282
|
+
return undefined; // the artificials can't all leave: no λ, so a rational solution exists
|
|
283
|
+
const support = [];
|
|
284
|
+
for (let r = 0; r < R; r++)
|
|
285
|
+
if (basis[r] < m && T[r][cols] > eps)
|
|
286
|
+
support.push(basis[r]);
|
|
287
|
+
if (!support.length)
|
|
288
|
+
return undefined;
|
|
289
|
+
const sub = solve(support.sort((a, b) => a - b).map(i => constraints[i]));
|
|
290
|
+
if (sub.kind !== 'refuted')
|
|
291
|
+
return undefined;
|
|
292
|
+
return { kind: 'refuted', steps: sub.steps.map(st => (st.kind === 'input' ? { ...st, index: support[st.index] } : st)) };
|
|
293
|
+
}
|
package/dist/kernel/kernel.d.ts
CHANGED
|
@@ -18,6 +18,7 @@ export type Path = Map<string, {
|
|
|
18
18
|
holds: boolean;
|
|
19
19
|
}>;
|
|
20
20
|
export declare const canon: (lin: Lin) => string;
|
|
21
|
+
export declare const canonFlip: (lin: Lin) => string;
|
|
21
22
|
export declare const atomOf: (lin: Lin, holds: boolean) => Constraint;
|
|
22
23
|
export declare function linear(e: E, path: Path): Lin | undefined;
|
|
23
24
|
export declare const termColumn: (e: E) => string;
|
|
@@ -27,6 +28,12 @@ export declare function termRows(name: string, path: Path): Lin[];
|
|
|
27
28
|
export declare function truth(c: C, path: Path): boolean | undefined;
|
|
28
29
|
export declare function facts(c: C, path: Path, positive?: boolean): Lin[];
|
|
29
30
|
export declare function leafConstraints(goal: Goal, path: Path): Constraint[];
|
|
31
|
+
export declare function leafConstraintsReusing(goal: Goal, path: Path): Constraint[];
|
|
32
|
+
type Seen<T> = {
|
|
33
|
+
reads: [string, boolean | undefined][];
|
|
34
|
+
out: T;
|
|
35
|
+
}[];
|
|
36
|
+
export declare function reusing<T>(seen: Seen<T>, path: Path, f: (path: Path) => T): T;
|
|
30
37
|
export declare function withProducts(rows: Constraint[], pairs: [number, number][]): Constraint[];
|
|
31
38
|
export declare function check(goal: Goal, tree: Tree, path?: Path): boolean;
|
|
32
39
|
export type Subst = {
|
|
@@ -46,7 +53,7 @@ export declare function historyMachine(m: Machine): {
|
|
|
46
53
|
machine: Machine;
|
|
47
54
|
goals: Goal[];
|
|
48
55
|
};
|
|
49
|
-
export declare function obligations(m: Machine, laws: Law[], instances?: Record<string, Instances
|
|
56
|
+
export declare function obligations(m: Machine, laws: Law[], instances?: Record<string, Instances>, only?: number): Goal[];
|
|
50
57
|
export declare function checkLaws(m: Machine, laws: Law[], proof: {
|
|
51
58
|
instances: Record<string, Instances>;
|
|
52
59
|
trees: Record<string, Tree>;
|
|
@@ -122,3 +129,4 @@ export type Fold = {
|
|
|
122
129
|
assume?: C;
|
|
123
130
|
};
|
|
124
131
|
export declare function foldObligations(f: Fold, laws: Law[]): Goal[];
|
|
132
|
+
export {};
|
package/dist/kernel/kernel.js
CHANGED
|
@@ -22,25 +22,99 @@ import { add, k, scale, sub, v } from "../core/solver.js";
|
|
|
22
22
|
// An exhaustive switch's default: TypeScript proves it unreachable, and if it is reached anyway (a value from outside
|
|
23
23
|
// the types), it fails loudly instead of returning undefined.
|
|
24
24
|
export function unreachable(value) { throw new Error(`unhandled case: ${JSON.stringify(value)}`); }
|
|
25
|
-
|
|
25
|
+
// (Remembered per linear form: a form is never changed once built, and the same forms are looked up at every node.)
|
|
26
|
+
const canonMemo = new WeakMap(), flipMemo = new WeakMap();
|
|
27
|
+
export const canon = (lin) => {
|
|
28
|
+
let out = canonMemo.get(lin);
|
|
29
|
+
if (out === undefined)
|
|
30
|
+
canonMemo.set(lin, (out = Object.keys(lin).sort().map(key => `${key}:${lin[key]}`).join(',')));
|
|
31
|
+
return out;
|
|
32
|
+
};
|
|
33
|
+
// canon(1 − lin), the atom that decides lin <= 0 the other way, without building it.
|
|
34
|
+
export const canonFlip = (lin) => {
|
|
35
|
+
let out = flipMemo.get(lin);
|
|
36
|
+
if (out !== undefined)
|
|
37
|
+
return out;
|
|
38
|
+
const keys = Object.keys(lin);
|
|
39
|
+
if (!('1' in lin))
|
|
40
|
+
keys.push('1');
|
|
41
|
+
const parts = [];
|
|
42
|
+
for (const key of keys.sort()) {
|
|
43
|
+
const c = key === '1' ? 1 - (lin['1'] ?? 0) : -lin[key];
|
|
44
|
+
if (c !== 0)
|
|
45
|
+
parts.push(`${key}:${c}`);
|
|
46
|
+
}
|
|
47
|
+
flipMemo.set(lin, (out = parts.join(',')));
|
|
48
|
+
return out;
|
|
49
|
+
};
|
|
26
50
|
export const atomOf = (lin, holds) => ({ lin: holds ? lin : add(k(1), scale(lin, -1)), label: '' });
|
|
27
51
|
// Is lin <= 0 decided by the path? A constant atom decides itself.
|
|
28
52
|
// Over the integers lin <= 0 fails exactly when 1 - lin <= 0 holds, so either atom decides the other.
|
|
29
53
|
function decide(lin, path) {
|
|
30
|
-
|
|
54
|
+
let constant = true;
|
|
55
|
+
for (const key in lin)
|
|
56
|
+
if (key !== '1') {
|
|
57
|
+
constant = false;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
if (constant)
|
|
31
61
|
return (lin['1'] ?? 0) <= 0;
|
|
32
|
-
const direct = path
|
|
62
|
+
const direct = look(path, canon(lin));
|
|
33
63
|
if (direct !== undefined)
|
|
34
64
|
return direct;
|
|
35
|
-
const flipped = path
|
|
65
|
+
const flipped = look(path, canonFlip(lin));
|
|
36
66
|
return flipped === undefined ? undefined : !flipped;
|
|
37
67
|
}
|
|
68
|
+
// Every decision a hypothesis' facts read, found or not, while they are recorded (leafConstraintsReusing).
|
|
69
|
+
let recording;
|
|
70
|
+
const look = (path, key) => { const out = path.get(key)?.holds; if (recording)
|
|
71
|
+
recording.set(key, out); return out; };
|
|
38
72
|
// A term's linear form under the path. Reads become cells, named by map and key: `M[key]`; a choice the path leaves
|
|
39
73
|
// open (∸, min, max, if) becomes a column of its own, `⟦n⟧`, standing for the term's value (see termRows).
|
|
40
74
|
// Remembered per path and term (a path is never changed once built; a new decision is a new path), so a term shared
|
|
41
75
|
// across hypotheses, or nested in a k-step run's later states, is read once per path.
|
|
76
|
+
// A term without a choice (no ∸, min, max or if inside, its keys' too) reads alike on every path: remembered once.
|
|
42
77
|
const linearMemo = new WeakMap();
|
|
78
|
+
const freeMemo = new WeakMap(), freeLinear = new WeakMap();
|
|
79
|
+
const choiceFree = (e) => {
|
|
80
|
+
let out = freeMemo.get(e);
|
|
81
|
+
if (out !== undefined)
|
|
82
|
+
return out;
|
|
83
|
+
switch (e.op) {
|
|
84
|
+
case 'var':
|
|
85
|
+
case 'const':
|
|
86
|
+
out = true;
|
|
87
|
+
break;
|
|
88
|
+
case 'mul':
|
|
89
|
+
out = choiceFree(e.a);
|
|
90
|
+
break;
|
|
91
|
+
case 'get':
|
|
92
|
+
out = choiceFree(e.key);
|
|
93
|
+
break;
|
|
94
|
+
case 'add':
|
|
95
|
+
case 'prod':
|
|
96
|
+
out = choiceFree(e.a) && choiceFree(e.b);
|
|
97
|
+
break;
|
|
98
|
+
case 'sub':
|
|
99
|
+
case 'min':
|
|
100
|
+
case 'max':
|
|
101
|
+
case 'if':
|
|
102
|
+
out = false;
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
freeMemo.set(e, out);
|
|
106
|
+
return out;
|
|
107
|
+
};
|
|
43
108
|
export function linear(e, path) {
|
|
109
|
+
if (choiceFree(e)) {
|
|
110
|
+
if (freeLinear.has(e))
|
|
111
|
+
return freeLinear.get(e);
|
|
112
|
+
const out = linearOf(e, path);
|
|
113
|
+
freeLinear.set(e, out);
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
if (reuse)
|
|
117
|
+
return reusing(seenOf(linearSeen, e), path, p => linearOf(e, p));
|
|
44
118
|
let memo = linearMemo.get(path);
|
|
45
119
|
if (!memo)
|
|
46
120
|
linearMemo.set(path, (memo = new WeakMap()));
|
|
@@ -158,8 +232,28 @@ function times(a, b) {
|
|
|
158
232
|
}
|
|
159
233
|
return add(...out);
|
|
160
234
|
}
|
|
161
|
-
|
|
235
|
+
const comparisonMemo = new WeakMap(), freeComparison = new WeakMap();
|
|
162
236
|
function comparison(c, path) {
|
|
237
|
+
const free = choiceFree(c.a) && choiceFree(c.b);
|
|
238
|
+
if (free && freeComparison.has(c))
|
|
239
|
+
return freeComparison.get(c);
|
|
240
|
+
if (!free && reuse)
|
|
241
|
+
return reusing(seenOf(comparisonSeen, c), path, p => comparisonOf(c, p));
|
|
242
|
+
let memo = comparisonMemo.get(path);
|
|
243
|
+
if (!free) {
|
|
244
|
+
if (!memo)
|
|
245
|
+
comparisonMemo.set(path, (memo = new WeakMap()));
|
|
246
|
+
if (memo.has(c))
|
|
247
|
+
return memo.get(c);
|
|
248
|
+
}
|
|
249
|
+
const out = comparisonOf(c, path);
|
|
250
|
+
if (free)
|
|
251
|
+
freeComparison.set(c, out);
|
|
252
|
+
else
|
|
253
|
+
memo.set(c, out);
|
|
254
|
+
return out;
|
|
255
|
+
}
|
|
256
|
+
function comparisonOf(c, path) {
|
|
163
257
|
const a = linear(c.a, path), b = linear(c.b, path);
|
|
164
258
|
if (!a || !b)
|
|
165
259
|
return undefined;
|
|
@@ -170,8 +264,21 @@ function comparison(c, path) {
|
|
|
170
264
|
return { when: [add(ab, k(1))], unless: [[ba]] };
|
|
171
265
|
return { when: [ab, ba], unless: [[add(ab, k(1))], [add(ba, k(1))]] };
|
|
172
266
|
}
|
|
173
|
-
// Kleene truth under the path: true, false, or undefined when open.
|
|
267
|
+
// Kleene truth under the path: true, false, or undefined when open (remembered per path and condition).
|
|
268
|
+
const truthMemo = new WeakMap();
|
|
174
269
|
export function truth(c, path) {
|
|
270
|
+
if (reuse)
|
|
271
|
+
return reusing(seenOf(truthSeen, c), path, p => truthOf(c, p));
|
|
272
|
+
let memo = truthMemo.get(path);
|
|
273
|
+
if (!memo)
|
|
274
|
+
truthMemo.set(path, (memo = new WeakMap()));
|
|
275
|
+
if (memo.has(c))
|
|
276
|
+
return memo.get(c);
|
|
277
|
+
const out = truthOf(c, path);
|
|
278
|
+
memo.set(c, out);
|
|
279
|
+
return out;
|
|
280
|
+
}
|
|
281
|
+
function truthOf(c, path) {
|
|
175
282
|
switch (c.op) {
|
|
176
283
|
case 'flag': throw new Error('flags are inlined before the kernel sees them');
|
|
177
284
|
case 'all': return undefined; // closed before a goal is checked; opaque here, so it gives a leaf nothing
|
|
@@ -224,10 +331,57 @@ export function facts(c, path, positive = true) {
|
|
|
224
331
|
}
|
|
225
332
|
}
|
|
226
333
|
// What a leaf's derivation must refute.
|
|
227
|
-
export function leafConstraints(goal, path) {
|
|
334
|
+
export function leafConstraints(goal, path) { return leafRows(goal, path, h => facts(h, path)); }
|
|
335
|
+
const factsSeen = new WeakMap();
|
|
336
|
+
export function leafConstraintsReusing(goal, path) {
|
|
337
|
+
let byHyp = factsSeen.get(goal);
|
|
338
|
+
if (!byHyp)
|
|
339
|
+
factsSeen.set(goal, (byHyp = new Map()));
|
|
340
|
+
return leafRows(goal, path, h => {
|
|
341
|
+
let seen = byHyp.get(h);
|
|
342
|
+
if (!seen)
|
|
343
|
+
byHyp.set(h, (seen = []));
|
|
344
|
+
return reusing(seen, path, p => facts(h, p));
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
export function reusing(seen, path, f) {
|
|
348
|
+
const hit = seen.find(e => e.reads.every(([key, holds]) => path.get(key)?.holds === holds));
|
|
349
|
+
if (hit) {
|
|
350
|
+
if (recording)
|
|
351
|
+
for (const [key, holds] of hit.reads)
|
|
352
|
+
recording.set(key, holds);
|
|
353
|
+
return hit.out;
|
|
354
|
+
}
|
|
355
|
+
const outer = recording, mode = reuse;
|
|
356
|
+
recording = new Map();
|
|
357
|
+
reuse = true;
|
|
358
|
+
try {
|
|
359
|
+
const out = f(new Map(path));
|
|
360
|
+
seen.unshift({ reads: [...recording], out });
|
|
361
|
+
if (seen.length > 16)
|
|
362
|
+
seen.pop();
|
|
363
|
+
return out;
|
|
364
|
+
}
|
|
365
|
+
finally {
|
|
366
|
+
// What was read inside is read by the outer value too.
|
|
367
|
+
if (outer)
|
|
368
|
+
for (const [key, holds] of recording)
|
|
369
|
+
outer.set(key, holds);
|
|
370
|
+
recording = outer;
|
|
371
|
+
reuse = mode;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
// While the prover reuses (inside `reusing`), a term's linear form, a comparison and a condition's truth are reused the
|
|
375
|
+
// same way, each by the decisions it reads: only what a new decision touches is read again. The kernel's check never
|
|
376
|
+
// runs in this mode: it reads every term afresh on every path.
|
|
377
|
+
let reuse = false;
|
|
378
|
+
const linearSeen = new WeakMap(), comparisonSeen = new WeakMap(), truthSeen = new WeakMap();
|
|
379
|
+
const seenOf = (m, k) => { let s = m.get(k); if (!s)
|
|
380
|
+
m.set(k, (s = [])); return s; };
|
|
381
|
+
function leafRows(goal, path, factsOf) {
|
|
228
382
|
const rows = [...path.values()].map(a => atomOf(a.lin, a.holds));
|
|
229
383
|
for (const h of goal.hyps)
|
|
230
|
-
for (const lin of
|
|
384
|
+
for (const lin of factsOf(h))
|
|
231
385
|
rows.push({ lin, label: '' });
|
|
232
386
|
const names = new Set(rows.flatMap(r => Object.keys(r.lin).filter(key => key !== '1')));
|
|
233
387
|
// Two reads of one map whose keys the path proves equal are equal.
|
|
@@ -755,10 +909,11 @@ export function historyMachine(m) {
|
|
|
755
909
|
}
|
|
756
910
|
return { machine, goals };
|
|
757
911
|
}
|
|
758
|
-
|
|
912
|
+
// `only`: the goals of that law alone (the prover's instance search asks for one law's at a time).
|
|
913
|
+
export function obligations(m, laws, instances = {}, only) {
|
|
759
914
|
if (m.history) {
|
|
760
915
|
const { machine, goals } = historyMachine(m);
|
|
761
|
-
return [...obligations(machine, laws, instances), ...goals];
|
|
916
|
+
return [...obligations(machine, laws, instances, only), ...(only === undefined ? goals : [])];
|
|
762
917
|
}
|
|
763
918
|
scope(m, laws);
|
|
764
919
|
const ok = [...Object.values(m.state), ...Object.values(m.step.state ?? {}), ...Object.values(m.step.maps ?? {}).flatMap(w => ('each' in w ? [w.value] : [w.key, w.value]))].every(natural)
|
|
@@ -770,7 +925,9 @@ export function obligations(m, laws, instances = {}) {
|
|
|
770
925
|
// value of it too (the Lean twin reads it so).
|
|
771
926
|
const proven = (law) => (law.forall ? substC(law.holds, { vars: { [law.forall]: { op: 'var', name: `${law.forall}#` } }, maps: {}, flags: {} }) : law.holds);
|
|
772
927
|
const pre = preOf(m), u = updateOf(m), assume = m.assume ? [inlineFlags(m, m.assume)] : [];
|
|
773
|
-
return laws.flatMap(law => {
|
|
928
|
+
return laws.flatMap((law, i) => {
|
|
929
|
+
if (only !== undefined && i !== only)
|
|
930
|
+
return [];
|
|
774
931
|
// At the start: the precondition, and no request. One step: from a state where the laws hold, a request allowed.
|
|
775
932
|
// The facts hold throughout, read where the run begins (they read no state).
|
|
776
933
|
const start = given(m, holds(from(startOf(m)), pre), instances[law.name] ?? {});
|
package/dist/kernel/prove.d.ts
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
import type { C, E, Law, Machine } from '../core/machine.js';
|
|
2
2
|
import { type Goal, type Instances, type Liveness, type Path, type StepLaw, type Tree } from './kernel.js';
|
|
3
|
+
export declare const profile: {
|
|
4
|
+
on: boolean;
|
|
5
|
+
goals: {
|
|
6
|
+
name: string;
|
|
7
|
+
ms: number;
|
|
8
|
+
nodes: number;
|
|
9
|
+
solves: number;
|
|
10
|
+
solveMs: number;
|
|
11
|
+
rowsMs: number;
|
|
12
|
+
proven: boolean;
|
|
13
|
+
}[];
|
|
14
|
+
solves: number;
|
|
15
|
+
solveMs: number;
|
|
16
|
+
rowsMs: number;
|
|
17
|
+
nodes: number;
|
|
18
|
+
};
|
|
19
|
+
export declare const solveKinds: Record<string, {
|
|
20
|
+
n: number;
|
|
21
|
+
ms: number;
|
|
22
|
+
rows: number;
|
|
23
|
+
}>;
|
|
3
24
|
export type Outcome = {
|
|
4
25
|
tree: Tree;
|
|
5
26
|
nodes: number;
|
package/dist/kernel/prove.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { add, k, scale, solve, sub, v } from "../core/solver.js";
|
|
2
|
-
import { assumed, canon, columnTerm, decided, stepZone, check, checkLaws, checkStepLaws, historyMachine, leafConstraints, linear, livenessHelpful, livenessParts, obligations, plainKey, progressObligations, stepLawObligations, trueC, truth, unreachable, withProducts } from "./kernel.js";
|
|
1
|
+
import { add, k, refuteFast, scale, solve, sub, v } from "../core/solver.js";
|
|
2
|
+
import { assumed, canon, canonFlip, columnTerm, decided, stepZone, check, checkLaws, checkStepLaws, historyMachine, leafConstraints, leafConstraintsReusing, reusing, linear, livenessHelpful, livenessParts, obligations, plainKey, progressObligations, stepLawObligations, trueC, truth, unreachable, withProducts } from "./kernel.js";
|
|
3
3
|
// The atoms the path leaves open, in evaluation order: deciding any of them makes progress.
|
|
4
4
|
function openE(e, path, out) {
|
|
5
5
|
switch (e.op) {
|
|
@@ -38,17 +38,18 @@ function openC(c, path, out) {
|
|
|
38
38
|
return openC(c.x, path, out);
|
|
39
39
|
if (c.op === 'and' || c.op === 'or')
|
|
40
40
|
return c.xs.forEach(y => openC(y, path, out));
|
|
41
|
-
|
|
42
|
-
const a = linear(c.a, path), b = linear(c.b, path);
|
|
41
|
+
const a = linear(c.a, path), b = linear(c.b, path), own = [];
|
|
43
42
|
if (a && b) {
|
|
44
43
|
const d = sub(a, b);
|
|
45
44
|
if (c.op === '<=')
|
|
46
|
-
|
|
45
|
+
own.push(d);
|
|
47
46
|
else if (c.op === '<')
|
|
48
|
-
|
|
47
|
+
own.push(add(d, k(1)));
|
|
49
48
|
else
|
|
50
|
-
|
|
49
|
+
own.push(d, scale(d, -1));
|
|
51
50
|
}
|
|
51
|
+
openE(c.a, path, out), openE(c.b, path, out);
|
|
52
|
+
out.push(...own);
|
|
52
53
|
}
|
|
53
54
|
// A hypothesis is settled when it gives facts that are all its content: here, when its truth is decided, or it is a
|
|
54
55
|
// conjunction of settled parts / a linear comparison. A disjunction is settled only once one disjunct is left open and
|
|
@@ -85,7 +86,7 @@ function openCells(rows, path) {
|
|
|
85
86
|
if (p.slice(0, p.indexOf('[')) !== q.slice(0, q.indexOf('[')))
|
|
86
87
|
continue;
|
|
87
88
|
const d = sub(parse(keyText(p)), parse(keyText(q)));
|
|
88
|
-
const dec = (lin) => path.get(canon(lin))?.holds ?? (path.has(
|
|
89
|
+
const dec = (lin) => path.get(canon(lin))?.holds ?? (path.has(canonFlip(lin)) ? !path.get(canonFlip(lin)).holds : undefined);
|
|
89
90
|
const a = dec(d), b = dec(scale(d, -1));
|
|
90
91
|
if (a === undefined)
|
|
91
92
|
out.push(d);
|
|
@@ -113,9 +114,40 @@ function parse(text) {
|
|
|
113
114
|
}
|
|
114
115
|
return out;
|
|
115
116
|
}
|
|
117
|
+
// Where a goal's time goes, when asked (research/perf/prover-bench.ts): each root goal's time, nodes, solver calls and
|
|
118
|
+
// the time in the solver and in building rows. Off unless `profile.on`; the counters cost nothing then.
|
|
119
|
+
export const profile = { on: false, goals: [], solves: 0, solveMs: 0, rowsMs: 0, nodes: 0 };
|
|
120
|
+
const timed = (f, add) => { if (!profile.on)
|
|
121
|
+
return f(); const t = performance.now(); try {
|
|
122
|
+
return f();
|
|
123
|
+
}
|
|
124
|
+
finally {
|
|
125
|
+
add(performance.now() - t);
|
|
126
|
+
} };
|
|
127
|
+
export const solveKinds = {};
|
|
128
|
+
const solveP = (rows, maxRows) => {
|
|
129
|
+
if (!profile.on)
|
|
130
|
+
return solve(rows, maxRows);
|
|
131
|
+
const t = performance.now(), out = solve(rows, maxRows), ms = performance.now() - t;
|
|
132
|
+
profile.solves++;
|
|
133
|
+
profile.solveMs += ms;
|
|
134
|
+
const s = (solveKinds[out.kind] ??= { n: 0, ms: 0, rows: 0 });
|
|
135
|
+
s.n++;
|
|
136
|
+
s.ms += ms;
|
|
137
|
+
s.rows += rows.length;
|
|
138
|
+
return out;
|
|
139
|
+
};
|
|
116
140
|
// The budget: 200,000 nodes and, by default, a second per hypothesis (at least 20 seconds) per goal; past either the goal
|
|
117
141
|
// is left `unknown`. A goal with many instances (a law about two keys at its pairs) has that many more rows per leaf.
|
|
118
142
|
export function grow(goal, path = new Map(), budget = { nodes: 0, until: performance.now() + Math.max(20_000, 1_000 * goal.hyps.length) }) {
|
|
143
|
+
if (!profile.on || path.size || budget.nodes)
|
|
144
|
+
return growRoot(goal, path, budget);
|
|
145
|
+
const t = performance.now(), s0 = profile.solves, m0 = profile.solveMs, r0 = profile.rowsMs, n0 = profile.nodes;
|
|
146
|
+
const out = growRoot(goal, path, budget);
|
|
147
|
+
profile.goals.push({ name: goal.name, ms: performance.now() - t, nodes: profile.nodes - n0, solves: profile.solves - s0, solveMs: profile.solveMs - m0, rowsMs: profile.rowsMs - r0, proven: 'tree' in out });
|
|
148
|
+
return out;
|
|
149
|
+
}
|
|
150
|
+
function growRoot(goal, path, budget) {
|
|
119
151
|
// At the root: plainly first; where the cases multiply past a budget, through cuts. The plain budget halves with each
|
|
120
152
|
// two open choices (what a plain search's cases grow by), so a long run's steps reach the cuts early.
|
|
121
153
|
if (path.size === 0 && budget.nodes === 0) {
|
|
@@ -132,24 +164,47 @@ export function grow(goal, path = new Map(), budget = { nodes: 0, until: perform
|
|
|
132
164
|
function growPlain(goal, path, budget) {
|
|
133
165
|
if (budget.cap !== undefined && budget.nodes >= budget.cap)
|
|
134
166
|
return { stuck: 'unknown', path, why: 'cap' };
|
|
167
|
+
if (profile.on)
|
|
168
|
+
profile.nodes++;
|
|
135
169
|
if (++budget.nodes > 200_000 || (budget.until !== undefined && performance.now() > budget.until))
|
|
136
170
|
return { stuck: 'unknown', path, why: outOfBudget(goal, budget.nodes > 200_000 ? 'nodes' : 'time', path.size) };
|
|
137
171
|
// Rows that read a term column only once a cut has put one on the path; before, the rows a choice-free search uses
|
|
138
172
|
// (what it saw before term columns), renumbered to the whole leaf's when a leaf is found.
|
|
139
|
-
const full =
|
|
173
|
+
const full = timed(() => leafConstraintsReusing(goal, path), ms => { profile.rowsMs += ms; });
|
|
140
174
|
const columns = [...path.values()].some(a => Object.keys(a.lin).some(key => key.startsWith('⟦')));
|
|
141
175
|
const idx = full.flatMap((r, i) => (columns || !Object.keys(r.lin).some(key => key.startsWith('⟦')) ? [i] : []));
|
|
142
176
|
const rows = idx.map(i => full[i]), at = (i) => (i < rows.length ? idx[i] : full.length + i - rows.length);
|
|
143
177
|
const leafOf = (steps, products) => ({ leaf: steps.map(st => (st.kind === 'input' ? { ...st, index: at(st.index) } : st)), ...(products ? { products: products.map(([i, j]) => [at(i), at(j)]) } : {}) });
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
return { tree: leafOf(verdict.steps), nodes: budget.nodes };
|
|
178
|
+
// The conclusion's own atoms first (the goal's last hypothesis is the law refuted): a case it doesn't read is often
|
|
179
|
+
// one no refutation needs, and splitting it first multiplies every case after it.
|
|
147
180
|
const open = [];
|
|
148
|
-
|
|
149
|
-
|
|
181
|
+
const hyps = goal.hyps, last = hyps.length - 1;
|
|
182
|
+
let byHyp = openSeen.get(goal);
|
|
183
|
+
if (!byHyp)
|
|
184
|
+
openSeen.set(goal, (byHyp = new Map()));
|
|
185
|
+
const openOf = (h) => { let seen = byHyp.get(h); if (!seen)
|
|
186
|
+
byHyp.set(h, (seen = [])); return reusing(seen, path, p => { const out = []; openH(h, p, out); return out; }); };
|
|
187
|
+
if (last >= 0)
|
|
188
|
+
open.push(...openOf(hyps[last]));
|
|
189
|
+
for (const [i, h] of hyps.entries())
|
|
190
|
+
if (i !== last)
|
|
191
|
+
open.push(...openOf(h));
|
|
150
192
|
open.push(...openCells(rows, path));
|
|
151
|
-
|
|
152
|
-
|
|
193
|
+
const next = open.find(lin => Object.keys(lin).some(key => key !== '1') && !path.has(canon(lin)) && !path.has(canonFlip(lin)));
|
|
194
|
+
// With a case still to split, only "refuted or not" matters (a witness here goes unused): a large system is tried by
|
|
195
|
+
// its Farkas rows alone, and left to the split if they don't refute it. Elimination to the last variable, which a
|
|
196
|
+
// satisfiable system needs to say so, is kept for small systems and for the leaves, where its answer is the outcome.
|
|
197
|
+
// Past the Farkas rows, elimination still runs, bounded (a refutation the rationals miss is an integer one, which
|
|
198
|
+
// elimination's cuts find early; a satisfiable system grows past the bound and is left to the split).
|
|
199
|
+
if (next && rows.length > 60) {
|
|
200
|
+
const fast = timed(() => refuteFast(rows), ms => { profile.solves++; profile.solveMs += ms; });
|
|
201
|
+
if (fast?.kind === 'refuted')
|
|
202
|
+
return { tree: leafOf(fast.steps), nodes: budget.nodes };
|
|
203
|
+
}
|
|
204
|
+
const verdict = next && rows.length > 60 ? solveP(rows, 2 * rows.length) : solveP(rows);
|
|
205
|
+
if (verdict.kind === 'refuted')
|
|
206
|
+
return { tree: leafOf(verdict.steps), nodes: budget.nodes };
|
|
207
|
+
let split = next ?? (verdict.kind === 'unknown' ? bisect(rows, path) ?? fractional(verdict.point, path) : undefined);
|
|
153
208
|
// Products: with monomials about, try every pair of rows multiplied; failing that, split a factor at zero.
|
|
154
209
|
const monomials = rows.some(r => Object.keys(r.lin).some(key => key.includes('*')));
|
|
155
210
|
// A witness that multiplies out (each monomial's value is its factors' product) is a real model: nothing to refute.
|
|
@@ -184,7 +239,7 @@ function growPlain(goal, path, budget) {
|
|
|
184
239
|
return { tree: leafOf(third.steps, [...sq, ...cubes]), nodes: budget.nodes };
|
|
185
240
|
}
|
|
186
241
|
const factors = [...new Set(rows.flatMap(r => Object.keys(r.lin).filter(key => key.includes('*')).flatMap(key => key.split('*'))))];
|
|
187
|
-
const fresh = (lin) => !path.has(canon(lin)) && !path.has(
|
|
242
|
+
const fresh = (lin) => !path.has(canon(lin)) && !path.has(canonFlip(lin));
|
|
188
243
|
// Split a factor at zero, else two factors against each other (so (x - y)² >= 0 becomes a product of rows).
|
|
189
244
|
const zero = factors.find(f => fresh({ [f]: 1 }));
|
|
190
245
|
const apart = factors.flatMap((f, i) => factors.slice(i + 1).map(g => ({ [f]: 1, [g]: -1 }))).find(fresh);
|
|
@@ -199,11 +254,62 @@ function growPlain(goal, path, budget) {
|
|
|
199
254
|
const yes = growPlain(goal, new Map(path).set(id, { lin: split, holds: true }), budget);
|
|
200
255
|
if (!('tree' in yes))
|
|
201
256
|
return yes;
|
|
257
|
+
// A refutation of the yes side that never reads the split refutes the case as it stands: the other side is skipped.
|
|
258
|
+
const lifted = lift(goal, yes.tree, new Map(path).set(id, { lin: split, holds: true }), path);
|
|
259
|
+
if (lifted)
|
|
260
|
+
return { tree: lifted, nodes: budget.nodes };
|
|
202
261
|
const no = growPlain(goal, new Map(path).set(id, { lin: split, holds: false }), budget);
|
|
203
262
|
if (!('tree' in no))
|
|
204
263
|
return no;
|
|
205
264
|
return { tree: { split, yes: yes.tree, no: no.tree }, nodes: budget.nodes };
|
|
206
265
|
}
|
|
266
|
+
// A tree found under one more decision (`from`, the path with it) as a tree under the path without it (`to`), if no
|
|
267
|
+
// leaf reads the decision: each leaf's rows found again among the rows the shorter path gives (the same rows, renumbered:
|
|
268
|
+
// the decision's own row, and any row only it makes, are not there), every split kept. The derivations are the same
|
|
269
|
+
// arithmetic over the same rows, and the kernel checks the whole tree in the end. Undefined at the first leaf that
|
|
270
|
+
// needs the decision.
|
|
271
|
+
function lift(goal, tree, from, to) {
|
|
272
|
+
const own = to.size; // the decision's row: the last of `from`'s path rows, below every later split's
|
|
273
|
+
const reads = (t) => ('leaf' in t ? t.leaf.some(st => st.kind === 'input' && st.index === own) || (t.products ?? []).some(([i, j]) => i === own || j === own) : reads(t.yes) || reads(t.no));
|
|
274
|
+
if (reads(tree))
|
|
275
|
+
return undefined;
|
|
276
|
+
const go = (t, a, b) => {
|
|
277
|
+
if (!('leaf' in t)) {
|
|
278
|
+
const sid = canon(t.split);
|
|
279
|
+
if (b.has(sid) || b.has(canonFlip(t.split)))
|
|
280
|
+
return undefined;
|
|
281
|
+
const yes = go(t.yes, new Map(a).set(sid, { lin: t.split, holds: true }), new Map(b).set(sid, { lin: t.split, holds: true }));
|
|
282
|
+
const no = yes && go(t.no, new Map(a).set(sid, { lin: t.split, holds: false }), new Map(b).set(sid, { lin: t.split, holds: false }));
|
|
283
|
+
return yes && no ? { split: t.split, yes, no } : undefined;
|
|
284
|
+
}
|
|
285
|
+
const ra = leafConstraintsReusing(goal, a), rb = leafConstraintsReusing(goal, b), at = new Map();
|
|
286
|
+
rb.forEach((r, i) => { const key = canon(r.lin); if (!at.has(key))
|
|
287
|
+
at.set(key, i); });
|
|
288
|
+
const map = (i) => (i < ra.length ? at.get(canon(ra[i].lin)) : undefined);
|
|
289
|
+
const leaf = [];
|
|
290
|
+
for (const st of t.leaf) {
|
|
291
|
+
if (st.kind !== 'input') {
|
|
292
|
+
leaf.push(st);
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
const j = map(st.index);
|
|
296
|
+
if (j === undefined)
|
|
297
|
+
return undefined;
|
|
298
|
+
leaf.push({ ...st, index: j });
|
|
299
|
+
}
|
|
300
|
+
const products = [];
|
|
301
|
+
for (const [i, j] of t.products ?? []) {
|
|
302
|
+
const pi = map(i), pj = map(j);
|
|
303
|
+
if (pi === undefined || pj === undefined)
|
|
304
|
+
return undefined;
|
|
305
|
+
products.push([pi, pj]);
|
|
306
|
+
}
|
|
307
|
+
return { leaf, ...(t.products ? { products } : {}) };
|
|
308
|
+
};
|
|
309
|
+
return go(tree, from, to);
|
|
310
|
+
}
|
|
311
|
+
// Each hypothesis' open atoms, as at an earlier path that decided the same atoms it reads (kernel.ts's reusing).
|
|
312
|
+
const openSeen = new WeakMap();
|
|
207
313
|
// What ran out, for a goal left unknown: its size, how deep the case splits went, and the two shapes that usually cause it.
|
|
208
314
|
function outOfBudget(goal, limit, depth) {
|
|
209
315
|
const steps = Number(/@(\d+) steps/.exec(goal.name)?.[1] ?? 1);
|
|
@@ -259,7 +365,7 @@ function topAtoms(c, path) {
|
|
|
259
365
|
if (!a || !b)
|
|
260
366
|
return [];
|
|
261
367
|
const atoms = c.op === '<=' ? [sub(a, b)] : c.op === '<' ? [add(sub(a, b), k(1))] : [sub(a, b), sub(b, a)];
|
|
262
|
-
return atoms.filter(l => Object.keys(l).some(key => key !== '1') && !path.has(canon(l)) && !path.has(
|
|
368
|
+
return atoms.filter(l => Object.keys(l).some(key => key !== '1') && !path.has(canon(l)) && !path.has(canonFlip(l)));
|
|
263
369
|
}
|
|
264
370
|
// A refutation that splits only on these atoms (a cut's failing side), within a small budget.
|
|
265
371
|
function growFocused(goal, path, focus, budget, near = focus) {
|
|
@@ -268,7 +374,7 @@ function growFocused(goal, path, focus, budget, near = focus) {
|
|
|
268
374
|
const verdict = solveNear(leafConstraints(goal, path), [...near, ...[...path.values()].slice(-1).map(a => a.lin)]);
|
|
269
375
|
if (verdict.kind === 'refuted')
|
|
270
376
|
return { tree: { leaf: verdict.steps }, nodes: budget.nodes };
|
|
271
|
-
const split = focus.find(l => !path.has(canon(l)) && !path.has(
|
|
377
|
+
const split = focus.find(l => !path.has(canon(l)) && !path.has(canonFlip(l)));
|
|
272
378
|
if (!split)
|
|
273
379
|
return { stuck: verdict.kind === 'sat' ? 'sat' : 'unknown', path };
|
|
274
380
|
const id = canon(split);
|
|
@@ -319,7 +425,7 @@ function bisect(rows, path) {
|
|
|
319
425
|
const mid = Math.floor((lo + hi) / 2);
|
|
320
426
|
for (const c of lo === hi ? [lo, lo - 1] : [mid]) {
|
|
321
427
|
const lin = c === 0 ? { [x]: 1 } : { [x]: 1, 1: -c };
|
|
322
|
-
if (!path.has(canon(lin)) && !path.has(
|
|
428
|
+
if (!path.has(canon(lin)) && !path.has(canonFlip(lin)))
|
|
323
429
|
return lin;
|
|
324
430
|
}
|
|
325
431
|
}
|
|
@@ -331,7 +437,7 @@ function fractional(point, path) {
|
|
|
331
437
|
if (Math.abs(value - Math.round(value)) < 1e-9 || x.includes('*'))
|
|
332
438
|
continue;
|
|
333
439
|
const c = Math.floor(value), lin = c === 0 ? { [x]: 1 } : { [x]: 1, 1: -c };
|
|
334
|
-
if (!path.has(canon(lin)) && !path.has(
|
|
440
|
+
if (!path.has(canon(lin)) && !path.has(canonFlip(lin)))
|
|
335
441
|
return lin;
|
|
336
442
|
}
|
|
337
443
|
return undefined;
|
|
@@ -616,7 +722,7 @@ function proveLawsAsGiven(m, laws) {
|
|
|
616
722
|
// Each law's two goals (start and step) get their own instances, seeded with its own keys.
|
|
617
723
|
const keys = [...readKeys(m, laws), ...witnesses(m)], instances = {};
|
|
618
724
|
laws.forEach((law, i) => {
|
|
619
|
-
instances[law.name] = instancesFor(m, laws, inst => obligations(m, laws, { ...instances, [law.name]: inst }
|
|
725
|
+
instances[law.name] = instancesFor(m, laws, inst => obligations(m, laws, { ...instances, [law.name]: inst }, i), {
|
|
620
726
|
seed: [...keys, ...(law.forall ? [{ op: 'var', name: `${law.forall}#` }] : []), ...(law.keys ?? []).map((name) => ({ op: 'var', name }))],
|
|
621
727
|
focus: [...(law.forall ? [`${law.forall}#`] : []), ...(law.keys ?? [])],
|
|
622
728
|
});
|
package/dist/lean-kernel/emit.js
CHANGED
|
@@ -105,12 +105,19 @@ export function encode(ex, tamper = false, dropStep = true, globalVars, globalMa
|
|
|
105
105
|
else if (key.includes('*'))
|
|
106
106
|
monos.add(key);
|
|
107
107
|
}
|
|
108
|
+
// Each cell a hypothesis reads under the path, kept or not: two reads of one cell cancel in a row (T − T), yet
|
|
109
|
+
// Lean's linear form of each still needs the cell's column to see the comparison decided.
|
|
108
110
|
for (const h of goal.hyps)
|
|
109
|
-
for (const e of subterms(h))
|
|
111
|
+
for (const e of subterms(h)) {
|
|
110
112
|
if (e.op === 'prod')
|
|
111
113
|
for (const key of Object.keys(linear(e, path) ?? {}))
|
|
112
114
|
if (key.includes('*'))
|
|
113
115
|
monos.add(key);
|
|
116
|
+
if (e.op === 'get')
|
|
117
|
+
for (const key of Object.keys(linear(e, path) ?? {}))
|
|
118
|
+
if (key.includes('['))
|
|
119
|
+
addCell(key);
|
|
120
|
+
}
|
|
114
121
|
});
|
|
115
122
|
const depth = (name) => name.split('[').length;
|
|
116
123
|
// Term columns (open choices, each standing for its term's value) before the cells, whose keys may read them.
|
|
@@ -1055,6 +1062,10 @@ export function toLean(items, opts = {}) {
|
|
|
1055
1062
|
parts.push(`-- ${item.id}\n${loopToLean(id, item.loop, item.laws ?? [], item.rank)}`);
|
|
1056
1063
|
theorems.push(`${id}_halts`);
|
|
1057
1064
|
}
|
|
1065
|
+
else if ('fold' in item) {
|
|
1066
|
+
parts.push(`-- ${item.id}\n${foldToLean(id, item.fold, item.laws)}`);
|
|
1067
|
+
theorems.push(`${id}_laws`);
|
|
1068
|
+
}
|
|
1058
1069
|
else {
|
|
1059
1070
|
// goalToLean names its theorem t<i>; here it is the item's id.
|
|
1060
1071
|
const n = goals++;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdp-wired",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"description": "An automatic prover for stateful rules (budgets, caps, windows, workflows, policies): laws proven with no proof to write, checked again in Lean, counterexamples found on the GPU, the rule shipped as code",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"verification",
|
|
@@ -89,7 +89,7 @@
|
|
|
89
89
|
"build": "bun scripts/build.ts",
|
|
90
90
|
"prepublishOnly": "bun scripts/build.ts",
|
|
91
91
|
"smoke": "sh scripts/smoke.sh",
|
|
92
|
-
"test": "bun scripts/gen-lean-library.ts --check && cd core && bun test solver.test.ts dsl.test.ts conformance.test.ts compose.test.ts quantifiers.test.ts functions.test.ts keyterms.test.ts lists.test.ts liveness.test.ts sizes.test.ts values.test.ts builders.test.ts gdp.test.ts datatypes.test.ts lemmas.test.ts && cd ../kernel && bun test tamper.test.ts refusals.test.ts steplaw.test.ts sets.test.ts && cd ../examples && bun test examples.test.ts && cd .. && bun examples/readme.ts && bun examples/composition.ts && bun examples/sets.ts && bun examples/quantifiers.ts && bun examples/functions.ts && bun examples/datatypes.ts && bun examples/lemmas.ts && bun examples/lists.ts && bun examples/sizes.ts && bun examples/liveness.ts && bun examples/budget.ts && bun examples/gdp.ts && bun examples/steps.ts && bun examples/jev-fabric/run.ts"
|
|
92
|
+
"test": "bun scripts/gen-lean-library.ts --check && cd core && bun test solver.test.ts dsl.test.ts conformance.test.ts compose.test.ts quantifiers.test.ts functions.test.ts keyterms.test.ts lists.test.ts liveness.test.ts sizes.test.ts values.test.ts builders.test.ts gdp.test.ts datatypes.test.ts lemmas.test.ts invoices.test.ts folds.test.ts && cd ../kernel && bun test tamper.test.ts refusals.test.ts steplaw.test.ts sets.test.ts && cd ../examples && bun test examples.test.ts && cd .. && bun examples/readme.ts && bun examples/composition.ts && bun examples/sets.ts && bun examples/quantifiers.ts && bun examples/functions.ts && bun examples/datatypes.ts && bun examples/lemmas.ts && bun examples/invoices.ts && bun examples/folds.ts && bun examples/lists.ts && bun examples/sizes.ts && bun examples/liveness.ts && bun examples/budget.ts && bun examples/gdp.ts && bun examples/steps.ts && bun examples/jev-fabric/run.ts"
|
|
93
93
|
},
|
|
94
94
|
"devDependencies": {
|
|
95
95
|
"@gdp-ts/core": "0.1.0",
|