Skip to content

Commit 939b2dc

Browse files
noahgiftclaude
andcommitted
fix(codegen): Add mut to loop variables that are reassigned in body (Refs DEPYLER-0756)
When Python code like `for line in lines: line = line.strip()` is transpiled, the loop variable needs `mut` in Rust because it's being reassigned in the loop body. Added is_var_reassigned_in_stmt() helper function that detects when a loop variable is the target of an assignment statement within the loop body. Modified codegen_for_stmt to generate `for mut var in ...` instead of `for var in ...` when reassignment is detected. This fixes E0384 "cannot assign twice to immutable variable" errors. Also fixed minor clippy warning in external_deps_mapping_test.rs. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent 1cf18c3 commit 939b2dc

File tree

2 files changed

+71
-6
lines changed

2 files changed

+71
-6
lines changed

crates/depyler-core/src/rust_gen/stmt_gen.rs

Lines changed: 70 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2217,6 +2217,52 @@ fn is_var_used_as_dict_key_in_stmt(var_name: &str, stmt: &HirStmt) -> bool {
22172217
}
22182218
}
22192219

2220+
/// Check if a variable is reassigned in a statement
2221+
/// DEPYLER-0756: Loop variables that are reassigned need `mut` in for pattern
2222+
fn is_var_reassigned_in_stmt(var_name: &str, stmt: &HirStmt) -> bool {
2223+
match stmt {
2224+
HirStmt::Assign { target, .. } => {
2225+
// Only count as reassignment if the target is the exact variable
2226+
// Note: Augmented assignment (+=, -=) is also represented as Assign in HIR
2227+
matches!(target, AssignTarget::Symbol(name) if name == var_name)
2228+
}
2229+
HirStmt::If {
2230+
then_body,
2231+
else_body,
2232+
..
2233+
} => {
2234+
then_body
2235+
.iter()
2236+
.any(|s| is_var_reassigned_in_stmt(var_name, s))
2237+
|| else_body.as_ref().is_some_and(|body| {
2238+
body.iter().any(|s| is_var_reassigned_in_stmt(var_name, s))
2239+
})
2240+
}
2241+
HirStmt::While { body, .. } => body.iter().any(|s| is_var_reassigned_in_stmt(var_name, s)),
2242+
HirStmt::For { body, .. } => body.iter().any(|s| is_var_reassigned_in_stmt(var_name, s)),
2243+
HirStmt::Try {
2244+
body,
2245+
handlers,
2246+
orelse,
2247+
finalbody,
2248+
..
2249+
} => {
2250+
body.iter().any(|s| is_var_reassigned_in_stmt(var_name, s))
2251+
|| handlers
2252+
.iter()
2253+
.any(|h| h.body.iter().any(|s| is_var_reassigned_in_stmt(var_name, s)))
2254+
|| orelse.as_ref().is_some_and(|stmts| {
2255+
stmts.iter().any(|s| is_var_reassigned_in_stmt(var_name, s))
2256+
})
2257+
|| finalbody.as_ref().is_some_and(|stmts| {
2258+
stmts.iter().any(|s| is_var_reassigned_in_stmt(var_name, s))
2259+
})
2260+
}
2261+
HirStmt::With { body, .. } => body.iter().any(|s| is_var_reassigned_in_stmt(var_name, s)),
2262+
_ => false,
2263+
}
2264+
}
2265+
22202266
/// Check if a variable is used in a statement
22212267
/// DEPYLER-0303 Phase 2: Fixed to check assignment targets too (for `d[k] = v`)
22222268
fn is_var_used_in_stmt(var_name: &str, stmt: &HirStmt) -> bool {
@@ -2360,6 +2406,13 @@ pub(crate) fn codegen_for_stmt(
23602406
body.iter().any(|stmt| is_var_used_in_stmt(var_name, stmt))
23612407
};
23622408

2409+
// DEPYLER-0756: Helper to check if a variable is reassigned in the loop body
2410+
// If a loop variable is reassigned (e.g., `line = line.strip()`), we need `mut`
2411+
let is_reassigned_in_body = |var_name: &str| -> bool {
2412+
body.iter()
2413+
.any(|stmt| is_var_reassigned_in_stmt(var_name, stmt))
2414+
};
2415+
23632416
// Generate target pattern based on AssignTarget type
23642417
let target_pattern: syn::Pat = match target {
23652418
AssignTarget::Symbol(name) => {
@@ -2370,11 +2423,17 @@ pub(crate) fn codegen_for_stmt(
23702423
format!("_{}", name)
23712424
};
23722425
let ident = safe_ident(&var_name); // DEPYLER-0023
2373-
parse_quote! { #ident }
2426+
// DEPYLER-0756: Add `mut` if variable is reassigned inside the loop
2427+
if is_reassigned_in_body(name) {
2428+
parse_quote! { mut #ident }
2429+
} else {
2430+
parse_quote! { #ident }
2431+
}
23742432
}
23752433
AssignTarget::Tuple(targets) => {
23762434
// For tuple unpacking, check each variable individually
2377-
let idents: Vec<syn::Ident> = targets
2435+
// DEPYLER-0756: Check if any tuple element is reassigned
2436+
let patterns: Vec<syn::Pat> = targets
23782437
.iter()
23792438
.map(|t| match t {
23802439
AssignTarget::Symbol(s) => {
@@ -2384,12 +2443,18 @@ pub(crate) fn codegen_for_stmt(
23842443
} else {
23852444
format!("_{}", s)
23862445
};
2387-
safe_ident(&var_name) // DEPYLER-0023
2446+
let ident = safe_ident(&var_name); // DEPYLER-0023
2447+
// DEPYLER-0756: Add `mut` if this tuple element is reassigned
2448+
if is_reassigned_in_body(s) {
2449+
parse_quote! { mut #ident }
2450+
} else {
2451+
parse_quote! { #ident }
2452+
}
23882453
}
2389-
_ => safe_ident("_nested"), // Nested tuple unpacking - use placeholder
2454+
_ => parse_quote! { _nested }, // Nested tuple unpacking - use placeholder
23902455
})
23912456
.collect();
2392-
parse_quote! { (#(#idents),*) }
2457+
parse_quote! { (#(#patterns),*) }
23932458
}
23942459
_ => bail!("Unsupported for loop target type"),
23952460
};

crates/depyler-core/tests/external_deps_mapping_test.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -776,7 +776,7 @@ mod integration {
776776

777777
// External deps should be present
778778
assert!(
779-
dep_names.contains(&"trueno") || !mapper.get_mapping("numpy").is_some(),
779+
dep_names.contains(&"trueno") || mapper.get_mapping("numpy").is_none(),
780780
"trueno should be in deps if numpy mapping exists"
781781
);
782782
assert!(dep_names.contains(&"serde_json"));

0 commit comments

Comments
 (0)