refactor(joinir): Phase 106 Pattern2 step boxes

This commit is contained in:
nyash-codex
2025-12-17 22:01:19 +09:00
parent 712e1ad755
commit 5f4d8ba112
11 changed files with 733 additions and 410 deletions

View File

@ -59,6 +59,7 @@ pub(in crate::mir::builder) mod read_digits_break_condition_box; // Phase 104: b
pub(in crate::mir::builder) mod pattern2_break_condition_policy_router; // Phase 105: policy router box for Pattern2 break condition
pub(in crate::mir::builder) mod pattern2_inputs_facts_box; // Phase 105: Pattern2 input facts (analysis only)
pub(in crate::mir::builder) mod pattern2_lowering_orchestrator; // Phase 105: Pattern2 orchestration (wiring/emission)
pub(in crate::mir::builder) mod pattern2_steps; // Phase 106: Pattern2 step boxes (pipeline SSOT)
pub(in crate::mir::builder) mod condition_env_builder;
pub(in crate::mir::builder) mod conversion_pipeline;
pub(in crate::mir::builder) mod exit_binding;

View File

@ -1,20 +1,17 @@
//! Pattern2 input analysis (facts collection only)
//!
//! This box collects everything Pattern2 lowering needs *without* emitting JoinIR:
//! This box collects everything Pattern2 lowering needs *without* emitting JoinIR
//! and *without* applying policy routing:
//! - capture/pinned local analysis
//! - mutable accumulator promotion into carriers
//! - condition env + JoinValueSpace initialization
//! - break-condition routing (policy SSOT)
//! - (optional) body-local allow-list + slot metadata (Phase 92 P3)
//!
//! The goal is to keep orchestration/emission code out of this module.
use super::pattern2_break_condition_policy_router::Pattern2BreakConditionPolicyRouterBox;
//! Policy routing (break condition normalization + allow-list) is a separate step
//! in Phase 106 (`pattern2_steps::ApplyPolicyStepBox`).
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::carrier_info::CarrierInfo;
use crate::mir::join_ir::lowering::condition_env::{ConditionBinding, ConditionEnv};
use crate::mir::join_ir::lowering::common::body_local_slot::ReadOnlyBodyLocalSlot;
use crate::mir::join_ir::lowering::debug_output_box::DebugOutputBox;
use crate::mir::join_ir::lowering::join_value_space::JoinValueSpace;
use crate::mir::join_ir::lowering::loop_body_local_env::LoopBodyLocalEnv;
@ -36,13 +33,25 @@ impl Pattern2DebugLog {
}
}
pub(crate) fn log(&self, tag: &str, message: impl AsRef<str>) {
pub(crate) fn log(&self, tag: &str, message: impl AsRef<str>) {
if self.verbose {
self.debug.log(tag, message.as_ref());
}
}
}
pub(in crate::mir::builder) struct Pattern2Facts {
pub loop_var_name: String,
pub loop_var_id: ValueId,
pub carrier_info: CarrierInfo,
pub scope: LoopScopeShape,
pub captured_env: CapturedEnv,
pub join_value_space: JoinValueSpace,
pub env: ConditionEnv,
pub condition_bindings: Vec<ConditionBinding>,
pub body_local_env: LoopBodyLocalEnv,
}
pub(in crate::mir::builder) struct Pattern2Inputs {
pub loop_var_name: String,
pub loop_var_id: ValueId,
@ -57,12 +66,10 @@ pub(in crate::mir::builder) struct Pattern2Inputs {
/// This must stay minimal (1 variable) and is validated by ReadOnlyBodyLocalSlotBox.
pub allowed_body_locals_for_conditions: Vec<String>,
/// Phase 92 P3: Diagnostics / debug metadata for the allow-listed variable.
pub read_only_body_local_slot: Option<ReadOnlyBodyLocalSlot>,
pub read_only_body_local_slot: Option<crate::mir::join_ir::lowering::common::body_local_slot::ReadOnlyBodyLocalSlot>,
/// Policy-routed "break when true" condition node.
pub break_condition_node: ASTNode,
/// loop(true) + break-only digitsread_digits_from family
///
/// Policy-routed: multiple breaks are normalized into a single `break when true`
/// condition, and Pattern2 must schedule body-init before break check.
pub is_loop_true_read_digits: bool,
/// Phase 93 P0: ConditionOnly recipe for derived slot recalculation
pub condition_only_recipe: Option<
@ -83,7 +90,7 @@ impl Pattern2InputsFactsBox {
fn_body: Option<&[ASTNode]>,
ctx: &super::pattern_pipeline::PatternPipelineContext,
verbose: bool,
) -> Result<Pattern2Inputs, String> {
) -> Result<Pattern2Facts, String> {
let log = Pattern2DebugLog::new(verbose);
use super::condition_env_builder::ConditionEnvBuilder;
use crate::mir::loop_pattern_detection::function_scope_capture::{analyze_captured_vars_v2, CapturedEnv};
@ -359,11 +366,7 @@ impl Pattern2InputsFactsBox {
}
let body_local_env = LoopBodyLocalEnv::new();
// Break condition extraction is policy-routed (SSOT).
let break_routing = Pattern2BreakConditionPolicyRouterBox::route(condition, body)?;
let mut inputs = Pattern2Inputs {
Ok(Pattern2Facts {
loop_var_name,
loop_var_id,
carrier_info,
@ -373,25 +376,6 @@ impl Pattern2InputsFactsBox {
env,
condition_bindings,
body_local_env,
allowed_body_locals_for_conditions: Vec::new(),
read_only_body_local_slot: None,
break_condition_node: break_routing.break_condition_node,
is_loop_true_read_digits: break_routing.is_loop_true_read_digits,
condition_only_recipe: None,
body_local_derived_recipe: None,
};
if !break_routing.allowed_body_locals_for_conditions.is_empty() {
use crate::mir::join_ir::lowering::common::body_local_slot::ReadOnlyBodyLocalSlotBox;
inputs.allowed_body_locals_for_conditions =
break_routing.allowed_body_locals_for_conditions.clone();
inputs.read_only_body_local_slot = Some(ReadOnlyBodyLocalSlotBox::extract_single(
&inputs.allowed_body_locals_for_conditions,
body,
)?);
}
Ok(inputs)
})
}
}

View File

@ -1,29 +1,17 @@
//! Pattern2 lowering orchestration (wiring + emission)
//!
//! This module is the "do the work" side of Pattern2:
//! - promotion/slot routing for body-local vars in conditions
//! - trim normalization (when enabled)
//! - derived-slot routing (Phase 94)
//! - carrier update analysis + filtering
//! - invoking JoinIR lowerer + boundary merge pipeline
//!
//! It intentionally depends on `Pattern2InputsFactsBox` for analysis-only inputs.
//! Phase 106: the orchestrator is intentionally thin.
//! Most "do the work" logic lives in explicit step boxes under `pattern2_steps/`.
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::condition_env::ConditionBinding;
use crate::mir::join_ir::lowering::loop_update_analyzer::UpdateExpr;
use crate::mir::loop_pattern_detection::error_messages;
use crate::mir::ValueId;
use super::pattern2_inputs_facts_box::{Pattern2DebugLog, Pattern2Inputs, Pattern2InputsFactsBox};
use super::body_local_policy::{classify_for_pattern2, BodyLocalRoute};
use super::policies::p5b_escape_derived_policy::{classify_p5b_escape_derived, P5bEscapeDerivedDecision};
use super::policies::PolicyDecision;
use std::collections::BTreeMap;
use super::pattern2_steps::apply_policy_step_box::ApplyPolicyStepBox;
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::promote_step_box::PromoteStepBox;
pub(crate) struct Pattern2LoweringOrchestrator;
@ -32,15 +20,12 @@ impl Pattern2LoweringOrchestrator {
builder: &mut MirBuilder,
condition: &ASTNode,
body: &[ASTNode],
func_name: &str,
_func_name: &str,
debug: bool,
fn_body: Option<&[ASTNode]>,
skeleton: Option<&crate::mir::loop_canonicalizer::LoopSkeleton>,
) -> Result<Option<ValueId>, String> {
use crate::mir::join_ir::lowering::loop_with_break_minimal::lower_loop_with_break_minimal;
let verbose = debug || crate::config::env::joinir_dev_enabled();
let log = Pattern2DebugLog::new(verbose);
super::super::trace::trace().debug("pattern2", "Calling Pattern 2 minimal lowerer");
@ -49,355 +34,26 @@ impl Pattern2LoweringOrchestrator {
super::super::trace::trace().varmap("pattern2_start", &builder.variable_ctx.variable_map);
let mut inputs = Pattern2InputsFactsBox::analyze(builder, condition, body, fn_body, &ctx, verbose)?;
let facts = GatherFactsStepBox::gather(builder, condition, body, fn_body, &ctx, verbose)?;
let inputs = ApplyPolicyStepBox::apply(condition, body, facts)?;
promote_and_prepare_carriers(builder, condition, body, &mut inputs, debug, verbose)?;
let (effective_break_condition, normalized_body) =
apply_trim_and_normalize(builder, condition, body, &mut inputs, verbose)?;
let mut promoted = PromoteStepBox::run(builder, condition, body, inputs, debug, verbose)?;
let normalized_body = promoted.normalized_body.take();
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) => {
log.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 =
LoopUpdateAnalyzer::analyze_carrier_updates(analysis_body, &inputs.carrier_info.carriers);
log.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);
log.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 {
log.log(
"updates",
format!(
"Phase 247-EX: Skipping host binding for loop-local carrier '{}' (init=LoopLocalZero)",
carrier.name
),
);
}
}
}
let lowering_inputs = crate::mir::join_ir::lowering::loop_with_break_minimal::LoopWithBreakLoweringInputs {
scope: inputs.scope,
let effective_break_condition = promoted.effective_break_condition;
let carrier_updates = promoted.carrier_updates;
let emitted = EmitJoinIRStepBox::emit(
builder,
condition,
break_condition: &effective_break_condition,
env: &inputs.env,
carrier_info: &inputs.carrier_info,
carrier_updates: &carrier_updates,
body_ast: analysis_body,
body_local_env: Some(&mut inputs.body_local_env),
allowed_body_locals_for_conditions: if inputs.allowed_body_locals_for_conditions.is_empty() {
None
} else {
Some(inputs.allowed_body_locals_for_conditions.as_slice())
},
join_value_space: &mut inputs.join_value_space,
analysis_body,
&effective_break_condition,
&carrier_updates,
&mut promoted.inputs,
debug,
verbose,
skeleton,
condition_only_recipe: inputs.condition_only_recipe.as_ref(),
body_local_derived_recipe: inputs.body_local_derived_recipe.as_ref(),
};
)?;
let (join_module, fragment_meta) = match lower_loop_with_break_minimal(lowering_inputs) {
Ok((module, meta)) => (module, meta),
Err(e) => {
super::super::trace::trace().debug("pattern2", &format!("Pattern 2 lowerer failed: {}", e));
return Err(format!("[cf_loop/pattern2] Lowering failed: {}", e));
}
};
let exit_meta = &fragment_meta.exit_meta;
use crate::mir::builder::control_flow::joinir::merge::exit_line::ExitMetaCollector;
let exit_bindings = ExitMetaCollector::collect(builder, exit_meta, Some(&inputs.carrier_info), debug);
// JoinIR main() params: [ValueId(0), ValueId(1), ...]
let mut join_input_slots = vec![ValueId(0)];
let mut host_input_values = vec![inputs.loop_var_id];
for (idx, carrier) in inputs.carrier_info.carriers.iter().enumerate() {
join_input_slots.push(ValueId((idx + 1) as u32));
host_input_values.push(carrier.host_id);
}
use crate::mir::join_ir::lowering::JoinInlineBoundaryBuilder;
let boundary = JoinInlineBoundaryBuilder::new()
.with_inputs(join_input_slots, host_input_values)
.with_condition_bindings(inputs.condition_bindings)
.with_exit_bindings(exit_bindings.clone())
.with_expr_result(fragment_meta.expr_result)
.with_loop_var_name(Some(inputs.loop_var_name.clone()))
.with_carrier_info(inputs.carrier_info.clone())
.build();
use super::conversion_pipeline::JoinIRConversionPipeline;
let _ = JoinIRConversionPipeline::execute(builder, join_module, Some(&boundary), "pattern2", debug)?;
let void_val = crate::mir::builder::emission::constant::emit_void(builder);
super::super::trace::trace().debug("pattern2", &format!("Loop complete, returning Void {:?}", void_val));
Ok(Some(void_val))
MergeStepBox::merge(builder, emitted.join_module, emitted.boundary, debug)
}
}
/// Updated carriers filtering helper (shared by orchestrator + tests).
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
});
}
pub(in crate::mir::builder) fn promote_and_prepare_carriers(
builder: &mut MirBuilder,
condition: &ASTNode,
body: &[ASTNode],
inputs: &mut Pattern2Inputs,
debug: bool,
verbose: bool,
) -> Result<(), String> {
use crate::mir::join_ir::lowering::digitpos_condition_normalizer::DigitPosConditionNormalizer;
use crate::mir::loop_pattern_detection::loop_condition_scope::LoopConditionScopeBox;
let cond_scope = LoopConditionScopeBox::analyze(
&inputs.loop_var_name,
&vec![condition, &inputs.break_condition_node],
Some(&inputs.scope),
);
let log = Pattern2DebugLog::new(verbose);
let mut promoted_pairs: Vec<(String, String)> = Vec::new();
let cond_body_local_vars: Vec<String> = cond_scope
.vars
.iter()
.filter(|v| matches!(v.scope, crate::mir::loop_pattern_detection::loop_condition_scope::CondVarScope::LoopBodyLocal))
.map(|v| v.name.clone())
.collect();
if cond_scope.has_loop_body_local() {
if !inputs.is_loop_true_read_digits {
match classify_for_pattern2(
builder,
&inputs.loop_var_name,
&inputs.scope,
&inputs.break_condition_node,
&cond_scope,
body,
) {
PolicyDecision::Use(BodyLocalRoute::Promotion {
promoted_carrier,
promoted_var,
carrier_name,
}) => {
let is_trim_promotion = promoted_carrier.trim_helper().is_some();
if !is_trim_promotion {
promoted_pairs.push((promoted_var.clone(), carrier_name.clone()));
}
#[cfg(feature = "normalized_dev")]
{
use crate::mir::join_ir::lowering::carrier_binding_assigner::CarrierBindingAssigner;
let mut promoted_carrier = promoted_carrier;
CarrierBindingAssigner::assign_promoted_binding(
builder,
&mut promoted_carrier,
&promoted_var,
&carrier_name,
)
.map_err(|e| format!("[phase78/binding_assign] {:?}", e))?;
inputs.carrier_info.merge_from(&promoted_carrier);
}
#[cfg(not(feature = "normalized_dev"))]
{
inputs.carrier_info.merge_from(&promoted_carrier);
}
inputs
.carrier_info
.promoted_loopbodylocals
.push(promoted_var.clone());
if !is_trim_promotion {
inputs.break_condition_node = DigitPosConditionNormalizer::normalize(
&inputs.break_condition_node,
&promoted_var,
&carrier_name,
);
}
}
PolicyDecision::Use(BodyLocalRoute::ReadOnlySlot(slot)) => {
inputs.allowed_body_locals_for_conditions = vec![slot.name.clone()];
inputs.read_only_body_local_slot = Some(slot);
}
PolicyDecision::Reject(reason) => {
return Err(error_messages::format_error_pattern2_promotion_failed(
&cond_body_local_vars,
&reason,
));
}
PolicyDecision::None => {}
}
}
}
// Allocate join_ids for carriers and register bindings.
for carrier in &mut inputs.carrier_info.carriers {
let carrier_join_id = inputs.join_value_space.alloc_param();
carrier.join_id = Some(carrier_join_id);
#[cfg(feature = "normalized_dev")]
if let Some(binding_id) = carrier.binding_id {
use crate::mir::join_ir::lowering::carrier_info::CarrierRole;
match carrier.role {
CarrierRole::ConditionOnly => inputs.env.register_condition_binding(binding_id, carrier_join_id),
CarrierRole::LoopState => inputs.env.register_carrier_binding(binding_id, carrier_join_id),
}
}
}
for (promoted_var, promoted_carrier_name) in promoted_pairs {
let join_id = inputs
.carrier_info
.find_carrier(&promoted_carrier_name)
.and_then(|c| c.join_id)
.ok_or_else(|| format!("[phase229] promoted carrier '{}' has no join_id", promoted_carrier_name))?;
inputs.env.insert(promoted_var, join_id);
}
// ExprLowerer validation (best-effort; unchanged behavior)
{
use crate::mir::join_ir::lowering::expr_lowerer::{ExprContext, ExprLowerer, ExprLoweringError};
use crate::mir::join_ir::lowering::scope_manager::Pattern2ScopeManager;
let scope_manager = Pattern2ScopeManager {
condition_env: &inputs.env,
loop_body_local_env: Some(&inputs.body_local_env),
captured_env: Some(&inputs.captured_env),
carrier_info: &inputs.carrier_info,
};
match ExprLowerer::new(&scope_manager, ExprContext::Condition, builder)
.with_debug(debug)
.lower(&inputs.break_condition_node)
{
Ok(_) => {}
Err(ExprLoweringError::UnsupportedNode(_)) => {}
Err(_) => {}
}
}
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::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 },
))
}

