Skip to content

XSLT generator: generated code faster than XslCompiledTransform - #43

Open
simonebortolin wants to merge 14 commits into
claude/xslt-generatorfrom
claude/gracious-mayer-wl7f46
Open

simonebortolin wants to merge 14 commits into
claude/xslt-generatorfrom
claude/gracious-mayer-wl7f46

Conversation

@simonebortolin

@simonebortolin simonebortolin commented Nov 11, 2026 •

Copy link
Copy Markdown
Owner

Stacked on #5 (the XSLT → C# source generator that makes the library Native AOT compatible): it optimises the generated code and its run time. main is merged in, so the diff also shows the three files of #37, which #5 does not have yet.

What changes

  • Location paths are compiled to inlined loops in methods of their own, instead of LINQ over node lists; no node-set is built for an existence test, a first string, a count or a comparison.
  • Template parameters are typed over several passes, so strings, numbers and single nodes are not boxed as node-sets; text-only variables stay strings.
  • The result tree allocates its stacks and builders only when needed.
  • translate() builds its map once; substring() and string-length() work without lists.
  • Rules such as char[mt_code_value='0x…'] (341 in mtef2mml.xsl) are found by a switch on the child's text; character references are decoded without a parser.
  • StylesheetBenchmarks measures the stylesheets alone, compiled and run by XslCompiledTransform.
  • conformance.xsl, a test stylesheet of what the generator compiles (axes, positional predicates, node-set and mixed-type parameters, cross-type comparisons, keyed dispatch), is run compiled and by XslCompiledTransform.

Numbers

BenchmarkDotNet StylesheetBenchmarks, short job, net10.0, time and allocation per whole corpus. The short job is noisy, so the ratios are approximate. Measured before the last merge of #5 and main.

Stylesheet #5 This branch XslCompiledTransform
OMML → MathML 206–247 ms / 461–470 MB 11.1 ms / 9.8 MB 65–108 ms / 160 MB
MathML → OMML 141–196 ms / 261–268 MB 17.8 ms / 16.9 MB 63–90 ms / 140 MB
MTEF → MathML 85–96 ms / 80–82 MB 5.5 ms / 5.8 MB 35–59 ms / 21 MB
Content MathML (ctop) 0.403 ms / 507 KB 0.117 ms / 167 KB 1.67–2.5 ms / 2.49 MB

Verification

  • dotnet test --solution SharpEquations.slnx on the merged head: 6994 of 6994 pass, on net10.0 and on net8.0 (the netstandard2.0 build).
  • CompiledStylesheetTests finds the output of the four stylesheets and of the conformance stylesheet identical to XslCompiledTransform. As in XSLT stylesheets compiled to C# by a source generator: Native AOT #5, inputs with characters outside the basic plane are left out of that comparison.
  • A Native AOT publish (linux-x64) of a console application using the library gave no warnings, and the binary converted UnicodeMath → OMML → back and Content MathML → LaTeX. This was done before the last merge; MTEF was not exercised in the AOT binary.
  • Unsupported XSLT (following axis, id(), xsl:sort) still fails the build with XSLT001: checked by hand, there is no automated test for it.
  • Not verified: Windows / net481 and macOS, which only CI runs.

To confirm

🤖 Generated with Claude Code

https://claude.ai/code/session_01Q8QQiasbLr6WtXXCEP1zrJ

claude and others added 14 commits November 10, 2026 12:31
ctop.xsl, which main runs for Content MathML, is compiled like the other stylesheets (CtopStylesheet) and compared
with XslCompiledTransform on the equations of tests/data/contentmathml. The tests of OmmlAstralTests that pinned
the UTF-16 counting of XslCompiledTransform pin what they said counting code points gives.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q8QQiasbLr6WtXXCEP1zrJ
…ransform [no-new-version]

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q8QQiasbLr6WtXXCEP1zrJ
…string-length() without lists

The map of a translate() with constant arguments is a static table, and that of the others is kept from one call to
the next; a text that nothing is replaced in is returned as it is.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q8QQiasbLr6WtXXCEP1zrJ
…as strings

