// Package parser is the hand-written GQL parser (lexer + recursive descent // + Pratt expressions): the read-only ISO GQL subset specified in // gql/GRAMMAR.md, lowered into the language-neutral AST. // // GQL surface forms normalize into the Rust engine's segment model: // RETURN ... NEXT becomes an ast.With projection boundary; LET x = e // becomes With{star + items}; FILTER pred becomes With{star, where}; FOR x // IN list becomes ast.Unwind; CALL (vars) { ... } becomes ast.CallSubquery // with the scope vars as imports AND a synthesized importing With // prepended to every branch of the body (the clause shape the binder // expects). Cypher-only spellings (WITH, UNWIND, *1..3, shortestPath()) // are rejected with pointers to the GQL forms.
package parser import ( ) // parser holds the token stream cursor. type parser struct { toks []Token i int } // Parse parses one GQL query. The error, when non-nil, is a *parser.Error // carrying the byte offset (the root gql package wraps it with ErrParse). func ( string) (*ast.Query, error) { , := lex() if != nil { return nil, } := &parser{toks: } , := .parseQuery() if != nil { return nil, } if .peek().Kind != TokEOF { return nil, errf(.peek().Pos, "unexpected trailing input %q", .peek().Text) } return , nil } func ( *parser) () Token { return .toks[.i] } func ( *parser) ( int) Token { if .i+ >= len(.toks) { return .toks[len(.toks)-1] } return .toks[.i+] } func ( *parser) () Token { := .toks[.i]; .i++; return } // kwIs reports whether t is the case-insensitive keyword kw. func kwIs( Token, string) bool { return .Kind == TokIdent && strings.EqualFold(.Text, ) } // reservedMaxLen is the longest reserved word; isReserved short-circuits any // longer identifier without folding. const reservedMaxLen = 8 // foldLower ASCII-lowercases s into buf and returns buf[:len(s)]; ok is false // when s does not fit buf, in which case s equals none of the fixed keywords // the parser tests (all shorter) and the caller treats it as a plain // identifier. The result is a view into buf and must be used only in the // non-copying string idioms -- m[string(b)] lookups and string(b) switch/ // compare against constants -- never retained. GQL keywords are ASCII, so an // ASCII fold matches exactly what strings.ToLower would for keyword purposes. func foldLower( string, []byte) ( []byte, bool) { if len() > len() { return nil, false } for := 0; < len(); ++ { := [] if >= 'A' && <= 'Z' { += 'a' - 'A' } [] = } return [:len()], true } // isReserved reports whether s is a reserved word case-insensitively without // allocating: an ASCII fold into a stack buffer, then the m[string(b)] // no-copy map probe. Replaces reserved[strings.ToLower(s)], whose ToLower // allocated a lowercased copy of every identifier token examined. func isReserved( string) bool { var [reservedMaxLen]byte , := foldLower(, [:]) return && reserved[string()] } // peekKw reports whether the next token is the keyword kw. func ( *parser) ( string) bool { return kwIs(.peek(), ) } // acceptKw consumes the keyword kw if it is next. func ( *parser) ( string) bool { if .peekKw() { .i++ return true } return false } // expectKw consumes the keyword kw or fails. func ( *parser) ( string) *Error { if !.acceptKw() { return errf(.peek().Pos, "expected %s, found %q", strings.ToUpper(), .peek().Text) } return nil } // expect consumes a token of the given kind or fails. func ( *parser) ( TokKind, string) (Token, *Error) { if .peek().Kind != { return Token{}, errf(.peek().Pos, "expected %s, found %q", , .peek().Text) } return .next(), nil } // identName consumes a non-reserved identifier (a variable, label, key, or // alias name). func ( *parser) ( string) (string, *Error) { := .peek() if .Kind != TokIdent { return "", errf(.Pos, "expected %s, found %q", , .Text) } if isReserved(.Text) { return "", errf(.Pos, "reserved word %q cannot be used as %s", .Text, ) } .i++ return .Text, nil } // parseQuery is: [EXPLAIN|PROFILE] part (UNION [ALL] part)*. func ( *parser) () (*ast.Query, error) { := &ast.Query{} if .acceptKw("explain") { .Mode = ast.Explain } else if .acceptKw("profile") { .Mode = ast.Profile } , := .parsePart() if != nil { return nil, } .Parts = append(.Parts, *) for { var ast.UnionKind switch { case .acceptKw("union"): = ast.UnionDistinct if .acceptKw("all") { = ast.UnionAll } else { .acceptKw("distinct") // explicit DISTINCT = the default } case .acceptKw("except"): = ast.UnionExcept .acceptKw("distinct") case .acceptKw("intersect"): = ast.UnionIntersect .acceptKw("distinct") default: return , nil } , := .parsePart() if != nil { return nil, } .Union = append(.Union, ) .Parts = append(.Parts, *) } } // parsePart is one UNION branch: statements ending in a RETURN with no // NEXT. RETURN ... NEXT lowers to a With boundary and the part continues. func ( *parser) () (*ast.QueryPart, error) { := &ast.QueryPart{} for { := .peek() if .Kind != TokIdent { return nil, errf(.Pos, "expected a statement, found %q", .Text) } // Fold the clause keyword into a stack buffer (no per-clause // lowercased copy); a too-long token folds empty and hits the default. var [16]byte , := foldLower(.Text, [:]) if writeKeywords[string()] { return nil, errf(.Pos, "%s is not supported: this is a read-only engine", strings.ToUpper(.Text)) } switch string() { case "match", "optional": , := .parseMatch() if != nil { return nil, } .Clauses = append(.Clauses, ) case "filter": .i++ , := .parseExpr() if != nil { return nil, } .Clauses = append(.Clauses, &ast.With{Proj: ast.Projection{Star: true}, Where: }) case "let": , := .parseLet() if != nil { return nil, } .Clauses = append(.Clauses, ) case "for": , := .parseFor() if != nil { return nil, } .Clauses = append(.Clauses, ) case "call": , := .parseCall() if != nil { return nil, } .Clauses = append(.Clauses, ) case "order": // Standalone ORDER BY [OFFSET] [LIMIT]: sort (and cut) the // binding table mid-pipeline -- a star projection carrying // only the ordering, the GQL analogue of Cypher's // `WITH * ORDER BY ... LIMIT n`. := ast.Projection{Star: true} if := .parseProjectionTail(&); != nil { return nil, } .Clauses = append(.Clauses, &ast.With{Proj: }) case "return": .i++ , := .parseProjection() if != nil { return nil, } if .acceptKw("next") { .Clauses = append(.Clauses, &ast.With{Proj: *}) continue } .Ret = * return , nil case "next": // A bare NEXT between statements is the ISO statement // separator: the binding table flows forward unchanged, so // it is a no-op here (RETURN ... NEXT is the projecting // boundary). .i++ case "with": return nil, errf(.Pos, "WITH is not GQL: use RETURN ... NEXT (projection boundary), LET (bindings), or FILTER (predicate)") case "unwind": return nil, errf(.Pos, "UNWIND is not GQL: use FOR x IN <list>") default: return nil, errf(.Pos, "expected a statement (MATCH, FILTER, LET, FOR, CALL, ORDER BY, RETURN), found %q", .Text) } } } // parseLet is: LET x = expr [, y = expr]* -- a pass-through projection // (star) extended with the new bindings. func ( *parser) () (ast.Clause, error) { .i++ // LET := ast.Projection{Star: true} for { , := .identName("a LET binding name") if != nil { return nil, } if , := .expect(TokEq, "'='"); != nil { return nil, } , := .parseExpr() if != nil { return nil, } .Items = append(.Items, ast.ReturnItem{Expr: , Alias: }) if !.acceptTok(TokComma) { break } } return &ast.With{Proj: }, nil } // parseFor is: FOR var IN expr. func ( *parser) () (ast.Clause, error) { .i++ // FOR , := .identName("a FOR variable") if != nil { return nil, } if := .expectKw("in"); != nil { return nil, } , := .parseExpr() if != nil { return nil, } return &ast.Unwind{Expr: , Var: }, nil } // acceptTok consumes a token of the given kind if it is next. func ( *parser) ( TokKind) bool { if .peek().Kind == { .i++ return true } return false } // parseMatch is: [OPTIONAL] MATCH [mode] <body> [WHERE expr]. The body is // either comma-separated patterns, a path bind `p = [mode] <pattern>`, or // a path search `p = ANY|ALL SHORTEST <pattern>`; mode is a path-mode // prefix (TRAIL / ACYCLIC -- see parsePathMode). func ( *parser) () (ast.Clause, error) { := .acceptKw("optional") if := .expectKw("match"); != nil { return nil, } // Match modes: DIFFERENT EDGES is the engine default (accepted as a // no-op); REPEATABLE ELEMENTS switches the clause to walk semantics // (no relationship-uniqueness enforcement). := false switch { case .peekKw("repeatable") && kwIs(.peekAt(1), "elements"): .i += 2 = true case .peekKw("different") && kwIs(.peekAt(1), "edges"): .i += 2 } // `ident =` introduces a path binding (a pattern starts with '('). if .peek().Kind == TokIdent && !isReserved(.peek().Text) && .peekAt(1).Kind == TokEq { , := .identName("a path variable") .i++ // '=' , := .parsePathMode() if != nil { return nil, } := false := false switch { case .peekKw("any") && kwIs(.peekAt(1), "shortest"): .i += 2 = true case .peekKw("all") && kwIs(.peekAt(1), "shortest"): .i += 2 , = true, true case .peekKw("shortest"): return nil, errf(.peek().Pos, "bare SHORTEST is not supported: use ANY SHORTEST or ALL SHORTEST") case .peekKw("shortestpath"), .peekKw("allshortestpaths"), .peekKw("weightedshortestpath"): return nil, errf(.peek().Pos, "%s(...) is not GQL: write MATCH p = ANY SHORTEST / ALL SHORTEST <pattern> [COST <expr>]", .peek().Text) } if && { return nil, errf(.peek().Pos, "a path mode does not combine with ANY/ALL SHORTEST (the search normalizes the mode away)") } , := .parsePattern() if != nil { return nil, } var *ast.CostSpec if .peekKw("cost") { if ! { return nil, errf(.peek().Pos, "COST applies only to a path search: MATCH p = ANY SHORTEST <pattern> COST <expr>") } .i++ , = .parseCostSpec() if != nil { return nil, } } , := .parseOptionalWhere() if != nil { return nil, } if { return &ast.ShortestPath{PathVar: , Pattern: *, Optional: , All: , Weight: , Where: }, nil } return &ast.PathBind{PathVar: , Pattern: *, Optional: , Where: , Acyclic: }, nil } , := .parsePathMode() if != nil { return nil, } var []ast.Pattern for { , := .parsePattern() if != nil { return nil, } = append(, *) if !.acceptTok(TokComma) { break } } , := .parseOptionalWhere() if != nil { return nil, } return &ast.Match{Patterns: , Where: , Optional: , Acyclic: , Repeatable: }, nil } // parseCostSpec parses the COST <expr> weight of a weighted path search. // A numeric literal is a constant per-edge weight; any other expression is // a per-edge formula (the planner narrows `rel.prop` to a property read). func ( *parser) () (*ast.CostSpec, error) { , := .parseExpr() if != nil { return nil, } if , := .(*ast.Lit); { switch .Value.Kind { case ast.LitInt: return &ast.CostSpec{Kind: ast.CostConstant, Const: float64(.Value.I)}, nil case ast.LitFloat: return &ast.CostSpec{Kind: ast.CostConstant, Const: .Value.F}, nil } } return &ast.CostSpec{Kind: ast.CostExpr, Expr: }, nil } // parsePathMode consumes an optional path-mode prefix. TRAIL is accepted // as a no-op -- relationship-unique traversal is the engine's native // semantics; ACYCLIC additionally forbids repeated nodes within each // quantified segment. WALK (repeats allowed) and SIMPLE are rejected with // targeted errors. func ( *parser) () ( bool, error) { := .peek() if .Kind != TokIdent { return false, nil } var [8]byte , := foldLower(.Text, [:]) switch string() { case "trail": .i++ case "acyclic": .i++ return true, nil case "walk": return false, errf(.Pos, "the WALK path mode is not supported: traversal is TRAIL (no repeated relationship)") case "simple": return false, errf(.Pos, "the SIMPLE path mode is not supported (TRAIL and ACYCLIC are)") } return false, nil } // parseOptionalWhere parses a trailing WHERE expr if present. func ( *parser) () (ast.Expr, error) { if !.acceptKw("where") { return nil, nil } return .parseExpr() }