decy_core/
lib.rs

1//! Core transpilation pipeline for C-to-Rust conversion.
2//!
3//! This crate orchestrates the entire transpilation process:
4//! 1. Parse C code (via decy-parser)
5//! 2. Convert to HIR (via decy-hir)
6//! 3. Analyze and infer types (via decy-analyzer)
7//! 4. Infer ownership and lifetimes (via decy-ownership)
8//! 5. Verify safety properties (via decy-verify)
9//! 6. Generate Rust code (via decy-codegen)
10
11#![warn(missing_docs)]
12#![warn(clippy::all)]
13#![deny(unsafe_code)]
14
15pub mod metrics;
16
17pub use metrics::{CompileMetrics, TranspilationResult};
18
19use anyhow::{Context, Result};
20use decy_analyzer::patterns::PatternDetector;
21use decy_codegen::CodeGenerator;
22use decy_hir::{HirExpression, HirFunction, HirStatement};
23use decy_ownership::{
24    array_slice::ArrayParameterTransformer, borrow_gen::BorrowGenerator,
25    classifier_integration::classify_with_rules, dataflow::DataflowAnalyzer,
26    lifetime::LifetimeAnalyzer, lifetime_gen::LifetimeAnnotator,
27};
28use decy_parser::parser::CParser;
29use decy_stdlib::StdlibPrototypes;
30use petgraph::graph::{DiGraph, NodeIndex};
31use petgraph::visit::Topo;
32use std::collections::HashMap;
33use std::path::{Path, PathBuf};
34
35/// Result of transpiling a single C file.
36///
37/// Contains the transpiled Rust code along with metadata about
38/// dependencies and exported symbols for cross-file reference tracking.
39#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
40pub struct TranspiledFile {
41    /// Path to the original C source file
42    pub source_path: PathBuf,
43
44    /// Generated Rust code
45    pub rust_code: String,
46
47    /// List of C files this file depends on (#include dependencies)
48    pub dependencies: Vec<PathBuf>,
49
50    /// Functions exported by this file (for FFI and cross-file references)
51    pub functions_exported: Vec<String>,
52
53    /// FFI declarations (extern "C") for C↔Rust boundaries
54    pub ffi_declarations: String,
55}
56
57impl TranspiledFile {
58    /// Create a new TranspiledFile with the given data.
59    pub fn new(
60        source_path: PathBuf,
61        rust_code: String,
62        dependencies: Vec<PathBuf>,
63        functions_exported: Vec<String>,
64        ffi_declarations: String,
65    ) -> Self {
66        Self {
67            source_path,
68            rust_code,
69            dependencies,
70            functions_exported,
71            ffi_declarations,
72        }
73    }
74}
75
76/// Context for tracking cross-file information during transpilation.
77///
78/// Maintains knowledge of types, functions, and other declarations
79/// across multiple C files to enable proper reference resolution.
80#[derive(Debug, Clone, Default)]
81pub struct ProjectContext {
82    /// Types (structs, enums) defined across the project
83    types: HashMap<String, String>,
84
85    /// Functions defined across the project
86    functions: HashMap<String, String>,
87
88    /// Transpiled files tracked in this context
89    transpiled_files: HashMap<PathBuf, TranspiledFile>,
90}
91
92impl ProjectContext {
93    /// Create a new empty project context.
94    pub fn new() -> Self {
95        Self::default()
96    }
97
98    /// Add a transpiled file to the context.
99    ///
100    /// This makes the file's types and functions available for
101    /// cross-file reference resolution.
102    pub fn add_transpiled_file(&mut self, file: &TranspiledFile) {
103        // Track file
104        self.transpiled_files
105            .insert(file.source_path.clone(), file.clone());
106
107        // Extract types from rust_code (simplified: just track that types exist)
108        // In real implementation, would parse the Rust code
109        if file.rust_code.contains("struct") {
110            // Extract struct names (simplified pattern matching)
111            for line in file.rust_code.lines() {
112                if line.contains("struct") {
113                    if let Some(name) = self.extract_type_name(line) {
114                        self.types.insert(name.clone(), line.to_string());
115                    }
116                }
117            }
118        }
119
120        // Track exported functions
121        for func_name in &file.functions_exported {
122            self.functions.insert(
123                func_name.clone(),
124                file.source_path.to_string_lossy().to_string(),
125            );
126        }
127    }
128
129    /// Check if a type is defined in the project context.
130    pub fn has_type(&self, type_name: &str) -> bool {
131        self.types.contains_key(type_name)
132    }
133
134    /// Check if a function is defined in the project context.
135    pub fn has_function(&self, func_name: &str) -> bool {
136        self.functions.contains_key(func_name)
137    }
138
139    /// Get the source file that defines a given function.
140    pub fn get_function_source(&self, func_name: &str) -> Option<&str> {
141        self.functions.get(func_name).map(|s| s.as_str())
142    }
143
144    /// Helper: Extract type name from a line containing struct/enum definition
145    fn extract_type_name(&self, line: &str) -> Option<String> {
146        // Simplified: Extract "Point" from "pub struct Point {"
147        let words: Vec<&str> = line.split_whitespace().collect();
148        if let Some(idx) = words.iter().position(|&w| w == "struct" || w == "enum") {
149            if idx + 1 < words.len() {
150                let name = words[idx + 1].trim_end_matches('{').trim_end_matches('<');
151                return Some(name.to_string());
152            }
153        }
154        None
155    }
156}
157
158/// Dependency graph for tracking file dependencies and computing build order.
159///
160/// Uses a directed acyclic graph (DAG) to represent file dependencies,
161/// where an edge from A to B means "A depends on B" (A includes B).
162#[derive(Debug, Clone)]
163pub struct DependencyGraph {
164    /// Directed graph where nodes are file paths
165    graph: DiGraph<PathBuf, ()>,
166
167    /// Map from file path to node index for fast lookups
168    path_to_node: HashMap<PathBuf, NodeIndex>,
169}
170
171impl DependencyGraph {
172    /// Create a new empty dependency graph.
173    pub fn new() -> Self {
174        Self {
175            graph: DiGraph::new(),
176            path_to_node: HashMap::new(),
177        }
178    }
179
180    /// Check if the graph is empty (has no files).
181    pub fn is_empty(&self) -> bool {
182        self.graph.node_count() == 0
183    }
184
185    /// Get the number of files in the graph.
186    pub fn file_count(&self) -> usize {
187        self.graph.node_count()
188    }
189
190    /// Check if a file is in the graph.
191    pub fn contains_file(&self, path: &Path) -> bool {
192        self.path_to_node.contains_key(path)
193    }
194
195    /// Add a file to the graph.
196    ///
197    /// If the file already exists, this is a no-op.
198    pub fn add_file(&mut self, path: &Path) {
199        if !self.contains_file(path) {
200            let node = self.graph.add_node(path.to_path_buf());
201            self.path_to_node.insert(path.to_path_buf(), node);
202        }
203    }
204
205    /// Add a dependency relationship: `from` depends on `to`.
206    ///
207    /// Both files must already be added to the graph via `add_file`.
208    pub fn add_dependency(&mut self, from: &Path, to: &Path) {
209        let from_node = *self
210            .path_to_node
211            .get(from)
212            .expect("from file must be added to graph first");
213        let to_node = *self
214            .path_to_node
215            .get(to)
216            .expect("to file must be added to graph first");
217
218        self.graph.add_edge(from_node, to_node, ());
219    }
220
221    /// Check if there is a direct dependency from `from` to `to`.
222    pub fn has_dependency(&self, from: &Path, to: &Path) -> bool {
223        if let (Some(&from_node), Some(&to_node)) =
224            (self.path_to_node.get(from), self.path_to_node.get(to))
225        {
226            self.graph.contains_edge(from_node, to_node)
227        } else {
228            false
229        }
230    }
231
232    /// Compute topological sort to determine build order.
233    ///
234    /// Returns files in the order they should be transpiled (dependencies first).
235    /// Returns an error if there are circular dependencies.
236    pub fn topological_sort(&self) -> Result<Vec<PathBuf>> {
237        // Check for cycles first
238        if petgraph::algo::is_cyclic_directed(&self.graph) {
239            return Err(anyhow::anyhow!(
240                "Circular dependency detected in file dependencies"
241            ));
242        }
243
244        let mut topo = Topo::new(&self.graph);
245        let mut build_order = Vec::new();
246
247        while let Some(node) = topo.next(&self.graph) {
248            if let Some(path) = self.graph.node_weight(node) {
249                build_order.push(path.clone());
250            }
251        }
252
253        // Reverse because we want dependencies before dependents
254        build_order.reverse();
255
256        Ok(build_order)
257    }
258
259    /// Build a dependency graph from a list of C files.
260    ///
261    /// Parses #include directives to build the dependency graph.
262    pub fn from_files(files: &[PathBuf]) -> Result<Self> {
263        let mut graph = Self::new();
264
265        // Add all files first
266        for file in files {
267            graph.add_file(file);
268        }
269
270        // Parse dependencies
271        for file in files {
272            let content = std::fs::read_to_string(file)
273                .with_context(|| format!("Failed to read file: {}", file.display()))?;
274
275            let includes = Self::parse_include_directives(&content);
276
277            // Resolve #include paths relative to the file's directory
278            let file_dir = file.parent().unwrap_or_else(|| Path::new("."));
279
280            for include in includes {
281                let include_path = file_dir.join(&include);
282
283                // Only add dependency if the included file is in our file list
284                if graph.contains_file(&include_path) {
285                    graph.add_dependency(file, &include_path);
286                }
287            }
288        }
289
290        Ok(graph)
291    }
292
293    /// Parse #include directives from C source code.
294    ///
295    /// Returns a list of filenames (e.g., ["utils.h", "stdio.h"]).
296    pub fn parse_include_directives(code: &str) -> Vec<String> {
297        let mut includes = Vec::new();
298
299        for line in code.lines() {
300            let trimmed = line.trim();
301            if trimmed.starts_with("#include") {
302                // Extract filename from #include "file.h" or #include <file.h>
303                if let Some(start) = trimmed.find('"').or_else(|| trimmed.find('<')) {
304                    let end_char = if trimmed.chars().nth(start) == Some('"') {
305                        '"'
306                    } else {
307                        '>'
308                    };
309                    if let Some(end) = trimmed[start + 1..].find(end_char) {
310                        let filename = &trimmed[start + 1..start + 1 + end];
311                        includes.push(filename.to_string());
312                    }
313                }
314            }
315        }
316
317        includes
318    }
319
320    /// Check if a C header file has header guards (#ifndef/#define/#endif).
321    pub fn has_header_guard(path: &Path) -> Result<bool> {
322        let content = std::fs::read_to_string(path)
323            .with_context(|| format!("Failed to read file: {}", path.display()))?;
324
325        let has_ifndef = content.lines().any(|line| {
326            let trimmed = line.trim();
327            trimmed.starts_with("#ifndef") || trimmed.starts_with("#if !defined")
328        });
329
330        let has_define = content
331            .lines()
332            .any(|line| line.trim().starts_with("#define"));
333        let has_endif = content
334            .lines()
335            .any(|line| line.trim().starts_with("#endif"));
336
337        Ok(has_ifndef && has_define && has_endif)
338    }
339}
340
341impl Default for DependencyGraph {
342    fn default() -> Self {
343        Self::new()
344    }
345}
346
347/// Statistics for transpilation cache performance.
348#[derive(Debug, Clone)]
349pub struct CacheStatistics {
350    /// Number of cache hits
351    pub hits: usize,
352    /// Number of cache misses
353    pub misses: usize,
354    /// Total number of files in cache
355    pub total_files: usize,
356}
357
358/// Cache entry storing file hash and transpilation result.
359#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
360struct CacheEntry {
361    /// SHA-256 hash of the file content
362    hash: String,
363    /// Cached transpilation result
364    transpiled: TranspiledFile,
365    /// Hashes of dependencies (for invalidation)
366    dependency_hashes: HashMap<PathBuf, String>,
367}
368
369/// Transpilation cache for avoiding re-transpilation of unchanged files.
370///
371/// Uses SHA-256 hashing to detect file changes and supports disk persistence.
372/// Provides 10-20x speedup on cache hits.
373///
374/// # Examples
375///
376/// ```no_run
377/// use decy_core::{TranspilationCache, ProjectContext, transpile_file};
378/// use std::path::Path;
379///
380/// let mut cache = TranspilationCache::new();
381/// let path = Path::new("src/main.c");
382/// let context = ProjectContext::new();
383///
384/// // First transpilation - cache miss
385/// let result = transpile_file(path, &context)?;
386/// cache.insert(path, &result);
387///
388/// // Second access - cache hit (if file unchanged)
389/// if let Some(cached) = cache.get(path) {
390///     println!("Cache hit! Using cached result");
391/// }
392/// # Ok::<(), anyhow::Error>(())
393/// ```
394#[derive(Debug, Clone)]
395pub struct TranspilationCache {
396    /// Cache entries mapped by file path
397    entries: HashMap<PathBuf, CacheEntry>,
398    /// Cache directory for disk persistence
399    cache_dir: Option<PathBuf>,
400    /// Performance statistics
401    hits: usize,
402    misses: usize,
403}
404
405impl TranspilationCache {
406    /// Create a new empty transpilation cache.
407    pub fn new() -> Self {
408        Self {
409            entries: HashMap::new(),
410            cache_dir: None,
411            hits: 0,
412            misses: 0,
413        }
414    }
415
416    /// Create a cache with a specific directory for persistence.
417    pub fn with_directory(cache_dir: &Path) -> Self {
418        Self {
419            entries: HashMap::new(),
420            cache_dir: Some(cache_dir.to_path_buf()),
421            hits: 0,
422            misses: 0,
423        }
424    }
425
426    /// Compute SHA-256 hash of a file's content.
427    ///
428    /// Returns a 64-character hex string.
429    pub fn compute_hash(&self, path: &Path) -> Result<String> {
430        use sha2::{Digest, Sha256};
431
432        let content = std::fs::read(path)
433            .with_context(|| format!("Failed to read file for hashing: {}", path.display()))?;
434
435        let mut hasher = Sha256::new();
436        hasher.update(&content);
437        let result = hasher.finalize();
438
439        Ok(format!("{:x}", result))
440    }
441
442    /// Insert a transpiled file into the cache.
443    pub fn insert(&mut self, path: &Path, transpiled: &TranspiledFile) {
444        let hash = match self.compute_hash(path) {
445            Ok(h) => h,
446            Err(_) => return, // Skip caching if hash fails
447        };
448
449        // Compute dependency hashes
450        let mut dependency_hashes = HashMap::new();
451        for dep_path in &transpiled.dependencies {
452            if let Ok(dep_hash) = self.compute_hash(dep_path) {
453                dependency_hashes.insert(dep_path.clone(), dep_hash);
454            }
455        }
456
457        let entry = CacheEntry {
458            hash,
459            transpiled: transpiled.clone(),
460            dependency_hashes,
461        };
462
463        self.entries.insert(path.to_path_buf(), entry);
464    }
465
466    /// Get a cached transpilation result if the file hasn't changed.
467    ///
468    /// Returns `None` if:
469    /// - File is not in cache
470    /// - File content has changed
471    /// - Any dependency has changed
472    pub fn get(&mut self, path: &Path) -> Option<&TranspiledFile> {
473        let entry = self.entries.get(&path.to_path_buf())?;
474
475        // Check if file hash matches
476        let current_hash = self.compute_hash(path).ok()?;
477        if current_hash != entry.hash {
478            self.misses += 1;
479            return None;
480        }
481
482        // Check if any dependency has changed
483        for (dep_path, cached_hash) in &entry.dependency_hashes {
484            if let Ok(current_dep_hash) = self.compute_hash(dep_path) {
485                if &current_dep_hash != cached_hash {
486                    self.misses += 1;
487                    return None;
488                }
489            }
490        }
491
492        self.hits += 1;
493        Some(&entry.transpiled)
494    }
495
496    /// Save the cache to disk (if cache_dir is set).
497    pub fn save(&self) -> Result<()> {
498        let cache_dir = self
499            .cache_dir
500            .as_ref()
501            .ok_or_else(|| anyhow::anyhow!("Cache directory not set"))?;
502
503        std::fs::create_dir_all(cache_dir).with_context(|| {
504            format!("Failed to create cache directory: {}", cache_dir.display())
505        })?;
506
507        let cache_file = cache_dir.join("cache.json");
508        let json =
509            serde_json::to_string_pretty(&self.entries).context("Failed to serialize cache")?;
510
511        std::fs::write(&cache_file, json)
512            .with_context(|| format!("Failed to write cache file: {}", cache_file.display()))?;
513
514        Ok(())
515    }
516
517    /// Load a cache from disk.
518    pub fn load(cache_dir: &Path) -> Result<Self> {
519        let cache_file = cache_dir.join("cache.json");
520
521        if !cache_file.exists() {
522            // No cache file exists yet, return empty cache
523            return Ok(Self::with_directory(cache_dir));
524        }
525
526        let json = std::fs::read_to_string(&cache_file)
527            .with_context(|| format!("Failed to read cache file: {}", cache_file.display()))?;
528
529        let entries: HashMap<PathBuf, CacheEntry> =
530            serde_json::from_str(&json).context("Failed to deserialize cache")?;
531
532        Ok(Self {
533            entries,
534            cache_dir: Some(cache_dir.to_path_buf()),
535            hits: 0,
536            misses: 0,
537        })
538    }
539
540    /// Clear all cached entries.
541    pub fn clear(&mut self) {
542        self.entries.clear();
543        self.hits = 0;
544        self.misses = 0;
545    }
546
547    /// Get cache statistics.
548    pub fn statistics(&self) -> CacheStatistics {
549        CacheStatistics {
550            hits: self.hits,
551            misses: self.misses,
552            total_files: self.entries.len(),
553        }
554    }
555}
556
557impl Default for TranspilationCache {
558    fn default() -> Self {
559        Self::new()
560    }
561}
562
563/// Preprocess #include directives in C source code (DECY-056).
564///
565/// Resolves and inlines #include directives recursively, tracking processed files
566/// to prevent infinite loops from circular dependencies.
567///
568/// **NEW (Stdlib Support)**: Injects built-in C stdlib prototypes when system
569/// headers are encountered, enabling parsing of code that uses stdlib functions
570/// without requiring actual header files.
571///
572/// # Arguments
573///
574/// * `source` - C source code with #include directives
575/// * `base_dir` - Base directory for resolving relative include paths (None = current dir)
576/// * `processed` - Set of already processed file paths (prevents circular includes)
577/// * `stdlib_prototypes` - Stdlib prototype database for injection (None = create new)
578/// * `injected_headers` - Set of already injected system headers (prevents duplicates)
579///
580/// # Returns
581///
582/// Preprocessed C code with includes inlined and stdlib prototypes injected
583fn preprocess_includes(
584    source: &str,
585    base_dir: Option<&Path>,
586    processed: &mut std::collections::HashSet<PathBuf>,
587    stdlib_prototypes: &StdlibPrototypes,
588    injected_headers: &mut std::collections::HashSet<String>,
589) -> Result<String> {
590    let mut result = String::new();
591    let base_dir = base_dir.unwrap_or_else(|| Path::new("."));
592
593    for line in source.lines() {
594        let trimmed = line.trim();
595
596        // Check for #include directive
597        if trimmed.starts_with("#include") {
598            // Extract filename from #include "file.h" or #include <file.h>
599            let (filename, is_system) = if let Some(start) = trimmed.find('"') {
600                if let Some(end) = trimmed[start + 1..].find('"') {
601                    let filename = &trimmed[start + 1..start + 1 + end];
602                    (filename, false)
603                } else {
604                    // Malformed include, keep original line
605                    result.push_str(line);
606                    result.push('\n');
607                    continue;
608                }
609            } else if let Some(start) = trimmed.find('<') {
610                if let Some(end) = trimmed[start + 1..].find('>') {
611                    let filename = &trimmed[start + 1..start + 1 + end];
612                    (filename, true)
613                } else {
614                    // Malformed include, keep original line
615                    result.push_str(line);
616                    result.push('\n');
617                    continue;
618                }
619            } else {
620                // No include found, keep original line
621                result.push_str(line);
622                result.push('\n');
623                continue;
624            };
625
626            // Skip system includes (<stdio.h>) - we don't have those files
627            // BUT: Inject stdlib prototypes so parsing succeeds
628            if is_system {
629                // Comment out the original include
630                result.push_str(&format!("// {}\n", line));
631
632                // Inject stdlib prototypes for this header (only once per header)
633                if !injected_headers.contains(filename) {
634                    // Mark as injected
635                    injected_headers.insert(filename.to_string());
636
637                    // Try to parse the header name and inject specific prototypes
638                    if let Some(header) = decy_stdlib::StdHeader::from_filename(filename) {
639                        result
640                            .push_str(&format!("// BEGIN: Built-in prototypes for {}\n", filename));
641                        result.push_str(&stdlib_prototypes.inject_prototypes_for_header(header));
642                        result.push_str(&format!("// END: Built-in prototypes for {}\n", filename));
643                    } else {
644                        // Unknown header - just comment it out
645                        result.push_str(&format!("// Unknown system header: {}\n", filename));
646                    }
647                }
648
649                continue;
650            }
651
652            // Resolve include path relative to base_dir
653            let include_path = base_dir.join(filename);
654
655            // Check if already processed (circular dependency or duplicate)
656            if processed.contains(&include_path) {
657                // Already processed, skip (header guards working)
658                result.push_str(&format!("// Already included: {}\n", filename));
659                continue;
660            }
661
662            // Try to read the included file
663            if let Ok(included_content) = std::fs::read_to_string(&include_path) {
664                // Mark as processed
665                processed.insert(include_path.clone());
666
667                // Get directory of included file for nested includes
668                let included_dir = include_path.parent().unwrap_or(base_dir);
669
670                // Recursively preprocess the included file
671                let preprocessed = preprocess_includes(
672                    &included_content,
673                    Some(included_dir),
674                    processed,
675                    stdlib_prototypes,
676                    injected_headers,
677                )?;
678
679                // Add marker comments for debugging
680                result.push_str(&format!("// BEGIN INCLUDE: {}\n", filename));
681                result.push_str(&preprocessed);
682                result.push_str(&format!("// END INCLUDE: {}\n", filename));
683            } else {
684                // File not found - return error for local includes
685                anyhow::bail!("Failed to find include file: {}", include_path.display());
686            }
687        } else {
688            // Regular line, keep as-is
689            result.push_str(line);
690            result.push('\n');
691        }
692    }
693
694    Ok(result)
695}
696
697/// Main transpilation pipeline entry point.
698///
699/// Converts C source code to safe Rust code with automatic ownership
700/// and lifetime inference.
701///
702/// Automatically preprocesses #include directives (DECY-056).
703///
704/// # Examples
705///
706/// ```no_run
707/// use decy_core::transpile;
708///
709/// let c_code = "int add(int a, int b) { return a + b; }";
710/// let rust_code = transpile(c_code)?;
711/// assert!(rust_code.contains("fn add"));
712/// # Ok::<(), anyhow::Error>(())
713/// ```
714///
715/// # Errors
716///
717/// Returns an error if:
718/// - #include file not found
719/// - C code parsing fails
720/// - HIR conversion fails
721/// - Code generation fails
722pub fn transpile(c_code: &str) -> Result<String> {
723    transpile_with_includes(c_code, None)
724}
725
726/// Transpile C code and return verification result.
727///
728/// This function transpiles C code to Rust and includes metadata about
729/// the transpilation for metrics tracking.
730///
731/// # Arguments
732///
733/// * `c_code` - C source code to transpile
734///
735/// # Returns
736///
737/// Returns a `TranspilationResult` containing the generated Rust code
738/// and verification status.
739///
740/// # Examples
741///
742/// ```
743/// use decy_core::transpile_with_verification;
744///
745/// let c_code = "int add(int a, int b) { return a + b; }";
746/// let result = transpile_with_verification(c_code);
747/// assert!(result.is_ok());
748/// ```
749pub fn transpile_with_verification(c_code: &str) -> Result<TranspilationResult> {
750    match transpile(c_code) {
751        Ok(rust_code) => Ok(TranspilationResult::success(rust_code)),
752        Err(e) => {
753            // Return empty code with error
754            Ok(TranspilationResult::failure(
755                String::new(),
756                vec![e.to_string()],
757            ))
758        }
759    }
760}
761
762/// Transpile C code with include directive support and custom base directory.
763///
764/// # Arguments
765///
766/// * `c_code` - C source code to transpile
767/// * `base_dir` - Base directory for resolving #include paths (None = current dir)
768///
769/// # Examples
770///
771/// ```no_run
772/// use decy_core::transpile_with_includes;
773/// use std::path::Path;
774///
775/// let c_code = "#include \"utils.h\"\nint main() { return 0; }";
776/// let rust_code = transpile_with_includes(c_code, Some(Path::new("/tmp/project")))?;
777/// # Ok::<(), anyhow::Error>(())
778/// ```
779pub fn transpile_with_includes(c_code: &str, base_dir: Option<&Path>) -> Result<String> {
780    // Step 0: Preprocess #include directives (DECY-056) + Inject stdlib prototypes
781    let stdlib_prototypes = StdlibPrototypes::new();
782    let mut processed_files = std::collections::HashSet::new();
783    let mut injected_headers = std::collections::HashSet::new();
784    let preprocessed = preprocess_includes(
785        c_code,
786        base_dir,
787        &mut processed_files,
788        &stdlib_prototypes,
789        &mut injected_headers,
790    )?;
791
792    // Step 1: Parse C code
793    // Note: We don't add standard type definitions (size_t, etc.) here because:
794    // 1. If code has #include directives, system headers define them
795    // 2. If code doesn't have includes and uses size_t, it should typedef it explicitly
796    // 3. Adding conflicting typedefs breaks parsing
797    let parser = CParser::new().context("Failed to create C parser")?;
798    let ast = parser
799        .parse(&preprocessed)
800        .context("Failed to parse C code")?;
801
802    // Step 2: Convert to HIR
803    let all_hir_functions: Vec<HirFunction> = ast
804        .functions()
805        .iter()
806        .map(HirFunction::from_ast_function)
807        .collect();
808
809    // DECY-190: Deduplicate functions - when a C file has both a declaration
810    // (prototype) and a definition, only keep the definition.
811    // This prevents "the name X is defined multiple times" errors in Rust.
812    let hir_functions: Vec<HirFunction> = {
813        use std::collections::HashMap;
814        let mut func_map: HashMap<String, HirFunction> = HashMap::new();
815
816        for func in all_hir_functions {
817            let name = func.name().to_string();
818            if let Some(existing) = func_map.get(&name) {
819                // Keep the one with a body (definition) over the one without (declaration)
820                if func.has_body() && !existing.has_body() {
821                    func_map.insert(name, func);
822                }
823                // Otherwise keep existing (either both have bodies, both don't, or existing has body)
824            } else {
825                func_map.insert(name, func);
826            }
827        }
828
829        // Collect in insertion order isn't guaranteed, but order shouldn't matter for codegen
830        func_map.into_values().collect()
831    };
832
833    // Convert structs to HIR
834    let hir_structs: Vec<decy_hir::HirStruct> = ast
835        .structs()
836        .iter()
837        .map(|s| {
838            let fields = s
839                .fields
840                .iter()
841                .map(|f| {
842                    decy_hir::HirStructField::new(
843                        f.name.clone(),
844                        decy_hir::HirType::from_ast_type(&f.field_type),
845                    )
846                })
847                .collect();
848            decy_hir::HirStruct::new(s.name.clone(), fields)
849        })
850        .collect();
851
852    // Convert global variables to HIR (DECY-054)
853    let hir_variables: Vec<decy_hir::HirStatement> = ast
854        .variables()
855        .iter()
856        .map(|v| decy_hir::HirStatement::VariableDeclaration {
857            name: v.name().to_string(),
858            var_type: decy_hir::HirType::from_ast_type(v.var_type()),
859            initializer: v
860                .initializer()
861                .map(decy_hir::HirExpression::from_ast_expression),
862        })
863        .collect();
864
865    // Convert typedefs to HIR (DECY-054, DECY-057)
866    let hir_typedefs: Vec<decy_hir::HirTypedef> = ast
867        .typedefs()
868        .iter()
869        .map(|t| {
870            decy_hir::HirTypedef::new(
871                t.name().to_string(),
872                decy_hir::HirType::from_ast_type(&t.underlying_type),
873            )
874        })
875        .collect();
876
877    // DECY-116: Build slice function arg mappings BEFORE transformation (while we still have original params)
878    let slice_func_args: Vec<(String, Vec<(usize, usize)>)> = hir_functions
879        .iter()
880        .filter_map(|func| {
881            let mut mappings = Vec::new();
882            let params = func.parameters();
883
884            for (i, param) in params.iter().enumerate() {
885                // Check if this is a pointer param (potential array)
886                if matches!(param.param_type(), decy_hir::HirType::Pointer(_)) {
887                    // Check if next param is an int with length-like name
888                    if i + 1 < params.len() {
889                        let next_param = &params[i + 1];
890                        if matches!(next_param.param_type(), decy_hir::HirType::Int) {
891                            let param_name = next_param.name().to_lowercase();
892                            if param_name.contains("len")
893                                || param_name.contains("size")
894                                || param_name.contains("count")
895                                || param_name == "n"
896                                || param_name == "num"
897                            {
898                                mappings.push((i, i + 1));
899                            }
900                        }
901                    }
902                }
903            }
904
905            if mappings.is_empty() {
906                None
907            } else {
908                Some((func.name().to_string(), mappings))
909            }
910        })
911        .collect();
912
913    // Step 3: Analyze ownership and lifetimes
914    let mut transformed_functions = Vec::new();
915
916    for func in hir_functions {
917        // Build dataflow graph for the function
918        let dataflow_analyzer = DataflowAnalyzer::new();
919        let dataflow_graph = dataflow_analyzer.analyze(&func);
920
921        // DECY-183: Infer ownership patterns using RuleBasedClassifier
922        // This replaces OwnershipInferencer with the new classifier system
923        let ownership_inferences = classify_with_rules(&dataflow_graph, &func);
924
925        // Generate borrow code (&T, &mut T)
926        let borrow_generator = BorrowGenerator::new();
927        let func_with_borrows = borrow_generator.transform_function(&func, &ownership_inferences);
928
929        // DECY-072 GREEN: Transform array parameters to slices
930        let array_transformer = ArrayParameterTransformer::new();
931        let func_with_slices = array_transformer.transform(&func_with_borrows, &dataflow_graph);
932
933        // Analyze lifetimes
934        let lifetime_analyzer = LifetimeAnalyzer::new();
935        let scope_tree = lifetime_analyzer.build_scope_tree(&func_with_slices);
936        let _lifetimes = lifetime_analyzer.track_lifetimes(&func_with_slices, &scope_tree);
937
938        // Generate lifetime annotations
939        let lifetime_annotator = LifetimeAnnotator::new();
940        let annotated_signature = lifetime_annotator.annotate_function(&func_with_slices);
941
942        // Store both function and its annotated signature
943        transformed_functions.push((func_with_slices, annotated_signature));
944    }
945
946    // Step 4: Generate Rust code with lifetime annotations
947    let code_generator = CodeGenerator::new();
948    let mut rust_code = String::new();
949
950    // DECY-119: Track emitted definitions to avoid duplicates
951    let mut emitted_structs = std::collections::HashSet::new();
952    let mut emitted_typedefs = std::collections::HashSet::new();
953
954    // Generate struct definitions first (deduplicated)
955    for hir_struct in &hir_structs {
956        let struct_name = hir_struct.name();
957        if emitted_structs.contains(struct_name) {
958            continue; // Skip duplicate
959        }
960        emitted_structs.insert(struct_name.to_string());
961
962        let struct_code = code_generator.generate_struct(hir_struct);
963        rust_code.push_str(&struct_code);
964        rust_code.push('\n');
965    }
966
967    // Generate typedefs (DECY-054, DECY-057) - deduplicated
968    for typedef in &hir_typedefs {
969        let typedef_name = typedef.name();
970        if emitted_typedefs.contains(typedef_name) {
971            continue; // Skip duplicate
972        }
973        emitted_typedefs.insert(typedef_name.to_string());
974
975        if let Ok(typedef_code) = code_generator.generate_typedef(typedef) {
976            rust_code.push_str(&typedef_code);
977            rust_code.push('\n');
978        }
979    }
980
981    // Generate global variables (DECY-054)
982    for var_stmt in &hir_variables {
983        if let decy_hir::HirStatement::VariableDeclaration {
984            name,
985            var_type,
986            initializer,
987        } = var_stmt
988        {
989            // Generate as static mut for C global variable equivalence
990            let type_str = CodeGenerator::map_type(var_type);
991
992            if let Some(init_expr) = initializer {
993                // DECY-201: Special handling for array initialization
994                let init_code = if let decy_hir::HirType::Array {
995                    element_type,
996                    size: Some(size_val),
997                } = var_type
998                {
999                    // Check if init_expr is just the size value (uninitialized array)
1000                    if let decy_hir::HirExpression::IntLiteral(n) = init_expr {
1001                        if *n as usize == *size_val {
1002                            // Use type-appropriate zero value
1003                            let element_init = match element_type.as_ref() {
1004                                decy_hir::HirType::Char => "0u8".to_string(),
1005                                decy_hir::HirType::Int => "0i32".to_string(),
1006                                decy_hir::HirType::Float => "0.0f32".to_string(),
1007                                decy_hir::HirType::Double => "0.0f64".to_string(),
1008                                _ => "0".to_string(),
1009                            };
1010                            format!("[{}; {}]", element_init, size_val)
1011                        } else {
1012                            code_generator.generate_expression(init_expr)
1013                        }
1014                    } else {
1015                        code_generator.generate_expression(init_expr)
1016                    }
1017                } else {
1018                    code_generator.generate_expression(init_expr)
1019                };
1020                rust_code.push_str(&format!(
1021                    "static mut {}: {} = {};\n",
1022                    name, type_str, init_code
1023                ));
1024            } else {
1025                // DECY-215: Use appropriate default values for uninitialized globals
1026                // Only use Option for function pointers and complex types
1027                let default_value = match var_type {
1028                    decy_hir::HirType::Int => "0".to_string(),
1029                    decy_hir::HirType::UnsignedInt => "0".to_string(),
1030                    decy_hir::HirType::Char => "0".to_string(),
1031                    decy_hir::HirType::Float => "0.0".to_string(),
1032                    decy_hir::HirType::Double => "0.0".to_string(),
1033                    decy_hir::HirType::Pointer(_) => "std::ptr::null_mut()".to_string(),
1034                    // DECY-217: Arrays need explicit zero initialization (Default only works for size <= 32)
1035                    decy_hir::HirType::Array { element_type, size } => {
1036                        let elem_default = match element_type.as_ref() {
1037                            decy_hir::HirType::Char => "0u8",
1038                            decy_hir::HirType::Int => "0i32",
1039                            decy_hir::HirType::UnsignedInt => "0u32",
1040                            decy_hir::HirType::Float => "0.0f32",
1041                            decy_hir::HirType::Double => "0.0f64",
1042                            _ => "0",
1043                        };
1044                        if let Some(n) = size {
1045                            format!("[{}; {}]", elem_default, n)
1046                        } else {
1047                            format!("[{}; 0]", elem_default)
1048                        }
1049                    }
1050                    decy_hir::HirType::FunctionPointer { .. } => {
1051                        // Function pointers need Option wrapping
1052                        rust_code.push_str(&format!(
1053                            "static mut {}: Option<{}> = None;\n",
1054                            name, type_str
1055                        ));
1056                        continue;
1057                    }
1058                    _ => "Default::default()".to_string(),
1059                };
1060                rust_code.push_str(&format!(
1061                    "static mut {}: {} = {};\n",
1062                    name, type_str, default_value
1063                ));
1064            }
1065        }
1066    }
1067    if !hir_variables.is_empty() {
1068        rust_code.push('\n');
1069    }
1070
1071    // DECY-117: Build function signatures for call site reference mutability
1072    // DECY-125: Keep pointers as pointers when pointer arithmetic is used
1073    // DECY-159: Keep pointers as pointers when NULL comparison is used
1074    let all_function_sigs: Vec<(String, Vec<decy_hir::HirType>)> = transformed_functions
1075        .iter()
1076        .map(|(func, _sig)| {
1077            let param_types: Vec<decy_hir::HirType> = func
1078                .parameters()
1079                .iter()
1080                .map(|p| {
1081                    // Transform pointer params to mutable references (matching DECY-111)
1082                    // DECY-125: But keep as pointer if pointer arithmetic is used
1083                    // DECY-159: Also keep as pointer if compared to NULL (NULL is valid input)
1084                    if let decy_hir::HirType::Pointer(inner) = p.param_type() {
1085                        // Check if this param uses pointer arithmetic or is compared to NULL
1086                        if uses_pointer_arithmetic(func, p.name())
1087                            || pointer_compared_to_null(func, p.name())
1088                        {
1089                            // Keep as raw pointer
1090                            p.param_type().clone()
1091                        } else {
1092                            decy_hir::HirType::Reference {
1093                                inner: inner.clone(),
1094                                mutable: true,
1095                            }
1096                        }
1097                    } else {
1098                        p.param_type().clone()
1099                    }
1100                })
1101                .collect();
1102            (func.name().to_string(), param_types)
1103        })
1104        .collect();
1105
1106    // DECY-134b: Build string iteration function info for call site transformation
1107    let string_iter_funcs: Vec<(String, Vec<(usize, bool)>)> = transformed_functions
1108        .iter()
1109        .filter_map(|(func, _)| {
1110            let params = code_generator.get_string_iteration_params(func);
1111            if params.is_empty() {
1112                None
1113            } else {
1114                Some((func.name().to_string(), params))
1115            }
1116        })
1117        .collect();
1118
1119    // Generate functions with struct definitions for field type awareness
1120    // Note: slice_func_args was built at line 814 BEFORE transformation to capture original params
1121    for (func, annotated_sig) in &transformed_functions {
1122        let generated = code_generator.generate_function_with_lifetimes_and_structs(
1123            func,
1124            annotated_sig,
1125            &hir_structs,
1126            &all_function_sigs,
1127            &slice_func_args,
1128            &string_iter_funcs,
1129        );
1130        rust_code.push_str(&generated);
1131        rust_code.push('\n');
1132    }
1133
1134    Ok(rust_code)
1135}
1136
1137/// Transpile with Box transformation enabled.
1138///
1139/// This variant applies Box pattern detection to transform malloc/free
1140/// patterns into safe Box allocations.
1141///
1142/// # Examples
1143///
1144/// ```no_run
1145/// use decy_core::transpile_with_box_transform;
1146///
1147/// let c_code = r#"
1148///     int* create_value() {
1149///         int* p = malloc(sizeof(int));
1150///         *p = 42;
1151///         return p;
1152///     }
1153/// "#;
1154/// let rust_code = transpile_with_box_transform(c_code)?;
1155/// assert!(rust_code.contains("Box"));
1156/// # Ok::<(), anyhow::Error>(())
1157/// ```
1158pub fn transpile_with_box_transform(c_code: &str) -> Result<String> {
1159    // Step 1: Parse C code
1160    let parser = CParser::new().context("Failed to create C parser")?;
1161    let ast = parser.parse(c_code).context("Failed to parse C code")?;
1162
1163    // Step 2: Convert to HIR
1164    let hir_functions: Vec<HirFunction> = ast
1165        .functions()
1166        .iter()
1167        .map(HirFunction::from_ast_function)
1168        .collect();
1169
1170    // Step 3: Generate Rust code with Box transformation
1171    let code_generator = CodeGenerator::new();
1172    let pattern_detector = PatternDetector::new();
1173    let mut rust_code = String::new();
1174
1175    for func in &hir_functions {
1176        // Detect Box candidates in this function
1177        let candidates = pattern_detector.find_box_candidates(func);
1178
1179        let generated = code_generator.generate_function_with_box_transform(func, &candidates);
1180        rust_code.push_str(&generated);
1181        rust_code.push('\n');
1182    }
1183
1184    Ok(rust_code)
1185}
1186
1187/// Transpile a single C file with project context.
1188///
1189/// This enables file-by-file transpilation for incremental C→Rust migration.
1190/// The `ProjectContext` tracks types and functions across files for proper
1191/// reference resolution.
1192///
1193/// # Examples
1194///
1195/// ```no_run
1196/// use decy_core::{transpile_file, ProjectContext};
1197/// use std::path::Path;
1198///
1199/// let path = Path::new("src/utils.c");
1200/// let context = ProjectContext::new();
1201/// let result = transpile_file(path, &context)?;
1202///
1203/// assert!(!result.rust_code.is_empty());
1204/// # Ok::<(), anyhow::Error>(())
1205/// ```
1206///
1207/// # Errors
1208///
1209/// Returns an error if:
1210/// - File does not exist or cannot be read
1211/// - C code parsing fails
1212/// - Code generation fails
1213pub fn transpile_file(path: &Path, _context: &ProjectContext) -> Result<TranspiledFile> {
1214    // Read the C source file
1215    let c_code = std::fs::read_to_string(path)
1216        .with_context(|| format!("Failed to read file: {}", path.display()))?;
1217
1218    // Parse dependencies from #include directives (simplified: just detect presence)
1219    let dependencies = extract_dependencies(path, &c_code)?;
1220
1221    // Transpile the C code using the main pipeline
1222    let rust_code = transpile(&c_code)?;
1223
1224    // Extract function names from the generated Rust code
1225    let functions_exported = extract_function_names(&rust_code);
1226
1227    // Generate FFI declarations for exported functions
1228    let ffi_declarations = generate_ffi_declarations(&functions_exported);
1229
1230    Ok(TranspiledFile::new(
1231        path.to_path_buf(),
1232        rust_code,
1233        dependencies,
1234        functions_exported,
1235        ffi_declarations,
1236    ))
1237}
1238
1239/// Extract dependencies from #include directives in C code.
1240///
1241/// This is a simplified implementation that detects #include directives
1242/// and resolves them relative to the source file's directory.
1243fn extract_dependencies(source_path: &Path, c_code: &str) -> Result<Vec<PathBuf>> {
1244    let mut dependencies = Vec::new();
1245    let source_dir = source_path
1246        .parent()
1247        .ok_or_else(|| anyhow::anyhow!("Source file has no parent directory"))?;
1248
1249    for line in c_code.lines() {
1250        let trimmed = line.trim();
1251        if trimmed.starts_with("#include") {
1252            // Extract header filename from #include "header.h" or #include <header.h>
1253            if let Some(start) = trimmed.find('"') {
1254                if let Some(end) = trimmed[start + 1..].find('"') {
1255                    let header_name = &trimmed[start + 1..start + 1 + end];
1256                    let header_path = source_dir.join(header_name);
1257                    if header_path.exists() {
1258                        dependencies.push(header_path);
1259                    }
1260                }
1261            }
1262        }
1263    }
1264
1265    Ok(dependencies)
1266}
1267
1268/// Extract function names from generated Rust code.
1269///
1270/// Parses function definitions to identify exported functions.
1271fn extract_function_names(rust_code: &str) -> Vec<String> {
1272    let mut functions = Vec::new();
1273
1274    for line in rust_code.lines() {
1275        let trimmed = line.trim();
1276        // Look for "fn function_name(" or "pub fn function_name("
1277        if (trimmed.starts_with("fn ") || trimmed.starts_with("pub fn ")) && trimmed.contains('(') {
1278            let start_idx = if trimmed.starts_with("pub fn ") {
1279                7 // length of "pub fn "
1280            } else {
1281                3 // length of "fn "
1282            };
1283
1284            if let Some(paren_idx) = trimmed[start_idx..].find('(') {
1285                let func_name = &trimmed[start_idx..start_idx + paren_idx];
1286                // Remove generic parameters if present (e.g., "foo<'a>" → "foo")
1287                let func_name_clean = if let Some(angle_idx) = func_name.find('<') {
1288                    &func_name[..angle_idx]
1289                } else {
1290                    func_name
1291                };
1292                functions.push(func_name_clean.trim().to_string());
1293            }
1294        }
1295    }
1296
1297    functions
1298}
1299
1300/// Generate FFI declarations for exported functions.
1301///
1302/// Creates extern "C" declarations to enable C↔Rust interoperability.
1303fn generate_ffi_declarations(functions: &[String]) -> String {
1304    if functions.is_empty() {
1305        return String::new();
1306    }
1307
1308    let mut ffi = String::from("// FFI declarations for C interoperability\n");
1309    ffi.push_str("#[no_mangle]\n");
1310    ffi.push_str("extern \"C\" {\n");
1311
1312    for func_name in functions {
1313        ffi.push_str(&format!("    // {}\n", func_name));
1314    }
1315
1316    ffi.push_str("}\n");
1317    ffi
1318}
1319
1320/// Check if a function parameter uses pointer arithmetic (DECY-125).
1321///
1322/// Returns true if the parameter is used in `p = p + n` or `p = p - n` patterns.
1323fn uses_pointer_arithmetic(func: &HirFunction, param_name: &str) -> bool {
1324    for stmt in func.body() {
1325        if statement_uses_pointer_arithmetic(stmt, param_name) {
1326            return true;
1327        }
1328    }
1329    false
1330}
1331
1332/// DECY-159: Check if a pointer parameter is compared to NULL.
1333///
1334/// If a pointer is compared to NULL, it means NULL is a valid input value.
1335/// Such parameters must remain as raw pointers, not references,
1336/// to avoid dereferencing NULL at call sites.
1337fn pointer_compared_to_null(func: &HirFunction, param_name: &str) -> bool {
1338    for stmt in func.body() {
1339        if statement_compares_to_null(stmt, param_name) {
1340            return true;
1341        }
1342    }
1343    false
1344}
1345
1346/// DECY-159: Recursively check if a statement compares a variable to NULL.
1347fn statement_compares_to_null(stmt: &HirStatement, var_name: &str) -> bool {
1348    match stmt {
1349        HirStatement::If {
1350            condition,
1351            then_block,
1352            else_block,
1353        } => {
1354            // Check if condition is var == NULL or var != NULL
1355            if expression_compares_to_null(condition, var_name) {
1356                return true;
1357            }
1358            // Recurse into blocks
1359            then_block
1360                .iter()
1361                .any(|s| statement_compares_to_null(s, var_name))
1362                || else_block
1363                    .as_ref()
1364                    .is_some_and(|blk| blk.iter().any(|s| statement_compares_to_null(s, var_name)))
1365        }
1366        HirStatement::While { condition, body } => {
1367            expression_compares_to_null(condition, var_name)
1368                || body.iter().any(|s| statement_compares_to_null(s, var_name))
1369        }
1370        HirStatement::For {
1371            condition, body, ..
1372        } => {
1373            expression_compares_to_null(condition, var_name)
1374                || body.iter().any(|s| statement_compares_to_null(s, var_name))
1375        }
1376        HirStatement::Switch {
1377            condition, cases, ..
1378        } => {
1379            expression_compares_to_null(condition, var_name)
1380                || cases.iter().any(|c| {
1381                    c.body
1382                        .iter()
1383                        .any(|s| statement_compares_to_null(s, var_name))
1384                })
1385        }
1386        _ => false,
1387    }
1388}
1389
1390/// DECY-159: Check if an expression compares a variable to NULL.
1391///
1392/// NULL in C can be:
1393/// - `NullLiteral` (explicit NULL)
1394/// - `IntLiteral(0)` (NULL macro expanded to 0)
1395/// - Cast of 0 to void* (less common)
1396fn expression_compares_to_null(expr: &HirExpression, var_name: &str) -> bool {
1397    use decy_hir::BinaryOperator;
1398    match expr {
1399        HirExpression::BinaryOp { op, left, right } => {
1400            // Check for var == NULL or var != NULL
1401            if matches!(op, BinaryOperator::Equal | BinaryOperator::NotEqual) {
1402                let left_is_var =
1403                    matches!(&**left, HirExpression::Variable(name) if name == var_name);
1404                let right_is_var =
1405                    matches!(&**right, HirExpression::Variable(name) if name == var_name);
1406                // NULL can be NullLiteral or IntLiteral(0) (when NULL macro expands to 0)
1407                let left_is_null = matches!(
1408                    &**left,
1409                    HirExpression::NullLiteral | HirExpression::IntLiteral(0)
1410                );
1411                let right_is_null = matches!(
1412                    &**right,
1413                    HirExpression::NullLiteral | HirExpression::IntLiteral(0)
1414                );
1415
1416                if (left_is_var && right_is_null) || (right_is_var && left_is_null) {
1417                    return true;
1418                }
1419            }
1420            // Recurse into binary op children for nested comparisons
1421            expression_compares_to_null(left, var_name)
1422                || expression_compares_to_null(right, var_name)
1423        }
1424        HirExpression::UnaryOp { operand, .. } => expression_compares_to_null(operand, var_name),
1425        _ => false,
1426    }
1427}
1428
1429/// Recursively check if a statement uses pointer arithmetic on a variable (DECY-125).
1430fn statement_uses_pointer_arithmetic(stmt: &HirStatement, var_name: &str) -> bool {
1431    use decy_hir::BinaryOperator;
1432    match stmt {
1433        HirStatement::Assignment { target, value } => {
1434            // Check if this is var = var + n or var = var - n (pointer arithmetic)
1435            if target == var_name {
1436                if let HirExpression::BinaryOp { op, left, .. } = value {
1437                    if matches!(op, BinaryOperator::Add | BinaryOperator::Subtract) {
1438                        if let HirExpression::Variable(name) = &**left {
1439                            if name == var_name {
1440                                return true;
1441                            }
1442                        }
1443                    }
1444                }
1445            }
1446            false
1447        }
1448        HirStatement::If {
1449            then_block,
1450            else_block,
1451            ..
1452        } => {
1453            then_block
1454                .iter()
1455                .any(|s| statement_uses_pointer_arithmetic(s, var_name))
1456                || else_block.as_ref().is_some_and(|blk| {
1457                    blk.iter()
1458                        .any(|s| statement_uses_pointer_arithmetic(s, var_name))
1459                })
1460        }
1461        HirStatement::While { body, .. } | HirStatement::For { body, .. } => body
1462            .iter()
1463            .any(|s| statement_uses_pointer_arithmetic(s, var_name)),
1464        _ => false,
1465    }
1466}
1467
1468#[cfg(test)]
1469mod tests {
1470    use super::*;
1471
1472    #[test]
1473    fn test_transpile_simple_function() {
1474        let c_code = "int add(int a, int b) { return a + b; }";
1475        let result = transpile(c_code);
1476        assert!(result.is_ok(), "Transpilation should succeed");
1477
1478        let rust_code = result.unwrap();
1479        assert!(rust_code.contains("fn add"), "Should contain function name");
1480        assert!(rust_code.contains("i32"), "Should contain Rust int type");
1481    }
1482
1483    #[test]
1484    fn test_transpile_with_parameters() {
1485        let c_code = "int multiply(int x, int y) { return x * y; }";
1486        let result = transpile(c_code);
1487        assert!(result.is_ok());
1488
1489        let rust_code = result.unwrap();
1490        assert!(rust_code.contains("fn multiply"));
1491        assert!(rust_code.contains("x"));
1492        assert!(rust_code.contains("y"));
1493    }
1494
1495    #[test]
1496    fn test_transpile_void_function() {
1497        let c_code = "void do_nothing() { }";
1498        let result = transpile(c_code);
1499        assert!(result.is_ok());
1500
1501        let rust_code = result.unwrap();
1502        assert!(rust_code.contains("fn do_nothing"));
1503    }
1504
1505    #[test]
1506    fn test_transpile_with_box_transform_simple() {
1507        // Simple test without actual malloc (just function structure)
1508        let c_code = "int get_value() { return 42; }";
1509        let result = transpile_with_box_transform(c_code);
1510        assert!(result.is_ok());
1511
1512        let rust_code = result.unwrap();
1513        assert!(rust_code.contains("fn get_value"));
1514    }
1515
1516    #[test]
1517    fn test_transpile_empty_input() {
1518        let c_code = "";
1519        let result = transpile(c_code);
1520        // Empty input should parse successfully but produce no functions
1521        assert!(result.is_ok());
1522    }
1523
1524    #[test]
1525    fn test_transpile_integration_pipeline() {
1526        // Test that the full pipeline runs without errors
1527        let c_code = r#"
1528            int calculate(int a, int b) {
1529                int result;
1530                result = a + b;
1531                return result;
1532            }
1533        "#;
1534        let result = transpile(c_code);
1535        assert!(result.is_ok(), "Full pipeline should execute");
1536
1537        let rust_code = result.unwrap();
1538        assert!(rust_code.contains("fn calculate"));
1539        assert!(rust_code.contains("let mut result"));
1540    }
1541
1542    #[test]
1543    fn test_transpile_with_lifetime_annotations() {
1544        // Test that functions with references get lifetime annotations
1545        // Note: This test depends on the C parser's ability to handle references
1546        // For now, we test that the pipeline runs successfully
1547        let c_code = "int add(int a, int b) { return a + b; }";
1548        let result = transpile(c_code);
1549        assert!(
1550            result.is_ok(),
1551            "Transpilation with lifetime analysis should succeed"
1552        );
1553
1554        let rust_code = result.unwrap();
1555        // Basic transpilation should work
1556        assert!(rust_code.contains("fn add"));
1557
1558        // When references are present, lifetime annotations would appear
1559        // Future: Add a test with actual C pointer parameters to verify '<'a> syntax
1560    }
1561}