/*
 * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package jdk.nashorn.internal.codegen;

import static jdk.nashorn.internal.codegen.CompilerConstants.ARGUMENTS;
import static jdk.nashorn.internal.codegen.CompilerConstants.CALLEE;
import static jdk.nashorn.internal.codegen.CompilerConstants.RETURN;
import static jdk.nashorn.internal.codegen.CompilerConstants.SCOPE;
import static jdk.nashorn.internal.codegen.CompilerConstants.THIS;
import static jdk.nashorn.internal.codegen.CompilerConstants.VARARGS;
import static jdk.nashorn.internal.runtime.logging.DebugLogger.quote;

import java.io.File;
import java.lang.invoke.MethodType;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.logging.Level;
import jdk.internal.dynalink.support.NameCodec;
import jdk.nashorn.internal.codegen.types.Type;
import jdk.nashorn.internal.ir.Expression;
import jdk.nashorn.internal.ir.FunctionNode;
import jdk.nashorn.internal.ir.Optimistic;
import jdk.nashorn.internal.ir.debug.ClassHistogramElement;
import jdk.nashorn.internal.ir.debug.ObjectSizeCalculator;
import jdk.nashorn.internal.runtime.CodeInstaller;
import jdk.nashorn.internal.runtime.Context;
import jdk.nashorn.internal.runtime.ErrorManager;
import jdk.nashorn.internal.runtime.FunctionInitializer;
import jdk.nashorn.internal.runtime.ParserException;
import jdk.nashorn.internal.runtime.RecompilableScriptFunctionData;
import jdk.nashorn.internal.runtime.ScriptEnvironment;
import jdk.nashorn.internal.runtime.ScriptObject;
import jdk.nashorn.internal.runtime.ScriptRuntime;
import jdk.nashorn.internal.runtime.Source;
import jdk.nashorn.internal.runtime.logging.DebugLogger;
import jdk.nashorn.internal.runtime.logging.Loggable;
import jdk.nashorn.internal.runtime.logging.Logger;

/**
 * Responsible for converting JavaScripts to java byte code. Main entry
 * point for code generator. The compiler may also install classes given some
 * predefined Code installation policy, given to it at construction time.
 * @see CodeInstaller
 */
@Logger(name="compiler")
public final class Compiler implements Loggable {

    /** Name of the scripts package */
    public static final String SCRIPTS_PACKAGE = "jdk/nashorn/internal/scripts";

    /** Name of the objects package */
    public static final String OBJECTS_PACKAGE = "jdk/nashorn/internal/objects";

    private final ScriptEnvironment env;

    private final Source source;

    private final String sourceName;

    private final ErrorManager errors;

    private final boolean optimistic;

    private final Map<String, byte[]> bytecode;

    private final Set<CompileUnit> compileUnits;

    private final ConstantData constantData;

    private final CodeInstaller installer;

    /** logger for compiler, trampolines and related code generation events
     *  that affect classes */
    private final DebugLogger log;

    private final Context context;

    private final TypeMap types;

    // Runtime scope in effect at the time of the compilation. Used to evaluate types of expressions and prevent overly
    // optimistic assumptions (which will lead to unnecessary deoptimizing recompilations).
    private final TypeEvaluator typeEvaluator;

    private final boolean strict;

    private final boolean onDemand;

    /**
     * If this is a recompilation, this is how we pass in the invalidations, e.g. programPoint=17, Type == int means
     * that using whatever was at program point 17 as an int failed.
     */
    private final Map<Integer, Type> invalidatedProgramPoints;

    /**
     * Descriptor of the location where we write the type information after compilation.
     */
    private final Object typeInformationFile;

    /**
     * Compile unit name of first compile unit - this prefix will be used for all
     * classes that a compilation generates.
     */
    private final String firstCompileUnitName;

    /**
     * Contains the program point that should be used as the continuation entry point, as well as all previous
     * continuation entry points executed as part of a single logical invocation of the function. In practical terms, if
     * we execute a rest-of method from the program point 17, but then we hit deoptimization again during it at program
     * point 42, and execute a rest-of method from the program point 42, and then we hit deoptimization again at program
     * point 57 and are compiling a rest-of method for it, the values in the array will be [57, 42, 17]. This is only
     * set when compiling a rest-of method. If this method is a rest-of for a non-rest-of method, the array will have
     * one element. If it is a rest-of for a rest-of, the array will have two elements, and so on.
     */
    private final int[] continuationEntryPoints;

