/* -*- Mode: java; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.javascript;

import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.mozilla.javascript.ast.AstNode;
import org.mozilla.javascript.ast.AstRoot;
import org.mozilla.javascript.ast.Block;
import org.mozilla.javascript.ast.FunctionNode;
import org.mozilla.javascript.ast.Jump;
import org.mozilla.javascript.ast.Scope;
import org.mozilla.javascript.ast.ScriptNode;
import org.mozilla.javascript.ast.TemplateCharacters;

/** Generates bytecode for the Interpreter. */
class CodeGenerator extends Icode {

    private static final int MIN_LABEL_TABLE_SIZE = 32;
    private static final int MIN_FIXUP_TABLE_SIZE = 40;

    private CompilerEnvirons compilerEnv;

    private boolean itsInFunctionFlag;
    private boolean itsInTryFlag;

    private InterpreterData itsData;

    private ScriptNode scriptOrFn;
    private int iCodeTop;
    private int stackDepth;
    private int lineNumber = -1;
    private int doubleTableTop;

    private final HashMap<String, Integer> strings = new HashMap<>();
    private final HashMap<BigInteger, Integer> bigInts = new HashMap<>();
    private int localTop;
    private int[] labelTable;
    private int labelTableTop;

    // fixupTable[i] = (label_index << 32) | fixup_site
    private long[] fixupTable;
    private int fixupTableTop;
    private final ArrayList<Object> literalIds = new ArrayList<>();

    private int exceptionTableTop;

    // ECF_ or Expression Context Flags constants: for now only TAIL
    private static final int ECF_TAIL = 1 << 0;

    public InterpreterData compile(
            CompilerEnvirons compilerEnv,
            ScriptNode tree,
            String rawSource,
            boolean returnFunction) {
        this.compilerEnv = compilerEnv;

        if (Token.printTrees) {
            System.out.println("before transform:");
            System.out.println(tree.toStringTree(tree));
        }

        new NodeTransformer().transform(tree, compilerEnv);

        if (Token.printTrees) {
            System.out.println("after transform:");
            System.out.println(tree.toStringTree(tree));
        }

        if (returnFunction) {
            scriptOrFn = tree.getFunctionNode(0);
        } else {
            scriptOrFn = tree;
        }

        itsData =
                new InterpreterData(
                        compilerEnv.getLanguageVersion(),
                        scriptOrFn.getSourceName(),
                        rawSource,
                        scriptOrFn.isInStrictMode());
        itsData.topLevel = true;

        if (returnFunction) {
            generateFunctionICode();
        } else {
            generateICodeFromTree(scriptOrFn);
        }
        return itsData;
    }

    private void generateFunctionICode() {
        itsInFunctionFlag = true;

        FunctionNode theFunction = (FunctionNode) scriptOrFn;

        itsData.itsFunctionType = theFunction.getFunctionType();
        itsData.itsNeedsActivation = theFunction.requiresActivation();
        if (theFunction.getFunctionName() != null) {
            itsData.itsName = theFunction.getName();
        }
        if (theFunction.isGenerator()) {
            addIcode(Icode_GENERATOR);
            addUint16(theFunction.getBaseLineno() & 0xFFFF);
        }
        if (theFunction.isInStrictMode()) {
            itsData.isStrict = true;
        }
        if (theFunction.isES6Generator()) {
            itsData.isES6Generator = true;
        }

        generateICodeFromTree(theFunction.getLastChild());
    }

    private void generateICodeFromTree(Node tree) {
        generateNestedFunctions();

        generateRegExpLiterals();

        generateTemplateLiterals();

        visitStatement(tree, 0);
        fixLabelGotos();
        // add RETURN_RESULT only to scripts as function always ends with RETURN
        if (itsData.itsFunctionType == 0) {
            addToken(Token.RETURN_RESULT);
        }

        if (itsData.itsICode.length != iCodeTop) {
            // Make itsData.itsICode length exactly iCodeTop to save memory
            // and catch bugs with jumps beyond icode as early as possible
            byte[] tmp = new byte[iCodeTop];
            System.arraycopy(itsData.itsICode, 0, tmp, 0, iCodeTop);
            itsData.itsICode = tmp;
        }
        if (strings.size() == 0) {
            itsData.itsStringTable = null;
        } else {
            itsData.itsStringTable = new String[strings.size()];
            for (Map.Entry<String, Integer> e : strings.entrySet()) {
                String str = e.getKey();
                int index = e.getValue();
                if (itsData.itsStringTable[index] != null) Kit.codeBug();
                itsData.itsStringTable[index] = str;
            }
        }
        if (doubleTableTop == 0) {
            itsData.itsDoubleTable = null;
        } else if (itsData.itsDoubleTable.length != doubleTableTop) {
            double[] tmp = new double[doubleTableTop];
            System.arraycopy(itsData.itsDoubleTable, 0, tmp, 0, doubleTableTop);
            itsData.itsDoubleTable = tmp;
        }
        if (bigInts.size() == 0) {
            itsData.itsBigIntTable = null;
        } else {
            itsData.itsBigIntTable = new BigInteger[bigInts.size()];
            for (Map.Entry<BigInteger, Integer> e : bigInts.entrySet()) {
                BigInteger bigInt = e.getKey();
                int index = e.getValue();
                if (itsData.itsBigIntTable[index] != null) Kit.codeBug();
                itsData.itsBigIntTable[index] = bigInt;
            }
        }
        if (exceptionTableTop != 0 && itsData.itsExceptionTable.length != exceptionTableTop) {
            int[] tmp = new int[exceptionTableTop];
            System.arraycopy(itsData.itsExceptionTable, 0, tmp, 0, exceptionTableTop);
            itsData.itsExceptionTable = tmp;
        }

        itsData.itsMaxVars = scriptOrFn.getParamAndVarCount();
        // itsMaxFrameArray: interpret method needs this amount for its
        // stack and sDbl arrays
        itsData.itsMaxFrameArray = itsData.itsMaxVars + itsData.itsMaxLocals + itsData.itsMaxStack;

        itsData.argNames = scriptOrFn.getParamAndVarNames();
        itsData.argIsConst = scriptOrFn.getParamAndVarConst();
        itsData.argCount = scriptOrFn.getParamCount();
        itsData.argsHasRest = scriptOrFn.hasRestParameter();
        itsData.argsHasDefaults = scriptOrFn.getDefaultParams() != null;

        itsData.rawSourceStart = scriptOrFn.getRawSourceStart();
        itsData.rawSourceEnd = scriptOrFn.getRawSourceEnd();

        if (literalIds.size() != 0) {
            itsData.literalIds = literalIds.toArray();
        }

        if (Token.printICode) Interpreter.dumpICode(itsData);
    }

    private void generateNestedFunctions() {
        int functionCount = scriptOrFn.getFunctionCount();
        if (functionCount == 0) return;

        InterpreterData[] array = new InterpreterData[functionCount];
        for (int i = 0; i != functionCount; i++) {
            FunctionNode fn = scriptOrFn.getFunctionNode(i);
            CodeGenerator gen = new CodeGenerator();
            gen.compilerEnv = compilerEnv;
            gen.scriptOrFn = fn;
            gen.itsData = new InterpreterData(itsData);
            gen.generateFunctionICode();
            array[i] = gen.itsData;

            final AstNode fnParent = fn.getParent();
            if (!(fnParent instanceof AstRoot
                    || fnParent instanceof Scope
                    || fnParent instanceof Block)) {
                gen.itsData.declaredAsFunctionExpression = true;
            }
        }
        itsData.itsNestedFunctions = array;
    }

