A compiler for a statically typed, C-like language, targeting x86-64 assembly, written in Rust from scratch (no LLVM, no existing backend).
Binted takes source in a small, statically typed, C-like language and compiles it all the way down to x86-64 assembly, which is then assembled and linked into a native executable. The pipeline is fully hand-written: hand-rolled recursive-descent parser, a TACKY-style three-address IR, and a from-scratch instruction-selection/register-allocation-free codegen backend emitting AT&T-syntax assembly.
source (.bnt) -> AST -> type-checked AST -> IR (three-address code) -> assembly AST -> .s -> gcc -> executable
- Parser: full recursive-descent parser with a complete precedence chain (logical, equality, comparison, bitwise, term, factor, unary), mandatory braces,
if/else,whilewithbreak/continue, function declarations and calls. - Type checker: two-pass hoisting (supports forward references and mutual recursion), scope stack, mandatory-return-path analysis on every function,
Int -> Doublewidening at assignment/call/var-decl boundaries. - IR generation: a three-address/TACKY-style intermediate representation with short-circuit lowering for
&&/||, full support for recursive and mutually recursive functions. - Codegen: a complete
int/bool/void/double/charpipeline — unary/binary ops, variables, control flow, and functions — emitting real, correct x86-64 assembly that assembles and links into a working native binary.
A full multi-function test program (recursive factorial, parity check, conditionals, loops with break/continue, negative division/modulo, bitwise ops, short-circuit boolean logic) compiles, links, and runs correctly end-to-end.
Stringtype in codegen yet (parsed/checked, not yet code-generated)- No structs
- No arrays
- No closures or first-class functions (calls are identifier-only)
- No multi-file compilation
- No optimization passes (register allocation, constant folding, dead code elimination)
Roughly in this order:
- Casting between numeric types
- Structs (fixed-offset layout)
- Multi-file compilation
- A test suite of known-correct programs
- An optimization pass (register allocation, constant folding, dead code elimination)
Binted's frontend deliberately departs from the "Crafting Interpreters" Lox design in a few places:
- No
null— there is no null/nil value in the type system at all - No inheritance — the AST is a plain enum, walked directly instead of through class hierarchies
- No implicit statements at the top level — a program is a sequence of function/variable declarations only, no bare top-level statements
- Mandatory braces everywhere — no dangling-
if/single-statement bodies - Constants are resolved at compile time — a
constmust be a literal (10,'a', ...) or reference anotherconstdeclared earlier; anything else fails to compile rather than being evaluated at runtime
While in the bintedc crate:
cargo run input.bnt
This compiles a .bnt source file into a native executable named output
- Parse —
input.bntis tokenized and parsed into an AST. - Type check — the AST is checked against Binted's static type rules. Function signatures (including
externdeclarations for the standard library) are hoisted first, so forward references and calls to library functions resolve correctly. The value of global constants are also computed and resolved in this stage. - IR generation — the checked AST is lowered into an intermediate representation.
- Code generation — the IR is compiled into x86-64 assembly (
output.s). - Assemble & link —
gccis invoked to assembleoutput.sand link it againstlibstdlib.a(Binted's standard library, providingprint/println/etc.), producing a standalone native binary. Functions from this library must be declared using the extern keyword.
gccmust be available on yourPATH(used as the assembler/linker driver).- The compiled
libstdlib.amust exist atstdlib/target/release/build it first with:
cd stdlib && cargo build --release
This project follows two books closely, one for each half of the compiler:
- Crafting Interpreters by Robert Nystrom — frontend (lexing, parsing, the overall structure of a tree-walking interpreter, adapted here into a fully static, compiled pipeline instead)
- Writing a C Compiler by Nora Sandler — backend (IR design, instruction selection, and the assembly-AST staging approach)
program: declaration* EOF
declaration: varDecl | funDecl | global VarDecl | extern funDecl | statement
extern funDecl: extern "fn" TYPE IDENTIFIER "(" parameters? ")"
global varDecl: const varDecl
varDecl: TYPE IDENTIFIER "=" expression ";"
funDecl: fn TYPE IDENTIFIER "(" parameters? ")" block
parameters: TYPE IDENTIFIER ("," TYPE IDENTIFIER)*
statement: exprStmt | block | ifStmt | whileStmt | forStmt | returnStmt | breakStmt | continueStmt
exprStmt: expression ";"
block: "{" declaration* "}"
ifStmt: "if" expression block ("else" ifStmt | block)?
whileStmt: "while" expression block
returnStmt: "return" expression? ";"
breakStmt: "break" ";"
continueStmt: "continue" ";"
expression: assignment
assignment: IDENTIFIER "=" logic_or | logic_or
logic_or: logic_and ("||" logic_and)*
logic_and: equality ("&&" equality)*
equality: comparison ((!= | ==) comparison)?
comparison: bitwise ((>= | > | <= | <) bitwise)?
bitwise: term ((& | | | ^ | >> | <<) term)*
term: factor ((+ | -) factor)*
factor: unary ((* | / | %) unary)*
unary: ((! | -) unary) | call
call: IDENTIFIER ("(" arguments? ")")* | primary
arguments: expression ("," expression)*
primary: IDENTIFIER | LITERAL | "(" expression ")"