
Source code repository: https://github.com/DmitrySoshnikov/eva-vm-source
In this lecture:
Course overview and agenda
Parsing pipeline
Tokenizer module (Lexical analysis)
Parser module (Syntactic analysis)
Abstract Syntax Tree (AST)
Interpreters vs. Compilers
AST interpreters
Bytecode interpreters
Eva programming language
S-expression
Instruction pointer
Eval loop
Halt instruction
Stack-based VM
Stack pointer
Constant pool
Java bytecode example
Python bytecode example
Compiler explorer tool
Register-based VM
Accumulator register
Program exit macro
Streaming error messages
Logging values
Errata:
Make sure to initialize the stack pointer before using push and pop operations: https://github.com/DmitrySoshnikov/eva-vm-source/blob/main/src/vm/EvaVM.h#L197
Lecture content:
CONST instruction
Constant pool
Operands stack
Push and pop
Eva value encoding
Tagged unions
Stack overflow
Simple numbers
Value accessors
Binary instructions
Math operators
Generic binary macro
Introduction to LLDB
Step debugging
Stack introspection
Complex operation
FILO/LIFO stack specifics
Generic Object type
Heap-allocated values
String object type
String concatenation
Memory leaks and Garbage Collection
Parser generators: Syntax tool
BNF grammars (Backus-Naur Form)
S-Expression grammar
AST node types: Atoms, Lists
LLDB introspection
Introduction to Compiler
Code objects
AST traversal
Recursive code generation
Compiling numbers and strings
Lists compilation
Value representation
Nested expressions
Compiling math operations
Boolean values
Comparison operators
Branch instruction
Control flow
If-else expression
Branch instruction
Conditional jumps
Unconditional jumps
Bytecode patching
Introduction to Disassembler
Bytecode offsets
Decompiling instructions
Opcode string representation
Instruction operands
Variable declaration
Value access
Global object and storage
Variable update
Blocks: groups of expressions
Pop operation
Local scope and local variables
Scope enter and Scope exit
Base pointer (aka Frame pointer)
Control flow
While loops
For loops
Jump instructions
Bytecode patching
Native function objects
Function calls
Function arity
Arguments on the stack
Callee cleanup
Stack dump
Function objects
Code objects
Custom bytecode
Function parameters
Scope exit
Return operator
Function calls
Jumping to function code
Call stack
Stack frame
Return address
Base pointer
Return operator
Anonymous functions
Immediately-invoked Lambda Expressions (IILE)
Syntactic sugar
Optimizing compiler
Bytecode restructuring
Automatic POP for globals
Automatic initialization of locals
Relative jumps
Pre-calculation of simple values
Introduction to closures
Global, Locals and Cells
Stack and Heap allocation
Scope structure
Escape analysis
Cell promotion
Globals, Locals, and Cells
Compiler support for cells
Scope info
Scope stack
Environments
Cell objects
Closure objects
Free and Own cells
Capturing free variables
Introduction to garbage collectors
Managed vs. Unmanaged languages
Tracing heap
Allocation | Deallocation
Object header
Memory stats
GC roots
Constant roots
Mark-Sweep GC algorithm
Abstract code and implementation
GC roots: stack, constants, globals
GC threshold
Tracing heap
Free list
Garbage reclaim
Object-oriented programming
Class objects
Inner vs. Global classes
Static vs. Dynamic dispatch
Environments
Methods resolution
Object-oriented programming
Instance objects
Inheritance
Self object
Constructor function
Property access
Bytecode patching
Child classes
Super classes
Inheritance chains
Eva VM executable
Evaluating expressions
Executing files
Next steps
Assembly
JIT-compilation
Course overview
How programming languages work under the hood? What’s the difference between compiler and interpreter? What is a virtual machine, and JIT-compiler? And what about the difference between functional and imperative programming?
There are so many questions when it comes to implementing a programming language!
The problem with “compiler classes” in school is such classes are usually presented as some “hardcore rocket science” which is only for advanced engineers.
Moreover, classic compiler books start from the least significant topic, such as Lexical analysis, going straight down to the theoretical aspects of formal grammars. And by the time of implementing the first Tokenizer module, students simply lose an interest to the topic, not having a chance to actually start implementing a programing language itself. And all this is spread to a whole semester of messing with tokenizers and BNF grammars, without understanding an actual semantics of programming languages.
I believe we should be able to build and understand a full programming language semantics, end-to-end, in 4-6 hours — with a content going straight to the point, showed in live coding sessions as pair-programming and described in a comprehensible way.
In the Building a Virtual Machine class we focus specifically on runtime semantics, and build a stack-based VM for a programming language very similar to JavaScript or Python. Working closely with the bytecode level you will understand how lower-level interpretation works in production VMs today.
Implementing a programing language would also make your practical level in other programming languages more professional.
Prerequisites
There are two prerequisites for this class.
The Building a Virtual Machine course is a natural extension for the previous class — Building an Interpreter from scratch (aka Essentials of Interpretation), where we build also a full programming language, but at a higher, AST-level. Unless you already have understanding of how programming languages work at this level, i.e. what eval, a closure, a scope chain, environments, and other constructs are — you have to take the interpreters class as a prerequisite.
Also, going to lower (bytecode) level where production VMs live, we need to have basic C++ experience. This class however is not about C++, so we use just very basic (and transferrable) to other languages constructs.
Watch the introduction video for the details.
Who this class is for?
This class is for any curious engineer, who would like to gain skills of building complex systems (and building a programming language is an advanced engineering task!), and obtain a transferable knowledge for building such systems.
If you are interested specifically in compilers, bytecode interpreters, virtual machines, and source code transformation, then this class is also for you.
What is used for implementation?
Since lower-level VMs are about performance, they are usually implemented in a low-level language such as C or C++. This is exactly what we use as well, however mainly basic features from C++, not distracting to C++ specifics. The code should be easily convertible and portable to any other language, e.g. to Rust or even higher-level languages such as JavaScript — leveraging typed arrays to mimic memory concept. Using C++ also makes it easier implementing further JIT-compiler.
Note: we want our students to actually follow, understand and implement every detail of the VM themselves, instead of just copy-pasting from final solution. Even though the full source code for the language is presented in the video lectures, the code repository for the project contains /* Implement here */ assignments, which students have to solve.
What’s specific in this class?
The main features of these lectures are:
Concise and straight to the point. Each lecture is self-sufficient, concise, and describes information directly related to the topic, not distracting on unrelated materials or talks.
Animated presentation combined with live-editing notes. This makes understanding of the topics easier, and shows how the object structures are connected. Static slides simply don’t work for a complex content.
Live coding session end-to-end with assignments. The full source code, starting from scratch, and up to the very end is presented in the video lectures
What is in the course?
The course is divided into five parts, in total of 29 lectures, and many sub-topics in each lecture. Below is the table of contents and curriculum.
PART 1: VM BASIC OPERATIONS
In this part we describe compilation and interpretation pipeline, starting building our language. Topics of Stack and Register VMs, heap-allocated objects and compilation of the bytecode are discussed.
PART 2: CONTROL FLOW AND VARIABLES
In this part we implement control flow structures such as if expressions and while loops, talk about Global object and global variables, nested blocks and local variables, and also implement a disassembler.
PART 3.1: FUNCTIONS AND CALL STACK
In this part we start talking and implementing function abstraction and function calls. We describe concept of the Call stack, native and user-defined functions, and IILEs (Immediately-invoked lambda expressions).
PART 3.2: CLOSURES IMPLEMENTATION
In this part we focus on closures implementation, talking about scope and escape analysis, capturing free variables, and adding runtime support for closures.
PART 4: GARBAGE COLLECTION
This part is devoted to the automatic memory management known as Garbage collection. We discuss a tracing heap and implement Mark-Sweep garbage collector.
PART 5: OBJECT-ORIENTED PROGRAMMING
In the final part we add support for Object-oriented programming, implementing classes and instances. In addition we build the final VM executable.