    private void generateRegExpLiterals() {
        int N = scriptOrFn.getRegexpCount();
        if (N == 0) return;

        Context cx = Context.getContext();
        RegExpProxy rep = ScriptRuntime.checkRegExpProxy(cx);
        Object[] array = new Object[N];
        for (int i = 0; i != N; i++) {
            String string = scriptOrFn.getRegexpString(i);
            String flags = scriptOrFn.getRegexpFlags(i);
            array[i] = rep.compileRegExp(cx, string, flags);
        }
        itsData.itsRegExpLiterals = array;
    }

    private void generateTemplateLiterals() {
        int N = scriptOrFn.getTemplateLiteralCount();
        if (N == 0) return;

        Object[] array = new Object[N];
        for (int i = 0; i != N; i++) {
            List<TemplateCharacters> strings = scriptOrFn.getTemplateLiteralStrings(i);
            int j = 0;
            String[] values = new String[strings.size() * 2];
            for (TemplateCharacters s : strings) {
                values[j++] = s.getValue();
                values[j++] = s.getRawValue();
            }
            array[i] = values;
        }
        itsData.itsTemplateLiterals = array;
    }

    private void updateLineNumber(Node node) {
        int lineno = node.getLineno();
        if (lineno != lineNumber && lineno >= 0) {
            if (itsData.firstLinePC < 0) {
                itsData.firstLinePC = lineno;
            }
            lineNumber = lineno;
            addIcode(Icode_LINE);
            addUint16(lineno & 0xFFFF);
        }
    }

    private static RuntimeException badTree(Node node) {
        throw new RuntimeException(node.toString());
    }

    private void visitStatement(Node node, int initialStackDepth) {
        int type = node.getType();
        Node child = node.getFirstChild();
        switch (type) {
            case Token.FUNCTION:
                {
                    int fnIndex = node.getExistingIntProp(Node.FUNCTION_PROP);
                    int fnType = scriptOrFn.getFunctionNode(fnIndex).getFunctionType();
                    // Only function expressions or function expression
                    // statements need closure code creating new function
                    // object on stack as function statements are initialized
                    // at script/function start.
                    // In addition, function expressions can not be present here
                    // at statement level, they must only be present as expressions.
                    if (fnType == FunctionNode.FUNCTION_EXPRESSION_STATEMENT) {
                        addIndexOp(Icode_CLOSURE_STMT, fnIndex);
                    } else {
                        if (fnType != FunctionNode.FUNCTION_STATEMENT) {
                            throw Kit.codeBug();
                        }
                    }
                    // For function statements or function expression statements
                    // in scripts, we need to ensure that the result of the script
                    // is the function if it is the last statement in the script.
                    // For example, eval("function () {}") should return a
                    // function, not undefined.
                    if (!itsInFunctionFlag) {
                        addIndexOp(Icode_CLOSURE_EXPR, fnIndex);
                        stackChange(1);
                        addIcode(Icode_POP_RESULT);
                        stackChange(-1);
                    }
                }
                break;

            case Token.LABEL:
            case Token.LOOP:
            case Token.BLOCK:
            case Token.EMPTY:
            case Token.WITH:
                updateLineNumber(node);
            // fall through
            case Token.SCRIPT:
                while (child != null) {
                    visitStatement(child, initialStackDepth);
                    child = child.getNext();
                }
                break;

            case Token.ENTERWITH:
                visitExpression(child, 0);
                addToken(Token.ENTERWITH);
                stackChange(-1);
                break;

            case Token.LEAVEWITH:
                addToken(Token.LEAVEWITH);
                break;

            case Token.LOCAL_BLOCK:
                {
                    int local = allocLocal();
                    node.putIntProp(Node.LOCAL_PROP, local);
                    updateLineNumber(node);
                    while (child != null) {
                        visitStatement(child, initialStackDepth);
                        child = child.getNext();
                    }
                    addIndexOp(Icode_LOCAL_CLEAR, local);
                    releaseLocal(local);
                }
                break;

            case Token.DEBUGGER:
                addIcode(Icode_DEBUGGER);
                break;

            case Token.SWITCH:
                updateLineNumber(node);
                // See comments in IRFactory.createSwitch() for description
                // of SWITCH node
                {
                    visitExpression(child, 0);
                    for (Jump caseNode = (Jump) child.getNext();
                            caseNode != null;
                            caseNode = (Jump) caseNode.getNext()) {
                        if (caseNode.getType() != Token.CASE) throw badTree(caseNode);
                        Node test = caseNode.getFirstChild();
                        addIcode(Icode_DUP);
                        stackChange(1);
                        visitExpression(test, 0);
                        addToken(Token.SHEQ);
                        stackChange(-1);
                        // If true, Icode_IFEQ_POP will jump and remove case
                        // value from stack
                        addGoto(caseNode.target, Icode_IFEQ_POP);
                        stackChange(-1);
                    }
                    addIcode(Icode_POP);
                    stackChange(-1);
                }
                break;

            case Token.TARGET:
                markTargetLabel(node);
                break;

            case Token.IFEQ:
            case Token.IFNE:
                {
                    Node target = ((Jump) node).target;
                    visitExpression(child, 0);
                    addGoto(target, type);
                    stackChange(-1);
                }
                break;

            case Token.GOTO:
                {
                    Node target = ((Jump) node).target;
                    addGoto(target, type);
                }
                break;

            case Token.JSR:
                {
                    Node target = ((Jump) node).target;
                    addGoto(target, Icode_GOSUB);
                }
                break;

            case Token.FINALLY:
                {
                    // Account for incomming GOTOSUB address
                    stackChange(1);
                    int finallyRegister = getLocalBlockRef(node);
                    addIndexOp(Icode_STARTSUB, finallyRegister);
                    stackChange(-1);
                    while (child != null) {
                        visitStatement(child, initialStackDepth);
                        child = child.getNext();
                    }
                    addIndexOp(Icode_RETSUB, finallyRegister);
                }
                break;

            case Token.EXPR_VOID:
            case Token.EXPR_RESULT:
                updateLineNumber(node);
                visitExpression(child, 0);
                addIcode((type == Token.EXPR_VOID) ? Icode_POP : Icode_POP_RESULT);
                stackChange(-1);
                break;

            case Token.TRY:
                {
                    Jump tryNode = (Jump) node;
                    int exceptionObjectLocal = getLocalBlockRef(tryNode);
                    int scopeLocal = allocLocal();

                    addIndexOp(Icode_SCOPE_SAVE, scopeLocal);

                    int tryStart = iCodeTop;
                    boolean savedFlag = itsInTryFlag;
                    itsInTryFlag = true;
                    while (child != null) {
                        visitStatement(child, initialStackDepth);
                        child = child.getNext();
                    }
                    itsInTryFlag = savedFlag;

                    Node catchTarget = tryNode.target;
                    if (catchTarget != null) {
                        int catchStartPC = labelTable[getTargetLabel(catchTarget)];
                        addExceptionHandler(
                                tryStart,
                                catchStartPC,
                                catchStartPC,
                                false,
                                exceptionObjectLocal,
                                scopeLocal);
                    }
                    Node finallyTarget = tryNode.getFinally();
                    if (finallyTarget != null) {
                        int finallyStartPC = labelTable[getTargetLabel(finallyTarget)];
                        addExceptionHandler(
                                tryStart,
                                finallyStartPC,
                                finallyStartPC,
                                true,
                                exceptionObjectLocal,
                                scopeLocal);
                    }

                    addIndexOp(Icode_LOCAL_CLEAR, scopeLocal);
                    releaseLocal(scopeLocal);
                }
                break;

            case Token.CATCH_SCOPE:
                {
                    int localIndex = getLocalBlockRef(node);
                    int scopeIndex = node.getExistingIntProp(Node.CATCH_SCOPE_PROP);
                    String name = child.getType() == Token.NAME ? child.getString() : "";
                    child = child.getNext();
                    visitExpression(child, 0); // load expression object
                    addStringPrefix(name);
                    addIndexPrefix(localIndex);
                    addToken(Token.CATCH_SCOPE);
                    addUint8(scopeIndex != 0 ? 1 : 0);
                    stackChange(-1);
                }
                break;

            case Token.THROW:
                updateLineNumber(node);
                visitExpression(child, 0);
                addToken(Token.THROW);
                addUint16(lineNumber & 0xFFFF);
                stackChange(-1);
                break;

            case Token.RETHROW:
                updateLineNumber(node);
                addIndexOp(Token.RETHROW, getLocalBlockRef(node));
                break;

            case Token.RETURN:
                updateLineNumber(node);
                if (node.getIntProp(Node.GENERATOR_END_PROP, 0) != 0) {
                    if ((child == null)
                            || (compilerEnv.getLanguageVersion() < Context.VERSION_ES6)) {
                        // End generator function with no result, or old language version
                        // in which generators never return a result.
                        addIcode(Icode_GENERATOR_END);
                        addUint16(lineNumber & 0xFFFF);
                    } else {
                        visitExpression(child, ECF_TAIL);
                        addIcode(Icode_GENERATOR_RETURN);
                        addUint16(lineNumber & 0xFFFF);
                        stackChange(-1);
                    }

                } else {
                    if (child == null) {
                        addIcode(Icode_RETUNDEF);
                    } else {
                        visitExpression(child, ECF_TAIL);
                        addToken(Token.RETURN);
                        stackChange(-1);
                    }
                }
                break;

            case Token.RETURN_RESULT:
                updateLineNumber(node);
                addToken(Token.RETURN_RESULT);
                break;

            case Token.ENUM_INIT_KEYS:
            case Token.ENUM_INIT_VALUES:
            case Token.ENUM_INIT_ARRAY:
            case Token.ENUM_INIT_VALUES_IN_ORDER:
                visitExpression(child, 0);
                addIndexOp(type, getLocalBlockRef(node));
                stackChange(-1);
                break;

            case Icode_GENERATOR:
                break;

            default:
                throw badTree(node);
        }

        if (stackDepth != initialStackDepth) {
            throw Kit.codeBug();
        }
    }

