
/*
 * Janino - An embedded Java[TM] compiler
 *
 * Copyright (c) 2001-2010 Arno Unkrig. All rights reserved.
 * Copyright (c) 2015-2016 TIBCO Software Inc. All rights reserved. // CHECKSTYLE:OFF CHECKSTYLE:ON
 *
 * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
 * following conditions are met:
 *
 *    1. Redistributions of source code must retain the above copyright notice, this list of conditions and the
 *       following disclaimer.
 *    2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
 *       following disclaimer in the documentation and/or other materials provided with the distribution.
 *    3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
 *       products derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package org.codehaus.janino;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.codehaus.commons.compiler.InternalCompilerException;
import org.codehaus.commons.nullanalysis.Nullable;
import org.codehaus.janino.util.ClassFile;
import org.codehaus.janino.util.ClassFile.ConstantClassInfo;
import org.codehaus.janino.util.ClassFile.StackMapTableAttribute;
import org.codehaus.janino.util.ClassFile.StackMapTableAttribute.AppendFrame;
import org.codehaus.janino.util.ClassFile.StackMapTableAttribute.ChopFrame;
import org.codehaus.janino.util.ClassFile.StackMapTableAttribute.FullFrame;
import org.codehaus.janino.util.ClassFile.StackMapTableAttribute.ObjectVariableInfo;
import org.codehaus.janino.util.ClassFile.StackMapTableAttribute.SameFrame;
import org.codehaus.janino.util.ClassFile.StackMapTableAttribute.SameFrameExtended;
import org.codehaus.janino.util.ClassFile.StackMapTableAttribute.SameLocals1StackItemFrame;
import org.codehaus.janino.util.ClassFile.StackMapTableAttribute.SameLocals1StackItemFrameExtended;
import org.codehaus.janino.util.ClassFile.StackMapTableAttribute.StackMapFrame;
import org.codehaus.janino.util.ClassFile.StackMapTableAttribute.UninitializedVariableInfo;
import org.codehaus.janino.util.ClassFile.StackMapTableAttribute.VerificationTypeInfo;

/**
 * The context of the compilation of a function (constructor or method). Manages generation of byte code, the exception
 * table, generation of line number tables, allocation of local variables, determining of stack size and local variable
 * table size and flow analysis.
 */
public
class CodeContext {

    private static final boolean SUPPRESS_STACK_MAP_TABLE = Boolean.getBoolean(CodeContext.class.getName() + ".suppressStackMapTable");

    private static final int INITIAL_SIZE = 128;

    private final ClassFile classFile;

    private int                             maxStack;
    private short                           maxLocals;
    private byte[]                          code;
    private final Offset                    beginning;
    private final Inserter                  end;
    private Inserter                        currentInserter;
    private final List<ExceptionTableEntry> exceptionTableEntries;

    /**
     * All the local variables that are allocated in any block in this {@link CodeContext}.
     */
    private final List<Java.LocalVariableSlot> allLocalVars = new ArrayList<>();

    /**
     * Each List of Java.LocalVariableSlot is the local variables allocated for a block. They are pushed and popped
     * onto the list together to make allocation of the next local variable slot easy.
     */
    @Nullable private LocalScope currentLocalScope;

    static
    class LocalScope {

        @Nullable final LocalScope         parent;
        final short                        startingLocalVariableSlot;
        final List<Java.LocalVariableSlot> localVars = new ArrayList<>();
        final StackMap                     startingStackMap;

        LocalScope(@Nullable LocalScope parent, short startingLocalSlot, StackMap startingStackMap) {
            this.parent                    = parent;
            this.startingLocalVariableSlot = startingLocalSlot;
            this.startingStackMap          = startingStackMap;
        }
    }

    private short                   nextLocalVariableSlot;
    private final List<Relocatable> relocatables = new ArrayList<>();

    /**
     * Creates an empty "Code" attribute.
     */
    public
    CodeContext(ClassFile classFile) {
        this.classFile = classFile;

        this.maxStack               = 0;
        this.maxLocals              = 0;
        this.code                   = new byte[CodeContext.INITIAL_SIZE];
        this.exceptionTableEntries  = new ArrayList<>();

        this.beginning              = new Offset();
        this.beginning.offset       = 0;

        this.currentInserter        = new Inserter();
        this.currentInserter.offset = 0;
        this.currentInserter.setStackMap(new StackMap(new VerificationTypeInfo[0], new VerificationTypeInfo[0]));

        this.beginning.next         = this.currentInserter;
        this.currentInserter.prev   = this.beginning;

        this.end                    = this.currentInserter;
    }

    /**
     * The {@link ClassFile} this context is related to.
     */
    public ClassFile
    getClassFile() { return this.classFile; }

    /**
     * Allocates space for a local variable of the given size (1 or 2) on the local variable array.
     * <p>
     *   As a side effect, the "max_locals" field of the "Code" attribute is updated.
     * </p>
     * <p>
     *   The only way to deallocate local variables is to {@link #saveLocalVariables()} and later {@link
     *   #restoreLocalVariables()}.
     * </p>
     *
     * @param size The number of slots to allocate (1 or 2)
     * @return The slot index of the allocated variable
     */
    public short
    allocateLocalVariable(short size) { return this.allocateLocalVariable(size, null, null).getSlotIndex(); }

    /**
     * Allocates space for a local variable of the given size (1 or 2) on the local variable array. As a side effect,
     * the "max_locals" field of the "Code" attribute is updated.
     * <p>
     *   The only way to deallocate local variables is to {@link #saveLocalVariables()} and later {@link
     *   #restoreLocalVariables()}.
     * </p>
     *
     * @param size Number of slots to use (1 or 2)
     * @param name The variable name; if {@code null}, then the variable won't be written to the LocalVariableTable
     * @param type The variable type; if the <var>name</var> is not null, then the type is needed to write to the
     *             LocalVariableTable
     */
    public Java.LocalVariableSlot
    allocateLocalVariable(short size, @Nullable String name, @Nullable IType type) {

        LocalScope currentScope = this.currentLocalScope;
        assert currentScope != null : "saveLocalVariables must be called first";

        final List<Java.LocalVariableSlot> currentVars = currentScope.localVars;

        Java.LocalVariableSlot slot = new Java.LocalVariableSlot(name, this.nextLocalVariableSlot, type);

        if (name != null) {
            slot.setStart(this.newOffset());
        }

//        // This check would be wrong, because one (Long|Double)_variable_info maps TWO local variable slots.
//        if (this.nextLocalVariableSlot != this.currentInserter.stackMap.locals().length) {
//            throw new InternalCompilerException(...);
//        }

        this.nextLocalVariableSlot += size;
        currentVars.add(slot);
        this.allLocalVars.add(slot);

        if (this.nextLocalVariableSlot > this.maxLocals) {
            this.maxLocals = this.nextLocalVariableSlot;
        }

        return slot;
    }

    /**
     * Remembers the current size of the local variables array.
     */
    public List<Java.LocalVariableSlot>
    saveLocalVariables() {

        // Push empty list on the local scope stack to hold a new block's local vars.
        return (this.currentLocalScope = new LocalScope(
            this.currentLocalScope,
            this.nextLocalVariableSlot,
            this.currentInserter.getStackMap()
        )).localVars;
    }

