feat(hako_check): Phase 154 MIR CFG integration & HC020 dead block detection
Implements block-level unreachable code detection using MIR CFG information. Complements Phase 153's method-level HC019 with fine-grained analysis. Core Infrastructure (Complete): - CFG Extractor: Extract block reachability from MirModule - DeadBlockAnalyzerBox: HC020 rule for unreachable blocks - CLI Integration: --dead-blocks flag and rule execution - Test Cases: 4 comprehensive patterns (early return, constant false, infinite loop, break) - Smoke Test: Validation script for all test cases Implementation Details: - src/mir/cfg_extractor.rs: New module for CFG→JSON extraction - tools/hako_check/rules/rule_dead_blocks.hako: HC020 analyzer box - tools/hako_check/cli.hako: Added --dead-blocks flag and HC020 integration - apps/tests/hako_check/test_dead_blocks_*.hako: 4 test cases Architecture: - Follows Phase 153 boxed modular pattern (DeadCodeAnalyzerBox) - Optional CFG field in Analysis IR (backward compatible) - Uses MIR's built-in reachability computation - Gracefully skips if CFG unavailable Known Limitation: - CFG data bridge pending (Phase 155): analysis_consumer.hako needs MIR access - Current: DeadBlockAnalyzerBox implemented, but CFG not yet in Analysis IR - Estimated 2-3 hours to complete bridge in Phase 155 Test Coverage: - Unit tests: cfg_extractor (simple CFG, unreachable blocks) - Integration tests: 4 test cases ready (will activate with bridge) - Smoke test: tools/hako_check_deadblocks_smoke.sh Documentation: - phase154_mir_cfg_inventory.md: CFG structure investigation - phase154_implementation_summary.md: Complete implementation guide - hako_check_design.md: HC020 rule documentation Next Phase 155: - Implement CFG data bridge (extract_mir_cfg builtin) - Update analysis_consumer.hako to call bridge - Activate HC020 end-to-end testing 🤖 Generated with Claude Code (https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
170
src/mir/cfg_extractor.rs
Normal file
170
src/mir/cfg_extractor.rs
Normal file
@ -0,0 +1,170 @@
|
||||
/*!
|
||||
* MIR CFG Extractor - Extract Control Flow Graph information for analysis
|
||||
*
|
||||
* Phase 154: Provides CFG data to hako_check for dead block detection
|
||||
*/
|
||||
|
||||
use super::{MirFunction, MirInstruction, MirModule};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
/// Extract CFG information from MIR module as JSON
|
||||
///
|
||||
/// Output format:
|
||||
/// ```json
|
||||
/// {
|
||||
/// "functions": [
|
||||
/// {
|
||||
/// "name": "Main.main/0",
|
||||
/// "entry_block": 0,
|
||||
/// "blocks": [
|
||||
/// {
|
||||
/// "id": 0,
|
||||
/// "reachable": true,
|
||||
/// "successors": [1, 2],
|
||||
/// "terminator": "Branch"
|
||||
/// }
|
||||
/// ]
|
||||
/// }
|
||||
/// ]
|
||||
/// }
|
||||
/// ```
|
||||
pub fn extract_cfg_info(module: &MirModule) -> Value {
|
||||
let mut functions = Vec::new();
|
||||
|
||||
for (_func_id, function) in &module.functions {
|
||||
functions.push(extract_function_cfg(function));
|
||||
}
|
||||
|
||||
json!({
|
||||
"functions": functions
|
||||
})
|
||||
}
|
||||
|
||||
/// Extract CFG info for a single function
|
||||
fn extract_function_cfg(function: &MirFunction) -> Value {
|
||||
let mut blocks = Vec::new();
|
||||
|
||||
for (block_id, block) in &function.blocks {
|
||||
// Extract successor IDs
|
||||
let successors: Vec<u32> = block.successors.iter().map(|id| id.0).collect();
|
||||
|
||||
// Determine terminator type
|
||||
let terminator_name = match &block.terminator {
|
||||
Some(inst) => terminator_to_string(inst),
|
||||
None => "None".to_string(),
|
||||
};
|
||||
|
||||
blocks.push(json!({
|
||||
"id": block_id.0,
|
||||
"reachable": block.reachable,
|
||||
"successors": successors,
|
||||
"terminator": terminator_name
|
||||
}));
|
||||
}
|
||||
|
||||
// Sort blocks by ID for deterministic output
|
||||
blocks.sort_by_key(|b| b["id"].as_u64().unwrap_or(0));
|
||||
|
||||
json!({
|
||||
"name": function.signature.name,
|
||||
"entry_block": function.entry_block.0,
|
||||
"blocks": blocks
|
||||
})
|
||||
}
|
||||
|
||||
/// Convert terminator instruction to string name
|
||||
fn terminator_to_string(inst: &MirInstruction) -> String {
|
||||
match inst {
|
||||
MirInstruction::Branch { .. } => "Branch".to_string(),
|
||||
MirInstruction::Jump { .. } => "Jump".to_string(),
|
||||
MirInstruction::Return { .. } => "Return".to_string(),
|
||||
_ => "Unknown".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::mir::{BasicBlock, BasicBlockId, MirFunction, MirModule, MirSignature};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[test]
|
||||
fn test_extract_simple_cfg() {
|
||||
let mut module = MirModule::new("test");
|
||||
|
||||
// Create simple function with 2 blocks
|
||||
let mut function = MirFunction::new(MirSignature::new("test_fn".to_string()));
|
||||
function.entry_block = BasicBlockId(0);
|
||||
|
||||
let mut block0 = BasicBlock::new(BasicBlockId(0));
|
||||
block0.reachable = true;
|
||||
block0.successors.insert(BasicBlockId(1));
|
||||
block0.terminator = Some(MirInstruction::Jump {
|
||||
target: BasicBlockId(1),
|
||||
});
|
||||
|
||||
let mut block1 = BasicBlock::new(BasicBlockId(1));
|
||||
block1.reachable = true;
|
||||
block1.terminator = Some(MirInstruction::Return { value: None });
|
||||
|
||||
function.blocks.insert(BasicBlockId(0), block0);
|
||||
function.blocks.insert(BasicBlockId(1), block1);
|
||||
|
||||
module.functions.insert("test_fn".to_string(), function);
|
||||
|
||||
// Extract CFG
|
||||
let cfg = extract_cfg_info(&module);
|
||||
|
||||
// Verify structure
|
||||
assert!(cfg["functions"].is_array());
|
||||
let functions = cfg["functions"].as_array().unwrap();
|
||||
assert_eq!(functions.len(), 1);
|
||||
|
||||
let func = &functions[0];
|
||||
assert_eq!(func["name"], "test_fn");
|
||||
assert_eq!(func["entry_block"], 0);
|
||||
|
||||
let blocks = func["blocks"].as_array().unwrap();
|
||||
assert_eq!(blocks.len(), 2);
|
||||
|
||||
// Check block 0
|
||||
assert_eq!(blocks[0]["id"], 0);
|
||||
assert_eq!(blocks[0]["reachable"], true);
|
||||
assert_eq!(blocks[0]["terminator"], "Jump");
|
||||
assert_eq!(blocks[0]["successors"].as_array().unwrap(), &[json!(1)]);
|
||||
|
||||
// Check block 1
|
||||
assert_eq!(blocks[1]["id"], 1);
|
||||
assert_eq!(blocks[1]["reachable"], true);
|
||||
assert_eq!(blocks[1]["terminator"], "Return");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unreachable_block() {
|
||||
let mut module = MirModule::new("test");
|
||||
|
||||
let mut function = MirFunction::new(MirSignature::new("test_dead".to_string()));
|
||||
function.entry_block = BasicBlockId(0);
|
||||
|
||||
let mut block0 = BasicBlock::new(BasicBlockId(0));
|
||||
block0.reachable = true;
|
||||
block0.terminator = Some(MirInstruction::Return { value: None });
|
||||
|
||||
// Unreachable block
|
||||
let mut block1 = BasicBlock::new(BasicBlockId(1));
|
||||
block1.reachable = false; // Marked as unreachable
|
||||
block1.terminator = Some(MirInstruction::Return { value: None });
|
||||
|
||||
function.blocks.insert(BasicBlockId(0), block0);
|
||||
function.blocks.insert(BasicBlockId(1), block1);
|
||||
|
||||
module.functions.insert("test_dead".to_string(), function);
|
||||
|
||||
let cfg = extract_cfg_info(&module);
|
||||
let blocks = cfg["functions"][0]["blocks"].as_array().unwrap();
|
||||
|
||||
// Find unreachable block
|
||||
let dead_block = blocks.iter().find(|b| b["id"] == 1).unwrap();
|
||||
assert_eq!(dead_block["reachable"], false);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user