refactor(joinir): boxify Pattern2 routing and schedule facts

This commit is contained in:
nyash-codex
2025-12-17 21:24:46 +09:00
parent 950560a3d9
commit ae2a1d4339
11 changed files with 550 additions and 148 deletions

View File

@ -75,7 +75,8 @@ use crate::mir::join_ir::lowering::loop_body_local_env::LoopBodyLocalEnv;
use crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape;
use crate::mir::join_ir::lowering::loop_update_analyzer::UpdateExpr;
use crate::mir::join_ir::lowering::step_schedule::{
build_pattern2_schedule_from_decision, decide_pattern2_schedule, Pattern2StepKind,
build_pattern2_schedule_from_decision, decide_pattern2_schedule, Pattern2ScheduleFactsBox,
Pattern2StepKind,
};
use crate::mir::join_ir::lowering::update_env::UpdateEnv;
use crate::mir::loop_canonicalizer::LoopSkeleton;
@ -381,12 +382,18 @@ pub(crate) fn lower_loop_with_break_minimal(
// Decide evaluation order (header/body-init/break/updates/tail) up-front.
// Phase 93 P0: Pass condition_only_recipe existence to schedule context.
// When recipe exists, body-init must happen before break check.
let schedule_decision = decide_pattern2_schedule(
let has_allowed_body_locals_in_conditions = loop_cond_scope.has_loop_body_local()
&& allowed_body_locals_for_conditions
.map(|allow| !allow.is_empty())
.unwrap_or(false);
let schedule_facts = Pattern2ScheduleFactsBox::gather(
body_local_env.as_ref().map(|env| &**env),
carrier_info,
condition_only_recipe.is_some(),
body_local_derived_recipe.is_some(),
has_allowed_body_locals_in_conditions,
);
let schedule_decision = decide_pattern2_schedule(&schedule_facts);
let schedule = build_pattern2_schedule_from_decision(&schedule_decision);
// Collect fragments per step; append them according to the schedule below.

View File

@ -91,7 +91,49 @@ pub(crate) struct ScheduleDebugContext {
pub has_body_local_derived_recipe: bool,
}
/// Decide Pattern2 schedule based on loop characteristics
/// 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
///
@ -111,30 +153,19 @@ pub(crate) struct ScheduleDebugContext {
/// # Returns
///
/// `ScheduleDecision` with decision, reason, and debug context
pub(crate) fn decide_pattern2_schedule(
body_local_env: Option<&LoopBodyLocalEnv>,
carrier_info: &CarrierInfo,
has_condition_only_recipe: bool,
has_body_local_derived_recipe: bool,
) -> ScheduleDecision {
let has_body_local_init = body_local_env.map(|env| !env.is_empty()).unwrap_or(false);
let has_loop_local_carrier = carrier_info
.carriers
.iter()
.any(|c| matches!(c.init, CarrierInit::LoopLocalZero));
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 body_init_first = has_condition_only_recipe
|| has_body_local_derived_recipe
|| has_body_local_init
|| has_loop_local_carrier;
let reason = if has_condition_only_recipe {
let reason = if facts.has_condition_only_recipe {
"ConditionOnly requires body-init before break"
} else if has_body_local_derived_recipe {
} else if facts.has_body_local_derived_recipe {
"BodyLocalDerived requires body-init before break"
} else if has_body_local_init {
} else if facts.has_body_local_init {
"body-local variables require init before break"
} else if has_loop_local_carrier {
} else if facts.has_loop_local_carrier {
"loop-local carrier requires init before break"
} else {
"default schedule"
@ -144,10 +175,10 @@ pub(crate) fn decide_pattern2_schedule(
body_init_first,
reason,
debug_ctx: ScheduleDebugContext {
has_body_local_init,
has_loop_local_carrier,
has_condition_only_recipe,
has_body_local_derived_recipe,
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,
},
}
}
@ -263,7 +294,8 @@ mod tests {
#[test]
fn default_schedule_break_before_body_init() {
let decision = decide_pattern2_schedule(None, &carrier_info(vec![]), false, false);
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(),
@ -283,12 +315,14 @@ mod tests {
let mut body_env = LoopBodyLocalEnv::new();
body_env.insert("tmp".to_string(), ValueId(5));
let decision = decide_pattern2_schedule(
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(),
@ -305,12 +339,9 @@ mod tests {
#[test]
fn loop_local_carrier_triggers_body_first() {
let decision = decide_pattern2_schedule(
None,
&carrier_info(vec![carrier(true)]),
false,
false,
);
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(),
@ -329,7 +360,8 @@ mod tests {
#[test]
fn condition_only_recipe_triggers_body_first() {
// Empty body_local_env but has condition_only_recipe
let decision = decide_pattern2_schedule(None, &carrier_info(vec![]), true, false);
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(),
@ -344,6 +376,24 @@ mod tests {
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();