    /**
     * Restores the previous size of the local variables array. This MUST to be called for every call to
     * saveLocalVariables as it closes the variable extent for all the active local variables in the current block.
     */
    public void
    restoreLocalVariables() {

        // Pop the list containing the current block's local vars.
        LocalScope scopeToPop = this.currentLocalScope;
        assert scopeToPop != null;

        for (Java.LocalVariableSlot slot : scopeToPop.localVars) {
            if (slot.getName() != null) slot.setEnd(this.newOffset());
            this.allLocalVars.remove(slot);
        }

        this.currentLocalScope = scopeToPop.parent;

        // Reuse local variable slots of the popped scope.
        this.nextLocalVariableSlot = scopeToPop.startingLocalVariableSlot;

        // To truncate the stack map, remove local variables indicated by the popped scope.
        if (this.currentLocalScope != null) {
            StackMap sm = this.currentInserter.getStackMap();

            if (sm != null && sm.locals().length > 0) {
                int numActiveSlots = 0;
                int nextLvIndex = 0;
                for (VerificationTypeInfo slot : sm.locals()) {
                    if (nextLvIndex >= this.nextLocalVariableSlot) break;
                    nextLvIndex += slot.category();
                    numActiveSlots += 1;
                }
                int numRemovedSlots = sm.locals().length - numActiveSlots;
                while (numRemovedSlots-- > 0) sm = sm.popLocal();
                this.currentInserter.setStackMap(sm);
            }
        }
    }

    /**
     * @param initialLocalsCount The number of parameters, plus one iff there is a "this" parameter
     */
    public ClassFile.CodeAttribute
    newCodeAttribute(int initialLocalsCount, boolean debugLines, boolean debugVars) {

        // Transform the exception table from o.c.j.CodeContext to o.c.j.u.ClassFile.
        ClassFile.CodeAttribute.ExceptionTableEntry[]
        etes = new ClassFile.CodeAttribute.ExceptionTableEntry[this.exceptionTableEntries.size()];

        for (int i = 0; i < etes.length; i++) {
            ExceptionTableEntry ete = (ExceptionTableEntry) this.exceptionTableEntries.get(i);
            etes[i] = new ClassFile.CodeAttribute.ExceptionTableEntry(
                (short) ete.startPc.offset,   // startPc
                (short) ete.endPc.offset,     // endPc
                (short) ete.handlerPc.offset, // handlerPc
                ete.catchType                 // catchType
            );
        }

        List<ClassFile.AttributeInfo> attributes = new ArrayList<>();

        // Add "LineNumberTable" attribute.
        if (debugLines) {
            attributes.add(this.newLineNumberTableAttribute());
        }

        // Add "LocalVariableTable" attribute.
        if (debugVars) {
            ClassFile.LocalVariableTableAttribute
            lvta = this.newLocalVariableTableAttribute();

            if (lvta != null) attributes.add(lvta);
        }

        // Add the "StackMapTable" attribute.
        if (!CodeContext.SUPPRESS_STACK_MAP_TABLE) {
            StackMapTableAttribute smta = this.newStackMapTableAttribute(initialLocalsCount);
            if (smta != null) attributes.add(smta);
        }

        ClassFile.AttributeInfo[]
        aia = (ClassFile.AttributeInfo[]) attributes.toArray(new ClassFile.AttributeInfo[attributes.size()]);
        return new ClassFile.CodeAttribute(
            this.classFile.addConstantUtf8Info("Code"), // attributeNameIndex
            (short) this.maxStack,                      // maxStack
            this.maxLocals,                             // maxLocals
            Arrays.copyOf(this.code, this.end.offset),  // code
            etes,                                       // exceptionTableEntries
            aia                                         // attributes
        );
    }

    private ClassFile.LineNumberTableAttribute
    newLineNumberTableAttribute() {

        List<ClassFile.LineNumberTableAttribute.Entry> lnt = new ArrayList<>();
        for (Offset o = this.beginning; o != null; o = o.next) {
            if (o instanceof LineNumberOffset) {

                int offset = o.offset;
                if (offset > 0xffff) {
                    throw new InternalCompilerException("LineNumberTable entry offset out of range");
                }

                short lineNumber = ((LineNumberOffset) o).lineNumber;

                lnt.add(new ClassFile.LineNumberTableAttribute.Entry((short) offset, lineNumber));
            }
        }

        ClassFile.LineNumberTableAttribute.Entry[] lnte = (ClassFile.LineNumberTableAttribute.Entry[]) lnt.toArray(
            new ClassFile.LineNumberTableAttribute.Entry[lnt.size()]
        );
        return new ClassFile.LineNumberTableAttribute(
            this.classFile.addConstantUtf8Info("LineNumberTable"), // attributeNameIndex
            lnte                                                   // lineNumberTableEntries
        );
    }

    /**
     * @return {@code null} iff the stack map is empty
     */
    @Nullable private StackMapTableAttribute
    newStackMapTableAttribute(int initialLocalsCount) {

        Offset frame = this.beginning.next;
        Offset previousFrame = null;

        for (; frame != this.end && frame.stackMap.locals().length < initialLocalsCount; frame = frame.next);

        previousFrame = frame;
        frame = frame.next;

        List<StackMapFrame> smfs = new ArrayList<>();
        for (; frame != null && frame.offset != this.end.offset; frame = frame.next) {

            if (!(frame instanceof BasicBlock)) continue;

            // Sometimes, e.g. when the last statement in a FOREACH body is an IF statement, the first BasicBlock
            // sees "too many" local variables, and the second BB is right.
            for (Offset o = frame.next; o != null && o.offset == frame.offset; o = o.next) {
                if (o instanceof BasicBlock) frame = o;
            }

            // Some intermediate offsets (e.g. right before a branch target) have no stack map.
            if (frame.getStackMap() == null) continue;

            final int                    offsetDelta               = smfs.isEmpty() ? frame.offset : frame.offset - previousFrame.offset - 1;
            final VerificationTypeInfo[] frameOperands             = frame.getStackMap().operands();
            final int                    frameOperandsLength       = frameOperands.length;
            final VerificationTypeInfo[] frameLocals               = frame.getStackMap().locals();
            final int                    frameLocalsLength         = frameLocals.length;
            final VerificationTypeInfo[] previousFrameLocals       = previousFrame.getStackMap().locals();
            final int                    previousFrameLocalsLength = previousFrameLocals.length;
            int                          k = 99; // SMT: Workaround for a known SMT bug in Janino.

            // Encode the stack map entry delta as "frames", see JVMS11 4.7.4
            if (
                frameOperandsLength == 0
                && Arrays.equals(frameLocals, previousFrameLocals)
            ) {
                if (offsetDelta <= 63) {
                    smfs.add(new SameFrame(offsetDelta));           // same_frame
                } else {
                    smfs.add(new SameFrameExtended(offsetDelta));   // same_frame_extended
                }
            } else

            if (
                frameOperandsLength == 1
                && Arrays.equals(frameLocals, previousFrameLocals)
            ) {
                if (offsetDelta <= 63) {
                    smfs.add(new SameLocals1StackItemFrame(         // same_locals_1_stack_item_frame
                        offsetDelta,                                //   offset_delta
                        frameOperands[0]                            //   stack
                    ));
                } else {
                    smfs.add(new SameLocals1StackItemFrameExtended( // same_locals_1_stack_item_frame_extended
                        offsetDelta,                                //   offset_delta
                        frameOperands[0]                            //   stack
                    ));
                }
            } else

            if (
                frameOperandsLength == 0
                && (k = previousFrameLocalsLength - frameLocalsLength) >= 1
                && k <= 3
                && Arrays.equals(frameLocals, Arrays.copyOf(previousFrameLocals, frameLocalsLength))
            ) {
                smfs.add(new ChopFrame(offsetDelta, k));            // chop_frame
            } else

            if (
                frameOperandsLength == 0
                && (k = frameLocalsLength - previousFrameLocalsLength) >= 1
                && k <= 3
                && Arrays.equals(previousFrameLocals, Arrays.copyOf(frameLocals, previousFrameLocalsLength))
            ) {
                smfs.add(new AppendFrame(                           // append_frame
                    offsetDelta,                                    //   offset_delta
                    (VerificationTypeInfo[]) Arrays.copyOfRange(    //   locals
                        frameLocals,
                        previousFrameLocalsLength,
                        frameLocalsLength
                    )
                ));
            } else

            {
                smfs.add(new FullFrame(                             // full_frame
                    offsetDelta,                                    //   offset_delta
                    frameLocals,                                    //   locals
                    frameOperands                                   //   stack
                ));
            }

            previousFrame = frame;
        }

        if (smfs.isEmpty()) return null;

        return new StackMapTableAttribute(
            this.classFile.addConstantUtf8Info("StackMapTable"),           // attributeNameIndex
            (StackMapFrame[]) smfs.toArray(new StackMapFrame[smfs.size()]) // entries
        );
    }

