进阶路线:手写一个字节码虚拟机
目标
树遍历解释器慢在"每次求值都要递归进树"。本课作为进阶总览,手写一个极简字节码虚拟机(VM):先把表达式编译成指令序列,再用一个栈去执行——这是 Java、Python、Lua 真实采用的路线。本例独立实现,可单独运行。
运行环境:Node.js v18+。运行方式:保存为 vm.js,执行 node vm.js。
// vm.js —— 把 "1 + 2 * 3" 编译成字节码,再用栈执行
// 1) 编译器:AST -> 指令数组(这里直接手写 AST 演示)
function compile(node) {
const code = []
function walk(n) {
if (n.type === 'num') { code.push(['PUSH', n.value]); return }
if (n.type === 'binary') {
walk(n.left); walk(n.right) // 先算左右操作数,压栈
code.push(['OP', n.op]) // 再发一条运算指令
return
}
throw new Error('不支持的节点: ' + n.type)
}
walk(node); return code
}
// 2) 虚拟机:用栈执行指令
function execute(code) {
const stack = []
for (const [op, val] of code) {
if (op === 'PUSH') stack.push(val)
else if (op === 'OP') {
const b = stack.pop(), a = stack.pop()
switch (val) {
case '+': stack.push(a + b); break
case '-': stack.push(a - b); break
case '*': stack.push(a * b); break
case '/': stack.push(a / b); break
}
}
}
return stack.pop()
}
// ===== 调用示例 =====
const ast = { type: 'binary', op: '+', left: { type: 'num', value: 1 },
right: { type: 'binary', op: '*', left: { type: 'num', value: 2 }, right: { type: 'num', value: 3 } } }
const code = compile(ast)
console.log('字节码 =', JSON.stringify(code))
console.log('结果 =', execute(code)) // 7compile 把树"拍平"成 [PUSH 1, PUSH 2, PUSH 3, OP *, OP +];execute 用一个栈顺序执行,结果就是 7。注意优先级被编译顺序"固化"进了指令序列,运行时不再需要递归进树。
名词解释
字节码(Bytecode):介于源码和机器码之间的中间指令,比 AST 紧凑、比原生码可移植。Java 的 .class、Python 的 .pyc 都是字节码。
虚拟机 / VM(Virtual Machine):执行字节码的程序,常见的是栈式 VM(用操作数栈,如本例)或寄存器式 VM(如 Lua 5)。
编译 vs 解释:编译是"源码 -> 别的表示"(一次性,可缓存);解释是"边读边执行"。本课是"先编译成字节码,再由 VM 执行",即编译型解释器的典型形态。
课后练习
练习:给 VM 增加 PRINT 指令,让程序能直接输出栈顶值(而不只是 return)。
答案:
// 编译器里可加:code.push(['PRINT'])
// 虚拟机里加分支:
// else if (op === 'PRINT') console.log('输出:', stack[stack.length - 1])总结
走到第12节,你已经拥有两条执行路线:树遍历解释(第3节,易写难快)和字节码 VM(本课,稍难但快得多)。真实语言往往两者结合——开发期用树遍历快速验证语义,定型后用字节码 VM 或 JIT 编译器提速。本课只演示了算术,但同样的"编译 + 栈执行"框架,套上变量指令 STORE/LOAD、跳转指令 JMP/JIF 就能表达 if/while,套上 CALL/RET 就能表达函数。你写的 12 节,其实就是一门真实语言从 0 到 1 的缩影:Lexer 切词、Parser 建树、Interpreter/VM 执行、Environment 管状态、标准库供能力、错误保体验、REPL 给入口。下一步,你可以挑一个方向深挖——加对象、加模块、或把它编译成真正的机器码。