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:
nyash-codex
2025-12-04 15:00:45 +09:00
parent 11221aec29
commit 000335c32e
16 changed files with 1535 additions and 4 deletions

170
src/mir/cfg_extractor.rs Normal file
View 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);
}
}