diff options
Diffstat (limited to 'src/jdk/nashorn/internal/runtime/RecompilableScriptFunctionData.java')
-rw-r--r-- | src/jdk/nashorn/internal/runtime/RecompilableScriptFunctionData.java | 983 |
1 files changed, 676 insertions, 307 deletions
diff --git a/src/jdk/nashorn/internal/runtime/RecompilableScriptFunctionData.java b/src/jdk/nashorn/internal/runtime/RecompilableScriptFunctionData.java index 90885444..06414edb 100644 --- a/src/jdk/nashorn/internal/runtime/RecompilableScriptFunctionData.java +++ b/src/jdk/nashorn/internal/runtime/RecompilableScriptFunctionData.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2014, 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 @@ -26,106 +26,233 @@ package jdk.nashorn.internal.runtime; import static jdk.nashorn.internal.lookup.Lookup.MH; - -import java.io.Serializable; +import java.io.IOException; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.LinkedList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; import java.util.Map; +import java.util.Set; +import java.util.TreeMap; import jdk.internal.dynalink.support.NameCodec; import jdk.nashorn.internal.codegen.Compiler; +import jdk.nashorn.internal.codegen.Compiler.CompilationPhases; import jdk.nashorn.internal.codegen.CompilerConstants; import jdk.nashorn.internal.codegen.FunctionSignature; +import jdk.nashorn.internal.codegen.Namespace; +import jdk.nashorn.internal.codegen.ObjectClassGenerator.AllocatorDescriptor; +import jdk.nashorn.internal.codegen.OptimisticTypesPersistence; +import jdk.nashorn.internal.codegen.TypeMap; import jdk.nashorn.internal.codegen.types.Type; import jdk.nashorn.internal.ir.FunctionNode; -import jdk.nashorn.internal.ir.FunctionNode.CompilationState; +import jdk.nashorn.internal.ir.LexicalContext; +import jdk.nashorn.internal.ir.visitor.NodeVisitor; +import jdk.nashorn.internal.objects.Global; +import jdk.nashorn.internal.parser.Parser; import jdk.nashorn.internal.parser.Token; import jdk.nashorn.internal.parser.TokenType; -import jdk.nashorn.internal.scripts.JS; - +import jdk.nashorn.internal.runtime.logging.DebugLogger; +import jdk.nashorn.internal.runtime.logging.Loggable; +import jdk.nashorn.internal.runtime.logging.Logger; /** * This is a subclass that represents a script function that may be regenerated, * for example with specialization based on call site types, or lazily generated. * The common denominator is that it can get new invokers during its lifespan, * unlike {@code FinalScriptFunctionData} */ -public final class RecompilableScriptFunctionData extends ScriptFunctionData implements Serializable { +@Logger(name="recompile") +public final class RecompilableScriptFunctionData extends ScriptFunctionData implements Loggable { + /** Prefix used for all recompiled script classes */ + public static final String RECOMPILATION_PREFIX = "Recompilation$"; - /** FunctionNode with the code for this ScriptFunction */ - private transient FunctionNode functionNode; + /** Unique function node id for this function node */ + private final int functionNodeId; - /** Source from which FunctionNode was parsed. */ - private transient Source source; + private final String functionName; /** The line number where this function begins. */ private final int lineNumber; - /** Allows us to retrieve the method handle for this function once the code is compiled */ - private MethodLocator methodLocator; + /** Source from which FunctionNode was parsed. */ + private transient Source source; + + /** Serialized, compressed form of the AST. Used by split functions as they can't be reparsed from source. */ + private final byte[] serializedAst; /** Token of this function within the source. */ private final long token; - /** Allocator map from makeMap() */ - private final PropertyMap allocatorMap; + /** + * Represents the allocation strategy (property map, script object class, and method handle) for when + * this function is used as a constructor. Note that majority of functions (those not setting any this.* + * properties) will share a single canonical "default strategy" instance. + */ + private final AllocationStrategy allocationStrategy; + + /** + * Opaque object representing parser state at the end of the function. Used when reparsing outer function + * to help with skipping parsing inner functions. + */ + private final Object endParserState; /** Code installer used for all further recompilation/specialization of this ScriptFunction */ private transient CodeInstaller<ScriptEnvironment> installer; - /** Name of class where allocator function resides */ - private final String allocatorClassName; + private final Map<Integer, RecompilableScriptFunctionData> nestedFunctions; - /** lazily generated allocator */ - private transient MethodHandle allocator; + /** Id to parent function if one exists */ + private RecompilableScriptFunctionData parent; + + /** Copy of the {@link FunctionNode} flags. */ + private final int functionFlags; private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup(); - /** - * Used for specialization based on runtime arguments. Whenever we specialize on - * callsite parameter types at runtime, we need to use a parameter type guard to - * ensure that the specialized version of the script function continues to be - * applicable for a particular callsite. - */ - private static final MethodHandle PARAM_TYPE_GUARD = findOwnMH("paramTypeGuard", boolean.class, Type[].class, Object[].class); + private transient DebugLogger log; - /** - * It is usually a good gamble whever we detect a runtime callsite with a double - * (or java.lang.Number instance) to specialize the parameter to an integer, if the - * parameter in question can be represented as one. The double typically only exists - * because the compiler doesn't know any better than "a number type" and conservatively - * picks doubles when it can't prove that an integer addition wouldn't overflow. - */ - private static final MethodHandle ENSURE_INT = findOwnMH("ensureInt", int.class, Object.class); + private final Map<String, Integer> externalScopeDepths; + + private final Set<String> internalSymbols; + + private static final int GET_SET_PREFIX_LENGTH = "*et ".length(); private static final long serialVersionUID = 4914839316174633726L; /** * Constructor - public as scripts use it * - * @param functionNode functionNode that represents this function code - * @param installer installer for code regeneration versions of this function - * @param allocatorClassName name of our allocator class, will be looked up dynamically if used as a constructor - * @param allocatorMap allocator map to seed instances with, when constructing + * @param functionNode functionNode that represents this function code + * @param installer installer for code regeneration versions of this function + * @param allocationDescriptor descriptor for the allocation behavior when this function is used as a constructor + * @param nestedFunctions nested function map + * @param externalScopeDepths external scope depths + * @param internalSymbols internal symbols to method, defined in its scope + * @param serializedAst a serialized AST representation. Normally only used for split functions. */ - public RecompilableScriptFunctionData(final FunctionNode functionNode, final CodeInstaller<ScriptEnvironment> installer, final String allocatorClassName, final PropertyMap allocatorMap) { + public RecompilableScriptFunctionData( + final FunctionNode functionNode, + final CodeInstaller<ScriptEnvironment> installer, + final AllocatorDescriptor allocationDescriptor, + final Map<Integer, RecompilableScriptFunctionData> nestedFunctions, + final Map<String, Integer> externalScopeDepths, + final Set<String> internalSymbols, + final byte[] serializedAst) { + super(functionName(functionNode), - functionNode.getParameters().size(), - getFlags(functionNode)); - this.functionNode = functionNode; - this.source = functionNode.getSource(); - this.lineNumber = functionNode.getLineNumber(); - this.token = tokenFor(functionNode); - this.installer = installer; - this.allocatorClassName = allocatorClassName; - this.allocatorMap = allocatorMap; - if (!functionNode.isLazy()) { - methodLocator = new MethodLocator(functionNode); + Math.min(functionNode.getParameters().size(), MAX_ARITY), + getDataFlags(functionNode)); + + this.functionName = functionNode.getName(); + this.lineNumber = functionNode.getLineNumber(); + this.functionFlags = functionNode.getFlags() | (functionNode.needsCallee() ? FunctionNode.NEEDS_CALLEE : 0); + this.functionNodeId = functionNode.getId(); + this.source = functionNode.getSource(); + this.endParserState = functionNode.getEndParserState(); + this.token = tokenFor(functionNode); + this.installer = installer; + this.allocationStrategy = AllocationStrategy.get(allocationDescriptor); + this.nestedFunctions = smallMap(nestedFunctions); + this.externalScopeDepths = smallMap(externalScopeDepths); + this.internalSymbols = smallSet(new HashSet<>(internalSymbols)); + + for (final RecompilableScriptFunctionData nfn : nestedFunctions.values()) { + assert nfn.getParent() == null; + nfn.setParent(this); + } + + this.serializedAst = serializedAst; + createLogger(); + } + + private static <K, V> Map<K, V> smallMap(final Map<K, V> map) { + if (map == null || map.isEmpty()) { + return Collections.emptyMap(); + } else if (map.size() == 1) { + final Map.Entry<K, V> entry = map.entrySet().iterator().next(); + return Collections.singletonMap(entry.getKey(), entry.getValue()); + } else { + return map; } } + private static <T> Set<T> smallSet(final Set<T> set) { + if (set == null || set.isEmpty()) { + return Collections.emptySet(); + } else if (set.size() == 1) { + return Collections.singleton(set.iterator().next()); + } else { + return set; + } + } + + @Override + public DebugLogger getLogger() { + return log; + } + + @Override + public DebugLogger initLogger(final Context ctxt) { + return ctxt.getLogger(this.getClass()); + } + + /** + * Check if a symbol is internally defined in a function. For example + * if "undefined" is internally defined in the outermost program function, + * it has not been reassigned or overridden and can be optimized + * + * @param symbolName symbol name + * @return true if symbol is internal to this ScriptFunction + */ + + public boolean hasInternalSymbol(final String symbolName) { + return internalSymbols.contains(symbolName); + } + + /** + * Return the external symbol table + * @param symbolName symbol name + * @return the external symbol table with proto depths + */ + public int getExternalSymbolDepth(final String symbolName) { + final Integer depth = externalScopeDepths.get(symbolName); + return depth == null ? -1 : depth; + } + + /** + * Returns the names of all external symbols this function uses. + * @return the names of all external symbols this function uses. + */ + public Set<String> getExternalSymbolNames() { + return Collections.unmodifiableSet(externalScopeDepths.keySet()); + } + + /** + * Returns the opaque object representing the parser state at the end of this function's body, used to + * skip parsing this function when reparsing its containing outer function. + * @return the object representing the end parser state + */ + public Object getEndParserState() { + return endParserState; + } + + /** + * Get the parent of this RecompilableScriptFunctionData. If we are + * a nested function, we have a parent. Note that "null" return value + * can also mean that we have a parent but it is unknown, so this can + * only be used for conservative assumptions. + * @return parent data, or null if non exists and also null IF UNKNOWN. + */ + public RecompilableScriptFunctionData getParent() { + return parent; + } + + void setParent(final RecompilableScriptFunctionData parent) { + this.parent = parent; + } + @Override String toSource() { if (source != null && token != 0) { @@ -135,46 +262,75 @@ public final class RecompilableScriptFunctionData extends ScriptFunctionData imp return "function " + (name == null ? "" : name) + "() { [native code] }"; } - public void setCodeAndSource(final Map<String, Class<?>> code, final Source source) { - this.source = source; - if (methodLocator != null) { - methodLocator.setClass(code.get(methodLocator.getClassName())); + /** + * Initialize transient fields on deserialized instances + * + * @param src source + * @param inst code installer + */ + public void initTransients(final Source src, final CodeInstaller<ScriptEnvironment> inst) { + if (this.source == null && this.installer == null) { + this.source = src; + this.installer = inst; + } else if (this.source != src || !this.installer.isCompatibleWith(inst)) { + // Existing values must be same as those passed as parameters + throw new IllegalArgumentException(); } } @Override public String toString() { + return super.toString() + '@' + functionNodeId; + } + + @Override + public String toStringVerbose() { final StringBuilder sb = new StringBuilder(); + sb.append("fnId=").append(functionNodeId).append(' '); + if (source != null) { - sb.append(source.getName()).append(':').append(lineNumber).append(' '); + sb.append(source.getName()) + .append(':') + .append(lineNumber) + .append(' '); } return sb.toString() + super.toString(); } + @Override + public String getFunctionName() { + return functionName; + } + + @Override + public boolean inDynamicContext() { + return getFunctionFlag(FunctionNode.IN_DYNAMIC_CONTEXT); + } + private static String functionName(final FunctionNode fn) { if (fn.isAnonymous()) { return ""; - } else { - final FunctionNode.Kind kind = fn.getKind(); - if (kind == FunctionNode.Kind.GETTER || kind == FunctionNode.Kind.SETTER) { - final String name = NameCodec.decode(fn.getIdent().getName()); - return name.substring(4); // 4 is "get " or "set " - } else { - return fn.getIdent().getName(); - } } + final FunctionNode.Kind kind = fn.getKind(); + if (kind == FunctionNode.Kind.GETTER || kind == FunctionNode.Kind.SETTER) { + final String name = NameCodec.decode(fn.getIdent().getName()); + return name.substring(GET_SET_PREFIX_LENGTH); + } + return fn.getIdent().getName(); } private static long tokenFor(final FunctionNode fn) { - final int position = Token.descPosition(fn.getFirstToken()); - final int length = Token.descPosition(fn.getLastToken()) - position + Token.descLength(fn.getLastToken()); + final int position = Token.descPosition(fn.getFirstToken()); + final long lastToken = Token.withDelimiter(fn.getLastToken()); + // EOL uses length field to store the line number + final int length = Token.descPosition(lastToken) - position + (Token.descType(lastToken) == TokenType.EOL ? 0 : Token.descLength(lastToken)); return Token.toDesc(TokenType.FUNCTION, position, length); } - private static int getFlags(final FunctionNode functionNode) { + private static int getDataFlags(final FunctionNode functionNode) { int flags = IS_CONSTRUCTOR; if (functionNode.isStrict()) { flags |= IS_STRICT; @@ -185,327 +341,540 @@ public final class RecompilableScriptFunctionData extends ScriptFunctionData imp if (functionNode.usesThis() || functionNode.hasEval()) { flags |= USES_THIS; } + if (functionNode.isVarArg()) { + flags |= IS_VARIABLE_ARITY; + } return flags; } @Override + PropertyMap getAllocatorMap() { + return allocationStrategy.getAllocatorMap(); + } + + @Override ScriptObject allocate(final PropertyMap map) { - try { - ensureHasAllocator(); //if allocatorClass name is set to null (e.g. for bound functions) we don't even try - return allocator == null ? null : (ScriptObject)allocator.invokeExact(map); - } catch (final RuntimeException | Error e) { - throw e; - } catch (final Throwable t) { - throw new RuntimeException(t); - } + return allocationStrategy.allocate(map); } - private void ensureHasAllocator() throws ClassNotFoundException { - if (allocator == null && allocatorClassName != null) { - this.allocator = MH.findStatic(LOOKUP, Context.forStructureClass(allocatorClassName), CompilerConstants.ALLOCATE.symbolName(), MH.type(ScriptObject.class, PropertyMap.class)); + boolean isSerialized() { + return serializedAst != null; + } + + FunctionNode reparse() { + if (isSerialized()) { + return deserialize(); + } + + final int descPosition = Token.descPosition(token); + final Context context = Context.getContextTrusted(); + final Parser parser = new Parser( + context.getEnv(), + source, + new Context.ThrowErrorManager(), + isStrict(), + // source starts at line 0, so even though lineNumber is the correct declaration line, back off + // one to make it exclusive + lineNumber - 1, + context.getLogger(Parser.class)); + + if (getFunctionFlag(FunctionNode.IS_ANONYMOUS)) { + parser.setFunctionName(functionName); + } + parser.setReparsedFunction(this); + + final FunctionNode program = parser.parse(CompilerConstants.PROGRAM.symbolName(), descPosition, + Token.descLength(token), true); + // Parser generates a program AST even if we're recompiling a single function, so when we are only + // recompiling a single function, extract it from the program. + return (isProgram() ? program : extractFunctionFromScript(program)).setName(null, functionName); + } + + private FunctionNode deserialize() { + final ScriptEnvironment env = installer.getOwner(); + final Timing timing = env._timing; + final long t1 = System.nanoTime(); + try { + return AstDeserializer.deserialize(serializedAst).initializeDeserialized(source, new Namespace(env.getNamespace())); + } finally { + timing.accumulateTime("'Deserialize'", System.nanoTime() - t1); } } - @Override - PropertyMap getAllocatorMap() { - return allocatorMap; + private boolean getFunctionFlag(final int flag) { + return (functionFlags & flag) != 0; } + private boolean isProgram() { + return getFunctionFlag(FunctionNode.IS_PROGRAM); + } - @Override - protected void ensureCompiled() { - if (functionNode != null && functionNode.isLazy()) { - Compiler.LOG.info("Trampoline hit: need to do lazy compilation of '", functionNode.getName(), "'"); - final Compiler compiler = new Compiler(installer); - functionNode = compiler.compile(functionNode); - assert !functionNode.isLazy(); - compiler.install(functionNode); - methodLocator = new MethodLocator(functionNode); - flags = getFlags(functionNode); + TypeMap typeMap(final MethodType fnCallSiteType) { + if (fnCallSiteType == null) { + return null; } - if (functionNode != null) { - methodLocator.setClass(functionNode.getCompileUnit().getCode()); + if (CompiledFunction.isVarArgsType(fnCallSiteType)) { + return null; } + + return new TypeMap(functionNodeId, explicitParams(fnCallSiteType), needsCallee()); } - @Override - protected synchronized void ensureCodeGenerated() { - if (!code.isEmpty()) { - return; // nothing to do, we have code, at least some. + private static ScriptObject newLocals(final ScriptObject runtimeScope) { + final ScriptObject locals = Global.newEmptyInstance(); + locals.setProto(runtimeScope); + return locals; + } + + private Compiler getCompiler(final FunctionNode fn, final MethodType actualCallSiteType, final ScriptObject runtimeScope) { + return getCompiler(fn, actualCallSiteType, newLocals(runtimeScope), null, null); + } + + /** + * Returns a code installer for installing new code. If we're using either optimistic typing or loader-per-compile, + * then asks for a code installer with a new class loader; otherwise just uses the current installer. We use + * a new class loader with optimistic typing so that deoptimized code can get reclaimed by GC. + * @return a code installer for installing new code. + */ + private CodeInstaller<ScriptEnvironment> getInstallerForNewCode() { + final ScriptEnvironment env = installer.getOwner(); + return env._optimistic_types || env._loader_per_compile ? installer.withNewLoader() : installer; + } + + Compiler getCompiler(final FunctionNode functionNode, final MethodType actualCallSiteType, + final ScriptObject runtimeScope, final Map<Integer, Type> invalidatedProgramPoints, + final int[] continuationEntryPoints) { + final TypeMap typeMap = typeMap(actualCallSiteType); + final Type[] paramTypes = typeMap == null ? null : typeMap.getParameterTypes(functionNodeId); + final Object typeInformationFile = OptimisticTypesPersistence.getLocationDescriptor(source, functionNodeId, paramTypes); + final Context context = Context.getContextTrusted(); + return new Compiler( + context, + context.getEnv(), + getInstallerForNewCode(), + functionNode.getSource(), // source + context.getErrorManager(), + isStrict() | functionNode.isStrict(), // is strict + true, // is on demand + this, // compiledFunction, i.e. this RecompilableScriptFunctionData + typeMap, // type map + getEffectiveInvalidatedProgramPoints(invalidatedProgramPoints, typeInformationFile), // invalidated program points + typeInformationFile, + continuationEntryPoints, // continuation entry points + runtimeScope); // runtime scope + } + + /** + * If the function being compiled already has its own invalidated program points map, use it. Otherwise, attempt to + * load invalidated program points map from the persistent type info cache. + * @param invalidatedProgramPoints the function's current invalidated program points map. Null if the function + * doesn't have it. + * @param typeInformationFile the object describing the location of the persisted type information. + * @return either the existing map, or a loaded map from the persistent type info cache, or a new empty map if + * neither an existing map or a persistent cached type info is available. + */ + @SuppressWarnings("unused") + private static Map<Integer, Type> getEffectiveInvalidatedProgramPoints( + final Map<Integer, Type> invalidatedProgramPoints, final Object typeInformationFile) { + if(invalidatedProgramPoints != null) { + return invalidatedProgramPoints; } + final Map<Integer, Type> loadedProgramPoints = OptimisticTypesPersistence.load(typeInformationFile); + return loadedProgramPoints != null ? loadedProgramPoints : new TreeMap<Integer, Type>(); + } + + private FunctionInitializer compileTypeSpecialization(final MethodType actualCallSiteType, final ScriptObject runtimeScope, final boolean persist) { + // We're creating an empty script object for holding local variables. AssignSymbols will populate it with + // explicit Undefined values for undefined local variables (see AssignSymbols#defineSymbol() and + // CompilationEnvironment#declareLocalSymbol()). - ensureCompiled(); + if (log.isEnabled()) { + log.info("Parameter type specialization of '", functionName, "' signature: ", actualCallSiteType); + } - /* - * We can't get to this program point unless we have bytecode, either from - * eager compilation or from running a lazy compile on the lines above - */ + final boolean persistentCache = usePersistentCodeCache() && persist; + String cacheKey = null; + if (persistentCache) { + final TypeMap typeMap = typeMap(actualCallSiteType); + final Type[] paramTypes = typeMap == null ? null : typeMap.getParameterTypes(functionNodeId); + cacheKey = CodeStore.getCacheKey(functionNodeId, paramTypes); + final CodeInstaller<ScriptEnvironment> newInstaller = getInstallerForNewCode(); + final StoredScript script = newInstaller.loadScript(source, cacheKey); - assert functionNode == null || functionNode.hasState(CompilationState.EMITTED) : - functionNode.getName() + " " + functionNode.getState() + " " + Debug.id(functionNode); + if (script != null) { + Compiler.updateCompilationId(script.getCompilationId()); + return installStoredScript(script, newInstaller); + } + } - // code exists - look it up and add it into the automatically sorted invoker list - addCode(functionNode); + final FunctionNode fn = reparse(); + final Compiler compiler = getCompiler(fn, actualCallSiteType, runtimeScope); + final FunctionNode compiledFn = compiler.compile(fn, + isSerialized() ? CompilationPhases.COMPILE_ALL_SERIALIZED : CompilationPhases.COMPILE_ALL); - if (functionNode != null && !functionNode.canSpecialize()) { - // allow GC to claim IR stuff that is not needed anymore - functionNode = null; - installer = null; + if (persist && !compiledFn.getFlag(FunctionNode.HAS_APPLY_TO_CALL_SPECIALIZATION)) { + compiler.persistClassInfo(cacheKey, compiledFn); } + return new FunctionInitializer(compiledFn, compiler.getInvalidatedProgramPoints()); } - private MethodHandle addCode(final FunctionNode fn) { - return addCode(fn, null, null, null); - } + private static Map<String, Class<?>> installStoredScriptClasses(final StoredScript script, final CodeInstaller<ScriptEnvironment> installer) { + final Map<String, Class<?>> installedClasses = new HashMap<>(); + final Map<String, byte[]> classBytes = script.getClassBytes(); + final String mainClassName = script.getMainClassName(); + final byte[] mainClassBytes = classBytes.get(mainClassName); + + final Class<?> mainClass = installer.install(mainClassName, mainClassBytes); + + installedClasses.put(mainClassName, mainClass); - private MethodHandle addCode(final FunctionNode fn, final MethodType runtimeType, final MethodHandle guard, final MethodHandle fallback) { - assert methodLocator != null; - MethodHandle target = methodLocator.getMethodHandle(); - final MethodType targetType = methodLocator.getMethodType(); + for (final Map.Entry<String, byte[]> entry : classBytes.entrySet()) { + final String className = entry.getKey(); + final byte[] bytecode = entry.getValue(); - /* - * For any integer argument. a double that is representable as an integer is OK. - * otherwise the guard would have failed. in that case introduce a filter that - * casts the double to an integer, which we know will preserve all precision. - */ - for (int i = 0; i < targetType.parameterCount(); i++) { - if (targetType.parameterType(i) == int.class) { - //representable as int - target = MH.filterArguments(target, i, ENSURE_INT); + if (className.equals(mainClassName)) { + continue; } + + installedClasses.put(className, installer.install(className, bytecode)); } + return installedClasses; + } - MethodHandle mh = target; - if (guard != null) { - mh = MH.guardWithTest(MH.asCollector(guard, Object[].class, target.type().parameterCount()), MH.asType(target, fallback.type()), fallback); + /** + * Install this script using the given {@code installer}. + * + * @param script the compiled script + * @return the function initializer + */ + private FunctionInitializer installStoredScript(final StoredScript script, final CodeInstaller<ScriptEnvironment> newInstaller) { + final Map<String, Class<?>> installedClasses = installStoredScriptClasses(script, newInstaller); + + final Map<Integer, FunctionInitializer> initializers = script.getInitializers(); + assert initializers != null; + assert initializers.size() == 1; + final FunctionInitializer initializer = initializers.values().iterator().next(); + + final Object[] constants = script.getConstants(); + for (int i = 0; i < constants.length; i++) { + if (constants[i] instanceof RecompilableScriptFunctionData) { + // replace deserialized function data with the ones we already have + constants[i] = getScriptFunctionData(((RecompilableScriptFunctionData) constants[i]).getFunctionNodeId()); + } } - final CompiledFunction cf = new CompiledFunction(runtimeType == null ? targetType : runtimeType, mh); - code.add(cf); + newInstaller.initialize(installedClasses.values(), source, constants); + initializer.setCode(installedClasses.get(initializer.getClassName())); + return initializer; + } - return cf.getInvoker(); + boolean usePersistentCodeCache() { + final ScriptEnvironment env = installer.getOwner(); + return env._persistent_cache && env._optimistic_types; } - private static Type runtimeType(final Object arg) { - if (arg == null) { - return Type.OBJECT; + private MethodType explicitParams(final MethodType callSiteType) { + if (CompiledFunction.isVarArgsType(callSiteType)) { + return null; } - final Class<?> clazz = arg.getClass(); - assert !clazz.isPrimitive() : "always boxed"; - if (clazz == Double.class) { - return JSType.isRepresentableAsInt((double)arg) ? Type.INT : Type.NUMBER; - } else if (clazz == Integer.class) { - return Type.INT; - } else if (clazz == Long.class) { - return Type.LONG; - } else if (clazz == String.class) { - return Type.STRING; + final MethodType noCalleeThisType = callSiteType.dropParameterTypes(0, 2); // (callee, this) is always in call site type + final int callSiteParamCount = noCalleeThisType.parameterCount(); + + // Widen parameters of reference types to Object as we currently don't care for specialization among reference + // types. E.g. call site saying (ScriptFunction, Object, String) should still link to (ScriptFunction, Object, Object) + final Class<?>[] paramTypes = noCalleeThisType.parameterArray(); + boolean changed = false; + for (int i = 0; i < paramTypes.length; ++i) { + final Class<?> paramType = paramTypes[i]; + if (!(paramType.isPrimitive() || paramType == Object.class)) { + paramTypes[i] = Object.class; + changed = true; + } } - return Type.OBJECT; - } + final MethodType generalized = changed ? MethodType.methodType(noCalleeThisType.returnType(), paramTypes) : noCalleeThisType; - private static boolean canCoerce(final Object arg, final Type type) { - Type argType = runtimeType(arg); - if (Type.widest(argType, type) == type || arg == ScriptRuntime.UNDEFINED) { - return true; + if (callSiteParamCount < getArity()) { + return generalized.appendParameterTypes(Collections.<Class<?>>nCopies(getArity() - callSiteParamCount, Object.class)); } - System.err.println(arg + " does not fit in "+ argType + " " + type + " " + arg.getClass()); - new Throwable().printStackTrace(); - return false; + return generalized; } - @SuppressWarnings("unused") - private static boolean paramTypeGuard(final Type[] paramTypes, final Object... args) { - final int length = args.length; - assert args.length >= paramTypes.length; - - //i==start, skip the this, callee params etc - int start = args.length - paramTypes.length; - for (int i = start; i < args.length; i++) { - final Object arg = args[i]; - if (!canCoerce(arg, paramTypes[i - start])) { + private FunctionNode extractFunctionFromScript(final FunctionNode script) { + final Set<FunctionNode> fns = new HashSet<>(); + script.getBody().accept(new NodeVisitor<LexicalContext>(new LexicalContext()) { + @Override + public boolean enterFunctionNode(final FunctionNode fn) { + fns.add(fn); return false; } + }); + assert fns.size() == 1 : "got back more than one method in recompilation"; + final FunctionNode f = fns.iterator().next(); + assert f.getId() == functionNodeId; + if (!getFunctionFlag(FunctionNode.IS_DECLARED) && f.isDeclared()) { + return f.clearFlag(null, FunctionNode.IS_DECLARED); + } + return f; + } + + private void logLookup(final boolean shouldLog, final MethodType targetType) { + if (shouldLog && log.isEnabled()) { + log.info("Looking up ", DebugLogger.quote(functionName), " type=", targetType); } - return true; } - @SuppressWarnings("unused") - private static int ensureInt(final Object arg) { - if (arg instanceof Number) { - return ((Number)arg).intValue(); - } else if (arg instanceof Undefined) { - return 0; + private MethodHandle lookup(final FunctionInitializer fnInit, final boolean shouldLog) { + final MethodType type = fnInit.getMethodType(); + logLookup(shouldLog, type); + return lookupCodeMethod(fnInit.getCode(), type); + } + + MethodHandle lookup(final FunctionNode fn) { + final MethodType type = new FunctionSignature(fn).getMethodType(); + logLookup(true, type); + return lookupCodeMethod(fn.getCompileUnit().getCode(), type); + } + + MethodHandle lookupCodeMethod(final Class<?> codeClass, final MethodType targetType) { + return MH.findStatic(LOOKUP, codeClass, functionName, targetType); + } + + /** + * Initializes this function data with the eagerly generated version of the code. This method can only be invoked + * by the compiler internals in Nashorn and is public for implementation reasons only. Attempting to invoke it + * externally will result in an exception. + * + * @param initializer FunctionInitializer for this data + */ + public void initializeCode(final FunctionInitializer initializer) { + // Since the method is public, we double-check that we aren't invoked with an inappropriate compile unit. + if(!code.isEmpty()) { + throw new IllegalStateException(name); } - throw new AssertionError(arg); + addCode(lookup(initializer, true), null, null, initializer.getFlags()); + } + + private CompiledFunction addCode(final MethodHandle target, final Map<Integer, Type> invalidatedProgramPoints, + final MethodType callSiteType, final int fnFlags) { + final CompiledFunction cfn = new CompiledFunction(target, this, invalidatedProgramPoints, callSiteType, fnFlags); + code.add(cfn); + return cfn; } /** - * Given the runtime callsite args, compute a method type that is equivalent to what - * was passed - this is typically a lot more specific that what the compiler has been - * able to deduce - * @param callSiteType callsite type for the compiled callsite target - * @param args runtime arguments to the compiled callsite target - * @return adjusted method type, narrowed as to conform to runtime callsite type instead + * Add code with specific call site type. It will adapt the type of the looked up method handle to fit the call site + * type. This is necessary because even if we request a specialization that takes an "int" parameter, we might end + * up getting one that takes a "double" etc. because of internal function logic causes widening (e.g. assignment of + * a wider value to the parameter variable). However, we use the method handle type for matching subsequent lookups + * for the same specialization, so we must adapt the handle to the expected type. + * @param fnInit the function + * @param callSiteType the call site type + * @return the compiled function object, with its type matching that of the call site type. */ - private static MethodType runtimeType(final MethodType callSiteType, final Object[] args) { - if (args == null) { - //for example bound, or otherwise runtime arguments to callsite unavailable, then - //do not change the type - return callSiteType; + private CompiledFunction addCode(final FunctionInitializer fnInit, final MethodType callSiteType) { + if (isVariableArity()) { + return addCode(lookup(fnInit, true), fnInit.getInvalidatedProgramPoints(), callSiteType, fnInit.getFlags()); + } + + final MethodHandle handle = lookup(fnInit, true); + final MethodType fromType = handle.type(); + MethodType toType = needsCallee(fromType) ? callSiteType.changeParameterType(0, ScriptFunction.class) : callSiteType.dropParameterTypes(0, 1); + toType = toType.changeReturnType(fromType.returnType()); + + final int toCount = toType.parameterCount(); + final int fromCount = fromType.parameterCount(); + final int minCount = Math.min(fromCount, toCount); + for(int i = 0; i < minCount; ++i) { + final Class<?> fromParam = fromType.parameterType(i); + final Class<?> toParam = toType.parameterType(i); + // If method has an Object parameter, but call site had String, preserve it as Object. No need to narrow it + // artificially. Note that this is related to how CompiledFunction.matchesCallSite() works, specifically + // the fact that various reference types compare to equal (see "fnType.isEquivalentTo(csType)" there). + if (fromParam != toParam && !fromParam.isPrimitive() && !toParam.isPrimitive()) { + assert fromParam.isAssignableFrom(toParam); + toType = toType.changeParameterType(i, fromParam); + } } - final Class<?>[] paramTypes = new Class<?>[callSiteType.parameterCount()]; - final int start = args.length - callSiteType.parameterCount(); - for (int i = start; i < args.length; i++) { - paramTypes[i - start] = runtimeType(args[i]).getTypeClass(); + if (fromCount > toCount) { + toType = toType.appendParameterTypes(fromType.parameterList().subList(toCount, fromCount)); + } else if (fromCount < toCount) { + toType = toType.dropParameterTypes(fromCount, toCount); } - return MH.type(callSiteType.returnType(), paramTypes); + + return addCode(lookup(fnInit, false).asType(toType), fnInit.getInvalidatedProgramPoints(), callSiteType, fnInit.getFlags()); } - private static ArrayList<Type> runtimeType(final MethodType mt) { - final ArrayList<Type> type = new ArrayList<>(); - for (int i = 0; i < mt.parameterCount(); i++) { - type.add(Type.typeFor(mt.parameterType(i))); - } - return type; + /** + * Returns the return type of a function specialization for particular parameter types.<br> + * <b>Be aware that the way this is implemented, it forces full materialization (compilation and installation) of + * code for that specialization.</b> + * @param callSiteType the parameter types at the call site. It must include the mandatory {@code callee} and + * {@code this} parameters, so it needs to start with at least {@code ScriptFunction.class} and + * {@code Object.class} class. Since the return type of the function is calculated from the code itself, it is + * irrelevant and should be set to {@code Object.class}. + * @param runtimeScope a current runtime scope. Can be null but when it's present it will be used as a source of + * current runtime values that can improve the compiler's type speculations (and thus reduce the need for later + * recompilations) if the specialization is not already present and thus needs to be freshly compiled. + * @return the return type of the function specialization. + */ + public Class<?> getReturnType(final MethodType callSiteType, final ScriptObject runtimeScope) { + return getBest(callSiteType, runtimeScope, CompiledFunction.NO_FUNCTIONS).type().returnType(); } @Override - synchronized MethodHandle getBestInvoker(final MethodType callSiteType, final Object[] args) { - final MethodType runtimeType = runtimeType(callSiteType, args); - assert runtimeType.parameterCount() == callSiteType.parameterCount(); - - final MethodHandle mh = super.getBestInvoker(runtimeType, args); - - /* - * Not all functions can be specialized, for example, if we deemed memory - * footprint too large to store a parse snapshot, or if it is meaningless - * to do so, such as e.g. for runScript - */ - if (functionNode == null || !functionNode.canSpecialize()) { - return mh; - } - - /* - * Check if best invoker is equally specific or more specific than runtime - * type. In that case, we don't need further specialization, but can use - * whatever we have already. We know that it will match callSiteType, or it - * would not have been returned from getBestInvoker - */ - if (!code.isLessSpecificThan(runtimeType)) { - return mh; - } - - int i; - final FunctionNode snapshot = functionNode.getSnapshot(); - assert snapshot != null; - - /* - * Create a list of the arg types that the compiler knows about - * typically, the runtime args are a lot more specific, and we should aggressively - * try to use those whenever possible - * We WILL try to make an aggressive guess as possible, and add guards if needed. - * For example, if the compiler can deduce that we have a number type, but the runtime - * passes and int, we might still want to keep it an int, and the gamble to - * check that whatever is passed is int representable usually pays off - * If the compiler only knows that a parameter is an "Object", it is still worth - * it to try to specialize it by looking at the runtime arg. - */ - final LinkedList<Type> compileTimeArgs = new LinkedList<>(); - for (i = callSiteType.parameterCount() - 1; i >= 0 && compileTimeArgs.size() < snapshot.getParameters().size(); i--) { - compileTimeArgs.addFirst(Type.typeFor(callSiteType.parameterType(i))); - } - - /* - * The classes known at compile time are a safe to generate as primitives without parameter guards - * But the classes known at runtime (if more specific than compile time types) are safe to generate as primitives - * IFF there are parameter guards - */ - MethodHandle guard = null; - final ArrayList<Type> runtimeParamTypes = runtimeType(runtimeType); - while (runtimeParamTypes.size() > functionNode.getParameters().size()) { - runtimeParamTypes.remove(0); - } - for (i = 0; i < compileTimeArgs.size(); i++) { - final Type rparam = Type.typeFor(runtimeType.parameterType(i)); - final Type cparam = compileTimeArgs.get(i); - - if (cparam.isObject() && !rparam.isObject()) { - //check that the runtime object is still coercible to the runtime type, because compiler can't prove it's always primitive - if (guard == null) { - guard = MH.insertArguments(PARAM_TYPE_GUARD, 0, (Object)runtimeParamTypes.toArray(new Type[runtimeParamTypes.size()])); - } + synchronized CompiledFunction getBest(final MethodType callSiteType, final ScriptObject runtimeScope, final Collection<CompiledFunction> forbidden) { + CompiledFunction existingBest = super.getBest(callSiteType, runtimeScope, forbidden); + if (existingBest == null) { + existingBest = addCode(compileTypeSpecialization(callSiteType, runtimeScope, true), callSiteType); + } + + assert existingBest != null; + //we are calling a vararg method with real args + boolean varArgWithRealArgs = existingBest.isVarArg() && !CompiledFunction.isVarArgsType(callSiteType); + + //if the best one is an apply to call, it has to match the callsite exactly + //or we need to regenerate + if (existingBest.isApplyToCall()) { + final CompiledFunction best = lookupExactApplyToCall(callSiteType); + if (best != null) { + return best; } + varArgWithRealArgs = true; } - Compiler.LOG.info("Callsite specialized ", name, " runtimeType=", runtimeType, " parameters=", snapshot.getParameters(), " args=", Arrays.asList(args)); - - assert snapshot != functionNode; + if (varArgWithRealArgs) { + // special case: we had an apply to call, but we failed to make it fit. + // Try to generate a specialized one for this callsite. It may + // be another apply to call specialization, or it may not, but whatever + // it is, it is a specialization that is guaranteed to fit + final FunctionInitializer fnInit = compileTypeSpecialization(callSiteType, runtimeScope, false); + existingBest = addCode(fnInit, callSiteType); + } - final Compiler compiler = new Compiler(installer); + return existingBest; + } - final FunctionNode compiledSnapshot = compiler.compile( - snapshot.setHints( - null, - new Compiler.Hints(runtimeParamTypes.toArray(new Type[runtimeParamTypes.size()])))); + @Override + boolean isRecompilable() { + return true; + } - /* - * No matter how narrow your types were, they can never be narrower than Attr during recompile made them. I.e. you - * can put an int into the function here, if you see it as a runtime type, but if the function uses a multiplication - * on it, it will still need to be a double. At least until we have overflow checks. Similarly, if an int is - * passed but it is used as a string, it makes no sense to make the parameter narrower than Object. At least until - * the "different types for one symbol in difference places" work is done - */ - compiler.install(compiledSnapshot); + @Override + public boolean needsCallee() { + return getFunctionFlag(FunctionNode.NEEDS_CALLEE); + } - return addCode(compiledSnapshot, runtimeType, guard, mh); + /** + * Returns the {@link FunctionNode} flags associated with this function data. + * @return the {@link FunctionNode} flags associated with this function data. + */ + public int getFunctionFlags() { + return functionFlags; } - private static MethodHandle findOwnMH(final String name, final Class<?> rtype, final Class<?>... types) { - return MH.findStatic(MethodHandles.lookup(), RecompilableScriptFunctionData.class, name, MH.type(rtype, types)); + @Override + MethodType getGenericType() { + // 2 is for (callee, this) + if (isVariableArity()) { + return MethodType.genericMethodType(2, true); + } + return MethodType.genericMethodType(2 + getArity()); } /** - * Helper class that allows us to retrieve the method handle for this function once it has been generated. + * Return the function node id. + * @return the function node id */ - private static class MethodLocator implements Serializable { - private transient Class<?> clazz; - private final String className; - private final String methodName; - private final MethodType methodType; - - private static final long serialVersionUID = -5420835725902966692L; + public int getFunctionNodeId() { + return functionNodeId; + } - MethodLocator(final FunctionNode functionNode) { - this.className = functionNode.getCompileUnit().getUnitClassName(); - this.methodName = functionNode.getName(); - this.methodType = new FunctionSignature(functionNode).getMethodType(); + /** + * Get the source for the script + * @return source + */ + public Source getSource() { + return source; + } - assert className != null; - assert methodName != null; + /** + * Return a script function data based on a function id, either this function if + * the id matches or a nested function based on functionId. This goes down into + * nested functions until all leaves are exhausted. + * + * @param functionId function id + * @return script function data or null if invalid id + */ + public RecompilableScriptFunctionData getScriptFunctionData(final int functionId) { + if (functionId == functionNodeId) { + return this; } + RecompilableScriptFunctionData data; - void setClass(final Class<?> clazz) { - if (!JS.class.isAssignableFrom(clazz)) { - throw new IllegalArgumentException(); + data = nestedFunctions == null ? null : nestedFunctions.get(functionId); + if (data != null) { + return data; + } + for (final RecompilableScriptFunctionData ndata : nestedFunctions.values()) { + data = ndata.getScriptFunctionData(functionId); + if (data != null) { + return data; } - this.clazz = clazz; } + return null; + } - String getClassName() { - return className; - } + /** + * Check whether a certain name is a global symbol, i.e. only exists as defined + * in outermost scope and not shadowed by being parameter or assignment in inner + * scopes + * + * @param functionNode function node to check + * @param symbolName symbol name + * @return true if global symbol + */ + public boolean isGlobalSymbol(final FunctionNode functionNode, final String symbolName) { + RecompilableScriptFunctionData data = getScriptFunctionData(functionNode.getId()); + assert data != null; - MethodType getMethodType() { - return methodType; - } + do { + if (data.hasInternalSymbol(symbolName)) { + return false; + } + data = data.getParent(); + } while(data != null); - MethodHandle getMethodHandle() { - return MH.findStatic(LOOKUP, clazz, methodName, methodType); - } + return true; } -} + /** + * Restores the {@link #getFunctionFlags()} flags to a function node. During on-demand compilation, we might need + * to restore flags to a function node that was otherwise not subjected to a full compile pipeline (e.g. its parse + * was skipped, or it's a nested function of a deserialized function. + * @param lc current lexical context + * @param fn the function node to restore flags onto + * @return the transformed function node + */ + public FunctionNode restoreFlags(final LexicalContext lc, final FunctionNode fn) { + assert fn.getId() == functionNodeId; + FunctionNode newFn = fn.setFlags(lc, functionFlags); + // This compensates for missing markEval() in case the function contains an inner function + // that contains eval(), that now we didn't discover since we skipped the inner function. + if (newFn.hasNestedEval()) { + assert newFn.hasScopeBlock(); + newFn = newFn.setBody(lc, newFn.getBody().setNeedsScope(null)); + } + return newFn; + } + + private void readObject(final java.io.ObjectInputStream in) throws IOException, ClassNotFoundException { + in.defaultReadObject(); + createLogger(); + } + private void createLogger() { + log = initLogger(Context.getContextTrusted()); + } +} |