View File

@ -0,0 +1,46 @@
//! ApplyPolicyStepBox (Phase 106)
//!
//! Responsibility: apply policy routing for Pattern2 break condition + allow-list.
use crate::ast::ASTNode;
use super::super::pattern2_break_condition_policy_router::Pattern2BreakConditionPolicyRouterBox;
use super::super::pattern2_inputs_facts_box::{Pattern2Facts, Pattern2Inputs};
pub(crate) struct ApplyPolicyStepBox;
impl ApplyPolicyStepBox {
pub(crate) fn apply(condition: &ASTNode, body: &[ASTNode], facts: Pattern2Facts) -> Result<Pattern2Inputs, String> {
use crate::mir::join_ir::lowering::common::body_local_slot::ReadOnlyBodyLocalSlotBox;
let break_routing = Pattern2BreakConditionPolicyRouterBox::route(condition, body)?;
let read_only_body_local_slot = if break_routing.allowed_body_locals_for_conditions.is_empty() {
None
} else {
Some(ReadOnlyBodyLocalSlotBox::extract_single(
&break_routing.allowed_body_locals_for_conditions,
body,
)?)
};
Ok(Pattern2Inputs {
loop_var_name: facts.loop_var_name,
loop_var_id: facts.loop_var_id,
carrier_info: facts.carrier_info,
scope: facts.scope,
captured_env: facts.captured_env,
join_value_space: facts.join_value_space,
env: facts.env,
condition_bindings: facts.condition_bindings,
body_local_env: facts.body_local_env,
allowed_body_locals_for_conditions: break_routing.allowed_body_locals_for_conditions,
read_only_body_local_slot,
break_condition_node: break_routing.break_condition_node,
is_loop_true_read_digits: break_routing.is_loop_true_read_digits,
condition_only_recipe: None,
body_local_derived_recipe: None,
})
}
}