    /**
     * ScriptFunction data for what is being compile, where applicable.
     * TODO: make this immutable, propagate it through the CompilationPhases
     */
    private RecompilableScriptFunctionData compiledFunction;

    /**
     * Most compile unit names are longer than the default StringBuilder buffer,
     * worth startup performance when massive class generation is going on to increase
     * this
     */
    private static final int COMPILE_UNIT_NAME_BUFFER_SIZE = 32;

    /**
     * Compilation phases that a compilation goes through
     */
    public static class CompilationPhases implements Iterable<CompilationPhase> {

        /**
         * Singleton that describes compilation up to the phase where a function can be cached.
         */
        private final static CompilationPhases COMPILE_UPTO_CACHED = new CompilationPhases(
                "Common initial phases",
                CompilationPhase.CONSTANT_FOLDING_PHASE,
                CompilationPhase.LOWERING_PHASE,
                CompilationPhase.APPLY_SPECIALIZATION_PHASE,
                CompilationPhase.SPLITTING_PHASE,
                CompilationPhase.PROGRAM_POINT_PHASE,
                CompilationPhase.SYMBOL_ASSIGNMENT_PHASE,
                CompilationPhase.SCOPE_DEPTH_COMPUTATION_PHASE,
                CompilationPhase.CACHE_AST_PHASE
                );

        private final static CompilationPhases COMPILE_CACHED_UPTO_BYTECODE = new CompilationPhases(
                "After common phases, before bytecode generator",
                CompilationPhase.OPTIMISTIC_TYPE_ASSIGNMENT_PHASE,
                CompilationPhase.LOCAL_VARIABLE_TYPE_CALCULATION_PHASE
                );

        /**
         * Singleton that describes additional steps to be taken after retrieving a cached function, all the
         * way up to (but not including) generating and installing code.
         */
        public final static CompilationPhases RECOMPILE_CACHED_UPTO_BYTECODE = new CompilationPhases(
                "Recompile cached function up to bytecode",
                CompilationPhase.REINITIALIZE_CACHED,
                COMPILE_CACHED_UPTO_BYTECODE
                );

        /**
         * Singleton that describes back end of method generation, given that we have generated the normal
         * method up to CodeGenerator as in {@link CompilationPhases#COMPILE_UPTO_BYTECODE}
         */
        public final static CompilationPhases GENERATE_BYTECODE_AND_INSTALL = new CompilationPhases(
                "Generate bytecode and install",
                CompilationPhase.BYTECODE_GENERATION_PHASE,
                CompilationPhase.INSTALL_PHASE
                );

        /** Singleton that describes compilation up to the CodeGenerator, but not actually generating code */
        public final static CompilationPhases COMPILE_UPTO_BYTECODE = new CompilationPhases(
                "Compile upto bytecode",
                COMPILE_UPTO_CACHED,
                COMPILE_CACHED_UPTO_BYTECODE);

        /** Singleton that describes a standard eager compilation, but no installation, for example used by --compile-only */
        public final static CompilationPhases COMPILE_ALL_NO_INSTALL = new CompilationPhases(
                "Compile without install",
                COMPILE_UPTO_BYTECODE,
                CompilationPhase.BYTECODE_GENERATION_PHASE);

        /** Singleton that describes a standard eager compilation - this includes code installation */
        public final static CompilationPhases COMPILE_ALL = new CompilationPhases(
                "Full eager compilation",
                COMPILE_UPTO_BYTECODE,
                GENERATE_BYTECODE_AND_INSTALL);

        /** Singleton that describes a full compilation - this includes code installation - from serialized state*/
        public final static CompilationPhases COMPILE_ALL_CACHED = new CompilationPhases(
                "Eager compilation from serializaed state",
                RECOMPILE_CACHED_UPTO_BYTECODE,
                GENERATE_BYTECODE_AND_INSTALL);

