Link to home
Start Free TrialLog in
Avatar of deleyd
deleydFlag for United States of America

asked on

Convert Recursive XSLT to avoid stack overflow

(EE doesn't seem to have an XSLT topic. Hope XML is close enough.)

I have an input which looks something like this:
<?xml version="1.0" encoding="UTF-8"?>
  <E count="17">
    <D pos="1"></D>
    <D pos="2"></D>
    <D pos="3"></D>
    <D pos="5"></D>
    <B nsize="5"></B>
    <D pos="11"></D>
    <B nsize="2"></B>
    <B nsize="3"></B>
</E>

Open in new window

The idea is the E element has a total count which is the total number of slots it has. Each element has a position. D elements take up one slot. In this example, we have D elements for positions 1,2,3,5. Note position 4 is missing.

Then we also have B elements which take up several slots. The size of the B element is given (how many slots it takes up).

The output from the XSLT transformation covers all the slots and tells us what is in each slot. It looks something like this:
1 D 
2 D 
3 D 
4 empty 
5 D 
6 - 10 B 
11 D 
12 - 13 B 
14 - 16 B 
17 empty 

Open in new window

This shows:
slot 1 has a D
slot 2 has a D
slot 3 has a D
slot 4 is empty
slot 5 has a D
slots 6-10 are occupied by a B
slot 11 has a D
slots 12-13 are occupied by a B
slots 14-16 are occupied by a B
slot 17 is empty.

I have a recursive XSLT transformation that can handle this for small number of slots. It just keeps calling itself recursively until done. Only problem is for large number of slots, the call stack eventually overflows with recursive calls.

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0"
                xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
>

  <xsl:template match="/E">
    <xsl:text>
</xsl:text>
    <xsl:apply-templates select="." mode="recursive_function">
      <xsl:with-param name="max" select="@count"></xsl:with-param>
    </xsl:apply-templates>
  </xsl:template>

  <xsl:template match="E" mode="recursive_function">
    <xsl:param name="num" select="1"></xsl:param>
    <xsl:param name="max"></xsl:param>
    <xsl:param name="preceding_sibling"></xsl:param>

    <!--my current node is: <xsl:copy-of select="self::node()" ></xsl:copy-of>
    num is: <xsl:value-of select="$num" ></xsl:value-of>
    max is: <xsl:value-of select="$max" ></xsl:value-of>
    <xsl:text xml:space="preserve">& #10;</xsl:text>-->
    
  <xsl:choose>

      <!-- Case: D -->
      <xsl:when test="D[@pos=$num]">
        <!-- Output -->
        <xsl:value-of select="$num"></xsl:value-of>
        <xsl:text> D & #10;</xsl:text>
        <!-- Recurse -->
        <xsl:if test="$num < $max">
          <xsl:apply-templates select="." mode="recursive_function">
            <xsl:with-param name="num" select="$num+1"></xsl:with-param>
            <xsl:with-param name="max" select="$max"></xsl:with-param>
            <xsl:with-param name="preceding_sibling" select="D[@pos=$num]" ></xsl:with-param>
          </xsl:apply-templates>
        </xsl:if>
      </xsl:when>

      <!-- Case: B -->
      <xsl:when test="B[($num=1) or (not($num=1) and ($preceding_sibling and (generate-id(preceding-sibling::*[1]) = generate-id($preceding_sibling))))]">
        <xsl:variable name="node" select="B[($preceding_sibling and (generate-id(preceding-sibling::*[1]) = generate-id($preceding_sibling)))]"></xsl:variable>
        <xsl:variable name="nsize" select="$node/@nsize"></xsl:variable>
        <xsl:value-of select="$num"></xsl:value-of>
        <!-- Output -->
        <xsl:text> - </xsl:text>
        <xsl:value-of select="$num + $nsize - 1"></xsl:value-of>
        <xsl:text> B & #10;</xsl:text>        
        <!-- Recurse -->
        <xsl:if test="$num + $nsize <= $max">
          <xsl:apply-templates select="." mode="recursive_function">
            <xsl:with-param name="num" select="$num + $nsize"></xsl:with-param>
            <xsl:with-param name="max" select="$max"></xsl:with-param>
            <xsl:with-param name="preceding_sibling" select="$node" ></xsl:with-param>
          </xsl:apply-templates>
        </xsl:if>
      </xsl:when>

      <!-- Case: Empty -->
      <xsl:otherwise>
        <!-- Output -->
        <xsl:value-of select="$num"></xsl:value-of>
        <xsl:text> empty & #10;</xsl:text>
        <!-- Recurse -->
        <xsl:if test="$num < $max">
          <xsl:apply-templates select="." mode="recursive_function">
            <xsl:with-param name="num" select="$num + 1"></xsl:with-param>
            <xsl:with-param name="max" select="$max"></xsl:with-param>
            <xsl:with-param name="preceding_sibling" select="D[@pos=$num]" ></xsl:with-param>
          </xsl:apply-templates>
        </xsl:if>
      </xsl:otherwise>
    </xsl:choose>

    <xsl:text> Return 
</xsl:text>
  
  </xsl:template>
</xsl:stylesheet>

Open in new window

I'm trying to convert this logic to something that is not recursive so we can avoid stack overflow.

My idea is to iterate over all the nodes, and figure out which slot(s) each node belongs in by examining the previous nodes and doing a bunch of math.

So node <D pos="1"></D> is in slot #1. There are no previous slots.
node <D pos="2"></D> is in slot #2. The previous slot is filled.
node <D pos="3"></D> is in slot #3. The previous slot is filled.
node <D pos="5"></D> is in slot #5. The previous slot is unaccounted for, so I need to output "empty" for it (and possibly "empty" for previous slots until I either come to a slot that's accounted for, or the beginning of the list).

node <B nsize="5"> The previous slot was 5 filled with a D. this B starts in slot 6 and occupies 5 slots (nsize=5).

node <D pos="11"> is in slot 11. The previous slot is occupied by the tail of B, (so there are no empty slots to account for).

node <B nsize="2"> starts in slot 12, because the previous slot was 11 with a D in it. This B occupies slots 12 and 13.

node <B nsize="3"> starts in slot 14 and occupies slots 14,15,16.

There's still an empty slot 17 I need to mention.

This is getting rather complicated.
In general,

if I'm matching a D node, that D node occupies the pos= position it states, and I need to check if there are any empty slots before it I need to print out. Do this by figuring out what slot # the previous node ends in. (This however may lead to a recursive algorithm, which is what I'm trying to avoid!)

