
/*
 * Janino - An embedded Java[TM] compiler
 *
 * Copyright (c) 2001-2010, Arno Unkrig
 * All rights reserved.
 *
 * 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. The name of the author may not be used to endorse or promote products derived from this software without
 *       specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``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 AUTHOR 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.io.DataOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.codehaus.janino.util.ClassFile;

/**
 * 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.
 */
@SuppressWarnings({ "rawtypes", "unchecked" }) public
class CodeContext {
    private static final boolean DEBUG = false;

    private static final int     INITIAL_SIZE   = 128;
    private static final byte    UNEXAMINED     = -1;
    private static final byte    INVALID_OFFSET = -2;
    private static final int     MAX_STACK_SIZE = 254;

    private final ClassFile                     classFile;
    private short                               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();

    /**
     * List of List of Java.LocalVariableSlot objects. Each List of Java.LocalVariableSlot is
     * the local variables allocated for a block. They are pushed and poped onto the list together
     * to make allocation of the next local variable slot easy.
     */
    private final List scopedVars = new ArrayList();

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

    /**
     * Create 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.beginning             = new Offset();
        this.end                   = new Inserter();
        this.currentInserter       = this.end;
        this.exceptionTableEntries = new ArrayList();

        this.beginning.offset = 0;
        this.end.offset       = 0;
        this.beginning.next   = this.end;
        this.end.prev         = this.beginning;
    }

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


    /**
     * Allocate 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.
     *
     * The only way to deallocate local variables is to
     * {@link #saveLocalVariables()} and later {@link
     * #restoreLocalVariables()}.
     *
     * @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(); }

    /**
     * Allocate 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.
     *
     * The only way to deallocate local variables is to
     * {@link #saveLocalVariables()} and later {@link
     * #restoreLocalVariables()}.
     * @param size Number of slots to use (1 or 2)
     * @param name The variable name, if it's null, the variable won't be written to the localvariabletable
     * @param type The variable type. if the name isn't null, the type is needed to write to the localvariabletable
     */
    public Java.LocalVariableSlot
    allocateLocalVariable(short size, String name, IClass type) {
        List currentVars = null;

        if (this.scopedVars.size() == 0) {
            throw new Error("saveLocalVariables must be called first");
        } else {
            currentVars = (List) this.scopedVars.get(this.scopedVars.size() - 1);
        }

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

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

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

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

        return slot;
    }

    /**
     * Remember the current size of the local variables array.
     */
    public List
    saveLocalVariables() {

        // Push empty list on the stack to hold a new block's local vars.
        List l = new ArrayList();
        this.scopedVars.add(l);

        return l;
    }

