"""
Implementation of LLVM IR instructions.
"""

from llvmlite.ir import types
from llvmlite.ir.values import (Block, Function, Value, NamedValue, Constant,
                                MetaDataArgument, MetaDataString, AttributeSet,
                                Undefined, ArgumentAttributes)
from llvmlite.ir._utils import _HasMetadata


class Instruction(NamedValue, _HasMetadata):
    def __init__(self, parent, typ, opname, operands, name='', flags=()):
        super(Instruction, self).__init__(parent, typ, name=name)
        assert isinstance(parent, Block)
        assert isinstance(flags, (tuple, list))
        self.opname = opname
        self.operands = operands
        self.flags = list(flags)
        self.metadata = {}

    @property
    def function(self):
        return self.parent.function

    @property
    def module(self):
        return self.parent.function.module

    def descr(self, buf):
        opname = self.opname
        if self.flags:
            opname = ' '.join([opname] + self.flags)
        operands = ', '.join([op.get_reference() for op in self.operands])
        typ = self.type
        metadata = self._stringify_metadata(leading_comma=True)
        buf.append("{0} {1} {2}{3}\n"
                   .format(opname, typ, operands, metadata))

    def replace_usage(self, old, new):
        if old in self.operands:
            ops = []
            for op in self.operands:
                ops.append(new if op is old else op)
            self.operands = tuple(ops)
            self._clear_string_cache()

    def __repr__(self):
        return "<ir.%s %r of type '%s', opname %r, operands %r>" % (
            self.__class__.__name__, self.name, self.type,
            self.opname, self.operands)


class CallInstrAttributes(AttributeSet):
    _known = frozenset(['convergent', 'noreturn', 'nounwind', 'readonly',
                        'readnone', 'noinline', 'alwaysinline'])


TailMarkerOptions = frozenset(['tail', 'musttail', 'notail'])


class FastMathFlags(AttributeSet):
    _known = frozenset(['fast', 'nnan', 'ninf', 'nsz', 'arcp', 'contract',
                        'afn', 'reassoc'])


class CallInstr(Instruction):
    def __init__(self, parent, func, args, name='', cconv=None, tail=None,
                 fastmath=(), attrs=(), arg_attrs=None):
        self.cconv = (func.calling_convention
                      if cconv is None and isinstance(func, Function)
                      else cconv)

        # For backwards compatibility with previous API of accepting a "truthy"
        # value for a hint to the optimizer to potentially tail optimize.
        if isinstance(tail, str) and tail in TailMarkerOptions:
            pass
        elif tail:
            tail = "tail"
        else:
            tail = ""

        self.tail = tail
        self.fastmath = FastMathFlags(fastmath)
        self.attributes = CallInstrAttributes(attrs)
        self.arg_attributes = {}
        if arg_attrs:
            for idx, attrs in arg_attrs.items():
                if not (0 <= idx < len(args)):
                    raise ValueError("Invalid argument index {}"
                                     .format(idx))
                self.arg_attributes[idx] = ArgumentAttributes(attrs)

        # Fix and validate arguments
        args = list(args)
        for i in range(len(func.function_type.args)):
            arg = args[i]
            expected_type = func.function_type.args[i]
            if (isinstance(expected_type, types.MetaDataType) and
                    arg.type != expected_type):
                arg = MetaDataArgument(arg)
            if arg.type != expected_type:
                msg = ("Type of #{0} arg mismatch: {1} != {2}"
                       .format(1 + i, expected_type, arg.type))
                raise TypeError(msg)
            args[i] = arg

        super(CallInstr, self).__init__(parent, func.function_type.return_type,
                                        "call", [func] + list(args), name=name)

    @property
    def callee(self):
        return self.operands[0]

    @callee.setter
    def callee(self, newcallee):
        self.operands[0] = newcallee

    @property
    def args(self):
        return self.operands[1:]

    def replace_callee(self, newfunc):
        if newfunc.function_type != self.callee.function_type:
            raise TypeError("New function has incompatible type")
        self.callee = newfunc

    @property
    def called_function(self):
        """The callee function"""
        return self.callee

    def _descr(self, buf, add_metadata):
        def descr_arg(i, a):
            if i in self.arg_attributes:
                attrs = ' '.join(self.arg_attributes[i]._to_list(a.type)) + ' '
            else:
                attrs = ''
            return '{0} {1}{2}'.format(a.type, attrs, a.get_reference())
        args = ', '.join([descr_arg(i, a) for i, a in enumerate(self.args)])

        fnty = self.callee.function_type
        # Only print function type if variable-argument
        if fnty.var_arg:
            ty = fnty
        # Otherwise, just print the return type.
        else:
            # Fastmath flag work only in this case
            ty = fnty.return_type
        callee_ref = "{0} {1}".format(ty, self.callee.get_reference())
        if self.cconv:
            callee_ref = "{0} {1}".format(self.cconv, callee_ref)

        tail_marker = ""
        if self.tail:
            tail_marker = "{0} ".format(self.tail)

        fn_attrs = ' ' + ' '.join(self.attributes._to_list(fnty.return_type))\
            if self.attributes else ''

        fm_attrs = ' ' + ' '.join(self.fastmath._to_list(fnty.return_type))\
            if self.fastmath else ''

        buf.append("{tail}{op}{fastmath} {callee}({args}){attr}{meta}\n".format(
            tail=tail_marker,
            op=self.opname,
            callee=callee_ref,
            fastmath=fm_attrs,
            args=args,
            attr=fn_attrs,
            meta=(self._stringify_metadata(leading_comma=True)
                  if add_metadata else ""),
        ))

    def descr(self, buf):
        self._descr(buf, add_metadata=True)


