Skip to content

engine: a macro substituted into a formula brings TeX with it - #179

Merged
tannevaled merged 1 commit into
mainfrom
math-expand-primitives
Aug 31, 2026
Merged

engine: a macro substituted into a formula brings TeX with it#179
tannevaled merged 1 commit into
mainfrom
math-expand-primitives

Conversation

@tannevaled

Copy link
Copy Markdown
Contributor

The maths retry path substitutes a macro's body into the source as text. A class's body is TeX, not maths — \the@inst is \number\c@inst, an author's mark is an \@for loop over \expandafter and \edef — and the maths layer can only refuse those primitives, which costs the whole formula. The census over 200 arXiv papers had \expandafter first by documents (7) and \number second (6), both of them the same affiliation machinery landing in a superscript:

^{\@for \@@affmark :=1\do {\edef \affnum {\@ifundefined {X@\@@affmark }…
^{\the@inst }

The substituted material now goes through the gullet, as \edef would. It is bounded to what was spliced, and an isolated expansion can no longer read past its own sentinel (#174) — which is what makes this safe where expanding the whole source was not: that experiment cost one paper 78% of its glyphs and is recorded in #146.

Retokenising the material needs @ to be a letter inside a control word, as TeX reads it under \makeatletter and as go-tex/math now does (go-tex/math#8): \c@inst split at the @ names \c, a macro nobody has. Every control sequence this engine writes carries a trailing space, so a name never bleeds into what follows it.

Measured — 200 arXiv papers, 200 beamer talks

pages glyph paths page error exact
arXiv 4416 → 4363 10283915 → 10593326 (+309411) 651 → 624 14 → 17
beamer 1175 → 1175 −771 over 8 talks 42 62/80

+3% of everything the corpus typesets, over 129 documents with 27 repaginating. On the worst paper the formulas the maths layer refuses go 94 → 18. beamer does not move a page; of its 8 changed talks one is a pandoc template whose "content" is $if(...)$ placeholders.

Test

mathexpandprimitives_test.go: a macro whose body holds \number, \the, \expandafter or \romannumeral must leave the maths layer with nothing to drop, and must reach the page. All four fail on main.

The maths retry path substitutes a macro's body into the source as text. A class's
body is TeX, not maths — \the@inst is \number\c@inst, an author's mark is an \@for
loop over \expandafter and \edef — and the maths layer can only refuse those
primitives, which costs the whole formula. The census over 200 arXiv papers had
\expandafter first by documents (7) and \number second (6), both of them the same
affiliation machinery landing in a superscript.

The substituted material now goes through the gullet, as \edef would. It is bounded
to what was spliced, and an isolated expansion can no longer read past its own
sentinel (#174) — which is what makes this safe where expanding the WHOLE source was
not: that experiment cost one paper 78% of its glyphs and is recorded in #146.

Retokenising the material needs @ to be a letter inside a control word, as TeX reads
it with \makeatletter and as go-tex/math now does: \c@inst split at the @ names \c,
a macro nobody has. Every control sequence this engine writes carries a trailing
space, so a name never bleeds into what follows it.

Measured over 200 arXiv papers and 200 beamer talks: arXiv gains 309411 glyphs — 3%
of everything the corpus typesets — over 129 documents, 27 of them repaginating; the
page error against the 157 tectonic renders falls 651 -> 624 with three more exact
matches. On the worst paper the formulas the maths layer refuses go 94 -> 18. beamer
does not move a page and loses 771 glyphs over 8 talks, one of them a pandoc template
whose "content" is $if(...)$ placeholders.
@tannevaled
tannevaled merged commit 291332a into main Aug 31, 2026
18 checks passed
tannevaled added a commit that referenced this pull request Sep 1, 2026
#179 says it runs "the substituted material" through the gullet; it ran the WHOLE
maths source. The rest of that source is the author's maths, and the engine's
text-mode stand-ins won there:

	\def\cdot{\char183\relax}                              (latex.go)
	\def\begin#1{\gotex@checkenv{#1}\csname #1\endcsname}

so every \cdot became \char — 935 dropped formulas over 45 papers — and every
\begin{bmatrix} became \bmatrix, 676 over 42. Worse than the drops: with \begin
expanded, \gotex@checkenv opened the environment and the ENGINE typeset the matrix as
text. One paper came out with 411251 glyphs on seven pages, 58000 a page, where it
has 69284.

Now only the body just substituted is flattened, which is what the seam was for: a
class's \the@inst is \number\c@inst and that still resolves. \begin and \end are
marked \noexpand inside the body too, for a macro that wraps an environment.

Measured over 200 arXiv papers and 200 beamer talks, against main:

	formulas the maths layer refuses   4312 -> 2715
	arXiv page error (157 renders)      624 -> 634, exacts 17 -> 19
	the flooded paper                411251 -> 69284 glyphs

The page error moves the wrong way by ten because the flood was flattering it. Against
the state before #179 the pair now stands at 651 -> 634 with 14 -> 19 exact, for 212
more dropped formulas — a trade the exact matches and the junk removal earn.
tannevaled added a commit that referenced this pull request Sep 1, 2026
#179 says it runs "the substituted material" through the gullet; it ran the WHOLE
maths source. The rest of that source is the author's maths, and the engine's
text-mode stand-ins won there:

	\def\cdot{\char183\relax}                              (latex.go)
	\def\begin#1{\gotex@checkenv{#1}\csname #1\endcsname}

so every \cdot became \char — 935 dropped formulas over 45 papers — and every
\begin{bmatrix} became \bmatrix, 676 over 42. Worse than the drops: with \begin
expanded, \gotex@checkenv opened the environment and the ENGINE typeset the matrix as
text. One paper came out with 411251 glyphs on seven pages, 58000 a page, where it
has 69284.

Now only the body just substituted is flattened, which is what the seam was for: a
class's \the@inst is \number\c@inst and that still resolves. \begin and \end are
marked \noexpand inside the body too, for a macro that wraps an environment.

Measured over 200 arXiv papers and 200 beamer talks, against main:

	formulas the maths layer refuses   4312 -> 2715
	arXiv page error (157 renders)      624 -> 634, exacts 17 -> 19
	the flooded paper                411251 -> 69284 glyphs

The page error moves the wrong way by ten because the flood was flattering it. Against
the state before #179 the pair now stands at 651 -> 634 with 14 -> 19 exact, for 212
more dropped formulas — a trade the exact matches and the junk removal earn.
tannevaled added a commit that referenced this pull request Sep 1, 2026
The engine spells some characters it has no glyph command for as \char:

	\def\cdot{\char183\relax}   \def\bullet{\char8226\relax}   (latex.go)
	\protected\def\_{\char95\relax}                            (classkernel.go)

In a formula those names belong to the maths layer, which renders ⋅ and ∙ itself. A
substituted body is flattened through the gullet (#179, #182), and flattening turned
them into \char — a primitive the layer refuses, costing the whole formula. A paper's
own \expect{}{\cdot} lost every occurrence.

A body that begins with \char is what makes a stand-in recognisable, without a list
to keep in step with the kernel; those names are marked \noexpand for the flattening,
beside \begin and \end.

Measured over 200 arXiv papers and 200 beamer talks: the formulas the maths layer
refuses fall 2715 -> 2635, arXiv gains 860 glyphs over 8 documents and one more exact
page match (20 of 157), the page error 634 -> 633. beamer does not move.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant