nashorn/src/jdk/nashorn/internal/codegen/Compiler.java
author jlaskey
Fri, 21 Dec 2012 16:36:24 -0400
changeset 16147 e63b63819133
child 16151 97c1e756ae1e
permissions -rw-r--r--
8005403: Open-source Nashorn Reviewed-by: attila, hannesw, lagergren, sundar Contributed-by: james.laskey@oracle.com, akhil.arora@oracle.com, andreas.woess@jku.at, attila.szegedi@oracle.com, hannes.wallnoefer@oracle.com, henry.jen@oracle.com, marcus.lagergren@oracle.com, pavel.semenov@oracle.com, pavel.stepanov@oracle.com, petr.hejl@oracle.com, petr.pisl@oracle.com, sundararajan.athijegannathan@oracle.com

/*
 * Copyright (c) 2010, 2012, 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.CONSTANTS;
import static jdk.nashorn.internal.codegen.CompilerConstants.DEFAULT_SCRIPT_NAME;
import static jdk.nashorn.internal.codegen.CompilerConstants.RUN_SCRIPT;
import static jdk.nashorn.internal.codegen.CompilerConstants.SCOPE;
import static jdk.nashorn.internal.codegen.CompilerConstants.SOURCE;
import static jdk.nashorn.internal.codegen.CompilerConstants.THIS;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;
import jdk.nashorn.internal.codegen.ClassEmitter.Flag;
import jdk.nashorn.internal.codegen.types.Type;
import jdk.nashorn.internal.ir.FunctionNode;
import jdk.nashorn.internal.ir.Node;
import jdk.nashorn.internal.ir.debug.ASTWriter;
import jdk.nashorn.internal.ir.debug.PrintVisitor;
import jdk.nashorn.internal.ir.visitor.NodeVisitor;
import jdk.nashorn.internal.parser.Parser;
import jdk.nashorn.internal.runtime.CodeInstaller;
import jdk.nashorn.internal.runtime.Context;
import jdk.nashorn.internal.runtime.DebugLogger;
import jdk.nashorn.internal.runtime.ECMAErrors;
import jdk.nashorn.internal.runtime.ErrorManager;
import jdk.nashorn.internal.runtime.Source;
import jdk.nashorn.internal.runtime.linker.Mangler;
import jdk.nashorn.internal.runtime.options.Options;

/**
 * Responsible for converting JavaScripts to java byte code. Main entry
 * point for code generator
 */
public final class Compiler {

    /** Compiler states available */
    public enum State {
        /** compiler is ready */
        INITIALIZED,
        /** method has been parsed */
        PARSED,
        /** method has been lowered */
        LOWERED,
        /** method has been emitted to bytecode */
        EMITTED
    }

    /** Current context */
    private final Context context;

    /** Currently compiled source */
    private final Source source;

    /** Current error manager */
    private final ErrorManager errors;

    /** Names uniqueName for this compile. */
    private final Namespace namespace;

    /** Current function node, or null if compiling from source until parsed */
    private FunctionNode functionNode;

    /** Current compiler state */
    private final EnumSet<State> state;

    /** 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";

    /** Name of the Global object, cannot be referred to as .class, @see CodeGenerator */
    public static final String GLOBAL_OBJECT = OBJECTS_PACKAGE + '/' + "Global";

    /** Name of the ScriptObjectImpl, cannot be referred to as .class @see FunctionObjectCreator */
    public static final String SCRIPTOBJECT_IMPL_OBJECT = OBJECTS_PACKAGE + '/' + "ScriptFunctionImpl";

    /** Name of the Trampoline, cannot be referred to as .class @see FunctionObjectCreator */
    public static final String TRAMPOLINE_OBJECT = OBJECTS_PACKAGE + '/' + "Trampoline";

    /** Compile unit (class) table. */
    private final Set<CompileUnit> compileUnits;

    /** All the "complex" constants used in the code. */
    private final ConstantData constantData;

    static final DebugLogger LOG = new DebugLogger("compiler");

    /** Script name */
    private String scriptName;

    /** Should we dump classes to disk and compile only? */
    private final boolean dumpClass;

    /** Code map class name -> byte code for all classes generated from this Source or FunctionNode */
    private Map<String, byte[]> code;

    /** Are we compiling in strict mode? */
    private boolean strict;

    /** Is this a lazy compilation - i.e. not from source, but jitting a previously parsed FunctionNode? */
    private boolean isLazy;

    /** Lazy jitting is disabled by default */
    private static final boolean LAZY_JIT = false;