        /**
         * Singleton that describes restOf method generation, given that we have generated the normal
         * method up to CodeGenerator as in {@link CompilationPhases#COMPILE_UPTO_BYTECODE}
         */
        public final static CompilationPhases GENERATE_BYTECODE_AND_INSTALL_RESTOF = new CompilationPhases(
                "Generate bytecode and install - RestOf method",
                CompilationPhase.REUSE_COMPILE_UNITS_PHASE,
                GENERATE_BYTECODE_AND_INSTALL);

        /** Compile all for a rest of method */
        public final static CompilationPhases COMPILE_ALL_RESTOF = new CompilationPhases(
                "Compile all, rest of",
                COMPILE_UPTO_BYTECODE,
                GENERATE_BYTECODE_AND_INSTALL_RESTOF);

        /** Compile from serialized for a rest of method */
        public final static CompilationPhases COMPILE_CACHED_RESTOF = new CompilationPhases(
                "Compile serialized, rest of",
                RECOMPILE_CACHED_UPTO_BYTECODE,
                GENERATE_BYTECODE_AND_INSTALL_RESTOF);

        private final List<CompilationPhase> phases;

        private final String desc;

        private CompilationPhases(final String desc, final CompilationPhase... phases) {
            this(desc, Arrays.asList(phases));
        }

        private CompilationPhases(final String desc, final CompilationPhases base, final CompilationPhase... phases) {
            this(desc, concat(base.phases, Arrays.asList(phases)));
        }

        private CompilationPhases(final String desc, final CompilationPhase first, final CompilationPhases rest) {
            this(desc, concat(Collections.singletonList(first), rest.phases));
        }

        private CompilationPhases(final String desc, final CompilationPhases base) {
            this(desc, base.phases);
        }

        private CompilationPhases(final String desc, final CompilationPhases... bases) {
            this(desc, concatPhases(bases));
        }

        private CompilationPhases(final String desc, final List<CompilationPhase> phases) {
            this.desc = desc;
            this.phases = phases;
        }

        private static List<CompilationPhase> concatPhases(final CompilationPhases[] bases) {
            final ArrayList<CompilationPhase> l = new ArrayList<>();
            for(final CompilationPhases base: bases) {
                l.addAll(base.phases);
            }
            l.trimToSize();
            return l;
        }

        private static <T> List<T> concat(final List<T> l1, final List<T> l2) {
            final ArrayList<T> l = new ArrayList<>(l1);
            l.addAll(l2);
            l.trimToSize();
            return l;
        }

        @Override
        public String toString() {
            return "'" + desc + "' " + phases.toString();
        }

        boolean contains(final CompilationPhase phase) {
            return phases.contains(phase);
        }

        @Override
        public Iterator<CompilationPhase> iterator() {
            return phases.iterator();
        }

        boolean isRestOfCompilation() {
            return this == COMPILE_ALL_RESTOF || this == GENERATE_BYTECODE_AND_INSTALL_RESTOF || this == COMPILE_CACHED_RESTOF;
        }

        String getDesc() {
            return desc;
        }

        String toString(final String prefix) {
            final StringBuilder sb = new StringBuilder();
            for (final CompilationPhase phase : phases) {
                sb.append(prefix).append(phase).append('\n');
            }
            return sb.toString();
        }
    }

    /**
     * This array contains names that need to be reserved at the start
     * of a compile, to avoid conflict with variable names later introduced.
     * See {@link CompilerConstants} for special names used for structures
     * during a compile.
     */
    private static String[] RESERVED_NAMES = {
        SCOPE.symbolName(),
        THIS.symbolName(),
        RETURN.symbolName(),
        CALLEE.symbolName(),
        VARARGS.symbolName(),
        ARGUMENTS.symbolName()
    };

    // per instance
    private final int compilationId = COMPILATION_ID.getAndIncrement();

    // per instance
    private final AtomicInteger nextCompileUnitId = new AtomicInteger(0);

    private static final AtomicInteger COMPILATION_ID = new AtomicInteger(0);

    /**
     * Creates a new compiler instance for initial compilation of a script.
     *
     * @param installer code installer
     * @param source    source to compile
     * @param errors    error manager
     * @param isStrict  is this a strict compilation
     * @return a new compiler
     */
    public static Compiler forInitialCompilation(
            final CodeInstaller installer,
            final Source source,
            final ErrorManager errors,
            final boolean isStrict) {
        return new Compiler(installer.getContext(), installer, source, errors, isStrict);
    }

