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,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) {
|
||||
|
||||
@ -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)))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user