//! Module to handle integer operations.
//! This module exists because some integer types are not supported on some gcc platforms, e.g.
//! 128-bit integers on 32-bit platforms and thus require to be handled manually.

// cSpell:words cmpti divti modti mulodi muloti udivti umodti

use gccjit::{
    BinaryOp, CType, ComparisonOp, FunctionType, Location, RValue, ToRValue, Type, UnaryOp,
};
use rustc_abi::{CanonAbi, Endian, ExternAbi};
use rustc_codegen_ssa::common::{IntPredicate, TypeKind};
use rustc_codegen_ssa::traits::{BackendTypes, BaseTypeCodegenMethods, BuilderMethods, OverflowOp};
use rustc_middle::ty::{self, Ty};
use rustc_target::callconv::{ArgAbi, ArgAttributes, FnAbi, PassMode};
use rustc_type_ir::{Interner, TyKind};

use crate::builder::{Builder, ToGccComp};
use crate::common::{SignType, TypeReflection};
use crate::context::CodegenCx;

impl<'a, 'gcc, 'tcx> Builder<'a, 'gcc, 'tcx> {
    pub fn gcc_urem(&self, a: RValue<'gcc>, b: RValue<'gcc>) -> RValue<'gcc> {
        // 128-bit unsigned %: __umodti3
        self.multiplicative_operation(BinaryOp::Modulo, "mod", false, a, b)
    }

    pub fn gcc_srem(&self, a: RValue<'gcc>, b: RValue<'gcc>) -> RValue<'gcc> {
        // 128-bit signed %:   __modti3
        self.multiplicative_operation(BinaryOp::Modulo, "mod", true, a, b)
    }

    pub fn gcc_not(&self, a: RValue<'gcc>) -> RValue<'gcc> {
        let typ = a.get_type();
        if self.is_native_int_type_or_bool(typ) {
            let operation =
                if typ.is_bool() { UnaryOp::LogicalNegate } else { UnaryOp::BitwiseNegate };
            self.cx.context.new_unary_op(self.location, operation, typ, a)
        } else {
            let element_type = typ.dyncast_array().expect("element type");
            self.concat_low_high_rvalues(
                typ,
                self.cx.context.new_unary_op(
                    self.location,
                    UnaryOp::BitwiseNegate,
                    element_type,
                    self.low(a),
                ),
                self.cx.context.new_unary_op(
                    self.location,
                    UnaryOp::BitwiseNegate,
                    element_type,
                    self.high(a),
                ),
            )
        }
    }

    pub fn gcc_neg(&self, a: RValue<'gcc>) -> RValue<'gcc> {
        let a_type = a.get_type();
        if self.is_native_int_type(a_type) || a_type.is_vector() {
            self.cx.context.new_unary_op(self.location, UnaryOp::Minus, a.get_type(), a)
        } else {
            self.gcc_add(self.gcc_not(a), self.gcc_int(a_type, 1))
        }
    }

    pub fn gcc_and(&self, a: RValue<'gcc>, b: RValue<'gcc>) -> RValue<'gcc> {
        self.cx.bitwise_operation(BinaryOp::BitwiseAnd, a, b, self.location)
    }

    pub fn gcc_lshr(&mut self, a: RValue<'gcc>, b: RValue<'gcc>) -> RValue<'gcc> {
        let a_type = a.get_type();
        let b_type = b.get_type();
        let a_native = self.is_native_int_type(a_type);
        let b_native = self.is_native_int_type(b_type);
        if a_native && b_native {
            // FIXME(antoyo): remove the casts when libgccjit can shift an unsigned number by a signed number.
            // FIXME(antoyo): cast to unsigned to do a logical shift if that does not work.
            if a_type.is_signed(self) != b_type.is_signed(self) {
                let b = self.context.new_cast(self.location, b, a_type);
                a >> b
            } else {
                let a_size = a_type.get_size();
                let b_size = b_type.get_size();
                match a_size.cmp(&b_size) {
                    std::cmp::Ordering::Equal => a >> b,
                    _ => {
                        // NOTE: it is OK to cast even if b has a type bigger than a because b has
                        // been masked by codegen_ssa before calling Builder::lshr or
                        // Builder::ashr.
                        let b = self.context.new_cast(self.location, b, a_type);
                        a >> b
                    }
                }
            }
        } else if a_type.is_vector() && b_type.is_vector() {
            a >> b
        } else if a_native && !b_native {
            self.gcc_lshr(a, self.gcc_int_cast(b, a_type))
        } else {
            // NOTE: we cannot use the lshr builtin because it's calling hi() (to get the most
            // significant half of the number) which uses lshr.

            let native_int_type = a_type.dyncast_array().expect("get element type");

            let func = self.current_func();
            let then_block = func.new_block("then");
            let else_block = func.new_block("else");
            let after_block = func.new_block("after");
            let b0_block = func.new_block("b0");
            let actual_else_block = func.new_block("actual_else");

            let result = func.new_local(self.location, a_type, "shiftResult");

            let sixty_four = self.gcc_int(native_int_type, 64);
            let sixty_three = self.gcc_int(native_int_type, 63);
            let zero = self.gcc_zero(native_int_type);
            let b = self.gcc_int_cast(b, native_int_type);
            let condition = self.gcc_icmp(IntPredicate::IntNE, self.gcc_and(b, sixty_four), zero);
            self.llbb().end_with_conditional(self.location, condition, then_block, else_block);

            let shift_value = self.gcc_sub(b, sixty_four);
            let high = self.high(a);
            let sign = if a_type.is_signed(self) { high >> sixty_three } else { zero };
            let array_value = self.concat_low_high_rvalues(a_type, high >> shift_value, sign);
            then_block.add_assignment(self.location, result, array_value);
            then_block.end_with_jump(self.location, after_block);

            let condition = self.gcc_icmp(IntPredicate::IntEQ, b, zero);
            else_block.end_with_conditional(self.location, condition, b0_block, actual_else_block);

            b0_block.add_assignment(self.location, result, a);
            b0_block.end_with_jump(self.location, after_block);

            let shift_value = self.gcc_sub(sixty_four, b);
            // NOTE: cast low to its unsigned type in order to perform a logical right shift.
            let unsigned_type = native_int_type.to_unsigned(self.cx);
            let casted_low = self.context.new_cast(self.location, self.low(a), unsigned_type);
            let shifted_low = casted_low >> self.context.new_cast(self.location, b, unsigned_type);
            let shifted_low = self.context.new_cast(self.location, shifted_low, native_int_type);
            let array_value = self.concat_low_high_rvalues(
                a_type,
                (high << shift_value) | shifted_low,
                high >> b,
            );
            actual_else_block.add_assignment(self.location, result, array_value);
            actual_else_block.end_with_jump(self.location, after_block);

            // NOTE: since jumps were added in a place rustc does not expect, the current block in the
            // state need to be updated.
            self.switch_to_block(after_block);

            result.to_rvalue()
        }
    }

    fn additive_operation(
        &self,
        operation: BinaryOp,
        a: RValue<'gcc>,
        mut b: RValue<'gcc>,
    ) -> RValue<'gcc> {
        let a_type = a.get_type();
        let b_type = b.get_type();
        if (self.is_native_int_type_or_bool(a_type) && self.is_native_int_type_or_bool(b_type))
            || (a_type.is_vector() && b_type.is_vector())
        {
            if a_type != b_type {
                if a_type.is_vector() {
                    // Vector types need to be bitcast.
                    // FIXME(antoyo): perhaps use __builtin_convertvector for vector casting.
                    b = self.context.new_bitcast(self.location, b, a_type);
                } else {
                    b = self.context.new_cast(self.location, b, a_type);
                }
            }
            self.context.new_binary_op(self.location, operation, a_type, a, b)
        } else {
            debug_assert!(a_type.dyncast_array().is_some());
            debug_assert!(b_type.dyncast_array().is_some());
            let signed = a_type.is_compatible_with(self.i128_type);
            let func_name = match (operation, signed) {
                (BinaryOp::Plus, true) => "__rust_i128_add",
                (BinaryOp::Plus, false) => "__rust_u128_add",
                (BinaryOp::Minus, true) => "__rust_i128_sub",
                (BinaryOp::Minus, false) => "__rust_u128_sub",
                _ => unreachable!("unexpected additive operation {:?}", operation),
            };
            let param_a = self.context.new_parameter(self.location, a_type, "a");
            let param_b = self.context.new_parameter(self.location, b_type, "b");
            let func = self.context.new_function(
                self.location,
                FunctionType::Extern,
                a_type,
                &[param_a, param_b],
                func_name,
                false,
            );
            self.context.new_call(self.location, func, &[a, b])
        }
    }

    pub fn gcc_add(&self, a: RValue<'gcc>, b: RValue<'gcc>) -> RValue<'gcc> {
        self.additive_operation(BinaryOp::Plus, a, b)
    }

    pub fn gcc_mul(&self, a: RValue<'gcc>, b: RValue<'gcc>) -> RValue<'gcc> {
        self.multiplicative_operation(BinaryOp::Mult, "mul", true, a, b)
    }

    pub fn gcc_sub(&self, a: RValue<'gcc>, b: RValue<'gcc>) -> RValue<'gcc> {
        self.additive_operation(BinaryOp::Minus, a, b)
    }

    fn multiplicative_operation(
        &self,
        operation: BinaryOp,
        operation_name: &str,
        signed: bool,
        a: RValue<'gcc>,
        mut b: RValue<'gcc>,
    ) -> RValue<'gcc> {
        let a_type = a.get_type();
        let b_type = b.get_type();
        if (self.is_native_int_type_or_bool(a_type) && self.is_native_int_type_or_bool(b_type))
            || (a_type.is_vector() && b_type.is_vector())
        {
            if !a_type.is_compatible_with(b_type) {
                if a_type.is_vector() {
                    // Vector types need to be bitcast.
                    // FIXME(antoyo): perhaps use __builtin_convertvector for vector casting.
                    b = self.context.new_bitcast(self.location, b, a_type);
                } else {
                    b = self.context.new_cast(self.location, b, a_type);
                }
            }
            self.context.new_binary_op(self.location, operation, a_type, a, b)
        } else {
            debug_assert!(a_type.dyncast_array().is_some());
            debug_assert!(b_type.dyncast_array().is_some());
            let sign = if signed { "" } else { "u" };
            let func_name = format!("__{}{}ti3", sign, operation_name);
            let param_a = self.context.new_parameter(self.location, a_type, "a");
            let param_b = self.context.new_parameter(self.location, b_type, "b");
            let func = self.context.new_function(
                self.location,
                FunctionType::Extern,
                a_type,
                &[param_a, param_b],
                func_name,
                false,
            );
            self.context.new_call(self.location, func, &[a, b])
        }
    }

    pub fn gcc_sdiv(&self, a: RValue<'gcc>, b: RValue<'gcc>) -> RValue<'gcc> {
        // FIXME(antoyo): check if the types are signed?
        // 128-bit, signed: __divti3
        // FIXME(antoyo): convert the arguments to signed?
        self.multiplicative_operation(BinaryOp::Divide, "div", true, a, b)
    }

    pub fn gcc_udiv(&self, a: RValue<'gcc>, b: RValue<'gcc>) -> RValue<'gcc> {
        // 128-bit, unsigned: __udivti3
        self.multiplicative_operation(BinaryOp::Divide, "div", false, a, b)
    }

    pub fn gcc_checked_binop(
        &self,
        oop: OverflowOp,
        typ: Ty<'_>,
        lhs: <Self as BackendTypes>::Value,
        rhs: <Self as BackendTypes>::Value,
    ) -> (<Self as BackendTypes>::Value, <Self as BackendTypes>::Value) {
        use rustc_middle::ty::IntTy::*;
        use rustc_middle::ty::UintTy::*;
        use rustc_middle::ty::{Int, Uint};

        let new_kind = match *typ.kind() {
            Int(t @ Isize) => Int(t.normalize(self.tcx.sess.target.pointer_width)),
            Uint(t @ Usize) => Uint(t.normalize(self.tcx.sess.target.pointer_width)),
            t @ (Uint(_) | Int(_)) => t,
            _ => panic!("tried to get overflow intrinsic for op applied to non-int type"),
        };

        // FIXME(antoyo): remove duplication with intrinsic?
        let name = if self.is_native_int_type(lhs.get_type()) {
            match oop {
                OverflowOp::Add => "__builtin_add_overflow",
                OverflowOp::Sub => "__builtin_sub_overflow",
                OverflowOp::Mul => "__builtin_mul_overflow",
            }
        } else {
            let (func_name, width) = match oop {
                OverflowOp::Add => match new_kind {
                    Int(I128) => ("__rust_i128_addo", 128),
                    Uint(U128) => ("__rust_u128_addo", 128),
                    _ => unreachable!(),
                },
                OverflowOp::Sub => match new_kind {
                    Int(I128) => ("__rust_i128_subo", 128),
                    Uint(U128) => ("__rust_u128_subo", 128),
                    _ => unreachable!(),
                },
                OverflowOp::Mul => match new_kind {
                    Int(I32) => ("__mulosi4", 32),
                    Int(I64) => ("__mulodi4", 64),
                    Int(I128) => ("__rust_i128_mulo", 128), // FIXME(antoyo): use __muloti4d instead?
                    Uint(U128) => ("__rust_u128_mulo", 128),
                    _ => unreachable!(),
                },
            };
            return self.operation_with_overflow(func_name, lhs, rhs, width);
        };

        let intrinsic = self.context.get_builtin_function(name);
        let res = self
            .current_func()
            // FIXME(antoyo): is it correct to use rhs type instead of the parameter typ?
            .new_local(self.location, rhs.get_type(), "binopResult")
            .get_address(self.location);
        let new_type = type_kind_to_gcc_type(new_kind);
        let new_type = self.context.new_c_type(new_type);
        let lhs = self.context.new_cast(self.location, lhs, new_type);
        let rhs = self.context.new_cast(self.location, rhs, new_type);
        let res = self.context.new_cast(self.location, res, new_type.make_pointer());
        let overflow = self.overflow_call(intrinsic, &[lhs, rhs, res], None);
        (res.dereference(self.location).to_rvalue(), overflow)
    }

    /// Non-`__builtin_*` overflow operations with a `fn(T, T, &mut i32) -> T` signature.
    pub fn operation_with_overflow(
        &self,
        func_name: &str,
        lhs: RValue<'gcc>,
        rhs: RValue<'gcc>,
        width: u64,
    ) -> (RValue<'gcc>, RValue<'gcc>) {
        let a_type = lhs.get_type();
        let b_type = rhs.get_type();
        debug_assert!(a_type.dyncast_array().is_some());
        debug_assert!(b_type.dyncast_array().is_some());
        let overflow_type = self.i32_type;
        let overflow_param_type = overflow_type.make_pointer();
        let res_type = a_type;

        let overflow_value =
            self.current_func().new_local(self.location, overflow_type, "overflow");
        let overflow_addr = overflow_value.get_address(self.location);

        let param_a = self.context.new_parameter(self.location, a_type, "a");
        let param_b = self.context.new_parameter(self.location, b_type, "b");
        let param_overflow =
            self.context.new_parameter(self.location, overflow_param_type, "overflow");

        let a_elem_type = a_type.dyncast_array().expect("non-array a value");
        debug_assert!(a_elem_type.is_integral());
        let res_ty = match width {
            32 => self.tcx.types.i32,
            64 => self.tcx.types.i64,
            128 => self.tcx.types.i128,
            _ => unreachable!("unexpected integer size"),
        };
        let layout = self
            .tcx
            .layout_of(ty::TypingEnv::fully_monomorphized().as_query_input(res_ty))
            .unwrap();

        let arg_abi = ArgAbi { layout, mode: PassMode::Direct(ArgAttributes::new()) };
        let mut fn_abi = FnAbi {
            args: vec![arg_abi.clone(), arg_abi.clone(), arg_abi.clone()].into_boxed_slice(),
            ret: arg_abi,
            c_variadic: false,
            fixed_count: 3,
            conv: CanonAbi::C,
            can_unwind: false,
        };
        fn_abi.adjust_for_foreign_abi(self.cx, ExternAbi::C { unwind: false });

        let ret_indirect = matches!(fn_abi.ret.mode, PassMode::Indirect { .. });

        let call = if ret_indirect {
            let res_value = self.current_func().new_local(self.location, res_type, "result_value");
            let res_addr = res_value.get_address(self.location);
            let res_param_type = res_type.make_pointer();
            let param_res = self.context.new_parameter(self.location, res_param_type, "result");

            let func = self.context.new_function(
                self.location,
                FunctionType::Extern,
                self.type_void(),
                &[param_res, param_a, param_b, param_overflow],
                func_name,
                false,
            );
            let _void =
                self.context.new_call(self.location, func, &[res_addr, lhs, rhs, overflow_addr]);
            res_value.to_rvalue()
        } else {
            let func = self.context.new_function(
                self.location,
                FunctionType::Extern,
                res_type,
                &[param_a, param_b, param_overflow],
                func_name,
                false,
            );
            self.context.new_call(self.location, func, &[lhs, rhs, overflow_addr])
        };
        // NOTE: we must assign the result of the operation to a variable at this point to make
        // sure it will be evaluated by libgccjit now.
        // Otherwise, it will only be evaluated when the rvalue for the call is used somewhere else
        // and overflow_value will not be initialized at the correct point in the program.
        let result = self.current_func().new_local(self.location, res_type, "result");
        self.block.add_assignment(self.location, result, call);

        (
            result.to_rvalue(),
            self.context.new_cast(self.location, overflow_value, self.bool_type).to_rvalue(),
        )
    }

    pub fn gcc_icmp(
        &mut self,
        op: IntPredicate,
        mut lhs: RValue<'gcc>,
        mut rhs: RValue<'gcc>,
    ) -> RValue<'gcc> {
        let a_type = lhs.get_type();
        let b_type = rhs.get_type();
        if self.is_non_native_int_type(a_type) || self.is_non_native_int_type(b_type) {
            // This algorithm is based on compiler-rt's __cmpti2:
            // https://github.com/llvm-mirror/compiler-rt/blob/f0745e8476f069296a7c71accedd061dce4cdf79/lib/builtins/cmpti2.c#L21
            let result = self.current_func().new_local(self.location, self.int_type, "icmp_result");
            let block1 = self.current_func().new_block("block1");
            let block2 = self.current_func().new_block("block2");
            let block3 = self.current_func().new_block("block3");
            let block4 = self.current_func().new_block("block4");
            let block5 = self.current_func().new_block("block5");
            let block6 = self.current_func().new_block("block6");
            let block7 = self.current_func().new_block("block7");
            let block8 = self.current_func().new_block("block8");
            let after = self.current_func().new_block("after");

            let native_int_type = a_type.dyncast_array().expect("get element type");
            // NOTE: cast low to its unsigned type in order to perform a comparison correctly (e.g.
            // the sign is only on high).
            let unsigned_type = native_int_type.to_unsigned(self.cx);

            let lhs_low = self.context.new_cast(self.location, self.low(lhs), unsigned_type);
            let rhs_low = self.context.new_cast(self.location, self.low(rhs), unsigned_type);

            let mut lhs_high = self.high(lhs);
            let mut rhs_high = self.high(rhs);

            match op {
                IntPredicate::IntUGT
                | IntPredicate::IntUGE
                | IntPredicate::IntULT
                | IntPredicate::IntULE => {
                    lhs_high = self.context.new_cast(self.location, lhs_high, unsigned_type);
                    rhs_high = self.context.new_cast(self.location, rhs_high, unsigned_type);
                }
                // FIXME(antoyo): we probably need to handle signed comparison for unsigned
                // integers.
                _ => (),
            }

            let condition = self.context.new_comparison(
                self.location,
                ComparisonOp::LessThan,
                lhs_high,
                rhs_high,
            );
            self.llbb().end_with_conditional(self.location, condition, block1, block2);

            block1.add_assignment(
                self.location,
                result,
                self.context.new_rvalue_zero(self.int_type),
            );
            block1.end_with_jump(self.location, after);

            let condition = self.context.new_comparison(
                self.location,
                ComparisonOp::GreaterThan,
                lhs_high,
                rhs_high,
            );
            block2.end_with_conditional(self.location, condition, block3, block4);

            block3.add_assignment(
                self.location,
                result,
                self.context.new_rvalue_from_int(self.int_type, 2),
            );
            block3.end_with_jump(self.location, after);

            let condition = self.context.new_comparison(
                self.location,
                ComparisonOp::LessThan,
                lhs_low,
                rhs_low,
            );
            block4.end_with_conditional(self.location, condition, block5, block6);

            block5.add_assignment(
                self.location,
                result,
                self.context.new_rvalue_zero(self.int_type),
            );
            block5.end_with_jump(self.location, after);

            let condition = self.context.new_comparison(
                self.location,
                ComparisonOp::GreaterThan,
                lhs_low,
                rhs_low,
            );
            block6.end_with_conditional(self.location, condition, block7, block8);

            block7.add_assignment(
                self.location,
                result,
                self.context.new_rvalue_from_int(self.int_type, 2),
            );
            block7.end_with_jump(self.location, after);

            block8.add_assignment(
                self.location,
                result,
                self.context.new_rvalue_one(self.int_type),
            );
            block8.end_with_jump(self.location, after);

            // NOTE: since jumps were added in a place rustc does not expect, the current block in the
            // state need to be updated.
            self.switch_to_block(after);

            let cmp = result.to_rvalue();
            let (op, limit) = match op {
                IntPredicate::IntEQ => {
                    return self.context.new_comparison(
                        self.location,
                        ComparisonOp::Equals,
                        cmp,
                        self.context.new_rvalue_one(self.int_type),
                    );
                }
                IntPredicate::IntNE => {
                    return self.context.new_comparison(
                        self.location,
                        ComparisonOp::NotEquals,
                        cmp,
                        self.context.new_rvalue_one(self.int_type),
                    );
                }
                // FIXME(antoyo): cast to u128 for unsigned comparison. See below.
                IntPredicate::IntUGT => (ComparisonOp::Equals, 2),
                IntPredicate::IntUGE => (ComparisonOp::GreaterThanEquals, 1),
                IntPredicate::IntULT => (ComparisonOp::Equals, 0),
                IntPredicate::IntULE => (ComparisonOp::LessThanEquals, 1),
                IntPredicate::IntSGT => (ComparisonOp::Equals, 2),
                IntPredicate::IntSGE => (ComparisonOp::GreaterThanEquals, 1),
                IntPredicate::IntSLT => (ComparisonOp::Equals, 0),
                IntPredicate::IntSLE => (ComparisonOp::LessThanEquals, 1),
            };
            self.context.new_comparison(
                self.location,
                op,
                cmp,
                self.context.new_rvalue_from_int(self.int_type, limit),
            )
        } else if a_type.get_pointee().is_some() && b_type.get_pointee().is_some() {
            // NOTE: gcc cannot compare pointers to different objects, but rustc does that, so cast them to usize.
            lhs = self.context.new_bitcast(self.location, lhs, self.usize_type);
            rhs = self.context.new_bitcast(self.location, rhs, self.usize_type);
            self.context.new_comparison(self.location, op.to_gcc_comparison(), lhs, rhs)
        } else {
            if a_type != b_type {
                // NOTE: because libgccjit cannot compare function pointers.
                if a_type.dyncast_function_ptr_type().is_some()
                    && b_type.dyncast_function_ptr_type().is_some()
                {
                    lhs = self.context.new_cast(self.location, lhs, self.usize_type.make_pointer());
                    rhs = self.context.new_cast(self.location, rhs, self.usize_type.make_pointer());
                }
                // NOTE: hack because we try to cast a vector type to the same vector type.
                else if format!("{:?}", a_type) != format!("{:?}", b_type) {
                    rhs = self.context.new_cast(self.location, rhs, a_type);
                }
            }
            match op {
                IntPredicate::IntUGT
                | IntPredicate::IntUGE
                | IntPredicate::IntULT
                | IntPredicate::IntULE => {
                    if !a_type.is_vector() {
                        let unsigned_type = a_type.to_unsigned(self.cx);
                        lhs = self.context.new_cast(self.location, lhs, unsigned_type);
                        rhs = self.context.new_cast(self.location, rhs, unsigned_type);
                    }
                }
                // FIXME(antoyo): we probably need to handle signed comparison for unsigned
                // integers.
                _ => (),
            }
            self.context.new_comparison(self.location, op.to_gcc_comparison(), lhs, rhs)
        }
    }

    pub fn gcc_xor(&self, a: RValue<'gcc>, mut b: RValue<'gcc>) -> RValue<'gcc> {
        let a_type = a.get_type();
        let b_type = b.get_type();
        if a_type.is_vector() && b_type.is_vector() {
            let b = self.bitcast_if_needed(b, a_type);
            a ^ b
        } else if self.is_native_int_type_or_bool(a_type) && self.is_native_int_type_or_bool(b_type)
        {
            if !a_type.is_compatible_with(b_type) {
                b = self.context.new_cast(self.location, b, a_type);
            }
            a ^ b
        } else {
            self.concat_low_high_rvalues(
                a_type,
                self.low(a) ^ self.low(b),
                self.high(a) ^ self.high(b),
            )
        }
    }

    pub fn gcc_shl(&mut self, a: RValue<'gcc>, b: RValue<'gcc>) -> RValue<'gcc> {
        let a_type = a.get_type();
        let b_type = b.get_type();
        let a_native = self.is_native_int_type(a_type);
        let b_native = self.is_native_int_type(b_type);
        if a_native && b_native {
            // FIXME(antoyo): remove the casts when libgccjit can shift an unsigned number by an unsigned number.
            if a_type.is_unsigned(self) && b_type.is_signed(self) {
                let a = self.context.new_cast(self.location, a, b_type);
                let result = a << b;
                self.context.new_cast(self.location, result, a_type)
            } else if a_type.is_signed(self) && b_type.is_unsigned(self) {
                let b = self.context.new_cast(self.location, b, a_type);
                a << b
            } else {
                let a_size = a_type.get_size();
                let b_size = b_type.get_size();
                match a_size.cmp(&b_size) {
                    std::cmp::Ordering::Equal => a << b,
                    _ => {
                        // NOTE: it is OK to cast even if b has a type bigger than a because b has
                        // been masked by codegen_ssa before calling Builder::shl.
                        let b = self.context.new_cast(self.location, b, a_type);
                        a << b
                    }
                }
            }
        } else if a_type.is_vector() && b_type.is_vector() {
            a << b
        } else if a_native && !b_native {
            self.gcc_shl(a, self.gcc_int_cast(b, a_type))
        } else {
            // NOTE: we cannot use the ashl builtin because it's calling widen_hi() which uses ashl.
            let native_int_type = a_type.dyncast_array().expect("get element type");

            let func = self.current_func();
            let then_block = func.new_block("then");
            let else_block = func.new_block("else");
            let after_block = func.new_block("after");
            let b0_block = func.new_block("b0");
            let actual_else_block = func.new_block("actual_else");

            let result = func.new_local(self.location, a_type, "shiftResult");

            let b = self.gcc_int_cast(b, native_int_type);
            let sixty_four = self.gcc_int(native_int_type, 64);
            let zero = self.gcc_zero(native_int_type);
            let condition = self.gcc_icmp(IntPredicate::IntNE, self.gcc_and(b, sixty_four), zero);
            self.llbb().end_with_conditional(self.location, condition, then_block, else_block);

            let array_value =
                self.concat_low_high_rvalues(a_type, zero, self.low(a) << (b - sixty_four));
            then_block.add_assignment(self.location, result, array_value);
            then_block.end_with_jump(self.location, after_block);

            let condition = self.gcc_icmp(IntPredicate::IntEQ, b, zero);
            else_block.end_with_conditional(self.location, condition, b0_block, actual_else_block);

            b0_block.add_assignment(self.location, result, a);
            b0_block.end_with_jump(self.location, after_block);

            // NOTE: cast low to its unsigned type in order to perform a logical right shift.
            // FIXME(antoyo): adjust this ^ comment.
            let unsigned_type = native_int_type.to_unsigned(self.cx);
            let casted_low = self.context.new_cast(self.location, self.low(a), unsigned_type);
            let shift_value = self.context.new_cast(self.location, sixty_four - b, unsigned_type);
            let high_low =
                self.context.new_cast(self.location, casted_low >> shift_value, native_int_type);

            let array_value = self.concat_low_high_rvalues(
                a_type,
                self.low(a) << b,
                (self.high(a) << b) | high_low,
            );
            actual_else_block.add_assignment(self.location, result, array_value);
            actual_else_block.end_with_jump(self.location, after_block);

            // NOTE: since jumps were added in a place rustc does not expect, the current block in the
            // state need to be updated.
            self.switch_to_block(after_block);

            result.to_rvalue()
        }
    }

    pub fn gcc_bswap(&mut self, mut arg: RValue<'gcc>, width: u64) -> RValue<'gcc> {
        let arg_type = arg.get_type();
        if !self.is_native_int_type(arg_type) {
            let native_int_type = arg_type.dyncast_array().expect("get element type");
            let lsb = self.low(arg);
            let swapped_lsb = self.gcc_bswap(lsb, width / 2);
            let swapped_lsb = self.context.new_cast(self.location, swapped_lsb, native_int_type);
            let msb = self.high(arg);
            let swapped_msb = self.gcc_bswap(msb, width / 2);
            let swapped_msb = self.context.new_cast(self.location, swapped_msb, native_int_type);

            // NOTE: we also need to swap the two elements here, in addition to swapping inside
            // the elements themselves like done above.
            return self.concat_low_high_rvalues(arg_type, swapped_msb, swapped_lsb);
        }

        // FIXME(antoyo): check if it's faster to use string literals and a
        // match instead of format!.
        let bswap = self.cx.context.get_builtin_function(format!("__builtin_bswap{}", width));
        // FIXME(antoyo): this cast should not be necessary. Remove
        // when having proper sized integer types.
        let param_type = bswap.get_param(0).to_rvalue().get_type();
        if param_type != arg_type {
            arg = self.bitcast(arg, param_type);
        }
        self.cx.context.new_call(self.location, bswap, &[arg])
    }
}

