gdp-wired 0.17.0 → 0.19.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 +10 -3
- package/dist/core/cycles.d.ts +73 -0
- package/dist/core/cycles.js +899 -0
- package/dist/core/cyclic.d.ts +106 -6
- package/dist/core/cyclic.js +1138 -52
- package/dist/core/data.d.ts +258 -0
- package/dist/core/data.js +1858 -0
- package/dist/core/dsl.d.ts +7 -1
- package/dist/core/dsl.js +43 -10
- package/dist/core/translate.d.ts +30 -0
- package/dist/core/translate.js +572 -0
- package/dist/kernel/prove.d.ts +18 -0
- package/dist/kernel/prove.js +272 -15
- package/dist/lean-kernel/emit.d.ts +95 -1
- package/dist/lean-kernel/emit.js +615 -83
- package/dist/lean-kernel/library.js +4 -1
- package/package.json +10 -2
|
@@ -0,0 +1,1858 @@
|
|
|
1
|
+
import { substC, substE } from "../kernel/kernel.js";
|
|
2
|
+
import { proveCyclic } from "./cyclic.js";
|
|
3
|
+
import { pairedKeys, withDeadline } from "../kernel/prove.js";
|
|
4
|
+
// --- sorts and the AST ------------------------------------------------------------------------------------------------
|
|
5
|
+
export const self = { self: true };
|
|
6
|
+
export const natural = 'nat';
|
|
7
|
+
export const boolean = 'bool';
|
|
8
|
+
// --- the surface ------------------------------------------------------------------------------------------------------
|
|
9
|
+
let fresh = 0;
|
|
10
|
+
const bindName = (field) => `${field}#${++fresh}`;
|
|
11
|
+
export class Nat {
|
|
12
|
+
x;
|
|
13
|
+
constructor(x) {
|
|
14
|
+
this.x = x;
|
|
15
|
+
}
|
|
16
|
+
plus(b) { return new Nat({ op: 'add', a: this.x, b: natX(b) }); }
|
|
17
|
+
minus(b) { return new Nat({ op: 'sub', a: this.x, b: natX(b) }); }
|
|
18
|
+
times(k) { return new Nat({ op: 'mul', k, a: this.x }); }
|
|
19
|
+
lt(b) { return new Bool({ op: '<', a: this.x, b: natX(b) }); }
|
|
20
|
+
atMost(b) { return new Bool({ op: '<=', a: this.x, b: natX(b) }); }
|
|
21
|
+
gt(b) { return new Bool({ op: '<', a: natX(b), b: this.x }); }
|
|
22
|
+
atLeast(b) { return new Bool({ op: '<=', a: natX(b), b: this.x }); }
|
|
23
|
+
eq(b) { return new Bool({ op: '=', a: this.x, b: natX(b) }); }
|
|
24
|
+
ne(b) { return this.eq(b).not(); }
|
|
25
|
+
}
|
|
26
|
+
const natX = (t) => (typeof t === 'number' ? { op: 'nat', value: t } : t.x);
|
|
27
|
+
export class Bool {
|
|
28
|
+
y;
|
|
29
|
+
constructor(y) {
|
|
30
|
+
this.y = y;
|
|
31
|
+
}
|
|
32
|
+
and(...bs) { return new Bool({ op: 'and', xs: [this.y, ...bs.map(b => b.y)] }); }
|
|
33
|
+
or(...bs) { return new Bool({ op: 'or', xs: [this.y, ...bs.map(b => b.y)] }); }
|
|
34
|
+
not() { return new Bool({ op: 'not', x: this.y }); }
|
|
35
|
+
implies(b) { return this.not().or(b); }
|
|
36
|
+
iff(b) { return this.implies(b).and(b.implies(this)); }
|
|
37
|
+
}
|
|
38
|
+
export const yes = new Bool({ op: '<=', a: { op: 'nat', value: 0 }, b: { op: 'nat', value: 0 } });
|
|
39
|
+
export const no = new Bool({ op: '<', a: { op: 'nat', value: 0 }, b: { op: 'nat', value: 0 } });
|
|
40
|
+
export class Val {
|
|
41
|
+
data;
|
|
42
|
+
x;
|
|
43
|
+
constructor(data, x) {
|
|
44
|
+
this.data = data;
|
|
45
|
+
this.x = x;
|
|
46
|
+
}
|
|
47
|
+
// Every case covered; each case's fields by name (a natural, or a value of the datatype).
|
|
48
|
+
// oxlint-disable-next-line typescript/no-explicit-any
|
|
49
|
+
match(cases) {
|
|
50
|
+
const d = this.data;
|
|
51
|
+
const built = d.ctors.map(c => {
|
|
52
|
+
const f = cases[c.name];
|
|
53
|
+
if (!f)
|
|
54
|
+
throw new Error(`${d.name}: match has no case ${c.name}`);
|
|
55
|
+
const binds = c.fields.map(fl => bindName(fl.name));
|
|
56
|
+
const fields = Object.fromEntries(c.fields.map((fl, i) => [fl.name, wrap(fl.sort === 'nat' ? 'nat' : d, { op: 'var', name: binds[i] })]));
|
|
57
|
+
return { binds, out: f(fields) };
|
|
58
|
+
});
|
|
59
|
+
const first = built[0].out;
|
|
60
|
+
const body = (o) => (typeof o === 'number' ? natX(o) : o instanceof Bool ? { op: 'bool', c: o.y } : o.x);
|
|
61
|
+
const x = { op: 'match', data: d.name, of: this.x, cases: built.map(b => ({ binds: b.binds, body: body(b.out) })) };
|
|
62
|
+
return (first instanceof Bool ? new Bool({ op: 'holds', x }) : first instanceof Val ? new Val(first.data, x) : new Nat(x));
|
|
63
|
+
}
|
|
64
|
+
is(ctor) { return new Bool({ op: 'is', data: this.data.name, of: this.x, ctor: this.data.index(ctor) }); }
|
|
65
|
+
eq(b) { return new Bool({ op: '=', a: this.x, b: b.x }); }
|
|
66
|
+
ne(b) { return this.eq(b).not(); }
|
|
67
|
+
}
|
|
68
|
+
// A value of a sort from its term.
|
|
69
|
+
export const wrap = (s, x) => (s === 'nat' ? new Nat(x) : s === 'bool' ? new Bool({ op: 'holds', x }) : new Val(s, x));
|
|
70
|
+
const xOf = (s, v) => (s === 'nat' ? natX(v) : s === 'bool' ? { op: 'bool', c: v.y } : v.x);
|
|
71
|
+
export function when(c, a, b) {
|
|
72
|
+
if (a instanceof Bool)
|
|
73
|
+
return new Bool({ op: 'or', xs: [{ op: 'and', xs: [c.y, a.y] }, { op: 'and', xs: [{ op: 'not', x: c.y }, b.y] }] });
|
|
74
|
+
const ax = typeof a === 'number' ? natX(a) : a.x, bx = typeof b === 'number' ? natX(b) : b.x;
|
|
75
|
+
const x = { op: 'if', c: c.y, then: ax, else: bx };
|
|
76
|
+
return (a instanceof Val ? new Val(a.data, x) : new Nat(x));
|
|
77
|
+
}
|
|
78
|
+
// A datatype: its constructors, each with fields that are naturals or values of the datatype itself (`self`). Its
|
|
79
|
+
// constructors are ordered as Lean's bijection numbers them: those of no field, then those with no field of the
|
|
80
|
+
// datatype, then the rest; a datatype with no constructor of the first two kinds has no value and is refused.
|
|
81
|
+
export class Data {
|
|
82
|
+
name;
|
|
83
|
+
ctors;
|
|
84
|
+
constructor(name, decl) {
|
|
85
|
+
this.name = name;
|
|
86
|
+
const all = Object.entries(decl).map(([c, fs]) => ({ name: c, fields: Object.entries(fs).map(([f, s]) => ({ name: f, sort: s === self ? 'self' : 'nat' })) }));
|
|
87
|
+
const rank = (c) => (!c.fields.length ? 0 : c.fields.every(f => f.sort === 'nat') ? 1 : 2);
|
|
88
|
+
this.ctors = [...all].sort((a, b) => rank(a) - rank(b));
|
|
89
|
+
if (!this.ctors.some(c => rank(c) < 2))
|
|
90
|
+
throw new Error(`${name}: every constructor has a field of ${name}, so it has no value`);
|
|
91
|
+
if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(name))
|
|
92
|
+
throw new Error(`${name}: a datatype's name is letters and digits`);
|
|
93
|
+
}
|
|
94
|
+
index(ctor) { const i = this.ctors.findIndex(c => c.name === ctor); if (i < 0)
|
|
95
|
+
throw new Error(`${this.name} has no constructor ${ctor}`); return i; }
|
|
96
|
+
get nullary() { return this.ctors.filter(c => !c.fields.length).length; }
|
|
97
|
+
make(ctor, ...args) {
|
|
98
|
+
const j = this.index(ctor), c = this.ctors[j];
|
|
99
|
+
if (args.length !== c.fields.length)
|
|
100
|
+
throw new Error(`${this.name}.${ctor} takes ${c.fields.length} fields`);
|
|
101
|
+
return new Val(this, { op: 'ctor', data: this.name, ctor: j, args: c.fields.map((f, i) => xOf(f.sort === 'nat' ? 'nat' : this, args[i])) });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// `data('List', {nil: {}, cons: {head: nat, tail: self}})`: `List.nil`, `List.cons(h, t)`, and `xs.match({...})`.
|
|
105
|
+
export function data(name, decl) {
|
|
106
|
+
const norm = Object.fromEntries(Object.entries(decl).map(([c, fs]) => [c, Object.fromEntries(Object.entries(fs).map(([f, s]) => [f, s === self ? self : 'nat']))]));
|
|
107
|
+
const d = new Data(name, norm);
|
|
108
|
+
for (const c of d.ctors)
|
|
109
|
+
Object.defineProperty(d, c.name, c.fields.length ? { value: (...args) => d.make(c.name, ...args) } : { get: () => d.make(c.name) });
|
|
110
|
+
return d;
|
|
111
|
+
}
|
|
112
|
+
const fns = new Map();
|
|
113
|
+
export function define(name, params, ret, body) {
|
|
114
|
+
if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(name))
|
|
115
|
+
throw new Error(`${name}: a function's name is letters and digits`);
|
|
116
|
+
const ps = Object.entries(params).map(([n, s]) => ({ name: n, sort: s }));
|
|
117
|
+
let built;
|
|
118
|
+
const fn = { name, params: ps, ret, paramFns: [], body: () => {
|
|
119
|
+
if (!built) {
|
|
120
|
+
const args = Object.fromEntries(ps.map(p => [p.name, wrap(p.sort, { op: 'var', name: `${name}.${p.name}` })]));
|
|
121
|
+
built = xOf(ret, body(args));
|
|
122
|
+
fn.decreases = decreasing(fn, built);
|
|
123
|
+
fn.paramFns = [...new Set(paramsIn(built))];
|
|
124
|
+
}
|
|
125
|
+
return built;
|
|
126
|
+
} };
|
|
127
|
+
fns.set(name, fn);
|
|
128
|
+
const call = (...args) => {
|
|
129
|
+
if (args.length !== ps.length)
|
|
130
|
+
throw new Error(`${name} takes ${ps.length} arguments`);
|
|
131
|
+
return wrap(ret, { op: 'call', fn: name, args: ps.map((p, i) => xOf(p.sort, args[i])) });
|
|
132
|
+
};
|
|
133
|
+
return Object.assign(call, { fn });
|
|
134
|
+
}
|
|
135
|
+
// An uninterpreted function of a natural (a law about every f): `const f = fnParam('f')`, `f(x)`.
|
|
136
|
+
export const fnParam = (name) => Object.assign((x) => new Nat({ op: 'apply', param: name, arg: natX(x) }), { param: name });
|
|
137
|
+
const paramsIn = (t) => { const out = []; walk(t, o => { if (o.op === 'apply')
|
|
138
|
+
out.push(o.param); }); return out; };
|
|
139
|
+
function walk(t, f) {
|
|
140
|
+
if (Array.isArray(t)) {
|
|
141
|
+
t.forEach(x => walk(x, f));
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (!t || typeof t !== 'object')
|
|
145
|
+
return;
|
|
146
|
+
const o = t;
|
|
147
|
+
f(o);
|
|
148
|
+
for (const v of Object.values(o))
|
|
149
|
+
walk(v, f);
|
|
150
|
+
}
|
|
151
|
+
// The parameter every recursive call passes a field of (bound by a match on that parameter), or a refusal.
|
|
152
|
+
function decreasing(fn, body) {
|
|
153
|
+
const calls = [];
|
|
154
|
+
const go = (x, fieldsOf) => {
|
|
155
|
+
if (!x || typeof x !== 'object')
|
|
156
|
+
return;
|
|
157
|
+
if (Array.isArray(x)) {
|
|
158
|
+
x.forEach(y => go(y, fieldsOf));
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const o = x;
|
|
162
|
+
if (o.op === 'match') {
|
|
163
|
+
go(o.of, fieldsOf);
|
|
164
|
+
const of = o.of.op === 'var' ? (fieldsOf.get(o.of.name) ?? o.of.name) : undefined;
|
|
165
|
+
o.cases.forEach(c => { const m = new Map(fieldsOf); if (of)
|
|
166
|
+
c.binds.forEach(b => m.set(b, of)); go(c.body, m); });
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (o.op === 'call' && o.fn === fn.name)
|
|
170
|
+
calls.push({ args: o.args, fieldsOf });
|
|
171
|
+
if (o.op === 'call' && o.fn !== fn.name && !fns.has(o.fn))
|
|
172
|
+
throw new Error(`${fn.name} calls ${o.fn}, which isn't defined`);
|
|
173
|
+
for (const v of Object.values(o))
|
|
174
|
+
go(v, fieldsOf);
|
|
175
|
+
};
|
|
176
|
+
go(body, new Map());
|
|
177
|
+
if (!calls.length)
|
|
178
|
+
return undefined;
|
|
179
|
+
for (const [i, p] of fn.params.entries()) {
|
|
180
|
+
if (p.sort === 'nat' || p.sort === 'bool')
|
|
181
|
+
continue;
|
|
182
|
+
const own = `${fn.name}.${p.name}`;
|
|
183
|
+
if (calls.every(c => { const a = c.args[i]; return a.op === 'var' && c.fieldsOf.get(a.name) === own; }))
|
|
184
|
+
return i;
|
|
185
|
+
}
|
|
186
|
+
throw new Error(`${fn.name}: not structural recursion (no parameter whose field every recursive call passes)`);
|
|
187
|
+
}
|
|
188
|
+
export function law(name, vars, holds) {
|
|
189
|
+
const vs = Object.entries(vars).map(([n, s]) => ({ name: n, sort: s }));
|
|
190
|
+
const y = holds(Object.fromEntries(vs.map(v => [v.name, wrap(v.sort, { op: 'var', name: v.name })]))).y;
|
|
191
|
+
return { name, vars: vs, holds: y, params: [...new Set(paramsIn(y))] };
|
|
192
|
+
}
|
|
193
|
+
const fnOf = (name) => { const f = fns.get(name); if (!f)
|
|
194
|
+
throw new Error(`no function ${name}`); return f; };
|
|
195
|
+
const asNat = (v) => v;
|
|
196
|
+
export function evalX(x, env, params) {
|
|
197
|
+
switch (x.op) {
|
|
198
|
+
case 'var': {
|
|
199
|
+
const v = env.get(x.name);
|
|
200
|
+
if (v === undefined)
|
|
201
|
+
throw new Error(`unbound ${x.name}`);
|
|
202
|
+
return v;
|
|
203
|
+
}
|
|
204
|
+
case 'nat': return x.value;
|
|
205
|
+
case 'add': return asNat(evalX(x.a, env, params)) + asNat(evalX(x.b, env, params));
|
|
206
|
+
case 'sub': return Math.max(0, asNat(evalX(x.a, env, params)) - asNat(evalX(x.b, env, params)));
|
|
207
|
+
case 'min': return Math.min(asNat(evalX(x.a, env, params)), asNat(evalX(x.b, env, params)));
|
|
208
|
+
case 'max': return Math.max(asNat(evalX(x.a, env, params)), asNat(evalX(x.b, env, params)));
|
|
209
|
+
case 'mul': return x.k * asNat(evalX(x.a, env, params));
|
|
210
|
+
case 'if': return evalY(x.c, env, params) ? evalX(x.then, env, params) : evalX(x.else, env, params);
|
|
211
|
+
case 'bool': return evalY(x.c, env, params) ? 1 : 0;
|
|
212
|
+
case 'apply': return params[x.param](asNat(evalX(x.arg, env, params)));
|
|
213
|
+
case 'ctor': return { c: x.ctor, f: x.args.map(a => evalX(a, env, params)) };
|
|
214
|
+
case 'match': {
|
|
215
|
+
const v = evalX(x.of, env, params), cs = x.cases[v.c];
|
|
216
|
+
const e2 = new Map(env);
|
|
217
|
+
cs.binds.forEach((b, i) => e2.set(b, v.f[i]));
|
|
218
|
+
return evalX(cs.body, e2, params);
|
|
219
|
+
}
|
|
220
|
+
case 'call': {
|
|
221
|
+
const f = fnOf(x.fn), body = f.body();
|
|
222
|
+
const e2 = new Map(f.params.map((p, i) => [`${f.name}.${p.name}`, evalX(x.args[i], env, params)]));
|
|
223
|
+
return evalX(body, e2, params);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
export function evalY(y, env, params) {
|
|
228
|
+
switch (y.op) {
|
|
229
|
+
case '<': return asNat(evalX(y.a, env, params)) < asNat(evalX(y.b, env, params));
|
|
230
|
+
case '<=': return asNat(evalX(y.a, env, params)) <= asNat(evalX(y.b, env, params));
|
|
231
|
+
case '=': return same(evalX(y.a, env, params), evalX(y.b, env, params));
|
|
232
|
+
case 'and': return y.xs.every(z => evalY(z, env, params));
|
|
233
|
+
case 'or': return y.xs.some(z => evalY(z, env, params));
|
|
234
|
+
case 'not': return !evalY(y.x, env, params);
|
|
235
|
+
case 'is': return evalX(y.of, env, params).c === y.ctor;
|
|
236
|
+
case 'holds': return evalX(y.x, env, params) === 1;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
const same = (a, b) => (typeof a === 'number' || typeof b === 'number' ? a === b : a.c === b.c && a.f.every((x, i) => same(x, b.f[i])));
|
|
240
|
+
export const showValue = (d, v) => {
|
|
241
|
+
if (typeof v === 'number')
|
|
242
|
+
return String(v);
|
|
243
|
+
if (d === 'nat' || d === 'bool')
|
|
244
|
+
return JSON.stringify(v);
|
|
245
|
+
const c = d.ctors[v.c];
|
|
246
|
+
return c.fields.length ? `${c.name}(${c.fields.map((f, i) => showValue(f.sort === 'nat' ? 'nat' : d, v.f[i])).join(', ')})` : c.name;
|
|
247
|
+
};
|
|
248
|
+
export const rng = (seed) => () => { seed = (seed * 1103515245 + 12345) % 2147483648; return seed / 2147483648; };
|
|
249
|
+
export function randomValue(s, rand, size, natMax = 5) {
|
|
250
|
+
if (s === 'nat')
|
|
251
|
+
return Math.floor(rand() * (natMax + 1));
|
|
252
|
+
if (s === 'bool')
|
|
253
|
+
return rand() < 0.5 ? 0 : 1;
|
|
254
|
+
const leaves = s.ctors.filter(c => c.fields.every(f => f.sort === 'nat'));
|
|
255
|
+
const pick = size <= 0 || rand() < 0.25 ? leaves : s.ctors;
|
|
256
|
+
const c = pick[Math.floor(rand() * pick.length)], j = s.ctors.indexOf(c);
|
|
257
|
+
const rec = c.fields.filter(f => f.sort === 'self').length;
|
|
258
|
+
return { c: j, f: c.fields.map(f => randomValue(f.sort === 'nat' ? 'nat' : s, rand, Math.floor((size - 1) / Math.max(1, rec)), natMax)) };
|
|
259
|
+
}
|
|
260
|
+
// A random function of a natural: a table of small values.
|
|
261
|
+
export const randomFn = (rand, natMax = 5) => { const t = new Map(); return n => { if (!t.has(n))
|
|
262
|
+
t.set(n, Math.floor(rand() * (natMax + 1))); return t.get(n); }; };
|
|
263
|
+
export function samples(vars, paramNames, n, seed = 1) {
|
|
264
|
+
const rand = rng(seed), out = [];
|
|
265
|
+
for (let i = 0; i < n; i++)
|
|
266
|
+
out.push({ env: new Map(vars.map(v => [v.name, randomValue(v.sort, rand, 1 + (i % 7))])), params: Object.fromEntries(paramNames.map(p => [p, randomFn(rand)])) });
|
|
267
|
+
return out;
|
|
268
|
+
}
|
|
269
|
+
// --- the encoding -----------------------------------------------------------------------------------------------------
|
|
270
|
+
const ev = (name) => ({ op: 'var', name });
|
|
271
|
+
const k = (value) => ({ op: 'const', value });
|
|
272
|
+
const get = (map, key) => ({ op: 'get', map, key });
|
|
273
|
+
const eqC = (a, b) => ({ op: '=', a, b });
|
|
274
|
+
const leC = (a, b) => ({ op: '<=', a, b });
|
|
275
|
+
const andC = (...xs) => (xs.length === 1 ? xs[0] : { op: 'and', xs });
|
|
276
|
+
const implies = (a, b) => ({ op: 'or', xs: [{ op: 'not', x: a }, b] });
|
|
277
|
+
const yesC = leC(k(0), k(0));
|
|
278
|
+
const pairE = (a, b) => { const s = { op: 'add', a, b }; return { op: 'add', a: { op: 'prod', a: s, b: s }, b }; };
|
|
279
|
+
export const keyOf = (args) => args.slice(1).reduce(pairE, args[0]);
|
|
280
|
+
// The arguments of a key built by keyOf (n of them), read back from its shape.
|
|
281
|
+
function argsOf(key, n) {
|
|
282
|
+
if (n === 1)
|
|
283
|
+
return [key];
|
|
284
|
+
if (key.op !== 'add' || key.a.op !== 'prod' || key.a.a.op !== 'add')
|
|
285
|
+
return undefined;
|
|
286
|
+
const left = argsOf(key.a.a.a, n - 1);
|
|
287
|
+
return left && [...left, key.b];
|
|
288
|
+
}
|
|
289
|
+
export const tagMap = (d) => `${d}.tag`, sizeMap = (d) => `${d}.size`;
|
|
290
|
+
export const fieldMap = (d, j, i) => `${d.name}.${d.ctors[j].name}.${d.ctors[j].fields[i].name}`;
|
|
291
|
+
export const ctorMap = (d, j) => `${d.name}.${d.ctors[j].name}`;
|
|
292
|
+
// Everything a law reads: the datatypes, the functions (with those they call), the function parameters.
|
|
293
|
+
export function programOf(laws, extra = []) {
|
|
294
|
+
const fs = new Map(), ds = new Map(), ps = new Set();
|
|
295
|
+
const addSort = (s) => { if (s instanceof Data)
|
|
296
|
+
ds.set(s.name, s); };
|
|
297
|
+
const visit = (t) => walk(t, o => {
|
|
298
|
+
if (o.op === 'call' && !fs.has(o.fn)) {
|
|
299
|
+
const f = fnOf(o.fn);
|
|
300
|
+
fs.set(f.name, f);
|
|
301
|
+
f.params.forEach(p => addSort(p.sort));
|
|
302
|
+
addSort(f.ret);
|
|
303
|
+
visit(f.body());
|
|
304
|
+
}
|
|
305
|
+
if (o.op === 'apply')
|
|
306
|
+
ps.add(o.param);
|
|
307
|
+
});
|
|
308
|
+
for (const l of laws) {
|
|
309
|
+
l.vars.forEach(v => addSort(v.sort));
|
|
310
|
+
visit(l.holds);
|
|
311
|
+
}
|
|
312
|
+
for (const f of extra)
|
|
313
|
+
visit({ op: 'call', fn: f.name, args: [] });
|
|
314
|
+
const datas = [...new Map([...ds.values()].map(d => [d.name, d])).values()];
|
|
315
|
+
// Functions in definition order (each after those it calls).
|
|
316
|
+
const order = [], seen = new Set();
|
|
317
|
+
const place = (f) => { if (seen.has(f.name))
|
|
318
|
+
return; seen.add(f.name); walk(f.body(), o => { if (o.op === 'call' && o.fn !== f.name)
|
|
319
|
+
place(fnOf(o.fn)); }); order.push(f); };
|
|
320
|
+
for (const f of fs.values())
|
|
321
|
+
place(f);
|
|
322
|
+
return { datas, fns: order, params: [...ps] };
|
|
323
|
+
}
|
|
324
|
+
const dataOf = (p, name) => p.datas.find(d => d.name === name);
|
|
325
|
+
export function encX(p, x, env) {
|
|
326
|
+
switch (x.op) {
|
|
327
|
+
case 'var': return env.get(x.name) ?? ev(x.name);
|
|
328
|
+
case 'nat': return k(x.value);
|
|
329
|
+
case 'add':
|
|
330
|
+
case 'sub':
|
|
331
|
+
case 'min':
|
|
332
|
+
case 'max': return { op: x.op, a: encX(p, x.a, env), b: encX(p, x.b, env) };
|
|
333
|
+
case 'mul': return { op: 'mul', k: x.k, a: encX(p, x.a, env) };
|
|
334
|
+
case 'if': return { op: 'if', c: encY(p, x.c, env), then: encX(p, x.then, env), else: encX(p, x.else, env) };
|
|
335
|
+
case 'bool': return { op: 'if', c: encY(p, x.c, env), then: k(1), else: k(0) };
|
|
336
|
+
case 'apply': return get(x.param, encX(p, x.arg, env));
|
|
337
|
+
case 'call': return get(x.fn, keyOf(x.args.map(a => encX(p, a, env))));
|
|
338
|
+
case 'ctor': {
|
|
339
|
+
const d = dataOf(p, x.data);
|
|
340
|
+
return x.args.length ? get(ctorMap(d, x.ctor), keyOf(x.args.map(a => encX(p, a, env)))) : k(x.ctor);
|
|
341
|
+
}
|
|
342
|
+
case 'match': {
|
|
343
|
+
const d = dataOf(p, x.data), of = encX(p, x.of, env);
|
|
344
|
+
const bodies = x.cases.map((c, j) => { const e2 = new Map(env); c.binds.forEach((b, i) => e2.set(b, get(fieldMap(d, j, i), of))); return encX(p, c.body, e2); });
|
|
345
|
+
return bodies.slice(0, -1).reduceRight((acc, b, j) => ({ op: 'if', c: eqC(get(tagMap(d.name), of), k(j)), then: b, else: acc }), bodies.at(-1));
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
export function encY(p, y, env) {
|
|
350
|
+
switch (y.op) {
|
|
351
|
+
case '<':
|
|
352
|
+
case '<=':
|
|
353
|
+
case '=': return { op: y.op, a: encX(p, y.a, env), b: encX(p, y.b, env) };
|
|
354
|
+
case 'and':
|
|
355
|
+
case 'or': return { op: y.op, xs: y.xs.map(z => encY(p, z, env)) };
|
|
356
|
+
case 'not': return { op: 'not', x: encY(p, y.x, env) };
|
|
357
|
+
case 'is': return eqC(get(tagMap(y.data), encX(p, y.of, env)), k(y.ctor));
|
|
358
|
+
case 'holds': return eqC(encX(p, y.x, env), k(1));
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
export function factsOf(p) {
|
|
362
|
+
const facts = [], at = new Map(), arity = new Map();
|
|
363
|
+
const put = (name, f) => { at.set(name, facts.length); facts.push({ ...f, name }); };
|
|
364
|
+
const x = ev('x$');
|
|
365
|
+
for (const d of p.datas) {
|
|
366
|
+
const tag = get(tagMap(d.name), x), size = (t) => get(sizeMap(d.name), t);
|
|
367
|
+
// Under tag j: the size, and every field of another constructor 0 (what Lean's model reads there).
|
|
368
|
+
const under = (c, j) => {
|
|
369
|
+
const sz = c.fields.flatMap((f, i) => (f.sort === 'self' ? [size(get(fieldMap(d, j, i), x))] : [])).reduce((a, b) => ({ op: 'add', a, b }), k(1));
|
|
370
|
+
const others = d.ctors.flatMap((c2, j2) => (j2 === j ? [] : c2.fields.map((_, i2) => eqC(get(fieldMap(d, j2, i2), x), k(0)))));
|
|
371
|
+
return andC(eqC(size(x), sz), ...others);
|
|
372
|
+
};
|
|
373
|
+
put(`shape ${d.name}`, { name: '', keys: ['x$'], holds: andC(leC(tag, k(d.ctors.length - 1)), ...d.ctors.map((c, j) => implies(eqC(tag, k(j)), under(c, j)))) });
|
|
374
|
+
put(`eta ${d.name}`, { name: '', keys: ['x$'], holds: andC(...d.ctors.map((c, j) => implies(eqC(tag, k(j)), eqC(x, c.fields.length ? get(ctorMap(d, j), keyOf(c.fields.map((_, i) => get(fieldMap(d, j, i), x)))) : k(j))))) });
|
|
375
|
+
if (d.nullary)
|
|
376
|
+
put(`nullary ${d.name}`, { name: '', keys: [], holds: andC(...d.ctors.slice(0, d.nullary).map((_, j) => eqC(get(tagMap(d.name), k(j)), k(j)))) });
|
|
377
|
+
for (const [j, c] of d.ctors.entries()) {
|
|
378
|
+
if (!c.fields.length)
|
|
379
|
+
continue;
|
|
380
|
+
const as = c.fields.map((_, i) => `a$${i}`), key = keyOf(as.map(ev)), cell = get(ctorMap(d, j), key);
|
|
381
|
+
arity.set(ctorMap(d, j), as.length);
|
|
382
|
+
put(`ctor ${ctorMap(d, j)}`, { name: '', keys: as, holds: andC(eqC(get(tagMap(d.name), cell), k(j)), ...c.fields.map((_, i) => eqC(get(fieldMap(d, j, i), cell), ev(as[i])))) });
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
for (const f of p.fns) {
|
|
386
|
+
const keys = f.params.map(q => `${f.name}.${q.name}`);
|
|
387
|
+
arity.set(f.name, keys.length);
|
|
388
|
+
const body = encX(p, f.body(), new Map());
|
|
389
|
+
put(`fn ${f.name}`, { name: '', keys, holds: andC(eqC(get(f.name, keyOf(keys.map(ev))), body), ...(f.ret === 'bool' ? [leC(get(f.name, keyOf(keys.map(ev))), k(1))] : [])) });
|
|
390
|
+
}
|
|
391
|
+
// Every map the encoding reads: the datatypes' tag, size and fields too (each of one argument).
|
|
392
|
+
for (const d of p.datas) {
|
|
393
|
+
arity.set(tagMap(d.name), 1);
|
|
394
|
+
arity.set(sizeMap(d.name), 1);
|
|
395
|
+
d.ctors.forEach((c, j) => c.fields.forEach((_, i) => arity.set(fieldMap(d, j, i), 1)));
|
|
396
|
+
}
|
|
397
|
+
for (const q of p.params)
|
|
398
|
+
arity.set(q, 1);
|
|
399
|
+
for (const [m, n] of arity) {
|
|
400
|
+
const as = Array.from({ length: n }, (_, i) => `a$${i}`), bs = Array.from({ length: n }, (_, i) => `b$${i}`);
|
|
401
|
+
put(`cong ${m}`, { name: '', keys: [...as, ...bs], holds: implies(andC(...as.map((a, i) => eqC(ev(a), ev(bs[i])))), eqC(get(m, keyOf(as.map(ev))), get(m, keyOf(bs.map(ev))))) });
|
|
402
|
+
}
|
|
403
|
+
return { facts, at, arity };
|
|
404
|
+
}
|
|
405
|
+
// --- the codes Lean's twin defines (lean-kernel/Data.lean, lean-kernel/data.ts) ------------------------------------------
|
|
406
|
+
// The naturals onto the pairs in order of their sum (Data.lean's cun) and back (cpair); a datatype's values as naturals:
|
|
407
|
+
// those of no field their tag, the rest z + m·q + r (z of no field, m with fields, r the constructor's place among
|
|
408
|
+
// those, q its fields packed by cpair). Every natural is a value; numbers past 2^53 are refused.
|
|
409
|
+
const tri = (s) => (s * (s + 1)) / 2;
|
|
410
|
+
const cun = (n) => { let s = Math.floor((Math.sqrt(8 * n + 1) - 1) / 2); while (tri(s) > n)
|
|
411
|
+
s--; while (tri(s + 1) <= n)
|
|
412
|
+
s++; const b = n - tri(s); return [s - b, b]; };
|
|
413
|
+
const cpair = (a, b) => tri(a + b) + b;
|
|
414
|
+
const tup = (q, k, i) => (k <= 1 ? q : i === 0 ? cun(q)[0] : tup(cun(q)[1], k - 1, i - 1));
|
|
415
|
+
const tupk = (xs) => (xs.length === 1 ? xs[0] : cpair(xs[0], tupk(xs.slice(1))));
|
|
416
|
+
const safe = (x) => { if (!Number.isSafeInteger(x))
|
|
417
|
+
throw new RangeError('past 2^53'); return x; };
|
|
418
|
+
export function decode(d, n) {
|
|
419
|
+
const z = d.nullary, m = d.ctors.length - z;
|
|
420
|
+
if (n < z)
|
|
421
|
+
return { c: n, f: [] };
|
|
422
|
+
const q = Math.floor((n - z) / m), j = z + ((n - z) % m), c = d.ctors[j];
|
|
423
|
+
return { c: j, f: c.fields.map((f, i) => { const x = tup(q, c.fields.length, i); return f.sort === 'nat' ? x : decode(d, x); }) };
|
|
424
|
+
}
|
|
425
|
+
export function encode(d, v) {
|
|
426
|
+
const o = v, z = d.nullary, m = d.ctors.length - z;
|
|
427
|
+
if (o.c < z)
|
|
428
|
+
return o.c;
|
|
429
|
+
const c = d.ctors[o.c];
|
|
430
|
+
return safe(z + m * tupk(c.fields.map((f, i) => (f.sort === 'nat' ? o.f[i] : encode(d, o.f[i])))) + (o.c - z));
|
|
431
|
+
}
|
|
432
|
+
// Each map of the encoding as a function of its arguments, on the codes (for core/cycles.ts's runs).
|
|
433
|
+
export function codeValues(p) {
|
|
434
|
+
const out = {};
|
|
435
|
+
for (const d of p.datas) {
|
|
436
|
+
out[tagMap(d.name)] = n => decode(d, n).c;
|
|
437
|
+
out[sizeMap(d.name)] = n => sizeOf(d, decode(d, n));
|
|
438
|
+
d.ctors.forEach((c, j) => {
|
|
439
|
+
c.fields.forEach((f, i) => { out[fieldMap(d, j, i)] = n => { const v = decode(d, n); return v.c === j ? (f.sort === 'nat' ? v.f[i] : encode(d, v.f[i])) : 0; }; });
|
|
440
|
+
if (c.fields.length)
|
|
441
|
+
out[ctorMap(d, j)] = (...xs) => encode(d, { c: j, f: c.fields.map((f, i) => (f.sort === 'nat' ? xs[i] : decode(d, xs[i]))) });
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
for (const f of p.fns)
|
|
445
|
+
out[f.name] = (...xs) => {
|
|
446
|
+
const v = evalX(f.body(), new Map(f.params.map((q, i) => [`${f.name}.${q.name}`, q.sort instanceof Data ? decode(q.sort, xs[i]) : xs[i]])), {});
|
|
447
|
+
return f.ret instanceof Data ? encode(f.ret, v) : v;
|
|
448
|
+
};
|
|
449
|
+
return out;
|
|
450
|
+
}
|
|
451
|
+
// That a value's fields of its datatype have smaller codes (true of the codes above: each field at most q).
|
|
452
|
+
export const orderFacts = (p) => p.datas.map(d => ({ name: `order ${d.name}`, keys: ['x$'], holds: andC(yesC, ...d.ctors.flatMap((c, j) => c.fields.flatMap((f, i) => (f.sort === 'self' ? [implies(eqC(get(tagMap(d.name), ev('x$')), k(j)), { op: '<', a: get(fieldMap(d, j, i), ev('x$')), b: ev('x$') })] : [])))) }));
|
|
453
|
+
// --- concrete models: a value of the encoding at random values (which terms are equal, for congruences and matches) ----
|
|
454
|
+
// Values interned per datatype: a constructor of no field is its tag; the rest numbered from there. A key is a
|
|
455
|
+
// pairing's natural, read back through its square root.
|
|
456
|
+
export class Model {
|
|
457
|
+
p;
|
|
458
|
+
params;
|
|
459
|
+
ids = new Map();
|
|
460
|
+
vals = new Map();
|
|
461
|
+
constructor(p, params) {
|
|
462
|
+
this.p = p;
|
|
463
|
+
this.params = params;
|
|
464
|
+
}
|
|
465
|
+
id(d, v) {
|
|
466
|
+
if (typeof v === 'number')
|
|
467
|
+
return v;
|
|
468
|
+
if (!d.ctors[v.c].fields.length)
|
|
469
|
+
return v.c;
|
|
470
|
+
const text = JSON.stringify(v);
|
|
471
|
+
let m = this.ids.get(d.name), list = this.vals.get(d.name);
|
|
472
|
+
if (!m || !list) {
|
|
473
|
+
this.ids.set(d.name, (m = new Map()));
|
|
474
|
+
this.vals.set(d.name, (list = []));
|
|
475
|
+
}
|
|
476
|
+
let i = m.get(text);
|
|
477
|
+
if (i === undefined) {
|
|
478
|
+
i = d.nullary + list.length;
|
|
479
|
+
m.set(text, i);
|
|
480
|
+
list.push(v);
|
|
481
|
+
}
|
|
482
|
+
return i;
|
|
483
|
+
}
|
|
484
|
+
value(d, id) {
|
|
485
|
+
if (id < d.nullary)
|
|
486
|
+
return { c: id, f: [] };
|
|
487
|
+
const v = this.vals.get(d.name)?.[id - d.nullary];
|
|
488
|
+
if (!v)
|
|
489
|
+
throw new Error(`${d.name}: no value numbered ${id} here`);
|
|
490
|
+
return v;
|
|
491
|
+
}
|
|
492
|
+
// A value of a sort as the encoding's natural, and back.
|
|
493
|
+
code(s, v) { return s instanceof Data ? this.id(s, v) : v; }
|
|
494
|
+
decode(s, n) { return s instanceof Data ? this.value(s, n) : n; }
|
|
495
|
+
memo = new Map();
|
|
496
|
+
read(map, key) {
|
|
497
|
+
const at = `${map}|${key}`, had = this.memo.get(at);
|
|
498
|
+
if (had !== undefined)
|
|
499
|
+
return had;
|
|
500
|
+
const out = this.readNew(map, key);
|
|
501
|
+
this.memo.set(at, out);
|
|
502
|
+
return out;
|
|
503
|
+
}
|
|
504
|
+
terms = new WeakMap();
|
|
505
|
+
readNew(map, key) {
|
|
506
|
+
const p = this.p;
|
|
507
|
+
for (const d of p.datas) {
|
|
508
|
+
if (map === tagMap(d.name))
|
|
509
|
+
return this.value(d, key).c;
|
|
510
|
+
if (map === sizeMap(d.name))
|
|
511
|
+
return sizeOf(d, this.value(d, key));
|
|
512
|
+
for (const [j, c] of d.ctors.entries()) {
|
|
513
|
+
for (const [i, f] of c.fields.entries())
|
|
514
|
+
if (map === fieldMap(d, j, i)) {
|
|
515
|
+
const v = this.value(d, key);
|
|
516
|
+
return v.c === j ? this.code(f.sort === 'nat' ? 'nat' : d, v.f[i]) : 0;
|
|
517
|
+
}
|
|
518
|
+
if (c.fields.length && map === ctorMap(d, j)) {
|
|
519
|
+
const as = unkey(key, c.fields.length);
|
|
520
|
+
return this.id(d, { c: j, f: c.fields.map((f, i) => this.decode(f.sort === 'nat' ? 'nat' : d, as[i])) });
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
const f = p.fns.find(g => g.name === map);
|
|
525
|
+
if (f) {
|
|
526
|
+
const as = unkey(key, f.params.length);
|
|
527
|
+
const env = new Map(f.params.map((q, i) => [`${f.name}.${q.name}`, this.decode(q.sort, as[i])]));
|
|
528
|
+
const out = evalX(f.body(), env, this.params);
|
|
529
|
+
return f.ret === 'nat' || f.ret === 'bool' ? out : this.id(f.ret, out);
|
|
530
|
+
}
|
|
531
|
+
const prm = this.params[map];
|
|
532
|
+
if (prm)
|
|
533
|
+
return prm(key);
|
|
534
|
+
throw new Error(`no map ${map}`);
|
|
535
|
+
}
|
|
536
|
+
evalE(e, env) {
|
|
537
|
+
let m = this.terms.get(env);
|
|
538
|
+
if (!m)
|
|
539
|
+
this.terms.set(env, (m = new WeakMap()));
|
|
540
|
+
const had = m.get(e);
|
|
541
|
+
if (had !== undefined)
|
|
542
|
+
return had;
|
|
543
|
+
const out = this.evalNew(e, env);
|
|
544
|
+
m.set(e, out);
|
|
545
|
+
return out;
|
|
546
|
+
}
|
|
547
|
+
evalNew(e, env) {
|
|
548
|
+
switch (e.op) {
|
|
549
|
+
case 'var': {
|
|
550
|
+
const v = env[e.name];
|
|
551
|
+
if (v === undefined)
|
|
552
|
+
throw new Error(`unbound ${e.name}`);
|
|
553
|
+
return v;
|
|
554
|
+
}
|
|
555
|
+
case 'const': return e.value;
|
|
556
|
+
case 'add': return this.evalE(e.a, env) + this.evalE(e.b, env);
|
|
557
|
+
case 'sub': return Math.max(0, this.evalE(e.a, env) - this.evalE(e.b, env));
|
|
558
|
+
case 'min': return Math.min(this.evalE(e.a, env), this.evalE(e.b, env));
|
|
559
|
+
case 'max': return Math.max(this.evalE(e.a, env), this.evalE(e.b, env));
|
|
560
|
+
case 'mul': return e.k * this.evalE(e.a, env);
|
|
561
|
+
case 'prod': return this.evalE(e.a, env) * this.evalE(e.b, env);
|
|
562
|
+
case 'if': return this.evalC(e.c, env) ? this.evalE(e.then, env) : this.evalE(e.else, env);
|
|
563
|
+
case 'get': return this.read(e.map, this.evalE(e.key, env));
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
evalC(c, env) {
|
|
567
|
+
switch (c.op) {
|
|
568
|
+
case '<': return this.evalE(c.a, env) < this.evalE(c.b, env);
|
|
569
|
+
case '<=': return this.evalE(c.a, env) <= this.evalE(c.b, env);
|
|
570
|
+
case '=': return this.evalE(c.a, env) === this.evalE(c.b, env);
|
|
571
|
+
case 'and': return c.xs.every(x => this.evalC(x, env));
|
|
572
|
+
case 'or': return c.xs.some(x => this.evalC(x, env));
|
|
573
|
+
case 'not': return !this.evalC(c.x, env);
|
|
574
|
+
case 'flag':
|
|
575
|
+
case 'all': throw new Error(`${c.op} isn't in the encoding`);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
export const sizeOf = (d, v) => { const o = v; return 1 + d.ctors[o.c].fields.reduce((s, f, i) => s + (f.sort === 'self' ? sizeOf(d, o.f[i]) : 0), 0); };
|
|
580
|
+
const isqrt = (n) => { let s = Math.floor(Math.sqrt(n)); while (s * s > n)
|
|
581
|
+
s--; while ((s + 1) * (s + 1) <= n)
|
|
582
|
+
s++; return s; };
|
|
583
|
+
export function unkey(key, n) {
|
|
584
|
+
if (n === 1)
|
|
585
|
+
return [key];
|
|
586
|
+
const s = isqrt(key), b = key - s * s, a = s - b;
|
|
587
|
+
return [...unkey(a, n - 1), b];
|
|
588
|
+
}
|
|
589
|
+
// Random values of the law's variables: a law false at one is refuted with it.
|
|
590
|
+
export function refute(l, n = 400, seed = 7) {
|
|
591
|
+
for (const s of samples(l.vars, programOf([l]).params, n, seed)) {
|
|
592
|
+
let ok;
|
|
593
|
+
try {
|
|
594
|
+
ok = evalY(l.holds, s.env, s.params);
|
|
595
|
+
}
|
|
596
|
+
catch (e) {
|
|
597
|
+
throw new Error(`${l.name}: ${e.message}`);
|
|
598
|
+
}
|
|
599
|
+
if (!ok)
|
|
600
|
+
return Object.fromEntries(l.vars.map(v => [v.name, showValue(v.sort, s.env.get(v.name))]));
|
|
601
|
+
}
|
|
602
|
+
return undefined;
|
|
603
|
+
}
|
|
604
|
+
function schemes(p, l) {
|
|
605
|
+
const out = [], seen = new Set();
|
|
606
|
+
const add = (s) => { const id = JSON.stringify(s.uses); if (!seen.has(id)) {
|
|
607
|
+
seen.add(id);
|
|
608
|
+
out.push(s);
|
|
609
|
+
} };
|
|
610
|
+
// Recursion induction: each call of the law to a recursive function at the law's variables, its recursive calls'
|
|
611
|
+
// arguments as the uses' substitution (where the case's tag holds).
|
|
612
|
+
walk(l.holds, o => {
|
|
613
|
+
if (o.op !== 'call')
|
|
614
|
+
return;
|
|
615
|
+
const f = fnOf(o.fn), args = o.args;
|
|
616
|
+
f.body();
|
|
617
|
+
if (f.decreases === undefined)
|
|
618
|
+
return;
|
|
619
|
+
const at = args[f.decreases];
|
|
620
|
+
if (at.op !== 'var' || !l.vars.some(v => v.name === at.name))
|
|
621
|
+
return;
|
|
622
|
+
const d = f.params[f.decreases].sort;
|
|
623
|
+
const formal = new Map(f.params.map((q, i) => [`${f.name}.${q.name}`, args[i].op === 'var' && l.vars.some(v => v.name === args[i].name) ? ev(args[i].name) : undefined]));
|
|
624
|
+
const uses = [];
|
|
625
|
+
const go = (x, env, guard) => {
|
|
626
|
+
if (!x || typeof x !== 'object')
|
|
627
|
+
return;
|
|
628
|
+
if (Array.isArray(x)) {
|
|
629
|
+
x.forEach(y => go(y, env, guard));
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
const t = x;
|
|
633
|
+
if (t.op === 'match') {
|
|
634
|
+
go(t.of, env, guard);
|
|
635
|
+
const dd = dataOf(p, t.data), of = encX(p, t.of, env);
|
|
636
|
+
t.cases.forEach((c, j) => { const e2 = new Map(env); c.binds.forEach((b, i) => e2.set(b, get(fieldMap(dd, j, i), of))); go(c.body, e2, [...guard, eqC(get(tagMap(dd.name), of), k(j))]); });
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
if (t.op === 'call' && t.fn === f.name) {
|
|
640
|
+
const subst = {};
|
|
641
|
+
let ok = true;
|
|
642
|
+
t.args.forEach((a, i) => {
|
|
643
|
+
const lv = args[i];
|
|
644
|
+
if (lv.op !== 'var' || !l.vars.some(v => v.name === lv.name)) {
|
|
645
|
+
ok = ok && JSON.stringify(a) === JSON.stringify({ op: 'var', name: `${f.name}.${f.params[i].name}` });
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
const e = encX(p, a, env);
|
|
649
|
+
if (JSON.stringify(e) !== JSON.stringify(ev(lv.name)))
|
|
650
|
+
subst[lv.name] = e;
|
|
651
|
+
});
|
|
652
|
+
if (ok && Object.keys(subst).length)
|
|
653
|
+
uses.push({ subst, guard: andC(yesC, ...guard) });
|
|
654
|
+
}
|
|
655
|
+
for (const v of Object.values(t))
|
|
656
|
+
go(v, env, guard);
|
|
657
|
+
};
|
|
658
|
+
const env = new Map([...formal].flatMap(([key, e]) => (e ? [[key, e]] : [])));
|
|
659
|
+
go(f.body(), env, []);
|
|
660
|
+
if (uses.length)
|
|
661
|
+
add({ name: `recursion of ${f.name} on ${at.name}`, on: { v: at.name, d }, uses });
|
|
662
|
+
});
|
|
663
|
+
// Structural induction on each variable of a datatype, the others fixed.
|
|
664
|
+
for (const v of l.vars) {
|
|
665
|
+
if (!(v.sort instanceof Data))
|
|
666
|
+
continue;
|
|
667
|
+
const d = v.sort, x = ev(v.name);
|
|
668
|
+
add({ name: `structure of ${v.name}`, on: { v: v.name, d }, uses: d.ctors.flatMap((c, j) => c.fields.flatMap((f, i) => (f.sort === 'self' ? [{ subst: { [v.name]: get(fieldMap(d, j, i), x) }, guard: eqC(get(tagMap(d.name), x), k(j)) }] : []))) });
|
|
669
|
+
}
|
|
670
|
+
return out;
|
|
671
|
+
}
|
|
672
|
+
function instancesOf(p, fs, w) {
|
|
673
|
+
// Past the attempt's time, what is found so far (the attempt then fails or proves with it).
|
|
674
|
+
const late = () => w.until !== undefined && performance.now() > w.until;
|
|
675
|
+
const tt = performance.now(), lap = (what) => { if (dataTrace)
|
|
676
|
+
console.error(`[data] instances: ${what} at ${Math.round(performance.now() - tt)} ms`); };
|
|
677
|
+
const insts = [], seen = new Set();
|
|
678
|
+
const inst = (fact, at) => { const i = fs.at.get(fact); if (i === undefined)
|
|
679
|
+
return; const id = JSON.stringify([i, at]); if (!seen.has(id)) {
|
|
680
|
+
seen.add(id);
|
|
681
|
+
insts.push({ fact: i, at });
|
|
682
|
+
} };
|
|
683
|
+
const holdsAt = (fact, at) => { const f = fs.facts[fs.at.get(fact)]; return substC(f.holds, { vars: Object.fromEntries(f.keys.map((key, q) => [key, at[q]])), maps: {}, flags: {} }); };
|
|
684
|
+
const reads = (t) => { const out = []; walk(t, o => { if (o.op === 'get')
|
|
685
|
+
out.push({ map: o.map, key: o.key }); }); return out; };
|
|
686
|
+
const fnOfMap = (map) => p.fns.find(g => g.name === map);
|
|
687
|
+
const ctorOf = (map) => { for (const d of p.datas)
|
|
688
|
+
for (const [j, c] of d.ctors.entries())
|
|
689
|
+
if (c.fields.length && map === ctorMap(d, j))
|
|
690
|
+
return c.fields.length; return undefined; };
|
|
691
|
+
const all = [], had = new Set();
|
|
692
|
+
const note = (rs) => { const out = []; for (const r of rs) {
|
|
693
|
+
const id = JSON.stringify(r);
|
|
694
|
+
if (!had.has(id)) {
|
|
695
|
+
had.add(id);
|
|
696
|
+
all.push(r);
|
|
697
|
+
out.push(r);
|
|
698
|
+
}
|
|
699
|
+
} return out; };
|
|
700
|
+
// Constructor facts at every constructor read, closed under the reads they bring.
|
|
701
|
+
const ctors = (rs) => {
|
|
702
|
+
let todo = rs;
|
|
703
|
+
while (todo.length) {
|
|
704
|
+
const next = [];
|
|
705
|
+
for (const r of todo) {
|
|
706
|
+
const n = ctorOf(r.map), as = n && argsOf(r.key, n);
|
|
707
|
+
if (n && as) {
|
|
708
|
+
inst(`ctor ${r.map}`, as);
|
|
709
|
+
next.push(...note(reads(holdsAt(`ctor ${r.map}`, as))));
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
todo = next;
|
|
713
|
+
}
|
|
714
|
+
};
|
|
715
|
+
const unfolded = new Set();
|
|
716
|
+
let frontier = reads(w.depth >= 3 ? [w.roots, w.others] : w.roots);
|
|
717
|
+
note(frontier);
|
|
718
|
+
note(reads(w.others));
|
|
719
|
+
ctors(all);
|
|
720
|
+
for (let round = 0; round < w.depth && frontier.length; round++) {
|
|
721
|
+
const next = [];
|
|
722
|
+
for (const r of frontier) {
|
|
723
|
+
const f = fnOfMap(r.map), as = f && argsOf(r.key, f.params.length), rid = JSON.stringify(r);
|
|
724
|
+
if (!f || !as || unfolded.has(rid))
|
|
725
|
+
continue;
|
|
726
|
+
unfolded.add(rid);
|
|
727
|
+
inst(`fn ${f.name}`, as);
|
|
728
|
+
const body = reads(holdsAt(`fn ${f.name}`, as));
|
|
729
|
+
ctors(note(body));
|
|
730
|
+
next.push(...body);
|
|
731
|
+
}
|
|
732
|
+
frontier = next;
|
|
733
|
+
}
|
|
734
|
+
lap(`unfolded, ${insts.length} instances, ${all.length} reads`);
|
|
735
|
+
const closure = (model) => {
|
|
736
|
+
const parent = new Map(), terms = new Map(), adj = new Map();
|
|
737
|
+
const find = (x) => { let r = x; while (parent.has(r))
|
|
738
|
+
r = parent.get(r); return r; };
|
|
739
|
+
const linked = new Set();
|
|
740
|
+
const edge = (x, y, cong) => {
|
|
741
|
+
const a2 = tid(x), b2 = tid(y);
|
|
742
|
+
terms.set(a2, x);
|
|
743
|
+
terms.set(b2, y);
|
|
744
|
+
if (a2 === b2)
|
|
745
|
+
return false;
|
|
746
|
+
const merged = find(a2) !== find(b2);
|
|
747
|
+
// A congruence is recorded even between reads already of one class (they may be so only through a constant).
|
|
748
|
+
if (!merged && (!cong || linked.has(a2 < b2 ? `${a2}|${b2}` : `${b2}|${a2}`)))
|
|
749
|
+
return false;
|
|
750
|
+
linked.add(a2 < b2 ? `${a2}|${b2}` : `${b2}|${a2}`);
|
|
751
|
+
if (merged)
|
|
752
|
+
parent.set(find(a2), find(b2));
|
|
753
|
+
(adj.get(a2) ?? adj.set(a2, []).get(a2)).push({ to: b2, cong });
|
|
754
|
+
(adj.get(b2) ?? adj.set(b2, []).get(b2)).push({ to: a2, cong: cong && { map: cong.map, x: cong.y, y: cong.x } });
|
|
755
|
+
return merged;
|
|
756
|
+
};
|
|
757
|
+
const truthOf = (c) => { if (!model)
|
|
758
|
+
return undefined; try {
|
|
759
|
+
return model.m.evalC(c, model.env);
|
|
760
|
+
}
|
|
761
|
+
catch {
|
|
762
|
+
return undefined;
|
|
763
|
+
} };
|
|
764
|
+
const leaves = (e) => { if (e.op !== 'if')
|
|
765
|
+
return [e]; const t = truthOf(e.c); return t === undefined ? [...leaves(e.then), ...leaves(e.else)] : leaves(t ? e.then : e.else); };
|
|
766
|
+
const active = (c) => {
|
|
767
|
+
if (c.op === 'and') {
|
|
768
|
+
c.xs.forEach(active);
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
if (c.op === 'or') {
|
|
772
|
+
if (!model) {
|
|
773
|
+
c.xs.forEach(x => { if (x.op !== 'not')
|
|
774
|
+
active(x); });
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
const open = c.xs.filter(x => truthOf(x) !== false);
|
|
778
|
+
if (open.length === 1)
|
|
779
|
+
active(open[0]);
|
|
780
|
+
else {
|
|
781
|
+
const neg = c.xs.find(x => x.op === 'not' && truthOf(x.x) === true);
|
|
782
|
+
if (neg)
|
|
783
|
+
c.xs.filter(x => x !== neg).forEach(x => { if (truthOf(x) === true)
|
|
784
|
+
active(x); });
|
|
785
|
+
}
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
if (c.op === '=' && (!model || truthOf(c) === true))
|
|
789
|
+
for (const leaf of leaves(c.b))
|
|
790
|
+
edge(c.a, leaf);
|
|
791
|
+
};
|
|
792
|
+
for (const q of insts)
|
|
793
|
+
active(holdsAt(fs.facts[q.fact].name, q.at));
|
|
794
|
+
for (const c of w.others)
|
|
795
|
+
active(c);
|
|
796
|
+
const cells = new Map();
|
|
797
|
+
for (const r of all) {
|
|
798
|
+
const n = fs.arity.get(r.map), as = n ? argsOf(r.key, n) : undefined;
|
|
799
|
+
if (as)
|
|
800
|
+
(cells.get(r.map) ?? cells.set(r.map, []).get(r.map)).push({ key: r.key, as });
|
|
801
|
+
}
|
|
802
|
+
for (let changed = true; changed && !late();) {
|
|
803
|
+
changed = false;
|
|
804
|
+
for (const [map, list] of cells) {
|
|
805
|
+
const bySig = new Map();
|
|
806
|
+
for (const x of list) {
|
|
807
|
+
const sig = x.as.map(t => find(tid(t))).join('\u0001'), y = bySig.get(sig);
|
|
808
|
+
if (!y) {
|
|
809
|
+
bySig.set(sig, x);
|
|
810
|
+
continue;
|
|
811
|
+
}
|
|
812
|
+
if (tid(y.key) !== tid(x.key) && edge(get(map, y.key), get(map, x.key), { map, x: y.as, y: x.as }))
|
|
813
|
+
changed = true;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
return { find, terms, adj, truthOf };
|
|
818
|
+
};
|
|
819
|
+
// A function read whose decreasing argument a model's classes make a constructor's value (the argument itself, or a
|
|
820
|
+
// term of its class) unfolds there too: where the definition's match is decided. Each such instance stands for the
|
|
821
|
+
// reads it was unfolded for (`reached` keeps it in a case where it is one of them).
|
|
822
|
+
if (w.depth >= 1) {
|
|
823
|
+
let extra = 0;
|
|
824
|
+
for (const model of w.models.length ? w.models : [undefined]) {
|
|
825
|
+
if (late())
|
|
826
|
+
break;
|
|
827
|
+
const cl = closure(model);
|
|
828
|
+
// Once per function and class of its arguments (this model's): a term the facts already make equal to one
|
|
829
|
+
// unfolded brings nothing new.
|
|
830
|
+
const sigOf = (fn, as) => `${fn}(${as.map(a => cl.find(tid(a))).join(',')})`;
|
|
831
|
+
const done = new Set();
|
|
832
|
+
for (const q of insts) {
|
|
833
|
+
const name = fs.facts[q.fact].name;
|
|
834
|
+
if (name.startsWith('fn '))
|
|
835
|
+
done.add(sigOf(name.slice(3), q.at));
|
|
836
|
+
}
|
|
837
|
+
for (const r of [...all]) {
|
|
838
|
+
const f = fnOfMap(r.map);
|
|
839
|
+
if (!f || f.decreases === undefined)
|
|
840
|
+
continue;
|
|
841
|
+
const as = argsOf(r.key, f.params.length);
|
|
842
|
+
if (!as)
|
|
843
|
+
continue;
|
|
844
|
+
const own = as[f.decreases], d = f.params[f.decreases].sort, root = cl.find(tid(own));
|
|
845
|
+
const built = (t) => (t.op === 'get' && d.ctors.some((c, j) => c.fields.length && t.map === ctorMap(d, j))) || (t.op === 'const' && t.value < d.nullary);
|
|
846
|
+
const cands = built(own) ? [own] : [...cl.terms.values()].filter(t => built(t) && cl.find(tid(t)) === root);
|
|
847
|
+
for (const t of cands) {
|
|
848
|
+
const as2 = as.map((a, i) => (i === f.decreases ? t : a)), rid = JSON.stringify({ map: f.name, key: keyOf(as2) }), key2 = JSON.stringify([fs.at.get(`fn ${f.name}`), as2]);
|
|
849
|
+
const anchor = { read: tid(get(f.name, r.key)), arg: own, value: t }, had = anchoredAt.get(key2) ?? [];
|
|
850
|
+
if (!had.some(x => x.read === anchor.read))
|
|
851
|
+
anchoredAt.set(key2, [...had, anchor]);
|
|
852
|
+
if (unfolded.has(rid) || extra >= 12 || done.has(sigOf(f.name, as2)))
|
|
853
|
+
continue;
|
|
854
|
+
unfolded.add(rid);
|
|
855
|
+
extra++;
|
|
856
|
+
done.add(sigOf(f.name, as2));
|
|
857
|
+
if (dataTrace)
|
|
858
|
+
console.error(`[data] unfold ${f.name} at ${JSON.stringify(as2).slice(0, 160)} for ${JSON.stringify(r.key).slice(0, 80)}`);
|
|
859
|
+
inst(`fn ${f.name}`, as2);
|
|
860
|
+
note([{ map: f.name, key: keyOf(as2) }]);
|
|
861
|
+
ctors(note(reads(holdsAt(`fn ${f.name}`, as2))));
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
lap(`by constructors, ${insts.length} instances, ${all.length} reads`);
|
|
867
|
+
const dataOfTerm = (t) => {
|
|
868
|
+
if (t.op === 'get') {
|
|
869
|
+
for (const d of p.datas)
|
|
870
|
+
if (t.map === tagMap(d.name) || t.map === sizeMap(d.name))
|
|
871
|
+
return d;
|
|
872
|
+
const f = fnOfMap(t.map);
|
|
873
|
+
if (f && f.ret instanceof Data)
|
|
874
|
+
return f.ret;
|
|
875
|
+
for (const d of p.datas)
|
|
876
|
+
for (const [j, c] of d.ctors.entries()) {
|
|
877
|
+
if (c.fields.length && t.map === ctorMap(d, j))
|
|
878
|
+
return d;
|
|
879
|
+
for (const [i, fl] of c.fields.entries())
|
|
880
|
+
if (fl.sort === 'self' && t.map === fieldMap(d, j, i))
|
|
881
|
+
return d;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
return undefined;
|
|
885
|
+
};
|
|
886
|
+
const shape = (t, d) => { const dd = d ?? dataOfTerm(t); if (dd) {
|
|
887
|
+
inst(`shape ${dd.name}`, [t]);
|
|
888
|
+
ctors(note(reads(holdsAt(`shape ${dd.name}`, [t]))));
|
|
889
|
+
} };
|
|
890
|
+
for (const t of w.shapesAt)
|
|
891
|
+
shape(t, typeOfVar(p, t));
|
|
892
|
+
if (w.depth >= 2)
|
|
893
|
+
for (const r of [...all])
|
|
894
|
+
for (const d of p.datas)
|
|
895
|
+
if (r.map === tagMap(d.name))
|
|
896
|
+
shape(r.key, d);
|
|
897
|
+
for (const t of w.etaAt) {
|
|
898
|
+
const d = typeOfVar(p, t) ?? dataOfTerm(t);
|
|
899
|
+
if (d) {
|
|
900
|
+
inst(`eta ${d.name}`, [t]);
|
|
901
|
+
shape(t, d);
|
|
902
|
+
ctors(note(reads(holdsAt(`eta ${d.name}`, [t]))));
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
// A constant's tag is read where its tag is (the nullary facts).
|
|
906
|
+
for (const d of p.datas)
|
|
907
|
+
if (d.nullary && all.some(r => r.map === tagMap(d.name))) {
|
|
908
|
+
inst(`nullary ${d.name}`, []);
|
|
909
|
+
for (let j = 0; j < d.nullary; j++)
|
|
910
|
+
all.push({ map: tagMap(d.name), key: k(j) });
|
|
911
|
+
}
|
|
912
|
+
// Congruences (a map is a function), in each model: every value the model reads explained against one of its class
|
|
913
|
+
// (the path between them, each congruence on it explained in turn); the congruences the explanations use are the
|
|
914
|
+
// instances (with `every`, those that every model's explanations use).
|
|
915
|
+
const used = new Map();
|
|
916
|
+
for (const model of w.models) {
|
|
917
|
+
if (late())
|
|
918
|
+
break;
|
|
919
|
+
const { find, adj, truthOf } = closure(model);
|
|
920
|
+
const counted = new Set(), explained = new Set();
|
|
921
|
+
const explain = (a2, b2) => {
|
|
922
|
+
const key = a2 < b2 ? `${a2}|${b2}` : `${b2}|${a2}`;
|
|
923
|
+
if (a2 === b2 || explained.has(key))
|
|
924
|
+
return;
|
|
925
|
+
explained.add(key);
|
|
926
|
+
const back = new Map([[a2, { from: '' }]]), queue = [a2];
|
|
927
|
+
// A path through no constant first: two terms the model gives one value aren't equal for that alone (each needs
|
|
928
|
+
// its own facts), and a congruence between them is what the proof can use; else one through a constant.
|
|
929
|
+
const isConst = (x) => x !== a2 && x !== b2 && x.startsWith('{"op":"const"');
|
|
930
|
+
for (const avoid of [true, false]) {
|
|
931
|
+
if (back.has(b2))
|
|
932
|
+
break;
|
|
933
|
+
back.clear();
|
|
934
|
+
back.set(a2, { from: '' });
|
|
935
|
+
queue.length = 0;
|
|
936
|
+
queue.push(a2);
|
|
937
|
+
while (queue.length && !back.has(b2)) {
|
|
938
|
+
const x = queue.shift();
|
|
939
|
+
if (avoid && isConst(x))
|
|
940
|
+
continue;
|
|
941
|
+
for (const e of adj.get(x) ?? [])
|
|
942
|
+
if (!back.has(e.to)) {
|
|
943
|
+
back.set(e.to, { from: x, cong: e.cong });
|
|
944
|
+
queue.push(e.to);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
if (!back.has(b2))
|
|
949
|
+
return;
|
|
950
|
+
for (let at = b2; at !== a2;) {
|
|
951
|
+
const step = back.get(at);
|
|
952
|
+
if (step.cong) {
|
|
953
|
+
const c = step.cong, k2 = JSON.stringify([c.map, c.x, c.y]);
|
|
954
|
+
if (!counted.has(k2)) {
|
|
955
|
+
counted.add(k2);
|
|
956
|
+
const had = used.get(k2) ?? used.get(JSON.stringify([c.map, c.y, c.x]));
|
|
957
|
+
if (had)
|
|
958
|
+
had.n++;
|
|
959
|
+
else
|
|
960
|
+
used.set(k2, { map: c.map, at: [...c.x, ...c.y], n: 1 });
|
|
961
|
+
}
|
|
962
|
+
c.x.forEach((t, q) => explain(tid(t), tid(c.y[q])));
|
|
963
|
+
}
|
|
964
|
+
at = step.from;
|
|
965
|
+
}
|
|
966
|
+
};
|
|
967
|
+
// The values this model reads: the conclusions' reads, and each fact's where the model takes it (an if's condition
|
|
968
|
+
// and taken branch, an implication's conclusion where its premise holds).
|
|
969
|
+
const read = new Set();
|
|
970
|
+
const look = (t, whole) => {
|
|
971
|
+
if (!t || typeof t !== 'object')
|
|
972
|
+
return;
|
|
973
|
+
if (Array.isArray(t)) {
|
|
974
|
+
t.forEach(x => look(x, whole));
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
const o = t;
|
|
978
|
+
if (!whole && o.op === 'if') {
|
|
979
|
+
look(o.c, whole);
|
|
980
|
+
const b2 = truthOf(o.c);
|
|
981
|
+
if (b2 !== false)
|
|
982
|
+
look(o.then, whole);
|
|
983
|
+
if (b2 !== true)
|
|
984
|
+
look(o.else, whole);
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
if (!whole && o.op === 'or' && o.xs.length === 2 && o.xs[0].op === 'not') {
|
|
988
|
+
look(o.xs[0], whole);
|
|
989
|
+
if (truthOf(o.xs[0].x) !== false)
|
|
990
|
+
look(o.xs[1], whole);
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
if (o.op === 'get') {
|
|
994
|
+
read.add(tid(o));
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
for (const v of Object.values(o))
|
|
998
|
+
look(v, whole);
|
|
999
|
+
};
|
|
1000
|
+
look([w.roots, w.others], true);
|
|
1001
|
+
look(insts.map(q => holdsAt(fs.facts[q.fact].name, q.at)), false);
|
|
1002
|
+
const byClass = new Map();
|
|
1003
|
+
for (const v of read) {
|
|
1004
|
+
const r = find(v);
|
|
1005
|
+
(byClass.get(r) ?? byClass.set(r, []).get(r)).push(v);
|
|
1006
|
+
}
|
|
1007
|
+
// Within a class, first the parts that join without a constant (each value against its part's first), then the parts.
|
|
1008
|
+
const isConstId = (x) => x.startsWith('{"op":"const"');
|
|
1009
|
+
for (const vs of byClass.values()) {
|
|
1010
|
+
if (vs.length < 2)
|
|
1011
|
+
continue;
|
|
1012
|
+
const part = new Map();
|
|
1013
|
+
for (const v of vs) {
|
|
1014
|
+
if (part.has(v))
|
|
1015
|
+
continue;
|
|
1016
|
+
const seen = new Set([v]), queue = [v];
|
|
1017
|
+
while (queue.length) {
|
|
1018
|
+
const x = queue.shift();
|
|
1019
|
+
for (const e of adj.get(x) ?? [])
|
|
1020
|
+
if (!seen.has(e.to) && !isConstId(e.to)) {
|
|
1021
|
+
seen.add(e.to);
|
|
1022
|
+
queue.push(e.to);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
for (const u of vs)
|
|
1026
|
+
if (seen.has(u) && !part.has(u))
|
|
1027
|
+
part.set(u, v);
|
|
1028
|
+
}
|
|
1029
|
+
for (const v of vs)
|
|
1030
|
+
if (part.get(v) !== v)
|
|
1031
|
+
explain(part.get(v), v);
|
|
1032
|
+
const heads = [...new Set(part.values())];
|
|
1033
|
+
for (const h of heads.slice(1))
|
|
1034
|
+
explain(heads[0], h);
|
|
1035
|
+
}
|
|
1036
|
+
if (allCong)
|
|
1037
|
+
for (const es of adj.values())
|
|
1038
|
+
for (const e of es)
|
|
1039
|
+
if (e.cong) {
|
|
1040
|
+
const k2 = JSON.stringify([e.cong.map, e.cong.x, e.cong.y]), k3 = JSON.stringify([e.cong.map, e.cong.y, e.cong.x]);
|
|
1041
|
+
if (!used.has(k2) && !used.has(k3))
|
|
1042
|
+
used.set(k2, { map: e.cong.map, at: [...e.cong.x, ...e.cong.y], n: 1 });
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
for (const d of used.values())
|
|
1046
|
+
if (!w.every || d.n >= w.models.length)
|
|
1047
|
+
inst(`cong ${d.map}`, d.at);
|
|
1048
|
+
lap(`congruences, ${insts.length} instances, ${w.models.length} models`);
|
|
1049
|
+
return insts;
|
|
1050
|
+
}
|
|
1051
|
+
let varSorts = new Map();
|
|
1052
|
+
// A term's text, once per object (terms are compared by it, and some are large).
|
|
1053
|
+
const idMemo = new WeakMap();
|
|
1054
|
+
const tid = (t) => { let out = idMemo.get(t); if (out === undefined)
|
|
1055
|
+
idMemo.set(t, (out = JSON.stringify(t))); return out; };
|
|
1056
|
+
// An instance unfolded where a read's argument equals a constructor's value: the read it stands for (for `reached`).
|
|
1057
|
+
const anchoredAt = new Map();
|
|
1058
|
+
const typeOfVar = (_p, t) => (t.op === 'var' ? varSorts.get(t.name) : undefined);
|
|
1059
|
+
// The values a law's conclusion compares (eta is wanted at each: a variable equal to a value built is the constructor at
|
|
1060
|
+
// its own fields).
|
|
1061
|
+
function valuesCompared(p, l) {
|
|
1062
|
+
const out = [];
|
|
1063
|
+
const dataTerm = (x) => (x.op === 'var' ? l.vars.some(v => v.name === x.name && v.sort instanceof Data) : x.op === 'call' ? fnOf(x.fn).ret instanceof Data : x.op === 'ctor');
|
|
1064
|
+
walk(l.holds, o => { if (o.op === '=' && dataTerm(o.a))
|
|
1065
|
+
for (const side of [o.a, o.b])
|
|
1066
|
+
if (side.op === 'var')
|
|
1067
|
+
out.push(encX(p, side, new Map())); });
|
|
1068
|
+
return out;
|
|
1069
|
+
}
|
|
1070
|
+
// Where a lemma's terms appear in a goal: a substitution of its variables by terms of the goal, for each term of the goal
|
|
1071
|
+
// that one of the lemma's sides matches (its variables read anywhere, the rest exactly).
|
|
1072
|
+
function matches(pattern, vars, t, s) {
|
|
1073
|
+
if (pattern.op === 'var' && vars.has(pattern.name)) {
|
|
1074
|
+
const had = s[pattern.name];
|
|
1075
|
+
if (had)
|
|
1076
|
+
return JSON.stringify(had) === JSON.stringify(t) ? s : undefined;
|
|
1077
|
+
return { ...s, [pattern.name]: t };
|
|
1078
|
+
}
|
|
1079
|
+
if (pattern.op !== t.op)
|
|
1080
|
+
return undefined;
|
|
1081
|
+
const po = pattern, to = t;
|
|
1082
|
+
let out = s;
|
|
1083
|
+
for (const key of Object.keys(po)) {
|
|
1084
|
+
if (!out)
|
|
1085
|
+
return undefined;
|
|
1086
|
+
const a = po[key], b = to[key];
|
|
1087
|
+
if (a && typeof a === 'object' && 'op' in a) {
|
|
1088
|
+
if (!b || typeof b !== 'object')
|
|
1089
|
+
return undefined;
|
|
1090
|
+
if (a.op && ['<', '<=', '=', 'and', 'or', 'not'].includes(a.op))
|
|
1091
|
+
out = JSON.stringify(a) === JSON.stringify(b) ? out : undefined;
|
|
1092
|
+
else
|
|
1093
|
+
out = matches(a, vars, b, out);
|
|
1094
|
+
}
|
|
1095
|
+
else if (JSON.stringify(a) !== JSON.stringify(b))
|
|
1096
|
+
return undefined;
|
|
1097
|
+
}
|
|
1098
|
+
return out;
|
|
1099
|
+
}
|
|
1100
|
+
function casesOf(p, unfolded, vars, models) {
|
|
1101
|
+
const plain = (e) => (e.op === 'var' ? vars.has(e.name) : e.op === 'const' ? true : e.op === 'get' ? p.datas.some(d => e.map.startsWith(`${d.name}.`)) && !p.datas.some(d => d.ctors.some((c, j) => c.fields.length > 0 && e.map === ctorMap(d, j))) && plain(e.key)
|
|
1102
|
+
: e.op === 'add' || e.op === 'sub' || e.op === 'min' || e.op === 'max' ? plain(e.a) && plain(e.b) : e.op === 'mul' ? plain(e.a) : false);
|
|
1103
|
+
const plainC = (c) => JSON.stringify(c).includes('"op":"var"') && (c.op === '<' || c.op === '<=' || c.op === '=' ? plain(c.a) && plain(c.b) : c.op === 'not' ? plainC(c.x) : c.op === 'and' || c.op === 'or' ? c.xs.every(plainC) : false);
|
|
1104
|
+
// The functions' facts the goal unfolds.
|
|
1105
|
+
const roots = unfolded;
|
|
1106
|
+
const sigs = models.map(({ m, env }) => {
|
|
1107
|
+
const out = new Map();
|
|
1108
|
+
const truthOf = (c) => { try {
|
|
1109
|
+
return m.evalC(c, env);
|
|
1110
|
+
}
|
|
1111
|
+
catch {
|
|
1112
|
+
return undefined;
|
|
1113
|
+
} };
|
|
1114
|
+
const go = (t) => {
|
|
1115
|
+
if (!t || typeof t !== 'object')
|
|
1116
|
+
return;
|
|
1117
|
+
if (Array.isArray(t)) {
|
|
1118
|
+
t.forEach(go);
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
const o = t;
|
|
1122
|
+
if (o.op === 'if') {
|
|
1123
|
+
const v = truthOf(o.c);
|
|
1124
|
+
if (v === undefined)
|
|
1125
|
+
return;
|
|
1126
|
+
if (plainC(o.c)) {
|
|
1127
|
+
const key = JSON.stringify(o.c);
|
|
1128
|
+
if (!out.has(key))
|
|
1129
|
+
out.set(key, { c: o.c, t: v });
|
|
1130
|
+
}
|
|
1131
|
+
go(v ? o.then : o.else);
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
for (const val of Object.values(o))
|
|
1135
|
+
go(val);
|
|
1136
|
+
};
|
|
1137
|
+
go(roots);
|
|
1138
|
+
return out;
|
|
1139
|
+
});
|
|
1140
|
+
const build = (ms, lits, decided, depth) => {
|
|
1141
|
+
const count = new Map();
|
|
1142
|
+
for (const i of ms)
|
|
1143
|
+
for (const key of sigs[i].keys())
|
|
1144
|
+
if (!decided.has(key))
|
|
1145
|
+
count.set(key, (count.get(key) ?? 0) + 1);
|
|
1146
|
+
const pick = [...count.entries()].sort((a2, b2) => b2[1] - a2[1])[0];
|
|
1147
|
+
if (!pick || depth >= 4)
|
|
1148
|
+
return [{ lits, models: ms.length ? ms.map(i => models[i]) : undefined }];
|
|
1149
|
+
const key = pick[0], c = ms.map(i => sigs[i].get(key)).find(x => x).c;
|
|
1150
|
+
const side = (t) => ms.filter(i => sigs[i].get(key)?.t !== !t);
|
|
1151
|
+
const d2 = new Set([...decided, key]);
|
|
1152
|
+
// A guard every one of many models takes one way is not split (its other side is likely no case at all).
|
|
1153
|
+
if (ms.length >= 20 && (!side(true).length || !side(false).length))
|
|
1154
|
+
return build(ms, lits, d2, depth);
|
|
1155
|
+
return [true, false].flatMap(t => { const s2 = side(t), lit = t ? c : { op: 'not', x: c }; return s2.length ? build(s2, [...lits, lit], d2, depth + 1) : [{ lits: [...lits, lit], models: undefined, near: ms.map(i => models[i]) }]; });
|
|
1156
|
+
};
|
|
1157
|
+
return build(models.map((_, i) => i), [], new Set(), 0);
|
|
1158
|
+
}
|
|
1159
|
+
// One attempt: the law's node (and the lemmas', proven before, with their own nodes) at a scheme and a depth. With
|
|
1160
|
+
// several cases, a main node uses a node per case (each its guards as hypotheses, its own instances), at rank
|
|
1161
|
+
// (level, size, 1) above theirs (level, size, 0); a case node uses the main node at the children.
|
|
1162
|
+
const dataTrace = !!globalThis.process?.env?.DATA_TRACE;
|
|
1163
|
+
// Debugging: every congruence a model's closure makes, not only those explanations use.
|
|
1164
|
+
const allCong = !!globalThis.process?.env?.DATA_ALLCONG;
|
|
1165
|
+
function attempt(p, fs, l, scheme, depth, lemmas, level, pool) {
|
|
1166
|
+
const t0 = performance.now();
|
|
1167
|
+
anchoredAt.clear();
|
|
1168
|
+
// A few models for the instances; the whole pool for the cases (a rare case gets its own models from it).
|
|
1169
|
+
const models = pool.slice(0, 24), few = (ms) => ms.slice(0, 16);
|
|
1170
|
+
const concl = encY(p, l.holds, new Map());
|
|
1171
|
+
const specs = [], where = new Map();
|
|
1172
|
+
for (const lm of lemmas) {
|
|
1173
|
+
const local = new Map();
|
|
1174
|
+
lm.nodes.forEach((n, i) => {
|
|
1175
|
+
if (where.has(n.name)) {
|
|
1176
|
+
local.set(i, where.get(n.name));
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
local.set(i, specs.length);
|
|
1180
|
+
where.set(n.name, specs.length);
|
|
1181
|
+
specs.push({ ...n, uses: n.uses.map(u => ({ ...u, node: local.get(u.node) ?? u.node })) });
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
const me = specs.length, name = `${l.name}${scheme ? ` (${scheme.name})` : ''}`;
|
|
1185
|
+
varSorts = new Map(l.vars.flatMap(v => (v.sort instanceof Data ? [[v.name, v.sort]] : [])));
|
|
1186
|
+
const size = scheme?.on ? get(sizeMap(scheme.on.d.name), ev(scheme.on.v)) : k(0);
|
|
1187
|
+
const rank = (sub) => [k(level), size, k(sub)];
|
|
1188
|
+
const ih = (scheme?.uses ?? []).map(u => ({ node: me, subst: u.subst, guard: u.guard }));
|
|
1189
|
+
const shapesAt = scheme?.on ? [ev(scheme.on.v)] : [];
|
|
1190
|
+
const ihConcls = ih.map(u => substC(concl, { vars: u.subst, maps: {}, flags: {} }));
|
|
1191
|
+
// Uses of the lemmas, where a lemma's largest read matches a read of this goal (unfolded), its key up to the equalities
|
|
1192
|
+
// the unfolded definitions give (any branch); twice over, the uses' own conclusions read the second time.
|
|
1193
|
+
const pre = instancesOf(p, fs, { roots: [concl], others: ihConcls, depth, shapesAt, etaAt: [], models: [], until: t0 + attemptMs.ms });
|
|
1194
|
+
const lemmaUses = lemmaUsesFor(p, fs, lemmas.map(lm => ({ law: lm.law, node: where.get(lm.spec.name), concl: specs[where.get(lm.spec.name)].concl })), [concl, ihConcls], pre, models);
|
|
1195
|
+
const uses = [...ih, ...lemmaUses];
|
|
1196
|
+
if (dataTrace)
|
|
1197
|
+
for (const u of lemmaUses)
|
|
1198
|
+
console.error(`[data] lemma use ${specs[u.node].name.slice(0, 40)} at ${Object.entries(u.subst).map(([x, t]) => `${x} := ${JSON.stringify(t).slice(0, 80)}`).join('; ')}`);
|
|
1199
|
+
const useConcl = (u) => substC(u.node === me ? concl : specs[u.node].concl, { vars: u.subst, maps: {}, flags: {} });
|
|
1200
|
+
// The lemmas' conclusions are unfolded like the law's (they are what the goal reads, said another way); the cycle's
|
|
1201
|
+
// own uses only from depth 3.
|
|
1202
|
+
const instsFor = (ms, us, every = false) => instancesOf(p, fs, { roots: [concl], others: us.map(useConcl), depth, shapesAt, etaAt: [...shapesAt, ...valuesCompared(p, l)], models: ms, every, until: t0 + attemptMs.ms });
|
|
1203
|
+
const cases = scheme ? casesOf(p, pre.filter(q => fs.facts[q.fact].name.startsWith('fn ')).map(q => holdsAt(fs, q)), new Set(l.vars.map(v => v.name)), pool) : [];
|
|
1204
|
+
if (cases.length <= 1) {
|
|
1205
|
+
specs.push({ name, hyps: [], concl, rank: rank(0), insts: instsFor(models, uses), uses });
|
|
1206
|
+
}
|
|
1207
|
+
else {
|
|
1208
|
+
specs.push({ name, hyps: [], concl, rank: rank(1), insts: [], uses: cases.map((c, i) => ({ node: me + 1 + i, subst: {}, guard: andC(yesC, ...c.lits) })) });
|
|
1209
|
+
for (const [i, c] of cases.entries()) {
|
|
1210
|
+
// The uses whose guard some model of the case meets (all, for a case no model reaches).
|
|
1211
|
+
const holdsIn = (g) => !c.models || c.models.some(({ m, env }) => { try {
|
|
1212
|
+
return m.evalC(g, env);
|
|
1213
|
+
}
|
|
1214
|
+
catch {
|
|
1215
|
+
return true;
|
|
1216
|
+
} });
|
|
1217
|
+
// A lemma's use where its read is the goal's read it matched, in every model of the case.
|
|
1218
|
+
const applies = (u) => { const at = matchedAt.get(u); return !at || !c.models || c.models.every(({ m, env }) => { try {
|
|
1219
|
+
return m.evalE(at.goal, env) === m.evalE(at.lemma, env);
|
|
1220
|
+
}
|
|
1221
|
+
catch {
|
|
1222
|
+
return false;
|
|
1223
|
+
} }); };
|
|
1224
|
+
const us = uses.filter(u => holdsIn(u.guard) && applies(u));
|
|
1225
|
+
const ms = few(c.models ?? c.near ?? models);
|
|
1226
|
+
specs.push({ name: `${name}, case ${i + 1}`, hyps: c.lits, concl, rank: rank(0), insts: reached(p, fs, instsFor(ms, us), [c.lits, concl, us.map(u => [u.guard, useConcl(u)]), shapesAt], ms), uses: us });
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
// Each attempt within its own time (a goal past it is left unknown, and the attempt fails).
|
|
1230
|
+
if (dataTrace)
|
|
1231
|
+
console.error(`[data] @${Math.round(performance.now())} ${l.name} (${scheme?.name}) depth ${depth}, ${lemmas.length} lemmas: built in ${Math.round(performance.now() - t0)} ms, ${specs.length} nodes, ${specs.reduce((n, x) => n + x.insts.length, 0)} instances`);
|
|
1232
|
+
if (performance.now() > t0 + attemptMs.ms)
|
|
1233
|
+
return { verdict: { proven: false, checked: false, node: name, goal: name, stuck: 'unknown' }, specs };
|
|
1234
|
+
const was = pairedKeys.skip;
|
|
1235
|
+
pairedKeys.skip = true;
|
|
1236
|
+
try {
|
|
1237
|
+
return { verdict: withDeadline(t0 + attemptMs.ms, () => proveCyclic(fs.facts, specs)), specs };
|
|
1238
|
+
}
|
|
1239
|
+
finally {
|
|
1240
|
+
pairedKeys.skip = was;
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
export const attemptMs = { ms: 8000 };
|
|
1244
|
+
// A case node's instances, those its models reach: from every read of its conclusion and its uses' (and their guards),
|
|
1245
|
+
// through each instance at a read reached (a function's at its call, a constructor's at its cell, a shape or eta at a
|
|
1246
|
+
// value whose tag is read), the reads of what it gives where the models take it (an if's taken branch, an implication's
|
|
1247
|
+
// conclusion where its premise holds); a congruence where both its reads are reached.
|
|
1248
|
+
function reached(p, fs, insts, from, ms) {
|
|
1249
|
+
const reach = new Set(), id = (t) => JSON.stringify(t);
|
|
1250
|
+
const active = (t, m, env) => {
|
|
1251
|
+
if (!t || typeof t !== 'object')
|
|
1252
|
+
return;
|
|
1253
|
+
if (Array.isArray(t)) {
|
|
1254
|
+
t.forEach(x => active(x, m, env));
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1257
|
+
const o = t;
|
|
1258
|
+
const truth = (c) => { try {
|
|
1259
|
+
return m.evalC(c, env);
|
|
1260
|
+
}
|
|
1261
|
+
catch {
|
|
1262
|
+
return undefined;
|
|
1263
|
+
} };
|
|
1264
|
+
if (o.op === 'if') {
|
|
1265
|
+
active(o.c, m, env);
|
|
1266
|
+
const v = truth(o.c);
|
|
1267
|
+
if (v !== false)
|
|
1268
|
+
active(o.then, m, env);
|
|
1269
|
+
if (v !== true)
|
|
1270
|
+
active(o.else, m, env);
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1273
|
+
if (o.op === 'or' && o.xs.length === 2 && o.xs[0].op === 'not') {
|
|
1274
|
+
active(o.xs[0], m, env);
|
|
1275
|
+
if (truth(o.xs[0].x) !== false)
|
|
1276
|
+
active(o.xs[1], m, env);
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
if (o.op === 'get')
|
|
1280
|
+
reach.add(id(o));
|
|
1281
|
+
for (const v of Object.values(o))
|
|
1282
|
+
active(v, m, env);
|
|
1283
|
+
};
|
|
1284
|
+
const each = (t) => { for (const { m, env } of ms)
|
|
1285
|
+
active(t, m, env); };
|
|
1286
|
+
// The conclusion and the uses' are read whole: the goal is where the law fails, which no model is.
|
|
1287
|
+
walk(from, o => { if (o.op === 'get')
|
|
1288
|
+
reach.add(JSON.stringify(o)); });
|
|
1289
|
+
const anchor = (q) => {
|
|
1290
|
+
const name = fs.facts[q.fact].name;
|
|
1291
|
+
if (name.startsWith('fn '))
|
|
1292
|
+
return [get(name.slice(3), keyOf(q.at))];
|
|
1293
|
+
if (name.startsWith('ctor '))
|
|
1294
|
+
return [get(name.slice(5), keyOf(q.at))];
|
|
1295
|
+
if (name.startsWith('shape ') || name.startsWith('eta ')) {
|
|
1296
|
+
const d = name.split(' ')[1];
|
|
1297
|
+
return [get(tagMap(d), q.at[0]), get(sizeMap(d), q.at[0]), ...p.datas.filter(x => x.name === d).flatMap(x => x.ctors.flatMap((c, j) => c.fields.map((_, i) => get(fieldMap(x, j, i), q.at[0]))))];
|
|
1298
|
+
}
|
|
1299
|
+
return [];
|
|
1300
|
+
};
|
|
1301
|
+
const kept = new Set();
|
|
1302
|
+
// A constant's tag (the nullary facts) is read wherever its constant is.
|
|
1303
|
+
for (const [i, q] of insts.entries())
|
|
1304
|
+
if (fs.facts[q.fact].name.startsWith('nullary ')) {
|
|
1305
|
+
kept.add(i);
|
|
1306
|
+
each(holdsAt(fs, q));
|
|
1307
|
+
}
|
|
1308
|
+
for (let changed = true; changed;) {
|
|
1309
|
+
changed = false;
|
|
1310
|
+
for (const [i, q] of insts.entries()) {
|
|
1311
|
+
if (kept.has(i))
|
|
1312
|
+
continue;
|
|
1313
|
+
const name = fs.facts[q.fact].name;
|
|
1314
|
+
if (name.startsWith('cong ') || name.startsWith('nullary '))
|
|
1315
|
+
continue;
|
|
1316
|
+
const virtual = anchoredAt.get(JSON.stringify([q.fact, q.at])) ?? [];
|
|
1317
|
+
const same = (v) => ms.every(({ m, env }) => { try {
|
|
1318
|
+
return m.evalE(v.arg, env) === m.evalE(v.value, env);
|
|
1319
|
+
}
|
|
1320
|
+
catch {
|
|
1321
|
+
return false;
|
|
1322
|
+
} });
|
|
1323
|
+
if (!anchor(q).some(a => reach.has(id(a))) && !virtual.some(v => reach.has(v.read) && same(v)))
|
|
1324
|
+
continue;
|
|
1325
|
+
kept.add(i);
|
|
1326
|
+
changed = true;
|
|
1327
|
+
each(holdsAt(fs, q));
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
return insts.filter((q, i) => {
|
|
1331
|
+
if (kept.has(i))
|
|
1332
|
+
return true;
|
|
1333
|
+
const name = fs.facts[q.fact].name;
|
|
1334
|
+
if (name.startsWith('nullary '))
|
|
1335
|
+
return true;
|
|
1336
|
+
if (!name.startsWith('cong '))
|
|
1337
|
+
return false;
|
|
1338
|
+
const m = name.slice(5), n = q.at.length / 2;
|
|
1339
|
+
return reach.has(id(get(m, keyOf(q.at.slice(0, n))))) && reach.has(id(get(m, keyOf(q.at.slice(n)))));
|
|
1340
|
+
});
|
|
1341
|
+
}
|
|
1342
|
+
// The uses of lemmas a goal's reads match: each lemma's largest reads (those inside no other) matched against the goal's
|
|
1343
|
+
// reads of the same map at keys equal, by the instances' equations (every branch), to the read's key.
|
|
1344
|
+
function lemmaUsesFor(p, fs, lemmas, from, insts, models = []) {
|
|
1345
|
+
const id = tid, parent = new Map(), terms = new Map();
|
|
1346
|
+
const find = (x) => { let r = x; while (parent.has(r))
|
|
1347
|
+
r = parent.get(r); return r; };
|
|
1348
|
+
const union = (x, y) => { terms.set(id(x), x); terms.set(id(y), y); const a = find(id(x)), b = find(id(y)); if (a !== b)
|
|
1349
|
+
parent.set(a, b); };
|
|
1350
|
+
const leaves = (e) => (e.op === 'if' ? [...leaves(e.then), ...leaves(e.else)] : [e]);
|
|
1351
|
+
for (const q of insts)
|
|
1352
|
+
for (const [a, b] of equalities(holdsAt(fs, q)))
|
|
1353
|
+
for (const leaf of leaves(b))
|
|
1354
|
+
union(a, leaf);
|
|
1355
|
+
// Closed under congruence (two reads of one map at arguments of one class), by signatures.
|
|
1356
|
+
const cells = new Map();
|
|
1357
|
+
walk([from, insts.map(q => holdsAt(fs, q))], o => { if (o.op !== 'get')
|
|
1358
|
+
return; const n = fs.arity.get(o.map), as = n ? argsOf(o.key, n) : undefined; if (as)
|
|
1359
|
+
(cells.get(o.map) ?? cells.set(o.map, []).get(o.map)).push({ key: o.key, as }); });
|
|
1360
|
+
for (let changed = true, rounds = 0; changed && rounds < 8; rounds++) {
|
|
1361
|
+
changed = false;
|
|
1362
|
+
for (const [map, list] of cells) {
|
|
1363
|
+
const bySig = new Map();
|
|
1364
|
+
for (const x of list) {
|
|
1365
|
+
const sig = x.as.map(t => find(id(t))).join('\u0001'), y = bySig.get(sig), cx = get(map, x.key);
|
|
1366
|
+
if (!y) {
|
|
1367
|
+
bySig.set(sig, cx);
|
|
1368
|
+
continue;
|
|
1369
|
+
}
|
|
1370
|
+
if (find(id(y)) !== find(id(cx))) {
|
|
1371
|
+
union(y, cx);
|
|
1372
|
+
changed = true;
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
const classes = new Map();
|
|
1378
|
+
for (const [key, e] of terms) {
|
|
1379
|
+
const r = find(key);
|
|
1380
|
+
(classes.get(r) ?? classes.set(r, []).get(r)).push(e);
|
|
1381
|
+
}
|
|
1382
|
+
const members = (t) => { const k0 = tid(t); return [t, ...(classes.get(find(k0)) ?? []).filter(e => tid(e) !== k0)]; };
|
|
1383
|
+
const out = [], seen = new Set();
|
|
1384
|
+
let reads = [];
|
|
1385
|
+
walk([from, insts.map(q => holdsAt(fs, q))], o => { if (o.op === 'get')
|
|
1386
|
+
reads.push(o); });
|
|
1387
|
+
for (let round = 0; round < 2; round++) {
|
|
1388
|
+
const fresh = [];
|
|
1389
|
+
// The newest lemma first (the one the law is tried again for).
|
|
1390
|
+
for (const lm of [...lemmas].reverse()) {
|
|
1391
|
+
if (round > 0 && out.some(u => u.node === lm.node))
|
|
1392
|
+
continue;
|
|
1393
|
+
const vars = new Set(lm.law.vars.map(v => v.name)), sides = [];
|
|
1394
|
+
walk(encY(p, lm.law.holds, new Map()), o => { if (o.op === 'get')
|
|
1395
|
+
sides.push(o); });
|
|
1396
|
+
const tops = sides.filter(a => !sides.some(b => b !== a && JSON.stringify(b).includes(JSON.stringify(a))));
|
|
1397
|
+
for (const top of tops)
|
|
1398
|
+
for (const t of reads) {
|
|
1399
|
+
if (t.op !== 'get' || t.map !== top.map)
|
|
1400
|
+
continue;
|
|
1401
|
+
// Each argument of the read matched up to its class (a key of several arguments is their pairing).
|
|
1402
|
+
const n = fs.arity.get(top.map) ?? 1, pats = argsOf(top.key, n), args = argsOf(t.key, n);
|
|
1403
|
+
if (!pats || !args)
|
|
1404
|
+
continue;
|
|
1405
|
+
let subs = [{}];
|
|
1406
|
+
for (const [q, pat] of pats.entries())
|
|
1407
|
+
subs = subs.flatMap(sb => members(args[q]).slice(0, 256).flatMap(m => { const r = matches(pat, vars, m, sb); return r ? [r] : []; })).slice(0, 512);
|
|
1408
|
+
for (const sub of subs) {
|
|
1409
|
+
if (Object.keys(sub).length !== vars.size)
|
|
1410
|
+
continue;
|
|
1411
|
+
// Where a model gives the lemma's arguments the goal read's (a match through every branch's classes may be none).
|
|
1412
|
+
const there = pats.map(a => substE(a, { vars: sub, maps: {}, flags: {} }));
|
|
1413
|
+
if (models.length && !models.some(({ m, env }) => { try {
|
|
1414
|
+
return there.every((a, q) => m.evalE(a, env) === m.evalE(args[q], env));
|
|
1415
|
+
}
|
|
1416
|
+
catch {
|
|
1417
|
+
return false;
|
|
1418
|
+
} }))
|
|
1419
|
+
continue;
|
|
1420
|
+
const k2 = JSON.stringify([lm.node, sub]);
|
|
1421
|
+
if (seen.has(k2) || out.length >= 12 || out.filter(u => u.node === lm.node).length >= 4)
|
|
1422
|
+
continue;
|
|
1423
|
+
seen.add(k2);
|
|
1424
|
+
out.push({ node: lm.node, subst: sub, guard: yesC });
|
|
1425
|
+
matchedAt.set(out.at(-1), { goal: t, lemma: substE(top, { vars: sub, maps: {}, flags: {} }) });
|
|
1426
|
+
walk(substC(lm.concl, { vars: sub, maps: {}, flags: {} }), o => { if (o.op === 'get')
|
|
1427
|
+
fresh.push(o); });
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
reads = fresh;
|
|
1432
|
+
}
|
|
1433
|
+
return out;
|
|
1434
|
+
}
|
|
1435
|
+
// The equalities a condition may give: those in a positive place (a conjunction's parts, a disjunction's disjuncts that
|
|
1436
|
+
// aren't negations, so an implication's conclusion and never its premise).
|
|
1437
|
+
function equalities(c) {
|
|
1438
|
+
if (c.op === '=')
|
|
1439
|
+
return [[c.a, c.b]];
|
|
1440
|
+
if (c.op === 'and' || c.op === 'or')
|
|
1441
|
+
return c.xs.flatMap(x => (x.op === 'not' ? [] : equalities(x)));
|
|
1442
|
+
return [];
|
|
1443
|
+
}
|
|
1444
|
+
// A lemma use: the goal's read it matched, and the lemma's read there (equal where the use applies).
|
|
1445
|
+
const matchedAt = new WeakMap();
|
|
1446
|
+
const holdsAt = (fs, q) => { const f = fs.facts[q.fact]; return substC(f.holds, { vars: Object.fromEntries(f.keys.map((key, i) => [key, q.at[i]])), maps: {}, flags: {} }); };
|
|
1447
|
+
// The models a search compares terms in: random values of the law's variables.
|
|
1448
|
+
function modelsFor(p, l, n, seed) {
|
|
1449
|
+
return samples(l.vars, p.params, n, seed).map(s => {
|
|
1450
|
+
const m = new Model(p, s.params);
|
|
1451
|
+
const env = Object.fromEntries(l.vars.map(v => [v.name, m.code(v.sort, s.env.get(v.name))]));
|
|
1452
|
+
return { m, env };
|
|
1453
|
+
});
|
|
1454
|
+
}
|
|
1455
|
+
// Prove a law with the lemmas given (each proven, with its nodes), trying each scheme at each depth.
|
|
1456
|
+
function proveWith(p, fs, l, lemmas, level, opts, until, per, depths) {
|
|
1457
|
+
const models = modelsFor(p, l, 240, opts.seed ?? 3);
|
|
1458
|
+
let tried = 0, last;
|
|
1459
|
+
const ss = opts.induction === false ? [undefined] : schemes(p, l);
|
|
1460
|
+
for (const depth of depths.filter(d => d <= (opts.depth ?? 3)))
|
|
1461
|
+
for (const s of ss) {
|
|
1462
|
+
if (performance.now() > until)
|
|
1463
|
+
return { tried, last };
|
|
1464
|
+
tried++;
|
|
1465
|
+
const { verdict, specs } = (attemptMs.ms = Math.min(per * (opts.patience ?? 1), until - performance.now()), attempt(p, fs, l, s, depth, lemmas, level, models));
|
|
1466
|
+
last = verdict;
|
|
1467
|
+
if (verdict.proven && verdict.checked)
|
|
1468
|
+
return { out: { law: l, spec: specs.find(n => n.name === specs[specs.length - 1].name.replace(/, case \d+$/, '')), nodes: specs }, verdict, scheme: s?.name, tried };
|
|
1469
|
+
}
|
|
1470
|
+
return { tried, last };
|
|
1471
|
+
}
|
|
1472
|
+
// Conjectures from the values (theory exploration): equations between terms of the law's functions over a few
|
|
1473
|
+
// variables (two of each datatype, two naturals; constructors, the functions, sums of naturals, up to five symbols), met
|
|
1474
|
+
// by every random value, smallest first, each named canonically (its variables in order of appearance), none ground,
|
|
1475
|
+
// none the law, none that follows by rewriting with smaller ones; and for each predicate the law reads, that a function
|
|
1476
|
+
// returning a datatype keeps it (p(x) implies p(f(x, ...)), under a comparison of two naturals where that is needed).
|
|
1477
|
+
export function conjectures(p, l, max = 24, maxSize = 6) {
|
|
1478
|
+
const sortsOf = (s) => (s instanceof Data ? s.name : s);
|
|
1479
|
+
const vars = [...p.datas.flatMap(d => [{ name: `${d.name.toLowerCase()}1`, sort: d }, { name: `${d.name.toLowerCase()}2`, sort: d }]), { name: 'n1', sort: 'nat' }, { name: 'n2', sort: 'nat' }];
|
|
1480
|
+
const sortOfVar = new Map(vars.map(v => [v.name, v.sort]));
|
|
1481
|
+
const ss = samples(vars, p.params, 80, 11);
|
|
1482
|
+
const used = new Set();
|
|
1483
|
+
walk(l.holds, o => { if (o.op === 'call')
|
|
1484
|
+
used.add(o.fn); });
|
|
1485
|
+
const callees = (f) => { const out = []; walk(f.body(), o => { if (o.op === 'call')
|
|
1486
|
+
out.push(o.fn); }); return out; };
|
|
1487
|
+
for (let changed = true; changed;) {
|
|
1488
|
+
changed = false;
|
|
1489
|
+
for (const f of p.fns)
|
|
1490
|
+
if (used.has(f.name))
|
|
1491
|
+
for (const g of callees(f))
|
|
1492
|
+
if (!used.has(g)) {
|
|
1493
|
+
used.add(g);
|
|
1494
|
+
changed = true;
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
const fnsUsed = p.fns.filter(f => used.has(f.name));
|
|
1498
|
+
const fp = (x) => { try {
|
|
1499
|
+
return JSON.stringify(ss.map(s => evalX(x, s.env, s.params)));
|
|
1500
|
+
}
|
|
1501
|
+
catch {
|
|
1502
|
+
return undefined;
|
|
1503
|
+
} };
|
|
1504
|
+
const classes = new Map();
|
|
1505
|
+
const addT = (t) => { const f = fp(t.x); if (f === undefined)
|
|
1506
|
+
return; const key = `${sortsOf(t.sort)}:${f}`; const c = classes.get(key) ?? []; c.push(t); classes.set(key, c); };
|
|
1507
|
+
for (const v of vars)
|
|
1508
|
+
addT({ x: { op: 'var', name: v.name }, sort: v.sort, size: 1 });
|
|
1509
|
+
addT({ x: { op: 'nat', value: 0 }, sort: 'nat', size: 1 });
|
|
1510
|
+
for (const d of p.datas)
|
|
1511
|
+
d.ctors.forEach((c, j) => { if (!c.fields.length)
|
|
1512
|
+
addT({ x: { op: 'ctor', data: d.name, ctor: j, args: [] }, sort: d, size: 1 }); });
|
|
1513
|
+
for (let size = 2; size <= maxSize; size++) {
|
|
1514
|
+
const reps = [...classes.values()].map(c => [...c].sort((a, b) => a.size - b.size)[0]);
|
|
1515
|
+
const build = (sorts, budget) => {
|
|
1516
|
+
if (!sorts.length)
|
|
1517
|
+
return budget === 0 ? [[]] : [];
|
|
1518
|
+
const out = [];
|
|
1519
|
+
for (const t of reps)
|
|
1520
|
+
if (sortsOf(t.sort) === sortsOf(sorts[0]) && t.size <= budget - (sorts.length - 1))
|
|
1521
|
+
for (const rest of build(sorts.slice(1), budget - t.size))
|
|
1522
|
+
out.push([t, ...rest]);
|
|
1523
|
+
return out;
|
|
1524
|
+
};
|
|
1525
|
+
for (const f of fnsUsed)
|
|
1526
|
+
for (const args of build(f.params.map(q => q.sort), size - 1))
|
|
1527
|
+
addT({ x: { op: 'call', fn: f.name, args: args.map(a => a.x) }, sort: f.ret, size });
|
|
1528
|
+
for (const d of p.datas)
|
|
1529
|
+
d.ctors.forEach((c, j) => { if (c.fields.length)
|
|
1530
|
+
for (const args of build(c.fields.map(f => (f.sort === 'nat' ? 'nat' : d)), size - 1))
|
|
1531
|
+
addT({ x: { op: 'ctor', data: d.name, ctor: j, args: args.map(a => a.x) }, sort: d, size }); });
|
|
1532
|
+
for (const [a2, b2] of build(['nat', 'nat'], size - 1))
|
|
1533
|
+
if (JSON.stringify(a2.x) <= JSON.stringify(b2.x))
|
|
1534
|
+
addT({ x: { op: 'add', a: a2.x, b: b2.x }, sort: 'nat', size });
|
|
1535
|
+
if (classes.size > 6000)
|
|
1536
|
+
break;
|
|
1537
|
+
}
|
|
1538
|
+
// Canonical names: each sort's variables renamed in order of appearance.
|
|
1539
|
+
const canon = (t) => {
|
|
1540
|
+
const order = [];
|
|
1541
|
+
walk(t, o => { if (o.op === 'var' && sortOfVar.has(o.name) && !order.includes(o.name))
|
|
1542
|
+
order.push(o.name); });
|
|
1543
|
+
const count = new Map(), ren = new Map();
|
|
1544
|
+
for (const v of order) {
|
|
1545
|
+
const s2 = sortOfVar.get(v), key = s2 instanceof Data ? s2.name.toLowerCase() : 'n', i = (count.get(key) ?? 0) + 1;
|
|
1546
|
+
count.set(key, i);
|
|
1547
|
+
ren.set(v, `${key}${i}`);
|
|
1548
|
+
}
|
|
1549
|
+
const go = (x) => (Array.isArray(x) ? x.map(go) : x && typeof x === 'object' ? (x.op === 'var' && ren.has(x.name) ? { op: 'var', name: ren.get(x.name) } : Object.fromEntries(Object.entries(x).map(([key, v]) => [key, go(v)]))) : x);
|
|
1550
|
+
return { t: go(t), vs: new Set(ren.values()) };
|
|
1551
|
+
};
|
|
1552
|
+
const callIn = (x) => JSON.stringify(x).includes('"op":"call"');
|
|
1553
|
+
const size = (x) => { let n = 0; walk(x, o => { if (['var', 'nat', 'call', 'ctor', 'add'].includes(o.op))
|
|
1554
|
+
n++; }); return n; };
|
|
1555
|
+
const eqs = [];
|
|
1556
|
+
for (const c of classes.values()) {
|
|
1557
|
+
const ts = [...c].sort((a, b) => a.size - b.size), rep = ts[0];
|
|
1558
|
+
const repVars = new Set();
|
|
1559
|
+
walk(rep.x, o => { if (o.op === 'var')
|
|
1560
|
+
repVars.add(o.name); });
|
|
1561
|
+
for (const t of ts.slice(1)) {
|
|
1562
|
+
if (!callIn(t.x))
|
|
1563
|
+
continue;
|
|
1564
|
+
const tv = new Set();
|
|
1565
|
+
walk(t.x, o => { if (o.op === 'var')
|
|
1566
|
+
tv.add(o.name); });
|
|
1567
|
+
if (![...repVars].every(v => tv.has(v)))
|
|
1568
|
+
continue;
|
|
1569
|
+
const { t: ct } = canon([t.x, rep.x]), [ca, cb] = ct;
|
|
1570
|
+
eqs.push({ a: ca, b: cb, size: t.size + rep.size });
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
// Smallest first, and of one size the most general first (an instance of a kept one is then dropped).
|
|
1574
|
+
const nvars = (e) => { const vs = new Set(); walk(e, o => { if (o.op === 'var')
|
|
1575
|
+
vs.add(o.name); }); return vs.size; };
|
|
1576
|
+
eqs.sort((a, b) => a.size - b.size || nvars(b) - nvars(a));
|
|
1577
|
+
const rules = [];
|
|
1578
|
+
const matchX = (pat, t, vs, sub) => {
|
|
1579
|
+
if (pat.op === 'var' && vs.has(pat.name)) {
|
|
1580
|
+
const had = sub.get(pat.name);
|
|
1581
|
+
if (had)
|
|
1582
|
+
return JSON.stringify(had) === JSON.stringify(t);
|
|
1583
|
+
sub.set(pat.name, t);
|
|
1584
|
+
return true;
|
|
1585
|
+
}
|
|
1586
|
+
if (pat.op !== t.op)
|
|
1587
|
+
return false;
|
|
1588
|
+
const po = pat, to = t;
|
|
1589
|
+
for (const key of Object.keys(po)) {
|
|
1590
|
+
const a2 = po[key], b2 = to[key];
|
|
1591
|
+
if (Array.isArray(a2)) {
|
|
1592
|
+
if (!Array.isArray(b2) || a2.length !== b2.length || !a2.every((x, i) => matchX(x, b2[i], vs, sub)))
|
|
1593
|
+
return false;
|
|
1594
|
+
}
|
|
1595
|
+
else if (a2 && typeof a2 === 'object') {
|
|
1596
|
+
if (!b2 || typeof b2 !== 'object' || !matchX(a2, b2, vs, sub))
|
|
1597
|
+
return false;
|
|
1598
|
+
}
|
|
1599
|
+
else if (a2 !== b2)
|
|
1600
|
+
return false;
|
|
1601
|
+
}
|
|
1602
|
+
return true;
|
|
1603
|
+
};
|
|
1604
|
+
const substX = (x, sub) => JSON.parse(JSON.stringify(x), (_, v) => (v && typeof v === 'object' && v.op === 'var' && sub.has(v.name) ? sub.get(v.name) : v));
|
|
1605
|
+
const norm = (x, depth = 0) => {
|
|
1606
|
+
if (depth > 30)
|
|
1607
|
+
return x;
|
|
1608
|
+
let t = JSON.parse(JSON.stringify(x));
|
|
1609
|
+
if (t.op === 'call' || t.op === 'ctor')
|
|
1610
|
+
t = { ...t, args: t.args.map(a2 => norm(a2, depth + 1)) };
|
|
1611
|
+
if (t.op === 'add') {
|
|
1612
|
+
const [a2, b2] = [norm(t.a, depth + 1), norm(t.b, depth + 1)];
|
|
1613
|
+
if (a2.op === 'nat' && a2.value === 0)
|
|
1614
|
+
return b2;
|
|
1615
|
+
if (b2.op === 'nat' && b2.value === 0)
|
|
1616
|
+
return a2;
|
|
1617
|
+
t = JSON.stringify(a2) <= JSON.stringify(b2) ? { op: 'add', a: a2, b: b2 } : { op: 'add', a: b2, b: a2 };
|
|
1618
|
+
}
|
|
1619
|
+
for (const r of rules) {
|
|
1620
|
+
const sub = new Map();
|
|
1621
|
+
if (matchX(r.l, t, r.vs, sub))
|
|
1622
|
+
return norm(substX(r.r, sub), depth + 1);
|
|
1623
|
+
}
|
|
1624
|
+
return t;
|
|
1625
|
+
};
|
|
1626
|
+
// The law in the same names (its variables by sort, in order of appearance).
|
|
1627
|
+
const lawText = (() => {
|
|
1628
|
+
const count = new Map(), ren = new Map();
|
|
1629
|
+
walk(l.holds, o => { if (o.op === 'var' && !ren.has(o.name)) {
|
|
1630
|
+
const v = l.vars.find(w => w.name === o.name);
|
|
1631
|
+
if (!v)
|
|
1632
|
+
return;
|
|
1633
|
+
const key = v.sort instanceof Data ? v.sort.name.toLowerCase() : 'n', i = (count.get(key) ?? 0) + 1;
|
|
1634
|
+
count.set(key, i);
|
|
1635
|
+
ren.set(v.name, `${key}${i}`);
|
|
1636
|
+
} });
|
|
1637
|
+
return JSON.stringify(l.holds, (_, v) => (v && typeof v === 'object' && v.op === 'var' && ren.has(v.name) ? { op: 'var', name: ren.get(v.name) } : v));
|
|
1638
|
+
})();
|
|
1639
|
+
const out = [], seen = new Set();
|
|
1640
|
+
const lawOf = (y, vs, name) => ({ name, vars: [...vs].map(v => ({ name: v, sort: sortOfVar.get(v) })), holds: y, params: [...new Set(paramsIn(y))] });
|
|
1641
|
+
const kept = [];
|
|
1642
|
+
// The definitions, unfolded at a constructor: f(.., C(x, y), ..) → its case's body, where that is a plain term.
|
|
1643
|
+
for (const f of fnsUsed) {
|
|
1644
|
+
const body = f.body(), di = f.decreases;
|
|
1645
|
+
if (body.op !== 'match' || di === undefined || body.of.op !== 'var' || body.of.name !== `${f.name}.${f.params[di].name}`)
|
|
1646
|
+
continue;
|
|
1647
|
+
const d = f.params[di].sort;
|
|
1648
|
+
body.cases.forEach((c, j) => {
|
|
1649
|
+
if (JSON.stringify(c.body).match(/"op":"(match|if|bool|apply)"/))
|
|
1650
|
+
return;
|
|
1651
|
+
const names = new Map(f.params.map((q, i) => [`${f.name}.${q.name}`, { op: 'var', name: `$p${i}` }]));
|
|
1652
|
+
c.binds.forEach((b, i) => names.set(b, { op: 'var', name: `$f${i}` }));
|
|
1653
|
+
const ren = (x) => JSON.parse(JSON.stringify(x), (_, v) => (v && typeof v === 'object' && v.op === 'var' && names.has(v.name) ? names.get(v.name) : v));
|
|
1654
|
+
const lhs = { op: 'call', fn: f.name, args: f.params.map((_, i) => (i === di ? { op: 'ctor', data: d.name, ctor: j, args: c.binds.map((_, q) => ({ op: 'var', name: `$f${q}` })) } : { op: 'var', name: `$p${i}` })) };
|
|
1655
|
+
const vs = new Set();
|
|
1656
|
+
walk(lhs, o => { if (o.op === 'var')
|
|
1657
|
+
vs.add(o.name); });
|
|
1658
|
+
rules.push({ l: lhs, r: ren(c.body), vs });
|
|
1659
|
+
});
|
|
1660
|
+
}
|
|
1661
|
+
const lawY = JSON.parse(lawText);
|
|
1662
|
+
if (lawY.op === '=') {
|
|
1663
|
+
const vs = new Set();
|
|
1664
|
+
walk(lawY, o => { if (o.op === 'var')
|
|
1665
|
+
vs.add(o.name); });
|
|
1666
|
+
const [big, small] = size(lawY.a) >= size(lawY.b) ? [lawY.a, lawY.b] : [lawY.b, lawY.a];
|
|
1667
|
+
if (size(big) > size(small))
|
|
1668
|
+
rules.push({ l: big, r: small, vs });
|
|
1669
|
+
}
|
|
1670
|
+
for (const e of eqs) {
|
|
1671
|
+
const na = norm(e.a), nb = norm(e.b);
|
|
1672
|
+
if (JSON.stringify(na) === JSON.stringify(nb))
|
|
1673
|
+
continue;
|
|
1674
|
+
// An instance of one kept (both sides at one substitution) adds nothing.
|
|
1675
|
+
if (kept.some(q => [[q.a, q.b], [q.b, q.a]].some(([x1, x2]) => { const sub = new Map(); return matchX(x1, e.a, q.vs, sub) && matchX(x2, e.b, q.vs, sub); })))
|
|
1676
|
+
continue;
|
|
1677
|
+
const y = { op: '=', a: e.a, b: e.b }, text = JSON.stringify(y);
|
|
1678
|
+
if (seen.has(text) || text === lawText || JSON.stringify({ op: '=', a: e.b, b: e.a }) === lawText)
|
|
1679
|
+
continue;
|
|
1680
|
+
seen.add(text);
|
|
1681
|
+
const vs = new Set();
|
|
1682
|
+
walk(y, o => { if (o.op === 'var')
|
|
1683
|
+
vs.add(o.name); });
|
|
1684
|
+
// A ground equation only rewrites (a definition unfolded proves it where it is needed).
|
|
1685
|
+
if (vs.size && out.length < max)
|
|
1686
|
+
out.push(lawOf(y, vs, `${showX(e.a)} = ${showX(e.b)}`));
|
|
1687
|
+
kept.push({ a: e.a, b: e.b, vs });
|
|
1688
|
+
const [big, small] = size(e.a) >= size(e.b) ? [e.a, e.b] : [e.b, e.a];
|
|
1689
|
+
if (size(big) > size(small))
|
|
1690
|
+
rules.push({ l: big, r: small, vs });
|
|
1691
|
+
}
|
|
1692
|
+
// Predicates kept by a function returning a datatype.
|
|
1693
|
+
const preds = p.fns.filter(f => f.ret === 'bool' && used.has(f.name)), makers = p.fns.filter(f => f.ret instanceof Data && used.has(f.name));
|
|
1694
|
+
const nats = vars.filter(v => v.sort === 'nat').map(v => v.name);
|
|
1695
|
+
for (const pr of preds)
|
|
1696
|
+
for (const mk of makers) {
|
|
1697
|
+
const di = pr.params.findIndex(q => q.sort === mk.ret);
|
|
1698
|
+
if (di < 0)
|
|
1699
|
+
continue;
|
|
1700
|
+
const dvar = vars.find(v => v.sort === mk.ret).name;
|
|
1701
|
+
const argSets = (qs) => qs.reduce((acc, q) => acc.flatMap(a2 => (q.sort === 'nat' ? nats.map(n => [...a2, { op: 'var', name: n }]) : q.sort === 'bool' ? [] : [[...a2, { op: 'var', name: vars.find(v => v.sort === q.sort).name }]])), [[]]);
|
|
1702
|
+
for (const margs of argSets(mk.params))
|
|
1703
|
+
for (const pargs of argSets(pr.params.filter((_, i) => i !== di))) {
|
|
1704
|
+
const pAt = (x) => { const as = [...pargs]; as.splice(di, 0, x); return { op: 'holds', x: { op: 'call', fn: pr.name, args: as } }; };
|
|
1705
|
+
const premise = pAt({ op: 'var', name: dvar }), conclusion = pAt({ op: 'call', fn: mk.name, args: margs });
|
|
1706
|
+
const conds = [undefined, ...nats.flatMap(a2 => nats.filter(b2 => b2 !== a2).flatMap(b2 => [{ op: '<', a: { op: 'var', name: a2 }, b: { op: 'var', name: b2 } }, { op: '<=', a: { op: 'var', name: a2 }, b: { op: 'var', name: b2 } }]))];
|
|
1707
|
+
for (const c of conds) {
|
|
1708
|
+
const hyp = c ? { op: 'and', xs: [premise, c] } : premise;
|
|
1709
|
+
const { t: cy, vs } = canon({ op: 'or', xs: [{ op: 'not', x: hyp }, conclusion] });
|
|
1710
|
+
const y = cy, text = JSON.stringify(y);
|
|
1711
|
+
if (seen.has(text) || text === lawText)
|
|
1712
|
+
break;
|
|
1713
|
+
let held = 0, ok = true;
|
|
1714
|
+
for (const sm of ss) {
|
|
1715
|
+
try {
|
|
1716
|
+
if (evalY(hyp, sm.env, sm.params)) {
|
|
1717
|
+
held++;
|
|
1718
|
+
if (!evalY(conclusion, sm.env, sm.params)) {
|
|
1719
|
+
ok = false;
|
|
1720
|
+
break;
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
catch {
|
|
1725
|
+
ok = false;
|
|
1726
|
+
break;
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
if (ok && held >= 5) {
|
|
1730
|
+
seen.add(text);
|
|
1731
|
+
out.push(lawOf(y, vs, `${showY(y.xs[0].op === 'not' ? y.xs[0].x : y)} implies ${showY(y.xs[1])}`));
|
|
1732
|
+
break;
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
return out;
|
|
1738
|
+
}
|
|
1739
|
+
export const showX = (x) => {
|
|
1740
|
+
switch (x.op) {
|
|
1741
|
+
case 'var': return x.name.replace(/#\d+$/, '');
|
|
1742
|
+
case 'nat': return String(x.value);
|
|
1743
|
+
case 'add': return `${showX(x.a)} + ${showX(x.b)}`;
|
|
1744
|
+
case 'sub': return `${showX(x.a)} − ${showX(x.b)}`;
|
|
1745
|
+
case 'min':
|
|
1746
|
+
case 'max': return `${x.op}(${showX(x.a)}, ${showX(x.b)})`;
|
|
1747
|
+
case 'mul': return `${x.k}·${showX(x.a)}`;
|
|
1748
|
+
case 'if': return `if ${showY(x.c)} then ${showX(x.then)} else ${showX(x.else)}`;
|
|
1749
|
+
case 'bool': return showY(x.c);
|
|
1750
|
+
case 'apply': return `${x.param}(${showX(x.arg)})`;
|
|
1751
|
+
case 'call': return `${x.fn}(${x.args.map(showX).join(', ')})`;
|
|
1752
|
+
case 'ctor': {
|
|
1753
|
+
const d = [...fns.values()].length >= 0 ? x.data : x.data;
|
|
1754
|
+
return `${d}.${x.ctor}${x.args.length ? `(${x.args.map(showX).join(', ')})` : ''}`;
|
|
1755
|
+
}
|
|
1756
|
+
case 'match': return `match ${showX(x.of)}`;
|
|
1757
|
+
}
|
|
1758
|
+
};
|
|
1759
|
+
export const showY = (y) => {
|
|
1760
|
+
switch (y.op) {
|
|
1761
|
+
case '<':
|
|
1762
|
+
case '<=':
|
|
1763
|
+
case '=': return `${showX(y.a)} ${y.op} ${showX(y.b)}`;
|
|
1764
|
+
case 'and': return y.xs.map(showY).join(' and ');
|
|
1765
|
+
case 'or': return y.xs.map(showY).join(' or ');
|
|
1766
|
+
case 'not': return `not (${showY(y.x)})`;
|
|
1767
|
+
case 'is': return `${showX(y.of)} is ${y.ctor}`;
|
|
1768
|
+
case 'holds': return showX(y.x);
|
|
1769
|
+
}
|
|
1770
|
+
};
|
|
1771
|
+
// Prove a law over datatypes: refuted by a random value where one breaks it; else proven by a cycle the search finds,
|
|
1772
|
+
// with lemmas found from the values when the law alone isn't (`lemmas: false` turns that off; an array gives lemmas to
|
|
1773
|
+
// prove and use instead, which the corpus never does).
|
|
1774
|
+
export function proveData(l, opts = {}) {
|
|
1775
|
+
const t0 = performance.now(), until = t0 + (opts.ms ?? 120_000);
|
|
1776
|
+
const p = programOf([l, ...(Array.isArray(opts.lemmas) ? opts.lemmas : [])]), fs = factsOf(p);
|
|
1777
|
+
const done = (v) => ({ law: l.name, ...v, ms: Math.round(performance.now() - t0) });
|
|
1778
|
+
const cex = refute(l, opts.samples ?? 400);
|
|
1779
|
+
if (cex)
|
|
1780
|
+
return done({ proven: false, checked: false, counterexample: cex, lemmas: [], tried: 0 });
|
|
1781
|
+
// The law alone at one unfolding; then lemmas at one unfolding each (the law tried again after each its goal
|
|
1782
|
+
// matches); then the law at two; then lemmas at two; last the law at three. A lemma is named in the verdict when the
|
|
1783
|
+
// proof uses it.
|
|
1784
|
+
let tried = 0;
|
|
1785
|
+
const used = (r, lemmas) => {
|
|
1786
|
+
const nodes = r.nodes, seen = new Set(), todo = [nodes.indexOf(r.spec)];
|
|
1787
|
+
while (todo.length) {
|
|
1788
|
+
const i = todo.pop();
|
|
1789
|
+
if (seen.has(i))
|
|
1790
|
+
continue;
|
|
1791
|
+
seen.add(i);
|
|
1792
|
+
for (const u of nodes[i].uses)
|
|
1793
|
+
todo.push(u.node);
|
|
1794
|
+
}
|
|
1795
|
+
return lemmas.filter(q => seen.has(nodes.indexOf(nodes.find(n => n.name === q.spec.name)))).map(q => q.law.name);
|
|
1796
|
+
};
|
|
1797
|
+
const ok = (r, lemmas, prog = p) => (tried += r.tried, r.out ? done({ proven: true, checked: true, proof: r.verdict, lemmas: used(r.out, lemmas), scheme: r.scheme, tried, program: prog, node: r.out.nodes.indexOf(r.out.spec) }) : undefined);
|
|
1798
|
+
const first = proveWith(p, fs, l, [], 1, opts, until, 6000, [1]);
|
|
1799
|
+
const one = ok(first, []);
|
|
1800
|
+
if (one)
|
|
1801
|
+
return one;
|
|
1802
|
+
const without = opts.lemmas === false || opts.induction === false;
|
|
1803
|
+
if (without)
|
|
1804
|
+
return ok(proveWith(p, fs, l, [], 1, opts, until, 20000, [2, 3]), []) ?? done({ proven: false, checked: false, lemmas: [], tried, ...stuckOf(first.last) });
|
|
1805
|
+
const cands = Array.isArray(opts.lemmas) ? opts.lemmas : conjectures(p, l);
|
|
1806
|
+
const p2 = programOf([l, ...cands]), fs2 = factsOf(p2);
|
|
1807
|
+
const goalOf = (s) => {
|
|
1808
|
+
const concl = encY(p2, l.holds, new Map()), ihc = (s?.uses ?? []).map(u => substC(concl, { vars: u.subst, maps: {}, flags: {} }));
|
|
1809
|
+
return { from: [concl, ihc], insts: instancesOf(p2, fs2, { roots: [concl], others: ihc, depth: 2, shapesAt: s?.on ? [ev(s.on.v)] : [], etaAt: [], models: [] }) };
|
|
1810
|
+
};
|
|
1811
|
+
const goals = schemes(p2, l).map(goalOf);
|
|
1812
|
+
const matchesGoal = (c) => goals.some(g => lemmaUsesFor(p2, fs2, [{ law: c, node: 0, concl: encY(p2, c.holds, new Map()) }], g.from, g.insts).length > 0);
|
|
1813
|
+
// Those the goal matches first, of those the ones of the law's shape (an implication for an implication) first.
|
|
1814
|
+
const implication = (y) => y.op === 'or' && y.xs.length === 2 && y.xs[0].op === 'not';
|
|
1815
|
+
const score = (c, hit) => (hit ? 2 : 0) + (implication(c.holds) === implication(l.holds) ? 1 : 0);
|
|
1816
|
+
const ranked = cands.map(c => ({ c, hit: matchesGoal(c) })).sort((a, b) => score(b.c, b.hit) - score(a.c, a.hit));
|
|
1817
|
+
const proven = [], failed = new Set();
|
|
1818
|
+
const lemmasAt = (depth) => {
|
|
1819
|
+
for (const { c, hit } of ranked) {
|
|
1820
|
+
if (performance.now() > until)
|
|
1821
|
+
return undefined;
|
|
1822
|
+
if (proven.some(q => q.law === c) || (depth === 2 && !failed.has(c)) || refute(c, 200))
|
|
1823
|
+
continue;
|
|
1824
|
+
const r = proveWith(p2, fs2, c, proven, 1 + proven.length, opts, Math.min(until, performance.now() + (depth === 1 ? 8000 : 12000) * (opts.patience ?? 1)), 5000, [depth]);
|
|
1825
|
+
tried += r.tried;
|
|
1826
|
+
if (!r.out) {
|
|
1827
|
+
failed.add(c);
|
|
1828
|
+
continue;
|
|
1829
|
+
}
|
|
1830
|
+
failed.delete(c);
|
|
1831
|
+
proven.push(r.out);
|
|
1832
|
+
if (!hit && implication(c.holds) !== implication(l.holds))
|
|
1833
|
+
continue;
|
|
1834
|
+
const again = ok(proveWith(p2, fs2, l, proven, 1 + proven.length, opts, until, 6000, depth === 1 ? [1] : [1, 2]), proven, p2);
|
|
1835
|
+
if (again)
|
|
1836
|
+
return again;
|
|
1837
|
+
}
|
|
1838
|
+
return undefined;
|
|
1839
|
+
};
|
|
1840
|
+
const steps = [
|
|
1841
|
+
() => lemmasAt(1),
|
|
1842
|
+
() => ok(proveWith(p, fs, l, [], 1, opts, until, 30000, [2]), []),
|
|
1843
|
+
() => (proven.length ? ok(proveWith(p2, fs2, l, proven, 1 + proven.length, opts, until, 30000, [2]), proven, p2) : undefined),
|
|
1844
|
+
() => lemmasAt(2),
|
|
1845
|
+
() => ok(proveWith(p2, fs2, l, proven, 1 + proven.length, opts, until, 12000, [3]), proven, p2),
|
|
1846
|
+
];
|
|
1847
|
+
for (const step of steps) {
|
|
1848
|
+
if (performance.now() > until)
|
|
1849
|
+
break;
|
|
1850
|
+
const out = step();
|
|
1851
|
+
if (out)
|
|
1852
|
+
return out;
|
|
1853
|
+
}
|
|
1854
|
+
return done({ proven: false, checked: false, lemmas: [], tried, ...stuckOf(first.last) });
|
|
1855
|
+
}
|
|
1856
|
+
const stuckOf = (v) => (v && !v.proven ? { goal: v.goal, stuck: v.stuck } : v && !v.checked ? { goal: 'check', stuck: v.why } : {});
|
|
1857
|
+
// The law with no cycle: its definitions unfolded, the kernel's prover alone (what the library proves without the search).
|
|
1858
|
+
export const proveDataToday = (l) => proveData(l, { induction: false });
|