Skip to main content

nova_codegen_cranelift/
lib.rs

1//! Cranelift codegen backend for Nova (the fast debug backend).
2//!
3//! Translates monomorphized MIR into native code, through two paths:
4//!
5//! - [`compile_jit`]: in-memory JIT for `nova run` — runtime symbols are
6//!   registered directly with the JIT.
7//! - [`compile_object`]: native object bytes for `nova build` — the MIR
8//!   entry is emitted as `nova_main` plus an exported C `main(argc, argv)`
9//!   wrapper, and runtime symbols resolve at link time against the
10//!   `nova-runtime` static library.
11//!
12//! MIR temps become Cranelift frontend `Variable`s, so the SSA builder
13//! inserts block parameters (phis) at join points automatically. Unit-class
14//! temps carry no runtime value and are never materialized.
15
16use anyhow::{anyhow, Context, Result};
17use cranelift::codegen::ir::{types, AbiParam, InstBuilder, MemFlags, Signature, Value};
18use cranelift::codegen::isa::TargetIsa;
19use cranelift::codegen::settings::{self, Configurable};
20use cranelift::frontend::{FunctionBuilder, FunctionBuilderContext, Switch, Variable};
21use cranelift::prelude::{EntityRef, FloatCC, IntCC, TrapCode};
22use cranelift_jit::{JITBuilder, JITModule};
23use cranelift_module::{DataDescription, DataId, FuncId, Linkage, Module as ClModule};
24use cranelift_object::{ObjectBuilder, ObjectModule};
25use nova_mir::{MirTy, Module as MirModule, OperandClass, RtFunc, Stmt, Terminator};
26use rustc_hash::FxHashMap;
27use std::sync::Arc;
28
29/// The symbol name the MIR `main` gets in object files, so the exported
30/// C-ABI `main(argc, argv)` wrapper can keep the conventional name.
31pub const NOVA_ENTRY_SYMBOL: &str = "nova_main";
32
33// `mir_ty` is re-exported for driver convenience.
34pub use nova_mir::mangle;
35
36/// An `extern` symbol the JIT could not resolve at run time. This is a user
37/// error (a bad FFI declaration), not a compiler bug, so the driver reports it
38/// as a clean diagnostic rather than an internal error.
39#[derive(Debug)]
40pub struct UnresolvedExternSymbol(pub String);
41
42impl std::fmt::Display for UnresolvedExternSymbol {
43    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44        write!(f, "cannot resolve external symbol `{}`", self.0)
45    }
46}
47
48impl std::error::Error for UnresolvedExternSymbol {}
49
50/// A JIT-compiled Nova program, ready to run in-process.
51pub struct CompiledProgram {
52    /// Kept alive for the lifetime of the compiled code.
53    _module: JITModule,
54    main: *const u8,
55}
56
57impl CompiledProgram {
58    /// Execute the program's `main` function.
59    pub fn run(&self) {
60        // SAFETY: `main` was compiled from a `fn main()` with no params and
61        // unit return, and `_module` keeps its memory alive.
62        let entry: extern "C" fn() = unsafe { std::mem::transmute(self.main) };
63        entry();
64    }
65
66    /// Execute `main` with `args` as the program's arguments: the list
67    /// `std/process`'s `args()` returns. The runtime keeps the first list it
68    /// is given for the rest of the process (`nova_runtime::process`).
69    pub fn run_with_args(&self, args: Vec<String>) {
70        let _ = nova_runtime::process::set_args(args);
71        self.run();
72    }
73}
74
75/// JIT-compile a MIR module and return the runnable program.
76///
77/// All `nova-runtime` symbols are registered with the JIT so compiled code
78/// can call the runtime directly.
79pub fn compile_jit(mir: &MirModule) -> Result<CompiledProgram> {
80    let isa = native_isa(false)?;
81    let mut builder = JITBuilder::with_isa(isa, cranelift_module::default_libcall_names());
82    for (name, ptr) in nova_runtime::symbols() {
83        builder.symbol(name, ptr);
84    }
85    let mut module = JITModule::new(builder);
86
87    let functions = {
88        let mut cg = Codegen::new(&mut module);
89        cg.declare_runtime()?;
90        cg.declare_externs(mir)?;
91        cg.declare_functions(mir, None)?;
92        cg.define_functions(mir)?;
93        cg.functions
94    };
95
96    // Imported extern symbols are resolved *inside* `finalize_definitions`,
97    // where cranelift-jit `panic!`s on a symbol it cannot resolve rather than
98    // returning an `Err`. Catch that unwind (silencing the default hook so no
99    // raw stack trace prints) and turn it into a clean error, so an
100    // unresolvable `extern` becomes a diagnostic instead of a compiler crash —
101    // matching the graceful `nova build` linker-error path.
102    let prev_hook = std::panic::take_hook();
103    std::panic::set_hook(Box::new(|_| {}));
104    // Map the (large) module error to a String inside the closure so the caught
105    // value stays small (clippy::result_large_err) and is unwind-safe.
106    let finalize = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
107        module.finalize_definitions().map_err(|e| e.to_string())
108    }));
109    std::panic::set_hook(prev_hook);
110    match finalize {
111        Ok(Ok(())) => {}
112        Ok(Err(msg)) => return Err(anyhow!("failed to finalize JIT code: {msg}")),
113        Err(payload) => {
114            let detail = payload
115                .downcast_ref::<String>()
116                .map(String::as_str)
117                .or_else(|| payload.downcast_ref::<&str>().copied())
118                .unwrap_or("unknown error");
119            // cranelift-jit panics as "can't resolve symbol <name>"; recover the
120            // symbol so the driver can render a clean, user-facing diagnostic.
121            let symbol = detail
122                .strip_prefix("can't resolve symbol ")
123                .unwrap_or(detail)
124                .trim()
125                .to_string();
126            return Err(anyhow::Error::new(UnresolvedExternSymbol(symbol)));
127        }
128    }
129
130    let main_id = *functions
131        .get("main")
132        .ok_or_else(|| anyhow!("no `main` function in MIR module"))?;
133    let main = module.get_finalized_function(main_id);
134
135    Ok(CompiledProgram {
136        _module: module,
137        main,
138    })
139}
140
141/// Compile a MIR module to native object-file bytes for `nova build`.
142///
143/// The object exports a C `main(argc, argv) -> i32` that calls the Nova
144/// entry ([`NOVA_ENTRY_SYMBOL`]); `nova_rt_*` symbols are unresolved
145/// imports satisfied by linking the `nova-runtime` static library.
146pub fn compile_object(mir: &MirModule) -> Result<Vec<u8>> {
147    // PIC on Unix-likes (PIE-by-default linkers); non-PIC COFF on Windows.
148    let isa = native_isa(!cfg!(windows))?;
149    let obj_builder = ObjectBuilder::new(isa, "nova", cranelift_module::default_libcall_names())
150        .context("creating object builder")?;
151    let mut module = ObjectModule::new(obj_builder);
152
153    {
154        let mut cg = Codegen::new(&mut module);
155        cg.declare_runtime()?;
156        cg.declare_externs(mir)?;
157        cg.declare_functions(mir, Some(NOVA_ENTRY_SYMBOL))?;
158        cg.define_functions(mir)?;
159        cg.emit_c_main()?;
160    }
161
162    let product = module.finish();
163    product.emit().context("emitting object file")
164}
165
166fn native_isa(pic: bool) -> Result<Arc<dyn TargetIsa>> {
167    let mut flags = settings::builder();
168    flags
169        .set("use_colocated_libcalls", "false")
170        .context("setting cranelift flags")?;
171    flags
172        .set("is_pic", if pic { "true" } else { "false" })
173        .context("setting cranelift flags")?;
174    let isa_builder = cranelift_native::builder()
175        .map_err(|e| anyhow!("host machine is not supported by cranelift: {e}"))?;
176    isa_builder
177        .finish(settings::Flags::new(flags))
178        .context("building native ISA")
179}
180
181/// Per-module codegen state: declared function and data ids. Generic over
182/// the Cranelift module flavor (JIT vs object emission).
183struct Codegen<'m, M: ClModule> {
184    module: &'m mut M,
185    functions: FxHashMap<String, FuncId>,
186    runtime: FxHashMap<&'static str, FuncId>,
187    strings: FxHashMap<String, DataId>,
188}
189
190impl<'m, M: ClModule> Codegen<'m, M> {
191    fn new(module: &'m mut M) -> Self {
192        Self {
193            module,
194            functions: FxHashMap::default(),
195            runtime: FxHashMap::default(),
196            strings: FxHashMap::default(),
197        }
198    }
199
200    fn ptr_ty(&self) -> types::Type {
201        self.module.target_config().pointer_type()
202    }
203
204    fn cl_ty(&self, ty: MirTy) -> Option<types::Type> {
205        match ty {
206            MirTy::I64 => Some(types::I64),
207            MirTy::F64 => Some(types::F64),
208            MirTy::I8 => Some(types::I8),
209            MirTy::Ptr => Some(self.ptr_ty()),
210            MirTy::Unit => None,
211        }
212    }
213
214    /// Number of leading ABI parameter temps: the real params plus a
215    /// leading environment pointer for function values (closures/wrappers).
216    fn abi_param_count(f: &nova_mir::Function) -> usize {
217        f.params as usize + if f.takes_env { 1 } else { 0 }
218    }
219
220    fn make_signature(&self, params: &[MirTy], ret: MirTy) -> Signature {
221        let mut sig = self.module.make_signature();
222        for p in params {
223            if let Some(t) = self.cl_ty(*p) {
224                sig.params.push(AbiParam::new(t));
225            }
226        }
227        if let Some(t) = self.cl_ty(ret) {
228            sig.returns.push(AbiParam::new(t));
229        }
230        sig
231    }
232
233    fn declare_runtime(&mut self) -> Result<()> {
234        for rt in RtFunc::ALL {
235            let (params, ret) = rt.signature();
236            let sig = self.make_signature(&params, ret);
237            let id = self
238                .module
239                .declare_function(rt.symbol(), Linkage::Import, &sig)
240                .with_context(|| format!("declaring runtime fn {}", rt.symbol()))?;
241            self.runtime.insert(rt.symbol(), id);
242        }
243        // String literal constructor (used by ConstStr only).
244        let ptr = self.ptr_ty();
245        let mut sig = self.module.make_signature();
246        sig.params.push(AbiParam::new(ptr));
247        sig.params.push(AbiParam::new(types::I64));
248        sig.returns.push(AbiParam::new(ptr));
249        let id = self
250            .module
251            .declare_function("nova_rt_str_new", Linkage::Import, &sig)
252            .context("declaring nova_rt_str_new")?;
253        self.runtime.insert("nova_rt_str_new", id);
254        Ok(())
255    }
256
257    /// Declare each `extern` (FFI) symbol as an imported function, registered
258    /// by its raw name so a `Stmt::Call` to that name resolves. The symbol is
259    /// satisfied at run time by the JIT's dlsym fallback (`nova run`) or by the
260    /// system linker against the C runtime (`nova build`).
261    fn declare_externs(&mut self, mir: &MirModule) -> Result<()> {
262        for ext in &mir.externs {
263            let sig = self.make_signature(&ext.params, ext.ret);
264            let id = self
265                .module
266                .declare_function(&ext.symbol, Linkage::Import, &sig)
267                .with_context(|| format!("declaring extern `{}`", ext.symbol))?;
268            self.functions.insert(ext.symbol.clone(), id);
269        }
270        Ok(())
271    }
272
273    /// Declare all MIR functions. With `rename_main`, the MIR entry is
274    /// declared under that symbol (object mode, where the conventional
275    /// `main` symbol is taken by the C wrapper); the lookup key stays
276    /// `"main"` either way.
277    fn declare_functions(&mut self, mir: &MirModule, rename_main: Option<&str>) -> Result<()> {
278        for f in &mir.functions {
279            let params: Vec<MirTy> = f.temps[..Self::abi_param_count(f)].to_vec();
280            let sig = self.make_signature(&params, f.ret);
281            let symbol = match rename_main {
282                Some(entry) if f.name == "main" => entry,
283                _ => f.name.as_str(),
284            };
285            let id = self
286                .module
287                .declare_function(symbol, Linkage::Local, &sig)
288                .with_context(|| format!("declaring `{}`", f.name))?;
289            self.functions.insert(f.name.clone(), id);
290        }
291        Ok(())
292    }
293
294    /// Emit the exported C entry point for object files:
295    /// `main(argc: i32, argv: ptr) -> i32 { nova_main(); return 0 }`.
296    fn emit_c_main(&mut self) -> Result<()> {
297        let ptr = self.ptr_ty();
298        let mut sig = self.module.make_signature();
299        sig.params.push(AbiParam::new(types::I32));
300        sig.params.push(AbiParam::new(ptr));
301        sig.returns.push(AbiParam::new(types::I32));
302        let main_id = self
303            .module
304            .declare_function("main", Linkage::Export, &sig)
305            .context("declaring C main wrapper")?;
306
307        let nova_main = *self
308            .functions
309            .get("main")
310            .ok_or_else(|| anyhow!("no `main` function in MIR module"))?;
311
312        let mut ctx = self.module.make_context();
313        ctx.func.signature = sig;
314        let mut fb_ctx = FunctionBuilderContext::new();
315        let mut builder = FunctionBuilder::new(&mut ctx.func, &mut fb_ctx);
316        let block = builder.create_block();
317        builder.append_block_params_for_function_params(block);
318        builder.switch_to_block(block);
319        let func_ref = self.module.declare_func_in_func(nova_main, builder.func);
320        builder.ins().call(func_ref, &[]);
321        let zero = builder.ins().iconst(types::I32, 0);
322        builder.ins().return_(&[zero]);
323        builder.seal_all_blocks();
324        builder.finalize();
325
326        self.module
327            .define_function(main_id, &mut ctx)
328            .context("defining C main wrapper")?;
329        self.module.clear_context(&mut ctx);
330        Ok(())
331    }
332
333    fn string_data(&mut self, s: &str) -> Result<DataId> {
334        if let Some(id) = self.strings.get(s) {
335            return Ok(*id);
336        }
337        let name = format!("str{}", self.strings.len());
338        let id = self
339            .module
340            .declare_data(&name, Linkage::Local, false, false)
341            .context("declaring string data")?;
342        let mut desc = DataDescription::new();
343        // Empty data definitions are rejected; pad the empty string.
344        let bytes: Box<[u8]> = if s.is_empty() {
345            Box::new([0u8])
346        } else {
347            s.as_bytes().into()
348        };
349        desc.define(bytes);
350        self.module
351            .define_data(id, &desc)
352            .context("defining string data")?;
353        self.strings.insert(s.to_string(), id);
354        Ok(id)
355    }
356
357    fn define_functions(&mut self, mir: &MirModule) -> Result<()> {
358        let mut fb_ctx = FunctionBuilderContext::new();
359        for f in &mir.functions {
360            self.define_function(f, &mut fb_ctx)
361                .with_context(|| format!("compiling `{}`", f.name))?;
362        }
363        Ok(())
364    }
365
366    fn define_function(
367        &mut self,
368        f: &nova_mir::Function,
369        fb_ctx: &mut FunctionBuilderContext,
370    ) -> Result<()> {
371        let func_id = self.functions[&f.name];
372        let abi_count = Self::abi_param_count(f);
373        let params: Vec<MirTy> = f.temps[..abi_count].to_vec();
374        let sig = self.make_signature(&params, f.ret);
375
376        let mut ctx = self.module.make_context();
377        ctx.func.signature = sig;
378
379        // Collect everything needed from `self` before borrowing mutably.
380        let mut builder = FunctionBuilder::new(&mut ctx.func, fb_ctx);
381
382        // Declare a variable per non-unit temp.
383        let mut vars: Vec<Option<Variable>> = Vec::with_capacity(f.temps.len());
384        for (i, ty) in f.temps.iter().enumerate() {
385            match self.cl_ty(*ty) {
386                Some(t) => {
387                    let var = Variable::new(i);
388                    builder.declare_var(var, t);
389                    vars.push(Some(var));
390                }
391                None => vars.push(None),
392            }
393        }
394
395        // One Cranelift block per MIR block.
396        let cl_blocks: Vec<_> = f.blocks.iter().map(|_| builder.create_block()).collect();
397        let entry = cl_blocks[0];
398        builder.append_block_params_for_function_params(entry);
399        builder.switch_to_block(entry);
400
401        // Bind incoming parameters to their variables (unit params carry
402        // no ABI value, so zip non-unit vars with the block params).
403        let entry_params: Vec<Value> = builder.block_params(entry).to_vec();
404        let param_vars = vars.iter().take(abi_count).copied().flatten();
405        for (var, value) in param_vars.zip(entry_params) {
406            builder.def_var(var, value);
407        }
408
409        let mut tr = Translator {
410            cg: self,
411            builder,
412            vars,
413            cl_blocks,
414        };
415        for (i, block) in f.blocks.iter().enumerate() {
416            if i != 0 {
417                tr.builder.switch_to_block(tr.cl_blocks[i]);
418            }
419            for stmt in &block.stmts {
420                tr.stmt(stmt)?;
421            }
422            tr.terminator(&block.term, f.ret)?;
423        }
424        tr.builder.seal_all_blocks();
425        tr.builder.finalize();
426
427        self.module
428            .define_function(func_id, &mut ctx)
429            .with_context(|| format!("defining `{}`", f.name))?;
430        self.module.clear_context(&mut ctx);
431        Ok(())
432    }
433}
434
435/// Per-function translation state.
436struct Translator<'a, 'm, M: ClModule> {
437    cg: &'a mut Codegen<'m, M>,
438    builder: FunctionBuilder<'a>,
439    vars: Vec<Option<Variable>>,
440    cl_blocks: Vec<cranelift::codegen::ir::Block>,
441}
442
443impl<'a, 'm, M: ClModule> Translator<'a, 'm, M> {
444    fn use_temp(&mut self, t: nova_mir::Temp) -> Result<Value> {
445        let var = self.vars[t.0 as usize]
446            .ok_or_else(|| anyhow!("attempted to read a unit temp %{}", t.0))?;
447        Ok(self.builder.use_var(var))
448    }
449
450    fn def_temp(&mut self, t: nova_mir::Temp, v: Value) {
451        if let Some(var) = self.vars[t.0 as usize] {
452            self.builder.def_var(var, v);
453        }
454    }
455
456    /// Address of array element `index`: `arr + 8 (len header) + index * 8`
457    /// (all values occupy 8-byte slots). Assumes a 64-bit pointer target.
458    fn array_elem_addr(&mut self, arr: nova_mir::Temp, index: nova_mir::Temp) -> Result<Value> {
459        let base = self.use_temp(arr)?;
460        let idx = self.use_temp(index)?;
461        let byte_off = self.builder.ins().imul_imm(idx, 8);
462        let elem = self.builder.ins().iadd(base, byte_off);
463        Ok(self.builder.ins().iadd_imm(elem, 8))
464    }
465
466    fn call_func_id(&mut self, id: FuncId, args: &[Value]) -> Result<Option<Value>> {
467        let func_ref = self.cg.module.declare_func_in_func(id, self.builder.func);
468        let inst = self.builder.ins().call(func_ref, args);
469        let results = self.builder.inst_results(inst);
470        Ok(results.first().copied())
471    }
472
473    fn rt(&self, name: &'static str) -> FuncId {
474        self.cg.runtime[name]
475    }
476
477    fn stmt(&mut self, stmt: &Stmt) -> Result<()> {
478        match stmt {
479            Stmt::ConstInt(t, v) => {
480                let val = self.builder.ins().iconst(types::I64, *v);
481                self.def_temp(*t, val);
482            }
483            Stmt::ConstFloat(t, v) => {
484                let val = self.builder.ins().f64const(*v);
485                self.def_temp(*t, val);
486            }
487            Stmt::ConstBool(t, v) => {
488                let val = self.builder.ins().iconst(types::I8, *v as i64);
489                self.def_temp(*t, val);
490            }
491            Stmt::ConstUnit(_) => {}
492            Stmt::ConstStr(t, s) => {
493                let data_id = self.cg.string_data(s)?;
494                let gv = self
495                    .cg
496                    .module
497                    .declare_data_in_func(data_id, self.builder.func);
498                let ptr_ty = self.cg.ptr_ty();
499                let ptr = self.builder.ins().global_value(ptr_ty, gv);
500                let len = self.builder.ins().iconst(types::I64, s.len() as i64);
501                let id = self.rt("nova_rt_str_new");
502                let result = self
503                    .call_func_id(id, &[ptr, len])?
504                    .ok_or_else(|| anyhow!("str_new returns a value"))?;
505                self.def_temp(*t, result);
506            }
507            Stmt::Copy { dst, src } => {
508                let v = self.use_temp(*src)?;
509                self.def_temp(*dst, v);
510            }
511            Stmt::Bin {
512                dst,
513                op,
514                class,
515                lhs,
516                rhs,
517            } => {
518                let l = self.use_temp(*lhs)?;
519                let r = self.use_temp(*rhs)?;
520                let v = self.binop(*op, *class, l, r)?;
521                self.def_temp(*dst, v);
522            }
523            Stmt::Neg { dst, class, src } => {
524                let v = self.use_temp(*src)?;
525                let out = match class {
526                    OperandClass::Float => self.builder.ins().fneg(v),
527                    _ => self.builder.ins().ineg(v),
528                };
529                self.def_temp(*dst, out);
530            }
531            Stmt::Not { dst, src } => {
532                let v = self.use_temp(*src)?;
533                let out = self.builder.ins().bxor_imm(v, 1);
534                self.def_temp(*dst, out);
535            }
536            Stmt::BitNot { dst, src } => {
537                let v = self.use_temp(*src)?;
538                let out = self.builder.ins().bnot(v);
539                self.def_temp(*dst, out);
540            }
541            Stmt::Call { dst, callee, args } => {
542                let id = *self
543                    .cg
544                    .functions
545                    .get(callee)
546                    .ok_or_else(|| anyhow!("call to undeclared function `{callee}`"))?;
547                let arg_vals = self.arg_values(args)?;
548                let result = self.call_func_id(id, &arg_vals)?;
549                if let (Some(dst), Some(v)) = (dst, result) {
550                    self.def_temp(*dst, v);
551                }
552            }
553            // `Stmt::Await` is a marker `nova-mir`'s async state-machine
554            // transform consumes; there is no machine code for it, because the
555            // state object the resumed body reads its values back out of is
556            // built by that transform and named by nothing here. Reported as a
557            // malformed-MIR error rather than as a panic, matching the
558            // undeclared-callee arm above: this is a compiler bug, but codegen
559            // is a library path.
560            Stmt::Await { .. } => {
561                return Err(anyhow!(
562                    "an `.await` marker reached codegen: `nova-mir`'s async \
563                     state-machine transform must split every `async fn` body \
564                     at its await points before a module is emitted"
565                ));
566            }
567            Stmt::CallRuntime { dst, func, args } => {
568                let id = self.rt(func.symbol());
569                let arg_vals = self.arg_values(args)?;
570                let result = self.call_func_id(id, &arg_vals)?;
571                if let (Some(dst), Some(v)) = (dst, result) {
572                    self.def_temp(*dst, v);
573                }
574            }
575            Stmt::CallIndirect {
576                dst,
577                callee,
578                params,
579                ret,
580                args,
581            } => {
582                // `callee` is a fat pointer `{ code_ptr, env_ptr }`. Load both
583                // and call `code_ptr(env_ptr, args...)`.
584                let ptr_ty = self.cg.ptr_ty();
585                let fat = self.use_temp(*callee)?;
586                let code = self.builder.ins().load(ptr_ty, MemFlags::trusted(), fat, 0);
587                let env = self.builder.ins().load(ptr_ty, MemFlags::trusted(), fat, 8);
588                // Signature: (env, params...) -> ret.
589                let mut sig_params = vec![MirTy::Ptr];
590                sig_params.extend_from_slice(params);
591                let sig = self.cg.make_signature(&sig_params, *ret);
592                let sig_ref = self.builder.import_signature(sig);
593                let mut arg_vals = vec![env];
594                arg_vals.extend(self.arg_values(args)?);
595                let inst = self.builder.ins().call_indirect(sig_ref, code, &arg_vals);
596                let result = self.builder.inst_results(inst).first().copied();
597                if let (Some(dst), Some(v)) = (dst, result) {
598                    self.def_temp(*dst, v);
599                }
600            }
601            Stmt::MakeClosure {
602                dst,
603                code,
604                captures,
605            } => {
606                let ptr_ty = self.cg.ptr_ty();
607                // Environment record of captured values (null when empty).
608                let env = if captures.is_empty() {
609                    self.builder.ins().iconst(ptr_ty, 0)
610                } else {
611                    let size = (8 * captures.len()) as i64;
612                    let size_val = self.builder.ins().iconst(types::I64, size);
613                    let alloc = self.rt("nova_rt_alloc");
614                    let env = self
615                        .call_func_id(alloc, &[size_val])?
616                        .ok_or_else(|| anyhow!("alloc returns a value"))?;
617                    for (i, (cap, ty)) in captures.iter().enumerate() {
618                        if *ty == MirTy::Unit {
619                            continue;
620                        }
621                        let v = self.use_temp(*cap)?;
622                        self.builder
623                            .ins()
624                            .store(MemFlags::trusted(), v, env, (8 * i) as i32);
625                    }
626                    env
627                };
628                // Fat pointer `{ code_ptr, env_ptr }`.
629                let size_val = self.builder.ins().iconst(types::I64, 16);
630                let alloc = self.rt("nova_rt_alloc");
631                let fat = self
632                    .call_func_id(alloc, &[size_val])?
633                    .ok_or_else(|| anyhow!("alloc returns a value"))?;
634                let id = *self
635                    .cg
636                    .functions
637                    .get(code)
638                    .ok_or_else(|| anyhow!("closure code `{code}` is undeclared"))?;
639                let func_ref = self.cg.module.declare_func_in_func(id, self.builder.func);
640                let code_ptr = self.builder.ins().func_addr(ptr_ty, func_ref);
641                self.builder
642                    .ins()
643                    .store(MemFlags::trusted(), code_ptr, fat, 0);
644                self.builder.ins().store(MemFlags::trusted(), env, fat, 8);
645                self.def_temp(*dst, fat);
646            }
647            Stmt::MakeSum { dst, tag, fields } => {
648                let size = 8 + 8 * fields.len() as i64;
649                let size_val = self.builder.ins().iconst(types::I64, size);
650                let id = self.rt("nova_rt_alloc");
651                let ptr = self
652                    .call_func_id(id, &[size_val])?
653                    .ok_or_else(|| anyhow!("alloc returns a value"))?;
654                let tag_val = self.builder.ins().iconst(types::I64, *tag as i64);
655                self.builder
656                    .ins()
657                    .store(MemFlags::trusted(), tag_val, ptr, 0);
658                for (i, (field, ty)) in fields.iter().enumerate() {
659                    if *ty == MirTy::Unit {
660                        continue;
661                    }
662                    let v = self.use_temp(*field)?;
663                    let offset = (8 + 8 * i) as i32;
664                    self.builder
665                        .ins()
666                        .store(MemFlags::trusted(), v, ptr, offset);
667                }
668                self.def_temp(*dst, ptr);
669            }
670            Stmt::MakeRecord { dst, fields } => {
671                // Records have no tag: fields sit at offsets 8*i.
672                let size = (8 * fields.len().max(1)) as i64;
673                let size_val = self.builder.ins().iconst(types::I64, size);
674                let id = self.rt("nova_rt_alloc");
675                let ptr = self
676                    .call_func_id(id, &[size_val])?
677                    .ok_or_else(|| anyhow!("alloc returns a value"))?;
678                for (i, (field, ty)) in fields.iter().enumerate() {
679                    if *ty == MirTy::Unit {
680                        continue;
681                    }
682                    let v = self.use_temp(*field)?;
683                    let offset = (8 * i) as i32;
684                    self.builder
685                        .ins()
686                        .store(MemFlags::trusted(), v, ptr, offset);
687                }
688                self.def_temp(*dst, ptr);
689            }
690            Stmt::RecordField {
691                dst,
692                record,
693                index,
694                ty,
695            } => {
696                let Some(cl_ty) = self.cg.cl_ty(*ty) else {
697                    return Ok(());
698                };
699                let ptr = self.use_temp(*record)?;
700                let offset = (8 * index) as i32;
701                let v = self
702                    .builder
703                    .ins()
704                    .load(cl_ty, MemFlags::trusted(), ptr, offset);
705                self.def_temp(*dst, v);
706            }
707            Stmt::SetField {
708                record,
709                index,
710                value,
711                ty,
712            } => {
713                // A unit-typed field has no machine representation to store.
714                if self.cg.cl_ty(*ty).is_none() {
715                    return Ok(());
716                }
717                let ptr = self.use_temp(*record)?;
718                let v = self.use_temp(*value)?;
719                // The same `8 * index` offset `RecordField` loads from.
720                let offset = (8 * index) as i32;
721                self.builder
722                    .ins()
723                    .store(MemFlags::trusted(), v, ptr, offset);
724            }
725            Stmt::MakeArray { dst, elems } => {
726                // Layout: { len: i64, elem0, elem1, ... } with 8-byte slots.
727                let size = (8 + 8 * elems.len()) as i64;
728                let size_val = self.builder.ins().iconst(types::I64, size);
729                let alloc = self.rt("nova_rt_alloc");
730                let ptr = self
731                    .call_func_id(alloc, &[size_val])?
732                    .ok_or_else(|| anyhow!("alloc returns a value"))?;
733                let len_val = self.builder.ins().iconst(types::I64, elems.len() as i64);
734                self.builder
735                    .ins()
736                    .store(MemFlags::trusted(), len_val, ptr, 0);
737                for (i, (el, ty)) in elems.iter().enumerate() {
738                    if *ty == MirTy::Unit {
739                        continue;
740                    }
741                    let v = self.use_temp(*el)?;
742                    let offset = (8 + 8 * i) as i32;
743                    self.builder
744                        .ins()
745                        .store(MemFlags::trusted(), v, ptr, offset);
746                }
747                self.def_temp(*dst, ptr);
748            }
749            Stmt::ArrayAlloc { dst, len } => {
750                // Same layout as `MakeArray`, but the element count is only
751                // known at runtime: `8 + 8*len` bytes with `len` at offset 0.
752                // The MIR lowering has already guaranteed `len >= 0`, and the
753                // allocator zeroes, so the elements start valid and are then
754                // filled by the lowering's own loop.
755                let n = self.use_temp(*len)?;
756                let eight = self.builder.ins().iconst(types::I64, 8);
757                let bytes = self.builder.ins().imul(n, eight);
758                let size = self.builder.ins().iadd(bytes, eight);
759                let alloc = self.rt("nova_rt_alloc");
760                let ptr = self
761                    .call_func_id(alloc, &[size])?
762                    .ok_or_else(|| anyhow!("alloc returns a value"))?;
763                self.builder.ins().store(MemFlags::trusted(), n, ptr, 0);
764                self.def_temp(*dst, ptr);
765            }
766            Stmt::ArrayLen { dst, arr } => {
767                let ptr = self.use_temp(*arr)?;
768                let len = self
769                    .builder
770                    .ins()
771                    .load(types::I64, MemFlags::trusted(), ptr, 0);
772                self.def_temp(*dst, len);
773            }
774            Stmt::ArrayGet {
775                dst,
776                arr,
777                index,
778                ty,
779            } => {
780                let Some(cl_ty) = self.cg.cl_ty(*ty) else {
781                    return Ok(());
782                };
783                let addr = self.array_elem_addr(*arr, *index)?;
784                let v = self.builder.ins().load(cl_ty, MemFlags::trusted(), addr, 0);
785                self.def_temp(*dst, v);
786            }
787            Stmt::ArraySet {
788                arr,
789                index,
790                value,
791                ty,
792            } => {
793                if self.cg.cl_ty(*ty).is_none() {
794                    return Ok(());
795                }
796                let addr = self.array_elem_addr(*arr, *index)?;
797                let v = self.use_temp(*value)?;
798                self.builder.ins().store(MemFlags::trusted(), v, addr, 0);
799            }
800            Stmt::SumTag { dst, sum } => {
801                let ptr = self.use_temp(*sum)?;
802                let tag = self
803                    .builder
804                    .ins()
805                    .load(types::I64, MemFlags::trusted(), ptr, 0);
806                self.def_temp(*dst, tag);
807            }
808            Stmt::SumField {
809                dst,
810                sum,
811                index,
812                ty,
813            } => {
814                let Some(cl_ty) = self.cg.cl_ty(*ty) else {
815                    return Ok(());
816                };
817                let ptr = self.use_temp(*sum)?;
818                let offset = (8 + 8 * index) as i32;
819                let v = self
820                    .builder
821                    .ins()
822                    .load(cl_ty, MemFlags::trusted(), ptr, offset);
823                self.def_temp(*dst, v);
824            }
825        }
826        Ok(())
827    }
828
829    /// Collect argument values, skipping unit temps (they carry no data).
830    fn arg_values(&mut self, args: &[nova_mir::Temp]) -> Result<Vec<Value>> {
831        let mut vals = Vec::with_capacity(args.len());
832        for a in args {
833            if self.vars[a.0 as usize].is_some() {
834                vals.push(self.use_temp(*a)?);
835            }
836        }
837        Ok(vals)
838    }
839
840    fn binop(
841        &mut self,
842        op: nova_hir::BinOp,
843        class: OperandClass,
844        l: Value,
845        r: Value,
846    ) -> Result<Value> {
847        use nova_hir::BinOp::*;
848        let ins = self.builder.ins();
849        let v = match (class, op) {
850            (OperandClass::Float, Add) => ins.fadd(l, r),
851            (OperandClass::Float, Sub) => ins.fsub(l, r),
852            (OperandClass::Float, Mul) => ins.fmul(l, r),
853            (OperandClass::Float, Div) => ins.fdiv(l, r),
854            (OperandClass::Float, Rem) => {
855                return Err(anyhow!(
856                    "float remainder (%) is not supported yet by the Cranelift backend"
857                ))
858            }
859            (OperandClass::Float, Eq) => ins.fcmp(FloatCC::Equal, l, r),
860            (OperandClass::Float, Ne) => ins.fcmp(FloatCC::NotEqual, l, r),
861            (OperandClass::Float, Lt) => ins.fcmp(FloatCC::LessThan, l, r),
862            (OperandClass::Float, Le) => ins.fcmp(FloatCC::LessThanOrEqual, l, r),
863            (OperandClass::Float, Gt) => ins.fcmp(FloatCC::GreaterThan, l, r),
864            (OperandClass::Float, Ge) => ins.fcmp(FloatCC::GreaterThanOrEqual, l, r),
865            (OperandClass::Float, _) => {
866                return Err(anyhow!("bitwise operators are not defined for Float"))
867            }
868            (_, Add) => ins.iadd(l, r),
869            (_, Sub) => ins.isub(l, r),
870            (_, Mul) => ins.imul(l, r),
871            (_, Div) => ins.sdiv(l, r),
872            (_, Rem) => ins.srem(l, r),
873            (_, Eq) => ins.icmp(IntCC::Equal, l, r),
874            (_, Ne) => ins.icmp(IntCC::NotEqual, l, r),
875            (_, Lt) => ins.icmp(IntCC::SignedLessThan, l, r),
876            (_, Le) => ins.icmp(IntCC::SignedLessThanOrEqual, l, r),
877            (_, Gt) => ins.icmp(IntCC::SignedGreaterThan, l, r),
878            (_, Ge) => ins.icmp(IntCC::SignedGreaterThanOrEqual, l, r),
879            (_, BitAnd) => ins.band(l, r),
880            (_, BitOr) => ins.bor(l, r),
881            (_, BitXor) => ins.bxor(l, r),
882            (_, Shl) => ins.ishl(l, r),
883            (_, Shr) => ins.sshr(l, r),
884        };
885        Ok(v)
886    }
887
888    fn terminator(&mut self, term: &Terminator, ret: MirTy) -> Result<()> {
889        match term {
890            Terminator::Goto(b) => {
891                let target = self.cl_blocks[b.0 as usize];
892                self.builder.ins().jump(target, &[]);
893            }
894            Terminator::Branch { cond, then_, else_ } => {
895                let c = self.use_temp(*cond)?;
896                let t = self.cl_blocks[then_.0 as usize];
897                let e = self.cl_blocks[else_.0 as usize];
898                self.builder.ins().brif(c, t, &[], e, &[]);
899            }
900            Terminator::Switch {
901                disc,
902                arms,
903                default,
904            } => {
905                let mut switch = Switch::new();
906                for (value, block) in arms {
907                    switch.set_entry(*value as u128, self.cl_blocks[block.0 as usize]);
908                }
909                let d = self.use_temp(*disc)?;
910                let default_block = self.cl_blocks[default.0 as usize];
911                switch.emit(&mut self.builder, d, default_block);
912            }
913            Terminator::Return(value) => match (value, ret) {
914                (Some(v), r) if r != MirTy::Unit => {
915                    let val = self.use_temp(*v)?;
916                    self.builder.ins().return_(&[val]);
917                }
918                _ => {
919                    self.builder.ins().return_(&[]);
920                }
921            },
922            Terminator::Trap => {
923                self.builder.ins().trap(TrapCode::UnreachableCodeReached);
924            }
925        }
926        Ok(())
927    }
928}
929
930// Re-export for the driver's convenience.
931pub use nova_mir::lower_module;
932
933#[cfg(test)]
934mod tests {
935    // End-to-end execution tests live in the nova-cli integration suite,
936    // where stdout of compiled programs can be captured; compiling MIR here
937    // and running it would print into the test harness output.
938
939    use nova_mir::RtFunc;
940
941    /// Every `RtFunc` must have a matching entry in `nova_runtime::symbols()`.
942    ///
943    /// This crate is the only place the containment can be checked: `nova-mir`
944    /// must not depend on `nova-runtime` (the poll ABI and state layout are
945    /// deliberately declared twice for that reason), and `nova-runtime` does
946    /// not depend on `nova-mir`. This crate depends on both.
947    ///
948    /// The gap this closes is real, not hypothetical: the `rt_funcs!` macro
949    /// guarantees a variant is declared to *both codegen backends*, because
950    /// `RtFunc::ALL` is generated from the same identifier list as the enum.
951    /// It guarantees nothing about `symbols()`, which is a separate,
952    /// hand-maintained table. `compile_jit` builds the JIT's resolver from
953    /// `symbols()` while `declare_runtime` imports all of `RtFunc::ALL`, and
954    /// `cranelift-jit` resolves imports inside `finalize_definitions`, where
955    /// an unresolvable symbol is a `panic!` rather than an `Err`. A variant
956    /// added to `rt_funcs!` and forgotten here therefore compiles clean,
957    /// passes every existing test, and fails the first time compiled code
958    /// actually calls it. The `nova_rt_task_*` entries shipped in exactly that
959    /// state, which is what this test was added for.
960    ///
961    /// Names, not addresses: `symbols()` returns `*const u8` function
962    /// pointers, and comparing those would only restate that the same `as`
963    /// cast appears twice. The symbol string is what the linker and the JIT
964    /// actually match on.
965    #[test]
966    fn every_rt_func_symbol_is_registered_with_the_jit() {
967        let registered: Vec<&str> = nova_runtime::symbols()
968            .into_iter()
969            .map(|(name, _)| name)
970            .collect();
971        let missing: Vec<&str> = RtFunc::ALL
972            .iter()
973            .map(|f| f.symbol())
974            .filter(|s| !registered.contains(s))
975            .collect();
976        assert!(
977            missing.is_empty(),
978            "RtFunc variants with no nova_runtime::symbols() entry: {missing:?}. \
979             Compiled code calling one of these makes cranelift-jit panic in \
980             finalize_definitions; add it to symbols() in nova-runtime/src/lib.rs."
981        );
982    }
983
984    /// The state-object and future-value layouts are declared twice — once in
985    /// `nova-mir` (`async_lower.rs`), which generates code against them, and
986    /// once in `nova-runtime` (`task.rs`), whose executor reads the same bytes
987    /// by raw offset. Neither crate may depend on the other; this one depends
988    /// on both, so this is where the two copies are held together.
989    ///
990    /// **A test that asserted only literal values would not close this gap.**
991    /// `nova-mir`'s own `the_state_layout_constants_match_the_runtime` pins its
992    /// copy to `0`/`1`/`2`/…, which catches an edit to that copy alone but says
993    /// nothing about the other one: both copies could be renumbered together
994    /// and every literal-value assertion would still pass while the layout
995    /// silently changed under an already-compiled program. Equality between the
996    /// two is the property that actually matters, because a disagreement is a
997    /// silent miscompile — the poll function stores the output at one offset
998    /// and `poll_one` reads another, which for a scalar output means a wrong
999    /// answer and for a heap-valued one means a wild pointer.
1000    ///
1001    /// `STATE_MIN_SIZE` is included deliberately: it is the one constant that
1002    /// is *derived* rather than written down, so it can drift from its own
1003    /// definition on either side without any slot index changing.
1004    ///
1005    /// The `as usize` casts only widen — `nova-mir` types slot indices as `u32`
1006    /// to match `Stmt::SetField`'s `index` field, `nova-runtime` types them as
1007    /// `usize` to match pointer arithmetic — so a differing value can never
1008    /// compare equal by truncation.
1009    #[test]
1010    fn the_state_layout_matches_nova_runtimes() {
1011        use nova_runtime::task;
1012        assert_eq!(
1013            nova_mir::STATE_SLOT_TAG as usize,
1014            task::STATE_SLOT_TAG,
1015            "resume tag slot"
1016        );
1017        assert_eq!(
1018            nova_mir::STATE_SLOT_OUTPUT as usize,
1019            task::STATE_SLOT_OUTPUT,
1020            "output slot -- read unconditionally on completion"
1021        );
1022        assert_eq!(
1023            nova_mir::STATE_SLOT_TEMPS as usize,
1024            task::STATE_SLOT_TEMPS,
1025            "first per-temp slot"
1026        );
1027        assert_eq!(
1028            nova_mir::STATE_MIN_SIZE as usize,
1029            task::STATE_MIN_SIZE,
1030            "smallest state object -- a shortfall here is a read past the \
1031             allocation, and it compiles clean"
1032        );
1033        assert_eq!(
1034            nova_mir::FUTURE_SLOT_POLL as usize,
1035            task::FUTURE_SLOT_POLL,
1036            "future word 0: the poll function's address"
1037        );
1038        assert_eq!(
1039            nova_mir::FUTURE_SLOT_STATE as usize,
1040            task::FUTURE_SLOT_STATE,
1041            "future word 1: the state object's address"
1042        );
1043        assert_eq!(
1044            nova_mir::POLL_PENDING,
1045            task::POLL_PENDING,
1046            "the executor re-queues on exactly this value"
1047        );
1048        assert_eq!(
1049            nova_mir::POLL_READY,
1050            task::POLL_READY,
1051            "the executor completes a task on exactly this value and panics on \
1052             anything else"
1053        );
1054    }
1055
1056    /// The reverse direction is deliberately *not* asserted as equality:
1057    /// `symbols()` legitimately carries entries no `RtFunc` names, because
1058    /// codegen reaches some runtime functions without going through the enum
1059    /// (`nova_rt_str_new` is declared directly by `declare_runtime` for
1060    /// `ConstStr`). What must hold is that every registered name is a real,
1061    /// distinct symbol -- a duplicate entry would mean one registration
1062    /// silently shadowing another.
1063    #[test]
1064    fn registered_runtime_symbols_are_distinct() {
1065        let mut names: Vec<&str> = nova_runtime::symbols()
1066            .into_iter()
1067            .map(|(name, _)| name)
1068            .collect();
1069        let before = names.len();
1070        names.sort_unstable();
1071        names.dedup();
1072        assert_eq!(
1073            names.len(),
1074            before,
1075            "nova_runtime::symbols() has a duplicate entry"
1076        );
1077    }
1078}