class InvokeInstr(CallInstr):
    def __init__(self, parent, func, args, normal_to, unwind_to, name='',
                 cconv=None, fastmath=(), attrs=(), arg_attrs=None):
        assert isinstance(normal_to, Block)
        assert isinstance(unwind_to, Block)
        super(InvokeInstr, self).__init__(parent, func, args, name, cconv,
                                          tail=False, fastmath=fastmath,
                                          attrs=attrs, arg_attrs=arg_attrs)
        self.opname = "invoke"
        self.normal_to = normal_to
        self.unwind_to = unwind_to

    def descr(self, buf):
        super(InvokeInstr, self)._descr(buf, add_metadata=False)
        buf.append("      to label {0} unwind label {1}{metadata}\n".format(
            self.normal_to.get_reference(),
            self.unwind_to.get_reference(),
            metadata=self._stringify_metadata(leading_comma=True),
        ))


class Terminator(Instruction):
    def __init__(self, parent, opname, operands):
        super(Terminator, self).__init__(parent, types.VoidType(), opname,
                                         operands)

    def descr(self, buf):
        opname = self.opname
        operands = ', '.join(["{0} {1}".format(op.type, op.get_reference())
                              for op in self.operands])
        metadata = self._stringify_metadata(leading_comma=True)
        buf.append("{0} {1}{2}".format(opname, operands, metadata))


class PredictableInstr(Instruction):

    def set_weights(self, weights):
        operands = [MetaDataString(self.module, "branch_weights")]
        for w in weights:
            if w < 0:
                raise ValueError("branch weight must be a positive integer")
            operands.append(Constant(types.IntType(32), w))
        md = self.module.add_metadata(operands)
        self.set_metadata("prof", md)


class Ret(Terminator):
    def __init__(self, parent, opname, return_value=None):
        operands = [return_value] if return_value is not None else []
        super(Ret, self).__init__(parent, opname, operands)

    @property
    def return_value(self):
        if self.operands:
            return self.operands[0]
        else:
            return None

    def descr(self, buf):
        return_value = self.return_value
        metadata = self._stringify_metadata(leading_comma=True)
        if return_value is not None:
            buf.append("{0} {1} {2}{3}\n"
                       .format(self.opname, return_value.type,
                               return_value.get_reference(),
                               metadata))
        else:
            buf.append("{0}{1}\n".format(self.opname, metadata))


class Branch(Terminator):
    pass


class ConditionalBranch(PredictableInstr, Terminator):
    pass


class IndirectBranch(PredictableInstr, Terminator):
    def __init__(self, parent, opname, addr):
        super(IndirectBranch, self).__init__(parent, opname, [addr])
        self.destinations = []

    @property
    def address(self):
        return self.operands[0]

    def add_destination(self, block):
        assert isinstance(block, Block)
        self.destinations.append(block)

    def descr(self, buf):
        destinations = ["label {0}".format(blk.get_reference())
                        for blk in self.destinations]
        buf.append("indirectbr {0} {1}, [{2}]  {3}\n".format(
            self.address.type,
            self.address.get_reference(),
            ', '.join(destinations),
            self._stringify_metadata(leading_comma=True),
        ))