    private static IClass
    rawTypeOf(IType iType) {
        while (iType instanceof IParameterizedType) iType = ((IParameterizedType) iType).getRawType();
        assert iType instanceof IClass;
        return (IClass) iType;
    }

    /**
     * @return A {@link org.codehaus.janino.util.ClassFile.LocalVariableTableAttribute} for this {@link CodeContext}
     */
    @Nullable protected ClassFile.LocalVariableTableAttribute
    newLocalVariableTableAttribute() {

        final List<ClassFile.LocalVariableTableAttribute.Entry>
        entryList = new ArrayList<>();

        for (Java.LocalVariableSlot slot : this.getAllLocalVars()) {

            String localVariableName = slot.getName();
            if (localVariableName != null) {

                String      typeName    = CodeContext.rawTypeOf(slot.getType()).getDescriptor();
                final short classSlot   = this.classFile.addConstantUtf8Info(typeName);
                final short varNameSlot = this.classFile.addConstantUtf8Info(localVariableName);

                Offset start = slot.getStart();
                Offset end2  = slot.getEnd();

                assert start != null;
                assert end2 != null;

                entryList.add(new ClassFile.LocalVariableTableAttribute.Entry(
                    (short) start.offset,
                    (short) (end2.offset - start.offset),
                    varNameSlot,
                    classSlot,
                    slot.getSlotIndex()
                ));
            }
        }

        if (entryList.isEmpty()) return null;

        return new ClassFile.LocalVariableTableAttribute(
            this.classFile.addConstantUtf8Info("LocalVariableTable"),
            (ClassFile.LocalVariableTableAttribute.Entry[]) entryList.toArray(
                new ClassFile.LocalVariableTableAttribute.Entry[entryList.size()]
            )
        );
    }

    /**
     * Fixes up all of the offsets and relocate() all relocatables.
     */
    public void
    fixUpAndRelocate() {
        this.maybeGrow();
        this.fixUp();
        this.relocate();
    }

    /**
     * Grow the code if relocatables are required to.
     */
    private void
    maybeGrow() {

        // "Relocatable.grow()" may add relocatables, so iterate with index instead of Iterator.
        for (int i = 0; i < this.relocatables.size(); i++) {
            ((Relocatable) this.relocatables.get(i)).grow();
        }
    }

    /**
     * Fixes up all offsets.
     */
    private void
    fixUp() {
        for (Offset o = this.beginning; o != this.end; o = o.next) {
            assert o != null;
            if (o instanceof FixUp) ((FixUp) o).fixUp();
        }
    }

    /**
     * Relocates all relocatables.
     */
    private void
    relocate() {
        for (Relocatable relocatable : this.relocatables) {
            relocatable.relocate();
        }
    }

    /**
     * Inserts a sequence of bytes at the current insertion position. Creates {@link LineNumberOffset}s as necessary.
     */
    public void
    write(byte[] b) {

        if (b.length == 0) return;

        // CAVEAT: "makeSpace()" may change "this.code", so make sure to call "makeSpace()" _before_ using "this.code"!
        int o = this.makeSpace(b.length);
        System.arraycopy(b, 0, this.code, o, b.length);
    }

    /**
     * Inserts a byte at the current insertion position. Creates {@link LineNumberOffset}s as necessary.
     * <p>
     *   This method is an optimization to avoid allocating small byte[] and ease GC load.
     * </p>
     */
    public void
    write(byte b1) {

        // CAVEAT: "makeSpace()" may change "this.code", so make sure to call "makeSpace()" _before_ using "this.code"!
        int o = this.makeSpace(1);
        this.code[o] = b1;
    }

    /**
     * Inserts bytes at the current insertion position. Creates {@link LineNumberOffset}s as necessary.
     * <p>
     *   This method is an optimization to avoid allocating small byte[] and ease GC load.
     * </p>
     */
    public void
    write(byte b1, byte b2) {

        int o = this.makeSpace(2);

        this.code[o++] = b1;
        this.code[o]   = b2;
    }

    /**
     * Inserts bytes at the current insertion position. Creates {@link LineNumberOffset}s as necessary.
     * <p>
     *   This method is an optimization to avoid allocating small byte[] and ease GC load.
     * </p>
     */
    public void
    write(byte b1, byte b2, byte b3) {

        int o = this.makeSpace(3);

        this.code[o++] = b1;
        this.code[o++] = b2;
        this.code[o]   = b3;
    }

    /**
     * Inserts bytes at the current insertion position. Creates {@link LineNumberOffset}s as necessary.
     * <p>
     *   This method is an optimization to avoid allocating small byte[] and ease GC load.
     * </p>
     */
    public void
    write(byte b1, byte b2, byte b3, byte b4) {

        int o = this.makeSpace(4);

        this.code[o++] = b1;
        this.code[o++] = b2;
        this.code[o++] = b3;
        this.code[o]   = b4;
    }

    /**
     * Inserts bytes at the current insertion position. Creates {@link LineNumberOffset}s as necessary.
     * <p>
     *   This method is an optimization to avoid allocating small byte[] and ease GC load.
     * </p>
     */
    public void
    write(byte b1, byte b2, byte b3, byte b4, byte b5) {

        int o = this.makeSpace(5);

        this.code[o++] = b1;
        this.code[o++] = b2;
        this.code[o++] = b3;
        this.code[o++] = b4;
        this.code[o]   = b5;
    }

