Improve parser and lexer (#811)

* convert source to a struct and fix minor issue displaying error

* lexer: add byte and char pos

* improve parser to use string instead of []rune

* add lexer benchmarks

* make lexer work as an iterator

* make parser use the new iterator API in lexer

* allow reusing the parser

* cleanup code

* add parser benchmarks
This commit is contained in:
Diego Augusto Molina
2025-06-30 20:08:46 +02:00
committed by GitHub
parent 037ef3dc3d
commit fefc347053
12 changed files with 532 additions and 246 deletions
+20
View File
@@ -0,0 +1,20 @@
package parser
import "testing"
func BenchmarkParser(b *testing.B) {
const source = `
/*
Showing worst case scenario
*/
let value = trim("contains escapes \n\"\\ \U0001F600 and non ASCII ñ"); // inline comment
len(value) == 0x2A
// let's introduce an error too
whatever
`
b.ReportAllocs()
p := new(Parser)
for i := 0; i < b.N; i++ {
p.Parse(source, nil)
}
}
+110 -84
View File
@@ -2,152 +2,178 @@ package lexer
import (
"fmt"
"io"
"strings"
"unicode/utf8"
"github.com/expr-lang/expr/file"
"github.com/expr-lang/expr/internal/ring"
)
const ringChunkSize = 10
// Lex will buffer and return the tokens of a disposable *[Lexer].
func Lex(source file.Source) ([]Token, error) {
l := &lexer{
source: source,
tokens: make([]Token, 0),
start: 0,
end: 0,
tokens := make([]Token, 0, ringChunkSize)
l := New()
l.Reset(source)
for {
t, err := l.Next()
switch err {
case nil:
tokens = append(tokens, t)
case io.EOF:
return tokens, nil
default:
return nil, err
}
}
l.commit()
for state := root; state != nil; {
state = state(l)
}
if l.err != nil {
return nil, l.err.Bind(source)
}
return l.tokens, nil
}
type lexer struct {
// New returns a reusable lexer.
func New() *Lexer {
return &Lexer{
tokens: ring.New[Token](ringChunkSize),
}
}
type Lexer struct {
state stateFn
source file.Source
tokens []Token
start, end int
tokens *ring.Ring[Token]
err *file.Error
start, end struct {
byte, rune int
}
eof bool
}
func (l *Lexer) Reset(source file.Source) {
l.source = source
l.tokens.Reset()
l.state = root
}
func (l *Lexer) Next() (Token, error) {
for l.state != nil && l.err == nil && l.tokens.Len() == 0 {
l.state = l.state(l)
}
if l.err != nil {
return Token{}, l.err.Bind(l.source)
}
if t, ok := l.tokens.Dequeue(); ok {
return t, nil
}
return Token{}, io.EOF
}
const eof rune = -1
func (l *lexer) commit() {
func (l *Lexer) commit() {
l.start = l.end
}
func (l *lexer) next() rune {
if l.end >= len(l.source) {
l.end++
func (l *Lexer) next() rune {
if l.end.byte >= len(l.source.String()) {
l.eof = true
return eof
}
r := l.source[l.end]
l.end++
r, sz := utf8.DecodeRuneInString(l.source.String()[l.end.byte:])
l.end.rune++
l.end.byte += sz
return r
}
func (l *lexer) peek() rune {
r := l.next()
l.backup()
return r
func (l *Lexer) peek() rune {
if l.end.byte < len(l.source.String()) {
r, _ := utf8.DecodeRuneInString(l.source.String()[l.end.byte:])
return r
}
return eof
}
func (l *lexer) backup() {
l.end--
func (l *Lexer) peekByte() (byte, bool) {
if l.end.byte >= 0 && l.end.byte < len(l.source.String()) {
return l.source.String()[l.end.byte], true
}
return 0, false
}
func (l *lexer) emit(t Kind) {
func (l *Lexer) backup() {
if l.eof {
l.eof = false
} else if l.end.rune > 0 {
_, sz := utf8.DecodeLastRuneInString(l.source.String()[:l.end.byte])
l.end.byte -= sz
l.end.rune--
}
}
func (l *Lexer) emit(t Kind) {
l.emitValue(t, l.word())
}
func (l *lexer) emitValue(t Kind, value string) {
l.tokens = append(l.tokens, Token{
Location: file.Location{From: l.start, To: l.end},
func (l *Lexer) emitValue(t Kind, value string) {
l.tokens.Enqueue(Token{
Location: file.Location{From: l.start.rune, To: l.end.rune},
Kind: t,
Value: value,
})
l.commit()
}
func (l *lexer) emitEOF() {
from := l.end - 2
func (l *Lexer) emitEOF() {
from := l.end.rune - 1
if from < 0 {
from = 0
}
to := l.end - 1
to := l.end.rune - 0
if to < 0 {
to = 0
}
l.tokens = append(l.tokens, Token{
l.tokens.Enqueue(Token{
Location: file.Location{From: from, To: to},
Kind: EOF,
})
l.commit()
}
func (l *lexer) skip() {
func (l *Lexer) skip() {
l.commit()
}
func (l *lexer) word() string {
// TODO: boundary check is NOT needed here, but for some reason CI fuzz tests are failing.
if l.start > len(l.source) || l.end > len(l.source) {
return "__invalid__"
}
return string(l.source[l.start:l.end])
func (l *Lexer) word() string {
return l.source.String()[l.start.byte:l.end.byte]
}
func (l *lexer) accept(valid string) bool {
if strings.ContainsRune(valid, l.next()) {
func (l *Lexer) accept(valid string) bool {
if strings.ContainsRune(valid, l.peek()) {
l.next()
return true
}
l.backup()
return false
}
func (l *lexer) acceptRun(valid string) {
for strings.ContainsRune(valid, l.next()) {
func (l *Lexer) acceptRun(valid string) {
for l.accept(valid) {
}
l.backup()
}
func (l *lexer) skipSpaces() {
r := l.peek()
for ; r == ' '; r = l.peek() {
l.next()
}
func (l *Lexer) skipSpaces() {
l.acceptRun(" ")
l.skip()
}
func (l *lexer) acceptWord(word string) bool {
pos := l.end
l.skipSpaces()
for _, ch := range word {
if l.next() != ch {
l.end = pos
return false
}
}
if r := l.peek(); r != ' ' && r != eof {
l.end = pos
return false
}
return true
}
func (l *lexer) error(format string, args ...any) stateFn {
func (l *Lexer) error(format string, args ...any) stateFn {
if l.err == nil { // show first error
end := l.end.rune
if l.eof {
end++
}
l.err = &file.Error{
Location: file.Location{
From: l.end - 1,
To: l.end,
From: end - 1,
To: end,
},
Message: fmt.Sprintf(format, args...),
}
@@ -167,7 +193,7 @@ func digitVal(ch rune) int {
func lower(ch rune) rune { return ('a' - 'A') | ch } // returns lower-case ch iff ch is ASCII letter
func (l *lexer) scanDigits(ch rune, base, n int) rune {
func (l *Lexer) scanDigits(ch rune, base, n int) rune {
for n > 0 && digitVal(ch) < base {
ch = l.next()
n--
@@ -178,7 +204,7 @@ func (l *lexer) scanDigits(ch rune, base, n int) rune {
return ch
}
func (l *lexer) scanEscape(quote rune) rune {
func (l *Lexer) scanEscape(quote rune) rune {
ch := l.next() // read character after '/'
switch ch {
case 'a', 'b', 'f', 'n', 'r', 't', 'v', '\\', quote:
@@ -198,7 +224,7 @@ func (l *lexer) scanEscape(quote rune) rune {
return ch
}
func (l *lexer) scanString(quote rune) (n int) {
func (l *Lexer) scanString(quote rune) (n int) {
ch := l.next() // read character after quote
for ch != quote {
if ch == '\n' || ch == eof {
@@ -215,7 +241,7 @@ func (l *lexer) scanString(quote rune) (n int) {
return
}
func (l *lexer) scanRawString(quote rune) (n int) {
func (l *Lexer) scanRawString(quote rune) (n int) {
ch := l.next() // read character after back tick
for ch != quote {
if ch == eof {
@@ -225,6 +251,6 @@ func (l *lexer) scanRawString(quote rune) (n int) {
ch = l.next()
n++
}
l.emitValue(String, string(l.source[l.start+1:l.end-1]))
l.emitValue(String, l.source.String()[l.start.byte+1:l.end.byte-1])
return
}
+1
View File
@@ -335,6 +335,7 @@ literal not terminated (1:10)
früh ♥︎
unrecognized character: U+2665 '♥' (1:6)
| früh ♥︎
| .....^
`
func TestLex_error(t *testing.T) {
+12 -12
View File
@@ -6,9 +6,9 @@ import (
"github.com/expr-lang/expr/parser/utils"
)
type stateFn func(*lexer) stateFn
type stateFn func(*Lexer) stateFn
func root(l *lexer) stateFn {
func root(l *Lexer) stateFn {
switch r := l.next(); {
case r == eof:
l.emitEOF()
@@ -61,7 +61,7 @@ func root(l *lexer) stateFn {
return root
}
func number(l *lexer) stateFn {
func number(l *Lexer) stateFn {
if !l.scanNumber() {
return l.error("bad number syntax: %q", l.word())
}
@@ -69,7 +69,7 @@ func number(l *lexer) stateFn {
return root
}
func (l *lexer) scanNumber() bool {
func (l *Lexer) scanNumber() bool {
digits := "0123456789_"
// Is it hex?
if l.accept("0") {
@@ -107,7 +107,7 @@ func (l *lexer) scanNumber() bool {
return true
}
func dot(l *lexer) stateFn {
func dot(l *Lexer) stateFn {
l.next()
if l.accept("0123456789") {
l.backup()
@@ -118,7 +118,7 @@ func dot(l *lexer) stateFn {
return root
}
func identifier(l *lexer) stateFn {
func identifier(l *Lexer) stateFn {
loop:
for {
switch r := l.next(); {
@@ -140,7 +140,7 @@ loop:
return root
}
func not(l *lexer) stateFn {
func not(l *Lexer) stateFn {
l.emit(Operator)
l.skipSpaces()
@@ -167,13 +167,13 @@ func not(l *lexer) stateFn {
return root
}
func questionMark(l *lexer) stateFn {
func questionMark(l *Lexer) stateFn {
l.accept(".?")
l.emit(Operator)
return root
}
func slash(l *lexer) stateFn {
func slash(l *Lexer) stateFn {
if l.accept("/") {
return singleLineComment
}
@@ -184,7 +184,7 @@ func slash(l *lexer) stateFn {
return root
}
func singleLineComment(l *lexer) stateFn {
func singleLineComment(l *Lexer) stateFn {
for {
r := l.next()
if r == eof || r == '\n' {
@@ -195,7 +195,7 @@ func singleLineComment(l *lexer) stateFn {
return root
}
func multiLineComment(l *lexer) stateFn {
func multiLineComment(l *Lexer) stateFn {
for {
r := l.next()
if r == eof {
@@ -209,7 +209,7 @@ func multiLineComment(l *lexer) stateFn {
return root
}
func pointer(l *lexer) stateFn {
func pointer(l *Lexer) stateFn {
l.accept("#")
l.emit(Operator)
for {
+4 -8
View File
@@ -31,17 +31,13 @@ func (t Token) String() string {
}
func (t Token) Is(kind Kind, values ...string) bool {
if len(values) == 0 {
return kind == t.Kind
if kind != t.Kind {
return false
}
for _, v := range values {
if v == t.Value {
goto found
return true
}
}
return false
found:
return kind == t.Kind
return len(values) == 0
}
+5 -4
View File
@@ -36,7 +36,8 @@ func unescape(value string) (string, error) {
if size >= math.MaxInt {
return "", fmt.Errorf("too large string")
}
buf := make([]byte, 0, size)
buf := new(strings.Builder)
buf.Grow(int(size))
for len(value) > 0 {
c, multibyte, rest, err := unescapeChar(value)
if err != nil {
@@ -44,13 +45,13 @@ func unescape(value string) (string, error) {
}
value = rest
if c < utf8.RuneSelf || !multibyte {
buf = append(buf, byte(c))
buf.WriteByte(byte(c))
} else {
n := utf8.EncodeRune(runeTmp[:], c)
buf = append(buf, runeTmp[:n]...)
buf.Write(runeTmp[:n])
}
}
return string(buf), nil
return buf.String(), nil
}
// unescapeChar takes a string input and returns the following info:
+92 -67
View File
@@ -1,7 +1,9 @@
package parser
import (
"errors"
"fmt"
"io"
"math"
"strconv"
"strings"
@@ -10,6 +12,7 @@ import (
"github.com/expr-lang/expr/builtin"
"github.com/expr-lang/expr/conf"
"github.com/expr-lang/expr/file"
"github.com/expr-lang/expr/parser/lexer"
. "github.com/expr-lang/expr/parser/lexer"
"github.com/expr-lang/expr/parser/operator"
"github.com/expr-lang/expr/parser/utils"
@@ -44,17 +47,50 @@ var predicates = map[string]struct {
"reduce": {[]arg{expr, predicate, expr | optional}},
}
type parser struct {
tokens []Token
current Token
pos int
err *file.Error
config *conf.Config
depth int // predicate call depth
nodeCount uint // tracks number of AST nodes created
// Parser is a reusable parser. The zero value is ready for use.
type Parser struct {
lexer *lexer.Lexer
current, stashed Token
hasStash bool
err *file.Error
config *conf.Config
depth int // predicate call depth
nodeCount uint // tracks number of AST nodes created
}
func (p *parser) checkNodeLimit() error {
func (p *Parser) Parse(input string, config *conf.Config) (*Tree, error) {
if p.lexer == nil {
p.lexer = lexer.New()
}
p.config = config
source := file.NewSource(input)
p.lexer.Reset(source)
p.next()
node := p.parseSequenceExpression()
if !p.current.Is(EOF) {
p.error("unexpected token %v", p.current)
}
tree := &Tree{
Node: node,
Source: source,
}
err := p.err
// cleanup non-reusable pointer values and reset state
p.err = nil
p.config = nil
p.lexer.Reset(file.Source{})
if err != nil {
return tree, err.Bind(source)
}
return tree, nil
}
func (p *Parser) checkNodeLimit() error {
p.nodeCount++
if p.config == nil {
if p.nodeCount > conf.DefaultMaxNodes {
@@ -70,7 +106,7 @@ func (p *parser) checkNodeLimit() error {
return nil
}
func (p *parser) createNode(n Node, loc file.Location) Node {
func (p *Parser) createNode(n Node, loc file.Location) Node {
if err := p.checkNodeLimit(); err != nil {
return nil
}
@@ -81,7 +117,7 @@ func (p *parser) createNode(n Node, loc file.Location) Node {
return n
}
func (p *parser) createMemberNode(n *MemberNode, loc file.Location) *MemberNode {
func (p *Parser) createMemberNode(n *MemberNode, loc file.Location) *MemberNode {
if err := p.checkNodeLimit(); err != nil {
return nil
}
@@ -102,42 +138,14 @@ func Parse(input string) (*Tree, error) {
}
func ParseWithConfig(input string, config *conf.Config) (*Tree, error) {
source := file.NewSource(input)
tokens, err := Lex(source)
if err != nil {
return nil, err
}
p := &parser{
tokens: tokens,
current: tokens[0],
config: config,
}
node := p.parseSequenceExpression()
if !p.current.Is(EOF) {
p.error("unexpected token %v", p.current)
}
tree := &Tree{
Node: node,
Source: source,
}
if p.err != nil {
return tree, p.err.Bind(source)
}
return tree, nil
return new(Parser).Parse(input, config)
}
func (p *parser) error(format string, args ...any) {
func (p *Parser) error(format string, args ...any) {
p.errorAt(p.current, format, args...)
}
func (p *parser) errorAt(token Token, format string, args ...any) {
func (p *Parser) errorAt(token Token, format string, args ...any) {
if p.err == nil { // show first error
p.err = &file.Error{
Location: token.Location,
@@ -146,16 +154,32 @@ func (p *parser) errorAt(token Token, format string, args ...any) {
}
}
func (p *parser) next() {
p.pos++
if p.pos >= len(p.tokens) {
p.error("unexpected end of expression")
func (p *Parser) next() {
if p.hasStash {
p.current = p.stashed
p.hasStash = false
return
}
p.current = p.tokens[p.pos]
token, err := p.lexer.Next()
var e *file.Error
switch {
case err == nil:
p.current = token
case errors.Is(err, io.EOF):
p.error("unexpected end of expression")
case errors.As(err, &e):
p.err = e
default:
p.err = &file.Error{
Location: p.current.Location,
Message: "unknown lexing error",
Prev: err,
}
}
}
func (p *parser) expect(kind Kind, values ...string) {
func (p *Parser) expect(kind Kind, values ...string) {
if p.current.Is(kind, values...) {
p.next()
return
@@ -165,7 +189,7 @@ func (p *parser) expect(kind Kind, values ...string) {
// parse functions
func (p *parser) parseSequenceExpression() Node {
func (p *Parser) parseSequenceExpression() Node {
nodes := []Node{p.parseExpression(0)}
for p.current.Is(Operator, ";") && p.err == nil {
@@ -186,7 +210,7 @@ func (p *parser) parseSequenceExpression() Node {
}, nodes[0].Location())
}
func (p *parser) parseExpression(precedence int) Node {
func (p *Parser) parseExpression(precedence int) Node {
if p.err != nil {
return nil
}
@@ -209,15 +233,16 @@ func (p *parser) parseExpression(precedence int) Node {
// Handle "not *" operator, like "not in" or "not contains".
if negate {
currentPos := p.pos
tokenBackup := p.current
p.next()
if operator.AllowedNegateSuffix(p.current.Value) {
if op, ok := operator.Binary[p.current.Value]; ok && op.Precedence >= precedence {
notToken = p.current
opToken = p.current
} else {
p.pos = currentPos
p.current = opToken
p.hasStash = true
p.stashed = p.current
p.current = tokenBackup
break
}
} else {
@@ -288,7 +313,7 @@ func (p *parser) parseExpression(precedence int) Node {
return nodeLeft
}
func (p *parser) parseVariableDeclaration() Node {
func (p *Parser) parseVariableDeclaration() Node {
p.expect(Operator, "let")
variableName := p.current
p.expect(Identifier)
@@ -303,7 +328,7 @@ func (p *parser) parseVariableDeclaration() Node {
}, variableName.Location)
}
func (p *parser) parseConditionalIf() Node {
func (p *Parser) parseConditionalIf() Node {
p.next()
nodeCondition := p.parseExpression(0)
p.expect(Bracket, "{")
@@ -322,7 +347,7 @@ func (p *parser) parseConditionalIf() Node {
}
func (p *parser) parseConditional(node Node) Node {
func (p *Parser) parseConditional(node Node) Node {
var expr1, expr2 Node
for p.current.Is(Operator, "?") && p.err == nil {
p.next()
@@ -349,7 +374,7 @@ func (p *parser) parseConditional(node Node) Node {
return node
}
func (p *parser) parsePrimary() Node {
func (p *Parser) parsePrimary() Node {
token := p.current
if token.Is(Operator) {
@@ -402,7 +427,7 @@ func (p *parser) parsePrimary() Node {
return p.parseSecondary()
}
func (p *parser) parseSecondary() Node {
func (p *Parser) parseSecondary() Node {
var node Node
token := p.current
@@ -501,7 +526,7 @@ func (p *parser) parseSecondary() Node {
return p.parsePostfixExpression(node)
}
func (p *parser) toIntegerNode(number int64) Node {
func (p *Parser) toIntegerNode(number int64) Node {
if number > math.MaxInt {
p.error("integer literal is too large")
return nil
@@ -509,7 +534,7 @@ func (p *parser) toIntegerNode(number int64) Node {
return p.createNode(&IntegerNode{Value: int(number)}, p.current.Location)
}
func (p *parser) toFloatNode(number float64) Node {
func (p *Parser) toFloatNode(number float64) Node {
if number > math.MaxFloat64 {
p.error("float literal is too large")
return nil
@@ -517,7 +542,7 @@ func (p *parser) toFloatNode(number float64) Node {
return p.createNode(&FloatNode{Value: number}, p.current.Location)
}
func (p *parser) parseCall(token Token, arguments []Node, checkOverrides bool) Node {
func (p *Parser) parseCall(token Token, arguments []Node, checkOverrides bool) Node {
var node Node
isOverridden := false
@@ -595,7 +620,7 @@ func (p *parser) parseCall(token Token, arguments []Node, checkOverrides bool) N
return node
}
func (p *parser) parseArguments(arguments []Node) []Node {
func (p *Parser) parseArguments(arguments []Node) []Node {
// If pipe operator is used, the first argument is the left-hand side
// of the operator, so we do not parse it as an argument inside brackets.
offset := len(arguments)
@@ -616,7 +641,7 @@ func (p *parser) parseArguments(arguments []Node) []Node {
return arguments
}
func (p *parser) parsePredicate() Node {
func (p *Parser) parsePredicate() Node {
startToken := p.current
withBrackets := false
if p.current.Is(Bracket, "{") {
@@ -648,7 +673,7 @@ func (p *parser) parsePredicate() Node {
return predicateNode
}
func (p *parser) parseArrayExpression(token Token) Node {
func (p *Parser) parseArrayExpression(token Token) Node {
nodes := make([]Node, 0)
p.expect(Bracket, "[")
@@ -672,7 +697,7 @@ end:
return node
}
func (p *parser) parseMapExpression(token Token) Node {
func (p *Parser) parseMapExpression(token Token) Node {
p.expect(Bracket, "{")
nodes := make([]Node, 0)
@@ -725,7 +750,7 @@ end:
return node
}
func (p *parser) parsePostfixExpression(node Node) Node {
func (p *Parser) parsePostfixExpression(node Node) Node {
postfixToken := p.current
for (postfixToken.Is(Operator) || postfixToken.Is(Bracket)) && p.err == nil {
optional := postfixToken.Value == "?."
@@ -855,7 +880,7 @@ func (p *parser) parsePostfixExpression(node Node) Node {
}
return node
}
func (p *parser) parseComparison(left Node, token Token, precedence int) Node {
func (p *Parser) parseComparison(left Node, token Token, precedence int) Node {
var rootNode Node
for {
comparator := p.parseExpression(precedence + 1)