diff --git a/src/bin/ngrc.rs b/src/bin/ngrc.rs index 23a9021..821b0e2 100644 --- a/src/bin/ngrc.rs +++ b/src/bin/ngrc.rs @@ -1,17 +1,7 @@ use clap::Parser; -use codespan_reporting::diagnostic::Diagnostic; -use codespan_reporting::files::SimpleFiles; -use codespan_reporting::term; -use codespan_reporting::term::termcolor::{ColorChoice, StandardStream}; -use cranelift_object::object; - -use ngr::backend::Backend; -use ngr::backend::BackendError; -use ngr::ir::Program as IR; -use ngr::syntax::{ParserError, Program as Syntax}; -use target_lexicon::Triple; -use thiserror::Error; +/// Clap is great! Even though we don't have many command line arguments +/// yet, this is just really neat. #[derive(Parser, Debug)] #[clap(author, version, about, long_about = None)] struct CommandLineArguments { @@ -23,76 +13,14 @@ struct CommandLineArguments { file: String, } -#[derive(Debug, Error)] -enum MainError { - #[error(transparent)] - Backend(#[from] BackendError), - #[error("Parser error")] - ParserError(#[from] ParserError), - #[error("IO error")] - IoError(#[from] std::io::Error), - #[error("write error")] - WriteError(#[from] object::write::Error), -} - -impl From for Diagnostic { - fn from(value: MainError) -> Self { - match value { - MainError::Backend(be) => be.into(), - MainError::ParserError(pe) => (&pe).into(), - MainError::IoError(e) => Diagnostic::error().with_message(format!("IO error: {}", e)), - MainError::WriteError(e) => { - Diagnostic::error().with_message(format!("Module write error: {}", e)) - } - } - } -} - -fn compile(file_database: &mut SimpleFiles) -> Result<(), MainError> { - let args = CommandLineArguments::parse(); - - let syntax = Syntax::parse_file(file_database, &args.file)?; - let (mut errors, mut warnings) = syntax.validate(); - let stop = !errors.is_empty(); - let messages = errors - .drain(..) - .map(Into::into) - .chain(warnings.drain(..).map(Into::into)); - let writer = StandardStream::stderr(ColorChoice::Auto); - let config = codespan_reporting::term::Config::default(); - - for message in messages { - term::emit(&mut writer.lock(), &config, file_database, &message).unwrap(); - } - - if stop { - return Ok(()); - } - - let ir = IR::from(syntax.simplify()); - let mut backend = Backend::object_file(Triple::host())?; - backend.compile_function("gogogo", ir)?; - let bytes = backend.bytes()?; - std::fs::write(args.output.unwrap_or_else(|| "output.o".to_string()), bytes)?; - Ok(()) -} - fn main() { - let mut file_database = SimpleFiles::new(); + let args = CommandLineArguments::parse(); + let mut compiler = ngr::Compiler::default(); - match compile(&mut file_database) { - Ok(()) => {} - Err(e) => { - let writer = StandardStream::stderr(ColorChoice::Auto); - let config = codespan_reporting::term::Config::default(); + let output_file = args.output.unwrap_or("output.o".to_string()); - term::emit( - &mut writer.lock(), - &config, - &file_database, - &Diagnostic::from(e), - ) - .unwrap(); - } + if let Some(bytes) = compiler.compile(&args.file) { + std::fs::write(&output_file, bytes) + .unwrap_or_else(|x| eprintln!("Could not write to file {}: {}", output_file, x)); } } diff --git a/src/compiler.rs b/src/compiler.rs new file mode 100644 index 0000000..f130e02 --- /dev/null +++ b/src/compiler.rs @@ -0,0 +1,136 @@ +use crate::backend::Backend; +use crate::ir::Program as IR; +use crate::syntax::Program as Syntax; +use codespan_reporting::{ + diagnostic::Diagnostic, + files::SimpleFiles, + term::{self, Config}, +}; +use pretty::termcolor::{ColorChoice, StandardStream}; +use target_lexicon::Triple; + +pub struct Compiler { + file_database: SimpleFiles, + console: StandardStream, + console_config: Config, +} + +impl Default for Compiler { + fn default() -> Self { + let console = StandardStream::stderr(ColorChoice::Auto); + Compiler::new(console, Config::default()) + } +} + +impl Compiler { + /// Create a new compiler object. + /// + /// This object can be re-used to compile as many files as you like. + /// Use this function if you want to configure your output console and/or + /// its configuration in some custom way. Alternatively, you can use the + /// `Default` implementation, which will emit information to `stderr` with + /// a reasonable default configuration. + pub fn new(console: StandardStream, console_config: Config) -> Self { + Compiler { + file_database: SimpleFiles::new(), + console, + console_config, + } + } + + /// Compile the given file, returning the object file as a vector of bytes. + /// + /// This function may create output, via the console configured with this + /// `Compiler` object. If the compilation fails for any reason, will return + /// `None`. + pub fn compile>(&mut self, input_file: P) -> Option> { + match self.compile_internal(input_file.as_ref()) { + Ok(x) => x, + Err(e) => { + self.emit(e.into()); + None + } + } + } + + // This is the actual meat of the compilation chain; we hide it from the user + // because the type is kind of unpleasant. + fn compile_internal(&mut self, input_file: &str) -> Result>, CompilerError> { + // Try to parse the file into our syntax AST. If we fail, emit the error + // and then immediately return `None`. + let syntax = Syntax::parse_file(&mut self.file_database, input_file)?; + + // Now validate the user's syntax AST. This can possibly find errors and/or + // create warnings. We can continue if we only get warnings, but need to stop + // if we get any errors. + let (mut errors, mut warnings) = syntax.validate(); + let stop = !errors.is_empty(); + let messages = errors + .drain(..) + .map(Into::into) + .chain(warnings.drain(..).map(Into::into)); + + // emit all the messages we receive; warnings *and* errors + for message in messages { + self.emit(message); + } + + // we got errors, so just stop right now. perhaps oddly, this is Ok(None); + // we've already said all we're going to say in the messags above, so there's + // no need to provide another `Err` result. + if stop { + return Ok(None); + } + + // Now that we've validated it, turn it into IR; first we simplify it, then + // we do the conversion. + let ir = IR::from(syntax.simplify()); + + // Finally, send all this to Cranelift for conversion into an object file. + let mut backend = Backend::object_file(Triple::host())?; + backend.compile_function("gogogo", ir)?; + Ok(Some(backend.bytes()?)) + } + + fn emit(&mut self, diagnostic: Diagnostic) { + term::emit( + &mut self.console.lock(), + &self.console_config, + &self.file_database, + &diagnostic, + ) + .expect("codespan reporting term::emit works"); + } +} + +// This is just a handy type that we can convert things into; it's not +// exposed outside this module, and doesn't actually do much of interest. +#[derive(Debug, thiserror::Error)] +enum CompilerError { + #[error(transparent)] + Backend(#[from] crate::backend::BackendError), + #[error(transparent)] + ParserError(#[from] crate::syntax::ParserError), + #[error(transparent)] + IoError(#[from] std::io::Error), + #[error(transparent)] + WriteError(#[from] cranelift_object::object::write::Error), +} + +// Since we're going to use codespan to report pretty much all errors, +// this just passes through most of the errors, or makes simple versions +// of `Diagnostic` for those that we don't have existing `From`s. +impl From for Diagnostic { + fn from(value: CompilerError) -> Self { + match value { + CompilerError::Backend(be) => be.into(), + CompilerError::ParserError(pe) => (&pe).into(), + CompilerError::IoError(e) => { + Diagnostic::error().with_message(format!("IO error: {}", e)) + } + CompilerError::WriteError(e) => { + Diagnostic::error().with_message(format!("Module write error: {}", e)) + } + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 71d55e5..aea5c29 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,7 @@ pub mod backend; +mod compiler; pub mod eval; pub mod ir; pub mod syntax; + +pub use crate::compiler::Compiler;