    private void visitExpression(Node node, int contextFlags) {
        int type = node.getType();
        Node child = node.getFirstChild();
        int savedStackDepth = stackDepth;
        switch (type) {
            case Token.FUNCTION:
                {
                    int fnIndex = node.getExistingIntProp(Node.FUNCTION_PROP);
                    FunctionNode fn = scriptOrFn.getFunctionNode(fnIndex);
                    // See comments in visitStatement for Token.FUNCTION case
                    if (fn.getFunctionType() != FunctionNode.FUNCTION_EXPRESSION
                            && fn.getFunctionType() != FunctionNode.ARROW_FUNCTION) {
                        throw Kit.codeBug();
                    }
                    addIndexOp(Icode_CLOSURE_EXPR, fnIndex);
                    if (fn.isMethodDefinition()) {
                        addIcode(ICode_FN_STORE_HOME_OBJECT);
                    }
                    stackChange(1);
                }
                break;

            case Token.LOCAL_LOAD:
                {
                    int localIndex = getLocalBlockRef(node);
                    addIndexOp(Token.LOCAL_LOAD, localIndex);
                    stackChange(1);
                }
                break;

            case Token.COMMA:
                {
                    Node lastChild = node.getLastChild();
                    while (child != lastChild) {
                        visitExpression(child, 0);
                        addIcode(Icode_POP);
                        stackChange(-1);
                        child = child.getNext();
                    }
                    // Preserve tail context flag if any
                    visitExpression(child, contextFlags & ECF_TAIL);
                }
                break;

            case Token.USE_STACK:
                // Indicates that stack was modified externally,
                // like placed catch object
                stackChange(1);
                break;

            case Token.REF_CALL:
            case Token.CALL:
            case Token.NEW:
                {
                    boolean isOptionalChainingCall =
                            node.getIntProp(Node.OPTIONAL_CHAINING, 0) == 1;
                    CompleteOptionalCallJump completeOptionalCallJump = null;
                    if (type == Token.NEW) {
                        visitExpression(child, 0);
                    } else {
                        completeOptionalCallJump =
                                generateCallFunAndThis(child, isOptionalChainingCall);
                        if (completeOptionalCallJump != null) {
                            resolveForwardGoto(completeOptionalCallJump.putArgsAndDoCallLabel);
                        }
                    }
                    int argCount = 0;
                    while ((child = child.getNext()) != null) {
                        visitExpression(child, 0);
                        ++argCount;
                    }
                    int callType = node.getIntProp(Node.SPECIALCALL_PROP, Node.NON_SPECIALCALL);
                    if (type != Token.REF_CALL && callType != Node.NON_SPECIALCALL) {
                        // embed line number and source filename
                        addIndexOp(Icode_CALLSPECIAL, argCount);
                        addUint8(callType);
                        addUint8(type == Token.NEW ? 1 : 0);
                        addUint16(lineNumber & 0xFFFF);
                    } else if (node.getIntProp(Node.SUPER_PROPERTY_ACCESS, 0) == 1) {
                        addIndexOp(Icode_CALL_ON_SUPER, argCount);
                    } else {
                        // Only use the tail call optimization if we're not in a try
                        // or we're not generating debug info (since the
                        // optimization will confuse the debugger)
                        if (type == Token.CALL
                                && (contextFlags & ECF_TAIL) != 0
                                && !compilerEnv.isGenerateDebugInfo()
                                && !itsInTryFlag) {
                            type = Icode_TAIL_CALL;
                        }
                        addIndexOp(type, argCount);
                    }
                    // adjust stack
                    if (type == Token.NEW) {
                        // new: f, args -> result
                        stackChange(-argCount);
                    } else {
                        // call: f, thisObj, args -> result
                        // ref_call: f, thisObj, args -> ref
                        stackChange(-1 - argCount);
                    }
                    if (argCount > itsData.itsMaxCalleeArgs) {
                        itsData.itsMaxCalleeArgs = argCount;
                    }

                    if (completeOptionalCallJump != null) {
                        resolveForwardGoto(completeOptionalCallJump.afterLabel);
                    }
                }
                break;

            case Token.AND:
            case Token.OR:
                {
                    visitExpression(child, 0);
                    addIcode(Icode_DUP);
                    stackChange(1);
                    int afterSecondJumpStart = iCodeTop;
                    int jump = (type == Token.AND) ? Token.IFNE : Token.IFEQ;
                    addGotoOp(jump);
                    stackChange(-1);
                    addIcode(Icode_POP);
                    stackChange(-1);
                    child = child.getNext();
                    // Preserve tail context flag if any
                    visitExpression(child, contextFlags & ECF_TAIL);
                    resolveForwardGoto(afterSecondJumpStart);
                }
                break;

            case Token.HOOK:
                {
                    Node ifThen = child.getNext();
                    Node ifElse = ifThen.getNext();
                    visitExpression(child, 0);
                    int elseJumpStart = iCodeTop;
                    addGotoOp(Token.IFNE);
                    stackChange(-1);
                    // Preserve tail context flag if any
                    visitExpression(ifThen, contextFlags & ECF_TAIL);
                    int afterElseJumpStart = iCodeTop;
                    addGotoOp(Token.GOTO);
                    resolveForwardGoto(elseJumpStart);
                    stackDepth = savedStackDepth;
                    // Preserve tail context flag if any
                    visitExpression(ifElse, contextFlags & ECF_TAIL);
                    resolveForwardGoto(afterElseJumpStart);
                }
                break;

            case Token.GETPROP:
            case Token.GETPROPNOWARN:
                visitExpression(child, 0);
                child = child.getNext();
                if (node.getIntProp(Node.OPTIONAL_CHAINING, 0) == 1) {
                    // Jump if null or undefined
                    addIcode(Icode_DUP);
                    stackChange(1);
                    int putUndefinedLabel = iCodeTop;
                    addGotoOp(Icode.Icode_IF_NULL_UNDEF);
                    stackChange(-1);

                    // Access property
                    addStringOp(type, child.getString());
                    int afterLabel = iCodeTop;
                    addGotoOp(Token.GOTO);

                    // Put undefined
                    resolveForwardGoto(putUndefinedLabel);
                    addIcode(Icode_POP);
                    addStringOp(Token.NAME, "undefined");
                    resolveForwardGoto(afterLabel);
                } else if (node.getIntProp(Node.SUPER_PROPERTY_ACCESS, 0) == 1) {
                    addStringOp(
                            type == Token.GETPROP ? Token.GETPROP_SUPER : Token.GETPROPNOWARN_SUPER,
                            child.getString());
                } else {
                    addStringOp(type, child.getString());
                }
                break;

            case Token.DELPROP:
                boolean isName = child.getType() == Token.BINDNAME;
                visitExpression(child, 0);
                child = child.getNext();
                visitExpression(child, 0);
                if (node.getIntProp(Node.SUPER_PROPERTY_ACCESS, 0) == 1) {
                    addIcode(Icode_DELPROP_SUPER);
                } else if (isName) {
                    // special handling for delete name
                    addIcode(Icode_DELNAME);
                } else {
                    addToken(Token.DELPROP);
                }
                stackChange(-1);
                break;

            case Token.GETELEM:
                visitExpression(child, 0);
                child = child.getNext();
                if (node.getIntProp(Node.OPTIONAL_CHAINING, 0) == 1) {
                    addIcode(Icode_DUP);
                    stackChange(1);
                    int putUndefinedLabel = iCodeTop;
                    addGotoOp(Icode.Icode_IF_NULL_UNDEF);
                    stackChange(-1);

                    // Infix op
                    finishGetElemGeneration(child);
                    int afterLabel = iCodeTop;
                    addGotoOp(Token.GOTO);

                    // Put undefined
                    resolveForwardGoto(putUndefinedLabel);
                    addIcode(Icode_POP);
                    addStringOp(Token.NAME, "undefined");
                    resolveForwardGoto(afterLabel);
                } else if (node.getIntProp(Node.SUPER_PROPERTY_ACCESS, 0) == 1) {
                    visitExpression(child, 0);
                    addToken(Token.GETELEM_SUPER);
                    stackChange(-1);
                } else {
                    finishGetElemGeneration(child);
                }
                break;

            case Token.BITAND:
            case Token.BITOR:
            case Token.BITXOR:
            case Token.LSH:
            case Token.RSH:
            case Token.URSH:
            case Token.ADD:
            case Token.SUB:
            case Token.MOD:
            case Token.DIV:
            case Token.MUL:
            case Token.EXP:
            case Token.EQ:
            case Token.NE:
            case Token.SHEQ:
            case Token.SHNE:
            case Token.IN:
            case Token.INSTANCEOF:
            case Token.LE:
            case Token.LT:
            case Token.GE:
            case Token.GT:
                visitExpression(child, 0);
                child = child.getNext();
                visitExpression(child, 0);
                addToken(type);
                stackChange(-1);
                break;

            case Token.POS:
            case Token.NEG:
            case Token.NOT:
            case Token.BITNOT:
            case Token.TYPEOF:
            case Token.VOID:
                visitExpression(child, 0);
                if (type == Token.VOID) {
                    addIcode(Icode_POP);
                    addIcode(Icode_UNDEF);
                } else {
                    addToken(type);
                }
                break;

            case Token.GET_REF:
            case Token.DEL_REF:
                visitExpression(child, 0);
                if (node.getIntProp(Node.OPTIONAL_CHAINING, 0) == 1) {
                    // On the stack we'll have either the Ref or undefined
                    addIcode(Icode_DUP);
                    stackChange(1);

                    // If it's null or undefined, just jump ahead
                    int afterLabel = iCodeTop;
                    addGotoOp(Icode.Icode_IF_NULL_UNDEF);
                    stackChange(-1);

                    // Otherwise do the GET_REF
                    addToken(type);

                    resolveForwardGoto(afterLabel);
                } else {
                    addToken(type);
                }
                break;

            case Token.SETPROP:
            case Token.SETPROP_OP:
                {
                    visitExpression(child, 0);
                    child = child.getNext();
                    String property = child.getString();
                    child = child.getNext();
                    if (type == Token.SETPROP_OP) {
                        addIcode(Icode_DUP);
                        stackChange(1);
                        addStringOp(Token.GETPROP, property);
                        // Compensate for the following USE_STACK
                        stackChange(-1);
                    }
                    visitExpression(child, 0);
                    addStringOp(
                            node.getIntProp(Node.SUPER_PROPERTY_ACCESS, 0) == 1
                                    ? Token.SETPROP_SUPER
                                    : Token.SETPROP,
                            property);
                    stackChange(-1);
                }
                break;

            case Token.SETELEM:
            case Token.SETELEM_OP:
                visitExpression(child, 0);
                child = child.getNext();
                visitExpression(child, 0);
                child = child.getNext();
                if (type == Token.SETELEM_OP) {
                    addIcode(Icode_DUP2);
                    stackChange(2);
                    addToken(Token.GETELEM);
                    stackChange(-1);
                    // Compensate for the following USE_STACK
                    stackChange(-1);
                }
                visitExpression(child, 0);
                addToken(
                        node.getIntProp(Node.SUPER_PROPERTY_ACCESS, 0) == 1
                                ? Token.SETELEM_SUPER
                                : Token.SETELEM);
                stackChange(-2);
                break;

            case Token.SET_REF:
            case Token.SET_REF_OP:
                visitExpression(child, 0);
                child = child.getNext();
                if (type == Token.SET_REF_OP) {
                    addIcode(Icode_DUP);
                    stackChange(1);
                    addToken(Token.GET_REF);
                    // Compensate for the following USE_STACK
                    stackChange(-1);
                }
                visitExpression(child, 0);
                addToken(Token.SET_REF);
                stackChange(-1);
                break;

            case Token.STRICT_SETNAME:
            case Token.SETNAME:
                {
                    String name = child.getString();
                    visitExpression(child, 0);
                    child = child.getNext();
                    visitExpression(child, 0);
                    addStringOp(type, name);
                    stackChange(-1);
                }
                break;

            case Token.SETCONST:
                {
                    String name = child.getString();
                    visitExpression(child, 0);
                    child = child.getNext();
                    visitExpression(child, 0);
                    addStringOp(Icode_SETCONST, name);
                    stackChange(-1);
                }
                break;

            case Token.TYPEOFNAME:
                {
                    int index = -1;
                    // use typeofname if an activation frame exists
                    // since the vars all exist there instead of in jregs
                    if (itsInFunctionFlag && !itsData.itsNeedsActivation)
                        index = scriptOrFn.getIndexForNameNode(node);
                    if (index == -1) {
                        addStringOp(Icode_TYPEOFNAME, node.getString());
                        stackChange(1);
                    } else {
                        addVarOp(Token.GETVAR, index);
                        stackChange(1);
                        addToken(Token.TYPEOF);
                    }
                }
                break;

            case Token.BINDNAME:
            case Token.NAME:
            case Token.STRING:
                addStringOp(type, node.getString());
                stackChange(1);
                break;

            case Token.INC:
            case Token.DEC:
                visitIncDec(node, child);
                break;

            case Token.NUMBER:
                {
                    double num = node.getDouble();
                    int inum = (int) num;
                    if (inum == num) {
                        if (inum == 0) {
                            addIcode(Icode_ZERO);
                            // Check for negative zero
                            if (1.0 / num < 0.0) {
                                addToken(Token.NEG);
                            }
                        } else if (inum == 1) {
                            addIcode(Icode_ONE);
                        } else if ((short) inum == inum) {
                            addIcode(Icode_SHORTNUMBER);
                            // write short as uin16 bit pattern
                            addUint16(inum & 0xFFFF);
                        } else {
                            addIcode(Icode_INTNUMBER);
                            addInt(inum);
                        }
                    } else {
                        int index = getDoubleIndex(num);
                        addIndexOp(Token.NUMBER, index);
                    }
                    stackChange(1);
                }
                break;

            case Token.GETVAR:
                {
                    if (itsData.itsNeedsActivation) Kit.codeBug();
                    int index = scriptOrFn.getIndexForNameNode(node);
                    addVarOp(Token.GETVAR, index);
                    stackChange(1);
                }
                break;

            case Token.SETVAR:
                {
                    if (itsData.itsNeedsActivation) Kit.codeBug();
                    int index = scriptOrFn.getIndexForNameNode(child);
                    child = child.getNext();
                    visitExpression(child, 0);
                    addVarOp(Token.SETVAR, index);
                }
                break;

            case Token.SETCONSTVAR:
                {
                    if (itsData.itsNeedsActivation) Kit.codeBug();
                    int index = scriptOrFn.getIndexForNameNode(child);
                    child = child.getNext();
                    visitExpression(child, 0);
                    addVarOp(Token.SETCONSTVAR, index);
                }
                break;

            case Token.NULL:
            case Token.THIS:
            case Token.SUPER:
            case Token.THISFN:
            case Token.FALSE:
            case Token.TRUE:
                addToken(type);
                stackChange(1);
                break;

            case Token.ENUM_NEXT:
            case Token.ENUM_ID:
                addIndexOp(type, getLocalBlockRef(node));
                stackChange(1);
                break;

            case Token.BIGINT:
                addBigInt(node.getBigInt());
                stackChange(1);
                break;

            case Token.REGEXP:
                {
                    int index = node.getExistingIntProp(Node.REGEXP_PROP);
                    addIndexOp(Token.REGEXP, index);
                    stackChange(1);
                }
                break;

            case Token.ARRAYLIT:
            case Token.OBJECTLIT:
                visitLiteral(node, child);
                break;

            case Token.ARRAYCOMP:
                visitArrayComprehension(node, child, child.getNext());
                break;

            case Token.REF_SPECIAL:
                visitExpression(child, 0);
                if (node.getIntProp(Node.OPTIONAL_CHAINING, 0) == 1) {
                    // Jump if null or undefined
                    addIcode(Icode_DUP);
                    stackChange(1);
                    int putUndefinedLabel = iCodeTop;
                    addGotoOp(Icode.Icode_IF_NULL_UNDEF);
                    stackChange(-1);

                    // Access property
                    addStringOp(type, (String) node.getProp(Node.NAME_PROP));
                    int afterLabel = iCodeTop;
                    addGotoOp(Token.GOTO);

                    // Put undefined
                    resolveForwardGoto(putUndefinedLabel);
                    addIcode(Icode_POP);
                    addStringOp(Token.NAME, "undefined");
                    resolveForwardGoto(afterLabel);
                } else {
                    addStringOp(type, (String) node.getProp(Node.NAME_PROP));
                }
                break;

            case Token.REF_MEMBER:
            case Token.REF_NS_MEMBER:
            case Token.REF_NAME:
            case Token.REF_NS_NAME:
                {
                    int memberTypeFlags = node.getIntProp(Node.MEMBER_TYPE_PROP, 0);
                    // generate possible target, possible namespace and member
                    int childCount = 0;
                    do {
                        visitExpression(child, 0);
                        ++childCount;
                        child = child.getNext();
                    } while (child != null);
                    addIndexOp(type, memberTypeFlags);
                    stackChange(1 - childCount);
                }
                break;

            case Token.DOTQUERY:
                {
                    int queryPC;
                    updateLineNumber(node);
                    visitExpression(child, 0);
                    addIcode(Icode_ENTERDQ);
                    stackChange(-1);
                    queryPC = iCodeTop;
                    visitExpression(child.getNext(), 0);
                    addBackwardGoto(Icode_LEAVEDQ, queryPC);
                }
                break;

            case Token.DEFAULTNAMESPACE:
            case Token.ESCXMLATTR:
            case Token.ESCXMLTEXT:
                visitExpression(child, 0);
                addToken(type);
                break;

            case Token.YIELD:
            case Token.YIELD_STAR:
                if (child != null) {
                    visitExpression(child, 0);
                } else {
                    addIcode(Icode_UNDEF);
                    stackChange(1);
                }
                if (type == Token.YIELD) {
                    addToken(Token.YIELD);
                } else {
                    addIcode(Icode_YIELD_STAR);
                }
                addUint16(node.getLineno() & 0xFFFF);
                break;

            case Token.WITHEXPR:
                {
                    Node enterWith = node.getFirstChild();
                    Node with = enterWith.getNext();
                    visitExpression(enterWith.getFirstChild(), 0);
                    addToken(Token.ENTERWITH);
                    stackChange(-1);
                    visitExpression(with.getFirstChild(), 0);
                    addToken(Token.LEAVEWITH);
                    break;
                }

            case Token.TEMPLATE_LITERAL:
                visitTemplateLiteral(node);
                break;

            case Token.NULLISH_COALESCING:
                {
                    visitExpression(child, 0);
                    child = child.getNext();

                    addIcode(Icode_DUP);
                    stackChange(1);
                    int end = iCodeTop;
                    addGotoOp(Icode.Icode_IF_NOT_NULL_UNDEF);
                    stackChange(-1);

                    addIcode(Icode_POP);
                    visitExpression(child, 0);
                    stackChange(-1);

                    resolveForwardGoto(end);
                    break;
                }

            default:
                throw badTree(node);
        }
        if (savedStackDepth + 1 != stackDepth) {
            Kit.codeBug();
        }
    }

