//! 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))); } }