From 8a40b5700a433b0a9dd5bb3ef88b47236cdd88f0 Mon Sep 17 00:00:00 2001 From: nyash-codex Date: Wed, 17 Dec 2025 06:10:38 +0900 Subject: [PATCH] feat(joinir): detect mutable accumulator pattern (AST-only, spec minimal) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Implement MutableAccumulatorAnalyzer in loop_pattern_detection/ - Detect shape: target = target + x (Add only, x ∈ {Var, Literal}) - Does NOT check read-only (delegated to ScopeManager) - Multiple assignments → return None (not our pattern, let other code handle it) - 6 unit tests covering OK/NG cases - Error prefix: [joinir/mutable-acc-spec] 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- src/mir/loop_pattern_detection/mod.rs | 4 + .../mutable_accumulator_analyzer.rs | 417 ++++++++++++++++++ 2 files changed, 421 insertions(+) create mode 100644 src/mir/loop_pattern_detection/mutable_accumulator_analyzer.rs diff --git a/src/mir/loop_pattern_detection/mod.rs b/src/mir/loop_pattern_detection/mod.rs index b24145fd..50238c1e 100644 --- a/src/mir/loop_pattern_detection/mod.rs +++ b/src/mir/loop_pattern_detection/mod.rs @@ -746,4 +746,8 @@ pub mod binding_map_provider; // Phase 100 P1-2: Pinned Local Analyzer pub mod pinned_local_analyzer; + +// Phase 100 P2-1: Mutable Accumulator Analyzer +pub mod mutable_accumulator_analyzer; + pub use binding_map_provider::BindingMapProvider; diff --git a/src/mir/loop_pattern_detection/mutable_accumulator_analyzer.rs b/src/mir/loop_pattern_detection/mutable_accumulator_analyzer.rs new file mode 100644 index 00000000..9bc71fc5 --- /dev/null +++ b/src/mir/loop_pattern_detection/mutable_accumulator_analyzer.rs @@ -0,0 +1,417 @@ +//! Phase 100 P2-1: Mutable Accumulator Analyzer +//! +//! Pure AST box for detecting accumulator patterns (s = s + x form only). +//! This analyzer ONLY detects the shape from AST, without checking: +//! - Whether target is loop-outer or loop-local (delegated to ScopeManager) +//! - Whether RHS is read-only (delegated to ScopeManager) +//! +//! Minimal spec: `target = target + x` where x ∈ {Variable, Literal} + +use crate::ast::{ASTNode, BinaryOperator}; + +/// RHS expression kind in accumulator pattern +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum RhsExprKind { + /// Variable reference (e.g., "ch") + Var, + /// Literal value (e.g., "\"hello\"" or "42") + Literal, +} + +/// Accumulator specification detected from AST +#[derive(Debug, Clone, PartialEq)] +pub struct MutableAccumulatorSpec { + /// Target variable name (e.g., "out") + pub target_name: String, + /// RHS expression kind + pub rhs_expr_kind: RhsExprKind, + /// RHS variable name or literal string representation + pub rhs_var_or_lit: String, + /// Binary operator (currently only Add supported) + pub op: BinaryOperator, +} + +/// Analyzer for detecting accumulator patterns in loop body AST. +/// +/// # Responsibility +/// +/// Detects accumulator **shape** from AST only. Does NOT check: +/// - Whether target is loop-outer or loop-local +/// - Whether RHS is read-only +/// +/// These checks are delegated to Pattern2 (ScopeManager). +pub struct MutableAccumulatorAnalyzer; + +impl MutableAccumulatorAnalyzer { + /// Analyzes loop body AST to detect accumulator pattern. + /// + /// # Arguments + /// + /// * `loop_body` - AST nodes of the loop body + /// + /// # Returns + /// + /// * `Ok(Some(spec))` - Accumulator pattern detected + /// * `Ok(None)` - No accumulator pattern found + /// * `Err(String)` - Pattern violation (Fail-Fast) + /// + /// # Fail-Fast Cases + /// + /// - Multiple assignments to the same variable + /// - Assignment form not accumulator pattern (e.g., reversed operands) + /// - RHS contains function call (not supported) + /// - Operator not Add (other ops not supported yet) + pub fn analyze(loop_body: &[ASTNode]) -> Result, String> { + // Collect all assignments in loop body + let assignments = collect_assignments(loop_body); + + if assignments.is_empty() { + return Ok(None); // No assignments, no accumulator + } + + // Check for multiple assignments to the same variable + // Note: We're looking for ONE accumulator variable with ONE assignment + // If there are multiple assignments to the same variable, it's not the simple + // accumulator pattern we support, so return None (not an error) + let mut assignment_counts = std::collections::BTreeMap::new(); + for (target_name, _, _) in &assignments { + *assignment_counts.entry(target_name.clone()).or_insert(0) += 1; + } + + // If any variable has multiple assignments, this is not the simple accumulator + // pattern we're looking for. Return None to allow other patterns to handle it. + for (_target_name, count) in &assignment_counts { + if *count > 1 { + return Ok(None); // Not our pattern, let other code handle it + } + } + + // Try to find accumulator pattern: target = target + x + for (target_name, value_node, _span) in assignments { + // Check if value is BinaryOp + if let ASTNode::BinaryOp { + operator, + left, + right, + .. + } = value_node + { + // Check operator is Add + if operator != &BinaryOperator::Add { + return Err(format!( + "[joinir/mutable-acc-spec] Operator '{}' not supported (only '+' allowed)", + operator + )); + } + + // Check left operand is Variable with same name as target + if let ASTNode::Variable { name, .. } = left.as_ref() { + if name == &target_name { + // Check RHS is Variable or Literal (no MethodCall) + match right.as_ref() { + ASTNode::Variable { name: rhs_name, .. } => { + return Ok(Some(MutableAccumulatorSpec { + target_name, + rhs_expr_kind: RhsExprKind::Var, + rhs_var_or_lit: rhs_name.clone(), + op: BinaryOperator::Add, + })); + } + ASTNode::Literal { value, .. } => { + return Ok(Some(MutableAccumulatorSpec { + target_name, + rhs_expr_kind: RhsExprKind::Literal, + rhs_var_or_lit: format!("{:?}", value), + op: BinaryOperator::Add, + })); + } + ASTNode::MethodCall { .. } | ASTNode::Call { .. } => { + return Err( + "[joinir/mutable-acc-spec] RHS contains function call (not supported)" + .to_string(), + ); + } + _ => { + return Err(format!( + "[joinir/mutable-acc-spec] RHS expression type not supported: {:?}", + right + )); + } + } + } else { + // Left operand is different variable (reversed operands) + return Err( + "[joinir/mutable-acc-spec] Assignment form not accumulator pattern (required: target = target + x)" + .to_string(), + ); + } + } else { + // Left operand is not Variable + return Err( + "[joinir/mutable-acc-spec] Assignment form not accumulator pattern (required: target = target + x)" + .to_string(), + ); + } + } else { + // Value is not BinaryOp, check if it's a potential mutation + // If the assignment is to a candidate variable, it's a non-accumulator mutation + return Err(format!( + "[joinir/mutable-acc-spec] Assignment to '{}' is not accumulator form (required: target = target + x)", + target_name + )); + } + } + + Ok(None) // No accumulator pattern found + } +} + +/// Collects all assignments in loop body AST. +/// +/// Returns: Vec<(target_name, value_node, span)> +fn collect_assignments<'a>(nodes: &'a [ASTNode]) -> Vec<(String, &'a ASTNode, crate::ast::Span)> { + let mut assignments = Vec::new(); + for node in nodes { + collect_assignments_in_node(node, &mut assignments); + } + assignments +} + +/// Recursively collects assignments from a single AST node. +fn collect_assignments_in_node<'a>( + node: &'a ASTNode, + assignments: &mut Vec<(String, &'a ASTNode, crate::ast::Span)>, +) { + match node { + ASTNode::Assignment { + target, value, span, .. + } => { + if let ASTNode::Variable { name, .. } = target.as_ref() { + assignments.push((name.clone(), value.as_ref(), span.clone())); + } + } + ASTNode::If { + then_body, + else_body, + .. + } => { + for node in then_body { + collect_assignments_in_node(node, assignments); + } + if let Some(else_stmts) = else_body { + for node in else_stmts { + collect_assignments_in_node(node, assignments); + } + } + } + ASTNode::Loop { body, .. } => { + for node in body { + collect_assignments_in_node(node, assignments); + } + } + // Other nodes don't contain assignments + _ => {} + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ast::{LiteralValue, Span}; + + #[test] + fn test_mutable_accumulator_spec_simple() { + // Build AST for: out = out + ch + let loop_body = vec![ASTNode::Assignment { + target: Box::new(ASTNode::Variable { + name: "out".to_string(), + span: Span::unknown(), + }), + value: Box::new(ASTNode::BinaryOp { + operator: BinaryOperator::Add, + left: Box::new(ASTNode::Variable { + name: "out".to_string(), + span: Span::unknown(), + }), + right: Box::new(ASTNode::Variable { + name: "ch".to_string(), + span: Span::unknown(), + }), + span: Span::unknown(), + }), + span: Span::unknown(), + }]; + + let result = MutableAccumulatorAnalyzer::analyze(&loop_body).unwrap(); + assert!(result.is_some()); + + let spec = result.unwrap(); + assert_eq!(spec.target_name, "out"); + assert_eq!(spec.rhs_expr_kind, RhsExprKind::Var); + assert_eq!(spec.rhs_var_or_lit, "ch"); + assert_eq!(spec.op, BinaryOperator::Add); + } + + #[test] + fn test_mutable_accumulator_spec_ng_reversed() { + // Build AST for: out = ch + out (reversed operands) + let loop_body = vec![ASTNode::Assignment { + target: Box::new(ASTNode::Variable { + name: "out".to_string(), + span: Span::unknown(), + }), + value: Box::new(ASTNode::BinaryOp { + operator: BinaryOperator::Add, + left: Box::new(ASTNode::Variable { + name: "ch".to_string(), + span: Span::unknown(), + }), + right: Box::new(ASTNode::Variable { + name: "out".to_string(), + span: Span::unknown(), + }), + span: Span::unknown(), + }), + span: Span::unknown(), + }]; + + let result = MutableAccumulatorAnalyzer::analyze(&loop_body); + assert!(result.is_err()); + assert!(result + .unwrap_err() + .contains("Assignment form not accumulator pattern")); + } + + #[test] + fn test_mutable_accumulator_spec_ng_multiple() { + // Build AST for: out = out + ch; out = out + "x" + // Expected: Ok(None) because multiple assignments means not our simple pattern + let loop_body = vec![ + ASTNode::Assignment { + target: Box::new(ASTNode::Variable { + name: "out".to_string(), + span: Span::unknown(), + }), + value: Box::new(ASTNode::BinaryOp { + operator: BinaryOperator::Add, + left: Box::new(ASTNode::Variable { + name: "out".to_string(), + span: Span::unknown(), + }), + right: Box::new(ASTNode::Variable { + name: "ch".to_string(), + span: Span::unknown(), + }), + span: Span::unknown(), + }), + span: Span::unknown(), + }, + ASTNode::Assignment { + target: Box::new(ASTNode::Variable { + name: "out".to_string(), + span: Span::unknown(), + }), + value: Box::new(ASTNode::BinaryOp { + operator: BinaryOperator::Add, + left: Box::new(ASTNode::Variable { + name: "out".to_string(), + span: Span::unknown(), + }), + right: Box::new(ASTNode::Literal { + value: LiteralValue::String("x".to_string()), + span: Span::unknown(), + }), + span: Span::unknown(), + }), + span: Span::unknown(), + }, + ]; + + let result = MutableAccumulatorAnalyzer::analyze(&loop_body); + assert!(result.is_ok()); + assert!(result.unwrap().is_none(), "Multiple assignments should return None, not error"); + } + + #[test] + fn test_mutable_accumulator_spec_ng_method_call() { + // Build AST for: out = out + f() + let loop_body = vec![ASTNode::Assignment { + target: Box::new(ASTNode::Variable { + name: "out".to_string(), + span: Span::unknown(), + }), + value: Box::new(ASTNode::BinaryOp { + operator: BinaryOperator::Add, + left: Box::new(ASTNode::Variable { + name: "out".to_string(), + span: Span::unknown(), + }), + right: Box::new(ASTNode::MethodCall { + object: Box::new(ASTNode::Variable { + name: "obj".to_string(), + span: Span::unknown(), + }), + method: "f".to_string(), + arguments: vec![], + span: Span::unknown(), + }), + span: Span::unknown(), + }), + span: Span::unknown(), + }]; + + let result = MutableAccumulatorAnalyzer::analyze(&loop_body); + assert!(result.is_err()); + assert!(result + .unwrap_err() + .contains("RHS contains function call (not supported)")); + } + + #[test] + fn test_no_accumulator_when_no_assignment() { + // Build AST for: local x = 1 + let loop_body = vec![ASTNode::Local { + variables: vec!["x".to_string()], + initial_values: vec![Some(Box::new(ASTNode::Literal { + value: LiteralValue::Integer(1), + span: Span::unknown(), + }))], + span: Span::unknown(), + }]; + + let result = MutableAccumulatorAnalyzer::analyze(&loop_body).unwrap(); + assert!(result.is_none()); + } + + #[test] + fn test_accumulator_with_literal_rhs() { + // Build AST for: count = count + 1 + let loop_body = vec![ASTNode::Assignment { + target: Box::new(ASTNode::Variable { + name: "count".to_string(), + span: Span::unknown(), + }), + value: Box::new(ASTNode::BinaryOp { + operator: BinaryOperator::Add, + left: Box::new(ASTNode::Variable { + name: "count".to_string(), + span: Span::unknown(), + }), + right: Box::new(ASTNode::Literal { + value: LiteralValue::Integer(1), + span: Span::unknown(), + }), + span: Span::unknown(), + }), + span: Span::unknown(), + }]; + + let result = MutableAccumulatorAnalyzer::analyze(&loop_body).unwrap(); + assert!(result.is_some()); + + let spec = result.unwrap(); + assert_eq!(spec.target_name, "count"); + assert_eq!(spec.rhs_expr_kind, RhsExprKind::Literal); + assert_eq!(spec.op, BinaryOperator::Add); + } +}