写一个中文编程语言:实现语言TVM虚拟机
在英语中,文学是 26个字母,10 个阿拉伯数字和大约 8 个标点符号的水平线上的特殊排列组合。
在计算机中,编程应该属于文科类别。
__ steve jobs 乔布斯 在某采访中回答。
简介 接上一章: 写在前面
这是我们在三个章节完成一个中文编程语言挑战的最后一章。
我们已经花了巨量的时间去把源代码表示为表达式语句,这样的AST 让我们可以更好将其表示为机器码。
在解释为机器码之前,我们还需要做一些事情,那就是实现指令虚拟机。
其最终的指令形式如下,这让我们更清楚地了解编译器如何将用户的源代码翻译成一系列的机器可以执行的代码
('IPUSH', 3), ('IPUSH', 4),('IPUSH', 5),('IMUL',),('IADD',), ('PRINTI',)
我们把字节码这些间接层都排除在外,也不去担心内存的管理,暂时将其理解为无穷大小,因为我们隐式地依赖GC垃圾回收机制,它会替我们处理好。
当专注于高级概念时,可以掩盖它们。我们在这里只是去了解,去模拟一个硬件如何执行计算,它就是堆栈虚拟机Stack VM
它的应用非常广泛,Python 虚拟机,JAVA 虚拟机,C# CIL, WebAssembly, Erlang
我们也不打算全部实现它,这里实现基本的 逻辑,整数计算和输出。
一个堆 就像一个只有一条没有出口的小路,如果太多人进入这其中,要排空这里,只有从入口依次取出
存入 start -> 堆 -> end
取出 <- 堆 <- end
add, sub, mul ,div, and ,or ...
按优先级调用这个堆,并把结果返回,最后的结果即是我们需要的。 优先级以此升高。
5 执行: 逻辑运算和算术运算函数
检查我们已有的表达式语句。
PrintStatement (*Node)(0xc0001011d0)
{data:BinOp {Expression: nil,
Op: '+',
Left: *Node {Next: nil,
Data: (*Integer)
data:{Value:'3'},
Right: *Node {Next: nil,
Data: (*BinOp) BinOp {Expression: nil,
Op: '*',
Left: *Node {Next: nil,
Data: (*Integer),
data:{Value:'4'},
Right: *Node {Next: nil,
Data: (*Integer),
data: {Value:'5' }
}
我们将不会支持全部的已知数据类型,这里支持整数int32,先看看我们的工作
3 + 4 * 5
在整数堆中存入的结果 右侧先存入,其取出顺序为
5,4,3
我们期望调用一次 VM IMUL 函数计算后的工作为
3 + 20
再一次执行 VM IADD 函数计算后 剩下的工作为
23
最后,只需要一个整数的取出调用和刷到控制台即可
PRINTI
23
因此,我们希望实现一个抽象机器,一个通用CPU,它拥有一组基本的标准操作说明,我们之前计算的表达式将映射到这些操作。
‘IPUSH’, ‘IMUL’, ‘IADD’, ‘ISUB’, ‘IDIV’, ‘PRINTI’, ‘LAND’, ‘LOR’, ‘HALT’
诸如以下例子
func (vm *TVM) IPUSH(v int) {
...
}
我们将在下一节实现它们。
5.1 实现 TVM虚拟机: 基于堆的TVM 模拟CPU的执行者
堆 在这里是数据结构,不是内存管理中操作系统的堆。
基于堆的虚拟机是解释器内部架构的一部分。
在基于堆的 VM 中执行指令非常简单。在最后,您还会发现将源语言编译为基于堆栈的指令集简直是小菜一碟。
然而,这种架构也足够快,可以被生产语言实现并广泛使用。感觉就像在编程语言游戏中弯道超车。
虽然基于堆栈的解释器不是灵丹妙药, 但是它们往往是足够的。
我们交给它一大块代码——实际上是一个块——然后它运行它。VM 的代码和数据结构驻留在一个新模块frame中。
我们从指令填充开始。VM 将逐渐获取它需要跟踪的一整堆状态,因此我们现在定义一个结构来填充所有内容。
目前,我们存储的只是固定地执行它的块。 把代码都注册到VM上下文管理器中,并且在执行时取出。
如果需要限定寄存器的大小,请在构造函数中进行限制,如 make([]int, 128)
// 标记是否运行中,是否全局等
type TVM struct {
Pc int
Running bool
IsTack []int
Globals map[string]any
Frame *Frame
}
以下是乐趣所在,它发生在run() 中,当有代码执行时,它将持续运行,如果被调度器把运行状态置为false,虚拟机将停止运行。
每次遍历该循环,我们读取并执行单个指令并使用调度器执行,要处理执行一条指令,我们首先要弄清楚我们正在处理的是哪种指令,这将在调度器实现。
同时需要注意Pc码,因为每一次执行,我们将前进一步,也执行一次,它可以用于跟踪堆执行位置。
这将在执行大型程序时考验虚拟机的性能,因此这是整个虚拟机中性能最关键的部分。
编程语言的知识充满了有效地进行调度的巧妙技术,可以追溯到计算机的早期。
如果想要了解,可以参考诸如"thread code"、“skip table” 和 “goto”,现代的PMG架构等等。
但是我们在这里专注实现一个基本可工作的虚拟机。
//启动和执行,直到上下文全部完成
func (vm *TVM) Run(VMCode *TVMContext) {
vm.Pc = 0
vm.Running = true
vms := *vm
for i, code := range VMCode.Code {
code := code
for vms.Running {
vms.Pc += 1
opp := code[0]
nodes, err := strconv.Atoi(code[1])
if err != nil {
vms = CallFuncs(vms, opp, []reflect.Value{})
} else {
refVal := reflect.ValueOf(nodes)
vms = CallFuncs(vms, opp, []reflect.Value{refVal})
}
break
}
}
}
func CallFuncs(vm *TVM, ...) *TVM {}
在将来如果我们实现了控制流,可以更好的方式去执行它。但是现在不需要。
调度者根据其名称 Call 虚拟机中的相关函数,这里隐式的依赖了其他语言的规范。这将在下一节介绍。
这里需要的是继续实现寄存器的其他执行函数。正如上一节所讲,这里需要它们,当然还要栈协议支持两种操作 PUSH POP:IPUSH’, ‘IMUL’, ‘IADD’, ‘ISUB’, ‘IDIV’, ‘PRINTI’, ‘LAND’, ‘LOR’, ‘HALT’
// 整数操作
func (vm *TVM) IPUSH(v int) {
vm.IsTack = append(vm.IsTack, v)
}
func (vm *TVM) IPOP() int {
lastOne := len(vm.IsTack) - 1
if lastOne < 0 {
empt := fmt.Sprintf("IsTack is empty:%v", vm.IsTack)
panic(empt)
}
pv := vm.IsTack[lastOne]
vm.IsTack = vm.IsTack[:lastOne]
return pv
}
// 加法
func (vm *TVM) IADD() {
right := vm.IPOP()
left := vm.IPOP()
rst := left + right
vm.IPUSH(rst)
}
func (vm *TVM) ISUB() {
right := vm.IPOP()
left := vm.IPOP()
vm.IPUSH(left - right)
}
func (vm *TVM) IMUL() {
right := vm.IPOP()
left := vm.IPOP()
vm.IPUSH(left * right)
}
func (vm *TVM) IDIV() {
right := vm.IPOP()
left := vm.IPOP()
vm.IPUSH(left / right)
}
func (vm *TVM) AND() {
right := vm.IPOP()
left := vm.IPOP()
vm.IPUSH(left & right)
}
func (vm *TVM) OR() {
right := vm.IPOP()
left := vm.IPOP()
vm.IPUSH(left | right)
}
func (vm *TVM) HALT() {
vm.Running = false
}
func (vm *TVM) PRINTI() {
fmt.Printf("%v\n", vm.IPOP())
}
这些函数的“实现”,帮助我们将VM状态初始化或释放,或计算 调用,以实现静态 VM 实例。
这里做的是一个全局变量,唯一的一个虚拟机,在大型编程时,可能有人听说过的全局变量的某些不好的东西,所以请避免在大型程序使用。
不希望这变成说教。 我也不会假装自己是任何语言的专家。 这里没有执行时跟踪,也没有完全的测试。
完全的测试有巨大的工作量,但是我们仍然希望有条件的人能够在使用它们前进行测试。
3 + 4 * 5
i8.push 3 [3]
i8.push 4 [3, 4]
i8.push 5 [3, 4, 5]
i8.mul [3, 20]
i8.add [23]
i8.ipop [23]
i8.printi [23]
现在已经有了执行运算的虚拟机,姑且称之为 TVM,也就是OTao Vritual Machine,属于通用stack machine.
仍然需要一个上下文管理器,来帮助调度和执行解析器返回的 代码和语句。
6 结语:
大量的语言使用虚拟机作为语言语句和机器语言之间的中介,比如JVM之于JAVA,PVM之于Python。我们这里也使用了一个自己的TVM虚拟机,它将我们的自定义语言,转换为机器可以执行的语言。
夜晚是美好的。下一节我们完成这个小小的挑战。然后你可以来享受它了,也许可以翘起脚看一看星辰。
- 点赞
- 收藏
- 关注作者
评论(0)