A C compiler in Java: preprocessor, parser, semantic analysis, and a
lowering to a three-address code (TAC), plus a VM that executes the TAC
and cshell, an interactive C shell in the spirit of jshell. There is
also an x86-64 assembler built in. The linker is planned but not yet implemented;
see Gaps for that and the other things missing or approximated.
The compiler is non-optimizing currently.
The compilation targets are x86-64 Linux and the cshell.
Most of the design lives in docs/. The compiler is WIP and is not production-ready at this point
but supports enough to play with it and build the Lua and curl test suites.
The project is developed with the assistance of claude Fable 5.1. I am driving the architecture and supervising and verifying the implementation.
The compiler implements C as of the C2y working draft N3886, which is
C23 plus a few small additions. It also supports partially C17,
including the restrict keyword.
Lua 5.4.7 builds with the compiler unchanged and passes its own test
suite, all.lua, in full. With the Lua sources unpacked in lua/:
for f in lua/src/l*.c; do bin/mycc -std=c17 -c $f -o obj/$(basename $f .c).o; done
gcc -o lua obj/*.o -lm
cd lua-tests && ../lua -e "_U=true" all.lua
luac.c is left out since it defines its own main.
curl 8.10.1 builds through its own configure with the compiler as
CC, and its test suite runs against the result:
./configure CC=$PWD/../myCc/bin/mycc --without-ssl --disable-shared --without-libpsl --without-zlib
make -j4 && cd tests && perl runtests.pl -n -a
The objects we produce follow the System V ABI, small structs in
registers included, so they link with gcc-built objects and with the
C library either way; AbiTest checks that in both directions.
What is missing or approximated, in the order it is likely to matter:
bin/mycc -o prog compiles every file itself and
hands the objects to gcc for the link. The plan is a dynamic linker
of our own producing a PIE against libc.so.6 with our own _start,
which removes gcc from the pipeline; static linking against libc.a
is not planned.long double is computed and stored as double in a 16-byte
slot. Its bits are wrong when handed to glibc, printf("%Lf")
above all, and va_arg(ap, long double) reads a double. The x87
conversions at the library boundary are the fix._Complex and the _Decimal types are rejected.... is wrong; to our own functions it is consistent.alignas above 16 on a local is not honored natively, since the
frame is addressed from a 16-aligned %rbp; globals of any alignment
are fine.malloc, math, and no setjmp, threads or sockets. A native
build has all of glibc.#include_next and __has_include,
neither of which glibc's headers need without __GNUC__; -M
dependency output is accepted and not produced.gdb shows symbols and disassembly only.Java 17 and the Gradle wrapper are all that is needed:
./gradlew build # compiles and runs every test
./gradlew test # the tests alone
The typed and TAC corpus goldens under src/test/resources are
regenerated with ./gradlew test -Dtyped.update=true -Dtac.update=true.
src/test/resources/c-testsuite holds the single-file programs of
c-testsuite, compiled as
C17 since they predate C23; the ones in passing.txt must pass on the
VM and natively. ./gradlew test -Dctestsuite.report=true --tests '*CTestsuiteTest*' -i | grep CTESTSUITE
tries every program and prints why the others fail.
./gradlew installDist # builds the launchers under bin/
bin/cshell
The launcher runs on your terminal, which is what gives line editing,
history (Up and Down arrows walk it, Ctrl-R searches it, kept in
~/.cshell_history) and tab completion. ./gradlew -q cshell --console=plain also runs the shell but through Gradle's pipes, so it
has none of those; use it for scripted input.
Type declarations, statements or expressions; a declaration is kept
for the lines that follow, a statement runs once, an expression is
evaluated and stored in $1, $2, ... for later use. /exit or
Ctrl-D ends the session.
cshell> int sq(int x) { return x * x; }
| defined sq
cshell> int v[3] = { 1, 4, 9 };
v ==> {1, 4, 9}
cshell> v[1] + sq(v[2])
$1 ==> 85
cshell> for (int i = 0; i < 3; i++) { v[i] *= 2; }
cshell> v
$2 ==> {2, 8, 18}
/help lists the commands: /list, /vars, /funcs, /types,
/macros, /tac name, /drop name, /load file, /save file,
/reset, /exit. On a terminal the shell has line editing, history
in ~/.cshell_history, and tab completion of names, members after
. or ->, commands and header names.
Headers are not included unless you ask: #include <stdio.h> declares
printf, and -I dir on the command line adds a directory for your
own headers, bin/cshell -I include. The shell
and the VM see the bundled headers, a subset of the C library that the
VM implements as builtins, bound by name while a header declares them;
calling a function the VM does not have faults with "no definition".
A session can also be piped in: echo '6 * 7' | ./gradlew -q cshell --console=plain.
bin/mycc is the compiler as a command that takes gcc's flags, so a
build system can be pointed at it with CC=bin/mycc:
bin/mycc -o prog file.c # compiles and links; the link step is gcc's for now
bin/mycc -c file.c -o file.o # an ELF object from our own assembler, for gcc or ld
bin/mycc -S file.c # x86-64 assembly for the GNU assembler, AT&T syntax
bin/mycc -S -a file.c # the same with each TAC instruction as a comment before its code
bin/mycc -E -DX=1 -I inc file.c # preprocessed, to standard output
bin/mycc -std=c17 -c old.c # the older standard described above
gcc -o prog file.s # or file.o: the C library is glibc's
-O, -g, -W..., -f..., -m... and the -M dependency flags
are accepted and ignored; -l, -L and objects on a link line go to
gcc with our objects. In the shell, /asm name shows a function's
assembly the way -S -a does, and bin/cshell -std=c17 selects the
older standard there.
The trees behind the code are printed by Main: the syntax tree, the
typed tree and the TAC of a file, and without a file those of its
built-in sample program:
java -cp "build/install/cshell/lib/*" org.jbm.mycc.Main file.c [-I dir] [-std=c17]
A native build (-S or -c) declares the C library from the system's
own headers in /usr/include, the ones that match the glibc the
program is linked with, so setjmp.h, time.h, pthread.h and the
rest are all there; the compiler brings only the headers that describe
itself, stddef.h, stdarg.h, stdbool.h, float.h, stdalign.h,
stdnoreturn.h and iso646.h. glibc's headers are read as they are,
without __GNUC__, which turns their GNU-only parts off. The bundled
library headers are for the VM, whose builtins they declare.
3 commits
Java
88.4%
C
9.5%
Python
2.1%
A C compiler in Java: preprocessor, parser, semantic analysis, and a
lowering to a three-address code (TAC), plus a VM that executes the TAC
and cshell, an interactive C shell in the spirit of jshell. There is
also an x86-64 assembler built in. The linker is planned but not yet implemented;
see Gaps for that and the other things missing or approximated.
The compiler is non-optimizing currently.
The compilation targets are x86-64 Linux and the cshell.
Most of the design lives in docs/. The compiler is WIP and is not production-ready at this point
but supports enough to play with it and build the Lua and curl test suites.
The project is developed with the assistance of claude Fable 5.1. I am driving the architecture and supervising and verifying the implementation.
The compiler implements C as of the C2y working draft N3886, which is
C23 plus a few small additions. It also supports partially C17,
including the restrict keyword.
Lua 5.4.7 builds with the compiler unchanged and passes its own test
suite, all.lua, in full. With the Lua sources unpacked in lua/:
for f in lua/src/l*.c; do bin/mycc -std=c17 -c $f -o obj/$(basename $f .c).o; done
gcc -o lua obj/*.o -lm
cd lua-tests && ../lua -e "_U=true" all.lua
luac.c is left out since it defines its own main.
curl 8.10.1 builds through its own configure with the compiler as
CC, and its test suite runs against the result:
./configure CC=$PWD/../myCc/bin/mycc --without-ssl --disable-shared --without-libpsl --without-zlib
make -j4 && cd tests && perl runtests.pl -n -a
The objects we produce follow the System V ABI, small structs in
registers included, so they link with gcc-built objects and with the
C library either way; AbiTest checks that in both directions.
What is missing or approximated, in the order it is likely to matter:
bin/mycc -o prog compiles every file itself and
hands the objects to gcc for the link. The plan is a dynamic linker
of our own producing a PIE against libc.so.6 with our own _start,
which removes gcc from the pipeline; static linking against libc.a
is not planned.long double is computed and stored as double in a 16-byte
slot. Its bits are wrong when handed to glibc, printf("%Lf")
above all, and va_arg(ap, long double) reads a double. The x87
conversions at the library boundary are the fix._Complex and the _Decimal types are rejected.... is wrong; to our own functions it is consistent.alignas above 16 on a local is not honored natively, since the
frame is addressed from a 16-aligned %rbp; globals of any alignment
are fine.malloc, math, and no setjmp, threads or sockets. A native
build has all of glibc.#include_next and __has_include,
neither of which glibc's headers need without __GNUC__; -M
dependency output is accepted and not produced.gdb shows symbols and disassembly only.Java 17 and the Gradle wrapper are all that is needed:
./gradlew build # compiles and runs every test
./gradlew test # the tests alone
The typed and TAC corpus goldens under src/test/resources are
regenerated with ./gradlew test -Dtyped.update=true -Dtac.update=true.
src/test/resources/c-testsuite holds the single-file programs of
c-testsuite, compiled as
C17 since they predate C23; the ones in passing.txt must pass on the
VM and natively. ./gradlew test -Dctestsuite.report=true --tests '*CTestsuiteTest*' -i | grep CTESTSUITE
tries every program and prints why the others fail.
./gradlew installDist # builds the launchers under bin/
bin/cshell
The launcher runs on your terminal, which is what gives line editing,
history (Up and Down arrows walk it, Ctrl-R searches it, kept in
~/.cshell_history) and tab completion. ./gradlew -q cshell --console=plain also runs the shell but through Gradle's pipes, so it
has none of those; use it for scripted input.
Type declarations, statements or expressions; a declaration is kept
for the lines that follow, a statement runs once, an expression is
evaluated and stored in $1, $2, ... for later use. /exit or
Ctrl-D ends the session.
cshell> int sq(int x) { return x * x; }
| defined sq
cshell> int v[3] = { 1, 4, 9 };
v ==> {1, 4, 9}
cshell> v[1] + sq(v[2])
$1 ==> 85
cshell> for (int i = 0; i < 3; i++) { v[i] *= 2; }
cshell> v
$2 ==> {2, 8, 18}
/help lists the commands: /list, /vars, /funcs, /types,
/macros, /tac name, /drop name, /load file, /save file,
/reset, /exit. On a terminal the shell has line editing, history
in ~/.cshell_history, and tab completion of names, members after
. or ->, commands and header names.
Headers are not included unless you ask: #include <stdio.h> declares
printf, and -I dir on the command line adds a directory for your
own headers, bin/cshell -I include. The shell
and the VM see the bundled headers, a subset of the C library that the
VM implements as builtins, bound by name while a header declares them;
calling a function the VM does not have faults with "no definition".
A session can also be piped in: echo '6 * 7' | ./gradlew -q cshell --console=plain.
bin/mycc is the compiler as a command that takes gcc's flags, so a
build system can be pointed at it with CC=bin/mycc:
bin/mycc -o prog file.c # compiles and links; the link step is gcc's for now
bin/mycc -c file.c -o file.o # an ELF object from our own assembler, for gcc or ld
bin/mycc -S file.c # x86-64 assembly for the GNU assembler, AT&T syntax
bin/mycc -S -a file.c # the same with each TAC instruction as a comment before its code
bin/mycc -E -DX=1 -I inc file.c # preprocessed, to standard output
bin/mycc -std=c17 -c old.c # the older standard described above
gcc -o prog file.s # or file.o: the C library is glibc's
-O, -g, -W..., -f..., -m... and the -M dependency flags
are accepted and ignored; -l, -L and objects on a link line go to
gcc with our objects. In the shell, /asm name shows a function's
assembly the way -S -a does, and bin/cshell -std=c17 selects the
older standard there.
The trees behind the code are printed by Main: the syntax tree, the
typed tree and the TAC of a file, and without a file those of its
built-in sample program:
java -cp "build/install/cshell/lib/*" org.jbm.mycc.Main file.c [-I dir] [-std=c17]
A native build (-S or -c) declares the C library from the system's
own headers in /usr/include, the ones that match the glibc the
program is linked with, so setjmp.h, time.h, pthread.h and the
rest are all there; the compiler brings only the headers that describe
itself, stddef.h, stdarg.h, stdbool.h, float.h, stdalign.h,
stdnoreturn.h and iso646.h. glibc's headers are read as they are,
without __GNUC__, which turns their GNU-only parts off. The bundled
library headers are for the VM, whose builtins they declare.
3 commits
Java
88.4%
C
9.5%
Python
2.1%