Skip to Content
Course content

201: How a C Compiler Works: Lexing, Parsing, Codegen

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I remember the first time I realized that the compiler wasn't some magical black box that just "understood" my intent. I had written a particularly nasty line of nested ternary operators, and the error message the compiler spat back was absolutely incomprehensible. It didn't point to the logic error; it complained about a missing semicolon three lines up. That's when I started wondering: How does this thing actually read my code?

Let's pull back the curtain. Instead of me just lecturing you on theory, let's take a specific line of C and pretend we're building a tiny compiler for it right now. Here is our target:

int result = 10 + 5 * 2;

The String Problem

To you and me, that looks like a variable declaration and some math. But to the computer, when the compiler first opens your .c file, it's just a long stream of characters. It sees 'i', then 'n', then 't', then a space, then 'r'... and so on. It has no idea that int is a keyword or that 10 is a number.

If I tried to process this character-by-character, my logic would be a nightmare of if statements. I'd be asking, "Is this '1'? Okay, is the next character also a digit? If so, keep going." That's tedious. I need a way to group these characters into meaningful "words."

Grouping the Chaos

This is where Lexing (or Tokenization) comes in. I'm going to write a mental script that scans the line and bundles characters into "tokens."

As I scan int result = 10 + 5 * 2;, I'll group them like this:

  • int → [KEYWORD]
  • result → [IDENTIFIER]
  • = → [OPERATOR]
  • 10 → [INTEGER_LITERAL]
  • + → [OPERATOR]
  • 5 → [INTEGER_LITERAL]
  • * → [OPERATOR]
  • 2 → [INTEGER_LITERAL]
  • ; → [PUNCTUATION]

Now we're getting somewhere. We've moved from a sea of characters to a list of categorized tokens. But wait—we still don't have "meaning." If I accidentally wrote 10 = result int;, the lexer would be perfectly happy. It would just say, "Yep, I see an integer, an operator, an identifier, and a keyword." It doesn't know that the order is illegal.

Building a Mental Tree

To handle the order and the logic, we need Parsing. I can't just read the tokens left-to-right because math doesn't work that way. If I just processed 10 + 5 * 2 from left to right, I'd get 30. But C follows the order of operations, so I need 20.

I'll build a tree (specifically, an Abstract Syntax Tree, or AST). I'll start at the "root" of the expression and work my way down based on precedence. The lowest precedence operator (the one that happens last) becomes the top of the tree.

In our case, the assignment = is the very last thing to happen. So the tree looks like this:


      [Assignment (=)]
     /                \
 [result]        [Addition (+)]
                /              \
            [10]         [Multiplication (*)]
                        /                \
                      [5]                [2]

Now the compiler "understands" the structure. It knows that it must solve the multiplication branch before it can solve the addition branch, and it must solve the addition before it can assign the value to result. This is why a missing semicolon or a misplaced bracket throws the parser into a panic—it can't complete the tree.

The Final Translation

We have a tree, but the CPU can't execute a tree. The CPU wants a linear list of simple instructions. This is Codegen (Code Generation). I'm now going to traverse that tree and translate each node into assembly instructions for the target architecture (like x86 or ARM).

I'll start at the bottom of the tree and work up:

  1. Multiply: mov eax, 5; imul eax, 2 (Now eax is 10)
  2. Add: add eax, 10 (Now eax is 20)
  3. Assign: mov [result_address], eax (Store 20 into memory)

And there it is. We went from a raw string of text, to categorized tokens, to a structural tree, and finally to machine-readable instructions. Every time you hit "build" in your IDE, this entire pipeline fires off in a fraction of a second.




📋 Practical Task

Analyzing GCC Assembly Output for a Custom Math Function

Instead of writing a compiler from scratch, you're going to use a real one to see the "Codegen" phase in action. Your goal is to observe how the C compiler translates a specific mathematical expression into assembly.

The Setup: Create a file named calc.c with the following function:

int calculate() {
    return (10 + 2) * 3;
}

The Task: Run the following command in your terminal to tell GCC to stop after the compilation phase and output the assembly code instead of a binary:
gcc -S -O0 calc.c
(The -S flag triggers assembly output; -O0 disables optimizations so the assembly remains easy to read and follows your code literally.)

The Analysis: Open the newly created calc.s file. Find the calculate: label and identify the following three things:

  1. Locate the instruction that handles the addition (10 + 2).
  2. Locate the instruction that handles the multiplication (* 3).
  3. Verify that the addition happens before the multiplication in the assembly sequence.

Submission: Copy the relevant 3-5 lines of assembly code from calc.s that perform these operations and explain which line corresponds to which part of your C expression.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.