Originally developed collaboratively by Noah Freelove, Shiva Mulwani, and Abdullah Shahid in a private repository. This public repository is a copy created for portfolio purposes with permission to share the project.
JDC compiles a subset of Go into portable C99. It is a complete front-to-back compiler written from scratch in Python: lexing, parsing, symbol resolution, type checking, an optimizing middle end, and C code generation.
┌──────────────────────┐
Go source ──▶ lexer ──▶ parser ──▶ AST ─┤ symbols → typecheck ├─▶ codegen ──▶ C99 ──▶ gcc ──▶ binary
│ ↓ │
│ optimizer (opt-in) │
└──────────────────────┘
Goroutines become pthreads, sync.Mutex becomes pthread_mutex_t, and
fmt.Println becomes printf with format specifiers derived from the
statically inferred argument types.
pip install -r requirements.txt # ply==3.11, the only dependency
python3 main.py examples/example1_hello.go --codegen
gcc -std=c99 -o hello .output/example1_hello.c -lpthread
./helloRun the full suite — 70 checks over 37 example programs. Each is compiled to C,
built with gcc, executed, and diffed against expected output; then compiled a
second time with --optimize and checked for identical behaviour:
./run.sh--optimize inserts an optimizing middle end between type checking and code
generation. Seven passes run in a fixed order, with constant folding repeated
after the passes that expose new foldable expressions:
| Pass | Transformation |
|---|---|
| Constant folding | Evaluate literal expressions at compile time, clamped to the int32 range, with Go's truncating division and sign-following modulo |
| Constant propagation | Substitute values of variables written exactly once |
| Dead code elimination | Drop statements after return; resolve if on a literal condition |
| Strength reduction | x * 2ⁿ → x << n, algebraic identities, boolean short-circuit identities |
| Function inlining | Inline non-recursive functions of ≤ 5 statements, with alpha-renaming of locals |
The pipeline only runs once type checking has passed with no errors.
Constant folding, then propagation, then folding again:
// python3 main.py examples/fold_arithmetic.go --codegen
int32_t x = (2 + (3 * 4)); int32_t x = 14;
printf("%d\n", x); printf("%d\n", 14);
int32_t y = (100 - (50 / 5)); int32_t y = 90;
printf("%d\n", y); ──▶ printf("%d\n", 90);Inlining a small function, which then exposes its body to folding:
int main() { int main() {
greet("World"); printf("%s %s\n", "Hello", "World");
greet("Go"); ──▶ printf("%s %s\n", "Hello", "Go");
} }Strength reduction, applied only where it is safe — v * 8 becomes a shift for
uint32, but the same rewrite is deliberately not applied to int32,
where arithmetic shift of a negative value would change the result:
uint32_t scale(uint32_t v) { uint32_t scale(uint32_t v) {
return (v * 8); ──▶ return (v << 3);
} }The optimizer is only useful if it preserves program behaviour, so the guarantee
the test suite enforces is that every example produces identical output with
and without --optimize — all 35 compilable examples are built both ways and
diffed on every run. The passes are correspondingly conservative:
- Division and modulo by zero are never folded — the expression is left for the runtime, so a program that should trap still traps.
- Integer folding clamps to the int32 range rather than using Python's arbitrary-precision integers.
- Go division semantics are reproduced exactly: truncation toward zero, and
a modulo whose sign follows the dividend — neither of which is Python's
//or%. x * 0 → 0requires a side-effect-free operand, so a call expression is not silently deleted.- Constant propagation is keyed on
(scope identity, name), never on the bare name, so shadowed variables in nested scopes stay distinct. Any write inside a loop body disqualifies a variable entirely. - Recursive functions, goroutine targets,
main, and struct-returning functions are excluded from inlining.
Types
: int32, uint32, float32, bool, string, byte, fixed-size arrays,
structs, and type aliases (int is accepted as an alias for int32)
Declarations
: var with and without initializer, := short declaration, multi-value
declaration, function declarations with typed parameters and return types
Control flow
: if / else if / else with optional init statement, three-part for,
condition-only for, infinite for, for ... range, break, continue
Expressions : arithmetic, comparison, logical, and bitwise operators across an 11-level precedence table; unary operators; function calls; struct field selection; array indexing; array literals
Concurrency
: go statements (translated to pthreads, including argument marshalling
through a generated wrapper struct), sync.Mutex
Builtins
: fmt.Println, fmt.Printf, fmt.Print
Lexer (jdc/lexer.py) — PLY-based, with Go's automatic semicolon insertion
implemented via a token queue that injects synthetic semicolons at newlines
following a token that can end a statement.
Parser (jdc/parser.py) — LALR(1) via PLY yacc, with an 11-level
precedence table. Error recovery resynchronizes at semicolon or brace
boundaries so a single syntax error does not cascade.
Symbol table (jdc/symbol_table.py) — a scope tree (global → function →
block → loop) that doubles as the scope container. Struct fields, function
signatures, and type aliases are kept in side tables. Types for := are
inferred here.
Type checker (jdc/type_checker.py, jdc/types.py) — a separate pass over
the AST validating assignments, operators, call arguments, return types, field
access, and the requirement that conditions be bool. It has its own type
hierarchy and an implicit-conversion table (int32↔uint32, int32→float32,
byte↔int32). Type errors accumulate rather than halting, then block code
generation — so one run reports every error in the file.
Optimizer (jdc/optimizer.py, jdc/passes/) — passes return new AST nodes
instead of mutating in place, and every constructed node inherits the
symbol_table reference of the node it replaces, so later passes and codegen
can still resolve scopes.
Codegen (jdc/codegen.py) — a single-pass AST walker emitting C99 directly,
with no intermediate representation. Emits struct typedefs and forward
declarations ahead of definitions, and derives printf format specifiers from
inferred argument types.
python3 main.py <file.go> [options]
--tokens Print the token stream
--ast Export the AST as XML to .xml/
--symbols Print the symbol table
--typecheck Run the type checker and print a summary
--codegen Generate C code into .output/
--optimize Run the optimization pipeline before code generation
./run.sh # 70 checks: Go → C → gcc → run → diff
python3 tests/test_constant_propagation.py
python3 tests/test_dead_code_elimination.pyrun.sh covers the language subset, type-error detection (two programs that must
fail to compile), each optimization pass individually, and the optimizer
equivalence check described above. Goroutine examples compare sorted output,
since thread interleaving is nondeterministic.
main.py CLI driver and pipeline orchestration
jdc/lexer.py Tokenizer with automatic semicolon insertion
jdc/parser.py LALR(1) grammar
jdc/ast_nodes.py 36 AST node types
jdc/symbol_table.py Scope tree construction and := inference
jdc/type_checker.py Type validation pass
jdc/types.py Type hierarchy and conversion rules
jdc/optimizer.py Pass interface and pipeline
jdc/passes/ The five optimization passes
jdc/codegen.py C99 emitter
jdc/xml_exporter.py AST → XML
examples/ 37 Go programs
expected/ Expected stdout for each example
tests/ Unit tests for the propagation and DCE passes
Built by Noah Freelove, Shiva Mulwani, and Abdullah Shahid.