Files
ngr/src/backend.rs

206 lines
8.6 KiB
Rust

//! # The compiler backend: generation of machine code, both static and JIT.
//!
//! 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`],
//! 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`], 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`] object is there to
//! help provide a clean interface to them all.
mod error;
mod eval;
mod into_crane;
mod runtime;
use std::collections::HashMap;
pub use self::error::BackendError;
pub use self::runtime::{RuntimeFunctionError, RuntimeFunctions};
use cranelift_codegen::settings::Configurable;
use cranelift_codegen::{isa, settings};
use cranelift_jit::{JITBuilder, JITModule};
use cranelift_module::{default_libcall_names, DataContext, DataId, FuncId, Linkage, Module};
use cranelift_object::{ObjectBuilder, ObjectModule};
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<M: Module> {
pub module: M,
data_ctx: DataContext,
runtime_functions: RuntimeFunctions,
defined_strings: HashMap<String, DataId>,
defined_symbols: HashMap<String, DataId>,
output_buffer: Option<String>,
}
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> {
let platform = Triple::host();
let isa_builder = isa::lookup(platform.clone())?;
let mut settings_builder = settings::builder();
settings_builder.set("use_colocated_libcalls", "false")?;
settings_builder.set("is_pic", "false")?;
let isa = isa_builder.finish(settings::Flags::new(settings_builder))?;
let mut builder = JITBuilder::with_isa(isa, cranelift_module::default_libcall_names());
RuntimeFunctions::register_jit_implementations(&mut builder);
let mut module = JITModule::new(builder);
let runtime_functions = RuntimeFunctions::new(&platform, &mut module)?;
Ok(Backend {
module,
data_ctx: DataContext::new(),
runtime_functions,
defined_strings: HashMap::new(),
defined_symbols: HashMap::new(),
output_buffer,
})
}
/// 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<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> {
let isa_builder = isa::lookup(platform.clone())?;
let mut settings_builder = settings::builder();
settings_builder.set("is_pic", "true")?;
let isa = isa_builder.finish(settings::Flags::new(settings_builder))?;
let object_builder = ObjectBuilder::new(isa, "example", default_libcall_names())?;
let mut module = ObjectModule::new(object_builder);
let runtime_functions = RuntimeFunctions::new(&platform, &mut module)?;
Ok(Backend {
module,
data_ctx: DataContext::new(),
runtime_functions,
defined_strings: HashMap::new(),
defined_symbols: HashMap::new(),
output_buffer: None,
})
}
/// Given all the functions defined, return the bytes the object file should contain.
pub fn bytes(self) -> Result<Vec<u8>, BackendError> {
self.module.finish().emit().map_err(Into::into)
}
}
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> {
let name = format!("<string_constant>{}", s);
let s0 = format!("{}\0", s);
let global_id = self
.module
.declare_data(&name, Linkage::Local, false, false)?;
let mut data_context = DataContext::new();
data_context.set_align(8);
data_context.define(s0.into_boxed_str().into_boxed_bytes());
self.module.define_data(global_id, &data_context)?;
self.defined_strings.insert(s.to_owned(), 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> {
self.data_ctx.define(Box::new(EMPTY_DATUM));
let id = self
.module
.declare_data(&name, Linkage::Export, true, false)?;
self.module.define_data(id, &self.data_ctx)?;
self.data_ctx.clear();
self.defined_symbols.insert(name, 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 {
if let Some(str) = self.output_buffer.as_mut() {
str as *mut String
} else {
std::ptr::null_mut()
}
}
/// 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
} else {
String::new()
}
}
}