Skip to content

compiler: the vert family's atom classes, bare \vert/\Vert, and \bmod's \nonscript - #298

Draft
GoKubar wants to merge 1 commit into
mainfrom
agent/linux-math-residuals/vert-class-bmod-nonscript
Draft

GoKubar wants to merge 1 commit into
mainfrom
agent/linux-math-residuals/vert-class-bmod-nonscript

Conversation

@GoKubar

@GoKubar GoKubar commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Three measured math residuals in crates/compiler, each left behind by a lane that ran out of scope. All three were verified against pdfTeX 3.141592653-2.6-1.40.27 (TeX Live 2025) with \showthe\wd of \hbox{$...$} before anything was written down.

FlashTeX's own numbers below are taken at 18pt, where 1mu is exactly 1pt, so the two columns agree in mu rather than in glyph width.

1. \lvert/\rvert/\lVert/\rVert were classed Rel

symbol_class is keyed by the glyph, so the four amsmath spellings inherited the class of the characters they share — \mid's U+2223 (Rel) and \|'s U+2016 (Ord). amsmath declares them \mathopen/\mathclose (amsmath.sty 182-209). They now force their class on the atom, exactly as \bot already does over \perp's glyph.

pdflatex 10pt mu before after
$a\lvert b\rvert c$ 19.46068 +0mu 56.00000 36.00000
$a\lVert b\rVert c$ 23.90514 +0mu 40.32000 40.32000
$a=\rvert b$ 22.91084 −5mu 43.15600 38.15600
$a=\rVert b$ 25.13307 −5mu 45.31600 40.31600
$a\mid b\mid c$ 30.57152 +20mu 56.00000 56.00000

Controls: $abc$ 13.90510pt, $a=b$ 22.91077pt.

Every \lvert x\rvert was paying the full 20mu of the \mid row. \lVert/\rVert were Ord, which coincides with Open/Close between two Ords — the $a=\rVert b$ row is where it does not, and it was 5mu wide. \mid is untouched, and $a\left|b\right|c$ is a third box again (22.79393pt) because \left/\right build an Inner atom, so it was never a substitute for either.

2. Bare $\Vert$ and $\vert$ did not parse

