Skip to content
MultivalencePublic

About

Go-to-C compiler with static type checking, optimization passes, concurrency support, and C99 code generation. Public copy of a collaborative course project originally hosted in a private repository.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

2 Commits

Folders and files

Repository files navigation

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 — a Go-to-C compiler

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.

Quick start

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
./hello

Run 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

Optimization

--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);
}                               }

Correctness

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 → 0 requires 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.

Language subset

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

Implementation notes

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.

CLI

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

Tests

./run.sh                                  # 70 checks: Go → C → gcc → run → diff
python3 tests/test_constant_propagation.py
python3 tests/test_dead_code_elimination.py

run.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.

Layout

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

Credits

Built by Noah Freelove, Shiva Mulwani, and Abdullah Shahid.

About

Go-to-C compiler with static type checking, optimization passes, concurrency support, and C99 code generation. Public copy of a collaborative course project originally hosted in a private repository.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages