venus_core/compile/
cell.rs

1//! Cell compiler for Venus notebooks.
2//!
3//! Compiles individual cells to dynamic libraries using Cranelift
4//! for fast compilation during development.
5
6use std::collections::hash_map::DefaultHasher;
7use std::fs;
8use std::hash::{Hash, Hasher};
9use std::path::PathBuf;
10use std::process::Command;
11use std::time::Instant;
12
13use crate::graph::CellInfo;
14
15use super::errors::ErrorMapper;
16use super::toolchain::ToolchainManager;
17use super::types::{
18    CompilationResult, CompiledCell, CompilerConfig, dylib_extension, dylib_prefix,
19};
20
21/// Compiles individual cells to dynamic libraries.
22pub struct CellCompiler {
23    /// Compiler configuration
24    config: CompilerConfig,
25
26    /// Toolchain manager
27    toolchain: ToolchainManager,
28
29    /// Path to the universe library (for linking)
30    universe_path: Option<PathBuf>,
31}
32
33impl CellCompiler {
34    /// Create a new cell compiler.
35    pub fn new(config: CompilerConfig, toolchain: ToolchainManager) -> Self {
36        Self {
37            config,
38            toolchain,
39            universe_path: None,
40        }
41    }
42
43    /// Set the universe library path for linking.
44    pub fn with_universe(mut self, path: PathBuf) -> Self {
45        self.universe_path = Some(path);
46        self
47    }
48
49    /// Compile a cell to a dynamic library.
50    pub fn compile(&self, cell: &CellInfo, deps_hash: u64) -> CompilationResult {
51        let source_hash = self.hash_source(&cell.source_code);
52
53        // Check cache
54        if let Some(cached) = self.check_cache(cell, source_hash, deps_hash) {
55            return CompilationResult::Cached(cached);
56        }
57
58        let start = Instant::now();
59
60        // Generate wrapper code
61        let wrapper_code = self.generate_wrapper(cell);
62
63        // Compile - include source_hash in dylib name to force reload on changes
64        match self.compile_to_dylib(cell, &wrapper_code, source_hash) {
65            Ok(dylib_path) => {
66                let compile_time = start.elapsed().as_millis() as u64;
67
68                let compiled = CompiledCell {
69                    cell_id: cell.id,
70                    name: cell.name.clone(),
71                    dylib_path,
72                    entry_symbol: format!("venus_cell_{}", cell.name),
73                    source_hash,
74                    deps_hash,
75                    compile_time_ms: compile_time,
76                };
77
78                // Save to cache
79                self.save_to_cache(&compiled);
80
81                CompilationResult::Success(compiled)
82            }
83            Err(errors) => CompilationResult::Failed {
84                cell_id: cell.id,
85                errors,
86            },
87        }
88    }
89
90    /// Generate the wrapper code for a cell.
91    fn generate_wrapper(&self, cell: &CellInfo) -> String {
92        let mut code = String::new();
93
94        // Header
95        code.push_str("// Auto-generated cell wrapper\n");
96        code.push_str("#![allow(unused_imports)]\n");
97        code.push_str("#![allow(dead_code)]\n\n");
98
99        // Import dependencies from universe (always built, includes rkyv)
100        // NOTE: venus_universe includes user-defined types from the notebook,
101        // external dependencies, and rkyv. The glob import is safe because:
102        // 1. User types are defined in the notebook itself
103        // 2. rkyv::rancor::Error is aliased as RkyvError to avoid conflicts
104        // 3. Cells can shadow imports locally if needed
105        code.push_str("extern crate venus_universe;\n");
106        code.push_str("use venus_universe::*;\n\n");
107
108        // Comment with source location for error mapping (not a real directive)
109        code.push_str(&format!(
110            "// Original source: {}:{}\n",
111            cell.source_file.display(),
112            cell.span.start_line
113        ));
114
115        // The cell function itself (from source)
116        code.push_str(&cell.source_code);
117        code.push_str("\n\n");
118
119        // Generate FFI entry point
120        code.push_str(&self.generate_ffi_entry(cell));
121
122        code
123    }
124
125    /// Generate the FFI entry point for a cell.
126    fn generate_ffi_entry(&self, cell: &CellInfo) -> String {
127        let mut code = String::new();
128
129        let fn_name = &cell.name;
130        let entry_name = format!("venus_cell_{}", fn_name);
131
132        // Determine return handling
133        let returns_result = cell.return_type.starts_with("Result<");
134
135        code.push_str("/// FFI entry point for the cell.\n");
136        code.push_str("/// \n");
137        code.push_str("/// # Safety\n");
138        code.push_str("/// This function is called from the Venus runtime.\n");
139        code.push_str("#[no_mangle]\n");
140        code.push_str(&format!("pub unsafe extern \"C\" fn {}(\n", entry_name));
141
142        // Input parameters (serialized)
143        for (i, dep) in cell.dependencies.iter().enumerate() {
144            code.push_str(&format!("    {}_ptr: *const u8,\n", dep.param_name));
145            code.push_str(&format!("    {}_len: usize,\n", dep.param_name));
146            if i < cell.dependencies.len() - 1 {
147                code.push('\n');
148            }
149        }
150
151        // Widget values input
152        code.push_str("    widget_values_ptr: *const u8,\n");
153        code.push_str("    widget_values_len: usize,\n");
154
155        // Output parameters
156        code.push_str("    out_ptr: *mut *mut u8,\n");
157        code.push_str("    out_len: *mut usize,\n");
158        code.push_str(") -> i32 {\n");
159
160        // Set up widget context with incoming values
161        code.push_str("    // Set up widget context\n");
162        code.push_str("    use std::collections::HashMap;\n");
163        code.push_str("    let widget_values: HashMap<String, WidgetValue> = if widget_values_len > 0 {\n");
164        code.push_str("        let json_slice = std::slice::from_raw_parts(widget_values_ptr, widget_values_len);\n");
165        code.push_str("        venus_universe::serde_json::from_slice(json_slice).unwrap_or_default()\n");
166        code.push_str("    } else {\n");
167        code.push_str("        HashMap::new()\n");
168        code.push_str("    };\n");
169        code.push_str("    set_widget_context(WidgetContext::with_values(widget_values));\n\n");
170
171        // Deserialize inputs using rkyv (zero-copy access then deserialize)
172        for dep in &cell.dependencies {
173            // Get the base type without reference
174            let base_type = dep.param_type.trim_start_matches('&').trim();
175
176            code.push_str(&format!(
177                "    let {}_bytes = std::slice::from_raw_parts({}_ptr, {}_len);\n",
178                dep.param_name, dep.param_name, dep.param_name
179            ));
180            // Access archived data (zero-copy)
181            code.push_str(&format!(
182                "    let {}_archived = match rkyv::access::<rkyv::Archived<{}>, RkyvError>({}_bytes) {{\n",
183                dep.param_name, base_type, dep.param_name
184            ));
185            code.push_str("        Ok(v) => v,\n");
186            code.push_str("        Err(_) => return -1, // Access error\n");
187            code.push_str("    };\n");
188            // Deserialize to owned type
189            code.push_str(&format!(
190                "    let {}: {} = match rkyv::deserialize::<_, RkyvError>({}_archived) {{\n",
191                dep.param_name, base_type, dep.param_name
192            ));
193            code.push_str("        Ok(v) => v,\n");
194            code.push_str("        Err(_) => return -1, // Deserialization error\n");
195            code.push_str("    };\n\n");
196        }
197
198        // Build argument list for cell call
199        let args: Vec<String> = cell
200            .dependencies
201            .iter()
202            .map(|d| {
203                if d.is_ref {
204                    if d.is_mut {
205                        format!("&mut {}", d.param_name)
206                    } else {
207                        format!("&{}", d.param_name)
208                    }
209                } else {
210                    d.param_name.clone()
211                }
212            })
213            .collect();
214
215        // Wrap cell execution in catch_unwind for panic safety.
216        // This prevents user code panics from crashing the Venus server.
217        code.push_str("    // Wrap execution in catch_unwind for panic safety\n");
218        code.push_str("    let execution_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {\n");
219
220        // Call the cell function (inside catch_unwind)
221        if returns_result {
222            code.push_str(&format!(
223                "        let result = match {}({}) {{\n",
224                fn_name,
225                args.join(", ")
226            ));
227            code.push_str("            Ok(v) => v,\n");
228            code.push_str("            Err(_) => return Err(-2i32), // Cell returned error\n");
229            code.push_str("        };\n\n");
230        } else {
231            code.push_str(&format!(
232                "        let result = {}({});\n\n",
233                fn_name,
234                args.join(", ")
235            ));
236        }
237
238        // Create debug display string (inside catch_unwind)
239        code.push_str("        let display_str = format!(\"{:?}\", result);\n");
240        code.push_str("        let display_bytes = display_str.as_bytes();\n\n");
241
242        // Serialize output with rkyv (inside catch_unwind)
243        code.push_str("        let rkyv_data = match rkyv::to_bytes::<RkyvError>(&result) {\n");
244        code.push_str("            Ok(v) => v,\n");
245        code.push_str("            Err(_) => return Err(-3i32), // Serialization error\n");
246        code.push_str("        };\n\n");
247
248        // Capture widgets from context (inside catch_unwind, after cell execution)
249        code.push_str("        // Capture registered widgets\n");
250        code.push_str("        let widgets_json = if let Some(mut ctx) = take_widget_context() {\n");
251        code.push_str("            let widgets = ctx.take_widgets();\n");
252        code.push_str("            if widgets.is_empty() { Vec::new() } else { venus_universe::serde_json::to_vec(&widgets).unwrap_or_default() }\n");
253        code.push_str("        } else { Vec::new() };\n\n");
254
255        // Format: display_len (8 bytes LE) | display_bytes | widgets_len (8 bytes LE) | widgets_json | rkyv_data
256        code.push_str("        let display_len = display_bytes.len() as u64;\n");
257        code.push_str("        let widgets_len = widgets_json.len() as u64;\n");
258        code.push_str("        let total_len = 8 + display_bytes.len() + 8 + widgets_json.len() + rkyv_data.len();\n");
259        code.push_str("        let mut output = Vec::with_capacity(total_len);\n");
260        code.push_str("        output.extend_from_slice(&display_len.to_le_bytes());\n");
261        code.push_str("        output.extend_from_slice(display_bytes);\n");
262        code.push_str("        output.extend_from_slice(&widgets_len.to_le_bytes());\n");
263        code.push_str("        output.extend_from_slice(&widgets_json);\n");
264        code.push_str("        output.extend_from_slice(&rkyv_data);\n\n");
265        code.push_str("        Ok(output)\n");
266        code.push_str("    }));\n\n");
267
268        // Handle catch_unwind result
269        code.push_str("    // Handle panic or success\n");
270        code.push_str("    match execution_result {\n");
271        code.push_str("        Ok(Ok(output)) => {\n");
272        code.push_str("            let len = output.len();\n");
273        code.push_str("            let ptr = output.as_ptr();\n");
274        code.push_str("            std::mem::forget(output);\n");
275        code.push_str("            *out_ptr = ptr as *mut u8;\n");
276        code.push_str("            *out_len = len;\n");
277        code.push_str("            0 // Success\n");
278        code.push_str("        }\n");
279        code.push_str("        Ok(Err(code)) => code, // Cell error or serialization error\n");
280        code.push_str("        Err(_) => -4, // Panic occurred\n");
281        code.push_str("    }\n");
282        code.push_str("}\n");
283
284        code
285    }
286
287    /// Compile wrapper code to a dynamic library.
288    fn compile_to_dylib(
289        &self,
290        cell: &CellInfo,
291        wrapper_code: &str,
292        source_hash: u64,
293    ) -> std::result::Result<PathBuf, Vec<super::CompileError>> {
294        let build_dir = self.config.cell_build_dir();
295        fs::create_dir_all(&build_dir).map_err(|e| {
296            super::CompileError::simple(format!("Failed to create build directory: {}", e))
297        })?;
298
299        // Write wrapper source
300        let src_file = build_dir.join(format!("{}.rs", cell.name));
301        fs::write(&src_file, wrapper_code)
302            .map_err(|e| super::CompileError::simple(format!("Failed to write source: {}", e)))?;
303
304        // Output path - include hash to force dlopen to reload on changes
305        // (Linux caches shared libraries by path, so we need unique paths)
306        let dylib_name = format!("{}cell_{}_{:x}.{}", dylib_prefix(), cell.name, source_hash, dylib_extension());
307        let dylib_path = build_dir.join(&dylib_name);
308
309        // Clean up old dylibs for this cell (they accumulate with different hashes)
310        let cell_prefix = format!("{}cell_{}_", dylib_prefix(), cell.name);
311        if let Ok(entries) = fs::read_dir(&build_dir) {
312            for entry in entries.flatten() {
313                let name = entry.file_name();
314                let name_str = name.to_string_lossy();
315                if name_str.starts_with(&cell_prefix) && name_str != dylib_name {
316                    let _ = fs::remove_file(entry.path());
317                }
318            }
319        }
320
321        // Build rustc command
322        let mut cmd = Command::new(self.toolchain.rustc_path());
323
324        cmd.arg(&src_file)
325            .arg("--crate-type=cdylib")
326            .arg("--edition=2021")
327            .arg("-o")
328            .arg(&dylib_path)
329            .arg("--error-format=json");
330
331        // Add Cranelift backend if available and configured
332        if self.config.use_cranelift && self.toolchain.has_cranelift() {
333            for flag in self.toolchain.cranelift_flags() {
334                cmd.arg(&flag);
335            }
336        }
337
338        // Optimization level
339        cmd.arg(format!("-Copt-level={}", self.config.opt_level));
340
341        // Debug info
342        if self.config.debug_info {
343            cmd.arg("-g");
344        }
345
346        // Link against universe rlib for compilation
347        if let Some(universe_dylib) = &self.universe_path {
348            // The universe build directory contains both cdylib and rlib
349            // We need the rlib for rustc compilation and cdylib for runtime
350            let universe_build_dir = universe_dylib.parent().unwrap_or(universe_dylib);
351            let target_release_dir = universe_build_dir.join("target").join("release");
352            let deps_dir = target_release_dir.join("deps");
353
354            // Add search paths for dependencies
355            cmd.arg("-L").arg(&target_release_dir);
356            cmd.arg("-L").arg(&deps_dir);
357
358            // Find and link the universe rlib using --extern
359            let rlib_path = target_release_dir.join("libvenus_universe.rlib");
360            if rlib_path.exists() {
361                cmd.arg("--extern").arg(format!("venus_universe={}", rlib_path.display()));
362            } else {
363                // Fallback: try to find it in deps
364                if let Ok(entries) = std::fs::read_dir(&deps_dir) {
365                    for entry in entries.flatten() {
366                        let name = entry.file_name();
367                        let name_str = name.to_string_lossy();
368                        if name_str.starts_with("libvenus_universe-") && name_str.ends_with(".rlib") {
369                            cmd.arg("--extern").arg(format!("venus_universe={}", entry.path().display()));
370                            break;
371                        }
372                    }
373                }
374            }
375
376            // Add rpath for runtime linking (Unix-like systems)
377            #[cfg(any(target_os = "linux", target_os = "macos"))]
378            {
379                // Runtime links against cdylib in the universe build dir
380                cmd.arg(format!("-Clink-arg=-Wl,-rpath,{}", universe_build_dir.display()));
381            }
382        }
383
384        // Extra flags
385        for flag in &self.config.extra_rustc_flags {
386            cmd.arg(flag);
387        }
388
389        // Run compilation
390        let output = cmd
391            .output()
392            .map_err(|e| super::CompileError::simple(format!("Failed to run rustc: {}", e)))?;
393
394        if output.status.success() {
395            Ok(dylib_path)
396        } else {
397            // Parse errors
398            let stderr = String::from_utf8_lossy(&output.stderr);
399            let mapper = ErrorMapper::new(cell.source_file.clone());
400            let errors = mapper.parse_rustc_output(&stderr);
401
402            if errors.is_empty() {
403                // Fallback if JSON parsing failed
404                Err(super::CompileError::simple_rendered(stderr.to_string()))
405            } else {
406                Err(errors)
407            }
408        }
409    }
410
411    /// Hash the source code.
412    fn hash_source(&self, source: &str) -> u64 {
413        let mut hasher = DefaultHasher::new();
414        source.hash(&mut hasher);
415        hasher.finish()
416    }
417
418    /// Check if a cached compilation exists.
419    fn check_cache(
420        &self,
421        cell: &CellInfo,
422        source_hash: u64,
423        deps_hash: u64,
424    ) -> Option<CompiledCell> {
425        let cache_file = self.cache_path(cell);
426        if !cache_file.exists() {
427            return None;
428        }
429
430        // Read cache metadata
431        let meta_file = self.cache_meta_path(&cell.name);
432        if let Ok(meta) = fs::read_to_string(&meta_file) {
433            let lines: Vec<&str> = meta.lines().collect();
434            if lines.len() >= 2
435                && let (Ok(cached_src), Ok(cached_deps)) =
436                    (lines[0].parse::<u64>(), lines[1].parse::<u64>())
437                && cached_src == source_hash
438                && cached_deps == deps_hash
439            {
440                return Some(CompiledCell {
441                    cell_id: cell.id,
442                    name: cell.name.clone(),
443                    dylib_path: cache_file,
444                    entry_symbol: format!("venus_cell_{}", cell.name),
445                    source_hash,
446                    deps_hash,
447                    compile_time_ms: 0,
448                });
449            }
450        }
451
452        None
453    }
454
455    /// Save compilation result to cache.
456    fn save_to_cache(&self, compiled: &CompiledCell) {
457        let meta_file = self.cache_meta_path(&compiled.name);
458
459        // Ensure cache directory exists
460        if let Some(parent) = meta_file.parent()
461            && let Err(e) = fs::create_dir_all(parent) {
462                tracing::warn!("Failed to create cache directory: {}", e);
463                return;
464            }
465
466        let meta = format!("{}\n{}", compiled.source_hash, compiled.deps_hash);
467        // Cache save is opportunistic; failure doesn't affect correctness
468        if let Err(e) = fs::write(&meta_file, meta) {
469            tracing::warn!("Failed to save cell cache: {}", e);
470        }
471    }
472
473    /// Get the cache path for a cell.
474    fn cache_path(&self, cell: &CellInfo) -> PathBuf {
475        let filename = format!("{}cell_{}.{}", dylib_prefix(), cell.name, dylib_extension());
476        self.config.cache_dir.join("cells").join(filename)
477    }
478
479    /// Get the cache metadata path by cell name.
480    fn cache_meta_path(&self, name: &str) -> PathBuf {
481        self.config
482            .cache_dir
483            .join("cells")
484            .join(format!("{}.meta", name))
485    }
486}
487
488#[cfg(test)]
489mod tests {
490    use super::*;
491    use crate::graph::{CellId, Dependency, SourceSpan};
492
493    fn make_test_cell() -> CellInfo {
494        CellInfo {
495            id: CellId::new(0),
496            name: "test_cell".to_string(),
497            display_name: "test_cell".to_string(),
498            dependencies: vec![],
499            return_type: "i32".to_string(),
500            doc_comment: None,
501            source_code: "pub fn test_cell() -> i32 { 42 }".to_string(),
502            source_file: PathBuf::from("test.rs"),
503            span: SourceSpan {
504                start_line: 1,
505                start_col: 0,
506                end_line: 1,
507                end_col: 30,
508            },
509        }
510    }
511
512    #[test]
513    fn test_generate_wrapper_simple() {
514        let config = CompilerConfig::default();
515        let toolchain = ToolchainManager::new().unwrap();
516        let compiler = CellCompiler::new(config, toolchain);
517
518        let cell = make_test_cell();
519        let wrapper = compiler.generate_wrapper(&cell);
520
521        assert!(wrapper.contains("venus_cell_test_cell"));
522        assert!(wrapper.contains("pub fn test_cell() -> i32"));
523        assert!(wrapper.contains("#[no_mangle]"));
524    }
525
526    #[test]
527    fn test_generate_wrapper_with_deps() {
528        let config = CompilerConfig::default();
529        let toolchain = ToolchainManager::new().unwrap();
530        let compiler = CellCompiler::new(config, toolchain);
531
532        let cell = CellInfo {
533            id: CellId::new(1),
534            name: "process".to_string(),
535            display_name: "process".to_string(),
536            dependencies: vec![Dependency {
537                param_name: "config".to_string(),
538                param_type: "Config".to_string(),
539                is_ref: true,
540                is_mut: false,
541            }],
542            return_type: "Output".to_string(),
543            doc_comment: None,
544            source_code: "pub fn process(config: &Config) -> Output { todo!() }".to_string(),
545            source_file: PathBuf::from("test.rs"),
546            span: SourceSpan {
547                start_line: 5,
548                start_col: 0,
549                end_line: 5,
550                end_col: 50,
551            },
552        };
553
554        let wrapper = compiler.generate_wrapper(&cell);
555
556        assert!(wrapper.contains("config_ptr: *const u8"));
557        assert!(wrapper.contains("config_len: usize"));
558        assert!(wrapper.contains("rkyv::access"));
559    }
560
561    #[test]
562    fn test_hash_source() {
563        let config = CompilerConfig::default();
564        let toolchain = ToolchainManager::new().unwrap();
565        let compiler = CellCompiler::new(config, toolchain);
566
567        let hash1 = compiler.hash_source("fn foo() {}");
568        let hash2 = compiler.hash_source("fn foo() {}");
569        let hash3 = compiler.hash_source("fn bar() {}");
570
571        assert_eq!(hash1, hash2);
572        assert_ne!(hash1, hash3);
573    }
574}