gdp-wired 0.15.0 → 0.17.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 +12 -0
- package/dist/core/chart.js +4 -4
- package/dist/core/cyclic.d.ts +51 -0
- package/dist/core/cyclic.js +295 -0
- package/dist/core/discover.d.ts +1 -1
- package/dist/core/discover.js +10 -5
- package/dist/core/dsl.d.ts +49 -4
- package/dist/core/dsl.js +143 -17
- package/dist/core/infer.js +123 -11
- package/dist/kernel/cyclic.d.ts +45 -0
- package/dist/kernel/cyclic.js +54 -0
- package/dist/kernel/prove.d.ts +10 -0
- package/dist/kernel/prove.js +348 -2
- package/dist/lean-kernel/emit.d.ts +13 -0
- package/dist/lean-kernel/emit.js +50 -2
- package/dist/lean-kernel/library.js +1 -0
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -69,6 +69,10 @@ All of these are proven automatically, and each proof is also a Lean theorem:
|
|
|
69
69
|
machine runs as, with each guard and action it names given again in gdp-wired's terms, and `conforms(chart, machine,
|
|
70
70
|
createActor)` drives XState's own actor and the compiled rule with the same random events, comparing every state and
|
|
71
71
|
context; what a chart can't run the same way is refused when built, with the reason (`examples/xstate.ts`).
|
|
72
|
+
- **What nobody stated, looked for when the prover alone stops**: where `proveRule` or `proveSteps` doesn't prove the laws
|
|
73
|
+
and no run breaks them, it looks for lemmas about the rule's recurrences and lists (`discover`) and then invariants
|
|
74
|
+
(`inferLaws`), within 10 s (`{search: {ms}}`; `{search: false}`, the prover alone); the verdict's `found` holds what
|
|
75
|
+
proved them, as laws and lemmas with the source to paste.
|
|
72
76
|
- **Invariants nobody wrote** (`gdp-wired/infer`): `inferLaws(rule, laws)` proves a law that holds but isn't inductive
|
|
73
77
|
alone, with the laws it finds (bounds, sums and differences over the state, the same where a flag or a choice holds
|
|
74
78
|
one value, and laws about every key of a map), each proven by the kernel with yours and handed back as the
|
|
@@ -116,6 +120,7 @@ sound.
|
|
|
116
120
|
| Part | Trusted? | What it does |
|
|
117
121
|
| --- | --- | --- |
|
|
118
122
|
| `kernel/kernel.ts`, `core/check.ts` | **Yes** | Turns a rule and its laws into goals, and checks a proof tree for each: case splits on linear facts, then a linear refutation at each leaf |
|
|
123
|
+
| `kernel/cyclic.ts` | **Yes** | Cyclic proofs: builds each node's goals (its own, and for each node it uses, that node's hypotheses and that the rank drops) and checks each tree with kernel.ts's check. Its twin is `lean-kernel/Cyclic.lean` (`cyclic_sound`) |
|
|
119
124
|
| `kernel/prove.ts` | No | Finds the proof trees. If it's wrong, the kernel rejects the tree |
|
|
120
125
|
| `lean-kernel/` | Optional | The kernel's twin in Lean 4.34.0, every rule proven sound there with the standard axioms only. The exporter sends the kernel's real trees; tampered ones compute to false |
|
|
121
126
|
| `research/search/`, `research/synth/`, `research/hw/` | No | The GPU searches for breaking runs, and invariant and rank synthesis. Every hit is replayed or proven before it counts |
|
|
@@ -246,6 +251,13 @@ const soon = retry.law('the next try is at most 64 after the last request', ({ne
|
|
|
246
251
|
`Acct.set(k, value)`, `Acct.each(k => value)`) and `.items({Queue: Job})` (`push`, `pop`, `set`, `insertAt`,
|
|
247
252
|
`removeAt`, `apply`, `has`, `within`, `increasing(by)`), one map per variable of the kind; `rule.decode` reads them back
|
|
248
253
|
as objects and arrays.
|
|
254
|
+
- **Sequences a request carries** (`.carriesSeq('Path')`, and tables of them, `.carriesSeqs('Paths')`): a sequence of
|
|
255
|
+
naturals as positions and a length, `Path.at(i)`, `Path.len`, a table's row `Paths.row(r)`; `A.prefixOf(B)`,
|
|
256
|
+
`startsWith`, `equals`, `extends(B, k)`, `has(x)`, each a condition about every position below a length, so laws about
|
|
257
|
+
prefixes of paths need no induction or lemma. The request gives an array of naturals (interned names), a table an
|
|
258
|
+
array of them. `everyBelow(n, k => ...)` checks bookkeeping a request carries row by row (where each grant went). A
|
|
259
|
+
condition about every row of a table can't compare the rows' sequences (that is two keys at once); compare a row at a
|
|
260
|
+
named row, a witness, instead (`examples/scopes.ts`: pi-fabric's path scopes, covering, attenuation and normalization).
|
|
249
261
|
- **Lists a request carries** (`.carriesList('Lines', Line, folds, lemmas)`): the items one carried map per variable of
|
|
250
262
|
the kind, the length `Lines.len`, each fold (`{total: {from: 0, step: (acc, line) => acc.plus(line.amount)}}`) a
|
|
251
263
|
carried map of its running values whose equations every request is assumed to meet (the encoding computes them from the
|
package/dist/core/chart.js
CHANGED
|
@@ -256,9 +256,9 @@ export class Chart {
|
|
|
256
256
|
missed.push({ name, law: r.law(`${name} is never reached`, (env) => env[reg.field].is(l).not()) });
|
|
257
257
|
}
|
|
258
258
|
const proven = (v) => v.proven && v.checked;
|
|
259
|
-
const allMissed = missed.length > 0 && proven(proveRule(r, [...this.chartLaws(), ...missed.map(x => x.law)]));
|
|
259
|
+
const allMissed = missed.length > 0 && proven(proveRule(r, [...this.chartLaws(), ...missed.map(x => x.law)], { search: false }));
|
|
260
260
|
for (const x of missed)
|
|
261
|
-
reached[x.name] = allMissed || proven(proveRule(r, [...this.chartLaws(), x.law])) ? 'unreachable' : 'unknown';
|
|
261
|
+
reached[x.name] = allMissed || proven(proveRule(r, [...this.chartLaws(), x.law], { search: false })) ? 'unreachable' : 'unknown';
|
|
262
262
|
// A way out is a transition to a state: an internal one stays where it is.
|
|
263
263
|
const stuck = regions.flatMap(reg => reg.leaves.filter(l => !reg.finals.has(l) && !placed.some(t => t.to !== undefined && t.region === reg.name && t.sources.includes(l))).map(l => label(reg, l)));
|
|
264
264
|
// Pairs as specific as each other, on one event, from a state they share: proven never both enabled, all at once,
|
|
@@ -270,9 +270,9 @@ export class Chart {
|
|
|
270
270
|
pairs.push({ t, u, law: r.stepLaw(`transitions ${t.order} and ${u.order} on ${t.event} are never both enabled`, (env) => all(env[`on${i}`], env[`on${j}`]).not()) });
|
|
271
271
|
}));
|
|
272
272
|
const ambiguous = [];
|
|
273
|
-
if (pairs.length && !proven(proveSteps(r, this.chartLaws(), pairs.map(x => x.law))))
|
|
273
|
+
if (pairs.length && !proven(proveSteps(r, this.chartLaws(), pairs.map(x => x.law), { search: false })))
|
|
274
274
|
for (const { t, u, law } of pairs) {
|
|
275
|
-
const v = proveSteps(r, this.chartLaws(), [law]);
|
|
275
|
+
const v = proveSteps(r, this.chartLaws(), [law], { search: false });
|
|
276
276
|
if (!proven(v))
|
|
277
277
|
ambiguous.push({ event: t.event, transitions: [t.order, u.order], verdict: v });
|
|
278
278
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { C, E, Law, Machine } from './machine.js';
|
|
2
|
+
import type { Fold, Instances } from '../kernel/kernel.js';
|
|
3
|
+
import { type CNode, type Cyclic, type Fact, type Use } from '../kernel/cyclic.js';
|
|
4
|
+
export type NodeSpec = Omit<CNode, 'uses' | 'main'> & {
|
|
5
|
+
uses: Omit<Use, 'premises' | 'descent'>[];
|
|
6
|
+
};
|
|
7
|
+
export type CyclicVerdict = {
|
|
8
|
+
proven: true;
|
|
9
|
+
checked: boolean;
|
|
10
|
+
proof: Cyclic;
|
|
11
|
+
why?: string;
|
|
12
|
+
} | {
|
|
13
|
+
proven: false;
|
|
14
|
+
checked: false;
|
|
15
|
+
node: string;
|
|
16
|
+
goal: string;
|
|
17
|
+
stuck: string;
|
|
18
|
+
};
|
|
19
|
+
export declare const pair: (u: E, key: E) => E;
|
|
20
|
+
export declare function proveCyclic(facts: Fact[], specs: NodeSpec[]): CyclicVerdict;
|
|
21
|
+
export type RecurCase = {
|
|
22
|
+
name: string;
|
|
23
|
+
args: string[];
|
|
24
|
+
zero: E;
|
|
25
|
+
next: E;
|
|
26
|
+
lemmas: {
|
|
27
|
+
name: string;
|
|
28
|
+
holds: C;
|
|
29
|
+
}[];
|
|
30
|
+
between: {
|
|
31
|
+
name: string;
|
|
32
|
+
holds: C;
|
|
33
|
+
}[];
|
|
34
|
+
facts: Law[];
|
|
35
|
+
};
|
|
36
|
+
export declare function recurNodes(r: RecurCase): {
|
|
37
|
+
facts: Fact[];
|
|
38
|
+
specs: NodeSpec[];
|
|
39
|
+
};
|
|
40
|
+
export declare const cyclicMachine: (m: Machine, laws?: Law[]) => boolean;
|
|
41
|
+
export declare function machineNodes(m: Machine, laws: Law[], instances: Record<string, Instances>): {
|
|
42
|
+
facts: Fact[];
|
|
43
|
+
specs: NodeSpec[];
|
|
44
|
+
};
|
|
45
|
+
export declare function foldNodes(f: Fold, laws: Law[]): {
|
|
46
|
+
facts: Fact[];
|
|
47
|
+
specs: NodeSpec[];
|
|
48
|
+
};
|
|
49
|
+
export declare const cyclicOn: () => boolean;
|
|
50
|
+
export declare const quietly: <T>(f: () => T) => T;
|
|
51
|
+
export declare function crossCyclic(kind: string, name: string, usual: boolean, run: () => CyclicVerdict, usualMs?: number): void;
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { substC, substE } from "../kernel/kernel.js";
|
|
2
|
+
import { descentGoal, mainGoal, premiseGoal, refuseCyclic } from "../kernel/cyclic.js";
|
|
3
|
+
import { cellProducts, grow } from "../kernel/prove.js";
|
|
4
|
+
const v = (name) => ({ op: 'var', name });
|
|
5
|
+
const k = (value) => ({ op: 'const', value });
|
|
6
|
+
const add = (a, b) => ({ op: 'add', a, b });
|
|
7
|
+
const less1 = (a) => ({ op: 'sub', a, b: k(1) });
|
|
8
|
+
const get = (map, key) => ({ op: 'get', map, key });
|
|
9
|
+
const le = (a, b) => ({ op: '<=', a, b });
|
|
10
|
+
const eq = (a, b) => ({ op: '=', a, b });
|
|
11
|
+
const and = (...xs) => (xs.length === 1 ? xs[0] : { op: 'and', xs });
|
|
12
|
+
const implies = (a, b) => ({ op: 'or', xs: [{ op: 'not', x: a }, b] });
|
|
13
|
+
const yes = le(k(0), k(0));
|
|
14
|
+
const vars = (s) => ({ vars: s, maps: {}, flags: {} });
|
|
15
|
+
// A map's cell at time u and key k, in the run's one map over both: u + k·Ω, where Ω is above the run's length T, so
|
|
16
|
+
// that no two (u, k) with u <= T meet (u < Ω). Linear in u, and two keys' cells differ by (k − k')·Ω.
|
|
17
|
+
const omega = v('O$');
|
|
18
|
+
export const pair = (u, key) => add(u, { op: 'prod', a: key, b: omega });
|
|
19
|
+
// Every goal grown, then the whole checked by the kernel.
|
|
20
|
+
export function proveCyclic(facts, specs) {
|
|
21
|
+
const nodes = [];
|
|
22
|
+
const grown = (g, node) => {
|
|
23
|
+
const was = cellProducts.on;
|
|
24
|
+
cellProducts.on = true;
|
|
25
|
+
let o;
|
|
26
|
+
try {
|
|
27
|
+
o = grow(g);
|
|
28
|
+
}
|
|
29
|
+
finally {
|
|
30
|
+
cellProducts.on = was;
|
|
31
|
+
}
|
|
32
|
+
return 'tree' in o ? o.tree : { proven: false, checked: false, node, goal: g.name, stuck: o.stuck };
|
|
33
|
+
};
|
|
34
|
+
for (const n of specs) {
|
|
35
|
+
const main = grown(mainGoal(facts, specs, n), n.name);
|
|
36
|
+
if ('proven' in main)
|
|
37
|
+
return main;
|
|
38
|
+
const uses = [];
|
|
39
|
+
for (const u of n.uses) {
|
|
40
|
+
const m = specs[u.node];
|
|
41
|
+
if (!m)
|
|
42
|
+
return { proven: false, checked: false, node: n.name, goal: `use of node ${u.node}`, stuck: 'no such node' };
|
|
43
|
+
const premises = [];
|
|
44
|
+
for (const [i, c] of m.hyps.entries()) {
|
|
45
|
+
const t = grown(premiseGoal(facts, n, u, c, i), n.name);
|
|
46
|
+
if ('proven' in t)
|
|
47
|
+
return t;
|
|
48
|
+
premises.push(t);
|
|
49
|
+
}
|
|
50
|
+
const descent = grown(descentGoal(facts, n, u, m), n.name);
|
|
51
|
+
if ('proven' in descent)
|
|
52
|
+
return descent;
|
|
53
|
+
uses.push({ ...u, premises, descent });
|
|
54
|
+
}
|
|
55
|
+
nodes.push({ ...n, uses, main });
|
|
56
|
+
}
|
|
57
|
+
const proof = { facts, nodes }, why = refuseCyclic(proof);
|
|
58
|
+
return { proven: true, checked: why === undefined, proof, ...(why ? { why } : {}) };
|
|
59
|
+
}
|
|
60
|
+
// The facts of a machine's functions (laws about every key of them), as cyclic facts.
|
|
61
|
+
const factOf = (l) => ({ name: l.name, keys: [...(l.forall ? [l.forall] : []), ...(l.keys ?? [])], holds: l.holds });
|
|
62
|
+
export function recurNodes(r) {
|
|
63
|
+
const f = `${r.name}$`, x = v('x'), y = v('y'), kk = v('k$');
|
|
64
|
+
const facts = [
|
|
65
|
+
{ name: `${r.name}(0)`, keys: [], holds: eq(get(f, k(0)), r.zero) },
|
|
66
|
+
{ name: `${r.name}(x + 1)`, keys: ['k$'], holds: eq(get(f, add(kk, k(1))), substE(r.next, vars({ x: kk, prev: get(f, kk) }))) },
|
|
67
|
+
...r.facts.map(factOf),
|
|
68
|
+
];
|
|
69
|
+
const at = (t) => vars({ x: t, fx: get(f, t) });
|
|
70
|
+
const n = r.lemmas.length;
|
|
71
|
+
const lemmaUses = (to, guard) => r.lemmas.map((_, j) => ({ node: j, subst: { x: to }, guard }));
|
|
72
|
+
const specs = r.lemmas.map(l => ({
|
|
73
|
+
name: `${r.name}: ${l.name}`, hyps: [], concl: substC(l.holds, at(x)), rank: [k(0), x],
|
|
74
|
+
insts: [{ fact: 0, at: [] }, { fact: 1, at: [less1(x)] }], uses: lemmaUses(less1(x), le(k(1), x)),
|
|
75
|
+
}));
|
|
76
|
+
// A law between two points: for every x <= y, by induction on y from x, the lemmas at x, at y and at y − 1.
|
|
77
|
+
for (const b of r.between)
|
|
78
|
+
specs.push({
|
|
79
|
+
name: `${r.name}: ${b.name}`, hyps: [], rank: [k(1), y],
|
|
80
|
+
concl: implies(le(x, y), substC(b.holds, vars({ x, fx: get(f, x), y, fy: get(f, y) }))),
|
|
81
|
+
insts: [{ fact: 1, at: [less1(y)] }],
|
|
82
|
+
uses: [...r.between.map((_, j) => ({ node: n + j, subst: { y: less1(y) }, guard: le(add(x, k(1)), y) })), ...lemmaUses(x, yes), ...lemmaUses(y, yes), ...lemmaUses(less1(y), le(k(1), y))],
|
|
83
|
+
});
|
|
84
|
+
return { facts, specs: withReads(facts, specs) };
|
|
85
|
+
}
|
|
86
|
+
// Function facts at the keys a node's goal reads them at: each fact about every key of a function read f[k] (the key
|
|
87
|
+
// alone) at each key f is read at, twice over (a fact's own reads give more keys).
|
|
88
|
+
function withReads(facts, specs) {
|
|
89
|
+
const simple = facts.flatMap((f, i) => {
|
|
90
|
+
if (f.keys.length !== 1)
|
|
91
|
+
return [];
|
|
92
|
+
const reads = [...JSON.stringify(f.holds).matchAll(/"op":"get","map":"([^"]+)","key":\{"op":"var","name":"([^"]+)"\}/g)].filter(m => m[2] === f.keys[0]);
|
|
93
|
+
return reads.map(m => ({ fact: i, map: m[1] }));
|
|
94
|
+
});
|
|
95
|
+
if (!simple.length)
|
|
96
|
+
return specs;
|
|
97
|
+
const readsIn = (t, map) => { const out = []; const walk = (x) => { if (x && typeof x === 'object') {
|
|
98
|
+
const o = x;
|
|
99
|
+
if (o.op === 'get' && o.map === map)
|
|
100
|
+
out.push(o.key);
|
|
101
|
+
for (const val of Object.values(o))
|
|
102
|
+
walk(val);
|
|
103
|
+
} }; walk(t); return out; };
|
|
104
|
+
return specs.map(n => {
|
|
105
|
+
const insts = [...n.insts], seen = new Set(insts.map(p => JSON.stringify(p)));
|
|
106
|
+
for (let round = 0; round < 2; round++) {
|
|
107
|
+
const goal = [n.hyps, n.concl, insts.map(p => substC(facts[p.fact].holds, vars(Object.fromEntries(facts[p.fact].keys.map((key, i) => [key, p.at[i]]))))), n.uses.map(u => substC(specs[u.node].concl, vars(u.subst)))];
|
|
108
|
+
for (const s of simple)
|
|
109
|
+
for (const key of readsIn(goal, s.map)) {
|
|
110
|
+
const p = { fact: s.fact, at: [key] }, id = JSON.stringify(p);
|
|
111
|
+
if (!seen.has(id)) {
|
|
112
|
+
seen.add(id);
|
|
113
|
+
insts.push(p);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return { ...n, insts };
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
// --- machines -------------------------------------------------------------------------------------------------------
|
|
121
|
+
// The machines this instance covers: no history, no flag holding at every key, and no law that reads a map at a key
|
|
122
|
+
// read from a map of the state (a table of nodes: a node's fold at its child), whose cells a run's maps put at keys
|
|
123
|
+
// the prover would have to prove equal through products (`machineNodes` proves those laws far slower than the usual
|
|
124
|
+
// rule, past its budget: research/cyclic/probe.ts).
|
|
125
|
+
export const cyclicMachine = (m, laws = []) => {
|
|
126
|
+
const maps = Object.keys(m.maps ?? {});
|
|
127
|
+
const nested = (t) => { let found = false; const walk = (x, inKey) => { if (found || !x || typeof x !== 'object')
|
|
128
|
+
return; const o = x; if (o.op === 'get' && inKey && maps.includes(o.map)) {
|
|
129
|
+
found = true;
|
|
130
|
+
return;
|
|
131
|
+
} if (o.op === 'get') {
|
|
132
|
+
walk(o.key, true);
|
|
133
|
+
return;
|
|
134
|
+
} for (const val of Object.values(o))
|
|
135
|
+
walk(val, inKey); }; walk(t, false); return found; };
|
|
136
|
+
return !m.history && !Object.values(m.step.flags ?? {}).some(c => c.op === 'all') && !laws.some(l => nested(l.holds));
|
|
137
|
+
};
|
|
138
|
+
// The run as maps over time: names at time u read as their maps there.
|
|
139
|
+
function trace(m) {
|
|
140
|
+
const maps = [...Object.keys(m.maps ?? {}), ...(m.inputMaps ?? [])];
|
|
141
|
+
const at = (u) => ({
|
|
142
|
+
vars: Object.fromEntries([...Object.keys(m.state), ...m.inputs].map(n => [n, get(`${n}@`, u)])),
|
|
143
|
+
maps: Object.fromEntries(maps.map(map => [map, (key) => get(`${map}@`, pair(u, key))])), flags: {},
|
|
144
|
+
});
|
|
145
|
+
return { at };
|
|
146
|
+
}
|
|
147
|
+
// Every map read at `from` (the same term) read at `to`.
|
|
148
|
+
const readsAt = (t, from, to) => {
|
|
149
|
+
const id = JSON.stringify(from);
|
|
150
|
+
const walk = (x) => {
|
|
151
|
+
if (Array.isArray(x))
|
|
152
|
+
return x.map(walk);
|
|
153
|
+
if (!x || typeof x !== 'object')
|
|
154
|
+
return x;
|
|
155
|
+
const o = x;
|
|
156
|
+
if (o.op === 'get' && JSON.stringify(o.key) === id)
|
|
157
|
+
return { ...o, key: to };
|
|
158
|
+
return Object.fromEntries(Object.entries(o).map(([key, val]) => [key, walk(val)]));
|
|
159
|
+
};
|
|
160
|
+
return walk(t);
|
|
161
|
+
};
|
|
162
|
+
// A machine's flags, each its condition over the frame before the step (through the flags before it).
|
|
163
|
+
const flagsOf = (m) => {
|
|
164
|
+
const out = {};
|
|
165
|
+
for (const [f, c] of Object.entries(m.step.flags ?? {}))
|
|
166
|
+
out[f] = substC(c, { vars: {}, maps: {}, flags: out });
|
|
167
|
+
return out;
|
|
168
|
+
};
|
|
169
|
+
export function machineNodes(m, laws, instances) {
|
|
170
|
+
const { at } = trace(m), flags = flagsOf(m), inline = (t) => substC(t, { vars: {}, maps: {}, flags });
|
|
171
|
+
const t = v('t$'), T = v('T$'), u = v('u$'), key = v('k$');
|
|
172
|
+
const inlineE = (e) => substE(e, { vars: {}, maps: {}, flags });
|
|
173
|
+
const now = (c, when) => substC(c, at(when)), nowE = (e, when) => substE(inlineE(e), at(when));
|
|
174
|
+
const state = Object.keys(m.state), mapNames = Object.keys(m.maps ?? {});
|
|
175
|
+
// The start and a step, each stated at the time u it gives (a start at u <= 0, a step at 1 <= u <= T from u − 1), so a
|
|
176
|
+
// node reads them at its own time t: the cells they give are the cells it reads, with no key to prove equal.
|
|
177
|
+
const start = le(u, k(0)), stepped = and(le(k(1), u), le(u, T)), prev = less1(u);
|
|
178
|
+
const facts = [
|
|
179
|
+
{ name: `${m.name}: the start`, keys: ['u$'], holds: implies(start, and(yes, ...state.map(s => eq(get(`${s}@`, u), m.state[s])))) },
|
|
180
|
+
{ name: `${m.name}: a step`, keys: ['u$'], holds: implies(stepped, and(yes, ...(m.assume ? [now(inline(m.assume), prev)] : []), ...state.map(s => eq(get(`${s}@`, u), nowE(m.step.state?.[s] ?? v(s), prev))))) },
|
|
181
|
+
...mapNames.flatMap((map) => {
|
|
182
|
+
const w = m.step.maps?.[map], cell = (when) => get(`${map}@`, pair(when, key));
|
|
183
|
+
// A write at one key: where the key is the write's, the value with every read at the write's key read at the key
|
|
184
|
+
// instead (the same cell there), so the cells the step gives are at the key the node reads.
|
|
185
|
+
const next = !w ? cell(prev) : 'each' in w ? substE(substE(w.value, { vars: { [w.each]: key }, maps: {}, flags }), at(prev))
|
|
186
|
+
: { op: 'if', c: eq(key, nowE(w.key, prev)), then: substE(readsAt(inlineE(w.value), inlineE(w.key), key), at(prev)), else: cell(prev) };
|
|
187
|
+
return [
|
|
188
|
+
{ name: `${m.name}: ${map} at the start`, keys: ['u$', 'k$'], holds: implies(start, eq(cell(u), k(m.maps[map]))) },
|
|
189
|
+
{ name: `${m.name}: ${map} at a step`, keys: ['u$', 'k$'], holds: implies(stepped, eq(cell(u), next)) },
|
|
190
|
+
{ name: `${m.name}: ${map}'s cells are naturals`, keys: ['u$', 'k$'], holds: le(k(0), cell(u)) },
|
|
191
|
+
];
|
|
192
|
+
}),
|
|
193
|
+
...(m.facts ?? []).map(factOf),
|
|
194
|
+
// That a state variable's value at a time is a natural holds of every model: read at the node's time, it makes the
|
|
195
|
+
// value a column of its own (a value read only inside a product would be none, and no two reads of it equal).
|
|
196
|
+
...state.map((s) => ({ name: `${m.name}: ${s} is a natural`, keys: ['u$'], holds: le(k(0), get(`${s}@`, u)) })),
|
|
197
|
+
{ name: `${m.name}: params are naturals`, keys: [], holds: and(yes, ...m.params.map(x => le(k(0), v(x)))) },
|
|
198
|
+
];
|
|
199
|
+
const fi = (name) => facts.findIndex(f => f.name === name);
|
|
200
|
+
const keysOf = (l) => [...(l.forall ? [l.forall] : []), ...(l.keys ?? [])];
|
|
201
|
+
// A map read at the node's time t at key e: that map's start, step and natural facts at (t, e).
|
|
202
|
+
const mapInsts = (c) => {
|
|
203
|
+
const out = [], seen = new Set();
|
|
204
|
+
const walk = (x) => {
|
|
205
|
+
if (!x || typeof x !== 'object')
|
|
206
|
+
return;
|
|
207
|
+
const o = x;
|
|
208
|
+
if (o.op === 'get' && typeof o.map === 'string' && o.map.endsWith('@') && mapNames.includes(o.map.slice(0, -1))) {
|
|
209
|
+
const kk = o.key;
|
|
210
|
+
if (kk.op === 'add' && JSON.stringify(kk.a) === JSON.stringify(t) && kk.b.op === 'prod') {
|
|
211
|
+
const read = kk.b.a, id = JSON.stringify(read), map = o.map.slice(0, -1);
|
|
212
|
+
if (!seen.has(`${map}:${id}`)) {
|
|
213
|
+
seen.add(`${map}:${id}`);
|
|
214
|
+
out.push(...['at the start', 'at a step', '\'s cells are naturals'].map(what => ({ fact: fi(`${m.name}: ${map}${what.startsWith('\'') ? what : ` ${what}`}`), at: [t, read] })));
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
for (const val of Object.values(o))
|
|
219
|
+
walk(val);
|
|
220
|
+
};
|
|
221
|
+
walk(c);
|
|
222
|
+
return out;
|
|
223
|
+
};
|
|
224
|
+
const specs = laws.map(l => {
|
|
225
|
+
const inst = instances[l.name] ?? {};
|
|
226
|
+
// Terms over the frame before the step (the machine's proof proves a law about every key at a fresh key k#: here
|
|
227
|
+
// the node's own k).
|
|
228
|
+
const fresh = Object.fromEntries(laws.flatMap(x => (x.forall ? [[`${x.forall}#`, v(x.forall)]] : [])));
|
|
229
|
+
const pre = at(less1(t)), before = (e) => substE(substE(e, vars(fresh)), pre);
|
|
230
|
+
// The laws at the step before, at the keys the machine's proof assumes them at.
|
|
231
|
+
const uses = laws.flatMap((l2, j) => {
|
|
232
|
+
const ks = keysOf(l2);
|
|
233
|
+
if (!ks.length)
|
|
234
|
+
return [{ node: j, subst: { 't$': less1(t) }, guard: le(k(1), t) }];
|
|
235
|
+
const flat = inst[l2.name] ?? [];
|
|
236
|
+
return Array.from({ length: Math.floor(flat.length / ks.length) }, (_, i) => ({ node: j, subst: { 't$': less1(t), ...Object.fromEntries(ks.map((kn, q) => [kn, before(flat[i * ks.length + q])])) }, guard: le(k(1), t) }));
|
|
237
|
+
});
|
|
238
|
+
// The functions' facts at the keys the proof names, over the frame before the step (at t = 0, the start's).
|
|
239
|
+
const factInsts = (m.facts ?? []).flatMap(f => {
|
|
240
|
+
const ks = keysOf(f), flat = inst[f.name] ?? [];
|
|
241
|
+
if (!ks.length)
|
|
242
|
+
return [{ fact: fi(f.name), at: [] }];
|
|
243
|
+
return Array.from({ length: Math.floor(flat.length / ks.length) }, (_, i) => ({ fact: fi(f.name), at: flat.slice(i * ks.length, (i + 1) * ks.length).map(before) }));
|
|
244
|
+
});
|
|
245
|
+
const concl = substC(l.holds, at(t));
|
|
246
|
+
const useConcls = uses.map(us => substC(substC(laws[us.node].holds, at(t)), vars(us.subst)));
|
|
247
|
+
const base = mapInsts([concl, useConcls]);
|
|
248
|
+
return {
|
|
249
|
+
name: `${m.name}: ${l.name}`, hyps: [...(m.pre ? [m.pre] : []), le(t, T), ...(mapNames.length ? [{ op: '<', a: T, b: omega }] : [])], concl, rank: [t],
|
|
250
|
+
insts: [{ fact: 0, at: [t] }, { fact: 1, at: [t] }, ...factInsts, ...base, ...state.map(st => ({ fact: fi(`${m.name}: ${st} is a natural`), at: [t] })), { fact: fi(`${m.name}: params are naturals`), at: [] }], uses,
|
|
251
|
+
};
|
|
252
|
+
});
|
|
253
|
+
return { facts, specs };
|
|
254
|
+
}
|
|
255
|
+
// --- folds over trees -----------------------------------------------------------------------------------------------
|
|
256
|
+
// The tree as a table of nodes below its size N: a node n is a leaf (Kind[n] = 0) whose values come from its inputs,
|
|
257
|
+
// or a node (Kind[n] = 1) with children Left[n], Right[n] numbered below n, its values from theirs and its inputs.
|
|
258
|
+
export function foldNodes(f, laws) {
|
|
259
|
+
const n = v('n$'), N = v('N$'), names = Object.keys(f.leaf);
|
|
260
|
+
const at = (i) => vars(Object.fromEntries([...names, ...f.inputs].map(x => [x, get(`${x}@`, i)])));
|
|
261
|
+
const kind = (i) => get('Kind@', i), left = (i) => get('Left@', i), right = (i) => get('Right@', i);
|
|
262
|
+
const side = (child, p) => Object.fromEntries(names.map(x => [`${p}${x}`, get(`${x}@`, child)]));
|
|
263
|
+
const nodeAt = (i) => vars({ ...Object.fromEntries(f.inputs.map(x => [x, get(`${x}@`, i)])), ...side(left(i), 'l_'), ...side(right(i), 'r_') });
|
|
264
|
+
const assume = (i) => (f.assume ? [substC(f.assume, at(i))] : []);
|
|
265
|
+
const facts = [{ name: `${f.name}: the table`, keys: ['n$'], holds: implies({ op: '<', a: n, b: N }, and(le(kind(n), k(1)), implies(eq(kind(n), k(0)), and(yes, ...assume(n), ...names.map(x => eq(get(`${x}@`, n), substE(f.leaf[x], at(n)))))), implies(eq(kind(n), k(1)), and({ op: '<', a: left(n), b: n }, { op: '<', a: right(n), b: n }, ...assume(n), ...names.map(x => eq(get(`${x}@`, n), substE(f.node[x], nodeAt(n)))))))) },
|
|
266
|
+
// Each value a natural (true of every model), so a value read only inside a product is a column of its own.
|
|
267
|
+
{ name: `${f.name}: values are naturals`, keys: ['n$'], holds: and(yes, ...names.map(x => le(k(0), get(`${x}@`, n)))) },
|
|
268
|
+
{ name: `${f.name}: params are naturals`, keys: [], holds: and(yes, ...f.params.map(x => le(k(0), v(x)))) }];
|
|
269
|
+
const specs = laws.map(l => ({
|
|
270
|
+
name: `${f.name}: ${l.name}`, hyps: [...(f.pre ? [f.pre] : []), { op: '<', a: n, b: N }], concl: substC(l.holds, at(n)), rank: [n],
|
|
271
|
+
insts: [{ fact: 0, at: [n] }, ...[n, left(n), right(n)].map(i => ({ fact: 1, at: [i] })), { fact: 2, at: [] }],
|
|
272
|
+
uses: laws.flatMap((_, j) => [left, right].map(c => ({ node: j, subst: { 'n$': c(n) }, guard: eq(kind(n), k(1)) }))),
|
|
273
|
+
}));
|
|
274
|
+
return { facts, specs };
|
|
275
|
+
}
|
|
276
|
+
// --- the cyclic route beside the usual one (GDP_CYCLIC=1) -------------------------------------------------------------
|
|
277
|
+
// With GDP_CYCLIC=1, every proof the instances above cover is made again as a cyclic proof, and a verdict that differs
|
|
278
|
+
// throws: the suite run so shows the rules are instances. Each comparison is printed to stderr as a line `cyclic: {...}`.
|
|
279
|
+
const env = () => globalThis.process?.env;
|
|
280
|
+
let quiet = 0;
|
|
281
|
+
export const cyclicOn = () => quiet === 0 && env()?.GDP_CYCLIC === '1';
|
|
282
|
+
// A search's many trial proofs (core/discover.ts, core/infer.ts) aren't compared: what it finds is proven again.
|
|
283
|
+
export const quietly = (f) => { quiet++; try {
|
|
284
|
+
return f();
|
|
285
|
+
}
|
|
286
|
+
finally {
|
|
287
|
+
quiet--;
|
|
288
|
+
} };
|
|
289
|
+
export function crossCyclic(kind, name, usual, run, usualMs) {
|
|
290
|
+
const t0 = performance.now(), out = run(), ms = Math.round(performance.now() - t0);
|
|
291
|
+
const cyclic = out.proven && out.checked;
|
|
292
|
+
console.error(`cyclic: ${JSON.stringify({ kind, name, usual, cyclic, ms, ...(usualMs === undefined ? {} : { usualMs: Math.round(usualMs) }), ...(out.proven ? { nodes: out.proof.nodes.length, ...(out.why ? { why: out.why } : {}) } : { goal: out.goal, stuck: out.stuck }) })}`);
|
|
293
|
+
if (cyclic !== usual)
|
|
294
|
+
throw new Error(`cyclic route: ${kind} ${name} is ${cyclic ? 'proven' : 'not proven'} as a cyclic proof, ${usual ? 'proven' : 'not proven'} the usual way${out.proven ? (out.why ? ` (${out.why})` : '') : ` (${out.goal}: ${out.stuck})`}`);
|
|
295
|
+
}
|
package/dist/core/discover.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export type Discovery = {
|
|
|
12
12
|
rule: AnyRule;
|
|
13
13
|
found: Record<string, Found>;
|
|
14
14
|
};
|
|
15
|
-
export declare function discover(r: AnyRule, laws: Law[], stepLaws?: StepLaw[]): Discovery;
|
|
15
|
+
export declare function discover(r: AnyRule, laws: Law[], stepLaws?: StepLaw[], first?: Verdict): Discovery;
|
|
16
16
|
type TreeFold = {
|
|
17
17
|
fold: Fold;
|
|
18
18
|
lemmas?: Law[];
|
package/dist/core/discover.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// const out = discover(r, laws, stepLaws)
|
|
11
11
|
// out.verdict // the laws, proven of out.rule: r with its functions and lists made again with the lemmas
|
|
12
12
|
// out.found // per function or list, the lemmas found, as source to paste into recur(...) or carriesList(...)
|
|
13
|
+
import { quietly } from "./cyclic.js";
|
|
13
14
|
import { any, atMost, compileFunction, evalC, evalE, n, plus, prod, same, times, x } from "./machine.js";
|
|
14
15
|
import { Unproven, lit, proveFold, proveSteps, recur, recurOf } from "./dsl.js";
|
|
15
16
|
const seeded = (seed) => { let s = seed; return (k) => { s = (s * 1103515245 + 12345) % 2 ** 31; return Math.floor(s / 2 ** 31 * k); }; };
|
|
@@ -259,8 +260,11 @@ function houdiniList(r, part, cands, params) {
|
|
|
259
260
|
// --- the search ----------------------------------------------------------------------------------------------------
|
|
260
261
|
// The laws of r, and where they aren't proven as r states them, the lemmas found for its functions and lists: all that
|
|
261
262
|
// is proven first, then each dropped in turn while the laws stay proven without it.
|
|
262
|
-
|
|
263
|
-
|
|
263
|
+
// `first`: the laws' verdict on r alone, when the caller has it.
|
|
264
|
+
export function discover(r, laws, stepLaws = [], first) {
|
|
265
|
+
return quietly(() => discoverSearch(r, laws, stepLaws, first ?? proveSteps(r, laws, stepLaws, { search: false })));
|
|
266
|
+
}
|
|
267
|
+
function discoverSearch(r, laws, stepLaws, first) {
|
|
264
268
|
if (first.proven && first.checked)
|
|
265
269
|
return { verdict: first, rule: r, found: {} };
|
|
266
270
|
const { params, fns, lists } = r.parts, ps = natParams(params);
|
|
@@ -284,7 +288,7 @@ export function discover(r, laws, stepLaws = []) {
|
|
|
284
288
|
listKept[part[0]] = out.kept;
|
|
285
289
|
}
|
|
286
290
|
}
|
|
287
|
-
const verdict = proveSteps(rule, laws, stepLaws);
|
|
291
|
+
const verdict = proveSteps(rule, laws, stepLaws, { search: false });
|
|
288
292
|
const found = () => ({
|
|
289
293
|
...Object.fromEntries(Object.entries(fnMade).map(([n, m]) => { const args = [...(recurOf(m.d).def.args ?? [])]; return [n, { ...(m.lemmas.length ? { lemmas: m.lemmas.map(c => recurSrc(c, args, false)) } : {}), ...(m.between.length ? { between: m.between.map(c => recurSrc(c, args, true)) } : {}) }]; })),
|
|
290
294
|
...Object.fromEntries(Object.entries(listKept).map(([n, cs]) => [n, { lemmas: cs.map(c => listSrc(c, Object.keys(lists.find(l => l[0] === n)[2]), ps, c.target)) }])),
|
|
@@ -297,7 +301,7 @@ export function discover(r, laws, stepLaws = []) {
|
|
|
297
301
|
// A rule made again and the laws proven of it; or the lemma whose proof failed without the one dropped.
|
|
298
302
|
const tryRule = (next) => {
|
|
299
303
|
try {
|
|
300
|
-
const r2 = next(), v = proveSteps(r2, laws, stepLaws);
|
|
304
|
+
const r2 = next(), v = proveSteps(r2, laws, stepLaws, { search: false });
|
|
301
305
|
return v.proven && v.checked ? { rule: r2, verdict: v } : undefined;
|
|
302
306
|
}
|
|
303
307
|
catch (e) {
|
|
@@ -405,7 +409,8 @@ const eOfSide = (side) => {
|
|
|
405
409
|
return es.length ? es.slice(1).reduce((acc, t) => plus(acc, t), es[0]) : n(0);
|
|
406
410
|
};
|
|
407
411
|
const cOfCand = (c) => { const { pos, neg } = sides(c.lin), a = eOfSide(pos), b = eOfSide(neg); return c.rel === '=' ? same(a, b) : atMost(a, b); };
|
|
408
|
-
export function discoverFold(f, laws) {
|
|
412
|
+
export function discoverFold(f, laws) { return quietly(() => discoverFoldSearch(f, laws)); }
|
|
413
|
+
function discoverFoldSearch(f, laws) {
|
|
409
414
|
const stated = f.lemmas ?? [], first = proveFold({ fold: f.fold, lemmas: stated }, laws);
|
|
410
415
|
if (first.proven && first.checked)
|
|
411
416
|
return { verdict: first, lemmas: [], found: [] };
|
package/dist/core/dsl.d.ts
CHANGED
|
@@ -299,6 +299,26 @@ export declare class ListRef {
|
|
|
299
299
|
within(): Forall;
|
|
300
300
|
increasing(): Forall;
|
|
301
301
|
}
|
|
302
|
+
export declare class SeqRef {
|
|
303
|
+
private readonly map;
|
|
304
|
+
readonly len: Term;
|
|
305
|
+
private readonly base?;
|
|
306
|
+
constructor(map: MapRef, len: Term, base?: Term | undefined);
|
|
307
|
+
at(i: TermLike): Term;
|
|
308
|
+
private agree;
|
|
309
|
+
prefixOf(o: SeqRef): Cond;
|
|
310
|
+
startsWith(o: SeqRef): Cond;
|
|
311
|
+
equals(o: SeqRef): Cond;
|
|
312
|
+
extends(o: SeqRef, by: TermLike): Cond;
|
|
313
|
+
has(x: TermLike): Cond;
|
|
314
|
+
}
|
|
315
|
+
export declare class SeqTable {
|
|
316
|
+
readonly name: string;
|
|
317
|
+
readonly rows: Term;
|
|
318
|
+
constructor(name: string, rows: Term);
|
|
319
|
+
row(r: TermLike): SeqRef;
|
|
320
|
+
}
|
|
321
|
+
export declare const everyBelow: (n: TermLike, f: (key: Term) => Cond) => Cond;
|
|
302
322
|
export type PartsWrite = {
|
|
303
323
|
readonly parts: Record<string, MapWrite>;
|
|
304
324
|
};
|
|
@@ -473,6 +493,8 @@ type Spec = {
|
|
|
473
493
|
nodes?: [string, Kind<unknown>, string[], Record<string, NodeFold<unknown, string>>][];
|
|
474
494
|
kinded?: [string, Kind<unknown>][];
|
|
475
495
|
carried?: string[];
|
|
496
|
+
seqs?: string[];
|
|
497
|
+
seqTables?: string[];
|
|
476
498
|
lists2?: [string, Kind<unknown>, Record<string, ListFold<unknown>>, Record<string, ListLemma>][];
|
|
477
499
|
fns?: [string, FnDef][];
|
|
478
500
|
facts?: ((env: never) => Record<string, Cond | Forall>)[];
|
|
@@ -539,6 +561,12 @@ export declare class Rule<P extends Obj = {}, I extends Obj = {}, S extends Obj
|
|
|
539
561
|
carries<const N extends string>(...names: N[]): Rule<P, I & {
|
|
540
562
|
[k in N]: MapRef;
|
|
541
563
|
}, S, M, F, R, Su, Q>;
|
|
564
|
+
carriesSeq<const N extends string>(...names: N[]): Rule<P, I & {
|
|
565
|
+
[k in N]: SeqRef;
|
|
566
|
+
}, S, M, F, R, Su, Q>;
|
|
567
|
+
carriesSeqs<const N extends string>(...names: N[]): Rule<P, I & {
|
|
568
|
+
[k in N]: SeqTable;
|
|
569
|
+
}, S, M, F, R, Su, Q>;
|
|
542
570
|
carriesList<const N extends string, V, Fd extends string = never>(name: N, of: Kind<V>, folds?: Record<Fd, ListFold<V, P>>, lemmas?: Record<string, (acc: Record<Fd, Term>, i: Term, p: P) => Cond>): Rule<P, I & {
|
|
543
571
|
[k in N]: CarriedList<V, Fd>;
|
|
544
572
|
}, S, M, F, R, Su, Q>;
|
|
@@ -604,7 +632,7 @@ export declare class Rule<P extends Obj = {}, I extends Obj = {}, S extends Obj
|
|
|
604
632
|
[k in keyof P]: Raw;
|
|
605
633
|
}): Record<string, number>;
|
|
606
634
|
encodeInput(values: {
|
|
607
|
-
[k in keyof I]: I[k] extends MapRef ? ReadonlySet<number> | readonly number[] | ReadonlyMap<number, number> : I[k] extends CarriedList<any, any> ? readonly Raw[] : Raw;
|
|
635
|
+
[k in keyof I]: I[k] extends MapRef ? ReadonlySet<number> | readonly number[] | ReadonlyMap<number, number> : I[k] extends SeqRef ? readonly number[] : I[k] extends SeqTable ? readonly (readonly number[])[] : I[k] extends CarriedList<any, any> ? readonly Raw[] : Raw;
|
|
608
636
|
}, params?: {
|
|
609
637
|
[k in keyof P]: Raw;
|
|
610
638
|
}): Request;
|
|
@@ -678,7 +706,7 @@ type Value<T> = {
|
|
|
678
706
|
[k in keyof T]: ValueOf<T[k]>;
|
|
679
707
|
};
|
|
680
708
|
type RawInputs<T> = {
|
|
681
|
-
[k in keyof T]: T[k] extends MapRef ? ReadonlySet<number> | readonly number[] | ReadonlyMap<number, number> : T[k] extends CarriedList<infer V, infer _F> ? readonly ValueOf<V>[] : ValueOf<T[k]>;
|
|
709
|
+
[k in keyof T]: T[k] extends MapRef ? ReadonlySet<number> | readonly number[] | ReadonlyMap<number, number> : T[k] extends SeqRef ? readonly number[] : T[k] extends SeqTable ? readonly (readonly number[])[] : T[k] extends CarriedList<infer V, infer _F> ? readonly ValueOf<V>[] : ValueOf<T[k]>;
|
|
682
710
|
};
|
|
683
711
|
type Field = Term | Cond | Int | Decimal | Choice<string> | Str | Variant<any, any> | Maybe<any> | {
|
|
684
712
|
[f: string]: Field | number;
|
|
@@ -770,13 +798,30 @@ export type Verdict = {
|
|
|
770
798
|
params: Record<string, number>;
|
|
771
799
|
trace: Record<string, unknown>[];
|
|
772
800
|
};
|
|
801
|
+
found?: Found;
|
|
802
|
+
};
|
|
803
|
+
export type Found = {
|
|
804
|
+
laws?: {
|
|
805
|
+
law: Law;
|
|
806
|
+
text: string;
|
|
807
|
+
source?: string;
|
|
808
|
+
}[];
|
|
809
|
+
lemmas?: Record<string, {
|
|
810
|
+
lemmas?: string[];
|
|
811
|
+
between?: string[];
|
|
812
|
+
}>;
|
|
813
|
+
};
|
|
814
|
+
export type ProveOptions = {
|
|
815
|
+
search?: boolean | {
|
|
816
|
+
ms?: number;
|
|
817
|
+
};
|
|
773
818
|
};
|
|
774
819
|
export declare class Unproven extends Error {
|
|
775
820
|
readonly verdict: Verdict;
|
|
776
821
|
constructor(what: string, verdict: Verdict);
|
|
777
822
|
}
|
|
778
|
-
export declare function proveRule(r: Rule<Obj, Obj, Obj, Obj, Obj, Obj, Obj, Obj>, laws: Law[]): Verdict;
|
|
779
|
-
export declare function proveSteps(r: Rule<Obj, Obj, Obj, Obj, Obj, Obj, Obj, Obj>, laws: Law[], stepLaws: StepLaw[]): Verdict;
|
|
823
|
+
export declare function proveRule(r: Rule<Obj, Obj, Obj, Obj, Obj, Obj, Obj, Obj>, laws: Law[], opts?: ProveOptions): Verdict;
|
|
824
|
+
export declare function proveSteps(r: Rule<Obj, Obj, Obj, Obj, Obj, Obj, Obj, Obj>, laws: Law[], stepLaws: StepLaw[], opts?: ProveOptions): Verdict;
|
|
780
825
|
export type { StepLaw };
|
|
781
826
|
export type { Liveness };
|
|
782
827
|
export declare function proveLeadsTo(r: Rule<Obj, Obj, Obj, Obj, Obj, Obj, Obj, Obj>, laws: Law[], l: Liveness): Verdict;
|