impl<'gcc, 'tcx> CodegenCx<'gcc, 'tcx> {
    pub fn gcc_int(&self, typ: Type<'gcc>, int: i64) -> RValue<'gcc> {
        if self.is_native_int_type_or_bool(typ) {
            self.context.new_rvalue_from_long(typ, int)
        } else {
            // NOTE: set the sign in high.
            self.concat_low_high(typ, int, -(int.is_negative() as i64))
        }
    }

    pub fn gcc_uint(&self, typ: Type<'gcc>, int: u64) -> RValue<'gcc> {
        if typ.is_u128(self) {
            // FIXME(antoyo): libgccjit cannot create 128-bit values yet.
            let num = self.context.new_rvalue_from_long(self.u64_type, int as i64);
            self.gcc_int_cast(num, typ)
        } else if self.is_native_int_type_or_bool(typ) {
            self.context.new_rvalue_from_long(typ, int as i64)
        } else {
            self.concat_low_high(typ, int as i64, 0)
        }
    }

    pub fn gcc_uint_big(&self, typ: Type<'gcc>, num: u128) -> RValue<'gcc> {
        let low = num as u64;
        let high = (num >> 64) as u64;
        if num >> 64 != 0 {
            // FIXME(antoyo): use a new function new_rvalue_from_unsigned_long()?
            if self.is_native_int_type(typ) {
                let low = self.context.new_rvalue_from_long(self.u64_type, low as i64);
                let high = self.context.new_rvalue_from_long(typ, high as i64);

                let sixty_four = self.context.new_rvalue_from_long(typ, 64);
                let shift = high << sixty_four;
                shift | self.context.new_cast(None, low, typ)
            } else {
                self.concat_low_high(typ, low as i64, high as i64)
            }
        } else if typ.is_i128(self) {
            // FIXME(antoyo): libgccjit cannot create 128-bit values yet.
            let num = self.context.new_rvalue_from_long(self.u64_type, num as u64 as i64);
            self.gcc_int_cast(num, typ)
        } else {
            self.gcc_uint(typ, num as u64)
        }
    }

    pub fn gcc_zero(&self, typ: Type<'gcc>) -> RValue<'gcc> {
        if self.is_native_int_type_or_bool(typ) {
            self.context.new_rvalue_zero(typ)
        } else {
            self.concat_low_high(typ, 0, 0)
        }
    }

    pub fn gcc_int_width(&self, typ: Type<'gcc>) -> u64 {
        if self.is_native_int_type_or_bool(typ) {
            typ.get_size() as u64 * 8
        } else {
            // NOTE: the only unsupported types are u128 and i128.
            128
        }
    }

    fn bitwise_operation(
        &self,
        operation: BinaryOp,
        a: RValue<'gcc>,
        mut b: RValue<'gcc>,
        loc: Option<Location<'gcc>>,
    ) -> RValue<'gcc> {
        let a_type = a.get_type();
        let b_type = b.get_type();
        let a_native = self.is_native_int_type_or_bool(a_type);
        let b_native = self.is_native_int_type_or_bool(b_type);
        if a_type.is_vector() && b_type.is_vector() {
            let b = self.bitcast_if_needed(b, a_type);
            self.context.new_binary_op(loc, operation, a_type, a, b)
        } else if a_native && b_native {
            if a_type != b_type {
                b = self.context.new_cast(loc, b, a_type);
            }
            self.context.new_binary_op(loc, operation, a_type, a, b)
        } else {
            assert!(
                !a_native && !b_native,
                "both types should either be native or non-native for or operation"
            );
            let native_int_type = a_type.dyncast_array().expect("get element type");
            self.concat_low_high_rvalues(
                a_type,
                self.context.new_binary_op(
                    loc,
                    operation,
                    native_int_type,
                    self.low(a),
                    self.low(b),
                ),
                self.context.new_binary_op(
                    loc,
                    operation,
                    native_int_type,
                    self.high(a),
                    self.high(b),
                ),
            )
        }
    }

    pub fn gcc_or(
        &self,
        a: RValue<'gcc>,
        b: RValue<'gcc>,
        loc: Option<Location<'gcc>>,
    ) -> RValue<'gcc> {
        self.bitwise_operation(BinaryOp::BitwiseOr, a, b, loc)
    }

    // FIXME(antoyo): can we use https://github.com/rust-lang/compiler-builtins/blob/master/src/int/mod.rs#L379 instead?
    pub fn gcc_int_cast(&self, value: RValue<'gcc>, dest_typ: Type<'gcc>) -> RValue<'gcc> {
        let value_type = value.get_type();
        if self.is_native_int_type_or_bool(dest_typ) && self.is_native_int_type_or_bool(value_type)
        {
            // FIXME: use self.location.
            self.context.new_cast(None, value, dest_typ)
        } else if self.is_native_int_type_or_bool(dest_typ) {
            self.context.new_cast(None, self.low(value), dest_typ)
        } else if self.is_native_int_type_or_bool(value_type) {
            let dest_element_type = dest_typ.dyncast_array().expect("get element type");

            // NOTE: set the sign of the value.
            let zero = self.context.new_rvalue_zero(value_type);
            let is_negative =
                self.context.new_comparison(None, ComparisonOp::LessThan, value, zero);
            let is_negative = self.gcc_int_cast(is_negative, dest_element_type);
            self.concat_low_high_rvalues(
                dest_typ,
                self.context.new_cast(None, value, dest_element_type),
                self.context.new_unary_op(None, UnaryOp::Minus, dest_element_type, is_negative),
            )
        } else {
            // Since u128 and i128 are the only types that can be unsupported, we know the type of
            // value and the destination type have the same size, so a bitcast is fine.

            // FIXME(antoyo): perhaps use __builtin_convertvector for vector casting.
            self.context.new_bitcast(None, value, dest_typ)
        }
    }

    fn int_to_float_cast(
        &self,
        signed: bool,
        value: RValue<'gcc>,
        dest_typ: Type<'gcc>,
    ) -> RValue<'gcc> {
        let value_type = value.get_type();
        if self.is_native_int_type_or_bool(value_type) {
            return self.context.new_cast(None, value, dest_typ);
        }

        debug_assert!(value_type.dyncast_array().is_some());
        let name_suffix = match self.type_kind(dest_typ) {
            // cSpell:disable
            TypeKind::Float => "tisf",
            TypeKind::Double => "tidf",
            TypeKind::FP128 => "titf",
            // cSpell:enable
            kind => panic!("cannot cast a non-native integer to type {:?}", kind),
        };
        let sign = if signed { "" } else { "un" };
        let func_name = format!("__float{}{}", sign, name_suffix);
        let param = self.context.new_parameter(None, value_type, "n");
        let func = self.context.new_function(
            None,
            FunctionType::Extern,
            dest_typ,
            &[param],
            func_name,
            false,
        );
        self.context.new_call(None, func, &[value])
    }

