diff --git a/src/mir/loop_pattern_detection/condition_var_analyzer.rs b/src/mir/loop_pattern_detection/condition_var_analyzer.rs new file mode 100644 index 00000000..557939d2 --- /dev/null +++ b/src/mir/loop_pattern_detection/condition_var_analyzer.rs @@ -0,0 +1,316 @@ +//! Phase 170-D-impl-2: Condition Variable Analyzer Box +//! +//! Pure functions for analyzing condition AST nodes and determining +//! variable scopes based on LoopScopeShape information. +//! +//! This Box extracts all variable references from condition expressions +//! and determines whether they are from the loop parameter, outer scope, +//! or loop body scope. +//! +//! # Design Philosophy +//! +//! - **Pure functions**: No side effects, only analysis +//! - **Composable**: Can be used independently or as part of LoopConditionScopeBox +//! - **Fail-Fast**: Defaults to conservative classification (LoopBodyLocal) + +use crate::ast::ASTNode; +use crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape; +use crate::mir::BasicBlockId; +use std::collections::HashSet; + +/// Extract all variable names from an AST expression +/// +/// Recursively traverses the AST node and collects all Variable references. +/// Handles: Variables, UnaryOp, BinaryOp, MethodCall, FieldAccess, Index, If +/// +/// # Arguments +/// +/// * `node` - AST node to analyze +/// +/// # Returns +/// +/// HashSet of all variable names found in the expression +/// +/// # Example +/// +/// For expression `(i < 10) && (ch != ' ')`, returns `{"i", "ch"}` +pub fn extract_all_variables(node: &ASTNode) -> HashSet { + let mut vars = HashSet::new(); + extract_vars_recursive(node, &mut vars); + vars +} + +/// Internal recursive helper for variable extraction +fn extract_vars_recursive(node: &ASTNode, vars: &mut HashSet) { + match node { + ASTNode::Variable { name, .. } => { + vars.insert(name.clone()); + } + ASTNode::UnaryOp { operand, .. } => { + extract_vars_recursive(operand, vars); + } + ASTNode::BinaryOp { left, right, .. } => { + extract_vars_recursive(left, vars); + extract_vars_recursive(right, vars); + } + ASTNode::MethodCall { + object, arguments, .. + } => { + extract_vars_recursive(object, vars); + for arg in arguments { + extract_vars_recursive(arg, vars); + } + } + ASTNode::FieldAccess { object, .. } => { + extract_vars_recursive(object, vars); + } + ASTNode::Index { target, index, .. } => { + extract_vars_recursive(target, vars); + extract_vars_recursive(index, vars); + } + ASTNode::If { + condition, + then_body, + else_body, + .. + } => { + extract_vars_recursive(condition, vars); + for stmt in then_body { + extract_vars_recursive(stmt, vars); + } + if let Some(else_body) = else_body { + for stmt in else_body { + extract_vars_recursive(stmt, vars); + } + } + } + _ => {} // Skip literals, constants, etc. + } +} + +/// Determine if a variable is from the outer scope +/// +/// # Phase 170-D-impl-2: Simple heuristic +/// +/// A variable is "outer local" if: +/// 1. It appears in LoopScopeShape.pinned (loop parameters or outer variables) +/// 2. It appears in LoopScopeShape.variable_definitions as being defined +/// in the header block (NOT in body/latch/exit) +/// +/// # Arguments +/// +/// * `var_name` - Name of the variable to check +/// * `scope` - Optional LoopScopeShape with variable definition information +/// +/// # Returns +/// +/// - `true` if variable is definitively from outer scope +/// - `false` if unknown, from body scope, or no scope info available +/// +/// # Notes +/// +/// This is a simplified implementation for Phase 170-D. +/// Future versions may include: +/// - Dominance tree analysis +/// - More sophisticated scope inference +pub fn is_outer_scope_variable( + var_name: &str, + scope: Option<&LoopScopeShape>, +) -> bool { + match scope { + None => false, // No scope info → assume body-local + Some(scope) => { + // Check 1: Is it a pinned variable (loop parameter or passed-in)? + if scope.pinned.contains(var_name) { + return true; + } + + // Check 2: Does it appear in variable_definitions and is it from header only? + if let Some(def_blocks) = scope.variable_definitions.get(var_name) { + // Simplified heuristic: if defined ONLY in header block → outer scope + // Phase 170-D: This is conservative - we only accept variables + // that are EXCLUSIVELY defined in the header (before loop enters) + if def_blocks.len() == 1 && def_blocks.contains(&scope.header) { + return true; + } + } + + false + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // Helper: Create a Variable node + fn var_node(name: &str) -> ASTNode { + ASTNode::Variable { + name: name.to_string(), + span: crate::ast::Span::unknown(), + } + } + + // Helper: Create a BinaryOp node + fn binop_node(left: ASTNode, right: ASTNode) -> ASTNode { + ASTNode::BinaryOp { + operator: crate::ast::BinaryOperator::Add, // Placeholder operator + left: Box::new(left), + right: Box::new(right), + span: crate::ast::Span::unknown(), + } + } + + // Helper: Create a UnaryOp node + fn unary_node(operand: ASTNode) -> ASTNode { + ASTNode::UnaryOp { + operator: crate::ast::UnaryOperator::Not, + operand: Box::new(operand), + span: crate::ast::Span::unknown(), + } + } + + #[test] + fn test_extract_single_variable() { + let node = var_node("x"); + let vars = extract_all_variables(&node); + assert_eq!(vars.len(), 1); + assert!(vars.contains("x")); + } + + #[test] + fn test_extract_multiple_variables() { + let node = binop_node(var_node("x"), var_node("y")); + let vars = extract_all_variables(&node); + assert_eq!(vars.len(), 2); + assert!(vars.contains("x")); + assert!(vars.contains("y")); + } + + #[test] + fn test_extract_deduplicated_variables() { + let node = binop_node(var_node("x"), var_node("x")); + let vars = extract_all_variables(&node); + assert_eq!(vars.len(), 1); // HashSet deduplicates + assert!(vars.contains("x")); + } + + #[test] + fn test_extract_nested_variables() { + // Create (x + y) + z structure + let inner = binop_node(var_node("x"), var_node("y")); + let outer = binop_node(inner, var_node("z")); + let vars = extract_all_variables(&outer); + + assert_eq!(vars.len(), 3); + assert!(vars.contains("x")); + assert!(vars.contains("y")); + assert!(vars.contains("z")); + } + + #[test] + fn test_extract_with_unary_op() { + // Create !(x) structure + let node = unary_node(var_node("x")); + let vars = extract_all_variables(&node); + + assert_eq!(vars.len(), 1); + assert!(vars.contains("x")); + } + + #[test] + fn test_extract_no_variables_from_literal() { + let node = ASTNode::Literal { + value: crate::ast::LiteralValue::Integer(42), + span: crate::ast::Span::unknown(), + }; + let vars = extract_all_variables(&node); + + assert!(vars.is_empty()); + } + + #[test] + fn test_is_outer_scope_variable_with_no_scope() { + let result = is_outer_scope_variable("x", None); + assert!(!result); // No scope → assume body-local + } + + #[test] + fn test_is_outer_scope_variable_pinned() { + use std::collections::{BTreeMap, BTreeSet}; + + let mut pinned = BTreeSet::new(); + pinned.insert("len".to_string()); + + let scope = LoopScopeShape { + header: BasicBlockId(0), + body: BasicBlockId(1), + latch: BasicBlockId(2), + exit: BasicBlockId(3), + pinned, + carriers: BTreeSet::new(), + body_locals: BTreeSet::new(), + exit_live: BTreeSet::new(), + progress_carrier: None, + variable_definitions: BTreeMap::new(), + }; + + assert!(is_outer_scope_variable("len", Some(&scope))); + assert!(!is_outer_scope_variable("unknown", Some(&scope))); + } + + #[test] + fn test_is_outer_scope_variable_from_header_only() { + use std::collections::{BTreeMap, BTreeSet}; + + let mut variable_definitions = BTreeMap::new(); + let mut header_only = BTreeSet::new(); + header_only.insert(BasicBlockId(0)); // Only in header + + variable_definitions.insert("start".to_string(), header_only); + + let scope = LoopScopeShape { + header: BasicBlockId(0), + body: BasicBlockId(1), + latch: BasicBlockId(2), + exit: BasicBlockId(3), + pinned: BTreeSet::new(), + carriers: BTreeSet::new(), + body_locals: BTreeSet::new(), + exit_live: BTreeSet::new(), + progress_carrier: None, + variable_definitions, + }; + + assert!(is_outer_scope_variable("start", Some(&scope))); + } + + #[test] + fn test_is_outer_scope_variable_from_body() { + use std::collections::{BTreeMap, BTreeSet}; + + let mut variable_definitions = BTreeMap::new(); + let mut header_and_body = BTreeSet::new(); + header_and_body.insert(BasicBlockId(0)); // header + header_and_body.insert(BasicBlockId(1)); // body + + variable_definitions.insert("ch".to_string(), header_and_body); + + let scope = LoopScopeShape { + header: BasicBlockId(0), + body: BasicBlockId(1), + latch: BasicBlockId(2), + exit: BasicBlockId(3), + pinned: BTreeSet::new(), + carriers: BTreeSet::new(), + body_locals: BTreeSet::new(), + exit_live: BTreeSet::new(), + progress_carrier: None, + variable_definitions, + }; + + // Variable defined in body (in addition to header) → NOT outer-only + assert!(!is_outer_scope_variable("ch", Some(&scope))); + } +} diff --git a/src/mir/loop_pattern_detection/loop_condition_scope.rs b/src/mir/loop_pattern_detection/loop_condition_scope.rs index 7514100c..b8f4391a 100644 --- a/src/mir/loop_pattern_detection/loop_condition_scope.rs +++ b/src/mir/loop_pattern_detection/loop_condition_scope.rs @@ -91,6 +91,14 @@ impl LoopConditionScopeBox { /// # Returns /// /// LoopConditionScope with classified variables + /// + /// # Algorithm + /// + /// 1. Extract all variables from condition AST nodes using condition_var_analyzer + /// 2. Classify each variable: + /// - If matches loop_param_name → LoopParam + /// - Else if in outer scope (via condition_var_analyzer) → OuterLocal + /// - Else → LoopBodyLocal (conservative default) pub fn analyze( loop_param_name: &str, condition_nodes: &[&ASTNode], @@ -99,16 +107,17 @@ impl LoopConditionScopeBox { let mut result = LoopConditionScope::new(); let mut found_vars = HashSet::new(); - // Extract variable names from all condition nodes + // Phase 170-D-impl-2: Use condition_var_analyzer for extraction for node in condition_nodes { - Self::extract_vars(node, &mut found_vars); + let vars = super::condition_var_analyzer::extract_all_variables(node); + found_vars.extend(vars); } // Classify each variable for var_name in found_vars { let var_scope = if var_name == loop_param_name { CondVarScope::LoopParam - } else if Self::is_outer_local(&var_name, scope) { + } else if super::condition_var_analyzer::is_outer_scope_variable(&var_name, scope) { CondVarScope::OuterLocal } else { // Default: assume it's loop-body-local if not identified as outer @@ -120,68 +129,6 @@ impl LoopConditionScopeBox { result } - - /// Recursively extract variable names from an AST node - fn extract_vars(node: &ASTNode, vars: &mut HashSet) { - match node { - ASTNode::Variable { name, .. } => { - vars.insert(name.clone()); - } - ASTNode::UnaryOp { operand, .. } => { - Self::extract_vars(operand, vars); - } - ASTNode::BinaryOp { - left, right, .. - } => { - Self::extract_vars(left, vars); - Self::extract_vars(right, vars); - } - ASTNode::MethodCall { - object, arguments, .. - } => { - Self::extract_vars(object, vars); - for arg in arguments { - Self::extract_vars(arg, vars); - } - } - ASTNode::FieldAccess { object, .. } => { - Self::extract_vars(object, vars); - } - ASTNode::Index { target, index, .. } => { - Self::extract_vars(target, vars); - Self::extract_vars(index, vars); - } - ASTNode::If { - condition, - then_body, - else_body, - .. - } => { - Self::extract_vars(condition, vars); - for stmt in then_body { - Self::extract_vars(stmt, vars); - } - if let Some(else_body) = else_body { - for stmt in else_body { - Self::extract_vars(stmt, vars); - } - } - } - _ => {} // Skip other node types for now - } - } - - /// Check if a variable is from outer scope - /// - /// Phase 170-D: Simplified heuristic - just check if LoopScopeShape exists - /// and has external variable definitions. Full implementation would track - /// variable_definitions in the LoopScopeShape. - fn is_outer_local(_var_name: &str, _scope: Option<&LoopScopeShape>) -> bool { - // Phase 170-D: Simplified implementation - // Real implementation would check variable_definitions from LoopScopeShape - // For now, we rely on the default fallback to LoopBodyLocal - false - } } #[cfg(test)] diff --git a/src/mir/loop_pattern_detection/mod.rs b/src/mir/loop_pattern_detection/mod.rs index 22948532..39caeea7 100644 --- a/src/mir/loop_pattern_detection/mod.rs +++ b/src/mir/loop_pattern_detection/mod.rs @@ -752,5 +752,6 @@ mod tests { } } -// Phase 170-D: Loop Condition Scope Analysis Box +// Phase 170-D: Loop Condition Scope Analysis Boxes pub mod loop_condition_scope; +pub mod condition_var_analyzer;