Backend documentation work.
This commit is contained in:
@@ -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<ObjectModule>`, 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<JITModule>`, 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 error;
|
||||||
mod eval;
|
mod eval;
|
||||||
mod into_crane;
|
mod into_crane;
|
||||||
@@ -16,6 +47,15 @@ use target_lexicon::Triple;
|
|||||||
|
|
||||||
const EMPTY_DATUM: [u8; 8] = [0; 8];
|
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<M: Module> {
|
pub struct Backend<M: Module> {
|
||||||
pub module: M,
|
pub module: M,
|
||||||
data_ctx: DataContext,
|
data_ctx: DataContext,
|
||||||
@@ -26,6 +66,12 @@ pub struct Backend<M: Module> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Backend<JITModule> {
|
impl Backend<JITModule> {
|
||||||
|
/// 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<String>) -> Result<Self, BackendError> {
|
pub fn jit(output_buffer: Option<String>) -> Result<Self, BackendError> {
|
||||||
let platform = Triple::host();
|
let platform = Triple::host();
|
||||||
let isa_builder = isa::lookup(platform.clone())?;
|
let isa_builder = isa::lookup(platform.clone())?;
|
||||||
@@ -50,12 +96,24 @@ impl Backend<JITModule> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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 {
|
pub fn bytes(&self, function_id: FuncId) -> *const u8 {
|
||||||
self.module.get_finalized_function(function_id)
|
self.module.get_finalized_function(function_id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Backend<ObjectModule> {
|
impl Backend<ObjectModule> {
|
||||||
|
/// 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<Self, BackendError> {
|
pub fn object_file(platform: Triple) -> Result<Self, BackendError> {
|
||||||
let isa_builder = isa::lookup(platform.clone())?;
|
let isa_builder = isa::lookup(platform.clone())?;
|
||||||
let mut settings_builder = settings::builder();
|
let mut settings_builder = settings::builder();
|
||||||
@@ -76,12 +134,22 @@ impl Backend<ObjectModule> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Given all the functions defined, return the bytes the object file should contain.
|
||||||
pub fn bytes(self) -> Result<Vec<u8>, BackendError> {
|
pub fn bytes(self) -> Result<Vec<u8>, BackendError> {
|
||||||
self.module.finish().emit().map_err(Into::into)
|
self.module.finish().emit().map_err(Into::into)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<M: Module> Backend<M> {
|
impl<M: Module> Backend<M> {
|
||||||
|
/// 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<DataId, BackendError> {
|
pub fn define_string(&mut self, s: &str) -> Result<DataId, BackendError> {
|
||||||
let name = format!("<string_constant>{}", s);
|
let name = format!("<string_constant>{}", s);
|
||||||
let s0 = format!("{}\0", s);
|
let s0 = format!("{}\0", s);
|
||||||
@@ -97,6 +165,11 @@ impl<M: Module> Backend<M> {
|
|||||||
Ok(global_id)
|
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<DataId, BackendError> {
|
pub fn define_variable(&mut self, name: String) -> Result<DataId, BackendError> {
|
||||||
self.data_ctx.define(Box::new(EMPTY_DATUM));
|
self.data_ctx.define(Box::new(EMPTY_DATUM));
|
||||||
let id = self
|
let id = self
|
||||||
@@ -108,6 +181,11 @@ impl<M: Module> Backend<M> {
|
|||||||
Ok(id)
|
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 {
|
pub fn output_buffer_ptr(&mut self) -> *mut String {
|
||||||
if let Some(str) = self.output_buffer.as_mut() {
|
if let Some(str) = self.output_buffer.as_mut() {
|
||||||
str as *mut String
|
str as *mut String
|
||||||
@@ -116,6 +194,10 @@ impl<M: Module> Backend<M> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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 {
|
pub fn output(self) -> String {
|
||||||
if let Some(s) = self.output_buffer {
|
if let Some(s) = self.output_buffer {
|
||||||
s
|
s
|
||||||
|
|||||||
Reference in New Issue
Block a user