Skip to main content

depyler_core/rust_gen/
stmt_gen_complex.rs

1//! Complex statement codegen: try/except, subcommand handling, nested functions
2//!
3//! DEPYLER-COVERAGE-95: Extracted from stmt_gen.rs to reduce file size
4//! and improve testability. Contains try/except and complex control flow.
5
6#[cfg(feature = "decision-tracing")]
7use crate::decision_trace::DecisionCategory;
8use crate::hir::*;
9use crate::rust_gen::context::{CodeGenContext, RustCodeGen, ToRustExpr};
10use crate::rust_gen::control_stmt_helpers::{
11    codegen_break_stmt, codegen_continue_stmt, codegen_pass_stmt,
12};
13use crate::rust_gen::expr_analysis::{
14    contains_floor_div, extract_divisor_from_floor_div, handler_contains_raise,
15    is_nested_function_recursive, to_pascal_case,
16};
17use crate::rust_gen::func_gen::propagate_return_type_to_vars;
18use crate::rust_gen::keywords::safe_ident;
19use crate::rust_gen::rust_type_to_syn;
20use crate::rust_gen::stmt_gen::{
21    codegen_assert_stmt, codegen_assign_stmt, codegen_expr_stmt, codegen_for_stmt, codegen_if_stmt,
22    codegen_raise_stmt, codegen_return_stmt, codegen_while_stmt, codegen_with_stmt,
23    find_variable_type, infer_try_body_return_type, try_generate_json_stdin_match,
24    try_return_type_to_tokens,
25};
26use crate::rust_gen::type_tokens::hir_type_to_tokens;
27use crate::rust_gen::var_analysis::extract_toplevel_assigned_symbols;
28use crate::trace_decision;
29use anyhow::Result;
30use quote::quote;
31
32/// Generate code for Try/except/finally statement
33#[inline]
34pub(crate) fn codegen_try_stmt(
35    body: &[HirStmt],
36    handlers: &[ExceptHandler],
37    finalbody: &Option<Vec<HirStmt>>,
38    ctx: &mut CodeGenContext,
39) -> Result<proc_macro2::TokenStream> {
40    // CITL: Trace error handling strategy
41    trace_decision!(
42        category = DecisionCategory::ErrorHandling,
43        name = "try_except",
44        chosen = "match_result",
45        alternatives = [
46            "unwrap_or",
47            "question_mark",
48            "anyhow_context",
49            "custom_error"
50        ],
51        confidence = 0.80
52    );
53
54    // DEPYLER-0681: Variable hoisting for try/except blocks
55    // Variables assigned in try/except blocks need to be accessible after the block.
56    // In Python, variables defined in try/except escape their scope. In Rust, they don't.
57    // We hoist variable declarations before the try block to fix this.
58    let try_vars = extract_toplevel_assigned_symbols(body);
59    let handler_vars: std::collections::HashSet<String> = handlers
60        .iter()
61        .flat_map(|h| extract_toplevel_assigned_symbols(&h.body))
62        .collect();
63
64    // Variables assigned in try body that might be assigned in handlers too (common pattern)
65    // or any variable assigned in try that's not just a loop variable
66    let hoisted_try_vars: Vec<String> = try_vars
67        .union(&handler_vars)
68        .filter(|v| !ctx.is_declared(v))
69        .cloned()
70        .collect();
71
72    // Generate hoisted variable declarations
73    let mut hoisted_decls = Vec::new();
74    for var_name in &hoisted_try_vars {
75        let var_ident = safe_ident(var_name);
76
77        // Find the variable's type from the first assignment in either try block or handlers
78        let var_type = find_variable_type(var_name, body).or_else(|| {
79            handlers
80                .iter()
81                .find_map(|h| find_variable_type(var_name, &h.body))
82        });
83
84        if let Some(ty) = var_type {
85            // DEPYLER-0931: Check if type implements Default
86            // Types like std::process::Child don't implement Default, so wrap in Option
87            let needs_option_wrap = matches!(
88                &ty,
89                Type::Custom(s) if s == "std::process::Child" || s == "Child"
90            );
91
92            if needs_option_wrap {
93                // Wrap non-Default types in Option
94                let opt_type = Type::Optional(Box::new(ty.clone()));
95                let rust_type = ctx.type_mapper.map_type(&opt_type);
96                let syn_type = rust_type_to_syn(&rust_type)?;
97                hoisted_decls.push(quote! { let mut #var_ident: #syn_type = None; });
98                ctx.var_types.insert(var_name.clone(), opt_type);
99            } else {
100                let rust_type = ctx.type_mapper.map_type(&ty);
101                let syn_type = rust_type_to_syn(&rust_type)?;
102                // DEPYLER-0931: Initialize with Default::default() to prevent E0381
103                // Variables hoisted from try/except must be initialized because the try block might fail
104                // before assignment, and we need a valid state for the except block (or after).
105                // This also allows closure capturing by reference.
106                hoisted_decls.push(quote! { let mut #var_ident: #syn_type = Default::default(); });
107                ctx.var_types.insert(var_name.clone(), ty);
108            }
109        } else {
110            // No type annotation - DEPYLER-0931: Default to Option<serde_json::Value>
111            // This ensures we have a safe container for whatever value is assigned
112            // and handles the "uninitialized" state via None.
113            let value_type = crate::hir::Type::Custom("serde_json::Value".to_string());
114            let opt_type = crate::hir::Type::Optional(Box::new(value_type));
115
116            ctx.var_types.insert(var_name.clone(), opt_type);
117            hoisted_decls.push(quote! { let mut #var_ident: Option<serde_json::Value> = None; });
118
119            // DEPYLER-0455 #2: Track hoisted inference vars
120            ctx.hoisted_inference_vars.insert(var_name.clone());
121        }
122
123        // Declare variable in outer scope so it's accessible after try block
124        ctx.declare_var(var_name);
125    }
126
127    // DEPYLER-0578: Detect json.load(sys.stdin) pattern with exit handler
128    // Pattern: try { data = json.load(sys.stdin) } except JSONDecodeError as e: { print; exit }
129    // This pattern assigns a variable that must be accessible AFTER the try/except block
130    // Generate: let data = match serde_json::from_reader(...) { Ok(d) => d, Err(e) => { handler } };
131    if let Some(result) = try_generate_json_stdin_match(body, handlers, finalbody, ctx)? {
132        return Ok(result);
133    }
134
135    // DEPYLER-0358: Detect simple try-except pattern for optimization
136    // Pattern: try { return int(str_var) } except ValueError { return literal }
137    // We can optimize this to: s.parse::<i32>().unwrap_or(literal)
138    // DEPYLER-0359: Exclude patterns with exception binding (except E as e:)
139    // Those need proper match with Err(e) binding
140    let simple_pattern_info = if body.len() == 1
141        && handlers.len() == 1
142        && handlers[0].body.len() == 1
143        && handlers[0].name.is_none()
144    // No exception variable binding
145    {
146        // Check if handler body is a Return statement with a simple value
147        match &handlers[0].body[0] {
148            // Direct literal: return 42, return "error", etc.
149            HirStmt::Return(Some(HirExpr::Literal(lit))) => Some((
150                (match lit {
151                    Literal::Int(n) => n.to_string(),
152                    Literal::Float(f) => f.to_string(),
153                    Literal::String(s) => format!("\"{}\"", s),
154                    Literal::Bool(b) => b.to_string(),
155                    _ => "Default::default()".to_string(),
156                })
157                .to_string(),
158                handlers[0].exception_type.clone(),
159            )),
160            // Unary negation: return -1, return -42, etc.
161            HirStmt::Return(Some(HirExpr::Unary { op, operand })) => {
162                if let HirExpr::Literal(lit) = &**operand {
163                    match (op, lit) {
164                        (crate::hir::UnaryOp::Neg, Literal::Int(n)) => {
165                            Some((format!("-{}", n), handlers[0].exception_type.clone()))
166                        }
167                        (crate::hir::UnaryOp::Neg, Literal::Float(f)) => {
168                            Some((format!("-{}", f), handlers[0].exception_type.clone()))
169                        }
170                        _ => None,
171                    }
172                } else {
173                    None
174                }
175            }
176            _ => None,
177        }
178    } else {
179        None
180    };
181
182    // DEPYLER-0333: Extract handled exception types for scope tracking
183    let handled_types: Vec<String> = handlers
184        .iter()
185        .filter_map(|h| h.exception_type.clone())
186        .collect();
187
188    // DEPYLER-0333: Enter try block scope with handled exception types
189    // Empty list means bare except (catches all exceptions)
190    ctx.enter_try_scope(handled_types.clone());
191
192    // DEPYLER-0360: Check for floor division with ZeroDivisionError handler BEFORE generating try_stmts
193    let has_zero_div_handler = handlers
194        .iter()
195        .any(|h| h.exception_type.as_deref() == Some("ZeroDivisionError"));
196
197    if has_zero_div_handler && body.len() == 1 {
198        if let HirStmt::Return(Some(expr)) = &body[0] {
199            if contains_floor_div(expr) {
200                // Extract divisor from floor division
201                let divisor_expr = extract_divisor_from_floor_div(expr)?;
202                let divisor_tokens = divisor_expr.to_rust_expr(ctx)?;
203
204                // Find ZeroDivisionError handler
205                let zero_div_handler_idx = handlers
206                    .iter()
207                    .position(|h| h.exception_type.as_deref() == Some("ZeroDivisionError"))
208                    .unwrap();
209
210                // Generate handler body
211                ctx.enter_scope();
212                // DEPYLER-0360: Ensure return keyword is included in handler
213                let old_is_final = ctx.is_final_statement;
214                ctx.is_final_statement = false;
215                let handler_stmts: Vec<_> = handlers[zero_div_handler_idx]
216                    .body
217                    .iter()
218                    .map(|s| s.to_rust_tokens(ctx))
219                    .collect::<Result<Vec<_>>>()?;
220                ctx.is_final_statement = old_is_final;
221                ctx.exit_scope();
222
223                // Generate try block expression (with params shadowing)
224                let floor_div_result = expr.to_rust_expr(ctx)?;
225
226                // DEPYLER-0333: Exit try block scope
227                ctx.exit_exception_scope();
228
229                // Generate: if divisor == 0 { handler } else { floor_div_result }
230                if let Some(finalbody) = finalbody {
231                    ctx.enter_scope();
232                    let finally_stmts: Vec<_> = finalbody
233                        .iter()
234                        .map(|s| s.to_rust_tokens(ctx))
235                        .collect::<Result<Vec<_>>>()?;
236                    ctx.exit_scope();
237
238                    // DEPYLER-1161: Wrap floor div result in Ok() when function returns Result
239                    return if ctx.current_function_can_fail {
240                        Ok(quote! {
241                            {
242                                if #divisor_tokens == 0 {
243                                    #(#handler_stmts)*
244                                } else {
245                                    return Ok(#floor_div_result);
246                                }
247                                #(#finally_stmts)*
248                            }
249                        })
250                    } else {
251                        Ok(quote! {
252                            {
253                                if #divisor_tokens == 0 {
254                                    #(#handler_stmts)*
255                                } else {
256                                    return #floor_div_result;
257                                }
258                                #(#finally_stmts)*
259                            }
260                        })
261                    };
262                } else {
263                    // DEPYLER-1161: Wrap floor div result in Ok() when function returns Result
264                    return if ctx.current_function_can_fail {
265                        Ok(quote! {
266                            if #divisor_tokens == 0 {
267                                #(#handler_stmts)*
268                            } else {
269                                return Ok(#floor_div_result);
270                            }
271                        })
272                    } else {
273                        Ok(quote! {
274                            if #divisor_tokens == 0 {
275                                #(#handler_stmts)*
276                            } else {
277                                return #floor_div_result;
278                            }
279                        })
280                    };
281                }
282            }
283        }
284    }
285
286    // Convert try body to statements
287    // DEPYLER-0395: Try block statements should include 'return' keyword
288    // Save and temporarily disable is_final_statement so return statements
289    // in try blocks get the explicit 'return' keyword (needed for proper exception handling)
290    let saved_is_final = ctx.is_final_statement;
291    ctx.is_final_statement = false;
292
293    ctx.enter_scope();
294    let try_stmts: Vec<_> = body
295        .iter()
296        .map(|s| s.to_rust_tokens(ctx))
297        .collect::<Result<Vec<_>>>()?;
298    ctx.exit_scope();
299
300    // Restore is_final_statement flag
301    ctx.is_final_statement = saved_is_final;
302
303    // DEPYLER-0333: Exit try block scope
304    ctx.exit_exception_scope();
305
306    // Generate except handler code
307    let mut handler_tokens = Vec::new();
308    for handler in handlers {
309        // DEPYLER-0333: Enter handler scope for each except clause
310        ctx.enter_handler_scope();
311        ctx.enter_scope();
312
313        // If there's a name binding, declare it in scope
314        if let Some(var_name) = &handler.name {
315            ctx.declare_var(var_name);
316        }
317
318        // DEPYLER-0357: Handler statements should include 'return' keyword
319        // Save and temporarily disable is_final_statement so return statements
320        // in handlers get the explicit 'return' keyword (needed for proper exception handling)
321        let saved_is_final = ctx.is_final_statement;
322        ctx.is_final_statement = false;
323
324        let handler_stmts: Vec<_> = handler
325            .body
326            .iter()
327            .map(|s| s.to_rust_tokens(ctx))
328            .collect::<Result<Vec<_>>>()?;
329
330        // Restore is_final_statement flag
331        ctx.is_final_statement = saved_is_final;
332        ctx.exit_scope();
333        // DEPYLER-0333: Exit handler scope
334        ctx.exit_exception_scope();
335
336        // DEPYLER-0931: Transform handler returns to wrap in Ok() when inside nested exception scope
337        // Handler code returns from the outer try/except closure which expects Result<T, E>
338        let is_nested = ctx.exception_nesting_depth() > 0;
339        let handler_stmts_transformed: Vec<_> = if is_nested {
340            handler_stmts
341                .iter()
342                .map(|stmt| {
343                    let stmt_str = stmt.to_string();
344                    if stmt_str.starts_with("return ") && !stmt_str.starts_with("return Ok (") {
345                        if let Some(expr_part) = stmt_str.strip_prefix("return ") {
346                            if let Some(expr) = expr_part.strip_suffix(" ;") {
347                                let wrapped = format!("return Ok({}) ;", expr);
348                                return wrapped.parse().unwrap_or_else(|_| stmt.clone());
349                            }
350                        }
351                    }
352                    stmt.clone()
353                })
354                .collect()
355        } else {
356            handler_stmts
357        };
358
359        handler_tokens.push(quote! { #(#handler_stmts_transformed)* });
360    }
361
362    // Generate finally clause if present
363    let finally_stmts = if let Some(finally_body) = finalbody {
364        let stmts: Vec<_> = finally_body
365            .iter()
366            .map(|s| s.to_rust_tokens(ctx))
367            .collect::<Result<Vec<_>>>()?;
368        Some(quote! { #(#stmts)* })
369    } else {
370        None
371    };
372
373    // Generate try/except/finally pattern
374    if handlers.is_empty() {
375        // Try/finally without except
376        if let Some(finally_code) = finally_stmts {
377            Ok(quote! {
378                #(#hoisted_decls)*
379                {
380                    #(#try_stmts)*
381                    #finally_code
382                }
383            })
384        } else {
385            // DEPYLER-0681: Include hoisted declarations for try block variables
386            Ok(quote! {
387                #(#hoisted_decls)*
388                #(#try_stmts)*
389            })
390        }
391    } else {
392        // DEPYLER-0437/0429: Generate proper match expressions for parse() patterns
393        // Check if try_stmts contains a .parse() call that we can convert to match
394        if handlers.len() == 1 {
395            if let Some((var_name, parse_expr_str, remaining_stmts)) =
396                extract_parse_from_tokens(&try_stmts)
397            {
398                // Parse the expression string back to token stream
399                let parse_expr: proc_macro2::TokenStream = match parse_expr_str.parse() {
400                    Ok(ts) => ts,
401                    Err(_) => return Ok(quote! { #(#try_stmts)* }), // Fallback on parse error
402                };
403                let ok_var = safe_ident(&var_name);
404
405                // Generate Ok branch (remaining statements after parse)
406                let ok_body = quote! { #(#remaining_stmts)* };
407
408                // Generate Err branch (handler body)
409                let err_body = &handler_tokens[0];
410
411                // DEPYLER-0429: Check if exception variable should be bound
412                let err_pattern = if let Some(exc_var) = &handlers[0].name {
413                    // Bind exception variable: Err(e) => { ... }
414                    let exc_ident = safe_ident(exc_var);
415                    quote! { Err(#exc_ident) }
416                } else {
417                    // No exception variable: Err(_) => { ... }
418                    quote! { Err(_) }
419                };
420
421                // Build match expression
422                let match_expr = quote! {
423                    match #parse_expr {
424                        Ok(#ok_var) => { #ok_body },
425                        #err_pattern => { #err_body }
426                    }
427                };
428
429                // Wrap with finally if present
430                if let Some(finally_code) = finally_stmts {
431                    return Ok(quote! {
432                        {
433                            #match_expr
434                            #finally_code
435                        }
436                    });
437                } else {
438                    return Ok(match_expr);
439                }
440            }
441        }
442
443        // Fall through to existing simple_pattern_info logic
444        if let Some((exception_value_str, _exception_type)) = simple_pattern_info {
445            // Fall through to existing unwrap_or logic if not a match pattern
446            // Convert try_stmts to string to post-process
447            let try_code = quote! { #(#try_stmts)* };
448            let try_str = try_code.to_string();
449
450            // DEPYLER-0358: Replace unwrap_or_default() with unwrap_or(exception_value)
451            // This handles the case where int(str) generates .parse().unwrap_or_default()
452            // but we want .parse().unwrap_or(-1) based on the except clause
453            if try_str.contains("unwrap_or_default") {
454                // Parse the try code and replace unwrap_or_default with unwrap_or(value)
455                // Handle both "unwrap_or_default ()" and "unwrap_or_default()"
456                let fixed_code = try_str
457                    .replace(
458                        "unwrap_or_default ()",
459                        &format!("unwrap_or ({})", exception_value_str),
460                    )
461                    .replace(
462                        "unwrap_or_default()",
463                        &format!("unwrap_or({})", exception_value_str),
464                    );
465
466                // Parse back to token stream
467                let fixed_tokens: proc_macro2::TokenStream = fixed_code.parse().unwrap_or(try_code);
468
469                // DEPYLER-0437: Include hoisted variable declarations
470                if let Some(finally_code) = finally_stmts {
471                    Ok(quote! {
472                        {
473                            #(#hoisted_decls)*
474                            #fixed_tokens
475                            #finally_code
476                        }
477                    })
478                } else if hoisted_decls.is_empty() {
479                    Ok(fixed_tokens)
480                } else {
481                    Ok(quote! {
482                        #(#hoisted_decls)*
483                        #fixed_tokens
484                    })
485                }
486            } else {
487                // Pattern matched but no unwrap_or_default found
488                // This means it's not a parse operation, so fall through to normal concatenation
489                // to include the exception handler code
490                // DEPYLER-0437: Include hoisted variable declarations
491                let handler_code = &handler_tokens[0];
492                if let Some(finally_code) = finally_stmts {
493                    Ok(quote! {
494                        {
495                            #(#hoisted_decls)*
496                            #(#try_stmts)*
497                            #handler_code
498                            #finally_code
499                        }
500                    })
501                } else {
502                    Ok(quote! {
503                        {
504                            #(#hoisted_decls)*
505                            #(#try_stmts)*
506                            #handler_code
507                        }
508                    })
509                }
510            }
511        } else {
512            // DEPYLER-0931: Always use closure pattern for robust control flow & scoping
513            // This guarantees that:
514            // 1. Variables are hoisted and accessible after the block (declared outside)
515            // 2. Control flow (return/raise) inside try block correctly jumps to handler or exits
516            // 3. Variables assigned in try block are correctly captured by mutable reference
517
518            // Infer return type from try body
519            let try_return_type = infer_try_body_return_type(body, ctx);
520            let return_type_tokens = try_return_type
521                .as_ref()
522                .map(try_return_type_to_tokens)
523                .unwrap_or_else(|| quote! { () });
524            let ok_value = try_return_type
525                .as_ref()
526                .map(|_| quote! { _result })
527                .unwrap_or_else(|| quote! { () });
528
529            // The Ok arm extracts _result from Result
530            // DEPYLER-0819: When handlers contain raise, the function returns Result<T, E>
531            // and we must wrap the success value in Ok()
532            // DEPYLER-0931: Always use Ok(_result) when returning from try/except closure
533            // because we're inside a Result-returning closure (even for nested try/except)
534            let any_handler_raises = handlers.iter().any(|h| handler_contains_raise(&h.body));
535            let ok_arm_body = if try_return_type.is_some() {
536                // Always wrap in Ok() - we're returning from a Result<T, E> closure
537                // If any_handler_raises OR outer function returns Result, we must wrap in Ok()
538                if any_handler_raises
539                    || ctx.exception_nesting_depth() > 0
540                    || ctx.current_function_can_fail
541                {
542                    quote! { return Ok(_result); }
543                } else {
544                    quote! { return _result; }
545                }
546            } else {
547                quote! {}
548            };
549
550            // Transform try body return statements to wrap values in Ok()
551            // The closure returns Result<T, E>, so `return expr;` must become `return Ok(expr);`
552            let try_stmts_transformed: Vec<_> = try_stmts
553                .iter()
554                .map(|stmt| {
555                    let stmt_str = stmt.to_string();
556                    // Transform `return expr ;` to `return Ok ( expr ) ;`
557                    // Simple text-based transformation for now (robust enough for generated code)
558                    if stmt_str.starts_with("return ") && !stmt_str.starts_with("return Ok (") {
559                        if let Some(expr_part) = stmt_str.strip_prefix("return ") {
560                            if let Some(expr) = expr_part.strip_suffix(" ;") {
561                                let wrapped = format!("return Ok({}) ;", expr);
562                                return wrapped.parse().unwrap_or_else(|_| stmt.clone());
563                            }
564                        }
565                    }
566                    stmt.clone()
567                })
568                .collect();
569
570            // Check if try body always returns (to avoid unreachable code warning)
571            let always_returns = body.iter().any(|s| matches!(s, HirStmt::Return(_)));
572
573            // Only add fallback Ok(Default::default()) when try body has no return
574            // If try body has returns, they're already wrapped in Ok() and there's no need for fallback
575            let closure_fallback = if always_returns {
576                quote! {}
577            } else if try_return_type.is_none() {
578                quote! { Ok(()) } // Return unit for fallthrough
579            } else {
580                // If try body returns a value, we need a fallback for fallthrough path
581                // (e.g., if try block finishes without returning)
582                // Use Default::default() for the return type
583                quote! { Ok(Default::default()) }
584            };
585
586            // Generate handler matching logic
587            let match_expr = if handlers.len() == 1 {
588                // Single handler - use match pattern
589                let err_pattern = if let Some(exc_var) = &handlers[0].name {
590                    let exc_ident = safe_ident(exc_var);
591                    quote! { Err(#exc_ident) }
592                } else {
593                    quote! { Err(_) }
594                };
595
596                let handler_code = &handler_tokens[0];
597
598                quote! {
599                    match (|| -> Result<#return_type_tokens, Box<dyn std::error::Error>> {
600                        #(#try_stmts_transformed)*
601                        #closure_fallback
602                    })() {
603                        Ok(#ok_value) => { #ok_arm_body },
604                        #err_pattern => { #handler_code }
605                    }
606                }
607            } else {
608                // Multiple handlers - find one with binding or fallback to catch-all
609                // Note: Implement proper type-based dispatch for multiple handlers
610                let exc_var_opt = handlers.iter().find_map(|h| h.name.as_ref());
611                let handler_code = if let Some(idx) = handlers.iter().position(|h| h.name.is_some())
612                {
613                    &handler_tokens[idx]
614                } else {
615                    &handler_tokens[0]
616                };
617
618                if let Some(exc_var) = exc_var_opt {
619                    let exc_ident = safe_ident(exc_var);
620                    quote! {
621                        match (|| -> Result<#return_type_tokens, Box<dyn std::error::Error>> {
622                            #(#try_stmts_transformed)*
623                            #closure_fallback
624                        })() {
625                            Ok(#ok_value) => { #ok_arm_body },
626                            Err(#exc_ident) => { #handler_code }
627                        }
628                    }
629                } else {
630                    quote! {
631                        match (|| -> Result<#return_type_tokens, Box<dyn std::error::Error>> {
632                            #(#try_stmts_transformed)*
633                            #closure_fallback
634                        })() {
635                            Ok(#ok_value) => { #ok_arm_body },
636                            Err(_) => { #handler_code }
637                        }
638                    }
639                }
640            };
641
642            // DEPYLER-0931: Emit hoisted declarations OUTSIDE the match/closure
643            // This ensures variables are captured by mutable reference and retain values
644            // after the try/except block.
645            if let Some(finally_code) = finally_stmts {
646                Ok(quote! {
647                    #(#hoisted_decls)*
648                    {
649                        #match_expr
650                        #finally_code
651                    }
652                })
653            } else {
654                Ok(quote! {
655                    #(#hoisted_decls)*
656                    #match_expr
657                })
658            }
659        }
660    }
661}
662
663/// DEPYLER-0437: Extract .parse() call from generated token stream
664///
665/// Looks for pattern: `let var = expr.parse::<i32>().unwrap_or_default();`
666/// Returns: (variable_name, parse_expression_without_unwrap_or, remaining_statements)
667// DEPYLER-FIX-RC1: Replaced fragile string matching with syn-based AST analysis
668fn extract_parse_from_tokens(
669    try_stmts: &[proc_macro2::TokenStream],
670) -> Option<(String, String, Vec<proc_macro2::TokenStream>)> {
671    if try_stmts.is_empty() {
672        return None;
673    }
674
675    let first_token_stream = &try_stmts[0];
676
677    // Attempt to parse as a Stmt. If it fails (e.g. partial tokens), we bail.
678    let stmt: syn::Stmt = syn::parse2(first_token_stream.clone()).ok()?;
679
680    match stmt {
681        // Handle: let var = expr;
682        syn::Stmt::Local(local) => {
683            // Extract variable name from pattern
684            let var_name = if let syn::Pat::Ident(pat_ident) = &local.pat {
685                pat_ident.ident.to_string()
686            } else {
687                return None;
688            };
689
690            // Check init expression
691            if let Some(init) = &local.init {
692                let parse_expr = extract_parse_expr(&init.expr)?;
693                let remaining = try_stmts[1..].to_vec();
694                return Some((var_name, parse_expr, remaining));
695            }
696        }
697        // Handle: var = expr; or expr;
698        syn::Stmt::Expr(expr, _) => {
699            if let syn::Expr::Assign(assign) = expr {
700                // Extract var name from LHS
701                let var_name = if let syn::Expr::Path(path) = *assign.left {
702                    path.path.segments.last()?.ident.to_string()
703                } else {
704                    return None;
705                };
706
707                let parse_expr = extract_parse_expr(&assign.right)?;
708                let remaining = try_stmts[1..].to_vec();
709                return Some((var_name, parse_expr, remaining));
710            }
711        }
712        // DEPYLER-FIX-RC1: Explicitly ignore Item (fn, struct) and Mac (macro)
713        // This ensures we never match a "for loop" or "while loop" even if it contains "parse"
714        _ => return None,
715    }
716
717    None
718}
719
720/// Helper to check if an expression is `... .parse::<...>().unwrap_or_default()`
721/// Returns the inner stringified expression if matched.
722fn extract_parse_expr(expr: &syn::Expr) -> Option<String> {
723    // We are looking for: MethodCall(unwrap_or_default) -> MethodCall(parse)
724
725    // 1. Check outer method: unwrap_or_default()
726    if let syn::Expr::MethodCall(unwrap_call) = expr {
727        if unwrap_call.method != "unwrap_or_default" {
728            return None;
729        }
730
731        // 2. Check inner receiver: parse()
732        if let syn::Expr::MethodCall(parse_call) = &*unwrap_call.receiver {
733            if parse_call.method == "parse" {
734                // Found it! Return the receiver of the parse call (the string being parsed)
735                // We reconstruct it to a string to match the original function signature
736                return Some(quote::quote!(#parse_call).to_string());
737            }
738        }
739    }
740    None
741}
742
743// DEPYLER-COVERAGE-95: extract_divisor_from_floor_div moved to expr_analysis module
744// DEPYLER-COVERAGE-95: extract_string_literal, extract_kwarg_string, extract_kwarg_bool
745// moved to crate::rust_gen::expr_analysis module for testability
746
747/// DEPYLER-0425: Extract subcommand fields accessed in handler body
748/// Analyzes HIR statements to find args.field attribute accesses
749///
750/// DEPYLER-0480: Now accepts dest_field parameter to filter dynamically
751/// DEPYLER-0481: Now accepts cmd_name and ctx to filter out top-level args
752///
753/// # Complexity
754/// 10 (recursive HIR walk + HashSet operations)
755fn extract_accessed_subcommand_fields(
756    body: &[HirStmt],
757    args_var: &str,
758    dest_field: &str,
759    cmd_name: &str,
760    ctx: &CodeGenContext,
761) -> Vec<String> {
762    let mut fields = std::collections::HashSet::new();
763    extract_fields_recursive(body, args_var, dest_field, &mut fields);
764
765    // DEPYLER-0481: Filter out top-level args that don't belong to this subcommand
766    // Only keep fields that are actual arguments of the subcommand
767    // DEPYLER-0605: Fix duplicate SubcommandInfo issue - prefer the one with arguments
768    // When preregister_subcommands_from_hir runs, it may create an empty SubcommandInfo
769    // with KEY = command_name. Later, assignment processing creates another with
770    // KEY = variable_name and the actual arguments. We need to find the one with args.
771    let subcommand_arg_names: std::collections::HashSet<String> = ctx
772        .argparser_tracker
773        .subcommands
774        .values()
775        .filter(|sub| sub.name == cmd_name)
776        .max_by_key(|sub| sub.arguments.len())
777        .map(|sub| {
778            sub.arguments
779                .iter()
780                .map(|arg| {
781                    // Extract dest name from argument
782                    arg.dest.clone().unwrap_or_else(|| {
783                        // If no dest, use the name (for positional) or long option without dashes
784                        if arg.is_positional {
785                            arg.name.clone()
786                        } else if let Some(long) = &arg.long {
787                            long.trim_start_matches("--").replace('-', "_")
788                        } else {
789                            arg.name.trim_start_matches('-').replace('-', "_")
790                        }
791                    })
792                })
793                .collect()
794        })
795        .unwrap_or_default();
796
797    let mut result: Vec<_> = fields
798        .into_iter()
799        .filter(|f| subcommand_arg_names.contains(f))
800        .collect();
801    result.sort(); // Deterministic order
802    result
803}
804
805/// DEPYLER-0425: Recursively extract fields from HIR statements
806///
807/// DEPYLER-0480: Now accepts dest_field parameter to pass through
808///
809/// # Complexity
810/// 8 (recursive statement traversal)
811pub(crate) fn extract_fields_recursive(
812    stmts: &[HirStmt],
813    args_var: &str,
814    dest_field: &str,
815    fields: &mut std::collections::HashSet<String>,
816) {
817    for stmt in stmts {
818        match stmt {
819            HirStmt::Expr(expr) => extract_fields_from_expr(expr, args_var, dest_field, fields),
820            HirStmt::Assign { value, .. } => {
821                extract_fields_from_expr(value, args_var, dest_field, fields)
822            }
823            HirStmt::If {
824                condition,
825                then_body,
826                else_body,
827            } => {
828                // DEPYLER-0518: Also extract fields from condition
829                // Example: `if not validate_email(args.address)` has args.address in condition
830                extract_fields_from_expr(condition, args_var, dest_field, fields);
831                extract_fields_recursive(then_body, args_var, dest_field, fields);
832                if let Some(else_stmts) = else_body {
833                    extract_fields_recursive(else_stmts, args_var, dest_field, fields);
834                }
835            }
836            // DEPYLER-0577: Recurse into While condition (may contain args.field)
837            HirStmt::While {
838                condition,
839                body: loop_body,
840            } => {
841                extract_fields_from_expr(condition, args_var, dest_field, fields);
842                extract_fields_recursive(loop_body, args_var, dest_field, fields);
843            }
844            // DEPYLER-0577: Recurse into For iterator (may contain args.field)
845            HirStmt::For {
846                iter,
847                body: loop_body,
848                ..
849            } => {
850                extract_fields_from_expr(iter, args_var, dest_field, fields);
851                extract_fields_recursive(loop_body, args_var, dest_field, fields);
852            }
853            HirStmt::Try {
854                body: try_body,
855                handlers,
856                orelse,
857                finalbody,
858            } => {
859                extract_fields_recursive(try_body, args_var, dest_field, fields);
860                for handler in handlers {
861                    extract_fields_recursive(&handler.body, args_var, dest_field, fields);
862                }
863                if let Some(orelse_stmts) = orelse {
864                    extract_fields_recursive(orelse_stmts, args_var, dest_field, fields);
865                }
866                if let Some(finally_stmts) = finalbody {
867                    extract_fields_recursive(finally_stmts, args_var, dest_field, fields);
868                }
869            }
870            HirStmt::With {
871                context,
872                body: with_body,
873                ..
874            } => {
875                // DEPYLER-0931: Extract fields from With context expression
876                // Pattern: `with open(args.file) as f:` - args.file is in context
877                // This was missing, causing E0425 errors for fields used in context
878                extract_fields_from_expr(context, args_var, dest_field, fields);
879                extract_fields_recursive(with_body, args_var, dest_field, fields);
880            }
881            _ => {}
882        }
883    }
884}
885
886/// DEPYLER-0425: Extract fields from HIR expression
887/// Finds patterns like `args.field` and collects field names
888///
889/// DEPYLER-0480: Now uses dest_field parameter instead of hardcoded "command"/"action"
890///
891/// # Complexity
892/// 10 (expression traversal + pattern matching)
893pub(crate) fn extract_fields_from_expr(
894    expr: &HirExpr,
895    args_var: &str,
896    dest_field: &str,
897    fields: &mut std::collections::HashSet<String>,
898) {
899    match expr {
900        // Pattern: args.field
901        HirExpr::Attribute { value, attr } => {
902            if let HirExpr::Var(var) = value.as_ref() {
903                if var == args_var {
904                    // DEPYLER-0480: Filter out the dest field dynamically
905                    // The dest field (e.g., "command" or "action") is the match discriminant,
906                    // so it shouldn't be included in the extracted fields list
907                    if attr != dest_field {
908                        fields.insert(attr.clone());
909                    }
910                }
911            }
912        }
913        // Recurse into nested expressions
914        HirExpr::Call {
915            args: call_args, ..
916        } => {
917            for arg in call_args {
918                extract_fields_from_expr(arg, args_var, dest_field, fields);
919            }
920        }
921        HirExpr::Binary { left, right, .. } => {
922            extract_fields_from_expr(left, args_var, dest_field, fields);
923            extract_fields_from_expr(right, args_var, dest_field, fields);
924        }
925        HirExpr::Unary { operand, .. } => {
926            extract_fields_from_expr(operand, args_var, dest_field, fields);
927        }
928        HirExpr::IfExpr { test, body, orelse } => {
929            extract_fields_from_expr(test, args_var, dest_field, fields);
930            extract_fields_from_expr(body, args_var, dest_field, fields);
931            extract_fields_from_expr(orelse, args_var, dest_field, fields);
932        }
933        HirExpr::Index { base, index } => {
934            extract_fields_from_expr(base, args_var, dest_field, fields);
935            extract_fields_from_expr(index, args_var, dest_field, fields);
936        }
937        HirExpr::List(elements) | HirExpr::Tuple(elements) | HirExpr::Set(elements) => {
938            for elem in elements {
939                extract_fields_from_expr(elem, args_var, dest_field, fields);
940            }
941        }
942        HirExpr::Dict(pairs) => {
943            for (key, value) in pairs {
944                extract_fields_from_expr(key, args_var, dest_field, fields);
945                extract_fields_from_expr(value, args_var, dest_field, fields);
946            }
947        }
948        HirExpr::MethodCall {
949            object,
950            args: method_args,
951            ..
952        } => {
953            extract_fields_from_expr(object, args_var, dest_field, fields);
954            for arg in method_args {
955                extract_fields_from_expr(arg, args_var, dest_field, fields);
956            }
957        }
958        // DEPYLER-0577: Handle f-strings - recurse into expression parts
959        HirExpr::FString { parts } => {
960            for part in parts {
961                if let crate::hir::FStringPart::Expr(expr) = part {
962                    extract_fields_from_expr(expr, args_var, dest_field, fields);
963                }
964            }
965        }
966        _ => {}
967    }
968}
969
970/// DEPYLER-0399: Try to generate a match statement for subcommand dispatch
971///
972/// Detects patterns like:
973/// ```python
974/// if args.command == "clone":
975///     handle_clone(args)
976/// elif args.command == "push":
977///     handle_push(args)
978/// ```
979///
980/// And converts to:
981/// ```rust,ignore
982/// match args.command {
983///     Commands::Clone { url } => {
984///         handle_clone(args);
985///     }
986///     Commands::Push { remote } => {
987///         handle_push(args);
988///     }
989/// }
990/// ```
991pub(crate) fn try_generate_subcommand_match(
992    condition: &HirExpr,
993    then_body: &[HirStmt],
994    else_body: &Option<Vec<HirStmt>>,
995    ctx: &mut CodeGenContext,
996) -> Result<Option<proc_macro2::TokenStream>> {
997    use quote::{format_ident, quote};
998
999    // DEPYLER-0456 #2: Get dest_field from subparser info
1000    // Find the dest_field name (e.g., "action" or "command")
1001    let dest_field = ctx
1002        .argparser_tracker
1003        .subparsers
1004        .values()
1005        .next()
1006        .map(|sp| sp.dest_field.clone())
1007        .unwrap_or_else(|| "command".to_string()); // Default to "command" for backwards compatibility
1008
1009    // Check if condition matches: args.<dest_field> == "string" OR CSE temp variable
1010    let command_name = match is_subcommand_check(condition, &dest_field, ctx) {
1011        Some(name) => name,
1012        None => return Ok(None),
1013    };
1014
1015    // Collect all branches (if + elif chain)
1016    let mut branches = vec![(command_name, then_body)];
1017
1018    // Check if else is another if statement (elif pattern)
1019    let mut current_else = else_body;
1020    while let Some(else_stmts) = current_else {
1021        // DEPYLER-0456 Bug #2 FIX: Handle CSE-optimized elif branches
1022        // CSE creates: [assignment: _cse_temp_N = check, if: _cse_temp_N { ... }]
1023        // Original (pre-CSE) elif is a single If statement
1024        let (elif_stmt, cse_cmd_name) = if else_stmts.len() == 1 {
1025            // Pre-CSE or direct elif: single If statement
1026            (&else_stmts[0], None)
1027        } else if else_stmts.len() == 2 {
1028            // CSE-optimized elif: [assignment, if]
1029            // Extract command name from the CSE assignment
1030            if let HirStmt::Assign {
1031                target: AssignTarget::Symbol(var),
1032                value,
1033                ..
1034            } = &else_stmts[0]
1035            {
1036                if var.starts_with("_cse_temp") {
1037                    // Extract command name from the assignment value
1038                    let cmd_name = is_subcommand_check(value, &dest_field, ctx);
1039                    (&else_stmts[1], cmd_name)
1040                } else {
1041                    // Not a CSE pattern, stop collecting
1042                    break;
1043                }
1044            } else {
1045                // Not a CSE pattern, stop collecting
1046                break;
1047            }
1048        } else {
1049            // Not an elif pattern, stop collecting
1050            break;
1051        };
1052
1053        // Check if this is an If statement with subcommand check
1054        if let HirStmt::If {
1055            condition: elif_cond,
1056            then_body: elif_then,
1057            else_body: elif_else,
1058        } = elif_stmt
1059        {
1060            // Use command name from CSE assignment if available, otherwise check condition
1061            let elif_name =
1062                cse_cmd_name.or_else(|| is_subcommand_check(elif_cond, &dest_field, ctx));
1063
1064            if let Some(name) = elif_name {
1065                branches.push((name, elif_then.as_slice()));
1066                current_else = elif_else;
1067                continue;
1068            }
1069        }
1070
1071        // Not an elif pattern, stop collecting
1072        break;
1073    }
1074
1075    // DEPYLER-0482: Check if any branch has an early return
1076    // If so, don't add wildcard unreachable!() because execution continues to next match
1077    let has_early_return = branches.iter().any(|(_, body)| {
1078        body.iter()
1079            .any(|stmt| matches!(stmt, HirStmt::Return { .. }))
1080    });
1081
1082    // Generate match arms
1083    // DEPYLER-0940: Filter out empty command names to prevent panic in format_ident!()
1084    let arms: Vec<proc_macro2::TokenStream> = branches
1085        .iter()
1086        .filter(|(cmd_name, _)| !cmd_name.is_empty())
1087        .map(|(cmd_name, body)| {
1088            // Convert command name to PascalCase variant
1089            let variant_name = format_ident!("{}", to_pascal_case(cmd_name));
1090
1091            // DEPYLER-0425: Detect which fields are accessed in the body
1092            // This determines whether we use Pattern A ({ .. }) or Pattern B ({ field1, field2, ... })
1093            // DEPYLER-0480: Pass dest_field to dynamically filter based on actual dest parameter
1094            // DEPYLER-0481: Pass cmd_name and ctx to filter out top-level args
1095            let mut accessed_fields =
1096                extract_accessed_subcommand_fields(body, "args", &dest_field, cmd_name, ctx);
1097
1098            // DEPYLER-0608: Detect if body calls a cmd_* handler
1099            // If so, get ALL subcommand fields since the handler accesses them internally
1100            // Pattern: the match arm body is `cmd_list(args)` which needs all `list` subcommand fields
1101            let calls_cmd_handler = body.iter().any(|stmt| {
1102                if let HirStmt::Expr(HirExpr::Call { func: func_name, args: call_args, .. }) = stmt {
1103                    // func is Symbol (String), not Box<HirExpr>
1104                    // Check if it's a cmd_* or handle_* function call with args parameter
1105                    let is_handler = func_name.starts_with("cmd_") || func_name.starts_with("handle_");
1106                    let has_args_param = call_args.iter().any(|a| matches!(a, HirExpr::Var(v) if v == "args"));
1107                    is_handler && has_args_param
1108                } else {
1109                    false
1110                }
1111            });
1112
1113            if calls_cmd_handler && accessed_fields.is_empty() {
1114                // Get ALL fields for this subcommand
1115                if let Some(subcommand) = ctx
1116                    .argparser_tracker
1117                    .subcommands
1118                    .values()
1119                    .filter(|sc| sc.name == *cmd_name)
1120                    .max_by_key(|sc| sc.arguments.len())
1121                {
1122                    for arg in &subcommand.arguments {
1123                        // DEPYLER-0762: Use rust_field_name() to properly sanitize flag names
1124                        // This strips leading dashes and converts hyphens to underscores
1125                        // e.g., "--format" → "format", "--no-color" → "no_color"
1126                        let field_name = arg.rust_field_name();
1127                        accessed_fields.push(field_name);
1128                    }
1129                }
1130            }
1131
1132            // DEPYLER-0608: Set context flags for handler call transformation
1133            // When in a subcommand match arm that calls a handler, expr_gen will
1134            // transform cmd_X(args) → cmd_X(field1, field2, ...)
1135            // DEPYLER-0665: Always set subcommand_match_fields for ref-pattern bindings
1136            // This allows clone detection in stmt_gen when assigning mutable vars from refs
1137            let was_in_match_arm = ctx.in_subcommand_match_arm;
1138            let old_match_fields = std::mem::take(&mut ctx.subcommand_match_fields);
1139            if calls_cmd_handler {
1140                ctx.in_subcommand_match_arm = true;
1141            }
1142            // Always track ref-pattern bindings, not just when calling handler
1143            if !accessed_fields.is_empty() {
1144                ctx.subcommand_match_fields = accessed_fields.clone();
1145            }
1146
1147            // Generate body statements
1148            ctx.enter_scope();
1149
1150            // DEPYLER-0577: Register field types in var_types before processing body
1151            // This allows type-aware codegen (e.g., float vs int comparisons)
1152            // DEPYLER-0605: Use filter + max_by_key to find the SubcommandInfo with most arguments
1153            // DEPYLER-0722: Handle Optional types and boolean flags correctly
1154            for field_name in &accessed_fields {
1155                if let Some(subcommand) = ctx
1156                    .argparser_tracker
1157                    .subcommands
1158                    .values()
1159                    .filter(|sc| sc.name == *cmd_name)
1160                    .max_by_key(|sc| sc.arguments.len())
1161                {
1162                    if let Some(arg) = subcommand.arguments.iter().find(|a| {
1163                        // DEPYLER-0722: Strip dashes from short options (-n → n)
1164                        let arg_name = a.long.as_ref()
1165                            .map(|s| s.trim_start_matches('-').to_string())
1166                            .unwrap_or_else(|| a.name.trim_start_matches('-').to_string());
1167                        &arg_name == field_name
1168                    }) {
1169                        // DEPYLER-0722: Determine actual type including Optional wrapper
1170                        let base_type = if let Some(ref ty) = arg.arg_type {
1171                            Some(ty.clone())
1172                        } else if matches!(arg.action.as_deref(), Some("store_true") | Some("store_false")) {
1173                            Some(Type::Bool)
1174                        } else {
1175                            None
1176                        };
1177
1178                        if let Some(ty) = base_type {
1179                            // DEPYLER-0722: Check if this is actually Option<T> in Clap
1180                            // An argument is Option<T> if: NOT required AND NO default AND NOT positional
1181                            // AND NOT a boolean flag (store_true/store_false)
1182                            let is_bool_flag = matches!(arg.action.as_deref(), Some("store_true") | Some("store_false"));
1183                            let is_option_type = !arg.is_positional
1184                                && !arg.required.unwrap_or(false)
1185                                && arg.default.is_none()
1186                                && !is_bool_flag;
1187
1188                            let actual_type = if is_option_type {
1189                                Type::Optional(Box::new(ty))
1190                            } else {
1191                                ty
1192                            };
1193                            ctx.var_types.insert(field_name.clone(), actual_type);
1194                        }
1195                    }
1196                }
1197            }
1198
1199            let body_stmts: Vec<_> = body
1200                .iter()
1201                .filter_map(|s| {
1202                    match s.to_rust_tokens(ctx) {
1203                        Ok(tokens) => Some(tokens),
1204                        Err(e) => {
1205                            // DEPYLER-0593: Log conversion errors instead of silently dropping
1206                            tracing::warn!("argparse body stmt conversion failed: {}", e);
1207                            None
1208                        }
1209                    }
1210                })
1211                .collect();
1212            ctx.exit_scope();
1213
1214            // DEPYLER-0608: Restore context flags
1215            ctx.in_subcommand_match_arm = was_in_match_arm;
1216            ctx.subcommand_match_fields = old_match_fields;
1217
1218            // DEPYLER-0456 Bug #3 FIX: Always use struct variant syntax `{}`
1219            // Clap generates struct variants (e.g., `Init {}`) not unit variants (e.g., `Init`)
1220            //
1221            // DEPYLER-0425: Pattern selection based on field usage
1222            // - Pattern A: No fields accessed → { .. } (handler gets &args)
1223            // - Pattern B: Fields accessed → { field1, field2, ... } (handler gets individual fields)
1224            if accessed_fields.is_empty() {
1225                // Pattern A: No field access, use { .. }
1226                // DEPYLER-1063: args.command is Option<Commands>, wrap pattern in Some()
1227                quote! {
1228                    Some(Commands::#variant_name { .. }) => {
1229                        #(#body_stmts)*
1230                    }
1231                }
1232            } else {
1233                // Pattern B: Extract accessed fields with explicit ref patterns
1234                // Using `ref` ensures consistent binding as references regardless of match ergonomics
1235                let _field_idents: Vec<syn::Ident> = accessed_fields
1236                    .iter()
1237                    .map(|f| format_ident!("{}", f))
1238                    .collect();
1239                // DEPYLER-0843: Use safe_ident for keyword escaping in match patterns
1240                // If a field is named 'type', it needs to be escaped as 'r#type' in patterns
1241                let ref_field_patterns: Vec<proc_macro2::TokenStream> = accessed_fields
1242                    .iter()
1243                    .map(|f| {
1244                        let ident = safe_ident(f);
1245                        quote! { ref #ident }
1246                    })
1247                    .collect();
1248
1249                // DEPYLER-0526: Generate field conversion bindings for borrowed match variables
1250                // When matching &args.command, destructured fields are references (&String, &bool)
1251                // Convert to owned values so they work with functions expecting either owned or borrowed:
1252                // - String fields: .to_string() converts &String → String
1253                //   String can then deref-coerce to &str if needed
1254                // - bool/primitives: dereference with *
1255                // DEPYLER-0843: Also use safe_ident for field bindings after match
1256                let field_bindings: Vec<proc_macro2::TokenStream> = accessed_fields
1257                    .iter()
1258                    .map(|field_name| {
1259                        let field_ident = safe_ident(field_name);
1260
1261                        // Look up field type from subcommand arguments
1262                        // Check both arg_type and action (for store_true/store_false bool flags)
1263                        // DEPYLER-0605: Use filter + max_by_key to find the SubcommandInfo with most arguments
1264                        let maybe_arg = ctx
1265                            .argparser_tracker
1266                            .subcommands
1267                            .values()
1268                            .filter(|sc| sc.name == *cmd_name)
1269                            .max_by_key(|sc| sc.arguments.len())
1270                            .and_then(|sc| {
1271                                sc.arguments.iter().find(|arg| {
1272                                    // Match by field name (from long flag or positional name)
1273                                    // DEPYLER-0722: Also strip dashes from short options (-n → n)
1274                                    let arg_field_name = arg
1275                                        .long
1276                                        .as_ref()
1277                                        .map(|s| s.trim_start_matches('-').to_string())
1278                                        .unwrap_or_else(|| arg.name.trim_start_matches('-').to_string());
1279                                    arg_field_name == *field_name
1280                                })
1281                            });
1282
1283                        // Determine type: check arg_type first, then action for bool flags
1284                        let field_type = maybe_arg
1285                            .and_then(|arg| {
1286                                // If arg_type is set, use it
1287                                if let Some(ref base_type) = arg.arg_type {
1288                                    // DEPYLER-0768: nargs="+" or nargs="*" wraps type in List
1289                                    // Python: add_argument("values", type=int, nargs="+")
1290                                    // Rust: Vec<i32> (not i32)
1291                                    let is_multi = matches!(
1292                                        arg.nargs.as_deref(),
1293                                        Some("+") | Some("*")
1294                                    );
1295                                    if is_multi {
1296                                        return Some(Type::List(Box::new(base_type.clone())));
1297                                    }
1298                                    return Some(base_type.clone());
1299                                }
1300                                // Check action for bool flags: store_true/store_false → Bool
1301                                if matches!(
1302                                    arg.action.as_deref(),
1303                                    Some("store_true") | Some("store_false") | Some("store_const")
1304                                ) {
1305                                    return Some(Type::Bool);
1306                                }
1307                                None
1308                            })
1309                            .or_else(|| {
1310                                // DEPYLER-0526: Name-based fallback for common boolean fields
1311                                // If argument lookup failed, use heuristics based on field name
1312                                let field_lower = field_name.to_lowercase();
1313                                let bool_indicators = [
1314                                    "binary",
1315                                    "append",
1316                                    "verbose",
1317                                    "quiet",
1318                                    "force",
1319                                    "dry_run",
1320                                    "recursive",
1321                                    "debug",
1322                                    "silent",
1323                                    "capture",
1324                                    "overwrite",
1325                                ];
1326                                if bool_indicators
1327                                    .iter()
1328                                    .any(|ind| field_lower == *ind || field_lower.ends_with(ind))
1329                                {
1330                                    Some(Type::Bool)
1331                                } else {
1332                                    None
1333                                }
1334                            });
1335
1336                        // Generate conversion based on type
1337                        // NOTE: With explicit `ref` patterns, all fields are bound as references:
1338                        //   - Copy types (bool, int, float) need dereferencing: *field
1339                        //   - Non-Copy types (String, Vec) are already &T
1340                        match field_type {
1341                            Some(Type::Bool) => {
1342                                // With explicit `ref` pattern, bool is &bool - dereference to get bool
1343                                quote! { let #field_ident = *#field_ident; }
1344                            }
1345                            Some(Type::Int) | Some(Type::Float) => {
1346                                // DEPYLER-0576: Check if field has a default value (is Option<T>)
1347                                // Clap represents optional args with defaults as Option<T>
1348                                // ref binding gives &Option<T>, need to unwrap with default
1349                                // DEPYLER-0722: Also check for Option<T> without default (NOT required AND NOT positional)
1350                                let has_default = maybe_arg
1351                                    .as_ref()
1352                                    .map(|a| a.default.is_some())
1353                                    .unwrap_or(false);
1354                                let is_required = maybe_arg
1355                                    .as_ref()
1356                                    .map(|a| a.required.unwrap_or(false))
1357                                    .unwrap_or(false);
1358                                let is_positional = maybe_arg
1359                                    .as_ref()
1360                                    .map(|a| a.is_positional)
1361                                    .unwrap_or(false);
1362                                // DEPYLER-0722: An argument is Option<T> if NOT required AND NOT positional AND NO default
1363                                let is_option_without_default = !is_required && !is_positional && !has_default;
1364
1365                                if has_default {
1366                                    // Field is Option<T>, unwrap with default
1367                                    // Clone the default expression to release borrow on ctx
1368                                    let default_expr_opt = maybe_arg
1369                                        .and_then(|a| a.default.clone());
1370
1371                                    let default_val = if let Some(ref d) = default_expr_opt {
1372                                        d.to_rust_expr(ctx).ok()
1373                                    } else {
1374                                        None
1375                                    }.unwrap_or_else(|| {
1376                                        // Fallback to 0.0 for Float, 0 for Int
1377                                        if matches!(field_type, Some(Type::Float)) {
1378                                            syn::parse_quote! { 0.0 }
1379                                        } else {
1380                                            syn::parse_quote! { 0 }
1381                                        }
1382                                    });
1383                                    quote! { let #field_ident = #field_ident.unwrap_or(#default_val); }
1384                                } else if is_option_without_default {
1385                                    // DEPYLER-0722: Option<T> without default - clone the &Option<T> to Option<T>
1386                                    // Body code can then use .is_some() for truthiness
1387                                    quote! { let #field_ident = #field_ident.clone(); }
1388                                } else {
1389                                    // Required field (not Option), just dereference
1390                                    quote! { let #field_ident = *#field_ident; }
1391                                }
1392                            }
1393                            Some(Type::String) => {
1394                                // DEPYLER-0933: First check if this is an Optional String field
1395                                // (NOT required AND NOT positional AND NO default)
1396                                let has_default = maybe_arg
1397                                    .as_ref()
1398                                    .map(|a| a.default.is_some())
1399                                    .unwrap_or(false);
1400                                let is_required = maybe_arg
1401                                    .as_ref()
1402                                    .map(|a| a.required.unwrap_or(false))
1403                                    .unwrap_or(false);
1404                                let is_positional = maybe_arg
1405                                    .as_ref()
1406                                    .map(|a| a.is_positional)
1407                                    .unwrap_or(false);
1408                                let is_option_without_default =
1409                                    !is_required && !is_positional && !has_default;
1410
1411                                // DEPYLER-0526: Name-based heuristics for String handling
1412                                let field_lower = field_name.to_lowercase();
1413                                let owned_indicators = [
1414                                    "file", "path", "filepath", "input", "output", "dir",
1415                                    "directory",
1416                                ];
1417                                let borrowed_indicators =
1418                                    ["content", "pattern", "text", "message", "data", "value"];
1419
1420                                let needs_owned = owned_indicators.iter().any(|ind| {
1421                                    field_lower == *ind
1422                                        || field_lower.ends_with(ind)
1423                                        || field_lower.starts_with(ind)
1424                                });
1425                                let needs_borrowed = borrowed_indicators.iter().any(|ind| {
1426                                    field_lower == *ind
1427                                        || field_lower.ends_with(ind)
1428                                        || field_lower.starts_with(ind)
1429                                });
1430
1431                                if is_option_without_default || has_default {
1432                                    // DEPYLER-0933: Option<String> field - unwrap with default
1433                                    // Use as_deref() to get Option<&str>, then unwrap_or("")
1434                                    // This gives &str which works for both &str and String params
1435                                    quote! { let #field_ident = #field_ident.as_deref().unwrap_or_default(); }
1436                                } else {
1437                                    // Required field - regular String handling
1438                                    if needs_borrowed {
1439                                        // Keep as &String, auto-derefs to &str
1440                                        quote! {}
1441                                    } else if needs_owned {
1442                                        // Convert to owned String
1443                                        quote! { let #field_ident = #field_ident.to_string(); }
1444                                    } else {
1445                                        // Default: convert to owned (safer for function calls)
1446                                        quote! { let #field_ident = #field_ident.to_string(); }
1447                                    }
1448                                }
1449                            }
1450                            Some(Type::Optional(_))
1451                            | Some(Type::List(_))
1452                            | Some(Type::Dict(_, _)) => {
1453                                // For complex container types, clone the reference
1454                                quote! { let #field_ident = #field_ident.clone(); }
1455                            }
1456                            None => {
1457                                // DEPYLER-0933: Check if this is an Optional field (unknown type)
1458                                let has_default = maybe_arg
1459                                    .as_ref()
1460                                    .map(|a| a.default.is_some())
1461                                    .unwrap_or(false);
1462                                let is_required = maybe_arg
1463                                    .as_ref()
1464                                    .map(|a| a.required.unwrap_or(false))
1465                                    .unwrap_or(false);
1466                                let is_positional = maybe_arg
1467                                    .as_ref()
1468                                    .map(|a| a.is_positional)
1469                                    .unwrap_or(false);
1470                                let is_option_without_default =
1471                                    !is_required && !is_positional && !has_default;
1472
1473                                // Unknown type: use name-based heuristics
1474                                let field_lower = field_name.to_lowercase();
1475                                let owned_indicators = [
1476                                    "file",
1477                                    "path",
1478                                    "filepath",
1479                                    "input",
1480                                    "output",
1481                                    "dir",
1482                                    "directory",
1483                                ];
1484                                let borrowed_indicators =
1485                                    ["content", "pattern", "text", "message", "data", "value"];
1486                                // DEPYLER-0579: String-like field indicators (should NOT be numeric-unwrapped)
1487                                let string_indicators = [
1488                                    "str", "string", "name", "line", "word", "char", "cmd",
1489                                    "url", "uri", "host", "token", "key", "id", "code",
1490                                    "hex", "oct", // hex/oct values are string representations
1491                                    "suffix", // DEPYLER-0933: suffix is a string field
1492                                ];
1493                                // DEPYLER-0576: Numeric field indicators (likely f64 with defaults)
1494                                // DEPYLER-0592: Removed single letters - too ambiguous, often strings
1495                                let numeric_indicators = [
1496                                    "x1", "x2", "y1", "y2", "z1", "z2",
1497                                    "val", "num", "count", "rate", "coef", "factor",
1498                                    "min", "max", "sum", "avg", "mean", "std",
1499                                    "width", "height", "size", "len", "length",
1500                                    "alpha", "beta", "gamma", "theta", "lr",
1501                                ];
1502
1503                                let needs_owned = owned_indicators.iter().any(|ind| {
1504                                    field_lower == *ind
1505                                        || field_lower.ends_with(ind)
1506                                        || field_lower.starts_with(ind)
1507                                });
1508                                let needs_borrowed = borrowed_indicators.iter().any(|ind| {
1509                                    field_lower == *ind
1510                                        || field_lower.ends_with(ind)
1511                                        || field_lower.starts_with(ind)
1512                                });
1513                                // DEPYLER-0579: Check if this looks like a string field
1514                                let looks_like_string = string_indicators.iter().any(|ind| {
1515                                    field_lower == *ind
1516                                        || field_lower.ends_with(ind)
1517                                        || field_lower.starts_with(ind)
1518                                        || field_lower.contains(ind)
1519                                });
1520                                // Only apply numeric unwrap if NOT string-like
1521                                let needs_numeric_unwrap = !looks_like_string
1522                                    && numeric_indicators.iter().any(|ind| {
1523                                        field_lower == *ind
1524                                            || field_lower.ends_with(ind)
1525                                            || field_lower.starts_with(ind)
1526                                    });
1527
1528                                // DEPYLER-0933: If optional without default and looks like string,
1529                                // use as_deref() to get &str which works for both &str and String params
1530                                if is_option_without_default && looks_like_string {
1531                                    quote! { let #field_ident = #field_ident.as_deref().unwrap_or_default(); }
1532                                } else if is_option_without_default && (needs_owned || needs_borrowed) {
1533                                    // Optional string-like field, unwrap with as_deref for &str
1534                                    quote! { let #field_ident = #field_ident.as_deref().unwrap_or_default(); }
1535                                } else if needs_borrowed || (is_positional && needs_owned) {
1536                                    // DEPYLER-0933: Keep as reference for:
1537                                    // - borrowed indicators (content, pattern, text, etc.)
1538                                    // - positional string fields that match owned indicators
1539                                    //   (file, path, output, etc.) - these are &String, not &Option<String>
1540                                    quote! {}
1541                                } else if needs_owned || looks_like_string {
1542                                    // Convert to owned String (for optional fields that weren't caught above)
1543                                    quote! { let #field_ident = #field_ident.to_string(); }
1544                                } else if needs_numeric_unwrap {
1545                                    // DEPYLER-0576: Likely numeric Option<T> field, unwrap with default
1546                                    // DEPYLER-0677: Use Default::default() for type-safe numeric defaults
1547                                    quote! { let #field_ident = #field_ident.unwrap_or_default(); }
1548                                } else {
1549                                    // Unknown: keep as reference (safer default)
1550                                    quote! {}
1551                                }
1552                            }
1553                            _ => {
1554                                // For other complex types, clone
1555                                quote! { let #field_ident = #field_ident.clone(); }
1556                            }
1557                        }
1558                    })
1559                    .collect();
1560
1561                // DEPYLER-0578: Add `..` to pattern to ignore unmentioned fields (fixes E0027)
1562                // The subcommand may have more fields than we extract from body statements
1563                // DEPYLER-1063: args.command is Option<Commands>, wrap pattern in Some()
1564                quote! {
1565                    Some(Commands::#variant_name { #(#ref_field_patterns,)* .. }) => {
1566                        #(#field_bindings)*
1567                        #(#body_stmts)*
1568                    }
1569                }
1570            }
1571        })
1572        .collect();
1573
1574    // DEPYLER-0456 Bug #3 FIX: Always use "command" as the Rust struct field name
1575    // The Args struct always has `command: Commands` regardless of Python's dest parameter
1576    // DEPYLER-0470: Add wildcard arm to make match exhaustive
1577    // When early returns split matches, not all Commands variants may be in this match
1578    // Use unreachable!() because split matches ensure mutually exclusive variants
1579    // DEPYLER-0474: Match by reference to avoid partial move errors
1580    // When handler functions take &args, we must borrow args.command, not move it
1581    // DEPYLER-0482: Only add wildcard if no early returns (otherwise execution continues to next match)
1582    Ok(Some(if has_early_return {
1583        // Early return present: Don't add wildcard, execution continues to next match
1584        quote! {
1585            match &args.command {
1586                #(#arms)*
1587                _ => {}
1588            }
1589        }
1590    } else {
1591        // No early returns: This is likely the final/complete match, add unreachable wildcard
1592        quote! {
1593            match &args.command {
1594                #(#arms)*
1595                _ => unreachable!("Other command variants handled elsewhere")
1596            }
1597        }
1598    }))
1599}
1600
1601/// DEPYLER-0399: Check if expression is a subcommand check pattern
1602/// DEPYLER-0456 Bug #2: Accept dest_field parameter to support custom field names
1603///
1604/// Returns the command name if pattern matches: args.<dest_field> == "string"
1605pub(crate) fn is_subcommand_check(
1606    expr: &HirExpr,
1607    dest_field: &str,
1608    ctx: &CodeGenContext,
1609) -> Option<String> {
1610    match expr {
1611        // Direct comparison: args.action == "init"
1612        HirExpr::Binary {
1613            op: BinOp::Eq,
1614            left,
1615            right,
1616        } => {
1617            // DEPYLER-0456 #2: Check if left side is args.<dest_field>
1618            // (e.g., args.action, args.command, etc.)
1619            let is_dest_field_attr = matches!(
1620                left.as_ref(),
1621                HirExpr::Attribute { attr, .. } if attr == dest_field
1622            );
1623
1624            // Check if right side is a string literal
1625            if is_dest_field_attr {
1626                if let HirExpr::Literal(Literal::String(cmd_name)) = right.as_ref() {
1627                    return Some(cmd_name.clone());
1628                }
1629            }
1630            None
1631        }
1632        // DEPYLER-0456 Bug #2 FIX: CSE temp variable (e.g., _cse_temp_0)
1633        // After CSE optimization, the condition becomes just a variable reference
1634        HirExpr::Var(var_name) => {
1635            // Look up in CSE subcommand temps map
1636            ctx.cse_subcommand_temps.get(var_name).cloned()
1637        }
1638        _ => None,
1639    }
1640}
1641
1642impl RustCodeGen for HirStmt {
1643    fn to_rust_tokens(&self, ctx: &mut CodeGenContext) -> Result<proc_macro2::TokenStream> {
1644        match self {
1645            HirStmt::Assign {
1646                target,
1647                value,
1648                type_annotation,
1649            } => codegen_assign_stmt(target, value, type_annotation, ctx),
1650            HirStmt::Return(expr) => codegen_return_stmt(expr, ctx),
1651            HirStmt::If {
1652                condition,
1653                then_body,
1654                else_body,
1655            } => codegen_if_stmt(condition, then_body, else_body, ctx),
1656            HirStmt::While { condition, body } => codegen_while_stmt(condition, body, ctx),
1657            HirStmt::For { target, iter, body } => codegen_for_stmt(target, iter, body, ctx),
1658            HirStmt::Expr(expr) => codegen_expr_stmt(expr, ctx),
1659            HirStmt::Raise {
1660                exception,
1661                cause: _,
1662            } => codegen_raise_stmt(exception, ctx),
1663            HirStmt::Break { label } => codegen_break_stmt(label),
1664            HirStmt::Continue { label } => codegen_continue_stmt(label),
1665            HirStmt::With {
1666                context,
1667                target,
1668                body,
1669                is_async,
1670            } => codegen_with_stmt(context, target, body, *is_async, ctx),
1671            HirStmt::Try {
1672                body,
1673                handlers,
1674                orelse: _,
1675                finalbody,
1676            } => codegen_try_stmt(body, handlers, finalbody, ctx),
1677            HirStmt::Assert { test, msg } => codegen_assert_stmt(test, msg, ctx),
1678            HirStmt::Pass => codegen_pass_stmt(),
1679            // DEPYLER-0614: Handle Block of statements (for multi-target assignment: i = j = 0)
1680            HirStmt::Block(stmts) => {
1681                let mut tokens = proc_macro2::TokenStream::new();
1682                for stmt in stmts {
1683                    tokens.extend(stmt.to_rust_tokens(ctx)?);
1684                }
1685                Ok(tokens)
1686            }
1687            HirStmt::FunctionDef {
1688                name,
1689                params,
1690                ret_type,
1691                body,
1692                docstring: _,
1693            } => codegen_nested_function_def(name, params, ret_type, body, ctx),
1694        }
1695    }
1696}
1697
1698// ============================================================================
1699// DEPYLER-0427: Nested Function Code Generation
1700// ============================================================================
1701
1702// Note: hir_type_to_tokens is imported from crate::rust_gen::type_tokens
1703// for better testability (DEPYLER-0759)
1704
1705/// Generate Rust code for nested function definitions (inner functions)
1706///
1707/// Python nested functions are converted to Rust inner functions.
1708/// This enables code like csv_filter.py and log_analyzer.py to transpile.
1709///
1710/// # Examples
1711///
1712/// Python:
1713/// ```python
1714/// def outer():
1715///     def inner(x):
1716///         return x * 2
1717///     return inner(5)
1718/// ```
1719///
1720/// Rust:
1721/// ```rust
1722/// fn outer() -> i64 {
1723///     fn inner(x: i64) -> i64 {
1724///         x * 2
1725///     }
1726///     inner(5)
1727/// }
1728/// ```
1729fn codegen_nested_function_def(
1730    name: &str,
1731    params: &[HirParam],
1732    ret_type: &Type,
1733    body: &[HirStmt],
1734    ctx: &mut CodeGenContext,
1735) -> Result<proc_macro2::TokenStream> {
1736    use quote::quote;
1737
1738    // DEPYLER-0842: Generate function name with keyword escaping
1739    // Previously used syn::Ident::new() which doesn't escape keywords.
1740    // If Python has `def const(...)`, this creates `let const = ...` which fails
1741    // because `const` is a Rust keyword. Using safe_ident produces `let r#const = ...`
1742    let fn_name = safe_ident(name);
1743
1744    // GH-70: Use inferred parameters from context if available
1745    // DEPYLER-0687: Clone params to avoid borrow conflicts with ctx.declare_var
1746    let effective_params: Vec<HirParam> = ctx
1747        .nested_function_params
1748        .get(name)
1749        .cloned()
1750        .unwrap_or_else(|| params.to_vec());
1751
1752    // GH-70: Populate ctx.var_types with inferred param types so that
1753    // expressions in body (like item[0]) can use proper type info
1754    // to decide between tuple syntax (.0) and array syntax ([0])
1755    for param in &effective_params {
1756        ctx.var_types.insert(param.name.clone(), param.ty.clone());
1757    }
1758
1759    // Generate parameters
1760    // DEPYLER-0550: For collection types (Dict, List), use references
1761    // This is more idiomatic in Rust and works correctly with filter() closures
1762    // DEPYLER-0769: Use safe_ident to escape Rust keywords in closure parameters
1763    let param_tokens: Vec<proc_macro2::TokenStream> = effective_params
1764        .iter()
1765        .map(|p| {
1766            let param_name = safe_ident(&p.name);
1767            let param_type = hir_type_to_tokens(&p.ty);
1768
1769            // For collection types and strings, take by reference for idiomatic Rust
1770            // This is necessary for closures used with filter() which provides &T
1771            // DEPYLER-0774: Also use &str for String params in nested functions
1772            // Parent functions receive &str but nested func expects String → use &str
1773            if matches!(p.ty, Type::Dict(_, _) | Type::List(_) | Type::Set(_)) {
1774                quote! { #param_name: &#param_type }
1775            } else if matches!(p.ty, Type::String) {
1776                // DEPYLER-0774: Use &str instead of String for nested function params
1777                // This matches how parent functions receive string args (&str)
1778                quote! { #param_name: &str }
1779            } else {
1780                quote! { #param_name: #param_type }
1781            }
1782        })
1783        .collect();
1784
1785    // Generate return type
1786    let return_type = hir_type_to_tokens(ret_type);
1787
1788    // DEPYLER-0550: Save and restore can_fail flag for nested closures
1789    // Nested closures should NOT inherit can_fail from parent function
1790    // Otherwise return statements get incorrectly wrapped in Ok()
1791    let saved_can_fail = ctx.current_function_can_fail;
1792    ctx.current_function_can_fail = false;
1793
1794    // DEPYLER-0731: Save and restore return type for nested functions
1795    // Without this, nested function body uses outer function's return type,
1796    // causing `return x * 2` to become `return;` when outer returns None
1797    let saved_return_type = ctx.current_return_type.take();
1798    ctx.current_return_type = Some(ret_type.clone());
1799
1800    // DEPYLER-0731: Save and restore is_main_function for nested functions
1801    // Without this, nested function inside main() would trigger main-specific
1802    // return handling (DEPYLER-0617) that discards the return value
1803    let saved_is_main = ctx.is_main_function;
1804    ctx.is_main_function = false;
1805
1806    // DEPYLER-0687: Enter new scope for nested function body
1807    // This isolates variable declarations so they don't leak between closures.
1808    // Without this, a variable like `result` declared in one closure would be
1809    // considered "already declared" in sibling closures, causing E0425 errors.
1810    ctx.enter_scope();
1811
1812    // Declare parameters in this scope
1813    for param in &effective_params {
1814        ctx.declare_var(&param.name);
1815    }
1816
1817    // DEPYLER-0766: Analyze mutability for nested function body
1818    // Without this, variables reassigned inside closures don't get `let mut`,
1819    // causing E0384 "cannot assign twice to immutable variable" errors.
1820    // DEPYLER-99MODE-S9: Save outer function's mutable_vars before analyzing nested function.
1821    // analyze_mutable_vars clears ctx.mutable_vars, which would wipe the outer function's
1822    // analysis. Variables declared AFTER nested functions would lose their `mut`.
1823    let saved_mutable_vars = ctx.mutable_vars.clone();
1824    crate::rust_gen::analyze_mutable_vars(body, ctx, &effective_params);
1825
1826    // DEPYLER-99MODE-S9: Detect FnMut closures — closures that mutate captured variables.
1827    // After analyze_mutable_vars, ctx.mutable_vars contains variables mutated in the closure.
1828    // If any of these are NOT closure params/locals, they are captured mutations → FnMut.
1829    let closure_param_names: std::collections::HashSet<String> =
1830        effective_params.iter().map(|p| p.name.clone()).collect();
1831    let closure_is_fn_mut = ctx.mutable_vars.iter().any(|var| {
1832        !closure_param_names.contains(var)
1833            && !body.iter().any(|stmt| {
1834                // Check if var is locally declared in the closure body
1835                matches!(stmt, HirStmt::Assign { target: AssignTarget::Symbol(n), .. } if n == var)
1836            })
1837    });
1838
1839    // DEPYLER-1160: Propagate return type annotation to returned variables
1840    // This enables target-typed inference for empty lists in nested functions
1841    // e.g., `result = []` gets type Vec<i32> when function returns list[int]
1842    propagate_return_type_to_vars(body, &mut ctx.var_types, ret_type);
1843
1844    // Generate body
1845    let body_tokens: Vec<proc_macro2::TokenStream> = body
1846        .iter()
1847        .map(|stmt| stmt.to_rust_tokens(ctx))
1848        .collect::<Result<Vec<_>>>()?;
1849
1850    // Exit scope before restoring context
1851    ctx.exit_scope();
1852
1853    // DEPYLER-99MODE-S9: Restore outer function's mutable_vars after nested function codegen.
1854    // The nested function's analyze_mutable_vars cleared and repopulated mutable_vars for
1855    // its own body. We must restore the outer function's analysis so that variables
1856    // declared after nested functions correctly get `let mut`.
1857    ctx.mutable_vars = saved_mutable_vars;
1858
1859    // Restore can_fail flag
1860    ctx.current_function_can_fail = saved_can_fail;
1861
1862    // DEPYLER-0731: Restore outer function's return type and is_main flag
1863    ctx.current_return_type = saved_return_type;
1864    ctx.is_main_function = saved_is_main;
1865
1866    // DEPYLER-0790: Check if this nested function is recursive (calls itself)
1867    // Recursive functions cannot be closures because closures can't reference themselves
1868    // For recursive nested functions that DON'T capture outer variables,
1869    // generate as `fn name(...)` instead of closure
1870    let is_recursive = is_nested_function_recursive(name, body);
1871
1872    // Get outer scope variables to check for captures
1873    // Collect all declared vars from all scopes
1874    let outer_vars: std::collections::HashSet<String> = ctx
1875        .declared_vars
1876        .iter()
1877        .flat_map(|scope| scope.iter().cloned())
1878        .collect();
1879    let has_captures = captures_outer_scope(params, body, &outer_vars);
1880
1881    // GH-70 FIX: Generate as closure instead of fn item
1882    // Closures can be returned as values and have better type inference
1883    // This fixes the issue where nested functions had all types defaulting to ()
1884    //
1885    // GH-70 CRITICAL FIX: Omit return type annotation for Type::Unknown
1886    // When no type annotation exists in Python, ret_type is Type::Unknown.
1887    // Previously, this defaulted to () in hir_type_to_tokens, causing explicit
1888    // `-> ()` in closure definition which conflicted with actual return values.
1889    // Solution: Omit return type entirely, allowing Rust's type inference to
1890    // determine correct return type from closure body.
1891    //
1892    // DEPYLER-0613: Support hoisting - if variable is already declared, use assignment
1893    let is_declared = ctx.is_declared(name);
1894
1895    // DEPYLER-0790: For recursive functions that DON'T capture outer variables,
1896    // generate as proper fn instead of closure
1897    // If recursive AND captures, we can't easily fix - keep as closure (will produce E0425)
1898    if is_recursive && !has_captures {
1899        // Declare the function name in context so sibling nested functions can detect it
1900        ctx.declare_var(name);
1901        return Ok(if matches!(ret_type, Type::Unknown) {
1902            quote! {
1903                fn #fn_name(#(#param_tokens),*) {
1904                    #(#body_tokens)*
1905                }
1906            }
1907        } else {
1908            quote! {
1909                fn #fn_name(#(#param_tokens),*) -> #return_type {
1910                    #(#body_tokens)*
1911                }
1912            }
1913        });
1914    }
1915
1916    // Declare the function name in context so sibling nested functions can detect it as a capture
1917    ctx.declare_var(name);
1918
1919    // DEPYLER-0783: Always use `move` for nested function closures
1920    // This is required when:
1921    // 1. The closure captures variables from outer scope AND is returned (E0373)
1922    // 2. For Copy types (i32, bool), move just copies - no harm
1923    // 3. For non-Copy types, move is required if captured and returned
1924    // Using `move` universally is safe and fixes the common closure capture issue
1925    //
1926    // DEPYLER-99MODE-S9: Use `let mut` for FnMut closures (those that mutate captured variables)
1927    let mutability = if closure_is_fn_mut {
1928        quote! { mut }
1929    } else {
1930        quote! {}
1931    };
1932    Ok(if matches!(ret_type, Type::Unknown) {
1933        if is_declared {
1934            quote! {
1935                #fn_name = move |#(#param_tokens),*| {
1936                    #(#body_tokens)*
1937                };
1938            }
1939        } else {
1940            quote! {
1941                let #mutability #fn_name = move |#(#param_tokens),*| {
1942                    #(#body_tokens)*
1943                };
1944            }
1945        }
1946    } else if is_declared {
1947        quote! {
1948            #fn_name = move |#(#param_tokens),*| -> #return_type {
1949                #(#body_tokens)*
1950            };
1951        }
1952    } else {
1953        quote! {
1954            let #mutability #fn_name = move |#(#param_tokens),*| -> #return_type {
1955                #(#body_tokens)*
1956            };
1957        }
1958    })
1959}
1960
1961/// DEPYLER-0790: Check if a nested function captures outer scope variables
1962/// Returns true if the function body references any variables that are NOT:
1963/// - The function's own parameters
1964/// - Local variables defined within the function
1965pub(crate) fn captures_outer_scope(
1966    params: &[crate::hir::HirParam],
1967    body: &[HirStmt],
1968    outer_vars: &std::collections::HashSet<String>,
1969) -> bool {
1970    use crate::hir::HirExpr;
1971
1972    // Collect parameter names
1973    let mut local_vars: std::collections::HashSet<&str> =
1974        params.iter().map(|p| p.name.as_str()).collect();
1975
1976    // Collect locally defined variables from assignments
1977    fn collect_local_vars<'a>(stmt: &'a HirStmt, locals: &mut std::collections::HashSet<&'a str>) {
1978        match stmt {
1979            HirStmt::Assign {
1980                target: crate::hir::AssignTarget::Symbol(name),
1981                ..
1982            } => {
1983                locals.insert(name.as_str());
1984            }
1985            HirStmt::Assign { .. } => {}
1986            HirStmt::For { target, body, .. } => {
1987                if let crate::hir::AssignTarget::Symbol(name) = target {
1988                    locals.insert(name.as_str());
1989                }
1990                for s in body {
1991                    collect_local_vars(s, locals);
1992                }
1993            }
1994            HirStmt::If {
1995                then_body,
1996                else_body,
1997                ..
1998            } => {
1999                for s in then_body {
2000                    collect_local_vars(s, locals);
2001                }
2002                if let Some(eb) = else_body {
2003                    for s in eb {
2004                        collect_local_vars(s, locals);
2005                    }
2006                }
2007            }
2008            HirStmt::While { body, .. } => {
2009                for s in body {
2010                    collect_local_vars(s, locals);
2011                }
2012            }
2013            HirStmt::With { body, target, .. } => {
2014                if let Some(t) = target {
2015                    locals.insert(t.as_str());
2016                }
2017                for s in body {
2018                    collect_local_vars(s, locals);
2019                }
2020            }
2021            HirStmt::Try {
2022                body,
2023                handlers,
2024                orelse,
2025                finalbody,
2026            } => {
2027                for s in body {
2028                    collect_local_vars(s, locals);
2029                }
2030                for h in handlers {
2031                    if let Some(name) = &h.name {
2032                        locals.insert(name.as_str());
2033                    }
2034                    for s in &h.body {
2035                        collect_local_vars(s, locals);
2036                    }
2037                }
2038                if let Some(els) = orelse {
2039                    for s in els {
2040                        collect_local_vars(s, locals);
2041                    }
2042                }
2043                if let Some(fin) = finalbody {
2044                    for s in fin {
2045                        collect_local_vars(s, locals);
2046                    }
2047                }
2048            }
2049            HirStmt::FunctionDef { name, .. } => {
2050                locals.insert(name.as_str());
2051            }
2052            HirStmt::Block(stmts) => {
2053                for s in stmts {
2054                    collect_local_vars(s, locals);
2055                }
2056            }
2057            _ => {}
2058        }
2059    }
2060
2061    // First pass: collect all locally defined variables
2062    for stmt in body {
2063        collect_local_vars(stmt, &mut local_vars);
2064    }
2065
2066    // Now check if body references any outer scope variables
2067    fn check_expr_for_capture(
2068        expr: &HirExpr,
2069        local_vars: &std::collections::HashSet<&str>,
2070        outer_vars: &std::collections::HashSet<String>,
2071    ) -> bool {
2072        match expr {
2073            HirExpr::Var(name) => {
2074                // If variable is not local and IS in outer scope, it's captured
2075                !local_vars.contains(name.as_str()) && outer_vars.contains(name)
2076            }
2077            HirExpr::Binary { left, right, .. } => {
2078                check_expr_for_capture(left, local_vars, outer_vars)
2079                    || check_expr_for_capture(right, local_vars, outer_vars)
2080            }
2081            HirExpr::Unary { operand, .. } => {
2082                check_expr_for_capture(operand, local_vars, outer_vars)
2083            }
2084            HirExpr::Call {
2085                func, args, kwargs, ..
2086            } => {
2087                // Check if calling a function defined in outer scope
2088                let captures_func =
2089                    !local_vars.contains(func.as_str()) && outer_vars.contains(func);
2090                captures_func
2091                    || args
2092                        .iter()
2093                        .any(|a| check_expr_for_capture(a, local_vars, outer_vars))
2094                    || kwargs
2095                        .iter()
2096                        .any(|(_, v)| check_expr_for_capture(v, local_vars, outer_vars))
2097            }
2098            HirExpr::DynamicCall {
2099                callee,
2100                args,
2101                kwargs,
2102            } => {
2103                check_expr_for_capture(callee, local_vars, outer_vars)
2104                    || args
2105                        .iter()
2106                        .any(|a| check_expr_for_capture(a, local_vars, outer_vars))
2107                    || kwargs
2108                        .iter()
2109                        .any(|(_, v)| check_expr_for_capture(v, local_vars, outer_vars))
2110            }
2111            HirExpr::MethodCall {
2112                object,
2113                args,
2114                kwargs,
2115                ..
2116            } => {
2117                check_expr_for_capture(object, local_vars, outer_vars)
2118                    || args
2119                        .iter()
2120                        .any(|a| check_expr_for_capture(a, local_vars, outer_vars))
2121                    || kwargs
2122                        .iter()
2123                        .any(|(_, v)| check_expr_for_capture(v, local_vars, outer_vars))
2124            }
2125            HirExpr::Attribute { value, .. } => {
2126                check_expr_for_capture(value, local_vars, outer_vars)
2127            }
2128            HirExpr::Index { base, index } => {
2129                check_expr_for_capture(base, local_vars, outer_vars)
2130                    || check_expr_for_capture(index, local_vars, outer_vars)
2131            }
2132            HirExpr::IfExpr { test, body, orelse } => {
2133                check_expr_for_capture(test, local_vars, outer_vars)
2134                    || check_expr_for_capture(body, local_vars, outer_vars)
2135                    || check_expr_for_capture(orelse, local_vars, outer_vars)
2136            }
2137            HirExpr::List(items)
2138            | HirExpr::Tuple(items)
2139            | HirExpr::Set(items)
2140            | HirExpr::FrozenSet(items) => items
2141                .iter()
2142                .any(|i| check_expr_for_capture(i, local_vars, outer_vars)),
2143            HirExpr::Dict(pairs) => pairs.iter().any(|(k, v)| {
2144                check_expr_for_capture(k, local_vars, outer_vars)
2145                    || check_expr_for_capture(v, local_vars, outer_vars)
2146            }),
2147            HirExpr::ListComp {
2148                element,
2149                generators,
2150            }
2151            | HirExpr::SetComp {
2152                element,
2153                generators,
2154            }
2155            | HirExpr::GeneratorExp {
2156                element,
2157                generators,
2158            } => {
2159                check_expr_for_capture(element, local_vars, outer_vars)
2160                    || generators.iter().any(|g| {
2161                        check_expr_for_capture(&g.iter, local_vars, outer_vars)
2162                            || g.conditions
2163                                .iter()
2164                                .any(|c| check_expr_for_capture(c, local_vars, outer_vars))
2165                    })
2166            }
2167            HirExpr::DictComp {
2168                key,
2169                value,
2170                generators,
2171            } => {
2172                check_expr_for_capture(key, local_vars, outer_vars)
2173                    || check_expr_for_capture(value, local_vars, outer_vars)
2174                    || generators.iter().any(|g| {
2175                        check_expr_for_capture(&g.iter, local_vars, outer_vars)
2176                            || g.conditions
2177                                .iter()
2178                                .any(|c| check_expr_for_capture(c, local_vars, outer_vars))
2179                    })
2180            }
2181            HirExpr::Lambda { body, .. } => check_expr_for_capture(body, local_vars, outer_vars),
2182            HirExpr::Await { value } => check_expr_for_capture(value, local_vars, outer_vars),
2183            HirExpr::Slice {
2184                base,
2185                start,
2186                stop,
2187                step,
2188            } => {
2189                check_expr_for_capture(base, local_vars, outer_vars)
2190                    || start
2191                        .as_ref()
2192                        .is_some_and(|e| check_expr_for_capture(e, local_vars, outer_vars))
2193                    || stop
2194                        .as_ref()
2195                        .is_some_and(|e| check_expr_for_capture(e, local_vars, outer_vars))
2196                    || step
2197                        .as_ref()
2198                        .is_some_and(|e| check_expr_for_capture(e, local_vars, outer_vars))
2199            }
2200            HirExpr::Borrow { expr, .. } => check_expr_for_capture(expr, local_vars, outer_vars),
2201            HirExpr::FString { parts } => parts.iter().any(|p| {
2202                if let crate::hir::FStringPart::Expr(e) = p {
2203                    check_expr_for_capture(e, local_vars, outer_vars)
2204                } else {
2205                    false
2206                }
2207            }),
2208            HirExpr::Yield { value } => value
2209                .as_ref()
2210                .is_some_and(|e| check_expr_for_capture(e, local_vars, outer_vars)),
2211            HirExpr::SortByKey {
2212                iterable,
2213                key_body,
2214                reverse_expr,
2215                ..
2216            } => {
2217                check_expr_for_capture(iterable, local_vars, outer_vars)
2218                    || check_expr_for_capture(key_body, local_vars, outer_vars)
2219                    || reverse_expr
2220                        .as_ref()
2221                        .is_some_and(|e| check_expr_for_capture(e, local_vars, outer_vars))
2222            }
2223            HirExpr::NamedExpr { value, .. } => {
2224                check_expr_for_capture(value, local_vars, outer_vars)
2225            }
2226            _ => false,
2227        }
2228    }
2229
2230    fn check_stmt_for_capture(
2231        stmt: &HirStmt,
2232        local_vars: &std::collections::HashSet<&str>,
2233        outer_vars: &std::collections::HashSet<String>,
2234    ) -> bool {
2235        match stmt {
2236            HirStmt::Expr(expr) | HirStmt::Return(Some(expr)) => {
2237                check_expr_for_capture(expr, local_vars, outer_vars)
2238            }
2239            HirStmt::Assign { value, .. } => check_expr_for_capture(value, local_vars, outer_vars),
2240            HirStmt::If {
2241                condition,
2242                then_body,
2243                else_body,
2244            } => {
2245                check_expr_for_capture(condition, local_vars, outer_vars)
2246                    || then_body
2247                        .iter()
2248                        .any(|s| check_stmt_for_capture(s, local_vars, outer_vars))
2249                    || else_body.as_ref().is_some_and(|b| {
2250                        b.iter()
2251                            .any(|s| check_stmt_for_capture(s, local_vars, outer_vars))
2252                    })
2253            }
2254            HirStmt::While { condition, body } => {
2255                check_expr_for_capture(condition, local_vars, outer_vars)
2256                    || body
2257                        .iter()
2258                        .any(|s| check_stmt_for_capture(s, local_vars, outer_vars))
2259            }
2260            HirStmt::For { iter, body, .. } => {
2261                check_expr_for_capture(iter, local_vars, outer_vars)
2262                    || body
2263                        .iter()
2264                        .any(|s| check_stmt_for_capture(s, local_vars, outer_vars))
2265            }
2266            HirStmt::With { context, body, .. } => {
2267                check_expr_for_capture(context, local_vars, outer_vars)
2268                    || body
2269                        .iter()
2270                        .any(|s| check_stmt_for_capture(s, local_vars, outer_vars))
2271            }
2272            HirStmt::Try {
2273                body,
2274                handlers,
2275                orelse,
2276                finalbody,
2277            } => {
2278                body.iter()
2279                    .any(|s| check_stmt_for_capture(s, local_vars, outer_vars))
2280                    || handlers.iter().any(|h| {
2281                        h.body
2282                            .iter()
2283                            .any(|s| check_stmt_for_capture(s, local_vars, outer_vars))
2284                    })
2285                    || orelse.as_ref().is_some_and(|b| {
2286                        b.iter()
2287                            .any(|s| check_stmt_for_capture(s, local_vars, outer_vars))
2288                    })
2289                    || finalbody.as_ref().is_some_and(|b| {
2290                        b.iter()
2291                            .any(|s| check_stmt_for_capture(s, local_vars, outer_vars))
2292                    })
2293            }
2294            HirStmt::FunctionDef { body, .. } => body
2295                .iter()
2296                .any(|s| check_stmt_for_capture(s, local_vars, outer_vars)),
2297            HirStmt::Block(stmts) => stmts
2298                .iter()
2299                .any(|s| check_stmt_for_capture(s, local_vars, outer_vars)),
2300            HirStmt::Assert { test, msg } => {
2301                check_expr_for_capture(test, local_vars, outer_vars)
2302                    || msg
2303                        .as_ref()
2304                        .is_some_and(|m| check_expr_for_capture(m, local_vars, outer_vars))
2305            }
2306            HirStmt::Raise { exception, cause } => {
2307                exception
2308                    .as_ref()
2309                    .is_some_and(|e| check_expr_for_capture(e, local_vars, outer_vars))
2310                    || cause
2311                        .as_ref()
2312                        .is_some_and(|c| check_expr_for_capture(c, local_vars, outer_vars))
2313            }
2314            _ => false,
2315        }
2316    }
2317
2318    body.iter()
2319        .any(|stmt| check_stmt_for_capture(stmt, &local_vars, outer_vars))
2320}
2321
2322#[cfg(test)]
2323mod tests {
2324    use super::*;
2325    use crate::hir::*;
2326    use std::collections::HashSet;
2327
2328    // === extract_fields_from_expr tests ===
2329
2330    #[test]
2331    fn test_extract_fields_attribute_match() {
2332        let mut fields = HashSet::new();
2333        let expr = HirExpr::Attribute {
2334            value: Box::new(HirExpr::Var("args".to_string())),
2335            attr: "name".to_string(),
2336        };
2337        extract_fields_from_expr(&expr, "args", "command", &mut fields);
2338        assert!(fields.contains("name"));
2339    }
2340
2341    #[test]
2342    fn test_extract_fields_skips_dest_field() {
2343        let mut fields = HashSet::new();
2344        let expr = HirExpr::Attribute {
2345            value: Box::new(HirExpr::Var("args".to_string())),
2346            attr: "command".to_string(),
2347        };
2348        extract_fields_from_expr(&expr, "args", "command", &mut fields);
2349        assert!(fields.is_empty());
2350    }
2351
2352    #[test]
2353    fn test_extract_fields_wrong_var_name() {
2354        let mut fields = HashSet::new();
2355        let expr = HirExpr::Attribute {
2356            value: Box::new(HirExpr::Var("config".to_string())),
2357            attr: "name".to_string(),
2358        };
2359        extract_fields_from_expr(&expr, "args", "command", &mut fields);
2360        assert!(fields.is_empty());
2361    }
2362
2363    #[test]
2364    fn test_extract_fields_in_call_args() {
2365        let mut fields = HashSet::new();
2366        let expr = HirExpr::Call {
2367            func: "process".to_string(),
2368            args: vec![HirExpr::Attribute {
2369                value: Box::new(HirExpr::Var("args".to_string())),
2370                attr: "file".to_string(),
2371            }],
2372            kwargs: vec![],
2373        };
2374        extract_fields_from_expr(&expr, "args", "command", &mut fields);
2375        assert!(fields.contains("file"));
2376    }
2377
2378    #[test]
2379    fn test_extract_fields_in_binary_expr() {
2380        let mut fields = HashSet::new();
2381        let expr = HirExpr::Binary {
2382            op: BinOp::Add,
2383            left: Box::new(HirExpr::Attribute {
2384                value: Box::new(HirExpr::Var("args".to_string())),
2385                attr: "host".to_string(),
2386            }),
2387            right: Box::new(HirExpr::Attribute {
2388                value: Box::new(HirExpr::Var("args".to_string())),
2389                attr: "port".to_string(),
2390            }),
2391        };
2392        extract_fields_from_expr(&expr, "args", "command", &mut fields);
2393        assert!(fields.contains("host"));
2394        assert!(fields.contains("port"));
2395    }
2396
2397    #[test]
2398    fn test_extract_fields_in_unary_expr() {
2399        let mut fields = HashSet::new();
2400        let expr = HirExpr::Unary {
2401            op: UnaryOp::Not,
2402            operand: Box::new(HirExpr::Attribute {
2403                value: Box::new(HirExpr::Var("args".to_string())),
2404                attr: "verbose".to_string(),
2405            }),
2406        };
2407        extract_fields_from_expr(&expr, "args", "command", &mut fields);
2408        assert!(fields.contains("verbose"));
2409    }
2410
2411    #[test]
2412    fn test_extract_fields_in_if_expr() {
2413        let mut fields = HashSet::new();
2414        let expr = HirExpr::IfExpr {
2415            test: Box::new(HirExpr::Attribute {
2416                value: Box::new(HirExpr::Var("args".to_string())),
2417                attr: "flag".to_string(),
2418            }),
2419            body: Box::new(HirExpr::Attribute {
2420                value: Box::new(HirExpr::Var("args".to_string())),
2421                attr: "val_a".to_string(),
2422            }),
2423            orelse: Box::new(HirExpr::Attribute {
2424                value: Box::new(HirExpr::Var("args".to_string())),
2425                attr: "val_b".to_string(),
2426            }),
2427        };
2428        extract_fields_from_expr(&expr, "args", "command", &mut fields);
2429        assert_eq!(fields.len(), 3);
2430        assert!(fields.contains("flag"));
2431        assert!(fields.contains("val_a"));
2432        assert!(fields.contains("val_b"));
2433    }
2434
2435    #[test]
2436    fn test_extract_fields_in_index() {
2437        let mut fields = HashSet::new();
2438        let expr = HirExpr::Index {
2439            base: Box::new(HirExpr::Attribute {
2440                value: Box::new(HirExpr::Var("args".to_string())),
2441                attr: "items".to_string(),
2442            }),
2443            index: Box::new(HirExpr::Literal(Literal::Int(0))),
2444        };
2445        extract_fields_from_expr(&expr, "args", "command", &mut fields);
2446        assert!(fields.contains("items"));
2447    }
2448
2449    #[test]
2450    fn test_extract_fields_in_list() {
2451        let mut fields = HashSet::new();
2452        let expr = HirExpr::List(vec![HirExpr::Attribute {
2453            value: Box::new(HirExpr::Var("args".to_string())),
2454            attr: "item".to_string(),
2455        }]);
2456        extract_fields_from_expr(&expr, "args", "command", &mut fields);
2457        assert!(fields.contains("item"));
2458    }
2459
2460    #[test]
2461    fn test_extract_fields_in_tuple() {
2462        let mut fields = HashSet::new();
2463        let expr = HirExpr::Tuple(vec![HirExpr::Attribute {
2464            value: Box::new(HirExpr::Var("args".to_string())),
2465            attr: "x".to_string(),
2466        }]);
2467        extract_fields_from_expr(&expr, "args", "command", &mut fields);
2468        assert!(fields.contains("x"));
2469    }
2470
2471    #[test]
2472    fn test_extract_fields_in_set() {
2473        let mut fields = HashSet::new();
2474        let expr = HirExpr::Set(vec![HirExpr::Attribute {
2475            value: Box::new(HirExpr::Var("args".to_string())),
2476            attr: "tag".to_string(),
2477        }]);
2478        extract_fields_from_expr(&expr, "args", "command", &mut fields);
2479        assert!(fields.contains("tag"));
2480    }
2481
2482    #[test]
2483    fn test_extract_fields_in_dict() {
2484        let mut fields = HashSet::new();
2485        let expr = HirExpr::Dict(vec![(
2486            HirExpr::Literal(Literal::String("key".to_string())),
2487            HirExpr::Attribute {
2488                value: Box::new(HirExpr::Var("args".to_string())),
2489                attr: "value".to_string(),
2490            },
2491        )]);
2492        extract_fields_from_expr(&expr, "args", "command", &mut fields);
2493        assert!(fields.contains("value"));
2494    }
2495
2496    #[test]
2497    fn test_extract_fields_in_method_call() {
2498        let mut fields = HashSet::new();
2499        let expr = HirExpr::MethodCall {
2500            object: Box::new(HirExpr::Attribute {
2501                value: Box::new(HirExpr::Var("args".to_string())),
2502                attr: "data".to_string(),
2503            }),
2504            method: "upper".to_string(),
2505            args: vec![],
2506            kwargs: vec![],
2507        };
2508        extract_fields_from_expr(&expr, "args", "command", &mut fields);
2509        assert!(fields.contains("data"));
2510    }
2511
2512    #[test]
2513    fn test_extract_fields_in_fstring() {
2514        let mut fields = HashSet::new();
2515        let expr = HirExpr::FString {
2516            parts: vec![
2517                crate::hir::FStringPart::Literal("Hello ".to_string()),
2518                crate::hir::FStringPart::Expr(Box::new(HirExpr::Attribute {
2519                    value: Box::new(HirExpr::Var("args".to_string())),
2520                    attr: "user".to_string(),
2521                })),
2522            ],
2523        };
2524        extract_fields_from_expr(&expr, "args", "command", &mut fields);
2525        assert!(fields.contains("user"));
2526    }
2527
2528    #[test]
2529    fn test_extract_fields_literal_no_match() {
2530        let mut fields = HashSet::new();
2531        let expr = HirExpr::Literal(Literal::Int(42));
2532        extract_fields_from_expr(&expr, "args", "command", &mut fields);
2533        assert!(fields.is_empty());
2534    }
2535
2536    // === extract_fields_recursive tests ===
2537
2538    #[test]
2539    fn test_extract_fields_recursive_empty() {
2540        let mut fields = HashSet::new();
2541        extract_fields_recursive(&[], "args", "command", &mut fields);
2542        assert!(fields.is_empty());
2543    }
2544
2545    #[test]
2546    fn test_extract_fields_recursive_expr_stmt() {
2547        let mut fields = HashSet::new();
2548        let stmts = vec![HirStmt::Expr(HirExpr::Attribute {
2549            value: Box::new(HirExpr::Var("args".to_string())),
2550            attr: "output".to_string(),
2551        })];
2552        extract_fields_recursive(&stmts, "args", "command", &mut fields);
2553        assert!(fields.contains("output"));
2554    }
2555
2556    #[test]
2557    fn test_extract_fields_recursive_assign() {
2558        let mut fields = HashSet::new();
2559        let stmts = vec![HirStmt::Assign {
2560            target: AssignTarget::Symbol("x".to_string()),
2561            value: HirExpr::Attribute {
2562                value: Box::new(HirExpr::Var("args".to_string())),
2563                attr: "count".to_string(),
2564            },
2565            type_annotation: None,
2566        }];
2567        extract_fields_recursive(&stmts, "args", "command", &mut fields);
2568        assert!(fields.contains("count"));
2569    }
2570
2571    #[test]
2572    fn test_extract_fields_recursive_if_with_condition() {
2573        let mut fields = HashSet::new();
2574        let stmts = vec![HirStmt::If {
2575            condition: HirExpr::Attribute {
2576                value: Box::new(HirExpr::Var("args".to_string())),
2577                attr: "debug".to_string(),
2578            },
2579            then_body: vec![HirStmt::Expr(HirExpr::Attribute {
2580                value: Box::new(HirExpr::Var("args".to_string())),
2581                attr: "level".to_string(),
2582            })],
2583            else_body: Some(vec![HirStmt::Expr(HirExpr::Attribute {
2584                value: Box::new(HirExpr::Var("args".to_string())),
2585                attr: "quiet".to_string(),
2586            })]),
2587        }];
2588        extract_fields_recursive(&stmts, "args", "command", &mut fields);
2589        assert!(fields.contains("debug"));
2590        assert!(fields.contains("level"));
2591        assert!(fields.contains("quiet"));
2592    }
2593
2594    #[test]
2595    fn test_extract_fields_recursive_while() {
2596        let mut fields = HashSet::new();
2597        let stmts = vec![HirStmt::While {
2598            condition: HirExpr::Attribute {
2599                value: Box::new(HirExpr::Var("args".to_string())),
2600                attr: "running".to_string(),
2601            },
2602            body: vec![],
2603        }];
2604        extract_fields_recursive(&stmts, "args", "command", &mut fields);
2605        assert!(fields.contains("running"));
2606    }
2607
2608    #[test]
2609    fn test_extract_fields_recursive_for() {
2610        let mut fields = HashSet::new();
2611        let stmts = vec![HirStmt::For {
2612            target: AssignTarget::Symbol("item".to_string()),
2613            iter: HirExpr::Attribute {
2614                value: Box::new(HirExpr::Var("args".to_string())),
2615                attr: "items".to_string(),
2616            },
2617            body: vec![],
2618        }];
2619        extract_fields_recursive(&stmts, "args", "command", &mut fields);
2620        assert!(fields.contains("items"));
2621    }
2622
2623    #[test]
2624    fn test_extract_fields_recursive_try() {
2625        let mut fields = HashSet::new();
2626        let stmts = vec![HirStmt::Try {
2627            body: vec![HirStmt::Expr(HirExpr::Attribute {
2628                value: Box::new(HirExpr::Var("args".to_string())),
2629                attr: "path".to_string(),
2630            })],
2631            handlers: vec![ExceptHandler {
2632                exception_type: None,
2633                name: None,
2634                body: vec![HirStmt::Expr(HirExpr::Attribute {
2635                    value: Box::new(HirExpr::Var("args".to_string())),
2636                    attr: "fallback".to_string(),
2637                })],
2638            }],
2639            orelse: Some(vec![HirStmt::Expr(HirExpr::Attribute {
2640                value: Box::new(HirExpr::Var("args".to_string())),
2641                attr: "success".to_string(),
2642            })]),
2643            finalbody: Some(vec![HirStmt::Expr(HirExpr::Attribute {
2644                value: Box::new(HirExpr::Var("args".to_string())),
2645                attr: "cleanup".to_string(),
2646            })]),
2647        }];
2648        extract_fields_recursive(&stmts, "args", "command", &mut fields);
2649        assert!(fields.contains("path"));
2650        assert!(fields.contains("fallback"));
2651        assert!(fields.contains("success"));
2652        assert!(fields.contains("cleanup"));
2653    }
2654
2655    #[test]
2656    fn test_extract_fields_recursive_with() {
2657        let mut fields = HashSet::new();
2658        let stmts = vec![HirStmt::With {
2659            context: HirExpr::Attribute {
2660                value: Box::new(HirExpr::Var("args".to_string())),
2661                attr: "resource".to_string(),
2662            },
2663            target: Some("r".to_string()),
2664            body: vec![],
2665            is_async: false,
2666        }];
2667        extract_fields_recursive(&stmts, "args", "command", &mut fields);
2668        assert!(fields.contains("resource"));
2669    }
2670
2671    #[test]
2672    fn test_extract_fields_recursive_custom_dest_field() {
2673        let mut fields = HashSet::new();
2674        let stmts = vec![HirStmt::Expr(HirExpr::Attribute {
2675            value: Box::new(HirExpr::Var("args".to_string())),
2676            attr: "action".to_string(),
2677        })];
2678        extract_fields_recursive(&stmts, "args", "action", &mut fields);
2679        // "action" is the dest_field so it should be filtered out
2680        assert!(fields.is_empty());
2681    }
2682
2683    // === is_subcommand_check tests ===
2684
2685    #[test]
2686    fn test_is_subcommand_check_direct_match() {
2687        let ctx = CodeGenContext::default();
2688        let expr = HirExpr::Binary {
2689            op: BinOp::Eq,
2690            left: Box::new(HirExpr::Attribute {
2691                value: Box::new(HirExpr::Var("args".to_string())),
2692                attr: "command".to_string(),
2693            }),
2694            right: Box::new(HirExpr::Literal(Literal::String("init".to_string()))),
2695        };
2696        let result = is_subcommand_check(&expr, "command", &ctx);
2697        assert_eq!(result, Some("init".to_string()));
2698    }
2699
2700    #[test]
2701    fn test_is_subcommand_check_custom_dest() {
2702        let ctx = CodeGenContext::default();
2703        let expr = HirExpr::Binary {
2704            op: BinOp::Eq,
2705            left: Box::new(HirExpr::Attribute {
2706                value: Box::new(HirExpr::Var("args".to_string())),
2707                attr: "action".to_string(),
2708            }),
2709            right: Box::new(HirExpr::Literal(Literal::String("deploy".to_string()))),
2710        };
2711        let result = is_subcommand_check(&expr, "action", &ctx);
2712        assert_eq!(result, Some("deploy".to_string()));
2713    }
2714
2715    #[test]
2716    fn test_is_subcommand_check_wrong_dest() {
2717        let ctx = CodeGenContext::default();
2718        let expr = HirExpr::Binary {
2719            op: BinOp::Eq,
2720            left: Box::new(HirExpr::Attribute {
2721                value: Box::new(HirExpr::Var("args".to_string())),
2722                attr: "name".to_string(),
2723            }),
2724            right: Box::new(HirExpr::Literal(Literal::String("test".to_string()))),
2725        };
2726        let result = is_subcommand_check(&expr, "command", &ctx);
2727        assert_eq!(result, None);
2728    }
2729
2730    #[test]
2731    fn test_is_subcommand_check_not_eq_op() {
2732        let ctx = CodeGenContext::default();
2733        let expr = HirExpr::Binary {
2734            op: BinOp::Lt,
2735            left: Box::new(HirExpr::Attribute {
2736                value: Box::new(HirExpr::Var("args".to_string())),
2737                attr: "command".to_string(),
2738            }),
2739            right: Box::new(HirExpr::Literal(Literal::String("init".to_string()))),
2740        };
2741        let result = is_subcommand_check(&expr, "command", &ctx);
2742        assert_eq!(result, None);
2743    }
2744
2745    #[test]
2746    fn test_is_subcommand_check_not_string_literal() {
2747        let ctx = CodeGenContext::default();
2748        let expr = HirExpr::Binary {
2749            op: BinOp::Eq,
2750            left: Box::new(HirExpr::Attribute {
2751                value: Box::new(HirExpr::Var("args".to_string())),
2752                attr: "command".to_string(),
2753            }),
2754            right: Box::new(HirExpr::Literal(Literal::Int(42))),
2755        };
2756        let result = is_subcommand_check(&expr, "command", &ctx);
2757        assert_eq!(result, None);
2758    }
2759
2760    #[test]
2761    fn test_is_subcommand_check_cse_temp() {
2762        let mut ctx = CodeGenContext::default();
2763        ctx.cse_subcommand_temps
2764            .insert("_cse_0".to_string(), "build".to_string());
2765        let expr = HirExpr::Var("_cse_0".to_string());
2766        let result = is_subcommand_check(&expr, "command", &ctx);
2767        assert_eq!(result, Some("build".to_string()));
2768    }
2769
2770    #[test]
2771    fn test_is_subcommand_check_unknown_cse_temp() {
2772        let ctx = CodeGenContext::default();
2773        let expr = HirExpr::Var("_cse_99".to_string());
2774        let result = is_subcommand_check(&expr, "command", &ctx);
2775        assert_eq!(result, None);
2776    }
2777
2778    #[test]
2779    fn test_is_subcommand_check_literal_no_match() {
2780        let ctx = CodeGenContext::default();
2781        let expr = HirExpr::Literal(Literal::Int(42));
2782        let result = is_subcommand_check(&expr, "command", &ctx);
2783        assert_eq!(result, None);
2784    }
2785
2786    // === captures_outer_scope tests ===
2787
2788    #[test]
2789    fn test_captures_outer_scope_no_capture() {
2790        let params = vec![HirParam::new("x".to_string(), Type::Int)];
2791        let body = vec![HirStmt::Return(Some(HirExpr::Var("x".to_string())))];
2792        let outer_vars: HashSet<String> = ["y".to_string()].into_iter().collect();
2793        assert!(!captures_outer_scope(&params, &body, &outer_vars));
2794    }
2795
2796    #[test]
2797    fn test_captures_outer_scope_uses_outer_var() {
2798        let params = vec![HirParam::new("x".to_string(), Type::Int)];
2799        let body = vec![HirStmt::Return(Some(HirExpr::Binary {
2800            op: BinOp::Add,
2801            left: Box::new(HirExpr::Var("x".to_string())),
2802            right: Box::new(HirExpr::Var("y".to_string())),
2803        }))];
2804        let outer_vars: HashSet<String> = ["y".to_string()].into_iter().collect();
2805        assert!(captures_outer_scope(&params, &body, &outer_vars));
2806    }
2807
2808    #[test]
2809    fn test_captures_outer_scope_locally_assigned() {
2810        let params = vec![];
2811        let body = vec![
2812            HirStmt::Assign {
2813                target: AssignTarget::Symbol("y".to_string()),
2814                value: HirExpr::Literal(Literal::Int(10)),
2815                type_annotation: None,
2816            },
2817            HirStmt::Return(Some(HirExpr::Var("y".to_string()))),
2818        ];
2819        let outer_vars: HashSet<String> = ["y".to_string()].into_iter().collect();
2820        // y is locally assigned, so NOT captured from outer
2821        assert!(!captures_outer_scope(&params, &body, &outer_vars));
2822    }
2823
2824    #[test]
2825    fn test_captures_outer_scope_empty_body() {
2826        let params = vec![];
2827        let body = vec![];
2828        let outer_vars: HashSet<String> = ["z".to_string()].into_iter().collect();
2829        assert!(!captures_outer_scope(&params, &body, &outer_vars));
2830    }
2831
2832    #[test]
2833    fn test_captures_outer_scope_empty_outer() {
2834        let params = vec![];
2835        let body = vec![HirStmt::Expr(HirExpr::Var("x".to_string()))];
2836        let outer_vars: HashSet<String> = HashSet::new();
2837        assert!(!captures_outer_scope(&params, &body, &outer_vars));
2838    }
2839
2840    // === Transpile-based integration tests ===
2841
2842    fn transpile(python_code: &str) -> String {
2843        use crate::ast_bridge::AstBridge;
2844        use crate::rust_gen::generate_rust_file;
2845        use crate::type_mapper::TypeMapper;
2846        use rustpython_parser::{parse, Mode};
2847
2848        let ast = parse(python_code, Mode::Module, "<test>").expect("parse");
2849        let (module, _) = AstBridge::new()
2850            .with_source(python_code.to_string())
2851            .python_to_hir(ast)
2852            .expect("hir");
2853        let tm = TypeMapper::default();
2854        let (result, _) = generate_rust_file(&module, &tm).expect("codegen");
2855        result
2856    }
2857
2858    #[test]
2859    fn test_transpile_try_except_basic() {
2860        let code = r#"
2861def safe_div(a: int, b: int) -> int:
2862    try:
2863        result = a // b
2864        return result
2865    except ZeroDivisionError:
2866        return 0
2867"#;
2868        let rust = transpile(code);
2869        assert!(rust.contains("fn safe_div"));
2870    }
2871
2872    #[test]
2873    fn test_transpile_try_except_finally() {
2874        let code = r#"
2875def cleanup_action() -> str:
2876    result = ""
2877    try:
2878        result = "ok"
2879    except ValueError:
2880        result = "error"
2881    finally:
2882        result = result + " done"
2883    return result
2884"#;
2885        let rust = transpile(code);
2886        assert!(rust.contains("fn cleanup_action"));
2887    }
2888
2889    #[test]
2890    fn test_transpile_try_except_multiple_handlers() {
2891        let code = r#"
2892def parse_value(s: str) -> int:
2893    try:
2894        return int(s)
2895    except ValueError:
2896        return -1
2897    except TypeError:
2898        return -2
2899"#;
2900        let rust = transpile(code);
2901        assert!(rust.contains("fn parse_value"));
2902    }
2903
2904    #[test]
2905    fn test_transpile_nested_function_with_closure() {
2906        let code = r#"
2907def outer(x: int) -> int:
2908    def inner(y: int) -> int:
2909        return x + y
2910    return inner(10)
2911"#;
2912        let rust = transpile(code);
2913        assert!(rust.contains("fn outer"));
2914    }
2915
2916    // === Additional transpile-based coverage tests ===
2917
2918    #[test]
2919    fn test_transpile_try_bare_except() {
2920        let code = r#"
2921def risky() -> str:
2922    try:
2923        return "ok"
2924    except:
2925        return "error"
2926"#;
2927        let rust = transpile(code);
2928        assert!(rust.contains("fn risky"));
2929    }
2930
2931    #[test]
2932    fn test_transpile_try_else_clause() {
2933        let code = r#"
2934def try_else(x: int) -> str:
2935    try:
2936        result = x + 1
2937    except ValueError:
2938        return "error"
2939    else:
2940        return "ok"
2941    return "done"
2942"#;
2943        let rust = transpile(code);
2944        assert!(rust.contains("fn try_else"));
2945    }
2946
2947    #[test]
2948    fn test_transpile_try_exception_variable() {
2949        let code = r#"
2950def catch_named(s: str) -> str:
2951    try:
2952        return s
2953    except ValueError as e:
2954        return "caught"
2955"#;
2956        let rust = transpile(code);
2957        assert!(rust.contains("fn catch_named"));
2958    }
2959
2960    #[test]
2961    fn test_transpile_try_reraise() {
2962        let code = r#"
2963def reraise(x: int) -> int:
2964    try:
2965        return x
2966    except ValueError:
2967        raise
2968"#;
2969        let rust = transpile(code);
2970        assert!(rust.contains("fn reraise"));
2971    }
2972
2973    #[test]
2974    fn test_transpile_nested_try() {
2975        let code = r#"
2976def nested_try() -> str:
2977    try:
2978        try:
2979            return "inner"
2980        except TypeError:
2981            return "inner error"
2982    except ValueError:
2983        return "outer error"
2984"#;
2985        let rust = transpile(code);
2986        assert!(rust.contains("fn nested_try"));
2987    }
2988
2989    #[test]
2990    fn test_transpile_try_with_return_in_finally() {
2991        let code = r#"
2992def finally_return() -> str:
2993    try:
2994        result = "ok"
2995    except ValueError:
2996        result = "error"
2997    finally:
2998        return result
2999"#;
3000        let rust = transpile(code);
3001        assert!(rust.contains("fn finally_return"));
3002    }
3003
3004    #[test]
3005    fn test_transpile_nested_function_no_capture() {
3006        let code = r#"
3007def outer() -> int:
3008    def inner(x: int) -> int:
3009        return x * 2
3010    return inner(5)
3011"#;
3012        let rust = transpile(code);
3013        assert!(rust.contains("fn outer"));
3014    }
3015
3016    #[test]
3017    fn test_transpile_nested_function_multiple() {
3018        let code = r#"
3019def parent() -> int:
3020    def add(a: int, b: int) -> int:
3021        return a + b
3022    def mul(a: int, b: int) -> int:
3023        return a * b
3024    return add(2, 3) + mul(4, 5)
3025"#;
3026        let rust = transpile(code);
3027        assert!(
3028            rust.contains("fn parent"),
3029            "Should contain parent function: {}",
3030            rust
3031        );
3032    }
3033
3034    #[test]
3035    fn test_transpile_try_with_assignment() {
3036        let code = r#"
3037def parse_int(s: str) -> int:
3038    try:
3039        n = int(s)
3040        return n
3041    except ValueError:
3042        return 0
3043"#;
3044        let rust = transpile(code);
3045        assert!(rust.contains("fn parse_int"));
3046    }
3047
3048    // === captures_outer_scope additional tests ===
3049
3050    #[test]
3051    fn test_captures_outer_scope_in_for_body() {
3052        let params = vec![HirParam::new("x".to_string(), Type::Int)];
3053        let body = vec![HirStmt::For {
3054            target: AssignTarget::Symbol("i".to_string()),
3055            iter: HirExpr::Var("outer_list".to_string()),
3056            body: vec![HirStmt::Expr(HirExpr::Var("x".to_string()))],
3057        }];
3058        let outer_vars: HashSet<String> = ["outer_list".to_string()].into_iter().collect();
3059        assert!(captures_outer_scope(&params, &body, &outer_vars));
3060    }
3061
3062    #[test]
3063    fn test_captures_outer_scope_param_shadows_outer() {
3064        let params = vec![HirParam::new("y".to_string(), Type::Int)];
3065        let body = vec![HirStmt::Return(Some(HirExpr::Var("y".to_string())))];
3066        let outer_vars: HashSet<String> = ["y".to_string()].into_iter().collect();
3067        // y is a param, so it shadows the outer var - not a capture
3068        assert!(!captures_outer_scope(&params, &body, &outer_vars));
3069    }
3070
3071    #[test]
3072    fn test_captures_outer_scope_in_if_condition() {
3073        let params = vec![];
3074        let body = vec![HirStmt::If {
3075            condition: HirExpr::Var("threshold".to_string()),
3076            then_body: vec![],
3077            else_body: None,
3078        }];
3079        let outer_vars: HashSet<String> = ["threshold".to_string()].into_iter().collect();
3080        assert!(captures_outer_scope(&params, &body, &outer_vars));
3081    }
3082
3083    #[test]
3084    fn test_captures_outer_scope_in_while_condition() {
3085        let params = vec![];
3086        let body = vec![HirStmt::While {
3087            condition: HirExpr::Var("running".to_string()),
3088            body: vec![],
3089        }];
3090        let outer_vars: HashSet<String> = ["running".to_string()].into_iter().collect();
3091        assert!(captures_outer_scope(&params, &body, &outer_vars));
3092    }
3093
3094    // === extract_fields_from_expr additional tests ===
3095
3096    #[test]
3097    fn test_extract_fields_in_comparison() {
3098        let mut fields = HashSet::new();
3099        let expr = HirExpr::Binary {
3100            op: BinOp::Gt,
3101            left: Box::new(HirExpr::Attribute {
3102                value: Box::new(HirExpr::Var("args".to_string())),
3103                attr: "threshold".to_string(),
3104            }),
3105            right: Box::new(HirExpr::Literal(Literal::Int(0))),
3106        };
3107        extract_fields_from_expr(&expr, "args", "command", &mut fields);
3108        assert!(fields.contains("threshold"));
3109    }
3110
3111    #[test]
3112    fn test_extract_fields_nested_attribute_no_match() {
3113        // Nested attributes (args.config.name) - only direct args.X is extracted
3114        let mut fields = HashSet::new();
3115        let expr = HirExpr::Attribute {
3116            value: Box::new(HirExpr::Attribute {
3117                value: Box::new(HirExpr::Var("args".to_string())),
3118                attr: "config".to_string(),
3119            }),
3120            attr: "name".to_string(),
3121        };
3122        extract_fields_from_expr(&expr, "args", "command", &mut fields);
3123        // Only direct args.X patterns are captured
3124        assert!(fields.is_empty());
3125    }
3126
3127    #[test]
3128    fn test_extract_fields_in_method_args() {
3129        let mut fields = HashSet::new();
3130        let expr = HirExpr::MethodCall {
3131            object: Box::new(HirExpr::Var("obj".to_string())),
3132            method: "process".to_string(),
3133            args: vec![HirExpr::Attribute {
3134                value: Box::new(HirExpr::Var("args".to_string())),
3135                attr: "input".to_string(),
3136            }],
3137            kwargs: vec![],
3138        };
3139        extract_fields_from_expr(&expr, "args", "command", &mut fields);
3140        assert!(fields.contains("input"));
3141    }
3142
3143    // === extract_fields_recursive additional tests ===
3144
3145    #[test]
3146    fn test_extract_fields_recursive_return_stmt_not_handled() {
3147        // Return statements are handled by the catch-all `_ => {}` in extract_fields_recursive
3148        // so fields in return expressions are not extracted
3149        let mut fields = HashSet::new();
3150        let stmts = vec![HirStmt::Return(Some(HirExpr::Attribute {
3151            value: Box::new(HirExpr::Var("args".to_string())),
3152            attr: "result".to_string(),
3153        }))];
3154        extract_fields_recursive(&stmts, "args", "command", &mut fields);
3155        assert!(fields.is_empty());
3156    }
3157
3158    #[test]
3159    fn test_extract_fields_recursive_return_none() {
3160        let mut fields = HashSet::new();
3161        let stmts = vec![HirStmt::Return(None)];
3162        extract_fields_recursive(&stmts, "args", "command", &mut fields);
3163        assert!(fields.is_empty());
3164    }
3165
3166    #[test]
3167    fn test_extract_fields_recursive_if_no_else() {
3168        let mut fields = HashSet::new();
3169        let stmts = vec![HirStmt::If {
3170            condition: HirExpr::Literal(Literal::Bool(true)),
3171            then_body: vec![HirStmt::Expr(HirExpr::Attribute {
3172                value: Box::new(HirExpr::Var("args".to_string())),
3173                attr: "only_then".to_string(),
3174            })],
3175            else_body: None,
3176        }];
3177        extract_fields_recursive(&stmts, "args", "command", &mut fields);
3178        assert!(fields.contains("only_then"));
3179    }
3180
3181    #[test]
3182    fn test_extract_fields_recursive_multiple_stmts() {
3183        let mut fields = HashSet::new();
3184        let stmts = vec![
3185            HirStmt::Expr(HirExpr::Attribute {
3186                value: Box::new(HirExpr::Var("args".to_string())),
3187                attr: "first".to_string(),
3188            }),
3189            HirStmt::Expr(HirExpr::Attribute {
3190                value: Box::new(HirExpr::Var("args".to_string())),
3191                attr: "second".to_string(),
3192            }),
3193        ];
3194        extract_fields_recursive(&stmts, "args", "command", &mut fields);
3195        assert!(fields.contains("first"));
3196        assert!(fields.contains("second"));
3197    }
3198
3199    // === is_subcommand_check additional tests ===
3200
3201    #[test]
3202    fn test_is_subcommand_check_non_binary() {
3203        let ctx = CodeGenContext::default();
3204        let expr = HirExpr::Var("something".to_string());
3205        let result = is_subcommand_check(&expr, "command", &ctx);
3206        assert_eq!(result, None);
3207    }
3208
3209    #[test]
3210    fn test_is_subcommand_check_non_attribute_left() {
3211        let ctx = CodeGenContext::default();
3212        let expr = HirExpr::Binary {
3213            op: BinOp::Eq,
3214            left: Box::new(HirExpr::Var("x".to_string())),
3215            right: Box::new(HirExpr::Literal(Literal::String("test".to_string()))),
3216        };
3217        let result = is_subcommand_check(&expr, "command", &ctx);
3218        assert_eq!(result, None);
3219    }
3220
3221    // ================================================================
3222    // Transpile-based tests for with statement / context managers
3223    // ================================================================
3224
3225    #[test]
3226    fn test_transpile_with_open_file() {
3227        let code = r#"
3228def read_file(path: str) -> str:
3229    with open(path) as f:
3230        return f.read()
3231"#;
3232        let rust = transpile(code);
3233        assert!(rust.contains("fn read_file"), "output: {}", rust);
3234    }
3235
3236    #[test]
3237    fn test_transpile_with_open_file_write() {
3238        let code = r#"
3239def write_file(path: str, data: str) -> None:
3240    with open(path, "w") as f:
3241        f.write(data)
3242"#;
3243        let rust = transpile(code);
3244        assert!(rust.contains("fn write_file"), "output: {}", rust);
3245    }
3246
3247    #[test]
3248    fn test_transpile_with_no_target() {
3249        let code = r#"
3250def process() -> None:
3251    with open("test.txt"):
3252        print("processing")
3253"#;
3254        let rust = transpile(code);
3255        assert!(rust.contains("fn process"), "output: {}", rust);
3256    }
3257
3258    // ================================================================
3259    // Transpile-based tests for raise statements
3260    // ================================================================
3261
3262    #[test]
3263    fn test_transpile_raise_value_error() {
3264        let code = r#"
3265def validate(x: int) -> int:
3266    if x < 0:
3267        raise ValueError("must be positive")
3268    return x
3269"#;
3270        let rust = transpile(code);
3271        assert!(rust.contains("fn validate"), "output: {}", rust);
3272    }
3273
3274    #[test]
3275    fn test_transpile_raise_runtime_error() {
3276        let code = r#"
3277def fail_fast() -> None:
3278    raise RuntimeError("not implemented")
3279"#;
3280        let rust = transpile(code);
3281        assert!(rust.contains("fn fail_fast"), "output: {}", rust);
3282    }
3283
3284    #[test]
3285    fn test_transpile_raise_bare() {
3286        let code = r#"
3287def reraise_bare() -> int:
3288    try:
3289        return 1
3290    except ValueError:
3291        raise
3292"#;
3293        let rust = transpile(code);
3294        assert!(rust.contains("fn reraise_bare"), "output: {}", rust);
3295    }
3296
3297    // ================================================================
3298    // Transpile-based tests for assert statements
3299    // ================================================================
3300
3301    #[test]
3302    fn test_transpile_assert_simple() {
3303        let code = r#"
3304def check(x: int) -> None:
3305    assert x > 0
3306"#;
3307        let rust = transpile(code);
3308        assert!(rust.contains("assert"), "output: {}", rust);
3309    }
3310
3311    #[test]
3312    fn test_transpile_assert_with_message() {
3313        let code = r#"
3314def check_msg(x: int) -> None:
3315    assert x > 0, "x must be positive"
3316"#;
3317        let rust = transpile(code);
3318        assert!(rust.contains("fn check_msg"), "output: {}", rust);
3319        assert!(
3320            rust.contains("assert") || rust.contains("panic"),
3321            "output: {}",
3322            rust
3323        );
3324    }
3325
3326    #[test]
3327    fn test_transpile_assert_equality() {
3328        let code = r#"
3329def check_eq(a: int, b: int) -> None:
3330    assert a == b
3331"#;
3332        let rust = transpile(code);
3333        assert!(rust.contains("fn check_eq"), "output: {}", rust);
3334    }
3335
3336    #[test]
3337    fn test_transpile_assert_not_equal() {
3338        let code = r#"
3339def check_ne(a: int, b: int) -> None:
3340    assert a != b
3341"#;
3342        let rust = transpile(code);
3343        assert!(rust.contains("fn check_ne"), "output: {}", rust);
3344    }
3345
3346    // ================================================================
3347    // Transpile-based tests for augmented assignments
3348    // ================================================================
3349
3350    #[test]
3351    fn test_transpile_augmented_add() {
3352        let code = r#"
3353def accumulate(n: int) -> int:
3354    total = 0
3355    total += n
3356    return total
3357"#;
3358        let rust = transpile(code);
3359        assert!(rust.contains("fn accumulate"), "output: {}", rust);
3360        assert!(rust.contains("+="), "Should contain += operator: {}", rust);
3361    }
3362
3363    #[test]
3364    fn test_transpile_augmented_sub() {
3365        let code = r#"
3366def decrement(n: int) -> int:
3367    value = 10
3368    value -= n
3369    return value
3370"#;
3371        let rust = transpile(code);
3372        assert!(rust.contains("fn decrement"), "output: {}", rust);
3373        assert!(rust.contains("-="), "Should contain -= operator: {}", rust);
3374    }
3375
3376    #[test]
3377    fn test_transpile_augmented_mul() {
3378        let code = r#"
3379def scale(n: int) -> int:
3380    result = 1
3381    result *= n
3382    return result
3383"#;
3384        let rust = transpile(code);
3385        assert!(rust.contains("fn scale"), "output: {}", rust);
3386        // Transpiler may use `*=`, expand to `result * n`, or use `py_mul` helper
3387        assert!(
3388            rust.contains("*=") || rust.contains("py_mul") || rust.contains("* n"),
3389            "Should contain multiplication: {}",
3390            rust
3391        );
3392    }
3393
3394    #[test]
3395    fn test_transpile_augmented_mod() {
3396        let code = r#"
3397def modulo_assign(n: int) -> int:
3398    value = 100
3399    value %= n
3400    return value
3401"#;
3402        let rust = transpile(code);
3403        assert!(rust.contains("fn modulo_assign"), "output: {}", rust);
3404        // Transpiler may use py_mod() helper for Python-compatible modulo
3405        assert!(
3406            rust.contains("%=") || rust.contains("py_mod") || rust.contains("mod"),
3407            "Should contain modulo operation: {}",
3408            rust
3409        );
3410    }
3411
3412    // ================================================================
3413    // Transpile-based tests for break and continue
3414    // ================================================================
3415
3416    #[test]
3417    fn test_transpile_break_in_loop() {
3418        let code = r#"
3419def find_first(n: int) -> int:
3420    i = 0
3421    while i < n:
3422        if i == 5:
3423            break
3424        i += 1
3425    return i
3426"#;
3427        let rust = transpile(code);
3428        assert!(rust.contains("fn find_first"), "output: {}", rust);
3429        assert!(rust.contains("break"), "Should contain break: {}", rust);
3430    }
3431
3432    #[test]
3433    fn test_transpile_continue_in_loop() {
3434        let code = r#"
3435def skip_evens(n: int) -> int:
3436    total = 0
3437    for i in range(n):
3438        if i % 2 == 0:
3439            continue
3440        total += i
3441    return total
3442"#;
3443        let rust = transpile(code);
3444        assert!(rust.contains("fn skip_evens"), "output: {}", rust);
3445        assert!(
3446            rust.contains("continue"),
3447            "Should contain continue: {}",
3448            rust
3449        );
3450    }
3451
3452    // ================================================================
3453    // Transpile-based tests for pass statement
3454    // ================================================================
3455
3456    #[test]
3457    fn test_transpile_pass_in_function() {
3458        let code = r#"
3459def noop() -> None:
3460    pass
3461"#;
3462        let rust = transpile(code);
3463        assert!(rust.contains("fn noop"), "output: {}", rust);
3464    }
3465
3466    #[test]
3467    fn test_transpile_pass_in_if() {
3468        let code = r#"
3469def conditional(x: int) -> int:
3470    if x > 0:
3471        pass
3472    else:
3473        return -1
3474    return x
3475"#;
3476        let rust = transpile(code);
3477        assert!(rust.contains("fn conditional"), "output: {}", rust);
3478    }
3479
3480    // ================================================================
3481    // Transpile-based tests for multi-target / chained assignment
3482    // ================================================================
3483
3484    #[test]
3485    fn test_transpile_chained_assignment() {
3486        let code = r#"
3487def chain() -> int:
3488    a = b = 0
3489    return a + b
3490"#;
3491        let rust = transpile(code);
3492        assert!(rust.contains("fn chain"), "output: {}", rust);
3493    }
3494
3495    // ================================================================
3496    // Transpile-based tests for class definitions
3497    // ================================================================
3498
3499    #[test]
3500    fn test_transpile_class_basic() {
3501        let code = r#"
3502class Point:
3503    def __init__(self, x: int, y: int):
3504        self.x = x
3505        self.y = y
3506"#;
3507        let rust = transpile(code);
3508        // Should produce some struct or impl output
3509        assert!(
3510            rust.contains("struct") || rust.contains("Point") || rust.contains("impl"),
3511            "Class should generate struct/impl: {}",
3512            rust
3513        );
3514    }
3515
3516    #[test]
3517    fn test_transpile_class_with_method() {
3518        let code = r#"
3519class Counter:
3520    def __init__(self):
3521        self.count = 0
3522
3523    def increment(self) -> None:
3524        self.count += 1
3525"#;
3526        let rust = transpile(code);
3527        assert!(
3528            rust.contains("Counter") || rust.contains("struct"),
3529            "Class with method should generate code: {}",
3530            rust
3531        );
3532    }
3533
3534    #[test]
3535    fn test_transpile_class_with_return_method() {
3536        let code = r#"
3537class Box:
3538    def __init__(self, value: int):
3539        self.value = value
3540
3541    def get_value(self) -> int:
3542        return self.value
3543"#;
3544        let rust = transpile(code);
3545        assert!(
3546            rust.contains("Box") || rust.contains("struct") || rust.contains("fn get_value"),
3547            "Class method should generate fn: {}",
3548            rust
3549        );
3550    }
3551
3552    // ================================================================
3553    // Transpile-based tests for nested try/except patterns
3554    // ================================================================
3555
3556    #[test]
3557    fn test_transpile_try_except_with_exception_as_var() {
3558        let code = r#"
3559def handle_error(s: str) -> str:
3560    try:
3561        return s
3562    except Exception as e:
3563        return "error"
3564"#;
3565        let rust = transpile(code);
3566        assert!(rust.contains("fn handle_error"), "output: {}", rust);
3567    }
3568
3569    #[test]
3570    fn test_transpile_nested_try_with_finally() {
3571        let code = r#"
3572def nested_cleanup() -> str:
3573    result = ""
3574    try:
3575        try:
3576            result = "inner"
3577        except ValueError:
3578            result = "inner_error"
3579        finally:
3580            result = result + "_inner_done"
3581    except RuntimeError:
3582        result = "outer_error"
3583    return result
3584"#;
3585        let rust = transpile(code);
3586        assert!(rust.contains("fn nested_cleanup"), "output: {}", rust);
3587    }
3588
3589    #[test]
3590    fn test_transpile_try_only_finally() {
3591        let code = r#"
3592def with_finally() -> int:
3593    x = 0
3594    try:
3595        x = 42
3596    finally:
3597        x = x + 1
3598    return x
3599"#;
3600        let rust = transpile(code);
3601        assert!(rust.contains("fn with_finally"), "output: {}", rust);
3602    }
3603
3604    // ================================================================
3605    // Transpile-based tests for nested functions (additional coverage)
3606    // ================================================================
3607
3608    #[test]
3609    fn test_transpile_nested_function_recursive() {
3610        let code = r#"
3611def outer() -> int:
3612    def factorial(n: int) -> int:
3613        if n <= 1:
3614            return 1
3615        return n * factorial(n - 1)
3616    return factorial(5)
3617"#;
3618        let rust = transpile(code);
3619        assert!(rust.contains("fn outer"), "output: {}", rust);
3620        assert!(
3621            rust.contains("factorial"),
3622            "Should contain factorial: {}",
3623            rust
3624        );
3625    }
3626
3627    #[test]
3628    fn test_transpile_nested_function_unknown_return() {
3629        let code = r#"
3630def outer():
3631    def helper():
3632        return 42
3633    return helper()
3634"#;
3635        let rust = transpile(code);
3636        assert!(rust.contains("outer"), "output: {}", rust);
3637        assert!(rust.contains("helper"), "Should contain helper: {}", rust);
3638    }
3639
3640    // ================================================================
3641    // Transpile-based tests for complex expression patterns
3642    // ================================================================
3643
3644    #[test]
3645    fn test_transpile_string_concatenation_augmented() {
3646        let code = r#"
3647def build_str() -> str:
3648    result = ""
3649    result += "hello"
3650    result += " world"
3651    return result
3652"#;
3653        let rust = transpile(code);
3654        assert!(rust.contains("fn build_str"), "output: {}", rust);
3655    }
3656
3657    #[test]
3658    fn test_transpile_for_with_break() {
3659        let code = r#"
3660def search(items: list, target: int) -> int:
3661    for i in range(len(items)):
3662        if items[i] == target:
3663            return i
3664    return -1
3665"#;
3666        let rust = transpile(code);
3667        assert!(rust.contains("fn search"), "output: {}", rust);
3668    }
3669
3670    #[test]
3671    fn test_transpile_while_true_break() {
3672        let code = r#"
3673def read_until_done() -> int:
3674    count = 0
3675    while True:
3676        count += 1
3677        if count >= 10:
3678            break
3679    return count
3680"#;
3681        let rust = transpile(code);
3682        assert!(
3683            rust.contains("fn read_until_done"),
3684            "output: {}",
3685            rust
3686        );
3687        assert!(rust.contains("loop") || rust.contains("while"), "output: {}", rust);
3688        assert!(rust.contains("break"), "Should contain break: {}", rust);
3689    }
3690
3691    // ================================================================
3692    // captures_outer_scope additional edge case tests
3693    // ================================================================
3694
3695    #[test]
3696    fn test_captures_outer_scope_in_with_context() {
3697        let params = vec![];
3698        let body = vec![HirStmt::With {
3699            context: HirExpr::Var("outer_ctx".to_string()),
3700            target: Some("f".to_string()),
3701            body: vec![],
3702            is_async: false,
3703        }];
3704        let outer_vars: HashSet<String> = ["outer_ctx".to_string()].into_iter().collect();
3705        assert!(captures_outer_scope(&params, &body, &outer_vars));
3706    }
3707
3708    #[test]
3709    fn test_captures_outer_scope_in_try_handler() {
3710        let params = vec![];
3711        let body = vec![HirStmt::Try {
3712            body: vec![],
3713            handlers: vec![ExceptHandler {
3714                exception_type: None,
3715                name: None,
3716                body: vec![HirStmt::Expr(HirExpr::Var("outer_val".to_string()))],
3717            }],
3718            orelse: None,
3719            finalbody: None,
3720        }];
3721        let outer_vars: HashSet<String> = ["outer_val".to_string()].into_iter().collect();
3722        assert!(captures_outer_scope(&params, &body, &outer_vars));
3723    }
3724
3725    #[test]
3726    fn test_captures_outer_scope_in_try_orelse() {
3727        let params = vec![];
3728        let body = vec![HirStmt::Try {
3729            body: vec![],
3730            handlers: vec![],
3731            orelse: Some(vec![HirStmt::Expr(HirExpr::Var(
3732                "else_var".to_string(),
3733            ))]),
3734            finalbody: None,
3735        }];
3736        let outer_vars: HashSet<String> = ["else_var".to_string()].into_iter().collect();
3737        assert!(captures_outer_scope(&params, &body, &outer_vars));
3738    }
3739
3740    #[test]
3741    fn test_captures_outer_scope_in_try_finalbody() {
3742        let params = vec![];
3743        let body = vec![HirStmt::Try {
3744            body: vec![],
3745            handlers: vec![],
3746            orelse: None,
3747            finalbody: Some(vec![HirStmt::Expr(HirExpr::Var(
3748                "fin_var".to_string(),
3749            ))]),
3750        }];
3751        let outer_vars: HashSet<String> = ["fin_var".to_string()].into_iter().collect();
3752        assert!(captures_outer_scope(&params, &body, &outer_vars));
3753    }
3754
3755    #[test]
3756    fn test_captures_outer_scope_in_method_call() {
3757        let params = vec![];
3758        let body = vec![HirStmt::Expr(HirExpr::MethodCall {
3759            object: Box::new(HirExpr::Var("outer_obj".to_string())),
3760            method: "do_thing".to_string(),
3761            args: vec![],
3762            kwargs: vec![],
3763        })];
3764        let outer_vars: HashSet<String> = ["outer_obj".to_string()].into_iter().collect();
3765        assert!(captures_outer_scope(&params, &body, &outer_vars));
3766    }
3767
3768    #[test]
3769    fn test_captures_outer_scope_in_attribute() {
3770        let params = vec![];
3771        let body = vec![HirStmt::Expr(HirExpr::Attribute {
3772            value: Box::new(HirExpr::Var("outer_ref".to_string())),
3773            attr: "field".to_string(),
3774        })];
3775        let outer_vars: HashSet<String> = ["outer_ref".to_string()].into_iter().collect();
3776        assert!(captures_outer_scope(&params, &body, &outer_vars));
3777    }
3778
3779    #[test]
3780    fn test_captures_outer_scope_in_index() {
3781        let params = vec![];
3782        let body = vec![HirStmt::Expr(HirExpr::Index {
3783            base: Box::new(HirExpr::Var("outer_list".to_string())),
3784            index: Box::new(HirExpr::Literal(Literal::Int(0))),
3785        })];
3786        let outer_vars: HashSet<String> = ["outer_list".to_string()].into_iter().collect();
3787        assert!(captures_outer_scope(&params, &body, &outer_vars));
3788    }
3789
3790    #[test]
3791    fn test_captures_outer_scope_in_if_expr() {
3792        let params = vec![];
3793        let body = vec![HirStmt::Expr(HirExpr::IfExpr {
3794            test: Box::new(HirExpr::Literal(Literal::Bool(true))),
3795            body: Box::new(HirExpr::Var("outer_val".to_string())),
3796            orelse: Box::new(HirExpr::Literal(Literal::Int(0))),
3797        })];
3798        let outer_vars: HashSet<String> = ["outer_val".to_string()].into_iter().collect();
3799        assert!(captures_outer_scope(&params, &body, &outer_vars));
3800    }
3801
3802    #[test]
3803    fn test_captures_outer_scope_in_unary() {
3804        let params = vec![];
3805        let body = vec![HirStmt::Expr(HirExpr::Unary {
3806            op: UnaryOp::Not,
3807            operand: Box::new(HirExpr::Var("flag".to_string())),
3808        })];
3809        let outer_vars: HashSet<String> = ["flag".to_string()].into_iter().collect();
3810        assert!(captures_outer_scope(&params, &body, &outer_vars));
3811    }
3812
3813    #[test]
3814    fn test_captures_outer_scope_in_list_literal() {
3815        let params = vec![];
3816        let body = vec![HirStmt::Expr(HirExpr::List(vec![HirExpr::Var(
3817            "outer_elem".to_string(),
3818        )]))];
3819        let outer_vars: HashSet<String> = ["outer_elem".to_string()].into_iter().collect();
3820        assert!(captures_outer_scope(&params, &body, &outer_vars));
3821    }
3822
3823    #[test]
3824    fn test_captures_outer_scope_in_dict_literal() {
3825        let params = vec![];
3826        let body = vec![HirStmt::Expr(HirExpr::Dict(vec![(
3827            HirExpr::Literal(Literal::String("k".to_string())),
3828            HirExpr::Var("outer_val".to_string()),
3829        )]))];
3830        let outer_vars: HashSet<String> = ["outer_val".to_string()].into_iter().collect();
3831        assert!(captures_outer_scope(&params, &body, &outer_vars));
3832    }
3833
3834    #[test]
3835    fn test_captures_outer_scope_in_assert() {
3836        let params = vec![];
3837        let body = vec![HirStmt::Assert {
3838            test: HirExpr::Var("invariant".to_string()),
3839            msg: None,
3840        }];
3841        let outer_vars: HashSet<String> = ["invariant".to_string()].into_iter().collect();
3842        assert!(captures_outer_scope(&params, &body, &outer_vars));
3843    }
3844
3845    #[test]
3846    fn test_captures_outer_scope_in_assert_msg() {
3847        let params = vec![];
3848        let body = vec![HirStmt::Assert {
3849            test: HirExpr::Literal(Literal::Bool(true)),
3850            msg: Some(HirExpr::Var("outer_msg".to_string())),
3851        }];
3852        let outer_vars: HashSet<String> = ["outer_msg".to_string()].into_iter().collect();
3853        assert!(captures_outer_scope(&params, &body, &outer_vars));
3854    }
3855
3856    #[test]
3857    fn test_captures_outer_scope_in_raise() {
3858        let params = vec![];
3859        let body = vec![HirStmt::Raise {
3860            exception: Some(HirExpr::Var("outer_exc".to_string())),
3861            cause: None,
3862        }];
3863        let outer_vars: HashSet<String> = ["outer_exc".to_string()].into_iter().collect();
3864        assert!(captures_outer_scope(&params, &body, &outer_vars));
3865    }
3866
3867    #[test]
3868    fn test_captures_outer_scope_in_raise_cause() {
3869        let params = vec![];
3870        let body = vec![HirStmt::Raise {
3871            exception: Some(HirExpr::Literal(Literal::String("err".to_string()))),
3872            cause: Some(HirExpr::Var("outer_cause".to_string())),
3873        }];
3874        let outer_vars: HashSet<String> = ["outer_cause".to_string()].into_iter().collect();
3875        assert!(captures_outer_scope(&params, &body, &outer_vars));
3876    }
3877
3878    #[test]
3879    fn test_captures_outer_scope_nested_function_def() {
3880        let params = vec![];
3881        let body = vec![HirStmt::FunctionDef {
3882            name: "nested".to_string(),
3883            params: Box::new(smallvec::smallvec![]),
3884            ret_type: Type::Int,
3885            body: vec![HirStmt::Return(Some(HirExpr::Var(
3886                "outer_val".to_string(),
3887            )))],
3888            docstring: None,
3889        }];
3890        let outer_vars: HashSet<String> = ["outer_val".to_string()].into_iter().collect();
3891        assert!(captures_outer_scope(&params, &body, &outer_vars));
3892    }
3893
3894    #[test]
3895    fn test_captures_outer_scope_in_block() {
3896        let params = vec![];
3897        let body = vec![HirStmt::Block(vec![HirStmt::Expr(HirExpr::Var(
3898            "outer_block".to_string(),
3899        ))])];
3900        let outer_vars: HashSet<String> = ["outer_block".to_string()].into_iter().collect();
3901        assert!(captures_outer_scope(&params, &body, &outer_vars));
3902    }
3903
3904    #[test]
3905    fn test_captures_outer_scope_call_outer_func() {
3906        let params = vec![];
3907        let body = vec![HirStmt::Expr(HirExpr::Call {
3908            func: "outer_func".to_string(),
3909            args: vec![],
3910            kwargs: vec![],
3911        })];
3912        let outer_vars: HashSet<String> = ["outer_func".to_string()].into_iter().collect();
3913        assert!(captures_outer_scope(&params, &body, &outer_vars));
3914    }
3915
3916    #[test]
3917    fn test_captures_outer_scope_call_kwarg() {
3918        let params = vec![];
3919        let body = vec![HirStmt::Expr(HirExpr::Call {
3920            func: "print".to_string(),
3921            args: vec![],
3922            kwargs: vec![("end".to_string(), HirExpr::Var("outer_kw".to_string()))],
3923        })];
3924        let outer_vars: HashSet<String> = ["outer_kw".to_string()].into_iter().collect();
3925        assert!(captures_outer_scope(&params, &body, &outer_vars));
3926    }
3927
3928    #[test]
3929    fn test_captures_outer_scope_no_capture_pass() {
3930        let params = vec![];
3931        let body = vec![HirStmt::Pass];
3932        let outer_vars: HashSet<String> = ["anything".to_string()].into_iter().collect();
3933        assert!(!captures_outer_scope(&params, &body, &outer_vars));
3934    }
3935
3936    #[test]
3937    fn test_captures_outer_scope_no_capture_return_none() {
3938        let params = vec![];
3939        let body = vec![HirStmt::Return(None)];
3940        let outer_vars: HashSet<String> = ["anything".to_string()].into_iter().collect();
3941        assert!(!captures_outer_scope(&params, &body, &outer_vars));
3942    }
3943
3944    #[test]
3945    fn test_captures_outer_scope_no_capture_break() {
3946        let params = vec![];
3947        let body = vec![HirStmt::Break { label: None }];
3948        let outer_vars: HashSet<String> = ["anything".to_string()].into_iter().collect();
3949        assert!(!captures_outer_scope(&params, &body, &outer_vars));
3950    }
3951
3952    #[test]
3953    fn test_captures_outer_scope_no_capture_continue() {
3954        let params = vec![];
3955        let body = vec![HirStmt::Continue { label: None }];
3956        let outer_vars: HashSet<String> = ["anything".to_string()].into_iter().collect();
3957        assert!(!captures_outer_scope(&params, &body, &outer_vars));
3958    }
3959
3960    // ================================================================
3961    // Transpile-based tests for complex try/except patterns
3962    // ================================================================
3963
3964    #[test]
3965    fn test_transpile_try_parse_int_with_negative_fallback() {
3966        let code = r#"
3967def safe_parse(s: str) -> int:
3968    try:
3969        return int(s)
3970    except ValueError:
3971        return -1
3972"#;
3973        let rust = transpile(code);
3974        assert!(rust.contains("fn safe_parse"), "output: {}", rust);
3975        assert!(
3976            rust.contains("parse") || rust.contains("unwrap_or"),
3977            "Should use parse pattern: {}",
3978            rust
3979        );
3980    }
3981
3982    #[test]
3983    fn test_transpile_try_parse_int_with_zero_fallback() {
3984        let code = r#"
3985def parse_or_zero(s: str) -> int:
3986    try:
3987        return int(s)
3988    except ValueError:
3989        return 0
3990"#;
3991        let rust = transpile(code);
3992        assert!(rust.contains("fn parse_or_zero"), "output: {}", rust);
3993    }
3994
3995    #[test]
3996    fn test_transpile_try_handler_with_print() {
3997        let code = r#"
3998def safe_op() -> str:
3999    try:
4000        return "ok"
4001    except ValueError:
4002        print("error occurred")
4003        return "error"
4004"#;
4005        let rust = transpile(code);
4006        assert!(rust.contains("fn safe_op"), "output: {}", rust);
4007    }
4008
4009    // ================================================================
4010    // Additional extract_fields_from_expr tests for edge cases
4011    // ================================================================
4012
4013    #[test]
4014    fn test_extract_fields_multiple_in_same_call() {
4015        let mut fields = HashSet::new();
4016        let expr = HirExpr::Call {
4017            func: "combine".to_string(),
4018            args: vec![
4019                HirExpr::Attribute {
4020                    value: Box::new(HirExpr::Var("args".to_string())),
4021                    attr: "first".to_string(),
4022                },
4023                HirExpr::Attribute {
4024                    value: Box::new(HirExpr::Var("args".to_string())),
4025                    attr: "second".to_string(),
4026                },
4027            ],
4028            kwargs: vec![],
4029        };
4030        extract_fields_from_expr(&expr, "args", "command", &mut fields);
4031        assert!(fields.contains("first"));
4032        assert!(fields.contains("second"));
4033        assert_eq!(fields.len(), 2);
4034    }
4035
4036    #[test]
4037    fn test_extract_fields_from_dict_key() {
4038        let mut fields = HashSet::new();
4039        let expr = HirExpr::Dict(vec![(
4040            HirExpr::Attribute {
4041                value: Box::new(HirExpr::Var("args".to_string())),
4042                attr: "key_field".to_string(),
4043            },
4044            HirExpr::Literal(Literal::Int(42)),
4045        )]);
4046        extract_fields_from_expr(&expr, "args", "command", &mut fields);
4047        assert!(fields.contains("key_field"));
4048    }
4049
4050    #[test]
4051    fn test_extract_fields_var_no_match() {
4052        let mut fields = HashSet::new();
4053        let expr = HirExpr::Var("something".to_string());
4054        extract_fields_from_expr(&expr, "args", "command", &mut fields);
4055        assert!(fields.is_empty());
4056    }
4057
4058    // ================================================================
4059    // extract_parse_from_tokens tests
4060    // ================================================================
4061
4062    #[test]
4063    fn test_extract_parse_from_tokens_empty() {
4064        let result = extract_parse_from_tokens(&[]);
4065        assert!(result.is_none());
4066    }
4067
4068    #[test]
4069    fn test_extract_parse_from_tokens_non_parse_stmt() {
4070        let tokens: proc_macro2::TokenStream =
4071            "let x = 42 ;".parse().unwrap();
4072        let result = extract_parse_from_tokens(&[tokens]);
4073        assert!(result.is_none());
4074    }
4075
4076    // ================================================================
4077    // Session 9: Coverage improvement tests for uncovered paths
4078    // ================================================================
4079
4080    #[test]
4081    fn test_transpile_try_except_with_variable_hoisting() {
4082        let code = r#"
4083def process(s: str) -> str:
4084    try:
4085        result = s.upper()
4086    except ValueError:
4087        result = "error"
4088    return result
4089"#;
4090        let rust = transpile(code);
4091        assert!(rust.contains("fn process"), "output: {}", rust);
4092        assert!(
4093            rust.contains("result") && rust.contains("mut"),
4094            "Should hoist result variable: {}",
4095            rust
4096        );
4097    }
4098
4099    #[test]
4100    fn test_transpile_try_with_floor_div_zerodiv() {
4101        let code = r#"
4102def safe_floor_div(a: int, b: int) -> int:
4103    try:
4104        return a // b
4105    except ZeroDivisionError:
4106        return 0
4107"#;
4108        let rust = transpile(code);
4109        assert!(rust.contains("fn safe_floor_div"), "output: {}", rust);
4110    }
4111
4112    #[test]
4113    fn test_transpile_try_exception_with_binding_and_return() {
4114        let code = r#"
4115def catch_with_msg(s: str) -> str:
4116    try:
4117        n = int(s)
4118        return str(n)
4119    except ValueError as e:
4120        return "bad input"
4121"#;
4122        let rust = transpile(code);
4123        assert!(rust.contains("fn catch_with_msg"), "output: {}", rust);
4124    }
4125
4126    #[test]
4127    fn test_transpile_try_except_in_loop() {
4128        let code = r#"
4129def parse_all(items: list) -> int:
4130    count = 0
4131    for item in items:
4132        try:
4133            n = int(item)
4134            count += n
4135        except ValueError:
4136            pass
4137    return count
4138"#;
4139        let rust = transpile(code);
4140        assert!(rust.contains("fn parse_all"), "output: {}", rust);
4141    }
4142
4143    #[test]
4144    fn test_transpile_try_with_bool_return_handler() {
4145        let code = r#"
4146def is_valid(s: str) -> bool:
4147    try:
4148        n = int(s)
4149        return True
4150    except ValueError:
4151        return False
4152"#;
4153        let rust = transpile(code);
4154        assert!(rust.contains("fn is_valid"), "output: {}", rust);
4155    }
4156
4157    #[test]
4158    fn test_transpile_try_negative_fallback() {
4159        let code = r#"
4160def parse_or_neg(s: str) -> int:
4161    try:
4162        return int(s)
4163    except ValueError:
4164        return -1
4165"#;
4166        let rust = transpile(code);
4167        assert!(rust.contains("fn parse_or_neg"), "output: {}", rust);
4168    }
4169
4170    #[test]
4171    fn test_transpile_try_float_fallback() {
4172        let code = r#"
4173def parse_float_safe(s: str) -> float:
4174    try:
4175        return float(s)
4176    except ValueError:
4177        return 0.0
4178"#;
4179        let rust = transpile(code);
4180        assert!(rust.contains("fn parse_float_safe"), "output: {}", rust);
4181    }
4182
4183    #[test]
4184    fn test_transpile_try_string_fallback() {
4185        let code = r#"
4186def safe_upper(s: str) -> str:
4187    try:
4188        return s.upper()
4189    except ValueError:
4190        return ""
4191"#;
4192        let rust = transpile(code);
4193        assert!(rust.contains("fn safe_upper"), "output: {}", rust);
4194    }
4195
4196    #[test]
4197    fn test_transpile_nested_function_with_capture_list() {
4198        let code = r#"
4199def outer() -> list:
4200    items = [1, 2, 3]
4201    def inner() -> int:
4202        return len(items)
4203    return [inner()]
4204"#;
4205        let rust = transpile(code);
4206        assert!(rust.contains("fn outer"), "output: {}", rust);
4207    }
4208
4209    #[test]
4210    fn test_transpile_nested_function_with_string_capture() {
4211        let code = r#"
4212def make_greeter(name: str) -> str:
4213    def greet() -> str:
4214        return "Hello " + name
4215    return greet()
4216"#;
4217        let rust = transpile(code);
4218        assert!(rust.contains("fn make_greeter"), "output: {}", rust);
4219    }
4220
4221    #[test]
4222    fn test_transpile_nested_function_no_params_no_capture() {
4223        let code = r#"
4224def wrapper() -> int:
4225    def constant() -> int:
4226        return 42
4227    return constant()
4228"#;
4229        let rust = transpile(code);
4230        assert!(rust.contains("fn wrapper"), "output: {}", rust);
4231    }
4232
4233    #[test]
4234    fn test_transpile_nested_function_shadowed_param() {
4235        let code = r#"
4236def outer(x: int) -> int:
4237    y = 20
4238    def inner(x: int) -> int:
4239        return x + y
4240    return inner(5)
4241"#;
4242        let rust = transpile(code);
4243        assert!(rust.contains("fn outer"), "output: {}", rust);
4244    }
4245
4246    #[test]
4247    fn test_transpile_try_except_os_error() {
4248        let code = r#"
4249def safe_read(path: str) -> str:
4250    try:
4251        with open(path) as f:
4252            return f.read()
4253    except OSError:
4254        return ""
4255"#;
4256        let rust = transpile(code);
4257        assert!(rust.contains("fn safe_read"), "output: {}", rust);
4258    }
4259
4260    #[test]
4261    fn test_transpile_try_except_index_error() {
4262        let code = r#"
4263def safe_first(items: list) -> int:
4264    try:
4265        return items[0]
4266    except IndexError:
4267        return 0
4268"#;
4269        let rust = transpile(code);
4270        assert!(rust.contains("fn safe_first"), "output: {}", rust);
4271    }
4272
4273    #[test]
4274    fn test_transpile_try_except_key_error() {
4275        let code = r#"
4276def safe_lookup(d: dict, key: str) -> str:
4277    try:
4278        return d[key]
4279    except KeyError:
4280        return "missing"
4281"#;
4282        let rust = transpile(code);
4283        assert!(rust.contains("fn safe_lookup"), "output: {}", rust);
4284    }
4285
4286    #[test]
4287    fn test_transpile_try_except_with_print_handler() {
4288        let code = r#"
4289def log_error(s: str) -> int:
4290    try:
4291        return int(s)
4292    except ValueError:
4293        print("invalid")
4294        return 0
4295"#;
4296        let rust = transpile(code);
4297        assert!(rust.contains("fn log_error"), "output: {}", rust);
4298    }
4299
4300    #[test]
4301    fn test_transpile_try_except_multiple_stmts_body() {
4302        let code = r#"
4303def multi_try(a: str, b: str) -> int:
4304    try:
4305        x = int(a)
4306        y = int(b)
4307        return x + y
4308    except ValueError:
4309        return 0
4310"#;
4311        let rust = transpile(code);
4312        assert!(rust.contains("fn multi_try"), "output: {}", rust);
4313    }
4314
4315    // === captures_outer_scope additional coverage ===
4316
4317    #[test]
4318    fn test_captures_outer_scope_in_assign_value() {
4319        let params = vec![];
4320        let body = vec![HirStmt::Assign {
4321            target: AssignTarget::Symbol("x".to_string()),
4322            value: HirExpr::Binary {
4323                op: BinOp::Add,
4324                left: Box::new(HirExpr::Var("outer_val".to_string())),
4325                right: Box::new(HirExpr::Literal(Literal::Int(1))),
4326            },
4327            type_annotation: None,
4328        }];
4329        let outer_vars: HashSet<String> = ["outer_val".to_string()].into_iter().collect();
4330        assert!(captures_outer_scope(&params, &body, &outer_vars));
4331    }
4332
4333    #[test]
4334    fn test_captures_outer_scope_in_call_args() {
4335        let params = vec![];
4336        let body = vec![HirStmt::Expr(HirExpr::Call {
4337            func: "print".to_string(),
4338            args: vec![HirExpr::Var("captured".to_string())],
4339            kwargs: vec![],
4340        })];
4341        let outer_vars: HashSet<String> = ["captured".to_string()].into_iter().collect();
4342        assert!(captures_outer_scope(&params, &body, &outer_vars));
4343    }
4344
4345    #[test]
4346    fn test_captures_outer_scope_in_method_call_object() {
4347        let params = vec![];
4348        let body = vec![HirStmt::Expr(HirExpr::MethodCall {
4349            object: Box::new(HirExpr::Var("outer_list".to_string())),
4350            method: "append".to_string(),
4351            args: vec![HirExpr::Literal(Literal::Int(1))],
4352            kwargs: vec![],
4353        })];
4354        let outer_vars: HashSet<String> = ["outer_list".to_string()].into_iter().collect();
4355        assert!(captures_outer_scope(&params, &body, &outer_vars));
4356    }
4357
4358    #[test]
4359    fn test_captures_outer_scope_in_comprehension() {
4360        let params = vec![];
4361        let body = vec![HirStmt::Return(Some(HirExpr::ListComp {
4362            element: Box::new(HirExpr::Binary {
4363                op: BinOp::Mul,
4364                left: Box::new(HirExpr::Var("x".to_string())),
4365                right: Box::new(HirExpr::Var("multiplier".to_string())),
4366            }),
4367            generators: vec![HirComprehension {
4368                target: "x".to_string(),
4369                iter: Box::new(HirExpr::List(vec![])),
4370                conditions: vec![],
4371            }],
4372        }))];
4373        let outer_vars: HashSet<String> = ["multiplier".to_string()].into_iter().collect();
4374        assert!(captures_outer_scope(&params, &body, &outer_vars));
4375    }
4376
4377    #[test]
4378    fn test_captures_outer_scope_in_fstring() {
4379        let params = vec![];
4380        let body = vec![HirStmt::Return(Some(HirExpr::FString {
4381            parts: vec![FStringPart::Expr(Box::new(HirExpr::Var(
4382                "name".to_string(),
4383            )))],
4384        }))];
4385        let outer_vars: HashSet<String> = ["name".to_string()].into_iter().collect();
4386        assert!(captures_outer_scope(&params, &body, &outer_vars));
4387    }
4388
4389    #[test]
4390    fn test_captures_outer_scope_s9_index() {
4391        let params = vec![];
4392        let body = vec![HirStmt::Return(Some(HirExpr::Index {
4393            base: Box::new(HirExpr::Var("data".to_string())),
4394            index: Box::new(HirExpr::Literal(Literal::Int(0))),
4395        }))];
4396        let outer_vars: HashSet<String> = ["data".to_string()].into_iter().collect();
4397        assert!(captures_outer_scope(&params, &body, &outer_vars));
4398    }
4399
4400    #[test]
4401    fn test_captures_outer_scope_s9_attribute() {
4402        let params = vec![];
4403        let body = vec![HirStmt::Return(Some(HirExpr::Attribute {
4404            value: Box::new(HirExpr::Var("obj".to_string())),
4405            attr: "name".to_string(),
4406        }))];
4407        let outer_vars: HashSet<String> = ["obj".to_string()].into_iter().collect();
4408        assert!(captures_outer_scope(&params, &body, &outer_vars));
4409    }
4410
4411    #[test]
4412    fn test_captures_outer_scope_s9_if_expr() {
4413        let params = vec![];
4414        let body = vec![HirStmt::Return(Some(HirExpr::IfExpr {
4415            test: Box::new(HirExpr::Var("flag".to_string())),
4416            body: Box::new(HirExpr::Literal(Literal::Int(1))),
4417            orelse: Box::new(HirExpr::Literal(Literal::Int(0))),
4418        }))];
4419        let outer_vars: HashSet<String> = ["flag".to_string()].into_iter().collect();
4420        assert!(captures_outer_scope(&params, &body, &outer_vars));
4421    }
4422
4423    #[test]
4424    fn test_captures_outer_scope_in_try_body() {
4425        let params = vec![];
4426        let body = vec![HirStmt::Try {
4427            body: vec![HirStmt::Expr(HirExpr::Var("resource".to_string()))],
4428            handlers: vec![],
4429            orelse: None,
4430            finalbody: None,
4431        }];
4432        let outer_vars: HashSet<String> = ["resource".to_string()].into_iter().collect();
4433        assert!(captures_outer_scope(&params, &body, &outer_vars));
4434    }
4435
4436    #[test]
4437    fn test_captures_outer_scope_s9_with_context() {
4438        let params = vec![];
4439        let body = vec![HirStmt::With {
4440            context: HirExpr::Var("manager".to_string()),
4441            target: Some("m".to_string()),
4442            body: vec![],
4443            is_async: false,
4444        }];
4445        let outer_vars: HashSet<String> = ["manager".to_string()].into_iter().collect();
4446        assert!(captures_outer_scope(&params, &body, &outer_vars));
4447    }
4448
4449    #[test]
4450    fn test_captures_outer_scope_in_dict_comp() {
4451        let params = vec![];
4452        let body = vec![HirStmt::Return(Some(HirExpr::DictComp {
4453            key: Box::new(HirExpr::Var("k".to_string())),
4454            value: Box::new(HirExpr::Var("default_val".to_string())),
4455            generators: vec![HirComprehension {
4456                target: "k".to_string(),
4457                iter: Box::new(HirExpr::List(vec![])),
4458                conditions: vec![],
4459            }],
4460        }))];
4461        let outer_vars: HashSet<String> = ["default_val".to_string()].into_iter().collect();
4462        assert!(captures_outer_scope(&params, &body, &outer_vars));
4463    }
4464
4465    #[test]
4466    fn test_captures_outer_scope_in_set_comp() {
4467        let params = vec![];
4468        let body = vec![HirStmt::Return(Some(HirExpr::SetComp {
4469            element: Box::new(HirExpr::Var("transform".to_string())),
4470            generators: vec![HirComprehension {
4471                target: "x".to_string(),
4472                iter: Box::new(HirExpr::List(vec![])),
4473                conditions: vec![],
4474            }],
4475        }))];
4476        let outer_vars: HashSet<String> = ["transform".to_string()].into_iter().collect();
4477        assert!(captures_outer_scope(&params, &body, &outer_vars));
4478    }
4479
4480    #[test]
4481    fn test_captures_outer_scope_in_generator_exp() {
4482        let params = vec![];
4483        let body = vec![HirStmt::Return(Some(HirExpr::GeneratorExp {
4484            element: Box::new(HirExpr::Binary {
4485                op: BinOp::Add,
4486                left: Box::new(HirExpr::Var("x".to_string())),
4487                right: Box::new(HirExpr::Var("offset".to_string())),
4488            }),
4489            generators: vec![HirComprehension {
4490                target: "x".to_string(),
4491                iter: Box::new(HirExpr::List(vec![])),
4492                conditions: vec![],
4493            }],
4494        }))];
4495        let outer_vars: HashSet<String> = ["offset".to_string()].into_iter().collect();
4496        assert!(captures_outer_scope(&params, &body, &outer_vars));
4497    }
4498
4499    #[test]
4500    fn test_captures_outer_scope_in_compare() {
4501        let params = vec![];
4502        let body = vec![HirStmt::Return(Some(HirExpr::Binary {
4503            op: BinOp::Gt,
4504            left: Box::new(HirExpr::Var("threshold".to_string())),
4505            right: Box::new(HirExpr::Literal(Literal::Int(0))),
4506        }))];
4507        let outer_vars: HashSet<String> = ["threshold".to_string()].into_iter().collect();
4508        assert!(captures_outer_scope(&params, &body, &outer_vars));
4509    }
4510
4511    #[test]
4512    fn test_captures_outer_scope_in_lambda() {
4513        let params = vec![];
4514        let body = vec![HirStmt::Return(Some(HirExpr::Lambda {
4515            params: vec![],
4516            body: Box::new(HirExpr::Var("captured".to_string())),
4517        }))];
4518        let outer_vars: HashSet<String> = ["captured".to_string()].into_iter().collect();
4519        assert!(captures_outer_scope(&params, &body, &outer_vars));
4520    }
4521
4522    #[test]
4523    fn test_captures_outer_scope_yield() {
4524        let params = vec![];
4525        let body = vec![HirStmt::Expr(HirExpr::Yield {
4526            value: Some(Box::new(HirExpr::Var("yielded".to_string()))),
4527        })];
4528        let outer_vars: HashSet<String> = ["yielded".to_string()].into_iter().collect();
4529        assert!(captures_outer_scope(&params, &body, &outer_vars));
4530    }
4531
4532    #[test]
4533    fn test_captures_outer_scope_named_expr() {
4534        let params = vec![];
4535        let body = vec![HirStmt::Return(Some(HirExpr::NamedExpr {
4536            target: "x".to_string(),
4537            value: Box::new(HirExpr::Var("source".to_string())),
4538        }))];
4539        let outer_vars: HashSet<String> = ["source".to_string()].into_iter().collect();
4540        assert!(captures_outer_scope(&params, &body, &outer_vars));
4541    }
4542
4543    // === Transpile-based tests for more complex patterns ===
4544
4545    #[test]
4546    fn test_transpile_try_with_variable_escape() {
4547        let code = r#"
4548def escape_var(s: str) -> str:
4549    try:
4550        result = s.strip()
4551        n = int(result)
4552    except ValueError:
4553        n = 0
4554    return str(n)
4555"#;
4556        let rust = transpile(code);
4557        assert!(rust.contains("fn escape_var"), "output: {}", rust);
4558    }
4559
4560    #[test]
4561    fn test_transpile_nested_fn_recursive() {
4562        let code = r#"
4563def counter() -> int:
4564    count = 0
4565    def increment() -> int:
4566        return count + 1
4567    return increment()
4568"#;
4569        let rust = transpile(code);
4570        assert!(rust.contains("fn counter"), "output: {}", rust);
4571    }
4572
4573    #[test]
4574    fn test_transpile_try_with_type_error() {
4575        let code = r#"
4576def type_safe(x: int) -> str:
4577    try:
4578        return str(x)
4579    except TypeError:
4580        return "type error"
4581"#;
4582        let rust = transpile(code);
4583        assert!(rust.contains("fn type_safe"), "output: {}", rust);
4584    }
4585
4586    #[test]
4587    fn test_transpile_try_with_attribute_error() {
4588        let code = r#"
4589def attr_safe(s: str) -> str:
4590    try:
4591        return s.upper()
4592    except AttributeError:
4593        return ""
4594"#;
4595        let rust = transpile(code);
4596        assert!(rust.contains("fn attr_safe"), "output: {}", rust);
4597    }
4598
4599    #[test]
4600    fn test_transpile_try_all_except_types() {
4601        let code = r#"
4602def robust(x: int) -> int:
4603    try:
4604        return x + 1
4605    except (ValueError, TypeError, IndexError):
4606        return 0
4607"#;
4608        let rust = transpile(code);
4609        assert!(rust.contains("fn robust"), "output: {}", rust);
4610    }
4611
4612    #[test]
4613    fn test_transpile_del_statement() {
4614        let code = r#"
4615def cleanup() -> None:
4616    x = 10
4617    del x
4618"#;
4619        let rust = transpile(code);
4620        assert!(rust.contains("fn cleanup"), "output: {}", rust);
4621    }
4622
4623    #[test]
4624    fn test_transpile_global_statement() {
4625        let code = r#"
4626counter = 0
4627
4628def increment() -> int:
4629    global counter
4630    counter += 1
4631    return counter
4632"#;
4633        let rust = transpile(code);
4634        assert!(
4635            rust.contains("fn increment") || rust.contains("counter"),
4636            "output: {}",
4637            rust
4638        );
4639    }
4640
4641    #[test]
4642    fn test_transpile_nested_fn_with_default_param() {
4643        let code = r#"
4644def outer() -> int:
4645    base = 10
4646    def inner(x: int = 5) -> int:
4647        return x + base
4648    return inner()
4649"#;
4650        let rust = transpile(code);
4651        assert!(rust.contains("fn outer"), "output: {}", rust);
4652    }
4653
4654    #[test]
4655    fn test_transpile_try_except_with_list_ops_in_handler() {
4656        let code = r#"
4657def collect_errors() -> list:
4658    errors = []
4659    try:
4660        x = int("bad")
4661    except ValueError:
4662        errors.append("parse error")
4663    return errors
4664"#;
4665        let rust = transpile(code);
4666        assert!(rust.contains("fn collect_errors"), "output: {}", rust);
4667    }
4668
4669    // === S9 Batch 3: try/except edge cases ===
4670
4671    #[test]
4672    fn test_s9_try_except_bool_return_with_finally() {
4673        let code = r#"
4674def check_valid(s: str) -> bool:
4675    try:
4676        x = int(s)
4677        return True
4678    except ValueError:
4679        return False
4680    finally:
4681        print("done")
4682"#;
4683        let rust = transpile(code);
4684        assert!(rust.contains("fn check_valid"), "output: {}", rust);
4685    }
4686
4687    #[test]
4688    fn test_s9_try_except_with_string_return_fallback() {
4689        let code = r#"
4690def safe_parse(s: str) -> str:
4691    try:
4692        return str(int(s))
4693    except ValueError:
4694        return "invalid"
4695"#;
4696        let rust = transpile(code);
4697        assert!(rust.contains("fn safe_parse"), "output: {}", rust);
4698    }
4699
4700    #[test]
4701    fn test_s9_try_except_with_float_fallback() {
4702        let code = r#"
4703def to_float(s: str) -> float:
4704    try:
4705        return float(s)
4706    except ValueError:
4707        return 0.0
4708"#;
4709        let rust = transpile(code);
4710        assert!(rust.contains("fn to_float"), "output: {}", rust);
4711    }
4712
4713    #[test]
4714    fn test_s9_try_except_with_negative_int_fallback() {
4715        let code = r#"
4716def parse_or_neg(s: str) -> int:
4717    try:
4718        return int(s)
4719    except ValueError:
4720        return -1
4721"#;
4722        let rust = transpile(code);
4723        assert!(rust.contains("fn parse_or_neg"), "output: {}", rust);
4724    }
4725
4726    #[test]
4727    fn test_s9_try_except_with_exception_binding() {
4728        let code = r#"
4729def handle_error(s: str) -> str:
4730    try:
4731        x = int(s)
4732        return str(x)
4733    except ValueError as e:
4734        return str(e)
4735"#;
4736        let rust = transpile(code);
4737        assert!(rust.contains("fn handle_error"), "output: {}", rust);
4738    }
4739
4740    #[test]
4741    fn test_s9_try_except_assignment_in_body_and_handler() {
4742        let code = r#"
4743def dual_assign(s: str) -> int:
4744    result = 0
4745    try:
4746        result = int(s)
4747    except ValueError:
4748        result = -1
4749    return result
4750"#;
4751        let rust = transpile(code);
4752        assert!(rust.contains("fn dual_assign"), "output: {}", rust);
4753    }
4754
4755    #[test]
4756    fn test_s9_try_except_with_loop_in_body() {
4757        let code = r#"
4758def sum_strings(items: list) -> int:
4759    total = 0
4760    try:
4761        for item in items:
4762            total += int(item)
4763    except ValueError:
4764        total = -1
4765    return total
4766"#;
4767        let rust = transpile(code);
4768        assert!(rust.contains("fn sum_strings"), "output: {}", rust);
4769    }
4770
4771    #[test]
4772    fn test_s9_nested_function_recursive() {
4773        let code = r#"
4774def outer(n: int) -> int:
4775    def factorial(x: int) -> int:
4776        if x <= 1:
4777            return 1
4778        return x * factorial(x - 1)
4779    return factorial(n)
4780"#;
4781        let rust = transpile(code);
4782        assert!(rust.contains("fn outer"), "output: {}", rust);
4783    }
4784
4785    #[test]
4786    fn test_s9_nested_function_with_string_param() {
4787        let code = r#"
4788def process(text: str) -> str:
4789    def transform(s: str) -> str:
4790        return s.upper()
4791    return transform(text)
4792"#;
4793        let rust = transpile(code);
4794        assert!(rust.contains("fn process"), "output: {}", rust);
4795    }
4796
4797    #[test]
4798    fn test_s9_nested_function_with_list_param() {
4799        let code = r#"
4800def filter_positive(nums: list) -> list:
4801    def is_positive(n: int) -> bool:
4802        return n > 0
4803    result = []
4804    for n in nums:
4805        if is_positive(n):
4806            result.append(n)
4807    return result
4808"#;
4809        let rust = transpile(code);
4810        assert!(rust.contains("fn filter_positive"), "output: {}", rust);
4811    }
4812
4813    #[test]
4814    fn test_s9_nested_function_with_dict_param() {
4815        let code = r#"
4816def get_value(data: dict, key: str) -> int:
4817    def lookup(d: dict, k: str) -> int:
4818        return d.get(k, 0)
4819    return lookup(data, key)
4820"#;
4821        let rust = transpile(code);
4822        assert!(rust.contains("fn get_value"), "output: {}", rust);
4823    }
4824
4825    #[test]
4826    fn test_s9_nested_function_void_return() {
4827        let code = r#"
4828def run() -> None:
4829    def helper() -> None:
4830        print("help")
4831    helper()
4832"#;
4833        let rust = transpile(code);
4834        assert!(rust.contains("fn run"), "output: {}", rust);
4835    }
4836
4837    #[test]
4838    fn test_s9_try_except_multiple_handlers_with_binding() {
4839        let code = r#"
4840def multi_catch(s: str) -> str:
4841    try:
4842        return str(int(s))
4843    except ValueError as e:
4844        return "value error"
4845    except TypeError as e:
4846        return "type error"
4847"#;
4848        let rust = transpile(code);
4849        assert!(rust.contains("fn multi_catch"), "output: {}", rust);
4850    }
4851
4852    #[test]
4853    fn test_s9_try_except_floor_div_zero_division() {
4854        let code = r#"
4855def safe_floor_div(a: int, b: int) -> int:
4856    try:
4857        return a // b
4858    except ZeroDivisionError:
4859        return 0
4860"#;
4861        let rust = transpile(code);
4862        assert!(rust.contains("fn safe_floor_div"), "output: {}", rust);
4863    }
4864
4865    #[test]
4866    fn test_s9_nested_try_except() {
4867        let code = r#"
4868def nested_error_handling(s: str) -> int:
4869    try:
4870        try:
4871            return int(s)
4872        except ValueError:
4873            return -1
4874    except TypeError:
4875        return -2
4876"#;
4877        let rust = transpile(code);
4878        assert!(rust.contains("fn nested_error_handling"), "output: {}", rust);
4879    }
4880
4881    #[test]
4882    fn test_s9_try_finally_no_except() {
4883        let code = r#"
4884def always_cleanup() -> int:
4885    x = 0
4886    try:
4887        x = 42
4888    finally:
4889        print("cleanup")
4890    return x
4891"#;
4892        let rust = transpile(code);
4893        assert!(rust.contains("fn always_cleanup"), "output: {}", rust);
4894    }
4895
4896    #[test]
4897    fn test_s9_nested_function_with_return_type_list() {
4898        let code = r#"
4899def make_list() -> list:
4900    def create() -> list:
4901        result = []
4902        result.append(1)
4903        result.append(2)
4904        return result
4905    return create()
4906"#;
4907        let rust = transpile(code);
4908        assert!(rust.contains("fn make_list"), "output: {}", rust);
4909    }
4910
4911    // === S9 Batch 6: Uncovered code paths ===
4912
4913    #[test]
4914    fn test_s9b6_parse_from_tokens_expr_stmt() {
4915        let code = r#"
4916def parse_int(s: str) -> int:
4917    x = int("42")
4918    return x
4919"#;
4920        let rust = transpile(code);
4921        assert!(rust.contains("fn parse_int"), "output: {}", rust);
4922    }
4923
4924    #[test]
4925    fn test_s9b6_parse_from_tokens_in_try() {
4926        let code = r#"
4927def safe_parse(s: str) -> int:
4928    try:
4929        x = int(s)
4930        return x
4931    except ValueError:
4932        return 0
4933"#;
4934        let rust = transpile(code);
4935        assert!(rust.contains("fn safe_parse"), "output: {}", rust);
4936    }
4937
4938    #[test]
4939    fn test_s9b6_try_with_else_block() {
4940        let code = r#"
4941def try_else(s: str) -> str:
4942    try:
4943        x = int(s)
4944    except ValueError:
4945        return "error"
4946    else:
4947        return "success"
4948"#;
4949        let rust = transpile(code);
4950        assert!(rust.contains("fn try_else"), "output: {}", rust);
4951    }
4952
4953    #[test]
4954    fn test_s9b6_bare_except() {
4955        let code = r#"
4956def catch_all(s: str) -> int:
4957    try:
4958        return int(s)
4959    except:
4960        return -1
4961"#;
4962        let rust = transpile(code);
4963        assert!(rust.contains("fn catch_all"), "output: {}", rust);
4964    }
4965
4966    #[test]
4967    fn test_s9b6_multiple_exception_types() {
4968        let code = r#"
4969def multi_handler(s: str) -> int:
4970    try:
4971        return int(s)
4972    except ValueError:
4973        return -1
4974    except TypeError:
4975        return -2
4976    except KeyError:
4977        return -3
4978"#;
4979        let rust = transpile(code);
4980        assert!(rust.contains("fn multi_handler"), "output: {}", rust);
4981    }
4982
4983    #[test]
4984    fn test_s9b6_string_ops_in_try() {
4985        let code = r#"
4986def string_transform(s: str) -> str:
4987    try:
4988        result = s.upper()
4989        result = result.strip()
4990        return result
4991    except AttributeError:
4992        return ""
4993"#;
4994        let rust = transpile(code);
4995        assert!(rust.contains("fn string_transform"), "output: {}", rust);
4996    }
4997
4998    #[test]
4999    fn test_s9b6_list_ops_in_try() {
5000        let code = r#"
5001def list_builder(n: int) -> list:
5002    try:
5003        items = []
5004        items.append(n)
5005        items.append(n * 2)
5006        return items
5007    except:
5008        return []
5009"#;
5010        let rust = transpile(code);
5011        assert!(rust.contains("fn list_builder"), "output: {}", rust);
5012    }
5013
5014    #[test]
5015    fn test_s9b6_nested_try_blocks() {
5016        let code = r#"
5017def nested_try(s: str) -> int:
5018    try:
5019        outer = int(s)
5020        try:
5021            inner = outer * 2
5022            return inner
5023        except:
5024            return outer
5025    except ValueError:
5026        return 0
5027"#;
5028        let rust = transpile(code);
5029        assert!(rust.contains("fn nested_try"), "output: {}", rust);
5030    }
5031
5032    #[test]
5033    fn test_s9b6_assignment_in_try_no_annotation() {
5034        let code = r#"
5035def infer_type(s: str) -> int:
5036    try:
5037        result = int(s)
5038    except ValueError:
5039        result = 0
5040    return result
5041"#;
5042        let rust = transpile(code);
5043        assert!(rust.contains("fn infer_type"), "output: {}", rust);
5044    }
5045
5046    #[test]
5047    fn test_s9b6_nested_function_returns_dict() {
5048        let code = r#"
5049def make_config() -> dict:
5050    def builder() -> dict:
5051        config = {}
5052        config["key"] = "value"
5053        return config
5054    return builder()
5055"#;
5056        let rust = transpile(code);
5057        assert!(rust.contains("fn make_config"), "output: {}", rust);
5058    }
5059
5060    #[test]
5061    fn test_s9b6_nested_function_with_capture_via_with() {
5062        let code = r#"
5063def with_context(filename: str) -> str:
5064    with open(filename) as f:
5065        def reader() -> str:
5066            return f.read()
5067        return reader()
5068"#;
5069        let rust = transpile(code);
5070        assert!(rust.contains("fn with_context"), "output: {}", rust);
5071    }
5072
5073    #[test]
5074    fn test_s9b6_nested_function_try_except_capture() {
5075        let code = r#"
5076def error_handler(s: str) -> int:
5077    try:
5078        base = int(s)
5079        def multiply() -> int:
5080            return base * 2
5081        return multiply()
5082    except ValueError:
5083        return 0
5084"#;
5085        let rust = transpile(code);
5086        assert!(rust.contains("fn error_handler"), "output: {}", rust);
5087    }
5088
5089    #[test]
5090    fn test_s9b6_try_with_finally_and_else() {
5091        let code = r#"
5092def full_try(s: str) -> int:
5093    try:
5094        x = int(s)
5095    except ValueError:
5096        return -1
5097    else:
5098        return x
5099    finally:
5100        print("cleanup")
5101"#;
5102        let rust = transpile(code);
5103        assert!(rust.contains("fn full_try"), "output: {}", rust);
5104    }
5105
5106    #[test]
5107    fn test_s9b6_recursive_nested_function_no_captures() {
5108        let code = r#"
5109def outer() -> int:
5110    def fibonacci(n: int) -> int:
5111        if n <= 1:
5112            return n
5113        return fibonacci(n - 1) + fibonacci(n - 2)
5114    return fibonacci(5)
5115"#;
5116        let rust = transpile(code);
5117        assert!(rust.contains("fn outer"), "output: {}", rust);
5118    }
5119
5120    #[test]
5121    fn test_s9b6_try_else_with_list_return() {
5122        let code = r#"
5123def parse_list(items: list) -> list:
5124    result = []
5125    try:
5126        for item in items:
5127            result.append(int(item))
5128    except ValueError:
5129        return []
5130    else:
5131        return result
5132"#;
5133        let rust = transpile(code);
5134        assert!(rust.contains("fn parse_list"), "output: {}", rust);
5135    }
5136
5137    #[test]
5138    fn test_s9b6_with_stmt_args_field_access() {
5139        let code = r#"
5140def process_file(args) -> str:
5141    with open(args.filename) as f:
5142        return f.read()
5143"#;
5144        let rust = transpile(code);
5145        assert!(rust.contains("fn process_file"), "output: {}", rust);
5146    }
5147
5148    #[test]
5149    fn test_s9b6_try_else_finally_args_access() {
5150        let code = r#"
5151def process_input(args) -> int:
5152    try:
5153        result = int(args.value)
5154    except ValueError:
5155        return 0
5156    else:
5157        return result
5158    finally:
5159        print(args.verbose)
5160"#;
5161        let rust = transpile(code);
5162        assert!(rust.contains("fn process_input"), "output: {}", rust);
5163    }
5164
5165    #[test]
5166    fn test_s9b6_nested_function_returns_tuple() {
5167        let code = r#"
5168def make_pair(x: int, y: int) -> tuple:
5169    def pair() -> tuple:
5170        return (x, y)
5171    return pair()
5172"#;
5173        let rust = transpile(code);
5174        assert!(rust.contains("fn make_pair"), "output: {}", rust);
5175    }
5176
5177    #[test]
5178    fn test_s9b6_try_with_break_continue() {
5179        let code = r#"
5180def find_valid(items: list) -> int:
5181    for item in items:
5182        try:
5183            x = int(item)
5184            if x > 0:
5185                return x
5186            continue
5187        except ValueError:
5188            break
5189    return 0
5190"#;
5191        let rust = transpile(code);
5192        assert!(rust.contains("fn find_valid"), "output: {}", rust);
5193    }
5194
5195    #[test]
5196    fn test_s9b6_multiple_nested_functions() {
5197        let code = r#"
5198def outer() -> int:
5199    def add(a: int, b: int) -> int:
5200        return a + b
5201    def multiply(a: int, b: int) -> int:
5202        return a * b
5203    return add(2, 3) + multiply(4, 5)
5204"#;
5205        let rust = transpile(code);
5206        assert!(rust.contains("fn outer"), "output: {}", rust);
5207    }
5208
5209    #[test]
5210    fn test_s9b6_try_except_with_walrus() {
5211        let code = r#"
5212def walrus_try(s: str) -> int:
5213    try:
5214        if (x := int(s)) > 0:
5215            return x
5216    except ValueError:
5217        return 0
5218    return -1
5219"#;
5220        let rust = transpile(code);
5221        assert!(rust.contains("fn walrus_try"), "output: {}", rust);
5222    }
5223
5224    #[test]
5225    fn test_s9b6_nested_function_with_nested_loop() {
5226        let code = r#"
5227def matrix_sum(rows: list) -> int:
5228    def sum_row(row: list) -> int:
5229        total = 0
5230        for item in row:
5231            total += item
5232        return total
5233    result = 0
5234    for row in rows:
5235        result += sum_row(row)
5236    return result
5237"#;
5238        let rust = transpile(code);
5239        assert!(rust.contains("fn matrix_sum"), "output: {}", rust);
5240    }
5241
5242    #[test]
5243    fn test_s9b6_try_except_type_annotation_escape() {
5244        let code = r#"
5245def annotated_escape(s: str) -> int:
5246    x: int
5247    try:
5248        x = int(s)
5249    except ValueError:
5250        x = 0
5251    return x
5252"#;
5253        let rust = transpile(code);
5254        assert!(rust.contains("fn annotated_escape"), "output: {}", rust);
5255    }
5256
5257    #[test]
5258    fn test_s9b6_nested_try_with_else() {
5259        let code = r#"
5260def nested_else(s: str) -> str:
5261    try:
5262        x = int(s)
5263        try:
5264            y = x * 2
5265        except:
5266            return "inner error"
5267        else:
5268            return "inner success"
5269    except ValueError:
5270        return "outer error"
5271"#;
5272        let rust = transpile(code);
5273        assert!(rust.contains("fn nested_else"), "output: {}", rust);
5274    }
5275
5276    #[test]
5277    fn test_s9b6_exception_tuple_handler() {
5278        let code = r#"
5279def tuple_catch(s: str) -> int:
5280    try:
5281        return int(s)
5282    except (ValueError, TypeError, AttributeError):
5283        return -1
5284"#;
5285        let rust = transpile(code);
5286        assert!(rust.contains("fn tuple_catch"), "output: {}", rust);
5287    }
5288
5289    #[test]
5290    fn test_s9b6_nested_function_bool_return() {
5291        let code = r#"
5292def outer() -> bool:
5293    x = 10
5294    def check() -> bool:
5295        return x > 5
5296    return check()
5297"#;
5298        let rust = transpile(code);
5299        assert!(rust.contains("fn outer"), "output: {}", rust);
5300    }
5301
5302    #[test]
5303    fn test_s9b6_try_finally_with_return_in_finally() {
5304        let code = r#"
5305def finally_return(s: str) -> int:
5306    try:
5307        x = int(s)
5308        return x
5309    finally:
5310        return 0
5311"#;
5312        let rust = transpile(code);
5313        assert!(rust.contains("fn finally_return"), "output: {}", rust);
5314    }
5315
5316    #[test]
5317    fn test_s9b6_complex_nested_capture() {
5318        let code = r#"
5319def complex_capture(base: int) -> int:
5320    multiplier = 2
5321    def level1() -> int:
5322        def level2() -> int:
5323            return base * multiplier
5324        return level2() + base
5325    return level1()
5326"#;
5327        let rust = transpile(code);
5328        assert!(rust.contains("fn complex_capture"), "output: {}", rust);
5329    }
5330
5331    #[test]
5332    fn test_s9b6_try_except_with_dict_ops() {
5333        let code = r#"
5334def dict_ops(key: str) -> dict:
5335    try:
5336        result = {}
5337        result[key] = "value"
5338        result["count"] = 1
5339        return result
5340    except:
5341        return {}
5342"#;
5343        let rust = transpile(code);
5344        assert!(rust.contains("fn dict_ops"), "output: {}", rust);
5345    }
5346}