Truffle Node Rewriting: Dynamic AST Optimization in GraalVM

Truffle is a Java framework for building language implementations that can be efficiently executed on the GraalVM. Node rewriting is a fundamental concept in Truffle that enables dynamic optimization of Abstract Syntax Trees (ASTs) during execution.

Understanding Truffle Node Rewriting

Truffle uses node rewriting to specialize ASTs based on runtime information, transforming generic nodes into optimized, specialized versions.

Basic Truffle Node Structure

Example 1: Foundation Node Classes

import com.oracle.truffle.api.nodes.Node;
import com.oracle.truffle.api.nodes.NodeInterface;
import com.oracle.truffle.api.dsl.Specialization;
import com.oracle.truffle.api.dsl.TypeSystem;
import com.oracle.truffle.api.dsl.ImplicitCast;
import com.oracle.truffle.api.CompilerDirectives;
// Type system for our language
@TypeSystem({
int.class,
double.class,
boolean.class
})
class SimpleTypes {
@ImplicitCast
public static double intToDouble(int value) {
return value;
}
}
// Base node class
abstract class ExpressionNode extends Node {
public abstract Object execute();
}
// Generic addition node that will be specialized through rewriting
abstract class AddNode extends ExpressionNode {
@Specialization(rewriteOn = ArithmeticException.class)
protected int addInts(int left, int right) {
return Math.addExact(left, right);
}
@Specialization(replaces = "addInts")
protected double addDoubles(double left, double right) {
return left + right;
}
@Specialization(guards = {"isString(left)", "isString(right)"})
protected String addStrings(Object left, Object right) {
return left.toString() + right.toString();
}
protected static boolean isString(Object value) {
return value instanceof String;
}
}

Manual Node Rewriting Patterns

Example 2: Explicit Node Replacement

import com.oracle.truffle.api.nodes.Node;
import com.oracle.truffle.api.nodes.UnexpectedResultException;
import com.oracle.truffle.api.CompilerDirectives.CompilationFinal;
// Manual node rewriting example
abstract class ManualRewriteNode extends Node {
// Node that starts generic and becomes specialized
static class GenericAddNode extends ExpressionNode {
@Child private ExpressionNode left;
@Child private ExpressionNode right;
// Track if we've specialized
@CompilationFinal private boolean intSpecialized = false;
@CompilationFinal private boolean doubleSpecialized = false;
public GenericAddNode(ExpressionNode left, ExpressionNode right) {
this.left = left;
this.right = right;
}
@Override
public Object execute() {
// Try to specialize to int operations
if (!intSpecialized) {
try {
int leftVal = expectInt(left);
int rightVal = expectInt(right);
// Success! Replace with specialized node
replace(new IntAddNode(left, right));
return leftVal + rightVal;
} catch (UnexpectedResultException e) {
// Not integers, mark as not specializable
intSpecialized = true;
}
}
// Try double specialization
if (!doubleSpecialized) {
try {
double leftVal = expectDouble(left);
double rightVal = expectDouble(right);
replace(new DoubleAddNode(left, right));
return leftVal + rightVal;
} catch (UnexpectedResultException e) {
doubleSpecialized = true;
}
}
// Fallback to generic object addition
Object leftVal = left.execute();
Object rightVal = right.execute();
if (leftVal instanceof String || rightVal instanceof String) {
return leftVal.toString() + rightVal.toString();
}
throw new RuntimeException("Unsupported types for addition");
}
private int expectInt(ExpressionNode node) throws UnexpectedResultException {
Object result = node.execute();
if (result instanceof Integer) {
return (Integer) result;
}
throw new UnexpectedResultException(result);
}
private double expectDouble(ExpressionNode node) throws UnexpectedResultException {
Object result = node.execute();
if (result instanceof Double) {
return (Double) result;
} else if (result instanceof Integer) {
return (Integer) result;
}
throw new UnexpectedResultException(result);
}
}
// Specialized integer addition node
static class IntAddNode extends ExpressionNode {
@Child private ExpressionNode left;
@Child private ExpressionNode right;
public IntAddNode(ExpressionNode left, ExpressionNode right) {
this.left = left;
this.right = right;
}
@Override
public Object execute() {
try {
int leftVal = expectInt(left);
int rightVal = expectInt(right);
return leftVal + rightVal;
} catch (UnexpectedResultException e) {
// De-specialize back to generic node
replace(new GenericAddNode(left, right));
return execute(); // Re-execute with generic node
}
}
private int expectInt(ExpressionNode node) throws UnexpectedResultException {
Object result = node.execute();
if (result instanceof Integer) {
return (Integer) result;
}
throw new UnexpectedResultException(result);
}
}
// Specialized double addition node
static class DoubleAddNode extends ExpressionNode {
@Child private ExpressionNode left;
@Child private ExpressionNode right;
public DoubleAddNode(ExpressionNode left, ExpressionNode right) {
this.left = left;
this.right = right;
}
@Override
public Object execute() {
double leftVal = expectDouble(left);
double rightVal = expectDouble(right);
return leftVal + rightVal;
}
private double expectDouble(ExpressionNode node) {
Object result = node.execute();
if (result instanceof Double) {
return (Double) result;
} else if (result instanceof Integer) {
return (Integer) result;
}
// For doubles, we don't de-specialize, we just convert
if (result instanceof String) {
try {
return Double.parseDouble(result.toString());
} catch (NumberFormatException e) {
return 0.0;
}
}
return 0.0;
}
}
}