A location path is compiled to loops over the nodes (first child and next sibling, the node test inlined, names
compared as static XName fields), in the method that uses it or in a method of its own whose parameters are what the
expression reads; no node-set is built for a test, the string value of the first node, a count or a comparison, and
position() and last() are counted only by the predicates that read them. xsl:for-each and xsl:apply-templates walk
the nodes without a list unless the content, or a template of the mode, reads their number.

The stylesheet is compiled until nothing new is learnt of its templates: the parameters of a named template have the
type of the values they are given, and a template is given the context node, position and size only if it reads them.
A variable whose content only writes text is a string, written as an expression when it is only values and choices,
and a global string that does not depend on the input is computed once.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q8QQiasbLr6WtXXCEP1zrJ
… are needed

The open element is found by its parent, the text between two nodes is kept as the string it mostly is, its markup is
built only once some is written with disable-output-escaping, and a short whole number is read without a copy.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q8QQiasbLr6WtXXCEP1zrJ
…ns of unknown types without LINQ

A variable or a parameter whose node-set has at most one node (the context node, a parent, a step with a position)
holds the node or null, not a list. The nodes of a path from a list are sorted only when the list has more than one
node. The comparison of values whose type is known at run time only walks the nodes without iterators, and takes a
string or a number as it is.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q8QQiasbLr6WtXXCEP1zrJ
…ces read without a parser

The many rules of an element whose pattern asks for a child with a text (the characters of MTEF, each with its code)
are found by a switch on the text of that child, when the element has one, and tried in order otherwise. Text written
with disable-output-escaping that is only text and references to characters is read as it is, not by an XML parser.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q8QQiasbLr6WtXXCEP1zrJ
…ces after a comma (#37)

Four equations that read from UnicodeMath and were written as text that read back
to another equation:

- a script of a matrix in brackets: ⒨(𝑎&𝑏@𝑐&𝑑)² read the ² apart from the
  matrix (and ^−1 did not parse); the matrix is grouped, 〖⒨(…)〗², as the other
  bases that a script would not apply to whole
- a function in a denominator: 𝑎/ sin⁡𝛼 read as a slash, the space after the
  bar ending the fraction; the space is not written, 𝑎/sin⁡𝛼
- a denominator that ends with a script before an operand: 1/4𝜋𝜀₀ 𝑞₁𝑞₂/𝑟²
  took 𝑞₁𝑞₂/𝑟² into the denominator, the space ending the script and not the
  denominator; it is in parentheses, 1/(4𝜋𝜀₀) (but not after superscript
  digits, 𝜕²/𝜕𝑥² 𝜓, which a space ends, nor before an operator)
- ",  " (a comma and two spaces, a space of ½ em after the comma in the
  grammar) was read as an empty <mo/>, which nothing writes: it is the space of
  that width, U+2002, as ", " reads


Claude-Session: https://claude.ai/code/session_01SyVio8afJsGwHW9i5v5HDs

Co-authored-by: Claude <noreply@anthropic.com>
… XslCompiledTransform [no-new-version]

The axes, the predicates that count, the node-sets as variables and parameters, the comparisons between the types,
the parameters of mixed types, the rules found by the text of a child: beyond what the stylesheets of the library
use of them on the corpora.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q8QQiasbLr6WtXXCEP1zrJ
Not yet verified to build and pass on both target frameworks.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q8QQiasbLr6WtXXCEP1zrJ
…de/gracious-mayer-wl7f46

# Conflicts:
#	tests/SharpEquations.Tests/Omml/OmmlAstralTests.cs
@simonebortolin
simonebortolin changed the base branch from main to claude/xslt-generator November 11, 2026 21:20
@simonebortolin
simonebortolin marked this pull request as ready for review November 11, 2026 21:20
@github-actions

Copy link
Copy Markdown

Pre-release 1.1.0-pr.43.118 is ready: https://github.com/simonebortolin/SharpEquations/releases/tag/pr-43-118-1

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

@simonebortolin
simonebortolin added this pull request to stack #46 November 11, 2026 21:37

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