70. GraalJS and Truffle
What if you never wrote a JavaScript compiler at all? Truffle lets you write a tree-walking interpreter whose nodes specialize themselves, and then derives the compiled code from that interpreter. GraalJS is a whole JavaScript engine built this way.
- Explain how a self-optimizing AST node specializes itself and generalizes when a guess fails.
- Partially evaluate a small interpreter by hand, step by step, down to its residual program.
- Say what Truffle treats as constant during compilation, and what happens when such a value changes.
- Compare Truffle’s approach with V8’s separate interpreter and compilers.
The question your tree-walker keeps asking
Your Part III interpreter evaluates a + b by asking the same questions
every single time: what kind of node is this? Are both values numbers?
Strings? In a loop that runs a million times, the answers never change,
and you pay for them a million times. That is the interpreter overhead
you measured in chapter 44.
V8’s answer is separate machinery: bytecode for Ignition, then compilers that rewrite hot code using what the interpreter observed[9]. Truffle, from Oracle and the Johannes Kepler University Linz, takes another road. You write an interpreter for “self-modifying Abstract Syntax Trees”, and the Graal compiler derives fast machine code from that interpreter itself[1]. GraalJS, “a JavaScript engine implemented in Java on top of GraalVM”, is built this way[5].
Two ideas make it work: nodes that specialize themselves, and partial evaluation.
Nodes that rewrite themselves
A post-room clerk notices that every letter for a month has been local. She puts up a “local only” tray and stops checking addresses. The day a foreign letter turns up, she takes the tray down for good and goes back to checking, a little slower but always right.
A + node starts with no idea what it will see. After the first few
executions it narrows itself to the case it saw, and it widens again
when something else turns up:
This is GraalJS’s real +, trimmed. Each @Specialization is one
case. The integer case uses Math.addExact, which throws on
overflow:
// JSAddNormalNode.java (GraalJS)
@Specialization(rewriteOn =
ArithmeticException.class)
static int doInt(int a, int b) {
return Math.addExact(a, b);
}
// JSAddNode.java (GraalJS)
@Specialization
static double doDouble(double a,
double b) {
return a + b;
}
rewriteOn means: if this exception is thrown even once, this node
never uses that case again, and the next one runs
instead[4]. Further down, a generic case that first
converts its operands replaces all the specific ones[5].
At the lowest level, any node can swap itself for another in the tree
with replace, which also invalidates compiled code that relied on the
old node[3].
A self-optimizing AST interpreter is a tree-walking interpreter whose nodes record what they have seen and rewrite themselves (or swap in a replacement node) into a version specialized for those cases, with a check that falls back and generalizes when a case fails. The GraalVM team traces the design to a 2012 paper on self-optimizing, self-specializing interpreters applied to JavaScript[6][7].
Partial evaluation
A recipe says “if a guest is vegetarian, use beans, otherwise use beef”. Once you know tonight’s guests, you can cross out the branch that can’t happen. You are left with a shorter recipe that only works for tonight, and needs no more thinking.
Truffle’s documentation starts with exactly this kind of example[2], here in JavaScript:
function round(v, up) {
return up ? Math.ceil(v)
: Math.floor(v);
}
// specialized for up = true:
function roundUp(v) {
return Math.ceil(v);
}
Now apply the same trick to an interpreter. Its two inputs are the program and the program’s data. The program doesn’t change while it runs, so fix it and simplify:
function interpret(prog, args) {return prog.execute(args);}class Add {execute(args) {return this.l.execute(args) +this.r.execute(args);}}class Arg {execute(args) {return args[this.i];}}
Partial evaluation specializes a program for the inputs that are
known (static), producing a residual program that only takes the
rest (dynamic): Input -> Output becomes DynamicInput -> Output.
Partially evaluating an interpreter with respect to one guest program
gives code specialized to that program, with the interpretation
removed. This is the first Futamura projection[2].
You can do a version of this by hand in JavaScript. Instead of a
switch that runs on every evaluation, walk the tree once and build
closures. The kind of each node is decided while building, not while
running:
// a tiny AST: (args[0] + 2) * args[1]
const t = { op: '*',
l: { op: '+',
l: { op: 'arg', i: 0 },
r: { op: 'num', v: 2 } },
r: { op: 'arg', i: 1 } };
// the interpreter: decides every time
function run(n, args) {
switch (n.op) {
case 'num': return n.v;
case 'arg': return args[n.i];
case '+': return run(n.l, args) +
run(n.r, args);
case '*': return run(n.l, args) *
run(n.r, args);
}
}
// specialize: decide once, return code
function spec(n) {
switch (n.op) {
case 'num': {
const v = n.v;
return () => v;
}
case 'arg': {
const i = n.i;
return (args) => args[i];
}
case '+': {
const l = spec(n.l);
const r = spec(n.r);
return (a) => l(a) + r(a);
}
case '*': {
const l = spec(n.l);
const r = spec(n.r);
return (a) => l(a) * r(a);
}
}
}
const f = spec(t); // the tree is gone
console.log(run(t, [1, 9]), f([1, 9]));
console.log(run(t, [5, 3]), f([5, 3]));How does Truffle know what may be treated as constant? It starts from
the root node of the code being compiled and static final fields,
then follows stable fields: final fields and declared @Child
nodes. Fields marked @CompilationFinal can still change, so a node
can record what it has seen, but once compilation starts they are
folded like constants. Truffle counts calls and loop iterations, and
compiles a root node (roughly, a function) once it is hot. If a recorded fact later turns out
wrong, the node calls transferToInterpreterAndInvalidate: the
compiled code is thrown away and execution continues in the
interpreter, which re-specializes and may be compiled
again[2].
Truffle’s documentation names the problem it avoids: a classic engine needs “semantics, profiling, and optimization knowledge to be represented in both interpreter and compiler”. With Truffle, the optimized code is derived from the interpreter. The price is that the interpreter must be written carefully, so that partial evaluation can see what is stable, and every speculation must eventually settle, or the code loops through compiling and invalidating[2]. The GraalVM team describes the whole stack, interpreter and dynamic compiler, in “One VM to Rule Them All”[8]. It is also where the columnar-array research of the next chapter was built[6].
Your turn
Write AddNode, a self-specializing + node. Its children l and r have an execute(env) method. state starts as 'uninit'. In execute(env), evaluate both children, then: in 'uninit' or 'int', if both values and their sum are int32 (isInt), set 'int' and return the sum. Otherwise, unless the state is 'generic', if both are numbers set 'double'. Otherwise set 'generic'. Always return a + b. A node must never move back to a more specific state.
const Lit = (v) =>
({ execute: () => v });
const Var = (k) =>
({ execute: (env) => env[k] });
const isInt = (x) => (x | 0) === x;
const isNum = (x) =>
typeof x === 'number';
class AddNode {
constructor(l, r) {
this.l = l;
this.r = r;
this.state = 'uninit';
}
execute(env) {
const a = this.l.execute(env);
const b = this.r.execute(env);
return a + b;
}
}Write pe(n), which turns a tree of { op: 'num', v }, { op: 'arg', i }, { op: '+', l, r } and { op: '*', l, r } into a JavaScript expression string over an array args. A num becomes its digits, an arg becomes args[i], and an operator becomes (L op R). But if both sides came out as numbers, fold them: return the computed number as a string instead.
function pe(n) {
return '0';
}What you learned
- A self-optimizing AST node records the types it sees, rewrites
itself into a specialized version, and generalizes, one way only,
when a guard fails. GraalJS’s
+has int, double, string and generic cases. - Partial evaluation fixes the known inputs and simplifies. Applied to an interpreter and one program, it inlines and folds the interpreter away, leaving a residual program: the first Futamura projection.
- Truffle treats the root node,
finalfields,@Childnodes and@CompilationFinalfields as constants, and invalidates compiled code when one of them must change. - The engine’s semantics live in one place, the interpreter.
Check your understanding
0/6- 1.
What does a Truffle language implementer mainly write?
- 2.
A
+node in the int state receives2 ** 31 - 1and1. What should it do? - 3.
Partially evaluating
round(v, up)withup = trueknown gives: - 4.
In the step-through, why can the specializer inline
prog.l.execute? - 5.
A
@CompilationFinalfield that compiled code depends on must change. What does Truffle require? - 6.
What is the main trade-off of Truffle’s approach?
References
- [1] Oracle and the GraalVM contributors (2026). Truffle Language Implementation Framework (truffle/README.md and truffle/docs/README.md). GraalVM source repository (github.com/oracle/graal), master branch. github.com/oracle/graal/tree/master/truffle (checked 2026-10-31)
- [2] Oracle and the GraalVM contributors (2026). Partial Evaluation in Truffle (truffle/docs/PartialEvaluation.md). GraalVM source repository (github.com/oracle/graal), master branch. github.com/oracle/graal/blob/master/truffle/docs/PartialEvaluation.md (checked 2026-10-31)
- [3] Oracle and the GraalVM contributors (2026). com.oracle.truffle.api.nodes.Node (Node.java): replace(Node). GraalVM source repository (github.com/oracle/graal), master branch. github.com/oracle/graal/blob/master/truffle/src/com.oracle.truffle.api/src/com/oracle/truffle/api/nodes/Node.java (checked 2026-10-31)
- [4] Oracle and the GraalVM contributors (2026). com.oracle.truffle.api.dsl.Specialization (Specialization.java): rewriteOn and replaces. GraalVM source repository (github.com/oracle/graal), master branch. github.com/oracle/graal/blob/master/truffle/src/com.oracle.truffle.api.dsl/src/com/oracle/truffle/api/dsl/Specialization.java (checked 2026-10-31)
- [5] Oracle and the GraalJS contributors (2026). GraalJS: JSAddNode.java and JSAddNormalNode.java (the JavaScript + operator). GraalJS source repository (github.com/oracle/graaljs), master branch. github.com/oracle/graaljs/blob/master/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/nodes/binary/JSAddNode.java (checked 2026-10-31)
- [6] Oracle and the GraalVM contributors (2026). GraalVM Academic Publications (docs/Publications.md). GraalVM source repository (github.com/oracle/graal), master branch. github.com/oracle/graal/blob/master/docs/Publications.md (checked 2026-10-31)
- [7] Thomas Würthinger, Andreas Wöß, Lukas Stadler, Gilles Duboscq, Doug Simon, Christian Wimmer (2012). Self-Optimizing AST Interpreters. Proceedings of the 8th Symposium on Dynamic Languages (DLS ’12), ACM. doi:10.1145/2384577.2384587 (checked 2026-10-31)
- [8] Thomas Würthinger, Christian Wimmer, Andreas Wöß, Lukas Stadler, Gilles Duboscq, Christian Humer, Gregor Richards, Doug Simon, Mario Wolczko (2013). One VM to Rule Them All. Onward! 2013 (SPLASH 2013), ACM, pp. 187–204. doi:10.1145/2509578.2509581 (checked 2026-10-31)
- [9] Toon Verwaest, Leszek Swirski, Victor Gomes, Olivier Flückiger, Darius Mercadier, Camillo Bruni (2023). Maglev - V8’s Fastest Optimizing JIT. V8 blog (v8.dev), 5 December 2023. v8.dev/blog/maglev (checked 2026-10-31)