    public void
    addLineNumberOffset(int lineNumber) {

        if (lineNumber == -1) return;

        if (lineNumber > 0xffff) lineNumber = 0xffff;

        // Find out whether the line number is different from the line number of the preceding insertion,
        // and, if so, insert a LineNumberOffset object, which will later lead to a LineNumberTable entry.
        for (Offset o = this.currentInserter.prev; o != this.beginning; o = o.prev) {
            assert o != null;
            if (o instanceof LineNumberOffset) {
                if ((((LineNumberOffset) o).lineNumber & 0xffff) == lineNumber) return;
                break;
            }
        }

        // Insert a LineNumberOffset _before_ the current inserter.
        LineNumberOffset lno = new LineNumberOffset(this.currentInserter.offset, this.currentInserter.getStackMap(), (short) lineNumber);

        Offset cip = this.currentInserter.prev;
        assert cip != null;

        lno.prev = cip;
        lno.next = this.currentInserter;

        cip.next = lno;

        this.currentInserter.prev = lno;
    }

    /**
     * Inserts <var>size</var> NUL bytes at the current inserter's offset, advances the current inserter's offset by
     * <var>size</var>, creates {@link LineNumberOffset}s as necessary, and returns the current inserter's original
     * offset (the offset of the first NUL byte that was inserted).
     * <p>
     *   Because the class file format does not support line numbers greater than 65535, these are treated as 65535.
     * </p>
     *
     * @param size The number of NUL bytes to inject
     * @return     The offset of the first inserted byte
     */
    public int
    makeSpace(final int size) {

        final int cio = this.currentInserter.offset;

        if (size == 0) {
            ;
        } else
        if (size < 0) {

            // Make "negative space", i.e. remove bytes after current position.
            assert cio <= this.end.offset + size; // Are there enough bytes to remove?
            System.arraycopy(this.code, cio - size, this.code, cio, this.end.offset - cio + size);
            for (Offset o = this.currentInserter.next; o != null; o = o.next) o.offset += size;
        } else
        if (this.end.offset + size <= this.code.length) {

            // Make space; no need to enlarge the byte array.
            if (cio != this.end.offset) { // Optimization to avoid a trivial method call in the common case
                System.arraycopy(this.code, cio, this.code, cio + size, this.end.offset - cio);
                Arrays.fill(this.code, cio, cio + size, (byte) 0);
            }
            for (Offset o = this.currentInserter; o != null; o = o.next) o.offset += size;
        } else {

            // Enlarge the byte array to make enough space.
            byte[] oldCode = this.code;
            //double size to avoid horrible performance, but don't grow over our limit
            int newSize = Math.max(Math.min(oldCode.length * 2, 0xffff), oldCode.length + size);
            if (newSize > 0xffff) throw new InternalCompilerException("Code grows beyond 64 KB");
            this.code = new byte[newSize];
            System.arraycopy(oldCode, 0, this.code, 0, cio);
            System.arraycopy(oldCode, cio, this.code, cio + size, this.end.offset - cio);
            Arrays.fill(this.code, cio, cio + size, (byte) 0);
            for (Offset o = this.currentInserter; o != null; o = o.next) o.offset += size;
        }

        return cio;
    }

    /**
     */
    public void
    writeShort(int v) { this.write((byte) (v >> 8), (byte) v); }

    /**
     * Generates a "branch" instruction.
     *
     * @param opcode One of {@link Opcode#GOTO}, {@link Opcode#JSR} and <code>Opcode.IF*</code>
     * @param dst    Where to branch
     */
    public void
    writeBranch(int opcode, final Offset dst) {

        assert dst instanceof CodeContext.BasicBlock;

        if (dst.offset == -1) {
            if (dst.stackMap == null) {
                dst.stackMap = this.currentInserter.getStackMap();
            }
        }

        @SuppressWarnings("deprecation") int opcodeJsr = Opcode.JSR;
        if (
            (opcode >= Opcode.IFEQ && opcode <= opcodeJsr)             // 6xIF??, 6xIF_ICMP??, 2xIF_ACMP??, GOTO, JSR
            || (opcode >= Opcode.IFNULL && opcode <= Opcode.IFNONNULL) // IFNULL, IFNONNULL
        ) {
            this.relocatables.add(new Branch(opcode, dst));
            this.write((byte) opcode, (byte) -1, (byte) -1);
        } else
        if (opcode >= Opcode.GOTO_W && opcode <= Opcode.JSR_W) {       // GOTO_W, JSR_W
            this.relocatables.add(new Branch(opcode, dst));
            this.write((byte) opcode, (byte) -1, (byte) -1, (byte) -1, (byte) -1);
        } else
        {
            throw new AssertionError(opcode);
        }
    }

    private
    class Branch extends Relocatable {

        Branch(int opcode, Offset destination) {
            this.opcode      = opcode;
            this.source      = CodeContext.this.newInserter();
            this.destination = destination;
        }

        @Override public void
        grow() {
            if (this.destination.offset == Offset.UNSET) {
                throw new InternalCompilerException("Cannot relocate branch to unset destination offset");
            }
            int offset = this.destination.offset - this.source.offset;

            @SuppressWarnings("deprecation") final int opcodeJsr = Opcode.JSR;

            if (this.opcode >= Opcode.GOTO && this.opcode <= opcodeJsr) { // GOTO, JSR
                if (offset > Short.MAX_VALUE || offset < Short.MIN_VALUE) {
                    CodeContext.this.pushInserter(this.source);
                    {
                        this.source = CodeContext.this.newInserter();

                        // Remove the original GOTO or JSR instruction.
                        CodeContext.this.makeSpace(-3);

                        // Insert a GOTO_W resp. JSR_instruction.
                        CodeContext.this.writeBranch((this.opcode = this.opcode + 33), this.destination);
                    }
                    CodeContext.this.popInserter();
                }
            } else
            if (
                (this.opcode >= Opcode.IFEQ && this.opcode <= Opcode.IF_ACMPNE)      // 6xIF??, 6xIF_ICMP??, 2xIF_ACMP??
                || (this.opcode >= Opcode.IFNULL && this.opcode <= Opcode.IFNONNULL) // IFNULL, IFNONNULL
            ) {
                if (offset > Short.MAX_VALUE || offset < Short.MIN_VALUE) {
                    CodeContext.this.pushInserter(this.source);
                    {
                        this.source = CodeContext.this.newInserter();

                        // Remove the original IF* instruction.
                        CodeContext.this.makeSpace(-3);

                        // Insert "IF-NEGATE-* skip; GOTO_W offset; skip:"
                        BasicBlock skip = CodeContext.this.new BasicBlock();
                        CodeContext.this.writeBranch((this.opcode = CodeContext.invertBranchOpcode(this.opcode)), skip);
                        if (this.opcode >= Opcode.IFEQ && this.opcode <= Opcode.IFLE) {
                            CodeContext.this.popIntOperand();
                        } else
                        if (this.opcode >= Opcode.IF_ICMPEQ && this.opcode <= Opcode.IF_ICMPLE) {
                            CodeContext.this.popIntOperand();
                            CodeContext.this.popIntOperand();
                        } else
                        if (this.opcode == Opcode.IF_ACMPEQ || this.opcode == Opcode.IF_ACMPNE) {
                            CodeContext.this.popReferenceOperand();
                            CodeContext.this.popReferenceOperand();
                        } else
                        if (this.opcode == Opcode.IFNULL || this.opcode == Opcode.IFNONNULL) {
                            CodeContext.this.popReferenceOperand();
                        } else
                        {
                            throw new AssertionError(this.opcode);
                        }
                        CodeContext.this.writeBranch(Opcode.GOTO_W, this.destination);
                        skip.setStackMap(CodeContext.this.currentInserter.getStackMap());
                        skip.set();
                    }
                    CodeContext.this.popInserter();
                }
            } else
            if (this.opcode >= Opcode.GOTO_W && this.opcode <= Opcode.JSR_W) { // GOTO_W, JSR_W
                ;
            } else {
                throw new AssertionError(this.opcode);
            }
        }

