C-Core-Compiler is a controlled C subset compiler project implemented from scratch in Python.
The first-generation goal is not extreme optimization. The priority is to build a compiler pipeline that is logically clear, structurally complete, testable, and explainable. The project transforms source code step by step into Tokens, AST, and IR, then generates normalized backend C code, and finally invokes the system toolchain to produce an executable.
This project is developed continuously in an SDD-style workflow. The goal is not a one-off demo, but an engineering-oriented compiler project with clear structure, complete tests, and room for maintenance and evolution. The first generation follows common industrial baseline expectations first: clear layering, well-defined responsibilities, stable interfaces, traceable errors, and regression-friendly tests.
There are two main reasons behind this design:
- The first generation prioritizes readability, verifiability, and cross-platform executability
- The backend starts with normalized C output so that real executables can be produced quickly on both macOS and Linux
If you want a more structured documentation entry, start here:
- Documentation site: https://billzi2016.github.io/C-Core-Compiler/
- Documentation site directory:
docs-site/ - English docs home:
docs-site/docs/en/index.md - Compiler implementation overview:
docs-site/docs/en/project/how-this-compiler-works.md - GitHub Actions workflow page: docs workflow on GitHub
- GitHub Actions workflow source:
.github/workflows/docs.yml
If you want the project explained by compiler stage, continue with:
Architecture Overview and the Two Compilation PathsRepository MapLexing and TokensParsing and the ASTSemantic Analysis and the Symbol TableIR and LoweringBackend and System ToolchainCLI GuideExamples Guide
inttype- Global variable declarations
- Local variable declarations
- Function definitions
- Parameter passing
- Function calls
if / elsewhileforreturn- Unary operators:
+ - ! - Binary operators:
+ - * / % - Comparison operators:
== != < <= > >= - Logical operators:
&& ||
char- Character literals
- String literals
- Fixed-length array declarations
- Array indexing
- Basic pointer declarations
- Address-of via
& - Dereference via
*
- The Lexer splits the character stream into Tokens
- The Parser converts the Token sequence into an AST
- The Semantic Analyzer performs symbol and basic semantic checks
- The IR Builder lowers the AST into three-address-style IR
- The Backend generates normalized C code from the IR
- The Toolchain Driver invokes
clangorccto produce the final executable
python3 -m unittest discover -s tests -v
python3 -m c_core_compiler examples/hello.c -o build/hello
./build/helloIf you want to directly see standard output, it is recommended to run examples with the _stdout suffix:
python3 -m c_core_compiler examples/fib_stdout.c -o build/fib_stdout
./build/fib_stdoutIf you want a visually obvious demo output, the full sequence example is a good starting point:
python3 -m c_core_compiler examples/fib_sequence_stdout.c -o build/fib_sequence_stdout
./build/fib_sequence_stdoutExamples in this project are divided into two categories with different purposes:
return-style examples:- Express the result through the process exit code
- Better suited for compiler correctness checks, scripts, and automated tests
stdout-style examples:- Print the result directly to standard output
- Better suited for humans to read and for public demonstrations
That maps to the build result directories like this:
build/results_return/: mainly inspectexit.txtbuild/results_stdout/: mainly inspectstdout.txt
For example:
- The result of
fib.cisreturn fib(6);, so the exit code is8 fib_sequence_stdout.cprints1 1 2 3 5 8 13line by line, so it is more suitable for direct presentation
Emit Tokens:
python3 -m c_core_compiler examples/hello.c --emit-tokensEmit AST:
python3 -m c_core_compiler examples/hello.c --emit-astEmit IR:
python3 -m c_core_compiler examples/hello.c --emit-irEmit backend code:
python3 -m c_core_compiler examples/hello.c --emit-cCompatibility-preserved option:
python3 -m c_core_compiler examples/hello.c --emit-asmIn the first generation, --emit-asm outputs the normalized C code generated by the current backend. This preserves a consistent debugging entry point. If the backend is later replaced with a native assembly backend, the same option can continue to be used.
hello.c
python3 -m c_core_compiler examples/hello.c -o build/hello
./build/hellofactorial.c
python3 -m c_core_compiler examples/factorial.c -o build/factorial
./build/factorialfib.c
python3 -m c_core_compiler examples/fib.c -o build/fib
./build/fibcontrol_flow_demo.c
python3 -m c_core_compiler examples/control_flow_demo.c -o build/control_flow_demo
./build/control_flow_demochar_demo.c
python3 -m c_core_compiler examples/char_demo.c -o build/char_demo
./build/char_demoarray_demo.c
python3 -m c_core_compiler examples/array_demo.c -o build/array_demo
./build/array_demopointer_demo.c
python3 -m c_core_compiler examples/pointer_demo.c -o build/pointer_demo
./build/pointer_demostring_demo.c
python3 -m c_core_compiler examples/string_demo.c -o build/string_demo
./build/string_demohello_stdout.c
python3 -m c_core_compiler examples/hello_stdout.c -o build/hello_stdout
./build/hello_stdoutfactorial_stdout.c
python3 -m c_core_compiler examples/factorial_stdout.c -o build/factorial_stdout
./build/factorial_stdoutfib_stdout.c
python3 -m c_core_compiler examples/fib_stdout.c -o build/fib_stdout
./build/fib_stdoutfib_sequence_stdout.c
python3 -m c_core_compiler examples/fib_sequence_stdout.c -o build/fib_sequence_stdout
./build/fib_sequence_stdoutcontrol_flow_demo_stdout.c
python3 -m c_core_compiler examples/control_flow_demo_stdout.c -o build/control_flow_demo_stdout
./build/control_flow_demo_stdoutchar_demo_stdout.c
python3 -m c_core_compiler examples/char_demo_stdout.c -o build/char_demo_stdout
./build/char_demo_stdoutarray_demo_stdout.c
python3 -m c_core_compiler examples/array_demo_stdout.c -o build/array_demo_stdout
./build/array_demo_stdoutpointer_demo_stdout.c
python3 -m c_core_compiler examples/pointer_demo_stdout.c -o build/pointer_demo_stdout
./build/pointer_demo_stdoutstring_demo_stdout.c
python3 -m c_core_compiler examples/string_demo_stdout.c -o build/string_demo_stdout
./build/string_demo_stdoutBatch-compile and run all basic examples, then save the results under build/results_return/<example_name>/:
mkdir -p build/results_return
for f in examples/*.c; do
if [[ "$f" == *_stdout.c ]]; then
continue
fi
name=$(basename "$f" .c)
mkdir -p "build/results_return/$name"
PYTHONPATH=src python3 -m c_core_compiler "$f" -o "build/results_return/$name/program"
"build/results_return/$name/program" > "build/results_return/$name/stdout.txt" 2> "build/results_return/$name/stderr.txt"
printf "%s\n" "$?" > "build/results_return/$name/exit.txt"
doneBatch-compile and run all examples with standard output, then save the results under build/results_stdout/<example_name>/:
mkdir -p build/results_stdout
for f in examples/*_stdout.c; do
name=$(basename "$f" .c)
mkdir -p "build/results_stdout/$name"
PYTHONPATH=src python3 -m c_core_compiler "$f" -o "build/results_stdout/$name/program"
"build/results_stdout/$name/program" > "build/results_stdout/$name/stdout.txt" 2> "build/results_stdout/$name/stderr.txt"
printf "%s\n" "$?" > "build/results_stdout/$name/exit.txt"
doneThere is an important distinction here:
-
return- Represents the process exit code
- Better suited for verification-oriented examples
- Better for testing whether the final computed value is correct
-
stdout- Represents what the program prints to standard output
- Better suited for presentation-oriented examples
- Better for directly showing what a person sees after the program runs
Therefore:
build/results_return/is mainly about exit codesbuild/results_stdout/is mainly about standard output
Basic example run results are stored in:
build/results_return/- One subdirectory per example, for example:
build/results_return/factorial/ - Summary table:
build/results_return/run_results.txt
Standard-output example run results are stored in:
build/results_stdout/- One subdirectory per example, for example:
build/results_stdout/fib_sequence_stdout/ - Summary table:
build/results_stdout/run_results.txt
If you want the most presentation-friendly result, prioritize:
examples/fib_sequence_stdout.c- Result file:
build/results_stdout/fib_sequence_stdout/stdout.txt
src/c_core_compiler/: compiler implementationtests/: unit tests, snapshot tests, and end-to-end testsexamples/: demonstration and regression example programsPRD.md: project scope and goalsTASKS.md: phase-one and phase-two task listsARCHITECTURE.md: module design and data flowTESTING.md: testing strategy and coverage notesCONTRIBUTING.md: collaboration and commit conventionsDEVELOPMENT.md: development workflow and module notesRELEASE.md: version summary and limitationsEXAMPLES.md: example notes and common commands
Thanks to the book Compilers: Principles, Techniques, and Tools for helping me build a more systematic compiler knowledge framework.
Thanks to https://pandolia.net/tinyc/, which gave me a more hands-on understanding of compiler principles and helped me think more clearly about how to break a compiler pipeline into an engineering structure that is implementable, verifiable, and sustainable to iterate on.