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

@ -739,4 +739,53 @@ mod tests {
"Pattern2 lowerer should accept JsonParser-like break loop"
);
}
#[test]
fn test_atoi_loop_routed_to_pattern2() {
// Phase 246-EX Step 2: _atoi loop router integration test
// loop(i < len) {
// if digit_pos < 0 { break }
// result = result * 10 + digit_pos // NumberAccumulation
// i = i + 1
// }
let condition = bin(BinaryOperator::Less, var("i"), var("len"));
let break_cond = bin(BinaryOperator::Less, var("digit_pos"), lit_i(0));
// result = result * 10 + digit_pos (NumberAccumulation pattern)
let mul_expr = bin(BinaryOperator::Multiply, var("result"), lit_i(10));
let result_update_value = bin(BinaryOperator::Add, mul_expr, var("digit_pos"));
let body = vec![
ASTNode::If {
condition: Box::new(break_cond),
then_body: vec![ASTNode::Break { span: span() }],
else_body: None,
span: span(),
},
ASTNode::Assignment {
target: Box::new(var("result")),
value: Box::new(result_update_value),
span: span(),
},
ASTNode::Assignment {
target: Box::new(var("i")),
value: Box::new(bin(BinaryOperator::Add, var("i"), lit_i(1))),
span: span(),
},
];
let ctx = LoopPatternContext::new(&condition, &body, "_atoi", true);
let builder = MirBuilder::new();
// Verify pattern classification
assert_eq!(ctx.pattern_kind, LoopPatternKind::Pattern2Break,
"_atoi loop should be classified as Pattern2Break");
// Verify Pattern2 lowerer accepts it
assert!(
can_lower(&builder, &ctx),
"Pattern2 lowerer should accept _atoi loop with NumberAccumulation"
);
}
}

View File

