Skip to content
OnlyProjects
M.Sc · Computer ScienceFor educational purposes only
Code included

A Two-Pass Compiler for a Tiny C-Subset Language with a Stack Virtual Machine (Python)

Lexer, recursive-descent parser, type checker, bytecode compiler and VM for a teaching language

Mapped to your syllabusUniversity of Madras PG CBCS · Project · Semester 4

Overview

1 min read

TinyC is a small, statically typed, C-shaped teaching language — int, float, bool and void types, arithmetic and logical operators, if/else, while, for, single-dimension arrays, and functions with parameters and recursion — and this project builds its complete compiler and virtual machine in Python 3.12. The pipeline is a classic two-pass design: the front end lexes, parses by recursive descent into an AST and runs semantic analysis with a scoped symbol table; the back end generates three-address code, lowers it to stack bytecode and executes it on a custom stack virtual machine with call frames.

The project is deliberately written from scratch — hand-written lexer, hand-written parser, hand-written VM — because the point is to make every phase of the dragon book (Aho, Lam, Sethi, Ullman) concrete and inspectable. Every stage can be run standalone: tinyc lex demo.tiny prints the token stream, tinyc parse dumps the AST, tinyc check lists semantic errors with line and column, tinyc compile emits three-address code and bytecode, and tinyc run executes the program on the VM.

What turns it from a code exercise into an M.Sc project is the measurement: a 70-program test corpus (40 valid, 30 invalid) pins down behaviour, and a benchmark suite (recursive Fibonacci, bubble sort on 1,000 elements, a prime sieve to 10,000) reports instruction counts and VM steps with and without the -O constant-folding pass. The dissertation quantifies what each phase costs and what the optimiser saves — numbers you can defend line by line in the viva.

Relevant for
Tamil Nadu
Level
Intermediate · 20 weeks · solo
Common at
University of Madras, Bharathiar University
Updated
Nov 2026

Syllabus alignment

University of Madras · PG CBCS

Project · Semester 4 · 2 credits · 100 = 20 internal (best 2 of 3 presentations) + 60 report + 20 viva

Subjects this project applies
  • Advanced Compiler Design
  • Design and Analysis of Algorithms
  • Advanced Python Programming Practical
How it is evaluated

See your department's project guidelines.

Also fits: Bharathiar University M.Sc CS 2023-24.

What's in the box

11 parts · 5 free · 2 previewed · 4 unlock for ₹2,000

Problem & synopsis

5 min read

Problem statement

Compiler-design papers are examined as theory — first/follow sets, parse tables, syntax-directed translation schemas — but students rarely assemble the phases into a working whole, so the connections between a grammar, a symbol table and generated code remain notional. Existing lab exercises use scanner and parser generators (flex, bison, ANTLR), which perform the interesting constructions invisibly: the student writes regular expressions and grammar rules but never implements a token stream, an LL(1) parse, a scope chain or an instruction encoder. There is also rarely any measurement: a compiler is treated as correct or incorrect, not as a pipeline whose phases have countable costs and whose optimisations have countable benefits.

The need is for a complete but small compiler — language design, hand-written front end, semantic analysis, explicit three-address intermediate code, bytecode and a virtual machine — that a single M.Sc student can build and fully understand, coupled with a test corpus that pins down erroneous input handling and a benchmark suite that quantifies the effect of optimisation. That combination makes compiler theory demonstrable, measurable and viva-defensible.

Synopsis

Abstract

Compiler construction integrates much of an M.Sc computer-science education — automata and grammars, data structures, algorithms, architecture — into one coherent artefact. This project designs and implements TinyC, a C-subset teaching language, with a two-pass compiler and a stack virtual machine in Python. The front end (lexer, recursive-descent parser, semantic analyser) and back end (three-address code, stack bytecode, VM) are built from scratch and evaluated on a 70-program corpus and a three-program benchmark suite measuring instruction counts and VM steps with and without constant folding.

Introduction

The University of Madras M.Sc (CS) curriculum treats compiler design as a core theory paper, but the theory stays abstract without an implementation: LL(1) grammars, first/follow sets and intermediate representations become real only when a parser you wrote fails on your own test case. TinyC is sized so one student can build, test and understand every line in one semester, while exercising every classical phase.

Language Design

  • Types: int, float, bool, void; implicit int-to-float widening only.
  • Statements: declarations, assignment, if/else, while, for, break, continue, block-scoped bodies.
  • Expressions: C-style precedence — arithmetic, relational, equality, logical && || ! — with short-circuit evaluation in the VM.
  • Data and functions: single-dimension arrays; built-in print(x); typed functions with positional parameters, recursion and a mandatory main.
  • The grammar is kept LL(1): left recursion is eliminated and if is disambiguated by mandatory blocks, so the parser needs one token of lookahead.

