A web-based compiler that translates a small pseudocode language into C++ and shows every compiler phase along the way.
▶ Live demo: codebridge-swart.vercel.app
- App: https://codebridge-swart.vercel.app (frontend on Vercel)
- API health: https://codebridge-api.onrender.com/health (FastAPI backend on Render)
- Source: https://github.com/ashmita-dev/CodeBridge
- Project report: DOCX · PDF (10 pages)
- Presentation: PPTX (15 slides, with speaker notes)
Using the live app:
- Open the live demo. The backend runs on Render's free plan and sleeps when idle, so the first compile can take up to a minute; meanwhile the output panel shows "Waking up the compiler server".
- Pick a program from the example menu, or type your own. With Auto on it recompiles as you type; otherwise press Compile (Ctrl/⌘+Enter).
- Use the tabs to see each phase: generated C++, Tokens, AST, Symbols, TAC, Optimized TAC and Diagnostics. Errors are underlined in the editor.
A minimal program to try:
let x = 3
let y = 4
let sum = x + y
print sum
print(x * y)
Direct links open an example on a chosen tab, for example optimizer showcase or FizzBuzz as C++. CodeBridge generates C++ but never runs it on the server. Copy it and build it with any C++17 compiler.
You write a program in the browser. CodeBridge then runs it through a hand-written compiler pipeline: lexer → recursive-descent parser → typed AST → semantic analysis → three-address code → optimizer → C++ code generator. The workbench shows the tokens, syntax tree, symbol table, original and optimized TAC, and the generated C++ for the code in the editor. Errors come back as structured diagnostics that point to a line and column.
CodeBridge is a third-year B.Tech Compiler Design mini-project. All phases are implemented by hand in Python, with no parser generators or existing transpilers.
- Problem statement
- Features
- The language
- Architecture
- Compiler phases
- Optimization examples
- Project structure
- Setup and running locally
- Testing
- API
- Environment variables
- Deployment
- Limitations
- Screenshots
Students usually meet compiler phases one at a time and on paper. They rarely get to watch a single program flow through every phase of a real, working compiler. CodeBridge makes that flow visible and interactive:
- Each phase is a separate, tested module with a well-defined input and output.
- The optimizer is shown in action next to the original code.
- The final C++ output is real, compilable code. Generated programs are built with g++ in the test suite, and their output is compared with a reference interpreter.
- Hand-written lexer: tracks line and column positions and reports invalid characters, malformed numbers, out-of-range literals and bad string escapes.
- Recursive-descent parser: implements a documented EBNF grammar with correct operator precedence and associativity,
else ifchains and structured syntax errors. - Typed AST: has a JSON serializer and an interactive tree view. Clicking a node highlights its source.
- Semantic analysis:
- a scoped symbol table, with shadowing and unique renaming
- type inference and checking for
int,float,stringandbool - int→float widening
- boolean conditions
- detection of division by a literal zero
- unused-variable warnings
- no cascading error messages
- Three-address code: uses temporaries, labels,
ifFalse/ifTrue/goto, explicit int→float conversions and short-circuitand/or. - Optimizer:
- local constant and copy propagation
- safe constant folding (C++ integer semantics, with no folding of overflow or division by zero)
- algebraic simplification
- branch folding
- temporary coalescing
- dead-temporary elimination
- unreachable-code, jump and label cleanup
- C++17 generator: works from the optimized TAC. It rebuilds expressions and recovers
if/else if/whilestructure, and emits correct types, string escaping and parenthesization. - FastAPI backend:
POST /compileandGET /health, with input validation, size limits, JSON errors and CORS configured from environment variables. - React workbench:
- CodeMirror editor with syntax highlighting, inline error underlines and Ctrl/⌘+Enter to compile
- 12 example programs (including 3 broken ones) with a reset button
- optimize and auto-compile toggles
- a live phase pipeline
- tabs for C++, tokens, AST, symbols, TAC, optimized TAC (with pass statistics and side-by-side comparison) and diagnostics
- copy buttons
- responsive mobile layout
- Testing:
- 213 backend tests, including a 300-program randomized differential test of the optimizer
- g++ build-and-run checks for every example
- an end-to-end HTTP test against a live Uvicorn server
- 20 frontend tests
The backend never executes user code. CodeBridge translates and displays C++; it does not run it.
The full specification is in docs/LANGUAGE.md.
# Comments start with '#'
let count = 10 # type inferred: int
let price: float = 19 # explicit type; int widens to float
let name = "Ada"
while count > 0 {
if count % 2 == 0 {
print("even", count)
} else if count == 5 {
print("five")
} else {
print("odd")
}
count = count - 1
}
print(name + "!", price * 2, not (count > 0))
program = { sep } { statement { sep } } EOF ; (* sep = NEWLINE | ";" *)
statement = let_stmt | assign_stmt | print_stmt | if_stmt | while_stmt ;
let_stmt = "let" IDENT [ ":" type ] "=" expression ;
assign_stmt = IDENT "=" expression ;
print_stmt = "print" "(" [ args ] ")" | "print" args ;
args = expression { "," expression } ;
if_stmt = "if" expression block [ "else" ( if_stmt | block ) ] ;
while_stmt = "while" expression block ;
block = "{" { sep } { statement { sep } } "}" ;
type = "int" | "float" | "string" | "bool" ;
expression = or_expr ;
or_expr = and_expr { "or" and_expr } ;
and_expr = not_expr { "and" not_expr } ;
not_expr = "not" not_expr | equality ;
equality = comparison { ( "==" | "!=" ) comparison } ;
comparison = additive { ( "<" | "<=" | ">" | ">=" ) additive } ;
additive = term { ( "+" | "-" ) term } ;
term = unary { ( "*" | "/" | "%" ) unary } ;
unary = "-" unary | primary ;
primary = INT | FLOAT | STRING | "true" | "false" | IDENT | "(" expression ")" ;| Supported | Not supported (rejected with a diagnostic) |
|---|---|
let declarations with optional type annotation |
functions / procedures |
assignment x = expr |
for, break, continue |
int, float, string, bool literals |
arrays, lists, string indexing |
+ - * / %, unary -, parentheses |
+=, ++, casts |
== != < <= > >= |
&&, ||, ! (use and, or, not) |
and, or (short-circuit), not |
input / reading values |
print(a, b, ...), or without parentheses: print a, b |
single-quoted strings, characters |
if / else if / else, while (nestable) |
free-standing { } blocks |
# comments, newline or ; statement ends |
expressions as statements |
Semantics follow C++ on purpose:
- Integer division truncates toward zero.
- The result of
%takes the sign of the dividend. intis 64-bit.printformats floats likestd::cout.
As a result, the generated program prints exactly what the source program means.
flowchart LR
subgraph Browser["Frontend · React + Vite + TypeScript (Vercel)"]
ED["CodeMirror editor<br/>examples · toggles"]
UI["Pipeline + tabs<br/>C++ · Tokens · AST · Symbols<br/>TAC · Optimized TAC · Diagnostics"]
end
subgraph API["Backend · FastAPI (Render)"]
EP["POST /compile<br/>GET /health"]
subgraph Compiler["app/compiler (framework-independent)"]
LX["Lexer<br/>lexer.py"] --> PR["Parser<br/>parser.py"]
PR --> SA["Semantic analysis<br/>semantic.py"]
SA --> TG["TAC generator<br/>tacgen.py"]
TG --> OP["Optimizer<br/>optimizer.py"]
OP --> CG["C++ generator<br/>codegen_cpp.py"]
end
end
ED -- "source (JSON)" --> EP
EP --> LX
CG -- "tokens · AST · symbols · TAC ·<br/>optimized TAC · C++ · diagnostics" --> UI
LX -. "lexical errors" .-> UI
PR -. "syntax errors" .-> UI
SA -. "semantic errors" .-> UI
Each phase stops the pipeline when it reports errors. The response marks every phase as ok, error or skipped, and a failed compilation never returns TAC or C++.
The lexer is a single-pass, hand-written scanner. It recognizes:
- keywords and identifiers (
[A-Za-z][A-Za-z0-9_]*) - integer, float and string literals, with escapes decoded
- operators, using a longest match first (
<=before<) - delimiters
Every token carries a 1-based line/column span. Newlines are emitted as statement terminators, except inside parentheses. The lexer keeps going after an error, so it reports all lexical errors at once:
- invalid characters, with hints such as "use 'not' for logical negation" for
! - malformed numbers (
1.2.3,12abc,3.) - out-of-range literals
- unterminated strings and unknown escapes
The parser is a recursive-descent parser with one function per grammar rule.
- Precedence comes from the call hierarchy:
or→and→not→ equality → comparison → additive → term → unary → primary. - Left associativity comes from loops (
while next is '+': left = Binary(left, op, right)).
The parser builds typed dataclass nodes (LetStmt, IfStmt, Binary, …) with source spans. It stops at the first syntax error and returns a structured diagnostic such as "expected ')' to close '(' but found '{'". Nesting depth and expression size are bounded, so pathological input cannot exhaust the Python stack.
A symbol table keeps a stack of scopes, one per block. Analysis walks the AST and does three things:
- Resolves names. Undefined variables, assignments to undeclared variables and redeclarations in the same scope are errors. Shadowing in an inner block is allowed, and the shadowing variable gets a unique name (
x_2) that later phases use. Names are also kept clear of C++ keywords and of TAC temporaries. - Infers and checks types. It annotates every expression with its type and enforces the type rules from the spec: only int→float widens implicitly, conditions must be
bool,%needs ints,+works on numbers or two strings, and so on. - Reports all errors, with no cascades. A variable whose initializer failed is "poisoned" so that its later uses don't add a misleading "undefined variable" error. Unused variables produce a warning.
The typed AST is lowered to TAC. Each instruction has at most one operator:
| Instruction | Form |
|---|---|
| Copy | x = y |
| Binary | t1 = a + b |
| Unary | t2 = -a, t3 = not c, t4 = (float) i |
| Jumps | goto L1, ifFalse t1 goto L2, ifTrue t1 goto L3 |
| Label | L1: |
| Output | print a, b |
The lowering patterns are:
- if/else:
ifFalse c goto Lelse; …then…; goto Lend; Lelse: …else…; Lend: - while:
Lstart: …cond…; ifFalse c goto Lend; …body…; goto Lstart; Lend: - and / or: jumps that skip the right operand. This is real short-circuit evaluation, so
d != 0 and 10 / d > 1never divides by zero. - Mixed int/float arithmetic: an explicit
(float)conversion instruction.
The passes are repeated until nothing changes (a fixed point):
| Pass | What it does | Safety rule |
|---|---|---|
| Constant folding | t1 = 2 * 3 → t1 = 6 |
Uses C++ semantics. Never folds overflow, division by zero or non-finite floats |
| Constant & copy propagation | replaces uses of x by its known value |
Only inside a basic block. Everything is forgotten at every label (loop head or branch join) and on reassignment |
| Algebraic simplification | x+0, x*1, x/1, x-0, x*0 (ints), s+"" |
Float rules are limited to identities that are exact in IEEE-754. x + 0.0 is kept because -0.0 + 0.0 = +0.0 |
| Branch folding | ifFalse true … disappears, ifFalse false goto L → goto L |
Conditions have no side effects |
| Temporary coalescing | t = a + b; x = t → x = a + b |
Only for temporaries defined and used exactly once |
| Dead temporary elimination | removes temporaries that are never read | User variables and prints are never removed |
| Unreachable code / jump / label cleanup | CFG reachability, goto to the next label, unused labels |
Iterated to a fixed point |
The optimizer's correctness is tested, not assumed:
- Every example, plus 300 randomly generated programs, is run through a reference AST interpreter, an interpreter for the original TAC and an interpreter for the optimized TAC. All three outputs must match.
C++ is generated only from the optimized TAC, so the output reflects the optimizations. The generator works in three steps:
- Expression rebuilding. A temporary that is defined once and used once is folded back into the expression that uses it. This gives
total = total + i * 5;instead of three statements. - Structuring. The label/jump patterns from step 4 are recognized and turned back into
while,if/elseandelse ifchains. If a pattern ever fails to match, the generator emits equivalentgotocode instead of guessing. - Emission:
long long,double,std::stringandbooldeclarations. A variable first assigned at the top level is declared there; all others are declared at the top ofmain.- Precedence-aware parentheses and correct string escaping.
std::coutwithstd::boolalpha, with adjacent literal text merged.
Source:
let x = 2 * 3 + 4
let i = 0
while i < x {
if i % 2 == 0 and i > 0 {
print(i)
}
i = i + 1
}
| Original TAC | Optimized TAC |
|---|---|
|
|
The optimizer applied these passes:
- 2 × constant folding
- 2 × constant propagation
- 1 × temporary coalescing
- 2 × dead-temporary elimination
i < x is deliberately not rewritten to i < 10. L1 is a loop header, so propagation stops there, which keeps the optimizer safe when variables change inside loops.
Generated C++:
int main() {
std::cout << std::boolalpha;
bool t4 = false;
long long x = 10;
long long i = 0;
while (i < x) {
t4 = i % 2 == 0;
if (t4) {
t4 = i > 0;
}
if (t4) {
std::cout << i << "\n";
}
i = i + 1;
}
return 0;
}The examples/optimization.cb sample shows branch folding and algebraic simplification. In the workbench, open Optimized TAC → Compare with original.
CodeBridge/
├── backend/
│ ├── app/
│ │ ├── main.py FastAPI app: /health, /compile, CORS, error handling
│ │ ├── config.py environment configuration
│ │ ├── schemas.py Pydantic request/response models
│ │ └── compiler/
│ │ ├── tokens.py token types, keyword and operator tables
│ │ ├── lexer.py lexical analysis
│ │ ├── ast_nodes.py typed AST and JSON serializer
│ │ ├── parser.py recursive-descent parser (grammar in docstring)
│ │ ├── typesys.py types and C++ type mapping
│ │ ├── semantic.py symbol table, scopes, type checking
│ │ ├── tac.py TAC instruction set
│ │ ├── tacgen.py AST → TAC
│ │ ├── semantics.py C++-compatible value semantics (folding)
│ │ ├── optimizer.py optimization passes
│ │ ├── codegen_cpp.py optimized TAC → C++
│ │ ├── diagnostics.py spans and structured diagnostics
│ │ └── pipeline.py compile_source(): runs all phases
│ ├── tests/ pytest suites + reference interpreters and g++ harness
│ ├── requirements.txt runtime deps (FastAPI, Uvicorn, Pydantic)
│ ├── requirements-dev.txt + pytest, httpx2
│ └── .env.example
├── frontend/
│ ├── src/
│ │ ├── App.tsx workbench state and layout
│ │ ├── components/ header, pipeline, editor panel, output tabs, views
│ │ ├── editor/ CodeMirror setup, language modes, theme
│ │ └── lib/ API client, examples, phase helpers, hooks
│ ├── package.json
│ └── .env.example
├── examples/ *.cb programs, index.json, expected/*.txt outputs
├── docs/
│ ├── LANGUAGE.md language specification
│ ├── TEST_MATRIX.md what is tested and where
│ ├── REPORT.md mini-project report
│ ├── SLIDES.md presentation slide content
│ └── screenshots/
├── render.yaml Render blueprint (backend)
├── vercel.json Vercel config (frontend)
└── .github/workflows/ci.yml CI: backend + frontend tests and build
You need:
- Python 3.13
- Node.js 22+
- optionally, g++ or clang++ to run the C++ build-and-run tests
Backend
cd backend
py -3.13 -m venv .venv # once
.venv\Scripts\python.exe -m pip install -r requirements-dev.txt
.venv\Scripts\python.exe -m uvicorn app.main:app --reload --port 8000On macOS/Linux, use python3.13 -m venv .venv and .venv/bin/python.
Frontend
cd frontend
npm install
npm run dev # http://localhost:5173The frontend calls http://localhost:8000 in development by default. To point it elsewhere, set VITE_API_URL in frontend/.env.local.
cd backend; .venv\Scripts\python.exe -m pytest # 213 tests
cd frontend; npm run typecheck; npm test; npm run build # 20 tests + type check + buildThe backend suite includes:
- unit tests for every phase
- API tests for success and for lexical, syntax and semantic errors, plus validation and size limits
- optimizer equivalence checks (examples + 300 random programs)
- g++ compilation of the generated C++ for every example, compared with the expected output in
examples/expected/ - an end-to-end test that starts a real Uvicorn server and compiles over HTTP
The C++ tests find the compiler from CODEBRIDGE_CXX, then g++ or clang++ on PATH, then C:\msys64\ucrt64\bin\g++.exe. If no compiler is found, they are skipped (not faked).
See docs/TEST_MATRIX.md for the full matrix. GitHub Actions runs both suites on every push.
{ "status": "ok", "service": "codebridge", "version": "1.0.0" }Request:
{ "source": "let x = 2 + 3\nprint(x * 2)", "optimize": true }The response is always HTTP 200 for a well-formed request, whether or not the program compiles. This is a real, abridged response:
{
"success": true,
"phases": [{ "name": "lexer", "status": "ok" }, "… parser, semantic, tac, optimizer, codegen"],
"diagnostics": [],
"tokens": [{ "type": "LET", "category": "keyword", "lexeme": "let", "line": 1, "column": 1, "end_line": 1, "end_column": 4, "value": null }, "…"],
"ast": { "kind": "Program", "statements": ["…"], "span": { "line": 1, "column": 1, "end_line": 2, "end_column": 13 } },
"symbols": [{ "name": "x", "unique_name": "x", "type": "int", "scope_depth": 0, "line": 1, "column": 5, "used": true }],
"tac": [" t1 = 2 + 3", " x = t1", " t2 = x * 2", " print t2"],
"optimized_tac": [" x = 5", " print 10"],
"optimization": {
"rounds": 2,
"passes": { "constant_folding": 2, "constant_propagation": 3, "dead_temp_elimination": 2 },
"labels": { "constant_folding": "Constant folding", "…": "…" },
"instructions_before": 4,
"instructions_after": 2
},
"cpp": "// Generated by CodeBridge …",
"structured_cpp": true,
"timings_ms": { "lexer": 0.12, "parser": 0.086, "semantic": 0.078, "tac": 0.037, "optimizer": 0.158, "codegen": 0.065 }
}When compilation fails, success is false:
- The failing phase has status
errorand later phases areskipped. diagnosticslists the errors. Each one hasstage(lexical/syntax/semantic),code,message,line,column, end position andnotes.tac,optimized_tacandcpparenull.
Other error responses:
422: malformed requests.413: source over the size limit.500: unexpected server errors, as JSON with no stack trace.
| Where | Variable | Default | Purpose |
|---|---|---|---|
| backend | CORS_ORIGINS |
http://localhost:5173,http://127.0.0.1:5173 |
Comma-separated origins allowed to call the API |
| backend | MAX_SOURCE_CHARS |
20000 |
Maximum source size accepted by /compile |
| backend (Render) | PYTHON_VERSION |
3.13.15 (in render.yaml) |
Python runtime on Render |
| frontend | VITE_API_URL |
http://localhost:8000 in dev |
API base URL. Required for production builds |
No secrets are needed. .env files are git-ignored, and only the .env.example files are committed.
The setup is a backend on Render and a frontend on Vercel. Both configs are in the repository.
- Render (API):
- Go to New → Blueprint and select this repository. Render reads
render.yaml, which uses rootDirbackend,pip install -r requirements.txtanduvicorn app.main:app --host 0.0.0.0 --port $PORT, with a health check on/health. - Set
CORS_ORIGINSto the Vercel production URL (exact origin, no trailing slash).
- Go to New → Blueprint and select this repository. Render reads
- Vercel (frontend):
- Import the repository and keep the root directory as the repository root.
vercel.jsonbuildsfrontend/, so the sharedexamples/folder is available. - Set
VITE_API_URL(Production) to the Render service URL.
- Import the repository and keep the root directory as the repository root.
- Redeploy the frontend after setting the variable, because Vite inlines it at build time. Then check:
GET <render-url>/healthreturnsok.- The deployed workbench compiles the starter program.
On Render's free plan the service sleeps when idle. The first request can take up to about a minute, and the UI shows a "waking up the compiler server" message while it waits.
Live deployment:
| Part | URL | Notes |
|---|---|---|
| Frontend (Vercel) | https://codebridge-swart.vercel.app | Built with VITE_API_URL=https://codebridge-api.onrender.com. Redeploys on every push to main. |
| Backend (Render) | https://codebridge-api.onrender.com | CORS_ORIGINS=https://codebridge-swart.vercel.app. Redeploys on every push to main. |
| Health check | https://codebridge-api.onrender.com/health | Returns {"status":"ok",...} |
- Missing language features: there are no functions, arrays,
for/break/continueor input. The language is intentionally small. - Syntax errors: the parser reports the first syntax error only. It does no error recovery. Lexical and semantic errors are all reported.
- Optimizer scope: propagation is local to basic blocks. There is no global data-flow analysis, so loop-invariant values (such as
xinwhile i < x) are not propagated, and loop-invariant code motion is not implemented. - Runtime errors: dividing by a variable that is zero at run time, and integer overflow, are undefined behavior, exactly as in C++. CodeBridge detects only literal division by zero.
- Float output: floats print with C++'s default 6 significant digits.
- No execution: the backend translates code but never compiles or runs it. Execution is out of scope by design.
| Optimized TAC with pass statistics | AST viewer |
|---|---|
![]() |
![]() |
| Semantic diagnostics | Token stream |
![]() |
![]() |





