【深入理解 quickjs 系列】01. 前置概念整理
虚拟机类型
栈式虚拟机
虚拟机会在其内部维护一个全局指令指针来指向下一条将要执行的指令所在位置。
堆栈机使用栈结构来作为暂存数据的容器,这使得我们无法对栈容器中的数据进行任意读取,我们需要遵循LIFO的数据操作原则来对数据进行处理。这导致无法从源代码直接生成最高效的虚拟机代码,因为对于某些较为复杂的语句来说,可能会涉及栈数据频繁交换的过程。但另一方面,基于栈结构实现的虚拟机模型最简单,所生成的虚拟机代码密度适中。
寄存式虚拟机
在寄存器型虚拟机中,需要为每一条指令都指定其操作数所在的寄存器地址,因此与堆栈机和累加器型虚拟机相比,寄存器型虚拟机的平均指令更长。
寄存器型虚拟机将操作数存储在多个不同的寄存器单元上,这使得每条指令在执行时都需要指定操作数所在的寄存器地址,而这无疑增加了指令的长度,同时也使虚拟机的实现变得复杂。但是存在多个寄存器单元为寄存器型虚拟机提供了更大的优化空间,因此可以生成执行效率更高的虚拟机代码。
如何将一个计算表达式变为中序表达式
栈式计算机在处理运算的时候用的逆波兰表达式,如何将一个计算表达变为一个树,然后用后序遍历生成逆波兰表达式,如下所示代码
function buildTree(s) {
let root = {};
let currentNode = root;
for (let i = 0, length = s.length; i < length; i++) {
let char = s.charAt(i);
if (/[0-9]/.test(char)) {
let number = char;
while (/[0-9]/.test(s[i + 1])) {
char = s[i + 1];
number += char;
i = i + 1;
}
if (currentNode.left == null) {
currentNode.left = { value: parseInt(number, 10) };
} else if (currentNode.right == null) {
currentNode.right = { value: parseInt(number, 10) };
}
}
if (["+", "-", "*", "/"].includes(char)) {
if (currentNode.operation == null) {
currentNode.operation = char;
} else {
const newNode = { operation: char };
if (
["+", "-"].includes(currentNode.operation) &&
["*", "/"].includes(newNode.operation)
) {
newNode.left = { ...currentNode.right };
currentNode.right = newNode;
newNode.parent = currentNode;
} else if (
["*", "/"].includes(currentNode.operation) &&
["*", "/"].includes(newNode.operation)
) {
if (!currentNode.parent) {
newNode.left = currentNode;
currentNode.parent = newNode;
root = newNode;
} else {
currentNode.parent.right = newNode;
newNode.parent = currentNode.parent;
newNode.left = currentNode;
}
} else {
newNode.left = root;
root.parent = newNode;
root = newNode;
}
currentNode = newNode;
}
}
}
return root;
}
类似的代码可以在Github上搜索到 “如何编写一个计算器”
opcode vs bytecode
quickjs 里通过将脚本变为 opcode,来执行代码
他们本质都是类似的,都是指令,告诉执行容器执行逻辑是什么。唯一的区别就是 Opcode 是面向硬件的,bytebode 是面向虚拟机的。但是目前来看 quickjs 的 opcode 也是面向虚拟机的。