chore: Phase 25.1 完了 - LoopForm v2/Stage1 CLI/環境変数削減 + Phase 26-D からの変更

Phase 25.1 完了成果:
-  LoopForm v2 テスト・ドキュメント・コメント完備
  - 4ケース(A/B/C/D)完全テストカバレッジ
  - 最小再現ケース作成(SSAバグ調査用)
  - SSOT文書作成(loopform_ssot.md)
  - 全ソースに [LoopForm] コメントタグ追加

-  Stage-1 CLI デバッグ環境構築
  - stage1_cli.hako 実装
  - stage1_bridge.rs ブリッジ実装
  - デバッグツール作成(stage1_debug.sh/stage1_minimal.sh)
  - アーキテクチャ改善提案文書

-  環境変数削減計画策定
  - 25変数の完全調査・分類
  - 6段階削減ロードマップ(25→5、80%削減)
  - 即時削除可能変数特定(NYASH_CONFIG/NYASH_DEBUG)

Phase 26-D からの累積変更:
- PHI実装改善(ExitPhiBuilder/HeaderPhiBuilder等)
- MIRビルダーリファクタリング
- 型伝播・最適化パス改善
- その他約300ファイルの累積変更

🎯 技術的成果:
- SSAバグ根本原因特定(条件分岐内loop変数変更)
- Region+next_iパターン適用完了(UsingCollectorBox等)
- LoopFormパターン文書化・テスト化完了
- セルフホスティング基盤強化

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: ChatGPT <noreply@openai.com>
Co-Authored-By: Task Assistant <task@anthropic.com>
This commit is contained in:
nyash-codex
2025-11-21 06:25:17 +09:00
parent baf028a94f
commit f9d100ce01
366 changed files with 14322 additions and 5236 deletions

View File

