A frame (also called an execution frame, stack frame, or activation record) is a data structure that holds the execution state of a method or function in a virtual machine. Each time a method is called at a call site, the VM creates a new frame to track that method’s execution.
What is a Frame?
Think of a frame as a snapshot of everything needed to execute a method:
def calculate(x, y)
result = x + y # Frame tracks: x, y, result, and execution state
result * 2
end
calculate(3, 4)
When calculate
executes, its frame contains:
- Local variables:
x
,y
,result
- Program counter: Current instruction being executed
- Stack pointer: Position in the value stack
- Environment pointer: Base of this frame’s stack region
- Return address: Where to resume after method completes
- Frame type: The kind of execution context (see YARV frame types)
Frame Structure in YARV
In YARV, a frame contains:
┌─────────────────────────────┐
│ Frame │
├─────────────────────────────┤
│ Frame Type │ → One of 9 YARV frame types
├─────────────────────────────┤
│ Program Counter (PC) │ → Points to current instruction
├─────────────────────────────┤
│ Stack Pointer (SP) │ → Points to next free stack slot
├─────────────────────────────┤
│ Environment Pointer (EP) │ → Points to frame's stack base
├─────────────────────────────┤
│ Instruction Sequence │ → Reference to [[instruction sequence]]
├─────────────────────────────┤
│ Local Variables │ → Method's local variables
├─────────────────────────────┤
│ Block/Proc Reference │ → Associated block if any
├─────────────────────────────┤
│ Self (receiver) │ → Current [[receiver]] object
└─────────────────────────────┘
YARV uses nine distinct YARV frame types to handle different execution contexts: top
, main
, method
, block
, class
, rescue
, ensure
, eval
, and plain
. Each type has specific characteristics for variable access, exception handling, and performance.
Environment Pointer
The environment pointer (EP) is crucial for stack management:
def outer
a = 1
b = 2
inner(a, b)
end
def inner(x, y)
z = x + y
end
When inner
executes:
Stack with Environment Pointers:
┌─────────┐
│ 3 │ ← SP (stack pointer)
├─────────┤
│ z │
├─────────┤
│ y │
├─────────┤
│ x │ ← EP for 'inner' frame
├─────────┤
│ b │
├─────────┤
│ a │ ← EP for 'outer' frame
└─────────┘
The EP marks where each frame’s stack space begins, allowing the VM to:
- Access local variables by offset from EP
- Clean up stack space when method returns
- Isolate frames from each other
Program Counter
Each frame has its own program counter pointing to the current instruction:
def example
a = 1 # PC points here initially
b = 2 # Then here
a + b # Then here
end
The frame’s PC tracks progress through the instruction sequence:
Frame:
PC → Instruction 3 of 8
(executing: putobject 2)
Stack Pointer
The stack pointer (SP) tracks the next available slot on the value stack:
def add(x, y)
x + y
end
# YARV execution:
# 1. Push x → SP moves up
# 2. Push y → SP moves up
# 3. Execute add → SP moves down (popped 2, pushed 1)
The SP ensures proper stack management:
- Points to next write position
- Updates on push/pop operations
- Must return to original position when frame exits
Call Stack vs Value Stack
Important distinction in YARV:
Call Stack: Stack of frames
┌─────────┐
│ Frame C │ ← Current executing method
├─────────┤
│ Frame B │ ← Called by A
├─────────┤
│ Frame A │ ← Bottom frame
└─────────┘
Value Stack: Stack of operands (within each frame)
Frame B's value stack:
┌─────────┐
│ 42 │ ← SP
├─────────┤
│ 10 │
├─────────┤
│ nil │ ← EP
└─────────┘
See stack-based virtual machine for how these work together.
Frame Lifecycle
graph TD A[Call Site Executed] --> B[Create New Frame] B --> C[Initialize PC, SP, EP] C --> D[Execute Instructions] D --> E{Return?} E -->|No| D E -->|Yes| F[Restore Previous Frame] F --> G[Resume at Call Site] style A fill:#e1f5ff style B fill:#fff4e1 style D fill:#e8f5e9 style F fill:#fce4ec
- Creation: Call site invoked
- Initialization: Set PC to first instruction, EP to stack base
- Execution: PC advances through instruction sequence
- Completion: Return value left on stack
- Destruction: Frame popped, control returns to caller
Blocks and Closures
Frames become more complex with blocks:
def outer
x = 10
lambda { x + 1 } # Block captures outer frame's state
end
proc = outer
proc.call # Block still references outer's frame
YARV must preserve frame data even after the method returns if a block captures it. This is called frame escape or reification - making the frame concrete/permanent on the heap.
Frame Inspection in Ruby
You can inspect frames using Ruby’s built-in tools:
# Get current frame's binding
def show_frame
puts binding.local_variables # [:x, :y] if those are defined
end
# Stack trace shows frames
begin
raise "error"
rescue => e
puts e.backtrace # Each line is a frame
end
# Caller information
def inner
puts caller # Shows calling frames
end
Performance Implications
Frame management affects YARV performance:
Fast Operations:
- Creating frames for simple methods
- Accessing local variables (offset from EP)
- Method returns with predictable stack cleanup
Slow Operations:
- Frame escape for closures (must heap-allocate)
- Deep call stacks (many frames)
- Exception handling (stack unwinding through frames)
See virtual machine architecture for optimization strategies.
Key Insights
- Execution Context: Frames encapsulate everything needed to execute a method
- Pointer Trio: PC, SP, and EP work together to manage execution
- Stack Isolation: EP separates each frame’s stack space
- Lifecycle Management: Frames are created, executed, and destroyed efficiently
- Closure Complexity: Captured frames must outlive their normal lifetime
- Performance Critical: Frame operations happen on every method call
Understanding frames is fundamental to grasping how YARV and other VMs execute code, manage state, and implement features like closures.