417 lines
14 KiB
Rust
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);
|
|
}
|
|
}
|