Skip to main content
The mcc-driver crate provides a callback-based API for observing and controlling the compilation pipeline. This is useful for building tools, tests, and custom workflows.

Overview

The run function executes the full compilation pipeline and calls trait methods at each stage:
  1. Preprocessing
  2. Parsing → after_parse
  3. Lowering → after_lower
  4. Code generation → after_codegen
  5. Rendering → after_render_assembly
  6. Assembling & linking → after_compile
Callbacks can inspect intermediate results, collect diagnostics, or stop the pipeline early.

Callbacks Trait

pub trait Callbacks {
    type Output;

    fn after_parse<'db>(
        &mut self,
        _db: &'db dyn mcc::Db,
        _source_file: mcc::SourceFile,
        _ast: Ast<'db>,
        _diags: Vec<&Diagnostics>,
    ) -> ControlFlow<Self::Output> {
        ControlFlow::Continue(())
    }

    fn after_lower<'db>(
        &mut self,
        _db: &'db dyn mcc::Db,
        _tacky: tacky::Program<'db>,
        _diags: Vec<&Diagnostics>,
    ) -> ControlFlow<Self::Output> {
        ControlFlow::Continue(())
    }

    fn after_codegen<'db>(
        &mut self,
        _db: &'db dyn mcc::Db,
        _asm: asm::Program<'db>,
        _diags: Vec<&Diagnostics>,
    ) -> ControlFlow<Self::Output> {
        ControlFlow::Continue(())
    }

    fn after_render_assembly(
        &mut self,
        _db: &dyn mcc::Db,
        _asm: Text,
        _diags: Vec<&Diagnostics>,
    ) -> ControlFlow<Self::Output> {
        ControlFlow::Continue(())
    }

    fn after_compile(
        &mut self,
        _db: &dyn mcc::Db,
        _binary: PathBuf,
    ) -> ControlFlow<Self::Output> {
        ControlFlow::Continue(())
    }
}

Type Parameter: Output

type Output;
The Output associated type specifies what value is returned when the pipeline stops early (via ControlFlow::Break).

Return Type: ControlFlow

Each callback returns std::ops::ControlFlow<Self::Output>:
  • ControlFlow::Continue(()) - Continue to the next stage
  • ControlFlow::Break(value) - Stop the pipeline and return value

Callback Methods

after_parse

fn after_parse<'db>(
    &mut self,
    _db: &'db dyn mcc::Db,
    _source_file: mcc::SourceFile,
    _ast: Ast<'db>,
    _diags: Vec<&Diagnostics>,
) -> ControlFlow<Self::Output>
Called after parsing the source file.
db
&'db dyn mcc::Db
The compilation database
source_file
SourceFile
The source file that was parsed (after preprocessing)
ast
Ast<'db>
The abstract syntax tree
diags
Vec<&Diagnostics>
Diagnostics accumulated during parsing

after_lower

fn after_lower<'db>(
    &mut self,
    _db: &'db dyn mcc::Db,
    _tacky: tacky::Program<'db>,
    _diags: Vec<&Diagnostics>,
) -> ControlFlow<Self::Output>
Called after lowering the AST to three-address code (TAC).
db
&'db dyn mcc::Db
The compilation database
tacky
tacky::Program<'db>
The three-address code representation
diags
Vec<&Diagnostics>
Diagnostics accumulated during lowering

after_codegen

fn after_codegen<'db>(
    &mut self,
    _db: &'db dyn mcc::Db,
    _asm: asm::Program<'db>,
    _diags: Vec<&Diagnostics>,
) -> ControlFlow<Self::Output>
Called after generating assembly IR from TAC.
db
&'db dyn mcc::Db
The compilation database
asm
asm::Program<'db>
The assembly IR representation
diags
Vec<&Diagnostics>
Diagnostics accumulated during code generation

after_render_assembly

fn after_render_assembly(
    &mut self,
    _db: &dyn mcc::Db,
    _asm: Text,
    _diags: Vec<&Diagnostics>,
) -> ControlFlow<Self::Output>
Called after rendering assembly IR to text.
db
&dyn mcc::Db
The compilation database
asm
Text
The rendered assembly text
diags
Vec<&Diagnostics>
Diagnostics accumulated during rendering

after_compile