View File

@ -0,0 +1,91 @@
//! EmitJoinIRStepBox (Phase 106)
//!
//! Responsibility: call Pattern2 JoinIR lowerer and build inline boundary.
use crate::ast::ASTNode;
use crate::mir::builder::MirBuilder;
use crate::mir::ValueId;
use super::super::pattern2_inputs_facts_box::{Pattern2DebugLog, Pattern2Inputs};
use crate::mir::join_ir::lowering::inline_boundary::JoinInlineBoundary;
use std::collections::BTreeMap;
pub(crate) struct EmitJoinIRStepOutput {
pub join_module: crate::mir::join_ir::JoinModule,
pub boundary: JoinInlineBoundary,
}
pub(crate) struct EmitJoinIRStepBox;
impl EmitJoinIRStepBox {
pub(crate) fn emit(
builder: &mut MirBuilder,
condition: &ASTNode,
body_ast: &[ASTNode],
effective_break_condition: &ASTNode,
carrier_updates: &BTreeMap<String, crate::mir::join_ir::lowering::loop_update_analyzer::UpdateExpr>,
inputs: &mut Pattern2Inputs,
debug: bool,
verbose: bool,
skeleton: Option<&crate::mir::loop_canonicalizer::LoopSkeleton>,
) -> Result<EmitJoinIRStepOutput, String> {
use crate::mir::join_ir::lowering::loop_with_break_minimal::lower_loop_with_break_minimal;
let log = Pattern2DebugLog::new(verbose);
let lowering_inputs = crate::mir::join_ir::lowering::loop_with_break_minimal::LoopWithBreakLoweringInputs {
scope: inputs.scope.clone(),
condition,
break_condition: effective_break_condition,
env: &inputs.env,
carrier_info: &inputs.carrier_info,
carrier_updates,
body_ast,
body_local_env: Some(&mut inputs.body_local_env),
allowed_body_locals_for_conditions: if inputs.allowed_body_locals_for_conditions.is_empty() {
None
} else {
Some(inputs.allowed_body_locals_for_conditions.as_slice())
},
join_value_space: &mut inputs.join_value_space,
skeleton,
condition_only_recipe: inputs.condition_only_recipe.as_ref(),
body_local_derived_recipe: inputs.body_local_derived_recipe.as_ref(),
};
let (join_module, fragment_meta) = match lower_loop_with_break_minimal(lowering_inputs) {
Ok((module, meta)) => (module, meta),
Err(e) => {
super::super::super::trace::trace().debug("pattern2", &format!("Pattern 2 lowerer failed: {}", e));
return Err(format!("[cf_loop/pattern2] Lowering failed: {}", e));
}
};
let exit_meta = &fragment_meta.exit_meta;
use crate::mir::builder::control_flow::joinir::merge::exit_line::ExitMetaCollector;
let exit_bindings = ExitMetaCollector::collect(builder, exit_meta, Some(&inputs.carrier_info), debug);
// JoinIR main() params: [ValueId(0), ValueId(1), ...]
let mut join_input_slots = vec![ValueId(0)];
let mut host_input_values = vec![inputs.loop_var_id];
for (idx, carrier) in inputs.carrier_info.carriers.iter().enumerate() {
join_input_slots.push(ValueId((idx + 1) as u32));
host_input_values.push(carrier.host_id);
}
use crate::mir::join_ir::lowering::JoinInlineBoundaryBuilder;
let boundary = JoinInlineBoundaryBuilder::new()
.with_inputs(join_input_slots, host_input_values)
.with_condition_bindings(std::mem::take(&mut inputs.condition_bindings))
.with_exit_bindings(exit_bindings.clone())
.with_expr_result(fragment_meta.expr_result)
.with_loop_var_name(Some(inputs.loop_var_name.clone()))
.with_carrier_info(inputs.carrier_info.clone())
.build();
log.log("emit", "JoinIR module + boundary constructed");
Ok(EmitJoinIRStepOutput { join_module, boundary })
}
}

