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::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 and flips it to read-execute.
215fn place(code: &[u8]) -> Result<Region, JitError> {
216    let mut region = Region::with_guard_pages(code.len())?;
217    region.write(0, code)?;
218    region.protect(Protection::ReadExecute)?;
219    Ok(region)
220}
221
222#[cfg(test)]
223#[allow(
224    clippy::unwrap_used,
225    clippy::expect_used,
226    reason = "tests build known-valid functions and call the compiled result directly"
227)]
228mod tests {
229    use super::{Jit, compile};
230    use crate::JitError;
231    use ir_lang::{BinOp, Builder, Type};
232
233    #[test]
234    fn test_compile_reports_signature_and_code_size() {
235        let mut b = Builder::new("double", &[Type::Int], Type::Int);
236        let x = b.block_params(b.entry())[0];
237        let sum = b.bin(BinOp::Add, x, x);
238        b.ret(Some(sum));
239        let compiled = compile(&b.finish()).expect("double is well-formed");
240
241        assert_eq!(compiled.name(), "double");
242        assert_eq!(compiled.params(), &[Type::Int]);
243        assert_eq!(compiled.ret(), Type::Int);
244        assert!(compiled.code_len() > 0);
245    }
246
247    #[test]
248    fn test_compiled_double_runs() {
249        let mut b = Builder::new("double", &[Type::Int], Type::Int);
250        let x = b.block_params(b.entry())[0];
251        let sum = b.bin(BinOp::Add, x, x);
252        b.ret(Some(sum));
253        let compiled = compile(&b.finish()).unwrap();
254
255        // SAFETY: the signature is `fn(int) -> int` and `compiled` outlives the call.
256        let double: extern "C" fn(i64) -> i64 = unsafe { compiled.entry() };
257        assert_eq!(double(21), 42);
258        assert_eq!(double(-1), -2);
259    }
260
261    #[test]
262    fn test_engine_is_reusable_across_functions() {
263        let jit = Jit::new().unwrap();
264        for (name, k) in [("a", 1_i64), ("b", 2), ("c", 3)] {
265            let mut b = Builder::new(name, &[Type::Int], Type::Int);
266            let x = b.block_params(b.entry())[0];
267            let c = b.iconst(k);
268            let sum = b.bin(BinOp::Add, x, c);
269            b.ret(Some(sum));
270            let compiled = jit.compile(&b.finish()).unwrap();
271            assert_eq!(compiled.name(), name);
272            // SAFETY: the signature is `fn(int) -> int` and `compiled` outlives the call.
273            let f: extern "C" fn(i64) -> i64 = unsafe { compiled.entry() };
274            assert_eq!(f(40), 40 + k);
275        }
276    }
277
278    #[test]
279    fn test_invalid_function_is_rejected() {
280        // Declares an int return but returns nothing.
281        let mut b = Builder::new("bad", &[], Type::Int);
282        b.ret(None);
283        assert!(matches!(compile(&b.finish()), Err(JitError::InvalidIr(_))));
284    }
285}