//! # Анализы поверх графа потока управления
//!
//! Структурные проверки, для которых одного линейного прохода по AST
//! недостаточно — нужен граф связей между базовыми блоками: чтобы понять,
//! ссылается ли хоть что-нибудь в программе на данную метку, нужно
//! рассмотреть переходы со всех остальных блоков, а не только соседние
//! инструкции.
//!
//! Мёртвый код, видимый уже в пределах одного линейного участка (сразу
//! после `jmp`/`ret`, до следующей метки), сюда не входит — это AST-уровневая
//! находка (см. `ASTAnalyseWarningKind::DeadCodeAfterTerminator`), которая не
//! требует графа: она видна из одного прохода по `Statement`.
use crate::{
    analysis::air::cfg::{BasicBlock, BlockId, Cfg, Terminator},
    lexer::token::Span,
};

/// Метка, на которую нет ни одного статически известного перехода
/// откуда-либо в программе. В отличие от мёртвого кода, видимого в пределах
/// одного линейного участка, для этой находки нужно знать **все** переходы
/// программы — это и есть то, для чего действительно нужен граф.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct UnreferencedLabel {
    pub block: BlockId,
    /// Позиция первого элемента блока — куда указывать диагностику.
    pub span: Span,
}

fn block_span(block: &BasicBlock) -> Option<Span> {
    block.instructions.first().map(|i| i.span)
}

/// Находит блоки с меткой, на которую нет ни одного входящего рёбра во всём
/// графе. Блоки **без** метки, оказавшиеся без входящих рёбер (мёртвый код
/// сразу после `jmp`/`ret`), сюда не включаются — они уже покрыты
/// AST-анализатором и не требуют графа для обнаружения.
pub fn find_unreferenced_labels(cfg: &Cfg) -> Vec<UnreferencedLabel> {
    cfg.unreachable_blocks()
        .into_iter()
        .filter_map(|id| {
            let block = cfg.block(id);
            if block.labels.is_empty() {
                return None;
            }
            let span = block_span(block)?;
            Some(UnreferencedLabel { block: id, span })
        })
        .collect()
}

/// Блоки, заканчивающиеся переходом/вызовом с динамически вычисляемой
/// целью (`jmp rax`, `call [rax]`). Не ошибка сама по себе, но граф не
/// может проверить достижимость и дальнейших свойств кода за этой точкой —
/// это структурное свойство терминатора блока в графе.
pub fn find_indirect_exits(cfg: &Cfg) -> Vec<BlockId> {
    cfg.blocks
        .iter()
        .filter(|b| matches!(b.terminator, Terminator::Indirect))
        .map(|b| b.id)
        .collect()
}

/// Блоки, у которых нет явного `ret`/перехода в конце — они заканчиваются
/// вместе с программой (`Terminator::FallOff`).
pub fn find_fall_off_blocks(cfg: &Cfg) -> Vec<BlockId> {
    cfg.blocks
        .iter()
        .filter(|b| matches!(b.terminator, Terminator::FallOff))
        .map(|b| b.id)
        .collect()
}
