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:
nyash-codex
2025-12-07 21:41:33 +09:00
parent 7be72e9e14
commit 25b9d01619
2 changed files with 201 additions and 0 deletions

View File

@ -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
}
}

View File

@ -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;