oxur_repl/
wrapper.rs

1//! Rust AST wrapper for REPL scaffolding
2//!
3//! Wraps user code with necessary boilerplate for REPL execution:
4//! - Entry point functions
5//! - Variable store access
6//! - Export declarations
7//!
8//! Based on ODD-0026 Section 2.1
9
10use thiserror::Error;
11
12/// Wrapper errors
13#[derive(Debug, Error)]
14pub enum WrapperError {
15    #[error("Invalid code structure: {0}")]
16    InvalidCode(String),
17
18    #[error("Variable name conflict: {0}")]
19    NameConflict(String),
20}
21
22pub type Result<T> = std::result::Result<T, WrapperError>;
23
24/// Wraps Rust code for REPL execution
25///
26/// Adds necessary scaffolding for dynamic library compilation and execution.
27///
28/// # Examples
29///
30/// ```
31/// use oxur_repl::wrapper::RustAstWrapper;
32///
33/// let wrapper = RustAstWrapper::new();
34///
35/// let user_code = "let x = 42;\nprintln!(\"x = {}\", x);";
36/// let wrapped = wrapper.wrap("test_key", user_code, None).unwrap();
37///
38/// // wrapped contains:
39/// // - #[no_mangle]
40/// // - pub extern "C" fn oxur_eval_test_key()
41/// // - user code inside the function
42/// ```
43#[derive(Debug, Clone)]
44pub struct RustAstWrapper {
45    /// Whether to include debug information
46    debug: bool,
47}
48
49impl RustAstWrapper {
50    /// Create a new wrapper
51    pub fn new() -> Self {
52        Self { debug: false }
53    }
54
55    /// Create a new wrapper with debug mode
56    pub fn with_debug(mut self, debug: bool) -> Self {
57        self.debug = debug;
58        self
59    }
60
61    /// Wrap user code for REPL execution
62    ///
63    /// # Arguments
64    ///
65    /// * `cache_key` - Cache key used for function naming
66    /// * `user_code` - User's Rust code to wrap
67    ///
68    /// # Returns
69    ///
70    /// Complete Rust source code ready for compilation
71    ///
72    /// # Errors
73    ///
74    /// Returns error if the user code contains invalid patterns
75    pub fn wrap(
76        &self,
77        cache_key: impl AsRef<str>,
78        user_code: impl AsRef<str>,
79        source_map: Option<&oxur_smap::SourceMap>,
80    ) -> Result<String> {
81        let cache_key = cache_key.as_ref();
82        let user_code = user_code.as_ref();
83
84        // Validate cache key is valid identifier
85        if !is_valid_identifier(cache_key) {
86            return Err(WrapperError::InvalidCode(format!(
87                "Cache key must be valid identifier: {}",
88                cache_key
89            )));
90        }
91
92        // Parse user code into AST
93        let mut user_stmts = self.parse_user_code(user_code)?;
94
95        // Check if last statement is an expression (needs result capture)
96        let needs_result_capture = if let Some(last_stmt) = user_stmts.last() {
97            matches!(last_stmt, syn::Stmt::Expr(_, None))
98        } else {
99            false
100        };
101
102        // If last statement is an expression, wrap it to capture the result
103        if needs_result_capture {
104            if let Some(syn::Stmt::Expr(expr, None)) = user_stmts.pop() {
105                // Generate code to capture result in a global static
106                // The subprocess will export a function to read this after execution
107                let result_capture = quote::quote! {
108                    {
109                        let __result_value = #expr;
110                        let __result_string = format!("{:?}", __result_value);
111                        // Store in a global variable that subprocess can read
112                        unsafe {
113                            OXUR_RESULT_BUFFER = Some(__result_string);
114                        }
115                    }
116                };
117
118                // Parse the generated code into a statement
119                let result_stmt: syn::Stmt = syn::parse2(result_capture).map_err(|e| {
120                    WrapperError::InvalidCode(format!("Failed to parse result capture: {}", e))
121                })?;
122
123                user_stmts.push(result_stmt);
124            }
125        }
126
127        // Generate function name identifier
128        let fn_name =
129            syn::Ident::new(&format!("oxur_eval_{}", cache_key), proc_macro2::Span::call_site());
130
131        // Generate wrapper function with global result buffer
132        let wrapped_fn = if needs_result_capture {
133            quote::quote! {
134                // Global buffer for storing expression results
135                static mut OXUR_RESULT_BUFFER: Option<String> = None;
136
137                #[no_mangle]
138                pub extern "C" fn #fn_name() {
139                    #(#user_stmts)*
140                }
141
142                // Export function to read the result buffer
143                #[no_mangle]
144                pub extern "C" fn oxur_get_result() -> *const u8 {
145                    unsafe {
146                        if let Some(ref s) = OXUR_RESULT_BUFFER {
147                            s.as_ptr()
148                        } else {
149                            std::ptr::null()
150                        }
151                    }
152                }
153
154                #[no_mangle]
155                pub extern "C" fn oxur_get_result_len() -> usize {
156                    unsafe {
157                        OXUR_RESULT_BUFFER.as_ref().map(|s| s.len()).unwrap_or(0)
158                    }
159                }
160            }
161        } else {
162            quote::quote! {
163                #[no_mangle]
164                pub extern "C" fn #fn_name() {
165                    #(#user_stmts)*
166                }
167            }
168        };
169
170        // Parse back into syn::File for proper formatting
171        let file: syn::File = syn::parse2(wrapped_fn).map_err(|e| {
172            WrapperError::InvalidCode(format!("Failed to parse generated code: {}", e))
173        })?;
174
175        // Format with prettyplease
176        let formatted = prettyplease::unparse(&file);
177
178        // Add header
179        let mut output = String::new();
180        output.push_str("// Generated by Oxur REPL wrapper\n");
181        output.push_str("// Do not edit manually\n\n");
182
183        if self.debug {
184            output.push_str("// DEBUG MODE ENABLED\n\n");
185        }
186
187        output.push_str(&formatted);
188
189        // Post-process: insert source map comments if source_map is provided
190        let final_output = if let Some(smap) = source_map {
191            self.insert_source_map_comments(&output, user_stmts.len(), smap)
192        } else {
193            output
194        };
195
196        Ok(final_output)
197    }
198
199    /// Parse user code into a list of statements
200    ///
201    /// Handles both complete files and bare statements.
202    fn parse_user_code(&self, code: &str) -> Result<Vec<syn::Stmt>> {
203        // Try parsing as a file first
204        if let Ok(file) = syn::parse_str::<syn::File>(code) {
205            // Convert all items to statements
206            let mut stmts = Vec::new();
207            for item in file.items {
208                // Keep all items as statements, including function definitions
209                stmts.push(syn::Stmt::Item(item));
210            }
211            return Ok(stmts);
212        }
213
214        // Try parsing as a block (statements)
215        let wrapped_code = format!("{{ {} }}", code);
216        if let Ok(block) = syn::parse_str::<syn::Block>(&wrapped_code) {
217            return Ok(block.stmts);
218        }
219
220        // Try parsing as a single expression
221        if let Ok(expr) = syn::parse_str::<syn::Expr>(code) {
222            // Wrap expression in a statement
223            return Ok(vec![syn::Stmt::Expr(expr, None)]);
224        }
225
226        // Failed to parse
227        Err(WrapperError::InvalidCode(format!("Could not parse user code as valid Rust: {}", code)))
228    }
229
230    /// Wrap code with variable store access
231    ///
232    /// Generates code that:
233    /// 1. Loads specified variables from VariableStore
234    /// 2. Executes user code
235    /// 3. Stores variables back to VariableStore
236    /// 4. (Phase 4) Adds source map comments for error translation
237    ///
238    /// # Arguments
239    ///
240    /// * `cache_key` - Cache key for function naming
241    /// * `user_code` - User's Rust code to wrap
242    /// * `variables` - Variables to load/store (name, type) pairs
243    /// * `_source_map` - Optional source map for Phase 4 integration (currently unused)
244    ///
245    /// # Returns
246    ///
247    /// Complete Rust source code with VariableStore integration
248    ///
249    /// # Examples
250    ///
251    /// ```no_run
252    /// use oxur_repl::wrapper::RustAstWrapper;
253    ///
254    /// let wrapper = RustAstWrapper::new();
255    /// let code = "let z = x + y;";
256    /// let vars = vec![("x".to_string(), "i32".to_string()),
257    ///                 ("y".to_string(), "i32".to_string())];
258    /// let wrapped = wrapper.wrap_with_store("test", code, &vars, None).unwrap();
259    /// // Generated code will load x and y, execute code, store z back
260    /// ```
261    pub fn wrap_with_store(
262        &self,
263        cache_key: impl AsRef<str>,
264        user_code: impl AsRef<str>,
265        variables: &[(String, String)],
266        _source_map: Option<&oxur_smap::SourceMap>,
267    ) -> Result<String> {
268        let cache_key = cache_key.as_ref();
269        let user_code = user_code.as_ref();
270
271        // Validate cache key
272        if !is_valid_identifier(cache_key) {
273            return Err(WrapperError::InvalidCode(format!(
274                "Cache key must be valid identifier: {}",
275                cache_key
276            )));
277        }
278
279        // Parse user code
280        let user_stmts = self.parse_user_code(user_code)?;
281
282        // Extract new variables defined in user code
283        let new_vars = self.extract_variables(user_code);
284
285        // Generate variable load statements
286        let var_loads = self.generate_var_loads(variables)?;
287
288        // Generate variable store statements (for existing + new variables)
289        let mut all_vars = variables.to_vec();
290        for (name, ty) in &new_vars {
291            // Add new variables that aren't already in the list
292            if !all_vars.iter().any(|(n, _)| n == name) {
293                all_vars.push((name.clone(), ty.clone()));
294            }
295        }
296        let var_stores = self.generate_var_stores(&all_vars)?;
297
298        // Generate function name
299        let fn_name =
300            syn::Ident::new(&format!("oxur_eval_{}", cache_key), proc_macro2::Span::call_site());
301
302        // Generate complete function (WITHOUT source map comments in AST)
303        // We'll add comments via string post-processing below
304        let wrapped_fn = quote::quote! {
305            #[no_mangle]
306            pub extern "C" fn #fn_name() {
307                #(#var_loads)*
308
309                #(#user_stmts)*
310
311                #(#var_stores)*
312            }
313        };
314
315        // Parse and format
316        let file: syn::File = syn::parse2(wrapped_fn).map_err(|e| {
317            WrapperError::InvalidCode(format!("Failed to parse generated code: {}", e))
318        })?;
319        let formatted = prettyplease::unparse(&file);
320
321        // Add header
322        let mut output = String::new();
323        output.push_str("// Generated by Oxur REPL wrapper\n");
324        output.push_str("// Do not edit manually\n\n");
325
326        if self.debug {
327            output.push_str("// DEBUG MODE ENABLED\n\n");
328        }
329
330        output.push_str(&formatted);
331
332        // Post-process: insert source map comments if source_map is provided
333        let final_output = if let Some(source_map) = _source_map {
334            self.insert_source_map_comments(&output, user_stmts.len(), source_map)
335        } else {
336            output
337        };
338
339        Ok(final_output)
340    }
341
342    /// Generate variable load statements
343    ///
344    /// Creates code to load variables from the global VariableStore.
345    fn generate_var_loads(&self, variables: &[(String, String)]) -> Result<Vec<syn::Stmt>> {
346        let mut stmts = Vec::new();
347
348        for (var_name, var_type) in variables {
349            // Parse the type
350            let ty: syn::Type = syn::parse_str(var_type).map_err(|e| {
351                WrapperError::InvalidCode(format!("Invalid type '{}': {}", var_type, e))
352            })?;
353
354            let var_ident = syn::Ident::new(var_name, proc_macro2::Span::call_site());
355
356            // Generate: let var_name: var_type = oxur_repl::subprocess::with_store(|store| { ... });
357            let stmt = quote::quote! {
358                let #var_ident: #ty = oxur_repl::subprocess::with_store(|store| {
359                    store.get::<#ty>(#var_name)
360                        .cloned()
361                        .unwrap_or_default()
362                });
363            };
364
365            let parsed_stmt: syn::Stmt = syn::parse2(stmt).map_err(|e| {
366                WrapperError::InvalidCode(format!("Failed to parse load statement: {}", e))
367            })?;
368            stmts.push(parsed_stmt);
369        }
370
371        Ok(stmts)
372    }
373
374    /// Generate variable store statements
375    ///
376    /// Creates code to store variables back to the global VariableStore.
377    fn generate_var_stores(&self, variables: &[(String, String)]) -> Result<Vec<syn::Stmt>> {
378        let mut stmts = Vec::new();
379
380        for (var_name, _var_type) in variables {
381            let var_ident = syn::Ident::new(var_name, proc_macro2::Span::call_site());
382
383            // Generate: oxur_repl::subprocess::with_store(|store| { store.set(...); });
384            let stmt = quote::quote! {
385                oxur_repl::subprocess::with_store(|store| {
386                    store.set(#var_name.to_string(), #var_ident);
387                });
388            };
389
390            let parsed_stmt: syn::Stmt = syn::parse2(stmt).map_err(|e| {
391                WrapperError::InvalidCode(format!("Failed to parse store statement: {}", e))
392            })?;
393            stmts.push(parsed_stmt);
394        }
395
396        Ok(stmts)
397    }
398
399    /// Extract variable definitions from user code
400    ///
401    /// Uses TypeInference to find all variable bindings and their types.
402    ///
403    /// # Returns
404    ///
405    /// A list of (variable_name, type_name) pairs found in the code.
406    ///
407    /// # Examples
408    ///
409    /// ```
410    /// use oxur_repl::wrapper::RustAstWrapper;
411    ///
412    /// let wrapper = RustAstWrapper::new();
413    /// let code = "let x: i32 = 42; let y = 100;";
414    /// let vars = wrapper.extract_variables(code);
415    ///
416    /// assert!(vars.iter().any(|(name, _)| name == "x"));
417    /// ```
418    pub fn extract_variables(&self, user_code: impl AsRef<str>) -> Vec<(String, String)> {
419        use crate::type_inference::TypeInference;
420
421        let inference = TypeInference::new();
422        let type_infos = inference.infer_from_code(user_code);
423
424        // Convert TypeInfo to (name, type_string) pairs
425        type_infos
426            .into_iter()
427            .map(|(name, type_info)| (name, type_info.type_name.clone()))
428            .collect()
429    }
430
431    /// Annotate statements with source map comments for error translation
432    ///
433    /// Adds /* oxur_node=N */ comments before each statement based on the
434    /// source map. This enables ErrorTranslator to map Rust compiler errors
435    /// back to original Oxur source positions.
436    ///
437    /// # Phase 4: Source Map Integration
438    ///
439    /// This method implements the core of Phase 4's error position translation.
440    /// The generated comments allow ErrorTranslator to:
441    /// 1. Parse rustc error with file position
442    /// 2. Extract /* oxur_node=N */ comment near error column
443    /// 3. Look up NodeId in SourceMap to find original Oxur position
444    ///
445    /// # Arguments
446    ///
447    /// * `stmts` - Original statements to annotate
448    /// * `source_map` - Source map containing NodeId → SourcePos mappings
449    ///
450    /// # Returns
451    ///
452    /// New list of statements with source map comments inserted
453    ///
454    /// # Note
455    ///
456    /// Currently uses a simple heuristic: assign sequential NodeIds to statements.
457    /// In a full implementation, the parser/lowerer would track which NodeId
458    /// corresponds to which AST node, and we'd look those up here.
459    /// Insert source map comments into generated code (string-based post-processing)
460    ///
461    /// Since syn/quote don't preserve comments, we post-process the generated string.
462    /// This inserts /* oxur_node=N */ comments before user code statements.
463    fn insert_source_map_comments(
464        &self,
465        generated_code: &str,
466        num_user_stmts: usize,
467        source_map: &oxur_smap::SourceMap,
468    ) -> String {
469        let stats = source_map.stats();
470        let mut lines: Vec<String> = generated_code.lines().map(|s| s.to_string()).collect();
471
472        // Find the function body (after the opening brace of the function)
473        let mut in_function_body = false;
474        let mut inserted_count = 0;
475
476        // We'll insert comments before user statements
477        // User statements start after variable loads and before variable stores
478        // Look for patterns like "let x: i32 = oxur_repl::subprocess::with_store"
479        // which are variable loads, and statements after those are user code
480
481        let mut insert_indices = Vec::new();
482
483        for (idx, line) in lines.iter().enumerate() {
484            // Skip until we're inside the function body
485            if line.contains("pub extern \"C\" fn") {
486                in_function_body = true;
487                continue;
488            }
489
490            if !in_function_body {
491                continue;
492            }
493
494            // Look for user code statements (not variable load/store)
495            // User statements are after all "let x: T = ... with_store" lines
496            // and before "oxur_repl::subprocess::with_store(|store| { store.set"
497            let is_var_load = line.contains("with_store(|store| {") && line.contains("store.get");
498            let is_var_store = line.contains("with_store(|store| {") && line.contains("store.set");
499
500            // If it's not a variable load/store and has actual code (not just braces/comments)
501            if !is_var_load
502                && !is_var_store
503                && !line.trim().is_empty()
504                && !line.trim().starts_with('}')
505                && !line.trim().starts_with("//")
506                && inserted_count < num_user_stmts
507            {
508                // Check if this looks like a user statement
509                // (contains let, or is an expression statement)
510                if line.trim().starts_with("let ")
511                    || (!line.contains("with_store") && line.trim().ends_with(';'))
512                {
513                    insert_indices.push(idx);
514                    inserted_count += 1;
515                }
516            }
517        }
518
519        // Insert comments at the identified positions (in reverse to maintain indices)
520        for (comment_idx, &line_idx) in insert_indices.iter().enumerate().rev() {
521            let offset = num_user_stmts.saturating_sub(comment_idx);
522            let node_id = stats.surface_nodes.saturating_sub(offset) as u32;
523            let comment = format!(
524                "{}/* oxur_node={} */",
525                " ".repeat(lines[line_idx].len() - lines[line_idx].trim_start().len()), // Match indentation
526                node_id
527            );
528            lines.insert(line_idx, comment);
529        }
530
531        lines.join("\n")
532    }
533}
534
535impl Default for RustAstWrapper {
536    fn default() -> Self {
537        Self::new()
538    }
539}
540
541/// Check if a string is a valid Rust identifier
542fn is_valid_identifier(s: &str) -> bool {
543    if s.is_empty() {
544        return false;
545    }
546
547    let mut chars = s.chars();
548
549    // First character must be alphabetic or underscore
550    if let Some(first) = chars.next() {
551        if !first.is_alphabetic() && first != '_' {
552            return false;
553        }
554    }
555
556    // Rest must be alphanumeric or underscore
557    chars.all(|c| c.is_alphanumeric() || c == '_')
558}
559
560#[cfg(test)]
561mod tests {
562    use super::*;
563
564    #[test]
565    fn test_wrapper_creation() {
566        let wrapper = RustAstWrapper::new();
567        assert!(!wrapper.debug);
568    }
569
570    #[test]
571    fn test_wrapper_with_debug() {
572        let wrapper = RustAstWrapper::new().with_debug(true);
573        assert!(wrapper.debug);
574    }
575
576    #[test]
577    fn test_wrap_simple_code() {
578        let wrapper = RustAstWrapper::new();
579
580        let user_code = "let x = 42;";
581        let wrapped = wrapper.wrap("test", user_code, None).expect("Wrapping failed");
582
583        assert!(wrapped.contains("#[no_mangle]"));
584        assert!(wrapped.contains("pub extern \"C\" fn oxur_eval_test()"));
585        assert!(wrapped.contains("let x = 42;"));
586    }
587
588    #[test]
589    fn test_wrap_multiline_code() {
590        let wrapper = RustAstWrapper::new();
591
592        let user_code = "let x = 42;\nlet y = 100;\nprintln!(\"sum = {}\", x + y);";
593        let wrapped = wrapper.wrap("multiline", user_code, None).expect("Wrapping failed");
594
595        assert!(wrapped.contains("let x = 42;"));
596        assert!(wrapped.contains("let y = 100;"));
597        assert!(wrapped.contains("println!"));
598    }
599
600    #[test]
601    fn test_wrap_with_debug() {
602        let wrapper = RustAstWrapper::new().with_debug(true);
603
604        let user_code = "let x = 1;";
605        let wrapped = wrapper.wrap("debug_test", user_code, None).expect("Wrapping failed");
606
607        assert!(wrapped.contains("DEBUG MODE ENABLED"));
608    }
609
610    #[test]
611    fn test_wrap_invalid_cache_key() {
612        let wrapper = RustAstWrapper::new();
613
614        let user_code = "let x = 1;";
615
616        // Invalid cache keys
617        assert!(wrapper.wrap("123invalid", user_code, None).is_err());
618        assert!(wrapper.wrap("invalid-key", user_code, None).is_err());
619        assert!(wrapper.wrap("", user_code, None).is_err());
620    }
621
622    #[test]
623    fn test_wrap_valid_cache_keys() {
624        let wrapper = RustAstWrapper::new();
625
626        let user_code = "let x = 1;";
627
628        // Valid cache keys
629        assert!(wrapper.wrap("valid", user_code, None).is_ok());
630        assert!(wrapper.wrap("valid_key", user_code, None).is_ok());
631        assert!(wrapper.wrap("_valid", user_code, None).is_ok());
632        assert!(wrapper.wrap("valid123", user_code, None).is_ok());
633    }
634
635    #[test]
636    fn test_is_valid_identifier() {
637        assert!(is_valid_identifier("valid"));
638        assert!(is_valid_identifier("_valid"));
639        assert!(is_valid_identifier("valid123"));
640        assert!(is_valid_identifier("Valid_Identifier_123"));
641
642        assert!(!is_valid_identifier(""));
643        assert!(!is_valid_identifier("123invalid"));
644        assert!(!is_valid_identifier("invalid-key"));
645        assert!(!is_valid_identifier("invalid.key"));
646        assert!(!is_valid_identifier("invalid key"));
647    }
648
649    #[test]
650    fn test_extract_variables() {
651        let wrapper = RustAstWrapper::new();
652
653        let user_code = "let x: i32 = 42;\nlet y: i32 = 100;";
654        let vars = wrapper.extract_variables(user_code);
655
656        // Should extract both variables with their types
657        assert!(vars.len() >= 2);
658        assert!(vars.iter().any(|(name, ty)| name == "x" && ty.contains("i32")));
659        assert!(vars.iter().any(|(name, ty)| name == "y" && ty.contains("i32")));
660    }
661
662    #[test]
663    fn test_extract_variables_type_inference() {
664        let wrapper = RustAstWrapper::new();
665
666        // Type inference should work without explicit type annotations
667        let user_code = "let x = 42;";
668        let vars = wrapper.extract_variables(user_code);
669
670        // Should infer i32 type
671        assert!(vars.len() >= 1);
672        // Type inference should detect integer literal
673        let has_x = vars.iter().any(|(name, _ty)| name == "x");
674        assert!(has_x, "Should extract variable 'x'");
675    }
676
677    #[test]
678    fn test_extract_variables_shadowing() {
679        let wrapper = RustAstWrapper::new();
680
681        // Test variable shadowing
682        let user_code = r#"
683            let x = 42;
684            let x = "hello";
685        "#;
686        let vars = wrapper.extract_variables(user_code);
687
688        // Should detect both bindings (shadowing)
689        // TypeInference tracks all bindings, even shadowed ones
690        assert!(vars.len() >= 2);
691        let x_count = vars.iter().filter(|(name, _)| name == "x").count();
692        assert!(x_count >= 1, "Should extract at least one 'x' binding");
693    }
694
695    #[test]
696    fn test_wrap_with_store_simple() {
697        let wrapper = RustAstWrapper::new();
698
699        let user_code = "let z = x + y;";
700        let variables =
701            vec![("x".to_string(), "i32".to_string()), ("y".to_string(), "i32".to_string())];
702
703        let wrapped = wrapper
704            .wrap_with_store("store_test", user_code, &variables, None)
705            .expect("Wrapping failed");
706
707        // Should have function declaration
708        assert!(wrapped.contains("#[no_mangle]"));
709        assert!(wrapped.contains("pub extern \"C\" fn oxur_eval_store_test()"));
710
711        // Should load x and y
712        assert!(wrapped.contains("with_store"));
713        assert!(wrapped.contains("\"x\"") || wrapped.contains("let x"));
714        assert!(wrapped.contains("\"y\"") || wrapped.contains("let y"));
715
716        // Should have user code
717        assert!(wrapped.contains("let z = x + y"));
718
719        // Should store variables back
720        assert!(wrapped.contains("store.set"));
721    }
722
723    #[test]
724    fn test_wrap_with_store_no_existing_vars() {
725        let wrapper = RustAstWrapper::new();
726
727        let user_code = "let x = 42;";
728        let variables = vec![]; // No existing variables
729
730        let wrapped = wrapper
731            .wrap_with_store("no_vars", user_code, &variables, None)
732            .expect("Wrapping failed");
733
734        // Should still generate function
735        assert!(wrapped.contains("#[no_mangle]"));
736        assert!(wrapped.contains("oxur_eval_no_vars"));
737
738        // Should have user code
739        assert!(wrapped.contains("let x = 42"));
740
741        // Should store newly created variable
742        assert!(wrapped.contains("store.set") || wrapped.contains("with_store"));
743    }
744
745    #[test]
746    fn test_wrap_with_store_multiple_vars() {
747        let wrapper = RustAstWrapper::new();
748
749        let user_code = "let sum = a + b + c;";
750        let variables = vec![
751            ("a".to_string(), "i32".to_string()),
752            ("b".to_string(), "i32".to_string()),
753            ("c".to_string(), "i32".to_string()),
754        ];
755
756        let wrapped = wrapper
757            .wrap_with_store("multi_vars", user_code, &variables, None)
758            .expect("Wrapping failed");
759
760        // Should load all three variables
761        assert!(wrapped.contains("\"a\"") || wrapped.contains("let a"));
762        assert!(wrapped.contains("\"b\"") || wrapped.contains("let b"));
763        assert!(wrapped.contains("\"c\"") || wrapped.contains("let c"));
764
765        // Should store sum back
766        assert!(wrapped.contains("store.set"));
767    }
768
769    #[test]
770    fn test_default() {
771        let wrapper1 = RustAstWrapper::default();
772        let wrapper2 = RustAstWrapper::new();
773
774        assert_eq!(wrapper1.debug, wrapper2.debug);
775    }
776
777    // Task 2.5: Integration tests
778
779    #[test]
780    fn test_generated_code_parses_as_valid_rust() {
781        let wrapper = RustAstWrapper::new();
782
783        let user_code = "let result = x * 2 + y;";
784        let variables =
785            vec![("x".to_string(), "i32".to_string()), ("y".to_string(), "i32".to_string())];
786
787        let wrapped =
788            wrapper.wrap_with_store("test", user_code, &variables, None).expect("Wrapping failed");
789
790        // The generated code should parse as valid Rust
791        let parsed = syn::parse_file(&wrapped);
792        assert!(parsed.is_ok(), "Generated code should parse as valid Rust: {}", wrapped);
793
794        if let Ok(file) = parsed {
795            // Should have at least one item (the function)
796            assert!(!file.items.is_empty());
797        }
798    }
799
800    #[test]
801    fn test_round_trip_wrap_and_parse() {
802        let wrapper = RustAstWrapper::new();
803
804        let user_code = r#"
805            let a = 10;
806            let b = 20;
807            let sum = a + b;
808        "#;
809
810        // Wrap without variables
811        let wrapped1 = wrapper.wrap("trip1", user_code, None).expect("Wrap failed");
812
813        // Parse the generated code
814        let parsed = syn::parse_file(&wrapped1);
815        assert!(parsed.is_ok());
816
817        // Wrap with variables
818        let vars = vec![("x".to_string(), "i32".to_string())];
819        let wrapped2 = wrapper
820            .wrap_with_store("trip2", user_code, &vars, None)
821            .expect("Wrap with store failed");
822
823        let parsed2 = syn::parse_file(&wrapped2);
824        assert!(parsed2.is_ok());
825    }
826
827    #[test]
828    fn test_end_to_end_variable_flow() {
829        let wrapper = RustAstWrapper::new();
830
831        // Scenario: REPL session with multiple evaluations
832
833        // Evaluation 1: Create variables
834        let code1 = "let x: i32 = 42; let y: i32 = 100;";
835        let wrapped1 = wrapper.wrap_with_store("eval1", code1, &[], None).expect("Eval 1 failed");
836
837        // Should create and store x and y
838        assert!(wrapped1.contains("store.set"));
839        assert!(syn::parse_file(&wrapped1).is_ok());
840
841        // Extract variables from first evaluation
842        let vars_after_eval1 = wrapper.extract_variables(code1);
843        assert!(vars_after_eval1.len() >= 2);
844
845        // Evaluation 2: Use existing variables
846        let code2 = "let sum = x + y;";
847        let wrapped2 = wrapper
848            .wrap_with_store("eval2", code2, &vars_after_eval1, None)
849            .expect("Eval 2 failed");
850
851        // Should load x and y, create sum
852        assert!(wrapped2.contains("with_store"));
853        assert!(wrapped2.contains("\"x\"") || wrapped2.contains("let x"));
854        assert!(wrapped2.contains("\"y\"") || wrapped2.contains("let y"));
855        assert!(syn::parse_file(&wrapped2).is_ok());
856
857        // Extract all variables after eval 2
858        let mut all_vars = vars_after_eval1.clone();
859        let new_vars = wrapper.extract_variables(code2);
860        for (name, ty) in new_vars {
861            if !all_vars.iter().any(|(n, _)| n == &name) {
862                all_vars.push((name, ty));
863            }
864        }
865
866        // Should now have x, y, and sum
867        assert!(all_vars.len() >= 3);
868
869        // Evaluation 3: Use all variables
870        let code3 = "let product = sum * x;";
871        let wrapped3 =
872            wrapper.wrap_with_store("eval3", code3, &all_vars, None).expect("Eval 3 failed");
873
874        assert!(wrapped3.contains("with_store"));
875        assert!(syn::parse_file(&wrapped3).is_ok());
876    }
877
878    #[test]
879    fn test_complex_type_handling() {
880        let wrapper = RustAstWrapper::new();
881
882        // Test with various Rust types
883        let code = r#"
884            let s: String = String::from("hello");
885            let v: Vec<i32> = vec![1, 2, 3];
886            let opt: Option<i32> = Some(42);
887        "#;
888
889        let vars = vec![
890            ("existing_str".to_string(), "String".to_string()),
891            ("existing_vec".to_string(), "Vec<i32>".to_string()),
892        ];
893
894        let wrapped =
895            wrapper.wrap_with_store("complex", code, &vars, None).expect("Complex types failed");
896
897        // Should handle complex types in variable loads/stores
898        assert!(wrapped.contains("with_store"));
899        assert!(syn::parse_file(&wrapped).is_ok());
900    }
901
902    // New tests for AST-based wrapping (Task 2.1)
903
904    #[test]
905    fn test_wrap_expression() {
906        let wrapper = RustAstWrapper::new();
907
908        // Test wrapping a single expression
909        let user_code = "42";
910        let wrapped = wrapper.wrap("expr_test", user_code, None).expect("Wrapping failed");
911
912        assert!(wrapped.contains("#[no_mangle]"));
913        assert!(wrapped.contains("pub extern \"C\" fn oxur_eval_expr_test()"));
914        assert!(wrapped.contains("42"));
915    }
916
917    #[test]
918    fn test_wrap_multiple_statements() {
919        let wrapper = RustAstWrapper::new();
920
921        let user_code = r#"
922            let x = 10;
923            let y = 20;
924            let z = x + y;
925        "#;
926        let wrapped = wrapper.wrap("multi", user_code, None).expect("Wrapping failed");
927
928        assert!(wrapped.contains("let x = 10;"));
929        assert!(wrapped.contains("let y = 20;"));
930        assert!(wrapped.contains("let z = x + y;"));
931    }
932
933    #[test]
934    fn test_wrap_function_definition_as_expression() {
935        let wrapper = RustAstWrapper::new();
936
937        // When parsing a function, we extract the body statements
938        let user_code = r#"
939            fn add(a: i32, b: i32) -> i32 {
940                a + b
941            }
942        "#;
943        let wrapped = wrapper.wrap("func_def", user_code, None).expect("Wrapping failed");
944
945        // The body expression gets wrapped
946        assert!(wrapped.contains("oxur_eval_func_def"));
947        assert!(wrapped.contains("a + b"));
948    }
949
950    #[test]
951    fn test_wrap_invalid_syntax() {
952        let wrapper = RustAstWrapper::new();
953
954        // Invalid Rust code should return an error
955        let user_code = "let x = ;";
956        let result = wrapper.wrap("invalid", user_code, None);
957
958        assert!(result.is_err());
959        match result {
960            Err(WrapperError::InvalidCode(msg)) => {
961                assert!(msg.contains("Could not parse"));
962            }
963            _ => panic!("Expected InvalidCode error"),
964        }
965    }
966
967    #[test]
968    fn test_parse_user_code_as_file() {
969        let wrapper = RustAstWrapper::new();
970
971        let code = "fn main() { println!(\"hello\"); }";
972        let stmts = wrapper.parse_user_code(code).expect("Parse failed");
973
974        // Should extract statements from the main function
975        assert!(!stmts.is_empty());
976    }
977
978    #[test]
979    fn test_parse_user_code_as_statements() {
980        let wrapper = RustAstWrapper::new();
981
982        let code = "let x = 42; let y = 100;";
983        let stmts = wrapper.parse_user_code(code).expect("Parse failed");
984
985        assert_eq!(stmts.len(), 2);
986    }
987
988    #[test]
989    fn test_parse_user_code_as_expression() {
990        let wrapper = RustAstWrapper::new();
991
992        let code = "42 + 58";
993        let stmts = wrapper.parse_user_code(code).expect("Parse failed");
994
995        assert_eq!(stmts.len(), 1);
996    }
997
998    // ===== Phase 4 Tests: Source Map Annotation =====
999
1000    #[test]
1001    fn test_wrap_with_store_and_source_map() {
1002        use oxur_smap::{new_node_id, SourceMap, SourcePos};
1003
1004        let wrapper = RustAstWrapper::new();
1005        let mut source_map = SourceMap::new();
1006
1007        // Simulate some surface nodes being added (would normally be done by parser)
1008        // For testing, we just need a source map with some stats
1009        let node1 = new_node_id();
1010        source_map.record_surface_node(node1, SourcePos::repl(1, 1, 10));
1011        let node2 = new_node_id();
1012        source_map.record_surface_node(node2, SourcePos::repl(2, 1, 15));
1013
1014        let user_code = "let result = x + y;";
1015        let variables =
1016            vec![("x".to_string(), "i32".to_string()), ("y".to_string(), "i32".to_string())];
1017
1018        // Wrap with source map
1019        let wrapped = wrapper
1020            .wrap_with_store("smap_test", user_code, &variables, Some(&source_map))
1021            .expect("Wrapping with source map failed");
1022
1023        // Should contain source map comments
1024        assert!(wrapped.contains("/* oxur_node="), "Should have source map comments: {}", wrapped);
1025
1026        // Should still have all the normal wrapping
1027        assert!(wrapped.contains("oxur_eval_smap_test"));
1028        assert!(wrapped.contains("with_store"));
1029
1030        // Generated code should still parse
1031        let parsed = syn::parse_file(&wrapped);
1032        assert!(parsed.is_ok(), "Code with source map comments should parse: {}", wrapped);
1033    }
1034
1035    #[test]
1036    fn test_wrap_with_store_without_source_map() {
1037        let wrapper = RustAstWrapper::new();
1038
1039        let user_code = "let z = 42;";
1040        let variables = vec![];
1041
1042        // Wrap without source map (None)
1043        let wrapped = wrapper
1044            .wrap_with_store("no_smap", user_code, &variables, None)
1045            .expect("Wrapping without source map failed");
1046
1047        // Should NOT contain source map comments
1048        assert!(
1049            !wrapped.contains("/* oxur_node="),
1050            "Should not have source map comments when source_map is None"
1051        );
1052
1053        // But should still be valid
1054        assert!(wrapped.contains("oxur_eval_no_smap"));
1055        let parsed = syn::parse_file(&wrapped);
1056        assert!(parsed.is_ok());
1057    }
1058
1059    #[test]
1060    fn test_source_map_comments_format() {
1061        use oxur_smap::{new_node_id, SourceMap, SourcePos};
1062
1063        let wrapper = RustAstWrapper::new();
1064        let mut source_map = SourceMap::new();
1065
1066        // Add multiple nodes
1067        for i in 0..5 {
1068            let node = new_node_id();
1069            source_map.record_surface_node(node, SourcePos::repl(i + 1, 1, 10));
1070        }
1071
1072        let user_code = r#"
1073            let a = 1;
1074            let b = 2;
1075            let c = a + b;
1076        "#;
1077
1078        let wrapped = wrapper
1079            .wrap_with_store("format_test", user_code, &[], Some(&source_map))
1080            .expect("Wrapping failed");
1081
1082        // Should have properly formatted comments
1083        // Check that comments match the /* oxur_node=N */ pattern
1084        assert!(wrapped.contains("/* oxur_node="));
1085
1086        // Comments should be followed by code
1087        if let Some(idx) = wrapped.find("/* oxur_node=") {
1088            let after_comment = &wrapped[idx..];
1089            // After comment, there should be valid Rust code
1090            assert!(
1091                after_comment.contains("let") || after_comment.contains("fn"),
1092                "Comments should be followed by code"
1093            );
1094        }
1095    }
1096
1097    #[test]
1098    fn test_insert_source_map_comments_preserves_semantics() {
1099        use oxur_smap::{new_node_id, SourceMap, SourcePos};
1100
1101        let wrapper = RustAstWrapper::new();
1102        let mut source_map = SourceMap::new();
1103
1104        let node = new_node_id();
1105        source_map.record_surface_node(node, SourcePos::repl(1, 1, 20));
1106
1107        let user_code = "let x: i32 = 42;";
1108
1109        // Generate wrapped code with source map
1110        let wrapped = wrapper
1111            .wrap_with_store("semantics_test", user_code, &[], Some(&source_map))
1112            .expect("Wrapping with source map failed");
1113
1114        // Should contain the original variable and value
1115        assert!(
1116            wrapped.contains("x") && wrapped.contains("42"),
1117            "Annotated code should preserve semantics"
1118        );
1119
1120        // Should contain source map comments
1121        assert!(wrapped.contains("/* oxur_node="), "Should have source map comments");
1122
1123        // Code should still parse as valid Rust
1124        let parsed = syn::parse_file(&wrapped);
1125        assert!(parsed.is_ok(), "Code with comments should be valid Rust: {}", wrapped);
1126    }
1127}