// // ExpressionParser.swift // Metrika // // Author: Simon-Pierre Boucher // Contact: contact@spboucher.ai // Copyright © 2026 Simon-Pierre Boucher. All rights reserved. // /// Pratt (precedence-climbing) parser for ZQL expressions, shared by `if` /// qualifiers, `gen`/`replace` right-hand sides, and weight expressions. /// /// Precedence (low → high): /// | · & · comparison (== != < <= > >=) · + - · * / · ^ · unary (- !) struct ExpressionParser { var tokens: [ZQToken] var position: Int init(tokens: [ZQToken], position: Int = 0) { self.tokens = tokens self.position = position } var current: ZQToken { tokens[position] } mutating func advance() -> ZQToken { let token = tokens[position] if position < tokens.count - 1 { position += 1 } return token } private static let precedence: [String: Int] = [ "|": 10, "&": 20, "==": 30, "!=": 30, "<": 30, "<=": 30, ">": 30, ">=": 30, "+": 40, "-": 40, "*": 50, "/": 50, "^": 60, ] mutating func parseExpression(minPrecedence: Int = 0) throws(ZQParseError) -> ZQExpression { var left = try parseUnary() while case .op(let symbol) = current.kind, let precedence = Self.precedence[symbol], precedence >= minPrecedence { _ = advance() // ^ is right-associative; everything else is left-associative. let nextMin = symbol == "^" ? precedence : precedence + 1 let right = try parseExpression(minPrecedence: nextMin) left = .binary(op: symbol, left, right) } return left } private mutating func parseUnary() throws(ZQParseError) -> ZQExpression { if case .op(let symbol) = current.kind, symbol == "-" || symbol == "!" { _ = advance() let operand = try parseUnary() return .unary(op: symbol, operand) } return try parsePrimary() } private mutating func parsePrimary() throws(ZQParseError) -> ZQExpression { let token = current switch token.kind { case .number(let value): _ = advance() return .number(value) case .string(let value): _ = advance() return .string(value) case .op("."): // Bare `.` is the missing-value literal (`if income == .`). _ = advance() return .missing case .identifier(let name): _ = advance() if case .lparen = current.kind { _ = advance() var arguments: [ZQExpression] = [] if !isKind(.rparen) { repeat { arguments.append(try parseExpression()) } while consumeIfKind(.comma) } try expect(.rparen) return .call(name: name, arguments: arguments) } return .variable(name) case .lparen: _ = advance() let inner = try parseExpression() try expect(.rparen) return inner default: throw ZQParseError( message: "expected an expression, found \(token.kind.describe)", column: token.column ) } } func isKind(_ kind: ZQToken.Kind) -> Bool { current.kind == kind } mutating func consumeIfKind(_ kind: ZQToken.Kind) -> Bool { if current.kind == kind { _ = advance() return true } return false } mutating func expect(_ kind: ZQToken.Kind) throws(ZQParseError) { guard consumeIfKind(kind) else { throw ZQParseError( message: "expected \(kind.describe), found \(current.kind.describe)", column: current.column ) } } }