refactor(joinir): split Pattern2 orchestrator into smaller steps

This commit is contained in:
nyash-codex
2025-12-18 00:44:31 +09:00
parent e4735f4054
commit 9bcda215f8
7 changed files with 268 additions and 181 deletions

View File

@ -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)
}
}

View File

@ -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(()),
}
}
}

View File

@ -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<String, UpdateExpr> {
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<String, UpdateExpr>) {
// 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
});
}

View File

@ -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;

View File

@ -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<Vec<ASTNode>>,
}
pub(crate) struct NormalizeBodyStepBox;
impl NormalizeBodyStepBox {
pub(crate) fn run(
builder: &mut MirBuilder,
condition: &ASTNode,
body: &[ASTNode],
inputs: &mut Pattern2Inputs,
verbose: bool,
) -> Result<NormalizedBodyResult, String> {
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
},
})
}
}

View File

@ -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<Vec<ASTNode>>,
pub carrier_updates: BTreeMap<String, UpdateExpr>,
}
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<PromoteStepResult, String> {
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<Vec<ASTNode>>), 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<String, UpdateExpr>) {
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
});
}

View File

@ -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,