class SwitchInstr(PredictableInstr, Terminator):

    def __init__(self, parent, opname, val, default):
        super(SwitchInstr, self).__init__(parent, opname, [val])
        self.default = default
        self.cases = []

    @property
    def value(self):
        return self.operands[0]

    def add_case(self, val, block):
        assert isinstance(block, Block)
        if not isinstance(val, Value):
            val = Constant(self.value.type, val)
        self.cases.append((val, block))

    def descr(self, buf):
        cases = ["{0} {1}, label {2}".format(val.type, val.get_reference(),
                                             blk.get_reference())
                 for val, blk in self.cases]
        buf.append("switch {0} {1}, label {2} [{3}]  {4}\n".format(
            self.value.type,
            self.value.get_reference(),
            self.default.get_reference(),
            ' '.join(cases),
            self._stringify_metadata(leading_comma=True),
        ))


class Resume(Terminator):
    pass


class SelectInstr(Instruction):
    def __init__(self, parent, cond, lhs, rhs, name='', flags=()):
        assert lhs.type == rhs.type
        super(SelectInstr, self).__init__(parent, lhs.type, "select",
                                          [cond, lhs, rhs], name=name,
                                          flags=flags)

    @property
    def cond(self):
        return self.operands[0]

    @property
    def lhs(self):
        return self.operands[1]

    @property
    def rhs(self):
        return self.operands[2]

    def descr(self, buf):
        buf.append("select {0} {1} {2}, {3} {4}, {5} {6} {7}\n".format(
            ' '.join(self.flags),
            self.cond.type, self.cond.get_reference(),
            self.lhs.type, self.lhs.get_reference(),
            self.rhs.type, self.rhs.get_reference(),
            self._stringify_metadata(leading_comma=True),
        ))


class CompareInstr(Instruction):
    # Define the following in subclasses
    OPNAME = 'invalid-compare'
    VALID_OP = {}

    def __init__(self, parent, op, lhs, rhs, name='', flags=[]):
        if op not in self.VALID_OP:
            raise ValueError("invalid comparison %r for %s" % (op, self.OPNAME))
        for flag in flags:
            if flag not in self.VALID_FLAG:
                raise ValueError("invalid flag %r for %s" % (flag, self.OPNAME))
        opname = self.OPNAME
        if isinstance(lhs.type, types.VectorType):
            typ = types.VectorType(types.IntType(1), lhs.type.count)
        else:
            typ = types.IntType(1)
        super(CompareInstr, self).__init__(parent, typ,
                                           opname, [lhs, rhs], flags=flags,
                                           name=name)
        self.op = op

    def descr(self, buf):
        buf.append("{opname}{flags} {op} {ty} {lhs}, {rhs} {meta}\n".format(
            opname=self.opname,
            flags=''.join(' ' + it for it in self.flags),
            op=self.op,
            ty=self.operands[0].type,
            lhs=self.operands[0].get_reference(),
            rhs=self.operands[1].get_reference(),
            meta=self._stringify_metadata(leading_comma=True),
        ))


class ICMPInstr(CompareInstr):
    OPNAME = 'icmp'
    VALID_OP = {
        'eq': 'equal',
        'ne': 'not equal',
        'ugt': 'unsigned greater than',
        'uge': 'unsigned greater or equal',
        'ult': 'unsigned less than',
        'ule': 'unsigned less or equal',
        'sgt': 'signed greater than',
        'sge': 'signed greater or equal',
        'slt': 'signed less than',
        'sle': 'signed less or equal',
    }
    VALID_FLAG = set()


