feat(expr): add the lexer
- commit
- 1facd5247420c7ccb5320fc9e9c35ecfe65eccd6
- author
- brice · · 2026-10-12 14:46 UTC
- parent
- 5904904
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +466 -0
A expr/errors.go +107 -0
| @@ -0,0 +1,107 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | ||
| 3 | + | package expr | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "fmt" | |
| 7 | + | ) | |
| 8 | + | ||
| 9 | + | // Limits of the reference implementation. | |
| 10 | + | const ( | |
| 11 | + | // MaxParserDepth is the deepest nesting of groupings, function arguments, | |
| 12 | + | // index accesses, negations and logical operators the parser accepts. | |
| 13 | + | MaxParserDepth = 50 | |
| 14 | + | // MaxExpressionLength is the longest expression accepted, counted in | |
| 15 | + | // UTF-16 code units like the JavaScript string length it mirrors. | |
| 16 | + | MaxExpressionLength = 21000 | |
| 17 | + | ) | |
| 18 | + | ||
| 19 | + | // ErrorType classifies an [Error]. | |
| 20 | + | type ErrorType int | |
| 21 | + | ||
| 22 | + | // The error types of the reference implementation, in its order. | |
| 23 | + | const ( | |
| 24 | + | ErrorUnexpectedSymbol ErrorType = iota | |
| 25 | + | // ErrorUnrecognizedNamedValue is declared by the reference but never | |
| 26 | + | // produced; ErrorUnrecognizedContext carries the same text. | |
| 27 | + | ErrorUnrecognizedNamedValue | |
| 28 | + | ErrorUnexpectedEndOfExpression | |
| 29 | + | ErrorExceededMaxDepth | |
| 30 | + | ErrorExceededMaxLength | |
| 31 | + | ErrorTooFewParameters | |
| 32 | + | ErrorTooManyParameters | |
| 33 | + | ErrorEvenParameters | |
| 34 | + | ErrorUnrecognizedContext | |
| 35 | + | ErrorUnrecognizedFunction | |
| 36 | + | ) | |
| 37 | + | ||
| 38 | + | func (t ErrorType) description() string { | |
| 39 | + | switch t { | |
| 40 | + | case ErrorUnexpectedEndOfExpression: | |
| 41 | + | return "Unexpected end of expression" | |
| 42 | + | case ErrorUnexpectedSymbol: | |
| 43 | + | return "Unexpected symbol" | |
| 44 | + | case ErrorUnrecognizedNamedValue: | |
| 45 | + | return "Unrecognized named-value" | |
| 46 | + | case ErrorExceededMaxDepth: | |
| 47 | + | return fmt.Sprintf("Exceeded max expression depth %d", MaxParserDepth) | |
| 48 | + | case ErrorExceededMaxLength: | |
| 49 | + | return fmt.Sprintf("Exceeded max expression length %d", MaxExpressionLength) | |
| 50 | + | case ErrorTooFewParameters: | |
| 51 | + | return "Too few parameters supplied" | |
| 52 | + | case ErrorTooManyParameters: | |
| 53 | + | return "Too many parameters supplied" | |
| 54 | + | case ErrorEvenParameters: | |
| 55 | + | return "Even number of parameters supplied, requires an odd number of parameters" | |
| 56 | + | case ErrorUnrecognizedContext: | |
| 57 | + | return "Unrecognized named-value" | |
| 58 | + | case ErrorUnrecognizedFunction: | |
| 59 | + | return "Unrecognized function" | |
| 60 | + | } | |
| 61 | + | return "Unknown error" | |
| 62 | + | } | |
| 63 | + | ||
| 64 | + | // Position is a zero-based line and column in an expression. Columns count | |
| 65 | + | // UTF-16 code units, as in the reference implementation. | |
| 66 | + | type Position struct { | |
| 67 | + | Line int | |
| 68 | + | Column int | |
| 69 | + | } | |
| 70 | + | ||
| 71 | + | // Error is a lexing or parsing error. Its message is exactly the one the | |
| 72 | + | // reference implementation produces. | |
| 73 | + | type Error struct { | |
| 74 | + | Type ErrorType | |
| 75 | + | // Message is the full text, e.g. "Unexpected symbol: ')'". | |
| 76 | + | Message string | |
| 77 | + | // Pos is where the offending token starts. | |
| 78 | + | Pos Position | |
| 79 | + | // Token is the text of the offending token. | |
| 80 | + | Token string | |
| 81 | + | } | |
| 82 | + | ||
| 83 | + | func (e *Error) Error() string { return e.Message } | |
| 84 | + | ||
| 85 | + | func newTokenError(t ErrorType, tok Token) *Error { | |
| 86 | + | return &Error{ | |
| 87 | + | Type: t, | |
| 88 | + | Message: t.description() + ": '" + tokenString(tok) + "'", | |
| 89 | + | Pos: tok.Range.Start, | |
| 90 | + | Token: tokenString(tok), | |
| 91 | + | } | |
| 92 | + | } | |
| 93 | + | ||
| 94 | + | // EvalError is an error raised while evaluating an expression: a failing | |
| 95 | + | // function, such as fromJSON on invalid input, or an invalid format string. | |
| 96 | + | type EvalError struct { | |
| 97 | + | Message string | |
| 98 | + | // Err is the underlying error, if any. | |
| 99 | + | Err error | |
| 100 | + | } | |
| 101 | + | ||
| 102 | + | func (e *EvalError) Error() string { return e.Message } | |
| 103 | + | func (e *EvalError) Unwrap() error { return e.Err } | |
| 104 | + | ||
| 105 | + | func evalErrorf(format string, a ...any) *EvalError { | |
| 106 | + | return &EvalError{Message: fmt.Sprintf(format, a...)} | |
| 107 | + | } | |
| @@ -0,0 +1,107 @@ | |||||
| 1 | + | // SPDX-License-Identifier: MIT | |||
| 2 | + | ||||
| 3 | + | package expr | |||
| 4 | + | ||||
| 5 | + | import ( | |||
| 6 | + | "fmt" | |||
| 7 | + | ) | |||
| 8 | + | ||||
| 9 | + | // Limits of the reference implementation. | |||
| 10 | + | const ( | |||
| 11 | + | // MaxParserDepth is the deepest nesting of groupings, function arguments, | |||
| 12 | + | // index accesses, negations and logical operators the parser accepts. | |||
| 13 | + | MaxParserDepth = 50 | |||
| 14 | + | // MaxExpressionLength is the longest expression accepted, counted in | |||
| 15 | + | // UTF-16 code units like the JavaScript string length it mirrors. | |||
| 16 | + | MaxExpressionLength = 21000 | |||
| 17 | + | ) | |||
| 18 | + | ||||
| 19 | + | // ErrorType classifies an [Error]. | |||
| 20 | + | type ErrorType int | |||
| 21 | + | ||||
| 22 | + | // The error types of the reference implementation, in its order. | |||
| 23 | + | const ( | |||
| 24 | + | ErrorUnexpectedSymbol ErrorType = iota | |||
| 25 | + | // ErrorUnrecognizedNamedValue is declared by the reference but never | |||
| 26 | + | // produced; ErrorUnrecognizedContext carries the same text. | |||
| 27 | + | ErrorUnrecognizedNamedValue | |||
| 28 | + | ErrorUnexpectedEndOfExpression | |||
| 29 | + | ErrorExceededMaxDepth | |||
| 30 | + | ErrorExceededMaxLength | |||
| 31 | + | ErrorTooFewParameters | |||
| 32 | + | ErrorTooManyParameters | |||
| 33 | + | ErrorEvenParameters | |||
| 34 | + | ErrorUnrecognizedContext | |||
| 35 | + | ErrorUnrecognizedFunction | |||
| 36 | + | ) | |||
| 37 | + | ||||
| 38 | + | func (t ErrorType) description() string { | |||
| 39 | + | switch t { | |||
| 40 | + | case ErrorUnexpectedEndOfExpression: | |||
| 41 | + | return "Unexpected end of expression" | |||
| 42 | + | case ErrorUnexpectedSymbol: | |||
| 43 | + | return "Unexpected symbol" | |||
| 44 | + | case ErrorUnrecognizedNamedValue: | |||
| 45 | + | return "Unrecognized named-value" | |||
| 46 | + | case ErrorExceededMaxDepth: | |||
| 47 | + | return fmt.Sprintf("Exceeded max expression depth %d", MaxParserDepth) | |||
| 48 | + | case ErrorExceededMaxLength: | |||
| 49 | + | return fmt.Sprintf("Exceeded max expression length %d", MaxExpressionLength) | |||
| 50 | + | case ErrorTooFewParameters: | |||
| 51 | + | return "Too few parameters supplied" | |||
| 52 | + | case ErrorTooManyParameters: | |||
| 53 | + | return "Too many parameters supplied" | |||
| 54 | + | case ErrorEvenParameters: | |||
| 55 | + | return "Even number of parameters supplied, requires an odd number of parameters" | |||
| 56 | + | case ErrorUnrecognizedContext: | |||
| 57 | + | return "Unrecognized named-value" | |||
| 58 | + | case ErrorUnrecognizedFunction: | |||
| 59 | + | return "Unrecognized function" | |||
| 60 | + | } | |||
| 61 | + | return "Unknown error" | |||
| 62 | + | } | |||
| 63 | + | ||||
| 64 | + | // Position is a zero-based line and column in an expression. Columns count | |||
| 65 | + | // UTF-16 code units, as in the reference implementation. | |||
| 66 | + | type Position struct { | |||
| 67 | + | Line int | |||
| 68 | + | Column int | |||
| 69 | + | } | |||
| 70 | + | ||||
| 71 | + | // Error is a lexing or parsing error. Its message is exactly the one the | |||
| 72 | + | // reference implementation produces. | |||
| 73 | + | type Error struct { | |||
| 74 | + | Type ErrorType | |||
| 75 | + | // Message is the full text, e.g. "Unexpected symbol: ')'". | |||
| 76 | + | Message string | |||
| 77 | + | // Pos is where the offending token starts. | |||
| 78 | + | Pos Position | |||
| 79 | + | // Token is the text of the offending token. | |||
| 80 | + | Token string | |||
| 81 | + | } | |||
| 82 | + | ||||
| 83 | + | func (e *Error) Error() string { return e.Message } | |||
| 84 | + | ||||
| 85 | + | func newTokenError(t ErrorType, tok Token) *Error { | |||
| 86 | + | return &Error{ | |||
| 87 | + | Type: t, | |||
| 88 | + | Message: t.description() + ": '" + tokenString(tok) + "'", | |||
| 89 | + | Pos: tok.Range.Start, | |||
| 90 | + | Token: tokenString(tok), | |||
| 91 | + | } | |||
| 92 | + | } | |||
| 93 | + | ||||
| 94 | + | // EvalError is an error raised while evaluating an expression: a failing | |||
| 95 | + | // function, such as fromJSON on invalid input, or an invalid format string. | |||
| 96 | + | type EvalError struct { | |||
| 97 | + | Message string | |||
| 98 | + | // Err is the underlying error, if any. | |||
| 99 | + | Err error | |||
| 100 | + | } | |||
| 101 | + | ||||
| 102 | + | func (e *EvalError) Error() string { return e.Message } | |||
| 103 | + | func (e *EvalError) Unwrap() error { return e.Err } | |||
| 104 | + | ||||
| 105 | + | func evalErrorf(format string, a ...any) *EvalError { | |||
| 106 | + | return &EvalError{Message: fmt.Sprintf(format, a...)} | |||
| 107 | + | } | |||
A expr/lexer.go +359 -0
| @@ -0,0 +1,359 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | ||
| 3 | + | package expr | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "fmt" | |
| 7 | + | "math" | |
| 8 | + | "strings" | |
| 9 | + | "unicode/utf16" | |
| 10 | + | ) | |
| 11 | + | ||
| 12 | + | // TokenType is the kind of a lexical token. | |
| 13 | + | type TokenType int | |
| 14 | + | ||
| 15 | + | // The token types, in the order of the reference implementation. | |
| 16 | + | const ( | |
| 17 | + | TokenUnknown TokenType = iota | |
| 18 | + | TokenLeftParen | |
| 19 | + | TokenRightParen | |
| 20 | + | TokenLeftBracket | |
| 21 | + | TokenRightBracket | |
| 22 | + | TokenComma | |
| 23 | + | TokenDot | |
| 24 | + | TokenBang | |
| 25 | + | TokenBangEqual | |
| 26 | + | TokenEqualEqual | |
| 27 | + | TokenGreater | |
| 28 | + | TokenGreaterEqual | |
| 29 | + | TokenLess | |
| 30 | + | TokenLessEqual | |
| 31 | + | TokenAnd | |
| 32 | + | TokenOr | |
| 33 | + | TokenStar | |
| 34 | + | TokenNumber | |
| 35 | + | TokenString | |
| 36 | + | TokenIdentifier | |
| 37 | + | TokenTrue | |
| 38 | + | TokenFalse | |
| 39 | + | TokenNull | |
| 40 | + | TokenEOF | |
| 41 | + | ) | |
| 42 | + | ||
| 43 | + | var tokenTypeNames = [...]string{ | |
| 44 | + | "UNKNOWN", "LEFT_PAREN", "RIGHT_PAREN", "LEFT_BRACKET", "RIGHT_BRACKET", | |
| 45 | + | "COMMA", "DOT", "BANG", "BANG_EQUAL", "EQUAL_EQUAL", "GREATER", | |
| 46 | + | "GREATER_EQUAL", "LESS", "LESS_EQUAL", "AND", "OR", "STAR", "NUMBER", | |
| 47 | + | "STRING", "IDENTIFIER", "TRUE", "FALSE", "NULL", "EOF", | |
| 48 | + | } | |
| 49 | + | ||
| 50 | + | func (t TokenType) String() string { | |
| 51 | + | if t >= 0 && int(t) < len(tokenTypeNames) { | |
| 52 | + | return tokenTypeNames[t] | |
| 53 | + | } | |
| 54 | + | return fmt.Sprintf("TokenType(%d)", int(t)) | |
| 55 | + | } | |
| 56 | + | ||
| 57 | + | // Range is the extent of a token. Both positions are on the line where the | |
| 58 | + | // token ends, as in the reference implementation. | |
| 59 | + | type Range struct { | |
| 60 | + | Start, End Position | |
| 61 | + | } | |
| 62 | + | ||
| 63 | + | // Token is a lexical token. | |
| 64 | + | type Token struct { | |
| 65 | + | Type TokenType | |
| 66 | + | Lexeme string | |
| 67 | + | Range Range | |
| 68 | + | // Value is the number of a TokenNumber, as a [Number], or the unescaped | |
| 69 | + | // text of a TokenString, as a [String]; nil for other tokens. | |
| 70 | + | Value Value | |
| 71 | + | } | |
| 72 | + | ||
| 73 | + | // tokenString is the text an error message shows for a token. | |
| 74 | + | func tokenString(t Token) string { | |
| 75 | + | switch t.Type { | |
| 76 | + | case TokenEOF: | |
| 77 | + | return "EOF" | |
| 78 | + | case TokenString: | |
| 79 | + | return string(t.Value.(String)) | |
| 80 | + | } | |
| 81 | + | return t.Lexeme | |
| 82 | + | } | |
| 83 | + | ||
| 84 | + | // Lex splits an expression into tokens. The result always ends with a | |
| 85 | + | // TokenEOF token. | |
| 86 | + | func Lex(input string) ([]Token, error) { | |
| 87 | + | units := utf16.Encode([]rune(input)) | |
| 88 | + | if len(units) > MaxExpressionLength { | |
| 89 | + | // The reference throws a bare Error("ErrorExceededMaxLength") here, | |
| 90 | + | // not the descriptive ExpressionError the parser would build. | |
| 91 | + | return nil, &Error{Type: ErrorExceededMaxLength, Message: "ErrorExceededMaxLength"} | |
| 92 | + | } | |
| 93 | + | lx := &lexer{input: string(utf16.Decode(units)), units: units} | |
| 94 | + | if err := lx.lex(); err != nil { | |
| 95 | + | return nil, err | |
| 96 | + | } | |
| 97 | + | return lx.tokens, nil | |
| 98 | + | } | |
| 99 | + | ||
| 100 | + | // lexer is a line-by-line port of Lexer in lexer.js. It works on UTF-16 code | |
| 101 | + | // units so that positions match the reference. | |
| 102 | + | type lexer struct { | |
| 103 | + | input string | |
| 104 | + | units []uint16 | |
| 105 | + | start int | |
| 106 | + | offset int | |
| 107 | + | line int | |
| 108 | + | lastLineOffset int | |
| 109 | + | tokens []Token | |
| 110 | + | } | |
| 111 | + | ||
| 112 | + | func (l *lexer) lex() error { | |
| 113 | + | for !l.atEnd() { | |
| 114 | + | l.start = l.offset | |
| 115 | + | c := l.next() | |
| 116 | + | var err error | |
| 117 | + | switch c { | |
| 118 | + | case '(': | |
| 119 | + | l.addToken(TokenLeftParen, nil) | |
| 120 | + | case ')': | |
| 121 | + | l.addToken(TokenRightParen, nil) | |
| 122 | + | case '[': | |
| 123 | + | l.addToken(TokenLeftBracket, nil) | |
| 124 | + | case ']': | |
| 125 | + | l.addToken(TokenRightBracket, nil) | |
| 126 | + | case ',': | |
| 127 | + | l.addToken(TokenComma, nil) | |
| 128 | + | case '.': | |
| 129 | + | p := l.previous() | |
| 130 | + | if p != TokenIdentifier && p != TokenRightBracket && p != TokenRightParen && p != TokenStar { | |
| 131 | + | err = l.consumeNumber() | |
| 132 | + | } else { | |
| 133 | + | l.addToken(TokenDot, nil) | |
| 134 | + | } | |
| 135 | + | case '-', '+': | |
| 136 | + | err = l.consumeNumber() | |
| 137 | + | case '!': | |
| 138 | + | l.addToken(l.pick('=', TokenBangEqual, TokenBang), nil) | |
| 139 | + | case '=': | |
| 140 | + | if !l.match('=') { | |
| 141 | + | // Illegal; continue reading until a boundary and report it. | |
| 142 | + | err = l.consumeIdentifier() | |
| 143 | + | break | |
| 144 | + | } | |
| 145 | + | l.addToken(TokenEqualEqual, nil) | |
| 146 | + | case '<': | |
| 147 | + | l.addToken(l.pick('=', TokenLessEqual, TokenLess), nil) | |
| 148 | + | case '>': | |
| 149 | + | l.addToken(l.pick('=', TokenGreaterEqual, TokenGreater), nil) | |
| 150 | + | case '&': | |
| 151 | + | if !l.match('&') { | |
| 152 | + | err = l.consumeIdentifier() | |
| 153 | + | break | |
| 154 | + | } | |
| 155 | + | l.addToken(TokenAnd, nil) | |
| 156 | + | case '|': | |
| 157 | + | if !l.match('|') { | |
| 158 | + | err = l.consumeIdentifier() | |
| 159 | + | break | |
| 160 | + | } | |
| 161 | + | l.addToken(TokenOr, nil) | |
| 162 | + | case '*': | |
| 163 | + | l.addToken(TokenStar, nil) | |
| 164 | + | case ' ', '\r', '\t': | |
| 165 | + | // Whitespace is ignored. Only these three characters and the | |
| 166 | + | // newline are: other Unicode spaces (U+00A0, \v, \f...) are not | |
| 167 | + | // skipped here and fall through to the identifier rule. | |
| 168 | + | case '\n': | |
| 169 | + | l.line++ | |
| 170 | + | l.lastLineOffset = l.offset | |
| 171 | + | case '\'': | |
| 172 | + | err = l.consumeString() | |
| 173 | + | default: | |
| 174 | + | if c >= '0' && c <= '9' { | |
| 175 | + | err = l.consumeNumber() | |
| 176 | + | } else { | |
| 177 | + | err = l.consumeIdentifier() | |
| 178 | + | } | |
| 179 | + | } | |
| 180 | + | if err != nil { | |
| 181 | + | return err | |
| 182 | + | } | |
| 183 | + | } | |
| 184 | + | l.tokens = append(l.tokens, Token{Type: TokenEOF, Range: l.rng()}) | |
| 185 | + | return nil | |
| 186 | + | } | |
| 187 | + | ||
| 188 | + | func (l *lexer) pos() Position { | |
| 189 | + | return Position{Line: l.line, Column: l.start - l.lastLineOffset} | |
| 190 | + | } | |
| 191 | + | ||
| 192 | + | func (l *lexer) rng() Range { | |
| 193 | + | return Range{ | |
| 194 | + | Start: l.pos(), | |
| 195 | + | End: Position{Line: l.line, Column: l.offset - l.lastLineOffset}, | |
| 196 | + | } | |
| 197 | + | } | |
| 198 | + | ||
| 199 | + | func (l *lexer) atEnd() bool { return l.offset >= len(l.units) } | |
| 200 | + | ||
| 201 | + | // peek returns the next unit without consuming it, or 0 at the end. | |
| 202 | + | func (l *lexer) peek() uint16 { | |
| 203 | + | if l.atEnd() { | |
| 204 | + | return 0 | |
| 205 | + | } | |
| 206 | + | return l.units[l.offset] | |
| 207 | + | } | |
| 208 | + | ||
| 209 | + | func (l *lexer) peekNext() uint16 { | |
| 210 | + | if l.offset+1 >= len(l.units) { | |
| 211 | + | return 0 | |
| 212 | + | } | |
| 213 | + | return l.units[l.offset+1] | |
| 214 | + | } | |
| 215 | + | ||
| 216 | + | func (l *lexer) previous() TokenType { | |
| 217 | + | if len(l.tokens) == 0 { | |
| 218 | + | return TokenEOF | |
| 219 | + | } | |
| 220 | + | return l.tokens[len(l.tokens)-1].Type | |
| 221 | + | } | |
| 222 | + | ||
| 223 | + | func (l *lexer) next() uint16 { | |
| 224 | + | c := l.units[l.offset] | |
| 225 | + | l.offset++ | |
| 226 | + | return c | |
| 227 | + | } | |
| 228 | + | ||
| 229 | + | func (l *lexer) match(expected uint16) bool { | |
| 230 | + | if l.atEnd() || l.units[l.offset] != expected { | |
| 231 | + | return false | |
| 232 | + | } | |
| 233 | + | l.offset++ | |
| 234 | + | return true | |
| 235 | + | } | |
| 236 | + | ||
| 237 | + | // pick consumes expected if it is next and returns yes, otherwise no. | |
| 238 | + | func (l *lexer) pick(expected uint16, yes, no TokenType) TokenType { | |
| 239 | + | if l.match(expected) { | |
| 240 | + | return yes | |
| 241 | + | } | |
| 242 | + | return no | |
| 243 | + | } | |
| 244 | + | ||
| 245 | + | func (l *lexer) substring(from, to int) string { | |
| 246 | + | return string(utf16.Decode(l.units[from:to])) | |
| 247 | + | } | |
| 248 | + | ||
| 249 | + | func (l *lexer) addToken(t TokenType, v Value) { | |
| 250 | + | l.tokens = append(l.tokens, Token{ | |
| 251 | + | Type: t, | |
| 252 | + | Lexeme: l.substring(l.start, l.offset), | |
| 253 | + | Range: l.rng(), | |
| 254 | + | Value: v, | |
| 255 | + | }) | |
| 256 | + | } | |
| 257 | + | ||
| 258 | + | func (l *lexer) unexpected(lexeme string) *Error { | |
| 259 | + | return &Error{ | |
| 260 | + | Type: ErrorUnexpectedSymbol, | |
| 261 | + | Message: fmt.Sprintf("Unexpected symbol: '%s'. Located at position %d within expression: %s", lexeme, l.start+1, l.input), | |
| 262 | + | Pos: l.pos(), | |
| 263 | + | Token: lexeme, | |
| 264 | + | } | |
| 265 | + | } | |
| 266 | + | ||
| 267 | + | func (l *lexer) consumeNumber() error { | |
| 268 | + | for !l.atEnd() && (!isBoundary(l.peek()) || l.peek() == '.') { | |
| 269 | + | l.next() | |
| 270 | + | } | |
| 271 | + | lexeme := l.substring(l.start, l.offset) | |
| 272 | + | v := jsToNumber(lexeme) | |
| 273 | + | if math.IsNaN(v) { | |
| 274 | + | return l.unexpected(lexeme) | |
| 275 | + | } | |
| 276 | + | l.addToken(TokenNumber, Number(v)) | |
| 277 | + | return nil | |
| 278 | + | } | |
| 279 | + | ||
| 280 | + | func (l *lexer) consumeString() error { | |
| 281 | + | for (l.peek() != '\'' || l.peekNext() == '\'') && !l.atEnd() { | |
| 282 | + | if l.peek() == '\n' { | |
| 283 | + | l.line++ | |
| 284 | + | } | |
| 285 | + | if l.peek() == '\'' && l.peekNext() == '\'' { | |
| 286 | + | // Escaped quote: consume the first of the pair here. | |
| 287 | + | l.next() | |
| 288 | + | } | |
| 289 | + | l.next() | |
| 290 | + | } | |
| 291 | + | if l.atEnd() { | |
| 292 | + | return l.unexpected(l.substring(l.start, len(l.units))) | |
| 293 | + | } | |
| 294 | + | l.next() // closing quote | |
| 295 | + | value := l.substring(l.start+1, l.offset-1) | |
| 296 | + | // The reference calls value.replace("''", "'") with a string pattern, | |
| 297 | + | // which replaces only the FIRST doubled quote: 'a''b''c' is a'b''c. | |
| 298 | + | // GitHub's C# lexer unescapes every pair. Followed as written; see | |
| 299 | + | // PORTING.md. | |
| 300 | + | value = strings.Replace(value, "''", "'", 1) | |
| 301 | + | l.addToken(TokenString, String(value)) | |
| 302 | + | return nil | |
| 303 | + | } | |
| 304 | + | ||
| 305 | + | func (l *lexer) consumeIdentifier() error { | |
| 306 | + | for !l.atEnd() && !isBoundary(l.peek()) { | |
| 307 | + | l.next() | |
| 308 | + | } | |
| 309 | + | tokenType := TokenIdentifier | |
| 310 | + | var tokenValue Value | |
| 311 | + | lexeme := l.substring(l.start, l.offset) | |
| 312 | + | if l.previous() != TokenDot { | |
| 313 | + | switch lexeme { | |
| 314 | + | case "true": | |
| 315 | + | tokenType = TokenTrue | |
| 316 | + | case "false": | |
| 317 | + | tokenType = TokenFalse | |
| 318 | + | case "null": | |
| 319 | + | tokenType = TokenNull | |
| 320 | + | case "NaN": | |
| 321 | + | tokenType = TokenNumber | |
| 322 | + | tokenValue = Number(nan) | |
| 323 | + | case "Infinity": | |
| 324 | + | tokenType = TokenNumber | |
| 325 | + | tokenValue = Number(posInf) | |
| 326 | + | } | |
| 327 | + | } | |
| 328 | + | if !isLegalIdentifier(lexeme) { | |
| 329 | + | return l.unexpected(lexeme) | |
| 330 | + | } | |
| 331 | + | l.addToken(tokenType, tokenValue) | |
| 332 | + | return nil | |
| 333 | + | } | |
| 334 | + | ||
| 335 | + | // isBoundary reports whether c ends a number or identifier. | |
| 336 | + | func isBoundary(c uint16) bool { | |
| 337 | + | switch c { | |
| 338 | + | case '(', '[', ')', ']', ',', '.', '!', '>', '<', '=', '&', '|': | |
| 339 | + | return true | |
| 340 | + | } | |
| 341 | + | return isJSSpace(rune(c)) | |
| 342 | + | } | |
| 343 | + | ||
| 344 | + | // isLegalIdentifier reports whether s matches [A-Za-z_][A-Za-z0-9_-]*. | |
| 345 | + | func isLegalIdentifier(s string) bool { | |
| 346 | + | if s == "" { | |
| 347 | + | return false | |
| 348 | + | } | |
| 349 | + | for i := 0; i < len(s); i++ { | |
| 350 | + | c := s[i] | |
| 351 | + | switch { | |
| 352 | + | case c >= 'a' && c <= 'z', c >= 'A' && c <= 'Z', c == '_': | |
| 353 | + | case i > 0 && (c >= '0' && c <= '9' || c == '-'): | |
| 354 | + | default: | |
| 355 | + | return false | |
| 356 | + | } | |
| 357 | + | } | |
| 358 | + | return true | |
| 359 | + | } | |
| @@ -0,0 +1,359 @@ | |||||
| 1 | + | // SPDX-License-Identifier: MIT | |||
| 2 | + | ||||
| 3 | + | package expr | |||
| 4 | + | ||||
| 5 | + | import ( | |||
| 6 | + | "fmt" | |||
| 7 | + | "math" | |||
| 8 | + | "strings" | |||
| 9 | + | "unicode/utf16" | |||
| 10 | + | ) | |||
| 11 | + | ||||
| 12 | + | // TokenType is the kind of a lexical token. | |||
| 13 | + | type TokenType int | |||
| 14 | + | ||||
| 15 | + | // The token types, in the order of the reference implementation. | |||
| 16 | + | const ( | |||
| 17 | + | TokenUnknown TokenType = iota | |||
| 18 | + | TokenLeftParen | |||
| 19 | + | TokenRightParen | |||
| 20 | + | TokenLeftBracket | |||
| 21 | + | TokenRightBracket | |||
| 22 | + | TokenComma | |||
| 23 | + | TokenDot | |||
| 24 | + | TokenBang | |||
| 25 | + | TokenBangEqual | |||
| 26 | + | TokenEqualEqual | |||
| 27 | + | TokenGreater | |||
| 28 | + | TokenGreaterEqual | |||
| 29 | + | TokenLess | |||
| 30 | + | TokenLessEqual | |||
| 31 | + | TokenAnd | |||
| 32 | + | TokenOr | |||
| 33 | + | TokenStar | |||
| 34 | + | TokenNumber | |||
| 35 | + | TokenString | |||
| 36 | + | TokenIdentifier | |||
| 37 | + | TokenTrue | |||
| 38 | + | TokenFalse | |||
| 39 | + | TokenNull | |||
| 40 | + | TokenEOF | |||
| 41 | + | ) | |||
| 42 | + | ||||
| 43 | + | var tokenTypeNames = [...]string{ | |||
| 44 | + | "UNKNOWN", "LEFT_PAREN", "RIGHT_PAREN", "LEFT_BRACKET", "RIGHT_BRACKET", | |||
| 45 | + | "COMMA", "DOT", "BANG", "BANG_EQUAL", "EQUAL_EQUAL", "GREATER", | |||
| 46 | + | "GREATER_EQUAL", "LESS", "LESS_EQUAL", "AND", "OR", "STAR", "NUMBER", | |||
| 47 | + | "STRING", "IDENTIFIER", "TRUE", "FALSE", "NULL", "EOF", | |||
| 48 | + | } | |||
| 49 | + | ||||
| 50 | + | func (t TokenType) String() string { | |||
| 51 | + | if t >= 0 && int(t) < len(tokenTypeNames) { | |||
| 52 | + | return tokenTypeNames[t] | |||
| 53 | + | } | |||
| 54 | + | return fmt.Sprintf("TokenType(%d)", int(t)) | |||
| 55 | + | } | |||
| 56 | + | ||||
| 57 | + | // Range is the extent of a token. Both positions are on the line where the | |||
| 58 | + | // token ends, as in the reference implementation. | |||
| 59 | + | type Range struct { | |||
| 60 | + | Start, End Position | |||
| 61 | + | } | |||
| 62 | + | ||||
| 63 | + | // Token is a lexical token. | |||
| 64 | + | type Token struct { | |||
| 65 | + | Type TokenType | |||
| 66 | + | Lexeme string | |||
| 67 | + | Range Range | |||
| 68 | + | // Value is the number of a TokenNumber, as a [Number], or the unescaped | |||
| 69 | + | // text of a TokenString, as a [String]; nil for other tokens. | |||
| 70 | + | Value Value | |||
| 71 | + | } | |||
| 72 | + | ||||
| 73 | + | // tokenString is the text an error message shows for a token. | |||
| 74 | + | func tokenString(t Token) string { | |||
| 75 | + | switch t.Type { | |||
| 76 | + | case TokenEOF: | |||
| 77 | + | return "EOF" | |||
| 78 | + | case TokenString: | |||
| 79 | + | return string(t.Value.(String)) | |||
| 80 | + | } | |||
| 81 | + | return t.Lexeme | |||
| 82 | + | } | |||
| 83 | + | ||||
| 84 | + | // Lex splits an expression into tokens. The result always ends with a | |||
| 85 | + | // TokenEOF token. | |||
| 86 | + | func Lex(input string) ([]Token, error) { | |||
| 87 | + | units := utf16.Encode([]rune(input)) | |||
| 88 | + | if len(units) > MaxExpressionLength { | |||
| 89 | + | // The reference throws a bare Error("ErrorExceededMaxLength") here, | |||
| 90 | + | // not the descriptive ExpressionError the parser would build. | |||
| 91 | + | return nil, &Error{Type: ErrorExceededMaxLength, Message: "ErrorExceededMaxLength"} | |||
| 92 | + | } | |||
| 93 | + | lx := &lexer{input: string(utf16.Decode(units)), units: units} | |||
| 94 | + | if err := lx.lex(); err != nil { | |||
| 95 | + | return nil, err | |||
| 96 | + | } | |||
| 97 | + | return lx.tokens, nil | |||
| 98 | + | } | |||
| 99 | + | ||||
| 100 | + | // lexer is a line-by-line port of Lexer in lexer.js. It works on UTF-16 code | |||
| 101 | + | // units so that positions match the reference. | |||
| 102 | + | type lexer struct { | |||
| 103 | + | input string | |||
| 104 | + | units []uint16 | |||
| 105 | + | start int | |||
| 106 | + | offset int | |||
| 107 | + | line int | |||
| 108 | + | lastLineOffset int | |||
| 109 | + | tokens []Token | |||
| 110 | + | } | |||
| 111 | + | ||||
| 112 | + | func (l *lexer) lex() error { | |||
| 113 | + | for !l.atEnd() { | |||
| 114 | + | l.start = l.offset | |||
| 115 | + | c := l.next() | |||
| 116 | + | var err error | |||
| 117 | + | switch c { | |||
| 118 | + | case '(': | |||
| 119 | + | l.addToken(TokenLeftParen, nil) | |||
| 120 | + | case ')': | |||
| 121 | + | l.addToken(TokenRightParen, nil) | |||
| 122 | + | case '[': | |||
| 123 | + | l.addToken(TokenLeftBracket, nil) | |||
| 124 | + | case ']': | |||
| 125 | + | l.addToken(TokenRightBracket, nil) | |||
| 126 | + | case ',': | |||
| 127 | + | l.addToken(TokenComma, nil) | |||
| 128 | + | case '.': | |||
| 129 | + | p := l.previous() | |||
| 130 | + | if p != TokenIdentifier && p != TokenRightBracket && p != TokenRightParen && p != TokenStar { | |||
| 131 | + | err = l.consumeNumber() | |||
| 132 | + | } else { | |||
| 133 | + | l.addToken(TokenDot, nil) | |||
| 134 | + | } | |||
| 135 | + | case '-', '+': | |||
| 136 | + | err = l.consumeNumber() | |||
| 137 | + | case '!': | |||
| 138 | + | l.addToken(l.pick('=', TokenBangEqual, TokenBang), nil) | |||
| 139 | + | case '=': | |||
| 140 | + | if !l.match('=') { | |||
| 141 | + | // Illegal; continue reading until a boundary and report it. | |||
| 142 | + | err = l.consumeIdentifier() | |||
| 143 | + | break | |||
| 144 | + | } | |||
| 145 | + | l.addToken(TokenEqualEqual, nil) | |||
| 146 | + | case '<': | |||
| 147 | + | l.addToken(l.pick('=', TokenLessEqual, TokenLess), nil) | |||
| 148 | + | case '>': | |||
| 149 | + | l.addToken(l.pick('=', TokenGreaterEqual, TokenGreater), nil) | |||
| 150 | + | case '&': | |||
| 151 | + | if !l.match('&') { | |||
| 152 | + | err = l.consumeIdentifier() | |||
| 153 | + | break | |||
| 154 | + | } | |||
| 155 | + | l.addToken(TokenAnd, nil) | |||
| 156 | + | case '|': | |||
| 157 | + | if !l.match('|') { | |||
| 158 | + | err = l.consumeIdentifier() | |||
| 159 | + | break | |||
| 160 | + | } | |||
| 161 | + | l.addToken(TokenOr, nil) | |||
| 162 | + | case '*': | |||
| 163 | + | l.addToken(TokenStar, nil) | |||
| 164 | + | case ' ', '\r', '\t': | |||
| 165 | + | // Whitespace is ignored. Only these three characters and the | |||
| 166 | + | // newline are: other Unicode spaces (U+00A0, \v, \f...) are not | |||
| 167 | + | // skipped here and fall through to the identifier rule. | |||
| 168 | + | case '\n': | |||
| 169 | + | l.line++ | |||
| 170 | + | l.lastLineOffset = l.offset | |||
| 171 | + | case '\'': | |||
| 172 | + | err = l.consumeString() | |||
| 173 | + | default: | |||
| 174 | + | if c >= '0' && c <= '9' { | |||
| 175 | + | err = l.consumeNumber() | |||
| 176 | + | } else { | |||
| 177 | + | err = l.consumeIdentifier() | |||
| 178 | + | } | |||
| 179 | + | } | |||
| 180 | + | if err != nil { | |||
| 181 | + | return err | |||
| 182 | + | } | |||
| 183 | + | } | |||
| 184 | + | l.tokens = append(l.tokens, Token{Type: TokenEOF, Range: l.rng()}) | |||
| 185 | + | return nil | |||
| 186 | + | } | |||
| 187 | + | ||||
| 188 | + | func (l *lexer) pos() Position { | |||
| 189 | + | return Position{Line: l.line, Column: l.start - l.lastLineOffset} | |||
| 190 | + | } | |||
| 191 | + | ||||
| 192 | + | func (l *lexer) rng() Range { | |||
| 193 | + | return Range{ | |||
| 194 | + | Start: l.pos(), | |||
| 195 | + | End: Position{Line: l.line, Column: l.offset - l.lastLineOffset}, | |||
| 196 | + | } | |||
| 197 | + | } | |||
| 198 | + | ||||
| 199 | + | func (l *lexer) atEnd() bool { return l.offset >= len(l.units) } | |||
| 200 | + | ||||
| 201 | + | // peek returns the next unit without consuming it, or 0 at the end. | |||
| 202 | + | func (l *lexer) peek() uint16 { | |||
| 203 | + | if l.atEnd() { | |||
| 204 | + | return 0 | |||
| 205 | + | } | |||
| 206 | + | return l.units[l.offset] | |||
| 207 | + | } | |||
| 208 | + | ||||
| 209 | + | func (l *lexer) peekNext() uint16 { | |||
| 210 | + | if l.offset+1 >= len(l.units) { | |||
| 211 | + | return 0 | |||
| 212 | + | } | |||
| 213 | + | return l.units[l.offset+1] | |||
| 214 | + | } | |||
| 215 | + | ||||
| 216 | + | func (l *lexer) previous() TokenType { | |||
| 217 | + | if len(l.tokens) == 0 { | |||
| 218 | + | return TokenEOF | |||
| 219 | + | } | |||
| 220 | + | return l.tokens[len(l.tokens)-1].Type | |||
| 221 | + | } | |||
| 222 | + | ||||
| 223 | + | func (l *lexer) next() uint16 { | |||
| 224 | + | c := l.units[l.offset] | |||
| 225 | + | l.offset++ | |||
| 226 | + | return c | |||
| 227 | + | } | |||
| 228 | + | ||||
| 229 | + | func (l *lexer) match(expected uint16) bool { | |||
| 230 | + | if l.atEnd() || l.units[l.offset] != expected { | |||
| 231 | + | return false | |||
| 232 | + | } | |||
| 233 | + | l.offset++ | |||
| 234 | + | return true | |||
| 235 | + | } | |||
| 236 | + | ||||
| 237 | + | // pick consumes expected if it is next and returns yes, otherwise no. | |||
| 238 | + | func (l *lexer) pick(expected uint16, yes, no TokenType) TokenType { | |||
| 239 | + | if l.match(expected) { | |||
| 240 | + | return yes | |||
| 241 | + | } | |||
| 242 | + | return no | |||
| 243 | + | } | |||
| 244 | + | ||||
| 245 | + | func (l *lexer) substring(from, to int) string { | |||
| 246 | + | return string(utf16.Decode(l.units[from:to])) | |||
| 247 | + | } | |||
| 248 | + | ||||
| 249 | + | func (l *lexer) addToken(t TokenType, v Value) { | |||
| 250 | + | l.tokens = append(l.tokens, Token{ | |||
| 251 | + | Type: t, | |||
| 252 | + | Lexeme: l.substring(l.start, l.offset), | |||
| 253 | + | Range: l.rng(), | |||
| 254 | + | Value: v, | |||
| 255 | + | }) | |||
| 256 | + | } | |||
| 257 | + | ||||
| 258 | + | func (l *lexer) unexpected(lexeme string) *Error { | |||
| 259 | + | return &Error{ | |||
| 260 | + | Type: ErrorUnexpectedSymbol, | |||
| 261 | + | Message: fmt.Sprintf("Unexpected symbol: '%s'. Located at position %d within expression: %s", lexeme, l.start+1, l.input), | |||
| 262 | + | Pos: l.pos(), | |||
| 263 | + | Token: lexeme, | |||
| 264 | + | } | |||
| 265 | + | } | |||
| 266 | + | ||||
| 267 | + | func (l *lexer) consumeNumber() error { | |||
| 268 | + | for !l.atEnd() && (!isBoundary(l.peek()) || l.peek() == '.') { | |||
| 269 | + | l.next() | |||
| 270 | + | } | |||
| 271 | + | lexeme := l.substring(l.start, l.offset) | |||
| 272 | + | v := jsToNumber(lexeme) | |||
| 273 | + | if math.IsNaN(v) { | |||
| 274 | + | return l.unexpected(lexeme) | |||
| 275 | + | } | |||
| 276 | + | l.addToken(TokenNumber, Number(v)) | |||
| 277 | + | return nil | |||
| 278 | + | } | |||
| 279 | + | ||||
| 280 | + | func (l *lexer) consumeString() error { | |||
| 281 | + | for (l.peek() != '\'' || l.peekNext() == '\'') && !l.atEnd() { | |||
| 282 | + | if l.peek() == '\n' { | |||
| 283 | + | l.line++ | |||
| 284 | + | } | |||
| 285 | + | if l.peek() == '\'' && l.peekNext() == '\'' { | |||
| 286 | + | // Escaped quote: consume the first of the pair here. | |||
| 287 | + | l.next() | |||
| 288 | + | } | |||
| 289 | + | l.next() | |||
| 290 | + | } | |||
| 291 | + | if l.atEnd() { | |||
| 292 | + | return l.unexpected(l.substring(l.start, len(l.units))) | |||
| 293 | + | } | |||
| 294 | + | l.next() // closing quote | |||
| 295 | + | value := l.substring(l.start+1, l.offset-1) | |||
| 296 | + | // The reference calls value.replace("''", "'") with a string pattern, | |||
| 297 | + | // which replaces only the FIRST doubled quote: 'a''b''c' is a'b''c. | |||
| 298 | + | // GitHub's C# lexer unescapes every pair. Followed as written; see | |||
| 299 | + | // PORTING.md. | |||
| 300 | + | value = strings.Replace(value, "''", "'", 1) | |||
| 301 | + | l.addToken(TokenString, String(value)) | |||
| 302 | + | return nil | |||
| 303 | + | } | |||
| 304 | + | ||||
| 305 | + | func (l *lexer) consumeIdentifier() error { | |||
| 306 | + | for !l.atEnd() && !isBoundary(l.peek()) { | |||
| 307 | + | l.next() | |||
| 308 | + | } | |||
| 309 | + | tokenType := TokenIdentifier | |||
| 310 | + | var tokenValue Value | |||
| 311 | + | lexeme := l.substring(l.start, l.offset) | |||
| 312 | + | if l.previous() != TokenDot { | |||
| 313 | + | switch lexeme { | |||
| 314 | + | case "true": | |||
| 315 | + | tokenType = TokenTrue | |||
| 316 | + | case "false": | |||
| 317 | + | tokenType = TokenFalse | |||
| 318 | + | case "null": | |||
| 319 | + | tokenType = TokenNull | |||
| 320 | + | case "NaN": | |||
| 321 | + | tokenType = TokenNumber | |||
| 322 | + | tokenValue = Number(nan) | |||
| 323 | + | case "Infinity": | |||
| 324 | + | tokenType = TokenNumber | |||
| 325 | + | tokenValue = Number(posInf) | |||
| 326 | + | } | |||
| 327 | + | } | |||
| 328 | + | if !isLegalIdentifier(lexeme) { | |||
| 329 | + | return l.unexpected(lexeme) | |||
| 330 | + | } | |||
| 331 | + | l.addToken(tokenType, tokenValue) | |||
| 332 | + | return nil | |||
| 333 | + | } | |||
| 334 | + | ||||
| 335 | + | // isBoundary reports whether c ends a number or identifier. | |||
| 336 | + | func isBoundary(c uint16) bool { | |||
| 337 | + | switch c { | |||
| 338 | + | case '(', '[', ')', ']', ',', '.', '!', '>', '<', '=', '&', '|': | |||
| 339 | + | return true | |||
| 340 | + | } | |||
| 341 | + | return isJSSpace(rune(c)) | |||
| 342 | + | } | |||
| 343 | + | ||||
| 344 | + | // isLegalIdentifier reports whether s matches [A-Za-z_][A-Za-z0-9_-]*. | |||
| 345 | + | func isLegalIdentifier(s string) bool { | |||
| 346 | + | if s == "" { | |||
| 347 | + | return false | |||
| 348 | + | } | |||
| 349 | + | for i := 0; i < len(s); i++ { | |||
| 350 | + | c := s[i] | |||
| 351 | + | switch { | |||
| 352 | + | case c >= 'a' && c <= 'z', c >= 'A' && c <= 'Z', c == '_': | |||
| 353 | + | case i > 0 && (c >= '0' && c <= '9' || c == '-'): | |||
| 354 | + | default: | |||
| 355 | + | return false | |||
| 356 | + | } | |||
| 357 | + | } | |||
| 358 | + | return true | |||
| 359 | + | } | |||