        @Override public void
        relocate() {
            if (this.destination.offset == Offset.UNSET) {
                throw new InternalCompilerException("Cannot relocate branch to unset destination offset");
            }

            @SuppressWarnings("deprecation") final int opcodeJsr = Opcode.JSR;

            if (
                (this.opcode >= Opcode.IFEQ && this.opcode <= opcodeJsr)             // 6xIF??, 6xIF_ICMP??, 2xIF_ACMP??, GOTO, JSR
                || (this.opcode >= Opcode.IFNULL && this.opcode <= Opcode.IFNONNULL) // IFNULL, IFNONNULL
            ) {
                int offset = this.destination.offset - this.source.offset;
                CodeContext.this.code[this.source.offset + 1] = (byte) (offset >> 8);
                CodeContext.this.code[this.source.offset + 2] = (byte) offset;
            } else
            if (this.opcode >= Opcode.GOTO_W && this.opcode <= Opcode.JSR_W) {       // GOTO_W, JSR_W
                int offset = this.destination.offset - this.source.offset;
                CodeContext.this.code[this.source.offset + 1] = (byte) (offset >> 24);
                CodeContext.this.code[this.source.offset + 2] = (byte) (offset >> 16);
                CodeContext.this.code[this.source.offset + 3] = (byte) (offset >> 8);
                CodeContext.this.code[this.source.offset + 4] = (byte) offset;
            } else
            {
                throw new AssertionError(this.opcode);
            }
        }

        private int          opcode;
        private Inserter     source;
        private final Offset destination;
    }

    /**
     * E.g. {@link Opcode#IFLT} ("less than") inverts to {@link Opcode#IFGE} ("greater than or equal to").
     */
    private static int
    invertBranchOpcode(int branchOpcode) {
        final Integer result = (Integer) CodeContext.BRANCH_OPCODE_INVERSION.get(Integer.valueOf(branchOpcode));
        assert result != null : branchOpcode;
        return result;
    }

    private static final Map<Integer /*branch-opcode*/, Integer /*inverted-branch-opcode*/>
    BRANCH_OPCODE_INVERSION = CodeContext.createBranchOpcodeInversion();
    private static Map<Integer, Integer>
    createBranchOpcodeInversion() {
        Map<Integer, Integer> m = new HashMap<>();
        m.put(Opcode.IF_ACMPEQ, Opcode.IF_ACMPNE);
        m.put(Opcode.IF_ACMPNE, Opcode.IF_ACMPEQ);
        m.put(Opcode.IF_ICMPEQ, Opcode.IF_ICMPNE);
        m.put(Opcode.IF_ICMPNE, Opcode.IF_ICMPEQ);
        m.put(Opcode.IF_ICMPGE, Opcode.IF_ICMPLT);
        m.put(Opcode.IF_ICMPLT, Opcode.IF_ICMPGE);
        m.put(Opcode.IF_ICMPGT, Opcode.IF_ICMPLE);
        m.put(Opcode.IF_ICMPLE, Opcode.IF_ICMPGT);
        m.put(Opcode.IFEQ,      Opcode.IFNE);
        m.put(Opcode.IFNE,      Opcode.IFEQ);
        m.put(Opcode.IFGE,      Opcode.IFLT);
        m.put(Opcode.IFLT,      Opcode.IFGE);
        m.put(Opcode.IFGT,      Opcode.IFLE);
        m.put(Opcode.IFLE,      Opcode.IFGT);
        m.put(Opcode.IFNULL,    Opcode.IFNONNULL);
        m.put(Opcode.IFNONNULL, Opcode.IFNULL);
        return Collections.unmodifiableMap(m);
    }

    /**
     * Writes a four-byte offset (as it is used in TABLESWITCH and LOOKUPSWITCH) into this code context.
     */
    public void
    writeOffset(Offset src, final Offset dst) {
        Offset o = new FourByteOffset();
        o.set();

        this.relocatables.add(new OffsetBranch(o, src, dst));
        this.makeSpace(4);
    }
    private final class FourByteOffset extends Offset {}

    private
    class OffsetBranch extends Relocatable {

        OffsetBranch(Offset where, Offset source, Offset destination) {
            this.where       = where;
            this.source      = source;
            this.destination = destination;
        }

        @Override public void
        grow() {}

        @Override public void
        relocate() {
            if (this.source.offset == Offset.UNSET || this.destination.offset == Offset.UNSET) {
                throw new InternalCompilerException("Cannot relocate offset branch to unset destination offset");
            }
            int    offset = this.destination.offset - this.source.offset;
            byte[] ba     = new byte[] {
                (byte) (offset >> 24),
                (byte) (offset >> 16),
                (byte) (offset >> 8),
                (byte) offset
            };
            System.arraycopy(ba, 0, CodeContext.this.code, this.where.offset, 4);
        }
        private final Offset where, source, destination;
    }

    /**
     * Creates and inserts an {@link CodeContext.Offset} at the current inserter's current position.
     */
    public Offset
    newOffset() {
        Offset o = new Offset();
        o.set();
        return o;
    }

    public Offset
    newBasicBlock() {
        Offset o = new BasicBlock();
        o.set();
        return o;
    }

    /**
     * Allocates an {@link Inserter}, set it to the current offset, and inserts it before the current offset.
     * <p>
     *   In clear text, this means that you can continue writing to the "Code" attribute, then {@link
     *   #pushInserter(CodeContext.Inserter)} the {@link Inserter}, then write again (which inserts bytes into the
     *   "Code" attribute at the previously remembered position), and then {@link #popInserter()}.
     * </p>
     */
    public Inserter
    newInserter() { Inserter i = new Inserter(); i.set(); return i; }

    /**
     * @return The current inserter
     */
    public Inserter
    currentInserter() { return this.currentInserter; }

    /**
     * Remembers the current {@link Inserter}, then replaces it with the new one.
     */
    public void
    pushInserter(Inserter ins) {

        // The current inserter MUST have a stack map; verify.
        ins.getStackMap();

        if (ins.nextInserter != null) {
            throw new InternalCompilerException("An Inserter can only be pushed once at a time");
        }
        ins.nextInserter     = this.currentInserter;
        this.currentInserter = ins;
    }

