- Article -

编译原理粗浅学习笔记2.2数学表达式词法分析

分类于 编译原理 标签 编译原理 笔记 词法分析 发表于2020-09-19 20:30

学习了词法分析之后,就来一波实操,我们仿造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