feat(mir): Phase 93 P0 - ConditionOnly Derived Slot実装

## 概要
body-local変数を参照するbreak条件が毎イテレーション正しく再計算される
ConditionOnlyパターンを実装。

## 問題
- `is_ch_match`がConditionBindingで運ばれると初回計算値が固定
- loop header PHIで更新されず、毎周回同じ値がコピーされる
- 結果: `if ch == "b" { break }` が正しく動作しない

## 解決策 (B: ConditionOnly)
1. ConditionOnlyRecipe作成 - Derived slot再計算レシピ
2. setup_condition_env_bindings()でConditionBinding登録停止
3. Pattern2スケジュールでbody-init → break順序保証
4. break条件: ConditionOnlyでは非反転版を使用

## 変更ファイル
- condition_only_emitter.rs (NEW): Derived slot再計算ロジック
- step_schedule.rs: from_env()にhas_condition_only_recipe追加
- loop_with_break_minimal.rs: スケジュール決定でrecipe考慮
- trim_loop_lowering.rs: ConditionOnly用break条件生成追加

## テスト
- step_schedule: 6 tests PASS (新規1: condition_only_recipe_triggers_body_first)
- condition_only_emitter: 3 tests PASS
- Phase 92 baseline: 2 cases PASS
- E2E: /tmp/test_body_local_simple.hako → 出力 "1" ✓

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
nyash-codex
2025-12-16 23:24:11 +09:00
parent d2972c1437
commit 04fdac42f2
8 changed files with 498 additions and 83 deletions

View File

