数值型for语句

Lua的for语句支持两种类型:

  • 数值型:for Name ‘=’ exp ‘,’ exp [‘,’ exp] do block end
  • 泛型:for namelist in explist do block end

泛型for需要函数支持,在下一章介绍了函数后再实现。本节实现数值型for。从BNF定义中可见,这两个类型的前2个Token一样,数值型的第3个Token是=。通过这个区别可以区分两种类型:

    fn for_stat(&mut self) {
        let name = self.read_name();
        if self.lex.peek() == &Token::Assign {
            self.for_numerical(name);  // 数值型
        } else {
            todo!("generic for");  // 泛型
        }
    }

控制结构

数值型for语句的语义很明显,等号=后面3个表达式依次是初始值init、限制limit、和步长step。step可以是正数或负数,不能是0。控制结构图如下(图中假设step>0):

     +--------------+
/--->| i <= limit ? |--No--\ 如果超过limit则跳转到结尾
|    +--------------+      |
|                          |
|        block             |
|                          |
|    +-----------+         |
\----| i += step |         |
     +-----------+         |
         <-----------------/

图中方框中的执行逻辑都可以分别用1条字节码实现,每此循环都要执行2条字节码:先i+=step,然后判断i<=limit。为了性能,可以把第1条字节码的判断功能也加到下面的字节码中,这样每次循环只用执行1条字节码。控制结构图如下:

       +--------------+
       | i <= limit ? |--No--\ 如果超过limit则跳转到结尾
       +--------------+      |
/------>                     |
|       block                |
|                            |
|       +--------------+     |
|       | i += step    |     |
\--Yes--| i <= limit ? |     |
        +--------------+     |
            <----------------/

新增2条字节码:

pub enum ByteCode {
    // for-loop
    ForPrepare(u8, u16),
    ForLoop(u8, u16),

这2个字节码分别对应上图中两个方框的字节码,关联的两个参数都分别是栈起始位置和跳转位置。后续会看到第一个字节码除了判断跳转以外,还需要做其他的准备工作,所以名叫prepare。

变量存储

上面2个字节码关联的第1个参数都是栈的起始位置。准确说就是存储上述3个值(init,limit,step)的位置。这3个值自然是需要存储在栈上的,因为栈的功能之一就是存储临时变量,另外也因为没有其他地方可用。这3个值依次存储,所以只需要一个参数就可以定位3个值。

另外,for语句还有个控制变量,可以复用init的栈上位置。在语法分析时,创建一个内部临时变量,名字就是BNF中的Name,指向栈上第一个变量的位置。为了让另外2个临时变量的位置不被占用,需要再创建2个匿名局部变量。所以,执行时的栈如下:

      |        |
sp    +--------+
      | init/i |  控制变量Name
sp+1  +--------+
      | limit  |  匿名变量""
sp+2  +--------+
      | step   |  匿名变量""
      +--------+
      |        |

数值型for语句就上面的3个临时变量比较特殊,其余部分跟之前介绍的控制结构类似,无非就是根据条件判断语句做跳转。语法分析代码如下:

    fn for_numerical(&mut self, name: String) {
        self.lex.next(); // skip `=`

        // 读取3个表达式:init、limit、step(默认是1),依次放置到栈上
        match self.explist() {
            2 => self.discharge(self.sp, ExpDesc::Integer(1)),
            3 => (),
            _ => panic!("invalid numerical for exp"),
        }

        // 创建3个局部变量,用以占住栈上位置。后续如果内部block需要局部或临时变量,
        // 就会使用栈上这3个变量之后的位置。
        self.locals.push(name);  // 控制变量,可以在内部block中被引用
        self.locals.push(String::from(""));  // 匿名变量,纯粹占位用
        self.locals.push(String::from(""));  // 同上

        self.lex.expect(Token::Do);

        // 生成ForPrepare字节码
        self.byte_codes.push(ByteCode::ForPrepare(0, 0));
        let iprepare = self.byte_codes.len() - 1;
        let iname = self.sp - 3;

        self.push_loop_block();

        // 内部block
        assert_eq!(self.block(), Token::End);

        // 删除3个临时变量
        self.locals.pop();
        self.locals.pop();
        self.locals.pop();

        // 生成ForLoop字节码,并修复之前的ForPrepare
        let d = self.byte_codes.len() - iprepare;
        self.byte_codes.push(ByteCode::ForLoop(iname as u8, d as u16));
        self.byte_codes[iprepare] = ByteCode::ForPrepare(iname as u8, d as u16);

        self.pop_loop_block(self.byte_codes.len() - 1);
    }

整数和浮点数类型

之前支持的语句类型,都主要介绍语法分析部分;而虚拟机执行部分只是按照字节码对栈进行简单的操作。但数值型for循环的语法分析部分相对比较简单(主要是因为跟之前的几个控制结构类似),而虚拟机执行部分却很复杂。其实也不难,就是繁琐。繁琐的原因是因为Lua支持2种数值类型,整数和浮点数。数值型for语句中一共3个语句(或者称为变量),init、limit、和step,每个都可能是两种类型之一,一共就是8种可能。虽然某些情况下在语法分析阶段就可以确定某些变量的类型的(比如是常量),但对这种特殊情况单独处理的意义不大,最终还是需要处理全部3个变量都是未知类型的情况,这就需要在虚拟机执行阶段处理。

逐个处理8种类型实在太复杂;又不能完全归为一种类型,因为整数和浮点数的表示范围不一样。对此,Lua语言规定分为2类:

