// The GQL lexer: whitespace-separated tokens, case-insensitive keywords // (classified by the parser, not here), quoted strings with quote // doubling and backslash escapes, scientific-notation floats, // and // /* */ comments, unicode identifiers, backtick-delimited identifiers, // and $name parameters.
package parser import ( ) // reserved is the reserved-word table: these never lex as identifiers used // for variables, labels, keys, or aliases. Extends the Rust engine's list // with the GQL statement keywords (filter/let/for/next/offset/shortest) // and the GQL write/catalog keywords, which are reserved so a write // statement fails with a clear read-only error instead of a generic one. var reserved = map[string]bool{ // Query structure (shared with the Rust engine's grammar). "distinct": true, "optional": true, "return": true, "exists": true, "order": true, "limit": true, "match": true, "where": true, "false": true, "yield": true, "with": true, "when": true, "then": true, "else": true, "case": true, "call": true, "skip": true, "null": true, "true": true, "desc": true, "asc": true, "end": true, "and": true, "not": true, "as": true, "by": true, "or": true, "is": true, "in": true, "union": true, "reduce": true, "unwind": true, // GQL statement keywords. "filter": true, "let": true, "for": true, "next": true, "offset": true, "shortest": true, // GQL write/catalog/session keywords -- reserved for clean rejection. "insert": true, "set": true, "delete": true, "remove": true, "create": true, "drop": true, "merge": true, "detach": true, "session": true, "commit": true, "rollback": true, } // writeKeywords are the reserved words that mark a write/catalog/session // statement; seeing one leads with the read-only explanation. var writeKeywords = map[string]bool{ "insert": true, "set": true, "delete": true, "remove": true, "create": true, "drop": true, "merge": true, "detach": true, "session": true, "commit": true, "rollback": true, } // lex tokenizes the whole input. The only error is an unterminated string. func lex( string) ([]Token, *Error) { := make([]Token, 0, len()/4+4) := 0 for < len() { := [] switch { case == ' ' || == '\t' || == '\r' || == '\n': ++ case == '/' && +1 < len() && [+1] == '/': // Line comment: to end of line. for < len() && [] != '\n' { ++ } case == '/' && +1 < len() && [+1] == '*': // Block comment (non-nesting), must terminate. := += 2 for +1 < len() && !([] == '*' && [+1] == '/') { ++ } if +1 >= len() { return nil, errf(, "unterminated block comment") } += 2 case isDigit(): := := false for < len() && isDigit([]) { ++ } // A float needs digits on both sides of a single dot; `1..3` // keeps the int and leaves the range dots. if +1 < len() && [] == '.' && isDigit([+1]) { = true ++ for < len() && isDigit([]) { ++ } } // Scientific notation: an exponent consumes only when digits // follow (with an optional sign), so `1e` stays int + ident. if < len() && ([] == 'e' || [] == 'E') { := + 1 if < len() && ([] == '+' || [] == '-') { ++ } if < len() && isDigit([]) { = true = for < len() && isDigit([]) { ++ } } } if { = append(, Token{TokFloat, [:], }) } else { = append(, Token{TokInt, [:], }) } case isIdentStart() || >= utf8.RuneSelf && identStartRune(, ): := for < len() { if isIdentChar([]) { ++ continue } if [] >= utf8.RuneSelf { , := utf8.DecodeRuneInString([:]) if unicode.IsLetter() || unicode.IsDigit() { += continue } } break } = append(, Token{TokIdent, [:], }) case == '`': // Backtick-delimited identifier: any characters except a // backtick (labels/keys with spaces or reserved spellings -- // the parser's reserved checks still apply by text). := ++ for < len() && [] != '`' { ++ } if >= len() { return nil, errf(, "unterminated delimited identifier") } if == +1 { return nil, errf(, "empty delimited identifier") } = append(, Token{TokIdent, [+1 : ], }) ++ case == '\'' || == '"': , , := lexString(, ) if != nil { return nil, } = append(, Token{TokStr, , }) = case == '$': := ++ for < len() && isIdentChar([]) { ++ } if == +1 { return nil, errf(, "empty parameter name after '$'") } = append(, Token{TokParam, [+1 : ], }) default: , , := punct(, ) if ! { return nil, errf(, "unexpected character %q", string()) } = append(, Token{, [ : +], }) += } } = append(, Token{TokEOF, "", len()}) return , nil } // punct matches punctuation/operators, longest first (<=, >=, <>, ..). func punct( string, int) (TokKind, int, bool) { if +1 < len() { switch [ : +2] { case "<=": return TokLte, 2, true case ">=": return TokGte, 2, true case "<>": return TokNeq, 2, true case "..": return TokDotDot, 2, true } } switch [] { case '(': return TokLParen, 1, true case ')': return TokRParen, 1, true case '[': return TokLBracket, 1, true case ']': return TokRBracket, 1, true case '{': return TokLBrace, 1, true case '}': return TokRBrace, 1, true case ',': return TokComma, 1, true case '.': return TokDot, 1, true case ':': return TokColon, 1, true case '|': return TokPipe, 1, true case '&': return TokAmp, 1, true case '!': return TokBang, 1, true case '=': return TokEq, 1, true case '<': return TokLt, 1, true case '>': return TokGt, 1, true case '+': return TokPlus, 1, true case '-': return TokMinus, 1, true case '*': return TokStar, 1, true case '/': return TokSlash, 1, true case '?': return TokQuestion, 1, true case '%': return TokPercent, 1, true } return TokEOF, 0, false } // lexString scans a quoted string starting at src[i], handling quote // doubling ('it”s') and backslash escapes (\\ \' \" \n \t \r, and // \uXXXX). The fast path (no escapes) returns a source slice; escapes // build once. An unknown escape is an error, not a silent literal. func lexString( string, int) (string, int, *Error) { := [] := ++ var *strings.Builder := := func( int) { if == nil { = &strings.Builder{} } .WriteString([:]) } for < len() { := [] switch { case == : // Doubled quote = literal quote; otherwise the terminator. if +1 < len() && [+1] == { () .WriteByte() += 2 = continue } if == nil { return [+1 : ], + 1, nil } .WriteString([:]) return .String(), + 1, nil case == '\\': if +1 >= len() { return "", 0, errf(, "unterminated escape") } () := [+1] switch { case '\\', '\'', '"', '`': .WriteByte() += 2 case 'n': .WriteByte('\n') += 2 case 't': .WriteByte('\t') += 2 case 'r': .WriteByte('\r') += 2 case 'u': if +6 > len() { return "", 0, errf(, "\\u needs four hex digits") } := rune(0) for , := range [+2 : +6] { := hexVal(byte()) if < 0 { return "", 0, errf(, "\\u needs four hex digits") } = <<4 | rune() } .WriteRune() += 6 default: return "", 0, errf(, "unknown string escape \\%c", ) } = default: ++ } } return "", 0, errf(, "unterminated string") } func hexVal( byte) int { switch { case >= '0' && <= '9': return int( - '0') case >= 'a' && <= 'f': return int(-'a') + 10 case >= 'A' && <= 'F': return int(-'A') + 10 } return -1 } // identStartRune reports whether the rune at src[i] can start an // identifier (unicode letter). func identStartRune( string, int) bool { , := utf8.DecodeRuneInString([:]) return unicode.IsLetter() } func isDigit( byte) bool { return >= '0' && <= '9' } func isIdentStart( byte) bool { return == '_' || ( >= 'a' && <= 'z') || ( >= 'A' && <= 'Z') } func isIdentChar( byte) bool { return isIdentStart() || isDigit() }