@ -228,7 +228,7 @@ mod tests {
// Pinned variable always needs exit PHI
let needs_phi = builder.should_generate_exit_phi(
"s",
&["s".to_string()], // pinned
&["s".to_string()], // pinned
&[],
&[BasicBlockId(1), BasicBlockId(2)],
);
@ -246,7 +246,7 @@ mod tests {
let needs_phi = builder.should_generate_exit_phi(
"i",
&[],
&["i".to_string()], // carrier
&["i".to_string()], // carrier
&[BasicBlockId(1), BasicBlockId(2)],
);
@ -265,12 +265,8 @@ mod tests {
let builder = BodyLocalPhiBuilder::new(classifier, inspector);
// BodyLocalExit: defined in all exit preds → needs exit PHI
let needs_phi = builder.should_generate_exit_phi(
"x",
&[],
&[],
&[BasicBlockId(1), BasicBlockId(2)],
);
let needs_phi =
builder.should_generate_exit_phi("x", &[], &[], &[BasicBlockId(1), BasicBlockId(2)]);
assert!(needs_phi);
}
@ -287,14 +283,10 @@ mod tests {
// BodyLocalInternal: defined only in block 5, but exit preds are [2, 5]
// → NO exit PHI (Option C fix!)
let needs_phi = builder.should_generate_exit_phi(
"ch",
&[],
&[],
&[BasicBlockId(2), BasicBlockId(5)],
);
let needs_phi =
builder.should_generate_exit_phi("ch", &[], &[], &[BasicBlockId(2), BasicBlockId(5)]);
assert!(!needs_phi); // ← Skip exit PHI!
assert!(!needs_phi); // ← Skip exit PHI!
}
#[test]
@ -333,7 +325,7 @@ mod tests {
assert!(phi_vars.contains(&"s".to_string()));
assert!(phi_vars.contains(&"idx".to_string()));
assert!(phi_vars.contains(&"n".to_string()));
assert!(!phi_vars.contains(&"ch".to_string())); // ← Filtered out!
assert!(!phi_vars.contains(&"ch".to_string())); // ← Filtered out!
}
#[test]
@ -358,13 +350,14 @@ mod tests {
// Exit preds: [block 2 (early break), block 5 (after ch definition)]
let exit_preds = vec![BasicBlockId(2), BasicBlockId(5)];
let phi_vars = builder.filter_exit_phi_candidates(&all_vars, &pinned, &carrier, &exit_preds);
let phi_vars =
builder.filter_exit_phi_candidates(&all_vars, &pinned, &carrier, &exit_preds);
// Expected: s, idx (ch filtered out!)
assert_eq!(phi_vars.len(), 2);
assert!(phi_vars.contains(&"s".to_string()));
assert!(phi_vars.contains(&"idx".to_string()));
assert!(!phi_vars.contains(&"ch".to_string())); // ← Option C fix!
assert!(!phi_vars.contains(&"ch".to_string())); // ← Option C fix!
}
#[test]
@ -376,12 +369,7 @@ mod tests {
let builder = BodyLocalPhiBuilder::new(classifier, inspector);
let class = builder.classify_variable(
"ch",
&[],
&[],
&[BasicBlockId(2), BasicBlockId(5)],
);
let class = builder.classify_variable("ch", &[], &[], &[BasicBlockId(2), BasicBlockId(5)]);
assert_eq!(class, LoopVarClass::BodyLocalInternal);
assert!(!class.needs_exit_phi());
@ -394,15 +382,12 @@ mod tests {
let mut builder = BodyLocalPhiBuilder::new(classifier, inspector);
// Test mutable access
builder.inspector_mut().record_definition("test", BasicBlockId(10));
builder
.inspector_mut()
.record_definition("test", BasicBlockId(10));
// Verify recording worked
let class = builder.classify_variable(
"test",
&[],
&[],
&[BasicBlockId(10)],
);
let class = builder.classify_variable("test", &[], &[], &[BasicBlockId(10)]);
assert_eq!(class, LoopVarClass::BodyLocalExit);
}
@ -421,12 +406,7 @@ mod tests {
let builder = BodyLocalPhiBuilder::new(classifier, inspector);
// Should be classified as BodyLocalInternal
let class = builder.classify_variable(
"__pin$42$@binop_lhs",
&[],
&[],
&[BasicBlockId(5)],
);
let class = builder.classify_variable("__pin$42$@binop_lhs", &[], &[], &[BasicBlockId(5)]);
assert_eq!(class, LoopVarClass::BodyLocalInternal);
assert!(!class.needs_exit_phi());
@ -435,12 +415,8 @@ mod tests {
let all_vars = vec!["__pin$42$@binop_lhs".to_string(), "s".to_string()];
let pinned = vec!["s".to_string()];
let phi_vars = builder.filter_exit_phi_candidates(
&all_vars,
&pinned,
&[],
&[BasicBlockId(5)],
);
let phi_vars =
builder.filter_exit_phi_candidates(&all_vars, &pinned, &[], &[BasicBlockId(5)]);
// Only s should remain
assert_eq!(phi_vars.len(), 1);

View File

@ -16,11 +16,19 @@ pub fn debug_verify_phi_inputs(
) {
use std::collections::HashSet;
// Always compute when env toggle is set; otherwise no-op in release use.
let verify_on = std::env::var("HAKO_PHI_VERIFY").ok().map(|v| v.to_ascii_lowercase())
.map(|v| v == "1" || v == "true" || v == "on").unwrap_or(false)
|| std::env::var("NYASH_PHI_VERIFY").ok().map(|v| v.to_ascii_lowercase())
.map(|v| v == "1" || v == "true" || v == "on").unwrap_or(false);
if !verify_on { return; }
let verify_on = std::env::var("HAKO_PHI_VERIFY")
.ok()
.map(|v| v.to_ascii_lowercase())
.map(|v| v == "1" || v == "true" || v == "on")
.unwrap_or(false)
|| std::env::var("NYASH_PHI_VERIFY")
.ok()
.map(|v| v.to_ascii_lowercase())
.map(|v| v == "1" || v == "true" || v == "on")
.unwrap_or(false);
if !verify_on {
return;
}
// Rebuild CFG to avoid stale predecessor sets
let mut func = function.clone();

View File

@ -38,11 +38,8 @@ impl ConservativeMerge {
all_vars.extend(else_map.keys().cloned());
}
let changed = crate::mir::phi_core::if_phi::compute_modified_names(
pre_if,
then_end,
else_end_opt,
);
let changed =
crate::mir::phi_core::if_phi::compute_modified_names(pre_if, then_end, else_end_opt);
let changed_vars = changed.into_iter().collect();
Self {
@ -102,7 +99,12 @@ impl ConservativeMerge {
}
/// Debug trace 出力Conservative PHI生成の可視化
pub fn trace_if_enabled(&self, pre_if: &HashMap<String, ValueId>, then_end: &HashMap<String, ValueId>, else_end_opt: &Option<HashMap<String, ValueId>>) {
pub fn trace_if_enabled(
&self,
pre_if: &HashMap<String, ValueId>,
then_end: &HashMap<String, ValueId>,
else_end_opt: &Option<HashMap<String, ValueId>>,
) {
let trace_conservative = std::env::var("NYASH_CONSERVATIVE_PHI_TRACE")
.ok()
.as_deref()

View File

@ -240,9 +240,7 @@ impl HeaderPhiBuilder {
/// }
/// ```
pub fn find_pinned_phi(&self, var_name: &str) -> Option<&PinnedPhiInfo> {
self.pinned_phis
.iter()
.find(|phi| phi.var_name == var_name)
self.pinned_phis.iter().find(|phi| phi.var_name == var_name)
}
/// Find carrier PHI by variable name
@ -354,12 +352,7 @@ mod tests {
#[test]
fn test_prepare_pinned_phi() {
let mut builder = HeaderPhiBuilder::new();
builder.prepare_pinned_phi(
"x".to_string(),
ValueId(10),
ValueId(1),
ValueId(2),
);
builder.prepare_pinned_phi("x".to_string(), ValueId(10), ValueId(1), ValueId(2));
assert_eq!(builder.pinned_phi_count(), 1);
let phi = &builder.pinned_phis()[0];
@ -372,12 +365,7 @@ mod tests {
#[test]
fn test_prepare_carrier_phi() {
let mut builder = HeaderPhiBuilder::new();
builder.prepare_carrier_phi(
"i".to_string(),
ValueId(20),
ValueId(0),
ValueId(3),
);
builder.prepare_carrier_phi("i".to_string(), ValueId(20), ValueId(0), ValueId(3));
assert_eq!(builder.carrier_phi_count(), 1);
let phi = &builder.carrier_phis()[0];

View File

@ -48,7 +48,11 @@ pub fn extract_assigned_var(ast: &ASTNode) -> Option<String> {
ASTNode::Program { statements, .. } => {
statements.last().and_then(|st| extract_assigned_var(st))
}
ASTNode::If { then_body, else_body, .. } => {
ASTNode::If {
then_body,
else_body,
..
} => {
let then_prog = ASTNode::Program {
statements: then_body.clone(),
span: crate::ast::Span::unknown(),
@ -80,13 +84,25 @@ pub fn collect_assigned_vars(ast: &ASTNode, out: &mut std::collections::HashSet<
}
}
ASTNode::Program { statements, .. } => {
for s in statements { collect_assigned_vars(s, out); }
for s in statements {
collect_assigned_vars(s, out);
}
}
ASTNode::If { then_body, else_body, .. } => {
let tp = ASTNode::Program { statements: then_body.clone(), span: crate::ast::Span::unknown() };
ASTNode::If {
then_body,
else_body,
..
} => {
let tp = ASTNode::Program {
statements: then_body.clone(),
span: crate::ast::Span::unknown(),
};
collect_assigned_vars(&tp, out);
if let Some(eb) = else_body {
let ep = ASTNode::Program { statements: eb.clone(), span: crate::ast::Span::unknown() };
let ep = ASTNode::Program {
statements: eb.clone(),
span: crate::ast::Span::unknown(),
};
collect_assigned_vars(&ep, out);
}
}
@ -104,9 +120,13 @@ pub fn compute_modified_names(
use std::collections::BTreeSet;
// 決定的順序のためBTreeSet使用
let mut names: BTreeSet<&str> = BTreeSet::new();
for k in then_map_end.keys() { names.insert(k.as_str()); }
for k in then_map_end.keys() {
names.insert(k.as_str());
}
if let Some(emap) = else_map_end_opt.as_ref() {
for k in emap.keys() { names.insert(k.as_str()); }
for k in emap.keys() {
names.insert(k.as_str());
}
}
let mut changed: Vec<String> = Vec::new();
// アルファベット順で決定的にイテレート
@ -181,18 +201,19 @@ pub fn merge_modified_at_merge_with<O: PhiMergeOps>(
// Build incoming pairs from reachable predecessors only
let mut inputs: Vec<(crate::mir::BasicBlockId, ValueId)> = Vec::new();
// Only include reachable predecessors; do not synthesize else_block when unreachable.
if let Some(tp) = then_pred_opt { inputs.push((tp, then_v)); }
if let Some(ep) = else_pred_opt { inputs.push((ep, else_v)); }
if let Some(tp) = then_pred_opt {
inputs.push((tp, then_v));
}
if let Some(ep) = else_pred_opt {
inputs.push((ep, else_v));
}
match inputs.len() {
0 => {}
1 => {
let (_pred, v) = inputs[0];
if trace {
eprintln!(
"[if-trace] merge bind var={} v={:?} (single pred)",
name, v
);
eprintln!("[if-trace] merge bind var={} v={:?} (single pred)", name, v);
}
ops.update_var(name, v);
}
@ -201,10 +222,7 @@ pub fn merge_modified_at_merge_with<O: PhiMergeOps>(
let dst = ops.new_value();
ops.emit_phi_at_block_start(merge_bb, dst, inputs)?;
if trace {
eprintln!(
"[if-trace] merge phi var={} dst={:?}",
name, dst
);
eprintln!("[if-trace] merge phi var={} dst={:?}", name, dst);
}
ops.update_var(name, dst);
}

View File

@ -17,7 +17,6 @@
/// This box has ONE job: track definition locations.
/// It doesn't know about loops, PHI nodes, or exit blocks.
/// It's a pure data structure that other boxes can query.
use crate::mir::{BasicBlockId, ValueId};
use std::collections::{HashMap, HashSet};
@ -62,11 +61,7 @@ impl LocalScopeInspectorBox {
/// ]);
/// inspector.record_snapshot(BasicBlockId::new(5), &snapshot);
/// ```
pub fn record_snapshot(
&mut self,
block: BasicBlockId,
vars: &HashMap<String, ValueId>,
) {
pub fn record_snapshot(&mut self, block: BasicBlockId, vars: &HashMap<String, ValueId>) {
for var_name in vars.keys() {
self.record_definition(var_name, block);
}
@ -117,7 +112,9 @@ impl LocalScopeInspectorBox {
/// ```
pub fn is_available_in_all(&self, var_name: &str, required_blocks: &[BasicBlockId]) -> bool {
if let Some(defining_blocks) = self.var_definitions.get(var_name) {
required_blocks.iter().all(|block| defining_blocks.contains(block))
required_blocks
.iter()
.all(|block| defining_blocks.contains(block))
} else {
// Variable doesn't exist at all
false
@ -191,10 +188,7 @@ mod tests {
inspector.record_definition("i", BasicBlockId::new(5));
inspector.record_definition("i", BasicBlockId::new(7));
let required = vec![
BasicBlockId::new(2),
BasicBlockId::new(5),
];
let required = vec![BasicBlockId::new(2), BasicBlockId::new(5)];
assert!(inspector.is_available_in_all("i", &required));
}
@ -206,10 +200,7 @@ mod tests {
// "ch" only defined in block 5
inspector.record_definition("ch", BasicBlockId::new(5));
let required = vec![
BasicBlockId::new(2),
BasicBlockId::new(5),
];
let required = vec![BasicBlockId::new(2), BasicBlockId::new(5)];
// Should fail because block 2 is missing
assert!(!inspector.is_available_in_all("ch", &required));
@ -249,7 +240,10 @@ mod tests {
fn test_get_defining_blocks_unknown() {
let inspector = LocalScopeInspectorBox::new();
assert_eq!(inspector.get_defining_blocks("unknown"), Vec::<BasicBlockId>::new());
assert_eq!(
inspector.get_defining_blocks("unknown"),
Vec::<BasicBlockId>::new()
);
}
#[test]

View File

@ -7,8 +7,8 @@
* - Phase 31.x 以降で段階的に削除予定。新しい PHI 実装をここに追加してはいけない。
*/
use crate::mir::{BasicBlockId, ValueId};
use crate::ast::ASTNode;
use crate::mir::{BasicBlockId, ValueId};
/// Loop-local placeholder of an incomplete PHI (header-time declaration).
/// Moved from loop_builder to centralize PHI-related types.
@ -27,7 +27,9 @@ pub type SnapshotAt = (BasicBlockId, VarSnapshot);
pub struct LoopPhiManager;
impl LoopPhiManager {
pub fn new() -> Self { Self::default() }
pub fn new() -> Self {
Self::default()
}
}
/// Operations required from a loop builder to finalize PHIs.
@ -41,7 +43,12 @@ pub trait LoopPhiOps {
) -> Result<(), String>;
fn update_var(&mut self, name: String, value: ValueId);
fn get_variable_at_block(&mut self, name: &str, block: BasicBlockId) -> Option<ValueId>;
fn debug_verify_phi_inputs(&mut self, _merge_bb: BasicBlockId, _inputs: &[(BasicBlockId, ValueId)]) {}
fn debug_verify_phi_inputs(
&mut self,
_merge_bb: BasicBlockId,
_inputs: &[(BasicBlockId, ValueId)],
) {
}
/// PHI UseBeforeDef修正: preheaderブロックでCopy命令を先行生成
fn emit_copy_at_preheader(
@ -57,7 +64,9 @@ pub trait LoopPhiOps {
&mut self,
_block: BasicBlockId,
_pred: BasicBlockId,
) -> Result<(), String> { Ok(()) }
) -> Result<(), String> {
Ok(())
}
}
/// Finalize PHIs at loop exit (merge of break points and header fall-through).
@ -151,7 +160,7 @@ pub fn seal_incomplete_phis_with<O: LoopPhiOps>(
// preheaderの値を使用する
let latch_value = if value_after == phi.phi_id {
// ループ不変変数preheaderの値を使用
phi.known_inputs[0].1 // preheaderからの初期値
phi.known_inputs[0].1 // preheaderからの初期値
} else {
value_after
};
@ -198,7 +207,7 @@ pub fn prepare_loop_variables_with<O: LoopPhiOps>(
let inc = IncompletePhi {
phi_id,
var_name: var_name.clone(),
known_inputs: vec![(preheader_id, pre_copy)], // ensure def at preheader
known_inputs: vec![(preheader_id, pre_copy)], // ensure def at preheader
};
// Insert an initial PHI at header with only the preheader input so that
// the header condition reads the PHI value (first iteration = preheader).
@ -225,17 +234,31 @@ pub fn collect_carrier_assigns(node: &ASTNode, vars: &mut Vec<String>, has_ctrl:
}
}
}
ASTNode::Break { .. } | ASTNode::Continue { .. } => { *has_ctrl = true; }
ASTNode::If { then_body, else_body, .. } => {
let tp = ASTNode::Program { statements: then_body.clone(), span: crate::ast::Span::unknown() };
ASTNode::Break { .. } | ASTNode::Continue { .. } => {
*has_ctrl = true;
}
ASTNode::If {
then_body,
else_body,
..
} => {
let tp = ASTNode::Program {
statements: then_body.clone(),
span: crate::ast::Span::unknown(),
};
collect_carrier_assigns(&tp, vars, has_ctrl);
if let Some(eb) = else_body {
let ep = ASTNode::Program { statements: eb.clone(), span: crate::ast::Span::unknown() };
let ep = ASTNode::Program {
statements: eb.clone(),
span: crate::ast::Span::unknown(),
};
collect_carrier_assigns(&ep, vars, has_ctrl);
}
}
ASTNode::Program { statements, .. } => {
for s in statements { collect_carrier_assigns(s, vars, has_ctrl); }
for s in statements {
collect_carrier_assigns(s, vars, has_ctrl);
}
}
_ => {}
}

View File

@ -106,11 +106,7 @@ impl LoopSnapshotManager {
/// ```ignore
/// manager.add_continue_snapshot(BasicBlockId(7), continue_vars);
/// ```
pub fn add_continue_snapshot(
&mut self,
block: BasicBlockId,
vars: HashMap<String, ValueId>,
) {
pub fn add_continue_snapshot(&mut self, block: BasicBlockId, vars: HashMap<String, ValueId>) {
self.continue_snapshots.push((block, vars));
}

View File

@ -19,7 +19,6 @@
/// This box has ONE job: classify variables.
/// It doesn't generate PHI nodes or modify IR.
/// It's a pure decision box that other boxes can query.
use super::local_scope_inspector::LocalScopeInspectorBox;
use crate::mir::BasicBlockId;
@ -82,7 +81,7 @@ impl LoopVarClass {
LoopVarClass::Pinned => true,
LoopVarClass::Carrier => true,
LoopVarClass::BodyLocalExit => true,
LoopVarClass::BodyLocalInternal => false, // ← Option C: Skip exit PHI!
LoopVarClass::BodyLocalInternal => false, // ← Option C: Skip exit PHI!
}
}
@ -91,7 +90,7 @@ impl LoopVarClass {
match self {
LoopVarClass::Pinned => true,
LoopVarClass::Carrier => true,
LoopVarClass::BodyLocalExit => false, // Body-local: no header PHI
LoopVarClass::BodyLocalExit => false, // Body-local: no header PHI
LoopVarClass::BodyLocalInternal => false,
}
}
@ -261,7 +260,7 @@ mod tests {
assert!(LoopVarClass::Pinned.needs_exit_phi());
assert!(LoopVarClass::Carrier.needs_exit_phi());
assert!(LoopVarClass::BodyLocalExit.needs_exit_phi());
assert!(!LoopVarClass::BodyLocalInternal.needs_exit_phi()); // ← Option C!
assert!(!LoopVarClass::BodyLocalInternal.needs_exit_phi()); // ← Option C!
}
#[test]
@ -279,7 +278,7 @@ mod tests {
let class = classifier.classify(
"limit",
&["limit".to_string()], // pinned
&["limit".to_string()], // pinned
&[],
&inspector,
&[],
@ -298,7 +297,7 @@ mod tests {
let class = classifier.classify(
"i",
&[],
&["i".to_string()], // carrier
&["i".to_string()], // carrier
&inspector,
&[],
);
@ -322,16 +321,10 @@ mod tests {
let exit_preds = vec![block_2, block_5];
let class = classifier.classify(
"x",
&[],
&[],
&inspector,
&exit_preds,
);
let class = classifier.classify("x", &[], &[], &inspector, &exit_preds);
assert_eq!(class, LoopVarClass::BodyLocalExit);
assert!(class.needs_exit_phi()); // Should get exit PHI
assert!(class.needs_exit_phi()); // Should get exit PHI
assert!(!class.needs_header_phi());
}
@ -344,20 +337,14 @@ mod tests {
let block_2 = BasicBlockId::new(2);
let block_5 = BasicBlockId::new(5);
inspector.record_definition("ch", block_5); // Only block 5!
inspector.record_definition("ch", block_5); // Only block 5!
let exit_preds = vec![block_2, block_5];
let class = classifier.classify(
"ch",
&[],
&[],
&inspector,
&exit_preds,
);
let class = classifier.classify("ch", &[], &[], &inspector, &exit_preds);
assert_eq!(class, LoopVarClass::BodyLocalInternal);
assert!(!class.needs_exit_phi()); // ← Option C: Skip exit PHI!
assert!(!class.needs_exit_phi()); // ← Option C: Skip exit PHI!
assert!(!class.needs_header_phi());
}
@ -407,7 +394,7 @@ mod tests {
// ch should NOT need exit PHI (Option C: prevents PHI pred mismatch!)
assert_eq!(ch_class, LoopVarClass::BodyLocalInternal);
assert!(!ch_class.needs_exit_phi()); // ← This fixes the bug!
assert!(!ch_class.needs_exit_phi()); // ← This fixes the bug!
}
#[test]
@ -435,7 +422,10 @@ mod tests {
assert_eq!(results.len(), 3);
assert_eq!(results[0], ("i".to_string(), LoopVarClass::Carrier));
assert_eq!(results[1], ("n".to_string(), LoopVarClass::Pinned));
assert_eq!(results[2], ("ch".to_string(), LoopVarClass::BodyLocalInternal));
assert_eq!(
results[2],
("ch".to_string(), LoopVarClass::BodyLocalInternal)
);
}
#[test]
@ -469,7 +459,7 @@ mod tests {
assert_eq!(candidates.len(), 2);
assert!(candidates.contains(&"i".to_string()));
assert!(candidates.contains(&"n".to_string()));
assert!(!candidates.contains(&"ch".to_string())); // ← Option C: ch is filtered out!
assert!(!candidates.contains(&"ch".to_string())); // ← Option C: ch is filtered out!
}
/// Test Task先生の発見: __pin$ temporary variables should be BodyLocalInternal
@ -490,26 +480,48 @@ mod tests {
for pin_var in pin_vars {
let class = classifier.classify(
pin_var,
&[], // Not pinned
&[], // Not carrier
&[], // Not pinned
&[], // Not carrier
&inspector,
&[], // No exit preds
&[], // No exit preds
);
assert_eq!(class, LoopVarClass::BodyLocalInternal,
"Variable '{}' should be BodyLocalInternal", pin_var);
assert!(!class.needs_exit_phi(),
"Variable '{}' should NOT need exit PHI", pin_var);
assert!(!class.needs_header_phi(),
"Variable '{}' should NOT need header PHI", pin_var);
assert_eq!(
class,
LoopVarClass::BodyLocalInternal,
"Variable '{}' should be BodyLocalInternal",
pin_var
);
assert!(
!class.needs_exit_phi(),
"Variable '{}' should NOT need exit PHI",
pin_var
);
assert!(
!class.needs_header_phi(),
"Variable '{}' should NOT need header PHI",
pin_var
);
}
}
#[test]
fn test_description() {
assert_eq!(LoopVarClass::Pinned.description(), "Loop-crossing parameter");
assert_eq!(LoopVarClass::Carrier.description(), "Loop-modified variable");
assert_eq!(LoopVarClass::BodyLocalExit.description(), "Body-local (all exits)");
assert_eq!(LoopVarClass::BodyLocalInternal.description(), "Body-local (partial exits)");
assert_eq!(
LoopVarClass::Pinned.description(),
"Loop-crossing parameter"
);
assert_eq!(
LoopVarClass::Carrier.description(),
"Loop-modified variable"
);
assert_eq!(
LoopVarClass::BodyLocalExit.description(),
"Body-local (all exits)"
);
assert_eq!(
LoopVarClass::BodyLocalInternal.description(),
"Body-local (partial exits)"
);
}
}

View File

@ -8,9 +8,9 @@
* Status: Always-onLoopForm PHI v2 は既定実装。NYASH_LOOPFORM_PHI_V2 は互換目的のみで、挙動切り替えには使われない)
*/
use crate::mir::{BasicBlockId, ValueId};
use crate::mir::phi_core::loop_snapshot_merge::LoopSnapshotMergeBox;
use crate::mir::phi_core::phi_input_collector::PhiInputCollector;
use crate::mir::{BasicBlockId, ValueId};
use std::collections::HashMap;
/// 📦 LoopForm Context - Box-First理論に基づくパラメータ予約明示化
@ -50,19 +50,19 @@ impl LoopFormContext {
#[derive(Debug, Clone)]
pub struct CarrierVariable {
pub name: String,
pub init_value: ValueId, // Initial value from preheader (local variable)
pub preheader_copy: ValueId, // Copy allocated in preheader block
pub header_phi: ValueId, // PHI node allocated in header block
pub latch_value: ValueId, // Updated value computed in latch (set during sealing)
pub init_value: ValueId, // Initial value from preheader (local variable)
pub preheader_copy: ValueId, // Copy allocated in preheader block
pub header_phi: ValueId, // PHI node allocated in header block
pub latch_value: ValueId, // Updated value computed in latch (set during sealing)
}
/// A pinned variable: not modified in loop body (loop-invariant, typically parameters)
#[derive(Debug, Clone)]
pub struct PinnedVariable {
pub name: String,
pub param_value: ValueId, // Original parameter or loop-invariant value
pub preheader_copy: ValueId, // Copy allocated in preheader block
pub header_phi: ValueId, // PHI node allocated in header block
pub param_value: ValueId, // Original parameter or loop-invariant value
pub preheader_copy: ValueId, // Copy allocated in preheader block
pub header_phi: ValueId, // PHI node allocated in header block
}
/// LoopForm Meta-Box: Structured representation of loop SSA construction
@ -98,7 +98,7 @@ impl LoopFormBuilder {
pinned: Vec::new(),
preheader_id,
header_id,
preheader_vars: HashMap::new(), // Will be set in prepare_structure()
preheader_vars: HashMap::new(), // Will be set in prepare_structure()
}
}
@ -118,7 +118,10 @@ impl LoopFormBuilder {
self.preheader_vars = current_vars.clone();
if debug_enabled {
eprintln!("[loopform/prepare] === START prepare_structure() === {} variables", current_vars.len());
eprintln!(
"[loopform/prepare] === START prepare_structure() === {} variables",
current_vars.len()
);
eprintln!("[loopform/prepare] Full variable list:");
let mut sorted_vars: Vec<_> = current_vars.iter().collect();
sorted_vars.sort_by_key(|(name, _)| name.as_str());
@ -147,7 +150,10 @@ impl LoopFormBuilder {
let max_existing_id = current_vars.values().map(|v| v.0).max().unwrap_or(0);
if debug_enabled {
eprintln!("[loopform/prepare] Calling ensure_counter_after(max_existing_id={})", max_existing_id);
eprintln!(
"[loopform/prepare] Calling ensure_counter_after(max_existing_id={})",
max_existing_id
);
}
ops.ensure_counter_after(max_existing_id)?;
@ -178,8 +184,10 @@ impl LoopFormBuilder {
header_phi: ops.new_value(), // Allocate NOW
};
if debug_enabled {
eprintln!("[loopform/prepare] PINNED: {} -> init={:?}, copy={:?}, phi={:?}",
name, value, pinned.preheader_copy, pinned.header_phi);
eprintln!(
"[loopform/prepare] PINNED: {} -> init={:?}, copy={:?}, phi={:?}",
name, value, pinned.preheader_copy, pinned.header_phi
);
}
self.pinned.push(pinned);
} else {
@ -193,8 +201,10 @@ impl LoopFormBuilder {
latch_value: ValueId(0), // Will be set during seal (placeholder)
};
if debug_enabled {
eprintln!("[loopform/prepare] CARRIER: {} -> init={:?}, copy={:?}, phi={:?}",
name, value, carrier.preheader_copy, carrier.header_phi);
eprintln!(
"[loopform/prepare] CARRIER: {} -> init={:?}, copy={:?}, phi={:?}",
name, value, carrier.preheader_copy, carrier.header_phi
);
}
self.carriers.push(carrier);
}
@ -217,26 +227,17 @@ impl LoopFormBuilder {
/// 2. Carrier variables second
///
/// This ordering ensures consistent ValueId allocation across runs.
pub fn emit_preheader<O: LoopFormOps>(
&self,
ops: &mut O,
) -> Result<(), String> {
pub fn emit_preheader<O: LoopFormOps>(&self, ops: &mut O) -> Result<(), String> {
ops.set_current_block(self.preheader_id)?;
// Emit copies for pinned variables
for pinned in &self.pinned {
ops.emit_copy(
pinned.preheader_copy,
pinned.param_value,
)?;
ops.emit_copy(pinned.preheader_copy, pinned.param_value)?;
}
// Emit copies for carrier variables
for carrier in &self.carriers {
ops.emit_copy(
carrier.preheader_copy,
carrier.init_value,
)?;
ops.emit_copy(carrier.preheader_copy, carrier.init_value)?;
}
// Jump to header
@ -249,10 +250,7 @@ impl LoopFormBuilder {
///
/// Creates incomplete PHI nodes with only preheader input.
/// These will be completed in seal_phis() after loop body is lowered.
pub fn emit_header_phis<O: LoopFormOps>(
&mut self,
ops: &mut O,
) -> Result<(), String> {
pub fn emit_header_phis<O: LoopFormOps>(&mut self, ops: &mut O) -> Result<(), String> {
ops.set_current_block(self.header_id)?;
// Emit PHIs for pinned variables
@ -277,8 +275,9 @@ impl LoopFormBuilder {
// Also update all previous pin levels (__pin$1$@recv, __pin$2$@recv, etc.)
// Extract the pin counter and update all lower levels
if let Some(idx) = pinned.name.find("$") {
if let Some(end_idx) = pinned.name[idx+1..].find("$") {
if let Ok(counter) = pinned.name[idx+1..idx+1+end_idx].parse::<u32>() {
if let Some(end_idx) = pinned.name[idx + 1..].find("$") {
if let Ok(counter) = pinned.name[idx + 1..idx + 1 + end_idx].parse::<u32>()
{
// Update all previous pin levels (1 through counter-1)
for i in 1..counter {
let alias = format!("__pin${}$@recv", i);
@ -321,7 +320,7 @@ impl LoopFormBuilder {
ops: &mut O,
latch_id: BasicBlockId,
continue_snapshots: &[(BasicBlockId, HashMap<String, ValueId>)],
_writes: &std::collections::HashSet<String>, // Step 5-1/5-2: Reserved for future optimization
_writes: &std::collections::HashSet<String>, // Step 5-1/5-2: Reserved for future optimization
) -> Result<(), String> {
let debug = std::env::var("NYASH_LOOPFORM_DEBUG").is_ok();
@ -402,14 +401,18 @@ impl LoopFormBuilder {
// They don't have explicit values at the latch block, so we use the header PHI
// itself as the latch value (creating a self-referencing PHI that will be
// optimized by PHI reduction).
carrier.latch_value = if carrier.name.starts_with("__pin$") && carrier.name.contains("$@") {
carrier.header_phi // Use the PHI value itself as the latch input
} else {
ops.get_variable_at_block(&carrier.name, latch_id)
.ok_or_else(|| {
format!("Carrier variable '{}' not found at latch block", carrier.name)
})?
};
carrier.latch_value =
if carrier.name.starts_with("__pin$") && carrier.name.contains("$@") {
carrier.header_phi // Use the PHI value itself as the latch input
} else {
ops.get_variable_at_block(&carrier.name, latch_id)
.ok_or_else(|| {
format!(
"Carrier variable '{}' not found at latch block",
carrier.name
)
})?
};
// Phase 26-B-3: Use PhiInputCollector for unified PHI input handling
let mut collector = PhiInputCollector::new();
@ -488,15 +491,27 @@ impl LoopFormBuilder {
let debug = std::env::var("NYASH_LOOPFORM_DEBUG").is_ok();
if debug {
eprintln!("[DEBUG/exit_phi] ====== Exit PHI Generation ======");
eprintln!("[DEBUG/exit_phi] exit_id = {:?}, header_id = {:?}, branch_source = {:?}",
exit_id, self.header_id, branch_source_block);
eprintln!("[DEBUG/exit_phi] exit_snapshots.len() = {}", exit_snapshots.len());
eprintln!(
"[DEBUG/exit_phi] exit_id = {:?}, header_id = {:?}, branch_source = {:?}",
exit_id, self.header_id, branch_source_block
);
eprintln!(
"[DEBUG/exit_phi] exit_snapshots.len() = {}",
exit_snapshots.len()
);
for (i, (bb, snap)) in exit_snapshots.iter().enumerate() {
eprintln!("[DEBUG/exit_phi] snapshot[{}]: block = {:?}, num_vars = {}",
i, bb, snap.len());
eprintln!(
"[DEBUG/exit_phi] snapshot[{}]: block = {:?}, num_vars = {}",
i,
bb,
snap.len()
);
}
eprintln!("[DEBUG/exit_phi] pinned.len() = {}, carriers.len() = {}",
self.pinned.len(), self.carriers.len());
eprintln!(
"[DEBUG/exit_phi] pinned.len() = {}, carriers.len() = {}",
self.pinned.len(),
self.carriers.len()
);
}
// Phase 25.2: LoopSnapshotMergeBox を使って exit PHI 統合
@ -512,7 +527,8 @@ impl LoopFormBuilder {
// 2. body_local_vars を収集決定的順序のためBTreeSet使用
let mut body_local_names = Vec::new();
let mut body_local_set: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
let mut body_local_set: std::collections::BTreeSet<String> =
std::collections::BTreeSet::new();
for (_block_id, snapshot) in exit_snapshots {
// 決定的順序のため、keysをソートしてからイテレート
let mut sorted_keys: Vec<_> = snapshot.keys().collect();
@ -537,7 +553,10 @@ impl LoopFormBuilder {
}
if debug && !body_local_names.is_empty() {
eprintln!("[DEBUG/exit_phi] Found {} body-local variables", body_local_names.len());
eprintln!(
"[DEBUG/exit_phi] Found {} body-local variables",
body_local_names.len()
);
}
// Option C: Body-local variables should NOT be added to header_vals!
@ -554,13 +573,19 @@ impl LoopFormBuilder {
for (block_id, snapshot) in exit_snapshots {
if !ops.block_exists(*block_id) {
if debug {
eprintln!("[DEBUG/exit_phi] ⚠️ Skipping non-existent block {:?}", block_id);
eprintln!(
"[DEBUG/exit_phi] ⚠️ Skipping non-existent block {:?}",
block_id
);
}
continue;
}
if !exit_preds.contains(block_id) {
if debug {
eprintln!("[DEBUG/exit_phi] ⚠️ Skipping block {:?} (not in CFG predecessors)", block_id);
eprintln!(
"[DEBUG/exit_phi] ⚠️ Skipping block {:?} (not in CFG predecessors)",
block_id
);
}
continue;
}
@ -590,7 +615,10 @@ impl LoopFormBuilder {
if exit_preds.contains(&branch_source_block) {
inspector.record_snapshot(branch_source_block, &header_vals);
if debug {
eprintln!("[DEBUG/exit_phi] Recorded header snapshot for block {:?}", branch_source_block);
eprintln!(
"[DEBUG/exit_phi] Recorded header snapshot for block {:?}",
branch_source_block
);
}
}
@ -606,7 +634,7 @@ impl LoopFormBuilder {
branch_source_block,
&header_vals,
&filtered_snapshots,
&exit_preds_vec, // ← 実際のCFG predecessorsを渡す
&exit_preds_vec, // ← 実際のCFG predecessorsを渡す
&pinned_names,
&carrier_names,
inspector,
@ -620,15 +648,21 @@ impl LoopFormBuilder {
collector.sanitize();
if debug {
eprintln!("[DEBUG/exit_phi] Variable '{}': {} inputs", var_name, collector.len());
eprintln!(
"[DEBUG/exit_phi] Variable '{}': {} inputs",
var_name,
collector.len()
);
}
// Phase 25.2: optimize_same_value() で最適化判定
if let Some(same_val) = collector.optimize_same_value() {
// 全て同じ値 or 単一入力 → PHI 不要
if debug {
eprintln!("[DEBUG/exit_phi] Variable '{}': single/same value, direct binding to {:?}",
var_name, same_val);
eprintln!(
"[DEBUG/exit_phi] Variable '{}': single/same value, direct binding to {:?}",
var_name, same_val
);
}
ops.update_var(var_name, same_val);
} else {
@ -637,8 +671,12 @@ impl LoopFormBuilder {
let phi_id = ops.new_value();
if debug {
eprintln!("[DEBUG/exit_phi] Creating PHI {:?} for var '{}' with {} inputs",
phi_id, var_name, final_inputs.len());
eprintln!(
"[DEBUG/exit_phi] Creating PHI {:?} for var '{}' with {} inputs",
phi_id,
var_name,
final_inputs.len()
);
for (bb, val) in &final_inputs {
eprintln!("[DEBUG/exit_phi] PHI input: pred={:?} val={:?}", bb, val);
}
@ -672,7 +710,10 @@ pub trait LoopFormOps {
/// 📦 Get actual CFG predecessors for a block (Hotfix 6: PHI input validation)
/// Returns the set of blocks that actually branch to this block in the CFG.
/// Used to validate exit PHI inputs against actual control flow.
fn get_block_predecessors(&self, block: BasicBlockId) -> std::collections::HashSet<BasicBlockId>;
fn get_block_predecessors(
&self,
block: BasicBlockId,
) -> std::collections::HashSet<BasicBlockId>;
/// Phase 26-A-4: ValueIdベースのパラメータ判定型安全化
///
@ -724,7 +765,10 @@ pub fn build_exit_phis_for_control<O: LoopFormOps>(
loopform: &LoopFormBuilder,
ops: &mut O,
form: &crate::mir::control_form::ControlForm,
exit_snapshots: &[(crate::mir::BasicBlockId, std::collections::HashMap<String, crate::mir::ValueId>)],
exit_snapshots: &[(
crate::mir::BasicBlockId,
std::collections::HashMap<String, crate::mir::ValueId>,
)],
// Existing implementation requires branch_source_block, so we accept it as a parameter
branch_source_block: crate::mir::BasicBlockId,
) -> Result<(), String> {
@ -756,7 +800,13 @@ pub fn build_exit_phis_for_control<O: LoopFormOps>(
// - Records pinned/carrier definitions in header
// - Records filtered exit snapshots
// - Records header snapshot if it's an exit predecessor
loopform.build_exit_phis(ops, exit_id, branch_source_block, exit_snapshots, &mut inspector)
loopform.build_exit_phis(
ops,
exit_id,
branch_source_block,
exit_snapshots,
&mut inspector,
)
}
#[cfg(test)]
@ -812,7 +862,10 @@ mod tests {
true
}
fn get_block_predecessors(&self, _block: BasicBlockId) -> std::collections::HashSet<BasicBlockId> {
fn get_block_predecessors(
&self,
_block: BasicBlockId,
) -> std::collections::HashSet<BasicBlockId> {
// MockOps: return empty set (no CFG in test)
std::collections::HashSet::new()
}
@ -1008,14 +1061,8 @@ mod tests {
fn update_var(&mut self, _name: String, _value: ValueId) {}
fn get_variable_at_block(
&self,
name: &str,
block: BasicBlockId,
) -> Option<ValueId> {
self.vars_at_block
.get(&(block, name.to_string()))
.copied()
fn get_variable_at_block(&self, name: &str, block: BasicBlockId) -> Option<ValueId> {
self.vars_at_block.get(&(block, name.to_string())).copied()
}
}
@ -1035,7 +1082,7 @@ mod tests {
// Act: seal PHIs
use std::collections::HashSet;
let writes = HashSet::new(); // Empty writes for test
let writes = HashSet::new(); // Empty writes for test
builder
.seal_phis(&mut ops, latch, &continue_snapshots, &writes)
.expect("seal_phis should succeed");
@ -1044,14 +1091,14 @@ mod tests {
assert_eq!(ops.phi_updates.len(), 2);
// Helper to find inputs for a given phi id
let find_inputs = |phi_id: ValueId,
updates: &[(BasicBlockId, ValueId, Vec<(BasicBlockId, ValueId)>)]| {
updates
.iter()
.find(|(_, id, _)| *id == phi_id)
.map(|(_, _, inputs)| inputs.clone())
.expect("phi id not found in updates")
};
let find_inputs =
|phi_id: ValueId, updates: &[(BasicBlockId, ValueId, Vec<(BasicBlockId, ValueId)>)]| {
updates
.iter()
.find(|(_, id, _)| *id == phi_id)
.map(|(_, _, inputs)| inputs.clone())
.expect("phi id not found in updates")
};
let pinned_inputs = find_inputs(ValueId::new(20), &ops.phi_updates);
assert!(pinned_inputs.contains(&(preheader, ValueId::new(10))));

View File

@ -19,12 +19,12 @@ pub mod local_scope_inspector;
pub mod loop_var_classifier;
// Phase 26-B: Box-First Refactoring
pub mod phi_input_collector;
pub mod body_local_phi_builder;
pub mod phi_input_collector;
// Phase 26-C: Loop Snapshot & Header PHI Management
pub mod loop_snapshot_manager;
pub mod header_phi_builder;
pub mod loop_snapshot_manager;
// Phase 26-D: Exit PHI Management
pub mod exit_phi_builder;

View File

@ -44,9 +44,7 @@ impl PhiInputCollector {
/// # Returns
/// Empty collector ready to collect PHI inputs
pub fn new() -> Self {
Self {
inputs: Vec::new(),
}
Self { inputs: Vec::new() }
}
/// Add preheader input
@ -282,11 +280,14 @@ mod tests {
let inputs = collector.finalize();
// Should be sorted by BasicBlockId
assert_eq!(inputs, vec![
(BasicBlockId(2), ValueId(20)),
(BasicBlockId(3), ValueId(30)),
(BasicBlockId(5), ValueId(50)),
]);
assert_eq!(
inputs,
vec![
(BasicBlockId(2), ValueId(20)),
(BasicBlockId(3), ValueId(30)),
(BasicBlockId(5), ValueId(50)),
]
);
}
#[test]
@ -310,7 +311,7 @@ mod tests {
let mut collector = PhiInputCollector::new();
// Typical loop PHI scenario
collector.add_preheader(BasicBlockId(100), ValueId(0)); // init value
collector.add_preheader(BasicBlockId(100), ValueId(0)); // init value
// Continue snapshots
collector.add_snapshot(&[
@ -322,7 +323,7 @@ mod tests {
collector.add_latch(BasicBlockId(130), ValueId(3));
// Add duplicate to test sanitization
collector.add_preheader(BasicBlockId(100), ValueId(99)); // Duplicate
collector.add_preheader(BasicBlockId(100), ValueId(99)); // Duplicate
assert_eq!(collector.len(), 5);
@ -337,12 +338,15 @@ mod tests {
let inputs = collector.finalize();
// Should be sorted
assert_eq!(inputs, vec![
(BasicBlockId(100), ValueId(99)), // Last value for bb 100
(BasicBlockId(110), ValueId(1)),
(BasicBlockId(120), ValueId(2)),
(BasicBlockId(130), ValueId(3)),
]);
assert_eq!(
inputs,
vec![
(BasicBlockId(100), ValueId(99)), // Last value for bb 100
(BasicBlockId(110), ValueId(1)),
(BasicBlockId(120), ValueId(2)),
(BasicBlockId(130), ValueId(3)),
]
);
}
#[test]
@ -358,8 +362,8 @@ mod tests {
// Continue snapshots (idx updated in loop body)
collector.add_snapshot(&[
(BasicBlockId(210), ValueId(50)), // idx after first iteration
(BasicBlockId(220), ValueId(51)), // idx after second iteration
(BasicBlockId(210), ValueId(50)), // idx after first iteration
(BasicBlockId(220), ValueId(51)), // idx after second iteration
]);
collector.sanitize();