diff --git a/src/mir/builder/control_flow/joinir/patterns/pattern2_lowering_orchestrator.rs b/src/mir/builder/control_flow/joinir/patterns/pattern2_lowering_orchestrator.rs index fc097f9f..a933378d 100644 --- a/src/mir/builder/control_flow/joinir/patterns/pattern2_lowering_orchestrator.rs +++ b/src/mir/builder/control_flow/joinir/patterns/pattern2_lowering_orchestrator.rs @@ -8,9 +8,12 @@ use crate::mir::builder::MirBuilder; use crate::mir::ValueId; use super::pattern2_steps::apply_policy_step_box::ApplyPolicyStepBox; +use super::pattern2_steps::body_local_derived_step_box::BodyLocalDerivedStepBox; +use super::pattern2_steps::carrier_updates_step_box::CarrierUpdatesStepBox; use super::pattern2_steps::emit_joinir_step_box::EmitJoinIRStepBox; use super::pattern2_steps::gather_facts_step_box::GatherFactsStepBox; use super::pattern2_steps::merge_step_box::MergeStepBox; +use super::pattern2_steps::normalize_body_step_box::NormalizeBodyStepBox; use super::pattern2_steps::post_loop_early_return_step_box::PostLoopEarlyReturnStepBox; use super::pattern2_steps::promote_step_box::PromoteStepBox; @@ -38,25 +41,31 @@ impl Pattern2LoweringOrchestrator { let facts = GatherFactsStepBox::gather(builder, condition, body, fn_body, &ctx, verbose)?; let inputs = ApplyPolicyStepBox::apply(condition, body, facts)?; - let mut promoted = PromoteStepBox::run(builder, condition, body, inputs, debug, verbose)?; - let normalized_body = promoted.normalized_body.take(); + let promoted = PromoteStepBox::run(builder, condition, body, inputs, debug, verbose)?; + let mut inputs = promoted.inputs; + + let normalized = NormalizeBodyStepBox::run(builder, condition, body, &mut inputs, verbose)?; + let normalized_body = normalized.normalized_body; let analysis_body = normalized_body.as_deref().unwrap_or(body); - let effective_break_condition = promoted.effective_break_condition; - let carrier_updates = promoted.carrier_updates; + let effective_break_condition = normalized.effective_break_condition; + + BodyLocalDerivedStepBox::apply(analysis_body, &mut inputs, verbose)?; + let carrier_updates = + CarrierUpdatesStepBox::analyze_and_filter(analysis_body, &mut inputs, verbose); let emitted = EmitJoinIRStepBox::emit( builder, condition, analysis_body, &effective_break_condition, &carrier_updates, - &mut promoted.inputs, + &mut inputs, debug, verbose, skeleton, )?; let out = MergeStepBox::merge(builder, emitted.join_module, emitted.boundary, debug)?; - PostLoopEarlyReturnStepBox::maybe_emit(builder, promoted.inputs.post_loop_early_return.as_ref())?; + PostLoopEarlyReturnStepBox::maybe_emit(builder, inputs.post_loop_early_return.as_ref())?; Ok(out) } } diff --git a/src/mir/builder/control_flow/joinir/patterns/pattern2_steps/body_local_derived_step_box.rs b/src/mir/builder/control_flow/joinir/patterns/pattern2_steps/body_local_derived_step_box.rs new file mode 100644 index 00000000..aee4fbb0 --- /dev/null +++ b/src/mir/builder/control_flow/joinir/patterns/pattern2_steps/body_local_derived_step_box.rs @@ -0,0 +1,38 @@ +//! BodyLocalDerivedStepBox (Phase 94, extracted in Phase 5) +//! +//! Responsibility: +//! - Apply the Phase 94 P5b escape-derived routing to Pattern2 inputs. +//! - No JoinIR generation. Purely sets `inputs.body_local_derived_recipe` or fails fast. + +use crate::ast::ASTNode; + +use super::super::pattern2_inputs_facts_box::{Pattern2DebugLog, Pattern2Inputs}; +use super::super::policies::p5b_escape_derived_policy::classify_p5b_escape_derived; +use super::super::policies::PolicyDecision; + +pub(crate) struct BodyLocalDerivedStepBox; + +impl BodyLocalDerivedStepBox { + pub(crate) fn apply( + analysis_body: &[ASTNode], + inputs: &mut Pattern2Inputs, + verbose: bool, + ) -> Result<(), String> { + match classify_p5b_escape_derived(analysis_body, &inputs.loop_var_name) { + PolicyDecision::Use(recipe) => { + Pattern2DebugLog::new(verbose).log( + "phase94", + format!( + "Phase 94: Enabled BodyLocalDerived for '{}' (counter='{}', pre_delta={}, post_delta={})", + recipe.name, recipe.loop_counter_name, recipe.pre_delta, recipe.post_delta + ), + ); + inputs.body_local_derived_recipe = Some(recipe); + Ok(()) + } + PolicyDecision::Reject(reason) => Err(format!("[cf_loop/pattern2] {}", reason)), + PolicyDecision::None => Ok(()), + } + } +} + diff --git a/src/mir/builder/control_flow/joinir/patterns/pattern2_steps/carrier_updates_step_box.rs b/src/mir/builder/control_flow/joinir/patterns/pattern2_steps/carrier_updates_step_box.rs new file mode 100644 index 00000000..1b4388ec --- /dev/null +++ b/src/mir/builder/control_flow/joinir/patterns/pattern2_steps/carrier_updates_step_box.rs @@ -0,0 +1,86 @@ +//! CarrierUpdatesStepBox (Phase 176+, extracted in Phase 5) +//! +//! Responsibility: +//! - Analyze carrier updates for Pattern2 (or use policy override). +//! - Filter carriers to only those required by updates / condition-only / loop-local-zero. +//! - Ensure JoinValue env has join-ids for carriers referenced only from body updates. + +use crate::ast::ASTNode; +use crate::mir::join_ir::lowering::carrier_info::{CarrierInfo, CarrierInit, CarrierRole}; +use crate::mir::join_ir::lowering::condition_env::ConditionBinding; +use crate::mir::join_ir::lowering::loop_update_analyzer::{LoopUpdateAnalyzer, UpdateExpr}; + +use super::super::pattern2_inputs_facts_box::{Pattern2DebugLog, Pattern2Inputs}; + +use std::collections::BTreeMap; + +pub(crate) struct CarrierUpdatesStepBox; + +impl CarrierUpdatesStepBox { + pub(crate) fn analyze_and_filter( + analysis_body: &[ASTNode], + inputs: &mut Pattern2Inputs, + verbose: bool, + ) -> BTreeMap { + let carrier_updates = if let Some(override_map) = inputs.carrier_updates_override.take() { + override_map + } else { + LoopUpdateAnalyzer::analyze_carrier_updates(analysis_body, &inputs.carrier_info.carriers) + }; + Pattern2DebugLog::new(verbose).log( + "updates", + format!("Phase 176-3: Analyzed {} carrier updates", carrier_updates.len()), + ); + + let original_carrier_count = inputs.carrier_info.carriers.len(); + filter_carriers_for_updates(&mut inputs.carrier_info, &carrier_updates); + Pattern2DebugLog::new(verbose).log( + "updates", + format!( + "Phase 176-4: Filtered carriers: {} → {} (kept only carriers with updates/condition-only/loop-local-zero)", + original_carrier_count, + inputs.carrier_info.carriers.len() + ), + ); + + // Ensure env has join-ids for carriers that are referenced only from body updates. + for carrier in &inputs.carrier_info.carriers { + if inputs.env.get(&carrier.name).is_none() { + let join_value = carrier + .join_id + .unwrap_or_else(|| inputs.join_value_space.alloc_param()); + inputs.env.insert(carrier.name.clone(), join_value); + + if carrier.init != CarrierInit::LoopLocalZero { + inputs.condition_bindings.push(ConditionBinding { + name: carrier.name.clone(), + host_value: carrier.host_id, + join_value, + }); + } else { + Pattern2DebugLog::new(verbose).log( + "updates", + format!( + "Phase 247-EX: Skipping host binding for loop-local carrier '{}' (init=LoopLocalZero)", + carrier.name + ), + ); + } + } + } + + carrier_updates + } +} + +fn filter_carriers_for_updates(info: &mut CarrierInfo, updates: &BTreeMap) { + // Keep carriers that: + // - have updates + // - are condition-only (used for break condition) + // - are loop-local-zero (policy-injected for derived computations) + info.carriers.retain(|c| { + updates.contains_key(&c.name) + || c.role == CarrierRole::ConditionOnly + || c.init == CarrierInit::LoopLocalZero + }); +} diff --git a/src/mir/builder/control_flow/joinir/patterns/pattern2_steps/mod.rs b/src/mir/builder/control_flow/joinir/patterns/pattern2_steps/mod.rs index 4b6cce6d..c38c8a28 100644 --- a/src/mir/builder/control_flow/joinir/patterns/pattern2_steps/mod.rs +++ b/src/mir/builder/control_flow/joinir/patterns/pattern2_steps/mod.rs @@ -7,8 +7,11 @@ //! at the step boundary. pub(crate) mod apply_policy_step_box; +pub(crate) mod body_local_derived_step_box; +pub(crate) mod carrier_updates_step_box; pub(crate) mod emit_joinir_step_box; pub(crate) mod gather_facts_step_box; pub(crate) mod merge_step_box; +pub(crate) mod normalize_body_step_box; pub(crate) mod post_loop_early_return_step_box; pub(crate) mod promote_step_box; diff --git a/src/mir/builder/control_flow/joinir/patterns/pattern2_steps/normalize_body_step_box.rs b/src/mir/builder/control_flow/joinir/patterns/pattern2_steps/normalize_body_step_box.rs new file mode 100644 index 00000000..a1429686 --- /dev/null +++ b/src/mir/builder/control_flow/joinir/patterns/pattern2_steps/normalize_body_step_box.rs @@ -0,0 +1,86 @@ +//! NormalizeBodyStepBox (Trim + break condition normalization) +//! +//! Responsibility: +//! - Apply trim normalization when enabled (Phase 93+). +//! - Return the effective break condition and an optional normalized body. + +use crate::ast::ASTNode; +use crate::mir::builder::MirBuilder; + +use super::super::pattern2_inputs_facts_box::{Pattern2DebugLog, Pattern2Inputs}; + +pub(crate) struct NormalizedBodyResult { + pub effective_break_condition: ASTNode, + pub normalized_body: Option>, +} + +pub(crate) struct NormalizeBodyStepBox; + +impl NormalizeBodyStepBox { + pub(crate) fn run( + builder: &mut MirBuilder, + condition: &ASTNode, + body: &[ASTNode], + inputs: &mut Pattern2Inputs, + verbose: bool, + ) -> Result { + let log = Pattern2DebugLog::new(verbose); + let mut alloc_join_value = || inputs.join_value_space.alloc_param(); + + // Trim-like loops have their own canonicalizer path; keep existing behavior: + // read_digits(loop(true)) disables trim handling here. + let disable_trim = inputs.is_loop_true_read_digits; + + let effective_break_condition = if !disable_trim { + if let Some(trim_result) = + super::super::trim_loop_lowering::TrimLoopLowerer::try_lower_trim_like_loop( + builder, + &inputs.scope, + condition, + &inputs.break_condition_node, + body, + &inputs.loop_var_name, + &mut inputs.carrier_info, + &mut alloc_join_value, + )? + { + log.log("trim", "TrimLoopLowerer processed Trim pattern successfully"); + inputs.carrier_info = trim_result.carrier_info; + inputs.condition_only_recipe = trim_result.condition_only_recipe; + trim_result.condition + } else { + inputs.break_condition_node.clone() + } + } else { + inputs.break_condition_node.clone() + }; + + use crate::mir::join_ir::lowering::complex_addend_normalizer::{ + ComplexAddendNormalizer, NormalizationResult, + }; + let mut normalized_body = Vec::new(); + let mut has_normalization = false; + + for node in body { + match ComplexAddendNormalizer::normalize_assign(node) { + NormalizationResult::Normalized { + temp_def, new_assign, .. + } => { + normalized_body.push(temp_def); + normalized_body.push(new_assign); + has_normalization = true; + } + NormalizationResult::Unchanged => normalized_body.push(node.clone()), + } + } + + Ok(NormalizedBodyResult { + effective_break_condition, + normalized_body: if has_normalization { + Some(normalized_body) + } else { + None + }, + }) + } +} diff --git a/src/mir/builder/control_flow/joinir/patterns/pattern2_steps/promote_step_box.rs b/src/mir/builder/control_flow/joinir/patterns/pattern2_steps/promote_step_box.rs index 82f71265..e3e7e0db 100644 --- a/src/mir/builder/control_flow/joinir/patterns/pattern2_steps/promote_step_box.rs +++ b/src/mir/builder/control_flow/joinir/patterns/pattern2_steps/promote_step_box.rs @@ -2,34 +2,18 @@ //! //! Responsibility: //! - promotion / read-only slot routing for body-local vars in conditions -//! - trim normalization (when enabled) -//! - derived-slot routing (Phase 94) -//! - carrier update analysis + filtering +//! - carrier preparation for JoinIR emission use crate::ast::ASTNode; use crate::mir::builder::MirBuilder; -use crate::mir::join_ir::lowering::carrier_info::{CarrierInfo, CarrierInit}; -use crate::mir::join_ir::lowering::loop_update_analyzer::UpdateExpr; use crate::mir::loop_pattern_detection::error_messages; use super::super::body_local_policy::{classify_for_pattern2, BodyLocalRoute}; -use super::super::pattern2_inputs_facts_box::{Pattern2DebugLog, Pattern2Inputs}; -use super::super::policies::p5b_escape_derived_policy::{classify_p5b_escape_derived, P5bEscapeDerivedDecision}; +use super::super::pattern2_inputs_facts_box::Pattern2Inputs; use super::super::policies::PolicyDecision; -use std::collections::BTreeMap; - pub(crate) struct PromoteStepResult { pub inputs: Pattern2Inputs, - pub effective_break_condition: ASTNode, - pub normalized_body: Option>, - pub carrier_updates: BTreeMap, -} - -impl PromoteStepResult { - pub(crate) fn analysis_body<'a>(&'a self, original_body: &'a [ASTNode]) -> &'a [ASTNode] { - self.normalized_body.as_deref().unwrap_or(original_body) - } } pub(crate) struct PromoteStepBox; @@ -44,100 +28,7 @@ impl PromoteStepBox { verbose: bool, ) -> Result { Self::promote_and_prepare_carriers(builder, condition, body, &mut inputs, debug, verbose)?; - - let (effective_break_condition, normalized_body) = - Self::apply_trim_and_normalize(builder, condition, body, &mut inputs, verbose)?; - - let analysis_body = normalized_body.as_deref().unwrap_or(body); - - // Phase 94: Detect P5b escape-derived (`ch` reassignment + escape counter). - match classify_p5b_escape_derived(analysis_body, &inputs.loop_var_name) { - P5bEscapeDerivedDecision::Use(recipe) => { - Pattern2DebugLog::new(verbose).log( - "phase94", - format!( - "Phase 94: Enabled BodyLocalDerived for '{}' (counter='{}', pre_delta={}, post_delta={})", - recipe.name, recipe.loop_counter_name, recipe.pre_delta, recipe.post_delta - ), - ); - inputs.body_local_derived_recipe = Some(recipe); - } - P5bEscapeDerivedDecision::Reject(reason) => return Err(format!("[cf_loop/pattern2] {}", reason)), - P5bEscapeDerivedDecision::None => { - let has_ch_reassign = analysis_body.iter().any(|n| match n { - ASTNode::Assignment { target, .. } => matches!( - target.as_ref(), - ASTNode::Variable { name, .. } if name == "ch" - ), - _ => false, - }); - if crate::config::env::joinir_dev::strict_enabled() && has_ch_reassign { - return Err(format!( - "[cf_loop/pattern2] {}", - crate::mir::join_ir::lowering::error_tags::freeze( - "[phase94/body_local_derived/contract/unhandled_reassign] Body-local reassignment to 'ch' detected but not supported by Phase 94 recipe" - ) - )); - } - } - } - - use crate::mir::join_ir::lowering::loop_update_analyzer::LoopUpdateAnalyzer; - let carrier_updates = if let Some(override_map) = inputs.carrier_updates_override.take() { - override_map - } else { - LoopUpdateAnalyzer::analyze_carrier_updates(analysis_body, &inputs.carrier_info.carriers) - }; - Pattern2DebugLog::new(verbose).log( - "updates", - format!("Phase 176-3: Analyzed {} carrier updates", carrier_updates.len()), - ); - - let original_carrier_count = inputs.carrier_info.carriers.len(); - filter_carriers_for_updates(&mut inputs.carrier_info, &carrier_updates); - Pattern2DebugLog::new(verbose).log( - "updates", - format!( - "Phase 176-4: Filtered carriers: {} → {} (kept only carriers with updates/condition-only/loop-local-zero)", - original_carrier_count, - inputs.carrier_info.carriers.len() - ), - ); - - // Ensure env has join-ids for carriers that are referenced only from body updates. - for carrier in &inputs.carrier_info.carriers { - if inputs.env.get(&carrier.name).is_none() { - let join_value = carrier - .join_id - .unwrap_or_else(|| inputs.join_value_space.alloc_param()); - - inputs.env.insert(carrier.name.clone(), join_value); - - if carrier.init != CarrierInit::LoopLocalZero { - use crate::mir::join_ir::lowering::condition_env::ConditionBinding; - inputs.condition_bindings.push(ConditionBinding { - name: carrier.name.clone(), - host_value: carrier.host_id, - join_value, - }); - } else { - Pattern2DebugLog::new(verbose).log( - "updates", - format!( - "Phase 247-EX: Skipping host binding for loop-local carrier '{}' (init=LoopLocalZero)", - carrier.name - ), - ); - } - } - } - - Ok(PromoteStepResult { - inputs, - effective_break_condition, - normalized_body, - carrier_updates, - }) + Ok(PromoteStepResult { inputs }) } pub(in crate::mir::builder) fn promote_and_prepare_carriers( @@ -286,67 +177,4 @@ impl PromoteStepBox { Ok(()) } - fn apply_trim_and_normalize( - builder: &mut MirBuilder, - condition: &ASTNode, - body: &[ASTNode], - inputs: &mut Pattern2Inputs, - verbose: bool, - ) -> Result<(ASTNode, Option>), String> { - let log = Pattern2DebugLog::new(verbose); - let mut alloc_join_value = || inputs.join_value_space.alloc_param(); - - let disable_trim = inputs.is_loop_true_read_digits; - - let effective_break_condition = if !disable_trim { - if let Some(trim_result) = super::super::trim_loop_lowering::TrimLoopLowerer::try_lower_trim_like_loop( - builder, - &inputs.scope, - condition, - &inputs.break_condition_node, - body, - &inputs.loop_var_name, - &mut inputs.carrier_info, - &mut alloc_join_value, - )? { - log.log("trim", "TrimLoopLowerer processed Trim pattern successfully"); - inputs.carrier_info = trim_result.carrier_info; - inputs.condition_only_recipe = trim_result.condition_only_recipe; - trim_result.condition - } else { - inputs.break_condition_node.clone() - } - } else { - inputs.break_condition_node.clone() - }; - - use crate::mir::join_ir::lowering::complex_addend_normalizer::{ComplexAddendNormalizer, NormalizationResult}; - let mut normalized_body = Vec::new(); - let mut has_normalization = false; - - for node in body { - match ComplexAddendNormalizer::normalize_assign(node) { - NormalizationResult::Normalized { temp_def, new_assign, .. } => { - normalized_body.push(temp_def); - normalized_body.push(new_assign); - has_normalization = true; - } - NormalizationResult::Unchanged => normalized_body.push(node.clone()), - } - } - - Ok(( - effective_break_condition, - if has_normalization { Some(normalized_body) } else { None }, - )) - } -} - -fn filter_carriers_for_updates(carrier_info: &mut CarrierInfo, carrier_updates: &BTreeMap) { - use crate::mir::join_ir::lowering::carrier_info::{CarrierInit, CarrierRole}; - carrier_info.carriers.retain(|carrier| { - carrier_updates.contains_key(&carrier.name) - || carrier.role == CarrierRole::ConditionOnly - || carrier.init == CarrierInit::LoopLocalZero - }); } diff --git a/src/mir/builder/control_flow/joinir/patterns/policies/p5b_escape_derived_policy.rs b/src/mir/builder/control_flow/joinir/patterns/policies/p5b_escape_derived_policy.rs index d83c87c8..06f8fadd 100644 --- a/src/mir/builder/control_flow/joinir/patterns/policies/p5b_escape_derived_policy.rs +++ b/src/mir/builder/control_flow/joinir/patterns/policies/p5b_escape_derived_policy.rs @@ -29,12 +29,26 @@ pub fn classify_p5b_escape_derived( body: &[ASTNode], loop_var_name: &str, ) -> P5bEscapeDerivedDecision { + let strict = joinir_dev::strict_enabled(); + let has_ch_init = find_local_init_expr(body, "ch").is_some(); + let has_ch_reassign = has_assignment_to_var(body, "ch"); + let Some(info) = super::super::ast_feature_extractor::detect_escape_skip_pattern(body) else { + if strict && has_ch_init && has_ch_reassign { + return P5bEscapeDerivedDecision::Reject(error_tags::freeze( + "[phase94/body_local_derived/contract/unhandled_reassign] Body-local reassignment to 'ch' detected but escape shape is not recognized", + )); + } return P5bEscapeDerivedDecision::None; }; if info.counter_name != loop_var_name { // Not the loop counter we lower as the JoinIR loop var; ignore to avoid misrouting. + if strict && has_ch_init && has_ch_reassign { + return P5bEscapeDerivedDecision::Reject(error_tags::freeze( + "[phase94/body_local_derived/contract/loop_counter_mismatch] Body-local reassignment to 'ch' detected but loop counter does not match Pattern2 loop var", + )); + } return P5bEscapeDerivedDecision::None; } @@ -55,6 +69,29 @@ pub fn classify_p5b_escape_derived( } } +fn has_assignment_to_var(body: &[ASTNode], name: &str) -> bool { + fn node_has_assignment(node: &ASTNode, name: &str) -> bool { + match node { + ASTNode::Assignment { target, .. } => is_var_named(target.as_ref(), name), + ASTNode::If { + then_body, + else_body, + .. + } => { + then_body.iter().any(|n| node_has_assignment(n, name)) + || else_body + .as_ref() + .map_or(false, |e| e.iter().any(|n| node_has_assignment(n, name))) + } + ASTNode::Loop { body, .. } => body.iter().any(|n| node_has_assignment(n, name)), + ASTNode::ScopeBox { body, .. } => body.iter().any(|n| node_has_assignment(n, name)), + _ => false, + } + } + + body.iter().any(|n| node_has_assignment(n, name)) +} + fn build_recipe_from_info( body: &[ASTNode], info: &EscapeSkipPatternInfo,