    private void finishGetElemGeneration(Node child) {
        visitExpression(child, 0);
        addToken(Token.GETELEM);
        stackChange(-1);
    }

    private CompleteOptionalCallJump generateCallFunAndThis(
            Node left, boolean isOptionalChainingCall) {
        // Generate code to place on stack function and thisObj
        int type = left.getType();
        switch (type) {
            case Token.NAME:
                {
                    String name = left.getString();
                    // stack: ... -> ... function thisObj
                    if (isOptionalChainingCall) {
                        addStringOp(Icode_NAME_AND_THIS_OPTIONAL, name);
                        stackChange(2);
                        return completeOptionalCallJump();
                    } else {
                        addStringOp(Icode_NAME_AND_THIS, name);
                        stackChange(2);
                    }
                    break;
                }
            case Token.GETPROP:
            case Token.GETELEM:
                {
                    Node target = left.getFirstChild();
                    visitExpression(target, 0);
                    Node id = target.getNext();
                    if (type == Token.GETPROP) {
                        String property = id.getString();
                        // stack: ... target -> ... function thisObj
                        if (isOptionalChainingCall) {
                            addStringOp(Icode_PROP_AND_THIS_OPTIONAL, property);
                            stackChange(1);
                            return completeOptionalCallJump();
                        } else {
                            addStringOp(Icode_PROP_AND_THIS, property);
                            stackChange(1);
                        }
                    } else {
                        visitExpression(id, 0);
                        // stack: ... target id -> ... function thisObj
                        if (isOptionalChainingCall) {
                            addIcode(Icode_ELEM_AND_THIS_OPTIONAL);
                            return completeOptionalCallJump();
                        } else {
                            addIcode(Icode_ELEM_AND_THIS);
                        }
                    }
                    break;
                }
            default:
                // Including Token.GETVAR
                visitExpression(left, 0);
                // stack: ... value -> ... function thisObj
                if (isOptionalChainingCall) {
                    addIcode(Icode_VALUE_AND_THIS_OPTIONAL);
                    stackChange(1);
                    return completeOptionalCallJump();
                } else {
                    addIcode(Icode_VALUE_AND_THIS);
                    stackChange(1);
                }
                break;
        }
        return null;
    }

