frontend 0.4.0

rustc's frontend with no LLVM and no std: parsing through MIR, as a library
// `#![no_std]`: these arrive with the standard prelude and name no path, so a `std::`
// search cannot see them - and a `#[derive]` can use them without the name appearing
// in this file at all, which is why they are not trimmed by inspection.
use alloc::borrow::ToOwned;
use alloc::boxed::Box;
use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec;
use alloc::vec::Vec;

use core::iter;

use crate::bug;
use crate::rustc_middle::mir::interpret::Scalar;
use crate::rustc_middle::mir::{
    BasicBlock, BinOp, Body, Operand, Place, Rvalue, StatementKind, SwitchTargets, TerminatorKind,
};
use crate::rustc_middle::ty::{Ty, TyCtxt};
use tracing::trace;

use crate::rustc_mir_transform::PassPolicy;
use crate::rustc_mir_transform::ssa::SsaLocals;

/// Pass to convert `if` conditions on integrals into switches on the integral.
/// For an example, it turns something like
///
/// ```ignore (MIR)
/// _3 = Eq(move _4, const 43i32);
/// switchInt(_3) -> [false: bb2, otherwise: bb3];
/// ```
///
/// into:
///
/// ```ignore (MIR)
/// switchInt(_4) -> [43i32: bb3, otherwise: bb2];
/// ```
pub(super) struct SimplifyComparisonIntegral;

impl<'tcx> crate::rustc_mir_transform::MirPass<'tcx> for SimplifyComparisonIntegral {
    fn policy(&self, sess: &crate::rustc_session::Session) -> PassPolicy {
        PassPolicy::optimization(sess.mir_opt_level() > 1)
    }

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

        let typing_env = body.typing_env(tcx);
        let ssa = SsaLocals::new(tcx, body, typing_env);
        let helper = OptimizationFinder { body };
        let opts = helper.find_optimizations(&ssa);
        for opt in opts {
            trace!("SUCCESS: Applying {:?}", opt);
            // replace terminator with a switchInt that switches on the integer directly
            let bbs = &mut body.basic_blocks_mut();
            let bb = &mut bbs[opt.bb_idx];
            let new_value = match opt.branch_value_scalar {
                Scalar::Int(int) => {
                    let layout = tcx
                        .layout_of(typing_env.as_query_input(opt.branch_value_ty))
                        .expect("if we have an evaluated constant we must know the layout");
                    int.to_bits(layout.size)
                }
                Scalar::Ptr(..) => continue,
            };
            const FALSE: u128 = 0;

            let mut new_targets = opt.targets;
            let first_value = new_targets.iter().next().unwrap().0;
            let first_is_false_target = first_value == FALSE;
            match opt.op {
                BinOp::Eq => {
                    // if the assignment was Eq we want the true case to be first
                    if first_is_false_target {
                        new_targets.all_targets_mut().swap(0, 1);
                    }
                }
                BinOp::Ne => {
                    // if the assignment was Ne we want the false case to be first
                    if !first_is_false_target {
                        new_targets.all_targets_mut().swap(0, 1);
                    }
                }
                _ => unreachable!(),
            }

            // if the integer being compared to a const integral is being moved into the
            // comparison, e.g `_2 = Eq(move _3, const 'x');`
            // we want to avoid making a double move later on in the switchInt on _3.
            // So to avoid `switchInt(move _3) -> ['x': bb2, otherwise: bb1];`,
            // we convert the move in the comparison statement to a copy.

            // unwrap is safe as we know this statement is an assign
            let (_, rhs) = bb.statements[opt.bin_op_stmt_idx].kind.as_assign_mut().unwrap();

            use Operand::*;
            if let Rvalue::BinaryOp(_, operands) = rhs {
                match &mut **operands {
                    (left @ Move(_), Constant(_)) => {
                        *left = Copy(opt.to_switch_on);
                    }
                    (Constant(_), right @ Move(_)) => {
                        *right = Copy(opt.to_switch_on);
                    }
                    _ => (),
                }
            }

            let [bb_cond, bb_otherwise] = match new_targets.all_targets() {
                [a, b] => [*a, *b],
                e => bug!("expected 2 switch targets, got: {:?}", e),
            };

            let targets = SwitchTargets::new(iter::once((new_value, bb_cond)), bb_otherwise);

            let terminator = bb.terminator_mut();
            terminator.kind =
                TerminatorKind::SwitchInt { discr: Operand::Copy(opt.to_switch_on), targets };
        }
    }
}

