diff --git a/docs/development/current/main/10-Now.md b/docs/development/current/main/10-Now.md index 40723dd7..d5eaa77c 100644 --- a/docs/development/current/main/10-Now.md +++ b/docs/development/current/main/10-Now.md @@ -2,14 +2,15 @@ ## Current Focus: Phase 29aa P4(Jump-chain propagation to Return) +**2025-12-27: Phase 29aa P4 完了** ✅ +- 目的: Jump の直列チェーン(単一 predecessor)を通して ReturnCleanup を成立させる(cleanup は Return block のみ) +- 入口: `docs/development/current/main/phases/phase-29aa/README.md` +- 次: Phase 29aa P5(候補: multi-predecessor/PHI/loop/early-exit の安全設計) + **2025-12-27: Phase 29aa P3 完了** ✅ - 目的: Jump→Return(単一 predecessor)で state 伝播し ReturnCleanup を成立 - 入口: `docs/development/current/main/phases/phase-29aa/README.md` -**2025-12-27: Phase 29aa P4 進行中** -- 目的: Jump の直列チェーン(単一 predecessor)を通して ReturnCleanup を成立させる(cleanup は Return block のみ) -- 入口: `docs/development/current/main/phases/phase-29aa/README.md` - **2025-12-27: Phase 29z P2 closeout** ✅ - `src/mir/passes/rc_insertion.rs`: `Store` 上書き + `Store null`(explicit drop)+ Return終端cleanup の最小 release 挿入(単一block・安全ガード) - 既定OFF: Cargo feature `rc-insertion-minimal`(env var 新設なし) diff --git a/docs/development/current/main/30-Backlog.md b/docs/development/current/main/30-Backlog.md index af467570..f99d2167 100644 --- a/docs/development/current/main/30-Backlog.md +++ b/docs/development/current/main/30-Backlog.md @@ -68,10 +68,11 @@ Related: - 入口: `docs/development/current/main/phases/phase-29z/README.md` - 指示書: `docs/development/current/main/phases/phase-29z/P0-RC_INSERTION_MINIMAL-INSTRUCTIONS.md` -- **Phase 29aa(P3 COMPLETE / P4 planned): RC insertion safety expansion(CFG-aware)** +- **Phase 29aa(P4 COMPLETE / P5 planned): RC insertion safety expansion(CFG-aware)** - 進捗: P2 ✅ 完了(Jump/Branch 終端で cleanup を入れない契約の SSOT 化) - 進捗: P3 ✅ 完了(Jump→Return single-predecessor state 伝播) - - 次: P4(Jump-chain propagation to Return) + - 進捗: P4 ✅ 完了(Jump-chain propagation to Return) + - 次: P5(multi-predecessor/PHI/loop/early-exit の安全設計) - 入口: `docs/development/current/main/phases/phase-29aa/README.md` - **Phase 29x(planned, post self-host): De-Rust runtime for LLVM execution** diff --git a/docs/development/current/main/phases/phase-29aa/README.md b/docs/development/current/main/phases/phase-29aa/README.md index deda572f..0601cf17 100644 --- a/docs/development/current/main/phases/phase-29aa/README.md +++ b/docs/development/current/main/phases/phase-29aa/README.md @@ -1,6 +1,6 @@ # Phase 29aa: RC insertion safety expansion(CFG-aware design) -Status: P3 Complete (Jump→Return single-predecessor propagation) +Status: P4 Complete (Jump-chain propagation to Return) Scope: Phase 29z の単一block限定実装から、誤releaseを起こさない形で CFG-aware に拡張するための設計を固める。 Entry: @@ -23,6 +23,7 @@ Progress: - P1: rc_insertion を RcPlan の Plan→Apply 2-stage へ分離(挙動不変) - P2: Jump/Branch 終端では cleanup を入れない契約を SSOT 化(Fail-Fast guard) - P3: Jump→Return(単一 predecessor)で state 伝播し ReturnCleanup を成立させる(P2維持) +- P4: Jump-chain(単一 predecessor 直列)で state 伝播し ReturnCleanup を成立させる(P2/P3 維持) P3 SSOT: - Contract: @@ -37,3 +38,17 @@ P3 SSOT: - quick 154/154 PASS 維持 - `cargo run --bin rc_insertion_selfcheck --features rc-insertion-minimal` PASS - 既定OFF維持(featureなしは no-op) + +P4 SSOT: +- Contract: + - cleanup は Return block の BeforeTerminator のみ(Jump/Branch block には入れない) + - Jump-chain(単一 predecessor の Jump 直列)のみ state 伝播を許可 + - cycle(loop)検出時は debug_assert! で Fail-Fast、伝播は停止 +- Non-goals: + - Branch/PHI/loop/early-exit の cleanup + - multi-predecessor の合流(PHI 問題回避) + - Jump block への release 挿入(P2維持) +- Acceptance: + - quick 154/154 PASS 維持 + - `cargo run --bin rc_insertion_selfcheck --features rc-insertion-minimal` PASS + - 既定OFF維持(featureなしは no-op) diff --git a/src/bin/rc_insertion_selfcheck.rs b/src/bin/rc_insertion_selfcheck.rs index 00cc72ca..a6ef3c9e 100644 --- a/src/bin/rc_insertion_selfcheck.rs +++ b/src/bin/rc_insertion_selfcheck.rs @@ -234,6 +234,52 @@ fn main() { "jump_return_single_pred", ); + // Case 3.6: Jump chain -> Return (single-predecessor only) + let ptr = ValueId::new(360); + let v1 = ValueId::new(22); + + let mut block_a = BasicBlock::new(BasicBlockId::new(0)); + block_a.instructions = vec![MirInstruction::Store { value: v1, ptr }]; + block_a.instruction_spans = vec![Span::unknown()]; + block_a.terminator = Some(MirInstruction::Jump { + target: BasicBlockId::new(1), + edge_args: None, + }); + block_a.terminator_span = Some(Span::unknown()); + + let mut block_b = BasicBlock::new(BasicBlockId::new(1)); + block_b.instructions = vec![]; + block_b.instruction_spans = vec![]; + block_b.terminator = Some(MirInstruction::Jump { + target: BasicBlockId::new(2), + edge_args: None, + }); + block_b.terminator_span = Some(Span::unknown()); + + let mut block_c = BasicBlock::new(BasicBlockId::new(2)); + block_c.instructions = vec![]; + block_c.instruction_spans = vec![]; + block_c.terminator = Some(MirInstruction::Return { value: None }); + block_c.terminator_span = Some(Span::unknown()); + + let module = build_module_with_blocks( + vec![block_a, block_b, block_c], + BasicBlockId::new(0), + "selfcheck_jump_chain_single_pred", + "selfcheck_mod3p6", + ); + assert_release_counts_in_blocks( + module, + "selfcheck_jump_chain_single_pred", + 1, + &[ + (BasicBlockId::new(0), 0), + (BasicBlockId::new(1), 0), + (BasicBlockId::new(2), 1), + ], + "jump_chain_single_pred", + ); + // Case 4: Jump terminator should NOT inject block-end cleanup (unsafe cross-block) let ptr = ValueId::new(400); let v1 = ValueId::new(30); diff --git a/src/mir/passes/rc_insertion.rs b/src/mir/passes/rc_insertion.rs index c6d6e997..b7704da0 100644 --- a/src/mir/passes/rc_insertion.rs +++ b/src/mir/passes/rc_insertion.rs @@ -126,33 +126,77 @@ pub fn insert_rc_instructions(module: &mut MirModule) -> RcInsertionStats { } } - let mut propagated_initial: HashMap> = - HashMap::new(); + let mut jump_chain_next: HashMap = HashMap::new(); for (bid, b) in &func.blocks { - if !matches!(b.terminator.as_ref(), Some(MirInstruction::Return { .. })) { - continue; - } - let preds = predecessors.get(bid).map(|p| p.as_slice()).unwrap_or(&[]); - if preds.len() != 1 { - continue; - } - let pred_id = preds[0]; - let Some(pred_block) = func.blocks.get(&pred_id) else { + let Some(MirInstruction::Jump { target, .. }) = b.terminator.as_ref() else { continue; }; - match pred_block.terminator.as_ref() { - Some(MirInstruction::Jump { target, .. }) if *target == *bid => { - let empty_state: HashMap = HashMap::new(); - let (_plan, out_state) = plan_rc_insertion_for_block( - &pred_block.instructions, - pred_block.terminator.as_ref(), - &empty_state, - ); - if !out_state.is_empty() { - propagated_initial.insert(*bid, out_state); - } + if !func.blocks.contains_key(target) { + continue; + } + let preds = predecessors.get(target).map(|p| p.as_slice()).unwrap_or(&[]); + if preds.len() == 1 { + debug_assert!( + preds[0] == *bid, + "rc_insertion: predecessor map mismatch for jump chain" + ); + jump_chain_next.insert(*bid, *target); + } + } + + let jump_chain_cycles = detect_jump_chain_cycles(&jump_chain_next); + if !jump_chain_cycles.is_empty() { + debug_assert!( + false, + "rc_insertion: jump-chain cycle detected; propagation disabled for cycle nodes" + ); + } + + let empty_state: HashMap = HashMap::new(); + let mut initial_state: HashMap> = HashMap::new(); + let max_iters = func.blocks.len().max(1); + for iter in 0..max_iters { + let mut changed = false; + for (bid, block) in &func.blocks { + let state_in = initial_state.get(bid).unwrap_or(&empty_state); + let (_plan, end_state) = plan_rc_insertion_for_block( + &block.instructions, + block.terminator.as_ref(), + state_in, + ); + + let Some(target) = jump_chain_next.get(bid).copied() else { + continue; + }; + if jump_chain_cycles.contains(bid) || jump_chain_cycles.contains(&target) { + continue; } - _ => {} + if end_state.is_empty() { + if initial_state.remove(&target).is_some() { + changed = true; + } + continue; + } + + let needs_update = match initial_state.get(&target) { + Some(existing) => existing != &end_state, + None => true, + }; + if needs_update { + initial_state.insert(target, end_state); + changed = true; + } + } + + if !changed { + break; + } + if iter + 1 == max_iters { + debug_assert!( + false, + "rc_insertion: jump-chain propagation did not converge; possible cycle" + ); + break; } } @@ -171,28 +215,34 @@ pub fn insert_rc_instructions(module: &mut MirModule) -> RcInsertionStats { spans.push(Span::unknown()); } - let empty_state: HashMap = HashMap::new(); - let initial_state = propagated_initial.get(bid); - if let Some(state) = initial_state { + let initial_state_for_block = initial_state.get(bid); + if let Some(state) = initial_state_for_block { let pred_count = predecessors.get(bid).map(|p| p.len()).unwrap_or(0); debug_assert!( pred_count == 1, "rc_insertion: initial state requires single predecessor" ); debug_assert!( - matches!(terminator.as_ref(), Some(MirInstruction::Return { .. })), - "rc_insertion: initial state must target Return block" + matches!(terminator.as_ref(), Some(MirInstruction::Return { .. })) + || matches!(terminator.as_ref(), Some(MirInstruction::Jump { .. })), + "rc_insertion: initial state only allowed for Jump/Return blocks" ); debug_assert!( !state.is_empty(), - "rc_insertion: initial state for Return block must be non-empty" + "rc_insertion: initial state must be non-empty" ); + if matches!(terminator.as_ref(), Some(MirInstruction::Jump { .. })) { + debug_assert!( + jump_chain_next.contains_key(bid), + "rc_insertion: jump-chain propagation requires single-predecessor Jump" + ); + } } let (plan, _end_state) = plan_rc_insertion_for_block( &insts, terminator.as_ref(), - initial_state.unwrap_or(&empty_state), + initial_state_for_block.unwrap_or(&empty_state), ); let (new_insts, new_spans, new_terminator, new_terminator_span) = apply_rc_plan(insts, spans, terminator, terminator_span, plan, &mut stats); @@ -208,6 +258,49 @@ pub fn insert_rc_instructions(module: &mut MirModule) -> RcInsertionStats { } } +#[cfg(feature = "rc-insertion-minimal")] +fn detect_jump_chain_cycles( + jump_chain_next: &HashMap, +) -> HashSet { + let mut cycles: HashSet = HashSet::new(); + let mut done: HashSet = HashSet::new(); + + for start in jump_chain_next.keys() { + if done.contains(start) { + continue; + } + let mut path: Vec = Vec::new(); + let mut index: HashMap = HashMap::new(); + let mut current = *start; + + loop { + if done.contains(¤t) { + break; + } + if let Some(&pos) = index.get(¤t) { + for node in &path[pos..] { + cycles.insert(*node); + } + break; + } + + index.insert(current, path.len()); + path.push(current); + + let Some(next) = jump_chain_next.get(¤t) else { + break; + }; + current = *next; + } + + for node in path { + done.insert(node); + } + } + + cycles +} + #[cfg(feature = "rc-insertion-minimal")] fn plan_rc_insertion_for_block( insts: &[MirInstruction],