//! Finds locals which are assigned once to a const and unused except for debuginfo and converts
//! their debuginfo to use the const directly, allowing the local to be removed.

use rustc_middle::{
    mir::{
        visit::{PlaceContext, Visitor},
        Body, Constant, Local, Location, Operand, Rvalue, StatementKind, VarDebugInfoContents,
    },
    ty::TyCtxt,
};

use crate::MirPass;
use rustc_index::{bit_set::BitSet, IndexVec};

pub struct ConstDebugInfo;

impl<'tcx> MirPass<'tcx> for ConstDebugInfo {
    fn is_enabled(&self, sess: &rustc_session::Session) -> bool {
        sess.mir_opt_level() > 0
    }

    fn run_pass(&self, _tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) {
        trace!("running ConstDebugInfo on {:?}", body.source);

        for (local, constant) in find_optimization_opportunities(body) {
            for debuginfo in &mut body.var_debug_info {
                if let VarDebugInfoContents::Place(p) = debuginfo.value {
                    if p.local == local && p.projection.is_empty() {
                        trace!(
                            "changing debug info for {:?} from place {:?} to constant {:?}",
                            debuginfo.name,
                            p,
                            constant
                        );
                        debuginfo.value = VarDebugInfoContents::Const(constant);
                    }
                }
            }
        }
    }
}

struct LocalUseVisitor {
    local_mutating_uses: IndexVec<Local, u8>,
    local_assignment_locations: IndexVec<Local, Option<Location>>,
}

fn find_optimization_opportunities<'tcx>(body: &Body<'tcx>) -> Vec<(Local, Constant<'tcx>)> {
    let mut visitor = LocalUseVisitor {
        local_mutating_uses: IndexVec::from_elem(0, &body.local_decls),
        local_assignment_locations: IndexVec::from_elem(None, &body.local_decls),
    };

    visitor.visit_body(body);

    let mut locals_to_debuginfo = BitSet::new_empty(body.local_decls.len());
    for debuginfo in &body.var_debug_info {
        if let VarDebugInfoContents::Place(p) = debuginfo.value && let Some(l) = p.as_local() {
            locals_to_debuginfo.insert(l);
        }
    }

    let mut eligible_locals = Vec::new();
    for (local, mutating_uses) in visitor.local_mutating_uses.drain_enumerated(..) {
        if mutating_uses != 1 || !locals_to_debuginfo.contains(local) {
            continue;
        }

        if let Some(location) = visitor.local_assignment_locations[local] {
            let bb = &body[location.block];

            // The value is assigned as the result of a call, not a constant
            if bb.statements.len() == location.statement_index {
                continue;
            }

            if let StatementKind::Assign(box (p, Rvalue::Use(Operand::Constant(box c)))) =
                &bb.statements[location.statement_index].kind
            {
                if let Some(local) = p.as_local() {
                    eligible_locals.push((local, *c));
                }
            }
        }
    }

    eligible_locals
}

impl Visitor<'_> for LocalUseVisitor {
    fn visit_local(&mut self, local: Local, context: PlaceContext, location: Location) {
        if context.is_mutating_use() {
            self.local_mutating_uses[local] = self.local_mutating_uses[local].saturating_add(1);

            if context.is_place_assignment() {
                self.local_assignment_locations[local] = Some(location);
            }
        }
    }
}
