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 operandThe 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 resultThe 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 422. Variable References:
# Ruby:
y = x
# YARV:
getlocal_WC_0 x # Operand: local variable index/name
setlocal_WC_0 y # Operand: local variable index/name3. 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 metadataThe 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 2Operand Optimization
YARV optimizes operands:
Specialized Instructions:
# General form:
putobject 0 # Generic instruction + operand
# Optimized form:
putobject_INT2FIX_0 # Specialized instruction, no operand neededSpecialized 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 operandThe 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
endThe 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 stackThe 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 visibleTools like RubyVM::InstructionSequence#disasm reveal operands clearly.