In this section I'll try to draw out some observations about the XSLT implementation.
Like most XSLT code, it has been developed incrementally: rules are added as the need for them is discovered. This is one of the strengths of XSLT as an implementation language for this kind of task: the program can grow very organically, with little need for structural refactoring. At the same time, uncontrolled growth can easily result in a lack of structure. How many modes should there be, and how do we decide? How should the code be split into modules? How should template rule priorities be allocated?
Again, like most XSLT applications, it's not just template rules: there are also quite a few functions. And as in other programming languages, the set of functions you end up with, and their internal complexity and external API, can grow rather arbitrarily.
It's worth looking a little bit at the nature of the XML we're dealing with. Here's a sample:
<member nodeType="MethodDeclaration"> <body nodeType="BlockStmt"> <statements> <statement nodeType="ReturnStmt"> <expression nodeType="BinaryExpr" operator="PLUS"> <left nodeType="MethodCallExpr" RETURN="double" RESOLVED_TYPE="net.sf.saxon.expr.Expression"> <name nodeType="SimpleName" identifier="getCost"/> <scope nodeType="MethodCallExpr" RETURN="net.sf.saxon.expr.Expression" DECLARING_TYPE="net.sf.saxon.expr.BinaryExpression"> <name nodeType="SimpleName" identifier="getLhsExpression"/> </scope> </left> <right nodeType="BinaryExpr" operator="DIVIDE"> <left nodeType="MethodCallExpr" RETURN="double" RESOLVED_TYPE="net.sf.saxon.expr.Expression"> <name nodeType="SimpleName" identifier="getCost"/> <scope nodeType="MethodCallExpr" RETURN="net.sf.saxon.expr.Expression" DECLARING_TYPE="net.sf.saxon.expr.BinaryExpression"> <name nodeType="SimpleName" identifier="getRhsExpression"/> </scope> </left> <right nodeType="IntegerLiteralExpr" value="2"/> </right> </expression> </statement> </statements> </body> <type nodeType="PrimitiveType" type="DOUBLE" RESOLVED_TYPE="double"/> <modifiers> <modifier nodeType="Modifier" keyword="PUBLIC"/> </modifiers> <annotations> <annotation nodeType="MarkerAnnotationExpr"> <name nodeType="Name" identifier="Override"/> </annotation> </annotations> </member>
This represents the Java code
@Override public double getCost() { return getLhsExpression().getCost() + getRhsExpression().getCost() / 2; }
It's interesting to look at the values used (a) for the element name (e.g.
body
, left
, right
, expression
, statement
),
and (b) for the nodeType
attribute
(e.g. ReturnStmt
, BinaryExpr
, SimpleName
).
Generally, the nodeType
attribute says
what kind of thing the element represents, and the element name indicates what
role it plays relative to the parent. (Reminiscent of SGML architectural forms, perhaps?)
As an aside, the same dichotomy is present in the design of Saxon's SEF file,
which represents a compiled stylesheet, but there we do it the other way around: if an
integer literal is used as the right hand side of an addition, the JavaParser
format expresses this as <right nodeType="IntegerLiteral">
, whereas the SEF
format expresses it as <IntegerLiteral role="right">
. Of course, neither
design is intrinsically better (though the SEF choice works better with XSD validation,
since XSD likes the content model of an element to depend only on the element name,
not the value of one of its attributes). But the choice does mean that most of our
template rules in the transpiler are matching on the nodeType
attribute, not on the
element name, and this perhaps makes the rules a bit more complicated.
Performance hasn't been a concern. I'm pleased to be able to report that of the various phases of processing, the phases written in XSLT are an order of magnitude faster than the phase written in Java; which means that there's no point worrying about speeding the XSLT up. This is despite the fact that (as the above example demonstrates) the XML representation of the code is about 10 times the size of the Java representation.
(Actually, the Java code is 29Mb, the XML is 120Mb, and the generated C# is 18Mb. The C# is smaller than the Java mainly because we drop all comments, and also because the Java total includes modules we don't (yet) convert, for example a lot of code dealing with SAX parsers, localisation, and optional extras such as the XQJ API and SQL extension functions).
But I would like to think that one reason performance hasn't been a concern is that the code was sensibly written. We've got about 200 template rules here, most of them with quite complicated match patterns, and we wouldn't want to be evaluating every match pattern for every element that's processed. In fact, a lot of the time we're doing three levels of matching:
If we find that we're processing a method call (which is rather common),
we have a single template rule in the top-level mode that matches
*[@nodeType='MethodCallExpr']
.
This template rule then does <xsl:apply-templates select="." mode="MethodCall"/>
,
which searches for a more specific template rule, but only needs to search the set of
rules for handling method calls, because they are all in this mode.
To make the code manageable and maintainable, we put all the template rules for a mode
in the same module, and use the XSLT 3.0 construct default-mode="M"
to reduce
the risk of accidentally omitting a mode
attribute on a template rule
or xsl:apply-templates
instruction.
Most of the template rules for method calls are structured as one rule per target class; as described earlier, this uses a microsyntax for defining the formatting of each possible method, using XSLT maps.
So it's not a flat set of hundreds of rules; we've used modes (and the microsyntax) to create a hierarchic decision tree. This both improves performance, and keeps the rules simpler and more manageable. It also makes debugging considerably easier: as with any XSLT stylesheet, working out which rules are firing to handle each input element can be difficult, but the splitting of rules into modes certainly helps.
(A little known Saxon trick here is the saxon:trace
attribute on xsl:mode
, which allows
tracing of template rule selection on a per-mode basis).