SPB Git

spb/metrika Public

Stata-class statistics, GPU-accelerated by Apple Silicon. Native Swift — no Electron, no Python runtime, no compromises.

Swift 92.4% HTML 3.3% R 3% Shell 1.3%
3.8 KB · 129 lines swift
Raw Blame History
1//2//  ExpressionParser.swift3//  Metrika4//5//  Author:  Simon-Pierre Boucher6//  Contact: contact@spboucher.ai7//  Copyright © 2026 Simon-Pierre Boucher. All rights reserved.8//910/// Pratt (precedence-climbing) parser for ZQL expressions, shared by `if`11/// qualifiers, `gen`/`replace` right-hand sides, and weight expressions.12///13/// Precedence (low → high):14///   | · & · comparison (== != < <= > >=) · + - · * / · ^ · unary (- !)15struct ExpressionParser {16    var tokens: [ZQToken]17    var position: Int1819    init(tokens: [ZQToken], position: Int = 0) {20        self.tokens = tokens21        self.position = position22    }2324    var current: ZQToken { tokens[position] }2526    mutating func advance() -> ZQToken {27        let token = tokens[position]28        if position < tokens.count - 1 { position += 1 }29        return token30    }3132    private static let precedence: [String: Int] = [33        "|": 10,34        "&": 20,35        "==": 30, "!=": 30, "<": 30, "<=": 30, ">": 30, ">=": 30,36        "+": 40, "-": 40,37        "*": 50, "/": 50,38        "^": 60,39    ]4041    mutating func parseExpression(minPrecedence: Int = 0) throws(ZQParseError) -> ZQExpression {42        var left = try parseUnary()4344        while case .op(let symbol) = current.kind,45              let precedence = Self.precedence[symbol],46              precedence >= minPrecedence {47            _ = advance()48            // ^ is right-associative; everything else is left-associative.49            let nextMin = symbol == "^" ? precedence : precedence + 150            let right = try parseExpression(minPrecedence: nextMin)51            left = .binary(op: symbol, left, right)52        }53        return left54    }5556    private mutating func parseUnary() throws(ZQParseError) -> ZQExpression {57        if case .op(let symbol) = current.kind, symbol == "-" || symbol == "!" {58            _ = advance()59            let operand = try parseUnary()60            return .unary(op: symbol, operand)61        }62        return try parsePrimary()63    }6465    private mutating func parsePrimary() throws(ZQParseError) -> ZQExpression {66        let token = current67        switch token.kind {68        case .number(let value):69            _ = advance()70            return .number(value)7172        case .string(let value):73            _ = advance()74            return .string(value)7576        case .op("."):77            // Bare `.` is the missing-value literal (`if income == .`).78            _ = advance()79            return .missing8081        case .identifier(let name):82            _ = advance()83            if case .lparen = current.kind {84                _ = advance()85                var arguments: [ZQExpression] = []86                if !isKind(.rparen) {87                    repeat {88                        arguments.append(try parseExpression())89                    } while consumeIfKind(.comma)90                }91                try expect(.rparen)92                return .call(name: name, arguments: arguments)93            }94            return .variable(name)9596        case .lparen:97            _ = advance()98            let inner = try parseExpression()99            try expect(.rparen)100            return inner101102        default:103            throw ZQParseError(104                message: "expected an expression, found \(token.kind.describe)",105                column: token.column106            )107        }108    }109110    func isKind(_ kind: ZQToken.Kind) -> Bool { current.kind == kind }111112    mutating func consumeIfKind(_ kind: ZQToken.Kind) -> Bool {113        if current.kind == kind {114            _ = advance()115            return true116        }117        return false118    }119120    mutating func expect(_ kind: ZQToken.Kind) throws(ZQParseError) {121        guard consumeIfKind(kind) else {122            throw ZQParseError(123                message: "expected \(kind.describe), found \(current.kind.describe)",124                column: current.column125            )126        }127    }128}129