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:
@ -54,6 +54,8 @@ struct Pattern2Inputs {
|
||||
/// Phase 92 P3: Diagnostics / debug metadata for the allow-listed variable.
|
||||
read_only_body_local_slot: Option<ReadOnlyBodyLocalSlot>,
|
||||
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(
|
||||
@ -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,19 +538,28 @@ 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!(
|
||||
"Extended condition_bindings with {} Trim bindings",
|
||||
"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()
|
||||
@ -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) {
|
||||
|
||||
@ -74,6 +74,11 @@ pub(crate) struct TrimLoweringResult {
|
||||
///
|
||||
/// Pattern2/4 will extend their condition_bindings with these
|
||||
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 {
|
||||
@ -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<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::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)))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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};
|
||||
|
||||
309
src/mir/join_ir/lowering/common/condition_only_emitter.rs
Normal file
309
src/mir/join_ir/lowering/common/condition_only_emitter.rs
Normal file
@ -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<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"
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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");
|
||||
|
||||
Reference in New Issue
Block a user