C++ Compiler Analyzer Project
Go beyond parsing: catch undeclared variables and type errors like a real compiler.
What you will build
Where the mini compiler stops at 'does it parse?', the analyzer asks 'does it make sense?': it builds symbol tables across scopes, checks every variable is declared before use, and type-checks expressions. It's the semantic-analysis phase of a compiler, built as its own tool.
What you will learn
- Scoped symbol tables (stack of scopes)
- Type checking over an AST
- Detecting use-before-declaration and shadowing
- Collecting multiple errors instead of stopping at the first
- Control-flow checks (return in every path)
Feature checklist
- Reads an AST (reuse the mini compiler's parser)
- Scope-aware symbol tables with shadowing warnings
- Type checks: no adding int to string, etc.
- Flags undeclared and unused variables
- Checks every function returns on all paths
- Reports all errors with line numbers in one pass
Starter code
#include <map>
#include <string>
#include <vector>
enum class Type { Int, Bool, Str, Void, Error };
class Scope {
std::map<std::string, Type> vars_;
Scope* parent_;
public:
explicit Scope(Scope* p = nullptr) : parent_(p) {}
bool declare(const std::string& name, Type t) {
if (vars_.count(name)) return false; // redeclaration
vars_[name] = t; return true;
}
Type lookup(const std::string& name) const {
auto it = vars_.find(name);
if (it != vars_.end()) return it->second;
return parent_ ? parent_->lookup(name) : Type::Error;
}
};
struct Analyzer {
std::vector<std::string> errors;
// Visit an expression node; return its type, logging problems.
Type checkExpr(/* Expr* e, */ Scope& scope, const std::string& lhs,
const std::string& rhs) {
Type a = scope.lookup(lhs), b = scope.lookup(rhs);
if (a == Type::Error) errors.push_back("undeclared: " + lhs);
if (b == Type::Error) errors.push_back("undeclared: " + rhs);
if (a != b) errors.push_back("type mismatch in expression");
return a;
}
};Full walkthrough
Step 1: Start where the mini compiler ended: an AST
This project consumes the AST the mini compiler's parser produces (same node types: let, assignment, if, while, calls, binary ops). Its job is everything the interpreter did NOT check: it walks the finished program BEFORE running it and reports problems a run might never reach. Static analysis = being the strict friend who reads the whole essay, not just the first page.
Step 2: Build scope-aware symbol tables
class Scope { std::map<std::string, Decl> vars; Scope* parent; } — entering a block pushes a Scope, leaving pops it. declare(name, type) inserts into the CURRENT scope; lookups walk the parent chain outward. Shadowing (an inner scope redeclaring an outer name) is legal but reported as a WARNING, which requires the lookup to distinguish 'found here' from 'found in a parent' — the Scope knows, because it performed the walk.
void Scope::declare(name, type, line) {
if (vars.count(name)) error(line, "redeclaration of " + name);
else if (lookupInParents(name)) warn(line, name + " shadows an outer declaration");
vars[name] = {type, line, false};
}Step 3: Type-check expressions with checkExpr()
checkExpr(node) returns the TYPE of an expression subtree: literals by kind, identifiers by their declaration, binary ops by a rule table (number + number = number; string + anything-nonstring = ERROR 'cannot add int to string'; comparisons yield bool). Undeclared identifiers error at their USE line and return a poison 'unknown' type that suppresses follow-on errors — one mistake should produce one error, not a cascade of twelve.
Step 4: Flag unused variables
Every Decl carries a used flag; reading a variable sets it. At scope exit, any declared-but-never-read variable earns a warning with its declaration line. (A variable that is assigned but never read is also flagged — decide the exact policy and apply it uniformly; an analyser that changes its mind per scope is noise, not signal.)
Step 5: Check that functions return on all paths
For each function, walk its statement tree: a return statement satisfies its path; an if WITHOUT else satisfies only the branch taken... the analysis asks 'is there ANY path from entry to the closing brace that skips a return?' An if/else where both branches return is safe; a lone if that returns is not. This is a reachability question over the AST shape — no execution, just structure.
Step 6: Collect ALL errors, then report once
The analyser never stops at the first problem: it records (line, severity, message) triples and continues with recovery types, so one run reports everything — 'undeclared variable at line 4', 'type error at line 7', 'unused variable at line 2'. Sorted by line at the end. Compilers that stop at error #1 force a fix-compile-fix loop; yours respects the programmer's time, as the checklist demands.
Step 7: Test with a program that is wrong in four ways at once
Feed it code containing: an undeclared variable, an int + string addition, a shadowed declaration, an unused variable, and a function with a missing return path. Expect exactly those diagnostics with correct line numbers and NO phantom sixth error — precision and recall, both, like the fraud detector's test. Then feed the mini compiler's demo program and demand silence.
Common mistakes
- Aborting at the first error, forcing users into a fix-recompile loop for every single problem.
- One flat global symbol table, making shadowing undetectable and block-scoped variables leak into sibling scopes.
- Cascading errors: reporting the type error, then five follow-on errors caused by the first one's unknown type — poison types exist to stop exactly this.
Compile and run
g++ -std=c++17 -o analyzer analyzer.cppSample run
$ ./analyzer
File:
> sample.mc
Analysing sample.mc (AST from the mini compiler's parser)...
line 2: warning — 'int total' declared but never used
line 4: error — use of undeclared variable 'cont'
line 5: warning — 'int x' shadows the declaration at line 1
line 7: error — cannot add int to string ('total + name')
line 9: error — function 'grade' may finish without returning a value (path through the if has no return)
3 errors, 2 warnings — analysis complete in one pass.
$ ./analyzer
File:
> demo.mc
No errors, no warnings. This program is clean.Try it in your browser
No browser demo is included for this project. Build the C++ version below — that is the real program.