@ceccec/millennium-solutions 0.1.1 โ 9.0.6
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,100 @@
|
|
|
1
|
+
// THE FACE โ the shape a repository publishes so a sibling session can check it instead of believing it.
|
|
2
|
+
//
|
|
3
|
+
// A leaf: it imports only the content-address primitives, so scripts/mcp.ts can use it without executing
|
|
4
|
+
// scripts/metrics.ts, whose top level RUNS the gates. Sharing a type by importing the module that produces
|
|
5
|
+
// it would mean every MCP call ran six gates.
|
|
6
|
+
//
|
|
7
|
+
// The whole protocol is two fields. `command` makes a row reproducible rather than merely quotable, and
|
|
8
|
+
// `receipt` is taken over the row's OWN contents so any holder can recompute it from the file alone. What
|
|
9
|
+
// that proves is INTEGRITY โ the row says now what it said when sealed โ and nothing more. It does not make
|
|
10
|
+
// a figure correct, and it does not let a reader verify another repository's numbers without its source.
|
|
11
|
+
import { toUuid, merkleFold } from '../0/index.ts'
|
|
12
|
+
|
|
13
|
+
export type Metric = { key: string; claim: string; value: string; command: string; receipt: string }
|
|
14
|
+
export type Face = { repo: string; definition: string; protocol?: unknown; generatedFrom?: string; specWitness?: { seed: string; receipt: string }; rows: Metric[]; root: string }
|
|
15
|
+
|
|
16
|
+
/** THE FORMULA, WRITTEN DOWN AS DATA, because leaving it implicit made a sibling look tampered-with and
|
|
17
|
+
* writing it as prose let it go stale without anything noticing.
|
|
18
|
+
*
|
|
19
|
+
* Version 1 was one English sentence. erpax-94 replied with a structured spec โ field list, merge function
|
|
20
|
+
* with its separator named by codepoint, the RFC clause for the address, the fold's odd and empty cases โ
|
|
21
|
+
* and reading it against mine found two defects that a prose string had hidden:
|
|
22
|
+
*
|
|
23
|
+
* 1. `command` WAS NOT SEALED. The header of this file says `command` is what makes a row reproducible
|
|
24
|
+
* rather than merely quotable, and the receipt covered key, claim and value only. The instruction for
|
|
25
|
+
* how to recompute a figure could be altered and every receipt, and the root, would still verify. A
|
|
26
|
+
* reader following the altered command measures the wrong thing and concludes the row is confirmed.
|
|
27
|
+
* erpax covers it; version 2 covers it.
|
|
28
|
+
*
|
|
29
|
+
* 2. THE ROOT DOES NOT BIND ROW ORDER, and version 1 said it did โ "merkleFold(receipts in row order)".
|
|
30
|
+
* merkleFold SORTS its leaves. Measured: reversing every row of this repository's own face leaves the
|
|
31
|
+
* root identical and checkFace returns `intact`. A peer implementing the sentence as written would
|
|
32
|
+
* produce a different root and be reported as another convention, which is the exact accusation this
|
|
33
|
+
* constant exists to prevent โ the sentence was the unreliable part.
|
|
34
|
+
*
|
|
35
|
+
* The sort is kept, not fixed. Order-invariance by canonicalisation is the deposit's own established
|
|
36
|
+
* position โ quantum.lean proves it as `the_invariance_is_canonicalisation_not_physics` โ and rows here
|
|
37
|
+
* are addressed by key, not by position. What changes is that the spec now says so.
|
|
38
|
+
*
|
|
39
|
+
* Structured rather than prose so a receiving session can compare fields instead of diffing English. */
|
|
40
|
+
export const PROTOCOL = {
|
|
41
|
+
id: 'millennium-solutions/metric-face/2',
|
|
42
|
+
covers: ['key', 'claim', 'value', 'command'],
|
|
43
|
+
chained: false,
|
|
44
|
+
receipt: 'toUuid(key + LF + claim + LF + value + LF + command)',
|
|
45
|
+
merge: "toUuid(utf8(a + ':' + b)) โ COLON U+003A",
|
|
46
|
+
// NOT SHA-256. I wrote that line today, copying the shape of a sibling's spec without checking my own
|
|
47
|
+
// implementation, and it is false: src/0 hashes with FNV-1a, not SHA-2. erpax-94 then spent an audit
|
|
48
|
+
// trying 242 candidate SEEDS against row 0 โ four field orders, eight separators, five wrappers,
|
|
49
|
+
// canonical and as-emitted JSON โ and none could ever have matched, because the seed was already correct
|
|
50
|
+
// and the HASH was wrong. Their sha256 of my exact declared seed gives c2cc6344; my receipt begins
|
|
51
|
+
// f5c2a766. They localised it to "a different seed, not a different hash" and were right that we agreed
|
|
52
|
+
// on how bytes become an address; the disagreement was one layer below where either of us looked.
|
|
53
|
+
//
|
|
54
|
+
// This is precisely the defect the PROTOCOL constant exists to prevent โ a spec another repo cannot
|
|
55
|
+
// reproduce โ committed in the constant itself. It is also, for the second time today, the deposit
|
|
56
|
+
// describing a stronger property than it has: FNV-1a is a non-cryptographic hash, and naming SHA-256
|
|
57
|
+
// implies collision resistance these addresses do not carry.
|
|
58
|
+
address: 'uuidv8 shaped per RFC 9562 ยง5.8 (version nibble 8, variant 10x) over FNV-1a โ NOT SHA-2: '
|
|
59
|
+
+ 'four 32-bit FNV-1a passes over the UTF-16 code units, each starting at the FNV OFFSET BASIS 0x811c9dc5 '
|
|
60
|
+
+ 'XOR the pass seed โ seeds 0, 0x9e3779b9, 0x243f6a88, 0xb7e15162, so h begins at (0x811c9dc5 ^ seed) and '
|
|
61
|
+
+ 'NOT at the seed itself, which is the one line an implementer cannot guess. '
|
|
62
|
+
+ 'with an xor-shift-13 per code unit and a two-round murmur finalizer (0x85ebca6b, 0xc2b2ae35, then >>> 16), '
|
|
63
|
+
+ 'concatenated big-endian to 16 octets. Non-cryptographic: integrity against accident, not against an '
|
|
64
|
+
+ 'adversary.',
|
|
65
|
+
// ONE ROW, REPRODUCIBLE IN A SINGLE STEP. erpax-94's suggestion, taken: a reader computes this seed and
|
|
66
|
+
// compares with the receipt beside it. If the two disagree, the spec has drifted from the code that wrote
|
|
67
|
+
// the file โ which `generatedFrom` cannot tell them, because that says the FILE is current, not that the
|
|
68
|
+
// SPEC is current with the emitter.
|
|
69
|
+
specWitness: 'see the specWitness field on the face itself: the verbatim seed of row 0 and its receipt',
|
|
70
|
+
root: 'merkleFold: leaves sorted lexicographically, then pairwise merge up the tree; an odd element '
|
|
71
|
+
+ "carries up unchanged; zero rows fold to toUuid('empty-mind')",
|
|
72
|
+
orderInvariant: true,
|
|
73
|
+
} as const
|
|
74
|
+
|
|
75
|
+
/** A peer may send its protocol as a structured object or, like version 1 of this one, as a sentence. */
|
|
76
|
+
export const protocolId = (p: unknown): string =>
|
|
77
|
+
typeof p === 'string' ? p : (p && typeof p === 'object' && 'id' in p ? String((p as { id: unknown }).id) : '(none declared)')
|
|
78
|
+
|
|
79
|
+
export const receiptOf = (key: string, claim: string, value: string, command = ''): string =>
|
|
80
|
+
toUuid(key + '\n' + claim + '\n' + value + '\n' + command)
|
|
81
|
+
|
|
82
|
+
export const rootOf = (rows: Metric[]): string => merkleFold(rows.map((r) => r.receipt))
|
|
83
|
+
|
|
84
|
+
export type Verdict = 'intact' | 'altered' | 'different-convention'
|
|
85
|
+
|
|
86
|
+
/** Recompute every receipt and the root from the rows themselves. Integrity only โ see the header.
|
|
87
|
+
*
|
|
88
|
+
* EVERY ROW FAILING IS NOT TAMPERING. Someone altering a face changes a row or two; a face sealed under
|
|
89
|
+
* another formula fails at every row AND at the root, which is a signature of its own. The two are
|
|
90
|
+
* reported as different verdicts because accusing a peer of tampering is not a thing to get wrong. */
|
|
91
|
+
export function checkFace(f: Face): { verdict: Verdict; ok: boolean; altered: string[]; root: string; failingGates: string[] } {
|
|
92
|
+
const altered = f.rows.filter((r) => receiptOf(r.key, r.claim, r.value, r.command) !== r.receipt).map((r) => r.key)
|
|
93
|
+
const root = rootOf(f.rows)
|
|
94
|
+
const rootMatches = root === f.root
|
|
95
|
+
const wholesale = f.rows.length > 1 && altered.length === f.rows.length && !rootMatches
|
|
96
|
+
const stated = f.protocol !== undefined && protocolId(f.protocol) !== PROTOCOL.id
|
|
97
|
+
const verdict: Verdict = altered.length === 0 && rootMatches ? 'intact'
|
|
98
|
+
: (wholesale || stated) ? 'different-convention' : 'altered'
|
|
99
|
+
return { verdict, ok: verdict === 'intact', altered, root, failingGates: f.rows.filter((r) => r.value === 'FAIL').map((r) => r.key) }
|
|
100
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// HTML escaping, once.
|
|
2
|
+
//
|
|
3
|
+
// Three byte-identical copies of this lived in scripts/atom-feed.ts, scripts/paper.ts and src/latex โ and a
|
|
4
|
+
// fourth, in scripts/rights.ts, escaped `&` and `<` but not `>`. Four copies of a one-line function is four
|
|
5
|
+
// chances for one of them to be the odd one out, which is exactly what had already happened.
|
|
6
|
+
//
|
|
7
|
+
// This is a LEAF: it imports nothing, so src/latex โ a pure module with no filesystem dependency โ can use
|
|
8
|
+
// it without acquiring one.
|
|
9
|
+
export const escapeHtml = (s: string): string =>
|
|
10
|
+
s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
// LEAN โ LATEX (and MathML), for the subset this deposit's statements actually use.
|
|
2
|
+
//
|
|
3
|
+
// The paper printed every statement as verbatim Lean. That is honest and it is hard to read: a reader who
|
|
4
|
+
// wants the mathematics has to parse `(List.range' 1 9).all (fun d => (List.range' 1 9).any (fun e =>
|
|
5
|
+
// M9 (d + e) == 0))` in their head. This translates that to โ d โ {1,โฆ,9}, โ e โ {1,โฆ,9} : M9(d+e) = 0.
|
|
6
|
+
//
|
|
7
|
+
// A TRANSLATION CAN LIE, so this one is built to refuse rather than approximate:
|
|
8
|
+
//
|
|
9
|
+
// ยท It is a real parser, not a chain of regex substitutions. A substitution pass silently produces
|
|
10
|
+
// plausible-looking output for input it did not understand, which is the worst possible failure here โ
|
|
11
|
+
// a formula that reads as mathematics and is not the theorem.
|
|
12
|
+
// ยท Parsing is TOTAL or it FAILS. Every token must be consumed and every construct must be one this
|
|
13
|
+
// grammar knows; anything else throws, and the caller prints the verbatim Lean instead. There is no
|
|
14
|
+
// partial rendering.
|
|
15
|
+
// ยท The verbatim Lean stays on the page next to the typeset form. The kernel checked the Lean, not this.
|
|
16
|
+
// The rendering is a reading aid and is labelled as one.
|
|
17
|
+
//
|
|
18
|
+
// The vocabulary was derived by counting what the 460 statements contain, not guessed: `.all` (280),
|
|
19
|
+
// `.contains` (124), `.length` (120), `List.range` (113), `.map` (96), `List.range'` (82), `.eraseDups`
|
|
20
|
+
// (74), `.filter` (36), `.any` (33), `.foldl` (27), and the arithmetic and logical operators.
|
|
21
|
+
|
|
22
|
+
import { escapeHtml } from '../html/index.ts'
|
|
23
|
+
|
|
24
|
+
// โโ tokens โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
25
|
+
type Tok = { k: 'num' | 'id' | 'op'; v: string }
|
|
26
|
+
|
|
27
|
+
const OPS3 = ['>>>', '<<<']
|
|
28
|
+
const OPS2 = ['==', '!=', '<=', '>=', '=>', '++', '&&', '||', ':=']
|
|
29
|
+
const OPS1 = '+-*/%^<>=()[],.ยฌโงโจโโโ โคโฅยท!:;'
|
|
30
|
+
|
|
31
|
+
export function lex(src: string): Tok[] {
|
|
32
|
+
const out: Tok[] = []
|
|
33
|
+
let i = 0
|
|
34
|
+
while (i < src.length) {
|
|
35
|
+
const c = src[i]
|
|
36
|
+
if (/\s/.test(c)) { i++; continue }
|
|
37
|
+
if (/[0-9]/.test(c)) {
|
|
38
|
+
let j = i; while (j < src.length && /[0-9]/.test(src[j])) j++
|
|
39
|
+
out.push({ k: 'num', v: src.slice(i, j) }); i = j; continue
|
|
40
|
+
}
|
|
41
|
+
if (/[A-Za-z_]/.test(c)) {
|
|
42
|
+
// A DOT JOINS THE NAME ONLY FOR A QUALIFIED CONSTANT โ one whose namespace is capitalised, as
|
|
43
|
+
// Lean writes them: List.range, Families.gcd'. Treating any dot as part of the name swallowed
|
|
44
|
+
// `axis.contains d` into one identifier and rendered it `axis.contains(d)` instead of `d โ axis`:
|
|
45
|
+
// a silently plausible formula that was not the theorem, which is the one failure this module
|
|
46
|
+
// exists to prevent. A lowercase receiver ends the identifier, so the dot becomes a method call.
|
|
47
|
+
const qualified = /[A-Z]/.test(c)
|
|
48
|
+
let j = i
|
|
49
|
+
while (j < src.length && (/[A-Za-z0-9_']/.test(src[j]) || src[j] === '!' || src[j] === '?' || (qualified && src[j] === '.' && /[A-Za-z_]/.test(src[j + 1] ?? '')))) j++
|
|
50
|
+
out.push({ k: 'id', v: src.slice(i, j) }); i = j; continue
|
|
51
|
+
}
|
|
52
|
+
const three = src.slice(i, i + 3), two = src.slice(i, i + 2)
|
|
53
|
+
if (OPS3.includes(three)) { out.push({ k: 'op', v: three }); i += 3; continue }
|
|
54
|
+
if (OPS2.includes(two)) { out.push({ k: 'op', v: two }); i += 2; continue }
|
|
55
|
+
if (OPS1.includes(c)) { out.push({ k: 'op', v: c }); i++; continue }
|
|
56
|
+
throw new Error('lex: unexpected character ' + JSON.stringify(c))
|
|
57
|
+
}
|
|
58
|
+
return out
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// โโ AST โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
62
|
+
export type Node =
|
|
63
|
+
| { t: 'num'; v: string }
|
|
64
|
+
| { t: 'id'; v: string }
|
|
65
|
+
| { t: 'bin'; op: string; l: Node; r: Node }
|
|
66
|
+
| { t: 'un'; op: string; e: Node }
|
|
67
|
+
| { t: 'app'; f: Node; a: Node }
|
|
68
|
+
| { t: 'dot'; o: Node; name: string }
|
|
69
|
+
| { t: 'lam'; ps: string[]; b: Node }
|
|
70
|
+
| { t: 'list'; xs: Node[] }
|
|
71
|
+
| { t: 'ite'; c: Node; a: Node; b: Node }
|
|
72
|
+
| { t: 'hole'; op: string } // (ยท + ยท) โ the operator section
|
|
73
|
+
| { t: 'asc'; e: Node; ty: Node } // (1 : Int) โ a type ascription; the value is unchanged
|
|
74
|
+
| { t: 'tuple'; xs: Node[] } // (a, b)
|
|
75
|
+
| { t: 'proj'; o: Node; i: string } // p.1, p.2 โ tuple projection
|
|
76
|
+
| { t: 'let'; name: string; e: Node; b: Node } // let x := e; body
|
|
77
|
+
|
|
78
|
+
const KEYWORD = new Set(['then', 'else', 'fun', 'if', 'let'])
|
|
79
|
+
|
|
80
|
+
class P {
|
|
81
|
+
i = 0
|
|
82
|
+
ts: Tok[]
|
|
83
|
+
// An explicit field, not a parameter property: this repo runs .ts through node directly, and node's
|
|
84
|
+
// strip-only TypeScript mode rejects `constructor(private ts: Tok[])`.
|
|
85
|
+
constructor(ts: Tok[]) { this.ts = ts }
|
|
86
|
+
peek(o = 0) { return this.ts[this.i + o] }
|
|
87
|
+
is(v: string, o = 0) { const t = this.peek(o); return !!t && t.v === v }
|
|
88
|
+
eat(v: string) { if (!this.is(v)) throw new Error('expected ' + v + ' got ' + (this.peek()?.v ?? 'EOF')); this.i++ }
|
|
89
|
+
done() { return this.i >= this.ts.length }
|
|
90
|
+
|
|
91
|
+
// Precedence, loosest first. Lean's real table is larger; this covers exactly what the statements use.
|
|
92
|
+
expr(): Node {
|
|
93
|
+
// `let x := e; body` โ a local binding. Ten statements use it, and it is written as mathematics the way
|
|
94
|
+
// mathematics writes it: the body, then "where x = e". The binding is NOT substituted into the body;
|
|
95
|
+
// substituting would show the reader an expression the source does not contain.
|
|
96
|
+
if (this.is('let')) {
|
|
97
|
+
this.i++
|
|
98
|
+
const n = this.peek(); if (!n || n.k !== 'id') throw new Error('expected a name after let')
|
|
99
|
+
this.i++; this.eat(':='); const e = this.expr(); this.eat(';')
|
|
100
|
+
return { t: 'let', name: n.v, e, b: this.expr() }
|
|
101
|
+
}
|
|
102
|
+
return this.iff()
|
|
103
|
+
}
|
|
104
|
+
iff(): Node { let l = this.implies(); while (this.is('โ')) { this.i++; l = { t: 'bin', op: 'โ', l, r: this.implies() } } return l }
|
|
105
|
+
implies(): Node { let l = this.or(); if (this.is('โ')) { this.i++; return { t: 'bin', op: 'โ', l, r: this.implies() } } return l }
|
|
106
|
+
or(): Node { let l = this.and(); while (this.is('โจ') || this.is('||')) { this.i++; l = { t: 'bin', op: 'โจ', l, r: this.and() } } return l }
|
|
107
|
+
and(): Node { let l = this.cmp(); while (this.is('โง') || this.is('&&')) { this.i++; l = { t: 'bin', op: 'โง', l, r: this.cmp() } } return l }
|
|
108
|
+
cmp(): Node {
|
|
109
|
+
let l = this.shift()
|
|
110
|
+
while (['==', '=', '!=', 'โ ', '<=', 'โค', '>=', 'โฅ', '<', '>'].some((o) => this.is(o))) {
|
|
111
|
+
const op = this.peek()!.v; this.i++; l = { t: 'bin', op, l, r: this.shift() }
|
|
112
|
+
}
|
|
113
|
+
return l
|
|
114
|
+
}
|
|
115
|
+
shift(): Node { let l = this.add(); while (this.is('>>>') || this.is('<<<')) { const op = this.peek()!.v; this.i++; l = { t: 'bin', op, l, r: this.add() } } return l }
|
|
116
|
+
add(): Node { let l = this.mul(); while (this.is('+') || this.is('-') || this.is('++')) { const op = this.peek()!.v; this.i++; l = { t: 'bin', op, l, r: this.mul() } } return l }
|
|
117
|
+
mul(): Node { let l = this.pow(); while (this.is('*') || this.is('/') || this.is('%')) { const op = this.peek()!.v; this.i++; l = { t: 'bin', op, l, r: this.pow() } } return l }
|
|
118
|
+
pow(): Node { const l = this.unary(); if (this.is('^')) { this.i++; return { t: 'bin', op: '^', l, r: this.pow() } } return l }
|
|
119
|
+
unary(): Node { if (this.is('ยฌ') || this.is('!')) { this.i++; return { t: 'un', op: 'ยฌ', e: this.unary() } } if (this.is('-')) { this.i++; return { t: 'un', op: '-', e: this.unary() } } return this.appl() }
|
|
120
|
+
|
|
121
|
+
/** Application is juxtaposition: `M9 (d + e)`, `List.range 9`, `refl (refl d)`. */
|
|
122
|
+
appl(): Node {
|
|
123
|
+
let f = this.post()
|
|
124
|
+
while (!this.done() && this.startsAtom()) f = { t: 'app', f, a: this.post() }
|
|
125
|
+
return f
|
|
126
|
+
}
|
|
127
|
+
startsAtom(): boolean {
|
|
128
|
+
const t = this.peek()!
|
|
129
|
+
// `then` and `else` lex as identifiers, so juxtaposition happily consumed them as arguments and the
|
|
130
|
+
// whole `if _ then _ else _` failed to parse. They terminate an application instead.
|
|
131
|
+
if (t.k === 'id') return !KEYWORD.has(t.v)
|
|
132
|
+
if (t.k === 'num') return true
|
|
133
|
+
return t.v === '(' || t.v === '['
|
|
134
|
+
}
|
|
135
|
+
post(): Node {
|
|
136
|
+
let e = this.atom()
|
|
137
|
+
while (this.is('.')) {
|
|
138
|
+
this.i++
|
|
139
|
+
const n = this.peek()
|
|
140
|
+
if (!n) throw new Error('expected method name')
|
|
141
|
+
// `p.1` is a projection, not a method โ the dot is followed by a numeral.
|
|
142
|
+
if (n.k === 'num') { this.i++; e = { t: 'proj', o: e, i: n.v }; continue }
|
|
143
|
+
if (n.k !== 'id') throw new Error('expected method name')
|
|
144
|
+
this.i++; e = { t: 'dot', o: e, name: n.v }
|
|
145
|
+
}
|
|
146
|
+
return e
|
|
147
|
+
}
|
|
148
|
+
atom(): Node {
|
|
149
|
+
const t = this.peek()
|
|
150
|
+
if (!t) throw new Error('unexpected end of statement')
|
|
151
|
+
if (t.k === 'num') { this.i++; return { t: 'num', v: t.v } }
|
|
152
|
+
if (t.v === 'fun') {
|
|
153
|
+
// SEVERAL PARAMETERS. `fun acc i => โฆ` binds two; reading only the first left the second where the
|
|
154
|
+
// arrow was expected, which is the "expected => got i" this grammar used to fail on. Curried into
|
|
155
|
+
// nested lambdas, which is what Lean means by it.
|
|
156
|
+
this.i++
|
|
157
|
+
const ps: string[] = []
|
|
158
|
+
while (this.peek() && this.peek()!.k === 'id' && !this.is('=>')) { ps.push(this.peek()!.v); this.i++ }
|
|
159
|
+
if (!ps.length) throw new Error('expected lambda parameter')
|
|
160
|
+
this.eat('=>')
|
|
161
|
+
// The parameter LIST is kept, not curried into nested lambdas. Currying is what Lean means, but
|
|
162
|
+
// unparse then writes back an extra `fun` and `=>` per parameter and the round-trip reports a loss
|
|
163
|
+
// that never happened. The tree records what was written.
|
|
164
|
+
return { t: 'lam', ps, b: this.expr() }
|
|
165
|
+
}
|
|
166
|
+
if (t.v === 'if') {
|
|
167
|
+
this.i++; const c = this.expr(); this.eat('then'); const a = this.expr(); this.eat('else'); const b = this.expr()
|
|
168
|
+
return { t: 'ite', c, a, b }
|
|
169
|
+
}
|
|
170
|
+
if (t.k === 'id') { this.i++; return { t: 'id', v: t.v } }
|
|
171
|
+
if (t.v === '(') {
|
|
172
|
+
this.i++
|
|
173
|
+
// the operator section (ยท + ยท) โ a lambda written in Lean's dot notation
|
|
174
|
+
if (this.is('ยท')) {
|
|
175
|
+
this.i++; const op = this.peek(); if (!op || op.k !== 'op') throw new Error('expected operator in section')
|
|
176
|
+
this.i++; this.eat('ยท'); this.eat(')')
|
|
177
|
+
return { t: 'hole', op: op.v }
|
|
178
|
+
}
|
|
179
|
+
const e = this.expr()
|
|
180
|
+
// `(1 : Int)` โ an ascription names the type and leaves the value alone. The type is KEPT in the tree
|
|
181
|
+
// so unparse can put it back; only the rendering drops it, because a reader of the mathematics wants
|
|
182
|
+
// the value and the round-trip wants every token.
|
|
183
|
+
if (this.is(':')) { this.i++; const ty = this.expr(); this.eat(')'); return { t: 'asc', e, ty } }
|
|
184
|
+
if (this.is(',')) {
|
|
185
|
+
const xs = [e]
|
|
186
|
+
while (this.is(',')) { this.i++; xs.push(this.expr()) }
|
|
187
|
+
this.eat(')'); return { t: 'tuple', xs }
|
|
188
|
+
}
|
|
189
|
+
this.eat(')'); return e
|
|
190
|
+
}
|
|
191
|
+
if (t.v === '[') {
|
|
192
|
+
this.i++
|
|
193
|
+
const xs: Node[] = []
|
|
194
|
+
if (!this.is(']')) { xs.push(this.expr()); while (this.is(',')) { this.i++; xs.push(this.expr()) } }
|
|
195
|
+
this.eat(']')
|
|
196
|
+
return { t: 'list', xs }
|
|
197
|
+
}
|
|
198
|
+
throw new Error('unexpected token ' + JSON.stringify(t.v))
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Parse a statement, or throw. Throwing is the point: the caller prints the Lean source instead. */
|
|
203
|
+
export function parse(src: string): Node {
|
|
204
|
+
const p = new P(lex(src))
|
|
205
|
+
const e = p.expr()
|
|
206
|
+
if (!p.done()) throw new Error('trailing tokens from ' + JSON.stringify(p.peek()!.v))
|
|
207
|
+
return e
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
/** `.filter f` and `.all f` pass a FUNCTION, not a lambda โ `instruments.filter claim`. The set-builder
|
|
212
|
+
* needs a bound variable the source never wrote, so one is supplied. `x` is chosen because these
|
|
213
|
+
* statements bind d, e, k, n, p, u and t, never x; a collision would shadow a real binder. */
|
|
214
|
+
const BOUND = 'x'
|
|
215
|
+
const applied = (f: Node, texOf: (n: Node) => string) => `${texOf(f)}\\mathopen{}\\left(${BOUND}\\right)`
|
|
216
|
+
|
|
217
|
+
// โโ LaTeX โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
218
|
+
export const LATEX_NAME: Record<string, string> = { '==': '=', '=': '=', '!=': '\\neq', 'โ ': '\\neq', '<=': '\\le', 'โค': '\\le', '>=': '\\ge', 'โฅ': '\\ge', '<': '<', '>': '>', '+': '+', '-': '-', '*': '\\cdot', '/': '/', '%': '\\bmod', 'โง': '\\land', 'โจ': '\\lor', 'โ': '\\to', 'โ': '\\leftrightarrow', '++': '\\mathbin{+\\!\\!+}' }
|
|
219
|
+
|
|
220
|
+
const idTex = (v: string) => (/^[a-zA-Z]$/.test(v) ? v : '\\mathrm{' + v.replace(/_/g, '\\_') + '}')
|
|
221
|
+
|
|
222
|
+
/** The domain a range denotes, as a set. List.range n = [0,โฆ,n-1]; List.range' a n = [a,โฆ,a+n-1]. */
|
|
223
|
+
const num = (n: Node): number | null => (n.t === 'num' ? Number(n.v) : null)
|
|
224
|
+
const rangeTex = (f: Node, args: Node[]): string | null => {
|
|
225
|
+
if (f.t !== 'id') return null
|
|
226
|
+
// With literal bounds the set is written out โ {1,โฆ,9}, not {1,โฆ,1+9-1}, which is the same set
|
|
227
|
+
// spelled as the arithmetic that produced it.
|
|
228
|
+
if (f.v === 'List.range' && args.length === 1) {
|
|
229
|
+
const n = num(args[0])
|
|
230
|
+
return n !== null ? `\\{0,\\dots,${n - 1}\\}` : `\\{0,\\dots,${tex(args[0])}-1\\}`
|
|
231
|
+
}
|
|
232
|
+
if (f.v === "List.range'" && args.length === 2) {
|
|
233
|
+
const a = num(args[0]), k = num(args[1])
|
|
234
|
+
return a !== null && k !== null ? `\\{${a},\\dots,${a + k - 1}\\}` : `\\{${tex(args[0])},\\dots,${tex(args[0])}+${tex(args[1])}-1\\}`
|
|
235
|
+
}
|
|
236
|
+
return null
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Flatten an application spine into head + arguments. */
|
|
240
|
+
const spine = (n: Node): { head: Node; args: Node[] } => {
|
|
241
|
+
const args: Node[] = []
|
|
242
|
+
let h = n
|
|
243
|
+
while (h.t === 'app') { args.unshift(h.a); h = h.f }
|
|
244
|
+
return { head: h, args }
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function tex(n: Node): string {
|
|
248
|
+
switch (n.t) {
|
|
249
|
+
case 'num': return n.v
|
|
250
|
+
case 'id': return idTex(n.v)
|
|
251
|
+
case 'hole': return `(\\cdot\\,${LATEX_NAME[n.op] ?? n.op}\\,\\cdot)`
|
|
252
|
+
case 'un': return n.op === 'ยฌ' ? `\\lnot ${tex(n.e)}` : `-${tex(n.e)}`
|
|
253
|
+
case 'lam': return `${n.ps.map(idTex).join(',\\,')} \\mapsto ${tex(n.b)}`
|
|
254
|
+
case 'list': return `[${n.xs.map(tex).join(',\\,')}]`
|
|
255
|
+
case 'asc': return tex(n.e)
|
|
256
|
+
case 'tuple': return `\\left(${n.xs.map(tex).join(',\\,')}\\right)`
|
|
257
|
+
case 'proj': return `${tex(n.o)}_{${n.i}}`
|
|
258
|
+
case 'let': return `${tex(n.b)} \\quad \\text{where } ${idTex(n.name)} = ${tex(n.e)}`
|
|
259
|
+
case 'ite': return `\\begin{cases}${tex(n.a)} & \\text{if } ${tex(n.c)}\\\\ ${tex(n.b)} & \\text{otherwise}\\end{cases}`
|
|
260
|
+
case 'bin': {
|
|
261
|
+
const l = tex(n.l), r = tex(n.r)
|
|
262
|
+
if (n.op === '^') return `${l}^{${r}}`
|
|
263
|
+
if (n.op === '/') return `\\frac{${l}}{${r}}`
|
|
264
|
+
if (n.op === '%') return `${l} \\bmod ${r}`
|
|
265
|
+
if (n.op === '>>>') return `${l} \\gg ${r}`
|
|
266
|
+
if (n.op === '<<<') return `${l} \\ll ${r}`
|
|
267
|
+
return `${l} ${LATEX_NAME[n.op] ?? n.op} ${r}`
|
|
268
|
+
}
|
|
269
|
+
case 'dot': return dotTex(n, [])
|
|
270
|
+
case 'app': {
|
|
271
|
+
const { head, args } = spine(n)
|
|
272
|
+
const r = rangeTex(head, args)
|
|
273
|
+
if (r) return r
|
|
274
|
+
if (head.t === 'dot') return dotTex(head, args)
|
|
275
|
+
return `${tex(head)}\\mathopen{}\\left(${args.map(tex).join(',\\,')}\\right)`
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** A method call, rendered as the mathematics it means. Unknown methods throw โ see the header. */
|
|
281
|
+
function dotTex(d: Extract<Node, { t: 'dot' }>, args: Node[]): string {
|
|
282
|
+
const o = tex(d.o)
|
|
283
|
+
const lam = (a: Node | undefined) => (a && a.t === 'lam' && a.ps.length === 1 ? a : null)
|
|
284
|
+
switch (d.name) {
|
|
285
|
+
case 'all': { const f = lam(args[0])
|
|
286
|
+
if (f) return `\\forall ${idTex(f.ps[0])} \\in ${o},\\; ${tex(f.b)}`
|
|
287
|
+
if (args.length === 1) return `\\forall ${BOUND} \\in ${o},\\; ${applied(args[0], tex)}`
|
|
288
|
+
break }
|
|
289
|
+
case 'any': { const f = lam(args[0])
|
|
290
|
+
if (f) return `\\exists ${idTex(f.ps[0])} \\in ${o} : ${tex(f.b)}`
|
|
291
|
+
if (args.length === 1) return `\\exists ${BOUND} \\in ${o} : ${applied(args[0], tex)}`
|
|
292
|
+
break }
|
|
293
|
+
case 'filter': { const f = lam(args[0])
|
|
294
|
+
if (f) return `\\{\\, ${idTex(f.ps[0])} \\in ${o} \\mid ${tex(f.b)} \\,\\}`
|
|
295
|
+
if (args.length === 1) return `\\{\\, ${BOUND} \\in ${o} \\mid ${applied(args[0], tex)} \\,\\}`
|
|
296
|
+
break }
|
|
297
|
+
case 'map': { const f = lam(args[0]); if (f) return `\\{\\, ${tex(f.b)} \\mid ${idTex(f.ps[0])} \\in ${o} \\,\\}`; if (args.length === 1) return `${tex(args[0])}[${o}]` ; break }
|
|
298
|
+
case 'length': if (!args.length) return `\\left|${o}\\right|`; break
|
|
299
|
+
case 'contains': if (args.length === 1) return `${tex(args[0])} \\in ${o}`; break
|
|
300
|
+
case 'eraseDups': if (!args.length) return `\\operatorname{dedup}\\left(${o}\\right)`; break
|
|
301
|
+
case 'flatMap': { const f = lam(args[0]); if (!f) break
|
|
302
|
+
return `\\bigcup_{${idTex(f.ps[0])} \\in ${o}} ${tex(f.b)}` }
|
|
303
|
+
case 'foldr':
|
|
304
|
+
case 'foldl': {
|
|
305
|
+
// foldl (ยท + ยท) 0 is a sum and nothing else here is; anything other than that exact shape falls
|
|
306
|
+
// through to the throw, rather than being rendered as a sum it is not.
|
|
307
|
+
if (args.length === 2 && args[0].t === 'hole' && args[0].op === '+' && args[1].t === 'num' && args[1].v === '0')
|
|
308
|
+
return `\\sum ${o}`
|
|
309
|
+
// Any other fold is named rather than given a symbol it does not have: an operator and a seed,
|
|
310
|
+
// applied along the list. Inventing โ-like notation for an arbitrary combiner would say more than
|
|
311
|
+
// the statement does.
|
|
312
|
+
if (args.length === 2) return `\\operatorname{fold}_{${tex(args[0])}}\\left(${o},\\, ${tex(args[1])}\\right)`
|
|
313
|
+
break
|
|
314
|
+
}
|
|
315
|
+
case 'get!': if (args.length === 1) return `${o}_{${tex(args[0])}}`; break
|
|
316
|
+
case 'head?': if (!args.length) return `\\operatorname{head}\\left(${o}\\right)`; break
|
|
317
|
+
case 'getLast?': if (!args.length) return `\\operatorname{last}\\left(${o}\\right)`; break
|
|
318
|
+
// Option's two predicates. `(invOf d).isSome` is how a statement says "an inverse EXISTS", and without
|
|
319
|
+
// notation for it the whole site build died on the one theorem that said so โ see the note in
|
|
320
|
+
// src/publication/index.ts. Rendered as the existence it means, not as the method it is spelled with.
|
|
321
|
+
case 'isSome': if (!args.length) return `\\exists\\,${o}`; break
|
|
322
|
+
case 'isNone': if (!args.length) return `\\nexists\\,${o}`; break
|
|
323
|
+
case 'reverse': if (!args.length) return `\\operatorname{reverse}\\left(${o}\\right)`; break
|
|
324
|
+
case 'sum': if (!args.length) return `\\sum ${o}`; break
|
|
325
|
+
}
|
|
326
|
+
throw new Error('no notation for method .' + d.name + ' with ' + args.length + ' argument(s)')
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** Lean source โ LaTeX, or null when this grammar does not cover it. Never a partial rendering. */
|
|
330
|
+
export function toLatex(statement: string): string | null {
|
|
331
|
+
try { return tex(parse(statement)) } catch { return null }
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// โโ MathML โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
335
|
+
// Emitted from the SAME tree as the LaTeX, so the typeset page and the copyable LaTeX cannot disagree.
|
|
336
|
+
// MathML is rendered natively by browsers, which keeps the page free of a maths library and a CDN.
|
|
337
|
+
const X = escapeHtml
|
|
338
|
+
const mo = (s: string) => `<mo>${X(s)}</mo>`
|
|
339
|
+
const mi = (s: string) => `<mi>${X(s)}</mi>`
|
|
340
|
+
const MOP: Record<string, string> = { '==': '=', '=': '=', '!=': 'โ ', 'โ ': 'โ ', '<=': 'โค', 'โค': 'โค', '>=': 'โฅ', 'โฅ': 'โฅ', 'โง': 'โง', 'โจ': 'โจ', 'โ': 'โ', 'โ': 'โ', '*': 'โ
', '%': 'mod', '++': 'โงบ' }
|
|
341
|
+
|
|
342
|
+
function ml(n: Node): string {
|
|
343
|
+
switch (n.t) {
|
|
344
|
+
case 'num': return `<mn>${X(n.v)}</mn>`
|
|
345
|
+
case 'id': return mi(n.v)
|
|
346
|
+
case 'hole': return `<mrow>${mo('(')}${mo('ยท')}${mo(MOP[n.op] ?? n.op)}${mo('ยท')}${mo(')')}</mrow>`
|
|
347
|
+
case 'un': return `<mrow>${mo(n.op === 'ยฌ' ? 'ยฌ' : 'โ')}${ml(n.e)}</mrow>`
|
|
348
|
+
case 'lam': return `<mrow>${n.ps.map(mi).join(mo(','))}${mo('โฆ')}${ml(n.b)}</mrow>`
|
|
349
|
+
case 'list': return `<mrow>${mo('[')}${n.xs.map(ml).join(mo(','))}${mo(']')}</mrow>`
|
|
350
|
+
case 'asc': return ml(n.e)
|
|
351
|
+
case 'tuple': return `<mrow>${mo('(')}${n.xs.map(ml).join(mo(','))}${mo(')')}</mrow>`
|
|
352
|
+
case 'proj': return `<msub>${ml(n.o)}<mn>${X(n.i)}</mn></msub>`
|
|
353
|
+
case 'let': return `<mrow>${ml(n.b)}${mo('where')}${mi(n.name)}${mo('=')}${ml(n.e)}</mrow>`
|
|
354
|
+
// Bracketed: unbracketed, `โฆ = 1 if isUnit(d) ; otherwise 0 โง provenHere = 0` reads as though the
|
|
355
|
+
// conjunct were part of the else-branch.
|
|
356
|
+
case 'ite': return `<mrow>${mo('(')}${ml(n.a)}${mo('if')}${ml(n.c)}${mo(';')}${mo('otherwise')}${ml(n.b)}${mo(')')}</mrow>`
|
|
357
|
+
case 'bin': {
|
|
358
|
+
if (n.op === '^') return `<msup>${ml(n.l)}${ml(n.r)}</msup>`
|
|
359
|
+
if (n.op === '/') return `<mfrac>${ml(n.l)}${ml(n.r)}</mfrac>`
|
|
360
|
+
if (n.op === '>>>') return `<mrow>${ml(n.l)}${mo('โซ')}${ml(n.r)}</mrow>`
|
|
361
|
+
if (n.op === '<<<') return `<mrow>${ml(n.l)}${mo('โช')}${ml(n.r)}</mrow>`
|
|
362
|
+
return `<mrow>${ml(n.l)}${mo(MOP[n.op] ?? n.op)}${ml(n.r)}</mrow>`
|
|
363
|
+
}
|
|
364
|
+
case 'dot': return dotMl(n, [])
|
|
365
|
+
case 'app': {
|
|
366
|
+
const { head, args } = spine(n)
|
|
367
|
+
if (head.t === 'id' && (head.v === 'List.range' || head.v === "List.range'")) {
|
|
368
|
+
// The SAME literal-bound simplification the LaTeX emitter applies. It was written only there, so
|
|
369
|
+
// one tree produced {0,โฆ,8} in LaTeX and {0,โฆ,9โ1} in MathML โ two renderings of one theorem
|
|
370
|
+
// disagreeing, in the module whose header claims they cannot.
|
|
371
|
+
const a = head.v === 'List.range' ? 0 : num(args[0])
|
|
372
|
+
const k = head.v === 'List.range' ? num(args[0]) : num(args[1])
|
|
373
|
+
const lo = head.v === 'List.range' ? '<mn>0</mn>' : ml(args[0])
|
|
374
|
+
if (a !== null && k !== null)
|
|
375
|
+
return `<mrow>${mo('{')}<mn>${a}</mn>${mo(',')}${mo('โฆ')}${mo(',')}<mn>${a + k - 1}</mn>${mo('}')}</mrow>`
|
|
376
|
+
const hi = head.v === 'List.range'
|
|
377
|
+
? `<mrow>${ml(args[0])}${mo('โ')}<mn>1</mn></mrow>`
|
|
378
|
+
: `<mrow>${ml(args[0])}${mo('+')}${ml(args[1])}${mo('โ')}<mn>1</mn></mrow>`
|
|
379
|
+
return `<mrow>${mo('{')}${lo}${mo(',')}${mo('โฆ')}${mo(',')}${hi}${mo('}')}</mrow>`
|
|
380
|
+
}
|
|
381
|
+
if (head.t === 'dot') return dotMl(head, args)
|
|
382
|
+
return `<mrow>${ml(head)}${mo('(')}${args.map(ml).join(mo(','))}${mo(')')}</mrow>`
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function dotMl(d: Extract<Node, { t: 'dot' }>, args: Node[]): string {
|
|
388
|
+
const o = ml(d.o)
|
|
389
|
+
const lam = (a: Node | undefined) => (a && a.t === 'lam' && a.ps.length === 1 ? a : null)
|
|
390
|
+
switch (d.name) {
|
|
391
|
+
case 'all': { const f = lam(args[0])
|
|
392
|
+
if (f) return `<mrow>${mo('โ')}${mi(f.ps[0])}${mo('โ')}${o}${mo(',')}${ml(f.b)}</mrow>`
|
|
393
|
+
if (args.length === 1) return `<mrow>${mo('โ')}${mi(BOUND)}${mo('โ')}${o}${mo(',')}${ml(args[0])}${mo('(')}${mi(BOUND)}${mo(')')}</mrow>`
|
|
394
|
+
break }
|
|
395
|
+
case 'any': { const f = lam(args[0])
|
|
396
|
+
if (f) return `<mrow>${mo('โ')}${mi(f.ps[0])}${mo('โ')}${o}${mo(':')}${ml(f.b)}</mrow>`
|
|
397
|
+
if (args.length === 1) return `<mrow>${mo('โ')}${mi(BOUND)}${mo('โ')}${o}${mo(':')}${ml(args[0])}${mo('(')}${mi(BOUND)}${mo(')')}</mrow>`
|
|
398
|
+
break }
|
|
399
|
+
case 'filter': { const f = lam(args[0])
|
|
400
|
+
if (f) return `<mrow>${mo('{')}${mi(f.ps[0])}${mo('โ')}${o}${mo('โฃ')}${ml(f.b)}${mo('}')}</mrow>`
|
|
401
|
+
if (args.length === 1) return `<mrow>${mo('{')}${mi(BOUND)}${mo('โ')}${o}${mo('โฃ')}${ml(args[0])}${mo('(')}${mi(BOUND)}${mo(')')}${mo('}')}</mrow>`
|
|
402
|
+
break }
|
|
403
|
+
case 'map': { const f = lam(args[0]); if (f) return `<mrow>${mo('{')}${ml(f.b)}${mo('โฃ')}${mi(f.ps[0])}${mo('โ')}${o}${mo('}')}</mrow>`; if (args.length === 1) return `<mrow>${ml(args[0])}${mo('[')}${o}${mo(']')}</mrow>`; break }
|
|
404
|
+
case 'length': if (!args.length) return `<mrow>${mo('|')}${o}${mo('|')}</mrow>`; break
|
|
405
|
+
case 'contains': if (args.length === 1) return `<mrow>${ml(args[0])}${mo('โ')}${o}</mrow>`; break
|
|
406
|
+
case 'eraseDups': if (!args.length) return `<mrow>${mi('dedup')}${mo('(')}${o}${mo(')')}</mrow>`; break
|
|
407
|
+
case 'flatMap': { const f = lam(args[0]); if (!f) break
|
|
408
|
+
return `<mrow>${mo('โ')}${mi(f.ps[0])}${mo('โ')}${o}${mo(',')}${ml(f.b)}</mrow>` }
|
|
409
|
+
case 'foldr':
|
|
410
|
+
case 'foldl':
|
|
411
|
+
if (args.length === 2 && args[0].t === 'hole' && args[0].op === '+' && args[1].t === 'num' && args[1].v === '0')
|
|
412
|
+
return `<mrow>${mo('โ')}${o}</mrow>`
|
|
413
|
+
if (args.length === 2) return `<mrow>${mi('fold')}${mo('(')}${ml(args[0])}${mo(',')}${o}${mo(',')}${ml(args[1])}${mo(')')}</mrow>`
|
|
414
|
+
break
|
|
415
|
+
case 'get!': if (args.length === 1) return `<msub>${o}${ml(args[0])}</msub>`; break
|
|
416
|
+
case 'head?': if (!args.length) return `<mrow>${mi('head')}${mo('(')}${o}${mo(')')}</mrow>`; break
|
|
417
|
+
case 'getLast?': if (!args.length) return `<mrow>${mi('last')}${mo('(')}${o}${mo(')')}</mrow>`; break
|
|
418
|
+
case 'reverse': if (!args.length) return `<mrow>${mi('reverse')}${mo('(')}${o}${mo(')')}</mrow>`; break
|
|
419
|
+
// the Option predicates, in the SAME reading as the LaTeX above โ latex-gate exists to catch exactly
|
|
420
|
+
// the case where one emitter learns a notation and the other does not, and it caught this one
|
|
421
|
+
case 'isSome': if (!args.length) return `<mrow>${mo('โ')}${o}</mrow>`; break
|
|
422
|
+
case 'isNone': if (!args.length) return `<mrow>${mo('โ')}${o}</mrow>`; break
|
|
423
|
+
case 'sum': if (!args.length) return `<mrow>${mo('โ')}${o}</mrow>`; break
|
|
424
|
+
}
|
|
425
|
+
throw new Error('no notation for method .' + d.name)
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/** Lean source โ MathML, or null when this grammar does not cover it. */
|
|
429
|
+
export function toMathML(statement: string): string | null {
|
|
430
|
+
try { return `<math display="block" xmlns="http://www.w3.org/1998/Math/MathML">${ml(parse(statement))}</math>` } catch { return null }
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// โโ round-trip, so a wrong parse cannot pass quietly โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
434
|
+
// Throwing catches input the grammar does not know. It does NOT catch input the grammar mis-reads: the
|
|
435
|
+
// lexer once folded `axis.contains d` into a single identifier and rendered `axis.contains(d)` โ no error,
|
|
436
|
+
// wrong formula. So the tree is written back out as Lean and the token sequence compared with the source.
|
|
437
|
+
// A parse that dropped, duplicated or reordered anything fails here.
|
|
438
|
+
//
|
|
439
|
+
// ITS LIMIT, STATED: parentheses are excluded from the comparison (the tree does not record where the
|
|
440
|
+
// source put redundant ones), so this verifies that the same symbols were read in the same order โ it does
|
|
441
|
+
// NOT prove the operators were grouped with the right precedence. Precedence is covered separately by the
|
|
442
|
+
// assertions in scripts/latex-gate.ts, against Lean's own table.
|
|
443
|
+
export function unparse(n: Node): string {
|
|
444
|
+
switch (n.t) {
|
|
445
|
+
case 'num': return n.v
|
|
446
|
+
case 'id': return n.v
|
|
447
|
+
case 'hole': return `(ยท ${n.op} ยท)`
|
|
448
|
+
case 'un': return `(${n.op} ${unparse(n.e)})`
|
|
449
|
+
case 'lam': return `(fun ${n.ps.join(' ')} => ${unparse(n.b)})`
|
|
450
|
+
case 'list': return `[${n.xs.map(unparse).join(', ')}]`
|
|
451
|
+
case 'asc': return `(${unparse(n.e)} : ${unparse(n.ty)})`
|
|
452
|
+
case 'tuple': return `(${n.xs.map(unparse).join(', ')})`
|
|
453
|
+
case 'proj': return `(${unparse(n.o)}).${n.i}`
|
|
454
|
+
case 'let': return `let ${n.name} := ${unparse(n.e)}; ${unparse(n.b)}`
|
|
455
|
+
case 'ite': return `(if ${unparse(n.c)} then ${unparse(n.a)} else ${unparse(n.b)})`
|
|
456
|
+
case 'bin': return `(${unparse(n.l)} ${n.op} ${unparse(n.r)})`
|
|
457
|
+
case 'dot': return `(${unparse(n.o)}).${n.name}`
|
|
458
|
+
case 'app': return `(${unparse(n.f)} ${unparse(n.a)})`
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Lean spells several operators two ways and the parser keeps one of them, so the comparison canonicalises
|
|
463
|
+
// both sides. Without this the check reported 72 failures that were only `||` against `โจ` โ a check that
|
|
464
|
+
// cries wolf gets switched off, which is its own way of proving nothing.
|
|
465
|
+
const CANON: Record<string, string> = { '||': 'โจ', '&&': 'โง', '==': '=', '!=': 'โ ', '<=': 'โค', '>=': 'โฅ', '!': 'ยฌ' }
|
|
466
|
+
|
|
467
|
+
/** The token sequence with grouping removed and spellings canonicalised โ what the round-trip compares. */
|
|
468
|
+
export const shape = (src: string): string =>
|
|
469
|
+
lex(src).filter((t) => !'()'.includes(t.v)).map((t) => CANON[t.v] ?? t.v).join(' ')
|
|
470
|
+
|
|
471
|
+
/** True when the parse read exactly the symbols the source contains, in order. */
|
|
472
|
+
export function roundTrips(statement: string): boolean {
|
|
473
|
+
try { return shape(unparse(parse(statement))) === shape(statement) } catch { return false }
|
|
474
|
+
}
|