天天看點

自己動手寫編譯器:while,for,do等循環語句的中間代碼生成

我們的簡易編譯器完成了一大部分,但還有一些關鍵的文法結構沒有處理,那就是for, while, do..while等循環語句對應的中間代碼還沒有生成,本節我們就針對這些文法結構進行相應的中間代碼生成。

首先我們要了解循環語句對應的文法表達式:

stmt -> "while"  "( " bool ")" stmts
stmt -> "do" stmts "while" "(" bool ")" ";"
stmt-> "break"           

複制

為了簡單起見,我們暫時不處理for循環,有興趣的同學可以自己添加試試。下面我們先建立while, do..while文法結構對應的文法樹節,在inter檔案夾中建立while.go,然後添加代碼如下:

package inter

import (
    "errors"
    "fmt"
)

type While struct {
    stmt       *Stmt         //繼承自Stmt節點
    expr       ExprInterface //對應while 後面的條件判斷表達式
    while_stmt StmtInterface //對應while的循環體部分
}

func NewWhile(line uint32, expr ExprInterface, while_stmt StmtInterface) *While {
    if expr.Type().Lexeme != "bool" {
        //用于while後面的表達式必須為bool類型
        err := errors.New("bool type required for while")
        panic(err)
    }

    return &While{
        stmt:       NewStmt(line),
        expr:       expr,
        while_stmt: while_stmt,
    }
}

//下面僅僅是調用其父類接口
func (w *While) Errors(str string) error {
    return w.stmt.Errors(str)
}

func (w *While) NewLabel() uint32 {
    return w.stmt.NewLabel()
}

func (w *While) EmitLabel(label uint32) {
    w.stmt.EmitLabel(label)
}

func (w *While) Emit(code string) {
    w.stmt.Emit(code)
}

func (w *While) Gen(start uint32, end uint32) {
    w.expr.Jumping(0, end)
    label := w.NewLabel()
    w.EmitLabel(label)
    w.while_stmt.Gen(label, start) //生成while循環體語句的起始标志
    emit_code := fmt.Sprintf("goto L%d", start)
    w.Emit(emit_code)
}           

複制

上面代碼中需要注意的就是Gen函數,首先它建立跳轉标簽,注意這些标簽對循環的正确執行有着非常重要的作用,然後它先對while後面的判斷表達式生成代碼,然後對while循環體内的語句集合生成代碼,具體的邏輯講解請參看b站搜尋Coding迪斯尼參看我的調試示範。

接下來我們要做的是修改文法解析代碼,在list_parser.go中修改stmt解析函數如下:

func (s *SimpleParser) stmt() inter.StmtInterface {
    /*
        if "(" bool ")"
        if -> "(" bool ")" ELSE  "{" stmt "}"

        bool -> bool "||"" join | join
        join -> join "&&" equality | equality
        equality -> equality "==" rel | equality != rel | rel
        rel -> expr < expr | expr <= expr | expr >= expr | expr > expr | expr
        rel : a > b , a < b, a <= b
        a < b && c > d || e < f
    */
    switch s.cur_tok.Tag {
    case lexer.IF:
        s.move_forward()
        err := s.matchLexeme("(")
        if err != nil {
            panic(err)
        }
        s.move_forward()
        x := s.bool()
        err = s.matchLexeme(")")
        if err != nil {
            panic(err)
        }
        s.move_forward() //越過 )
        s.move_forward() //越過{
        s1 := s.stmt()
        err = s.matchLexeme("}")
        if err != nil {
            panic(err)
        }
        s.move_forward() //越過}

        //判斷if 後面是否跟着else
        if s.cur_tok.Tag != lexer.ELSE {
            return inter.NewIf(s.lexer.Line, x, s1)
        } else {
            s.move_forward() //越過else關鍵字
            err = s.matchLexeme("{")
            if err != nil {
                panic(err)
            }
            s.move_forward() //越過{
            s2 := s.stmt()   //else 裡面包含的代碼塊
            err = s.matchLexeme("}")
            if err != nil {
                panic(err)
            }
            return inter.NewElse(s.lexer.Line, x, s1, s2)
        }

    case lexer.WHILE:
        s.move_forward()
        //while 後面跟着左括号, 然後是判斷表達式,以右括号結尾
        err := s.matchLexeme("(")
        if err != nil {
            panic(err)
        }
        s.move_forward()
        while_bool := s.bool()
        err = s.matchLexeme(")")
        if err != nil {
            panic(err)
        }
        s.move_forward() //越過 )
        s.move_forward() //越過{
        //解析while循環成立時要執行的語句塊
        while_stmt := s.stmts()
        err = s.matchLexeme("}")
        if err != nil {
            panic(err)
        }
        s.move_forward() //越過}
        return inter.NewWhile(s.lexer.Line, while_bool, while_stmt)

    default:
        return s.expression()
    }
}           

