diff options
Diffstat (limited to 'src/jdk/nashorn/internal/runtime/CompiledFunction.java')
-rw-r--r-- | src/jdk/nashorn/internal/runtime/CompiledFunction.java | 955 |
1 files changed, 864 insertions, 91 deletions
diff --git a/src/jdk/nashorn/internal/runtime/CompiledFunction.java b/src/jdk/nashorn/internal/runtime/CompiledFunction.java index 18b20a3d..ce20ed31 100644 --- a/src/jdk/nashorn/internal/runtime/CompiledFunction.java +++ b/src/jdk/nashorn/internal/runtime/CompiledFunction.java @@ -24,97 +24,301 @@ */ package jdk.nashorn.internal.runtime; +import static jdk.nashorn.internal.lookup.Lookup.MH; +import static jdk.nashorn.internal.runtime.UnwarrantedOptimismException.INVALID_PROGRAM_POINT; +import static jdk.nashorn.internal.runtime.UnwarrantedOptimismException.isValid; +import java.lang.invoke.CallSite; import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; - +import java.lang.invoke.MutableCallSite; +import java.lang.invoke.SwitchPoint; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.function.Supplier; +import java.util.logging.Level; +import jdk.internal.dynalink.linker.GuardedInvocation; +import jdk.nashorn.internal.codegen.Compiler; +import jdk.nashorn.internal.codegen.Compiler.CompilationPhases; +import jdk.nashorn.internal.codegen.TypeMap; +import jdk.nashorn.internal.codegen.types.ArrayType; import jdk.nashorn.internal.codegen.types.Type; +import jdk.nashorn.internal.ir.FunctionNode; +import jdk.nashorn.internal.objects.annotations.SpecializedFunction.LinkLogic; +import jdk.nashorn.internal.runtime.events.RecompilationEvent; +import jdk.nashorn.internal.runtime.linker.Bootstrap; +import jdk.nashorn.internal.runtime.logging.DebugLogger; /** * An version of a JavaScript function, native or JavaScript. * Supports lazily generating a constructor version of the invocation. */ -final class CompiledFunction implements Comparable<CompiledFunction> { - - /** The method type may be more specific than the invoker, if. e.g. - * the invoker is guarded, and a guard with a generic object only - * fallback, while the target is more specific, we still need the - * more specific type for sorting */ - private final MethodType type; - private final MethodHandle invoker; +final class CompiledFunction { + + private static final MethodHandle NEWFILTER = findOwnMH("newFilter", Object.class, Object.class, Object.class); + private static final MethodHandle RELINK_COMPOSABLE_INVOKER = findOwnMH("relinkComposableInvoker", void.class, CallSite.class, CompiledFunction.class, boolean.class); + private static final MethodHandle HANDLE_REWRITE_EXCEPTION = findOwnMH("handleRewriteException", MethodHandle.class, CompiledFunction.class, OptimismInfo.class, RewriteException.class); + private static final MethodHandle RESTOF_INVOKER = MethodHandles.exactInvoker(MethodType.methodType(Object.class, RewriteException.class)); + + private final DebugLogger log; + + static final Collection<CompiledFunction> NO_FUNCTIONS = Collections.emptySet(); + + /** + * The method type may be more specific than the invoker, if. e.g. + * the invoker is guarded, and a guard with a generic object only + * fallback, while the target is more specific, we still need the + * more specific type for sorting + */ + private MethodHandle invoker; private MethodHandle constructor; + private OptimismInfo optimismInfo; + private final int flags; // from FunctionNode + private final MethodType callSiteType; + + private final Specialization specialization; - CompiledFunction(final MethodType type, final MethodHandle invoker) { - this(type, invoker, null); + CompiledFunction(final MethodHandle invoker) { + this(invoker, null, null); } - CompiledFunction(final MethodType type, final MethodHandle invoker, final MethodHandle constructor) { - assert type != null; - this.type = type; - this.invoker = invoker; + static CompiledFunction createBuiltInConstructor(final MethodHandle invoker, final Specialization specialization) { + return new CompiledFunction(MH.insertArguments(invoker, 0, false), createConstructorFromInvoker(MH.insertArguments(invoker, 0, true)), specialization); + } + + CompiledFunction(final MethodHandle invoker, final MethodHandle constructor, final Specialization specialization) { + this(invoker, constructor, 0, null, specialization, DebugLogger.DISABLED_LOGGER); + } + + CompiledFunction(final MethodHandle invoker, final MethodHandle constructor, final int flags, final MethodType callSiteType, final Specialization specialization, final DebugLogger log) { + this.specialization = specialization; + if (specialization != null && specialization.isOptimistic()) { + /* + * An optimistic builtin with isOptimistic=true works like any optimistic generated function, i.e. it + * can throw unwarranted optimism exceptions. As native functions trivially can't have parts of them + * regenerated as restof methods, this only works if the methods are atomic/functional in their behavior + * and doesn't modify state before an UOE can be thrown. If they aren't, we can reexecute a wider version + * of the same builtin in a recompilation handler for FinalScriptFunctionData. There are several + * candidate methods in Native* that would benefit from this, but I haven't had time to implement any + * of them currently. In order to fit in with the relinking framework, the current thinking is + * that the methods still take a program point to fit in with other optimistic functions, but + * it is set to "first", which is the beginning of the method. The relinker can tell the difference + * between builtin and JavaScript functions. This might change. TODO + */ + this.invoker = MH.insertArguments(invoker, invoker.type().parameterCount() - 1, UnwarrantedOptimismException.FIRST_PROGRAM_POINT); + throw new AssertionError("Optimistic (UnwarrantedOptimismException throwing) builtin functions are currently not in use"); + } + this.invoker = invoker; this.constructor = constructor; + this.flags = flags; + this.callSiteType = callSiteType; + this.log = log; } - @Override - public String toString() { - return "<callSiteType= " + type + " invoker=" + invoker + " ctor=" + constructor + ">"; + CompiledFunction(final MethodHandle invoker, final RecompilableScriptFunctionData functionData, + final Map<Integer, Type> invalidatedProgramPoints, final MethodType callSiteType, final int flags) { + this(invoker, null, flags, callSiteType, null, functionData.getLogger()); + if ((flags & FunctionNode.IS_DEOPTIMIZABLE) != 0) { + optimismInfo = new OptimismInfo(functionData, invalidatedProgramPoints); + } else { + optimismInfo = null; + } } - MethodHandle getInvoker() { - return invoker; + static CompiledFunction createBuiltInConstructor(final MethodHandle invoker) { + return new CompiledFunction(MH.insertArguments(invoker, 0, false), createConstructorFromInvoker(MH.insertArguments(invoker, 0, true)), null); } - MethodHandle getConstructor() { - return constructor; + boolean isSpecialization() { + return specialization != null; } - void setConstructor(final MethodHandle constructor) { - this.constructor = constructor; + boolean hasLinkLogic() { + return getLinkLogicClass() != null; } - boolean hasConstructor() { - return constructor != null; + Class<? extends LinkLogic> getLinkLogicClass() { + if (isSpecialization()) { + final Class<? extends LinkLogic> linkLogicClass = specialization.getLinkLogicClass(); + assert !LinkLogic.isEmpty(linkLogicClass) : "empty link logic classes should have been removed by nasgen"; + return linkLogicClass; + } + return null; } - MethodType type() { - return type; + int getFlags() { + return flags; + } + + /** + * An optimistic specialization is one that can throw UnwarrantedOptimismException. + * This is allowed for native methods, as long as they are functional, i.e. don't change + * any state between entering and throwing the UOE. Then we can re-execute a wider version + * of the method in the continuation. Rest-of method generation for optimistic builtins is + * of course not possible, but this approach works and fits into the same relinking + * framework + * + * @return true if optimistic builtin + */ + boolean isOptimistic() { + return isSpecialization() ? specialization.isOptimistic() : false; + } + + boolean isApplyToCall() { + return (flags & FunctionNode.HAS_APPLY_TO_CALL_SPECIALIZATION) != 0; + } + + boolean isVarArg() { + return isVarArgsType(invoker.type()); } @Override - public int compareTo(final CompiledFunction o) { - return compareMethodTypes(type(), o.type()); + public String toString() { + final StringBuilder sb = new StringBuilder(); + final Class<? extends LinkLogic> linkLogicClass = getLinkLogicClass(); + + sb.append("[invokerType="). + append(invoker.type()). + append(" ctor="). + append(constructor). + append(" weight="). + append(weight()). + append(" linkLogic="). + append(linkLogicClass != null ? linkLogicClass.getSimpleName() : "none"); + + return sb.toString(); + } + + boolean needsCallee() { + return ScriptFunctionData.needsCallee(invoker); } - private static int compareMethodTypes(final MethodType ownType, final MethodType otherType) { - // Comparable interface demands that compareTo() should only return 0 if objects are equal. - // Failing to meet this requirement causes same weight functions to replace each other in TreeSet, - // so we go some lengths to come up with an ordering between same weight functions, - // first falling back to parameter count and then to hash code. - if (ownType.equals(otherType)) { - return 0; + /** + * Returns an invoker method handle for this function. Note that the handle is safely composable in + * the sense that you can compose it with other handles using any combinators even if you can't affect call site + * invalidation. If this compiled function is non-optimistic, then it returns the same value as + * {@link #getInvokerOrConstructor(boolean)}. However, if the function is optimistic, then this handle will + * incur an overhead as it will add an intermediate internal call site that can relink itself when the function + * needs to regenerate its code to always point at the latest generated code version. + * @return a guaranteed composable invoker method handle for this function. + */ + MethodHandle createComposableInvoker() { + return createComposableInvoker(false); + } + + /** + * Returns an invoker method handle for this function when invoked as a constructor. Note that the handle should be + * considered non-composable in the sense that you can only compose it with other handles using any combinators if + * you can ensure that the composition is guarded by {@link #getOptimisticAssumptionsSwitchPoint()} if it's + * non-null, and that you can relink the call site it is set into as a target if the switch point is invalidated. In + * all other cases, use {@link #createComposableConstructor()}. + * @return a direct constructor method handle for this function. + */ + private MethodHandle getConstructor() { + if (constructor == null) { + constructor = createConstructorFromInvoker(createInvokerForPessimisticCaller()); } - final int diff = weight(ownType) - weight(otherType); - if (diff != 0) { - return diff; + return constructor; + } + + /** + * Creates a version of the invoker intended for a pessimistic caller (return type is Object, no caller optimistic + * program point available). + * @return a version of the invoker intended for a pessimistic caller. + */ + private MethodHandle createInvokerForPessimisticCaller() { + return createInvoker(Object.class, INVALID_PROGRAM_POINT); + } + + /** + * Compose a constructor from an invoker. + * + * @param invoker invoker + * @return the composed constructor + */ + private static MethodHandle createConstructorFromInvoker(final MethodHandle invoker) { + final boolean needsCallee = ScriptFunctionData.needsCallee(invoker); + // If it was (callee, this, args...), permute it to (this, callee, args...). We're doing this because having + // "this" in the first argument position is what allows the elegant folded composition of + // (newFilter x constructor x allocator) further down below in the code. Also, ensure the composite constructor + // always returns Object. + final MethodHandle swapped = needsCallee ? swapCalleeAndThis(invoker) : invoker; + + final MethodHandle returnsObject = MH.asType(swapped, swapped.type().changeReturnType(Object.class)); + + final MethodType ctorType = returnsObject.type(); + + // Construct a dropping type list for NEWFILTER, but don't include constructor "this" into it, so it's actually + // captured as "allocation" parameter of NEWFILTER after we fold the constructor into it. + // (this, [callee, ]args...) => ([callee, ]args...) + final Class<?>[] ctorArgs = ctorType.dropParameterTypes(0, 1).parameterArray(); + + // Fold constructor into newFilter that replaces the return value from the constructor with the originally + // allocated value when the originally allocated value is a JS primitive (String, Boolean, Number). + // (result, this, [callee, ]args...) x (this, [callee, ]args...) => (this, [callee, ]args...) + final MethodHandle filtered = MH.foldArguments(MH.dropArguments(NEWFILTER, 2, ctorArgs), returnsObject); + + // allocate() takes a ScriptFunction and returns a newly allocated ScriptObject... + if (needsCallee) { + // ...we either fold it into the previous composition, if we need both the ScriptFunction callee object and + // the newly allocated object in the arguments, so (this, callee, args...) x (callee) => (callee, args...), + // or... + return MH.foldArguments(filtered, ScriptFunction.ALLOCATE); } - if (ownType.parameterCount() != otherType.parameterCount()) { - return ownType.parameterCount() - otherType.parameterCount(); + + // ...replace the ScriptFunction argument with the newly allocated object, if it doesn't need the callee + // (this, args...) filter (callee) => (callee, args...) + return MH.filterArguments(filtered, 0, ScriptFunction.ALLOCATE); + } + + /** + * Permutes the parameters in the method handle from {@code (callee, this, ...)} to {@code (this, callee, ...)}. + * Used when creating a constructor handle. + * @param mh a method handle with order of arguments {@code (callee, this, ...)} + * @return a method handle with order of arguments {@code (this, callee, ...)} + */ + private static MethodHandle swapCalleeAndThis(final MethodHandle mh) { + final MethodType type = mh.type(); + assert type.parameterType(0) == ScriptFunction.class : type; + assert type.parameterType(1) == Object.class : type; + final MethodType newType = type.changeParameterType(0, Object.class).changeParameterType(1, ScriptFunction.class); + final int[] reorder = new int[type.parameterCount()]; + reorder[0] = 1; + assert reorder[1] == 0; + for (int i = 2; i < reorder.length; ++i) { + reorder[i] = i; } - // We're just interested in not returning 0 here, not correct ordering - return ownType.hashCode() - otherType.hashCode(); + return MethodHandles.permuteArguments(mh, newType, reorder); } - @Override - public boolean equals(Object obj) { - return obj instanceof CompiledFunction && type().equals(((CompiledFunction)obj).type()); + /** + * Returns an invoker method handle for this function when invoked as a constructor. Note that the handle is safely + * composable in the sense that you can compose it with other handles using any combinators even if you can't affect + * call site invalidation. If this compiled function is non-optimistic, then it returns the same value as + * {@link #getConstructor()}. However, if the function is optimistic, then this handle will incur an overhead as it + * will add an intermediate internal call site that can relink itself when the function needs to regenerate its code + * to always point at the latest generated code version. + * @return a guaranteed composable constructor method handle for this function. + */ + MethodHandle createComposableConstructor() { + return createComposableInvoker(true); } - @Override - public int hashCode() { - return type().hashCode(); + boolean hasConstructor() { + return constructor != null; + } + + MethodType type() { + return invoker.type(); } - private int weight() { + int weight() { return weight(type()); } @@ -124,76 +328,645 @@ final class CompiledFunction implements Comparable<CompiledFunction> { } int weight = Type.typeFor(type.returnType()).getWeight(); - for (final Class<?> paramType : type.parameterArray()) { + for (int i = 0 ; i < type.parameterCount() ; i++) { + final Class<?> paramType = type.parameterType(i); final int pweight = Type.typeFor(paramType).getWeight() * 2; //params are more important than call types as return values are always specialized weight += pweight; } + + weight += type.parameterCount(); //more params outweigh few parameters + return weight; } - private static boolean isVarArgsType(final MethodType type) { + static boolean isVarArgsType(final MethodType type) { assert type.parameterCount() >= 1 : type; return type.parameterType(type.parameterCount() - 1) == Object[].class; } - boolean moreGenericThan(final CompiledFunction o) { - return weight() > o.weight(); + static boolean moreGenericThan(final MethodType mt0, final MethodType mt1) { + return weight(mt0) > weight(mt1); } - boolean moreGenericThan(final MethodType mt) { - return weight() > weight(mt); + boolean betterThanFinal(final CompiledFunction other, final MethodType callSiteMethodType) { + // Prefer anything over nothing, as we can't compile new versions. + if (other == null) { + return true; + } + return betterThanFinal(this, other, callSiteMethodType); } - /** - * Check whether a given method descriptor is compatible with this invocation. - * It is compatible if the types are narrower than the invocation type so that - * a semantically equivalent linkage can be performed. - * - * @param mt type to check against - * @return true if types are compatible - */ - boolean typeCompatible(final MethodType mt) { - final int wantedParamCount = mt.parameterCount(); - final int existingParamCount = type.parameterCount(); - - //if we are not examining a varargs type, the number of parameters must be the same - if (wantedParamCount != existingParamCount && !isVarArgsType(mt)) { + private static boolean betterThanFinal(final CompiledFunction cf, final CompiledFunction other, final MethodType callSiteMethodType) { + final MethodType thisMethodType = cf.type(); + final MethodType otherMethodType = other.type(); + final int thisParamCount = getParamCount(thisMethodType); + final int otherParamCount = getParamCount(otherMethodType); + final int callSiteRawParamCount = getParamCount(callSiteMethodType); + final boolean csVarArg = callSiteRawParamCount == Integer.MAX_VALUE; + // Subtract 1 for callee for non-vararg call sites + final int callSiteParamCount = csVarArg ? callSiteRawParamCount : callSiteRawParamCount - 1; + + // Prefer the function that discards less parameters + final int thisDiscardsParams = Math.max(callSiteParamCount - thisParamCount, 0); + final int otherDiscardsParams = Math.max(callSiteParamCount - otherParamCount, 0); + if(thisDiscardsParams < otherDiscardsParams) { + return true; + } + if(thisDiscardsParams > otherDiscardsParams) { return false; } - //we only go as far as the shortest array. the only chance to make this work if - //parameters lengths do not match is if our type ends with a varargs argument. - //then every trailing parameter in the given callsite can be folded into it, making - //us compatible (albeit slower than a direct specialization) - final int lastParamIndex = Math.min(wantedParamCount, existingParamCount); - for (int i = 0; i < lastParamIndex; i++) { - final Type w = Type.typeFor(mt.parameterType(i)); - final Type e = Type.typeFor(type.parameterType(i)); + final boolean thisVarArg = thisParamCount == Integer.MAX_VALUE; + final boolean otherVarArg = otherParamCount == Integer.MAX_VALUE; + if(!(thisVarArg && otherVarArg && csVarArg)) { + // At least one of them isn't vararg + final Type[] thisType = toTypeWithoutCallee(thisMethodType, 0); // Never has callee + final Type[] otherType = toTypeWithoutCallee(otherMethodType, 0); // Never has callee + final Type[] callSiteType = toTypeWithoutCallee(callSiteMethodType, 1); // Always has callee + + int narrowWeightDelta = 0; + int widenWeightDelta = 0; + final int minParamsCount = Math.min(Math.min(thisParamCount, otherParamCount), callSiteParamCount); + for(int i = 0; i < minParamsCount; ++i) { + final int callSiteParamWeight = getParamType(i, callSiteType, csVarArg).getWeight(); + // Delta is negative for narrowing, positive for widening + final int thisParamWeightDelta = getParamType(i, thisType, thisVarArg).getWeight() - callSiteParamWeight; + final int otherParamWeightDelta = getParamType(i, otherType, otherVarArg).getWeight() - callSiteParamWeight; + // Only count absolute values of narrowings + narrowWeightDelta += Math.max(-thisParamWeightDelta, 0) - Math.max(-otherParamWeightDelta, 0); + // Only count absolute values of widenings + widenWeightDelta += Math.max(thisParamWeightDelta, 0) - Math.max(otherParamWeightDelta, 0); + } - //don't specialize on booleans, we have the "true" vs int 1 ambiguity in resolution - //we also currently don't support boolean as a javascript function callsite type. - //it will always box. - if (w.isBoolean()) { + // If both functions accept more arguments than what is passed at the call site, account for ability + // to receive Undefined un-narrowed in the remaining arguments. + if(!thisVarArg) { + for(int i = callSiteParamCount; i < thisParamCount; ++i) { + narrowWeightDelta += Math.max(Type.OBJECT.getWeight() - thisType[i].getWeight(), 0); + } + } + if(!otherVarArg) { + for(int i = callSiteParamCount; i < otherParamCount; ++i) { + narrowWeightDelta -= Math.max(Type.OBJECT.getWeight() - otherType[i].getWeight(), 0); + } + } + + // Prefer function that narrows less + if(narrowWeightDelta < 0) { + return true; + } + if(narrowWeightDelta > 0) { return false; } - //This callsite type has a vararg here. it will swallow all remaining args. - //for consistency, check that it's the last argument - if (e.isArray()) { + // Prefer function that widens less + if(widenWeightDelta < 0) { return true; } + if(widenWeightDelta > 0) { + return false; + } + } + + // Prefer the function that exactly matches the arity of the call site. + if(thisParamCount == callSiteParamCount && otherParamCount != callSiteParamCount) { + return true; + } + if(thisParamCount != callSiteParamCount && otherParamCount == callSiteParamCount) { + return false; + } + + // Otherwise, neither function matches arity exactly. We also know that at this point, they both can receive + // more arguments than call site, otherwise we would've already chosen the one that discards less parameters. + // Note that variable arity methods are preferred, as they actually match the call site arity better, since they + // really have arbitrary arity. + if(thisVarArg) { + if(!otherVarArg) { + return true; // + } + } else if(otherVarArg) { + return false; + } + + // Neither is variable arity; chose the one that has less extra parameters. + final int fnParamDelta = thisParamCount - otherParamCount; + if(fnParamDelta < 0) { + return true; + } + if(fnParamDelta > 0) { + return false; + } + + final int callSiteRetWeight = Type.typeFor(callSiteMethodType.returnType()).getWeight(); + // Delta is negative for narrower return type, positive for wider return type + final int thisRetWeightDelta = Type.typeFor(thisMethodType.returnType()).getWeight() - callSiteRetWeight; + final int otherRetWeightDelta = Type.typeFor(otherMethodType.returnType()).getWeight() - callSiteRetWeight; + + // Prefer function that returns a less wide return type + final int widenRetDelta = Math.max(thisRetWeightDelta, 0) - Math.max(otherRetWeightDelta, 0); + if(widenRetDelta < 0) { + return true; + } + if(widenRetDelta > 0) { + return false; + } + + // Prefer function that returns a less narrow return type + final int narrowRetDelta = Math.max(-thisRetWeightDelta, 0) - Math.max(-otherRetWeightDelta, 0); + if(narrowRetDelta < 0) { + return true; + } + if(narrowRetDelta > 0) { + return false; + } + + //if they are equal, pick the specialized one first + if (cf.isSpecialization() != other.isSpecialization()) { + return cf.isSpecialization(); //always pick the specialized version if we can + } + + if (cf.isSpecialization() && other.isSpecialization()) { + return cf.getLinkLogicClass() != null; //pick link logic specialization above generic specializations + } + + // Signatures are identical + throw new AssertionError(thisMethodType + " identically applicable to " + otherMethodType + " for " + callSiteMethodType); + } + + private static Type[] toTypeWithoutCallee(final MethodType type, final int thisIndex) { + final int paramCount = type.parameterCount(); + final Type[] t = new Type[paramCount - thisIndex]; + for(int i = thisIndex; i < paramCount; ++i) { + t[i - thisIndex] = Type.typeFor(type.parameterType(i)); + } + return t; + } + + private static Type getParamType(final int i, final Type[] paramTypes, final boolean isVarArg) { + final int fixParamCount = paramTypes.length - (isVarArg ? 1 : 0); + if(i < fixParamCount) { + return paramTypes[i]; + } + assert isVarArg; + return ((ArrayType)paramTypes[paramTypes.length - 1]).getElementType(); + } + + boolean matchesCallSite(final MethodType other, final boolean pickVarArg) { + if (other.equals(this.callSiteType)) { + return true; + } + final MethodType type = type(); + final int fnParamCount = getParamCount(type); + final boolean isVarArg = fnParamCount == Integer.MAX_VALUE; + if (isVarArg) { + return pickVarArg; + } + + final int csParamCount = getParamCount(other); + final boolean csIsVarArg = csParamCount == Integer.MAX_VALUE; + final int thisThisIndex = needsCallee() ? 1 : 0; // Index of "this" parameter in this function's type + + final int fnParamCountNoCallee = fnParamCount - thisThisIndex; + final int minParams = Math.min(csParamCount - 1, fnParamCountNoCallee); // callSiteType always has callee, so subtract 1 + // We must match all incoming parameters, except "this". Starting from 1 to skip "this". + for(int i = 1; i < minParams; ++i) { + final Type fnType = Type.typeFor(type.parameterType(i + thisThisIndex)); + final Type csType = csIsVarArg ? Type.OBJECT : Type.typeFor(other.parameterType(i + 1)); + if(!fnType.isEquivalentTo(csType)) { + return false; + } + } - //Our arguments must be at least as wide as the wanted one, if not wider - if (Type.widest(w, e) != e) { - //e.g. this invocation takes double and callsite says "object". reject. won't fit - //but if invocation takes a double and callsite says "int" or "long" or "double", that's fine + // Must match any undefined parameters to Object type. + for(int i = minParams; i < fnParamCountNoCallee; ++i) { + if(!Type.typeFor(type.parameterType(i + thisThisIndex)).isEquivalentTo(Type.OBJECT)) { return false; } } - return true; // anything goes for return type, take the convenient one and it will be upcasted thru dynalink magic. + return true; + } + + private static int getParamCount(final MethodType type) { + final int paramCount = type.parameterCount(); + return type.parameterType(paramCount - 1).isArray() ? Integer.MAX_VALUE : paramCount; + } + + private boolean canBeDeoptimized() { + return optimismInfo != null; + } + + private MethodHandle createComposableInvoker(final boolean isConstructor) { + final MethodHandle handle = getInvokerOrConstructor(isConstructor); + + // If compiled function is not optimistic, it can't ever change its invoker/constructor, so just return them + // directly. + if(!canBeDeoptimized()) { + return handle; + } + + // Otherwise, we need a new level of indirection; need to introduce a mutable call site that can relink itslef + // to the compiled function's changed target whenever the optimistic assumptions are invalidated. + final CallSite cs = new MutableCallSite(handle.type()); + relinkComposableInvoker(cs, this, isConstructor); + return cs.dynamicInvoker(); + } + + private static class HandleAndAssumptions { + final MethodHandle handle; + final SwitchPoint assumptions; + + HandleAndAssumptions(final MethodHandle handle, final SwitchPoint assumptions) { + this.handle = handle; + this.assumptions = assumptions; + } + + GuardedInvocation createInvocation() { + return new GuardedInvocation(handle, assumptions); + } + } + + /** + * Returns a pair of an invocation created with a passed-in supplier and a non-invalidated switch point for + * optimistic assumptions (or null for the switch point if the function can not be deoptimized). While the method + * makes a best effort to return a non-invalidated switch point (compensating for possible deoptimizing + * recompilation happening on another thread) it is still possible that by the time this method returns the + * switchpoint has been invalidated by a {@code RewriteException} triggered on another thread for this function. + * This is not a problem, though, as these switch points are always used to produce call sites that fall back to + * relinking when they are invalidated, and in this case the execution will end up here again. What this method + * basically does is minimize such busy-loop relinking while the function is being recompiled on a different thread. + * @param invocationSupplier the supplier that constructs the actual invocation method handle; should use the + * {@code CompiledFunction} method itself in some capacity. + * @return a tuple object containing the method handle as created by the supplier and an optimistic assumptions + * switch point that is guaranteed to not have been invalidated before the call to this method (or null if the + * function can't be further deoptimized). + */ + private synchronized HandleAndAssumptions getValidOptimisticInvocation(final Supplier<MethodHandle> invocationSupplier) { + for(;;) { + final MethodHandle handle = invocationSupplier.get(); + final SwitchPoint assumptions = canBeDeoptimized() ? optimismInfo.optimisticAssumptions : null; + if(assumptions != null && assumptions.hasBeenInvalidated()) { + // We can be in a situation where one thread is in the middle of a deoptimizing compilation when we hit + // this and thus, it has invalidated the old switch point, but hasn't created the new one yet. Note that + // the behavior of invalidating the old switch point before recompilation, and only creating the new one + // after recompilation is by design. If we didn't wait here for the recompilation to complete, we would + // be busy looping through the fallback path of the invalidated switch point, relinking the call site + // again with the same invalidated switch point, invoking the fallback, etc. stealing CPU cycles from + // the recompilation task we're dependent on. This can still happen if the switch point gets invalidated + // after we grabbed it here, in which case we'll indeed do one busy relink immediately. + try { + wait(); + } catch (final InterruptedException e) { + // Intentionally ignored. There's nothing meaningful we can do if we're interrupted + } + } else { + return new HandleAndAssumptions(handle, assumptions); + } + } + } + + private static void relinkComposableInvoker(final CallSite cs, final CompiledFunction inv, final boolean constructor) { + final HandleAndAssumptions handleAndAssumptions = inv.getValidOptimisticInvocation(new Supplier<MethodHandle>() { + @Override + public MethodHandle get() { + return inv.getInvokerOrConstructor(constructor); + } + }); + final MethodHandle handle = handleAndAssumptions.handle; + final SwitchPoint assumptions = handleAndAssumptions.assumptions; + final MethodHandle target; + if(assumptions == null) { + target = handle; + } else { + final MethodHandle relink = MethodHandles.insertArguments(RELINK_COMPOSABLE_INVOKER, 0, cs, inv, constructor); + target = assumptions.guardWithTest(handle, MethodHandles.foldArguments(cs.dynamicInvoker(), relink)); + } + cs.setTarget(target.asType(cs.type())); + } + + private MethodHandle getInvokerOrConstructor(final boolean selectCtor) { + return selectCtor ? getConstructor() : createInvokerForPessimisticCaller(); + } + + /** + * Returns a guarded invocation for this function when not invoked as a constructor. The guarded invocation has no + * guard but it potentially has an optimistic assumptions switch point. As such, it will probably not be used as a + * final guarded invocation, but rather as a holder for an invocation handle and switch point to be decomposed and + * reassembled into a different final invocation by the user of this method. Any recompositions should take care to + * continue to use the switch point. If that is not possible, use {@link #createComposableInvoker()} instead. + * @return a guarded invocation for an ordinary (non-constructor) invocation of this function. + */ + GuardedInvocation createFunctionInvocation(final Class<?> callSiteReturnType, final int callerProgramPoint) { + return getValidOptimisticInvocation(new Supplier<MethodHandle>() { + @Override + public MethodHandle get() { + return createInvoker(callSiteReturnType, callerProgramPoint); + } + }).createInvocation(); + } + + /** + * Returns a guarded invocation for this function when invoked as a constructor. The guarded invocation has no guard + * but it potentially has an optimistic assumptions switch point. As such, it will probably not be used as a final + * guarded invocation, but rather as a holder for an invocation handle and switch point to be decomposed and + * reassembled into a different final invocation by the user of this method. Any recompositions should take care to + * continue to use the switch point. If that is not possible, use {@link #createComposableConstructor()} instead. + * @return a guarded invocation for invocation of this function as a constructor. + */ + GuardedInvocation createConstructorInvocation() { + return getValidOptimisticInvocation(new Supplier<MethodHandle>() { + @Override + public MethodHandle get() { + return getConstructor(); + } + }).createInvocation(); + } + + private MethodHandle createInvoker(final Class<?> callSiteReturnType, final int callerProgramPoint) { + final boolean isOptimistic = canBeDeoptimized(); + MethodHandle handleRewriteException = isOptimistic ? createRewriteExceptionHandler() : null; + + MethodHandle inv = invoker; + if(isValid(callerProgramPoint)) { + inv = OptimisticReturnFilters.filterOptimisticReturnValue(inv, callSiteReturnType, callerProgramPoint); + inv = changeReturnType(inv, callSiteReturnType); + if(callSiteReturnType.isPrimitive() && handleRewriteException != null) { + // because handleRewriteException always returns Object + handleRewriteException = OptimisticReturnFilters.filterOptimisticReturnValue(handleRewriteException, + callSiteReturnType, callerProgramPoint); + } + } else if(isOptimistic) { + // Required so that rewrite exception has the same return type. It'd be okay to do it even if we weren't + // optimistic, but it isn't necessary as the linker upstream will eventually convert the return type. + inv = changeReturnType(inv, callSiteReturnType); + } + + if(isOptimistic) { + assert handleRewriteException != null; + final MethodHandle typedHandleRewriteException = changeReturnType(handleRewriteException, inv.type().returnType()); + return MH.catchException(inv, RewriteException.class, typedHandleRewriteException); + } + return inv; + } + + private MethodHandle createRewriteExceptionHandler() { + return MH.foldArguments(RESTOF_INVOKER, MH.insertArguments(HANDLE_REWRITE_EXCEPTION, 0, this, optimismInfo)); + } + + private static MethodHandle changeReturnType(final MethodHandle mh, final Class<?> newReturnType) { + return Bootstrap.getLinkerServices().asType(mh, mh.type().changeReturnType(newReturnType)); + } + + @SuppressWarnings("unused") + private static MethodHandle handleRewriteException(final CompiledFunction function, final OptimismInfo oldOptimismInfo, final RewriteException re) { + return function.handleRewriteException(oldOptimismInfo, re); + } + + /** + * Debug function for printing out all invalidated program points and their + * invalidation mapping to next type + * @param ipp + * @return string describing the ipp map + */ + private static List<String> toStringInvalidations(final Map<Integer, Type> ipp) { + if (ipp == null) { + return Collections.emptyList(); + } + + final List<String> list = new ArrayList<>(); + + for (final Iterator<Map.Entry<Integer, Type>> iter = ipp.entrySet().iterator(); iter.hasNext(); ) { + final Map.Entry<Integer, Type> entry = iter.next(); + final char bct = entry.getValue().getBytecodeStackType(); + final String type; + + switch (entry.getValue().getBytecodeStackType()) { + case 'A': + type = "object"; + break; + case 'I': + type = "int"; + break; + case 'J': + type = "long"; + break; + case 'D': + type = "double"; + break; + default: + type = String.valueOf(bct); + break; + } + + final StringBuilder sb = new StringBuilder(); + sb.append('['). + append("program point: "). + append(entry.getKey()). + append(" -> "). + append(type). + append(']'); + + list.add(sb.toString()); + } + + return list; + } + + private void logRecompile(final String reason, final FunctionNode fn, final MethodType type, final Map<Integer, Type> ipp) { + if (log.isEnabled()) { + log.info(reason, DebugLogger.quote(fn.getName()), " signature: ", type); + log.indent(); + for (final String str : toStringInvalidations(ipp)) { + log.fine(str); + } + log.unindent(); + } + } + + /** + * Handles a {@link RewriteException} raised during the execution of this function by recompiling (if needed) the + * function with an optimistic assumption invalidated at the program point indicated by the exception, and then + * executing a rest-of method to complete the execution with the deoptimized version. + * @param oldOptInfo the optimism info of this function. We must store it explicitly as a bound argument in the + * method handle, otherwise it could be null for handling a rewrite exception in an outer invocation of a recursive + * function when recursive invocations of the function have completely deoptimized it. + * @param re the rewrite exception that was raised + * @return the method handle for the rest-of method, for folding composition. + */ + private synchronized MethodHandle handleRewriteException(final OptimismInfo oldOptInfo, final RewriteException re) { + if (log.isEnabled()) { + log.info( + new RecompilationEvent( + Level.INFO, + re, + re.getReturnValueNonDestructive()), + "caught RewriteException ", + re.getMessageShort()); + log.indent(); + } + + final MethodType type = type(); + + // Compiler needs a call site type as its input, which always has a callee parameter, so we must add it if + // this function doesn't have a callee parameter. + final MethodType ct = type.parameterType(0) == ScriptFunction.class ? + type : + type.insertParameterTypes(0, ScriptFunction.class); + final OptimismInfo currentOptInfo = optimismInfo; + final boolean shouldRecompile = currentOptInfo != null && currentOptInfo.requestRecompile(re); + + // Effective optimism info, for subsequent use. We'll normally try to use the current (latest) one, but if it + // isn't available, we'll use the old one bound into the call site. + final OptimismInfo effectiveOptInfo = currentOptInfo != null ? currentOptInfo : oldOptInfo; + FunctionNode fn = effectiveOptInfo.reparse(); + final boolean serialized = effectiveOptInfo.isSerialized(); + final Compiler compiler = effectiveOptInfo.getCompiler(fn, ct, re); //set to non rest-of + + if (!shouldRecompile) { + // It didn't necessarily recompile, e.g. for an outer invocation of a recursive function if we already + // recompiled a deoptimized version for an inner invocation. + // We still need to do the rest of from the beginning + logRecompile("Rest-of compilation [STANDALONE] ", fn, ct, effectiveOptInfo.invalidatedProgramPoints); + return restOfHandle(effectiveOptInfo, compiler.compile(fn, serialized ? CompilationPhases.COMPILE_SERIALIZED_RESTOF : CompilationPhases.COMPILE_ALL_RESTOF), currentOptInfo != null); + } + + logRecompile("Deoptimizing recompilation (up to bytecode) ", fn, ct, effectiveOptInfo.invalidatedProgramPoints); + fn = compiler.compile(fn, serialized ? CompilationPhases.RECOMPILE_SERIALIZED_UPTO_BYTECODE : CompilationPhases.COMPILE_UPTO_BYTECODE); + log.fine("Reusable IR generated"); + + // compile the rest of the function, and install it + log.info("Generating and installing bytecode from reusable IR..."); + logRecompile("Rest-of compilation [CODE PIPELINE REUSE] ", fn, ct, effectiveOptInfo.invalidatedProgramPoints); + final FunctionNode normalFn = compiler.compile(fn, CompilationPhases.GENERATE_BYTECODE_AND_INSTALL); + + if (effectiveOptInfo.data.usePersistentCodeCache()) { + final RecompilableScriptFunctionData data = effectiveOptInfo.data; + final int functionNodeId = data.getFunctionNodeId(); + final TypeMap typeMap = data.typeMap(ct); + final Type[] paramTypes = typeMap == null ? null : typeMap.getParameterTypes(functionNodeId); + final String cacheKey = CodeStore.getCacheKey(functionNodeId, paramTypes); + compiler.persistClassInfo(cacheKey, normalFn); + } + + final boolean canBeDeoptimized = normalFn.canBeDeoptimized(); + + if (log.isEnabled()) { + log.unindent(); + log.info("Done."); + + log.info("Recompiled '", fn.getName(), "' (", Debug.id(this), ") ", canBeDeoptimized ? "can still be deoptimized." : " is completely deoptimized."); + log.finest("Looking up invoker..."); + } + + final MethodHandle newInvoker = effectiveOptInfo.data.lookup(fn); + invoker = newInvoker.asType(type.changeReturnType(newInvoker.type().returnType())); + constructor = null; // Will be regenerated when needed + + log.info("Done: ", invoker); + final MethodHandle restOf = restOfHandle(effectiveOptInfo, compiler.compile(fn, CompilationPhases.GENERATE_BYTECODE_AND_INSTALL_RESTOF), canBeDeoptimized); + + // Note that we only adjust the switch point after we set the invoker/constructor. This is important. + if (canBeDeoptimized) { + effectiveOptInfo.newOptimisticAssumptions(); // Otherwise, set a new switch point. + } else { + optimismInfo = null; // If we got to a point where we no longer have optimistic assumptions, let the optimism info go. + } + notifyAll(); + + return restOf; } + private MethodHandle restOfHandle(final OptimismInfo info, final FunctionNode restOfFunction, final boolean canBeDeoptimized) { + assert info != null; + assert restOfFunction.getCompileUnit().getUnitClassName().contains("restOf"); + final MethodHandle restOf = + changeReturnType( + info.data.lookupCodeMethod( + restOfFunction.getCompileUnit().getCode(), + MH.type(restOfFunction.getReturnType().getTypeClass(), + RewriteException.class)), + Object.class); + + if (!canBeDeoptimized) { + return restOf; + } + + // If rest-of is itself optimistic, we must make sure that we can repeat a deoptimization if it, too hits an exception. + return MH.catchException(restOf, RewriteException.class, createRewriteExceptionHandler()); + + } + + private static class OptimismInfo { + // TODO: this is pointing to its owning ScriptFunctionData. Re-evaluate if that's okay. + private final RecompilableScriptFunctionData data; + private final Map<Integer, Type> invalidatedProgramPoints; + private SwitchPoint optimisticAssumptions; + private final DebugLogger log; + + OptimismInfo(final RecompilableScriptFunctionData data, final Map<Integer, Type> invalidatedProgramPoints) { + this.data = data; + this.log = data.getLogger(); + this.invalidatedProgramPoints = invalidatedProgramPoints == null ? new TreeMap<Integer, Type>() : invalidatedProgramPoints; + newOptimisticAssumptions(); + } + + private void newOptimisticAssumptions() { + optimisticAssumptions = new SwitchPoint(); + } + boolean requestRecompile(final RewriteException e) { + final Type retType = e.getReturnType(); + final Type previousFailedType = invalidatedProgramPoints.put(e.getProgramPoint(), retType); + if (previousFailedType != null && !previousFailedType.narrowerThan(retType)) { + final StackTraceElement[] stack = e.getStackTrace(); + final String functionId = stack.length == 0 ? + data.getName() : + stack[0].getClassName() + "." + stack[0].getMethodName(); + + log.info("RewriteException for an already invalidated program point ", e.getProgramPoint(), " in ", functionId, ". This is okay for a recursive function invocation, but a bug otherwise."); + + return false; + } + + SwitchPoint.invalidateAll(new SwitchPoint[] { optimisticAssumptions }); + + return true; + } + + Compiler getCompiler(final FunctionNode fn, final MethodType actualCallSiteType, final RewriteException e) { + return data.getCompiler(fn, actualCallSiteType, e.getRuntimeScope(), invalidatedProgramPoints, getEntryPoints(e)); + } + + private static int[] getEntryPoints(final RewriteException e) { + final int[] prevEntryPoints = e.getPreviousContinuationEntryPoints(); + final int[] entryPoints; + if (prevEntryPoints == null) { + entryPoints = new int[1]; + } else { + final int l = prevEntryPoints.length; + entryPoints = new int[l + 1]; + System.arraycopy(prevEntryPoints, 0, entryPoints, 1, l); + } + entryPoints[0] = e.getProgramPoint(); + return entryPoints; + } + + FunctionNode reparse() { + return data.reparse(); + } + + boolean isSerialized() { + return data.isSerialized(); + } + } + + @SuppressWarnings("unused") + private static Object newFilter(final Object result, final Object allocation) { + return (result instanceof ScriptObject || !JSType.isPrimitive(result))? result : allocation; + } + + private static MethodHandle findOwnMH(final String name, final Class<?> rtype, final Class<?>... types) { + return MH.findStatic(MethodHandles.lookup(), CompiledFunction.class, name, MH.type(rtype, types)); + } } |