A stack based bytecode virtual machine for the Eva programming language, implemented in Python using a functional programming approach like Lisp (no classes/OOP in the VM).
This is a complete rewrite of the Eva VM from C++ to Python, using:
- Dictionaries and lists instead of classes
- Pure functions instead of methods
- Functional composition for program flow
- Simple, readable code structure
- eva_value.py - Value system (numbers, booleans, strings, objects)
- opcodes.py - Bytecode instruction definitions
- parser.py - S-expression parser
- scope.py - Lexical scope analysis
- compiler_helpers.py - Helper functions for compilation
- compiler.py - AST to bytecode compiler
- vm.py - Virtual machine and execution engine
- eva-vm.py - Main entry point
- Arithmetic operations (+, -, *, /)
- Comparison operations (<, >, ==, >=, <=, !=)
- Variables (global and local)
- Functions and closures
- Control flow (if, while)
- Classes and inheritance
- Object properties
- Lexical scoping with cells (for closures)
No installation required! Just needs Python 3.6+
chmod +x eva-vm.pypython3 eva-vm.py -e "(+ 1 2)"python3 eva-vm.py -f test.evapython3 eva-vm.py -e "(def square (x) (* x x)) (square 5)" -d(+ 2 3) ; => 5
(* (+ 2 3) 4) ; => 20(var x 10)
(var y 20)
(+ x y) ; => 30(def square (x)
(* x x))
(square 5) ; => 25(def makeCounter (start)
(begin
(var count start)
(lambda ()
(begin
(set count (+ count 1))
count))))
(var counter (makeCounter 0))
(counter) ; => 1
(counter) ; => 2
(counter) ; => 3; If statement
(if (> 10 5)
"yes"
"no") ; => "yes"
; While loop
(var i 0)
(while (< i 5)
(set i (+ i 1))) ; i becomes 5(class Point null
(def constructor (self x y)
(begin
(set (prop self x) x)
(set (prop self y) y)))
(def calc (self)
(+ (prop self x) (prop self y))))
(class Point3D Point
(def constructor (self x y z)
(begin
((prop (super Point3D) constructor) self x y)
(set (prop self z) z)))
(def calc (self)
(+ ((prop (super Point3D) calc) self) (prop self z))))
(var p (new Point3D 10 20 30))
((prop p calc) p) ; => 60The parser converts S-expressions into a simple AST:
- Numbers → integers
- Strings → strings (without quotes)
- Symbols → strings
- Lists → Python lists
Before compilation, the analyzer:
- Identifies all variable declarations
- Determines variable allocation (global, local, or cell)
- Promotes captured variables to heap-allocated cells
- Tracks free variables for closures
The compiler walks the AST and:
- Generates bytecode instructions
- Builds constant pools
- Creates code objects for functions
- Handles lexical scoping
The VM:
- Uses a stack for operands and local variables
- Uses a call stack for function frames
- Executes bytecode instructions sequentially
- Manages closures via heap-allocated cells
All values are dictionaries with a 'type' field:
# Number
{'type': 'NUMBER', 'value': 42}
# Boolean
{'type': 'BOOLEAN', 'value': True}
# String object
{'type': 'OBJECT', 'obj_type': 'STRING', 'value': 'hello'}
# Function object
{'type': 'OBJECT', 'obj_type': 'FUNCTION', 'code': {...}, 'cells': [...]}The VM uses a compact bytecode format:
| Opcode | Name | Description |
|---|---|---|
| 0x00 | HALT | Stop execution |
| 0x01 | CONST | Push constant |
| 0x02 | ADD | Add two values |
| 0x03 | SUB | Subtract |
| 0x04 | MUL | Multiply |
| 0x05 | DIV | Divide |
| 0x06 | COMPARE | Compare values |
| 0x07 | JMP_IF_FALSE | Conditional jump |
| 0x08 | JMP | Unconditional jump |
| 0x09 | GET_GLOBAL | Get global variable |
| 0x0A | SET_GLOBAL | Set global variable |
| 0x0B | POP | Pop stack |
| 0x0C | GET_LOCAL | Get local variable |
| 0x0D | SET_LOCAL | Set local variable |
| 0x0E | SCOPE_EXIT | Clean up scope |
| 0x0F | CALL | Call function |
| 0x10 | RETURN | Return from function |
| 0x11 | GET_CELL | Get cell variable |
| 0x12 | SET_CELL | Set cell variable |
| 0x13 | LOAD_CELL | Load cell for closure |
| 0x14 | MAKE_FUNCTION | Create closure |
| 0x15 | NEW | Create instance |
| 0x16 | GET_PROP | Get property |
| 0x17 | SET_PROP | Set property |
Instead of classes, we use:
- Dictionaries for structured data
- Functions for operations
- Closures for encapsulation
VM state is explicitly passed to functions:
def push(vm, value):
vm['stack'][vm['sp']] = value
vm['sp'] += 1Complex operations are built from simple functions:
def gen(state, exp):
if is_number_ast(exp):
emit(state['co'], OP_CONST)
emit(state['co'], alloc_numeric_const(state['co'], exp))The original C++ code uses static constants. In Python, we use module-level constants:
OP_HALT = 0x00
OP_CONST = 0x01
# ...# Simple math
python3 eva-vm.py -e "(+ 2 3)"
# Output: 5
# Functions
python3 eva-vm.py -e "(def square (x) (* x x)) (square 5)"
# Output: 25
# Factorial
python3 eva-vm.py -f examples/factorial.eva
# Output: 120
# Closures
python3 eva-vm.py -f examples/closure.eva
# Output: 13
# Fibonacci
python3 eva-vm.py -f examples/fibonacci.eva
# Output: 55
# Classes
python3 eva-vm.py -f examples/class.eva
# Output: 50
# Class inheritance (original test)
python3 eva-vm.py -f test.eva
# Output: 60Run the test file:
python3 eva-vm.py -f test.evaShould output: 60
Run with disassembly to see bytecode:
python3 eva-vm.py -f test.eva -dThis implementation prioritizes:
- Readability over performance
- Simplicity over optimization
- Learning over production use
For production use, consider:
- PyPy for JIT compilation
- Cython for C-level performance
- Or stick with the original C++ version
Based on the Eva VM course by Dmitry Soshnikov: http://dmitrysoshnikov.com/courses/virtual-machine/
Original C++ implementation: https://github.com/DmitrySoshnikov/eva-vm
Pwn College Yan85 emulator from it's Reverse Engineering Dojo.