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%
6.4 KB · 175 lines swift
Raw Blame History
1//2//  ParserTests.swift3//  Metrika4//5//  Author:  Simon-Pierre Boucher6//  Contact: contact@spboucher.ai7//  Copyright © 2026 Simon-Pierre Boucher. All rights reserved.8//910import Testing11@testable import ZQParser1213@Suite("ZQL parser golden tests")14struct ParserTests {15    let parser = ZQCommandParser()1617    @Test("use file")18    func useFile() throws {19        let command = try #require(try parser.parse("use sales.parquet"))20        #expect(command.verb == "use")21        #expect(command.argument == "sales.parquet")22    }2324    @Test("summarize with detail option")25    func summarizeDetail() throws {26        let command = try #require(try parser.parse("summarize revenue price, detail"))27        #expect(command.verb == "summarize")28        #expect(command.varlist == [.simple("revenue"), .simple("price")])29        #expect(command.hasOption("detail"))30    }3132    @Test("verb abbreviation resolution")33    func abbreviations() throws {34        #expect(try parser.parse("su price")?.verb == "summarize")35        #expect(try parser.parse("reg y x")?.verb == "regress")36        #expect(try parser.parse("gen z = 1")?.verb == "generate")37        #expect(try parser.parse("d")?.verb == "describe")38    }3940    @Test("gen with expression")41    func generate() throws {42        let command = try #require(try parser.parse("gen log_rev = ln(revenue)"))43        #expect(command.verb == "generate")44        let assignment = try #require(command.assignment)45        #expect(assignment.target == "log_rev")46        #expect(assignment.expression == .call(name: "ln", arguments: [.variable("revenue")]))47    }4849    @Test("regress with factor variable and options")50    func regressFactors() throws {51        let command = try #require(try parser.parse("reg log_rev price i.region, robust"))52        #expect(command.verb == "regress")53        #expect(command.varlist == [54            .simple("log_rev"), .simple("price"), .factor(op: "i", name: "region"),55        ])56        #expect(command.hasOption("robust"))57    }5859    @Test("cluster option carries its argument")60    func clusterOption() throws {61        let command = try #require(try parser.parse("xtreg log_rev price, fe cluster(firm_id)"))62        #expect(command.verb == "xtreg")63        #expect(command.hasOption("fe"))64        #expect(command.option("cluster")?.firstArgument == "firm_id")65    }6667    @Test("bootstrap prefix command")68    func bootstrapPrefix() throws {69        let command = try #require(70            try parser.parse("bootstrap, reps(100000) seed(42): reg log_rev price")71        )72        #expect(command.verb == "bootstrap")73        #expect(command.option("reps")?.firstArgument == "100000")74        #expect(command.option("seed")?.firstArgument == "42")75        let body = try #require(command.body)76        #expect(body.verb == "regress")77        #expect(body.varlist == [.simple("log_rev"), .simple("price")])78    }7980    @Test("xtset panel declaration")81    func xtset() throws {82        let command = try #require(try parser.parse("xtset firm_id year"))83        #expect(command.verb == "xtset")84        #expect(command.varlist == [.simple("firm_id"), .simple("year")])85    }8687    @Test("graph compound verb with by option")88    func graphScatter() throws {89        let command = try #require(try parser.parse("graph scatter log_rev price, by(region)"))90        #expect(command.verb == "graph")91        #expect(command.subverb == "scatter")92        #expect(command.option("by")?.firstArgument == "region")93    }9495    @Test("if, in, and weight qualifiers")96    func qualifiers() throws {97        let command = try #require(98            try parser.parse("summarize price if revenue > 100 & !missing(price) in 1/50 [aweight = pop]")99        )100        #expect(command.range == ZQRange(lower: 1, upper: 50))101        #expect(command.weight == ZQWeight(kind: .aweight, expression: .variable("pop")))102        let condition = try #require(command.condition)103        #expect(condition == .binary(104            op: "&",105            .binary(op: ">", .variable("revenue"), .number(100)),106            .unary(op: "!", .call(name: "missing", arguments: [.variable("price")]))107        ))108    }109110    @Test("continuous interaction c.age#c.age")111    func interaction() throws {112        let command = try #require(try parser.parse("reg wage c.age#c.age education"))113        #expect(command.varlist == [114            .simple("wage"),115            .interaction(116                [.factor(op: "c", name: "age"), .factor(op: "c", name: "age")],117                full: false118            ),119            .simple("education"),120        ])121    }122123    @Test("missing-value literal in expressions")124    func missingLiteral() throws {125        let command = try #require(try parser.parse("count if income == ."))126        #expect(command.condition == .binary(op: "==", .variable("income"), .missing))127    }128129    @Test("comments and blank lines yield nil")130    func comments() throws {131        #expect(try parser.parse("") == nil)132        #expect(try parser.parse("   ") == nil)133        #expect(try parser.parse("// a comment") == nil)134        #expect(try parser.parse("* stata-style comment") == nil)135    }136137    @Test("unknown verb error carries column and suggestion")138    func unknownVerbSuggestion() {139        do {140            _ = try parser.parse("regresss y x")141            Issue.record("expected a parse error")142        } catch let error as ZQParseError {143            #expect(error.column == 1)144            #expect(error.suggestion == "regress")145        } catch {146            Issue.record("unexpected error type: \(error)")147        }148    }149150    @Test("error cites the offending column")151    func errorColumn() {152        do {153            _ = try parser.parse("summarize price, level(")154            Issue.record("expected a parse error")155        } catch let error as ZQParseError {156            // The unclosed '(' is reported at end of line: one past the157            // final character of the 23-character command.158            #expect(error.column == 24)159        } catch {160            Issue.record("unexpected error type: \(error)")161        }162    }163164    @Test("operator precedence in expressions")165    func precedence() throws {166        let command = try #require(try parser.parse("count if a + b * c == d ^ 2 ^ 3"))167        // b*c binds before +; ^ is right-associative.168        #expect(command.condition == .binary(169            op: "==",170            .binary(op: "+", .variable("a"), .binary(op: "*", .variable("b"), .variable("c"))),171            .binary(op: "^", .variable("d"), .binary(op: "^", .number(2), .number(3)))172        ))173    }174}175