feat(joinir): Phase 190-impl NumberAccumulation pattern implementation

Phase 190 implementation: Detect and emit number accumulation patterns
like `result = result * 10 + digit` in Pattern 2 loops.

## Changes

### Task 190-impl-1: UpdateRhs enum extension
- Added `NumberAccumulation { base, digit_var }` variant to UpdateRhs
- Implemented detection logic in `analyze_update_value()`:
  - Detects pattern: `(carrier * base) + digit`
  - Supports both Add and Subtract operations
  - Base must be integer constant, digit must be variable
- Added 3 unit tests (base10, base2, wrong_lhs cases)

### Task 190-impl-2: Pattern2/4 whitelist update
- Updated `check_carrier_updates_allowed()` in common_init.rs
- NumberAccumulation now allowed in can_lower()
- Pattern 4 (continue) rejects with passthrough (not yet implemented)

### Task 190-impl-3: Carrier update emission
- Implemented NumberAccumulation emission in carrier_update_emitter.rs
- Emits 3 instructions:
  1. Const(base)
  2. BinOp(Mul, carrier, base) → tmp
  3. BinOp(Add/Sub, tmp, digit) → result
- Added 2 unit tests (base10 emission, digit_not_found error)
- Both UpdateEnv and ConditionEnv versions supported

### Task 190-impl-4: E2E tests (in progress)
- Created phase190_atoi_impl.hako (Pattern 2 with break)
- Created phase190_parse_number_impl.hako (Pattern 2 with break)
- Tests compile and use Pattern 2 correctly
- Runtime execution validation pending

## Files Modified
- loop_update_analyzer.rs (+180 lines: enum, detection, 3 tests)
- carrier_update_emitter.rs (+182 lines: emission, 2 tests)
- common_init.rs (+4 lines: whitelist update)
- loop_with_continue_minimal.rs (+16 lines: Pattern 4 passthrough)

## Test Results
-  All analyzer unit tests pass (4/4)
-  All emitter unit tests pass (12/12)
- 🔄 E2E runtime validation in progress

## Architecture Notes
- **Box-first modular design**: Single responsibility per function
- **Fail-fast**: Complex patterns rejected early in can_lower()
- **Pattern 2 only**: Pattern 1/3 don't support carriers yet
- **Pattern 4 future**: Passthrough stub for continue support

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
nyash-codex
2025-12-09 02:14:57 +09:00
parent 0b705f9ca0
commit 4bff4ecf43
6 changed files with 485 additions and 2 deletions

View File