Existing Work and Positioning

  • The dragon book defines the phase model; Crafting Interpreters demonstrates a tree-walking interpreter and a bytecode VM; Python's ast module shows an industrial AST.
  • Most student compilers stop at interpretation or skip semantic analysis; TinyC implements both a semantic checker and an explicit three-address IR before bytecode, because TAC is what the syllabus examines and what makes optimisation measurable.
  • Generator-based labs (flex/bison) hide scanner and parser construction behind tools; hand-building is the point.

Proposed System

  1. Lexer — longest-match tokenizer with line/column tracking and an error token for illegal characters.
  2. Parser — recursive descent over the LL(1) grammar; dataclass AST; panic-mode recovery skipping to the next ; or } so one compile surfaces multiple errors.
  3. Semantic analyser — scope-chained symbol table checking undeclared/redeclared identifiers, type mismatches, argument counts and return paths.
  4. IR and bytecode — linear three-address quadruples lowered to compact stack bytecode with a constant pool; jump targets back-patched by label.
  5. Stack VM — operand stack, frame stack, bounds-checked arrays, and a step limit turning runaway recursion into a diagnostic.
  6. Optimiser (-O) — constant folding and algebraic identities on the AST; the harness reports instruction-count and VM-step savings.

Feasibility

  • Technical: every phase is standard and Python-expressible; the largest component (parser + semantic) is ~1,200 lines.
  • Time: 20 weeks at PG project pace covers phases, corpus and measurement; the 60% report weight reflects substantial written analysis.
  • Operational: everything runs offline on a plain laptop; the demo needs only Python and pytest.

Objectives

  1. 01Design the TinyC language: an LL(1) grammar for a statically typed C subset with functions, arrays and control flow, documented with first/follow sets.
  2. 02Implement a hand-written lexer and recursive-descent parser producing a typed AST, with line/column-accurate error messages and panic-mode recovery.
  3. 03Implement semantic analysis with a scope-chained symbol table, catching undeclared, redeclared, type-mismatch and return-path errors.
  4. 04Generate linear three-address code as an explicit intermediate representation, then lower it to stack bytecode with a constant pool.
  5. 05Implement a stack virtual machine with operand and frame stacks, short-circuit booleans, arrays with bounds checks and a recursion step limit.
  6. 06Implement a `-O` constant-folding and algebraic-simplification pass and measure its effect on instruction counts and VM steps.
  7. 07Build a 70-program corpus (40 valid with expected outputs, 30 invalid with expected diagnostics) and a three-program benchmark suite, both wired into pytest.

Scope

In scope

  • One statically typed language (TinyC) with the feature list above; one grammar; one target (custom stack bytecode).
  • Hand-written lexer and parser (no flex/bison/ANTLR/PLY); three-address code and bytecode as text/JSON artefacts.
  • Semantic checks limited to scoping, typing, argument counts and return paths.
  • One optimisation pass (constant folding + algebraic identities); measurement of its effect on the benchmark suite.
  • Deterministic execution on the VM (no concurrency, no externals beyond print).

Out of scope

  • Register allocation, liveness, instruction scheduling and native/assembly code generation.
  • Pointers, structs, multi-dimensional arrays, heap allocation and a garbage collector.
  • Debugger, IDE integration, and any parser-generator toolchain.
  • Competitive performance against CPython — Python is the implementation language for clarity; speed of the compiled language itself is a measured quantity, not a goal.

Methodology

SDLC: iterative model with a fixed phase order. Compiler phases have strict dependencies (you cannot check types before an AST), so increments follow the pipeline rather than feature areas; each phase ends with its own pytest suite before the next begins.

PhaseWeeks (20 total)ActivitiesDeliverable
Language & grammar design1–2Token set, EBNF grammar, left-recursion elimination, first/follow sets to prove LL(1)Language specification
Lexer3–4Tokenizer, keyword table, line/column tracking, error tokensLexer + tests
Parser & AST5–7Recursive descent, AST classes, panic-mode recovery, Graphviz dumpParser + AST + tests
Semantic analysis8–10Scope chains, type rules, diagnostics catalogueChecker + tests
Back end11–13TAC generation, bytecode lowering, label back-patchingCode generator + tests
Virtual machine14–15Operand/frame stacks, dispatch loop, bounds and step guardsVM + tests
Optimiser & benchmarks16–17Constant folding pass, benchmark harness, measurement runs-O pass + results tables
Corpus & dissertation18–1970-program corpus complete; write chapters; internal presentation 1 & 2Draft dissertation
Finalisation20Final report, viva rehearsal, code cleanupSubmission