View File

@ -0,0 +1,24 @@
//! GatherFactsStepBox (Phase 106)
//!
//! Responsibility: gather Pattern2 analysis-only inputs.
use crate::ast::ASTNode;
use crate::mir::builder::MirBuilder;
use super::super::pattern2_inputs_facts_box::{Pattern2Facts, Pattern2InputsFactsBox};
pub(crate) struct GatherFactsStepBox;
impl GatherFactsStepBox {
pub(crate) fn gather(
builder: &MirBuilder,
condition: &ASTNode,
body: &[ASTNode],
fn_body: Option<&[ASTNode]>,
ctx: &super::super::pattern_pipeline::PatternPipelineContext,
verbose: bool,
) -> Result<Pattern2Facts, String> {
Pattern2InputsFactsBox::analyze(builder, condition, body, fn_body, ctx, verbose)
}
}

View File

@ -0,0 +1,27 @@
//! MergeStepBox (Phase 106)
//!
//! Responsibility: run JoinIR conversion pipeline (JoinIR → MIR merge) and return void.
use crate::mir::builder::MirBuilder;
use crate::mir::ValueId;
use crate::mir::join_ir::lowering::inline_boundary::JoinInlineBoundary;
pub(crate) struct MergeStepBox;
impl MergeStepBox {
pub(crate) fn merge(
builder: &mut MirBuilder,
join_module: crate::mir::join_ir::JoinModule,
boundary: JoinInlineBoundary,
debug: bool,
) -> Result<Option<ValueId>, String> {
use crate::mir::builder::emission::constant::emit_void;
use super::super::conversion_pipeline::JoinIRConversionPipeline;
let _ = JoinIRConversionPipeline::execute(builder, join_module, Some(&boundary), "pattern2", debug)?;
let void_val = emit_void(builder);
super::super::super::trace::trace().debug("pattern2", &format!("Loop complete, returning Void {:?}", void_val));
Ok(Some(void_val))
}
}