    /**
     * Creates a compiler without a code installer. It can only be used to compile code, not install the
     * generated classes and as such it is useful only for implementation of {@code --compile-only} command
     * line option.
     * @param context  the current context
     * @param source   source to compile
     * @param isStrict is this a strict compilation
     * @return a new compiler
     */
    public static Compiler forNoInstallerCompilation(
            final Context context,
            final Source source,
            final boolean isStrict) {
        return new Compiler(context, null, source, context.getErrorManager(), isStrict);
    }

    /**
     * Creates a compiler for an on-demand compilation job.
     *
     * @param installer                code installer
     * @param source                   source to compile
     * @param isStrict                 is this a strict compilation
     * @param compiledFunction         compiled function, if any
     * @param types                    parameter and return value type information, if any is known
     * @param invalidatedProgramPoints invalidated program points for recompilation
     * @param typeInformationFile      descriptor of the location where type information is persisted
     * @param continuationEntryPoints  continuation entry points for restof method
     * @param runtimeScope             runtime scope for recompilation type lookup in {@code TypeEvaluator}
     * @return a new compiler
     */
    public static Compiler forOnDemandCompilation(
            final CodeInstaller installer,
            final Source source,
            final boolean isStrict,
            final RecompilableScriptFunctionData compiledFunction,
            final TypeMap types,
            final Map<Integer, Type> invalidatedProgramPoints,
            final Object typeInformationFile,
            final int[] continuationEntryPoints,
            final ScriptObject runtimeScope) {
        final Context context = installer.getContext();
        return new Compiler(context, installer, source, context.getErrorManager(), isStrict, true,
                compiledFunction, types, invalidatedProgramPoints, typeInformationFile,
                continuationEntryPoints, runtimeScope);
    }

    /**
     * Convenience constructor for non on-demand compiler instances.
     */
    private Compiler(
            final Context context,
            final CodeInstaller installer,
            final Source source,
            final ErrorManager errors,
            final boolean isStrict) {
        this(context, installer, source, errors, isStrict, false, null, null, null, null, null, null);
    }

    private Compiler(
            final Context context,
            final CodeInstaller installer,
            final Source source,
            final ErrorManager errors,
            final boolean isStrict,
            final boolean isOnDemand,
            final RecompilableScriptFunctionData compiledFunction,
            final TypeMap types,
            final Map<Integer, Type> invalidatedProgramPoints,
            final Object typeInformationFile,
            final int[] continuationEntryPoints,
            final ScriptObject runtimeScope) {
        this.context                  = context;
        this.env                      = context.getEnv();
        this.installer                = installer;
        this.constantData             = new ConstantData();
        this.compileUnits             = CompileUnit.createCompileUnitSet();
        this.bytecode                 = new LinkedHashMap<>();
        this.log                      = initLogger(context);
        this.source                   = source;
        this.errors                   = errors;
        this.sourceName               = FunctionNode.getSourceName(source);
        this.onDemand                 = isOnDemand;
        this.compiledFunction         = compiledFunction;
        this.types                    = types;
        this.invalidatedProgramPoints = invalidatedProgramPoints == null ? new HashMap<Integer, Type>() : invalidatedProgramPoints;
        this.typeInformationFile      = typeInformationFile;
        this.continuationEntryPoints  = continuationEntryPoints == null ? null: continuationEntryPoints.clone();
        this.typeEvaluator            = new TypeEvaluator(this, runtimeScope);
        this.firstCompileUnitName     = firstCompileUnitName();
        this.strict                   = isStrict;

        this.optimistic = env._optimistic_types;
    }

    private String safeSourceName() {
        String baseName = new File(source.getName()).getName();

        final int index = baseName.lastIndexOf(".js");
        if (index != -1) {
            baseName = baseName.substring(0, index);
        }

        baseName = baseName.replace('.', '_').replace('-', '_');
        if (!env._loader_per_compile) {
            baseName = baseName + installer.getUniqueScriptId();
        }

        // ASM's bytecode verifier does not allow JVM allowed safe escapes using '\' as escape char.
        // While ASM accepts such escapes for method names, field names, it enforces Java identifier
        // for class names. Workaround that ASM bug here by replacing JVM 'dangerous' chars with '_'
        // rather than safe encoding using '\'.
        final String mangled = env._verify_code? replaceDangerChars(baseName) : NameCodec.encode(baseName);
        return mangled != null ? mangled : baseName;
    }