    private CompleteOptionalCallJump completeOptionalCallJump() {
        // If it's null or undefined, pop undefined and skip the arguments and call
        addIcode(Icode_DUP);
        stackChange(1);
        int putArgsAndDoCallLabel = iCodeTop;
        addGotoOp(Icode.Icode_IF_NOT_NULL_UNDEF);
        stackChange(-1);

        // Put undefined
        addIcode(Icode_POP);
        addIcode(Icode_POP);
        addStringOp(Token.NAME, "undefined");
        int afterLabel = iCodeTop;
        addGotoOp(Token.GOTO);

        return new CompleteOptionalCallJump(putArgsAndDoCallLabel, afterLabel);
    }

    private void visitIncDec(Node node, Node child) {
        int incrDecrMask = node.getExistingIntProp(Node.INCRDECR_PROP);
        int childType = child.getType();

        if (child.getIntProp(Node.SUPER_PROPERTY_ACCESS, 0) == 1) {
            visitSuperIncDec(node, child, childType, incrDecrMask);
            return;
        }

        switch (childType) {
            case Token.GETVAR:
                {
                    if (itsData.itsNeedsActivation) Kit.codeBug();
                    int i = scriptOrFn.getIndexForNameNode(child);
                    addVarOp(Icode_VAR_INC_DEC, i);
                    addUint8(incrDecrMask);
                    stackChange(1);
                    break;
                }
            case Token.NAME:
                {
                    String name = child.getString();
                    addStringOp(Icode_NAME_INC_DEC, name);
                    addUint8(incrDecrMask);
                    stackChange(1);
                    break;
                }
            case Token.GETPROP:
                {
                    Node object = child.getFirstChild();
                    visitExpression(object, 0);
                    String property = object.getNext().getString();
                    addStringOp(Icode_PROP_INC_DEC, property);
                    addUint8(incrDecrMask);
                    break;
                }
            case Token.GETELEM:
                {
                    Node object = child.getFirstChild();
                    visitExpression(object, 0);
                    Node index = object.getNext();
                    visitExpression(index, 0);
                    addIcode(Icode_ELEM_INC_DEC);
                    addUint8(incrDecrMask);
                    stackChange(-1);
                    break;
                }
            case Token.GET_REF:
                {
                    Node ref = child.getFirstChild();
                    visitExpression(ref, 0);
                    addIcode(Icode_REF_INC_DEC);
                    addUint8(incrDecrMask);
                    break;
                }
            default:
                {
                    throw badTree(node);
                }
        }
    }

