//! # 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`, 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`, 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 { pub module: M, data_ctx: DataContext, runtime_functions: RuntimeFunctions, defined_strings: HashMap, defined_symbols: HashMap, output_buffer: Option, } 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())?; 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 { /// 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(); 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, 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); 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 { 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() } } }