複制

這裡我們增加了對while關鍵字的判斷,然後執行其對應的文法解析邏輯,完成上面代碼後,我們在main.go中實作包含while語句的代碼,這樣就能運作上面代碼并檢視結果:

func main() {

    source := `{int a; int b; int c; 
                a = 3;
                b = 0;
                while (a >= 0 && b <= 4) {
                    a = a - 1;
                    b = b + 1;
                }

                c = 2;

    }`
    my_lexer := lexer.NewLexer(source)
    parser := simple_parser.NewSimpleParser(my_lexer)
    parser.Parse()
}           

複制

代碼運作後輸出結果如下:

自己動手寫編譯器:while,for,do等循環語句的中間代碼生成

我們簡單分析一下輸出結果,從L4開始就是while循環體輸出的代碼,L4對應的語句就是while後面條件判斷對應的中間代碼,它表明如果a >= 0 , b <= 4 這兩個條件隻要有一個不成立 ,那麼就跳轉到L5,注意到L5正好對應while循環體出去後的第一條語句,是以生成的中間代碼其邏輯符合我們在main.go中給定代碼的意圖。如果進入L6,也就是 a>=0和b <= 4都成立,那麼就進入while循環體内部,從L6, L7可以看出他們确實是while循環體内兩條語句對應的中間代碼,注意到L7還有一條goto L4的語句,它表明循環體執行結束後再次調到循環體開頭去對條件進行判斷,如果條件依然成立,那麼代碼繼續進入L6開始的語句進行執行,要不然就直接跳轉到L5,是以從輸出結果看,它是滿足我們給定代碼邏輯的。

接着我們看看break語句的實作,break必須要出現在循環中才能成立,是以我們在遇到該語句時,需要判斷其是否位于while 或者do..while循環中,一旦執行break語句時,編譯器會使用goto語句跳轉到循環體外面接下來的語句,例如從上面例子中,接着循環體的第一條語句是L5,是以break執行時對應的輸出就是goto L5,是以要生成break語句對應的中間代碼就需要記錄它所在循環體外邊接下來第一條語句的标号。

在實作break時還有一點要注意,那就是循環嵌套,代碼可能有多個while嵌套,于是在執行break時一定要對應到給定的while上,例如:

while() {
    while() {
        while() {
            break; //對應最裡面的while
        }
        //對應中間while
    }
    break; //對應最外層while
}           

複制

是以為了應對這種情況,我們在文法解析時需要使用一個棧來記錄while循環的嵌套,是以我們首先在list_parser.go中做一些修改:

type SimpleParser struct {
    lexer          lexer.Lexer
    top            *Env
    saved          *Env
    cur_tok        lexer.Token           //目前讀取到的token
    used_storage   uint32                //目前用于存儲變量的記憶體位元組數
    loop_enclosing []inter.StmtInterface //用于循環體記錄
}           

複制

在解析到while的時候,我們要把目前生成的while節點壓入loop_enclosing棧,在解析到break語句時需要從堆棧上彈出與它對應的while節點,是以在parser函數的while部分我們要做一些修改:

case lexer.WHILE:
        s.move_forward()
        //while 後面跟着左括号, 然後是判斷表達式,以右括号結尾
        err := s.matchLexeme("(")
        if err != nil {
            panic(err)
        }
        s.move_forward()
        while_bool := s.bool()
        err = s.matchLexeme(")")
        if err != nil {
            panic(err)
        }
        s.move_forward() //越過 )
        s.move_forward() //越過{
        //解析while循環成立時要執行的語句塊
        //這裡需要注意可能解析到break語句,是以要提前生成while節點
        while_node := inter.NewWhile(s.lexer.Line, while_bool)
        //将目前while節點加入棧,解析break語句時從棧頂拿到包圍它的循環語句
        s.loop_enclosing = append(s.loop_enclosing, while_node)

        while_stmt := s.stmts()
        err = s.matchLexeme("}")
        if err != nil {
            panic(err)
        }
        s.move_forward() //越過}
        while_node.InitWhileBody(while_stmt)
        return while_node           

