Next: , Up: Expansion Sequence   [Contents][Index]


A.1.1.1 Predicating Expansion

To start, let’s consider possible nodes. We do not want to make any assumptions of what may or may not be expandable, so we will handle them all consistently. The first question is whether the node is even expandable.

Our perspective is abstraction—we do not know (nor should we) what the caller may consider to be expandable, or how it may be expanded.

To solve this problem, we introduce an abstract predicate that must be overridden by an implementation to provide anything of value. Otherwise, all we can do is assume that the node we have encountered cannot be expanded (what would we even do to expand it?).

function: xs:boolean eseq:is-expandable (node as node())

xmlns:eseq="http://www.lovullo.com/tame/preproc/expand/eseq"

An implementation must override this function.

This predicate determines exclusively whether a node should be expanded or hoisted. Therefore, it should account for both nodes that cannot be expanded (for example, text nodes may not be expanded), and nodes that have already been expanded that can undergo no further expansion.

The default implementation therefore will always yield false, meaning that no processing will take place.

Definition:

<function name="eseq:is-expandable"  as="xs:boolean">
  <param name="node"  as="node()" />

  <sequence select="false()" />
</function>

With that question answered, we are now able to proceed:

  1. If a node is not expandable, we can immediately hoist it (see Node Hoisting);
  2. Otherwise, we must allow the node to attempt to expand.

The former allows us to save re-passes (and therefore improve performance), as well as the headache of handling every possible case.5 The latter has an important consideration.

Let’s start with the first case.

match: _eseq:expand on *[ node()[1][ not( eseq:is-expandable( . ) ) ] ]

When we encounter a head that is not expandable, it will be immediately hoisted, as there is no work to be done.

The result of this operation will be a sequence of nodes, one of them being the hoisted node, and the last being the remaining expansion sequence. See Node Hoisting.

<template mode="_eseq:expand"  as="node()+"  match="*[ node()[1][ not( eseq:is-expandable( . ) ) ] ]">
  <sequence select="_eseq:hoist( . )" />
</template>

As we only have an abstract view of the concept of “expansion”, we need a way for an implementation to notify us whether a node needs further expansion, or is ready to be hoisted. Fortunately, we already have that information because of how we defined eseq:is-expandable#1. In other words, our previously declared processing will already take care of hoisting for us when necessary, so we need only continue to expand nodes as necessary.

match: _eseq:expand on *[ node()[1][ eseq:is-expandable( . ) ] ]

We must continue to expand expandable nodes; otherwise, nested expansions would never take place.

Once expansion is complete, by the definition of eseq:is-expandable#1, expansion will halt.

<template mode="_eseq:expand"  as="element()"  match="*[ node()[1][ eseq:is-expandable( . ) ] ]">
  <sequence select="_eseq:expand-head( . )" />
</template>

Up until this point, we have been assuming that there is an actual head node to process; this may not be the case. In fact, we are guaranteed to encounter and empty expansion sequence at some point, because all nodes will have been hoisted (see Node Hoisting)!

In this instance, we will consider our job to have been completed, and self-destruct.

match: _eseq:expand on *[ not( node() ) ]

Once there is no head node, expansion is complete and the sequence parent is no longer necessary.

<template mode="_eseq:expand"  match="*[ not( node() ) ]" />

The above matches satisfy all possible conditions.

PROOF: Let the expansion sequence element be the context node. As it is an element, it is matched by *, which is the context of each match. The expansion sequence either has a head node, or it does not have a head node. If it does have a head node, then it can be defined as node()[1]; otherwise node() yields an empty sequence, and the final template is matched. When the head node is available, it is either expandable or non-expandable, determined by the predicate eseq:is-expandable#1. Since the predicate returns a boolean, it must be either false() or true(), and so it must satisfy either the first or second template respectively. ∎

We have therefore determined hoisting/expansion actions through use of a single predicate.

An astute reader may have come to an uncomfortable realization: after all expansions are complete and the expansion sequence node itself is eliminated (per the final match above), then the node that was last expanded and hoisted will be considered to be the expansion sequence by eseq:expand-step#1. This is true, but should not be a problem in practice: hoisting is intended to place nodes into context for the caller; it is expected that the caller will recognize when to invoke sequence expansion (likely on a pre-defined node type, which would no longer match after it is eliminated). The discomfort comes from the fact that we cannot use this implementation recursively; this is a consequence of the current preprocessor implementation, and is subject to change in the future.


Footnotes

(5)

Well, we deferred that complexity to the caller via our eseq:is-expandable predicate.


Next: , Up: Expansion Sequence   [Contents][Index]