@ -54,6 +54,8 @@ struct Pattern2Inputs {
/// Phase 92 P3: Diagnostics / debug metadata for the allow-listed variable. /// Phase 92 P3: Diagnostics / debug metadata for the allow-listed variable.
read_only_body_local_slot: Option<ReadOnlyBodyLocalSlot>, read_only_body_local_slot: Option<ReadOnlyBodyLocalSlot>,
break_condition_node: ASTNode, break_condition_node: ASTNode,
/// Phase 93 P0: ConditionOnly recipe for derived slot recalculation
condition_only_recipe: Option<crate::mir::join_ir::lowering::common::condition_only_emitter::ConditionOnlyRecipe>,
} }
fn prepare_pattern2_inputs( fn prepare_pattern2_inputs(
@ -251,6 +253,7 @@ fn prepare_pattern2_inputs(
allowed_body_locals_for_conditions: Vec::new(), allowed_body_locals_for_conditions: Vec::new(),
read_only_body_local_slot: None, read_only_body_local_slot: None,
break_condition_node, 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"); log.log("trim", "TrimLoopLowerer processed Trim pattern successfully");
inputs.carrier_info = trim_result.carrier_info; inputs.carrier_info = trim_result.carrier_info;
inputs
.condition_bindings // Phase 93 P0: Save condition_only_recipe instead of adding to condition_bindings
.extend(trim_result.condition_bindings.iter().cloned()); inputs.condition_only_recipe = trim_result.condition_only_recipe;
for binding in &trim_result.condition_bindings {
inputs.env.insert(binding.name.clone(), binding.join_value); // 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( log.log(
"trim", "trim",
format!( format!(
"Extended condition_bindings with {} Trim bindings", "Phase 93 P0: condition_only_recipe={}",
trim_result.condition_bindings.len() if inputs.condition_only_recipe.is_some() { "Some" } else { "None" }
), ),
); );
trim_result.condition trim_result.condition
@ -881,6 +893,7 @@ impl MirBuilder {
}, },
join_value_space, join_value_space,
skeleton, 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) { let (join_module, fragment_meta) = match lower_loop_with_break_minimal(lowering_inputs) {

View File

@ -74,6 +74,11 @@ pub(crate) struct TrimLoweringResult {
/// ///
/// Pattern2/4 will extend their condition_bindings with these /// Pattern2/4 will extend their condition_bindings with these
pub condition_bindings: Vec<ConditionBinding>, pub condition_bindings: Vec<ConditionBinding>,
/// 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<crate::mir::join_ir::lowering::common::condition_only_emitter::ConditionOnlyRecipe>,
} }
impl TrimLoopLowerer { impl TrimLoopLowerer {
@ -269,22 +274,34 @@ impl TrimLoopLowerer {
verbose, 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")] #[cfg(feature = "normalized_dev")]
// Phase 136 Step 4/7: Use binding_ctx for binding_map reference {
let promoted_carrier = use crate::mir::loop_pattern_detection::loop_body_carrier_promoter::PromotedBindingRecorder;
trim_info.to_carrier_info(Some(builder.binding_ctx.binding_map())); let recorder = PromotedBindingRecorder::new(Some(builder.binding_ctx.binding_map()));
#[cfg(not(feature = "normalized_dev"))] if let Err(e) = recorder.record_promotion(&mut carrier_info, &trim_info.var_name, &trim_info.carrier_name) {
let promoted_carrier = trim_info.to_carrier_info(); eprintln!("[TrimLoopLowerer] Failed to record promoted binding: {}", e);
carrier_info.merge_from(&promoted_carrier); }
}
trace.emit_if( trace.emit_if(
"trim", "trim",
"promote", "promote",
&format!( &format!(
"Merged carrier '{}' into CarrierInfo (total carriers: {})", "Promoted body-local '{}' to condition-only variable '{}' (not a LoopState carrier)",
trim_info.carrier_name, trim_info.var_name,
carrier_info.carrier_count() trim_info.carrier_name
), ),
verbose, verbose,
); );
@ -336,29 +353,41 @@ impl TrimLoopLowerer {
verbose, verbose,
); );
// Step 6: Generate Trim break condition // Step 6: Setup ConditionEnv bindings FIRST to determine if ConditionOnly
let trim_break_condition = Self::generate_trim_break_condition(trim_helper); let (condition_bindings, condition_only_recipe) =
trace.emit_if(
"trim",
"cond",
&format!(
"Replaced break condition with !{}",
trim_helper.carrier_name
),
verbose,
);
// Step 7: Setup ConditionEnv bindings
let condition_bindings =
Self::setup_condition_env_bindings(builder, trim_helper, alloc_join_value)?; Self::setup_condition_env_bindings(builder, trim_helper, alloc_join_value)?;
trace.emit_if( trace.emit_if(
"trim", "trim",
"cond", "cond",
&format!( &format!(
"Added {} condition bindings", "Phase 93 P0: condition_bindings={}, condition_only_recipe={}",
condition_bindings.len() 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, verbose,
); );
@ -368,6 +397,7 @@ impl TrimLoopLowerer {
condition: trim_break_condition, condition: trim_break_condition,
carrier_info: carrier_info.clone(), carrier_info: carrier_info.clone(),
condition_bindings, condition_bindings,
condition_only_recipe,
})) }))
} }
PromotionResult::CannotPromote { reason, vars } => { PromotionResult::CannotPromote { reason, vars } => {
@ -490,11 +520,12 @@ impl TrimLoopLowerer {
Ok(()) Ok(())
} }
/// Generate Trim break condition /// Generate Trim break condition (normal Trim pattern)
/// ///
/// Phase 180-3: Extracted from Pattern2 (lines 343-377) /// Phase 180-3: Extracted from Pattern2 (lines 343-377)
/// ///
/// Returns: !is_carrier (negated carrier check) /// Returns: !is_carrier (negated carrier check)
/// Used for "break when NOT match" semantics (e.g., str.trim())
fn generate_trim_break_condition( fn generate_trim_break_condition(
trim_helper: &crate::mir::loop_pattern_detection::trim_loop_helper::TrimLoopHelper, trim_helper: &crate::mir::loop_pattern_detection::trim_loop_helper::TrimLoopHelper,
) -> ASTNode { ) -> ASTNode {
@ -502,6 +533,24 @@ impl TrimLoopLowerer {
TrimPatternLowerer::generate_trim_break_condition(trim_helper) 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 /// Setup ConditionEnv bindings for Trim carrier
/// ///
/// Phase 180-3: Extracted from Pattern2 (lines 345-377) /// Phase 180-3: Extracted from Pattern2 (lines 345-377)
@ -513,60 +562,28 @@ impl TrimLoopLowerer {
builder: &mut MirBuilder, builder: &mut MirBuilder,
trim_helper: &crate::mir::loop_pattern_detection::trim_loop_helper::TrimLoopHelper, trim_helper: &crate::mir::loop_pattern_detection::trim_loop_helper::TrimLoopHelper,
alloc_join_value: &mut dyn FnMut() -> ValueId, alloc_join_value: &mut dyn FnMut() -> ValueId,
) -> Result<Vec<ConditionBinding>, String> { ) -> Result<(Vec<ConditionBinding>, Option<crate::mir::join_ir::lowering::common::condition_only_emitter::ConditionOnlyRecipe>), String> {
use crate::mir::builder::control_flow::joinir::patterns::trim_pattern_lowerer::TrimPatternLowerer; 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 trace = crate::mir::builder::control_flow::joinir::trace::trace();
let verbose = crate::config::env::joinir_dev_enabled() || trace.is_joinir_enabled(); let verbose = crate::config::env::joinir_dev_enabled() || trace.is_joinir_enabled();
let mut bindings = Vec::new(); // Phase 93 P0: Do NOT add is_ch_match to ConditionBinding
// Instead, create a ConditionOnlyRecipe for recalculation every iteration
// Add carrier to ConditionEnv let recipe = ConditionOnlyRecipe::from_trim_helper(trim_helper);
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,
)?;
trace.emit_if( trace.emit_if(
"trim", "trim",
"cond-env", "condition-only",
&format!( &format!(
"Added carrier '{}' to ConditionEnv: HOST {:?} → JoinIR {:?}", "Phase 93 P0: Created ConditionOnlyRecipe for '{}' (will be recalculated each iteration, not carried via ConditionBinding)",
trim_helper.carrier_name, binding.host_value, binding.join_value trim_helper.carrier_name
), ),
verbose, verbose,
); );
bindings.push(binding.clone()); // Return empty bindings - the derived slot will be recalculated, not bound
Ok((Vec::new(), Some(recipe)))
// 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)
} }
} }

