An open-source, all-Java compiler modeled after Java’s C2.
Cliff ClickSea of Nodes · from source to machine code
REBASE / SPLASH · 2026
'+external(R.repo,'github.com/SeaOfNodes/Simple','title-repo'),
notes:notes('An executable compiler tutorial',[
'Assume SSA and compiler background. Simple is a small language used to explain a real optimizing compiler architecture.',
'The compiler is written in Java, modeled on C2, and open source. The teaching unit is a complete compiler snapshot, with runnable tests and an explanation.',
'Today: four live graphs, then the path to machine code. Keep the title still; advance when ready.'
])
},
{
id:'bio',label:'Who am I?',section:'Cliff Click',minutes:1,steps:0,
body:()=>'
Who am I?
Cliff Click
HotSpot’s original C2 compilerSea of Nodes, optimization, and the JVM
Azul, H2O, and half a dozen other startupsJVMs, low-pause GC, and distributed computing
Coffee Compiler ClubFounder and host · language implementation discussions
'+links()+'
People who enjoy talking about compilers.
',
notes:notes('Keep the biography to a minute',[
'Select two relevant facts: original C2 and Sea of Nodes; subsequent JVM and runtime work at Azul.',
'Coffee Compiler Club is the community connection. Point to the YouTube channel and invite people to email for a Discord invite.',
'Transition: rather than describe the entire compiler, watch four graph transformations.'
],'
Short bio adapted from the supplied 2024 biography.
')
},
{
id:'arithmetic',label:'1–4 · Parsing and peepholes',section:'Chapters 1–4',minutes:5,steps:2,demo:'arithmetic',
body:()=>'
Parse straight into an optimized graph.
'+source('arithmetic')+'
SSA construction and local rewriting happen together.
'+result('arithmetic','Expected optimized expression; inspect the actual graph in the viewer.')+'
',
notes:notes('Live demo 1 · leave at 07:00',[
'0:00–0:45: Show the Lattice tab briefly, then return to Example and predict the source result. No separate AST is needed for this construction. The chapter boundary is intentionally tiny.',
'0:45–1:15: Copy code. In a terminal at the repository root run make -C chapter04 view. Paste into the viewer and Compile.',
'1:15–4:15: Use Next/Right to observe a few enclosing peepholes: arg+0 disappears, constants reassociate, and repeated arg becomes multiplication. Point to types on nodes and uses pointing to definitions.',
'4:15–5:00: Jump Last; the result is 2*arg+6. Return to slides. Before launching the next chapter, stop this viewer with Ctrl+C in its terminal.'
],'
Fallback: “Final graph” is a captured compiler graph. Advance builds to reveal the conclusion. No need to step through all 51 captured frames.
')
},
{
id:'control',label:'5–8 · Control and loops',section:'Chapters 5–8',minutes:5,steps:2,demo:'control',
body:state=>'
Control flow is part of the graph.
'+source('control',true)+'
'+(state.step===0?'Regions merge control. Phis select the corresponding values.':'Loop is a Region. Phi_i merges entry and backedge.')+'
'+graph(state.step===0?'phi':'loop')+'
LoopsA backedge supplies another Phi input. Construction can be incomplete.
'+result('control',null,'')+'
',
notes:notes('Live demo 2 · leave at 12:00',[
'0:00–0:45: Briefly show the Lattice tab: control now has live/dead types. Return to Example. A branch chooses abs(arg); the loop accumulates integers from 1 through abs(arg). Build 1 shows the absolute-value Phi; highlight the corresponding branch in the source. Build 2 switches to Loop, Phi_i, and Add(i,1). Build 3 keeps that graph and reveals the sigma sum. The counter sketch omits the Loop control predecessors.',
'0:45–1:15: Stop the prior viewer. Run make -C chapter08 view; paste this example and Compile.',
'1:15–4:15: Find the initial Region/Phi, then the Loop and its backedge. Show the loop-carried i and sum values. Explain lazy Phi creation and how the backedge supplies the next iteration. Fold/unfold the loop for context.',
'4:15–5:00: Jump Last and trace the exit value. For arg=5 the result is 1+2+3+4+5=15. The viewer shows compilation, not the iterations of runtime execution.'
],'
Chapters 5/6 introduce branches and dead control; 7/8 introduce loops and lazy Phis. Avoid walking every frame.
t.sum / t.visitsSeparate field classes carry separate dependencies.
',
'
4*arg + 10Runtime result; the graph still contains loops.
'
]:[
'
p.x → Pair.xAll Pair.x fields share one equivalence class.
',
'
p.y = 99 → Pair.yOnly this slice changes; Pair.x is untouched.
',
'
p.x − before → 0The short example isolates this fold.
'
])+'
'+(state.demo==='memory'?'Inspect: two loops, array Loads/Stores, and blue memory edges. Runtime result: 4*arg+10.':'Inspect: writing p.y leaves p.x unchanged. Result: 0.')+'
Simple type-based aliasing approached the RLE limit. '+external('https://people.cs.umass.edu/~moss/papers/pldi-1998-tbaa.pdf#page=8','Diwan et al., PLDI \u201998 · 8 Modula-3 programs')+'
',
notes:notes('Live demo 3 · leave at 17:00',[
'0:00–0:45: Briefly show Domains and Memory types, then return to Examples. Use the partition picture: Pair.x, Pair.y, and the rest are disjoint equivalence classes whose union is whole memory. The classes group locations across all instances, not one class per object.',
'0:45–1:15: Run make -C chapter17b view. Paste Short alias example. Show that p.y changes while p.x does not, and the result becomes zero.',
'1:15–3:15: Trace the Loads and Store through their field equivalence classes. Same-class accesses may alias; different classes cannot. MemMerge recombines the slices without emitting an instruction. Explain why writing p.y does not clobber p.x. Read-only access would not make a Load independent of memory.',
'3:15–4:30: Switch back to this slide, select Array + object loops, Copy code, then compile it in the same viewer. Jump Last, fold/unfold the loops, and inspect loop-carried memory and the sum and visits aliases.',
'4:30–5:00: GCM uses these dependencies to choose legal placements; it does not guess ordering from source order. Return to slides.'
],'
The graph viewer captures parsing/optimization/type checking, not GCM execution. The large graph has 54 nodes at its final checkpoint; do not step through all 319 frames.
Research context: Diwan, McKinley, and Moss, Type-Based Alias Analysis, PLDI 1998, sections 3.5–3.6 (p. 113). A runtime upper bound left at most an average 2.5% more heap loads eliminable beyond their analysis on eight Modula-3 benchmarks. This is a load-elimination bound, not a speedup percentage. More static opportunities did not translate into corresponding runtime gains. This supports choosing useful, inexpensive alias distinctions; it does not establish that this exact equivalence-class scheme is optimal for Java. The study is a close match to the recalled oracle experiment, not an identified IBM/Java paper.
')
},
{
id:'inline',label:'18 · Functions and Calls',section:'Chapter 18',minutes:5,steps:2,demo:'inline',
body:state=>'
Functions and Calls
'+source('inline')+'
'+[
'Declare and define functions. Parse their bodies and calls. Build the graph as we go.',
'Trivial inlining: erase the call boundary with simple graph rewrites.',
'Fun is a Region. Parm is a Phi. The familiar rules still apply.'
][state.step]+ '
'+states([
'
twice(1 + add1(2)) + 3Two functions. Two call sites.
return 11;Ordinary constant folding finishes the job.
'
])+(state.step===0?'
FunctionsFun, Parm, Return
CallsCall, CallEnd
':'
ProofOne target, one use of its function pointer, no self-recursive inline.
')+result('inline')+'
',
notes:notes('Live demo 4 · leave at 22:00',[
'0:00–0:45: Introduce function definitions and calls: parsing builds Fun, Parm, Return, Call, and CallEnd nodes. The whole top-level body is an implicit main. Each explicit function has a Fun, Parms, and one merged Return. Briefly show the Lattice tab and its singleton function targets, then return to Example. The second build shows trivial inlining by simple graph rewrites.',
'0:45–1:15: Stop the prior viewer. Run make -C chapter18 view; paste and Compile.',
'1:15–4:15: Find a Call/CallEnd and the function entry. Look for the FOLDING label while a single-use function is being inlined. Follow one parameter becoming the caller argument and the Return value taking the place of the call result. Gather → rewrite → release makes the local edit visible.',
'4:15–5:00: Jump Last. Both call boundaries are gone and the return value is 11. This example moves single-caller bodies; it does not demonstrate cloning for multi-callsite inlining.'
],'
If tracing takes too long, show the captured final graph and use the three schematic builds on the slide.
')
},
{
id:'backend',label:'19–21 · From IR to instructions',section:'Chapters 19–21',minutes:2,steps:2,
body:state=>'
'+
backendCard(state,0,'Select instructions','Match ideal nodes to machine operations. Carry operand and calling-convention constraints.','Ideal graph ↓ Machine graph','selected','Selected graph')+
backendCard(state,1,'Allocate registers','Build live ranges and interference. Coalesce, color, split, and spill.','Virtual values ↓ Registers + stack','assembly','Allocated assembly')+
backendCard(state,2,'Encode and execute','Lay out code, emit instruction bytes, resolve relocations, and build object files.','Scheduled instructions ↓ Executable code','encoded','Instruction bytes')+
'
One program · x86-64 System V · Chapter 21 compiler
x86-64ARM64RISC-V
',
notes:notes('Two minutes for the backend',[
'19: instruction selection happens before GCM. Target nodes still carry dependencies. Matching can absorb a Load into an arithmetic instruction.',
'Click each revealed column to flip between discussion and its captured dump; click again to return. Flips persist across builds and synchronize with the presenter window. The returned function receives an existing writable Box, so there is no allocation or calloc lead-in. It adds abs(arg) into box.x and returns that same value.',
'All three captures use the Chapter 21 compiler at the corresponding phase: Chapters 19 and 20 do not yet select unary Minus. The selected table includes every node and input slot, with concrete Java class names instead of shortened op labels; types and redundant use lists are omitted. Follow NegX86 #20, Phi #19, and MemAddX86 #16. Load + Add + Store become one read-modify-write instruction.',
'20: global graph coloring, with register masks and calling conventions. Legality first; spill quality is a separate question. ARM joins the existing x86/RISC-V ports here.',
'The middle capture is CodeGen.asm() immediately after register allocation: arg is rsi, box is rdi, and the result is rax. Labels show ordered blocks; the Phi coalesces into rax. Offsets and the ?? jump are provisional here, and the return still displays callee-save bookkeeping.',
'21: actual instruction encodings and object output. Distinguish native execution from ARM/RISC-V test evaluators; the encoders are shared, but selection and allocation remain in chapter snapshots.',
'The final CodeGen.asm() capture follows encode(): 48f7d8 is neg rax; 480107 is add qword [rdi], rax. Layout reverses the branch to j>= and removes the extra jump. Hex is shown in memory byte order. The printer labels the compare-with-zero as test; 4883f800 actually encodes cmp rax, 0. Only whitespace and long divider rules are compacted in these assembly displays; raw dumps and a regeneration tool are included in the deck.'
],'
Target time: 24:00. This is a progression, not a claim that the three phases exhaust the backend pipeline.
')
},
{
id:'language',label:'22–23 · I/O, methods, types',section:'Chapters 22–23',minutes:2,steps:1,
body:state=>R.languageBody(state),
notes:notes('Language features stress the representation',[
'22: printing is a real chain of function and field accesses, ending at a C ABI boundary. Show how much existing infrastructure participates in one line of code.',
'Click the Chapter 22 column to browse the bundled Chapter 25 standard library. Start in sys; Up reaches the root and sys.smp. Open adt for bitset.smp, or io.smp, aryu8.smp, and libc.smp to connect the library to language features. Breadcrumbs and Up navigate folders or leave a file; Discussion restores the original column. Full width gives long files more room. File contents scroll and can be copied. This is a snapshot of the latest chapter, including features beyond Chapter 22.',
'23: methods add a self parameter. A struct contains a function whose signature mentions the struct; types themselves become cyclic.',
'Click the Chapter 23 column for a complete Counter class and calls, validated with the Chapter 23 compiler and evaluator. n is an instance field; new initializes it to arg. add mutates n and returns self, so calls can chain; get reads n. The result is arg + 7. Discussion restores the conceptual explanation.',
'The interesting point for this audience is preserving canonical type identity and lattice operations while the representation becomes recursive.'
],'
Target time: 26:00. No extra live demo is budgeted.
This one stays on the slide. The loop’s return value is always 1.
The lattice and transfer functions are already there.
'+states([
'
Phi(1, ⊥) = ⊥Pessimistic propagation gets stuck.
',
'
Phi(1, ⊤) = 1Start at the top; propagate reachable information.
',
'
2 − 1 = 1The backedge confirms the invariant.
'
])+'
Value
Initial
Fixed point
Loop Phi
⊤
1
2 − Phi
⊤
1
After SCCP, the program is fully typed. SCCP is the type analysis pass.
',
notes:notes('Optimism earns its keep',[
'Use the source as a concrete loop, but the equations are SSA pseudocode. The pessimistic fixed point cannot discover x=1 through the cycle.',
'For this talk, Copy viewer command launches make -C chapter25 view. Chapter 25 demonstrates the same SCCP example using its separately compiled standard library, avoiding Chapter 24’s thousands of library capture frames. The slide topic and lattice remain Chapter 24.',
'Use Rich lattice for the big picture, then return to Example. The entry value sharpens the Phi to 1; the backedge evaluates to 1 and confirms it. Explain the direction of monotonicity, not just “assume it works.”',
'Control propagation makes the solve conditional. Chapter 24 also discovers interprocedural call-graph edges; Chapter 18’s local linking was only the beginning.'
],'
Target time: 28:00. Continue to the closing slide, or use Overview → Chapter 25 if two minutes remain and replace the closing discussion.
Imported functions can participate in optimization.
Identity crosses a boundary.
Types, aliases, and function indices must be reconciled between units.
Information arrives late.
SSA construction, forward types, and constructor memory need stronger contracts.
',
notes:notes('Optional · spend at most two minutes',[
'This is outside the default 30-minute path. Use it instead of the final two-minute discussion, not in addition.',
'The object contains both native code and a .simple section with types and ideal IR. Imported IR can be optimized and inlined in the client.',
'The compiler must reconcile global identities and incomplete information. This is why Chapter 25 is more than an object writer. Do not attempt its whole architecture here.'
])
},
{
id:'close',label:'Takeaways and discussion',section:'A Simple Tutorial',minutes:2,steps:0,
body:()=>'
One graph. Many compiler lessons.
Build
Construct SSA directly. Make dependencies explicit.
Rewrite
Use types and local rules. Iterate to a fixed point.
'+links()+'',
notes:notes('Leave room for the room',[
'Each chapter is a runnable compiler snapshot, not a diff that needs the final compiler to make sense.',
'Invite questions about where the unified graph helps and where it imposes costs. There is no need to sell a universal IR winner.',
'Point people to the repository, chapter READMEs, and Coffee Compiler Club. Stop at 30:00.'
])
}
];
R.escape=esc;
})();