From ca46cf842e9bfafc3ce31448b38d1756eef334b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=88=B1=E7=89=B9=E6=AF=8F=E7=BE=8A=E9=98=9F?= Date: Thu, 6 Aug 2026 14:09:03 +0800 Subject: [PATCH] =?UTF-8?q?test(cfg):=20=E5=90=84=20Pass=20=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=E8=A1=A5=E6=A8=A1=E5=9D=97=E7=BA=A7=E5=8D=95=E6=B5=8B?= =?UTF-8?q?=EF=BC=8C=E6=B5=8B=E8=AF=95=E9=9A=8F=E5=AE=9E=E7=8E=B0=E6=8B=86?= =?UTF-8?q?=E5=88=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_cfg/src/exception.rs | 48 ++++++++++++++++++++ crates/oxide_cfg/src/finalize.rs | 35 +++++++++++++++ crates/oxide_cfg/src/partition.rs | 61 ++++++++++++++++++++++++++ crates/oxide_cfg/src/split.rs | 73 +++++++++++++++++++++++++++++++ 4 files changed, 217 insertions(+) diff --git a/crates/oxide_cfg/src/exception.rs b/crates/oxide_cfg/src/exception.rs index 967fc4f..ea36b15 100644 --- a/crates/oxide_cfg/src/exception.rs +++ b/crates/oxide_cfg/src/exception.rs @@ -33,3 +33,51 @@ pub(super) fn add_exception_edges(f: &IRFunction, blocks: &mut [BasicBlock]) { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::BasicBlock; + use oxide_ir::inst::Inst; + + /// TRY_BEGIN 所在 BB 出发 Exception 边 → catch 入口块(D-07/D-08 仅起始 BB)。 + #[test] + fn try_begin_emits_exception_edge_to_handler() { + let mut f = IRFunction::new(); + f.insts.push(Inst::try_begin(0)); // 0: TRY_BEGIN → catch(label 0 → inst 2) + f.insts.push(Inst::new(OpCode::NOP, Operand::None, Operand::None, Operand::None)); // 1 + f.insts.push(Inst::new(OpCode::NOP, Operand::None, Operand::None, Operand::None)); // 2: catch 入口 + f.label_pos = vec![Some(2)]; + f.label_count = 1; + + let mut blocks = vec![ + BasicBlock { inst_range: 0..2, preds: Vec::new(), succs: Vec::new() }, + BasicBlock { inst_range: 2..3, preds: Vec::new(), succs: Vec::new() }, + ]; + add_exception_edges(&f, &mut blocks); + assert_eq!(blocks[0].succs, vec![(1, EdgeKind::Exception)], "TRY_BEGIN 所在 BB 出 Exception 边"); + assert!(blocks[1].succs.is_empty(), "catch 入口块无出边"); + } + + /// 块内多个 TRY 标记指向同一目标:Exception 边去重(Pitfall 4)。 + #[test] + fn duplicate_exception_targets_deduplicated() { + let mut f = IRFunction::new(); + f.insts.push(Inst::try_begin(0)); // 0: TRY_BEGIN → L0 + f.insts.push(Inst::try_finally_begin(0)); // 1: TRY_FINALLY_BEGIN → 同一 L0 + f.insts.push(Inst::new(OpCode::NOP, Operand::None, Operand::None, Operand::None)); // 2: L0 目标 + f.label_pos = vec![Some(2)]; + f.label_count = 1; + + let mut blocks = vec![ + BasicBlock { inst_range: 0..2, preds: Vec::new(), succs: Vec::new() }, + BasicBlock { inst_range: 2..3, preds: Vec::new(), succs: Vec::new() }, + ]; + add_exception_edges(&f, &mut blocks); + assert_eq!( + blocks[0].succs, + vec![(1, EdgeKind::Exception)], + "两个 TRY 标记指向同一目标只保留一条 Exception 边" + ); + } +} diff --git a/crates/oxide_cfg/src/finalize.rs b/crates/oxide_cfg/src/finalize.rs index 40365e8..6d45b38 100644 --- a/crates/oxide_cfg/src/finalize.rs +++ b/crates/oxide_cfg/src/finalize.rs @@ -19,3 +19,38 @@ pub(super) fn finalize(mut blocks: Vec, exit_id: usize) -> Cfg { Cfg { blocks, entry: 0, exit: exit_id } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::{BasicBlock, EdgeKind}; + + /// exit 哨兵恒在 blocks 末尾(空块 0..0),preds 反推正确。 + #[test] + fn exit_sentinel_appended_with_preds_reversed() { + let blocks_in = vec![ + BasicBlock { inst_range: 0..1, preds: Vec::new(), succs: vec![(1, EdgeKind::Jump)] }, + BasicBlock { inst_range: 1..2, preds: Vec::new(), succs: vec![(2, EdgeKind::Fallthrough)] }, + ]; + let cfg = finalize(blocks_in, 2); + assert_eq!(cfg.blocks.len(), 3, "2 实块 + exit 哨兵"); + assert_eq!(cfg.entry, 0); + assert_eq!(cfg.exit, 2); + assert_eq!(cfg.blocks[2].inst_range, 0..0, "exit 是空块"); + assert_eq!(cfg.blocks[1].preds, vec![0], "块 1 preds 反推自块 0 的 Jump 边"); + assert_eq!(cfg.blocks[2].preds, vec![1], "exit preds 反推自块 1 的 Fallthrough 边"); + } + + /// succs/preds 对称:total 边数一致(Pitfall 4 防御性约束)。 + #[test] + fn preds_and_succs_symmetric() { + let blocks_in = vec![ + BasicBlock { inst_range: 0..1, preds: Vec::new(), succs: vec![(1, EdgeKind::Jump)] }, + BasicBlock { inst_range: 1..2, preds: Vec::new(), succs: vec![(2, EdgeKind::Fallthrough)] }, + ]; + let cfg = finalize(blocks_in, 2); + let succs_total: usize = cfg.blocks.iter().map(|b| b.succs.len()).sum(); + let preds_total: usize = cfg.blocks.iter().map(|b| b.preds.len()).sum(); + assert_eq!(succs_total, preds_total, "succs/preds 对称"); + } +} diff --git a/crates/oxide_cfg/src/partition.rs b/crates/oxide_cfg/src/partition.rs index b2c3be7..a7b396e 100644 --- a/crates/oxide_cfg/src/partition.rs +++ b/crates/oxide_cfg/src/partition.rs @@ -34,3 +34,64 @@ pub(super) fn partition_blocks(f: &IRFunction) -> Vec { } heads } + +#[cfg(test)] +mod tests { + use super::*; + use oxide_ir::inst::Inst; + + /// 线性无跳转函数:仅 entry 为块头,其余位置非块头。 + #[test] + fn linear_function_only_entry_is_head() { + let mut f = IRFunction::new(); + f.insts.push(Inst::new(OpCode::NOP, Operand::None, Operand::None, Operand::None)); + f.insts.push(Inst::new(OpCode::NOP, Operand::None, Operand::None, Operand::None)); + f.insts.push(Inst::new(OpCode::RETURN, Operand::None, Operand::None, Operand::None)); + + let heads = partition_blocks(&f); + assert_eq!(heads, vec![true, false, false, false], "仅 inst 0 是块头"); + } + + /// label 目标是块头:`label_pos` 指向的指令位置置 heads。 + #[test] + fn label_target_is_head() { + let mut f = IRFunction::new(); + f.insts.push(Inst::new(OpCode::NOP, Operand::None, Operand::None, Operand::None)); // 0 + f.insts.push(Inst::new(OpCode::NOP, Operand::None, Operand::None, Operand::None)); // 1: label 0 目标 + f.insts.push(Inst::new(OpCode::RETURN, Operand::None, Operand::None, Operand::None)); // 2 + f.label_pos = vec![Some(1)]; + f.label_count = 1; + + let heads = partition_blocks(&f); + assert!(heads[0] && heads[1], "entry 与 label 目标都是块头"); + assert!(!heads[2], "非块头位置保持 false"); + } + + /// 条件跳转的 fallthrough 后继是块头(否则 cond+then 融块、条件边丢失)。 + #[test] + fn conditional_jump_fallthrough_successor_is_head() { + let mut f = IRFunction::new(); + f.insts.push(Inst::jmp_if_false(1, 0)); // 0: cond → L0(else) + f.insts.push(Inst::new(OpCode::NOP, Operand::None, Operand::None, Operand::None)); // 1: then + f.insts.push(Inst::new(OpCode::RETURN, Operand::None, Operand::None, Operand::None)); // 2 + f.label_pos = vec![Some(2)]; + f.label_count = 1; + + let heads = partition_blocks(&f); + assert!(heads[1], "条件跳转 fallthrough 后继是块头"); + } + + /// 无条件 JMP 的后继位置成块头(JMP 不 fallthrough,后续代码需独立成块,修复 Jump 边丢失)。 + #[test] + fn unconditional_jmp_successor_is_head() { + let mut f = IRFunction::new(); + f.insts.push(Inst::jmp(0)); // 0: → L0 + f.insts.push(Inst::new(OpCode::NOP, Operand::None, Operand::None, Operand::None)); // 1: 独立块头 + f.insts.push(Inst::new(OpCode::RETURN, Operand::None, Operand::None, Operand::None)); // 2 + f.label_pos = vec![Some(2)]; + f.label_count = 1; + + let heads = partition_blocks(&f); + assert!(heads[1], "无条件 JMP 后继位置是块头"); + } +} diff --git a/crates/oxide_cfg/src/split.rs b/crates/oxide_cfg/src/split.rs index 3f4f1e3..8ea0d88 100644 --- a/crates/oxide_cfg/src/split.rs +++ b/crates/oxide_cfg/src/split.rs @@ -74,3 +74,76 @@ pub(super) fn split_and_edges(f: &IRFunction, heads: &[bool]) -> (Vec usize { blocks.partition_point(|b| b.inst_range.start <= p) - 1 } + +#[cfg(test)] +mod tests { + use super::*; + use oxide_ir::inst::Inst; + + /// JMP → Jump 边指向 label 目标块。 + #[test] + fn unconditional_jmp_produces_jump_edge() { + let mut f = IRFunction::new(); + f.insts.push(Inst::jmp(0)); // 0: → L0 + f.insts.push(Inst::new(OpCode::RETURN, Operand::None, Operand::None, Operand::None)); // 1: L0 目标 + f.label_pos = vec![Some(1)]; + f.label_count = 1; + + // heads: 0(entry)+ 1(label 目标)+ JMP 后继 1 + let heads = crate::partition::partition_blocks(&f); + let (blocks, _exit_id) = split_and_edges(&f, &heads); + assert_eq!(blocks[0].succs, vec![(1, EdgeKind::Jump)], "JMP 产出 Jump 边"); + assert_eq!(blocks[1].succs, vec![(2, EdgeKind::Fallthrough)], "RETURN 汇 exit"); + } + + /// 条件跳转双出边:Jump → 目标块 + Fallthrough → 下一块。 + #[test] + fn conditional_jump_dual_edges() { + let mut f = IRFunction::new(); + f.insts.push(Inst::jmp_if_false(1, 0)); // 0: cond → L0(else) + f.insts.push(Inst::new(OpCode::NOP, Operand::None, Operand::None, Operand::None)); // 1: then + f.insts.push(Inst::new(OpCode::RETURN, Operand::None, Operand::None, Operand::None)); // 2: else 入口 + f.label_pos = vec![Some(2)]; + f.label_count = 1; + + let heads = crate::partition::partition_blocks(&f); + let (blocks, _exit_id) = split_and_edges(&f, &heads); + assert_eq!( + blocks[0].succs, + vec![(2, EdgeKind::Jump), (1, EdgeKind::Fallthrough)], + "条件跳转双出边:Jump→else + Fallthrough→then" + ); + } + + /// 非 terminator 块尾:Fallthrough → 下一块;RETURN 汇 exit 哨兵。 + #[test] + fn non_terminator_fallthrough_and_return_to_exit() { + let mut f = IRFunction::new(); + f.insts.push(Inst::new(OpCode::NOP, Operand::None, Operand::None, Operand::None)); // 0 + f.insts.push(Inst::new(OpCode::NOP, Operand::None, Operand::None, Operand::None)); // 1 + f.insts.push(Inst::new(OpCode::RETURN, Operand::None, Operand::None, Operand::None)); // 2 + + let heads = crate::partition::partition_blocks(&f); + let (blocks, exit_id) = split_and_edges(&f, &heads); + assert_eq!(blocks.len(), 1, "线性函数单块"); + assert_eq!(exit_id, 1, "exit 哨兵 id = 实块数"); + assert_eq!(blocks[0].succs, vec![(1, EdgeKind::Fallthrough)], "RETURN Fallthrough 汇 exit"); + } + + /// exit_id 恒为实块数(split 的 exit 约定,finalize 追加哨兵)。 + #[test] + fn exit_id_equals_block_count() { + let mut f = IRFunction::new(); + f.insts.push(Inst::jmp_if_false(1, 0)); + f.insts.push(Inst::call(Operand::Reg(2), Operand::Reg(0), Operand::Reg(3), 1)); + f.insts.push(Inst::jmp(1)); + f.insts.push(Inst::call(Operand::Reg(4), Operand::Reg(0), Operand::Reg(3), 1)); + f.insts.push(Inst::new(OpCode::RETURN, Operand::None, Operand::None, Operand::None)); + f.label_pos = vec![Some(3), Some(4)]; + f.label_count = 2; + + let heads = crate::partition::partition_blocks(&f); + let (blocks, exit_id) = split_and_edges(&f, &heads); + assert_eq!(exit_id, blocks.len(), "exit_id = 实块数(4 实块)"); + } +}