feat(joinir): Phase 183 LoopBodyLocal role separation + test fixes

Phase 183 Implementation:
- Added is_var_used_in_condition() helper for AST variable detection
- Implemented LoopBodyLocal filtering in TrimLoopLowerer
- Created 4 test files for P1/P2 patterns
- Added 5 unit tests for variable detection

Test Fixes:
- Fixed test_is_outer_scope_variable_pinned (BasicBlockId import)
- Fixed test_pattern2_accepts_loop_param_only (literal node usage)

Refactoring:
- Unified pattern detection documentation
- Consolidated CarrierInfo initialization
- Documented LoopScopeShape construction paths

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
nyash-codex
2025-12-08 23:43:26 +09:00
parent a3df5ecc7a
commit 440f8646b1
66 changed files with 279 additions and 183 deletions

View File

@ -60,7 +60,7 @@ impl ConditionEnvBuilder {
break_condition: &ASTNode,
loop_var_name: &str,
variable_map: &BTreeMap<String, ValueId>,
loop_var_id: ValueId,
_loop_var_id: ValueId,
) -> Result<(ConditionEnv, Vec<ConditionBinding>), String> {
// Extract all variables used in the condition (excluding loop parameter)
let condition_var_names = extract_condition_variables(

View File

@ -6,23 +6,12 @@
//! This box fully abstractifies loop exit binding generation for Pattern 3 & 4.
use crate::mir::ValueId;
use crate::mir::join_ir::lowering::inline_boundary::JoinInlineBoundary;
use crate::mir::join_ir::lowering::inline_boundary::{
JoinInlineBoundary, LoopExitBinding,
};
use crate::mir::join_ir::lowering::carrier_info::{CarrierInfo, ExitMeta};
use std::collections::HashMap;
/// Mapping from JoinIR exit value to host function variable
#[derive(Debug, Clone)]
pub struct LoopExitBinding {
/// Carrier variable name (e.g., "sum", "printed")
pub carrier_name: String,
/// Host-side ValueId for this carrier
pub host_id: ValueId,
/// Join-side exit ValueId (from ExitMeta, in JoinIR space)
pub join_exit_id: ValueId,
}
/// Builder for generating loop exit bindings
///
/// Phase 193-4: Fully boxifies exit binding generation.
@ -112,8 +101,8 @@ impl<'a> ExitBindingBuilder<'a> {
bindings.push(LoopExitBinding {
carrier_name: carrier.name.clone(),
host_id: carrier.host_id,
join_exit_id,
join_exit_value: join_exit_id,
host_slot: carrier.host_id,
});
// Allocate new ValueId for post-loop carrier value
@ -127,7 +116,7 @@ impl<'a> ExitBindingBuilder<'a> {
/// Apply bindings to JoinInlineBoundary
///
/// Sets host_outputs and join_outputs based on loop_var + carriers.
/// Sets exit_bindings (and join_outputs for legacy) based on loop_var + carriers.
/// Must be called after build_loop_exit_bindings().
///
/// # Arguments
@ -138,27 +127,38 @@ impl<'a> ExitBindingBuilder<'a> {
///
/// Success or error if boundary cannot be updated
pub fn apply_to_boundary(&self, boundary: &mut JoinInlineBoundary) -> Result<(), String> {
// Always include loop_var exit first
let mut host_outputs = vec![self.carrier_info.loop_var_id];
let mut join_outputs = vec![self.carrier_info.loop_var_id]; // Loop var exit id in JoinIR
// Build explicit exit bindings (loop var + carriers)
let mut bindings = Vec::new();
bindings.push(self.loop_var_exit_binding());
let mut join_outputs = vec![self.carrier_info.loop_var_id]; // legacy field for compatibility
// Add carrier exits in sorted order
for carrier in &self.carrier_info.carriers {
let post_loop_id = self.variable_map.get(&carrier.name)
.copied()
.ok_or_else(|| {
format!("Post-loop ValueId not found for carrier '{}'", carrier.name)
})?;
let post_loop_id = self.variable_map.get(&carrier.name).copied().ok_or_else(|| {
format!("Post-loop ValueId not found for carrier '{}'", carrier.name)
})?;
let join_exit_id = self.exit_meta.find_binding(&carrier.name)
.ok_or_else(|| format!("Exit value not found for carrier '{}'", carrier.name))?;
let join_exit_id = self.exit_meta.find_binding(&carrier.name).ok_or_else(|| {
format!("Exit value not found for carrier '{}'", carrier.name)
})?;
bindings.push(LoopExitBinding {
carrier_name: carrier.name.clone(),
host_slot: post_loop_id,
join_exit_value: join_exit_id,
});
host_outputs.push(post_loop_id);
join_outputs.push(join_exit_id);
}
boundary.host_outputs = host_outputs;
boundary.exit_bindings = bindings;
// Deprecated fields kept in sync for legacy consumers
let join_outputs_clone = join_outputs.clone();
boundary.join_outputs = join_outputs;
#[allow(deprecated)]
{
boundary.host_outputs = join_outputs_clone;
}
Ok(())
}
@ -169,8 +169,8 @@ impl<'a> ExitBindingBuilder<'a> {
pub fn loop_var_exit_binding(&self) -> LoopExitBinding {
LoopExitBinding {
carrier_name: self.carrier_info.loop_var_name.clone(),
host_id: self.carrier_info.loop_var_id,
join_exit_id: self.carrier_info.loop_var_id, // Loop var maps to itself
join_exit_value: self.carrier_info.loop_var_id, // Loop var maps to itself
host_slot: self.carrier_info.loop_var_id,
}
}
@ -226,8 +226,8 @@ mod tests {
assert_eq!(bindings.len(), 1);
assert_eq!(bindings[0].carrier_name, "sum");
assert_eq!(bindings[0].host_id, ValueId(10));
assert_eq!(bindings[0].join_exit_id, ValueId(15));
assert_eq!(bindings[0].host_slot, ValueId(10));
assert_eq!(bindings[0].join_exit_value, ValueId(15));
// Check that variable_map was updated with new post-loop ValueId
assert!(variable_map.contains_key("sum"));
@ -400,9 +400,11 @@ mod tests {
let mut boundary = JoinInlineBoundary {
host_inputs: vec![],
join_inputs: vec![],
host_outputs: vec![],
join_outputs: vec![],
exit_bindings: vec![], // Phase 171: Add missing field
#[allow(deprecated)]
host_outputs: vec![], // legacy, unused in new assertions
join_outputs: vec![],
#[allow(deprecated)]
condition_inputs: vec![], // Phase 171: Add missing field
condition_bindings: vec![], // Phase 171-fix: Add missing field
expr_result: None, // Phase 33-14: Add missing field
@ -412,11 +414,15 @@ mod tests {
builder.apply_to_boundary(&mut boundary)
.expect("Failed to apply to boundary");
// Should have loop_var + sum carrier
assert_eq!(boundary.host_outputs.len(), 2);
assert_eq!(boundary.join_outputs.len(), 2);
// Should have loop_var + sum carrier in exit_bindings
assert_eq!(boundary.exit_bindings.len(), 2);
assert_eq!(boundary.exit_bindings[0].carrier_name, "i");
assert_eq!(boundary.exit_bindings[0].host_slot, ValueId(5));
assert_eq!(boundary.exit_bindings[0].join_exit_value, ValueId(5));
assert_eq!(boundary.host_outputs[0], ValueId(5)); // loop_var
assert_eq!(boundary.join_outputs[0], ValueId(5)); // loop_var in JoinIR
assert_eq!(boundary.exit_bindings[1].carrier_name, "sum");
// Post-loop carrier id is freshly allocated (10 -> 11)
assert_eq!(boundary.exit_bindings[1].host_slot, ValueId(11));
assert_eq!(boundary.exit_bindings[1].join_exit_value, ValueId(15));
}
}

View File

@ -192,7 +192,7 @@ impl MirBuilder {
// Extract from context
let loop_var_name = ctx.loop_var_name.clone();
let loop_var_id = ctx.loop_var_id;
let _loop_var_id = ctx.loop_var_id;
let carrier_info_prelim = ctx.carrier_info.clone();
let scope = ctx.loop_scope.clone();