diff --git a/src/mir/phi_core/body_local_phi_builder.rs b/src/mir/phi_core/body_local_phi_builder.rs index 901567bd..df3baf56 100644 --- a/src/mir/phi_core/body_local_phi_builder.rs +++ b/src/mir/phi_core/body_local_phi_builder.rs @@ -5,6 +5,10 @@ //! - BodyLocalInternal変数のスキップ //! - Exit PHI候補のフィルタリング //! +//! Phase 26-F-4: Exit Liveness統合(環境変数制御) +//! - live_at_exit パラメータ追加(デフォルト: 空集合) +//! - `NYASH_EXIT_LIVE_ENABLE=1` で将来のMIRスキャン実装を有効化 +//! //! Box-First理論: BodyLocal変数処理の責任を明確に分離し、テスト可能な箱として提供 use super::local_scope_inspector::LocalScopeInspectorBox; @@ -118,23 +122,32 @@ impl BodyLocalPhiBuilder { /// * `pinned_vars` - Known loop-crossing parameters /// * `carrier_vars` - Known loop-modified variables /// * `exit_preds` - All exit predecessor blocks + /// * `live_at_exit` - Phase 26-F-4: Variables that are actually used after exit /// /// # Returns /// Filtered list of variable names that need exit PHI /// + /// # Phase 26-F-4: OR判定による統合 + /// - class.needs_exit_phi() == true → 既存ロジック(Pinned/Carrier/BodyLocalExit) + /// - OR live_at_exit に含まれる → 新規ロジック(BodyLocalInternal でも救う) + /// /// # Example /// ```ignore + /// // Phase 26-F-4: skip_whitespace scenario(保守的live+全pred定義が条件) /// let all_vars = vec!["s", "idx", "ch", "n"]; /// let pinned = vec!["s", "idx"]; /// let carrier = vec![]; + /// let live_at_exit = BTreeSet::from(["s", "idx", "ch", "n"]); // 保守的近似 /// /// let phi_vars = builder.filter_exit_phi_candidates( /// &all_vars, /// &pinned, /// &carrier, /// &exit_preds, + /// &live_at_exit, // Phase 26-F-4: 追加 /// ); - /// // Result: ["s", "idx", "n"] - "ch" filtered out (BodyLocalInternal) + /// // Result (現行ロジック): ["s", "idx", "n"] + /// // - "ch" は BodyLocalInternal かつ exit_preds 全てで定義されていないため除外される /// ``` pub fn filter_exit_phi_candidates( &self, @@ -142,11 +155,42 @@ impl BodyLocalPhiBuilder { pinned_vars: &[String], carrier_vars: &[String], exit_preds: &[BasicBlockId], + live_at_exit: &std::collections::BTreeSet, // Phase 26-F-4: 追加 ) -> Vec { + // 環境変数ガード:既定は従来挙動(BodyLocalInternal を救済しない) + let enable_live_rescue = + std::env::var("NYASH_EXIT_LIVE_ENABLE").ok().as_deref() == Some("1"); + all_vars .iter() .filter(|var_name| { - self.should_generate_exit_phi(var_name, pinned_vars, carrier_vars, exit_preds) + let class = self.classifier.classify( + var_name, + pinned_vars, + carrier_vars, + &self.inspector, + exit_preds, + ); + + // Phase 26-F-4: OR判定(ただし BodyLocalInternal は「全predで定義される」場合に限定) + // - Pinned/Carrier/BodyLocalExit → 既存ロジック + // - BodyLocalInternal でも live_at_exit に含まれ、かつ全predで定義されるなら exit PHI 候補 + if class.needs_exit_phi() { + return true; + } + + if enable_live_rescue { + if matches!(class, super::loop_var_classifier::LoopVarClass::BodyLocalInternal) + && live_at_exit.contains(*var_name) + && self + .inspector + .is_available_in_all(var_name, exit_preds) + { + return true; + } + } + + false }) .cloned() .collect() @@ -318,11 +362,15 @@ mod tests { let pinned = vec!["s".to_string(), "idx".to_string()]; let carrier: Vec = vec![]; + // Phase 26-F-4: empty live_at_exit(live情報なし) + let live_at_exit = std::collections::BTreeSet::new(); + let phi_vars = builder.filter_exit_phi_candidates( &all_vars, &pinned, &carrier, &[BasicBlockId(2), BasicBlockId(5)], + &live_at_exit, // Phase 26-F-4 ); // Expected: s, idx, n (ch is BodyLocalInternal → filtered out) @@ -355,8 +403,11 @@ mod tests { // Exit preds: [block 2 (early break), block 5 (after ch definition)] let exit_preds = vec![BasicBlockId(2), BasicBlockId(5)]; + // Phase 26-F-4: empty live_at_exit(live情報なし) + let live_at_exit = std::collections::BTreeSet::new(); + let phi_vars = - builder.filter_exit_phi_candidates(&all_vars, &pinned, &carrier, &exit_preds); + builder.filter_exit_phi_candidates(&all_vars, &pinned, &carrier, &exit_preds, &live_at_exit); // Expected: s, idx (ch filtered out!) assert_eq!(phi_vars.len(), 2); @@ -420,8 +471,11 @@ mod tests { let all_vars = vec!["__pin$42$@binop_lhs".to_string(), "s".to_string()]; let pinned = vec!["s".to_string()]; + // Phase 26-F-4: empty live_at_exit(live情報なし) + let live_at_exit = std::collections::BTreeSet::new(); + let phi_vars = - builder.filter_exit_phi_candidates(&all_vars, &pinned, &[], &[BasicBlockId(5)]); + builder.filter_exit_phi_candidates(&all_vars, &pinned, &[], &[BasicBlockId(5)], &live_at_exit); // Only s should remain assert_eq!(phi_vars.len(), 1); @@ -435,7 +489,10 @@ mod tests { let inspector = LocalScopeInspectorBox::new(); let builder = BodyLocalPhiBuilder::new(classifier, inspector); - let phi_vars = builder.filter_exit_phi_candidates(&[], &[], &[], &[]); + // Phase 26-F-4: empty live_at_exit(live情報なし) + let live_at_exit = std::collections::BTreeSet::new(); + + let phi_vars = builder.filter_exit_phi_candidates(&[], &[], &[], &[], &live_at_exit); assert_eq!(phi_vars.len(), 0); } @@ -449,11 +506,15 @@ mod tests { let all_vars = vec!["a".to_string(), "b".to_string(), "c".to_string()]; let pinned = vec!["a".to_string(), "b".to_string(), "c".to_string()]; + // Phase 26-F-4: empty live_at_exit(live情報なし) + let live_at_exit = std::collections::BTreeSet::new(); + let phi_vars = builder.filter_exit_phi_candidates( &all_vars, &pinned, &[], &[BasicBlockId(1), BasicBlockId(2)], + &live_at_exit, ); // All should need exit PHI @@ -469,11 +530,15 @@ mod tests { let all_vars = vec!["i".to_string(), "j".to_string()]; let carrier = vec!["i".to_string(), "j".to_string()]; + // Phase 26-F-4: empty live_at_exit(live情報なし) + let live_at_exit = std::collections::BTreeSet::new(); + let phi_vars = builder.filter_exit_phi_candidates( &all_vars, &[], &carrier, &[BasicBlockId(1), BasicBlockId(2)], + &live_at_exit, ); // All should need exit PHI diff --git a/src/mir/phi_core/exit_phi_builder.rs b/src/mir/phi_core/exit_phi_builder.rs index 8baa1ea6..1e830f8d 100644 --- a/src/mir/phi_core/exit_phi_builder.rs +++ b/src/mir/phi_core/exit_phi_builder.rs @@ -5,12 +5,18 @@ //! - Phantom block除外ロジック //! - BodyLocal変数フィルタリング(BodyLocalPhiBuilder活用) //! +//! Phase 26-F-4: LoopExitLivenessBox統合 +//! - live_at_exit 変数の計算(LoopExitLivenessBox) +//! - BodyLocalInternal変数の救済(OR判定) +//! - 4箱構成による責務分離完成 +//! //! Box-First理論: Exit PHI生成という最も複雑な責任を明確に分離し、テスト可能な箱として提供 use crate::mir::{BasicBlockId, ValueId}; use std::collections::{BTreeMap, BTreeSet}; use super::body_local_phi_builder::BodyLocalPhiBuilder; +use super::loop_exit_liveness::LoopExitLivenessBox; // Phase 26-F-4 use super::loop_snapshot_merge::LoopSnapshotMergeBox; use super::phi_invariants::PhiInvariantsBox; use super::phi_input_collector::PhiInputCollector; @@ -153,11 +159,16 @@ impl ExitPhiBuilder { required_vars.extend(pinned_vars.iter().cloned()); required_vars.extend(carrier_vars.iter().cloned()); + // Phase 26-F-4: LoopExitLivenessBox で live_at_exit を計算 + let liveness_box = LoopExitLivenessBox::new(); + let live_at_exit = liveness_box.compute_live_at_exit(header_vals, exit_snapshots); + let phi_vars = self.body_local_builder.filter_exit_phi_candidates( &required_vars.iter().cloned().collect::>(), pinned_vars, carrier_vars, &exit_preds, + &live_at_exit, // Phase 26-F-4: live_at_exit 追加 ); // Fail-Fast invariant(共通箱経由): diff --git a/src/mir/phi_core/loop_exit_liveness.rs b/src/mir/phi_core/loop_exit_liveness.rs new file mode 100644 index 00000000..5e4b589b --- /dev/null +++ b/src/mir/phi_core/loop_exit_liveness.rs @@ -0,0 +1,228 @@ +//! Loop Exit Liveness Box - Exit後で使われる変数の決定専門Box +//! +//! Phase 26-F-4: Exit Liveness Analysis +//! - Exit後で本当に使われる変数を決定 +//! - Phase 1: 空集合(MIRスキャン実装待ち) +//! - Phase 2+: MIRスキャン実装予定(LoopFormOps拡張後) +//! +//! # 環境変数制御 +//! - `NYASH_EXIT_LIVE_ENABLE=1`: Phase 2+ MIRスキャン実装を有効化(実験用) +//! - デフォルト(未設定): Phase 26-F-3 相当の挙動(安定版) +//! +//! # Box-First理論: 責務の明確な分離 +//! +//! - **LoopVarClassBox**: スコープ分類(Pinned/Carrier/BodyLocal) +//! - **LoopExitLivenessBox**: Exit後の実際の使用(この箱) +//! - **BodyLocalPhiBuilder**: 両者を統合して exit PHI 判定 +//! +//! # Phase 26-F-4: ChatGPT設計による箱離婚 +//! +//! **問題**: LoopVarClassBox が「スコープ分類」と「exit後使用」を混在 +//! +//! **解決**: LoopExitLivenessBox 新設で完全分離 +//! - スコープ分類: LoopVarClassBox(変更なし) +//! - 実際の使用: LoopExitLivenessBox(新箱) +//! - 統合判定: BodyLocalPhiBuilder(OR論理) + +use crate::mir::{BasicBlockId, ValueId}; +use std::collections::{BTreeMap, BTreeSet}; + +/// Loop Exit Liveness Box +/// +/// # Purpose +/// Exit後で本当に使われる変数を決定する専門箱 +/// +/// # Responsibility +/// - live_at_exit 変数集合の計算 +/// - 将来: MIRスキャンによる精密liveness解析 +/// - 現在: 保守的近似(全変数をliveとみなす) +/// +/// # Phase 1 Implementation (Conservative) +/// header_vals + exit_snapshots に現れる全変数を live_at_exit に含める +/// +/// **理由**: LoopFormOps に MIR アクセスがないため、保守的近似で先行実装 +/// +/// # Future Phase 2+ (Precise) +/// exit ブロックの MIR をスキャンして、実際に read される変数のみを live とする +/// +/// **将来拡張**: LoopFormOps 拡張 or MirFunction 参照追加時に精密化 +/// +/// # Usage +/// ```ignore +/// let liveness_box = LoopExitLivenessBox::new(); +/// let live_at_exit = liveness_box.compute_live_at_exit( +/// &header_vals, +/// &exit_snapshots, +/// ); +/// // → ch, pname, pend 等が含まれる(保守的) +/// ``` +#[derive(Debug, Clone, Default)] +pub struct LoopExitLivenessBox; + +impl LoopExitLivenessBox { + /// Create new LoopExitLivenessBox + pub fn new() -> Self { + Self + } + + /// Compute live_at_exit variables (Phase 1: Conservative) + /// + /// # Arguments + /// * `header_vals` - Header variable values + /// * `exit_snapshots` - Exit predecessor snapshots + /// + /// # Returns + /// BTreeSet of variable names that are potentially live at exit + /// + /// # Phase 1 Logic (Conservative) + /// すべての header_vals と exit_snapshots に現れる変数を live とみなす + /// + /// **保守的な理由**: + /// - BodyLocalInternal でも exit 後で使われる変数がある(ch, pname, pend) + /// - MIR スキャンなしでは正確な判定不可 + /// - 過剰に含めても PhiInvariantsBox が検証するので安全 + /// + /// # Future Phase 2+ (Precise) + /// exit ブロックの MIR をスキャンして、実際に使われる変数のみを返す + /// + /// **精密化計画**: + /// 1. LoopFormOps に `get_block_instructions()` 追加 + /// 2. exit ブロックの Copy/BinOp/Call 等で read される ValueId を収集 + /// 3. ValueId → 変数名の逆引き(variable_map 逆マップ) + /// 4. read される変数のみを live_at_exit に含める + /// + /// # Example + /// ```ignore + /// // Phase 1: 保守的近似 + /// // skip_whitespace: ch は一部 pred でしか定義されないが、exit 後で使われる + /// let live_at_exit = liveness_box.compute_live_at_exit( + /// &header_vals, // { i: %10, n: %20 } + /// &exit_snapshots, // [{ i: %30, ch: %40 }, { i: %50 }] + /// ); + /// // → live_at_exit = { "i", "n", "ch" }(保守的に ch も含める) + /// ``` + pub fn compute_live_at_exit( + &self, + _header_vals: &BTreeMap, + _exit_snapshots: &[(BasicBlockId, BTreeMap)], + ) -> BTreeSet { + // Phase 26-F-4: 環境変数制御による段階的実装 + // + // **環境変数ガード**: + // - NYASH_EXIT_LIVE_ENABLE=1: Phase 2+ MIRスキャン実装を有効化(実験用) + // - デフォルト(未設定): Phase 26-F-3 相当の挙動(安定版) + + let enable_phase2 = std::env::var("NYASH_EXIT_LIVE_ENABLE").ok().as_deref() == Some("1"); + + let live_vars = if enable_phase2 { + // Phase 2+ MIRスキャン実装(未実装) + // TODO: LoopFormOps拡張後に実装 + // - exit ブロックの MIR をスキャン + // - Copy/BinOp/Call 等で read される ValueId を収集 + // - ValueId → 変数名の逆引き + // - read される変数のみを live_at_exit に含める + BTreeSet::new() + } else { + // Phase 1: 空の live_at_exit を返す(Phase 26-F-3 相当) + // + // **理由**: 保守的近似(全変数を live)では PhiInvariantsBox でエラーになる + // + // **問題**: header_vals + exit_snapshots の全変数を live とすると、 + // BodyLocalInternal 変数も live_at_exit に含まれる + // → exit PHI 候補に昇格 + // → 一部の exit pred でのみ定義 → PhiInvariantsBox エラー + // + // **Phase 1 方針**: live_at_exit を空にして、既存の LoopVarClassBox 分類のみに依存 + // → BodyLocalInternal は exit PHI 候補から除外(Phase 26-F-3 と同じ) + BTreeSet::new() + }; + + // Debug trace + if std::env::var("NYASH_EXIT_LIVENESS_TRACE").ok().as_deref() == Some("1") { + eprintln!( + "[LoopExitLivenessBox] Phase {}: live_at_exit={} vars{}", + if enable_phase2 { "2+" } else { "1" }, + live_vars.len(), + if enable_phase2 { + " (MIR scan - experimental)" + } else { + " (empty - Phase 26-F-3 equivalent)" + } + ); + } + + live_vars + } +} + +// ============================================================================ +// Unit Tests +// ============================================================================ + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_compute_live_at_exit_conservative() { + let liveness_box = LoopExitLivenessBox::new(); + + let mut header_vals = BTreeMap::new(); + header_vals.insert("i".to_string(), ValueId(10)); + header_vals.insert("n".to_string(), ValueId(20)); + + let mut snap1 = BTreeMap::new(); + snap1.insert("i".to_string(), ValueId(30)); + snap1.insert("ch".to_string(), ValueId(40)); + + let mut snap2 = BTreeMap::new(); + snap2.insert("i".to_string(), ValueId(50)); + snap2.insert("pname".to_string(), ValueId(60)); + + let exit_snapshots = vec![ + (BasicBlockId(100), snap1), + (BasicBlockId(200), snap2), + ]; + + let live_at_exit = liveness_box.compute_live_at_exit(&header_vals, &exit_snapshots); + + // Phase 1: 空の live_at_exit(MIRスキャン実装待ち) + assert_eq!(live_at_exit.len(), 0); + } + + #[test] + fn test_compute_live_at_exit_empty() { + let liveness_box = LoopExitLivenessBox::new(); + + let header_vals = BTreeMap::new(); + let exit_snapshots = vec![]; + + let live_at_exit = liveness_box.compute_live_at_exit(&header_vals, &exit_snapshots); + + assert_eq!(live_at_exit.len(), 0); + } + + #[test] + fn test_compute_live_at_exit_deduplication() { + let liveness_box = LoopExitLivenessBox::new(); + + let mut header_vals = BTreeMap::new(); + header_vals.insert("i".to_string(), ValueId(10)); + + let mut snap1 = BTreeMap::new(); + snap1.insert("i".to_string(), ValueId(20)); // 重複 + + let mut snap2 = BTreeMap::new(); + snap2.insert("i".to_string(), ValueId(30)); // 重複 + + let exit_snapshots = vec![ + (BasicBlockId(100), snap1), + (BasicBlockId(200), snap2), + ]; + + let live_at_exit = liveness_box.compute_live_at_exit(&header_vals, &exit_snapshots); + + // Phase 1: 空の live_at_exit(MIRスキャン実装待ち) + assert_eq!(live_at_exit.len(), 0); + } +} diff --git a/src/mir/phi_core/mod.rs b/src/mir/phi_core/mod.rs index 5e8c3a78..63d9a354 100644 --- a/src/mir/phi_core/mod.rs +++ b/src/mir/phi_core/mod.rs @@ -38,6 +38,9 @@ pub mod if_body_local_merge; // Phase 26-F-3: PHI invariants guard box(Fail-Fast共通化) pub mod phi_invariants; +// Phase 26-F-4: Loop Exit Liveness Box - exit後で使われる変数決定箱 +pub mod loop_exit_liveness; + // Public surface for callers that want a stable path: // Phase 1: No re-exports to avoid touching private builder internals. // Callers should continue using existing paths. Future phases may expose