学习了词法分析之后,就来一波实操,我们仿造golang的源代码做一个简单的词法分析,由于是学习,我们尽量做得清晰一点,不涉及优化部分,分别设计三个包token、scanner、lexer,token用来描述数学表达式最基础的组成,scanner是一个扫描器,lexer产生Token 。
Token
type Pos int
type Token struct {
Lit string
Type int
Pos Pos
}
//token type
const (
ILLEGAL int = iota
NUMBER // 12345 123.45
OPERATOR //+-*/()
)
func (tok Token) IsNumber() bool { return tok.Type == NUMBER }
func (tok Token) IsOperator() bool { return tok.Type == OPERATOR }
const LowestPrec = 0
//操作符的优先级
func (tok Token) Precedence() int {
if !tok.IsOperator() {
return LowestPrec
}
switch tok.Lit {
case "+", "-":
return 1
case "*", "/":
return 2
case "(", ")":
return 3
}
return LowestPrec
}
func (tok Token) String() string {
return fmt.Sprintf("{Lit:%s,Type:%v,Pos:%v}", tok.Lit, tok.Type, tok.Pos)
}
实现 Token 的 String 方法有助于输出清晰的Token串。
scanner
type Scanner struct {
src string // source
ch string // current 当前字符
offset int // character offset
err error
eof bool //扫描结束
}
func New(expr string) *Scanner {
return &Scanner{
src: expr,
ch: string(expr[0]),
offset: 0,
err: nil,
eof: false,
}
}
func (s *Scanner) String() string {
return fmt.Sprintf("&Scanner{\n\tsrc:%s\n\tch:%s\n\toffset:%v\n\teof:%v\n}\n", s.src, s.ch, s.offset, s.eof)
}
func (s *Scanner) EOF() bool {
return s.eof
}
func (s *Scanner) Err() error {
return s.err
}
func (s *Scanner) Scan() *token.Token {
s.skipWhitespace()
switch ch := s.ch; {
case isOperator(ch):
tok := &token.Token{
Lit: ch,
Type: token.OPERATOR,
Pos: s.offset,
}
s.next()
return tok
case isDecimal(ch):
lit, t, p := s.scanNumber()
return &token.Token{
Lit: lit,
Type: t,
Pos: p,
}
default:
s.err = errors.New(fmt.Sprintf("expr err pos:%v", s.offset))
return &token.Token{
Lit: "",
Type: token.ILLEGAL,
Pos: s.offset,
}
}
}
func (s *Scanner) skipWhitespace() {
for s.ch == " " || s.ch == "\t" || s.ch == "\n" {
s.next()
}
}
func (s *Scanner) next() {
length := len(s.src) - 1
if s.offset >= length {
s.offset = length
s.eof = true
return
}
s.offset++
s.ch = string(s.src[s.offset])
}
func (s *Scanner) peek() string {
if s.offset < len(s.src)-1 {
return string(s.src[s.offset+1])
}
return ""
}
func (s *Scanner) scanNumber() (string, int, int) {
lit := s.ch
t := token.NUMBER
p := s.offset
for {
s.next()
if isDecimal(s.ch) {
lit += string(s.ch)
continue
}
if s.ch == "." {
if !isDecimal(s.peek()) {
s.err = errors.New(fmt.Sprintf("expr err pos:%v", s.offset))
return lit, token.ILLEGAL, p
}
lit += string(s.ch)
continue
}
return lit, t, p
}
}
func isOperator(cb string) bool {
return cb == "+" || cb == "-" || cb == "*" || cb == "/" || cb == "(" || cb == ")"
}
func isDecimal(cb string) bool {
return cb == "0" || cb == "1" || cb == "2" || cb == "3" || cb == "4" || cb == "5" || cb == "6" || cb == "7" || cb == "8" || cb == "9"
}
lexer
func Tokenizer(expr string) ([]*token.Token, error) {
scan := scanner.New(expr)
var tokens []*token.Token
for {
if scan.EOF() {
break
}
tok := scan.Scan()
if scan.Err() != nil {
return tokens, scan.Err()
}
tokens = append(tokens, tok)
}
return tokens, nil
}
输出效果
var src = "1+54*(9.5+10)"
tokens, err := lexer.Tokenizer(src)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(tokens)
[
{Lit:1,Type:1,Pos:0}
{Lit:+,Type:2,Pos:1}
{Lit:54,Type:1,Pos:2}
{Lit:*,Type:2,Pos:4}
{Lit:(,Type:2,Pos:5}
{Lit:9.5,Type:1,Pos:6}
{Lit:+,Type:2,Pos:9}
{Lit:10,Type:1,Pos:10}
{Lit:),Type:2,Pos:12}
]
提示:在词法分析阶段设计 Token 的时候切记不要将太多的内容设计为一个 Token ,比如在 markdown 语法中,将 图片语法设计为一个token可以省下不少事情,但是这种做法都是错的,首先这样设计包含了一定的语义,其次表示不了语法的 title 属性,token 的设计一定是最细粒度的,尽量不要做其他步骤(语法分析、语义分析)的事情,需要明确我们这一步就是做词法分析。
使用GOLANG的标准库来做词法分析
上面是我们自己手工构建的词法分析,我们也可以调用go的词法分析来分析我们的表达式和程序。
src := []byte("1+54*(9.5+10)")
var s scanner.Scanner
fset := token.NewFileSet()
file := fset.AddFile("", fset.Base(), len(src))
s.Init(file, src, nil /* no error handler */, scanner.ScanComments)
// Repeated calls to Scan yield the token sequence found in the input.
for {
pos, tok, lit := s.Scan()
if tok == token.EOF {
break
}
fmt.Printf("%s\t%s\t%q\n", fset.Position(pos), tok, lit)
}
输出:
1:1 INT "1"
1:2 + ""
1:3 INT "54"
1:5 * ""
1:6 ( ""
1:7 FLOAT "9.5"
1:10 + ""
1:11 INT "10"
1:13 ) ""
1:14 func "func"
可以看到 go 的 token 更规范,而我们对于所有数字字面量类型都是NUMBER