mirror of
https://github.com/hanzoai/expr.git
synced 2026-09-24 16:58:58 +00:00
Add lexer
This commit is contained in:
@@ -0,0 +1,319 @@
|
||||
package expr
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
type tokenKind int
|
||||
|
||||
const (
|
||||
name tokenKind = iota
|
||||
number
|
||||
text
|
||||
operator
|
||||
punctuation
|
||||
eof = -1
|
||||
)
|
||||
|
||||
type token struct {
|
||||
kind tokenKind
|
||||
value string
|
||||
pos int
|
||||
}
|
||||
|
||||
// is tests if token kind and value matches
|
||||
func (t token) is(kind tokenKind, values ...string) bool {
|
||||
var value *string
|
||||
if len(values) == 1 {
|
||||
value = &values[0]
|
||||
}
|
||||
return t.kind == kind && (value == nil || *value == t.value)
|
||||
}
|
||||
|
||||
func (t token) String() string {
|
||||
switch t.kind {
|
||||
case name:
|
||||
return fmt.Sprintf("name(%s)", t.value)
|
||||
case number:
|
||||
return fmt.Sprintf("number(%s)", t.value)
|
||||
case text:
|
||||
return fmt.Sprintf("text(%q)", t.value)
|
||||
case operator:
|
||||
return fmt.Sprintf("operator(%s)", t.value)
|
||||
case punctuation:
|
||||
return fmt.Sprintf("punctuation(%q)", t.value)
|
||||
case eof:
|
||||
return "EOF"
|
||||
default:
|
||||
return t.value
|
||||
}
|
||||
}
|
||||
|
||||
type stateFn func(*lexer) stateFn
|
||||
|
||||
type lexer struct {
|
||||
input string // the string being scanned
|
||||
pos int // current position in the input
|
||||
start int // start position of this token
|
||||
width int // width of last rune read from input
|
||||
brackets []rune // stack of brackets
|
||||
tokens []token // slice of scanned tokens
|
||||
err error // last error
|
||||
}
|
||||
|
||||
// next returns the next rune in the input.
|
||||
func (l *lexer) next() rune {
|
||||
if l.pos >= len(l.input) {
|
||||
l.width = 0
|
||||
return eof
|
||||
}
|
||||
r, w := utf8.DecodeRuneInString(l.input[l.pos:])
|
||||
l.width = w
|
||||
l.pos += l.width
|
||||
return r
|
||||
}
|
||||
|
||||
// peek returns but does not consume the next rune in the input.
|
||||
func (l *lexer) peek() rune {
|
||||
r := l.next()
|
||||
l.backup()
|
||||
return r
|
||||
}
|
||||
|
||||
// backup steps back one rune. Can only be called once per call of next.
|
||||
func (l *lexer) backup() {
|
||||
l.pos -= l.width
|
||||
}
|
||||
|
||||
func (l *lexer) word() string {
|
||||
return l.input[l.start:l.pos]
|
||||
}
|
||||
|
||||
func (l *lexer) emit(t tokenKind) {
|
||||
l.emitValue(t, l.word())
|
||||
}
|
||||
|
||||
func (l *lexer) emitValue(t tokenKind, value string) {
|
||||
c := len(l.tokens)
|
||||
// Special case for joining "not" and ".." operators
|
||||
if c > 0 && l.tokens[c-1].is(operator, "not") && t == operator && value == "in" {
|
||||
l.tokens[c-1].value = "not in"
|
||||
} else if c > 0 && l.tokens[c-1].is(punctuation, ".") && t == punctuation && value == "." {
|
||||
l.tokens[c-1].kind = operator
|
||||
l.tokens[c-1].value = ".."
|
||||
} else {
|
||||
l.tokens = append(l.tokens, token{
|
||||
kind: t,
|
||||
value: value,
|
||||
pos: l.start,
|
||||
})
|
||||
}
|
||||
l.start = l.pos
|
||||
}
|
||||
|
||||
// ignore skips over the pending input before this point.
|
||||
func (l *lexer) ignore() {
|
||||
l.start = l.pos
|
||||
}
|
||||
|
||||
func (l *lexer) accept(valid string) bool {
|
||||
if strings.ContainsRune(valid, l.next()) {
|
||||
return true
|
||||
}
|
||||
l.backup()
|
||||
return false
|
||||
}
|
||||
|
||||
func (l *lexer) acceptRun(valid string) {
|
||||
for strings.ContainsRune(valid, l.next()) {
|
||||
}
|
||||
l.backup()
|
||||
}
|
||||
|
||||
func (l *lexer) errorf(format string, args ...interface{}) stateFn {
|
||||
l.err = &syntaxError{
|
||||
message: fmt.Sprintf(format, args...),
|
||||
input: l.input,
|
||||
pos: l.start,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func lex(input string) ([]token, error) {
|
||||
l := &lexer{
|
||||
input: input,
|
||||
tokens: make([]token, 0),
|
||||
}
|
||||
for state := lexRoot; state != nil; {
|
||||
state = state(l)
|
||||
}
|
||||
return l.tokens, l.err
|
||||
}
|
||||
|
||||
// state functions
|
||||
|
||||
func lexRoot(l *lexer) stateFn {
|
||||
switch r := l.next(); {
|
||||
case r == eof:
|
||||
if len(l.brackets) > 0 {
|
||||
return l.errorf("unclosed %q", string(l.brackets[0]))
|
||||
}
|
||||
l.emit(eof)
|
||||
return nil
|
||||
case isSpace(r):
|
||||
l.ignore()
|
||||
return lexRoot
|
||||
case r == '\'' || r == '"':
|
||||
l.backup()
|
||||
return lexQuote
|
||||
case '0' <= r && r <= '9':
|
||||
l.backup()
|
||||
return lexNumber
|
||||
case strings.ContainsRune("([{", r):
|
||||
l.emit(punctuation)
|
||||
l.brackets = append(l.brackets, r)
|
||||
case strings.ContainsRune(")]}", r):
|
||||
if len(l.brackets) > 0 {
|
||||
bracket := l.brackets[len(l.brackets)-1]
|
||||
l.brackets = l.brackets[:len(l.brackets)-1]
|
||||
if isBracketMatch(bracket, r) {
|
||||
l.emit(punctuation)
|
||||
} else {
|
||||
return l.errorf("unclosed %q", string(bracket))
|
||||
}
|
||||
} else {
|
||||
return l.errorf("unexpected %q", string(r))
|
||||
}
|
||||
case strings.ContainsRune(".,?:", r):
|
||||
l.emit(punctuation)
|
||||
case strings.ContainsRune("!%&*+-/<=>^|~", r):
|
||||
l.backup()
|
||||
return lexOperator
|
||||
case isAlphaNumeric(r):
|
||||
l.backup()
|
||||
return lexName
|
||||
default:
|
||||
return l.errorf("unrecognized character: %#U", r)
|
||||
}
|
||||
return lexRoot
|
||||
}
|
||||
|
||||
func lexNumber(l *lexer) stateFn {
|
||||
if !l.scanNumber() {
|
||||
return l.errorf("bad number syntax: %q", l.word())
|
||||
}
|
||||
l.emit(number)
|
||||
return lexRoot
|
||||
}
|
||||
|
||||
func (l *lexer) scanNumber() bool {
|
||||
// Is it hex?
|
||||
digits := "0123456789"
|
||||
l.acceptRun(digits)
|
||||
if l.accept(".") {
|
||||
// Lookup for .. operator.
|
||||
if l.peek() == '.' {
|
||||
l.backup()
|
||||
return true
|
||||
}
|
||||
l.acceptRun(digits)
|
||||
}
|
||||
if l.accept("eE") {
|
||||
l.accept("+-")
|
||||
l.acceptRun("0123456789")
|
||||
}
|
||||
// Next thing mustn't be alphanumeric.
|
||||
if isAlphaNumeric(l.peek()) {
|
||||
l.next()
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func lexQuote(l *lexer) stateFn {
|
||||
quote := l.next()
|
||||
Loop:
|
||||
for {
|
||||
switch l.next() {
|
||||
case '\\':
|
||||
if r := l.next(); r != eof && r != '\n' {
|
||||
break
|
||||
}
|
||||
fallthrough
|
||||
case eof:
|
||||
return l.errorf("unterminated string")
|
||||
case quote:
|
||||
break Loop
|
||||
}
|
||||
}
|
||||
word := strings.Trim(l.word(), `"'`)
|
||||
value, err := strconv.Unquote(`"` + word + `"`)
|
||||
if err != nil {
|
||||
return l.errorf("unquote error: %v", err)
|
||||
}
|
||||
l.emitValue(text, value)
|
||||
return lexRoot
|
||||
}
|
||||
|
||||
func lexOperator(l *lexer) stateFn {
|
||||
l.next()
|
||||
l.accept("|&=*")
|
||||
l.emit(operator)
|
||||
return lexRoot
|
||||
}
|
||||
|
||||
func lexName(l *lexer) stateFn {
|
||||
Loop:
|
||||
for {
|
||||
switch r := l.next(); {
|
||||
case isAlphaNumeric(r):
|
||||
// absorb.
|
||||
default:
|
||||
l.backup()
|
||||
switch l.word() {
|
||||
case "not":
|
||||
l.emit(operator)
|
||||
case "in":
|
||||
l.emit(operator)
|
||||
case "or":
|
||||
l.emit(operator)
|
||||
case "and":
|
||||
l.emit(operator)
|
||||
case "matches":
|
||||
l.emit(operator)
|
||||
default:
|
||||
l.emit(name)
|
||||
}
|
||||
break Loop
|
||||
}
|
||||
}
|
||||
return lexRoot
|
||||
}
|
||||
|
||||
func isSpace(r rune) bool {
|
||||
return r == ' ' || r == '\t' || r == '\r' || r == '\n'
|
||||
}
|
||||
|
||||
func isAlphaNumeric(r rune) bool {
|
||||
return isAlphabetic(r) || unicode.IsDigit(r)
|
||||
}
|
||||
|
||||
func isAlphabetic(r rune) bool {
|
||||
return r == '_' || unicode.IsLetter(r)
|
||||
}
|
||||
|
||||
func isBracketMatch(open, close rune) bool {
|
||||
switch string([]rune{open, close}) {
|
||||
case "()":
|
||||
return true
|
||||
case "[]":
|
||||
return true
|
||||
case "{}":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
package expr
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type lexTest struct {
|
||||
input string
|
||||
tokens []token
|
||||
}
|
||||
|
||||
var lexTests = []lexTest{
|
||||
{
|
||||
"1 02 1e3 1.2e-4",
|
||||
[]token{
|
||||
{kind: number, value: "1"},
|
||||
{kind: number, value: "02"},
|
||||
{kind: number, value: "1e3"},
|
||||
{kind: number, value: "1.2e-4"},
|
||||
{kind: eof},
|
||||
},
|
||||
},
|
||||
{
|
||||
`"double" 'single' "abc \n\t\" "`,
|
||||
[]token{
|
||||
{kind: text, value: "double"},
|
||||
{kind: text, value: "single"},
|
||||
{kind: text, value: "abc \n\t\" "},
|
||||
{kind: eof},
|
||||
},
|
||||
},
|
||||
{
|
||||
"+0 != -0",
|
||||
[]token{
|
||||
{kind: operator, value: "+"},
|
||||
{kind: number, value: "0"},
|
||||
{kind: operator, value: "!="},
|
||||
{kind: operator, value: "-"},
|
||||
{kind: number, value: "0"},
|
||||
{kind: eof},
|
||||
},
|
||||
},
|
||||
{
|
||||
"a and b or not in c not orx",
|
||||
[]token{
|
||||
{kind: name, value: "a"},
|
||||
{kind: operator, value: "and"},
|
||||
{kind: name, value: "b"},
|
||||
{kind: operator, value: "or"},
|
||||
{kind: operator, value: "not in"},
|
||||
{kind: name, value: "c"},
|
||||
{kind: operator, value: "not"},
|
||||
{kind: name, value: "orx"},
|
||||
{kind: eof},
|
||||
},
|
||||
},
|
||||
{
|
||||
`(3 + 5) ~ foo("bar").baz[4]`,
|
||||
[]token{
|
||||
{kind: punctuation, value: "("},
|
||||
{kind: number, value: "3"},
|
||||
{kind: operator, value: "+"},
|
||||
{kind: number, value: "5"},
|
||||
{kind: punctuation, value: ")"},
|
||||
{kind: operator, value: "~"},
|
||||
{kind: name, value: "foo"},
|
||||
{kind: punctuation, value: "("},
|
||||
{kind: text, value: "bar"},
|
||||
{kind: punctuation, value: ")"},
|
||||
{kind: punctuation, value: "."},
|
||||
{kind: name, value: "baz"},
|
||||
{kind: punctuation, value: "["},
|
||||
{kind: number, value: "4"},
|
||||
{kind: punctuation, value: "]"},
|
||||
{kind: eof},
|
||||
},
|
||||
},
|
||||
{
|
||||
`1..2`,
|
||||
[]token{
|
||||
{kind: number, value: "1"},
|
||||
{kind: operator, value: ".."},
|
||||
{kind: number, value: "2"},
|
||||
{kind: eof},
|
||||
},
|
||||
},
|
||||
{
|
||||
`matches`,
|
||||
[]token{
|
||||
{kind: operator, value: "matches"},
|
||||
{kind: eof},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type lexErrorTest struct {
|
||||
input string
|
||||
err string
|
||||
}
|
||||
|
||||
var lexErrorTests = []lexErrorTest{
|
||||
{
|
||||
`{[}]`,
|
||||
`unclosed "["`,
|
||||
},
|
||||
}
|
||||
|
||||
func compareTokens(i1, i2 []token) bool {
|
||||
if len(i1) != len(i2) {
|
||||
return false
|
||||
}
|
||||
for k := range i1 {
|
||||
if i1[k].kind != i2[k].kind {
|
||||
return false
|
||||
}
|
||||
if i1[k].value != i2[k].value {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func TestLex(t *testing.T) {
|
||||
for _, test := range lexTests {
|
||||
tokens, err := lex(test.input)
|
||||
if err != nil {
|
||||
t.Errorf("%s:\n%v", test.input, err)
|
||||
continue
|
||||
}
|
||||
if !compareTokens(tokens, test.tokens) {
|
||||
t.Errorf("%s:\ngot\n\t%+v\nexpected\n\t%v", test.input, tokens, test.tokens)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLexError(t *testing.T) {
|
||||
for _, test := range lexErrorTests {
|
||||
_, err := lex(test.input)
|
||||
if !strings.HasPrefix(err.Error(), test.err) {
|
||||
t.Errorf("%s:\ngot\n\t%+v\nexpected\n\t%v", test.input, err.Error(), test.err)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user