latex — built-in LaTeX support
Goal: built-in, compiled LaTeX support. Math-mode formulas → our own tree (enums) → back to canonical LaTeX or plain text. Everything is in the binary, no external TeX needed; on the website formulas are drawn by KaTeX. Its place in the mini-lab is described in Smart annotator — layers, context, memory, "Mini-lab".
How it is built
| file | what it does |
|---|---|
data/commands.txt + build.rs | a single command table → enums Sym (TeX class, Unicode, whether it is a delimiter), FracKind, Accent, Font, TextStyle, Stack, XArrow, EnvKind and constant matches: name → Entry, character (≤, α) → Sym, environment → EnvKind. The first form is canonical for printing (\leq → \le). A duplicate form or an unknown kind or class is a build error |
lex.rs | math-mode lexer (TeX rules: \alpha eats spaces, % is a comment, spaces merge) and a stack stream onto which a macro pushes its body |
macros.rs | \newcommand/\renewcommand/\providecommand with [n][default], \def with #1…#9 (and with delimiters, as in TeX: \def\pair(#1,#2){…}), \let (keeps the old value), \DeclareMathOperator(*); Macros::scan(tex) collects definitions from a document |
parse.rs | parser: tokens → Node; errors with a byte position (Error::column, Error::context — a ⟦ mark in the source) |
ast.rs | tree: Row, Num, Char, Sym, Group, Frac, Sqrt, Scripts (base, scripts, primes), OpName, LeftRight/Middle, Env (tables), Font, Text, Accent, Stack, XArrow, Class, Not, Unknown (name, arguments, position). Span does not take part in == |
print.rs | to_latex (canonical LaTeX), to_text (Unicode: (a + b)/2, x², ℝⁿ, matrix (a b; c d)), to_sexpr (debugging), to_latex_faulty (deliberate defects for the negative control) |
gate.rs | round-trip gate parse(print(parse(x))) == parse(x); affected — whether a defect touches the tree (for a precise negative control) |
extract.rs | formulas from .tex: $…$, $$…$$, \(…\), \[…\], equation, align, alignat{n}, gather, multline, eqnarray, flalign, displaymath, math (and starred forms); comments, \$, \verb, verbatim are skipped; $…$ does not cross an empty line |
tomath.rs (feature math) | bridge to math::Expr and back (see below) |
main.rs, corpus.rs | CLI: parse, tex, corpus (arXiv metadata .xml.gz, .tex) |
What is supported
- numbers (
3.14; a space breaks a number), Latin and Greek letters (including\var…,\Alpha…, Unicodeα,≤,ℝ); \frac \dfrac \tfrac \cfrac \binom \dbinom \tbinom \genfrac, infix\over \choose \atop \brack \brace;\sqrt[n]{},\root n \of x; superscripts and subscripts (as in TeX — to the preceding atom,^{14}C— to{}), primesf'', groups{};- 493 symbols (800 command names together with the other kinds, 24 environment names): operators and relations (
+ - \cdot \times \div = \ne \le \ge < > \in \subset \to \mapsto …,\not X→ negated symbol), arrows, delimiters, punctuation,\dots; - functions
\sin \cos \log \ln \exp … \lim \max \sup \det \gcd \argmax,\operatorname{…}/\operatorname*; big operators with limits\sum \prod \int \iint \oint \bigcup …(\limits/\nolimitsare dropped); \left … \middle … \right(with.); sizes\big…are dropped;- tables:
matrix pmatrix bmatrix Bmatrix vmatrix Vmatrix smallmatrix(and*),array,subarray,cases,dcases,rcases,aligned,alignedat,gathered,split,\substack; plain TeX\pmatrix{…\cr…}, AMS-TeX\aligned…\endaligned;&and\\at the top level — implicitaligned(the body ofalign); - fonts
\mathbb \mathbf \mathrm \mathcal \mathscr \mathfrak \mathit \mathsf \mathtt \boldsymbol \bm(and old-style{\bf x},{\cal A},\Bbb), text\text \mbox \textrm \textbf …(with$…$inside), accents\hat \bar \vec \tilde \dot \overline \widehat \underbrace \boxed \cancel \pmod …,\overset \underset \stackrel,\xrightarrow[…]{…},\mathrel{…}etc.,\unicode{x…}; - spacing (
\, \; \quad \hspace{…} \kern…), style, labels (\label \tag \nonumber \eqno), colour,\phantom— dropped;\ensuremath,\smash,\rlap— content is merged into the row; - an unknown command becomes an
Unknown(name, group arguments, position)node; an unknown environment becomes anEnvwith its name and position. An argument of an unknown command that does not parse as a formula (\setpres{…}{$x$ for all $s$}) is kept as text. Nothing is lost silently; - unbalanced input (
{,},\left/\right,\begin/\end,[), a double script,$inside a formula — an error with a position. Lenient, like MathJax:$#P$→\#P,CP^2#2→\#2.
Bridge to math::Expr (feature math)
to_expr covers a subset: numbers, variables, + − · × / ÷ and implicit multiplication, unary minus, fractions, roots
(\sqrt[3]{x} → x^(1/3)), powers, \sin \cos \tan \exp \ln, absolute value |x|, parentheses ((x+1)^2 — in TeX the script
attaches to ), and the bridge accounts for this), e, \pi. \sin 2x = sin(2x), \sin x\cos x = sin(x)·cos(x),
\sin^2 x = (sin x)². \frac{1}{2} (an irreducible fraction of integers) is a rational literal; \frac{2}{4} stays
a division (no step is lost). to_relation handles a chain a = b ≤ c. A refusal carries the kind of reason ("variable",
"script", "function", "construct", "relation", "syntax", …). from_expr is the reverse side: printing math steps and
answers in LaTeX; a test checks Expr → LaTeX → tree → Expr on 35 expressions (with normalization
Div(1,2) ≡ 1/2). math::Expr may change after the v2 merge — the bridge will be fixed at merge time.
CLI
cargo build --release [--features math]
../target/release/latex parse '\frac{a+b}{2} \le \sqrt[3]{x}' # tree, LaTeX, text, unknowns, round trip (and math::Expr)
../target/release/latex tex main.tex chap1.tex -v # document formulas with macros from all files
../target/release/latex corpus $MOVA_DATA/raw/math/arxiv-oai --out DIR [--math] [--fault swapfrac|dropsup|droplast] [--limit N]
Measurements (26.09, 1 thread)
Real formulas — arXiv abstracts and titles from the library pilot (arxiv-oai, OAI-PMH
arXivRaw metadata, 868 .xml.gz; the library has no .tex sources): 789 040 records after removing 316 757 duplicates
between sets, 2 540 333 formulas (729 362 distinct).
| measure | value |
|---|---|
| parsed | 2 539 822 (99.98 %); 511 errors — all on the author's side (unclosed {, % in reformatted abstracts swallowing \end{…}, $ inside a formula) |
without Unknown | 2 474 313 (97.40 %); distinct formulas — 94.51 % |
| round trip | 2 539 822 of 2 539 822 green (100 %) |
bridge to math::Expr | 8.96 % (expression 7.91 %, relation 1.05 %); the main reason for refusals is variables outside x y z t u v w n (math::Var) |
| speed | parsing 1.7–1.8 M formulas/s (18–19 MB/s of formula text; spread due to box load, three runs); gate (print + reparse) ~1 M/s; the whole run with gzip and XML — 18–19 s |
Top unknowns are author macros without a preamble (abstracts have none): \R 7530, \Z 2892, \C 2576, \F 1449,
\eps 1244, \A, \Q, \g, \N, \RR… In .tex they are expanded by Macros::scan. Full list —
unknown.tsv.
A real multi-file document (the IRSTLM manual, 29 .tex in raw/ext, read-only): 101 formulas, all
parsed without Unknown, macros from the main file.
Negative controls
-
unbalanced input → an error with the exact position (17 cases in
tests/gate.rs); -
a corrupted printer → the gate turns red exactly where the defect touches the tree, over the whole corpus:
defect formulas affected (by construction) red red among unaffected SwapFrac(numerator ↔ denominator)51 122 51 122 0 DropSup(drops superscripts)442 956 442 956 0 DropLast(drops the last atom of a row ≥ 3)1 105 073 1 105 073 0 -
a macro loop (
\def\a{\a}) is an error, not a hang; 30 000 random token inputs — no panic, everything parsed passes the round trip; bridge: a different expression gives a different tree.
cargo test and cargo test --features math are green (unit, end-to-end, bridge, property on random inputs).
Independent tests (tests3, 26.09)
tests/data/vmm/latex.tsv— 100 cases from an independent author (Opus, effort high, did not see the code; prompts alongside), frozen by hash before the first run (commit66711cecf):same20 anddiff15 (tree equality without positions),err15 (must be an error),ok20 (noUnknown, round trip),value15 (value through the bridge tomath::Expr, only with--features math),count8 (formulas in a document fragment),macro7.- First run — 95/100; 4 red
macrocases were a defect of the test runner, not the parser: "\n" in the format meant a line break and broke\newcommand. Fixed in the runner (a break only when not followed by a letter) → 99/100. - Remaining:
vmm-tex-088— a test error caused by the same format: "…commented\nand (c)" —\nandis read as a command, so the comment swallows\(c\). No parser defects found. The test istests/vmm_latex.rs; its negative control (same ↔ diff) is red.
Licenses
- Code — the Elamur Cosmic License (Elamur Cosmic License at the repo root). The command table is our own, from our own knowledge of LaTeX/amsmath/amssymb; the KaTeX function list (MIT) served only as a reference for what to support (no KaTeX files were taken).
- Dependencies:
flate2(MIT/Apache-2.0, featuregz— only the CLI for.xml.gz),math(our own, featuremath). - arXiv abstracts are used only for measurement and do not go into the repo.
What v1 lacks
\sideset,\tensor,\prescript,CDdiagrams (@>>>),\genfracwith non-standard delimiters — as\left…\right.- Text in
\text{…}is stored as a canonicalized string; its inner$…$are not parsed into the tree. - A preset of common author shorthands (
\R→\mathbb{R},\eps→\varepsilon) — only as an explicit option, not by default: it is a guess about the author's intent. Not done. - The bridge is limited by the
mathvariable table; scripts (x_1), sums, integrals, matrices — aftermathv2.
Licence
Elamur Cosmic License (Elamur Cosmic License). Third-party data keeps its own terms, as noted where it is used.
This page in Markdown → https://md.mova.elamur.ai/free/en/docs/latex/