Golang 編譯原理 計算器(通俗易懂)

本文不需要你掌握任何編譯原理的知識。 只需要看懂簡單的golang語言即可, 完整的代碼示例在GIT, 代碼是從writing an interpreter in go這本書抽取了簡單的部分出來, 如果需要進一步了解,請詳閱此書.

聽到編譯原理,就覺得很高大上。記得上大學時,這門課要記憶一些BNF,LEX,AST,CFG這些有的沒的。一個聽不懂,二個沒興趣。隨著使用了幾門語言之后,也嘗試用編譯原理的基本知識寫過一個sql轉es的工具之后。發(fā)現(xiàn)其實了解一點點編譯原理的知識,能夠提高我們的生產(chǎn)效率,做出一些很酷的小工具來。

本文將用golang和編譯原理的基本技術實現(xiàn)一個計算器。雖然功能簡單,網(wǎng)上也有很多人做過類似事情,但這篇博客會有三個優(yōu)點:

  • 我暫時沒有找到有人用golang寫
  • 我會用最直白的語言去描述我們要做什么,這樣當你閱讀的時候,會發(fā)現(xiàn)該步驟和書中哪一步是對應的,幫助你更好的理解編譯原理的知識。
  • 我會用測試驅動整個博客和代碼,會讓大家看到如何慢慢得演化出這個計算器得解釋器。就像小說中人物的黑化有一個發(fā)酵的過程才會好看,我希望在本文中能夠讓讀者看到一個解釋器編寫發(fā)酵的過程。

目標

整體會實現(xiàn)一個函數(shù),輸入一個String, 輸出一個int64。

// calc.go
func calc(input string) int64 {
}

而我們的終極目標是能夠讓我們的calc的方法能夠通過以下的測試

// calc_test.go
func TestFinal(t *testing.T) {
    tests := []struct{
        input string
        expected int64
    }{
        {"5", 5},
        {"10", 10},
        {"-5", -5},
        {"-10", -10},
        {"5 + 5 + 5 + 5 - 10", 10},
        {"2 * 2 * 2 * 2 * 2", 32},
        {"-50 + 100 + -50", 0},
        {"5 * 2 + 10", 20},
        {"5 + 2 * 10", 25},
        {"20 + 2 * -10", 0},
        {"50 / 2 * 2 + 10", 60},
        {"2 * (5 + 10)", 30},
        {"3 * 3 * 3 + 10", 37},
        {"3 * (3 * 3) + 10", 37},
        {"(5 + 10 * 2 + 15 / 3) * 2 + -10", 50},
    }

    for _, tt := range tests{
        res := Calc(tt.input)
        if res != tt.expected{
            t.Errorf("Wrong answer, got=%d, want=%d", res, tt.expected)
        }
    }
}

我們運行這個測試,毫無疑問會失敗。不過沒關系,我們先把這個測試放到一邊,我們從編譯器最簡單的開始。

把句子變成一個一個單詞

首先我們注意到上面的測試中,我們包含多個字符。有1-9 +-*/(),并且-在數(shù)字前面表示這是一個負數(shù)。我們現(xiàn)在要做一個函數(shù),將input的輸入變成一個一個單詞。那么一個計算輸入有多少種單詞呢?我們可以區(qū)分出以下幾種。值得注意的是EOF表示結束,ILLEGAL表示非法字符。

const (
    ILLEGAL = "ILLEGAL"
    EOF = "EOF"
    INT = "INT"

    PLUS = "+"
    MINUS = "-"
    BANG = "!"
    ASTERISK = "*"
    SLASH = "/"

    LPAREN = "("
    RPAREN = ")"
)

另外我們要設計一個讀取字符器,更專業(yè)的名字叫做詞法分析器。他的功能就是不斷的讀取每一個字符,然后生成我們的詞元。注意我們有兩個名詞了,一個叫詞元,一個叫詞法分析器。我們都用結構體來描述他們。另外詞法分析器的核心函數(shù)是NextToken()用于獲取下一個詞元。

type Token struct {
    Type string  //對應我們上面的詞元類型
    Literal string // 實際的string字符
}

type Lexer struct {
    input string // 輸入
    position int   // 當前位置                                                                                                                                                                                                                                                                                                                                                                                               
    readPosition int  // 將要讀取的位置
    ch byte //當前字符
}

func (l *Lexer) NextToken() Token {
}

我們不著急實現(xiàn)。照例我們先設計我們的測試。這次我們要達到的目標是我們能夠將句子分成特定的詞元。

