Skip to content

ble27/c-compiler

Repository files navigation

c-compiler

A small subset C compiler written in C++ from scratch. It accepts a restricted form of C source, walks it through a classic compiler pipeline, and emits ARM64 assembly for macOS on Apple Silicon. The compiler does not produce a binary on its own—it writes an output.s file, which you then assemble and link with clang to get a runnable executable.

This is not a production compiler. It covers enough of C to be useful for learning how lexing, parsing, semantic analysis, and code generation fit together.

How it works

Source input. You provide a single .c file (or pipe source on stdin). The compiler reads the full text and passes it to the next stage.

Lexer. The lexer scans the source character by character and groups it into tokens—keywords like int and if, identifiers, numeric and character literals, operators, and punctuation. Lines starting with # (such as #include <stdio.h>) are skipped entirely; they never become tokens.

Parser. The parser reads the token stream and builds a tree-shaped representation of the program (an abstract syntax tree, or AST). It recognizes function definitions, variable declarations, assignments, if/else, return, expressions, and function calls according to the supported grammar.

AST. The AST is the in-memory structure shared by later stages. Each custom node corresponds to something meaningful in the source — a function, an if statement, a binary operation, and so on.

Semantic analysis. A separate pass walks the AST and checks that the program makes sense: variables are declared before use, types match on assignment and in function calls, if conditions are boolean, and return types agree with the enclosing function. Built-in libc calls like printf are registered here without a real declaration in source.

Code generation. The code generator walks the validated AST and prints ARM64 assembly to output.s. It handles stack frames, local variables, control flow for if/else, arithmetic, comparisons, and calls—including variadic-style handling for printf.

Assembly and linking. clang takes output.s, assembles it, and links against the system C library so calls like printf resolve at link time. The result is a native macOS executable.

File structure

File Role
lexer.hpp / lexer.cpp Defines token types and scans source text into a token list.
parser.hpp / parser.cpp Builds the AST from tokens using recursive-descent parsing.
ast.hpp Declares AST node types (functions, statements, expressions, etc.).
semantic.hpp / semantic.cpp Type-checks the AST and maintains symbol scopes.
codegen.hpp / codegen.cpp Emits ARM64 assembly from the AST.
main.cpp Reads input, runs the pipeline, and writes output.s.

Supported grammar

Readable pseudocode for what the compiler accepts:

program        ::= { top_level_item }

top_level_item ::= function_decl
                 | declaration
                 | assignment

function_decl  ::= type IDENT '(' [ param_list ] ')' block
param_list     ::= type IDENT { ',' type IDENT }

declaration    ::= type IDENT [ '=' expression ] ';'
assignment     ::= IDENT '=' expression ';'

block          ::= '{' { statement } '}'
statement      ::= declaration
                 | assignment
                 | if_stmt
                 | return_stmt
                 | function_call ';'

if_stmt        ::= 'if' '(' expression ')' block [ 'else' block ]
return_stmt    ::= 'return' [ expression ] ';'

function_call  ::= IDENT '(' [ arg_list ] ')'
arg_list       ::= expression { ',' expression }

expression     ::= term { ( '+' | '-' | '==' | '!=' | '<' | '>' ) term }
term           ::= factor { ( '*' | '/' ) factor }
factor         ::= INT_LITERAL
                 | CHAR_LITERAL
                 | BOOL_LITERAL          /* true, false */
                 | STRING_LITERAL        /* only for printf format strings */
                 | IDENT
                 | function_call
                 | '(' expression ')'

type           ::= 'int' | 'char' | 'bool' | 'void'   /* void only as function return type */

/* Preprocessor: lines beginning with # are ignored by the lexer. */
/* Builtins: printf is treated as an external libc function. */

C style required

The input must look like normal C, but stricter than the real language:

  • Every statement block uses { and }—no one-line if or else without braces.
  • Semicolons are required after declarations, assignments, returns, and expression statements.
  • Function parameters and locals use only int, char, or bool. void is allowed only as a function return type.
  • No pointers, arrays, string variables, or struct types.
  • No for or do loops yet.
  • String literals may appear only as arguments to printf (format strings).

Valid examples:

int add(int a, int b) {
    return a + b;
}

int main() {
    int x = 10;
    int y = 20;
    if (x < y) {
        return add(x, y);
    } else {
        return 0;
    }
}
#include <stdio.h>

int main() {
    printf("hello\n");
    return 0;
}

Invalid (will not compile):

if (x > 0) return 1;          /* missing braces */
int arr[10];                  /* no arrays */
char* s = "hello";            /* no pointers or string variables */

Current limitations

  • No loops (for and do).
  • No pointers or address-of/dereference operators.
  • No arrays.
  • No general string type—only string literals passed to printf.
  • Single-file programs only; no linking multiple translation units.
  • At most 8 function parameters (register/stack calling convention limit in the codegen).
  • #include is skipped, not processed—only printf is supported as a builtin.
  • No explicit type casts.
  • No ++ / -- operators.

How to build and run

1. Build the compiler

clang++ -std=c++20 -Wall -Wextra main.cpp lexer.cpp parser.cpp semantic.cpp codegen.cpp -o c-compiler

Or, using the included Makefile:

make

2. Compile a .c file to assembly

./c-compiler program.c

This writes output.s in the current directory.

3. Assemble and link with clang

clang output.s -o program

4. Run the executable and check the exit code

./program
echo $?

On macOS, main's return value becomes the process exit code (0–255).

Example program

int max(int a, int b) {
    if (a > b) {
        return a;
    } else {
        return b;
    }
}

int main() {
    return max(3, 7);
}

Build and run:

./c-compiler example.c
clang output.s -o example
./example
echo $?    # prints 7

About

A compiler for a subset of C

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages