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%
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