Skip to content

Line breaking: TeX's two passes, and the parameters a document sets - #194

Merged
tannevaled merged 1 commit into
mainfrom
linebreak-fitness
Sep 3, 2026
Merged

Line breaking: TeX's two passes, and the parameters a document sets#194
tannevaled merged 1 commit into
mainfrom
linebreak-fitness

Conversation

@tannevaled

Copy link
Copy Markdown
Contributor

The paragraph builder hyphenated every word up front and ran a single pass over
the result, with the tolerances hard-coded in the call. TeX does neither.

Two passes (tex.web §16986-16999). The first pass has no hyphenation points
at all and works at \pretolerance; only when it finds nothing does the second
run over the hyphenated list at \tolerance. That ordering is why real TeX
hyphenates far less than an optimiser that holds the hyphens from the start —
an unhyphenated setting wins whenever one is within reach. The emergency pass
stays behind both, unchanged.

The document's own parameters. \tolerance, \linepenalty, \adjdemerits,
\doublehyphendemerits and \finalhyphendemerits are read from the count
registers they live in, so a class that sets them gets the paragraphs it asked
for. They reach the optimiser as linebreak.Params.

The library side is go-typeset/linebreak#2, released as v1.1.0: fitness classes
with the best break kept per class, TeX's demerits in full, and — the change
that moves the corpus — feasibility tested on the BADNESS rather than the
adjustment ratio. \tolerance=200 had meant "stretch a line up to 200 times its
stretchability" instead of "up to about 1.26 of it", so no line was ever
rejected for being too loose.

Measured, against tectonic

main this
arXiv page error (157 papers) 633 624
beamer page error (79 decks) 40 40
layout divergence, 50 papers, seed 1 4.097 4.165
layout divergence, 50 papers, seed 7 10.363 10.350

The two divergence samples disagree in sign, so that barometer is flat within
its own noise; the page count, deterministic over 157 documents, is 9 pages
closer. Two intermediate states were measured and are not what shipped: the
fitness classes alone move nothing (633, 4.140), and the two passes without the
badness fix are worse on both (636, 4.350) — the passes only pay once a
tolerance means what it says.

… sets

The paragraph builder hyphenated every word up front and ran one pass over the
result. TeX runs the paragraph twice (tex.web §16986-16999): the FIRST pass
knows no hyphenation points at all and works at \pretolerance, so an
unhyphenated setting wins whenever one exists — which is why real TeX hyphenates
far less than an optimiser holding the hyphens from the start. Only when that
pass finds nothing does the second one run, over the hyphenated list, at
\tolerance; the emergency pass stays behind it as before.

The tolerances and demerits are now read from the count registers they live in
rather than hard-coded, so \tolerance, \linepenalty, \adjdemerits,
\doublehyphendemerits and \finalhyphendemerits do what a document says. They
reach the optimiser through linebreak.Params (go-typeset/linebreak v1.1.0),
which also brings TeX's fitness classes and a tolerance compared against the
BADNESS rather than the adjustment ratio.

Measured against tectonic: arXiv page error 633 → 624 over 157 papers, beamer
unchanged at 40 over 79 decks, layout divergence flat within its own noise
(+0.068 on a 50-paper sample, −0.013 on another).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@tannevaled
tannevaled merged commit 2952092 into main Sep 3, 2026
18 checks passed
@tannevaled
tannevaled deleted the linebreak-fitness branch September 3, 2026 08:52
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