Evaluation approach: correctness is pinned by the corpus (golden output files for valid programs; expected diagnostic catalogue for invalid ones); quality is measured by benchmark tables (static instruction counts, dynamic VM steps, wall time, with and without -O); robustness is measured by the recovery suite (a file with five seeded errors must yield five located diagnostics, not one). Every number in the dissertation is regenerable with one pytest command.

Architecture & tech stack

2 diagrams · 3 technologies · 2 min read

  • Python 3.12
  • pytest
  • Graphviz (AST and control-flow dumps)

The compiler is a strict seven-stage pipeline; each stage consumes the previous stage's output file or object, so any stage can be inspected in isolation — the property that makes the project examinable.

flowchart TD
  SRC["source .tiny file"] --> LEX["Lexer (tokens + line/col)"]
  LEX --> PAR["Recursive-descent parser (LL(1))"]
  PAR --> AST["AST"]
  AST --> SEM["Semantic analyser + scope-chain symbol table"]
  SEM -->|"typed, annotated AST"| OPT["-O constant folding (optional)"]
  OPT --> TAC["Three-address code generator"]
  TAC --> BC["Bytecode encoder + constant pool"]
  BC --> VM["Stack VM (operand stack + frame stack)"]
  VM --> OUT["program output"]
  SEM -->|"errors"| DIAG["Diagnostic list (line, col, code)"]
  PAR -->|"syntax errors"| DIAG

Data structures at the seams. Tokens are (kind, lexeme, line, col) tuples; the AST is a hierarchy of small dataclasses; the symbol table is a chain of dictionaries with parent pointers (one entry per scope, pushed on block/function entry); TAC is a list of op a b c quadruples; bytecode is an array of opcode operand pairs plus a constant pool of literals.

Runtime model. The VM keeps an operand stack for expression evaluation and a frame stack for calls — each frame holds the return address, the function's locals and its own operand region. CALL pushes a frame, RET pops it and leaves the return value on the caller's stack; recursion (Fibonacci) therefore exercises real stack discipline, and the step limit (default 10 million VM steps) turns infinite recursion into a clean diagnostic instead of a hang.

flowchart LR
  subgraph VM["Stack VM per frame"]
    FS["Frame: locals + return addr"] --> OS["Operand stack"]
    OS --> ALU["Arithmetic / compare / logic"]
    ALU --> OS
    FS --> CT["Control: JMP / JZ / CALL / RET"]
  end
  BC["Bytecode + constant pool"] --> FS

Error flow. Syntax and semantic errors never raise raw exceptions to the user: they accumulate as structured diagnostics (line, column, code, message), and after panic-mode recovery the compiler reports all found errors, sorted by position — the behaviour the invalid-program corpus asserts.

Modules

6 modules

  1. Module 01

    Lexer

    Hand-written longest-match tokenizer for the TinyC token set: identifiers, int/float/bool literals, operators and keywords, with a two-slot lookahead, line and column tracking on every token, comments and whitespace skipped, and illegal characters emitted as ERROR tokens carrying position instead of aborting the scan.

  2. Module 02

    Recursive-Descent Parser and AST

    One function per non-terminal, driven by the documented LL(1) grammar with first/follow sets in the report; builds dataclass AST nodes; handles operator precedence via the left-recursion-eliminated expression grammar; recovers from syntax errors by synchronising to ; or } and continuing, so one compile lists every syntax error found.

  3. Module 03

    Semantic Analyser and Symbol Table

    Walks the annotated AST with a scope chain (global, per-function, per-block). Checks declaration-before-use, duplicate declarations in a scope, assignment and operand type compatibility with int-to-float widening, argument count and type per call, and that every typed function returns on all paths. Diagnostics are structured records with line, column and a stable error code.

  4. Module 04

    Intermediate Code Generator (Three-Address Code)

    Emits linear quadruples (ADD t1 x y, LABEL L3, IFZ t1 L2) from the typed AST, with temporaries numbered per expression and labels per construct; short-circuit &&/|| compile to control flow at this stage. TAC is also printed as text, which the report uses to show exactly how source constructs lower.

  5. Module 05

    Bytecode Encoder and Stack VM

    Lowers TAC to a compact stack instruction set with a constant pool, back-patching jump targets after sizes are known. The VM interprets with an operand stack and a frame stack per call; arrays are bounds-checked; short-circuit booleans skip evaluation; a configurable step limit converts runaway recursion into a diagnostic. tinyc run executes and prints program output.

  6. Module 06

    Optimiser and Measurement Harness

    The -O pass performs constant folding and algebraic identities (x+0, x1, 0x, 1*x) bottom-up on the AST before code generation. The harness compiles the three benchmarks (recursive Fibonacci(22), bubble sort over 1,000 elements, prime sieve to 10,000) with and without -O, and reports static instruction counts, dynamic VM steps and wall time into a CSV the dissertation tabulates.

Slides

