use crate::annotation_aware_type_mapper::AnnotationAwareTypeMapper;
use crate::hir::*;
use crate::lifetime_analysis::LifetimeInference;
use crate::string_optimization::{StringContext, StringOptimizer};
use anyhow::{bail, Result};
use quote::{quote, ToTokens};
use std::collections::HashSet;
use syn::{self, parse_quote};
// Module declarations for rust_gen refactoring (v3.18.0 Phases 2-4)
mod context;
mod error_gen;
mod format;
mod generator_gen;
mod import_gen;
mod type_gen;
// Internal imports
use error_gen::generate_error_type_definitions;
use format::format_rust_code;
use generator_gen::codegen_generator_function;
use import_gen::process_module_imports;
use type_gen::{convert_binop, update_import_needs};
// Public re-exports for external modules (union_enum_gen, etc.)
pub use context::{CodeGenContext, RustCodeGen, ToRustExpr};
pub use type_gen::rust_type_to_syn;
/// Analyze functions for string optimization
///
/// Performs string optimization analysis on all functions.
/// Complexity: 2 (well within ≤10 target)
fn analyze_string_optimization(ctx: &mut CodeGenContext, functions: &[HirFunction]) {
for func in functions {
ctx.string_optimizer.analyze_function(func);
}
}
/// Analyze which variables are reassigned (mutated) in a list of statements
///
/// Populates ctx.mutable_vars with variables that are reassigned after declaration.
/// Complexity: 4 (loop + match + if)
fn analyze_mutable_vars(stmts: &[HirStmt], ctx: &mut CodeGenContext) {
let mut declared = HashSet::new();
fn analyze_stmt(stmt: &HirStmt, declared: &mut HashSet<String>, mutable: &mut HashSet<String>) {
match stmt {
HirStmt::Assign { target, .. } => {
match target {
AssignTarget::Symbol(name) => {
if declared.contains(name) {
// Variable is being reassigned - mark as mutable
mutable.insert(name.clone());
} else {
// First declaration
declared.insert(name.clone());
}
}
AssignTarget::Tuple(targets) => {
// Tuple assignment - analyze each element
for t in targets {
if let AssignTarget::Symbol(name) = t {
if declared.contains(name) {
// Variable is being reassigned - mark as mutable
mutable.insert(name.clone());
} else {
// First declaration
declared.insert(name.clone());
}
}
}
}
_ => {}
}
}
HirStmt::If {
then_body,
else_body,
..
} => {
for stmt in then_body {
analyze_stmt(stmt, declared, mutable);
}
if let Some(else_stmts) = else_body {
for stmt in else_stmts {
analyze_stmt(stmt, declared, mutable);
}
}
}
HirStmt::While { body, .. } | HirStmt::For { body, .. } => {
for stmt in body {
analyze_stmt(stmt, declared, mutable);
}
}
_ => {}
}
}
for stmt in stmts {
analyze_stmt(stmt, &mut declared, &mut ctx.mutable_vars);
}
}
/// Convert Python classes to Rust structs
///
/// Processes all classes and generates token streams.
/// Complexity: 3 (well within ≤10 target)
fn convert_classes_to_rust(
classes: &[HirClass],
type_mapper: &crate::type_mapper::TypeMapper,
) -> Result<Vec<proc_macro2::TokenStream>> {
let mut class_items = Vec::new();
for class in classes {
let items = crate::direct_rules::convert_class_to_struct(class, type_mapper)?;
for item in items {
let tokens = item.to_token_stream();
class_items.push(tokens);
}
}
Ok(class_items)
}
/// Convert HIR functions to Rust token streams
///
/// Processes all functions using the code generation context.
/// Complexity: 2 (well within ≤10 target)
fn convert_functions_to_rust(
functions: &[HirFunction],
ctx: &mut CodeGenContext,
) -> Result<Vec<proc_macro2::TokenStream>> {
functions
.iter()
.map(|f| f.to_rust_tokens(ctx))
.collect::<Result<Vec<_>>>()
}
/// Generate conditional imports based on code generation context
///
/// Adds imports for collections and smart pointers as needed.
/// Complexity: 1 (data-driven approach, well within ≤10 target)
fn generate_conditional_imports(ctx: &CodeGenContext) -> Vec<proc_macro2::TokenStream> {
let mut imports = Vec::new();
// Define all possible conditional imports
let conditional_imports = [
(ctx.needs_hashmap, quote! { use std::collections::HashMap; }),
(ctx.needs_hashset, quote! { use std::collections::HashSet; }),
(ctx.needs_fnv_hashmap, quote! { use fnv::FnvHashMap; }),
(ctx.needs_ahash_hashmap, quote! { use ahash::AHashMap; }),
(ctx.needs_arc, quote! { use std::sync::Arc; }),
(ctx.needs_rc, quote! { use std::rc::Rc; }),
(ctx.needs_cow, quote! { use std::borrow::Cow; }),
];
// Add imports where needed
for (needed, import_tokens) in conditional_imports {
if needed {
imports.push(import_tokens);
}
}
imports
}
/// Generate import token streams from Python imports
///
/// Maps Python imports to Rust use statements.
/// Complexity: ~7-8 (within ≤10 target)
fn generate_import_tokens(
imports: &[Import],
module_mapper: &crate::module_mapper::ModuleMapper,
) -> Vec<proc_macro2::TokenStream> {
let mut items = Vec::new();
let mut external_imports = Vec::new();
let mut std_imports = Vec::new();
// Categorize imports
for import in imports {
let rust_imports = module_mapper.map_import(import);
for rust_import in rust_imports {
if rust_import.path.starts_with("//") {
// Comment for unmapped imports
let comment = &rust_import.path;
items.push(quote! { #[doc = #comment] });
} else if rust_import.is_external {
external_imports.push(rust_import);
} else {
std_imports.push(rust_import);
}
}
}
// Add external imports
for import in external_imports {
let path: syn::Path =
syn::parse_str(&import.path).unwrap_or_else(|_| parse_quote! { unknown });
if let Some(alias) = import.alias {
let alias_ident = syn::Ident::new(&alias, proc_macro2::Span::call_site());
items.push(quote! { use #path as #alias_ident; });
} else {
items.push(quote! { use #path; });
}
}
// Add standard library imports
for import in std_imports {
// Skip typing imports as they're handled by the type system
if import.path.starts_with("::") || import.path.is_empty() {
continue;
}
let path: syn::Path = syn::parse_str(&import.path).unwrap_or_else(|_| parse_quote! { std });
if let Some(alias) = import.alias {
let alias_ident = syn::Ident::new(&alias, proc_macro2::Span::call_site());
items.push(quote! { use #path as #alias_ident; });
} else {
items.push(quote! { use #path; });
}
}
items
}
/// Generate interned string constant tokens
///
/// Generates constant definitions for interned strings.
/// Complexity: 2 (well within ≤10 target)
fn generate_interned_string_tokens(optimizer: &StringOptimizer) -> Vec<proc_macro2::TokenStream> {
let interned_constants = optimizer.generate_interned_constants();
interned_constants
.into_iter()
.filter_map(|constant| constant.parse().ok())
.collect()
}
/// Generate a complete Rust file from HIR module
pub fn generate_rust_file(
module: &HirModule,
type_mapper: &crate::type_mapper::TypeMapper,
) -> Result<String> {
let module_mapper = crate::module_mapper::ModuleMapper::new();
// Process imports to populate the context
let (imported_modules, imported_items) =
process_module_imports(&module.imports, &module_mapper);
let mut ctx = CodeGenContext {
type_mapper,
annotation_aware_mapper: AnnotationAwareTypeMapper::with_base_mapper(type_mapper.clone()),
string_optimizer: StringOptimizer::new(),
union_enum_generator: crate::union_enum_gen::UnionEnumGenerator::new(),
generated_enums: Vec::new(),
needs_hashmap: false,
needs_hashset: false,
needs_fnv_hashmap: false,
needs_ahash_hashmap: false,
needs_arc: false,
needs_rc: false,
needs_cow: false,
declared_vars: vec![HashSet::new()],
current_function_can_fail: false,
current_return_type: None,
module_mapper,
imported_modules,
imported_items,
mutable_vars: HashSet::new(),
needs_zerodivisionerror: false,
in_generator: false,
needs_indexerror: false,
is_classmethod: false,
generator_state_vars: HashSet::new(),
};
// Analyze all functions first for string optimization
analyze_string_optimization(&mut ctx, &module.functions);
// Convert classes first (they might be used by functions)
let classes = convert_classes_to_rust(&module.classes, ctx.type_mapper)?;
// Convert all functions to detect what imports we need
let functions = convert_functions_to_rust(&module.functions, &mut ctx)?;
// Build items list with all generated code
let mut items = Vec::new();
// Add module imports (create new mapper for token generation)
let import_mapper = crate::module_mapper::ModuleMapper::new();
items.extend(generate_import_tokens(&module.imports, &import_mapper));
// Add interned string constants
items.extend(generate_interned_string_tokens(&ctx.string_optimizer));
// Add collection imports if needed
items.extend(generate_conditional_imports(&ctx));
// Add error type definitions if needed
items.extend(generate_error_type_definitions(&ctx));
// Add generated union enums
items.extend(ctx.generated_enums.clone());
// Add classes
items.extend(classes);
// Add all functions
items.extend(functions);
// Generate tests for functions if applicable
let test_gen = crate::test_generation::TestGenerator::new(Default::default());
let mut test_modules = Vec::new();
for func in &module.functions {
if let Some(test_module) = test_gen.generate_tests(func)? {
test_modules.push(test_module);
}
}
// Add test modules
items.extend(test_modules);
let file = quote! {
#(#items)*
};
Ok(format_rust_code(file.to_string()))
}
// ============================================================================
// DEPYLER-0141 Phase 1: HirFunction Helper Functions
// ============================================================================
/// Generate combined generic parameters (<'a, 'b, T, U: Bound>)
#[inline]
fn codegen_generic_params(
type_params: &[crate::generic_inference::TypeParameter],
lifetime_params: &[String],
) -> proc_macro2::TokenStream {
if type_params.is_empty() && lifetime_params.is_empty() {
return quote! {};
}
let mut all_params = Vec::new();
// Add lifetime parameters first
for lt in lifetime_params {
let lt_ident = syn::Lifetime::new(lt, proc_macro2::Span::call_site());
all_params.push(quote! { #lt_ident });
}
// Add type parameters with their bounds
for type_param in type_params {
let param_name = syn::Ident::new(&type_param.name, proc_macro2::Span::call_site());
if type_param.bounds.is_empty() {
all_params.push(quote! { #param_name });
} else {
let bounds: Vec<_> = type_param
.bounds
.iter()
.map(|b| {
let bound: syn::Path =
syn::parse_str(b).unwrap_or_else(|_| parse_quote! { Clone });
quote! { #bound }
})
.collect();
all_params.push(quote! { #param_name: #(#bounds)+* });
}
}
quote! { <#(#all_params),*> }
}
/// Generate where clause for lifetime bounds (where 'a: 'b, 'c: 'd)
#[inline]
fn codegen_where_clause(lifetime_bounds: &[(String, String)]) -> proc_macro2::TokenStream {
if lifetime_bounds.is_empty() {
return quote! {};
}
let bounds: Vec<_> = lifetime_bounds
.iter()
.map(|(from, to)| {
let from_lt = syn::Lifetime::new(from, proc_macro2::Span::call_site());
let to_lt = syn::Lifetime::new(to, proc_macro2::Span::call_site());
quote! { #from_lt: #to_lt }
})
.collect();
quote! { where #(#bounds),* }
}
/// Generate function attributes (doc comments, panic-free, termination proofs)
#[inline]
fn codegen_function_attrs(
docstring: &Option<String>,
properties: &crate::hir::FunctionProperties,
) -> Vec<proc_macro2::TokenStream> {
let mut attrs = vec![];
// Add docstring as documentation if present
if let Some(docstring) = docstring {
attrs.push(quote! {
#[doc = #docstring]
});
}
if properties.panic_free {
attrs.push(quote! {
#[doc = " Depyler: verified panic-free"]
});
}
if properties.always_terminates {
attrs.push(quote! {
#[doc = " Depyler: proven to terminate"]
});
}
attrs
}
// ============================================================================
// DEPYLER-0141 Phase 2: Medium Complexity Helpers
// ============================================================================
/// Process function body statements with proper scoping
#[inline]
fn codegen_function_body(
func: &HirFunction,
can_fail: bool,
ctx: &mut CodeGenContext,
) -> Result<Vec<proc_macro2::TokenStream>> {
// Enter function scope and declare parameters
ctx.enter_scope();
ctx.current_function_can_fail = can_fail;
ctx.current_return_type = Some(func.ret_type.clone());
for param in &func.params {
ctx.declare_var(¶m.name);
}
// Analyze which variables are mutated in the function body
analyze_mutable_vars(&func.body, ctx);
// Convert body
let body_stmts: Vec<_> = func
.body
.iter()
.map(|stmt| stmt.to_rust_tokens(ctx))
.collect::<Result<Vec<_>>>()?;
ctx.exit_scope();
ctx.current_function_can_fail = false;
ctx.current_return_type = None;
Ok(body_stmts)
}
// ============================================================================
// DEPYLER-0141 Phase 3: Complex Sections
// ============================================================================
// ========== Phase 3a: Parameter Conversion ==========
/// Convert function parameters with lifetime and borrowing analysis
#[inline]
fn codegen_function_params(
func: &HirFunction,
lifetime_result: &crate::lifetime_analysis::LifetimeResult,
ctx: &mut CodeGenContext,
) -> Result<Vec<proc_macro2::TokenStream>> {
func.params
.iter()
.map(|param| codegen_single_param(param, func, lifetime_result, ctx))
.collect()
}
/// Convert a single parameter with all borrowing strategies
fn codegen_single_param(
param: &HirParam,
func: &HirFunction,
lifetime_result: &crate::lifetime_analysis::LifetimeResult,
ctx: &mut CodeGenContext,
) -> Result<proc_macro2::TokenStream> {
let param_ident = syn::Ident::new(¶m.name, proc_macro2::Span::call_site());
// Check if parameter is mutated
let is_param_mutated = matches!(
lifetime_result.borrowing_strategies.get(¶m.name),
Some(crate::borrowing_context::BorrowingStrategy::TakeOwnership)
) && func.body.iter().any(
|stmt| matches!(stmt, HirStmt::Assign { target: AssignTarget::Symbol(s), .. } if s == ¶m.name),
);
// Get the inferred parameter info
if let Some(inferred) = lifetime_result.param_lifetimes.get(¶m.name) {
let rust_type = &inferred.rust_type;
// Handle Union type placeholders
let actual_rust_type = if let crate::type_mapper::RustType::Enum { name, variants: _ } = rust_type {
if name == "UnionType" {
if let Type::Union(types) = ¶m.ty {
let enum_name = ctx.process_union_type(types);
crate::type_mapper::RustType::Custom(enum_name)
} else {
rust_type.clone()
}
} else {
rust_type.clone()
}
} else {
rust_type.clone()
};
update_import_needs(ctx, &actual_rust_type);
let ty = apply_param_borrowing_strategy(
¶m.name,
&actual_rust_type,
inferred,
lifetime_result,
ctx,
)?;
Ok(if is_param_mutated {
quote! { mut #param_ident: #ty }
} else {
quote! { #param_ident: #ty }
})
} else {
// Fallback to original mapping
let rust_type = ctx
.annotation_aware_mapper
.map_type_with_annotations(¶m.ty, &func.annotations);
update_import_needs(ctx, &rust_type);
let ty = rust_type_to_syn(&rust_type)?;
Ok(if is_param_mutated {
quote! { mut #param_ident: #ty }
} else {
quote! { #param_ident: #ty }
})
}
}
/// Apply borrowing strategy to parameter type
fn apply_param_borrowing_strategy(
param_name: &str,
rust_type: &crate::type_mapper::RustType,
inferred: &crate::lifetime_analysis::InferredParam,
lifetime_result: &crate::lifetime_analysis::LifetimeResult,
ctx: &mut CodeGenContext,
) -> Result<syn::Type> {
let mut ty = rust_type_to_syn(rust_type)?;
// Check if we have a borrowing strategy
if let Some(strategy) = lifetime_result.borrowing_strategies.get(param_name) {
match strategy {
crate::borrowing_context::BorrowingStrategy::UseCow { lifetime } => {
ctx.needs_cow = true;
let lt = syn::Lifetime::new(lifetime, proc_macro2::Span::call_site());
ty = parse_quote! { Cow<#lt, str> };
}
_ => {
// Apply normal borrowing if needed
if inferred.should_borrow {
ty = apply_borrowing_to_type(ty, rust_type, inferred)?;
}
}
}
} else {
// Fallback to normal borrowing
if inferred.should_borrow {
ty = apply_borrowing_to_type(ty, rust_type, inferred)?;
}
}
Ok(ty)
}
/// Apply borrowing (&, &mut, with lifetime) to a type
fn apply_borrowing_to_type(
mut ty: syn::Type,
rust_type: &crate::type_mapper::RustType,
inferred: &crate::lifetime_analysis::InferredParam,
) -> Result<syn::Type> {
// Special case for strings: use &str instead of &String
if matches!(rust_type, crate::type_mapper::RustType::String) {
if let Some(ref lifetime) = inferred.lifetime {
let lt = syn::Lifetime::new(lifetime.as_str(), proc_macro2::Span::call_site());
ty = if inferred.needs_mut {
parse_quote! { &#lt mut str }
} else {
parse_quote! { &#lt str }
};
} else {
ty = if inferred.needs_mut {
parse_quote! { &mut str }
} else {
parse_quote! { &str }
};
}
} else {
// Non-string types
if let Some(ref lifetime) = inferred.lifetime {
let lt = syn::Lifetime::new(lifetime.as_str(), proc_macro2::Span::call_site());
ty = if inferred.needs_mut {
parse_quote! { &#lt mut #ty }
} else {
parse_quote! { &#lt #ty }
};
} else {
ty = if inferred.needs_mut {
parse_quote! { &mut #ty }
} else {
parse_quote! { &#ty }
};
}
}
Ok(ty)
}
// ========== String Method Return Type Analysis (v3.16.0) ==========
/// Classification of string methods by their return type semantics
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum StringMethodReturnType {
/// Returns owned String (e.g., upper, lower, strip, replace)
Owned,
/// Returns borrowed &str or bool (e.g., starts_with, is_digit)
Borrowed,
}
/// Classify a string method by its return type semantics
fn classify_string_method(method_name: &str) -> StringMethodReturnType {
match method_name {
// Transformation methods that return owned String
"upper" | "lower" | "strip" | "lstrip" | "rstrip"
| "replace" | "format" | "title" | "capitalize"
| "swapcase" | "expandtabs" | "center" | "ljust"
| "rjust" | "zfill" => StringMethodReturnType::Owned,
// Query/test methods that return bool or &str (borrowed)
"startswith" | "endswith" | "isalpha" | "isdigit"
| "isalnum" | "isspace" | "islower" | "isupper"
| "istitle" | "isascii" | "isprintable"
| "find" | "rfind" | "index" | "rindex"
| "count" => StringMethodReturnType::Borrowed,
// Default: assume owned to be safe
_ => StringMethodReturnType::Owned,
}
}
/// Check if an expression contains a string method call that returns owned String
fn contains_owned_string_method(expr: &HirExpr) -> bool {
match expr {
HirExpr::MethodCall { method, .. } => {
// Check if this method returns owned String
classify_string_method(method) == StringMethodReturnType::Owned
}
HirExpr::Binary { left, right, .. } => {
// Check both sides of binary operations
contains_owned_string_method(left) || contains_owned_string_method(right)
}
HirExpr::Unary { operand, .. } => {
contains_owned_string_method(operand)
}
HirExpr::IfExpr { body, orelse, .. } => {
// Check both branches of conditional
contains_owned_string_method(body) || contains_owned_string_method(orelse)
}
HirExpr::Call { .. }
| HirExpr::Var(_)
| HirExpr::Literal(_)
| HirExpr::List(_)
| HirExpr::Dict(_)
| HirExpr::Tuple(_)
| HirExpr::Set(_)
| HirExpr::FrozenSet(_)
| HirExpr::Index { .. }
| HirExpr::Slice { .. }
| HirExpr::Attribute { .. }
| HirExpr::Borrow { .. }
| HirExpr::ListComp { .. }
| HirExpr::SetComp { .. }
| HirExpr::Lambda { .. }
| HirExpr::Await { .. }
| HirExpr::FString { .. }
| HirExpr::Yield { .. }
| HirExpr::SortByKey { .. }
| HirExpr::GeneratorExp { .. } => false,
}
}
/// Check if the function's return expressions contain owned-returning string methods
fn function_returns_owned_string(func: &HirFunction) -> bool {
// Check all return statements in the function body
for stmt in &func.body {
if let HirStmt::Return(Some(expr)) = stmt {
if contains_owned_string_method(expr) {
return true;
}
}
}
false
}
/// Check if a type expects float values (recursively checks Option, Result, etc.)
fn return_type_expects_float(ty: &Type) -> bool {
match ty {
Type::Float => true,
Type::Optional(inner) => return_type_expects_float(inner),
Type::List(inner) => return_type_expects_float(inner),
Type::Tuple(types) => types.iter().any(return_type_expects_float),
_ => false,
}
}
// ========== Phase 3b: Return Type Generation ==========
/// Generate return type with Result wrapper and lifetime handling
#[inline]
fn codegen_return_type(
func: &HirFunction,
lifetime_result: &crate::lifetime_analysis::LifetimeResult,
ctx: &mut CodeGenContext,
) -> Result<(proc_macro2::TokenStream, crate::type_mapper::RustType, bool)> {
// Convert return type using annotation-aware mapping
let mapped_ret_type = ctx
.annotation_aware_mapper
.map_return_type_with_annotations(&func.ret_type, &func.annotations);
// Check if this is a placeholder Union enum that needs proper generation
let rust_ret_type =
if let crate::type_mapper::RustType::Enum { name, .. } = &mapped_ret_type {
if name == "UnionType" {
// Generate a proper enum name and definition from the original Union type
if let Type::Union(types) = &func.ret_type {
let enum_name = ctx.process_union_type(types);
crate::type_mapper::RustType::Custom(enum_name)
} else {
mapped_ret_type
}
} else {
mapped_ret_type
}
} else {
mapped_ret_type
};
// v3.16.0 Phase 1: Override return type to String if function returns owned via string methods
// This prevents lifetime analysis from incorrectly converting to borrowed &str
let rust_ret_type = if matches!(func.ret_type, Type::String)
&& function_returns_owned_string(func)
{
// Force owned String return, don't use lifetime borrowing
crate::type_mapper::RustType::String
} else {
rust_ret_type
};
// Update import needs based on return type
update_import_needs(ctx, &rust_ret_type);
// Check if function can fail and needs Result wrapper
let can_fail = func.properties.can_fail;
let error_type_str = if can_fail && !func.properties.error_types.is_empty() {
// Use first error type or generic for mixed types
if func.properties.error_types.len() == 1 {
func.properties.error_types[0].clone()
} else {
"Box<dyn std::error::Error>".to_string()
}
} else {
"Box<dyn std::error::Error>".to_string()
};
// Mark error types as needed for type generation
if error_type_str.contains("ZeroDivisionError") {
ctx.needs_zerodivisionerror = true;
}
if error_type_str.contains("IndexError") {
ctx.needs_indexerror = true;
}
let return_type = if matches!(rust_ret_type, crate::type_mapper::RustType::Unit) {
if can_fail {
let error_type: syn::Type = syn::parse_str(&error_type_str)
.unwrap_or_else(|_| parse_quote! { Box<dyn std::error::Error> });
quote! { -> Result<(), #error_type> }
} else {
quote! {}
}
} else {
let mut ty = rust_type_to_syn(&rust_ret_type)?;
// Check if any parameter escapes through return and uses Cow
let mut uses_cow_return = false;
for param in &func.params {
if let Some(strategy) = lifetime_result.borrowing_strategies.get(¶m.name) {
if matches!(
strategy,
crate::borrowing_context::BorrowingStrategy::UseCow { .. }
) {
if let Some(_usage) = lifetime_result.param_lifetimes.get(¶m.name) {
// If a Cow parameter escapes, return type should also be Cow
if matches!(func.ret_type, crate::hir::Type::String) {
uses_cow_return = true;
break;
}
}
}
}
}
if uses_cow_return {
// Use the same Cow type for return
ctx.needs_cow = true;
if let Some(ref return_lt) = lifetime_result.return_lifetime {
let lt = syn::Lifetime::new(return_lt.as_str(), proc_macro2::Span::call_site());
ty = parse_quote! { Cow<#lt, str> };
} else {
ty = parse_quote! { Cow<'static, str> };
}
} else {
// v3.16.0 Phase 1: Check if function returns owned String via transformation methods
// If so, don't convert to borrowed &str even if lifetime analysis suggests it
let returns_owned_string = matches!(func.ret_type, Type::String)
&& function_returns_owned_string(func);
// Apply return lifetime if needed (unless returning owned String)
if let Some(ref return_lt) = lifetime_result.return_lifetime {
// Check if the return type needs lifetime substitution
if matches!(
rust_ret_type,
crate::type_mapper::RustType::Str { .. }
| crate::type_mapper::RustType::Reference { .. }
) && !returns_owned_string {
// Only apply lifetime if NOT returning owned String
let lt = syn::Lifetime::new(return_lt.as_str(), proc_macro2::Span::call_site());
match &rust_ret_type {
crate::type_mapper::RustType::Str { .. } => {
ty = parse_quote! { &#lt str };
}
crate::type_mapper::RustType::Reference { mutable, inner, .. } => {
let inner_ty = rust_type_to_syn(inner)?;
ty = if *mutable {
parse_quote! { &#lt mut #inner_ty }
} else {
parse_quote! { &#lt #inner_ty }
};
}
_ => {}
}
}
}
// If returns_owned_string is true, keep ty as String (already set from rust_type_to_syn)
}
if can_fail {
let error_type: syn::Type = syn::parse_str(&error_type_str)
.unwrap_or_else(|_| parse_quote! { Box<dyn std::error::Error> });
quote! { -> Result<#ty, #error_type> }
} else {
quote! { -> #ty }
}
};
Ok((return_type, rust_ret_type, can_fail))
}
// ========== Phase 3c: Generator Implementation ==========
// (Moved to generator_gen.rs in v3.18.0 Phase 4)
impl RustCodeGen for HirFunction {
fn to_rust_tokens(&self, ctx: &mut CodeGenContext) -> Result<proc_macro2::TokenStream> {
let name = syn::Ident::new(&self.name, proc_macro2::Span::call_site());
// Perform generic type inference
let mut generic_registry = crate::generic_inference::TypeVarRegistry::new();
let type_params = generic_registry.infer_function_generics(self)?;
// Perform lifetime analysis
let mut lifetime_inference = LifetimeInference::new();
let lifetime_result = lifetime_inference.analyze_function(self, ctx.type_mapper);
// Generate combined generic parameters (lifetimes + type params)
let generic_params = codegen_generic_params(&type_params, &lifetime_result.lifetime_params);
// Generate lifetime bounds
let where_clause = codegen_where_clause(&lifetime_result.lifetime_bounds);
// Convert parameters using lifetime analysis results
let params = codegen_function_params(self, &lifetime_result, ctx)?;
// Generate return type with Result wrapper and lifetime handling
let (return_type, rust_ret_type, can_fail) = codegen_return_type(self, &lifetime_result, ctx)?;
// Process function body with proper scoping
let body_stmts = codegen_function_body(self, can_fail, ctx)?;
// Add documentation
let attrs = codegen_function_attrs(&self.docstring, &self.properties);
// Check if function is a generator (contains yield)
let func_tokens = if self.properties.is_generator {
codegen_generator_function(
self,
&name,
&generic_params,
&where_clause,
¶ms,
&attrs,
&rust_ret_type,
ctx,
)?
} else if self.properties.is_async {
quote! {
#(#attrs)*
pub async fn #name #generic_params(#(#params),*) #return_type #where_clause {
#(#body_stmts)*
}
}
} else {
quote! {
#(#attrs)*
pub fn #name #generic_params(#(#params),*) #return_type #where_clause {
#(#body_stmts)*
}
}
};
Ok(func_tokens)
}
}
/// Helper to build nested dictionary access for assignment
/// Returns (base_expr, access_chain) where access_chain is a vec of index expressions
fn extract_nested_indices_tokens(
expr: &HirExpr,
ctx: &mut CodeGenContext,
) -> Result<(syn::Expr, Vec<syn::Expr>)> {
let mut indices = Vec::new();
let mut current = expr;
// Walk up the chain collecting indices
loop {
match current {
HirExpr::Index { base, index } => {
let index_expr = index.to_rust_expr(ctx)?;
indices.push(index_expr);
current = base;
}
_ => {
// We've reached the base
let base_expr = current.to_rust_expr(ctx)?;
indices.reverse(); // We collected from inner to outer, need outer to inner
return Ok((base_expr, indices));
}
}
}
}
/// Check if a type annotation requires explicit conversion
///
/// For numeric types like Int, we always apply conversion to ensure correctness
/// even after optimizations like CSE transform the expression.
/// Complexity: 2 (simple match)
fn needs_type_conversion(target_type: &Type) -> bool {
// For Int annotations, always apply conversion to handle cases where
// the value might be usize (from len(), range, etc.)
matches!(target_type, Type::Int)
}
/// Apply type conversion to value expression
///
/// Wraps the expression with appropriate conversion (e.g., `as i32`)
/// Complexity: 2 (simple match)
fn apply_type_conversion(value_expr: syn::Expr, target_type: &Type) -> syn::Expr {
match target_type {
Type::Int => {
// Convert to i32 using 'as' cast
// This handles usize->i32 conversions and is a no-op if already i32
// Note: Removed defensive parens - Rust's precedence rules handle this correctly
parse_quote! { #value_expr as i32 }
}
_ => value_expr,
}
}
// ============================================================================
// Statement Code Generation Helpers (DEPYLER-0140 Phase 1)
// Extracted to reduce complexity of HirStmt::to_rust_tokens
// ============================================================================
/// Generate code for Pass statement (no-op)
#[inline]
fn codegen_pass_stmt() -> Result<proc_macro2::TokenStream> {
Ok(quote! {})
}
/// Generate code for Break statement with optional label
#[inline]
fn codegen_break_stmt(label: &Option<String>) -> Result<proc_macro2::TokenStream> {
if let Some(label_name) = label {
let label_ident = syn::Lifetime::new(
&format!("'{}", label_name),
proc_macro2::Span::call_site(),
);
Ok(quote! { break #label_ident; })
} else {
Ok(quote! { break; })
}
}
/// Generate code for Continue statement with optional label
#[inline]
fn codegen_continue_stmt(label: &Option<String>) -> Result<proc_macro2::TokenStream> {
if let Some(label_name) = label {
let label_ident = syn::Lifetime::new(
&format!("'{}", label_name),
proc_macro2::Span::call_site(),
);
Ok(quote! { continue #label_ident; })
} else {
Ok(quote! { continue; })
}
}
/// Generate code for expression statement
#[inline]
fn codegen_expr_stmt(expr: &HirExpr, ctx: &mut CodeGenContext) -> Result<proc_macro2::TokenStream> {
let expr_tokens = expr.to_rust_expr(ctx)?;
Ok(quote! { #expr_tokens; })
}
// ============================================================================
// Statement Code Generation Helpers (DEPYLER-0140 Phase 2)
// Medium-complexity handlers extracted from HirStmt::to_rust_tokens
// ============================================================================
/// Generate code for Return statement with optional expression
#[inline]
fn codegen_return_stmt(
expr: &Option<HirExpr>,
ctx: &mut CodeGenContext,
) -> Result<proc_macro2::TokenStream> {
if let Some(e) = expr {
let expr_tokens = e.to_rust_expr(ctx)?;
// Check if return type is Optional and wrap value in Some()
let is_optional_return =
matches!(ctx.current_return_type.as_ref(), Some(Type::Optional(_)));
// Check if the expression is None literal
let is_none_literal = matches!(e, HirExpr::Literal(Literal::None));
if ctx.current_function_can_fail {
if is_optional_return && !is_none_literal {
// Wrap value in Some() for Optional return types
Ok(quote! { return Ok(Some(#expr_tokens)); })
} else {
Ok(quote! { return Ok(#expr_tokens); })
}
} else if is_optional_return && !is_none_literal {
// Wrap value in Some() for Optional return types
Ok(quote! { return Some(#expr_tokens); })
} else {
Ok(quote! { return #expr_tokens; })
}
} else if ctx.current_function_can_fail {
// No expression - check if return type is Optional
let is_optional_return =
matches!(ctx.current_return_type.as_ref(), Some(Type::Optional(_)));
if is_optional_return {
Ok(quote! { return Ok(None); })
} else {
Ok(quote! { return Ok(()); })
}
} else {
Ok(quote! { return; })
}
}
/// Generate code for While loop statement
#[inline]
fn codegen_while_stmt(
condition: &HirExpr,
body: &[HirStmt],
ctx: &mut CodeGenContext,
) -> Result<proc_macro2::TokenStream> {
let cond = condition.to_rust_expr(ctx)?;
ctx.enter_scope();
let body_stmts: Vec<_> = body
.iter()
.map(|s| s.to_rust_tokens(ctx))
.collect::<Result<Vec<_>>>()?;
ctx.exit_scope();
Ok(quote! {
while #cond {
#(#body_stmts)*
}
})
}
/// Generate code for Raise (exception) statement
#[inline]
fn codegen_raise_stmt(
exception: &Option<HirExpr>,
ctx: &mut CodeGenContext,
) -> Result<proc_macro2::TokenStream> {
// For V1, we'll implement basic error handling
if let Some(exc) = exception {
let exc_expr = exc.to_rust_expr(ctx)?;
Ok(quote! { return Err(#exc_expr); })
} else {
// Re-raise or bare raise - use generic error
Ok(quote! { return Err("Exception raised".into()); })
}
}
/// Generate code for With (context manager) statement
#[inline]
fn codegen_with_stmt(
context: &HirExpr,
target: &Option<String>,
body: &[HirStmt],
ctx: &mut CodeGenContext,
) -> Result<proc_macro2::TokenStream> {
// Convert context expression
let context_expr = context.to_rust_expr(ctx)?;
// Convert body statements
let body_stmts: Vec<_> = body
.iter()
.map(|stmt| stmt.to_rust_tokens(ctx))
.collect::<Result<_>>()?;
// Note: Currently generates a simple scope block for context managers.
// Proper RAII pattern with Drop trait implementation is not yet supported.
// This is a known limitation - __enter__/__exit__ methods are not translated.
if let Some(var_name) = target {
let var_ident = syn::Ident::new(var_name, proc_macro2::Span::call_site());
ctx.declare_var(var_name);
Ok(quote! {
{
let mut #var_ident = #context_expr;
#(#body_stmts)*
}
})
} else {
Ok(quote! {
{
let _context = #context_expr;
#(#body_stmts)*
}
})
}
}
// ============================================================================
// Statement Code Generation Helpers (DEPYLER-0140 Phase 3)
// Complex handlers extracted from HirStmt::to_rust_tokens
// ============================================================================
/// Generate code for If statement with optional else clause
#[inline]
fn codegen_if_stmt(
condition: &HirExpr,
then_body: &[HirStmt],
else_body: &Option<Vec<HirStmt>>,
ctx: &mut CodeGenContext,
) -> Result<proc_macro2::TokenStream> {
let cond = condition.to_rust_expr(ctx)?;
ctx.enter_scope();
let then_stmts: Vec<_> = then_body
.iter()
.map(|s| s.to_rust_tokens(ctx))
.collect::<Result<Vec<_>>>()?;
ctx.exit_scope();
if let Some(else_stmts) = else_body {
ctx.enter_scope();
let else_tokens: Vec<_> = else_stmts
.iter()
.map(|s| s.to_rust_tokens(ctx))
.collect::<Result<Vec<_>>>()?;
ctx.exit_scope();
Ok(quote! {
if #cond {
#(#then_stmts)*
} else {
#(#else_tokens)*
}
})
} else {
Ok(quote! {
if #cond {
#(#then_stmts)*
}
})
}
}
/// Generate code for For loop statement
#[inline]
fn codegen_for_stmt(
target: &str,
iter: &HirExpr,
body: &[HirStmt],
ctx: &mut CodeGenContext,
) -> Result<proc_macro2::TokenStream> {
let target_ident = syn::Ident::new(target, proc_macro2::Span::call_site());
let mut iter_expr = iter.to_rust_expr(ctx)?;
// Check if we're iterating over a borrowed collection
// If iter is a simple variable that refers to a borrowed collection (e.g., &Vec<T>),
// we need to add .iter() to properly iterate over it
if let HirExpr::Var(_var_name) = iter {
// This is a simple heuristic: if the expression is just a variable name,
// it's likely a parameter or local var that might be borrowed
// The generated code already has the variable as borrowed (e.g., data: &Vec<T>)
// so we need to call .iter() on it
iter_expr = parse_quote! { #iter_expr.iter() };
}
ctx.enter_scope();
ctx.declare_var(target); // for loop variable is declared in the loop scope
let body_stmts: Vec<_> = body
.iter()
.map(|s| s.to_rust_tokens(ctx))
.collect::<Result<Vec<_>>>()?;
ctx.exit_scope();
Ok(quote! {
for #target_ident in #iter_expr {
#(#body_stmts)*
}
})
}
/// Generate code for Assign statement (variable/index/attribute/tuple assignment)
#[inline]
fn codegen_assign_stmt(
target: &AssignTarget,
value: &HirExpr,
type_annotation: &Option<Type>,
ctx: &mut CodeGenContext,
) -> Result<proc_macro2::TokenStream> {
let mut value_expr = value.to_rust_expr(ctx)?;
// If there's a type annotation, handle type conversions
let type_annotation_tokens = if let Some(target_type) = type_annotation {
let target_rust_type = ctx.type_mapper.map_type(target_type);
let target_syn_type = rust_type_to_syn(&target_rust_type)?;
// Check if we need type conversion (e.g., usize to i32)
if needs_type_conversion(target_type) {
value_expr = apply_type_conversion(value_expr, target_type);
}
Some(quote! { : #target_syn_type })
} else {
None
};
match target {
AssignTarget::Symbol(symbol) => {
codegen_assign_symbol(symbol, value_expr, type_annotation_tokens, ctx)
}
AssignTarget::Index { base, index } => {
codegen_assign_index(base, index, value_expr, ctx)
}
AssignTarget::Attribute { value, attr } => {
codegen_assign_attribute(value, attr, value_expr, ctx)
}
AssignTarget::Tuple(targets) => {
codegen_assign_tuple(targets, value_expr, type_annotation_tokens, ctx)
}
}
}
/// Generate code for symbol (variable) assignment
#[inline]
fn codegen_assign_symbol(
symbol: &str,
value_expr: syn::Expr,
type_annotation_tokens: Option<proc_macro2::TokenStream>,
ctx: &mut CodeGenContext,
) -> Result<proc_macro2::TokenStream> {
let target_ident = syn::Ident::new(symbol, proc_macro2::Span::call_site());
// Inside generators, check if variable is a state variable
if ctx.in_generator && ctx.generator_state_vars.contains(symbol) {
// State variable assignment: self.field = value
Ok(quote! { self.#target_ident = #value_expr; })
} else if ctx.is_declared(symbol) {
// Variable already exists, just assign
Ok(quote! { #target_ident = #value_expr; })
} else {
// First declaration - check if variable needs mut
ctx.declare_var(symbol);
if ctx.mutable_vars.contains(symbol) {
if let Some(type_ann) = type_annotation_tokens {
Ok(quote! { let mut #target_ident #type_ann = #value_expr; })
} else {
Ok(quote! { let mut #target_ident = #value_expr; })
}
} else if let Some(type_ann) = type_annotation_tokens {
Ok(quote! { let #target_ident #type_ann = #value_expr; })
} else {
Ok(quote! { let #target_ident = #value_expr; })
}
}
}
/// Generate code for index (dictionary/list subscript) assignment
#[inline]
fn codegen_assign_index(
base: &HirExpr,
index: &HirExpr,
value_expr: syn::Expr,
ctx: &mut CodeGenContext,
) -> Result<proc_macro2::TokenStream> {
let final_index = index.to_rust_expr(ctx)?;
// Extract the base and all intermediate indices
let (base_expr, indices) = extract_nested_indices_tokens(base, ctx)?;
if indices.is_empty() {
// Simple assignment: d[k] = v
Ok(quote! { #base_expr.insert(#final_index, #value_expr); })
} else {
// Nested assignment: build chain of get_mut calls
let mut chain = quote! { #base_expr };
for idx in &indices {
chain = quote! {
#chain.get_mut(&#idx).unwrap()
};
}
Ok(quote! { #chain.insert(#final_index, #value_expr); })
}
}
/// Generate code for attribute (struct field) assignment
#[inline]
fn codegen_assign_attribute(
base: &HirExpr,
attr: &str,
value_expr: syn::Expr,
ctx: &mut CodeGenContext,
) -> Result<proc_macro2::TokenStream> {
let base_expr = base.to_rust_expr(ctx)?;
let attr_ident = syn::Ident::new(attr, proc_macro2::Span::call_site());
Ok(quote! { #base_expr.#attr_ident = #value_expr; })
}
/// Generate code for tuple unpacking assignment
#[inline]
fn codegen_assign_tuple(
targets: &[AssignTarget],
value_expr: syn::Expr,
_type_annotation_tokens: Option<proc_macro2::TokenStream>,
ctx: &mut CodeGenContext,
) -> Result<proc_macro2::TokenStream> {
// Check if all targets are simple symbols
let all_symbols: Option<Vec<&str>> = targets
.iter()
.map(|t| match t {
AssignTarget::Symbol(s) => Some(s.as_str()),
_ => None,
})
.collect();
match all_symbols {
Some(symbols) => {
let all_declared = symbols.iter().all(|s| ctx.is_declared(s));
if all_declared {
// All variables exist, do reassignment
let idents: Vec<_> = symbols
.iter()
.map(|s| syn::Ident::new(s, proc_macro2::Span::call_site()))
.collect();
Ok(quote! { (#(#idents),*) = #value_expr; })
} else {
// First declaration - mark each variable individually
symbols.iter().for_each(|s| ctx.declare_var(s));
let idents_with_mut: Vec<_> = symbols
.iter()
.map(|s| {
let ident = syn::Ident::new(s, proc_macro2::Span::call_site());
if ctx.mutable_vars.contains(*s) {
quote! { mut #ident }
} else {
quote! { #ident }
}
})
.collect();
Ok(quote! { let (#(#idents_with_mut),*) = #value_expr; })
}
}
None => {
bail!("Complex tuple unpacking not yet supported")
}
}
}
/// Generate code for Try/except/finally statement
#[inline]
fn codegen_try_stmt(
body: &[HirStmt],
handlers: &[ExceptHandler],
finalbody: &Option<Vec<HirStmt>>,
ctx: &mut CodeGenContext,
) -> Result<proc_macro2::TokenStream> {
// Convert try body to statements
ctx.enter_scope();
let try_stmts: Vec<_> = body
.iter()
.map(|s| s.to_rust_tokens(ctx))
.collect::<Result<Vec<_>>>()?;
ctx.exit_scope();
// Generate except handler code
let mut handler_tokens = Vec::new();
for handler in handlers {
ctx.enter_scope();
// If there's a name binding, declare it in scope
if let Some(var_name) = &handler.name {
ctx.declare_var(var_name);
}
let handler_stmts: Vec<_> = handler
.body
.iter()
.map(|s| s.to_rust_tokens(ctx))
.collect::<Result<Vec<_>>>()?;
ctx.exit_scope();
handler_tokens.push(quote! { #(#handler_stmts)* });
}
// Generate finally clause if present
let finally_stmts = if let Some(finally_body) = finalbody {
let stmts: Vec<_> = finally_body
.iter()
.map(|s| s.to_rust_tokens(ctx))
.collect::<Result<Vec<_>>>()?;
Some(quote! { #(#stmts)* })
} else {
None
};
// Generate try/except/finally pattern
if handlers.is_empty() {
// Try/finally without except
if let Some(finally_code) = finally_stmts {
Ok(quote! {
{
#(#try_stmts)*
#finally_code
}
})
} else {
// Just try block
Ok(quote! { #(#try_stmts)* })
}
} else if handlers.len() == 1 {
let handler_code = &handler_tokens[0];
if let Some(finally_code) = finally_stmts {
Ok(quote! {
{
let _result = (|| -> Result<(), Box<dyn std::error::Error>> {
#(#try_stmts)*
Ok(())
})();
if let Err(_e) = _result {
#handler_code
}
#finally_code
}
})
} else {
Ok(quote! {
{
let _result = (|| -> Result<(), Box<dyn std::error::Error>> {
#(#try_stmts)*
Ok(())
})();
if let Err(_e) = _result {
#handler_code
}
}
})
}
} else {
// Multiple handlers
if let Some(finally_code) = finally_stmts {
Ok(quote! {
{
let _result = (|| -> Result<(), Box<dyn std::error::Error>> {
#(#try_stmts)*
Ok(())
})();
if let Err(_e) = _result {
#(#handler_tokens)*
}
#finally_code
}
})
} else {
Ok(quote! {
{
let _result = (|| -> Result<(), Box<dyn std::error::Error>> {
#(#try_stmts)*
Ok(())
})();
if let Err(_e) = _result {
#(#handler_tokens)*
}
}
})
}
}
}
impl RustCodeGen for HirStmt {
fn to_rust_tokens(&self, ctx: &mut CodeGenContext) -> Result<proc_macro2::TokenStream> {
match self {
HirStmt::Assign {
target,
value,
type_annotation,
} => codegen_assign_stmt(target, value, type_annotation, ctx),
HirStmt::Return(expr) => codegen_return_stmt(expr, ctx),
HirStmt::If {
condition,
then_body,
else_body,
} => codegen_if_stmt(condition, then_body, else_body, ctx),
HirStmt::While { condition, body } => codegen_while_stmt(condition, body, ctx),
HirStmt::For { target, iter, body } => codegen_for_stmt(target, iter, body, ctx),
HirStmt::Expr(expr) => codegen_expr_stmt(expr, ctx),
HirStmt::Raise {
exception,
cause: _,
} => codegen_raise_stmt(exception, ctx),
HirStmt::Break { label } => codegen_break_stmt(label),
HirStmt::Continue { label } => codegen_continue_stmt(label),
HirStmt::With {
context,
target,
body,
} => codegen_with_stmt(context, target, body, ctx),
HirStmt::Try {
body,
handlers,
orelse: _,
finalbody,
} => codegen_try_stmt(body, handlers, finalbody, ctx),
HirStmt::Pass => codegen_pass_stmt(),
}
}
}
/// Expression converter to reduce complexity
struct ExpressionConverter<'a, 'b> {
ctx: &'a mut CodeGenContext<'b>,
}
impl<'a, 'b> ExpressionConverter<'a, 'b> {
fn new(ctx: &'a mut CodeGenContext<'b>) -> Self {
Self { ctx }
}
fn convert_variable(&self, name: &str) -> Result<syn::Expr> {
// Inside generators, check if variable is a state variable
if self.ctx.in_generator && self.ctx.generator_state_vars.contains(name) {
// Generate self.field for state variables
let ident = syn::Ident::new(name, proc_macro2::Span::call_site());
Ok(parse_quote! { self.#ident })
} else {
// Regular variable
let ident = syn::Ident::new(name, proc_macro2::Span::call_site());
Ok(parse_quote! { #ident })
}
}
fn convert_binary(&mut self, op: BinOp, left: &HirExpr, right: &HirExpr) -> Result<syn::Expr> {
let left_expr = left.to_rust_expr(self.ctx)?;
let right_expr = right.to_rust_expr(self.ctx)?;
match op {
BinOp::In => {
// Convert "x in dict" to "dict.contains_key(x)" or "dict.contains_key(&x)"
// String literals are already &str, so don't add extra &
if matches!(left, HirExpr::Literal(Literal::String(_))) {
Ok(parse_quote! { #right_expr.contains_key(#left_expr) })
} else {
Ok(parse_quote! { #right_expr.contains_key(&#left_expr) })
}
}
BinOp::NotIn => {
// Convert "x not in dict" to "!dict.contains_key(x)" or "!dict.contains_key(&x)"
// String literals are already &str, so don't add extra &
if matches!(left, HirExpr::Literal(Literal::String(_))) {
Ok(parse_quote! { !#right_expr.contains_key(#left_expr) })
} else {
Ok(parse_quote! { !#right_expr.contains_key(&#left_expr) })
}
}
BinOp::Add => {
// Special handling for string concatenation
// Only use format! if we're certain at least one operand is a string
let is_definitely_string = matches!(left, HirExpr::Literal(Literal::String(_)))
|| matches!(right, HirExpr::Literal(Literal::String(_)));
if is_definitely_string {
// This is string concatenation - use format! to handle references properly
Ok(parse_quote! { format!("{}{}", #left_expr, #right_expr) })
} else {
// Regular arithmetic addition or unknown types
let rust_op = convert_binop(op)?;
Ok(parse_quote! { #left_expr #rust_op #right_expr })
}
}
BinOp::FloorDiv => {
// Python floor division semantics differ from Rust integer division
// Python: rounds towards negative infinity (floor)
// Rust: truncates towards zero
// For now, we generate code that works for integers with proper floor semantics
Ok(parse_quote! {
{
let a = #left_expr;
let b = #right_expr;
let q = a / b;
let r = a % b;
// Avoid != in boolean expression due to formatting issues
let r_negative = r < 0;
let b_negative = b < 0;
let r_nonzero = r != 0;
let signs_differ = r_negative != b_negative;
let needs_adjustment = r_nonzero && signs_differ;
if needs_adjustment { q - 1 } else { q }
}
})
}
// Set operators - check if both operands are sets
BinOp::BitAnd | BinOp::BitOr | BinOp::BitXor
if self.is_set_expr(left) && self.is_set_expr(right) =>
{
self.convert_set_operation(op, left_expr, right_expr)
}
BinOp::Sub if self.is_set_expr(left) && self.is_set_expr(right) => {
// Set difference operation
self.convert_set_operation(op, left_expr, right_expr)
}
BinOp::Sub => {
// Check if we're subtracting from a .len() call to prevent underflow
if self.is_len_call(left) {
// Use saturating_sub to prevent underflow when subtracting from array length
Ok(parse_quote! { #left_expr.saturating_sub(#right_expr) })
} else {
let rust_op = convert_binop(op)?;
Ok(parse_quote! { #left_expr #rust_op #right_expr })
}
}
BinOp::Mul => {
// Special case: [value] * n or n * [value] creates an array
match (left, right) {
// Pattern: [x] * n
(HirExpr::List(elts), HirExpr::Literal(Literal::Int(size)))
if elts.len() == 1 && *size > 0 && *size <= 32 =>
{
let elem = elts[0].to_rust_expr(self.ctx)?;
let size_lit =
syn::LitInt::new(&size.to_string(), proc_macro2::Span::call_site());
Ok(parse_quote! { [#elem; #size_lit] })
}
// Pattern: n * [x]
(HirExpr::Literal(Literal::Int(size)), HirExpr::List(elts))
if elts.len() == 1 && *size > 0 && *size <= 32 =>
{
let elem = elts[0].to_rust_expr(self.ctx)?;
let size_lit =
syn::LitInt::new(&size.to_string(), proc_macro2::Span::call_site());
Ok(parse_quote! { [#elem; #size_lit] })
}
// Default multiplication
_ => {
let rust_op = convert_binop(op)?;
Ok(parse_quote! { #left_expr #rust_op #right_expr })
}
}
}
BinOp::Div => {
// v3.16.0 Phase 2: Python's `/` always returns float
// Rust's `/` does integer division when both operands are integers
// Check if we need to cast to float based on return type context
let needs_float_division = self.ctx.current_return_type.as_ref()
.map(return_type_expects_float)
.unwrap_or(false);
if needs_float_division {
// Cast both operands to f64 for Python float division semantics
Ok(parse_quote! { (#left_expr as f64) / (#right_expr as f64) })
} else {
// Regular division (int/int → int, float/float → float)
let rust_op = convert_binop(op)?;
Ok(parse_quote! { #left_expr #rust_op #right_expr })
}
}
BinOp::Pow => {
// Python power operator ** needs type-specific handling in Rust
// For integers: use .pow() with u32 exponent
// For floats: use .powf() with f64 exponent
// For negative integer exponents: convert to float
// Check if we have literals to determine types
match (left, right) {
// Integer literal base with integer literal exponent
(HirExpr::Literal(Literal::Int(_)), HirExpr::Literal(Literal::Int(exp))) => {
if *exp < 0 {
// Negative exponent: convert to float operation
Ok(parse_quote! {
(#left_expr as f64).powf(#right_expr as f64)
})
} else {
// Positive integer exponent: use .pow() with u32
// Add checked_pow for overflow safety
Ok(parse_quote! {
#left_expr.checked_pow(#right_expr as u32)
.expect("Power operation overflowed")
})
}
}
// Float literal base: always use .powf()
(HirExpr::Literal(Literal::Float(_)), _) => Ok(parse_quote! {
#left_expr.powf(#right_expr as f64)
}),
// Any base with float exponent: use .powf()
(_, HirExpr::Literal(Literal::Float(_))) => Ok(parse_quote! {
(#left_expr as f64).powf(#right_expr)
}),
// Variables or complex expressions: generate type-safe code
_ => {
// For non-literal expressions, we need runtime type checking
// This is a conservative approach that works for common cases
Ok(parse_quote! {
{
// Try integer power first if exponent can be u32
if #right_expr >= 0 && #right_expr <= u32::MAX as i64 {
#left_expr.checked_pow(#right_expr as u32)
.expect("Power operation overflowed")
} else {
// Fall back to float power for negative or large exponents
(#left_expr as f64).powf(#right_expr as f64) as i64
}
}
})
}
}
}
_ => {
let rust_op = convert_binop(op)?;
Ok(parse_quote! { #left_expr #rust_op #right_expr })
}
}
}
fn convert_unary(&mut self, op: &UnaryOp, operand: &HirExpr) -> Result<syn::Expr> {
let operand_expr = operand.to_rust_expr(self.ctx)?;
match op {
UnaryOp::Not => Ok(parse_quote! { !#operand_expr }),
UnaryOp::Neg => Ok(parse_quote! { -#operand_expr }),
UnaryOp::Pos => Ok(operand_expr), // No +x in Rust
UnaryOp::BitNot => Ok(parse_quote! { !#operand_expr }),
}
}
fn convert_call(&mut self, func: &str, args: &[HirExpr]) -> Result<syn::Expr> {
// Handle classmethod cls(args) → Self::new(args)
if func == "cls" && self.ctx.is_classmethod {
let arg_exprs: Vec<syn::Expr> = args
.iter()
.map(|arg| arg.to_rust_expr(self.ctx))
.collect::<Result<Vec<_>>>()?;
return Ok(parse_quote! { Self::new(#(#arg_exprs),*) });
}
// Handle map() with lambda → convert to Rust iterator pattern
if func == "map" && args.len() >= 2 {
if let Some(result) = self.try_convert_map_with_zip(args)? {
return Ok(result);
}
}
// Handle sum(generator_exp) → generator_exp.sum()
if func == "sum" && args.len() == 1
&& matches!(args[0], HirExpr::GeneratorExp { .. }) {
let gen_expr = args[0].to_rust_expr(self.ctx)?;
return Ok(parse_quote! { #gen_expr.sum() });
}
// Handle max(generator_exp) → generator_exp.max()
if func == "max" && args.len() == 1
&& matches!(args[0], HirExpr::GeneratorExp { .. }) {
let gen_expr = args[0].to_rust_expr(self.ctx)?;
return Ok(parse_quote! { #gen_expr.max() });
}
// Handle enumerate(items) → items.into_iter().enumerate()
if func == "enumerate" && args.len() == 1 {
let items_expr = args[0].to_rust_expr(self.ctx)?;
return Ok(parse_quote! { #items_expr.into_iter().enumerate() });
}
// Handle zip(a, b, ...) → a.iter().zip(b.iter()).zip(c.iter())...
if func == "zip" && args.len() >= 2 {
let arg_exprs: Vec<syn::Expr> = args
.iter()
.map(|arg| arg.to_rust_expr(self.ctx))
.collect::<Result<Vec<_>>>()?;
// Start with first.iter()
let first = &arg_exprs[0];
let mut chain: syn::Expr = parse_quote! { #first.iter() };
// Chain .zip() for each subsequent argument
for arg in &arg_exprs[1..] {
chain = parse_quote! { #chain.zip(#arg.iter()) };
}
return Ok(chain);
}
let arg_exprs: Vec<syn::Expr> = args
.iter()
.map(|arg| arg.to_rust_expr(self.ctx))
.collect::<Result<Vec<_>>>()?;
match func {
// Python built-in type conversions → Rust casting
"int" => self.convert_int_cast(&arg_exprs),
"float" => self.convert_float_cast(&arg_exprs),
"str" => self.convert_str_conversion(&arg_exprs),
"bool" => self.convert_bool_cast(&arg_exprs),
// Other built-in functions
"len" => self.convert_len_call(&arg_exprs),
"range" => self.convert_range_call(&arg_exprs),
"zeros" | "ones" | "full" => self.convert_array_init_call(func, args, &arg_exprs),
"set" => self.convert_set_constructor(&arg_exprs),
"frozenset" => self.convert_frozenset_constructor(&arg_exprs),
_ => self.convert_generic_call(func, &arg_exprs),
}
}
fn try_convert_map_with_zip(&mut self, args: &[HirExpr]) -> Result<Option<syn::Expr>> {
// Check if first argument is a lambda
if let HirExpr::Lambda { params, body } = &args[0] {
let num_iterables = args.len() - 1;
// Check if lambda has matching number of parameters
if params.len() != num_iterables {
bail!(
"Lambda has {} parameters but map() called with {} iterables",
params.len(),
num_iterables
);
}
// Convert the iterables
let mut iterable_exprs: Vec<syn::Expr> = Vec::new();
for iterable in &args[1..] {
iterable_exprs.push(iterable.to_rust_expr(self.ctx)?);
}
// Create lambda parameter pattern
let param_idents: Vec<syn::Ident> = params
.iter()
.map(|p| syn::Ident::new(p, proc_macro2::Span::call_site()))
.collect();
// Convert lambda body
let body_expr = body.to_rust_expr(self.ctx)?;
// Handle based on number of iterables
if num_iterables == 1 {
// Single iterable: iterable.iter().map(|x| ...).collect()
let iter_expr = &iterable_exprs[0];
let param = ¶m_idents[0];
Ok(Some(parse_quote! {
#iter_expr.iter().map(|#param| #body_expr).collect::<Vec<_>>()
}))
} else {
// Multiple iterables: use zip pattern
// Build the zip chain
let first_iter = &iterable_exprs[0];
let mut zip_expr: syn::Expr = parse_quote! { #first_iter.iter() };
for iter_expr in &iterable_exprs[1..] {
zip_expr = parse_quote! { #zip_expr.zip(#iter_expr.iter()) };
}
// Build the tuple pattern based on number of parameters
let tuple_pat: syn::Pat = if param_idents.len() == 2 {
let p0 = ¶m_idents[0];
let p1 = ¶m_idents[1];
parse_quote! { (#p0, #p1) }
} else if param_idents.len() == 3 {
// For 3 parameters, zip creates ((a, b), c)
let p0 = ¶m_idents[0];
let p1 = ¶m_idents[1];
let p2 = ¶m_idents[2];
parse_quote! { ((#p0, #p1), #p2) }
} else {
// For 4+ parameters, continue the nested pattern
bail!("map() with more than 3 iterables is not yet supported");
};
// Generate the final expression
Ok(Some(parse_quote! {
#zip_expr.map(|#tuple_pat| #body_expr).collect::<Vec<_>>()
}))
}
} else {
// Not a lambda, fall through to normal handling
Ok(None)
}
}
fn convert_len_call(&self, args: &[syn::Expr]) -> Result<syn::Expr> {
if args.len() != 1 {
bail!("len() requires exactly one argument");
}
let arg = &args[0];
// Python's len() returns int, which we map to i32/i64/isize based on type mapper.
// Rust's .len() returns usize, so we cast to match Python's int type.
// This ensures type consistency: len() - 1, len() comparisons, etc. all work with i32.
//
// Note: This matches the type mapper's integer width preference.
// For functions returning indices, they should explicitly use usize in their return type.
// Removed outer parens - they're unnecessary and cause clippy warnings
Ok(parse_quote! { #arg.len() as i32 })
}
fn convert_int_cast(&self, args: &[syn::Expr]) -> Result<syn::Expr> {
if args.is_empty() || args.len() > 2 {
bail!("int() requires 1-2 arguments");
}
let arg = &args[0];
// Python int() serves two purposes:
// 1. Convert floats to integers (truncation)
// 2. Ensure integer type for indexing
//
// In Rust, integer division already works, so we check if the argument
// is already an integer expression and skip the cast to let Rust infer
// the correct integer type (usize for indices, i32 for general use).
//
// For now, we omit the cast and let Rust's type inference determine
// the appropriate integer type based on usage context.
// This fixes type consistency issues where array indices need usize.
//
// TODO: Add heuristic to detect float expressions and cast those explicitly
// TODO: Handle base parameter for int(str, base) conversions
Ok(arg.clone())
}
fn convert_float_cast(&self, args: &[syn::Expr]) -> Result<syn::Expr> {
if args.len() != 1 {
bail!("float() requires exactly one argument");
}
let arg = &args[0];
Ok(parse_quote! { (#arg) as f64 })
}
fn convert_str_conversion(&self, args: &[syn::Expr]) -> Result<syn::Expr> {
if args.len() != 1 {
bail!("str() requires exactly one argument");
}
let arg = &args[0];
Ok(parse_quote! { #arg.to_string() })
}
fn convert_bool_cast(&self, args: &[syn::Expr]) -> Result<syn::Expr> {
if args.len() != 1 {
bail!("bool() requires exactly one argument");
}
let arg = &args[0];
// In Python, bool(x) checks truthiness
// In Rust, we cast to bool or use appropriate conversion
Ok(parse_quote! { (#arg) as bool })
}
fn convert_range_call(&self, args: &[syn::Expr]) -> Result<syn::Expr> {
match args.len() {
1 => {
let end = &args[0];
Ok(parse_quote! { 0..#end })
}
2 => {
let start = &args[0];
let end = &args[1];
Ok(parse_quote! { #start..#end })
}
3 => {
let start = &args[0];
let end = &args[1];
let step = &args[2];
// Check if step is negative by looking at the expression
let is_negative_step = if let syn::Expr::Unary(unary) = step {
matches!(unary.op, syn::UnOp::Neg(_))
} else {
false
};
if is_negative_step {
// For negative steps, we need to reverse the range
// Python: range(10, 0, -1) → Rust: (0..10).rev()
// But we also need to handle step sizes > 1
Ok(parse_quote! {
{
let step = (#step).abs() as usize;
if step == 0 {
panic!("range() arg 3 must not be zero");
}
if step == 1 {
(#end..#start).rev()
} else {
(#end..#start).rev().step_by(step)
}
}
})
} else {
// Positive step - check for zero
Ok(parse_quote! {
{
let step = #step as usize;
if step == 0 {
panic!("range() arg 3 must not be zero");
}
(#start..#end).step_by(step)
}
})
}
}
_ => bail!("Invalid number of arguments for range()"),
}
}
fn convert_array_init_call(
&mut self,
func: &str,
args: &[HirExpr],
_arg_exprs: &[syn::Expr],
) -> Result<syn::Expr> {
// Handle zeros(n), ones(n), full(n, value) patterns
if args.is_empty() {
bail!("{} requires at least one argument", func);
}
// Extract size from first argument if it's a literal
if let HirExpr::Literal(Literal::Int(size)) = &args[0] {
if *size > 0 && *size <= 32 {
let size_lit = syn::LitInt::new(&size.to_string(), proc_macro2::Span::call_site());
match func {
"zeros" => Ok(parse_quote! { [0; #size_lit] }),
"ones" => Ok(parse_quote! { [1; #size_lit] }),
"full" => {
if args.len() >= 2 {
let value = args[1].to_rust_expr(self.ctx)?;
Ok(parse_quote! { [#value; #size_lit] })
} else {
bail!("full() requires a value argument");
}
}
_ => unreachable!(),
}
} else {
// For large arrays or dynamic sizes, fall back to vec!
match func {
"zeros" => {
let size_expr = args[0].to_rust_expr(self.ctx)?;
Ok(parse_quote! { vec![0; #size_expr as usize] })
}
"ones" => {
let size_expr = args[0].to_rust_expr(self.ctx)?;
Ok(parse_quote! { vec![1; #size_expr as usize] })
}
"full" => {
if args.len() >= 2 {
let size_expr = args[0].to_rust_expr(self.ctx)?;
let value = args[1].to_rust_expr(self.ctx)?;
Ok(parse_quote! { vec![#value; #size_expr as usize] })
} else {
bail!("full() requires a value argument");
}
}
_ => unreachable!(),
}
}
} else {
// Dynamic size - use vec!
let size_expr = args[0].to_rust_expr(self.ctx)?;
match func {
"zeros" => Ok(parse_quote! { vec![0; #size_expr as usize] }),
"ones" => Ok(parse_quote! { vec![1; #size_expr as usize] }),
"full" => {
if args.len() >= 2 {
let value = args[1].to_rust_expr(self.ctx)?;
Ok(parse_quote! { vec![#value; #size_expr as usize] })
} else {
bail!("full() requires a value argument");
}
}
_ => unreachable!(),
}
}
}
fn convert_set_constructor(&mut self, args: &[syn::Expr]) -> Result<syn::Expr> {
self.ctx.needs_hashset = true;
if args.is_empty() {
// Empty set: set()
Ok(parse_quote! { HashSet::new() })
} else if args.len() == 1 {
// Set from iterable: set([1, 2, 3])
let arg = &args[0];
Ok(parse_quote! {
#arg.into_iter().collect::<HashSet<_>>()
})
} else {
bail!("set() takes at most 1 argument ({} given)", args.len())
}
}
fn convert_frozenset_constructor(&mut self, args: &[syn::Expr]) -> Result<syn::Expr> {
self.ctx.needs_hashset = true;
if args.is_empty() {
// Empty frozenset: frozenset()
// In Rust, we can use Arc<HashSet> to make it immutable
Ok(parse_quote! { std::sync::Arc::new(HashSet::new()) })
} else if args.len() == 1 {
// Frozenset from iterable: frozenset([1, 2, 3])
let arg = &args[0];
Ok(parse_quote! {
std::sync::Arc::new(#arg.into_iter().collect::<HashSet<_>>())
})
} else {
bail!(
"frozenset() takes at most 1 argument ({} given)",
args.len()
)
}
}
fn convert_generic_call(&self, func: &str, args: &[syn::Expr]) -> Result<syn::Expr> {
// Check if this is an imported function
if let Some(rust_path) = self.ctx.imported_items.get(func) {
// Parse the rust path and generate the call
let path_parts: Vec<&str> = rust_path.split("::").collect();
let mut path = quote! {};
for (i, part) in path_parts.iter().enumerate() {
let part_ident = syn::Ident::new(part, proc_macro2::Span::call_site());
if i == 0 {
path = quote! { #part_ident };
} else {
path = quote! { #path::#part_ident };
}
}
if args.is_empty() {
return Ok(parse_quote! { #path() });
} else {
return Ok(parse_quote! { #path(#(#args),*) });
}
}
// Check if this might be a constructor call (capitalized name)
if func
.chars()
.next()
.map(|c| c.is_uppercase())
.unwrap_or(false)
{
// Treat as constructor call - ClassName::new(args)
let class_ident = syn::Ident::new(func, proc_macro2::Span::call_site());
if args.is_empty() {
// Note: Constructor default parameter handling uses simple heuristics.
// Ideally this would be context-aware and know the actual default values
// for each class constructor, but currently uses hardcoded patterns.
// This is a known limitation - constructors may require explicit arguments.
match func {
"Counter" => Ok(parse_quote! { #class_ident::new(0) }),
_ => Ok(parse_quote! { #class_ident::new() }),
}
} else {
Ok(parse_quote! { #class_ident::new(#(#args),*) })
}
} else {
// Regular function call
let func_ident = syn::Ident::new(func, proc_macro2::Span::call_site());
Ok(parse_quote! { #func_ident(#(#args),*) })
}
}
// ========================================================================
// DEPYLER-0142 Phase 1: Preamble Helpers
// ========================================================================
/// Try to convert classmethod call (cls.method())
#[inline]
fn try_convert_classmethod(
&mut self,
object: &HirExpr,
method: &str,
args: &[HirExpr],
) -> Result<Option<syn::Expr>> {
if let HirExpr::Var(var_name) = object {
if var_name == "cls" && self.ctx.is_classmethod {
let method_ident = syn::Ident::new(method, proc_macro2::Span::call_site());
let arg_exprs: Vec<syn::Expr> = args
.iter()
.map(|arg| arg.to_rust_expr(self.ctx))
.collect::<Result<Vec<_>>>()?;
return Ok(Some(parse_quote! { Self::#method_ident(#(#arg_exprs),*) }));
}
}
Ok(None)
}
/// Try to convert module method call (e.g., os.getcwd())
#[inline]
fn try_convert_module_method(
&mut self,
object: &HirExpr,
method: &str,
args: &[HirExpr],
) -> Result<Option<syn::Expr>> {
if let HirExpr::Var(module_name) = object {
let rust_name_opt = self
.ctx
.imported_modules
.get(module_name)
.and_then(|mapping| mapping.item_map.get(method).cloned());
if let Some(rust_name) = rust_name_opt {
// Convert args
let arg_exprs: Vec<syn::Expr> = args
.iter()
.map(|arg| arg.to_rust_expr(self.ctx))
.collect::<Result<Vec<_>>>()?;
// Build the Rust function path
let path_parts: Vec<&str> = rust_name.split("::").collect();
let mut path = quote! { std };
for part in path_parts {
let part_ident = syn::Ident::new(part, proc_macro2::Span::call_site());
path = quote! { #path::#part_ident };
}
// Special handling for certain functions
let result = match rust_name.as_str() {
"env::current_dir" => {
// current_dir returns Result<PathBuf>, we need to convert to String
parse_quote! {
#path().unwrap().to_string_lossy().to_string()
}
}
"Regex::new" => {
// re.compile(pattern) -> Regex::new(pattern)
if arg_exprs.is_empty() {
bail!("re.compile() requires a pattern argument");
}
let pattern = &arg_exprs[0];
parse_quote! {
regex::Regex::new(#pattern).unwrap()
}
}
_ => {
if arg_exprs.is_empty() {
parse_quote! { #path() }
} else {
parse_quote! { #path(#(#arg_exprs),*) }
}
}
};
return Ok(Some(result));
}
}
Ok(None)
}
// ========================================================================
// DEPYLER-0142 Phase 2: Category Handlers
// ========================================================================
/// Handle list methods (append, extend, pop, insert, remove)
#[inline]
fn convert_list_method(
&mut self,
object_expr: &syn::Expr,
object: &HirExpr,
method: &str,
arg_exprs: &[syn::Expr],
) -> Result<syn::Expr> {
match method {
"append" => {
if arg_exprs.len() != 1 {
bail!("append() requires exactly one argument");
}
let arg = &arg_exprs[0];
Ok(parse_quote! { #object_expr.push(#arg) })
}
"extend" => {
if arg_exprs.len() != 1 {
bail!("extend() requires exactly one argument");
}
let arg = &arg_exprs[0];
Ok(parse_quote! { #object_expr.extend(#arg) })
}
"pop" => {
if self.is_set_expr(object) {
if !arg_exprs.is_empty() {
bail!("pop() takes no arguments for sets");
}
Ok(parse_quote! {
#object_expr.iter().next().cloned().map(|x| {
#object_expr.remove(&x);
x
}).expect("pop from empty set")
})
} else if arg_exprs.is_empty() {
Ok(parse_quote! { #object_expr.pop().unwrap_or_default() })
} else {
bail!("pop() with index not supported in V1");
}
}
"insert" => {
if arg_exprs.len() != 2 {
bail!("insert() requires exactly two arguments");
}
let index = &arg_exprs[0];
let value = &arg_exprs[1];
Ok(parse_quote! { #object_expr.insert(#index as usize, #value) })
}
"remove" => {
if arg_exprs.len() != 1 {
bail!("remove() requires exactly one argument");
}
let value = &arg_exprs[0];
if self.is_set_expr(object) {
Ok(parse_quote! {
if !#object_expr.remove(&#value) {
panic!("KeyError: element not in set");
}
})
} else {
Ok(parse_quote! {
if let Some(pos) = #object_expr.iter().position(|x| x == &#value) {
#object_expr.remove(pos)
} else {
panic!("ValueError: list.remove(x): x not in list")
}
})
}
}
_ => bail!("Unknown list method: {}", method),
}
}
/// Handle dict methods (get, keys, values, items, update)
#[inline]
fn convert_dict_method(
&mut self,
object_expr: &syn::Expr,
method: &str,
arg_exprs: &[syn::Expr],
) -> Result<syn::Expr> {
match method {
"get" => {
if arg_exprs.len() == 1 {
let key = &arg_exprs[0];
Ok(parse_quote! { #object_expr.get(&#key).cloned() })
} else if arg_exprs.len() == 2 {
let key = &arg_exprs[0];
let default = &arg_exprs[1];
Ok(parse_quote! { #object_expr.get(&#key).cloned().unwrap_or(#default) })
} else {
bail!("get() requires 1 or 2 arguments");
}
}
"keys" => {
if !arg_exprs.is_empty() {
bail!("keys() takes no arguments");
}
Ok(parse_quote! { #object_expr.keys().cloned().collect::<Vec<_>>() })
}
"values" => {
if !arg_exprs.is_empty() {
bail!("values() takes no arguments");
}
Ok(parse_quote! { #object_expr.values().cloned().collect::<Vec<_>>() })
}
"items" => {
if !arg_exprs.is_empty() {
bail!("items() takes no arguments");
}
Ok(parse_quote! { #object_expr.iter().map(|(k, v)| (k.clone(), v.clone())).collect::<Vec<_>>() })
}
"update" => {
if arg_exprs.len() != 1 {
bail!("update() requires exactly one argument");
}
let arg = &arg_exprs[0];
Ok(parse_quote! {
for (k, v) in #arg {
#object_expr.insert(k, v);
}
})
}
_ => bail!("Unknown dict method: {}", method),
}
}
/// Handle string methods (upper, lower, strip, startswith, endswith, split, join)
#[inline]
fn convert_string_method(
&mut self,
object_expr: &syn::Expr,
method: &str,
arg_exprs: &[syn::Expr],
) -> Result<syn::Expr> {
match method {
"upper" => {
if !arg_exprs.is_empty() {
bail!("upper() takes no arguments");
}
Ok(parse_quote! { #object_expr.to_uppercase() })
}
"lower" => {
if !arg_exprs.is_empty() {
bail!("lower() takes no arguments");
}
Ok(parse_quote! { #object_expr.to_lowercase() })
}
"strip" => {
if !arg_exprs.is_empty() {
bail!("strip() with arguments not supported in V1");
}
Ok(parse_quote! { #object_expr.trim().to_string() })
}
"startswith" => {
if arg_exprs.len() != 1 {
bail!("startswith() requires exactly one argument");
}
let prefix = &arg_exprs[0];
Ok(parse_quote! { #object_expr.starts_with(#prefix) })
}
"endswith" => {
if arg_exprs.len() != 1 {
bail!("endswith() requires exactly one argument");
}
let suffix = &arg_exprs[0];
Ok(parse_quote! { #object_expr.ends_with(#suffix) })
}
"split" => {
if arg_exprs.is_empty() {
Ok(parse_quote! { #object_expr.split_whitespace().map(|s| s.to_string()).collect::<Vec<String>>() })
} else if arg_exprs.len() == 1 {
let sep = &arg_exprs[0];
Ok(parse_quote! { #object_expr.split(#sep).map(|s| s.to_string()).collect::<Vec<String>>() })
} else {
bail!("split() with maxsplit not supported in V1");
}
}
"join" => {
if arg_exprs.len() != 1 {
bail!("join() requires exactly one argument");
}
let iterable = &arg_exprs[0];
Ok(parse_quote! { #iterable.join(#object_expr) })
}
_ => bail!("Unknown string method: {}", method),
}
}
/// Handle set methods (add, discard, clear)
#[inline]
fn convert_set_method(
&mut self,
object_expr: &syn::Expr,
method: &str,
arg_exprs: &[syn::Expr],
) -> Result<syn::Expr> {
match method {
"add" => {
if arg_exprs.len() != 1 {
bail!("add() requires exactly one argument");
}
let arg = &arg_exprs[0];
Ok(parse_quote! { #object_expr.insert(#arg) })
}
"discard" => {
if arg_exprs.len() != 1 {
bail!("discard() requires exactly one argument");
}
let arg = &arg_exprs[0];
Ok(parse_quote! { #object_expr.remove(&#arg) })
}
"clear" => {
if !arg_exprs.is_empty() {
bail!("clear() takes no arguments");
}
Ok(parse_quote! { #object_expr.clear() })
}
_ => bail!("Unknown set method: {}", method),
}
}
/// Handle regex methods (findall)
#[inline]
fn convert_regex_method(
&mut self,
object_expr: &syn::Expr,
method: &str,
arg_exprs: &[syn::Expr],
) -> Result<syn::Expr> {
match method {
"findall" => {
if arg_exprs.is_empty() {
bail!("findall() requires at least one argument");
}
let text = &arg_exprs[0];
Ok(parse_quote! {
#object_expr.find_iter(#text)
.map(|m| m.as_str().to_string())
.collect::<Vec<String>>()
})
}
_ => bail!("Unknown regex method: {}", method),
}
}
/// Convert instance method calls (main dispatcher)
#[inline]
fn convert_instance_method(
&mut self,
object: &HirExpr,
object_expr: &syn::Expr,
method: &str,
arg_exprs: &[syn::Expr],
) -> Result<syn::Expr> {
// Dispatch by method category
match method {
// List methods
"append" | "extend" | "pop" | "insert" | "remove" => {
self.convert_list_method(object_expr, object, method, arg_exprs)
}
// Dict methods
"get" | "keys" | "values" | "items" | "update" => {
self.convert_dict_method(object_expr, method, arg_exprs)
}
// String methods
"upper" | "lower" | "strip" | "startswith" | "endswith" | "split" | "join" => {
self.convert_string_method(object_expr, method, arg_exprs)
}
// Set methods
"add" | "discard" | "clear" => {
self.convert_set_method(object_expr, method, arg_exprs)
}
// Regex methods
"findall" => {
self.convert_regex_method(object_expr, method, arg_exprs)
}
// Default: generic method call
_ => {
let method_ident = syn::Ident::new(method, proc_macro2::Span::call_site());
Ok(parse_quote! { #object_expr.#method_ident(#(#arg_exprs),*) })
}
}
}
fn convert_method_call(
&mut self,
object: &HirExpr,
method: &str,
args: &[HirExpr],
) -> Result<syn::Expr> {
// Try classmethod handling first
if let Some(result) = self.try_convert_classmethod(object, method, args)? {
return Ok(result);
}
// Try module method handling
if let Some(result) = self.try_convert_module_method(object, method, args)? {
return Ok(result);
}
let object_expr = object.to_rust_expr(self.ctx)?;
let arg_exprs: Vec<syn::Expr> = args
.iter()
.map(|arg| arg.to_rust_expr(self.ctx))
.collect::<Result<Vec<_>>>()?;
// Dispatch to instance method handler
self.convert_instance_method(object, &object_expr, method, &arg_exprs)
}
fn convert_index(&mut self, base: &HirExpr, index: &HirExpr) -> Result<syn::Expr> {
let base_expr = base.to_rust_expr(self.ctx)?;
// Discriminate between HashMap and Vec access based on base type or index type
let is_string_key = self.is_string_index(base, index)?;
if is_string_key {
// HashMap/Dict access with string keys
match index {
HirExpr::Literal(Literal::String(s)) => {
// String literal - use it directly without .to_string()
Ok(parse_quote! {
#base_expr.get(#s).cloned().unwrap_or_default()
})
}
_ => {
// String variable - needs proper referencing
let index_expr = index.to_rust_expr(self.ctx)?;
Ok(parse_quote! {
#base_expr.get(#index_expr).cloned().unwrap_or_default()
})
}
}
} else {
// Vec/List access with numeric index
let index_expr = index.to_rust_expr(self.ctx)?;
Ok(parse_quote! {
#base_expr.get(#index_expr as usize).copied().unwrap_or_default()
})
}
}
/// Check if the index expression is a string key (for HashMap access)
/// Returns true if: index is string literal, OR base is Dict/HashMap type
fn is_string_index(&self, base: &HirExpr, index: &HirExpr) -> Result<bool> {
// Check 1: Is index a string literal?
if matches!(index, HirExpr::Literal(Literal::String(_))) {
return Ok(true);
}
// Check 2: Is base expression a Dict/HashMap type?
// We need to look at the base's inferred type
if let HirExpr::Var(sym) = base {
// Try to find the variable's type in the current function context
// For parameters, we can check the function signature
// For local variables, this is harder without full type inference
//
// Heuristic: If the symbol name contains "dict" or "data" or "map"
// and index doesn't look numeric, assume HashMap
let name = sym.as_str();
if (name.contains("dict") || name.contains("data") || name.contains("map"))
&& !self.is_numeric_index(index)
{
return Ok(true);
}
}
// Check 3: Does the index expression look like a string variable?
if self.is_string_variable(index) {
return Ok(true);
}
// Default: assume numeric index (Vec/List access)
Ok(false)
}
/// Check if expression is likely a string variable (heuristic)
fn is_string_variable(&self, expr: &HirExpr) -> bool {
match expr {
HirExpr::Var(sym) => {
let name = sym.as_str();
// Heuristic: variable names like "key", "name", "id", "word", etc.
name == "key"
|| name == "name"
|| name == "id"
|| name == "word"
|| name == "text"
|| name.ends_with("_key")
|| name.ends_with("_name")
}
_ => false,
}
}
/// Check if expression is likely numeric (heuristic)
fn is_numeric_index(&self, expr: &HirExpr) -> bool {
match expr {
HirExpr::Literal(Literal::Int(_)) => true,
HirExpr::Var(sym) => {
let name = sym.as_str();
// Common numeric index names
name == "i"
|| name == "j"
|| name == "k"
|| name == "idx"
|| name == "index"
|| name.starts_with("idx_")
|| name.ends_with("_idx")
|| name.ends_with("_index")
}
HirExpr::Binary { .. } => true, // Arithmetic expressions are numeric
HirExpr::Call { .. } => false, // Could be anything
_ => false,
}
}
fn convert_slice(
&mut self,
base: &HirExpr,
start: &Option<Box<HirExpr>>,
stop: &Option<Box<HirExpr>>,
step: &Option<Box<HirExpr>>,
) -> Result<syn::Expr> {
let base_expr = base.to_rust_expr(self.ctx)?;
// Convert slice parameters
let start_expr = if let Some(s) = start {
Some(s.to_rust_expr(self.ctx)?)
} else {
None
};
let stop_expr = if let Some(s) = stop {
Some(s.to_rust_expr(self.ctx)?)
} else {
None
};
let step_expr = if let Some(s) = step {
Some(s.to_rust_expr(self.ctx)?)
} else {
None
};
// Generate slice code based on the parameters
match (start_expr, stop_expr, step_expr) {
// Full slice with step: base[::step]
(None, None, Some(step)) => {
Ok(parse_quote! {
{
let step = #step;
if step == 1 {
#base_expr.clone()
} else if step > 0 {
#base_expr.iter().step_by(step as usize).cloned().collect::<Vec<_>>()
} else if step == -1 {
#base_expr.iter().rev().cloned().collect::<Vec<_>>()
} else {
// Negative step with abs value
let abs_step = (-step) as usize;
#base_expr.iter().rev().step_by(abs_step).cloned().collect::<Vec<_>>()
}
}
})
}
// Start and stop: base[start:stop]
(Some(start), Some(stop), None) => Ok(parse_quote! {
{
let start = (#start).max(0) as usize;
let stop = (#stop).max(0) as usize;
if start < #base_expr.len() {
#base_expr[start..stop.min(#base_expr.len())].to_vec()
} else {
Vec::new()
}
}
}),
// Start only: base[start:]
(Some(start), None, None) => Ok(parse_quote! {
{
let start = (#start).max(0) as usize;
if start < #base_expr.len() {
#base_expr[start..].to_vec()
} else {
Vec::new()
}
}
}),
// Stop only: base[:stop]
(None, Some(stop), None) => Ok(parse_quote! {
{
let stop = (#stop).max(0) as usize;
#base_expr[..stop.min(#base_expr.len())].to_vec()
}
}),
// Full slice: base[:]
(None, None, None) => Ok(parse_quote! { #base_expr.clone() }),
// Start, stop, and step: base[start:stop:step]
(Some(start), Some(stop), Some(step)) => {
Ok(parse_quote! {
{
let start = (#start).max(0) as usize;
let stop = (#stop).max(0) as usize;
let step = #step;
if step == 1 {
if start < #base_expr.len() {
#base_expr[start..stop.min(#base_expr.len())].to_vec()
} else {
Vec::new()
}
} else if step > 0 {
#base_expr[start..stop.min(#base_expr.len())]
.iter()
.step_by(step as usize)
.cloned()
.collect::<Vec<_>>()
} else {
// Negative step - slice in reverse
let abs_step = (-step) as usize;
if start < #base_expr.len() {
#base_expr[start..stop.min(#base_expr.len())]
.iter()
.rev()
.step_by(abs_step)
.cloned()
.collect::<Vec<_>>()
} else {
Vec::new()
}
}
}
})
}
// Start and step: base[start::step]
(Some(start), None, Some(step)) => Ok(parse_quote! {
{
let start = (#start).max(0) as usize;
let step = #step;
if start < #base_expr.len() {
if step == 1 {
#base_expr[start..].to_vec()
} else if step > 0 {
#base_expr[start..]
.iter()
.step_by(step as usize)
.cloned()
.collect::<Vec<_>>()
} else if step == -1 {
#base_expr[start..]
.iter()
.rev()
.cloned()
.collect::<Vec<_>>()
} else {
let abs_step = (-step) as usize;
#base_expr[start..]
.iter()
.rev()
.step_by(abs_step)
.cloned()
.collect::<Vec<_>>()
}
} else {
Vec::new()
}
}
}),
// Stop and step: base[:stop:step]
(None, Some(stop), Some(step)) => Ok(parse_quote! {
{
let stop = (#stop).max(0) as usize;
let step = #step;
if step == 1 {
#base_expr[..stop.min(#base_expr.len())].to_vec()
} else if step > 0 {
#base_expr[..stop.min(#base_expr.len())]
.iter()
.step_by(step as usize)
.cloned()
.collect::<Vec<_>>()
} else if step == -1 {
#base_expr[..stop.min(#base_expr.len())]
.iter()
.rev()
.cloned()
.collect::<Vec<_>>()
} else {
let abs_step = (-step) as usize;
#base_expr[..stop.min(#base_expr.len())]
.iter()
.rev()
.step_by(abs_step)
.cloned()
.collect::<Vec<_>>()
}
}
}),
}
}
fn convert_list(&mut self, elts: &[HirExpr]) -> Result<syn::Expr> {
let elt_exprs: Vec<syn::Expr> = elts
.iter()
.map(|e| e.to_rust_expr(self.ctx))
.collect::<Result<Vec<_>>>()?;
// Always use vec! for now to ensure mutability works
// In the future, we should analyze if the list is mutated before deciding
Ok(parse_quote! { vec![#(#elt_exprs),*] })
}
fn convert_dict(&mut self, items: &[(HirExpr, HirExpr)]) -> Result<syn::Expr> {
self.ctx.needs_hashmap = true;
let mut insert_stmts = Vec::new();
for (key, value) in items {
let key_expr = key.to_rust_expr(self.ctx)?;
let val_expr = value.to_rust_expr(self.ctx)?;
insert_stmts.push(quote! { map.insert(#key_expr, #val_expr); });
}
Ok(parse_quote! {
{
let mut map = HashMap::new();
#(#insert_stmts)*
map
}
})
}
fn convert_tuple(&mut self, elts: &[HirExpr]) -> Result<syn::Expr> {
let elt_exprs: Vec<syn::Expr> = elts
.iter()
.map(|e| e.to_rust_expr(self.ctx))
.collect::<Result<Vec<_>>>()?;
Ok(parse_quote! { (#(#elt_exprs),*) })
}
fn convert_set(&mut self, elts: &[HirExpr]) -> Result<syn::Expr> {
self.ctx.needs_hashset = true;
let mut insert_stmts = Vec::new();
for elem in elts {
let elem_expr = elem.to_rust_expr(self.ctx)?;
insert_stmts.push(quote! { set.insert(#elem_expr); });
}
Ok(parse_quote! {
{
let mut set = HashSet::new();
#(#insert_stmts)*
set
}
})
}
fn convert_frozenset(&mut self, elts: &[HirExpr]) -> Result<syn::Expr> {
self.ctx.needs_hashset = true;
self.ctx.needs_arc = true;
let mut insert_stmts = Vec::new();
for elem in elts {
let elem_expr = elem.to_rust_expr(self.ctx)?;
insert_stmts.push(quote! { set.insert(#elem_expr); });
}
Ok(parse_quote! {
{
let mut set = HashSet::new();
#(#insert_stmts)*
std::sync::Arc::new(set)
}
})
}
fn convert_attribute(&mut self, value: &HirExpr, attr: &str) -> Result<syn::Expr> {
// Handle classmethod cls.ATTR → Self::ATTR
if let HirExpr::Var(var_name) = value {
if var_name == "cls" && self.ctx.is_classmethod {
let attr_ident = syn::Ident::new(attr, proc_macro2::Span::call_site());
return Ok(parse_quote! { Self::#attr_ident });
}
}
// Check if this is a module attribute access
if let HirExpr::Var(module_name) = value {
let rust_name_opt = self
.ctx
.imported_modules
.get(module_name)
.and_then(|mapping| mapping.item_map.get(attr).cloned());
if let Some(rust_name) = rust_name_opt {
// Map to the Rust equivalent
let path_parts: Vec<&str> = rust_name.split("::").collect();
if path_parts.len() > 1 {
// It's a path like "env::current_dir"
let mut path = quote! { std };
for part in path_parts {
let part_ident = syn::Ident::new(part, proc_macro2::Span::call_site());
path = quote! { #path::#part_ident };
}
return Ok(parse_quote! { #path });
} else {
// Simple identifier
let ident = syn::Ident::new(&rust_name, proc_macro2::Span::call_site());
return Ok(parse_quote! { #ident });
}
}
}
// Default behavior for non-module attributes
let value_expr = value.to_rust_expr(self.ctx)?;
let attr_ident = syn::Ident::new(attr, proc_macro2::Span::call_site());
Ok(parse_quote! { #value_expr.#attr_ident })
}
fn convert_borrow(&mut self, expr: &HirExpr, mutable: bool) -> Result<syn::Expr> {
let expr_tokens = expr.to_rust_expr(self.ctx)?;
if mutable {
Ok(parse_quote! { &mut #expr_tokens })
} else {
Ok(parse_quote! { &#expr_tokens })
}
}
fn convert_list_comp(
&mut self,
element: &HirExpr,
target: &str,
iter: &HirExpr,
condition: &Option<Box<HirExpr>>,
) -> Result<syn::Expr> {
let target_ident = syn::Ident::new(target, proc_macro2::Span::call_site());
let iter_expr = iter.to_rust_expr(self.ctx)?;
let element_expr = element.to_rust_expr(self.ctx)?;
if let Some(cond) = condition {
// With condition: iter().filter().map().collect()
let cond_expr = cond.to_rust_expr(self.ctx)?;
Ok(parse_quote! {
#iter_expr
.into_iter()
.filter(|#target_ident| #cond_expr)
.map(|#target_ident| #element_expr)
.collect::<Vec<_>>()
})
} else {
// Without condition: iter().map().collect()
Ok(parse_quote! {
#iter_expr
.into_iter()
.map(|#target_ident| #element_expr)
.collect::<Vec<_>>()
})
}
}
fn is_set_expr(&self, expr: &HirExpr) -> bool {
match expr {
HirExpr::Set(_) | HirExpr::FrozenSet(_) => true,
HirExpr::Call { func, .. } if func == "set" || func == "frozenset" => true,
HirExpr::Var(_name) => {
// For rust_gen, we're more conservative since we don't have type info
// Only treat explicit set literals and calls as sets
false
}
_ => false,
}
}
fn convert_set_operation(
&self,
op: BinOp,
left: syn::Expr,
right: syn::Expr,
) -> Result<syn::Expr> {
match op {
BinOp::BitAnd => Ok(parse_quote! {
#left.intersection(&#right).cloned().collect()
}),
BinOp::BitOr => Ok(parse_quote! {
#left.union(&#right).cloned().collect()
}),
BinOp::Sub => Ok(parse_quote! {
#left.difference(&#right).cloned().collect()
}),
BinOp::BitXor => Ok(parse_quote! {
#left.symmetric_difference(&#right).cloned().collect()
}),
_ => bail!("Invalid set operator"),
}
}
fn convert_set_comp(
&mut self,
element: &HirExpr,
target: &str,
iter: &HirExpr,
condition: &Option<Box<HirExpr>>,
) -> Result<syn::Expr> {
self.ctx.needs_hashset = true;
let target_ident = syn::Ident::new(target, proc_macro2::Span::call_site());
let iter_expr = iter.to_rust_expr(self.ctx)?;
let element_expr = element.to_rust_expr(self.ctx)?;
if let Some(cond) = condition {
// With condition: iter().filter().map().collect()
let cond_expr = cond.to_rust_expr(self.ctx)?;
Ok(parse_quote! {
#iter_expr
.into_iter()
.filter(|#target_ident| #cond_expr)
.map(|#target_ident| #element_expr)
.collect::<HashSet<_>>()
})
} else {
// Without condition: iter().map().collect()
Ok(parse_quote! {
#iter_expr
.into_iter()
.map(|#target_ident| #element_expr)
.collect::<HashSet<_>>()
})
}
}
fn convert_lambda(&mut self, params: &[String], body: &HirExpr) -> Result<syn::Expr> {
// Convert parameters to pattern identifiers
let param_pats: Vec<syn::Pat> = params
.iter()
.map(|p| {
let ident = syn::Ident::new(p, proc_macro2::Span::call_site());
parse_quote! { #ident }
})
.collect();
// Convert body expression
let body_expr = body.to_rust_expr(self.ctx)?;
// Generate closure
if params.is_empty() {
// No parameters
Ok(parse_quote! { || #body_expr })
} else if params.len() == 1 {
// Single parameter
let param = ¶m_pats[0];
Ok(parse_quote! { |#param| #body_expr })
} else {
// Multiple parameters
Ok(parse_quote! { |#(#param_pats),*| #body_expr })
}
}
/// Check if an expression is a len() call
fn is_len_call(&self, expr: &HirExpr) -> bool {
matches!(expr, HirExpr::Call { func, args } if func == "len" && args.len() == 1)
}
fn convert_await(&mut self, value: &HirExpr) -> Result<syn::Expr> {
let value_expr = value.to_rust_expr(self.ctx)?;
Ok(parse_quote! { #value_expr.await })
}
fn convert_yield(&mut self, value: &Option<Box<HirExpr>>) -> Result<syn::Expr> {
if self.ctx.in_generator {
// Inside Iterator::next() - convert to return Some(value)
if let Some(v) = value {
let value_expr = v.to_rust_expr(self.ctx)?;
Ok(parse_quote! { return Some(#value_expr) })
} else {
Ok(parse_quote! { return None })
}
} else {
// Outside generator context - keep as yield (placeholder for future)
if let Some(v) = value {
let value_expr = v.to_rust_expr(self.ctx)?;
Ok(parse_quote! { yield #value_expr })
} else {
Ok(parse_quote! { yield })
}
}
}
fn convert_fstring(&mut self, parts: &[FStringPart]) -> Result<syn::Expr> {
// Handle empty f-strings
if parts.is_empty() {
return Ok(parse_quote! { "".to_string() });
}
// Check if it's just a plain string (no expressions)
let has_expressions = parts.iter().any(|p| matches!(p, FStringPart::Expr(_)));
if !has_expressions {
// Just literal parts - concatenate them
let mut result = String::new();
for part in parts {
if let FStringPart::Literal(s) = part {
result.push_str(s);
}
}
return Ok(parse_quote! { #result.to_string() });
}
// Build format string template and collect arguments
let mut template = String::new();
let mut args = Vec::new();
for part in parts {
match part {
FStringPart::Literal(s) => {
template.push_str(s);
}
FStringPart::Expr(expr) => {
template.push_str("{}");
let arg_expr = expr.to_rust_expr(self.ctx)?;
args.push(arg_expr);
}
}
}
// Generate format!() macro call
if args.is_empty() {
// No arguments (shouldn't happen but be safe)
Ok(parse_quote! { #template.to_string() })
} else {
// Build the format! call with template and arguments
Ok(parse_quote! { format!(#template, #(#args),*) })
}
}
fn convert_ifexpr(&mut self, test: &HirExpr, body: &HirExpr, orelse: &HirExpr) -> Result<syn::Expr> {
let test_expr = test.to_rust_expr(self.ctx)?;
let body_expr = body.to_rust_expr(self.ctx)?;
let orelse_expr = orelse.to_rust_expr(self.ctx)?;
Ok(parse_quote! {
if #test_expr { #body_expr } else { #orelse_expr }
})
}
fn convert_sort_by_key(
&mut self,
iterable: &HirExpr,
key_params: &[String],
key_body: &HirExpr,
reverse: bool,
) -> Result<syn::Expr> {
let iter_expr = iterable.to_rust_expr(self.ctx)?;
let body_expr = key_body.to_rust_expr(self.ctx)?;
// Create the closure parameter pattern
let param_pat: syn::Pat = if key_params.len() == 1 {
let param = syn::Ident::new(&key_params[0], proc_macro2::Span::call_site());
parse_quote! { #param }
} else {
bail!("sorted() key lambda must have exactly one parameter");
};
// Generate: { let mut result = iterable.clone(); result.sort_by_key(|param| body); [result.reverse();] result }
if reverse {
Ok(parse_quote! {
{
let mut __sorted_result = #iter_expr.clone();
__sorted_result.sort_by_key(|#param_pat| #body_expr);
__sorted_result.reverse();
__sorted_result
}
})
} else {
Ok(parse_quote! {
{
let mut __sorted_result = #iter_expr.clone();
__sorted_result.sort_by_key(|#param_pat| #body_expr);
__sorted_result
}
})
}
}
fn convert_generator_expression(
&mut self,
element: &HirExpr,
generators: &[crate::hir::HirComprehension],
) -> Result<syn::Expr> {
// Strategy: Simple cases use iterator chains, nested use flat_map
if generators.is_empty() {
bail!("Generator expression must have at least one generator");
}
// Single generator case (simple iterator chain)
if generators.len() == 1 {
let gen = &generators[0];
let iter_expr = gen.iter.to_rust_expr(self.ctx)?;
let element_expr = element.to_rust_expr(self.ctx)?;
let target_pat = self.parse_target_pattern(&gen.target)?;
let mut chain: syn::Expr = parse_quote! { #iter_expr.into_iter() };
// Add filters for each condition
for cond in &gen.conditions {
let cond_expr = cond.to_rust_expr(self.ctx)?;
chain = parse_quote! { #chain.filter(|#target_pat| #cond_expr) };
}
// Add the map transformation
chain = parse_quote! { #chain.map(|#target_pat| #element_expr) };
return Ok(chain);
}
// Multiple generators case (nested iteration with flat_map)
// Pattern: (x + y for x in range(3) for y in range(3))
// Becomes: (0..3).flat_map(|x| (0..3).map(move |y| x + y))
self.convert_nested_generators(element, generators)
}
fn convert_nested_generators(
&mut self,
element: &HirExpr,
generators: &[crate::hir::HirComprehension],
) -> Result<syn::Expr> {
// Start with the outermost generator
let first_gen = &generators[0];
let first_iter = first_gen.iter.to_rust_expr(self.ctx)?;
let first_pat = self.parse_target_pattern(&first_gen.target)?;
// Build the nested expression recursively
let inner_expr = self.build_nested_chain(element, generators, 1)?;
// Start the chain with the first generator
let mut chain: syn::Expr = parse_quote! { #first_iter.into_iter() };
// Add filters for first generator's conditions
for cond in &first_gen.conditions {
let cond_expr = cond.to_rust_expr(self.ctx)?;
chain = parse_quote! { #chain.filter(|#first_pat| #cond_expr) };
}
// Use flat_map for the first generator
chain = parse_quote! { #chain.flat_map(|#first_pat| #inner_expr) };
Ok(chain)
}
fn build_nested_chain(
&mut self,
element: &HirExpr,
generators: &[crate::hir::HirComprehension],
depth: usize,
) -> Result<syn::Expr> {
if depth >= generators.len() {
// Base case: no more generators, return the element expression
let element_expr = element.to_rust_expr(self.ctx)?;
return Ok(element_expr);
}
let gen = &generators[depth];
let iter_expr = gen.iter.to_rust_expr(self.ctx)?;
let target_pat = self.parse_target_pattern(&gen.target)?;
// Build the inner expression (recursive)
let inner_expr = self.build_nested_chain(element, generators, depth + 1)?;
// Build the chain for this level
let mut chain: syn::Expr = parse_quote! { #iter_expr.into_iter() };
// Add filters for this generator's conditions
for cond in &gen.conditions {
let cond_expr = cond.to_rust_expr(self.ctx)?;
chain = parse_quote! { #chain.filter(|#target_pat| #cond_expr) };
}
// Use flat_map for intermediate generators, map for the last
if depth < generators.len() - 1 {
// Intermediate generator: use flat_map
chain = parse_quote! { #chain.flat_map(move |#target_pat| #inner_expr) };
} else {
// Last generator: use map
chain = parse_quote! { #chain.map(move |#target_pat| #inner_expr) };
}
Ok(chain)
}
fn parse_target_pattern(&self, target: &str) -> Result<syn::Pat> {
// Handle simple variable: x
// Handle tuple: (x, y)
if target.starts_with('(') && target.ends_with(')') {
// Tuple pattern
let inner = &target[1..target.len() - 1];
let parts: Vec<&str> = inner.split(',').map(|s| s.trim()).collect();
let idents: Vec<syn::Ident> = parts
.iter()
.map(|s| syn::Ident::new(s, proc_macro2::Span::call_site()))
.collect();
Ok(parse_quote! { ( #(#idents),* ) })
} else {
// Simple variable
let ident = syn::Ident::new(target, proc_macro2::Span::call_site());
Ok(parse_quote! { #ident })
}
}
}
impl ToRustExpr for HirExpr {
fn to_rust_expr(&self, ctx: &mut CodeGenContext) -> Result<syn::Expr> {
let mut converter = ExpressionConverter::new(ctx);
match self {
HirExpr::Literal(lit) => {
let expr = literal_to_rust_expr(lit, &ctx.string_optimizer, &ctx.needs_cow, ctx);
if let Literal::String(s) = lit {
let context = StringContext::Literal(s.clone());
if matches!(
ctx.string_optimizer.get_optimal_type(&context),
crate::string_optimization::OptimalStringType::CowStr
) {
ctx.needs_cow = true;
}
}
Ok(expr)
}
HirExpr::Var(name) => converter.convert_variable(name),
HirExpr::Binary { op, left, right } => converter.convert_binary(*op, left, right),
HirExpr::Unary { op, operand } => converter.convert_unary(op, operand),
HirExpr::Call { func, args } => converter.convert_call(func, args),
HirExpr::MethodCall {
object,
method,
args,
} => converter.convert_method_call(object, method, args),
HirExpr::Index { base, index } => converter.convert_index(base, index),
HirExpr::Slice {
base,
start,
stop,
step,
} => converter.convert_slice(base, start, stop, step),
HirExpr::List(elts) => converter.convert_list(elts),
HirExpr::Dict(items) => converter.convert_dict(items),
HirExpr::Tuple(elts) => converter.convert_tuple(elts),
HirExpr::Set(elts) => converter.convert_set(elts),
HirExpr::FrozenSet(elts) => converter.convert_frozenset(elts),
HirExpr::Attribute { value, attr } => converter.convert_attribute(value, attr),
HirExpr::Borrow { expr, mutable } => converter.convert_borrow(expr, *mutable),
HirExpr::ListComp {
element,
target,
iter,
condition,
} => converter.convert_list_comp(element, target, iter, condition),
HirExpr::Lambda { params, body } => converter.convert_lambda(params, body),
HirExpr::SetComp {
element,
target,
iter,
condition,
} => converter.convert_set_comp(element, target, iter, condition),
HirExpr::Await { value } => converter.convert_await(value),
HirExpr::Yield { value } => converter.convert_yield(value),
HirExpr::FString { parts } => converter.convert_fstring(parts),
HirExpr::IfExpr { test, body, orelse } => converter.convert_ifexpr(test, body, orelse),
HirExpr::SortByKey {
iterable,
key_params,
key_body,
reverse,
} => converter.convert_sort_by_key(iterable, key_params, key_body, *reverse),
HirExpr::GeneratorExp { element, generators } => {
converter.convert_generator_expression(element, generators)
}
}
}
}
fn literal_to_rust_expr(
lit: &Literal,
string_optimizer: &StringOptimizer,
_needs_cow: &bool,
ctx: &CodeGenContext,
) -> syn::Expr {
match lit {
Literal::Int(n) => {
let lit = syn::LitInt::new(&n.to_string(), proc_macro2::Span::call_site());
parse_quote! { #lit }
}
Literal::Float(f) => {
// Ensure float literals always have a decimal point
// f64::to_string() outputs "0" for 0.0, which parses as integer
let s = f.to_string();
let float_str = if s.contains('.') || s.contains('e') || s.contains('E') {
s
} else {
format!("{}.0", s)
};
let lit = syn::LitFloat::new(&float_str, proc_macro2::Span::call_site());
parse_quote! { #lit }
}
Literal::String(s) => {
// Check if this string should be interned
if let Some(interned_name) = string_optimizer.get_interned_name(s) {
let ident = syn::Ident::new(&interned_name, proc_macro2::Span::call_site());
parse_quote! { #ident }
} else {
let lit = syn::LitStr::new(s, proc_macro2::Span::call_site());
// Use string optimizer to determine if we need .to_string()
let context = StringContext::Literal(s.clone());
match string_optimizer.get_optimal_type(&context) {
crate::string_optimization::OptimalStringType::StaticStr => {
// For read-only strings, just use the literal
parse_quote! { #lit }
}
crate::string_optimization::OptimalStringType::BorrowedStr { .. } => {
// Use &'static str for literals that can be borrowed
parse_quote! { #lit }
}
crate::string_optimization::OptimalStringType::CowStr => {
// Check if we're in a context where String is required
if let Some(Type::String) = &ctx.current_return_type {
// Function returns String, so convert to owned
parse_quote! { #lit.to_string() }
} else {
// Use Cow for flexible ownership
parse_quote! { std::borrow::Cow::Borrowed(#lit) }
}
}
crate::string_optimization::OptimalStringType::OwnedString => {
// Only use .to_string() when absolutely necessary
parse_quote! { #lit.to_string() }
}
}
}
}
Literal::Bool(b) => {
let lit = syn::LitBool::new(*b, proc_macro2::Span::call_site());
parse_quote! { #lit }
}
Literal::None => parse_quote! { None },
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::type_mapper::TypeMapper;
use depyler_annotations::TranspilationAnnotations;
fn create_test_context() -> CodeGenContext<'static> {
// This is a bit of a hack for testing - in real use, the TypeMapper would have a longer lifetime
let type_mapper: &'static TypeMapper = Box::leak(Box::new(TypeMapper::default()));
CodeGenContext {
type_mapper,
annotation_aware_mapper: AnnotationAwareTypeMapper::with_base_mapper(
type_mapper.clone(),
),
string_optimizer: StringOptimizer::new(),
union_enum_generator: crate::union_enum_gen::UnionEnumGenerator::new(),
generated_enums: Vec::new(),
needs_hashmap: false,
needs_hashset: false,
needs_fnv_hashmap: false,
needs_ahash_hashmap: false,
needs_arc: false,
needs_rc: false,
needs_cow: false,
declared_vars: vec![HashSet::new()],
current_function_can_fail: false,
current_return_type: None,
module_mapper: crate::module_mapper::ModuleMapper::new(),
imported_modules: std::collections::HashMap::new(),
imported_items: std::collections::HashMap::new(),
mutable_vars: HashSet::new(),
needs_zerodivisionerror: false,
needs_indexerror: false,
is_classmethod: false,
in_generator: false,
generator_state_vars: HashSet::new(),
}
}
#[test]
fn test_simple_function_generation() {
let func = HirFunction {
name: "add".to_string(),
params: vec![
HirParam::new("a".to_string(), Type::Int),
HirParam::new("b".to_string(), Type::Int),
]
.into(),
ret_type: Type::Int,
body: vec![HirStmt::Return(Some(HirExpr::Binary {
op: BinOp::Add,
left: Box::new(HirExpr::Var("a".to_string())),
right: Box::new(HirExpr::Var("b".to_string())),
}))],
properties: FunctionProperties::default(),
annotations: TranspilationAnnotations::default(),
docstring: None,
};
let mut ctx = create_test_context();
let tokens = func.to_rust_tokens(&mut ctx).unwrap();
let code = tokens.to_string();
assert!(code.contains("pub fn add"));
assert!(code.contains("i32"));
assert!(code.contains("return"));
}
#[test]
fn test_control_flow_generation() {
let if_stmt = HirStmt::If {
condition: HirExpr::Binary {
op: BinOp::Gt,
left: Box::new(HirExpr::Var("x".to_string())),
right: Box::new(HirExpr::Literal(Literal::Int(0))),
},
then_body: vec![HirStmt::Return(Some(HirExpr::Literal(Literal::String(
"positive".to_string(),
))))],
else_body: Some(vec![HirStmt::Return(Some(HirExpr::Literal(
Literal::String("negative".to_string()),
)))]),
};
let mut ctx = create_test_context();
let tokens = if_stmt.to_rust_tokens(&mut ctx).unwrap();
let code = tokens.to_string();
assert!(code.contains("if"));
assert!(code.contains("else"));
assert!(code.contains("return"));
}
#[test]
fn test_list_generation() {
// Test literal array generation
let list_expr = HirExpr::List(vec![
HirExpr::Literal(Literal::Int(1)),
HirExpr::Literal(Literal::Int(2)),
HirExpr::Literal(Literal::Int(3)),
]);
let mut ctx = create_test_context();
let expr = list_expr.to_rust_expr(&mut ctx).unwrap();
let code = quote! { #expr }.to_string();
// Small literal lists should generate arrays
assert!(code.contains("[") && code.contains("]"));
assert!(code.contains("1"));
assert!(code.contains("2"));
assert!(code.contains("3"));
// Test non-literal list still uses vec!
let var_list = HirExpr::List(vec![
HirExpr::Var("x".to_string()),
HirExpr::Var("y".to_string()),
]);
let expr2 = var_list.to_rust_expr(&mut ctx).unwrap();
let code2 = quote! { #expr2 }.to_string();
assert!(code2.contains("vec !"));
}
#[test]
fn test_dict_generation_sets_needs_hashmap() {
let dict_expr = HirExpr::Dict(vec![(
HirExpr::Literal(Literal::String("key".to_string())),
HirExpr::Literal(Literal::Int(42)),
)]);
let mut ctx = create_test_context();
assert!(!ctx.needs_hashmap);
let _ = dict_expr.to_rust_expr(&mut ctx).unwrap();
assert!(ctx.needs_hashmap);
}
#[test]
fn test_binary_operations() {
let ops = vec![
(BinOp::Add, "+"),
(BinOp::Sub, "-"),
(BinOp::Mul, "*"),
(BinOp::Eq, "=="),
(BinOp::Lt, "<"),
];
for (op, expected) in ops {
let result = convert_binop(op).unwrap();
assert_eq!(quote! { #result }.to_string(), expected);
}
}
#[test]
fn test_unsupported_operators() {
assert!(convert_binop(BinOp::Pow).is_err());
assert!(convert_binop(BinOp::In).is_err());
assert!(convert_binop(BinOp::NotIn).is_err());
}
// ========================================================================
// DEPYLER-0140 Phase 1: Tests for extracted statement handlers
// ========================================================================
#[test]
fn test_codegen_pass_stmt() {
let result = codegen_pass_stmt().unwrap();
assert!(result.is_empty(), "Pass statement should generate no code");
}
#[test]
fn test_codegen_break_stmt_simple() {
let result = codegen_break_stmt(&None).unwrap();
assert_eq!(result.to_string(), "break ;");
}
#[test]
fn test_codegen_break_stmt_with_label() {
let result = codegen_break_stmt(&Some("outer".to_string())).unwrap();
assert_eq!(result.to_string(), "break 'outer ;");
}
#[test]
fn test_codegen_continue_stmt_simple() {
let result = codegen_continue_stmt(&None).unwrap();
assert_eq!(result.to_string(), "continue ;");
}
#[test]
fn test_codegen_continue_stmt_with_label() {
let result = codegen_continue_stmt(&Some("outer".to_string())).unwrap();
assert_eq!(result.to_string(), "continue 'outer ;");
}
#[test]
fn test_codegen_expr_stmt() {
use crate::hir::Literal;
let mut ctx = create_test_context();
let expr = HirExpr::Literal(Literal::Int(42));
let result = codegen_expr_stmt(&expr, &mut ctx).unwrap();
assert_eq!(result.to_string(), "42 ;");
}
// ========================================================================
// DEPYLER-0140 Phase 2: Tests for medium-complexity statement handlers
// ========================================================================
#[test]
fn test_codegen_return_stmt_simple() {
use crate::hir::Literal;
let mut ctx = create_test_context();
let expr = Some(HirExpr::Literal(Literal::Int(42)));
let result = codegen_return_stmt(&expr, &mut ctx).unwrap();
assert_eq!(result.to_string(), "return 42 ;");
}
#[test]
fn test_codegen_return_stmt_none() {
let mut ctx = create_test_context();
let result = codegen_return_stmt(&None, &mut ctx).unwrap();
assert_eq!(result.to_string(), "return ;");
}
#[test]
fn test_codegen_while_stmt() {
use crate::hir::Literal;
let mut ctx = create_test_context();
let condition = HirExpr::Literal(Literal::Bool(true));
let body = vec![HirStmt::Pass];
let result = codegen_while_stmt(&condition, &body, &mut ctx).unwrap();
assert!(result.to_string().contains("while true"));
}
#[test]
fn test_codegen_raise_stmt_with_exception() {
use crate::hir::Literal;
let mut ctx = create_test_context();
let exc = Some(HirExpr::Literal(Literal::String("Error".to_string())));
let result = codegen_raise_stmt(&exc, &mut ctx).unwrap();
assert_eq!(result.to_string(), "return Err (\"Error\" . to_string ()) ;");
}
#[test]
fn test_codegen_raise_stmt_bare() {
let mut ctx = create_test_context();
let result = codegen_raise_stmt(&None, &mut ctx).unwrap();
assert_eq!(result.to_string(), "return Err (\"Exception raised\" . into ()) ;");
}
#[test]
fn test_codegen_with_stmt_with_target() {
use crate::hir::Literal;
let mut ctx = create_test_context();
let context = HirExpr::Literal(Literal::Int(42));
let target = Some("file".to_string());
let body = vec![HirStmt::Pass];
let result = codegen_with_stmt(&context, &target, &body, &mut ctx).unwrap();
assert!(result.to_string().contains("let mut file"));
}
#[test]
fn test_codegen_with_stmt_no_target() {
use crate::hir::Literal;
let mut ctx = create_test_context();
let context = HirExpr::Literal(Literal::Int(42));
let body = vec![HirStmt::Pass];
let result = codegen_with_stmt(&context, &None, &body, &mut ctx).unwrap();
assert!(result.to_string().contains("let _context"));
}
// Phase 3b tests - Assign handler tests
#[test]
fn test_codegen_assign_symbol_new_var() {
let mut ctx = create_test_context();
let value_expr = syn::parse_quote! { 42 };
let result = codegen_assign_symbol("x", value_expr, None, &mut ctx).unwrap();
assert!(result.to_string().contains("let x = 42"));
}
#[test]
fn test_codegen_assign_symbol_with_type() {
let mut ctx = create_test_context();
let value_expr = syn::parse_quote! { 42 };
let type_ann = Some(quote! { : i32 });
let result = codegen_assign_symbol("x", value_expr, type_ann, &mut ctx).unwrap();
assert!(result.to_string().contains("let x : i32 = 42"));
}
#[test]
fn test_codegen_assign_symbol_existing_var() {
let mut ctx = create_test_context();
ctx.declare_var("x");
let value_expr = syn::parse_quote! { 100 };
let result = codegen_assign_symbol("x", value_expr, None, &mut ctx).unwrap();
assert_eq!(result.to_string(), "x = 100 ;");
}
#[test]
fn test_codegen_assign_index() {
use crate::hir::Literal;
let mut ctx = create_test_context();
let base = HirExpr::Var("dict".to_string());
let index = HirExpr::Literal(Literal::String("key".to_string()));
let value_expr = syn::parse_quote! { 42 };
let result = codegen_assign_index(&base, &index, value_expr, &mut ctx).unwrap();
assert!(result.to_string().contains("dict . insert"));
}
#[test]
fn test_codegen_assign_attribute() {
let mut ctx = create_test_context();
let base = HirExpr::Var("obj".to_string());
let value_expr = syn::parse_quote! { 42 };
let result = codegen_assign_attribute(&base, "field", value_expr, &mut ctx).unwrap();
assert_eq!(result.to_string(), "obj . field = 42 ;");
}
#[test]
fn test_codegen_assign_tuple_new_vars() {
use crate::hir::AssignTarget;
let mut ctx = create_test_context();
let targets = vec![
AssignTarget::Symbol("a".to_string()),
AssignTarget::Symbol("b".to_string()),
];
let value_expr = syn::parse_quote! { (1, 2) };
let result = codegen_assign_tuple(&targets, value_expr, None, &mut ctx).unwrap();
assert!(result.to_string().contains("let (a , b) = (1 , 2)"));
}
// Phase 3b tests - Try handler tests
#[test]
fn test_codegen_try_stmt_simple() {
use crate::hir::ExceptHandler;
let mut ctx = create_test_context();
let body = vec![HirStmt::Pass];
let handlers = vec![ExceptHandler {
exception_type: None,
name: None,
body: vec![HirStmt::Pass],
}];
let result = codegen_try_stmt(&body, &handlers, &None, &mut ctx).unwrap();
let result_str = result.to_string();
assert!(result_str.contains("let _result"));
assert!(result_str.contains("if let Err (_e) = _result"));
}
#[test]
fn test_codegen_try_stmt_with_finally() {
let mut ctx = create_test_context();
let body = vec![HirStmt::Pass];
let handlers = vec![];
let finally = Some(vec![HirStmt::Pass]);
let result = codegen_try_stmt(&body, &handlers, &finally, &mut ctx).unwrap();
assert!(!result.to_string().is_empty());
}
#[test]
fn test_codegen_try_stmt_except_and_finally() {
use crate::hir::ExceptHandler;
let mut ctx = create_test_context();
let body = vec![HirStmt::Pass];
let handlers = vec![ExceptHandler {
exception_type: None,
name: Some("e".to_string()),
body: vec![HirStmt::Pass],
}];
let finally = Some(vec![HirStmt::Pass]);
let result = codegen_try_stmt(&body, &handlers, &finally, &mut ctx).unwrap();
let result_str = result.to_string();
assert!(result_str.contains("let _result"));
assert!(result_str.contains("if let Err (_e) = _result"));
}
// Phase 1b/1c tests - Type conversion functions (DEPYLER-0149)
#[test]
fn test_int_cast_conversion() {
// Python: int(x) → Rust: x (no cast, let type inference handle it)
// Phase 1c change: Removed cast to allow usize/i32 inference based on context
let call_expr = HirExpr::Call {
func: "int".to_string(),
args: vec![HirExpr::Var("x".to_string())],
};
let mut ctx = create_test_context();
let result = call_expr.to_rust_expr(&mut ctx).unwrap();
let code = quote! { #result }.to_string();
// Should just be 'x' without cast
assert!(code.contains("x"), "Expected 'x', got: {}", code);
assert!(!code.contains("as"), "Should not contain cast, got: {}", code);
}
#[test]
fn test_float_cast_conversion() {
// Python: float(x) → Rust: (x) as f64
let call_expr = HirExpr::Call {
func: "float".to_string(),
args: vec![HirExpr::Var("y".to_string())],
};
let mut ctx = create_test_context();
let result = call_expr.to_rust_expr(&mut ctx).unwrap();
let code = quote! { #result }.to_string();
assert!(code.contains("as f64"), "Expected '(y) as f64', got: {}", code);
}
#[test]
fn test_str_conversion() {
// Python: str(x) → Rust: x.to_string()
let call_expr = HirExpr::Call {
func: "str".to_string(),
args: vec![HirExpr::Var("value".to_string())],
};
let mut ctx = create_test_context();
let result = call_expr.to_rust_expr(&mut ctx).unwrap();
let code = quote! { #result }.to_string();
assert!(code.contains("to_string"), "Expected 'value.to_string()', got: {}", code);
}
#[test]
fn test_bool_cast_conversion() {
// Python: bool(x) → Rust: (x) as bool
let call_expr = HirExpr::Call {
func: "bool".to_string(),
args: vec![HirExpr::Var("flag".to_string())],
};
let mut ctx = create_test_context();
let result = call_expr.to_rust_expr(&mut ctx).unwrap();
let code = quote! { #result }.to_string();
assert!(code.contains("as bool"), "Expected '(flag) as bool', got: {}", code);
}
#[test]
fn test_int_cast_with_expression() {
// Python: int((low + high) / 2) → Rust: (low + high) / 2 (no cast)
// Phase 1c: Integer division already works in Rust, let type inference handle it
let division = HirExpr::Binary {
op: BinOp::Div,
left: Box::new(HirExpr::Binary {
op: BinOp::Add,
left: Box::new(HirExpr::Var("low".to_string())),
right: Box::new(HirExpr::Var("high".to_string())),
}),
right: Box::new(HirExpr::Literal(Literal::Int(2))),
};
let call_expr = HirExpr::Call {
func: "int".to_string(),
args: vec![division],
};
let mut ctx = create_test_context();
let result = call_expr.to_rust_expr(&mut ctx).unwrap();
let code = quote! { #result }.to_string();
// Should preserve the expression without cast
assert!(code.contains("low"), "Expected 'low' variable, got: {}", code);
assert!(code.contains("high"), "Expected 'high' variable, got: {}", code);
assert!(!code.contains("as"), "Should not contain cast, got: {}", code);
}
#[test]
fn test_float_literal_decimal_point() {
// Regression test for DEPYLER-TBD: Ensure float literals always have decimal point
// Bug: f64::to_string() for 0.0 produces "0" (no decimal), parsed as integer
// Fix: Always ensure ".0" suffix for floats without decimal/exponent
let mut ctx = create_test_context();
// Test 0.0 → should generate "0.0" not "0"
let zero_float = HirExpr::Literal(Literal::Float(0.0));
let result = zero_float.to_rust_expr(&mut ctx).unwrap();
let code = quote! { #result }.to_string();
assert!(code.contains("0.0") || code.contains("0 ."),
"Expected '0.0' for float zero, got: {}", code);
// Test 42.0 → should generate "42.0" not "42"
let forty_two = HirExpr::Literal(Literal::Float(42.0));
let result = forty_two.to_rust_expr(&mut ctx).unwrap();
let code = quote! { #result }.to_string();
assert!(code.contains("42.0") || code.contains("42 ."),
"Expected '42.0' for float, got: {}", code);
// Test 1.5 → should preserve "1.5" (already has decimal)
let one_half = HirExpr::Literal(Literal::Float(1.5));
let result = one_half.to_rust_expr(&mut ctx).unwrap();
let code = quote! { #result }.to_string();
assert!(code.contains("1.5"), "Expected '1.5', got: {}", code);
// Test scientific notation: 1e10 → should preserve (has 'e')
let scientific = HirExpr::Literal(Literal::Float(1e10));
let result = scientific.to_rust_expr(&mut ctx).unwrap();
let code = quote! { #result }.to_string();
assert!(code.contains("e") || code.contains("E") || code.contains("."),
"Expected scientific notation or decimal, got: {}", code);
}
#[test]
fn test_string_method_return_types() {
// Regression test for v3.16.0 Phase 1
// String transformation methods (.upper(), .lower(), .strip()) return owned String
// Function signatures should reflect this: `fn f(s: &str) -> String` not `-> &str`
// Test 1: .upper() should generate String return type
let upper_func = HirFunction {
name: "to_upper".to_string(),
params: vec![HirParam::new("text".to_string(), Type::String)].into(),
ret_type: Type::String,
body: vec![HirStmt::Return(Some(HirExpr::MethodCall {
object: Box::new(HirExpr::Var("text".to_string())),
method: "upper".to_string(),
args: vec![],
}))],
properties: FunctionProperties::default(),
annotations: TranspilationAnnotations::default(),
docstring: None,
};
let mut ctx = create_test_context();
let result = upper_func.to_rust_tokens(&mut ctx).unwrap();
let code = result.to_string();
// Should generate: fn to_upper(text: &str) -> String
// NOT: fn to_upper<'a>(text: &'a str) -> &'a str
assert!(code.contains("-> String"),
"Expected '-> String' for .upper() method, got: {}", code);
assert!(!code.contains("-> & ") && !code.contains("-> &'"),
"Should not generate borrowed return for .upper(), got: {}", code);
// Test 2: .lower() should also generate String return type
let lower_func = HirFunction {
name: "to_lower".to_string(),
params: vec![HirParam::new("text".to_string(), Type::String)].into(),
ret_type: Type::String,
body: vec![HirStmt::Return(Some(HirExpr::MethodCall {
object: Box::new(HirExpr::Var("text".to_string())),
method: "lower".to_string(),
args: vec![],
}))],
properties: FunctionProperties::default(),
annotations: TranspilationAnnotations::default(),
docstring: None,
};
let mut ctx = create_test_context();
let result = lower_func.to_rust_tokens(&mut ctx).unwrap();
let code = result.to_string();
assert!(code.contains("-> String"),
"Expected '-> String' for .lower() method, got: {}", code);
// Test 3: .strip() should also generate String return type
let strip_func = HirFunction {
name: "trim_text".to_string(),
params: vec![HirParam::new("text".to_string(), Type::String)].into(),
ret_type: Type::String,
body: vec![HirStmt::Return(Some(HirExpr::MethodCall {
object: Box::new(HirExpr::Var("text".to_string())),
method: "strip".to_string(),
args: vec![],
}))],
properties: FunctionProperties::default(),
annotations: TranspilationAnnotations::default(),
docstring: None,
};
let mut ctx = create_test_context();
let result = strip_func.to_rust_tokens(&mut ctx).unwrap();
let code = result.to_string();
assert!(code.contains("-> String"),
"Expected '-> String' for .strip() method, got: {}", code);
}
#[test]
fn test_int_float_division_semantics() {
// Regression test for v3.16.0 Phase 2
// Python's `/` operator always returns float, even with int operands
// Rust's `/` does integer division with int operands
// We need to cast to float when the context expects float
// Test 1: int / int returning float (the main bug)
let divide_func = HirFunction {
name: "safe_divide".to_string(),
params: vec![
HirParam::new("a".to_string(), Type::Int),
HirParam::new("b".to_string(), Type::Int),
].into(),
ret_type: Type::Float, // Expects float return!
body: vec![HirStmt::Return(Some(HirExpr::Binary {
op: BinOp::Div,
left: Box::new(HirExpr::Var("a".to_string())),
right: Box::new(HirExpr::Var("b".to_string())),
}))],
properties: FunctionProperties::default(),
annotations: TranspilationAnnotations::default(),
docstring: None,
};
let mut ctx = create_test_context();
let result = divide_func.to_rust_tokens(&mut ctx).unwrap();
let code = result.to_string();
// Should generate: (a as f64) / (b as f64)
// NOT: a / b (which would do integer division)
assert!(code.contains("as f64") || code.contains("as f32"),
"Expected float cast for int/int division with float return, got: {}", code);
assert!(code.contains("-> f64") || code.contains("-> f32"),
"Expected float return type, got: {}", code);
// Test 2: int // int returning int (floor division - should NOT cast)
let floor_div_func = HirFunction {
name: "floor_divide".to_string(),
params: vec![
HirParam::new("a".to_string(), Type::Int),
HirParam::new("b".to_string(), Type::Int),
].into(),
ret_type: Type::Int, // Expects int return
body: vec![HirStmt::Return(Some(HirExpr::Binary {
op: BinOp::FloorDiv,
left: Box::new(HirExpr::Var("a".to_string())),
right: Box::new(HirExpr::Var("b".to_string())),
}))],
properties: FunctionProperties::default(),
annotations: TranspilationAnnotations::default(),
docstring: None,
};
let mut ctx = create_test_context();
let result = floor_div_func.to_rust_tokens(&mut ctx).unwrap();
let code = result.to_string();
// Floor division should NOT add float casts
assert!(code.contains("-> i32") || code.contains("-> i64"),
"Expected int return type for floor division, got: {}", code);
// Test 3: float / float should work without changes
let float_div_func = HirFunction {
name: "divide_floats".to_string(),
params: vec![
HirParam::new("a".to_string(), Type::Float),
HirParam::new("b".to_string(), Type::Float),
].into(),
ret_type: Type::Float,
body: vec![HirStmt::Return(Some(HirExpr::Binary {
op: BinOp::Div,
left: Box::new(HirExpr::Var("a".to_string())),
right: Box::new(HirExpr::Var("b".to_string())),
}))],
properties: FunctionProperties::default(),
annotations: TranspilationAnnotations::default(),
docstring: None,
};
let mut ctx = create_test_context();
let result = float_div_func.to_rust_tokens(&mut ctx).unwrap();
let code = result.to_string();
assert!(code.contains("-> f64") || code.contains("-> f32"),
"Expected float return type, got: {}", code);
}
}