PureXML/Sources/XPath/Evaluator.swift
Mihaela Mihaljevic b8dbf8e97a 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
2026-06-10 03:39:19 +02:00

189 lines
7.7 KiB
Swift

extension PureXML.XPath {
/// Evaluates a compiled ``Expression`` against a document, producing an XPath
/// ``Value``. Location paths run over the parent-aware tree; the operator
/// grammar, functions, and variables build on the four-type model.
enum Evaluator {
/// The full XPath 1.0 function library: the core functions plus the
/// string, node, and number families.
nonisolated(unsafe) static let library = CoreFunctions.table
.merging(StringFunctions.table)
.merging(NodeFunctions.table)
/// Evaluates an expression and returns its node-set as selections in
/// document order. A non-node-set result yields an empty list; use
/// ``value(_:over:variables:)`` for typed results.
static func evaluate(_ expression: Expression, over node: PureXML.Model.Node) -> [Selection] {
let context = rootContext(node, variables: [:])
guard let value = try? eval(expression, context), case let .nodeSet(nodes) = value else {
return []
}
return orderUnique(nodes).map(selection)
}
/// Evaluates an expression to a typed value with optional variable bindings.
static func value(
_ expression: Expression,
over node: PureXML.Model.Node,
variables: [String: Value],
) throws -> Value {
try eval(expression, rootContext(node, variables: variables))
}
/// Evaluates a node-set expression against a pre-built tree and returns the
/// matched tree nodes in document order (attribute and namespace results
/// are dropped). Lets a caller build the tree once and query it repeatedly.
static func nodes(
_ expression: Expression,
over root: PureXML.Model.TreeNode,
functions: FunctionTable = FunctionTable(),
) -> [PureXML.Model.TreeNode] {
let context = EvaluationContext(
node: .tree(root),
position: 1,
size: 1,
variables: [:],
functions: library.merging(functions),
)
guard let value = try? eval(expression, context), case let .nodeSet(nodes) = value else {
return []
}
return orderUnique(nodes).compactMap(\.treeNode)
}
/// Evaluates an expression against an explicit context: a node already in a
/// tree, its proximity position and size, and variable bindings. This is
/// the entry point downstream engines (XSLT, Schematron) drive per node.
static func value(
_ expression: Expression,
at node: PureXML.Model.TreeNode,
position: Int,
size: Int,
variables: [String: Value],
functions: FunctionTable = FunctionTable(),
namespaces: [String: String] = [:],
) throws -> Value {
let context = EvaluationContext(
node: .tree(node),
position: position,
size: size,
variables: variables,
functions: library.merging(functions),
namespaces: namespaces,
)
return try eval(expression, context)
}
/// Evaluates a node-set expression over a value tree with eval-time prefix
/// bindings, returning the selected nodes in document order.
static func evaluate(
_ expression: Expression,
over node: PureXML.Model.Node,
namespaces: [String: String],
) -> [Selection] {
let context = rootContext(node, variables: [:], namespaces: namespaces)
guard let value = try? eval(expression, context), case let .nodeSet(nodes) = value else {
return []
}
return orderUnique(nodes).map(selection)
}
private static func rootContext(
_ node: PureXML.Model.Node,
variables: [String: Value],
namespaces: [String: String] = [:],
) -> EvaluationContext {
let root = PureXML.Model.TreeNode(node)
return EvaluationContext(
node: .tree(root),
position: 1,
size: 1,
variables: variables,
functions: library,
namespaces: namespaces,
)
}
static func eval(_ expression: Expression, _ context: EvaluationContext) throws -> Value {
switch expression {
case let .number(value):
return .number(value)
case let .string(value):
return .string(value)
case let .variable(name):
guard let value = context.variables[name] else { throw QueryError.undefinedVariable(name) }
return value
case let .negate(inner):
return try .number(-eval(inner, context).number)
case let .function(name, arguments):
let values = try arguments.map { try eval($0, context) }
return try context.functions.call(name, values, context)
case let .union(left, right):
return try .nodeSet(orderUnique(nodeSet(eval(left, context)) + nodeSet(eval(right, context))))
case let .binary(oper, left, right):
return try evalBinary(oper, left, right, context)
case let .path(absolute, steps):
return try evalPath(absolute: absolute, steps: steps, context)
case let .filter(primary, predicates, steps):
return try evalFilter(primary, predicates, steps, context)
}
}
private static func evalPath(absolute: Bool, steps: [Step], _ context: EvaluationContext) throws -> Value {
let start: Node = absolute ? root(of: context.node) : context.node
return .nodeSet(orderUnique(evaluateSteps(steps, from: [start], context)))
}
private static func evalFilter(
_ primary: Expression,
_ predicates: [Expression],
_ steps: [Step],
_ context: EvaluationContext,
) throws -> Value {
var nodes = try orderUnique(nodeSet(eval(primary, context)))
nodes = try applyPredicates(predicates, to: nodes, context)
if !steps.isEmpty {
nodes = orderUnique(evaluateSteps(steps, from: nodes, context))
}
return .nodeSet(nodes)
}
// MARK: Node-set helpers
static func nodeSet(_ value: Value) throws -> [Node] {
guard case let .nodeSet(nodes) = value else {
throw QueryError.invalidArguments("a node-set was expected")
}
return nodes
}
static func orderUnique(_ nodes: [Node]) -> [Node] {
var seen: Set<Node> = []
var unique: [Node] = []
for node in nodes where seen.insert(node).inserted {
unique.append(node)
}
return unique.sortedByDocumentOrder()
}
private static func root(of node: Node) -> Node {
var current = node
while let parent = current.parent {
current = parent
}
return current
}
private static func selection(_ node: Node) -> Selection {
switch node {
case let .tree(tree):
return .node(tree.node)
case let .attribute(_, attribute):
return .attribute(attribute)
case let .namespace(_, prefix, uri):
let name = prefix.isEmpty ? "xmlns" : "xmlns:\(prefix)"
return .attribute(PureXML.Model.Attribute(name, uri))
}
}
}
}