gdp-wired 0.11.0 → 0.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/dist/core/compose.js +1 -1
- package/dist/core/dsl.d.ts +1 -1
- package/dist/core/dsl.js +18 -9
- package/dist/core/machine.d.ts +1 -0
- package/dist/core/machine.js +52 -12
- package/dist/core/solver.d.ts +12 -0
- package/dist/core/solver.js +68 -17
- package/dist/kernel/kernel.d.ts +19 -3
- package/dist/kernel/kernel.js +104 -26
- package/dist/kernel/prove.js +80 -38
- package/dist/lean-kernel/emit.js +44 -19
- package/dist/lean-kernel/library.js +6 -6
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -51,7 +51,8 @@ All of these are proven automatically, and each proof is also a Lean theorem:
|
|
|
51
51
|
|
|
52
52
|
- **Invariants**: a law that holds after every sequence of requests ("spend stays within the budget").
|
|
53
53
|
- **Maps, sets and lists of names**: laws at every key ("no day passes its cap"), sets a request carries, conditions
|
|
54
|
-
about every key, lists kept in order
|
|
54
|
+
about every key or every two keys ("a batch is taken only when its values increase"), lists kept in order
|
|
55
|
+
(`examples/sets.ts`, `examples/lists.ts`, `examples/pairs.ts`).
|
|
55
56
|
- **Step laws**: what one step does ("a balance only goes down", "a grant adds one"), and laws over several steps in a
|
|
56
57
|
row.
|
|
57
58
|
- **Histories**: sums and queries over a log of requests, like a sliding-window rate limit.
|
|
@@ -261,7 +262,8 @@ const soon = retry.law('the next try is at most 64 after the last request', ({ne
|
|
|
261
262
|
- **Arithmetic** over the naturals: `plus`, `minus` (truncated), `times(k)`, `mul` (a product of two terms), `min`,
|
|
262
263
|
`max`, `when(cond, a, b)`, `count(cond)`; conditions `lt`, `atMost`, `eq`, `and`, `or`, `not`, `implies`.
|
|
263
264
|
- **Sets and quantifiers**: `.carries(...)` for sets a request brings, `everyKey(k => ...)` (and `has`, `subsetOf`,
|
|
264
|
-
`disjointFrom`, `sameAs`) anywhere in a step or inside a law, `
|
|
265
|
+
`disjointFrom`, `sameAs`) anywhere in a step or inside a law, `everyKey((i, j) => ...)` in a step for every two keys
|
|
266
|
+
(each read on its own, compared with the other plainly: `i.lt(j)`), `M.each(k => ...)` to write every key, and
|
|
265
267
|
`someKey(k => ..., witness?)` for "some key where", proven through a witness the prover finds or the one given.
|
|
266
268
|
- **Functions** (`.fns({perMinute: divBy(60)})`, or `fn(arity, js, {facts, lean})` for your own): read like params that
|
|
267
269
|
take arguments, `perMinute(t)`; the kernel knows each only by its facts (`.facts(...)`, and a library function's
|
package/dist/core/compose.js
CHANGED
|
@@ -37,7 +37,7 @@ function renameC(c, prefix, m, inputs) {
|
|
|
37
37
|
if (c.op === 'and' || c.op === 'or')
|
|
38
38
|
return { op: c.op, xs: c.xs.map(y => renameC(y, prefix, m, inputs)) };
|
|
39
39
|
if (c.op === 'all')
|
|
40
|
-
return {
|
|
40
|
+
return { ...c, key: `${prefix}${c.key}`, ...(c.keys ? { keys: c.keys.map(k => `${prefix}${k}`) } : {}), c: renameC(c.c, prefix, m, inputs) }; // the keys renamed with their side
|
|
41
41
|
return { ...c, a: rename(c.a, prefix, m, inputs), b: rename(c.b, prefix, m, inputs) };
|
|
42
42
|
}
|
|
43
43
|
// A condition (or law) of one side, read in the composed machine.
|
package/dist/core/dsl.d.ts
CHANGED
|
@@ -253,7 +253,7 @@ export type MapWrite = {
|
|
|
253
253
|
at: (key: Term) => TermLike;
|
|
254
254
|
};
|
|
255
255
|
};
|
|
256
|
-
export declare const everyKey: (f: (key: Term) => Cond) => Cond;
|
|
256
|
+
export declare const everyKey: (f: (key: Term, key2: Term, key3: Term) => Cond) => Cond;
|
|
257
257
|
export declare const someKey: (f: (key: Term) => Cond, witness?: TermLike) => Cond;
|
|
258
258
|
export declare class MapRef {
|
|
259
259
|
readonly name: string;
|
package/dist/core/dsl.js
CHANGED
|
@@ -774,8 +774,15 @@ export const decimal = (places) => {
|
|
|
774
774
|
};
|
|
775
775
|
let boundCounter = 0;
|
|
776
776
|
// A condition at every key: only as a whole flag (`flags: ({...}) => ({covered: everyKey(k => ...)})`), the key read
|
|
777
|
-
// only as a map's key. The kernel closes it (instances where it holds, a witness where it fails).
|
|
778
|
-
|
|
777
|
+
// only as a map's key. The kernel closes it (instances where it holds, a witness where it fails). With two parameters
|
|
778
|
+
// or more, at every two keys or more (`everyKey((i, j) => i.lt(j).implies(At.at(i).atMost(At.at(j))))`): each key read
|
|
779
|
+
// on its own, and compared with another only plainly (i < j, i <= j, i = j); the runtimes decide it on every tuple of
|
|
780
|
+
// a few keys per breakpoint, so it costs the square of the cells (the cube, over three keys).
|
|
781
|
+
export const everyKey = (f) => {
|
|
782
|
+
const names = Array.from({ length: Math.max(1, f.length) }, () => `k$${++boundCounter}`), g = f;
|
|
783
|
+
const ok = (picked) => (picked.length === names.length ? g(...picked).ok : everyOk(scope?.keys, k => ok([...picked, keyTerm(k)])));
|
|
784
|
+
return new Cond({ op: 'all', key: names[0], ...(names.length > 1 ? { keys: names.slice(1) } : {}), c: g(...names.map(v)).c }, ok([]));
|
|
785
|
+
};
|
|
779
786
|
// Some key where f holds, in a law or a step law's conclusion (¬∀k. ¬f(k)): proven through a witness, a term naming the
|
|
780
787
|
// key, given here or found by the prover among a few candidates (kernel/prove.ts).
|
|
781
788
|
export const someKey = (f, witness) => {
|
|
@@ -1079,7 +1086,7 @@ function swapC(c, from, to) {
|
|
|
1079
1086
|
case 'not': return { op: 'not', x: swapC(c.x, from, to) };
|
|
1080
1087
|
case 'and':
|
|
1081
1088
|
case 'or': return { op: c.op, xs: c.xs.map(y => swapC(y, from, to)) };
|
|
1082
|
-
case 'all': return {
|
|
1089
|
+
case 'all': return { ...c, c: swapC(c.c, from, to) };
|
|
1083
1090
|
case 'flag': return c;
|
|
1084
1091
|
case '<':
|
|
1085
1092
|
case '<=':
|
|
@@ -1170,15 +1177,15 @@ class Hoister {
|
|
|
1170
1177
|
quantifier(q, bound, name) {
|
|
1171
1178
|
const outer = bound.filter(key => namesIn(q).includes(key));
|
|
1172
1179
|
if (outer.length)
|
|
1173
|
-
throw new Error(`a condition at every key reads ${outer[0]}, a key bound outside it: two keys at once are
|
|
1174
|
-
const inner = perStep(q.c);
|
|
1180
|
+
throw new Error(`a condition at every key reads ${outer[0]}, a key bound outside it: two keys at once are named together (everyKey((i, j) => ...))`);
|
|
1181
|
+
const inner = perStep(q.c), keys = q.keys?.length ? { keys: q.keys } : {};
|
|
1175
1182
|
if (!inner) {
|
|
1176
1183
|
const f = name ?? `${this.prefix}${++this.n}`;
|
|
1177
|
-
this.flags[f] = { op: 'all', key: q.key, c: q.c };
|
|
1184
|
+
this.flags[f] = { op: 'all', key: q.key, ...keys, c: q.c };
|
|
1178
1185
|
return { op: 'flag', name: f };
|
|
1179
1186
|
}
|
|
1180
|
-
const truth = inner.op === 'flag' ? inner : this.quantifier(inner, [...bound, q.key]);
|
|
1181
|
-
const split = (by) => this.quantifier({ op: 'all', key: q.key, c: swapC(q.c, inner, by) }, bound);
|
|
1187
|
+
const truth = inner.op === 'flag' ? inner : this.quantifier(inner, [...bound, q.key, ...(q.keys ?? [])]);
|
|
1188
|
+
const split = (by) => this.quantifier({ op: 'all', key: q.key, ...keys, c: swapC(q.c, inner, by) }, bound);
|
|
1182
1189
|
return { op: 'or', xs: [{ op: 'and', xs: [truth, split(yes.c)] }, { op: 'and', xs: [{ op: 'not', x: truth }, split(no.c)] }] };
|
|
1183
1190
|
}
|
|
1184
1191
|
}
|
|
@@ -1211,6 +1218,8 @@ function prenex(c, what, key) {
|
|
|
1211
1218
|
const walk = (d, positive) => {
|
|
1212
1219
|
switch (d.op) {
|
|
1213
1220
|
case 'all':
|
|
1221
|
+
if (d.keys?.length)
|
|
1222
|
+
throw new Error(`${what}: a condition at every two keys in a law is the law's own keys (forall((i, j) => ...))`);
|
|
1214
1223
|
if (!positive)
|
|
1215
1224
|
return d;
|
|
1216
1225
|
found = true;
|
|
@@ -1456,7 +1465,7 @@ const markedE = (e) => {
|
|
|
1456
1465
|
case 'prod': return { ...e, a: markedE(e.a), b: markedE(e.b) };
|
|
1457
1466
|
}
|
|
1458
1467
|
};
|
|
1459
|
-
const marked = (c) => c.op === 'flag' ? c : c.op === 'all' ? {
|
|
1468
|
+
const marked = (c) => c.op === 'flag' ? c : c.op === 'all' ? { ...c, c: marked(c.c) } : c.op === 'not' ? { op: 'not', x: marked(c.x) } : c.op === 'and' || c.op === 'or' ? { op: c.op, xs: c.xs.map(marked) } : { ...c, a: markedE(c.a), b: markedE(c.b) };
|
|
1460
1469
|
// P params, I inputs, S state, M maps, F flags, R a history's record, Su its sums, Q its queries: each a record of the
|
|
1461
1470
|
// values the builders hand out, grown by each call.
|
|
1462
1471
|
export class Rule {
|
package/dist/core/machine.d.ts
CHANGED
package/dist/core/machine.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// Arithmetic is linear: +, −, × by a constant, min, max, if. Maps are naturals indexed by naturals, every cell starting
|
|
7
7
|
// at the same value; a law about a map holds for every key x, and is proven at one arbitrary x (with the law assumed at
|
|
8
8
|
// every other key the step reads).
|
|
9
|
-
import { historyMachine, keyTerm, plainKey, scope } from "../kernel/kernel.js";
|
|
9
|
+
import { boundOf, crossFree, historyMachine, keyTerm, plainKey, scope } from "../kernel/kernel.js";
|
|
10
10
|
import { add, eq, k, le, lt, nat, scale, show, solve, sub, v } from "./solver.js";
|
|
11
11
|
// A write at one key, for the parts that take no write at every key (the symbolic prover, the GPU synthesizers, the
|
|
12
12
|
// hand-written shader, the first Lean export): they refuse one by name.
|
|
@@ -49,7 +49,7 @@ function keysAt(st, maps) {
|
|
|
49
49
|
}
|
|
50
50
|
const breaksCache = new WeakMap();
|
|
51
51
|
function breaksOf(t, key) {
|
|
52
|
-
const hit = breaksCache.get(t);
|
|
52
|
+
const hit = breaksCache.get(t)?.get(key);
|
|
53
53
|
if (hit)
|
|
54
54
|
return hit;
|
|
55
55
|
const out = { reads: [], cmps: [] };
|
|
@@ -70,7 +70,9 @@ function breaksOf(t, key) {
|
|
|
70
70
|
(Array.isArray(v) ? v : [v]).forEach(y => walk(y));
|
|
71
71
|
};
|
|
72
72
|
walk(t);
|
|
73
|
-
breaksCache.
|
|
73
|
+
if (!breaksCache.has(t))
|
|
74
|
+
breaksCache.set(t, new Map());
|
|
75
|
+
breaksCache.get(t).set(key, out);
|
|
74
76
|
return out;
|
|
75
77
|
}
|
|
76
78
|
function breakKeys(b, env, st, m) {
|
|
@@ -103,6 +105,25 @@ function breakKeys(b, env, st, m) {
|
|
|
103
105
|
const sorted = [...keys].sort((a, b) => a - b);
|
|
104
106
|
return [...sorted, sorted.at(-1) + 1];
|
|
105
107
|
}
|
|
108
|
+
// A condition at every tuple of n keys: the breakpoints of each key (each read on its own, the comparisons between two
|
|
109
|
+
// of them set aside) together, each case with the n - 1 keys after it. Every stretch where no read moves and no
|
|
110
|
+
// comparison turns starts at one of those cases (a breakpoint, the key after one, or 0), so the keys after its start
|
|
111
|
+
// place n keys there in every order they can take; keys read only as a map's key need only the cells and one past them.
|
|
112
|
+
// Keys read only as a map's key and compared with each other: a read changes only at a cell, so each cell, the n keys
|
|
113
|
+
// after it (the stretch up to the next cell), 0 and the n after it, and past the last cell are every case.
|
|
114
|
+
function tupleKeys(c, env, st, m) {
|
|
115
|
+
const names = boundOf(c);
|
|
116
|
+
if (names.every(n => plainKey(c.c, n)))
|
|
117
|
+
return keysAt(st, mapsIn(c.c));
|
|
118
|
+
if (names.length === 1)
|
|
119
|
+
return breakKeys(breaksOf(c.c, c.key), env, st, m);
|
|
120
|
+
const body = crossFree(c.c, names);
|
|
121
|
+
if (names.every(n => plainKey(body, n)))
|
|
122
|
+
return spread([0, ...keysAt(st, mapsIn(c.c))], names.length + 1);
|
|
123
|
+
const parts = names.map(n => breaksOf(body, n));
|
|
124
|
+
return spread(breakKeys({ reads: parts.flatMap(b => b.reads), cmps: parts.flatMap(b => b.cmps) }, env, st, m), names.length);
|
|
125
|
+
}
|
|
126
|
+
const spread = (keys, n) => [...new Set(keys.flatMap(k => Array.from({ length: n }, (_, i) => k + i)))].sort((a, b) => a - b);
|
|
106
127
|
export const naturalIn = (v, what) => {
|
|
107
128
|
if (typeof v !== 'number' || !Number.isSafeInteger(v) || v < 0)
|
|
108
129
|
throw new RangeError(`${what} must be a natural below 2^53, not ${String(v)}`);
|
|
@@ -189,7 +210,11 @@ export function evalC(c, env, st, m) {
|
|
|
189
210
|
case 'or': return c.xs.some(y => evalC(y, env, st, m));
|
|
190
211
|
case 'not': return !evalC(c.x, env, st, m);
|
|
191
212
|
case 'flag': return env[`?${c.name}`] === 1;
|
|
192
|
-
case 'all':
|
|
213
|
+
case 'all': {
|
|
214
|
+
const names = boundOf(c), keys = tupleKeys(c, env, st, m);
|
|
215
|
+
const go = (i, at) => (i === names.length ? evalC(c.c, at, st, m) : keys.every(key => go(i + 1, { ...at, [names[i]]: key })));
|
|
216
|
+
return go(0, env);
|
|
217
|
+
}
|
|
193
218
|
}
|
|
194
219
|
}
|
|
195
220
|
// A write at every key: its value at each key the maps it reads hold a cell at, and its new default past them.
|
|
@@ -343,11 +368,11 @@ export function compileJs(m) {
|
|
|
343
368
|
const { h, plain, queries, carried } = runnable(m);
|
|
344
369
|
const id = (name) => JSON.stringify(name);
|
|
345
370
|
const flags = Object.keys(plain.step.flags ?? {}), carriedMaps = m.inputMaps ?? [], fns = Object.keys(m.fns ?? {});
|
|
346
|
-
// A key bound by a quantified flag or a write at every key is the JS parameter k.
|
|
347
|
-
let bound;
|
|
371
|
+
// A key bound by a quantified flag or a write at every key is the JS parameter k (k1, k2 for a flag's further keys).
|
|
372
|
+
let bound = {};
|
|
348
373
|
const ex = (e) => {
|
|
349
374
|
switch (e.op) {
|
|
350
|
-
case 'var': return e.name
|
|
375
|
+
case 'var': return bound[e.name] ?? (e.name.startsWith('r.') ? `r[${id(e.name.slice(2))}]` : plain.params.includes(e.name) || plain.inputs.includes(e.name) ? `env[${id(e.name)}]` : `s[${id(e.name)}]`);
|
|
351
376
|
case 'const': return String(e.value);
|
|
352
377
|
case 'add': return `$x(${ex(e.a)} + ${ex(e.b)})`;
|
|
353
378
|
case 'sub': return `Math.max(0, ${ex(e.a)} - ${ex(e.b)})`;
|
|
@@ -374,11 +399,11 @@ export function compileJs(m) {
|
|
|
374
399
|
case 'not': return `(!${cx(c.x)})`;
|
|
375
400
|
case 'flag': return `flag${flags.indexOf(c.name)}`;
|
|
376
401
|
case 'all': {
|
|
377
|
-
const keys = keysJs(c.c, c.key), outer = bound;
|
|
378
|
-
bound =
|
|
402
|
+
const names = boundOf(c), ks = names.map((_, i) => (i ? `k${i}` : 'k')), keys = names.length === 1 ? keysJs(c.c, c.key) : tupleJs(c), outer = bound;
|
|
403
|
+
bound = { ...bound, ...Object.fromEntries(names.map((n, i) => [n, ks[i]])) };
|
|
379
404
|
const body = cx(c.c);
|
|
380
405
|
bound = outer;
|
|
381
|
-
return `${keys}.every(k => ${body})`;
|
|
406
|
+
return names.length === 1 ? `${keys}.every(k => ${body})` : `(ks => ${ks.map(k => `ks.every(${k} => `).join('')}${body}${')'.repeat(ks.length)})(${keys})`;
|
|
382
407
|
}
|
|
383
408
|
}
|
|
384
409
|
};
|
|
@@ -386,10 +411,24 @@ export function compileJs(m) {
|
|
|
386
411
|
const keysJs = (t, key) => {
|
|
387
412
|
if (plainKey(t, key))
|
|
388
413
|
return `$keys(maps, ${JSON.stringify(mapsIn(t).filter(mp => !fns.includes(mp)))})`;
|
|
389
|
-
|
|
414
|
+
return breaksJs(breaksOf(t, key));
|
|
415
|
+
};
|
|
416
|
+
const breaksJs = (b) => {
|
|
417
|
+
const off = (o) => (o ? ex(o) : '0');
|
|
390
418
|
return `$breaks(maps, [${b.reads.map(r => `[${id(r.map)}, ${off(r.off)}, ${r.minus}]`).join(', ')}], [${b.cmps.map(c => `[${ex(c.value)}, ${off(c.off)}, ${c.minus}]`).join(', ')}])`;
|
|
391
419
|
};
|
|
392
|
-
|
|
420
|
+
// The interpreter's tupleKeys.
|
|
421
|
+
const tupleJs = (c) => {
|
|
422
|
+
const names = boundOf(c);
|
|
423
|
+
if (names.every(n => plainKey(c.c, n)))
|
|
424
|
+
return `$keys(maps, ${JSON.stringify(mapsIn(c.c).filter(mp => !fns.includes(mp)))})`;
|
|
425
|
+
const body = crossFree(c.c, names);
|
|
426
|
+
if (names.every(n => plainKey(body, n)))
|
|
427
|
+
return `$spread([0, ...$keys(maps, ${JSON.stringify(mapsIn(c.c).filter(mp => !fns.includes(mp)))})], ${names.length + 1})`;
|
|
428
|
+
const parts = names.map(n => breaksOf(body, n));
|
|
429
|
+
return `$spread(${breaksJs({ reads: parts.flatMap(b => b.reads), cmps: parts.flatMap(b => b.cmps) })}, ${names.length})`;
|
|
430
|
+
};
|
|
431
|
+
const every = (w) => { const keys = keysJs(w.value, w.each), dense = !plainKey(w.value, w.each), outer = bound; bound = { [w.each]: 'k' }; const body = ex(w.value); bound = outer; return `${dense ? '$dense' : '$every'}(${keys}, k => ${body})`; };
|
|
393
432
|
const writes = Object.entries(plain.step.maps ?? {});
|
|
394
433
|
const env = (input) => `{${[...m.params.map(p => `${id(p)}: $n(params[${id(p)}], ${id(`param ${p}`)})`), ...(input ? carried.map(i => `${id(i)}: $n(input[${id(i)}], ${id(`input ${i}`)})`) : [])].join(', ')}}`;
|
|
395
434
|
// Reads go through `maps` (with the maps the request carries); writes go to the state's own.
|
|
@@ -418,6 +457,7 @@ function $x(v) { if (v > 9007199254740991) throw new RangeError('a value passed
|
|
|
418
457
|
function $m(v, what) { if (!(v instanceof Map)) throw new RangeError(what + ' must be a Map of naturals'); for (const [key, x] of v) { $n(key, 'a key of ' + what); $n(x, 'a cell of ' + what); } return v; }
|
|
419
458
|
function $keys(maps, names) { const keys = new Set(); let past = 0; for (const c of Object.values(maps)) for (const key of c.keys()) past = Math.max(past, key + 1); for (const n of names) for (const key of maps[n].keys()) keys.add(key); return [...keys, past]; }
|
|
420
459
|
${fns.map((name, i) => `const fn${i} = (${m.fns[name].js});\n${m.fns[name].call ? `const fnc${i} = (${m.fns[name].call});\n` : ''}`).join('')}function $breaks(maps, reads, cmps) { const p = new Set([0]); for (const [n, d, minus] of reads) { for (const s of maps[n].keys()) { if (minus) p.add(s + d); else if (s >= d) p.add(s - d); } if (minus) p.add(d); } for (const [v, d, minus] of cmps) { if (minus) { p.add(v + d); p.add(d); } else if (v >= d) p.add(v - d); } const keys = new Set(); for (const q of p) for (let i = -1; i <= 2; i++) if (q + i >= 0) keys.add(q + i); const sorted = [...keys].sort((a, b) => a - b); sorted.push(sorted[sorted.length - 1] + 1); return sorted; }
|
|
460
|
+
function $spread(keys, n) { const out = new Set(); for (const k of keys) for (let i = 0; i < n; i++) out.add(k + i); return [...out].sort((a, b) => a - b); }
|
|
421
461
|
function $dense(keys, f) { const top = keys[keys.length - 1]; if (top > 1000000) throw new RangeError('a write at every key reads its key in arithmetic past a million keys (' + top + ')'); const d = f(top), cells = new Map(); for (let k = 0; k < top; k++) { const v = f(k); if (v !== d) cells.set(k, v); } return {cells, d}; }
|
|
422
462
|
function $every(keys, f) { const past = keys[keys.length - 1], d = f(past), cells = new Map(); for (let i = 0; i < keys.length - 1; i++) { const v = f(keys[i]); if (v !== d) cells.set(keys[i], v); } return {cells, d}; }
|
|
423
463
|
export function init(params) {
|
package/dist/core/solver.d.ts
CHANGED
|
@@ -2,6 +2,18 @@ export type Lin = Record<string, number>;
|
|
|
2
2
|
export type Constraint = {
|
|
3
3
|
lin: Lin;
|
|
4
4
|
label: string;
|
|
5
|
+
src?: RowSrc;
|
|
6
|
+
};
|
|
7
|
+
export type RowSrc = {
|
|
8
|
+
atom: number;
|
|
9
|
+
} | {
|
|
10
|
+
fact: number;
|
|
11
|
+
} | {
|
|
12
|
+
nat: string;
|
|
13
|
+
} | {
|
|
14
|
+
pair: [string, string];
|
|
15
|
+
} | {
|
|
16
|
+
term: string;
|
|
5
17
|
};
|
|
6
18
|
export declare const v: (name: string) => Lin;
|
|
7
19
|
export declare const k: (value: number) => Lin;
|
package/dist/core/solver.js
CHANGED
|
@@ -54,7 +54,8 @@ export const nat = (...names) => names.flatMap(name => le(k(0), v(name), `${name
|
|
|
54
54
|
export const negate = (c) => ({ lin: add(k(1), scale(c.lin, -1)), label: `not (${c.label})` });
|
|
55
55
|
const gcd = (a, b) => (b === 0 ? Math.abs(a) : gcd(b, a % b));
|
|
56
56
|
const vars = (lin) => Object.keys(lin).filter(key => key !== '1');
|
|
57
|
-
// maxRows: elimination can square the rows each round; past this many live rows
|
|
57
|
+
// maxRows: elimination can square the rows each round; past this many live rows, or a round that would make more than
|
|
58
|
+
// five times as many before merging them, it gives up with `unknown`.
|
|
58
59
|
export function solve(constraints, maxRows = 20_000) {
|
|
59
60
|
const steps = [];
|
|
60
61
|
const lins = [];
|
|
@@ -62,26 +63,41 @@ export function solve(constraints, maxRows = 20_000) {
|
|
|
62
63
|
// A row after its cut, if its variables share a divisor.
|
|
63
64
|
const tighten = (index) => {
|
|
64
65
|
const lin = lins[index];
|
|
65
|
-
|
|
66
|
+
let g = 0;
|
|
67
|
+
for (const key in lin)
|
|
68
|
+
if (key !== '1') {
|
|
69
|
+
g = gcd(g, lin[key]);
|
|
70
|
+
if (g === 1)
|
|
71
|
+
return index;
|
|
72
|
+
}
|
|
66
73
|
if (g <= 1)
|
|
67
74
|
return index;
|
|
68
|
-
const cut =
|
|
75
|
+
const cut = {};
|
|
76
|
+
for (const key in lin)
|
|
77
|
+
if (key !== '1')
|
|
78
|
+
cut[key] = lin[key] / g;
|
|
69
79
|
const constant = Math.ceil((lin['1'] ?? 0) / g);
|
|
70
80
|
return push({ kind: 'cut', of: index, g }, add(cut, k(constant)));
|
|
71
81
|
};
|
|
72
82
|
let rows = constraints.map((c, index) => tighten(push({ kind: 'input', index }, c.lin)));
|
|
73
|
-
|
|
83
|
+
// A row with no variable and a positive constant (kept per row: a row never changes once pushed).
|
|
84
|
+
const contra = [];
|
|
85
|
+
const isContra = (r) => (contra[r] ??= (lins[r]['1'] ?? 0) > 0 && Object.keys(lins[r]).every(key => key === '1'));
|
|
86
|
+
const contradiction = () => rows.find(isContra);
|
|
74
87
|
const eliminated = [];
|
|
75
88
|
let exact = true;
|
|
76
89
|
// Equalities first: a pair of rows that are each other's negation, with a variable of coefficient ±1, defines that
|
|
77
90
|
// variable. Substituting it into every other row (a combination with one row of the pair) eliminates it exactly,
|
|
78
91
|
// with |rows| new rows instead of the |pos| × |neg| of a plain elimination.
|
|
92
|
+
// A row's key and its negation's, kept per row (a row never changes once pushed).
|
|
93
|
+
const keyOf = (lin) => JSON.stringify(Object.entries(lin).sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0)));
|
|
94
|
+
const keys = [], negKeys = [];
|
|
95
|
+
const key = (r) => (keys[r] ??= keyOf(lins[r])), negKey = (r) => (negKeys[r] ??= keyOf(scale(lins[r], -1)));
|
|
79
96
|
for (let changed = true; changed && contradiction() === undefined;) {
|
|
80
97
|
changed = false;
|
|
81
|
-
const
|
|
82
|
-
const byKey = new Map(rows.map(r => [key(lins[r]), r]));
|
|
98
|
+
const byKey = new Map(rows.map(r => [key(r), r]));
|
|
83
99
|
for (const r of rows) {
|
|
84
|
-
const neg = byKey.get(
|
|
100
|
+
const neg = byKey.get(negKey(r));
|
|
85
101
|
if (neg === undefined || neg === r)
|
|
86
102
|
continue;
|
|
87
103
|
const x = vars(lins[r]).find(name => Math.abs(lins[r][name]) === 1);
|
|
@@ -105,17 +121,52 @@ export function solve(constraints, maxRows = 20_000) {
|
|
|
105
121
|
break;
|
|
106
122
|
}
|
|
107
123
|
}
|
|
124
|
+
const sigs = [];
|
|
125
|
+
const sigOf = (r) => {
|
|
126
|
+
let sig = sigs[r];
|
|
127
|
+
if (sig === undefined) {
|
|
128
|
+
const lin = lins[r], keys = [];
|
|
129
|
+
for (const key in lin)
|
|
130
|
+
if (key !== '1')
|
|
131
|
+
keys.push(key);
|
|
132
|
+
keys.sort();
|
|
133
|
+
sig = JSON.stringify(keys.map(key => [key, lin[key]]));
|
|
134
|
+
sigs[r] = sig;
|
|
135
|
+
}
|
|
136
|
+
return sig;
|
|
137
|
+
};
|
|
108
138
|
for (let found = contradiction(); found === undefined; found = contradiction()) {
|
|
109
|
-
|
|
110
|
-
|
|
139
|
+
// Each variable's rows by sign, in one pass (variables in the order they first appear).
|
|
140
|
+
const index = new Map();
|
|
141
|
+
for (const r of rows)
|
|
142
|
+
for (const [x, c] of Object.entries(lins[r])) {
|
|
143
|
+
if (x === '1')
|
|
144
|
+
continue;
|
|
145
|
+
let e = index.get(x);
|
|
146
|
+
if (!e)
|
|
147
|
+
index.set(x, (e = { pos: [], neg: [], posUnit: true, negUnit: true }));
|
|
148
|
+
if (c > 0) {
|
|
149
|
+
e.pos.push(r);
|
|
150
|
+
e.posUnit &&= c === 1;
|
|
151
|
+
}
|
|
152
|
+
else if (c < 0) {
|
|
153
|
+
e.neg.push(r);
|
|
154
|
+
e.negUnit &&= c === -1;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (index.size === 0)
|
|
111
158
|
break;
|
|
112
|
-
// Eliminate the variable that makes the fewest new rows, preferring exact eliminations.
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
159
|
+
// Eliminate the variable that makes the fewest new rows, preferring exact eliminations (the first such).
|
|
160
|
+
let choice;
|
|
161
|
+
for (const [x, e] of index) {
|
|
162
|
+
const c = { x, pos: e.pos, neg: e.neg, isExact: e.posUnit || e.negUnit, size: e.pos.length * e.neg.length - e.pos.length - e.neg.length };
|
|
163
|
+
if (!choice || (c.isExact && !choice.isExact) || (c.isExact === choice.isExact && c.size < choice.size))
|
|
164
|
+
choice = c;
|
|
165
|
+
}
|
|
166
|
+
if (!choice)
|
|
167
|
+
break;
|
|
168
|
+
if (choice.pos.length * choice.neg.length > 5 * maxRows)
|
|
169
|
+
return { kind: 'unknown' };
|
|
119
170
|
exact &&= choice.isExact;
|
|
120
171
|
eliminated.push({ x: choice.x, rows: [...choice.pos, ...choice.neg] });
|
|
121
172
|
const next = rows.filter(r => (lins[r][choice.x] ?? 0) === 0);
|
|
@@ -129,7 +180,7 @@ export function solve(constraints, maxRows = 20_000) {
|
|
|
129
180
|
// say nothing.
|
|
130
181
|
const best = new Map();
|
|
131
182
|
for (const r of next) {
|
|
132
|
-
const lin = lins[r], sig =
|
|
183
|
+
const lin = lins[r], sig = sigOf(r);
|
|
133
184
|
if (sig === '[]' && (lin['1'] ?? 0) <= 0)
|
|
134
185
|
continue;
|
|
135
186
|
const seen = best.get(sig);
|
package/dist/kernel/kernel.d.ts
CHANGED
|
@@ -29,10 +29,19 @@ export declare function truth(c: C, path: Path): boolean | undefined;
|
|
|
29
29
|
export declare function facts(c: C, path: Path, positive?: boolean): Lin[];
|
|
30
30
|
export declare function leafConstraints(goal: Goal, path: Path): Constraint[];
|
|
31
31
|
export declare function leafConstraintsReusing(goal: Goal, path: Path): Constraint[];
|
|
32
|
-
type
|
|
33
|
-
|
|
32
|
+
type Decisions = [string, boolean | undefined][];
|
|
33
|
+
type Node<T> = {
|
|
34
|
+
at: number;
|
|
35
|
+
key: string;
|
|
36
|
+
next: Map<boolean | undefined, Node<T>>;
|
|
37
|
+
} | {
|
|
38
|
+
reads: Decisions;
|
|
34
39
|
out: T;
|
|
35
|
-
}
|
|
40
|
+
};
|
|
41
|
+
export type Seen<T> = {
|
|
42
|
+
root?: Node<T>;
|
|
43
|
+
size?: number;
|
|
44
|
+
};
|
|
36
45
|
export declare function reusing<T>(seen: Seen<T>, path: Path, f: (path: Path) => T): T;
|
|
37
46
|
export declare function withProducts(rows: Constraint[], pairs: [number, number][]): Constraint[];
|
|
38
47
|
export declare function check(goal: Goal, tree: Tree, path?: Path): boolean;
|
|
@@ -45,6 +54,13 @@ export declare function substE(e: E, s: Subst): E;
|
|
|
45
54
|
export declare function substC(c: C, s: Subst): C;
|
|
46
55
|
export type Instances = Record<string, E[]>;
|
|
47
56
|
export declare const stateName: (n: string, j: number) => string;
|
|
57
|
+
export declare const boundOf: (c: Extract<C, {
|
|
58
|
+
op: "all";
|
|
59
|
+
}>) => string[];
|
|
60
|
+
export declare const witnessesOf: (f: string, q: Extract<C, {
|
|
61
|
+
op: "all";
|
|
62
|
+
}>) => string[];
|
|
63
|
+
export declare const crossFree: (c: C, keys: string[]) => C;
|
|
48
64
|
export declare const assumed: (laws: Law[], instances: Instances) => C[];
|
|
49
65
|
export declare function scope(m: Machine, laws?: Law[]): void;
|
|
50
66
|
export declare const keyTerm: (e: E, key: string) => boolean;
|