    /**
     * Replaces the current {@link Inserter} with the remembered one (see {@link #pushInserter(CodeContext.Inserter)}).
     */
    public void
    popInserter() {
        Inserter ni = this.currentInserter.nextInserter;
        if (ni == null) throw new InternalCompilerException("Code inserter stack underflow");
        ni.getStackMap();

        this.currentInserter.nextInserter = null; // Mark it as "unpushed".
        this.currentInserter              = ni;
    }

    /**
     * A class that represents an offset within a "Code" attribute.
     * <p>
     *   The concept of an "offset" is that if one writes into the middle of a "Code" attribute, all offsets behind the
     *   insertion point are automatically shifted.
     * </p>
     */
    public
    class Offset {

        /**
         * The offset in the code attribute that this object represents.
         */
        int offset = Offset.UNSET;

        /**
         * Links to preceding and succeeding offsets. Both are {@code null} <em>before</em> {@link #set()} is called,
         * and both are non-{@code null} <em>after</em> {@link #set()} has been called. This implies that {@link
         * #set()} must be invoked at most <em>once</em>.
         */
        @Nullable Offset prev, next;

        /**
         * Special value for {@link #offset} which indicates that this {@link Offset} has not yet been {@link #set()}
         */
        static final int UNSET = -1;

        @Nullable private StackMap stackMap;     // Is null until "set()".

        /**
         * Sets this "Offset" to the offset of the current inserter; inserts this "Offset" before the current inserter.
         */
        public void
        set() {

            this.setOffset();

            this.setStackMap();

            Inserter ci = CodeContext.this.currentInserter;

            Offset cip = ci.prev;
            assert cip != null;

            this.prev = cip;
            this.next = ci;

            cip.next = this;
            ci.prev  = this;
        }

        /**
         * Set this offset, and mark it as the the beginning of a "basic block".
         */
        public void
        setBasicBlock() {
            this.set();
            assert CodeContext.this.currentInserter.getStackMap() != null;
            new BasicBlock().set();
        }

        /**
         * Merges the stack maps of the current inserter and THIS offset, and assigns the result to the current
         * inserter and THIS offset.
         */
        void
        setStackMap() {

            Inserter ci = CodeContext.this.currentInserter;

            ((Offset) ci).stackMap = this.stackMap = CodeContext.mergeStackMaps(((Offset) ci).stackMap, this.stackMap);
        }

        public void
        setOffset() {
            Inserter ci = CodeContext.this.currentInserter;

            if (this.offset != Offset.UNSET) throw new InternalCompilerException("Offset already set");
            this.offset = ci.offset;
        }

        public StackMap
        getStackMap() { return this.stackMap; }

        public void
        setStackMap(StackMap stackMap) {
            this.stackMap = stackMap;
        }

        /**
         * @return The {@link CodeContext} that this {@link Offset} belongs to
         */
        public final CodeContext getCodeContext() { return CodeContext.this; }

        @Override public String
        toString() { return CodeContext.this.classFile.getThisClassName() + ": " + this.offset; }
    }

    @Nullable private static final StackMap
    mergeStackMaps(@Nullable StackMap sm1, @Nullable StackMap sm2) {

        if (sm1 == null) return sm2;

        if (sm2 == null) return sm1;

        if (sm1 == sm2) return sm1;

        if (sm1.equals(sm2)) return sm1;

        if (!Arrays.equals(sm1.operands(), sm2.operands())) {
            throw new InternalCompilerException("Inconsistent operand stack: " + sm1 + " vs. " + sm2);
        }

        VerificationTypeInfo[]     locals1 = sm1.locals();
        VerificationTypeInfo[]     locals2 = sm2.locals();
        List<VerificationTypeInfo> tmp     = new ArrayList<>();

        for (int i1 = 0, i2 = 0; i1 < locals1.length && i2 < locals2.length;) {

            VerificationTypeInfo local1 = locals1[i1++];
            VerificationTypeInfo local2 = locals2[i2++];

            if (local1.equals(local2)) {
                tmp.add(local1);
            } else {
                if (local1 == StackMapTableAttribute.TOP_VARIABLE_INFO && local2.category() == 2) {

                    // Issue #178:
                    // top,top / double => top,top
                    assert i1 < locals1.length;
                    assert locals1[i1] == StackMapTableAttribute.TOP_VARIABLE_INFO;
                    i1++;
                    tmp.add(StackMapTableAttribute.TOP_VARIABLE_INFO);
                } else
                if (local2 == StackMapTableAttribute.TOP_VARIABLE_INFO && local1.category() == 2) {

                    // Issue #178:
                    // double / top,top => top,top
                    assert i2 < locals2.length;
                    assert locals2[i2] == StackMapTableAttribute.TOP_VARIABLE_INFO;
                    i2++;
                    tmp.add(StackMapTableAttribute.TOP_VARIABLE_INFO);
                }

                tmp.add(StackMapTableAttribute.TOP_VARIABLE_INFO);
            }
        }

        return new StackMap((VerificationTypeInfo[]) tmp.toArray(new VerificationTypeInfo[tmp.size()]), sm1.operands());
    }

    /**
     * Adds another entry to the "exception_table" of this code attribute (see JVMS 4.7.3).
     *
     * @param catchTypeFd {@code null} means {@code finally} clause
     */
    public void
    addExceptionTableEntry(Offset startPc, Offset endPc, Offset handlerPc, @Nullable String catchTypeFd) {
        this.exceptionTableEntries.add(new ExceptionTableEntry(
            startPc,
            endPc,
            handlerPc,
            catchTypeFd == null ? (short) 0 : this.classFile.addConstantClassInfo(catchTypeFd)
        ));
    }

    /**
     * Representation of an entry in the "exception_table" of a "Code" attribute (see JVMS 4.7.3).
     */
    private static
    class ExceptionTableEntry {
        ExceptionTableEntry(Offset startPc, Offset endPc, Offset handlerPc, short  catchType) {
            this.startPc   = startPc;
            this.endPc     = endPc;
            this.handlerPc = handlerPc;
            this.catchType = catchType;
        }
        final Offset startPc, endPc, handlerPc;
        final short  catchType; // 0 == "finally" clause
    }

    /**
     * A class that implements an insertion point into a "Code" attribute.
     */
    public
    class Inserter extends Offset {
        @Nullable private Inserter nextInserter; // null == not in "currentInserter" stack
    }

    /**
     * An {@link Offset} who's sole purpose is to later create a 'LineNumberTable' attribute.
     */
    public
    class LineNumberOffset extends Offset {

        private final short lineNumber;

        /**
         * @param lineNumber 1...65535
         */
        public
        LineNumberOffset(int offset, StackMap stackMap, short lineNumber) {
            this.lineNumber = lineNumber;
            this.offset     = offset;
            this.setStackMap(stackMap);
        }
    }

    /**
     * This {@link Offset} marks the first byte of a "basic block" in the sense of JLS 17 "4.10.1 Verification by Type
     * Checking":
     *
     *      The intent is that a stack map frame must appear at the beginning of each basic block in a method.
     */
    public final
    class BasicBlock extends Offset {}

    private abstract
    class Relocatable {