    // Handles super.x++ and variants thereof. We don't want to create new icode in the interpreter
    // for this edge case, so we will transform this into something like super.x = super.x + 1
    private void visitSuperIncDec(Node node, Node child, int childType, int incrDecrMask) {
        Node object = child.getFirstChild();

        // Push the old value on the stack
        visitExpression(object, 0); // stack: [super]
        switch (childType) {
            case Token.GETPROP:
                addStringOp(Token.GETPROP_SUPER, object.getNext().getString()); // stack: [p]
                break;

            case Token.GETELEM:
                {
                    Node index = object.getNext();
                    visitExpression(index, 0); // stack: [super, elem]
                    addToken(Token.GETELEM_SUPER); // stack: [p]
                    stackChange(-1);
                    break;
                }

            default:
                throw badTree(node);
        }

        // If it's a postfix expression, we copy the old value
        // If it's postfix, we only need the _new_ value on the stack
        if ((incrDecrMask & Node.POST_FLAG) != 0) {
            addIcode(Icode_DUP); // stack: postfix [p, p], prefix: [p]
            stackChange(+1);
        }

        // We need, in order, super and then the new value
        addToken(Token.SUPER); // stack: postfix [p, p, super], prefix: [p, super]
        stackChange(+1);
        addIcode(Icode_SWAP); // stack: postfix [p, super, p], prefix: [super, p]

        // Increment or decrement the new value
        addIcode(Icode_ONE); // stack: prefix  [p, super, p, 1], postfix: [super, p, 1]
        stackChange(+1);
        if ((incrDecrMask & Node.DECR_FLAG) == 0) {
            addToken(Token.ADD); // stack: prefix [p, super, p+1], postfix: [super, p+1]
        } else {
            addToken(Token.SUB); // stack: prefix [p, super, p-1], postfix: [super, p-1]
        }
        stackChange(-1);

        // Assign the new value to the property
        switch (childType) {
            case Token.GETPROP:
                addStringOp(Token.SETPROP_SUPER, object.getNext().getString());
                // stack: prefix [p, p+-1], postfix: [p+-1]
                stackChange(-1);
                break;

            case Token.GETELEM:
                {
                    Node index = object.getNext();
                    visitExpression(index, 0);
                    // stack: prefix [p, super, p+-1, elem], postfix: [super, p+-1, elem]
                    addToken(Token.SETELEM_SUPER); // stack: prefix [p, p+-1], postfix: [p+-1]
                    stackChange(-2);
                    break;
                }
        }

        // If it was a postfix, just drop the new value
        if ((incrDecrMask & Node.POST_FLAG) != 0) {
            addIcode(Icode_POP); // stack: [p]
            stackChange(-1);
        }
    }

    private void visitLiteral(Node node, Node child) {
        int type = node.getType();
        if (type == Token.ARRAYLIT) {
            visitArrayLiteral(node, child);
        } else if (type == Token.OBJECTLIT) {
            visitObjectLiteral(node, child);
        } else {
            throw badTree(node);
        }
    }

