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
  1. Creation: Call site invoked
  2. Initialization: Set PC to first instruction, EP to stack base
  3. Execution: PC advances through instruction sequence
  4. Completion: Return value left on stack
  5. 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

  1. Execution Context: Frames encapsulate everything needed to execute a method
  2. Pointer Trio: PC, SP, and EP work together to manage execution
  3. Stack Isolation: EP separates each frame’s stack space
  4. Lifecycle Management: Frames are created, executed, and destroyed efficiently
  5. Closure Complexity: Captured frames must outlive their normal lifetime
  6. 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.