// // ParserTests.swift // Metrika // // Author: Simon-Pierre Boucher // Contact: contact@spboucher.ai // Copyright © 2026 Simon-Pierre Boucher. All rights reserved. // import Testing @testable import ZQParser @Suite("ZQL parser golden tests") struct ParserTests { let parser = ZQCommandParser() @Test("use file") func useFile() throws { let command = try #require(try parser.parse("use sales.parquet")) #expect(command.verb == "use") #expect(command.argument == "sales.parquet") } @Test("summarize with detail option") func summarizeDetail() throws { let command = try #require(try parser.parse("summarize revenue price, detail")) #expect(command.verb == "summarize") #expect(command.varlist == [.simple("revenue"), .simple("price")]) #expect(command.hasOption("detail")) } @Test("verb abbreviation resolution") func abbreviations() throws { #expect(try parser.parse("su price")?.verb == "summarize") #expect(try parser.parse("reg y x")?.verb == "regress") #expect(try parser.parse("gen z = 1")?.verb == "generate") #expect(try parser.parse("d")?.verb == "describe") } @Test("gen with expression") func generate() throws { let command = try #require(try parser.parse("gen log_rev = ln(revenue)")) #expect(command.verb == "generate") let assignment = try #require(command.assignment) #expect(assignment.target == "log_rev") #expect(assignment.expression == .call(name: "ln", arguments: [.variable("revenue")])) } @Test("regress with factor variable and options") func regressFactors() throws { let command = try #require(try parser.parse("reg log_rev price i.region, robust")) #expect(command.verb == "regress") #expect(command.varlist == [ .simple("log_rev"), .simple("price"), .factor(op: "i", name: "region"), ]) #expect(command.hasOption("robust")) } @Test("cluster option carries its argument") func clusterOption() throws { let command = try #require(try parser.parse("xtreg log_rev price, fe cluster(firm_id)")) #expect(command.verb == "xtreg") #expect(command.hasOption("fe")) #expect(command.option("cluster")?.firstArgument == "firm_id") } @Test("bootstrap prefix command") func bootstrapPrefix() throws { let command = try #require( try parser.parse("bootstrap, reps(100000) seed(42): reg log_rev price") ) #expect(command.verb == "bootstrap") #expect(command.option("reps")?.firstArgument == "100000") #expect(command.option("seed")?.firstArgument == "42") let body = try #require(command.body) #expect(body.verb == "regress") #expect(body.varlist == [.simple("log_rev"), .simple("price")]) } @Test("xtset panel declaration") func xtset() throws { let command = try #require(try parser.parse("xtset firm_id year")) #expect(command.verb == "xtset") #expect(command.varlist == [.simple("firm_id"), .simple("year")]) } @Test("graph compound verb with by option") func graphScatter() throws { let command = try #require(try parser.parse("graph scatter log_rev price, by(region)")) #expect(command.verb == "graph") #expect(command.subverb == "scatter") #expect(command.option("by")?.firstArgument == "region") } @Test("if, in, and weight qualifiers") func qualifiers() throws { let command = try #require( try parser.parse("summarize price if revenue > 100 & !missing(price) in 1/50 [aweight = pop]") ) #expect(command.range == ZQRange(lower: 1, upper: 50)) #expect(command.weight == ZQWeight(kind: .aweight, expression: .variable("pop"))) let condition = try #require(command.condition) #expect(condition == .binary( op: "&", .binary(op: ">", .variable("revenue"), .number(100)), .unary(op: "!", .call(name: "missing", arguments: [.variable("price")])) )) } @Test("continuous interaction c.age#c.age") func interaction() throws { let command = try #require(try parser.parse("reg wage c.age#c.age education")) #expect(command.varlist == [ .simple("wage"), .interaction( [.factor(op: "c", name: "age"), .factor(op: "c", name: "age")], full: false ), .simple("education"), ]) } @Test("missing-value literal in expressions") func missingLiteral() throws { let command = try #require(try parser.parse("count if income == .")) #expect(command.condition == .binary(op: "==", .variable("income"), .missing)) } @Test("comments and blank lines yield nil") func comments() throws { #expect(try parser.parse("") == nil) #expect(try parser.parse(" ") == nil) #expect(try parser.parse("// a comment") == nil) #expect(try parser.parse("* stata-style comment") == nil) } @Test("unknown verb error carries column and suggestion") func unknownVerbSuggestion() { do { _ = try parser.parse("regresss y x") Issue.record("expected a parse error") } catch let error as ZQParseError { #expect(error.column == 1) #expect(error.suggestion == "regress") } catch { Issue.record("unexpected error type: \(error)") } } @Test("error cites the offending column") func errorColumn() { do { _ = try parser.parse("summarize price, level(") Issue.record("expected a parse error") } catch let error as ZQParseError { // The unclosed '(' is reported at end of line: one past the // final character of the 23-character command. #expect(error.column == 24) } catch { Issue.record("unexpected error type: \(error)") } } @Test("operator precedence in expressions") func precedence() throws { let command = try #require(try parser.parse("count if a + b * c == d ^ 2 ^ 3")) // b*c binds before +; ^ is right-associative. #expect(command.condition == .binary( op: "==", .binary(op: "+", .variable("a"), .binary(op: "*", .variable("b"), .variable("c"))), .binary(op: "^", .variable("d"), .binary(op: "^", .number(2), .number(3))) )) } }