View File

@ -0,0 +1,14 @@
//! Phase 106: Pattern2 Step Boxes (SSOT)
//!
//! Goal: keep `pattern2_with_break.rs` and the orchestrator thin by splitting
//! the Pattern2 pipeline into explicit steps with clear boundaries.
//!
//! Each step should have a single responsibility and fail-fast with a clear tag
//! at the step boundary.
pub(crate) mod apply_policy_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 promote_step_box;

View File

@ -0,0 +1,342 @@
//! PromoteStepBox (Phase 106)
//!
//! 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
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::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;
impl PromoteStepBox {
pub(crate) fn run(
builder: &mut MirBuilder,
condition: &ASTNode,
body: &[ASTNode],
mut inputs: Pattern2Inputs,
debug: bool,
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 =
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,
})
}
pub(in crate::mir::builder) fn promote_and_prepare_carriers(
builder: &mut MirBuilder,
condition: &ASTNode,
body: &[ASTNode],
inputs: &mut Pattern2Inputs,
debug: bool,
verbose: bool,
) -> Result<(), String> {
use crate::mir::join_ir::lowering::digitpos_condition_normalizer::DigitPosConditionNormalizer;
use crate::mir::loop_pattern_detection::loop_condition_scope::LoopConditionScopeBox;
let cond_scope = LoopConditionScopeBox::analyze(
&inputs.loop_var_name,
&vec![condition, &inputs.break_condition_node],
Some(&inputs.scope),
);
let mut promoted_pairs: Vec<(String, String)> = Vec::new();
let cond_body_local_vars: Vec<String> = cond_scope
.vars
.iter()
.filter(|v| matches!(v.scope, crate::mir::loop_pattern_detection::loop_condition_scope::CondVarScope::LoopBodyLocal))
.map(|v| v.name.clone())
.collect();
if cond_scope.has_loop_body_local() {
if !inputs.is_loop_true_read_digits {
match classify_for_pattern2(
builder,
&inputs.loop_var_name,
&inputs.scope,
&inputs.break_condition_node,
&cond_scope,
body,
) {
PolicyDecision::Use(BodyLocalRoute::Promotion {
promoted_carrier,
promoted_var,
carrier_name,
}) => {
let is_trim_promotion = promoted_carrier.trim_helper().is_some();
if !is_trim_promotion {
promoted_pairs.push((promoted_var.clone(), carrier_name.clone()));
}
#[cfg(feature = "normalized_dev")]
{
use crate::mir::join_ir::lowering::carrier_binding_assigner::CarrierBindingAssigner;
let mut promoted_carrier = promoted_carrier;
CarrierBindingAssigner::assign_promoted_binding(
builder,
&mut promoted_carrier,
&promoted_var,
&carrier_name,
)
.map_err(|e| format!("[phase78/binding_assign] {:?}", e))?;
inputs.carrier_info.merge_from(&promoted_carrier);
}
#[cfg(not(feature = "normalized_dev"))]
{
inputs.carrier_info.merge_from(&promoted_carrier);
}
inputs
.carrier_info
.promoted_loopbodylocals
.push(promoted_var.clone());
if !is_trim_promotion {
inputs.break_condition_node = DigitPosConditionNormalizer::normalize(
&inputs.break_condition_node,
&promoted_var,
&carrier_name,
);
}
}
PolicyDecision::Use(BodyLocalRoute::ReadOnlySlot(slot)) => {
inputs.allowed_body_locals_for_conditions = vec![slot.name.clone()];
inputs.read_only_body_local_slot = Some(slot);
}
PolicyDecision::Reject(reason) => {
return Err(error_messages::format_error_pattern2_promotion_failed(
&cond_body_local_vars,
&reason,
));
}
PolicyDecision::None => {}
}
}
}
// Allocate join_ids for carriers and register bindings.
for carrier in &mut inputs.carrier_info.carriers {
let carrier_join_id = inputs.join_value_space.alloc_param();
carrier.join_id = Some(carrier_join_id);
#[cfg(feature = "normalized_dev")]
if let Some(binding_id) = carrier.binding_id {
use crate::mir::join_ir::lowering::carrier_info::CarrierRole;
match carrier.role {
CarrierRole::ConditionOnly => inputs.env.register_condition_binding(binding_id, carrier_join_id),
CarrierRole::LoopState => inputs.env.register_carrier_binding(binding_id, carrier_join_id),
}
}
}
for (promoted_var, promoted_carrier_name) in promoted_pairs {
let join_id = inputs
.carrier_info
.find_carrier(&promoted_carrier_name)
.and_then(|c| c.join_id)
.ok_or_else(|| format!("[phase229] promoted carrier '{}' has no join_id", promoted_carrier_name))?;
inputs.env.insert(promoted_var, join_id);
}
// ExprLowerer validation (best-effort; unchanged behavior)
{
use crate::mir::join_ir::lowering::expr_lowerer::{ExprContext, ExprLowerer, ExprLoweringError};
use crate::mir::join_ir::lowering::scope_manager::Pattern2ScopeManager;
let scope_manager = Pattern2ScopeManager {
condition_env: &inputs.env,
loop_body_local_env: Some(&inputs.body_local_env),
captured_env: Some(&inputs.captured_env),
carrier_info: &inputs.carrier_info,
};
match ExprLowerer::new(&scope_manager, ExprContext::Condition, builder)
.with_debug(debug)
.lower(&inputs.break_condition_node)
{
Ok(_) => {}
Err(ExprLoweringError::UnsupportedNode(_)) => {}
Err(_) => {}
}
}
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

@ -233,7 +233,8 @@ mod tests {
use crate::ast::Span;
use crate::mir::ValueId;
use super::super::pattern2_inputs_facts_box::Pattern2InputsFactsBox;
use super::super::pattern2_lowering_orchestrator::promote_and_prepare_carriers;
use super::super::pattern2_steps::apply_policy_step_box::ApplyPolicyStepBox;
use super::super::pattern2_steps::promote_step_box::PromoteStepBox;
let mut builder = MirBuilder::new();
builder
@ -311,10 +312,12 @@ mod tests {
let ctx = build_pattern_context(&mut builder, &condition, &body, PatternVariant::Pattern2)
.expect("build_pattern_context");
let mut inputs =
let facts =
Pattern2InputsFactsBox::analyze(&builder, &condition, &body, None, &ctx, false)
.expect("Pattern2InputsFactsBox::analyze");
promote_and_prepare_carriers(&mut builder, &condition, &body, &mut inputs, false, false)
let mut inputs = ApplyPolicyStepBox::apply(&condition, &body, facts)
.expect("ApplyPolicyStepBox::apply");
PromoteStepBox::promote_and_prepare_carriers(&mut builder, &condition, &body, &mut inputs, false, false)
.expect("promote_and_prepare_carriers");
assert!(

View File

@ -12,6 +12,7 @@
use super::PolicyDecision;
use crate::ast::{ASTNode, BinaryOperator, Span, UnaryOperator};
#[derive(Debug, Clone)]
pub(crate) struct LoopTrueReadDigitsPolicyResult {
pub break_condition_node: ASTNode,
pub allowed_ch_var: String,
@ -59,3 +60,137 @@ pub(crate) fn classify_loop_true_read_digits(
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::{BinaryOperator, LiteralValue, Span, UnaryOperator};
fn span() -> Span {
Span::unknown()
}
fn bool_lit(v: bool) -> ASTNode {
ASTNode::Literal {
value: LiteralValue::Bool(v),
span: span(),
}
}
fn var(name: &str) -> ASTNode {
ASTNode::Variable {
name: name.to_string(),
span: span(),
}
}
fn str_lit(s: &str) -> ASTNode {
ASTNode::Literal {
value: LiteralValue::String(s.to_string()),
span: span(),
}
}
fn int_lit(n: i64) -> ASTNode {
ASTNode::Literal {
value: LiteralValue::Integer(n),
span: span(),
}
}
fn eq(left: ASTNode, right: ASTNode) -> ASTNode {
ASTNode::BinaryOp {
operator: BinaryOperator::Equal,
left: Box::new(left),
right: Box::new(right),
span: span(),
}
}
fn add(left: ASTNode, right: ASTNode) -> ASTNode {
ASTNode::BinaryOp {
operator: BinaryOperator::Add,
left: Box::new(left),
right: Box::new(right),
span: span(),
}
}
fn or(left: ASTNode, right: ASTNode) -> ASTNode {
ASTNode::BinaryOp {
operator: BinaryOperator::Or,
left: Box::new(left),
right: Box::new(right),
span: span(),
}
}
fn not(node: ASTNode) -> ASTNode {
ASTNode::UnaryOp {
operator: UnaryOperator::Not,
operand: Box::new(node),
span: span(),
}
}
fn digit_chain(var_name: &str, digits: &[&str]) -> ASTNode {
let mut it = digits.iter();
let first = it
.next()
.expect("digits must be non-empty");
let mut acc = eq(var(var_name), str_lit(first));
for d in it {
acc = or(acc, eq(var(var_name), str_lit(d)));
}
acc
}
#[test]
fn read_digits_loop_true_policy_returns_break_when_true_and_allowlist() {
// loop(true) {
// if ch == "" { break }
// if is_digit(ch) { out = out + ch; i = i + 1 } else { break }
// }
let condition = bool_lit(true);
let eos_cond = eq(var("ch"), str_lit(""));
let digit_cond = digit_chain(
"ch",
&["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"],
);
let body = vec![
ASTNode::If {
condition: Box::new(eos_cond.clone()),
then_body: vec![ASTNode::Break { span: span() }],
else_body: None,
span: span(),
},
ASTNode::If {
condition: Box::new(digit_cond.clone()),
then_body: vec![
ASTNode::Assignment {
target: Box::new(var("out")),
value: Box::new(add(var("out"), var("ch"))),
span: span(),
},
ASTNode::Assignment {
target: Box::new(var("i")),
value: Box::new(add(var("i"), int_lit(1))),
span: span(),
},
],
else_body: Some(vec![ASTNode::Break { span: span() }]),
span: span(),
},
];
let decision = classify_loop_true_read_digits(&condition, &body);
let result = match decision {
PolicyDecision::Use(v) => v,
other => panic!("expected PolicyDecision::Use, got {:?}", other),
};
assert_eq!(result.allowed_ch_var, "ch");
assert_eq!(result.break_condition_node, or(eos_cond, not(digit_cond)));
}
}