Skip to main content

tidepool_codegen/
pipeline.rs

1use cranelift_codegen::ir::{self, AbiParam, types};
2use cranelift_codegen::isa::TargetIsa;
3use cranelift_codegen::settings::{self, Configurable};
4use cranelift_codegen::Context;
5use cranelift_codegen::control::ControlPlane;
6use cranelift_jit::{JITBuilder, JITModule};
7use cranelift_module::{Module, Linkage, FuncId};
8use std::sync::Arc;
9
10use crate::debug::LambdaRegistry;
11use crate::stack_map::{StackMapRegistry, RawStackMap};
12
13/// Cranelift JIT compilation pipeline.
14///
15/// Implements the double-compile strategy:
16/// 1. `Context::compile(isa)` to extract stack maps from CompiledCode
17/// 2. `module.define_function()` for executable code (recompiles from IR)
18pub struct CodegenPipeline {
19    /// The JIT module that manages executable memory.
20    ///
21    /// This field is public as an **escape hatch** for advanced use cases and tests
22    /// that need direct access to Cranelift's `JITModule`. Most users should prefer
23    /// the safe wrapper methods on `CodegenPipeline` (e.g., `declare_function`)
24    /// instead of calling into `module` directly.
25    pub module: JITModule,
26    /// Target ISA (needed for Context::compile).
27    pub isa: Arc<dyn TargetIsa>,
28    /// Stack map registry populated during compilation.
29    pub stack_maps: StackMapRegistry,
30    /// Pending stack maps waiting for finalization to get base pointers.
31    /// Stores (func_id, func_size, raw_maps).
32    pending_stack_maps: Vec<(FuncId, u32, Vec<RawStackMap>)>,
33    /// Lambda name registry: (func_id, name). Populated during define_function.
34    lambda_names: Vec<(FuncId, String)>,
35}
36
37impl CodegenPipeline {
38    /// Create a new CodegenPipeline with default x86-64 settings.
39    ///
40    /// `symbols` is a list of (name, pointer) pairs for host functions
41    /// that JIT code can call (e.g., gc_trigger, heap_alloc).
42    pub fn new(symbols: &[(&str, *const u8)]) -> Self {
43        let mut flag_builder = settings::builder();
44        // REQUIRED: enables RBP frame chain for GC stack walking
45        flag_builder.set("preserve_frame_pointers", "true").unwrap();
46        flag_builder.set("opt_level", "speed").unwrap();
47
48        let isa_builder = cranelift_native::builder().expect("host machine not supported");
49        let isa = isa_builder.finish(settings::Flags::new(flag_builder.clone())).unwrap();
50
51        let mut jit_builder = JITBuilder::with_isa(
52            isa.clone(),
53            cranelift_module::default_libcall_names(),
54        );
55
56        for (name, ptr) in symbols {
57            jit_builder.symbol(*name, *ptr);
58        }
59
60        let module = JITModule::new(jit_builder);
61
62        Self {
63            module,
64            isa,
65            stack_maps: StackMapRegistry::new(),
66            pending_stack_maps: Vec::new(),
67            lambda_names: Vec::new(),
68        }
69    }
70
71    /// Create the standard function signature for compiled tidepool functions.
72    ///
73    /// Uses the target ISA's default C ABI calling convention, with vmctx: i64
74    /// as the first parameter and an i64 return value.
75    pub fn make_func_signature(&self) -> ir::Signature {
76        let mut sig = ir::Signature::new(self.isa.default_call_conv());
77        sig.params.push(AbiParam::new(types::I64)); // vmctx pointer
78        sig.returns.push(AbiParam::new(types::I64)); // result pointer
79        sig
80    }
81
82    /// Declare a function in the JIT module.
83    pub fn declare_function(&mut self, name: &str) -> FuncId {
84        let sig = self.make_func_signature();
85        self.module
86            .declare_function(name, Linkage::Export, &sig)
87            .unwrap_or_else(|e| panic!("failed to declare function `{}`: {}", name, e))
88    }
89
90    /// Compile a function using the double-compile strategy.
91    ///
92    /// 1. Calls `Context::compile(isa)` to extract stack maps
93    /// 2. Registers stack maps in the registry
94    /// 3. Calls `module.define_function()` which recompiles for execution
95    ///
96    /// After calling this for all functions, call `finalize()` to make them callable.
97    pub fn define_function(&mut self, func_id: FuncId, ctx: &mut Context) {
98        // First compile: extract stack maps
99        let mut ctrl_plane = ControlPlane::default();
100        let compiled = ctx.compile(self.isa.as_ref(), &mut ctrl_plane)
101            .unwrap_or_else(|e| {
102                panic!("first compilation failed for function ID {:?}: {:?}", func_id, e);
103            });
104
105        let func_size = compiled.buffer.data().len() as u32;
106
107        // Extract stack map data before define_function recompiles
108        let raw_maps: Vec<RawStackMap> = compiled
109            .buffer
110            .user_stack_maps()
111            .iter()
112            .map(|(offset, span, usm)| {
113                let entries: Vec<_> = usm.entries().collect();
114                (*offset, *span, entries)
115            })
116            .collect();
117
118        // Second compile: define in module for execution
119        self.module
120            .define_function(func_id, ctx)
121            .unwrap_or_else(|e| {
122                panic!("define_function failed for FuncId {:?}: {:?}", func_id, e);
123            });
124
125        // Store raw maps and register after finalize.
126        self.pending_stack_maps.push((func_id, func_size, raw_maps));
127    }
128
129    /// Finalize all defined functions, making them callable.
130    /// Also registers stack maps now that we have function base pointers.
131    pub fn finalize(&mut self) {
132        self.module.finalize_definitions()
133            .expect("finalize_definitions failed");
134
135        // Now register stack maps with actual base pointers
136        let pending = std::mem::take(&mut self.pending_stack_maps);
137        for (func_id, func_size, raw_maps) in pending {
138            let base_ptr = self.module.get_finalized_function(func_id) as usize;
139            self.stack_maps.register(base_ptr, func_size, &raw_maps);
140        }
141    }
142
143    /// Get the callable function pointer after finalization.
144    pub fn get_function_ptr(&self, func_id: FuncId) -> *const u8 {
145        self.module.get_finalized_function(func_id)
146    }
147
148    /// Register a lambda name for a function ID (call before finalize).
149    pub fn register_lambda(&mut self, func_id: FuncId, name: String) {
150        self.lambda_names.push((func_id, name));
151    }
152
153    /// Build a LambdaRegistry from all registered lambdas.
154    /// Must be called after `finalize()` so code pointers are available.
155    pub fn build_lambda_registry(&self) -> LambdaRegistry {
156        let mut registry = LambdaRegistry::new();
157        for (func_id, name) in &self.lambda_names {
158            let ptr = self.module.get_finalized_function(*func_id) as usize;
159            registry.register(ptr, name.clone());
160        }
161        registry
162    }
163}