use crate::borrowing_context::{BorrowingContext, BorrowingStrategy};
use depyler_hir::hir::{AssignTarget, HirExpr, HirFunction, HirStmt, Type};
use crate::param_type_inference::infer_param_type_from_body;
use crate::type_mapper::RustType;
use indexmap::IndexMap;
use std::collections::{HashMap, HashSet};
#[derive(Debug)]
pub struct LifetimeInference {
lifetime_counter: usize,
variable_lifetimes: HashMap<String, LifetimeInfo>,
lifetime_constraints: HashMap<String, HashSet<String>>,
param_analysis: HashMap<String, ParamUsage>,
}
#[derive(Debug, Clone)]
pub struct LifetimeInfo {
pub name: String,
pub is_static: bool,
pub outlives: HashSet<String>,
pub source: LifetimeSource,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LifetimeSource {
Parameter(String),
StaticLiteral,
Local,
Return,
Field(String),
}
#[derive(Debug, Clone, Default)]
pub struct ParamUsage {
pub is_mutated: bool,
pub is_moved: bool,
pub escapes: bool,
pub is_read_only: bool,
pub used_in_loop: bool,
pub has_nested_borrows: bool,
}
#[derive(Debug, Clone)]
pub enum LifetimeConstraint {
Outlives,
Equal,
AtLeast,
}
#[derive(Debug, Clone)]
pub struct LifetimeResult {
pub param_lifetimes: IndexMap<String, InferredParam>,
pub return_lifetime: Option<String>,
pub lifetime_params: Vec<String>,
pub lifetime_bounds: Vec<(String, String)>,
pub borrowing_strategies: IndexMap<String, BorrowingStrategy>,
}
#[derive(Debug, Clone)]
pub struct InferredParam {
pub should_borrow: bool,
pub needs_mut: bool,
pub lifetime: Option<String>,
pub rust_type: RustType,
}
impl LifetimeInference {
pub fn new() -> Self {
Self {
lifetime_counter: 0,
variable_lifetimes: HashMap::new(),
lifetime_constraints: HashMap::new(),
param_analysis: HashMap::new(),
}
}
fn next_lifetime(&mut self) -> String {
let name = match self.lifetime_counter {
0 => "'a".to_string(),
1 => "'b".to_string(),
2 => "'c".to_string(),
n => format!("'l{}", n - 2),
};
self.lifetime_counter += 1;
name
}
#[allow(dead_code)]
fn add_constraint(&mut self, from: &str, to: &str, _constraint: LifetimeConstraint) {
self.lifetime_constraints
.entry(from.to_string())
.or_default()
.insert(to.to_string());
}
pub fn analyze_function(
&mut self,
func: &HirFunction,
type_mapper: &crate::type_mapper::TypeMapper,
) -> LifetimeResult {
let mut borrowing_ctx = BorrowingContext::new(Some(func.ret_type.clone()));
let borrowing_result = borrowing_ctx.analyze_function(func, type_mapper);
let mut param_lifetimes = IndexMap::new();
let mut lifetime_params = HashSet::new();
for param in &func.params {
let strategy = borrowing_result
.param_strategies
.get(¶m.name)
.cloned()
.unwrap_or(BorrowingStrategy::TakeOwnership);
let param_type = if matches!(param.ty, Type::Unknown) {
if let Some(inferred) = infer_param_type_from_body(¶m.name, &func.body) {
inferred
} else {
param.ty.clone()
}
} else {
param.ty.clone()
};
let rust_type = type_mapper.map_type(¶m_type);
let (should_borrow, needs_mut, lifetime) = match strategy {
BorrowingStrategy::BorrowImmutable { lifetime } => {
let lt = lifetime.unwrap_or_else(|| self.next_lifetime());
lifetime_params.insert(lt.clone());
(true, false, Some(lt))
}
BorrowingStrategy::BorrowMutable { lifetime } => {
let lt = lifetime.unwrap_or_else(|| self.next_lifetime());
lifetime_params.insert(lt.clone());
(true, true, Some(lt))
}
BorrowingStrategy::UseCow { lifetime } => {
lifetime_params.insert(lifetime.clone());
(false, false, Some(lifetime))
}
_ => (false, false, None),
};
if let Some(ref lt) = lifetime {
self.variable_lifetimes.insert(
param.name.clone(),
LifetimeInfo {
name: lt.clone(),
is_static: lt == "'static",
outlives: HashSet::new(),
source: LifetimeSource::Parameter(param.name.clone()),
},
);
}
param_lifetimes.insert(
param.name.clone(),
InferredParam {
should_borrow,
needs_mut,
lifetime,
rust_type,
},
);
}
let return_lifetime = self.analyze_return_lifetime(func, type_mapper);
if let Some(ref lt) = return_lifetime {
lifetime_params.insert(lt.clone());
}
let lifetime_bounds = self.compute_lifetime_bounds();
LifetimeResult {
param_lifetimes,
return_lifetime,
lifetime_params: lifetime_params.into_iter().collect(),
lifetime_bounds,
borrowing_strategies: borrowing_result.param_strategies,
}
}
#[allow(dead_code)]
fn analyze_parameter_usage(&mut self, func: &HirFunction) {
for param in &func.params {
let mut usage = ParamUsage::default();
for stmt in &func.body {
self.analyze_stmt_for_param(¶m.name, stmt, &mut usage, false);
}
self.param_analysis.insert(param.name.clone(), usage);
}
}
#[allow(dead_code)]
fn analyze_stmt_for_param(
&self,
param: &str,
stmt: &HirStmt,
usage: &mut ParamUsage,
in_loop: bool,
) {
match stmt {
HirStmt::Expr(expr) => self.analyze_expr_for_param(param, expr, usage, in_loop, false),
HirStmt::Assign { target, value, .. } => {
match target {
AssignTarget::Symbol(symbol) => {
if symbol == param {
usage.is_mutated = true;
}
}
AssignTarget::Index { base, .. } => {
if let HirExpr::Var(var_name) = base.as_ref() {
if var_name == param {
usage.is_mutated = true;
}
}
}
AssignTarget::Attribute {
value: attr_base, ..
} => {
if let HirExpr::Var(var_name) = attr_base.as_ref() {
if var_name == param {
usage.is_mutated = true;
}
}
}
AssignTarget::Tuple(targets) => {
for t in targets {
Self::check_target_mutation(t, param, usage);
}
}
}
self.analyze_expr_for_param(param, value, usage, in_loop, false);
}
HirStmt::Return(value) => {
if let Some(expr) = value {
self.analyze_expr_for_param(param, expr, usage, in_loop, true);
}
}
HirStmt::If {
condition,
then_body,
else_body,
} => {
self.analyze_expr_for_param(param, condition, usage, in_loop, false);
for stmt in then_body {
self.analyze_stmt_for_param(param, stmt, usage, in_loop);
}
if let Some(else_stmts) = else_body {
for stmt in else_stmts {
self.analyze_stmt_for_param(param, stmt, usage, in_loop);
}
}
}
HirStmt::While { condition, body } => {
self.analyze_expr_for_param(param, condition, usage, true, false);
for stmt in body {
self.analyze_stmt_for_param(param, stmt, usage, true);
}
}
HirStmt::For { iter, body, .. } => {
self.analyze_expr_for_param(param, iter, usage, true, false);
for stmt in body {
self.analyze_stmt_for_param(param, stmt, usage, true);
}
}
HirStmt::Raise { exception, cause } => {
if let Some(exc) = exception {
self.analyze_expr_for_param(param, exc, usage, in_loop, false);
}
if let Some(c) = cause {
self.analyze_expr_for_param(param, c, usage, in_loop, false);
}
}
HirStmt::Break { .. } | HirStmt::Continue { .. } | HirStmt::Pass => {
}
HirStmt::Block(stmts) => {
for s in stmts {
self.analyze_stmt_for_param(param, s, usage, in_loop);
}
}
HirStmt::Assert { test, msg } => {
self.analyze_expr_for_param(param, test, usage, in_loop, false);
if let Some(message) = msg {
self.analyze_expr_for_param(param, message, usage, in_loop, false);
}
}
HirStmt::With {
context,
target: _,
body,
..
} => {
self.analyze_expr_for_param(param, context, usage, in_loop, false);
for stmt in body {
self.analyze_stmt_for_param(param, stmt, usage, in_loop);
}
}
HirStmt::Try {
body,
handlers,
orelse,
finalbody,
} => {
for stmt in body {
self.analyze_stmt_for_param(param, stmt, usage, in_loop);
}
for handler in handlers {
for stmt in &handler.body {
self.analyze_stmt_for_param(param, stmt, usage, in_loop);
}
}
if let Some(else_stmts) = orelse {
for stmt in else_stmts {
self.analyze_stmt_for_param(param, stmt, usage, in_loop);
}
}
if let Some(finally_stmts) = finalbody {
for stmt in finally_stmts {
self.analyze_stmt_for_param(param, stmt, usage, in_loop);
}
}
}
HirStmt::FunctionDef { body, .. } => {
for stmt in body {
self.analyze_stmt_for_param(param, stmt, usage, in_loop);
}
}
}
}
fn check_target_mutation(target: &AssignTarget, param: &str, usage: &mut ParamUsage) {
match target {
AssignTarget::Symbol(symbol) => {
if symbol == param {
usage.is_mutated = true;
}
}
AssignTarget::Index { base, .. } => {
if let HirExpr::Var(var_name) = base.as_ref() {
if var_name == param {
usage.is_mutated = true;
}
}
}
AssignTarget::Attribute { value, .. } => {
if let HirExpr::Var(var_name) = value.as_ref() {
if var_name == param {
usage.is_mutated = true;
}
}
}
AssignTarget::Tuple(targets) => {
for t in targets {
Self::check_target_mutation(t, param, usage);
}
}
}
}
#[allow(dead_code, clippy::only_used_in_recursion)]
fn analyze_expr_for_param(
&self,
param: &str,
expr: &HirExpr,
usage: &mut ParamUsage,
in_loop: bool,
in_return: bool,
) {
match expr {
HirExpr::Var(id) => {
if id == param {
usage.is_read_only = true;
if in_return {
usage.escapes = true;
}
if in_loop {
usage.used_in_loop = true;
}
}
}
HirExpr::Attribute { value, .. } => {
if let HirExpr::Var(id) = &**value {
if id == param {
usage.is_read_only = true;
usage.has_nested_borrows = true;
}
}
self.analyze_expr_for_param(param, value, usage, in_loop, in_return);
}
HirExpr::Index { base, index } => {
self.analyze_expr_for_param(param, base, usage, in_loop, false);
self.analyze_expr_for_param(param, index, usage, in_loop, false);
}
HirExpr::Call { func: _, args, .. } => {
for arg in args {
if let HirExpr::Var(id) = arg {
if id == param {
usage.is_moved = true;
}
}
}
for arg in args {
self.analyze_expr_for_param(param, arg, usage, in_loop, false);
}
}
HirExpr::List(elements) | HirExpr::Tuple(elements) => {
for elem in elements {
self.analyze_expr_for_param(param, elem, usage, in_loop, in_return);
}
}
HirExpr::Dict(pairs) => {
for (k, v) in pairs {
self.analyze_expr_for_param(param, k, usage, in_loop, false);
self.analyze_expr_for_param(param, v, usage, in_loop, in_return);
}
}
HirExpr::Binary { left, right, .. } => {
self.analyze_expr_for_param(param, left, usage, in_loop, false);
self.analyze_expr_for_param(param, right, usage, in_loop, false);
}
HirExpr::Unary { operand, .. } => {
self.analyze_expr_for_param(param, operand, usage, in_loop, false);
}
HirExpr::Literal(_) => {}
HirExpr::MethodCall { object, args, .. } => {
self.analyze_expr_for_param(param, object, usage, in_loop, in_return);
for arg in args {
self.analyze_expr_for_param(param, arg, usage, in_loop, in_return);
}
}
HirExpr::Slice {
base,
start,
stop,
step,
} => {
self.analyze_expr_for_param(param, base, usage, in_loop, in_return);
if let Some(s) = start {
self.analyze_expr_for_param(param, s, usage, in_loop, in_return);
}
if let Some(s) = stop {
self.analyze_expr_for_param(param, s, usage, in_loop, in_return);
}
if let Some(s) = step {
self.analyze_expr_for_param(param, s, usage, in_loop, in_return);
}
}
HirExpr::Borrow { expr, .. } => {
self.analyze_expr_for_param(param, expr, usage, in_loop, in_return);
}
HirExpr::ListComp {
element,
generators,
} => {
for gen in generators {
let target_shadows = gen.target == param
|| gen.target.contains(&format!("({})", param))
|| gen.target.contains(&format!("{},", param))
|| gen.target.contains(&format!(", {}", param));
if !target_shadows {
self.analyze_expr_for_param(param, &gen.iter, usage, true, false);
for cond in &gen.conditions {
self.analyze_expr_for_param(param, cond, usage, true, false);
}
}
}
self.analyze_expr_for_param(param, element, usage, true, in_return);
}
HirExpr::Lambda { params: _, body } => {
self.analyze_expr_for_param(param, body, usage, in_loop, in_return);
}
HirExpr::Set(elements) | HirExpr::FrozenSet(elements) => {
for elem in elements {
self.analyze_expr_for_param(param, elem, usage, in_loop, in_return);
}
}
HirExpr::SetComp {
element,
generators,
} => {
for gen in generators {
let target_shadows = gen.target == param
|| gen.target.contains(&format!("({})", param))
|| gen.target.contains(&format!("{},", param))
|| gen.target.contains(&format!(", {}", param));
if !target_shadows {
self.analyze_expr_for_param(param, &gen.iter, usage, true, false);
for cond in &gen.conditions {
self.analyze_expr_for_param(param, cond, usage, true, false);
}
}
}
self.analyze_expr_for_param(param, element, usage, true, in_return);
}
HirExpr::DictComp {
key,
value,
generators,
} => {
for gen in generators {
let target_shadows = gen.target == param
|| gen.target.contains(&format!("({})", param))
|| gen.target.contains(&format!("{},", param))
|| gen.target.contains(&format!(", {}", param));
if !target_shadows {
self.analyze_expr_for_param(param, &gen.iter, usage, true, false);
for cond in &gen.conditions {
self.analyze_expr_for_param(param, cond, usage, true, false);
}
}
}
self.analyze_expr_for_param(param, key, usage, true, in_return);
self.analyze_expr_for_param(param, value, usage, true, in_return);
}
HirExpr::Await { value } => {
self.analyze_expr_for_param(param, value, usage, in_loop, in_return);
}
HirExpr::Yield { value } => {
if let Some(v) = value {
self.analyze_expr_for_param(param, v, usage, in_loop, in_return);
}
}
HirExpr::FString { .. } => {
}
HirExpr::IfExpr { test, body, orelse } => {
self.analyze_expr_for_param(param, test, usage, in_loop, in_return);
self.analyze_expr_for_param(param, body, usage, in_loop, in_return);
self.analyze_expr_for_param(param, orelse, usage, in_loop, in_return);
}
HirExpr::SortByKey {
iterable, key_body, ..
} => {
self.analyze_expr_for_param(param, iterable, usage, in_loop, in_return);
self.analyze_expr_for_param(param, key_body, usage, in_loop, in_return);
}
HirExpr::GeneratorExp {
element,
generators,
} => {
self.analyze_expr_for_param(param, element, usage, in_loop, in_return);
for gen in generators {
self.analyze_expr_for_param(param, &gen.iter, usage, in_loop, in_return);
for cond in &gen.conditions {
self.analyze_expr_for_param(param, cond, usage, in_loop, in_return);
}
}
}
HirExpr::NamedExpr { value, .. } => {
self.analyze_expr_for_param(param, value, usage, in_loop, in_return);
}
HirExpr::DynamicCall { callee, args, .. } => {
self.analyze_expr_for_param(param, callee, usage, in_loop, in_return);
for arg in args {
self.analyze_expr_for_param(param, arg, usage, in_loop, in_return);
}
}
}
}
#[allow(dead_code)]
fn infer_parameter_lifetimes(
&mut self,
func: &HirFunction,
type_mapper: &crate::type_mapper::TypeMapper,
) -> IndexMap<String, InferredParam> {
let mut result = IndexMap::new();
for param in &func.params {
let usage = self
.param_analysis
.get(¶m.name)
.cloned()
.unwrap_or_default();
let param_type = if matches!(param.ty, Type::Unknown) {
if let Some(inferred) = infer_param_type_from_body(¶m.name, &func.body) {
inferred
} else {
param.ty.clone()
}
} else {
param.ty.clone()
};
let rust_type = type_mapper.map_type(¶m_type);
let escapes_as_self =
usage.escapes && rust_type == type_mapper.map_return_type(&func.ret_type);
let is_callable_type = matches!(¶m_type, Type::Function { .. })
|| matches!(¶m_type, Type::Optional(inner) if matches!(inner.as_ref(), Type::Function { .. }));
let should_borrow = !is_callable_type
&& !usage.is_moved
&& !escapes_as_self
&& (usage.is_read_only || usage.is_mutated);
let needs_mut = usage.is_mutated;
let lifetime = if should_borrow {
let lt = self.next_lifetime();
self.variable_lifetimes.insert(
param.name.clone(),
LifetimeInfo {
name: lt.clone(),
is_static: false,
outlives: HashSet::new(),
source: LifetimeSource::Parameter(param.name.clone()),
},
);
if usage.escapes {
self.add_constraint(<, "'return", LifetimeConstraint::Outlives);
}
Some(lt)
} else {
None
};
result.insert(
param.name.clone(),
InferredParam {
should_borrow,
needs_mut,
lifetime,
rust_type,
},
);
}
result
}
fn analyze_return_lifetime(
&mut self,
func: &HirFunction,
type_mapper: &crate::type_mapper::TypeMapper,
) -> Option<String> {
let return_rust_type = type_mapper.map_return_type(&func.ret_type);
if self.return_type_needs_lifetime(&return_rust_type) {
for (param_name, usage) in &self.param_analysis {
if usage.escapes {
if let Some(info) = self.variable_lifetimes.get(param_name) {
return Some(info.name.clone());
}
}
}
Some(self.next_lifetime())
} else {
None
}
}
#[allow(clippy::only_used_in_recursion)]
fn return_type_needs_lifetime(&self, rust_type: &RustType) -> bool {
match rust_type {
RustType::Str { .. } => true,
RustType::Reference { .. } => true,
RustType::Cow { .. } => true,
RustType::Vec(inner) | RustType::Option(inner) => {
self.return_type_needs_lifetime(inner)
}
RustType::Result(ok, err) => {
self.return_type_needs_lifetime(ok) || self.return_type_needs_lifetime(err)
}
RustType::Tuple(types) => types.iter().any(|t| self.return_type_needs_lifetime(t)),
_ => false,
}
}
fn compute_lifetime_bounds(&self) -> Vec<(String, String)> {
let mut bounds = Vec::new();
for (from, tos) in &self.lifetime_constraints {
for to in tos {
if to != "'return" {
bounds.push((from.clone(), to.clone()));
}
}
}
bounds
}
pub fn apply_elision_rules(
&mut self,
func: &HirFunction,
type_mapper: &crate::type_mapper::TypeMapper,
) -> Option<LifetimeResult> {
let full_result = self.analyze_function(func, type_mapper);
let ref_params: Vec<_> = full_result
.param_lifetimes
.iter()
.filter(|(_, param)| param.should_borrow)
.collect();
let return_needs_lifetime = full_result.return_lifetime.is_some();
if ref_params.is_empty() {
return Some(LifetimeResult {
param_lifetimes: full_result.param_lifetimes,
return_lifetime: None,
lifetime_params: vec![],
lifetime_bounds: vec![],
borrowing_strategies: full_result.borrowing_strategies,
});
}
if ref_params.len() == 1 {
return Some(LifetimeResult {
param_lifetimes: full_result.param_lifetimes,
return_lifetime: None, lifetime_params: vec![], lifetime_bounds: vec![],
borrowing_strategies: full_result.borrowing_strategies,
});
}
if let Some((_name, _)) = ref_params.iter().find(|(name, _)| *name == "self") {
if return_needs_lifetime {
return Some(LifetimeResult {
param_lifetimes: full_result.param_lifetimes,
return_lifetime: None, lifetime_params: vec![],
lifetime_bounds: vec![],
borrowing_strategies: full_result.borrowing_strategies,
});
}
}
Some(full_result)
}
#[allow(dead_code)]
fn is_reference_type(&self, rust_type: &RustType) -> bool {
matches!(
rust_type,
RustType::Str { .. } | RustType::Reference { .. } | RustType::Cow { .. }
)
}
}
impl Default for LifetimeInference {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use depyler_hir::hir::{
BinOp, FunctionProperties, HirComprehension, HirFunction, HirParam, Literal, Symbol,
Type as PythonType, UnaryOp,
};
use crate::type_mapper::PrimitiveType;
use depyler_annotations::TranspilationAnnotations;
use smallvec::smallvec;
#[test]
fn test_lifetime_generation() {
let mut inference = LifetimeInference::new();
assert_eq!(inference.next_lifetime(), "'a");
assert_eq!(inference.next_lifetime(), "'b");
assert_eq!(inference.next_lifetime(), "'c");
assert_eq!(inference.next_lifetime(), "'l1");
}
#[test]
fn test_parameter_usage_analysis() {
let mut inference = LifetimeInference::new();
let _type_mapper = crate::type_mapper::TypeMapper::new();
let func = HirFunction {
name: "test".to_string(),
params: smallvec![HirParam::new("x".to_string(), PythonType::String)],
ret_type: PythonType::String,
body: vec![HirStmt::Return(Some(HirExpr::Var("x".to_string())))],
properties: FunctionProperties::default(),
annotations: TranspilationAnnotations::default(),
docstring: None,
};
inference.analyze_parameter_usage(&func);
let usage = inference.param_analysis.get("x").unwrap();
assert!(usage.is_read_only);
assert!(usage.escapes);
assert!(!usage.is_mutated);
}
#[test]
fn test_lifetime_inference() {
let mut inference = LifetimeInference::new();
let type_mapper = crate::type_mapper::TypeMapper::new();
let func = HirFunction {
name: "get_len".to_string(),
params: smallvec![HirParam::new("s".to_string(), PythonType::String)],
ret_type: PythonType::Int,
body: vec![HirStmt::Return(Some(HirExpr::Attribute {
value: Box::new(HirExpr::Var("s".to_string())),
attr: "len".to_string(),
}))],
properties: FunctionProperties::default(),
annotations: TranspilationAnnotations::default(),
docstring: None,
};
let result = inference.analyze_function(&func, &type_mapper);
let s_param = result.param_lifetimes.get("s").unwrap();
assert!(s_param.should_borrow);
assert!(!s_param.needs_mut);
assert!(s_param.lifetime.is_some());
}
#[test]
fn test_elision_rules() {
let mut inference = LifetimeInference::new();
let type_mapper = crate::type_mapper::TypeMapper::new();
let func = HirFunction {
name: "identity".to_string(),
params: smallvec![HirParam::new("x".to_string(), PythonType::String)],
ret_type: PythonType::String,
body: vec![],
properties: FunctionProperties::default(),
annotations: TranspilationAnnotations::default(),
docstring: None,
};
let elision_result = inference.apply_elision_rules(&func, &type_mapper);
assert!(elision_result.is_some());
if let Some(result) = elision_result {
assert!(result.lifetime_params.is_empty());
}
}
#[test]
fn test_mutable_parameter_detection() {
let mut inference = LifetimeInference::new();
let type_mapper = crate::type_mapper::TypeMapper::new();
let func = HirFunction {
name: "append_bang".to_string(),
params: smallvec![HirParam::new("s".to_string(), PythonType::String)],
ret_type: PythonType::None,
body: vec![HirStmt::Assign {
target: AssignTarget::Symbol(Symbol::from("s")),
value: HirExpr::Binary {
op: BinOp::Add,
left: Box::new(HirExpr::Var("s".to_string())),
right: Box::new(HirExpr::Literal(Literal::String("!".to_string()))),
},
type_annotation: None,
}],
properties: FunctionProperties::default(),
annotations: TranspilationAnnotations::default(),
docstring: None,
};
let result = inference.analyze_function(&func, &type_mapper);
let s_param = result.param_lifetimes.get("s").unwrap();
assert!(!s_param.should_borrow); assert!(!s_param.needs_mut); }
#[test]
fn test_lifetime_info_construction() {
let info = LifetimeInfo {
name: "'a".to_string(),
is_static: false,
outlives: HashSet::new(),
source: LifetimeSource::Parameter("x".to_string()),
};
assert_eq!(info.name, "'a");
assert!(!info.is_static);
assert!(info.outlives.is_empty());
}
#[test]
fn test_lifetime_info_static() {
let info = LifetimeInfo {
name: "'static".to_string(),
is_static: true,
outlives: HashSet::new(),
source: LifetimeSource::StaticLiteral,
};
assert!(info.is_static);
}
#[test]
fn test_lifetime_info_with_outlives() {
let mut outlives = HashSet::new();
outlives.insert("'b".to_string());
outlives.insert("'c".to_string());
let info = LifetimeInfo {
name: "'a".to_string(),
is_static: false,
outlives,
source: LifetimeSource::Return,
};
assert_eq!(info.outlives.len(), 2);
assert!(info.outlives.contains("'b"));
}
#[test]
fn test_lifetime_source_variants() {
let param_src = LifetimeSource::Parameter("x".to_string());
let static_src = LifetimeSource::StaticLiteral;
let local_src = LifetimeSource::Local;
let return_src = LifetimeSource::Return;
let field_src = LifetimeSource::Field("name".to_string());
assert_eq!(param_src, LifetimeSource::Parameter("x".to_string()));
assert_eq!(static_src, LifetimeSource::StaticLiteral);
assert_eq!(local_src, LifetimeSource::Local);
assert_eq!(return_src, LifetimeSource::Return);
assert_eq!(field_src, LifetimeSource::Field("name".to_string()));
}
#[test]
fn test_lifetime_source_ne() {
let src1 = LifetimeSource::Parameter("x".to_string());
let src2 = LifetimeSource::Parameter("y".to_string());
assert_ne!(src1, src2);
}
#[test]
fn test_param_usage_default() {
let usage = ParamUsage::default();
assert!(!usage.is_mutated);
assert!(!usage.is_moved);
assert!(!usage.escapes);
assert!(!usage.is_read_only);
assert!(!usage.used_in_loop);
assert!(!usage.has_nested_borrows);
}
#[test]
fn test_param_usage_all_true() {
let usage = ParamUsage {
is_mutated: true,
is_moved: true,
escapes: true,
is_read_only: true,
used_in_loop: true,
has_nested_borrows: true,
};
assert!(usage.is_mutated);
assert!(usage.is_moved);
assert!(usage.escapes);
}
#[test]
fn test_lifetime_constraint_variants() {
let outlives = LifetimeConstraint::Outlives;
let equal = LifetimeConstraint::Equal;
let at_least = LifetimeConstraint::AtLeast;
assert!(matches!(outlives, LifetimeConstraint::Outlives));
assert!(matches!(equal, LifetimeConstraint::Equal));
assert!(matches!(at_least, LifetimeConstraint::AtLeast));
}
#[test]
fn test_lifetime_result_empty() {
let result = LifetimeResult {
param_lifetimes: IndexMap::new(),
return_lifetime: None,
lifetime_params: vec![],
lifetime_bounds: vec![],
borrowing_strategies: IndexMap::new(),
};
assert!(result.param_lifetimes.is_empty());
assert!(result.return_lifetime.is_none());
assert!(result.lifetime_params.is_empty());
}
#[test]
fn test_lifetime_result_with_data() {
let mut param_lifetimes = IndexMap::new();
param_lifetimes.insert(
"x".to_string(),
InferredParam {
should_borrow: true,
needs_mut: false,
lifetime: Some("'a".to_string()),
rust_type: RustType::String,
},
);
let result = LifetimeResult {
param_lifetimes,
return_lifetime: Some("'a".to_string()),
lifetime_params: vec!["'a".to_string()],
lifetime_bounds: vec![("'a".to_string(), "'b".to_string())],
borrowing_strategies: IndexMap::new(),
};
assert_eq!(result.param_lifetimes.len(), 1);
assert_eq!(result.return_lifetime, Some("'a".to_string()));
assert_eq!(result.lifetime_bounds.len(), 1);
}
#[test]
fn test_inferred_param_construction() {
let param = InferredParam {
should_borrow: true,
needs_mut: true,
lifetime: Some("'a".to_string()),
rust_type: RustType::String,
};
assert!(param.should_borrow);
assert!(param.needs_mut);
assert_eq!(param.lifetime, Some("'a".to_string()));
}
#[test]
fn test_inferred_param_owned() {
let param = InferredParam {
should_borrow: false,
needs_mut: false,
lifetime: None,
rust_type: RustType::Primitive(PrimitiveType::I64),
};
assert!(!param.should_borrow);
assert!(param.lifetime.is_none());
}
#[test]
fn test_lifetime_inference_default() {
let inference = LifetimeInference::default();
assert_eq!(inference.lifetime_counter, 0);
assert!(inference.variable_lifetimes.is_empty());
}
#[test]
fn test_add_constraint() {
let mut inference = LifetimeInference::new();
inference.add_constraint("'a", "'b", LifetimeConstraint::Outlives);
inference.add_constraint("'a", "'c", LifetimeConstraint::Equal);
let constraints = inference.lifetime_constraints.get("'a").unwrap();
assert!(constraints.contains("'b"));
assert!(constraints.contains("'c"));
}
#[test]
fn test_compute_lifetime_bounds() {
let mut inference = LifetimeInference::new();
inference.add_constraint("'a", "'b", LifetimeConstraint::Outlives);
inference.add_constraint("'c", "'return", LifetimeConstraint::Outlives);
let bounds = inference.compute_lifetime_bounds();
assert!(bounds.iter().any(|(f, t)| f == "'a" && t == "'b"));
assert!(!bounds.iter().any(|(_, t)| t == "'return"));
}
#[test]
fn test_return_type_needs_lifetime_str() {
let inference = LifetimeInference::new();
let str_type = RustType::Str { lifetime: None };
assert!(inference.return_type_needs_lifetime(&str_type));
}
#[test]
fn test_return_type_needs_lifetime_reference() {
let inference = LifetimeInference::new();
let ref_type = RustType::Reference {
lifetime: None,
inner: Box::new(RustType::Primitive(PrimitiveType::I64)),
mutable: false,
};
assert!(inference.return_type_needs_lifetime(&ref_type));
}
#[test]
fn test_return_type_needs_lifetime_cow() {
let inference = LifetimeInference::new();
let cow_type = RustType::Cow {
lifetime: "'a".to_string(),
};
assert!(inference.return_type_needs_lifetime(&cow_type));
}
#[test]
fn test_return_type_needs_lifetime_vec_str() {
let inference = LifetimeInference::new();
let vec_str = RustType::Vec(Box::new(RustType::Str { lifetime: None }));
assert!(inference.return_type_needs_lifetime(&vec_str));
}
#[test]
fn test_return_type_needs_lifetime_option_i64() {
let inference = LifetimeInference::new();
let opt_i64 = RustType::Option(Box::new(RustType::Primitive(PrimitiveType::I64)));
assert!(!inference.return_type_needs_lifetime(&opt_i64));
}
#[test]
fn test_return_type_needs_lifetime_result() {
let inference = LifetimeInference::new();
let result_type = RustType::Result(
Box::new(RustType::Str { lifetime: None }),
Box::new(RustType::String),
);
assert!(inference.return_type_needs_lifetime(&result_type));
}
#[test]
fn test_return_type_needs_lifetime_tuple() {
let inference = LifetimeInference::new();
let tuple = RustType::Tuple(vec![
RustType::Primitive(PrimitiveType::I64),
RustType::Str { lifetime: None },
]);
assert!(inference.return_type_needs_lifetime(&tuple));
}
#[test]
fn test_return_type_needs_lifetime_simple() {
let inference = LifetimeInference::new();
assert!(!inference.return_type_needs_lifetime(&RustType::Primitive(PrimitiveType::I64)));
assert!(!inference.return_type_needs_lifetime(&RustType::Primitive(PrimitiveType::F64)));
assert!(!inference.return_type_needs_lifetime(&RustType::Primitive(PrimitiveType::Bool)));
assert!(!inference.return_type_needs_lifetime(&RustType::Unit));
}
#[test]
fn test_is_reference_type() {
let inference = LifetimeInference::new();
let str_type = RustType::Str { lifetime: None };
assert!(inference.is_reference_type(&str_type));
let ref_type = RustType::Reference {
lifetime: None,
inner: Box::new(RustType::Primitive(PrimitiveType::I64)),
mutable: false,
};
assert!(inference.is_reference_type(&ref_type));
let cow_type = RustType::Cow {
lifetime: "'a".to_string(),
};
assert!(inference.is_reference_type(&cow_type));
assert!(!inference.is_reference_type(&RustType::String));
assert!(!inference.is_reference_type(&RustType::Primitive(PrimitiveType::I64)));
}
#[test]
fn test_analyze_stmt_for_param_assign() {
let inference = LifetimeInference::new();
let mut usage = ParamUsage::default();
let stmt = HirStmt::Assign {
target: AssignTarget::Symbol(Symbol::from("x")),
value: HirExpr::Literal(Literal::Int(42)),
type_annotation: None,
};
inference.analyze_stmt_for_param("x", &stmt, &mut usage, false);
assert!(usage.is_mutated);
}
#[test]
fn test_analyze_stmt_for_param_if() {
let inference = LifetimeInference::new();
let mut usage = ParamUsage::default();
let stmt = HirStmt::If {
condition: HirExpr::Var("x".to_string()),
then_body: vec![HirStmt::Return(Some(HirExpr::Var("x".to_string())))],
else_body: Some(vec![HirStmt::Pass]),
};
inference.analyze_stmt_for_param("x", &stmt, &mut usage, false);
assert!(usage.is_read_only);
assert!(usage.escapes);
}
#[test]
fn test_analyze_stmt_for_param_while() {
let inference = LifetimeInference::new();
let mut usage = ParamUsage::default();
let stmt = HirStmt::While {
condition: HirExpr::Var("x".to_string()),
body: vec![HirStmt::Expr(HirExpr::Var("x".to_string()))],
};
inference.analyze_stmt_for_param("x", &stmt, &mut usage, false);
assert!(usage.used_in_loop);
}
#[test]
fn test_analyze_stmt_for_param_for() {
let inference = LifetimeInference::new();
let mut usage = ParamUsage::default();
let stmt = HirStmt::For {
target: AssignTarget::Symbol(Symbol::from("i")),
iter: HirExpr::Var("items".to_string()),
body: vec![HirStmt::Pass],
};
inference.analyze_stmt_for_param("items", &stmt, &mut usage, false);
assert!(usage.used_in_loop);
}
#[test]
fn test_analyze_stmt_for_param_break_continue_pass() {
let inference = LifetimeInference::new();
let mut usage = ParamUsage::default();
inference.analyze_stmt_for_param("x", &HirStmt::Break { label: None }, &mut usage, false);
inference.analyze_stmt_for_param(
"x",
&HirStmt::Continue { label: None },
&mut usage,
false,
);
inference.analyze_stmt_for_param("x", &HirStmt::Pass, &mut usage, false);
assert!(!usage.is_read_only);
assert!(!usage.is_mutated);
}
#[test]
fn test_analyze_stmt_for_param_block() {
let inference = LifetimeInference::new();
let mut usage = ParamUsage::default();
let stmt = HirStmt::Block(vec![
HirStmt::Expr(HirExpr::Var("x".to_string())),
HirStmt::Return(Some(HirExpr::Var("x".to_string()))),
]);
inference.analyze_stmt_for_param("x", &stmt, &mut usage, false);
assert!(usage.is_read_only);
assert!(usage.escapes);
}
#[test]
fn test_analyze_stmt_for_param_assert() {
let inference = LifetimeInference::new();
let mut usage = ParamUsage::default();
let stmt = HirStmt::Assert {
test: HirExpr::Var("x".to_string()),
msg: Some(HirExpr::Literal(Literal::String("error".to_string()))),
};
inference.analyze_stmt_for_param("x", &stmt, &mut usage, false);
assert!(usage.is_read_only);
}
#[test]
fn test_analyze_stmt_for_param_raise() {
let inference = LifetimeInference::new();
let mut usage = ParamUsage::default();
let stmt = HirStmt::Raise {
exception: Some(HirExpr::Var("x".to_string())),
cause: None,
};
inference.analyze_stmt_for_param("x", &stmt, &mut usage, false);
assert!(usage.is_read_only);
}
#[test]
fn test_analyze_expr_for_param_attribute() {
let inference = LifetimeInference::new();
let mut usage = ParamUsage::default();
let expr = HirExpr::Attribute {
value: Box::new(HirExpr::Var("obj".to_string())),
attr: "field".to_string(),
};
inference.analyze_expr_for_param("obj", &expr, &mut usage, false, false);
assert!(usage.is_read_only);
assert!(usage.has_nested_borrows);
}
#[test]
fn test_analyze_expr_for_param_index() {
let inference = LifetimeInference::new();
let mut usage = ParamUsage::default();
let expr = HirExpr::Index {
base: Box::new(HirExpr::Var("arr".to_string())),
index: Box::new(HirExpr::Literal(Literal::Int(0))),
};
inference.analyze_expr_for_param("arr", &expr, &mut usage, false, false);
assert!(usage.is_read_only);
}
#[test]
fn test_analyze_expr_for_param_call() {
let inference = LifetimeInference::new();
let mut usage = ParamUsage::default();
let expr = HirExpr::Call {
func: Symbol::from("func"),
args: vec![HirExpr::Var("x".to_string())],
kwargs: vec![],
};
inference.analyze_expr_for_param("x", &expr, &mut usage, false, false);
assert!(usage.is_moved);
}
#[test]
fn test_analyze_expr_for_param_list() {
let inference = LifetimeInference::new();
let mut usage = ParamUsage::default();
let expr = HirExpr::List(vec![
HirExpr::Var("x".to_string()),
HirExpr::Literal(Literal::Int(1)),
]);
inference.analyze_expr_for_param("x", &expr, &mut usage, false, true);
assert!(usage.is_read_only);
assert!(usage.escapes);
}
#[test]
fn test_analyze_expr_for_param_dict() {
let inference = LifetimeInference::new();
let mut usage = ParamUsage::default();
let expr = HirExpr::Dict(vec![(
HirExpr::Literal(Literal::String("key".to_string())),
HirExpr::Var("x".to_string()),
)]);
inference.analyze_expr_for_param("x", &expr, &mut usage, false, true);
assert!(usage.escapes);
}
#[test]
fn test_analyze_expr_for_param_binary() {
let inference = LifetimeInference::new();
let mut usage = ParamUsage::default();
let expr = HirExpr::Binary {
op: BinOp::Add,
left: Box::new(HirExpr::Var("x".to_string())),
right: Box::new(HirExpr::Literal(Literal::Int(1))),
};
inference.analyze_expr_for_param("x", &expr, &mut usage, false, false);
assert!(usage.is_read_only);
}
#[test]
fn test_analyze_expr_for_param_unary() {
let inference = LifetimeInference::new();
let mut usage = ParamUsage::default();
let expr = HirExpr::Unary {
op: UnaryOp::Not,
operand: Box::new(HirExpr::Var("x".to_string())),
};
inference.analyze_expr_for_param("x", &expr, &mut usage, false, false);
assert!(usage.is_read_only);
}
#[test]
fn test_analyze_expr_for_param_method_call() {
let inference = LifetimeInference::new();
let mut usage = ParamUsage::default();
let expr = HirExpr::MethodCall {
object: Box::new(HirExpr::Var("obj".to_string())),
method: "method".to_string(),
args: vec![HirExpr::Var("arg".to_string())],
kwargs: vec![],
};
inference.analyze_expr_for_param("obj", &expr, &mut usage, false, false);
assert!(usage.is_read_only);
}
#[test]
fn test_analyze_expr_for_param_slice() {
let inference = LifetimeInference::new();
let mut usage = ParamUsage::default();
let expr = HirExpr::Slice {
base: Box::new(HirExpr::Var("arr".to_string())),
start: Some(Box::new(HirExpr::Var("start".to_string()))),
stop: Some(Box::new(HirExpr::Var("stop".to_string()))),
step: None,
};
inference.analyze_expr_for_param("arr", &expr, &mut usage, false, false);
assert!(usage.is_read_only);
}
#[test]
fn test_analyze_expr_for_param_borrow() {
let inference = LifetimeInference::new();
let mut usage = ParamUsage::default();
let expr = HirExpr::Borrow {
expr: Box::new(HirExpr::Var("x".to_string())),
mutable: false,
};
inference.analyze_expr_for_param("x", &expr, &mut usage, false, false);
assert!(usage.is_read_only);
}
#[test]
fn test_analyze_expr_for_param_list_comp() {
let inference = LifetimeInference::new();
let mut usage = ParamUsage::default();
let expr = HirExpr::ListComp {
element: Box::new(HirExpr::Var("x".to_string())),
generators: vec![HirComprehension {
target: Symbol::from("i"),
iter: Box::new(HirExpr::Var("items".to_string())),
conditions: vec![],
}],
};
inference.analyze_expr_for_param("items", &expr, &mut usage, false, false);
assert!(usage.used_in_loop);
}
#[test]
fn test_analyze_expr_for_param_set() {
let inference = LifetimeInference::new();
let mut usage = ParamUsage::default();
let expr = HirExpr::Set(vec![HirExpr::Var("x".to_string())]);
inference.analyze_expr_for_param("x", &expr, &mut usage, false, true);
assert!(usage.escapes);
}
#[test]
fn test_analyze_expr_for_param_if_expr() {
let inference = LifetimeInference::new();
let mut usage = ParamUsage::default();
let expr = HirExpr::IfExpr {
test: Box::new(HirExpr::Var("cond".to_string())),
body: Box::new(HirExpr::Var("x".to_string())),
orelse: Box::new(HirExpr::Literal(Literal::Int(0))),
};
inference.analyze_expr_for_param("x", &expr, &mut usage, false, true);
assert!(usage.escapes);
}
#[test]
fn test_analyze_expr_for_param_lambda() {
let inference = LifetimeInference::new();
let mut usage = ParamUsage::default();
let expr = HirExpr::Lambda {
params: vec![],
body: Box::new(HirExpr::Var("x".to_string())),
};
inference.analyze_expr_for_param("x", &expr, &mut usage, false, false);
assert!(usage.is_read_only);
}
#[test]
fn test_analyze_expr_for_param_await() {
let inference = LifetimeInference::new();
let mut usage = ParamUsage::default();
let expr = HirExpr::Await {
value: Box::new(HirExpr::Var("future".to_string())),
};
inference.analyze_expr_for_param("future", &expr, &mut usage, false, true);
assert!(usage.escapes);
}
#[test]
fn test_analyze_expr_for_param_yield() {
let inference = LifetimeInference::new();
let mut usage = ParamUsage::default();
let expr = HirExpr::Yield {
value: Some(Box::new(HirExpr::Var("val".to_string()))),
};
inference.analyze_expr_for_param("val", &expr, &mut usage, false, true);
assert!(usage.escapes);
}
#[test]
fn test_inferred_param_clone() {
let param = InferredParam {
should_borrow: true,
needs_mut: false,
lifetime: Some("'a".to_string()),
rust_type: RustType::String,
};
let cloned = param.clone();
assert_eq!(param.should_borrow, cloned.should_borrow);
assert_eq!(param.lifetime, cloned.lifetime);
}
#[test]
fn test_lifetime_result_clone() {
let result = LifetimeResult {
param_lifetimes: IndexMap::new(),
return_lifetime: Some("'a".to_string()),
lifetime_params: vec!["'a".to_string()],
lifetime_bounds: vec![],
borrowing_strategies: IndexMap::new(),
};
let cloned = result.clone();
assert_eq!(result.return_lifetime, cloned.return_lifetime);
}
#[test]
fn test_lifetime_info_clone() {
let info = LifetimeInfo {
name: "'a".to_string(),
is_static: false,
outlives: HashSet::new(),
source: LifetimeSource::Local,
};
let cloned = info.clone();
assert_eq!(info.name, cloned.name);
}
#[test]
fn test_param_usage_clone() {
let usage = ParamUsage {
is_mutated: true,
is_moved: false,
escapes: true,
is_read_only: false,
used_in_loop: true,
has_nested_borrows: false,
};
let cloned = usage.clone();
assert_eq!(usage.is_mutated, cloned.is_mutated);
assert_eq!(usage.escapes, cloned.escapes);
}
#[test]
fn test_lifetime_constraint_clone() {
let constraint = LifetimeConstraint::Outlives;
let _cloned = constraint.clone();
}
#[test]
fn test_lifetime_source_clone() {
let src = LifetimeSource::Field("name".to_string());
let cloned = src.clone();
assert_eq!(src, cloned);
}
#[test]
fn test_multiple_lifetime_generation() {
let mut inference = LifetimeInference::new();
let lifetimes: Vec<String> = (0..10).map(|_| inference.next_lifetime()).collect();
assert_eq!(lifetimes[0], "'a");
assert_eq!(lifetimes[1], "'b");
assert_eq!(lifetimes[2], "'c");
assert_eq!(lifetimes[3], "'l1");
assert_eq!(lifetimes[9], "'l7");
}
#[test]
fn test_analyze_function_with_no_params() {
let mut inference = LifetimeInference::new();
let type_mapper = crate::type_mapper::TypeMapper::new();
let func = HirFunction {
name: "no_params".to_string(),
params: smallvec![],
ret_type: PythonType::Int,
body: vec![HirStmt::Return(Some(HirExpr::Literal(Literal::Int(42))))],
properties: FunctionProperties::default(),
annotations: TranspilationAnnotations::default(),
docstring: None,
};
let result = inference.analyze_function(&func, &type_mapper);
assert!(result.param_lifetimes.is_empty());
}
#[test]
fn test_elision_with_no_references() {
let mut inference = LifetimeInference::new();
let type_mapper = crate::type_mapper::TypeMapper::new();
let func = HirFunction {
name: "add".to_string(),
params: smallvec![
HirParam::new("a".to_string(), PythonType::Int),
HirParam::new("b".to_string(), PythonType::Int),
],
ret_type: PythonType::Int,
body: vec![],
properties: FunctionProperties::default(),
annotations: TranspilationAnnotations::default(),
docstring: None,
};
let result = inference.apply_elision_rules(&func, &type_mapper).unwrap();
assert!(result.lifetime_params.is_empty());
assert!(result.return_lifetime.is_none());
}
}