Variables never change, so a running total once needed a template calling itself; processors turn such tail calls into loops, but other recursion overflows the stack on long input. xsl:iterate writes the loop directly: xsl:param holds state, xsl:next-iteration passes it on, xsl:break stops early, and xsl:on-completion runs at the end. It is streamable (XSLT Analysis and Streaming). Run xslt3 -s:booknest-catalog.xml -xsl:iterate.xsl:
<xsl:stylesheet version="3.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema" expand-text="yes">
<xsl:output method="text"/>
<xsl:param name="budget" as="xs:decimal" select="70"/>
<xsl:template match="/catalog">
<xsl:iterate select="book">
<xsl:param name="total" as="xs:decimal" select="0"/>
<xsl:on-completion>All books fit: {$total} </xsl:on-completion>
<xsl:variable name="new" select="$total + xs:decimal(.//price)"/>
<xsl:choose>
<xsl:when test="$new gt $budget">
<xsl:break>Stop at {@id}: {$total} </xsl:break></xsl:when>
<xsl:otherwise>{@id} +{.//price} = {$new} <xsl:next-iteration>
<xsl:with-param name="total" select="$new"/></xsl:next-iteration></xsl:otherwise>
</xsl:choose>
</xsl:iterate>
</xsl:template>
</xsl:stylesheet>Output
b1 +14.99 = 14.99 b2 +39.50 = 54.49 Stop at b3: 54.49
With budget=200 the loop ends in xsl:on-completion: All books fit: 134.74. It has no context item: a draft that counted orders inside it failed with XPDY0002.