// The Pratt expression parser. Precedence, loosest to tightest (the Rust // engine's table exactly): OR < AND < NOT < comparisons / IN / STARTS WITH // / ENDS WITH / CONTAINS < + - < * / < unary - < postfix (IS [NOT] NULL, // slice, index, .prop, :Label). NOT binds looser than comparisons, so // NOT a = b reads NOT (a = b).
package parser import ( ) // Binding powers; an infix binds into the left operand while its power // exceeds the context minimum (left associativity). const ( bpOr = 1 bpXor = 2 bpAnd = 3 bpNot = 4 bpCmp = 5 bpAdd = 6 bpMul = 7 bpNeg = 8 ) // parseExpr parses one full expression. func ( *parser) () (ast.Expr, error) { return .parseBP(0) } // parseBP parses an expression whose infix operators all bind tighter // than minBP. func ( *parser) ( int) (ast.Expr, error) { var ast.Expr var error switch { case .peekKw("not"): .i++ , := .(bpNot - 1) if != nil { return nil, } = &ast.Unary{Op: ast.Not, Expr: } case .peek().Kind == TokMinus: .i++ , := .(bpNeg - 1) if != nil { return nil, } = foldNeg() default: , = .parsePrimary() if != nil { return nil, } , = .parsePostfix() if != nil { return nil, } } for { , , , , := .peekInfix() if ! || <= { return , nil } := .peek().Kind == TokPercent .i += , := .() if != nil { return nil, } switch { case : // `a % b` is parse-time sugar for mod(a, b) -- one evaluation // path, multiplicative precedence. = &ast.Func{Name: "mod", Args: []ast.Expr{, }} case : = &ast.In{Expr: , List: } default: = &ast.Binary{Op: , LHS: , RHS: } } } } // foldNeg folds a unary minus over a numeric literal into the literal, so // the constant-matching paths see a negative value as one Lit rather than a // Unary they decline: an inline prop {balance: -50} classifies as a Props // seek instead of a desugared post-scan filter, and a WHERE bound a.x < -5 // auto-parameterizes into the shared plan template. Non-literal operands // (-a.x, -(a + 1), -$p) stay a runtime Unary. The MinInt64 magnitude also // stays folded to its recovered literal rather than re-negated: -(2^63) is // MinInt64 exactly, and negating MinInt64 would overflow, so it is left to // the overflow-to-null eval policy (mirrors the Rust parser). func foldNeg( ast.Expr) ast.Expr { , := .(*ast.Lit) if ! { return &ast.Unary{Op: ast.Neg, Expr: } } switch .Value.Kind { case ast.LitInt: if .Value.I == math.MinInt64 { return } return &ast.Lit{Value: ast.IntLit(-.Value.I)} case ast.LitFloat: return &ast.Lit{Value: ast.FloatLit(-.Value.F)} default: return &ast.Unary{Op: ast.Neg, Expr: } } } // peekInfix classifies the next token(s) as an infix operator: its BinOp, // binding power, token width (2 for STARTS WITH / ENDS WITH), and whether // it is the IN membership operator. func ( *parser) () ( ast.BinOp, , int, , bool) { := .peek() switch .Kind { case TokIdent: // A case-insensitive keyword match without allocating: peekInfix runs // at every operand boundary, and most tokens here are clause keywords // (RETURN, ORDER, ...) that match nothing, so a lowercased copy per // probe was pure waste. EqualFold short-circuits on length. switch { case kwIs(, "or"): return ast.OpOr, bpOr, 1, false, true case kwIs(, "xor"): return ast.OpXor, bpXor, 1, false, true case kwIs(, "and"): return ast.OpAnd, bpAnd, 1, false, true case kwIs(, "in"): return 0, bpCmp, 1, true, true case kwIs(, "starts"): if kwIs(.peekAt(1), "with") { return ast.OpStartsWith, bpCmp, 2, false, true } case kwIs(, "ends"): if kwIs(.peekAt(1), "with") { return ast.OpEndsWith, bpCmp, 2, false, true } case kwIs(, "contains"): return ast.OpContains, bpCmp, 1, false, true } case TokEq: return ast.OpEq, bpCmp, 1, false, true case TokNeq: return ast.OpNeq, bpCmp, 1, false, true case TokLt: return ast.OpLt, bpCmp, 1, false, true case TokLte: return ast.OpLte, bpCmp, 1, false, true case TokGt: return ast.OpGt, bpCmp, 1, false, true case TokGte: return ast.OpGte, bpCmp, 1, false, true case TokPlus: return ast.OpAdd, bpAdd, 1, false, true case TokMinus: return ast.OpSub, bpAdd, 1, false, true case TokStar: return ast.OpMul, bpMul, 1, false, true case TokSlash: return ast.OpDiv, bpMul, 1, false, true case TokPercent: return ast.OpMul, bpMul, 1, false, true // op unused: %% builds mod(a,b) case TokPipe: if .peekAt(1).Kind == TokPipe { return ast.OpConcat, bpAdd, 2, false, true } } return 0, 0, 0, false, false } // parsePostfix applies the postfix operators to a parsed primary: IS [NOT] // NULL, [index], [from..to] slices, .prop, and the :Label predicate. A '{' // here is a map projection, which is not in the GQL subset. func ( *parser) ( ast.Expr) (ast.Expr, error) { for { := .peek() switch { case kwIs(, "is"): .i++ := .acceptKw("not") if .acceptKw("labeled") { // IS [NOT] LABELED <labelexpr> -- the GQL spelling of the // label predicate; desugars to the ':' postfix form. , := .parseLabelOr() if != nil { return nil, } , := .(*ast.Var) if ! { return nil, errf(.Pos, "IS LABELED must apply to a variable (e.g. n IS LABELED Comment)") } var ast.Expr = &ast.HasLabelExpr{Var: .Name, Expr: } if { = &ast.Unary{Op: ast.Not, Expr: } } = continue } if .acceptKw("true") { = &ast.IsTruth{Expr: , Want: true, Negated: } continue } if .acceptKw("false") { = &ast.IsTruth{Expr: , Want: false, Negated: } continue } if .acceptKw("unknown") { // UNKNOWN is the null truth value: IS UNKNOWN == IS NULL. = &ast.IsNull{Expr: , Negated: } continue } // IS [NOT] [form] NORMALIZED lowers to the is_normalized // function (Func nodes flow through every generic pass -- // binder, fingerprint, autoparam, compile -- untouched). if .acceptKw("normalized") { = isNormalizedExpr(, "NFC", ) continue } if , := .acceptNormFormKw(); { if !.acceptKw("normalized") { return nil, errf(.peek().Pos, "expected NORMALIZED after IS %s", ) } = isNormalizedExpr(, , ) continue } if .acceptKw("typed") { , := .identName("a type name after IS TYPED") if != nil { return nil, } := strings.ToLower() switch { case "int", "integer", "bigint": = "integer" case "float", "double": = "float" case "string", "varchar": = "string" case "bool", "boolean": = "boolean" case "list", "array": = "list" case "node", "vertex": = "node" case "relationship", "edge": = "relationship" default: return nil, errf(.peek().Pos, "IS TYPED %s is not supported (INTEGER, FLOAT, STRING, BOOLEAN, LIST, NODE, RELATIONSHIP)", ) } = &ast.IsTyped{Expr: , Kind: , Negated: } continue } if !.acceptKw("null") { return nil, errf(.peek().Pos, "expected NULL, TRUE, FALSE, UNKNOWN, TYPED, LABELED, or [form] NORMALIZED after IS") } = &ast.IsNull{Expr: , Negated: } case .Kind == TokLBracket: var error , = .parseIndexOrSlice() if != nil { return nil, } case .Kind == TokDot: .i++ , := .identName("a property key") if != nil { return nil, } if , := .(*ast.Var); { = &ast.Prop{Var: .Name, Key: } } else { = &ast.PropOf{Base: , Key: } } case .Kind == TokColon: .i++ , := .parseLabelOr() if != nil { return nil, } , := .(*ast.Var) if ! { return nil, errf(.Pos, "label predicate ':' must apply to a variable (e.g. n:Label)") } = &ast.HasLabelExpr{Var: .Name, Expr: } case .Kind == TokLBrace: return nil, errf(.Pos, "map projections (var{.key}) are not in the GQL subset: project properties explicitly") default: return , nil } } } // parseIndexOrSlice parses base[index] or base[from..to] (either slice // bound optional). func ( *parser) ( ast.Expr) (ast.Expr, error) { .i++ // '[' // A leading '..' is a from-less slice. if .acceptTok(TokDotDot) { return .finishSlice(, nil) } , := .parseExpr() if != nil { return nil, } if .acceptTok(TokDotDot) { return .finishSlice(, ) } if , := .expect(TokRBracket, "']' closing an index"); != nil { return nil, } return &ast.Index{Base: , Idx: }, nil } // finishSlice parses the optional upper bound and the closing bracket. func ( *parser) ( ast.Expr, ast.Expr) (ast.Expr, error) { var ast.Expr if .peek().Kind != TokRBracket { , := .parseExpr() if != nil { return nil, } = } if , := .expect(TokRBracket, "']' closing a slice"); != nil { return nil, } return &ast.Slice{Base: , From: , To: }, nil } // acceptNormFormKw accepts a Unicode normalization form keyword (NFC, // NFD, NFKC, NFKD), returning its canonical name. func ( *parser) () (string, bool) { for , := range [...]string{"nfc", "nfd", "nfkc", "nfkd"} { if .acceptKw() { return strings.ToUpper(), true } } return "", false } // isNormalizedExpr lowers x IS [NOT] [form] NORMALIZED to the // is_normalized function (negation as a plain NOT wrapper). func isNormalizedExpr( ast.Expr, string, bool) ast.Expr { var ast.Expr = &ast.Func{Name: "is_normalized", Args: []ast.Expr{, &ast.Lit{Value: ast.StrLit()}}} if { = &ast.Unary{Op: ast.Not, Expr: } } return }