// 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 parserimport ()// 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 {returnnil, } := &parser{toks: } , := .parseQuery()if != nil {returnnil, }if .peek().Kind != TokEOF {returnnil, 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) {iflen() > len() {returnnil, 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 { returnkwIs(.peek(), ) }// acceptKw consumes the keyword kw if it is next.func ( *parser) ( string) bool {if .peekKw() { .i++returntrue }returnfalse}// expectKw consumes the keyword kw or fails.func ( *parser) ( string) *Error {if !.acceptKw() {returnerrf(.peek().Pos, "expected %s, found %q", strings.ToUpper(), .peek().Text) }returnnil}// expect consumes a token of the given kind or fails.func ( *parser) ( TokKind, string) (Token, *Error) {if .peek().Kind != {returnToken{}, 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) }ifisReserved(.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 } elseif .acceptKw("profile") { .Mode = ast.Profile } , := .parsePart()if != nil {returnnil, } .Parts = append(.Parts, *)for {varast.UnionKindswitch {case .acceptKw("union"): = ast.UnionDistinctif .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 {returnnil, } .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 {returnnil, 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, [:])ifwriteKeywords[string()] {returnnil, errf(.Pos, "%s is not supported: this is a read-only engine", strings.ToUpper(.Text)) }switchstring() {case"match", "optional": , := .parseMatch()if != nil {returnnil, } .Clauses = append(.Clauses, )case"filter": .i++ , := .parseExpr()if != nil {returnnil, } .Clauses = append(.Clauses, &ast.With{Proj: ast.Projection{Star: true}, Where: })case"let": , := .parseLet()if != nil {returnnil, } .Clauses = append(.Clauses, )case"for": , := .parseFor()if != nil {returnnil, } .Clauses = append(.Clauses, )case"call": , := .parseCall()if != nil {returnnil, } .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 {returnnil, } .Clauses = append(.Clauses, &ast.With{Proj: })case"return": .i++ , := .parseProjection()if != nil {returnnil, }if .acceptKw("next") { .Clauses = append(.Clauses, &ast.With{Proj: *})continue } .Ret = *return , nilcase"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":returnnil, errf(.Pos, "WITH is not GQL: use RETURN ... NEXT (projection boundary), LET (bindings), or FILTER (predicate)")case"unwind":returnnil, errf(.Pos, "UNWIND is not GQL: use FOR x IN <list>")default:returnnil, 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 {returnnil, }if , := .expect(TokEq, "'='"); != nil {returnnil, } , := .parseExpr()if != nil {returnnil, } .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 {returnnil, }if := .expectKw("in"); != nil {returnnil, } , := .parseExpr()if != nil {returnnil, }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++returntrue }returnfalse}// 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 {returnnil, }// 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). := falseswitch {case .peekKw("repeatable") && kwIs(.peekAt(1), "elements"): .i += 2 = truecase .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 {returnnil, } := false := falseswitch {case .peekKw("any") && kwIs(.peekAt(1), "shortest"): .i += 2 = truecase .peekKw("all") && kwIs(.peekAt(1), "shortest"): .i += 2 , = true, truecase .peekKw("shortest"):returnnil, errf(.peek().Pos, "bare SHORTEST is not supported: use ANY SHORTEST or ALL SHORTEST")case .peekKw("shortestpath"), .peekKw("allshortestpaths"), .peekKw("weightedshortestpath"):returnnil, errf(.peek().Pos, "%s(...) is not GQL: write MATCH p = ANY SHORTEST / ALL SHORTEST <pattern> [COST <expr>]", .peek().Text) }if && {returnnil, errf(.peek().Pos, "a path mode does not combine with ANY/ALL SHORTEST (the search normalizes the mode away)") } , := .parsePattern()if != nil {returnnil, }var *ast.CostSpecif .peekKw("cost") {if ! {returnnil, errf(.peek().Pos, "COST applies only to a path search: MATCH p = ANY SHORTEST <pattern> COST <expr>") } .i++ , = .parseCostSpec()if != nil {returnnil, } } , := .parseOptionalWhere()if != nil {returnnil, }if {return &ast.ShortestPath{PathVar: , Pattern: *, Optional: , All: , Weight: , Where: }, nil }return &ast.PathBind{PathVar: , Pattern: *, Optional: , Where: , Acyclic: }, nil } , := .parsePathMode()if != nil {returnnil, }var []ast.Patternfor { , := .parsePattern()if != nil {returnnil, } = append(, *)if !.acceptTok(TokComma) {break } } , := .parseOptionalWhere()if != nil {returnnil, }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 {returnnil, }if , := .(*ast.Lit); {switch .Value.Kind {caseast.LitInt:return &ast.CostSpec{Kind: ast.CostConstant, Const: float64(.Value.I)}, nilcaseast.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 {returnfalse, nil }var [8]byte , := foldLower(.Text, [:])switchstring() {case"trail": .i++case"acyclic": .i++returntrue, nilcase"walk":returnfalse, errf(.Pos, "the WALK path mode is not supported: traversal is TRAIL (no repeated relationship)")case"simple":returnfalse, errf(.Pos, "the SIMPLE path mode is not supported (TRAIL and ACYCLIC are)") }returnfalse, nil}// parseOptionalWhere parses a trailing WHERE expr if present.func ( *parser) () (ast.Expr, error) {if !.acceptKw("where") {returnnil, nil }return .parseExpr()}
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.