class FCMPInstr(CompareInstr):
    OPNAME = 'fcmp'
    VALID_OP = {
        'false': 'no comparison, always returns false',
        'oeq': 'ordered and equal',
        'ogt': 'ordered and greater than',
        'oge': 'ordered and greater than or equal',
        'olt': 'ordered and less than',
        'ole': 'ordered and less than or equal',
        'one': 'ordered and not equal',
        'ord': 'ordered (no nans)',
        'ueq': 'unordered or equal',
        'ugt': 'unordered or greater than',
        'uge': 'unordered or greater than or equal',
        'ult': 'unordered or less than',
        'ule': 'unordered or less than or equal',
        'une': 'unordered or not equal',
        'uno': 'unordered (either nans)',
        'true': 'no comparison, always returns true',
    }
    VALID_FLAG = {'nnan', 'ninf', 'nsz', 'arcp', 'contract', 'afn', 'reassoc',
                  'fast'}


class CastInstr(Instruction):
    def __init__(self, parent, op, val, typ, name=''):
        super(CastInstr, self).__init__(parent, typ, op, [val], name=name)

    def descr(self, buf):
        buf.append("{0} {1} {2} to {3} {4}\n".format(
            self.opname,
            self.operands[0].type,
            self.operands[0].get_reference(),
            self.type,
            self._stringify_metadata(leading_comma=True),
        ))


class LoadInstr(Instruction):

    def __init__(self, parent, ptr, name='', typ=None):
        if typ is None:
            if isinstance(ptr, AllocaInstr):
                typ = ptr.allocated_type
            # For compatibility with typed pointers. Eventually this should
            # probably be removed (when typed pointers are fully removed).
            elif not ptr.type.is_opaque:
                typ = ptr.type.pointee
            else:
                raise ValueError("Load lacks type.")
        super(LoadInstr, self).__init__(parent, typ, "load", [ptr], name=name)
        self.align = None

    def descr(self, buf):
        [val] = self.operands
        if self.align is not None:
            align = ', align %d' % (self.align)
        else:
            align = ''
        buf.append("load {0}, {1} {2}{3}{4}\n".format(
            self.type,
            val.type,
            val.get_reference(),
            align,
            self._stringify_metadata(leading_comma=True),
        ))


class StoreInstr(Instruction):
    def __init__(self, parent, val, ptr):
        super(StoreInstr, self).__init__(parent, types.VoidType(), "store",
                                         [val, ptr])

    def descr(self, buf):
        val, ptr = self.operands
        if self.align is not None:
            align = ', align %d' % (self.align)
        else:
            align = ''
        buf.append("store {0} {1}, {2} {3}{4}{5}\n".format(
            val.type,
            val.get_reference(),
            ptr.type,
            ptr.get_reference(),
            align,
            self._stringify_metadata(leading_comma=True),
        ))


class LoadAtomicInstr(Instruction):
    def __init__(self, parent, ptr, ordering, align, name='', typ=None):
        if typ is None:
            if isinstance(ptr, AllocaInstr):
                typ = ptr.allocated_type
            # For compatibility with typed pointers. Eventually this should
            # probably be removed (when typed pointers are fully removed).
            elif not ptr.type.is_opaque:
                typ = ptr.type.pointee
            else:
                raise ValueError("Load atomic lacks type.")
        super(LoadAtomicInstr, self).__init__(parent, typ, "load atomic",
                                              [ptr], name=name)
        self.ordering = ordering
        self.align = align

    def descr(self, buf):
        [val] = self.operands
        buf.append("load atomic {0}, {1} {2} {3}, align {4}{5}\n".format(
            self.type,
            val.type,
            val.get_reference(),
            self.ordering,
            self.align,
            self._stringify_metadata(leading_comma=True),
        ))


class StoreAtomicInstr(Instruction):
    def __init__(self, parent, val, ptr, ordering, align):
        super(StoreAtomicInstr, self).__init__(parent, types.VoidType(),
                                               "store atomic", [val, ptr])
        self.ordering = ordering
        self.align = align

    def descr(self, buf):
        val, ptr = self.operands
        buf.append("store atomic {0} {1}, {2} {3} {4}, align {5}{6}\n".format(
            val.type,
            val.get_reference(),
            ptr.type,
            ptr.get_reference(),
            self.ordering,
            self.align,
            self._stringify_metadata(leading_comma=True),
        ))


class AllocaInstr(Instruction):
    def __init__(self, parent, typ, count, name):
        operands = [count] if count else ()
        super(AllocaInstr, self).__init__(parent, typ.as_pointer(), "alloca",
                                          operands, name)
        self.allocated_type = typ
        self.align = None

    def descr(self, buf):
        buf.append("{0} {1}".format(self.opname, self.allocated_type))
        if self.operands:
            op, = self.operands
            buf.append(", {0} {1}".format(op.type, op.get_reference()))
        if self.align is not None:
            buf.append(", align {0}".format(self.align))
        if self.metadata:
            buf.append(self._stringify_metadata(leading_comma=True))


