A Different View of Your Code
Most developers know that compilers initially parse source code into an abstract syntax tree (AST). What’s less widely known is that the AST is only a starting point. A serious compiler typically discards it early and switches to a more sophisticated intermediate representation for the heavy lifting of optimization and code generation. In TruffleRuby—the just-in-time compiler Shopify builds for Ruby—that representation is called a sea-of-nodes graph.
Sea-of-nodes is both a practical tool and a revealing lens on what programs actually mean. It’s also, to my eyes at least, quite beautiful. Before diving in, a note on the examples: they’re written in Java, not Ruby. Ruby’s semantics are rich—array indexing alone can involve positive and negative indices, ranges, coercion, and more—which makes Ruby graphs considerably more complex. Java has simpler rules, so its graphs are easier to read. The code used here is basic enough that you can treat it as pseudocode.
Reading the Graph
Consider a straightforward recursive Java method that returns a number from the Fibonacci sequence. Its AST is a tree that maps one-to-one onto the source text, adding or removing nothing. Executing it means walking that tree depth-first from the root.
The corresponding sea-of-nodes graph for the same method looks quite different: it’s a web of boxes and arrows. Boxes are operations; arrows are connections. An operation runs only after everything pointing into it has run.
Two kinds of arrows matter most. Thick red arrows show control flow—the imperative order of execution. Thin green arrows show data flow. (Some renderings point data arrows upward; on our team we prefer data flowing down.) Dashed black arrows carry meta-information. The boxes come in two main flavors as well: square red ones perform side-effecting, imperative work, while diamond green ones compute pure, side-effect-free values that are safe to evaluate anytime. Labels like P(0) mean parameter zero, and C(2) is a constant 2. Each node carries a number for reference.
To trace the program by hand, start at the Start node and follow the thick red arrows down toward a Return. When a square red box has an arrow coming from a green box, run that green computation (plus anything feeding it) first.
The core strength of this representation is visible immediately: the red portions form an imperative skeleton, while the green portions are small functional programs. The compiler has separated the two concerns from the original single source file, joining them only where the language semantics require it.
What the Graph Reveals
Source code is linear text, which implies ordering and structure that aren’t truly there. A graph encodes only the precise rules of the language and relaxes everything else.
A concrete example: a three-way if statement that computes b * c in two of its three branches. The source suggests you pick a branch first, then possibly perform the multiplication. The graph shows otherwise. There is exactly one multiplication node, not two: the same computation appearing twice in text is de-duplicated through global value numbering. Moreover, that single multiplication isn’t tied to either branch, because it’s a pure functional operation. It can execute before the branch is resolved, or only on the paths that need it—whichever produces better machine code.
This view also explains why manually hoisting a common subexpression into a local variable changes nothing for the compiler. It may improve readability, but the compiler already sees through variable names and floats the expression to wherever it makes sense.
Loops and the Phi Node
Loops add a distinctive element to the graph: a thick red arrow pointing backward, closing the iteration. For a method that adds parameter a into an accumulator n times, the isolated functional part shows a self-referential loop—a + 1 node repeatedly consuming its own output.
Loops also introduce the phi node, drawn as a small circle. Despite its intimidating name, the concept is straightforward: a phi node represents a value that could be one of several possibilities depending on which path through the loop was taken.
Why Not Code in Graphs?
Every few years a thesis argues for replacing textual programming with visual graphs. The sea-of-nodes view shows both the appeal and the obstacle.
The appeal is freedom: with the graph, you can reshape and re-optimize almost arbitrarily, as long as you respect the language’s rules. The obstacle is scale. A six-line method becomes a full-screen diagram with 21 nodes and 22 arrows. Larger programs quickly produce graphs where arrows cross, stretch out of context, and become unusable as a human-facing format.
Sea-of-Nodes at Shopify
Idiomatic Ruby code, like the kind in Shopify’s Storefront Renderer, produces very large and dense graphs—the Ruby version of the Fibonacci example is already far more complex than its Java counterpart.
To make sense of this complexity, we’re building tools around the graph dumps the compiler produces. One is a visualizer that renders compiler debug output into diagrams like those shown here. Another is a decompiler that works backward: it turns optimized graphs back into Ruby source, so developers who know only Ruby can inspect exactly how their code was transformed. Both tools help us understand and improve the optimizations TruffleRuby applies in production.
Summary
Sea-of-nodes graphs are an intermediate representation that preserves the connections that matter in a program while discarding the artifacts of linear text. They encode imperative control flow alongside pure functional computation, enabling optimizations like global value numbering and free-floating expressions. Textual source code makes a program look like a sequence of steps; the compiler sees through to something simpler and more malleable, and in TruffleRuby that happens through sea-of-nodes.
Further Reading
- A simple graph-based intermediate representation, Cliff Click, Michael Paleczny, 1995
- An intermediate representation for speculative optimizations in a dynamic compiler, Gilles Duboscq, Thomas Würthinger, Lukas Stadler, Christian Wimmer, Doug Simon, Hanspeter Mössenböck, 2013
Why Graphs Matter in Program Analysis
Graphs are one of the most powerful tools for understanding what a program really does. While source code expresses intent in a linear, textual form, the actual computation involves data flowing through operations in ways that are often obscured by syntax. Representing a program as a graph makes these relationships explicit and analyzable.
In the context of TruffleRuby, the high-performance Ruby implementation built on GraalVM, program analysis centers on a specific kind of graph known as the "sea-of-nodes." This representation goes beyond a simple abstract syntax tree (AST) or a linear control-flow graph by combining both data dependencies and control dependencies into a single unified structure.
The Sea-of-Nodes Structure
Unlike traditional compiler intermediate representations that separate the flow of values from the flow of control, the sea-of-nodes merges them. Each node in the graph represents an operation, and edges between nodes indicate either a data dependency (one operation produces a value consumed by another) or a control dependency (one operation must complete before another starts).
This design allows the graph to be highly flexible. Because data and control edges are treated uniformly, the optimizer can move operations around freely as long as their dependencies are respected. This flexibility is what enables aggressive optimizations that would be difficult or impossible to express in a more rigid, linear representation.
Reading the Graph to Find Insights
The real payoff comes when you use this graph to inspect a running program. TruffleRuby's tools allow developers to dump the sea-of-nodes graph for a specific method at various stages of compilation. The resulting visualization can reveal surprising details about how the compiler understands the code.
Consider something as simple as a loop summing integers. When you look at the graph generated during compilation, one thing the optimizer has already discovered becomes immediately visible: the loop variable never escapes the loop, and thus can be held in an unboxed, primitive integer. This removes a significant amount of overhead compared to the boxed Integer objects Ruby normally uses.
More tellingly, the graph can show when the compiler has been able to prove that certain operations are redundant. For example, in code that calls a method repeatedly with immutable results, the optimizer might be able to remove redundant reads or computations—facts that are often hard to spot in the source code but become patently obvious when viewing the graph's shape.
This introspective ability turns the compiler into a diagnostic tool. When performance is not what you expect, inspecting the intermediate graph helps answer questions like: Is the compiler able to infer a static type here? Did it successfully inline that call? Is there an unexpected branch preventing a clean straight-line execution? These insights are essential for understanding the discrepancy between how a developer reads the code and how the machine actually executes it.
More Than Just Optimization
The sea-of-nodes philosophy also underpins the way TruffleRuby handles the dynamic nature of Ruby. Because Ruby allows methods to be redefined at runtime, the graph must be capable of handling change. The compiler constructs the graph based on assumptions about the current state of the program, such as the type of a variable or the implementation of a method.
When these assumptions hold, the optimized code runs at speeds that rival statically-typed languages. When an assumption is broken—say, by a define_method call that changes an existing method—the graph includes guards that detect the change. This triggers a deoptimization, smoothly falling back to a slower, more generic version of the code that still honors the new semantics. The graph structure elegantly supports both this speculative optimization and the fallback safety mechanism.
Putting the Graph to Work
This level of understanding is not just an academic exercise. It directly enables the work of the Shopify Developer Acceleration team in making Ruby faster. By revealing the true shape of a computation, the sea-of-nodes graph allows engineers to harness the power of the GraalVM runtime to deliver significant performance improvements for production applications.



