Skip to content

Fix crash on stretchy glyphs with empty variant lists#206

Merged
kostub merged 3 commits into
masterfrom
fix/stretchy-glyph-empty-variants
May 29, 2026
Merged

Fix crash on stretchy glyphs with empty variant lists#206
kostub merged 3 commits into
masterfrom
fix/stretchy-glyph-empty-variants

Conversation

@kostub
Copy link
Copy Markdown
Owner

@kostub kostub commented May 29, 2026

Problem

iosMathExample crashes with an assertion when switching the font and rendering a stretchy construction such as \overrightarrow{x}:

*** Assertion failure in -[MTTypesetter findStretchyVariantGlyph:...], MTTypesetter.m:1948
reason: 'A glyph is always its own variant, so numVariants should be > 0'

Root cause

Two compounding defects:

  1. Empty variant lists (the crash). MTFontMathTable -getVariantsForGlyph:inDictionary: returned the font's variant list verbatim. Assembly-only glyphs — whose OpenType MathGlyphConstruction has a GlyphAssembly but zero MathGlyphVariantRecords — are stored as an empty h_variants array (e.g. XITS's stretchy arrows uni2190/2192/2194). The lookup returned that empty array, and both -findStretchyVariantGlyph: and -findVariantGlyph:withMaxWidth: assert numVariants > 0, a false premise for these glyphs.

  2. Wrong font actually selected (why it looked like "TeX Gyre Termes"). The iOS example's font-picker switch was missing a break after the Termes case, so selecting TeX Gyre Termes fell through into xitsButtonPressed and the app actually switched to XITS — the font whose arrows trigger the crash. Termes itself names its arrows with real variants and never crashed.

The bug is font-data driven (identical .plist data and CoreText glyph IDs on macOS and iOS), not platform-specific.

Fix

  • Treat an empty variant list the same as an absent one in getVariantsForGlyph:inDictionary: — fall back to the glyph itself. This restores the "a glyph is always its own variant" invariant the callers depend on, so the stretchy path falls through to the horizontal glyph assembly as intended.
  • Add the missing break statements in the example's font picker so each font selection is honored.

Testing

  • New regression test testStretchyArrowAssemblyOnlyFont renders the stretchy arrows in the XITS font; it reproduced the exact assertion before the fix and passes after.
  • Full suite green on both targets: swift test (263 tests, macOS) and the iOS simulator scheme (xcodebuild ... -only-testing:.../testStretchyArrowAssemblyOnlyFont).

🤖 Generated with Claude Code

MTFontMathTable -getVariantsForGlyph:inDictionary: returned the font's
variant list verbatim. For assembly-only glyphs — whose OpenType
MathGlyphConstruction has a GlyphAssembly but zero MathGlyphVariantRecords
(e.g. XITS's stretchy arrows U+2190/2192/2194, stored as an empty
h_variants array) — this returned an empty array. Both
-findStretchyVariantGlyph: and -findVariantGlyph:withMaxWidth: then tripped
the "numVariants > 0" assertion, crashing on e.g. \overrightarrow{x} in XITS.

Treat an empty variant list the same as an absent one: fall back to the
glyph itself. This restores the "a glyph is always its own variant"
invariant the callers rely on, so the stretchy path falls through to the
horizontal glyph assembly as intended.

Also fix a missing break in the iOS example's font picker: selecting
"TeX Gyre Termes" fell through into the XITS case, so the app actually
switched to XITS (the font that triggers the crash) — which is why the
crash was reported as happening "when switching to TeX Gyre Termes".

Add a regression test rendering the stretchy arrows in the XITS font.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request addresses an issue with assembly-only glyphs that have zero variant records by checking if variantGlyphs.count == 0 instead of !variantGlyphs. It also fixes missing break statements in a switch-case block in ViewController.m and adds a regression test. The review feedback highlights a redundant glyph lookup and variable shadowing in MTFontMathTable.m, suggesting to directly use the existing glyph parameter instead.

Comment on lines 436 to 437
CGGlyph glyph = [self.font getGlyphWithName:glyphName];
[glyphArray addObject:@(glyph)];
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The method already receives the glyph as a parameter. Performing a lookup via [self.font getGlyphWithName:glyphName] is redundant and introduces unnecessary overhead (string/dictionary lookups). Additionally, declaring CGGlyph glyph inside this block shadows the method's parameter glyph. We can directly add the parameter glyph to glyphArray.

Suggested change
CGGlyph glyph = [self.font getGlyphWithName:glyphName];
[glyphArray addObject:@(glyph)];
[glyphArray addObject:@(glyph)];

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied. Dropped the round-trip and used the glyph parameter directly. It's also subtly more correct: for a glyph with no name, getGlyphName: returns nil and getGlyphWithName:nil would return glyph 0, losing the real glyph — the parameter is always right.

kostub and others added 2 commits May 29, 2026 23:18
The empty-variant fix in getVariantsForGlyph:inDictionary: also repairs the
vertical large-delimiter path. XITS encodes the stretchy vertical arrows
(U+2191/2193/2195, used as \uparrow/\downarrow/\updownarrow delimiters) as
assembly-only glyphs: empty v_variants but a populated v_assembly. Unlike the
horizontal path, -findGlyph:withHeight: has no numVariants > 0 assertion — with
an empty list it read glyphs[numVariants - 1] = glyphs[-1], an out-of-bounds
stack read (confirmed by AddressSanitizer as a dynamic-stack-buffer-overflow at
MTTypesetter.m:1516). It did not crash without sanitizers because the bogus
glyph value is discarded (glyphAscent/Descent stay 0, forcing the assembly
path), so the defect was latent.

testStretchyVerticalArrowAssemblyOnlyFont renders \left\uparrow \frac{..}{..}
\right\downarrow (and relatives) in XITS, asserting the boundary falls through
to a vertical glyph assembly that covers the content height within the allowed
delimiter shortfall.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop the redundant getGlyphWithName: round-trip in
getVariantsForGlyph:inDictionary:. The fallback already receives the
glyph as a parameter; re-deriving it from its name added an unnecessary
lookup, shadowed the parameter, and would return glyph 0 for nameless
glyphs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@kostub kostub merged commit 9ca84b0 into master May 29, 2026
1 check 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