DSL-Based Node Rewriting

Example 3: Using Truffle DSL for Automatic Rewriting

import com.oracle.truffle.api.dsl.Fallback;
import com.oracle.truffle.api.dsl.NodeChild;
import com.oracle.truffle.api.dsl.NodeChildren;
import com.oracle.truffle.api.dsl.ReportPolymorphism;
import com.oracle.truffle.api.source.SourceSection;
// DSL-based node with automatic rewriting
@NodeChildren({
@NodeChild("left"),
@NodeChild("right")
})
@ReportPolymorphism
abstract class DSLAddNode extends ExpressionNode {
// Specialized for exact integers
@Specialization(rewriteOn = ArithmeticException.class)
protected int addInts(int left, int right) {
return Math.addExact(left, right);
}
// Fallback for integer overflow - switch to doubles
@Specialization(replaces = "addInts")
protected double addIntsToDouble(int left, int right) {
return (double) left + (double) right;
}
// Double specialization
@Specialization
protected double addDoubles(double left, double right) {
return left + right;
}
// String concatenation
@Specialization(guards = {"isString(left)", "isString(right)"})
protected String addStrings(String left, String right) {
return left + right;
}
// Mixed string and number
@Specialization(guards = "isString(left)")
protected String addStringLeft(String left, Object right) {
return left + right.toString();
}
@Specialization(guards = "isString(right)")
protected String addStringRight(Object left, String right) {
return left.toString() + right;
}
// Fallback for any other types
@Fallback
protected Object addGeneric(Object left, Object right) {
// This is slow path - might trigger deoptimization
CompilerDirectives.transferToInterpreterAndInvalidate();
return left.toString() + right.toString();
}
protected static boolean isString(Object value) {
return value instanceof String;
}
}

Advanced Rewriting Patterns

Example 4: Polymorphic Inline Caches with Rewriting