if I'm matching a B node, then either there are no previous nodes, this is the first node, in which case it starts in slot #1
OR the previous node is a D, in which case the slot is the next slot after that D,
OR the previous node is a B, in which case I need to figure out what slots that previous B occupies, (avoiding using a recursive algorithm that can overflow the stack, otherwise I've gained nothing.)

(I hope that makes sense.)

So my question is, how can I implement that complex algorithm in XSLT?
Or perhaps there's a better way? I just need to avoid stack overflow due to many recursive calls (overflow seems to happen after 300 - 400 recursive calls build up.)
Avatar of Gertone (Geert Bormans)
Gertone (Geert Bormans)
Flag of Belgium image

There used to be an XSLT topic, but since traffic became lower it is now merged into the XML topic
So you are at the right location :-)

You don't need recursion at all on the nodes. Your nodes are in the order you want to show them
So all you need to do is to walk the sibling axis
(this is called sibling recursion, but it is not recursion and you don't run into stack overflow)

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    >
    
    <xsl:variable name="nl" select="'&#10;'"/>
    <xsl:strip-space elements="*"/>
    <xsl:output method="text"/>
    
    <xsl:template match="E">
        <xsl:apply-templates select="*[1]">
            <xsl:with-param name="max" select="@counts"/>
            <xsl:with-param name="next" select="1"/>
        </xsl:apply-templates>
    </xsl:template>
    
    <xsl:template match="D">
        <xsl:param name="max"/>
        <xsl:param name="next"/>
        <xsl:text>slot </xsl:text>
        <xsl:value-of select="@pos"/>
        <xsl:text> has a D</xsl:text>
        <xsl:value-of select="$nl"/>
        <xsl:apply-templates select="following-sibling::*[1]">
            <xsl:with-param name="max" select="$max"/>
            <xsl:with-param name="next" select="@pos + 1"/>
        </xsl:apply-templates>
    </xsl:template>
    
    <xsl:template match="B">
        <xsl:param name="max"/>
        <xsl:param name="next"/>
        <xsl:text>slot </xsl:text>
        <xsl:value-of select="$next"/>
        <xsl:text>-</xsl:text>
        <xsl:value-of select="$next + @nsize - 1"/>
        <xsl:text> are occupied by a B</xsl:text>
        <xsl:value-of select="$nl"/>
        <xsl:apply-templates select="following-sibling::*[1]">
            <xsl:with-param name="max" select="$max"/>
            <xsl:with-param name="next" select="$next + @nsize"/>
        </xsl:apply-templates>
    </xsl:template>

</xsl:stylesheet>

Open in new window

This is not the solution by the way.
I showed you this to show the mechanism.
Most of your logic is at the level of the element, so you can elegantly move from one element to the next, passing state through

As you can see, this code almost does all you need

All you need now is a recursive named template to fill in the empty from n to m call it
- at the start of template D when @pos is larger than $next (n = $next, m is @pos)
- at the end of both template D and E (well, before the apply-templates when there is no next element node and new $next is smaller than $max (n = new $next, m = $max)

This recursive named template will never overflow your stack if you make it tail recursive, you can test that by setting @count to a ridiculously high number)

This named template would be easy for you to do.
Please post the final solution once you are done (for the archive)
or call for help if you need it on the named template
Just did this now, here is the full working code

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    >
    
    <xsl:variable name="nl" select="'&#10;'"/>
    <xsl:strip-space elements="*"/>
    <xsl:output method="text"/>
    
    <xsl:template match="E">
        <xsl:apply-templates select="*[1]">
             <xsl:with-param name="next" select="1"/>
            <xsl:with-param name="max" select="@count"/>
        </xsl:apply-templates>
    </xsl:template>
    
    <xsl:template match="D">
        <xsl:param name="next"/>
        <xsl:param name="max"/>
        <xsl:call-template name="add-empty">
            <xsl:with-param name="n" select="$next"/>
            <xsl:with-param name="m" select="@pos - 1"/>
        </xsl:call-template>
        <xsl:text>slot </xsl:text>
        <xsl:value-of select="@pos"/>
        <xsl:text> has a D</xsl:text>
        <xsl:value-of select="$nl"/>
        <xsl:variable name="new-next" select="@pos + 1"/>
        <xsl:if test="not(following-sibling::*)">
            <xsl:call-template name="add-empty">
                <xsl:with-param name="n" select="$new-next"/>
                <xsl:with-param name="m" select="$max"/>
            </xsl:call-template>
        </xsl:if>
        <xsl:apply-templates select="following-sibling::*[1]">
            <xsl:with-param name="next" select="$new-next"/>
            <xsl:with-param name="max" select="$max"/>
        </xsl:apply-templates>
    </xsl:template>
    
    <xsl:template match="B">
        <xsl:param name="next"/>
        <xsl:param name="max"/>
        <xsl:variable name="new-next" select="$next + @nsize"/>
        <xsl:text>slot </xsl:text>
        <xsl:value-of select="$next"/>
        <xsl:text>-</xsl:text>
        <xsl:value-of select="$new-next - 1"/>
        <xsl:text> are occupied by a B</xsl:text>
        <xsl:value-of select="$nl"/>
        <xsl:if test="not(following-sibling::*)">
            <xsl:call-template name="add-empty">
                <xsl:with-param name="n" select="$new-next"/>
                <xsl:with-param name="m" select="$max"/>
            </xsl:call-template>
        </xsl:if>
        <xsl:apply-templates select="following-sibling::*[1]">
            <xsl:with-param name="next" select="$new-next"/>
            <xsl:with-param name="max" select="$max"/>
        </xsl:apply-templates>
    </xsl:template>
    
    <xsl:template name="add-empty">
        <xsl:param name="n"/>
        <xsl:param name="m"/>
        <xsl:choose>
            <xsl:when test="$n > $m"></xsl:when>
            <xsl:otherwise>
                <xsl:text>slot </xsl:text>
                <xsl:value-of select="$n"/>
                <xsl:text> is empty</xsl:text>
                <xsl:value-of select="$nl"/>
                <xsl:call-template name="add-empty">
                    <xsl:with-param name="n" select="$n + 1"/>
                    <xsl:with-param name="m" select="$m"/>
                </xsl:call-template>
            </xsl:otherwise>
        </xsl:choose>
    </xsl:template>

</xsl:stylesheet>

Open in new window

ASKER CERTIFIED SOLUTION
Avatar of Gertone (Geert Bormans)
Gertone (Geert Bormans)
Flag of Belgium image

Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
There are a few approaches to fixing this.

1) Reduce your recursion, by popping out before you overflow your stack.

