feat(joinir): Phase 247-EX - DigitPos dual-value architecture

Extends DigitPos promotion to generate TWO carriers for Pattern A/B support:
- Boolean carrier (is_digit_pos) for break conditions
- Integer carrier (digit_value) for NumberAccumulation

## Implementation

1. **DigitPosPromoter** (loop_body_digitpos_promoter.rs)
   - Generates dual carriers: is_<var> (bool) + <base>_value (int)
   - Smart naming: "digit_pos" → "digit" (removes "_pos" suffix)

2. **UpdateEnv** (update_env.rs)
   - Context-aware promoted variable resolution
   - Priority: <base>_value (int) → is_<var> (bool) → standard
   - Pass promoted_loopbodylocals from CarrierInfo

3. **Integration** (loop_with_break_minimal.rs)
   - UpdateEnv constructor updated to pass promoted list

## Test Results

- **Before**: 925 tests PASS
- **After**: 931 tests PASS (+6 new tests, 0 failures)

## New Tests

- test_promoted_variable_resolution_digit_pos - Full dual-value
- test_promoted_variable_resolution_fallback_to_bool - Fallback
- test_promoted_variable_not_a_carrier - Error handling

## Impact

| Pattern | Before | After |
|---------|--------|-------|
| _parse_number |  Works (bool only) |  Works (bool used, int unused) |
| _atoi |  Failed (missing int) |  READY (int carrier available!) |

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
nyash-codex
2025-12-11 15:08:14 +09:00
parent d4597dacfa
commit 8900a3cc44
12 changed files with 1868 additions and 40 deletions

View File

@ -486,4 +486,164 @@ mod tests {
// Should detect assignment but with Other (complex) RHS
assert_eq!(updates.len(), 0); // Won't match because lhs != carrier
}
#[test]
fn test_analyze_num_str_string_append() {
// Phase 245B: Test case: num_str = num_str + ch (string append pattern)
use crate::ast::Span;
let body = vec![ASTNode::Assignment {
target: Box::new(ASTNode::Variable {
name: "num_str".to_string(),
span: Span::unknown(),
}),
value: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Add,
left: Box::new(ASTNode::Variable {
name: "num_str".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Variable {
name: "ch".to_string(),
span: Span::unknown(),
}),
span: Span::unknown(),
}),
span: Span::unknown(),
}];
let carriers = vec![CarrierVar {
name: "num_str".to_string(),
host_id: crate::mir::ValueId(0),
join_id: None,
role: crate::mir::join_ir::lowering::carrier_info::CarrierRole::LoopState,
init: crate::mir::join_ir::lowering::carrier_info::CarrierInit::FromHost,
}];
let updates = LoopUpdateAnalyzer::analyze_carrier_updates(&body, &carriers);
assert_eq!(updates.len(), 1);
assert!(updates.contains_key("num_str"));
if let Some(UpdateExpr::BinOp { lhs, op, rhs }) = updates.get("num_str") {
assert_eq!(lhs, "num_str");
assert_eq!(*op, BinOpKind::Add);
if let UpdateRhs::Variable(var_name) = rhs {
assert_eq!(var_name, "ch");
} else {
panic!("Expected Variable('ch'), got {:?}", rhs);
}
} else {
panic!("Expected BinOp, got {:?}", updates.get("num_str"));
}
}
#[test]
fn test_atoi_update_expr_detection() {
// Phase 246-EX Step 3: _atoi loop multi-carrier update detection
// Tests two carriers with different update patterns:
// - i = i + 1 (Const increment)
// - result = result * 10 + digit_pos (NumberAccumulation)
use crate::ast::Span;
let body = vec![
// result = result * 10 + digit_pos
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_pos".to_string(),
span: Span::unknown(),
}),
span: Span::unknown(),
}),
span: Span::unknown(),
},
// i = i + 1
ASTNode::Assignment {
target: Box::new(ASTNode::Variable {
name: "i".to_string(),
span: Span::unknown(),
}),
value: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Add,
left: Box::new(ASTNode::Variable {
name: "i".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(1),
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,
role: crate::mir::join_ir::lowering::carrier_info::CarrierRole::LoopState,
init: crate::mir::join_ir::lowering::carrier_info::CarrierInit::FromHost,
},
CarrierVar {
name: "i".to_string(),
host_id: crate::mir::ValueId(1),
join_id: None,
role: crate::mir::join_ir::lowering::carrier_info::CarrierRole::LoopState,
init: crate::mir::join_ir::lowering::carrier_info::CarrierInit::FromHost,
},
];
let updates = LoopUpdateAnalyzer::analyze_carrier_updates(&body, &carriers);
// Verify both carriers have updates
assert_eq!(updates.len(), 2, "Should detect updates for both i and result");
// Verify i = i + 1 (Const increment)
if let Some(UpdateExpr::BinOp { lhs, op, rhs }) = updates.get("i") {
assert_eq!(lhs, "i");
assert_eq!(*op, BinOpKind::Add);
if let UpdateRhs::Const(n) = rhs {
assert_eq!(*n, 1, "i should increment by 1");
} else {
panic!("Expected Const(1) for i update, got {:?}", rhs);
}
} else {
panic!("Expected BinOp for i update, got {:?}", updates.get("i"));
}
// Verify result = result * 10 + digit_pos (NumberAccumulation)
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, "NumberAccumulation should use base 10");
assert_eq!(digit_var, "digit_pos", "Should use digit_pos variable");
} else {
panic!("Expected NumberAccumulation for result update, got {:?}", rhs);
}
} else {
panic!("Expected BinOp for result update, got {:?}", updates.get("result"));
}
}
}