#!/usr/bin/env python
# -*- coding: utf8 -*-
"""
Copyright (c) 2011 Tyler Kenendy <tk@tkte.ch>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
"""

import re
import sys
import traceback
import six
import six.moves

from types import LambdaType

from jawa.util.descriptor import method_descriptor, parse_descriptor
from jawa.constants import *
from jawa.transforms import simple_swap

from .topping import Topping
from burger.util import InvokeDynamicInfo, REF_invokeStatic

SUB_INS_EPSILON = .01
PACKETBUF_NAME = "packetbuffer" # Used to specially identify the PacketBuffer we care about

class PacketInstructionsTopping(Topping):
    """Provides the instructions used to construct network packets."""

    PROVIDES = [
        "packets.instructions"
    ]

    DEPENDS = [
        "packets.classes",
        "identify.packet.packetbuffer",
        "identify.nbtcompound",
        "identify.itemstack",
        "identify.chatcomponent",
        "identify.metadata",
        "identify.resourcelocation"
    ]

    TYPES = {
        "writeBoolean": "boolean",
        "writeByte": "byte",
        "writeBytes": "byte[]",
        "writeChar": "char",
        "writeDouble": "double",
        "writeFloat": "float",
        "writeInt": "int",
        "writeLong": "long",
        "writeShort": "short"
    }

    CACHE = {}

    # Simple instructions are registered below
    OPCODES = {}

    @classmethod
    def register_ins(cls, opcodes, stack_count, template, extra_method=None, category=1):
        """
        Registers an instruction handler.  This should be used for instructions
        that pop some one or more things from the stack and then push a new
        value onto it.

        opcodes: A single opcode or a list of opcodes for that handler
        stack_count: The number of things to pop from the stack
        template: A format string; uses stack and operands (and extra if given)
        extra_method: Used to get a bit of additional information.  Param is ins
        category: JVM category for the resulting StackOperand
        """
        if isinstance(opcodes, six.string_types):
            opcodes = [opcodes]
        data = {
            "stack_count": stack_count,
            "template": template,
            "extra_method": extra_method,
            "category": category
        }
        for opcode in opcodes:
            cls.OPCODES[opcode] = data

    # Prefix types used in instructions
    INSTRUCTION_TYPES = {
        'a': 'Object',
        'b': 'boolean',
        'c': 'char',
        'd': 'double',
        'f': 'float',
        'i': 'int',
        'l': 'long',
        's': 'short'
    }

    CLEANUP_PATTERN = [
        (re.compile("^\((.*[^(])\)$"), "\\1"),
        (re.compile("(^|[() ])this\."), "\\1")
    ]

    @staticmethod
    def act(aggregate, classloader, verbose=False):
        """Finds all packets and decompiles them"""
        for key, packet in six.iteritems(aggregate["packets"]["packet"]):
            operations = None
            try:
                classname = packet["class"][:-len(".class")]
                operations = _PIT.class_operations(classloader, classname, aggregate["classes"], verbose)
                packet.update(_PIT.format(operations))
            except Exception as e:
                if verbose:
                    print("Error: Failed to parse instructions of packet %s (%s): %s" % (key, packet["class"], e))
                    traceback.print_exc()
                    if operations:
                        import json
                        print(json.dumps(operations, default=lambda o:o.__dict__, indent=4))
                    print()

    @staticmethod
    def class_operations(classloader, classname, classes, verbose):
        """Decompiles the instructions for a specific packet."""
        # Find the writing method
        cf = classloader[classname]

        methods = list(cf.methods.find(returns="V", args="L" + classes["packet.packetbuffer"] + ";"))

        if len(methods) == 2:
            # Assume the second method is the one that writes
            method = methods[1]
        elif len(methods) == 1:
            # 21w08a+: A constructor or static method now handles reading.
            # The constructor still returns void, so the above case is still
            # usually hit, but the static method returns the packet.  When
            # the static method exists, there only is one matching method,
            # so just assume that that method handles writing.
            method = methods[0]
        else:
            assert len(methods) == 0 # There shouldn't be more than 2 packetbuffer-related methods
            if cf.super_.name.value != "java/lang/Object":
                # Try the superclass
                return _PIT.class_operations(classloader, cf.super_.name.value, classes, verbose)
            else:
                raise Exception("Failed to find method in class or superclass")

        assert not method.access_flags.acc_static
        assert not method.access_flags.acc_abstract

        return _PIT.operations(classloader, cf, classes, verbose, method, ("this", PACKETBUF_NAME))

    @staticmethod
    def operations(classloader, cf, classes, verbose, method, arg_names):
        """Decompiles the specified method."""
        if method.access_flags.acc_static:
            assert len(arg_names) == len(method.args)
        else:
            # `this` is a local variable and thus needs to be counted.
            assert len(arg_names) == len(method.args) + 1

        # Decode the instructions
        operations = []
        stack = []
        skip_until = -1
        shortif_pos = None
        shortif_cond = None

        # NOTE: we only use the simple_swap transform here due to the
        # expand_constants transform making it hard to use InstructionField
        # InstructionField should probably be cleaned up first
        for instruction in method.code.disassemble(transforms=[simple_swap]):
            if skip_until != -1:
                if instruction.pos == skip_until:
                    skip_until = -1
                else:
                    continue

            mnemonic = instruction.mnemonic
            operands = [InstructionField(operand, instruction, cf.constants)
                        for operand in instruction.operands]

            # Shortcut if
            if instruction.pos == shortif_pos:
                # Check to make sure that this actually is a ternary if
                assert len(operations) >= 3
                assert operations[-1].operation == "endif"
                assert operations[-2].operation == "else"
                assert operations[-3].operation == "if"
                # Now get rid of the unneeded if's
                operations.pop()
                operations.pop()
                operations.pop()
                category = stack[-1].category
                stack.append(StackOperand("((%(cond)s) ? %(sec)s : %(first)s)" % {
                    "cond": shortif_cond,
                    "first": stack.pop(),
                    "sec": stack.pop()
                }, category))
                shortif_cond = None
                shortif_pos = None

            # Method calls
            if mnemonic in ("invokevirtual", "invokespecial", "invokestatic", "invokeinterface"):
                operations.extend(_PIT._handle_invoke(
                    classloader, classes, instruction, verbose, operands[0].c,
                    operands[0].name, method_descriptor(operands[0].descriptor), stack
                ))

            elif mnemonic == "invokedynamic":
                InvokeDynamicInfo.create(instruction, cf).apply_to_stack(stack)

            # Conditional statements and loops
            elif mnemonic.startswith("if"):
                if "icmp" in mnemonic or "acmp" in mnemonic:
                    value2 = stack.pop()
                    value1 = stack.pop()
                elif "null" in mnemonic:
                    value1 = stack.pop()
                    value2 = "null"
                else:
                    value1 = stack.pop()
                    value2 = 0

                # All conditions are reversed: if the condition in the mnemonic
                # passes, then we'd jump; thus, to execute the following code,
                # the condition must _not_ pass
                if mnemonic in ("ifeq", "if_icmpeq", "if_acmpeq", "ifnull"):
                    comparison = "!="
                elif mnemonic in ("ifne", "if_icmpne", "if_acmpne", "ifnonnull"):
                    comparison = "=="
                elif mnemonic in ("iflt", "if_icmplt"):
                    comparison = ">="
                elif mnemonic in ("ifge", "if_icmpge"):
                    comparison = "<"
                elif mnemonic in ("ifgt", "if_icmpgt"):
                    comparison = "<="
                elif mnemonic in ("ifle", "if_icmple"):
                    comparison = ">"
                else:
                    raise Exception("Unknown if mnemonic %s (0x%x)" % (mnemonic, instruction.opcode))

                if comparison == "!=" and value2 == 0:
                    # if (something != 0) -> if (something)
                    condition = value1
                else:
                    condition = "%s %s %s" % (value1, comparison, value2)

                operations.append(Operation(instruction.pos, "if",
                                            condition=condition))
                operations.append(Operation(operands[0].target, "endif"))
                if shortif_pos is not None:
                    # Clearly not a ternary-if if we have another nested if
                    # (assuming that it's not a nested ternary, which we
                    # already don't handle for other reasons)
                    # If we don't do this, then the following code can have
                    # problems:
                    # if (a) {
                    #     if (b) {
                    #         // ...
                    #     }
                    # } else if (c) {
                    #     // ...
                    # }
                    # as there would be a goto instruction to skip the
                    # `else if (c)` portion that would be parsed as a shortif
                    shortif_pos = None
                shortif_cond = condition

            elif mnemonic == "tableswitch":
                operations.append(Operation(instruction.pos, "switch",
                                            field=stack.pop()))

                default = operands[0].target
                low = operands[1].value
                high = operands[2].value
                for opr in six.moves.range(3, len(operands)):
                    target = operands[opr].target
                    operations.append(Operation(target, "case",
                                                value=low + opr - 3))
                # TODO: Default might not be the right place for endswitch,
                # though it seems like default isn't used in any other way
                # in the normal code.
                operations.append(Operation(default, "endswitch"))

            elif mnemonic == "lookupswitch":
                raise Exception("lookupswitch is not supported")
                # operations.append(Operation(instruction.pos, "switch",
                #                             field=stack.pop()))
                # for opr in six.moves.range(1, len(operands)):
                #     target = operands[opr].find_target(1)
                #     operations.append(Operation(target, "case",
                #                                 value=operands[opr].value[0]))
                # operations.append(Operation(operands[0].target, "endswitch"))

            elif mnemonic == "goto":
                target = operands[0].target
                endif = _PIT.find_next(operations, instruction.pos, "endif")
                case = _PIT.find_next(operations, instruction.pos, "case")
                if case is not None and target > case.position:
                    operations.append(Operation(instruction.pos, "break"))
                elif endif is not None:
                    if target > instruction.pos:
                        endif.operation = "else"
                        operations.append(Operation(target, "endif"))
                        if len(stack) != 0:
                            shortif_pos = target
                    else:
                        endif.operation = "endloop"
                        _PIT.find_next(
                            operations, target, "if"
                        ).operation = "loop"
                elif target > instruction.pos:
                    skip_until = target

            elif mnemonic == "iinc":
                operations.append(Operation(instruction.pos, "increment",
                                            field="var%s" % operands[0],
                                            amount=operands[1]))

            # Other manually handled instructions
            elif mnemonic == "multianewarray":
                operand = ""
                for i in six.moves.range(operands[1].value):
                    operand = "[%s]%s" % (stack.pop(), operand)
                stack.append(StackOperand(
                    "new %s%s" % (operands[0].type, operand)))
            elif mnemonic == "pop":
                stack.pop()
            elif mnemonic == "pop2":
                if stack.pop().category != 2:
                    stack.pop()
            elif mnemonic == "swap":
                stack[-2], stack[-1] = stack[-1], stack[-2]
            elif mnemonic == "dup":
                stack.append(stack[-1])
            elif mnemonic == "dup_x1":
                stack.insert(-2, stack[-1])
            elif mnemonic == "dup_x2":
                stack.insert(-2 if stack[-2].category == 2 else -3, stack[-1])
            elif mnemonic == "dup2":
                if stack[-1].category == 2:
                    stack.append(stack[-1])
                else:
                    stack += stack[-2:]
            elif mnemonic == "dup2_x1":
                if stack[-1].category == 2:
                    stack.insert(-2, stack[-1])
                else:
                    stack.insert(-3, stack[-2])
                    stack.insert(-3, stack[-1])
            elif mnemonic == "dup2_x2":
                if stack[-1].category == 2:
                    stack.insert(
                        -2 if stack[-2].category == 2 else -3, stack[-1]
                    )
                else:
                    stack.insert(
                        -3 if stack[-3].category == 2 else -4, stack[-2]
                    )
                    stack.insert(
                        -3 if stack[-3].category == 2 else -4, stack[-1]
                    )
            elif mnemonic == "return":
                # Don't attempt to lookup the instruction in the handler
                pass

            elif instruction in ("istore", "lstore", "fstore", "dstore", "astore"):
                # Keep track of what is being stored, for clarity
                type = _PIT.INSTRUCTION_TYPES[instruction.mnemonic[0]]
                arg = operands.pop().value

                var = arg_names[arg] if arg < len(arg_names) else "var%s" % arg
                operations.append(Operation(instruction.pos, "store",
                                            type=type,
                                            var=var,
                                            value=stack.pop()))

            elif instruction in ("iastore", "lastore", "fastore", "dastore", "aastore", "bastore", "castore", "sastore"):
                type = _PIT.INSTRUCTION_TYPES[instruction.mnemonic[0]]

                # Array store
                value = stack.pop()
                index = stack.pop()
                array = stack.pop()
                operations.append(Operation(instruction.pos, "arraystore",
                                            type=type,
                                            index=index,
                                            var=array,
                                            value=value))

            # Default handlers
            else:
                if mnemonic not in _PIT.OPCODES:
                    raise Exception("Unhandled instruction opcode %s (0x%x)" % (mnemonic, instruction.opcode))

                handler = _PIT.OPCODES[mnemonic]

                ins_stack = []
                assert len(stack) >= handler["stack_count"]

                for _ in six.moves.range(handler["stack_count"]):
                    ins_stack.insert(0, stack.pop())

                ctx = {
                    "operands": operands,
                    "stack": ins_stack,
                    "ins": instruction,
                    "arg_names": arg_names
                }

                if handler["extra_method"]:
                    ctx["extra"] = handler["extra_method"](ctx)

                category = handler["category"]
                try:
                    formatted = handler["template"].format(**ctx)
                except Exception as ex:
                    raise Exception("Failed to format info for %s (0x%x) with template %s and ctx %s: %s" %
                        (mnemonic, instruction.opcode, handler["template"], ctx, ex))

                stack.append(StackOperand(formatted, handler["category"]))

        return operations

    @staticmethod
    def _handle_invoke(classloader, classes, instruction, verbose,
                      cls, name, desc, stack):
        """
        Handles invocation of a method, returning the operations for it and also
        updating the stack.
        """

        num_arguments = len(desc.args)
        assert len(stack) >= num_arguments
        if num_arguments > 0:
            arguments = stack[-num_arguments:]
        else:
            arguments = []
        for i in six.moves.range(num_arguments):
            stack.pop()

        is_static = (instruction.mnemonic == "invokestatic")
        obj = cls if is_static else stack.pop()

        if name in _PIT.TYPES:
            # Builtin netty buffer methods
            assert num_arguments == 1
            assert not is_static
            # These methods always return the same buffer.
            stack.append(obj)
            return [Operation(instruction.pos, "write", type=_PIT.TYPES[name],
                              field=arguments[0])]
        elif len(name) == 1 and isinstance(obj, StackOperand) and obj.value == PACKETBUF_NAME:
            # Checking len(name) == 1 is used to see if it's a Minecraft method
            # (due to obfuscation).  Netty methods have real (and thus longer) names.
            if num_arguments == 1:
                result = _PIT._handle_1_arg_buffer_call(classloader, classes,
                                                        instruction, verbose,
                                                        cls, name, desc, obj,
                                                        arguments[0])
            elif num_arguments == 2:
                result = _PIT._handle_2_arg_buffer_call(classloader, classes,
                                                        instruction, verbose,
                                                        cls, name, desc, obj,
                                                        arguments)
            elif num_arguments == 3:
                result = _PIT._handle_3_arg_buffer_call(classloader, classes,
                                                        instruction, verbose,
                                                        cls, name, desc, obj,
                                                        arguments)
            else:
                raise Exception("Unexpected num_arguments: " + str(num_arguments) + " - desc " + desc)

            if desc.returns.name == classes["packet.packetbuffer"]:
                # Return the packetbuffer back to the stack.
                stack.append(obj)
            elif desc.returns.name != "void":
                if verbose:
                    print("PacketBuffer method that returns something other than PacketBuffer used!")
                stack.append(object())

            return result
        elif name == "<init>":
            # Constructor call.  Should have the instance right on the stack
            # as well (due to constructors returning void).
            # Add the arguments to that object.
            assert stack[-1] is obj
            assert isinstance(obj, StackOperand)
            obj.value += "(" + _PIT.join(arguments) + ")";
            return []
        elif name == "forEach":
            assert num_arguments == 1
            assert not is_static
            return _PIT._handle_foreach(classloader, classes, instruction, verbose,
                                        cls, name, desc, obj, arguments[0])
        else:
            if desc.returns.name != "void":
                # Assume that any function that returns something does not write
                # to the buffer.
                stack.append(StackOperand(
                    "%s.%s(%s)" % (
                        obj, name, _PIT.join(arguments)
                    ),
                    2 if desc.returns.name in ("long", "double") else 1)
                )
                return []
            else:
                for arg in desc.args:
                    if arg.name == classes["packet.packetbuffer"]:
                        if cls == classes["metadata"]:
                            # Special case - metadata is a complex type but
                            # well documented; we don't want to include its
                            # exact writing but just want to instead say
                            # 'metadata'.

                            # There are two cases - one is calling an
                            # instance method of metadata that writes
                            # out the instance, and the other is a
                            # static method that takes a list and then
                            # writes that list.
                            return [Operation(instruction.pos, "write", type="metadata",
                                              field=obj if not is_static else arguments[0])]

                        # If calling a sub-method that takes a packetbuffer as a
                        # parameter, it's possible that it's a sub-method that
                        # writes to the buffer, so we need to check it.
                        # Note that we do this even if the method is abstract
                        # or part of an interface; _sub_operations checks that.
                        return _PIT._sub_operations(
                            classloader, classes, instruction, verbose,
                            cls, name, desc,
                            [obj] + arguments if not is_static else arguments
                        )
                else:
                    # Call to a method that does not take a packetbuffer.
                    # It might have side-effects, but we don't know what they
                    # would be and can't do anything with them.
                    if verbose:
                        print("Call to %s.%s%s does not use buffer; ignoring" % (cls, name, desc.descriptor))
                    return []

    @staticmethod
    def _handle_1_arg_buffer_call(classloader, classes, instruction, verbose,
                                  cls, name, desc, instance, arg):
        arg_type = desc.args[0].name

        if desc.args[0].dimensions == 1:
            # Array methods, which prefix a length
            operations = [Operation(instruction.pos, "write",
                                    type="varint", field="%s.length" % arg)]
            if arg_type == "byte":
                operations.append(Operation(instruction.pos, "write",
                                            type="byte[]", field=arg))
            elif arg_type == "int":
                operations.append(Operation(instruction.pos, "write",
                                            type="varint[]", field=arg))
            elif arg_type == "long":
                operations.append(Operation(instruction.pos, "write",
                                            type="long[]", field=arg))
            else:
                raise Exception("Unexpected array type: " + arg_type)
            return operations

        assert desc.args[0].dimensions == 0
        if arg_type == "java/lang/String":
            max_length = 32767 # not using this at the time
            return [Operation(instruction.pos, "write", type="string", field=arg)]
        elif arg_type == "java/util/UUID":
            return [Operation(instruction.pos, "write", type="uuid", field=arg)]
        elif arg_type == "java/util/Date":
            return [Operation(instruction.pos, "write", type="long", field="%s.getTime()" % arg)]
        elif arg_type == "int":
            # We know that the obfuscated function that takes an int or long is
            # the VarInt/VarLong version, and the non-obfuscated one with a netty
            # name is the regular version.
            return [Operation(instruction.pos, "write", type="varint", field=arg)]
        elif arg_type == "long":
            return [Operation(instruction.pos, "write", type="varlong", field=arg)]
        elif arg_type == "java/lang/Enum":
            # If we were using the read method instead of the write method, then we could get the class for this enum...
            return [Operation(instruction.pos, "write", type="enum", field=arg)]
        elif arg_type == classes["nbtcompound"]:
            return [Operation(instruction.pos, "write", type="nbtcompound", field=arg)]
        elif arg_type == classes["itemstack"]:
            return [Operation(instruction.pos, "write", type="itemstack", field=arg)]
        elif arg_type == classes["chatcomponent"]:
            return [Operation(instruction.pos, "write", type="chatcomponent", field=arg)]
        elif arg_type == classes["identifier"]:
            return [Operation(instruction.pos, "write", type="identifier", field=arg)]
        elif "position" not in classes or arg_type == classes["position"]:
            if "position" not in classes:
                classes["position"] = arg_type
                if verbose:
                    print("Assuming", arg_type, "is the position class")
            return [Operation(instruction.pos, "write", type="position", field=arg)]

        # Unknown type in packetbuffer; try inlining it as well
        # (on the assumption that it's something made of a few calls,
        # and not e.g. writeVarInt)
        if verbose:
            print("Inlining PacketBuffer.%s(%s)" % (name, arg_type))
        return _PIT._sub_operations(classloader, classes, instruction, verbose,
                                    cls, name, desc, [instance, arg])

    @staticmethod
    def _handle_2_arg_buffer_call(classloader, classes, instruction, verbose,
                                  cls, name, desc, instance, args):
        if desc.args[0].name == "java/lang/String" and desc.args[1].name == "int":
            max_length = args[1] # not using this at this time
            return [Operation(instruction.pos, "write", type="string", field=args[0])]
        elif desc.args[0].name == "com/mojang/serialization/Codec":
            codec = args[0]
            value = args[1]
            # This isn't the exact syntax used by DataFixerUpper,
            # but it's close enough for our purposes
            field = "%s.encode(%s)" % (codec, value)
            return [Operation(instruction.pos, "write", type="nbtcompound", field=field)]
        elif desc.args[0].name == "java/util/Collection" and \
                desc.args[1].name == "java/util/function/BiConsumer":
            # Loop that calls the consumer with the packetbuffer
            # and value for each value in collection
            # TODO: Disambiguate names it and itv if there are multiple loops
            operations = []
            field = args[0]
            assert isinstance(field, StackOperand)
            operations.append(Operation(instruction.pos, "write", type="varint",
                                        field=field.value + ".size()"))
            operations.append(Operation(instruction.pos, "store",
                                        type="Iterator", var="it",
                                        value=field.value + ".iterator()"))
            operations.append(Operation(instruction.pos, "loop",
                                        condition="it.hasNext()"))
            info = args[1]
            assert isinstance(info, InvokeDynamicInfo)
            operations.append(Operation(instruction.pos, "store",
                                        type=info.method_desc.args[-1].name.replace("/", "."),
                                        var="itv", value="it.next()"))
            operations += _PIT._lambda_operations(
                classloader, classes, instruction, verbose,
                info, [instance, "itv"]
            )
            # Jank: the part of the program that converts loop+endloop
            # to a nested setup sorts the operations.
            # Thus, if instruction.pos is used, _sub_operations
            # adds epsilon to each sub-instruction, making them
            # come after the endloop.
            # Assume that 1 - SUB_INS_EPSILON (e.g. .99) will put
            # the endloop past everything.
            operations.append(Operation(instruction.pos + 1 - SUB_INS_EPSILON, "endloop"))
            return operations
        elif desc.args[0].name == "java/util/Optional" and \
                desc.args[1].name == "java/util/function/BiConsumer":
            # Write a boolean indicating whether the optional is present.
            # Call the consumer with the packetbuffer and value if the optional is present.
            operations = []
            field = args[0]
            assert isinstance(field, StackOperand)
            operations.append(Operation(instruction.pos, "write", type="boolean",
                                        field=field.value + ".isPresent()"))
            operations.append(Operation(instruction.pos, "if",
                                        condition=field.value + ".isPresent()"))
            info = args[1]
            assert isinstance(info, InvokeDynamicInfo)
            operations += _PIT._lambda_operations(
                classloader, classes, instruction, verbose,
                info, [instance, field.value + ".get()"]
            )
            # Jank: the part of the program that converts loop+endloop
            # to a nested setup sorts the operations.
            # Thus, if instruction.pos is used, _sub_operations
            # adds epsilon to each sub-instruction, making them
            # come after the endloop.
            # Assume that 1 - SUB_INS_EPSILON (e.g. .99) will put
            # the endloop past everything.
            operations.append(Operation(instruction.pos + 1 - SUB_INS_EPSILON, "endif"))
            return operations
        else: