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 995c538b..95ab5f04 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 @@ -54,6 +54,8 @@ struct Pattern2Inputs { /// Phase 92 P3: Diagnostics / debug metadata for the allow-listed variable. read_only_body_local_slot: Option, break_condition_node: ASTNode, + /// Phase 93 P0: ConditionOnly recipe for derived slot recalculation + condition_only_recipe: Option, } fn prepare_pattern2_inputs( @@ -251,6 +253,7 @@ fn prepare_pattern2_inputs( allowed_body_locals_for_conditions: Vec::new(), read_only_body_local_slot: None, break_condition_node, + condition_only_recipe: None, // Phase 93 P0: Will be set by apply_trim_and_normalize }) } @@ -535,17 +538,26 @@ fn apply_trim_and_normalize( )? { log.log("trim", "TrimLoopLowerer processed Trim pattern successfully"); inputs.carrier_info = trim_result.carrier_info; - inputs - .condition_bindings - .extend(trim_result.condition_bindings.iter().cloned()); - for binding in &trim_result.condition_bindings { - inputs.env.insert(binding.name.clone(), binding.join_value); + + // 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!( - "Extended condition_bindings with {} Trim bindings", - trim_result.condition_bindings.len() + "Phase 93 P0: condition_only_recipe={}", + if inputs.condition_only_recipe.is_some() { "Some" } else { "None" } ), ); trim_result.condition @@ -881,6 +893,7 @@ impl MirBuilder { }, join_value_space, skeleton, + condition_only_recipe: inputs.condition_only_recipe.as_ref(), // Phase 93 P0 }; let (join_module, fragment_meta) = match lower_loop_with_break_minimal(lowering_inputs) { diff --git a/src/mir/builder/control_flow/joinir/patterns/trim_loop_lowering.rs b/src/mir/builder/control_flow/joinir/patterns/trim_loop_lowering.rs index 551f7789..c5f45a1e 100644 --- a/src/mir/builder/control_flow/joinir/patterns/trim_loop_lowering.rs +++ b/src/mir/builder/control_flow/joinir/patterns/trim_loop_lowering.rs @@ -74,6 +74,11 @@ pub(crate) struct TrimLoweringResult { /// /// Pattern2/4 will extend their condition_bindings with these pub condition_bindings: Vec, + + /// Phase 93 P0: ConditionOnly recipe for derived slot recalculation + /// + /// Pattern2/4 will use this to emit recalculation after body-local init + pub condition_only_recipe: Option, } impl TrimLoopLowerer { @@ -269,22 +274,34 @@ impl TrimLoopLowerer { verbose, ); - // Step 3: Convert to CarrierInfo and merge + // Step 3: Register promoted body-local variable (ConditionOnly) + // Note: is_ch_match is NOT a LoopState carrier (no header PHI). + // It's a condition-only variable recalculated each loop iteration. + carrier_info + .promoted_loopbodylocals + .push(trim_info.var_name.clone()); + + // Step 3.5: Attach TrimLoopHelper for pattern-specific lowering logic + use crate::mir::loop_pattern_detection::trim_loop_helper::TrimLoopHelper; + carrier_info.trim_helper = Some(TrimLoopHelper::from_pattern_info(&trim_info)); + + // Phase 78: Type-safe BindingId promotion tracking (using PromotedBindingRecorder) #[cfg(feature = "normalized_dev")] - // Phase 136 Step 4/7: Use binding_ctx for binding_map reference - let promoted_carrier = - trim_info.to_carrier_info(Some(builder.binding_ctx.binding_map())); - #[cfg(not(feature = "normalized_dev"))] - let promoted_carrier = trim_info.to_carrier_info(); - carrier_info.merge_from(&promoted_carrier); + { + use crate::mir::loop_pattern_detection::loop_body_carrier_promoter::PromotedBindingRecorder; + let recorder = PromotedBindingRecorder::new(Some(builder.binding_ctx.binding_map())); + if let Err(e) = recorder.record_promotion(&mut carrier_info, &trim_info.var_name, &trim_info.carrier_name) { + eprintln!("[TrimLoopLowerer] Failed to record promoted binding: {}", e); + } + } trace.emit_if( "trim", "promote", &format!( - "Merged carrier '{}' into CarrierInfo (total carriers: {})", - trim_info.carrier_name, - carrier_info.carrier_count() + "Promoted body-local '{}' to condition-only variable '{}' (not a LoopState carrier)", + trim_info.var_name, + trim_info.carrier_name ), verbose, ); @@ -336,29 +353,41 @@ impl TrimLoopLowerer { verbose, ); - // Step 6: Generate Trim break condition - let trim_break_condition = Self::generate_trim_break_condition(trim_helper); - - trace.emit_if( - "trim", - "cond", - &format!( - "Replaced break condition with !{}", - trim_helper.carrier_name - ), - verbose, - ); - - // Step 7: Setup ConditionEnv bindings - let condition_bindings = + // Step 6: Setup ConditionEnv bindings FIRST to determine if ConditionOnly + let (condition_bindings, condition_only_recipe) = Self::setup_condition_env_bindings(builder, trim_helper, alloc_join_value)?; trace.emit_if( "trim", "cond", &format!( - "Added {} condition bindings", - condition_bindings.len() + "Phase 93 P0: condition_bindings={}, condition_only_recipe={}", + condition_bindings.len(), + if condition_only_recipe.is_some() { "Some" } else { "None" } + ), + verbose, + ); + + // Step 7: Generate break condition based on pattern type + // Phase 93 P0: ConditionOnly uses non-negated condition (break when is_ch_match is TRUE) + // Normal Trim uses negated condition (break when !is_ch_match, i.e., ch is NOT whitespace) + let trim_break_condition = if condition_only_recipe.is_some() { + // ConditionOnly: "break when match" semantics + // Generate: is_ch_match (TRUE when we should break) + Self::generate_condition_only_break_condition(trim_helper) + } else { + // Normal Trim: "break when NOT match" semantics + // Generate: !is_ch_match (TRUE when we should break) + Self::generate_trim_break_condition(trim_helper) + }; + + trace.emit_if( + "trim", + "cond", + &format!( + "Break condition: {} (ConditionOnly={})", + trim_helper.carrier_name, + condition_only_recipe.is_some() ), verbose, ); @@ -368,6 +397,7 @@ impl TrimLoopLowerer { condition: trim_break_condition, carrier_info: carrier_info.clone(), condition_bindings, + condition_only_recipe, })) } PromotionResult::CannotPromote { reason, vars } => { @@ -490,11 +520,12 @@ impl TrimLoopLowerer { Ok(()) } - /// Generate Trim break condition + /// Generate Trim break condition (normal Trim pattern) /// /// Phase 180-3: Extracted from Pattern2 (lines 343-377) /// /// Returns: !is_carrier (negated carrier check) + /// Used for "break when NOT match" semantics (e.g., str.trim()) fn generate_trim_break_condition( trim_helper: &crate::mir::loop_pattern_detection::trim_loop_helper::TrimLoopHelper, ) -> ASTNode { @@ -502,6 +533,24 @@ impl TrimLoopLowerer { TrimPatternLowerer::generate_trim_break_condition(trim_helper) } + /// Generate ConditionOnly break condition + /// + /// Phase 93 P0: For ConditionOnly patterns where break happens WHEN the condition is TRUE. + /// + /// Returns: is_carrier (non-negated carrier check) + /// Used for "break when match" semantics (e.g., find-first pattern) + fn generate_condition_only_break_condition( + trim_helper: &crate::mir::loop_pattern_detection::trim_loop_helper::TrimLoopHelper, + ) -> ASTNode { + use crate::ast::Span; + // Return just the carrier variable (non-negated) + // When is_ch_match is TRUE, we should break + ASTNode::Variable { + name: trim_helper.carrier_name.clone(), + span: Span::unknown(), + } + } + /// Setup ConditionEnv bindings for Trim carrier /// /// Phase 180-3: Extracted from Pattern2 (lines 345-377) @@ -513,60 +562,28 @@ impl TrimLoopLowerer { builder: &mut MirBuilder, trim_helper: &crate::mir::loop_pattern_detection::trim_loop_helper::TrimLoopHelper, alloc_join_value: &mut dyn FnMut() -> ValueId, - ) -> Result, String> { + ) -> Result<(Vec, Option), String> { use crate::mir::builder::control_flow::joinir::patterns::trim_pattern_lowerer::TrimPatternLowerer; + use crate::mir::join_ir::lowering::common::condition_only_emitter::ConditionOnlyRecipe; let trace = crate::mir::builder::control_flow::joinir::trace::trace(); let verbose = crate::config::env::joinir_dev_enabled() || trace.is_joinir_enabled(); - let mut bindings = Vec::new(); - - // Add carrier to ConditionEnv - let get_value = |name: &str| builder.variable_ctx.variable_map.get(name).copied(); - let mut env_temp = std::collections::HashMap::new(); // Temporary env for closure - - let binding = TrimPatternLowerer::add_to_condition_env( - trim_helper, - get_value, - |name, value| { - env_temp.insert(name, value); - }, - alloc_join_value, - )?; + // Phase 93 P0: Do NOT add is_ch_match to ConditionBinding + // Instead, create a ConditionOnlyRecipe for recalculation every iteration + let recipe = ConditionOnlyRecipe::from_trim_helper(trim_helper); trace.emit_if( "trim", - "cond-env", + "condition-only", &format!( - "Added carrier '{}' to ConditionEnv: HOST {:?} → JoinIR {:?}", - trim_helper.carrier_name, binding.host_value, binding.join_value + "Phase 93 P0: Created ConditionOnlyRecipe for '{}' (will be recalculated each iteration, not carried via ConditionBinding)", + trim_helper.carrier_name ), verbose, ); - bindings.push(binding.clone()); - - // Phase 176-6: Also map the original variable name to the same JoinIR ValueId - // This allows the loop body to reference the original variable (e.g., 'ch') - // even though it was promoted to a carrier (e.g., 'is_ch_match') - let original_binding = ConditionBinding { - name: trim_helper.original_var.clone(), - host_value: binding.host_value, - join_value: binding.join_value, - }; - - trace.emit_if( - "trim", - "cond-env", - &format!( - "Phase 176-6: Also mapped original var '{}' → JoinIR {:?}", - trim_helper.original_var, binding.join_value - ), - verbose, - ); - - bindings.push(original_binding); - - Ok(bindings) + // Return empty bindings - the derived slot will be recalculated, not bound + Ok((Vec::new(), Some(recipe))) } } diff --git a/src/mir/join_ir/lowering/common.rs b/src/mir/join_ir/lowering/common.rs index fbcf9c91..a9bf6a4d 100644 --- a/src/mir/join_ir/lowering/common.rs +++ b/src/mir/join_ir/lowering/common.rs @@ -6,6 +6,7 @@ pub mod case_a; pub mod conditional_step_emitter; // Phase 92 P1-1: ConditionalStep emission module pub mod body_local_slot; // Phase 92 P3: Read-only body-local slot for conditions pub mod dual_value_rewriter; // Phase 246-EX/247-EX: name-based dual-value rewrites +pub mod condition_only_emitter; // Phase 93 P0: ConditionOnly (Derived Slot) recalculation use crate::mir::loop_form::LoopForm; use crate::mir::query::{MirQuery, MirQueryBox}; diff --git a/src/mir/join_ir/lowering/common/condition_only_emitter.rs b/src/mir/join_ir/lowering/common/condition_only_emitter.rs new file mode 100644 index 00000000..462c2d69 --- /dev/null +++ b/src/mir/join_ir/lowering/common/condition_only_emitter.rs @@ -0,0 +1,309 @@ +//! ConditionOnly Variable Emitter - 毎イテレーション再計算される派生値 +//! +//! # Phase 93 P0: ConditionOnly(Derived Slot)アーキテクチャ +//! +//! ## 問題 +//! Trim patternの`is_ch_match`は: +//! - LoopState carrier(PHI対象)ではない(ConditionOnly) +//! - でも初回計算値がConditionBindingで運ばれてしまい、毎イテレーション同じ値がコピーされる +//! +//! ## 解決策 +//! ConditionOnlyをConditionBindingで運ばず、「再計算レシピ」として扱う: +//! 1. 初回計算値をConditionBindingに入れない +//! 2. 代わりにDerived slotとして、毎イテレーションで再計算する +//! 3. TrimPatternValidator::emit_whitespace_check()を毎周回呼ぶ +//! +//! ## アーキテクチャ +//! ``` +//! ループ前: +//! is_ch_match0 = (s.substring(0, 1) == "b") ← 初期化のみ(使われない) +//! +//! ループbody(毎イテレーション): +//! ch = s.substring(i, i+1) ← body-local init +//! is_ch_match = (ch == "b") ← Derived slot再計算 ★ここ! +//! if is_ch_match { break } ← break条件で使用 +//! ``` +//! +//! ## 配置理由 +//! Phase 92の`body_local_slot.rs`と並べる(`src/mir/join_ir/lowering/common/`): +//! - body-local変数とConditionOnly変数は密接に関連 +//! - 両方とも「ループbody内でのみ有効」な派生値 + +use crate::ast::ASTNode; +use crate::mir::join_ir::lowering::condition_env::ConditionEnv; +use crate::mir::join_ir::lowering::loop_body_local_env::LoopBodyLocalEnv; +use crate::mir::join_ir::JoinInst; +use crate::mir::loop_pattern_detection::trim_loop_helper::TrimLoopHelper; +use crate::mir::ValueId; + +/// ConditionOnly変数の再計算レシピ +/// +/// Phase 93 P0: Trim patternの`is_ch_match`など、毎イテレーション再計算される変数 +#[derive(Debug, Clone)] +pub struct ConditionOnlyRecipe { + /// 変数名(例: "is_ch_match") + pub name: String, + /// 元の変数名(例: "ch") + pub original_var: String, + /// Trim pattern用のホワイトスペース文字リスト + pub whitespace_chars: Vec, +} + +impl ConditionOnlyRecipe { + /// Trim patternからレシピを作成 + pub fn from_trim_helper(trim_helper: &TrimLoopHelper) -> Self { + Self { + name: trim_helper.carrier_name.clone(), + original_var: trim_helper.original_var.clone(), + whitespace_chars: trim_helper.whitespace_chars.clone(), + } + } +} + +/// ConditionOnly Variable Emitter - 毎イテレーション再計算 +/// +/// # 責務 +/// - ConditionOnly変数(`is_ch_match`など)を毎イテレーション再計算 +/// - body-local変数(`ch`)から派生値を生成 +/// - ConditionEnvに再計算後の値を登録 +/// +/// # Phase 93 P0設計 +/// - ConditionBindingでは運ばない(初回値を固定しない) +/// - 代わりに毎周回、body-local initの後に再計算 +pub struct ConditionOnlyEmitter; + +impl ConditionOnlyEmitter { + /// ConditionOnly変数を再計算してemit + /// + /// # Phase 93 P0: Trim pattern専用 + /// + /// ## 処理フロー + /// 1. body-local env から元変数(`ch`)のValueIdを取得 + /// 2. Trim patternの条件(`ch == "b"`など)を再評価 + /// 3. 結果をConditionEnvに登録 + /// + /// ## 呼び出しタイミング + /// Pattern2の`emit_body_local_init()`の直後 + /// + /// # Arguments + /// - `recipe`: 再計算レシピ(Trim pattern情報) + /// - `body_local_env`: body-local変数の環境(`ch`の値を取得) + /// - `condition_env`: 条件式環境(再計算後の`is_ch_match`を登録) + /// - `alloc_value`: ValueId割り当て関数 + /// - `instructions`: 出力先JoinIR命令列 + /// + /// # Returns + /// - Ok(condition_value_id): 再計算後のConditionOnly変数のValueId + /// - Err(msg): エラーメッセージ + pub fn emit_condition_only_recalc( + recipe: &ConditionOnlyRecipe, + body_local_env: &LoopBodyLocalEnv, + condition_env: &mut ConditionEnv, + alloc_value: &mut dyn FnMut() -> ValueId, + instructions: &mut Vec, + ) -> Result { + // Step 1: body-local envから元変数(例: "ch")のValueIdを取得 + let source_value = body_local_env.get(&recipe.original_var).ok_or_else(|| { + format!( + "[ConditionOnlyEmitter] Original variable '{}' not found in LoopBodyLocalEnv for '{}'", + recipe.original_var, recipe.name + ) + })?; + + // Step 2: Trim patternの条件を再評価 + // TrimPatternValidator::emit_whitespace_check()相当の処理 + let condition_value = Self::emit_whitespace_check_inline( + source_value, + &recipe.whitespace_chars, + alloc_value, + instructions, + )?; + + // Step 3: ConditionEnvに再計算後の値を登録 + condition_env.insert(recipe.name.clone(), condition_value); + + Ok(condition_value) + } + + /// Whitespace check(インライン実装) + /// + /// # Phase 93 P0: TrimPatternValidator::emit_whitespace_check()相当 + /// + /// ## 処理 + /// ```mir + /// %cond1 = icmp Eq %ch, " " + /// %cond2 = icmp Eq %ch, "\t" + /// %result = %cond1 Or %cond2 // 複数文字の場合はORチェーン + /// ``` + fn emit_whitespace_check_inline( + source_value: ValueId, + whitespace_chars: &[String], + alloc_value: &mut dyn FnMut() -> ValueId, + instructions: &mut Vec, + ) -> Result { + use crate::mir::join_ir::{BinOpKind, CompareOp, ConstValue, MirLikeInst}; + + if whitespace_chars.is_empty() { + return Err("[ConditionOnlyEmitter] Whitespace chars list is empty".to_string()); + } + + // 各ホワイトスペース文字との比較を生成 + let mut cond_values = Vec::new(); + for ws_char in whitespace_chars { + // Const: ホワイトスペース文字 + let const_dst = alloc_value(); + instructions.push(JoinInst::Compute(MirLikeInst::Const { + dst: const_dst, + value: ConstValue::String(ws_char.clone()), + })); + + // Compare: source_value == ws_char + let cmp_dst = alloc_value(); + instructions.push(JoinInst::Compute(MirLikeInst::Compare { + dst: cmp_dst, + op: CompareOp::Eq, + lhs: source_value, + rhs: const_dst, + })); + + cond_values.push(cmp_dst); + } + + // 複数の条件をORでチェーン + let mut result = cond_values[0]; + for &cond in &cond_values[1..] { + let or_dst = alloc_value(); + instructions.push(JoinInst::Compute(MirLikeInst::BinOp { + dst: or_dst, + op: BinOpKind::Or, + lhs: result, + rhs: cond, + })); + result = or_dst; + } + + Ok(result) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mir::join_ir::lowering::condition_env::ConditionEnv; + use crate::mir::join_ir::lowering::loop_body_local_env::LoopBodyLocalEnv; + use crate::mir::join_ir::JoinInst; + use crate::mir::ValueId; + + #[test] + fn test_emit_condition_only_recalc_single_char() { + // Setup + let recipe = ConditionOnlyRecipe { + name: "is_ch_match".to_string(), + original_var: "ch".to_string(), + whitespace_chars: vec!["b".to_string()], + }; + + let mut body_local_env = LoopBodyLocalEnv::new(); + body_local_env.insert("ch".to_string(), ValueId(100)); // ch = ValueId(100) + + let mut condition_env = ConditionEnv::new(); + let mut value_counter = 200u32; + let mut alloc_value = || { + let id = ValueId(value_counter); + value_counter += 1; + id + }; + let mut instructions = Vec::new(); + + // Execute + let result = ConditionOnlyEmitter::emit_condition_only_recalc( + &recipe, + &body_local_env, + &mut condition_env, + &mut alloc_value, + &mut instructions, + ); + + // Verify + assert!(result.is_ok(), "Should succeed"); + let condition_value = result.unwrap(); + + // Should generate: Const("b"), Compare(ch == "b") + assert_eq!(instructions.len(), 2, "Should generate Const + Compare"); + + // ConditionEnv should have is_ch_match registered + assert_eq!( + condition_env.get("is_ch_match"), + Some(condition_value), + "is_ch_match should be registered in ConditionEnv" + ); + } + + #[test] + fn test_emit_condition_only_recalc_multiple_chars() { + let recipe = ConditionOnlyRecipe { + name: "is_ws".to_string(), + original_var: "ch".to_string(), + whitespace_chars: vec![" ".to_string(), "\t".to_string(), "\n".to_string()], + }; + + let mut body_local_env = LoopBodyLocalEnv::new(); + body_local_env.insert("ch".to_string(), ValueId(100)); + + let mut condition_env = ConditionEnv::new(); + let mut value_counter = 200u32; + let mut alloc_value = || { + let id = ValueId(value_counter); + value_counter += 1; + id + }; + let mut instructions = Vec::new(); + + let result = ConditionOnlyEmitter::emit_condition_only_recalc( + &recipe, + &body_local_env, + &mut condition_env, + &mut alloc_value, + &mut instructions, + ); + + assert!(result.is_ok()); + + // Should generate: 3 * (Const + Compare) + 2 * Or = 8 instructions + // Const(" "), Compare, Const("\t"), Compare, Or, Const("\n"), Compare, Or + assert_eq!(instructions.len(), 8, "Should generate 3 comparisons + 2 ORs"); + } + + #[test] + fn test_missing_body_local_variable() { + let recipe = ConditionOnlyRecipe { + name: "is_ch_match".to_string(), + original_var: "ch".to_string(), + whitespace_chars: vec!["b".to_string()], + }; + + let body_local_env = LoopBodyLocalEnv::new(); // Empty - no "ch" + let mut condition_env = ConditionEnv::new(); + let mut value_counter = 200u32; + let mut alloc_value = || { + let id = ValueId(value_counter); + value_counter += 1; + id + }; + let mut instructions = Vec::new(); + + let result = ConditionOnlyEmitter::emit_condition_only_recalc( + &recipe, + &body_local_env, + &mut condition_env, + &mut alloc_value, + &mut instructions, + ); + + assert!(result.is_err(), "Should fail when body-local variable missing"); + assert!( + result.unwrap_err().contains("not found in LoopBodyLocalEnv"), + "Error should mention missing variable" + ); + } +} diff --git a/src/mir/join_ir/lowering/loop_with_break_minimal.rs b/src/mir/join_ir/lowering/loop_with_break_minimal.rs index d8296a33..1d86b576 100644 --- a/src/mir/join_ir/lowering/loop_with_break_minimal.rs +++ b/src/mir/join_ir/lowering/loop_with_break_minimal.rs @@ -105,6 +105,8 @@ pub(crate) struct LoopWithBreakLoweringInputs<'a> { pub allowed_body_locals_for_conditions: Option<&'a [String]>, pub join_value_space: &'a mut JoinValueSpace, pub skeleton: Option<&'a LoopSkeleton>, + /// Phase 93 P0: ConditionOnly recipe for derived slot recalculation + pub condition_only_recipe: Option<&'a crate::mir::join_ir::lowering::common::condition_only_emitter::ConditionOnlyRecipe>, } /// Lower Pattern 2 (Loop with Conditional Break) to JoinIR @@ -181,6 +183,7 @@ pub(crate) fn lower_loop_with_break_minimal( allowed_body_locals_for_conditions, join_value_space, skeleton, + condition_only_recipe, } = inputs; let mut body_local_env = body_local_env; @@ -372,8 +375,13 @@ pub(crate) fn lower_loop_with_break_minimal( let mut loop_step_func = JoinFunction::new(loop_step_id, "loop_step".to_string(), loop_params); // Decide evaluation order (header/body-init/break/updates/tail) up-front. - let schedule_ctx = - Pattern2ScheduleContext::from_env(body_local_env.as_ref().map(|env| &**env), carrier_info); + // Phase 93 P0: Pass condition_only_recipe existence to schedule context. + // When recipe exists, body-init must happen before break check. + let schedule_ctx = Pattern2ScheduleContext::from_env( + body_local_env.as_ref().map(|env| &**env), + carrier_info, + condition_only_recipe.is_some(), + ); let schedule = build_pattern2_schedule(&schedule_ctx); // Collect fragments per step; append them according to the schedule below. @@ -438,6 +446,39 @@ pub(crate) fn lower_loop_with_break_minimal( body_env.len() ) }); + + // Phase 93 P0: Drop init_lowerer to release borrows before ConditionOnly emission + drop(init_lowerer); + + // Phase 93 P0: Emit ConditionOnly variable recalculation after body-local init + // Note: We emit instructions into body_init_block here, but we cannot update env + // since it's immutable. This is OK because the recalculated value will be available + // through body_init_block instructions, and the break condition will reference it + // through the body_local_env lookup mechanism (see dual_value_rewriter). + if let Some(recipe) = condition_only_recipe { + use crate::mir::join_ir::lowering::common::condition_only_emitter::ConditionOnlyEmitter; + // Create a temporary env for the emitter to insert into + // The actual value lookup will happen through body_local_env in break condition lowering + let mut temp_env = env.clone(); + + let condition_value = ConditionOnlyEmitter::emit_condition_only_recalc( + recipe, + body_env, + &mut temp_env, + &mut alloc_value, + &mut body_init_block, + )?; + + // Phase 93 P0: Register the derived value in body_local_env so break condition can find it + body_env.insert(recipe.name.clone(), condition_value); + + dev_log.log_if_enabled(|| { + format!( + "Phase 93 P0: Recalculated ConditionOnly variable '{}' → {:?} (registered in body_local_env)", + recipe.name, condition_value + ) + }); + } } // ------------------------------------------------------------------ diff --git a/src/mir/join_ir/lowering/loop_with_break_minimal/tests.rs b/src/mir/join_ir/lowering/loop_with_break_minimal/tests.rs index cc4e9d8f..c004e2eb 100644 --- a/src/mir/join_ir/lowering/loop_with_break_minimal/tests.rs +++ b/src/mir/join_ir/lowering/loop_with_break_minimal/tests.rs @@ -146,6 +146,7 @@ fn test_pattern2_header_condition_via_exprlowerer() { allowed_body_locals_for_conditions: None, join_value_space: &mut join_value_space, skeleton: None, // Phase 92 P0-3: skeleton=None for backward compatibility + condition_only_recipe: None, // Phase 93 P0: None for normal loops }); assert!(result.is_ok(), "ExprLowerer header path should succeed"); diff --git a/src/mir/join_ir/lowering/step_schedule.rs b/src/mir/join_ir/lowering/step_schedule.rs index d59be342..0089733d 100644 --- a/src/mir/join_ir/lowering/step_schedule.rs +++ b/src/mir/join_ir/lowering/step_schedule.rs @@ -79,11 +79,24 @@ pub(crate) struct Pattern2ScheduleContext { } impl Pattern2ScheduleContext { + /// Build schedule context from environment. + /// + /// # Phase 93 P0: has_condition_only_recipe parameter + /// + /// When a ConditionOnly recipe exists, body-local init MUST happen before break check + /// even if body_local_env is currently empty. This is because ConditionOnly variables + /// (e.g., `is_ch_match`) are recalculated in body_init_block, and the break condition + /// depends on them. pub(crate) fn from_env( body_local_env: Option<&LoopBodyLocalEnv>, carrier_info: &CarrierInfo, + has_condition_only_recipe: bool, ) -> Self { - let has_body_local_init = body_local_env.map(|env| !env.is_empty()).unwrap_or(false); + // Phase 93 P0: body_local_init is true if: + // 1. body_local_env has entries, OR + // 2. condition_only_recipe exists (will be emitted to body_init_block) + let has_body_local_init = body_local_env.map(|env| !env.is_empty()).unwrap_or(false) + || has_condition_only_recipe; let has_loop_local_carrier = carrier_info .carriers .iter() @@ -206,7 +219,7 @@ mod tests { #[test] fn default_schedule_break_before_body_init() { - let ctx = Pattern2ScheduleContext::from_env(None, &carrier_info(vec![])); + let ctx = Pattern2ScheduleContext::from_env(None, &carrier_info(vec![]), false); let schedule = build_pattern2_schedule(&ctx); assert_eq!( schedule.steps(), @@ -227,7 +240,7 @@ mod tests { body_env.insert("tmp".to_string(), ValueId(5)); let ctx = - Pattern2ScheduleContext::from_env(Some(&body_env), &carrier_info(vec![carrier(false)])); + Pattern2ScheduleContext::from_env(Some(&body_env), &carrier_info(vec![carrier(false)]), false); let schedule = build_pattern2_schedule(&ctx); assert_eq!( schedule.steps(), @@ -244,7 +257,26 @@ mod tests { #[test] fn loop_local_carrier_triggers_body_first() { - let ctx = Pattern2ScheduleContext::from_env(None, &carrier_info(vec![carrier(true)])); + let ctx = Pattern2ScheduleContext::from_env(None, &carrier_info(vec![carrier(true)]), false); + let schedule = build_pattern2_schedule(&ctx); + assert_eq!( + schedule.steps(), + &[ + Pattern2StepKind::HeaderCond, + Pattern2StepKind::BodyInit, + Pattern2StepKind::BreakCheck, + Pattern2StepKind::Updates, + Pattern2StepKind::Tail + ] + ); + assert_eq!(schedule.reason(), "body-local break dependency"); + } + + /// Phase 93 P0: ConditionOnly recipe triggers body-init before break + #[test] + fn condition_only_recipe_triggers_body_first() { + // Empty body_local_env but has condition_only_recipe + let ctx = Pattern2ScheduleContext::from_env(None, &carrier_info(vec![]), true); let schedule = build_pattern2_schedule(&ctx); assert_eq!( schedule.steps(), diff --git a/src/mir/join_ir/normalized/fixtures.rs b/src/mir/join_ir/normalized/fixtures.rs index 59a167aa..6adc76fb 100644 --- a/src/mir/join_ir/normalized/fixtures.rs +++ b/src/mir/join_ir/normalized/fixtures.rs @@ -135,6 +135,7 @@ pub fn build_pattern2_minimal_structured() -> JoinModule { allowed_body_locals_for_conditions: None, join_value_space: &mut join_value_space, skeleton: None, // Phase 92 P0-3: skeleton=None for backward compatibility + condition_only_recipe: None, // Phase 93 P0: None for normal loops }, ) .expect("pattern2 minimal lowering should succeed");