自己动手写编译器: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()
}

代码运行后输出结果如下:
请添加图片描述
我们简单分析一下输出结果,从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语句,我们看看代码运行结果:

请添加图片描述
我们分析一下生成的指令,现在我们的代码已经比较复杂了,我们需要关注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()
}

最后我们看看运行结果:
请添加图片描述
我们分析一下结果,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恰好就是循环体的入口,如此看来我们生成代码的逻辑基本正确。

更详细的讲解和演示请在B站搜索Coding迪斯尼,更多干货:http://m.study.163.com/provider/7600199/index.htm?share=2&shareId=7600199,代码下载地址:
链接: https://pan.baidu.com/s/16ysPz1r_HmAKkMpSA4wUbg 提取码: j5s0

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
自己动手编译器和链接器pdf是一个非常有挑战性的任务,需要具备一定的编程和计算机原理知识。 首先,编译器是用来将高级语言代码转换成机器可执行的低级语言代码的工具。编编译器的第一步是熟悉目标语言的语法和语义规则,然后设计并实现词法分析器和语法分析器以将输入的源代码转化为语法树。接下来,需要设计并实现语义分析器,包括类型检查和语义错误检测等,在这一步中还需定义中间代码生成规则。最后,根据目标语言的特性,设计并实现代码优化器和代码生成器,将中间代码转化为目标语言的机器代码。 其次,链接器是用来将多个目标文件和库文件合并为一个可执行文件的工具。编链接器的第一步是了解目标文件和库文件的格式,包括ELF、COFF等。然后,将输入的目标文件和库文件解析为可读的数据结构,对各个模块进行地址重定位和符号解析,解决重复定义和未定义符号等问题。接下来,需要进行代码和数据的合并和对齐等操作,生成最终的可执行文件。 在编编译器和链接器的过程中,需要使用一种编程语言,例如C或C++,并利用相关的开发工具和库函数辅助实现。同时,还需要深入理解计算机的体系结构、寄存器分配和内存管理等概念。 总之,自己动手编译器和链接器pdf是一个充满挑战和学习机会的任务,需要掌握相关的编程和计算机原理知识,并有较强的实践能力和问题解决能力。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值