feat(edgecfg): Phase 281 P3 - cleanup Normal wiring + docs

This commit is contained in:
2025-12-23 04:11:02 +09:00
parent 2d5607930c
commit a744be929a
9 changed files with 675 additions and 111 deletions

View File

@ -381,35 +381,134 @@ pub(crate) fn loop_(
}
}
/// cleanup 合成: finally の後継(すべての exit を正規化)
/// Phase 281 P3: cleanup() Normal + Return exit wiring implementation
///
/// # Phase 280: Composition SSOT
/// Wires cleanup Normal/Return exits to specified targets or propagates them upward.
///
/// ## Planned Composition Law (Future: Phase 280+)
/// # Contract (P3 Implementation)
///
/// - All exits (Normal/Break/Continue/Return/Unwind) → `cleanup` (`EdgeStub` → `wires`)
/// - Cleanup re-dispatches original exit (`ExitTag` + payload via block params)
/// - `Invoke.err` also routed through cleanup
/// **Input**:
/// - `main`: Main control flow (loop structure Frag)
/// - `cleanup_frag`: Exit handler (Normal/Return exits only, no wires/branches)
/// - `normal_target`: Where to wire Normal exits
/// - `Some(bb)`: Wire Normal → bb (internal closure, target = Some)
/// - `None`: Propagate Normal → wires (upward propagation, target = None)
/// - `ret_target`: Where to wire Return exits
/// - `Some(bb)`: Wire Return → bb (internal closure, target = Some)
/// - `None`: Propagate Return → wires (upward propagation, target = None)
///
/// ## Status
/// **Output**:
/// - Frag with main's structure + cleanup's exits wired/propagated
///
/// - **Signature fixed**: Phase 264
/// - **Implementation**: TODO (Phase 280+)
/// - **Usage**: Not yet used (planned for exception/async cleanup)
/// **Invariants**:
/// - 1 block = 1 terminator (no duplicate BranchStubs)
/// - cleanup_frag must have empty wires/branches (Fail-Fast if not)
/// - cleanup_frag.exits must contain only Normal/Return (Fail-Fast for other kinds)
/// - normal_target=Some: Normal exits → wires (internal)
/// - normal_target=None: Normal exits → wires (target=None, propagate upward)
/// - ret_target=Some: Return exits → wires (internal)
/// - ret_target=None: Return exits → wires (target=None, propagate upward)
///
/// # 配線ルールTODO実装
/// - body の全 exit を cleanup 経由へリライト
/// - cleanup 後に元の exit を再発射ExitTag + payload を block params で運ぶ)
/// - Invoke.err も cleanup に寄せる
/// # Implementation Status
///
/// # 引数
/// - `body`: 本体の断片
/// - `cleanup_block`: cleanup 処理を行うブロック
/// P3: Normal + Return wiring logic implemented
/// Future: Break/Continue/Unwind support (P4+)
///
/// # Migration Notes (Phase 264 → Phase 281)
///
/// Old signature (Phase 264): `cleanup(body: Frag, cleanup_block: BasicBlockId) -> Frag`
/// Phase 281 P1: `cleanup(main: Frag, cleanup: Frag) -> Result<Frag, String>`
/// Phase 281 P2: `cleanup(main: Frag, cleanup_frag: Frag, ret_target: Option<BasicBlockId>) -> Result<Frag, String>`
/// Phase 281 P3: `cleanup(main: Frag, cleanup_frag: Frag, normal_target: Option<BasicBlockId>, ret_target: Option<BasicBlockId>) -> Result<Frag, String>`
///
/// Rationale: Pattern6/7 require flexible exit wiring for Normal/Return exits.
/// cleanup_frag must be "exit-only" to prevent terminator confusion.
pub(crate) fn cleanup(
_body: Frag,
_cleanup_block: BasicBlockId,
) -> Frag {
todo!("Phase 280+: cleanup 合成は将来実装予定exception/async cleanup 用)")
main: Frag,
cleanup_frag: Frag,
normal_target: Option<BasicBlockId>,
ret_target: Option<BasicBlockId>,
) -> Result<Frag, String> {
// Phase 281 P3: Normal + Return exit wiring implementation
// - Supported: Normal, Return exits
// - Unsupported: Break, Continue, Unwind (Fail-Fast)
let mut exits = BTreeMap::new();
let mut wires = Vec::new();
let mut branches = Vec::new();
// Validate cleanup_frag structure (only exits allowed, no wires/branches)
if !cleanup_frag.wires.is_empty() || !cleanup_frag.branches.is_empty() {
return Err(format!(
"compose::cleanup() Phase 281 P3: cleanup_frag must have empty wires/branches (only exits allowed), found {} wires, {} branches",
cleanup_frag.wires.len(),
cleanup_frag.branches.len()
));
}
// Validate cleanup_frag exits (only Normal + Return allowed in P3)
for (kind, _) in &cleanup_frag.exits {
match kind {
ExitKind::Normal | ExitKind::Return => {}, // OK
_ => {
return Err(format!(
"compose::cleanup() Phase 281 P3: unsupported exit kind {:?} in cleanup_frag (only Normal/Return allowed)",
kind
));
}
}
}
// Process cleanup Normal exits
if let Some(normal_stubs) = cleanup_frag.exits.get(&ExitKind::Normal) {
for mut stub in normal_stubs.clone() {
match normal_target {
Some(target_bb) => {
// Wire: Normal → target_bb (internal closure)
stub.target = Some(target_bb);
wires.push(stub);
}
None => {
// Propagate: Normal → wires (target=None, upward propagation)
stub.target = None;
wires.push(stub);
}
}
}
}
// Process cleanup Return exits
if let Some(return_stubs) = cleanup_frag.exits.get(&ExitKind::Return) {
for mut stub in return_stubs.clone() {
match ret_target {
Some(target_bb) => {
// Wire: Return → target_bb (internal closure)
stub.target = Some(target_bb);
wires.push(stub);
}
None => {
// Propagate: Return → wires (target=None, will be emitted as Return terminator)
// Note: Return exits can have target=None in wires (Phase 267 special case)
stub.target = None;
wires.push(stub);
}
}
}
}
// Preserve main's exits/wires/branches
for (kind, stubs) in main.exits {
exits.entry(kind).or_insert_with(Vec::new).extend(stubs);
}
wires.extend(main.wires);
branches.extend(main.branches);
Ok(Frag {
entry: main.entry, // Entry = main entry (header_bb)
exits,
wires,
branches,
})
}
#[cfg(test)]
@ -418,6 +517,7 @@ mod tests {
use crate::mir::basic_block::BasicBlockId;
use super::super::exit_kind::ExitKind;
use super::super::edge_stub::EdgeStub;
use crate::mir::join_ir::lowering::inline_boundary::JumpArgsLayout;
#[test]
fn test_loop_preserves_exits() {
@ -866,4 +966,200 @@ mod tests {
// then/else Normal are in wires
assert_eq!(if_frag.wires.len(), 2);
}
// Phase 281 P2: cleanup() test - Return propagation
#[test]
fn test_cleanup_return_propagation() {
let main_entry = BasicBlockId(100);
let cleanup_bb = BasicBlockId(200);
// Main Frag: empty (no exits)
let main_frag = Frag {
entry: main_entry,
exits: BTreeMap::new(),
wires: vec![],
branches: vec![],
};
// Cleanup Frag: Return exit
let cleanup_frag = Frag {
entry: cleanup_bb,
exits: BTreeMap::from([(
ExitKind::Return,
vec![EdgeStub {
from: cleanup_bb,
kind: ExitKind::Return,
target: None, // Unresolved
args: EdgeArgs {
layout: JumpArgsLayout::CarriersOnly,
values: vec![],
},
}],
)]),
wires: vec![],
branches: vec![],
};
// Execute: normal_target=None, ret_target=None → propagate Return
let result = cleanup(main_frag, cleanup_frag, None, None);
// Verify: Return in wires (target=None, to be emitted as terminator)
assert!(result.is_ok());
let composed = result.unwrap();
assert_eq!(composed.entry, main_entry);
assert_eq!(composed.wires.len(), 1); // Return in wires
let return_wire = &composed.wires[0];
assert_eq!(return_wire.from, cleanup_bb);
assert_eq!(return_wire.kind, ExitKind::Return);
assert_eq!(return_wire.target, None); // Unresolved (upward propagation)
}
// Phase 281 P2: cleanup() test - Return wiring
#[test]
fn test_cleanup_return_wiring() {
let main_entry = BasicBlockId(100);
let cleanup_bb = BasicBlockId(200);
let target_bb = BasicBlockId(300); // Wire destination
// Main Frag: empty
let main_frag = Frag {
entry: main_entry,
exits: BTreeMap::new(),
wires: vec![],
branches: vec![],
};
// Cleanup Frag: Return exit
let cleanup_frag = Frag {
entry: cleanup_bb,
exits: BTreeMap::from([(
ExitKind::Return,
vec![EdgeStub {
from: cleanup_bb,
kind: ExitKind::Return,
target: None, // Unresolved
args: EdgeArgs {
layout: JumpArgsLayout::CarriersOnly,
values: vec![],
},
}],
)]),
wires: vec![],
branches: vec![],
};
// Execute: normal_target=None, ret_target=Some(target_bb) → wire Return
let result = cleanup(main_frag, cleanup_frag, None, Some(target_bb));
// Verify: Return in wires (not exits), wired to target_bb
assert!(result.is_ok());
let composed = result.unwrap();
assert_eq!(composed.entry, main_entry);
assert_eq!(composed.exits.len(), 0); // No exits (closed)
assert_eq!(composed.wires.len(), 1); // Return wired
let wired_stub = &composed.wires[0];
assert_eq!(wired_stub.from, cleanup_bb);
assert_eq!(wired_stub.kind, ExitKind::Return);
assert_eq!(wired_stub.target, Some(target_bb)); // Wired!
}
// Phase 281 P3: cleanup() test - Normal propagation
#[test]
fn test_cleanup_normal_propagation() {
let main_entry = BasicBlockId(100);
let cleanup_bb = BasicBlockId(200);
// Main Frag: empty
let main_frag = Frag {
entry: main_entry,
exits: BTreeMap::new(),
wires: vec![],
branches: vec![],
};
// Cleanup Frag: Normal exit
let cleanup_frag = Frag {
entry: cleanup_bb,
exits: BTreeMap::from([(
ExitKind::Normal,
vec![EdgeStub {
from: cleanup_bb,
kind: ExitKind::Normal,
target: None, // Unresolved
args: EdgeArgs {
layout: JumpArgsLayout::CarriersOnly,
values: vec![],
},
}],
)]),
wires: vec![],
branches: vec![],
};
// Execute: normal_target=None, ret_target=None → propagate Normal
let result = cleanup(main_frag, cleanup_frag, None, None);
// Verify: Normal in wires (target=None, upward propagation)
assert!(result.is_ok());
let composed = result.unwrap();
assert_eq!(composed.entry, main_entry);
assert_eq!(composed.wires.len(), 1); // Normal in wires
let normal_wire = &composed.wires[0];
assert_eq!(normal_wire.from, cleanup_bb);
assert_eq!(normal_wire.kind, ExitKind::Normal);
assert_eq!(normal_wire.target, None); // Unresolved (upward propagation)
}
// Phase 281 P3: cleanup() test - Normal wiring
#[test]
fn test_cleanup_normal_wiring() {
let main_entry = BasicBlockId(100);
let cleanup_bb = BasicBlockId(200);
let target_bb = BasicBlockId(300); // Wire destination
// Main Frag: empty
let main_frag = Frag {
entry: main_entry,
exits: BTreeMap::new(),
wires: vec![],
branches: vec![],
};
// Cleanup Frag: Normal exit
let cleanup_frag = Frag {
entry: cleanup_bb,
exits: BTreeMap::from([(
ExitKind::Normal,
vec![EdgeStub {
from: cleanup_bb,
kind: ExitKind::Normal,
target: None, // Unresolved
args: EdgeArgs {
layout: JumpArgsLayout::CarriersOnly,
values: vec![],
},
}],
)]),
wires: vec![],
branches: vec![],
};
// Execute: normal_target=Some(target_bb), ret_target=None → wire Normal
let result = cleanup(main_frag, cleanup_frag, Some(target_bb), None);
// Verify: Normal in wires (not exits), wired to target_bb
assert!(result.is_ok());
let composed = result.unwrap();
assert_eq!(composed.entry, main_entry);
assert_eq!(composed.exits.len(), 0); // No exits (closed)
assert_eq!(composed.wires.len(), 1); // Normal wired
let wired_stub = &composed.wires[0];
assert_eq!(wired_stub.from, cleanup_bb);
assert_eq!(wired_stub.kind, ExitKind::Normal);
assert_eq!(wired_stub.target, Some(target_bb)); // Wired!
}
}

View File

@ -21,6 +21,7 @@ use crate::mir::{BinaryOp, CompareOp, ConstValue, Effect, EffectMask, MirType};
use crate::mir::builder::control_flow::edgecfg::api::{BranchStub, EdgeStub, ExitKind, Frag};
use crate::mir::basic_block::EdgeArgs;
use crate::mir::join_ir::lowering::inline_boundary::JumpArgsLayout;
use std::collections::BTreeMap;
/// Phase 273 P1: PlanNormalizer - DomainPlan → CorePlan 変換 (SSOT)
pub(in crate::mir::builder) struct PlanNormalizer;
@ -295,11 +296,13 @@ impl PlanNormalizer {
values: vec![i_current],
};
// Phase 280 TODO: Hand-rolled Frag construction for early exit pattern
// Reason: `found_bb` is early Return, doesn't fit compose::if_() model
// Future: Consider compose::cleanup() for early exit normalization (Phase 281+)
// Current structure: 2 BranchStub (header→body/after, body→found/step) + 2 EdgeStub (step→header, found→Return)
let branches = vec![
// Phase 281 P2: compose::cleanup() for early exit pattern
// Reason: `found_bb` is early Return, handled by cleanup() wiring
// Structure: main Frag (header, body, step) + cleanup Frag (found Return)
use crate::mir::builder::control_flow::edgecfg::api::compose;
// Build main Frag (loop structure: header, body, step)
let main_branches = vec![
BranchStub {
from: header_bb,
cond: cond_loop,
@ -311,31 +314,49 @@ impl PlanNormalizer {
BranchStub {
from: body_bb,
cond: cond_match,
then_target: found_bb,
then_target: found_bb, // Early exit (handled by cleanup)
then_args: empty_args.clone(),
else_target: step_bb,
else_args: empty_args.clone(),
},
];
let wires = vec![
let main_wires = vec![
EdgeStub {
from: step_bb,
kind: ExitKind::Normal,
target: Some(header_bb),
args: empty_args,
args: empty_args.clone(),
},
];
let main_frag = Frag {
entry: header_bb,
exits: BTreeMap::new(), // No exits from main (only wires)
wires: main_wires,
branches: main_branches,
};
// Build cleanup Frag (found_bb Return)
let cleanup_exits = vec![
EdgeStub {
from: found_bb,
kind: ExitKind::Return,
target: None,
target: None, // Will be wired by cleanup()
args: ret_found_args,
},
];
let mut frag = Frag::new(header_bb);
frag.branches = branches;
frag.wires = wires;
let cleanup_frag = Frag {
entry: found_bb, // cleanup entry (not used, but required)
exits: BTreeMap::from([(ExitKind::Return, cleanup_exits)]),
wires: vec![], // Return is in exits, not wires
branches: vec![],
};
// Compose! normal_target=None, ret_target=None → Return を上へ伝播
let frag = compose::cleanup(main_frag, cleanup_frag, None, None)
.expect("compose::cleanup() failed in normalize_scan_with_init");
// Step 11: Build final_values (Phase 273 P2)
let final_values = vec![(parts.loop_var.clone(), i_current)];
@ -381,6 +402,8 @@ impl PlanNormalizer {
ctx: &LoopPatternContext,
) -> Result<CorePlan, String> {
use crate::mir::builder::control_flow::joinir::trace;
use crate::mir::builder::control_flow::edgecfg::api::compose;
use std::collections::BTreeMap;
let trace_logger = trace::trace();
let debug = ctx.debug;
@ -637,6 +660,72 @@ impl PlanNormalizer {
},
];
// Step 9.5: Build Frags for compose::if_() (Phase 281 P0)
// Create empty_args for EdgeStub construction
let empty_args = EdgeArgs {
layout: JumpArgsLayout::CarriersOnly,
values: vec![],
};
// Build then_frag: entry=then_bb, Normal exit to step
// IMPORTANT: Use explicit EdgeStub construction with empty_args (not without_args)
let mut then_exits = BTreeMap::new();
then_exits.insert(
ExitKind::Normal,
vec![EdgeStub {
from: then_bb,
kind: ExitKind::Normal,
target: None, // Unresolved exit (compose will wire to join)
args: empty_args.clone(), // CarriersOnly layout
}],
);
let then_frag = Frag {
entry: then_bb,
exits: then_exits,
wires: vec![],
branches: vec![],
};
// Build else_frag: entry=else_bb, Normal exit to step
// IMPORTANT: Use explicit EdgeStub construction with empty_args (not without_args)
let mut else_exits = BTreeMap::new();
else_exits.insert(
ExitKind::Normal,
vec![EdgeStub {
from: else_bb,
kind: ExitKind::Normal,
target: None, // Unresolved exit (compose will wire to join)
args: empty_args.clone(), // CarriersOnly layout
}],
);
let else_frag = Frag {
entry: else_bb,
exits: else_exits,
wires: vec![],
branches: vec![],
};
// Build step_frag: entry=step_bb, EMPTY (no back-edge here)
// Reason: step→header back-edge is loop-level responsibility, not body_if_frag's
// The back-edge will be added manually in Step 12 (final merge)
let step_frag = Frag {
entry: step_bb,
exits: BTreeMap::new(),
wires: vec![], // Empty - no back-edge here
branches: vec![],
};
// Phase 281 P0: Use compose::if_() for body branch (cond_match: then/else → step)
let body_if_frag = compose::if_(
body_bb, // header: where cond_match is evaluated
cond_match, // condition ValueId
then_frag, // then branch
empty_args.clone(), // then_entry_args
else_frag, // else branch
empty_args.clone(), // else_entry_args
step_frag, // join target (step_bb)
);
// Step 10: Build block_effects (SSOT ordering: preheader, header, body, then, else, step)
let block_effects = vec![
(preheader_bb, vec![]), // No effects in preheader
@ -691,20 +780,12 @@ impl PlanNormalizer {
},
];
// Step 12: Build Frag (2 branches + 3 wires)
//
// Phase 280 TODO: Hand-rolled Frag construction for split scan pattern
// Target (Phase 281): compose::if_(body_bb, cond_match, then_frag, else_frag, step_frag)
// Reason deferred: 挙動不変保証が難しい、Phase 280 は SSOT positioning 優先
// Migration: Phase 281+ で compose::if_() への移行を検討
// Current structure: 2 BranchStub (header→body/after, body→then/else) + 3 EdgeStub (then→step, else→step, step→header)
let empty_args = EdgeArgs {
layout: JumpArgsLayout::CarriersOnly,
values: vec![],
};
let branches = vec![
// header -> body/after
// Step 12: Build Frag - Phase 281 P0 Complete
// - Header branch (hand-rolled): header → body/after
// - Body branch (composed): body → then/else → step (from compose::if_())
// - Step back-edge (hand-rolled): step → header
let mut branches = vec![
// Header branch (cond_loop) - hand-rolled
BranchStub {
from: header_bb,
cond: cond_loop,
@ -713,44 +794,34 @@ impl PlanNormalizer {
else_target: after_bb,
else_args: empty_args.clone(),
},
// body -> then/else
BranchStub {
from: body_bb,
cond: cond_match,
then_target: then_bb,
then_args: empty_args.clone(),
else_target: else_bb,
else_args: empty_args.clone(),
},
];
let wires = vec![
// then -> step
EdgeStub {
from: then_bb,
kind: ExitKind::Normal,
target: Some(step_bb),
args: empty_args.clone(),
},
// else -> step
EdgeStub {
from: else_bb,
kind: ExitKind::Normal,
target: Some(step_bb),
args: empty_args.clone(),
},
// step -> header (back-edge)
EdgeStub {
from: step_bb,
kind: ExitKind::Normal,
target: Some(header_bb),
args: empty_args,
},
];
// Merge body_if_frag branches (body → then/else)
branches.extend(body_if_frag.branches);
let mut frag = Frag::new(header_bb);
frag.branches = branches;
frag.wires = wires;
// Merge body_if_frag wires (then/else → step)
let mut wires = Vec::new();
wires.extend(body_if_frag.wires);
// Add step back-edge (hand-rolled) - loop-level responsibility
wires.push(EdgeStub {
from: step_bb,
kind: ExitKind::Normal,
target: Some(header_bb),
args: empty_args.clone(),
});
let mut exits = BTreeMap::new();
for (kind, stubs) in body_if_frag.exits {
exits.insert(kind, stubs);
}
let frag = Frag {
entry: header_bb,
exits,
wires,
branches,
};
// Step 13: Build final_values (i, start for post-loop)
let final_values = vec![