perf(xpath): compute document-order keys once per sort
Node-set ordering previously recomputed both nodes' root paths on every precedes comparison (treePath is O(depth x siblings), so sorts were O(n log n x depth)). Add decorate-sort-undecorate helpers, sortedByDocumentOrder() and firstInDocumentOrder(), that compute each node's order key once, and switch the evaluator's order-and-dedup, the EXSLT set functions, string-value coercion, and NodeFunctions' target lookup to them. set:leading/trailing also decorate once, reusing the keys for the pivot filter and the final sort. precedes stays for pairwise use, sharing the key comparator (Node.ordered). Behavior unchanged, all suites green. Closes #113
This commit is contained in:
parent
0edc1f77f9
commit
b8dbf8e97a
@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Changed
|
||||
|
||||
- Document-order sorts no longer recompute root paths per comparison (#113): node-set ordering (the evaluator's order-and-dedup, EXSLT's set functions, string-value coercion's first-node lookup) goes through decorate-sort-undecorate helpers (`sortedByDocumentOrder()`, `firstInDocumentOrder()`) that compute each node's order key once, turning O(n log n x depth) sorts into O(n log n). `set:leading`/`set:trailing` also reuse one precomputed pivot key. Behavior is unchanged.
|
||||
- XPath compilation moved out of the per-node loops (#112): XSLT template selection now resolves each match pattern once per transform through a per-run match cache (the pattern's node set is computed once over the source tree, then membership is an identity lookup), instead of recompiling and re-walking the tree for every (node, template) pair; and the XSD identity validator compiles each selector and field XPath once per run through a query cache instead of once per visited element. Schematron already stored compiled queries. Behavior is unchanged; a multi-template shape test guards the path.
|
||||
- The HTML document builder's body is now constructed as a live mutable `TreeNode` tree with a single open-elements stack (the HTML5 model), attaching nodes to their parent as they open rather than accumulating children on close (#83, internal). Behavior is unchanged; this is the foundation the adoption agency algorithm needs to reparent already-built subtrees.
|
||||
- DTD attribute validation is now decomposed into five named, independently composable `Validation` rules (#101): `DTD.requiredAttributes`, `fixedAttributeValues`, `enumeratedAttributeValues`, `tokenizedAttributeTypes`, and `notationAttributes`, replacing the single `attributeDeclarations` rule. Each is removable by identity and isolation-tested one constraint at a time, honoring the validation-framework idiom. Behavior is unchanged. (XSD content validation stays one recursive rule by a documented scope decision, since its constraints are interdependent through type resolution; Schematron already exposes its rules as composable `Validation<Node, Void>` values.)
|
||||
|
||||
@ -63,7 +63,7 @@ extension PureXML.XPath {
|
||||
guard !target.isNaN else { return [] }
|
||||
return nodes
|
||||
.filter { PureXML.XPath.Value.parseNumber($0.stringValue) == target }
|
||||
.sorted(by: Node.precedes)
|
||||
.sortedByDocumentOrder()
|
||||
}
|
||||
|
||||
// MARK: sets
|
||||
@ -84,7 +84,7 @@ extension PureXML.XPath {
|
||||
guard let nodes = value?.nodes else { return [] }
|
||||
var seen: Set<String> = []
|
||||
var result: [Node] = []
|
||||
for node in nodes.sorted(by: Node.precedes) where seen.insert(node.stringValue).inserted {
|
||||
for node in nodes.sortedByDocumentOrder() where seen.insert(node.stringValue).inserted {
|
||||
result.append(node)
|
||||
}
|
||||
return result
|
||||
@ -93,13 +93,13 @@ extension PureXML.XPath {
|
||||
private static func difference(_ arguments: [Value]) -> [Node] {
|
||||
let first = arguments.first?.nodes ?? []
|
||||
let second = Set(arguments.count > 1 ? arguments[1].nodes ?? [] : [])
|
||||
return first.filter { !second.contains($0) }.sorted(by: Node.precedes)
|
||||
return first.filter { !second.contains($0) }.sortedByDocumentOrder()
|
||||
}
|
||||
|
||||
private static func intersection(_ arguments: [Value]) -> [Node] {
|
||||
let first = arguments.first?.nodes ?? []
|
||||
let second = Set(arguments.count > 1 ? arguments[1].nodes ?? [] : [])
|
||||
return first.filter { second.contains($0) }.sorted(by: Node.precedes)
|
||||
return first.filter { second.contains($0) }.sortedByDocumentOrder()
|
||||
}
|
||||
|
||||
private static func hasSameNode(_ arguments: [Value]) -> Bool {
|
||||
@ -111,12 +111,17 @@ extension PureXML.XPath {
|
||||
/// (or follow) the first node, in document order, of the second set.
|
||||
private static func relative(_ arguments: [Value], leading: Bool) -> [Node] {
|
||||
let first = arguments.first?.nodes ?? []
|
||||
guard let pivot = (arguments.count > 1 ? arguments[1].nodes ?? [] : []).min(by: Node.precedes) else {
|
||||
guard let pivot = (arguments.count > 1 ? arguments[1].nodes ?? [] : []).firstInDocumentOrder() else {
|
||||
return []
|
||||
}
|
||||
// Decorate once: each node's order key is computed a single time and
|
||||
// reused for both the pivot comparison and the final sort.
|
||||
let pivotKey = pivot.documentOrder
|
||||
return first
|
||||
.filter { leading ? Node.precedes($0, pivot) : Node.precedes(pivot, $0) }
|
||||
.sorted(by: Node.precedes)
|
||||
.map { (node: $0, key: $0.documentOrder) }
|
||||
.filter { leading ? Node.ordered($0.key, before: pivotKey) : Node.ordered(pivotKey, before: $0.key) }
|
||||
.sorted { Node.ordered($0.key, before: $1.key) }
|
||||
.map(\.node)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -162,7 +162,7 @@ extension PureXML.XPath {
|
||||
for node in nodes where seen.insert(node).inserted {
|
||||
unique.append(node)
|
||||
}
|
||||
return unique.sorted(by: Node.precedes)
|
||||
return unique.sortedByDocumentOrder()
|
||||
}
|
||||
|
||||
private static func root(of node: Node) -> Node {
|
||||
|
||||
@ -30,7 +30,7 @@ extension PureXML.XPath {
|
||||
|
||||
private static func target(_ arguments: [Value], _ context: EvaluationContext) -> Node? {
|
||||
if arguments.isEmpty { return context.node }
|
||||
return arguments.first?.nodes?.min(by: Node.precedes)
|
||||
return arguments.first?.nodes?.firstInDocumentOrder()
|
||||
}
|
||||
|
||||
private static func identified(_ argument: Value?, _ context: EvaluationContext) -> [Node] {
|
||||
|
||||
@ -37,7 +37,7 @@ public extension PureXML.XPath {
|
||||
public var string: String {
|
||||
switch self {
|
||||
case let .nodeSet(nodes):
|
||||
guard let first = nodes.min(by: Node.precedes) else { return "" }
|
||||
guard let first = nodes.firstInDocumentOrder() else { return "" }
|
||||
return first.stringValue
|
||||
case let .boolean(value): return value ? "true" : "false"
|
||||
case let .number(value): return Self.format(value)
|
||||
|
||||
@ -118,10 +118,16 @@ public extension PureXML.XPath {
|
||||
return path.reversed()
|
||||
}
|
||||
|
||||
/// Whether `lhs` precedes `rhs` in document order.
|
||||
/// Whether `lhs` precedes `rhs` in document order. Each call recomputes
|
||||
/// both order keys; for sorts and minimums over node-sets, prefer
|
||||
/// ``Swift/Sequence/sortedByDocumentOrder()`` and
|
||||
/// ``Swift/Sequence/firstInDocumentOrder()``, which compute each key once.
|
||||
static func precedes(_ lhs: Node, _ rhs: Node) -> Bool {
|
||||
let left = lhs.documentOrder
|
||||
let right = rhs.documentOrder
|
||||
ordered(lhs.documentOrder, before: rhs.documentOrder)
|
||||
}
|
||||
|
||||
/// Lexicographic compare of two document-order keys.
|
||||
static func ordered(_ left: [Int], before right: [Int]) -> Bool {
|
||||
for (one, two) in zip(left, right) where one != two {
|
||||
return one < two
|
||||
}
|
||||
@ -129,3 +135,21 @@ public extension PureXML.XPath {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Sequence<PureXML.XPath.Node> {
|
||||
/// The nodes sorted into document order, computing each node's order key once
|
||||
/// (decorate-sort-undecorate) rather than recomputing the root path on every
|
||||
/// comparison, which turns an O(n log n x depth) sort into O(n log n).
|
||||
func sortedByDocumentOrder() -> [PureXML.XPath.Node] {
|
||||
map { (node: $0, key: $0.documentOrder) }
|
||||
.sorted { PureXML.XPath.Node.ordered($0.key, before: $1.key) }
|
||||
.map(\.node)
|
||||
}
|
||||
|
||||
/// The first node in document order, computing each node's order key once.
|
||||
func firstInDocumentOrder() -> PureXML.XPath.Node? {
|
||||
map { (node: $0, key: $0.documentOrder) }
|
||||
.min { PureXML.XPath.Node.ordered($0.key, before: $1.key) }?
|
||||
.node
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user