In YARV, different execution contexts require different types of frames. Each frame type has specific characteristics that determine how variables are accessed, how control flows, and how the stack is managed.
Overview of Frame Types
YARV defines nine distinct frame types, each serving a specific execution context:
Frame Type Hierarchy:
┌─────────────────────────────────────┐
│ top Top-level execution │
│ main Main script frame │
├─────────────────────────────────────┤
│ method Method definition │
│ block Block execution │
│ class Class/module body │
├─────────────────────────────────────┤
│ rescue Exception handling │
│ ensure Cleanup blocks │
│ eval Dynamic evaluation │
│ plain Special (once insn) │
└─────────────────────────────────────┘
Core Frame Types
Top Frame
The top frame represents top-level execution context outside of any method or class:
# This code executes in a top frame
puts "Hello, world!"
x = 42
Characteristics:
- First frame created when program starts
- Contains top-level local variables
- Forms the base of the call stack
- Constants defined here become global-level constants
Main Frame
The main frame is the entry point for script execution:
# main.rb - executes in main frame
def helper
# method frame
end
helper # Called from main frame
Characteristics:
- Created for the primary script file
self
refers to themain
object- Top-level method definitions become private methods on Object
- Distinct from the top frame (subtle differences in scope)
Method Frame
A method frame is created each time a method is called at a call site:
def calculate(x, y)
# This body executes in a method frame
result = x + y
result * 2
end
calculate(3, 4) # Creates new method frame
Characteristics:
- Contains method’s local variables
- Has access to method parameters
self
is the receiver object- PC tracks execution through method’s instruction sequence
- Frame destroyed when method returns via
leave
instruction
See frame for detailed frame structure.
Block Frame
A block frame is created for block execution (lambdas, procs, iterators):
[1, 2, 3].each do |number|
# This block body executes in a block frame
double = number * 2
puts double
end
Characteristics:
- Can access parent frame’s variables (closure behavior)
- Block parameters stored in frame’s local table
- May escape parent frame (requires reification)
- Multiple block frames created in iteration
Block Frame Access Pattern:
Parent Frame (method)
├── Local: x = 10
└── Child Block Frame
├── Can read: x
├── Can write: x (shared scope)
└── Local: y = 20
Class Frame
A class frame executes class, module, or singleton class bodies:
class MyClass
# This body executes in a class frame
attr_reader :name
def initialize
# method frame (nested within class frame context)
end
class << self
# Another class frame (singleton class)
end
end
Characteristics:
self
is the class/module object- Constant definitions scoped to the class
- Method definitions added to class’s method table
- Nested class definitions create nested class frames
Exception Handling Frames
Rescue Frame
A rescue frame handles exception catching:
begin
# code that might raise
risky_operation
rescue StandardError => e
# This rescue body executes in a rescue frame
puts "Error: #{e.message}"
end
Characteristics:
- Created when exception is raised and caught
- Has access to exception object
- Can access variables from parent frame
- Multiple rescue clauses may create multiple frames
Rescue Frame Flow:
graph TD A[Method Frame: begin block] -->|Exception raised| B{Rescue frame created} B -->|Matches StandardError| C[Execute rescue body] B -->|No match| D[Propagate to parent frame] C --> E[Resume execution after rescue] D --> F[Continue unwinding stack] style A fill:#e1f5ff style B fill:#fff4e1 style C fill:#e8f5e9 style D fill:#fce4ec
Ensure Frame
An ensure frame executes cleanup code that must run regardless of exceptions:
def read_file(path)
file = File.open(path)
# method frame
process(file)
ensure
# This ensure block executes in an ensure frame
file.close if file
end
Characteristics:
- Always executes, even if exception raised
- Runs after method completes (normally or via exception)
- Cannot prevent exception propagation
- Used for resource cleanup
Ensure Frame Guarantees:
Normal Flow:
Method → Ensure → Return
Exception Flow:
Method → Exception → Ensure → Re-raise
Return Flow:
Method → Return → Ensure → Resume
Special Frame Types
Eval Frame
An eval frame is created for dynamically evaluated code:
code = "x = 42; x * 2"
result = eval(code) # Creates eval frame
Characteristics:
- Executes dynamically compiled instruction sequences
- Can access surrounding scope (like a block)
- Has access to caller’s binding
- Performance cost: cannot be optimized like static code
Security Note: eval
frames can access parent scope, making them potentially dangerous with untrusted input.
Plain Frame
A plain frame is a special-purpose frame used primarily by the once
instruction:
# The 'once' instruction creates a plain frame
# Used for ensuring code runs exactly once
Characteristics:
- Minimal frame type
- Used internally by YARV
- Not directly created by user code
- Optimizes certain Ruby patterns (like
@@var ||= value
)
Frame Type Comparison
Frame Type | Self Context | Variable Access | Exception Handling | Use Case |
---|---|---|---|---|
top | main | Top-level only | No | Program initialization |
main | main | Script scope | Yes | Script entry point |
method | Receiver | Parameters + locals | Yes | Method execution |
block | Parent’s self | Parent + locals | Yes | Closures, iteration |
class | Class object | Class scope | Yes | Class/module definition |
rescue | Parent’s self | Parent + exception | No | Exception handling |
ensure | Parent’s self | Parent scope | No | Cleanup code |
eval | Caller’s self | Caller’s binding | Yes | Dynamic code |
plain | N/A | Minimal | No | Internal optimization |
Frame Stack Visualization
When frames nest, they form a call stack:
class Calculator # class frame
def compute(x, y) # method frame (nested)
[x, y].map do |n| # block frame (nested)
begin
n * 2
rescue => e # rescue frame (nested)
0
ensure # ensure frame (nested)
cleanup
end
end
end
end
# Call stack at deepest point:
┌──────────────────┐
│ ensure frame │ ← Current frame
├──────────────────┤
│ rescue frame │
├──────────────────┤
│ block frame │
├──────────────────┤
│ method frame │
├──────────────────┤
│ class frame │
├──────────────────┤
│ main frame │
└──────────────────┘
Parent-Child Frame Relationships
Frames can access parent frames differently based on type:
Method Frames (Isolated)
def outer
x = 10
inner # inner cannot access x
end
def inner
# x not visible here - method frames are isolated
end
Block Frames (Shared Access)
def outer
x = 10
lambda { x + 1 } # Block can access x
end
proc = outer
proc.call # => 11 (accessed parent's x)
Access Pattern:
Method Frame A → Method Frame B
(isolated - no variable sharing)
Method Frame → Block Frame
(shared - block sees method's variables)
Block Frame → Nested Block Frame
(shared - closures all the way down)
Frame Lifecycle with Types
graph TD A[Execution begins] --> B{What context?} B -->|Top-level code| C[Create main frame] B -->|Method call| D[Create method frame] B -->|Block execution| E[Create block frame] B -->|Class/module| F[Create class frame] B -->|Exception raised| G[Create rescue frame] B -->|Cleanup needed| H[Create ensure frame] D --> I[Execute instruction sequence] E --> I F --> I I --> J{Exception?} J -->|Yes| G J -->|No| K{Ensure block?} K -->|Yes| H K -->|No| L[Return to caller] G --> K H --> L style C fill:#e1f5ff style D fill:#fff4e1 style E fill:#e8f5e9 style F fill:#fce4ec style G fill:#ffebee style H fill:#f3e5f5
Implementation Details
Each frame type is implemented with a type tag in YARV’s C code:
// Simplified representation
enum frame_type {
VM_FRAME_MAGIC_TOP = 0x41,
VM_FRAME_MAGIC_METHOD = 0x11,
VM_FRAME_MAGIC_BLOCK = 0x21,
VM_FRAME_MAGIC_CLASS = 0x31,
VM_FRAME_MAGIC_RESCUE = 0x51,
VM_FRAME_MAGIC_ENSURE = 0x61,
VM_FRAME_MAGIC_EVAL = 0x71,
VM_FRAME_MAGIC_MAIN = 0x81,
VM_FRAME_MAGIC_PLAIN = 0x91
};
The frame type determines:
- How instructions access variables
- Whether the frame can escape (be captured by closure)
- How exceptions propagate
- What
self
refers to
Performance Implications
Different frame types have different performance characteristics:
Fast Frames:
method
: Optimized for common case, predictable cleanupblock
: Can be optimized if doesn’t escape
Slower Frames:
eval
: Dynamic compilation overheadrescue
: Exception handling adds overheadensure
: Always-run guarantee requires special handling
Escaping Frames: When a block captures variables, its frame may need to escape to the heap (see reify):
def create_counter
count = 0
lambda { count += 1 } # Frame must escape - referenced after method returns
end
counter = create_counter
counter.call # => 1
counter.call # => 2
# Frame still alive, even though create_counter returned
Inspecting Frame Types
You can inspect the current frame type using Ruby’s internal APIs:
def what_frame_type
# Using binding to inspect current frame
puts RubyVM::InstructionSequence.of(binding).to_a[9]
end
what_frame_type # Shows frame type information