使用JavaScript实现一个“字节码解释器”,并用它重新实现JS科学计算器的后端(待续)

为简化问题,

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的理解。


 6、下一步工作就是允许表达式中的自定义函数,以及为此增加的嵌套调用栈,CallStack实际上就是一个有向上反向引用的“局部变量”链,不过这里变量存在一个根据名字的作用域查找问题,并且“局部变量”的概念需要从下标索引访问扩充为按名字查找,这样,再增加一个HashMap应该就够了 

定义这个微型的字节码指令集是小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)


 参数的求值顺序,实际上是一个很重要的问题,在FP里面,某些细微的差别就足以导致不同的语言。LISP是支持代码存储为数据的,以及eval指令,这要求解释器运行环境可以嵌套。我的第一版本要不要支持eval呢?? 

SICP那本书的后期实现了编译器,将玩具语言编译为CPU风格的指令(其实这里没必要搞得这么复杂),然后又用Scheme去解释执行它。这就有点类似于我现在做的,只不过我的目标是在理解解释器、CPS转换的基础上,将实际的代码尽量简化。

注意,对表达式求值而言,在parser递归下降的过程中直接求值,这其实就是一个解释器了,而如果把计算器表达式转换为等价的JS代码,这其实就是编译器了,只不过是源代码级别的转换,跟真正的编译器差那么一点点。

缺点:我之前写的代码并没有实现parser解析为中间的AST,AST实际上直接去除了运算符优先级和结合性歧义的问题。


假如我想实现一个JavaScript-in-JavaScript解释器,AST生成可以借助Esprima库,但是解释器本身的runtime怎么设计就有点难度了


你可能感兴趣的:(读书笔记,编译器技术)