import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import com.oracle.truffle.api.CompilerDirectives.CompilationFinal;
import java.util.concurrent.atomic.AtomicReference;
// Polymorphic property access with node rewriting
abstract class PropertyAccessNode extends ExpressionNode {
private final String propertyName;
public PropertyAccessNode(String propertyName) {
this.propertyName = propertyName;
}
public abstract Object execute(Object target);
// Monomorphic case - single type observed
static class MonomorphicPropertyNode extends PropertyAccessNode {
@CompilationFinal private Class<?> expectedType;
private final AtomicReference<Object> cachedValue = new AtomicReference<>();
public MonomorphicPropertyNode(String propertyName) {
super(propertyName);
}
@Override
public Object execute(Object target) {
if (expectedType == null) {
// First execution - initialize
CompilerDirectives.transferToInterpreterAndInvalidate();
expectedType = target.getClass();
Object value = getProperty(target);
cachedValue.set(value);
return value;
}
if (expectedType == target.getClass()) {
// Fast path - monomorphic case
Object cached = cachedValue.get();
if (cached != null) {
return cached;
}
return getProperty(target);
} else {
// Polymorphic case - rewrite to polymorphic node
CompilerDirectives.transferToInterpreterAndInvalidate();
PolymorphicPropertyNode newNode = new PolymorphicPropertyNode(propertyName);
newNode.addCache(expectedType, cachedValue.get());
newNode.addCache(target.getClass(), getProperty(target));
replace(newNode);
return newNode.execute(target);
}
}
@TruffleBoundary
private Object getProperty(Object target) {
// Simulate property access
if ("length".equals(propertyName)) {
if (target instanceof String) return ((String) target).length();
if (target instanceof Object[]) return ((Object[]) target).length;
}
return null;
}
}
// Polymorphic case - multiple types observed
static class PolymorphicPropertyNode extends PropertyAccessNode {
@CompilationFinal private Class<?>[] cachedTypes;
@CompilationFinal private Object[] cachedValues;
private static final int MAX_POLYMORPHIC = 3;
public PolymorphicPropertyNode(String propertyName) {
super(propertyName);
this.cachedTypes = new Class<?>[0];
this.cachedValues = new Object[0];
}
void addCache(Class<?> type, Object value) {
CompilerDirectives.transferToInterpreterAndInvalidate();
Class<?>[] newTypes = new Class<?>[cachedTypes.length + 1];
Object[] newValues = new Object[cachedValues.length + 1];
System.arraycopy(cachedTypes, 0, newTypes, 0, cachedTypes.length);
System.arraycopy(cachedValues, 0, newValues, 0, cachedValues.length);
newTypes[cachedTypes.length] = type;
newValues[cachedValues.length] = value;
cachedTypes = newTypes;
cachedValues = newValues;
}
@Override
public Object execute(Object target) {
// Check cached types
for (int i = 0; i < cachedTypes.length; i++) {
if (cachedTypes[i] == target.getClass()) {
return cachedValues[i];
}
}
// Cache miss
Object value = getProperty(target);
if (cachedTypes.length < MAX_POLYMORPHIC) {
// Add to cache
addCache(target.getClass(), value);
} else {
// Too polymorphic - rewrite to megamorphic node
CompilerDirectives.transferToInterpreterAndInvalidate();
MegamorphicPropertyNode newNode = new MegamorphicPropertyNode(propertyName);
replace(newNode);
return newNode.execute(target);
}
return value;
}
@TruffleBoundary
private Object getProperty(Object target) {
if ("length".equals(propertyName)) {
if (target instanceof String) return ((String) target).length();
if (target instanceof Object[]) return ((Object[]) target).length;
}
return null;
}
}
// Megamorphic case - too many types for caching
static class MegamorphicPropertyNode extends PropertyAccessNode {
public MegamorphicPropertyNode(String propertyName) {
super(propertyName);
}
@Override
@TruffleBoundary
public Object execute(Object target) {
// Always use slow path
if ("length".equals(propertyName)) {
if (target instanceof String) return ((String) target).length();
if (target instanceof Object[]) return ((Object[]) target).length;
if (target.getClass().isArray()) return java.lang.reflect.Array.getLength(target);
}
return null;
}
}
}

Control Flow Rewriting

Example 5: Loop and Branch Optimization

