上一篇:语义分析
执行指令
在我们有了带类型的语法树后,我们就可以生成指令了。如果你有读过Java虚拟机规范的话,可以看到庞大的JVM指令集,好像是近200条?接下来我们要考虑代码的执行过程并生成指令。
首先我们考虑代码的执行过程,我们知道程序运行是基于栈的,那么具体是怎么运行的呢?开篇我们提到了这样的一个语法树:
S
/|\
F = E
| /|\
i F + T
| /|\
i F * F
| |
2 i
这个是关于语句x = a + 2 * c
的,我介绍了生成的指令是
iload a
iconst 2
iload c
imul
iadd
istore x
这样的(应该和JVM规范比较类似)。
我们可以看到有这样的一些指令:const
,load
,mul
,add
和store
。我首先介绍他们的意义分别是向栈中压入一个常量、向栈中压入一个变量的值、在栈顶取出两个值做乘法再将值压入栈,在栈顶取出两个值做加法再将值压入栈,将栈顶的值存入变量。我们看到load
,const
,store
指令还可以带参数。
你应该注意到了我实际调用的指令都有前缀i
,这表示指令是操作int
整型变量,因为我们要加快实际运行的速度,我们不应该在调用运行的时候才来判断指令要操作的操作数的类型。因此我们在生成指令的时候可以根据语义分析得到的类型生成对应的指令,这也是我们在语义分析的时候就把隐式类型转换转化为强制类型转换的原因,如果我们对任意两种基本类型都来一种指令,那指令数量就爆炸了。而强制类型转换的指令数并不会很多。
接下来我们模拟一下这段指令的执行过程(假设c=4;a=5
):
<空> // 栈初始为空,左侧为栈底,右侧为栈顶
5 // 执行了iload a,因为a = 5,所以栈顶压入5
5 2 // 执行了iconst 2,栈顶压入2
5 2 4 // 执行了iload c,因为c = 4,所以栈顶压入4
5 8 // 执行了imul,栈顶取出2个元素2和4,做乘法得到8,重新压入栈
13 // 执行了iadd,栈顶取出2个元素5和8,做加法得到13,重新压入栈
<空> // 执行了istore x,栈顶取出13,赋给x,现在x就等于13了
执行完一段有意义的语句后栈就应该清空,比如我们有这样的一个语句func(1, 2);
。假设func
的返回值为int
,那么我们生成的指令应该是这样的:
iconst 1
iconst 2
invoke_static #func
其中invoke_static
表示调用静态(全局)的函数,并将栈顶的参数弹出,执行#func
,将函数的返回值压入栈。假设func(1, 2) = 3
,那么栈有:
<空>
1 // 执行了iconst 1
1 2 // 执行了iconst 2
3 // 执行了invoke_static #func
因为func
的返回值永远不会被使用到,所以我们还需要将多余的元素弹出。比如加一个指令pop
表示弹出栈顶元素。
也许你会问不弹出会怎么样,如果我们有这样的一个语句:
i + (1 + (func(1, 2), 1));
由于逗号表达式的特性,只返回最右的那个分句的值,因此前面分句的值是无用的。生成的指令可能这样:
i // iload i
i 1 // iconst 1
i 1 1 // iconst 1
i 1 1 2 // iconst 2
i 1 3 // invoke_static #func
i 1 3 1 // iconst 1
i