複制

上面代碼中我們對while的初始化也做了修改,原因是在解析它的循環體語句時可能會遇到break語句,這時候我們需要確定while節點已經生成了,是以代碼改成了先構造while節點,然後再調用stmts()去解析while内部語句,這樣解析到break語句時它才能找到對應的while節點,下面我們看看break節點的實作,在inter目錄下建立break.go,實作代碼如下:

package inter

import (
    "fmt"
)

type Break struct {
    stmt  StmtInterface //父節點
    enclosing   StmtInterface  //包裹break語句的循環體對象
}

func NewBreak(line uint32, enclosing StmtInterface) *Break {
    if _, ok := enclosing.(*While); !ok {
        //後面增加Do循環時還需修改這裡的判斷
        panic("unenclosed break") //break語句沒有處于循環體中
    }

    return &Break {
        stmt: NewStmt(line),
        enclosing: enclosing,
    }
}

//下面僅僅是調用其父類接口
func (b *Break) Errors(str string) error {
    return b.stmt.Errors(str)
}

func (b *Break) NewLabel() uint32 {
    return b.stmt.NewLabel()
}

func (b *Break) EmitLabel(label uint32) {
    b.stmt.EmitLabel(label)
}

func (b *Break) Emit(code string) {
    b.stmt.Emit(code)
}

func (b *Break)Gen(_ uint32, _ uint32) {
    enclosing_loop, _ := b.enclosing.(*While)
    code := fmt.Sprintf("goto L%d", enclosing_loop.GetAfter())
    b.Emit(code)
}           

複制

它的實作沒有什麼特别,唯一值得關注的就是Gen函數,它從對應的while節點取得循環體出去後的第一條語句位址,然後建立goto 指令直接跳轉到給定語句處。最後我們在parse函數中增加對break語句的解析:

case lexer.BREAK:
        s.move_forward()
        s.matchLexeme(";")
        enclosing_while := s.loop_enclosing[len(s.loop_enclosing)-1]
        s.loop_enclosing = s.loop_enclosing[0 : len(s.loop_enclosing)-1]
        s.move_forward()
        return inter.NewBreak(s.lexer.Line, enclosing_while)           

複制

完成上面代碼後,我們把main.go裡面要解析的代碼修改如下:

source := `{int a; int b; int c; 
                a = 3;
                b = 0;
                while (a >= 0 && b <= 4) {
                    a = a - 1;
                    b = b + 1;
                    if (b < 2) {
                        break;
                    } else {
                       c = c + 1;
                    }
                }

                c = 2;

    }`           

複制

我們在while 循環中加了if判斷,如果條件成立則執行break語句,我們看看代碼運作結果:

自己動手寫編譯器:while,for,do等循環語句的中間代碼生成

我們分析一下生成的指令,現在我們的代碼已經比較複雜了,我們需要關注L7開始部分,L7開始對應的是while循環體裡面的if語句,如果if判斷不成立就跳轉到L9,而L9正好對應else部分,也就是要執行c = c+1;如果if成立那麼直接進入L8, 而在if内部直接運作break語句,由于break語句要跳出循環體直接指向循環體外面接下來的第一條語句,而代碼中循環體外面第一條語句所在處就是L2,是以L8接下來就是goto L2,這條指令是break語句生成。問題在于後面還接着goto L4,這是為什麼?goto L4其實是else節點生成,它的作用是指向if成立部分代碼後就要跳過else部分代碼,goto L4是else出來後接下來的第一條指令,而這條指令恰巧又對應while循環體最後一條指令,是以這裡又産生了L4, 當然這條語句其實是備援,在後面生成代碼優化時我們再處理。

最後我們看看do…while…循環的實作。在inter裡面建立do.go,添加代碼如下:

package inter

import (
    "errors"
)

type Do struct {
    stmt       *Stmt         //繼承自Stmt節點
    expr       ExprInterface //對應while 後面的條件判斷表達式
    while_stmt StmtInterface //對應while的循環體部分
    after      uint32
}

//為了確定break語句能與給定的while節點對應,我們需要進行修改
func NewDo(line uint32) *Do {
    return &Do{
        stmt:       NewStmt(line),
        expr:       nil,
        while_stmt: nil,
        after:      0,
    }
}