View File

@ -6,6 +6,7 @@ pub mod case_a;
pub mod conditional_step_emitter; // Phase 92 P1-1: ConditionalStep emission module 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 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 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::loop_form::LoopForm;
use crate::mir::query::{MirQuery, MirQueryBox}; use crate::mir::query::{MirQuery, MirQueryBox};

View File

@ -0,0 +1,309 @@
//! ConditionOnly Variable Emitter - 毎イテレーション再計算される派生値
//!
//! # Phase 93 P0: ConditionOnlyDerived Slotアーキテクチャ
//!
//! ## 問題
//! Trim patternの`is_ch_match`は:
//! - LoopState carrierPHI対象ではない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<String>,
}
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<JoinInst>,
) -> Result<ValueId, String> {
// 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<JoinInst>,
) -> Result<ValueId, String> {
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"
);
}
}

View File

@ -105,6 +105,8 @@ pub(crate) struct LoopWithBreakLoweringInputs<'a> {
pub allowed_body_locals_for_conditions: Option<&'a [String]>, pub allowed_body_locals_for_conditions: Option<&'a [String]>,
pub join_value_space: &'a mut JoinValueSpace, pub join_value_space: &'a mut JoinValueSpace,
pub skeleton: Option<&'a LoopSkeleton>, 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 /// 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, allowed_body_locals_for_conditions,
join_value_space, join_value_space,
skeleton, skeleton,
condition_only_recipe,
} = inputs; } = inputs;
let mut body_local_env = body_local_env; 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); 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. // Decide evaluation order (header/body-init/break/updates/tail) up-front.
let schedule_ctx = // Phase 93 P0: Pass condition_only_recipe existence to schedule context.
Pattern2ScheduleContext::from_env(body_local_env.as_ref().map(|env| &**env), carrier_info); // 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); let schedule = build_pattern2_schedule(&schedule_ctx);
// Collect fragments per step; append them according to the schedule below. // 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() 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
)
});
}
} }
// ------------------------------------------------------------------ // ------------------------------------------------------------------

View File

@ -146,6 +146,7 @@ fn test_pattern2_header_condition_via_exprlowerer() {
allowed_body_locals_for_conditions: None, allowed_body_locals_for_conditions: None,
join_value_space: &mut join_value_space, join_value_space: &mut join_value_space,
skeleton: None, // Phase 92 P0-3: skeleton=None for backward compatibility 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"); assert!(result.is_ok(), "ExprLowerer header path should succeed");

View File

@ -79,11 +79,24 @@ pub(crate) struct Pattern2ScheduleContext {
} }
impl 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( pub(crate) fn from_env(
body_local_env: Option<&LoopBodyLocalEnv>, body_local_env: Option<&LoopBodyLocalEnv>,
carrier_info: &CarrierInfo, carrier_info: &CarrierInfo,
has_condition_only_recipe: bool,
) -> Self { ) -> 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 let has_loop_local_carrier = carrier_info
.carriers .carriers
.iter() .iter()
@ -206,7 +219,7 @@ mod tests {
#[test] #[test]
fn default_schedule_break_before_body_init() { 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); let schedule = build_pattern2_schedule(&ctx);
assert_eq!( assert_eq!(
schedule.steps(), schedule.steps(),
@ -227,7 +240,7 @@ mod tests {
body_env.insert("tmp".to_string(), ValueId(5)); body_env.insert("tmp".to_string(), ValueId(5));
let ctx = 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); let schedule = build_pattern2_schedule(&ctx);
assert_eq!( assert_eq!(
schedule.steps(), schedule.steps(),
@ -244,7 +257,26 @@ mod tests {
#[test] #[test]
fn loop_local_carrier_triggers_body_first() { 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); let schedule = build_pattern2_schedule(&ctx);
assert_eq!( assert_eq!(
schedule.steps(), schedule.steps(),

View File

@ -135,6 +135,7 @@ pub fn build_pattern2_minimal_structured() -> JoinModule {
allowed_body_locals_for_conditions: None, allowed_body_locals_for_conditions: None,
join_value_space: &mut join_value_space, join_value_space: &mut join_value_space,
skeleton: None, // Phase 92 P0-3: skeleton=None for backward compatibility 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"); .expect("pattern2 minimal lowering should succeed");