From 0126624c854d892985156d0675551e6704887fb1 Mon Sep 17 00:00:00 2001 From: Emanuel Peter Date: Fri, 5 Jun 2026 08:20:41 +0200 Subject: [PATCH 01/44] JDK-8385855 --- .../loopopts/TestHasTruncationWrap.java | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java diff --git a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java new file mode 100644 index 0000000000000..4c4af74bf0ca3 --- /dev/null +++ b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2026, 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 8385855 + * @summary Test CountedLoopConverter::has_truncation_wrap logic that checks if + * a truncated iv (e.g. byte or char iv) is still a valid counted loop. + * @library /test/lib / + * @run main ${test.main.class} + */ + +/* + * @test id=Xcomp + * @bug 8385855 + * @library /test/lib / + * @run main ${test.main.class} -Xcomp -XX:-TieredCompilation -XX:CompileCommand=compileonly,${test.main.class}::test* + */ + +package compiler.loopopts; + +import compiler.lib.ir_framework.*; + +/** + * TODO: descr + */ +public class TestHasTruncationWrap { + + public static void main(String[] args) { + TestFramework framework = new TestFramework(); + framework.addFlags(args); + framework.start(); + } + + // ------------------------- Failing cases for JDK-8385855 ------------------------------ + + // Test shape first reported in JDK-8385855, led to assert in JDK27: + // assert(cmp->Opcode() == Op_CmpI) failed: signed comparison required + public static int test0_start = 0; + public static int test0_stop = 100; + public static int[] test0_array = new int[100]; + + @Test + public static void test0() { + int start = test0_start; + int stop = test0_stop; + int[] array = test0_array; + + stop = (stop << 16) >> 16; + int v = array[start]; // dominating CmpU detected by filtered_int_type + for (int i = start; i < stop;) { + i++; + i = (i << 16) >> 16; // iv truncation + } + } + + // ---- More general tests, Checking that truncated iv loops become CountedLoops --------- + + // TODO: replace with real test! + //@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(); + // } + //} +} From a80c98916471682f35f44a3ea6832717b3baaa17 Mon Sep 17 00:00:00 2001 From: Emanuel Peter Date: Tue, 9 Jun 2026 15:30:36 +0200 Subject: [PATCH 02/44] add TestX1 --- src/hotspot/share/opto/ifnode.cpp | 3 +- .../loopopts/TestHasTruncationWrap.java | 93 ++++++++++++++++--- 2 files changed, 80 insertions(+), 16 deletions(-) diff --git a/src/hotspot/share/opto/ifnode.cpp b/src/hotspot/share/opto/ifnode.cpp index 347d63ef57cfc..573a62ad3a088 100644 --- a/src/hotspot/share/opto/ifnode.cpp +++ b/src/hotspot/share/opto/ifnode.cpp @@ -671,7 +671,8 @@ const TypeInt* IfNode::filtered_int_type(PhaseGVN* gvn, Node* val, Node* if_proj const CmpNode* cmp = bol->in(1)->as_Cmp(); // Val is always the lhs of the comparision: val cmp2 if (cmp->in(1) == val) { - assert(cmp->Opcode() == Op_CmpI, "signed comparison required"); + // TODO: rm + //assert(cmp->Opcode() == Op_CmpI, "signed comparison required"); const TypeInt* cmp2_t = gvn->type(cmp->in(2))->isa_int(); if (cmp2_t != nullptr) { jint lo = cmp2_t->_lo; diff --git a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java index 4c4af74bf0ca3..5200d5160ca44 100644 --- a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java +++ b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java @@ -54,24 +54,87 @@ public static void main(String[] args) { // ------------------------- Failing cases for JDK-8385855 ------------------------------ - // Test shape first reported in JDK-8385855, led to assert in JDK27: - // assert(cmp->Opcode() == Op_CmpI) failed: signed comparison required - public static int test0_start = 0; - public static int test0_stop = 100; - public static int[] test0_array = new int[100]; + // TODO: add back in + // // Test shape first reported in JDK-8385855, led to assert in JDK27: + // // assert(cmp->Opcode() == Op_CmpI) failed: signed comparison required + // public static int test0_start = 0; + // public static int test0_stop = 100; + // public static int[] test0_array = new int[100]; + + // @Test + // public static void test0() { + // int start = test0_start; + // int stop = test0_stop; + // int[] array = test0_array; + + // stop = (stop << 16) >> 16; + // int v = array[start]; // dominating CmpU detected by filtered_int_type + // for (int i = start; i < stop;) { + // i++; + // i = (i << 16) >> 16; // iv truncation + // } + // } + + + // A second reproducer from JDK-8385855, leads to wrong result since JDK18 (JDK-8276162). + public static int test1_gold0 = test1(-2); + public static int test1_gold1 = test1(2); + + @Run(test = "test1") + private static void run1() { + int val0 = test1(-2); + int val1 = test1( 2); + if (val0 != test1_gold0) { throw new RuntimeException("wrong value test( 2): " + test1_gold0 + " vs " + val0); } + if (val1 != test1_gold1) { throw new RuntimeException("wrong value test(-2): " + test1_gold1 + " vs " + val1); } + } @Test - public static void test0() { - int start = test0_start; - int stop = test0_stop; - int[] array = test0_array; - - stop = (stop << 16) >> 16; - int v = array[start]; // dominating CmpU detected by filtered_int_type - for (int i = start; i < stop;) { - i++; - i = (i << 16) >> 16; // iv truncation + private static int test1(int start) { + // CmpU Condition: start =u 2 + // But filtered_int_type mistakes it as a CmpI. + // Bad CmpU assumption: start >= 2 + + int i = start; + while (i < 3) { + // While condition: i <= 2 + + // char-truncation of iv: has_truncation_wrap + // We try to see if the char-truncation can be removed. + // + // Computing loop entry type: + // While condition: i <= 2 + // Bad assumption from CmpU: start >= 2 + // -> entry type i = 2 + // + // Together with the backedge type, we get the complete phi type: + // i in [1..2] + // + // The truncation below would be a no-op for input ranges [0 .. 32767]. + // Since [1..2] is a subrange: remove truncation! + // + // But: the correct CmpU assumption would only be: + // start >=u 2 + // And that allows almost all values (except 0 and 1), in particular + // it allows the whole negative int range. + // And the while condition also allows all negative ints. + // And for negative ints, the truncation is NOT a no-op. + i = (i + 1) & 0x7fff; + + // Continuing after the backedge would mean: + // i >= 1 + // Together with while condition: + // i <= 2 + // We get a backedge type: + // i in [1..2] + if (i < 1) { + break; + } } + return i; } // ---- More general tests, Checking that truncated iv loops become CountedLoops --------- From fa75fe9b88ca38c3e62eada054cc8148f5c42fd2 Mon Sep 17 00:00:00 2001 From: Emanuel Peter Date: Tue, 9 Jun 2026 15:39:18 +0200 Subject: [PATCH 03/44] TestX2 --- .../loopopts/TestHasTruncationWrap.java | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java index 5200d5160ca44..de0a2d1c3b1ed 100644 --- a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java +++ b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java @@ -75,8 +75,8 @@ public static void main(String[] args) { // } // } - // A second reproducer from JDK-8385855, leads to wrong result since JDK18 (JDK-8276162). + // We make use of the CmpU via Integer.compareUnsigned, introduced by JDK-8276162. public static int test1_gold0 = test1(-2); public static int test1_gold1 = test1(2); @@ -137,6 +137,40 @@ private static int test1(int start) { return i; } + // A third reproducer from JDK-8385855, leads to wrong result since 6u. + // We make use of the CmpU in the RangeCheck of an array access. + // To flip the condition, we just use a try/catch. + static final int[] test2_A = new int[2]; + public static int test2_gold0 = test2(-2); + public static int test2_gold1 = test2(2); + + @Run(test = "test2") + private static void run2() { + int val0 = test2(-2); + int val1 = test2( 2); + if (val0 != test2_gold0) { throw new RuntimeException("wrong value test( 2): " + test2_gold0 + " vs " + val0); } + if (val1 != test2_gold1) { throw new RuntimeException("wrong value test(-2): " + test2_gold1 + " vs " + val1); } + } + + @Test + static int test2(int start) { + try { + // CmpU Condition: start =u A.length = 2 + int i = start; + while (i < 3) { + // Truncating induction-variable update. + i = (i + 1) & 0x7fff; + if (i < 1) { + break; + } + } + return i; + } + } + // ---- More general tests, Checking that truncated iv loops become CountedLoops --------- // TODO: replace with real test! From a46bebb4e64e8627fb345195ce5280ac1bd2b900 Mon Sep 17 00:00:00 2001 From: Emanuel Peter Date: Tue, 9 Jun 2026 16:16:11 +0200 Subject: [PATCH 04/44] fix and more testing --- src/hotspot/share/opto/ifnode.cpp | 8 +++- .../loopopts/TestHasTruncationWrap.java | 46 +++++++++++-------- 2 files changed, 32 insertions(+), 22 deletions(-) diff --git a/src/hotspot/share/opto/ifnode.cpp b/src/hotspot/share/opto/ifnode.cpp index 573a62ad3a088..41ca97ef8ecab 100644 --- a/src/hotspot/share/opto/ifnode.cpp +++ b/src/hotspot/share/opto/ifnode.cpp @@ -671,8 +671,12 @@ const TypeInt* IfNode::filtered_int_type(PhaseGVN* gvn, Node* val, Node* if_proj const CmpNode* cmp = bol->in(1)->as_Cmp(); // Val is always the lhs of the comparision: val cmp2 if (cmp->in(1) == val) { - // TODO: rm - //assert(cmp->Opcode() == Op_CmpI, "signed comparison required"); + if (cmp->Opcode() != Op_CmpI) { + // Only CmpI allowed, assumed by signed logic below. + // We could extend to CmpU in the future, and would + // have to implment unsigned range logic below. + return nullptr; + } const TypeInt* cmp2_t = gvn->type(cmp->in(2))->isa_int(); if (cmp2_t != nullptr) { jint lo = cmp2_t->_lo; diff --git a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java index de0a2d1c3b1ed..b573bb131d197 100644 --- a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java +++ b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java @@ -54,26 +54,25 @@ public static void main(String[] args) { // ------------------------- Failing cases for JDK-8385855 ------------------------------ - // TODO: add back in - // // Test shape first reported in JDK-8385855, led to assert in JDK27: - // // assert(cmp->Opcode() == Op_CmpI) failed: signed comparison required - // public static int test0_start = 0; - // public static int test0_stop = 100; - // public static int[] test0_array = new int[100]; - - // @Test - // public static void test0() { - // int start = test0_start; - // int stop = test0_stop; - // int[] array = test0_array; - - // stop = (stop << 16) >> 16; - // int v = array[start]; // dominating CmpU detected by filtered_int_type - // for (int i = start; i < stop;) { - // i++; - // i = (i << 16) >> 16; // iv truncation - // } - // } + // Test shape first reported in JDK-8385855, led to assert in JDK27: + // assert(cmp->Opcode() == Op_CmpI) failed: signed comparison required + public static int test0_start = 0; + public static int test0_stop = 100; + public static int[] test0_array = new int[100]; + + @Test + public static void test0() { + int start = test0_start; + int stop = test0_stop; + int[] array = test0_array; + + stop = (stop << 16) >> 16; + int v = array[start]; // dominating CmpU detected by filtered_int_type + for (int i = start; i < stop;) { + i++; + i = (i << 16) >> 16; // iv truncation + } + } // A second reproducer from JDK-8385855, leads to wrong result since JDK18 (JDK-8276162). // We make use of the CmpU via Integer.compareUnsigned, introduced by JDK-8276162. @@ -173,6 +172,13 @@ static int test2(int start) { // ---- More general tests, Checking that truncated iv loops become CountedLoops --------- + // TODO: ensure coverage + // - char, byte and short truncation + // - check for IRNode.COUNTED_LOOP + // - dontinline call to prevent empty loop + // - increment and decrement cases + // - Cases with and without compare before loop: positive and negative tests + // TODO: replace with real test! //@Test //@IR(counts = {IRNode.CMP_I, "= 2", IRNode.CMP_U, "= 0"}, phase = CompilePhase.AFTER_PARSING) From d6ac06c5144ad4a3ece38170800d375f6b399d24 Mon Sep 17 00:00:00 2001 From: Emanuel Peter Date: Tue, 9 Jun 2026 17:31:50 +0200 Subject: [PATCH 05/44] some more tests --- .../loopopts/TestHasTruncationWrap.java | 53 ++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java index b573bb131d197..548c52be37b55 100644 --- a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java +++ b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java @@ -139,7 +139,7 @@ private static int test1(int start) { // A third reproducer from JDK-8385855, leads to wrong result since 6u. // We make use of the CmpU in the RangeCheck of an array access. // To flip the condition, we just use a try/catch. - static final int[] test2_A = new int[2]; + public static final int[] test2_A = new int[2]; public static int test2_gold0 = test2(-2); public static int test2_gold1 = test2(2); @@ -172,6 +172,57 @@ static int test2(int start) { // ---- More general tests, Checking that truncated iv loops become CountedLoops --------- + @DontInline + public static int dontinline(int i) { + return i + 1; + } + + public static int lo = 11; + public static int hi = 33; + + // testIR0: just a regular int loop + public static int testIR0_gold = testIR0(); + + @Run(test = "testIR0") + private static void runIR0() { + int val = testIR0(); + if (val != testIR0_gold) { throw new RuntimeException("wrong value: " + testIR0_gold + " vs " + val); } + } + + @Test + @IR(counts = {IRNode.COUNTED_LOOP, "> 0"}) + static int testIR0() { + int init = lo; + int limit = hi; + int sum = 0; + for (int i = init; i < limit; i++) { + sum = dontinline(sum); + } + return sum; + } + + // testIR1: short truncation, with checks before loop. + public static int testIR1_gold = testIR1(); + + @Run(test = "testIR1") + private static void runIR1() { + int val = testIR1(); + if (val != testIR1_gold) { throw new RuntimeException("wrong value: " + testIR1_gold + " vs " + val); } + } + + @Test + @IR(counts = {IRNode.COUNTED_LOOP, "> 0"}) + static int testIR1() { + int init = lo; + int limit = hi; + if (init < -100 || limit > 100 || init >= limit) { return -1; } + int sum = 0; + for (int i = init; i < limit; i = (short)(i+1)) { + sum = dontinline(sum); + } + return sum; + } + // TODO: ensure coverage // - char, byte and short truncation // - check for IRNode.COUNTED_LOOP From dc8b6fa5155f3bafc33fcfa0756d79994fa51c4e Mon Sep 17 00:00:00 2001 From: Emanuel Peter Date: Tue, 9 Jun 2026 17:39:31 +0200 Subject: [PATCH 06/44] short test --- .../compiler/loopopts/TestHasTruncationWrap.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java index 548c52be37b55..7f73051e48d38 100644 --- a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java +++ b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java @@ -213,12 +213,13 @@ private static void runIR1() { @Test @IR(counts = {IRNode.COUNTED_LOOP, "> 0"}) static int testIR1() { - int init = lo; - int limit = hi; - if (init < -100 || limit > 100 || init >= limit) { return -1; } + int i = (short)lo; + int limit = (short)hi; + if (i >= limit) { return -1; } int sum = 0; - for (int i = init; i < limit; i = (short)(i+1)) { - sum = dontinline(sum); + while (i < limit) { + sum = dontinline(sum); // work to keep loop alive + i = (short)(i+1); // truncated iv } return sum; } From a2ebeb3fc044cad0293903867d7ab80b309a4d15 Mon Sep 17 00:00:00 2001 From: Emanuel Peter Date: Tue, 9 Jun 2026 17:54:09 +0200 Subject: [PATCH 07/44] more test --- .../loopopts/TestHasTruncationWrap.java | 40 ++++++++++++++++--- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java index 7f73051e48d38..0283f29e63be4 100644 --- a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java +++ b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java @@ -201,7 +201,7 @@ static int testIR0() { return sum; } - // testIR1: short truncation, with checks before loop. + // testIR1: short loop, but values are trivially in short range. public static int testIR1_gold = testIR1(); @Run(test = "testIR1") @@ -213,13 +213,41 @@ private static void runIR1() { @Test @IR(counts = {IRNode.COUNTED_LOOP, "> 0"}) static int testIR1() { - int i = (short)lo; - int limit = (short)hi; - if (i >= limit) { return -1; } + short init = (short)lo; + short limit = (short)hi; int sum = 0; - while (i < limit) { + for (short i = init; i < limit; i++) { sum = dontinline(sum); // work to keep loop alive - i = (short)(i+1); // truncated iv + } + return sum; + } + + // testIR2: short loop, ranges proved in short range via CmpI before loop. + public static int testIR2_gold = testIR2(); + + @Run(test = "testIR2") + private static void runIR2() { + int val = testIR2(); + if (val != testIR2_gold) { throw new RuntimeException("wrong value: " + testIR2_gold + " vs " + val); } + } + + @Test + @IR(counts = {IRNode.COUNTED_LOOP, "> 0"}) + static int testIR2() { + int init = Math.max(lo, 0); // init in [0..max_int] + int limit = Math.min(hi, 100); // limit in [min_int..100] + if (init >= limit) { return -1; } // CmpI before loop + // -> init < limit <= 100 + // -> filtered_int_type return [min_int..99] + // -> and intersected with its previous type [0..max_int] + // we get init in [0..99], which is in short range. + int sum = 0; + for (int i = init; i < limit; i = (short)(i+1)) { + sum = dontinline(sum); // work to keep loop alive + // The backedge value of i is also far + // enough from short boundaries, because of + // the loop exit check: + // i < limit <= 100 } return sum; } From 9e0e9802cdee39a64661037dcf84ecef3968ea59 Mon Sep 17 00:00:00 2001 From: Emanuel Peter Date: Tue, 9 Jun 2026 17:57:59 +0200 Subject: [PATCH 08/44] yet another test --- .../loopopts/TestHasTruncationWrap.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java index 0283f29e63be4..d89513629e404 100644 --- a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java +++ b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java @@ -252,6 +252,28 @@ static int testIR2() { return sum; } + // testIR3: short loop, but without the necessary CmpI. + public static int testIR3_gold = testIR3(); + + @Run(test = "testIR3") + private static void runIR3() { + int val = testIR3(); + if (val != testIR3_gold) { throw new RuntimeException("wrong value: " + testIR3_gold + " vs " + val); } + } + + @Test + @IR(counts = {IRNode.COUNTED_LOOP, "> 0"}) + static int testIR3() { + int init = Math.max(lo, 0); // init in [0..max_int] + int limit = Math.min(hi, 100); // limit in [min_int..100] + // Missing CmpI, so init could be outside the short range. + int sum = 0; + for (int i = init; i < limit; i = (short)(i+1)) { + sum = dontinline(sum); // work to keep loop alive + } + return sum; + } + // TODO: ensure coverage // - char, byte and short truncation // - check for IRNode.COUNTED_LOOP From f535431f552eaf5b73c5e69db505b137f6df859c Mon Sep 17 00:00:00 2001 From: Emanuel Peter Date: Tue, 9 Jun 2026 18:06:46 +0200 Subject: [PATCH 09/44] more tests and TODO --- .../loopopts/TestHasTruncationWrap.java | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java index d89513629e404..6bf157dd5fd24 100644 --- a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java +++ b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java @@ -253,6 +253,7 @@ static int testIR2() { } // testIR3: short loop, but without the necessary CmpI. + // TODO: why does this work? public static int testIR3_gold = testIR3(); @Run(test = "testIR3") @@ -274,6 +275,35 @@ static int testIR3() { return sum; } + // testIR3: short loop, with a CmpI, but the limit ranges are bad. + public static int testIR4_gold = testIR4(); + + @Run(test = "testIR4") + private static void runIR4() { + int val = testIR4(); + if (val != testIR4_gold) { throw new RuntimeException("wrong value: " + testIR4_gold + " vs " + val); } + } + + @Test + @IR(counts = {IRNode.COUNTED_LOOP, "= 0"}) + static int testIR4() { + int init = Math.max(lo, 0); // init in [0..max_int] + int limit = Math.min(hi, 100_000); // limit in [min_int..100_000] + if (init >= limit) { return -1; } // CmpI before loop + // -> init < limit <= 100_000 + // -> filtered_int_type return [min_int..99_999] + // -> and intersected with its previous type [0..max_int] + // we get init in [0..99_999], which is NOT in short range. + int sum = 0; + for (int i = init; i < limit; i = (short)(i+1)) { + sum = dontinline(sum); // work to keep loop alive + // Also: the backedge range is not good because + // the exit check is not strong enough for short: + // i < limit <= 100_000 + } + return sum; + } + // TODO: ensure coverage // - char, byte and short truncation // - check for IRNode.COUNTED_LOOP From 2cd845fd45bff4dec9704ff84e756b587a731f30 Mon Sep 17 00:00:00 2001 From: Emanuel Peter Date: Tue, 9 Jun 2026 18:24:21 +0200 Subject: [PATCH 10/44] better annotations --- .../compiler/loopopts/TestHasTruncationWrap.java | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java index 6bf157dd5fd24..65fd3d2a38fa3 100644 --- a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java +++ b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java @@ -252,8 +252,7 @@ static int testIR2() { return sum; } - // testIR3: short loop, but without the necessary CmpI. - // TODO: why does this work? + // testIR3: short loop, and range in short range via CmpI before loop (for loop limit). public static int testIR3_gold = testIR3(); @Run(test = "testIR3") @@ -267,8 +266,15 @@ private static void runIR3() { static int testIR3() { int init = Math.max(lo, 0); // init in [0..max_int] int limit = Math.min(hi, 100); // limit in [min_int..100] - // Missing CmpI, so init could be outside the short range. int sum = 0; + // While there is no explicit CmpI before the loop, we + // actually have "i < limit" in the for loop check, which + // is also checked before entering the loop. + // So also here, we have: + // -> init < limit <= 100 + // -> filtered_int_type return [min_int..99] + // -> and intersected with its previous type [0..max_int] + // we get init in [0..99], which is in short range. for (int i = init; i < limit; i = (short)(i+1)) { sum = dontinline(sum); // work to keep loop alive } @@ -289,12 +295,12 @@ private static void runIR4() { static int testIR4() { int init = Math.max(lo, 0); // init in [0..max_int] int limit = Math.min(hi, 100_000); // limit in [min_int..100_000] - if (init >= limit) { return -1; } // CmpI before loop + int sum = 0; + // Now, the check is not good enough: // -> init < limit <= 100_000 // -> filtered_int_type return [min_int..99_999] // -> and intersected with its previous type [0..max_int] // we get init in [0..99_999], which is NOT in short range. - int sum = 0; for (int i = init; i < limit; i = (short)(i+1)) { sum = dontinline(sum); // work to keep loop alive // Also: the backedge range is not good because From ce522e337896fde65c7f3fd59cb3284a892f5963 Mon Sep 17 00:00:00 2001 From: Emanuel Peter Date: Tue, 9 Jun 2026 18:31:03 +0200 Subject: [PATCH 11/44] more tests still --- .../loopopts/TestHasTruncationWrap.java | 54 ++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java index 65fd3d2a38fa3..0426c0e99d77a 100644 --- a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java +++ b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java @@ -281,7 +281,7 @@ static int testIR3() { return sum; } - // testIR3: short loop, with a CmpI, but the limit ranges are bad. + // testIR4: short loop, with a CmpI, but the limit ranges are bad. public static int testIR4_gold = testIR4(); @Run(test = "testIR4") @@ -310,6 +310,58 @@ static int testIR4() { return sum; } + // testIR5: short do-while-loop, and range in short range via CmpI before loop (for loop limit). + public static int testIR5_gold = testIR5(); + + @Run(test = "testIR5") + private static void runIR5() { + int val = testIR5(); + if (val != testIR5_gold) { throw new RuntimeException("wrong value: " + testIR5_gold + " vs " + val); } + } + + @Test + @IR(counts = {IRNode.COUNTED_LOOP, "> 0"}) + static int testIR5() { + int init = Math.max(lo, 0); // init in [0..max_int] + int limit = Math.min(hi, 100); // limit in [min_int..100] + if (init >= limit) { return -1; } // CmpI before loop + // -> init < limit <= 100 + // -> filtered_int_type return [min_int..99] + // -> and intersected with its previous type [0..max_int] + // we get init in [0..99], which is in short range. + int sum = 0; + int i = init; + do { + sum = dontinline(sum); // work to keep loop alive + i = (short)(i+1); + } while (i < limit); // exit check at the end. + return sum; + } + + // testIR6: short do-while-loop, missing the CmpI before the loop. + public static int testIR6_gold = testIR6(); + + @Run(test = "testIR6") + private static void runIR6() { + int val = testIR6(); + if (val != testIR6_gold) { throw new RuntimeException("wrong value: " + testIR6_gold + " vs " + val); } + } + + @Test + @IR(counts = {IRNode.COUNTED_LOOP, "= 0"}) + static int testIR6() { + int init = Math.max(lo, 0); // init in [0..max_int] + int limit = Math.min(hi, 100); // limit in [min_int..100] + // No CmpI before the loop! + int sum = 0; + int i = init; + do { + sum = dontinline(sum); // work to keep loop alive + i = (short)(i+1); + } while (i < limit); // exit check at the end. + return sum; + } + // TODO: ensure coverage // - char, byte and short truncation // - check for IRNode.COUNTED_LOOP From f05f57dbc17a06912defbdb3b06a2aec302983a5 Mon Sep 17 00:00:00 2001 From: Emanuel Peter Date: Tue, 9 Jun 2026 19:01:34 +0200 Subject: [PATCH 12/44] more tests and TODOs --- .../loopopts/TestHasTruncationWrap.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java index 0426c0e99d77a..1cc006004ebab 100644 --- a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java +++ b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java @@ -201,6 +201,27 @@ static int testIR0() { return sum; } + // testIR0b: just a regular int loop, but with NEQ exit check. + public static int testIR0b_gold = testIR0b(); + + @Run(test = "testIR0b") + private static void runIR0b() { + int val = testIR0b(); + if (val != testIR0b_gold) { throw new RuntimeException("wrong value: " + testIR0b_gold + " vs " + val); } + } + + @Test + @IR(counts = {IRNode.COUNTED_LOOP, "> 0"}) + static int testIR0b() { + int init = lo; + int limit = hi; + int sum = 0; + for (int i = init; i != limit; i++) { + sum = dontinline(sum); + } + return sum; + } + // testIR1: short loop, but values are trivially in short range. public static int testIR1_gold = testIR1(); @@ -221,6 +242,7 @@ static int testIR1() { } return sum; } + // TODO: try the NEQ trick here as well, and in other cases below. // testIR2: short loop, ranges proved in short range via CmpI before loop. public static int testIR2_gold = testIR2(); From 5da70ddccde891a0aef6f1c0e235173c719fa6a3 Mon Sep 17 00:00:00 2001 From: Emanuel Peter Date: Wed, 10 Jun 2026 09:38:08 +0200 Subject: [PATCH 13/44] more testing --- .../loopopts/TestHasTruncationWrap.java | 89 +++++++++++-------- 1 file changed, 54 insertions(+), 35 deletions(-) diff --git a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java index 1cc006004ebab..f2eb55b6598cf 100644 --- a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java +++ b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java @@ -242,7 +242,6 @@ static int testIR1() { } return sum; } - // TODO: try the NEQ trick here as well, and in other cases below. // testIR2: short loop, ranges proved in short range via CmpI before loop. public static int testIR2_gold = testIR2(); @@ -274,6 +273,37 @@ static int testIR2() { return sum; } + // testIR2b: short loop, ranges proved in short range via CmpI before loop. + // Compared to testIR2, the check in the loop is an NEQ. + public static int testIR2b_gold = testIR2b(); + + @Run(test = "testIR2b") + private static void runIR2b() { + int val = testIR2b(); + if (val != testIR2b_gold) { throw new RuntimeException("wrong value: " + testIR2b_gold + " vs " + val); } + } + + @Test + @IR(counts = {IRNode.COUNTED_LOOP, "> 0"}) + static int testIR2b() { + int init = Math.max(lo, 0); // init in [0..max_int] + int limit = Math.min(hi, 100); // limit in [min_int..100] + if (init >= limit) { return -1; } // CmpI before loop + // -> init < limit <= 100 + // -> filtered_int_type return [min_int..99] + // -> and intersected with its previous type [0..max_int] + // we get init in [0..99], which is in short range. + int sum = 0; + for (int i = init; i != limit; i = (short)(i+1)) { + sum = dontinline(sum); // work to keep loop alive + // The backedge value of i is also far + // enough from short boundaries, because of + // the loop exit check: + // i < limit <= 100 + } + return sum; + } + // testIR3: short loop, and range in short range via CmpI before loop (for loop limit). public static int testIR3_gold = testIR3(); @@ -360,29 +390,29 @@ static int testIR5() { return sum; } - // testIR6: short do-while-loop, missing the CmpI before the loop. - public static int testIR6_gold = testIR6(); - - @Run(test = "testIR6") - private static void runIR6() { - int val = testIR6(); - if (val != testIR6_gold) { throw new RuntimeException("wrong value: " + testIR6_gold + " vs " + val); } - } - - @Test - @IR(counts = {IRNode.COUNTED_LOOP, "= 0"}) - static int testIR6() { - int init = Math.max(lo, 0); // init in [0..max_int] - int limit = Math.min(hi, 100); // limit in [min_int..100] - // No CmpI before the loop! - int sum = 0; - int i = init; - do { - sum = dontinline(sum); // work to keep loop alive - i = (short)(i+1); - } while (i < limit); // exit check at the end. - return sum; - } + // // testIR6: short do-while-loop, missing the CmpI before the loop. + // public static int testIR6_gold = testIR6(); + + // @Run(test = "testIR6") + // private static void runIR6() { + // int val = testIR6(); + // if (val != testIR6_gold) { throw new RuntimeException("wrong value: " + testIR6_gold + " vs " + val); } + // } + + // @Test + // @IR(counts = {IRNode.COUNTED_LOOP, "= 0"}) + // static int testIR6() { + // int init = Math.max(lo, 0); // init in [0..max_int] + // int limit = Math.min(hi, 100); // limit in [min_int..100] + // // No CmpI before the loop! + // int sum = 0; + // int i = init; + // do { + // sum = dontinline(sum); // work to keep loop alive + // i = (short)(i+1); + // } while (i < limit); // exit check at the end. + // return sum; + // } // TODO: ensure coverage // - char, byte and short truncation @@ -390,15 +420,4 @@ static int testIR6() { // - dontinline call to prevent empty loop // - increment and decrement cases // - Cases with and without compare before loop: positive and negative tests - - // TODO: replace with real test! - //@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(); - // } - //} } From a4fd1d3724aefd48f58499b83f0248c60f584d56 Mon Sep 17 00:00:00 2001 From: Emanuel Peter Date: Wed, 10 Jun 2026 09:42:41 +0200 Subject: [PATCH 14/44] and one more test --- .../loopopts/TestHasTruncationWrap.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java index f2eb55b6598cf..4a80fc0615742 100644 --- a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java +++ b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java @@ -333,6 +333,30 @@ static int testIR3() { return sum; } + // testIR3b: short loop, fails to be recognized as CountedLoop. + // Compared to testIR2, the check in the loop is an NEQ. + public static int testIR3b_gold = testIR3b(); + + @Run(test = "testIR3b") + private static void runIR3b() { + int val = testIR3b(); + if (val != testIR3b_gold) { throw new RuntimeException("wrong value: " + testIR3b_gold + " vs " + val); } + } + + @Test + @IR(counts = {IRNode.COUNTED_LOOP, "= 0"}) + static int testIR3b() { + int init = Math.max(lo, 0); // init in [0..max_int] + int limit = Math.min(hi, 100); // limit in [min_int..100] + int sum = 0; + // No useful CmpI before the loop. + // And the CmpI of the for limit is NEQ, so not useful either. + for (int i = init; i != limit; i = (short)(i+1)) { + sum = dontinline(sum); // work to keep loop alive + } + return sum; + } + // testIR4: short loop, with a CmpI, but the limit ranges are bad. public static int testIR4_gold = testIR4(); From f5a4fb9f820a930002074e9b40e0ef46b3a886b4 Mon Sep 17 00:00:00 2001 From: Emanuel Peter Date: Wed, 10 Jun 2026 10:21:05 +0200 Subject: [PATCH 15/44] more tests yet again --- .../loopopts/TestHasTruncationWrap.java | 104 ++++++++++++++---- 1 file changed, 80 insertions(+), 24 deletions(-) diff --git a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java index 4a80fc0615742..8a6a03cad052f 100644 --- a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java +++ b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java @@ -334,7 +334,7 @@ static int testIR3() { } // testIR3b: short loop, fails to be recognized as CountedLoop. - // Compared to testIR2, the check in the loop is an NEQ. + // Compared to testIR3, the check in the loop is an NEQ. public static int testIR3b_gold = testIR3b(); @Run(test = "testIR3b") @@ -414,29 +414,85 @@ static int testIR5() { return sum; } - // // testIR6: short do-while-loop, missing the CmpI before the loop. - // public static int testIR6_gold = testIR6(); - - // @Run(test = "testIR6") - // private static void runIR6() { - // int val = testIR6(); - // if (val != testIR6_gold) { throw new RuntimeException("wrong value: " + testIR6_gold + " vs " + val); } - // } - - // @Test - // @IR(counts = {IRNode.COUNTED_LOOP, "= 0"}) - // static int testIR6() { - // int init = Math.max(lo, 0); // init in [0..max_int] - // int limit = Math.min(hi, 100); // limit in [min_int..100] - // // No CmpI before the loop! - // int sum = 0; - // int i = init; - // do { - // sum = dontinline(sum); // work to keep loop alive - // i = (short)(i+1); - // } while (i < limit); // exit check at the end. - // return sum; - // } + // testIR5b: short do-while-loop, but the backedge check with NEQ is not strong enough to prevent wrapping. + // Compared to testIR5, the check in the loop is an NEQ. + public static int testIR5b_gold = testIR5b(); + + @Run(test = "testIR5b") + private static void runIR5b() { + int val = testIR5b(); + if (val != testIR5b_gold) { throw new RuntimeException("wrong value: " + testIR5b_gold + " vs " + val); } + } + + @Test + @IR(counts = {IRNode.COUNTED_LOOP, "= 0"}) + static int testIR5b() { + int init = Math.max(lo, 0); // init in [0..max_int] + int limit = Math.min(hi, 100); // limit in [min_int..100] + if (init >= limit) { return -1; } // CmpI before loop + // -> init < limit <= 100 + // -> filtered_int_type return [min_int..99] + // -> and intersected with its previous type [0..max_int] + // we get init in [0..99], which is in short range. + int sum = 0; + int i = init; + do { + sum = dontinline(sum); // work to keep loop alive + i = (short)(i+1); + } while (i != limit); // exit check at the end, but with NEQ. + return sum; + } + + // testIR6: short do-while-loop, missing the CmpI before the loop. + public static int testIR6_gold = testIR6(); + + @Run(test = "testIR6") + private static void runIR6() { + int val = testIR6(); + if (val != testIR6_gold) { throw new RuntimeException("wrong value: " + testIR6_gold + " vs " + val); } + } + + @Test + @IR(counts = {IRNode.COUNTED_LOOP, "> 0"}) + static int testIR6() { + int init = Math.max(lo, 0); // init in [0..max_int] + int limit = Math.min(hi, 100); // limit in [min_int..100] + // No CmpI before the loop! + // But the loop exit check is strong enough to ignore truncation. + int sum = 0; + int i = init; + do { + sum = dontinline(sum); // work to keep loop alive + i = (short)(i+1); + } while (i < limit); // exit check at the end. + return sum; + } + + // testIR6b: short do-while-loop, missing the CmpI before the loop. + // Compared to testIR6, the check in the loop is an NEQ. + public static int testIR6b_gold = testIR6b(); + + @Run(test = "testIR6b") + private static void runIR6b() { + int val = testIR6b(); + if (val != testIR6b_gold) { throw new RuntimeException("wrong value: " + testIR6b_gold + " vs " + val); } + } + + @Test + @IR(counts = {IRNode.COUNTED_LOOP, "= 0"}) + static int testIR6b() { + int init = Math.max(lo, 0); // init in [0..max_int] + int limit = Math.min(hi, 100); // limit in [min_int..100] + // No CmpI before the loop! + // And the loop exit check is NOT strong enough to ignore truncation. + int sum = 0; + int i = init; + do { + sum = dontinline(sum); // work to keep loop alive + i = (short)(i+1); + } while (i != limit); // exit check at the end. + return sum; + } // TODO: ensure coverage // - char, byte and short truncation From 329d2b899dcb344cb0c08e202330cf97a9f9c4e6 Mon Sep 17 00:00:00 2001 From: Emanuel Peter Date: Wed, 10 Jun 2026 10:53:02 +0200 Subject: [PATCH 16/44] one more test --- .../loopopts/TestHasTruncationWrap.java | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java index 8a6a03cad052f..8156ece3b570e 100644 --- a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java +++ b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java @@ -443,6 +443,36 @@ static int testIR5b() { return sum; } + // testIR5c: short do-while-loop, but the backedge check with NEQ is not strong enough to prevent wrapping. + // Compared to testIR5, the check in the loop is an NEQ. + public static int testIR5c_gold = testIR5c(); + + @Run(test = "testIR5c") + private static void runIR5c() { + int val = testIR5c(); + if (val != testIR5c_gold) { throw new RuntimeException("wrong value: " + testIR5c_gold + " vs " + val); } + } + + @Test + @IR(counts = {IRNode.COUNTED_LOOP, "> 0"}) + static int testIR5c() { + int init = Math.max(lo, 0); // init in [0..max_int] + int limit = Math.min(hi, 100); // limit in [min_int..100] + if (init >= limit) { return -1; } // CmpI before loop + // -> init < limit <= 100 + // -> filtered_int_type return [min_int..99] + // -> and intersected with its previous type [0..max_int] + // we get init in [0..99], which is in short range. + int sum = 0; + int i = init; + if (i != limit) { return -1; } // additional "exit check" before loop. + do { + sum = dontinline(sum); // work to keep loop alive + i = (short)(i+1); + } while (i != limit); // exit check at the end, but with NEQ. + return sum; + } + // testIR6: short do-while-loop, missing the CmpI before the loop. public static int testIR6_gold = testIR6(); From 6687f63ca58338ab07e34cb6f6f49d0ba8d16f8a Mon Sep 17 00:00:00 2001 From: Emanuel Peter Date: Wed, 10 Jun 2026 10:57:34 +0200 Subject: [PATCH 17/44] fix check --- test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java index 8156ece3b570e..aee7202a55474 100644 --- a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java +++ b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java @@ -465,7 +465,7 @@ static int testIR5c() { // we get init in [0..99], which is in short range. int sum = 0; int i = init; - if (i != limit) { return -1; } // additional "exit check" before loop. + if (i == limit) { return sum; } // additional "exit check" before loop. do { sum = dontinline(sum); // work to keep loop alive i = (short)(i+1); From fbd52fe7d32b5ca5421a383824409c29d7066b6b Mon Sep 17 00:00:00 2001 From: Emanuel Peter Date: Wed, 10 Jun 2026 11:25:46 +0200 Subject: [PATCH 18/44] fix short test --- .../jtreg/compiler/loopopts/TestHasTruncationWrap.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java index aee7202a55474..e79b6430826f3 100644 --- a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java +++ b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java @@ -443,8 +443,8 @@ static int testIR5b() { return sum; } - // testIR5c: short do-while-loop, but the backedge check with NEQ is not strong enough to prevent wrapping. - // Compared to testIR5, the check in the loop is an NEQ. + // testIR5c: short while-loop. + // Similar code pattern to testIR2b. public static int testIR5c_gold = testIR5c(); @Run(test = "testIR5c") @@ -465,11 +465,10 @@ static int testIR5c() { // we get init in [0..99], which is in short range. int sum = 0; int i = init; - if (i == limit) { return sum; } // additional "exit check" before loop. - do { + while (i != limit) { // "exit check" before loop, using NEQ. sum = dontinline(sum); // work to keep loop alive i = (short)(i+1); - } while (i != limit); // exit check at the end, but with NEQ. + } return sum; } From c59ab3938dca2f67236aae1ee52cd79f2d96bc29 Mon Sep 17 00:00:00 2001 From: Emanuel Peter Date: Wed, 10 Jun 2026 11:32:32 +0200 Subject: [PATCH 19/44] fix up more tests --- .../loopopts/TestHasTruncationWrap.java | 39 +++++++++++++++++-- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java index e79b6430826f3..5fc120f7bc24d 100644 --- a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java +++ b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java @@ -443,8 +443,9 @@ static int testIR5b() { return sum; } - // testIR5c: short while-loop. - // Similar code pattern to testIR2b. + // testIR5c: short do-while-loop. + // While the code shape looks very close to testIR2b, somehow the additional "exit check" + // does not do the trick, and C2 does not recognize the pattern. public static int testIR5c_gold = testIR5c(); @Run(test = "testIR5c") @@ -454,7 +455,7 @@ private static void runIR5c() { } @Test - @IR(counts = {IRNode.COUNTED_LOOP, "> 0"}) + @IR(counts = {IRNode.COUNTED_LOOP, "= 0"}) static int testIR5c() { int init = Math.max(lo, 0); // init in [0..max_int] int limit = Math.min(hi, 100); // limit in [min_int..100] @@ -465,7 +466,37 @@ static int testIR5c() { // we get init in [0..99], which is in short range. int sum = 0; int i = init; - while (i != limit) { // "exit check" before loop, using NEQ. + if (i == limit) { return sum; } // additional "exit check" before loop. + do { + sum = dontinline(sum); // work to keep loop alive + i = (short)(i+1); + } while (i != limit); // exit check at the end, but with NEQ. + return sum; + } + + // testIR5d: short while-loop, again similar to testIR2b and testIR5c, but with while-loop form. + // Somehow, that does get recognized by C2. + public static int testIR5d_gold = testIR5d(); + + @Run(test = "testIR5d") + private static void runIR5d() { + int val = testIR5d(); + if (val != testIR5d_gold) { throw new RuntimeException("wrong value: " + testIR5d_gold + " vs " + val); } + } + + @Test + @IR(counts = {IRNode.COUNTED_LOOP, "> 0"}) + static int testIR5d() { + int init = Math.max(lo, 0); // init in [0..max_int] + int limit = Math.min(hi, 100); // limit in [min_int..100] + if (init >= limit) { return -1; } // CmpI before loop + // -> init < limit <= 100 + // -> filtered_int_type return [min_int..99] + // -> and intersected with its previous type [0..max_int] + // we get init in [0..99], which is in short range. + int sum = 0; + int i = init; + while (i != limit) { sum = dontinline(sum); // work to keep loop alive i = (short)(i+1); } From a7f23d0170caf3752601e9b17229997c78918607 Mon Sep 17 00:00:00 2001 From: Emanuel Peter Date: Wed, 10 Jun 2026 12:00:52 +0200 Subject: [PATCH 20/44] small improvements --- src/hotspot/share/opto/ifnode.cpp | 2 +- .../loopopts/TestHasTruncationWrap.java | 35 +++++++++++++------ 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/src/hotspot/share/opto/ifnode.cpp b/src/hotspot/share/opto/ifnode.cpp index 41ca97ef8ecab..5ffeff86fb1ba 100644 --- a/src/hotspot/share/opto/ifnode.cpp +++ b/src/hotspot/share/opto/ifnode.cpp @@ -674,7 +674,7 @@ const TypeInt* IfNode::filtered_int_type(PhaseGVN* gvn, Node* val, Node* if_proj if (cmp->Opcode() != Op_CmpI) { // Only CmpI allowed, assumed by signed logic below. // We could extend to CmpU in the future, and would - // have to implment unsigned range logic below. + // have to implement unsigned range logic below. return nullptr; } const TypeInt* cmp2_t = gvn->type(cmp->in(2))->isa_int(); diff --git a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java index 5fc120f7bc24d..17fe963ee91a9 100644 --- a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java +++ b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java @@ -76,15 +76,15 @@ public static void test0() { // A second reproducer from JDK-8385855, leads to wrong result since JDK18 (JDK-8276162). // We make use of the CmpU via Integer.compareUnsigned, introduced by JDK-8276162. - public static int test1_gold0 = test1(-2); - public static int test1_gold1 = test1(2); + public static int test1_gold0 = 32767; // test1(-2); + public static int test1_gold1 = 3; // test1(2); @Run(test = "test1") private static void run1() { int val0 = test1(-2); int val1 = test1( 2); - if (val0 != test1_gold0) { throw new RuntimeException("wrong value test( 2): " + test1_gold0 + " vs " + val0); } - if (val1 != test1_gold1) { throw new RuntimeException("wrong value test(-2): " + test1_gold1 + " vs " + val1); } + if (val0 != test1_gold0) { throw new RuntimeException("wrong value test(-2): " + test1_gold0 + " vs " + val0); } + if (val1 != test1_gold1) { throw new RuntimeException("wrong value test( 2): " + test1_gold1 + " vs " + val1); } } @Test @@ -140,15 +140,15 @@ private static int test1(int start) { // We make use of the CmpU in the RangeCheck of an array access. // To flip the condition, we just use a try/catch. public static final int[] test2_A = new int[2]; - public static int test2_gold0 = test2(-2); - public static int test2_gold1 = test2(2); + public static int test2_gold0 = 32767; // test2(-2); + public static int test2_gold1 = 3; // test2(2); @Run(test = "test2") private static void run2() { int val0 = test2(-2); int val1 = test2( 2); - if (val0 != test2_gold0) { throw new RuntimeException("wrong value test( 2): " + test2_gold0 + " vs " + val0); } - if (val1 != test2_gold1) { throw new RuntimeException("wrong value test(-2): " + test2_gold1 + " vs " + val1); } + if (val0 != test2_gold0) { throw new RuntimeException("wrong value test(-2): " + test2_gold0 + " vs " + val0); } + if (val1 != test2_gold1) { throw new RuntimeException("wrong value test( 2): " + test2_gold1 + " vs " + val1); } } @Test @@ -446,6 +446,7 @@ static int testIR5b() { // testIR5c: short do-while-loop. // While the code shape looks very close to testIR2b, somehow the additional "exit check" // does not do the trick, and C2 does not recognize the pattern. + // The one-off pre-loop guard and bottom exit check are not enough. Not 100% sure why. public static int testIR5c_gold = testIR5c(); @Run(test = "testIR5c") @@ -475,7 +476,8 @@ static int testIR5c() { } // testIR5d: short while-loop, again similar to testIR2b and testIR5c, but with while-loop form. - // Somehow, that does get recognized by C2. + // Somehow, that does get recognized by C2: the top-check seems to trigger optimizationd differently, + // evne though Java semantics would be the same. public static int testIR5d_gold = testIR5d(); @Run(test = "testIR5d") @@ -558,6 +560,19 @@ static int testIR6b() { // - char, byte and short truncation // - check for IRNode.COUNTED_LOOP // - dontinline call to prevent empty loop - // - increment and decrement cases + // - increment and decrement cases, non-unit stride // - Cases with and without compare before loop: positive and negative tests + // + // Template Fuzzing ideas: + // - Mostly about correctness, not IR rules + // - truncation: + // - short cast + // - short shift: ((i + s) << 16) >> 16 + // - char/byte mask/shift + // - stride: pos/neg, small integers (rarely also large?) + // - loop shape: for, top-tested while, bottom tested do-while. Each with < or !=. + // - endless loop control: additional loop exit with hidden condition? - verify with IR test here. + // - pre-loop cmp: none, explicit "init < limit", using min/max or not, using CmpU vs CmpI. + // - bounds: small, in around short/char/byte, totally random. + // - reference vs test methods for correctness comparison. } From 8cb7d3d532d275e503a1f502ff139ce0aef963db Mon Sep 17 00:00:00 2001 From: Emanuel Peter Date: Wed, 10 Jun 2026 13:45:29 +0200 Subject: [PATCH 21/44] update comments --- .../loopopts/TestHasTruncationWrap.java | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java index 17fe963ee91a9..3cf654e9b6e43 100644 --- a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java +++ b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java @@ -42,7 +42,10 @@ import compiler.lib.ir_framework.*; /** - * TODO: descr + * Tests for CountedLoopConverter::has_truncation_wrap, which deals with wrapped iv, for byte/char/short iv cases. + * We have some regression tests for JDK-8385855, as well as some IR tests that ensure that we detect counted + * loops in many cases, where we have to check that truncation does not lead to wrapping, which would mean + * the iv would not be linear, but possibly overflow the byte/char/short ranges. */ public class TestHasTruncationWrap { @@ -444,9 +447,15 @@ static int testIR5b() { } // testIR5c: short do-while-loop. - // While the code shape looks very close to testIR2b, somehow the additional "exit check" - // does not do the trick, and C2 does not recognize the pattern. - // The one-off pre-loop guard and bottom exit check are not enough. Not 100% sure why. + // While the code shape looks very close to testIR2b, it does not behave the same. + // The while loop below is peeled once. The additional "exit check" is eliminated, + // because redundant after "init >= limit" check. + // From peeling, the new initial value is a truncated short value, and not init, so + // the "init >= limit" check is not helpful any more, as far as I can see. + // Also the backedge value is truncated to short value. But this is not enough to + // guarantee that there is no short-overflow (truncation): we do not manage to + // prove that i could never be short_max, and then overflow the short range at + // the next increment. public static int testIR5c_gold = testIR5c(); @Run(test = "testIR5c") @@ -476,8 +485,8 @@ static int testIR5c() { } // testIR5d: short while-loop, again similar to testIR2b and testIR5c, but with while-loop form. - // Somehow, that does get recognized by C2: the top-check seems to trigger optimizationd differently, - // evne though Java semantics would be the same. + // No peeling, and so the entry value is init, and so the "init >= limit" check is useful, + // and used by has_truncation_wrap. With it, C2 manages to prove no short-overflow. public static int testIR5d_gold = testIR5d(); @Run(test = "testIR5d") From 3c484d6f1a9c96c91e5bf2ef81b20cf55cd4ade9 Mon Sep 17 00:00:00 2001 From: Emanuel Peter Date: Wed, 10 Jun 2026 13:49:06 +0200 Subject: [PATCH 22/44] more comments --- .../jtreg/compiler/loopopts/TestHasTruncationWrap.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java index 3cf654e9b6e43..59602abc08eeb 100644 --- a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java +++ b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java @@ -46,6 +46,11 @@ * We have some regression tests for JDK-8385855, as well as some IR tests that ensure that we detect counted * loops in many cases, where we have to check that truncation does not lead to wrapping, which would mean * the iv would not be linear, but possibly overflow the byte/char/short ranges. + * + * Note: the optimization around CountedLoopConverter::has_truncation_wrap is a bit fragile, and depends on + * the exact loop shape, and if peeling happens or not, etc. The goal of this test is not to prove that we + * recognize all truncated cases where one could in theory prove there is no wrap/overflow, but simply to + * list some examples of today's state, so we don't get further regressions in the future. */ public class TestHasTruncationWrap { @@ -453,7 +458,7 @@ static int testIR5b() { // From peeling, the new initial value is a truncated short value, and not init, so // the "init >= limit" check is not helpful any more, as far as I can see. // Also the backedge value is truncated to short value. But this is not enough to - // guarantee that there is no short-overflow (truncation): we do not manage to + // guarantee that there is no short-overflow (wrap): we do not manage to // prove that i could never be short_max, and then overflow the short range at // the next increment. public static int testIR5c_gold = testIR5c(); From fa7e081313393224904a0daeb91a8713f4d02c4c Mon Sep 17 00:00:00 2001 From: Emanuel Peter Date: Wed, 10 Jun 2026 13:56:26 +0200 Subject: [PATCH 23/44] 2 more basic cases --- .../loopopts/TestHasTruncationWrap.java | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java index 59602abc08eeb..210aa7ab32913 100644 --- a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java +++ b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java @@ -251,6 +251,69 @@ static int testIR1() { return sum; } + // testIR1b: short loop, but values are trivially in short range. Decrement iv. + public static int testIR1b_gold = testIR1b(); + + @Run(test = "testIR1b") + private static void runIR1b() { + int val = testIR1b(); + if (val != testIR1b_gold) { throw new RuntimeException("wrong value: " + testIR1b_gold + " vs " + val); } + } + + @Test + @IR(counts = {IRNode.COUNTED_LOOP, "> 0"}) + static int testIR1b() { + short init = (short)hi; + short limit = (short)lo; + int sum = 0; + for (short i = init; i > limit; i--) { + sum = dontinline(sum); // work to keep loop alive + } + return sum; + } + + // testIR1c: short loop, but values are trivially in short range. Incr by 2. + public static int testIR1c_gold = testIR1c(); + + @Run(test = "testIR1c") + private static void runIR1c() { + int val = testIR1c(); + if (val != testIR1c_gold) { throw new RuntimeException("wrong value: " + testIR1c_gold + " vs " + val); } + } + + @Test + @IR(counts = {IRNode.COUNTED_LOOP, "> 0"}) + static int testIR1c() { + short init = (short)lo; + short limit = (short)hi; + int sum = 0; + for (short i = init; i < limit; i+=2) { + sum = dontinline(sum); // work to keep loop alive + } + return sum; + } + + // testIR1d: short loop, but values are trivially in short range. Decrement iv by 2. + public static int testIR1d_gold = testIR1d(); + + @Run(test = "testIR1d") + private static void runIR1d() { + int val = testIR1d(); + if (val != testIR1d_gold) { throw new RuntimeException("wrong value: " + testIR1d_gold + " vs " + val); } + } + + @Test + @IR(counts = {IRNode.COUNTED_LOOP, "> 0"}) + static int testIR1d() { + short init = (short)hi; + short limit = (short)lo; + int sum = 0; + for (short i = init; i > limit; i-=2) { + sum = dontinline(sum); // work to keep loop alive + } + return sum; + } + // testIR2: short loop, ranges proved in short range via CmpI before loop. public static int testIR2_gold = testIR2(); From d98bb65d7355cbfbde1af8753a23d06c81cc862c Mon Sep 17 00:00:00 2001 From: Emanuel Peter Date: Wed, 10 Jun 2026 14:02:09 +0200 Subject: [PATCH 24/44] fix testIR1c/d --- .../jtreg/compiler/loopopts/TestHasTruncationWrap.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java index 210aa7ab32913..810e3c44ad421 100644 --- a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java +++ b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java @@ -273,6 +273,7 @@ static int testIR1b() { } // testIR1c: short loop, but values are trivially in short range. Incr by 2. + // Not safe: lo=32766+2 would wrap past short_max. public static int testIR1c_gold = testIR1c(); @Run(test = "testIR1c") @@ -282,7 +283,7 @@ private static void runIR1c() { } @Test - @IR(counts = {IRNode.COUNTED_LOOP, "> 0"}) + @IR(counts = {IRNode.COUNTED_LOOP, "= 0"}) static int testIR1c() { short init = (short)lo; short limit = (short)hi; @@ -294,6 +295,7 @@ static int testIR1c() { } // testIR1d: short loop, but values are trivially in short range. Decrement iv by 2. + // Not safe: lo=-32767-2 would wrap past short_min. public static int testIR1d_gold = testIR1d(); @Run(test = "testIR1d") @@ -303,7 +305,7 @@ private static void runIR1d() { } @Test - @IR(counts = {IRNode.COUNTED_LOOP, "> 0"}) + @IR(counts = {IRNode.COUNTED_LOOP, "= 0"}) static int testIR1d() { short init = (short)hi; short limit = (short)lo; From ddcaa7f2da13779950ccae41f5a8abd3089c16dd Mon Sep 17 00:00:00 2001 From: Emanuel Peter Date: Wed, 10 Jun 2026 14:07:13 +0200 Subject: [PATCH 25/44] new testIR3b --- .../loopopts/TestHasTruncationWrap.java | 39 ++++++++++++------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java index 810e3c44ad421..531d534c93efd 100644 --- a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java +++ b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java @@ -406,8 +406,18 @@ static int testIR3() { return sum; } - // testIR3b: short loop, fails to be recognized as CountedLoop. + // testIR3x: short loop, fails to be recognized as CountedLoop. // Compared to testIR3, the check in the loop is an NEQ. + public static int testIR3x_gold = testIR3x(); + + @Run(test = "testIR3x") + private static void runIR3x() { + int val = testIR3x(); + if (val != testIR3x_gold) { throw new RuntimeException("wrong value: " + testIR3x_gold + " vs " + val); } + } + + // testIR3b: short loop, and range in short range via CmpI before loop (for loop limit). + // Decr iv. public static int testIR3b_gold = testIR3b(); @Run(test = "testIR3b") @@ -417,8 +427,20 @@ private static void runIR3b() { } @Test - @IR(counts = {IRNode.COUNTED_LOOP, "= 0"}) + @IR(counts = {IRNode.COUNTED_LOOP, "> 0"}) static int testIR3b() { + int limit = Math.max(lo, 0); // init in [0..max_int] + int init = Math.min(hi, 100); // limit in [min_int..100] + int sum = 0; + for (int i = init; i > limit; i = (short)(i-1)) { + sum = dontinline(sum); // work to keep loop alive + } + return sum; + } + + @Test + @IR(counts = {IRNode.COUNTED_LOOP, "= 0"}) + static int testIR3x() { int init = Math.max(lo, 0); // init in [0..max_int] int limit = Math.min(hi, 100); // limit in [min_int..100] int sum = 0; @@ -641,17 +663,4 @@ static int testIR6b() { // - dontinline call to prevent empty loop // - increment and decrement cases, non-unit stride // - Cases with and without compare before loop: positive and negative tests - // - // Template Fuzzing ideas: - // - Mostly about correctness, not IR rules - // - truncation: - // - short cast - // - short shift: ((i + s) << 16) >> 16 - // - char/byte mask/shift - // - stride: pos/neg, small integers (rarely also large?) - // - loop shape: for, top-tested while, bottom tested do-while. Each with < or !=. - // - endless loop control: additional loop exit with hidden condition? - verify with IR test here. - // - pre-loop cmp: none, explicit "init < limit", using min/max or not, using CmpU vs CmpI. - // - bounds: small, in around short/char/byte, totally random. - // - reference vs test methods for correctness comparison. } From 57f91aef4002dbef5e4c411e796edebbaf433df1 Mon Sep 17 00:00:00 2001 From: Emanuel Peter Date: Wed, 10 Jun 2026 14:19:58 +0200 Subject: [PATCH 26/44] opaque check case --- .../loopopts/TestHasTruncationWrap.java | 58 +++++++++++++++---- 1 file changed, 48 insertions(+), 10 deletions(-) diff --git a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java index 531d534c93efd..b645f9f7ab5b4 100644 --- a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java +++ b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java @@ -406,16 +406,6 @@ static int testIR3() { return sum; } - // testIR3x: short loop, fails to be recognized as CountedLoop. - // Compared to testIR3, the check in the loop is an NEQ. - public static int testIR3x_gold = testIR3x(); - - @Run(test = "testIR3x") - private static void runIR3x() { - int val = testIR3x(); - if (val != testIR3x_gold) { throw new RuntimeException("wrong value: " + testIR3x_gold + " vs " + val); } - } - // testIR3b: short loop, and range in short range via CmpI before loop (for loop limit). // Decr iv. public static int testIR3b_gold = testIR3b(); @@ -438,6 +428,16 @@ static int testIR3b() { return sum; } + // testIR3x: short loop, fails to be recognized as CountedLoop. + // Compared to testIR3, the check in the loop is an NEQ. + public static int testIR3x_gold = testIR3x(); + + @Run(test = "testIR3x") + private static void runIR3x() { + int val = testIR3x(); + if (val != testIR3x_gold) { throw new RuntimeException("wrong value: " + testIR3x_gold + " vs " + val); } + } + @Test @IR(counts = {IRNode.COUNTED_LOOP, "= 0"}) static int testIR3x() { @@ -657,6 +657,44 @@ static int testIR6b() { return sum; } + public static int opaqueCounter; + + @DontInline + public static void opaqueReset() { + opaqueCounter = 0; + } + + @DontInline + public static boolean opaqueCheck() { + return (opaqueCounter++) >= 100_000; + } + + // testIR7: with additional opaque exit check. + // Useful to verify that TestTruncationWrapFuzzer.java opaque exit checks + // do not prohibit CountedLoop detection. + // We start from testIR3 and testIR4, but add the additional opaque exit. + public static int testIR7_gold = testIR7(); + + @Run(test = "testIR7") + private static void runIR7() { + int val = testIR7(); + if (val != testIR7_gold) { throw new RuntimeException("wrong value: " + testIR7_gold + " vs " + val); } + } + + @Test + @IR(counts = {IRNode.COUNTED_LOOP, "= 0"}) + static int testIR7() { + opaqueReset(); + int init = Math.max(lo, 0); + int limit = Math.min(hi, 100_000); // bad bounds + int sum = 0; + for (int i = init; i < limit; i = (short)(i+1)) { + sum = dontinline(sum); + if (opaqueCheck()) { break; } + } + return sum; + } + // TODO: ensure coverage // - char, byte and short truncation // - check for IRNode.COUNTED_LOOP From 9d18d0b17bc456a9dd2eee1222fd0a843d3bec02 Mon Sep 17 00:00:00 2001 From: Emanuel Peter Date: Wed, 10 Jun 2026 14:34:47 +0200 Subject: [PATCH 27/44] more tests and comments --- .../loopopts/TestHasTruncationWrap.java | 35 +++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java index b645f9f7ab5b4..1a1390e5b0a8f 100644 --- a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java +++ b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java @@ -408,6 +408,14 @@ static int testIR3() { // testIR3b: short loop, and range in short range via CmpI before loop (for loop limit). // Decr iv. + // Missed optimization opportunity: + // CountedLoopConverter::LoopStructure::is_infinite_loop + // It wrongly fires, and prevents CountedLoop detection. + // This check is increment-specific, and fails to accound for decrement: + // if (limit_t->hi_as_long() > incr_t->hi_as_long()) { + // I don't think this is intentional, because we have handling for positive and + // negative stride in CountedLoopConverter::has_truncation_wrap. + // TODO: file RFE public static int testIR3b_gold = testIR3b(); @Run(test = "testIR3b") @@ -417,7 +425,7 @@ private static void runIR3b() { } @Test - @IR(counts = {IRNode.COUNTED_LOOP, "> 0"}) + @IR(counts = {IRNode.COUNTED_LOOP, "= 0"}) static int testIR3b() { int limit = Math.max(lo, 0); // init in [0..max_int] int init = Math.min(hi, 100); // limit in [min_int..100] @@ -682,8 +690,31 @@ private static void runIR7() { } @Test - @IR(counts = {IRNode.COUNTED_LOOP, "= 0"}) + @IR(counts = {IRNode.COUNTED_LOOP, "> 0"}) static int testIR7() { + opaqueReset(); + int init = Math.max(lo, 0); + int limit = Math.min(hi, 100); // good bounds + int sum = 0; + for (int i = init; i < limit; i = (short)(i+1)) { + sum = dontinline(sum); + if (opaqueCheck()) { break; } + } + return sum; + } + + // testIR7b + public static int testIR7b_gold = testIR7b(); + + @Run(test = "testIR7b") + private static void runIR7b() { + int val = testIR7b(); + if (val != testIR7b_gold) { throw new RuntimeException("wrong value: " + testIR7b_gold + " vs " + val); } + } + + @Test + @IR(counts = {IRNode.COUNTED_LOOP, "= 0"}) + static int testIR7b() { opaqueReset(); int init = Math.max(lo, 0); int limit = Math.min(hi, 100_000); // bad bounds From f1230e242e8a9534d1f358e1d1884c5694c970f7 Mon Sep 17 00:00:00 2001 From: Emanuel Peter Date: Wed, 10 Jun 2026 15:34:59 +0200 Subject: [PATCH 28/44] wip fuzzer --- .../loopopts/TestTruncationWrapFuzzer.java | 531 ++++++++++++++++++ 1 file changed, 531 insertions(+) create mode 100644 test/hotspot/jtreg/compiler/loopopts/TestTruncationWrapFuzzer.java diff --git a/test/hotspot/jtreg/compiler/loopopts/TestTruncationWrapFuzzer.java b/test/hotspot/jtreg/compiler/loopopts/TestTruncationWrapFuzzer.java new file mode 100644 index 0000000000000..c7fb9eec244c3 --- /dev/null +++ b/test/hotspot/jtreg/compiler/loopopts/TestTruncationWrapFuzzer.java @@ -0,0 +1,531 @@ +/* + * Copyright (c) 2026, 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 8385855 + * @summary Fuzz patterns for CountedLoopConverter::has_truncation_wrap + * @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.HashSet; +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 TestHasTruncationWrap.java + * + * TODO: + * Template Fuzzing ideas: + * - Mostly about correctness, not IR rules + * - truncation: + * - short cast + * - short shift: ((i + s) << 16) >> 16 + * - char/byte mask/shift + * - stride: pos/neg, small integers (rarely also large?) + * - loop shape: for, top-tested while, bottom tested do-while. Each with < or !=. + * - endless loop control: additional loop exit with hidden condition? - verify with IR test here. + * - pre-loop cmp: none, explicit "init < limit", using min/max or not, using CmpU vs CmpI. + * - bounds: small, in around short/char/byte, totally random. + * - reference vs test methods for correctness comparison. + */ +public class TestTruncationWrapFuzzer { + private static final Random RANDOM = Utils.getRandomInstance(); + private static final RestrictableGenerator INT_GEN = Generators.G.ints(); + + 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<>(); + + // Some utilities, to help us get an additional exit, in case the + // generated loops spin too long, or are infinite loops. + Template.ZeroArgs utilsTemplate = Template.make(() -> scope( + """ + private static final Random RANDOM = Utils.getRandomInstance(); + + public static int opaqueCounter; + public static int opaqueCounterMax; + + @DontInline + public static void opaqueReset() { + opaqueCounter = 0; + } + + @DontInline + public static boolean opaqueCheck() { + return (opaqueCounter++) > opaqueCounterMax; + } + + @DontInline + public static int opaqueIncr(int i) { + return i + 1; + } + """ + )); + testTemplateTokens.add(utilsTemplate.asToken()); + + for (int i = 0; i < 100; i++) { + testTemplateTokens.add(generateTest(/* no warmup, like -Xcomp */ 0)); + } + for (int i = 0; i < 5; i++) { + testTemplateTokens.add(generateTest(/* with warmup, slower */ 100)); + } + + // 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); + } + + // TODO: remove what is not needed + enum Comparator { + ULT(" < 0", false), + ULE(" <= 0", false), + UGT(" > 0", false), + UGE(" >= 0", false), + UEQ(" == 0", false), + UNE(" != 0", false), + LT(" < ", true), + LE(" <= ", true), + GT(" > ", true), + GE(" >= ", true), + EQ(" == ", true), + NE(" != ", true); + + private final String token; + private final boolean signed; + + Comparator(String token, boolean signed) { + this.token = token; + this.signed = signed; + } + + public String getToken() { + return token; + } + + public boolean isSigned() { + return signed; + } + + public Comparator negate() { + return switch(this) { + case ULT -> UGE; + case ULE -> UGT; + case UGT -> ULE; + case UGE -> ULT; + case UEQ -> UNE; + case UNE -> UEQ; + case LT -> GE; + case LE -> GT; + case GT -> LE; + case GE -> LT; + case EQ -> NE; + case NE -> EQ; + }; + } + + public Comparator flip() { + return switch(this) { + case ULT -> UGT; + case ULE -> UGE; + case UGT -> ULT; + case UGE -> ULE; + case UEQ -> UEQ; + case UNE -> UNE; + case LT -> GT; + case LE -> GE; + case GT -> LT; + case GE -> LE; + case EQ -> EQ; + case NE -> NE; + }; + } + + static Comparator random() { + return values()[RANDOM.nextInt(values().length)]; + } + + static Comparator randomGreater() { + return RANDOM.nextBoolean() ? GE : GT; + } + + static Comparator randomLess() { + return RANDOM.nextBoolean() ? LE : LT; + } + } + + record Comparison(String lhs, Comparator cmp, String rhs, boolean negated) { + public Comparison(String lhs, Comparator cmp, String rhs) { + this(lhs, cmp, rhs, false); + } + + public String toString() { + return cmp.isSigned() + ? ((negated ? "!" : "") + "(" + lhs + " "+ cmp.getToken() + " " + rhs + ")") + : ((negated ? "!" : "") + "(Integer.compareUnsigned(" + lhs + ", " + rhs + ")" + cmp.getToken() + ")"); + } + + // Keep the same semantics of the test, but change its form. + Comparison permuteRandom() { + return flipRandom().complementRandom(); + } + + Comparison flipRandom() { + return RANDOM.nextBoolean() ? this : new Comparison(rhs, cmp.flip(), lhs); + } + + Comparison complementRandom() { + return RANDOM.nextBoolean() ? this : new Comparison(lhs, cmp.negate(), rhs, true); + } + + Comparison negateCmp() { + return new Comparison(lhs, cmp.negate(), rhs, negated); + } + } + + interface TestMethodGenerator { + Template.OneArg getTestTemplate(); + + default Template.ZeroArgs getInputTemplate() { + return Template.make(() -> scope( + """ + RestrictableGenerator gen = Generators.G.ints(); + int init = gen.next(); + int limit = gen.next(); + """ + )); + }; + } + + // Loop init/limit are constants. + static class TestMethodGeneratorConst implements TestMethodGenerator { + private final int init = INT_GEN.next(); + private final int limit = INT_GEN.next(); + + //private final Comparison c1 = new Comparison("n", Comparator.random(), "con1").permuteRandom(); + //private final Comparison c2 = new Comparison("n", Comparator.random(), "con2").permuteRandom(); + + private final Template.OneArg testTemplate = Template.make("methodName", (String methodName) -> scope( + let("init", init), + let("limit", limit), + //let("c1", c1), + //let("c2", c2), + """ + static int #methodName(int unused0, int unused1) { + opaqueReset(); + int init = #init; + int limit = #limit; + int sum = 0; + for (int i = init; i < limit; i = (short)(i+1)) { + sum = opaqueIncr(sum); + if (opaqueCheck()) { break; } + } + return sum; + } + """ + )); + + public Template.OneArg getTestTemplate() { return testTemplate; } + } + +// // Cases where a and b are ranges that touch min_int/max_int. +// // Note: if con1=0 and con2=1 then this is like the cases: +// // - test_Case3a_LTLE_overflow +// // - test_Case3b_LTLE_overflow +// // - test_Case4a_LELE_assert +// // +// // Hence, I think this test gives us quite good coverage for the kinds of bugs +// // such as JDK-8346420. +// static class TestMethodGeneratorWithIf implements TestMethodGenerator { +// private final int con1 = INT_GEN.next(); +// private final int con2 = INT_GEN.next(); +// private final String m1 = RANDOM.nextBoolean() ? "Integer.MIN_VALUE" : "Integer.MAX_VALUE"; +// private final String m2 = RANDOM.nextBoolean() ? "Integer.MIN_VALUE" : "Integer.MAX_VALUE"; +// +// private final Comparison c1 = new Comparison("n", Comparator.random(), "a").permuteRandom(); +// private final Comparison c2 = new Comparison("n", Comparator.random(), "b").permuteRandom(); +// +// private final Template.OneArg testTemplate = Template.make("methodName", (String methodName) -> scope( +// let("con1", con1), +// let("con2", con2), +// let("m1", m1), +// let("m2", m2), +// let("c1", c1), +// let("c2", c2), +// """ +// static boolean #methodName(int n, int a, int b) { +// if (a < b) { +// a = #con1; +// b = #con2; +// } else { +// a = #m1; +// b = #m2; +// } +// if (#c1 || #c2) { +// return true; +// } +// return false; +// } +// """ +// )); +// +// public Template.OneArg getTestTemplate() { return testTemplate; } +// } +// +// // Just for good practice: add some case where the ranges are more free. +// static class TestMethodGeneratorRanges implements TestMethodGenerator { +// private final int n_hi = INT_GEN.next(); +// private final int n_lo = INT_GEN.next(); +// private final int a_hi = INT_GEN.next(); +// private final int a_lo = INT_GEN.next(); +// private final int b_hi = INT_GEN.next(); +// private final int b_lo = INT_GEN.next(); +// +// private final Comparison c1 = new Comparison("n", Comparator.random(), "a").permuteRandom(); +// private final Comparison c2 = new Comparison("n", Comparator.random(), "b").permuteRandom(); +// +// private final Template.OneArg template = 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), +// let("c1", c1), +// let("c2", c2), +// """ +// 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 (#c1 || #c2) { +// return true; +// } +// return false; +// } +// """ +// )); +// +// public Template.OneArg getTestTemplate() { +// return template; +// } +// } +// +// // Generate some more constrained cases, but with IR rules +// static class TestMethodGeneratorConstIR implements TestMethodGenerator { +// private final int lo; +// private final int hi; +// { // instance initializer +// // We want to cover all cases for lo and hi combinations. But the +// // critical cases happen around int_min and int_max, and when +// // lo and hi are close to each other. +// switch (RANDOM.nextInt(3)) { +// case 0 -> { +// // Full freedom, will eventually cover all cases +// lo = INT_GEN.next(); +// hi = INT_GEN.next(); +// } +// case 1 -> { +// // Pick cases around overflow and underflow +// lo = Integer.MAX_VALUE - 5 + RANDOM.nextInt(10); +// hi = Integer.MAX_VALUE - 5 + RANDOM.nextInt(10); +// } +// default -> { +// // Pick cases where lo and hi are close to each other +// lo = INT_GEN.next(); +// hi = lo - 5 + RANDOM.nextInt(10); +// } +// } +// } +// +// // Since we are using constants for lo and hi, the checks should get canonicalized, +// // so that n is always in the lhs. We only create cases that are covered by the +// // 4 cases of "2 CmpI -> 1 CmpU" optimization in IfNode::fold_compares_helper. +// private final Comparison c_lo = new Comparison("n", Comparator.randomGreater(), "lo"); +// private final Comparison c_hi = new Comparison("n", Comparator.randomLess(), "hi"); +// private final boolean swap = RANDOM.nextBoolean(); +// private final Comparison c1Permuted = (swap ? c_lo : c_hi).permuteRandom(); +// private final Comparison c2Permuted = (swap ? c_hi : c_lo).permuteRandom(); +// // n > lo && n < hi -> check for inside range +// // n <= lo || n >= hi -> chedk for outside range +// private final boolean withAnd = RANDOM.nextBoolean(); +// private final String operator = withAnd ? "&&" : "||"; +// private final Comparison c1 = withAnd ? c1Permuted : c1Permuted.negateCmp(); +// private final Comparison c2 = withAnd ? c2Permuted : c2Permuted.negateCmp(); +// +// private final Template.OneArg testTemplate = Template.make("methodName", (String methodName) -> scope( +// let("lo", lo), +// let("hi", hi), +// let("c1", c1), +// let("c2", c2), +// let("op", operator), +// """ +// static boolean #methodName(int n, int a, int b) { +// int lo = #lo; +// int hi = #hi; +// if (#c1 #op #c2) { +// return true; +// } +// return false; +// } +// """ +// )); +// +// public Template.OneArg getTestTemplate() { return testTemplate; } +// +// @Override +// public Template.ZeroArgs getInputTemplate() { +// return Template.make(() -> scope( +// let("lo", lo), +// let("hi", hi), +// """ +// Random r = Utils.getRandomInstance(); +// RestrictableGenerator gen = Generators.G.ints(); +// int a = gen.next(); +// int b = gen.next(); +// """, +// switch (RANDOM.nextInt(9)) { +// // Random values +// case 0 -> "int n = gen.next();\n"; +// // Fuzz around specific values +// case 1 -> "int n = r.nextInt(10) - 5 + #lo;\n"; +// case 2 -> "int n = r.nextInt(10) - 5 + #hi;\n"; +// case 3 -> "int n = r.nextInt(10) - 5 + (r.nextBoolean() ? #lo : #hi);\n"; +// case 4 -> "int n = r.nextInt(10) - 5 + Integer.MAX_VALUE;\n"; +// // Only very low or very high values, or in the middle +// case 5 -> "int n = r.nextInt(10) - 10 + Integer.MAX_VALUE;\n"; +// case 6 -> "int n = r.nextInt(10) + Integer.MIN_VALUE;\n"; +// case 7 -> "int n = r.nextInt(10) - 5 + #lo/2 + #hi/2;\n"; +// // Always the same constant +// default -> "int n = " + INT_GEN.next() + ";\n"; +// } +// )); +// }; +// } + + public static TemplateToken generateTest(int warmup) { + TestMethodGenerator tg = switch(RANDOM.nextInt(1)) { + case 0 -> new TestMethodGeneratorConst(); + default -> throw new RuntimeException("not expected"); + }; + Template.ZeroArgs testInputTemplate = tg.getInputTemplate(); + Template.OneArg testMethodTemplate = tg.getTestTemplate(); + + var testTemplate = Template.make(() -> scope( + let("warmup", warmup), + """ + // --- $test start --- + @Run(test = "$test") + @Warmup(#warmup) + public static void $run() { + for (int i = 0; i < 100; i++) { + // Generate random values for init and limit. + """, + testInputTemplate.asToken(), + """ + + // Limit how long we can spin in the loop: + opaqueCounterMax = 100_000 + RANDOM.nextInt(1000); + + // Run test and compare with interpreter results. + var result = $test(init, limit); + var expected = $reference(init, limit); + if (result != expected) { + throw new RuntimeException("wrong result: " + result + " vs " + expected + + "\\ninit: " + init + + "\\nlimit: " + limit + + "\\nopaqueCounterMax: " + opaqueCounterMax); + } + } + } + + @Test + """, + testMethodTemplate.asToken($("test")), + """ + + @DontCompile + """, + testMethodTemplate.asToken($("reference")), + """ + // --- $test end --- + """ + )); + return testTemplate.asToken(); + } +} From 7c6abca215508e0a61139ea8024a649da5388980 Mon Sep 17 00:00:00 2001 From: Emanuel Peter Date: Wed, 10 Jun 2026 16:15:47 +0200 Subject: [PATCH 29/44] more for fuzzer --- .../loopopts/TestTruncationWrapFuzzer.java | 50 ++++++++++++++++--- 1 file changed, 42 insertions(+), 8 deletions(-) diff --git a/test/hotspot/jtreg/compiler/loopopts/TestTruncationWrapFuzzer.java b/test/hotspot/jtreg/compiler/loopopts/TestTruncationWrapFuzzer.java index c7fb9eec244c3..84eb488a29fc2 100644 --- a/test/hotspot/jtreg/compiler/loopopts/TestTruncationWrapFuzzer.java +++ b/test/hotspot/jtreg/compiler/loopopts/TestTruncationWrapFuzzer.java @@ -272,26 +272,59 @@ default Template.ZeroArgs getInputTemplate() { }; } + private static record IVMutation(String s0, String s1) { + public String withRandomStride() { + int stride = switch(RANDOM.nextInt(3)) { + case 0 -> INT_GEN.next(); + case 1 -> RANDOM.nextInt(9) - 4; + case 2 -> RANDOM.nextInt(129) - 64; + default -> throw new RuntimeException("not expected"); + }; + + return "i = " + s0 + "i + " + stride + s1; + } + } + + // Different patterns relevant for triggering truncation/wrap. + private static final IVMutation[] IV_MUTATIONS = new IVMutation[] { + new IVMutation("", ""), + new IVMutation("(byte)(", ")"), + new IVMutation("(short)(", ")"), + new IVMutation("(char)(", ")"), + new IVMutation("((", ") << 8) >> 8"), + new IVMutation("((", ") << 16) >> 16"), + new IVMutation("((", ") << 24) >> 24"), + new IVMutation("((", ") & 0x7f)"), + new IVMutation("((", ") & 0xff)"), + new IVMutation("((", ") & 0x7fff)"), + new IVMutation("((", ") & 0xffff)") + }; + + private static String randomIVMutation() { + return IV_MUTATIONS[RANDOM.nextInt(IV_MUTATIONS.length)].withRandomStride(); + } + // Loop init/limit are constants. static class TestMethodGeneratorConst implements TestMethodGenerator { private final int init = INT_GEN.next(); private final int limit = INT_GEN.next(); - //private final Comparison c1 = new Comparison("n", Comparator.random(), "con1").permuteRandom(); - //private final Comparison c2 = new Comparison("n", Comparator.random(), "con2").permuteRandom(); + private final String ivMutation = randomIVMutation(); + + private final Comparison exitCheck = new Comparison("i", Comparator.random(), "limit").permuteRandom(); private final Template.OneArg testTemplate = Template.make("methodName", (String methodName) -> scope( let("init", init), let("limit", limit), - //let("c1", c1), - //let("c2", c2), + let("ivMutation", ivMutation), + let("exitCheck", exitCheck), """ static int #methodName(int unused0, int unused1) { opaqueReset(); int init = #init; int limit = #limit; int sum = 0; - for (int i = init; i < limit; i = (short)(i+1)) { + for (int i = init; #exitCheck; #ivMutation) { sum = opaqueIncr(sum); if (opaqueCheck()) { break; } } @@ -492,15 +525,16 @@ public static TemplateToken generateTest(int warmup) { // --- $test start --- @Run(test = "$test") @Warmup(#warmup) - public static void $run() { - for (int i = 0; i < 100; i++) { + public static void $run(RunInfo info) { + int reps = info.isWarmUp() ? 1 : 100; + for (int i = 0; i < reps; i++) { // Generate random values for init and limit. """, testInputTemplate.asToken(), """ // Limit how long we can spin in the loop: - opaqueCounterMax = 100_000 + RANDOM.nextInt(1000); + opaqueCounterMax = 10_000 + RANDOM.nextInt(1000); // Run test and compare with interpreter results. var result = $test(init, limit); From 1f20619ec1efef94a3f925e4c49dad4ff31d05eb Mon Sep 17 00:00:00 2001 From: Emanuel Peter Date: Wed, 10 Jun 2026 16:31:13 +0200 Subject: [PATCH 30/44] more fuzzer material --- .../loopopts/TestTruncationWrapFuzzer.java | 238 ++++-------------- 1 file changed, 43 insertions(+), 195 deletions(-) diff --git a/test/hotspot/jtreg/compiler/loopopts/TestTruncationWrapFuzzer.java b/test/hotspot/jtreg/compiler/loopopts/TestTruncationWrapFuzzer.java index 84eb488a29fc2..54bd69ac7f5f3 100644 --- a/test/hotspot/jtreg/compiler/loopopts/TestTruncationWrapFuzzer.java +++ b/test/hotspot/jtreg/compiler/loopopts/TestTruncationWrapFuzzer.java @@ -263,17 +263,40 @@ interface TestMethodGenerator { default Template.ZeroArgs getInputTemplate() { return Template.make(() -> scope( - """ - RestrictableGenerator gen = Generators.G.ints(); - int init = gen.next(); - int limit = gen.next(); - """ + switch (RANDOM.nextInt(5)) { + case 0 -> """ + RestrictableGenerator gen = Generators.G.ints(); + int init = gen.next(); + int limit = gen.next(); + """; + case 1 -> """ + int init = (byte)RANDOM.nextInt(); + int limit = (byte)RANDOM.nextInt(); + """; + case 2 -> """ + int init = (short)RANDOM.nextInt(); + int limit = (short)RANDOM.nextInt(); + """; + case 3 -> """ + int init = (char)RANDOM.nextInt(); + int limit = (char)RANDOM.nextInt(); + """; + case 4 -> """ + int e0 = RANDOM.nextInt(32); + int e1 = RANDOM.nextInt(32); + int r0 = RANDOM.nextInt(32); + int r1 = RANDOM.nextInt(32); + int init = (1 << e0) + r0; + int limit = (1 << e1) + r1; + """; + default -> throw new RuntimeException("not expected"); + } )); }; } - private static record IVMutation(String s0, String s1) { - public String withRandomStride() { + private static record Truncation(String s0, String s1) { + public String ivMutationWithRandomStride() { int stride = switch(RANDOM.nextInt(3)) { case 0 -> INT_GEN.next(); case 1 -> RANDOM.nextInt(9) - 4; @@ -286,22 +309,22 @@ public String withRandomStride() { } // Different patterns relevant for triggering truncation/wrap. - private static final IVMutation[] IV_MUTATIONS = new IVMutation[] { - new IVMutation("", ""), - new IVMutation("(byte)(", ")"), - new IVMutation("(short)(", ")"), - new IVMutation("(char)(", ")"), - new IVMutation("((", ") << 8) >> 8"), - new IVMutation("((", ") << 16) >> 16"), - new IVMutation("((", ") << 24) >> 24"), - new IVMutation("((", ") & 0x7f)"), - new IVMutation("((", ") & 0xff)"), - new IVMutation("((", ") & 0x7fff)"), - new IVMutation("((", ") & 0xffff)") + private static final Truncation[] TRUNCATIONS = new Truncation[] { + new Truncation("", ""), + new Truncation("(byte)(", ")"), + new Truncation("(short)(", ")"), + new Truncation("(char)(", ")"), + new Truncation("((", ") << 8) >> 8"), + new Truncation("((", ") << 16) >> 16"), + new Truncation("((", ") << 24) >> 24"), + new Truncation("((", ") & 0x7f)"), + new Truncation("((", ") & 0xff)"), + new Truncation("((", ") & 0x7fff)"), + new Truncation("((", ") & 0xffff)") }; private static String randomIVMutation() { - return IV_MUTATIONS[RANDOM.nextInt(IV_MUTATIONS.length)].withRandomStride(); + return TRUNCATIONS[RANDOM.nextInt(TRUNCATIONS.length)].ivMutationWithRandomStride(); } // Loop init/limit are constants. @@ -336,181 +359,6 @@ static class TestMethodGeneratorConst implements TestMethodGenerator { public Template.OneArg getTestTemplate() { return testTemplate; } } -// // Cases where a and b are ranges that touch min_int/max_int. -// // Note: if con1=0 and con2=1 then this is like the cases: -// // - test_Case3a_LTLE_overflow -// // - test_Case3b_LTLE_overflow -// // - test_Case4a_LELE_assert -// // -// // Hence, I think this test gives us quite good coverage for the kinds of bugs -// // such as JDK-8346420. -// static class TestMethodGeneratorWithIf implements TestMethodGenerator { -// private final int con1 = INT_GEN.next(); -// private final int con2 = INT_GEN.next(); -// private final String m1 = RANDOM.nextBoolean() ? "Integer.MIN_VALUE" : "Integer.MAX_VALUE"; -// private final String m2 = RANDOM.nextBoolean() ? "Integer.MIN_VALUE" : "Integer.MAX_VALUE"; -// -// private final Comparison c1 = new Comparison("n", Comparator.random(), "a").permuteRandom(); -// private final Comparison c2 = new Comparison("n", Comparator.random(), "b").permuteRandom(); -// -// private final Template.OneArg testTemplate = Template.make("methodName", (String methodName) -> scope( -// let("con1", con1), -// let("con2", con2), -// let("m1", m1), -// let("m2", m2), -// let("c1", c1), -// let("c2", c2), -// """ -// static boolean #methodName(int n, int a, int b) { -// if (a < b) { -// a = #con1; -// b = #con2; -// } else { -// a = #m1; -// b = #m2; -// } -// if (#c1 || #c2) { -// return true; -// } -// return false; -// } -// """ -// )); -// -// public Template.OneArg getTestTemplate() { return testTemplate; } -// } -// -// // Just for good practice: add some case where the ranges are more free. -// static class TestMethodGeneratorRanges implements TestMethodGenerator { -// private final int n_hi = INT_GEN.next(); -// private final int n_lo = INT_GEN.next(); -// private final int a_hi = INT_GEN.next(); -// private final int a_lo = INT_GEN.next(); -// private final int b_hi = INT_GEN.next(); -// private final int b_lo = INT_GEN.next(); -// -// private final Comparison c1 = new Comparison("n", Comparator.random(), "a").permuteRandom(); -// private final Comparison c2 = new Comparison("n", Comparator.random(), "b").permuteRandom(); -// -// private final Template.OneArg template = 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), -// let("c1", c1), -// let("c2", c2), -// """ -// 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 (#c1 || #c2) { -// return true; -// } -// return false; -// } -// """ -// )); -// -// public Template.OneArg getTestTemplate() { -// return template; -// } -// } -// -// // Generate some more constrained cases, but with IR rules -// static class TestMethodGeneratorConstIR implements TestMethodGenerator { -// private final int lo; -// private final int hi; -// { // instance initializer -// // We want to cover all cases for lo and hi combinations. But the -// // critical cases happen around int_min and int_max, and when -// // lo and hi are close to each other. -// switch (RANDOM.nextInt(3)) { -// case 0 -> { -// // Full freedom, will eventually cover all cases -// lo = INT_GEN.next(); -// hi = INT_GEN.next(); -// } -// case 1 -> { -// // Pick cases around overflow and underflow -// lo = Integer.MAX_VALUE - 5 + RANDOM.nextInt(10); -// hi = Integer.MAX_VALUE - 5 + RANDOM.nextInt(10); -// } -// default -> { -// // Pick cases where lo and hi are close to each other -// lo = INT_GEN.next(); -// hi = lo - 5 + RANDOM.nextInt(10); -// } -// } -// } -// -// // Since we are using constants for lo and hi, the checks should get canonicalized, -// // so that n is always in the lhs. We only create cases that are covered by the -// // 4 cases of "2 CmpI -> 1 CmpU" optimization in IfNode::fold_compares_helper. -// private final Comparison c_lo = new Comparison("n", Comparator.randomGreater(), "lo"); -// private final Comparison c_hi = new Comparison("n", Comparator.randomLess(), "hi"); -// private final boolean swap = RANDOM.nextBoolean(); -// private final Comparison c1Permuted = (swap ? c_lo : c_hi).permuteRandom(); -// private final Comparison c2Permuted = (swap ? c_hi : c_lo).permuteRandom(); -// // n > lo && n < hi -> check for inside range -// // n <= lo || n >= hi -> chedk for outside range -// private final boolean withAnd = RANDOM.nextBoolean(); -// private final String operator = withAnd ? "&&" : "||"; -// private final Comparison c1 = withAnd ? c1Permuted : c1Permuted.negateCmp(); -// private final Comparison c2 = withAnd ? c2Permuted : c2Permuted.negateCmp(); -// -// private final Template.OneArg testTemplate = Template.make("methodName", (String methodName) -> scope( -// let("lo", lo), -// let("hi", hi), -// let("c1", c1), -// let("c2", c2), -// let("op", operator), -// """ -// static boolean #methodName(int n, int a, int b) { -// int lo = #lo; -// int hi = #hi; -// if (#c1 #op #c2) { -// return true; -// } -// return false; -// } -// """ -// )); -// -// public Template.OneArg getTestTemplate() { return testTemplate; } -// -// @Override -// public Template.ZeroArgs getInputTemplate() { -// return Template.make(() -> scope( -// let("lo", lo), -// let("hi", hi), -// """ -// Random r = Utils.getRandomInstance(); -// RestrictableGenerator gen = Generators.G.ints(); -// int a = gen.next(); -// int b = gen.next(); -// """, -// switch (RANDOM.nextInt(9)) { -// // Random values -// case 0 -> "int n = gen.next();\n"; -// // Fuzz around specific values -// case 1 -> "int n = r.nextInt(10) - 5 + #lo;\n"; -// case 2 -> "int n = r.nextInt(10) - 5 + #hi;\n"; -// case 3 -> "int n = r.nextInt(10) - 5 + (r.nextBoolean() ? #lo : #hi);\n"; -// case 4 -> "int n = r.nextInt(10) - 5 + Integer.MAX_VALUE;\n"; -// // Only very low or very high values, or in the middle -// case 5 -> "int n = r.nextInt(10) - 10 + Integer.MAX_VALUE;\n"; -// case 6 -> "int n = r.nextInt(10) + Integer.MIN_VALUE;\n"; -// case 7 -> "int n = r.nextInt(10) - 5 + #lo/2 + #hi/2;\n"; -// // Always the same constant -// default -> "int n = " + INT_GEN.next() + ";\n"; -// } -// )); -// }; -// } - public static TemplateToken generateTest(int warmup) { TestMethodGenerator tg = switch(RANDOM.nextInt(1)) { case 0 -> new TestMethodGeneratorConst(); From deaf59add542ad87ffc620e7aedc32ed3dc9c709 Mon Sep 17 00:00:00 2001 From: Emanuel Peter Date: Wed, 10 Jun 2026 16:45:41 +0200 Subject: [PATCH 31/44] loop shapes --- .../loopopts/TestTruncationWrapFuzzer.java | 64 +++++++++++++++---- 1 file changed, 53 insertions(+), 11 deletions(-) diff --git a/test/hotspot/jtreg/compiler/loopopts/TestTruncationWrapFuzzer.java b/test/hotspot/jtreg/compiler/loopopts/TestTruncationWrapFuzzer.java index 54bd69ac7f5f3..edae9c812d735 100644 --- a/test/hotspot/jtreg/compiler/loopopts/TestTruncationWrapFuzzer.java +++ b/test/hotspot/jtreg/compiler/loopopts/TestTruncationWrapFuzzer.java @@ -57,14 +57,14 @@ /** * For more basic examples, see TestHasTruncationWrap.java * + * So far, this test does not have IR verification, only result verification. + * + * Features: + * - Truncation patterns, see TRUNCATIONS and randomIVMutation. + * - Stride: positive, negative, small and large, see ivMutationWithRandomStride. + * - Reference (not compiled) vs test (compiled), and result verification. + * * TODO: - * Template Fuzzing ideas: - * - Mostly about correctness, not IR rules - * - truncation: - * - short cast - * - short shift: ((i + s) << 16) >> 16 - * - char/byte mask/shift - * - stride: pos/neg, small integers (rarely also large?) * - loop shape: for, top-tested while, bottom tested do-while. Each with < or !=. * - endless loop control: additional loop exit with hidden condition? - verify with IR test here. * - pre-loop cmp: none, explicit "init < limit", using min/max or not, using CmpU vs CmpI. @@ -327,12 +327,55 @@ private static String randomIVMutation() { return TRUNCATIONS[RANDOM.nextInt(TRUNCATIONS.length)].ivMutationWithRandomStride(); } + private static final String[] LOOP_SHAPES = new String[] { + """ + // Loop Shape: For + for (int i = init; #exitCheck; #ivMutation) { + sum = opaqueIncr(sum); + if (opaqueCheck()) { break; } + } + """, + """ + // Loop Shape: While: + int i = init; + while (#exitCheck) { + sum = opaqueIncr(sum); + if (opaqueCheck()) { break; } + #ivMutation; + } + """, + """ + // Loop Shape: Do-While: + int i = init; + do { + sum = opaqueIncr(sum); + if (opaqueCheck()) { break; } + #ivMutation; + } while (#exitCheck); + """, + """ + // Loop Shape: Do-While + pre-loop check. + int i = init; + if (!(#exitCheck)) { return sum; } + do { + sum = opaqueIncr(sum); + if (opaqueCheck()) { break; } + #ivMutation; + } while (#exitCheck); + """ + }; + + private static String randomLoopShape() { + return LOOP_SHAPES[RANDOM.nextInt(LOOP_SHAPES.length)]; + } + // Loop init/limit are constants. static class TestMethodGeneratorConst implements TestMethodGenerator { private final int init = INT_GEN.next(); private final int limit = INT_GEN.next(); private final String ivMutation = randomIVMutation(); + private final String loopShape = randomLoopShape(); private final Comparison exitCheck = new Comparison("i", Comparator.random(), "limit").permuteRandom(); @@ -347,10 +390,9 @@ static class TestMethodGeneratorConst implements TestMethodGenerator { int init = #init; int limit = #limit; int sum = 0; - for (int i = init; #exitCheck; #ivMutation) { - sum = opaqueIncr(sum); - if (opaqueCheck()) { break; } - } + """, + loopShape, + """ return sum; } """ From 0f2eaa0beabd9aaf30396d86f011c8c131deea06 Mon Sep 17 00:00:00 2001 From: Emanuel Peter Date: Wed, 10 Jun 2026 17:15:07 +0200 Subject: [PATCH 32/44] even more fuzzing --- .../loopopts/TestTruncationWrapFuzzer.java | 88 +++++++++++++++++-- 1 file changed, 82 insertions(+), 6 deletions(-) diff --git a/test/hotspot/jtreg/compiler/loopopts/TestTruncationWrapFuzzer.java b/test/hotspot/jtreg/compiler/loopopts/TestTruncationWrapFuzzer.java index edae9c812d735..1ff653c0ea9d7 100644 --- a/test/hotspot/jtreg/compiler/loopopts/TestTruncationWrapFuzzer.java +++ b/test/hotspot/jtreg/compiler/loopopts/TestTruncationWrapFuzzer.java @@ -63,13 +63,17 @@ * - Truncation patterns, see TRUNCATIONS and randomIVMutation. * - Stride: positive, negative, small and large, see ivMutationWithRandomStride. * - Reference (not compiled) vs test (compiled), and result verification. + * - Loop Shapes: for, while, do-while, see LOOP_SHAPES. + * - Exit checks: random Comparison, see Comparator and Comparison (signed and unsigned). + * - For endless loops / loops that would take too long: early exit via opaqueCheck, + * Note: it is verified that this does not hinder optimization, see: + * TestHasTruncationWrap.java -> testIR7. + * - Interesting loop bounds: init/limit + * - constant + * - variable, sampled (see getInputTemplate), and modified (no-op, truncate, clamp). * * TODO: - * - loop shape: for, top-tested while, bottom tested do-while. Each with < or !=. - * - endless loop control: additional loop exit with hidden condition? - verify with IR test here. * - pre-loop cmp: none, explicit "init < limit", using min/max or not, using CmpU vs CmpI. - * - bounds: small, in around short/char/byte, totally random. - * - reference vs test methods for correctness comparison. */ public class TestTruncationWrapFuzzer { private static final Random RANDOM = Utils.getRandomInstance(); @@ -306,6 +310,10 @@ public String ivMutationWithRandomStride() { return "i = " + s0 + "i + " + stride + s1; } + + public String truncate(String val) { + return val + " = " + s0 + val + s1; + } } // Different patterns relevant for triggering truncation/wrap. @@ -323,8 +331,16 @@ public String ivMutationWithRandomStride() { new Truncation("((", ") & 0xffff)") }; + private static Truncation randomTruncation() { + return TRUNCATIONS[RANDOM.nextInt(TRUNCATIONS.length)]; + } + private static String randomIVMutation() { - return TRUNCATIONS[RANDOM.nextInt(TRUNCATIONS.length)].ivMutationWithRandomStride(); + return randomTruncation().ivMutationWithRandomStride(); + } + + private static String randomTruncation(String val) { + return randomTruncation().truncate(val); } private static final String[] LOOP_SHAPES = new String[] { @@ -401,9 +417,69 @@ static class TestMethodGeneratorConst implements TestMethodGenerator { public Template.OneArg getTestTemplate() { return testTemplate; } } + // Clamp randomly, but not always on both sides. + private static String randomClamping(String value) { + String clamp = value; + if (RANDOM.nextBoolean()) { + clamp = "Math.max(" + clamp + ", " + INT_GEN.next() + ")"; + } + if (RANDOM.nextBoolean()) { + clamp = "Math.min(" + clamp + ", " + INT_GEN.next() + ")"; + } + return value + " = " + clamp; + } + + // We want to be able to modify the incoming init/limit. + // - nothing + // - truncate + // - clamp with min/max, maybe even only one-sided + private static String randomModifyValue(String value) { + return switch(RANDOM.nextInt(3)) { + case 0 -> "// Don't modify " + value + ";\n"; + case 1 -> randomTruncation(value) + ";\n"; + case 2 -> randomClamping(value) + ";\n"; + default -> throw new RuntimeException("not expected"); + }; + } + + // Loop init/limit are variables. + static class TestMethodGeneratorVars implements TestMethodGenerator { + private final int init = INT_GEN.next(); + private final int limit = INT_GEN.next(); + + private final String ivMutation = randomIVMutation(); + private final String loopShape = randomLoopShape(); + private final String modifyInit = randomModifyValue("init"); + private final String modifyLimit = randomModifyValue("limit"); + + private final Comparison exitCheck = new Comparison("i", Comparator.random(), "limit").permuteRandom(); + + private final Template.OneArg testTemplate = Template.make("methodName", (String methodName) -> scope( + let("init", init), + let("limit", limit), + let("ivMutation", ivMutation), + let("exitCheck", exitCheck), + """ + static int #methodName(int init, int limit) { + opaqueReset(); + int sum = 0; + """, + modifyInit, + modifyLimit, + // TODO: extra checks + loopShape, + """ + return sum; + } + """ + )); + + public Template.OneArg getTestTemplate() { return testTemplate; } + } public static TemplateToken generateTest(int warmup) { - TestMethodGenerator tg = switch(RANDOM.nextInt(1)) { + TestMethodGenerator tg = switch(RANDOM.nextInt(2)) { case 0 -> new TestMethodGeneratorConst(); + case 1 -> new TestMethodGeneratorVars(); default -> throw new RuntimeException("not expected"); }; Template.ZeroArgs testInputTemplate = tg.getInputTemplate(); From 14e00ce85f3191169fffbcfbba8f79723af34314 Mon Sep 17 00:00:00 2001 From: Emanuel Peter Date: Wed, 10 Jun 2026 17:27:37 +0200 Subject: [PATCH 33/44] almost done with fuzzer --- .../loopopts/TestTruncationWrapFuzzer.java | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/test/hotspot/jtreg/compiler/loopopts/TestTruncationWrapFuzzer.java b/test/hotspot/jtreg/compiler/loopopts/TestTruncationWrapFuzzer.java index 1ff653c0ea9d7..4211aee383d52 100644 --- a/test/hotspot/jtreg/compiler/loopopts/TestTruncationWrapFuzzer.java +++ b/test/hotspot/jtreg/compiler/loopopts/TestTruncationWrapFuzzer.java @@ -71,9 +71,9 @@ * - Interesting loop bounds: init/limit * - constant * - variable, sampled (see getInputTemplate), and modified (no-op, truncate, clamp). - * - * TODO: - * - pre-loop cmp: none, explicit "init < limit", using min/max or not, using CmpU vs CmpI. + * - Extra check dominating the loop: compare against constant of limit. + * Note: has_truncation_wrap can use such checks to constrain the entry type. + * Note2: We've had bugs around this, confusing CmpI/CmpU, see JDK-8385855. */ public class TestTruncationWrapFuzzer { private static final Random RANDOM = Utils.getRandomInstance(); @@ -435,13 +435,22 @@ private static String randomClamping(String value) { // - clamp with min/max, maybe even only one-sided private static String randomModifyValue(String value) { return switch(RANDOM.nextInt(3)) { - case 0 -> "// Don't modify " + value + ";\n"; + case 0 -> "// Don't modify " + value + "\n"; case 1 -> randomTruncation(value) + ";\n"; case 2 -> randomClamping(value) + ";\n"; default -> throw new RuntimeException("not expected"); }; } + private static String randomExtraCheck() { + // We can constrain the init value with limit or a constant. + String other = RANDOM.nextBoolean() ? "limit" : INT_GEN.next().toString(); + Comparison check = new Comparison("init", Comparator.random(), other).permuteRandom(); + return RANDOM.nextBoolean() + ? "// No extra check.\n" + : "if (" + check + ") { return -1; }\n"; + } + // Loop init/limit are variables. static class TestMethodGeneratorVars implements TestMethodGenerator { private final int init = INT_GEN.next(); @@ -451,6 +460,7 @@ static class TestMethodGeneratorVars implements TestMethodGenerator { private final String loopShape = randomLoopShape(); private final String modifyInit = randomModifyValue("init"); private final String modifyLimit = randomModifyValue("limit"); + private final String extraCheck = randomExtraCheck(); private final Comparison exitCheck = new Comparison("i", Comparator.random(), "limit").permuteRandom(); @@ -464,9 +474,9 @@ static class TestMethodGeneratorVars implements TestMethodGenerator { opaqueReset(); int sum = 0; """, - modifyInit, - modifyLimit, - // TODO: extra checks + modifyInit, // modify type of init + modifyLimit, // modify type of limit + extraCheck, // extra CmpI/CmpU dominating the loop, might constrain entry value. loopShape, """ return sum; From 158ae5358ebe6a6b7e750d75c459acd9b8671155 Mon Sep 17 00:00:00 2001 From: Emanuel Peter Date: Thu, 11 Jun 2026 07:46:44 +0200 Subject: [PATCH 34/44] cleanup --- .../loopopts/TestTruncationWrapFuzzer.java | 24 +++---------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/test/hotspot/jtreg/compiler/loopopts/TestTruncationWrapFuzzer.java b/test/hotspot/jtreg/compiler/loopopts/TestTruncationWrapFuzzer.java index 4211aee383d52..801bafb233b8f 100644 --- a/test/hotspot/jtreg/compiler/loopopts/TestTruncationWrapFuzzer.java +++ b/test/hotspot/jtreg/compiler/loopopts/TestTruncationWrapFuzzer.java @@ -30,7 +30,6 @@ * @library /test/lib / * @compile ../lib/ir_framework/TestFramework.java * @compile ../lib/generators/Generators.java - * @compile ../lib/verify/Verify.java * @run driver ${test.main.class} */ @@ -39,7 +38,6 @@ import java.util.List; import java.util.ArrayList; import java.util.Random; -import java.util.HashSet; import java.util.Set; import jdk.test.lib.Utils; @@ -146,7 +144,6 @@ public static int opaqueIncr(int i) { "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. @@ -155,7 +152,9 @@ public static int opaqueIncr(int i) { testTemplateTokens); } - // TODO: remove what is not needed + // This is copied from TestFoldComparesFuzzer.java, and we should + // refactor this out into the template framework library, in a + // future RFE. enum Comparator { ULT(" < 0", false), ULE(" <= 0", false), @@ -223,14 +222,6 @@ public Comparator flip() { static Comparator random() { return values()[RANDOM.nextInt(values().length)]; } - - static Comparator randomGreater() { - return RANDOM.nextBoolean() ? GE : GT; - } - - static Comparator randomLess() { - return RANDOM.nextBoolean() ? LE : LT; - } } record Comparison(String lhs, Comparator cmp, String rhs, boolean negated) { @@ -256,10 +247,6 @@ Comparison flipRandom() { Comparison complementRandom() { return RANDOM.nextBoolean() ? this : new Comparison(lhs, cmp.negate(), rhs, true); } - - Comparison negateCmp() { - return new Comparison(lhs, cmp.negate(), rhs, negated); - } } interface TestMethodGenerator { @@ -453,9 +440,6 @@ private static String randomExtraCheck() { // Loop init/limit are variables. static class TestMethodGeneratorVars implements TestMethodGenerator { - private final int init = INT_GEN.next(); - private final int limit = INT_GEN.next(); - private final String ivMutation = randomIVMutation(); private final String loopShape = randomLoopShape(); private final String modifyInit = randomModifyValue("init"); @@ -465,8 +449,6 @@ static class TestMethodGeneratorVars implements TestMethodGenerator { private final Comparison exitCheck = new Comparison("i", Comparator.random(), "limit").permuteRandom(); private final Template.OneArg testTemplate = Template.make("methodName", (String methodName) -> scope( - let("init", init), - let("limit", limit), let("ivMutation", ivMutation), let("exitCheck", exitCheck), """ From 71631d5cd828a42b321ec3c701dded8b729b8798 Mon Sep 17 00:00:00 2001 From: Emanuel Peter Date: Thu, 11 Jun 2026 07:57:55 +0200 Subject: [PATCH 35/44] more testing --- .../loopopts/TestHasTruncationWrap.java | 49 ++++++++++--------- .../loopopts/TestTruncationWrapFuzzer.java | 23 +++++---- 2 files changed, 41 insertions(+), 31 deletions(-) diff --git a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java index 1a1390e5b0a8f..590a01a51b8c0 100644 --- a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java +++ b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java @@ -181,10 +181,15 @@ static int test2(int start) { // ---- More general tests, Checking that truncated iv loops become CountedLoops --------- @DontInline - public static int dontinline(int i) { + public static int opaqueSum(int i) { return i + 1; } + @DontInline + public static int opaqueSum(int i, int j) { + return i + j + 1; + } + public static int lo = 11; public static int hi = 33; @@ -204,7 +209,7 @@ static int testIR0() { int limit = hi; int sum = 0; for (int i = init; i < limit; i++) { - sum = dontinline(sum); + sum = opaqueSum(sum); } return sum; } @@ -225,7 +230,7 @@ static int testIR0b() { int limit = hi; int sum = 0; for (int i = init; i != limit; i++) { - sum = dontinline(sum); + sum = opaqueSum(sum); } return sum; } @@ -246,7 +251,7 @@ static int testIR1() { short limit = (short)hi; int sum = 0; for (short i = init; i < limit; i++) { - sum = dontinline(sum); // work to keep loop alive + sum = opaqueSum(sum); // work to keep loop alive } return sum; } @@ -267,7 +272,7 @@ static int testIR1b() { short limit = (short)lo; int sum = 0; for (short i = init; i > limit; i--) { - sum = dontinline(sum); // work to keep loop alive + sum = opaqueSum(sum); // work to keep loop alive } return sum; } @@ -289,7 +294,7 @@ static int testIR1c() { short limit = (short)hi; int sum = 0; for (short i = init; i < limit; i+=2) { - sum = dontinline(sum); // work to keep loop alive + sum = opaqueSum(sum); // work to keep loop alive } return sum; } @@ -311,7 +316,7 @@ static int testIR1d() { short limit = (short)lo; int sum = 0; for (short i = init; i > limit; i-=2) { - sum = dontinline(sum); // work to keep loop alive + sum = opaqueSum(sum); // work to keep loop alive } return sum; } @@ -337,7 +342,7 @@ static int testIR2() { // we get init in [0..99], which is in short range. int sum = 0; for (int i = init; i < limit; i = (short)(i+1)) { - sum = dontinline(sum); // work to keep loop alive + sum = opaqueSum(sum); // work to keep loop alive // The backedge value of i is also far // enough from short boundaries, because of // the loop exit check: @@ -368,7 +373,7 @@ static int testIR2b() { // we get init in [0..99], which is in short range. int sum = 0; for (int i = init; i != limit; i = (short)(i+1)) { - sum = dontinline(sum); // work to keep loop alive + sum = opaqueSum(sum); // work to keep loop alive // The backedge value of i is also far // enough from short boundaries, because of // the loop exit check: @@ -401,7 +406,7 @@ static int testIR3() { // -> and intersected with its previous type [0..max_int] // we get init in [0..99], which is in short range. for (int i = init; i < limit; i = (short)(i+1)) { - sum = dontinline(sum); // work to keep loop alive + sum = opaqueSum(sum); // work to keep loop alive } return sum; } @@ -431,7 +436,7 @@ static int testIR3b() { int init = Math.min(hi, 100); // limit in [min_int..100] int sum = 0; for (int i = init; i > limit; i = (short)(i-1)) { - sum = dontinline(sum); // work to keep loop alive + sum = opaqueSum(sum); // work to keep loop alive } return sum; } @@ -455,7 +460,7 @@ static int testIR3x() { // No useful CmpI before the loop. // And the CmpI of the for limit is NEQ, so not useful either. for (int i = init; i != limit; i = (short)(i+1)) { - sum = dontinline(sum); // work to keep loop alive + sum = opaqueSum(sum); // work to keep loop alive } return sum; } @@ -481,7 +486,7 @@ static int testIR4() { // -> and intersected with its previous type [0..max_int] // we get init in [0..99_999], which is NOT in short range. for (int i = init; i < limit; i = (short)(i+1)) { - sum = dontinline(sum); // work to keep loop alive + sum = opaqueSum(sum); // work to keep loop alive // Also: the backedge range is not good because // the exit check is not strong enough for short: // i < limit <= 100_000 @@ -511,7 +516,7 @@ static int testIR5() { int sum = 0; int i = init; do { - sum = dontinline(sum); // work to keep loop alive + sum = opaqueSum(sum); // work to keep loop alive i = (short)(i+1); } while (i < limit); // exit check at the end. return sum; @@ -540,7 +545,7 @@ static int testIR5b() { int sum = 0; int i = init; do { - sum = dontinline(sum); // work to keep loop alive + sum = opaqueSum(sum); // work to keep loop alive i = (short)(i+1); } while (i != limit); // exit check at the end, but with NEQ. return sum; @@ -578,7 +583,7 @@ static int testIR5c() { int i = init; if (i == limit) { return sum; } // additional "exit check" before loop. do { - sum = dontinline(sum); // work to keep loop alive + sum = opaqueSum(sum); // work to keep loop alive i = (short)(i+1); } while (i != limit); // exit check at the end, but with NEQ. return sum; @@ -608,7 +613,7 @@ static int testIR5d() { int sum = 0; int i = init; while (i != limit) { - sum = dontinline(sum); // work to keep loop alive + sum = opaqueSum(sum); // work to keep loop alive i = (short)(i+1); } return sum; @@ -633,7 +638,7 @@ static int testIR6() { int sum = 0; int i = init; do { - sum = dontinline(sum); // work to keep loop alive + sum = opaqueSum(sum); // work to keep loop alive i = (short)(i+1); } while (i < limit); // exit check at the end. return sum; @@ -659,7 +664,7 @@ static int testIR6b() { int sum = 0; int i = init; do { - sum = dontinline(sum); // work to keep loop alive + sum = opaqueSum(sum); // work to keep loop alive i = (short)(i+1); } while (i != limit); // exit check at the end. return sum; @@ -697,7 +702,7 @@ static int testIR7() { int limit = Math.min(hi, 100); // good bounds int sum = 0; for (int i = init; i < limit; i = (short)(i+1)) { - sum = dontinline(sum); + sum = opaqueSum(sum, i); if (opaqueCheck()) { break; } } return sum; @@ -720,7 +725,7 @@ static int testIR7b() { int limit = Math.min(hi, 100_000); // bad bounds int sum = 0; for (int i = init; i < limit; i = (short)(i+1)) { - sum = dontinline(sum); + sum = opaqueSum(sum, i); if (opaqueCheck()) { break; } } return sum; @@ -729,7 +734,7 @@ static int testIR7b() { // TODO: ensure coverage // - char, byte and short truncation // - check for IRNode.COUNTED_LOOP - // - dontinline call to prevent empty loop + // - opaqueSum call to prevent empty loop // - increment and decrement cases, non-unit stride // - Cases with and without compare before loop: positive and negative tests } diff --git a/test/hotspot/jtreg/compiler/loopopts/TestTruncationWrapFuzzer.java b/test/hotspot/jtreg/compiler/loopopts/TestTruncationWrapFuzzer.java index 801bafb233b8f..45a90139cef81 100644 --- a/test/hotspot/jtreg/compiler/loopopts/TestTruncationWrapFuzzer.java +++ b/test/hotspot/jtreg/compiler/loopopts/TestTruncationWrapFuzzer.java @@ -124,8 +124,8 @@ public static boolean opaqueCheck() { } @DontInline - public static int opaqueIncr(int i) { - return i + 1; + public static int opaqueSum(int i, int j) { + return i + j + 1; } """ )); @@ -333,8 +333,9 @@ private static String randomTruncation(String val) { private static final String[] LOOP_SHAPES = new String[] { """ // Loop Shape: For - for (int i = init; #exitCheck; #ivMutation) { - sum = opaqueIncr(sum); + int i; + for (i = init; #exitCheck; #ivMutation) { + sum = opaqueSum(sum, #addValue); if (opaqueCheck()) { break; } } """, @@ -342,7 +343,7 @@ private static String randomTruncation(String val) { // Loop Shape: While: int i = init; while (#exitCheck) { - sum = opaqueIncr(sum); + sum = opaqueSum(sum, #addValue); if (opaqueCheck()) { break; } #ivMutation; } @@ -351,7 +352,7 @@ private static String randomTruncation(String val) { // Loop Shape: Do-While: int i = init; do { - sum = opaqueIncr(sum); + sum = opaqueSum(sum, #addValue); if (opaqueCheck()) { break; } #ivMutation; } while (#exitCheck); @@ -361,7 +362,7 @@ private static String randomTruncation(String val) { int i = init; if (!(#exitCheck)) { return sum; } do { - sum = opaqueIncr(sum); + sum = opaqueSum(sum, #addValue); if (opaqueCheck()) { break; } #ivMutation; } while (#exitCheck); @@ -379,6 +380,7 @@ static class TestMethodGeneratorConst implements TestMethodGenerator { private final String ivMutation = randomIVMutation(); private final String loopShape = randomLoopShape(); + private final String addValue = RANDOM.nextBoolean() ? "0" : "i"; private final Comparison exitCheck = new Comparison("i", Comparator.random(), "limit").permuteRandom(); @@ -387,6 +389,7 @@ static class TestMethodGeneratorConst implements TestMethodGenerator { let("limit", limit), let("ivMutation", ivMutation), let("exitCheck", exitCheck), + let("addValue", addValue), """ static int #methodName(int unused0, int unused1) { opaqueReset(); @@ -396,7 +399,7 @@ static class TestMethodGeneratorConst implements TestMethodGenerator { """, loopShape, """ - return sum; + return sum + #addValue; } """ )); @@ -442,6 +445,7 @@ private static String randomExtraCheck() { static class TestMethodGeneratorVars implements TestMethodGenerator { private final String ivMutation = randomIVMutation(); private final String loopShape = randomLoopShape(); + private final String addValue = RANDOM.nextBoolean() ? "0" : "i"; private final String modifyInit = randomModifyValue("init"); private final String modifyLimit = randomModifyValue("limit"); private final String extraCheck = randomExtraCheck(); @@ -451,6 +455,7 @@ static class TestMethodGeneratorVars implements TestMethodGenerator { private final Template.OneArg testTemplate = Template.make("methodName", (String methodName) -> scope( let("ivMutation", ivMutation), let("exitCheck", exitCheck), + let("addValue", addValue), """ static int #methodName(int init, int limit) { opaqueReset(); @@ -461,7 +466,7 @@ static class TestMethodGeneratorVars implements TestMethodGenerator { extraCheck, // extra CmpI/CmpU dominating the loop, might constrain entry value. loopShape, """ - return sum; + return sum + #addValue; } """ )); From a6aeb88a26d26a90be3298487dcc44e7ff8793b0 Mon Sep 17 00:00:00 2001 From: Emanuel Peter Date: Thu, 11 Jun 2026 08:32:36 +0200 Subject: [PATCH 36/44] small documentation fixes --- .../jtreg/compiler/loopopts/TestHasTruncationWrap.java | 10 +++++----- .../compiler/loopopts/TestTruncationWrapFuzzer.java | 3 +++ 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java index 590a01a51b8c0..4eb457db75426 100644 --- a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java +++ b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java @@ -416,7 +416,7 @@ static int testIR3() { // Missed optimization opportunity: // CountedLoopConverter::LoopStructure::is_infinite_loop // It wrongly fires, and prevents CountedLoop detection. - // This check is increment-specific, and fails to accound for decrement: + // This check is increment-specific, and fails to acocunt for decrement: // if (limit_t->hi_as_long() > incr_t->hi_as_long()) { // I don't think this is intentional, because we have handling for positive and // negative stride in CountedLoopConverter::has_truncation_wrap. @@ -432,8 +432,8 @@ private static void runIR3b() { @Test @IR(counts = {IRNode.COUNTED_LOOP, "= 0"}) static int testIR3b() { - int limit = Math.max(lo, 0); // init in [0..max_int] - int init = Math.min(hi, 100); // limit in [min_int..100] + int limit = Math.max(lo, 0); // limit in [0..max_int] + int init = Math.min(hi, 100); // init in [min_int..100] int sum = 0; for (int i = init; i > limit; i = (short)(i-1)) { sum = opaqueSum(sum); // work to keep loop alive @@ -457,8 +457,8 @@ static int testIR3x() { int init = Math.max(lo, 0); // init in [0..max_int] int limit = Math.min(hi, 100); // limit in [min_int..100] int sum = 0; - // No useful CmpI before the loop. - // And the CmpI of the for limit is NEQ, so not useful either. + // No useful CmpI before the loop. + // And the CmpI of the for limit is NEQ, so not useful either. for (int i = init; i != limit; i = (short)(i+1)) { sum = opaqueSum(sum); // work to keep loop alive } diff --git a/test/hotspot/jtreg/compiler/loopopts/TestTruncationWrapFuzzer.java b/test/hotspot/jtreg/compiler/loopopts/TestTruncationWrapFuzzer.java index 45a90139cef81..4adc9a22f18f6 100644 --- a/test/hotspot/jtreg/compiler/loopopts/TestTruncationWrapFuzzer.java +++ b/test/hotspot/jtreg/compiler/loopopts/TestTruncationWrapFuzzer.java @@ -57,6 +57,9 @@ * * So far, this test does not have IR verification, only result verification. * + * This test generates a wide range of patterns, and will require a lot of + * runs to find a specific code shape. + * * Features: * - Truncation patterns, see TRUNCATIONS and randomIVMutation. * - Stride: positive, negative, small and large, see ivMutationWithRandomStride. From 66f9804fab26cd96fd5dbe5782debed1888c24d8 Mon Sep 17 00:00:00 2001 From: Emanuel Peter Date: Thu, 11 Jun 2026 08:53:52 +0200 Subject: [PATCH 37/44] IR tests for byte and char --- .../loopopts/TestHasTruncationWrap.java | 549 +++++++++++++----- 1 file changed, 397 insertions(+), 152 deletions(-) diff --git a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java index 4eb457db75426..ae4ce42c8f963 100644 --- a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java +++ b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java @@ -193,18 +193,18 @@ public static int opaqueSum(int i, int j) { public static int lo = 11; public static int hi = 33; - // testIR0: just a regular int loop - public static int testIR0_gold = testIR0(); + // testIRShort0: just a regular int loop + public static int testIRShort0_gold = testIRShort0(); - @Run(test = "testIR0") - private static void runIR0() { - int val = testIR0(); - if (val != testIR0_gold) { throw new RuntimeException("wrong value: " + testIR0_gold + " vs " + val); } + @Run(test = "testIRShort0") + private static void runIRShort0() { + int val = testIRShort0(); + if (val != testIRShort0_gold) { throw new RuntimeException("wrong value: " + testIRShort0_gold + " vs " + val); } } @Test @IR(counts = {IRNode.COUNTED_LOOP, "> 0"}) - static int testIR0() { + static int testIRShort0() { int init = lo; int limit = hi; int sum = 0; @@ -214,18 +214,18 @@ static int testIR0() { return sum; } - // testIR0b: just a regular int loop, but with NEQ exit check. - public static int testIR0b_gold = testIR0b(); + // testIRShort0b: just a regular int loop, but with NEQ exit check. + public static int testIRShort0b_gold = testIRShort0b(); - @Run(test = "testIR0b") - private static void runIR0b() { - int val = testIR0b(); - if (val != testIR0b_gold) { throw new RuntimeException("wrong value: " + testIR0b_gold + " vs " + val); } + @Run(test = "testIRShort0b") + private static void runIRShort0b() { + int val = testIRShort0b(); + if (val != testIRShort0b_gold) { throw new RuntimeException("wrong value: " + testIRShort0b_gold + " vs " + val); } } @Test @IR(counts = {IRNode.COUNTED_LOOP, "> 0"}) - static int testIR0b() { + static int testIRShort0b() { int init = lo; int limit = hi; int sum = 0; @@ -235,18 +235,18 @@ static int testIR0b() { return sum; } - // testIR1: short loop, but values are trivially in short range. - public static int testIR1_gold = testIR1(); + // testIRShort1: short loop, but values are trivially in short range. + public static int testIRShort1_gold = testIRShort1(); - @Run(test = "testIR1") - private static void runIR1() { - int val = testIR1(); - if (val != testIR1_gold) { throw new RuntimeException("wrong value: " + testIR1_gold + " vs " + val); } + @Run(test = "testIRShort1") + private static void runIRShort1() { + int val = testIRShort1(); + if (val != testIRShort1_gold) { throw new RuntimeException("wrong value: " + testIRShort1_gold + " vs " + val); } } @Test @IR(counts = {IRNode.COUNTED_LOOP, "> 0"}) - static int testIR1() { + static int testIRShort1() { short init = (short)lo; short limit = (short)hi; int sum = 0; @@ -256,18 +256,18 @@ static int testIR1() { return sum; } - // testIR1b: short loop, but values are trivially in short range. Decrement iv. - public static int testIR1b_gold = testIR1b(); + // testIRShort1b: short loop, but values are trivially in short range. Decrement iv. + public static int testIRShort1b_gold = testIRShort1b(); - @Run(test = "testIR1b") - private static void runIR1b() { - int val = testIR1b(); - if (val != testIR1b_gold) { throw new RuntimeException("wrong value: " + testIR1b_gold + " vs " + val); } + @Run(test = "testIRShort1b") + private static void runIRShort1b() { + int val = testIRShort1b(); + if (val != testIRShort1b_gold) { throw new RuntimeException("wrong value: " + testIRShort1b_gold + " vs " + val); } } @Test @IR(counts = {IRNode.COUNTED_LOOP, "> 0"}) - static int testIR1b() { + static int testIRShort1b() { short init = (short)hi; short limit = (short)lo; int sum = 0; @@ -277,19 +277,19 @@ static int testIR1b() { return sum; } - // testIR1c: short loop, but values are trivially in short range. Incr by 2. + // testIRShort1c: short loop, but values are trivially in short range. Incr by 2. // Not safe: lo=32766+2 would wrap past short_max. - public static int testIR1c_gold = testIR1c(); + public static int testIRShort1c_gold = testIRShort1c(); - @Run(test = "testIR1c") - private static void runIR1c() { - int val = testIR1c(); - if (val != testIR1c_gold) { throw new RuntimeException("wrong value: " + testIR1c_gold + " vs " + val); } + @Run(test = "testIRShort1c") + private static void runIRShort1c() { + int val = testIRShort1c(); + if (val != testIRShort1c_gold) { throw new RuntimeException("wrong value: " + testIRShort1c_gold + " vs " + val); } } @Test @IR(counts = {IRNode.COUNTED_LOOP, "= 0"}) - static int testIR1c() { + static int testIRShort1c() { short init = (short)lo; short limit = (short)hi; int sum = 0; @@ -299,19 +299,19 @@ static int testIR1c() { return sum; } - // testIR1d: short loop, but values are trivially in short range. Decrement iv by 2. + // testIRShort1d: short loop, but values are trivially in short range. Decrement iv by 2. // Not safe: lo=-32767-2 would wrap past short_min. - public static int testIR1d_gold = testIR1d(); + public static int testIRShort1d_gold = testIRShort1d(); - @Run(test = "testIR1d") - private static void runIR1d() { - int val = testIR1d(); - if (val != testIR1d_gold) { throw new RuntimeException("wrong value: " + testIR1d_gold + " vs " + val); } + @Run(test = "testIRShort1d") + private static void runIRShort1d() { + int val = testIRShort1d(); + if (val != testIRShort1d_gold) { throw new RuntimeException("wrong value: " + testIRShort1d_gold + " vs " + val); } } @Test @IR(counts = {IRNode.COUNTED_LOOP, "= 0"}) - static int testIR1d() { + static int testIRShort1d() { short init = (short)hi; short limit = (short)lo; int sum = 0; @@ -321,18 +321,18 @@ static int testIR1d() { return sum; } - // testIR2: short loop, ranges proved in short range via CmpI before loop. - public static int testIR2_gold = testIR2(); + // testIRShort2: short loop, ranges proved in short range via CmpI before loop. + public static int testIRShort2_gold = testIRShort2(); - @Run(test = "testIR2") - private static void runIR2() { - int val = testIR2(); - if (val != testIR2_gold) { throw new RuntimeException("wrong value: " + testIR2_gold + " vs " + val); } + @Run(test = "testIRShort2") + private static void runIRShort2() { + int val = testIRShort2(); + if (val != testIRShort2_gold) { throw new RuntimeException("wrong value: " + testIRShort2_gold + " vs " + val); } } @Test @IR(counts = {IRNode.COUNTED_LOOP, "> 0"}) - static int testIR2() { + static int testIRShort2() { int init = Math.max(lo, 0); // init in [0..max_int] int limit = Math.min(hi, 100); // limit in [min_int..100] if (init >= limit) { return -1; } // CmpI before loop @@ -351,19 +351,19 @@ static int testIR2() { return sum; } - // testIR2b: short loop, ranges proved in short range via CmpI before loop. - // Compared to testIR2, the check in the loop is an NEQ. - public static int testIR2b_gold = testIR2b(); + // testIRShort2b: short loop, ranges proved in short range via CmpI before loop. + // Compared to testIRShort2, the check in the loop is an NEQ. + public static int testIRShort2b_gold = testIRShort2b(); - @Run(test = "testIR2b") - private static void runIR2b() { - int val = testIR2b(); - if (val != testIR2b_gold) { throw new RuntimeException("wrong value: " + testIR2b_gold + " vs " + val); } + @Run(test = "testIRShort2b") + private static void runIRShort2b() { + int val = testIRShort2b(); + if (val != testIRShort2b_gold) { throw new RuntimeException("wrong value: " + testIRShort2b_gold + " vs " + val); } } @Test @IR(counts = {IRNode.COUNTED_LOOP, "> 0"}) - static int testIR2b() { + static int testIRShort2b() { int init = Math.max(lo, 0); // init in [0..max_int] int limit = Math.min(hi, 100); // limit in [min_int..100] if (init >= limit) { return -1; } // CmpI before loop @@ -382,18 +382,18 @@ static int testIR2b() { return sum; } - // testIR3: short loop, and range in short range via CmpI before loop (for loop limit). - public static int testIR3_gold = testIR3(); + // testIRShort3: short loop, and range in short range via CmpI before loop (for loop limit). + public static int testIRShort3_gold = testIRShort3(); - @Run(test = "testIR3") - private static void runIR3() { - int val = testIR3(); - if (val != testIR3_gold) { throw new RuntimeException("wrong value: " + testIR3_gold + " vs " + val); } + @Run(test = "testIRShort3") + private static void runIRShort3() { + int val = testIRShort3(); + if (val != testIRShort3_gold) { throw new RuntimeException("wrong value: " + testIRShort3_gold + " vs " + val); } } @Test @IR(counts = {IRNode.COUNTED_LOOP, "> 0"}) - static int testIR3() { + static int testIRShort3() { int init = Math.max(lo, 0); // init in [0..max_int] int limit = Math.min(hi, 100); // limit in [min_int..100] int sum = 0; @@ -411,7 +411,7 @@ static int testIR3() { return sum; } - // testIR3b: short loop, and range in short range via CmpI before loop (for loop limit). + // testIRShort3b: short loop, and range in short range via CmpI before loop (for loop limit). // Decr iv. // Missed optimization opportunity: // CountedLoopConverter::LoopStructure::is_infinite_loop @@ -421,17 +421,17 @@ static int testIR3() { // I don't think this is intentional, because we have handling for positive and // negative stride in CountedLoopConverter::has_truncation_wrap. // TODO: file RFE - public static int testIR3b_gold = testIR3b(); + public static int testIRShort3b_gold = testIRShort3b(); - @Run(test = "testIR3b") - private static void runIR3b() { - int val = testIR3b(); - if (val != testIR3b_gold) { throw new RuntimeException("wrong value: " + testIR3b_gold + " vs " + val); } + @Run(test = "testIRShort3b") + private static void runIRShort3b() { + int val = testIRShort3b(); + if (val != testIRShort3b_gold) { throw new RuntimeException("wrong value: " + testIRShort3b_gold + " vs " + val); } } @Test @IR(counts = {IRNode.COUNTED_LOOP, "= 0"}) - static int testIR3b() { + static int testIRShort3b() { int limit = Math.max(lo, 0); // limit in [0..max_int] int init = Math.min(hi, 100); // init in [min_int..100] int sum = 0; @@ -441,19 +441,19 @@ static int testIR3b() { return sum; } - // testIR3x: short loop, fails to be recognized as CountedLoop. - // Compared to testIR3, the check in the loop is an NEQ. - public static int testIR3x_gold = testIR3x(); + // testIRShort3x: short loop, fails to be recognized as CountedLoop. + // Compared to testIRShort3, the check in the loop is an NEQ. + public static int testIRShort3x_gold = testIRShort3x(); - @Run(test = "testIR3x") - private static void runIR3x() { - int val = testIR3x(); - if (val != testIR3x_gold) { throw new RuntimeException("wrong value: " + testIR3x_gold + " vs " + val); } + @Run(test = "testIRShort3x") + private static void runIRShort3x() { + int val = testIRShort3x(); + if (val != testIRShort3x_gold) { throw new RuntimeException("wrong value: " + testIRShort3x_gold + " vs " + val); } } @Test @IR(counts = {IRNode.COUNTED_LOOP, "= 0"}) - static int testIR3x() { + static int testIRShort3x() { int init = Math.max(lo, 0); // init in [0..max_int] int limit = Math.min(hi, 100); // limit in [min_int..100] int sum = 0; @@ -465,18 +465,18 @@ static int testIR3x() { return sum; } - // testIR4: short loop, with a CmpI, but the limit ranges are bad. - public static int testIR4_gold = testIR4(); + // testIRShort4: short loop, with a CmpI, but the limit ranges are bad. + public static int testIRShort4_gold = testIRShort4(); - @Run(test = "testIR4") - private static void runIR4() { - int val = testIR4(); - if (val != testIR4_gold) { throw new RuntimeException("wrong value: " + testIR4_gold + " vs " + val); } + @Run(test = "testIRShort4") + private static void runIRShort4() { + int val = testIRShort4(); + if (val != testIRShort4_gold) { throw new RuntimeException("wrong value: " + testIRShort4_gold + " vs " + val); } } @Test @IR(counts = {IRNode.COUNTED_LOOP, "= 0"}) - static int testIR4() { + static int testIRShort4() { int init = Math.max(lo, 0); // init in [0..max_int] int limit = Math.min(hi, 100_000); // limit in [min_int..100_000] int sum = 0; @@ -494,18 +494,18 @@ static int testIR4() { return sum; } - // testIR5: short do-while-loop, and range in short range via CmpI before loop (for loop limit). - public static int testIR5_gold = testIR5(); + // testIRShort5: short do-while-loop, and range in short range via CmpI before loop (for loop limit). + public static int testIRShort5_gold = testIRShort5(); - @Run(test = "testIR5") - private static void runIR5() { - int val = testIR5(); - if (val != testIR5_gold) { throw new RuntimeException("wrong value: " + testIR5_gold + " vs " + val); } + @Run(test = "testIRShort5") + private static void runIRShort5() { + int val = testIRShort5(); + if (val != testIRShort5_gold) { throw new RuntimeException("wrong value: " + testIRShort5_gold + " vs " + val); } } @Test @IR(counts = {IRNode.COUNTED_LOOP, "> 0"}) - static int testIR5() { + static int testIRShort5() { int init = Math.max(lo, 0); // init in [0..max_int] int limit = Math.min(hi, 100); // limit in [min_int..100] if (init >= limit) { return -1; } // CmpI before loop @@ -522,19 +522,19 @@ static int testIR5() { return sum; } - // testIR5b: short do-while-loop, but the backedge check with NEQ is not strong enough to prevent wrapping. - // Compared to testIR5, the check in the loop is an NEQ. - public static int testIR5b_gold = testIR5b(); + // testIRShort5b: short do-while-loop, but the backedge check with NEQ is not strong enough to prevent wrapping. + // Compared to testIRShort5, the check in the loop is an NEQ. + public static int testIRShort5b_gold = testIRShort5b(); - @Run(test = "testIR5b") - private static void runIR5b() { - int val = testIR5b(); - if (val != testIR5b_gold) { throw new RuntimeException("wrong value: " + testIR5b_gold + " vs " + val); } + @Run(test = "testIRShort5b") + private static void runIRShort5b() { + int val = testIRShort5b(); + if (val != testIRShort5b_gold) { throw new RuntimeException("wrong value: " + testIRShort5b_gold + " vs " + val); } } @Test @IR(counts = {IRNode.COUNTED_LOOP, "= 0"}) - static int testIR5b() { + static int testIRShort5b() { int init = Math.max(lo, 0); // init in [0..max_int] int limit = Math.min(hi, 100); // limit in [min_int..100] if (init >= limit) { return -1; } // CmpI before loop @@ -551,8 +551,8 @@ static int testIR5b() { return sum; } - // testIR5c: short do-while-loop. - // While the code shape looks very close to testIR2b, it does not behave the same. + // testIRShort5c: short do-while-loop. + // While the code shape looks very close to testIRShort2b, it does not behave the same. // The while loop below is peeled once. The additional "exit check" is eliminated, // because redundant after "init >= limit" check. // From peeling, the new initial value is a truncated short value, and not init, so @@ -561,17 +561,17 @@ static int testIR5b() { // guarantee that there is no short-overflow (wrap): we do not manage to // prove that i could never be short_max, and then overflow the short range at // the next increment. - public static int testIR5c_gold = testIR5c(); + public static int testIRShort5c_gold = testIRShort5c(); - @Run(test = "testIR5c") - private static void runIR5c() { - int val = testIR5c(); - if (val != testIR5c_gold) { throw new RuntimeException("wrong value: " + testIR5c_gold + " vs " + val); } + @Run(test = "testIRShort5c") + private static void runIRShort5c() { + int val = testIRShort5c(); + if (val != testIRShort5c_gold) { throw new RuntimeException("wrong value: " + testIRShort5c_gold + " vs " + val); } } @Test @IR(counts = {IRNode.COUNTED_LOOP, "= 0"}) - static int testIR5c() { + static int testIRShort5c() { int init = Math.max(lo, 0); // init in [0..max_int] int limit = Math.min(hi, 100); // limit in [min_int..100] if (init >= limit) { return -1; } // CmpI before loop @@ -589,20 +589,20 @@ static int testIR5c() { return sum; } - // testIR5d: short while-loop, again similar to testIR2b and testIR5c, but with while-loop form. + // testIRShort5d: short while-loop, again similar to testIRShort2b and testIRShort5c, but with while-loop form. // No peeling, and so the entry value is init, and so the "init >= limit" check is useful, // and used by has_truncation_wrap. With it, C2 manages to prove no short-overflow. - public static int testIR5d_gold = testIR5d(); + public static int testIRShort5d_gold = testIRShort5d(); - @Run(test = "testIR5d") - private static void runIR5d() { - int val = testIR5d(); - if (val != testIR5d_gold) { throw new RuntimeException("wrong value: " + testIR5d_gold + " vs " + val); } + @Run(test = "testIRShort5d") + private static void runIRShort5d() { + int val = testIRShort5d(); + if (val != testIRShort5d_gold) { throw new RuntimeException("wrong value: " + testIRShort5d_gold + " vs " + val); } } @Test @IR(counts = {IRNode.COUNTED_LOOP, "> 0"}) - static int testIR5d() { + static int testIRShort5d() { int init = Math.max(lo, 0); // init in [0..max_int] int limit = Math.min(hi, 100); // limit in [min_int..100] if (init >= limit) { return -1; } // CmpI before loop @@ -619,18 +619,18 @@ static int testIR5d() { return sum; } - // testIR6: short do-while-loop, missing the CmpI before the loop. - public static int testIR6_gold = testIR6(); + // testIRShort6: short do-while-loop, missing the CmpI before the loop. + public static int testIRShort6_gold = testIRShort6(); - @Run(test = "testIR6") - private static void runIR6() { - int val = testIR6(); - if (val != testIR6_gold) { throw new RuntimeException("wrong value: " + testIR6_gold + " vs " + val); } + @Run(test = "testIRShort6") + private static void runIRShort6() { + int val = testIRShort6(); + if (val != testIRShort6_gold) { throw new RuntimeException("wrong value: " + testIRShort6_gold + " vs " + val); } } @Test @IR(counts = {IRNode.COUNTED_LOOP, "> 0"}) - static int testIR6() { + static int testIRShort6() { int init = Math.max(lo, 0); // init in [0..max_int] int limit = Math.min(hi, 100); // limit in [min_int..100] // No CmpI before the loop! @@ -644,19 +644,19 @@ static int testIR6() { return sum; } - // testIR6b: short do-while-loop, missing the CmpI before the loop. - // Compared to testIR6, the check in the loop is an NEQ. - public static int testIR6b_gold = testIR6b(); + // testIRShort6b: short do-while-loop, missing the CmpI before the loop. + // Compared to testIRShort6, the check in the loop is an NEQ. + public static int testIRShort6b_gold = testIRShort6b(); - @Run(test = "testIR6b") - private static void runIR6b() { - int val = testIR6b(); - if (val != testIR6b_gold) { throw new RuntimeException("wrong value: " + testIR6b_gold + " vs " + val); } + @Run(test = "testIRShort6b") + private static void runIRShort6b() { + int val = testIRShort6b(); + if (val != testIRShort6b_gold) { throw new RuntimeException("wrong value: " + testIRShort6b_gold + " vs " + val); } } @Test @IR(counts = {IRNode.COUNTED_LOOP, "= 0"}) - static int testIR6b() { + static int testIRShort6b() { int init = Math.max(lo, 0); // init in [0..max_int] int limit = Math.min(hi, 100); // limit in [min_int..100] // No CmpI before the loop! @@ -682,21 +682,21 @@ public static boolean opaqueCheck() { return (opaqueCounter++) >= 100_000; } - // testIR7: with additional opaque exit check. + // testIRShort7: with additional opaque exit check. // Useful to verify that TestTruncationWrapFuzzer.java opaque exit checks // do not prohibit CountedLoop detection. - // We start from testIR3 and testIR4, but add the additional opaque exit. - public static int testIR7_gold = testIR7(); + // We start from testIRShort3 and testIRShort4, but add the additional opaque exit. + public static int testIRShort7_gold = testIRShort7(); - @Run(test = "testIR7") - private static void runIR7() { - int val = testIR7(); - if (val != testIR7_gold) { throw new RuntimeException("wrong value: " + testIR7_gold + " vs " + val); } + @Run(test = "testIRShort7") + private static void runIRShort7() { + int val = testIRShort7(); + if (val != testIRShort7_gold) { throw new RuntimeException("wrong value: " + testIRShort7_gold + " vs " + val); } } @Test @IR(counts = {IRNode.COUNTED_LOOP, "> 0"}) - static int testIR7() { + static int testIRShort7() { opaqueReset(); int init = Math.max(lo, 0); int limit = Math.min(hi, 100); // good bounds @@ -708,18 +708,18 @@ static int testIR7() { return sum; } - // testIR7b - public static int testIR7b_gold = testIR7b(); + // testIRShort7b + public static int testIRShort7b_gold = testIRShort7b(); - @Run(test = "testIR7b") - private static void runIR7b() { - int val = testIR7b(); - if (val != testIR7b_gold) { throw new RuntimeException("wrong value: " + testIR7b_gold + " vs " + val); } + @Run(test = "testIRShort7b") + private static void runIRShort7b() { + int val = testIRShort7b(); + if (val != testIRShort7b_gold) { throw new RuntimeException("wrong value: " + testIRShort7b_gold + " vs " + val); } } @Test @IR(counts = {IRNode.COUNTED_LOOP, "= 0"}) - static int testIR7b() { + static int testIRShort7b() { opaqueReset(); int init = Math.max(lo, 0); int limit = Math.min(hi, 100_000); // bad bounds @@ -731,10 +731,255 @@ static int testIR7b() { return sum; } - // TODO: ensure coverage - // - char, byte and short truncation - // - check for IRNode.COUNTED_LOOP - // - opaqueSum call to prevent empty loop - // - increment and decrement cases, non-unit stride - // - Cases with and without compare before loop: positive and negative tests + // testIRByte1: byte loop, but values are trivially in byte range. + // TODO: why not recognized? + public static int testIRByte1_gold = testIRByte1(); + + @Run(test = "testIRByte1") + private static void runIRByte1() { + int val = testIRByte1(); + if (val != testIRByte1_gold) { throw new RuntimeException("wrong value: " + testIRByte1_gold + " vs " + val); } + } + + @Test + @IR(counts = {IRNode.COUNTED_LOOP, "> 0"}) + static int testIRByte1() { + byte init = (byte)lo; + byte limit = (byte)hi; + int sum = 0; + for (byte i = init; i < limit; i++) { + sum = opaqueSum(sum); // work to keep loop alive + } + return sum; + } + + // testIRByte2: byte loop, ranges proved in byte range via CmpI before loop. + // TODO: why not recognized? + public static int testIRByte2_gold = testIRByte2(); + + @Run(test = "testIRByte2") + private static void runIRByte2() { + int val = testIRByte2(); + if (val != testIRByte2_gold) { throw new RuntimeException("wrong value: " + testIRByte2_gold + " vs " + val); } + } + + @Test + @IR(counts = {IRNode.COUNTED_LOOP, "> 0"}) + static int testIRByte2() { + int init = Math.max(lo, 0); // init in [0..max_int] + int limit = Math.min(hi, 100); // limit in [min_int..100] + if (init >= limit) { return -1; } // CmpI before loop + // -> init < limit <= 100 + // -> filtered_int_type return [min_int..99] + // -> and intersected with its previous type [0..max_int] + // we get init in [0..99], which is in byte range. + int sum = 0; + for (int i = init; i < limit; i = (byte)(i+1)) { + sum = opaqueSum(sum); // work to keep loop alive + // The backedge value of i is also far + // enough from byte boundaries, because of + // the loop exit check: + // i < limit <= 100 + } + return sum; + } + + // testIRByte4: byte loop, with a CmpI, but the limit ranges are bad. + public static int testIRByte4_gold = testIRByte4(); + + @Run(test = "testIRByte4") + private static void runIRByte4() { + int val = testIRByte4(); + if (val != testIRByte4_gold) { throw new RuntimeException("wrong value: " + testIRByte4_gold + " vs " + val); } + } + + @Test + @IR(counts = {IRNode.COUNTED_LOOP, "= 0"}) + static int testIRByte4() { + int init = Math.max(lo, 0); // init in [0..max_int] + int limit = Math.min(hi, 1_000); // limit in [min_int..1_000] + int sum = 0; + // Now, the check is not good enough: + // -> init < limit <= 1_000 + // -> filtered_int_type return [min_int..999] + // -> and intersected with its previous type [0..max_int] + // we get init in [0..999], which is NOT in byte range. + for (int i = init; i < limit; i = (byte)(i+1)) { + sum = opaqueSum(sum); // work to keep loop alive + // Also: the backedge range is not good because + // the exit check is not strong enough for byte: + // i < limit <= 1_000 + } + return sum; + } + + // testIRChar1: char loop, but values are trivially in char range. + // TODO: why not recognized? + public static int testIRChar1_gold = testIRChar1(); + + @Run(test = "testIRChar1") + private static void runIRChar1() { + int val = testIRChar1(); + if (val != testIRChar1_gold) { throw new RuntimeException("wrong value: " + testIRChar1_gold + " vs " + val); } + } + + @Test + @IR(counts = {IRNode.COUNTED_LOOP, "> 0"}) + static int testIRChar1() { + char init = (char)lo; + char limit = (char)hi; + int sum = 0; + for (char i = init; i < limit; i++) { + sum = opaqueSum(sum); // work to keep loop alive + } + return sum; + } + + // testIRChar2: char loop, ranges proved in char range via CmpI before loop. + // TODO: why not recognized? + public static int testIRChar2_gold = testIRChar2(); + + @Run(test = "testIRChar2") + private static void runIRChar2() { + int val = testIRChar2(); + if (val != testIRChar2_gold) { throw new RuntimeException("wrong value: " + testIRChar2_gold + " vs " + val); } + } + + @Test + @IR(counts = {IRNode.COUNTED_LOOP, "> 0"}) + static int testIRChar2() { + int init = Math.max(lo, 0); // init in [0..max_int] + int limit = Math.min(hi, 100); // limit in [min_int..100] + if (init >= limit) { return -1; } // CmpI before loop + // -> init < limit <= 100 + // -> filtered_int_type return [min_int..99] + // -> and intersected with its previous type [0..max_int] + // we get init in [0..99], which is in char range. + int sum = 0; + for (int i = init; i < limit; i = (char)(i+1)) { + sum = opaqueSum(sum); // work to keep loop alive + // The backedge value of i is also far + // enough from char boundaries, because of + // the loop exit check: + // i < limit <= 100 + } + return sum; + } + + // testIRChar3: char loop, and range in char range via CmpI before loop (for loop limit). + // TODO: why not recognized? + public static int testIRChar3_gold = testIRChar3(); + + @Run(test = "testIRChar3") + private static void runIRChar3() { + int val = testIRChar3(); + if (val != testIRChar3_gold) { throw new RuntimeException("wrong value: " + testIRChar3_gold + " vs " + val); } + } + + @Test + @IR(counts = {IRNode.COUNTED_LOOP, "> 0"}) + static int testIRChar3() { + int init = Math.max(lo, 0); // init in [0..max_int] + int limit = Math.min(hi, 100); // limit in [min_int..100] + int sum = 0; + // While there is no explicit CmpI before the loop, we + // actually have "i < limit" in the for loop check, which + // is also checked before entering the loop. + // So also here, we have: + // -> init < limit <= 100 + // -> filtered_int_type return [min_int..99] + // -> and intersected with its previous type [0..max_int] + // we get init in [0..99], which is in char range. + for (int i = init; i < limit; i = (char)(i+1)) { + sum = opaqueSum(sum); // work to keep loop alive + } + return sum; + } + + // testIRChar3Mask: char loop, and range in char range via CmpI before loop (for loop limit). + public static int testIRChar3Mask_gold = testIRChar3Mask(); + + @Run(test = "testIRChar3Mask") + private static void runIRChar3Mask() { + int val = testIRChar3Mask(); + if (val != testIRChar3Mask_gold) { throw new RuntimeException("wrong value: " + testIRChar3Mask_gold + " vs " + val); } + } + + @Test + @IR(counts = {IRNode.COUNTED_LOOP, "> 0"}) + static int testIRChar3Mask() { + int init = Math.max(lo, 0); // init in [0..max_int] + int limit = Math.min(hi, 100); // limit in [min_int..100] + int sum = 0; + // While there is no explicit CmpI before the loop, we + // actually have "i < limit" in the for loop check, which + // is also checked before entering the loop. + // So also here, we have: + // -> init < limit <= 100 + // -> filtered_int_type return [min_int..99] + // -> and intersected with its previous type [0..max_int] + // we get init in [0..99], which is in char range. + for (int i = init; i < limit; i = (i+1) & 0x7fff) { // mask instead of cast + sum = opaqueSum(sum); // work to keep loop alive + } + return sum; + } + + // testIRChar4: char loop, with a CmpI, but the limit ranges are bad. + public static int testIRChar4_gold = testIRChar4(); + + @Run(test = "testIRChar4") + private static void runIRChar4() { + int val = testIRChar4(); + if (val != testIRChar4_gold) { throw new RuntimeException("wrong value: " + testIRChar4_gold + " vs " + val); } + } + + @Test + @IR(counts = {IRNode.COUNTED_LOOP, "= 0"}) + static int testIRChar4() { + int init = Math.max(lo, 0); // init in [0..max_int] + int limit = Math.min(hi, 100_000); // limit in [min_int..100_000] + int sum = 0; + // Now, the check is not good enough: + // -> init < limit <= 100_000 + // -> filtered_int_type return [min_int..99_999] + // -> and intersected with its previous type [0..max_int] + // we get init in [0..99_999], which is NOT in char range. + for (int i = init; i < limit; i = (char)(i+1)) { + sum = opaqueSum(sum); // work to keep loop alive + // Also: the backedge range is not good because + // the exit check is not strong enough for char: + // i < limit <= 100_000 + } + return sum; + } + + // testIRChar4Mask: char loop, with a CmpI, but the limit ranges are bad. + public static int testIRChar4Mask_gold = testIRChar4Mask(); + + @Run(test = "testIRChar4Mask") + private static void runIRChar4Mask() { + int val = testIRChar4Mask(); + if (val != testIRChar4Mask_gold) { throw new RuntimeException("wrong value: " + testIRChar4Mask_gold + " vs " + val); } + } + + @Test + @IR(counts = {IRNode.COUNTED_LOOP, "= 0"}) + static int testIRChar4Mask() { + int init = Math.max(lo, 0); // init in [0..max_int] + int limit = Math.min(hi, 100_000); // limit in [min_int..100_000] + int sum = 0; + // Now, the check is not good enough: + // -> init < limit <= 100_000 + // -> filtered_int_type return [min_int..99_999] + // -> and intersected with its previous type [0..max_int] + // we get init in [0..99_999], which is NOT in char range. + for (int i = init; i < limit; i = (i+1) & 0x7fff) { // mask instead of cast + sum = opaqueSum(sum); // work to keep loop alive + // Also: the backedge range is not good because + // the exit check is not strong enough for char: + // i < limit <= 100_000 + } + return sum; + } } From c89892ac983e3e5f088f6775f0c57fcb5284eb93 Mon Sep 17 00:00:00 2001 From: Emanuel Peter Date: Thu, 11 Jun 2026 09:09:39 +0200 Subject: [PATCH 38/44] more comments and fixes --- .../loopopts/TestHasTruncationWrap.java | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java index ae4ce42c8f963..57b5fe475db9d 100644 --- a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java +++ b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java @@ -732,7 +732,9 @@ static int testIRShort7b() { } // testIRByte1: byte loop, but values are trivially in byte range. - // TODO: why not recognized? + // But: "byte i++" goes through "<< 24 >> 24" truncation with signed extension, + // and that's not recognized by TruncatedIncrement::build. + // TODO: can we get masking or shift with 8 to work? public static int testIRByte1_gold = testIRByte1(); @Run(test = "testIRByte1") @@ -742,7 +744,7 @@ private static void runIRByte1() { } @Test - @IR(counts = {IRNode.COUNTED_LOOP, "> 0"}) + @IR(counts = {IRNode.COUNTED_LOOP, "= 0"}) static int testIRByte1() { byte init = (byte)lo; byte limit = (byte)hi; @@ -754,7 +756,9 @@ static int testIRByte1() { } // testIRByte2: byte loop, ranges proved in byte range via CmpI before loop. - // TODO: why not recognized? + // But: "byte i++" goes through "<< 24 >> 24" truncation with signed extension, + // and that's not recognized by TruncatedIncrement::build. + // TODO: can we get masking or shift with 8 to work? public static int testIRByte2_gold = testIRByte2(); @Run(test = "testIRByte2") @@ -764,7 +768,7 @@ private static void runIRByte2() { } @Test - @IR(counts = {IRNode.COUNTED_LOOP, "> 0"}) + @IR(counts = {IRNode.COUNTED_LOOP, "= 0"}) static int testIRByte2() { int init = Math.max(lo, 0); // init in [0..max_int] int limit = Math.min(hi, 100); // limit in [min_int..100] @@ -785,6 +789,9 @@ static int testIRByte2() { } // testIRByte4: byte loop, with a CmpI, but the limit ranges are bad. + // And: "byte i++" goes through "<< 24 >> 24" truncation with signed extension, + // and that's not recognized by TruncatedIncrement::build. + // TODO: alternative case that fails for the right reason? public static int testIRByte4_gold = testIRByte4(); @Run(test = "testIRByte4") @@ -814,7 +821,8 @@ static int testIRByte4() { } // testIRChar1: char loop, but values are trivially in char range. - // TODO: why not recognized? + // But: "char i++" lowers through mask "& 0xffff", not recognized by TruncatedIncrement::build. + // TODO: alternative that could work? public static int testIRChar1_gold = testIRChar1(); @Run(test = "testIRChar1") @@ -824,7 +832,7 @@ private static void runIRChar1() { } @Test - @IR(counts = {IRNode.COUNTED_LOOP, "> 0"}) + @IR(counts = {IRNode.COUNTED_LOOP, "= 0"}) static int testIRChar1() { char init = (char)lo; char limit = (char)hi; @@ -836,7 +844,8 @@ static int testIRChar1() { } // testIRChar2: char loop, ranges proved in char range via CmpI before loop. - // TODO: why not recognized? + // But: "char i++" lowers through mask "& 0xffff", not recognized by TruncatedIncrement::build. + // TODO: alternative that could work? public static int testIRChar2_gold = testIRChar2(); @Run(test = "testIRChar2") @@ -846,7 +855,7 @@ private static void runIRChar2() { } @Test - @IR(counts = {IRNode.COUNTED_LOOP, "> 0"}) + @IR(counts = {IRNode.COUNTED_LOOP, "= 0"}) static int testIRChar2() { int init = Math.max(lo, 0); // init in [0..max_int] int limit = Math.min(hi, 100); // limit in [min_int..100] @@ -867,7 +876,8 @@ static int testIRChar2() { } // testIRChar3: char loop, and range in char range via CmpI before loop (for loop limit). - // TODO: why not recognized? + // But: "char i++" lowers through mask "& 0xffff", not recognized by TruncatedIncrement::build. + // TODO: alternative that could work? public static int testIRChar3_gold = testIRChar3(); @Run(test = "testIRChar3") @@ -877,7 +887,7 @@ private static void runIRChar3() { } @Test - @IR(counts = {IRNode.COUNTED_LOOP, "> 0"}) + @IR(counts = {IRNode.COUNTED_LOOP, "= 0"}) static int testIRChar3() { int init = Math.max(lo, 0); // init in [0..max_int] int limit = Math.min(hi, 100); // limit in [min_int..100] @@ -926,6 +936,8 @@ static int testIRChar3Mask() { } // testIRChar4: char loop, with a CmpI, but the limit ranges are bad. + // And: "char i++" lowers through mask "& 0xffff", not recognized by TruncatedIncrement::build. + // TODO: alternative case that fails for the right reason? public static int testIRChar4_gold = testIRChar4(); @Run(test = "testIRChar4") From 873ad952136e650c0c266bb88b8e05dbea349d45 Mon Sep 17 00:00:00 2001 From: Emanuel Peter Date: Thu, 11 Jun 2026 09:57:45 +0200 Subject: [PATCH 39/44] more tests, almost there --- .../loopopts/TestHasTruncationWrap.java | 129 +++++++++++++++++- 1 file changed, 122 insertions(+), 7 deletions(-) diff --git a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java index 57b5fe475db9d..acc21355368d2 100644 --- a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java +++ b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java @@ -734,7 +734,6 @@ static int testIRShort7b() { // testIRByte1: byte loop, but values are trivially in byte range. // But: "byte i++" goes through "<< 24 >> 24" truncation with signed extension, // and that's not recognized by TruncatedIncrement::build. - // TODO: can we get masking or shift with 8 to work? public static int testIRByte1_gold = testIRByte1(); @Run(test = "testIRByte1") @@ -758,7 +757,6 @@ static int testIRByte1() { // testIRByte2: byte loop, ranges proved in byte range via CmpI before loop. // But: "byte i++" goes through "<< 24 >> 24" truncation with signed extension, // and that's not recognized by TruncatedIncrement::build. - // TODO: can we get masking or shift with 8 to work? public static int testIRByte2_gold = testIRByte2(); @Run(test = "testIRByte2") @@ -791,7 +789,6 @@ static int testIRByte2() { // testIRByte4: byte loop, with a CmpI, but the limit ranges are bad. // And: "byte i++" goes through "<< 24 >> 24" truncation with signed extension, // and that's not recognized by TruncatedIncrement::build. - // TODO: alternative case that fails for the right reason? public static int testIRByte4_gold = testIRByte4(); @Run(test = "testIRByte4") @@ -822,7 +819,6 @@ static int testIRByte4() { // testIRChar1: char loop, but values are trivially in char range. // But: "char i++" lowers through mask "& 0xffff", not recognized by TruncatedIncrement::build. - // TODO: alternative that could work? public static int testIRChar1_gold = testIRChar1(); @Run(test = "testIRChar1") @@ -845,7 +841,6 @@ static int testIRChar1() { // testIRChar2: char loop, ranges proved in char range via CmpI before loop. // But: "char i++" lowers through mask "& 0xffff", not recognized by TruncatedIncrement::build. - // TODO: alternative that could work? public static int testIRChar2_gold = testIRChar2(); @Run(test = "testIRChar2") @@ -877,7 +872,6 @@ static int testIRChar2() { // testIRChar3: char loop, and range in char range via CmpI before loop (for loop limit). // But: "char i++" lowers through mask "& 0xffff", not recognized by TruncatedIncrement::build. - // TODO: alternative that could work? public static int testIRChar3_gold = testIRChar3(); @Run(test = "testIRChar3") @@ -937,7 +931,6 @@ static int testIRChar3Mask() { // testIRChar4: char loop, with a CmpI, but the limit ranges are bad. // And: "char i++" lowers through mask "& 0xffff", not recognized by TruncatedIncrement::build. - // TODO: alternative case that fails for the right reason? public static int testIRChar4_gold = testIRChar4(); @Run(test = "testIRChar4") @@ -994,4 +987,126 @@ static int testIRChar4Mask() { } return sum; } + + // testIRShift16: short loop, and range in short range via CmpI before loop (for loop limit). + public static int testIRShift16_gold = testIRShift16(); + + @Run(test = "testIRShift16") + private static void runIRShift16() { + int val = testIRShift16(); + if (val != testIRShift16_gold) { throw new RuntimeException("wrong value: " + testIRShift16_gold + " vs " + val); } + } + + // TODO: why does this fail to compile? + @Test(allowNotCompilable = true) + @IR(counts = {IRNode.COUNTED_LOOP, "> 0"}) + static int testIRShift16() { + int init = Math.max(lo, 0); // init in [0..max_int] + int limit = Math.min(hi, 100); // limit in [min_int..100] + int sum = 0; + // While there is no explicit CmpI before the loop, we + // actually have "i < limit" in the for loop check, which + // is also checked before entering the loop. + // So also here, we have: + // -> init < limit <= 100 + // -> filtered_int_type return [min_int..99] + // -> and intersected with its previous type [0..max_int] + // we get init in [0..99], which is in short range. + for (int i = init; i < limit; i = ((i+1) << 16) >> 16) { // explicit shift instead of cast + sum = opaqueSum(sum); // work to keep loop alive + } + return sum; + } + + // testIRShift16BadBounds: short loop, with a CmpI, but the limit ranges are bad. + public static int testIRShift16BadBounds_gold = testIRShift16BadBounds(); + + @Run(test = "testIRShift16BadBounds") + private static void runIRShift16BadBounds() { + int val = testIRShift16BadBounds(); + if (val != testIRShift16BadBounds_gold) { throw new RuntimeException("wrong value: " + testIRShift16BadBounds_gold + " vs " + val); } + } + + @Test + @IR(counts = {IRNode.COUNTED_LOOP, "= 0"}) + static int testIRShift16BadBounds() { + int init = Math.max(lo, 0); // init in [0..max_int] + int limit = Math.min(hi, 100_000); // limit in [min_int..100_000] + int sum = 0; + // Now, the check is not good enough: + // -> init < limit <= 100_000 + // -> filtered_int_type return [min_int..99_999] + // -> and intersected with its previous type [0..max_int] + // we get init in [0..99_999], which is NOT in short range. + for (int i = init; i < limit; i = ((i+1) << 16) >> 16) { // explicit shift instead of cast + sum = opaqueSum(sum); // work to keep loop alive + // Also: the backedge range is not good because + // the exit check is not strong enough for short: + // i < limit <= 100_000 + } + return sum; + } + + // testIRShift8: 24-bit loop, and range in 24-bit range via CmpI before loop (for loop limit). + // Note: this shift value is strange, we probably wanted to implement byte truncation + // with shift=24, but instead we have 24-bit signed truncation. + // TODO: why does this not work? + public static int testIRShift8_gold = testIRShift8(); + + @Run(test = "testIRShift8") + private static void runIRShift8() { + int val = testIRShift8(); + if (val != testIRShift8_gold) { throw new RuntimeException("wrong value: " + testIRShift8_gold + " vs " + val); } + } + + @Test + @IR(counts = {IRNode.COUNTED_LOOP, "> 0"}) + static int testIRShift8() { + int init = Math.max(lo, 0); // init in [0..max_int] + int limit = Math.min(hi, 100); // limit in [min_int..100] + int sum = 0; + // While there is no explicit CmpI before the loop, we + // actually have "i < limit" in the for loop check, which + // is also checked before entering the loop. + // So also here, we have: + // -> init < limit <= 100 + // -> filtered_int_type return [min_int..99] + // -> and intersected with its previous type [0..max_int] + // we get init in [0..99], which is in 24-bit range. + for (int i = init; i < limit; i = ((i+1) << 8) >> 8) { // explicit shift instead of cast + sum = opaqueSum(sum); // work to keep loop alive + } + return sum; + } + + // testIRShift8BadBounds: 24-bit loop, with a CmpI, but the limit ranges are bad. + // TODO: I'm not so sure about the bad bounds argument here... + // Why can't 1_000 fit in 24-bit? + public static int testIRShift8BadBounds_gold = testIRShift8BadBounds(); + + @Run(test = "testIRShift8BadBounds") + private static void runIRShift8BadBounds() { + int val = testIRShift8BadBounds(); + if (val != testIRShift8BadBounds_gold) { throw new RuntimeException("wrong value: " + testIRShift8BadBounds_gold + " vs " + val); } + } + + @Test + @IR(counts = {IRNode.COUNTED_LOOP, "= 0"}) + static int testIRShift8BadBounds() { + int init = Math.max(lo, 0); // init in [0..max_int] + int limit = Math.min(hi, 1_000); // limit in [min_int..1_000] + int sum = 0; + // Now, the check is not good enough: + // -> init < limit <= 1_000 + // -> filtered_int_type return [min_int..999] + // -> and intersected with its previous type [0..max_int] + // we get init in [0..999], which is NOT in 24-bit range. + for (int i = init; i < limit; i = ((i+1) << 8) >> 8) { // explicit shift instead of cast + sum = opaqueSum(sum); // work to keep loop alive + // Also: the backedge range is not good because + // the exit check is not strong enough for 24-bit: + // i < limit <= 1_000 + } + return sum; + } } From 9797def095fbc2b4f0ab5aae8e7cb2f333acc080 Mon Sep 17 00:00:00 2001 From: Emanuel Peter Date: Thu, 11 Jun 2026 10:51:47 +0200 Subject: [PATCH 40/44] finish up test --- .../loopopts/TestHasTruncationWrap.java | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java index acc21355368d2..df78855407865 100644 --- a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java +++ b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java @@ -420,7 +420,6 @@ static int testIRShort3() { // if (limit_t->hi_as_long() > incr_t->hi_as_long()) { // I don't think this is intentional, because we have handling for positive and // negative stride in CountedLoopConverter::has_truncation_wrap. - // TODO: file RFE public static int testIRShort3b_gold = testIRShort3b(); @Run(test = "testIRShort3b") @@ -997,8 +996,7 @@ private static void runIRShift16() { if (val != testIRShift16_gold) { throw new RuntimeException("wrong value: " + testIRShift16_gold + " vs " + val); } } - // TODO: why does this fail to compile? - @Test(allowNotCompilable = true) + @Test @IR(counts = {IRNode.COUNTED_LOOP, "> 0"}) static int testIRShift16() { int init = Math.max(lo, 0); // init in [0..max_int] @@ -1050,7 +1048,10 @@ static int testIRShift16BadBounds() { // testIRShift8: 24-bit loop, and range in 24-bit range via CmpI before loop (for loop limit). // Note: this shift value is strange, we probably wanted to implement byte truncation // with shift=24, but instead we have 24-bit signed truncation. - // TODO: why does this not work? + // Note2: this pattern would have been supported by TruncatedIncrement::build, but it gets + // modified by LShiftINode::Ideal: + // RShiftI(AddI(LShiftI(Phi, 8), 256), 8) + // The same is explicitly excluded for shift 16, to preserve short/byte idioms. public static int testIRShift8_gold = testIRShift8(); @Run(test = "testIRShift8") @@ -1060,7 +1061,7 @@ private static void runIRShift8() { } @Test - @IR(counts = {IRNode.COUNTED_LOOP, "> 0"}) + @IR(counts = {IRNode.COUNTED_LOOP, "= 0"}) static int testIRShift8() { int init = Math.max(lo, 0); // init in [0..max_int] int limit = Math.min(hi, 100); // limit in [min_int..100] @@ -1080,8 +1081,10 @@ static int testIRShift8() { } // testIRShift8BadBounds: 24-bit loop, with a CmpI, but the limit ranges are bad. - // TODO: I'm not so sure about the bad bounds argument here... - // Why can't 1_000 fit in 24-bit? + // Note: same issues as for testIRShift8. + // Note2: the range argument seems a bit strange here, but it turns out that + // TruncatedIncrement::build maps shift=8 to BYTE, which just shows that + // the implementation confused the shift=24 with shift=8. public static int testIRShift8BadBounds_gold = testIRShift8BadBounds(); @Run(test = "testIRShift8BadBounds") From a1597f4fcf30953930325f166e6eb32db0f76627 Mon Sep 17 00:00:00 2001 From: Emanuel Peter Date: Thu, 11 Jun 2026 12:17:15 +0200 Subject: [PATCH 41/44] small fixes --- .../jtreg/compiler/loopopts/TestHasTruncationWrap.java | 6 +++--- .../jtreg/compiler/loopopts/TestTruncationWrapFuzzer.java | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java index df78855407865..69cf52fcffd5a 100644 --- a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java +++ b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java @@ -1073,7 +1073,7 @@ static int testIRShift8() { // -> init < limit <= 100 // -> filtered_int_type return [min_int..99] // -> and intersected with its previous type [0..max_int] - // we get init in [0..99], which is in 24-bit range. + // we get init in [0..99], which is in 24-bit (and byte) range. for (int i = init; i < limit; i = ((i+1) << 8) >> 8) { // explicit shift instead of cast sum = opaqueSum(sum); // work to keep loop alive } @@ -1103,11 +1103,11 @@ static int testIRShift8BadBounds() { // -> init < limit <= 1_000 // -> filtered_int_type return [min_int..999] // -> and intersected with its previous type [0..max_int] - // we get init in [0..999], which is NOT in 24-bit range. + // we get init in [0..999], which is NOT in byte range. for (int i = init; i < limit; i = ((i+1) << 8) >> 8) { // explicit shift instead of cast sum = opaqueSum(sum); // work to keep loop alive // Also: the backedge range is not good because - // the exit check is not strong enough for 24-bit: + // the exit check is not strong enough for byte: // i < limit <= 1_000 } return sum; diff --git a/test/hotspot/jtreg/compiler/loopopts/TestTruncationWrapFuzzer.java b/test/hotspot/jtreg/compiler/loopopts/TestTruncationWrapFuzzer.java index 4adc9a22f18f6..2f58359aba642 100644 --- a/test/hotspot/jtreg/compiler/loopopts/TestTruncationWrapFuzzer.java +++ b/test/hotspot/jtreg/compiler/loopopts/TestTruncationWrapFuzzer.java @@ -33,7 +33,7 @@ * @run driver ${test.main.class} */ -package compiler.rangechecks; +package compiler.loopopts; import java.util.List; import java.util.ArrayList; @@ -68,7 +68,7 @@ * - Exit checks: random Comparison, see Comparator and Comparison (signed and unsigned). * - For endless loops / loops that would take too long: early exit via opaqueCheck, * Note: it is verified that this does not hinder optimization, see: - * TestHasTruncationWrap.java -> testIR7. + * TestHasTruncationWrap.java -> testIRShort7. * - Interesting loop bounds: init/limit * - constant * - variable, sampled (see getInputTemplate), and modified (no-op, truncate, clamp). From d95562f2580aa879239306bf72aacce5b26c965d Mon Sep 17 00:00:00 2001 From: Emanuel Peter Date: Thu, 11 Jun 2026 12:47:30 +0200 Subject: [PATCH 42/44] more small fixes --- .../jtreg/compiler/loopopts/TestHasTruncationWrap.java | 2 ++ .../jtreg/compiler/loopopts/TestTruncationWrapFuzzer.java | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java index 69cf52fcffd5a..efc3831dc3a21 100644 --- a/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java +++ b/test/hotspot/jtreg/compiler/loopopts/TestHasTruncationWrap.java @@ -1085,6 +1085,8 @@ static int testIRShift8() { // Note2: the range argument seems a bit strange here, but it turns out that // TruncatedIncrement::build maps shift=8 to BYTE, which just shows that // the implementation confused the shift=24 with shift=8. + // Since we map to BYTE, 1_000 would be out of bounds, that's why this + // is still a bad bounds example. public static int testIRShift8BadBounds_gold = testIRShift8BadBounds(); @Run(test = "testIRShift8BadBounds") diff --git a/test/hotspot/jtreg/compiler/loopopts/TestTruncationWrapFuzzer.java b/test/hotspot/jtreg/compiler/loopopts/TestTruncationWrapFuzzer.java index 2f58359aba642..8945b430d7f65 100644 --- a/test/hotspot/jtreg/compiler/loopopts/TestTruncationWrapFuzzer.java +++ b/test/hotspot/jtreg/compiler/loopopts/TestTruncationWrapFuzzer.java @@ -86,7 +86,7 @@ public static void main(String[] args) { long t0 = System.nanoTime(); // Add a java source file. - comp.addJavaSourceCode("compiler.rangecheck.templated.Generated", generate(comp)); + comp.addJavaSourceCode("compiler.loopopts.templated.Generated", generate(comp)); long t1 = System.nanoTime(); // Compile the source file. @@ -95,7 +95,7 @@ public static void main(String[] args) { long t2 = System.nanoTime(); // Run the tests without any additional VM flags. - comp.invoke("compiler.rangecheck.templated.Generated", "main", new Object[] {new String[] {}}); + comp.invoke("compiler.loopopts.templated.Generated", "main", new Object[] {new String[] {}}); long t3 = System.nanoTime(); System.out.println("Code Generation: " + (t1-t0) * 1e-9f); @@ -144,7 +144,7 @@ public static int opaqueSum(int i, int j) { // Create the test class, which runs all testTemplateTokens. return TestFrameworkClass.render( // package and class name. - "compiler.rangecheck.templated", "Generated", + "compiler.loopopts.templated", "Generated", // List of imports. Set.of("compiler.lib.generators.*", "java.util.Random", From d0e511aeb60d764ac4d782ac8a72782b0c057d05 Mon Sep 17 00:00:00 2001 From: Emanuel Peter Date: Fri, 12 Jun 2026 07:34:49 +0200 Subject: [PATCH 43/44] apply review suggestion --- src/hotspot/share/opto/ifnode.cpp | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/hotspot/share/opto/ifnode.cpp b/src/hotspot/share/opto/ifnode.cpp index 5ffeff86fb1ba..d4c91d6675922 100644 --- a/src/hotspot/share/opto/ifnode.cpp +++ b/src/hotspot/share/opto/ifnode.cpp @@ -669,14 +669,11 @@ const TypeInt* IfNode::filtered_int_type(PhaseGVN* gvn, Node* val, Node* if_proj BoolNode* bol = iff->in(1)->as_Bool(); if (bol->in(1) && bol->in(1)->is_Cmp()) { const CmpNode* cmp = bol->in(1)->as_Cmp(); - // Val is always the lhs of the comparision: val cmp2 - if (cmp->in(1) == val) { - if (cmp->Opcode() != Op_CmpI) { - // Only CmpI allowed, assumed by signed logic below. - // We could extend to CmpU in the future, and would - // have to implement unsigned range logic below. - return nullptr; - } + // Val is always the lhs of the comparision: val CmpI cmp2 + if (cmp->Opcode() == Op_CmpI && cmp->in(1) == val) { + // Only CmpI allowed, assumed by signed logic below. + // We could extend to CmpU in the future, and would + // have to implement unsigned range logic below. const TypeInt* cmp2_t = gvn->type(cmp->in(2))->isa_int(); if (cmp2_t != nullptr) { jint lo = cmp2_t->_lo; From db51806d68e1ad74f0bb3d30feb67bc831bce08f Mon Sep 17 00:00:00 2001 From: Emanuel Peter Date: Fri, 12 Jun 2026 15:55:09 +0200 Subject: [PATCH 44/44] rm fuzzer, will integrate it with JDK-8386597 --- .../loopopts/TestTruncationWrapFuzzer.java | 531 ------------------ 1 file changed, 531 deletions(-) delete mode 100644 test/hotspot/jtreg/compiler/loopopts/TestTruncationWrapFuzzer.java diff --git a/test/hotspot/jtreg/compiler/loopopts/TestTruncationWrapFuzzer.java b/test/hotspot/jtreg/compiler/loopopts/TestTruncationWrapFuzzer.java deleted file mode 100644 index 8945b430d7f65..0000000000000 --- a/test/hotspot/jtreg/compiler/loopopts/TestTruncationWrapFuzzer.java +++ /dev/null @@ -1,531 +0,0 @@ -/* - * Copyright (c) 2026, 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 8385855 - * @summary Fuzz patterns for CountedLoopConverter::has_truncation_wrap - * @modules java.base/jdk.internal.misc - * @library /test/lib / - * @compile ../lib/ir_framework/TestFramework.java - * @compile ../lib/generators/Generators.java - * @run driver ${test.main.class} - */ - -package compiler.loopopts; - -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 TestHasTruncationWrap.java - * - * So far, this test does not have IR verification, only result verification. - * - * This test generates a wide range of patterns, and will require a lot of - * runs to find a specific code shape. - * - * Features: - * - Truncation patterns, see TRUNCATIONS and randomIVMutation. - * - Stride: positive, negative, small and large, see ivMutationWithRandomStride. - * - Reference (not compiled) vs test (compiled), and result verification. - * - Loop Shapes: for, while, do-while, see LOOP_SHAPES. - * - Exit checks: random Comparison, see Comparator and Comparison (signed and unsigned). - * - For endless loops / loops that would take too long: early exit via opaqueCheck, - * Note: it is verified that this does not hinder optimization, see: - * TestHasTruncationWrap.java -> testIRShort7. - * - Interesting loop bounds: init/limit - * - constant - * - variable, sampled (see getInputTemplate), and modified (no-op, truncate, clamp). - * - Extra check dominating the loop: compare against constant of limit. - * Note: has_truncation_wrap can use such checks to constrain the entry type. - * Note2: We've had bugs around this, confusing CmpI/CmpU, see JDK-8385855. - */ -public class TestTruncationWrapFuzzer { - private static final Random RANDOM = Utils.getRandomInstance(); - private static final RestrictableGenerator INT_GEN = Generators.G.ints(); - - 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.loopopts.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.loopopts.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<>(); - - // Some utilities, to help us get an additional exit, in case the - // generated loops spin too long, or are infinite loops. - Template.ZeroArgs utilsTemplate = Template.make(() -> scope( - """ - private static final Random RANDOM = Utils.getRandomInstance(); - - public static int opaqueCounter; - public static int opaqueCounterMax; - - @DontInline - public static void opaqueReset() { - opaqueCounter = 0; - } - - @DontInline - public static boolean opaqueCheck() { - return (opaqueCounter++) > opaqueCounterMax; - } - - @DontInline - public static int opaqueSum(int i, int j) { - return i + j + 1; - } - """ - )); - testTemplateTokens.add(utilsTemplate.asToken()); - - for (int i = 0; i < 100; i++) { - testTemplateTokens.add(generateTest(/* no warmup, like -Xcomp */ 0)); - } - for (int i = 0; i < 5; i++) { - testTemplateTokens.add(generateTest(/* with warmup, slower */ 100)); - } - - // Create the test class, which runs all testTemplateTokens. - return TestFrameworkClass.render( - // package and class name. - "compiler.loopopts.templated", "Generated", - // List of imports. - Set.of("compiler.lib.generators.*", - "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); - } - - // This is copied from TestFoldComparesFuzzer.java, and we should - // refactor this out into the template framework library, in a - // future RFE. - enum Comparator { - ULT(" < 0", false), - ULE(" <= 0", false), - UGT(" > 0", false), - UGE(" >= 0", false), - UEQ(" == 0", false), - UNE(" != 0", false), - LT(" < ", true), - LE(" <= ", true), - GT(" > ", true), - GE(" >= ", true), - EQ(" == ", true), - NE(" != ", true); - - private final String token; - private final boolean signed; - - Comparator(String token, boolean signed) { - this.token = token; - this.signed = signed; - } - - public String getToken() { - return token; - } - - public boolean isSigned() { - return signed; - } - - public Comparator negate() { - return switch(this) { - case ULT -> UGE; - case ULE -> UGT; - case UGT -> ULE; - case UGE -> ULT; - case UEQ -> UNE; - case UNE -> UEQ; - case LT -> GE; - case LE -> GT; - case GT -> LE; - case GE -> LT; - case EQ -> NE; - case NE -> EQ; - }; - } - - public Comparator flip() { - return switch(this) { - case ULT -> UGT; - case ULE -> UGE; - case UGT -> ULT; - case UGE -> ULE; - case UEQ -> UEQ; - case UNE -> UNE; - case LT -> GT; - case LE -> GE; - case GT -> LT; - case GE -> LE; - case EQ -> EQ; - case NE -> NE; - }; - } - - static Comparator random() { - return values()[RANDOM.nextInt(values().length)]; - } - } - - record Comparison(String lhs, Comparator cmp, String rhs, boolean negated) { - public Comparison(String lhs, Comparator cmp, String rhs) { - this(lhs, cmp, rhs, false); - } - - public String toString() { - return cmp.isSigned() - ? ((negated ? "!" : "") + "(" + lhs + " "+ cmp.getToken() + " " + rhs + ")") - : ((negated ? "!" : "") + "(Integer.compareUnsigned(" + lhs + ", " + rhs + ")" + cmp.getToken() + ")"); - } - - // Keep the same semantics of the test, but change its form. - Comparison permuteRandom() { - return flipRandom().complementRandom(); - } - - Comparison flipRandom() { - return RANDOM.nextBoolean() ? this : new Comparison(rhs, cmp.flip(), lhs); - } - - Comparison complementRandom() { - return RANDOM.nextBoolean() ? this : new Comparison(lhs, cmp.negate(), rhs, true); - } - } - - interface TestMethodGenerator { - Template.OneArg getTestTemplate(); - - default Template.ZeroArgs getInputTemplate() { - return Template.make(() -> scope( - switch (RANDOM.nextInt(5)) { - case 0 -> """ - RestrictableGenerator gen = Generators.G.ints(); - int init = gen.next(); - int limit = gen.next(); - """; - case 1 -> """ - int init = (byte)RANDOM.nextInt(); - int limit = (byte)RANDOM.nextInt(); - """; - case 2 -> """ - int init = (short)RANDOM.nextInt(); - int limit = (short)RANDOM.nextInt(); - """; - case 3 -> """ - int init = (char)RANDOM.nextInt(); - int limit = (char)RANDOM.nextInt(); - """; - case 4 -> """ - int e0 = RANDOM.nextInt(32); - int e1 = RANDOM.nextInt(32); - int r0 = RANDOM.nextInt(32); - int r1 = RANDOM.nextInt(32); - int init = (1 << e0) + r0; - int limit = (1 << e1) + r1; - """; - default -> throw new RuntimeException("not expected"); - } - )); - }; - } - - private static record Truncation(String s0, String s1) { - public String ivMutationWithRandomStride() { - int stride = switch(RANDOM.nextInt(3)) { - case 0 -> INT_GEN.next(); - case 1 -> RANDOM.nextInt(9) - 4; - case 2 -> RANDOM.nextInt(129) - 64; - default -> throw new RuntimeException("not expected"); - }; - - return "i = " + s0 + "i + " + stride + s1; - } - - public String truncate(String val) { - return val + " = " + s0 + val + s1; - } - } - - // Different patterns relevant for triggering truncation/wrap. - private static final Truncation[] TRUNCATIONS = new Truncation[] { - new Truncation("", ""), - new Truncation("(byte)(", ")"), - new Truncation("(short)(", ")"), - new Truncation("(char)(", ")"), - new Truncation("((", ") << 8) >> 8"), - new Truncation("((", ") << 16) >> 16"), - new Truncation("((", ") << 24) >> 24"), - new Truncation("((", ") & 0x7f)"), - new Truncation("((", ") & 0xff)"), - new Truncation("((", ") & 0x7fff)"), - new Truncation("((", ") & 0xffff)") - }; - - private static Truncation randomTruncation() { - return TRUNCATIONS[RANDOM.nextInt(TRUNCATIONS.length)]; - } - - private static String randomIVMutation() { - return randomTruncation().ivMutationWithRandomStride(); - } - - private static String randomTruncation(String val) { - return randomTruncation().truncate(val); - } - - private static final String[] LOOP_SHAPES = new String[] { - """ - // Loop Shape: For - int i; - for (i = init; #exitCheck; #ivMutation) { - sum = opaqueSum(sum, #addValue); - if (opaqueCheck()) { break; } - } - """, - """ - // Loop Shape: While: - int i = init; - while (#exitCheck) { - sum = opaqueSum(sum, #addValue); - if (opaqueCheck()) { break; } - #ivMutation; - } - """, - """ - // Loop Shape: Do-While: - int i = init; - do { - sum = opaqueSum(sum, #addValue); - if (opaqueCheck()) { break; } - #ivMutation; - } while (#exitCheck); - """, - """ - // Loop Shape: Do-While + pre-loop check. - int i = init; - if (!(#exitCheck)) { return sum; } - do { - sum = opaqueSum(sum, #addValue); - if (opaqueCheck()) { break; } - #ivMutation; - } while (#exitCheck); - """ - }; - - private static String randomLoopShape() { - return LOOP_SHAPES[RANDOM.nextInt(LOOP_SHAPES.length)]; - } - - // Loop init/limit are constants. - static class TestMethodGeneratorConst implements TestMethodGenerator { - private final int init = INT_GEN.next(); - private final int limit = INT_GEN.next(); - - private final String ivMutation = randomIVMutation(); - private final String loopShape = randomLoopShape(); - private final String addValue = RANDOM.nextBoolean() ? "0" : "i"; - - private final Comparison exitCheck = new Comparison("i", Comparator.random(), "limit").permuteRandom(); - - private final Template.OneArg testTemplate = Template.make("methodName", (String methodName) -> scope( - let("init", init), - let("limit", limit), - let("ivMutation", ivMutation), - let("exitCheck", exitCheck), - let("addValue", addValue), - """ - static int #methodName(int unused0, int unused1) { - opaqueReset(); - int init = #init; - int limit = #limit; - int sum = 0; - """, - loopShape, - """ - return sum + #addValue; - } - """ - )); - - public Template.OneArg getTestTemplate() { return testTemplate; } - } - - // Clamp randomly, but not always on both sides. - private static String randomClamping(String value) { - String clamp = value; - if (RANDOM.nextBoolean()) { - clamp = "Math.max(" + clamp + ", " + INT_GEN.next() + ")"; - } - if (RANDOM.nextBoolean()) { - clamp = "Math.min(" + clamp + ", " + INT_GEN.next() + ")"; - } - return value + " = " + clamp; - } - - // We want to be able to modify the incoming init/limit. - // - nothing - // - truncate - // - clamp with min/max, maybe even only one-sided - private static String randomModifyValue(String value) { - return switch(RANDOM.nextInt(3)) { - case 0 -> "// Don't modify " + value + "\n"; - case 1 -> randomTruncation(value) + ";\n"; - case 2 -> randomClamping(value) + ";\n"; - default -> throw new RuntimeException("not expected"); - }; - } - - private static String randomExtraCheck() { - // We can constrain the init value with limit or a constant. - String other = RANDOM.nextBoolean() ? "limit" : INT_GEN.next().toString(); - Comparison check = new Comparison("init", Comparator.random(), other).permuteRandom(); - return RANDOM.nextBoolean() - ? "// No extra check.\n" - : "if (" + check + ") { return -1; }\n"; - } - - // Loop init/limit are variables. - static class TestMethodGeneratorVars implements TestMethodGenerator { - private final String ivMutation = randomIVMutation(); - private final String loopShape = randomLoopShape(); - private final String addValue = RANDOM.nextBoolean() ? "0" : "i"; - private final String modifyInit = randomModifyValue("init"); - private final String modifyLimit = randomModifyValue("limit"); - private final String extraCheck = randomExtraCheck(); - - private final Comparison exitCheck = new Comparison("i", Comparator.random(), "limit").permuteRandom(); - - private final Template.OneArg testTemplate = Template.make("methodName", (String methodName) -> scope( - let("ivMutation", ivMutation), - let("exitCheck", exitCheck), - let("addValue", addValue), - """ - static int #methodName(int init, int limit) { - opaqueReset(); - int sum = 0; - """, - modifyInit, // modify type of init - modifyLimit, // modify type of limit - extraCheck, // extra CmpI/CmpU dominating the loop, might constrain entry value. - loopShape, - """ - return sum + #addValue; - } - """ - )); - - public Template.OneArg getTestTemplate() { return testTemplate; } - } - public static TemplateToken generateTest(int warmup) { - TestMethodGenerator tg = switch(RANDOM.nextInt(2)) { - case 0 -> new TestMethodGeneratorConst(); - case 1 -> new TestMethodGeneratorVars(); - default -> throw new RuntimeException("not expected"); - }; - Template.ZeroArgs testInputTemplate = tg.getInputTemplate(); - Template.OneArg testMethodTemplate = tg.getTestTemplate(); - - var testTemplate = Template.make(() -> scope( - let("warmup", warmup), - """ - // --- $test start --- - @Run(test = "$test") - @Warmup(#warmup) - public static void $run(RunInfo info) { - int reps = info.isWarmUp() ? 1 : 100; - for (int i = 0; i < reps; i++) { - // Generate random values for init and limit. - """, - testInputTemplate.asToken(), - """ - - // Limit how long we can spin in the loop: - opaqueCounterMax = 10_000 + RANDOM.nextInt(1000); - - // Run test and compare with interpreter results. - var result = $test(init, limit); - var expected = $reference(init, limit); - if (result != expected) { - throw new RuntimeException("wrong result: " + result + " vs " + expected - + "\\ninit: " + init - + "\\nlimit: " + limit - + "\\nopaqueCounterMax: " + opaqueCounterMax); - } - } - } - - @Test - """, - testMethodTemplate.asToken($("test")), - """ - - @DontCompile - """, - testMethodTemplate.asToken($("reference")), - """ - // --- $test end --- - """ - )); - return testTemplate.asToken(); - } -}