refactor(mir): Phase 93 リファクタリング - 箱化モジュール化

## 概要
Phase 93 P0実装後のコード整理。スケジュール決定ロジックとbreak semanticsを
明確化し、デバッグログを統一。

## 変更内容

### 1. スケジュール決定ロジックの関数化 (step_schedule.rs)
- `ScheduleDecision`構造体追加(判定結果+理由+デバッグコンテキスト)
- `decide_pattern2_schedule()` - スケジュール決定のSSOT
- `build_pattern2_schedule_from_decision()` - 新しい決定ベースAPI
- 判定理由が4種類で明確化(ConditionOnly → body-local → loop-local → default)
- 後方互換性維持(`Pattern2ScheduleContext`はwrapperに)

### 2. ConditionOnlyRecipe強化 (condition_only_emitter.rs)
- `BreakSemantics` enum追加(WhenMatch vs WhenNotMatch)
- `generate_break_condition()` - semanticsに基づくAST生成
- `from_trim_helper_condition_only()` - factory method追加
- break semanticsがrecipeに明示的に含まれる

### 3. trim_loop_lowering.rs簡素化
- `generate_condition_only_break_condition()`削除(DRY原則)
- `recipe.generate_break_condition()`で統一
- break条件生成ロジックが1箇所に集約

### 4. デバッグログ統一
- `[phase93/schedule]` - スケジュール決定
- `[phase93/condition-only]` - ConditionOnlyレシピ作成
- `[phase93/break-cond]` - break条件生成
- 既存の`joinir_dev_enabled()`使用(新規env var不要)

## テスト結果
- step_schedule: 10 tests PASS
- condition_only_emitter: 4 tests PASS
- 後方互換性維持

## 統計
- 3ファイル変更
- +249行 / -57行 = +192 net

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
nyash-codex
2025-12-16 23:43:03 +09:00
parent 93e62b1433
commit c213ecc3c0
3 changed files with 249 additions and 57 deletions

View File

