diff --git a/src/data/lex.rs b/src/data/lex.rs index eb59b433..813fc03d 100644 --- a/src/data/lex.rs +++ b/src/data/lex.rs @@ -427,9 +427,7 @@ pub(crate) mod test { } /// Create a new preprocessor with `s` as the input, but without a trailing newline pub(crate) fn cpp_no_newline(s: &str) -> PreProcessor { - let mut files: Files = Default::default(); - let id = files.add("", String::new().into()); - PreProcessorBuilder::new(s, id, Box::leak(Box::new(files))).build() + PreProcessorBuilder::new(s).build() } #[test] diff --git a/src/lex/cpp.rs b/src/lex/cpp.rs index 615daa4a..c57fb3d4 100644 --- a/src/lex/cpp.rs +++ b/src/lex/cpp.rs @@ -6,8 +6,6 @@ use std::convert::TryFrom; use std::path::{Path, PathBuf}; use std::rc::Rc; -use codespan::FileId; - use super::{Lexer, Token}; use crate::arch::TARGET; use crate::data::error::CppError; @@ -20,13 +18,9 @@ use crate::Files; /// /// Here is the example for `PreProcessor::new()` using the builder: /// ``` -/// use rcc::{Files, PreProcessorBuilder, Source}; +/// use rcc::PreProcessorBuilder; /// -/// let mut files = Files::new(); -/// let code = String::from("int main(void) { char *hello = \"hi\"; }\n").into(); -/// let src = Source { path: "example.c".into(), code: std::rc::Rc::clone(&code) }; -/// let file = files.add("example.c", src); -/// let cpp = PreProcessorBuilder::new(code, file, &mut files).build(); +/// let cpp = PreProcessorBuilder::new("int main(void) { char *hello = \"hi\"; }\n").filename("example.c").build(); /// for token in cpp { /// assert!(token.is_ok()); /// } @@ -34,10 +28,8 @@ use crate::Files; pub struct PreProcessorBuilder<'a> { /// The buffer for the starting file buf: Rc, - /// The starting file - file: FileId, - /// All known files, including files which have already been read. - files: &'a mut Files, + /// The name of the file + filename: PathBuf, /// Whether to print each token before replacement debug: bool, /// The paths to search for `#include`d files @@ -47,20 +39,19 @@ pub struct PreProcessorBuilder<'a> { } impl<'a> PreProcessorBuilder<'a> { - pub fn new>>( - buf: S, - file: FileId, - files: &'a mut Files, - ) -> PreProcessorBuilder<'a> { + pub fn new>>(buf: S) -> PreProcessorBuilder<'a> { PreProcessorBuilder { debug: false, - files, - file, + filename: PathBuf::default(), buf: buf.into(), search_path: Vec::new(), definitions: HashMap::new(), } } + pub fn filename>(mut self, name: P) -> Self { + self.filename = name.into(); + self + } pub fn debug(mut self, yes: bool) -> Self { self.debug = yes; self @@ -75,12 +66,11 @@ impl<'a> PreProcessorBuilder<'a> { } pub fn build(self) -> PreProcessor<'a> { PreProcessor::new( - self.file, self.buf, + self.filename, self.debug, self.search_path, self.definitions, - self.files, ) } } @@ -104,13 +94,9 @@ impl<'a> PreProcessorBuilder<'a> { /// Examples: /// /// ``` -/// use rcc::{Files, PreProcessor, Source}; +/// use rcc::PreProcessor; /// -/// let mut files = Files::new(); -/// let code = String::from("int main(void) { char *hello = \"hi\"; }\n").into(); -/// let src = Source { path: "example.c".into(), code: std::rc::Rc::clone(&code) }; -/// let file = files.add("example.c", src); -/// let cpp = PreProcessor::new(file, code, false, vec![], Default::default(), &mut files); +/// let cpp = PreProcessor::new("int main(void) { char *hello = \"hi\"; }\n", "example.c", false, vec![], Default::default()); /// for token in cpp { /// assert!(token.is_ok()); /// } @@ -122,7 +108,7 @@ pub struct PreProcessor<'a> { /// Each lexer represents a separate source file that is currently being processed. includes: Vec, /// All known files, including files which have already been read. - files: &'a mut Files, + files: Files, /// Note that this is a simple HashMap and not a Scope, because /// the preprocessor has no concept of scope other than `undef` definitions: HashMap, @@ -385,18 +371,12 @@ impl<'a> PreProcessor<'a> { /// but will never delete a file. /// /// The `debug` parameter specifies whether to print out tokens before replacement. - pub fn new< - 'files: 'a, - 'search: 'a, - I: IntoIterator>, - S: Into>, - >( - file: FileId, + pub fn new<'search: 'a, I: IntoIterator>, S: Into>>( chars: S, + filename: impl Into, debug: bool, user_search_path: I, user_definitions: HashMap, - files: &'files mut Files, ) -> Self { let system_path = format!( "{}-{}-{}", @@ -424,6 +404,16 @@ impl<'a> PreProcessor<'a> { Path::new("/usr/include").into(), ]; search_path.extend(user_search_path.into_iter()); + + let mut files = Files::new(); + let chars = chars.into(); + let filename = filename.into(); + let source = crate::Source { + code: Rc::clone(&chars), + path: filename.clone().into(), + }; + let file = files.add(filename, source); + Self { first_lexer: Lexer::new(file, chars, debug), includes: Default::default(), @@ -459,6 +449,22 @@ impl<'a> PreProcessor<'a> { std::mem::take(&mut self.error_handler.warnings) } + /// Return a `Location` representing the end of the first file. + pub fn eof(&self) -> Location { + let lex = &self.first_lexer; + Location { + span: (lex.chars.len() as u32..lex.chars.len() as u32).into(), + file: lex.location.file, + } + } + + /// Return all files loaded by the preprocessor, consuming it in the process. + /// + /// Files can be loaded by C source using `#include` directives. + pub fn into_files(self) -> Files { + self.files + } + /* internal functions */ /// Return all tokens from the current position until the end of the current line. /// diff --git a/src/lib.rs b/src/lib.rs index e76f1f53..0511c0b1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,7 +14,6 @@ use std::process::Command; use std::rc::Rc; pub use codespan; -use codespan::FileId; #[cfg(feature = "codegen")] use cranelift_module::{Backend, Module}; @@ -32,6 +31,7 @@ compile_error!("The color-backtrace feature does nothing unless used by the `rcc /// then the path will be relative to the _compiler_, not to the current file. /// This is recommended only for test code and proof of concepts, /// since it does not adhere to the C standard. +#[derive(Debug, Clone)] pub struct Source { pub code: Rc, pub path: PathBuf, @@ -49,10 +49,26 @@ pub type Product = ::Product; /// A result which includes all warnings, even for `Err` variants. /// /// If successful, this returns an `Ok(T)`. -/// If unsuccessful, this returns an `Err(VecDeque)`, -/// so you can iterate the tokens in order without consuming them. +/// If unsuccessful, this returns an `Err(E)`. /// Regardless, this always returns all warnings found. -pub type WarningResult = (Result>, VecDeque); +pub struct Program> { + /// Either the errors found while compiling the program, or the successfully compiled program. + pub result: Result, + /// The warnings emitted while compiling the program + pub warnings: VecDeque, + /// The files that were `#include`d by the preprocessor + pub files: Files, +} + +impl Program { + fn from_cpp(mut cpp: PreProcessor, result: Result) -> Self { + Program { + result, + warnings: cpp.warnings(), + files: cpp.into_files(), + } + } +} pub use analyze::Analyzer; pub use data::*; @@ -168,22 +184,24 @@ pub struct Opt { /// If None, allows an unlimited number of errors. pub max_errors: Option, - /// The directories to consider as part of the search path. + /// The directories to consider as part of the system search path. pub search_path: Vec, /// The pre-defined macros to have as part of the preprocessor. pub definitions: HashMap, + + /// The path of the original file. + /// + /// This allows looking for local includes relative to that file. + /// An empty path is allowed but not recommended; it will cause the preprocessor + /// to look for includes relative to the current directory of the process. + pub filename: PathBuf, } /// Preprocess the source and return the tokens. -pub fn preprocess( - buf: &str, - opt: Opt, - file: FileId, - files: &mut Files, -) -> WarningResult>> { +pub fn preprocess(buf: &str, opt: Opt) -> Program>> { let path = opt.search_path.iter().map(|p| p.into()); - let mut cpp = PreProcessor::new(file, buf, opt.debug_lex, path, opt.definitions, files); + let mut cpp = PreProcessor::new(buf, opt.filename, opt.debug_lex, path, opt.definitions); let mut tokens = VecDeque::new(); let mut errs = VecDeque::new(); @@ -193,23 +211,22 @@ pub fn preprocess( Err(err) => errs.push_back(err), } } - let res = if errs.is_empty() { + let result = if errs.is_empty() { Ok(tokens) } else { Err(errs) }; - (res, cpp.warnings()) + Program { + result, + warnings: cpp.warnings(), + files: cpp.into_files(), + } } /// Perform semantic analysis, including type checking and constant folding. -pub fn check_semantics( - buf: &str, - opt: Opt, - file: FileId, - files: &mut Files, -) -> WarningResult>> { +pub fn check_semantics(buf: &str, opt: Opt) -> Program>> { let path = opt.search_path.iter().map(|p| p.into()); - let mut cpp = PreProcessor::new(file, buf, opt.debug_lex, path, opt.definitions, files); + let mut cpp = PreProcessor::new(buf, opt.filename, opt.debug_lex, path, opt.definitions); let mut errs = VecDeque::new(); @@ -218,7 +235,7 @@ pub fn check_semantics( errs.push_back($err); if let Some(max) = opt.max_errors { if errs.len() >= max.into() { - return (Err(errs), cpp.warnings()); + return Program::from_cpp(cpp, Err(errs)); } } }}; @@ -230,18 +247,14 @@ pub fn check_semantics( None => break None, } }; - let eof = || Location { - span: (buf.len() as u32..buf.len() as u32).into(), - file, - }; let first = match first { Some(token) => token, None => { if errs.is_empty() { - errs.push_back(eof().error(SemanticError::EmptyProgram)); + errs.push_back(cpp.eof().error(SemanticError::EmptyProgram)); } - return (Err(errs), cpp.warnings()); + return Program::from_cpp(cpp, Err(errs)); } }; @@ -253,33 +266,42 @@ pub fn check_semantics( Err(err) => handle_err!(err), } } - if hir.is_empty() && errs.is_empty() { - errs.push_back(eof().error(SemanticError::EmptyProgram)); - } let mut warnings = parser.warnings(); warnings.extend(cpp.warnings()); - let res = if !errs.is_empty() { Err(errs) } else { Ok(hir) }; - (res, warnings) + if hir.is_empty() && errs.is_empty() { + errs.push_back(cpp.eof().error(SemanticError::EmptyProgram)); + } + let result = if !errs.is_empty() { Err(errs) } else { Ok(hir) }; + Program { + result, + warnings, + files: cpp.into_files(), + } } #[cfg(feature = "codegen")] /// Compile and return the declarations and warnings. -pub fn compile( - module: Module, - buf: &str, - opt: Opt, - file: FileId, - files: &mut Files, -) -> (Result, Error>, VecDeque) { +pub fn compile(module: Module, buf: &str, opt: Opt) -> Program> { let debug_asm = opt.debug_asm; - let (hir, mut warnings) = match check_semantics(buf, opt, file, files) { - (Err(errs), warnings) => return (Err(Error::Source(errs)), warnings), - (Ok(hir), warnings) => (hir, warnings), + let mut program = check_semantics(buf, opt); + let hir = match program.result { + Ok(hir) => hir, + Err(err) => { + return Program { + result: Err(err), + warnings: program.warnings, + files: program.files, + } + } }; let (result, ir_warnings) = ir::compile(module, hir, debug_asm); - warnings.extend(ir_warnings); - (result.map_err(Error::from), warnings) + program.warnings.extend(ir_warnings); + Program { + result: result.map_err(|errs| vec_deque![errs]), + warnings: program.warnings, + files: program.files, + } } #[cfg(feature = "codegen")] @@ -349,28 +371,26 @@ mod jit { impl TryFrom> for JIT { type Error = Error; - fn try_from(program: Rc) -> Result { - JIT::from_string(program, Opt::default()).0 + fn try_from(source: Rc) -> Result { + JIT::from_string(source, Opt::default()).result } } impl JIT { /// Compile string and return JITed code. - pub fn from_string>>( - program: R, - opt: Opt, - ) -> (Result, VecDeque) { - let program = program.into(); + pub fn from_string>>(source: R, opt: Opt) -> Program { + let source = source.into(); let module = initialize_jit_module(); - let mut files = Files::new(); - let source = Source { - path: PathBuf::new(), - code: Rc::clone(&program), + let program = compile(module, &source, opt); + let result = match program.result { + Ok(module) => Ok(JIT::from(module)), + Err(errs) => Err(errs.into()), }; - let file = files.add("", source); - let (result, warnings) = compile(module, &program, opt, file, &mut files); - let result = result.map(JIT::from); - (result, warnings) + Program { + result, + warnings: program.warnings, + files: program.files, + } } /// Invoke this function before trying to get access to "new" compiled functions. @@ -451,9 +471,7 @@ mod tests { use super::*; fn compile(src: &str) -> Result, Error> { let options = Opt::default(); - let mut files: Files = Default::default(); - let id = files.add("", src.into()); - let res = super::check_semantics(src, options, id, &mut files).0; + let res = super::check_semantics(src, options).result; match res { Ok(decls) => Ok(decls.into_iter().map(|l| l.data).collect()), Err(errs) => Err(Error::Source(errs)), diff --git a/src/main.rs b/src/main.rs index cc4da5ff..00b79891 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,13 +8,12 @@ use std::rc::Rc; use std::sync::atomic::{AtomicUsize, Ordering}; use ansi_term::{ANSIString, Colour}; -use codespan::FileId; use git_testament::git_testament_macros; use pico_args::Arguments; use rcc::{ assemble, compile, data::{error::CompileWarning, Location}, - link, preprocess, Error, Files, Opt, + link, preprocess, Error, Files, Opt, Program, }; use std::ffi::OsStr; use tempfile::NamedTempFile; @@ -77,9 +76,6 @@ struct BinOpt { /// Note that preprocessing discards whitespace and comments. /// There is not currently a way to disable this behavior. preprocess_only: bool, - /// The file to read C source from. \"-\" means stdin (use ./- to read a file called '-'). - /// Only one file at a time is currently accepted. [default: -] - filename: PathBuf, /// Whether or not to use color color: ColorChoice, } @@ -113,24 +109,31 @@ impl std::str::FromStr for ColorChoice { } } +macro_rules! rcc_try { + ($res: expr, $files: expr) => { + match $res { + Ok(program) => program, + Err(err) => return Err((err.into(), $files)), + } + }; +} + // TODO: when std::process::termination is stable, make err_exit an impl for CompileError // TODO: then we can move this into `main` and have main return `Result<(), Error>` -fn real_main( - buf: Rc, - file_db: &mut Files, - file_id: FileId, - bin_opt: BinOpt, - output: &Path, -) -> Result<(), Error> { +fn real_main(buf: Rc, bin_opt: BinOpt, output: &Path) -> Result<(), (Error, Files)> { let opt = if bin_opt.preprocess_only { use std::io::{BufWriter, Write}; - let (tokens, warnings) = preprocess(&buf, bin_opt.opt, file_id, file_db); - handle_warnings(warnings, file_db, bin_opt.color); + let Program { + result: tokens, + warnings, + files, + } = preprocess(&buf, bin_opt.opt); + handle_warnings(warnings, &files, bin_opt.color); let stdout = io::stdout(); let mut stdout_buf = BufWriter::new(stdout.lock()); - for token in tokens.map_err(Error::Source)? { + for token in rcc_try!(tokens, files) { write!(stdout_buf, "{} ", token.data).expect("failed to write to stdout"); } writeln!(stdout_buf).expect("failed to write to stdout"); @@ -142,12 +145,16 @@ fn real_main( #[cfg(feature = "jit")] { if !opt.jit { - aot_main(&buf, opt, file_id, file_db, output, bin_opt.color) + aot_main(&buf, opt, output, bin_opt.color) } else { let module = rcc::initialize_jit_module(); - let (result, warnings) = compile(module, &buf, opt, file_id, file_db); - handle_warnings(warnings, file_db, bin_opt.color); - let mut rccjit = rcc::JIT::from(result?); + let Program { + result, + warnings, + files, + } = compile(module, &buf, opt); + handle_warnings(warnings, &files, bin_opt.color); + let mut rccjit = rcc::JIT::from(rcc_try!(result, files)); if let Some(exit_code) = unsafe { rccjit.run_main() } { std::process::exit(exit_code); } @@ -155,30 +162,29 @@ fn real_main( } } #[cfg(not(feature = "jit"))] - aot_main(&buf, opt, file_id, file_db, output, bin_opt.color) + aot_main(&buf, opt, output, bin_opt.color) } #[inline] -fn aot_main( - buf: &str, - opt: Opt, - file_id: FileId, - file_db: &mut Files, - output: &Path, - color: ColorChoice, -) -> Result<(), Error> { +fn aot_main(buf: &str, opt: Opt, output: &Path, color: ColorChoice) -> Result<(), (Error, Files)> { let no_link = opt.no_link; let module = rcc::initialize_aot_module("rccmain".to_owned()); - let (result, warnings) = compile(module, buf, opt, file_id, file_db); - handle_warnings(warnings, file_db, color); - - let product = result.map(|x| x.finish())?; + let Program { + result, + warnings, + files, + } = compile(module, buf, opt); + handle_warnings(warnings, &files, color); + + let product = rcc_try!(result.map(|x| x.finish()), files); if no_link { - return assemble(product, output); + rcc_try!(assemble(product, output), files); + return Ok(()); } - let tmp_file = NamedTempFile::new()?; - assemble(product, tmp_file.as_ref())?; - link(tmp_file.as_ref(), output).map_err(io::Error::into) + let tmp_file = rcc_try!(NamedTempFile::new(), files); + rcc_try!(assemble(product, tmp_file.as_ref()), files); + rcc_try!(link(tmp_file.as_ref(), output), files); + Ok(()) } fn handle_warnings(warnings: VecDeque, file_db: &Files, color: ColorChoice) { @@ -217,33 +223,30 @@ fn main() { // NOTE: only holds valid UTF-8; will panic otherwise let mut buf = String::new(); - opt.filename = if opt.filename == PathBuf::from("-") { + opt.opt.filename = if opt.opt.filename == PathBuf::from("-") { io::stdin().read_to_string(&mut buf).unwrap_or_else(|err| { eprintln!("Failed to read stdin: {}", err); process::exit(1); }); PathBuf::from("") } else { - File::open(opt.filename.as_path()) + File::open(opt.opt.filename.as_path()) .and_then(|mut file| file.read_to_string(&mut buf)) .unwrap_or_else(|err| { - eprintln!("Failed to read {}: {}", opt.filename.to_string_lossy(), err); + eprintln!( + "Failed to read {}: {}", + opt.opt.filename.to_string_lossy(), + err + ); process::exit(1); }); - opt.filename + opt.opt.filename }; let buf: Rc<_> = buf.into(); - let source = rcc::Source { - path: opt.filename.clone(), - code: Rc::clone(&buf), - }; - - let mut file_db = Files::new(); - let file_id = file_db.add(&opt.filename, source); let max_errors = opt.opt.max_errors; let color_choice = opt.color; - real_main(buf, &mut file_db, file_id, opt, &output) - .unwrap_or_else(|err| err_exit(err, max_errors, &file_db, color_choice)); + real_main(buf, opt, &output) + .unwrap_or_else(|(err, files)| err_exit(err, max_errors, color_choice, &files)); } fn os_str_to_path_buf(os_str: &OsStr) -> Result { @@ -324,41 +327,36 @@ fn parse_args() -> Result<(BinOpt, PathBuf), pico_args::Error> { })?; definitions.insert(key.into(), def); } - Ok(( - BinOpt { - preprocess_only: input.contains(["-E", "--preprocess-only"]), - opt: Opt { - debug_lex: input.contains("--debug-lex"), - debug_asm: input.contains("--debug-ir"), - debug_ast: input.contains("--debug-ast"), - debug_hir: input.contains("--debug-hir"), - no_link: input.contains(["-c", "--no-link"]), - #[cfg(feature = "jit")] - jit: input.contains("--jit"), - max_errors, - definitions, - search_path, - }, + let bin_opt = BinOpt { + preprocess_only: input.contains(["-E", "--preprocess-only"]), + opt: Opt { + debug_lex: input.contains("--debug-lex"), + debug_asm: input.contains("--debug-ir"), + debug_ast: input.contains("--debug-ast"), + debug_hir: input.contains("--debug-hir"), + no_link: input.contains(["-c", "--no-link"]), + #[cfg(feature = "jit")] + jit: input.contains("--jit"), + max_errors, + definitions, + search_path, + // This is a little odd because `free` expects no arguments to be left, + // so we have to parse it last. filename: input .free_from_os_str(os_str_to_path_buf)? .unwrap_or_else(|| "-".into()), - color: color_choice, }, - output, - )) + color: color_choice, + }; + Ok((bin_opt, output)) } -fn err_exit( - err: Error, - max_errors: Option, - file_db: &Files, - color: ColorChoice, -) -> ! { +fn err_exit(err: Error, max_errors: Option, color: ColorChoice, files: &Files) -> ! { use Error::*; match err { Source(errs) => { for err in &errs { - error(&err.data, err.location(), file_db, color); + error(&err.data, err.location(), files, color); } if let Some(max) = max_errors { if usize::from(max) <= errs.len() { diff --git a/tests/jit.rs b/tests/jit.rs index ce3bfc6b..01b833ab 100644 --- a/tests/jit.rs +++ b/tests/jit.rs @@ -1,13 +1,13 @@ mod utils; -use rcc::{Opt, JIT}; +use rcc::{Opt, Program, JIT}; #[test] fn jit_readme() -> Result<(), Box> { let _ = env_logger::try_init(); let path = "tests/runner-tests/readme.c"; let readme = std::fs::read_to_string(path)?; - let (jit, _warnings) = JIT::from_string(readme, Opt::default()); + let Program { result: jit, .. } = JIT::from_string(readme, Opt::default()); let code = unsafe { jit?.run_main() }; assert_eq!(code, Some(6)); Ok(()) diff --git a/tests/utils/mod.rs b/tests/utils/mod.rs index 1c189884..bd215f9b 100644 --- a/tests/utils/mod.rs +++ b/tests/utils/mod.rs @@ -41,17 +41,17 @@ pub fn compile_and_run(program: &str, path: PathBuf, args: &[&str]) -> Result Result { - let opts = Default::default(); - let mut files = rcc::Files::default(); - let source = rcc::Source { - code: String::from(program).into(), - path, +pub fn compile( + program: &str, + filename: PathBuf, + no_link: bool, +) -> Result { + let opts = rcc::Opt { + filename, + ..Default::default() }; - let id = files.add("", source); let module = rcc::initialize_aot_module(program.to_owned()); - let (result, _warnings) = rcc::compile(module, program, opts, id, &mut files); - let module = result?.finish(); + let module = rcc::compile(module, program, opts).result?.finish(); let output = tempfile::NamedTempFile::new() .expect("cannot create tempfile") .into_temp_path();