@ -686,7 +686,8 @@ mod tests {
};
let (cond_env, body_env) = test_update_env();
let update_env = UpdateEnv::new(&cond_env, &body_env);
let promoted: Vec<String> = vec![];
let update_env = UpdateEnv::new(&cond_env, &body_env, &promoted);
let mut value_counter = 110u32;
let mut alloc_value = || {
@ -734,7 +735,8 @@ mod tests {
let mut body_env = LoopBodyLocalEnv::new();
body_env.insert("x".to_string(), ValueId(200)); // Body-local: x=200 (should be ignored)
let update_env = UpdateEnv::new(&cond_env, &body_env);
let promoted: Vec<String> = vec![];
let update_env = UpdateEnv::new(&cond_env, &body_env, &promoted);
let carrier = test_carrier("sum", 200);
let update = UpdateExpr::BinOp {
@ -774,7 +776,8 @@ mod tests {
fn test_emit_update_with_env_variable_not_found() {
// Phase 184: Test error when variable not in either env
let (cond_env, body_env) = test_update_env();
let update_env = UpdateEnv::new(&cond_env, &body_env);
let promoted: Vec<String> = vec![];
let update_env = UpdateEnv::new(&cond_env, &body_env, &promoted);
let carrier = test_carrier("sum", 200);
let update = UpdateExpr::BinOp {
@ -809,7 +812,8 @@ mod tests {
fn test_emit_update_with_env_const_update() {
// Phase 184: Test UpdateEnv with simple const update (baseline)
let (cond_env, body_env) = test_update_env();
let update_env = UpdateEnv::new(&cond_env, &body_env);
let promoted: Vec<String> = vec![];
let update_env = UpdateEnv::new(&cond_env, &body_env, &promoted);
let carrier = test_carrier("count", 100);
let update = UpdateExpr::Const(1);
@ -842,7 +846,8 @@ mod tests {
cond_env.insert("digit".to_string(), ValueId(30)); // Digit variable
let body_env = LoopBodyLocalEnv::new();
let update_env = UpdateEnv::new(&cond_env, &body_env);
let promoted: Vec<String> = vec![];
let update_env = UpdateEnv::new(&cond_env, &body_env, &promoted);
let carrier = test_carrier("result", 200);
let update = UpdateExpr::BinOp {
@ -921,7 +926,8 @@ mod tests {
// Note: digit NOT in env
let body_env = LoopBodyLocalEnv::new();
let update_env = UpdateEnv::new(&cond_env, &body_env);
let promoted: Vec<String> = vec![];
let update_env = UpdateEnv::new(&cond_env, &body_env, &promoted);
let carrier = test_carrier("result", 200);
let update = UpdateExpr::BinOp {

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"));
}
}
}

View File

@ -415,9 +415,10 @@ pub(crate) fn lower_loop_with_break_minimal(
})?;
// Phase 185-2: Emit carrier update with body-local support
// Phase 247-EX: Pass promoted_loopbodylocals for dual-value carrier resolution
let updated_value = if let Some(ref body_env) = body_local_env {
// Use UpdateEnv for body-local variable resolution
let update_env = UpdateEnv::new(env, body_env);
let update_env = UpdateEnv::new(env, body_env, &carrier_info.promoted_loopbodylocals);
emit_carrier_update_with_env(
carrier,
update_expr,

View File

@ -1,9 +1,18 @@
//! Phase 184: Update Expression Environment
//! Phase 247-EX: Extended with promoted variable resolution for dual-value carriers
//!
//! This module provides a unified variable resolution layer for carrier update expressions.
//! It combines ConditionEnv (condition variables) and LoopBodyLocalEnv (body-local variables)
//! with clear priority order.
//!
//! ## Phase 247-EX: Promoted Variable Resolution
//!
//! For promoted LoopBodyLocal variables (e.g., `digit_pos` → `is_digit_pos` + `digit_value`):
//! - When resolving `digit_pos` in update expressions (e.g., `result = result * 10 + digit_pos`)
//! - Try `<var>_value` first (e.g., `digit_value`)
//! - Then fall back to `is_<var>` (boolean carrier, less common in updates)
//! - Finally fall back to standard resolution
//!
//! ## Design Philosophy
//!
//! **Single Responsibility**: This module ONLY handles variable resolution priority logic.
@ -49,7 +58,7 @@ use crate::mir::ValueId;
/// ```ignore
/// let condition_env = /* ... i, sum ... */;
/// let body_local_env = /* ... temp ... */;
/// let update_env = UpdateEnv::new(&condition_env, &body_local_env);
/// let update_env = UpdateEnv::new(&condition_env, &body_local_env, &[]);
///
/// // Resolve "sum" → ConditionEnv (priority 1)
/// assert_eq!(update_env.resolve("sum"), Some(ValueId(X)));
@ -60,6 +69,17 @@ use crate::mir::ValueId;
/// // Resolve "unknown" → None
/// assert_eq!(update_env.resolve("unknown"), None);
/// ```
///
/// # Phase 247-EX Example
///
/// ```ignore
/// // digit_pos promoted → is_digit_pos (bool) + digit_value (i64)
/// let promoted = vec!["digit_pos".to_string()];
/// let update_env = UpdateEnv::new(&condition_env, &body_local_env, &promoted);
///
/// // Resolve "digit_pos" in NumberAccumulation → digit_value (integer carrier)
/// assert_eq!(update_env.resolve("digit_pos"), Some(ValueId(X))); // digit_value
/// ```
#[derive(Debug)]
pub struct UpdateEnv<'a> {
/// Condition variable environment (priority 1)
@ -67,6 +87,10 @@ pub struct UpdateEnv<'a> {
/// Body-local variable environment (priority 2)
body_local_env: &'a LoopBodyLocalEnv,
/// Phase 247-EX: List of promoted LoopBodyLocal variable names
/// For these variables, resolve to <var>_value carrier instead of is_<var>
promoted_loopbodylocals: &'a [String],
}
impl<'a> UpdateEnv<'a> {
@ -76,22 +100,28 @@ impl<'a> UpdateEnv<'a> {
///
/// * `condition_env` - Condition variable environment (highest priority)
/// * `body_local_env` - Body-local variable environment (fallback)
/// * `promoted_loopbodylocals` - Phase 247-EX: List of promoted variable names
pub fn new(
condition_env: &'a ConditionEnv,
body_local_env: &'a LoopBodyLocalEnv,
promoted_loopbodylocals: &'a [String],
) -> Self {
Self {
condition_env,
body_local_env,
promoted_loopbodylocals,
}
}
/// Resolve a variable name to JoinIR ValueId
///
/// Resolution order:
/// 1. Try condition_env.get(name)
/// 2. If not found, try body_local_env.get(name)
/// 3. If still not found, return None
/// Resolution order (Phase 247-EX extended):
/// 1. If name is in promoted_loopbodylocals:
/// a. Try condition_env.get("<name>_value") // Integer carrier for accumulation
/// b. If not found, try condition_env.get("is_<name>") // Boolean carrier (rare in updates)
/// 2. Try condition_env.get(name)
/// 3. If not found, try body_local_env.get(name)
/// 4. If still not found, return None
///
/// # Arguments
///
@ -101,7 +131,52 @@ impl<'a> UpdateEnv<'a> {
///
/// * `Some(ValueId)` - Variable found in one of the environments
/// * `None` - Variable not found in either environment
///
/// # Phase 247-EX Example
///
/// ```ignore
/// // digit_pos promoted → is_digit_pos + digit_value
/// // When resolving "digit_pos" in update expr:
/// env.resolve("digit_pos") → env.get("digit_value") → Some(ValueId(X))
/// ```
pub fn resolve(&self, name: &str) -> Option<ValueId> {
// Phase 247-EX: Check if this is a promoted variable
if self.promoted_loopbodylocals.iter().any(|v| v == name) {
// Phase 247-EX: Naming convention - "digit_pos" → "digit_value" (not "digit_pos_value")
// Extract base name: "digit_pos" → "digit", "pos" → "pos"
let base_name = if name.ends_with("_pos") {
&name[..name.len() - 4] // Remove "_pos" suffix
} else {
name
};
// Priority 1a: Try <base>_value (integer carrier for NumberAccumulation)
let int_carrier_name = format!("{}_value", base_name);
if let Some(value_id) = self.condition_env.get(&int_carrier_name) {
eprintln!(
"[update_env/phase247ex] Resolved promoted '{}' → '{}' (integer carrier): {:?}",
name, int_carrier_name, value_id
);
return Some(value_id);
}
// Priority 1b: Try is_<name> (boolean carrier, less common in updates)
let bool_carrier_name = format!("is_{}", name);
if let Some(value_id) = self.condition_env.get(&bool_carrier_name) {
eprintln!(
"[update_env/phase247ex] Resolved promoted '{}' → '{}' (boolean carrier): {:?}",
name, bool_carrier_name, value_id
);
return Some(value_id);
}
eprintln!(
"[update_env/phase247ex] WARNING: Promoted variable '{}' not found as carrier ({} or {})",
name, int_carrier_name, bool_carrier_name
);
}
// Standard resolution (Phase 184)
self.condition_env
.get(name)
.or_else(|| self.body_local_env.get(name))
@ -149,7 +224,8 @@ mod tests {
// Condition variables should be found first
let cond_env = test_condition_env();
let body_env = LoopBodyLocalEnv::new(); // Empty
let update_env = UpdateEnv::new(&cond_env, &body_env);
let promoted: Vec<String> = vec![]; // Phase 247-EX: No promoted variables
let update_env = UpdateEnv::new(&cond_env, &body_env, &promoted);
assert_eq!(update_env.resolve("i"), Some(ValueId(10)));
assert_eq!(update_env.resolve("sum"), Some(ValueId(20)));
@ -161,7 +237,8 @@ mod tests {
// Body-local variables should be found when not in condition env
let cond_env = ConditionEnv::new(); // Empty
let body_env = test_body_local_env();
let update_env = UpdateEnv::new(&cond_env, &body_env);
let promoted: Vec<String> = vec![];
let update_env = UpdateEnv::new(&cond_env, &body_env, &promoted);
assert_eq!(update_env.resolve("temp"), Some(ValueId(50)));
assert_eq!(update_env.resolve("digit"), Some(ValueId(60)));
@ -176,7 +253,8 @@ mod tests {
let mut body_env = LoopBodyLocalEnv::new();
body_env.insert("x".to_string(), ValueId(200)); // Body-local: x=200
let update_env = UpdateEnv::new(&cond_env, &body_env);
let promoted: Vec<String> = vec![];
let update_env = UpdateEnv::new(&cond_env, &body_env, &promoted);
// Should resolve to condition env value (100), not body-local (200)
assert_eq!(update_env.resolve("x"), Some(ValueId(100)));
@ -187,7 +265,8 @@ mod tests {
// Variable not in either environment → None
let cond_env = test_condition_env();
let body_env = test_body_local_env();
let update_env = UpdateEnv::new(&cond_env, &body_env);
let promoted: Vec<String> = vec![];
let update_env = UpdateEnv::new(&cond_env, &body_env, &promoted);
assert_eq!(update_env.resolve("unknown"), None);
assert_eq!(update_env.resolve("nonexistent"), None);
@ -198,7 +277,8 @@ mod tests {
// Mixed lookup: some in condition, some in body-local
let cond_env = test_condition_env();
let body_env = test_body_local_env();
let update_env = UpdateEnv::new(&cond_env, &body_env);
let promoted: Vec<String> = vec![];
let update_env = UpdateEnv::new(&cond_env, &body_env, &promoted);
// Condition variables
assert_eq!(update_env.resolve("i"), Some(ValueId(10)));
@ -216,7 +296,8 @@ mod tests {
fn test_contains() {
let cond_env = test_condition_env();
let body_env = test_body_local_env();
let update_env = UpdateEnv::new(&cond_env, &body_env);
let promoted: Vec<String> = vec![];
let update_env = UpdateEnv::new(&cond_env, &body_env, &promoted);
assert!(update_env.contains("i"));
assert!(update_env.contains("temp"));
@ -228,7 +309,8 @@ mod tests {
// Both environments empty
let cond_env = ConditionEnv::new();
let body_env = LoopBodyLocalEnv::new();
let update_env = UpdateEnv::new(&cond_env, &body_env);
let promoted: Vec<String> = vec![];
let update_env = UpdateEnv::new(&cond_env, &body_env, &promoted);
assert_eq!(update_env.resolve("anything"), None);
assert!(!update_env.contains("anything"));
@ -239,10 +321,60 @@ mod tests {
// Test diagnostic accessor methods
let cond_env = test_condition_env();
let body_env = test_body_local_env();
let update_env = UpdateEnv::new(&cond_env, &body_env);
let promoted: Vec<String> = vec![];
let update_env = UpdateEnv::new(&cond_env, &body_env, &promoted);
// Should return references to underlying environments
assert_eq!(update_env.condition_env().len(), 3);
assert_eq!(update_env.body_local_env().len(), 2);
}
// Phase 247-EX: Test promoted variable resolution (dual-value carriers)
#[test]
fn test_promoted_variable_resolution_digit_pos() {
// Test case: digit_pos promoted → is_digit_pos (bool) + digit_value (i64)
// Naming: "digit_pos" → "is_digit_pos" + "digit_value" (base_name="_pos" removed)
let mut cond_env = ConditionEnv::new();
// Register both carriers in ConditionEnv
cond_env.insert("is_digit_pos".to_string(), ValueId(100)); // Boolean carrier
cond_env.insert("digit_value".to_string(), ValueId(200)); // Integer carrier (digit_pos → digit)
let body_env = LoopBodyLocalEnv::new();
let promoted: Vec<String> = vec!["digit_pos".to_string()];
let update_env = UpdateEnv::new(&cond_env, &body_env, &promoted);
// When resolving "digit_pos" in update expr → should get digit_value (integer carrier)
assert_eq!(update_env.resolve("digit_pos"), Some(ValueId(200)));
// Direct carrier access still works
assert_eq!(update_env.resolve("is_digit_pos"), Some(ValueId(100)));
assert_eq!(update_env.resolve("digit_value"), Some(ValueId(200)));
}
#[test]
fn test_promoted_variable_resolution_fallback_to_bool() {
// Test case: Only boolean carrier exists (integer carrier missing)
let mut cond_env = ConditionEnv::new();
cond_env.insert("is_pos".to_string(), ValueId(150)); // Only boolean carrier
let body_env = LoopBodyLocalEnv::new();
let promoted: Vec<String> = vec!["pos".to_string()];
let update_env = UpdateEnv::new(&cond_env, &body_env, &promoted);
// Should fall back to is_pos (boolean carrier)
assert_eq!(update_env.resolve("pos"), Some(ValueId(150)));
}
#[test]
fn test_promoted_variable_not_a_carrier() {
// Test case: Variable in promoted list but no carrier exists
let cond_env = ConditionEnv::new(); // Empty
let body_env = LoopBodyLocalEnv::new();
let promoted: Vec<String> = vec!["missing_var".to_string()];
let update_env = UpdateEnv::new(&cond_env, &body_env, &promoted);
// Should return None (with warning logged)
assert_eq!(update_env.resolve("missing_var"), None);
}
}

View File

@ -180,23 +180,44 @@ impl DigitPosPromoter {
);
// Step 6: Build CarrierInfo
// For DigitPos pattern, we add a NEW carrier (not replace loop_var)
let carrier_name = format!("is_{}", var_in_cond);
// Phase 247-EX: DigitPos generates TWO carriers (dual-value model)
// - is_<var> (boolean): for break condition
// - <prefix>_value (integer): for NumberAccumulation
// Naming: "digit_pos" → "is_digit_pos" + "digit_value" (not "digit_pos_value")
let bool_carrier_name = format!("is_{}", var_in_cond);
// Extract the base name for integer carrier (e.g., "digit_pos" → "digit")
let base_name = if var_in_cond.ends_with("_pos") {
&var_in_cond[..var_in_cond.len() - 4] // Remove "_pos" suffix
} else {
var_in_cond.as_str()
};
let int_carrier_name = format!("{}_value", base_name);
use crate::mir::join_ir::lowering::carrier_info::{CarrierVar, CarrierRole, CarrierInit};
let promoted_carrier = CarrierVar {
name: carrier_name.clone(),
// Boolean carrier (condition-only, for break)
let promoted_carrier_bool = CarrierVar {
name: bool_carrier_name.clone(),
host_id: ValueId(0), // Placeholder (will be remapped)
join_id: None, // Will be allocated later
role: CarrierRole::ConditionOnly, // Phase 227: DigitPos is condition-only
init: CarrierInit::BoolConst(false), // Phase 228: Initialize with false
};
// Integer carrier (loop-state, for NumberAccumulation)
let promoted_carrier_int = CarrierVar {
name: int_carrier_name.clone(),
host_id: ValueId(0), // Placeholder (will be remapped)
join_id: None, // Will be allocated later
role: CarrierRole::LoopState, // Phase 247-EX: LoopState for accumulation
init: CarrierInit::FromHost, // Phase 228: Initialize from indexOf() result
};
// Create CarrierInfo with a dummy loop_var_name (will be ignored during merge)
let mut carrier_info = CarrierInfo::with_carriers(
"__dummy_loop_var__".to_string(), // Placeholder, not used
ValueId(0), // Placeholder
vec![promoted_carrier],
vec![promoted_carrier_bool, promoted_carrier_int],
);
// Phase 229: Record promoted variable (no need for condition_aliases)
@ -204,18 +225,18 @@ impl DigitPosPromoter {
carrier_info.promoted_loopbodylocals.push(var_in_cond.clone());
eprintln!(
"[digitpos_promoter] A-4 DigitPos pattern promoted: {}{}",
var_in_cond, carrier_name
"[digitpos_promoter] Phase 247-EX: A-4 DigitPos pattern promoted: {}{} (bool) + {} (i64)",
var_in_cond, bool_carrier_name, int_carrier_name
);
eprintln!(
"[digitpos_promoter] Phase 229: Recorded promoted variable '{}' (carrier: '{}')",
var_in_cond, carrier_name
"[digitpos_promoter] Phase 229: Recorded promoted variable '{}' (carriers: '{}', '{}')",
var_in_cond, bool_carrier_name, int_carrier_name
);
return DigitPosPromotionResult::Promoted {
carrier_info,
promoted_var: var_in_cond,
carrier_name,
carrier_name: bool_carrier_name, // Return bool carrier name for compatibility
};
} else {
eprintln!(

View File

@ -192,3 +192,43 @@ fn pattern4_continue_loop_is_detected() {
let kind = classify_body(&body);
assert_eq!(kind, LoopPatternKind::Pattern4Continue);
}
#[test]
fn test_atoi_loop_classified_as_pattern2() {
// Phase 246-EX Step 1: _atoi loop pattern classification
// loop(i < len) {
// local ch = s.substring(i, i+1)
// local digit_pos = digits.indexOf(ch)
// if digit_pos < 0 { break }
// result = result * 10 + digit_pos
// i = i + 1
// }
// Simplified: loop with break + two carrier updates
let break_cond = bin(BinaryOperator::Less, var("digit_pos"), lit_i(0));
// result = result * 10 + digit_pos (NumberAccumulation pattern)
let mul_expr = bin(BinaryOperator::Multiply, var("result"), lit_i(10));
let result_update = assignment(
var("result"),
bin(BinaryOperator::Add, mul_expr, var("digit_pos"))
);
// i = i + 1
let i_update = assignment(var("i"), bin(BinaryOperator::Add, var("i"), lit_i(1)));
let body = vec![
ASTNode::If {
condition: Box::new(break_cond),
then_body: vec![ASTNode::Break { span: span() }],
else_body: None,
span: span(),
},
result_update,
i_update,
];
let kind = classify_body(&body);
assert_eq!(kind, LoopPatternKind::Pattern2Break,
"_atoi loop should be classified as Pattern2 (Break) due to if-break structure");
}