@ -40,6 +40,7 @@ pub enum UpdateExpr {
/// Right-hand side of update expression
///
/// Phase 178: Extended to detect string updates for multi-carrier loops.
/// Phase 190: Extended to detect number accumulation pattern (result = result * base + digit).
/// The goal is "carrier detection", not full semantic understanding.
#[derive(Debug, Clone)]
pub enum UpdateRhs {
@ -49,6 +50,14 @@ pub enum UpdateRhs {
Variable(String),
/// Phase 178: String literal: result + "x"
StringLiteral(String),
/// Phase 190: Number accumulation pattern: result = result * base + digit
/// Represents expressions like: result * 10 + digit
NumberAccumulation {
/// Multiplication base (e.g., 10 for decimal, 2 for binary)
base: i64,
/// Variable name containing the digit to add
digit_var: String,
},
/// Phase 178: Other expression (method call, complex expr)
/// Used to detect "carrier has an update" without understanding semantics
Other,
@ -136,6 +145,7 @@ impl LoopUpdateAnalyzer {
/// Recognizes patterns like:
/// - `sum + i` → BinOp { lhs: "sum", op: Add, rhs: Variable("i") }
/// - `count + 1` → BinOp { lhs: "count", op: Add, rhs: Const(1) }
/// - Phase 190: `result * 10 + digit` → BinOp { lhs: "result", op: Add, rhs: NumberAccumulation {...} }
fn analyze_update_value(carrier_name: &str, value: &ASTNode) -> Option<UpdateExpr> {
match value {
ASTNode::BinaryOp {
@ -144,6 +154,43 @@ impl LoopUpdateAnalyzer {
right,
..
} => {
// Phase 190: Check for number accumulation pattern first
// Pattern: (carrier * base) + digit
if matches!(operator, BinaryOperator::Add | BinaryOperator::Subtract) {
if let ASTNode::BinaryOp {
operator: BinaryOperator::Multiply,
left: mul_left,
right: mul_right,
..
} = left.as_ref()
{
// Check if multiplication is: carrier * base
if let Some(mul_lhs_name) = Self::extract_variable_name(mul_left) {
if mul_lhs_name == carrier_name {
if let ASTNode::Literal {
value: LiteralValue::Integer(base),
..
} = mul_right.as_ref()
{
// Check if RHS is a variable (digit)
if let Some(digit_var) = Self::extract_variable_name(right) {
// This is number accumulation pattern!
let op = Self::convert_operator(operator)?;
return Some(UpdateExpr::BinOp {
lhs: carrier_name.to_string(),
op,
rhs: UpdateRhs::NumberAccumulation {
base: *base,
digit_var,
},
});
}
}
}
}
}
}
// Check if LHS is the carrier itself (e.g., sum in "sum + i")
if let Some(lhs_name) = Self::extract_variable_name(left) {
if lhs_name == carrier_name {
@ -169,6 +216,7 @@ impl LoopUpdateAnalyzer {
/// Analyze right-hand side of update expression
///
/// Phase 178: Extended to detect string updates.
/// Phase 190: Extended to detect number accumulation pattern.
/// Goal: "carrier has update" detection, not semantic understanding.
fn analyze_rhs(node: &ASTNode) -> Option<UpdateRhs> {
match node {
@ -189,11 +237,16 @@ impl LoopUpdateAnalyzer {
Some(UpdateRhs::Variable(name.clone()))
}
// Phase 190: Number accumulation pattern detection
// This is called from analyze_update_value, so we're analyzing the full RHS of an assignment
// Pattern not detected at this level - handled in analyze_update_value
// BinaryOp nodes that aren't simple Add/Sub are treated as complex
ASTNode::BinaryOp { .. } => Some(UpdateRhs::Other),
// Phase 178: Method call or other complex expression
// e.g., result + s.substring(pos, end)
ASTNode::Call { .. }
| ASTNode::MethodCall { .. }
| ASTNode::BinaryOp { .. }
| ASTNode::UnaryOp { .. } => Some(UpdateRhs::Other),
_ => None,
@ -264,4 +317,165 @@ mod tests {
panic!("Expected BinOp, got {:?}", updates.get("count"));
}
}
#[test]
fn test_analyze_number_accumulation_base10() {
// Test case: result = result * 10 + digit
use crate::ast::Span;
let body = vec![ASTNode::Assignment {
target: Box::new(ASTNode::Variable {
name: "result".to_string(),
span: Span::unknown(),
}),
value: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Add,
left: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Multiply,
left: Box::new(ASTNode::Variable {
name: "result".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(10),
span: Span::unknown(),
}),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Variable {
name: "digit".to_string(),
span: Span::unknown(),
}),
span: Span::unknown(),
}),
span: Span::unknown(),
}];
let carriers = vec![CarrierVar {
name: "result".to_string(),
host_id: crate::mir::ValueId(0),
join_id: None,
}];
let updates = LoopUpdateAnalyzer::analyze_carrier_updates(&body, &carriers);
assert_eq!(updates.len(), 1);
assert!(updates.contains_key("result"));
if let Some(UpdateExpr::BinOp { lhs, op, rhs }) = updates.get("result") {
assert_eq!(lhs, "result");
assert_eq!(*op, BinOpKind::Add);
if let UpdateRhs::NumberAccumulation { base, digit_var } = rhs {
assert_eq!(*base, 10);
assert_eq!(digit_var, "digit");
} else {
panic!("Expected NumberAccumulation, got {:?}", rhs);
}
} else {
panic!("Expected BinOp, got {:?}", updates.get("result"));
}
}
#[test]
fn test_analyze_number_accumulation_base2() {
// Test case: result = result * 2 + bit
use crate::ast::Span;
let body = vec![ASTNode::Assignment {
target: Box::new(ASTNode::Variable {
name: "result".to_string(),
span: Span::unknown(),
}),
value: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Add,
left: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Multiply,
left: Box::new(ASTNode::Variable {
name: "result".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(2),
span: Span::unknown(),
}),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Variable {
name: "bit".to_string(),
span: Span::unknown(),
}),
span: Span::unknown(),
}),
span: Span::unknown(),
}];
let carriers = vec![CarrierVar {
name: "result".to_string(),
host_id: crate::mir::ValueId(0),
join_id: None,
}];
let updates = LoopUpdateAnalyzer::analyze_carrier_updates(&body, &carriers);
assert_eq!(updates.len(), 1);
assert!(updates.contains_key("result"));
if let Some(UpdateExpr::BinOp { lhs, op, rhs }) = updates.get("result") {
assert_eq!(lhs, "result");
assert_eq!(*op, BinOpKind::Add);
if let UpdateRhs::NumberAccumulation { base, digit_var } = rhs {
assert_eq!(*base, 2);
assert_eq!(digit_var, "bit");
} else {
panic!("Expected NumberAccumulation, got {:?}", rhs);
}
} else {
panic!("Expected BinOp, got {:?}", updates.get("result"));
}
}
#[test]
fn test_analyze_number_accumulation_wrong_lhs() {
// Test case: result = other * 10 + digit (should be Complex/Other)
use crate::ast::Span;
let body = vec![ASTNode::Assignment {
target: Box::new(ASTNode::Variable {
name: "result".to_string(),
span: Span::unknown(),
}),
value: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Add,
left: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Multiply,
left: Box::new(ASTNode::Variable {
name: "other".to_string(), // Wrong variable!
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(10),
span: Span::unknown(),
}),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Variable {
name: "digit".to_string(),
span: Span::unknown(),
}),
span: Span::unknown(),
}),
span: Span::unknown(),
}];
let carriers = vec![CarrierVar {
name: "result".to_string(),
host_id: crate::mir::ValueId(0),
join_id: None,
}];
let updates = LoopUpdateAnalyzer::analyze_carrier_updates(&body, &carriers);
// Should detect assignment but with Other (complex) RHS
assert_eq!(updates.len(), 0); // Won't match because lhs != carrier
}
}