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:
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user