import com.oracle.truffle.api.frame.VirtualFrame;
import com.oracle.truffle.api.nodes.LoopNode;
import com.oracle.truffle.api.nodes.RepeatingNode;
import com.oracle.truffle.api.nodes.ControlFlowException;
// Loop optimization with node rewriting
abstract class OptimizedLoopNode extends ExpressionNode {
// Generic loop node that specializes based on loop behavior
static class AdaptiveLoopNode extends ExpressionNode implements RepeatingNode {
@Child private ExpressionNode condition;
@Child private ExpressionNode body;
private final int loopId;
@CompilationFinal private int iterationCount = 0;
@CompilationFinal private boolean isCountedLoop = false;
@CompilationFinal private int expectedIterations = -1;
public AdaptiveLoopNode(int loopId, ExpressionNode condition, ExpressionNode body) {
this.loopId = loopId;
this.condition = condition;
this.body = body;
}
@Override
public Object execute() {
LoopNode loopNode = Truffle.getRuntime().createLoopNode(this);
return loopNode.execute(null);
}
@Override
public boolean executeRepeating(VirtualFrame frame) {
iterationCount++;
// Specialize after observing loop behavior
if (iterationCount == 10 && !isCountedLoop) {
CompilerDirectives.transferToInterpreterAndInvalidate();
// Check if this looks like a counted loop
if (isCountedLoopPattern()) {
replace(new CountedLoopNode(loopId, condition, body, estimateIterations()));
return false; // Let the new node take over
}
}
// Execute condition
Object conditionResult = condition.execute();
if (!(conditionResult instanceof Boolean)) {
throw new RuntimeException("Loop condition must be boolean");
}
if (!(Boolean) conditionResult) {
return false; // Loop exit
}
// Execute body
body.execute();
return true; // Continue loop
}
private boolean isCountedLoopPattern() {
// Analyze if this loop has a predictable pattern
// In real implementation, this would analyze the condition node
return iterationCount > 5 && (iterationCount % 2 == 0); // Simplified
}
private int estimateIterations() {
return iterationCount * 2; // Simplified estimation
}
}
// Specialized counted loop node
static class CountedLoopNode extends ExpressionNode implements RepeatingNode {
@Child private ExpressionNode body;
private final int totalIterations;
private int currentIteration = 0;
public CountedLoopNode(int loopId, ExpressionNode condition, ExpressionNode body, int iterations) {
this.body = body;
this.totalIterations = iterations;
}
@Override
public Object execute() {
LoopNode loopNode = Truffle.getRuntime().createLoopNode(this);
return loopNode.execute(null);
}
@Override
public boolean executeRepeating(VirtualFrame frame) {
if (currentIteration >= totalIterations) {
return false;
}
try {
body.execute();
currentIteration++;
return true;
} catch (BreakException e) {
if (e.getLoopId() == loopId) {
return false;
}
throw e;
} catch (ContinueException e) {
if (e.getLoopId() == loopId) {
currentIteration++;
return true;
}
throw e;
}
}
}
// Control flow exceptions for break/continue
static class BreakException extends ControlFlowException {
private final int loopId;
public BreakException(int loopId) {
this.loopId = loopId;
}
public int getLoopId() {
return loopId;
}
}
static class ContinueException extends ControlFlowException {
private final int loopId;
public ContinueException(int loopId) {
this.loopId = loopId;
}
public int getLoopId() {
return loopId;
}
}
}

State-Based Rewriting

Example 6: Runtime State Specialization

