Skip to main content

depyler_analysis/
inlining.rs

1/// Function inlining heuristics and implementation for the optimizer
2use depyler_hir::hir::{HirExpr, HirFunction, HirProgram, HirStmt};
3use std::collections::{HashMap, HashSet};
4
5/// Inlining analyzer that determines which functions should be inlined
6pub struct InliningAnalyzer {
7    /// Configuration for inlining decisions
8    config: InliningConfig,
9    /// Function call graph for dependency analysis
10    call_graph: CallGraph,
11    /// Metrics for each function
12    function_metrics: HashMap<String, FunctionMetrics>,
13}
14
15#[derive(Debug, Clone)]
16pub struct InliningConfig {
17    /// Maximum size (in HIR nodes) for a function to be inlined
18    pub max_inline_size: usize,
19    /// Maximum depth of inlining (to prevent infinite recursion)
20    pub max_inline_depth: usize,
21    /// Whether to inline functions called only once
22    pub inline_single_use: bool,
23    /// Whether to inline trivial functions (single expression)
24    pub inline_trivial: bool,
25    /// Cost threshold for inlining decision
26    pub cost_threshold: f64,
27    /// Whether to inline functions with loops
28    pub inline_loops: bool,
29}
30
31impl Default for InliningConfig {
32    fn default() -> Self {
33        Self {
34            max_inline_size: 20,
35            max_inline_depth: 3,
36            inline_single_use: true,
37            inline_trivial: true,
38            cost_threshold: 1.5,
39            inline_loops: false,
40        }
41    }
42}
43
44#[derive(Debug, Default)]
45struct CallGraph {
46    /// Map from function name to functions it calls
47    calls: HashMap<String, HashSet<String>>,
48    /// Map from function name to functions that call it
49    called_by: HashMap<String, HashSet<String>>,
50    /// Recursive functions
51    recursive: HashSet<String>,
52}
53
54#[derive(Debug, Clone)]
55struct FunctionMetrics {
56    /// Number of HIR nodes in the function
57    size: usize,
58    /// Number of parameters
59    _param_count: usize,
60    /// Number of return statements
61    _return_count: usize,
62    /// Contains loops
63    has_loops: bool,
64    /// Contains I/O or other side effects
65    has_side_effects: bool,
66    /// Is a trivial function (single return expression)
67    is_trivial: bool,
68    /// Number of times this function is called
69    call_count: usize,
70    /// Estimated execution cost
71    cost: f64,
72}
73
74/// Inlining decision for a specific call site
75#[derive(Debug, Clone)]
76pub struct InliningDecision {
77    pub should_inline: bool,
78    pub reason: InliningReason,
79    pub cost_benefit: f64,
80}
81
82#[derive(Debug, Clone)]
83pub enum InliningReason {
84    /// Function is trivial (single expression)
85    Trivial,
86    /// Function is called only once
87    SingleUse,
88    /// Function is small and frequently called
89    SmallHotFunction,
90    /// Inlining would enable further optimizations
91    EnablesOptimization,
92    /// Not inlined due to size
93    TooLarge,
94    /// Not inlined due to recursion
95    Recursive,
96    /// Not inlined due to side effects
97    HasSideEffects,
98    /// Not inlined due to loops
99    ContainsLoops,
100    /// Not inlined due to cost
101    CostTooHigh,
102}
103
104impl InliningAnalyzer {
105    pub fn new(config: InliningConfig) -> Self {
106        Self {
107            config,
108            call_graph: CallGraph::default(),
109            function_metrics: HashMap::new(),
110        }
111    }
112
113    /// Analyze a program and determine which functions should be inlined
114    pub fn analyze_program(&mut self, program: &HirProgram) -> HashMap<String, InliningDecision> {
115        // Step 1: Build call graph
116        self.build_call_graph(program);
117
118        // Step 2: Detect recursive functions
119        self.detect_recursion();
120
121        // Step 3: Calculate function metrics
122        self.calculate_metrics(program);
123
124        // Step 4: Make inlining decisions
125        self.make_decisions()
126    }
127
128    /// Apply inlining decisions to transform the program
129    pub fn apply_inlining(
130        &self,
131        mut program: HirProgram,
132        decisions: &HashMap<String, InliningDecision>,
133    ) -> HirProgram {
134        // Create a map of functions for quick lookup
135        let function_map: HashMap<String, HirFunction> = program
136            .functions
137            .iter()
138            .map(|f| (f.name.clone(), f.clone()))
139            .collect();
140
141        // Track inlined functions to remove later
142        let mut inlined_functions = HashSet::new();
143
144        // Process each function
145        for func_idx in 0..program.functions.len() {
146            let func = &mut program.functions[func_idx];
147            let mut modified_body = Vec::new();
148
149            for stmt in &func.body {
150                match self.try_inline_stmt(stmt, &function_map, decisions, 0) {
151                    Some(inlined_stmts) => {
152                        modified_body.extend(inlined_stmts);
153                        // Track which functions were inlined
154                        if let HirStmt::Expr(HirExpr::Call { func: callee, .. }) = stmt {
155                            if decisions
156                                .get(callee)
157                                .map(|d| d.should_inline)
158                                .unwrap_or(false)
159                            {
160                                inlined_functions.insert(callee.clone());
161                            }
162                        }
163                    }
164                    None => modified_body.push(stmt.clone()),
165                }
166            }
167
168            func.body = modified_body;
169        }
170
171        // Remove functions that were fully inlined (called only once)
172        if self.config.inline_single_use {
173            program.functions.retain(|f| {
174                !inlined_functions.contains(&f.name)
175                    || self
176                        .function_metrics
177                        .get(&f.name)
178                        .map(|m| m.call_count > 1)
179                        .unwrap_or(true)
180            });
181        }
182
183        program
184    }
185
186    fn build_call_graph(&mut self, program: &HirProgram) {
187        for func in &program.functions {
188            let calls = self.extract_calls_from_function(func);
189
190            self.call_graph
191                .calls
192                .insert(func.name.clone(), calls.clone());
193
194            for callee in calls {
195                self.call_graph
196                    .called_by
197                    .entry(callee)
198                    .or_default()
199                    .insert(func.name.clone());
200            }
201        }
202    }
203
204    fn extract_calls_from_function(&self, func: &HirFunction) -> HashSet<String> {
205        let mut calls = HashSet::new();
206
207        for stmt in &func.body {
208            self.extract_calls_from_stmt(stmt, &mut calls);
209        }
210
211        calls
212    }
213
214    fn extract_calls_from_stmt(&self, stmt: &HirStmt, calls: &mut HashSet<String>) {
215        match stmt {
216            HirStmt::Expr(expr) => self.extract_calls_from_expr(expr, calls),
217            HirStmt::Assign { value, .. } => self.extract_calls_from_expr(value, calls),
218            HirStmt::Return(Some(expr)) => self.extract_calls_from_expr(expr, calls),
219            HirStmt::If {
220                condition,
221                then_body,
222                else_body,
223            } => self.extract_calls_from_if(condition, then_body, else_body, calls),
224            HirStmt::While { condition, body }
225            | HirStmt::For {
226                iter: condition,
227                body,
228                ..
229            } => self.extract_calls_from_loop(condition, body, calls),
230            _ => {}
231        }
232    }
233
234    fn extract_calls_from_if(
235        &self,
236        condition: &HirExpr,
237        then_body: &[HirStmt],
238        else_body: &Option<Vec<HirStmt>>,
239        calls: &mut HashSet<String>,
240    ) {
241        self.extract_calls_from_expr(condition, calls);
242        self.extract_calls_from_body(then_body, calls);
243        if let Some(else_stmts) = else_body {
244            self.extract_calls_from_body(else_stmts, calls);
245        }
246    }
247
248    fn extract_calls_from_loop(
249        &self,
250        condition: &HirExpr,
251        body: &[HirStmt],
252        calls: &mut HashSet<String>,
253    ) {
254        self.extract_calls_from_expr(condition, calls);
255        self.extract_calls_from_body(body, calls);
256    }
257
258    fn extract_calls_from_body(&self, body: &[HirStmt], calls: &mut HashSet<String>) {
259        for s in body {
260            self.extract_calls_from_stmt(s, calls);
261        }
262    }
263
264    fn extract_calls_from_expr(&self, expr: &HirExpr, calls: &mut HashSet<String>) {
265        extract_calls_from_expr_inner(expr, calls);
266    }
267}
268
269fn extract_calls_from_expr_inner(expr: &HirExpr, calls: &mut HashSet<String>) {
270    match expr {
271        HirExpr::Call { func, args, .. } => extract_from_call(func, args, calls),
272        HirExpr::Binary { left, right, .. } => extract_from_binary(left, right, calls),
273        HirExpr::Unary { operand, .. } => extract_calls_from_expr_inner(operand, calls),
274        HirExpr::List(items) | HirExpr::Tuple(items) => extract_from_items(items, calls),
275        HirExpr::Dict(pairs) => extract_from_dict(pairs, calls),
276        HirExpr::MethodCall { object, args, .. } => extract_from_method_call(object, args, calls),
277        HirExpr::Lambda { body, .. } => extract_calls_from_expr_inner(body, calls),
278        _ => {}
279    }
280}
281
282fn extract_from_call(func: &str, args: &[HirExpr], calls: &mut HashSet<String>) {
283    calls.insert(func.to_string());
284    for arg in args {
285        extract_calls_from_expr_inner(arg, calls);
286    }
287}
288
289fn extract_from_binary(left: &HirExpr, right: &HirExpr, calls: &mut HashSet<String>) {
290    extract_calls_from_expr_inner(left, calls);
291    extract_calls_from_expr_inner(right, calls);
292}
293
294fn extract_from_items(items: &[HirExpr], calls: &mut HashSet<String>) {
295    for item in items {
296        extract_calls_from_expr_inner(item, calls);
297    }
298}
299
300fn extract_from_dict(pairs: &[(HirExpr, HirExpr)], calls: &mut HashSet<String>) {
301    for (k, v) in pairs {
302        extract_calls_from_expr_inner(k, calls);
303        extract_calls_from_expr_inner(v, calls);
304    }
305}
306
307fn extract_from_method_call(object: &HirExpr, args: &[HirExpr], calls: &mut HashSet<String>) {
308    extract_calls_from_expr_inner(object, calls);
309    for arg in args {
310        extract_calls_from_expr_inner(arg, calls);
311    }
312}
313
314impl InliningAnalyzer {
315    fn detect_recursion(&mut self) {
316        // Use DFS to detect cycles in the call graph
317        for func_name in self.call_graph.calls.keys() {
318            let mut visited = HashSet::new();
319            let mut stack = HashSet::new();
320
321            if self.is_recursive_dfs(func_name, &mut visited, &mut stack) {
322                self.call_graph.recursive.insert(func_name.clone());
323            }
324        }
325    }
326
327    fn is_recursive_dfs(
328        &self,
329        func: &str,
330        visited: &mut HashSet<String>,
331        stack: &mut HashSet<String>,
332    ) -> bool {
333        visited.insert(func.to_string());
334        stack.insert(func.to_string());
335
336        if let Some(callees) = self.call_graph.calls.get(func) {
337            for callee in callees {
338                if stack.contains(callee) {
339                    return true; // Found cycle
340                }
341
342                if !visited.contains(callee) && self.is_recursive_dfs(callee, visited, stack) {
343                    return true;
344                }
345            }
346        }
347
348        stack.remove(func);
349        false
350    }
351
352    fn calculate_metrics(&mut self, program: &HirProgram) {
353        for func in &program.functions {
354            let size = self.calculate_function_size(func);
355            let has_loops = self.contains_loops(&func.body);
356            let has_side_effects = self.has_side_effects(func);
357            let is_trivial = self.is_trivial_function(func);
358            let return_count = self.count_returns(&func.body);
359
360            // Calculate call count
361            let call_count = self
362                .call_graph
363                .called_by
364                .get(&func.name)
365                .map(|callers| callers.len())
366                .unwrap_or(0);
367
368            // Estimate execution cost
369            let cost = self.estimate_cost(func, size, has_loops, has_side_effects);
370
371            let metrics = FunctionMetrics {
372                size,
373                _param_count: func.params.len(),
374                _return_count: return_count,
375                has_loops,
376                has_side_effects,
377                is_trivial,
378                call_count,
379                cost,
380            };
381
382            self.function_metrics.insert(func.name.clone(), metrics);
383        }
384    }
385
386    fn calculate_function_size(&self, func: &HirFunction) -> usize {
387        let mut size = 0;
388        for stmt in &func.body {
389            size += self.calculate_stmt_size(stmt);
390        }
391        size
392    }
393
394    fn calculate_stmt_size(&self, stmt: &HirStmt) -> usize {
395        match stmt {
396            HirStmt::Expr(expr) => self.calculate_expr_size(expr),
397            HirStmt::Assign { value, .. } => 1 + self.calculate_expr_size(value),
398            HirStmt::Return(Some(expr)) => 1 + self.calculate_expr_size(expr),
399            HirStmt::Return(None) => 1,
400            HirStmt::If {
401                condition,
402                then_body,
403                else_body,
404            } => self.calculate_if_size(condition, then_body, else_body),
405            HirStmt::While { condition, body }
406            | HirStmt::For {
407                iter: condition,
408                body,
409                ..
410            } => self.calculate_loop_size(condition, body),
411            _ => 1,
412        }
413    }
414
415    fn calculate_if_size(
416        &self,
417        condition: &HirExpr,
418        then_body: &[HirStmt],
419        else_body: &Option<Vec<HirStmt>>,
420    ) -> usize {
421        let mut size = 1 + self.calculate_expr_size(condition);
422        size += self.calculate_body_size(then_body);
423        if let Some(else_stmts) = else_body {
424            size += self.calculate_body_size(else_stmts);
425        }
426        size
427    }
428
429    fn calculate_loop_size(&self, condition: &HirExpr, body: &[HirStmt]) -> usize {
430        let mut size = 1 + self.calculate_expr_size(condition);
431        size += self.calculate_body_size(body);
432        size
433    }
434
435    fn calculate_body_size(&self, body: &[HirStmt]) -> usize {
436        body.iter().map(|s| self.calculate_stmt_size(s)).sum()
437    }
438
439    fn calculate_expr_size(&self, expr: &HirExpr) -> usize {
440        calculate_expr_size_inner(expr)
441    }
442
443    fn contains_loops(&self, body: &[HirStmt]) -> bool {
444        contains_loops_inner(body)
445    }
446
447    fn has_side_effects(&self, func: &HirFunction) -> bool {
448        // Check function properties
449        // A function has side effects if it's not pure
450        if !func.properties.is_pure {
451            return true;
452        }
453
454        // Check for I/O operations, mutations, etc.
455        for stmt in &func.body {
456            if self.stmt_has_side_effects(stmt) {
457                return true;
458            }
459        }
460
461        false
462    }
463
464    fn stmt_has_side_effects(&self, stmt: &HirStmt) -> bool {
465        match stmt {
466            HirStmt::Expr(expr) => self.expr_has_side_effects(expr),
467            HirStmt::Assign { value, .. } => self.expr_has_side_effects(value),
468            HirStmt::Return(Some(expr)) => self.expr_has_side_effects(expr),
469            HirStmt::If {
470                condition,
471                then_body,
472                else_body,
473            } => self.if_has_side_effects(condition, then_body, else_body),
474            HirStmt::While { condition, body }
475            | HirStmt::For {
476                iter: condition,
477                body,
478                ..
479            } => self.loop_has_side_effects(condition, body),
480            HirStmt::Raise { .. } => true,
481            _ => false,
482        }
483    }
484
485    fn if_has_side_effects(
486        &self,
487        condition: &HirExpr,
488        then_body: &[HirStmt],
489        else_body: &Option<Vec<HirStmt>>,
490    ) -> bool {
491        self.expr_has_side_effects(condition)
492            || self.body_has_side_effects(then_body)
493            || else_body
494                .as_ref()
495                .map(|stmts| self.body_has_side_effects(stmts))
496                .unwrap_or(false)
497    }
498
499    fn loop_has_side_effects(&self, condition: &HirExpr, body: &[HirStmt]) -> bool {
500        self.expr_has_side_effects(condition) || self.body_has_side_effects(body)
501    }
502
503    fn body_has_side_effects(&self, body: &[HirStmt]) -> bool {
504        body.iter().any(|s| self.stmt_has_side_effects(s))
505    }
506
507    fn expr_has_side_effects(&self, expr: &HirExpr) -> bool {
508        expr_has_side_effects_inner(expr)
509    }
510
511    fn is_trivial_function(&self, func: &HirFunction) -> bool {
512        // A trivial function has a single return statement
513        if func.body.len() == 1 {
514            matches!(func.body[0], HirStmt::Return(_))
515        } else {
516            false
517        }
518    }
519
520    fn count_returns(&self, body: &[HirStmt]) -> usize {
521        count_returns_inner(body)
522    }
523
524    fn estimate_cost(
525        &self,
526        func: &HirFunction,
527        size: usize,
528        has_loops: bool,
529        has_side_effects: bool,
530    ) -> f64 {
531        let mut cost = size as f64;
532
533        // Loops significantly increase cost
534        if has_loops {
535            cost *= 10.0;
536        }
537
538        // Side effects add cost
539        if has_side_effects {
540            cost *= 2.0;
541        }
542
543        // Multiple returns add complexity
544        let return_count = count_returns_inner(&func.body);
545        if return_count > 1 {
546            cost *= 1.0 + (return_count as f64 * 0.2);
547        }
548
549        // Parameters add overhead
550        cost += func.params.len() as f64 * 0.5;
551
552        cost
553    }
554
555    fn make_decisions(&self) -> HashMap<String, InliningDecision> {
556        let mut decisions = HashMap::new();
557
558        for (func_name, metrics) in &self.function_metrics {
559            let decision = self.decide_inlining(func_name, metrics);
560            decisions.insert(func_name.clone(), decision);
561        }
562
563        decisions
564    }
565
566    fn decide_inlining(&self, func_name: &str, metrics: &FunctionMetrics) -> InliningDecision {
567        // Check disqualifying conditions first
568        if let Some(rejection) = self.check_inlining_rejections(func_name, metrics) {
569            return rejection;
570        }
571
572        // Check fast-path approvals
573        if let Some(approval) = self.check_inlining_approvals(metrics) {
574            return approval;
575        }
576
577        // Fall back to cost-benefit analysis
578        self.decide_by_cost_benefit(metrics)
579    }
580
581    fn check_inlining_rejections(
582        &self,
583        func_name: &str,
584        metrics: &FunctionMetrics,
585    ) -> Option<InliningDecision> {
586        if self.call_graph.recursive.contains(func_name) {
587            return Some(InliningDecision {
588                should_inline: false,
589                reason: InliningReason::Recursive,
590                cost_benefit: 0.0,
591            });
592        }
593
594        if metrics.size > self.config.max_inline_size {
595            return Some(InliningDecision {
596                should_inline: false,
597                reason: InliningReason::TooLarge,
598                cost_benefit: 0.0,
599            });
600        }
601
602        if metrics.has_loops && !self.config.inline_loops {
603            return Some(InliningDecision {
604                should_inline: false,
605                reason: InliningReason::ContainsLoops,
606                cost_benefit: 0.0,
607            });
608        }
609
610        if metrics.has_side_effects {
611            return Some(InliningDecision {
612                should_inline: false,
613                reason: InliningReason::HasSideEffects,
614                cost_benefit: 0.0,
615            });
616        }
617
618        None
619    }
620
621    fn check_inlining_approvals(&self, metrics: &FunctionMetrics) -> Option<InliningDecision> {
622        if self.config.inline_trivial && metrics.is_trivial {
623            return Some(InliningDecision {
624                should_inline: true,
625                reason: InliningReason::Trivial,
626                cost_benefit: 10.0,
627            });
628        }
629
630        if self.config.inline_single_use && metrics.call_count == 1 && !metrics.has_side_effects {
631            return Some(InliningDecision {
632                should_inline: true,
633                reason: InliningReason::SingleUse,
634                cost_benefit: 5.0,
635            });
636        }
637
638        None
639    }
640
641    fn decide_by_cost_benefit(&self, metrics: &FunctionMetrics) -> InliningDecision {
642        let call_overhead = 1.0;
643        let benefit = (call_overhead * metrics.call_count as f64) - metrics.cost;
644        let cost_benefit = benefit / metrics.cost;
645
646        if cost_benefit >= self.config.cost_threshold {
647            InliningDecision {
648                should_inline: true,
649                reason: InliningReason::SmallHotFunction,
650                cost_benefit,
651            }
652        } else {
653            InliningDecision {
654                should_inline: false,
655                reason: InliningReason::CostTooHigh,
656                cost_benefit,
657            }
658        }
659    }
660
661    fn try_inline_stmt(
662        &self,
663        stmt: &HirStmt,
664        function_map: &HashMap<String, HirFunction>,
665        decisions: &HashMap<String, InliningDecision>,
666        depth: usize,
667    ) -> Option<Vec<HirStmt>> {
668        // Prevent excessive inlining depth
669        if depth >= self.config.max_inline_depth {
670            return None;
671        }
672
673        match stmt {
674            HirStmt::Expr(HirExpr::Call { func, args, .. }) => {
675                self.try_inline_expr_call(func, args, function_map, decisions, depth)
676            }
677            HirStmt::Assign { target, value, .. } => {
678                self.try_inline_assign_call(target, value, function_map, decisions, depth)
679            }
680            _ => None,
681        }
682    }
683
684    fn try_inline_expr_call(
685        &self,
686        func: &str,
687        args: &[HirExpr],
688        function_map: &HashMap<String, HirFunction>,
689        decisions: &HashMap<String, InliningDecision>,
690        depth: usize,
691    ) -> Option<Vec<HirStmt>> {
692        let decision = decisions.get(func)?;
693        if !decision.should_inline {
694            return None;
695        }
696        let target_func = function_map.get(func)?;
697        Some(self.inline_function_call(target_func, args, function_map, decisions, depth))
698    }
699
700    fn try_inline_assign_call(
701        &self,
702        target: &depyler_hir::hir::AssignTarget,
703        value: &HirExpr,
704        function_map: &HashMap<String, HirFunction>,
705        decisions: &HashMap<String, InliningDecision>,
706        depth: usize,
707    ) -> Option<Vec<HirStmt>> {
708        if let HirExpr::Call { func, args, .. } = value {
709            let decision = decisions.get(func)?;
710            if !decision.should_inline {
711                return None;
712            }
713            let target_func = function_map.get(func)?;
714            let inlined =
715                self.inline_function_call(target_func, args, function_map, decisions, depth);
716
717            if inlined.is_empty() {
718                return None;
719            }
720
721            let mut result = inlined;
722            self.replace_return_with_assign(&mut result, target);
723            Some(result)
724        } else {
725            None
726        }
727    }
728
729    fn replace_return_with_assign(
730        &self,
731        statements: &mut [HirStmt],
732        target: &depyler_hir::hir::AssignTarget,
733    ) {
734        if let Some(last) = statements.last_mut() {
735            if let HirStmt::Return(Some(expr)) = last {
736                *last = HirStmt::Assign {
737                    target: target.clone(),
738                    value: expr.clone(),
739                    type_annotation: None,
740                };
741            }
742        }
743    }
744
745    fn inline_function_call(
746        &self,
747        func: &HirFunction,
748        args: &[HirExpr],
749        function_map: &HashMap<String, HirFunction>,
750        decisions: &HashMap<String, InliningDecision>,
751        depth: usize,
752    ) -> Vec<HirStmt> {
753        let mut inlined_body = Vec::new();
754
755        // Create parameter bindings
756        for (i, param) in func.params.iter().enumerate() {
757            if let Some(arg) = args.get(i) {
758                inlined_body.push(HirStmt::Assign {
759                    target: depyler_hir::hir::AssignTarget::Symbol(format!("_inline_{}", param.name)),
760                    value: arg.clone(),
761                    type_annotation: None,
762                });
763            }
764        }
765
766        // Copy and transform the function body
767        for stmt in &func.body {
768            let transformed = self.transform_stmt_for_inlining(stmt, &func.params, depth + 1);
769
770            // Recursively try to inline nested calls
771            if let Some(inlined) =
772                self.try_inline_stmt(&transformed, function_map, decisions, depth + 1)
773            {
774                inlined_body.extend(inlined);
775            } else {
776                inlined_body.push(transformed);
777            }
778        }
779
780        inlined_body
781    }
782
783    fn transform_stmt_for_inlining(
784        &self,
785        stmt: &HirStmt,
786        params: &[depyler_hir::hir::HirParam],
787        _depth: usize,
788    ) -> HirStmt {
789        match stmt {
790            HirStmt::Expr(expr) => HirStmt::Expr(transform_expr_for_inlining_inner(expr, params)),
791            HirStmt::Assign {
792                target,
793                value,
794                type_annotation,
795            } => HirStmt::Assign {
796                target: self.transform_assign_target_for_inlining(target, params),
797                value: transform_expr_for_inlining_inner(value, params),
798                type_annotation: type_annotation.clone(),
799            },
800            HirStmt::Return(Some(expr)) => {
801                HirStmt::Return(Some(transform_expr_for_inlining_inner(expr, params)))
802            }
803            HirStmt::If {
804                condition,
805                then_body,
806                else_body,
807            } => HirStmt::If {
808                condition: transform_expr_for_inlining_inner(condition, params),
809                then_body: then_body
810                    .iter()
811                    .map(|s| self.transform_stmt_for_inlining(s, params, _depth))
812                    .collect(),
813                else_body: else_body.as_ref().map(|stmts| {
814                    stmts
815                        .iter()
816                        .map(|s| self.transform_stmt_for_inlining(s, params, _depth))
817                        .collect()
818                }),
819            },
820            _ => stmt.clone(),
821        }
822    }
823
824    #[allow(dead_code)]
825    fn transform_expr_for_inlining(
826        &self,
827        expr: &HirExpr,
828        params: &[depyler_hir::hir::HirParam],
829    ) -> HirExpr {
830        transform_expr_for_inlining_inner(expr, params)
831    }
832
833    fn transform_assign_target_for_inlining(
834        &self,
835        target: &depyler_hir::hir::AssignTarget,
836        params: &[depyler_hir::hir::HirParam],
837    ) -> depyler_hir::hir::AssignTarget {
838        match target {
839            depyler_hir::hir::AssignTarget::Symbol(name) => {
840                if params.iter().any(|p| &p.name == name) {
841                    depyler_hir::hir::AssignTarget::Symbol(format!("_inline_{}", name))
842                } else {
843                    target.clone()
844                }
845            }
846            _ => target.clone(),
847        }
848    }
849}
850
851fn calculate_expr_size_inner(expr: &HirExpr) -> usize {
852    match expr {
853        HirExpr::Literal(_) | HirExpr::Var(_) => 1,
854        HirExpr::Binary { left, right, .. } => {
855            1 + calculate_expr_size_inner(left) + calculate_expr_size_inner(right)
856        }
857        HirExpr::Unary { operand, .. } => 1 + calculate_expr_size_inner(operand),
858        HirExpr::Call { args, .. } => 1 + args.iter().map(calculate_expr_size_inner).sum::<usize>(),
859        HirExpr::List(items) | HirExpr::Tuple(items) => {
860            1 + items.iter().map(calculate_expr_size_inner).sum::<usize>()
861        }
862        HirExpr::Dict(pairs) => {
863            1 + pairs
864                .iter()
865                .map(|(k, v)| calculate_expr_size_inner(k) + calculate_expr_size_inner(v))
866                .sum::<usize>()
867        }
868        _ => 1,
869    }
870}
871
872fn contains_loops_inner(body: &[HirStmt]) -> bool {
873    for stmt in body {
874        match stmt {
875            HirStmt::While { .. } | HirStmt::For { .. } => return true,
876            HirStmt::If {
877                then_body,
878                else_body,
879                ..
880            } => {
881                if contains_loops_inner(then_body) {
882                    return true;
883                }
884                if let Some(else_stmts) = else_body {
885                    if contains_loops_inner(else_stmts) {
886                        return true;
887                    }
888                }
889            }
890            _ => {}
891        }
892    }
893    false
894}
895
896fn expr_has_side_effects_inner(expr: &HirExpr) -> bool {
897    match expr {
898        HirExpr::Call { func, args, .. } => call_has_side_effects(func, args),
899        HirExpr::MethodCall { object, method, .. } => {
900            method_has_side_effects(method) || expr_has_side_effects_inner(object)
901        }
902        HirExpr::Binary { left, right, .. } => binary_has_side_effects(left, right),
903        HirExpr::Unary { operand, .. } => expr_has_side_effects_inner(operand),
904        HirExpr::List(items) | HirExpr::Tuple(items) => collection_has_side_effects(items),
905        HirExpr::Dict(pairs) => dict_has_side_effects(pairs),
906        _ => false,
907    }
908}
909
910fn call_has_side_effects(func: &str, args: &[HirExpr]) -> bool {
911    let pure_functions = ["len", "abs", "min", "max", "sum", "str", "int", "float"];
912    !pure_functions.contains(&func) || args.iter().any(expr_has_side_effects_inner)
913}
914
915fn method_has_side_effects(method: &str) -> bool {
916    let mutating_methods = [
917        "append", "extend", "remove", "pop", "clear", "sort", "reverse", "insert", "update",
918        "write",
919    ];
920    mutating_methods.contains(&method)
921}
922
923fn binary_has_side_effects(left: &HirExpr, right: &HirExpr) -> bool {
924    expr_has_side_effects_inner(left) || expr_has_side_effects_inner(right)
925}
926
927fn collection_has_side_effects(items: &[HirExpr]) -> bool {
928    items.iter().any(expr_has_side_effects_inner)
929}
930
931fn dict_has_side_effects(pairs: &[(HirExpr, HirExpr)]) -> bool {
932    pairs
933        .iter()
934        .any(|(k, v)| expr_has_side_effects_inner(k) || expr_has_side_effects_inner(v))
935}
936
937fn count_returns_inner(body: &[HirStmt]) -> usize {
938    let mut count = 0;
939    for stmt in body {
940        match stmt {
941            HirStmt::Return(_) => count += 1,
942            HirStmt::If {
943                then_body,
944                else_body,
945                ..
946            } => {
947                count += count_returns_inner(then_body);
948                if let Some(else_stmts) = else_body {
949                    count += count_returns_inner(else_stmts);
950                }
951            }
952            HirStmt::While { body, .. } | HirStmt::For { body, .. } => {
953                count += count_returns_inner(body);
954            }
955            _ => {}
956        }
957    }
958    count
959}
960
961fn transform_expr_for_inlining_inner(expr: &HirExpr, params: &[depyler_hir::hir::HirParam]) -> HirExpr {
962    match expr {
963        HirExpr::Var(name) => {
964            // Replace parameter references with inlined versions
965            if params.iter().any(|p| &p.name == name) {
966                HirExpr::Var(format!("_inline_{}", name))
967            } else {
968                expr.clone()
969            }
970        }
971        HirExpr::Binary { left, right, op } => HirExpr::Binary {
972            left: Box::new(transform_expr_for_inlining_inner(left, params)),
973            right: Box::new(transform_expr_for_inlining_inner(right, params)),
974            op: *op,
975        },
976        HirExpr::Unary { operand, op } => HirExpr::Unary {
977            operand: Box::new(transform_expr_for_inlining_inner(operand, params)),
978            op: *op,
979        },
980        HirExpr::Call { func, args, .. } => HirExpr::Call {
981            func: func.clone(),
982            args: args
983                .iter()
984                .map(|a| transform_expr_for_inlining_inner(a, params))
985                .collect(),
986            kwargs: vec![],
987        },
988        _ => expr.clone(),
989    }
990}
991
992#[cfg(test)]
993mod tests {
994    use super::*;
995    use depyler_hir::hir::*;
996    use smallvec::smallvec;
997
998    fn create_simple_function(name: &str, size: usize) -> HirFunction {
999        let mut body = Vec::new();
1000        for i in 0..size {
1001            body.push(HirStmt::Assign {
1002                target: AssignTarget::Symbol(Symbol::from(format!("x{}", i).as_str())),
1003                value: HirExpr::Literal(Literal::Int(i as i64)),
1004                type_annotation: None,
1005            });
1006        }
1007        body.push(HirStmt::Return(Some(HirExpr::Var("x0".to_string()))));
1008
1009        HirFunction {
1010            name: name.to_string(),
1011            params: smallvec![],
1012            ret_type: Type::Int,
1013            body,
1014            properties: FunctionProperties::default(),
1015            annotations: Default::default(),
1016            docstring: None,
1017        }
1018    }
1019
1020    #[test]
1021    fn test_trivial_function_detection() {
1022        let func = HirFunction {
1023            name: "identity".to_string(),
1024            params: smallvec![HirParam::new("x".to_string(), Type::Int)],
1025            ret_type: Type::Int,
1026            body: vec![HirStmt::Return(Some(HirExpr::Var("x".to_string())))],
1027            properties: FunctionProperties::default(),
1028            annotations: Default::default(),
1029            docstring: None,
1030        };
1031
1032        let analyzer = InliningAnalyzer::new(InliningConfig::default());
1033        assert!(analyzer.is_trivial_function(&func));
1034    }
1035
1036    #[test]
1037    fn test_function_size_calculation() {
1038        let func = create_simple_function("test", 5);
1039        let analyzer = InliningAnalyzer::new(InliningConfig::default());
1040        let size = analyzer.calculate_function_size(&func);
1041        assert_eq!(size, 12); // 5 assignments (2 each) + 1 return (2) = 12
1042    }
1043
1044    #[test]
1045    fn test_loop_detection() {
1046        let body = vec![HirStmt::While {
1047            condition: HirExpr::Literal(Literal::Bool(true)),
1048            body: vec![HirStmt::Break { label: None }],
1049        }];
1050
1051        let _analyzer = InliningAnalyzer::new(InliningConfig::default());
1052        assert!(contains_loops_inner(&body));
1053    }
1054
1055    #[test]
1056    fn test_side_effect_detection() {
1057        let expr = HirExpr::MethodCall {
1058            object: Box::new(HirExpr::Var("list".to_string())),
1059            method: "append".to_string(),
1060            args: vec![HirExpr::Literal(Literal::Int(42))],
1061            kwargs: vec![],
1062        };
1063
1064        let _analyzer = InliningAnalyzer::new(InliningConfig::default());
1065        assert!(expr_has_side_effects_inner(&expr));
1066    }
1067
1068    #[test]
1069    fn test_inlining_config_default() {
1070        let config = InliningConfig::default();
1071        assert_eq!(config.max_inline_size, 20);
1072        assert_eq!(config.max_inline_depth, 3);
1073        assert!(config.inline_single_use);
1074        assert!(config.inline_trivial);
1075    }
1076
1077    #[test]
1078    fn test_inlining_config_custom() {
1079        let config = InliningConfig {
1080            max_inline_size: 50,
1081            max_inline_depth: 5,
1082            inline_single_use: false,
1083            inline_trivial: false,
1084            cost_threshold: 2.0,
1085            inline_loops: true,
1086        };
1087        assert_eq!(config.max_inline_size, 50);
1088        assert_eq!(config.max_inline_depth, 5);
1089        assert!(!config.inline_single_use);
1090        assert!(!config.inline_trivial);
1091        assert_eq!(config.cost_threshold, 2.0);
1092        assert!(config.inline_loops);
1093    }
1094
1095    #[test]
1096    fn test_inlining_reason_variants() {
1097        let reasons = [
1098            InliningReason::Trivial,
1099            InliningReason::SingleUse,
1100            InliningReason::SmallHotFunction,
1101            InliningReason::EnablesOptimization,
1102            InliningReason::TooLarge,
1103            InliningReason::Recursive,
1104            InliningReason::HasSideEffects,
1105            InliningReason::ContainsLoops,
1106        ];
1107        for reason in &reasons {
1108            let _ = format!("{:?}", reason);
1109        }
1110    }
1111
1112    #[test]
1113    fn test_inlining_decision_creation() {
1114        let decision = InliningDecision {
1115            should_inline: true,
1116            reason: InliningReason::Trivial,
1117            cost_benefit: 1.5,
1118        };
1119        assert!(decision.should_inline);
1120        assert_eq!(decision.cost_benefit, 1.5);
1121    }
1122
1123    #[test]
1124    fn test_analyzer_new() {
1125        let analyzer = InliningAnalyzer::new(InliningConfig::default());
1126        assert!(std::mem::size_of_val(&analyzer) > 0);
1127    }
1128
1129    #[test]
1130    fn test_non_trivial_function() {
1131        let func = HirFunction {
1132            name: "complex".to_string(),
1133            params: smallvec![HirParam::new("x".to_string(), Type::Int)],
1134            ret_type: Type::Int,
1135            body: vec![
1136                HirStmt::Assign {
1137                    target: AssignTarget::Symbol("y".to_string()),
1138                    value: HirExpr::Var("x".to_string()),
1139                    type_annotation: None,
1140                },
1141                HirStmt::Return(Some(HirExpr::Var("y".to_string()))),
1142            ],
1143            properties: FunctionProperties::default(),
1144            annotations: Default::default(),
1145            docstring: None,
1146        };
1147
1148        let analyzer = InliningAnalyzer::new(InliningConfig::default());
1149        assert!(!analyzer.is_trivial_function(&func));
1150    }
1151
1152    #[test]
1153    fn test_for_loop_detection() {
1154        let body = vec![HirStmt::For {
1155            target: AssignTarget::Symbol("i".to_string()),
1156            iter: HirExpr::Call {
1157                func: "range".to_string(),
1158                args: vec![HirExpr::Literal(Literal::Int(10))],
1159                kwargs: vec![],
1160            },
1161            body: vec![],
1162        }];
1163        assert!(contains_loops_inner(&body));
1164    }
1165
1166    #[test]
1167    fn test_if_no_loop() {
1168        let body = vec![HirStmt::If {
1169            condition: HirExpr::Literal(Literal::Bool(true)),
1170            then_body: vec![HirStmt::Return(Some(HirExpr::Literal(Literal::Int(1))))],
1171            else_body: None,
1172        }];
1173        assert!(!contains_loops_inner(&body));
1174    }
1175
1176    #[test]
1177    fn test_print_side_effect() {
1178        let expr = HirExpr::Call {
1179            func: "print".to_string(),
1180            args: vec![HirExpr::Literal(Literal::String("hello".to_string()))],
1181            kwargs: vec![],
1182        };
1183        assert!(expr_has_side_effects_inner(&expr));
1184    }
1185
1186    #[test]
1187    fn test_append_side_effect() {
1188        let expr = HirExpr::MethodCall {
1189            object: Box::new(HirExpr::Var("list".to_string())),
1190            method: "append".to_string(),
1191            args: vec![HirExpr::Literal(Literal::Int(1))],
1192            kwargs: vec![],
1193        };
1194        assert!(expr_has_side_effects_inner(&expr));
1195    }
1196
1197    #[test]
1198    fn test_pure_function_no_side_effect() {
1199        let expr = HirExpr::Binary {
1200            left: Box::new(HirExpr::Literal(Literal::Int(1))),
1201            op: BinOp::Add,
1202            right: Box::new(HirExpr::Literal(Literal::Int(2))),
1203        };
1204        assert!(!expr_has_side_effects_inner(&expr));
1205    }
1206
1207    #[test]
1208    fn test_function_size_empty() {
1209        let func = HirFunction {
1210            name: "empty".to_string(),
1211            params: smallvec![],
1212            ret_type: Type::None,
1213            body: vec![],
1214            properties: FunctionProperties::default(),
1215            annotations: Default::default(),
1216            docstring: None,
1217        };
1218        let analyzer = InliningAnalyzer::new(InliningConfig::default());
1219        assert_eq!(analyzer.calculate_function_size(&func), 0);
1220    }
1221
1222    #[test]
1223    fn test_call_graph_default() {
1224        let cg = CallGraph::default();
1225        assert!(cg.calls.is_empty());
1226        assert!(cg.called_by.is_empty());
1227        assert!(cg.recursive.is_empty());
1228    }
1229
1230    #[test]
1231    fn test_expr_size_literal() {
1232        let expr = HirExpr::Literal(Literal::Int(42));
1233        assert_eq!(calculate_expr_size_inner(&expr), 1);
1234    }
1235
1236    #[test]
1237    fn test_expr_size_var() {
1238        let expr = HirExpr::Var("x".to_string());
1239        assert_eq!(calculate_expr_size_inner(&expr), 1);
1240    }
1241
1242    #[test]
1243    fn test_expr_size_binary() {
1244        let expr = HirExpr::Binary {
1245            left: Box::new(HirExpr::Literal(Literal::Int(1))),
1246            op: BinOp::Add,
1247            right: Box::new(HirExpr::Literal(Literal::Int(2))),
1248        };
1249        assert_eq!(calculate_expr_size_inner(&expr), 3);
1250    }
1251
1252    #[test]
1253    fn test_expr_size_list() {
1254        let expr = HirExpr::List(vec![
1255            HirExpr::Literal(Literal::Int(1)),
1256            HirExpr::Literal(Literal::Int(2)),
1257        ]);
1258        assert_eq!(calculate_expr_size_inner(&expr), 3);
1259    }
1260
1261    #[test]
1262    fn test_stmt_size_return_none() {
1263        let analyzer = InliningAnalyzer::new(InliningConfig::default());
1264        let stmt = HirStmt::Return(None);
1265        assert_eq!(analyzer.calculate_stmt_size(&stmt), 1);
1266    }
1267
1268    #[test]
1269    fn test_stmt_size_return_some() {
1270        let analyzer = InliningAnalyzer::new(InliningConfig::default());
1271        let stmt = HirStmt::Return(Some(HirExpr::Literal(Literal::Int(42))));
1272        assert_eq!(analyzer.calculate_stmt_size(&stmt), 2);
1273    }
1274
1275    #[test]
1276    fn test_stmt_size_expr() {
1277        let analyzer = InliningAnalyzer::new(InliningConfig::default());
1278        let stmt = HirStmt::Expr(HirExpr::Literal(Literal::Int(42)));
1279        assert_eq!(analyzer.calculate_stmt_size(&stmt), 1);
1280    }
1281
1282    #[test]
1283    fn test_stmt_size_pass() {
1284        let analyzer = InliningAnalyzer::new(InliningConfig::default());
1285        let stmt = HirStmt::Pass;
1286        assert_eq!(analyzer.calculate_stmt_size(&stmt), 1);
1287    }
1288
1289    #[test]
1290    fn test_stmt_size_break() {
1291        let analyzer = InliningAnalyzer::new(InliningConfig::default());
1292        let stmt = HirStmt::Break { label: None };
1293        assert_eq!(analyzer.calculate_stmt_size(&stmt), 1);
1294    }
1295
1296    #[test]
1297    fn test_stmt_size_continue() {
1298        let analyzer = InliningAnalyzer::new(InliningConfig::default());
1299        let stmt = HirStmt::Continue { label: None };
1300        assert_eq!(analyzer.calculate_stmt_size(&stmt), 1);
1301    }
1302
1303    // ============================================================================
1304    // ADDITIONAL COVERAGE TESTS - Call extraction and analysis
1305    // ============================================================================
1306
1307    #[test]
1308    fn test_extract_calls_from_call_expr() {
1309        let mut calls = HashSet::new();
1310        let expr = HirExpr::Call {
1311            func: "foo".to_string(),
1312            args: vec![HirExpr::Literal(Literal::Int(1))],
1313            kwargs: vec![],
1314        };
1315        extract_calls_from_expr_inner(&expr, &mut calls);
1316        assert!(calls.contains("foo"));
1317    }
1318
1319    #[test]
1320    fn test_extract_calls_from_binary_expr() {
1321        let mut calls = HashSet::new();
1322        let expr = HirExpr::Binary {
1323            left: Box::new(HirExpr::Call {
1324                func: "left_fn".to_string(),
1325                args: vec![],
1326                kwargs: vec![],
1327            }),
1328            op: BinOp::Add,
1329            right: Box::new(HirExpr::Call {
1330                func: "right_fn".to_string(),
1331                args: vec![],
1332                kwargs: vec![],
1333            }),
1334        };
1335        extract_calls_from_expr_inner(&expr, &mut calls);
1336        assert!(calls.contains("left_fn"));
1337        assert!(calls.contains("right_fn"));
1338    }
1339
1340    #[test]
1341    fn test_extract_calls_from_unary_expr() {
1342        let mut calls = HashSet::new();
1343        let expr = HirExpr::Unary {
1344            op: depyler_hir::hir::UnaryOp::Neg,
1345            operand: Box::new(HirExpr::Call {
1346                func: "inner".to_string(),
1347                args: vec![],
1348                kwargs: vec![],
1349            }),
1350        };
1351        extract_calls_from_expr_inner(&expr, &mut calls);
1352        assert!(calls.contains("inner"));
1353    }
1354
1355    #[test]
1356    fn test_extract_calls_from_list() {
1357        let mut calls = HashSet::new();
1358        let expr = HirExpr::List(vec![
1359            HirExpr::Call {
1360                func: "fn1".to_string(),
1361                args: vec![],
1362                kwargs: vec![],
1363            },
1364            HirExpr::Call {
1365                func: "fn2".to_string(),
1366                args: vec![],
1367                kwargs: vec![],
1368            },
1369        ]);
1370        extract_calls_from_expr_inner(&expr, &mut calls);
1371        assert!(calls.contains("fn1"));
1372        assert!(calls.contains("fn2"));
1373    }
1374
1375    #[test]
1376    fn test_extract_calls_from_tuple() {
1377        let mut calls = HashSet::new();
1378        let expr = HirExpr::Tuple(vec![HirExpr::Call {
1379            func: "tuple_fn".to_string(),
1380            args: vec![],
1381            kwargs: vec![],
1382        }]);
1383        extract_calls_from_expr_inner(&expr, &mut calls);
1384        assert!(calls.contains("tuple_fn"));
1385    }
1386
1387    #[test]
1388    fn test_extract_calls_from_dict() {
1389        let mut calls = HashSet::new();
1390        let expr = HirExpr::Dict(vec![(
1391            HirExpr::Call {
1392                func: "key_fn".to_string(),
1393                args: vec![],
1394                kwargs: vec![],
1395            },
1396            HirExpr::Call {
1397                func: "val_fn".to_string(),
1398                args: vec![],
1399                kwargs: vec![],
1400            },
1401        )]);
1402        extract_calls_from_expr_inner(&expr, &mut calls);
1403        assert!(calls.contains("key_fn"));
1404        assert!(calls.contains("val_fn"));
1405    }
1406
1407    #[test]
1408    fn test_extract_calls_from_method_call() {
1409        let mut calls = HashSet::new();
1410        let expr = HirExpr::MethodCall {
1411            object: Box::new(HirExpr::Call {
1412                func: "get_obj".to_string(),
1413                args: vec![],
1414                kwargs: vec![],
1415            }),
1416            method: "method".to_string(),
1417            args: vec![HirExpr::Call {
1418                func: "get_arg".to_string(),
1419                args: vec![],
1420                kwargs: vec![],
1421            }],
1422            kwargs: vec![],
1423        };
1424        extract_calls_from_expr_inner(&expr, &mut calls);
1425        assert!(calls.contains("get_obj"));
1426        assert!(calls.contains("get_arg"));
1427    }
1428
1429    #[test]
1430    fn test_extract_calls_from_lambda() {
1431        let mut calls = HashSet::new();
1432        let expr = HirExpr::Lambda {
1433            params: vec!["x".to_string()],
1434            body: Box::new(HirExpr::Call {
1435                func: "lambda_call".to_string(),
1436                args: vec![],
1437                kwargs: vec![],
1438            }),
1439        };
1440        extract_calls_from_expr_inner(&expr, &mut calls);
1441        assert!(calls.contains("lambda_call"));
1442    }
1443
1444    #[test]
1445    fn test_extract_calls_from_var() {
1446        let mut calls = HashSet::new();
1447        let expr = HirExpr::Var("x".to_string());
1448        extract_calls_from_expr_inner(&expr, &mut calls);
1449        assert!(calls.is_empty());
1450    }
1451
1452    #[test]
1453    fn test_extract_calls_from_literal() {
1454        let mut calls = HashSet::new();
1455        let expr = HirExpr::Literal(Literal::Int(42));
1456        extract_calls_from_expr_inner(&expr, &mut calls);
1457        assert!(calls.is_empty());
1458    }
1459
1460    #[test]
1461    fn test_analyzer_extract_calls_from_function() {
1462        let analyzer = InliningAnalyzer::new(InliningConfig::default());
1463        let func = HirFunction {
1464            name: "caller".to_string(),
1465            params: smallvec![],
1466            ret_type: Type::Int,
1467            body: vec![
1468                HirStmt::Expr(HirExpr::Call {
1469                    func: "callee1".to_string(),
1470                    args: vec![],
1471                    kwargs: vec![],
1472                }),
1473                HirStmt::Assign {
1474                    target: AssignTarget::Symbol("x".to_string()),
1475                    value: HirExpr::Call {
1476                        func: "callee2".to_string(),
1477                        args: vec![],
1478                        kwargs: vec![],
1479                    },
1480                    type_annotation: None,
1481                },
1482                HirStmt::Return(Some(HirExpr::Call {
1483                    func: "callee3".to_string(),
1484                    args: vec![],
1485                    kwargs: vec![],
1486                })),
1487            ],
1488            properties: FunctionProperties::default(),
1489            annotations: Default::default(),
1490            docstring: None,
1491        };
1492        let calls = analyzer.extract_calls_from_function(&func);
1493        assert!(calls.contains("callee1"));
1494        assert!(calls.contains("callee2"));
1495        assert!(calls.contains("callee3"));
1496    }
1497
1498    #[test]
1499    fn test_analyzer_extract_calls_from_if() {
1500        let analyzer = InliningAnalyzer::new(InliningConfig::default());
1501        let func = HirFunction {
1502            name: "if_caller".to_string(),
1503            params: smallvec![],
1504            ret_type: Type::Int,
1505            body: vec![HirStmt::If {
1506                condition: HirExpr::Call {
1507                    func: "cond_fn".to_string(),
1508                    args: vec![],
1509                    kwargs: vec![],
1510                },
1511                then_body: vec![HirStmt::Expr(HirExpr::Call {
1512                    func: "then_fn".to_string(),
1513                    args: vec![],
1514                    kwargs: vec![],
1515                })],
1516                else_body: Some(vec![HirStmt::Expr(HirExpr::Call {
1517                    func: "else_fn".to_string(),
1518                    args: vec![],
1519                    kwargs: vec![],
1520                })]),
1521            }],
1522            properties: FunctionProperties::default(),
1523            annotations: Default::default(),
1524            docstring: None,
1525        };
1526        let calls = analyzer.extract_calls_from_function(&func);
1527        assert!(calls.contains("cond_fn"));
1528        assert!(calls.contains("then_fn"));
1529        assert!(calls.contains("else_fn"));
1530    }
1531
1532    #[test]
1533    fn test_analyzer_extract_calls_from_while() {
1534        let analyzer = InliningAnalyzer::new(InliningConfig::default());
1535        let func = HirFunction {
1536            name: "while_caller".to_string(),
1537            params: smallvec![],
1538            ret_type: Type::Int,
1539            body: vec![HirStmt::While {
1540                condition: HirExpr::Call {
1541                    func: "while_cond".to_string(),
1542                    args: vec![],
1543                    kwargs: vec![],
1544                },
1545                body: vec![HirStmt::Expr(HirExpr::Call {
1546                    func: "while_body".to_string(),
1547                    args: vec![],
1548                    kwargs: vec![],
1549                })],
1550            }],
1551            properties: FunctionProperties::default(),
1552            annotations: Default::default(),
1553            docstring: None,
1554        };
1555        let calls = analyzer.extract_calls_from_function(&func);
1556        assert!(calls.contains("while_cond"));
1557        assert!(calls.contains("while_body"));
1558    }
1559
1560    #[test]
1561    fn test_analyzer_extract_calls_from_for() {
1562        let analyzer = InliningAnalyzer::new(InliningConfig::default());
1563        let func = HirFunction {
1564            name: "for_caller".to_string(),
1565            params: smallvec![],
1566            ret_type: Type::Int,
1567            body: vec![HirStmt::For {
1568                target: AssignTarget::Symbol("i".to_string()),
1569                iter: HirExpr::Call {
1570                    func: "for_iter".to_string(),
1571                    args: vec![],
1572                    kwargs: vec![],
1573                },
1574                body: vec![HirStmt::Expr(HirExpr::Call {
1575                    func: "for_body".to_string(),
1576                    args: vec![],
1577                    kwargs: vec![],
1578                })],
1579            }],
1580            properties: FunctionProperties::default(),
1581            annotations: Default::default(),
1582            docstring: None,
1583        };
1584        let calls = analyzer.extract_calls_from_function(&func);
1585        assert!(calls.contains("for_iter"));
1586        assert!(calls.contains("for_body"));
1587    }
1588
1589    #[test]
1590    fn test_function_metrics_default() {
1591        let metrics = FunctionMetrics {
1592            size: 10,
1593            _param_count: 2,
1594            _return_count: 1,
1595            has_loops: false,
1596            has_side_effects: false,
1597            is_trivial: true,
1598            call_count: 3,
1599            cost: 1.5,
1600        };
1601        assert_eq!(metrics.size, 10);
1602        assert!(!metrics.has_loops);
1603        assert!(!metrics.has_side_effects);
1604        assert!(metrics.is_trivial);
1605        assert_eq!(metrics.call_count, 3);
1606    }
1607
1608    #[test]
1609    fn test_expr_size_call() {
1610        let expr = HirExpr::Call {
1611            func: "fn".to_string(),
1612            args: vec![
1613                HirExpr::Literal(Literal::Int(1)),
1614                HirExpr::Literal(Literal::Int(2)),
1615            ],
1616            kwargs: vec![],
1617        };
1618        let size = calculate_expr_size_inner(&expr);
1619        assert_eq!(size, 3); // 1 for call + 1 for each arg (2 args)
1620    }
1621
1622    #[test]
1623    fn test_expr_size_method_call() {
1624        let expr = HirExpr::MethodCall {
1625            object: Box::new(HirExpr::Var("obj".to_string())),
1626            method: "method".to_string(),
1627            args: vec![HirExpr::Literal(Literal::Int(1))],
1628            kwargs: vec![],
1629        };
1630        let size = calculate_expr_size_inner(&expr);
1631        assert_eq!(size, 1); // MethodCall falls into catchall case
1632    }
1633
1634    #[test]
1635    fn test_expr_size_dict() {
1636        let expr = HirExpr::Dict(vec![
1637            (
1638                HirExpr::Literal(Literal::String("a".to_string())),
1639                HirExpr::Literal(Literal::Int(1)),
1640            ),
1641            (
1642                HirExpr::Literal(Literal::String("b".to_string())),
1643                HirExpr::Literal(Literal::Int(2)),
1644            ),
1645        ]);
1646        let size = calculate_expr_size_inner(&expr);
1647        assert!(size >= 5); // 2 pairs * 2 elements + 1 base
1648    }
1649
1650    #[test]
1651    fn test_expr_size_lambda() {
1652        let expr = HirExpr::Lambda {
1653            params: vec!["x".to_string()],
1654            body: Box::new(HirExpr::Binary {
1655                left: Box::new(HirExpr::Var("x".to_string())),
1656                op: BinOp::Add,
1657                right: Box::new(HirExpr::Literal(Literal::Int(1))),
1658            }),
1659        };
1660        let size = calculate_expr_size_inner(&expr);
1661        assert_eq!(size, 1); // Lambda falls into catchall case
1662    }
1663
1664    #[test]
1665    fn test_expr_size_unary() {
1666        let expr = HirExpr::Unary {
1667            op: depyler_hir::hir::UnaryOp::Neg,
1668            operand: Box::new(HirExpr::Literal(Literal::Int(5))),
1669        };
1670        let size = calculate_expr_size_inner(&expr);
1671        assert_eq!(size, 2); // unary + operand
1672    }
1673
1674    #[test]
1675    fn test_contains_loops_with_nested_while() {
1676        let body = vec![HirStmt::If {
1677            condition: HirExpr::Literal(Literal::Bool(true)),
1678            then_body: vec![HirStmt::While {
1679                condition: HirExpr::Literal(Literal::Bool(true)),
1680                body: vec![],
1681            }],
1682            else_body: None,
1683        }];
1684        assert!(contains_loops_inner(&body));
1685    }
1686
1687    #[test]
1688    fn test_contains_loops_with_nested_for() {
1689        let body = vec![HirStmt::If {
1690            condition: HirExpr::Literal(Literal::Bool(true)),
1691            then_body: vec![HirStmt::For {
1692                target: AssignTarget::Symbol("i".to_string()),
1693                iter: HirExpr::Var("items".to_string()),
1694                body: vec![],
1695            }],
1696            else_body: None,
1697        }];
1698        assert!(contains_loops_inner(&body));
1699    }
1700
1701    #[test]
1702    fn test_side_effect_write() {
1703        let expr = HirExpr::MethodCall {
1704            object: Box::new(HirExpr::Var("file".to_string())),
1705            method: "write".to_string(),
1706            args: vec![HirExpr::Literal(Literal::String("data".to_string()))],
1707            kwargs: vec![],
1708        };
1709        assert!(expr_has_side_effects_inner(&expr));
1710    }
1711
1712    #[test]
1713    fn test_side_effect_extend() {
1714        let expr = HirExpr::MethodCall {
1715            object: Box::new(HirExpr::Var("list".to_string())),
1716            method: "extend".to_string(),
1717            args: vec![HirExpr::List(vec![])],
1718            kwargs: vec![],
1719        };
1720        assert!(expr_has_side_effects_inner(&expr));
1721    }
1722
1723    #[test]
1724    fn test_side_effect_insert() {
1725        let expr = HirExpr::MethodCall {
1726            object: Box::new(HirExpr::Var("list".to_string())),
1727            method: "insert".to_string(),
1728            args: vec![
1729                HirExpr::Literal(Literal::Int(0)),
1730                HirExpr::Literal(Literal::Int(42)),
1731            ],
1732            kwargs: vec![],
1733        };
1734        assert!(expr_has_side_effects_inner(&expr));
1735    }
1736
1737    #[test]
1738    fn test_side_effect_pop() {
1739        let expr = HirExpr::MethodCall {
1740            object: Box::new(HirExpr::Var("list".to_string())),
1741            method: "pop".to_string(),
1742            args: vec![],
1743            kwargs: vec![],
1744        };
1745        assert!(expr_has_side_effects_inner(&expr));
1746    }
1747
1748    #[test]
1749    fn test_side_effect_remove() {
1750        let expr = HirExpr::MethodCall {
1751            object: Box::new(HirExpr::Var("list".to_string())),
1752            method: "remove".to_string(),
1753            args: vec![HirExpr::Literal(Literal::Int(1))],
1754            kwargs: vec![],
1755        };
1756        assert!(expr_has_side_effects_inner(&expr));
1757    }
1758
1759    #[test]
1760    fn test_side_effect_clear() {
1761        let expr = HirExpr::MethodCall {
1762            object: Box::new(HirExpr::Var("list".to_string())),
1763            method: "clear".to_string(),
1764            args: vec![],
1765            kwargs: vec![],
1766        };
1767        assert!(expr_has_side_effects_inner(&expr));
1768    }
1769
1770    #[test]
1771    fn test_side_effect_update() {
1772        let expr = HirExpr::MethodCall {
1773            object: Box::new(HirExpr::Var("dict".to_string())),
1774            method: "update".to_string(),
1775            args: vec![HirExpr::Dict(vec![])],
1776            kwargs: vec![],
1777        };
1778        assert!(expr_has_side_effects_inner(&expr));
1779    }
1780
1781    #[test]
1782    fn test_pure_method_call() {
1783        let expr = HirExpr::MethodCall {
1784            object: Box::new(HirExpr::Var("list".to_string())),
1785            method: "copy".to_string(),
1786            args: vec![],
1787            kwargs: vec![],
1788        };
1789        assert!(!expr_has_side_effects_inner(&expr));
1790    }
1791
1792    #[test]
1793    fn test_side_effect_in_args() {
1794        let expr = HirExpr::Call {
1795            func: "pure_fn".to_string(),
1796            args: vec![HirExpr::Call {
1797                func: "print".to_string(),
1798                args: vec![],
1799                kwargs: vec![],
1800            }],
1801            kwargs: vec![],
1802        };
1803        assert!(expr_has_side_effects_inner(&expr));
1804    }
1805
1806    #[test]
1807    fn test_side_effect_in_method_object() {
1808        let expr = HirExpr::MethodCall {
1809            object: Box::new(HirExpr::MethodCall {
1810                object: Box::new(HirExpr::Var("list".to_string())),
1811                method: "append".to_string(),
1812                args: vec![HirExpr::Literal(Literal::Int(1))],
1813                kwargs: vec![],
1814            }),
1815            method: "copy".to_string(),
1816            args: vec![],
1817            kwargs: vec![],
1818        };
1819        assert!(expr_has_side_effects_inner(&expr));
1820    }
1821
1822    // ============================================================================
1823    // EXTENDED INLINING COVERAGE TESTS
1824    // Focus on uncovered code paths in inlining.rs
1825    // ============================================================================
1826
1827    // --- CallGraph tests ---
1828    #[test]
1829    fn test_call_graph_with_function() {
1830        let mut analyzer = InliningAnalyzer::new(InliningConfig::default());
1831        let program = HirProgram {
1832            functions: vec![
1833                HirFunction {
1834                    name: "caller".to_string(),
1835                    params: smallvec![],
1836                    ret_type: Type::Int,
1837                    body: vec![HirStmt::Return(Some(HirExpr::Call {
1838                        func: "callee".to_string(),
1839                        args: vec![],
1840                        kwargs: vec![],
1841                    }))],
1842                    properties: FunctionProperties::default(),
1843                    annotations: Default::default(),
1844                    docstring: None,
1845                },
1846                HirFunction {
1847                    name: "callee".to_string(),
1848                    params: smallvec![],
1849                    ret_type: Type::Int,
1850                    body: vec![HirStmt::Return(Some(HirExpr::Literal(Literal::Int(42))))],
1851                    properties: FunctionProperties::default(),
1852                    annotations: Default::default(),
1853                    docstring: None,
1854                },
1855            ],
1856            classes: vec![],
1857            imports: vec![],
1858        };
1859        let decisions = analyzer.analyze_program(&program);
1860        assert!(!decisions.is_empty());
1861    }
1862
1863    // --- Recursion detection ---
1864    #[test]
1865    fn test_recursive_function_detection() {
1866        let mut analyzer = InliningAnalyzer::new(InliningConfig::default());
1867        let program = HirProgram {
1868            functions: vec![HirFunction {
1869                name: "recursive".to_string(),
1870                params: smallvec![HirParam::new("n".to_string(), Type::Int)],
1871                ret_type: Type::Int,
1872                body: vec![HirStmt::If {
1873                    condition: HirExpr::Binary {
1874                        left: Box::new(HirExpr::Var("n".to_string())),
1875                        op: BinOp::Eq,
1876                        right: Box::new(HirExpr::Literal(Literal::Int(0))),
1877                    },
1878                    then_body: vec![HirStmt::Return(Some(HirExpr::Literal(Literal::Int(1))))],
1879                    else_body: Some(vec![HirStmt::Return(Some(HirExpr::Binary {
1880                        left: Box::new(HirExpr::Var("n".to_string())),
1881                        op: BinOp::Mul,
1882                        right: Box::new(HirExpr::Call {
1883                            func: "recursive".to_string(),
1884                            args: vec![HirExpr::Binary {
1885                                left: Box::new(HirExpr::Var("n".to_string())),
1886                                op: BinOp::Sub,
1887                                right: Box::new(HirExpr::Literal(Literal::Int(1))),
1888                            }],
1889                            kwargs: vec![],
1890                        }),
1891                    }))]),
1892                }],
1893                properties: FunctionProperties::default(),
1894                annotations: Default::default(),
1895                docstring: None,
1896            }],
1897            classes: vec![],
1898            imports: vec![],
1899        };
1900        let decisions = analyzer.analyze_program(&program);
1901        // Recursive function should not be inlined
1902        if let Some(decision) = decisions.get("recursive") {
1903            assert!(
1904                !decision.should_inline || matches!(decision.reason, InliningReason::Recursive)
1905            );
1906        }
1907    }
1908
1909    // --- Side effects in expression collection ---
1910    #[test]
1911    fn test_side_effect_sort() {
1912        let expr = HirExpr::MethodCall {
1913            object: Box::new(HirExpr::Var("list".to_string())),
1914            method: "sort".to_string(),
1915            args: vec![],
1916            kwargs: vec![],
1917        };
1918        assert!(expr_has_side_effects_inner(&expr));
1919    }
1920
1921    #[test]
1922    fn test_side_effect_reverse() {
1923        let expr = HirExpr::MethodCall {
1924            object: Box::new(HirExpr::Var("list".to_string())),
1925            method: "reverse".to_string(),
1926            args: vec![],
1927            kwargs: vec![],
1928        };
1929        assert!(expr_has_side_effects_inner(&expr));
1930    }
1931
1932    #[test]
1933    fn test_side_effect_dict_update() {
1934        let expr = HirExpr::MethodCall {
1935            object: Box::new(HirExpr::Var("dict".to_string())),
1936            method: "update".to_string(),
1937            args: vec![HirExpr::Dict(vec![])],
1938            kwargs: vec![],
1939        };
1940        assert!(expr_has_side_effects_inner(&expr));
1941    }
1942
1943    #[test]
1944    fn test_pure_expr_get() {
1945        let expr = HirExpr::MethodCall {
1946            object: Box::new(HirExpr::Var("dict".to_string())),
1947            method: "get".to_string(),
1948            args: vec![HirExpr::Literal(Literal::String("key".to_string()))],
1949            kwargs: vec![],
1950        };
1951        // get is a pure method, no side effects
1952        assert!(!expr_has_side_effects_inner(&expr));
1953    }
1954
1955    // --- Collection expression size ---
1956    #[test]
1957    fn test_expr_size_set() {
1958        let expr = HirExpr::Set(vec![
1959            HirExpr::Literal(Literal::Int(1)),
1960            HirExpr::Literal(Literal::Int(2)),
1961        ]);
1962        let size = calculate_expr_size_inner(&expr);
1963        assert!(size > 0);
1964    }
1965
1966    #[test]
1967    fn test_expr_size_tuple() {
1968        let expr = HirExpr::Tuple(vec![
1969            HirExpr::Literal(Literal::Int(1)),
1970            HirExpr::Literal(Literal::String("a".to_string())),
1971        ]);
1972        let size = calculate_expr_size_inner(&expr);
1973        assert!(size >= 2);
1974    }
1975
1976    #[test]
1977    fn test_expr_size_binary_complex() {
1978        let expr = HirExpr::Binary {
1979            left: Box::new(HirExpr::Binary {
1980                left: Box::new(HirExpr::Var("a".to_string())),
1981                op: BinOp::Add,
1982                right: Box::new(HirExpr::Var("b".to_string())),
1983            }),
1984            op: BinOp::Mul,
1985            right: Box::new(HirExpr::Var("c".to_string())),
1986        };
1987        let size = calculate_expr_size_inner(&expr);
1988        assert!(size >= 3);
1989    }
1990
1991    #[test]
1992    fn test_expr_size_call_with_args() {
1993        let expr = HirExpr::Call {
1994            func: "func".to_string(),
1995            args: vec![
1996                HirExpr::Literal(Literal::Int(1)),
1997                HirExpr::Literal(Literal::Int(2)),
1998            ],
1999            kwargs: vec![],
2000        };
2001        let size = calculate_expr_size_inner(&expr);
2002        assert!(size >= 2);
2003    }
2004
2005    #[test]
2006    fn test_expr_size_method_with_args() {
2007        let expr = HirExpr::MethodCall {
2008            object: Box::new(HirExpr::Var("obj".to_string())),
2009            method: "method".to_string(),
2010            args: vec![HirExpr::Literal(Literal::Int(1))],
2011            kwargs: vec![],
2012        };
2013        let size = calculate_expr_size_inner(&expr);
2014        assert!(size >= 1);
2015    }
2016
2017    // --- Nested loop detection ---
2018    #[test]
2019    fn test_contains_loops_nested_for_in_while() {
2020        let body = vec![HirStmt::While {
2021            condition: HirExpr::Literal(Literal::Bool(true)),
2022            body: vec![HirStmt::For {
2023                target: AssignTarget::Symbol("i".to_string()),
2024                iter: HirExpr::Call {
2025                    func: "range".to_string(),
2026                    args: vec![HirExpr::Literal(Literal::Int(10))],
2027                    kwargs: vec![],
2028                },
2029                body: vec![],
2030            }],
2031        }];
2032        assert!(contains_loops_inner(&body));
2033    }
2034
2035    #[test]
2036    fn test_contains_loops_in_while_body() {
2037        let body = vec![HirStmt::While {
2038            condition: HirExpr::Literal(Literal::Bool(true)),
2039            body: vec![HirStmt::While {
2040                condition: HirExpr::Literal(Literal::Bool(false)),
2041                body: vec![],
2042            }],
2043        }];
2044        assert!(contains_loops_inner(&body));
2045    }
2046
2047    #[test]
2048    fn test_contains_loops_in_if_then() {
2049        let body = vec![HirStmt::If {
2050            condition: HirExpr::Literal(Literal::Bool(true)),
2051            then_body: vec![HirStmt::While {
2052                condition: HirExpr::Literal(Literal::Bool(true)),
2053                body: vec![HirStmt::Break { label: None }],
2054            }],
2055            else_body: None,
2056        }];
2057        assert!(contains_loops_inner(&body));
2058    }
2059
2060    #[test]
2061    fn test_contains_loops_in_if_else() {
2062        let body = vec![HirStmt::If {
2063            condition: HirExpr::Literal(Literal::Bool(true)),
2064            then_body: vec![],
2065            else_body: Some(vec![HirStmt::For {
2066                target: AssignTarget::Symbol("i".to_string()),
2067                iter: HirExpr::List(vec![]),
2068                body: vec![],
2069            }]),
2070        }];
2071        assert!(contains_loops_inner(&body));
2072    }
2073
2074    // --- Return count tests ---
2075    #[test]
2076    fn test_count_returns_multiple() {
2077        let body = vec![HirStmt::If {
2078            condition: HirExpr::Literal(Literal::Bool(true)),
2079            then_body: vec![HirStmt::Return(Some(HirExpr::Literal(Literal::Int(1))))],
2080            else_body: Some(vec![HirStmt::Return(Some(HirExpr::Literal(Literal::Int(
2081                2,
2082            ))))]),
2083        }];
2084        let count = count_returns_inner(&body);
2085        assert_eq!(count, 2);
2086    }
2087
2088    #[test]
2089    fn test_count_returns_in_while() {
2090        let body = vec![HirStmt::While {
2091            condition: HirExpr::Literal(Literal::Bool(true)),
2092            body: vec![HirStmt::Return(Some(HirExpr::Literal(Literal::Int(1))))],
2093        }];
2094        let count = count_returns_inner(&body);
2095        assert_eq!(count, 1);
2096    }
2097
2098    #[test]
2099    fn test_count_returns_in_loop() {
2100        let body = vec![HirStmt::For {
2101            target: AssignTarget::Symbol("i".to_string()),
2102            iter: HirExpr::List(vec![]),
2103            body: vec![HirStmt::Return(Some(HirExpr::Var("i".to_string())))],
2104        }];
2105        let count = count_returns_inner(&body);
2106        assert_eq!(count, 1);
2107    }
2108
2109    // --- Inlining decision tests ---
2110    #[test]
2111    fn test_inlining_decision_too_large() {
2112        let decision = InliningDecision {
2113            should_inline: false,
2114            reason: InliningReason::TooLarge,
2115            cost_benefit: 0.0,
2116        };
2117        assert!(!decision.should_inline);
2118        assert!(matches!(decision.reason, InliningReason::TooLarge));
2119    }
2120
2121    #[test]
2122    fn test_inlining_decision_contains_loops() {
2123        let decision = InliningDecision {
2124            should_inline: false,
2125            reason: InliningReason::ContainsLoops,
2126            cost_benefit: 0.0,
2127        };
2128        assert!(!decision.should_inline);
2129        assert!(matches!(decision.reason, InliningReason::ContainsLoops));
2130    }
2131
2132    #[test]
2133    fn test_inlining_decision_cost_too_high() {
2134        let decision = InliningDecision {
2135            should_inline: false,
2136            reason: InliningReason::CostTooHigh,
2137            cost_benefit: -0.5,
2138        };
2139        assert!(!decision.should_inline);
2140        assert!(decision.cost_benefit < 0.0);
2141    }
2142
2143    // --- Apply inlining tests ---
2144    #[test]
2145    fn test_apply_inlining_no_changes() {
2146        let analyzer = InliningAnalyzer::new(InliningConfig::default());
2147        let program = HirProgram {
2148            functions: vec![create_simple_function("main", 5)],
2149            classes: vec![],
2150            imports: vec![],
2151        };
2152        let decisions = std::collections::HashMap::new();
2153        let result = analyzer.apply_inlining(program.clone(), &decisions);
2154        assert_eq!(result.functions.len(), program.functions.len());
2155    }
2156
2157    // --- Expression transformation tests ---
2158    #[test]
2159    fn test_transform_expr_literal() {
2160        let expr = HirExpr::Literal(Literal::Int(42));
2161        let result = transform_expr_for_inlining_inner(&expr, &[]);
2162        assert!(matches!(result, HirExpr::Literal(Literal::Int(42))));
2163    }
2164
2165    #[test]
2166    fn test_transform_expr_var_no_param_match() {
2167        let expr = HirExpr::Var("x".to_string());
2168        let params = vec![HirParam::new("y".to_string(), Type::Int)];
2169        let result = transform_expr_for_inlining_inner(&expr, &params);
2170        assert!(matches!(result, HirExpr::Var(ref name) if name == "x"));
2171    }
2172
2173    #[test]
2174    fn test_transform_expr_binary() {
2175        let expr = HirExpr::Binary {
2176            left: Box::new(HirExpr::Literal(Literal::Int(1))),
2177            op: BinOp::Add,
2178            right: Box::new(HirExpr::Literal(Literal::Int(2))),
2179        };
2180        let result = transform_expr_for_inlining_inner(&expr, &[]);
2181        assert!(matches!(result, HirExpr::Binary { .. }));
2182    }
2183
2184    #[test]
2185    fn test_transform_expr_call() {
2186        let expr = HirExpr::Call {
2187            func: "foo".to_string(),
2188            args: vec![HirExpr::Literal(Literal::Int(1))],
2189            kwargs: vec![],
2190        };
2191        let result = transform_expr_for_inlining_inner(&expr, &[]);
2192        assert!(matches!(result, HirExpr::Call { .. }));
2193    }
2194
2195    // --- Extract calls from various expression types ---
2196    #[test]
2197    fn test_extract_calls_from_nested_call() {
2198        let expr = HirExpr::Call {
2199            func: "outer".to_string(),
2200            args: vec![HirExpr::Call {
2201                func: "inner".to_string(),
2202                args: vec![],
2203                kwargs: vec![],
2204            }],
2205            kwargs: vec![],
2206        };
2207        let mut calls = std::collections::HashSet::new();
2208        extract_calls_from_expr_inner(&expr, &mut calls);
2209        assert!(calls.contains("outer"));
2210        assert!(calls.contains("inner"));
2211    }
2212
2213    #[test]
2214    fn test_extract_calls_from_method_with_call_arg() {
2215        let expr = HirExpr::MethodCall {
2216            object: Box::new(HirExpr::Var("obj".to_string())),
2217            method: "process".to_string(),
2218            args: vec![HirExpr::Call {
2219                func: "get_value".to_string(),
2220                args: vec![],
2221                kwargs: vec![],
2222            }],
2223            kwargs: vec![],
2224        };
2225        let mut calls = std::collections::HashSet::new();
2226        extract_calls_from_expr_inner(&expr, &mut calls);
2227        assert!(calls.contains("get_value"));
2228    }
2229
2230    #[test]
2231    fn test_extract_calls_from_binary_with_calls() {
2232        let expr = HirExpr::Binary {
2233            left: Box::new(HirExpr::Call {
2234                func: "left_fn".to_string(),
2235                args: vec![],
2236                kwargs: vec![],
2237            }),
2238            op: BinOp::Add,
2239            right: Box::new(HirExpr::Call {
2240                func: "right_fn".to_string(),
2241                args: vec![],
2242                kwargs: vec![],
2243            }),
2244        };
2245        let mut calls = std::collections::HashSet::new();
2246        extract_calls_from_expr_inner(&expr, &mut calls);
2247        assert!(calls.contains("left_fn"));
2248        assert!(calls.contains("right_fn"));
2249    }
2250
2251    #[test]
2252    fn test_extract_calls_from_list_with_call() {
2253        let expr = HirExpr::List(vec![HirExpr::Call {
2254            func: "get_item".to_string(),
2255            args: vec![],
2256            kwargs: vec![],
2257        }]);
2258        let mut calls = std::collections::HashSet::new();
2259        extract_calls_from_expr_inner(&expr, &mut calls);
2260        assert!(calls.contains("get_item"));
2261    }
2262
2263    // --- Statement size calculation ---
2264    #[test]
2265    fn test_stmt_size_assign() {
2266        let analyzer = InliningAnalyzer::new(InliningConfig::default());
2267        let stmt = HirStmt::Assign {
2268            target: AssignTarget::Symbol("x".to_string()),
2269            value: HirExpr::Literal(Literal::Int(1)),
2270            type_annotation: None,
2271        };
2272        let size = analyzer.calculate_stmt_size(&stmt);
2273        assert!(size > 0);
2274    }
2275
2276    #[test]
2277    fn test_stmt_size_if() {
2278        let analyzer = InliningAnalyzer::new(InliningConfig::default());
2279        let stmt = HirStmt::If {
2280            condition: HirExpr::Literal(Literal::Bool(true)),
2281            then_body: vec![HirStmt::Return(Some(HirExpr::Literal(Literal::Int(1))))],
2282            else_body: Some(vec![HirStmt::Return(Some(HirExpr::Literal(Literal::Int(
2283                2,
2284            ))))]),
2285        };
2286        let size = analyzer.calculate_stmt_size(&stmt);
2287        assert!(size >= 3);
2288    }
2289
2290    #[test]
2291    fn test_stmt_size_while() {
2292        let analyzer = InliningAnalyzer::new(InliningConfig::default());
2293        let stmt = HirStmt::While {
2294            condition: HirExpr::Literal(Literal::Bool(true)),
2295            body: vec![HirStmt::Break { label: None }],
2296        };
2297        let size = analyzer.calculate_stmt_size(&stmt);
2298        assert!(size >= 2);
2299    }
2300
2301    #[test]
2302    fn test_stmt_size_for() {
2303        let analyzer = InliningAnalyzer::new(InliningConfig::default());
2304        let stmt = HirStmt::For {
2305            target: AssignTarget::Symbol("i".to_string()),
2306            iter: HirExpr::List(vec![]),
2307            body: vec![],
2308        };
2309        let size = analyzer.calculate_stmt_size(&stmt);
2310        assert!(size >= 1);
2311    }
2312
2313    #[test]
2314    fn test_stmt_size_try() {
2315        let analyzer = InliningAnalyzer::new(InliningConfig::default());
2316        let stmt = HirStmt::Try {
2317            body: vec![HirStmt::Return(Some(HirExpr::Literal(Literal::Int(1))))],
2318            handlers: vec![],
2319            orelse: None,
2320            finalbody: None,
2321        };
2322        let size = analyzer.calculate_stmt_size(&stmt);
2323        assert!(size >= 1);
2324    }
2325
2326    // --- Side effect detection in statements ---
2327    #[test]
2328    fn test_side_effect_in_assign() {
2329        let analyzer = InliningAnalyzer::new(InliningConfig::default());
2330        let stmt = HirStmt::Assign {
2331            target: AssignTarget::Symbol("x".to_string()),
2332            value: HirExpr::Call {
2333                func: "print".to_string(),
2334                args: vec![],
2335                kwargs: vec![],
2336            },
2337            type_annotation: None,
2338        };
2339        assert!(analyzer.stmt_has_side_effects(&stmt));
2340    }
2341
2342    #[test]
2343    fn test_side_effect_in_raise() {
2344        let analyzer = InliningAnalyzer::new(InliningConfig::default());
2345        let stmt = HirStmt::Raise {
2346            exception: Some(HirExpr::Call {
2347                func: "ValueError".to_string(),
2348                args: vec![HirExpr::Literal(Literal::String("error".to_string()))],
2349                kwargs: vec![],
2350            }),
2351            cause: None,
2352        };
2353        assert!(analyzer.stmt_has_side_effects(&stmt));
2354    }
2355
2356    // --- Config variations ---
2357    #[test]
2358    fn test_config_no_inline_single_use() {
2359        let config = InliningConfig {
2360            inline_single_use: false,
2361            ..Default::default()
2362        };
2363        assert!(!config.inline_single_use);
2364    }
2365
2366    #[test]
2367    fn test_config_no_inline_trivial() {
2368        let config = InliningConfig {
2369            inline_trivial: false,
2370            ..Default::default()
2371        };
2372        assert!(!config.inline_trivial);
2373    }
2374
2375    #[test]
2376    fn test_config_inline_loops() {
2377        let config = InliningConfig {
2378            inline_loops: true,
2379            ..Default::default()
2380        };
2381        assert!(config.inline_loops);
2382    }
2383
2384    #[test]
2385    fn test_config_high_cost_threshold() {
2386        let config = InliningConfig {
2387            cost_threshold: 10.0,
2388            ..Default::default()
2389        };
2390        assert_eq!(config.cost_threshold, 10.0);
2391    }
2392
2393    // --- Function metrics ---
2394    #[test]
2395    fn test_function_metrics_new() {
2396        let metrics = FunctionMetrics {
2397            size: 10,
2398            _param_count: 2,
2399            _return_count: 1,
2400            has_loops: false,
2401            has_side_effects: false,
2402            is_trivial: false,
2403            call_count: 5,
2404            cost: 2.5,
2405        };
2406        assert_eq!(metrics.size, 10);
2407        assert_eq!(metrics.call_count, 5);
2408        assert_eq!(metrics.cost, 2.5);
2409        assert!(!metrics.has_loops);
2410    }
2411
2412    // --- Empty body handling ---
2413    #[test]
2414    fn test_empty_function_body() {
2415        let func = HirFunction {
2416            name: "empty".to_string(),
2417            params: smallvec![],
2418            ret_type: Type::None,
2419            body: vec![],
2420            properties: FunctionProperties::default(),
2421            annotations: Default::default(),
2422            docstring: None,
2423        };
2424        let analyzer = InliningAnalyzer::new(InliningConfig::default());
2425        let size = analyzer.calculate_function_size(&func);
2426        assert_eq!(size, 0);
2427    }
2428
2429    // --- Complex call graph ---
2430    #[test]
2431    fn test_mutual_recursion() {
2432        let mut analyzer = InliningAnalyzer::new(InliningConfig::default());
2433        let program = HirProgram {
2434            functions: vec![
2435                HirFunction {
2436                    name: "even".to_string(),
2437                    params: smallvec![HirParam::new("n".to_string(), Type::Int)],
2438                    ret_type: Type::Bool,
2439                    body: vec![HirStmt::Return(Some(HirExpr::Call {
2440                        func: "odd".to_string(),
2441                        args: vec![HirExpr::Var("n".to_string())],
2442                        kwargs: vec![],
2443                    }))],
2444                    properties: FunctionProperties::default(),
2445                    annotations: Default::default(),
2446                    docstring: None,
2447                },
2448                HirFunction {
2449                    name: "odd".to_string(),
2450                    params: smallvec![HirParam::new("n".to_string(), Type::Int)],
2451                    ret_type: Type::Bool,
2452                    body: vec![HirStmt::Return(Some(HirExpr::Call {
2453                        func: "even".to_string(),
2454                        args: vec![HirExpr::Var("n".to_string())],
2455                        kwargs: vec![],
2456                    }))],
2457                    properties: FunctionProperties::default(),
2458                    annotations: Default::default(),
2459                    docstring: None,
2460                },
2461            ],
2462            classes: vec![],
2463            imports: vec![],
2464        };
2465        let decisions = analyzer.analyze_program(&program);
2466        // Should detect mutual recursion
2467        assert!(!decisions.is_empty());
2468    }
2469}