feat(joinir): detect mutable accumulator pattern (AST-only, spec minimal)
- 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 <noreply@anthropic.com>
This commit is contained in:
@ -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;
|
||||
|
||||
417
src/mir/loop_pattern_detection/mutable_accumulator_analyzer.rs
Normal file
417
src/mir/loop_pattern_detection/mutable_accumulator_analyzer.rs
Normal file
@ -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<Option<MutableAccumulatorSpec>, 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user