    /**
     * Should we use integers for literals and all operations
     * that are based in constant and parameter assignment as
     * long as they can be proven not to overflow? With this enabled
     * var x = 17 would tag x as an integer, rather than a double,
     * but as soon as it is used in an operation that may potentially
     * overflow, such as an add, we conservatively widen it to double
     * (number type). This is because overflow checks in the code
     * are likely much more expensive that method specialization
     *
     * @return true if numbers should start as ints, false if they should
     *   start as doubles
     */
    static boolean shouldUseIntegers() {
        return USE_INTS;
    }

    private static final boolean USE_INTS;

    /**
     * Should we use integers for arithmetic operations as well?
     * TODO: We currently generate no overflow checks so this is
     * disabled
     *
     * @see #shouldUseIntegers()
     *
     * @return true if arithmetic operations should not widen integer
     *   operands by default.
     */
    static boolean shouldUseIntegerArithmetic() {
        return Compiler.shouldUseIntegers() && USE_INT_ARITH;
    }

    private static final boolean USE_INT_ARITH;

    static {
        USE_INTS       = !Options.getBooleanProperty("nashorn.compiler.ints.disable");
        USE_INT_ARITH  =  Options.getBooleanProperty("nashorn.compiler.intarithmetic");

        assert !USE_INT_ARITH : "Integer arithmetic is not enabled";
    }

    /**
     * Factory method for compiler that should compile from source to bytecode
     *
     * @param source  the source
     * @param context context
     *
     * @return compiler instance
     */
    public static Compiler compiler(final Source source, final Context context) {
        return Compiler.compiler(source, context, context.getErrors(), context._strict);
    }

    /**
     * Factory method to get a compiler that goes from from source to bytecode
     *
     * @param source  source code
     * @param context context
     * @param errors  error manager
     * @param strict  compilation in strict mode?
     *
     * @return compiler instance
     */
    public static Compiler compiler(final Source source, final Context context, final ErrorManager errors, final boolean strict) {
        return new Compiler(source, context, errors, strict);
    }

    /**
     * Factory method to get a compiler that goes from FunctionNode (parsed) to bytecode
     * Requires previous compiler for state
     *
     * @param compiler primordial compiler
     * @param functionNode functionNode to compile
     *
     * @return compiler
     */
    public static Compiler compiler(final Compiler compiler, final FunctionNode functionNode) {
        assert false : "lazy jit - not implemented";
        final Compiler newCompiler = new Compiler(compiler);
        newCompiler.state.add(State.PARSED);
        newCompiler.functionNode = functionNode;
        newCompiler.isLazy = true;
        return compiler;
    }

    private Compiler(final Compiler compiler) {
        this(compiler.source, compiler.context, compiler.errors, compiler.strict);
    }

    /**
     * Constructor
     *
     * @param source  the source to compile
     * @param context context
     * @param errors  error manager
     * @param strict  compile in strict mode
     */
    private Compiler(final Source source, final Context context, final ErrorManager errors, final boolean strict) {
        this.source       = source;
        this.context      = context;
        this.errors       = errors;
        this.strict       = strict;
        this.namespace    = new Namespace(context.getNamespace());
        this.compileUnits = new HashSet<>();
        this.constantData = new ConstantData();
        this.state        = EnumSet.of(State.INITIALIZED);
        this.dumpClass    = context._compile_only && context._dest_dir != null;
    }

    private String scriptsPackageName() {
        return dumpClass ? "" : (SCRIPTS_PACKAGE + '/');
    }

    private int nextCompileUnitIndex() {
        return compileUnits.size() + 1;
    }

    private String firstCompileUnitName() {
        return scriptsPackageName() + scriptName;
    }

    private String nextCompileUnitName() {
        return firstCompileUnitName() + '$' + nextCompileUnitIndex();
    }

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

    private CompileUnit addCompileUnit(final String unitClassName, final long initialWeight) {
        final CompileUnit compileUnit = initCompileUnit(unitClassName, initialWeight);
        compileUnits.add(compileUnit);
        LOG.info("Added compile unit " + compileUnit);
        return compileUnit;
    }

    private CompileUnit initCompileUnit(final String unitClassName, final long initialWeight) {
        final ClassEmitter classEmitter = new ClassEmitter(this, unitClassName, strict);
        final CompileUnit  compileUnit  = new CompileUnit(unitClassName, classEmitter, initialWeight);

        classEmitter.begin();

        final MethodEmitter initMethod = classEmitter.init(EnumSet.of(Flag.PRIVATE));
        initMethod.begin();
        initMethod.load(Type.OBJECT, 0);
        initMethod.newInstance(jdk.nashorn.internal.scripts.JS$.class);
        initMethod.returnVoid();
        initMethod.end();

        return compileUnit;
    }