2) Use XML Parsing which is impervious to stack overflows as with a PERL parser. The downside here is you might exhaust all your machine memory, which will initiate swapping, then run the OOM (Out Of Memory) Killer.

3) Increase the amount of stack available for your shell process, by setting the stack limit (however you do this, for whatever shell you're using, as each can be slightly different) to unlimited. You may also require resetting your system's hard limit for stack size, then doing a reboot for new unlimited stack value to take effect.

4) Be very careful with #3, as if you do this incorrectly (for all users + all processes), you'll create a highly unstable runtime environment.

Best to limit this to one user + one process.

5) So the summary is you'll change your data set to reduce recursion or pop out of recursion earlier or allow recursion to use all available machine memory.

In other words, no magic bullet. How you proceed is highly dependent on outcome you require.
@David, I guess you missed the point that this is an XSLT question...
1) pop out before.... hmm, XSLT and iterators maybe, not in XSLT1
2) it is not the XML parsing that causes the stack overflow, but the XSLT processing. XML depth rarely is the source of stack overflow.
And always use XML parsing on working on XML, that is rule number one
3) yes, as a final measure AND when you know what your stylesheet is doing (as my final suggestion indicated)
4) yes as in 3
5) summary: disagree with change the dataset, sounds a bit like if you can't handle the requirement, change the requirment

