blob: a045fb5616d0911b1509a82c5ff16200514a4a1b [file] [edit]
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package parser
import (
"fmt"
"cel.dev/cel-go/common/runes"
)
type tokenKind int
const (
tokError tokenKind = iota
tokEnd
tokWhitespace
tokComment
// Keywords
tokNull
tokFalse
tokTrue
tokIn
tokReservedWord
// Literals
tokInt
tokUint
tokFloat
tokString
tokBytes
// Identifiers
tokIdent
// Delimiters
tokLeftBracket // [
tokRightBracket // ]
tokLeftBrace // {
tokRightBrace // }
tokLeftParen // (
tokRightParen // )
// Operators
tokDot // .
tokComma // ,
tokMinus // -
tokPlus // +
tokAsterisk // *
tokSlash // /
tokPercent // %
tokQuestion // ?
tokColon // :
tokExclamation // !
tokEqual // =
tokEqualEqual // ==
tokExclamationEqual // !=
tokLess // <
tokLessEqual // <=
tokGreater // >
tokGreaterEqual // >=
tokLogicalAnd // &&
tokLogicalOr // ||
)
func (t tokenKind) String() string {
switch t {
case tokError:
return "error"
case tokEnd:
return "end"
case tokWhitespace:
return "whitespace"
case tokComment:
return "comment"
case tokNull:
return "null"
case tokFalse:
return "false"
case tokTrue:
return "true"
case tokIn:
return "in"
case tokReservedWord:
return "reserved_word"
case tokInt:
return "int"
case tokUint:
return "uint"
case tokFloat:
return "float"
case tokString:
return "string"
case tokBytes:
return "bytes"
case tokIdent:
return "ident"
case tokLeftBracket:
return "["
case tokRightBracket:
return "]"
case tokLeftBrace:
return "{"
case tokRightBrace:
return "}"
case tokLeftParen:
return "("
case tokRightParen:
return ")"
case tokDot:
return "."
case tokComma:
return ","
case tokMinus:
return "-"
case tokPlus:
return "+"
case tokAsterisk:
return "*"
case tokSlash:
return "/"
case tokPercent:
return "%"
case tokQuestion:
return "?"
case tokColon:
return ":"
case tokExclamation:
return "!"
case tokEqual:
return "="
case tokEqualEqual:
return "=="
case tokExclamationEqual:
return "!="
case tokLess:
return "<"
case tokLessEqual:
return "<="
case tokGreater:
return ">"
case tokGreaterEqual:
return ">="
case tokLogicalAnd:
return "&&"
case tokLogicalOr:
return "||"
default:
return "<unknown>"
}
}
type token struct {
kind tokenKind
start int32
end int32
}
type lexerError struct {
start int32
end int32
message string
}
type lexerPosition struct {
pos int32
atEnd bool
done bool
err lexerError
}
var keywords = map[string]tokenKind{
"false": tokFalse,
"true": tokTrue,
"null": tokNull,
"in": tokIn,
"as": tokReservedWord,
"break": tokReservedWord,
"const": tokReservedWord,
"continue": tokReservedWord,
"else": tokReservedWord,
"for": tokReservedWord,
"function": tokReservedWord,
"if": tokReservedWord,
"import": tokReservedWord,
"let": tokReservedWord,
"loop": tokReservedWord,
"package": tokReservedWord,
"namespace": tokReservedWord,
"return": tokReservedWord,
"var": tokReservedWord,
"void": tokReservedWord,
"while": tokReservedWord,
}
func isIdentTrailing(r rune) bool {
return r <= 0x7f && ((r >= '0' && r <= '9') || (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || r == '_')
}
func isDigit(r rune) bool {
return r >= '0' && r <= '9'
}
func isHexDigit(r rune) bool {
return (r >= '0' && r <= '9') || (r >= 'a' && r <= 'f') || (r >= 'A' && r <= 'F')
}
func isAlpha(r rune) bool {
return (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z')
}
func isPlusOrMinus(r rune) bool {
return r == '+' || r == '-'
}
// lexer performs fast tokenization of CEL expression source code.
type lexer struct {
content runes.Buffer
length int32
pos int32
atEnd bool
done bool
err lexerError
}
func newLexer(content runes.Buffer) *lexer {
return &lexer{
content: content,
length: int32(content.Len()),
pos: 0,
}
}
func (l *lexer) SavePosition() lexerPosition {
return lexerPosition{
pos: l.pos,
atEnd: l.atEnd,
done: l.done,
err: l.err,
}
}
func (l *lexer) RestorePosition(p lexerPosition) {
l.pos = p.pos
l.atEnd = p.atEnd
l.done = p.done
l.err = p.err
}
func (l *lexer) GetError() lexerError {
return l.err
}
func (l *lexer) GetPosition() int32 {
return l.pos
}
func (l *lexer) makeToken(kind tokenKind, start, end int32) token {
if l.atEnd {
l.done = true
}
return token{kind: kind, start: start, end: end}
}
func (l *lexer) setError(start, end int32, msg string) token {
l.err = lexerError{start: start, end: end, message: msg}
return token{kind: tokError, start: start, end: end}
}
func (l *lexer) advance(n int32) {
l.pos += n
}
func (l *lexer) match(c rune) bool {
return l.pos < l.length && l.content.Get(int(l.pos)) == c
}
func (l *lexer) matchIgnoreCase(c rune) bool {
if l.pos >= l.length {
return false
}
cp := l.content.Get(int(l.pos))
if cp <= 0x7f && c <= 0x7f {
cpLower := cp
if cp >= 'A' && cp <= 'Z' {
cpLower += 'a' - 'A'
}
cLower := c
if c >= 'A' && c <= 'Z' {
cLower += 'a' - 'A'
}
return cpLower == cLower
}
return cp == c
}
func (l *lexer) consume(c rune) bool {
if l.match(c) {
l.advance(1)
return true
}
return false
}
func (l *lexer) consumeIgnoreCase(c rune) bool {
if l.matchIgnoreCase(c) {
l.advance(1)
return true
}
return false
}
func (l *lexer) consumeIf(predicate func(rune) bool) bool {
if l.pos < l.length && predicate(l.content.Get(int(l.pos))) {
l.advance(1)
return true
}
return false
}
func (l *lexer) consumeLine() {
for l.pos < l.length {
if l.content.Get(int(l.pos)) == '\n' {
l.advance(1)
return
}
l.advance(1)
}
}
func (l *lexer) consumeWhitespace() {
for l.pos < l.length {
c := l.content.Get(int(l.pos))
switch c {
case '\f', '\n', ' ', '\r', '\v', '\t':
l.advance(1)
default:
return
}
}
}
func (l *lexer) consumeDigits() bool {
advanced := false
for l.pos < l.length {
c := l.content.Get(int(l.pos))
if !isDigit(c) {
break
}
l.advance(1)
advanced = true
}
return advanced
}
func (l *lexer) consumeHexDigits() bool {
advanced := false
for l.pos < l.length {
c := l.content.Get(int(l.pos))
if !isHexDigit(c) {
break
}
l.advance(1)
advanced = true
}
return advanced
}
func (l *lexer) consumeIntegralSuffix() tokenKind {
if l.consumeIgnoreCase('u') {
return tokUint
}
return tokInt
}
func (l *lexer) consumeUntilAfter(c rune) bool {
for pos := l.pos; pos < l.length; pos++ {
if l.content.Get(int(pos)) == c {
l.pos = pos + 1
return true
}
}
l.pos = l.length
return false
}
func (l *lexer) consumeUntilAfterTriple(quote rune) bool {
pos := l.pos
for pos+3 <= l.length {
if l.content.Get(int(pos)) == quote &&
l.content.Get(int(pos+1)) == quote &&
l.content.Get(int(pos+2)) == quote {
l.pos = pos + 3
return true
}
pos++
}
l.pos = l.length
return false
}
func (l *lexer) consumeUntilAfterUnescaped(c rune) bool {
pos := l.pos
escaped := false
for pos < l.length {
cc := l.content.Get(int(pos))
if cc == '\\' {
escaped = !escaped
} else {
if cc == c && !escaped {
l.pos = pos + 1
return true
}
escaped = false
}
pos++
}
l.pos = l.length
return false
}
func (l *lexer) consumeUntilAfterUnescapedTriple(quote rune) bool {
pos := l.pos
escaped := false
for pos < l.length {
cc := l.content.Get(int(pos))
if cc == '\\' {
escaped = !escaped
} else {
if !escaped && pos+3 <= l.length {
if l.content.Get(int(pos)) == quote &&
l.content.Get(int(pos+1)) == quote &&
l.content.Get(int(pos+2)) == quote {
l.pos = pos + 3
return true
}
}
escaped = false
}
pos++
}
l.pos = l.length
return false
}
func (l *lexer) consumeQuotedIdent() token {
start := l.pos
l.advance(1)
if !l.consumeUntilAfter('`') {
return l.setError(start, l.pos, "unterminated quoted identifier")
}
return l.makeToken(tokIdent, start, l.pos)
}
func (l *lexer) consumeStringLiteral(start int32, quote rune, isBytes, isRaw bool) token {
l.advance(1)
if l.pos+2 <= l.length && l.content.Get(int(l.pos)) == quote && l.content.Get(int(l.pos+1)) == quote {
l.advance(2)
var found bool
if isRaw {
found = l.consumeUntilAfterTriple(quote)
} else {
found = l.consumeUntilAfterUnescapedTriple(quote)
}
if !found {
msg := "unterminated string literal"
if isBytes {
msg = "unterminated bytes literal"
}
return l.setError(start, l.pos, msg)
}
kind := tokString
if isBytes {
kind = tokBytes
}
return l.makeToken(kind, start, l.pos)
}
var found bool
if isRaw {
found = l.consumeUntilAfter(quote)
} else {
found = l.consumeUntilAfterUnescaped(quote)
}
if !found {
msg := "unterminated string literal"
if isBytes {
msg = "unterminated bytes literal"
}
return l.setError(start, l.pos, msg)
}
kind := tokString
if isBytes {
kind = tokBytes
}
return l.makeToken(kind, start, l.pos)
}
func (l *lexer) consumePrefixedStringLiteral() (token, bool) {
start := l.pos
if l.pos >= l.length {
return token{}, false
}
c := l.content.Get(int(l.pos))
isBytes := (c == 'b' || c == 'B')
isRaw := (c == 'r' || c == 'R')
lookahead := int32(1)
if l.pos+1 < l.length {
c2 := l.content.Get(int(l.pos + 1))
if (isBytes && (c2 == 'r' || c2 == 'R')) || (!isBytes && (c2 == 'b' || c2 == 'B')) {
isBytes = true
isRaw = true
lookahead = 2
}
}
if l.pos+lookahead < l.length {
quote := l.content.Get(int(l.pos + lookahead))
if quote == '"' || quote == '\'' {
l.advance(lookahead)
return l.consumeStringLiteral(start, quote, isBytes, isRaw), true
}
}
return token{}, false
}
func (l *lexer) consumeNumericLiteral() token {
start := l.pos
c := l.content.Get(int(l.pos))
floatingPoint := false
if c == '.' {
floatingPoint = true
l.advance(1)
if !l.consumeDigits() {
return l.setError(start, l.pos, "floating point literal missing digits after decimal separator")
}
} else {
l.advance(1)
if c == '0' {
if l.consumeIgnoreCase('x') {
if !l.consumeHexDigits() {
return l.setError(start, l.pos, "integral literal missing digits after hexadecimal separator")
}
tokType := l.consumeIntegralSuffix()
if l.consumeIf(isIdentTrailing) {
return l.setError(start, l.pos, fmt.Sprintf("%s literal has unexpected trailing characters", tokType))
}
return l.makeToken(tokType, start, l.pos)
}
}
_ = l.consumeDigits()
if l.pos < l.length && l.content.Get(int(l.pos)) == '.' &&
l.pos+1 < l.length && isDigit(l.content.Get(int(l.pos+1))) {
floatingPoint = true
l.advance(1)
_ = l.consumeDigits()
}
}
if l.consumeIgnoreCase('e') {
floatingPoint = true
_ = l.consumeIf(isPlusOrMinus)
if !l.consumeDigits() {
return l.setError(start, l.pos, "floating point literal missing digits after exponent separator")
}
}
var tokType tokenKind
if floatingPoint {
tokType = tokFloat
} else {
tokType = l.consumeIntegralSuffix()
}
if l.consumeIf(isIdentTrailing) {
return l.setError(start, l.pos, fmt.Sprintf("%s literal has unexpected trailing characters", tokType))
}
return l.makeToken(tokType, start, l.pos)
}
func (l *lexer) consumeIdent() token {
start := l.pos
for l.pos < l.length {
c := l.content.Get(int(l.pos))
if !isIdentTrailing(c) {
break
}
l.advance(1)
}
end := l.pos
word := l.content.Slice(int(start), int(end))
if kind, ok := keywords[word]; ok {
return l.makeToken(kind, start, end)
}
return l.makeToken(tokIdent, start, end)
}
// Lex scans and returns the next token from the source.
func (l *lexer) Lex() token {
start := l.pos
if l.pos >= l.length {
l.atEnd = true
l.done = true
return l.makeToken(tokEnd, start, start)
}
c := l.content.Get(int(l.pos))
switch c {
case '\f', '\v', '\t', '\r', '\n', ' ':
l.consumeWhitespace()
return l.makeToken(tokWhitespace, start, l.pos)
case '.':
if l.pos+1 < l.length && isDigit(l.content.Get(int(l.pos+1))) {
return l.consumeNumericLiteral()
}
l.advance(1)
return l.makeToken(tokDot, start, l.pos)
case ',':
l.advance(1)
return l.makeToken(tokComma, start, l.pos)
case '!':
l.advance(1)
if l.consume('=') {
return l.makeToken(tokExclamationEqual, start, l.pos)
}
return l.makeToken(tokExclamation, start, l.pos)
case '?':
l.advance(1)
return l.makeToken(tokQuestion, start, l.pos)
case '(':
l.advance(1)
return l.makeToken(tokLeftParen, start, l.pos)
case ')':
l.advance(1)
return l.makeToken(tokRightParen, start, l.pos)
case '{':
l.advance(1)
return l.makeToken(tokLeftBrace, start, l.pos)
case '}':
l.advance(1)
return l.makeToken(tokRightBrace, start, l.pos)
case '[':
l.advance(1)
return l.makeToken(tokLeftBracket, start, l.pos)
case ']':
l.advance(1)
return l.makeToken(tokRightBracket, start, l.pos)
case '=':
l.advance(1)
if l.consume('=') {
return l.makeToken(tokEqualEqual, start, l.pos)
}
return l.makeToken(tokEqual, start, l.pos)
case '<':
l.advance(1)
if l.consume('=') {
return l.makeToken(tokLessEqual, start, l.pos)
}
return l.makeToken(tokLess, start, l.pos)
case '>':
l.advance(1)
if l.consume('=') {
return l.makeToken(tokGreaterEqual, start, l.pos)
}
return l.makeToken(tokGreater, start, l.pos)
case ':':
l.advance(1)
return l.makeToken(tokColon, start, l.pos)
case '%':
l.advance(1)
return l.makeToken(tokPercent, start, l.pos)
case '+':
l.advance(1)
return l.makeToken(tokPlus, start, l.pos)
case '-':
l.advance(1)
return l.makeToken(tokMinus, start, l.pos)
case '*':
l.advance(1)
return l.makeToken(tokAsterisk, start, l.pos)
case '/':
l.advance(1)
if l.consume('/') {
l.consumeLine()
return l.makeToken(tokComment, start, l.pos)
}
return l.makeToken(tokSlash, start, l.pos)
case '&':
l.advance(1)
if l.consume('&') {
return l.makeToken(tokLogicalAnd, start, l.pos)
}
return l.setError(start, l.pos, "unexpected single '&', expected '&&'")
case '|':
l.advance(1)
if l.consume('|') {
return l.makeToken(tokLogicalOr, start, l.pos)
}
return l.setError(start, l.pos, "unexpected single '|', expected '||'")
case '_':
return l.consumeIdent()
case '`':
return l.consumeQuotedIdent()
case '\'':
return l.consumeStringLiteral(start, '\'', false, false)
case '"':
return l.consumeStringLiteral(start, '"', false, false)
case 'r', 'R', 'b', 'B':
if tok, ok := l.consumePrefixedStringLiteral(); ok {
return tok
}
}
if isDigit(c) {
return l.consumeNumericLiteral()
}
if isAlpha(c) {
return l.consumeIdent()
}
l.advance(1)
return l.setError(start, l.pos, "unexpected character")
}