    /**
     * Perform compilation
     *
     * @return true if successful, false otherwise - if false check the error manager
     */
    public boolean compile() {
        assert state.contains(State.INITIALIZED);

        /** do we need to parse source? */
        if (!state.contains(State.PARSED)) {
            assert this.functionNode == null;
            this.functionNode = new Parser(this, strict).parse(RUN_SCRIPT.tag());

            state.add(State.PARSED);
            debugPrintParse();

            if (errors.hasErrors() || context._parse_only) {
                return false;
            }

            assert !isLazy;
            //tag lazy nodes for later code generation and trampolines
            functionNode.accept(new NodeVisitor() {
                @Override
                public Node enter(final FunctionNode node) {
                    if (LAZY_JIT) {
                        node.setIsLazy(!node.isScript());
                    }
                    return node;
                }
            });
        } else {
            assert isLazy;
            functionNode.accept(new NodeVisitor() {
                @Override
                public Node enter(final FunctionNode node) {
                    node.setIsLazy(false);
                    return null; //TODO do we want to do this recursively? then return "node" instead
                }
            });
        }

        assert functionNode != null;
        final boolean oldStrict = strict;

        try {
            strict |= functionNode.isStrictMode();

            if (!state.contains(State.LOWERED)) {
                debugPrintAST();
                functionNode.accept(new Lower(this));
                state.add(State.LOWERED);

                if (errors.hasErrors()) {
                    return false;
                }
            }

            scriptName = computeNames();

            // Main script code always goes to this compile unit. Note that since we start this with zero weight
            // and add script code last this class may end up slightly larger than others, but reserving one class
            // just for the main script seems wasteful.
            final CompileUnit scriptCompileUnit = addCompileUnit(firstCompileUnitName(), 0l);
            new Splitter(this, functionNode, scriptCompileUnit).split();
            assert functionNode.getCompileUnit() == scriptCompileUnit;

            /** Compute compile units */
            assert strict == functionNode.isStrictMode() : "strict == " + strict + " but functionNode == " + functionNode.isStrictMode();
            if (functionNode.isStrictMode()) {
                strict = true;
            }

            try {
                final CodeGenerator codegen = new CodeGenerator(this);
                functionNode.accept(codegen);
                codegen.generateScopeCalls();
                debugPrintAST();
                debugPrintParse();
            } catch (final VerifyError e) {
                if (context._verify_code || context._print_code) {
                    context.getErr().println(e.getClass().getSimpleName() + ": " + e.getMessage());
                    if (context._dump_on_error) {
                        e.printStackTrace(context.getErr());
                    }
                } else {
                    throw e;
                }
            }

            state.add(State.EMITTED);

            code = new TreeMap<>();
            for (final CompileUnit compileUnit : compileUnits) {
                final ClassEmitter classEmitter = compileUnit.getClassEmitter();
                classEmitter.end();

                if (!errors.hasErrors()) {
                    final byte[] bytecode = classEmitter.toByteArray();
                    if (bytecode != null) {
                        code.put(compileUnit.getUnitClassName(), bytecode);
                        debugDisassemble();
                        debugVerify();
                    }
               }
            }

            if (code.isEmpty()) {
                return false;
            }

            try {
                dumpClassFiles();
            } catch (final IOException e) {
                throw new RuntimeException(e);
            }

            return true;
        } finally {
            strict = oldStrict;
        }
    }

    /**
     * Install compiled classes into a given loader
     * @param installer that takes the generated classes and puts them in the system
     * @return root script class - if there are several compile units they will also be installed
     */
    public Class<?> install(final CodeInstaller installer) {
        assert state.contains(State.EMITTED);
        assert scriptName != null;

        Class<?> rootClass = null;

        for (final Entry<String, byte[]> entry : code.entrySet()) {
            final String     className = entry.getKey();
            LOG.info("Installing class " + className);

            final byte[]     bytecode  = entry.getValue();
            final Class<?>   clazz     = installer.install(Compiler.binaryName(className), bytecode);

            if (rootClass == null && firstCompileUnitName().equals(className)) {
                rootClass = clazz;
            }

            try {
                //use reflection to write source and constants table to installed classes
                clazz.getField(SOURCE.tag()).set(null, source);
                clazz.getField(CONSTANTS.tag()).set(null, constantData.toArray());
            } catch (final NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) {
                throw new RuntimeException(e);
            }
        }

        LOG.info("Root class: " + rootClass);

        return rootClass;
    }

    /**
     * Find a unit that will hold a node of the specified weight.
     *
     * @param weight Weight of a node
     * @return Unit to hold node.
     */
    CompileUnit findUnit(final long weight) {
        for (final CompileUnit unit : compileUnits) {
            if (unit.canHold(weight)) {
                unit.addWeight(weight);
                return unit;
            }
        }

        return addCompileUnit(weight);
    }