    private static final String DANGEROUS_CHARS   = "\\/.;:$[]<>";
    private static String replaceDangerChars(final String name) {
        final int len = name.length();
        final StringBuilder buf = new StringBuilder();
        for (int i = 0; i < len; i++) {
            final char ch = name.charAt(i);
            if (DANGEROUS_CHARS.indexOf(ch) != -1) {
                buf.append('_');
            } else {
                buf.append(ch);
            }
        }
        return buf.toString();
    }

    private String firstCompileUnitName() {
        final StringBuilder sb = new StringBuilder(SCRIPTS_PACKAGE).
                append('/').
                append(CompilerConstants.DEFAULT_SCRIPT_NAME.symbolName()).
                append('$');

        if (isOnDemandCompilation()) {
            sb.append(RecompilableScriptFunctionData.RECOMPILATION_PREFIX);
        }

        if (compilationId > 0) {
            sb.append(compilationId).append('$');
        }

        if (types != null && compiledFunction.getFunctionNodeId() > 0) {
            sb.append(compiledFunction.getFunctionNodeId());
            final Type[] paramTypes = types.getParameterTypes(compiledFunction.getFunctionNodeId());
            for (final Type t : paramTypes) {
                sb.append(Type.getShortSignatureDescriptor(t));
            }
            sb.append('$');
        }

        sb.append(safeSourceName());

        return sb.toString();
    }

    void declareLocalSymbol(final String symbolName) {
        typeEvaluator.declareLocalSymbol(symbolName);
    }

    void setData(final RecompilableScriptFunctionData data) {
        assert this.compiledFunction == null : data;
        this.compiledFunction = data;
    }

    @Override
    public DebugLogger getLogger() {
        return log;
    }

    @Override
    public DebugLogger initLogger(final Context ctxt) {
        final boolean optimisticTypes = env._optimistic_types;
        final boolean lazyCompilation = env._lazy_compilation;

        return ctxt.getLogger(this.getClass(), new Consumer<DebugLogger>() {
            @Override
            public void accept(final DebugLogger newLogger) {
                if (!lazyCompilation) {
                    newLogger.warning("WARNING: Running with lazy compilation switched off. This is not a default setting.");
                }
                newLogger.warning("Optimistic types are ", optimisticTypes ? "ENABLED." : "DISABLED.");
            }
        });
    }

    ScriptEnvironment getScriptEnvironment() {
        return env;
    }

    boolean isOnDemandCompilation() {
        return onDemand;
    }

    boolean useOptimisticTypes() {
        return optimistic;
    }

    Context getContext() {
        return context;
    }

    Type getOptimisticType(final Optimistic node) {
        return typeEvaluator.getOptimisticType(node);
    }

    /**
     * Returns true if the expression can be safely evaluated, and its value is an object known to always use
     * String as the type of its property names retrieved through
     * {@link ScriptRuntime#toPropertyIterator(Object)}. It is used to avoid optimistic assumptions about its
     * property name types.
     * @param expr the expression to test
     * @return true if the expression can be safely evaluated, and its value is an object known to always use
     * String as the type of its property iterators.
     */
    boolean hasStringPropertyIterator(final Expression expr) {
        return typeEvaluator.hasStringPropertyIterator(expr);
    }

    void addInvalidatedProgramPoint(final int programPoint, final Type type) {
        invalidatedProgramPoints.put(programPoint, type);
    }


    /**
     * Returns a copy of this compiler's current mapping of invalidated optimistic program points to their types. The
     * copy is not live with regard to changes in state in this compiler instance, and is mutable.
     * @return a copy of this compiler's current mapping of invalidated optimistic program points to their types.
     */
    public Map<Integer, Type> getInvalidatedProgramPoints() {
        return invalidatedProgramPoints.isEmpty() ? null : new TreeMap<>(invalidatedProgramPoints);
    }

    TypeMap getTypeMap() {
        return types;
    }

