//! Lowers intrinsic calls

use rustc_middle::mir::*;
use rustc_middle::ty::{self, TyCtxt};
use rustc_middle::{bug, span_bug};
use rustc_span::sym;

use crate::take_array;

pub(super) struct LowerIntrinsics;

impl<'tcx> crate::MirPass<'tcx> for LowerIntrinsics {
    fn run_pass(&self, tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) {
        let local_decls = &body.local_decls;
        for block in body.basic_blocks.as_mut() {
            let terminator = block.terminator.as_mut().unwrap();
            if let TerminatorKind::Call { func, args, destination, target, .. } =
                &mut terminator.kind
                && let ty::FnDef(def_id, generic_args) = *func.ty(local_decls, tcx).kind()
                && let Some(intrinsic) = tcx.intrinsic(def_id)
            {
                match intrinsic.name {
                    sym::unreachable => {
                        terminator.kind = TerminatorKind::Unreachable;
                    }
                    sym::ub_checks => {
                        let target = target.unwrap();
                        block.statements.push(Statement {
                            source_info: terminator.source_info,
                            kind: StatementKind::Assign(Box::new((
                                *destination,
                                Rvalue::NullaryOp(NullOp::UbChecks, tcx.types.bool),
                            ))),
                        });
                        terminator.kind = TerminatorKind::Goto { target };
                    }
                    sym::contract_checks => {
                        let target = target.unwrap();
                        block.statements.push(Statement {
                            source_info: terminator.source_info,
                            kind: StatementKind::Assign(Box::new((
                                *destination,
                                Rvalue::NullaryOp(NullOp::ContractChecks, tcx.types.bool),
                            ))),
                        });
                        terminator.kind = TerminatorKind::Goto { target };
                    }
                    sym::forget => {
                        let target = target.unwrap();
                        block.statements.push(Statement {
                            source_info: terminator.source_info,
                            kind: StatementKind::Assign(Box::new((
                                *destination,
                                Rvalue::Use(Operand::Constant(Box::new(ConstOperand {
                                    span: terminator.source_info.span,
                                    user_ty: None,
                                    const_: Const::zero_sized(tcx.types.unit),
                                }))),
                            ))),
                        });
                        terminator.kind = TerminatorKind::Goto { target };
                    }
                    sym::copy_nonoverlapping => {
                        let target = target.unwrap();
                        let Ok([src, dst, count]) = take_array(args) else {
                            bug!("Wrong arguments for copy_non_overlapping intrinsic");
                        };
                        block.statements.push(Statement {
                            source_info: terminator.source_info,
                            kind: StatementKind::Intrinsic(Box::new(
                                NonDivergingIntrinsic::CopyNonOverlapping(
                                    rustc_middle::mir::CopyNonOverlapping {
                                        src: src.node,
                                        dst: dst.node,
                                        count: count.node,
                                    },
                                ),
                            )),
                        });
                        terminator.kind = TerminatorKind::Goto { target };
                    }
                    sym::assume => {
                        let target = target.unwrap();
                        let Ok([arg]) = take_array(args) else {
                            bug!("Wrong arguments for assume intrinsic");
                        };
                        block.statements.push(Statement {
                            source_info: terminator.source_info,
                            kind: StatementKind::Intrinsic(Box::new(
                                NonDivergingIntrinsic::Assume(arg.node),
                            )),
                        });
                        terminator.kind = TerminatorKind::Goto { target };
                    }
                    sym::wrapping_add
                    | sym::wrapping_sub
                    | sym::wrapping_mul
                    | sym::three_way_compare
                    | sym::unchecked_add
                    | sym::unchecked_sub
                    | sym::unchecked_mul
                    | sym::unchecked_div
                    | sym::unchecked_rem
                    | sym::unchecked_shl
                    | sym::unchecked_shr => {
                        let target = target.unwrap();
                        let Ok([lhs, rhs]) = take_array(args) else {
                            bug!("Wrong arguments for {} intrinsic", intrinsic.name);
                        };
                        let bin_op = match intrinsic.name {
                            sym::wrapping_add => BinOp::Add,
                            sym::wrapping_sub => BinOp::Sub,
                            sym::wrapping_mul => BinOp::Mul,
                            sym::three_way_compare => BinOp::Cmp,
                            sym::unchecked_add => BinOp::AddUnchecked,
                            sym::unchecked_sub => BinOp::SubUnchecked,
                            sym::unchecked_mul => BinOp::MulUnchecked,
                            sym::unchecked_div => BinOp::Div,
                            sym::unchecked_rem => BinOp::Rem,
                            sym::unchecked_shl => BinOp::ShlUnchecked,
                            sym::unchecked_shr => BinOp::ShrUnchecked,
                            _ => bug!("unexpected intrinsic"),
                        };
                        block.statements.push(Statement {
                            source_info: terminator.source_info,
                            kind: StatementKind::Assign(Box::new((
                                *destination,
                                Rvalue::BinaryOp(bin_op, Box::new((lhs.node, rhs.node))),
                            ))),
                        });
                        terminator.kind = TerminatorKind::Goto { target };
                    }
                    sym::add_with_overflow | sym::sub_with_overflow | sym::mul_with_overflow => {
                        let target = target.unwrap();
                        let Ok([lhs, rhs]) = take_array(args) else {
                            bug!("Wrong arguments for {} intrinsic", intrinsic.name);
                        };
                        let bin_op = match intrinsic.name {
                            sym::add_with_overflow => BinOp::AddWithOverflow,
                            sym::sub_with_overflow => BinOp::SubWithOverflow,
                            sym::mul_with_overflow => BinOp::MulWithOverflow,
                            _ => bug!("unexpected intrinsic"),
                        };
                        block.statements.push(Statement {
                            source_info: terminator.source_info,
                            kind: StatementKind::Assign(Box::new((
                                *destination,
                                Rvalue::BinaryOp(bin_op, Box::new((lhs.node, rhs.node))),
                            ))),
                        });
                        terminator.kind = TerminatorKind::Goto { target };
                    }
                    sym::size_of | sym::align_of => {
                        let target = target.unwrap();
                        let tp_ty = generic_args.type_at(0);
                        let null_op = match intrinsic.name {
                            sym::size_of => NullOp::SizeOf,
                            sym::align_of => NullOp::AlignOf,
                            _ => bug!("unexpected intrinsic"),
                        };
                        block.statements.push(Statement {
                            source_info: terminator.source_info,
                            kind: StatementKind::Assign(Box::new((
                                *destination,
                                Rvalue::NullaryOp(null_op, tp_ty),
                            ))),
                        });
                        terminator.kind = TerminatorKind::Goto { target };
                    }
                    sym::read_via_copy => {
                        let Ok([arg]) = take_array(args) else {
                            span_bug!(terminator.source_info.span, "Wrong number of arguments");
                        };
                        let derefed_place = if let Some(place) = arg.node.place()
                            && let Some(local) = place.as_local()
                        {
                            tcx.mk_place_deref(local.into())
                        } else {
                            span_bug!(
                                terminator.source_info.span,
                                "Only passing a local is supported"
                            );
                        };
                        // Add new statement at the end of the block that does the read, and patch
                        // up the terminator.
                        block.statements.push(Statement {
                            source_info: terminator.source_info,
                            kind: StatementKind::Assign(Box::new((
                                *destination,
                                Rvalue::Use(Operand::Copy(derefed_place)),
                            ))),
                        });
                        terminator.kind = match *target {
                            None => {
                                // No target means this read something uninhabited,
                                // so it must be unreachable.
                                TerminatorKind::Unreachable
                            }
                            Some(target) => TerminatorKind::Goto { target },
                        }
                    }
                    sym::write_via_move => {
                        let target = target.unwrap();
                        let Ok([ptr, val]) = take_array(args) else {
                            span_bug!(
                                terminator.source_info.span,
                                "Wrong number of arguments for write_via_move intrinsic",
                            );
                        };
                        let derefed_place = if let Some(place) = ptr.node.place()
                            && let Some(local) = place.as_local()
                        {
                            tcx.mk_place_deref(local.into())
                        } else {
                            span_bug!(
                                terminator.source_info.span,
                                "Only passing a local is supported"
                            );
                        };
                        block.statements.push(Statement {
                            source_info: terminator.source_info,
                            kind: StatementKind::Assign(Box::new((
                                derefed_place,
                                Rvalue::Use(val.node),
                            ))),
                        });
                        terminator.kind = TerminatorKind::Goto { target };
                    }
                    sym::discriminant_value => {
                        let target = target.unwrap();
                        let Ok([arg]) = take_array(args) else {
                            span_bug!(
                                terminator.source_info.span,
                                "Wrong arguments for discriminant_value intrinsic"
                            );
                        };
                        let arg = arg.node.place().unwrap();
                        let arg = tcx.mk_place_deref(arg);
                        block.statements.push(Statement {
                            source_info: terminator.source_info,
                            kind: StatementKind::Assign(Box::new((
                                *destination,
                                Rvalue::Discriminant(arg),
                            ))),
                        });
                        terminator.kind = TerminatorKind::Goto { target };
                    }
                    sym::offset => {
                        let target = target.unwrap();
                        let Ok([ptr, delta]) = take_array(args) else {
                            span_bug!(
                                terminator.source_info.span,
                                "Wrong number of arguments for offset intrinsic",
                            );
                        };
                        block.statements.push(Statement {
                            source_info: terminator.source_info,
                            kind: StatementKind::Assign(Box::new((
                                *destination,
                                Rvalue::BinaryOp(BinOp::Offset, Box::new((ptr.node, delta.node))),
                            ))),
                        });
                        terminator.kind = TerminatorKind::Goto { target };
                    }
                    sym::slice_get_unchecked => {
                        let target = target.unwrap();
                        let Ok([ptrish, index]) = take_array(args) else {
                            span_bug!(
                                terminator.source_info.span,
                                "Wrong number of arguments for {intrinsic:?}",
                            );
                        };

                        let place = ptrish.node.place().unwrap();
                        assert!(!place.is_indirect());
                        let updated_place = place.project_deeper(
                            &[
                                ProjectionElem::Deref,
                                ProjectionElem::Index(
                                    index.node.place().unwrap().as_local().unwrap(),
                                ),
                            ],
                            tcx,
                        );

                        let ret_ty = generic_args.type_at(0);
                        let rvalue = match *ret_ty.kind() {
                            ty::RawPtr(_, Mutability::Not) => {
                                Rvalue::RawPtr(RawPtrKind::Const, updated_place)
                            }
                            ty::RawPtr(_, Mutability::Mut) => {
                                Rvalue::RawPtr(RawPtrKind::Mut, updated_place)
                            }
                            ty::Ref(region, _, Mutability::Not) => {
                                Rvalue::Ref(region, BorrowKind::Shared, updated_place)
                            }
                            ty::Ref(region, _, Mutability::Mut) => Rvalue::Ref(
                                region,
                                BorrowKind::Mut { kind: MutBorrowKind::Default },
                                updated_place,
                            ),
                            _ => bug!("Unknown return type {ret_ty:?}"),
                        };

                        block.statements.push(Statement {
                            source_info: terminator.source_info,
                            kind: StatementKind::Assign(Box::new((*destination, rvalue))),
                        });
                        terminator.kind = TerminatorKind::Goto { target };
                    }
                    sym::transmute | sym::transmute_unchecked => {
                        let dst_ty = destination.ty(local_decls, tcx).ty;
                        let Ok([arg]) = take_array(args) else {
                            span_bug!(
                                terminator.source_info.span,
                                "Wrong number of arguments for transmute intrinsic",
                            );
                        };

                        // Always emit the cast, even if we transmute to an uninhabited type,
                        // because that lets CTFE and codegen generate better error messages
                        // when such a transmute actually ends up reachable.
                        block.statements.push(Statement {
                            source_info: terminator.source_info,
                            kind: StatementKind::Assign(Box::new((
                                *destination,
                                Rvalue::Cast(CastKind::Transmute, arg.node, dst_ty),
                            ))),
                        });
                        if let Some(target) = *target {
                            terminator.kind = TerminatorKind::Goto { target };
                        } else {
                            terminator.kind = TerminatorKind::Unreachable;
                        }
                    }
                    sym::aggregate_raw_ptr => {
                        let Ok([data, meta]) = take_array(args) else {
                            span_bug!(
                                terminator.source_info.span,
                                "Wrong number of arguments for aggregate_raw_ptr intrinsic",
                            );
                        };
                        let target = target.unwrap();
                        let pointer_ty = generic_args.type_at(0);
                        let kind = if let ty::RawPtr(pointee_ty, mutability) = pointer_ty.kind() {
                            AggregateKind::RawPtr(*pointee_ty, *mutability)
                        } else {
                            span_bug!(
                                terminator.source_info.span,
                                "Return type of aggregate_raw_ptr intrinsic must be a raw pointer",
                            );
                        };
                        let fields = [data.node, meta.node];
                        block.statements.push(Statement {
                            source_info: terminator.source_info,
                            kind: StatementKind::Assign(Box::new((
                                *destination,
                                Rvalue::Aggregate(Box::new(kind), fields.into()),
                            ))),
                        });
                        terminator.kind = TerminatorKind::Goto { target };
                    }
                    sym::ptr_metadata => {
                        let Ok([ptr]) = take_array(args) else {
                            span_bug!(
                                terminator.source_info.span,
                                "Wrong number of arguments for ptr_metadata intrinsic",
                            );
                        };
                        let target = target.unwrap();
                        block.statements.push(Statement {
                            source_info: terminator.source_info,
                            kind: StatementKind::Assign(Box::new((
                                *destination,
                                Rvalue::UnaryOp(UnOp::PtrMetadata, ptr.node),
                            ))),
                        });
                        terminator.kind = TerminatorKind::Goto { target };
                    }
                    _ => {}
                }
            }
        }
    }

    fn is_required(&self) -> bool {
        true
    }
}