    /**
     * Generate a uniqueName name. Public as {@link Parser} is using this to
     * create symbols in a different package
     *
     * @param  name to base unique name on
     * @return unique name
     */
    public String uniqueName(final String name) {
        return namespace.uniqueName(name);
    }

    /**
     * Internal function to compute reserved names and base names for class to
     * be generated
     *
     * @return scriptName
     */
    private String computeNames() {
        // Reserve internally used names.
        addReservedNames();

        if (dumpClass) {
            // get source file name and remove ".js"
            final String baseName = getSource().getName();
            final int    index    = baseName.lastIndexOf(".js");
            if (index != -1) {
                return baseName.substring(0, index);
            }
            return baseName;
        }

        return namespace.getParent().uniqueName(
            DEFAULT_SCRIPT_NAME.tag() +
            '$' +
            safeSourceName(source) +
            (isLazy ? CompilerConstants.LAZY.tag() : "")
        );
    }

    private static String safeSourceName(final Source source) {
        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('-', '_');
        final String mangled = Mangler.mangle(baseName);

        baseName = mangled != null ? mangled : baseName;
        return baseName;
    }

    static void verify(final Context context, final byte[] code) {
        context.verify(code);
    }

    /**
     * Fill in the namespace with internally reserved names.
     */
    private void addReservedNames() {
        namespace.uniqueName(SCOPE.tag());
        namespace.uniqueName(THIS.tag());
    }

    /**
     * Get the constant data for this Compiler
     *
     * @return the constant data
     */
    public ConstantData getConstantData() {
        return constantData;
    }

    /**
     * Get the Context used for Compilation
     * @see Context
     * @return the context
     */
    public Context getContext() {
        return context;
    }

    /**
     * Get the Source being compiled
     * @see Source
     * @return the source
     */
    public Source getSource() {
        return source;
    }

    /**
     * Get the error manager used for this compiler
     * @return the error manager
     */
    public ErrorManager getErrors() {
        return errors;
    }

    /**
     * Get the namespace used for this Compiler
     * @see Namespace
     * @return the namespace
     */
    public Namespace getNamespace() {
        return namespace;
    }

    /*
     * Debugging
     */

    /**
     * Print the AST before or after lowering, see --print-ast, --print-lower-ast
     */
    private void debugPrintAST() {
        assert functionNode != null;
        if (context._print_lower_ast && state.contains(State.LOWERED) ||
            context._print_ast && !state.contains(State.LOWERED)) {
            context.getErr().println(new ASTWriter(functionNode));
        }
    }

    /**
     * Print the parsed code before or after lowering, see --print-parse, --print-lower-parse
     */
    private boolean debugPrintParse() {
        if (errors.hasErrors()) {
            return false;
        }

        assert functionNode != null;

        if (context._print_lower_parse && state.contains(State.LOWERED) ||
             context._print_parse && !state.contains(State.LOWERED)) {
            final PrintVisitor pv = new PrintVisitor();
            functionNode.accept(pv);
            context.getErr().print(pv);
            context.getErr().flush();
        }

        return true;
    }

    private void debugDisassemble() {
        assert code != null;
        if (context._print_code) {
            for (final Map.Entry<String, byte[]> entry : code.entrySet()) {
                context.getErr().println("CLASS: " + entry.getKey());
                context.getErr().println();
                ClassEmitter.disassemble(context, entry.getValue());
                context.getErr().println("======");
            }
        }
    }

    private void debugVerify() {
        if (context._verify_code) {
            for (final Map.Entry<String, byte[]> entry : code.entrySet()) {
                Compiler.verify(context, entry.getValue());
            }
        }
    }

    /**
     * Implements the "-d" option - dump class files from script to specified output directory
     *
     * @throws IOException if classes cannot be written
     */
    private void dumpClassFiles() throws IOException {
        if (context._dest_dir == null) {
            return;
        }

        assert code != null;

        for (final Entry<String, byte[]> entry : code.entrySet()) {
            final String className = entry.getKey();
            final String fileName  = className.replace('.', File.separatorChar) + ".class";
            final int    index     = fileName.lastIndexOf(File.separatorChar);

            if (index != -1) {
                final File dir = new File(fileName.substring(0, index));
                if (!dir.exists() && !dir.mkdirs()) {
                    throw new IOException(ECMAErrors.getMessage("io.error.cant.write", dir.toString()));
                }
            }

            final byte[] bytecode = entry.getValue();
            final File outFile = new File(context._dest_dir, fileName);
            try (final FileOutputStream fos = new FileOutputStream(outFile)) {
                fos.write(bytecode);
            }
        }
    }

    /**
     * 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('/', '.');
    }

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


}