    private void visitObjectLiteral(Node node, Node child) {
        Object[] propertyIds = (Object[]) node.getProp(Node.OBJECT_IDS_PROP);
        int count = propertyIds == null ? 0 : propertyIds.length;
        boolean hasAnyComputedProperty =
                propertyIds != null
                        && Arrays.stream(propertyIds).anyMatch(id -> id instanceof Node);

        int nextLiteralIndex = literalIds.size();
        literalIds.add(propertyIds);

        addIndexOp(Icode_LITERAL_NEW_OBJECT, nextLiteralIndex);
        addUint8(hasAnyComputedProperty ? 1 : 0);
        stackChange(4);

        int i = 0;
        while (child != null) {
            // Computed key
            Object propertyId = propertyIds == null ? null : propertyIds[i];
            if (propertyId instanceof Node) {
                // Will be a node of type Token.COMPUTED_PROPERTY wrapping the actual expression
                Node computedPropertyNode = (Node) propertyId;
                visitExpression(computedPropertyNode.first, 0);
                addIcode(Icode_LITERAL_KEY_SET);
                stackChange(-1);
            }

            // Value
            visitLiteralValue(child);
            child = child.getNext();
            i++;
        }

        addToken(Token.OBJECTLIT);

        stackChange(-3);
    }

    private void visitArrayLiteral(Node node, Node child) {
        int count = 0;
        for (Node n = child; n != null; n = n.getNext()) {
            ++count;
        }
        addIndexOp(Icode_LITERAL_NEW_ARRAY, count);
        stackChange(2);
        while (child != null) {
            visitLiteralValue(child);
            child = child.getNext();
        }
        int[] skipIndexes = (int[]) node.getProp(Node.SKIP_INDEXES_PROP);
        if (skipIndexes == null) {
            addToken(Token.ARRAYLIT);
        } else {
            int index = literalIds.size();
            literalIds.add(skipIndexes);
            addIndexOp(Icode_SPARE_ARRAYLIT, index);
        }
        stackChange(-1);
    }

    private void visitLiteralValue(Node child) {
        int childType = child.getType();
        if (childType == Token.GET) {
            visitExpression(child.getFirstChild(), 0);
            addIcode(Icode_LITERAL_GETTER);
        } else if (childType == Token.SET) {
            visitExpression(child.getFirstChild(), 0);
            addIcode(Icode_LITERAL_SETTER);
        } else if (childType == Token.METHOD) {
            visitExpression(child.getFirstChild(), 0);
            addIcode(Icode_LITERAL_SET);
        } else {
            visitExpression(child, 0);
            addIcode(Icode_LITERAL_SET);
        }
        stackChange(-1);
    }

    private void visitTemplateLiteral(Node node) {
        int index = node.getExistingIntProp(Node.TEMPLATE_LITERAL_PROP);
        addIndexOp(Icode_TEMPLATE_LITERAL_CALLSITE, index);
        stackChange(1);
    }

    private void visitArrayComprehension(Node node, Node initStmt, Node expr) {
        // A bit of a hack: array comprehensions are implemented using
        // statement nodes for the iteration, yet they appear in an
        // expression context. So we pass the current stack depth to
        // visitStatement so it can check that the depth is not altered
        // by statements.
        visitStatement(initStmt, stackDepth);
        visitExpression(expr, 0);
    }

    private static int getLocalBlockRef(Node node) {
        Node localBlock = (Node) node.getProp(Node.LOCAL_BLOCK_PROP);
        return localBlock.getExistingIntProp(Node.LOCAL_PROP);
    }

    private int getTargetLabel(Node target) {
        int label = target.labelId();
        if (label != -1) {
            return label;
        }
        label = labelTableTop;
        if (labelTable == null || label == labelTable.length) {
            if (labelTable == null) {
                labelTable = new int[MIN_LABEL_TABLE_SIZE];
            } else {
                int[] tmp = new int[labelTable.length * 2];
                System.arraycopy(labelTable, 0, tmp, 0, label);
                labelTable = tmp;
            }
        }
        labelTableTop = label + 1;
        labelTable[label] = -1;

        target.labelId(label);
        return label;
    }

    private void markTargetLabel(Node target) {
        int label = getTargetLabel(target);
        if (labelTable[label] != -1) {
            // Can mark label only once
            Kit.codeBug();
        }
        labelTable[label] = iCodeTop;
    }

    private void addGoto(Node target, int gotoOp) {
        int label = getTargetLabel(target);
        if (!(label < labelTableTop)) Kit.codeBug();
        int targetPC = labelTable[label];

        if (targetPC != -1) {
            addBackwardGoto(gotoOp, targetPC);
        } else {
            int gotoPC = iCodeTop;
            addGotoOp(gotoOp);
            int top = fixupTableTop;
            if (fixupTable == null || top == fixupTable.length) {
                if (fixupTable == null) {
                    fixupTable = new long[MIN_FIXUP_TABLE_SIZE];
                } else {
                    long[] tmp = new long[fixupTable.length * 2];
                    System.arraycopy(fixupTable, 0, tmp, 0, top);
                    fixupTable = tmp;
                }
            }
            fixupTableTop = top + 1;
            fixupTable[top] = ((long) label << 32) | gotoPC;
        }
    }

    private void fixLabelGotos() {
        for (int i = 0; i < fixupTableTop; i++) {
            long fixup = fixupTable[i];
            int label = (int) (fixup >> 32);
            int jumpSource = (int) fixup;
            int pc = labelTable[label];
            if (pc == -1) {
                // Unlocated label
                throw Kit.codeBug();
            }
            resolveGoto(jumpSource, pc);
        }
        fixupTableTop = 0;
    }

    private void addBackwardGoto(int gotoOp, int jumpPC) {
        int fromPC = iCodeTop;
        // Ensure that this is a jump backward
        if (fromPC <= jumpPC) throw Kit.codeBug();
        addGotoOp(gotoOp);
        resolveGoto(fromPC, jumpPC);
    }

    private void resolveForwardGoto(int fromPC) {
        // Ensure that forward jump skips at least self bytecode
        if (iCodeTop < fromPC + 3) throw Kit.codeBug();
        resolveGoto(fromPC, iCodeTop);
    }

    private void resolveGoto(int fromPC, int jumpPC) {
        int offset = jumpPC - fromPC;
        // Ensure that jumps do not overlap
        if (0 <= offset && offset <= 2) throw Kit.codeBug();
        int offsetSite = fromPC + 1;
        if (offset != (short) offset) {
            if (itsData.longJumps == null) {
                itsData.longJumps = new HashMap<>();
            }
            itsData.longJumps.put(offsetSite, jumpPC);
            offset = 0;
        }
        byte[] array = itsData.itsICode;
        array[offsetSite] = (byte) (offset >> 8);
        array[offsetSite + 1] = (byte) offset;
    }

    private void addToken(int token) {
        if (!Icode.validTokenCode(token)) throw Kit.codeBug();
        addUint8(token);
    }

    private void addIcode(int icode) {
        if (!Icode.validIcode(icode)) throw Kit.codeBug();
        // Write negative icode as uint8 bits
        addUint8(icode & 0xFF);
    }

    private void addUint8(int value) {
        if ((value & ~0xFF) != 0) throw Kit.codeBug();
        byte[] array = itsData.itsICode;
        int top = iCodeTop;
        if (top == array.length) {
            array = increaseICodeCapacity(1);
        }
        array[top] = (byte) value;
        iCodeTop = top + 1;
    }