    pub fn gcc_int_to_float_cast(&self, value: RValue<'gcc>, dest_typ: Type<'gcc>) -> RValue<'gcc> {
        self.int_to_float_cast(true, value, dest_typ)
    }

    pub fn gcc_uint_to_float_cast(
        &self,
        value: RValue<'gcc>,
        dest_typ: Type<'gcc>,
    ) -> RValue<'gcc> {
        self.int_to_float_cast(false, value, dest_typ)
    }

    fn float_to_int_cast(
        &self,
        signed: bool,
        mut value: RValue<'gcc>,
        dest_typ: Type<'gcc>,
    ) -> RValue<'gcc> {
        let value_type = value.get_type();
        if self.is_native_int_type_or_bool(dest_typ) {
            return self.context.new_cast(None, value, dest_typ);
        }

        debug_assert!(dest_typ.dyncast_array().is_some());
        let (dest_type, param_type) = match self.type_kind(value_type) {
            TypeKind::Half => (Some(self.float_type), self.float_type),
            _ => (None, value_type),
        };
        let name_suffix = match self.type_kind(value_type) {
            // cSpell:disable
            // Since we will cast Half to a float, we use sfti for both.
            TypeKind::Half | TypeKind::Float => "sfti",
            TypeKind::Double => "dfti",
            TypeKind::FP128 => "tfti",
            // cSpell:enable
            kind => panic!("cannot cast a {:?} to non-native integer", kind),
        };
        let sign = if signed { "" } else { "uns" };
        let func_name = format!("__fix{}{}", sign, name_suffix);
        let param = self.context.new_parameter(None, param_type, "n");
        let func = self.context.new_function(
            None,
            FunctionType::Extern,
            dest_typ,
            &[param],
            func_name,
            false,
        );
        if let Some(dest_type) = dest_type {
            value = self.context.new_cast(None, value, dest_type);
        }
        self.context.new_call(None, func, &[value])
    }

    pub fn gcc_float_to_int_cast(&self, value: RValue<'gcc>, dest_typ: Type<'gcc>) -> RValue<'gcc> {
        self.float_to_int_cast(true, value, dest_typ)
    }

    pub fn gcc_float_to_uint_cast(
        &self,
        value: RValue<'gcc>,
        dest_typ: Type<'gcc>,
    ) -> RValue<'gcc> {
        self.float_to_int_cast(false, value, dest_typ)
    }

    fn high(&self, value: RValue<'gcc>) -> RValue<'gcc> {
        let index = match self.sess().target.options.endian {
            Endian::Little => 1,
            Endian::Big => 0,
        };
        self.context
            .new_array_access(None, value, self.context.new_rvalue_from_int(self.int_type, index))
            .to_rvalue()
    }

    fn low(&self, value: RValue<'gcc>) -> RValue<'gcc> {
        let index = match self.sess().target.options.endian {
            Endian::Little => 0,
            Endian::Big => 1,
        };
        self.context
            .new_array_access(None, value, self.context.new_rvalue_from_int(self.int_type, index))
            .to_rvalue()
    }

    fn concat_low_high_rvalues(
        &self,
        typ: Type<'gcc>,
        low: RValue<'gcc>,
        high: RValue<'gcc>,
    ) -> RValue<'gcc> {
        let (first, last) = match self.sess().target.options.endian {
            Endian::Little => (low, high),
            Endian::Big => (high, low),
        };

        let values = [first, last];
        self.context.new_array_constructor(None, typ, &values)
    }

    fn concat_low_high(&self, typ: Type<'gcc>, low: i64, high: i64) -> RValue<'gcc> {
        let (first, last) = match self.sess().target.options.endian {
            Endian::Little => (low, high),
            Endian::Big => (high, low),
        };

        let native_int_type = typ.dyncast_array().expect("get element type");
        let values = [
            self.context.new_rvalue_from_long(native_int_type, first),
            self.context.new_rvalue_from_long(native_int_type, last),
        ];
        self.context.new_array_constructor(None, typ, &values)
    }
}

fn type_kind_to_gcc_type<I: Interner>(kind: TyKind<I>) -> CType {
    use rustc_middle::ty::IntTy::*;
    use rustc_middle::ty::UintTy::*;
    use rustc_middle::ty::{Int, Uint};

    match kind {
        Int(I8) => CType::Int8t,
        Int(I16) => CType::Int16t,
        Int(I32) => CType::Int32t,
        Int(I64) => CType::Int64t,
        Int(I128) => CType::Int128t,

        Uint(U8) => CType::UInt8t,
        Uint(U16) => CType::UInt16t,
        Uint(U32) => CType::UInt32t,
        Uint(U64) => CType::UInt64t,
        Uint(U128) => CType::UInt128t,

        _ => unimplemented!("Kind: {:?}", kind),
    }
}
