Welcome to the Thrust Compiler (thrustc). If you are reading this, you probably found a bug, want to add something, or are just curious how a compiler project this size actually runs. Whatever the reason, good to have you around.
This guide is long on purpose. It covers where things live, how the code is written, and how to go from an idea to a pull request without tripping over conventions you did not know existed. Skim it once, then come back to whatever section you need.
Thrust is written and reviewed by people who want to understand every line that goes in. You are expected to understand the code you propose and to be able to talk about it if someone asks. Read the Code of Conduct before anything else.
You need a machine that can build the project. The LLVM part is unavoidable, but it is a one-time setup.
- Rust 1.85 or newer, edition 2024. The repo pins
stableat the root (rust-toolchain.toml); thefuzz/workspace pinsnightlybecause cargo-fuzz needs it. - LLVM 17. The project does not use system LLVM directly. You build it once with the compiler-dependency-builder, following the README instructions under "From Scratch".
- The cargo tool deps:
sccache,panic-analyzer,git-cliff. Install them withscripts/cargo-dependencies.sh(or the.ps1,.fish,.battwins. Every script in this repo ships all four flavors).
Then:
$ cargo build --release
$ ./target/release/thrustc --helpThe binary is a thin main that parses the command line and hands off to thrustc_core (thrustc/src/main.rs). If --help prints, you are ready to go.
Read PROJECT_STRUCTURE.md first. It walks the whole pipeline with a diagram: reader → lexer → preprocessor → parser → AST verification → semantic analysis (scoper, typechecker, analyzer, linter, attributes) → optional MIR → LLVM codegen → emission and linking.
The short version, by crate:
thrustc, the binary entry point, nothing else.thrustc_cli,thrustc_options, argument parsing and configuration.thrustc_core, the driver. Orchestrates the pipeline through stage modules:cleaner,starter,interrupt,finisher,validate, plusemitters/andprinters/.thrustc_lexer,thrustc_reader,thrustc_token,thrustc_token_type,thrustc_code_location,thrustc_preprocessor,thrustc_parser(+thrustc_parser_context,thrustc_parser_table,thrustc_parser_external_table), the frontend.thrustc_ast,thrustc_ast_external,thrustc_ast_verifier,thrustc_ast_modificators, the AST and its checks.thrustc_scoper,thrustc_typesystem,thrustc_typechecker,thrustc_general_analyzer,thrustc_linter,thrustc_semantic_analysis,thrustc_entities,thrustc_attributes,thrustc_attribute_checker,thrustc_constants,thrustc_directive,thrustc_atomic_ordering,thrustc_thread_mode, semantic analysis and the middle end.thrustc_llvm_*, the backend: codegen, ABI (System V, NVIDIA CUDA), attributes, call conventions, target triples, linker driver.thrustc_backends,thrustc_heap_allocator,thrustc_abi,thrustc_utils,thrustc_errors,thrustc_diagnostician,thrustc_logging, shared infrastructure.crates/llvm/, vendored, patched bindings (llvm-sys,inkwell,clang,clang-sys). You almost never touch these.fuzz/, the fuzzing suite. Separate cargo workspace, excluded from the main workspace in the rootCargo.toml.scripts/,.github/workflows/,changelogs/,showcase/,tests/,highlighting/, tooling, CI, release artifacts, examples, and editor support.
The tests/ folder contains Thrust source inputs used for manual and integration-style checks. Keep changes there focused and intentional.
If you want a first change you can actually finish, in increasing order of difficulty:
- Fix an issue from the fuzz backlog. The continuous fuzzer archives crashes under
fuzz/backlog/with the AST dump and the LLVM IR. Pick a target, reproduce withcargo fuzz-reproduce-case <target> <input.bin>, find the faulty crate, fix it, then mark it done withcargo fuzz-backlog fixed <target> <issue-id>. SeeCOMPILER_FUZZING.mdandfuzz/COMPILER_CONTINUOUS_FUZZING.md. - Add or improve a diagnostic. New error and warning codes are small, self-contained changes. The codes live in
thrustc_errors, the formatting inthrustc_diagnostician. Details in the diagnostics section below. - Extend an AST trait. Need a predicate or accessor on all AST nodes? Add a trait in
thrustc_ast/src/traits.rsand implement it ingetters.rsorimpls/mod.rs. There is a mechanical pattern for it; see AST extension traits. - Touch the lexer.
thrustc_lexeris small and self-contained (five files). Good way to learn the frontend style without the parser's complexity. - Take on a parser or codegen task. These are the hard ones. Save them for when you have a feel for the conventions.
Every .rs file starts with the same GPL-3.0 block comment, lines 1-18, followed by one blank line. Copy it from an existing file rather than retyping it:
/*
Copyright (C) 2026 Stevens Benavides
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/A few files skip it (a handful of lib.rs files), but new files should include it. If the year in the header is out of date, scripts/license_updater.py fixes the whole tree; no need to hand-edit years file by file.
These conventions are not hard law, but the project is pretty consistent about them. Following them makes review a lot smoother.
- One crate per folder, named
thrustc_<name>. Modules are declared inlib.rsright after theusestatements:mod foo;for private,pub mod foo;for public (thrustc_core/src/lib.rs:20-30). - Split by concern, one concern per file. The lexer has
identifier.rs,number.rs,string.rs,character.rs, andlex.rsdispatches between them (thrustc_lexer/src/lex.rs:107-114). - Use subdirectories when a group gets big, each with its own
mod.rs. The parser is the example:expressions/,expressions/precedences/,statements/,toplevel/(thrustc_parser/src/lib.rs:34-43).
- 4 spaces, no tabs. Allman braces. Opening brace on its own line for
fn,impl,struct,enum, and match arm blocks. snake_casefor functions and variables,CamelCasefor types,SCREAMING_SNAKE_CASEfor constants (PREALLOCATED_TOKENS_CAPACITY,thrustc_lexer/src/lib.rs:31).- Accessors are named
get_xxxand return by reference (get_any_type,get_span). Predicates areis_xxxand returnbool, implemented withmatches!where possible. - Annotate local bindings with their type. The codebase does
let span: Span = ...;everywhere (thrustc_parser/src/toplevel/global_function.rs:51). Follow it. - Use
Selfin constructors and struct literals (thrustc_lexer/src/lib.rs:58). - Mark small accessors
#[inline]or#[inline(always)]. There are ~160 of them; yours will be one more. - No doc comments (
///,//!). Documentation lives in the*.mdguides, not in the code. Plain//comments are sparse and used mostly as section markers inside long enums and matches (thrustc_ast/src/lib.rs:188,// Loops). A brief comment explaining a non-obvious choice is welcome; commenting every line is not.
The project does not use thiserror or anyhow. Error handling is centered around a single ADT and a dispatcher, with a small internal derive macro for diagnostic-code helpers:
CompilationIssueinthrustc_errors/src/lib.rs:25-47is the one error type. Variants carry data positionally:Error(CompilationIssueCode, help, message, Option<note>, span).- Construct errors inline where they happen. See
thrustc_parser/src/lib.rs:303-309andthrustc_lexer/src/lex.rs:57-63. - Dispatch through
Diagnostician::dispatch_diagnostic(&mut self, &CompilationIssue, LoggingType)(thrustc_diagnostician/src/lib.rs:81-202). - Fatal internal bugs go through
abort_compilation(frontend:thrustc_parser/src/abort.rs, backend:thrustc_llvm_abi/src/abort.rs), which dispatches aFrontendBug/BackendBugand exits withthrustc_constants::FAILURE_CODE. - Pipeline-boundary crates often return
Result<T, ()>, the coarse failed/ok channel, with#![allow(clippy::result_unit_err)]at the top of the file.
To add a behavior to every AST node you do two things:
- Declare the trait in
thrustc_ast/src/traits.rs(AstGetType,AstStandardExtensions,AstExpressionExtensions,AstCodeLocation). - Implement it for
Ast<'_>:- a big exhaustive
matchover every variant, inthrustc_ast/src/getters.rs:30-229, for things that reach into variant fields; - or a series of
matches!one-liners, inthrustc_ast/src/impls/mod.rs:46-50, for boolean predicates.
- a big exhaustive
Recursive predicates call the trait method on their children, like has_terminator walking If/Elif/Else blocks (thrustc_ast/src/impls/mod.rs:253-304).
Ast is one big enum with struct-variants (thrustc_ast/src/lib.rs:49-537). Every node derives Debug, Clone, Serialize, and the AST crates add #[cfg_attr(feature = "fuzz", derive(Arbitrary))] (thrustc_ast/src/lib.rs:49-50, thrustc_code_location/src/lib.rs:24-25).
If you add an AST variant, you also need to:
- keep it serializable (
Serialize) andArbitrarybehind thefuzzfeature; - handle it in the accessor
matches ingetters.rs. This one is non-negotiable in a fun way: the compiler will not compile until you do. - if it matters for fuzzing, cover it in the scoped generators under
fuzz/src/(llvm_codegen_local.rs,llvm_codegen_local_loops.rs,llvm_codegen_top_level.rs).
The lint policy is deliberately permissive, configured once in .cargo/config.toml. dead_code and missing_abi are allowed, and the clippy style, complexity, and pedantic groups are allowed. Individual files opt out of specific lints with an inner attribute right after the license header, e.g. #![allow(clippy::result_unit_err)] (thrustc_lexer/src/lib.rs:20).
Please don't widen those global allowances. If you hit a lint, silence it locally in your file with a justification, or fix the code. Same for adding a rustfmt.toml or a per-crate [lints] section: ask before you add one.
Say you want a new error. The concrete steps:
- Define the code. Add a variant to
CompilationIssueCodeinthrustc_errors/src/lib.rs. Existing codes currently extend throughE0055for errors andW0032for warnings. - Give it a title. The
to_title()method maps the code to the colored string printed in the header. - Emit it. Build a
CompilationIssue::Error(code, help, message, None, span)at the failure site (parser, semantic, wherever), and route it todispatch_diagnosticfollowing the pattern inthrustc_parser/src/lib.rs:160-178(verify). - Document it. Add an example to
COMPILER_DIAGNOSTICS.md. Screenshots of terminal output live inassets/examples/diagnostics/.
Warnings work the same way with CompilationIssue::Warning(code, message, span). The linter (thrustc_linter) is a good place to add style warnings, and the linter codes (W0005, local not used, etc.) show the kind of thing that gets a code.
Here's the honest part: the project relies heavily on manual validation and fuzzing, even though there are some Rust unit/integration tests in the workspace. The testing burden still sits on you and on the fuzzers.
What to do before opening a PR:
cargo build --releaseand fix every warning you introduced.- Compile and run at least one real program end to end.
showcase/has examples;tests/has test inputs if you want a quick loop. - If you touched the frontend, run the fuzzers that cover it.
cargo fuzz-lexer,cargo fuzz-pipeline-stablefor a bounded sanity check, and the continuous supervisor if you have time:cargo fuzz-continuous-<target>-<mode>with a--max-time. The fuzz docs (COMPILER_FUZZING.md,fuzz/COMPILER_CONTINUOUS_FUZZING.md) explain the workflow. - If you fixed a bug the fuzzer found, reproduce the old artifact first (
cargo fuzz-reproduce-case <target> <input.bin>) to confirm it crashed before, then confirm it no longer crashes after, and finish by marking the issuefixedin the backlog.
If the lack of a test suite bothers you, that is a real gap, and adding tests is one of the most useful contributions you can make. There is room to build an integration harness on top of tests/ or the fuzz corpus. Propose it.
Commit titles follow COMMIT_CONVENTIONS.md. The shape is feat(scope) or fix(scope) (or (feat(...), fix(...)) when combined), where scope is one of:
llvm_backend, the LLVM backend.llvm_linker_driver, the linker driver invocation.gcc_backend, reserved for the (not yet existing) GCC backend.frontend, AST, lexer, parser, typechecker, and friends.fuzzing, thefuzz/suite, corpora, fuzz targets.project, Cargo, Rust toolchain, GitHub Actions, new crates.project-visual, general and visual project changes (README, assets, editor highlighting, banners).doc, the compiler documentation and guides (CONTRIBUTING, theCOMPILER_*.mdfiles).abi, ABI representation, lowering, calling conventions, or target ABI handling.preprocessador, preprocessor and module/import resolution.std, the standard library.
Title first, then a short, specific description. "feat(frontend) Adding support for X" reads better than "Update parser".
Branch off main, open the PR, and fill in the description with what changed and how you verified it. There is no PR template in the repo; just be honest and complete.
- New
.rsfile has the GPL header. - New AST variant is
Serialize+Arbitrary(fuzz feature), handled ingetters.rs, and covered by the fuzz generators if relevant. - New diagnostic code has a variant, a
to_title()entry, and a doc example. cargo build --releaseis clean.- You ran the relevant fuzzer or a real program, and you can say what you ran in the PR.
- Commit title follows
COMMIT_CONVENTIONS.mdand the scope is right. - You understand the code you are submitting, line by line, and you can talk about it in review.
PROJECT_STRUCTURE.md, the full architecture and pipeline.CLI.md, commands and flags.COMPILER_DIAGNOSTICS.md, diagnostic examples and codes.COMPILER_FUZZING.mdandfuzz/COMPILER_CONTINUOUS_FUZZING.md, fuzzing, one-shot and continuous.COMMIT_CONVENTIONS.md, commit titles.COMPILER_RELEASING.md, how releases are cut (mostly scripts, mostly not your concern unless you maintain releases).CODE_OF_CONDUCT.md, the ground rules.
If a guide is missing something or wrong, fix the guide. Documentation is doc work and it counts.

