Files
hakorune/src/mir/join_ir/lowering/loop_update_summary.rs
nyash-codex f0536fa330 feat(joinir): Phase 222-2 ConditionPatternBox normalization implementation
Phase 222: If Condition Normalization - Part 2
Goal: Support '0 < i', 'i > j' patterns in addition to 'i > 0'

Changes:
1. condition_pattern.rs (+160 lines):
   - Added ConditionValue enum (Variable | Literal)
   - Added NormalizedCondition struct (left_var, op, right)
   - Added flip_compare_op() for operator reversal
   - Added binary_op_to_compare_op() converter
   - Added normalize_comparison() main normalization function
   - Extended analyze_condition_pattern() to accept 3 cases:
     * Phase 219: var CmpOp literal (e.g., i > 0)
     * Phase 222: literal CmpOp var (e.g., 0 < i) → normalized
     * Phase 222: var CmpOp var (e.g., i > j)
   - Added 9 unit tests (all passing)

2. loop_update_summary.rs (cleanup):
   - Commented out obsolete test_typical_index_names
   - Function is_typical_index_name() was removed in earlier phase

Test results:
- 7 normalization tests: PASS 
- 2 pattern analysis tests: PASS 

Next: Phase 222-3 - integrate normalization into is_if_sum_pattern()
Status: Ready for integration
2025-12-10 09:18:21 +09:00

478 lines
16 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! 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<crate::ast::ASTNode>,
/// 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<crate::ast::ASTNode>,
}
/// ループ全体の更新サマリ
///
/// Phase 170-C-2: CaseALoweringShape の判定入力として使用
#[derive(Debug, Clone, Default)]
pub struct LoopUpdateSummary {
/// 各キャリアの更新情報
pub carriers: Vec<CarrierUpdateInfo>,
}
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<String> {
use crate::ast::ASTNode;
let mut assigned = std::collections::HashSet::new();
fn visit_node(node: &ASTNode, assigned: &mut std::collections::HashSet<String>) {
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
}
}