struct OptimizationFinder<'a, 'tcx> {
    body: &'a Body<'tcx>,
}

impl<'tcx> OptimizationFinder<'_, 'tcx> {
    fn find_optimizations(&self, ssa: &SsaLocals) -> Vec<OptimizationInfo<'tcx>> {
        self.body
            .basic_blocks
            .iter_enumerated()
            .filter_map(|(bb_idx, bb)| {
                // find switch
                let (discr, targets) = bb.terminator().kind.as_switch()?;
                let place_switched_on = discr.place()?;
                // Make sure that the place is not modified.
                if !ssa.is_ssa(place_switched_on.local) || !place_switched_on.is_stable_offset() {
                    return None;
                }

                // find the statement that assigns the place being switched on
                bb.statements.iter().enumerate().rev().find_map(|(stmt_idx, stmt)| {
                    match &stmt.kind {
                        crate::rustc_middle::mir::StatementKind::Assign(assign)
                            if assign.0 == place_switched_on =>
                        {
                            let (_, rhs) = &**assign;
                            match rhs {
                                Rvalue::BinaryOp(op @ (BinOp::Eq | BinOp::Ne), operands) => {
                                    let (left, right) = &**operands;
                                    let (branch_value_scalar, branch_value_ty, to_switch_on) =
                                        find_branch_value_info(left, right, ssa)?;

                                    // The transformation adds a use of `to_switch_on` at the
                                    // terminator. Both storage markers make the local uninitialized,
                                    // so either invalidates the value used by the comparison.
                                    if bb.statements[stmt_idx + 1..].iter().any(|stmt| {
                                        matches!(
                                            stmt.kind,
                                            StatementKind::StorageLive(local)
                                                | StatementKind::StorageDead(local)
                                                if local == to_switch_on.local
                                        )
                                    }) {
                                        return None;
                                    }

                                    Some(OptimizationInfo {
                                        bin_op_stmt_idx: stmt_idx,
                                        bb_idx,
                                        to_switch_on,
                                        branch_value_scalar,
                                        branch_value_ty,
                                        op: *op,
                                        targets: targets.clone(),
                                    })
                                }
                                _ => None,
                            }
                        }
                        _ => None,
                    }
                })
            })
            .collect()
    }
}

fn find_branch_value_info<'tcx>(
    left: &Operand<'tcx>,
    right: &Operand<'tcx>,
    ssa: &SsaLocals,
) -> Option<(Scalar, Ty<'tcx>, Place<'tcx>)> {
    // check that either left or right is a constant.
    // if any are, we can use the other to switch on, and the constant as a value in a switch
    use Operand::*;
    match (left, right) {
        (Constant(branch_value), Copy(to_switch_on) | Move(to_switch_on))
        | (Copy(to_switch_on) | Move(to_switch_on), Constant(branch_value)) => {
            // Make sure that the place is not modified.
            if !ssa.is_ssa(to_switch_on.local) || !to_switch_on.is_stable_offset() {
                return None;
            }
            let branch_value_ty = branch_value.const_.ty();
            // we only want to apply this optimization if we are matching on integrals (and chars),
            // as it is not possible to switch on floats
            if !branch_value_ty.is_integral() && !branch_value_ty.is_char() {
                return None;
            };
            let branch_value_scalar = branch_value.const_.try_to_scalar()?;
            Some((branch_value_scalar, branch_value_ty, *to_switch_on))
        }
        _ => None,
    }
}

#[derive(Debug)]
struct OptimizationInfo<'tcx> {
    /// Basic block to apply the optimization
    bb_idx: BasicBlock,
    /// Statement index of Eq/Ne assignment
    bin_op_stmt_idx: usize,
    /// Place that needs to be switched on. This place is of type integral
    to_switch_on: Place<'tcx>,
    /// Constant to use in switch target value
    branch_value_scalar: Scalar,
    /// Type of the constant value
    branch_value_ty: Ty<'tcx>,
    /// Either Eq or Ne
    op: BinOp,
    /// Current targets used in the switch
    targets: SwitchTargets,
}