@ceccec/millennium-solutions 0.1.1 → 9.0.4

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.
Files changed (91) hide show
  1. package/.zenodo.json +33 -9
  2. package/ACCOUNTING.md +7 -7
  3. package/AXIOMS.md +559 -0
  4. package/CHALLENGES.md +2181 -2479
  5. package/CITATION.cff +1 -1
  6. package/DEPLOY.md +2 -2
  7. package/DEVELOP.md +1 -1
  8. package/LICENSE +6 -5
  9. package/PRIOR-ART.md +149 -0
  10. package/README.md +221 -123
  11. package/SIGNATURE.md +2 -2
  12. package/TRIAL.md +69 -0
  13. package/WHITEPAPER.md +5 -5
  14. package/captain.md +2 -2
  15. package/compare.md +4 -4
  16. package/compute.md +9 -0
  17. package/dashboard.md +1 -1
  18. package/examples.md +5 -5
  19. package/forensic.md +67 -0
  20. package/guide.md +5 -5
  21. package/index.md +209 -139
  22. package/package.json +88 -9
  23. package/paper.md +7183 -0
  24. package/quantum.md +42 -0
  25. package/rights.md +39 -0
  26. package/solutions.md +7 -7
  27. package/speedup.md +4 -4
  28. package/src/0/index.ts +12 -2
  29. package/src/1/index.ts +1 -1
  30. package/src/2/frequency-scales.ts +13 -1
  31. package/src/2/index.ts +1 -1
  32. package/src/3/index.ts +1 -1
  33. package/src/4/index.ts +1 -1
  34. package/src/5/index.ts +1 -1
  35. package/src/6/index.ts +1 -1
  36. package/src/7/index.ts +1 -1
  37. package/src/9/funding.ts +2 -2
  38. package/src/api/gates.ts +117 -0
  39. package/src/api/index.ts +488 -0
  40. package/src/api/lanes.ts +81 -0
  41. package/src/demand/queries.json +1764 -0
  42. package/src/face/index.ts +100 -0
  43. package/src/html/index.ts +10 -0
  44. package/src/latex/index.ts +474 -0
  45. package/src/millennium/index.ts +53 -0
  46. package/src/proof/README.md +1 -1
  47. package/src/proof/address.lean +117 -0
  48. package/src/proof/coin.lean +148 -0
  49. package/src/proof/covered.json +9 -0
  50. package/src/proof/demand.lean +112 -0
  51. package/src/proof/demand2.lean +171 -0
  52. package/src/proof/demand3.lean +117 -0
  53. package/src/proof/discovered.json +10965 -1864
  54. package/src/proof/elementary.lean +393 -0
  55. package/src/proof/energy.lean +215 -0
  56. package/src/proof/families.lean +435 -0
  57. package/src/proof/fixtures/axiom-control.lean +39 -0
  58. package/src/proof/fnv.lean +99 -0
  59. package/src/proof/generated-theorems.json +207 -0
  60. package/src/proof/generated.lean +101 -0
  61. package/src/proof/imagined.lean +492 -0
  62. package/src/proof/index.lean +98 -18
  63. package/src/proof/index.ts +2 -2
  64. package/src/proof/involution.lean +90 -0
  65. package/src/proof/ledgerclaims.lean +84 -0
  66. package/src/proof/light.lean +135 -0
  67. package/src/proof/mechanical.lean +421 -0
  68. package/src/proof/merkaba.lean +89 -0
  69. package/src/proof/merkle.lean +132 -0
  70. package/src/proof/nim.lean +105 -0
  71. package/src/proof/phenomena.lean +90 -0
  72. package/src/proof/priorart.lean +189 -0
  73. package/src/proof/quantum.lean +161 -0
  74. package/src/proof/reach.lean +84 -0
  75. package/src/proof/recovered.lean +56 -0
  76. package/src/proof/reversal.lean +73 -0
  77. package/src/proof/rights.lean +157 -0
  78. package/src/proof/sequences.lean +105 -0
  79. package/src/proof/speed.lean +129 -0
  80. package/src/proof/split.lean +180 -0
  81. package/src/proof/theorems.lean +48 -1
  82. package/src/proof/trial-all.json +15260 -0
  83. package/src/proof/z9.lean +92 -0
  84. package/src/proof/z9plus.lean +208 -0
  85. package/src/proofs.lisp +3 -3
  86. package/src/prove/emit.ts +115 -0
  87. package/src/prove/index.ts +143 -0
  88. package/src/prove/translate.ts +638 -0
  89. package/src/publication/index.ts +363 -0
  90. package/src/quantum/field.ts +73 -0
  91. package/src/quantum/tree.ts +79 -0
