<- Back to README | Visitors | SSA Transforms -> | Migration Guide
Static Single Assignment (SSA) form is an intermediate representation where each variable is assigned exactly once. YABR's SSA system enables powerful analysis and optimization of Java bytecode.
In SSA form:
- Each variable is defined exactly once
- Phi functions merge values at control flow join points
- Makes dataflow analysis and optimization simpler
Before SSA (stack-based bytecode):
iload 0
iconst 1
iadd
istore 0 // x = x + 1
iload 0
iconst 2
iadd
istore 0 // x = x + 2
After SSA:
v1 = load local[0]
v2 = const 1
v3 = ADD v1, v2
v4 = const 2
v5 = ADD v3, v4
store local[0] = v5
Bytecode --[BytecodeLifter]--> SSA IR --[Transforms]--> Optimized IR --[BytecodeLowerer]--> Bytecode
The pipeline has three stages:
- Lift - Convert stack-based bytecode to register-based SSA IR
- Transform - Apply optimizations
- Lower - Convert SSA IR back to bytecode
import com.tonic.analysis.ssa.SSA;
import com.tonic.analysis.ssa.cfg.IRMethod;
import com.tonic.parser.ConstPool;
import com.tonic.parser.MethodEntry;
ConstPool cp = classFile.getConstPool();
SSA ssa = new SSA(cp);
// Just lift (no transforms)
IRMethod irMethod = ssa.lift(methodEntry);
// Lift and optimize
IRMethod optimized = ssa.liftAndOptimize(methodEntry);
// Complete transform: lift -> optimize -> lower
ssa.transform(methodEntry);SSA ssa = new SSA(cp)
.withConstantFolding()
.withCopyPropagation()
.withDeadCodeElimination();
// Or use the standard set
SSA ssa = new SSA(cp).withStandardOptimizations();// 1. Lift to SSA
IRMethod irMethod = ssa.lift(methodEntry);
// 2. Run transforms manually
ssa.runTransforms(irMethod);
// 3. Lower back to bytecode
ssa.lower(irMethod, methodEntry);For common workflows, SSA provides convenience methods that combine multiple steps:
// optimizeAndLower: Run transforms on existing IR and lower to bytecode
// Useful when you've already lifted and want to optimize before lowering
IRMethod ir = ssa.lift(methodEntry);
// ... inspect or modify IR ...
ssa.optimizeAndLower(ir, methodEntry); // Runs transforms, then lowersTo transform an entire class file including cross-method optimizations:
SSA ssa = new SSA(cp)
.withMethodInlining() // Class-level: inline small methods
.withDeadMethodElimination() // Class-level: remove unused private methods
.withAllOptimizations(); // Method-level: all standard optimizations
// Transform entire class: runs class transforms first, then each method
ssa.transformClass(classFile);
// Rebuild the class file after transformation
classFile.computeFrames();
classFile.rebuild();The transformClass() method:
- Runs all registered class-level transforms (e.g., method inlining)
- For each regular method with code (skips constructors and static initializers):
- Lifts to SSA
- Runs method-level transforms
- Lowers back to bytecode
Represents a method in SSA form:
IRMethod ir = ssa.lift(methodEntry);
// Get blocks
IRBlock entry = ir.getEntryBlock();
List<IRBlock> blocks = ir.getBlocksInOrder();
int blockCount = ir.getBlocks().size();
// Method info
String name = ir.getName();
String descriptor = ir.getDescriptor();A basic block containing phi instructions and regular instructions:
IRBlock block = ir.getEntryBlock();
// Block properties
String name = block.getName(); // "block_0"
List<IRBlock> preds = block.getPredecessors();
List<IRBlock> succs = block.getSuccessors();
// Instructions
List<PhiInstruction> phis = block.getPhiInstructions();
List<IRInstruction> instrs = block.getInstructions();
// Add instructions
block.addPhi(phiInstruction);
block.addInstruction(instruction);Values in SSA form:
// Get result of an instruction
SSAValue result = instruction.getResult();
// Value properties
String name = result.getName(); // "v3"
SSAType type = result.getType(); // INT, LONG, OBJECT, etc.YABR defines IR instruction types organized into functional categories. Recent refactoring consolidated symmetric instruction pairs (e.g., GetField/PutField) into unified classes with mode enums. See SSA IR Migration Guide for migration details.
// Constant value
ConstantInstruction ci; // v1 = const 42
// Load from local variable
LoadLocalInstruction load; // v2 = load local[0]
// Store to local variable
StoreLocalInstruction store; // store local[1] = v3// Binary operations
BinaryOpInstruction bin; // v3 = ADD v1, v2
// Ops: ADD, SUB, MUL, DIV, REM, AND, OR, XOR, SHL, SHR, USHR
// Unary operations
UnaryOpInstruction un; // v3 = NEG v1
// Ops: NEG, I2L, I2F, I2D, L2I, etc.// Conditional branch
BranchInstruction br; // if v1 EQ v2 goto block_1 else block_2
// Unconditional jump (use SimpleInstruction)
SimpleInstruction gt = SimpleInstruction.createGoto(targetBlock);
// Check: gt.getOp() == SimpleOp.GOTO
// Return
ReturnInstruction ret; // return v5
// Switch
SwitchInstruction sw; // switch v1 [3 cases]// FieldAccessInstruction combines field reads and writes with AccessMode enum
FieldAccessInstruction fieldAccess;
// Check mode
fieldAccess.isLoad() // true for field reads
fieldAccess.isStore() // true for field writes
// Factory methods:
FieldAccessInstruction.createLoad(result, owner, name, desc, objectRef)
FieldAccessInstruction.createStaticLoad(result, owner, name, desc)
FieldAccessInstruction.createStore(owner, name, desc, objectRef, value)
FieldAccessInstruction.createStaticStore(owner, name, desc, value)InvokeInstruction inv; // v4 = invoke VIRTUAL MyClass.method(2 args)
// Types: VIRTUAL, STATIC, SPECIAL, INTERFACE, DYNAMICNewInstruction ni; // v1 = new MyClass
NewArrayInstruction na; // v2 = newarray int
// ArrayAccessInstruction combines array reads and writes
ArrayAccessInstruction arrayAccess;
// Check mode: arrayAccess.isLoad() or arrayAccess.isStore()
// Factory methods:
ArrayAccessInstruction.createLoad(result, array, index)
ArrayAccessInstruction.createStore(array, index, value)
// TypeCheckInstruction combines cast and instanceof operations
TypeCheckInstruction typeCheck;
// Check mode: typeCheck.isCast() or typeCheck.isInstanceOf()
// Factory methods:
TypeCheckInstruction.createCast(result, operand, targetType)
TypeCheckInstruction.createInstanceOf(result, operand, checkType)
// SimpleInstruction for array length and other simple operations
SimpleInstruction simple; // For ARRAYLENGTH, MONITORENTER, MONITOREXIT, ATHROW, GOTO
// Check op: simple.getOp() == SimpleOp.ARRAYLENGTH
// Factory: SimpleInstruction.createArrayLength(result, array)// Use SimpleInstruction for monitor operations
SimpleInstruction monEnter = SimpleInstruction.createMonitorEnter(objectRef);
SimpleInstruction monExit = SimpleInstruction.createMonitorExit(objectRef);
// Check: simple.getOp() == SimpleOp.MONITORENTER / MONITOREXIT// Use SimpleInstruction for throw
SimpleInstruction throwInstr = SimpleInstruction.createThrow(exception);
// Check: throwInstr.getOp() == SimpleOp.ATHROWPhiInstruction phi; // v3 = phi(block_0:v1, block_1:v2)
// Get incoming values
Map<IRBlock, SSAValue> incoming = phi.getIncomingValues();
// Add incoming value
phi.addIncoming(predecessorBlock, value);Format IR for debugging:
import com.tonic.analysis.ssa.IRPrinter;
// Format single instruction
System.out.println(IRPrinter.format(instruction));
// Output: v3 = ADD v1, v2
// Format block header
System.out.println(IRPrinter.formatBlockHeader(block));
// Output:
// Block: block_0
// Predecessors: []
// Successors: [block_1, block_2]
// Format entire method
System.out.println(IRPrinter.format(irMethod));import com.tonic.analysis.ssa.SSA;
import com.tonic.analysis.ssa.IRPrinter;
import com.tonic.analysis.ssa.cfg.*;
import com.tonic.analysis.ssa.ir.*;
public void analyzeMethod(MethodEntry method) {
ConstPool cp = method.getClassFile().getConstPool();
SSA ssa = new SSA(cp);
IRMethod ir = ssa.lift(method);
System.out.println("=== " + ir.getName() + ir.getDescriptor() + " ===");
for (IRBlock block : ir.getBlocksInOrder()) {
System.out.println("\n" + block.getName() + ":");
System.out.println(" preds: " + block.getPredecessors().stream()
.map(IRBlock::getName).toList());
// Print phi instructions
for (PhiInstruction phi : block.getPhiInstructions()) {
System.out.println(" [PHI] " + IRPrinter.format(phi));
}
// Print regular instructions
for (IRInstruction instr : block.getInstructions()) {
System.out.println(" " + IRPrinter.format(instr));
// Count instruction types
if (instr instanceof InvokeInstruction) {
System.out.println(" ^ method call");
} else if (instr instanceof BranchInstruction) {
System.out.println(" ^ conditional");
}
}
}
}public void optimizeMethod(MethodEntry method) {
ConstPool cp = method.getClassFile().getConstPool();
// Configure optimizations
SSA ssa = new SSA(cp)
.withConstantFolding() // 2 + 3 -> 5
.withCopyPropagation() // x = y; use(x) -> use(y)
.withDeadCodeElimination(); // Remove unused code
// Lift to SSA
IRMethod ir = ssa.lift(method);
System.out.println("Before optimization:\n" + IRPrinter.format(ir));
// Run transforms
ssa.runTransforms(ir);
System.out.println("After optimization:\n" + IRPrinter.format(ir));
// Lower back to bytecode
ssa.lower(ir, method);
}import com.tonic.analysis.visitor.AbstractBlockVisitor;
public class IRAnalyzer extends AbstractBlockVisitor {
private int invokeCount = 0;
private int branchCount = 0;
@Override
public void visitInstruction(IRInstruction instruction) {
if (instruction instanceof InvokeInstruction) {
invokeCount++;
} else if (instruction instanceof BranchInstruction) {
branchCount++;
}
}
public void printStats() {
System.out.println("Method calls: " + invokeCount);
System.out.println("Branches: " + branchCount);
}
}
// Usage
IRAnalyzer analyzer = new IRAnalyzer();
analyzer.process(methodEntry);
analyzer.printStats();- SSA Transforms - Optimization passes
- Analysis APIs - Call graph, dependency analysis, type inference, pattern search
- SSA IR Migration Guide - API changes from the SSA IR redesign
<- Back to README | Visitors | SSA Transforms -> | Migration Guide