@ -369,28 +369,34 @@ impl TrimLoopLowerer {
); );
// Step 7: Generate break condition based on pattern type // Step 7: Generate break condition based on pattern type
// Phase 93 P0: ConditionOnly uses non-negated condition (break when is_ch_match is TRUE) // Phase 93 Refactoring: Use recipe.generate_break_condition() for unified logic
// Normal Trim uses negated condition (break when !is_ch_match, i.e., ch is NOT whitespace) let trim_break_condition = if let Some(ref recipe) = condition_only_recipe {
let trim_break_condition = if condition_only_recipe.is_some() { // Use recipe's break semantics (WhenMatch or WhenNotMatch)
// 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( trace.emit_if(
"trim", "trim",
"cond", "break-cond",
&format!( &format!(
"Break condition: {} (ConditionOnly={})", "Generated break condition from recipe: {} (semantics: {:?})",
trim_helper.carrier_name, trim_helper.carrier_name,
condition_only_recipe.is_some() recipe.break_semantics
), ),
verbose, verbose,
); );
recipe.generate_break_condition()
} else {
// Normal Trim: "break when NOT match" semantics
// Generate: !is_ch_match (TRUE when we should break)
trace.emit_if(
"trim",
"break-cond",
&format!(
"Generated normal trim break condition: !{}",
trim_helper.carrier_name
),
verbose,
);
Self::generate_trim_break_condition(trim_helper)
};
// Step 8: Return result with all updates // Step 8: Return result with all updates
Ok(Some(TrimLoweringResult { Ok(Some(TrimLoweringResult {
@ -533,27 +539,11 @@ 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)
/// Phase 93 Refactoring: Use explicit factory methods for recipe creation
/// ///
/// Creates bindings for: /// Creates bindings for:
/// 1. Carrier variable (e.g., "is_ch_match") /// 1. Carrier variable (e.g., "is_ch_match")
@ -569,15 +559,16 @@ impl TrimLoopLowerer {
let verbose = crate::config::env::joinir_dev_enabled() || trace.is_joinir_enabled(); let verbose = crate::config::env::joinir_dev_enabled() || trace.is_joinir_enabled();
// Phase 93 P0: Do NOT add is_ch_match to ConditionBinding // Phase 93 P0: Do NOT add is_ch_match to ConditionBinding
// Instead, create a ConditionOnlyRecipe for recalculation every iteration // Phase 93 Refactoring: Use explicit factory method for ConditionOnly pattern
let recipe = ConditionOnlyRecipe::from_trim_helper(trim_helper); let recipe = ConditionOnlyRecipe::from_trim_helper_condition_only(trim_helper);
trace.emit_if( trace.emit_if(
"trim", "trim",
"condition-only", "condition-only",
&format!( &format!(
"Phase 93 P0: Created ConditionOnlyRecipe for '{}' (will be recalculated each iteration, not carried via ConditionBinding)", "[phase93/condition-only] Created ConditionOnlyRecipe for '{}' (semantics: {:?}, will be recalculated each iteration)",
trim_helper.carrier_name trim_helper.carrier_name,
recipe.break_semantics
), ),
verbose, verbose,
); );

View File

@ -36,9 +36,21 @@ use crate::mir::join_ir::JoinInst;
use crate::mir::loop_pattern_detection::trim_loop_helper::TrimLoopHelper; use crate::mir::loop_pattern_detection::trim_loop_helper::TrimLoopHelper;
use crate::mir::ValueId; use crate::mir::ValueId;
/// Break semantics for ConditionOnly patterns
///
/// Phase 93 Refactoring: Explicit break condition semantics
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BreakSemantics {
/// Break when condition is TRUE (e.g., find-first: break on ch == "b")
WhenMatch,
/// Break when condition is FALSE (e.g., trim: break on ch != whitespace)
WhenNotMatch,
}
/// ConditionOnly変数の再計算レシピ /// ConditionOnly変数の再計算レシピ
/// ///
/// Phase 93 P0: Trim patternの`is_ch_match`など、毎イテレーション再計算される変数 /// Phase 93 P0: Trim patternの`is_ch_match`など、毎イテレーション再計算される変数
/// Phase 93 Refactoring: Break semantics明確化
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct ConditionOnlyRecipe { pub struct ConditionOnlyRecipe {
/// 変数名(例: "is_ch_match" /// 変数名(例: "is_ch_match"
@ -47,15 +59,68 @@ pub struct ConditionOnlyRecipe {
pub original_var: String, pub original_var: String,
/// Trim pattern用のホワイトスペース文字リスト /// Trim pattern用のホワイトスペース文字リスト
pub whitespace_chars: Vec<String>, pub whitespace_chars: Vec<String>,
/// Break semantics (WhenMatch or WhenNotMatch)
pub break_semantics: BreakSemantics,
} }
impl ConditionOnlyRecipe { impl ConditionOnlyRecipe {
/// Trim patternからレシピを作成 /// Trim patternからレシピを作成ConditionOnly pattern用
pub fn from_trim_helper(trim_helper: &TrimLoopHelper) -> Self { ///
/// Phase 93 P0: ConditionOnly patternWhenMatch semantics
/// 例: find-first patternch == "b"のときにbreak
pub fn from_trim_helper_condition_only(trim_helper: &TrimLoopHelper) -> Self {
Self { Self {
name: trim_helper.carrier_name.clone(), name: trim_helper.carrier_name.clone(),
original_var: trim_helper.original_var.clone(), original_var: trim_helper.original_var.clone(),
whitespace_chars: trim_helper.whitespace_chars.clone(), whitespace_chars: trim_helper.whitespace_chars.clone(),
break_semantics: BreakSemantics::WhenMatch,
}
}
/// Trim patternからレシピを作成Normal Trim pattern用
///
/// Phase 93 Refactoring: Normal Trim patternWhenNotMatch semantics
/// 例: str.trim()ch != whitespaceのときにbreak
pub fn from_trim_helper_normal_trim(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(),
break_semantics: BreakSemantics::WhenNotMatch,
}
}
/// Trim patternからレシピを作成後方互換性
///
/// Phase 93 Refactoring: WhenMatchをデフォルトとして使用
#[allow(dead_code)]
pub fn from_trim_helper(trim_helper: &TrimLoopHelper) -> Self {
Self::from_trim_helper_condition_only(trim_helper)
}
/// Break条件AST生成semanticsに基づく
///
/// Phase 93 Refactoring: Break semanticsに基づいて適切な条件を生成
///
/// # Returns
///
/// - `WhenMatch`: carrier変数そのままTRUE時にbreak
/// - `WhenNotMatch`: !carrierFALSE時にbreak
pub fn generate_break_condition(&self) -> ASTNode {
use crate::ast::{Span, UnaryOperator};
let carrier_var = ASTNode::Variable {
name: self.name.clone(),
span: Span::unknown(),
};
match self.break_semantics {
BreakSemantics::WhenMatch => carrier_var,
BreakSemantics::WhenNotMatch => ASTNode::UnaryOp {
operator: UnaryOperator::Not,
operand: Box::new(carrier_var),
span: Span::unknown(),
},
} }
} }
} }
@ -201,6 +266,7 @@ mod tests {
name: "is_ch_match".to_string(), name: "is_ch_match".to_string(),
original_var: "ch".to_string(), original_var: "ch".to_string(),
whitespace_chars: vec!["b".to_string()], whitespace_chars: vec!["b".to_string()],
break_semantics: BreakSemantics::WhenMatch,
}; };
let mut body_local_env = LoopBodyLocalEnv::new(); let mut body_local_env = LoopBodyLocalEnv::new();
@ -245,6 +311,7 @@ mod tests {
name: "is_ws".to_string(), name: "is_ws".to_string(),
original_var: "ch".to_string(), original_var: "ch".to_string(),
whitespace_chars: vec![" ".to_string(), "\t".to_string(), "\n".to_string()], whitespace_chars: vec![" ".to_string(), "\t".to_string(), "\n".to_string()],
break_semantics: BreakSemantics::WhenMatch,
}; };
let mut body_local_env = LoopBodyLocalEnv::new(); let mut body_local_env = LoopBodyLocalEnv::new();
@ -280,6 +347,7 @@ mod tests {
name: "is_ch_match".to_string(), name: "is_ch_match".to_string(),
original_var: "ch".to_string(), original_var: "ch".to_string(),
whitespace_chars: vec!["b".to_string()], whitespace_chars: vec!["b".to_string()],
break_semantics: BreakSemantics::WhenMatch,
}; };
let body_local_env = LoopBodyLocalEnv::new(); // Empty - no "ch" let body_local_env = LoopBodyLocalEnv::new(); // Empty - no "ch"

View File

@ -71,7 +71,84 @@ impl Pattern2StepSchedule {
} }
} }
/// Schedule decision result with reasoning
///
/// Phase 93 Refactoring: Unified schedule decision with explicit reasons
#[derive(Debug, Clone)]
pub(crate) struct ScheduleDecision {
/// Whether body-init should come before break check
pub body_init_first: bool,
/// Human-readable reason for this decision
pub reason: &'static str,
/// Debug context for logging
pub debug_ctx: ScheduleDebugContext,
}
/// Debug context for schedule decisions
#[derive(Debug, Clone)]
pub(crate) struct ScheduleDebugContext {
pub has_body_local_init: bool,
pub has_loop_local_carrier: bool,
pub has_condition_only_recipe: bool,
}
/// Decide Pattern2 schedule based on loop characteristics
///
/// Phase 93 Refactoring: Single source of truth for schedule decisions
///
/// # Decision Logic
///
/// Body-init comes BEFORE break check if any of these conditions are true:
/// 1. ConditionOnly recipe exists (derived slots need recalculation)
/// 2. Body-local variables exist (break condition depends on them)
/// 3. Loop-local carriers exist (need initialization before use)
///
/// # Arguments
///
/// * `body_local_env` - Body-local variable environment
/// * `carrier_info` - Carrier information (for loop-local detection)
/// * `has_condition_only_recipe` - Whether ConditionOnly derived slots exist
///
/// # Returns
///
/// `ScheduleDecision` with decision, reason, and debug context
pub(crate) fn decide_pattern2_schedule(
body_local_env: Option<&LoopBodyLocalEnv>,
carrier_info: &CarrierInfo,
has_condition_only_recipe: bool,
) -> ScheduleDecision {
let has_body_local_init = body_local_env.map(|env| !env.is_empty()).unwrap_or(false);
let has_loop_local_carrier = carrier_info
.carriers
.iter()
.any(|c| matches!(c.init, CarrierInit::LoopLocalZero));
let body_init_first = has_condition_only_recipe || has_body_local_init || has_loop_local_carrier;
let reason = if has_condition_only_recipe {
"ConditionOnly requires body-init before break"
} else if has_body_local_init {
"body-local variables require init before break"
} else if has_loop_local_carrier {
"loop-local carrier requires init before break"
} else {
"default schedule"
};
ScheduleDecision {
body_init_first,
reason,
debug_ctx: ScheduleDebugContext {
has_body_local_init,
has_loop_local_carrier,
has_condition_only_recipe,
},
}
}
/// Minimal context for deciding the step order. /// Minimal context for deciding the step order.
///
/// Phase 93 Refactoring: Kept for backward compatibility, delegates to `decide_pattern2_schedule`
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub(crate) struct Pattern2ScheduleContext { pub(crate) struct Pattern2ScheduleContext {
pub(crate) has_body_local_init: bool, pub(crate) has_body_local_init: bool,
@ -81,30 +158,23 @@ pub(crate) struct Pattern2ScheduleContext {
impl Pattern2ScheduleContext { impl Pattern2ScheduleContext {
/// Build schedule context from environment. /// Build schedule context from environment.
/// ///
/// # Phase 93 P0: has_condition_only_recipe parameter /// # Phase 93 Refactoring: Backward compatibility wrapper
/// ///
/// When a ConditionOnly recipe exists, body-local init MUST happen before break check /// Delegates to `decide_pattern2_schedule()` for actual decision making.
/// even if body_local_env is currently empty. This is because ConditionOnly variables /// Note: When `has_condition_only_recipe` is true, we set `has_body_local_init` to true
/// (e.g., `is_ch_match`) are recalculated in body_init_block, and the break condition /// to ensure the correct schedule is generated.
/// 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, has_condition_only_recipe: bool,
) -> Self { ) -> Self {
// Phase 93 P0: body_local_init is true if: let decision = decide_pattern2_schedule(body_local_env, carrier_info, has_condition_only_recipe);
// 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()
.any(|c| matches!(c.init, CarrierInit::LoopLocalZero));
Self { Self {
has_body_local_init, // Phase 93 Refactoring: Include condition_only_recipe in has_body_local_init
has_loop_local_carrier, // to maintain backward compatibility with requires_body_init_before_break()
has_body_local_init: decision.debug_ctx.has_body_local_init
|| decision.debug_ctx.has_condition_only_recipe,
has_loop_local_carrier: decision.debug_ctx.has_loop_local_carrier,
} }
} }
@ -115,6 +185,46 @@ impl Pattern2ScheduleContext {
/// Build a schedule for Pattern 2 lowering. /// Build a schedule for Pattern 2 lowering.
/// ///
/// Phase 93 Refactoring: Now accepts `ScheduleDecision` for explicit reasoning
///
/// - Default P2: header → break → body-init → updates → tail
/// - Body-local break dependency (DigitPos/_atoi style):
/// header → body-init → break → updates → tail
pub(crate) fn build_pattern2_schedule_from_decision(
decision: &ScheduleDecision,
) -> Pattern2StepSchedule {
let schedule = if decision.body_init_first {
Pattern2StepSchedule {
steps: vec![
Pattern2StepKind::HeaderCond,
Pattern2StepKind::BodyInit,
Pattern2StepKind::BreakCheck,
Pattern2StepKind::Updates,
Pattern2StepKind::Tail,
],
reason: decision.reason,
}
} else {
Pattern2StepSchedule {
steps: vec![
Pattern2StepKind::HeaderCond,
Pattern2StepKind::BreakCheck,
Pattern2StepKind::BodyInit,
Pattern2StepKind::Updates,
Pattern2StepKind::Tail,
],
reason: decision.reason,
}
};
log_schedule_from_decision(decision, &schedule);
schedule
}
/// Build a schedule for Pattern 2 lowering (legacy wrapper).
///
/// Phase 93 Refactoring: Kept for backward compatibility
///
/// - Default P2: header → break → body-init → updates → tail /// - Default P2: header → break → body-init → updates → tail
/// - Body-local break dependency (DigitPos/_atoi style): /// - Body-local break dependency (DigitPos/_atoi style):
/// header → body-init → break → updates → tail /// header → body-init → break → updates → tail
@ -147,6 +257,28 @@ pub(crate) fn build_pattern2_schedule(ctx: &Pattern2ScheduleContext) -> Pattern2
schedule schedule
} }
fn log_schedule_from_decision(decision: &ScheduleDecision, schedule: &Pattern2StepSchedule) {
if !(env::joinir_dev_enabled() || joinir_test_debug_enabled()) {
return;
}
let steps_desc = schedule
.steps()
.iter()
.map(Pattern2StepKind::as_str)
.collect::<Vec<_>>()
.join(" -> ");
eprintln!(
"[phase93/schedule] steps={} reason={} ctx={{body_local_init={}, loop_local_carrier={}, condition_only={}}}",
steps_desc,
schedule.reason(),
decision.debug_ctx.has_body_local_init,
decision.debug_ctx.has_loop_local_carrier,
decision.debug_ctx.has_condition_only_recipe
);
}
fn log_schedule(ctx: &Pattern2ScheduleContext, schedule: &Pattern2StepSchedule) { fn log_schedule(ctx: &Pattern2ScheduleContext, schedule: &Pattern2StepSchedule) {
if !(env::joinir_dev_enabled() || joinir_test_debug_enabled()) { if !(env::joinir_dev_enabled() || joinir_test_debug_enabled()) {
return; return;
@ -288,6 +420,7 @@ mod tests {
Pattern2StepKind::Tail Pattern2StepKind::Tail
] ]
); );
// Phase 93 Refactoring: Reason is now preserved from backward compat wrapper
assert_eq!(schedule.reason(), "body-local break dependency"); assert_eq!(schedule.reason(), "body-local break dependency");
} }