func TestTokenizer(t *testing.T) {
    input := `(5 + -10 * 2 + 15 / 3) * 2`
    tests := []struct {
        expectedType    string
        expectedLiteral string
    }{
        {LPAREN, "("},
        {INT, "5"},
        {PLUS, "+"},
        {MINUS, "-"},
        {INT, "10"},
        {ASTERISK, "*"},
        {INT, "2"},
        {PLUS, "+"},
        {INT, "15"},
        {SLASH, "/"},
        {INT, "3"},
        {RPAREN, ")"},
        {ASTERISK, "*"},
        {INT, "2"},
    }

    l := NewLex(input)

    for i, tt := range tests {
        tok := l.NextToken()

        if tok.Type != tt.expectedType {
            t.Fatalf("tests[%d] - tokentype wrong. expected=%q, got=%q",
                i, tt.expectedType, tok.Type)
        }

        if tok.Literal != tt.expectedLiteral {
            t.Fatalf("tests[%d] - literal wrong. expected=%q, got=%q",
                i, tt.expectedLiteral, tok.Literal)
        }
    }

}

ok , 為了通過這個測試。我們來實現(xiàn)NextToken()這個函數(shù),首先構建幾個輔助函數(shù)。
首先我們給lexer提供一個動作函數(shù)readChar。這個函數(shù)不斷讀取字符,并且更新結構體的值

func (l *Lexer) readChar() {
    if l.readPosition >= len(l.input) {
        l.ch = 0
    } else {
        l.ch = l.input[l.readPosition]
    }
    l.position = l.readPosition
    l.readPosition += 1
}

另外再來一個skipWhitespace用于在讀取時候直接跳過空白字符

func (l *Lexer) skipWhitespace() {
    for l.ch == ' ' || l.ch == '\t' || l.ch == '\n' || l.ch == '\r' {
        l.readChar()
    }
}

其實我們讀取詞源挺簡單的,除了像123這種幾位數(shù)字,其他都是單個字符做一個詞元。我們搞一個函數(shù)專門來讀數(shù)字,不過我們先搞一個函數(shù)判斷字符是不是數(shù)字,這里原理很簡單,如果是數(shù)字不斷讀下一個,讀到不是數(shù)字為止。

func isDigit(ch byte) bool {
    return '0' <= ch && ch <= '9'
}

func (l *Lexer) readNumber() string {
    position := l.position
    for isDigit(l.ch) {
        l.readChar()
    }
    return l.input[position:l.position]
}

好了。我們可以開始寫NextToken這個核心函數(shù)啦。其實很簡單,一個switch當前字符,針對不同字符返回不同的Token結構值

func (l *Lexer) NextToken() Token {
    var tok Token

    l.skipWhitespace()

    switch l.ch {
    case '(':
        tok = newToken(LPAREN, l.ch)
    case ')':
        tok = newToken(RPAREN, l.ch)
    case '+':
        tok = newToken(PLUS, l.ch)
    case '-':
        tok = newToken(MINUS, l.ch)
    case '/':
        tok = newToken(SLASH, l.ch)
    case '*':
        tok = newToken(ASTERISK, l.ch)
    case 0:
        tok.Literal = ""
        tok.Type = EOF
    default:
        if isDigit(l.ch) {
            tok.Type = INT
            tok.Literal = l.readNumber()
            return tok
        } else {
            tok = newToken(ILLEGAL, l.ch)
        }
    }

    l.readChar()
    return tok
}

OK. 在運行測試,測試就通過了,每個input都變成了每個詞元。接下來我們要高出一個ast用于運行。

把一個一個詞元組成語法樹

什么是語法/語法樹

首先語法到底是什么?比如說中文中我愛你主謂賓三種詞表示一個意思,而必須按照我愛你這三個字順序來表達,而不是用愛你我這種順序來說。這個規(guī)則便是語法。而表達的意思便是如何告訴計算機你要干什么。
那什么是語法樹呢?比如我們要計算機求1 + 2。你可以通過1 + 2這種中綴表達式寫,或者是+ 12 這種前綴表達式來表達。但最后該語法的語言大概都會解析成一樣的樹

     +
   /    \
   1    2

而這樣的樹就是語法樹,表示源代碼1+2或者+12的抽象語法結構。

那么計算表達式的語法是什么

首先我們定義兩種情況。我們在有時候會見到這種語法++i。也就是某個操作符作為前綴與后面數(shù)字發(fā)生反應。同樣還包括我們的-1。同時還有一種更加常見的情況1 + 2。操作符在中間。另外我只是是填寫一個數(shù)字類似于12。這也是一個計算表達式。 我們先把這三種情況都定義出來。
首先統(tǒng)一使用一個接口。

