How Does xsl:accumulator Maintain State in Streamed XSLT?
XSLT 3.0 streaming enables processors to transform massive XML
documents without loading the entire tree into memory, but this pipeline
eliminates random access to past or future nodes. To perform operations
that require memory of previously processed nodes—such as running
totals, position counters, or context-aware formatting—XSLT introduces
<xsl:accumulator>. This mechanism binds declarative,
rule-based state transitions directly to the streaming parser's
traversal events, maintaining an updated internal value without
violating single-pass constraints.
The Streaming Challenge and State
Traditional XSLT relies on an in-memory document tree, allowing full
access to axes like preceding-sibling::* or
ancestor::*. In a streaming scenario, the processor reads
XML as a sequential token stream (start tags, content, end tags) and
discards nodes immediately after processing to maintain a flat memory
footprint. Because backward navigation is impossible, standard XPath
expressions cannot inspect previously consumed elements.
Accumulators solve this by decoupling state computation from node matching. Instead of querying historical nodes through XPath axes, an accumulator computes a running value as nodes stream through the processor.
Architecture of
<xsl:accumulator>
An accumulator is declared at the stylesheet package level using
<xsl:accumulator>. It defines a typed variable, an
initial value, and whether evaluation occurs before or after element
children are read.
<xsl:accumulator name="item-counter"
as="xs:integer"
initial-value="0"
streamable="yes">
<xsl:accumulator-rule match="item"
select="$value + 1"
phase="start"/>
</xsl:accumulator>The core attributes and structures include:
name: The unique QName used to access the accumulator's value via theaccumulator-before()andaccumulator-after()functions.initial-value: The baseline value assigned to the accumulator before document processing begins.streamable="yes": Designates that the accumulator rules must adhere to streamability guarantees, ensuring expressions do not attempt non-streamable operations.<xsl:accumulator-rule>: Specifies pattern matches and computation logic. Inside each rule, the implicit variable$valuerepresents the state immediately prior to the event.
Pre-Descent vs. Post-Descent Phases
A streaming parser encounters an element twice: when opening the tag and when closing it. Accumulator rules hook into these two distinct phases:
phase="start"(Pre-descent): Evaluated when the parser encounters the opening tag of a matching node, prior to processing any child nodes or text content. The resulting state is queried usingaccumulator-before().phase="end"(Post-descent): Evaluated when the parser reaches the closing tag, after all descendants have been streamed and discarded. The updated state is queried usingaccumulator-after().
This two-phase design allows accumulators to manage hierarchical context, such as tracking ancestor nesting levels or computing aggregations over a subtree without buffering child elements.
Evaluating Rules During Traversal
State maintenance is strictly deterministic and follows depth-first, document-order traversal:
- When processing begins, the accumulator initializes to its
initial-value. - Upon encountering an element node opening, the processor checks for
matching
phase="start"rules. If a match occurs, theselectexpression evaluates with$valuebound to the current state, and the accumulator updates. - As the processor traverses the node's children, the updated value serves as the baseline for nested evaluations.
- Upon reaching the element closing tag, matching
phase="end"rules evaluate against the latest state. - If no rule matches an event, the accumulator preserves its existing value untouched.
Because accumulators calculate values in step with parsing, functions
like accumulator-before('item-counter') execute in constant
\(O(1)\) time, retrieving the exact
state corresponding to the current point in the stream.
Ensuring Streamability
To prevent memory leaks during streaming, expressions inside
<xsl:accumulator-rule> must satisfy strict W3C
streamability rules:
- Motionless Expressions: Rules cannot consume
streamable child nodes within their
selectattribute. They can inspect element attributes or evaluate simple atomic values, but they cannot invoke downstream child selections. - Deterministic Side-Effect Isolation: Accumulators remain purely functional. They do not alter the input stream or modify global variables; they merely create a sequentially updated property associated with the document's streaming progress.
By tethering value updates directly to parser events,
<xsl:accumulator> allows complex state tracking,
running metrics, and contextual styling across multi-gigabyte XML
datasets while maintaining strict single-pass efficiency.