package org.elasticsearch.painless; import org.elasticsearch.common.SuppressForbidden; /* * Licensed to Elasticsearch under one or more contributor * license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright * ownership. Elasticsearch licenses this file to you under * the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import java.lang.invoke.CallSite; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodHandles.Lookup; import java.lang.invoke.MethodType; import java.lang.invoke.MutableCallSite; import java.lang.invoke.WrongMethodTypeException; /** * Painless invokedynamic bootstrap for the call site. * <p> * Has 11 flavors (passed as static bootstrap parameters): dynamic method call, * dynamic field load (getter), and dynamic field store (setter), dynamic array load, * dynamic array store, iterator, method reference, unary operator, binary operator, * shift operator, and dynamic array index normalize. * <p> * When a new type is encountered at the call site, we lookup from the appropriate * whitelist, and cache with a guard. If we encounter too many types, we stop caching. * <p> * Based on the cascaded inlining cache from the JSR 292 cookbook * (https://code.google.com/archive/p/jsr292-cookbook/, BSD license) */ // NOTE: this class must be public, because generated painless classes are in a different classloader, // and it needs to be accessible by that code. public final class DefBootstrap { private DefBootstrap() {} // no instance! // NOTE: these must be primitive types, see https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.invokedynamic /** static bootstrap parameter indicating a dynamic method call, e.g. foo.bar(...) */ public static final int METHOD_CALL = 0; /** static bootstrap parameter indicating a dynamic load (getter), e.g. baz = foo.bar */ public static final int LOAD = 1; /** static bootstrap parameter indicating a dynamic store (setter), e.g. foo.bar = baz */ public static final int STORE = 2; /** static bootstrap parameter indicating a dynamic array load, e.g. baz = foo[bar] */ public static final int ARRAY_LOAD = 3; /** static bootstrap parameter indicating a dynamic array store, e.g. foo[bar] = baz */ public static final int ARRAY_STORE = 4; /** static bootstrap parameter indicating a dynamic iteration, e.g. for (x : y) */ public static final int ITERATOR = 5; /** static bootstrap parameter indicating a dynamic method reference, e.g. foo::bar */ public static final int REFERENCE = 6; /** static bootstrap parameter indicating a unary math operator, e.g. ~foo */ public static final int UNARY_OPERATOR = 7; /** static bootstrap parameter indicating a binary math operator, e.g. foo / bar */ public static final int BINARY_OPERATOR = 8; /** static bootstrap parameter indicating a shift operator, e.g. foo >> bar */ public static final int SHIFT_OPERATOR = 9; /** static bootstrap parameter indicating a request to normalize an index for array-like-access */ public static final int INDEX_NORMALIZE = 10; // constants for the flags parameter of operators /** * static bootstrap parameter indicating the binary operator allows nulls (e.g. == and +) * <p> * requires additional {@link MethodHandles#catchException} guard, which will invoke * the fallback if a null is encountered. */ public static final int OPERATOR_ALLOWS_NULL = 1 << 0; /** * static bootstrap parameter indicating the binary operator is part of compound assignment (e.g. +=). * <p> * may require {@link MethodHandles#explicitCastArguments}, or a dynamic cast * to cast back to the receiver's type, depending on types seen. */ public static final int OPERATOR_COMPOUND_ASSIGNMENT = 1 << 1; /** * static bootstrap parameter indicating an explicit cast to the return type. * <p> * may require {@link MethodHandles#explicitCastArguments}, depending on types seen. */ public static final int OPERATOR_EXPLICIT_CAST = 1 << 2; /** * CallSite that implements the polymorphic inlining cache (PIC). */ static final class PIC extends MutableCallSite { /** maximum number of types before we go megamorphic */ static final int MAX_DEPTH = 5; private final Definition definition; private final Lookup lookup; private final String name; private final int flavor; private final Object[] args; int depth; // pkg-protected for testing PIC(Definition definition, Lookup lookup, String name, MethodType type, int initialDepth, int flavor, Object[] args) { super(type); if (type.parameterType(0) != Object.class) { throw new BootstrapMethodError("The receiver type (1st arg) of invokedynamic descriptor must be Object."); } this.definition = definition; this.lookup = lookup; this.name = name; this.flavor = flavor; this.args = args; this.depth = initialDepth; MethodHandle fallback = FALLBACK.bindTo(this) .asCollector(Object[].class, type.parameterCount()) .asType(type); setTarget(fallback); } /** * guard method for inline caching: checks the receiver's class is the same * as the cached class */ static boolean checkClass(Class<?> clazz, Object receiver) { return receiver.getClass() == clazz; } /** * Does a slow lookup against the whitelist. */ private MethodHandle lookup(int flavor, String name, Class<?> receiver) throws Throwable { switch(flavor) { case METHOD_CALL: return Def.lookupMethod(definition, lookup, type(), receiver, name, args); case LOAD: return Def.lookupGetter(definition, receiver, name); case STORE: return Def.lookupSetter(definition, receiver, name); case ARRAY_LOAD: return Def.lookupArrayLoad(receiver); case ARRAY_STORE: return Def.lookupArrayStore(receiver); case ITERATOR: return Def.lookupIterator(receiver); case REFERENCE: return Def.lookupReference(definition, lookup, (String) args[0], receiver, name); case INDEX_NORMALIZE: return Def.lookupIndexNormalize(receiver); default: throw new AssertionError(); } } /** * Creates the {@link MethodHandle} for the megamorphic call site * using {@link ClassValue} and {@link MethodHandles#exactInvoker(MethodType)}: */ private MethodHandle createMegamorphicHandle() { final MethodType type = type(); final ClassValue<MethodHandle> megamorphicCache = new ClassValue<MethodHandle>() { @Override protected MethodHandle computeValue(Class<?> receiverType) { // it's too stupid that we cannot throw checked exceptions... (use rethrow puzzler): try { return lookup(flavor, name, receiverType).asType(type); } catch (Throwable t) { Def.rethrow(t); throw new AssertionError(); } } }; return MethodHandles.foldArguments(MethodHandles.exactInvoker(type), MEGAMORPHIC_LOOKUP.bindTo(megamorphicCache)); } /** * Called when a new type is encountered (or, when we have encountered more than {@code MAX_DEPTH} * types at this call site and given up on caching using this fallback and we switch to a * megamorphic cache using {@link ClassValue}). */ @SuppressForbidden(reason = "slow path") Object fallback(final Object[] callArgs) throws Throwable { if (depth >= MAX_DEPTH) { // we revert the whole cache and build a new megamorphic one final MethodHandle target = this.createMegamorphicHandle(); setTarget(target); return target.invokeWithArguments(callArgs); } else { final Class<?> receiver = callArgs[0].getClass(); final MethodHandle target = lookup(flavor, name, receiver).asType(type()); MethodHandle test = CHECK_CLASS.bindTo(receiver); MethodHandle guard = MethodHandles.guardWithTest(test, target, getTarget()); depth++; setTarget(guard); return target.invokeWithArguments(callArgs); } } private static final MethodHandle CHECK_CLASS; private static final MethodHandle FALLBACK; private static final MethodHandle MEGAMORPHIC_LOOKUP; static { final Lookup lookup = MethodHandles.lookup(); final Lookup publicLookup = MethodHandles.publicLookup(); try { CHECK_CLASS = lookup.findStatic(lookup.lookupClass(), "checkClass", MethodType.methodType(boolean.class, Class.class, Object.class)); FALLBACK = lookup.findVirtual(lookup.lookupClass(), "fallback", MethodType.methodType(Object.class, Object[].class)); MethodHandle mh = publicLookup.findVirtual(ClassValue.class, "get", MethodType.methodType(Object.class, Class.class)); mh = MethodHandles.filterArguments(mh, 1, publicLookup.findVirtual(Object.class, "getClass", MethodType.methodType(Class.class))); MEGAMORPHIC_LOOKUP = mh.asType(mh.type().changeReturnType(MethodHandle.class)); } catch (ReflectiveOperationException e) { throw new AssertionError(e); } } } /** * CallSite that implements the monomorphic inlining cache (for operators). */ static final class MIC extends MutableCallSite { private boolean initialized; private final String name; private final int flavor; private final int flags; MIC(String name, MethodType type, int initialDepth, int flavor, int flags) { super(type); this.name = name; this.flavor = flavor; this.flags = flags; if (initialDepth > 0) { initialized = true; } MethodHandle fallback = FALLBACK.bindTo(this) .asCollector(Object[].class, type.parameterCount()) .asType(type); setTarget(fallback); } /** * Does a slow lookup for the operator */ private MethodHandle lookup(Object[] args) throws Throwable { switch(flavor) { case UNARY_OPERATOR: case SHIFT_OPERATOR: // shifts are treated as unary, as java allows long arguments without a cast (but bits are ignored) MethodHandle unary = DefMath.lookupUnary(args[0].getClass(), name); if ((flags & OPERATOR_EXPLICIT_CAST) != 0) { unary = DefMath.cast(type().returnType(), unary); } else if ((flags & OPERATOR_COMPOUND_ASSIGNMENT) != 0) { unary = DefMath.cast(args[0].getClass(), unary); } return unary; case BINARY_OPERATOR: if (args[0] == null || args[1] == null) { return lookupGeneric(); // can handle nulls, casts if supported } else { MethodHandle binary = DefMath.lookupBinary(args[0].getClass(), args[1].getClass(), name); if ((flags & OPERATOR_EXPLICIT_CAST) != 0) { binary = DefMath.cast(type().returnType(), binary); } else if ((flags & OPERATOR_COMPOUND_ASSIGNMENT) != 0) { binary = DefMath.cast(args[0].getClass(), binary); } return binary; } default: throw new AssertionError(); } } private MethodHandle lookupGeneric() { MethodHandle target = DefMath.lookupGeneric(name); if ((flags & OPERATOR_EXPLICIT_CAST) != 0) { // static cast to the return type target = DefMath.dynamicCast(target, type().returnType()); } else if ((flags & OPERATOR_COMPOUND_ASSIGNMENT) != 0) { // dynamic cast to the receiver's type target = DefMath.dynamicCast(target); } return target; } /** * Called when a new type is encountered or if cached type does not match. * In that case we revert to a generic, but slower operator handling. */ @SuppressForbidden(reason = "slow path") Object fallback(Object[] args) throws Throwable { if (initialized) { // caching defeated MethodHandle generic = lookupGeneric(); setTarget(generic.asType(type())); return generic.invokeWithArguments(args); } final MethodType type = type(); MethodHandle target = lookup(args); // for math operators: WrongMethodType can be confusing. convert into a ClassCastException if they screw up. try { target = target.asType(type); } catch (WrongMethodTypeException e) { Exception exc = new ClassCastException("Cannot cast from: " + target.type().returnType() + " to " + type.returnType()); exc.initCause(e); throw exc; } final MethodHandle test; if (flavor == BINARY_OPERATOR || flavor == SHIFT_OPERATOR) { // some binary operators support nulls, we handle them separate Class<?> clazz0 = args[0] == null ? null : args[0].getClass(); Class<?> clazz1 = args[1] == null ? null : args[1].getClass(); if (type.parameterType(1) != Object.class) { // case 1: only the receiver is unknown, just check that MethodHandle unaryTest = CHECK_LHS.bindTo(clazz0); test = unaryTest.asType(unaryTest.type() .changeParameterType(0, type.parameterType(0))); } else if (type.parameterType(0) != Object.class) { // case 2: only the argument is unknown, just check that MethodHandle unaryTest = CHECK_RHS.bindTo(clazz0).bindTo(clazz1); test = unaryTest.asType(unaryTest.type() .changeParameterType(0, type.parameterType(0)) .changeParameterType(1, type.parameterType(1))); } else { // case 3: check both receiver and argument MethodHandle binaryTest = CHECK_BOTH.bindTo(clazz0).bindTo(clazz1); test = binaryTest.asType(binaryTest.type() .changeParameterType(0, type.parameterType(0)) .changeParameterType(1, type.parameterType(1))); } } else { // unary operator MethodHandle receiverTest = CHECK_LHS.bindTo(args[0].getClass()); test = receiverTest.asType(receiverTest.type() .changeParameterType(0, type.parameterType(0))); } MethodHandle guard = MethodHandles.guardWithTest(test, target, getTarget()); // very special cases, where even the receiver can be null (see JLS rules for string concat) // we wrap + with an NPE catcher, and use our generic method in that case. if (flavor == BINARY_OPERATOR && (flags & OPERATOR_ALLOWS_NULL) != 0) { MethodHandle handler = MethodHandles.dropArguments(lookupGeneric().asType(type()), 0, NullPointerException.class); guard = MethodHandles.catchException(guard, NullPointerException.class, handler); } initialized = true; setTarget(guard); return target.invokeWithArguments(args); } /** * guard method for inline caching: checks the receiver's class is the same * as the cached class */ static boolean checkLHS(Class<?> clazz, Object leftObject) { return leftObject.getClass() == clazz; } /** * guard method for inline caching: checks the first argument is the same * as the cached first argument. */ static boolean checkRHS(Class<?> left, Class<?> right, Object leftObject, Object rightObject) { return rightObject.getClass() == right; } /** * guard method for inline caching: checks the receiver's class and the first argument * are the same as the cached receiver and first argument. */ static boolean checkBoth(Class<?> left, Class<?> right, Object leftObject, Object rightObject) { return leftObject.getClass() == left && rightObject.getClass() == right; } private static final MethodHandle CHECK_LHS; private static final MethodHandle CHECK_RHS; private static final MethodHandle CHECK_BOTH; private static final MethodHandle FALLBACK; static { final Lookup lookup = MethodHandles.lookup(); try { CHECK_LHS = lookup.findStatic(lookup.lookupClass(), "checkLHS", MethodType.methodType(boolean.class, Class.class, Object.class)); CHECK_RHS = lookup.findStatic(lookup.lookupClass(), "checkRHS", MethodType.methodType(boolean.class, Class.class, Class.class, Object.class, Object.class)); CHECK_BOTH = lookup.findStatic(lookup.lookupClass(), "checkBoth", MethodType.methodType(boolean.class, Class.class, Class.class, Object.class, Object.class)); FALLBACK = lookup.findVirtual(lookup.lookupClass(), "fallback", MethodType.methodType(Object.class, Object[].class)); } catch (ReflectiveOperationException e) { throw new AssertionError(e); } } } /** * invokeDynamic bootstrap method * <p> * In addition to ordinary parameters, we also take some parameters defined at the call site: * <ul> * <li>{@code initialDepth}: initial call site depth. this is used to exercise megamorphic fallback. * <li>{@code flavor}: type of dynamic call it is (and which part of whitelist to look at). * <li>{@code args}: flavor-specific args. * </ul> * And we take the {@link Definition} used to compile the script for whitelist checking. * <p> * see https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.invokedynamic */ public static CallSite bootstrap(Definition definition, Lookup lookup, String name, MethodType type, int initialDepth, int flavor, Object... args) { // validate arguments switch(flavor) { // "function-call" like things get a polymorphic cache case METHOD_CALL: if (args.length == 0) { throw new BootstrapMethodError("Invalid number of parameters for method call"); } if (args[0] instanceof String == false) { throw new BootstrapMethodError("Illegal parameter for method call: " + args[0]); } String recipe = (String) args[0]; int numLambdas = recipe.length(); if (numLambdas > type.parameterCount()) { throw new BootstrapMethodError("Illegal recipe for method call: too many bits"); } if (args.length != numLambdas + 1) { throw new BootstrapMethodError("Illegal number of parameters: expected " + numLambdas + " references"); } return new PIC(definition, lookup, name, type, initialDepth, flavor, args); case LOAD: case STORE: case ARRAY_LOAD: case ARRAY_STORE: case ITERATOR: case INDEX_NORMALIZE: if (args.length > 0) { throw new BootstrapMethodError("Illegal static bootstrap parameters for flavor: " + flavor); } return new PIC(definition, lookup, name, type, initialDepth, flavor, args); case REFERENCE: if (args.length != 1) { throw new BootstrapMethodError("Invalid number of parameters for reference call"); } if (args[0] instanceof String == false) { throw new BootstrapMethodError("Illegal parameter for reference call: " + args[0]); } return new PIC(definition, lookup, name, type, initialDepth, flavor, args); // operators get monomorphic cache, with a generic impl for a fallback case UNARY_OPERATOR: case SHIFT_OPERATOR: case BINARY_OPERATOR: if (args.length != 1) { throw new BootstrapMethodError("Invalid number of parameters for operator call"); } if (args[0] instanceof Integer == false) { throw new BootstrapMethodError("Illegal parameter for reference call: " + args[0]); } int flags = (int)args[0]; if ((flags & OPERATOR_ALLOWS_NULL) != 0 && flavor != BINARY_OPERATOR) { // we just don't need it anywhere else. throw new BootstrapMethodError("This parameter is only supported for BINARY_OPERATORs"); } if ((flags & OPERATOR_COMPOUND_ASSIGNMENT) != 0 && flavor != BINARY_OPERATOR && flavor != SHIFT_OPERATOR) { // we just don't need it anywhere else. throw new BootstrapMethodError("This parameter is only supported for BINARY/SHIFT_OPERATORs"); } return new MIC(name, type, initialDepth, flavor, flags); default: throw new BootstrapMethodError("Illegal static bootstrap parameter for flavor: " + flavor); } } }