PureXML/Sources/XPath/DocumentOrder.swift
Mihaela Mihaljevic 3e4f596b7c
perf(xpath): order results by a per-root index, linear not quadratic in depth (#347)
Sorting a node-set into document order keyed each node by its root path, an
array as long as the node's depth, computed with a per-node ancestor walk.
Ordering the descendants of a deeply-nested document was therefore
O(n log n x depth): `//a` over an 8000-deep document took ~19s through the
reference query path.

The key is now a single O(1) pre-order index from a per-root numbering,
built once per document and shared through the evaluation's navigation
cache, with a band ordering an element before its namespace nodes, its
attribute nodes, and its children. A node-set spanning several documents
(an XSLT document() result) is grouped by root, ranked by first appearance,
so only a foreign node walks its ancestors. `appendSubtree` (the numbering
walk) is now iterative, so it no longer recurses on depth either.

Document order is unchanged: 1786 tests pass, including the XPath, XSLT,
XPointer, and Schematron conformance corpora. `Query.nodes` over a
50000-deep document now orders in a fraction of a second.
2026-06-26 18:22:20 +02:00

98 lines
5.0 KiB
Swift

extension PureXML.XPath.AxisNavigation {
/// A document-order sort key. Comparing two nodes is O(1): the document each
/// belongs to (ranked by first appearance), the pre-order index of the node's
/// owning tree node, a band placing an element before its namespace nodes
/// before its attribute nodes before its children, and a tie-breaking
/// sub-index. This replaces the former per-node root path, an array as long as
/// the node's depth, which made ordering a deep document quadratic.
private struct OrderKey: Comparable {
var rootRank = 0
var index = 0
var band = 0
var sub = 0
static func < (lhs: OrderKey, rhs: OrderKey) -> Bool {
if lhs.rootRank != rhs.rootRank { return lhs.rootRank < rhs.rootRank }
if lhs.index != rhs.index { return lhs.index < rhs.index }
if lhs.band != rhs.band { return lhs.band < rhs.band }
return lhs.sub < rhs.sub
}
}
/// Sorts `nodes` into XPath document order using a per-root pre-order index
/// (built once and cached) rather than a per-node root path, so the sort is
/// O(n log n + document) instead of O(n log n x depth).
///
/// A node-set spanning several documents (an XSLT `document()` result) is
/// grouped by root, ranked by first appearance; only a node outside the first
/// node's document pays an O(depth) root walk, so the common single-document
/// case never walks a node's ancestors.
static func sortByDocumentOrder(_ nodes: [PureXML.XPath.Node], cache: PureXML.XPath.DocumentNavigationCache?) -> [PureXML.XPath.Node] {
guard nodes.count > 1, let primaryRoot = rootTree(of: nodes[0]) else { return nodes }
return decorate(nodes, primaryRoot: primaryRoot, cache: cache)
.sorted { $0.key < $1.key }
.map(\.node)
}
/// The single node first in document order, by the same keying as
/// ``sortByDocumentOrder(_:cache:)`` but in O(n) rather than a full sort.
static func firstByDocumentOrder(_ nodes: [PureXML.XPath.Node], cache: PureXML.XPath.DocumentNavigationCache?) -> PureXML.XPath.Node? {
guard nodes.count > 1, let primaryRoot = rootTree(of: nodes[0]) else { return nodes.first }
return decorate(nodes, primaryRoot: primaryRoot, cache: cache)
.min { $0.key < $1.key }?
.node
}
/// Pairs each node with its document-order key. The primary document's index
/// is built once; a foreign node falls back to its own document's index,
/// ranked after the primary by first appearance.
private static func decorate(
_ nodes: [PureXML.XPath.Node],
primaryRoot: PureXML.Model.TreeNode,
cache: PureXML.XPath.DocumentNavigationCache?,
) -> [(node: PureXML.XPath.Node, key: OrderKey)] {
let primaryID = ObjectIdentifier(primaryRoot)
var indexByRoot: [ObjectIdentifier: [PureXML.XPath.Node: Int]] = [primaryID: orderedDocument(rootedAt: primaryRoot, cache: cache).index]
var rootRank: [ObjectIdentifier: Int] = [primaryID: 0]
return nodes.map { node in
if var key = compactKey(node, in: indexByRoot[primaryID] ?? [:]) {
key.rootRank = 0
return (node, key)
}
guard let root = rootTree(of: node) else { return (node, OrderKey(rootRank: .max)) }
let rootID = ObjectIdentifier(root)
let rank: Int
if let existing = rootRank[rootID] {
rank = existing
} else {
rank = rootRank.count
rootRank[rootID] = rank
indexByRoot[rootID] = orderedDocument(rootedAt: root, cache: cache).index
}
var key = compactKey(node, in: indexByRoot[rootID] ?? [:]) ?? OrderKey()
key.rootRank = rank
return (node, key)
}
}
/// `node`'s order key within `index` (a pre-order numbering of one document's
/// tree nodes), or nil when `node` belongs to a different document. The band
/// orders an element before its namespace nodes before its attribute nodes
/// before its children (whose own index is greater), mirroring the bands of
/// the former path-key form.
private static func compactKey(_ node: PureXML.XPath.Node, in index: [PureXML.XPath.Node: Int]) -> OrderKey? {
switch node {
case .tree:
guard let position = index[node] else { return nil }
return OrderKey(index: position, band: 0)
case let .attribute(owner, attribute):
guard let position = index[.tree(owner)] else { return nil }
let attributeIndex = owner.attributes.firstIndex { $0.name == attribute.name } ?? 0
return OrderKey(index: position, band: 2, sub: attributeIndex)
case let .namespace(owner, prefix, _):
guard let position = index[.tree(owner)] else { return nil }
return OrderKey(index: position, band: 1, sub: prefix.hashValue)
}
}
}