type Expression interface {
    String() string
}

這個接口沒什么特別的含義。另外我們依據(jù)上面考慮的三種情況實現(xiàn)三個結構體,另外都實現(xiàn)了String方法。

type IntegerLiteralExpression struct {
    Token Token
    Value int64
}

func (il *IntegerLiteralExpression) String() string { return il.Token.Literal }

type PrefixExpression struct {
    Token    Token
    Operator string
    Right    Expression
}

func (pe *PrefixExpression) String() string {
    var out bytes.Buffer

    out.WriteString("(")
    out.WriteString(pe.Operator)
    out.WriteString(pe.Right.String())
    out.WriteString(")")

    return out.String()
}

type InfixExpression struct {
    Token    Token
    Left     Expression
    Operator string
    Right    Expression
}

func (ie *InfixExpression) String() string {
    var out bytes.Buffer

    out.WriteString("(")
    out.WriteString(ie.Left.String())
    out.WriteString(" ")
    out.WriteString(ie.Operator)
    out.WriteString(" ")
    out.WriteString(ie.Right.String())
    out.WriteString(")")

    return out.String()
}

解析器

我們定義完了上面幾種expression情況。接下來用一個結構parser來把我們的字符串變成expression。parser里面包含我們上一步的lexer。以及存儲error的數(shù)組。當前的詞元和下一個詞元。另外針對于上面提到的兩種不同的expression。利用不同的處理方法。

type Parser struct {
    l *lexer.Lexer
    errors []string
    curToken token.Token
    peekToken token.Token
    prefixParseFns map[token.TokenType]prefixParseFn
    infixParseFns map[token.TokenType]infixParseFn
}

// 往結構體里面篩處理方法
func (p *Parser) registerPrefix(tokenType token.TokenType, fn prefixParseFn) {
  p.prefixParseFns[tokenType] = fn
}
func (p *Parser) registerInfix(tokenType token.TokenType, fn infixParseFn) {
  p.infixParseFns[tokenType] = fn
}

另外我們的核心函數(shù)是將lexer要變成ast,這個核心函數(shù)是ParseExpression

func (p *Parser) ParseExpression(precedence int) Expression {
}

測試

好啦,準備工作已經(jīng)做完了。那么開始寫測試。我們剛才分析計算表達式只有三個語法。我們針對三個語法做三個簡單測試

  1. 針對單個數(shù)字例如250,我們進行以下測試。這個測試主要測試兩個點,一個我們ParseExpression出來的是一個InterLieralExpression。另外一個這個AST節(jié)點的值為250。并且我們把integerLiteral的測試單獨拿出來。之后可以服用
func TestIntegerLiteralExpression(t *testing.T) {
    input := "250"
    var expectValue int64 = 250

    l := NewLex(input)
    p := NewParser(l)


    checkParseErrors(t, p)
    expression := p.ParseExpression(LOWEST)
    testInterLiteral(t, expression, expectValue)
}

 
func testInterLiteral(t *testing.T, il Expression, value int64) bool {
    integ, ok := il.(*IntegerLiteralExpression)
    if !ok {
        t.Errorf("il not *ast.IntegerLiteral. got=%T", il)
        return false
    }

    if integ.Value != value {
        t.Errorf("integ.Value not %d. got=%d", value, integ.Value)
        return false
    }
    return true
}
  1. 針對前綴表達式例如-250, 我們進行一下測試. 這個測試主要測試兩個點,一個我們ParseExpression出來的右值是InterLieralExpression。操作符是-
func TestParsingPrefixExpression(t *testing.T) {
    input := "-15"
    expectedOp := "-"
    var expectedValue int64 =  15


    l := NewLex(input)
    p := NewParser(l)
    checkParseErrors(t, p)

    expression := p.ParseExpression(LOWEST)
    exp, ok := expression.(*PrefixExpression)

    if !ok {
        t.Fatalf("stmt is not PrefixExpression, got=%T", exp)
    }

    if exp.Operator != expectedOp {
        t.Fatalf("exp.Operator is not %s, go=%s", expectedOp, exp.Operator)
    }

    testInterLiteral(t, exp.Right, expectedValue)
}
  1. 對于中綴表達式如5+5,進行如下測試,當然我們加減乘除都測試一遍
