Skip to main content

depyler_core/rust_gen/
func_gen_inference.rs

1//! Advanced function codegen: nested functions, type inference helpers
2//!
3//! DEPYLER-COVERAGE-95: Extracted from func_gen.rs to reduce file size
4//! and improve testability. Contains return type inference and nested function detection.
5
6use crate::hir::*;
7use crate::lifetime_analysis::LifetimeInference;
8use crate::rust_gen::context::{CodeGenContext, RustCodeGen};
9use crate::rust_gen::control_flow_analysis::stmt_always_returns;
10use crate::rust_gen::func_gen::{
11    build_var_type_env, codegen_function_body, codegen_function_params,
12    collect_return_types_with_env, function_returns_owned_string,
13    function_returns_string_concatenation, infer_expr_type_with_env, infer_param_type_from_body,
14    infer_return_type_from_body_with_params, rewrite_adt_child_type,
15};
16use crate::rust_gen::func_gen_helpers::{
17    codegen_function_attrs, codegen_generic_params, codegen_where_clause,
18};
19use crate::rust_gen::generator_gen::codegen_generator_function;
20use crate::rust_gen::keywords::is_rust_keyword;
21use crate::rust_gen::rust_type_to_syn;
22use crate::rust_gen::type_gen::update_import_needs;
23use anyhow::Result;
24use quote::quote;
25use syn::parse_quote;
26
27/// GH-70: Detect if function returns a nested function/closure
28/// Returns Some((nested_fn_name, params, ret_type)) if detected
29/// Stores inferred params in ctx.nested_function_params for use during code generation
30pub(crate) fn detect_returns_nested_function(
31    func: &HirFunction,
32    ctx: &mut CodeGenContext,
33) -> Option<(String, Vec<HirParam>, Type)> {
34    // Look for pattern: function contains nested FunctionDef and ends with returning that name
35    let mut nested_functions: std::collections::HashMap<String, (Vec<HirParam>, Type)> =
36        std::collections::HashMap::new();
37
38    // Collect nested function definitions with type inference
39    for stmt in &func.body {
40        if let HirStmt::FunctionDef {
41            name,
42            params,
43            ret_type,
44            body,
45            ..
46        } = stmt
47        {
48            // GH-70: Apply type inference to parameters
49            // DEPYLER-0737: Also handle Optional(Unknown) for params with default=None
50            let mut inferred_params = params.to_vec();
51            for param in &mut inferred_params {
52                if matches!(param.ty, Type::Unknown) {
53                    // Try to infer from body usage
54                    if let Some(inferred_ty) = infer_param_type_from_body(&param.name, body) {
55                        param.ty = inferred_ty;
56                    }
57                } else if let Type::Optional(inner) = &param.ty {
58                    // DEPYLER-0737: If param is Optional(Unknown), infer inner type and wrap
59                    if matches!(inner.as_ref(), Type::Unknown) {
60                        if let Some(inferred_ty) = infer_param_type_from_body(&param.name, body) {
61                            param.ty = Type::Optional(Box::new(inferred_ty));
62                        }
63                    }
64                }
65            }
66
67            // GH-70: Apply type inference to return type
68            // Include inferred param types in the environment so that
69            // expressions like `return item[0]` can infer the element type
70            let inferred_ret_type = if matches!(ret_type, Type::Unknown) {
71                // Build type env with inferred params
72                let mut var_types: std::collections::HashMap<String, Type> =
73                    std::collections::HashMap::new();
74                for p in &inferred_params {
75                    var_types.insert(p.name.clone(), p.ty.clone());
76                }
77                // Build from body assignments
78                build_var_type_env(body, &mut var_types);
79
80                // Collect return types using the enhanced environment
81                let mut return_types = Vec::new();
82                collect_return_types_with_env(body, &mut return_types, &var_types);
83
84                // Check for trailing expression
85                if let Some(HirStmt::Expr(expr)) = body.last() {
86                    let trailing_type = infer_expr_type_with_env(expr, &var_types);
87                    if !matches!(trailing_type, Type::Unknown) {
88                        return_types.push(trailing_type);
89                    }
90                }
91
92                // Get first known type
93                return_types
94                    .iter()
95                    .find(|t| !matches!(t, Type::Unknown))
96                    .cloned()
97                    .unwrap_or_else(|| ret_type.clone())
98            } else {
99                ret_type.clone()
100            };
101
102            // Store inferred params in context for use during code generation
103            ctx.nested_function_params
104                .insert(name.clone(), inferred_params.clone());
105
106            nested_functions.insert(name.clone(), (inferred_params, inferred_ret_type));
107        }
108    }
109
110    // Check if last statement returns one of the nested functions
111    if let Some(last_stmt) = func.body.last() {
112        // Pattern 1: explicit return statement
113        if let HirStmt::Return(Some(HirExpr::Var(var_name))) = last_stmt {
114            if let Some((params, ret_type)) = nested_functions.get(var_name) {
115                return Some((var_name.clone(), params.clone(), ret_type.clone()));
116            }
117        }
118        // Pattern 2: implicit return (expression statement at end)
119        if let HirStmt::Expr(HirExpr::Var(var_name)) = last_stmt {
120            if let Some((params, ret_type)) = nested_functions.get(var_name) {
121                return Some((var_name.clone(), params.clone(), ret_type.clone()));
122            }
123        }
124    }
125
126    None
127}
128
129/// DEPYLER-0626: Check if function returns heterogeneous IO types (File vs Stdout)
130/// Returns true if function has return statements that return both file and stdio types
131pub(crate) fn function_returns_heterogeneous_io(func: &HirFunction) -> bool {
132    let mut has_file_return = false;
133    let mut has_stdio_return = false;
134
135    collect_io_return_types(&func.body, &mut has_file_return, &mut has_stdio_return);
136
137    has_file_return && has_stdio_return
138}
139
140/// DEPYLER-0626: Helper to collect IO return types from statements
141pub(crate) fn collect_io_return_types(
142    stmts: &[HirStmt],
143    has_file: &mut bool,
144    has_stdio: &mut bool,
145) {
146    for stmt in stmts {
147        match stmt {
148            HirStmt::Return(Some(expr)) => {
149                if is_file_creating_return_expr(expr) {
150                    *has_file = true;
151                }
152                if is_stdio_return_expr(expr) {
153                    *has_stdio = true;
154                }
155            }
156            HirStmt::If {
157                then_body,
158                else_body,
159                ..
160            } => {
161                collect_io_return_types(then_body, has_file, has_stdio);
162                if let Some(else_stmts) = else_body {
163                    collect_io_return_types(else_stmts, has_file, has_stdio);
164                }
165            }
166            HirStmt::While { body, .. } | HirStmt::For { body, .. } => {
167                collect_io_return_types(body, has_file, has_stdio);
168            }
169            _ => {}
170        }
171    }
172}
173
174/// DEPYLER-0626: Check if expression creates a File (open() or File::create())
175pub(crate) fn is_file_creating_return_expr(expr: &HirExpr) -> bool {
176    match expr {
177        HirExpr::Call { func, .. } => func == "open",
178        HirExpr::MethodCall { object, method, .. } => {
179            if method == "create" || method == "open" {
180                if let HirExpr::Var(name) = object.as_ref() {
181                    return name == "File";
182                }
183                if let HirExpr::Attribute { attr, .. } = object.as_ref() {
184                    return attr == "File";
185                }
186            }
187            false
188        }
189        _ => false,
190    }
191}
192
193/// DEPYLER-0626: Check if expression is sys.stdout or sys.stderr
194pub(crate) fn is_stdio_return_expr(expr: &HirExpr) -> bool {
195    if let HirExpr::Attribute { value, attr } = expr {
196        if attr == "stdout" || attr == "stderr" {
197            if let HirExpr::Var(name) = value.as_ref() {
198                return name == "sys";
199            }
200        }
201    }
202    false
203}
204
205/// Generate return type with Result wrapper and lifetime handling
206///
207/// DEPYLER-0310: Now returns ErrorType (4th tuple element) for raise statement wrapping
208/// GH-70: Now detects when function returns nested function and uses Box<dyn Fn>
209#[inline]
210pub(crate) fn codegen_return_type(
211    func: &HirFunction,
212    lifetime_result: &crate::lifetime_analysis::LifetimeResult,
213    ctx: &mut CodeGenContext,
214) -> Result<(
215    proc_macro2::TokenStream,
216    crate::type_mapper::RustType,
217    bool,
218    Option<crate::rust_gen::context::ErrorType>,
219)> {
220    // GH-70: Check if function returns a nested function/closure
221    if let Some((_nested_name, params, nested_ret_type)) = detect_returns_nested_function(func, ctx)
222    {
223        use quote::quote;
224
225        // Build Box<dyn Fn(params) -> ret> type
226        let param_types: Vec<proc_macro2::TokenStream> = params
227            .iter()
228            .map(|p| crate::rust_gen::type_tokens::hir_type_to_tokens(&p.ty))
229            .collect();
230
231        let ret_ty_tokens = crate::rust_gen::type_tokens::hir_type_to_tokens(&nested_ret_type);
232
233        let fn_type = if params.is_empty() {
234            quote! { -> Box<dyn Fn() -> #ret_ty_tokens> }
235        } else {
236            quote! { -> Box<dyn Fn(#(#param_types),*) -> #ret_ty_tokens> }
237        };
238
239        return Ok((
240            fn_type.clone(),
241            crate::type_mapper::RustType::Custom("BoxedFn".to_string()),
242            false, // can_fail
243            None,  // error_type
244        ));
245    }
246
247    // DEPYLER-0626: Check if function returns heterogeneous IO types (File vs Stdout)
248    // If so, return type should be Box<dyn std::io::Write>
249    if function_returns_heterogeneous_io(func) {
250        use quote::quote;
251        ctx.function_returns_boxed_write = true;
252        ctx.needs_io_write = true;
253
254        // Check if function can fail (uses open() which can fail)
255        let can_fail = func.properties.can_fail;
256        let error_type = if can_fail {
257            Some(crate::rust_gen::context::ErrorType::Concrete(
258                "std::io::Error".to_string(),
259            ))
260        } else {
261            None
262        };
263
264        let return_type = if can_fail {
265            quote! { -> Result<Box<dyn std::io::Write>, std::io::Error> }
266        } else {
267            quote! { -> Box<dyn std::io::Write> }
268        };
269
270        return Ok((
271            return_type,
272            crate::type_mapper::RustType::Custom("BoxedWrite".to_string()),
273            can_fail,
274            error_type,
275        ));
276    }
277
278    // DEPYLER-0410: Infer return type from body when annotation is Unknown
279    // DEPYLER-0420: Also infer when tuple/list contains Unknown elements
280    // DEPYLER-0460: Use _with_params version for Optional pattern detection
281    // DEPYLER-0460: Also infer when ret_type is None, because that could be:
282    // 1. A function returning None in all paths → () in Rust
283    // 2. A function returning None|T (Optional pattern) → Option<T> in Rust
284    // DEPYLER-0662: Also infer when ret_type is empty tuple (from `-> tuple` annotation)
285    // Python `-> tuple` without type params should be inferred from return statements
286    // DEPYLER-0662: Python `-> tuple` parses to Type::Custom("tuple"), not Type::Tuple
287    // DEPYLER-1209: Also check for UnificationVar
288    let should_infer = matches!(func.ret_type, Type::Unknown | Type::None)
289        || matches!(&func.ret_type, Type::Tuple(elems) if elems.is_empty() || elems.iter().any(|t| matches!(t, Type::Unknown)))
290        || matches!(&func.ret_type, Type::List(elem) if matches!(**elem, Type::Unknown | Type::UnificationVar(_)))
291        || matches!(&func.ret_type, Type::Custom(name) if name == "tuple");
292
293    let effective_ret_type = if should_infer {
294        // Try to infer from return statements in body (with parameter type tracking for Optional detection)
295        infer_return_type_from_body_with_params(func, ctx).unwrap_or_else(|| func.ret_type.clone())
296    } else {
297        func.ret_type.clone()
298    };
299
300    // DEPYLER-0719: Update function_return_types with inferred type
301    // When a function's return type is inferred (e.g., `-> tuple` → `(f64, f64)`),
302    // update the map so callers like `point: tuple = get_point()` can use the inferred type
303    if should_infer && effective_ret_type != func.ret_type {
304        ctx.function_return_types
305            .insert(func.name.clone(), effective_ret_type.clone());
306    }
307
308    // DEPYLER-0716: Apply type substitutions to return type
309    // When generic parameters are substituted (e.g., T -> String), apply to return type too
310    let effective_ret_type = if !ctx.type_substitutions.is_empty() {
311        crate::generic_inference::TypeVarRegistry::apply_substitutions(
312            &effective_ret_type,
313            &ctx.type_substitutions,
314        )
315    } else {
316        effective_ret_type
317    };
318
319    // DEPYLER-0936: Rewrite ADT child types to parent enum types
320    // When a Python ABC hierarchy is converted to a Rust enum, return types mentioning
321    // child classes (e.g., ListIter[T]) must be rewritten to parent (e.g., Iter[T])
322    let effective_ret_type = rewrite_adt_child_type(&effective_ret_type, &ctx.adt_child_to_parent);
323
324    // Convert return type using annotation-aware mapping
325    let mapped_ret_type = ctx
326        .annotation_aware_mapper
327        .map_return_type_with_annotations(&effective_ret_type, &func.annotations);
328
329    // Check if this is a placeholder Union enum that needs proper generation
330    let rust_ret_type = if let crate::type_mapper::RustType::Enum { name, .. } = &mapped_ret_type {
331        if name == "UnionType" {
332            // Generate a proper enum name and definition from the original Union type
333            if let Type::Union(types) = &func.ret_type {
334                let enum_name = ctx.process_union_type(types);
335                crate::type_mapper::RustType::Custom(enum_name)
336            } else {
337                mapped_ret_type
338            }
339        } else {
340            mapped_ret_type
341        }
342    } else {
343        mapped_ret_type
344    };
345
346    // v3.16.0 Phase 1: Override return type to String if function returns owned via string methods
347    // This prevents lifetime analysis from incorrectly converting to borrowed &str
348    let rust_ret_type =
349        if matches!(func.ret_type, Type::String) && function_returns_owned_string(func) {
350            // Force owned String return, don't use lifetime borrowing
351            crate::type_mapper::RustType::String
352        } else {
353            rust_ret_type
354        };
355
356    // Update import needs based on return type
357    update_import_needs(ctx, &rust_ret_type);
358
359    // Check if function can fail and needs Result wrapper
360    let can_fail = func.properties.can_fail;
361    let mut error_type_str = if can_fail && !func.properties.error_types.is_empty() {
362        // Use first error type or generic for mixed types
363        if func.properties.error_types.len() == 1 {
364            func.properties.error_types[0].clone()
365        } else {
366            "Box<dyn std::error::Error>".to_string()
367        }
368    } else {
369        "Box<dyn std::error::Error>".to_string()
370    };
371
372    // DEPYLER-0597: Map Python exception types to Rust error types
373    // This ensures function signatures like `-> Result<T, OSError>` compile
374    // Using Box<dyn std::error::Error> for most exceptions since it doesn't require external crates
375    error_type_str = match error_type_str.as_str() {
376        // File/IO related exceptions map to std::io::Error for idiomatic Rust
377        "OSError" | "IOError" | "FileNotFoundError" | "PermissionError" => {
378            "std::io::Error".to_string()
379        }
380        // General exceptions map to Box<dyn std::error::Error> (no external crate needed)
381        "Exception"
382        | "BaseException"
383        | "ValueError"
384        | "TypeError"
385        | "KeyError"
386        | "IndexError"
387        | "RuntimeError"
388        | "AttributeError"
389        | "NotImplementedError"
390        | "AssertionError"
391        | "StopIteration"
392        | "ZeroDivisionError"
393        | "OverflowError"
394        | "ArithmeticError" => "Box<dyn std::error::Error>".to_string(),
395        // Keep other types as-is (might be custom error types)
396        _ => error_type_str,
397    };
398
399    // DEPYLER-0447: Validators always use Box<dyn Error> for compatibility with clap
400    if ctx.validator_functions.contains(&func.name) {
401        error_type_str = "Box<dyn std::error::Error>".to_string();
402    }
403
404    // DEPYLER-0310: Determine ErrorType for raise statement wrapping
405    // If Box<dyn Error>, we need to wrap exceptions with Box::new()
406    // If concrete type, no wrapping needed
407    let error_type = if can_fail {
408        Some(if error_type_str.contains("Box<dyn") {
409            crate::rust_gen::context::ErrorType::DynBox
410        } else {
411            crate::rust_gen::context::ErrorType::Concrete(error_type_str.clone())
412        })
413    } else {
414        None
415    };
416
417    // DEPYLER-0327 Fix #5: Mark error types as needed for type generation
418    // Check BOTH error_type_str (for functions that return Result) AND
419    // func.properties.error_types (for types used in try/except blocks)
420    // DEPYLER-0551: Added RuntimeError and FileNotFoundError
421    if error_type_str.contains("ZeroDivisionError") {
422        ctx.needs_zerodivisionerror = true;
423    }
424    if error_type_str.contains("IndexError") {
425        ctx.needs_indexerror = true;
426    }
427    if error_type_str.contains("ValueError") {
428        ctx.needs_valueerror = true;
429    }
430    if error_type_str.contains("RuntimeError") {
431        ctx.needs_runtimeerror = true;
432    }
433    if error_type_str.contains("FileNotFoundError") {
434        ctx.needs_filenotfounderror = true;
435    }
436
437    // Also check all error_types from properties (even if can_fail=false)
438    // This ensures types used in try/except blocks are generated
439    for err_type in &func.properties.error_types {
440        if err_type.contains("ZeroDivisionError") {
441            ctx.needs_zerodivisionerror = true;
442        }
443        if err_type.contains("IndexError") {
444            ctx.needs_indexerror = true;
445        }
446        if err_type.contains("ValueError") {
447            ctx.needs_valueerror = true;
448        }
449        if err_type.contains("RuntimeError") {
450            ctx.needs_runtimeerror = true;
451        }
452        if err_type.contains("FileNotFoundError") {
453            ctx.needs_filenotfounderror = true;
454        }
455    }
456
457    let return_type = if matches!(rust_ret_type, crate::type_mapper::RustType::Unit) {
458        if can_fail {
459            let error_type: syn::Type = syn::parse_str(&error_type_str)
460                .unwrap_or_else(|_| parse_quote! { Box<dyn std::error::Error> });
461
462            // DEPYLER-0455 #7: Infer return type from function body
463            // Functions without type annotations but that return values (e.g., argparse validators)
464            // should infer their return type from actual return statements
465            //
466            // Example: def email_address(value):
467            //              return value  # <- Returns string, not None
468            //
469            // Before fix: Result<(), Box<dyn Error>>  [WRONG - type mismatch with returned value]
470            // After fix:  Result<String, Box<dyn Error>>  [CORRECT - matches return value]
471            if let Some(inferred_type) = infer_return_type_from_body_with_params(func, ctx) {
472                // We found a return statement with a value!
473                // Map the inferred HIR type to Rust type
474                let inferred_rust_type = ctx
475                    .annotation_aware_mapper
476                    .map_return_type_with_annotations(&inferred_type, &func.annotations);
477
478                // Convert to syn type
479                if let Ok(ty) = rust_type_to_syn(&inferred_rust_type) {
480                    // DEPYLER-0612: main() can only return () or Result<(), E>
481                    if func.name == "main" {
482                        quote! { -> Result<(), #error_type> }
483                    } else {
484                        // Use inferred type instead of ()
485                        quote! { -> Result<#ty, #error_type> }
486                    }
487                } else {
488                    // Fallback to () if conversion fails
489                    quote! { -> Result<(), #error_type> }
490                }
491            } else {
492                // No return value found, use ()
493                quote! { -> Result<(), #error_type> }
494            }
495        } else {
496            quote! {}
497        }
498    } else {
499        let mut ty = rust_type_to_syn(&rust_ret_type)?;
500
501        // DEPYLER-0270: Check if function returns string concatenation
502        // String concatenation (format!(), a + b) always returns owned String
503        // Never use Cow for concatenation results
504        let returns_concatenation = matches!(func.ret_type, crate::hir::Type::String)
505            && function_returns_string_concatenation(func);
506
507        // Check if any parameter escapes through return and uses Cow
508        let mut uses_cow_return = false;
509        if !returns_concatenation {
510            // Only consider Cow if NOT doing string concatenation
511            for param in &func.params {
512                if let Some(strategy) = lifetime_result.borrowing_strategies.get(&param.name) {
513                    if matches!(
514                        strategy,
515                        crate::borrowing_context::BorrowingStrategy::UseCow { .. }
516                    ) {
517                        if let Some(_usage) = lifetime_result.param_lifetimes.get(&param.name) {
518                            // If a Cow parameter escapes, return type should also be Cow
519                            if matches!(func.ret_type, crate::hir::Type::String) {
520                                uses_cow_return = true;
521                                break;
522                            }
523                        }
524                    }
525                }
526            }
527        }
528
529        if uses_cow_return && !returns_concatenation {
530            // Use the same Cow type for return
531            ctx.needs_cow = true;
532            if let Some(ref return_lt) = lifetime_result.return_lifetime {
533                let lt = syn::Lifetime::new(return_lt.as_str(), proc_macro2::Span::call_site());
534                ty = parse_quote! { Cow<#lt, str> };
535            } else {
536                ty = parse_quote! { Cow<'static, str> };
537            }
538        } else {
539            // v3.16.0 Phase 1: Check if function returns owned String via transformation methods
540            // If so, don't convert to borrowed &str even if lifetime analysis suggests it
541            let returns_owned_string =
542                matches!(func.ret_type, Type::String) && function_returns_owned_string(func);
543
544            // Apply return lifetime if needed (unless returning owned String)
545            if let Some(ref return_lt) = lifetime_result.return_lifetime {
546                // Check if the return type needs lifetime substitution
547                if matches!(
548                    rust_ret_type,
549                    crate::type_mapper::RustType::Str { .. }
550                        | crate::type_mapper::RustType::Reference { .. }
551                ) && !returns_owned_string
552                {
553                    // Only apply lifetime if NOT returning owned String
554                    let lt = syn::Lifetime::new(return_lt.as_str(), proc_macro2::Span::call_site());
555                    match &rust_ret_type {
556                        crate::type_mapper::RustType::Str { .. } => {
557                            ty = parse_quote! { &#lt str };
558                        }
559                        crate::type_mapper::RustType::Reference { mutable, inner, .. } => {
560                            let inner_ty = rust_type_to_syn(inner)?;
561                            ty = if *mutable {
562                                parse_quote! { &#lt mut #inner_ty }
563                            } else {
564                                parse_quote! { &#lt #inner_ty }
565                            };
566                        }
567                        _ => {}
568                    }
569                }
570            }
571            // If returns_owned_string is true, keep ty as String (already set from rust_type_to_syn)
572        }
573
574        if can_fail {
575            let error_type: syn::Type = syn::parse_str(&error_type_str)
576                .unwrap_or_else(|_| parse_quote! { Box<dyn std::error::Error> });
577
578            // DEPYLER-0612: main() can only return () or Result<(), E>
579            // Convert Result<i32, E> to Result<(), E> for main
580            if func.name == "main" {
581                quote! { -> Result<(), #error_type> }
582            } else {
583                quote! { -> Result<#ty, #error_type> }
584            }
585        } else if func.name == "main" && matches!(func.ret_type, Type::Int) {
586            // DEPYLER-0617: main() can only return () or Result<(), E>
587            // Convert i32 return to () for non-fallible main
588            quote! {} // No return type annotation (defaults to ())
589        } else {
590            quote! { -> #ty }
591        }
592    };
593
594    Ok((return_type, rust_ret_type, can_fail, error_type))
595}
596
597// ========== Phase 3c: Generator Implementation ==========
598// (Moved to generator_gen.rs in v3.18.0 Phase 4)
599
600/// DEPYLER-1181: Preload type annotations from HIR statements into var_types context
601/// This ensures that inferred types from the constraint solver are available during
602/// expression code generation. Without this, the "Neural Link" (DEPYLER-1180) propagation
603/// of types to HIR annotations is ignored by the code generator.
604///
605/// The flow is:
606/// 1. Constraint solver infers types (DEPYLER-1173)
607/// 2. apply_substitutions writes types to HIR type_annotation fields (DEPYLER-1180)
608/// 3. THIS function reads those annotations into ctx.var_types (DEPYLER-1181)
609/// 4. Expression codegen can now access inferred types
610pub(crate) fn preload_hir_type_annotations(body: &[HirStmt], ctx: &mut CodeGenContext) {
611    for stmt in body {
612        preload_stmt_type_annotations(stmt, ctx);
613    }
614}
615
616/// DEPYLER-1181: Recursively extract type annotations from a single statement
617fn preload_stmt_type_annotations(stmt: &HirStmt, ctx: &mut CodeGenContext) {
618    match stmt {
619        HirStmt::Assign {
620            target: AssignTarget::Symbol(var_name),
621            type_annotation: Some(ty),
622            ..
623        } => {
624            // Only preload non-Unknown types to avoid overwriting better inferences
625            if !matches!(ty, Type::Unknown) {
626                // DEPYLER-99MODE-S9: Don't overwrite concrete parameter types with
627                // incorrect HM-inferred types. E.g., `prefix: str` should not be
628                // overwritten by HM inference of `prefix = prefix[:-1]` as List(Int).
629                // Only overwrite if existing type is Unknown or if new type is the same kind.
630                let should_overwrite = match ctx.var_types.get(var_name) {
631                    None => true,
632                    Some(Type::Unknown) => true,
633                    Some(existing) => {
634                        // Don't overwrite concrete types (String, Int, etc.) with
635                        // structurally different inferred types from HM
636                        std::mem::discriminant(existing) == std::mem::discriminant(ty)
637                    }
638                };
639                if should_overwrite {
640                    ctx.var_types.insert(var_name.clone(), ty.clone());
641                }
642            }
643        }
644        // Recursively handle nested statements
645        HirStmt::If {
646            then_body,
647            else_body,
648            ..
649        } => {
650            for s in then_body {
651                preload_stmt_type_annotations(s, ctx);
652            }
653            if let Some(else_stmts) = else_body {
654                for s in else_stmts {
655                    preload_stmt_type_annotations(s, ctx);
656                }
657            }
658        }
659        HirStmt::While { body, .. } | HirStmt::Block(body) => {
660            for s in body {
661                preload_stmt_type_annotations(s, ctx);
662            }
663        }
664        HirStmt::For { body, .. } => {
665            for s in body {
666                preload_stmt_type_annotations(s, ctx);
667            }
668        }
669        HirStmt::Try {
670            body,
671            handlers,
672            orelse,
673            finalbody,
674        } => {
675            for s in body {
676                preload_stmt_type_annotations(s, ctx);
677            }
678            for handler in handlers {
679                for s in &handler.body {
680                    preload_stmt_type_annotations(s, ctx);
681                }
682            }
683            if let Some(orelse_stmts) = orelse {
684                for s in orelse_stmts {
685                    preload_stmt_type_annotations(s, ctx);
686                }
687            }
688            if let Some(final_stmts) = finalbody {
689                for s in final_stmts {
690                    preload_stmt_type_annotations(s, ctx);
691                }
692            }
693        }
694        HirStmt::With { body, .. } => {
695            for s in body {
696                preload_stmt_type_annotations(s, ctx);
697            }
698        }
699        HirStmt::FunctionDef { body, .. } => {
700            // Note: Nested functions have their own scope, but we still preload
701            // for consistency. The function's own codegen will clear and repopulate.
702            for s in body {
703                preload_stmt_type_annotations(s, ctx);
704            }
705        }
706        _ => {}
707    }
708}
709
710impl RustCodeGen for HirFunction {
711    fn to_rust_tokens(&self, ctx: &mut CodeGenContext) -> Result<proc_macro2::TokenStream> {
712        // DEPYLER-0717: Clear var_types at the start of each function to prevent type leaking
713        // Without this, parameter types from one function can leak to the next function
714        // when they share the same parameter name (e.g., both have `items` parameter)
715        ctx.var_types.clear();
716        ctx.type_substitutions.clear();
717        // DEPYLER-1150: Clear slice params from previous function
718        ctx.slice_params.clear();
719
720        // DEPYLER-0306 FIX: Use raw identifiers for function names that are Rust keywords
721        let name = if is_rust_keyword(&self.name) {
722            syn::Ident::new_raw(&self.name, proc_macro2::Span::call_site())
723        } else {
724            syn::Ident::new(&self.name, proc_macro2::Span::call_site())
725        };
726
727        // DEPYLER-0269: Track function return type for Display trait selection
728        // Store function return type in ctx for later lookup when processing assignments
729        // This enables tracking `result = merge(&a, &b)` where merge returns list[int]
730        ctx.function_return_types
731            .insert(self.name.clone(), self.ret_type.clone());
732
733        // DEPYLER-0621: Track parameter defaults for call-site argument completion
734        // When a function like `def f(x=None)` is called as `f()`, we need to supply `None`
735        let param_defaults: Vec<Option<crate::hir::HirExpr>> =
736            self.params.iter().map(|p| p.default.clone()).collect();
737        ctx.function_param_defaults
738            .insert(self.name.clone(), param_defaults);
739
740        // Perform generic type inference
741        let mut generic_registry = crate::generic_inference::TypeVarRegistry::new();
742
743        // DEPYLER-0716: Infer type substitutions (e.g., T -> String when comparing to strings)
744        let type_substitutions = generic_registry.infer_type_substitutions(self)?;
745
746        // DEPYLER-0716: Apply substitutions to parameter types in var_types
747        // This ensures List(Unknown) becomes List(String) when elements are compared to strings
748        if !type_substitutions.is_empty() {
749            for param in &self.params {
750                let substituted_ty = crate::generic_inference::TypeVarRegistry::apply_substitutions(
751                    &param.ty,
752                    &type_substitutions,
753                );
754                if substituted_ty != param.ty {
755                    ctx.var_types.insert(param.name.clone(), substituted_ty);
756                }
757            }
758            // DEPYLER-0716: Store substitutions in context for return type processing
759            ctx.type_substitutions = type_substitutions;
760        }
761
762        // DEPYLER-0524: Infer parameter types from usage in function body
763        // This updates var_types so parameters with Unknown type can be inferred from usage
764        // DEPYLER-0737: Also handle Optional(Unknown) for params with default=None
765        // IMPORTANT: Must run BEFORE generic inference so that inferred concrete types
766        // prevent unnecessary generic parameters from being generated
767        let mut inferred_params = self.params.clone();
768        for param in &mut inferred_params {
769            if matches!(param.ty, Type::Unknown) {
770                if let Some(inferred_ty) = infer_param_type_from_body(&param.name, &self.body) {
771                    param.ty = inferred_ty.clone();
772                    ctx.var_types.insert(param.name.clone(), inferred_ty);
773                }
774            } else if let Type::Optional(inner) = &param.ty {
775                // DEPYLER-0737: If param is Optional(Unknown), infer inner type and wrap in Optional
776                if matches!(inner.as_ref(), Type::Unknown) {
777                    if let Some(inferred_ty) = infer_param_type_from_body(&param.name, &self.body) {
778                        let new_ty = Type::Optional(Box::new(inferred_ty));
779                        param.ty = new_ty.clone();
780                        ctx.var_types.insert(param.name.clone(), new_ty);
781                    }
782                }
783            }
784        }
785
786        // Refine container element types for params like `numbers: list` → Vec<i32>
787        // This handles List(Unknown), Dict(_, Unknown), Set(Unknown) etc.
788        for param in &mut inferred_params {
789            if crate::container_element_inference::has_unknown_inner_type(&param.ty) {
790                if let Some(refined) =
791                    crate::container_element_inference::infer_container_element_type(
792                        &param.name,
793                        &param.ty,
794                        &self.body,
795                    )
796                {
797                    param.ty = refined.clone();
798                    ctx.var_types.insert(param.name.clone(), refined);
799                }
800            }
801        }
802
803        // Create a modified version of self with inferred params for generic inference
804        let inferred_self = HirFunction {
805            params: inferred_params,
806            ..self.clone()
807        };
808        let type_params = generic_registry.infer_function_generics(&inferred_self)?;
809
810        // Perform lifetime analysis with automatic elision (DEPYLER-0275)
811        let mut lifetime_inference = LifetimeInference::new();
812        let lifetime_result = lifetime_inference
813            .apply_elision_rules(self, ctx.type_mapper)
814            .unwrap_or_else(|| lifetime_inference.analyze_function(self, ctx.type_mapper));
815
816        // Generate combined generic parameters (lifetimes + type params)
817        let generic_params = codegen_generic_params(&type_params, &lifetime_result.lifetime_params);
818
819        // Generate lifetime bounds
820        let where_clause = codegen_where_clause(&lifetime_result.lifetime_bounds);
821
822        // DEPYLER-0738: Analyze variable mutability BEFORE parameter generation
823        // This detects reassignments (x = 1; x = 2) and method mutations (.insert(), .push())
824        // Must run before codegen_function_params so param_muts can access ctx.mutable_vars
825        crate::rust_gen::analyze_mutable_vars(&self.body, ctx, &self.params);
826
827        // Convert parameters using lifetime analysis results
828        let params = codegen_function_params(self, &lifetime_result, ctx)?;
829
830        // DEPYLER-0270: Extract parameter borrowing information for auto-borrow decisions
831        // Check which parameters are references (borrowed) vs owned
832        let param_borrows: Vec<bool> = self
833            .params
834            .iter()
835            .map(|p| {
836                lifetime_result
837                    .param_lifetimes
838                    .get(&p.name)
839                    .map(|inf| inf.should_borrow)
840                    .unwrap_or(false)
841            })
842            .collect();
843
844        // DEPYLER-0758: Populate ref_params with borrowed parameter names for current function
845        // Used in convert_binary to dereference reference params in arithmetic operations
846        ctx.ref_params.clear();
847        for (p, &is_borrowed) in self.params.iter().zip(param_borrows.iter()) {
848            if is_borrowed {
849                ctx.ref_params.insert(p.name.clone());
850            }
851        }
852
853        ctx.function_param_borrows
854            .insert(self.name.clone(), param_borrows);
855
856        // DEPYLER-0574: Extract parameter mutability information for &mut decisions
857        // Check which borrowed parameters need &mut (mutable borrow)
858        let param_muts: Vec<bool> = self
859            .params
860            .iter()
861            .map(|p| {
862                let is_mutated = ctx.mutable_vars.contains(&p.name);
863                let should_borrow = lifetime_result
864                    .param_lifetimes
865                    .get(&p.name)
866                    .map(|inf| inf.should_borrow)
867                    .unwrap_or(false);
868                // needs_mut = mutated in body AND borrowed (not owned)
869                is_mutated && should_borrow
870            })
871            .collect();
872
873        // DEPYLER-1217: Track parameters that are mutable references (&mut T)
874        // Used at call sites to avoid double &mut when passing these params to functions
875        ctx.mut_ref_params.clear();
876        for (p, &needs_mut) in self.params.iter().zip(param_muts.iter()) {
877            if needs_mut {
878                ctx.mut_ref_params.insert(p.name.clone());
879            }
880        }
881
882        ctx.function_param_muts
883            .insert(self.name.clone(), param_muts);
884
885        // DEPYLER-0779: Extract parameter optionality for Some() wrapping at call sites
886        // A parameter is optional if: (a) type is Optional(T), OR (b) default is None
887        let param_optionals: Vec<bool> = self
888            .params
889            .iter()
890            .map(|p| {
891                // Check if type is Optional(T)
892                let type_is_optional = matches!(p.ty, Type::Optional(_));
893                // Check if default value is None
894                let default_is_none = matches!(p.default, Some(HirExpr::Literal(Literal::None)));
895                type_is_optional || default_is_none
896            })
897            .collect();
898        ctx.function_param_optionals
899            .insert(self.name.clone(), param_optionals);
900
901        // DEPYLER-0648: Track if function has vararg parameter (*args in Python)
902        // These become &[T] in Rust, so call sites need to wrap args in &[...]
903        if self.params.iter().any(|p| p.is_vararg) {
904            ctx.vararg_functions.insert(self.name.clone());
905        }
906
907        // DEPYLER-0964: Track parameters that are &mut Option<HashMap<K, V>>
908        // These occur when param type is Dict[K,V] with default None
909        // Inside the function body, we need special handling:
910        // - Assignment: `memo = {}` → `*memo = Some(HashMap::new())`
911        // - Method calls: `memo.get(k)` → `memo.as_ref().unwrap().get(&k)`
912        // - Subscript: `memo[k] = v` → `memo.as_mut().unwrap().insert(k, v)`
913        for param in &self.params {
914            let is_dict = matches!(&param.ty, Type::Dict { .. })
915                || matches!(&param.ty, Type::Custom(name) if name == "dict");
916            let has_none_default = matches!(&param.default, Some(HirExpr::Literal(Literal::None)));
917            // Also check for Optional(Dict) type
918            let is_optional_dict = matches!(
919                &param.ty,
920                Type::Optional(inner) if matches!(inner.as_ref(), Type::Dict { .. })
921            );
922            if (is_dict && has_none_default) || is_optional_dict {
923                ctx.mut_option_dict_params.insert(param.name.clone());
924            }
925
926            // DEPYLER-1126: Track ALL parameters that are &mut Option<T> (any T)
927            // When param type is Optional<T> (or T | None) and the param is mutated,
928            // it becomes &mut Option<T>. Assignments need dereferencing: *param = value
929            let is_optional = matches!(&param.ty, Type::Optional(_))
930                || matches!(&param.ty, Type::Union(types) if types.iter().any(|t| matches!(t, Type::None)));
931            // Check if parameter is in the "needs_mut" set from lifetime analysis
932            let inferred_needs_mut = lifetime_result
933                .param_lifetimes
934                .get(&param.name)
935                .map(|ip| ip.needs_mut)
936                .unwrap_or(false);
937
938            // DEPYLER-99MODE-E0308: Track Option params that are mutated
939            if (is_optional || has_none_default) && inferred_needs_mut {
940                ctx.mut_option_params.insert(param.name.clone());
941            }
942        }
943
944        // Generate return type with Result wrapper and lifetime handling
945        let (return_type, rust_ret_type, can_fail, error_type) =
946            codegen_return_type(self, &lifetime_result, ctx)?;
947
948        // DEPYLER-0839/1075: Fix E0700 "hidden type captures lifetime" for impl Fn/Iterator returns
949        // When a function returns `impl Fn(...)` or `impl Iterator<...>` and captures reference parameters,
950        // the return type must include a lifetime bound: `impl Fn(...) + 'a` or `impl Iterator<...> + '_`
951        // and the function must have the lifetime parameter: `fn foo<'a>(p: &'a str) -> impl Fn(...) + 'a`
952        // Additionally, reference parameters must have the 'a lifetime: `&str` -> `&'a str`
953        let (generic_params, return_type, params) =
954            if let crate::type_mapper::RustType::Custom(ref type_str) = rust_ret_type {
955                if type_str.contains("impl Fn")
956                    || type_str.contains("impl Iterator")
957                    || type_str.contains("impl IntoIterator")
958                {
959                    // Check if any parameter is a reference (borrowed)
960                    // Access from ctx since param_borrows was moved into function_param_borrows earlier
961                    let has_ref_params = ctx
962                        .function_param_borrows
963                        .get(&self.name)
964                        .map(|borrows| borrows.iter().any(|&b| b))
965                        .unwrap_or(false);
966                    if has_ref_params {
967                        // DEPYLER-1080: Use single lifetime 'a for all reference params
968                        // When returning impl Iterator, all captured refs must share the same lifetime
969                        // Using separate lifetimes causes E0623 "lifetime may not live long enough"
970                        let mut lifetime_params_with_a = lifetime_result.lifetime_params.clone();
971                        if !lifetime_params_with_a.contains(&"'a".to_string()) {
972                            lifetime_params_with_a.push("'a".to_string());
973                        }
974                        // Remove any other lifetimes - use only 'a
975                        lifetime_params_with_a.retain(|lt| lt == "'a");
976                        let new_generic_params =
977                            codegen_generic_params(&type_params, &lifetime_params_with_a);
978
979                        // Modify return type to add + 'a bound
980                        // The return type looks like "-> impl Fn(...) -> R" and we need "-> impl Fn(...) -> R + 'a"
981                        // DEPYLER-1075: Also handle impl Iterator<Item=T> -> impl Iterator<Item=T> + 'a
982                        // DEPYLER-1080: Use single 'a lifetime for all refs to avoid E0623
983                        let return_str = return_type.to_string();
984                        let modified_return = if return_str.contains("impl Fn")
985                            || return_str.contains("impl Iterator")
986                            || return_str.contains("impl IntoIterator")
987                        {
988                            // Find the impl type and add + 'a at the end
989                            // Handle both simple `impl Fn(T) -> R` and `impl Iterator<Item=T>`
990                            let modified = format!("{} + 'a", return_str.trim());
991                            syn::parse_str::<proc_macro2::TokenStream>(&modified)
992                                .unwrap_or(return_type.clone())
993                        } else {
994                            return_type.clone()
995                        };
996
997                        // DEPYLER-0839/1075: Add 'a lifetime to reference parameter types
998                        // `&str` -> `&'a str`, `& mut T` -> `&'a mut T`, `& Vec<T>` -> `&'a Vec<T>`
999                        // DEPYLER-1080: Use SAME lifetime 'a for ALL ref params to avoid E0623
1000                        // This includes REPLACING any existing lifetimes ('b, 'c, etc.) with 'a
1001                        let modified_params: Vec<proc_macro2::TokenStream> = params
1002                            .into_iter()
1003                            .map(|p| {
1004                                let param_str = p.to_string();
1005                                // DEPYLER-1080: Replace ANY lifetime with 'a for impl Iterator returns
1006                                // First, replace existing lifetimes like 'b, 'c with 'a
1007                                let modified_param = param_str
1008                                    .replace("& 'b ", "& 'a ")
1009                                    .replace("& 'c ", "& 'a ")
1010                                    .replace("& 'd ", "& 'a ")
1011                                    .replace("& 'e ", "& 'a ");
1012                                // Then add 'a to refs without any lifetime
1013                                let modified_param = if modified_param.contains("& ")
1014                                    && !modified_param.contains("& '")
1015                                {
1016                                    modified_param
1017                                        .replace("& mut ", "& 'a mut ")
1018                                        .replace("& Vec", "& 'a Vec")
1019                                        .replace("& str", "& 'a str")
1020                                } else {
1021                                    modified_param
1022                                };
1023                                syn::parse_str::<proc_macro2::TokenStream>(&modified_param)
1024                                    .unwrap_or(p)
1025                            })
1026                            .collect();
1027
1028                        (new_generic_params, modified_return, modified_params)
1029                    } else {
1030                        (generic_params.clone(), return_type, params)
1031                    }
1032                } else {
1033                    (generic_params.clone(), return_type, params)
1034                }
1035            } else {
1036                (generic_params.clone(), return_type, params)
1037            };
1038
1039        // DEPYLER-0425: Analyze subcommand field access BEFORE generating body
1040        // This sets ctx.current_subcommand_fields so expression generation can rewrite args.field → field
1041        let subcommand_info = if ctx.argparser_tracker.has_subcommands() {
1042            crate::rust_gen::argparse_transform::analyze_subcommand_field_access(
1043                self,
1044                &ctx.argparser_tracker,
1045            )
1046        } else {
1047            None
1048        };
1049
1050        // Set context for expression generation
1051        if let Some((_, ref fields)) = subcommand_info {
1052            ctx.current_subcommand_fields = Some(fields.iter().cloned().collect());
1053        }
1054
1055        // DEPYLER-0456 #1: Pre-register all add_parser() calls before body codegen
1056        // This ensures expression statement subcommands (no variable assignment) are included
1057        // in Commands enum generation. Must run BEFORE codegen_function_body() below.
1058        crate::rust_gen::argparse_transform::preregister_subcommands_from_hir(
1059            self,
1060            &mut ctx.argparser_tracker,
1061        );
1062
1063        // DEPYLER-0108: Pre-populate Option fields for substitution BEFORE body codegen
1064        // This must happen before codegen_function_body() so that convert_method_call
1065        // can substitute args.<field>.is_some() with has_<field>
1066        if ctx.argparser_tracker.has_parsers() {
1067            if let Some(parser_info) = ctx.argparser_tracker.get_first_parser() {
1068                for arg in &parser_info.arguments {
1069                    if arg.rust_type().starts_with("Option<") {
1070                        ctx.precomputed_option_fields
1071                            .insert(arg.rust_field_name().to_string());
1072                    }
1073                }
1074            }
1075        }
1076
1077        // DEPYLER-0617: Set flag if we're generating main() function
1078        // This affects return statement handling (integer returns → process::exit)
1079        let was_main = ctx.is_main_function;
1080        ctx.is_main_function = self.name == "main";
1081
1082        // DEPYLER-1182: Preload PARAMETER types into var_types FIRST
1083        // Parameters have explicit type annotations from the function signature, which are
1084        // more reliable than HM-inferred types. Load these first so that
1085        // preload_hir_type_annotations won't overwrite them with incorrect inferences.
1086        for param in &self.params {
1087            if !matches!(param.ty, Type::Unknown) {
1088                ctx.var_types.insert(param.name.clone(), param.ty.clone());
1089            }
1090        }
1091
1092        // DEPYLER-1181: Preload HIR type annotations into var_types BEFORE body codegen
1093        // This ensures that types inferred by the constraint solver (DEPYLER-1173) and
1094        // propagated to HIR (DEPYLER-1180) are available during expression generation.
1095        // Without this, the code generator ignores inferred types (the "Deaf Mapper" problem).
1096        // NOTE: Runs AFTER parameter preload so that HM-inferred types don't overwrite
1097        // correct parameter types (DEPYLER-99MODE-S9).
1098        preload_hir_type_annotations(&self.body, ctx);
1099
1100        // Process function body with proper scoping (expressions will now be rewritten if needed)
1101        let mut body_stmts = codegen_function_body(self, can_fail, error_type, ctx)?;
1102
1103        // DEPYLER-0838: If function body is effectively empty (only pass statements) AND
1104        // return type is not unit, add unimplemented!() to satisfy the return type.
1105        // This handles Python's @abstractmethod pattern where body is just `pass`.
1106        {
1107            use quote::quote;
1108            let body_is_empty = body_stmts.iter().all(|stmt| stmt.is_empty());
1109            let is_non_unit_return = !matches!(rust_ret_type, crate::type_mapper::RustType::Unit);
1110            if body_is_empty && is_non_unit_return {
1111                body_stmts.push(quote! { unimplemented!() });
1112            }
1113        }
1114
1115        // DEPYLER-0694: If function returns unit type (no return annotation in Python),
1116        // ensure trailing expressions don't accidentally return a value.
1117        // Add semicolon to discard the expression's value when not returning.
1118        // DEPYLER-0702: Use `let _ = expr;` instead of `expr;` to avoid unused-must-use warnings
1119        if matches!(rust_ret_type, crate::type_mapper::RustType::Unit) {
1120            if let Some(last) = body_stmts.last_mut() {
1121                let last_str = last.to_string();
1122                // If statement doesn't end with semicolon or closing brace, it's an expression
1123                // that would return a value - we need to discard it for Unit return types
1124                // DEPYLER-0711: Skip empty tokens (e.g., from `pass` statement)
1125                if !last_str.is_empty()
1126                    && !last_str.trim_end().ends_with(';')
1127                    && !last_str.trim_end().ends_with('}')
1128                {
1129                    use quote::quote;
1130                    let tokens = std::mem::take(last);
1131                    // Use `let _ = expr;` to discard the value without triggering
1132                    // "unused arithmetic operation" or similar warnings
1133                    *last = quote! { let _ = #tokens; };
1134                }
1135            }
1136        }
1137
1138        // DEPYLER-0617: Restore flag after body generation
1139        ctx.is_main_function = was_main;
1140
1141        // GH-70: Wrap returned closure in Box::new() if function returns Box<dyn Fn>
1142        if let Some((nested_name, _, _)) = detect_returns_nested_function(self, ctx) {
1143            // Find last statement and wrap if it's returning the nested function
1144            if let Some(last_stmt) = body_stmts.last_mut() {
1145                use quote::quote;
1146                let nested_ident = syn::Ident::new(&nested_name, proc_macro2::Span::call_site());
1147                // Check if last statement is just the variable name (implicit return)
1148                let last_stmt_str = last_stmt.to_string();
1149                if last_stmt_str.trim() == nested_name {
1150                    // Replace with Box::new(name)
1151                    *last_stmt = quote! { Box::new(#nested_ident) };
1152                }
1153            }
1154        }
1155
1156        // Clear the subcommand fields context after body generation
1157        ctx.current_subcommand_fields = None;
1158
1159        // DEPYLER-0363: Check if ArgumentParser was detected and generate Args struct
1160        // DEPYLER-0424: Store Args struct and Commands enum in context for module-level emission
1161        // (hoisted outside function to make Args accessible to handler functions)
1162        if ctx.argparser_tracker.has_parsers() {
1163            if let Some(parser_info) = ctx.argparser_tracker.get_first_parser() {
1164                // DEPYLER-0384: Set flag to include clap dependency in Cargo.toml
1165                ctx.needs_clap = true;
1166
1167                // DEPYLER-0399: Generate Commands enum if subcommands exist
1168                let commands_enum = crate::rust_gen::argparse_transform::generate_commands_enum(
1169                    &ctx.argparser_tracker,
1170                );
1171                if !commands_enum.is_empty() {
1172                    ctx.generated_commands_enum = Some(commands_enum);
1173                }
1174
1175                // Generate the Args struct definition
1176                let args_struct = crate::rust_gen::argparse_transform::generate_args_struct(
1177                    parser_info,
1178                    &ctx.argparser_tracker,
1179                );
1180                ctx.generated_args_struct = Some(args_struct);
1181
1182                // DEPYLER-0108: Inject precompute statements for Option fields
1183                // This prevents borrow-after-move when Option is passed then checked with is_some()
1184                let precompute_stmts =
1185                    crate::rust_gen::argparse_transform::generate_option_precompute(parser_info);
1186                if !precompute_stmts.is_empty() {
1187                    // DEPYLER-0108: FIRST post-process body to replace args.<field>.is_some() with has_<field>
1188                    // This must happen BEFORE injecting precompute statements to avoid replacing them too
1189                    let option_fields: Vec<String> = parser_info
1190                        .arguments
1191                        .iter()
1192                        .filter(|arg| arg.rust_type().starts_with("Option<"))
1193                        .map(|arg| arg.rust_field_name().to_string())
1194                        .collect();
1195
1196                    if !option_fields.is_empty() {
1197                        body_stmts = body_stmts
1198                            .into_iter()
1199                            .map(|stmt| {
1200                                let mut stmt_str = stmt.to_string();
1201                                for field in &option_fields {
1202                                    // Replace "args . <field> . is_some ()" with "has_<field>"
1203                                    let pattern = format!("args . {} . is_some ()", field);
1204                                    let replacement = format!("has_{}", field);
1205                                    stmt_str = stmt_str.replace(&pattern, &replacement);
1206                                    // Also handle is_none
1207                                    let pattern_none = format!("args . {} . is_none ()", field);
1208                                    let replacement_none = format!("! has_{}", field);
1209                                    stmt_str = stmt_str.replace(&pattern_none, &replacement_none);
1210                                }
1211                                syn::parse_str(&stmt_str).unwrap_or(stmt)
1212                            })
1213                            .collect();
1214                    }
1215
1216                    // THEN inject precompute statements after replacement
1217                    // Find the Args::parse() statement index and insert after it
1218                    // The parse() call is typically the first statement in main()
1219                    let insert_idx = body_stmts
1220                        .iter()
1221                        .position(|s| s.to_string().contains("Args :: parse"))
1222                        .map(|i| i + 1)
1223                        .unwrap_or(0);
1224                    for (offset, stmt) in precompute_stmts.into_iter().enumerate() {
1225                        body_stmts.insert(insert_idx + offset, stmt);
1226                    }
1227                }
1228
1229                // Note: ArgumentParser-related statements are filtered in stmt_gen.rs
1230                // parse_args() calls are transformed in stmt_gen.rs::codegen_assign_stmt
1231            }
1232
1233            // DO NOT clear tracker yet - we need it for parameter type resolution
1234            // It will be cleared after all functions are generated
1235        }
1236
1237        // DEPYLER-0425: Wrap handler functions with subcommand pattern matching
1238        // If this function accesses subcommand-specific fields, wrap body in pattern matching
1239        // DEPYLER-0914: Skip wrapping when in_cmd_handler is true - fields are already parameters
1240        // In cmd_* handlers, expr_gen transforms args.field → field, so we don't need
1241        // the if let pattern to extract fields from args.command
1242        if let Some((variant_name, fields)) = subcommand_info {
1243            if !ctx.in_cmd_handler {
1244                // Get args parameter name (first parameter)
1245                if let Some(args_param) = self.params.first() {
1246                    let args_param_name = args_param.name.as_ref();
1247                    // Wrap body statements in pattern matching to extract fields from enum variant
1248                    body_stmts =
1249                        crate::rust_gen::argparse_transform::wrap_body_with_subcommand_pattern(
1250                            body_stmts,
1251                            &variant_name,
1252                            &fields,
1253                            args_param_name,
1254                        );
1255                }
1256            }
1257        }
1258
1259        // DEPYLER-0270: Add Ok(()) for functions with Result<(), E> return type
1260        // When Python function has `-> None` but uses fallible operations (e.g., indexing),
1261        // the Rust return type becomes `Result<(), IndexError>` and needs Ok(()) at the end
1262        // Only add Ok(()) if the function doesn't already end with a return statement
1263        //
1264        // DEPYLER-0450: Extended to handle all Result return types, not just Type::None
1265        // This fixes functions with side effects that use error handling (raise/try/except)
1266        // Also handles Type::Unknown (functions without type annotations that don't explicitly return)
1267        //
1268        // DEPYLER-0455 #6: Check if last statement always returns (including try-except)
1269        // Validator functions with try-except that return in all branches should not get Ok(())
1270        // Use stmt_always_returns() instead of simple Return check to handle exhaustive returns
1271        if can_fail {
1272            let needs_ok = self
1273                .body
1274                .last()
1275                .is_none_or(|stmt| !stmt_always_returns(stmt));
1276            if needs_ok {
1277                // For functions returning unit type (or Unknown which defaults to unit), add Ok(())
1278                // For functions returning values with explicit returns, they already have Ok() wrapping
1279                if matches!(self.ret_type, Type::None | Type::Unknown) {
1280                    body_stmts.push(parse_quote! { Ok(()) });
1281                }
1282            }
1283        }
1284
1285        // Add documentation and custom attributes
1286        let attrs = codegen_function_attrs(
1287            &self.docstring,
1288            &self.properties,
1289            &self.annotations.custom_attributes,
1290        );
1291
1292        // Check if function is a generator (contains yield)
1293        let func_tokens = if self.properties.is_generator {
1294            codegen_generator_function(
1295                self,
1296                &name,
1297                &generic_params,
1298                &where_clause,
1299                &params,
1300                &attrs,
1301                &rust_ret_type,
1302                ctx,
1303            )?
1304        } else if self.properties.is_async {
1305            // DEPYLER-1024: In NASA mode, convert async to sync (no tokio dependency)
1306            let nasa_mode = ctx.type_mapper.nasa_mode;
1307            if nasa_mode {
1308                // NASA mode: Convert async functions to regular sync functions
1309                // This allows single-shot compilation without tokio
1310                quote! {
1311                    #(#attrs)*
1312                    pub fn #name #generic_params(#(#params),*) #return_type #where_clause {
1313                        #(#body_stmts)*
1314                    }
1315                }
1316            } else if self.name == "main" {
1317                // DEPYLER-0748: If this is async main(), add #[tokio::main] attribute
1318                ctx.needs_tokio = true;
1319                quote! {
1320                    #(#attrs)*
1321                    #[tokio::main]
1322                    pub async fn #name #generic_params(#(#params),*) #return_type #where_clause {
1323                        #(#body_stmts)*
1324                    }
1325                }
1326            } else {
1327                quote! {
1328                    #(#attrs)*
1329                    pub async fn #name #generic_params(#(#params),*) #return_type #where_clause {
1330                        #(#body_stmts)*
1331                    }
1332                }
1333            }
1334        } else {
1335            quote! {
1336                #(#attrs)*
1337                pub fn #name #generic_params(#(#params),*) #return_type #where_clause {
1338                    #(#body_stmts)*
1339                }
1340            }
1341        };
1342
1343        Ok(func_tokens)
1344    }
1345}
1346
1347#[cfg(test)]
1348mod tests {
1349    use super::*;
1350    use crate::hir::*;
1351
1352    // === is_file_creating_return_expr tests ===
1353
1354    #[test]
1355    fn test_is_file_creating_open_call() {
1356        let expr = HirExpr::Call {
1357            func: "open".to_string(),
1358            args: vec![HirExpr::Literal(Literal::String("test.txt".to_string()))],
1359            kwargs: vec![],
1360        };
1361        assert!(is_file_creating_return_expr(&expr));
1362    }
1363
1364    #[test]
1365    fn test_is_file_creating_other_call() {
1366        let expr = HirExpr::Call {
1367            func: "read".to_string(),
1368            args: vec![],
1369            kwargs: vec![],
1370        };
1371        assert!(!is_file_creating_return_expr(&expr));
1372    }
1373
1374    #[test]
1375    fn test_is_file_creating_file_create_method() {
1376        let expr = HirExpr::MethodCall {
1377            object: Box::new(HirExpr::Var("File".to_string())),
1378            method: "create".to_string(),
1379            args: vec![HirExpr::Literal(Literal::String("out.txt".to_string()))],
1380            kwargs: vec![],
1381        };
1382        assert!(is_file_creating_return_expr(&expr));
1383    }
1384
1385    #[test]
1386    fn test_is_file_creating_file_open_method() {
1387        let expr = HirExpr::MethodCall {
1388            object: Box::new(HirExpr::Var("File".to_string())),
1389            method: "open".to_string(),
1390            args: vec![HirExpr::Literal(Literal::String("in.txt".to_string()))],
1391            kwargs: vec![],
1392        };
1393        assert!(is_file_creating_return_expr(&expr));
1394    }
1395
1396    #[test]
1397    fn test_is_file_creating_attribute_file() {
1398        let expr = HirExpr::MethodCall {
1399            object: Box::new(HirExpr::Attribute {
1400                value: Box::new(HirExpr::Var("io".to_string())),
1401                attr: "File".to_string(),
1402            }),
1403            method: "create".to_string(),
1404            args: vec![],
1405            kwargs: vec![],
1406        };
1407        assert!(is_file_creating_return_expr(&expr));
1408    }
1409
1410    #[test]
1411    fn test_is_file_creating_non_file_method() {
1412        let expr = HirExpr::MethodCall {
1413            object: Box::new(HirExpr::Var("List".to_string())),
1414            method: "create".to_string(),
1415            args: vec![],
1416            kwargs: vec![],
1417        };
1418        assert!(!is_file_creating_return_expr(&expr));
1419    }
1420
1421    #[test]
1422    fn test_is_file_creating_non_create_method() {
1423        let expr = HirExpr::MethodCall {
1424            object: Box::new(HirExpr::Var("File".to_string())),
1425            method: "read".to_string(),
1426            args: vec![],
1427            kwargs: vec![],
1428        };
1429        assert!(!is_file_creating_return_expr(&expr));
1430    }
1431
1432    #[test]
1433    fn test_is_file_creating_var_expr() {
1434        let expr = HirExpr::Var("f".to_string());
1435        assert!(!is_file_creating_return_expr(&expr));
1436    }
1437
1438    // === is_stdio_return_expr tests ===
1439
1440    #[test]
1441    fn test_is_stdio_stdout() {
1442        let expr = HirExpr::Attribute {
1443            value: Box::new(HirExpr::Var("sys".to_string())),
1444            attr: "stdout".to_string(),
1445        };
1446        assert!(is_stdio_return_expr(&expr));
1447    }
1448
1449    #[test]
1450    fn test_is_stdio_stderr() {
1451        let expr = HirExpr::Attribute {
1452            value: Box::new(HirExpr::Var("sys".to_string())),
1453            attr: "stderr".to_string(),
1454        };
1455        assert!(is_stdio_return_expr(&expr));
1456    }
1457
1458    #[test]
1459    fn test_is_stdio_not_sys() {
1460        let expr = HirExpr::Attribute {
1461            value: Box::new(HirExpr::Var("os".to_string())),
1462            attr: "stdout".to_string(),
1463        };
1464        assert!(!is_stdio_return_expr(&expr));
1465    }
1466
1467    #[test]
1468    fn test_is_stdio_not_stdout() {
1469        let expr = HirExpr::Attribute {
1470            value: Box::new(HirExpr::Var("sys".to_string())),
1471            attr: "path".to_string(),
1472        };
1473        assert!(!is_stdio_return_expr(&expr));
1474    }
1475
1476    #[test]
1477    fn test_is_stdio_plain_var() {
1478        let expr = HirExpr::Var("stdout".to_string());
1479        assert!(!is_stdio_return_expr(&expr));
1480    }
1481
1482    // === collect_io_return_types tests ===
1483
1484    #[test]
1485    fn test_collect_io_return_types_empty() {
1486        let mut has_file = false;
1487        let mut has_stdio = false;
1488        collect_io_return_types(&[], &mut has_file, &mut has_stdio);
1489        assert!(!has_file);
1490        assert!(!has_stdio);
1491    }
1492
1493    #[test]
1494    fn test_collect_io_return_file() {
1495        let stmts = vec![HirStmt::Return(Some(HirExpr::Call {
1496            func: "open".to_string(),
1497            args: vec![HirExpr::Literal(Literal::String("f.txt".to_string()))],
1498            kwargs: vec![],
1499        }))];
1500        let mut has_file = false;
1501        let mut has_stdio = false;
1502        collect_io_return_types(&stmts, &mut has_file, &mut has_stdio);
1503        assert!(has_file);
1504        assert!(!has_stdio);
1505    }
1506
1507    #[test]
1508    fn test_collect_io_return_stdio() {
1509        let stmts = vec![HirStmt::Return(Some(HirExpr::Attribute {
1510            value: Box::new(HirExpr::Var("sys".to_string())),
1511            attr: "stdout".to_string(),
1512        }))];
1513        let mut has_file = false;
1514        let mut has_stdio = false;
1515        collect_io_return_types(&stmts, &mut has_file, &mut has_stdio);
1516        assert!(!has_file);
1517        assert!(has_stdio);
1518    }
1519
1520    #[test]
1521    fn test_collect_io_return_types_in_if_branch() {
1522        let stmts = vec![HirStmt::If {
1523            condition: HirExpr::Var("flag".to_string()),
1524            then_body: vec![HirStmt::Return(Some(HirExpr::Call {
1525                func: "open".to_string(),
1526                args: vec![],
1527                kwargs: vec![],
1528            }))],
1529            else_body: Some(vec![HirStmt::Return(Some(HirExpr::Attribute {
1530                value: Box::new(HirExpr::Var("sys".to_string())),
1531                attr: "stderr".to_string(),
1532            }))]),
1533        }];
1534        let mut has_file = false;
1535        let mut has_stdio = false;
1536        collect_io_return_types(&stmts, &mut has_file, &mut has_stdio);
1537        assert!(has_file);
1538        assert!(has_stdio);
1539    }
1540
1541    #[test]
1542    fn test_collect_io_return_types_in_loop() {
1543        let stmts = vec![HirStmt::While {
1544            condition: HirExpr::Literal(Literal::Bool(true)),
1545            body: vec![HirStmt::Return(Some(HirExpr::Call {
1546                func: "open".to_string(),
1547                args: vec![],
1548                kwargs: vec![],
1549            }))],
1550        }];
1551        let mut has_file = false;
1552        let mut has_stdio = false;
1553        collect_io_return_types(&stmts, &mut has_file, &mut has_stdio);
1554        assert!(has_file);
1555    }
1556
1557    #[test]
1558    fn test_collect_io_return_types_in_for() {
1559        let stmts = vec![HirStmt::For {
1560            target: AssignTarget::Symbol("x".to_string()),
1561            iter: HirExpr::Call {
1562                func: "range".to_string(),
1563                args: vec![HirExpr::Literal(Literal::Int(10))],
1564                kwargs: vec![],
1565            },
1566            body: vec![HirStmt::Return(Some(HirExpr::Attribute {
1567                value: Box::new(HirExpr::Var("sys".to_string())),
1568                attr: "stdout".to_string(),
1569            }))],
1570        }];
1571        let mut has_file = false;
1572        let mut has_stdio = false;
1573        collect_io_return_types(&stmts, &mut has_file, &mut has_stdio);
1574        assert!(has_stdio);
1575    }
1576
1577    #[test]
1578    fn test_collect_io_return_none() {
1579        let stmts = vec![HirStmt::Return(None)];
1580        let mut has_file = false;
1581        let mut has_stdio = false;
1582        collect_io_return_types(&stmts, &mut has_file, &mut has_stdio);
1583        assert!(!has_file);
1584        assert!(!has_stdio);
1585    }
1586
1587    // === function_returns_heterogeneous_io tests ===
1588
1589    #[test]
1590    fn test_heterogeneous_io_both_types() {
1591        let func = HirFunction {
1592            name: "get_output".to_string(),
1593            params: smallvec::smallvec![HirParam::new("use_file".to_string(), Type::Bool)],
1594            ret_type: Type::Unknown,
1595            body: vec![HirStmt::If {
1596                condition: HirExpr::Var("use_file".to_string()),
1597                then_body: vec![HirStmt::Return(Some(HirExpr::Call {
1598                    func: "open".to_string(),
1599                    args: vec![],
1600                    kwargs: vec![],
1601                }))],
1602                else_body: Some(vec![HirStmt::Return(Some(HirExpr::Attribute {
1603                    value: Box::new(HirExpr::Var("sys".to_string())),
1604                    attr: "stdout".to_string(),
1605                }))]),
1606            }],
1607            properties: FunctionProperties::default(),
1608            annotations: depyler_annotations::TranspilationAnnotations::default(),
1609            docstring: None,
1610        };
1611        assert!(function_returns_heterogeneous_io(&func));
1612    }
1613
1614    #[test]
1615    fn test_heterogeneous_io_only_file() {
1616        let func = HirFunction {
1617            name: "get_file".to_string(),
1618            params: smallvec::smallvec![],
1619            ret_type: Type::Unknown,
1620            body: vec![HirStmt::Return(Some(HirExpr::Call {
1621                func: "open".to_string(),
1622                args: vec![],
1623                kwargs: vec![],
1624            }))],
1625            properties: FunctionProperties::default(),
1626            annotations: depyler_annotations::TranspilationAnnotations::default(),
1627            docstring: None,
1628        };
1629        assert!(!function_returns_heterogeneous_io(&func));
1630    }
1631
1632    #[test]
1633    fn test_heterogeneous_io_neither() {
1634        let func = HirFunction {
1635            name: "get_val".to_string(),
1636            params: smallvec::smallvec![],
1637            ret_type: Type::Int,
1638            body: vec![HirStmt::Return(Some(HirExpr::Literal(Literal::Int(42))))],
1639            properties: FunctionProperties::default(),
1640            annotations: depyler_annotations::TranspilationAnnotations::default(),
1641            docstring: None,
1642        };
1643        assert!(!function_returns_heterogeneous_io(&func));
1644    }
1645
1646    // === preload_hir_type_annotations tests ===
1647
1648    #[test]
1649    fn test_preload_simple_assign() {
1650        let mut ctx = CodeGenContext::default();
1651        let body = vec![HirStmt::Assign {
1652            target: AssignTarget::Symbol("x".to_string()),
1653            value: HirExpr::Literal(Literal::Int(42)),
1654            type_annotation: Some(Type::Int),
1655        }];
1656        preload_hir_type_annotations(&body, &mut ctx);
1657        assert_eq!(ctx.var_types.get("x"), Some(&Type::Int));
1658    }
1659
1660    #[test]
1661    fn test_preload_skips_unknown_type() {
1662        let mut ctx = CodeGenContext::default();
1663        let body = vec![HirStmt::Assign {
1664            target: AssignTarget::Symbol("x".to_string()),
1665            value: HirExpr::Literal(Literal::Int(1)),
1666            type_annotation: Some(Type::Unknown),
1667        }];
1668        preload_hir_type_annotations(&body, &mut ctx);
1669        assert!(ctx.var_types.get("x").is_none());
1670    }
1671
1672    #[test]
1673    fn test_preload_no_annotation() {
1674        let mut ctx = CodeGenContext::default();
1675        let body = vec![HirStmt::Assign {
1676            target: AssignTarget::Symbol("x".to_string()),
1677            value: HirExpr::Literal(Literal::Int(1)),
1678            type_annotation: None,
1679        }];
1680        preload_hir_type_annotations(&body, &mut ctx);
1681        assert!(ctx.var_types.get("x").is_none());
1682    }
1683
1684    #[test]
1685    fn test_preload_in_if_branches() {
1686        let mut ctx = CodeGenContext::default();
1687        let body = vec![HirStmt::If {
1688            condition: HirExpr::Literal(Literal::Bool(true)),
1689            then_body: vec![HirStmt::Assign {
1690                target: AssignTarget::Symbol("a".to_string()),
1691                value: HirExpr::Literal(Literal::Int(1)),
1692                type_annotation: Some(Type::Int),
1693            }],
1694            else_body: Some(vec![HirStmt::Assign {
1695                target: AssignTarget::Symbol("b".to_string()),
1696                value: HirExpr::Literal(Literal::String("hi".to_string())),
1697                type_annotation: Some(Type::String),
1698            }]),
1699        }];
1700        preload_hir_type_annotations(&body, &mut ctx);
1701        assert_eq!(ctx.var_types.get("a"), Some(&Type::Int));
1702        assert_eq!(ctx.var_types.get("b"), Some(&Type::String));
1703    }
1704
1705    #[test]
1706    fn test_preload_in_while_loop() {
1707        let mut ctx = CodeGenContext::default();
1708        let body = vec![HirStmt::While {
1709            condition: HirExpr::Literal(Literal::Bool(true)),
1710            body: vec![HirStmt::Assign {
1711                target: AssignTarget::Symbol("counter".to_string()),
1712                value: HirExpr::Literal(Literal::Int(0)),
1713                type_annotation: Some(Type::Int),
1714            }],
1715        }];
1716        preload_hir_type_annotations(&body, &mut ctx);
1717        assert_eq!(ctx.var_types.get("counter"), Some(&Type::Int));
1718    }
1719
1720    #[test]
1721    fn test_preload_in_for_loop() {
1722        let mut ctx = CodeGenContext::default();
1723        let body = vec![HirStmt::For {
1724            target: AssignTarget::Symbol("i".to_string()),
1725            iter: HirExpr::Call {
1726                func: "range".to_string(),
1727                args: vec![HirExpr::Literal(Literal::Int(10))],
1728                kwargs: vec![],
1729            },
1730            body: vec![HirStmt::Assign {
1731                target: AssignTarget::Symbol("total".to_string()),
1732                value: HirExpr::Literal(Literal::Int(0)),
1733                type_annotation: Some(Type::Float),
1734            }],
1735        }];
1736        preload_hir_type_annotations(&body, &mut ctx);
1737        assert_eq!(ctx.var_types.get("total"), Some(&Type::Float));
1738    }
1739
1740    #[test]
1741    fn test_preload_in_try_block() {
1742        let mut ctx = CodeGenContext::default();
1743        let body = vec![HirStmt::Try {
1744            body: vec![HirStmt::Assign {
1745                target: AssignTarget::Symbol("result".to_string()),
1746                value: HirExpr::Literal(Literal::Int(0)),
1747                type_annotation: Some(Type::Int),
1748            }],
1749            handlers: vec![ExceptHandler {
1750                exception_type: Some("ValueError".to_string()),
1751                name: None,
1752                body: vec![HirStmt::Assign {
1753                    target: AssignTarget::Symbol("err_msg".to_string()),
1754                    value: HirExpr::Literal(Literal::String("error".to_string())),
1755                    type_annotation: Some(Type::String),
1756                }],
1757            }],
1758            orelse: Some(vec![HirStmt::Assign {
1759                target: AssignTarget::Symbol("ok".to_string()),
1760                value: HirExpr::Literal(Literal::Bool(true)),
1761                type_annotation: Some(Type::Bool),
1762            }]),
1763            finalbody: Some(vec![HirStmt::Assign {
1764                target: AssignTarget::Symbol("done".to_string()),
1765                value: HirExpr::Literal(Literal::Bool(true)),
1766                type_annotation: Some(Type::Bool),
1767            }]),
1768        }];
1769        preload_hir_type_annotations(&body, &mut ctx);
1770        assert_eq!(ctx.var_types.get("result"), Some(&Type::Int));
1771        assert_eq!(ctx.var_types.get("err_msg"), Some(&Type::String));
1772        assert_eq!(ctx.var_types.get("ok"), Some(&Type::Bool));
1773        assert_eq!(ctx.var_types.get("done"), Some(&Type::Bool));
1774    }
1775
1776    #[test]
1777    fn test_preload_in_with_block() {
1778        let mut ctx = CodeGenContext::default();
1779        let body = vec![HirStmt::With {
1780            context: HirExpr::Call {
1781                func: "open".to_string(),
1782                args: vec![],
1783                kwargs: vec![],
1784            },
1785            target: Some("f".to_string()),
1786            body: vec![HirStmt::Assign {
1787                target: AssignTarget::Symbol("data".to_string()),
1788                value: HirExpr::Literal(Literal::String("".to_string())),
1789                type_annotation: Some(Type::String),
1790            }],
1791            is_async: false,
1792        }];
1793        preload_hir_type_annotations(&body, &mut ctx);
1794        assert_eq!(ctx.var_types.get("data"), Some(&Type::String));
1795    }
1796
1797    #[test]
1798    fn test_preload_in_nested_function() {
1799        let mut ctx = CodeGenContext::default();
1800        let body = vec![HirStmt::FunctionDef {
1801            name: "inner".to_string(),
1802            params: Box::new(smallvec::smallvec![]),
1803            ret_type: Type::None,
1804            body: vec![HirStmt::Assign {
1805                target: AssignTarget::Symbol("local".to_string()),
1806                value: HirExpr::Literal(Literal::Int(0)),
1807                type_annotation: Some(Type::Int),
1808            }],
1809            docstring: None,
1810        }];
1811        preload_hir_type_annotations(&body, &mut ctx);
1812        assert_eq!(ctx.var_types.get("local"), Some(&Type::Int));
1813    }
1814
1815    #[test]
1816    fn test_preload_in_block() {
1817        let mut ctx = CodeGenContext::default();
1818        let body = vec![HirStmt::Block(vec![HirStmt::Assign {
1819            target: AssignTarget::Symbol("x".to_string()),
1820            value: HirExpr::Literal(Literal::Int(1)),
1821            type_annotation: Some(Type::Int),
1822        }])];
1823        preload_hir_type_annotations(&body, &mut ctx);
1824        assert_eq!(ctx.var_types.get("x"), Some(&Type::Int));
1825    }
1826
1827    #[test]
1828    fn test_preload_empty_body() {
1829        let mut ctx = CodeGenContext::default();
1830        preload_hir_type_annotations(&[], &mut ctx);
1831        assert!(ctx.var_types.is_empty());
1832    }
1833
1834    #[test]
1835    fn test_preload_tuple_target_ignored() {
1836        let mut ctx = CodeGenContext::default();
1837        let body = vec![HirStmt::Assign {
1838            target: AssignTarget::Tuple(vec![
1839                AssignTarget::Symbol("a".to_string()),
1840                AssignTarget::Symbol("b".to_string()),
1841            ]),
1842            value: HirExpr::Tuple(vec![
1843                HirExpr::Literal(Literal::Int(1)),
1844                HirExpr::Literal(Literal::Int(2)),
1845            ]),
1846            type_annotation: Some(Type::Int),
1847        }];
1848        preload_hir_type_annotations(&body, &mut ctx);
1849        // Tuple targets don't match Symbol pattern
1850        assert!(ctx.var_types.get("a").is_none());
1851    }
1852
1853    // === Transpile-based integration tests ===
1854
1855    fn transpile(python_code: &str) -> String {
1856        use crate::ast_bridge::AstBridge;
1857        use crate::rust_gen::generate_rust_file;
1858        use crate::type_mapper::TypeMapper;
1859        use rustpython_parser::{parse, Mode};
1860
1861        let ast = parse(python_code, Mode::Module, "<test>").expect("parse");
1862        let (module, _) = AstBridge::new()
1863            .with_source(python_code.to_string())
1864            .python_to_hir(ast)
1865            .expect("hir");
1866        let tm = TypeMapper::default();
1867        let (result, _) = generate_rust_file(&module, &tm).expect("codegen");
1868        result
1869    }
1870
1871    #[test]
1872    fn test_transpile_nested_function_return() {
1873        let code = r#"
1874def make_adder(n: int) -> int:
1875    def adder(x: int) -> int:
1876        return n + x
1877    return adder
1878"#;
1879        let rust = transpile(code);
1880        assert!(rust.contains("fn make_adder"));
1881    }
1882
1883    #[test]
1884    fn test_transpile_function_with_type_annotations() {
1885        let code = r#"
1886def greet(name: str) -> str:
1887    result: str = "Hello, " + name
1888    return result
1889"#;
1890        let rust = transpile(code);
1891        assert!(rust.contains("fn greet"));
1892        assert!(rust.contains("String") || rust.contains("str"));
1893    }
1894
1895    #[test]
1896    fn test_transpile_function_return_type_inferred() {
1897        let code = r#"
1898def double(x: int) -> int:
1899    return x * 2
1900"#;
1901        let rust = transpile(code);
1902        assert!(rust.contains("fn double"));
1903        assert!(rust.contains("i64") || rust.contains("i32"));
1904    }
1905
1906    #[test]
1907    fn test_transpile_async_function() {
1908        let code = r#"
1909async def fetch_data(url: str) -> str:
1910    return url
1911"#;
1912        let rust = transpile(code);
1913        // The transpiler currently generates synchronous functions for async Python defs.
1914        // Verify the function is emitted correctly (async support is not yet implemented).
1915        assert!(rust.contains("fn fetch_data"));
1916        assert!(rust.contains("String") || rust.contains("str"));
1917    }
1918
1919    // === detect_returns_nested_function tests ===
1920
1921    fn make_function(
1922        name: &str,
1923        params: Vec<HirParam>,
1924        ret_type: Type,
1925        body: Vec<HirStmt>,
1926    ) -> HirFunction {
1927        HirFunction {
1928            name: name.to_string(),
1929            params: params.into_iter().collect(),
1930            ret_type,
1931            body,
1932            properties: Default::default(),
1933            annotations: Default::default(),
1934            docstring: None,
1935        }
1936    }
1937
1938    #[test]
1939    fn test_detect_nested_no_nested_functions() {
1940        let mut ctx = CodeGenContext::default();
1941        let func = make_function(
1942            "simple",
1943            vec![],
1944            Type::Int,
1945            vec![HirStmt::Return(Some(HirExpr::Literal(Literal::Int(42))))],
1946        );
1947        let result = detect_returns_nested_function(&func, &mut ctx);
1948        assert!(result.is_none());
1949    }
1950
1951    #[test]
1952    fn test_detect_nested_with_explicit_return() {
1953        let mut ctx = CodeGenContext::default();
1954        let func = make_function(
1955            "make_adder",
1956            vec![HirParam {
1957                name: "n".to_string(),
1958                ty: Type::Int,
1959                default: None,
1960                is_vararg: false,
1961            }],
1962            Type::Unknown,
1963            vec![
1964                HirStmt::FunctionDef {
1965                    name: "adder".to_string(),
1966                    params: Box::new(smallvec::smallvec![HirParam {
1967                        name: "x".to_string(),
1968                        ty: Type::Int,
1969                        default: None,
1970                        is_vararg: false,
1971                    }]),
1972                    ret_type: Type::Int,
1973                    body: vec![HirStmt::Return(Some(HirExpr::Binary {
1974                        left: Box::new(HirExpr::Var("n".to_string())),
1975                        op: BinOp::Add,
1976                        right: Box::new(HirExpr::Var("x".to_string())),
1977                    }))],
1978                    docstring: None,
1979                },
1980                HirStmt::Return(Some(HirExpr::Var("adder".to_string()))),
1981            ],
1982        );
1983        let result = detect_returns_nested_function(&func, &mut ctx);
1984        assert!(result.is_some());
1985        let (name, params, ret_type) = result.unwrap();
1986        assert_eq!(name, "adder");
1987        assert_eq!(params.len(), 1);
1988        assert_eq!(params[0].name, "x");
1989        assert!(matches!(ret_type, Type::Int));
1990    }
1991
1992    #[test]
1993    fn test_detect_nested_with_implicit_return() {
1994        let mut ctx = CodeGenContext::default();
1995        let func = make_function(
1996            "make_fn",
1997            vec![],
1998            Type::Unknown,
1999            vec![
2000                HirStmt::FunctionDef {
2001                    name: "inner".to_string(),
2002                    params: Box::new(smallvec::smallvec![]),
2003                    ret_type: Type::String,
2004                    body: vec![HirStmt::Return(Some(HirExpr::Literal(Literal::String(
2005                        "hello".to_string(),
2006                    ))))],
2007                    docstring: None,
2008                },
2009                HirStmt::Expr(HirExpr::Var("inner".to_string())),
2010            ],
2011        );
2012        let result = detect_returns_nested_function(&func, &mut ctx);
2013        assert!(result.is_some());
2014        let (name, _, ret_type) = result.unwrap();
2015        assert_eq!(name, "inner");
2016        assert!(matches!(ret_type, Type::String));
2017    }
2018
2019    #[test]
2020    fn test_detect_nested_returns_wrong_name() {
2021        let mut ctx = CodeGenContext::default();
2022        let func = make_function(
2023            "make_fn",
2024            vec![],
2025            Type::Unknown,
2026            vec![
2027                HirStmt::FunctionDef {
2028                    name: "inner".to_string(),
2029                    params: Box::new(smallvec::smallvec![]),
2030                    ret_type: Type::Int,
2031                    body: vec![HirStmt::Return(Some(HirExpr::Literal(Literal::Int(1))))],
2032                    docstring: None,
2033                },
2034                HirStmt::Return(Some(HirExpr::Var("other_fn".to_string()))),
2035            ],
2036        );
2037        let result = detect_returns_nested_function(&func, &mut ctx);
2038        assert!(result.is_none());
2039    }
2040
2041    #[test]
2042    fn test_detect_nested_stores_params_in_ctx() {
2043        let mut ctx = CodeGenContext::default();
2044        let func = make_function(
2045            "factory",
2046            vec![],
2047            Type::Unknown,
2048            vec![
2049                HirStmt::FunctionDef {
2050                    name: "product".to_string(),
2051                    params: Box::new(smallvec::smallvec![
2052                        HirParam {
2053                            name: "a".to_string(),
2054                            ty: Type::Int,
2055                            default: None,
2056                            is_vararg: false,
2057                        },
2058                        HirParam {
2059                            name: "b".to_string(),
2060                            ty: Type::Float,
2061                            default: None,
2062                            is_vararg: false,
2063                        },
2064                    ]),
2065                    ret_type: Type::Float,
2066                    body: vec![],
2067                    docstring: None,
2068                },
2069                HirStmt::Return(Some(HirExpr::Var("product".to_string()))),
2070            ],
2071        );
2072        let _ = detect_returns_nested_function(&func, &mut ctx);
2073        assert!(ctx.nested_function_params.contains_key("product"));
2074        let params = ctx.nested_function_params.get("product").unwrap();
2075        assert_eq!(params.len(), 2);
2076    }
2077
2078    #[test]
2079    fn test_detect_nested_unknown_param_inference() {
2080        let mut ctx = CodeGenContext::default();
2081        let func = make_function(
2082            "make_fn",
2083            vec![],
2084            Type::Unknown,
2085            vec![
2086                HirStmt::FunctionDef {
2087                    name: "inner".to_string(),
2088                    params: Box::new(smallvec::smallvec![HirParam {
2089                        name: "x".to_string(),
2090                        ty: Type::Unknown,
2091                        default: None,
2092                        is_vararg: false,
2093                    }]),
2094                    ret_type: Type::Unknown,
2095                    body: vec![HirStmt::Return(Some(HirExpr::Binary {
2096                        left: Box::new(HirExpr::Var("x".to_string())),
2097                        op: BinOp::Add,
2098                        right: Box::new(HirExpr::Literal(Literal::Int(1))),
2099                    }))],
2100                    docstring: None,
2101                },
2102                HirStmt::Return(Some(HirExpr::Var("inner".to_string()))),
2103            ],
2104        );
2105        let result = detect_returns_nested_function(&func, &mut ctx);
2106        assert!(result.is_some());
2107    }
2108
2109    #[test]
2110    fn test_detect_nested_optional_unknown_param() {
2111        let mut ctx = CodeGenContext::default();
2112        let func = make_function(
2113            "make_fn",
2114            vec![],
2115            Type::Unknown,
2116            vec![
2117                HirStmt::FunctionDef {
2118                    name: "inner".to_string(),
2119                    params: Box::new(smallvec::smallvec![HirParam {
2120                        name: "val".to_string(),
2121                        ty: Type::Optional(Box::new(Type::Unknown)),
2122                        default: None,
2123                        is_vararg: false,
2124                    }]),
2125                    ret_type: Type::Int,
2126                    body: vec![HirStmt::Return(Some(HirExpr::Literal(Literal::Int(0))))],
2127                    docstring: None,
2128                },
2129                HirStmt::Return(Some(HirExpr::Var("inner".to_string()))),
2130            ],
2131        );
2132        let result = detect_returns_nested_function(&func, &mut ctx);
2133        assert!(result.is_some());
2134    }
2135
2136    #[test]
2137    fn test_detect_nested_empty_body() {
2138        let mut ctx = CodeGenContext::default();
2139        let func = make_function("empty", vec![], Type::Unknown, vec![]);
2140        let result = detect_returns_nested_function(&func, &mut ctx);
2141        assert!(result.is_none());
2142    }
2143
2144    #[test]
2145    fn test_detect_nested_non_var_return() {
2146        let mut ctx = CodeGenContext::default();
2147        let func = make_function(
2148            "make_fn",
2149            vec![],
2150            Type::Unknown,
2151            vec![
2152                HirStmt::FunctionDef {
2153                    name: "inner".to_string(),
2154                    params: Box::new(smallvec::smallvec![]),
2155                    ret_type: Type::Int,
2156                    body: vec![],
2157                    docstring: None,
2158                },
2159                HirStmt::Return(Some(HirExpr::Literal(Literal::Int(42)))),
2160            ],
2161        );
2162        let result = detect_returns_nested_function(&func, &mut ctx);
2163        assert!(result.is_none()); // Returns literal, not nested fn name
2164    }
2165
2166    // === Transpile-based tests for nested function patterns ===
2167
2168    #[test]
2169    fn test_transpile_closure_returning_function() {
2170        let code = r#"
2171def make_multiplier(factor: int):
2172    def multiply(x: int) -> int:
2173        return x * factor
2174    return multiply
2175"#;
2176        let rust = transpile(code);
2177        assert!(rust.contains("fn make_multiplier"));
2178    }
2179
2180    #[test]
2181    fn test_transpile_function_with_default_params() {
2182        let code = r#"
2183def greet(name: str, greeting: str = "Hello") -> str:
2184    return greeting + " " + name
2185"#;
2186        let rust = transpile(code);
2187        assert!(rust.contains("fn greet"));
2188    }
2189
2190    #[test]
2191    fn test_transpile_function_returning_bool() {
2192        let code = r#"
2193def is_even(n: int) -> bool:
2194    return n % 2 == 0
2195"#;
2196        let rust = transpile(code);
2197        assert!(rust.contains("fn is_even"));
2198        assert!(rust.contains("bool"));
2199    }
2200
2201    #[test]
2202    fn test_transpile_function_returning_list() {
2203        let code = r#"
2204def make_list(n: int) -> list:
2205    result = []
2206    for i in range(n):
2207        result.append(i)
2208    return result
2209"#;
2210        let rust = transpile(code);
2211        assert!(rust.contains("fn make_list"));
2212        assert!(rust.contains("Vec"));
2213    }
2214
2215    #[test]
2216    fn test_transpile_function_returning_optional() {
2217        let code = r#"
2218def find_item(items: list, target: int):
2219    for item in items:
2220        if item == target:
2221            return item
2222    return None
2223"#;
2224        let rust = transpile(code);
2225        assert!(rust.contains("fn find_item"));
2226        assert!(rust.contains("Option") || rust.contains("None"));
2227    }
2228
2229    #[test]
2230    fn test_transpile_function_with_multiple_returns() {
2231        let code = r#"
2232def classify(x: int) -> str:
2233    if x > 0:
2234        return "positive"
2235    elif x < 0:
2236        return "negative"
2237    else:
2238        return "zero"
2239"#;
2240        let rust = transpile(code);
2241        assert!(rust.contains("fn classify"));
2242        assert!(rust.contains("positive"));
2243        assert!(rust.contains("negative"));
2244        assert!(rust.contains("zero"));
2245    }
2246
2247    // === Session 9 Batch 6: Targeted coverage ===
2248
2249    #[test]
2250    fn test_s9b6_infer_dict_return() {
2251        let code = r#"
2252def make_dict() -> dict:
2253    d = {}
2254    d["a"] = 1
2255    d["b"] = 2
2256    return d
2257"#;
2258        let rust = transpile(code);
2259        assert!(rust.contains("fn make_dict"), "output: {}", rust);
2260    }
2261
2262    #[test]
2263    fn test_s9b6_infer_tuple_return() {
2264        let code = r#"
2265def pair(a: int, b: int) -> tuple:
2266    return a, b
2267"#;
2268        let rust = transpile(code);
2269        assert!(rust.contains("fn pair"), "output: {}", rust);
2270    }
2271
2272    #[test]
2273    fn test_s9b6_infer_set_return() {
2274        let code = r#"
2275def unique_chars(s: str) -> set:
2276    result = set()
2277    for c in s:
2278        result.add(c)
2279    return result
2280"#;
2281        let rust = transpile(code);
2282        assert!(rust.contains("fn unique_chars"), "output: {}", rust);
2283    }
2284
2285    #[test]
2286    fn test_s9b6_infer_from_binary_op() {
2287        let code = r#"
2288def compute(a: int, b: int):
2289    return a * b + a - b
2290"#;
2291        let rust = transpile(code);
2292        assert!(rust.contains("fn compute"), "output: {}", rust);
2293    }
2294
2295    #[test]
2296    fn test_s9b6_infer_from_comparison() {
2297        let code = r#"
2298def check(x: int, y: int):
2299    return x > y
2300"#;
2301        let rust = transpile(code);
2302        assert!(rust.contains("fn check"), "output: {}", rust);
2303        assert!(rust.contains("bool"), "should infer bool: {}", rust);
2304    }
2305
2306    #[test]
2307    fn test_s9b6_infer_from_method_call() {
2308        let code = r#"
2309def upper(s: str):
2310    return s.upper()
2311"#;
2312        let rust = transpile(code);
2313        assert!(rust.contains("fn upper"), "output: {}", rust);
2314    }
2315
2316    #[test]
2317    fn test_s9b6_infer_from_len() {
2318        let code = r#"
2319def count(items: list):
2320    return len(items)
2321"#;
2322        let rust = transpile(code);
2323        assert!(rust.contains("fn count"), "output: {}", rust);
2324    }
2325
2326    #[test]
2327    fn test_s9b6_infer_from_sum() {
2328        let code = r#"
2329def total(nums: list):
2330    return sum(nums)
2331"#;
2332        let rust = transpile(code);
2333        assert!(rust.contains("fn total"), "output: {}", rust);
2334    }
2335
2336    #[test]
2337    fn test_s9b6_infer_from_min_max() {
2338        let code = r#"
2339def largest(nums: list):
2340    return max(nums)
2341"#;
2342        let rust = transpile(code);
2343        assert!(rust.contains("fn largest"), "output: {}", rust);
2344    }
2345
2346    #[test]
2347    fn test_s9b6_infer_from_str_join() {
2348        let code = r#"
2349def combine(parts: list):
2350    return ",".join(parts)
2351"#;
2352        let rust = transpile(code);
2353        assert!(rust.contains("fn combine"), "output: {}", rust);
2354    }
2355
2356    #[test]
2357    fn test_s9b6_infer_from_str_split() {
2358        let code = r#"
2359def split_csv(line: str):
2360    return line.split(",")
2361"#;
2362        let rust = transpile(code);
2363        assert!(rust.contains("fn split_csv"), "output: {}", rust);
2364    }
2365
2366    #[test]
2367    fn test_s9b6_infer_void_function() {
2368        let code = r#"
2369def log(msg: str):
2370    print(msg)
2371"#;
2372        let rust = transpile(code);
2373        assert!(rust.contains("fn log"), "output: {}", rust);
2374    }
2375
2376    #[test]
2377    fn test_s9b6_infer_from_list_comprehension() {
2378        let code = r#"
2379def doubles(nums: list):
2380    return [x * 2 for x in nums]
2381"#;
2382        let rust = transpile(code);
2383        assert!(rust.contains("fn doubles"), "output: {}", rust);
2384    }
2385
2386    #[test]
2387    fn test_s9b6_infer_nested_return() {
2388        let code = r#"
2389def safe_div(a: int, b: int):
2390    if b == 0:
2391        return None
2392    return a // b
2393"#;
2394        let rust = transpile(code);
2395        assert!(rust.contains("fn safe_div"), "output: {}", rust);
2396    }
2397
2398    #[test]
2399    fn test_s9b6_infer_from_isinstance() {
2400        let code = r#"
2401def is_str(x) -> bool:
2402    return isinstance(x, str)
2403"#;
2404        let rust = transpile(code);
2405        assert!(rust.contains("fn is_str"), "output: {}", rust);
2406    }
2407
2408    #[test]
2409    fn test_s9b6_infer_multiple_exit_paths() {
2410        let code = r#"
2411def analyze(x: int):
2412    if x > 0:
2413        return "positive"
2414    return "non-positive"
2415"#;
2416        let rust = transpile(code);
2417        assert!(rust.contains("fn analyze"), "output: {}", rust);
2418    }
2419}