Annotated AST
let : SELF_TYPEx : Int <- 42out_string(...) : SELF_TYPE
I built a custom COOL compiler for Stanford's CS143! The project is an end-to-end systems build that moves from parsing and semantic analysis toward backend and code generation work.
Code generation takes the typed AST from semantic analysis and lowers it into MIPS assembly that the COOL runtime can execute. By this point, the frontend has already rejected malformed programs, so the backend's job is to implement COOL's behavior and emit code that matches the runtime system's expectations.
A big part of this work is deciding how objects and methods live at runtime. That means laying out attributes in memory, emitting global constants and prototype objects, building tables like the class name table, object table, and dispatch tables, and then generating initializer code plus method bodies for every class. It is a much more architectural phase than the earlier passes because small design choices around stack frames, register usage, and attribute offsets affect every expression the compiler emits.
The implementation strategy I followed is a two-pass approach. First, the code gen layer has to figure out the layout and offsets each class will use. Then, it must recursively walk the annotated AST and emit MIPS for expressions, dispatches, and initializers while preserving the calling conventions expected by the COOL runtime. This part also includes runtime checks for situations like dispatch on void, so the final compiler is not just parsing valid COOL, but actually turning it into executable behavior.
Backend Trace
This is a compact walkthrough of the backend pass. It starts from the annotated let expression, locks in runtime layout decisions, and progressively emits the MIPS structures and instructions the COOL runtime expects.
Current Stage
ReadyFocus
ClassTable
Emission Detail
collect class tags, object sizes, and method slots
Pipeline
Backend Snapshot
Annotated AST
let : SELF_TYPEx : Int <- 42out_string(...) : SELF_TYPEObject Layout
tag | size | disp_ptrx @ +12prototype copied on allocDispatch Slots
0: Object.abort1: IO.out_string2: Main.mainMIPS Emission
$s0 = self$a0 = receiver/resultstack frames preserve callsEmitted Assembly
main.s
Once the backend was generating correct MIPS, I started tightening the emitted code with a few local optimizations. The goal was simple: preserve program output while reducing instruction count and related runtime work on the same benchmark script.
Backend Optimization
I measured a baseline with the unoptimized PA4 compiler, then compared each optimization pass against the same program. With all four optimizations enabled, the compiler still produces the same arithmetic output while doing substantially less work at runtime.
What changed
The first pass adds compile-time constant evaluation directly inside code generation, which lets the compiler collapse constant integer and boolean expressions before they ever become runtime instructions.
From there, I layered on local algebraic simplifications such as x + 0, x - 0, x * 1, and x / 1. For cases like x * 0, the compiler still evaluates the non-constant side first so side effects and runtime checks are preserved before returning zero.
I also optimized dispatch. Static dispatch can emit a direct jal because the target class is already known, and dynamic dispatch can be conservatively devirtualized when the receiver's static type is a leaf class with no subclasses.
Optimized build
./cgen -O -o tests/optimizer.opt.s /tmp/optimizer.sem
Constant folding
Constant expressions are evaluated during code generation instead of at runtime, removing unnecessary arithmetic and object traffic for foldable subtrees.
6855 to 5587 instructions
Algebraic identities
Local rewrites like x + 0, 1 * x, and x / 1 cut stack movement and arithmetic while preserving evaluation order where it matters.
5587 to 3923 instructions
Static dispatch direct calls
When the program explicitly names the dispatch type, the backend resolves the target method ahead of time and emits a direct call instead of loading through the dispatch table.
3923 to 3917 instructions
Leaf-class devirtualization
If a receiver's static class has no subclasses, the method binding is fixed, so dynamic dispatch can safely become a direct call in optimized builds.
3917 to 3891 instructions
Once the parser produces an AST, the next question is whether that tree actually describes a legal COOL program. That is the semantic analyzer's job. This is the pass where syntax turns into meaning: the analyzer builds the class hierarchy, rejects malformed inheritance, tracks scopes, resolves dispatches, and enforces COOL's type rules before any code is generated.
For each class, my semantic analyzer walks the AST, checks every expression against the current environment, and records the static type that later backend stages will rely on. If a node violates the language rules, the compiler can stop here with a precise semantic error; otherwise, it produces an annotated AST that is safe to hand off to code generation.
Semantic Pass
This trace picks up from the parser demo's AST for let x : Int <- 42 in out_string("Hello, COOL!\n"). The analyzer checks the current environment, validates each subtree, and stamps expressions with the static types that code generation will consume.
Current Pass
ReadyNode In Focus
Global
Rule / Annotation
build class + method environments
Checks
x : Int before checking the body of the let.
42 conforms to the declared binding type.
Annotated AST
let
: SELF_TYPE
x : Int
scope + x : Int
42
: Int
out_string
: SELF_TYPE
"Hello, COOL!\n"
: String
Environment
The symbol and method context the current semantic check is using.
Once I had a lexical analyzer, I could go on to build a parser using a generator called bison and a package for manipulating trees. The purpose of the parser is to ingest tokens output by the lexical analyzer and to use context free grammars to determine whether a sequence of tokens forms a valid program. A CFG defines a language structure via a set of production rules describing how smaller components (i.e., expressions, structures, declarations) can be composed into larger constructs. As the parser recognizes valid patterns in the token stream, it builds up an abstract syntax tree (AST). The AST is a simplified, structured representation of the program that captures its barest, logical organization.
Parser Trace
This simplified trace follows a parser as it shifts tokens onto a stack, reduces handles into grammar symbols, and uses those reductions to grow the AST for let x : Int <- 42 in out_string("Hello, COOL!\n").
Current Action
ReadyLookahead
LET
Grammar Move
stack = [$]
Input Buffer
Parse Stack
AST Build
let
x : Int
42
out_string
"Hello, COOL!\n"
As the first component, I built a lexical analyzer in C++ with Flex. The objective is to parse Cool files and output an appropriate set of tokens which are used to generate code. Flex allows us to implement a lexical analyzer by defining rules that match user-defined regexes and perform specific actions for each matched pattern. It compiles the rule file into C source code implementing a finite automaton recognizing the defined regexes. r
Lexer Trace
This is a small representative scan of a COOL program fragment. The highlight shows the lexeme currently being matched, while the token stream on the right fills in as the lexer emits tokens.
Source
class Main inherits IO {
main(): Object {
let x: Int <- 42 in
out_string("Hello, COOL!\n");
};
};
Tokens