import com.oracle.truffle.api.Truffle;
import com.oracle.truffle.api.frame.FrameDescriptor;
import com.oracle.truffle.api.frame.FrameSlot;
import com.oracle.truffle.api.frame.FrameSlotTypeException;
import com.oracle.truffle.api.frame.VirtualFrame;
// State-based node rewriting based on runtime information
abstract class StateAwareNode extends ExpressionNode {
// Variable access node that specializes based on usage patterns
static class SmartVariableNode extends ExpressionNode {
private final FrameSlot slot;
private final String variableName;
@CompilationFinal private boolean isReadOnly = false;
@CompilationFinal private Class<?> knownType = null;
@CompilationFinal private int readCount = 0;
@CompilationFinal private int writeCount = 0;
public SmartVariableNode(FrameSlot slot, String variableName) {
this.slot = slot;
this.variableName = variableName;
}
@Override
public Object execute(VirtualFrame frame) {
try {
return frame.getObject(slot);
} catch (FrameSlotTypeException e) {
throw new RuntimeException("Variable not found: " + variableName);
}
}
public void executeWrite(VirtualFrame frame, Object value) {
writeCount++;
// Specialize based on write patterns
if (writeCount > 10 && knownType == null && value != null) {
CompilerDirectives.transferToInterpreterAndInvalidate();
knownType = value.getClass();
if (isReadOnly) {
// Become read-only specialized node
replace(new ReadOnlyVariableNode(slot, variableName, knownType));
} else {
// Become type-specialized node
replace(new TypedVariableNode(slot, variableName, knownType));
}
}
frame.setObject(slot, value);
}
public void recordRead() {
readCount++;
if (readCount > 100 && writeCount == 0) {
CompilerDirectives.transferToInterpreterAndInvalidate();
isReadOnly = true;
if (knownType != null) {
replace(new ReadOnlyVariableNode(slot, variableName, knownType));
}
}
}
}
// Specialized node for read-only variables
static class ReadOnlyVariableNode extends ExpressionNode {
private final FrameSlot slot;
private final String variableName;
private final Class<?> type;
@CompilationFinal private Object cachedValue;
@CompilationFinal private boolean hasCachedValue = false;
public ReadOnlyVariableNode(FrameSlot slot, String variableName, Class<?> type) {
this.slot = slot;
this.variableName = variableName;
this.type = type;
}
@Override
public Object execute(VirtualFrame frame) {
if (hasCachedValue) {
return cachedValue;
}
try {
Object value = frame.getObject(slot);
if (value != null && type.isInstance(value)) {
CompilerDirectives.transferToInterpreterAndInvalidate();
cachedValue = value;
hasCachedValue = true;
}
return value;
} catch (FrameSlotTypeException e) {
throw new RuntimeException("Variable not found: " + variableName);
}
}
}
// Specialized node for typed variables
static class TypedVariableNode extends ExpressionNode {
private final FrameSlot slot;
private final String variableName;
private final Class<?> expectedType;
public TypedVariableNode(FrameSlot slot, String variableName, Class<?> expectedType) {
this.slot = slot;
this.variableName = variableName;
this.expectedType = expectedType;
}
@Override
public Object execute(VirtualFrame frame) {
try {
Object value = frame.getObject(slot);
if (value != null && !expectedType.isInstance(value)) {
// Type changed - deoptimize
CompilerDirectives.transferToInterpreterAndInvalidate();
replace(new SmartVariableNode(slot, variableName));
return value;
}
return value;
} catch (FrameSlotTypeException e) {
throw new RuntimeException("Variable not found: " + variableName);
}
}
public void executeWrite(VirtualFrame frame, Object value) {
if (value != null && !expectedType.isInstance(value)) {
// Type change - deoptimize
CompilerDirectives.transferToInterpreterAndInvalidate();
SmartVariableNode newNode = new SmartVariableNode(slot, variableName);
replace(newNode);
newNode.executeWrite(frame, value);
return;
}
frame.setObject(slot, value);
}
}
}

Best Practices for Node Rewriting

Example 7: Production-Grade Rewriting Patterns

