Skip to content

engine: \@ifnextchar halves ## in the branch it picks, and package names expand - #99

Merged
tannevaled merged 1 commit into
mainfrom
ifnextchar-halving
Aug 27, 2026
Merged

engine: \@ifnextchar halves ## in the branch it picks, and package names expand#99
tannevaled merged 1 commit into
mainfrom
ifnextchar-halving

Conversation

@tannevaled

Copy link
Copy Markdown
Contributor

Two defects that turn out to be one story: no beamer theme had ever been loaded,
and every keyval value was silently lost.

1. \@ifnextchar must halve ## in the branch it picks

It does not insert the branch — it stores it first. ltdefns.dtx:

\long\def\@ifnextchar#1#2#3{\let\reserved@d=#1
  \def\reserved@a{#2}\def\reserved@b{#3}\futurelet\@let@token\@ifnch}

and \def\reserved@b{#3} scans the branch as a macro body, which halves ## a
second time (tex.web §479: a # followed by a # stores one #). This engine's
\@ifnextchar is a primitive that inserted the branch verbatim, skipping it.

keyval is built on exactly that:

\def\define@key#1#2{\@ifnextchar[{\KV@def{#1}{#2}}{\long\@namedef{KV@#1@#2}####1}}

The #### is halved once into \define@key's own body and a second time by
\@ifnextchar, leaving the one # that \def reads as the parameter text #1.
Without it the generated macro's parameter text was ##1 and bound nothing
\setkeys{fam}{k=VAL} gave <<>>.

\meaning\KV@fam@k
real LaTeX \long macro:#1-><<#1>>
before macro:##1-><<#1>>
after macro:#1-><<#1>>

\@ifstar and \@ifnextbracket pick their branch the same way and are halved for
the same reason (\def\@ifstar#1{\@ifnextchar *{\@firstoftwo{#1}}}).

2. Package names expand

\usepackage, \documentclass and \LoadClass now expand their {name}.
Checked in one line: \def\nm{marker}\usepackage{zz\nm} loads zzmarker.sty in
real LaTeX and loaded nothing here. beamer builds every theme file name that way —
\beamer@calltheme does \usepackage[{#1}]{beamertheme\beamer@themename} — so
\usetheme{default} asked for a package literally named
beamertheme\beamer@themename.

Together

The frametitle template lives in beamerouterthemedefault.sty, so it finally
exists and a frame title renders; and because key values no longer leak, the
theme files stop contributing four pages of debris. On the minimal document that is
exactly the reference:

before after reference
n frames n pages n pages n
frame title absent present present
body present present present
page 364.2×273.15 364.2×273.15 364.195×273.147

Measured, including every apparent loss

200 beamer talks: 0 content lost, pages 1061 → 1095, vector paths
155 106 → 174 863. Eight documents stop producing a sized page; each was checked by
hand:

  • four are corpus fragments with no \begin{document} — 0 pages is what real
    LaTeX gives them;
  • one is a talk with no frame at all;
  • one is a [fragile] frame whose verbatim body this engine cannot render
    either way — debris before, less debris after;
  • one is unchanged when re-run cold;
  • one (039a234b02c5…) trips the runaway guard on main too — the guard now
    fires sooner, so 40 KB of debris becomes none.

200 arXiv papers: the single apparent loss renders 2.46 MB in both binaries
when re-run cold — it had simply exceeded the sweep's 90 s cap.

…mes expand

Two defects that turn out to be one story: no beamer theme had ever been loaded,
and every keyval value was silently lost.

1. \@ifnextchar does not INSERT the branch it picks — it stores it first.
   ltdefns.dtx:

	\long\def\@ifnextchar#1#2#3{\let\reserved@d=#1
	  \def\reserved@a{#2}\def\reserved@b{#3}\futurelet\@let@token\@ifnch}

   and \def\reserved@b{#3} SCANS the branch as a macro body, which halves ## a
   second time (tex.web §479: a # followed by a # stores one #). This engine's
   \@ifnextchar is a primitive that inserted the branch verbatim, skipping it.

   keyval is built on exactly that:

	\def\define@key#1#2{\@ifnextchar[{\KV@def{#1}{#2}}{\long\@namedef{KV@#1@#2}####1}}

   The #### is halved once into \define@key's own body and a second time by
   \@ifnextchar, leaving the one # that \def reads as the parameter text #1.
   Without it the generated macro's parameter text was ##1 and bound nothing:
   \setkeys{fam}{k=VAL} gave <<>>. Real LaTeX gives \long macro:#1-><<#1>> where
   this engine gave macro:##1-><<#1>>. \@ifstar and \@ifnextbracket pick their
   branch the same way and are halved for the same reason.

2. \usepackage, \documentclass and \LoadClass now EXPAND their {name}. Checked in
   one line: \def\nm{marker}\usepackage{zz\nm} loads zzmarker.sty in real LaTeX
   and loaded nothing here. beamer builds every theme file name that way —
   \beamer@calltheme does \usepackage[{#1}]{beamertheme\beamer@themename} — so
   \usetheme{default} asked for a package literally named
   "beamertheme\beamer@themename".

Together: the frametitle template (beamerouterthemedefault.sty) finally exists, so
a FRAME TITLE renders; and because key values no longer leak, the theme files stop
contributing four pages of debris. On the minimal document that is exactly the
reference — n frames make n pages, title and body both present, page 364.2x273.15.

Measured over 200 real beamer talks: 0 content lost, pages 1061 -> 1095, vector
paths 155106 -> 174863. Eight documents stop producing a sized page and each was
checked by hand: four are corpus FRAGMENTS with no \begin{document} (0 pages is
what real LaTeX gives them), one is a talk with no frame at all, one is a fragile
frame whose verbatim body this engine cannot render either way, one is unchanged
when re-run cold, and one (039a234b02c5…) trips the runaway guard on main TOO —
the guard now fires sooner, so 40KB of debris becomes none. Over 200 arXiv papers:
the single apparent loss renders 2.46MB in both binaries when re-run cold; it had
exceeded the sweep's time cap.
@tannevaled
tannevaled merged commit 68eebf5 into main Aug 27, 2026
18 checks passed
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