//! Phase 47-A: Generic step scheduling for Pattern2/Pattern3 loops //! //! Determines evaluation order for loop steps (header cond, body init, break check, etc). //! Used by both P2 (break) and P3 (if-sum) patterns. //! //! This keeps the lowerer focused on emitting fragments, while this box decides //! how to interleave them (e.g., body-local init before break checks when the //! break depends on body-local values). use crate::config::env; use crate::config::env::joinir_dev::joinir_test_debug_enabled; use crate::mir::join_ir::lowering::carrier_info::{CarrierInfo, CarrierInit}; use crate::mir::join_ir::lowering::loop_body_local_env::LoopBodyLocalEnv; /// Steps that can be reordered by the scheduler. #[allow(dead_code)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum Pattern2StepKind { // P2 (Pattern2 Break) steps HeaderCond, // loop(cond) BodyInit, // local ch = ... BreakCheck, // if (cond) break Updates, // sum = sum + 1 Tail, // i = i + 1 // Phase 47-A: P3 (Pattern3 If-Sum) steps IfCond, // if (cond) in body ThenUpdates, // carrier updates in then branch ElseUpdates, // carrier updates in else branch (if any) // Phase 48-A: P4 (Pattern4 Continue) steps ContinueCheck, // if (cond) continue } impl Pattern2StepKind { fn as_str(&self) -> &'static str { match self { Pattern2StepKind::HeaderCond => "header-cond", Pattern2StepKind::BodyInit => "body-init", Pattern2StepKind::BreakCheck => "break", Pattern2StepKind::Updates => "updates", Pattern2StepKind::Tail => "tail", // Phase 47-A: P3 steps Pattern2StepKind::IfCond => "if-cond", Pattern2StepKind::ThenUpdates => "then-updates", Pattern2StepKind::ElseUpdates => "else-updates", // Phase 48-A: P4 steps Pattern2StepKind::ContinueCheck => "continue-check", } } } /// Data-driven schedule for Pattern 2 lowering. #[derive(Debug, Clone)] pub(crate) struct Pattern2StepSchedule { steps: Vec, reason: &'static str, } impl Pattern2StepSchedule { pub(crate) fn iter(&self) -> impl Iterator + '_ { self.steps.iter().copied() } pub(crate) fn reason(&self) -> &'static str { self.reason } pub(crate) fn steps(&self) -> &[Pattern2StepKind] { &self.steps } } /// Schedule decision result with reasoning (SSOT) #[derive(Debug, Clone)] pub(crate) struct ScheduleDecision { /// Whether body-init should come before break check pub body_init_first: bool, /// Human-readable reason for this decision pub reason: &'static str, /// Debug context for logging pub debug_ctx: ScheduleDebugContext, } /// Debug context for schedule decisions #[derive(Debug, Clone)] pub(crate) struct ScheduleDebugContext { pub has_body_local_init: bool, pub has_loop_local_carrier: bool, pub has_condition_only_recipe: bool, pub has_body_local_derived_recipe: bool, } /// Facts about Pattern2 that drive step scheduling. /// /// This struct is intentionally "facts only" (no decision). #[derive(Debug, Clone)] pub(crate) struct Pattern2ScheduleFacts { pub has_body_local_init: bool, pub has_loop_local_carrier: bool, pub has_condition_only_recipe: bool, pub has_body_local_derived_recipe: bool, } pub(crate) struct Pattern2ScheduleFactsBox; impl Pattern2ScheduleFactsBox { pub(crate) fn gather( body_local_env: Option<&LoopBodyLocalEnv>, carrier_info: &CarrierInfo, has_condition_only_recipe: bool, has_body_local_derived_recipe: bool, has_allowed_body_locals_in_conditions: bool, ) -> Pattern2ScheduleFacts { // NOTE: `body_local_env` may be empty here because it's populated after schedule // decision (Phase 191 body-local init lowering happens later). // // For Phase 92+ patterns where conditions reference allowed body-local variables // (e.g., `ch == ""`), we must still schedule BodyInit before BreakCheck. let has_body_local_init = body_local_env.map(|env| !env.is_empty()).unwrap_or(false) || has_allowed_body_locals_in_conditions; let has_loop_local_carrier = carrier_info .carriers .iter() .any(|c| matches!(c.init, CarrierInit::LoopLocalZero)); Pattern2ScheduleFacts { has_body_local_init, has_loop_local_carrier, has_condition_only_recipe, has_body_local_derived_recipe, } } } /// Decide Pattern2 schedule based on loop characteristics (SSOT). /// /// Phase 93 Refactoring: Single source of truth for schedule decisions /// /// # Decision Logic /// /// Body-init comes BEFORE break check if any of these conditions are true: /// 1. ConditionOnly recipe exists (derived slots need recalculation) /// 2. Body-local variables exist (break condition depends on them) /// 3. Loop-local carriers exist (need initialization before use) /// /// # Arguments /// /// * `body_local_env` - Body-local variable environment /// * `carrier_info` - Carrier information (for loop-local detection) /// * `has_condition_only_recipe` - Whether ConditionOnly derived slots exist /// /// # Returns /// /// `ScheduleDecision` with decision, reason, and debug context pub(crate) fn decide_pattern2_schedule(facts: &Pattern2ScheduleFacts) -> ScheduleDecision { let body_init_first = facts.has_condition_only_recipe || facts.has_body_local_derived_recipe || facts.has_body_local_init || facts.has_loop_local_carrier; let reason = if facts.has_condition_only_recipe { "ConditionOnly requires body-init before break" } else if facts.has_body_local_derived_recipe { "BodyLocalDerived requires body-init before break" } else if facts.has_body_local_init { "body-local variables require init before break" } else if facts.has_loop_local_carrier { "loop-local carrier requires init before break" } else { "default schedule" }; ScheduleDecision { body_init_first, reason, debug_ctx: ScheduleDebugContext { has_body_local_init: facts.has_body_local_init, has_loop_local_carrier: facts.has_loop_local_carrier, has_condition_only_recipe: facts.has_condition_only_recipe, has_body_local_derived_recipe: facts.has_body_local_derived_recipe, }, } } /// Build a schedule for Pattern 2 lowering. /// /// - Default P2: header → break → body-init → updates → tail /// - Body-local break dependency (DigitPos/_atoi style): /// header → body-init → break → updates → tail pub(crate) fn build_pattern2_schedule_from_decision( decision: &ScheduleDecision, ) -> Pattern2StepSchedule { let schedule = if decision.body_init_first { Pattern2StepSchedule { steps: vec![ Pattern2StepKind::HeaderCond, Pattern2StepKind::BodyInit, Pattern2StepKind::BreakCheck, Pattern2StepKind::Updates, Pattern2StepKind::Tail, ], reason: decision.reason, } } else { Pattern2StepSchedule { steps: vec![ Pattern2StepKind::HeaderCond, Pattern2StepKind::BreakCheck, Pattern2StepKind::BodyInit, Pattern2StepKind::Updates, Pattern2StepKind::Tail, ], reason: decision.reason, } }; log_schedule_from_decision(decision, &schedule); schedule } fn log_schedule_from_decision(decision: &ScheduleDecision, schedule: &Pattern2StepSchedule) { if !(env::joinir_dev_enabled() || joinir_test_debug_enabled()) { return; } let steps_desc = schedule .steps() .iter() .map(Pattern2StepKind::as_str) .collect::>() .join(" -> "); eprintln!( "[phase93/schedule] steps={} reason={} ctx={{body_local_init={}, loop_local_carrier={}, condition_only={}, body_local_derived={}}}", steps_desc, schedule.reason(), decision.debug_ctx.has_body_local_init, decision.debug_ctx.has_loop_local_carrier, decision.debug_ctx.has_condition_only_recipe, decision.debug_ctx.has_body_local_derived_recipe ); } /// Phase 47-A: Generate step schedule for Pattern3 (if-sum) loops #[allow(dead_code)] pub(crate) fn pattern3_if_sum_schedule() -> Vec { vec![ Pattern2StepKind::HeaderCond, // loop(i < n) Pattern2StepKind::IfCond, // if (i > 0) Pattern2StepKind::ThenUpdates, // sum = sum + i // ElseUpdates omitted for minimal (no else branch) Pattern2StepKind::Tail, // i = i + 1 ] } /// Phase 48-A: Generate step schedule for Pattern4 (continue) loops #[allow(dead_code)] pub(crate) fn pattern4_continue_schedule() -> Vec { vec![ Pattern2StepKind::HeaderCond, // loop(i < n) Pattern2StepKind::ContinueCheck, // if (i == 2) continue (skip processing) Pattern2StepKind::Updates, // count = count + 1 (processing) Pattern2StepKind::Tail, // i = i + 1 ] } #[cfg(test)] mod tests { use super::*; use crate::mir::join_ir::lowering::carrier_info::{CarrierRole, CarrierVar}; use crate::mir::ValueId; fn carrier(loop_local: bool) -> CarrierVar { let init = if loop_local { CarrierInit::LoopLocalZero } else { CarrierInit::FromHost }; CarrierVar::with_role_and_init("c".to_string(), ValueId(1), CarrierRole::LoopState, init) } fn carrier_info(carriers: Vec) -> CarrierInfo { CarrierInfo { loop_var_name: "i".to_string(), loop_var_id: ValueId(0), carriers, trim_helper: None, promoted_loopbodylocals: vec![], #[cfg(feature = "normalized_dev")] promoted_bindings: std::collections::BTreeMap::new(), } } #[test] fn default_schedule_break_before_body_init() { let facts = Pattern2ScheduleFactsBox::gather(None, &carrier_info(vec![]), false, false, false); let decision = decide_pattern2_schedule(&facts); let schedule = build_pattern2_schedule_from_decision(&decision); assert_eq!( schedule.steps(), &[ Pattern2StepKind::HeaderCond, Pattern2StepKind::BreakCheck, Pattern2StepKind::BodyInit, Pattern2StepKind::Updates, Pattern2StepKind::Tail ] ); assert_eq!(schedule.reason(), "default schedule"); } #[test] fn body_local_moves_init_before_break() { let mut body_env = LoopBodyLocalEnv::new(); body_env.insert("tmp".to_string(), ValueId(5)); let facts = Pattern2ScheduleFactsBox::gather( Some(&body_env), &carrier_info(vec![carrier(false)]), false, false, false, ); let decision = decide_pattern2_schedule(&facts); let schedule = build_pattern2_schedule_from_decision(&decision); assert_eq!( schedule.steps(), &[ Pattern2StepKind::HeaderCond, Pattern2StepKind::BodyInit, Pattern2StepKind::BreakCheck, Pattern2StepKind::Updates, Pattern2StepKind::Tail ] ); assert_eq!(schedule.reason(), "body-local variables require init before break"); } #[test] fn loop_local_carrier_triggers_body_first() { let facts = Pattern2ScheduleFactsBox::gather(None, &carrier_info(vec![carrier(true)]), false, false, false); let decision = decide_pattern2_schedule(&facts); let schedule = build_pattern2_schedule_from_decision(&decision); assert_eq!( schedule.steps(), &[ Pattern2StepKind::HeaderCond, Pattern2StepKind::BodyInit, Pattern2StepKind::BreakCheck, Pattern2StepKind::Updates, Pattern2StepKind::Tail ] ); assert_eq!(schedule.reason(), "loop-local carrier requires init before break"); } /// Phase 93 P0: ConditionOnly recipe triggers body-init before break #[test] fn condition_only_recipe_triggers_body_first() { // Empty body_local_env but has condition_only_recipe let facts = Pattern2ScheduleFactsBox::gather(None, &carrier_info(vec![]), true, false, false); let decision = decide_pattern2_schedule(&facts); let schedule = build_pattern2_schedule_from_decision(&decision); assert_eq!( schedule.steps(), &[ Pattern2StepKind::HeaderCond, Pattern2StepKind::BodyInit, Pattern2StepKind::BreakCheck, Pattern2StepKind::Updates, Pattern2StepKind::Tail ] ); assert_eq!(schedule.reason(), "ConditionOnly requires body-init before break"); } #[test] fn allowed_body_local_deps_trigger_body_first_even_if_env_empty() { let facts = Pattern2ScheduleFactsBox::gather(None, &carrier_info(vec![]), false, false, true); let decision = decide_pattern2_schedule(&facts); let schedule = build_pattern2_schedule_from_decision(&decision); assert_eq!( schedule.steps(), &[ Pattern2StepKind::HeaderCond, Pattern2StepKind::BodyInit, Pattern2StepKind::BreakCheck, Pattern2StepKind::Updates, Pattern2StepKind::Tail ] ); assert_eq!(schedule.reason(), "body-local variables require init before break"); } #[test] fn test_pattern3_if_sum_schedule() { let schedule = pattern3_if_sum_schedule(); assert_eq!(schedule.len(), 4); assert_eq!(schedule[0], Pattern2StepKind::HeaderCond); assert_eq!(schedule[1], Pattern2StepKind::IfCond); assert_eq!(schedule[2], Pattern2StepKind::ThenUpdates); assert_eq!(schedule[3], Pattern2StepKind::Tail); } #[test] fn test_pattern4_continue_schedule() { let schedule = pattern4_continue_schedule(); assert_eq!(schedule.len(), 4); assert_eq!(schedule[0], Pattern2StepKind::HeaderCond); assert_eq!(schedule[1], Pattern2StepKind::ContinueCheck); assert_eq!(schedule[2], Pattern2StepKind::Updates); assert_eq!(schedule[3], Pattern2StepKind::Tail); } }