class GEPInstr(Instruction):
    def __init__(self, parent, ptr, indices, inbounds, name,
                 source_etype=None):
        if source_etype is not None:
            typ = ptr.type
            self.source_etype = source_etype
        # For compatibility with typed pointers. Eventually this should
        # probably be removed (when typed pointers are fully removed).
        elif not ptr.type.is_opaque:
            typ = ptr.type
            lasttyp = None
            lastaddrspace = 0
            for i in indices:
                lasttyp, typ = typ, typ.gep(i)
                # inherit the addrspace from the last seen pointer
                if isinstance(lasttyp, types.PointerType):
                    lastaddrspace = lasttyp.addrspace

            if (not isinstance(typ, types.PointerType) and
                    isinstance(lasttyp, types.PointerType)):
                typ = lasttyp
            else:
                typ = typ.as_pointer(lastaddrspace)
            self.source_etype = ptr.type.pointee
        else:
            raise ValueError("GEP lacks type.")
        super(GEPInstr, self).__init__(parent, typ, "getelementptr",
                                       [ptr] + list(indices), name=name)
        self.pointer = ptr
        self.indices = indices
        self.inbounds = inbounds

    def descr(self, buf):
        indices = ['{0} {1}'.format(i.type, i.get_reference())
                   for i in self.indices]
        op = "getelementptr inbounds" if self.inbounds else "getelementptr"
        buf.append("{0} {1}, {2} {3}, {4} {5}\n".format(
                   op,
                   self.source_etype,
                   self.pointer.type,
                   self.pointer.get_reference(),
                   ', '.join(indices),
                   self._stringify_metadata(leading_comma=True),
                   ))


class PhiInstr(Instruction):
    def __init__(self, parent, typ, name, flags=()):
        super(PhiInstr, self).__init__(parent, typ, "phi", (), name=name,
                                       flags=flags)
        self.incomings = []

    def descr(self, buf):
        incs = ', '.join('[{0}, {1}]'.format(v.get_reference(),
                                             b.get_reference())
                         for v, b in self.incomings)
        buf.append("phi {0} {1} {2} {3}\n".format(
                   ' '.join(self.flags),
                   self.type,
                   incs,
                   self._stringify_metadata(leading_comma=True),
                   ))

    def add_incoming(self, value, block):
        assert isinstance(block, Block)
        self.incomings.append((value, block))

    def replace_usage(self, old, new):
        self.incomings = [((new if val is old else val), blk)
                          for (val, blk) in self.incomings]


class ExtractElement(Instruction):
    def __init__(self, parent, vector, index, name=''):
        if not isinstance(vector.type, types.VectorType):
            raise TypeError("vector needs to be of VectorType.")
        if not isinstance(index.type, types.IntType):
            raise TypeError("index needs to be of IntType.")
        typ = vector.type.element
        super(ExtractElement, self).__init__(parent, typ, "extractelement",
                                             [vector, index], name=name)

    def descr(self, buf):
        operands = ", ".join("{0} {1}".format(
                   op.type, op.get_reference()) for op in self.operands)
        buf.append("{opname} {operands}\n".format(
                   opname=self.opname, operands=operands))


class InsertElement(Instruction):
    def __init__(self, parent, vector, value, index, name=''):
        if not isinstance(vector.type, types.VectorType):
            raise TypeError("vector needs to be of VectorType.")
        if not value.type == vector.type.element:
            raise TypeError(
                "value needs to be of type {} not {}.".format(
                    vector.type.element, value.type))
        if not isinstance(index.type, types.IntType):
            raise TypeError("index needs to be of IntType.")
        typ = vector.type
        super(InsertElement, self).__init__(parent, typ, "insertelement",
                                            [vector, value, index], name=name)

    def descr(self, buf):
        operands = ", ".join("{0} {1}".format(
                   op.type, op.get_reference()) for op in self.operands)
        buf.append("{opname} {operands}\n".format(
                   opname=self.opname, operands=operands))