fn after_compile(
    &mut self,
    _db: &dyn mcc::Db,
    _binary: PathBuf,
) -> ControlFlow<Self::Output>
Called after assembling and linking the final binary.
db
&dyn mcc::Db
The compilation database
binary
PathBuf
Path to the compiled executable

Config

pub struct Config {
    pub db: mcc::Database,
    pub target: Triple,
    pub cc: OsString,
    pub output: Option<PathBuf>,
    pub input: SourceFile,
}
Configuration for a compilation session.
db
Database
The compilation database
target
Triple
Target triple (e.g., x86_64-unknown-linux-gnu)
cc
OsString
C compiler command for preprocessing/linking (e.g., "cc" or "gcc")
output
Option<PathBuf>
Output path for the binary. If None, uses input path without extension.
input
SourceFile
The input source file

Outcome

pub enum Outcome<Ret> {
    Ok,
    Err(anyhow::Error),
    EarlyReturn(Ret),
}
The result of running the compiler.
Ok
Compilation succeeded
Err
anyhow::Error
Compilation failed with an error
EarlyReturn
Ret
Compilation stopped early via a callback (contains the Output value)

Methods

impl<Ret> Outcome<Ret> {
    pub fn to_result(self) -> Result<(), anyhow::Error>;
    pub fn to_result_with(
        self,
        f: impl FnOnce(Ret) -> Result<(), anyhow::Error>,
    ) -> Result<(), anyhow::Error>;
}
to_result: Convert to Result, treating early returns as errors. to_result_with: Convert to Result, applying a custom handler for early returns.

run Function

pub fn run<C: Callbacks>(cb: &mut C, cfg: Config) -> Outcome<C::Output>
Executes the compilation pipeline with callbacks.
cb
&mut C
required
The callbacks implementation
cfg
Config
required
Compilation configuration
return
Outcome<C::Output>
The result of compilation

Example: Noop Callbacks

The simplest implementation does nothing:
use std::ops::ControlFlow;
use mcc_driver::{Callbacks, Config, Outcome};
use mcc::{Ast, Text, diagnostics::Diagnostics, codegen::asm, lowering::tacky};

struct Noop;

impl Callbacks for Noop {
    type Output = ();

    fn after_parse<'db>(
        &mut self,
        _db: &'db dyn mcc::Db,
        _source_file: mcc::SourceFile,
        _ast: Ast<'db>,
        _diags: Vec<&Diagnostics>,
    ) -> ControlFlow<Self::Output> {
        ControlFlow::Continue(())
    }

    fn after_lower<'db>(
        &mut self,
        _db: &'db dyn mcc::Db,
        _tacky: tacky::Program<'db>,
        _diags: Vec<&Diagnostics>,
    ) -> ControlFlow<Self::Output> {
        ControlFlow::Continue(())
    }

    fn after_codegen<'db>(
        &mut self,
        _db: &'db dyn mcc::Db,
        _asm: asm::Program<'db>,
        _diags: Vec<&Diagnostics>,
    ) -> ControlFlow<Self::Output> {
        ControlFlow::Continue(())
    }

    fn after_render_assembly(
        &mut self,
        _db: &dyn mcc::Db,
        _asm: Text,
        _diags: Vec<&Diagnostics>,
    ) -> ControlFlow<Self::Output> {
        ControlFlow::Continue(())
    }
}

Example: Stop After Parse

Return the AST and stop the pipeline:
use std::ops::ControlFlow;
use mcc_driver::{Callbacks, Config, Outcome, run};
use mcc::{Ast, SourceFile, Text, Database, default_target};

struct StopAfterParse {
    result: Option<Ast<'static>>,
}

impl Callbacks for StopAfterParse {
    type Output = ();

    fn after_parse<'db>(
        &mut self,
        _db: &'db dyn mcc::Db,
        _source_file: SourceFile,
        ast: Ast<'db>,
        diags: Vec<&mcc::diagnostics::Diagnostics>,
    ) -> ControlFlow<Self::Output> {
        if !diags.is_empty() {
            eprintln!("Parse errors:");
            for diag in diags {
                eprintln!("  {}", diag.message);
            }
        }
        // Stop the pipeline after parsing
        ControlFlow::Break(())
    }
}

let db = Database::default();
let file = SourceFile::new(&db, "test.c".into(), "int main() {}".into());

let config = Config {
    db: db.clone(),
    target: default_target(),
    cc: "cc".into(),
    output: None,
    input: file,
};

