Sussman Lab

This message automatically appears on the site's most-visited pages. If you've found this resource valuable, please consider making a small contribution to keep things running.

Watch the companion video for this overview: Demo of a C++23 TeX engine

What if we rewrote TeX82 today, thinking about the realities of modern typesetting?

Like many scientists, I spend a nontrivial fraction of my working life inside LaTeX documents. Over years of writing papers, lecture notes, grant proposals, and simulation guides, it became hard not to wonder what was actually going on inside the engine. TeX is simultaneously one of the most stable, reliable pieces of software ever written, but also one of the (perhaps) most notoriously impenetrable black boxes from a very different era of software engineering.

A while ago, driven mostly by curiosity and a desire to understand how the underlying system works, I started a hobby project: building a modern TeX engine from scratch in C++23 (which I’ve called NemaTeX; that repo also contains the roadmap for when I hope to actually open-source everything). I never really expected the project to last as long as it has, or to reach the point where it could compile modern, real-world LaTeX documents. But building it has been an illuminating journey through 1980s computer science history, compiler design, and data-oriented architecture. I think I’ve become a better programmer by far by working through all of this (although, admittedly, that might have been something of a low bar).

One of the central questions motivating this project was to see what might happen if we shifted the boundaries between what lives in TeX’s macro layer what is handled natively by the engine. Existing engines like pdftex, xetex, and luatex have all answered such questions in different ways. Nevertheless, in these standard TeX systems decades of absolutely remarkable features (from Unicode support to micro-typography to complex output formats) have been bolted onto the engine through intricate, arcade macro packages or outsourcing to Lua code.

By bringing those capabilities into the engine while preserving strict TeX82 typesetting semantics, I’ve been thinking about unlocking a variety of interesting modern capabilities:

  • Native OpenType Font Handling: Shifting font loading, glyph shaping, and subsetting directly into engine routines (interfacing cleanly with libraries like HarfBuzz and FreeType) rather than relying on convoluted macro intermediaries.
  • A Decoupled Intermediate Representation (IR): Decoupling the typesetting process from the physical output driver. The engine produces a clean, structured representation of the page, letting us easily abstract out the typesetting from the output generation. This gives an immediate opportunity for parallelizing document compilation, and also makes it easy to target very different file formats.
  • Accessible PDFs (PDF/UA-2): Building the semantic structure tree of the document as pages are constructed, allowing the generation of fully tagged, accessible PDFs directly from TeX source.
  • Absolute-Positioned HTML/CSS: Exporting web-native layouts directly from the same intermediate representation, with glyphs positioned to match the print output. (Particularly helpful in the part of work life where I make slides and presentations, since embedding an mp4 is as trivial in html5 as it is difficult in pdf).

Of course, building a new engine from scratch is an exercise in tradeoffs. Shifting boundaries into the engine requires grappling with TeX’s legendary memory model, figuring out how to pass Donald Knuth’s adversarial test suites, and discovering why LaTeX macro expansion can feel so sluggish on modern hardware.

The posts below explore these internal mechanics in detail. They walk through the architectural choices, the historical constraints of 1982 that shaped the original codebase, what happens when you start tuning a custom C++ engine for millisecond compilation, and more.

Posts

Expressive diagnostics in a custom TeX engine

TeX error messages are infamous for being cryptic, two-line terminal dumps. This post looks at how we can decouple diagnostic reporting into an abstract interface, allowing the engine to toggle between strict TeX82 logging for automated tests and Clang-style expressive diagnostics, with colored source spans, typo suggestions, and overfull box visualizers for human users.

Literate programming and 1982 hardware constraints

Before refactoring code, it helps to understand why it was written in the first place. This post explores Knuth’s original tex.web source, looking at how the constraints of 1982 hardware (such as scarce RAM, a very different landscape of compiler support, and various Pascal quirks) motivated things like fixed-point arithmetic, dense math spacing tables, and macro-based global mutations.

A data-oriented approach to TeX’s memory model

In the original TeX82 engine, all tokens, boxes, glue, and penalties were packed into a single monolithic array of memory_words. Here we discuss how modern Data-Oriented Design (DOD) principles let us dismantle that monolithic array into segregated, typed memory pools wrapped in zero-overhead C++ view facades.

Learning from TeX’s testing philosophy

TeX is famously bug-free, largely because of Donald Knuth’s “TRIP test”, a 450-line handwritten torture test designed to break the engine in every way imaginable. This post is a little meditation on this testing philosophy. I cover how I set up automated golden-snapshot testing in CMake, looked at the code coverage of the trip test, and reflect a little bit on what I am trying to learn from the TRIP test.

(Why) is LaTeX slow? Profiling macro expansion

At this point I hit a cool milestone: instead of just being able to compile base (plain) TeX, I finished implementing the additional engine features required to compile modern LaTeX documents! While I’m not primarily motivated by performance, it naturally made me wonder why is LaTeX actually so much slower than plain TeX? We instrument the engine’s macro expander, and use tools like KCachegrind and flame graphs to visualize exactly where CPU cycles are spent.

Compiling LaTeX documents in milliseconds

A short post this time… having prioritized the easy implementation of new architecture in the earlier phases, what might happen if I took the easy performance wins that I knew I had left on the table? This post looks at timings across representative workloads (from compilign a document with just the letter “A” to stress-testing with a 7,000-page file).