PureXML/Sources/XPath/Axis.swift
Mihaela Mihaljevic 5ad3cf10fc
perf(xpath): skip the document-order sort for an already-ordered single step (#283)
A path that is a single forward-axis step from one context node returns its
nodes already in document order with no duplicates, so the document-order
sort is redundant. AxisNavigation produces the structural forward axes
(child, descendant, descendant-or-self, self, following, following-sibling)
in document order, deriving sibling order from the same children array the
order key compares, so the two can never disagree. This is the common shape
after //x compiles and fuses to a single descendant::x step.

The attribute axis (whose order interleaves with namespace declarations), the
namespace axis (produced in dictionary order), and every reverse axis
(nearest-first) are excluded and still sorted. Measured xpath ~7% faster on
the 20k-item generated corpus via a same-state A/B, identical results.
XPathSingleStepOrderTests pins document order for each included axis, that the
reverse axes still normalize, and that multi-step paths still sort. An
adversarial order-correctness review found no query whose order or uniqueness
the change breaks.
2026-06-25 05:04:00 +02:00

79 lines
3.1 KiB
Swift

extension PureXML.XPath {
/// The thirteen XPath 1.0 axes. Every axis is reachable now that the tree is
/// parent-aware, so upward and sibling navigation are first-class.
enum Axis: Equatable {
case child
case descendant
case parent
case ancestor
case followingSibling
case precedingSibling
case following
case preceding
case attribute
case namespace
case selfAxis
case descendantOrSelf
case ancestorOrSelf
/// Whether the axis is a reverse axis (its nodes are numbered in reverse
/// document order for positional predicates).
var isReverse: Bool {
switch self {
case .parent, .ancestor, .precedingSibling, .preceding, .ancestorOrSelf:
true
default:
false
}
}
/// Whether the axis, taken from a SINGLE context node, yields its nodes
/// already in document order with no duplicates, so a one-step path over
/// it needs neither the de-duplication nor the document-order sort. True
/// for the structural forward axes, which `AxisNavigation` produces in
/// document order (child by child-array order, descendant by pre-order,
/// the following families by document position). The attribute axis is
/// excluded because its order interleaves with namespace declarations,
/// and the namespace axis because its nodes are produced in dictionary
/// (hash) order; the sort normalizes both, so they are left to it.
var preservesDocumentOrderFromSingleContext: Bool {
switch self {
case .child, .descendant, .descendantOrSelf, .selfAxis, .followingSibling, .following: true
default: false
}
}
/// Whether the axis yields disjoint node-sets from distinct context nodes,
/// so accumulating its results across a step's context nodes can never
/// produce a duplicate and needs no cross-context de-duplication. A child,
/// attribute, or namespace node belongs to exactly one element, and the
/// self axis maps each distinct context to itself; every other axis
/// (descendant, ancestor, the siblings, following/preceding, parent) can
/// reach one node from two different contexts.
var yieldsDisjointResults: Bool {
switch self {
case .child, .attribute, .namespace, .selfAxis: true
default: false
}
}
/// The principal node kind of the axis: attributes for the attribute axis,
/// namespaces for the namespace axis, elements otherwise. Governs what a
/// name test or `*` selects.
var principalKind: PrincipalKind {
switch self {
case .attribute: .attribute
case .namespace: .namespace
default: .element
}
}
}
/// The principal node kind selected by name tests on an axis.
enum PrincipalKind: Equatable {
case element
case attribute
case namespace
}
}