import com.oracle.truffle.api.CompilerAsserts;
import com.oracle.truffle.api.CompilerDirectives;
import com.oracle.truffle.api.TruffleLanguage;
import com.oracle.truffle.api.instrumentation.GenerateWrapper;
import com.oracle.truffle.api.instrumentation.InstrumentableNode;
import com.oracle.truffle.api.instrumentation.ProbeNode;
// Professional node rewriting with instrumentation support
public class ProductionReadyRewriting {
@GenerateWrapper
public static class ProductionAddNode extends ExpressionNode implements InstrumentableNode {
@Child private ExpressionNode left;
@Child private ExpressionNode right;
@CompilationFinal private boolean intSpecialized = false;
@CompilationFinal private boolean stringSpecialized = false;
public ProductionAddNode(ExpressionNode left, ExpressionNode right) {
this.left = left;
this.right = right;
}
@Override
public Object execute() {
CompilerAsserts.partialEvaluationConstant(this);
// Try integer specialization first
if (!intSpecialized) {
try {
int leftVal = expectInteger(left);
int rightVal = expectInteger(right);
// Check for overflow
long result = (long) leftVal + (long) rightVal;
if (result == (int) result) {
// No overflow - stay with integers
return (int) result;
} else {
// Overflow - specialize to doubles
CompilerDirectives.transferToInterpreterAndInvalidate();
replace(new DoubleAddNode(left, right));
return (double) result;
}
} catch (UnexpectedResultException e) {
CompilerDirectives.transferToInterpreterAndInvalidate();
intSpecialized = true;
}
}
// Try string specialization
if (!stringSpecialized) {
Object leftVal = left.execute();
Object rightVal = right.execute();
if (leftVal instanceof String || rightVal instanceof String) {
CompilerDirectives.transferToInterpreterAndInvalidate();
StringAddNode stringNode = new StringAddNode(left, right);
replace(stringNode);
return stringNode.execute();
} else {
CompilerDirectives.transferToInterpreterAndInvalidate();
stringSpecialized = true;
}
}
// Generic fallback
Object leftVal = left.execute();
Object rightVal = right.execute();
if (leftVal instanceof Number && rightVal instanceof Number) {
return ((Number) leftVal).doubleValue() + ((Number) rightVal).doubleValue();
}
return leftVal.toString() + rightVal.toString();
}
@Override
public boolean isInstrumentable() {
return true;
}
@Override
public WrapperNode createWrapper(ProbeNode probe) {
return new ProductionAddNodeWrapper(this, probe);
}
private int expectInteger(ExpressionNode node) throws UnexpectedResultException {
Object result = node.execute();
if (result instanceof Integer) {
return (Integer) result;
}
throw new UnexpectedResultException(result);
}
}
// Specialized nodes with instrumentation support
@GenerateWrapper
public static class DoubleAddNode extends ExpressionNode implements InstrumentableNode {
@Child private ExpressionNode left;
@Child private ExpressionNode right;
public DoubleAddNode(ExpressionNode left, ExpressionNode right) {
this.left = left;
this.right = right;
}
@Override
public Object execute() {
double leftVal = expectDouble(left);
double rightVal = expectDouble(right);
return leftVal + rightVal;
}
@Override
public boolean isInstrumentable() {
return true;
}
@Override
public WrapperNode createWrapper(ProbeNode probe) {
return new DoubleAddNodeWrapper(this, probe);
}
private double expectDouble(ExpressionNode node) {
Object result = node.execute();
if (result instanceof Double) {
return (Double) result;
} else if (result instanceof Integer) {
return (Integer) result;
} else if (result instanceof String) {
try {
return Double.parseDouble(result.toString());
} catch (NumberFormatException e) {
return 0.0;
}
}
return 0.0;
}
}
@GenerateWrapper
public static class StringAddNode extends ExpressionNode implements InstrumentableNode {
@Child private ExpressionNode left;
@Child private ExpressionNode right;
public StringAddNode(ExpressionNode left, ExpressionNode right) {
this.left = left;
this.right = right;
}
@Override
public Object execute() {
Object leftVal = left.execute();
Object rightVal = right.execute();
return leftVal.toString() + rightVal.toString();
}
@Override
public boolean isInstrumentable() {
return true;
}
@Override
public WrapperNode createWrapper(ProbeNode probe) {
return new StringAddNodeWrapper(this, probe);
}
}
}
// Exception for type expectations
class UnexpectedResultException extends Exception {
private final Object result;
public UnexpectedResultException(Object result) {
super("Unexpected result type: " + (result != null ? result.getClass().getSimpleName() : "null"));
this.result = result;
}
public Object getResult() {
return result;
}
}

Key Concepts in Truffle Node Rewriting

1. Specialization

  • Transform generic nodes to type-specific versions
  • Use runtime profiling information
  • Handle deoptimization gracefully

2. Polymorphism Management

  • Monomorphic (1 type)
  • Polymorphic (2-3 types)
  • Megamorphic (many types)

3. Compilation Directives

  • @CompilationFinal for stable state
  • transferToInterpreterAndInvalidate() for deoptimization
  • TruffleBoundary for slow paths

4. DSL vs Manual Rewriting

  • DSL: Less boilerplate, automatic optimization
  • Manual: Full control, complex patterns

Performance Considerations

  1. Rewrite Cost: Balance specialization benefits vs rewrite overhead
  2. Memory Footprint: Specialized nodes may use more memory
  3. Warm-up Time: Allow sufficient execution before specializing
  4. Deoptimization: Plan for specialization failures

Conclusion

Truffle node rewriting enables highly optimized language implementations by:

  • Dynamic Specialization: Adapt AST based on runtime types
  • Progressive Optimization: Start simple, become specialized
  • Graceful Degradation: Handle corner cases without crashing
  • Instrumentation Support: Maintain debuggability during optimization

Key patterns include:

  • Type-based specialization
  • Polymorphic inline caches
  • Loop and control flow optimization
  • State-aware node transformation

By mastering these techniques, you can build language implementations that approach or even exceed the performance of statically compiled languages while maintaining dynamic language flexibility.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper