/*
 * Copyright (c) 2014, 2025, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

/*
 * @test
 * @bug 8030976 8059226
 * @requires !vm.graal.enabled & (vm.opt.StressUnstableIfTraps == null | !vm.opt.StressUnstableIfTraps)
 * @library /test/lib /
 * @library /testlibrary/asm
 * @modules java.base/jdk.internal.misc
 *          java.compiler
 *          java.management
 *          jdk.internal.jvmstat/sun.jvmstat.monitor
 *
 * @build jdk.test.whitebox.WhiteBox
 * @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox
 * @run main/othervm -Xbatch -Xbootclasspath/a:. -XX:+UnlockDiagnosticVMOptions
 *                   -XX:+UnlockExperimentalVMOptions -XX:PerMethodTrapLimit=100
 *                   -XX:+WhiteBoxAPI -XX:+LogCompilation
 *                   -XX:CompileCommand=compileonly,UnstableIfExecutable.test
 *                   -XX:LogFile=always_taken_not_fired.xml
 *                   compiler.uncommontrap.TestUnstableIfTrap ALWAYS_TAKEN false
 * @run main/othervm -Xbatch -Xbootclasspath/a:. -XX:+UnlockDiagnosticVMOptions
 *                   -XX:+UnlockExperimentalVMOptions -XX:PerMethodTrapLimit=100
 *                   -XX:+WhiteBoxAPI -XX:+LogCompilation
 *                   -XX:CompileCommand=compileonly,UnstableIfExecutable.test
 *                   -XX:LogFile=always_taken_fired.xml
 *                   compiler.uncommontrap.TestUnstableIfTrap ALWAYS_TAKEN true
 * @run main/othervm -Xbatch -Xbootclasspath/a:. -XX:+UnlockDiagnosticVMOptions
 *                   -XX:+UnlockExperimentalVMOptions -XX:PerMethodTrapLimit=100
 *                   -XX:+WhiteBoxAPI -XX:+LogCompilation
 *                   -XX:CompileCommand=compileonly,UnstableIfExecutable.test
 *                   -XX:LogFile=never_taken_not_fired.xml
 *                   compiler.uncommontrap.TestUnstableIfTrap NEVER_TAKEN false
 * @run main/othervm -Xbatch -Xbootclasspath/a:. -XX:+UnlockDiagnosticVMOptions
 *                   -XX:+UnlockExperimentalVMOptions -XX:PerMethodTrapLimit=100
 *                   -XX:+WhiteBoxAPI -XX:+LogCompilation
 *                   -XX:CompileCommand=compileonly,UnstableIfExecutable.test
 *                   -XX:LogFile=never_taken_fired.xml
 *                   compiler.uncommontrap.TestUnstableIfTrap NEVER_TAKEN true
 * @run driver compiler.testlibrary.uncommontrap.Verifier always_taken_not_fired.xml
 *                                                        always_taken_fired.xml
 *                                                        never_taken_not_fired.xml
 *                                                        never_taken_fired.xml
 */

package compiler.uncommontrap;

import compiler.testlibrary.uncommontrap.Verifier;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import jdk.test.lib.ByteCodeLoader;
import jdk.test.lib.Platform;
import jdk.test.whitebox.WhiteBox;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.Properties;

import static org.objectweb.asm.Opcodes.ACC_ABSTRACT;
import static org.objectweb.asm.Opcodes.ACC_PUBLIC;
import static org.objectweb.asm.Opcodes.ACC_STATIC;
import static org.objectweb.asm.Opcodes.ACC_VOLATILE;
import static org.objectweb.asm.Opcodes.GETSTATIC;
import static org.objectweb.asm.Opcodes.GOTO;
import static org.objectweb.asm.Opcodes.IADD;
import static org.objectweb.asm.Opcodes.ICONST_1;
import static org.objectweb.asm.Opcodes.IFEQ;
import static org.objectweb.asm.Opcodes.ILOAD;
import static org.objectweb.asm.Opcodes.ISUB;
import static org.objectweb.asm.Opcodes.RETURN;

public class TestUnstableIfTrap {
    private static final WhiteBox WB = WhiteBox.getWhiteBox();
    private static final String CLASS_NAME = "UnstableIfExecutable";
    private static final String METHOD_NAME = "test";
    private static final String FIELD_NAME = "field";
    private static final int ITERATIONS = 1_000_000;
    // There is no dependency on particular class file version, so it could be
    // set to any version (if you're updating this test for Java 42).
    private static final int CLASS_FILE_VERSION = 49;
    private static final int MAX_TIER = 4;
    // This test aimed to verify that uncommon trap with reason "unstable_if"
    // is emitted when method that contain control-flow divergence such that
    // one of two branches is never taken (and other one is taken always).
    // C2 will made a decision whether or not the branch was ever taken
    // depending on method's profile.
    // If profile was collected for a few method's invocations, then C2 will not
    // trust in branches' probabilities and the tested trap won't be emitted.
    // In fact, a method has to be invoked at least 40 time at the day when this
    // comment was written (see Parse::dynamic_branch_prediction for an actual
    // value). It would be to implementation dependent to use "40" as
    // a threshold value in the test, so in order to improve test's robustness
    // the threshold value is 1000: if the tested method was compiled by C2
    // before it was invoked 1000 times, then we won't verify that trap was
    // emitted and fired.
    private static final int MIN_INVOCATIONS_BEFORE_C2_COMPILATION = 1000;
    /**
     * Description of test case parameters and uncommon trap that will
     * be emitted during tested method compilation.
     */
    private static enum TestCaseName {
        ALWAYS_TAKEN(false, "taken always"),
        NEVER_TAKEN(true, "taken never");
        TestCaseName(boolean predicate, String comment) {
            this.predicate = predicate;
            this.comment = comment;
        }

        public final boolean predicate;
        public final String name = "unstable_if";
        public final String comment;
    }

    public static void main(String args[]) {
        if (args.length != 2) {
            throw new Error("Expected two arguments: test case name and a "
                    + "boolean determining if uncommon trap should be fired.");
        }
        test(TestCaseName.valueOf(args[0]), Boolean.valueOf(args[1]));
    }

    private static void test(TestCaseName testCase, boolean shouldBeFired) {
        Method testMethod;
        Label unstableIfLocation = new Label();
        boolean shouldBeEmitted;
        boolean compiledToEarly = false;

        try {
            Class testClass = ByteCodeLoader.load(CLASS_NAME,
                    generateTest(unstableIfLocation));
            testMethod = testClass.getDeclaredMethod(METHOD_NAME,
                    boolean.class);
            for (int i = 0; i < ITERATIONS; i++) {
                testMethod.invoke(null, testCase.predicate);
                if (i < MIN_INVOCATIONS_BEFORE_C2_COMPILATION
                        && isMethodCompiledByC2(testMethod)) {
                    compiledToEarly = true;
                    // There is no sense in further invocations: we already
                    // decided to avoid verification.
                    break;
                }
            }
            // We're checking that trap should be emitted (i.e. it was compiled
            // by C2) before the trap is fired, because otherwise the nmethod
            // will be deoptimized and isMethodCompiledByC2 will return false.
            shouldBeEmitted = isMethodCompiledByC2(testMethod)
                    && !compiledToEarly;
            if (shouldBeFired) {
                testMethod.invoke(null, !testCase.predicate);
            }
        } catch (ReflectiveOperationException e) {
            throw new Error("Test case should be generated, loaded and executed"
                    + " without any issues.", e);
        }

        shouldBeFired &= shouldBeEmitted;

        Properties properties = new Properties();
        properties.setProperty(Verifier.VERIFICATION_SHOULD_BE_SKIPPED,
                Boolean.toString(compiledToEarly));
        properties.setProperty(Verifier.UNCOMMON_TRAP_SHOULD_EMITTED,
                Boolean.toString(shouldBeEmitted));
        properties.setProperty(Verifier.UNCOMMON_TRAP_SHOULD_FIRED,
                Boolean.toString(shouldBeFired));
        properties.setProperty(Verifier.UNCOMMON_TRAP_NAME, testCase.name);
        properties.setProperty(Verifier.UNCOMMON_TRAP_COMMENT,
                testCase.comment);
        properties.setProperty(Verifier.UNCOMMON_TRAP_BCI,
                Integer.toString(unstableIfLocation.getOffset()));

        properties.list(System.out);

        File f = new File(WB.getStringVMFlag("LogFile") +
                Verifier.PROPERTIES_FILE_SUFFIX);
        try (FileWriter wr = new FileWriter(f)) {
            properties.store(wr, "");
        } catch (IOException e) {
            throw new Error("Unable to store test properties.", e);
        }
    }

    private static boolean isMethodCompiledByC2(Method m) {
        boolean isTiered = WB.getBooleanVMFlag("TieredCompilation");
        boolean isMethodCompiled = WB.isMethodCompiled(m);
        boolean isMethodCompiledAtMaxTier
                = WB.getMethodCompilationLevel(m) == MAX_TIER;

        return Platform.isServer() && !Platform.isEmulatedClient() && isMethodCompiled
                && (!isTiered || isMethodCompiledAtMaxTier);
    }

    /**
     * Generates class with name {@code CLASS_NAME}, which will contain a
     * static method {@code METHOD_NAME}:
     *
     * <pre>{@code
     * public abstract class UnstableIfExecutable {
     *   private static int field = 0;
     *
     *   public static void test(boolean alwaysTrue) {
     *     if (alwaysTrue) {
     *       field++;
     *     } else {
     *       field--;
     *     }
     *   }
     * }
     * }</pre>
     *
     * @return generated bytecode.
     */
    private static byte[] generateTest(Label unstableIfLocation) {
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

        cw.visit(CLASS_FILE_VERSION, ACC_PUBLIC | ACC_ABSTRACT, CLASS_NAME,
                null, "java/lang/Object", null);

        cw.visitField(ACC_PUBLIC | ACC_STATIC | ACC_VOLATILE, FIELD_NAME,
                "I", null, Integer.valueOf(0));

        generateTestMethod(cw, unstableIfLocation);

        return cw.toByteArray();
    }

    private static void generateTestMethod(ClassVisitor cv,
            Label unstableIfLocation) {
        MethodVisitor mv = cv.visitMethod(ACC_PUBLIC | ACC_STATIC, METHOD_NAME,
                "(Z)V", null, null);
        mv.visitCode();

        Label end = new Label();
        Label falseBranch = new Label();

        // push "field" field's value and 1 to stack
        mv.visitFieldInsn(GETSTATIC, CLASS_NAME, FIELD_NAME, "I");
        mv.visitInsn(ICONST_1);
        // load argument's value
        mv.visitVarInsn(ILOAD, 0); // alwaysTrue
        // here is our unstable if
        mv.visitLabel(unstableIfLocation);
        mv.visitJumpInsn(IFEQ, falseBranch);
        // increment on "true"
        mv.visitInsn(IADD);
        mv.visitJumpInsn(GOTO, end);
        // decrement on "false"
        mv.visitLabel(falseBranch);
        mv.visitInsn(ISUB);
        mv.visitLabel(end);
        // bye bye
        mv.visitInsn(RETURN);

        mv.visitMaxs(0, 0);
        mv.visitEnd();
    }
}

