/* * Copyright (c) 2014, 2015, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package org.graalvm.compiler.replacements.test; import static org.graalvm.compiler.java.BytecodeParserOptions.InlinePartialIntrinsicExitDuringParsing; import java.util.function.Function; import org.graalvm.compiler.api.directives.GraalDirectives; import org.graalvm.compiler.api.replacements.ClassSubstitution; import org.graalvm.compiler.api.replacements.MethodSubstitution; import org.graalvm.compiler.api.replacements.SnippetReflectionProvider; import org.graalvm.compiler.bytecode.BytecodeProvider; import org.graalvm.compiler.core.common.LIRKind; import org.graalvm.compiler.core.common.spi.ForeignCallDescriptor; import org.graalvm.compiler.core.common.spi.ForeignCallLinkage; import org.graalvm.compiler.core.common.spi.ForeignCallsProvider; import org.graalvm.compiler.core.common.type.Stamp; import org.graalvm.compiler.core.common.type.StampFactory; import org.graalvm.compiler.debug.DebugContext; import org.graalvm.compiler.graph.GraalGraphError; import org.graalvm.compiler.graph.Node.ConstantNodeParameter; import org.graalvm.compiler.graph.Node.NodeIntrinsic; import org.graalvm.compiler.nodes.FrameState; import org.graalvm.compiler.nodes.PiNode; import org.graalvm.compiler.nodes.StructuredGraph; import org.graalvm.compiler.nodes.ValueNode; import org.graalvm.compiler.nodes.extended.OpaqueNode; import org.graalvm.compiler.nodes.extended.ForeignCallNode; import org.graalvm.compiler.nodes.graphbuilderconf.GraphBuilderContext; import org.graalvm.compiler.nodes.graphbuilderconf.InlineInvokePlugin; import org.graalvm.compiler.nodes.graphbuilderconf.InvocationPlugin.Receiver; import org.graalvm.compiler.nodes.graphbuilderconf.InvocationPlugins; import org.graalvm.compiler.nodes.graphbuilderconf.InvocationPlugins.Registration; import org.graalvm.compiler.nodes.graphbuilderconf.NodeIntrinsicPluginFactory; import org.graalvm.compiler.nodes.spi.LoweringTool; import org.graalvm.compiler.options.OptionValues; import org.graalvm.compiler.phases.common.CanonicalizerPhase; import org.graalvm.compiler.phases.common.DeadCodeEliminationPhase; import org.graalvm.compiler.phases.common.FloatingReadPhase; import org.graalvm.compiler.phases.common.FrameStateAssignmentPhase; import org.graalvm.compiler.phases.common.GuardLoweringPhase; import org.graalvm.compiler.phases.common.LoweringPhase; import org.graalvm.compiler.phases.tiers.HighTierContext; import jdk.internal.vm.compiler.word.LocationIdentity; import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; import jdk.vm.ci.code.InstalledCode; import jdk.vm.ci.meta.JavaKind; import jdk.vm.ci.meta.ResolvedJavaMethod; /** * Tests for expected behavior when parsing snippets and intrinsics. */ public class ReplacementsParseTest extends ReplacementsTest { private static final String IN_COMPILED_HANDLER_MARKER = "*** in compiled handler ***"; /** * Marker value to indicate an exception handler was interpreted. We cannot use a complex string * expression in this context without risking non-deterministic behavior dependent on whether * String intrinsics are applied or whether String expression evaluation hit an uncommon trap * when executed by C1 or C2 (and thus potentially altering the profile such that the exception * handler is *not* compiled by Graal even when we want it to be). */ private static final String IN_INTERPRETED_HANDLER_MARKER = "*** in interpreted handler ***"; private InlineInvokePlugin.InlineInfo inlineInvokeDecision; private String inlineInvokeMethodName = null; @SuppressWarnings("serial") static class CustomError extends Error { CustomError(String message) { super(message); } } static final Object THROW_EXCEPTION_MARKER = new Object() { @Override public String toString() { return "THROW_EXCEPTION_MARKER"; } }; static int copyFirstBody(byte[] left, byte[] right, boolean left2right) { if (left2right) { byte e = left[0]; right[0] = e; return e; } else { byte e = right[0]; left[0] = e; return e; } } static int copyFirstL2RBody(byte[] left, byte[] right) { byte e = left[0]; right[0] = e; return e; } static class TestObject { static double next(double v) { return Math.nextAfter(v, 1.0); } static double next2(double v) { return Math.nextAfter(v, 1.0); } static double nextAfter(double x, double d) { return Math.nextAfter(x, d); } TestObject() { this(null); } TestObject(Object id) { this.id = id; } final Object id; String stringizeId() { Object res = id; if (res == THROW_EXCEPTION_MARKER) { // Tests exception throwing from partial intrinsification throw new CustomError("ex"); } return String.valueOf(res); } static String stringize(Object obj) { Object res = obj; if (res == THROW_EXCEPTION_MARKER) { // Tests exception throwing from partial intrinsification throw new CustomError("ex"); } return String.valueOf(res); } static String identity(String s) { return s; } /** * @see TestObjectSubstitutions#copyFirst(byte[], byte[], boolean) */ static int copyFirst(byte[] left, byte[] right, boolean left2right) { return copyFirstBody(left, right, left2right); } /** * @see TestObjectSubstitutions#copyFirstL2R(byte[], byte[]) */ static int copyFirstL2R(byte[] left, byte[] right) { return copyFirstL2RBody(left, right); } static int nonVoidIntrinsicWithCall(@SuppressWarnings("unused") int x, int y) { return y; } static int nonVoidIntrinsicWithOptimizedSplit(int x) { return x; } } @ClassSubstitution(TestObject.class) static class TestObjectSubstitutions { @MethodSubstitution(isStatic = true) static double nextAfter(double x, double d) { double xx = (x == -0.0 ? 0.0 : x); return Math.nextAfter(xx, d); } /** * Tests conditional intrinsification of a static method. */ @MethodSubstitution static String stringize(Object obj) { if (obj != null && obj.getClass() == String.class) { return asNonNullString(obj); } else { // A recursive call denotes exiting/deoptimizing // out of the partial intrinsification to the // slow/uncommon case. return stringize(obj); } } /** * Tests conditional intrinsification of a non-static method. */ @MethodSubstitution(isStatic = false) static String stringizeId(TestObject thisObj) { if (thisObj.id != null && thisObj.id.getClass() == String.class) { return asNonNullString(thisObj.id); } else { // A recursive call denotes exiting/deoptimizing // out of the partial intrinsification to the // slow/uncommon case. return outOfLinePartialIntrinsification(thisObj); } } static String outOfLinePartialIntrinsification(TestObject thisObj) { return stringizeId(thisObj); } public static String asNonNullString(Object object) { return asNonNullStringIntrinsic(object, String.class, true, true); } @NodeIntrinsic(PiNode.class) private static native String asNonNullStringIntrinsic(Object object, @ConstantNodeParameter Class toType, @ConstantNodeParameter boolean exactType, @ConstantNodeParameter boolean nonNull); /** * An valid intrinsic as the frame state associated with the merge should prevent the frame * states associated with the array stores from being associated with subsequent * deoptimizing nodes. */ @MethodSubstitution static int copyFirst(byte[] left, byte[] right, boolean left2right) { return copyFirstBody(left, right, left2right); } /** * An invalid intrinsic as the frame state associated with the array assignment can leak out * to subsequent deoptimizing nodes. */ @MethodSubstitution static int copyFirstL2R(byte[] left, byte[] right) { return copyFirstL2RBody(left, right); } /** * Tests that non-capturing lambdas are folded away. */ @MethodSubstitution static String identity(String value) { return apply(s -> s, value); } private static String apply(Function f, String value) { return f.apply(value); } @MethodSubstitution(isStatic = true) static int nonVoidIntrinsicWithCall(int x, int y) { nonVoidIntrinsicWithCallStub(x); return y; } @MethodSubstitution(isStatic = true) static int nonVoidIntrinsicWithOptimizedSplit(int x) { if (x == GraalDirectives.opaque(x)) { nonVoidIntrinsicWithCallStub(x); } return x; } public static void nonVoidIntrinsicWithCallStub(int zLen) { nonVoidIntrinsicWithCallStub(STUB_CALL, zLen); } static final ForeignCallDescriptor STUB_CALL = new ForeignCallDescriptor("stubCall", void.class, int.class); @NodeIntrinsic(ForeignCallNode.class) private static native void nonVoidIntrinsicWithCallStub(@ConstantNodeParameter ForeignCallDescriptor descriptor, int zLen); } @Override protected void registerInvocationPlugins(InvocationPlugins invocationPlugins) { BytecodeProvider replacementBytecodeProvider = getSystemClassLoaderBytecodeProvider(); Registration r = new Registration(invocationPlugins, TestObject.class, replacementBytecodeProvider); NodeIntrinsicPluginFactory.InjectionProvider injections = new DummyInjectionProvider(); new PluginFactory_ReplacementsParseTest().registerPlugins(invocationPlugins, injections); r.registerMethodSubstitution(TestObjectSubstitutions.class, "nextAfter", double.class, double.class); r.registerMethodSubstitution(TestObjectSubstitutions.class, "stringize", Object.class); r.registerMethodSubstitution(TestObjectSubstitutions.class, "stringizeId", Receiver.class); r.registerMethodSubstitution(TestObjectSubstitutions.class, "copyFirst", byte[].class, byte[].class, boolean.class); r.registerMethodSubstitution(TestObjectSubstitutions.class, "copyFirstL2R", byte[].class, byte[].class); r.registerMethodSubstitution(TestObjectSubstitutions.class, "nonVoidIntrinsicWithCall", int.class, int.class); r.registerMethodSubstitution(TestObjectSubstitutions.class, "nonVoidIntrinsicWithOptimizedSplit", int.class); if (replacementBytecodeProvider.supportsInvokedynamic()) { r.registerMethodSubstitution(TestObjectSubstitutions.class, "identity", String.class); } super.registerInvocationPlugins(invocationPlugins); } @BeforeClass public static void warmupProfiles() { for (int i = 0; i < 40000; i++) { callCopyFirst(new byte[16], new byte[16], true); callCopyFirstL2R(new byte[16], new byte[16]); } } /** * Ensure that calling the original method from the substitution binds correctly. */ @Test public void test1() { test("test1Snippet", 1.0); } public double test1Snippet(double d) { return TestObject.next(d); } /** * Ensure that calling the substitution method binds to the original method properly. */ @Test public void test2() { test("test2Snippet", 1.0); } public double test2Snippet(double d) { return TestObject.next2(d); } /** * Ensure that substitution methods with assertions in them don't complain when the exception * constructor is deleted. */ @Test public void testNextAfter() { double[] inArray = new double[1024]; double[] outArray = new double[1024]; for (int i = 0; i < inArray.length; i++) { inArray[i] = -0.0; } test("doNextAfter", inArray, outArray); } public void doNextAfter(double[] outArray, double[] inArray) { for (int i = 0; i < inArray.length; i++) { double direction = (i & 1) == 0 ? Double.POSITIVE_INFINITY : -Double.NEGATIVE_INFINITY; outArray[i] = TestObject.nextAfter(inArray[i], direction); } } private void testWithDifferentReturnValues(OptionValues options, String standardReturnValue, String compiledReturnValue, String name, Object... args) { ResolvedJavaMethod method = getResolvedJavaMethod(name); Object receiver = null; Result expect = executeExpected(method, receiver, args); Assert.assertEquals(standardReturnValue, expect.returnValue); expect = new Result(compiledReturnValue, null); testAgainstExpected(options, method, expect, receiver, args); } @Override protected InstalledCode getCode(final ResolvedJavaMethod installedCodeOwner, StructuredGraph graph, boolean forceCompile, boolean installAsDefault, OptionValues options) { return super.getCode(installedCodeOwner, graph, forceCompileOverride, installAsDefault, options); } boolean forceCompileOverride; @Test public void testCallStringize() { test("callStringize", "a string"); test("callStringize", Boolean.TRUE); // Unset 'exception seen' bit if testCallStringizeWithoutInlinePartialIntrinsicExit // is executed before this test getResolvedJavaMethod("callStringize").reprofile(); forceCompileOverride = true; String standardReturnValue = IN_INTERPRETED_HANDLER_MARKER; String compiledReturnValue = IN_COMPILED_HANDLER_MARKER; testWithDifferentReturnValues(getInitialOptions(), standardReturnValue, compiledReturnValue, "callStringize", THROW_EXCEPTION_MARKER); } @Test public void testCallStringizeWithoutInlinePartialIntrinsicExit() { OptionValues options = new OptionValues(getInitialOptions(), InlinePartialIntrinsicExitDuringParsing, false); test(options, "callStringize", "a string"); test(options, "callStringize", Boolean.TRUE); String standardReturnValue = IN_INTERPRETED_HANDLER_MARKER; String compiledReturnValue = IN_COMPILED_HANDLER_MARKER; forceCompileOverride = true; inlineInvokeDecision = InlineInvokePlugin.InlineInfo.DO_NOT_INLINE_WITH_EXCEPTION; inlineInvokeMethodName = "stringizeId"; try { testWithDifferentReturnValues(options, standardReturnValue, compiledReturnValue, "callStringize", THROW_EXCEPTION_MARKER); } finally { inlineInvokeDecision = null; inlineInvokeMethodName = null; } } @Test public void testCallStringizeId() { test("callStringizeId", new TestObject("a string")); test("callStringizeId", new TestObject(Boolean.TRUE)); // Unset 'exception seen' bit if testCallStringizeIdWithoutInlinePartialIntrinsicExit // is executed before this test getResolvedJavaMethod("callStringize").reprofile(); forceCompileOverride = true; String standardReturnValue = IN_INTERPRETED_HANDLER_MARKER; String compiledReturnValue = IN_COMPILED_HANDLER_MARKER; testWithDifferentReturnValues(getInitialOptions(), standardReturnValue, compiledReturnValue, "callStringizeId", new TestObject(THROW_EXCEPTION_MARKER)); } @Test public void testCallStringizeIdWithoutInlinePartialIntrinsicExit() { OptionValues options = new OptionValues(getInitialOptions(), InlinePartialIntrinsicExitDuringParsing, false); test(options, "callStringizeId", new TestObject("a string")); test(options, "callStringizeId", new TestObject(Boolean.TRUE)); TestObject exceptionTestObject = new TestObject(THROW_EXCEPTION_MARKER); String standardReturnValue = IN_INTERPRETED_HANDLER_MARKER; String compiledReturnValue = IN_COMPILED_HANDLER_MARKER; forceCompileOverride = true; inlineInvokeDecision = InlineInvokePlugin.InlineInfo.DO_NOT_INLINE_WITH_EXCEPTION; inlineInvokeMethodName = "stringizeId"; try { testWithDifferentReturnValues(options, standardReturnValue, compiledReturnValue, "callStringizeId", exceptionTestObject); } finally { inlineInvokeDecision = null; inlineInvokeMethodName = null; } } public static Object callStringize(Object obj) { try { return TestObject.stringize(obj); } catch (CustomError e) { if (GraalDirectives.inCompiledCode()) { return IN_COMPILED_HANDLER_MARKER; } return IN_INTERPRETED_HANDLER_MARKER; } } public static Object callStringizeId(TestObject testObj) { try { return testObj.stringizeId(); } catch (CustomError e) { if (GraalDirectives.inCompiledCode()) { return IN_COMPILED_HANDLER_MARKER; } return IN_INTERPRETED_HANDLER_MARKER; } } @Test public void testRootCompileStringize() { ResolvedJavaMethod method = getResolvedJavaMethod(TestObject.class, "stringize"); test(method, null, "a string"); test(method, null, Boolean.TRUE); test(method, null, THROW_EXCEPTION_MARKER); } @Test public void testLambda() { test("callLambda", (String) null); test("callLambda", "a string"); } public static String callLambda(String value) { return TestObject.identity(value); } public static int callCopyFirst(byte[] in, byte[] out, boolean left2right) { int res = TestObject.copyFirst(in, out, left2right); if (res == 17) { // A node after the intrinsic that needs a frame state. GraalDirectives.deoptimize(); } return res; } public static int callCopyFirstWrapper(byte[] in, byte[] out, boolean left2right) { return callCopyFirst(in, out, left2right); } public static int callCopyFirstL2R(byte[] in, byte[] out) { int res = TestObject.copyFirstL2R(in, out); if (res == 17) { // A node after the intrinsic that needs a frame state. GraalDirectives.deoptimize(); } return res; } @Test public void testCallCopyFirst() { byte[] in = {0, 1, 2, 3, 4}; byte[] out = new byte[in.length]; test("callCopyFirst", in, out, true); test("callCopyFirst", in, out, false); } @SuppressWarnings("try") @Test public void testCallCopyFirstL2R() { byte[] in = {0, 1, 2, 3, 4}; byte[] out = new byte[in.length]; try { test("callCopyFirstL2R", in, out); } catch (GraalGraphError e) { assertTrue(e.getMessage().startsWith("Invalid frame state")); } } @Override protected InlineInvokePlugin.InlineInfo bytecodeParserShouldInlineInvoke(GraphBuilderContext b, ResolvedJavaMethod method, ValueNode[] args) { if (inlineInvokeMethodName == null || inlineInvokeMethodName.equals(method.getName())) { return inlineInvokeDecision; } return null; } @Test public void testCallCopyFirstWithoutInlinePartialIntrinsicExit() { OptionValues options = new OptionValues(getInitialOptions(), InlinePartialIntrinsicExitDuringParsing, false); inlineInvokeDecision = InlineInvokePlugin.InlineInfo.DO_NOT_INLINE_WITH_EXCEPTION; try { byte[] in = {0, 1, 2, 3, 4}; byte[] out = new byte[in.length]; test(options, "callCopyFirstWrapper", in, out, true); test(options, "callCopyFirstWrapper", in, out, false); } finally { inlineInvokeDecision = null; } } public static int nonVoidIntrinsicWithCall(int x, int y) { if (TestObject.nonVoidIntrinsicWithCall(x, y) == x) { GraalDirectives.deoptimize(); } return y; } /** * This tests the case where an intrinsic ends with a runtime call but returns some kind of * value. This requires that a FrameState is available after the {@link ForeignCallNode} since * the return value must be computed on return from the call. */ @Test public void testNonVoidIntrinsicWithCall() { testGraph("nonVoidIntrinsicWithCall"); } public static int nonVoidIntrinsicWithOptimizedSplit(int x) { if (TestObject.nonVoidIntrinsicWithOptimizedSplit(x) == x) { GraalDirectives.deoptimize(); } return x; } /** * This is similar to {@link #testNonVoidIntrinsicWithCall()} but has a merge after the call * which would normally capture the {@link FrameState} but in this case we force the merge to be * optimized away. */ @Test public void testNonVoidIntrinsicWithOptimizedSplit() { testGraph("nonVoidIntrinsicWithOptimizedSplit"); } @SuppressWarnings("try") private void testGraph(String name) { StructuredGraph graph = parseEager(name, StructuredGraph.AllowAssumptions.YES); try (DebugContext.Scope s0 = graph.getDebug().scope(name, graph)) { for (OpaqueNode node : graph.getNodes().filter(OpaqueNode.class)) { node.remove(); } HighTierContext context = getDefaultHighTierContext(); CanonicalizerPhase canonicalizer = new CanonicalizerPhase(); new LoweringPhase(canonicalizer, LoweringTool.StandardLoweringStage.HIGH_TIER).apply(graph, context); new FloatingReadPhase().apply(graph); canonicalizer.apply(graph, context); new DeadCodeEliminationPhase().apply(graph); new GuardLoweringPhase().apply(graph, getDefaultMidTierContext()); new FrameStateAssignmentPhase().apply(graph); } catch (Throwable e) { throw graph.getDebug().handle(e); } } private class DummyInjectionProvider implements NodeIntrinsicPluginFactory.InjectionProvider { @SuppressWarnings("unchecked") @Override public T getInjectedArgument(Class type) { if (type == ForeignCallsProvider.class) { return (T) new ForeignCallsProvider() { @Override public LIRKind getValueKind(JavaKind javaKind) { return null; } @Override public boolean isReexecutable(ForeignCallDescriptor descriptor) { return false; } @Override public LocationIdentity[] getKilledLocations(ForeignCallDescriptor descriptor) { return new LocationIdentity[0]; } @Override public boolean canDeoptimize(ForeignCallDescriptor descriptor) { return false; } @Override public boolean isGuaranteedSafepoint(ForeignCallDescriptor descriptor) { return false; } @Override public ForeignCallLinkage lookupForeignCall(ForeignCallDescriptor descriptor) { return null; } }; } if (type == SnippetReflectionProvider.class) { return (T) getSnippetReflection(); } return null; } @Override public Stamp getInjectedStamp(Class type, boolean nonNull) { JavaKind kind = JavaKind.fromJavaClass(type); return StampFactory.forKind(kind); } } }