diff --git a/src/mir/join_ir/lowering/loop_with_break_minimal.rs b/src/mir/join_ir/lowering/loop_with_break_minimal.rs index 357d8897..3a22ede5 100644 --- a/src/mir/join_ir/lowering/loop_with_break_minimal.rs +++ b/src/mir/join_ir/lowering/loop_with_break_minimal.rs @@ -63,6 +63,9 @@ use crate::mir::join_ir::{ BinOpKind, ConstValue, JoinFuncId, JoinFunction, JoinInst, JoinModule, MirLikeInst, UnaryOp, }; +use crate::mir::loop_pattern_detection::loop_condition_scope::{ + LoopConditionScopeBox, CondVarScope, +}; use crate::mir::ValueId; /// Lower Pattern 2 (Loop with Conditional Break) to JoinIR @@ -126,6 +129,33 @@ pub fn lower_loop_with_break_minimal( env: &ConditionEnv, loop_var_name: &str, ) -> Result<(JoinModule, JoinFragmentMeta), String> { + // Phase 170-D-impl-3: Validate that conditions only use supported variable scopes + // LoopConditionScopeBox checks that loop conditions don't reference loop-body-local variables + let loop_cond_scope = LoopConditionScopeBox::analyze( + loop_var_name, + &[condition, break_condition], + Some(&_scope), + ); + + if loop_cond_scope.has_loop_body_local() { + let body_local_names: Vec<&String> = loop_cond_scope.vars.iter() + .filter(|v| v.scope == CondVarScope::LoopBodyLocal) + .map(|v| &v.name) + .collect(); + + return Err(format!( + "[joinir/pattern2] Unsupported condition: uses loop-body-local variables: {:?}. \ + Pattern 2 supports only loop parameters and outer-scope variables. \ + Consider using Pattern 5+ for complex loop conditions.", + body_local_names + )); + } + + eprintln!( + "[joinir/pattern2] Phase 170-D: Condition variables verified: {:?}", + loop_cond_scope.var_names() + ); + // Phase 188-Impl-2: Use local ValueId allocator (sequential from 0) // JoinIR has NO knowledge of host ValueIds - boundary handled separately let mut value_counter = 0u32; @@ -309,3 +339,143 @@ pub fn lower_loop_with_break_minimal( Ok((join_module, fragment_meta)) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::ast::Span; + use crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape; + use std::collections::{BTreeMap, BTreeSet}; + + // Helper: Create a simple variable node + fn var_node(name: &str) -> ASTNode { + ASTNode::Variable { + name: name.to_string(), + span: Span::unknown(), + } + } + + // Helper: Create a binary operation node (Less operator for comparisons) + fn binop_node(left: ASTNode, right: ASTNode) -> ASTNode { + ASTNode::BinaryOp { + operator: crate::ast::BinaryOperator::Less, + left: Box::new(left), + right: Box::new(right), + span: Span::unknown(), + } + } + + // Helper: Create a minimal LoopScopeShape + fn minimal_scope() -> LoopScopeShape { + LoopScopeShape { + header: crate::mir::BasicBlockId(0), + body: crate::mir::BasicBlockId(1), + latch: crate::mir::BasicBlockId(2), + exit: crate::mir::BasicBlockId(3), + pinned: BTreeSet::new(), + carriers: BTreeSet::new(), + body_locals: BTreeSet::new(), + exit_live: BTreeSet::new(), + progress_carrier: None, + variable_definitions: BTreeMap::new(), + } + } + + // Helper: Create a scope with outer variable + fn scope_with_outer_var(var_name: &str) -> LoopScopeShape { + let mut scope = minimal_scope(); + let mut pinned = BTreeSet::new(); + pinned.insert(var_name.to_string()); + scope.pinned = pinned; + scope + } + + // Helper: Create a scope with loop-body-local variable + fn scope_with_body_local_var(var_name: &str) -> LoopScopeShape { + let mut scope = minimal_scope(); + let mut body_locals = BTreeSet::new(); + body_locals.insert(var_name.to_string()); + scope.body_locals = body_locals; + scope + } + + #[test] + fn test_pattern2_accepts_loop_param_only() { + // Simple case: loop(i < 10) { if i >= 5 { break } } + let loop_cond = binop_node(var_node("i"), var_node("10")); + let break_cond = binop_node(var_node("i"), var_node("5")); + + let scope = scope_with_outer_var("i"); // i is loop parameter (pinned) + let cond_scope = LoopConditionScopeBox::analyze("i", &[&loop_cond, &break_cond], Some(&scope)); + + assert!(!cond_scope.has_loop_body_local()); + assert_eq!(cond_scope.var_names().len(), 1); + assert!(cond_scope.var_names().contains("i")); + } + + #[test] + fn test_pattern2_accepts_outer_scope_variables() { + // Case: loop(i < end) { if i >= threshold { break } } + let loop_cond = binop_node(var_node("i"), var_node("end")); + let break_cond = binop_node(var_node("i"), var_node("threshold")); + + let mut scope = minimal_scope(); + let mut pinned = BTreeSet::new(); + pinned.insert("i".to_string()); + pinned.insert("end".to_string()); + pinned.insert("threshold".to_string()); + scope.pinned = pinned; + + let cond_scope = LoopConditionScopeBox::analyze("i", &[&loop_cond, &break_cond], Some(&scope)); + + assert!(!cond_scope.has_loop_body_local()); + assert_eq!(cond_scope.var_names().len(), 3); + } + + #[test] + fn test_pattern2_rejects_loop_body_local_variables() { + // Case: loop(i < 10) { local ch = ... if ch == ' ' { break } } + let loop_cond = binop_node(var_node("i"), var_node("10")); + let break_cond = binop_node(var_node("ch"), var_node("' '")); + + let scope = scope_with_body_local_var("ch"); // ch is defined in loop body + let cond_scope = LoopConditionScopeBox::analyze("i", &[&loop_cond, &break_cond], Some(&scope)); + + assert!(cond_scope.has_loop_body_local()); + let body_local_vars: Vec<&String> = cond_scope.vars.iter() + .filter(|v| v.scope == CondVarScope::LoopBodyLocal) + .map(|v| &v.name) + .collect(); + assert_eq!(body_local_vars.len(), 1); + assert_eq!(*body_local_vars[0], "ch"); + } + + #[test] + fn test_pattern2_detects_mixed_scope_variables() { + // Case: loop(i < end) { local ch = ... if ch == ' ' && i >= start { break } } + let loop_cond = binop_node(var_node("i"), var_node("end")); + // More complex: (ch == ' ') && (i >= start) - using nested binops with Less operator + let ch_eq = binop_node(var_node("ch"), var_node("' '")); + let i_ge = binop_node(var_node("i"), var_node("start")); + let break_cond = binop_node(ch_eq, i_ge); + + let mut scope = minimal_scope(); + let mut pinned = BTreeSet::new(); + pinned.insert("i".to_string()); + pinned.insert("end".to_string()); + pinned.insert("start".to_string()); + scope.pinned = pinned; + let mut body_locals = BTreeSet::new(); + body_locals.insert("ch".to_string()); + scope.body_locals = body_locals; + + let cond_scope = LoopConditionScopeBox::analyze("i", &[&loop_cond, &break_cond], Some(&scope)); + + assert!(cond_scope.has_loop_body_local()); + let vars = cond_scope.var_names(); + assert!(vars.contains("i")); + assert!(vars.contains("end")); + assert!(vars.contains("start")); + assert!(vars.contains("ch")); // The problematic body-local variable + } +} diff --git a/src/mir/join_ir/lowering/loop_with_continue_minimal.rs b/src/mir/join_ir/lowering/loop_with_continue_minimal.rs index 3f15a392..267f860e 100644 --- a/src/mir/join_ir/lowering/loop_with_continue_minimal.rs +++ b/src/mir/join_ir/lowering/loop_with_continue_minimal.rs @@ -54,6 +54,9 @@ use crate::mir::join_ir::{ BinOpKind, CompareOp, ConstValue, JoinFuncId, JoinFunction, JoinInst, JoinModule, MirLikeInst, UnaryOp, }; +use crate::mir::loop_pattern_detection::loop_condition_scope::{ + LoopConditionScopeBox, CondVarScope, +}; use crate::mir::ValueId; use std::collections::HashMap; @@ -104,6 +107,34 @@ pub fn lower_loop_with_continue_minimal( carrier_info: &CarrierInfo, carrier_updates: &HashMap, ) -> Result<(JoinModule, ExitMeta), String> { + // Phase 170-D-impl-3: Validate that loop condition only uses supported variable scopes + // LoopConditionScopeBox checks that loop conditions don't reference loop-body-local variables + let loop_var_name = carrier_info.loop_var_name.clone(); + let loop_cond_scope = LoopConditionScopeBox::analyze( + &loop_var_name, + &[condition], + Some(&_scope), + ); + + if loop_cond_scope.has_loop_body_local() { + let body_local_names: Vec<&String> = loop_cond_scope.vars.iter() + .filter(|v| v.scope == CondVarScope::LoopBodyLocal) + .map(|v| &v.name) + .collect(); + + return Err(format!( + "[joinir/pattern4] Unsupported condition: uses loop-body-local variables: {:?}. \ + Pattern 4 supports only loop parameters and outer-scope variables. \ + Consider using Pattern 5+ for complex loop conditions.", + body_local_names + )); + } + + eprintln!( + "[joinir/pattern4] Phase 170-D: Condition variables verified: {:?}", + loop_cond_scope.var_names() + ); + // Phase 196: Use local ValueId allocator (sequential from 0) // JoinIR has NO knowledge of host ValueIds - boundary handled separately let mut value_counter = 0u32;