Add lexer

This commit is contained in:
Anton Medvedev
2018-07-14 11:34:12 +03:00
commit 57998c1085
2 changed files with 463 additions and 0 deletions
+319
View File
@@ -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
View File
@@ -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)
}
}
}