use crate::hir::{BinOp, HirExpr, HirFunction, HirProgram, HirStmt, Type};
use colored::Colorize;
pub struct PerformanceAnalyzer {
warnings: Vec<PerformanceWarning>,
config: PerformanceConfig,
current_loop_depth: usize,
}
#[derive(Debug, Clone)]
pub struct PerformanceConfig {
pub warn_string_concat: bool,
pub warn_allocations: bool,
pub warn_algorithms: bool,
pub warn_repeated_computation: bool,
pub max_loop_depth: usize,
pub quadratic_threshold: usize,
}
impl Default for PerformanceConfig {
fn default() -> Self {
Self {
warn_string_concat: true,
warn_allocations: true,
warn_algorithms: true,
warn_repeated_computation: true,
max_loop_depth: 3,
quadratic_threshold: 100,
}
}
}
#[derive(Debug, Clone)]
pub struct PerformanceWarning {
pub category: WarningCategory,
pub severity: WarningSeverity,
pub message: String,
pub explanation: String,
pub suggestion: String,
pub impact: PerformanceImpact,
pub location: Option<Location>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WarningCategory {
StringPerformance,
MemoryAllocation,
AlgorithmComplexity,
RedundantComputation,
IoPerformance,
CollectionUsage,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum WarningSeverity {
Low,
Medium,
High,
Critical,
}
#[derive(Debug, Clone)]
pub struct PerformanceImpact {
pub complexity: String,
pub scales_with_input: bool,
pub in_hot_path: bool,
}
#[derive(Debug, Clone)]
pub struct Location {
pub function: String,
pub line: usize,
pub in_loop: bool,
pub loop_depth: usize,
}
impl PerformanceAnalyzer {
pub fn new(config: PerformanceConfig) -> Self {
Self {
warnings: Vec::new(),
config,
current_loop_depth: 0,
}
}
pub fn analyze_program(&mut self, program: &HirProgram) -> Vec<PerformanceWarning> {
self.warnings.clear();
for func in &program.functions {
self.analyze_function(func);
}
self.warnings.sort_by(|a, b| b.severity.cmp(&a.severity));
self.warnings.clone()
}
fn analyze_function(&mut self, func: &HirFunction) {
self.current_loop_depth = 0;
self.check_function_level_issues(func);
for (idx, stmt) in func.body.iter().enumerate() {
self.analyze_stmt(stmt, func, idx);
}
}
fn check_function_level_issues(&mut self, func: &HirFunction) {
for param in &func.params {
if self.is_large_type(¶m.ty) && !self.is_reference_type(¶m.ty) {
self.add_warning(PerformanceWarning {
category: WarningCategory::MemoryAllocation,
severity: WarningSeverity::Medium,
message: format!("Large value '{}' passed by copy", param.name),
explanation: "Passing large values by copy is inefficient".to_string(),
suggestion:
"Consider passing by reference (&) or using Box/Arc for large types"
.to_string(),
impact: PerformanceImpact {
complexity: "O(n)".to_string(),
scales_with_input: true,
in_hot_path: false,
},
location: Some(Location {
function: func.name.clone(),
line: 0,
in_loop: false,
loop_depth: 0,
}),
});
}
}
}
fn analyze_stmt(&mut self, stmt: &HirStmt, func: &HirFunction, line: usize) {
match stmt {
HirStmt::For {
target: _,
iter,
body,
} => {
self.current_loop_depth += 1;
if self.current_loop_depth > self.config.max_loop_depth {
self.add_warning(PerformanceWarning {
category: WarningCategory::AlgorithmComplexity,
severity: WarningSeverity::High,
message: format!("Deeply nested loops (depth: {})", self.current_loop_depth),
explanation: "Deeply nested loops can lead to exponential time complexity".to_string(),
suggestion: "Consider refactoring to reduce nesting or use more efficient algorithms".to_string(),
impact: PerformanceImpact {
complexity: format!("O(n^{})", self.current_loop_depth),
scales_with_input: true,
in_hot_path: true,
},
location: Some(Location {
function: func.name.clone(),
line,
in_loop: true,
loop_depth: self.current_loop_depth,
}),
});
}
self.check_iteration_pattern(iter, func, line);
for inner_stmt in body {
self.analyze_stmt(inner_stmt, func, line);
}
self.current_loop_depth -= 1;
}
HirStmt::While { condition: _, body } => {
self.current_loop_depth += 1;
for inner_stmt in body {
self.analyze_stmt(inner_stmt, func, line);
}
self.current_loop_depth -= 1;
}
HirStmt::Assign { target: _, value } => {
self.analyze_expr(value, func, line);
if self.current_loop_depth > 0 && self.is_string_concatenation(value) {
self.add_warning(PerformanceWarning {
category: WarningCategory::StringPerformance,
severity: WarningSeverity::High,
message: "String concatenation in loop".to_string(),
explanation:
"String concatenation in loops creates many intermediate strings"
.to_string(),
suggestion:
"Use String::with_capacity() and push_str(), or collect into a String"
.to_string(),
impact: PerformanceImpact {
complexity: "O(n²)".to_string(),
scales_with_input: true,
in_hot_path: true,
},
location: Some(Location {
function: func.name.clone(),
line,
in_loop: true,
loop_depth: self.current_loop_depth,
}),
});
}
}
HirStmt::Expr(expr) => {
self.analyze_expr(expr, func, line);
}
_ => {}
}
}
fn analyze_expr(&mut self, expr: &HirExpr, func: &HirFunction, line: usize) {
match expr {
HirExpr::Binary { left, right, op } => {
self.analyze_expr(left, func, line);
self.analyze_expr(right, func, line);
if matches!(op, BinOp::Pow) && self.current_loop_depth > 0 {
self.add_warning(PerformanceWarning {
category: WarningCategory::RedundantComputation,
severity: WarningSeverity::Medium,
message: "Power operation in loop".to_string(),
explanation: "Power operations are computationally expensive".to_string(),
suggestion: "Consider caching the result if the value doesn't change"
.to_string(),
impact: PerformanceImpact {
complexity: "O(log n) per operation".to_string(),
scales_with_input: false,
in_hot_path: true,
},
location: Some(Location {
function: func.name.clone(),
line,
in_loop: true,
loop_depth: self.current_loop_depth,
}),
});
}
}
HirExpr::Call { func: fname, args } => {
if self.current_loop_depth > 0 && self.is_expensive_function(fname) {
self.add_warning(PerformanceWarning {
category: WarningCategory::RedundantComputation,
severity: WarningSeverity::Medium,
message: format!("Expensive function '{}' called in loop", fname),
explanation:
"Calling expensive functions repeatedly can impact performance"
.to_string(),
suggestion: "Cache the result if the inputs don't change".to_string(),
impact: PerformanceImpact {
complexity: "Depends on function".to_string(),
scales_with_input: true,
in_hot_path: true,
},
location: Some(Location {
function: func.name.clone(),
line,
in_loop: true,
loop_depth: self.current_loop_depth,
}),
});
}
self.check_function_call_patterns(fname, args, func, line);
for arg in args {
self.analyze_expr(arg, func, line);
}
}
HirExpr::MethodCall {
object,
method,
args,
} => {
self.analyze_expr(object, func, line);
self.check_method_patterns(object, method, args, func, line);
for arg in args {
self.analyze_expr(arg, func, line);
}
}
HirExpr::List(items) => {
if self.current_loop_depth > 0 && items.len() > 10 {
self.add_warning(PerformanceWarning {
category: WarningCategory::MemoryAllocation,
severity: WarningSeverity::Medium,
message: "Large list created in loop".to_string(),
explanation:
"Creating large collections in loops causes repeated allocations"
.to_string(),
suggestion:
"Move the list creation outside the loop or use a pre-allocated buffer"
.to_string(),
impact: PerformanceImpact {
complexity: "O(n) allocations".to_string(),
scales_with_input: true,
in_hot_path: true,
},
location: Some(Location {
function: func.name.clone(),
line,
in_loop: true,
loop_depth: self.current_loop_depth,
}),
});
}
for item in items {
self.analyze_expr(item, func, line);
}
}
_ => {}
}
}
fn check_iteration_pattern(&mut self, iter: &HirExpr, func: &HirFunction, line: usize) {
if let HirExpr::Call { func: fname, args } = iter {
if fname == "range" && !args.is_empty() {
if let HirExpr::Call {
func: inner_func, ..
} = &args[0]
{
if inner_func == "len" {
self.add_warning(PerformanceWarning {
category: WarningCategory::CollectionUsage,
severity: WarningSeverity::Low,
message: "Using range(len(x)) instead of enumerate".to_string(),
explanation: "This pattern is less efficient and less idiomatic"
.to_string(),
suggestion: "Use enumerate() to get both index and value".to_string(),
impact: PerformanceImpact {
complexity: "O(1) overhead".to_string(),
scales_with_input: false,
in_hot_path: true,
},
location: Some(Location {
function: func.name.clone(),
line,
in_loop: false,
loop_depth: self.current_loop_depth,
}),
});
}
}
}
}
}
fn check_function_call_patterns(
&mut self,
fname: &str,
args: &[HirExpr],
func: &HirFunction,
line: usize,
) {
if fname == "sorted" && self.current_loop_depth > 0 {
self.add_warning(PerformanceWarning {
category: WarningCategory::AlgorithmComplexity,
severity: WarningSeverity::High,
message: "Sorting inside a loop".to_string(),
explanation: "Sorting has O(n log n) complexity and shouldn't be repeated"
.to_string(),
suggestion: "Sort once before the loop or maintain sorted order".to_string(),
impact: PerformanceImpact {
complexity: "O(n² log n)".to_string(),
scales_with_input: true,
in_hot_path: true,
},
location: Some(Location {
function: func.name.clone(),
line,
in_loop: true,
loop_depth: self.current_loop_depth,
}),
});
}
if ["sum", "min", "max"].contains(&fname) && !args.is_empty() {
if self.current_loop_depth > 1 {
self.add_warning(PerformanceWarning {
category: WarningCategory::RedundantComputation,
severity: WarningSeverity::Medium,
message: format!("Aggregate function '{}' in nested loop", fname),
explanation: "Computing aggregates repeatedly is inefficient".to_string(),
suggestion: "Compute once and cache the result".to_string(),
impact: PerformanceImpact {
complexity: "O(n) per call".to_string(),
scales_with_input: true,
in_hot_path: true,
},
location: Some(Location {
function: func.name.clone(),
line,
in_loop: true,
loop_depth: self.current_loop_depth,
}),
});
}
}
}
fn check_method_patterns(
&mut self,
_object: &HirExpr,
method: &str,
_args: &[HirExpr],
func: &HirFunction,
line: usize,
) {
if self.current_loop_depth > 0 {
match method {
"append" => {
self.add_warning(PerformanceWarning {
category: WarningCategory::CollectionUsage,
severity: WarningSeverity::Low,
message: "Multiple append calls in loop".to_string(),
explanation: "Multiple append operations can be less efficient than extend"
.to_string(),
suggestion: "Consider collecting items and using extend() once".to_string(),
impact: PerformanceImpact {
complexity: "O(1) amortized, but more calls".to_string(),
scales_with_input: true,
in_hot_path: true,
},
location: Some(Location {
function: func.name.clone(),
line,
in_loop: true,
loop_depth: self.current_loop_depth,
}),
});
}
"remove" if self.current_loop_depth > 1 => {
self.add_warning(PerformanceWarning {
category: WarningCategory::AlgorithmComplexity,
severity: WarningSeverity::Critical,
message: "List remove() in nested loop".to_string(),
explanation: "remove() is O(n) and in nested loops becomes O(n²) or worse"
.to_string(),
suggestion: "Use a set for O(1) removal or filter to create a new list"
.to_string(),
impact: PerformanceImpact {
complexity: format!("O(n^{})", self.current_loop_depth + 1),
scales_with_input: true,
in_hot_path: true,
},
location: Some(Location {
function: func.name.clone(),
line,
in_loop: true,
loop_depth: self.current_loop_depth,
}),
});
}
"index" | "count" if self.current_loop_depth > 0 => {
self.add_warning(PerformanceWarning {
category: WarningCategory::AlgorithmComplexity,
severity: WarningSeverity::Medium,
message: format!("Linear search method '{}' in loop", method),
explanation: "Linear search in loops can lead to quadratic complexity"
.to_string(),
suggestion: "Consider using a HashMap/HashSet for O(1) lookups".to_string(),
impact: PerformanceImpact {
complexity: "O(n²)".to_string(),
scales_with_input: true,
in_hot_path: true,
},
location: Some(Location {
function: func.name.clone(),
line,
in_loop: true,
loop_depth: self.current_loop_depth,
}),
});
}
_ => {}
}
}
}
fn is_large_type(&self, ty: &Type) -> bool {
match ty {
Type::List(_) | Type::Dict(_, _) | Type::String => true,
Type::Custom(name) => {
!["i32", "i64", "f32", "f64", "bool", "char"].contains(&name.as_str())
}
_ => false,
}
}
fn is_reference_type(&self, _ty: &Type) -> bool {
false
}
fn is_string_concatenation(&self, expr: &HirExpr) -> bool {
if let HirExpr::Binary {
op: BinOp::Add,
left,
right,
} = expr
{
matches!(left.as_ref(), HirExpr::Var(_)) || matches!(right.as_ref(), HirExpr::Var(_))
} else {
false
}
}
fn is_expensive_function(&self, fname: &str) -> bool {
let expensive = [
"sorted", "sort", "reverse", "compile", "eval", "exec", "deepcopy", "copy", "hash",
"checksum",
];
expensive.contains(&fname)
}
fn add_warning(&mut self, warning: PerformanceWarning) {
self.warnings.push(warning);
}
pub fn format_warnings(&self, warnings: &[PerformanceWarning]) -> String {
let mut output = String::new();
if warnings.is_empty() {
output.push_str(&"✅ No performance warnings found!\n".green().to_string());
return output;
}
output.push_str(&format!("\n{}\n", "Performance Warnings".bold().yellow()));
output.push_str(&format!("{}\n\n", "═".repeat(50)));
for (idx, warning) in warnings.iter().enumerate() {
let severity_color = match warning.severity {
WarningSeverity::Critical => "red",
WarningSeverity::High => "bright red",
WarningSeverity::Medium => "yellow",
WarningSeverity::Low => "bright yellow",
};
output.push_str(&format!(
"{} {} {}\n",
format!("[{}]", idx + 1).dimmed(),
format!("[{:?}]", warning.severity)
.color(severity_color)
.bold(),
warning.message.bold()
));
if let Some(loc) = &warning.location {
let loop_info = if loc.in_loop {
format!(" (in loop, depth: {})", loc.loop_depth)
.red()
.to_string()
} else {
String::new()
};
output.push_str(&format!(
" {} {}, line {}{}\n",
"Location:".dimmed(),
loc.function,
loc.line,
loop_info
));
}
output.push_str(&format!(
" {} Complexity: {}, Scales: {}, Hot path: {}\n",
"Impact:".dimmed(),
warning.impact.complexity.yellow(),
if warning.impact.scales_with_input {
"Yes".red()
} else {
"No".green()
},
if warning.impact.in_hot_path {
"Yes".red()
} else {
"No".green()
}
));
output.push_str(&format!(" {} {}\n", "Why:".dimmed(), warning.explanation));
output.push_str(&format!(
" {} {}\n",
"Fix:".green(),
warning.suggestion.green()
));
output.push('\n');
}
let critical = warnings
.iter()
.filter(|w| w.severity == WarningSeverity::Critical)
.count();
let high = warnings
.iter()
.filter(|w| w.severity == WarningSeverity::High)
.count();
output.push_str(&format!(
"{} Found {} warnings ({} critical, {} high severity)\n",
"Summary:".bold(),
warnings.len(),
critical,
high
));
if critical > 0 || high > 0 {
output.push_str(
&"⚠️ Address critical and high severity warnings for better performance\n"
.red()
.to_string(),
);
}
output
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::hir::*;
use smallvec::smallvec;
fn create_test_function(name: &str, body: Vec<HirStmt>) -> HirFunction {
HirFunction {
name: name.to_string(),
params: smallvec![],
ret_type: Type::Unknown,
body,
properties: FunctionProperties::default(),
annotations: Default::default(),
docstring: None,
}
}
#[test]
fn test_performance_config_default() {
let config = PerformanceConfig::default();
assert!(config.warn_string_concat);
assert!(config.warn_allocations);
assert_eq!(config.max_loop_depth, 3);
}
#[test]
fn test_severity_ordering() {
assert!(WarningSeverity::Critical > WarningSeverity::High);
assert!(WarningSeverity::High > WarningSeverity::Medium);
assert!(WarningSeverity::Medium > WarningSeverity::Low);
}
#[test]
fn test_string_concat_in_loop_detection() {
let body = vec![HirStmt::For {
target: "i".to_string(),
iter: HirExpr::Call {
func: "range".to_string(),
args: vec![HirExpr::Literal(Literal::Int(10))],
},
body: vec![HirStmt::Assign {
target: AssignTarget::Symbol("s".to_string()),
value: HirExpr::Binary {
op: BinOp::Add,
left: Box::new(HirExpr::Var("s".to_string())),
right: Box::new(HirExpr::Var("i".to_string())),
},
}],
}];
let func = create_test_function("test", body);
let program = HirProgram {
functions: vec![func],
classes: vec![],
imports: vec![],
};
let mut analyzer = PerformanceAnalyzer::new(PerformanceConfig::default());
let warnings = analyzer.analyze_program(&program);
assert!(!warnings.is_empty());
assert!(warnings
.iter()
.any(|w| w.category == WarningCategory::StringPerformance));
}
#[test]
fn test_nested_loop_detection() {
let inner_loop = HirStmt::For {
target: "j".to_string(),
iter: HirExpr::Var("items".to_string()),
body: vec![HirStmt::Expr(HirExpr::Var("j".to_string()))],
};
let body = vec![HirStmt::For {
target: "i".to_string(),
iter: HirExpr::Var("items".to_string()),
body: vec![inner_loop],
}];
let func = create_test_function("test", body);
let program = HirProgram {
functions: vec![func],
classes: vec![],
imports: vec![],
};
let mut analyzer = PerformanceAnalyzer::new(PerformanceConfig {
max_loop_depth: 2,
..Default::default()
});
let warnings = analyzer.analyze_program(&program);
assert!(warnings.is_empty()); }
#[test]
fn test_expensive_function_in_loop() {
let body = vec![HirStmt::For {
target: "item".to_string(),
iter: HirExpr::Var("items".to_string()),
body: vec![HirStmt::Assign {
target: AssignTarget::Symbol("s".to_string()),
value: HirExpr::Call {
func: "sorted".to_string(),
args: vec![HirExpr::Var("data".to_string())],
},
}],
}];
let func = create_test_function("test", body);
let program = HirProgram {
functions: vec![func],
classes: vec![],
imports: vec![],
};
let mut analyzer = PerformanceAnalyzer::new(PerformanceConfig::default());
let warnings = analyzer.analyze_program(&program);
assert!(!warnings.is_empty());
assert!(warnings.iter().any(|w| {
w.category == WarningCategory::AlgorithmComplexity
|| w.category == WarningCategory::RedundantComputation
}));
}
}