Compilers always felt like this mysterious black box to me. Code goes in, magic happens, a program comes out. So for a compiler design assignment, I built one myself: Frog Mini Compiler, a compiler for "Frog," a small teaching language given to us for the assignment.
No frameworks, no compiler-building libraries. Just TypeScript, HTML/CSS, and the three classic phases every compiler goes through: lexical analysis, syntax analysis, and semantic analysis.
So what is "Frog"?
Frog is a simple language defined in our university assignment. It supports variables, printing, if/else statements, and a repeat-until loop. I think it's inspired by Pascal — here's what a Frog program looks like:
FRG_Begin
FRG_Int i, j #
i := 10 #
If [i < 20]
FRG_Print "Small number" #
Else
FRG_Print "Big number" #
FRG_End
A few things to notice:
- Every program starts with
FRG_Beginand ends withFRG_End -
#marks the end of an instruction (like a;) -
:=is used for assignment -
##starts a comment - Conditions go inside
[ ]
The three phases, explained simply
1. Lexical Analysis (the Lexer)
This is the first step, and honestly the most satisfying one to build. The Lexer reads your code character by character and groups those characters into meaningful chunks called tokens.
So i := 10 # doesn't stay as raw text, it becomes something like: ID(i), ASSIGN, INTNUMBER(10), END.
The Lexer also has to be smart about small stuff:
- Skip whitespace and comments (
##) without producing junk tokens - Tell the difference between
3(an int) and3.14(a real number) - Catch errors, like a string that never gets closed with a
"
Basically: the Lexer's job is to turn a wall of text into a clean list of tokens the next phase can actually work with.
2. Syntax Analysis (the Parser)
Once I have tokens, I need to check: do they actually follow the grammar rules of Frog? That's the Parser's job.
Think of the Parser as a strict grammar teacher. It looks at the token stream and expects specific patterns:
- After
FRG_Int, it expects at least one identifier, maybe more separated by commas, then a# - After
If, it expects[, then a condition, then] - After an identifier, it expects
:=and an expression
If any of these patterns break, the Parser reports an error with the line number. The tricky part here was writing parseExpression() and parseFactor() so they could handle chained operations like x + 5 * 2, calling themselves recursively to build up the full expression.
3. Semantic Analysis (the "does this actually make sense" phase)
This is where the real logic lives, and it's also where the code actually executes.
The Semantic Analyzer keeps a symbol table, basically a dictionary that tracks every variable's name, type, whether it's initialized, and its current value. As it walks through the parsed program, it checks things like:
- Was this variable declared before you tried to use it?
- Was it initialized before you tried to print it?
- Are you assigning the right type of value?
And then it just... runs the program. If statements branch based on evaluated conditions, Repeat/until loops keep executing until the condition is true (capped at 1000 iterations so a bug in my code doesn't freeze the browser), and expressions get evaluated with proper operator precedence: parentheses first, then *//, then +/-.
Tying it together with a simple UI
I wrapped all three phases in a small web interface. You can:
- Upload a
.frogfile or type code directly - Run each phase on its own (Lexical, then Syntax, then Semantic) to see intermediate results before moving to the next
Seeing the token list, then the parse results, then the final output side by side made the whole "three phases" concept click way more than reading about it ever did.
What I actually learned
- How tokenization really works, not just the theory
- Why grammar rules matter and how a parser enforces them
- How symbol tables track variable state during execution
- How to implement control flow (if/else, loops) by manipulating indices into a parsed statement list
- How to evaluate expressions correctly, respecting operator precedence
- Why compilers are split into phases in the first place: each phase has one job, and that makes the whole thing easier to reason about and debug
Known limitations
This is a learning project, so it's intentionally simple:
- No functions/procedures
- Only int and real number types
- No string operations beyond printing
- Basic error recovery, it stops at the first semantic error per line
- No optimization phase
Try it yourself
If you're curious how compilers work under the hood, I'd genuinely recommend building a tiny one like this. You don't need a fancy language, just pick 5-6 features (variables, print, if, loop) and force yourself to implement all three phases by hand. It's one of those projects where the "aha" moments come fast.
Check out the code here: github.com/alaa-mekibes/frog-mini-compiler 🐸
Top comments (0)