Files
hakorune/src/mir/join_ir/lowering/step_schedule.rs

417 lines
14 KiB
Rust

//! 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<Pattern2StepKind>,
reason: &'static str,
}
impl Pattern2StepSchedule {
pub(crate) fn iter(&self) -> impl Iterator<Item = Pattern2StepKind> + '_ {
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::<Vec<_>>()
.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<Pattern2StepKind> {
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<Pattern2StepKind> {
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<CarrierVar>) -> 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);
}
}