    /**
     * Restore 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.
        Iterator iter = ((List) this.scopedVars.remove(this.scopedVars.size() - 1)).iterator();

        while (iter.hasNext()) {
            Java.LocalVariableSlot slot = (Java.LocalVariableSlot) iter.next();

            if (slot.getName() != null) {
                slot.setEnd(newOffset());
            }
        }
    }

    /**
     *
     * @param dos
     * @param lineNumberTableAttributeNameIndex 0 == don't generate a "LineNumberTable" attribute
     * @throws IOException
     */
    protected void
    storeCodeAttributeBody(
        DataOutputStream dos,
        short            lineNumberTableAttributeNameIndex,
        short            localVariableTableAttributeNameIndex
    ) throws IOException {
        dos.writeShort(this.maxStack);                                // max_stack
        dos.writeShort(this.maxLocals);                               // max_locals
        dos.writeInt(this.end.offset);                                // code_length
        dos.write(this.code, 0, this.end.offset);                     // code
        dos.writeShort(this.exceptionTableEntries.size());            // exception_table_length
        for (int i = 0; i < this.exceptionTableEntries.size(); ++i) { // exception_table
            ExceptionTableEntry exceptionTableEntry = (ExceptionTableEntry) this.exceptionTableEntries.get(i);
            dos.writeShort(exceptionTableEntry.startPC.offset);
            dos.writeShort(exceptionTableEntry.endPC.offset);
            dos.writeShort(exceptionTableEntry.handlerPC.offset);
            dos.writeShort(exceptionTableEntry.catchType);
        }

        List attributes = new ArrayList(); // ClassFile.AttributeInfo

        // Add "LineNumberTable" attribute.
        if (lineNumberTableAttributeNameIndex != 0) {
            List lnt = new ArrayList();
            for (Offset o = this.beginning; o != null; o = o.next) {
                if (o instanceof LineNumberOffset) {
                    lnt.add(new ClassFile.LineNumberTableAttribute.Entry(o.offset, ((LineNumberOffset) o).lineNumber));
                }
            }
            ClassFile.LineNumberTableAttribute.Entry[] lnte = (ClassFile.LineNumberTableAttribute.Entry[]) lnt.toArray(
                new ClassFile.LineNumberTableAttribute.Entry[lnt.size()]
            );
            attributes.add(new ClassFile.LineNumberTableAttribute(
                lineNumberTableAttributeNameIndex, // attributeNameIndex
                lnte                               // lineNumberTableEntries
            ));
        }

        // Add "LocalVariableTable" attribute.
        if (localVariableTableAttributeNameIndex != 0) {
            ClassFile.AttributeInfo ai = storeLocalVariableTable(dos, localVariableTableAttributeNameIndex);

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

        dos.writeShort(attributes.size());                         // attributes_count
        for (Iterator it = attributes.iterator(); it.hasNext();) { // attributes;
            ClassFile.AttributeInfo attribute = (ClassFile.AttributeInfo) it.next();
            attribute.store(dos);
        }
    }

    /**
     * @return A {@link org.codehaus.janino.util.ClassFile.LocalVariableTableAttribute} for this {@link CodeContext}
     */
    protected ClassFile.AttributeInfo
    storeLocalVariableTable(DataOutputStream dos, short localVariableTableAttributeNameIndex) {
        ClassFile       cf        = this.getClassFile();
        final List      entryList = new ArrayList();

        for (Iterator/*<Java.LocalVariableSlot>*/ iter = getAllLocalVars().iterator(); iter.hasNext();) {
            Java.LocalVariableSlot slot = (Java.LocalVariableSlot) iter.next();

            if (slot.getName() != null) {
                String typeName    = slot.getType().getDescriptor();
                short  classSlot   = cf.addConstantUtf8Info(typeName);
                short  varNameSlot = cf.addConstantUtf8Info(slot.getName());

//                System.out.println("slot: " + slot + ", typeSlot: " + classSlot + ", varSlot: " + varNameSlot);

                ClassFile.LocalVariableTableAttribute.Entry entry = new ClassFile.LocalVariableTableAttribute.Entry(
                    (short) slot.getStart().offset,
                    (short) (slot.getEnd().offset - slot.getStart().offset),
                    varNameSlot,
                    classSlot,
                    slot.getSlotIndex()
                );
                entryList.add(entry);
            }
        }

        if (entryList.size() > 0) {
            Object entries = entryList.toArray(new ClassFile.LocalVariableTableAttribute.Entry[entryList.size()]);

            return new ClassFile.LocalVariableTableAttribute(
                localVariableTableAttributeNameIndex,
                (ClassFile.LocalVariableTableAttribute.Entry[]) entries
            );
        } else {
            return null;
        }
    }

    /**
     * Checks the code for consistency; updates the "maxStack" member.
     *
     * Notice: On inconsistencies, a "RuntimeException" is thrown (KLUDGE).
     */
    public void
    flowAnalysis(String functionName) {
        if (CodeContext.DEBUG) {
            System.err.println("flowAnalysis(" + functionName + ")");
        }

        short[] stackSizes = new short[this.end.offset];
        Arrays.fill(stackSizes, CodeContext.UNEXAMINED);

        // Analyze flow from offset zero.
        this.flowAnalysis(
            functionName,
            this.code,       // code
            this.end.offset, // codeSize
            0,               // offset
            (short) 0,       // stackSize
            stackSizes       // stackSizes
        );

        // Analyze flow from exception handler entry points.
        int analyzedExceptionHandlers = 0;
        while (analyzedExceptionHandlers != this.exceptionTableEntries.size()) {
            for (int i = 0; i < this.exceptionTableEntries.size(); ++i) {
                ExceptionTableEntry exceptionTableEntry = (ExceptionTableEntry) this.exceptionTableEntries.get(i);
                if (stackSizes[exceptionTableEntry.startPC.offset] != CodeContext.UNEXAMINED) {
                    this.flowAnalysis(
                        functionName,
                        this.code,                                                    // code
                        this.end.offset,                                              // codeSize
                        exceptionTableEntry.handlerPC.offset,                         // offset
                        (short) (stackSizes[exceptionTableEntry.startPC.offset] + 1), // stackSize
                        stackSizes                                                    // stackSizes
                    );
                    ++analyzedExceptionHandlers;
                }
            }
        }

        // Check results and determine maximum stack size.
        this.maxStack = 0;
        for (int i = 0; i < stackSizes.length; ++i) {
            short ss = stackSizes[i];
            if (ss == CodeContext.UNEXAMINED) {
                if (CodeContext.DEBUG) {
                    System.out.println(functionName + ": Unexamined code at offset " + i);
                    return;
                } else {
                    throw new JaninoRuntimeException(functionName + ": Unexamined code at offset " + i);
                }
            }
            if (ss > this.maxStack) this.maxStack = ss;
        }
    }

    private void
    flowAnalysis(
        String  functionName,
        byte[]  code,      // Bytecode
        int     codeSize,  // Size
        int     offset,    // Current PC
        short   stackSize, // Stack size on entry
        short[] stackSizes // Stack sizes in code
    ) {
        for (;;) {
            if (CodeContext.DEBUG) System.out.println("Offset = " + offset + ", stack size = " + stackSize);

            // Check current bytecode offset.
            if (offset < 0 || offset >= codeSize) {
                throw new JaninoRuntimeException(functionName + ": Offset out of range");
            }

            // Have we hit an area that has already been analyzed?
            int css = stackSizes[offset];
            if (css == stackSize) return; // OK.
            if (css == CodeContext.INVALID_OFFSET) throw new JaninoRuntimeException(functionName + ": Invalid offset");
            if (css != CodeContext.UNEXAMINED) {
                if (CodeContext.DEBUG) {
                    System.err.println(
                        functionName
                        + ": Operand stack inconsistent at offset "
                        + offset
                        + ": Previous size "
                        + css
                        + ", now "
                        + stackSize
                    );
                    return;
                } else {
                    throw new JaninoRuntimeException(
                        functionName
                        + ": Operand stack inconsistent at offset "
                        + offset
                        + ": Previous size "
                        + css
                        + ", now "
                        + stackSize
                    );
                }
            }
            stackSizes[offset] = stackSize;

            // Analyze current opcode.
            byte  opcode        = code[offset];
            int   operandOffset = offset + 1;
            short props;
            if (opcode == Opcode.WIDE) {
                opcode = code[operandOffset++];
                props  = Opcode.WIDE_OPCODE_PROPERTIES[0xff & opcode];
            } else {
                props = Opcode.OPCODE_PROPERTIES[0xff & opcode];
            }
            if (props == Opcode.INVALID_OPCODE) {
                throw new JaninoRuntimeException(
                    functionName
                    + ": Invalid opcode "
                    + (0xff & opcode)
                    + " at offset "
                    + offset
                );
            }

            switch (props & Opcode.SD_MASK) {

            case Opcode.SD_M4:
            case Opcode.SD_M3:
            case Opcode.SD_M2:
            case Opcode.SD_M1:
            case Opcode.SD_P0:
            case Opcode.SD_P1:
            case Opcode.SD_P2:
                stackSize += (props & Opcode.SD_MASK) - Opcode.SD_P0;
                break;

            case Opcode.SD_0:
                stackSize = 0;
                break;

            case Opcode.SD_GETFIELD:
                --stackSize;
                /* FALL THROUGH */
            case Opcode.SD_GETSTATIC:
                stackSize += this.determineFieldSize((short) (
                    extract16BitValue(0, operandOffset, code)
                ));
                break;

            case Opcode.SD_PUTFIELD:
                --stackSize;
                /* FALL THROUGH */
            case Opcode.SD_PUTSTATIC:
                stackSize -= this.determineFieldSize((short) (
                    extract16BitValue(0, operandOffset, code)
                ));
                break;

            case Opcode.SD_INVOKEVIRTUAL:
            case Opcode.SD_INVOKESPECIAL:
            case Opcode.SD_INVOKEINTERFACE:
                --stackSize;
                /* FALL THROUGH */
            case Opcode.SD_INVOKESTATIC:
                stackSize -= this.determineArgumentsSize((short) (
                    extract16BitValue(0, operandOffset, code)
                ));
                break;

            case Opcode.SD_MULTIANEWARRAY:
                stackSize -= code[operandOffset + 2] - 1;
                break;

            default:
                throw new JaninoRuntimeException(functionName + ": Invalid stack delta");
            }

            if (stackSize < 0) {
                String msg = (
                    this.classFile.getThisClassName()
                    + '.'
                    + functionName
                    + ": Operand stack underrun at offset "
                    + offset
                );
                if (CodeContext.DEBUG) {
                    System.err.println(msg);
                    return;
                } else {
                    throw new JaninoRuntimeException(msg);
                }
            }

            if (stackSize > MAX_STACK_SIZE) {
                String msg = (
                    this.classFile.getThisClassName()
                    + '.'
                    + functionName
                    + ": Operand stack overflow at offset "
                    + offset
                );
                if (CodeContext.DEBUG) {
                    System.err.println(msg);
                    return;
                } else {
                    throw new JaninoRuntimeException(msg);
                }
            }

            switch (props & Opcode.OP1_MASK) {

            case 0:
                ;
                break;

            case Opcode.OP1_SB:
            case Opcode.OP1_UB:
            case Opcode.OP1_CP1:
            case Opcode.OP1_LV1:
                ++operandOffset;
                break;

            case Opcode.OP1_SS:
            case Opcode.OP1_CP2:
            case Opcode.OP1_LV2:
                operandOffset += 2;
                break;

            case Opcode.OP1_BO2:
                if (CodeContext.DEBUG) {
                    System.out.println("Offset = " + offset);
                    System.out.println("Operand offset = " + operandOffset);
                    System.out.println(code[operandOffset]);
                    System.out.println(code[operandOffset + 1]);
                }
                this.flowAnalysis(
                    functionName,
                    code, codeSize,
                    extract16BitValue(offset, operandOffset, code),
                    stackSize,
                    stackSizes
                );
                operandOffset += 2;
                break;

            case Opcode.OP1_JSR:
                if (CodeContext.DEBUG) {
                    System.out.println("Offset = " + offset);
                    System.out.println("Operand offset = " + operandOffset);
                    System.out.println(code[operandOffset]);
                    System.out.println(code[operandOffset + 1]);
                }
                int targetOffset = extract16BitValue(offset, operandOffset, code);
                operandOffset += 2;
                if (stackSizes[targetOffset] == CodeContext.UNEXAMINED) {
                    this.flowAnalysis(
                        functionName,
                        code, codeSize,
                        targetOffset,
                        (short) (stackSize + 1),
                        stackSizes
                    );
                }
                break;

            case Opcode.OP1_BO4:
                this.flowAnalysis(
                    functionName,
                    code, codeSize,
                    extract32BitValue(offset, operandOffset, code),
                    stackSize, stackSizes
                );
                operandOffset += 4;
                break;

            case Opcode.OP1_LOOKUPSWITCH:
                while ((operandOffset & 3) != 0) ++operandOffset;
                this.flowAnalysis(
                    functionName,
                    code, codeSize,
                    extract32BitValue(offset, operandOffset, code),
                    stackSize, stackSizes
                );
                operandOffset += 4;

                int npairs = extract32BitValue(0, operandOffset, code);
                operandOffset += 4;

                for (int i = 0; i < npairs; ++i) {
                    operandOffset += 4; //skip match value
                    this.flowAnalysis(
                        functionName,
                        code, codeSize,
                        extract32BitValue(offset, operandOffset, code),
                        stackSize, stackSizes
                    );
                    operandOffset += 4; //advance over offset
                }
                break;

            case Opcode.OP1_TABLESWITCH:
                while ((operandOffset & 3) != 0) ++operandOffset;
                this.flowAnalysis(
                    functionName,
                    code, codeSize,
                    extract32BitValue(offset, operandOffset, code),
                    stackSize, stackSizes
                );
                operandOffset += 4;
                int low = extract32BitValue(offset, operandOffset, code);
                operandOffset += 4;
                int hi = extract32BitValue(offset, operandOffset, code);
                operandOffset += 4;
                for (int i = low; i <= hi; ++i) {
                    this.flowAnalysis(
                        functionName,
                        code, codeSize,
                        extract32BitValue(offset, operandOffset, code),
                        stackSize, stackSizes
                    );
                    operandOffset += 4;
                }
                break;

            default:
                throw new JaninoRuntimeException(functionName + ": Invalid OP1");
            }

            switch (props & Opcode.OP2_MASK) {

            case 0:
                ;
                break;

            case Opcode.OP2_SB:
                ++operandOffset;
                break;

            case Opcode.OP2_SS:
                operandOffset += 2;
                break;

            default:
                throw new JaninoRuntimeException(functionName + ": Invalid OP2");
            }

            switch (props & Opcode.OP3_MASK) {

            case 0:
                ;
                break;

            case Opcode.OP3_SB:
                ++operandOffset;
                break;

            default:
                throw new JaninoRuntimeException(functionName + ": Invalid OP3");
            }

            Arrays.fill(stackSizes, offset + 1, operandOffset, CodeContext.INVALID_OFFSET);

            if ((props & Opcode.NO_FALLTHROUGH) != 0) return;
            offset = operandOffset;
        }
    }

    /**
     * Extract a 16 bit value at offset in code and add bias to it
     *
     * @param bias   An int to skew the final result by (useful for calculating relative offsets)
     * @param offset The position in the code array to extract the bytes from
     * @param code   The array of bytes
     * @return       An integer that treats the two bytes at position offset as an UNSIGNED SHORT
     */
    private int
    extract16BitValue(int bias, int offset, byte[] code) {
        int res = bias + (
            ((code[offset]) << 8)
            + (code[offset + 1] & 0xff)
        );
        if (CodeContext.DEBUG) {
            System.out.println("extract16BitValue(bias, offset) = (" + bias + ", " + offset + ")");
            System.out.println("bytes = {" + code[offset] + ", " + code[offset + 1] + "}");
            System.out.println("result = " + res);
        }
        return res;
    }

    /**
     * Extract a 32 bit value at offset in code and add bias to it
     *
     * @param bias   An int to skew the final result by (useful for calculating relative offsets)
     * @param offset The position in the code array to extract the bytes from
     * @param code   The array of bytes
     * @return       The 4 bytes at position offset + bias
     */
    private int
    extract32BitValue(int bias, int offset, byte[] code) {
        int res = bias + (
            (code[offset] << 24)
            + ((0xff & code[offset + 1]) << 16)
            + ((0xff & code[offset + 2]) << 8)
            + (0xff & code[offset + 3])
        );
        if (CodeContext.DEBUG) {
            System.out.println("extract32BitValue(bias, offset) = (" + bias + ", " + offset + ")");
            System.out.println(
                ""
                + "bytes = {"
                + code[offset]
                + ", "
                + code[offset + 1]
                + ", "
                + code[offset + 2]
                + ", "
                + code[offset + 3]
                + "}"
            );
            System.out.println("result = " + res);
        }
        return res;
    }

    /**
     * fixUp() all of the offsets and relocate() all relocatables
     */
    public void
    fixUpAndRelocate() {

        // We do this in a loop to allow relocatables to adjust the size
        // of things in the byte stream.  It is extremely unlikely, but possible
        // that a late relocatable will grow the size of the bytecode, and require
        // an earlier relocatable to switch from 32K mode to 64K mode branching
        do {
            fixUp();
        } while (!relocate());
    }

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

    /**
     * Relocate all relocatables and aggregate their response into a single one
     * @return true if all of them relocated successfully
     *         false if any of them needed to change size
     */
    private boolean
    relocate() {
        boolean finished = true;
        for (int i = 0; i < this.relocatables.size(); ++i) {
            //do not terminate earlier so that everything gets a chance to grow in the first pass
            //changes the common case for this to be O(n) instead of O(n**2)
            boolean part =  ((Relocatable) this.relocatables.get(i)).relocate();
            finished = finished && part;
        }
        return finished;
    }

    /**
     * Analyse the descriptor of the Fieldref and return its size.
     */
    private int
    determineFieldSize(short idx) {
        ClassFile.ConstantFieldrefInfo cfi = (
            (ClassFile.ConstantFieldrefInfo) this.classFile.getConstantPoolInfo(idx)
        );
        ClassFile.ConstantNameAndTypeInfo cnati = (
            (ClassFile.ConstantNameAndTypeInfo) this.classFile.getConstantPoolInfo(cfi.getNameAndTypeIndex())
        );
        ClassFile.ConstantUtf8Info cui = (
            (ClassFile.ConstantUtf8Info) this.classFile.getConstantPoolInfo(cnati.getDescriptorIndex())
        );
        return Descriptor.size(cui.getString());
    }

    /**
     * Analyse the descriptor of the Methodref and return the sum of the
     * arguments' sizes minus the return value's size.
     */
    private int
    determineArgumentsSize(short idx) {
        ClassFile.ConstantPoolInfo        cpi = this.classFile.getConstantPoolInfo(idx);
        ClassFile.ConstantNameAndTypeInfo nat = (ClassFile.ConstantNameAndTypeInfo) this.classFile.getConstantPoolInfo(
            cpi instanceof ClassFile.ConstantInterfaceMethodrefInfo
            ? ((ClassFile.ConstantInterfaceMethodrefInfo) cpi).getNameAndTypeIndex()
            : ((ClassFile.ConstantMethodrefInfo) cpi).getNameAndTypeIndex()
        );
        ClassFile.ConstantUtf8Info cui = (
            (ClassFile.ConstantUtf8Info) this.classFile.getConstantPoolInfo(nat.getDescriptorIndex())
        );
        String desc = cui.getString();

        if (desc.charAt(0) != '(') throw new JaninoRuntimeException("Method descriptor does not start with \"(\"");
        int i   = 1;
        int res = 0;
        for (;;) {
            switch (desc.charAt(i++)) {
            case ')':
                return res - Descriptor.size(desc.substring(i));
            case 'B': case 'C': case 'F': case 'I': case 'S': case 'Z':
                res += 1;
                break;
            case 'D': case 'J':
                res += 2;
                break;
            case '[':
                res += 1;
                while (desc.charAt(i) == '[') ++i;
                if ("BCFISZDJ".indexOf(desc.charAt(i)) != -1) { ++i; break; }
                if (desc.charAt(i) != 'L') throw new JaninoRuntimeException("Invalid char after \"[\"");
                ++i;
                while (desc.charAt(i++) != ';');
                break;
            case 'L':
                res += 1;
                while (desc.charAt(i++) != ';');
                break;
            default:
                throw new JaninoRuntimeException("Invalid method descriptor");
            }
        }
    }

    /**
     * Inserts a sequence of bytes at the current insertion position. Creates
     * {@link LineNumberOffset}s as necessary.
     *
     * @param lineNumber The line number that corresponds to the byte code, or -1
     * @param b
     */
    public void
    write(short lineNumber, byte[] b) {
        if (b.length == 0) return;

        int ico = this.currentInserter.offset;
        this.makeSpace(lineNumber, b.length);
        System.arraycopy(b, 0, this.code, ico, 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.
     *
     * @param lineNumber The line number that corresponds to the byte code, or -1
     * @param b1
     */
    public void
    write(short lineNumber, byte b1) {
        int ico = this.currentInserter.offset;
        this.makeSpace(lineNumber, 1);
        this.code[ico] = 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.
     *
     * @param lineNumber The line number that corresponds to the byte code, or -1
     * @param b1
     * @param b2
     */
    public void
    write(short lineNumber, byte b1, byte b2) {
        int ico = this.currentInserter.offset;
        this.makeSpace(lineNumber, 2);
        this.code[ico++] = b1;
        this.code[ico]   = 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.
     *
     * @param lineNumber The line number that corresponds to the byte code, or -1
     * @param b1
     * @param b2
     * @param b3
     */
    public void
    write(short lineNumber, byte b1, byte b2, byte b3) {
        int ico = this.currentInserter.offset;
        this.makeSpace(lineNumber, 3);
        this.code[ico++] = b1;
        this.code[ico++] = b2;
        this.code[ico]   = 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.
     *
     * @param lineNumber The line number that corresponds to the byte code, or -1
     * @param b1
     * @param b2
     * @param b3
     * @param b4
     */
    public void
    write(short lineNumber, byte b1, byte b2, byte b3, byte b4) {
        int ico = this.currentInserter.offset;
        this.makeSpace(lineNumber, 4);
        this.code[ico++] = b1;
        this.code[ico++] = b2;
        this.code[ico++] = b3;
        this.code[ico]   = b4;
    }

    /**
     * Add space for size bytes at current offset. Creates
     * {@link LineNumberOffset}s as necessary.
     *
     * @param lineNumber The line number that corresponds to the byte code, or -1
     * @param size       The size in bytes to inject
     */
    public void
    makeSpace(short lineNumber, int size) {
        if (size == 0) return;

        INSERT_LINE_NUMBER_OFFSET:
        if (lineNumber != -1) {
            Offset o;
            for (o = this.currentInserter.prev; o != this.beginning; o = o.prev) {
                if (o instanceof LineNumberOffset) {
                    if (((LineNumberOffset) o).lineNumber == lineNumber) break INSERT_LINE_NUMBER_OFFSET;
                    break;
                }
            }
            LineNumberOffset lno = new LineNumberOffset(this.currentInserter.offset, lineNumber);
            lno.prev = this.currentInserter.prev;
            lno.next = this.currentInserter;

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

        int ico = this.currentInserter.offset;
        if (this.end.offset + size <= this.code.length) {
            // Optimization to avoid a trivial method call in the common case
            if (ico != this.end.offset) {
                System.arraycopy(this.code, ico, this.code, ico + size, this.end.offset - ico);
            }
        } else {
            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 JaninoRuntimeException(
                    "Code attribute in class \""
                    + this.classFile.getThisClassName()
                    + "\" grows beyond 64 KB"
                );
            }
            this.code = new byte[newSize];
            System.arraycopy(oldCode, 0, this.code, 0, ico);
            System.arraycopy(oldCode, ico, this.code, ico + size, this.end.offset - ico);
        }
        Arrays.fill(this.code, ico, ico + size, (byte) 0);
        for (Offset o = this.currentInserter; o != null; o = o.next) o.offset += size;
    }

    /**
     * @param lineNumber The line number that corresponds to the byte code, or -1
     */
    public void
    writeShort(short lineNumber, int v) { this.write(lineNumber, (byte) (v >> 8), (byte) v); }

    /**
     * @param lineNumber The line number that corresponds to the byte code, or -1
     */
    public void
    writeBranch(short lineNumber, int opcode, final Offset dst) {
        this.relocatables.add(new Branch(opcode, dst));
        this.write(lineNumber, (byte) opcode, (byte) -1, (byte) -1);
    }

    private
    class Branch extends Relocatable {

        public
        Branch(int opcode, Offset destination) {
            this.opcode      = opcode;
            this.source      = CodeContext.this.newInserter();
            this.destination = destination;
            if (opcode == Opcode.JSR_W || opcode == Opcode.GOTO_W) {
                //no need to expand wide opcodes
                this.expanded = true;
            } else {
                this.expanded = false;
            }
        }

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

            if (!this.expanded && (offset > Short.MAX_VALUE || offset < Short.MIN_VALUE)) {
                //we want to insert the data without skewing our source position,
                //so we will cache it and then restore it later.
                int pos = this.source.offset;
                CodeContext.this.pushInserter(this.source);
                {
                    // promotion to a wide instruction only requires 2 extra bytes
                    // everything else requires a new GOTO_W instruction after a negated if
                    CodeContext.this.makeSpace(
                        (short) -1,
                        this.opcode == Opcode.GOTO ? 2 : this.opcode == Opcode.JSR ? 2 : 5
                    );
                }
                CodeContext.this.popInserter();
                this.source.offset = pos;
                this.expanded      = true;
                return false;
            }

            final byte[] ba;
            if (!this.expanded) {
                //we fit in a 16-bit jump
                ba = new byte[] { (byte) this.opcode, (byte) (offset >> 8), (byte) offset };
            } else {
                if (this.opcode == Opcode.GOTO || this.opcode == Opcode.JSR) {
                    ba = new byte[] {
                        (byte) (this.opcode + 33), // GOTO => GOTO_W; JSR => JSR_W
                        (byte) (offset >> 24),
                        (byte) (offset >> 16),
                        (byte) (offset >> 8),
                        (byte) offset
                    };
                } else
                {
                    //exclude the if-statement from jump target
                    //if jumping backwards this will increase the jump to go over it
                    //if jumping forwards this will decrease the jump by it
                    offset -= 3;

                    //  [if cond offset]
                    //expands to
                    //  [if !cond skip_goto]
                    //  [GOTO_W offset]
                    ba = new byte[] {
                        CodeContext.invertBranchOpcode((byte) this.opcode),
                        0,
                        8, // Jump from this instruction past the GOTO_W
                        Opcode.GOTO_W,
                        (byte) (offset >> 24),
                        (byte) (offset >> 16),
                        (byte) (offset >> 8),
                        (byte) offset
                    };
                }
            }
            System.arraycopy(ba, 0, CodeContext.this.code, this.source.offset, ba.length);
            return true;
        }

        private boolean        expanded; //marks whether this has been expanded to account for a wide branch
        private final int      opcode;
        private final 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 byte
    invertBranchOpcode(byte branchOpcode) {
        return ((Byte) CodeContext.BRANCH_OPCODE_INVERSION.get(new Byte(branchOpcode))).byteValue();
    }

    /** Byte branch-opcode => Byte inverted-branch-opcode */
    private static final Map BRANCH_OPCODE_INVERSION = CodeContext.createBranchOpcodeInversion();
    private static Map
    createBranchOpcodeInversion() {
        Map m = new HashMap();
        m.put(new Byte(Opcode.IF_ACMPEQ), new Byte(Opcode.IF_ACMPNE));
        m.put(new Byte(Opcode.IF_ACMPNE), new Byte(Opcode.IF_ACMPEQ));
        m.put(new Byte(Opcode.IF_ICMPEQ), new Byte(Opcode.IF_ICMPNE));
        m.put(new Byte(Opcode.IF_ICMPNE), new Byte(Opcode.IF_ICMPEQ));
        m.put(new Byte(Opcode.IF_ICMPGE), new Byte(Opcode.IF_ICMPLT));
        m.put(new Byte(Opcode.IF_ICMPLT), new Byte(Opcode.IF_ICMPGE));
        m.put(new Byte(Opcode.IF_ICMPGT), new Byte(Opcode.IF_ICMPLE));
        m.put(new Byte(Opcode.IF_ICMPLE), new Byte(Opcode.IF_ICMPGT));
        m.put(new Byte(Opcode.IFEQ),      new Byte(Opcode.IFNE));
        m.put(new Byte(Opcode.IFNE),      new Byte(Opcode.IFEQ));
        m.put(new Byte(Opcode.IFGE),      new Byte(Opcode.IFLT));
        m.put(new Byte(Opcode.IFLT),      new Byte(Opcode.IFGE));
        m.put(new Byte(Opcode.IFGT),      new Byte(Opcode.IFLE));
        m.put(new Byte(Opcode.IFLE),      new Byte(Opcode.IFGT));
        m.put(new Byte(Opcode.IFNULL),    new Byte(Opcode.IFNONNULL));
        m.put(new Byte(Opcode.IFNONNULL), new Byte(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(short lineNumber, Offset src, final Offset dst) {
        this.relocatables.add(new OffsetBranch(this.newOffset(), src, dst));
        this.write(lineNumber, (byte) -1, (byte) -1, (byte) -1, (byte) -1);
    }

    private
    class OffsetBranch extends Relocatable {

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

        @Override public boolean
        relocate() {
            if (this.source.offset == Offset.UNSET || this.destination.offset == Offset.UNSET) {
                throw new JaninoRuntimeException("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);
            return true;
        }
        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;
    }

    /**
     * Allocate an {@link Inserter}, set it to the current offset, and
     * insert it before the current offset.
     *
     * 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()}.
     */
    public Inserter
    newInserter() { Inserter i = new Inserter(); i.set(); return i; }

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

    /**
     * Remember the current {@link Inserter}, then replace it with the
     * new one.
     */
    public void
    pushInserter(Inserter ins) {
        if (ins.nextInserter != null) throw new JaninoRuntimeException("An Inserter can only be pushed once at a time");
        ins.nextInserter     = this.currentInserter;
        this.currentInserter = ins;
    }

    /**
     * Replace 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 JaninoRuntimeException("Code inserter stack underflow");
        this.currentInserter.nextInserter = null; // Mark it as "unpushed".
        this.currentInserter              = ni;
    }

    /**
     * A class that represents an offset within a "Code" attribute.
     *
     * 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.
     */
    public
    class Offset {

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

        /** Links to preceding and succeding offsets. */
        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;

        /**
         * Sets this "Offset" to the offset of the current inserter; inserts this "Offset" before the current inserter.
         */
        public void
        set() {
            if (this.offset != Offset.UNSET) throw new JaninoRuntimeException("Cannot \"set()\" Offset more than once");

            this.offset = CodeContext.this.currentInserter.offset;

            this.prev      = CodeContext.this.currentInserter.prev;
            this.next      = CodeContext.this.currentInserter;
            this.prev.next = this;
            this.next.prev = this;
        }

        /** @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; }
    }

    /**
     * Add another entry to the "exception_table" of this code attribute (see JVMS 4.7.3).
     *
     * @param catchTypeFd null == "finally" clause
     */
    public void
    addExceptionTableEntry(Offset startPc, Offset endPc, Offset handlerPc, 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 {
        private Inserter nextInserter; // null == not in "currentInserter" stack
    }

    /**
     * An {@link Offset} who#s sole purpose is to later create a 'LneNumberTable' attribute.
     */
    public
    class LineNumberOffset extends Offset {
        private final int lineNumber;

        public
        LineNumberOffset(int offset, int lineNumber) {
            this.lineNumber = lineNumber;
            this.offset     = offset;
        }
    }

    private abstract
    class Relocatable {

        /**
         * Relocate this object.
         * @return true if the relocation succeeded in place
         *         false if the relocation grew the number of bytes required
         */
        public abstract boolean 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.
     */
    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; }
}
