use depyler_hir::hir::{AssignTarget, HirExpr, HirFunction, HirStmt, Type};
use std::collections::HashSet;
#[derive(Debug, Default)]
pub struct BorrowingContext {
mutated_params: HashSet<String>,
escaping_params: HashSet<String>,
read_only_params: HashSet<String>,
loop_used_params: HashSet<String>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum BorrowingPattern {
Owned,
Borrowed,
MutableBorrow,
}
impl BorrowingContext {
pub fn new() -> Self {
Self::default()
}
pub fn analyze_function(&mut self, func: &HirFunction) {
for param in &func.params {
self.read_only_params.insert(param.name.clone());
}
for stmt in &func.body {
self.analyze_stmt(stmt);
}
for param in &self.mutated_params {
self.read_only_params.remove(param);
}
for param in &self.escaping_params {
self.read_only_params.remove(param);
}
}
pub fn get_pattern(&self, param_name: &str, param_type: &Type) -> BorrowingPattern {
if self.escaping_params.contains(param_name) {
BorrowingPattern::Owned
} else if self.mutated_params.contains(param_name) {
BorrowingPattern::MutableBorrow
} else if self.is_copyable(param_type) {
BorrowingPattern::Owned
} else {
BorrowingPattern::Borrowed
}
}
pub fn generate_param_signature(&self, param_name: &str, param_type: &Type) -> String {
let pattern = self.get_pattern(param_name, param_type);
let type_str = self.type_to_rust_string(param_type);
match pattern {
BorrowingPattern::Owned => format!("{}: {}", param_name, type_str),
BorrowingPattern::Borrowed => format!("{}: &{}", param_name, type_str),
BorrowingPattern::MutableBorrow => format!("{}: &mut {}", param_name, type_str),
}
}
fn analyze_stmt(&mut self, stmt: &HirStmt) {
match stmt {
HirStmt::Assign { target, value, .. } => self.analyze_assign(target, value),
HirStmt::Return(Some(expr)) => self.analyze_return(expr),
HirStmt::Expr(expr) => self.analyze_expr(expr),
HirStmt::If {
condition,
then_body,
else_body,
} => self.analyze_if(condition, then_body, else_body),
HirStmt::While { condition, body } => self.analyze_while(condition, body),
HirStmt::For {
target: _,
iter,
body,
} => self.analyze_for(iter, body),
_ => {}
}
}
fn analyze_assign(&mut self, target: &AssignTarget, value: &HirExpr) {
if let AssignTarget::Symbol(symbol) = target {
if self.read_only_params.contains(symbol) {
self.mutated_params.insert(symbol.clone());
}
}
self.check_escaping_expr(value);
self.analyze_expr(value);
}
fn analyze_return(&mut self, expr: &HirExpr) {
self.check_escaping_expr(expr);
self.analyze_expr(expr);
}
fn analyze_if(
&mut self,
condition: &HirExpr,
then_body: &[HirStmt],
else_body: &Option<Vec<HirStmt>>,
) {
self.analyze_expr(condition);
for stmt in then_body {
self.analyze_stmt(stmt);
}
if let Some(else_stmts) = else_body {
for stmt in else_stmts {
self.analyze_stmt(stmt);
}
}
}
fn analyze_while(&mut self, condition: &HirExpr, body: &[HirStmt]) {
self.analyze_expr(condition);
self.mark_loop_params(body);
for stmt in body {
self.analyze_stmt(stmt);
}
}
fn analyze_for(&mut self, iter: &HirExpr, body: &[HirStmt]) {
self.analyze_expr(iter);
self.mark_loop_params(body);
for stmt in body {
self.analyze_stmt(stmt);
}
}
#[allow(clippy::only_used_in_recursion)]
fn analyze_expr(&mut self, expr: &HirExpr) {
match expr {
HirExpr::Binary { op: _, left, right } => self.analyze_binary(left, right),
HirExpr::Unary { op: _, operand } => self.analyze_expr(operand),
HirExpr::Call { func: _, args, .. } => self.analyze_call(args),
HirExpr::List(elts) | HirExpr::Tuple(elts) => self.analyze_collection(elts),
HirExpr::Dict(items) => self.analyze_dict(items),
HirExpr::Index { base, index } => self.analyze_index(base, index),
_ => {}
}
}
fn analyze_binary(&mut self, left: &HirExpr, right: &HirExpr) {
self.analyze_expr(left);
self.analyze_expr(right);
}
fn analyze_call(&mut self, args: &[HirExpr]) {
for arg in args {
self.analyze_expr(arg);
}
}
fn analyze_collection(&mut self, elts: &[HirExpr]) {
for elt in elts {
self.analyze_expr(elt);
}
}
fn analyze_dict(&mut self, items: &[(HirExpr, HirExpr)]) {
for (k, v) in items {
self.analyze_expr(k);
self.analyze_expr(v);
}
}
fn analyze_index(&mut self, base: &HirExpr, index: &HirExpr) {
self.analyze_expr(base);
self.analyze_expr(index);
}
fn check_escaping_expr(&mut self, expr: &HirExpr) {
match expr {
HirExpr::Var(name) => {
self.escaping_params.insert(name.clone());
}
HirExpr::List(elts) | HirExpr::Tuple(elts) => {
for elt in elts {
if let HirExpr::Var(name) = elt {
self.escaping_params.insert(name.clone());
}
}
}
_ => {}
}
}
fn mark_loop_params(&mut self, body: &[HirStmt]) {
for stmt in body {
self.find_params_in_stmt(stmt);
}
}
fn find_params_in_stmt(&mut self, stmt: &HirStmt) {
match stmt {
HirStmt::Expr(expr) => self.find_params_in_expr(expr),
HirStmt::Assign { value, .. } => self.find_params_in_expr(value),
_ => {}
}
}
fn find_params_in_expr(&mut self, expr: &HirExpr) {
match expr {
HirExpr::Var(name) => {
if self.read_only_params.contains(name)
|| self.mutated_params.contains(name)
|| self.escaping_params.contains(name)
{
self.loop_used_params.insert(name.clone());
}
}
HirExpr::Binary { left, right, .. } => {
self.find_params_in_expr(left);
self.find_params_in_expr(right);
}
HirExpr::Unary { operand, .. } => {
self.find_params_in_expr(operand);
}
HirExpr::Call { args, .. } => {
for arg in args {
self.find_params_in_expr(arg);
}
}
HirExpr::List(elts) | HirExpr::Tuple(elts) | HirExpr::Set(elts) => {
for elt in elts {
self.find_params_in_expr(elt);
}
}
HirExpr::Dict(items) => {
for (key, value) in items {
self.find_params_in_expr(key);
self.find_params_in_expr(value);
}
}
HirExpr::Index { base, index } => {
self.find_params_in_expr(base);
self.find_params_in_expr(index);
}
_ => {}
}
}
fn is_copyable(&self, ty: &Type) -> bool {
matches!(ty, Type::Int | Type::Float | Type::Bool | Type::None)
}
#[allow(clippy::only_used_in_recursion)]
fn type_to_rust_string(&self, ty: &Type) -> String {
match ty {
Type::Unknown | Type::Int | Type::Float | Type::String | Type::Bool | Type::None => {
self.primitive_type_to_rust(ty)
}
Type::List(_) | Type::Dict(_, _) | Type::Set(_) | Type::Array { .. } => {
self.collection_type_to_rust(ty)
}
Type::Tuple(types) => self.tuple_type_to_rust(types),
Type::Optional(inner) => self.optional_type_to_rust(inner),
Type::Custom(name) | Type::TypeVar(name) => name.clone(),
Type::Generic { base, .. } => base.clone(),
Type::Function { .. } => "/* function */".to_string(),
Type::Union(_) => "Union".to_string(),
Type::Final(inner) => self.type_to_rust_string(inner), Type::UnificationVar(id) => {
panic!("BUG: UnificationVar({}) encountered during code generation. Type inference did not complete.", id)
}
}
}
fn primitive_type_to_rust(&self, ty: &Type) -> String {
match ty {
Type::Unknown => "serde_json::Value".to_string(),
Type::Int => "i32".to_string(),
Type::Float => "f64".to_string(),
Type::String => "String".to_string(),
Type::Bool => "bool".to_string(),
Type::None => "()".to_string(),
_ => unreachable!("primitive_type_to_rust called with non-primitive type"),
}
}
fn collection_type_to_rust(&self, ty: &Type) -> String {
match ty {
Type::List(inner) => self.list_type_to_rust(inner),
Type::Dict(k, v) => self.dict_type_to_rust(k, v),
Type::Set(element) => self.set_type_to_rust(element),
Type::Array { element_type, .. } => self.array_type_to_rust(element_type),
_ => unreachable!("collection_type_to_rust called with non-collection type"),
}
}
fn list_type_to_rust(&self, inner: &Type) -> String {
format!("Vec<{}>", self.type_to_rust_string(inner))
}
fn set_type_to_rust(&self, element: &Type) -> String {
format!("HashSet<{}>", self.type_to_rust_string(element))
}
fn array_type_to_rust(&self, element_type: &Type) -> String {
format!("Array<{}>", self.type_to_rust_string(element_type))
}
fn dict_type_to_rust(&self, k: &Type, v: &Type) -> String {
format!(
"HashMap<{}, {}>",
self.type_to_rust_string(k),
self.type_to_rust_string(v)
)
}
fn tuple_type_to_rust(&self, types: &[Type]) -> String {
if types.is_empty() {
"()".to_string()
} else {
let type_strs: Vec<String> =
types.iter().map(|t| self.type_to_rust_string(t)).collect();
format!("({})", type_strs.join(", "))
}
}
fn optional_type_to_rust(&self, inner: &Type) -> String {
format!("Option<{}>", self.type_to_rust_string(inner))
}
}
#[cfg(test)]
mod tests {
use super::*;
use depyler_hir::hir::{BinOp, FunctionProperties, HirParam, Literal};
use depyler_annotations::TranspilationAnnotations;
use smallvec::smallvec;
#[test]
fn test_read_only_parameter() {
let mut ctx = BorrowingContext::new();
let func = HirFunction {
name: "test".to_string(),
params: smallvec![HirParam::new("x".to_string(), Type::String)],
ret_type: Type::Int,
body: vec![HirStmt::Return(Some(HirExpr::Call {
func: "len".to_string(),
args: vec![HirExpr::Var("x".to_string())],
kwargs: vec![],
}))],
properties: FunctionProperties::default(),
annotations: TranspilationAnnotations::default(),
docstring: None,
};
ctx.analyze_function(&func);
assert_eq!(
ctx.get_pattern("x", &Type::String),
BorrowingPattern::Borrowed
);
}
#[test]
fn test_mutated_parameter() {
let mut ctx = BorrowingContext::new();
let func = HirFunction {
name: "test".to_string(),
params: smallvec![HirParam::new(
"x".to_string(),
Type::List(Box::new(Type::Int))
)],
ret_type: Type::None,
body: vec![HirStmt::Expr(HirExpr::Call {
func: "append".to_string(),
args: vec![
HirExpr::Var("x".to_string()),
HirExpr::Literal(Literal::Int(42)),
],
kwargs: vec![],
})],
properties: FunctionProperties::default(),
annotations: TranspilationAnnotations::default(),
docstring: None,
};
ctx.analyze_function(&func);
}
#[test]
fn test_escaping_parameter() {
let mut ctx = BorrowingContext::new();
let func = HirFunction {
name: "test".to_string(),
params: smallvec![HirParam::new("x".to_string(), Type::String)],
ret_type: Type::String,
body: vec![HirStmt::Return(Some(HirExpr::Var("x".to_string())))],
properties: FunctionProperties::default(),
annotations: TranspilationAnnotations::default(),
docstring: None,
};
ctx.analyze_function(&func);
assert_eq!(ctx.get_pattern("x", &Type::String), BorrowingPattern::Owned);
}
#[test]
fn test_copyable_parameter() {
let mut ctx = BorrowingContext::new();
let func = HirFunction {
name: "test".to_string(),
params: smallvec![HirParam::new("x".to_string(), Type::Int)],
ret_type: Type::Int,
body: vec![HirStmt::Return(Some(HirExpr::Binary {
op: BinOp::Add,
left: Box::new(HirExpr::Var("x".to_string())),
right: Box::new(HirExpr::Literal(Literal::Int(1))),
}))],
properties: FunctionProperties::default(),
annotations: TranspilationAnnotations::default(),
docstring: None,
};
ctx.analyze_function(&func);
assert_eq!(ctx.get_pattern("x", &Type::Int), BorrowingPattern::Owned);
}
#[test]
fn test_generate_param_signature() {
let ctx = BorrowingContext::new();
let mut ctx_borrow = BorrowingContext::new();
ctx_borrow.read_only_params.insert("s".to_string());
assert_eq!(
ctx_borrow.generate_param_signature("s", &Type::String),
"s: &String"
);
assert_eq!(ctx.generate_param_signature("n", &Type::Int), "n: i32");
}
#[test]
fn test_analyze_stmt_assign_mutation() {
let mut ctx = BorrowingContext::new();
ctx.read_only_params.insert("x".to_string());
let stmt = HirStmt::Assign {
target: depyler_hir::hir::AssignTarget::Symbol("x".to_string()),
value: HirExpr::Binary {
op: BinOp::Add,
left: Box::new(HirExpr::Var("x".to_string())),
right: Box::new(HirExpr::Literal(Literal::Int(1))),
},
type_annotation: None,
};
ctx.analyze_stmt(&stmt);
assert!(
ctx.mutated_params.contains("x"),
"Parameter x should be marked as mutated after assignment"
);
}
#[test]
fn test_analyze_stmt_return_escaping() {
let mut ctx = BorrowingContext::new();
ctx.read_only_params.insert("data".to_string());
let stmt = HirStmt::Return(Some(HirExpr::Var("data".to_string())));
ctx.analyze_stmt(&stmt);
assert!(
ctx.escaping_params.contains("data"),
"Parameter data should be marked as escaping when returned"
);
}
#[test]
fn test_check_escaping_direct_var() {
let mut ctx = BorrowingContext::new();
let expr = HirExpr::Var("result".to_string());
ctx.check_escaping_expr(&expr);
assert!(
ctx.escaping_params.contains("result"),
"Direct variable return should mark parameter as escaping"
);
}
#[test]
fn test_analyze_if_branches() {
let mut ctx = BorrowingContext::new();
ctx.read_only_params.insert("x".to_string());
ctx.read_only_params.insert("y".to_string());
let condition = HirExpr::Binary {
op: BinOp::Gt,
left: Box::new(HirExpr::Var("x".to_string())),
right: Box::new(HirExpr::Literal(Literal::Int(0))),
};
let then_body = vec![HirStmt::Assign {
target: depyler_hir::hir::AssignTarget::Symbol("y".to_string()),
value: HirExpr::Binary {
op: BinOp::Add,
left: Box::new(HirExpr::Var("y".to_string())),
right: Box::new(HirExpr::Literal(Literal::Int(1))),
},
type_annotation: None,
}];
let else_body = vec![];
let stmt = HirStmt::If {
condition,
then_body,
else_body: Some(else_body),
};
ctx.analyze_stmt(&stmt);
assert!(
ctx.read_only_params.contains("x"),
"Condition parameter should remain read-only"
);
assert!(
ctx.mutated_params.contains("y"),
"Parameter mutated in if branch should be tracked"
);
}
#[test]
fn test_analyze_while_loop() {
let mut ctx = BorrowingContext::new();
ctx.read_only_params.insert("limit".to_string());
ctx.read_only_params.insert("counter".to_string());
let condition = HirExpr::Binary {
op: BinOp::Lt,
left: Box::new(HirExpr::Var("counter".to_string())),
right: Box::new(HirExpr::Var("limit".to_string())),
};
let body = vec![HirStmt::Assign {
target: depyler_hir::hir::AssignTarget::Symbol("counter".to_string()),
value: HirExpr::Binary {
op: BinOp::Add,
left: Box::new(HirExpr::Var("counter".to_string())),
right: Box::new(HirExpr::Literal(Literal::Int(1))),
},
type_annotation: None,
}];
let stmt = HirStmt::While { condition, body };
ctx.analyze_stmt(&stmt);
assert!(
ctx.loop_used_params.contains("limit") || ctx.read_only_params.contains("limit"),
"Loop condition param should be tracked"
);
assert!(
ctx.mutated_params.contains("counter"),
"Loop-mutated param should be tracked"
);
}
#[test]
fn test_analyze_for_loop() {
let mut ctx = BorrowingContext::new();
ctx.read_only_params.insert("items".to_string());
ctx.read_only_params.insert("total".to_string());
let iter = HirExpr::Var("items".to_string());
let body = vec![HirStmt::Assign {
target: depyler_hir::hir::AssignTarget::Symbol("total".to_string()),
value: HirExpr::Binary {
op: BinOp::Add,
left: Box::new(HirExpr::Var("total".to_string())),
right: Box::new(HirExpr::Var("item".to_string())),
},
type_annotation: None,
}];
let stmt = HirStmt::For {
target: depyler_hir::hir::AssignTarget::Symbol("item".to_string()),
iter,
body,
};
ctx.analyze_stmt(&stmt);
assert!(
ctx.loop_used_params.contains("items") || ctx.read_only_params.contains("items"),
"For loop iterator param should be tracked"
);
assert!(
ctx.mutated_params.contains("total"),
"Loop-mutated param should be tracked"
);
}
#[test]
fn test_is_copyable_primitives() {
let ctx = BorrowingContext::new();
assert!(ctx.is_copyable(&Type::Int), "Int should be copyable");
assert!(ctx.is_copyable(&Type::Float), "Float should be copyable");
assert!(ctx.is_copyable(&Type::Bool), "Bool should be copyable");
assert!(ctx.is_copyable(&Type::None), "None should be copyable");
assert!(
!ctx.is_copyable(&Type::String),
"String should NOT be copyable"
);
assert!(
!ctx.is_copyable(&Type::List(Box::new(Type::Int))),
"List should NOT be copyable"
);
assert!(
!ctx.is_copyable(&Type::Dict(Box::new(Type::String), Box::new(Type::Int))),
"Dict should NOT be copyable"
);
}
#[test]
fn test_analyze_binary_expression() {
let mut ctx = BorrowingContext::new();
ctx.read_only_params.insert("x".to_string());
ctx.read_only_params.insert("y".to_string());
let expr = HirExpr::Binary {
op: BinOp::Add,
left: Box::new(HirExpr::Var("x".to_string())),
right: Box::new(HirExpr::Var("y".to_string())),
};
ctx.analyze_expr(&expr);
assert!(
ctx.read_only_params.contains("x"),
"Binary operand x should be tracked"
);
assert!(
ctx.read_only_params.contains("y"),
"Binary operand y should be tracked"
);
}
#[test]
fn test_analyze_unary_expression() {
let mut ctx = BorrowingContext::new();
ctx.read_only_params.insert("value".to_string());
let expr = HirExpr::Unary {
op: depyler_hir::hir::UnaryOp::Neg,
operand: Box::new(HirExpr::Var("value".to_string())),
};
ctx.analyze_expr(&expr);
assert!(
ctx.read_only_params.contains("value"),
"Unary operand should be tracked"
);
}
#[test]
fn test_analyze_call_arguments() {
let mut ctx = BorrowingContext::new();
ctx.read_only_params.insert("a".to_string());
ctx.read_only_params.insert("b".to_string());
let expr = HirExpr::Call {
func: "func".to_string(),
args: vec![HirExpr::Var("a".to_string()), HirExpr::Var("b".to_string())],
kwargs: vec![],
};
ctx.analyze_expr(&expr);
assert!(
ctx.read_only_params.contains("a"),
"Call argument a should be tracked"
);
assert!(
ctx.read_only_params.contains("b"),
"Call argument b should be tracked"
);
}
#[test]
fn test_analyze_list_collection() {
let mut ctx = BorrowingContext::new();
ctx.read_only_params.insert("item1".to_string());
ctx.read_only_params.insert("item2".to_string());
let expr = HirExpr::List(vec![
HirExpr::Var("item1".to_string()),
HirExpr::Var("item2".to_string()),
]);
ctx.analyze_expr(&expr);
assert!(
ctx.read_only_params.contains("item1"),
"List element item1 should be tracked"
);
assert!(
ctx.read_only_params.contains("item2"),
"List element item2 should be tracked"
);
}
#[test]
fn test_analyze_dict_items() {
let mut ctx = BorrowingContext::new();
ctx.read_only_params.insert("key1".to_string());
ctx.read_only_params.insert("val1".to_string());
let expr = HirExpr::Dict(vec![(
HirExpr::Var("key1".to_string()),
HirExpr::Var("val1".to_string()),
)]);
ctx.analyze_expr(&expr);
assert!(
ctx.read_only_params.contains("key1"),
"Dict key should be tracked"
);
assert!(
ctx.read_only_params.contains("val1"),
"Dict value should be tracked"
);
}
#[test]
fn test_analyze_index_expression() {
let mut ctx = BorrowingContext::new();
ctx.read_only_params.insert("arr".to_string());
ctx.read_only_params.insert("idx".to_string());
let expr = HirExpr::Index {
base: Box::new(HirExpr::Var("arr".to_string())),
index: Box::new(HirExpr::Var("idx".to_string())),
};
ctx.analyze_expr(&expr);
assert!(
ctx.read_only_params.contains("arr"),
"Index base should be tracked"
);
assert!(
ctx.read_only_params.contains("idx"),
"Index subscript should be tracked"
);
}
#[test]
fn test_check_escaping_tuple_elements() {
let mut ctx = BorrowingContext::new();
let expr = HirExpr::Tuple(vec![
HirExpr::Var("x".to_string()),
HirExpr::Var("y".to_string()),
]);
ctx.check_escaping_expr(&expr);
assert!(
ctx.escaping_params.contains("x"),
"Tuple element x should be marked as escaping"
);
assert!(
ctx.escaping_params.contains("y"),
"Tuple element y should be marked as escaping"
);
}
#[test]
fn test_mark_loop_params_tracking() {
let mut ctx = BorrowingContext::new();
ctx.read_only_params.insert("counter".to_string());
let body = vec![HirStmt::Expr(HirExpr::Binary {
op: BinOp::Add,
left: Box::new(HirExpr::Var("counter".to_string())),
right: Box::new(HirExpr::Literal(Literal::Int(1))),
})];
ctx.mark_loop_params(&body);
assert!(
ctx.loop_used_params.contains("counter"),
"Parameter used in loop should be tracked"
);
}
#[test]
fn test_primitive_type_conversions() {
let ctx = BorrowingContext::new();
assert_eq!(
ctx.primitive_type_to_rust(&Type::Unknown),
"serde_json::Value"
);
assert_eq!(ctx.primitive_type_to_rust(&Type::Int), "i32");
assert_eq!(ctx.primitive_type_to_rust(&Type::Float), "f64");
assert_eq!(ctx.primitive_type_to_rust(&Type::String), "String");
assert_eq!(ctx.primitive_type_to_rust(&Type::Bool), "bool");
assert_eq!(ctx.primitive_type_to_rust(&Type::None), "()");
}
#[test]
fn test_list_type_conversion() {
let ctx = BorrowingContext::new();
let list_type = Type::List(Box::new(Type::Int));
let result = ctx.type_to_rust_string(&list_type);
assert_eq!(result, "Vec<i32>", "List of int should map to Vec<i32>");
}
#[test]
fn test_dict_type_conversion() {
let ctx = BorrowingContext::new();
let dict_type = Type::Dict(Box::new(Type::String), Box::new(Type::Int));
let result = ctx.type_to_rust_string(&dict_type);
assert_eq!(
result, "HashMap<String, i32>",
"Dict[str, int] should map to HashMap<String, i32>"
);
}
#[test]
fn test_set_type_conversion() {
let ctx = BorrowingContext::new();
let set_type = Type::Set(Box::new(Type::String));
let result = ctx.type_to_rust_string(&set_type);
assert_eq!(
result, "HashSet<String>",
"Set[str] should map to HashSet<String>"
);
}
#[test]
fn test_array_type_conversion() {
let ctx = BorrowingContext::new();
let array_type = Type::Array {
element_type: Box::new(Type::Float),
size: depyler_hir::hir::ConstGeneric::Literal(10),
};
let result = ctx.type_to_rust_string(&array_type);
assert_eq!(
result, "Array<f64>",
"Array of float should map to Array<f64>"
);
}
#[test]
fn test_tuple_type_conversion() {
let ctx = BorrowingContext::new();
let empty_tuple = Type::Tuple(vec![]);
assert_eq!(
ctx.type_to_rust_string(&empty_tuple),
"()",
"Empty tuple should map to unit ()"
);
let tuple_type = Type::Tuple(vec![Type::Int, Type::String, Type::Bool]);
let result = ctx.type_to_rust_string(&tuple_type);
assert_eq!(
result, "(i32, String, bool)",
"Tuple[int, str, bool] should map to (i32, String, bool)"
);
}
#[test]
fn test_optional_type_conversion() {
let ctx = BorrowingContext::new();
let optional_type = Type::Optional(Box::new(Type::Int));
let result = ctx.type_to_rust_string(&optional_type);
assert_eq!(
result, "Option<i32>",
"Optional[int] should map to Option<i32>"
);
}
#[test]
fn test_complex_type_conversions() {
let ctx = BorrowingContext::new();
let custom = Type::Custom("MyClass".to_string());
assert_eq!(
ctx.type_to_rust_string(&custom),
"MyClass",
"Custom type should preserve name"
);
let typevar = Type::TypeVar("T".to_string());
assert_eq!(
ctx.type_to_rust_string(&typevar),
"T",
"TypeVar should preserve name"
);
let generic = Type::Generic {
base: "Vec".to_string(),
params: vec![Type::Int],
};
assert_eq!(
ctx.type_to_rust_string(&generic),
"Vec",
"Generic type should use base name"
);
let function = Type::Function {
params: vec![Type::Int],
ret: Box::new(Type::String),
};
assert_eq!(
ctx.type_to_rust_string(&function),
"/* function */",
"Function type should return comment"
);
let union = Type::Union(vec![Type::Int, Type::String]);
assert_eq!(
ctx.type_to_rust_string(&union),
"Union",
"Union type should return 'Union'"
);
}
#[test]
fn test_borrowing_context_default() {
let ctx: BorrowingContext = Default::default();
assert!(ctx.mutated_params.is_empty());
assert!(ctx.escaping_params.is_empty());
assert!(ctx.read_only_params.is_empty());
assert!(ctx.loop_used_params.is_empty());
}
#[test]
fn test_borrowing_context_debug() {
let ctx = BorrowingContext::new();
let debug = format!("{:?}", ctx);
assert!(debug.contains("BorrowingContext"));
assert!(debug.contains("mutated_params"));
assert!(debug.contains("escaping_params"));
}
#[test]
fn test_borrowing_pattern_debug() {
assert!(format!("{:?}", BorrowingPattern::Owned).contains("Owned"));
assert!(format!("{:?}", BorrowingPattern::Borrowed).contains("Borrowed"));
assert!(format!("{:?}", BorrowingPattern::MutableBorrow).contains("MutableBorrow"));
}
#[test]
fn test_borrowing_pattern_clone() {
let owned = BorrowingPattern::Owned;
let cloned = owned.clone();
assert_eq!(cloned, BorrowingPattern::Owned);
let borrowed = BorrowingPattern::Borrowed;
let cloned2 = borrowed.clone();
assert_eq!(cloned2, BorrowingPattern::Borrowed);
}
#[test]
fn test_borrowing_pattern_partial_eq() {
assert_eq!(BorrowingPattern::Owned, BorrowingPattern::Owned);
assert_eq!(BorrowingPattern::Borrowed, BorrowingPattern::Borrowed);
assert_eq!(
BorrowingPattern::MutableBorrow,
BorrowingPattern::MutableBorrow
);
assert_ne!(BorrowingPattern::Owned, BorrowingPattern::Borrowed);
assert_ne!(BorrowingPattern::Borrowed, BorrowingPattern::MutableBorrow);
assert_ne!(BorrowingPattern::MutableBorrow, BorrowingPattern::Owned);
}
#[test]
fn test_analyze_stmt_expression() {
let mut ctx = BorrowingContext::new();
ctx.read_only_params.insert("x".to_string());
let stmt = HirStmt::Expr(HirExpr::Var("x".to_string()));
ctx.analyze_stmt(&stmt);
assert!(ctx.read_only_params.contains("x"));
}
#[test]
fn test_analyze_stmt_return_none() {
let mut ctx = BorrowingContext::new();
let stmt = HirStmt::Return(None);
ctx.analyze_stmt(&stmt);
assert!(ctx.escaping_params.is_empty());
}
#[test]
fn test_analyze_if_no_else() {
let mut ctx = BorrowingContext::new();
ctx.read_only_params.insert("x".to_string());
let stmt = HirStmt::If {
condition: HirExpr::Var("x".to_string()),
then_body: vec![HirStmt::Return(Some(HirExpr::Literal(Literal::Int(1))))],
else_body: None,
};
ctx.analyze_stmt(&stmt);
assert!(ctx.read_only_params.contains("x"));
}
#[test]
fn test_find_params_in_expr_set() {
let mut ctx = BorrowingContext::new();
ctx.read_only_params.insert("item".to_string());
let expr = HirExpr::Set(vec![HirExpr::Var("item".to_string())]);
ctx.find_params_in_expr(&expr);
assert!(ctx.loop_used_params.contains("item"));
}
#[test]
fn test_find_params_in_stmt_assign() {
let mut ctx = BorrowingContext::new();
ctx.read_only_params.insert("value".to_string());
let stmt = HirStmt::Assign {
target: AssignTarget::Symbol("x".to_string()),
value: HirExpr::Var("value".to_string()),
type_annotation: None,
};
ctx.find_params_in_stmt(&stmt);
assert!(ctx.loop_used_params.contains("value"));
}
#[test]
fn test_check_escaping_expr_list() {
let mut ctx = BorrowingContext::new();
let expr = HirExpr::List(vec![
HirExpr::Var("a".to_string()),
HirExpr::Literal(Literal::Int(1)),
HirExpr::Var("b".to_string()),
]);
ctx.check_escaping_expr(&expr);
assert!(ctx.escaping_params.contains("a"));
assert!(ctx.escaping_params.contains("b"));
}
#[test]
fn test_check_escaping_expr_other() {
let mut ctx = BorrowingContext::new();
let expr = HirExpr::Binary {
op: BinOp::Add,
left: Box::new(HirExpr::Var("x".to_string())),
right: Box::new(HirExpr::Var("y".to_string())),
};
ctx.check_escaping_expr(&expr);
assert!(!ctx.escaping_params.contains("x"));
assert!(!ctx.escaping_params.contains("y"));
}
#[test]
fn test_generate_param_signature_mutable_borrow() {
let mut ctx = BorrowingContext::new();
ctx.mutated_params.insert("data".to_string());
let sig = ctx.generate_param_signature("data", &Type::List(Box::new(Type::Int)));
assert_eq!(sig, "data: &mut Vec<i32>");
}
#[test]
fn test_generate_param_signature_owned_escaping() {
let mut ctx = BorrowingContext::new();
ctx.escaping_params.insert("result".to_string());
let sig = ctx.generate_param_signature("result", &Type::String);
assert_eq!(sig, "result: String");
}
#[test]
fn test_nested_type_conversion() {
let ctx = BorrowingContext::new();
let nested_list = Type::List(Box::new(Type::List(Box::new(Type::Int))));
assert_eq!(ctx.type_to_rust_string(&nested_list), "Vec<Vec<i32>>");
let dict_with_list = Type::Dict(
Box::new(Type::String),
Box::new(Type::List(Box::new(Type::Int))),
);
assert_eq!(
ctx.type_to_rust_string(&dict_with_list),
"HashMap<String, Vec<i32>>"
);
}
#[test]
fn test_optional_nested_type() {
let ctx = BorrowingContext::new();
let optional_list = Type::Optional(Box::new(Type::List(Box::new(Type::String))));
assert_eq!(
ctx.type_to_rust_string(&optional_list),
"Option<Vec<String>>"
);
}
#[test]
fn test_final_type_unwrapping() {
let ctx = BorrowingContext::new();
let final_int = Type::Final(Box::new(Type::Int));
assert_eq!(ctx.type_to_rust_string(&final_int), "i32");
let final_list = Type::Final(Box::new(Type::List(Box::new(Type::String))));
assert_eq!(ctx.type_to_rust_string(&final_list), "Vec<String>");
}
#[test]
fn test_analyze_function_removes_read_only_for_mutated() {
let mut ctx = BorrowingContext::new();
let func = HirFunction {
name: "mutate".to_string(),
params: smallvec![HirParam::new("x".to_string(), Type::Int)],
ret_type: Type::None,
body: vec![HirStmt::Assign {
target: AssignTarget::Symbol("x".to_string()),
value: HirExpr::Literal(Literal::Int(1)),
type_annotation: None,
}],
properties: FunctionProperties::default(),
annotations: TranspilationAnnotations::default(),
docstring: None,
};
ctx.analyze_function(&func);
assert!(ctx.mutated_params.contains("x"));
assert!(!ctx.read_only_params.contains("x"));
}
#[test]
fn test_analyze_function_removes_read_only_for_escaping() {
let mut ctx = BorrowingContext::new();
let func = HirFunction {
name: "return_param".to_string(),
params: smallvec![HirParam::new("data".to_string(), Type::String)],
ret_type: Type::String,
body: vec![HirStmt::Return(Some(HirExpr::Var("data".to_string())))],
properties: FunctionProperties::default(),
annotations: TranspilationAnnotations::default(),
docstring: None,
};
ctx.analyze_function(&func);
assert!(ctx.escaping_params.contains("data"));
assert!(!ctx.read_only_params.contains("data"));
}
#[test]
fn test_tuple_expression_escaping() {
let mut ctx = BorrowingContext::new();
let expr = HirExpr::Tuple(vec![
HirExpr::Var("a".to_string()),
HirExpr::Var("b".to_string()),
]);
ctx.check_escaping_expr(&expr);
assert!(ctx.escaping_params.contains("a"));
assert!(ctx.escaping_params.contains("b"));
}
#[test]
fn test_analyze_tuple_expression() {
let mut ctx = BorrowingContext::new();
ctx.read_only_params.insert("x".to_string());
ctx.read_only_params.insert("y".to_string());
let expr = HirExpr::Tuple(vec![
HirExpr::Var("x".to_string()),
HirExpr::Var("y".to_string()),
]);
ctx.analyze_expr(&expr);
assert!(ctx.read_only_params.contains("x"));
assert!(ctx.read_only_params.contains("y"));
}
#[test]
fn test_find_params_nested_binary() {
let mut ctx = BorrowingContext::new();
ctx.read_only_params.insert("a".to_string());
ctx.read_only_params.insert("b".to_string());
ctx.read_only_params.insert("c".to_string());
let expr = HirExpr::Binary {
op: BinOp::Mul,
left: Box::new(HirExpr::Binary {
op: BinOp::Add,
left: Box::new(HirExpr::Var("a".to_string())),
right: Box::new(HirExpr::Var("b".to_string())),
}),
right: Box::new(HirExpr::Var("c".to_string())),
};
ctx.find_params_in_expr(&expr);
assert!(ctx.loop_used_params.contains("a"));
assert!(ctx.loop_used_params.contains("b"));
assert!(ctx.loop_used_params.contains("c"));
}
#[test]
fn test_find_params_call_args() {
let mut ctx = BorrowingContext::new();
ctx.read_only_params.insert("arg1".to_string());
ctx.read_only_params.insert("arg2".to_string());
let expr = HirExpr::Call {
func: "func".to_string(),
args: vec![
HirExpr::Var("arg1".to_string()),
HirExpr::Var("arg2".to_string()),
],
kwargs: vec![],
};
ctx.find_params_in_expr(&expr);
assert!(ctx.loop_used_params.contains("arg1"));
assert!(ctx.loop_used_params.contains("arg2"));
}
#[test]
fn test_find_params_dict() {
let mut ctx = BorrowingContext::new();
ctx.read_only_params.insert("key".to_string());
ctx.read_only_params.insert("val".to_string());
let expr = HirExpr::Dict(vec![(
HirExpr::Var("key".to_string()),
HirExpr::Var("val".to_string()),
)]);
ctx.find_params_in_expr(&expr);
assert!(ctx.loop_used_params.contains("key"));
assert!(ctx.loop_used_params.contains("val"));
}
#[test]
fn test_find_params_index() {
let mut ctx = BorrowingContext::new();
ctx.read_only_params.insert("arr".to_string());
ctx.read_only_params.insert("idx".to_string());
let expr = HirExpr::Index {
base: Box::new(HirExpr::Var("arr".to_string())),
index: Box::new(HirExpr::Var("idx".to_string())),
};
ctx.find_params_in_expr(&expr);
assert!(ctx.loop_used_params.contains("arr"));
assert!(ctx.loop_used_params.contains("idx"));
}
#[test]
fn test_find_params_unary() {
let mut ctx = BorrowingContext::new();
ctx.read_only_params.insert("x".to_string());
let expr = HirExpr::Unary {
op: depyler_hir::hir::UnaryOp::Not,
operand: Box::new(HirExpr::Var("x".to_string())),
};
ctx.find_params_in_expr(&expr);
assert!(ctx.loop_used_params.contains("x"));
}
#[test]
fn test_empty_function_analysis() {
let mut ctx = BorrowingContext::new();
let func = HirFunction {
name: "empty".to_string(),
params: smallvec![],
ret_type: Type::None,
body: vec![],
properties: FunctionProperties::default(),
annotations: TranspilationAnnotations::default(),
docstring: None,
};
ctx.analyze_function(&func);
assert!(ctx.read_only_params.is_empty());
assert!(ctx.mutated_params.is_empty());
assert!(ctx.escaping_params.is_empty());
}
#[test]
fn test_multiple_params_analysis() {
let mut ctx = BorrowingContext::new();
let func = HirFunction {
name: "multi".to_string(),
params: smallvec![
HirParam::new("a".to_string(), Type::Int),
HirParam::new("b".to_string(), Type::String),
HirParam::new("c".to_string(), Type::Bool),
],
ret_type: Type::String,
body: vec![HirStmt::Return(Some(HirExpr::Var("b".to_string())))],
properties: FunctionProperties::default(),
annotations: TranspilationAnnotations::default(),
docstring: None,
};
ctx.analyze_function(&func);
assert!(ctx.read_only_params.contains("a"));
assert!(ctx.read_only_params.contains("c"));
assert!(ctx.escaping_params.contains("b"));
assert!(!ctx.read_only_params.contains("b"));
}
}