class ShuffleVector(Instruction):
    def __init__(self, parent, vector1, vector2, mask, name=''):
        if not isinstance(vector1.type, types.VectorType):
            raise TypeError("vector1 needs to be of VectorType.")
        if vector2 != Undefined:
            if vector2.type != vector1.type:
                raise TypeError("vector2 needs to be " +
                                "Undefined or of the same type as vector1.")
        if (not isinstance(mask, Constant) or
            not isinstance(mask.type, types.VectorType) or
            not (isinstance(mask.type.element, types.IntType) and
                 mask.type.element.width == 32)):
            raise TypeError("mask needs to be a constant i32 vector.")
        typ = types.VectorType(vector1.type.element, mask.type.count)
        index_range = range(vector1.type.count
                            if vector2 == Undefined
                            else 2 * vector1.type.count)
        if not all(ii.constant in index_range for ii in mask.constant):
            raise IndexError(
                "mask values need to be in {0}".format(index_range),
            )
        super(ShuffleVector, self).__init__(parent, typ, "shufflevector",
                                            [vector1, vector2, mask], name=name)

    def descr(self, buf):
        buf.append("shufflevector {0} {1}\n".format(
                   ", ".join("{0} {1}".format(op.type, op.get_reference())
                             for op in self.operands),
                   self._stringify_metadata(leading_comma=True),
                   ))


class ExtractValue(Instruction):
    def __init__(self, parent, agg, indices, name=''):
        typ = agg.type
        try:
            for i in indices:
                typ = typ.elements[i]
        except (AttributeError, IndexError):
            raise TypeError("Can't index at %r in %s"
                            % (list(indices), agg.type))

        super(ExtractValue, self).__init__(parent, typ, "extractvalue",
                                           [agg], name=name)

        self.aggregate = agg
        self.indices = indices

    def descr(self, buf):
        indices = [str(i) for i in self.indices]

        buf.append("extractvalue {0} {1}, {2} {3}\n".format(
                   self.aggregate.type,
                   self.aggregate.get_reference(),
                   ', '.join(indices),
                   self._stringify_metadata(leading_comma=True),
                   ))


class InsertValue(Instruction):
    def __init__(self, parent, agg, elem, indices, name=''):
        typ = agg.type
        try:
            for i in indices:
                typ = typ.elements[i]
        except (AttributeError, IndexError):
            raise TypeError("Can't index at %r in %s"
                            % (list(indices), agg.type))
        if elem.type != typ:
            raise TypeError("Can only insert %s at %r in %s: got %s"
                            % (typ, list(indices), agg.type, elem.type))
        super(InsertValue, self).__init__(parent, agg.type, "insertvalue",
                                          [agg, elem], name=name)

        self.aggregate = agg
        self.value = elem
        self.indices = indices

    def descr(self, buf):
        indices = [str(i) for i in self.indices]

        buf.append("insertvalue {0} {1}, {2} {3}, {4} {5}\n".format(
                   self.aggregate.type, self.aggregate.get_reference(),
                   self.value.type, self.value.get_reference(),
                   ', '.join(indices),
                   self._stringify_metadata(leading_comma=True),
                   ))


class Unreachable(Instruction):
    def __init__(self, parent):
        super(Unreachable, self).__init__(parent, types.VoidType(),
                                          "unreachable", (), name='')

    def descr(self, buf):
        buf += (self.opname, "\n")


class InlineAsm(object):
    def __init__(self, ftype, asm, constraint, side_effect=False):
        self.type = ftype.return_type
        self.function_type = ftype
        self.asm = asm
        self.constraint = constraint
        self.side_effect = side_effect

    def descr(self, buf):
        sideeffect = 'sideeffect' if self.side_effect else ''
        fmt = 'asm {sideeffect} "{asm}", "{constraint}"\n'
        buf.append(fmt.format(sideeffect=sideeffect, asm=self.asm,
                              constraint=self.constraint))

    def get_reference(self):
        buf = []
        self.descr(buf)
        return "".join(buf)

    def __str__(self):
        return "{0} {1}".format(self.type, self.get_reference())


