📜 Add better documentation across the compiler. #3

Merged
acw merged 19 commits from acw/better-docs into develop 2023-05-13 12:34:48 -07:00
3 changed files with 137 additions and 7 deletions
Showing only changes of commit 9b72fb7827 - Show all commits

View File

@@ -9,6 +9,13 @@ use codespan_reporting::{
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,
@@ -53,8 +60,19 @@ impl Compiler {
}
}
// This is the actual meat of the compilation chain; we hide it from the user
// because the type is kind of unpleasant.
/// 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`.
@@ -92,6 +110,10 @@ impl Compiler {
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(),

View File

@@ -1,9 +1,79 @@
pub mod backend;
mod compiler;
pub mod eval;
pub mod ir;
mod repl;
//! # NGR (No Good Reason) Compiler
//!
//! This is the top-level module for the NGR compiler; a compiler written
//! in Rust for no good reason. I may eventually try to turn this into a
//! basic guide for writing compilers, but for now it's a fairly silly
//! (although complete) language and implementation, featuring:
//!
//! * Variable binding with basic arithmetic operators.
//! * The ability to print variable values.
//!
//! I'll be extending this list into the future, with the eventual goal of
//! being able to implement basic programming tasks with it. For example,
//! I have a goal of eventually writing reasonably-clear
//! [Advent of Code](https://adventofcode.com/) implementations with it.
//!
//! Users of this as a library will want to choose their adventure based
//! on how much they want to customize their experience; I've defaulted
//! to providing the ability to see internals, rather than masking them,
//! so folks can play with things as they see fit.
//!
//! ## Easy Mode - Just Running a REPL or Compiler
//!
//! For easiest use, you will want to use either the [`Compiler`] object
//! or the [`REPL`] object.
//!
//! As you might expect, the [`Compiler`] object builds a compiler, which
//! can be re-used to compile as many files as you'd like. Right now,
//! that's all it does. (TODO: Add a linker function to it.)
//!
//! The [`REPL`] object implements the core of what you'll need to
//! implement a just-in-time compiled read-eval-print loop. It will
//! maintain variable state and make sure that variables are linked
//! appropriately as the loop progresses.
//!
//! ## Hard Mode - Looking at the individual passes
//!
//! This compiler is broken into three core parts:
//!
//! 1. The front-end / syntax engine. This portion of the compiler is
//! responsible for turning basic strings (or files) into a machine-
//! friendly abstract syntax tree. See the [`syntax`] module for
//! more information.
//! 2. The IR. This portion of the compiler will be responsible for
//! high-level code analysis and transformation ... although for
//! now, it doesn't do much at all. See the [`ir`] module for more
//! information.
//! 3. The Backend implementation. This portion of the compiler turns
//! the IR from the previous section into Cranelift structures, and
//! helps with either compiling them via JIT or statically compiling
//! them into a file. The [`backend`] module also contains information
//! about the runtime functions made available to the user.
//!
//! ## Testing
//!
//! Testing is a key focus of this effort. To that end, both the syntax
//! tree used in the syntax module and the IR used in the middle of the
//! compiler both implement `Arbitrary`, and are subject to property-based
//! testing to make sure that various passes work properly.
//!
//! In addition, to support basic equivalence testing, we include support
//! for evaluating all expressions. The [`eval`] module provides some
//! utility support for this work.
//!
/// The front-end of the compiler: lexing, parsing, validation
pub mod syntax;
/// The middle of the compiler: analysis, simplification, optimization
pub mod ir;
/// The backend of the compiler: transformation to Cranelift, runtime
pub mod backend;
/// Helpful functions for evaluating NGR programs
pub mod eval;
/// Implementation module for the high-level compiler
mod compiler;
/// Implementation module for the high-level REPL
mod repl;
pub use crate::compiler::Compiler;
pub use crate::repl::REPL;

View File

@@ -9,6 +9,17 @@ use cranelift_module::ModuleError;
use pretty::termcolor::{ColorChoice, StandardStream};
use std::collections::HashMap;
/// A high-level REPL helper for NGR.
///
/// This object holds most of the state required to implement some
/// form of interactive compiler for NGR; all you need to do is provide
/// the actual user IO.
///
/// For most console-based used cases, the [`Default`] implementation
/// should be sufficient; it prints any warnings or errors to `stdout`,
/// using a default color scheme that should work based on the terminal
/// type. For more complex interactions, though, you may want to use
/// the `REPL::new` function to provide your own print substrate.
pub struct REPL {
file_database: SimpleFiles<String, String>,
jitter: Backend<JITModule>,
@@ -50,6 +61,12 @@ impl From<REPLError> for Diagnostic<usize> {
}
impl REPL {
/// Construct a new REPL helper, using the given stream implementation and console configuration.
///
/// For most users, the [`Default::default`] implementation will be sufficient;
/// it will use `stdout` and a default console configuration. But if you need to
/// be more specific, this will help you provide more guidance to the REPL as it
/// evaluates things.
pub fn new(console: StandardStream, console_config: Config) -> Result<Self, BackendError> {
Ok(REPL {
file_database: SimpleFiles::new(),
@@ -61,6 +78,10 @@ impl REPL {
})
}
/// Emit a diagnostic to the configured console.
///
/// This is just a convenience function; there's a lot of boilerplate in printing
/// diagnostics, and it was nice to pull it out into its own function.
fn emit_diagnostic(
&mut self,
diagnostic: Diagnostic<usize>,
@@ -73,6 +94,17 @@ impl REPL {
)
}
/// Process a line of input, printing any problems or the results.
///
/// The line number argument is just for a modicum of source information, to
/// provide to the user if some parsing or validation step fails. It can be
/// changed to be any value you like that provides some insight into what
/// failed, although it is probably a good idea for it to be different for
/// every invocation of this function. (Not critical, but a good idea.)
///
/// Any warnings or errors generated in processing this command will be
/// printed to the configured console. If there are no problems, the
/// command will be compiled and then executed.
pub fn process_input(&mut self, line_no: usize, command: String) {
if let Err(err) = self.process(line_no, command) {
if let Err(e) = self.emit_diagnostic(Diagnostic::from(err)) {
@@ -84,6 +116,12 @@ impl REPL {
}
}
/// The internal implementation, with a handy `Result` type.
///
/// All information from the documentation of `REPL::process_input` applies here,
/// as well; this is the internal implementation of that function, which is
/// differentiated by returning a `Result` type that is hidden from the user
/// in the case of `REPL::process_input`.
fn process(&mut self, line_no: usize, command: String) -> Result<(), REPLError> {
let entry = self.file_database.add("entry".to_string(), command);
let source = self