📜 Add better documentation across the compiler. (#3)
These changes pay particular attention to API endpoints, to try to ensure that any rustdocs generated are detailed and sensible. A good next step, eventually, might be to include doctest examples, as well. For the moment, it's not clear that they would provide a lot of value, though. In addition, this does a couple refactors to simplify the code base in ways that make things clearer or, at least, briefer.
This commit is contained in:
157
src/compiler.rs
Normal file
157
src/compiler.rs
Normal file
@@ -0,0 +1,157 @@
|
||||
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;
|
||||
|
||||
/// A high-level compiler for NGR programs.
|
||||
///
|
||||
/// This object can be built once, and then re-used many times to build multiple
|
||||
/// files. For most users, the [`Default`] implementation should be sufficient;
|
||||
/// it will use `stderr` for warnings and errors, with default colors based on
|
||||
/// what we discover from the terminal. For those who want to provide alternate
|
||||
/// outputs, though, the `Compiler::new` constructor is available.
|
||||
pub struct Compiler {
|
||||
file_database: SimpleFiles<String, String>,
|
||||
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<P: AsRef<str>>(&mut self, input_file: P) -> Option<Vec<u8>> {
|
||||
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.
|
||||
///
|
||||
/// The weird error type comes from the fact that we can run into three types
|
||||
/// of result:
|
||||
///
|
||||
/// * Fundamental errors, like an incorrectly formatted file or some
|
||||
/// oddity with IO. These return `Err`.
|
||||
/// * Validation errors, where we reject the program due to something
|
||||
/// semantically wrong with them. These return `Ok(None)`.
|
||||
/// * Success! In this case, we return `Ok(Some(...))`, where the bytes
|
||||
/// returned is the contents of the compiled object file.
|
||||
///
|
||||
fn compile_internal(&mut self, input_file: &str) -> Result<Option<Vec<u8>>, 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.
|
||||
let ir = IR::from(syntax);
|
||||
|
||||
// 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()?))
|
||||
}
|
||||
|
||||
/// Emit a diagnostic.
|
||||
///
|
||||
/// This is just a really handy shorthand we use elsewhere in the object, because
|
||||
/// there's a lot of boilerplate we'd like to skip.
|
||||
fn emit(&mut self, diagnostic: Diagnostic<usize>) {
|
||||
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<CompilerError> for Diagnostic<usize> {
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user