Back to blog

Building Courage: a compiler with a name

April 13, 2026
3 min read
Compilers

TL;DR — Courage is the name of a compiler I'm building as a learning project: parse a real language shape, lower it through an IR, and emit something executable. This post is the map, not the manual.


Why a compiler, and why "Courage"

Compilers sit at the boundary between what humans write and what machines run. I want that boundary in my head, not only as a user of rustc or LLVM.

Naming it Courage is half joke, half reminder: the scary part isn't the buzzwords, it's sitting down and finishing each stage before moving on. Same energy as the little dog on the home page: keep going anyway.


What "a compiler" actually means (for this project)

I'm not trying to ship a production language on day one. For Courage, the stack looks like this:

  1. Lexer — turn source text into tokens (keywords, identifiers, literals, punctuation).
  2. Parser — build an AST that matches the grammar I actually want (expression precedence, blocks, functions).
  3. Semantic pass — scopes, types (even a tiny type system), and errors that point at a line instead of at "segfault".
  4. IR — an intermediate representation that is easier to optimize and codegen than raw syntax trees.
  5. Codegen — emit assembly or a simple bytecode / C backend first, whatever keeps the feedback loop short.

You can swap step 5 for LLVM later; the point is to own the pipeline far enough that you understand what LLVM would be doing for you.


Design choices I'm holding myself to

  • Small surface syntax at first: expressions, let, functions, if, maybe a while. No macro system on week one.
  • Readable errors: if the parser fails, the message should say what was expected and where.
  • One test at a time: every phase gets a folder of tiny programs that must pass before I add features.

Where things stand

This is a living project: the repo and exact feature set will move. The invariant is the goal: Courage should stay understandable enough that I can explain each file to someone else without hand-waving.

If you're building something similar: pick a subset of C or a Lisp-y S-exp, implement end-to-end hello world, then grow. The name on the tin matters less than closing the loop from source to running binary.


AST

Jumping straight from an AST to assembly is a nightmare because trees are deeply nested and hardware is stubbornly linear. The IR in Courage bridges this gap by flattening the tree into a linear sequence of instructions—similar to Three-Address Code (TAC). Every operation gets broken down: a = b + c * d becomes a sequence where the multiplication is calculated into a temporary variable, and then added to b. This flattened format makes it drastically easier to write optimization passes (like constant folding) before we even think about touching the metal.

If there’s a rite of passage in compiler writing, it’s handling operator precedence. Getting 1 + 2 * 3 to parse correctly without writing an infinite loop of recursive functions requires a shift in thinking. Implementing a Pratt parser (top-down operator precedence) was the unlock here. It’s elegant once it clicks, but wrapping my head around binding powers took a few solid days of whiteboard staring.


What's next

  • Nail down the grammar on paper (or in a .ebnf comment) before writing more parser code(Done).
  • Get one function compiling and returning an integer(Done).
  • Document the IR format so future-me doesn't reverse-engineer my own project(Ongoing).
  • Make up a LLVM infra clone(deadline: 9 June 2026).

I'll keep updating this site as Courage moves; older long-form stuff may still live elsewhere, but compiler notes from here on live here.

Resources

  • https://blog.regehr.org/archives/1453
  • http://beza1e1.tuxen.de/articles/accidentally_turing_complete.html
  • working with others...

About the Author

muon is trying to just learn