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 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 (`examples/sets.ts`, `examples/lists.ts`).
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, `M.each(k => ...)` to write every key, and
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
@@ -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 { op: 'all', key: `${prefix}${c.key}`, c: renameC(c.c, prefix, m, inputs) }; // the key renamed with its side
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.
@@ -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
- export const everyKey = (f) => { const key = `k$${++boundCounter}`; return new Cond({ op: 'all', key, c: f(v(key)).c }, everyOk(scope?.keys, k => f(keyTerm(k)).ok)); };
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 { op: 'all', key: c.key, c: swapC(c.c, from, to) };
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 outside the kernel`);
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' ? { op: 'all', key: c.key, 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) };
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 {
@@ -46,6 +46,7 @@ export type C = {
46
46
  } | {
47
47
  op: 'all';
48
48
  key: string;
49
+ keys?: string[];
49
50
  c: C;
50
51
  witness?: E;
51
52
  };
@@ -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.set(t, out);
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': return (plainKey(c.c, c.key) ? keysAt(st, mapsIn(c.c)) : breakKeys(breaksOf(c.c, c.key), env, st, m)).every(key => evalC(c.c, { ...env, [c.key]: key }, st, m));
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 === bound ? 'k' : 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)}]`;
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 = c.key;
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
- const b = breaksOf(t, key), off = (o) => (o ? ex(o) : '0');
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
- const every = (w) => { const keys = keysJs(w.value, w.each), dense = !plainKey(w.value, w.each), outer = bound; bound = w.each; const body = ex(w.value); bound = outer; return `${dense ? '$dense' : '$every'}(${keys}, k => ${body})`; };
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) {
@@ -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;
@@ -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 it gives up with `unknown`.
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
- const g = vars(lin).reduce((acc, key) => gcd(acc, lin[key]), 0);
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 = Object.fromEntries(vars(lin).map(key => [key, lin[key] / g]));
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
- const contradiction = () => rows.find(r => vars(lins[r]).length === 0 && (lins[r]['1'] ?? 0) > 0);
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 key = (lin) => JSON.stringify(Object.entries(lin).sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0)));
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(key(scale(lins[r], -1)));
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
- const remaining = [...new Set(rows.flatMap(r => vars(lins[r])))];
110
- if (remaining.length === 0)
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
- const cost = (x) => {
114
- const pos = rows.filter(r => (lins[r][x] ?? 0) > 0), neg = rows.filter(r => (lins[r][x] ?? 0) < 0);
115
- const isExact = pos.every(p => lins[p][x] === 1) || neg.every(n => lins[n][x] === -1);
116
- return { x, pos, neg, isExact, size: pos.length * neg.length - pos.length - neg.length };
117
- };
118
- const choice = remaining.map(cost).sort((a, b) => Number(b.isExact) - Number(a.isExact) || a.size - b.size)[0];
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 = JSON.stringify(vars(lin).sort().map(key => [key, lin[key]]));
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);
@@ -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 Seen<T> = {
33
- reads: [string, boolean | undefined][];
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;