engine: an align cell keeps its nested environments whole - #153
Merged
Conversation
collectAlignBody tracked brace depth, and a matrix is not braced. So the & and \\
inside \begin{bmatrix}…&…\\…\end{bmatrix} were read as the OUTER alignment's own
separators, and one matrix became four cells over two rows. The maths layer then
refused each fragment:
texmath: \begin{bmatrix} without \end " = \begin {bmatrix} \mathrm {mat}…"
texmath: unexpected "}" " \begin {bmatrix} 1}"
A nested \end at depth 0 was worse than mis-split: its name was read and thrown
away, so \end{cases} inside a cell left the environment open.
Environments are now counted as well as braces, and while one is open the
separators belong to it. \begin/\end are written back into the cell with the space
scanMathSource puts after a control word, so the maths layer reads them exactly as
it would have.
Measured over the 200-paper arXiv corpus, counting every formula the maths layer
drops: 4228 → 3702, and on the worst documents 112 → 4, 78 → 10, 122 → 58. 39
documents change, 4 repaginate (three closer to tectonic, one further), +2087
glyphs; the page error against the 157 tectonic renders falls 672 → 670 with one
more exact. beamer does not move.
tannevaled
added a commit
that referenced
this pull request
Aug 31, 2026
#153 said "a matrix is not braced". That is true of this engine and FALSE of TeX, and the difference is worth stating where the code is read. tex.web §6738-6742: align_state is increased by 1 for a { and decreased by 1 for a }, and §7259-7264: an alignment entry ends at a tab or \cr only when align_state is back where it started. A nested matrix is protected by a real brace, an implicit one: \newenvironment{bmatrix}{\left[\env@matrix}{\endmatrix\right]} amsmath.sty:1082 \def\env@matrix{…\array{*\c@MaxMatrixCols c}} amsmath.sty:1058 \def\@array[#1]#2{…\bgroup …} latex.ltx:12101 This engine never expands \bmatrix — the maths layer parses the source text itself — so that \bgroup never arrives and the counter cannot see it. Counting the environment is the same protection by another road, and the comment now says so. The reading also turned up something the code did not do: tex.web §7492-7493 counts IMPLICIT braces, \bgroup and \egroup, in align_state. The cell scanner looked only at category-1 and -2 characters, so a & inside \bgroup…\egroup cut a cell that TeX would not have cut. implicitChar is what the box scanner already uses for \bgroup; the cell scanner now uses it too. Measured over 200 arXiv papers and 200 beamer talks: not one document changes — the corpus does not exercise it. The unit test does, on the scanner directly: with \bgroup x & y\egroup a row has two cells, and had three.
tannevaled
added a commit
that referenced
this pull request
Aug 31, 2026
collectTabularBody tracked brace depth, and a matrix is not braced here — the same fault #153 fixed in the maths alignment scanner, in the scanner next door. So $\begin{matrix} a & b \end{matrix}$ & $\begin{bmatrix} c & d \end{bmatrix}$ became four cells of fragments, and the maths layer refused each one: texmath: unexpected "}" "\begin {matrix} d_{\ell _2}}" 224 of the corpus's dropped formulas are that, all of them tables of matrices. TeX protects a nested alignment with the brace \@array opens (latex.ltx:12101, reached through amsmath's \env@matrix), counted in align_state (tex.web §6738-6742, §7259-7264). This engine never expands \matrix, so the environment is counted instead, and \bgroup/\egroup count as the braces they are (§7492-7493). A nested tabular was broken the same way and is fixed with it: its \end closed the OUTER table, so everything after it was swallowed. Measured over 200 arXiv papers and 200 beamer talks: 15 documents change and 7 repaginate, for +5094 glyphs. One goes 5 -> 14 pages, exactly what tectonic renders; another 41 -> 35 against 34. The page error against the 157 tectonic renders falls 670 -> 656. beamer does not move by one glyph.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
collectAlignBodytracked brace depth — and a matrix is not braced. So the&and\\inside\begin{bmatrix}…&…\\…\end{bmatrix}were read as the outer alignment's own separators, and one matrix became four cells over two rows. The maths layer then refused each fragment:A nested
\endat depth 0 was worse than mis-split: its name was read and thrown away, so an\end{cases}inside a cell left the environment it closes open.Environments are now counted as well as braces, and while one is open the separators belong to it.
\begin/\endare written back into the cell with the spacescanMathSourceputs after a control word, so the maths layer reads them exactly as it would have.Measured — every formula the maths layer drops, 200 arXiv papers
On the worst documents: 112 → 4, 78 → 10, 122 → 58, 232 → 204.
39 documents change and 4 repaginate — three closer to tectonic, one further — for +2087 glyphs. The page error against the 157 tectonic renders falls 672 → 670 with one more exact match. beamer does not move by one glyph.
Test
alignnestedenv_test.go:bmatrix,casesand a nestedarrayinsidealign*must each leave the maths layer with nothing to drop — andalignmust still number two rows(1)(2), since the guard is nesting, not blindness. The first fails onmainon all three.