Skip to content

UnicodeMath: fewer allocations reading, and the writer reads back only when needed - #45

Open
simonebortolin wants to merge 1 commit into
claude/gracious-mayer-wl7f46from
claude/unicodemath-parser-speed
Open

simonebortolin wants to merge 1 commit into
claude/gracious-mayer-wl7f46from
claude/unicodemath-parser-speed

Conversation

@simonebortolin

Copy link
Copy Markdown
Owner

Three optimisations of the UnicodeMath port, which LaTeX also goes through. The output is unchanged: every conversion of the corpora gives the same text as before.

What changes

  • Strings read by code points. Js.Ch, Js.Len, Js.Sub and Characters walk the string instead of building a list of runes on each call. CodePoints gets the helpers they use (Count, Offset, LengthAt, ValueAt, TextAt, HasLoneSurrogate). A lone surrogate still reads as U+FFFD.
  • Nothing to map, nothing done. MapToPrivate, MapFromPrivate and SetBuilderBars return their input when it holds nothing they change; AstMapFromPrivate no longer copies the keys of each object.
  • The writer reads back only when reading could differ. UnicodeMathSerializer.Serialize read back everything it wrote, up to three times. It now does so only when NeedsReadBack says the read-back could find something: an intent or argument name the equation holds, an n-ary operator, or an operator with a character that reading gives identifiers with (a letter, ∞, an emoji).

The read-back rule is empirical

NeedsReadBack is not proved from the grammar. The set of operator characters it treats as safe comes from an experiment: every character of the basic plane and of the mathematical, Arabic mathematical and emoji blocks, read as UnicodeMath in eleven contexts, to see which ones ever end up in an mi. Two tests guard it:

  • UnicodeMathWrittenWithoutReadingBackIsTheSame: on both corpora, writing with the rule gives the same text as always reading back.
  • OperatorsWrittenWithoutReadingBackAreTheSame: the same for an operator of each non-letter character of the basic plane, in five contexts. With the character rule switched off, this test reports 468 differences, so it can fail.

An equation outside these contexts could still be written so that it reads back slightly differently, where the full read-back would have caught it.

Numbers

Per equation, from a stopwatch harness over the corpora (738 UnicodeMath examples, 721 of them as LaTeX, 192 Word equations), net10.0. The times are noisy; the allocations are stable.

Conversion Before After
UnicodeMath → UnicodeMath 423–474 µs / 341 KB 261–290 µs / 171 KB
LaTeX → UnicodeMath 446–465 µs / 384 KB 256–264 µs / 187 KB
OMML → UnicodeMath 301 µs / 200 KB 163–184 µs / 152 KB
UnicodeMath → MathML 281 µs / 173 KB 231–286 µs / 137 KB
LaTeX → MathML 238–241 µs / 212 KB 216–241 µs / 159 KB

Verification

  • dotnet test --solution SharpEquations.slnx: 6988 of 6988 pass, on net10.0 and on net8.0 (the netstandard2.0 build).
  • Not verified: Windows / net481 and macOS, which only CI runs. IsPlainOperator uses Unicode categories, whose tables differ between runtimes; a character a runtime does not know is read back, which is the safe side.

Not in this change

A profile of a UnicodeMath round trip puts about 37% of the time in the parser Parlot generates from the grammar. That is the next candidate, and a larger piece of work. LaTeXReader was not profiled and is untouched.

The commit says [no-new-version].


Generated by Claude Code

… only when reading could differ [no-new-version]

Js.Ch, Js.Len, Js.Sub and Characters walk the string instead of building lists of runes; MapToPrivate,
MapFromPrivate and SetBuilderBars return their input when there is nothing to map. The UnicodeMath writer
reads back what it wrote only for an equation with an intent or argument name to keep, an n-ary operator
or an operator of a character that reading gives identifiers with; two tests compare what it writes
with and without reading back, on the corpora and for every operator character of the basic plane.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q8QQiasbLr6WtXXCEP1zrJ
@simonebortolin
simonebortolin changed the base branch from main to claude/xslt-generator November 11, 2026 21:35
@simonebortolin
simonebortolin changed the base branch from claude/xslt-generator to claude/gracious-mayer-wl7f46 November 11, 2026 21:37
@simonebortolin
simonebortolin added this pull request to stack #46 November 11, 2026 21:37
@github-actions

Copy link
Copy Markdown

Pre-release 1.0.7-pr.45.119 is ready: https://github.com/simonebortolin/SharpEquations/releases/tag/pr-45-119-1

NuGet package of d04da98. Updated on every push, deleted when this PR is closed.

This branch has not been deployed

No deployments
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.

2 participants