A lightweight stack-based virtual machine with 18 instructions, written in C++17. ByteVM provides a simple environment for understanding how virtual machines work and how instructions are executed.
- Stack-based architecture with 1000-element stack
- 18 essential opcodes covering arithmetic, logic, control flow, and I/O
- Function calls with local memory support
- Bytecode compiler for assembly-like syntax
- Fast opcode lookup using hash maps
bytevm/ - Virtual machine implementation
bytevm-compiler/ - Bytecode compiler
test/ - Example programs
Compile both the VM and compiler using g++:
g++ -std=c++17 -o bytevm-compiler/bin/Debug/bytevm-compiler bytevm-compiler/*.cpp
g++ -std=c++17 -o bytevm/bin/Debug/bytevm bytevm/*.cppCreate a file with ByteVM assembly instructions (e.g., program.asm):
PUSH 10
PUSH 5
ADD
PRINT
HALT./bytevm-compiler/bin/Debug/bytevm-compiler program.asmThis generates test/out.bytevm containing the compiled bytecode.
./bytevm/bin/Debug/bytevm test/out.bytevm| Opcode | Value | Description |
|---|---|---|
| PUSH | 0 | Push value onto stack |
| POP | 1 | Pop value from stack |
| ADD | 2 | Pop two values, push sum |
| SUB | 3 | Pop two values, push difference |
| MUL | 4 | Pop two values, push product |
| AND | 5 | Bitwise AND of top two values |
| OR | 6 | Bitwise OR of top two values |
| NOT | 7 | Logical NOT of top value |
| LT | 8 | Push 1 if second < top, else 0 |
| EQ | 9 | Push 1 if second == top, else 0 |
| JMP | 10 | Jump to instruction address |
| JZ | 11 | Jump if top of stack is 0 |
| LOAD | 12 | Load from local memory |
| STORE | 13 | Store to local memory |
| CALL | 14 | Call function |
| RET | 15 | Return from function |
| 16 | Pop and print top value | |
| HALT | 17 | Stop execution |
PUSH 15
PUSH 7
SUB
PRINT
HALTOutput: 8
PUSH 5
PUSH 10
LT
PRINT
HALTOutput: 1 (true, because 5 < 10)
PUSH 0
JZ 8
PUSH 999
PRINT
HALT
PUSH 42
PRINT
HALTOutput: 42 (jumps to instruction 8 when stack top is 0)
PUSH 100
PUSH 50
ADD
PRINT
PUSH 10
PUSH 3
MUL
PRINT
PUSH 25
PUSH 5
SUB
PRINT
HALTOutput:
150
30
20
The VM operates on a simple fetch-decode-execute cycle:
- Fetch: Read the opcode at the current instruction pointer
- Decode: Determine which operation to perform
- Execute: Perform the operation on the stack
- Repeat: Move to the next instruction
The compiler tokenizes assembly source code and converts instruction names to their numeric opcodes, producing a space-separated bytecode file that the VM can execute.
- Stack Size: 1000 integers
- Call Stack: 100 frames
- Local Memory: 100 integers per frame
- Opcode Lookup: O(1) using unordered_map
- Language: C++17