// Primary expressions: literals, parens, lists, map literals, CASE,// EXISTS/COUNT subqueries, list-predicate quantifiers, function calls, and// variables. Ambiguity ordering mirrors the Rust grammar: COUNT/EXISTS// followed by '{' are subqueries (count(...) stays the aggregate); the// quantifiers all/any/none/single with a 'var IN' head are list// predicates, otherwise plain function calls. Excluded surface// (comprehensions, reduce, map projections, Cypher path-search functions)// gets targeted errors.
package parserimport ()// parsePrimary parses one primary expression (no prefix/infix/postfix).func ( *parser) () (ast.Expr, error) {// VALUE { ... } is the ISO scalar subquery -- not yet supported, but // it must say so rather than misparse into the map-projection error.if .peekKw("value") && .peekAt(1).Kind == TokLBrace {returnnil, errf(.peek().Pos, "VALUE { ... } scalar subqueries are not supported") } := .peek()switch .Kind {caseTokLParen: .i++ , := .parseExpr()if != nil {returnnil, }if , := .expect(TokRParen, "')'"); != nil {returnnil, }return , nilcaseTokLBracket:return .parseListLit()caseTokLBrace:return .parseMapLit()caseTokInt, TokFloat, TokStr, TokParam: , := .parseLiteralTok()if != nil {returnnil, }return &ast.Lit{Value: }, nilcaseTokIdent:return .parseIdentPrimary() }returnnil, errf(.Pos, "expected an expression, found %q", .Text)}// parseIdentPrimary handles every identifier-led primary.func ( *parser) () (ast.Expr, error) { := .peek()// Fold the identifier into a stack buffer for the keyword dispatch: a // plain variable or column reference (the common case) matches no keyword // and is not reserved, so it never needs a heap-allocated lowercased copy // -- the switch and reserved probe both run over the non-copying // string(folded) view. An identifier longer than any keyword folds to // ok=false and falls straight through to the variable/function path.var [24]byte , := foldLower(.Text, [:])if {switchstring() {case"true", "false", "null": , := .parseLiteralTok()if != nil {returnnil, }return &ast.Lit{Value: }, nilcase"case":return .parseCase()case"exists": .i++ , , := .parseBracedMatch("EXISTS")if != nil {returnnil, }return &ast.Exists{Pattern: , Where: }, nilcase"count":if .peekAt(1).Kind == TokLBrace { .i++ , , := .parseBracedMatch("COUNT")if != nil {returnnil, }return &ast.CountSub{Pattern: , Where: }, nil }case"collect":// COLLECT { ... } is the list subquery (the brace form); collect(...) // is the aggregate function call and falls through to parseFuncCall.if .peekAt(1).Kind == TokLBrace { .i++ , , , := .parseBracedCollect()if != nil {returnnil, }return &ast.PatternComp{Pattern: , Where: , Proj: }, nil }case"date", "datetime", "zoned_datetime", "localdatetime", "timestamp", "duration":// A temporal keyword directly followed by a string literal is the // GQL temporal-literal form (DATE '2024-01-01'); it lowers to the // matching constructor function. The function-call spelling has a // '(' here instead, so the forms never collide.if .peekAt(1).Kind == TokStr { := string()if == "datetime" || == "timestamp" { = "zoned_datetime" } .i++ , := .parseLiteralTok()if != nil {returnnil, }return &ast.Func{Name: , Args: []ast.Expr{&ast.Lit{Value: }}}, nil }case"all", "any", "none", "single":// A quantifier with a `var IN` head is a list predicate; anything // else falls through to a plain function call.if .peekAt(1).Kind == TokLParen && .peekAt(2).Kind == TokIdent && kwIs(.peekAt(3), "in") {return .parseListPred(string()) }case"cast":if .peekAt(1).Kind == TokLParen {return .parseCast() }case"reduce":returnnil, errf(.Pos, "reduce(...) is not in the GQL subset")case"shortestpath", "allshortestpaths", "weightedshortestpath":returnnil, errf(.Pos, "%s(...) is not GQL: write MATCH p = ANY SHORTEST / ALL SHORTEST <pattern> [COST <expr>]", .Text)case"cost":if .peekAt(1).Kind == TokLParen {returnnil, errf(.Pos, "cost(shortestPath(...)) is not in the GQL subset: write MATCH p = ANY SHORTEST <pattern> COST <expr>") } }ifreserved[string()] {returnnil, errf(.Pos, "reserved word %q cannot start an expression", .Text) } }if .peekAt(1).Kind == TokLParen {return .parseFuncCall() } .i++return &ast.Var{Name: .Text}, nil}// parseListLit parses a list literal, or a list comprehension// [x IN xs [WHERE pred] [| map]] -- an extension with no ISO GQL// spelling, matching the Rust grammar's ordered choice: a leading// `ident IN` always opens a comprehension (parenthesize a membership// test to keep it a literal element).func ( *parser) () (ast.Expr, error) { .i++ // '['if := .peek(); .Kind == TokIdent && !isReserved(.Text) && kwIs(.peekAt(1), "in") {return .parseListComp() } := &ast.ListExpr{}// A '(' opening the list is how a pattern comprehension presents when // its pattern cannot parse as an expression (e.g. [(a)-[:R]-(b) | x]), // so an element parse failure there gets the targeted rejection. := .peek().Kind == TokLParenfor .peek().Kind != TokRBracket { , := .parseExpr()if != nil {if {returnnil, patternCompErr(.peek().Pos) }returnnil, }if .peekKw("where") || .peek().Kind == TokPipe {returnnil, patternCompErr(.peek().Pos) } .Elems = append(.Elems, )if !.acceptTok(TokComma) {break } }if , := .expect(TokRBracket, "']' closing a list"); != nil {returnnil, }return , nil}// patternCompErr is the targeted pattern-comprehension rejection with its// GQL-conformant rewrites.func patternCompErr( int) *Error {returnerrf(, "pattern comprehensions are not in the GQL subset: rewrite size([pat | x]) as COUNT { MATCH pat } and value collection as MATCH pat + collect(...)")}// parseListComp parses the comprehension body after '[': var IN list// [WHERE pred] ['|' map] ']'.func ( *parser) () (ast.Expr, error) { , := .identName("a comprehension variable")if != nil {returnnil, }if := .expectKw("in"); != nil {returnnil, } , := .parseExpr()if != nil {returnnil, } := &ast.ListComp{Var: , List: }if .acceptKw("where") { , := .parseExpr()if != nil {returnnil, } .Filter = }if .acceptTok(TokPipe) { , := .parseExpr()if != nil {returnnil, } .Map = }if , := .expect(TokRBracket, "']' closing a comprehension"); != nil {returnnil, }return , nil}// parseCast parses CAST(expr AS TYPE) -- the GQL cast spelling, lowered// to the matching conversion function.func ( *parser) () (ast.Expr, error) { .i += 2// 'cast' '(' , := .parseExpr()if != nil {returnnil, }if := .expectKw("as"); != nil {returnnil, } , := .expect(TokIdent, "a CAST target type")if != nil {returnnil, }var [16]byte , := foldLower(.Text, [:])varstringswitchstring() {case"float", "double": = "tofloat"case"int", "integer", "bigint": = "tointeger"case"string", "varchar": = "tostring"case"bool", "boolean": = "toboolean"case"date": = "date"case"datetime", "zoned_datetime", "timestamp": = "zoned_datetime"case"localdatetime": = "localdatetime"case"duration": = "duration"default:returnnil, errf(.Pos, "CAST target %q is not supported (FLOAT, INTEGER, STRING, BOOLEAN, DATE, DATETIME, DURATION)", .Text) }if , := .expect(TokRParen, "')' closing the CAST"); != nil {returnnil, }return &ast.Func{Name: , Args: []ast.Expr{}}, nil}// parseMapLit parses {key: expr, ...} in expression position.func ( *parser) () (ast.Expr, error) { .i++ // '{' := &ast.MapLit{}for .peek().Kind != TokRBrace { , := .identName("a map key")if != nil {returnnil, }if , := .expect(TokColon, "':'"); != nil {returnnil, } , := .parseExpr()if != nil {returnnil, } .Fields = append(.Fields, ast.MapField{Key: , Val: })if !.acceptTok(TokComma) {break } }if , := .expect(TokRBrace, "'}' closing a map"); != nil {returnnil, }return , nil}// parseCase parses CASE [operand] (WHEN cond THEN result)+ [ELSE default]// END; without an operand it is the searched form.func ( *parser) () (ast.Expr, error) { .i++ // CASE := &ast.Case{}if !.peekKw("when") { , := .parseExpr()if != nil {returnnil, } .Operand = }for .acceptKw("when") { , := .parseExpr()if != nil {returnnil, }if := .expectKw("then"); != nil {returnnil, } , := .parseExpr()if != nil {returnnil, } .Whens = append(.Whens, ast.CaseWhen{Cond: , Result: }) }iflen(.Whens) == 0 {returnnil, errf(.peek().Pos, "CASE needs at least one WHEN") }if .acceptKw("else") { , := .parseExpr()if != nil {returnnil, } .Else = }if := .expectKw("end"); != nil {returnnil, }return , nil}// parseBracedMatch parses { [MATCH] pattern [WHERE expr] } -- the body of// EXISTS / COUNT subqueries. The MATCH keyword is optional (GQL's bare// EXISTS { <pattern> } spelling).func ( *parser) ( string) (*ast.Pattern, ast.Expr, error) {if , := .expect(TokLBrace, "'{' after "+); != nil {returnnil, nil, } .acceptKw("match") , := .parsePattern()if != nil {returnnil, nil, } , := .parseOptionalWhere()if != nil {returnnil, nil, }if , := .expect(TokRBrace, "'}' closing "+); != nil {returnnil, nil, }return , , nil}// parseBracedCollect parses { [MATCH] pattern [WHERE expr] RETURN proj [AS// alias] } -- the body of a COLLECT subquery. Unlike EXISTS/COUNT, RETURN and// a single projection are required (COLLECT must be told what to gather); the// MATCH keyword is optional and an AS alias is accepted but ignored (a list// value carries no column name).func ( *parser) () (*ast.Pattern, ast.Expr, ast.Expr, error) {if , := .expect(TokLBrace, "'{' after COLLECT"); != nil {returnnil, nil, nil, } .acceptKw("match") , := .parsePattern()if != nil {returnnil, nil, nil, } , := .parseOptionalWhere()if != nil {returnnil, nil, nil, }if := .expectKw("return"); != nil {returnnil, nil, nil, } , := .parseExpr()if != nil {returnnil, nil, nil, }if .acceptKw("as") {if , := .identName("a COLLECT alias"); != nil {returnnil, nil, nil, } }if , := .expect(TokRBrace, "'}' closing COLLECT"); != nil {returnnil, nil, nil, }return , , , nil}// parseListPred parses quant(var IN list WHERE pred).func ( *parser) ( string) (ast.Expr, error) {varast.Quantswitch {case"all": = ast.QuantAllcase"any": = ast.QuantAnycase"none": = ast.QuantNonedefault: = ast.QuantSingle } .i += 2// quant '(' , := .identName("a quantifier variable")if != nil {returnnil, }if := .expectKw("in"); != nil {returnnil, } , := .parseExpr()if != nil {returnnil, }if := .expectKw("where"); != nil {returnnil, } , := .parseExpr()if != nil {returnnil, }if , := .expect(TokRParen, "')' closing the quantifier"); != nil {returnnil, }return &ast.ListPred{Quant: , Var: , List: , Pred: }, nil}// parseFuncCall parses name([DISTINCT] * | args...).func ( *parser) () (ast.Expr, error) { , := .identName("a function name")if != nil {returnnil, } .i++ // '(' := &ast.Func{Name: } .Distinct = .acceptKw("distinct")if .acceptTok(TokStar) { .Star = true } else {for .peek().Kind != TokRParen { , := .parseExpr()if != nil {returnnil, } .Args = append(.Args, )if !.acceptTok(TokComma) {break } } }if , := .expect(TokRParen, "')' closing the call"); != nil {returnnil, }// The ISO surface passes NORMALIZE's form as a bare keyword // (NORMALIZE(s, NFC)); it parses as a variable reference, so lower it // to the string literal the evaluator reads (a quoted form works too).if (strings.EqualFold(, "normalize") || strings.EqualFold(, "is_normalized")) && len(.Args) == 2 {if , := .Args[1].(*ast.Var); && isNormForm(.Name) { .Args[1] = &ast.Lit{Value: ast.StrLit(strings.ToUpper(.Name))} } }return , nil}// isNormForm reports whether name is a Unicode normalization form keyword.func isNormForm( string) bool {switchstrings.ToUpper() {case"NFC", "NFD", "NFKC", "NFKD":returntrue }returnfalse}
The pages are generated with Goldsv0.8.7. (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu.
PR and bug reports are welcome and can be submitted to the issue list.
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds.