Binary operation: add

        :param b: The other operand
        :return: self + b

def add(self, b):
        """
        Binary operation: add

        :param b: The other operand
        :return: self + b
        """
        new_bits = max(self.bits, b.bits)

        # TODO: Some improvements can be made here regarding the following case
        # TODO: SI<16>0xff[0x0, 0xff] + 3
        # TODO: In current implementation, it overflows, but it doesn't have to

        # optimization
        # case: SI<16>0xff[0x0, 0xff] + 3
        """
        if self.is_top and b.is_integer:
            si = self.copy()
            si.lower_bound = b.lower_bound
            return si
        elif b.is_top and self.is_integer:
            si = b.copy()
            si.lower_bound = self.lower_bound
            return si
        """
        #FIXME

        overflow = self._wrapped_overflow_add(self, b)
        if overflow:
            return StridedInterval.top(self.bits)

        lb = self._modular_add(self.lower_bound, b.lower_bound, new_bits)
        ub = self._modular_add(self.upper_bound, b.upper_bound, new_bits)

        # Is it initialized?
        uninitialized = self.uninitialized or b.uninitialized

        # Take the GCD of two operands' strides
        stride = fractions.gcd(self.stride, b.stride)

        return StridedInterval(bits=new_bits, stride=stride, lower_bound=lb, upper_bound=ub,
                               uninitialized=uninitialized).normalize()