自己动手写编译器:实现if判断中“||“和“&&“条件判断的中间代码生成

Posted tyler_download

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了自己动手写编译器:实现if判断中“||“和“&&“条件判断的中间代码生成相关的知识,希望对你有一定的参考价值。

上一节我们完成了if条件判断语句的中间代码生成,我们看到针对if语句的生成代码,我们针对if 条件满足时所要执行的代码赋予了一个跳转标签,同时对if()… 右边大括号后面的代码也赋予一个跳转标签,这样我们就能根据if条件判断成立与否进行跳转。

但是上一节实现的if条件判断比较简单,在if()括号里面我们只支持一个算术表达式,事实上它可以通过"||“和”&&“运算符支持更加复杂的表达式,也就是用这些运算符将多个表达式连接在一起,我想每一个写过几行代码的同学都会在if条件判断中使用”||“或者”&&"实现过多个判断条件的组合判断,本节我们看看这种复杂组合判断条件如何实现代码生成。

要实现复杂的组合判断条件,它涉及到比较复杂的语法规则,我们看看对应的语法表达式:

        if "(" bool ")" 
		bool -> bool "||"" join | join
		join -> join "&&" equality | equality
		equality -> equality "==" rel | equality != rel | rel 
		rel -> expr < expr | expr <= expr | expr >= expr | expr > expr | expr

我们通过具体例子来理解上面规则,形如"a > b", “a <= 3”,”“等这类语句显然对应表达式rel,由于rel包含了expr,因此算术表达式例如"a+2", “c+d"也是与rel的范畴。我们继续往上一层走也就是equality,它是rel根据符号”==“和”!=“进行的组合,于是"a!=b”, "c+d!=e-f"就属于equality, 注意到 “a>b != c -d"也属于equality对应的规则,虽然这个表达式看起来比较诡异。再往上走equality 对应的表达式可以使用符号”&&"连接起来,于是类似"a>b && c > d"就属于jion的范畴。继续往上走,类似"a>b && c>d || e<f && g > h"则属于boll的范畴。

不知道你能否看出一个规律,那就是越往下对应符号的优先级就越高,例如"a>b && c>d || e < f",这样的语句在自行时,我们先处理由"&&“连接的表达式,然后再处理”||“连接的表达式,于是给定语句中,编译器要先处理 a>b && c > d的结果,然后再用这个表达式的结果进行”||”运算,这种方法也是编译器确定运算符优先级时常用的方法。

下面我们看看相应代码的实现,上一节我们已经实现了bool函数,在该函数中我们实际上实现的是rel,因为我们在里面直接判断了算术表达式是不是由<, >=, 等这类符号连接的,因此我们把上一节在bool里面的代码抽离出来形成rel函数:

func(s *SimpleParser) rel() *inter.Rel 
    expr1 := s.expr()
	var tok *lexer.Token

	switch s.cur_tok.Tag 
	case lexer.LE:
	case lexer.LESS_OPERATOR:
		fallthrough
	case lexer.GE:
		fallthrough
	case lexer.GREATER_OPERATOR:
		fallthrough
	case lexer.NE:
		tok = lexer.NewTokenWithString(s.cur_tok.Tag, s.lexer.Lexeme)
	default:
		tok = nil
	

	if tok == nil 
		panic("wrong operator in if")
	
	s.move_forward()
	expr2 := s.expr()
	return inter.NewRel(s.lexer.Line, tok, expr1, expr2)

根据前面给定的语法规则 bool -> bool “||” join | join ,我们在bool函数中首先执行join函数,如果接下来遇到符号"||"那么就持续再次调用join函数进行解析,于是bool函数代码变成如下模式:

func (s *SimpleParser) bool() inter.ExprInterface 
    expr := join() 
	for s.cur_tok.Tag == lexer.OR 
		tok := lexer.NewTokenWithString(s.cur_tok.Tag, s.lexer.Lexeme)
		s.move_forward()
		x := new (tok, expr, s.join())
		

	return x

上面代码中,我们根据语法解析式的逻辑,首先解析join部分,然后判断是否跟着"||"符号,如果是则持续进行join解析,最后生成一个OR节点进行返回,于是这里有两部分我们需要进一步处理,一部分是join函数的实现,一部分是or节点的设计,我们先看后者,在inter目录中创建or.go文件,其实现代码如下:

