We write the stack virtual machine on Rust'e
Hello, Habr! For several weeks I have been developing my own programming language in Rust. I would like to tell you about what a newcomer in this business may face and what he should know about.
Brief prehistory
It all started with fork ein , I forked it in order to learn how programming languages are built. So, as ein is interpreted from and to, then its execution speed was not the highest, and after I started to understand at least something, I decided to start writing my own interpreter, which I eventually abandoned.
But it's too early to despair! I read a couple of articles about VMs and how they happen and decided to write a simple stackable VM.
What is a "stack virtual machine" and how does it work?
On Habré there is a detached article about it, but that would not drive through the links I will briefly outline the meaning of this thing.
A stack VM performs all operations on data that is stored as a stack, each operation retrieves the necessary amount of data for the operation, and after executing it can “send” a new number to the stack.
Getting started
First you need to create a new project using cargo:
cargo new habr_vm
First we need to create some basic operations for our VM:
enumOpcode {
Push(i32),
Add,
AddAssign(i32),
Sub,
SubAssign(i32),
}
These are our basic operations, the Push command will add a new number to the stack, Add and Sub will take two numbers from the stack and perform actions with them (addition and subtraction, respectively), I don’t need to explain AddAssign and SubAssign.
The next task is to create the virtual machine itself, for this we will create a non-complex structure:
structVm {
pub stack: Vec<i32>,
}
And implement it:
impl Vm {
//Упрощаем себе жизнь тем что сокращаем писанинуpubfnpop(&mutself) -> i32 {
self.stack.pop().unwrap()
}
//Здесь будет происходить все самое интересноеpubfnrun(&mutself,program: Vec<Opcode>) {
for opcode in program { //Проверяем каждую команду в нашей программеmatch opcode {
Opcode::Push(n) => {
//Просто добавляем число в наш стекself.stack.push(n);
}
Opcode::Add => {
// Берем два числа из стека и складываем их, добавляем в стек уже новое числоlet value = self.pop() + self.pop();
self.stack.push(value);
}
Opcode::Sub => {
// Тут то же самое что и с складыванием только наоборот let value = self.pop() - self.pop();
self.stack.push(value);
}
// Думаю следующие две операции объяснять не нужно
Opcode::AddAssign(n) => {
letmut value = self.pop();
value += n;
self.stack.push(value);
}
Opcode::SubAssign(n) => {
letmut value = self.pop();
value -= n;
self.stack.push(value);
}
}
}
}
}
We and our structure, what's next? Next you need to create our "program".
Here’s how it should look:
let program = vec![
Opcode::Push(2),//Добавляем 2 в наш стек
Opcode::Push(4),// Добавляем 4 в наш стек
Opcode::Sub,// Вычитаем 4 - 2
];
It's simple, isn't it? If so, then let's run our program!
letmut vm = Vm {stack: Vec::new()};
vm.run(program);
// Выводим все числа из стека, на данный момент это будет просто 2 for i in vm.stack() {
println!("{}", i);
}
// Вывод2
It's very simple as for me, so you can add enough opcodes for the operation you need.
Conclusion
I think that I quite clearly explained how to write all this on a plant and how it works.
I would like to add that you can easily write your own YAP thanks to such a VM, all you have to do is write a parser, lexer and "compiler", and if you want to look at a ready-made project, you can follow this link .
All code from the article is available in this repository.
Good luck Habr!