        /**
         * Grows the code if the relocation cannot be done without growing code.
         */
        public abstract void grow();

        /**
         * Relocates this object.
         */
        public abstract void relocate();
    }

    /**
     * A throw-in interface that marks {@link CodeContext.Offset}s as "fix-ups": During the execution of {@link
     * CodeContext#fixUp}, all "fix-ups" are invoked and can do last touches to the code attribute.
     * <p>
     *   This is currently used for inserting the "padding bytes" into the TABLESWITCH and LOOKUPSWITCH instructions.
     * </p>
     */
    public
    interface FixUp {

        /**
         * @see FixUp
         */
        void fixUp();
    }

    /**
     * @return All the local variables that are allocated in any block in this {@link CodeContext}
     */
    public List<Java.LocalVariableSlot>
    getAllLocalVars() { return this.allLocalVars; }

    /**
     * Removes all code between <var>from</var> and <var>to</var>. Also removes any {@link CodeContext.Relocatable}s
     * existing in that range.
     */
    public void
    removeCode(Offset from, Offset to) {

        if (from == to) return;

        int size = to.offset - from.offset;
        assert size >= 0;

        if (size == 0) return; // Short circuit.

        // Shift down the bytecode past "to".
        System.arraycopy(this.code, to.offset, this.code, from.offset, this.end.offset - to.offset);

        // Invalidate all offsets between "from" and "to".
        // Remove all relocatables that originate between "from" and "to".
        Set<Offset> invalidOffsets = new HashSet<>();
        {
            Offset o = from.next;
            assert o != null;

            for (; o != to;) {
                assert o != null;

                invalidOffsets.add(o);

                // Invalidate the offset for fast failure.
                final Offset n = o.next;
                o.offset    = -77;
                o.prev      = null;
                o.next      = null;

                o = n;
                assert o != null;
            }

            for (;;) {
                o.offset -= size;
                if (o == this.end) break;
                o = o.next;
                assert o != null;
            }
        }

        // Invalidate all relocatables which originate or target a removed offset.
        for (Iterator<Relocatable> it = this.relocatables.iterator(); it.hasNext();) {
            Relocatable r = (Relocatable) it.next();

            if (r instanceof Branch) {
                Branch b = (Branch) r;

                if (invalidOffsets.contains(b.source)) {
                    it.remove();
                } else {
                    assert !invalidOffsets.contains(b.destination);
                }
            }

            if (r instanceof OffsetBranch) {
                OffsetBranch ob = (OffsetBranch) r;

                if (invalidOffsets.contains(ob.source)) {
                    it.remove();
                } else {
                    assert !invalidOffsets.contains(ob.destination);
                }
            }
        }

        for (Iterator<ExceptionTableEntry> it = this.exceptionTableEntries.iterator(); it.hasNext();) {
            ExceptionTableEntry ete = (ExceptionTableEntry) it.next();

            // Start, end and handler must either ALL lie IN the range to remove or ALL lie outside.

            if (invalidOffsets.contains(ete.startPc)) {
                assert invalidOffsets.contains(ete.endPc);
                assert invalidOffsets.contains(ete.handlerPc);
                it.remove();
            } else {
                assert !invalidOffsets.contains(ete.endPc);
                assert !invalidOffsets.contains(ete.handlerPc);
            }
        }

        // Remove local variables in dead-code block.
        for (Iterator<Java.LocalVariableSlot> it = this.allLocalVars.iterator(); it.hasNext();) {
            final Java.LocalVariableSlot var = (Java.LocalVariableSlot) it.next();
            if (invalidOffsets.contains(var.getStart())) {
                assert invalidOffsets.contains(var.getEnd());
                it.remove();
            } else {
                assert !invalidOffsets.contains(var.getEnd());
            }
        }

        from.next = to;
        to.prev   = from;
    }

    @Override public String
    toString() { return this.classFile.getThisClassName() + "/cio=" + this.currentInserter.offset; }

    // Convenience methods for "pushOperand(VTI)".

    /**
     * Pushes one {@code object_variable_info}, {@code integer_variable_info}, {@code double_variable_info}, {@code
     * float_variable_info} or {@code long_variable_info} entry onto the current inserter's operand stack.
     */
    public void
    pushOperand(String fieldDescriptor) {

        if (Descriptor.isReference(fieldDescriptor)) {
            this.pushObjectOperand(fieldDescriptor);
        } else
        if (
            fieldDescriptor.equals(Descriptor.BYTE)
            || fieldDescriptor.equals(Descriptor.CHAR)
            || fieldDescriptor.equals(Descriptor.INT)
            || fieldDescriptor.equals(Descriptor.SHORT)
            || fieldDescriptor.equals(Descriptor.BOOLEAN)
        ) {
            this.pushIntOperand();
        } else
        if (fieldDescriptor.equals(Descriptor.DOUBLE)) {
            this.pushDoubleOperand();
        } else
        if (fieldDescriptor.equals(Descriptor.FLOAT)) {
            this.pushFloatOperand();
        } else
        if (fieldDescriptor.equals(Descriptor.LONG)) {
            this.pushLongOperand();
        } else
        {
            throw new AssertionError(fieldDescriptor);
        }
    }

    public void pushTopOperand()               { this.pushOperand(StackMapTableAttribute.TOP_VARIABLE_INFO);                }
    public void pushIntOperand()               { this.pushOperand(StackMapTableAttribute.INTEGER_VARIABLE_INFO);            }
    public void pushLongOperand()              { this.pushOperand(StackMapTableAttribute.LONG_VARIABLE_INFO);               }
    public void pushFloatOperand()             { this.pushOperand(StackMapTableAttribute.FLOAT_VARIABLE_INFO);              }
    public void pushDoubleOperand()            { this.pushOperand(StackMapTableAttribute.DOUBLE_VARIABLE_INFO);             }
    public void pushNullOperand()              { this.pushOperand(StackMapTableAttribute.NULL_VARIABLE_INFO);               }
    public void pushUninitializedThisOperand() { this.pushOperand(StackMapTableAttribute.UNINITIALIZED_THIS_VARIABLE_INFO); }

    public void
    pushUninitializedOperand() {

        final Offset                    o   = this.newOffset();
        final UninitializedVariableInfo uvi = this.classFile.newUninitializedVariableInfo((short) o.offset);

        this.relocatables.add(new Relocatable() {

            @Override public void
            grow() {}

            @Override public void
            relocate() { uvi.offset = (short) o.offset; }
        });

        this.pushOperand(uvi);
    }

    public void
    pushObjectOperand(String fieldDescriptor) {
        this.pushOperand(this.classFile.newObjectVariableInfo(fieldDescriptor));
    }

    public void
    pushOperand(VerificationTypeInfo topOperand) {
        final Inserter ci = this.currentInserter();

        StackMap sm = ci.getStackMap();
        assert sm != null;
        sm = sm.pushOperand(topOperand);
        ci.setStackMap(sm);

        int ss = 0;
        for (VerificationTypeInfo vti : sm.operands()) ss += vti.category();
        if (ss > this.maxStack) this.maxStack = ss;
    }