package inter

import (
	"lexer"
)

type Or struct 
	logic *Logic  //它负责处理||, &&, !,等操作符一些共同的逻辑
	expr1 *Expr // "||"前面的表达式
	expr2 *Expr  // "||"后面的表达式


func NewOr(line uint32, token *lexer.Token,
	expr1 *Expr, expr2 *Expr) *Or 
	return &Or
		logic: NewLogic(line, token, expr1, expr2, logicCheckType),
		expr1: expr1,
		expr2: expr2,
	


func (o *Or) Errors(s string) error 
	return o.logic.Errors(s)


func (o *Or) NewLabel() uint32 
	return o.logic.NewLabel()


func (o *Or) EmitLabel(l uint32) 
	o.logic.EmitLabel(l)


func (o *Or) Emit(code string) 
	o.logic.Emit(code)


func (o *Or) Gen() ExprInterface 
	return o.logic.Gen()


func (o *Or) Reduce() ExprInterface 
	return o


func (o *Or) Type() *Type 
	return o.logic.Type()


func (o *Or) ToString() string 
	return o.logic.ToString()


func (o *Or) Jumping(t uint32, f uint32) 
	var label uint32
	if t != 0 
		label = t
	 else 
		label = o.NewLabel()
	

	o.expr1.Jumping(label, 0)
	o.expr2.Jumping(t, f)
	if t == 0 
		o.EmitLabel(label)
	


func (o *Or) EmitJumps(test string, t uint32, l uint32) 
	o.logic.EmitJumps(test, t, l)


在OR节点的实现中,它在创建时需要三个字段,分别是token,它对应运算符"||“,其次是两个算术表达式,分别对应”||“左右两边的算术表达式。在代码实现中需要使用一个名为Logic的对象,它的责任是用于处理”||", “&&”, "!"等符号对应表达式需要的一些共同操作,它的实现我们一会再看,现在需要看看它的Jumping代码实现逻辑。

假设我们给定的表达式为"a || b",那么expr1对应符号a,expr2对应符号b,假设执行Jumping接口调用时输入参数为1,2,那么o.expr1.Jumping(label, 0) 就会生成中间代码:

if a goto 1

同时o.expr2.Jumping(t,f)生成的代码就是:

if b goto 1
goto 2

如果两部分是比较复杂的表达式,例如
我们看到在运行"a||b"这个表达式的跳转逻辑时,编译器首先判断第一个表达式是否为真,如果为真则直接跳转到if大括号里面的代码,这里对应的就是标号1,如果为假,那么才继续判断第二个表达式。接下来我们看看Logic节点的实现内容,创建logic.go,实现代码如下:

package inter

import (
	"errors"
	"lexer"
	"strconv"
)



/*
实现or, and , !等操作
*/

type Logic struct 
	expr      ExprInterface
	token     *lexer.Token
	expr1     ExprInterface
	expr2     ExprInterface
	expr_type *Type
	line      uint32


type CheckType func(type1 *Type, type2 *Type) *Type

func logicCheckType(type1 *Type, type2 *Type) *Type 

	if type1.Lexeme == "bool" && type2.Lexeme == "bool" 
		return type1
	

	return nil


func NewLogic(line uint32, token *lexer.Token,
	expr1 ExprInterface, expr2 ExprInterface, checkType CheckType) *Logic 
	expr_type := checkType(expr1.Type(), expr2.Type())
	if expr_type == nil 
		err := errors.New("type error")
		panic(err)
	

	return &Logic
		expr:      NewExpr(line, token, expr_type),
		token:     token,
		expr1:     expr1,
		expr2:     expr2,
		expr_type: expr_type,
		line:      line,
	


func (l *Logic) Errors(s string) error 
	return l.expr.Errors(s)


func (l *Logic) NewLabel() uint32 
	return l.expr.NewLabel()


func (l *Logic) EmitLabel(label uint32) 
	l.expr.EmitLabel(label)


func (l *Logic) Emit(code string) 
	l.expr.Emit(code)


