diff --git a/src/mir/builder/control_flow/joinir/patterns/mod.rs b/src/mir/builder/control_flow/joinir/patterns/mod.rs index 38aa9f2d..79d026fd 100644 --- a/src/mir/builder/control_flow/joinir/patterns/mod.rs +++ b/src/mir/builder/control_flow/joinir/patterns/mod.rs @@ -57,6 +57,8 @@ pub(in crate::mir::builder) mod common_init; pub(in crate::mir::builder) mod loop_true_counter_extractor; // Phase 104: loop(true) counter extraction for Pattern2 pub(in crate::mir::builder) mod read_digits_break_condition_box; // Phase 104: break cond normalization for read_digits(loop(true)) 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 condition_env_builder; pub(in crate::mir::builder) mod conversion_pipeline; pub(in crate::mir::builder) mod exit_binding; diff --git a/src/mir/builder/control_flow/joinir/patterns/pattern2_inputs_facts_box.rs b/src/mir/builder/control_flow/joinir/patterns/pattern2_inputs_facts_box.rs new file mode 100644 index 00000000..df8d58d1 --- /dev/null +++ b/src/mir/builder/control_flow/joinir/patterns/pattern2_inputs_facts_box.rs @@ -0,0 +1,397 @@ +//! Pattern2 input analysis (facts collection only) +//! +//! This box collects everything Pattern2 lowering needs *without* emitting JoinIR: +//! - 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; +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, 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; +use crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape; +use crate::mir::ValueId; + +use crate::mir::loop_pattern_detection::function_scope_capture::CapturedEnv; + +pub(crate) struct Pattern2DebugLog { + verbose: bool, + debug: DebugOutputBox, +} + +impl Pattern2DebugLog { + pub(crate) fn new(verbose: bool) -> Self { + Self { + verbose, + debug: DebugOutputBox::new_with_enabled("joinir/pattern2", verbose), + } + } + + pub(crate) fn log(&self, tag: &str, message: impl AsRef) { + if self.verbose { + self.debug.log(tag, message.as_ref()); + } + } +} + +pub(in crate::mir::builder) struct Pattern2Inputs { + 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, + pub body_local_env: LoopBodyLocalEnv, + /// Phase 92 P3: Allow-list of LoopBodyLocal variable names permitted in conditions. + /// This must stay minimal (1 variable) and is validated by ReadOnlyBodyLocalSlotBox. + pub allowed_body_locals_for_conditions: Vec, + /// Phase 92 P3: Diagnostics / debug metadata for the allow-listed variable. + pub read_only_body_local_slot: Option, + pub break_condition_node: ASTNode, + /// loop(true) + break-only digits(read_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< + crate::mir::join_ir::lowering::common::condition_only_emitter::ConditionOnlyRecipe, + >, + /// Phase 94: BodyLocalDerived recipe for P5b "ch" reassignment + escape counter. + pub body_local_derived_recipe: + Option, +} + +pub(crate) struct Pattern2InputsFactsBox; + +impl Pattern2InputsFactsBox { + pub(in crate::mir::builder) fn analyze( + builder: &MirBuilder, + condition: &ASTNode, + body: &[ASTNode], + fn_body: Option<&[ASTNode]>, + ctx: &super::pattern_pipeline::PatternPipelineContext, + verbose: bool, + ) -> Result { + 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}; + + let loop_var_name = ctx.loop_var_name.clone(); + let loop_var_id = ctx.loop_var_id; + let mut carrier_info = + ctx.carrier_info.clone(); // Phase 100 P2-2: Make mutable for accumulator promotion + let scope = ctx.loop_scope.clone(); + + log.log( + "init", + format!( + "PatternPipelineContext: loop_var='{}', loop_var_id={:?}, carriers={}", + loop_var_name, + loop_var_id, + carrier_info.carriers.len() + ), + ); + + // Capture analysis + log.log( + "phase200c", + format!( + "fn_body is {}", + if fn_body.is_some() { "SOME" } else { "NONE" } + ), + ); + let captured_env = if let Some(fn_body_ref) = fn_body { + log.log("phase200c", format!("fn_body has {} nodes", fn_body_ref.len())); + analyze_captured_vars_v2(fn_body_ref, condition, body, &scope) + } else { + log.log("phase200c", "fn_body is None, using empty CapturedEnv"); + CapturedEnv::new() + }; + if verbose { + log.log( + "capture", + format!("Phase 200-C: Captured {} variables", captured_env.vars.len()), + ); + for var in &captured_env.vars { + log.log( + "capture", + format!( + " '{}': host_id={:?}, immutable={}", + var.name, var.host_id, var.is_immutable + ), + ); + } + } + + // Phase 100 P1-3: Pinned Local Analysis (Judgment Box) + // Analyze loop body AST to identify pinned locals (read-only loop-outer locals) + let mut captured_env = captured_env; // Make mutable for pinned insertions + + // Collect candidate locals from variable_map (all variables defined before loop) + let candidate_locals: std::collections::BTreeSet = + builder.variable_ctx.variable_map.keys().cloned().collect(); + + if !candidate_locals.is_empty() { + use crate::mir::loop_pattern_detection::pinned_local_analyzer::analyze_pinned_locals; + + match analyze_pinned_locals(body, &candidate_locals) { + Ok(pinned_names) => { + if verbose && !pinned_names.is_empty() { + log.log( + "phase100_p1", + format!("Detected {} pinned locals", pinned_names.len()), + ); + } + + for pinned_name in pinned_names { + if let Some(&host_id) = builder.variable_ctx.variable_map.get(&pinned_name) { + if verbose { + log.log( + "phase100_p1", + format!( + "Wiring pinned local '{}' with host_id={:?}", + pinned_name, host_id + ), + ); + } + captured_env.insert_pinned(pinned_name, host_id); + } else { + return Err(format!( + "Pinned local '{}' not found in variable_map (internal error)", + pinned_name + )); + } + } + } + Err(e) => return Err(format!("Pinned local analysis failed: {}", e)), + } + } + + // Phase 100 P2-2: Mutable Accumulator Analysis + use crate::mir::loop_pattern_detection::mutable_accumulator_analyzer::{ + AccumulatorKind, MutableAccumulatorAnalyzer, RhsExprKind, + }; + + let mutable_spec = MutableAccumulatorAnalyzer::analyze(body)?; + + if let Some(spec) = mutable_spec { + if verbose { + log.log( + "phase100_p2", + format!( + "Detected mutable accumulator: '{}' = '{}' + '{}'", + spec.target_name, spec.target_name, spec.rhs_var_or_lit + ), + ); + } + + let target_id = builder + .variable_ctx + .variable_map + .get(&spec.target_name) + .ok_or_else(|| { + format!( + "[joinir/mutable-acc] Target '{}' not found in variable_map", + spec.target_name + ) + })?; + + let mut refined_kind = spec.kind; + if spec.rhs_expr_kind == RhsExprKind::Var && refined_kind == AccumulatorKind::Int { + use crate::mir::MirType; + if let Some(target_type) = builder.type_ctx.value_types.get(target_id) { + match target_type { + MirType::Box(box_name) if box_name == "StringBox" => { + refined_kind = AccumulatorKind::String; + if verbose { + log.log( + "phase100_p3", + format!( + "Refined accumulator kind: Int → String (target '{}' is StringBox)", + spec.target_name + ), + ); + } + } + MirType::Integer => { + if verbose { + log.log( + "phase100_p3", + format!( + "Confirmed accumulator kind: Int (target '{}' is Integer)", + spec.target_name + ), + ); + } + } + _ => { + if verbose { + log.log( + "phase100_p3", + format!( + "Accumulator kind: Int (default, target '{}' type unknown: {:?})", + spec.target_name, target_type + ), + ); + } + } + } + } + } + + if refined_kind == AccumulatorKind::String { + match spec.rhs_expr_kind { + RhsExprKind::Var => { + if verbose { + log.log( + "phase100_p3", + format!( + "String accumulator '{}' = '{}' + '{}' (Variable RHS: OK)", + spec.target_name, spec.target_name, spec.rhs_var_or_lit + ), + ); + } + } + RhsExprKind::Literal => { + return Err(format!( + "[joinir/mutable-acc] String accumulator '{}' with Literal RHS not supported in Phase 100 P3 (will be P3.1)", + spec.target_name + )); + } + } + } + + match spec.rhs_expr_kind { + RhsExprKind::Literal => {} + RhsExprKind::Var => { + let rhs_name = &spec.rhs_var_or_lit; + let in_captured = captured_env.vars.iter().any(|v| &v.name == rhs_name); + let in_carrier = carrier_info.carriers.iter().any(|c| &c.name == rhs_name); + + if in_carrier { + return Err(format!( + "[joinir/mutable-acc] RHS '{}' must be read-only (Condition/BodyLocal/Captured/Pinned), but found mutable Carrier", + rhs_name + )); + } else if !in_captured && !builder.variable_ctx.variable_map.contains_key(rhs_name) { + if verbose { + log.log( + "phase100_p2", + format!( + "RHS '{}' not in captured/variable_map, assuming LoopBodyLocal (will validate later)", + rhs_name + ), + ); + } + } + } + } + + if verbose { + log.log( + "phase100_p2", + format!("Promoting '{}' to mutable LoopState carrier", spec.target_name), + ); + } + + use crate::mir::join_ir::lowering::carrier_info::CarrierVar; + carrier_info + .carriers + .push(CarrierVar::new(spec.target_name.clone(), *target_id)); + } else if verbose { + log.log("phase100_p2", "No mutable accumulator pattern detected"); + } + + // Value space + condition env + let mut join_value_space = JoinValueSpace::new(); + let (mut env, mut condition_bindings, _loop_var_join_id) = + ConditionEnvBuilder::build_for_break_condition_v2( + condition, + &loop_var_name, + &builder.variable_ctx.variable_map, + loop_var_id, + &mut join_value_space, + )?; + + // Phase 79-2: Register loop variable BindingId (dev-only) + #[cfg(feature = "normalized_dev")] + if let Some(loop_var_bid) = builder.binding_ctx.lookup(&loop_var_name) { + env.register_loop_var_binding(loop_var_bid, _loop_var_join_id); + log.log( + "phase79", + format!( + "Registered loop var BindingId: '{}' BindingId({}) → ValueId({})", + loop_var_name, loop_var_bid.0, _loop_var_join_id.0 + ), + ); + } + + // Add captured vars + for var in &captured_env.vars { + if let Some(&host_id) = builder.variable_ctx.variable_map.get(&var.name) { + let join_id = join_value_space.alloc_param(); + env.insert(var.name.clone(), join_id); + condition_bindings.push(ConditionBinding { + name: var.name.clone(), + host_value: host_id, + join_value: join_id, + }); + log.log( + "capture", + format!( + "Phase 201: Added captured '{}': host={:?}, join={:?}", + var.name, host_id, join_id + ), + ); + } + } + + let body_local_env = LoopBodyLocalEnv::new(); + + // Break condition extraction is policy-routed (SSOT). + let break_routing = Pattern2BreakConditionPolicyRouterBox::route(condition, body)?; + + let mut inputs = Pattern2Inputs { + loop_var_name, + loop_var_id, + carrier_info, + scope, + captured_env, + join_value_space, + 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) + } +} + diff --git a/src/mir/builder/control_flow/joinir/patterns/pattern2_lowering_orchestrator.rs b/src/mir/builder/control_flow/joinir/patterns/pattern2_lowering_orchestrator.rs new file mode 100644 index 00000000..8ef583df --- /dev/null +++ b/src/mir/builder/control_flow/joinir/patterns/pattern2_lowering_orchestrator.rs @@ -0,0 +1,403 @@ +//! 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. + +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; + +pub(crate) struct Pattern2LoweringOrchestrator; + +impl Pattern2LoweringOrchestrator { + pub(in crate::mir::builder) fn run( + builder: &mut MirBuilder, + condition: &ASTNode, + body: &[ASTNode], + func_name: &str, + debug: bool, + fn_body: Option<&[ASTNode]>, + skeleton: Option<&crate::mir::loop_canonicalizer::LoopSkeleton>, + ) -> Result, 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"); + + use super::pattern_pipeline::{build_pattern_context, PatternVariant}; + let ctx = build_pattern_context(builder, condition, body, PatternVariant::Pattern2)?; + + super::super::trace::trace().varmap("pattern2_start", &builder.variable_ctx.variable_map); + + let mut inputs = Pattern2InputsFactsBox::analyze(builder, condition, body, fn_body, &ctx, verbose)?; + + 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 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, + 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, + 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)) + } +} + +/// Updated carriers filtering helper (shared by orchestrator + tests). +fn filter_carriers_for_updates(carrier_info: &mut CarrierInfo, carrier_updates: &BTreeMap) { + 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 = 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>), 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 }, + )) +} + diff --git a/src/mir/builder/control_flow/joinir/patterns/pattern2_with_break.rs b/src/mir/builder/control_flow/joinir/patterns/pattern2_with_break.rs index 176e3419..2381b34b 100644 --- a/src/mir/builder/control_flow/joinir/patterns/pattern2_with_break.rs +++ b/src/mir/builder/control_flow/joinir/patterns/pattern2_with_break.rs @@ -3,939 +3,7 @@ use super::super::trace; 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, ConditionEnv}; -use super::body_local_policy::{classify_for_pattern2, BodyLocalRoute}; -use crate::mir::join_ir::lowering::common::body_local_slot::ReadOnlyBodyLocalSlot; -use crate::mir::join_ir::lowering::common::body_local_derived_emitter::BodyLocalDerivedRecipe; -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; -use crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape; -use crate::mir::join_ir::lowering::loop_with_break_minimal::LoopWithBreakLoweringInputs; -use crate::mir::join_ir::lowering::loop_update_analyzer::UpdateExpr; -use crate::mir::loop_pattern_detection::error_messages; -use crate::mir::loop_pattern_detection::function_scope_capture::CapturedEnv; use crate::mir::ValueId; -use super::policies::p5b_escape_derived_policy::{ - classify_p5b_escape_derived, P5bEscapeDerivedDecision, -}; -use super::policies::PolicyDecision; -use super::pattern2_break_condition_policy_router::Pattern2BreakConditionPolicyRouterBox; -use std::collections::BTreeMap; - -struct Pattern2DebugLog { - verbose: bool, - debug: DebugOutputBox, -} - -impl Pattern2DebugLog { - fn new(verbose: bool) -> Self { - Self { - verbose, - debug: DebugOutputBox::new_with_enabled("joinir/pattern2", verbose), - } - } - - fn log(&self, tag: &str, message: impl AsRef) { - if self.verbose { - self.debug.log(tag, message.as_ref()); - } - } -} - -struct Pattern2Inputs { - loop_var_name: String, - loop_var_id: ValueId, - carrier_info: CarrierInfo, - scope: LoopScopeShape, - captured_env: CapturedEnv, - join_value_space: JoinValueSpace, - env: ConditionEnv, - condition_bindings: Vec, - body_local_env: LoopBodyLocalEnv, - /// Phase 92 P3: Allow-list of LoopBodyLocal variable names permitted in conditions. - /// This must stay minimal (1 variable) and is validated by ReadOnlyBodyLocalSlotBox. - allowed_body_locals_for_conditions: Vec, - /// Phase 92 P3: Diagnostics / debug metadata for the allow-listed variable. - read_only_body_local_slot: Option, - break_condition_node: ASTNode, - /// loop(true) + break-only digits(read_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. - is_loop_true_read_digits: bool, - /// Phase 93 P0: ConditionOnly recipe for derived slot recalculation - condition_only_recipe: Option, - /// Phase 94: BodyLocalDerived recipe for P5b "ch" reassignment + escape counter. - body_local_derived_recipe: Option, -} - -fn prepare_pattern2_inputs( - builder: &MirBuilder, - condition: &ASTNode, - body: &[ASTNode], - fn_body: Option<&[ASTNode]>, - ctx: &super::pattern_pipeline::PatternPipelineContext, - verbose: bool, -) -> Result { - 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, - }; - - let loop_var_name = ctx.loop_var_name.clone(); - let loop_var_id = ctx.loop_var_id; - let mut carrier_info = ctx.carrier_info.clone(); // Phase 100 P2-2: Make mutable for accumulator promotion - let scope = ctx.loop_scope.clone(); - - log.log( - "init", - format!( - "PatternPipelineContext: loop_var='{}', loop_var_id={:?}, carriers={}", - loop_var_name, - loop_var_id, - carrier_info.carriers.len() - ), - ); - - // Capture analysis - log.log( - "phase200c", - format!( - "fn_body is {}", - if fn_body.is_some() { "SOME" } else { "NONE" } - ), - ); - let captured_env = if let Some(fn_body_ref) = fn_body { - log.log( - "phase200c", - format!("fn_body has {} nodes", fn_body_ref.len()), - ); - analyze_captured_vars_v2(fn_body_ref, condition, body, &scope) - } else { - log.log( - "phase200c", - "fn_body is None, using empty CapturedEnv", - ); - CapturedEnv::new() - }; - if verbose { - log.log( - "capture", - format!( - "Phase 200-C: Captured {} variables", - captured_env.vars.len() - ), - ); - for var in &captured_env.vars { - log.log( - "capture", - format!( - " '{}': host_id={:?}, immutable={}", - var.name, var.host_id, var.is_immutable - ), - ); - } - } - - // Phase 100 P1-3: Pinned Local Analysis (Judgment Box) - // Analyze loop body AST to identify pinned locals (read-only loop-outer locals) - let mut captured_env = captured_env; // Make mutable for pinned insertions - - // Collect candidate locals from variable_map (all variables defined before loop) - let candidate_locals: std::collections::BTreeSet = - builder.variable_ctx.variable_map.keys().cloned().collect(); - - if !candidate_locals.is_empty() { - use crate::mir::loop_pattern_detection::pinned_local_analyzer::analyze_pinned_locals; - - match analyze_pinned_locals(body, &candidate_locals) { - Ok(pinned_names) => { - if verbose && !pinned_names.is_empty() { - log.log( - "phase100_p1", - format!("Detected {} pinned locals", pinned_names.len()), - ); - } - - // Wire pinned locals into CapturedEnv - for pinned_name in pinned_names { - // Look up host ValueId from variable_map - if let Some(&host_id) = builder.variable_ctx.variable_map.get(&pinned_name) { - if verbose { - log.log( - "phase100_p1", - format!( - "Wiring pinned local '{}' with host_id={:?}", - pinned_name, host_id - ), - ); - } - captured_env.insert_pinned(pinned_name, host_id); - } else { - // Fail-Fast: host_id not found - return Err(format!( - "Pinned local '{}' not found in variable_map (internal error)", - pinned_name - )); - } - } - } - Err(e) => { - // Fail-Fast: analyzer error - return Err(format!("Pinned local analysis failed: {}", e)); - } - } - } - - // Phase 100 P2-2: Mutable Accumulator Analysis - // Detect accumulator pattern (target = target + x) and promote to carrier - // Phase 100 P3-3: Added AccumulatorKind support for string accumulators - use crate::mir::loop_pattern_detection::mutable_accumulator_analyzer::{ - AccumulatorKind, MutableAccumulatorAnalyzer, RhsExprKind, - }; - - let mutable_spec = MutableAccumulatorAnalyzer::analyze(body)?; - - if let Some(spec) = mutable_spec { - if verbose { - log.log( - "phase100_p2", - format!( - "Detected mutable accumulator: '{}' = '{}' + '{}'", - spec.target_name, - spec.target_name, - spec.rhs_var_or_lit - ), - ); - } - - // Check target is loop-outer (not in LoopBodyLocalEnv which is empty at this point) - // This check will be redundant for Pattern2 since body_local_env is always empty, - // but we keep it for consistency with the spec - - // Resolve target in variable_map - let target_id = builder - .variable_ctx - .variable_map - .get(&spec.target_name) - .ok_or_else(|| { - format!( - "[joinir/mutable-acc] Target '{}' not found in variable_map", - spec.target_name - ) - })?; - - // Phase 100 P3-3: Refine AccumulatorKind based on actual target type - // AST-only detection (P3-1) defaults to Int for Variables - // Here we check actual type from builder's type context - let mut refined_kind = spec.kind; // Start with AST-detected kind - if spec.rhs_expr_kind == RhsExprKind::Var && refined_kind == AccumulatorKind::Int { - // Check if target is string-typed (refine Int → String if needed) - use crate::mir::MirType; - if let Some(target_type) = builder.type_ctx.value_types.get(target_id) { - match target_type { - MirType::Box(box_name) if box_name == "StringBox" => { - refined_kind = AccumulatorKind::String; - if verbose { - log.log( - "phase100_p3", - format!( - "Refined accumulator kind: Int → String (target '{}' is StringBox)", - spec.target_name - ), - ); - } - } - MirType::Integer => { - // Confirmed Int - if verbose { - log.log( - "phase100_p3", - format!( - "Confirmed accumulator kind: Int (target '{}' is Integer)", - spec.target_name - ), - ); - } - } - _ => { - // Unknown type - keep default Int for backward compat - if verbose { - log.log( - "phase100_p3", - format!( - "Accumulator kind: Int (default, target '{}' type unknown: {:?})", - spec.target_name, target_type - ), - ); - } - } - } - } - } - - // Phase 100 P3-3: Validate String accumulator constraints - if refined_kind == AccumulatorKind::String { - // String accumulator RHS must be Variable (not Literal, not MethodCall) - match spec.rhs_expr_kind { - RhsExprKind::Var => { - // OK: Variable RHS is allowed for String accumulators - if verbose { - log.log( - "phase100_p3", - format!( - "String accumulator '{}' = '{}' + '{}' (Variable RHS: OK)", - spec.target_name, spec.target_name, spec.rhs_var_or_lit - ), - ); - } - } - RhsExprKind::Literal => { - // Fail-Fast: Literal RHS not supported in P3 (will be P3.1) - return Err(format!( - "[joinir/mutable-acc] String accumulator '{}' with Literal RHS not supported in Phase 100 P3 (will be P3.1)", - spec.target_name - )); - } - } - } - - // Check RHS read-only via captured_env lookup - // According to spec: x ∈ {Const, BodyLocal, Captured, Pinned, Carrier} - // At this point in prepare_pattern2_inputs: - // - Const/Literal: Always read-only - // - BodyLocal: Not yet defined (body_local_env is empty) - // - Captured: In captured_env - // - Pinned: In captured_env (just added above) - // - Carrier: In carrier_info (from ctx) - - match spec.rhs_expr_kind { - RhsExprKind::Literal => { - // Literals are always read-only, OK - if verbose { - log.log( - "phase100_p2", - format!( - "RHS is literal '{}' (read-only, OK)", - spec.rhs_var_or_lit - ), - ); - } - } - RhsExprKind::Var => { - // Use captured_env + carrier_info to check if rhs_var is read-only - let rhs_name = &spec.rhs_var_or_lit; - - // Check if RHS is in captured_env (read-only) - let in_captured = captured_env.vars.iter().any(|v| &v.name == rhs_name); - - // Check if RHS is in carrier_info (could be mutable) - let in_carrier = carrier_info - .carriers - .iter() - .any(|c| &c.name == rhs_name); - - if in_carrier { - // RHS is a carrier - this could be mutable - // Fail-Fast: We don't support mutable RHS yet - return Err(format!( - "[joinir/mutable-acc] RHS '{}' must be read-only (Condition/BodyLocal/Captured/Pinned), but found mutable Carrier", - rhs_name - )); - } else if !in_captured && !builder.variable_ctx.variable_map.contains_key(rhs_name) { - // RHS might be a LoopBodyLocal (defined inside loop) - // We can't validate it at this point because body_local_env is empty - // The lowering phase will validate it later - if verbose { - log.log( - "phase100_p2", - format!( - "RHS '{}' not in captured/variable_map, assuming LoopBodyLocal (will validate later)", - rhs_name - ), - ); - } - } else { - // RHS is read-only (in captured_env or loop-outer variable_map) - if verbose { - log.log( - "phase100_p2", - format!( - "RHS '{}' is read-only (in_captured={}, in_variable_map={})", - rhs_name, - in_captured, - builder.variable_ctx.variable_map.contains_key(rhs_name) - ), - ); - } - } - } - } - - // OK → Register as mutable LoopState carrier - // Note: We add this to carrier_info which was cloned from ctx.carrier_info - // This means it will participate in the loop state carrier mechanism - - if verbose { - log.log( - "phase100_p2", - format!( - "Promoting '{}' to mutable LoopState carrier", - spec.target_name - ), - ); - } - - // Add to carrier_info - use crate::mir::join_ir::lowering::carrier_info::CarrierVar; - carrier_info - .carriers - .push(CarrierVar::new(spec.target_name.clone(), *target_id)); - - if verbose { - log.log( - "phase100_p2", - format!( - "carrier_info now has {} carriers", - carrier_info.carriers.len() - ), - ); - } - } else if verbose { - log.log("phase100_p2", "No mutable accumulator pattern detected"); - } - - // Value space + condition env - let mut join_value_space = JoinValueSpace::new(); - let (mut env, mut condition_bindings, _loop_var_join_id) = - ConditionEnvBuilder::build_for_break_condition_v2( - condition, - &loop_var_name, - &builder.variable_ctx.variable_map, - loop_var_id, - &mut join_value_space, - )?; - - // Phase 79-2: Register loop variable BindingId (dev-only) - #[cfg(feature = "normalized_dev")] - // Phase 136 Step 4/7: Use binding_ctx for lookup - if let Some(loop_var_bid) = builder.binding_ctx.lookup(&loop_var_name) { - env.register_loop_var_binding(loop_var_bid, _loop_var_join_id); - log.log( - "phase79", - format!( - "Registered loop var BindingId: '{}' BindingId({}) → ValueId({})", - loop_var_name, loop_var_bid.0, _loop_var_join_id.0 - ), - ); - } - - log.log( - "phase201", - format!( - "Using JoinValueSpace: loop_var '{}' → {:?}", - loop_var_name, - env.get(&loop_var_name) - ), - ); - - // Add captured vars - for var in &captured_env.vars { - if let Some(&host_id) = builder.variable_ctx.variable_map.get(&var.name) { - let join_id = join_value_space.alloc_param(); - env.insert(var.name.clone(), join_id); - condition_bindings.push(ConditionBinding { - name: var.name.clone(), - host_value: host_id, - join_value: join_id, - }); - log.log( - "capture", - format!( - "Phase 201: Added captured '{}': host={:?}, join={:?}", - var.name, host_id, join_id - ), - ); - } - } - - let body_local_env = LoopBodyLocalEnv::new(); - log.log( - "body-local", - format!( - "Phase 201: Created empty body-local environment (param_count={})", - join_value_space.param_count() - ), - ); - if verbose { - log.log( - "cond-env", - format!("Phase 201: ConditionEnv contains {} variables:", env.len()), - ); - log.log( - "cond-env", - format!( - " Loop param '{}' → JoinIR {:?}", - loop_var_name, - env.get(&loop_var_name) - ), - ); - if !condition_bindings.is_empty() { - log.log( - "cond-env", - format!(" {} condition-only bindings:", condition_bindings.len()), - ); - for binding in &condition_bindings { - log.log( - "cond-env", - format!( - " '{}': HOST {:?} → JoinIR {:?}", - binding.name, binding.host_value, binding.join_value - ), - ); - } - } else { - log.log("cond-env", " No condition-only variables"); - } - } - - // Break condition extraction is policy-routed (SSOT). - let break_routing = Pattern2BreakConditionPolicyRouterBox::route(condition, body)?; - - let mut inputs = Pattern2Inputs { - loop_var_name, - loop_var_id, - carrier_info, - scope, - captured_env, - join_value_space, - 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, // Phase 93 P0: Will be set by apply_trim_and_normalize - body_local_derived_recipe: None, // Phase 94: Will be set after normalization - }; - - // loop(true) read-digits family wires body-local allow-list in one place. - 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) -} - -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 = 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() { - // loop(true) read-digits family pre-wires slot/allow-list in prepare_pattern2_inputs. - // Do not run promotion heuristics here. - 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, - }) => { - // Phase 133 P1: Check if this is a Trim promotion (A-3 pattern) - // Trim promotions are handled by TrimLoopLowerer (apply_trim_and_normalize) - // which provides SSOT for env/join_id, so we defer to that path. - let is_trim_promotion = promoted_carrier.trim_helper().is_some(); - - // Phase 133 P1: Only add to promoted_pairs for non-Trim promotions - // (e.g., DigitPos/A-4). Trim carriers don't have join_id in carrier_info - // because they use loop_var_name instead of carriers vector. - 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); - } - - log.log( - "cond_promoter", - format!( - "LoopBodyLocal '{}' promoted to carrier '{}'", - promoted_var, carrier_name - ), - ); - - inputs - .carrier_info - .promoted_loopbodylocals - .push(promoted_var.clone()); - - log.log( - "cond_promoter", - format!( - "Merged carrier '{}' into CarrierInfo (total carriers: {})", - carrier_name, - inputs.carrier_info.carrier_count() - ), - ); - log.log( - "cond_promoter", - format!( - "Phase 224: Recorded promoted variable '{}' in carrier_info.promoted_loopbodylocals", - promoted_var - ), - ); - - if let Some(helper) = inputs.carrier_info.trim_helper() { - if helper.is_safe_trim() { - log.log("cond_promoter", "Safe Trim pattern detected"); - log.log( - "cond_promoter", - format!( - "Carrier: '{}', original var: '{}', whitespace chars: {:?}", - helper.carrier_name, helper.original_var, helper.whitespace_chars - ), - ); - log.log( - "phase133", - format!( - "Phase 133 P1: Trim promotion deferred to TrimLoopLowerer (SSOT for env/join_id)" - ), - ); - } else { - return Err(error_messages::format_error_pattern2_trim_not_safe( - &helper.carrier_name, - helper.whitespace_count(), - )); - } - } - - // Phase 133 P1: Only normalize for non-Trim promotions - // Trim promotions are normalized by TrimLoopLowerer - if !is_trim_promotion { - inputs.break_condition_node = DigitPosConditionNormalizer::normalize( - &inputs.break_condition_node, - &promoted_var, - &carrier_name, - ); - - log.log( - "phase224e", - format!( - "Normalized break condition for promoted variable '{}' → carrier '{}'", - promoted_var, carrier_name - ), - ); - } - } - PolicyDecision::Use(BodyLocalRoute::ReadOnlySlot(slot)) => { - log.log( - "body_local_slot", - format!( - "Phase 92 P3: BodyLocalPolicy=UseReadOnlySlot var='{}' (decl@{}, break_if@{}, init={:?})", - slot.name, slot.decl_stmt_index, slot.break_guard_stmt_index, &slot.init_expr - ), - ); - 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 => {} - } - } - } - - log.log( - "phase224d", - format!( - "Allocating join_ids for {} carriers", - inputs.carrier_info.carriers.len() - ), - ); - 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 { - // Phase 79-2: Use role-specific registration method - use crate::mir::join_ir::lowering::carrier_info::CarrierRole; - match carrier.role { - CarrierRole::ConditionOnly => { - inputs - .env - .register_condition_binding(binding_id, carrier_join_id); - log.log( - "phase79", - format!( - "Registered condition-only carrier '{}' BindingId({}) → ValueId({})", - carrier.name, binding_id.0, carrier_join_id.0 - ), - ); - } - CarrierRole::LoopState => { - inputs - .env - .register_carrier_binding(binding_id, carrier_join_id); - log.log( - "phase79", - format!( - "Registered loop-state carrier '{}' BindingId({}) → ValueId({})", - carrier.name, binding_id.0, carrier_join_id.0 - ), - ); - } - } - } - log.log( - "phase224d", - format!( - "Allocated carrier '{}' param ID: {:?}", - carrier.name, 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.clone(), join_id); - log.log( - "phase229", - format!( - "Resolved promoted '{}' → carrier '{}' (join_id={:?})", - promoted_var, promoted_carrier_name, join_id - ), - ); - } - - // ExprLowerer validation (unchanged) - { - 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(_value_id) => { - log.log("phase231", "ExprLowerer successfully validated break condition"); - } - Err(ExprLoweringError::UnsupportedNode(msg)) => { - log.log("phase231", format!("ExprLowerer fallback (unsupported): {}", msg)); - } - Err(e) => { - log.log("phase231", format!("ExprLowerer validation error: {}", e)); - } - } - } - - Ok(()) -} - -fn apply_trim_and_normalize( - builder: &mut MirBuilder, - condition: &ASTNode, - body: &[ASTNode], - inputs: &mut Pattern2Inputs, - verbose: bool, -) -> Result<(ASTNode, Option>), String> { - let log = Pattern2DebugLog::new(verbose); - let mut alloc_join_value = || inputs.join_value_space.alloc_param(); - - // loop(true) read-digits family uses a digit-guard OR-chain which resembles - // "trim-like" patterns; do not route through TrimLoopLowerer. - 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; - - // Phase 93 P0: Save condition_only_recipe instead of adding to condition_bindings - inputs.condition_only_recipe = trim_result.condition_only_recipe; - - // Phase 93 P0: condition_bindings should be empty now (no more ConditionBinding for derived slots) - if !trim_result.condition_bindings.is_empty() { - log.log( - "trim", - format!( - "WARNING: Phase 93 P0 expects empty condition_bindings, got {}", - trim_result.condition_bindings.len() - ), - ); - } - - log.log( - "trim", - format!( - "Phase 93 P0: condition_only_recipe={}", - if inputs.condition_only_recipe.is_some() { "Some" } else { "None" } - ), - ); - 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, - temp_name, - } => { - log.log( - "phase192", - format!( - "Normalized complex addend: temp='{}' inserted before update", - temp_name - ), - ); - normalized_body.push(temp_def); - normalized_body.push(new_assign); - has_normalization = true; - } - NormalizationResult::Unchanged => { - normalized_body.push(node.clone()); - } - } - } - - let normalized_body = if has_normalization { - Some(normalized_body) - } else { - None - }; - - Ok((effective_break_condition, normalized_body)) -} - -/// Phase 185-2: Collect body-local variable declarations from loop body -/// -/// Returns Vec<(name, ValueId)> for variables declared with `local` in loop body. -/// This function scans the loop body AST for Local nodes and allocates -/// JoinIR-local ValueIds for each one. -/// -/// # Arguments -/// -/// * `body` - Loop body AST nodes to scan -/// * `alloc_join_value` - JoinIR ValueId allocator function -/// -/// # Returns -/// -/// Vector of (variable_name, join_value_id) pairs for all body-local variables -#[allow(dead_code)] -fn collect_body_local_variables( - body: &[ASTNode], - alloc_join_value: &mut dyn FnMut() -> ValueId, -) -> Vec<(String, ValueId)> { - let mut locals = Vec::new(); - let verbose = crate::config::env::joinir_dev_enabled(); - let log = Pattern2DebugLog::new(verbose); - for node in body { - if let ASTNode::Local { variables, .. } = node { - // Local declaration can have multiple variables (e.g., local a, b, c) - for name in variables { - let value_id = alloc_join_value(); - locals.push((name.clone(), value_id)); - log.log("body-local", format!("Collected local '{}' → {:?}", name, value_id)); - } - } - } - locals -} /// Phase 194: Detection function for Pattern 2 /// @@ -1112,243 +180,18 @@ impl MirBuilder { fn_body: Option<&[ASTNode]>, skeleton: Option<&crate::mir::loop_canonicalizer::LoopSkeleton>, ) -> Result, 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); - - // Phase 195: Use unified trace - trace::trace().debug("pattern2", "Calling Pattern 2 minimal lowerer"); - - // Phase 179-B: Use PatternPipelineContext for unified preprocessing - // Note: Pattern 2 still needs inline processing for Trim/carrier logic - use super::pattern_pipeline::{build_pattern_context, PatternVariant}; - let ctx = build_pattern_context(self, condition, _body, PatternVariant::Pattern2)?; - - trace::trace().varmap("pattern2_start", &self.variable_ctx.variable_map); - - let mut inputs = prepare_pattern2_inputs(self, condition, _body, fn_body, &ctx, verbose)?; - promote_and_prepare_carriers(self, condition, _body, &mut inputs, debug, verbose)?; - let (effective_break_condition, normalized_body) = - apply_trim_and_normalize(self, 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). - // Route is explicit; in strict mode, partial matches that require derived support must fail-fast. - 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 => { - // Strict-only guard: if we see a body-local reassignment to `ch`, don't silently miscompile. - 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() - ), - ); - - 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 - ), - ); - } - - log.log( - "updates", - format!( - "Phase 176-5: Added body-only carrier '{}' to ConditionEnv: HOST {:?} → JoinIR {:?}", - carrier.name, carrier.host_id, join_value - ), - ); - } - } - - log.log( - "before_lowerer", - format!( - "About to call lower_loop_with_break_minimal with carrier_info.loop_var_name='{}'", - inputs.carrier_info.loop_var_name - ), - ); - - let body_local_env = &mut inputs.body_local_env; - let join_value_space = &mut inputs.join_value_space; - let lowering_inputs = LoopWithBreakLoweringInputs { - scope: inputs.scope, - 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(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, - skeleton, - condition_only_recipe: inputs.condition_only_recipe.as_ref(), // Phase 93 P0 - body_local_derived_recipe: inputs.body_local_derived_recipe.as_ref(), // Phase 94 - }; - - let (join_module, fragment_meta) = match lower_loop_with_break_minimal(lowering_inputs) { - Ok((module, meta)) => (module, meta), - Err(e) => { - // Phase 195: Use unified trace - trace::trace().debug("pattern2", &format!("Pattern 2 lowerer failed: {}", e)); - return Err(format!("[cf_loop/pattern2] Lowering failed: {}", e)); - } - }; - - // Phase 33-14: Extract exit_meta from fragment_meta for backward compatibility - let exit_meta = &fragment_meta.exit_meta; - - // Phase 33-10: Collect exit bindings from ExitMeta using Box - // Phase 228-8: Pass carrier_info to include ConditionOnly carriers - use crate::mir::builder::control_flow::joinir::merge::exit_line::ExitMetaCollector; - let exit_bindings = - ExitMetaCollector::collect(self, &exit_meta, Some(&inputs.carrier_info), debug); - - // Phase 176-3: Build input mappings for all carriers - // JoinIR main() params: [ValueId(0), ValueId(1), ValueId(2), ...] for (i, carrier1, carrier2, ...) - // Host values: [loop_var_id, carrier1_host_id, carrier2_host_id, ...] - let mut join_input_slots = vec![ValueId(0)]; // Loop variable - let mut host_input_values = vec![inputs.loop_var_id]; // Loop variable - - 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); - } - - log.log( - "boundary", - format!( - "Phase 176-3: Boundary inputs - {} JoinIR slots, {} host values", - join_input_slots.len(), - host_input_values.len() - ), - ); - - // Phase 200-2: Use JoinInlineBoundaryBuilder for clean construction - // Canonical Builder pattern - see docs/development/current/main/joinir-boundary-builder-pattern.md - use crate::mir::join_ir::lowering::JoinInlineBoundaryBuilder; - let boundary = JoinInlineBoundaryBuilder::new() - .with_inputs( - join_input_slots, // JoinIR's main() parameters (loop variable + carriers) - host_input_values, // Host's loop variable + carrier values - ) - .with_condition_bindings(inputs.condition_bindings) - .with_exit_bindings(exit_bindings.clone()) - .with_expr_result(fragment_meta.expr_result) // Phase 33-14: Pass expr_result to merger - .with_loop_var_name(Some(inputs.loop_var_name.clone())) // Phase 33-16: For LoopHeaderPhiBuilder - .with_carrier_info(inputs.carrier_info.clone()) // Phase 228-6: Pass carrier_info for ConditionOnly header PHI init - .build(); - - // Phase 33-22: Use JoinIRConversionPipeline for unified conversion flow - use super::conversion_pipeline::JoinIRConversionPipeline; - let _ = JoinIRConversionPipeline::execute( + super::pattern2_lowering_orchestrator::Pattern2LoweringOrchestrator::run( self, - join_module, - Some(&boundary), - "pattern2", + condition, + _body, + _func_name, debug, - )?; - - // Phase 188-Impl-2: Return Void (loops don't produce values) - // The subsequent "return i" statement will emit its own Load + Return - let void_val = crate::mir::builder::emission::constant::emit_void(self); - - // Phase 195: Use unified trace - trace::trace().debug( - "pattern2", - &format!("Loop complete, returning Void {:?}", void_val), - ); - - Ok(Some(void_val)) + fn_body, + skeleton, + ) } } -/// 更新を持たない FromHost キャリアを落とすヘルパー。 -fn filter_carriers_for_updates( - carrier_info: &mut CarrierInfo, - carrier_updates: &BTreeMap, -) { - 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 - }); -} - #[cfg(test)] mod tests { use super::*; @@ -1389,6 +232,8 @@ mod tests { use super::super::pattern_pipeline::{build_pattern_context, PatternVariant}; 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; let mut builder = MirBuilder::new(); builder @@ -1466,9 +311,9 @@ mod tests { let ctx = build_pattern_context(&mut builder, &condition, &body, PatternVariant::Pattern2) .expect("build_pattern_context"); - let mut inputs = prepare_pattern2_inputs(&builder, &condition, &body, None, &ctx, false) - .expect("prepare_pattern2_inputs"); - + let mut inputs = + Pattern2InputsFactsBox::analyze(&builder, &condition, &body, None, &ctx, false) + .expect("Pattern2InputsFactsBox::analyze"); promote_and_prepare_carriers(&mut builder, &condition, &body, &mut inputs, false, false) .expect("promote_and_prepare_carriers");