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.
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) {

View File

@ -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)))
}
}