An operand is a value that an instruction uses to perform its operation. In YARV and other virtual machines, operands are known at compile-time and are built directly into the instruction sequence.

What is an Operand?

Operands are the “arguments” to VM instructions:

# Ruby code:
x = 42
 
# YARV bytecode:
putobject 42    # ← '42' is the operand
setlocal x      # ← 'x' (local variable index) is the operand

The instruction putobject needs to know what to push onto the stack. That “what” is the operand 42.

Operands vs Stack Values

Important distinction in stack-based VMs:

Operands: Known at compile-time, embedded in instructions

putobject 100   # 100 is an operand (compile-time constant)

Stack Values: Computed at runtime, live on the stack

getlocal a      # Gets 'a' (operand) and pushes its value (stack value)
getlocal b      # Gets 'b' (operand) and pushes its value (stack value)
add             # Pops two stack values, computes sum, pushes result

The add instruction has no operands - it operates purely on stack values.

Types of Operands

1. Literal Values:

# Ruby:
x = 42
 
# YARV:
putobject 42         # Operand: literal integer 42

2. Variable References:

# Ruby:
y = x
 
# YARV:
getlocal_WC_0 x      # Operand: local variable index/name
setlocal_WC_0 y      # Operand: local variable index/name

3. Jump Targets:

# Ruby:
if condition
  # code
end
 
# YARV:
getlocal condition
branchunless 10      # Operand: instruction offset (10)
# code
# label 10:

4. Call Data:

# Ruby:
object.method(arg)
 
# YARV:
opt_send_without_block <call_data>   # Operand: call site metadata

The call data includes method name, argument count, and flags.

5. Symbol/String References:

# Ruby:
hash = { key: "value" }
 
# YARV:
putobject :key       # Operand: symbol :key
putstring "value"    # Operand: string "value"
newhash 2            # Operand: hash size (2)

Operand Encoding

YARV uses variable-width encoding for operands:

Short operands (fit in instruction):

putobject_INT2FIX_1    # Operand '1' encoded in instruction name

Long operands (follow instruction):

putobject <operand>    # Operand follows as separate bytes
  42                   # ← Operand value here

This makes bytecode compact - common values use specialized instructions, rare values use general instructions with operands.

Operands in the Instruction Sequence

Operands are stored in the iseq:

Instruction Sequence Memory Layout:
┌─────────────┬──────────┬─────────────┬──────────┐
│ Instruction │ Operand  │ Instruction │ Operand  │ ...
├─────────────┼──────────┼─────────────┼──────────┤
│  putobject  │    42    │  setlocal   │    x     │ ...
└─────────────┴──────────┴─────────────┴──────────┘
       ↑                         ↑
       └── [[program counter|PC]] points here        └── PC advances to here

The program counter advances past both instructions and their operands.

Constant Pool References

Some operands reference the constant pool:

# Ruby:
def greet
  puts "Hello, World!"
end
 
# Constant pool in iseq:
# [0] = "Hello, World!"
# [1] = :puts
# [2] = <iseq:greet>
 
# Instructions:
# putstring @0         # Operand: index 0 in constant pool
# opt_send_without_block @1, ...   # Operand: index 1 (method :puts)

The operand is an index into the pool, not the value itself. This avoids duplicating large values in the iseq.

Immediate vs Indirect Operands

Immediate operands - value directly in instruction:

putobject 1          # '1' stored immediately
putnil               # No operand (nil is implicit)

Indirect operands - reference to elsewhere:

putstring @5         # Reference to constant pool slot 5
getlocal_WC_0 @2     # Reference to local variable slot 2

Operand Optimization

YARV optimizes operands:

Specialized Instructions:

# General form:
putobject 0          # Generic instruction + operand
 
# Optimized form:
putobject_INT2FIX_0  # Specialized instruction, no operand needed

Specialized instructions are faster - no operand to read, less instruction dispatch overhead.

Peephole Optimization:

# Before optimization:
putobject 2
putobject 2
add
 
# After optimization:
putobject 4          # Constant folding, new operand

The compiler pre-computes constant expressions and embeds the result as an operand.

Runtime Access

At runtime, the VM reads operands:

# Simplified VM execution loop
def execute(frame)
  instruction = frame.iseq[frame.pc]
 
  case instruction.type
  when :putobject
    operand = instruction.operand    # Read operand
    frame.stack.push(operand)        # Use it
    frame.pc += 1                    # Advance past instruction
  when :add
    # No operands to read
    b = frame.stack.pop
    a = frame.stack.pop
    frame.stack.push(a + b)
    frame.pc += 1
  end
end

The frame reads operands from the iseq as needed.

Operands and JIT Compilation

JIT compilers use operands to generate native code:

# Bytecode:
putobject 42
 
# YJIT generates (conceptual):
mov rax, 42          # Load immediate operand into register
push rax             # Push to stack

The operand 42 becomes an immediate value in the generated machine code.

Debugging Operands

You can inspect operands:

# View bytecode with operands
ruby --dump=insns -e 'x = 42'
 
# Output shows:
# putobject 42        ← operand visible
# setlocal_WC_0 x     ← operand visible

Tools like RubyVM::InstructionSequence#disasm reveal operands clearly.