This document provides a comprehensive overview of the Ruby compiler's architecture, design decisions, and implementation details.
This is a self-hosting Ruby compiler that compiles Ruby source code to native x86 (32-bit) assembly. The compiler follows a "bottom-up" development approach, prioritizing incremental functionality over completeness. The ultimate goal is a fully self-hosting compiler that can compile itself through a three-stage bootstrap process.
Ruby Source → Scanner → Parser → AST → Transformer → Compiler → x86 Assembly → GCC → Native Binary
- Scanner (
scanner.rb): Tokenizes Ruby source into a stream of tokens - Parser (
parser.rb): Builds an Abstract Syntax Tree (AST) using recursive descent with Shunting Yard for expressions - Transformer (
transform.rb): Applies tree transformations and optimizations - Compiler (
compiler.rb): Walks the AST and generates x86 assembly code - Emitter (
emitter.rb): Outputs assembly with register allocation and optimization - GCC: Assembles and links with the garbage collector to produce the final binary
The compiler achieves self-hosting through a three-stage bootstrap:
- Stage 1: MRI Ruby compiles the compiler source →
out/driver(native binary) - Stage 2:
out/drivercompiles the compiler source →out/driver2 - Stage 3:
out/driver2compiles the compiler source →out/driver3(should be identical todriver2)
- Character-by-character lexical analysis
- Handles Ruby tokens including keywords, operators, literals
- Position tracking for error reporting
- Special handling for string interpolation and regex patterns
Parser Architecture:
- Recursive descent parser for statements and control structures
- Shunting Yard algorithm (
shunting.rb) for expression parsing with operator precedence - Handles Ruby's complex grammar including blocks, method calls, and class definitions
Key Parsing Components:
SEXParserfor S-expression support- Operator precedence handling (
operators.rb) - Error recovery and position tracking
- Support for Ruby's flexible syntax (optional parentheses, etc.)
- Node-based representation of parsed Ruby code
- Supports all major Ruby constructs: classes, methods, blocks, control flow
- Provides visitor pattern for tree walking
- Enables tree transformations and optimizations
- Walks the AST and generates x86 assembly
- Manages scope hierarchies and symbol resolution
- Handles method dispatch and object model implementation
- Coordinates with specialized compilation modules
compile_arithmetic.rb: Mathematical operations, integer arithmeticcompile_calls.rb: Method calls, blocks, closures, argument handlingcompile_class.rb: Class definitions, inheritance, vtablescompile_control.rb: Control flow (if/while/case/rescue)compile_comparisons.rb: Comparison operatorscompile_include.rb: Module inclusion and mixing
- Generates x86 32-bit assembly code
- Integrates register allocation
- Handles calling conventions and stack management
- Supports debugging information (STABS format)
- Peephole optimization
- Custom register allocator for x86 registers
- Handles register spilling and reloading
- Manages register locking across function call boundaries
- Optimizes register usage for expression evaluation
- Vtable-based method dispatch
- Support for eigenclasses and singleton methods
- Implementation of
method_missing - Type tagging for integers to reduce object allocation
- Mark-and-sweep garbage collector written in C
- Integrated with Ruby object allocation
- Handles circular references and finalization
- Currently simple but functional
- Reimplementation of Ruby core classes for the compiled environment
- Includes: Object, Class, Array, Hash, String, Integer, Symbol
- Handles bootstrapping issues with circular dependencies
- Optimized for the compiled runtime rather than compatibility
- Base
Scopeclass for variable and constant resolution - Chains scopes to handle nested contexts
- Manages local variable allocation and access
GlobalScope(globalscope.rb): Global variables, constants, and built-ins- Automatically registers variables starting with
$as global variables - Returns
[:global, name]for registered globals,[:addr, name]for constants - Special handling for
$:(LOAD_PATH),$0, etc. via aliases
- Automatically registers variables starting with
ClassScope(classcope.rb): Class definitions, instance variables, inheritanceLocalVarScope(localvarscope.rb): Method-local variablesControlScope(controlscope.rb): Control flow constructs (blocks, loops)DebugScope(debugscope.rb): Debug information and symbol tables
Global variables (starting with $) require special handling:
- Registration: When
GlobalScope.get_argencounters a symbol starting with$, it auto-registers it in the globals hash - Assembly naming: The
$prefix must be stripped for x86 assembly (done inemitter.rb) - Initialization: Uninitialized globals default to 0 in BSS, requiring explicit nil initialization
__init_globalsfunction generated incompiler.rb:output_global_init- Called from
lib/core/nil.rbafter nil is initialized - Checks each
$-prefixed global: if still 0, sets to nil - Avoids overwriting already-initialized values (e.g., if user assigned before initialization ran)
- Global symbol table for method names, constants, and identifiers
- Handles symbol interning and deduplication
- Supports symbol-to-string and string-to-symbol conversion
- Pre-creates commonly used symbols at startup
Static vs Dynamic Features:
requirestatements processed at compile time, not runtime- Method definitions and class structures mostly static
- Dynamic features like
evalnot supported method_missingsupported through vtable mechanisms
Memory Management:
- Objects allocated on heap and managed by garbage collector
- Type tagging for immediate values (integers)
- Stack-allocated local variables where possible
- Reference counting not used due to complexity
x86 32-bit Target:
- 4-byte pointers (
PTR_SIZE = 4) - Uses x86 calling conventions
- Leverages x86 specific optimizations
- All development done in Docker containers for consistency
Dependencies:
- GCC for final assembly and linking
- Docker environment for reproducible builds
- Valgrind for memory debugging
Unsupported Features:
- Exceptions (begin/rescue/ensure) - minimal support only
- Regular expressions
- Floating point arithmetic
- eval and runtime code generation
- Full metaprogramming (const_missing, etc.)
Workarounds in Compiler Source:
- Code marked with
@bugindicates compiler bug workarounds FIXMEcomments mark temporary solutions- Some Ruby idioms avoided to work around missing features
- Expression nodes for operators, method calls, literals
- Statement nodes for control flow, assignments, definitions
- Visitor pattern support for tree traversal
Valueclass wraps objects with optional type information- Supports type tracking through compilation pipeline
- Delegates to underlying Ruby objects for operations
Registerclass represents x86 registersRegisterAllocatormanages allocation and spilling- Supports register locking and cross-call preservation
Functionclass manages method definitions- Handles argument parsing and local variable allocation
- Supports blocks and closure creation
- Constant folding and dead code elimination
- Variable lifting and scope analysis
- Type inference where possible
- Method call rewriting (e.g.,
a.b = c→a.b=(c)) - Block and closure handling
- Control flow normalization
- Recursive AST traversal
- Register allocation and stack management
- Method dispatch code generation
- Runtime system integration
- Peephole optimizations
- Dead code elimination at assembly level
- Register usage optimization
- Minimal test framework avoiding external dependencies
- Tests core compiler functionality required for self-hosting
- Validates parser, code generation, and runtime behavior
- RSpec tests for development
- Cucumber features for behavior validation
- Comparison testing (MRI vs compiled output)
- Three-stage bootstrap ensures compiler correctness
- Assembly output comparison between stages
- Functional testing of compiled binaries
- Type tagging for integers reduces allocation
- Vtable caching for method dispatch
- Pre-allocation of common symbols
- Stack allocation for local variables
- Garbage collection overhead significant
- Large number of small object allocations
- Simple mark-and-sweep collector not optimized for many objects
- No inlining or advanced optimizations
- Symbol table grows without bounds
- No incremental compilation
- Memory usage grows linearly with program size
- Single-threaded execution model
This architecture represents a functional but evolving compiler design, with many opportunities for optimization and feature completion while maintaining the core goal of self-hosting compilation.