Both resolved only as fence names inside \left/\right/\big, so $\Vert$ answered "\Vert is not supported in math mode" and typeset the literal text, while $\|$ — the same symbol, the same \DeclareMathDelimiter{..}{\mathord}{symbols}{"6B} at fontmath.ltx 465-468 — worked. Both are \mathord in the kernel, so they join the arm above with that class: \Vert → U+2016 at 7.16400pt (\lVert's box), \vert\mid's U+2223 at 5.00400pt. Both were diagnostics before.

3. \bmod kept 1mu where pdflatex keeps 5mu in script styles

\bmod is \nonscript\mskip-\medmuskip\mkern5mu\mathbin{mod}\penalty900\mkern5mu\nonscript\mskip-\medmuskip (latex.ltx 15556-15559). The \mkern5mu is unconditional; the \mskip-\medmuskip sits behind a \nonscript and fires in text and display style only — exactly where the Bin class contributes its own 4mu. The two cancel whenever the Bin survives, so the total is 5mu a side in every style, and a boundary that degrades the Bin to Ord leaves 1mu in text but the whole 5mu in script. The old arm folded that into one constant 1mu kern: right in text, 4mu short a side in script.

Against \mathrm{mod} at 10pt (1mu = 0.555542pt text, 0.455246pt script, 0.408950pt scriptscript):

\bmod control difference
$a\bmod b$ 34.29970 28.74428 10mu
$\scriptstyle a\bmod b$ 27.59433 23.04187 10mu
$\scriptscriptstyle a\bmod b$ 23.94104 19.85153 10mu
$\bmod b$ 24.56947 23.45839 2mu
$\scriptstyle\bmod b$ 23.25668 18.70422 10mu
$a\bmod$ 25.56370 24.45262 2mu
$\scriptstyle a\bmod$ 24.07767 19.52520 10mu

Nucleus::Space grows a nonscript flag — glue that vanishes at level > 0, the same test the class-spacing table already uses for its text-styles-only rows — and \bmod emits the definition's five nodes in its own order. In FlashTeX at 18pt, $x^{a\bmod b}$ goes 44.39480 → 49.99480 and $x^{\bmod b}$ 38.09480 → 43.69480, both +5.6pt = 8mu at the script size; text style is unchanged at 60.00400.

Tests

Two tests in package_gating_tests. the_vert_family_is_open_close_and_ord_but_never_rel derives the bar widths from the lone glyphs and pins all six spellings against the \mid control; bmod_keeps_the_full_five_mu_a_side_in_script_styles measures inside a superscript under all three package combinations. The stale note in double_bar_tests saying \Vert is a fence name only is now an assertion that it parses.

Scope

The six names move from generated math_symbol rows to math_structure rows because they now carry a class, and \vert/\Vert are genuinely new, so --supported goes 542 → 544 math entries. docs/user/compiler.md and supported/supported-latex.json are regenerated by crates/compiler/scripts/render_supported_latex.sh, which tests/supported_latex.rs gates. No oracle data was regenerated.

crates/render-pipeline builds against vendor/compiler, so all of this is inert in the pipeline until that pin moves. No vendor copy is touched here; the re-pin is left to the integration lane, as #274 did for #265.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Xd5Hmwh5GHNTiAmHUJ1MZu

… `\bmod`'s `\nonscript`

Three measured math residuals, each left behind by an earlier lane.

## 1. `\lvert`/`\rvert`/`\lVert`/`\rVert` were `\mathrel`

`symbol_class` is keyed by the glyph, so the four amsmath spellings
inherited the class of the characters they share: `\mid`'s U+2223 (Rel)
and `\|`'s U+2016 (Ord). amsmath declares them open and close fences
(`amsmath.sty` 182-209) by taking `\vert`/`\Vert`'s delimiter code and
adding `"4000000` — class nibble 4, `\mathopen` — then another
`"1000000` for 5, `\mathclose`; its fallback branch writes the same as
`\DeclareMathDelimiter{\lvert}{\mathopen}{symbols}{"6A}...`. They now
force their class on the atom, as `\bot` already does over `\perp`'s
glyph.

Measured with pdfTeX 3.141592653-2.6-1.40.27 (TeX Live 2025) at 10pt
under amsmath, `\showthe\wd` of `\hbox{$...$}`, against `$abc$`
13.90510pt and `$a=b$` 22.91077pt. FlashTeX's own column is at 18pt,
where 1mu is exactly 1pt, so the two columns agree in mu and not in
glyph width:

| | pdflatex | mu | before | after |
| --- | --- | --- | --- | --- |
| `$a\lvert b\rvert c$` | 19.46068 | +0mu | 56.00000 | **36.00000** |
| `$a\lVert b\rVert c$` | 23.90514 | +0mu | 40.32000 | 40.32000 |
| `$a=\rvert b$` | 22.91084 | -5mu | 43.15600 | **38.15600** |
| `$a=\rVert b$` | 25.13307 | -5mu | 45.31600 | **40.31600** |
| `$a\mid b\mid c$` | 30.57152 | +20mu | 56.00000 | 56.00000 |

so every `\lvert x\rvert` was paying the full 20mu of the `\mid` row —
5mu on each of four boundaries — and a closing bar after a relation was
5mu wide where `Rel`→`Close` is 0mu. `\lVert`/`\rVert` were Ord rather
than Rel, which coincides with Open/Close between two Ords; the
`$a=\rVert b$` row is where it does not. `\mid` itself is untouched,
and `$a\left|b\right|c$` is a third box again (22.79393pt) because
`\left`/`\right` build an Inner atom, so it was never a substitute.

## 2. Bare `$\Vert$` and `$\vert$` did not parse

Both resolved only as fence names inside `\left`/`\right`/`\big`
(`DELIMITER_COMMANDS`), so `$\Vert$` answered "\Vert is not supported in
math mode" and typeset the literal text while `$\|$` — the same symbol,
the same `\DeclareMathDelimiter{..}{\mathord}{symbols}{"6B}` at
`fontmath.ltx` 465-468 — worked. Both are `\mathord` in the kernel, not
aliases of the amsmath pair, and they join the arm above with that
class: `\Vert` is U+2016 at 7.16400pt (`\lVert`'s box), `\vert` is
`\mid`'s U+2223 at 5.00400pt, both previously diagnostics.

`\left\vert`/`\left\Vert` keep spelling the same cmsy `"6A`/`"6B` slots
as `|`/U+2016; `cm::delimiter_slot` maps `|` and U+2223 to one slot, so
the two spellings are one delimiter downstream.

## 3. `\bmod` kept 1mu where pdflatex keeps 5mu in script styles

`\bmod` is `\nonscript\mskip-\medmuskip\mkern5mu\mathbin{mod}\penalty900
\mkern5mu\nonscript\mskip-\medmuskip` (`latex.ltx` 15556-15559). The
`\mkern5mu` is unconditional; the `\mskip-\medmuskip` sits behind a
`\nonscript` and so fires in text and display style only — exactly where
the Bin class contributes its own 4mu. The two cancel whenever the Bin
survives, so the total is 5mu a side in *every* style, and a boundary
that degrades the Bin to Ord leaves 1mu in text but the whole 5mu in
script. The previous arm folded that into one constant 1mu kern, which
is right in text and 4mu short a side in script.

Measured at 10pt with `\mathrm{mod}` as the control, 1mu being
0.555542pt in text, 0.455246pt in script and 0.408950pt in
scriptscript (quads of 10, 8.19443 and 7.36111pt):

| | `\bmod` | control | difference |
| --- | --- | --- | --- |
| `$a\bmod b$` | 34.29970 | 28.74428 | 10mu |
| `$\scriptstyle a\bmod b$` | 27.59433 | 23.04187 | 10mu |
| `$\scriptscriptstyle a\bmod b$` | 23.94104 | 19.85153 | 10mu |
| `$\bmod b$` | 24.56947 | 23.45839 | 2mu |
| `$\scriptstyle\bmod b$` | 23.25668 | 18.70422 | 10mu |
| `$\scriptscriptstyle\bmod b$` | 20.06890 | 15.97939 | 10mu |
| `$a\bmod$` | 25.56370 | 24.45262 | 2mu |
| `$\scriptstyle a\bmod$` | 24.07767 | 19.52520 | 10mu |

`Nucleus::Space` grows a `nonscript` flag for this — glue that vanishes
at `level > 0`, the same test the class-spacing table already uses for
its text-styles-only rows — and `\bmod` emits the definition's five
nodes in its own order instead of one folded kern. In FlashTeX at 18pt,
`$x^{a\bmod b}$` goes 44.39480 → **49.99480** and `$x^{\bmod b}$`
38.09480 → **43.69480**, both +5.6pt = 8mu at the script size; text
style is unchanged at 60.00400.

## Tests and gates

Two tests in `package_gating_tests`:
`the_vert_family_is_open_close_and_ord_but_never_rel` derives the bar
widths from the lone glyphs and pins all six spellings plus the `\mid`
control, and `bmod_keeps_the_full_five_mu_a_side_in_script_styles`
measures inside a superscript under all three package combinations. The
stale note in `double_bar_tests` saying `\Vert` is a fence name only is
now an assertion that it parses.

`crates/compiler`: 296 lib tests passed, 0 failed (294 on this same
base — the 2 new); every integration target passes except
`tests/robustness.rs::deeply_nested_input_does_not_blow_the_stack`,
which overflows its stack identically on the unmodified base and is not
this change. `crates/math-layout`: 66 passed, 0 failed, untouched.

The six names move from generated `math_symbol` rows to `math_structure`
rows because they now carry a class, and `\vert`/`\Vert` are genuinely
new, so `--supported` goes 542 → 544 math entries;
`docs/user/compiler.md` and `supported/supported-latex.json` are
regenerated by `crates/compiler/scripts/render_supported_latex.sh`,
which `tests/supported_latex.rs` gates. No oracle data was regenerated.

`crates/render-pipeline` builds against `vendor/compiler`, so all of
this is inert in the pipeline until that pin moves; no vendor copy is
touched here and the re-pin is left to the integration lane.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xd5Hmwh5GHNTiAmHUJ1MZu

Implementation-Agent: engine-engineer (math residuals lane, linux-primary)
Commit-Executor: Claude Code direct (Cursor usage limit fallback)
GoKubar added a commit that referenced this pull request Sep 21, 2026
)

#856 stopped math mode typesetting an unresolved control sequence's name;
what it left is the dropped command itself. Re-measured on main
(13285fe) with `tools/real-world-corpus/math_leaks.py` over the public
documents of #846's table (Green–Tao amsart, the Horodecki revtex4
review, the transformer paper, amsldoc, tufte, three Evan Chen handouts
with today's evan.sty): the PDF-text leak of names is 0 everywhere, and
the `\X is not supported in math mode` count per name is

  379 \varrho, 228 \cal, 203 \bf, 192 \rm, 56 \ensuremath, 50 \Vert,
  10 \mkern, 6 \pmb, 4 \framebox, 4 \columnwidth, 3 \backslash,
  2 \bigvee, 2 \smash, 2 \hdotsfor, 2 \varliminf, 1 each of \coprod,
  \bigotimes, \bigoplus, \bullet, \prec, \longmapsto, \kern, \vert,
  \negmedspace, \negthickspace, \leftroot, \uproot, \injlim, \projlim,
  \varinjlim, \varprojlim, \varlimsup, \iiiint, \idotsint,
  \varGamma..\varOmega (11), \it (2)

evan.sty's `\dang`/`\half`/`\dg` rows of the original table are gone
since .sty reading landed; `\dagger` since #441.

Implemented, each per fontmath.ltx / latex.ltx / amsmath.sty (TeX Live
2026), line numbers in the arms:

* `COMMAND_GLYPHS` rows for the kernel symbols math-layout already boxes
  from their cmmi/cmsy/cmex slots (9bdcd5c) but the compiler never
  named: \varrho, \bullet, \prec, \succ, \preceq, \succeq, \coprod,
  \bigvee, \bigwedge, \biguplus, \bigcap, \bigcup, \bigotimes, \bigoplus,
  \bigodot, \bigsqcup, \longmapsto — with their classes in `symbol_class`,
  display limits in `takes_display_limits`, and Latin Modern Math
  advances (read from the bundled latinmodern-math.otf) for the base-14
  export route.
* `\backslash`: Ord over the \setminus glyph (cmsy "6E). The six `vert`
  spellings with forced Open/Close/Ord classes — the arm first written for
  #298, which never landed.
* `\ensuremath` in math (the group) and in text (inline math over the
  braced argument, `Parser::ensuremath`).
* `\mkern`/`\mskip` mu glue (`take_mu_glue`); amsmath's `\medspace`,
  `\thickspace`, `\negmedspace`, `\negthickspace` (gated); `\thinspace`/
  `\negthinspace` (kernel .16667em text kern, 3mu under amsmath);
  `\hdots` (amsmath alias of \ldots).
* `\rm \bf \it \sf \tt \cal \mit` in math as `\DeclareOldFontCommand`'s
  math branch (`\@fontswitch`, latex.ltx 14312): the rest of the current
  group re-read as `\mathrm{...}` etc. (`rest_of_group_alphabet`; the
  group ends at `}`, an enclosing `\right`, `&` or `\\`). `{\bf 1}_S`,
  `${\cal H}_A$` and `\inf_{\sigma\in\rm SEP}` all come out as pdflatex
  sets them.
* `missing_package` recovery drops the command instead of typesetting its
  literal name, as pdflatex and #856 do.
* `\mathbf` with a non-plain argument parses it as math instead of
  keeping a control word's *name* as bold text (`\mathbf{\alpha}` printed
  `\alpha`; the revtex idiom `\bf\sigma` surfaced it): lowercase Greek
  passes through unchanged as in pdflatex, and letters/digits that
  pdflatex would embolden are reported as not set bold.
* Generated inventory (`render_supported_latex.sh`) regenerated.
* `tools/real-world-corpus/math_leaks.py`: the re-measurement harness
  (pdflatex is the oracle only; never in the product path).

Verified: `cargo test` and `cargo test --release` in crates/compiler,
0 failures (both profiles, --no-fail-fast; 528 unit tests, with new
ones for the group boundary of `\rm`/`\bf`/`\cal` and `\mkern`'s mu glue). Oracle fixtures for glyph
identity and positions land with the render-pipeline re-pin.

Left out, with root causes: \pmb (amsbsy triple overprint), \smash,
\framebox/\parbox/\columnwidth in math, \kern<dimen>, \leftroot/\uproot,
\hdotsfor (unimplemented constructs, 1–6 uses, all in amsldoc);
\varGamma..\varOmega and the \varliminf/\injlim family (amsmath
declarations needing cmmi italic capitals and \mathop{\underline{\lim}}
builds, 1 use each); \xymatrix/\ar (xypic, unsupported package).

Implementation-Agent: Claude Fable 5.1 (kabir-claude GH846, mac-m5pro-kabir)
Commit-Executor: Claude Fable 5.1 via Claude Code (direct git)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xd5Hmwh5GHNTiAmHUJ1MZu
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