func (d *Do) InitDo(expr ExprInterface, while_stmt StmtInterface) {
    if expr.Type().Lexeme != "bool" {
        //用于while後面的表達式必須為bool類型
        err := errors.New("bool type required for Do...While")
        panic(err)
    }
    d.while_stmt = while_stmt
    d.expr = expr
}

//下面僅僅是調用其父類接口
func (d *Do) Errors(str string) error {
    return d.stmt.Errors(str)
}

func (d *Do) NewLabel() uint32 {
    return d.stmt.NewLabel()
}

func (d *Do) EmitLabel(label uint32) {
    d.stmt.EmitLabel(label)
}

func (d *Do) Emit(code string) {
    d.stmt.Emit(code)
}

func (d *Do) setAfter(after uint32) {
    d.after = after
}

func (d *Do) GetAfter() uint32 {
    return d.after
}

func (d *Do) Gen(start uint32, end uint32) {
    d.setAfter(end) //記錄下循環體外第一句代碼的标号
    label := d.NewLabel()
    d.while_stmt.Gen(start, label) //生成while循環體語句的起始标志
    d.EmitLabel(label)
    d.expr.Jumping(start, 0)
}           

複制

do 節點實作跟while沒有太大差别,隻是跳轉的位置稍微有些差異。接着我們在解析時要添加對do語句的處理,代碼如下:

case lexer.DO:
        s.move_forward()
        do_node := inter.NewDo(s.lexer.Line)
        //将目前do節點加入棧,解析break語句時從棧頂拿到包圍它的循環語句
        s.loop_enclosing = append(s.loop_enclosing, do_node)

        //解析do的循環體部分
        err := s.matchLexeme("{")
        if err != nil {
            panic(err)
        }
        s.move_forward() //越過{
        while_stmt := s.stmts()
        err = s.matchLexeme("}")
        if err != nil {
            panic(err)
        }
        s.move_forward() //越過}

        s.matchLexeme("while")
        s.move_forward()
        s.matchLexeme("(")
        s.move_forward()
        expr := s.bool()
        s.matchLexeme(")")
        s.move_forward()
        s.matchLexeme(";")
        s.move_forward()
        do_node.InitDo(expr, while_stmt)
        return do_node           

複制

完成上面代碼後,我們修改一下要編譯的代碼,在main.go中修改如下:

func main() {
    /*
     if (b < 2) {
                            break;
                        } else {
                           c = c + 1;
                        }
    */
    source := `{int a; int b; int c; 
                a = 3;
                b = 0;
                do {
                    if (b < 2) {
                            break;
                        } else {
                           c = c + 1;
                        }
                } while (a >= 0 && b <= 4);

                c = 2;

    }`
    my_lexer := lexer.NewLexer(source)
    parser :=![請添加圖檔描述](https://img-blog.csdnimg.cn/1f14e7be0a74446dab2f689d40873c21.png)
 simple_parser.NewSimpleParser(my_lexer)
    parser.Parse()
}           

複制

最後我們看看運作結果:

自己動手寫編譯器:while,for,do等循環語句的中間代碼生成

我們分析一下結果,L4對應循環體内部的if語句,如果b<2不成立,那麼跳轉到L8,可以看到L8對應的正好是else部分語句,如果成立,那麼直接進入L7,其中它有兩條goto語句,第一條跳轉到L5,那裡對應正好是do..while循環出去後的第一條語句,goto L6是else語句塊生成的跳轉,它的目的是當if成立後,執行了if成立時的語句塊,那麼就要越過else部分,而L8就是else部分代碼入口,顯然這裡兩個goto語句是一種備援,我們需要在代碼優化部分再進行處理。

L6對應的正好就是while的判斷語句,如果循環條件a>=0不成立,那麼跳到L9,但是L9沒有指令,是以直接進入L5,也就是跳出了循環,如果a >=0 成立,那麼再判斷b <= 4是否成立,不成立同樣進入L9然後進入L5于是跳出循環,如果成立那麼進入L4,而L4恰好就是循環體的入口,如此看來我們生成代碼的邏輯基本正确。

代碼下載下傳位址:

連結: https://pan.baidu.com/s/16ysPz1r_HmAKkMpSA4wUbg 提取碼: j5s0