func TestParsingInfixExpression(t *testing.T) {
    infixTests := []struct{
        input string
        leftValue int64
        operator string
        rightValue int64
    }{
        {"5 + 5;", 5, "+", 5},
        {"5 - 5;", 5, "-", 5},
        {"5 * 5;", 5, "*", 5},
        {"5 / 5;", 5, "/", 5},
    }

    for _, tt := range infixTests {
        l := NewLex(tt.input)
        p := NewParser(l)
        checkParseErrors(t, p)

        expression := p.ParseExpression(LOWEST)
        exp, ok := expression.(*InfixExpression)

        if !ok {
            t.Fatalf("exp is not InfixExpression, got=%T", exp)
        }

        if exp.Operator != tt.operator {
            t.Fatalf("exp.Operator is not %s, go=%s", tt.operator, exp.Operator)
        }

        testInterLiteral(t, exp.Left, tt.leftValue)
        testInterLiteral(t, exp.Right, tt.rightValue)
    }
}

實現(xiàn)

上面測試寫完了,我們就要開始實現(xiàn)了。首先想象一下,我們將input變成了一個一個的詞元, 接下來我們對于一個又一個的詞元進行處理。我們用到的算法叫做pratt parser。這里具體不展開來講,有興趣自己閱讀。對于每一個詞元,我們都有兩個函數(shù)去處理她infixParse或者prefixParse。選擇哪個函數(shù)取決于你在哪個位置。首先我們寫一個初始化的函數(shù)newParser。

func NewParser(l *Lexer) *Parser {
    p := &Parser{
        l:      l,
        errors: []string{},
    }

    p.prefixParseFns = make(map[string]prefixParseFn)
    p.infixParseFns = make(map[string]infixParseFn)

    p.nextToken()
    p.nextToken()
    return p
}

當遇到Integer Token

考慮當我們遇到IntegerExpression時候,就是250 這樣當都一個字符。我們注冊一下這種情況的處理函數(shù)p.registerPrefix(INT, p.parseIntegerLiteral)。 處理函數(shù)這里非常簡單,我們直接返回一個IntegerLiteralExpression

func (p *Parser) parseIntegerLiteral() Expression {

    lit := &IntegerLiteralExpression{Token: p.curToken}

    value, err := strconv.ParseInt(p.curToken.Literal, 0, 64)
    if err != nil {
        msg := fmt.Sprintf("could not parse %q as integer", p.curToken.Literal)
        p.errors = append(p.errors, msg)
        return nil
    }

    lit.Value = value
    return lit
}

// 在newParser里面加上

當遇到+-*/ Token

我們支持-5 這種形式。同時我們支持5 -1這種形式。我們在newParser里面注冊兩個處理函數(shù)。同樣我們遇到+ * /其他三個token。采用parseInfixExpression

// func NewParser
    p.registerPrefix(MINUS, p.parsePrefixExpression)

    p.registerInfix(MINUS, p.parseInfixExpression)

    p.registerInfix(PLUS, p.parseInfixExpression)
    p.registerInfix(MINUS, p.parseInfixExpression)
    p.registerInfix(SLASH, p.parseInfixExpression)
    p.registerInfix(ASTERISK, p.parseInfixExpression)

如何實現(xiàn)parsePrefixExpression很簡單,獲取當前Token。也就是-。下一個TOken是數(shù)字。我們遞歸使用ParseExpression解析出來。不出錯的話。這里解析出來的是一個IntegerLiteral

func (p *Parser) parsePrefixExpression() Expression {

    expression := &PrefixExpression{
        Token:    p.curToken,
        Operator: p.curToken.Literal,
    }
    p.nextToken()
    expression.Right = p.ParseExpression(PREFIX)
    return expression
}

parseInfixExpression差不多情況。但是有一個輸入?yún)?shù)left。比如1 + 21就是left

func (p *Parser) parseInfixExpression(left Expression) Expression {

    expression := &InfixExpression{
        Token:    p.curToken,
        Operator: p.curToken.Literal,
        Left:     left,
    }

    precedence := p.curPrecedence()
    p.nextToken()

    expression.Right = p.ParseExpression(precedence)

    return expression
}

優(yōu)先級

考慮這樣一種情況1 + 3 * 4。如果解析成語法樹。我們可以有兩種解法

            * 
         /      \
        +       4
      /    \
     1      3
            + 
         /      \
        1       *
               /    \
             3      4
  

按照我們小學教育,我們應該選擇下面的解法。也就是說乘法比加法要有更高的優(yōu)先級?;蛘哒f在我們的語法樹中乘法要比加法處于更高的位置。我們定義出以下幾個級別的優(yōu)先級,與各符號對應的優(yōu)先級

