一元运算

Lua中一元运算的语法参见:

exp ::=  nil | false | true | Numeral | LiteralString | ‘...’ | functiondef | 
		 prefixexp | tableconstructor | exp binop exp | unop exp 

一元运算在最后一项:exp ::= unop exp。即在表达式exp中,可以前置一元运算符。

Lua支持4个一元运算符:

  • -,取负。这个Token也是二元运算符:减法。
  • not,逻辑取反。
  • ~,按位取反。这个Token也是二元运算符:按位亦或。
  • #,取长度,用于字符串和表等。

语法分析代码中,增加这4个一元运算符即可:

    fn exp(&mut self) -> ExpDesc {
        match self.lex.next() {
            Token::Sub => self.unop_neg(),
            Token::Not => self.unop_not(),
            Token::BitNot => self.unop_bitnot(),
            Token::Len => self.unop_len(),
            // 省略其他exp分支

下面以取负-来举例,其他几个类似。

取负

由上面BNF可见,取负运算的操作数也是表达式exp,而表达式由ExpDesc来表示,所以考虑ExpDesc的几种类型:

  • 整数和浮点数,则直接取负,比如对于ExpDesc::Integer(10)直接转换为ExpDesc::Integer(-10)。也就是说,对于源码中的-10,在词法分析阶段会生成SubInteger(10)这两个Token,然后由语法分析转换为-10。没有必要在词法分析中直接支持负数,因为还可以有如下情况- -10,即连续多个取负操作,对于这种情况,语法分析就比词法分析更适合了。

  • 其他常量类型,比如字符串等,都不支持取负,所以报错panic。

  • 其他类型,则在虚拟机运行时求值。生成新增的字节码Neg(u8, u8),两个参数分别是栈上的目的和源操作数地址。这里只新增了1个字节码。相比之下,前面章节介绍的读取全局变量读表操作为了优化而都设置3个字节码,分别处理参数的3种类型:栈上变量、常量、小整数。但是对于这里的取负操作,上面的两种情况已经处理了后两种类型(常量和小整数),所以只需要新增Neg(u8, u8)这一个字节码来处理第一种类型(栈上变量)即可。而下一节的二元运算就不能完全处理常量类型,也就需要像读表操作一样对每种运算符都新增3个字节码了。

根据上一章对ExpDesc的介绍,对于最后一种情况,生成字节码,需要两步:首先exp()函数返回ExpDesc类型,然后discharge()函数根据ExpDesc生成字节码。目前ExpDesc现有类型无法表达一元运算语句,需要新增一个类型UnaryOp。这个新类型如何定义呢?

从执行角度考虑,一元运算操作和局部变量间的赋值操作非常类似。后者是把栈上一个值复制到另外一个位置;前者也是,只是在复制过程中增加了一个运算的转换。所以对于一元运算语句返回的ExpDesc类型就可以参考局部变量。对于局部变量,表达式exp()函数返回ExpDesc::Local(usize)类型,关联的usize类型参数为局部变量在栈上的位置。那对于一元运算操作,新增ExpDesc::UnaryOp(fn(u8,u8)->ByteCode, usize)类型,相对于ExpDesc::Local类型增加了一个关联参数,即复制过程中做的运算。这个运算的参数类型为fn(u8,u8)->ByteCode,这种通过函数类型来传递enum的tag的方法,在用ExpDesc重新表构造中介绍过,这里不再重复。还以取负操作为例,生成ExpDesc::UnaryOp(ByteCode::Neg, i),其中i为操作数的栈地址。

具体解析代码如下:

    fn unop_neg(&mut self) -> ExpDesc {
        match self.exp_unop() {
            ExpDesc::Integer(i) => ExpDesc::Integer(-i),
            ExpDesc::Float(f) => ExpDesc::Float(-f),
            ExpDesc::Nil | ExpDesc::Boolean(_) | ExpDesc::String(_) => panic!("invalid - operator"),
            desc => ExpDesc::UnaryOp(ByteCode::Neg, self.discharge_top(desc))
        }
    }

在生成ExpDesc::UnaryOp类型后,按照此类型生成字节码就很简单了:

    fn discharge(&mut self, dst: usize, desc: ExpDesc) {
        let code = match desc {
            ExpDesc::UnaryOp(op, i) => op(dst as u8, i as u8),

至此,我们完成了取负这个一元运算,其他3个一元运算大同小异,这里省略。

另外,由于一元运算语句的定义为:exp ::= unop exp,操作数也是表达式语句,这里是递归引用,所以就自然支持了连续多个一元运算,比如not - ~123语句。

上述是语法分析部分;而虚拟机执行部分需要添加这4个新增字节码的处理。也很简单,这里省略。

下一节介绍二元运算,会复杂很多。