ruby/ruby
Bytecode compiler
compile.c walks an AST and emits a packed iseq (instruction sequence). It's the bridge between either parser and the VM. At ~14,800 lines, it's the largest hand-edited C file in the tree and the place where most new language features land.
Purpose
Translate an in-memory AST into:
- A flat array of bytecode opcodes and operands (the actual bytecode).
- A constant pool referenced by index.
- A line-number table for backtraces.
- A catch table for
rescue/ensure/retry/break/redo/nexttargets. - Metadata: argument descriptor, local variable table, parent iseq, optimization level.
Files
| File | Purpose |
|---|---|
compile.c |
The compiler proper. Walks NODE trees from parse.y. |
prism_compile.c |
The Prism compiler. Walks pm_node_t trees. |
prism_compile.h |
Shared declarations between both compilers. |
iseq.c |
iseq allocation, marshaling, disassembly, GC integration. |
iseq.h |
iseq layout. The rb_iseq_t struct. |
insns.def |
Bytecode instruction definitions. |
vm_core.h |
The runtime types (rb_iseq_t, rb_vm_t, callinfo, callcache). |
vm_callinfo.h |
Inline cache, callinfo, and callcache structs. |
tool/ruby_vm/ |
Codegen for vm_exec.c, dispatch table, JIT helpers. |
tool/instruction.rb |
Loader for insns.def. |
Compilation pipeline
graph LR
ast[AST] -->|enter scope| optimize[NODE optimizations\ncompile_optimized_call]
optimize -->|tree walk| emit[Emit ADD_INSN]
emit -->|build linked list| linked[ANCHOR list]
linked -->|peephole| peep[iseq_peephole_optimize]
peep -->|fix labels| labels[Resolve jump targets]
labels -->|finalize| iseq[rb_iseq_t]Compilation happens in three phases:
1. Tree walk (iseq_compile_each)
iseq_compile_each is a giant switch on nd_type(node) that recursively emits bytecode for each AST node into a doubly-linked list of LINK_ANCHOR entries. Each entry is either an instruction (INSN) or a label (LABEL).
Many node types have their own helpers — compile_call, compile_iter, compile_match, etc. The compiler tracks:
- The current scope (
ISEQ_BODY(iseq)->local_table,ISEQ_BODY(iseq)->local_table_size). - Whether the current node's value is "popped" or "used".
- The current
rescue/ensurestack for catch-table generation.
2. Peephole optimization (iseq_optimize)
The linked list goes through several optimization passes:
- Constant folding:
putobject 1; putobject 2; opt_plus→putobject 3. - Tail-call optimization: under
--enable-tailcall. - Specialized instructions:
opt_*_specialvariants for common patterns (+,-,<,==,[], etc.) that avoid full method dispatch when the receiver isInteger,Float,String, orArray. - Dead-code elimination: branches that can be statically resolved.
- Stack caching: the
--enable-stack-cachingbuild option enables an alternate dispatch where the top of the stack is always in registers.
The flags controlling each optimization are in rb_compile_option_t (iseq.h).
3. Finalization
After optimizations, the compiler:
- Assigns numeric offsets to labels.
- Builds the catch table from collected
rescue/ensureentries. - Builds the line-number table from collected source locations.
- Allocates the final
iseq->body->iseq_encodedarray and copies the linked list into it. - Computes operand types per instruction (for the GC scanner).
The result is an rb_iseq_t whose body field has fields like:
struct rb_iseq_constant_body {
enum rb_iseq_type type; /* :method, :block, :class, :top, :rescue, :ensure, ... */
unsigned int iseq_size;
VALUE *iseq_encoded; /* the bytecode */
struct rb_iseq_param_keyword *param_keyword;
struct rb_iseq_constant_body::iseq_insn_info insn_info;
rb_id_table_t *local_table;
/* + many more fields */
};insns.def — the instruction set
insns.def is a custom DSL describing every VM instruction. A typical entry:
DEFINE_INSN
opt_plus
(CALL_DATA cd)
(VALUE recv, VALUE obj)
(VALUE val)
{
val = vm_opt_plus(recv, obj);
if (UNDEF_P(val)) {
CALL_SIMPLE_METHOD();
}
}The four parts are:
- Operands: pulled from the bytecode stream (e.g., a
CALL_DATA*for an inline cache). - Stack inputs: popped before the instruction body runs.
- Stack outputs: pushed after.
- Body: C code that runs the instruction.
tool/ruby_vm/ parses insns.def and emits:
vm_exec.c— the dispatch loop. Two flavours:switch-based and threaded.vmtc.inc— the threaded-code label table.optunifs.inc,opt_sc.inc— optimizer support files.- JIT entry points and decompilers.
A few example instructions:
| Opcode | Purpose |
|---|---|
putobject |
Push a literal value |
putstring |
Push a frozen String |
getlocal/setlocal |
Local variable read/write |
getinstancevariable/setinstancevariable |
IV read/write (shape-aware) |
opt_send_without_block |
Method call with an inline cache |
opt_plus/opt_minus/opt_lt/opt_eq |
Specialized arithmetic and comparison |
branchif/branchunless |
Conditional jump |
jump |
Unconditional jump |
leave |
Return from current frame |
throw |
Raise an exception (or break/return) |
defineclass |
Open or define a class |
The full list and semantics live in insns.def.
Inline caches
Method dispatch (opt_send_without_block and friends) consults a per-call-site rb_callcache (vm_callinfo.h). The cache stores:
- The class of the last-seen receiver.
- The resolved
rb_method_entry_t. - A hit counter used by JITs.
If the cache hits, dispatch is roughly a class compare + indirect call. On miss, the VM walks the method table (vm_method.c) and updates the cache.
Constant lookups (getconstant) and instance variable accesses (getinstancevariable) have similar caches, keyed on the constant table version and the object shape respectively.
Shapes
shape.c / shape.h implement Object Shapes — small immutable transition records that describe an object's instance variables. The compiler emits IV access using shape-aware instructions; the VM looks up an IV by (shape_id, ivar_index) instead of by name. This makes IV access a couple of indirected loads in the common case.
The basic flow:
- New object starts with shape 0 (root).
- First
obj.@x = ...transitions to a new shape "root → x". - Future
obj.@xaccesses on objects with the same shape skip the lookup.
Selected optimizations
compile.c implements several non-obvious optimizations:
- Inline definitions:
lambda { |x| x + 1 }body is compiled into the parent iseq's pool and linked, not relegated to a separate dynamic alloc. - Specialized literals:
[1, 2, 3]bypasses generalArray.newand usesduparray. - Frozen string literals: with
# frozen_string_literal: true, all"..."in the file lower toputobjectof a frozen String. - Speculative inlining: methods named in
tool/instruction.rb's "always inlined" list (Integer#+,Array#[], etc.) get specialized opcodes when the receiver type matches.
Disassembling
puts RubyVM::InstructionSequence.compile("a = 1; a + 2").disasmProduces:
== disasm: #<ISeq:<compiled>@<compiled>:1 (1,0)-(1,12)> (catch: false)
local table (size: 1, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])
[ 1] a@0
0000 putobject_INT2FIX_1_ ( 1)[Li]
0001 setlocal_WC_0 a@0
0003 getlocal_WC_0 a@0
0005 putobject 2
0007 opt_plus <calldata!mid:+, argc:1, ARGS_SIMPLE>[CcCr]
0009 leave./ruby --dump=insns_without_opt -e 'expr' produces unoptimized output, useful when debugging the optimizer.
Entry points for modification
- Add a new instruction: edit
insns.def, runmake srcs, updatecompile.cto emit it, updatevm_exec.c(regenerated). Both YJIT (yjit/src/codegen.rs) and ZJIT (zjit/src/) need handling. - Fix a miscompile: the offending node type's compile function in
compile.cis the place to look.--dump=parsetreeand--dump=insns_without_optare your friends. - Add an optimization: extend
iseq_peephole_optimizeor add a new pass invoked fromiseq_setup. - Prism parity: every change to
compile.cmust be mirrored inprism_compile.c(or a justification given).
See vm.md for what runs the iseqs and jits/index.md for how YJIT/ZJIT consume them.
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.