//! Phase 170-C-2: LoopUpdateSummary - ループ更新パターン解析 //! //! キャリア変数の更新パターン(CounterLike / AccumulationLike)を判定し、 //! CaseALoweringShape の検出精度を向上させる。 //! //! ## 設計思想 //! //! - 責務: AST のループ body から「各キャリアがどう更新されているか」を判定 //! - 差し替え可能: 名前ヒューリスティック → AST 解析 → MIR 解析と段階的に精度向上 //! - LoopFeatures / CaseALoweringShape から独立したモジュール //! //! ## 使用例 //! //! ```ignore //! let summary = analyze_loop_updates(&carrier_names); //! if summary.has_single_counter() { //! // StringExamination パターン //! } //! ``` /// キャリア変数の更新パターン /// /// Phase 170-C-2: 3種類のパターンを区別 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum UpdateKind { /// カウンタ系: i = i + 1, i = i - 1, i += 1 /// /// 典型的な skip/trim パターン。進捗変数として使われる。 CounterLike, /// 蓄積系: result = result + x, arr.push(x), list.append(x) /// /// 典型的な collect/filter パターン。結果を蓄積する変数。 AccumulationLike, /// 判定不能 /// /// 複雑な更新パターン、または解析できなかった場合。 Other, } impl UpdateKind { /// デバッグ用の名前を返す pub fn name(&self) -> &'static str { match self { UpdateKind::CounterLike => "CounterLike", UpdateKind::AccumulationLike => "AccumulationLike", UpdateKind::Other => "Other", } } } /// 単一キャリアの更新情報 #[derive(Debug, Clone)] pub struct CarrierUpdateInfo { /// キャリア変数名 pub name: String, /// 更新パターン pub kind: UpdateKind, /// Phase 213: Then branch update expression (for Pattern 3 if-sum) /// e.g., for "if (cond) { sum = sum + 1 }", then_expr is "sum + 1" #[allow(dead_code)] pub then_expr: Option, /// Phase 213: Else branch update expression (for Pattern 3 if-sum) /// e.g., for "else { sum = sum + 0 }", else_expr is "sum + 0" /// If no else branch, this can be the identity update (e.g., "sum") #[allow(dead_code)] pub else_expr: Option, } /// ループ全体の更新サマリ /// /// Phase 170-C-2: CaseALoweringShape の判定入力として使用 #[derive(Debug, Clone, Default)] pub struct LoopUpdateSummary { /// 各キャリアの更新情報 pub carriers: Vec, } impl LoopUpdateSummary { /// 空のサマリを作成 pub fn empty() -> Self { Self { carriers: vec![] } } /// 単一 CounterLike キャリアを持つか /// /// StringExamination パターンの判定に使用 pub fn has_single_counter(&self) -> bool { self.carriers.len() == 1 && self.carriers[0].kind == UpdateKind::CounterLike } /// AccumulationLike キャリアを含むか /// /// ArrayAccumulation パターンの判定に使用 pub fn has_accumulation(&self) -> bool { self.carriers .iter() .any(|c| c.kind == UpdateKind::AccumulationLike) } /// CounterLike キャリアの数 pub fn counter_count(&self) -> usize { self.carriers .iter() .filter(|c| c.kind == UpdateKind::CounterLike) .count() } /// AccumulationLike キャリアの数 pub fn accumulation_count(&self) -> usize { self.carriers .iter() .filter(|c| c.kind == UpdateKind::AccumulationLike) .count() } /// Phase 213: Check if this is a simple if-sum pattern /// /// Simple if-sum pattern: /// - Has exactly 1 CounterLike carrier (loop index, e.g., "i") /// - Has exactly 1 AccumulationLike carrier (accumulator, e.g., "sum") /// - Optionally has additional accumulators (e.g., "count") /// /// Examples: /// - `loop(i < len) { if cond { sum = sum + 1 } i = i + 1 }` ✅ /// - `loop(i < len) { if cond { sum = sum + 1; count = count + 1 } i = i + 1 }` ✅ /// - `loop(i < len) { result = result + data[i]; i = i + 1 }` ❌ (no if statement) pub fn is_simple_if_sum_pattern(&self) -> bool { let counter_count = self.counter_count(); let accumulation_count = self.accumulation_count(); // Must have exactly 1 counter (loop index) if counter_count != 1 { return false; } // Must have at least 1 accumulator (sum) if accumulation_count < 1 { return false; } // For now, only support up to 2 accumulators (sum, count) // This matches the Phase 212 if-sum minimal test case if accumulation_count > 2 { return false; } true } } /// Phase 219: Extract all assigned variable names from loop body AST /// /// Returns a set of variable names that are assigned (LHS) in the loop body. /// This prevents phantom carriers from non-assigned variables. fn extract_assigned_variables(loop_body: &[crate::ast::ASTNode]) -> std::collections::HashSet { use crate::ast::ASTNode; let mut assigned = std::collections::HashSet::new(); fn visit_node(node: &ASTNode, assigned: &mut std::collections::HashSet) { match node { // Direct assignment: target = value ASTNode::Assignment { target, value, .. } => { if let ASTNode::Variable { name, .. } = target.as_ref() { assigned.insert(name.clone()); } // Recurse into value (for nested assignments) visit_node(value, assigned); } // If statement: recurse into then/else branches ASTNode::If { then_body, else_body, .. } => { for stmt in then_body { visit_node(stmt, assigned); } if let Some(else_stmts) = else_body { for stmt in else_stmts { visit_node(stmt, assigned); } } } // Loop statement: recurse into body (for nested loops) ASTNode::Loop { body, .. } => { for stmt in body { visit_node(stmt, assigned); } } // Other nodes: no assignment tracking needed _ => {} } } for stmt in loop_body { visit_node(stmt, &mut assigned); } assigned } /// Phase 219: Classify update kind from RHS expression structure /// /// Returns UpdateKind based on RHS pattern, NOT variable name. fn classify_update_kind_from_rhs(rhs: &crate::ast::ASTNode) -> UpdateKind { use crate::ast::{ASTNode, BinaryOperator, LiteralValue}; match rhs { // x = x + 1 → CounterLike // x = x + n → AccumulationLike (where n is not 1) ASTNode::BinaryOp { operator, left, right, .. } => { if matches!(operator, BinaryOperator::Add) { // Check if left is self-reference (will be validated by caller) if matches!(left.as_ref(), ASTNode::Variable { .. }) { // Check right operand if let ASTNode::Literal { value, .. } = right.as_ref() { if let LiteralValue::Integer(n) = value { if *n == 1 { return UpdateKind::CounterLike; // x = x + 1 } else { return UpdateKind::AccumulationLike; // x = x + n } } } else { // x = x + expr (variable accumulation) return UpdateKind::AccumulationLike; } } } UpdateKind::Other } _ => UpdateKind::Other, } } /// Phase 219: Analyze loop updates from loop body AST (assignment-based) /// /// # New Design (Phase 219) /// /// - Takes loop body AST as input (not just carrier names) /// - Only analyzes variables that are ASSIGNED in loop body /// - Uses RHS structure analysis (NOT name heuristics) /// /// # Arguments /// /// * `carrier_names` - Candidate carrier variable names from scope /// * `loop_body` - Loop body AST for assignment detection /// /// # Returns /// /// LoopUpdateSummary with only actually-assigned carriers /// Phase 219: Extract assignment RHS for a given variable /// /// Returns the RHS expression of the first assignment to `var_name` in loop body. fn find_assignment_rhs<'a>(var_name: &str, loop_body: &'a [crate::ast::ASTNode]) -> Option<&'a crate::ast::ASTNode> { use crate::ast::ASTNode; fn visit_node<'a>(var_name: &str, node: &'a ASTNode) -> Option<&'a ASTNode> { match node { ASTNode::Assignment { target, value, .. } => { if let ASTNode::Variable { name, .. } = target.as_ref() { if name == var_name { return Some(value.as_ref()); } } // Recurse into value visit_node(var_name, value) } ASTNode::If { then_body, else_body, .. } => { for stmt in then_body { if let Some(rhs) = visit_node(var_name, stmt) { return Some(rhs); } } if let Some(else_stmts) = else_body { for stmt in else_stmts { if let Some(rhs) = visit_node(var_name, stmt) { return Some(rhs); } } } None } ASTNode::Loop { body, .. } => { for stmt in body { if let Some(rhs) = visit_node(var_name, stmt) { return Some(rhs); } } None } _ => None, } } for stmt in loop_body { if let Some(rhs) = visit_node(var_name, stmt) { return Some(rhs); } } None } /// Phase 219: Check if variable name looks like loop index /// /// Simple heuristic: single-letter names (i, j, k, e) or "index"/"idx" fn is_likely_loop_index(name: &str) -> bool { matches!(name, "i" | "j" | "k" | "e" | "idx" | "index" | "pos" | "n") } pub fn analyze_loop_updates_from_ast( carrier_names: &[String], loop_body: &[crate::ast::ASTNode], ) -> LoopUpdateSummary { // Phase 219-1: Extract assigned variables from loop body let assigned_vars = extract_assigned_variables(loop_body); // Phase 219-2: Filter carriers to only assigned ones and classify by RHS let mut carriers = Vec::new(); for name in carrier_names { if assigned_vars.contains(name) { // Phase 219-3: Classify by variable name + RHS structure // - Loop index-like names (i, j, k) with `x = x + 1` → CounterLike // - Other names with `x = x + 1` or `x = x + expr` → AccumulationLike let kind = if is_likely_loop_index(name) { UpdateKind::CounterLike } else if let Some(rhs) = find_assignment_rhs(name, loop_body) { let classified = classify_update_kind_from_rhs(rhs); match classified { UpdateKind::CounterLike => UpdateKind::AccumulationLike, // Override: non-index + `x=x+1` → accumulation other => other, } } else { UpdateKind::Other }; carriers.push(CarrierUpdateInfo { name: name.clone(), kind, then_expr: None, else_expr: None, }); } } LoopUpdateSummary { carriers } } /// Phase 219: Legacy wrapper for backward compatibility /// /// # Deprecated (Phase 219) /// /// This function uses name-based heuristics and is deprecated. /// Use `analyze_loop_updates_from_ast()` instead. /// /// # Arguments /// /// * `carrier_names` - キャリア変数名のリスト /// /// # Returns /// /// 各キャリアの更新パターンをまとめた LoopUpdateSummary #[deprecated(since = "Phase 219", note = "Use analyze_loop_updates_from_ast() instead")] pub fn analyze_loop_updates(carrier_names: &[String]) -> LoopUpdateSummary { // Phase 219: Fallback to simple heuristic (for legacy call sites) // This will be removed once all call sites are migrated let carriers = carrier_names .iter() .map(|name| CarrierUpdateInfo { name: name.clone(), kind: UpdateKind::AccumulationLike, // Default to accumulation then_expr: None, else_expr: None, }) .collect(); LoopUpdateSummary { carriers } } #[cfg(test)] mod tests { use super::*; #[test] fn test_update_kind_name() { assert_eq!(UpdateKind::CounterLike.name(), "CounterLike"); assert_eq!(UpdateKind::AccumulationLike.name(), "AccumulationLike"); assert_eq!(UpdateKind::Other.name(), "Other"); } // NOTE: Phase 222 - test_typical_index_names commented out // Function is_typical_index_name() was removed in earlier phase // #[test] // fn test_typical_index_names() { // assert!(is_typical_index_name("i")); // assert!(is_typical_index_name("idx")); // assert!(is_typical_index_name("pos")); // assert!(!is_typical_index_name("result")); // assert!(!is_typical_index_name("sum")); // } #[test] fn test_analyze_single_counter() { let names = vec!["i".to_string()]; let summary = analyze_loop_updates(&names); assert!(summary.has_single_counter()); assert!(!summary.has_accumulation()); assert_eq!(summary.counter_count(), 1); } #[test] fn test_analyze_accumulation() { let names = vec!["result".to_string()]; let summary = analyze_loop_updates(&names); assert!(!summary.has_single_counter()); assert!(summary.has_accumulation()); assert_eq!(summary.accumulation_count(), 1); } #[test] fn test_analyze_mixed() { let names = vec!["i".to_string(), "sum".to_string()]; let summary = analyze_loop_updates(&names); assert!(!summary.has_single_counter()); // 2つあるので false assert!(summary.has_accumulation()); assert_eq!(summary.counter_count(), 1); assert_eq!(summary.accumulation_count(), 1); } // Phase 213 tests for is_simple_if_sum_pattern #[test] fn test_is_simple_if_sum_pattern_basic() { // phase212_if_sum_min.hako pattern: i (counter) + sum (accumulator) let names = vec!["i".to_string(), "sum".to_string()]; let summary = analyze_loop_updates(&names); assert!(summary.is_simple_if_sum_pattern()); } #[test] fn test_is_simple_if_sum_pattern_with_count() { // Phase 195 pattern: i (counter) + sum + count (2 accumulators) let names = vec!["i".to_string(), "sum".to_string(), "count".to_string()]; let summary = analyze_loop_updates(&names); assert!(summary.is_simple_if_sum_pattern()); } #[test] fn test_is_simple_if_sum_pattern_no_accumulator() { // Only counter, no accumulator let names = vec!["i".to_string()]; let summary = analyze_loop_updates(&names); assert!(!summary.is_simple_if_sum_pattern()); // No accumulator } #[test] fn test_is_simple_if_sum_pattern_no_counter() { // Only accumulator, no counter let names = vec!["sum".to_string()]; let summary = analyze_loop_updates(&names); assert!(!summary.is_simple_if_sum_pattern()); // No counter } #[test] fn test_is_simple_if_sum_pattern_multiple_counters() { // Multiple counters (not supported) let names = vec!["i".to_string(), "j".to_string(), "sum".to_string()]; let summary = analyze_loop_updates(&names); assert!(!summary.is_simple_if_sum_pattern()); // 2 counters } }