source

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 the main 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 TypeSelf ContextVariable AccessException HandlingUse Case
topmainTop-level onlyNoProgram initialization
mainmainScript scopeYesScript entry point
methodReceiverParameters + localsYesMethod execution
blockParent’s selfParent + localsYesClosures, iteration
classClass objectClass scopeYesClass/module definition
rescueParent’s selfParent + exceptionNoException handling
ensureParent’s selfParent scopeNoCleanup code
evalCaller’s selfCaller’s bindingYesDynamic code
plainN/AMinimalNoInternal 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 cleanup
  • block: Can be optimized if doesn’t escape

Slower Frames:

  • eval: Dynamic compilation overhead
  • rescue: Exception handling adds overhead
  • ensure: 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