12 slides · titles free · bullets, notes and the .pptx unlock

  1. Slide 1

    A Two-Pass Compiler for TinyC with a Stack Virtual Machine

  2. Slide 2

    Why Build a Compiler by Hand?

  3. Slide 3

    The TinyC Language

  4. Slide 4

    Grammar and Parsing

  5. Slide 5

    Front End: Semantic Analysis

  6. Slide 6

    Back End: TAC and Bytecode

  7. Slide 7

    The Stack Virtual Machine

  8. Slide 8

    Optimisation: the -O Pass

  9. Slide 9

    Testing: the 70-Program Corpus

  10. Slide 10

    Results

  11. Slide 11

    Demo

  12. Slide 12

    Conclusion & Future Work

Bullets, speaker notes and the .pptx · ₹2,000

The titles above are the real outline. The rest is what you present from.

Unlock for ₹2,000

Viva questions & answers

3 of 16 answered free · 13 unlock

Read each answer, close it, and say it in your own words. Your viva will ask. Now you'll know.

  1. 01Concept

    What are the phases of a compiler, and which of them does your project implement?

    The classical phases are lexical analysis, syntax analysis, semantic analysis, intermediate code generation, optimisation and code generation, held together by symbol-table management and error handling. My project implements all of them: a hand-written lexer, recursive-descent parser, scope-and-type checker, three-address code generator, one optimisation pass, bytecode generation, and a virtual machine as the target, with diagnostics running through every phase.

  2. 02Concept

    Why is your grammar LL(1), and what did you change to make it so?

    A grammar is LL(1) when one lookahead token suffices to pick a production, which needs no left recursion and no common prefixes between alternatives. I eliminated the natural left recursion in the expression grammar by introducing the term/factor layering for precedence, and I made blocks mandatory after if and while so the dangling-else ambiguity disappears; the report shows the first and follow sets that prove the claim.

  3. 03Concept

    What is the difference between a syntax error and a semantic error in your compiler?

    A syntax error violates the grammar — a missing semicolon or an unbalanced brace — and is caught by the parser against the first/follow machinery. A semantic error is grammatically fine but meaningless in context — using an undeclared variable, adding an int and a bool, calling a function with three arguments when it takes two. My parser reports the first kind with expected-versus-found tokens; the checker reports the second kind with line, column and a stable error code.

+13 more questions, with answers

Try the free ones out loud first. Your examiner will not accept “it's in the PPT”.

Source code & how to run

5 steps · complete source code · unlocks with the project

Unlocks with the project · ₹2,000

5 steps · source code zip

Prerequisites, install, seed data and tests, written for demo day. Plus the complete source.

Unlock for ₹2,000

READMEFIRST

288 words · unlocks with the project

What to change before it goes anywhere near a submission. Skipping it is how the demo fails live.

Unlocks with the project · ₹2,000

288 words · ~1 min read

The checklist of things to rename, re-seed and re-think so the project reads as yours.

Unlock for ₹2,000

Instructions

406 words · unlocks with the project

How to study it, adapt it and walk your examiner through it, step by step.

Unlocks with the project · ₹2,000

406 words · ~2 min read

Report structure, formatting rules and the submission checklist your department will grade.

Unlock for ₹2,000

References

6 sources

  1. Alfred V. Aho, Monica S. Lam, Ravi Sethi and Jeffrey D. Ullman, Compilers: Principles, Techniques, and Tools (2nd ed.), Pearson
  2. Robert Nystrom, Crafting Interpreters, Genever Benning (free web edition)
  3. Python 3 Documentation — ast module (Abstract Syntax Trees)
  4. pytest Documentation
  5. Keith D. Cooper and Linda Torczon, Engineering a Compiler (2nd ed.), Morgan Kaufmann
  6. Niklaus Wirth, Compiler Construction, Addison-Wesley (free PDF by the author)

Cite this bundle

OnlyProjects. (2026). A Two-Pass Compiler for a Tiny C-Subset Language with a Stack Virtual Machine (Python): M.Sc Computer Science project bundle [Educational resource]. https://onlyprojects.online/projects/msc-cs-tinyc-compiler-stack-vm

Future scope

1 min read

  • Register-based bytecode and a comparison study of stack vs register VM step counts on the same corpus.
  • More optimisations: dead-code elimination for pure assignments, loop-invariant hoisting and constant propagation across statements, each measured like -O.
  • Type checker upgrade: user-defined structs with member access, and a small trait-based print protocol.
  • Compiler as a service: a tiny web front end that renders the AST and TAC live in the browser for classroom demos.
  • Retargeting: a MIPS-subset assembly generator behind the same TAC IR, turning the project into a full three-target (interpret/TAC/assembly) system.

Keep browsing

Same degree, same syllabus slot, different idea. Pick the one your guide will actually approve.