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:
@ -17,7 +17,7 @@ use super::super::trace;
|
||||
pub(super) fn allocate_blocks(
|
||||
builder: &mut crate::mir::builder::MirBuilder,
|
||||
mir_module: &MirModule,
|
||||
debug: bool,
|
||||
_debug: bool,
|
||||
) -> Result<(JoinIrIdRemapper, crate::mir::BasicBlockId), String> {
|
||||
let mut remapper = JoinIrIdRemapper::new();
|
||||
|
||||
|
||||
@ -122,7 +122,7 @@ impl ExitMetaCollector {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_empty_exit_meta() {
|
||||
|
||||
@ -151,7 +151,7 @@ impl ExitLineReconnector {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_empty_exit_bindings() {
|
||||
|
||||
@ -583,13 +583,15 @@ pub(in crate::mir::builder) fn merge_joinir_mir_blocks(
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
if let Some(boundary) = boundary {
|
||||
verify_joinir_contracts(
|
||||
builder.function(),
|
||||
entry_block_remapped,
|
||||
exit_block_id,
|
||||
&loop_header_phi_info,
|
||||
boundary,
|
||||
);
|
||||
if let Some(ref func) = builder.current_function {
|
||||
verify_joinir_contracts(
|
||||
func,
|
||||
entry_block_remapped,
|
||||
exit_block_id,
|
||||
&loop_header_phi_info,
|
||||
boundary,
|
||||
);
|
||||
}
|
||||
if debug {
|
||||
eprintln!("[cf_loop/joinir] Phase 200-3: Contract verification passed");
|
||||
}
|
||||
@ -647,7 +649,13 @@ fn verify_loop_header_phis(
|
||||
) {
|
||||
// Check 1: Loop variable PHI existence
|
||||
if let Some(ref loop_var_name) = boundary.loop_var_name {
|
||||
let header_block_data = &func.blocks[header_block.0];
|
||||
let header_block_data = func.blocks.get(&header_block).unwrap_or_else(|| {
|
||||
panic!(
|
||||
"[JoinIRVerifier] Header block {} not found ({} blocks in func)",
|
||||
header_block,
|
||||
func.blocks.len()
|
||||
)
|
||||
});
|
||||
let has_loop_var_phi = header_block_data
|
||||
.instructions
|
||||
.iter()
|
||||
@ -663,7 +671,13 @@ fn verify_loop_header_phis(
|
||||
|
||||
// Check 2: Carrier PHI existence
|
||||
if !loop_info.carrier_phis.is_empty() {
|
||||
let header_block_data = &func.blocks[header_block.0];
|
||||
let header_block_data = func.blocks.get(&header_block).unwrap_or_else(|| {
|
||||
panic!(
|
||||
"[JoinIRVerifier] Header block {} not found ({} blocks in func)",
|
||||
header_block,
|
||||
func.blocks.len()
|
||||
)
|
||||
});
|
||||
let phi_count = header_block_data
|
||||
.instructions
|
||||
.iter()
|
||||
@ -715,7 +729,7 @@ fn verify_exit_line(
|
||||
boundary: &JoinInlineBoundary,
|
||||
) {
|
||||
// Check 1: Exit block exists
|
||||
if exit_block.0 >= func.blocks.len() {
|
||||
if !func.blocks.contains_key(&exit_block) {
|
||||
panic!(
|
||||
"[JoinIRVerifier] Exit block {} out of range (func has {} blocks)",
|
||||
exit_block.0,
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -39,10 +39,16 @@ impl MirBuilder {
|
||||
// Phase 170-4: Structure-based routing option
|
||||
// When NYASH_JOINIR_STRUCTURE_ONLY=1, skip function name whitelist
|
||||
// and route purely based on loop structure analysis
|
||||
let structure_only = std::env::var("NYASH_JOINIR_STRUCTURE_ONLY")
|
||||
// Phase 196: Default to structure-first routing now that LoopBuilder is removed.
|
||||
// - Default: ON (structure_only = true) to allow JoinIR patterns to run for all loops.
|
||||
// - To revert to the previous whitelist-only behavior, set NYASH_JOINIR_STRUCTURE_ONLY=0.
|
||||
let structure_only = match std::env::var("NYASH_JOINIR_STRUCTURE_ONLY")
|
||||
.ok()
|
||||
.as_deref()
|
||||
== Some("1");
|
||||
{
|
||||
Some("0") | Some("off") => false,
|
||||
_ => true,
|
||||
};
|
||||
|
||||
if structure_only {
|
||||
trace::trace().routing("router", &func_name, "Structure-only mode enabled, skipping whitelist");
|
||||
|
||||
Reference in New Issue
Block a user