    MethodType getCallSiteType(final FunctionNode fn) {
        if (types == null || !isOnDemandCompilation()) {
            return null;
        }
        return types.getCallSiteType(fn);
    }

    Type getParamType(final FunctionNode fn, final int pos) {
        return types == null ? null : types.get(fn, pos);
    }

    /**
     * Do a compilation job
     *
     * @param functionNode function node to compile
     * @param phases phases of compilation transforms to apply to function

     * @return transformed function
     *
     * @throws CompilationException if error occurs during compilation
     */
    public FunctionNode compile(final FunctionNode functionNode, final CompilationPhases phases) throws CompilationException {
        if (log.isEnabled()) {
            log.info(">> Starting compile job for ", DebugLogger.quote(functionNode.getName()), " phases=", quote(phases.getDesc()));
            log.indent();
        }

        final String name = DebugLogger.quote(functionNode.getName());

        FunctionNode newFunctionNode = functionNode;

        for (final String reservedName : RESERVED_NAMES) {
            newFunctionNode.uniqueName(reservedName);
        }

        final boolean info = log.isLoggable(Level.INFO);

        final DebugLogger timeLogger = env.isTimingEnabled() ? env._timing.getLogger() : null;

        long time = 0L;

        for (final CompilationPhase phase : phases) {
            log.fine(phase, " starting for ", name);

            try {
                newFunctionNode = phase.apply(this, phases, newFunctionNode);
            } catch (final ParserException error) {
                errors.error(error);
                if (env._dump_on_error) {
                    error.printStackTrace(env.getErr());
                }
                return null;
            }

            log.fine(phase, " done for function ", quote(name));

            if (env._print_mem_usage) {
                printMemoryUsage(functionNode, phase.toString());
            }

            time += (env.isTimingEnabled() ? phase.getEndTime() - phase.getStartTime() : 0L);
        }

        if (typeInformationFile != null && !phases.isRestOfCompilation()) {
            OptimisticTypesPersistence.store(typeInformationFile, invalidatedProgramPoints);
        }

        log.unindent();

        if (info) {
            final StringBuilder sb = new StringBuilder("<< Finished compile job for ");
            sb.append(newFunctionNode.getSource()).
            append(':').
            append(quote(newFunctionNode.getName()));

            if (time > 0L && timeLogger != null) {
                assert env.isTimingEnabled();
                sb.append(" in ").append(TimeUnit.NANOSECONDS.toMillis(time)).append(" ms");
            }
            log.info(sb);
        }

        return newFunctionNode;
    }

    Source getSource() {
        return source;
    }

    Map<String, byte[]> getBytecode() {
        return Collections.unmodifiableMap(bytecode);
    }

    /**
     * Reset bytecode cache for compiler reuse.
     */
    void clearBytecode() {
        bytecode.clear();
    }

    CompileUnit getFirstCompileUnit() {
        assert !compileUnits.isEmpty();
        return compileUnits.iterator().next();
    }

    Set<CompileUnit> getCompileUnits() {
        return compileUnits;
    }

    ConstantData getConstantData() {
        return constantData;
    }

    CodeInstaller getCodeInstaller() {
        return installer;
    }

    void addClass(final String name, final byte[] code) {
        bytecode.put(name, code);
    }

    String nextCompileUnitName() {
        final StringBuilder sb = new StringBuilder(COMPILE_UNIT_NAME_BUFFER_SIZE);
        sb.append(firstCompileUnitName);
        final int cuid = nextCompileUnitId.getAndIncrement();
        if (cuid > 0) {
            sb.append("$cu").append(cuid);
        }

        return sb.toString();
    }

    /**
     * Persist current compilation with the given {@code cacheKey}.
     * @param cacheKey cache key
     * @param functionNode function node
     */
    public void persistClassInfo(final String cacheKey, final FunctionNode functionNode) {
        if (cacheKey != null && env._persistent_cache) {
            // If this is an on-demand compilation create a function initializer for the function being compiled.
            // Otherwise use function initializer map generated by codegen.
            final Map<Integer, FunctionInitializer> initializers = new HashMap<>();
            if (isOnDemandCompilation()) {
                initializers.put(functionNode.getId(), new FunctionInitializer(functionNode, getInvalidatedProgramPoints()));
            } else {
                for (final CompileUnit compileUnit : getCompileUnits()) {
                    for (final FunctionNode fn : compileUnit.getFunctionNodes()) {
                        initializers.put(fn.getId(), new FunctionInitializer(fn));
                    }
                }
            }
            final String mainClassName = getFirstCompileUnit().getUnitClassName();
            installer.storeScript(cacheKey, source, mainClassName, bytecode, initializers, constantData.toArray(), compilationId);
        }
    }

