Skip to main content

jit_lang/
engine.rs

1//! The [`Jit`] engine: it owns a code generator for the host and turns IR functions
2//! into runnable [`Compiled`] code.
3
4use std::fmt;
5
6use cranelift_codegen::Context;
7use cranelift_codegen::isa::OwnedTargetIsa;
8use cranelift_codegen::settings::{self, Configurable};
9use ir_lang::Function;
10use pager_lang::{Protection, Region};
11
12use crate::compiled::Compiled;
13use crate::error::JitError;
14use crate::{icache, translate};
15
16/// A just-in-time compiler for the host machine.
17///
18/// A `Jit` holds a code generator configured for the CPU it is running on, built once
19/// in [`new`](Self::new). Each call to [`compile`](Self::compile) validates an
20/// [`ir_lang::Function`], lowers it to native machine code, and places that code in
21/// executable memory, returning a [`Compiled`] that can be called. The engine carries
22/// no per-function state, so one `Jit` compiles many functions, and it is [`Send`] and
23/// [`Sync`], so it can be shared across threads.
24///
25/// For a one-off compile where reuse does not matter, the free function
26/// [`compile`](crate::compile) builds a `Jit` and compiles in a single call.
27///
28/// # Examples
29///
30/// Build the engine once, compile two functions, and run them:
31///
32/// ```
33/// use jit_lang::Jit;
34/// use ir_lang::{Builder, BinOp, Type};
35///
36/// let jit = Jit::new().expect("the host is supported");
37///
38/// // fn inc(x: int) -> int { x + 1 }
39/// let mut b = Builder::new("inc", &[Type::Int], Type::Int);
40/// let x = b.block_params(b.entry())[0];
41/// let one = b.iconst(1);
42/// let sum = b.bin(BinOp::Add, x, one);
43/// b.ret(Some(sum));
44/// let inc = jit.compile(&b.finish()).expect("inc is well-formed");
45///
46/// // fn neg(x: int) -> int { 0 - x }
47/// let mut b = Builder::new("neg", &[Type::Int], Type::Int);
48/// let x = b.block_params(b.entry())[0];
49/// let zero = b.iconst(0);
50/// let r = b.bin(BinOp::Sub, zero, x);
51/// b.ret(Some(r));
52/// let neg = jit.compile(&b.finish()).expect("neg is well-formed");
53///
54/// // SAFETY: both signatures are `fn(int) -> int`, and each handle outlives its call.
55/// let inc_fn: extern "C" fn(i64) -> i64 = unsafe { inc.entry() };
56/// let neg_fn: extern "C" fn(i64) -> i64 = unsafe { neg.entry() };
57/// assert_eq!(inc_fn(41), 42);
58/// assert_eq!(neg_fn(42), -42);
59/// ```
60pub struct Jit {
61    /// The host code generator. Reference-counted internally, so cloning the engine's
62    /// target is cheap; it is built once and reused for every compile.
63    isa: OwnedTargetIsa,
64}
65
66impl Jit {
67    /// Builds an engine targeting the host CPU, with its native instruction-set
68    /// features enabled and the optimizing code path selected.
69    ///
70    /// # Errors
71    ///
72    /// - [`JitError::Unsupported`] if the host architecture has no Cranelift back-end.
73    /// - [`JitError::Codegen`] if the code generator rejects the configuration — not
74    ///   expected on a supported host.
75    ///
76    /// # Examples
77    ///
78    /// ```
79    /// use jit_lang::Jit;
80    ///
81    /// let jit = Jit::new().expect("the host is supported");
82    /// let _ = jit; // ready to compile
83    /// ```
84    pub fn new() -> Result<Self, JitError> {
85        let mut flags = settings::builder();
86        // Optimize for speed; we run the code we generate, so its quality is the point.
87        set_flag(&mut flags, "opt_level", "speed")?;
88        // The code runs at the fixed address of its own region, so position-independent
89        // addressing buys nothing and only adds indirection.
90        set_flag(&mut flags, "is_pic", "false")?;
91        let flags = settings::Flags::new(flags);
92
93        let isa = cranelift_native::builder()
94            .map_err(JitError::Unsupported)?
95            .finish(flags)
96            .map_err(|err| JitError::Codegen(err.to_string()))?;
97        Ok(Self { isa })
98    }
99
100    /// Compiles a function to native machine code and makes it runnable.
101    ///
102    /// The function is validated, translated to Cranelift IR, compiled for the host,
103    /// and copied into a guard-flanked, read-execute memory region that the returned
104    /// [`Compiled`] owns. The emitted code is self-contained — a leaf function with no
105    /// outgoing calls — so it needs no runtime relocation; a function that somehow
106    /// required one is refused rather than run wrong.
107    ///
108    /// # Errors
109    ///
110    /// - [`JitError::InvalidIr`] if the function is not well-formed SSA.
111    /// - [`JitError::Unsupported`] if it uses something this back-end cannot lower, such
112    ///   as a [`Unit`](ir_lang::Type::Unit)-typed parameter.
113    /// - [`JitError::Codegen`] if the code generator fails on otherwise valid input.
114    /// - [`JitError::Memory`] if executable memory cannot be obtained or protected.
115    ///
116    /// # Examples
117    ///
118    /// ```
119    /// use jit_lang::Jit;
120    /// use ir_lang::{Builder, BinOp, Type};
121    ///
122    /// // fn mul(a: int, b: int) -> int { a * b }
123    /// let mut b = Builder::new("mul", &[Type::Int, Type::Int], Type::Int);
124    /// let a = b.block_params(b.entry())[0];
125    /// let c = b.block_params(b.entry())[1];
126    /// let p = b.bin(BinOp::Mul, a, c);
127    /// b.ret(Some(p));
128    ///
129    /// let jit = Jit::new().unwrap();
130    /// let mul = jit.compile(&b.finish()).expect("mul is well-formed");
131    ///
132    /// // SAFETY: the signature is `fn(int, int) -> int`, and `mul` outlives the call.
133    /// let mul_fn: extern "C" fn(i64, i64) -> i64 = unsafe { mul.entry() };
134    /// assert_eq!(mul_fn(6, 7), 42);
135    /// ```
136    pub fn compile(&self, func: &Function) -> Result<Compiled, JitError> {
137        func.validate()?;
138        let clif = translate::translate(func, &*self.isa)?;
139
140        let mut context = Context::for_function(clif);
141        let code = context
142            .compile(&*self.isa, &mut Default::default())
143            .map_err(|err| JitError::Codegen(err.inner.to_string()))?;
144
145        if !code.buffer.relocs().is_empty() {
146            return Err(JitError::Unsupported(
147                "the compiled code requires runtime relocations",
148            ));
149        }
150
151        let bytes = code.code_buffer();
152        let region = place(bytes)?;
153        Ok(Compiled::new(
154            region,
155            func.name().to_string(),
156            func.params().to_vec(),
157            func.ret(),
158            bytes.len(),
159        ))
160    }
161}
162
163impl fmt::Debug for Jit {
164    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
165        f.debug_struct("Jit")
166            .field("triple", &self.isa.triple())
167            .finish()
168    }
169}
170
171/// Compiles a function with a fresh host engine, for the one-off case.
172///
173/// This is the shortcut for compiling a single function where holding onto a
174/// [`Jit`] does not pay off. It builds an engine, compiles, and returns the runnable
175/// [`Compiled`]. When compiling several functions, build one [`Jit`] with
176/// [`Jit::new`] and reuse it — that creates the host code generator once instead of
177/// per call.
178///
179/// # Errors
180///
181/// The same as [`Jit::new`] followed by [`Jit::compile`]: the host being unsupported,
182/// the function being invalid or using an unsupported feature, a code-generation
183/// failure, or executable memory being unavailable.
184///
185/// # Examples
186///
187/// ```
188/// use jit_lang::compile;
189/// use ir_lang::{Builder, BinOp, Type};
190///
191/// // fn square(x: int) -> int { x * x }
192/// let mut b = Builder::new("square", &[Type::Int], Type::Int);
193/// let x = b.block_params(b.entry())[0];
194/// let sq = b.bin(BinOp::Mul, x, x);
195/// b.ret(Some(sq));
196///
197/// let square = compile(&b.finish()).expect("square is well-formed");
198///
199/// // SAFETY: the signature is `fn(int) -> int`, and `square` outlives the call.
200/// let square_fn: extern "C" fn(i64) -> i64 = unsafe { square.entry() };
201/// assert_eq!(square_fn(7), 49);
202/// ```
203pub fn compile(func: &Function) -> Result<Compiled, JitError> {
204    Jit::new()?.compile(func)
205}
206
207/// Sets one code-generator flag, mapping a rejected setting to a [`JitError::Codegen`].
208fn set_flag(flags: &mut settings::Builder, name: &str, value: &str) -> Result<(), JitError> {
209    flags
210        .set(name, value)
211        .map_err(|err| JitError::Codegen(err.to_string()))
212}
213
214/// Copies machine code into a fresh guard-flanked region, flips it to read-execute, and
215/// synchronizes the instruction cache so the code is safe to run on any supported host.
216fn place(code: &[u8]) -> Result<Region, JitError> {
217    let mut region = Region::with_guard_pages(code.len())?;
218    region.write(0, code)?;
219    region.protect(Protection::ReadExecute)?;
220    icache::synchronize(region.as_ptr(), code.len());
221    Ok(region)
222}
223
224#[cfg(test)]
225#[allow(
226    clippy::unwrap_used,
227    clippy::expect_used,
228    reason = "tests build known-valid functions and call the compiled result directly"
229)]
230mod tests {
231    use super::{Jit, compile};
232    use crate::JitError;
233    use ir_lang::{BinOp, Builder, Type};
234
235    #[test]
236    fn test_compile_reports_signature_and_code_size() {
237        let mut b = Builder::new("double", &[Type::Int], Type::Int);
238        let x = b.block_params(b.entry())[0];
239        let sum = b.bin(BinOp::Add, x, x);
240        b.ret(Some(sum));
241        let compiled = compile(&b.finish()).expect("double is well-formed");
242
243        assert_eq!(compiled.name(), "double");
244        assert_eq!(compiled.params(), &[Type::Int]);
245        assert_eq!(compiled.ret(), Type::Int);
246        assert!(compiled.code_len() > 0);
247    }
248
249    #[test]
250    fn test_compiled_double_runs() {
251        let mut b = Builder::new("double", &[Type::Int], Type::Int);
252        let x = b.block_params(b.entry())[0];
253        let sum = b.bin(BinOp::Add, x, x);
254        b.ret(Some(sum));
255        let compiled = compile(&b.finish()).unwrap();
256
257        // SAFETY: the signature is `fn(int) -> int` and `compiled` outlives the call.
258        let double: extern "C" fn(i64) -> i64 = unsafe { compiled.entry() };
259        assert_eq!(double(21), 42);
260        assert_eq!(double(-1), -2);
261    }
262
263    #[test]
264    fn test_engine_is_reusable_across_functions() {
265        let jit = Jit::new().unwrap();
266        for (name, k) in [("a", 1_i64), ("b", 2), ("c", 3)] {
267            let mut b = Builder::new(name, &[Type::Int], Type::Int);
268            let x = b.block_params(b.entry())[0];
269            let c = b.iconst(k);
270            let sum = b.bin(BinOp::Add, x, c);
271            b.ret(Some(sum));
272            let compiled = jit.compile(&b.finish()).unwrap();
273            assert_eq!(compiled.name(), name);
274            // SAFETY: the signature is `fn(int) -> int` and `compiled` outlives the call.
275            let f: extern "C" fn(i64) -> i64 = unsafe { compiled.entry() };
276            assert_eq!(f(40), 40 + k);
277        }
278    }
279
280    #[test]
281    fn test_invalid_function_is_rejected() {
282        // Declares an int return but returns nothing.
283        let mut b = Builder::new("bad", &[], Type::Int);
284        b.ret(None);
285        assert!(matches!(compile(&b.finish()), Err(JitError::InvalidIr(_))));
286    }
287}