diff --git a/test/hotspot/jtreg/compiler/rangechecks/TestFoldCompares.java b/test/hotspot/jtreg/compiler/rangechecks/TestFoldCompares.java new file mode 100644 index 0000000000000..374c9c883d4c4 --- /dev/null +++ b/test/hotspot/jtreg/compiler/rangechecks/TestFoldCompares.java @@ -0,0 +1,345 @@ +/* + * Copyright (c) 2025, 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. + */ + +/* + * @test id=vanilla + * @bug 8346420 + * @summary Test logic in IfNode::fold_compares, which folds 2 signed comparisons + * into a single comparison. + * @library /test/lib / + * @run main ${test.main.class} vanilla + */ + +/* + * @test id=Xcomp + * @bug 8346420 + * @library /test/lib / + * @run main ${test.main.class} Xcomp + */ + +package compiler.rangechecks; + +import compiler.lib.ir_framework.*; + +/** + * This test here is here to cover some basic cases of IfNode::fold_compares. It also contains the + * reproducers for JDK-8346420. We don't do any result verification, other than that we should never + * hit an Exception. For a test with result verification, see TestFoldComparesFuzzer.java + */ +public class TestFoldCompares { + public static boolean FLAG_FALSE = false; + + public static void main(String[] args) { + TestFramework framework = new TestFramework(); + switch (args[0]) { + case "vanilla" -> { /* no extra flags */ } + case "Xcomp" -> { framework.addFlags("-Xcomp", "-XX:-TieredCompilation", "-XX:CompileCommand=compileonly,compiler.rangechecks.TestFoldCompares::test*"); } + default -> { throw new RuntimeException("Test argument not recognized: " + args[0]); } + }; + framework.start(); + } + +// TODO: // // ------------------------- Failing cases for JDK-8346420 ------------------------------ +// TODO: // +// TODO: // @Test +// TODO: // @Arguments(values = {Argument.NUMBER_42}) +// TODO: // // Reported overflow case with wrong result in JDK-8346420 +// TODO: // public static void test_Case3a_LTLE_overflow(int i) { +// TODO: // int minimum, maximum; +// TODO: // if (FLAG_FALSE) { +// TODO: // minimum = 0; +// TODO: // maximum = 1; +// TODO: // } else { +// TODO: // // Always goes to else-path +// TODO: // minimum = Integer.MIN_VALUE; +// TODO: // maximum = Integer.MAX_VALUE; +// TODO: // } +// TODO: // // i < INT_MIN || i > MAX_INT +// TODO: // // 42 < INT_MIN || 42 > MAX_INT +// TODO: // // false false +// TODO: // // => false +// TODO: // // +// TODO: // // C2 transforms this into: +// TODO: // // i - minimum >=u (maximum - minimum) + 1 +// TODO: // // 42 - INT_MIN >=u (INT_MAX - INT_MIN) + 1 +// TODO: // // 42 + MIN_INT >=u -1 + 1 +// TODO: // // ------ overflow ------- +// TODO: // // 42 + MIN_INT >=u 0 +// TODO: // // => true +// TODO: // if (i < minimum || i > maximum) { +// TODO: // throw new RuntimeException("i can never be outside [min_int, max_int]"); +// TODO: // } +// TODO: // } +// TODO: // +// TODO: // @Test +// TODO: // @Arguments(values = {Argument.NUMBER_42}) +// TODO: // // Same as test_Case3a_LTLE_overflow, just with swapped conditions (JDK-8346420). +// TODO: // public static void test_Case3b_LTLE_overflow(int i) { +// TODO: // int minimum, maximum; +// TODO: // if (FLAG_FALSE) { +// TODO: // minimum = 0; +// TODO: // maximum = 1; +// TODO: // } else { +// TODO: // // Always goes to else-path +// TODO: // minimum = Integer.MIN_VALUE; +// TODO: // maximum = Integer.MAX_VALUE; +// TODO: // } +// TODO: // if (i > maximum || i < minimum) { +// TODO: // throw new RuntimeException("i can never be outside [min_int, max_int]"); +// TODO: // } +// TODO: // } +// TODO: // +// TODO: // @Test +// TODO: // @Arguments(values = {Argument.NUMBER_42}) +// TODO: // // 22 ConI === 0 [[ 25 37 ]] #int:0 +// TODO: // // 35 ConI === 0 [[ 37 ]] #int:minint +// TODO: // // 33 ConI === 0 [[ 38 81 ]] #int:1 +// TODO: // // 37 Phi === 34 35 22 [[ 42 80 81 84 ]] #int:minint..0, 0u..maxint+1 +// TODO: // // 81 AddI === _ 37 33 [[ 82 ]] +// TODO: // // 82 Node === 81 [[ ]] <----- hook +// TODO: // // +// TODO: // // We hit this assert, also found during work for JDK-8346420: +// TODO: // // "fatal error: no reachable node should have no use" +// TODO: // // +// TODO: // // Because we compute: +// TODO: // // lo = lo + 1 +// TODO: // // hook = Node(lo) +// TODO: // // adjusted_val = i - lo +// TODO: // // -> gvn transformed to: (i - lo) + -1 +// TODO: // // -> the "lo = lo + 1" AddI now is only used by the hook, +// TODO: // // but once the hook is destroyed, it has no use any more, +// TODO: // // and we hit the assert. +// TODO: // public static void test_Case4a_LELE_assert(int i) { +// TODO: // int minimum, maximum; +// TODO: // if (FLAG_FALSE) { +// TODO: // minimum = 0; +// TODO: // maximum = 1; +// TODO: // } else { +// TODO: // minimum = Integer.MIN_VALUE; +// TODO: // maximum = Integer.MAX_VALUE; +// TODO: // } +// TODO: // if (i <= minimum || i > maximum) { +// TODO: // throw new RuntimeException("should never be reached"); +// TODO: // } +// TODO: // } + + // ------------------- IR tests to check that optimization was performed ------------------------ + + // The following tests with constant bounds are expected to fold to a single CmpU. + + @Test + @IR(counts = {IRNode.CMP_I, "= 2", IRNode.CMP_U, "= 0"}, phase = CompilePhase.AFTER_PARSING) + @IR(counts = {IRNode.CMP_I, "= 0", IRNode.CMP_U, "= 1"}) + @Arguments(values = {Argument.NUMBER_42}) + public static void test_lohi_ltle(int i) { + if (i < -100_000 || i > 100_000) { + throw new RuntimeException(); + } + } + + @Test + @IR(counts = {IRNode.CMP_I, "= 2", IRNode.CMP_U, "= 0"}, phase = CompilePhase.AFTER_PARSING) + @IR(counts = {IRNode.CMP_I, "= 0", IRNode.CMP_U, "= 1"}) + @Arguments(values = {Argument.NUMBER_42}) + public static void test_lohi_lele(int i) { + if (i <= -100_000 || i > 100_000) { + throw new RuntimeException(); + } + } + + @Test + @IR(counts = {IRNode.CMP_I, "= 2", IRNode.CMP_U, "= 0"}, phase = CompilePhase.AFTER_PARSING) + @IR(counts = {IRNode.CMP_I, "= 0", IRNode.CMP_U, "= 1"}) + @Arguments(values = {Argument.NUMBER_42}) + public static void test_lohi_ltlt(int i) { + if (i < -100_000 || i >= 100_000) { + throw new RuntimeException(); + } + } + + @Test + @IR(counts = {IRNode.CMP_I, "= 2", IRNode.CMP_U, "= 0"}, phase = CompilePhase.AFTER_PARSING) + @IR(counts = {IRNode.CMP_I, "= 0", IRNode.CMP_U, "= 1"}) + @Arguments(values = {Argument.NUMBER_42}) + public static void test_lohi_lelt(int i) { + if (i <= -100_000 || i >= 100_000) { + throw new RuntimeException(); + } + } + + @Test + @IR(counts = {IRNode.CMP_I, "= 2", IRNode.CMP_U, "= 0"}, phase = CompilePhase.AFTER_PARSING) + @IR(counts = {IRNode.CMP_I, "= 0", IRNode.CMP_U, "= 1"}) + @Arguments(values = {Argument.NUMBER_42}) + public static void test_hilo_ltle(int i) { + if (i >= 100_000 || i <= -100_000) { + throw new RuntimeException(); + } + } + + @Test + @IR(counts = {IRNode.CMP_I, "= 2", IRNode.CMP_U, "= 0"}, phase = CompilePhase.AFTER_PARSING) + @IR(counts = {IRNode.CMP_I, "= 0", IRNode.CMP_U, "= 1"}) + @Arguments(values = {Argument.NUMBER_42}) + public static void test_hilo_lele(int i) { + if (i > 100_000 || i <= -100_000) { + throw new RuntimeException(); + } + } + + @Test + @IR(counts = {IRNode.CMP_I, "= 2", IRNode.CMP_U, "= 0"}, phase = CompilePhase.AFTER_PARSING) + @IR(counts = {IRNode.CMP_I, "= 0", IRNode.CMP_U, "= 1"}) + @Arguments(values = {Argument.NUMBER_42}) + public static void test_hilo_lelt(int i) { + if (i > 100_000 || i < -100_000) { + throw new RuntimeException(); + } + } + + @Test + @IR(counts = {IRNode.CMP_I, "= 2", IRNode.CMP_U, "= 0"}, phase = CompilePhase.AFTER_PARSING) + @IR(counts = {IRNode.CMP_I, "= 0", IRNode.CMP_U, "= 1"}) + @Arguments(values = {Argument.NUMBER_42}) + public static void test_hilo_ltlt(int i) { + if (i >= 100_000 || i < -100_000) { + throw new RuntimeException(); + } + } + + // The following tests can completely remove the test and branches, we can prove that + // the path cannot be taken. + + @Setup + public static Object[] range256(SetupInfo info) { + return new Object[]{info.invocationCounter() & 255}; + } + + @Setup + public static Object[] rangeM128P127(SetupInfo info) { + return new Object[]{(info.invocationCounter() & 255) - 128}; + } + + @Test + @IR(counts = {IRNode.CMP_I, "= 2", IRNode.CMP_U, "= 0"}, phase = CompilePhase.AFTER_PARSING) + @IR(counts = {IRNode.CMP_I, "= 0", IRNode.CMP_U, "= 0"}) + @Arguments(setup = "rangeM128P127") + // Case from JDK-8135069. We used to do the CmpI->CmpU trick, but we can also constant fold + // this directly! + public static void test_empty_0(int i) { + if (i < 0 || i > -1) { + return; // always success + } + throw new RuntimeException("should not be reached"); + } + + @Test + @IR(counts = {IRNode.CMP_I, "= 2", IRNode.CMP_U, "= 0"}, phase = CompilePhase.AFTER_PARSING) + @IR(counts = {IRNode.CMP_I, "= 0", IRNode.CMP_U, "= 0"}) + @Arguments(setup = "range256") + public static void test_empty_1(int i) { + if (i < 100 || i > 50) { + return; // always success + } + throw new RuntimeException("should not be reached"); + } + + @Test + @IR(counts = {IRNode.CMP_I, "= 2", IRNode.CMP_U, "= 0"}, phase = CompilePhase.AFTER_PARSING) + @IR(counts = {IRNode.CMP_I, "= 0", IRNode.CMP_U, "= 0"}) + @Arguments(setup = "range256") + public static void test_empty_2(int i) { + if (i <= 100 || i >= 101) { + return; // always success + } + throw new RuntimeException("should not be reached"); + } + + @Test + @IR(counts = {IRNode.CMP_I, "= 1", IRNode.CMP_U, "= 0"}, phase = CompilePhase.AFTER_PARSING) + // Note: the two CmpI->Bool pairs are already canonicallized and commoned to a single pair. + @IR(counts = {IRNode.CMP_I, "= 0", IRNode.CMP_U, "= 0"}) + @Arguments(setup = "range256") + public static void test_empty_3(int i) { + if (i <= 100 || i > 100) { + return; // always success + } + throw new RuntimeException("should not be reached"); + } + + @Test + @IR(counts = {IRNode.CMP_I, "= 1", IRNode.CMP_U, "= 0"}, phase = CompilePhase.AFTER_PARSING) + // Note: the two CmpI->Bool pairs are already canonicallized and commoned to a single pair. + @IR(counts = {IRNode.CMP_I, "= 0", IRNode.CMP_U, "= 0"}) + @Arguments(setup = "range256") + public static void test_empty_4(int i) { + if (i < 101 || i >= 101) { + return; // always success + } + throw new RuntimeException("should not be reached"); + } + + @Test + @IR(counts = {IRNode.CMP_I, "= 2", IRNode.CMP_U, "= 0"}, phase = CompilePhase.AFTER_PARSING) + @IR(counts = {IRNode.CMP_I, "= 0", IRNode.CMP_U, "= 0"}) + @Arguments(setup = "range256") + public static void test_empty_5(int i) { + if (i < 101 || i > 100) { + return; // always success + } + throw new RuntimeException("should not be reached"); + } + + // Now test that we can use a.length, which means we do a null-check + // and then a comparison with a LoadRange that has type int[>=0] + + public static int[] ARR = new int[256]; + + @Test + @IR(counts = {IRNode.CMP_I, "= 2", IRNode.CMP_U, "= 0"}, phase = CompilePhase.AFTER_PARSING, + applyIf = {"TieredCompilation", "true"}) // proxy for "not Xcomp" + @IR(counts = {IRNode.CMP_I, "= 0", IRNode.CMP_U, "= 1"}, + applyIf = {"TieredCompilation", "true"}) // proxy for "not Xcomp" + @Arguments(setup = "range256") + // Note: cannot get optimized with Xcomp + static int test_array_length_and_null_check_1(int i) { + if (i < 0 || i >= ARR.length) { + return -1; // never happens + } + return i; + } + + @Test + @IR(counts = {IRNode.CMP_I, "= 2", IRNode.CMP_U, "= 0"}, phase = CompilePhase.AFTER_PARSING, + applyIf = {"TieredCompilation", "true"}) // proxy for "not Xcomp" + @IR(counts = {IRNode.CMP_I, "= 0", IRNode.CMP_U, "= 1"}, + applyIf = {"TieredCompilation", "true"}) // proxy for "not Xcomp" + @Arguments(setup = "range256") + // Note: cannot get optimized with Xcomp + static int test_array_length_and_null_check_2(int i) { + if (i < 0 || i >= ARR.length) { + throw new RuntimeException("never go out of bounds"); + } + return i; + } +} diff --git a/test/hotspot/jtreg/compiler/rangechecks/TestFoldComparesFuzzer.java b/test/hotspot/jtreg/compiler/rangechecks/TestFoldComparesFuzzer.java new file mode 100644 index 0000000000000..ba31fa42aeac7 --- /dev/null +++ b/test/hotspot/jtreg/compiler/rangechecks/TestFoldComparesFuzzer.java @@ -0,0 +1,196 @@ +/* + * Copyright (c) 2025, 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. + */ + + +/* + * @test + * @bug 8346420 + * @summary Fuzz patterns for IfNode::fold_compares_helper + * @modules java.base/jdk.internal.misc + * @library /test/lib / + * @compile ../lib/ir_framework/TestFramework.java + * @compile ../lib/generators/Generators.java + * @compile ../lib/verify/Verify.java + * @run driver ${test.main.class} + */ + +package compiler.rangechecks; + +import java.util.List; +import java.util.ArrayList; +import java.util.Random; +import java.util.Set; + +import jdk.test.lib.Utils; + +import compiler.lib.compile_framework.*; +import compiler.lib.generators.*; +import compiler.lib.template_framework.Template; +import compiler.lib.template_framework.TemplateToken; +import static compiler.lib.template_framework.Template.scope; +import static compiler.lib.template_framework.Template.let; +import static compiler.lib.template_framework.Template.$; + +import compiler.lib.template_framework.library.TestFrameworkClass; + +/** + * For more basic examples, see TestFoldCompares.java + * TODO: description + */ +public class TestFoldComparesFuzzer { + private static final Random RANDOM = Utils.getRandomInstance(); + + public static void main(String[] args) { + // Create a new CompileFramework instance. + CompileFramework comp = new CompileFramework(); + + long t0 = System.nanoTime(); + // Add a java source file. + comp.addJavaSourceCode("compiler.rangecheck.templated.Generated", generate(comp)); + + long t1 = System.nanoTime(); + // Compile the source file. + comp.compile(); + + long t2 = System.nanoTime(); + + // Run the tests without any additional VM flags. + comp.invoke("compiler.rangecheck.templated.Generated", "main", new Object[] {new String[] {}}); + long t3 = System.nanoTime(); + + System.out.println("Code Generation: " + (t1-t0) * 1e-9f); + System.out.println("Code Compilation: " + (t2-t1) * 1e-9f); + System.out.println("Running Tests: " + (t3-t2) * 1e-9f); + } + + public static String generate(CompileFramework comp) { + // Create a list to collect all tests. + List testTemplateTokens = new ArrayList<>(); + + // TODO: adjust number + for (int i = 0; i < 100; i++) { + testTemplateTokens.add(generateTest()); + } + + // Create the test class, which runs all testTemplateTokens. + return TestFrameworkClass.render( + // package and class name. + "compiler.rangecheck.templated", "Generated", + // List of imports. + Set.of("compiler.lib.generators.*", + "compiler.lib.verify.*", + "java.util.Random", + "jdk.test.lib.Utils"), + // classpath, so the Test VM has access to the compiled class files. + comp.getEscapedClassPathOfCompiledClasses(), + // The list of tests. + testTemplateTokens); + } + + public static TemplateToken generateTest() { + RestrictableGenerator gen = Generators.G.ints(); + final int N_HI = gen.next(); + final int N_LO = gen.next(); + final int A_HI = gen.next(); + final int A_LO = gen.next(); + final int B_HI = gen.next(); + final int B_LO = gen.next(); + + // TODO: brainstorming + // + // - All permutations of tests. All comparisons. + // - Cases where we are always in/out / mixed. + // - Cases with array length. + // - Cases with switch + // - limits: constant, range, array.length + // - type: int and long + var testMethodTemplate = Template.make("methodName", (String methodName) -> scope( + let("N_HI", N_HI), + let("N_LO", N_LO), + let("A_HI", A_HI), + let("A_LO", A_LO), + let("B_HI", B_HI), + let("B_LO", B_LO), + """ + static boolean #methodName(int n, int a, int b) { + //n = Math.min(#N_HI, Math.max(#N_LO, n)); + //a = Math.min(#A_HI, Math.max(#A_LO, a)); + //b = Math.min(#B_HI, Math.max(#B_LO, b)); + + if (a > b) { + a = #A_LO; //0; + b = #B_LO; //1; + } else { + a = #A_HI; //Integer.MIN_VALUE; + b = #B_HI; //Integer.MAX_VALUE; + } + + if (n < a || n > b) { + return true; + } + return false; + } + """ + )); + + // TODO: Xcomp or not? + var testTemplate = Template.make(() -> scope( + """ + // --- $test start --- + @Run(test = "$test") + @Warmup(0) // like XComp + public static void $run() { + for (int i = 0; i < 100; i++) { + // Generate random values. + RestrictableGenerator gen = Generators.G.ints(); + int n = gen.next(); + int a = gen.next(); + int b = gen.next(); + + // Run test and compare with interpreter results. + var result = $test(n, a, b); + var expected = $reference(n, a, b); + if (result != expected) { + throw new RuntimeException("wrong result: " + result + " vs " + expected + + "\\nn: " + n + + "\\na: " + a + + "\\nb: " + b); + } + } + } + + @Test + """, + testMethodTemplate.asToken($("test")), + """ + + @DontCompile + """, + testMethodTemplate.asToken($("reference")), + """ + // --- $test end --- + """ + )); + return testTemplate.asToken(); + } +}