const (
    _ int = iota
    LOWEST
    SUM         // +, -
    PRODUCT     // *, /
    PREFIX      // -X
    CALL        // (X)
)

var precedences = map[string]int{
    PLUS:     SUM,
    MINUS:    SUM,
    SLASH:    PRODUCT,
    ASTERISK: PRODUCT,
    LPAREN:   CALL,
}

當遇到( ) Token

我們支持(1 + 5) * 3 這種形式。這個時候我們強制提升了1 + 5的優(yōu)先級。我們采用一個處理函數(shù)parseGroupedExpression

// func NewParser
    p.registerPrefix(MINUS, p.parseGroupedExpression)

如何實現(xiàn)用()來提升優(yōu)先級,其實就是強制讀取()內(nèi)的內(nèi)容

func (p *Parser) parseGroupedExpression() Expression {
    p.nextToken()
    exp := p.ParseExpression(LOWEST)

    if !p.expectPeek(token.RPAREN){
        return nil
    }
    return exp
}

遞歸主函數(shù)ParseExpression

我們通過當前優(yōu)先級和下一個token的優(yōu)先級進行對比,如果這個優(yōu)先級比下一個優(yōu)先級別低,那就變成infix。用parseInfixExpression處理。如果這個優(yōu)先級等于或者比下一個優(yōu)先級高,那就變成了prefix。用parsePrefixExpression處理

func (p *Parser) ParseExpression(precedence int) Expression {
    prefix := p.prefixParseFns[p.curToken.Type]
    returnExp := prefix()

    for precedence < p.peekPrecedence() {
        infix := p.infixParseFns[p.peekToken.Type]
        if infix == nil {
            return returnExp
        }

        p.nextToken()
        returnExp = infix(returnExp)
    }

    return returnExp
}

當然還有一些輔助函數(shù),這里不再贅述。運行一下測試,??通過啦

執(zhí)行語法樹得到結果

這里我們直接要開始搞定我們最開始的測試啦。首先我們豐富一下主函數(shù)。

func Calc(input string) int64 {
    lexer := NewLex(input)
    parser := NewParser(lexer)

    exp := parser.ParseExpression(LOWEST)
    return Eval(exp)
}

關鍵就是我們的Eval函數(shù)啦。這里很簡單,因為我們有三種Expression。對于不同的Expression做不同的處理方法

func Eval(exp Expression) int64 {
    switch node := exp.(type) {
    case *IntegerLiteralExpression:
        return node.Value
    case *PrefixExpression:
        rightV := Eval(node.Right)
        return evalPrefixExpression(node.Operator, rightV)
    case *InfixExpression:
        leftV := Eval(node.Left)
        rightV := Eval(node.Right)
        return evalInfixExpression(leftV, node.Operator, rightV)
    }

    return 0
}

func evalPrefixExpression(operator string, right int64) int64{
    if operator != "-" {
        return 0
    }
    return -right
}


func evalInfixExpression(left int64, operator string, right int64) int64 {

    switch operator {
    case "+":
        return left + right
    case "-":
        return left - right
    case "*":
        return left * right
    case "/":
        if right != 0{
            return left / right
        }else{
            return 0
        }
    default:
        return 0
    }
}

在運行一下測試,搞定。。。

總結

當然這里有很多東西沒講述,比如錯誤處理。但是我相信從上面走下來,比較容易理解編譯原理的一些概念。

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

  • 官網(wǎng) 中文版本 好的網(wǎng)站 Content-type: text/htmlBASH Section: User ...
    不排版閱讀 4,695評論 0 5
  • 鏈接地址:https://www.tutorialspoint.com/compiler_design/compi...
    dannyvi閱讀 4,895評論 1 12
  • Lua 5.1 參考手冊 by Roberto Ierusalimschy, Luiz Henrique de F...
    蘇黎九歌閱讀 14,235評論 0 38
  • 自律嚴格要求自己,嚴格進行下來后,自己將進步很大。 自己應對自己的工作、生活、學習和家庭有目標和要求。 工作中,應...
    百思方成Helen閱讀 181評論 0 0
  • 美麗都是錢花出來的,這句話一點都沒錯,看了這本書,我似乎感覺自己在變美麗的旅途中任重道遠,長期堅持,這也是變美麗的...
    和自己較個勁閱讀 357評論 0 0

友情鏈接更多精彩內(nèi)容