    /**
     * @return Whether the top operand is a {@code null_variable_info}
     */
    public boolean
    peekNullOperand() {
        return this.peekOperand() == StackMapTableAttribute.NULL_VARIABLE_INFO;
    }

    /**
     * @return Whether the top operand is a {@code object_variable_info}
     */
    public boolean
    peekObjectOperand() {
        return this.peekOperand() instanceof StackMapTableAttribute.ObjectVariableInfo;
    }

    /**
     * @return The verification type of the top operand
     */
    public VerificationTypeInfo
    peekOperand() { return this.currentInserter().getStackMap().peekOperand(); }

    /**
     * Pops one entry from the current inserter's operand stack.
     */
    public VerificationTypeInfo
    popOperand() {

        Inserter ci = this.currentInserter();
        StackMap sm = ci.getStackMap();

        for (;;) {
            VerificationTypeInfo result = sm.peekOperand();

            sm = sm.popOperand();

            if (result != StackMapTableAttribute.TOP_VARIABLE_INFO) {
                ci.setStackMap(sm);
                return result;
            }
        }
    }

    /**
     * Pops the top entry from the operand stack and assert that it equals <var>expected</var>.
     */
    public void
    popOperand(VerificationTypeInfo expected) {
        VerificationTypeInfo actual = this.popOperand();
        assert actual.equals(expected) : actual;
    }

    /**
     * Pops the top operand, asserts that it is an {@code integer_variable_info}, {@code long_variable_info}, {@code
     * float_variable_info}, {@code double_variable_info} or {@code variable_object_info}, and asserts that it matches
     * the given field descriptor.
     */
    public void
    popOperand(String expectedFd) {

        VerificationTypeInfo vti = this.popOperand();

        if (vti == StackMapTableAttribute.INTEGER_VARIABLE_INFO) {
            assert (
                expectedFd.equals(Descriptor.BOOLEAN)
                || expectedFd.equals(Descriptor.BYTE)
                || expectedFd.equals(Descriptor.CHAR)
                || expectedFd.equals(Descriptor.SHORT)
                || expectedFd.equals(Descriptor.INT)
            ) : expectedFd;
        } else
        if (vti == StackMapTableAttribute.LONG_VARIABLE_INFO) {
            assert expectedFd.equals(Descriptor.LONG) : expectedFd;
        } else
        if (vti == StackMapTableAttribute.FLOAT_VARIABLE_INFO) {
            assert expectedFd.equals(Descriptor.FLOAT) : expectedFd;
        } else
        if (vti == StackMapTableAttribute.DOUBLE_VARIABLE_INFO) {
            assert expectedFd.equals(Descriptor.DOUBLE) : expectedFd;
        } else
        if (vti == StackMapTableAttribute.NULL_VARIABLE_INFO) {
            assert expectedFd.equals(Descriptor.VOID) || Descriptor.isReference(expectedFd) : expectedFd;
        } else
        if (vti instanceof StackMapTableAttribute.ObjectVariableInfo) {
            assert Descriptor.isReference(expectedFd) : expectedFd + " vs. " + vti;

            final ObjectVariableInfo ovi = (StackMapTableAttribute.ObjectVariableInfo) vti;
            final ConstantClassInfo  cci = this.classFile.getConstantClassInfo(ovi.getConstantClassInfoIndex());

            final String computationalTypeFd = Descriptor.fromInternalForm(cci.getName(this.classFile));
            assert expectedFd.equals(computationalTypeFd) : expectedFd + " vs. " + computationalTypeFd;
        } else
        if (vti instanceof StackMapTableAttribute.UninitializedVariableInfo) {
            assert Descriptor.isReference(expectedFd) : expectedFd;
        } else
        {
            throw new AssertionError(vti);
        }
    }

    public void
    popOperandAssignableTo(String declaredFd) {

        if (Descriptor.isPrimitive(declaredFd)) {
            this.popOperand(declaredFd);
        } else {
            this.popObjectOrUninitializedOrUninitializedThisOperand();
        }
    }

    /**
     * Asserts that the top operand is an {@code integer_variable_info} and pops it.
     */
    public void
    popIntOperand() { this.popOperand(StackMapTableAttribute.INTEGER_VARIABLE_INFO); }

    /**
     * Asserts that the top operand is a {@code long_variable_info} and pops it.
     */
    public void
    popLongOperand() { this.popOperand(StackMapTableAttribute.LONG_VARIABLE_INFO); }

    /**
     * Asserts that the top operand is an {@code uninitializedThis_variable_info} and pops it.
     */
    public void
    popUninitializedThisOperand() { this.popOperand(StackMapTableAttribute.UNINITIALIZED_THIS_VARIABLE_INFO); }

    /**
     * Asserts that the top operand is an {@code uninitialized_variable_info} and pops it.
     */
    public void
    popUninitializedVariableOperand() {
        final VerificationTypeInfo op = this.popOperand();
        assert op instanceof StackMapTableAttribute.UninitializedVariableInfo : String.valueOf(op);
    }

    /**
     * Asserts that the top operand is an {@code object_variable_info} or a {@code null_variable_info} and pops it.
     */
    public void
    popReferenceOperand() {
        assert this.peekObjectOperand() || this.peekNullOperand() : this.peekOperand();
        this.popOperand();
    }

    public void
    popNullOperand() {
        VerificationTypeInfo vti = this.popOperand();
        assert vti == StackMapTableAttribute.NULL_VARIABLE_INFO;
    }

    /**
     * Asserts that the top operand is an {@code object_variable_info}, and pops it.
     *
     * @return The field descriptor of the popped object operand
     */
    public String
    popObjectOperand() {

        VerificationTypeInfo vti = this.popOperand();
        assert vti instanceof StackMapTableAttribute.ObjectVariableInfo : vti;
        final ObjectVariableInfo ovi = (StackMapTableAttribute.ObjectVariableInfo) vti;

        short             ccii = ovi.getConstantClassInfoIndex();
        ConstantClassInfo cci  = this.classFile.getConstantClassInfo(ccii);
        return Descriptor.fromInternalForm(cci.getName(this.classFile));
    }

    /**
     * Asserts that the top operand is an {@code object_variable_info}, {@code uninitialized_variable_info} or
     * {@code uninitializedThis_variable_info}, and pops it.
     */
    public VerificationTypeInfo
    popObjectOrUninitializedOrUninitializedThisOperand() {
        VerificationTypeInfo result = this.popOperand();
        assert (
            result instanceof StackMapTableAttribute.UninitializedVariableInfo
            || result instanceof StackMapTableAttribute.ObjectVariableInfo
            || result == StackMapTableAttribute.UNINITIALIZED_THIS_VARIABLE_INFO
            || result == StackMapTableAttribute.NULL_VARIABLE_INFO
        ) : result;
        return result;
    }

    /**
     * Asserts that the top operand is an {@code int_variable_info} or {@code long_variable_info}, then pops and
     * returns it.
     */
    public VerificationTypeInfo
    popIntOrLongOperand() {
        VerificationTypeInfo result = this.popOperand();
        assert (
            result == StackMapTableAttribute.INTEGER_VARIABLE_INFO
            || result == StackMapTableAttribute.LONG_VARIABLE_INFO
        ) : result;
        return result;
    }
}
