4 个版本 (1 个稳定版)
使用旧的 Rust 2015
1.0.1 | 2019 年 4 月 8 日 |
---|---|
0.3.0 | 2017 年 11 月 21 日 |
0.2.0 | 2017 年 11 月 14 日 |
0.1.0 | 2017 年 11 月 12 日 |
#264 in 模拟器
79KB
1.5K SLoC
stack-vm
此包实现了一个通用栈式虚拟机,您提供操作数和指令,该包提供运行所需的所有其他基础设施。
它还提供了一个简单的指令构建器,您可以使用它来生成您的程序。
栈式计算机是使用操作数栈来执行后缀表达式的评估的计算机。每个计算机架构都有自己的指令集,这是计算机可以执行的基本操作集。
指令通常描述基本的算术运算、I/O、跳转等。
许可证
此包根据 MIT 许可证条款授权。有关详细信息,请参阅 LICENSE
文件。
计算机架构
计算机系统的架构是指各种逻辑组件如何连接在一起以执行指令并产生副作用(有用的结果)。
组织计算机架构主要有两种方式
- 冯·诺依曼 - 程序数据和指令存储在相同的内存中。
- 哈佛 - 程序数据和指令存储在单独的内存部分。
现代处理器的主体都是冯·诺依曼类型的机器。
我们还可以根据它们存储中间值的方式对我们的机器进行分类
- 累加器 - 最基本的处理器形式,其中只有一个寄存器用于存储计算结果。
- 栈 - 栈式计算机使用操作数栈来推入和弹出结果。
- 寄存器 - 寄存器式计算机使用多个命名(或编号)的寄存器来存储值或传递参数。
大多数现代处理器都是寄存器式计算机,尽管有趣的是,寄存器式和栈式计算机都可以用来模拟其近亲。
指令集
指令集是机器的定义。没有指令,您的机器就无法 执行 任何操作。它们是计算机的基本构建块,因此在构建之前,您需要仔细考虑这一点。
这个虚拟机使用Rust函数作为指令,而不是晶体管和逻辑门,但效果是相同的。
为了生成您的指令,您需要创建一些符合 stack_vm::InstructionFn
签名的Rust函数。
例如
use stack_vm::Machine;
type Operand = i64;
fn push(machine: &mut Machine<Operand>, args: &[usize]) {
let arg = machine.get_data(args[0]).clone();
machine.operand_push(arg);
}
一旦您完成了指令的定义,您就可以使用它们构建一个 stack_vm::InstructionTable
,其中每个指令都由它的 op_code
、name
和 arity
来标识。
-
op_code
是一个正整数,用于唯一标识这个指令。这需要手动输入,而不是从插入顺序自动生成,以便尽可能保持您虚拟机版本的兼容性。 -
name
是一个字符串,用于标识此指令;主要用于调试。 -
arity
指令期望从程序数据中获取的参数数量。这不是函数从操作数栈中需要的操作数数量。这用于在编译时将常量数据放置到程序中。
use stack_vm::{Instruction, InstructionTable, Machine};
type Operand = i64;
fn push(machine: &mut Machine<Operand>, args: &[usize]) {
let arg = machine.get_data(args[0]).clone();
machine.operand_push(arg);
}
fn add(machine: &mut Machine<Operand>, _args: &[usize]) {
let rhs = machine.operand_pop().clone();
let lhs = machine.operand_pop().clone();
machine.operand_push(lhs + rhs);
}
let mut instruction_table = InstructionTable::new();
instruction_table.insert(Instruction::new(0, "push", 1, push));
instruction_table.insert(Instruction::new(1, "add", 0, add));
代码生成
一旦定义了指令集,您就可以使用 stack_vm::Builder
对象构建虚拟机可以执行的表达式。
例如,要在栈上推送两个整数并将它们相加
use stack_vm::{Instruction, InstructionTable, Machine, Builder};
type Operand = i64;
fn push(machine: &mut Machine<Operand>, args: &[usize]) {
let arg = machine.get_data(args[0]).clone();
machine.operand_push(arg);
}
fn add(machine: &mut Machine<Operand>, _args: &[usize]) {
let rhs = machine.operand_pop().clone();
let lhs = machine.operand_pop().clone();
machine.operand_push(lhs + rhs);
}
let mut instruction_table = InstructionTable::new();
instruction_table.insert(Instruction::new(0, "push", 1, push));
instruction_table.insert(Instruction::new(1, "add", 0, add));
let mut builder: Builder<Operand> = Builder::new(&instruction_table);
builder.push("push", vec![3 as Operand]);
builder.push("push", vec![4 as Operand]);
builder.push("add", vec![]);
这将生成以下代码
@0 = 3
@1 = 4
.main:
push @0
push @1
add
运行您的程序
一旦有了指令和生成的代码,您就可以将它们与 stack_vm::Machine
结合起来执行。
use stack_vm::{Instruction, InstructionTable, Machine, Builder, WriteManyTable};
type Operand = i64;
fn push(machine: &mut Machine<Operand>, args: &[usize]) {
let arg = machine.get_data(args[0]).clone();
machine.operand_push(arg);
}
fn add(machine: &mut Machine<Operand>, _args: &[usize]) {
let rhs = machine.operand_pop().clone();
let lhs = machine.operand_pop().clone();
machine.operand_push(lhs + rhs);
}
let mut instruction_table = InstructionTable::new();
instruction_table.insert(Instruction::new(0, "push", 1, push));
instruction_table.insert(Instruction::new(1, "add", 0, add));
let mut builder: Builder<Operand> = Builder::new(&instruction_table);
builder.push("push", vec![3 as Operand]);
builder.push("push", vec![4 as Operand]);
builder.push("add", vec![]);
let constants: WriteManyTable<Operand> = WriteManyTable::new();
let machine = Machine::from(builder, &constants);
let mut machine = Machine::run(machine);
assert_eq!(machine.operand_pop(), 7);
调用函数
函数通过机器跳转到代码中的另一个标签来执行,并从那里继续执行。
每次机器跳转时,它都会创建一个新的调用帧,这使得它可以存储和检索局部变量而不会破坏其父调用上下文。它还包含返回地址,这意味着当您要求机器返回时,它将知道在移除帧后返回代码中的哪个地址。
您可以在该包的验收测试中找到一个函数调用的示例。
注意事项
目前,由于底层使用 Vec
实现,Stack
是“无限”的。我可能需要提供一个机制,让用户在机器崩溃后提供自己的栈限制。
更多信息
我在 dev.to 上写了一系列关于如何从头开始构建基于栈的虚拟机的博客,如果您是从零开始的,可能会很有用。
依赖项
~270–365KB