class AtomicRMW(Instruction):
    def __init__(self, parent, op, ptr, val, ordering, name):
        super(AtomicRMW, self).__init__(parent, val.type, "atomicrmw",
                                        (ptr, val), name=name)
        self.operation = op
        self.ordering = ordering

    def descr(self, buf):
        ptr, val = self.operands
        fmt = ("atomicrmw {op} {ptrty} {ptr}, {valty} {val} {ordering} "
               "{metadata}\n")
        buf.append(fmt.format(op=self.operation,
                              ptrty=ptr.type,
                              ptr=ptr.get_reference(),
                              valty=val.type,
                              val=val.get_reference(),
                              ordering=self.ordering,
                              metadata=self._stringify_metadata(
                                  leading_comma=True),
                              ))


class CmpXchg(Instruction):
    """This instruction has changed since llvm3.5.  It is not compatible with
    older llvm versions.
    """

    def __init__(self, parent, ptr, cmp, val, ordering, failordering, name):
        outtype = types.LiteralStructType([val.type, types.IntType(1)])
        super(CmpXchg, self).__init__(parent, outtype, "cmpxchg",
                                      (ptr, cmp, val), name=name)
        self.ordering = ordering
        self.failordering = failordering

    def descr(self, buf):
        ptr, cmpval, val = self.operands
        fmt = "cmpxchg {ptrty} {ptr}, {ty} {cmp}, {ty} {val} {ordering} " \
              "{failordering} {metadata}\n"
        buf.append(fmt.format(ptrty=ptr.type,
                              ptr=ptr.get_reference(),
                              ty=cmpval.type,
                              cmp=cmpval.get_reference(),
                              val=val.get_reference(),
                              ordering=self.ordering,
                              failordering=self.failordering,
                              metadata=self._stringify_metadata(
                                  leading_comma=True),
                              ))


class _LandingPadClause(object):
    def __init__(self, value):
        self.value = value

    def __str__(self):
        return "{kind} {type} {value}".format(
            kind=self.kind,
            type=self.value.type,
            value=self.value.get_reference())


class CatchClause(_LandingPadClause):
    kind = 'catch'


class FilterClause(_LandingPadClause):
    kind = 'filter'

    def __init__(self, value):
        assert isinstance(value, Constant)
        assert isinstance(value.type, types.ArrayType)
        super(FilterClause, self).__init__(value)


class LandingPadInstr(Instruction):
    def __init__(self, parent, typ, name='', cleanup=False):
        super(LandingPadInstr, self).__init__(parent, typ, "landingpad", [],
                                              name=name)
        self.cleanup = cleanup
        self.clauses = []

    def add_clause(self, clause):
        assert isinstance(clause, _LandingPadClause)
        self.clauses.append(clause)

    def descr(self, buf):
        fmt = "landingpad {type}{cleanup}{clauses}\n"
        buf.append(fmt.format(type=self.type,
                              cleanup=' cleanup' if self.cleanup else '',
                              clauses=''.join(["\n      {0}".format(clause)
                                               for clause in self.clauses]),
                              ))


class Fence(Instruction):
    """
    The `fence` instruction.

    As of LLVM 5.0.1:

    fence [syncscope("<target-scope>")] <ordering>  ; yields void
    """

    VALID_FENCE_ORDERINGS = {"acquire", "release", "acq_rel", "seq_cst"}

    def __init__(self, parent, ordering, targetscope=None, name=''):
        super(Fence, self).__init__(parent, types.VoidType(), "fence", (),
                                    name=name)
        if ordering not in self.VALID_FENCE_ORDERINGS:
            msg = "Invalid fence ordering \"{0}\"! Should be one of {1}."
            raise ValueError(msg .format(ordering,
                                         ", ".join(self.VALID_FENCE_ORDERINGS)))
        self.ordering = ordering
        self.targetscope = targetscope

    def descr(self, buf):
        if self.targetscope is None:
            syncscope = ""
        else:
            syncscope = 'syncscope("{0}") '.format(self.targetscope)

        fmt = "fence {syncscope}{ordering}\n"
        buf.append(fmt.format(syncscope=syncscope,
                              ordering=self.ordering,
                              ))


class Comment(Instruction):
    """
    A line comment.
    """

    def __init__(self, parent, text):
        super(Comment, self).__init__(parent, types.VoidType(), ";", (),
                                      name='')
        assert "\n" not in text, "Comment cannot contain new line"
        self.text = text

    def descr(self, buf):
        buf.append(f"; {self.text}")
