go语言的ast生成简述(解析表达式) | 青训营笔记

262 阅读5分钟

这是我参与「第五届青训营」伴学笔记创作活动的第 11 天

今天继续看函数parseBinaryExpr,之前也提到过在这个函数还会用来判断一个表达式是单目还是双目的,这显然不符合单一职责,因此1.19.3版本的源码中也用TODO注释了,本代码的责任过重,未来可能需要重写。那么它是如何判断的呢,这个函数有一个类型为ast.Expr的参数x如果这个参数不为空,那么这个参数x被用作双目操作的左表达式,如果这个参数为空,那么函数内会先给这个参数进行赋值,也就是读取一个单目表达式。这时对于一个双目表达式(如果是一个双目表达式)就有了其最左侧的第一个表达式。

if x == nil {
    x = p.parseUnaryExpr()
}

但是,作为一个基于符号优先级解析的语法解析器,虽然在符号带有相同优先级的情况下使用左结合的方式进行计算,有些操作符的优先级高于其他操作符虽然在表达式的后面部分却要先计算,例如乘法在没有括号指明的前提下要在加减法之前进行计算,那么在生成抽象语法树时也就需要在较深层的节点这就需要在解析双目表达式的左右两侧时关注第二目的右侧是否有操作符,如果有那么需要看它和其之前的操作符优先级的大小,如果右侧已经没有操作符或操作符已经小于左侧最后一个操作符的优先级,那么就直接返回前面已经完成计算的那部分表达式,这个表达式可能是一个单目表达式,并且这个表达式可以作为后续另一个双目表达式的左侧。为了统一操作,go语言将当一个表达式没有前没有双目操作符的表达式也设置了一个符号优先级,那就是所有符号优先级中最低的优先级,当然不是双目操作符的token也直接定义为返回最低优先级,保证前面的表达式计算。在符号优先级的定义和返回上这部分在之前介绍的词法分析部分,也就是GOROOT/src/go/token.go中的Precedence函数。

// Precedence returns the operator precedence of the binary
// operator op. If op is not a binary operator, the result
// is LowestPrecedence.
func (op Token) Precedence() int {
	switch op {
	case LOR: // 逻辑或优先级最低,使与或表达式不用加括号
		return 1
	case LAND: // 逻辑与优先级高一点,保证表达式的逻辑运算已经完成
		return 2
	case EQL, NEQ, LSS, LEQ, GTR, GEQ: // 所有的比较运算符,比逻辑运算符优先级高,比计算运算符低,保证比较值前完成数值运算
		return 3
	case ADD, SUB, OR, XOR: // 加减法与按位或和按位异或在同一优先级
		return 4
	case MUL, QUO, REM, SHL, SHR, AND , AND_NOT : // 乘除法,求余和按位移动,按位与是双目运算符的最高优先级
		return 5
	}
	return LowestPrec
}

函数tokPrec是给出当前look ahead的token的符号优先级,如果读到的符号优先级小于前者操作的符号优先级那么前面的表达式就已经成为一个正确执行的表达式部分了,否则要根据优先级将未完成的操作和另一目表达式加进来。为了使表达式的计算遵守左结合的规律,需要让相同优先级的操作做特殊判断,这里go语言的语法分析器使用在递归调用时为前一个操作符的优先级加1的操作保证这种特判。

for n = 1; ; n++ {
    incNestLev(p)
    // 给出当前
    op, oprec := p.tokPrec()
    // 如果后面的优先级小于等于前面的
    // 那么认为前面的表达式已经可以部分正确执行了
    // 返回这部分表达式
    if oprec < prec1 {
        return x
    }
    pos := p.expect(op)
    // 保证表达式计算的左结合,在递归调用时对前一个操作符的优先级加1
    y := p.parseBinaryExpr(nil, oprec+1, check)
    if check {
	x = p.checkExpr(x)
	y = p.checkExpr(y)
    }
    x = &ast.BinaryExpr{X: x, OpPos: pos, Op: op, Y: y}
}

接下来看函数parseBinaryExpr中调用的parseUnaryExpr函数,从这一步也可以看出go语言和大多数语言一样单目操作符的优先级高于双目操作符。在go语言中可以作为单目表达式的前缀操作符为+-(正负号),&^(按位非),!(逻辑非),&*(取地址和解引用或指针类型前缀),<-(channel)。通过一个switch进行符号的判断。大概的逻辑就是读取完前缀之后递归读取一个单目表达式如果不是switch中前缀认为不是单目表达式而是初级表达式,调用函数parsePrimaryExpr,会在这个函数里处理括号表达式等。

从上面的各种处理方法可以看出,由于go语言减少了自增和自减前缀并且将自增自减操作从会返回值得表达式改为了语句,取消了逗号表达式,没有了三目操作符,减少了操作符优先级的计算和其他表达式实现的复杂度。