TeXForm checks correctness with corpus regression, transform contracts, and renderer validation. These checks cover different properties; none alone establishes that a normalization is suitable for every input.
How correctness is checked#
- Corpus regression. Every significant parser change is run against tracked real-world datasets, and failure rates are compared before and after.
- The eliminated-forms contract. Each rewrite rule declares which source forms it promises to remove, and the engine verifies after the full pipeline that those forms are gone. The check is re-run across the same corpora. See Rules and the transform contract.
- Text idempotency. Serialization guarantees that a second parse-and-serialize round trip is a fixed point. See Serialization.
- Renderer-validated knowledge base. Command signatures are validated by rendering generated test cases against real renderers rather than transcribed from documentation. See The knowledge base.
Parse failure rates#
Strict parsing rejects unknown commands and stops at syntax errors. Lenient parsing preserves unknown commands and recovers where possible. A strict failure may indicate an unsupported command rather than malformed input; a recovered document may still contain errors. Measure both on input representative of your application, and inspect diagnostics before transforming a document. See Handle parse errors.
Eliminated-form contract#
The contract checker runs a profile and checks that forms its rules promise to eliminate are absent from the output. A successful check establishes that property for the checked input; it does not establish mathematical equivalence or renderer compatibility. See Rules and the transform contract for rule metadata and the scope of the guarantee.
Performance#
Reference measurements#
The tables below show what to expect from each binding on a public dataset. They come from one machine, so read them for orders of magnitude and for the differences between bindings, not as a promise for your hardware.
Measured on 2026-10-28 with TeXForm 0.10.1 on one thread of an AMD Ryzen 9 9950X 16-Core Processor (16 cores, 92 GiB, Ubuntu 24.04). The input is the 6,810 formulas of the im2latex-100k test split; the 71 that fail to parse are left out of the rates. Engines load the default packages (base, ams, braket, textmacros, bboldx, boldsymbol, color). Every binding produced byte-identical normalized output for every profile.
Throughput, in formulas per second, for parsing alone and for normalizing with each profile:
| Binding | Parse | authoring | faithful | corpus | equiv |
|---|---|---|---|---|---|
| Rust rustc 1.97.0 | 12,217 | 10,403 | 10,359 | 10,269 | 10,185 |
| Python CPython 3.12.10 | 11,778 | 10,204 | 10,102 | 10,066 | 9,994 |
| Node.js (WebAssembly) Node.js 24.11.1 | 10,146 | 8,669 | 8,624 | 8,439 | 8,415 |
Time per formula:
| Binding | Parse, median | Parse, p95 | corpus, median | corpus, p95 |
|---|---|---|---|---|
| Rust | 77 µs | 140 µs | 92 µs | 167 µs |
| Python | 80 µs | 144 µs | 93 µs | 171 µs |
| Node.js (WebAssembly) | 93 µs | 167 µs | 111 µs | 204 µs |
Parsing accounts for most of the work, so the four profiles normalize at similar rates. The Python binding calls the same native core as Rust and stays close to it; the WebAssembly build is slower and its first calls also pay for loading the module and warming up the JavaScript engine.
One-time costs in a fresh process, before the first formula is processed:
| Binding | Import | First parser | corpus engine | First parse |
|---|---|---|---|---|
| Rust | — | 0.28 ms | 0.18 ms | 0.11 ms |
| Python | 0.53 ms | 0.31 ms | 0.18 ms | 0.14 ms |
| Node.js (WebAssembly) | 4.2 ms | 1.7 ms | 0.36 ms | 3.3 ms |
The WebAssembly module that the browser and Node.js load is 1.7 MiB uncompressed, 592 KiB with gzip, and 412 KiB with Brotli.
Measuring your own workload#
Measure throughput on your own hardware and representative input, using the profile and package selection your application needs. Separate engine construction from repeated normalization when reporting timings.
Two things do have a large and predictable effect on throughput.
Reuse the engine. Constructing a Parser or TransformEngine loads and indexes the knowledge base and builds the rule plan for the profile. That cost belongs once per process, not once per formula: build the engine outside your loop and call normalize inside it. Normalize a corpus shows the shape.
Build in release mode if you build from source. Debug builds run structural invariant sweeps that release builds omit, and those sweeps are quadratic on wide formulas. The published wheels and the npm package are release builds already; this matters only if you compile the crate yourself.
The bindings are synchronous, and there is no batch or parallel API to reach for. Parallelism, when you need it, is your own — partition the input and give each worker its own engine.