PureXML/Sources/XPath/Compiler.swift
Mihaela Mihaljevic d83e5a88ce feat(xpath): expression engine and the four-type model
Compile and evaluate full XPath 1.0 expressions, not just location paths.
Add the four-type Value (node-set, boolean, number, string) with the spec's
coercion rules and canonical number formatting, a recursive-descent parser
for the operator grammar (or/and, equality, relational, additive,
multiplicative, unary minus, union) whose grammatical position disambiguates
* and the operator keywords, function calls, variable references, and filter
expressions. Predicates are now arbitrary expressions (a numeric predicate
is a position test). Comparison follows the node-set quantification rules.
Query gains value/number/string/boolean accessors and a $variable binding
map; the core functions last, position, count, not, true, false, boolean,
number, and string ship with it.

Closes #42
2026-06-08 05:33:13 +02:00

203 lines
6.6 KiB
Swift

extension PureXML.XPath {
/// Compiles an XPath expression into an ``Expression`` tree. A recursive
/// descent parser whose grammatical position disambiguates the tokens XPath
/// overloads: `*` is a wildcard in a node test but multiplication after an
/// operand, and `div`/`mod`/`and`/`or` are operator names only where an
/// operator is expected. The path, primary, and token parsing live in a
/// companion file. Reimplemented from the XPath 1.0 grammar.
struct Compiler {
let chars: [Character]
var index = 0
private init(_ path: String) {
chars = Array(path)
}
static func compile(_ path: String) throws -> Expression {
var compiler = Compiler(path)
compiler.skipSpace()
guard !compiler.isAtEnd else { throw QueryError.empty }
let expression = try compiler.parseExpression()
compiler.skipSpace()
guard compiler.isAtEnd else {
throw QueryError.unexpectedToken(String(compiler.peek() ?? " "))
}
return expression
}
// MARK: Operator precedence ladder
mutating func parseExpression() throws -> Expression {
try parseOr()
}
private mutating func parseOr() throws -> Expression {
var left = try parseAnd()
while peekKeyword("or") {
advance(by: 2)
left = try .binary(.logicalOr, left, parseAnd())
}
return left
}
private mutating func parseAnd() throws -> Expression {
var left = try parseEquality()
while peekKeyword("and") {
advance(by: 3)
left = try .binary(.logicalAnd, left, parseEquality())
}
return left
}
private mutating func parseEquality() throws -> Expression {
var left = try parseRelational()
while true {
skipSpace()
if consume("!=") {
left = try .binary(.notEqual, left, parseRelational())
} else if peek() == "=" {
advance()
left = try .binary(.equal, left, parseRelational())
} else {
return left
}
}
}
private mutating func parseRelational() throws -> Expression {
var left = try parseAdditive()
while let look = relationalOperator() {
left = try .binary(look, left, parseAdditive())
}
return left
}
private mutating func relationalOperator() -> BinaryOperator? {
skipSpace()
if consume("<=") { return .lessOrEqual }
if consume(">=") { return .greaterOrEqual }
if peek() == "<" {
advance()
return .lessThan
}
if peek() == ">" {
advance()
return .greaterThan
}
return nil
}
private mutating func parseAdditive() throws -> Expression {
var left = try parseMultiplicative()
while true {
skipSpace()
if peek() == "+" {
advance()
left = try .binary(.add, left, parseMultiplicative())
} else if peek() == "-" {
advance()
left = try .binary(.subtract, left, parseMultiplicative())
} else {
return left
}
}
}
private mutating func parseMultiplicative() throws -> Expression {
var left = try parseUnary()
while let look = multiplicativeOperator() {
left = try .binary(look, left, parseUnary())
}
return left
}
private mutating func multiplicativeOperator() -> BinaryOperator? {
skipSpace()
if peek() == "*" {
advance()
return .multiply
}
if peekKeyword("div") {
advance(by: 3)
return .divide
}
if peekKeyword("mod") {
advance(by: 3)
return .modulo
}
return nil
}
private mutating func parseUnary() throws -> Expression {
skipSpace()
if peek() == "-" {
advance()
return try .negate(parseUnary())
}
return try parseUnion()
}
private mutating func parseUnion() throws -> Expression {
var left = try parsePath()
while true {
skipSpace()
guard peek() == "|" else { return left }
advance()
left = try .union(left, parsePath())
}
}
// MARK: Cursor
mutating func expect(_ character: Character) throws {
guard peek() == character else {
throw QueryError.unexpectedToken(String(peek() ?? " "))
}
advance()
}
var isAtEnd: Bool {
index >= chars.count
}
func peek(_ ahead: Int = 0) -> Character? {
let target = index + ahead
return target < chars.count ? chars[target] : nil
}
/// Whether `word` appears next as a whole token (not followed by a name
/// character), used to read operator keywords in operator position.
func peekKeyword(_ word: String) -> Bool {
guard matches(word) else { return false }
if let after = peek(word.count), after.isXMLNameContinuation { return false }
return true
}
func matches(_ literal: String) -> Bool {
let target = Array(literal)
guard index + target.count <= chars.count else { return false }
for (offset, character) in target.enumerated() where chars[index + offset] != character {
return false
}
return true
}
@discardableResult
mutating func consume(_ literal: String) -> Bool {
guard matches(literal) else { return false }
index += literal.count
return true
}
mutating func advance(by count: Int = 1) {
index = Swift.min(index + count, chars.count)
}
mutating func skipSpace() {
while let character = peek(), character == " " || character == "\t" || character == "\n" || character == "\r" {
advance()
}
}
}
}