    /**
     * Make sure the next compilation id is greater than {@code value}.
     * @param value compilation id value
     */
    public static void updateCompilationId(final int value) {
        if (value >= COMPILATION_ID.get()) {
            COMPILATION_ID.set(value + 1);
        }
    }

    CompileUnit addCompileUnit(final long initialWeight) {
        final CompileUnit compileUnit = createCompileUnit(initialWeight);
        compileUnits.add(compileUnit);
        log.fine("Added compile unit ", compileUnit);
        return compileUnit;
    }

    CompileUnit createCompileUnit(final String unitClassName, final long initialWeight) {
        final ClassEmitter classEmitter = new ClassEmitter(context, sourceName, unitClassName, isStrict());
        final CompileUnit  compileUnit  = new CompileUnit(unitClassName, classEmitter, initialWeight);
        classEmitter.begin();

        return compileUnit;
    }

    private CompileUnit createCompileUnit(final long initialWeight) {
        return createCompileUnit(nextCompileUnitName(), initialWeight);
    }

    boolean isStrict() {
        return strict;
    }

    void replaceCompileUnits(final Set<CompileUnit> newUnits) {
        compileUnits.clear();
        compileUnits.addAll(newUnits);
    }

    CompileUnit findUnit(final long weight) {
        for (final CompileUnit unit : compileUnits) {
            if (unit.canHold(weight)) {
                unit.addWeight(weight);
                return unit;
            }
        }

        return addCompileUnit(weight);
    }

    /**
     * Convert a package/class name to a binary name.
     *
     * @param name Package/class name.
     * @return Binary name.
     */
    public static String binaryName(final String name) {
        return name.replace('/', '.');
    }

    RecompilableScriptFunctionData getScriptFunctionData(final int functionId) {
        assert compiledFunction != null;
        final RecompilableScriptFunctionData fn = compiledFunction.getScriptFunctionData(functionId);
        assert fn != null : functionId;
        return fn;
    }

    boolean isGlobalSymbol(final FunctionNode fn, final String name) {
        return getScriptFunctionData(fn.getId()).isGlobalSymbol(fn, name);
    }

    int[] getContinuationEntryPoints() {
        return continuationEntryPoints;
    }

    Type getInvalidatedProgramPointType(final int programPoint) {
        return invalidatedProgramPoints.get(programPoint);
    }

    private void printMemoryUsage(final FunctionNode functionNode, final String phaseName) {
        if (!log.isEnabled()) {
            return;
        }

        log.info(phaseName, "finished. Doing IR size calculation...");

        final ObjectSizeCalculator osc = new ObjectSizeCalculator(ObjectSizeCalculator.getEffectiveMemoryLayoutSpecification());
        osc.calculateObjectSize(functionNode);

        final List<ClassHistogramElement> list      = osc.getClassHistogram();
        final StringBuilder               sb        = new StringBuilder();
        final long                        totalSize = osc.calculateObjectSize(functionNode);

        sb.append(phaseName).
        append(" Total size = ").
        append(totalSize / 1024 / 1024).
        append("MB");
        log.info(sb);

        Collections.sort(list, new Comparator<ClassHistogramElement>() {
            @Override
            public int compare(final ClassHistogramElement o1, final ClassHistogramElement o2) {
                final long diff = o1.getBytes() - o2.getBytes();
                if (diff < 0) {
                    return 1;
                } else if (diff > 0) {
                    return -1;
                } else {
                    return 0;
                }
            }
        });
        for (final ClassHistogramElement e : list) {
            final String line = String.format("    %-48s %10d bytes (%8d instances)", e.getClazz(), e.getBytes(), e.getInstances());
            log.info(line);
            if (e.getBytes() < totalSize / 200) {
                log.info("    ...");
                break; // never mind, so little memory anyway
            }
        }
    }
}
