To witness TurboFan build its speculative Sea of Nodes is to watch a map form from raw heat — from the shapeless warmth of type feedback into a crystalline structure of assumptions, ready to shatter gracefully. I traced this node-by-node construction, following a hot function from the quiet murmur of Ignition’s profiling into the blazing speed of optimized code, and what I found was a dance of insertion, merging, and bailout so precise it felt inevitable.
Ignition collects feedback as a side effect of interpretation. Every bytecode that does addition, property access, or function calls stashes a tiny observation: a type vector, a hidden class, a call count. This is stored in the FeedbackVector, a living record of the function’s recent behavior. When the heat tips over a threshold, TurboFan’s Graph Builder steps in, opens that vector, and begins to draw the first nodes. It doesn’t just translate bytecodes monotonically — for each operation it queries the feedback slot through a FeedbackNexus, an abstract lens that decodes the raw bits into ProcessedFeedback. That feedback becomes the seed of speculation.
Consider a simple `a + b`. If the feedback says both operands are often small integers, the builder doesn’t insert a general JSAdd node. Instead it chooses a SpeculativeSafeIntegerAdd. That node is a wager: it carries the assumption that its inputs will be small integers, and it computes the sum without overflow checks, without tagging, in the speed of machine words. The speculation is implicit in the node’s very existence — the graph now contains a promise that the runtime will shatter if broken. The builder wires it into the sea: data edges from `a` and `b`, an effect edge from the preceding state, and a dangling thread to a frame state that remembers how to restart in Ignition if the promise fails.
For property accesses, the speculation becomes visible as explicit TypeGuard nodes. When feedback says an object consistently carries a particular map (hidden class), the builder inserts a CheckMaps node. This guard takes the object, the expected map, and a control input, and it clamps the object’s type to that known shape. Downstream operations — loading a field, calling a method — can then assume a fixed offset without any further checks. The graph glows with these guards, each a narrow pass through which the execution must flow. But duplication is rampant: if a value flows through multiple paths that each demand the same type, the builder might plant identical guards on both sides of a junction.
That’s where the sea’s gift of merging shows its quiet art. Because the Sea of Nodes is a value-numbered graph, identical inputs and operations produce the same node. Two CheckMaps guards with the same object and map, even if placed in different basic blocks, collapse into a single node the moment the graph sees they are the same. A later pass of common subexpression elimination sweeps through and erases the redundant ones — and if a guard dominates all uses, any duplicate before it is dead. The Effect-Control Linearization phase doesn’t need to know it merged them; the merge already happened in the graph’s own logic. What begins as a spray of cautious checks becomes a minimal lattice of assumptions, each guard sitting at the narrowest point of convergence.
But the guards are living things: they listen. At runtime, every speculative node has a silent sentinel. The SpeculativeSafeIntegerAdd, if handed a non-integer, doesn’t just fail — it triggers a deoptimization. The CheckMaps node, encountering an unexpected map, pulls the same ripcord. TurboFan had baked into each of these nodes a deoptimization ID that points to a precise frame state: a translated snapshot of the interpreter’s registers, accumulator, and context at the moment the speculation was planted. When the ripcord is pulled, the compiled code stops, the deoptimizer reads that frame state, builds a FrameDescription, and reconstructs an Ignition interpreter frame byte by byte. The function resumes where it left off, as if it had never left the interpreter, now collecting fresh feedback that whispers of the failed assumption.
The speculatively built code is then left hanging. It’s not torn down instantly — instead, the optimized code object is placed on a weak list for the garbage collector to unlink lazily. The next time the function is called, it might run through a trampoline back to the interpreter until enough heat rebuilds, and the cycle turns again. This entire arc — from hot feedback through node insertion, guard merging, and bailout — is a single living cycle, a proof that speed need not be brittle, and that the sea of nodes can hold both the thrill of speculation and the calm of a safe fallback. I watched it, node by node, and the elegance still lingers.
Comments
No comments yet — be the first.