@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.
- package/.zenodo.json +33 -9
- package/ACCOUNTING.md +7 -7
- package/AXIOMS.md +559 -0
- package/CHALLENGES.md +2181 -2479
- package/CITATION.cff +1 -1
- package/DEPLOY.md +2 -2
- package/DEVELOP.md +1 -1
- package/LICENSE +6 -5
- package/PRIOR-ART.md +149 -0
- package/README.md +221 -123
- package/SIGNATURE.md +2 -2
- package/TRIAL.md +69 -0
- package/WHITEPAPER.md +5 -5
- package/captain.md +2 -2
- package/compare.md +4 -4
- package/compute.md +9 -0
- package/dashboard.md +1 -1
- package/examples.md +5 -5
- package/forensic.md +67 -0
- package/guide.md +5 -5
- package/index.md +209 -139
- package/package.json +88 -9
- package/paper.md +7183 -0
- package/quantum.md +42 -0
- package/rights.md +39 -0
- package/solutions.md +7 -7
- package/speedup.md +4 -4
- package/src/0/index.ts +12 -2
- package/src/1/index.ts +1 -1
- package/src/2/frequency-scales.ts +13 -1
- package/src/2/index.ts +1 -1
- package/src/3/index.ts +1 -1
- package/src/4/index.ts +1 -1
- package/src/5/index.ts +1 -1
- package/src/6/index.ts +1 -1
- package/src/7/index.ts +1 -1
- package/src/9/funding.ts +2 -2
- package/src/api/gates.ts +117 -0
- package/src/api/index.ts +488 -0
- package/src/api/lanes.ts +81 -0
- package/src/demand/queries.json +1764 -0
- package/src/face/index.ts +100 -0
- package/src/html/index.ts +10 -0
- package/src/latex/index.ts +474 -0
- package/src/millennium/index.ts +53 -0
- package/src/proof/README.md +1 -1
- package/src/proof/address.lean +117 -0
- package/src/proof/coin.lean +148 -0
- package/src/proof/covered.json +9 -0
- package/src/proof/demand.lean +112 -0
- package/src/proof/demand2.lean +171 -0
- package/src/proof/demand3.lean +117 -0
- package/src/proof/discovered.json +10965 -1864
- package/src/proof/elementary.lean +393 -0
- package/src/proof/energy.lean +215 -0
- package/src/proof/families.lean +435 -0
- package/src/proof/fixtures/axiom-control.lean +39 -0
- package/src/proof/fnv.lean +99 -0
- package/src/proof/generated-theorems.json +207 -0
- package/src/proof/generated.lean +101 -0
- package/src/proof/imagined.lean +492 -0
- package/src/proof/index.lean +98 -18
- package/src/proof/index.ts +2 -2
- package/src/proof/involution.lean +90 -0
- package/src/proof/ledgerclaims.lean +84 -0
- package/src/proof/light.lean +135 -0
- package/src/proof/mechanical.lean +421 -0
- package/src/proof/merkaba.lean +89 -0
- package/src/proof/merkle.lean +132 -0
- package/src/proof/nim.lean +105 -0
- package/src/proof/phenomena.lean +90 -0
- package/src/proof/priorart.lean +189 -0
- package/src/proof/quantum.lean +161 -0
- package/src/proof/reach.lean +84 -0
- package/src/proof/recovered.lean +56 -0
- package/src/proof/reversal.lean +73 -0
- package/src/proof/rights.lean +157 -0
- package/src/proof/sequences.lean +105 -0
- package/src/proof/speed.lean +129 -0
- package/src/proof/split.lean +180 -0
- package/src/proof/theorems.lean +48 -1
- package/src/proof/trial-all.json +15260 -0
- package/src/proof/z9.lean +92 -0
- package/src/proof/z9plus.lean +208 -0
- package/src/proofs.lisp +3 -3
- package/src/prove/emit.ts +115 -0
- package/src/prove/index.ts +143 -0
- package/src/prove/translate.ts +638 -0
- package/src/publication/index.ts +363 -0
- package/src/quantum/field.ts +73 -0
- 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`
|