func (l *Logic) Type() *Type 
	return l.expr_type


func (l *Logic) Gen() ExprInterface 
	f := l.NewLabel()
	a := l.NewLabel()
	temp := NewTemp(l.line, l.expr_type)
	l.Jumping(0, f)
	l.Emit(temp.ToString() + " = true")
	l.Emit("goto L" + strconv.Itoa(int(a)))
	l.EmitLabel(f)
	l.Emit(temp.ToString() + "=false")
	l.EmitLabel(a)
	return temp


func (l *Logic) Reduce() ExprInterface 
	return l


func (l *Logic) ToString() string 
	return l.expr1.ToString() + " " + l.token.ToString() + " " + l.expr2.ToString()


func (l *Logic) Jumping(t uint32, f uint32) 
	l.expr.Jumping(t, f)


func (l *Logic) EmitJumps(test string, t uint32, f uint32) 
	l.expr.EmitJumps(test, t, f)


它的作用是首先确定符号"||“, “&&”, 作用两边的表达式是否为bool类型,只有各个类型才能进行相应操作,也就是目前我们的编译器支持这样的语句"if(a > b || c < d)”,但是暂时不支持"if ( || b)",事实上对于全功能编译器而言,它其实会在暗地里将a, b等算术表达式转换为bool类型,为了简单起见,我们暂时忽略这种转换。

上面代码中Gen函数的实现逻辑有点诡异,if条件判断语句除了生成跳转代码外,它还能生成其他代码,后面我们在调试代码时会看到它的作用,在这里我们先放一放对它的理解。现在我们看看join函数的实现,在语法表达式里,join对应"&&"操作符的处理,为了简单起见,我们这里直接让它调用rel函数,然后先把当前实现的代码运行起来看看,于是join的实现就是:

func (s *SimpleParser) join() inter.ExprInterface 
    return s.rel()

完成上面代码后,我们在main.go设计一段代码,然后进行编译和代码生成:

unc main() 

	source := `int a; int b; int c; int d;
		        int e;
		        a = 1;
				b = 2;
				c = 3;
				d = 4;
				if (b > a || c < d) 
					e = 2;
				
				e = 3;
	`
	my_lexer := lexer.NewLexer(source)
	parser := simple_parser.NewSimpleParser(my_lexer)
	parser.Parse()

执行后生成的中间代码如下:


在生成的代码中,需要我们注意的是if语句生成的代码,首先是if b > a goto L9,这里L9标签没有任何代码,因此进入L9后就会直接进入L8,而L8对应的是给变量e赋值2,这与我们代码的逻辑一致。如果执行if b > a后没有跳转到L9,那说明b>a不成立,于是判断第二个条件c < d,这里编译器使用iffalse进行判断,如果c < d不成立,那么直接跳转到L7,而L7对应的是给变量e赋值3,这与我们代码的逻辑一致。

接下来我们看看&&操作符如何实现,首先跟前面的“||”操作符一样,我们需要建立一个名为AND的节点,创建and.go,实现代码如下:

package inter

import (
	"lexer"
)

type And struct 
	logic *Logic
	expr1 ExprInterface	
	expr2 ExprInterface


func NewAnd(line uint32, token *lexer.Token,
	expr1 ExprInterface, expr2 ExprInterface) *And 
	return &And
		logic: NewLogic(line, token, expr1, expr2, logicCheckType),
		expr1: expr1,
		expr2: expr2,
	


func (a *And) Errors(s string) error 
	return a.logic.Errors(s)


func (a *And) NewLabel() uint32 
	return a.logic.NewLabel()


func (a *And) EmitLabel(l uint32) 
	a.logic.EmitLabel(l)


func (a *And) Emit(code string) 
	a.logic.Emit(code)


func (a *And) Gen() ExprInterface 
	return a.logic.Gen()


func (a *And) Reduce() ExprInterface 
	return a


func (a *And) Type() *Type 
	return a.logic.Type()


func (a *And) ToString() string 
	return a.logic.ToString()


func (a *And) Jumping(t uint32, f uint32) 
	var label uint32
	if f != 0 
		label = f
	 else 
		f = a.NewLabel()
	
	a.expr1.Jumping(0, label)
	a.expr2.Jumping(t, f)
	if f == 0 
		a.EmitLabel(label)
	


