# 所谓 "解释器"

我们除了能对遵循 ToyLang 语法的源文件编译之外,还要能够执行编译的结果。
若编译结果需要由其他程序负责执行,该程序便可称之为解释器。

  • 如先前提及的 AST 解释器。

百度百科上对解释器的定义也较为模糊:

解释器(英语:Interpreter),又译为直译器,是一种电脑程序,能够把高级编程语言一行一行直接转译运行。解释器不会一次把整个程序转译出来,只像一位 “中间人”,每次运行程序时都要先转成另一种语言再作运行,因此解释器的程序运行速度比较缓慢。它每转译一行程序叙述就立刻运行,然后再转译下一行,再运行,如此不停地进行下去。

按照百度百科的定义,一行一行动态执行高级编程语言的程序,才能算解释器。
但是以现在常见的编程语言,如 Python、Java 等也在某些情况下会被定义为解释型语言来看,已经与 "解释器" 的定义相违背了。
因为 Python 和 Java 并不是逐行解释源代码的,实际上得以 "解释" 的是字节码,而执行字节码的程序又被称为 "虚拟机"。

我的个人理解是,“解释器”、"解释型语言" 等名词的诞生,或许是为了与在当时已成体系的本地编译型语言进行区分,不过历史是否如此我也不好追溯了。

因此,不必过度纠结 "解释器" 和 "虚拟机",可以简单理解成解释器是一种笼统的统称,虚拟机是一种具体的实现,AST 解释器则是另一种实现,最终目标都是为了使得源代码得以以某种方式运行起来。

# 虚拟机简述

想必各位读者读者也经常见过虚拟机这个词。

咱们的 ToyLang 不选择直接解释 AST,而是设计虚拟机以执行编译结果。

虚拟机在不同场景下,所代指的东西存在差异,此处仅解释在编译原理中虚拟机的定义。

实际上,虚拟机的开发应该是在更靠后一些的章节中讲述,在编写指令生成时顺带完成虚拟机。
不过虚拟机的编写并不复杂,并且相对独立,因此就提前将其设计好,代码生成时以我们所设计的虚拟机为准去生成代码,有需要再对虚拟机进行修改与补充。

我们并不选择生成本地代码 (与机器相关的机器指令),而是生成一种被称为 字节码 的编译产物。

如果读者曾经学习过 Java ,应该或多或少听过这种说法:

Java 并不直接生成 机器指令 ,而是生成 字节码 ,交给 JVM 去执行。

以我个人的理解看来, 机器指令字节码 并没有特别多的区别,只不过负责执行的对象不一样:

  • 一个是直接由硬件执行
  • 一个是模拟了硬件执行指令的流程的程序,自己设计了一套指令集。

是的,我们所做的也与 Java 类似,我们也会设计 ToyLang 所生成的字节码,以及执行字节码的虚拟机。
当然这个虚拟机你要叫什么都可以,我在这里就把它叫成 TVM(Toy Virtual Machine)

# TVM

我们的 TVM 设计十分简单,只有十几条指令,但也足够我们使用了。

为了实现更加简单, TVM 被设计为基于栈的虚拟机。

# 指令集设计

# 指令结构

字段 长度 描述
Reserved 1bit 保留位
Opcode 7bit 操作码
Immediate 4byte 立即数,可选

# Opcode

Opcode,即操作码,是指令的一部分,在虚拟机的指令解码器在会根据指令的 Opcode 来进行不同的操作。
以下是 TVM 指令集的 Opcode 表:

助记符 编码 描述
stop 0x00 停止虚拟机
nop 0x01 空指令
pushk 0x02 将常量推入栈顶
pushv 0x03 将变量推入栈顶
pop 0x04 弹出栈顶值并抛弃
popv 0x05 弹出栈顶值并保存到变量中
add 0x06 依次弹出栈顶的值,stack2 + stack1,压入结果
sub 0x07 依次弹出栈顶的值,stack2 - stack1,压入结果
mul 0x08 依次弹出栈顶的值,stack2 * stack1,压入结果
div 0x09 依次弹出栈顶的值,stack2 /stack1,压入结果
call 0x0a 函数调用
ret 0x0b 函数调用返回
ne 0x0c 不等于比较
eq 0x0d 等于比较
lt 0x0e 小于比较
le 0x0f 小于等于比较
gt 0x10 大于比较
ge 0x11 大于等于比较
jcf 0x12 条件为否则跳转
jmp 0x13 无条件跳转

# ToyLang 源码 最终转换为 虚拟机指令的简单示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if a == 1 {
print("a==1");
}
else {
print("a!=1");
}

pushv a
pushk 1
eq
jcf .else
...
jmp .end
...
.end:
...