diff --git a/src/backend.rs b/src/backend.rs index f70a52f..6e10075 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -1,3 +1,34 @@ +//! # The compiler backend! +//! +//! This module is responsible for taking our intermediate representation from +//! [`crate::ir`] and turning it into Cranelift and then into object code that +//! can either be saved to disk or run in memory. Because the runtime functions +//! for NGR are very closely tied to the compiler implentation, we also include +//! information about these functions as part of the module. +//! +//! ## Using the `Backend` +//! +//! The backend of this compiler can be used in two modes: a static compilation +//! mode, where the goal is to write the compiled object to disk and then link +//! it later, and a JIT mode, where the goal is to write the compiled object to +//! memory and then run it. Both modes use the same `Backend` object, because +//! they share a lot of behaviors. However, you'll want to use different variants +//! based on your goals: +//! +//! * Use `Backend`, constructed via +//! [`Backend::object_file`](self::backend::Backend::object_file), if you +//! want to compile to an object file on disk, which you're then going to +//! link to later. +//! * Use `Backend`, constructed via +//! [`Backend::jit`](self::backend::Backend::jit), if you want to do +//! just-in-time compilation and are just going to run things immediately. +//! +//! ## Working with Runtime Functions +//! +//! For now, runtime functions are pretty easy to describe, because there's +//! only one. In the future, though, the +//! [`RuntimeFunctions`](self::backend::RuntimeFunctions) object is there to +//! help provide a clean interface to them all. mod error; mod eval; mod into_crane; @@ -16,6 +47,15 @@ use target_lexicon::Triple; const EMPTY_DATUM: [u8; 8] = [0; 8]; +/// An object representing an active backend. +/// +/// Internally, this object holds a bunch of state useful for compiling one +/// or more functions into an object file or memory. It can be passed around, +/// but cannot currently be duplicated because some of that state is not +/// easily duplicated. You should be able to share this across threads, assuming +/// normal Rust safety, but you should be thoughtful about transferring it across +/// processes in a JIT context due to some special cases in the runtime function +/// implementations. pub struct Backend { pub module: M, data_ctx: DataContext, @@ -26,6 +66,12 @@ pub struct Backend { } impl Backend { + /// Create a new JIT backend for compiling NGR into memory. + /// + /// The provided output buffer is not for the compiled code, but for the output + /// of any `print` expressions that are evaluated. If set to `None`, the output + /// will be written to `stdout` as per normal, but if a String buffer is provided, + /// it will be extended by any `print` statements that happen during code execution. pub fn jit(output_buffer: Option) -> Result { let platform = Triple::host(); let isa_builder = isa::lookup(platform.clone())?; @@ -50,12 +96,24 @@ impl Backend { }) } + /// Given a compiled function ID, get a pointer to where that function was written + /// in memory. + /// + /// The data at this pointer should not be mutated unless you really, really, + /// really know what you're doing. It can be run by casting it into a Rust + /// `fn() -> ()`, and then calling it from normal Rust. pub fn bytes(&self, function_id: FuncId) -> *const u8 { self.module.get_finalized_function(function_id) } } impl Backend { + /// Generate a backend for compiling into an object file for the given target. + /// + /// This backend will generate a single output file per `Backend` object, although + /// that file may have multiple functions defined within it. Data between those + /// functions (in particular, strings) will be defined once and shared between + /// the different functions. pub fn object_file(platform: Triple) -> Result { let isa_builder = isa::lookup(platform.clone())?; let mut settings_builder = settings::builder(); @@ -76,12 +134,22 @@ impl Backend { }) } + /// Given all the functions defined, return the bytes the object file should contain. pub fn bytes(self) -> Result, BackendError> { self.module.finish().emit().map_err(Into::into) } } impl Backend { + /// Define a string within the current backend. + /// + /// Note that this is a Cranelift [`DataId`], which then must be redeclared inside the + /// context of any functions or data items that want to use it. That being said, the + /// string value will be defined once in the file and then shared by all referencers. + /// + /// This function will automatically add a null character (`'\0'`) to the end of the + /// string, to ensure that strings are non-terminated for interactions with other + /// languages. pub fn define_string(&mut self, s: &str) -> Result { let name = format!("{}", s); let s0 = format!("{}\0", s); @@ -97,6 +165,11 @@ impl Backend { Ok(global_id) } + /// Define a global variable within the current backend. + /// + /// These variables can be shared between functions, and will be exported from the + /// module itself as public data in the case of static compilation. There initial + /// value will be null. pub fn define_variable(&mut self, name: String) -> Result { self.data_ctx.define(Box::new(EMPTY_DATUM)); let id = self @@ -108,6 +181,11 @@ impl Backend { Ok(id) } + /// Get a pointer to the output buffer for `print`ing, or `null`. + /// + /// As suggested, returns `null` in the case where the user has not provided an + /// output buffer; it is your responsibility to check for this case and do + /// something sensible. pub fn output_buffer_ptr(&mut self) -> *mut String { if let Some(str) = self.output_buffer.as_mut() { str as *mut String @@ -116,6 +194,10 @@ impl Backend { } } + /// Get any captured output `print`ed by the program during execution. + /// + /// If an output buffer was not provided, or if the program has not done any + /// printing, then this function will return an empty string. pub fn output(self) -> String { if let Some(s) = self.output_buffer { s