@@ -0,0 +1,638 @@
1
+ // TRANSLATE — the deposit's test idiom into Lean, mechanically.
2
+ //
3
+ // The tests are not arbitrary TypeScript. They are written in a small, repeated vocabulary — digits(),
4
+ // units(), m9(), .every, .some, **, === — because they all describe the same ring. A vocabulary that small is
5
+ // a language, and a language can be translated without judgement. The previous classifier called 173 of 176
6
+ // claims "needs an author" when many were `digits().every(d => ...)`, which is `(List.range' 1 9).all` and
7
+ // nothing more. That was a limit of the reader, not of the claims, and reporting it as the latter would have
8
+ // overstated how much of this genuinely needs a person.
9
+ //
10
+ // What is still refused: anything using a construct not in the table. Refusal is by whitelist, never by
11
+ // best-effort — a translation that guesses would produce a theorem that compiles and states the wrong thing,
12
+ // which is the exact failure this deposit has been cleaning out all along.
13
+
14
+ import { units as apiUnits, triad as apiTriad, orbit as apiOrbit } from '../api/index.ts'
15
+ import { BASE, TRINITY, A432_STEP } from '../0/index.ts'
16
+
17
+ /** the set literal a rendered theorem will carry — built from the computed set, never typed out */
18
+ const asSet = (xs: number[]) => '[' + xs.join(',') + ']'
19
+
20
+ /** The vocabulary. Order matters: longer patterns first. */
21
+ // PRE-RULES run BEFORE the refusal list. They are structural rewrites that REMOVE a construct rather than
22
+ // translate it, so the refusal list must judge what is left instead of what was written. Ordering them after
23
+ // REFUSE meant `join(` was rejected before the rewrite that eliminates it could fire — the refusal list was
24
+ // reading a form the translator no longer produces. Anything these do not consume is still refused below.
25
+ const PRE_RULES: [RegExp, string | ((...a: string[]) => string)][] = [
26
+ // `.reduce((a, b) => E, init)` is `.foldl (fun a b => E) init` — same order, same seed. It belongs BEFORE
27
+ // the refusal list, which rejects the word `reduce` outright; sitting after it, the rule could never fire.
28
+ // An earlier probe of mine also reported zero claims of this shape, looking for a pattern the bodies do not
29
+ // use — two ways of concluding "nothing here" about something that was there all along.
30
+ [/\.reduce\(\s*\(\s*([a-z]\w*)\s*,\s*([a-z]\w*)\s*\)\s*=>\s*([^,]+?),\s*([^)]+)\)/g, '.foldl (fun $1 $2 => $3) $4'],
31
+ // the bit operators, in their exact arithmetic form on {0,1} — see boolQuantifiers above for why this is
32
+ // sound only under the [0,1] binder it travels with.
33
+ [/\bNOT\(([^()]*)\)/g, '(1 - $1)'],
34
+ [/\bAND\(([^(),]*),\s*([^(),]*)\)/g, '($1 * $2)'],
35
+ [/\bOR\(([^(),]*),\s*([^(),]*)\)/g, '($1 + $2 - $1 * $2)'],
36
+ [/\bXOR\(([^(),]*),\s*([^(),]*)\)/g, '(($1 + $2) % 2)'],
37
+ // toUuid OF AN ASCII LITERAL IS toUuidBytes OF ITS CHARACTER CODES. The Lean port is pinned to the shipped
38
+ // implementation at published values (address.lean: to_uuid_bytes_of_a, to_uuid_bytes_of_uuidna), and the
39
+ // dashed-hex rendering is injective, so comparing two address STRINGS is comparing their byte lists. Only
40
+ // ASCII literals are rewritten: a character above 127 encodes to more than one byte and guessing which
41
+ // would be inventing the claim rather than reading it.
42
+ // the class EXCLUDES the apostrophe (0x27). Including it let the match run straight past the closing quote
43
+ // and swallow the rest of the expression, so `toUuid('a') === toUuid('a')` rendered as one enormous literal
44
+ // that happened to type-check. A greedy literal is worse than a refusal: it produces a theorem about the
45
+ // wrong string with no sign anything went wrong.
46
+ [/\btoUuid\('([\x20-\x26\x28-\x7e]*)'\)/g, ((_m: string, lit: string) =>
47
+ `Address.toUuidBytes [${[...lit].map((c) => c.charCodeAt(0)).join(', ')}]`) as unknown as string],
48
+ // COMPARING JOINED LISTS IS COMPARING THE LISTS. `a.join(',') === b.join(',')` holds exactly when a and b
49
+ // are equal, PROVIDED no element's rendering contains the separator — and every element here is a natural
50
+ // number, so none does. The join is a way of comparing lists in JavaScript, not part of the claim; keeping
51
+ // it would mean rendering string concatenation into Lean to say something about numbers.
52
+ [/\.join\('[^']*'\)\s*===\s*([\s\S]+?)\.join\('[^']*'\)/g, ' == $1'],
53
+ // slice(a,b) on a list is drop-then-take. Both bounds are literals wherever this fires, so the rewrite is
54
+ // exact rather than a guess about what the ends might be.
55
+ [/([A-Za-z_]\w*)\.slice\((\d+),\s*(\d+)\)/g, (_m: string, l: string, a: string, b: string) =>
56
+ `(List.take ${Number(b) - Number(a)} (List.drop ${a} ${l}))` as string] as unknown as [RegExp, string],
57
+ // THE IMPORTED CONSTANTS ARE NUMBERS, NOT CONSTRUCTS, and they fold HERE rather than in the table below so
58
+ // that the refusal list and the exactness check below both see what the test actually computes. `360 / BASE` is a
59
+ // division of two literals; `360 / <an identifier>` is not, and the difference decides whether the division
60
+ // may be kept at all. Read from src/0/index.ts: TRINITY = 3, BASE = TRINITY ** 2 = 9, and
61
+ // A432_STEP = 360 / BASE = 40 — exact in binary floating point, so the constant every test sees is the
62
+ // integer 40. Checked at the definition rather than guessed from the name.
63
+ // They come AFTER the toUuid rule on purpose: rewriting a literal first means a constant's name can never
64
+ // be substituted inside the string whose bytes are being taken.
65
+ [/\bA432_STEP\b/g, String(A432_STEP)],
66
+ [/\bBASE\b/g, String(BASE)],
67
+ [/\bTRINITY\b/g, String(TRINITY)],
68
+ ]
69
+
70
+ /** JAVASCRIPT DIVISION IS NOT LEAN'S NAT DIVISION, and the gap is silent. `9 / 2` is 4.5 in the test and 4
71
+ * in the kernel, so `9 / 2 === 4` is FALSE where the ledger runs it and TRUE where the kernel checks it —
72
+ * the exact failure this file exists to prevent: a theorem that compiles, passes the agreement check on its
73
+ * truth value, and states something else. Measured before touching it: thirteen already-translated claims
74
+ * carried a `/`, and among them `3 / 2 == 1.5` and `4 / 3 > 1.333` were being emitted, which are not even
75
+ * well-typed over Nat. Nothing here was ever right; it was surviving on the kernel throwing it out — and a
76
+ * rendering that depends on the kernel disagreeing is not a whitelist, it is a coin toss with a safety net.
77
+ *
78
+ * WHAT IS SAFE IS EXACTLY THIS: a division whose numerator and denominator are both CLOSED literal
79
+ * arithmetic and which leaves no remainder. `360 / 9` and `(8 * 7) / 2` are 40 and 28 under both readings,
80
+ * because truncation can only differ from real division when something is actually truncated. Anything else
81
+ * — a non-literal operand like `n * (360 / n)` or `f * 3 / 2`, or a literal pair with a remainder — keeps
82
+ * its slash and is refused by name below. Deciding whether a general JS division happens to land on an
83
+ * integer would mean evaluating the claim instead of reading it, which is not this translator's job.
84
+ *
85
+ * VERIFIED AND KEPT, NOT FOLDED AWAY. The first version replaced `360 / 9` with `40`, and that quietly made
86
+ * the theorem worse: `the_regular_nonagon_exterior_angle_is_the_a432_step` became `40 == 40 && 40 == 40`,
87
+ * a tautology where the ledger claims an arithmetic fact about the nonagon. A rendering may not weaken the
88
+ * claim any more than it may strengthen it. So the division is checked and then LEFT IN, with the slash
89
+ * parked on a marker character while the refusal list runs — the same neutralise-and-restore trick
90
+ * `fixChains` uses to advance a scan. The arithmetic is only ever used to decide, never to substitute.
91
+ */
92
+ const EXACT_DIV = '\u0001'
93
+
94
+ /** The value of closed literal arithmetic over `+` and `*`, or null if it is anything else. Deliberately
95
+ * excludes `-`: Nat subtraction truncates at zero, so a numerator written with a minus is not a value the
96
+ * two languages are guaranteed to agree on, and this function exists precisely to decide agreement. */
97
+ function litValue(s: string): number | null {
98
+ if (!/^[\d\s+*]+$/.test(s) || !/\d/.test(s)) return null
99
+ let total = 0
100
+ for (const term of s.split('+')) {
101
+ let prod = 1
102
+ for (const f of term.split('*')) {
103
+ const n = Number(f.trim())
104
+ if (!Number.isSafeInteger(n)) return null
105
+ prod *= n
106
+ }
107
+ total += prod
108
+ }
109
+ return Number.isSafeInteger(total) ? total : null
110
+ }
111
+
112
+ function markExactDivision(b: string): string {
113
+ return b.replace(/(?:\(([\d\s+*]+)\)|(?<![\d.])(\d+))\s*\/\s*(\d+)(?![\d.])/g,
114
+ (m: string, paren: string | undefined, bare: string | undefined, den: string) => {
115
+ const n = litValue(paren ?? bare ?? ''), d = Number(den)
116
+ return n === null || d === 0 || n % d !== 0 ? m : m.replace('/', EXACT_DIV)
117
+ })
118
+ }
119
+
120
+ /** `new Set(xs).size` IS `xs.eraseDups.length`, and it belongs in the PRE phase for the same reason `join`
121
+ * does: the rewrite REMOVES the construct, so it has to run before the refusal list judges what is left.
122
+ *
123
+ * THE RULE THAT COULD NOT FIRE. There was already an entry for this shape in the table below, and it had
124
+ * never once fired in the whole ledger. Two independent reasons, either of them fatal: `Set` is on the
125
+ * refusal list, so every body carrying it was rejected before the table was reached; and its argument
126
+ * pattern `[^)]+` cannot cross a nested call, so `new Set(digits().map(…)).size` — the form almost every
127
+ * claim actually uses — would not have matched even if it had been reached. A rule that cannot fire is not
128
+ * a rule, it is a comment that looks like code, and counting it as coverage is how a translator comes to
129
+ * believe it reads more than it does. Bracket-walking rather than a regex, because counting the brackets
130
+ * is the entire job.
131
+ *
132
+ * WHY THE TWO ARE THE SAME NUMBER. A JavaScript Set keeps one member per SameValueZero class; Lean's
133
+ * `List.eraseDups` keeps the first occurrence of each `BEq` class and drops the rest. On the values that
134
+ * can survive this translator — naturals built from literals, `+ * %` and the deposit's own lists, with
135
+ * negative literals and every string-producing call already refused — SameValueZero and Nat equality are
136
+ * the same relation, so the two keep the same elements and `.size` and `.length` count the same thing.
137
+ * Only the count is read, so the one difference that does exist — a Set has no order, eraseDups keeps
138
+ * first-seen order — is not observable in the claim. `new Set(xs).size === xs.length` therefore states
139
+ * distinctness on both sides, which is what the ledger's tests use it for.
140
+ *
141
+ * ONLY `new Set(E).size` IS CONSUMED, and that is what makes widening this safe. `.has(...)`, a Set bound
142
+ * to a name and read later, a `Set<number>` annotation, union or intersection built from spreads — none of
143
+ * them are touched, so the word `Set` is still in the text when the refusal list runs and they are still
144
+ * refused by name. The rewrite either eliminates the construct completely or leaves it to be refused;
145
+ * there is no third outcome where a Set survives half-translated.
146
+ */
147
+ function setSizeToLength(b: string): string {
148
+ let from = 0
149
+ for (;;) {
150
+ const i = b.indexOf('new Set(', from)
151
+ if (i < 0) return b
152
+ let d = 0, j = i + 'new Set'.length
153
+ for (; j < b.length; j++) { if (b[j] === '(') d++; else if (b[j] === ')') { d--; if (d === 0) break } }
154
+ if (j >= b.length) return b // unbalanced: leave it for the refusal list
155
+ // `.size` and nothing else. `.sizeOf` or a bare `new Set(x)` keeps the word Set and stays refused.
156
+ if (b.slice(j + 1, j + 6) !== '.size' || /[A-Za-z0-9_]/.test(b[j + 6] ?? '')) { from = i + 8; continue }
157
+ b = b.slice(0, i) + '(' + b.slice(i + 8, j) + ').eraseDups.length' + b.slice(j + 6)
158
+ from = i
159
+ }
160
+ }
161
+
162
+ const RULES: [RegExp, string][] = [
163
+ [/\bdigitalRoot\(/g, 'DR ('], // defined in the emitted preamble
164
+ // THE VOCABULARY'S SETS ARE COMPUTED, NOT TYPED. These four were literals, which made the translator a
165
+ // second opinion about what ℤ/9 is: it would have gone on rendering `units()` as [1,2,4,5,7,8] after the
166
+ // modulus moved, producing theorems that compile, pass the agreement check on their truth value, and state
167
+ // something about a ring nobody uses. Of every hardcoded set in this repo these were the dangerous ones,
168
+ // because they end up INSIDE sealed theorems rather than in a report. The API serves them computed and
169
+ // refuses them outright if the theorem proving them stops standing.
170
+ [/\bdigits\(\)/g, `(List.range' 1 ${BASE})`],
171
+ [/\bunits\(\)/g, asSet(apiUnits())],
172
+ [/\btriad\(\)/g, asSet(apiTriad().map((d) => (d === 0 ? 9 : d)))], // the deposit's prose writes 9 for 0
173
+ [/\bvortexOrbit\(\)/g, asSet(apiOrbit())],
174
+ [/\bm9\(/g, 'M9 ('], // wrapper defined in the emitted preamble
175
+ // BASE, TRINITY and A432_STEP fold in the PRE phase now — see the note there
176
+ [/\.every\(/g, '.all ('],
177
+ [/\.some\(/g, '.any ('],
178
+ [/\.includes\(/g, '.contains ('],
179
+ // the distinct-count idiom used to be a rule HERE and had never once fired; see setSizeToLength above
180
+ [/\.filter\(/g, '.filter ('],
181
+ // `.map(` NEEDS THE SPACE, and it was the one method in the vocabulary that never got a rule. Lean has no
182
+ // call-parenthesis syntax: `xs.map(f)` is a parse error (`unexpected token '('`), not a call. Every other
183
+ // method here — every, some, includes, filter — had its space inserted; map only ever appeared in output
184
+ // built by the loop rewrites, which write the space themselves, so the gap stayed invisible until a claim
185
+ // arrived with `.map(` written by hand. It reached the kernel, failed to parse, and was quarantined, which
186
+ // is the safety net doing its job and not a substitute for the rule.
187
+ [/\.map\(/g, '.map ('],
188
+ [/\.length\b/g, '.length'],
189
+ [/\(([a-z][a-zA-Z0-9]*)\)\s*=>/g, 'fun $1 =>'], // arrow to lambda, any single binder
190
+ [/\bArray\.from\(\{ ?length: ?(\d+) ?\}, ?\(_, ?([a-z])\) => \2\)/g, '(List.range $1)'],
191
+ [/\*\*/g, '^'],
192
+ [/===/g, '=='],
193
+ [/!==/g, '!='],
194
+ [/&&/g, '&&'],
195
+ [/\|\|/g, '||'],
196
+ ]
197
+
198
+ /** Constructs that have no faithful mechanical rendering — presence means refuse. */
199
+ const REFUSE = [
200
+ // merkleFold IS NOT THE SAME FUNCTION ON BOTH SIDES, and the difference is invisible in the name. The
201
+ // TypeScript merkleFold takes RAW STRINGS, sorts them as strings and merges them directly; the Lean port
202
+ // takes uuid BYTE-LISTS and renders each as dashed hex before merging. They agree only when every leaf is
203
+ // already an address. Measured over this ledger: of the fold claims written with an array literal, ZERO
204
+ // pass addresses and all thirteen pass raw strings — so translating any of them would produce a theorem
205
+ // that compiles, passes the agreement check on its truth value, and states something else. Refused by name,
206
+ // with the reason, which is the only version of this that helps anybody.
207
+ /\bmerkleFold\b/,
208
+ // `.find(...)` — REFUSED BY NAME, and the reason is the `!` rather than the Option.
209
+ //
210
+ // `List.find?` is the obvious counterpart and the Option is not the hard part: at depth one,
211
+ // `xs.find(p) === v` renders exactly as `xs.find? p == some v`, since a JS `undefined` is never equal to a
212
+ // number and `none` is never equal to `some v`. What the ledger actually writes is depth TWO —
213
+ // `const inv = (u) => U.find((w) => m9(u * w) === 1)!; return U.every((u) => inv(inv(u)) === u)` — and
214
+ // there the two languages part company. TypeScript's `!` is ERASED at runtime; it asserts, it does not
215
+ // check. So when the inner find misses, JavaScript does not stop: it calls `inv(undefined)` and runs the
216
+ // predicate again with `undefined` as an operand. Lean's `Option.bind` short-circuits to `none`.
217
+ //
218
+ // Those agree only when the predicate propagates the miss, which is a fact about the PREDICATE, not about
219
+ // the shape. `(w) => m9(u * w) === 1` does propagate — `undefined * w` is NaN and every comparison with
220
+ // NaN is false — but `(w) => w === 1` does not: JavaScript would find 1 and could return true where
221
+ // `none.bind` is false. A rule keyed on the shape would render both the same way and be wrong about the
222
+ // second, which is the "compiles and states something else" failure this file is built to avoid.
223
+ //
224
+ // Measured before deciding, over the whole queue: FIVE bodies contain `.find(`. Three are already refused
225
+ // for something unrelated (a negative literal, a JSON round-trip, an `undefined` sentinel compared
226
+ // directly), and the remaining two are both the depth-two involution above. So a depth-one rule would
227
+ // fire on nothing — the same dead rule as the `new Set` entry noted above — and a depth-two rule would
228
+ // need a side condition on the predicate's behaviour at `undefined`. Refused by name until a claim exists
229
+ // that a shape rule can read honestly.
230
+ /\.find\(/,
231
+ /\bsort\(/, /\bjoin\(/, /\bJSON\b/, /\bSet\b/, /\bMap\b/, /\bString\b/, /\bNumber\b/,
232
+ /\btoUuid\b/, /\bcomputes\b/, /\bmerkleFold\b/, /\bimprint/, /\breduce\(/, /\bflatMap\(/,
233
+ /\bmatch\(/, /\breplace\(/, /=>\s*\{/, /\bcls\(/,
234
+ /-\s*m9|m9\(\s*-/, // unary minus: Nat subtraction truncates, so refuse
235
+ // NEGATIVE LITERALS. Lean's Nat has none, and on Int the modulo differs — a knight-move test written with
236
+ // [-1, 2] rendered into a statement that is not the one the ledger asserts. Caught by reading the output,
237
+ // not by the count: it translated cleanly and was wrong. Refuse rather than pick a numeric tower.
238
+ /(^|[\s,[(])-\d/,
239
+ // DESTRUCTURED parameters: the arrow rule handles a single binder only, so ([a, b]) => would survive
240
+ // untranslated and change meaning silently.
241
+ /\(\s*\[[^\]]*\]\s*\)\s*=>/,
242
+ /\([a-z][a-zA-Z0-9]*\s*,\s*[a-z]/, // multi-parameter arrow: (x, i) => has no single-binder form
243
+ // ANY SLASH LEFT AFTER THE EXACTNESS CHECK. markExactDivision has already parked the slash of every
244
+ // literal-over-literal division that leaves no remainder on a marker character, so a surviving `/` is one
245
+ // of exactly two things: a division whose value differs between the test (real) and the kernel (Nat,
246
+ // truncating), or a regular-expression literal, which has no rendering here at all. Both must be refused,
247
+ // and refusing the character is the only check that cannot be fooled by which of the two it was.
248
+ /\//,
249
+ // A DECIMAL LITERAL IS NOT A NAT. `1.5` and `1.333` appeared in emitted statements and were only ever kept
250
+ // out of the sealed file by the kernel rejecting them; a rendering that relies on the kernel to catch it is
251
+ // not a whitelist. Refused where it is written, so the reason is the literal instead of a type error.
252
+ /\d\.\d/,
253
+ // `^` IS XOR IN JAVASCRIPT AND POWER IN LEAN, and the collision was live. The table below rewrites `**`
254
+ // into `^`, so by the time a statement is emitted the two are indistinguishable — and a claim written with
255
+ // the XOR operator was being rendered as exponentiation. It shipped as
256
+ // `(1 ^ 1) == 0 && (5 ^ 5) == 0 && ((3 ^ 5) ^ 0) == (3 ^ 5)`, which reads as 1¹ = 0, and the only thing
257
+ // that stopped it was `decide` proving it false. A rendering caught by the kernel disagreeing is not a
258
+ // whitelist working; it is a whitelist that missed and got lucky about the direction.
259
+ // This check runs BEFORE the table, which is what makes it exact: at this point `**` is still `**`, so a
260
+ // `^` in the text can only be the JavaScript XOR, and there is nothing to disambiguate.
261
+ /\^/,
262
+ // `toUuid(x).length` IS THE LENGTH OF A STRING, AND THE PORT RETURNS BYTES. This is the merkleFold lesson
263
+ // again, one method call further in. The address rewrite above is sound for COMPARING addresses, because
264
+ // the dashed-hex rendering is injective and two byte lists are equal exactly when their renderings are;
265
+ // but `.length` does not survive that argument. The JavaScript reads 36, the character count of
266
+ // `8-4-4-4-12` with its four dashes. `Address.toUuidBytes` (address.lean:49) returns `List Nat`, sixteen
267
+ // bytes, so the Lean side reads 16. Two claims were being emitted with `== 36` against a list of length
268
+ // sixteen — and one of them multiplied it out to `1000 * one == 36000`. They failed to parse before they
269
+ // could be false, which is the sort of near miss that gets recorded here rather than quietly fixed.
270
+ // Matched on the rewrite's own output, which is the only place the pair can be seen together: by the time
271
+ // the refusal list runs, `toUuid('out:a')` has already become `Address.toUuidBytes [111, …]`.
272
+ //
273
+ // The match is the CO-OCCURRENCE of the port and any `.length`, not the two written next to each other,
274
+ // because the first version of this rule only caught the adjacent form and a third claim slipped past it
275
+ // by binding the address to a name first (`let a := Address.toUuidBytes […]; … a.length == 36`). Once a
276
+ // value can be bound, "next to each other" stops being a property of the text. Coarse on purpose: it will
277
+ // refuse a body that takes the length of some unrelated list while also comparing addresses, and refusing
278
+ // more than necessary is the side a whitelist is supposed to err on.
279
+ /^(?=[\s\S]*toUuidBytes)(?=[\s\S]*\.length)/,
280
+ // THE TERNARY AND THE ZERO-ARGUMENT ARROW have no rendering here and were reaching the kernel as syntax
281
+ // errors — `(b ? 1 : 0)` and `let t := () => 7 % 3 == 1; t() == t()`. Lean's `if … then … else` would need
282
+ // the condition lifted to a Decidable proposition and the thunk would need a unit binder; neither is a
283
+ // rewrite, both are decisions about what the claim means. Refused where they are written so the reason is
284
+ // the construct rather than a parse error pointing at a column.
285
+ /\?/,
286
+ /\(\s*\)\s*=>/,
287
+ /<<|>>/, // bit shifts: Nat's are well-founded and pull in propext
288
+ /\[\s*[a-z]\w*\s*[\]\[]/, // indexing a[i]: Lean needs get!/getD and a bound proof
289
+ ]
290
+
291
+ export type Translation = { ok: true; lean: string } | { ok: false; why: string }
292
+
293
+ /** A body of `const a = X; const b = Y; return Z` is a chain of lets over one expression. Lean writes exactly
294
+ * that, so the shape is mechanical — the earlier version refused every such body and lost ~57 claims to a
295
+ * limitation of the reader rather than of the claims. Only simple `const NAME = EXPR;` bindings qualify;
296
+ * anything else (destructuring, functions, reassignment) still refuses. */
297
+ /** A BOUNDED FOR-PUSH LOOP IS A LIST COMPREHENSION. `const s = []; for (let k = A; k < B; k++) s.push(E)`
298
+ * builds exactly the list `(List.range' A (B-A)).map (fun k => E)` — same elements, same order, and the
299
+ * bound is a literal so the length is known. Refusing this shape as "control flow" was the same error as
300
+ * calling digit reversal string manipulation: the loop is not doing anything a map cannot say. Only the
301
+ * literal-bounded, single-push, no-branch form is rewritten; anything with a condition, an early exit, a
302
+ * second push or a non-literal bound is left refused, because those are genuinely not maps. */
303
+ function loopToMap(b: string): string | null {
304
+ // One pattern, matched whole: `const s = []; for (let k = A; k < B; k++) s.push(E);` followed by the rest.
305
+ // The push is anchored on `);` so a call inside E (digitalRoot(2 ** k)) is kept intact rather than cut at
306
+ // its first bracket — splitting the head from the push across two matches is what made the first version
307
+ // silently never fire.
308
+ const m = b.match(/^const\s+([A-Za-z_]\w*)\s*=\s*\[\s*\]\s*;\s*for\s*\(\s*(?:let|var)\s+([A-Za-z_]\w*)\s*=\s*(\d+)\s*;\s*\2\s*<\s*(\d+)\s*;\s*\2\+\+\s*\)\s*\{?\s*\1\.push\(([\s\S]*?)\)\s*;\s*\}?\s*([\s\S]*)$/)
309
+ if (!m) return null
310
+ const [, name, v, from, to, expr, after] = m
311
+ if (/\bfor\b|\bwhile\b|\.push\(/.test(after)) return null // a second loop or push is not this shape
312
+ const n = Number(to) - Number(from)
313
+ if (!(n > 0)) return null
314
+ const list = from === '0' ? `(List.range ${to})` : `(List.range' ${from} ${n})`
315
+ return `const ${name} = ${list}.map (fun ${v} => ${expr.trim()}); ${after.trim()}`
316
+ }
317
+
318
+ /** A NESTED COUNTING LOOP IS A COUNT OVER A PRODUCT. `let a = 0, b = 0; for (r) for (c) COND ? a++ : b++`
319
+ * walks every (r,c) pair once and sorts each into one of two buckets, which is exactly
320
+ * `((range N).flatMap fun r => (range M).map fun c => COND)` counted by truth — same pairs, same order, and
321
+ * the two totals sum to the product because every pair lands in exactly one bucket. Only the literal-bounded,
322
+ * two-counter, single-ternary form is rewritten: a break, a third counter or a condition on the loop bounds
323
+ * is a different program and stays refused. */
324
+ function countingLoop(b: string): string | null {
325
+ const m = b.match(/^let\s+([A-Za-z_]\w*)\s*=\s*0\s*,\s*([A-Za-z_]\w*)\s*=\s*0\s*;\s*for\s*\(\s*(?:let|var)\s+([A-Za-z_]\w*)\s*=\s*0\s*;\s*\3\s*<\s*(\d+)\s*;\s*\3\+\+\s*\)\s*for\s*\(\s*(?:let|var)\s+([A-Za-z_]\w*)\s*=\s*0\s*;\s*\5\s*<\s*(\d+)\s*;\s*\5\+\+\s*\)\s*(.+?)\s*\?\s*\1\+\+\s*:\s*\2\+\+\s*;\s*([\s\S]*)$/)
326
+ if (!m) return null
327
+ const [, a, bb, r, n, c, mm, cond, after] = m
328
+ if (/\bfor\b|\bwhile\b|\+\+/.test(after)) return null
329
+ const pairs = `((List.range ${n}).flatMap (fun ${r} => (List.range ${mm}).map (fun ${c} => ${cond.trim()})))`
330
+ return `const __p = ${pairs}; const ${a} = (__p.filter (fun z => z)).length; const ${bb} = __p.length - (__p.filter (fun z => z)).length; ${after.trim()}`
331
+ }
332
+
333
+ /** BOUNDED STATE ITERATION IS A FOLD. `let s = init; for (let k = 0; k < N; k++) s = f(s)` applies f exactly
334
+ * N times to init, which is `(List.range N).foldl (fun s _ => f s) init` — same function, same count, same
335
+ * order, and the bound is a literal so the count is known. I previously reported 327 for-loop claims as
336
+ * needing judgement to render; measuring the shapes instead of asserting them showed the largest bucket was
337
+ * this one, and it needs no judgement at all. The loop variable is unused in the body of every case this
338
+ * fires on, which is checked — if the body reads k it is a different construct and stays refused. */
339
+ function stateIteration(b: string): string | null {
340
+ const m = b.match(/^(?:let|const)\s+([A-Za-z_]\w*)\s*(?::\s*[^=]+)?=\s*([^;]+);\s*([\s\S]*?)for\s*\(\s*(?:let|var)\s+([A-Za-z_]\w*)\s*=\s*0\s*;\s*\4\s*<\s*(\d+)\s*;\s*\4\+\+\s*\)\s*\1\s*=\s*([^;]+);\s*([\s\S]*)$/)
341
+ if (!m) return null
342
+ const [, name, init, between, k, n, step, after] = m
343
+ if (new RegExp('\\b' + k + '\\b').test(step)) return null // the body reads the index: not a plain iteration
344
+ if (/\bfor\b|\bwhile\b/.test(after)) return null
345
+ return `${between}const ${name} = (List.range ${n}).foldl (fun ${name} _ => ${step.trim()}) (${init.trim()}); ${after.trim()}`
346
+ }
347
+
348
+ /** A CONDITIONAL PUSH OVER A RANGE IS A FILTER. `const t = []; for (let v = a; v <= b; v++) if (C) t.push(v)`
349
+ * collects exactly the values in the range satisfying C, in order — `(List.range' a (b-a+1)).filter (fun v => C)`.
350
+ * Only the form that pushes the loop VARIABLE itself is rewritten; pushing an expression of it is a map over
351
+ * the filter and is left alone rather than guessed at. */
352
+ function conditionalPush(b: string): string | null {
353
+ const m = b.match(/^const\s+([A-Za-z_]\w*)\s*(?::\s*[^=]+)?=\s*\[\s*\]\s*;\s*for\s*\(\s*(?:let|var)\s+([A-Za-z_]\w*)\s*=\s*(\d+)\s*;\s*\2\s*(<=?)\s*(\d+)\s*;\s*\2\+\+\s*\)\s*if\s*\(([\s\S]*?)\)\s*\1\.push\(\s*\2\s*\)\s*;\s*([\s\S]*)$/)
354
+ if (!m) return null
355
+ const [, name, v, from, cmp, to, cond, after] = m
356
+ if (/\bfor\b|\bwhile\b|\.push\(/.test(after)) return null
357
+ const len = Number(to) - Number(from) + (cmp === '<=' ? 1 : 0)
358
+ if (!(len > 0)) return null
359
+ const list = from === '0' && cmp === '<' ? `(List.range ${to})` : `(List.range' ${from} ${len})`
360
+ return `const ${name} = ${list}.filter (fun ${v} => ${cond.trim()}); ${after.trim()}`
361
+ }
362
+
363
+ /** AN EARLY-RETURN VALIDATION LOOP IS A UNIVERSAL QUANTIFIER. `for (let v = a; v <= b; v++) if (C) return
364
+ * false; return true` says exactly "no v in the range satisfies C", which is `(range).all (fun v => ¬ C)` —
365
+ * same range, same predicate, and the early exit is what `all` does. The two-level form nests the same way.
366
+ *
367
+ * This is the shape I twice reported as needing judgement. The first time I asserted it without measuring;
368
+ * the second time I measured with a classifier that matched the `v++` in the loop HEADER and filed 259 of
369
+ * these under "counter accumulation". Both readings were wrong, and the loop needs no judgement at all. A
370
+ * measurement is only as good as the thing doing the measuring, which is not a lesson I expected to need
371
+ * twice in one file. */
372
+ function validationLoop(b: string): string | null {
373
+ const one = /^for\s*\(\s*(?:let|var)\s+([A-Za-z_]\w*)\s*=\s*(\d+)\s*;\s*\1\s*(<=?)\s*(\d+)\s*;\s*\1\+\+\s*\)\s*if\s*\(([\s\S]*?)\)\s*return\s+false\s*;\s*return\s+true\s*;?\s*$/
374
+ const two = /^for\s*\(\s*(?:let|var)\s+([A-Za-z_]\w*)\s*=\s*(\d+)\s*;\s*\1\s*(<=?)\s*(\d+)\s*;\s*\1\+\+\s*\)\s*for\s*\(\s*(?:let|var)\s+([A-Za-z_]\w*)\s*=\s*(\d+)\s*;\s*\5\s*(<=?)\s*(\d+)\s*;\s*\5\+\+\s*\)\s*if\s*\(([\s\S]*?)\)\s*return\s+false\s*;\s*return\s+true\s*;?\s*$/
375
+ const span = (from: string, cmp: string, to: string) => {
376
+ const len = Number(to) - Number(from) + (cmp === '<=' ? 1 : 0)
377
+ if (!(len > 0)) return null
378
+ return from === '0' && cmp === '<' ? `(List.range ${to})` : `(List.range' ${from} ${len})`
379
+ }
380
+ const m2 = b.match(two)
381
+ if (m2) {
382
+ const [, v1, a1, c1, b1, v2, a2, c2, b2, cond] = m2
383
+ const s1 = span(a1, c1, b1), s2 = span(a2, c2, b2)
384
+ if (!s1 || !s2) return null
385
+ return `${s1}.all (fun ${v1} => ${s2}.all (fun ${v2} => ¬ (${cond.trim()})))`
386
+ }
387
+ const m1 = b.match(one)
388
+ if (m1) {
389
+ const [, v, from, cmp, to, cond] = m1
390
+ const sp = span(from, cmp, to)
391
+ if (!sp) return null
392
+ return `${sp}.all (fun ${v} => ¬ (${cond.trim()}))`
393
+ }
394
+ return null
395
+ }
396
+
397
+ /** AN ACCUMULATOR LOOP IS A MAP THEN A FOLD. `let s = 0; for (let k = a; k <= b; k++) s += f(k)` sums f over
398
+ * the range, which is `((range).map (fun k => f k)).foldl (·+·) 0` — same terms, same order, and addition
399
+ * associates so the grouping does not matter. `*=` with an initial 1 is the same statement for products.
400
+ * Found by bucketing the catch-all: 343 claims were sitting in "other", and this was the shape leading them.
401
+ * A bucket named "other" is where tractable work goes to be overlooked. */
402
+ function accumulatorLoop(b: string): string | null {
403
+ const m = b.match(/^let\s+([A-Za-z_]\w*)\s*(?::\s*[^=]+)?=\s*(0|1)\s*;\s*for\s*\(\s*(?:let|var)\s+([A-Za-z_]\w*)\s*=\s*(\d+)\s*;\s*\3\s*(<=?)\s*(\d+)\s*;\s*\3\+\+\s*\)\s*\1\s*(\+=|\*=)\s*([^;]+);\s*([\s\S]*)$/)
404
+ if (!m) return null
405
+ const [, acc, init, v, from, cmp, to, op, term, after] = m
406
+ if (/\bfor\b|\bwhile\b|\+=|\*=/.test(after)) return null
407
+ // the identity must match the operation, or the fold starts from the wrong place
408
+ if ((op === '+=' && init !== '0') || (op === '*=' && init !== '1')) return null
409
+ const len = Number(to) - Number(from) + (cmp === '<=' ? 1 : 0)
410
+ if (!(len > 0)) return null
411
+ const list = from === '0' && cmp === '<' ? `(List.range ${to})` : `(List.range' ${from} ${len})`
412
+ const fold = op === '+=' ? `.foldl (fun x y => x + y) 0` : `.foldl (fun x y => x * y) 1`
413
+ return `const ${acc} = (${list}.map (fun ${v} => ${term.trim()}))${fold}; ${after.trim()}`
414
+ }
415
+
416
+ /** THE DEPOSIT'S SHARED CONSTANTS — resolved, but only when the body has not rebound them.
417
+ * discover.ts binds `const U = units()` and `const ALL = digits()` at file scope, so tests written against
418
+ * them reach the translator with a name it cannot see. Resolving those two is the same move as rendering
419
+ * `units()` itself.
420
+ *
421
+ * The guard is the whole of it. One test binds its OWN `const U = [ONE, NEG(ONE), I, ...]` for the
422
+ * quaternions, and substituting the ℤ/9 units there would produce a theorem that compiles, passes the
423
+ * agreement check on its truth value, and states something about a completely different group. A shadowed
424
+ * name is a different name; a rewrite that cannot see the shadow is exactly the class of error this file
425
+ * exists to prevent. */
426
+ function sharedConstants(b: string, sets: { U: string; ALL: string }): string {
427
+ let out = b
428
+ // `:=` as well as `=`. By the time this runs the binding chain has already been rewritten into Lean form,
429
+ // so a shadowed name reads `let U := [...]` — and a guard matching only `=` did not see it. Tested with a
430
+ // body that rebinds U: without this it produced `let [1,2,4,5,7,8] := [1, 2]`, which is not a mistranslation
431
+ // that any later stage would have caught, because it type-checks.
432
+ const rebinds = (n: string) => new RegExp(`\\b(?:const|let)\\s+${n}\\s*:?=`).test(b)
433
+ if (!rebinds('U')) out = out.replace(/\bU\b/g, sets.U)
434
+ if (!rebinds('ALL')) out = out.replace(/\bALL\b/g, sets.ALL)
435
+ return out
436
+ }
437
+
438
+ function unwrapBindings(b: string): { lets: [string, string][]; expr: string } | null {
439
+ const lets: [string, string][] = []
440
+ let rest = b
441
+ for (;;) {
442
+ const m = rest.match(/^(?:const|let)\s+([A-Za-z_][A-Za-z0-9_]*)\s*(?::\s*[^=]+)?=\s*([^;]+);\s*/)
443
+ if (!m) break
444
+ // A bound FUNCTION is not a value binding and is still refused. A `.map (fun k => …)` is a value — the
445
+ // list — and the `=>` inside it belongs to the map, not to the binding. Distinguishing them is the
446
+ // difference between refusing a whole shape and refusing what actually cannot be rendered.
447
+ if (/\bfunction\b/.test(m[2])) return null
448
+ // A SINGLE-PARAMETER ARROW WITH AN EXPRESSION BODY IS A LEAN `fun`. `const f = (x: number) => E` is
449
+ // `let f := fun x => E` — same binder, same body, no statement sequence to lose. Only that exact form:
450
+ // a braced body, a second parameter, a default, or a rest parameter is a different construct and stays
451
+ // refused. Whatever the body turns out to contain is judged by the refusal list and the whitelist below
452
+ // like any other expression, so widening the binding shape does not widen what may appear inside it.
453
+ const arrow = m[2].match(/^\(\s*([A-Za-z_]\w*)\s*(?::\s*[A-Za-z_<>\[\]]+\s*)?\)\s*=>\s*([\s\S]+)$/)
454
+ if (arrow && !/^\s*\{/.test(arrow[2])) { lets.push([m[1], `fun ${arrow[1]} => ${arrow[2].trim()}`]); rest = rest.slice(m[0].length); continue }
455
+ // The guard exists to keep BOUND FUNCTIONS out, not arrows in general. A value like
456
+ // `(xs.filter (fun z => z)).length` is a number that happens to contain an arrow inside a call, and
457
+ // rejecting it threw away every binding produced by the loop rewrites above. Only a binding whose value
458
+ // IS an arrow is refused, and the parenthesised form is handled just above; this catches the bare one.
459
+ if (/^\s*[A-Za-z_]\w*\s*=>/.test(m[2])) return null
460
+ lets.push([m[1], m[2].trim()])
461
+ rest = rest.slice(m[0].length)
462
+ }
463
+ const r = rest.match(/^return\s+([\s\S]+?)\s*$/)
464
+ const expr = r ? r[1] : rest
465
+ if (/\b(const|let|return|if|for|while)\b/.test(expr)) return null
466
+ return { lets, expr: expr.replace(/;$/, '').trim() }
467
+ }
468
+
469
+ export function translate(body: string): Translation {
470
+ let b = body.replace(/\s+/g, ' ').trim().replace(/;$/, '')
471
+ if (!b) return { ok: false, why: 'empty body' }
472
+ // strip an outer block, then unwrap simple bindings into Lean lets
473
+ const block = b.match(/^\{\s*([\s\S]*?)\s*\}$/)
474
+ let prefix = ''
475
+ if (block || /^(?:const|let)\s/.test(b)) {
476
+ let inner = block ? block[1] : b
477
+ const counted = countingLoop(inner)
478
+ if (counted) inner = counted
479
+ const accumulated = accumulatorLoop(inner)
480
+ if (accumulated) inner = accumulated
481
+ const validated = validationLoop(inner)
482
+ if (validated) inner = validated
483
+ const iterated = stateIteration(inner)
484
+ if (iterated) inner = iterated
485
+ const pushed = conditionalPush(inner)
486
+ if (pushed) inner = pushed
487
+ const looped = loopToMap(inner)
488
+ if (looped) inner = looped
489
+ const u = unwrapBindings(inner)
490
+ if (!u) return { ok: false, why: 'body is not a chain of simple bindings over one expression' }
491
+ prefix = u.lets.map(([n, v]) => `let ${n} := ${v}; `).join('')
492
+ b = prefix + u.expr
493
+ }
494
+ b = sharedConstants(b, { U: asSet(apiUnits()), ALL: `(List.range' 1 ${BASE})` })
495
+ b = bitOps(boolQuantifiers(b))
496
+ for (const [re, to] of PRE_RULES) b = b.replace(re, to as string)
497
+ // pre-phase: both REMOVE a construct, so both run before the refusal list judges what is left
498
+ b = setSizeToLength(b)
499
+ b = markExactDivision(b)
500
+ for (const r of REFUSE) if (r.test(b)) return { ok: false, why: 'uses ' + String(r).slice(1, 24) + ' — no faithful mechanical rendering' }
501
+ b = b.split(EXACT_DIV).join('/') // the divisions that were checked, put back as written
502
+ let out = b
503
+ for (const [re, to] of RULES) out = out.replace(re, to)
504
+ out = fixChains(out)
505
+ // WHITELIST, not a heuristic. Every identifier surviving translation must be one this file put there or a
506
+ // binder it introduced. The previous check was a regex that let `nonHarmonic` and array indexing through;
507
+ // both compiled into nonsense the kernel rejected. Anything unrecognised is refused by name, so a failure
508
+ // is legible instead of mysterious.
509
+ // LEAN HAS NO CALL-PAREN SYNTAX. A bound function invoked as `rot(z)` is a parse error, and a parse error
510
+ // takes the WHOLE FILE down — after which the emitter's position-based attribution blames whichever theorem
511
+ // the reported line lands in. That is how an innocent rendering came to be quarantined: it compiled
512
+ // perfectly on its own and was collateral from a syntax error three theorems away. Refusing the shape here
513
+ // means the file never breaks, so nothing has to be blamed. Method calls are already rewritten to `.m (`
514
+ // with a space, so anything left in `name(` form is a JS call this translator cannot render.
515
+ const jsCall = out.match(/(?<![.\w])[a-z]\w*\(/)
516
+ if (jsCall && !/^(fun|let|if|then|else)\(/.test(jsCall[0]))
517
+ return { ok: false, why: `calls ${jsCall[0]}…) in JavaScript form — Lean applies by juxtaposition, and a parse error here breaks every theorem in the file` }
518
+
519
+ const ALLOWED = new Set(['List', 'range', 'all', 'any', 'contains', 'length', 'fun', 'M9', 'DR', 'true', 'false', 'let', 'eraseDups', 'filter', 'map', 'take', 'drop', 'Address', 'toUuidBytes', 'flatMap', '__p', 'foldl', '_', 'x', 'y', 'a', 'b'])
520
+ const binders = new Set([
521
+ ...[...out.matchAll(/fun ([a-z][a-zA-Z0-9]*) =>/g)].map((m) => m[1]),
522
+ ...[...out.matchAll(/let ([A-Za-z_][A-Za-z0-9_]*) :=/g)].map((m) => m[1]),
523
+ ])
524
+ const idents = [...out.matchAll(/\b([A-Za-z_][A-Za-z0-9_]*)\b/g)].map((m) => m[1])
525
+ const unknown = [...new Set(idents.filter((i) => !ALLOWED.has(i) && !binders.has(i)))]
526
+ if (unknown.length) return { ok: false, why: 'unknown identifier: ' + unknown.slice(0, 3).join(', ') }
527
+ return { ok: true, lean: out }
528
+ }
529
+
530
+ /** The preamble every emitted file needs: the deposit's own definitions, in Lean. */
531
+ /** JS METHOD CHAINS ARE NOT LEAN APPLICATION CHAINS. `xs.filter(f).length` needs no brackets in JavaScript,
532
+ * but its literal rendering `xs.filter (f).length` parses in Lean as `xs.filter ((f).length)` — the projection
533
+ * binds to the FUNCTION instead of to the filtered list, and the kernel reports a type error somewhere that
534
+ * looks nothing like the cause. This walks the brackets and wraps the call so the chain means what it meant:
535
+ * `(xs.filter (f)).length`. Paren-aware rather than a regex, because the predicate contains brackets of its
536
+ * own and counting them is the whole job. */
537
+ export function fixChains(t: string): string {
538
+ for (const head of ['.filter (', '.map (', '.all (', '.any (']) {
539
+ for (;;) {
540
+ const i = t.indexOf(head)
541
+ if (i < 0) break
542
+ let d = 0, j = i + head.length - 1
543
+ for (; j < t.length; j++) { if (t[j] === '(') d++; else if (t[j] === ')') { d--; if (d === 0) break } }
544
+ if (j >= t.length) break
545
+ const after = t.slice(j + 1)
546
+ const proj = after.match(/^\.(length|eraseDups|reverse)\b/)
547
+ if (!proj) { // nothing to fix here; neutralise this head so the scan advances
548
+ t = t.slice(0, i) + head.replace('.', '\u0000') + t.slice(i + head.length)
549
+ continue
550
+ }
551
+ // find the start of the receiver expression: back over a balanced bracketed term or an identifier
552
+ let k = i - 1
553
+ if (t[k] === ')' || t[k] === ']') {
554
+ const open = t[k] === ')' ? '(' : '[', close = t[k]
555
+ let e = 0
556
+ for (; k >= 0; k--) { if (t[k] === close) e++; else if (t[k] === open) { e--; if (e === 0) break } }
557
+ } else { while (k >= 0 && /[A-Za-z0-9_.']/.test(t[k])) k-- ; k++ }
558
+ t = t.slice(0, k) + '(' + t.slice(k, j + 1) + ')' + t.slice(j + 1)
559
+ }
560
+ t = t.replace(/\u0000/g, '.')
561
+ }
562
+ return t
563
+ }
564
+
565
+ /** Which Lean modules the rendered statement needs. Emitted as imports so the port is USED rather than
566
+ * restated — a second copy of toUuid inside the generated file would be one more thing to drift. */
567
+ export const importsFor = (lean: string): string[] =>
568
+ lean.includes('Address.') ? ['Address'] : []
569
+
570
+ /** THE BOOLEAN HELPERS, ON BITS. discover.ts defines B = [0,1] and quantifies with
571
+ * `all2(p) = B.every(a => B.every(b => p(a,b)))`, using bitwise NOT/AND/OR/XOR on those bits. Over {0,1}
572
+ * each has an exact arithmetic form — 1−x, a·b, a+b−a·b, (a+b)%2 — so the claims render without a single
573
+ * bitwise operator, which matters because Nat's `^^^` and `>>>` are well-founded and would cost the file its
574
+ * axiom-free status. The rewrite is sound ONLY because the binder ranges over [0,1] and nothing else; that
575
+ * is why the quantifier and the operators are rewritten together, and why a loose `NOT(` outside an all2 is
576
+ * left alone rather than assumed to be on a bit. */
577
+ /** The bit operators, rewritten INNERMOST-FIRST with balanced brackets. A pattern that cannot see nested
578
+ * calls leaves `NOT(AND(a,b))` half-translated and the whitelist then reports "unknown identifier: NOT",
579
+ * which is a true statement about a self-inflicted problem. This walks the arguments properly and repeats
580
+ * until nothing changes. */
581
+ function bitOps(t: string): string {
582
+ const arity: Record<string, number> = { NOT: 1, AND: 2, OR: 2, XOR: 2 }
583
+ for (let pass = 0; pass < 24; pass++) {
584
+ let hit = false
585
+ for (const name of ['NOT', 'AND', 'OR', 'XOR']) {
586
+ const i = t.indexOf(name + '(')
587
+ if (i < 0) continue
588
+ let d = 0, j = i + name.length
589
+ for (; j < t.length; j++) { if (t[j] === '(') d++; else if (t[j] === ')') { d--; if (d === 0) break } }
590
+ if (j >= t.length) continue
591
+ const inner = t.slice(i + name.length + 1, j)
592
+ if (/\b(NOT|AND|OR|XOR)\(/.test(inner)) continue // rewrite the inside first
593
+ const parts: string[] = []
594
+ let depth = 0, cur = ''
595
+ for (const ch of inner) {
596
+ if (ch === '(') depth++
597
+ if (ch === ')') depth--
598
+ if (ch === ',' && depth === 0) { parts.push(cur); cur = '' } else cur += ch
599
+ }
600
+ parts.push(cur)
601
+ if (parts.length !== arity[name]) continue
602
+ const [a, b] = parts.map((x) => x.trim())
603
+ const out = name === 'NOT' ? `(1 - ${a})`
604
+ : name === 'AND' ? `(${a} * ${b})`
605
+ : name === 'OR' ? `(${a} + ${b} - ${a} * ${b})`
606
+ : `((${a} + ${b}) % 2)`
607
+ t = t.slice(0, i) + out + t.slice(j + 1)
608
+ hit = true
609
+ }
610
+ if (!hit) break
611
+ }
612
+ return t
613
+ }
614
+
615
+ function boolQuantifiers(t: string): string {
616
+ for (const [name, vars] of [['all3', ['a', 'b', 'c']], ['all2', ['a', 'b']]] as [string, string[]][]) {
617
+ for (;;) {
618
+ const i = t.indexOf(name + '((')
619
+ if (i < 0) break
620
+ // find the arrow, then the balanced end of the call
621
+ const arrow = t.indexOf('=>', i)
622
+ if (arrow < 0) break
623
+ let d = 0, j = i + name.length
624
+ for (; j < t.length; j++) { if (t[j] === '(') d++; else if (t[j] === ')') { d--; if (d === 0) break } }
625
+ if (j >= t.length) break
626
+ const body = t.slice(arrow + 2, j).trim()
627
+ const open = vars.map((v) => `[0,1].all (fun ${v} => `).join('')
628
+ t = t.slice(0, i) + open + body + ')'.repeat(vars.length) + t.slice(j + 1)
629
+ }
630
+ }
631
+ return t
632
+ }
633
+
634
+ export const PREAMBLE = `def M9 (n : Nat) : Nat := n % 9
635
+
636
+ -- the digital root: 0 for 0, otherwise the residue mod 9 taken in 1..9 rather than 0..8. Written without
637
+ -- recursion so the kernel evaluates it directly.
638
+ def DR (n : Nat) : Nat := if n == 0 then 0 else 1 + (n - 1) % 9`