A C compiler written in Rust, targeting native binaries through LLVM.
Catalyst takes a single .c file, runs it through a hand written lexer, recursive descent parser, semantic analyzer, and LLVM IR code generator, then links the result with clang into an executable.
echo 'int main() { return 42; }' > test.c
cargo run -- test.c
./test; echo $? # 42Prerequisites:
- Rust (edition 2024)
- LLVM 18.1 development libraries
libpolly-18-devclangonPATHfor linking
cargo build --release
./target/release/catalyst source.c # writes ./source
./target/release/catalyst source.c -o myprog # writes ./myprogint,char,long,float,double,void- Pointers of arbitrary depth:
int *p,char **pp - Integer promotion by rank:
char < int < long, integers promote intofloat < double
- Local variables, with and without initializers (uninitialized locals are zeroed)
- Global variables with constant initializers (integer or char literal), zeroed by default
- Tentative globals:
int foo;followed byint foo = 3;completes the declaration, two full definitions are an error - Functions with up to 6 parameters
- Forward declarations, checked against their definition for return type, parameter count, and parameter types
- Duplicate function definitions and name collisions between globals and functions are rejected
{ }blocks with nested scopingif/elsewhile,do ... while,for(theforinitializer gets its own scope)breakandcontinue, rejected at parse time outside a loopreturnwith or without a value
- Integer and floating literals, character literals (
'a','0') - Identifiers, function calls
- Unary
-,!,~, address-of&, dereference* - Binary
+ - * / %, comparisons== != < <= > >=, logical&& || - Ternary
cond ? a : b - Assignment to a variable or through a pointer:
x = 1,*p = 1
allocainstructions are hoisted into the function entry block&&and||short circuit with real branches and aphimerge- Ternaries compile to branches plus
phi - Comparisons produce
i1, zero extended toi32 - Integer widths are coerced at three boundaries: binary operands, stores, and returns
- Dereference picks its load type from the declared pointer type, so
**ppworks - Calls to functions Catalyst has never seen (
printf, etc.) are implicitly declared asi32 (i32, ...)so they link against libc
Ranked by how much they are needed:
- Arrays and indexing — no
[], no aggregate types - Pointer arithmetic —
p + 1is rejected by the type checker - Float and double arithmetic — the types parse, declare, and store, but binary operations assume integers
- Compound and increment operators — no
+=,++,-- - Bitwise binary operators —
& | ^ << >>are not parsed (only unary~)
Also missing: struct, union, enum, typedef, string literals (lexed, never consumed), casts, sizeof, switch, goto, the preprocessor and #include, multiple translation units, unsigned and short qualifiers, and debug info.
Known behavioral gaps:
- Functions are hoisted, so any function can call any other regardless of source order. This is more permissive than C99.
- Character literals accept only ASCII alphanumerics, so
'\n'and'+'do not lex. - No check that a non-void function returns on every path; a function that falls off the end gets an implicit zero return.
main's signature is not validated, only its existence.
Highest value areas right now: arrays, pointer arithmetic, a float code generation path, and an actual test harness.
Educational project, in active development.