use std::fmt;
use cranelift_codegen::Context;
use cranelift_codegen::isa::OwnedTargetIsa;
use cranelift_codegen::settings::{self, Configurable};
use ir_lang::Function;
use pager_lang::{Protection, Region};
use crate::compiled::Compiled;
use crate::error::JitError;
use crate::{icache, translate};
pub struct Jit {
isa: OwnedTargetIsa,
}
impl Jit {
pub fn new() -> Result<Self, JitError> {
let mut flags = settings::builder();
set_flag(&mut flags, "opt_level", "speed")?;
set_flag(&mut flags, "is_pic", "false")?;
let flags = settings::Flags::new(flags);
let isa = cranelift_native::builder()
.map_err(JitError::Unsupported)?
.finish(flags)
.map_err(|err| JitError::Codegen(err.to_string()))?;
Ok(Self { isa })
}
pub fn compile(&self, func: &Function) -> Result<Compiled, JitError> {
func.validate()?;
let clif = translate::translate(func, &*self.isa)?;
let mut context = Context::for_function(clif);
let code = context
.compile(&*self.isa, &mut Default::default())
.map_err(|err| JitError::Codegen(err.inner.to_string()))?;
if !code.buffer.relocs().is_empty() {
return Err(JitError::Unsupported(
"the compiled code requires runtime relocations",
));
}
let bytes = code.code_buffer();
let region = place(bytes)?;
Ok(Compiled::new(
region,
func.name().to_string(),
func.params().to_vec(),
func.ret(),
bytes.len(),
))
}
}
impl fmt::Debug for Jit {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Jit")
.field("triple", &self.isa.triple())
.finish()
}
}
pub fn compile(func: &Function) -> Result<Compiled, JitError> {
Jit::new()?.compile(func)
}
fn set_flag(flags: &mut settings::Builder, name: &str, value: &str) -> Result<(), JitError> {
flags
.set(name, value)
.map_err(|err| JitError::Codegen(err.to_string()))
}
fn place(code: &[u8]) -> Result<Region, JitError> {
let mut region = Region::with_guard_pages(code.len())?;
region.write(0, code)?;
region.protect(Protection::ReadExecute)?;
icache::synchronize(region.as_ptr(), code.len());
Ok(region)
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
reason = "tests build known-valid functions and call the compiled result directly"
)]
mod tests {
use super::{Jit, compile};
use crate::JitError;
use ir_lang::{BinOp, Builder, Type};
#[test]
fn test_compile_reports_signature_and_code_size() {
let mut b = Builder::new("double", &[Type::Int], Type::Int);
let x = b.block_params(b.entry())[0];
let sum = b.bin(BinOp::Add, x, x);
b.ret(Some(sum));
let compiled = compile(&b.finish()).expect("double is well-formed");
assert_eq!(compiled.name(), "double");
assert_eq!(compiled.params(), &[Type::Int]);
assert_eq!(compiled.ret(), Type::Int);
assert!(compiled.code_len() > 0);
}
#[test]
fn test_compiled_double_runs() {
let mut b = Builder::new("double", &[Type::Int], Type::Int);
let x = b.block_params(b.entry())[0];
let sum = b.bin(BinOp::Add, x, x);
b.ret(Some(sum));
let compiled = compile(&b.finish()).unwrap();
let double: extern "C" fn(i64) -> i64 = unsafe { compiled.entry() };
assert_eq!(double(21), 42);
assert_eq!(double(-1), -2);
}
#[test]
fn test_engine_is_reusable_across_functions() {
let jit = Jit::new().unwrap();
for (name, k) in [("a", 1_i64), ("b", 2), ("c", 3)] {
let mut b = Builder::new(name, &[Type::Int], Type::Int);
let x = b.block_params(b.entry())[0];
let c = b.iconst(k);
let sum = b.bin(BinOp::Add, x, c);
b.ret(Some(sum));
let compiled = jit.compile(&b.finish()).unwrap();
assert_eq!(compiled.name(), name);
let f: extern "C" fn(i64) -> i64 = unsafe { compiled.entry() };
assert_eq!(f(40), 40 + k);
}
}
#[test]
fn test_invalid_function_is_rejected() {
let mut b = Builder::new("bad", &[], Type::Int);
b.ret(None);
assert!(matches!(compile(&b.finish()), Err(JitError::InvalidIr(_))));
}
}