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}