1、科学计算器不包含用户自定义函数,也就是说,“字节码解释器”不考虑嵌套调用栈的问题;
2、需要把表达式转换为序列化的基于“寄存器”(假设R0~R15)和“局部变量”(像LLVM那样%1~无限)的微指令字节码序列,这里面最重要的是一个CPS转换
3、需要定义字节码调用原语函数(PrimitiveFunction)的ABI规范,目前简化为:最多2个参数,用R0、R1传递,结果用R0返回
+-*/,sin cos都映射为原语函数,为此可定义指令CallPrimitiveFunction
4、关于Value,不需要考虑int、long、float、double,只有一个number类型(!!),由于是表达式求值,不需要考虑string类型
5、字节码指令设计参考RISC,Load/Store、CallPrimitiveFunction,局部变量直接引用,不需要LLVM里面的alloca
这个项目主要是为了考察考察对解释器、以及CPS/SSA的理解。
定义这个微型的字节码指令集是小case,暂不考虑,cps转换算法也不需要考虑(可能是隐式的,而非lisp/scheme里面的那种源代码级别的转换)
例如,表达式 1+sin(2*3),对应的指令序列可能如下:
LoadImm 1, R0
StoreLocal R0, %0 #将number 1存储到局部变量槽%0里面
LoadImm 2, R0 #加载立即数到寄存器
LoadImm 3, R1
CallPrimitiveFunction sin #临时变量的结果现在在R0里
Move R0, R1 #将结果从R0移动到R1
LoadLocal %0, R0
CallPrimitiveFunction ADD
根据递归下降的parser前端,应该可以直接翻译为上面的字节码序列。这里不考虑CISC的指令架构,以及可能的优化Pass。
从这个简单的手工转换的例子来讲,我发现了一个问题:
考虑一个二元函数,就是这里的+,它的左边是一个子表达式Left,右边Right,根据优先级,Right端必须先运算。则有2种处理思路:
1、先生成Right端对应的字节码,再考虑左边的——这时相当于利用+的交换律做了转换
2、先把Left端的子表达式压栈(栈目前只支持number类型的局部变量),再生成Right端对应的字节码
递归下降的parser前端架构支持2比较方便,问题是,考虑嵌套的表达式:
1+(2+(3+(4+5)))
这种情况下LoadLocal、StoreLocal指令似乎存在问题,需要在parser中管理局部变量的索引状态,对于表达式而言,似乎Push/Pop指令更直接一点
但单有Push/Pop解决不了下面的表达式:
sin(1+2)* cos(3-4)
SICP那本书的后期实现了编译器,将玩具语言编译为CPU风格的指令(其实这里没必要搞得这么复杂),然后又用Scheme去解释执行它。这就有点类似于我现在做的,只不过我的目标是在理解解释器、CPS转换的基础上,将实际的代码尽量简化。
注意,对表达式求值而言,在parser递归下降的过程中直接求值,这其实就是一个解释器了,而如果把计算器表达式转换为等价的JS代码,这其实就是编译器了,只不过是源代码级别的转换,跟真正的编译器差那么一点点。
缺点:我之前写的代码并没有实现parser解析为中间的AST,AST实际上直接去除了运算符优先级和结合性歧义的问题。
假如我想实现一个JavaScript-in-JavaScript解释器,AST生成可以借助Esprima库,但是解释器本身的runtime怎么设计就有点难度了