CD Notes
Complete project building an interpreter for a small procedural language
Introduction and Motivation
Building your own programming language — even a tiny one — is the ultimate compiler design exercise. It ties together every concept from this course: lexical analysis, parsing, semantic analysis, symbol tables, type checking, and code execution. When you finish this project, you will truly understand how programming languages work from the inside out.
Our mini language will be a simple procedural language supporting integer and float types, variables, arithmetic and logical operations, control flow (if-else, while loops), functions with parameters and return values, and basic input/output. Think of it as a stripped-down C with just enough features to write interesting programs like factorial, fibonacci, or sorting algorithms.
Language Design Decisions
Before writing any code, you need to design your language. Every choice has consequences for the complexity of your compiler:
// Example program in our mini language
int factorial(int n) {
if (n <= 1) {
return 1;
}
return n * factorial(n - 1);
}
void main() {
int x = 5;
int result = factorial(x);
print(result); // Output: 120
}Key design decisions include: Will types be static or dynamic? (We choose static — checked at compile time.) Will variables need explicit declaration? (Yes — helps catch typos.) Will functions support recursion? (Yes — essential for teaching.) These choices shape every phase of our compiler.
Implementation Architecture
The implementation follows the classic compiler pipeline, with interpretation instead of machine code generation:
Phase 1: The Lexer
The lexer recognizes these token categories:
typedef enum {
// Keywords
TOK_INT, TOK_FLOAT, TOK_VOID, TOK_IF, TOK_ELSE,
TOK_WHILE, TOK_FOR, TOK_RETURN, TOK_PRINT,
// Identifiers and literals
TOK_IDENTIFIER, TOK_INT_LITERAL, TOK_FLOAT_LITERAL,
// Operators
TOK_PLUS, TOK_MINUS, TOK_STAR, TOK_SLASH,
TOK_ASSIGN, TOK_EQUAL, TOK_NOT_EQUAL,
TOK_LESS, TOK_GREATER, TOK_LESS_EQ, TOK_GREATER_EQ,
TOK_AND, TOK_OR, TOK_NOT,
// Delimiters
TOK_LPAREN, TOK_RPAREN, TOK_LBRACE, TOK_RBRACE,
TOK_SEMICOLON, TOK_COMMA,
TOK_EOF
} TokenType;The key challenge is distinguishing keywords from identifiers. The lexer first reads an alphabetic sequence, then checks if it matches any keyword. If not, it is an identifier:
Phase 2: The Parser and AST
The parser builds an Abstract Syntax Tree where each node represents a language construct. Here is the AST node structure:
The parser uses recursive descent. Here is how it handles if-statements:
ASTNode* parse_if_statement(Parser *p) {
expect(p, TOK_IF);
expect(p, TOK_LPAREN);
ASTNode *condition = parse_expression(p);
expect(p, TOK_RPAREN);
ASTNode *then_branch = parse_block(p);
ASTNode *else_branch = NULL;
if (match(p, TOK_ELSE)) {
else_branch = parse_block(p);
}
return make_if_node(condition, then_branch, else_branch);
}Phase 3: Semantic Analysis
The semantic analyzer traverses the AST and performs critical checks using a symbol table:
The symbol table supports nested scopes using a stack of hash tables:
typedef struct Scope {
HashMap *symbols; // name → Symbol mapping
struct Scope *parent; // enclosing scope
} Scope;
Symbol* lookup(Scope *scope, const char *name) {
while (scope != NULL) {
Symbol *s = hashmap_get(scope->symbols, name);
if (s != NULL) return s;
scope = scope->parent; // Search enclosing scope
}
return NULL; // Not found in any scope
}Phase 4: The Interpreter
The interpreter walks the AST and executes each node. It maintains a runtime environment with variable values and a call stack for function invocations:
Tracing Through Execution
Let us trace factorial(3):
| Call factorial(3) | create new environment {n=3} |
| Call factorial(2) | create new environment {n=2} |
| Call factorial(1) | create new environment {n=1} |
Testing Strategy
Create a systematic test suite covering each language feature:
Extension Ideas
Once the basic interpreter works, consider these enhancements:
- Arrays:
int arr[10]; arr[0] = 5;— requires array type in symbol table and bounds checking - String type: String literals, concatenation, and length operations
- For loops:
for (int i = 0; i < 10; i++)syntactic sugar - Code generation: Instead of interpreting, generate x86 assembly or LLVM IR
- Standard library: Built-in functions like
sqrt(),abs(),strlen() - Error recovery: Continue after the first error to report multiple issues
Key Takeaways
Building a mini language teaches you that a programming language is not magic — it is a carefully designed system with a lexer, parser, semantic analyzer, and execution engine working together. Every feature you use daily in languages like Python or Java was deliberately designed, implemented, and tested by someone who understood these same compiler principles. This project gives you that same understanding, making you a better programmer regardless of which language you use.
Exam Focus
Revise definitions, diagrams, examples, and short-answer points for Mini Programming Language Project.
Interview Use
Prepare one clear explanation, one practical example, and one common mistake for this Compiler Design topic.
Search Terms
compiler-design, compiler design, compiler, design, projects, mini, programming, language
Related Compiler Design Topics