diff --git a/.gitignore b/.gitignore index ea8c4bf..eb5a316 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -/target +target diff --git a/Cargo.lock b/Cargo.lock index b22cc33..a80ede4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -68,9 +68,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.95" +version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" dependencies = [ "backtrace", ] @@ -220,10 +220,15 @@ dependencies = [ "anyhow", "clap", "console", + "mcc-preprocessor", "tempfile", "thiserror", ] +[[package]] +name = "mcc-preprocessor" +version = "0.1.0" + [[package]] name = "memchr" version = "2.7.4" diff --git a/Cargo.toml b/Cargo.toml index c4910bf..cb59d1e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,14 @@ +[workspace] +members = ["preprocessor"] + [package] name = "mcc" version = "0.1.0" edition = "2024" [dependencies] +mcc-preprocessor = { path = "./preprocessor" } + anyhow = { version = "1.0.95", features = ["backtrace"] } clap = { version = "4.5.23", features = ["derive"] } console = "0.15.10" diff --git a/preprocessor/Cargo.lock b/preprocessor/Cargo.lock new file mode 100644 index 0000000..0491a60 --- /dev/null +++ b/preprocessor/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "mcc-preprocessor" +version = "0.1.0" diff --git a/preprocessor/Cargo.toml b/preprocessor/Cargo.toml new file mode 100644 index 0000000..cfb7565 --- /dev/null +++ b/preprocessor/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "mcc-preprocessor" +version = "0.1.0" +edition = "2024" + +[dependencies] diff --git a/preprocessor/src/lib.rs b/preprocessor/src/lib.rs new file mode 100644 index 0000000..5bba478 --- /dev/null +++ b/preprocessor/src/lib.rs @@ -0,0 +1,197 @@ +use std::collections::BTreeMap; +use std::str::FromStr; + +pub fn main(input: &str) -> String { + let mut defines: BTreeMap<&str, &str> = BTreeMap::new(); + let mut cond_counter = CondCounter::default(); + + input + .lines() + .filter_map(|line| -> Option { + match line.strip_prefix('#') { + // Line contains a preprocessor directive + Some(rest) => { + handle_directive(rest, &mut defines, &mut cond_counter); + None + } + + // Line does not contain a preprocessor directive + None => { + if !cond_counter.currently_active() { + return None; + } + + // TODO: This is horrible performance wise, right? + let mut line: String = line.to_owned(); + for (define, replace_with) in &defines { + line = line.replace(define, replace_with); + } + + Some(line) + } + } + }) + .collect() +} + +enum Directive { + // Macros + Define, + Undef, + + // Conditionals + // If, + // Elif, + Else, + Endif, + + // Macro conditionals + Ifdef, + Ifndef, + Elifdef, + Elifndef, + + // Output + Error, + Warning, +} + +impl FromStr for Directive { + type Err = String; + fn from_str(s: &str) -> Result { + match s { + "define" => Ok(Self::Define), + "undef" => Ok(Self::Undef), + + // "if" => Ok(Self::If), + // "elif" => Ok(Self::Elif), + "else" => Ok(Self::Else), + "endif" => Ok(Self::Endif), + + "ifdef" => Ok(Self::Ifdef), + "ifndef" => Ok(Self::Ifndef), + "elifdef" => Ok(Self::Elifdef), + "elifndef" => Ok(Self::Elifndef), + + "error" => Ok(Self::Error), + "warning" => Ok(Self::Warning), + + _ => Err(format!("unknown preprocessor directive: {s}")), + } + } +} + +fn handle_directive<'a>( + rest: &'a str, + defines: &mut BTreeMap<&'a str, &'a str>, + cond_counter: &mut CondCounter, +) { + let (directive, rest) = rest.split_once(char::is_whitespace).unwrap_or((rest, "")); + + // TODO: Handle parsing error + let directive: Directive = directive.parse().unwrap(); + + match directive { + Directive::Define => { + if cond_counter.currently_active() { + let (name, remains) = rest.split_once(char::is_whitespace).unwrap(); + defines.insert(name, remains); + } + } + Directive::Undef => { + if cond_counter.currently_active() { + let name = rest; + assert!(!name.chars().any(char::is_whitespace)); + defines.remove(name).unwrap(); + } + } + + Directive::Else => { + assert!(rest.is_empty()); + cond_counter.else_if(|| true); + } + Directive::Endif => { + assert!(rest.is_empty()); + cond_counter.pop().unwrap(); + } + + Directive::Ifdef => { + let name = rest; + assert!(!name.chars().any(char::is_whitespace)); + cond_counter.push(defines.contains_key(name)); + } + Directive::Ifndef => { + let name = rest; + assert!(!name.chars().any(char::is_whitespace)); + cond_counter.push(!defines.contains_key(name)); + } + Directive::Elifdef => { + let name = rest; + assert!(!name.chars().any(char::is_whitespace)); + cond_counter.else_if(|| defines.contains_key(name)); + } + Directive::Elifndef => { + let name = rest; + assert!(!name.chars().any(char::is_whitespace)); + cond_counter.else_if(|| !defines.contains_key(name)); + } + + Directive::Error => { + if cond_counter.currently_active() { + panic!("{rest}"); + } + } + Directive::Warning => { + if cond_counter.currently_active() { + eprintln!("{rest}"); + } + } + } +} + +use utils::*; +mod utils { + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub enum CondState { + Active, + Inactive { was_active: bool }, + } + + #[derive(Debug, Default)] + pub struct CondCounter(Vec); + + impl CondCounter { + pub fn push(&mut self, active: bool) { + self.0.push(match active { + true => CondState::Active, + false => CondState::Inactive { was_active: false }, + }); + } + + pub fn currently_active(&self) -> bool { + self.0 + .last() + .copied() + .map(|state| state == CondState::Active) + .unwrap_or(true) + } + + // TODO: Not sure I like this name + pub fn else_if(&mut self, pred: impl Fn() -> bool) { + let current = self.0.last_mut().unwrap(); + match *current { + CondState::Active => *current = CondState::Inactive { was_active: true }, + CondState::Inactive { was_active: true } => { /* nothing to do */ } + CondState::Inactive { was_active: false } => { + if pred() { + *current = CondState::Active; + } + } + } + } + + pub fn pop(&mut self) -> Option { + self.0.pop() + } + } +} diff --git a/resources/with_macros.c b/resources/with_macros.c new file mode 100644 index 0000000..48512e9 --- /dev/null +++ b/resources/with_macros.c @@ -0,0 +1,35 @@ +#define VALUE 3 * 2 + +int main(void) { +#ifdef PIZZA + return 10; +#endif + +#ifndef VALUE + int a = 2; +#else + int a = 10; +#endif + +#ifdef PIZZA + int b = 0; +#elifndef VALUE + int b = 1; +#elifdef VALUE + int b = 2; +#elifdef VALUE + int b = 3; +#else + int b = 4; +#endif + +#ifdef BURGER +#warning "Burger is defined" +#elifndef PIZZA +#warning "PIZZA is not defined" +#else +#error "This should not happen" +#endif + + return a * b * VALUE; +} diff --git a/src/lib.rs b/src/lib.rs index 23aa130..998c678 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -184,7 +184,7 @@ pub fn compiler( anyhow!("failed to lex file") })?; if options.lex { - println!("{tokens:?}"); + println!("{tokens:#?}"); return Ok(false); } @@ -221,3 +221,12 @@ pub fn compiler( Ok(true) } + +pub fn preprocessor(input: &Path, output: &Path) -> Result<(), anyhow::Error> { + let input_content = + std::fs::read_to_string(input).context("failed to read input file for preprocessor")?; + let output_content = mcc_preprocessor::main(&input_content); + std::fs::write(output, output_content.as_bytes()) + .context("failed to write preprocessor output to file")?; + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index 59e7a50..8dc7f74 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ use std::path::{Path, PathBuf}; use std::process::Command; +use std::str::FromStr; use anyhow::Context; use clap::Parser; @@ -12,6 +13,38 @@ struct Cli { #[clap(flatten)] options: Options, + + #[clap(short, long, default_value_t = Preprocessor::Mcc)] + preprocessor: Preprocessor, + + #[clap(long)] + only_preprocess: bool, +} + +#[derive(Debug, Clone, Copy)] +enum Preprocessor { + Mcc, + Gcc, +} + +impl std::fmt::Display for Preprocessor { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Mcc => f.write_str("mcc"), + Self::Gcc => f.write_str("gcc"), + } + } +} + +impl FromStr for Preprocessor { + type Err = String; + fn from_str(s: &str) -> Result { + match s { + "mcc" => Ok(Self::Mcc), + "gcc" => Ok(Self::Gcc), + _ => Err(format!("'{s}' is not a valid preprocessor")), + } + } } fn main() -> Result<(), anyhow::Error> { @@ -34,7 +67,16 @@ fn main() -> Result<(), anyhow::Error> { let tempdir = tempfile::tempdir().context("failed to create tempdir")?; let preprocessed_file = tempdir.path().join(filename).with_extension("i"); - preprocessor(&cli.filepath, &preprocessed_file)?; + match cli.preprocessor { + Preprocessor::Mcc => mcc::preprocessor(&cli.filepath, &preprocessed_file)?, + Preprocessor::Gcc => gcc::preprocessor(&cli.filepath, &preprocessed_file)?, + } + + if cli.only_preprocess { + let content = std::fs::read_to_string(&preprocessed_file).unwrap(); + println!("{content}"); + return Ok(()); + } let assembly_file = tempdir.path().join(filename).with_extension("s"); @@ -49,7 +91,7 @@ fn main() -> Result<(), anyhow::Error> { Ok(_keep_going @ false) => Ok(()), Ok(_keep_going @ true) => { let executable_file = basedir.join(filename).with_extension(""); - produce_executable(&assembly_file, &executable_file)?; + gcc::produce_executable(&assembly_file, &executable_file)?; Ok(()) } Err(maybe_error_message) => { @@ -61,42 +103,46 @@ fn main() -> Result<(), anyhow::Error> { } } -fn preprocessor(input: &Path, output: &Path) -> Result<(), anyhow::Error> { - let output = Command::new("gcc") - .arg("-E") - .arg("-P") - .arg(input) - .arg("-o") - .arg(output) - .output() - .context("failed to run `gcc` for preprocessing")?; - - if !output.status.success() { - return Err(anyhow::anyhow!( - "Process failed:\n --- stdout ---\n{}\n\n --- stderr --- \n{}\n\n", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr), - )); +mod gcc { + use super::*; + + pub fn preprocessor(input: &Path, output: &Path) -> Result<(), anyhow::Error> { + let output = Command::new("gcc") + .arg("-E") + .arg("-P") + .arg(input) + .arg("-o") + .arg(output) + .output() + .context("failed to run `gcc` for preprocessing")?; + + if !output.status.success() { + return Err(anyhow::anyhow!( + "Process failed:\n --- stdout ---\n{}\n\n --- stderr --- \n{}\n\n", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + )); + } + + Ok(()) } - Ok(()) -} + pub fn produce_executable(input: &Path, output: &Path) -> Result<(), anyhow::Error> { + let output = Command::new("gcc") + .arg(input) + .arg("-o") + .arg(output) + .output() + .unwrap(); + + if !output.status.success() { + return Err(anyhow::anyhow!( + "Process failed:\n --- stdout ---\n{}\n\n --- stderr --- \n{}\n\n", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + )); + } -fn produce_executable(input: &Path, output: &Path) -> Result<(), anyhow::Error> { - let output = Command::new("gcc") - .arg(input) - .arg("-o") - .arg(output) - .output() - .unwrap(); - - if !output.status.success() { - return Err(anyhow::anyhow!( - "Process failed:\n --- stdout ---\n{}\n\n --- stderr --- \n{}\n\n", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr), - )); + Ok(()) } - - Ok(()) }