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:
@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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!(
|
||||
|
||||
@ -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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user