feat(joinir): Phase 170-D-impl-3 LoopConditionScopeBox integration into Pattern 2/4
## Summary Integrated LoopConditionScopeBox validation into Pattern 2 (loop with break) and Pattern 4 (loop with continue) lowerers for JoinIR. Added validation to ensure loop conditions only use supported variable scopes (loop parameters and outer-scope variables), implementing Fail-Fast error detection. ## Changes ### Pattern 2 (loop_with_break_minimal.rs) - Added import: LoopConditionScopeBox, CondVarScope - Added validation check at function entry using LoopConditionScopeBox.analyze() - Detects and reports loop-body-local variables in conditions with clear error message - Added 4 comprehensive unit tests: - test_pattern2_accepts_loop_param_only: Validates loop parameter acceptance - test_pattern2_accepts_outer_scope_variables: Validates outer-scope variables - test_pattern2_rejects_loop_body_local_variables: Validates rejection logic - test_pattern2_detects_mixed_scope_variables: Validates complex mixed scenarios ### Pattern 4 (loop_with_continue_minimal.rs) - Added import: LoopConditionScopeBox, CondVarScope - Added validation check at function entry for loop condition only - Detects and reports unsupported loop-body-local variables - Consistent error messaging with Pattern 2 ## Implementation Notes - Validation uses LoopScopeShape from JoinIR infrastructure - Fail-Fast principle: errors detected before JoinIR generation attempt - Error messages suggest Pattern 5+ for complex conditions - Phase 170-D design fully operational for Pattern 2/4 ## Test Results ✅ Pattern 2 accepts loop parameter only (test_pattern2_then_break.hako) ✅ Pattern 2 rejects loop-body-local variables (test_trim_main_pattern.hako) ✅ Build successful with all compilation warnings (no errors) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<String, UpdateExpr>,
|
||||
) -> 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;
|
||||
|
||||
Reference in New Issue
Block a user