0) read the question, analyse the stylesheet and fix the stylesheet to remove the recursion... as I did 5 hours ago ;-)
Avatar of deleyd

ASKER

Thank you very much!
I was afraid no one would answer, and here it is answered the very next day!
Avatar of deleyd

ASKER

I love the simplifying, removing the "choose" statement.
However, (oh you know there's a catch whenever a sentence starts with 'However',...)
it's still recursive. I still get a stack overflow.
The call to "apply-templates"... (if only we could 'jump' instead of call?)

I get a stack overflow with this input file:
<?xml version="1.0" encoding="UTF-8"?>
<E count="10000">
    <D pos="1"/>
</E>

Open in new window

That's actually not problematic, as it overflows around 1,300 and I don't expect to ever have that many empty slots.

I also get a stack overflow if I input 1000 "D" entries, as in the attached "test2.xml" file. (That is problematic.)

Here's my test XSML:
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl"
>
    <xsl:variable name="nl" select="'&#10;'"/>
    <xsl:strip-space elements="*"/>
    <xsl:output method="text"/>
  
  <!-- 
  E encloses our set of elements
  select="following-sibling::*[1]" gives us the next node in the list
  We call apply-templates passing (@pos + 1) or ($next + @nsize), asking it to process the next node in our E list (select="following-sibling::*[1]")

If the next node is a "D" node,
      1. Output any empty slots not accounted for prior to this D slot, which is at position @pos
      2. Output this D slot
      3. Check if we are at the end of the list, and if we are, output any remaining empty slots not accounted for (from our current position+1 to max)
      
If the next node is a "B" node,
      1. new-next is the next slot after this B node
      2. Output this B node and the slots it covers
      3. Perform the same "check if we are at the end of the list, and if we are, output any remaining empty slots"
  -->

    <xsl:template match="E"><!-- E encloses our set of emelents -->
      <xsl:apply-templates select="*[1]"><!-- apply template to the first child element -->
        <xsl:with-param name="next" select="1"/><!-- our slot number. We start at slot #1. -->
        <xsl:with-param name="max" select="@count"/><!-- our total number of slots -->
      </xsl:apply-templates>
    </xsl:template>

  
    <xsl:template match="D">
      <xsl:param name="next"/><!-- our slot number -->
      <xsl:param name="max"/><!-- our total number of slots -->
      
      <xsl:call-template name="add-empty"><!-- first check for any empty slots before this slot. Output for slots n to m, if n <= m -->
        <xsl:with-param name="n" select="$next"/><!-- our slot number -->
        <xsl:with-param name="m" select="@pos - 1"/><!-- previous slot number. D gives us our current slot # as @pos -->
      </xsl:call-template>
      
      <xsl:text>slot </xsl:text>
      <xsl:value-of select="@pos"/>
      <xsl:text> has a D</xsl:text>
      <xsl:value-of select="$nl"/><!-- new line -->
      
      <xsl:variable name="new-next" select="@pos + 1"/><!-- the next slot number -->
      <xsl:call-template name="ProcessNextElementOrEndOfList"><!-- If we are we at the end of the list, fill in next slot if it is empty. (Only adds if we are below or at max) -->
        <xsl:with-param name="new-next" select="$new-next" />
        <xsl:with-param name="max" select="$max" />
      </xsl:call-template>
    </xsl:template>

  
    <xsl:template match="B">
      <xsl:param name="next"/>
      <xsl:param name="max"/>
      
      <xsl:variable name="new-next" select="$next + @nsize"/>
      
      <xsl:text>slot </xsl:text>
      <xsl:value-of select="$next"/>
      <xsl:text>-</xsl:text>
      <xsl:value-of select="$new-next - 1"/>
      <xsl:text> are occupied by a B</xsl:text>
      <xsl:value-of select="$nl"/>

      <xsl:call-template name="ProcessNextElementOrEndOfList">
        <xsl:with-param name="new-next" select="$new-next" />
        <xsl:with-param name="max" select="$max" />
      </xsl:call-template>
    </xsl:template>

  
  <!-- Fill in empty slots for slots n to m -->
    <xsl:template name="add-empty">
      <xsl:param name="n"/>
      <xsl:param name="m"/>
      <xsl:choose>
        <xsl:when test="$n > $m"></xsl:when>
        <xsl:otherwise>
          <xsl:text>slot </xsl:text>
          <xsl:value-of select="$n"/>
          <xsl:text> is empty</xsl:text>
          <xsl:value-of select="$nl"/>
          <xsl:call-template name="add-empty">
            <xsl:with-param name="n" select="$n + 1"/>
            <xsl:with-param name="m" select="$m"/>
          </xsl:call-template>
        </xsl:otherwise>
      </xsl:choose>
      <xsl:text> BlankReturn </xsl:text>
      <xsl:value-of select="$n"/>
      <xsl:value-of select="$nl"/>
    </xsl:template>


  <!-- 
  Check if we are we at the end of the list, and if we are, output any remaining empty slots. (Only adds if we are below or at max) 
  Then process the next element (which there won't be any, if we are at the end of the list)
  -->
  <xsl:template name="ProcessNextElementOrEndOfList">    
    <xsl:param name="new-next"/>
    <xsl:param name="max"/>
    <!-- Check for end of list, and output any remaining empty slots -->
    <xsl:if test="not(following-sibling::*)">
        <xsl:call-template name="add-empty">
          <xsl:with-param name="n" select="$new-next"/>
          <xsl:with-param name="m" select="$max"/>
        </xsl:call-template>
      </xsl:if>
    <!-- Process next element. (There won't be a next element if we are at the end of the list) -->
      <xsl:apply-templates select="following-sibling::*[1]">
        <xsl:with-param name="next" select="$new-next"/>
        <xsl:with-param name="max" select="$max"/>
      </xsl:apply-templates>
    <xsl:text> Return </xsl:text>
    <xsl:value-of select="$new-next"/>
  </xsl:template>

</xsl:stylesheet>

Open in new window

I'm thinking I might be able to overcome this problem using the "Batching" technique I XSLT Cookbook by Sal Mangano. The idea is to process the nodes in chunks of say 100, and recursively call the chunker until all sets of 100 are complete.

(Allow me to write out my thoughts here as that will help me solve this.)

The 'chunker' strips off the first 100 nodes, using select="$nodes[position() &lt; 100]"
and sends that to a recursive processor,
then it recursively calls itself passing the remaining nodes via select="$nodes[position() >= 100]"

This gives us two nested recursive loops, the outer loop processing chunks of 100, the inner loop processing 100 recursively.

This can handle 100x100=10000 and just before it finishes, the inner loop will have 100 calls on the stack, and the outer loop will have 100 calls on the stack, totaling 200 calls, instead of 10000 calls if it was all done recursively with one loop.

The only trick is, finding a way to pass the "next" count from the inner loop to the outer loop when the inner loop completes. (Sorry if that sounds confusing.)

The book does it by having the outer loop capture the output from the inner loop into a variable. My idea (which I haven't tested yet) is append the data I want to pass from inner loop to outer loop to the output, and have it stripped off by the receiver.

<xsl:template name="outerloop">
  <xsl:variable name="partial-output">
    <xsl:call-template name="process100nodes">
      <xsl:with-param name="nodes" select="$nodes[position() &lt; 100]"/>
      <xsl:with-param name="next" select="$next"/><!-- need to initialize $next to 1 somewhere above... -->
    </…>
  </…>

  <!-- extract $next-so-far from variable "partial-output" -->

  <xsl:call-template name="outerloop">
    <xsl:with-param name="nodes" select="$nodes[position() >= 100]">
    <xsl:with-param name="next" select="$next-so-far"/>
  </…>
</xsl:template>

Open in new window

That's the general idea at least, where "process100nodes" is our inner loop, which may also be recursive.
test2.xml
Of course, me not so smart ;-)

Sibling recursion is called sibling recursion because it is not necessarily recursing, but is stacking the templates
You have the nodes in document order, so I would not go into the complex nested recursion in Sal's book
You can simply apply-templates all the nodes and handle them in situ

One thing you can think of is doing an apply-templates for all D elements. Because you know for each one of them where they need to be in the result. And build your sibling recursion from there. Divide and conquer, but not blindly as in Sal's book, but using tyhe context of your task.

I need to think about this, will not happen this morning
So, some questions
- an empty can only appear right before a D?
- the number of emptys before a D can be calculated from the difference in pos with the previous D and the width of all the B between two D?

Then breaking the problem apart in groups starting with a D would be an interesting thing to do

Maybe you want to do this for the exercise of getting it done
If this is serious production work, I would consider the use of XSLT2 (so use group adjacent to cut the pieces and a for loop $i in 1 to $size for the empties) or even XSLT3 iterator then you stack overflow is gone right away
If you are bound to XSLT1, you might consider the use of a two step transform

The thing is, if you would go with Sals solution, you get tricky code and keeping it simple is a good help for future maintenance
On this assignment I would not go for a one step XSLT1 solution (not over 10 years since XSLT2 became a recommendation)
Just to show you one thing...

    <xsl:template name="add-empty" xmlns:xs="http://www.w3.org/2001/XMLSchema">
        <xsl:param name="n"/>
        <xsl:param name="m"/>
        <xsl:for-each select="for $i in xs:integer($n) to xs:integer($m) return $i">
            <xsl:text>slot </xsl:text>
            <xsl:value-of select="."/>
            <xsl:text> is empty</xsl:text>
            <xsl:value-of select="$nl"/>
        </xsl:for-each>
    </xsl:template>
    

Open in new window


Would be the XSLT2.0 variant of the create empty function
Avatar of deleyd

ASKER

And yes I discovered there's no way to specify an empty slot before a <B>. It just uses whatever the next slot position the parser is at when it reaches the <B>.
Avatar of deleyd

ASKER

(Hmm did my previous comment not show up? I'll repeat it here.)

I'd like to use XSLT version 2 or 3 but all we have is Visual Studio.

I don't know why Microsoft decided not to upgrade to versions 2 or 3. I recall they gave a rationale, which I personally translated as "We're too lazy and we're Microsoft so we don't have to."
Can be short about Microsoft
Many vendors did an XSLT1 processor, that was not too hard to build, but nobody made too much money out of that effort
The amount of work to go from a XSLT1 processor to an XSLT2 processor (from a non typed system to a typed system among other things) was estimated for at 1 to 3 person-years of effort for someone capable and knowledgeable about the specification (I did not invent this, Michael Kay, the owner of the company that did Saxon told me this)
So Microsoft found out that they never would do an XSLT2 processor to make no money at all from it and invest in LinQ instead and a bit in XQuery for SQL Server (and were very open about that really early)

Having said that, saxon has a dot net variant which I have used in fairly critical environments over the past 2-3 years
https://www.saxonica.com/html/documentation/dotnet/dotnetapi.html
You can download the HE variant (free of charge) and if you download the samples, you can work from the well documented C# samples
I can only suggest that you give that some attention. It would make your entire system a lot more robust

About the missing comment... you did not post that as a comment but as a testimonial
Although I want to thank you for the testimonial, I am not sure whether I want to keep your comments about lazy MS as a testimonial on my profile ;-)