func (a *And) EmitJumps(test string, t uint32, l uint32) 
	a.logic.EmitJumps(test, t, l)


它的逻辑与前面or.go差不多,唯一确保在于Jumping函数生成中间代码时有所不同,它的逻辑跟or正好相反。对于or而言,如果第一个判断成立那么直接进行跳转,但对&&而言,它需要检测的是,如果当前判断不成立,那么就进行跳转,这一点在后面我们调试时会有所体现。接下来我们按照前面描述的语法规则修改一下代码解析的步骤,在list-parser.go中修改如下:

func (s *SimpleParser) bool() inter.ExprInterface 
	x := s.join()
	for s.cur_tok.Tag == lexer.OR 
		tok := lexer.NewTokenWithString(s.cur_tok.Tag, s.lexer.Lexeme)
		s.move_forward()
		x = inter.NewOr(s.lexer.Line, tok, x, s.join())
	

	return x


func (s *SimpleParser) join() inter.ExprInterface 
	expr := s.equality()
	var x inter.ExprInterface
	for s.cur_tok.Tag == lexer.AND 
		tok := lexer.NewTokenWithString(s.cur_tok.Tag, s.lexer.Lexeme)
		s.move_forward()
		x = inter.NewAnd(s.lexer.Line, tok, expr, s.equality())
	

	return x


func (s *SimpleParser) equality() inter.ExprInterface 
	expr := s.rel()
	var x inter.ExprInterface
	for s.cur_tok.Tag == lexer.EQ || s.cur_tok.Tag == lexer.NE 
		tok := lexer.NewTokenWithString(s.cur_tok.Tag, s.lexer.Lexeme)
		s.move_forward()
		x = inter.NewRel(s.lexer.Line, tok, expr, s.rel())
	

	return x


func (s *SimpleParser) rel() inter.ExprInterface 
	expr1 := s.expr()
	var tok *lexer.Token

	switch s.cur_tok.Tag 
	case lexer.LE:
	case lexer.LESS_OPERATOR:
		fallthrough
	case lexer.GE:
		fallthrough
	case lexer.GREATER_OPERATOR:
		fallthrough
	default:
		tok = nil
	

	if tok == nil 
		return expr1
	 else 
		s.move_forward()
	
	expr2 := s.expr()
	return inter.NewRel(s.lexer.Line, tok, expr1, expr2)

完成上面代码后,我们创建使用&&操作符的源代码用于解析,在main.go中输入代码如下:

func main() 

	source := `int a; int b; int c; int d;
		        int e;
		        a = 1;
				b = 2;
				c = 3;
				d = 4;
				if (b == a && c != d) 
					e = 2;
				
				e = 3;
	`
	my_lexer := lexer.NewLexer(source)
	parser := simple_parser.NewSimpleParser(my_lexer)
	parser.Parse()

完成后运行起来所得结果如下:

可以看到,编译器在对if(a==b && c!=d)进行代码生成时,创建了两个iffalse语句,这符号逻辑,因为只要有一个判断条件失败,那么跳转就不会进入if语句对应的内部代码,而是直接跳转出if对应大括号后面的代码,因此编译器分别判断条件"b == a" 和"c != d"是否成立,只有有一个不成立就不执行e = 2,直接跳转去执行e = 3,因此我们实现的逻辑没有问题。

更多详细的讲解和调试演示请参看B站视频,搜索coding迪斯尼,代码下载链接: https://pan.baidu.com/s/1JSUikD56p7GQu_a2saUELA 提取码: vvt8.更多干货:http://m.study.163.com/provider/7600199/index.htm?share=2&shareId=7600199

以上是关于自己动手写编译器:实现if判断中“||“和“&&“条件判断的中间代码生成的主要内容,如果未能解决你的问题,请参考以下文章

自己动手写编译器:实现else语句块的中间代码生成

自己动手写编译器:实现else语句块的中间代码生成

自己动手写编译器:实现简单if语句的跳转代码生成

自己动手写编译器:汤普森构造法

自己动手写编译器:汤普森构造法

自己动手写编译器:代码实现正则表达式到NFA状态机