    private void addUint16(int value) {
        if ((value & ~0xFFFF) != 0) throw Kit.codeBug();
        byte[] array = itsData.itsICode;
        int top = iCodeTop;
        if (top + 2 > array.length) {
            array = increaseICodeCapacity(2);
        }
        array[top] = (byte) (value >>> 8);
        array[top + 1] = (byte) value;
        iCodeTop = top + 2;
    }

    private void addInt(int i) {
        byte[] array = itsData.itsICode;
        int top = iCodeTop;
        if (top + 4 > array.length) {
            array = increaseICodeCapacity(4);
        }
        array[top] = (byte) (i >>> 24);
        array[top + 1] = (byte) (i >>> 16);
        array[top + 2] = (byte) (i >>> 8);
        array[top + 3] = (byte) i;
        iCodeTop = top + 4;
    }

    private int getDoubleIndex(double num) {
        int index = doubleTableTop;
        if (index == 0) {
            itsData.itsDoubleTable = new double[64];
        } else if (itsData.itsDoubleTable.length == index) {
            double[] na = new double[index * 2];
            System.arraycopy(itsData.itsDoubleTable, 0, na, 0, index);
            itsData.itsDoubleTable = na;
        }
        itsData.itsDoubleTable[index] = num;
        doubleTableTop = index + 1;
        return index;
    }

    private void addGotoOp(int gotoOp) {
        byte[] array = itsData.itsICode;
        int top = iCodeTop;
        if (top + 3 > array.length) {
            array = increaseICodeCapacity(3);
        }
        array[top] = (byte) gotoOp;
        // Offset would written later
        iCodeTop = top + 1 + 2;
    }

    private void addVarOp(int op, int varIndex) {
        switch (op) {
            case Token.SETCONSTVAR:
                if (varIndex < 128) {
                    addIcode(Icode_SETCONSTVAR1);
                    addUint8(varIndex);
                    return;
                }
                addIndexOp(Icode_SETCONSTVAR, varIndex);
                return;
            case Token.GETVAR:
            case Token.SETVAR:
                if (varIndex < 128) {
                    addIcode(op == Token.GETVAR ? Icode_GETVAR1 : Icode_SETVAR1);
                    addUint8(varIndex);
                    return;
                }
            // fallthrough
            case Icode_VAR_INC_DEC:
                addIndexOp(op, varIndex);
                return;
        }
        throw Kit.codeBug();
    }

    private void addStringOp(int op, String str) {
        addStringPrefix(str);
        if (Icode.validIcode(op)) {
            addIcode(op);
        } else {
            addToken(op);
        }
    }

    private void addIndexOp(int op, int index) {
        addIndexPrefix(index);
        if (Icode.validIcode(op)) {
            addIcode(op);
        } else {
            addToken(op);
        }
    }

    private void addStringPrefix(String str) {
        int index = strings.getOrDefault(str, -1);
        if (index == -1) {
            index = strings.size();
            strings.put(str, index);
        }
        if (index < 4) {
            addIcode(Icode_REG_STR_C0 - index);
        } else if (index <= 0xFF) {
            addIcode(Icode_REG_STR1);
            addUint8(index);
        } else if (index <= 0xFFFF) {
            addIcode(Icode_REG_STR2);
            addUint16(index);
        } else {
            addIcode(Icode_REG_STR4);
            addInt(index);
        }
    }

    private void addBigInt(BigInteger n) {
        int index = bigInts.getOrDefault(n, -1);
        if (index == -1) {
            index = bigInts.size();
            bigInts.put(n, index);
        }
        if (index < 4) {
            addIcode(Icode_REG_BIGINT_C0 - index);
        } else if (index <= 0xFF) {
            addIcode(Icode_REG_BIGINT1);
            addUint8(index);
        } else if (index <= 0xFFFF) {
            addIcode(Icode_REG_BIGINT2);
            addUint16(index);
        } else {
            addIcode(Icode_REG_BIGINT4);
            addInt(index);
        }
        addToken(Token.BIGINT);
    }

    private void addIndexPrefix(int index) {
        if (index < 0) Kit.codeBug();
        if (index < 6) {
            addIcode(Icode_REG_IND_C0 - index);
        } else if (index <= 0xFF) {
            addIcode(Icode_REG_IND1);
            addUint8(index);
        } else if (index <= 0xFFFF) {
            addIcode(Icode_REG_IND2);
            addUint16(index);
        } else {
            addIcode(Icode_REG_IND4);
            addInt(index);
        }
    }

    private void addExceptionHandler(
            int icodeStart,
            int icodeEnd,
            int handlerStart,
            boolean isFinally,
            int exceptionObjectLocal,
            int scopeLocal) {
        int top = exceptionTableTop;
        int[] table = itsData.itsExceptionTable;
        if (table == null) {
            if (top != 0) Kit.codeBug();
            table = new int[Interpreter.EXCEPTION_SLOT_SIZE * 2];
            itsData.itsExceptionTable = table;
        } else if (table.length == top) {
            table = new int[table.length * 2];
            System.arraycopy(itsData.itsExceptionTable, 0, table, 0, top);
            itsData.itsExceptionTable = table;
        }
        table[top + Interpreter.EXCEPTION_TRY_START_SLOT] = icodeStart;
        table[top + Interpreter.EXCEPTION_TRY_END_SLOT] = icodeEnd;
        table[top + Interpreter.EXCEPTION_HANDLER_SLOT] = handlerStart;
        table[top + Interpreter.EXCEPTION_TYPE_SLOT] = isFinally ? 1 : 0;
        table[top + Interpreter.EXCEPTION_LOCAL_SLOT] = exceptionObjectLocal;
        table[top + Interpreter.EXCEPTION_SCOPE_SLOT] = scopeLocal;

        exceptionTableTop = top + Interpreter.EXCEPTION_SLOT_SIZE;
    }

    private byte[] increaseICodeCapacity(int extraSize) {
        int capacity = itsData.itsICode.length;
        int top = iCodeTop;
        if (top + extraSize <= capacity) throw Kit.codeBug();
        capacity *= 2;
        if (top + extraSize > capacity) {
            capacity = top + extraSize;
        }
        byte[] array = new byte[capacity];
        System.arraycopy(itsData.itsICode, 0, array, 0, top);
        itsData.itsICode = array;
        return array;
    }

    private void stackChange(int change) {
        if (change <= 0) {
            stackDepth += change;
        } else {
            int newDepth = stackDepth + change;
            if (newDepth > itsData.itsMaxStack) {
                itsData.itsMaxStack = newDepth;
            }
            stackDepth = newDepth;
        }
    }

    private int allocLocal() {
        int localSlot = localTop;
        ++localTop;
        if (localTop > itsData.itsMaxLocals) {
            itsData.itsMaxLocals = localTop;
        }
        return localSlot;
    }

    private void releaseLocal(int localSlot) {
        --localTop;
        if (localSlot != localTop) Kit.codeBug();
    }

    private static final class CompleteOptionalCallJump {
        private final int putArgsAndDoCallLabel;
        private final int afterLabel;

        public CompleteOptionalCallJump(int putArgsAndDoCallLabel, int afterLabel) {
            this.putArgsAndDoCallLabel = putArgsAndDoCallLabel;
            this.afterLabel = afterLabel;
        }
    }
}