  • 如果init和step是整数,那么按照整数处理;
  • 否则,都按照浮点数处理。

至于在第一类里为什么没有考虑第2个limit的变量,就不清楚了。我想到有一些可能的原因,但都不确定,这里就不讨论了。就按照Lua的规定实现即可。但这也确实带来了一些复杂。

需要在某个地方把8种可能归类为上述2种类型。在语法分析阶段做不到,而在每次执行循环时又太费性能,所以就在循环开始的时候归类一次。这也就是ForPrepare字节码要做的事情:

  • 如果init和step是整数,那么把limit也转换为整数;
  • 否则,把3个变量都转换为浮点数。

这样,在每次执行循环时,即ForLoop字节码时,只需要处理2种情况即可。

第2类中把整数转换为浮点数简单,但第1类中把浮点数limit转换为整数,就要注意下面两点:

  • 如果step为正,则limit向下取整;如果step为负,则limit向上取整。
  • 如果limit超过整数的表示范围,那么就转换为整数的最大或最小值。这里就有个极端情况,比如step为负,init为整数最大值,limit超过了整数的最大值,那么init就小于limit,又因为Lua明确规定数值型for循环的控制变量不会溢出反转,所以预期是不会执行循环。但按照上述转换,limit由于超过整数的最大值,就被转换为最大值,就等于init了,就会执行一次循环。所以要特殊处理,可以把init和limit分别设置为0和1,这样就不会执行循环了。

limit变量转换的具体代码如下:

fn for_int_limit(limit: f64, is_step_positive: bool, i: &mut i64) -> i64 {
    if is_step_positive {
        if limit < i64::MIN as f64 {
            *i = 0;  // 一并修改init,保证不会执行循环
            -1
        } else {
            limit.floor() as i64  // 向下取整
        }
    } else {
        if limit > i64::MAX as f64 {
            *i = 0;
            1
        } else {
            limit.ceil() as i64  // 向上取整
        }
    }
}

虚拟机执行

介绍完上述整数和浮点数类型和转换细节后,接下来就实现相关两个字节码的虚拟机执行部分。

ForPrepare字节码做2件事情:首先根据变量类型分为整数和浮点数类型循环;然后比较init和limit判断是否执行第一次循环。代码如下:

                ByteCode::ForPrepare(dst, jmp) => {
                    // clear into 2 cases: integer and float
                    // stack: i, limit, step
                    if let (&Value::Integer(mut i), &Value::Integer(step)) =
                            (&self.stack[dst as usize], &self.stack[dst as usize + 2]) {
                        // integer case
                        if step == 0 {
                            panic!("0 step in numerical for");
                        }
                        let limit = match self.stack[dst as usize + 1] {
                            Value::Integer(limit) => limit,
                            Value::Float(limit) => {
                                let limit = for_int_limit(limit, step>0, &mut i);
                                self.set_stack(dst+1, Value::Integer(limit));
                                limit
                            }
                            // TODO convert string
                            _ => panic!("invalid limit type"),
                        };
                        if !for_check(i, limit, step>0) {
                            pc += jmp as usize;
                        }
                    } else {
                        // float case
                        let i = self.make_float(dst);
                        let limit = self.make_float(dst+1);
                        let step = self.make_float(dst+2);
                        if step == 0.0 {
                            panic!("0 step in numerical for");
                        }
                        if !for_check(i, limit, step>0.0) {
                            pc += jmp as usize;
                        }
                    }
                }

ForLoop字节码也做2件事情:首先控制变量加上step;然后比较控制变量和limit判断是否执行下一次循环。这里省略代码。

至此,我们完成数值型for语句。