let mut cb = StopAfterParse { result: None };
let outcome = run(&mut cb, config);

match outcome {
    Outcome::EarlyReturn(()) => println!("Stopped after parse"),
    Outcome::Ok => println!("Unexpectedly completed"),
    Outcome::Err(e) => eprintln!("Error: {}", e),
}

Example: Collect All Diagnostics

use std::ops::ControlFlow;
use mcc_driver::Callbacks;
use mcc::diagnostics::Diagnostics;

struct DiagCollector {
    all_diags: Vec<Diagnostics>,
}

impl Callbacks for DiagCollector {
    type Output = Vec<Diagnostics>;

    fn after_parse<'db>(
        &mut self,
        _db: &'db dyn mcc::Db,
        _source_file: mcc::SourceFile,
        _ast: mcc::Ast<'db>,
        diags: Vec<&Diagnostics>,
    ) -> ControlFlow<Self::Output> {
        self.all_diags.extend(diags.into_iter().cloned());
        ControlFlow::Continue(())
    }

    fn after_lower<'db>(
        &mut self,
        _db: &'db dyn mcc::Db,
        _tacky: mcc::lowering::tacky::Program<'db>,
        diags: Vec<&Diagnostics>,
    ) -> ControlFlow<Self::Output> {
        self.all_diags.extend(diags.into_iter().cloned());
        ControlFlow::Continue(())
    }

    fn after_codegen<'db>(
        &mut self,
        _db: &'db dyn mcc::Db,
        _asm: mcc::codegen::asm::Program<'db>,
        diags: Vec<&Diagnostics>,
    ) -> ControlFlow<Self::Output> {
        self.all_diags.extend(diags.into_iter().cloned());
        ControlFlow::Continue(())
    }

    fn after_render_assembly(
        &mut self,
        _db: &dyn mcc::Db,
        _asm: mcc::Text,
        diags: Vec<&Diagnostics>,
    ) -> ControlFlow<Self::Output> {
        self.all_diags.extend(diags.into_iter().cloned());
        ControlFlow::Continue(())
    }
}

Example: Write Assembly to File

use std::{fs, ops::ControlFlow, path::PathBuf};
use mcc_driver::Callbacks;
use mcc::Text;

struct AsmWriter {
    output_path: PathBuf,
}

impl Callbacks for AsmWriter {
    type Output = ();

    fn after_render_assembly(
        &mut self,
        _db: &dyn mcc::Db,
        asm: Text,
        _diags: Vec<&mcc::diagnostics::Diagnostics>,
    ) -> ControlFlow<Self::Output> {
        fs::write(&self.output_path, asm.as_str()).unwrap();
        println!("Wrote assembly to {}", self.output_path.display());
        // Stop after writing assembly
        ControlFlow::Break(())
    }
}

Best Practices

Check for errors in diagnostics and handle them appropriately:
fn after_parse<'db>(
    &mut self,
    db: &'db dyn mcc::Db,
    file: SourceFile,
    ast: Ast<'db>,
    diags: Vec<&Diagnostics>,
) -> ControlFlow<Self::Output> {
    if diags.iter().any(|d| d.severity == Severity::Error) {
        // Stop on error
        return ControlFlow::Break(ExitCode::FAILURE);
    }
    ControlFlow::Continue(())
}
If you only need certain stages (e.g., parse + typecheck for a linter), use ControlFlow::Break to skip later stages:
fn after_lower<'db>(
    &mut self,
    _db: &'db dyn mcc::Db,
    tacky: tacky::Program<'db>,
    _diags: Vec<&Diagnostics>,
) -> ControlFlow<Self::Output> {
    // Analyze TAC, then stop
    self.analyze(tacky);
    ControlFlow::Break(self.result())
}
The trait provides default implementations for all methods. Override only what you need:
impl Callbacks for MyTool {
    type Output = ();
    
    // Only implement after_codegen
    fn after_codegen<'db>(
        &mut self,
        db: &'db dyn mcc::Db,
        asm: asm::Program<'db>,
        diags: Vec<&Diagnostics>,
    ) -> ControlFlow<Self::Output> {
        // Custom logic
    }
}

API Overview

Learn about the core compilation API

Diagnostics

Handle errors collected in callbacks

Database

Understand the compilation database

CLI

See how the CLI uses callbacks internally

Build docs developers (and LLMs) love