Kelick Tech Hub

Build, learn & keep up — tech, Linux & sports news included.

C++ mini Compiler for C++ Project

Advanced•2–3 weeks•lexer, parser, AST

Compile a tiny language of your own design: source in, meaning out.

Share on X

What you will build

Write a compiler (or interpreter) for a small language — variables, arithmetic, if statements, while loops, print. You'll build the classic pipeline: tokeniser, recursive-descent parser producing an AST, then a tree-walking evaluator. It's the deepest project on this list and the one that changes how you think about code itself.

What you will learn

Feature checklist

Starter code

#include <string>
#include <vector>
#include <memory>
#include <cctype>

enum class Tok { Num, Ident, Let, Print, If, While,
                 Plus, Minus, Star, Slash, Eq, LParen, RParen,
                 LBrace, RBrace, Semi, End };
struct Token { Tok type; std::string text; double num = 0; };

std::vector<Token> lex(const std::string& src) {
    std::vector<Token> out; size_t i = 0;
    while (i < src.size()) {
        char c = src[i];
        if (std::isspace(c)) { ++i; continue; }
        if (std::isdigit(c)) {
            size_t j = i;
            while (j < src.size() && (std::isdigit(src[j]) || src[j] == '.')) ++j;
            out.push_back({Tok::Num, src.substr(i, j - i),
                           std::stod(src.substr(i, j - i))});
            i = j; continue;
        }
        if (std::isalpha(c)) {
            size_t j = i;
            while (j < src.size() && std::isalnum(src[j])) ++j;
            std::string w = src.substr(i, j - i);
            Tok t = w == "let" ? Tok::Let : w == "print" ? Tok::Print
                  : w == "if" ? Tok::If : w == "while" ? Tok::While : Tok::Ident;
            out.push_back({t, w}); i = j; continue;
        }
        // single-character tokens...
        ++i;
    }
    out.push_back({Tok::End, ""});
    return out;
}

// Next stages: Parser builds an AST of Expr/Stmt nodes,
// then an Evaluator walks it with a variable map.

Full walkthrough

Step 1: Name the three stages before writing any of them

A compiler is a pipeline: lexer (text → tokens), parser (tokens → a tree, the AST), interpreter/compiler-walk (tree → behaviour). The starter's language is tiny — let, print, if/else, while, arithmetic — and each stage gets its own section of the file. Most beginner pain comes from mixing stages; keep them separated and each one is simple.

Step 2: Write the lexer: lex()

struct Token { type, text, line, col } where type is Number, Ident, Keyword, Op, etc. lex() scans the source char by char: skip whitespace (tracking line/col as you go — error messages need them), group digits into Numbers, letters into identifiers/keywords, and single chars into operator tokens. peek/next over the source string; no regex library needed.

// 'let x = 7;' lexes to:
// Let, Ident(x), Eq, Number(7), Semicolon

Step 3: Parse expressions with precedence climbing

parseExpr() handles +, -; it calls parseTerm() for *, /; which calls parseFactor() for numbers, variables, and parenthesised expressions. The CALL STRUCTURE is the precedence table: deeper calls bind tighter. '2 + 3 * 4' parses as 2 + (3 * 4) = 14 because parseExpr only sees the + after parseTerm has claimed the 3 * 4. This is the single most elegant idea in the whole catalogue — recursion as grammar.

Step 4: Parse statements: let, print, if, while

parseStatement() dispatches on the first token: Let → name, '=', expression, ';' (a declaration NODE storing the name and the expression tree); Print → expression; If → condition, block, optional else block; While → condition, block. Blocks are vectors of statement nodes. The AST node types (struct with a kind enum, or a small class hierarchy with std::unique_ptr children) mirror the grammar one-to-one.

Step 5: Interpret with an environment

The interpreter walks the AST carrying std::map<std::string, long long> env — variable name → value. let evaluates its expression and stores it; print evaluates and outputs; if evaluates the condition (nonzero = true) and walks one branch; while re-evaluates its condition before every iteration. Evaluation of expressions is itself a recursive walk: a binary-op node evaluates both children and combines.

Step 6: Report errors with line and column

Both lexer and parser throw/report errors that carry the token's line and col: 'Error at line 3, column 8: expected ; after let statement'. Because tokens remembered their positions from the lexer stage, good errors are nearly free — and a compiler with position-less errors teaches you why real ones obsess over it.

Step 7: Test with programs whose output you can predict

Write demo programs in your own language: arithmetic with precedence (print 2 + 3 * 4 → 14), a while loop summing 1..10 (print → 55), an if/else on a computed value, and a FizzBuzz for the full workout. Then feed it broken input — missing semicolon, unknown variable, unbalanced paren — and grade the error messages, not just the crash-freedom.

Common mistakes

Compile and run

g++ -std=c++17 -o minicompiler minicompiler.cpp

Sample run

$ ./minicompiler
Program file:
> demo.mc
Running demo.mc —
43
1
2
3
6
1
(program finished, exit code 0)
let x = 7;
let y = x * 6 + 1;
print y;
let i = 1;
while i <= 3 { print i; i = i + 1; }
print 1 + 2 + 3;
if y > 40 { print 1; } else { print 0; }
$ ./minicompiler
Program file:
> broken.mc
Error at line 2, column 10: expected ';' after let statement

Try it in your browser

No browser demo is included for this project. Build the C++ version below — that is the real program.

Related Advanced projects

← All 60 projects · Classic site (Learn & Sandbox)

Download Fake GPS