diff --git a/Makefile b/Makefile index 0b23f35479..1bbd43e128 100644 --- a/Makefile +++ b/Makefile @@ -759,7 +759,7 @@ endif update-bibtex2web: ifndef NONETWORK - if test -d .utils/bibtex2web/.git ; then \ + @if test -d .utils/bibtex2web/.git ; then \ (cd .utils/bibtex2web && (git pull -q || (sleep 1m && (git pull || true)))) \ elif ! test -d .utils/bibtex2web ; then \ (mkdir -p .utils && (git clone -q --depth=1 https://github.com/mernst/bibtex2web.git .utils/bibtex2web || (sleep 1m && git clone -q --depth=1 https://github.com/mernst/bibtex2web.git .utils/bibtex2web))) \ @@ -768,7 +768,7 @@ endif update-checklink: ifndef NONETWORK - if test -d .utils/checklink/.git ; then \ + @if test -d .utils/checklink/.git ; then \ (cd .utils/checklink && (git pull -q || (sleep 1m && (git pull || true)))) \ elif ! test -d .utils/checklink ; then \ (mkdir -p .utils && (git clone -q --depth=1 https://github.com/plume-lib/checklink.git .utils/checklink || (sleep 1m && git clone -q --depth=1 https://github.com/plume-lib/checklink.git .utils/checklink))) \ @@ -777,7 +777,7 @@ endif update-git-scripts: ifndef NONETWORK - if test -d .utils/git-scripts/.git ; then \ + @if test -d .utils/git-scripts/.git ; then \ (cd .utils/git-scripts && (git pull -q || (sleep 1m && (git pull || true)))) \ elif ! test -d .utils/git-scripts ; then \ (mkdir -p .utils && (git clone -q --depth=1 https://github.com/plume-lib/git-scripts.git .utils/git-scripts || (sleep 1m && git clone -q --depth=1 https://github.com/plume-lib/git-scripts.git .utils/git-scripts))) \ @@ -786,7 +786,7 @@ endif update-html-tools: ifndef NONETWORK - if test -d ${HTMLTOOLS}/.git ; then \ + @if test -d ${HTMLTOOLS}/.git ; then \ (cd ${HTMLTOOLS} && (git pull -q || (sleep 1m && (git pull || true)))) \ elif ! test -d ${HTMLTOOLS} ; then \ (mkdir -p .utils && (git clone -q --depth=1 https://github.com/plume-lib/html-tools.git ${HTMLTOOLS} || (sleep 1m && git clone -q --depth=1 https://github.com/plume-lib/html-tools.git ${HTMLTOOLS}))) \ @@ -795,7 +795,7 @@ endif update-plume-scripts-in-utils: ifndef NONETWORK - if test -d ${PLUMESCRIPTS}/.git ; then \ + @if test -d ${PLUMESCRIPTS}/.git ; then \ (cd ${PLUMESCRIPTS} && (git pull -q || (sleep 1m && (git pull || true)))) \ elif ! test -d ${PLUMESCRIPTS} ; then \ mkdir -p .utils && (git clone -q --depth=1 https://github.com/plume-lib/plume-scripts.git ${PLUMESCRIPTS} || (sleep 1m && git clone -q --depth=1 https://github.com/plume-lib/plume-scripts.git ${PLUMESCRIPTS})) \ @@ -804,7 +804,7 @@ endif update-run-google-java-format: ifndef NONETWORK - if test -d .utils/run-google-java-format/.git ; then \ + @if test -d .utils/run-google-java-format/.git ; then \ (cd .utils/run-google-java-format && (git pull -q || (sleep 1m && (git pull || true)))) \ elif ! test -d .utils/run-google-java-format ; then \ (mkdir -p .utils && (git clone -q --depth=1 https://github.com/plume-lib/run-google-java-format.git .utils/run-google-java-format || (sleep 1m && git clone -q --depth=1 https://github.com/plume-lib/run-google-java-format.git .utils/run-google-java-format))) \ diff --git a/java/Makefile b/java/Makefile index 3172335701..8a3483245f 100644 --- a/java/Makefile +++ b/java/Makefile @@ -23,6 +23,8 @@ JAVAC ?= javac JAVADOC ?= javadoc JAR ?= jar +BuildJDKTool = BuildJDK + # The "JAVANN" variables mean "at least version NN", except JAVA8. JAVA_VERSION_STRING_WITH_EA := $(shell javac -version 2>&1 | head -1 | cut "-d " -f2) JAVA_VERSION_STRING := $(shell javac -version 2>&1 | head -1 | cut "-d " -f2 | sed 's/-ea//') @@ -50,6 +52,7 @@ endif # Temporary, since Java 24 is not a LTS release. ifeq ($(shell test ${JAVA_RELEASE_NUMBER} -ge 24; echo $$?),0) JAVA24 := 1 + BuildJDKTool = BuildJDK24 endif ifeq ($(shell test ${JAVA_RELEASE_NUMBER} -ge 25; echo $$?),0) JAVA25 := 1 @@ -537,13 +540,13 @@ endif # compiled, but changing JDKs doesn't cause this Makefile to otherwise # trigger re-execution. .PRECIOUS: ChicoryPremain.jar -ChicoryPremain.jar : daikon/Chicory.class daikon/chicory/manifest.txt +ChicoryPremain.jar : daikon/Daikon.class daikon/chicory/manifest.txt ${JAR} cfm ChicoryPremain.jar daikon/chicory/manifest.txt \ daikon/chicory/ChicoryPremain.class -.PHONY: chicory chicory-test -chicory chicory-test : daikon/chicory/inv_out.diff -daikon/chicory/inv_out.diff : ChicoryPremain.jar daikon/Daikon.class +.PHONY: chicory-test +chicory-test: daikon/chicory/inv_out.diff +daikon/chicory/inv_out.diff: ChicoryPremain.jar cd daikon/chicory && rm -f ChicoryTest.log ChicoryTest.dtrace.gz ChicoryTest.inv.gz ChicoryTest.inv.out cd daikon/chicory && ${JAVA_COMMAND} daikon.Chicory --verbose --debug daikon.chicory.ChicoryTest > ChicoryTest.log || (cat ChicoryTest.log; false) cd daikon/chicory && ${JAVA_COMMAND} daikon.Daikon --no_text_output --no_show_progress ChicoryTest.dtrace.gz @@ -610,7 +613,7 @@ dyncomp-jdk dcomp-jdk : dcomp_rt.jar ${DCOMP_RT} dcomp_rt.jar : dcomp_premain.jar /bin/rm -rf ${DCOMP_RT} ${INSTALL} -d ${DCOMP_RT} - ${JAVA_COMMAND} -Xmx7g daikon.dcomp.BuildJDK ${DCOMP_RT} + ${JAVA_COMMAND} -Xmx7g daikon.dcomp.${BuildJDKTool} ${DCOMP_RT} # "then" clause is Java 8, "else" clause is Java 9+. # For Java 9+, there appears to be a bug in the Java reflection invoke code that it # does not check that the arg list matches. Hence, it tries to invoke @@ -2064,6 +2067,21 @@ daikon/derive/unary/SequenceInitialFactoryFloat.java: daikon/derive/unary/Sequen $(JAVA_CPP:DEFINEDVAR=TYPEDOUBLE) +########################################################################### +### Diffs +### + +# Don't diff StackMapUtils24.java, ClassGen24.java, MethodGen24.java, or +# OperandStack24.java, because there are too many differences between the +# upstream version (in BCEL or in bcel-util) and the version in Daikon. +.PHONY: diff24 +diff24: + -diff -u daikon/dcomp/Instrument.java daikon/dcomp/Instrument24.java + -diff -u daikon/dcomp/DCInstrument.java daikon/dcomp/DCInstrument24.java + -diff -u daikon/dcomp/BuildJDK.java daikon/dcomp/BuildJDK24.java + -diff -u daikon/chicory/Instrument.java daikon/chicory/Instrument24.java + + ########################################################################### ### Delete-on-error ### diff --git a/java/daikon/DynComp.java b/java/daikon/DynComp.java index 9f7d791e6d..c1c6ab08e5 100644 --- a/java/daikon/DynComp.java +++ b/java/daikon/DynComp.java @@ -304,6 +304,10 @@ void start_target(String premain_args, String[] targetArgs) { // allow DCRuntime to make reflective access to java.land.Object.clone() without a warning cmdlist.add("--add-opens"); cmdlist.add("java.base/java.lang=ALL-UNNAMED"); + if (Runtime.isJava24orLater()) { + // needed to eliminate warning for JNI access to native code + cmdlist.add("--enable-native-access=ALL-UNNAMED"); + } if (!no_jdk) { // If we are processing JDK classes, then we need our code on the boot classpath as well. // Otherwise, references to DCRuntime from the JDK would fail. diff --git a/java/daikon/chicory/ClassInfo.java b/java/daikon/chicory/ClassInfo.java index f771669a84..7bd70e04a8 100644 --- a/java/daikon/chicory/ClassInfo.java +++ b/java/daikon/chicory/ClassInfo.java @@ -25,12 +25,14 @@ public class ClassInfo { /** True if the class has a class initializer. */ public boolean hasClinit; + /** True if the class is a JUnit test class. */ + public boolean isJunitTestClass; + // set by initViaReflection() /** reflection object for this class. */ public @MonotonicNonNull Class clazz; - // Does not include class initializers, so each element's .member field - // is non-null. + // Does not include class initializers, so each element's .member field is non-null. /** list of methods in the class. */ public List method_infos = new ArrayList<>(); @@ -47,9 +49,8 @@ public class ClassInfo { /** True if any methods in this class were instrumented. */ public boolean shouldInclude = false; - /** Mapping from field name to string representation of its value* */ - // only for static final primitives - // which are declared by a CONSTANT VALUE in the code + /** Mapping from field name to string representation of its value. */ + // Only for static final primitives which are declared by a CONSTANT VALUE in the code. public Map staticMap = new HashMap<>(); /** Create ClassInfo with specified name. */ diff --git a/java/daikon/chicory/Instrument.java b/java/daikon/chicory/Instrument.java index 152aff8e93..47a2672e0a 100644 --- a/java/daikon/chicory/Instrument.java +++ b/java/daikon/chicory/Instrument.java @@ -1,10 +1,13 @@ package daikon.chicory; +import static org.apache.bcel.Const.ACC_SYNTHETIC; + import daikon.Chicory; import daikon.plumelib.bcelutil.BcelUtil; import daikon.plumelib.bcelutil.InstructionListUtils; import daikon.plumelib.bcelutil.SimpleLog; import daikon.plumelib.reflection.Signatures; +import daikon.plumelib.util.ArraysPlume; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; @@ -19,6 +22,7 @@ import org.apache.bcel.Const; import org.apache.bcel.classfile.Attribute; import org.apache.bcel.classfile.ClassParser; +import org.apache.bcel.classfile.Code; import org.apache.bcel.classfile.Constant; import org.apache.bcel.classfile.ConstantUtf8; import org.apache.bcel.classfile.ConstantValue; @@ -43,6 +47,7 @@ import org.apache.bcel.generic.ObjectType; import org.apache.bcel.generic.PUSH; import org.apache.bcel.generic.Type; +import org.checkerframework.checker.interning.qual.InternedDistinct; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.signature.qual.BinaryName; @@ -52,29 +57,29 @@ import org.checkerframework.dataflow.qual.Pure; /** - * This class is responsible for modifying another class's bytecodes. Specifically, its main task is - * to add calls into the Chicory runtime at method entries and exits for instrumentation purposes. - * These added calls are sometimes referred to as "hooks". + * This class modifies another class's bytecodes. It adds calls into the Chicory runtime at method + * entries and exits for instrumentation purposes. These added calls are sometimes referred to as + * "hooks". * *

This class is loaded by ChicoryPremain at startup. It is a ClassFileTransformer which means - * that its {@code transform} method gets called each time the JVM loads a class. + * that its {@link #transform} method gets called each time the JVM loads a class. */ public class Instrument extends InstructionListUtils implements ClassFileTransformer { - /** The location of the runtime support class. */ + /** The name of the Chicory runtime support class. */ private static final String runtime_classname = "daikon.chicory.Runtime"; - /** Debug information about which classes and/or methods are transformed and why. */ + /** A log for debug information about which classes and/or methods are transformed and why. */ protected static final SimpleLog debug_transform = new SimpleLog(false); - // Public so can be enabled from daikon.dcomp.Instrument. - /** Debug information about ppt-omit and ppt-select. */ + // Public so daikon.dcomp.Instrument can enable it. + /** A log for debug information about ppt-omit and ppt-select. */ public static final SimpleLog debug_ppt_omit = new SimpleLog(false); /** Directory for debug output. */ final File debug_dir; - /** Directory into which to dump debug-instrumented classes. */ + /** Directory into which to dump instrumented classes. */ final File debug_instrumented_dir; /** Directory into which to dump original classes. */ @@ -86,12 +91,52 @@ public class Instrument extends InstructionListUtils implements ClassFileTransfo /** InstructionFactory for a class. */ public InstructionFactory instFactory; + // Type descriptors + + /** "java.lang.Object". */ + private static final ObjectType CD_Object = Type.OBJECT; + + // /** Type for "java.lang.Class". */ + // private static final ObjectType CD_Class = Type.CLASS; + + /** Type for "java.lang.String". */ + private static final ObjectType CD_String = Type.STRING; + + // /** Type for "java.lang.Throwable". */ + // protected static ObjectType CD_Throwable = new ObjectType("java.lang.Throwable"); + // private static final ObjectType CD_Throwable = Type.THROWABLE; + + // /** Type for "boolean". */ + // private static final @InternedDistinct BasicType CD_boolean = Type.BOOLEAN; + // /** Type for "byte". */ + // private static final @InternedDistinct BasicType CD_byte = Type.BYTE; + // /** Type for "char". */ + // private static final @InternedDistinct BasicType CD_char = Type.CHAR; + // /** Type for "double". */ + // private static final @InternedDistinct BasicType CD_double = Type.DOUBLE; + // /** Type for "float". */ + // private static final @InternedDistinct BasicType CD_float = Type.FLOAT; + /** Type for "int". */ + private static final @InternedDistinct BasicType CD_int = Type.INT; + + // /** Type for "long". */ + // private static final @InternedDistinct BasicType CD_long = Type.LONG; + // /** Type for "short". */ + // private static final @InternedDistinct BasicType CD_short = Type.SHORT; + /** Type for "void". */ + private static final @InternedDistinct BasicType CD_void = Type.VOID; + + /** "java.lang.Object[]". */ + protected static Type CD_Object_array = new ArrayType(CD_Object, 1); + + // protected static ObjectType CD_Throwable = new ObjectType("java.lang.Throwable"); + /** Create an instrumenter. Setup debug directories, if needed. */ @SuppressWarnings("nullness:initialization") public Instrument() { - super(); debug_transform.enabled = Chicory.debug_transform || Chicory.debug || Chicory.verbose; - debug_ppt_omit.enabled = debugInstrument.enabled = Chicory.debug; + debug_ppt_omit.enabled = Chicory.debug; + debugInstrument.enabled = Chicory.debug; debug_dir = Chicory.debug_dir; debug_instrumented_dir = new File(debug_dir, "instrumented"); @@ -106,7 +151,7 @@ public Instrument() { /** * Returns true if the given ppt should be ignored. Uses the patterns in {@link * daikon.chicory.Runtime#ppt_omit_pattern} and {@link daikon.chicory.Runtime#ppt_select_pattern}. - * This method is used by both Chicory and Dyncomp. + * This method is called by both Chicory and DynComp. * * @param className class name to be checked * @param methodName method name to be checked @@ -116,14 +161,12 @@ public Instrument() { public static boolean shouldIgnore( @BinaryName String className, @Identifier String methodName, String pptName) { + // Because this comes first, exclusion takes precedence. // Don't instrument the class if it matches an excluded regular expression. for (Pattern pattern : Runtime.ppt_omit_pattern) { - - Matcher mPpt = pattern.matcher(pptName); - Matcher mClass = pattern.matcher(className); - Matcher mMethod = pattern.matcher(methodName); - - if (mPpt.find() || mClass.find() || mMethod.find()) { + if (pattern.matcher(pptName).find() + || pattern.matcher(className).find() + || pattern.matcher(methodName).find()) { debug_ppt_omit.log("ignoring %s, it matches ppt_omit regex %s%n", pptName, pattern); return true; } @@ -132,19 +175,16 @@ public static boolean shouldIgnore( // If any include regular expressions are specified, only instrument // classes that match them. for (Pattern pattern : Runtime.ppt_select_pattern) { - - Matcher mPpt = pattern.matcher(pptName); - Matcher mClass = pattern.matcher(className); - Matcher mMethod = pattern.matcher(methodName); - - if (mPpt.find() || mClass.find() || mMethod.find()) { + if (pattern.matcher(pptName).find() + || pattern.matcher(className).find() + || pattern.matcher(methodName).find()) { debug_ppt_omit.log("including %s, it matches ppt_select regex %s%n", pptName, pattern); return false; } } - // If we're here, this ppt is not explicitly included or excluded, - // so keep unless there were items in the "include only" list. + // If we're here, this ppt is not explicitly included or excluded. + // Keep unless there were items in the "include only" list. if (!Runtime.ppt_select_pattern.isEmpty()) { debug_ppt_omit.log("ignoring %s, not included in ppt_select patterns%n", pptName); return true; @@ -155,19 +195,20 @@ public static boolean shouldIgnore( } /** - * Don't instrument boot classes. They are uninteresting and will not be able to access - * daikon.chicory.Runtime (because it is not on the boot classpath). Previously this code skipped - * classes that started with java, com, javax, or sun, but this is not correct in many cases. Most - * boot classes have the null loader, but some generated classes (such as those in sun.reflect) - * will have a non-null loader. Some of these have a null parent loader, but some do not. The - * check for the sun.reflect package is a hack to catch all of these. A more consistent mechanism - * to determine boot classes would be preferrable. + * Don't instrument boot classes. They are not relevant to the user and cannot access + * daikon.chicory.Runtime (because it is not on the boot classpath). + * + *

Most boot classes have the null loader, but some generated classes (such as those in + * sun.reflect) will have a non-null loader. Some of these have a null parent loader, but some do + * not. The check for the sun.reflect package is a hack to catch all of these. A more consistent + * mechanism to determine boot classes would be preferable. * * @param className class name to be checked * @param loader the class loader for the class * @return true if this is a boot class */ private boolean isBootClass(@BinaryName String className, @Nullable ClassLoader loader) { + // Chicory.boot_classes is extra classes specified by the user. if (Chicory.boot_classes != null) { Matcher matcher = Chicory.boot_classes.matcher(className); if (matcher.find()) { @@ -180,10 +221,10 @@ private boolean isBootClass(@BinaryName String className, @Nullable ClassLoader } else if (loader.getParent() == null) { debug_transform.log("Ignoring system class %s, parent loader == null%n", className); return true; - } else if (className.startsWith("sun.reflect")) { + } else if (className.startsWith("sun.reflect.")) { debug_transform.log("Ignoring system class %s, in sun.reflect package%n", className); return true; - } else if (className.startsWith("jdk.internal.reflect")) { + } else if (className.startsWith("jdk.internal.reflect.")) { // Starting with Java 9 sun.reflect => jdk.internal.reflect. debug_transform.log("Ignoring system class %s, in jdk.internal.reflect package", className); return true; @@ -192,13 +233,13 @@ private boolean isBootClass(@BinaryName String className, @Nullable ClassLoader } /** - * Output a .class file and a .bcel version of the class file. + * Write a .class file and a .bcel version of the class file. * * @param c the Java class to output * @param directory output location for the files * @param className the current class */ - private void outputDebugFiles(JavaClass c, File directory, @BinaryName String className) { + private void writeDebugClassFiles(JavaClass c, File directory, @BinaryName String className) { try { debug_transform.log("Dumping .class and .bcel for %s to %s%n", className, directory); // Write the byte array to a .class file. @@ -208,8 +249,10 @@ private void outputDebugFiles(JavaClass c, File directory, @BinaryName String cl BcelUtil.dump(c, directory); } catch (Throwable t) { System.err.printf("Error %s writing debug files for: %s%n", t, className); - t.printStackTrace(); - // ignore the error, it shouldn't affect the instrumentation + if (debug_transform.enabled) { + t.printStackTrace(); + } + // Ignore the error, it shouldn't affect the instrumentation. } } @@ -229,10 +272,15 @@ private void outputDebugFiles(JavaClass c, File directory, @BinaryName String cl byte[] classfileBuffer) throws IllegalClassFormatException { - // for debugging + // For debugging. // new Throwable().printStackTrace(); - debug_transform.log("Entering chicory.Instrument.transform(): class = %s%n", className); + debug_transform.log("%nEntering chicory.Instrument.transform(): class = %s%n", className); + + if (className == null) { + // most likely a lambda-related class + return null; + } @BinaryName String binaryClassName = Signatures.internalFormToBinaryName(className); @@ -246,7 +294,7 @@ private void outputDebugFiles(JavaClass c, File directory, @BinaryName String cl } // Don't instrument our own code. - if (isChicory(className)) { + if (isChicoryClass(className)) { debug_transform.log("Not transforming Chicory class %s%n", binaryClassName); return null; } @@ -254,11 +302,11 @@ private void outputDebugFiles(JavaClass c, File directory, @BinaryName String cl ClassLoader cfLoader; if (loader == null) { cfLoader = ClassLoader.getSystemClassLoader(); - debug_transform.log("Transforming class %s, loader %s - %s%n", className, loader, cfLoader); + debug_transform.log("Transforming class %s, loaders %s, %s%n", className, loader, cfLoader); } else { cfLoader = loader; debug_transform.log( - "Transforming class %s, loader %s - %s%n", className, loader, loader.getParent()); + "Transforming class %s, loaders %s, %s%n", className, loader, loader.getParent()); } // Parse the bytes of the classfile, die on any errors. @@ -267,39 +315,43 @@ private void outputDebugFiles(JavaClass c, File directory, @BinaryName String cl ClassParser parser = new ClassParser(bais, className); c = parser.parse(); } catch (Throwable t) { - System.err.printf("Error %s while reading %s%n", t, binaryClassName); - t.printStackTrace(); - // No changes to the bytecodes + System.err.printf("Error %s while parsing bytes of %s%n", t, binaryClassName); + if (debug_transform.enabled) { + t.printStackTrace(); + } + // No changes to the bytecodes. return null; } if (Chicory.dump) { - outputDebugFiles(c, debug_uninstrumented_dir, binaryClassName); + writeDebugClassFiles(c, debug_uninstrumented_dir, binaryClassName); } - // Instrument the classfile, die on any errors + // Instrument the classfile, die on any errors. ClassInfo classInfo = new ClassInfo(binaryClassName, cfLoader); JavaClass njc; try { - // Get the class information + // Get the class information. ClassGen cg = new ClassGen(c); instrumentClass(cg, classInfo); njc = cg.getJavaClass(); } catch (Throwable t) { - RuntimeException re = - new RuntimeException(String.format("Error %s in transform of %s", t, binaryClassName), t); - re.printStackTrace(); - throw re; + System.err.printf("Error %s in transform of %s%n", t, binaryClassName); + if (debug_transform.enabled) { + t.printStackTrace(); + } + // No changes to the bytecodes. + return null; } if (classInfo.shouldInclude) { if (Chicory.dump) { - outputDebugFiles(njc, debug_instrumented_dir, binaryClassName); + writeDebugClassFiles(njc, debug_instrumented_dir, binaryClassName); } return njc.getBytes(); } else { debug_transform.log("Didn't instrument %s%n", binaryClassName); - // No changes to the bytecodes + // No changes to the bytecodes. return null; } } @@ -317,24 +369,27 @@ private void instrumentClass(ClassGen cg, ClassInfo classInfo) { // Modify each non-void method to save its result in a local variable before returning. instrument_all_methods(cg, classInfo); - // Remember any constant static fields. + // Store constant static fields in `classInfo`. + // This ought to be a method of ClassInfo, + // but that wouldn't work with both Instrument.java and Instrument24.java. Field[] fields = cg.getFields(); for (Field field : fields) { if (field.isFinal() && field.isStatic() && (field.getType() instanceof BasicType)) { - ConstantValue value = field.getConstantValue(); - String valString; - - if (value == null) { - // System.out.println("WARNING FROM " + field.getName()); - // valString = "WARNING!!!"; - valString = null; + ConstantValue constantValue = field.getConstantValue(); + String valueString; + + String name = field.getName(); + if (constantValue == null) { + // System.out.println("WARNING FROM " + name); + // valueString = "WARNING!!!"; + valueString = null; } else { - valString = value.toString(); - // System.out.println("GOOD FROM " + field.getName() + - // " --- " + valString); + valueString = constantValue.toString(); + // System.out.println("GOOD FROM " + name + + // " --- " + valueString); } - if (valString != null) { - classInfo.staticMap.put(field.getName(), valString); + if (valueString != null) { + classInfo.staticMap.put(name, valueString); } } } @@ -346,15 +401,15 @@ private void instrumentClass(ClassGen cg, ClassInfo classInfo) { } /** - * Adds a call (or calls) to the Chicory Runtime {@code initNotify} method prior to each return in - * the given method. Clients pass the class static initializer {@code } as the method. + * Adds a call to the Chicory Runtime {@code initNotify} method prior to each return in the given + * method. Clients pass the class static initializer {@code } as the method. * * @param cg a class * @param mgen the method to modify, typically the class static initializer {@code } * @param classInfo for the given class * @return the modified method */ - private Method addInvokeToClinit(ClassGen cg, MethodGen mgen, ClassInfo classInfo) { + private Method addInitNotifyCalls(ClassGen cg, MethodGen mgen, ClassInfo classInfo) { try { InstructionList il = mgen.getInstructionList(); @@ -363,23 +418,23 @@ private Method addInvokeToClinit(ClassGen cg, MethodGen mgen, ClassInfo classInf for (InstructionHandle ih = il.getStart(); ih != null; ) { Instruction inst = ih.getInstruction(); - // Get the translation for this instruction (if any) + // Get the translation for this instruction (if any). InstructionList new_il = xform_clinit(cg, classInfo, inst); - // Remember the next instruction to process + // Remember the next instruction to process. InstructionHandle next_ih = ih.getNext(); - // will do nothing if new_il == null + // Will do nothing if new_il == null. insertBeforeHandle(mgen, ih, new_il, false); - // Go on to the next instruction in the list + // Go on to the next instruction in the list. ih = next_ih; } remove_local_variable_type_table(mgen); createNewStackMapAttribute(mgen); - // Update the max stack and Max Locals + // Update the max stack and Max Locals. mgen.setMaxLocals(); mgen.setMaxStack(); mgen.update(); @@ -392,7 +447,7 @@ private Method addInvokeToClinit(ClassGen cg, MethodGen mgen, ClassInfo classInf } /** - * Called by {@link #addInvokeToClinit} to obtain the instructions that represent a call to the + * Called by {@link #addInitNotifyCalls} to obtain the instructions that represent a call to the * Chicory Runtime {@code initNotify} method prior to a return opcode. Returns null if the given * instruction is not a return. * @@ -431,12 +486,12 @@ private Method createClinit(ClassGen cg, ClassInfo classInfo) { InstructionList il = new InstructionList(); il.append(call_initNotify(cg, classInfo)); - il.append(InstructionFactory.createReturn(Type.VOID)); // need to return! + il.append(InstructionFactory.createReturn(CD_void)); // need to return! MethodGen newMethGen = new MethodGen( 8, - Type.VOID, + CD_void, new Type[0], new String[0], "", @@ -445,7 +500,7 @@ private Method createClinit(ClassGen cg, ClassInfo classInfo) { cg.getConstantPool()); newMethGen.update(); - // Update the max stack and Max Locals + // Update the max stack and Max Locals. newMethGen.setMaxLocals(); newMethGen.setMaxStack(); newMethGen.update(); @@ -468,11 +523,7 @@ private InstructionList call_initNotify(ClassGen cg, ClassInfo classInfo) { instructions.append(new PUSH(cp, classInfo.class_name)); instructions.append( instFactory.createInvoke( - runtime_classname, - "initNotify", - Type.VOID, - new Type[] {Type.STRING}, - Const.INVOKESTATIC)); + runtime_classname, "initNotify", CD_void, new Type[] {CD_String}, Const.INVOKESTATIC)); return instructions; } @@ -480,8 +531,8 @@ private InstructionList call_initNotify(ClassGen cg, ClassInfo classInfo) { /** * Instruments all the methods in a class. For each method, adds instrumentation code at the entry * and at each return from the method. In addition, changes each return statement to first place - * the value being returned into a local and then return. This allows us to work around the JDI - * deficiency of not being able to query return values. + * the value being returned into a local and then return. Note that {@link #callEnterOrExit} + * special cases the instrumentation for constructor entry. * * @param cg ClassGen for current class * @param classInfo for the given class @@ -489,32 +540,34 @@ private InstructionList call_initNotify(ClassGen cg, ClassInfo classInfo) { private void instrument_all_methods(ClassGen cg, ClassInfo classInfo) { if (cg.getMajor() < Const.MAJOR_1_6) { - System.out.printf( - "Chicory warning: ClassFile: %s - classfile version (%d) is out of date and may not be" - + " processed correctly.%n", - classInfo.class_name, cg.getMajor()); + String output = + String.format( + "Chicory warning: ClassFile: %s - classfile version (%d) is out of date and may not" + + " be processed correctly.", + classInfo.class_name, cg.getMajor()); + System.out.printf("%s%n", output); + debugInstrument.log("%s%n", output); } - List method_infos = new ArrayList<>(); - + Method[] methods = cg.getMethods(); + List method_infos = new ArrayList<>(methods.length); boolean shouldInclude = false; - try { - for (Method m : cg.getMethods()) { + for (Method m : methods) { - // The class data in StackMapUtils is not thread safe, - // allow only one method at a time to be instrumented. + // The class data in StackMapUtils is not thread safe. + // Allow only one method at a time to be instrumented. // DynComp does this by creating a new instrumentation object // for each class - probably a cleaner solution. synchronized (this) { pool = cg.getConstantPool(); MethodGen mgen = new MethodGen(m, cg.getClassName(), pool); - // check for the class static initializer method + // Check for the class static initializer method. if (mgen.getName().equals("")) { classInfo.hasClinit = true; if (Chicory.checkStaticInit) { - cg.replaceMethod(m, addInvokeToClinit(cg, mgen, classInfo)); + cg.replaceMethod(m, addInitNotifyCalls(cg, mgen, classInfo)); cg.update(); } if (!Chicory.instrument_clinit) { @@ -524,12 +577,12 @@ private void instrument_all_methods(ClassGen cg, ClassInfo classInfo) { } // If method is synthetic... (default constructors and are not synthetic). - if ((Const.ACC_SYNTHETIC & mgen.getAccessFlags()) > 0) { + if ((ACC_SYNTHETIC & mgen.getAccessFlags()) > 0) { // We are not going to instrument this method. continue; } - // Get the instruction list and skip methods with no instructions. + // Skip methods with no instructions. InstructionList il = mgen.getInstructionList(); if (il == null) { // We are not going to instrument this method. @@ -561,15 +614,15 @@ private void instrument_all_methods(ClassGen cg, ClassInfo classInfo) { debugInstrument.log("%n"); } - // Get existing StackMapTable (if present) + // Get existing StackMapTable (if present). setCurrentStackMapTable(mgen, cg.getMajor()); fixLocalVariableTable(mgen); // Create a MethodInfo that describes this method's arguments and exit line numbers // (information not available via reflection) and add it to the list for this class. - MethodInfo curMethodInfo = create_method_info(classInfo, mgen); + MethodInfo curMethodInfo = create_method_info_if_instrumented(classInfo, mgen); - printStackMapTable("After create_method_info"); + printStackMapTable("After create_method_info_if_instrumented"); if (curMethodInfo == null) { // method filtered out! // We are not going to instrument this method. @@ -594,7 +647,7 @@ private void instrument_all_methods(ClassGen cg, ClassInfo classInfo) { SharedData.methods.add(curMethodInfo); } - // Add nonce local to matchup enter/exits + // Add nonce local that matches up enter/exits. addInstrumentationAtEntry(il, mgen); printStackMapTable("After addInstrumentationAtEntry"); @@ -606,30 +659,36 @@ private void instrument_all_methods(ClassGen cg, ClassInfo classInfo) { // the amount of the switch padding changed. modifyStackMapsForSwitches(il.getStart(), il); - Iterator shouldIncludeIter = curMethodInfo.is_included.iterator(); + // exit_location_is_included contains exactly one boolean per return instruction, + // exit_locations contains an integer only when that boolean is true. + Iterator shouldIncludeIter = curMethodInfo.exit_location_is_included.iterator(); Iterator exitLocationIter = curMethodInfo.exit_locations.iterator(); - // Loop through each instruction looking for the return(s) + // Loop through each instruction looking for the return(s). for (InstructionHandle ih = il.getStart(); ih != null; ) { Instruction inst = ih.getInstruction(); - // If this is a return instruction, insert method exit instrumentation + // If this is a return instruction, insert method exit instrumentation. InstructionList new_il = generate_return_instrumentation(inst, mgen, shouldIncludeIter, exitLocationIter); - // Remember the next instruction to process + // Remember the next instruction to process. InstructionHandle next_ih = ih.getNext(); // If this instruction was modified, replace it with the new // instruction list. If this instruction was the target of any - // jumps, replace it with the first instruction in the new list + // jumps, replace it with the first instruction in the new list. insertBeforeHandle(mgen, ih, new_il, true); - // Go on to the next instruction in the list + // Go on to the next instruction in the list. ih = next_ih; } - // Update the Uninitialized_variable_info offsets before + // Check for unused entries. + assert !shouldIncludeIter.hasNext(); + assert !exitLocationIter.hasNext(); + + // Update the Uninitialized_variable_info offsets before. // we write out the new StackMapTable. updateUninitializedNewOffsets(il); @@ -637,15 +696,15 @@ private void instrument_all_methods(ClassGen cg, ClassInfo classInfo) { remove_local_variable_type_table(mgen); - // Update the instruction list + // Update the instruction list. mgen.setInstructionList(il); mgen.update(); - // Update the max stack + // Update the max stack. mgen.setMaxStack(); mgen.update(); - // Update the method in the class + // Update the method in the class. try { cg.replaceMethod(m, mgen.getMethod()); } catch (Exception e) { @@ -669,7 +728,7 @@ private void instrument_all_methods(ClassGen cg, ClassInfo classInfo) { } } } catch (Exception e) { - System.out.printf("Unexpected exception encountered: %s", e); + System.err.printf("Exception encountered: %s", e); e.printStackTrace(); } @@ -723,6 +782,11 @@ private void instrument_all_methods(ClassGen cg, ClassInfo classInfo) { return null; } + // There is a single boolean element on shouldIncludeIter for every return in the method. Its + // value was calculated by {@link #shouldIgnore} and indicates whether or not that return should + // be instrumented. If the value is true the next exitLocationIter element contains the source + // line number for the return in question. + if (!shouldIncludeIter.hasNext()) { throw new RuntimeException("Not enough entries in shouldIncludeIter"); } @@ -735,7 +799,7 @@ private void instrument_all_methods(ClassGen cg, ClassInfo classInfo) { Type type = mgen.getReturnType(); InstructionList newCode = new InstructionList(); - if (type != Type.VOID) { + if (type != CD_void) { LocalVariableGen return_loc = getReturnLocal(mgen, type); newCode.append(InstructionFactory.createDup(type.getSize())); newCode.append(InstructionFactory.createStore(type, return_loc.getIndex())); @@ -753,13 +817,13 @@ private void instrument_all_methods(ClassGen cg, ClassInfo classInfo) { * Returns the local variable used to store the return result. If it is not present, creates it * with the specified type. If the variable is known to already exist, the type can be null. * - * @param mgen describes the given method + * @param mgen describes a method * @param returnType the type of the return; may be null if the variable is known to already exist * @return a local variable to save the return value */ private LocalVariableGen getReturnLocal(MethodGen mgen, @Nullable Type returnType) { - // Find the local used for the return value + // Find the local used for the return value. LocalVariableGen returnLocal = null; for (LocalVariableGen lv : mgen.getLocalVariables()) { if (lv.getName().equals("return__$trace2_val")) { @@ -790,12 +854,12 @@ private LocalVariableGen getReturnLocal(MethodGen mgen, @Nullable Type returnTyp /** * Finds the nonce local variable. Returns null if not present. * - * @param mgen describes the given method + * @param mgen describes a method * @return a local variable to save the nonce value, or null */ private @Nullable LocalVariableGen get_nonce_local(MethodGen mgen) { - // Find the local used for the nonce value + // Find the local used for the nonce value. for (LocalVariableGen lv : mgen.getLocalVariables()) { if (lv.getName().equals("this_invocation_nonce")) { return lv; @@ -808,11 +872,11 @@ private LocalVariableGen getReturnLocal(MethodGen mgen, @Nullable Type returnTyp /** * Inserts instrumentation code at the start of the method. This includes adding a local variable * (this_invocation_nonce) that is initialized to Runtime.nonce++. This provides a unique id on - * each method entry/exit that allows them to be matched up from the dtrace file. Inserts code to - * call daikon.chicory.Runtime.enter(). + * each method entry/exit that allows them to be matched up from the dtrace file. Also inserts + * code to call daikon.chicory.Runtime.enter(). * - * @param instructions instruction list for method - * @param mgen describes the given method + * @param instructions instruction list for the method + * @param mgen describes the method * @throws IOException if there is trouble with I/O */ @SuppressWarnings({ @@ -827,8 +891,8 @@ private void addInstrumentationAtEntry(InstructionList instructions, MethodGen m InstructionList newCode = new InstructionList(); - // create the nonce local variable - LocalVariableGen nonce_lv = create_method_scope_local(mgen, "this_invocation_nonce", Type.INT); + // Create the nonce local variable. + LocalVariableGen nonce_lv = create_method_scope_local(mgen, "this_invocation_nonce", CD_int); printStackMapTable("After cln"); @@ -839,23 +903,23 @@ private void addInstrumentationAtEntry(InstructionList instructions, MethodGen m // The following implements: // this_invocation_nonce = Runtime.nonce++; - // getstatic Runtime.nonce (load reference to AtomicInteger daikon.chicory.Runtime.nonce) + // getstatic Runtime.nonce (load reference to AtomicInteger daikon.chicory.Runtime.nonce). newCode.append(instFactory.createGetStatic(runtime_classname, "nonce", atomic_int_type)); // Do an atomic get and increment of nonce value. // This is multi-thread safe and leaves int value of nonce on stack. newCode.append( instFactory.createInvoke( - atomic_int_classname, "getAndIncrement", Type.INT, new Type[] {}, Const.INVOKEVIRTUAL)); + atomic_int_classname, "getAndIncrement", CD_int, new Type[] {}, Const.INVOKEVIRTUAL)); - // istore (pop original value of nonce into this_invocation_nonce) - newCode.append(InstructionFactory.createStore(Type.INT, nonce_lv.getIndex())); + // istore (pop original value of nonce into this_invocation_nonce). + newCode.append(InstructionFactory.createStore(CD_int, nonce_lv.getIndex())); newCode.setPositions(); InstructionHandle end = newCode.getEnd(); int len_part1 = end.getPosition() + end.getInstruction().getLength(); - // call Runtime.enter() + // Call Runtime.enter(). newCode.append(callEnterOrExit(mgen, "enter", -1)); newCode.setPositions(); @@ -882,7 +946,7 @@ private void addInstrumentationAtEntry(InstructionList instructions, MethodGen m boolean skipFirst = false; - // Modify existing StackMapTable (if present) + // Modify existing StackMapTable (if present). if (stackMapTable.length > 0) { // Each stack map frame specifies (explicity or implicitly) an // offset_delta that is used to calculate the actual bytecode @@ -891,7 +955,7 @@ private void addInstrumentationAtEntry(InstructionList instructions, MethodGen m // previous frame, unless the previous frame is the initial // frame of the method, in which case the bytecode offset is // offset_delta. (From the Java Virual Machine Specification, - // Java SE 7 Edition, section 4.7.4) + // Java SE 7 Edition, section 4.7.4.) // Since we are inserting (1 or 2) new stack map frames at the // beginning of the stack map table, we need to adjust the @@ -942,11 +1006,11 @@ private void addInstrumentationAtEntry(InstructionList instructions, MethodGen m /** * Pushes the object, nonce, parameters, and return value on the stack and calls the specified - * method (normally enter or exit) in daikon.chicory.Runtime. The parameters are passed as an - * array of objects. Any primitive values are wrapped in the appropriate daikon.chicory.Runtime - * wrapper (IntWrap, FloatWrap, etc). + * method (either enter or exit) in daikon.chicory.Runtime. The parameters are passed as an array + * of objects. Any primitive values are wrapped in the appropriate daikon.chicory.Runtime wrapper + * (IntWrap, FloatWrap, etc). * - * @param mgen describes the given method + * @param mgen describes the method to be instrumented * @param methodToCall either "enter" or "exit" * @param line source line number if this is an exit * @return instruction list for instrumenting the enter or exit of the method @@ -957,21 +1021,24 @@ private InstructionList callEnterOrExit(MethodGen mgen, String methodToCall, int Type[] paramTypes = mgen.getArgumentTypes(); // aload - // Push the object. Push null if this is a static method or a constructor. + // Push the object. if (mgen.isStatic() || (methodToCall.equals("enter") && isConstructor(mgen))) { + // Push null if this is a static method or a constructor. newCode.append(new ACONST_NULL()); - } else { // must be an instance method - newCode.append(InstructionFactory.createLoad(Type.OBJECT, 0)); + } else { + // Must be an instance method. + newCode.append(InstructionFactory.createLoad(CD_Object, 0)); } // The offset of the first parameter. int param_offset = mgen.isStatic() ? 0 : 1; + // Assumes addInstrumentationAtEntry has been called to create the nonce local. // iload // Push the nonce. @SuppressWarnings("nullness:assignment") // the nonce local exists @NonNull LocalVariableGen nonce_lv = get_nonce_local(mgen); - newCode.append(InstructionFactory.createLoad(Type.INT, nonce_lv.getIndex())); + newCode.append(InstructionFactory.createLoad(CD_int, nonce_lv.getIndex())); // iconst // Push the MethodInfo index. @@ -981,22 +1048,20 @@ private InstructionList callEnterOrExit(MethodGen mgen, String methodToCall, int // anewarray // Create an array of objects with elements for each parameter. newCode.append(instFactory.createConstant(paramTypes.length)); - newCode.append(instFactory.createNewArray(Type.OBJECT, (short) 1)); - - Type object_arr_typ = new ArrayType("java.lang.Object", 1); + newCode.append(instFactory.createNewArray(CD_Object, (short) 1)); // Put each parameter into the array. int param_index = param_offset; for (int ii = 0; ii < paramTypes.length; ii++) { - newCode.append(InstructionFactory.createDup(object_arr_typ.getSize())); + newCode.append(InstructionFactory.createDup(CD_Object_array.getSize())); newCode.append(instFactory.createConstant(ii)); Type at = paramTypes[ii]; if (at instanceof BasicType) { newCode.append(createPrimitiveWrapper(at, param_index)); } else { // must be reference of some sort - newCode.append(InstructionFactory.createLoad(Type.OBJECT, param_index)); + newCode.append(InstructionFactory.createLoad(CD_Object, param_index)); } - newCode.append(InstructionFactory.createArrayStore(Type.OBJECT)); + newCode.append(InstructionFactory.createArrayStore(CD_Object)); param_index += at.getSize(); } @@ -1005,33 +1070,32 @@ private InstructionList callEnterOrExit(MethodGen mgen, String methodToCall, int // If the return value is a primitive, wrap it in the appropriate wrapper. if (methodToCall.equals("exit")) { Type ret_type = mgen.getReturnType(); - if (ret_type == Type.VOID) { + if (ret_type == CD_void) { newCode.append(new ACONST_NULL()); } else { LocalVariableGen returnLocal = getReturnLocal(mgen, ret_type); if (ret_type instanceof BasicType) { newCode.append(createPrimitiveWrapper(ret_type, returnLocal.getIndex())); } else { - newCode.append(InstructionFactory.createLoad(Type.OBJECT, returnLocal.getIndex())); + newCode.append(InstructionFactory.createLoad(CD_Object, returnLocal.getIndex())); } } - // push line number + // Push the line number. // System.out.println(mgen.getName() + " --> " + line); newCode.append(instFactory.createConstant(line)); } - // Call the specified method - Type[] methodArgs; + // Call the specified method. + Type[] methodParams; if (methodToCall.equals("exit")) { - methodArgs = - new Type[] {Type.OBJECT, Type.INT, Type.INT, object_arr_typ, Type.OBJECT, Type.INT}; + methodParams = new Type[] {CD_Object, CD_int, CD_int, CD_Object_array, CD_Object, CD_int}; } else { - methodArgs = new Type[] {Type.OBJECT, Type.INT, Type.INT, object_arr_typ}; + methodParams = new Type[] {CD_Object, CD_int, CD_int, CD_Object_array}; } newCode.append( instFactory.createInvoke( - runtime_classname, methodToCall, Type.VOID, methodArgs, Const.INVOKESTATIC)); + runtime_classname, methodToCall, CD_void, methodParams, Const.INVOKESTATIC)); return newCode; } @@ -1082,11 +1146,11 @@ private InstructionList createPrimitiveWrapper(Type prim_type, int var_index) { InstructionList newCode = new InstructionList(); String classname = runtime_classname + "$" + wrapperClassName; newCode.append(instFactory.createNew(classname)); - newCode.append(InstructionFactory.createDup(Type.OBJECT.getSize())); + newCode.append(InstructionFactory.createDup(CD_Object.getSize())); newCode.append(InstructionFactory.createLoad(prim_type, var_index)); newCode.append( instFactory.createInvoke( - classname, "", Type.VOID, new Type[] {prim_type}, Const.INVOKESPECIAL)); + classname, "", CD_void, new Type[] {prim_type}, Const.INVOKESPECIAL)); return newCode; } @@ -1108,40 +1172,25 @@ private boolean isConstructor(MethodGen mgen) { } /** - * Returns an array of strings, each corresponding to mgen's parameter types as a fully qualified - * name: how a type is represented in Java source code. + * Returns an array of fully qualified names, one for each of mgen's parameter types. * * @param mgen describes the given method * @return an array of strings, each corresponding to mgen's parameter types */ + @SuppressWarnings("signature") // BCEL is not annotated private @BinaryName String[] getFullyQualifiedParameterTypes(MethodGen mgen) { - - Type[] paramTypes = mgen.getArgumentTypes(); - @BinaryName String[] param_type_strings = new @BinaryName String[paramTypes.length]; - - for (int ii = 0; ii < paramTypes.length; ii++) { - Type t = paramTypes[ii]; - /*if (t instanceof ObjectType) - param_type_strings[ii] = ((ObjectType) t).getClassName(); - else { - param_type_strings[ii] = t.getSignature().replace('/', '.'); - } - */ - param_type_strings[ii] = t.toString(); - } - - return param_type_strings; + return ArraysPlume.mapArray(Type::toString, mgen.getArgumentTypes(), String.class); } /** - * Creates a MethodInfo struct corresponding to {@code mgen}. + * Creates a MethodInfo corresponding to {@code mgen}. * - * @param classInfo a class - * @param mgen describes the given method + * @param classInfo class containing the method + * @param mgen method to inspect * @return a new MethodInfo for the method, or null if the method should not be instrumented */ - @SuppressWarnings("unchecked") - private @Nullable MethodInfo create_method_info(ClassInfo classInfo, MethodGen mgen) { + private @Nullable MethodInfo create_method_info_if_instrumented( + ClassInfo classInfo, MethodGen mgen) { // Get the parameter names for this method. String[] paramNames = mgen.getArgumentNames(); @@ -1150,10 +1199,12 @@ private boolean isConstructor(MethodGen mgen) { if (mgen.isStatic()) { param_offset = 0; } + if (debugInstrument.enabled) { - debugInstrument.log("create_method_info1 %s%n", paramNames.length); - for (int ii = 0; ii < paramNames.length; ii++) { - debugInstrument.log("param: %s%n", paramNames[ii]); + debugInstrument.log("create_method_info_if_instrumented for: %s%n", classInfo.class_name); + debugInstrument.log("number of parameters: %s%n", paramNames.length); + for (String paramName : paramNames) { + debugInstrument.log("param name: %s%n", paramName); } } @@ -1166,7 +1217,7 @@ private boolean isConstructor(MethodGen mgen) { String arg0Name = mgen.getArgumentType(0).toString(); if (dollarPos >= 0 && - // type of first parameter is classname up to the "$" + // Type of first parameter is classname up to the "$". mgen.getClassName().substring(0, dollarPos).equals(arg0Name)) { // As a further check, for javac-generated classfiles, the // constant pool index #1 is "this$0", and the first 5 bytes of @@ -1180,16 +1231,17 @@ private boolean isConstructor(MethodGen mgen) { } } - for (int ii = lv_start; ii < paramNames.length; ii++) { - if ((ii + param_offset) < lvs.length) { - paramNames[ii] = lvs[ii + param_offset].getName(); + for (int i = lv_start; i < paramNames.length; i++) { + if ((i + param_offset) < lvs.length) { + paramNames[i] = lvs[i + param_offset].getName(); } } if (debugInstrument.enabled) { - debugInstrument.log("create_method_info2 %s%n", paramNames.length); - for (int ii = 0; ii < paramNames.length; ii++) { - debugInstrument.log("param: %s%n", paramNames[ii]); + debugInstrument.log("create_method_info_if_instrumented part 2%n"); + debugInstrument.log("number of parameters: %s%n", paramNames.length); + for (String paramName : paramNames) { + debugInstrument.log("param name: %s%n", paramName); } } @@ -1215,7 +1267,7 @@ private boolean isConstructor(MethodGen mgen) { } // Loop through each instruction and find the line number for each return opcode. - List exit_locs = new ArrayList<>(); + List exit_line_numbers = new ArrayList<>(); // Tells whether each exit loc in the method is included or not (based on filters). List isIncluded = new ArrayList<>(); @@ -1223,7 +1275,7 @@ private boolean isConstructor(MethodGen mgen) { debugInstrument.log("Looking for exit points in %s%n", mgen.getName()); InstructionList il = mgen.getInstructionList(); int line_number = 0; - int last_line_number = 0; + int prev_line_number = 0; for (InstructionHandle ih : il) { boolean foundLine = false; @@ -1248,13 +1300,13 @@ private boolean isConstructor(MethodGen mgen) { case Const.RETURN: debugInstrument.log("Exit at line %d%n", line_number); - // Only do incremental lines if we don't have the line generator. - if (line_number == last_line_number && foundLine == false) { + // Only do incremental lines if we haven't seen a line number since the last return. + if (line_number == prev_line_number && !foundLine) { debugInstrument.log("Could not find line %d%n", line_number); line_number++; } - last_line_number = line_number; + prev_line_number = line_number; if (!shouldIgnore( classInfo.class_name, @@ -1266,7 +1318,7 @@ private boolean isConstructor(MethodGen mgen) { mgen.getName(), line_number))) { shouldInclude = true; - exit_locs.add(line_number); + exit_line_numbers.add(line_number); isIncluded.add(true); } else { @@ -1282,7 +1334,7 @@ private boolean isConstructor(MethodGen mgen) { if (shouldInclude) { return new MethodInfo( - classInfo, mgen.getName(), paramNames, param_type_strings, exit_locs, isIncluded); + classInfo, mgen.getName(), paramNames, param_type_strings, exit_line_numbers, isIncluded); } else { return null; } @@ -1293,11 +1345,12 @@ private boolean isConstructor(MethodGen mgen) { * * @param mgen describes the given method */ - @SuppressWarnings("nullness:dereference.of.nullable") public void dump_code_attributes(MethodGen mgen) { // mgen.getMethod().getCode().getAttributes() forces attributes - // to be instantiated; mgen.getCodeAttributes() does not - for (Attribute a : mgen.getMethod().getCode().getAttributes()) { + // to be instantiated; mgen.getCodeAttributes() does not. + @SuppressWarnings("nullness:assignment") + @NonNull Code code = mgen.getMethod().getCode(); + for (Attribute a : code.getAttributes()) { int con_index = a.getNameIndex(); Constant c = pool.getConstant(con_index); String att_name = ((ConstantUtf8) c).getBytes(); @@ -1313,7 +1366,7 @@ public void dump_code_attributes(MethodGen mgen) { * @return true if the given class is part of Chicory itself */ @Pure - private static boolean isChicory(@InternalForm String classname) { + private static boolean isChicoryClass(@InternalForm String classname) { if (classname.startsWith("daikon/chicory/") && !classname.equals("daikon/chicory/ChicoryTest")) { diff --git a/java/daikon/chicory/Instrument24.java b/java/daikon/chicory/Instrument24.java index e7ac7c0d64..b69fb1171f 100644 --- a/java/daikon/chicory/Instrument24.java +++ b/java/daikon/chicory/Instrument24.java @@ -1,5 +1,7 @@ package daikon.chicory; +import static java.lang.classfile.ClassFile.ACC_STATIC; +import static java.lang.classfile.ClassFile.ACC_SYNTHETIC; import static java.lang.constant.ConstantDescs.CD_Object; import static java.lang.constant.ConstantDescs.CD_String; import static java.lang.constant.ConstantDescs.CD_int; @@ -9,6 +11,7 @@ import daikon.plumelib.bcelutil.BcelUtil; import daikon.plumelib.bcelutil.SimpleLog; import daikon.plumelib.reflection.Signatures; +import daikon.plumelib.util.ArraysPlume; import java.io.ByteArrayInputStream; import java.io.File; import java.lang.classfile.Attributes; @@ -86,14 +89,14 @@ import org.checkerframework.dataflow.qual.Pure; /** - * This class is responsible for modifying another class's bytecodes. Specifically, its main task is - * to add calls into the Chicory runtime at method entries and exits for instrumentation purposes. - * These added calls are sometimes referred to as "hooks". + * This class modifies another class's bytecodes. It adds calls into the Chicory runtime at method + * entries and exits for instrumentation purposes. These added calls are sometimes referred to as + * "hooks". * *

This class is loaded by ChicoryPremain at startup. It is a ClassFileTransformer which means - * that its {@code transform} method gets called each time the JVM loads a class. + * that its {@link #transform} method gets called each time the JVM loads a class. * - *

Instrument24 uses Java's ({@code java.lang.classfile}) APIs for reading and modifying .class + *

Instrument24 uses Java's {@code java.lang.classfile} APIs for reading and modifying .class * files. Those APIs were added in JDK 24. Compared to BCEL, these APIs are more complete and robust * (no more fiddling with StackMaps) and are always up to date with any .class file changes (since * they are part of the JDK). (We will need to continue to support Instrument.java using BCEL, as we @@ -101,17 +104,17 @@ */ public class Instrument24 implements ClassFileTransformer { - /** The location of the runtime support class. */ + /** The name of the Chicory runtime support class. */ private static final String runtime_classname = "daikon.chicory.Runtime"; /** The ClassDesc for the Chicory runtime support class. */ private static final ClassDesc runtimeCD = ClassDesc.of(runtime_classname); - /** Debug information about which classes and/or methods are transformed and why. */ + /** A log for debug information about which classes and/or methods are transformed and why. */ protected static final SimpleLog debug_transform = new SimpleLog(false); - // Public so can be enabled from daikon.dcomp.Instrument24. - /** Debug information about ppt-omit and ppt-select. */ + // Public so daikon.dcomp.Instrument24 can enable it. + /** A log for debug information about ppt-omit and ppt-select. */ public static final SimpleLog debug_ppt_omit = new SimpleLog(false); /** A log to which to print debugging information about program instrumentation. */ @@ -120,7 +123,7 @@ public class Instrument24 implements ClassFileTransformer { /** Directory for debug output. */ final File debug_dir; - /** Directory into which to dump debug-instrumented classes. */ + /** Directory into which to dump instrumented classes. */ final File debug_instrumented_dir; /** Directory into which to dump original classes. */ @@ -129,7 +132,8 @@ public class Instrument24 implements ClassFileTransformer { /** Create an instrumenter. Setup debug directories, if needed. */ public Instrument24() { debug_transform.enabled = Chicory.debug_transform || Chicory.debug || Chicory.verbose; - debug_ppt_omit.enabled = debugInstrument.enabled = Chicory.debug; + debug_ppt_omit.enabled = Chicory.debug; + debugInstrument.enabled = Chicory.debug; debug_dir = Chicory.debug_dir; debug_instrumented_dir = new File(debug_dir, "instrumented"); @@ -144,7 +148,7 @@ public Instrument24() { /** * Returns true if the given ppt should be ignored. Uses the patterns in {@link * daikon.chicory.Runtime#ppt_omit_pattern} and {@link daikon.chicory.Runtime#ppt_select_pattern}. - * This method is used by both Chicory and DynComp. + * This method is called by both Chicory and DynComp. * * @param className class name to be checked * @param methodName method name to be checked @@ -154,14 +158,12 @@ public Instrument24() { public static boolean shouldIgnore( @BinaryName String className, @Identifier String methodName, String pptName) { + // Because this comes first, exclusion takes precedence. // Don't instrument the class if it matches an excluded regular expression. for (Pattern pattern : Runtime.ppt_omit_pattern) { - - Matcher mPpt = pattern.matcher(pptName); - Matcher mClass = pattern.matcher(className); - Matcher mMethod = pattern.matcher(methodName); - - if (mPpt.find() || mClass.find() || mMethod.find()) { + if (pattern.matcher(pptName).find() + || pattern.matcher(className).find() + || pattern.matcher(methodName).find()) { debug_ppt_omit.log("ignoring %s, it matches ppt_omit regex %s%n", pptName, pattern); return true; } @@ -170,19 +172,16 @@ public static boolean shouldIgnore( // If any include regular expressions are specified, only instrument // classes that match them. for (Pattern pattern : Runtime.ppt_select_pattern) { - - Matcher mPpt = pattern.matcher(pptName); - Matcher mClass = pattern.matcher(className); - Matcher mMethod = pattern.matcher(methodName); - - if (mPpt.find() || mClass.find() || mMethod.find()) { + if (pattern.matcher(pptName).find() + || pattern.matcher(className).find() + || pattern.matcher(methodName).find()) { debug_ppt_omit.log("including %s, it matches ppt_select regex %s%n", pptName, pattern); return false; } } - // If we're here, this ppt is not explicitly included or excluded, - // so keep unless there were items in the "include only" list. + // If we're here, this ppt is not explicitly included or excluded. + // Keep unless there were items in the "include only" list. if (!Runtime.ppt_select_pattern.isEmpty()) { debug_ppt_omit.log("ignoring %s, not included in ppt_select patterns%n", pptName); return true; @@ -193,19 +192,20 @@ public static boolean shouldIgnore( } /** - * Don't instrument boot classes. They are uninteresting and will not be able to access - * daikon.chicory.Runtime (because it is not on the boot classpath). Previously this code skipped - * classes that started with java, com, javax, or sun, but this is not correct in many cases. Most - * boot classes have the null loader, but some generated classes (such as those in sun.reflect) - * will have a non-null loader. Some of these have a null parent loader, but some do not. The - * check for the sun.reflect package is a hack to catch all of these. A more consistent mechanism - * to determine boot classes would be preferable. + * Don't instrument boot classes. They are not relevant to the user and cannot access + * daikon.chicory.Runtime (because it is not on the boot classpath). + * + *

Most boot classes have the null loader, but some generated classes (such as those in + * sun.reflect) will have a non-null loader. Some of these have a null parent loader, but some do + * not. The check for the sun.reflect package is a hack to catch all of these. A more consistent + * mechanism to determine boot classes would be preferable. * * @param className class name to be checked * @param loader the class loader for the class * @return true if this is a boot class */ private boolean isBootClass(@BinaryName String className, @Nullable ClassLoader loader) { + // Chicory.boot_classes is extra classes specified by the user. if (Chicory.boot_classes != null) { Matcher matcher = Chicory.boot_classes.matcher(className); if (matcher.find()) { @@ -218,10 +218,10 @@ private boolean isBootClass(@BinaryName String className, @Nullable ClassLoader } else if (loader.getParent() == null) { debug_transform.log("Ignoring system class %s, parent loader == null%n", className); return true; - } else if (className.startsWith("sun.reflect")) { + } else if (className.startsWith("sun.reflect.")) { debug_transform.log("Ignoring system class %s, in sun.reflect package%n", className); return true; - } else if (className.startsWith("jdk.internal.reflect")) { + } else if (className.startsWith("jdk.internal.reflect.")) { // Starting with Java 9 sun.reflect => jdk.internal.reflect. debug_transform.log("Ignoring system class %s, in jdk.internal.reflect package", className); return true; @@ -230,23 +230,27 @@ private boolean isBootClass(@BinaryName String className, @Nullable ClassLoader } /** - * Output a .class file and a .bcel version of the class file. + * Write a .class file and a .bcel version of the class file. * - * @param classBytes a byte array of the class file to output + * @param classBytes a byte array of the class file to output. This is the whole classfile, not + * just the bytecodes part of it. * @param directory output location for the files * @param className the current class */ - private void outputDebugFiles(byte[] classBytes, File directory, @BinaryName String className) { + private void writeDebugClassFiles( + byte[] classBytes, File directory, @BinaryName String className) { + // UNDONE: Should we stop using bcel and use classModel.toDebugString() instead? // Convert the classBytes to a BCEL JavaClass - JavaClass c; + JavaClass c = null; try (ByteArrayInputStream bais = new ByteArrayInputStream(classBytes)) { ClassParser parser = new ClassParser(bais, className); c = parser.parse(); } catch (Throwable t) { - System.err.printf("Error %s while reading %s%n", t, className); - t.printStackTrace(); - // ignore the error, it shouldn't affect the instrumentation - return; + System.err.printf("Error %s while parsing the bytes of %s%n", t, className); + if (debug_transform.enabled) { + t.printStackTrace(); + } + // Ignore the error, it shouldn't affect the instrumentation. } try { @@ -255,11 +259,15 @@ private void outputDebugFiles(byte[] classBytes, File directory, @BinaryName Str File outputFile = new File(directory, className + ".class"); Files.write(outputFile.toPath(), classBytes); // Write a BCEL-like file. - BcelUtil.dump(c, directory); + if (c != null) { + BcelUtil.dump(c, directory); + } } catch (Throwable t) { System.err.printf("Error %s writing debug files for: %s%n", t, className); - t.printStackTrace(); - // ignore the error, it shouldn't affect the instrumentation + if (debug_transform.enabled) { + t.printStackTrace(); + } + // Ignore the error, it shouldn't affect the instrumentation. } } @@ -279,10 +287,15 @@ private void outputDebugFiles(byte[] classBytes, File directory, @BinaryName Str byte[] classfileBuffer) throws IllegalClassFormatException { - // for debugging + // For debugging. // new Throwable().printStackTrace(); - debug_transform.log("Entering chicory.Instrument24.transform(): class = %s%n", className); + debug_transform.log("%nEntering chicory.Instrument24.transform(): class = %s%n", className); + + if (className == null) { + // most likely a lambda-related class + return null; + } @BinaryName String binaryClassName = Signatures.internalFormToBinaryName(className); @@ -290,8 +303,13 @@ private void outputDebugFiles(byte[] classBytes, File directory, @BinaryName Str return null; } + if (className.contains("/$Proxy")) { + debug_transform.log("Skipping proxy class %s%n", binaryClassName); + return null; + } + // Don't instrument our own code. - if (isChicory(className)) { + if (isChicoryClass(className)) { debug_transform.log("Not transforming Chicory class %s%n", binaryClassName); return null; } @@ -299,11 +317,11 @@ private void outputDebugFiles(byte[] classBytes, File directory, @BinaryName Str ClassLoader cfLoader; if (loader == null) { cfLoader = ClassLoader.getSystemClassLoader(); - debug_transform.log("Transforming class %s, loader %s - %s%n", className, loader, cfLoader); + debug_transform.log("Transforming class %s, loaders %s, %s%n", className, loader, cfLoader); } else { cfLoader = loader; debug_transform.log( - "Transforming class %s, loader %s - %s%n", className, loader, loader.getParent()); + "Transforming class %s, loaders %s, %s%n", className, loader, loader.getParent()); } // Parse the bytes of the classfile, die on any errors. @@ -316,43 +334,46 @@ private void outputDebugFiles(byte[] classBytes, File directory, @BinaryName Str try { classModel = classFile.parse(classfileBuffer); } catch (Throwable t) { - System.err.printf("Error %s while reading %s%n", t, binaryClassName); - t.printStackTrace(); - // No changes to the bytecodes + System.err.printf("Error %s while parsing bytes of %s%n", t, binaryClassName); + if (debug_transform.enabled) { + t.printStackTrace(); + } + // No changes to the bytecodes. return null; } if (Chicory.dump) { - outputDebugFiles( + writeDebugClassFiles( classFile.transformClass(classModel, ClassTransform.ACCEPT_ALL), debug_uninstrumented_dir, binaryClassName); } - // Instrument the classfile, die on any errors + // Instrument the classfile, die on any errors. ClassInfo classInfo = new ClassInfo(binaryClassName, cfLoader); byte[] newBytes; - debug_transform.log("%nTransforming: %s%n", binaryClassName); try { newBytes = classFile.build( classModel.thisClass().asSymbol(), classBuilder -> instrumentClass(classBuilder, classModel, classInfo)); } catch (Throwable t) { - RuntimeException re = - new RuntimeException(String.format("Error %s in transform of %s", t, binaryClassName), t); - re.printStackTrace(); - throw re; + System.err.printf("Error %s in transform of %s%n", t, binaryClassName); + if (debug_transform.enabled) { + t.printStackTrace(); + } + // No changes to the bytecodes. + return null; } if (classInfo.shouldInclude) { if (Chicory.dump) { - outputDebugFiles(newBytes, debug_instrumented_dir, binaryClassName); + writeDebugClassFiles(newBytes, debug_instrumented_dir, binaryClassName); } return newBytes; } else { debug_transform.log("Didn't instrument %s%n", binaryClassName); - // No changes to the bytecodes + // No changes to the bytecodes. return null; } } @@ -367,9 +388,7 @@ private void outputDebugFiles(byte[] classBytes, File directory, @BinaryName Str private void instrumentClass( ClassBuilder classBuilder, ClassModel classModel, ClassInfo classInfo) { - debugInstrument.log("Class Name:%n"); - @InternalForm String temp = classModel.thisClass().asInternalName(); - debugInstrument.log(" %s%n", Signatures.internalFormToBinaryName(temp)); + debug_transform.log("%nInstrumenting class: %s%n", classInfo.class_name); debugInstrument.log("Class Attributes:%n"); for (java.lang.classfile.Attribute a : classModel.attributes()) { @@ -384,15 +403,39 @@ private void instrumentClass( // Modify each non-void method to save its result in a local variable before returning. instrument_all_methods(classModel, classBuilder, classInfo); - // Remember any constant static fields. + // Copy all other ClassElements to output class unchanged. + for (ClassElement ce : classModel) { + debugInstrument.log("ClassElement: %s%n", ce); + switch (ce) { + case MethodModel mm -> {} + // Copy all other ClassElements to output class unchanged. + default -> classBuilder.with(ce); + } + } + + if (classInfo.shouldInclude) { + debug_transform.log("Added trace info to class %s%n", classInfo); + synchronized (SharedData.new_classes) { + SharedData.new_classes.add(classInfo); + } + synchronized (SharedData.all_classes) { + SharedData.all_classes.add(classInfo); + } + } else { // not included + debug_transform.log("Trace info not added to class %s%n", classInfo); + } + + // Store constant static fields in `classInfo`. + // This ought to be a method of ClassInfo, + // but that wouldn't work with both Instrument.java and Instrument24.java. List fields = classModel.fields(); for (FieldModel fm : fields) { Optional cva = fm.findAttribute(Attributes.constantValue()); if (cva.isPresent()) { String name = fm.fieldName().stringValue(); - String value = formatConstantDesc(cva.get().constant().constantValue()); - debugInstrument.log(" Constant field: %s, value: %s%n", name, value); - classInfo.staticMap.put(name, value); + String valueString = formatConstantDesc(cva.get().constant().constantValue()); + debugInstrument.log(" Constant field: %s, valueString: %s%n", name, valueString); + classInfo.staticMap.put(name, valueString); } } @@ -403,13 +446,13 @@ private void instrumentClass( } /** - * Adds a call (or calls) to the Chicory Runtime {@code initNotify} method prior to each return in - * the given method. Clients pass the class static initializer {@code } as the method. + * Adds a call to the Chicory Runtime {@code initNotify} method prior to each return in the given + * method. Clients pass the class static initializer {@code } as the method. * * @param mgen the method to modify, typically the class static initializer {@code } * @param classInfo for the given class */ - private void addInvokeToClinit(MethodGen24 mgen, ClassInfo classInfo) { + private void addInitNotifyCalls(MethodGen24 mgen, ClassInfo classInfo) { try { List il = mgen.getInstructionList(); @@ -418,19 +461,17 @@ private void addInvokeToClinit(MethodGen24 mgen, ClassInfo classInfo) { CodeElement inst = li.next(); - // Back up iterator to point to `inst`. - li.previous(); - // Get the translation for this instruction (if any). if (inst instanceof ReturnInstruction) { + // Back up iterator to point before `inst`. + li.previous(); // Insert code prior to `inst`. for (CodeElement ce : call_initNotify(mgen.getPoolBuilder(), classInfo)) { li.add(ce); } + // Skip over `inst` we just inserted new_il in front of. + li.next(); } - - // Skip over `inst` we just inserted new_il in front of. - li.next(); } } catch (Exception e) { System.err.printf("Unexpected exception encountered: %s", e); @@ -453,7 +494,7 @@ private void createClinit(ClassBuilder classBuilder, ClassInfo classInfo) { classBuilder.withMethod( "", MethodTypeDesc.of(CD_void), - ClassFile.ACC_STATIC, + ACC_STATIC, methodBuilder -> methodBuilder.withCode(codeBuilder -> copyCode(codeBuilder, instructions))); } @@ -467,7 +508,7 @@ private void createClinit(ClassBuilder classBuilder, ClassInfo classInfo) { */ private List call_initNotify(ConstantPoolBuilder poolBuilder, ClassInfo classInfo) { - List instructions = new ArrayList<>(); + List instructions = new ArrayList<>(2); MethodRefEntry mre = poolBuilder.methodRefEntry(runtimeCD, "initNotify", MethodTypeDesc.of(CD_void, CD_String)); @@ -480,8 +521,8 @@ private List call_initNotify(ConstantPoolBuilder poolBuilder, Class /** * Instruments all the methods in a class. For each method, adds instrumentation code at the entry * and at each return from the method. In addition, changes each return statement to first place - * the value being returned into a local and then return. This allows us to work around the JDI - * deficiency of not being able to query return values. + * the value being returned into a local and then return. Note that {@link #callEnterOrExit} + * special cases the instrumentation for constructor entry. * * @param classModel for current class * @param classBuilder for current class @@ -491,55 +532,54 @@ private void instrument_all_methods( ClassModel classModel, ClassBuilder classBuilder, ClassInfo classInfo) { if (classModel.majorVersion() < ClassFile.JAVA_6_VERSION) { - System.out.printf( - "Chicory warning: ClassFile: %s - classfile version (%d) is out of date and may not be" - + " processed correctly.%n", - classInfo.class_name, classModel.majorVersion()); + String output = + String.format( + "Chicory warning: ClassFile: %s - classfile version (%d) is out of date and may not" + + " be processed correctly.", + classInfo.class_name, classModel.majorVersion()); + System.out.printf("%s%n", output); + debugInstrument.log("%s%n", output); } - List method_infos = new ArrayList<>(); - + List methods = classModel.methods(); + List method_infos = new ArrayList<>(methods.size()); boolean shouldInclude = false; - try { - for (MethodModel mm : classModel.methods()) { + for (MethodModel mm : methods) { - // NOT SURE THIS APPLIES ANYMORE - // don't plan to use StackMapUtils - // The class data in StackMapUtils is not thread safe, - // allow only one method at a time to be instrumented. + // Allow only one method at a time to be instrumented. // DynComp does this by creating a new instrumentation object // for each class - probably a cleaner solution. synchronized (this) { MethodGen24 mgen = new MethodGen24(mm, classInfo.class_name, classBuilder); - // check for the class static initializer method + // Check for the class static initializer method. if (mgen.getName().equals("")) { classInfo.hasClinit = true; if (Chicory.checkStaticInit) { - addInvokeToClinit(mgen, classInfo); + addInitNotifyCalls(mgen, classInfo); } if (!Chicory.instrument_clinit) { // We are not going to instrument this method. // We need to copy it to the output class. - outputMethodUnchanged(classBuilder, mm, mgen); + copyMethodToOutputUnchanged(classBuilder, mm, mgen); continue; } } // If method is synthetic... (default constructors and are not synthetic). - if ((mgen.getAccessFlagsMask() & ClassFile.ACC_SYNTHETIC) != 0) { + if ((mgen.getAccessFlagsMask() & ACC_SYNTHETIC) != 0) { // We are not going to instrument this method. // We need to copy it to the output class. - outputMethodUnchanged(classBuilder, mm, mgen); + copyMethodToOutputUnchanged(classBuilder, mm, mgen); continue; } - // Get the instruction list and skip methods with no instructions. + // Skip methods with no instructions. if (mgen.getInstructionList().isEmpty()) { // We are not going to instrument this method. // We need to copy it to the output class. - outputMethodUnchanged(classBuilder, mm, mgen); + copyMethodToOutputUnchanged(classBuilder, mm, mgen); continue; } @@ -581,12 +621,12 @@ private void instrument_all_methods( // Create a MethodInfo that describes this method's arguments and exit line numbers // (information not available via reflection) and add it to the list for this class. - MethodInfo curMethodInfo = create_method_info(classInfo, mgen); + MethodInfo curMethodInfo = create_method_info_if_instrumented(classInfo, mgen); if (curMethodInfo == null) { // method filtered out! // We are not going to instrument this method. // We need to copy it to the output class. - outputMethodUnchanged(classBuilder, mm, mgen); + copyMethodToOutputUnchanged(classBuilder, mm, mgen); continue; } @@ -601,7 +641,7 @@ private void instrument_all_methods( SharedData.methods.add(curMethodInfo); } - // Add entry instrumentation and instrument return instructions. + // Instrument entry and exits (return instructions). classBuilder.withMethod( mm.methodName().stringValue(), mm.methodTypeSymbol(), @@ -615,43 +655,23 @@ private void instrument_all_methods( e.printStackTrace(); } - // Copy all other ClassElements to output class (unchanged). - for (ClassElement ce : classModel) { - debugInstrument.log("ClassElement: %s%n", ce); - switch (ce) { - case MethodModel mm -> {} - // Copy all other ClassElements to output class (unchanged). - default -> classBuilder.with(ce); - } - } + classInfo.shouldInclude = shouldInclude; // Add the class and method information to runtime so it is available // as enter/exit ppts are processed. classInfo.set_method_infos(method_infos); - - if (shouldInclude) { - debug_transform.log("Added trace info to class %s%n", classInfo); - synchronized (SharedData.new_classes) { - SharedData.new_classes.add(classInfo); - } - synchronized (SharedData.all_classes) { - SharedData.all_classes.add(classInfo); - } - } else { // not included - debug_transform.log("Trace info not added to class %s%n", classInfo); - } - classInfo.shouldInclude = shouldInclude; } /** - * Copy the given method from the input class file to the output output class with no changes. - * Uses {@code copyMethod} to perform the actual copy. + * Copy the given method from the input class file to the output class with no changes. Uses + * {@link #copyMethod} to perform the actual copy. * * @param classBuilder for the output class - * @param mm MethodModel describes the input method + * @param mm the input method * @param mgen describes the output method */ - private void outputMethodUnchanged(ClassBuilder classBuilder, MethodModel mm, MethodGen24 mgen) { + private void copyMethodToOutputUnchanged( + ClassBuilder classBuilder, MethodModel mm, MethodGen24 mgen) { classBuilder.withMethod( mm.methodName().stringValue(), mm.methodTypeSymbol(), @@ -660,7 +680,7 @@ private void outputMethodUnchanged(ClassBuilder classBuilder, MethodModel mm, Me } /** - * Copy the given method from the input class file to the output output class with no changes. + * Copy the given method from the input class file to the output class with no changes. * * @param methodBuilder for the output class * @param methodModel describes the input method @@ -674,7 +694,7 @@ private void copyMethod(MethodBuilder methodBuilder, MethodModel methodModel, Me case CodeModel codeModel -> methodBuilder.withCode(codeBuilder -> copyCode(codeBuilder, mgen.getInstructionList())); - // copy all other MethodElements to output class (unchanged) + // Copy all other MethodElements to output class unchanged. default -> methodBuilder.with(me); } } @@ -695,12 +715,12 @@ private void copyCode(CodeBuilder codeBuilder, List instructions) { } /** - * Instrument the given method using {@link #instrumentCode}. + * Instrument a method using {@link #instrumentCode}. * * @param methodBuilder for the given method * @param methodModel for the given method * @param mgen describes the given method - * @param curMethodInfo provides additional information about the method + * @param curMethodInfo additional information about the method * @param method_info_index the index of the method in SharedData.methods */ private void instrumentMethod( @@ -718,65 +738,18 @@ private void instrumentMethod( codeBuilder -> instrumentCode(codeBuilder, codeModel, mgen, curMethodInfo, method_info_index)); - // copy all other MethodElements to output class (unchanged) + // Copy all other MethodElements to output class unchanged. default -> methodBuilder.with(me); } } } /** - * Insert the our instrumentation code into the instruction list for the given method. This - * includes adding instrumentation code at the entry and at each return from the method. In - * addition, it changes each return statement to first place the value being returned into a local - * and then return. - * - * @param instructions instruction list for method - * @param mgen describes the given method - * @param curMethodInfo provides additional information about the method - * @param minfo for the given method's code - */ - private void insertInstrumentationCode( - List instructions, - MethodGen24 mgen, - MethodInfo curMethodInfo, - MethodGen24.MInfo24 minfo) { - - // Add nonce local to matchup enter/exits - addInstrumentationAtEntry(instructions, mgen, minfo); - - // debugInstrument.log("Modified code: %s%n", mgen.getMethod().getCode()); - - Iterator shouldIncludeIter = curMethodInfo.is_included.iterator(); - Iterator exitLocationIter = curMethodInfo.exit_locations.iterator(); - - // instrument return instructions - ListIterator li = instructions.listIterator(); - while (li.hasNext()) { - - CodeElement inst = li.next(); - - // back up iterator to point to `inst` - li.previous(); - - // If this is a return instruction, insert method exit instrumentation - List new_il = - generate_return_instrumentation(inst, mgen, minfo, shouldIncludeIter, exitLocationIter); - - // insert code prior to `inst` - for (CodeElement ce : new_il) { - li.add(ce); - } - - // skip over `inst` we just inserted new_il in front of - li.next(); - } - } - - /** - * Generate instrumentation code for the given method. This includes reading in and processing the - * original instruction list, calling {@code insertInstrumentationCode} to add the instrumentation - * code, and then copying the modified instruction list to the output method while updating the - * code labels, if needed. + * Generate instrumentation code for the given method. The first step is to read and process the + * original instruction list to create the initial version of the new instruction list for the + * method (codeList). It then calls {@link #instrumentInstructionList} to add the instrumentation + * code to the codeList. Finally, it inserts this modified instruction list into the output method + * while updating the any code labels. * * @param codeBuilder for the given method's code * @param codeModel for the input method's code @@ -836,7 +809,7 @@ private void instrumentCode( } // Generate and insert our instrumentation code. - insertInstrumentationCode(codeList, mgen, curMethodInfo, minfo); + instrumentInstructionList(codeList, mgen, curMethodInfo, minfo); // Copy the modified local variable table to the output class. debugInstrument.log("LocalVariableTable:%n"); @@ -866,6 +839,168 @@ private void instrumentCode( } } + /** + * Insert our instrumentation code into the instruction list for the given method. This comprises + * adding instrumentation code at method entry and at each return from the method. + * + * @param instructions instruction list for method + * @param mgen describes the given method + * @param curMethodInfo provides additional information about the method + * @param minfo for the given method's code + */ + @EnsuresNonNull("#4.nonceLocal") + private void instrumentInstructionList( + List instructions, + MethodGen24 mgen, + MethodInfo curMethodInfo, + MethodGen24.MInfo24 minfo) { + + addInstrumentationAtEntry(instructions, mgen, minfo); + addInstrumentationAtExits(instructions, mgen, curMethodInfo, minfo); + } + + /** + * Inserts instrumentation code at the start of the method. This includes adding a local variable + * (this_invocation_nonce) that is initialized to Runtime.nonce++. This provides a unique id on + * each method entry/exit that allows them to be matched up from the dtrace file. Also inserts + * code to call daikon.chicory.Runtime.enter(). + * + * @param instructions instruction list for method + * @param mgen describes the given method + * @param minfo for the given method's code + */ + @EnsuresNonNull("#3.nonceLocal") + private void addInstrumentationAtEntry( + List instructions, MethodGen24 mgen, MethodGen24.MInfo24 minfo) { + + List newCode = generateIncrementNonce(mgen, minfo); + + callEnterOrExit(newCode, mgen, minfo, "enter", -1); + + // The start of the list of CodeElements looks as follows: + // LocalVariable declarations (if any) + // Label for start of code (if present) + // LineNumber for start of code (if present) + // + // + // We want to insert our instrumentation code after the LocalVariables (if any) and after the + // initial label (if present), but before any LineNumber or Instruction. + CodeElement inst = null; + try { + ListIterator li = instructions.listIterator(); + while (li.hasNext()) { + inst = li.next(); + if ((inst instanceof LineNumber) || (inst instanceof Instruction)) { + break; + } + } + + // Label for new location of start of original code. + debugInstrument.log("entryLabel: %s%n", minfo.entryLabel); + assert inst != null : "@AssumeAssertion(nullness): inst will always be set in loop above"; + minfo.labelMap.put(inst, minfo.entryLabel); + + // Insert code before this LineNumber or Instruction. + // Back up iterator to point to `inst`. + li.previous(); + for (CodeElement ce : newCode) { + li.add(ce); + } + } catch (Exception e) { + System.err.printf("Exception encountered: %s", e); + e.printStackTrace(); + } + } + + /** + * Generates code to initialize a new local variable (this_invocation_nonce) to Runtime.nonce++. + * + * @param mgen describes the given method + * @param minfo for the given method's code + */ + @EnsuresNonNull("#2.nonceLocal") + private List generateIncrementNonce(MethodGen24 mgen, MethodGen24.MInfo24 minfo) { + String atomic_int_classname = "java.util.concurrent.atomic.AtomicInteger"; + ClassDesc atomic_intClassDesc = ClassDesc.of(atomic_int_classname); + + List newCode = new ArrayList<>(); + + // Create the nonce local variable. + minfo.nonceLocal = createLocalWithMethodScope(mgen, minfo, "this_invocation_nonce", CD_int); + + // The following implements: + // this_invocation_nonce = Runtime.nonce++; + + // getstatic Runtime.nonce (load reference to AtomicInteger daikon.chicory.Runtime.nonce) + newCode.add( + FieldInstruction.of( + Opcode.GETSTATIC, + mgen.getPoolBuilder().fieldRefEntry(runtimeCD, "nonce", atomic_intClassDesc))); + + // Do an atomic get and increment of nonce value. + // This is multi-thread safe and leaves int value of nonce on stack. + MethodRefEntry mre = + mgen.getPoolBuilder() + .methodRefEntry(atomic_intClassDesc, "getAndIncrement", MethodTypeDesc.of(CD_int)); + newCode.add(InvokeInstruction.of(Opcode.INVOKEVIRTUAL, mre)); + + // store original value of nonce into this_invocation_nonce) + assert minfo.nonceLocal != null : "@AssumeAssertion(nullness): can't get here if null"; + newCode.add(StoreInstruction.of(TypeKind.INT, minfo.nonceLocal.slot())); + + return newCode; + } + + /** + * Inserts instrumentation code at each exit (return) from the method. For each return + * instruction, generate additional instructions to assign the method's result to a local variable + * (return__$trace2_val) and then call daikon.chicory.Runtime.exit(). + * + * @param instructions instruction list for method + * @param mgen describes the given method + * @param curMethodInfo provides additional information about the method + * @param minfo for the given method's code + */ + @RequiresNonNull("#4.nonceLocal") + private void addInstrumentationAtExits( + List instructions, + MethodGen24 mgen, + MethodInfo curMethodInfo, + MethodGen24.MInfo24 minfo) { + + // exit_location_is_included contains exactly one boolean per return instruction, + // exit_locations contains an integer only when that boolean is true. + assert curMethodInfo != null : "@AssumeAssertion(nullness): can't get here if null"; + Iterator shouldIncludeIter = curMethodInfo.exit_location_is_included.iterator(); + Iterator exitLocationIter = curMethodInfo.exit_locations.iterator(); + + // Instrument return instructions. + ListIterator li = instructions.listIterator(); + while (li.hasNext()) { + + CodeElement inst = li.next(); + + // back up iterator to point to `inst` + li.previous(); + + // If this is a return instruction, insert method exit instrumentation + List new_il = + generate_return_instrumentation(inst, mgen, minfo, shouldIncludeIter, exitLocationIter); + + // insert instrumentation code prior to `inst` + for (CodeElement ce : new_il) { + li.add(ce); + } + + // skip over `inst` we just inserted new_il in front of + li.next(); + } + + // Check for unused entries. + assert !shouldIncludeIter.hasNext(); + assert !exitLocationIter.hasNext(); + } + /** * If this is a return instruction, generate a new instruction list to assign the result to a * local variable (return__$trace2_val) and then call daikon.chicory.Runtime.exit(). This @@ -874,8 +1009,9 @@ private void instrumentCode( * @param inst the instruction to inspect, which might be a return instruction * @param mgen describes the given method * @param minfo for the given method's code - * @param shouldIncludeIter if true, instrument this return - * @param exitLocationIter list of exit line numbers + * @param shouldIncludeIter if shouldIncludeIter.next() is true, instrument this return + * @param exitLocationIter if we should instrument this return, exitLocationIter.next() is its + * line number * @return instruction list for instrumenting the return, or an empty list if {@code inst} is not * a return or the return should not be instrumented */ @@ -892,6 +1028,11 @@ private List generate_return_instrumentation( return Collections.emptyList(); } + // There is a single boolean element on shouldIncludeIter for every return in the method. Its + // value was calculated by {@link #shouldIgnore} and indicates whether or not that return should + // be instrumented. If the value is true the next exitLocationIter element contains the source + // line number for the return in question. + if (!shouldIncludeIter.hasNext()) { throw new RuntimeException("Not enough entries in shouldIncludeIter"); } @@ -902,16 +1043,12 @@ private List generate_return_instrumentation( return Collections.emptyList(); } - List newCode = new ArrayList<>(); + List newCode = new ArrayList<>(2); ClassDesc type = mgen.getReturnType(); if (!type.equals(CD_void)) { TypeKind typeKind = TypeKind.from(type); LocalVariable returnLocal = getReturnLocal(mgen, type, minfo); - if (typeKind.slotSize() == 1) { - newCode.add(StackInstruction.of(Opcode.DUP)); - } else { - newCode.add(StackInstruction.of(Opcode.DUP2)); - } + newCode.add(StackInstruction.of(typeKind.slotSize() == 1 ? Opcode.DUP : Opcode.DUP2)); newCode.add(StoreInstruction.of(typeKind, returnLocal.slot())); } @@ -927,9 +1064,9 @@ private List generate_return_instrumentation( * Returns the local variable used to store the return result. If it is not present, creates it * with the specified type. If the variable is known to already exist, the type can be null. * - * @param mgen describes the given method + * @param mgen describes a method * @param returnType the type of the return; may be null if the variable is known to already exist - * @param minfo for the given method's code + * @param minfo the method's code * @return a local variable to save the return value */ @SuppressWarnings("nullness") @@ -953,102 +1090,11 @@ private LocalVariable getReturnLocal( return minfo.returnLocal; } - /** - * Generates code to initialize a new local variable (this_invocation_nonce) to Runtime.nonce++. - * - * @param mgen describes the given method - * @param minfo for the given method's code - */ - @EnsuresNonNull("#2.nonceLocal") - private List generateIncrementNonce(MethodGen24 mgen, MethodGen24.MInfo24 minfo) { - String atomic_int_classname = "java.util.concurrent.atomic.AtomicInteger"; - ClassDesc atomic_intClassDesc = ClassDesc.of(atomic_int_classname); - - List newCode = new ArrayList<>(); - - // create the nonce local variable - minfo.nonceLocal = createLocalWithMethodScope(mgen, minfo, "this_invocation_nonce", CD_int); - - // The following implements: - // this_invocation_nonce = Runtime.nonce++; - - // getstatic Runtime.nonce (load reference to AtomicInteger daikon.chicory.Runtime.nonce) - newCode.add( - FieldInstruction.of( - Opcode.GETSTATIC, - mgen.getPoolBuilder().fieldRefEntry(runtimeCD, "nonce", atomic_intClassDesc))); - - // Do an atomic get and increment of nonce value. - // This is multi-thread safe and leaves int value of nonce on stack. - MethodRefEntry mre = - mgen.getPoolBuilder() - .methodRefEntry(atomic_intClassDesc, "getAndIncrement", MethodTypeDesc.of(CD_int)); - newCode.add(InvokeInstruction.of(Opcode.INVOKEVIRTUAL, mre)); - - // store original value of nonce into this_invocation_nonce) - newCode.add(StoreInstruction.of(TypeKind.INT, minfo.nonceLocal.slot())); - - return newCode; - } - - /** - * Inserts the given instrumentation code at the start of the method. This includes adding a local - * variable (this_invocation_nonce) that is initialized to Runtime.nonce++. This provides a unique - * id on each method entry/exit that allows them to be matched up from the dtrace file. Inserts - * code to call daikon.chicory.Runtime.enter(). - * - * @param instructions instruction list for method - * @param mgen describes the given method - * @param minfo for the given method's code - */ - @EnsuresNonNull("#3.nonceLocal") - private void addInstrumentationAtEntry( - List instructions, MethodGen24 mgen, MethodGen24.MInfo24 minfo) { - - List newCode = generateIncrementNonce(mgen, minfo); - - callEnterOrExit(newCode, mgen, minfo, "enter", -1); - - // The start of the list of CodeElements looks as follows: - // LocalVariable declarations (if any) - // Label for start of code (if present) - // LineNumber for start of code (if present) - // - // - // We want to insert our instrumentation code after the LocalVariables (if any) and after the - // initial label (if present), but before any LineNumber or Instruction. - CodeElement inst = null; - try { - ListIterator li = instructions.listIterator(); - while (li.hasNext()) { - inst = li.next(); - if ((inst instanceof LineNumber) || (inst instanceof Instruction)) { - break; - } - } - - // Label for new location of start of original code. - debugInstrument.log("entryLabel: %s%n", minfo.entryLabel); - assert inst != null : "@AssumeAssertion(nullness): inst will always be set in loop above"; - minfo.labelMap.put(inst, minfo.entryLabel); - - // Insert code before this LineNumber or Instruction. - // Back up iterator to point to `inst`. - li.previous(); - for (CodeElement ce : newCode) { - li.add(ce); - } - } catch (Exception e) { - System.err.printf("Unexpected exception encountered: %s", e); - e.printStackTrace(); - } - } - /** * Pushes the object, nonce, parameters, and return value on the stack and calls the specified - * method (normally enter or exit) in daikon.chicory.Runtime. The parameters are passed as an - * array of objects. Any primitive values are wrapped in the appropriate daikon.chicory.Runtime - * wrapper (IntWrap, FloatWrap, etc). + * method (either enter or exit) in daikon.chicory.Runtime. The parameters are passed as an array + * of objects. Any primitive values are wrapped in the appropriate daikon.chicory.Runtime wrapper + * (IntWrap, FloatWrap, etc). * * @param newCode an instruction list to append the enter/exit code to * @param mgen describes the method to be instrumented @@ -1067,16 +1113,19 @@ private void callEnterOrExit( ClassDesc[] paramTypes = mgen.getParameterTypes(); // aload - // Push the object. Push null if this is a static method or a constructor. + // Push the object. if (mgen.isStatic() || (methodToCall.equals("enter") && isConstructor(mgen))) { + // Push null if this is a static method or a constructor. newCode.add(ConstantInstruction.ofIntrinsic(Opcode.ACONST_NULL)); - } else { // must be an instance method + } else { + // Must be an instance method. newCode.add(LoadInstruction.of(TypeKind.REFERENCE, 0)); } // The offset of the first parameter. int param_offset = mgen.isStatic() ? 0 : 1; + // Assumes addInstrumentationAtEntry has been called to create the nonce local. // iload // Push the nonce. newCode.add(LoadInstruction.of(TypeKind.INT, minfo.nonceLocal.slot())); @@ -1122,50 +1171,33 @@ private void callEnterOrExit( } } - // push line number + // Push the line number. newCode.add(loadIntegerConstant(line, mgen)); } ClassDesc objectArrayCD = CD_Object.arrayType(1); - MethodTypeDesc methodArgs; + MethodTypeDesc methodParams; // Call the specified method. if (methodToCall.equals("exit")) { - methodArgs = + methodParams = MethodTypeDesc.of(CD_void, CD_Object, CD_int, CD_int, objectArrayCD, CD_Object, CD_int); } else { - methodArgs = MethodTypeDesc.of(CD_void, CD_Object, CD_int, CD_int, objectArrayCD); + methodParams = MethodTypeDesc.of(CD_void, CD_Object, CD_int, CD_int, objectArrayCD); } - MethodRefEntry mre = mgen.getPoolBuilder().methodRefEntry(runtimeCD, methodToCall, methodArgs); + MethodRefEntry mre = + mgen.getPoolBuilder().methodRefEntry(runtimeCD, methodToCall, methodParams); newCode.add(InvokeInstruction.of(Opcode.INVOKESTATIC, mre)); } - /** Variables used for processing a switch instruction. */ - private static class ModifiedSwitchInfo { - - /** Possibly modified default switch target. */ - Label modifiedTarget; - - /** Possibly modified switch case list. */ - List modifiedCaseList; - - /** - * Creates a ModifiedSwitchInfo. - * - * @param modifiedTarget possibly modified default switch target - * @param modifiedCaseList possibly modified switch case list - */ - ModifiedSwitchInfo(Label modifiedTarget, List modifiedCaseList) { - this.modifiedTarget = modifiedTarget; - this.modifiedCaseList = modifiedCaseList; - } - } + /** Used for processing a switch instruction. */ + private record ModifiedSwitchInfo(Label modifiedTarget, List modifiedCaseList) {} /** * Checks to see if the instruction targets the method's CodeModel startLabel (held in - * oldStartLabel). If so, it replaces the target with the entryLabel. Unfortunately, the classfile - * API does not allow us to simply replace the label, we have to replace the entire instruction. - * Note that oldStartLabel may be null, but that is okay as any comparison to it will fail and we - * will do nothing. + * minfo.oldStartLabel). If so, it replaces the target with the minfo.entryLabel. Unfortunately, + * the classfile API does not allow us to simply replace the label, we have to replace the entire + * instruction. Note that oldStartLabel may be null, but that is okay as any comparison to it will + * fail and we will do nothing. * * @param inst the instruction to check * @param minfo for the given method's code @@ -1204,7 +1236,7 @@ private CodeElement retargetStartLabel(CodeElement inst, MethodGen24.MInfo24 min /** * Checks to see if a switch instruction's default target or any of the case targets refers to - * {@code minfo.oldStartLabel}. If so, replace those targets with the entryLabel, and return the + * {@code minfo.oldStartLabel}. If so, replace those targets with minfo.entryLabel, and return the * result in a ModifiedSwitchInfo. Otherwise, return null. * * @param defaultTarget the default target for the switch instruction @@ -1332,7 +1364,7 @@ private boolean isConstructor(MethodGen24 mgen) { * @return the class name in ClassGetName format */ @SuppressWarnings("signature") // conversion method - public static @ClassGetName String typeToClassGetName(ClassDesc t) { + public static @ClassGetName String classDescToClassGetName(ClassDesc t) { String s = t.descriptorString(); if (s.startsWith("[")) { return s.replace('/', '.'); @@ -1342,33 +1374,28 @@ private boolean isConstructor(MethodGen24 mgen) { } /** - * Returns an array of strings, each corresponding to mgen's parameter types as a fully qualified - * name. + * Returns an array of fully qualified names, one for each of mgen's parameter types. * * @param mgen describes the given method * @return an array of strings, each corresponding to mgen's parameter types */ @SuppressWarnings("signature") // conversion method - private @BinaryName String[] getFullyQualifiedParameterTypes(MethodGen24 mgen) { - - ClassDesc[] paramTypes = mgen.getParameterTypes(); - @BinaryName String[] result = new @BinaryName String[paramTypes.length]; - - for (int i = 0; i < paramTypes.length; i++) { - result[i] = convertDescriptorToFqBinaryName(paramTypes[i].descriptorString()); - } - - return result; + private @BinaryName String[] getFqBinaryNameParameterTypes(MethodGen24 mgen) { + return ArraysPlume.mapArray( + paramType -> convertDescriptorToFqBinaryName(paramType.descriptorString()), + mgen.getParameterTypes(), + String.class); } /** - * Creates a MethodInfo struct corresponding to {@code mgen}. + * Creates a MethodInfo corresponding to {@code mgen}. * - * @param classInfo a class - * @param mgen a method in the given class + * @param classInfo class containing the method + * @param mgen method to inspect * @return a new MethodInfo for the method, or null if the method should not be instrumented */ - private @Nullable MethodInfo create_method_info(ClassInfo classInfo, MethodGen24 mgen) { + private @Nullable MethodInfo create_method_info_if_instrumented( + ClassInfo classInfo, MethodGen24 mgen) { // Get the parameter names for this method. String[] paramNames = mgen.getParameterNames(); @@ -1379,7 +1406,7 @@ private boolean isConstructor(MethodGen24 mgen) { } if (debugInstrument.enabled) { - debugInstrument.log("create_method_info for: %s%n", classInfo.class_name); + debugInstrument.log("create_method_info_if_instrumented for: %s%n", classInfo.class_name); debugInstrument.log("number of parameters: %s%n", paramNames.length); for (String paramName : paramNames) { debugInstrument.log("param name: %s%n", paramName); @@ -1396,7 +1423,7 @@ private boolean isConstructor(MethodGen24 mgen) { String arg0Name = convertDescriptorToFqBinaryName(arg0Fd); if (dollarPos >= 0 && - // type of first parameter is classname up to the "$" + // Type of first parameter is classname up to the "$". mgen.getClassName().substring(0, dollarPos).equals(arg0Name)) { // As a further check, for javac-generated classfiles, the // constant pool index #1 is "this$0", and the first 5 bytes of @@ -1417,7 +1444,7 @@ private boolean isConstructor(MethodGen24 mgen) { } if (debugInstrument.enabled) { - debugInstrument.log("create_method_info part 2%n"); + debugInstrument.log("create_method_info_if_instrumented part 2%n"); debugInstrument.log("number of parameters: %s%n", paramNames.length); for (String paramName : paramNames) { debugInstrument.log("param name: %s%n", paramName); @@ -1432,7 +1459,7 @@ private boolean isConstructor(MethodGen24 mgen) { mgen.getName(), DaikonWriter.methodEntryName( classInfo.class_name, - getFullyQualifiedParameterTypes(mgen), + getFqBinaryNameParameterTypes(mgen), // It looks like DaikonWriter.methodEntryName does not use the mgen.toString() argument. mgen.toString(), mgen.getName()))) { @@ -1442,11 +1469,11 @@ private boolean isConstructor(MethodGen24 mgen) { ClassDesc[] paramTypes = mgen.getParameterTypes(); @ClassGetName String[] param_type_strings = new @ClassGetName String[paramTypes.length]; for (int i = 0; i < paramTypes.length; i++) { - param_type_strings[i] = typeToClassGetName(paramTypes[i]); + param_type_strings[i] = classDescToClassGetName(paramTypes[i]); } if (debugInstrument.enabled) { - debugInstrument.log("create_method_info part 3%n"); + debugInstrument.log("create_method_info_if_instrumented part 3%n"); debugInstrument.log("number of parameters: %s%n", paramNames.length); for (int ii = 0; ii < paramTypes.length; ii++) { debugInstrument.log("param type: %s%n", param_type_strings[ii]); @@ -1454,7 +1481,7 @@ private boolean isConstructor(MethodGen24 mgen) { } // Loop through each instruction and find the line number for each return opcode. - List exit_locs = new ArrayList<>(); + List exit_line_numbers = new ArrayList<>(); // Tells whether each exit loc in the method is included or not (based on filters). List isIncluded = new ArrayList<>(); @@ -1462,7 +1489,7 @@ private boolean isConstructor(MethodGen24 mgen) { debugInstrument.log("Looking for exit points in %s%n", mgen.getName()); List il = mgen.getInstructionList(); int line_number = 0; - int last_line_number = 0; + int prev_line_number = 0; for (CodeElement inst : il) { boolean foundLine = false; @@ -1475,25 +1502,25 @@ private boolean isConstructor(MethodGen24 mgen) { if (inst instanceof ReturnInstruction) { debugInstrument.log("Exit at line %d%n", line_number); - // Only do incremental lines if we don't have the line generator. - if (line_number == last_line_number && foundLine == false) { + // Only do incremental lines if we haven't seen a line number since the last return. + if (line_number == prev_line_number && !foundLine) { debugInstrument.log("Could not find line %d%n", line_number); line_number++; } - last_line_number = line_number; + prev_line_number = line_number; if (!shouldIgnore( classInfo.class_name, mgen.getName(), DaikonWriter.methodExitName( classInfo.class_name, - getFullyQualifiedParameterTypes(mgen), + getFqBinaryNameParameterTypes(mgen), mgen.toString(), mgen.getName(), line_number))) { shouldInclude = true; - exit_locs.add(line_number); + exit_line_numbers.add(line_number); isIncluded.add(true); } else { @@ -1504,7 +1531,7 @@ private boolean isConstructor(MethodGen24 mgen) { if (shouldInclude) { return new MethodInfo( - classInfo, mgen.getName(), paramNames, param_type_strings, exit_locs, isIncluded); + classInfo, mgen.getName(), paramNames, param_type_strings, exit_line_numbers, isIncluded); } else { return null; } @@ -1518,7 +1545,7 @@ private boolean isConstructor(MethodGen24 mgen) { * @return true if the given class is part of Chicory itself */ @Pure - private static boolean isChicory(@InternalForm String classname) { + private static boolean isChicoryClass(@InternalForm String classname) { if (classname.startsWith("daikon/chicory/") && !classname.equals("daikon/chicory/ChicoryTest")) { @@ -1533,35 +1560,17 @@ private static boolean isChicory(@InternalForm String classname) { return false; } - // UNFINISHED and maybe unneeded - // // converts a method descriptor to a Java language string - // public static String convertDescriptorToFqBinaryName(String descriptor) { - // StringBuilder args = new StringBuilder("("); - // if (descriptor.charAt(0) != '(') { - // throw new IllegalArgumentException("Invalid method descriptor: " + descriptor); - // } - // int paren = descriptor.indexOf(')'); - // if (paren < 0) { - // throw new IllegalArgumentException("Invalid method descriptor: " + descriptor); - // } - // int end; - // int comma = descriptor.indexOf(','); - // if (comma < 0) { - // end = paren; - // } else { - // end = min(comma, paren); - // } - /** - * Format a field descriptor for output. The main difference between a descriptor and a signature - * is that the latter may contain type arguments. This routine was orginaly written for - * descriptors, but some support for type arguments has been added. + * Format a field descriptor for output. In addition, this method includes some support for + * signatures as well. Signatures are a superset of descriptors and may include type variables and + * parameterized types. However, this method does not support type variables and type arguments + * with wildcard bounds. * *

The output format is an extension of binary name format that includes primitives and arrays. - * It is almost identical to a fully qualified name, but using “$” instead of “.” to separate - * nested classes from their enclosing classes. + * It is the same as a fully qualified name, but using “$” instead of “.” to separate nested + * classes from their enclosing classes. * - * @param descriptor the object to format + * @param descriptor the descriptor to format * @return a @FqBinaryName formatted string */ @SuppressWarnings("signature") // conversion method @@ -1577,7 +1586,7 @@ private static boolean isChicory(@InternalForm String classname) { descriptor = descriptorFd; } - // Convert primitive types + // Convert primitive types. switch (descriptor.charAt(0)) { case 'B': result.append("byte"); @@ -1606,14 +1615,14 @@ private static boolean isChicory(@InternalForm String classname) { case 'V': result.append("void"); break; - case 'L': // Object type, starts with 'L' and ends with ';' - result.append(descriptorToFqBinaryName(descriptor)); + case 'L': // Class type, starts with 'L' and ends with ';' + result.append(convertClassTypeDescriptorToFqBinaryName(descriptor)); break; default: throw new IllegalArgumentException("Invalid descriptor: " + descriptor); } - // Append array brackets if applicable + // Append array brackets if applicable. for (int i = 0; i < arrayDimensions; i++) { result.append("[]"); } @@ -1624,25 +1633,25 @@ private static boolean isChicory(@InternalForm String classname) { /** * Format a class name that may contain type arguments. * - * @param descriptor the object to format + * @param descriptor the class descriptor to format * @return a @FqBinaryName formatted string */ @SuppressWarnings("signature") // conversion method - private static @FqBinaryName String descriptorToFqBinaryName(String descriptor) { + private static @FqBinaryName String convertClassTypeDescriptorToFqBinaryName(String descriptor) { StringBuilder result = new StringBuilder(); int genericStart = descriptor.indexOf('<'); int genericEnd = descriptor.lastIndexOf('>'); int endOfBaseType = descriptor.indexOf(';'); if (genericStart > 0 && genericEnd > genericStart) { - // Base type with generics + // Base type with generics. String baseType = descriptor.substring(1, genericStart).replace('/', '.'); result.append(baseType).append('<'); String genericPart = descriptor.substring(genericStart + 1, genericEnd); - result.append(typeArgumentsToBinaryNames(genericPart)); + result.append(convertTypeArgumentsToBinaryNames(genericPart)); result.append('>'); } else if (endOfBaseType > 0) { - // Regular object type + // Regular object type. result.append(descriptor.substring(1, endOfBaseType).replace('/', '.')); } else { throw new IllegalArgumentException("Malformed object type descriptor: " + descriptor); @@ -1657,7 +1666,12 @@ private static boolean isChicory(@InternalForm String classname) { * @return a string containing a list of types as binary names */ @SuppressWarnings("signature") // string manipulation - private static String typeArgumentsToBinaryNames(String genericPart) { + private static String convertTypeArgumentsToBinaryNames(String genericPart) { + + if (genericPart.equals("*")) { + return "?"; + } + StringBuilder result = new StringBuilder(); int depth = 0; StringBuilder current = new StringBuilder(); @@ -1689,12 +1703,13 @@ private static String typeArgumentsToBinaryNames(String genericPart) { } /** - * Format a constant value for printing. + * Convert the value of a constant static field to a string. {@link #instrumentClass} saves these + * values for use in the Chicory runtime. * * @param item the constant to format * @return a string containing the constant's value */ - private String formatConstantDesc(ConstantDesc item) { + private final String formatConstantDesc(ConstantDesc item) { try { return item.resolveConstantDesc(MethodHandles.lookup()).toString(); } catch (Exception e) { @@ -1719,6 +1734,7 @@ protected LocalVariable createLocalWithMethodScope( minfo.nextLocalIndex, localName, localType, minfo.startLabel, minfo.endLabel); mgen.localsTable.add(newVar); minfo.nextLocalIndex += TypeKind.from(localType).slotSize(); + mgen.setMaxLocals(minfo.nextLocalIndex); return newVar; } diff --git a/java/daikon/chicory/MethodGen24.java b/java/daikon/chicory/MethodGen24.java index cbbd70b09f..5fdcdc7d7a 100644 --- a/java/daikon/chicory/MethodGen24.java +++ b/java/daikon/chicory/MethodGen24.java @@ -1,5 +1,9 @@ package daikon.chicory; +import static java.lang.constant.ConstantDescs.CD_Object; +import static java.lang.constant.ConstantDescs.CD_String; +import static java.lang.constant.ConstantDescs.CD_void; + import java.lang.classfile.Attributes; import java.lang.classfile.ClassBuilder; import java.lang.classfile.ClassFile; @@ -9,6 +13,7 @@ import java.lang.classfile.Instruction; import java.lang.classfile.Label; import java.lang.classfile.MethodModel; +import java.lang.classfile.TypeKind; import java.lang.classfile.attribute.CodeAttribute; import java.lang.classfile.attribute.SignatureAttribute; import java.lang.classfile.constantpool.ConstantPoolBuilder; @@ -16,25 +21,29 @@ import java.lang.constant.ClassDesc; import java.lang.constant.MethodTypeDesc; import java.util.ArrayList; +import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Optional; +import org.checkerframework.checker.initialization.qual.UnderInitialization; import org.checkerframework.checker.lock.qual.GuardSatisfied; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; import org.checkerframework.checker.signature.qual.BinaryName; +import org.checkerframework.checker.signature.qual.FieldDescriptor; +import org.checkerframework.checker.signature.qual.FqBinaryName; import org.checkerframework.checker.signature.qual.Identifier; import org.checkerframework.checker.signature.qual.MethodDescriptor; /** - * MethodGen24 collects and stores all the relevant information about a method that Instrument24 - * might need. MethodGen24 is analogous to the BCEL MethodGen class. The similarity makes it easier - * to keep Instrument.java and Instrument24.java in sync. + * MethodGen24 represents a method. MethodGen24 is analogous to the BCEL MethodGen class. The + * similarity makes it easier to keep Instrument.java and Instrument24.java in sync. * - *

MethodGen24 uses Java's ({@code java.lang.classfile}) APIs for reading and modifying .class + *

MethodGen24 uses Java's {@code java.lang.classfile} APIs for reading and modifying .class * files. Those APIs were added in JDK 24. Compared to BCEL, these APIs are more complete and robust * (no more fiddling with StackMaps) and are always up to date with any .class file changes (since * they are part of the JDK). (We will need to continue to support Instrument.java using BCEL, as we @@ -43,34 +52,43 @@ public class MethodGen24 { /** - * Models the body of the method (the Code attribute). A Code attribute is viewed as a sequence of - * CodeElements, which is the only way to access Instructions; the order of elements of a code - * model is significant. May be null if the method has no code. + * Per the CodeModel Javadoc: "Models the body of the method (the Code attribute). A Code + * attribute is viewed as a sequence of CodeElements, which is the only way to access + * Instructions; the order of elements of a code model is significant." May be null if the method + * has no code. * - *

Several fields of CodeModel are declared as fields of MethodGen24 to better correspond to - * BCEL's version of MethodGen and to reduce re-computation. Currently we set these fields in the - * constructor, but they could be calculated lazily on first reference. + *

Several fields of MethodModel are cached as fields of MethodGen24 to better correspond to + * BCEL's version of MethodGen and to reduce re-computation. */ + + // + // Start of MethodModel items. + // + private @Nullable CodeModel code; - /** The method's access flags as a bit mask. */ + /** This method's access flags as a bit mask. */ private final int accessFlagsMask; - /** The method's name. */ + /** This method's name. */ private @Identifier String methodName; /** - * The method's type descriptor. This contains information about the parameters and return type of - * the method. + * This method's type descriptor. This contains information about the parameters and return type + * of this method. */ private MethodTypeDesc mtd; - /** True if the method is static. */ + // + // End of MethodModel items. + // + + /** True if this method is static. */ private boolean isStatic; /** - * The method's CodeAttribute. This contains information about the bytecodes (instructions) of - * this method. May be null if the method has no code. + * The method's CodeAttribute. This contains the bytecodes (instructions) of the method as well as + * additional information about the bytecodes. May be null if the method has no code. * *

Several fields of CodeAttribute are declared as fields of MethodGen24 to better model BCEL's * version of MethodGen and to reduce re-computation. Note that we set these fields in the @@ -78,12 +96,16 @@ public class MethodGen24 { */ private @Nullable CodeAttribute codeAttribute; - /** The method's maximum number of locals. */ + // Start of CodeAttribute items. + + /** The method's maximum number of local slots. */ private int maxLocals; /** The method's maximum stack size. */ private int maxStack; + // End of CodeAttribute items. + /** The name of the method's enclosing class, in binary name format. */ private @BinaryName String className; @@ -104,13 +126,13 @@ public class MethodGen24 { /** * The method's signature. This is a String that encodes type information about a (possibly * generic) method declaration. It describes any type parameters of the method; the (possibly - * parameterized) types of any formal parameters; the (possibly parameterized) return type, if + * parameterized) types of any formal parameters; and the (possibly parameterized) return type, if * any. It is not a true method signature as documented in the Java Virtual Machine Specification * as it does not include the types of any exceptions declared in the method's throws clause. */ private @MethodDescriptor String signature; - // Information extracted from {@code mtd}, the MethodTypeDescriptor. + // The next two items are extracted from {@link mtd}, the MethodTypeDescriptor. /** The method's parameter types. */ private ClassDesc[] paramTypes; @@ -127,19 +149,22 @@ public class MethodGen24 { * The method's local variable table. Often modified by clients, normally to add additional local * variables needed for instrumentation. */ - protected List localsTable; + public List localsTable; /** ConstantPool builder for entire class. */ // TODO: Should uses of this be synchronized? private ConstantPoolBuilder poolBuilder; - /** Variables used for processing the current method. */ - protected static class MInfo24 { + /** Information about the current method. */ + public static class MInfo24 { /** The index of this method in SharedData.methods. */ public final int method_info_index; - /** Next available slot in localsTable, currently always = max locals. */ + /** + * Next available slot in localsTable, user is expected to maintain and update maxLocals as + * needed. + */ public int nextLocalIndex; /** @@ -153,10 +178,13 @@ protected static class MInfo24 { /** Label for first byte code of method, used to give new locals method scope. */ public final Label startLabel; - /** Label for last byte code of method, used to give new locals method scope. */ + /** Label for last byte code of method. Used when creating a new method-scope local variable. */ public final Label endLabel; - /** Label for start of original code, post insertion of entry instrumentation. */ + /** + * Label for start of original code, post insertion of entry instrumentation. Used when creating + * a new method-scope local variable. + */ public Label entryLabel; /** @@ -175,7 +203,7 @@ protected static class MInfo24 { * Creates a MInfo24. * * @param method_info_index the index of the method in SharedData.methods - * @param nextLocalIndex next available slot in localsTable, currently always = max locals + * @param nextLocalIndex next available slot in localsTable, user should set to maxLocals * @param codeBuilder a CodeBuilder */ public MInfo24(int method_info_index, int nextLocalIndex, CodeBuilder codeBuilder) { @@ -221,7 +249,7 @@ public MethodGen24( this.codeList = cl; } else { this.code = null; - this.codeList = new ArrayList<>(); + this.codeList = Collections.emptyList(); } Optional ca = methodModel.findAttribute(Attributes.code()); @@ -239,7 +267,6 @@ public MethodGen24( Optional sa = methodModel.findAttribute(Attributes.signature()); if (sa.isPresent()) { - @SuppressWarnings("signature") // JDK 24 is not annotated as yet @MethodDescriptor String signature1 = sa.get().signature().stringValue(); signature = signature1; } else { @@ -247,10 +274,6 @@ public MethodGen24( signature = descriptor; } - mtd = methodModel.methodTypeSymbol(); - paramTypes = mtd.parameterArray(); - returnType = mtd.returnType(); - // Set up the localsTable. localsTable = new ArrayList<>(); @@ -269,20 +292,237 @@ public MethodGen24( localsTable.sort(Comparator.comparing(LocalVariable::slot)); origLocalVariables = localsTable.toArray(new LocalVariable[localsTable.size()]); - // System.out.println("locals:" + Arrays.toString(origLocalVariables)); - // System.out.println("types:" + Arrays.toString(paramTypes)); - // System.out.println("length: " + paramTypes.length + ", offset: " + offset); + poolBuilder = classBuilder.constantPool(); + + // Set up the parameter types and names. + mtd = methodModel.methodTypeSymbol(); + returnType = mtd.returnType(); + paramTypes = getParamTypes(); + paramNames = getParamNames(); + } + + /** + * Creates a MethodGen24 object for a new method created by DCInstrument24. + * + * @param className the containing class, in binary name format + * @param classBuilder for the class + * @param methodName for the method + * @param accessFlagsMask for the method + * @param mtd MethodTypeDescriptor for the method + * @param instructions instruction list for the method + * @param maxStack for the method + * @param maxLocals for the method + */ + public MethodGen24( + final @BinaryName String className, + ClassBuilder classBuilder, + final @Identifier String methodName, + final int accessFlagsMask, + final MethodTypeDesc mtd, + List instructions, + int maxStack, + int maxLocals) { + + this.className = className; + this.accessFlagsMask = accessFlagsMask; + this.methodName = methodName; + this.mtd = mtd; + @MethodDescriptor String descriptor1 = mtd.descriptorString(); + descriptor = descriptor1; + signature = descriptor; + code = null; + codeList = instructions; + codeAttribute = null; + this.maxStack = maxStack; + this.maxLocals = maxLocals; + isStatic = (accessFlagsMask & ClassFile.ACC_STATIC) != 0; + + // Create an empty localsTable. This will be filled in when InstrumentCode calls fixLocals. + localsTable = new ArrayList<>(); + origLocalVariables = localsTable.toArray(new LocalVariable[localsTable.size()]); - paramNames = new String[paramTypes.length]; - int offset = isStatic ? 0 : 1; - for (int i = 0; i < paramTypes.length; i++) { - if ((offset + i) < origLocalVariables.length) { - @Identifier String paramName = origLocalVariables[offset + i].name().stringValue(); - paramNames[i] = paramName; + poolBuilder = classBuilder.constantPool(); + + returnType = mtd.returnType(); + paramTypes = getParamTypes(); + paramNames = getParamNames(); + } + + /** + * Returns the value for the {@link #paramTypes} field. Intended to be called only once, from the + * constructor. + * + * @return the value for the {@link #paramTypes} field + */ + @RequiresNonNull({"mtd", "methodName", "origLocalVariables", "className"}) + private ClassDesc[] getParamTypes(@UnderInitialization(Object.class) MethodGen24 this) { + ClassDesc[] result = mtd.parameterArray(); + + // java.lang.classfile seems to be inconsistent with the parameter types + // of an inner class constructor. It may optimize away the hidden 'this$0' + // parameter, but it does not remove the corresponding entry from the + // parameterArray(). In order to correctly derive the names of the + // parameters I need to detect this special case and remove the + // incorrect entry from the parameterArray(). This check is ugly. + if (methodName.equals("") && result.length > 0 && origLocalVariables.length > 1) { + int dollarPos = className.lastIndexOf("$"); + @FieldDescriptor String arg0Fd = result[0].descriptorString(); + String arg0Type = Instrument24.convertDescriptorToFqBinaryName(arg0Fd); + // Note for tests below: first local will always be 'this'. + if (dollarPos >= 0 + && + // see if type of first parameter is classname up to the "$" + className.substring(0, dollarPos).equals(arg0Type) + && + // don't change if second local is in slot 2 + !(origLocalVariables[1].slot() == 2) + && + // don't change if type of first param matches type of second local + !origLocalVariables[1].typeSymbol().equals(result[0]) + && + // don't change result if 'this$0' is present + !origLocalVariables[1].name().stringValue().equals("this$0")) { + // some need some don't. what is difference? + // remove first param type so consistent with other methods + ClassDesc[] newArray = new ClassDesc[result.length - 1]; + System.arraycopy(result, 1, newArray, 0, result.length - 1); + result = newArray; } } - poolBuilder = classBuilder.constantPool(); + return result; + } + + /** + * Returns the value for the {@link #paramNames} field. Intended to be called only once, from the + * constructor. + * + * @return the value for the {@link #paramNames} field + */ + @RequiresNonNull({"mtd", "paramTypes", "origLocalVariables"}) + private @Identifier String[] getParamNames(@UnderInitialization(Object.class) MethodGen24 this) { + + // These initial values for {@code paramNames} may be incorrect. They could + // be altered in {@code fixLocals}. + @Identifier String[] result = new String[paramTypes.length]; + + int pIndex = 0; + int lIndex = isStatic ? 0 : 1; + int slot = isStatic ? 0 : 1; + int lLen = origLocalVariables.length; // we may add dummy param names + + while (pIndex < paramTypes.length) { + if ((lIndex >= origLocalVariables.length) + || (pIndex >= lLen) + || (origLocalVariables[lIndex].slot() != slot)) { + result[pIndex] = "param" + slot; + } else { + // UNDONE: should we assert type of paramTypes[pIndex] == type of + // origLocalVariables[lindex]? + @Identifier String paramName = origLocalVariables[lIndex].name().stringValue(); + result[pIndex] = paramName; + lIndex++; + lLen++; // pretend there is one more local + } + slot += TypeKind.from(paramTypes[pIndex]).slotSize(); + pIndex++; + } + + return result; + } + + /** + * Due to Java compiler optimizations, unused parameters may not be included in the local + * variables. Some of DynComp's instrumentation requires their presence. This routine makes these + * changes, if necessary: + * + *

+ * + * @param minfo MInfo24 object for current method + * @return true if modified localsTable, false otherwise + */ + public boolean fixLocals(MInfo24 minfo) { + boolean modified = false; + // If this is a native method the + // localsTable may not exist. We may need to add a 'this' pointer. + if ((localsTable.size() == 0) && !isStatic) { + ClassDesc thisDesc = ClassDesc.of(className); + LocalVariable newVar = + LocalVariable.of(0, "this", thisDesc, minfo.startLabel, minfo.endLabel); + localsTable.add(newVar); + modified = true; + } + + int lBase = isStatic ? 0 : 1; + int slot = isStatic ? 0 : 1; + int pLen = paramTypes.length; // we may add dummy params + int lIndex = lBase; + for (int pIndex = 0; pIndex < pLen; pIndex++) { + if (lIndex >= origLocalVariables.length) { + // more parameters than locals; need to add a LocalVariable for this parameter + LocalVariable newVar = + LocalVariable.of( + slot, paramNames[pIndex], paramTypes[pIndex], minfo.startLabel, minfo.endLabel); + localsTable.add(newVar); + modified = true; + } else if (slot != origLocalVariables[lIndex].slot()) { + // we have an alignment gap or missing local + if ((TypeKind.from(paramTypes[pIndex]).slotSize() == 2) + && paramTypes[pIndex].equals(origLocalVariables[lIndex].typeSymbol())) { + // has to have length 2 otherwise slot would be equal + // we have an alignment gap + // insert a dummy entry into the param tables + int newLen = paramTypes.length + 1; + ClassDesc[] newTypes = new ClassDesc[newLen]; + @Identifier String[] newNames = new String[newLen]; + System.arraycopy(paramTypes, 0, newTypes, 0, pIndex); + System.arraycopy(paramNames, 0, newNames, 0, pIndex); + newTypes[pIndex] = CD_Object; // good as any for dummy + newNames[pIndex] = "align" + pIndex; + System.arraycopy(paramTypes, pIndex, newTypes, pIndex + 1, paramTypes.length - pIndex); + System.arraycopy(paramNames, pIndex, newNames, pIndex + 1, paramTypes.length - pIndex); + paramTypes = newTypes; + paramNames = newNames; + pLen++; // we've added a new param; will create corresponding local below + } + // create an alignment temp or a local that compiler has optimized out + LocalVariable newVar = + LocalVariable.of( + slot, paramNames[pIndex], paramTypes[pIndex], minfo.startLabel, minfo.endLabel); + localsTable.add(newVar); + lIndex--; // we've added an alignment local or missing local, we need to visit current local + // again + modified = true; + } + slot += TypeKind.from(paramTypes[pIndex]).slotSize(); + lIndex++; + } + + // UNDONE: do we need to check for alignment gaps in locals after the parameters? + + // If we added locals, then table is no longer sorted by slot. + if (modified) { + localsTable.sort(Comparator.comparing(LocalVariable::slot)); + } + + // Now that we have updated and/or corrected the locals table, the paramNames + // may need to be updated. + for (int pIndex = 0; pIndex < paramTypes.length; pIndex++) { + lIndex = lBase + pIndex; + if (lIndex < localsTable.size()) { + @Identifier String localName = localsTable.get(lIndex).name().stringValue(); + if (!paramNames[pIndex].equals(localName)) { + paramNames[pIndex] = localName; + modified = true; + } + } + } + + return modified; } /** @@ -294,6 +534,38 @@ public int getAccessFlagsMask() { return accessFlagsMask; } + /** + * Returns true if the method is a constructor. + * + * @return true iff the method is a constructor + */ + public final boolean isConstructor() { + return methodName.equals(""); + } + + /** + * Returns true if the method is a class initializer. + * + * @return true iff the method is a class initializer + */ + public final boolean isClinit() { + return methodName.equals(""); + } + + /** + * Returns true if this is a standard main method (static, void, name is "main", and one formal + * parameter: a string array). + * + * @return true iff the method is a main method + */ + public final boolean isMain() { + return isStatic + && returnType.equals(CD_void) + && methodName.equals("main") + && (paramTypes.length == 1) + && paramTypes[0].equals(CD_String.arrayType(1)); + } + /** * Returns true if the method is static. * @@ -340,6 +612,16 @@ public ClassDesc getReturnType() { return paramNames.clone(); } + /** + * Set the parameter names. The user must ensure that the length of paramNames equals the length + * of paramTypes. + * + * @param paramNames the new paramNames array + */ + public void setParameterNames(final @Identifier String[] paramNames) { + this.paramNames = paramNames; + } + /** * Returns the type of the ith parameter. * @@ -359,6 +641,16 @@ public ClassDesc[] getParameterTypes() { return paramTypes.clone(); } + /** + * Set the parameter types. The user must ensure that the length of paramNames equals the length + * of paramTypes. + * + * @param paramTypes the new paramTypes array + */ + public void setParameterTypes(final ClassDesc[] paramTypes) { + this.paramTypes = paramTypes; + } + /** * Returns the original local variable table. In most cases, instrumentation code should use the * {@code localsTable} instead. @@ -369,6 +661,16 @@ public LocalVariable[] getOriginalLocalVariables() { return origLocalVariables.clone(); } + /** + * Set the original local variable table. This method is only used when DCInstrument24 creates a + * new user method. + * + * @param locals the new original local variable table + */ + public void setOriginalLocalVariables(LocalVariable[] locals) { + origLocalVariables = locals.clone(); + } + /** * Returns the name of the containing class. * @@ -397,6 +699,15 @@ public int getMaxLocals() { return maxLocals; } + /** + * Set the maximum number of locals. + * + * @param size the maximum number of locals + */ + public void setMaxLocals(int size) { + maxLocals = size; + } + /** * Returns the maximum stack size. * @@ -419,7 +730,7 @@ public int getMaxStack() { /** * Returns the signature for the current method. This is a String that encodes type information * about a (possibly generic) method declaration. It describes any type parameters of the method; - * the (possibly parameterized) types of any formal parameters; the (possibly parameterized) + * the (possibly parameterized) types of any formal parameters; and the (possibly parameterized) * return type, if any. * * @return signature for the current method @@ -446,37 +757,90 @@ public ConstantPoolBuilder getPoolBuilder() { return poolBuilder; } - // Not sure we need this - // public void setInstructionList(List il) { - // codeList = il; - // } + /** + * Set the method's instruction list. Used to add instruction list to our native code wrapper. + * + * @param il the instruction list + */ + public void setInstructionList(List il) { + codeList = il; + } - // need to fancy up! + /** + * Returns string representation close to declaration format, 'public static void main(String[])', + * e.g. + * + * @return String representation of the method declaration. + */ @Override public final String toString(@GuardSatisfied MethodGen24 this) { - return methodName; + StringBuilder result = new StringBuilder(getAccess(accessFlagsMask)); + result.append(returnType.equals(CD_void) ? "void" : convertClassDesc(returnType)); + result.append(" " + methodName + "("); + if (paramTypes.length > 0) { + for (int i = 0; i < paramTypes.length; i++) { + String paramType = convertClassDesc(paramTypes[i]); + if ((accessFlagsMask & ClassFile.ACC_VARARGS) != 0 && (i == (paramTypes.length - 1))) { + paramType = paramType.replace("[]", "..."); + } + result.append(paramType + " " + paramNames[i] + ", "); + } + result.setLength(result.length() - 2); // remove last ", " + } + result.append(")"); + return result.toString(); } - /* - @Override - public final String toString() { - final String access = Utility.accessToString(super.getAccessFlags()); - String signature = Type.getMethodSignature(super.getType(), paramTypes); - signature = Utility.methodSignatureToString(signature, super.getName(), access, true, getLocalVariableTable(super.getConstantPool())); - final StringBuilder buf = new StringBuilder(signature); - for (final Attribute a : getAttributes()) { - if (!(a instanceof Code || a instanceof ExceptionTable)) { - buf.append(" [").append(a).append("]"); - } - } - - if (!throwsList.isEmpty()) { - for (final String throwsDescriptor : throwsList) { - buf.append("\n\t\tthrows ").append(throwsDescriptor); - } - } - return buf.toString(); - } - */ + /** + * Returns a string representation of a ClassDesc. It is a fully-qualified binary name, except + * that any leading "java.lang." is removed. + * + * @param type the ClassDesc to translate + * @return the class name, without + */ + private @FqBinaryName String convertClassDesc(@GuardSatisfied MethodGen24 this, ClassDesc type) { + @FieldDescriptor String arg0Fd = type.descriptorString(); + String result = daikon.chicory.Instrument24.convertDescriptorToFqBinaryName(arg0Fd); + if (result.startsWith("java.lang.")) { + @SuppressWarnings("signature:assignment") // string manipulation + @FqBinaryName String truncated = result.replace("java.lang.", ""); + return truncated; + } + return result; + } + /** + * Returns a string representation of a method's access flags. + * + * @param accessFlagsMask some access flags. + * @return a space-separated string of access modifier keywords + */ + private String getAccess(@GuardSatisfied MethodGen24 this, final int accessFlagsMask) { + StringBuilder result = new StringBuilder(); + if ((accessFlagsMask & ClassFile.ACC_PUBLIC) != 0) { + result.append("public "); + } + if ((accessFlagsMask & ClassFile.ACC_PRIVATE) != 0) { + result.append("private "); + } + if ((accessFlagsMask & ClassFile.ACC_PROTECTED) != 0) { + result.append("protected "); + } + if ((accessFlagsMask & ClassFile.ACC_ABSTRACT) != 0) { + result.append("abstract "); + } + if ((accessFlagsMask & ClassFile.ACC_STATIC) != 0) { + result.append("static "); + } + if ((accessFlagsMask & ClassFile.ACC_FINAL) != 0) { + result.append("final "); + } + if ((accessFlagsMask & ClassFile.ACC_SYNCHRONIZED) != 0) { + result.append("synchronized "); + } + if ((accessFlagsMask & ClassFile.ACC_NATIVE) != 0) { + result.append("native "); + } + return result.toString(); + } } diff --git a/java/daikon/chicory/MethodInfo.java b/java/daikon/chicory/MethodInfo.java index 3cf7a6f003..db50acae3d 100644 --- a/java/daikon/chicory/MethodInfo.java +++ b/java/daikon/chicory/MethodInfo.java @@ -49,11 +49,27 @@ public class MethodInfo { /** Array of argument types as classes for this method. */ public Class[] arg_types; - /** Exit locations for this method. */ + /** + * Exit locations for this method. + * + *

Chicory and DynComp treat this field differently. Chicory only adds an exit location if the + * corresponding entry in exit_location_is_included is true. (Based on the ppt omit/select + * patterns.) DynComp adds all exit locations and sets every exit_location_is_included value to + * true. Thus: + * + *

    + *
  • there is always an exit_location_is_included boolean for every return instruction + *
  • exit_locations contains only those exits whose corresponding boolean is true + *
+ */ public List exit_locations; - /** Tells whether each exit point in method is instrumented, based on filters. */ - public List is_included; + /** + * Tells whether each exit point in the method is instrumented, based on filters. Note: that + * exit_locations and exit_location_is_included are not index-aligned; exit_locations is a + * filtered subset. + */ + public List exit_location_is_included; /** * The root of the variable tree for the method entry program point. @@ -89,14 +105,14 @@ public MethodInfo( String[] arg_names, @ClassGetName String[] arg_type_strings, List exit_locations, - List is_included) { + List exit_location_is_included) { this.class_info = class_info; this.method_name = method_name; this.arg_names = arg_names; this.arg_type_strings = arg_type_strings; this.exit_locations = exit_locations; - this.is_included = is_included; + this.exit_location_is_included = exit_location_is_included; } // Use reserved keyword for basic type rather than signature to diff --git a/java/daikon/chicory/Runtime.java b/java/daikon/chicory/Runtime.java index 6f01624f15..90cc301241 100644 --- a/java/daikon/chicory/Runtime.java +++ b/java/daikon/chicory/Runtime.java @@ -41,6 +41,7 @@ import org.checkerframework.checker.signature.qual.ClassGetName; import org.checkerframework.checker.signature.qual.FieldDescriptor; import org.checkerframework.checker.signature.qual.FqBinaryName; +import org.checkerframework.checker.signature.qual.InternalForm; import org.checkerframework.dataflow.qual.SideEffectFree; /** @@ -535,12 +536,12 @@ public static void setDtraceOnlineMode(int port) { // System.out.println("Attempting to connect to Daikon on port --- " + port); daikonSocket.connect(new InetSocketAddress(InetAddress.getLocalHost(), port), 5000); } catch (UnknownHostException e) { - System.out.println( + System.err.println( "UnknownHostException connecting to Daikon : " + e.getMessage() + ". Exiting"); System.exit(1); throw new Error("Unreachable control flow"); } catch (IOException e) { - System.out.println( + System.err.println( "IOException, could not connect to Daikon : " + e.getMessage() + ". Exiting"); System.exit(1); throw new Error("Unreachable control flow"); @@ -551,7 +552,7 @@ public static void setDtraceOnlineMode(int port) { new PrintWriter( new BufferedWriter(new OutputStreamWriter(daikonSocket.getOutputStream(), UTF_8))); } catch (IOException e) { - System.out.println("IOException connecting to Daikon : " + e.getMessage() + ". Exiting"); + System.err.println("IOException connecting to Daikon : " + e.getMessage() + ". Exiting"); System.exit(1); } @@ -1082,6 +1083,9 @@ public static String escapeJava(String orig) { // From class SignaturesUtil // + // Eventually the code should use the library rather than copying its code. + // (As of 2026-01-10, I'm having trouble with the shadowJar plugin.) + /** A map from field descriptor (sach as "I") to Java primitive type (such as "int"). */ private static HashMap fieldDescriptorToPrimitive = new HashMap<>(8); @@ -1171,6 +1175,26 @@ public static String escapeJava(String orig) { } } + /** + * Given a class name in internal form, return it as a binary name. + * + * @param internalForm a class name in internal form + * @return the class name as a binary name + */ + public static @BinaryName String internalFormToBinaryName(@InternalForm String internalForm) { + return internalForm.replace('/', '.'); + } + + /** + * Given a class name in binary name form, return it in internal form. + * + * @param binaryName a class name in binary name form + * @return the class name in internal form + */ + public static @InternalForm String binaryNameToInternalForm(@BinaryName String binaryName) { + return binaryName.replace('.', '/'); + } + // /////////////////////////////////////////////////////////////////////////// // end of copied code // diff --git a/java/daikon/config/ParameterDoclet.java11 b/java/daikon/config/ParameterDoclet.java11 index 34d75c1001..a87bcc4131 100644 --- a/java/daikon/config/ParameterDoclet.java11 +++ b/java/daikon/config/ParameterDoclet.java11 @@ -214,7 +214,7 @@ public class ParameterDoclet implements Doclet { } /** - * Return true if the given variable (that represents a configuration option) should be included + * Returns true if the given variable (that represents a configuration option) should be included * in this section. * * @param fullConfigName the fully-qualified name of a Daikon configuration variable (no diff --git a/java/daikon/config/ParameterDoclet.java8 b/java/daikon/config/ParameterDoclet.java8 index f9ef3bcff0..3f842fffb8 100644 --- a/java/daikon/config/ParameterDoclet.java8 +++ b/java/daikon/config/ParameterDoclet.java8 @@ -169,7 +169,7 @@ public class ParameterDoclet { } /** - * Return true if the given variable (that represents a configuration option) should be included + * Returns true if the given variable (that represents a configuration option) should be included * in this section. * * @param fullConfigName the fully-qualified name of a Daikon configuration variable (no diff --git a/java/daikon/dcomp/BuildJDK.java b/java/daikon/dcomp/BuildJDK.java index 4540d3c816..15f09865a1 100644 --- a/java/daikon/dcomp/BuildJDK.java +++ b/java/daikon/dcomp/BuildJDK.java @@ -41,21 +41,21 @@ import org.checkerframework.checker.signature.qual.BinaryName; /** - * BuildJDK uses {@link DCInstrument} to add comparability instrumentation to Java class files, then - * stores the modified files into a directory identified by a (required) command line argument. + * Add comparability instrumentation to Java class files, then stores the modified files into a + * directory identified by a (required) command line argument. * - *

DCInstrument duplicates each method of a class file. The new methods are distinguished by the - * addition of a final parameter of type DCompMarker and are instrumented to track comparability. - * Based on its invocation arguments, DynComp will decide whether to call the instrumented or - * uninstrumented version of a method. + *

Duplicates each method of a class file. The new methods are distinguished by the addition of a + * final parameter of type DCompMarker and are instrumented to track comparability. Based on its + * invocation arguments, DynComp will decide whether to call the instrumented or uninstrumented + * version of a method. */ @SuppressWarnings({ "mustcall:type.argument", "mustcall:type.arguments.not.inferred" }) // assignments into owning collection -public class BuildJDK { +public final class BuildJDK { - /** Creates a new BuildJDK. */ + /** Do not instantiate from external code; only instantiate in {@link #main}. */ private BuildJDK() {} /** @@ -75,7 +75,7 @@ private BuildJDK() {} /** * Collects names of all methods that DCInstrument could not process. Should be empty. Format is - * <fully-qualified class name>.<method name> + * {@code .}. */ private static List skipped_methods = new ArrayList<>(); @@ -129,8 +129,8 @@ public static void main(String[] args) throws IOException { //

We want to share code to read and instrument the Java class file members of a jar file // (JDK 8) or a module file (JDK 9+). However, jar files and module files are located in two // completely different file systems. So we open an InputStream for each class file we wish to - // instrument and save it in the class_stream_map with the file name as the key. From that point - // the code to instrument a class file can be shared. + // instrument and save it in the class_stream_map with the file name as the key. From that + // point the code to instrument a class file can be shared. Map class_stream_map; if (cl_args.length > 1) { @@ -247,6 +247,8 @@ Map gather_runtime_from_jar() { Map class_stream_map = new HashMap<>(); String jar_name = java_home + "/lib/rt.jar"; System.out.printf("using jar file %s%n", jar_name); + // We intentionally do not close the jar file as we save + // input streams into it to be read later. try { JarFile jfile = new JarFile(jar_name); // Get each class to be instrumented and store it away @@ -272,8 +274,8 @@ Map gather_runtime_from_jar() { /** * For Java 9+ the Java runtime is located in a series of modules. At this time, we are only * pre-instrumenting the java.base module. This method initializes the DirectoryStream used to - * explore java.base. It calls gather_runtime_from_modules_directory to process the directory - * structure. + * explore java.base. It calls {@link #gather_runtime_from_modules_directory} to process the + * directory structure. * * @return a map from class file name to the associated InputStream */ @@ -463,7 +465,7 @@ private void createDCompClass( } /** Formats just the time part of a DateTime. */ - private DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HH:mm:ss"); + private static final DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HH:mm:ss"); /** * Instruments the JavaClass {@code jc} (whose name is {@code classFileName}). Writes the @@ -471,7 +473,7 @@ private void createDCompClass( * * @param jc JavaClass to be instrumented * @param outputDir output directory for instrumented class - * @param classFileName name of class to be instrumented + * @param classFileName name of class to be instrumented (in internal form) * @param classTotal total number of classes to be processed; used for progress display * @throws IOException if unable to write out instrumented class */ @@ -484,7 +486,7 @@ private void instrumentClassFile( } DCInstrument dci = new DCInstrument(jc, true, null); JavaClass inst_jc; - inst_jc = dci.instrument_jdk(); + inst_jc = dci.instrument_jdk_class(); skipped_methods.addAll(dci.get_skipped_methods()); File classfile = new File(classFileName); File dir; diff --git a/java/daikon/dcomp/BuildJDK24.java b/java/daikon/dcomp/BuildJDK24.java new file mode 100644 index 0000000000..5a983cd978 --- /dev/null +++ b/java/daikon/dcomp/BuildJDK24.java @@ -0,0 +1,625 @@ +package daikon.dcomp; + +import static java.lang.constant.ConstantDescs.CD_Object; +import static java.lang.constant.ConstantDescs.CD_boolean; +import static java.nio.charset.StandardCharsets.UTF_8; + +import daikon.DynComp; +import daikon.chicory.ClassInfo; +import daikon.chicory.Runtime; +import daikon.plumelib.bcelutil.BcelUtil; +import daikon.plumelib.options.Options; +import daikon.plumelib.reflection.Signatures; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintWriter; +import java.io.UncheckedIOException; +import java.lang.classfile.ClassBuilder; +import java.lang.classfile.ClassFile; +import java.lang.classfile.ClassHierarchyResolver; +import java.lang.classfile.ClassModel; +import java.lang.classfile.attribute.SourceFileAttribute; +import java.lang.constant.ClassDesc; +import java.lang.constant.MethodTypeDesc; +import java.net.URI; +import java.nio.file.DirectoryStream; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.signature.qual.BinaryName; +import org.checkerframework.checker.signature.qual.DotSeparatedIdentifiers; +import org.checkerframework.checker.signature.qual.InternalForm; + +/** + * Add comparability instrumentation to Java class files, then stores the modified files into a + * directory identified by a (required) command line argument. + * + *

Duplicates each method of a class file. The new methods are distinguished by the addition of a + * final parameter of type DCompMarker and are instrumented to track comparability. Based on its + * invocation arguments, DynComp will decide whether to call the instrumented or uninstrumented + * version of a method. + */ +@SuppressWarnings({ + "mustcall:type.argument", + "mustcall:type.arguments.not.inferred" +}) // assignments into owning collection +public final class BuildJDK24 { + + /** Do not instantiate from external code; only instantiate in {@link #main}. */ + private BuildJDK24() {} + + /** + * The "java.home" system property. Note that there is also a JAVA_HOME variable that contains + * {@code System.getenv("JAVA_HOME")}. + */ + public static final String java_home = System.getProperty("java.home"); + + /** If true, print information about the classes being instrumented. */ + private static boolean verbose = false; + + /** Number of class files processed; used for progress display. */ + private int _numFilesProcessed = 0; + + /** Name of file in output jar containing the static-fields map. */ + private static String static_field_id_filename = "dcomp_jdk_static_field_id"; + + /** Allow BuildJDK24 to access outputDebugFiles. */ + @SuppressWarnings("nullness:initialization.static.field.uninitialized") // TODO + private static daikon.dcomp.Instrument24 inst24; + + /** + * Collects names of all methods that DCInstrument24 could not process. Should be empty. Format is + * {@code .}. + */ + private static List skipped_methods = new ArrayList<>(); + + /** + * A list of methods known to cause DCInstrument24 to fail. This is used to remove known problems + * from the list of failures displayed at the end of BuildJDK's execution. Format is + * <fully-qualified class name>.<method name> + */ + public static List known_uninstrumentable_methods = + Arrays.asList( + // None at present + ); + + /** + * Instruments each class file in the Java runtime and puts the result in the first non-option + * command-line argument. + * + *

By default, BuildJDK will locate the appropriate Java runtime library and instrument each of + * its member class files. However, if there are additional arguments on the command line after + * the destination directory, then we assume these are class files to be instrumented and the Java + * runtime library is not used. This usage is primarily for testing purposes. + * + * @param args arguments being passed to BuildJDK + * @throws IOException if unable to read or write file {@code dcomp_jdk_static_field_id} or if + * unable to write {@code jdk_classes.txt} + */ + @SuppressWarnings("builder:required.method.not.called") // assignment into collection of @Owning + public static void main(String[] args) throws IOException { + + System.out.println("BuildJDK24 starting at " + LocalDateTime.now(ZoneId.systemDefault())); + + BuildJDK24 build = new BuildJDK24(); + + Options options = + new Options( + "daikon.BuildJDK24 [options] dest_dir [classfiles...]", + DynComp.class, + DCInstrument24.class); + String[] cl_args = options.parse(true, args); + if (cl_args.length < 1) { + System.err.println("must specify destination dir"); + options.printUsage(); + System.exit(1); + } + verbose = DynComp.verbose; + inst24 = new daikon.dcomp.Instrument24(); + + File dest_dir = new File(cl_args[0]); + + // Key is a class file name, value is a stream that opens that file name. + // + //

We want to share code to read and instrument the Java class file members of a jar file + // (JDK 8) or a module file (JDK 9+). However, jar files and module files are located in two + // completely different file systems. So we open an InputStream for each class file we wish to + // instrument and save it in the class_stream_map with the file name as the key. From that + // point the code to instrument a class file can be shared. + Map class_stream_map; + + if (cl_args.length > 1) { + + // Arguments are [...] + @NonNull String[] class_files = Arrays.copyOfRange(cl_args, 1, cl_args.length); + + // Instrumenting a specific list of class files is usually used for testing. + // But if we're using it to fix a broken classfile, then we need + // to restore the static-fields map from when our runtime jar was originally + // built. We assume it is in the destination directory. + DCInstrument24.restore_static_field_id(new File(dest_dir, static_field_id_filename)); + System.out.printf( + "Restored %d entries in static map.%n", DCInstrument24.static_field_id.size()); + + class_stream_map = new HashMap<>(); + for (String classFileName : class_files) { + try { + class_stream_map.put(classFileName, new FileInputStream(classFileName)); + } catch (FileNotFoundException e) { + throw new Error("File not found: " + classFileName, e); + } + } + + // Instrument the classes identified in class_stream_map. + build.instrument_classes(dest_dir, class_stream_map); + + } else { + + check_java_home(); + + if (Runtime.isJava9orLater()) { + class_stream_map = build.gather_runtime_from_modules(); + } else { + class_stream_map = build.gather_runtime_from_jar(); + } + + // Instrument the Java runtime classes identified in class_stream_map. + build.instrument_classes(dest_dir, class_stream_map); + + // We've finished instrumenting all the class files. Now we create some + // abstract interface classes for use by the DynComp runtime. + build.addInterfaceClasses(dest_dir); + + // Write out the file containing the static-fields map. + System.out.printf("Found %d static fields.%n", DCInstrument24.static_field_id.size()); + DCInstrument24.save_static_field_id(new File(dest_dir, static_field_id_filename)); + + // Write out the list of all classes in the jar file + File jdk_classes_file = new File(dest_dir, "java/lang/jdk_classes.txt"); + System.out.printf("Writing a list of class names to %s%n", jdk_classes_file); + // Class names are written in internal form. + try (PrintWriter pw = new PrintWriter(jdk_classes_file, UTF_8.name())) { + for (String classFileName : class_stream_map.keySet()) { + pw.println(removeSuffix(classFileName, ".class")); + } + } + } + + // Print out any methods that could not be instrumented + print_skipped_methods(); + + System.out.println("BuildJDK24 done at " + LocalDateTime.now(ZoneId.systemDefault())); + } + + /** Verify that java.home and JAVA_HOME match. Exits the JVM if there is an error. */ + public static void check_java_home() { + + // We are going to instrument the default Java runtime library. + // We need to verify where we should look for it. + + String JAVA_HOME = System.getenv("JAVA_HOME"); + if (JAVA_HOME == null) { + if (verbose) { + System.out.println("JAVA_HOME not defined; using java.home: " + java_home); + } + JAVA_HOME = java_home; + } + + File jrt = new File(JAVA_HOME); + if (!jrt.exists()) { + System.err.printf("Java home directory %s does not exist.%n", jrt); + System.exit(1); + } + + try { + jrt = jrt.getCanonicalFile(); + } catch (Exception e) { + System.err.printf("Error getting canonical file for %s: %s%n", jrt, e.getMessage()); + System.exit(1); + } + + JAVA_HOME = jrt.getAbsolutePath(); + if (!java_home.startsWith(JAVA_HOME)) { + System.err.printf( + "JAVA_HOME (%s) does not agree with java.home (%s).%n", JAVA_HOME, java_home); + System.err.printf("Please correct your Java environment.%n"); + System.exit(1); + } + } + + /** + * For Java 8 the Java runtime is located in rt.jar. This method creates an InputStream for each + * class in rt.jar and returns this information in a map from class file name to InputStream. + * + * @return a map from class file name to the associated InputStream + */ + @SuppressWarnings({ + "JdkObsolete", // JarEntry.entries() returns Enumeration + "builder:required.method.not.called" // assignment into collection of @Owning + }) + Map gather_runtime_from_jar() { + + Map class_stream_map = new HashMap<>(); + String jar_name = java_home + "/lib/rt.jar"; + System.out.printf("using jar file %s%n", jar_name); + // We intentionally do not close the jar file as we save + // input streams into it to be read later. + try { + JarFile jfile = new JarFile(jar_name); + // Get each class to be instrumented and store it away + Enumeration entries = jfile.entries(); + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + // System.out.printf("processing entry %s%n", entry); + final String entryName = entry.getName(); + if (entryName.endsWith("/") || entryName.endsWith("~")) { + continue; + } + + // Get the InputStream for this file + InputStream is = jfile.getInputStream(entry); + class_stream_map.put(entryName, is); + } + } catch (Exception e) { + throw new Error("Problem while reading " + jar_name, e); + } + return class_stream_map; + } + + /** + * For Java 9+ the Java runtime is located in a series of modules. At this time, we are only + * pre-instrumenting the java.base module. This method initializes the DirectoryStream used to + * explore java.base. It calls {@link #gather_runtime_from_modules_directory} to process the + * directory structure. + * + * @return a map from class file name to the associated InputStream + */ + Map gather_runtime_from_modules() { + + Map class_stream_map = new HashMap<>(); + FileSystem fs = FileSystems.getFileSystem(URI.create("jrt:/")); + Path modules = fs.getPath("/modules"); + // The path java_home+/lib/modules is the file in the host file system that + // corresponds to the modules file in the jrt: file system. + System.out.printf("using modules directory %s/lib/modules%n", java_home); + try (DirectoryStream directoryStream = Files.newDirectoryStream(modules, "java.base*")) { + for (Path moduleDir : directoryStream) { + gather_runtime_from_modules_directory( + moduleDir, moduleDir.toString().length(), class_stream_map); + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + return class_stream_map; + } + + /** + * This is a helper method for {@link #gather_runtime_from_modules}. It recurses down the module + * directory tree, selects the classes we want to instrument, creates an InputStream for each of + * these classes, and adds this information to the {@code class_stream_map} argument. + * + * @param path module file, which might be subdirectory + * @param modulePrefixLength length of "/module/..." path prefix before start of actual member + * path + * @param class_stream_map a map from class file name to InputStream that collects the results + */ + @SuppressWarnings("builder:required.method.not.called") // assignment into collection of @Owning + void gather_runtime_from_modules_directory( + Path path, int modulePrefixLength, Map class_stream_map) { + + if (Files.isDirectory(path)) { + try (DirectoryStream directoryStream = Files.newDirectoryStream(path)) { + for (Path subpath : directoryStream) { + gather_runtime_from_modules_directory(subpath, modulePrefixLength, class_stream_map); + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } else { + String entryName = path.toString().substring(modulePrefixLength + 1); + if (!entryName.endsWith(".class") || entryName.equals("java/lang/Object.class")) { + // For JDK 9+ we do not process non-.class files and Object.class. + return; + } + // System.out.printf("processing entry %s%n", entryName); + try { + // Get the InputStream for this file + InputStream is = Files.newInputStream(path); + class_stream_map.put(entryName, is); + } catch (Exception e) { + throw new Error(e); + } + } + } + + /** + * Instrument each of the classes identified by the class_stream_map argument. + * + * @param dest_dir where to store the instrumented classes + * @param class_stream_map maps from class file name to an input stream on that file + */ + void instrument_classes(File dest_dir, Map class_stream_map) { + + @DotSeparatedIdentifiers String dcompPrefix; + + // Get our ClassLoader. + ClassLoader loader = BuildJDK24.class.getClassLoader(); + if (loader == null) { + loader = ClassLoader.getSystemClassLoader(); + } + + try { + // Create the destination directory + dest_dir.mkdirs(); + + // Process each file. + for (String classFileName : class_stream_map.keySet()) { + if (verbose) { + System.out.println("instrument_classes: " + classFileName); + } + + if (classFileName.equals("module-info.class")) { + System.out.printf("Skipping file %s%n", classFileName); + continue; + } + + // Handle non-.class files and Object.class. In JDK 8, copy them unchanged. + // For JDK 9+ they have not been added to class_stream_map. + if (!classFileName.endsWith(".class") || classFileName.equals("java/lang/Object.class")) { + // This File constructor ignores dest_dir if classFileName is absolute. + File classFile = new File(dest_dir, classFileName); + if (classFile.getParentFile() == null) { + throw new Error("This can't happen: " + classFile); + } + classFile.getParentFile().mkdirs(); + if (verbose) { + System.out.println("Copying Object.class or non-classfile: " + classFile); + } + try (InputStream in = class_stream_map.get(classFileName)) { + Files.copy(in, classFile.toPath()); + } + continue; + } + + ClassFile classFile = + ClassFile.of( + ClassFile.ClassHierarchyResolverOption.of( + ClassHierarchyResolver.ofResourceParsing(loader))); + ClassModel classModel; + byte[] buffer; + + // Parse the bytes of the classfile, die on any errors. + try (InputStream is = class_stream_map.get(classFileName)) { + buffer = is.readAllBytes(); + classModel = classFile.parse(buffer); + } catch (Throwable e) { + throw new Error("Failed to parse classfile " + classFileName, e); + } + + @SuppressWarnings("signature:assignment") // string manipulation + @InternalForm String classnameIF = removeSuffix(classFileName, ".class"); + String classname = Signatures.internalFormToBinaryName(classnameIF); + if (DynComp.dump) { + inst24.writeDebugClassFiles(buffer, inst24.debug_uninstrumented_dir, classname); + } + + // As `instrumentation_interface` is a static field, we initialize it here rather than + // in the DCInstrument24 constructor. + if (Premain.jdk_instrumented && Runtime.isJava9orLater()) { + dcompPrefix = "java.lang"; + } else { + dcompPrefix = "daikon.dcomp"; + } + DCRuntime.instrumentation_interface = + Signatures.addPackage(dcompPrefix, "DCompInstrumented"); + + // Instrument the class file. + try { + instrumentClassFile( + classFile, classModel, loader, dest_dir, classFileName, class_stream_map.size()); + } catch (Throwable e) { + throw new Error("Couldn't instrument " + classFileName, e); + } + } + } catch (Exception e) { + throw new Error(e); + } + } + + /** + * Add abstract interface classes needed by the DynComp runtime. + * + * @param destDir where to store the interface classes + */ + private void addInterfaceClasses(File destDir) { + // Create the DcompMarker class which is used to identify instrumented calls. + createDCompClass(destDir, "DCompMarker", false); + + // The remainder of the generated classes are needed for JDK 9+ only. + if (Runtime.isJava9orLater()) { + createDCompClass(destDir, "DCompInstrumented", true); + createDCompClass(destDir, "DCompClone", false); + createDCompClass(destDir, "DCompToString", false); + } + } + + /** + * Create an abstract interface class for use by the DynComp runtime. + * + * @param destDir where to store the new class + * @param className name of class + * @param dcompInstrumented if true, add equals_dcomp_instrumented method to class + */ + private void createDCompClass( + File destDir, @BinaryName String className, boolean dcompInstrumented) { + byte[] classBytes; + + try { + classBytes = + ClassFile.of() + .build( + ClassDesc.of(Signatures.addPackage("java.lang", className)), + classBuilder -> finishCreateDCompClass(classBuilder, dcompInstrumented)); + // Write the byte array to a .class file. + Path outputPath = Path.of(destDir.toString(), "java", "lang", className + ".class"); + Files.write(outputPath, classBytes); + } catch (Exception e) { + throw new Error(e); + } + } + + /** + * Create an abstract interface class for use by the DynComp runtime. + * + * @param classBuilder for the class + * @param dcompInstrumented if true, add equals_dcomp_instrumented method to class + */ + private void finishCreateDCompClass(ClassBuilder classBuilder, boolean dcompInstrumented) { + classBuilder.withSuperclass(ClassDesc.of("java.lang.Object")); + classBuilder.withFlags(ClassFile.ACC_INTERFACE | ClassFile.ACC_PUBLIC | ClassFile.ACC_ABSTRACT); + // Convert from JDK version number to ClassFile major_version. + classBuilder.withVersion(BcelUtil.javaVersion + 44, 0); + classBuilder.with(SourceFileAttribute.of("daikon.dcomp.BuildJDK24 tool")); + + if (dcompInstrumented) { + classBuilder.withMethod( + "equals_dcomp_instrumented", + MethodTypeDesc.of(CD_boolean, CD_Object), + ClassFile.ACC_PUBLIC | ClassFile.ACC_ABSTRACT, + methodBuilder -> {}); + } + } + + /** Formats just the time part of a DateTime, for progress diagnostics. */ + private static final DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HH:mm:ss"); + + /** + * Instruments the JavaClass {@code jc} (whose name is {@code classFileName}). Writes the + * resulting class to its corresponding location in the directory outputDir. + * + * @param classFile ClassFile of class to be instrumented + * @param classModel ClassModel of class to be instrumented + * @param loader ClassLoader of class to be instrumented + * @param outputDir output directory for instrumented class + * @param classFileName name of class to be instrumented (in internal form) + * @param classTotal total number of classes to be processed; used for progress display + * @throws IOException if unable to write out instrumented class + */ + @SuppressWarnings("SystemConsoleNull") // https://errorprone.info/bugpattern/SystemConsoleNull + private void instrumentClassFile( + ClassFile classFile, + ClassModel classModel, + ClassLoader loader, + File outputDir, + String classFileName, + int classTotal) + throws IOException { + if (verbose) { + System.out.printf("processing target %s%n", classFileName); + } + + // remove '.class' first + @SuppressWarnings("signature:assignment") // type conversion + @InternalForm String classnameIF = removeSuffix(classFileName, ".class"); + String classname = Signatures.internalFormToBinaryName(classnameIF); + ClassInfo classInfo = new ClassInfo(classname, loader); + DCInstrument24 dci = new DCInstrument24(classFile, classModel, true); + byte[] classBytes = dci.instrument_jdk_class(classInfo); + if (classBytes == null) { + throw new Error("Instrumentation failed: " + classFile); + } + if (DynComp.dump) { + inst24.writeDebugClassFiles(classBytes, inst24.debug_instrumented_dir, classname); + } + skipped_methods.addAll(dci.get_skipped_methods()); + File classfile = new File(classFileName); + File dir; + if (classfile.getParent() == null) { + dir = outputDir; + } else { + dir = new File(outputDir, classfile.getParent()); + } + dir.mkdirs(); + File classpath = new File(dir, classfile.getName()); + if (verbose) { + System.out.printf("writing to file %s%n", classpath); + } + Files.write(classpath.toPath(), classBytes); + _numFilesProcessed++; + if (((_numFilesProcessed % 100) == 0) && (System.console() != null)) { + System.out.printf( + "Processed %d/%d classes at %s%n", + _numFilesProcessed, + classTotal, + LocalDateTime.now(ZoneId.systemDefault()).format(timeFormatter)); + } + } + + /** + * Print information about methods that were not instrumented. This happens when a method fails + * the ClassFile API's verifier. + */ + private static void print_skipped_methods() { + + if (skipped_methods.isEmpty()) { + // System.out.printf("No methods were skipped.%n"); + return; + } + + System.err.println( + "Warning: The following JDK methods could not be instrumented. DynComp will"); + System.err.println("still work as long as these methods are not called by your application."); + System.err.println("If your application calls one, it will throw a NoSuchMethodException."); + + List unknown = new ArrayList<>(skipped_methods); + unknown.removeAll(known_uninstrumentable_methods); + List known = new ArrayList<>(skipped_methods); + known.retainAll(known_uninstrumentable_methods); + + if (!unknown.isEmpty()) { + System.err.println("Please report the following problems to the Daikon maintainers."); + System.err.println( + "Please give sufficient details; see \"Reporting problems\" in the Daikon manual."); + for (String method : unknown) { + System.err.printf(" %s%n", method); + } + } + if (!known.isEmpty()) { + System.err.printf("The following are known problems; you do not need to report them."); + for (String method : known) { + System.err.printf(" %s%n", method); + } + } + } + + /** + * Returns the given string, with the suffix removed if it was present. + * + * @param s a string + * @param suffix a suffix + * @return {@code s}, with the suffix removed if it was present. + */ + private static String removeSuffix(String s, String suffix) { + if (s.endsWith(suffix)) { + return s.substring(0, s.length() - suffix.length()); + } else { + return s; + } + } +} diff --git a/java/daikon/dcomp/CalcStack24.java b/java/daikon/dcomp/CalcStack24.java new file mode 100644 index 0000000000..318511077c --- /dev/null +++ b/java/daikon/dcomp/CalcStack24.java @@ -0,0 +1,1017 @@ +package daikon.dcomp; + +import static java.lang.constant.ConstantDescs.CD_Class; +import static java.lang.constant.ConstantDescs.CD_String; +import static java.lang.constant.ConstantDescs.CD_boolean; +import static java.lang.constant.ConstantDescs.CD_byte; +import static java.lang.constant.ConstantDescs.CD_char; +import static java.lang.constant.ConstantDescs.CD_double; +import static java.lang.constant.ConstantDescs.CD_float; +import static java.lang.constant.ConstantDescs.CD_int; +import static java.lang.constant.ConstantDescs.CD_long; +import static java.lang.constant.ConstantDescs.CD_short; +import static java.lang.constant.ConstantDescs.CD_void; + +import daikon.chicory.MethodGen24; +import java.lang.classfile.CodeElement; +import java.lang.classfile.Instruction; +import java.lang.classfile.Label; +import java.lang.classfile.Opcode; +import java.lang.classfile.constantpool.ClassEntry; +import java.lang.classfile.constantpool.ConstantDynamicEntry; +import java.lang.classfile.constantpool.DoubleEntry; +import java.lang.classfile.constantpool.FloatEntry; +import java.lang.classfile.constantpool.IntegerEntry; +import java.lang.classfile.constantpool.LoadableConstantEntry; +import java.lang.classfile.constantpool.LongEntry; +import java.lang.classfile.constantpool.MethodHandleEntry; +import java.lang.classfile.constantpool.MethodTypeEntry; +import java.lang.classfile.constantpool.StringEntry; +import java.lang.classfile.instruction.BranchInstruction; +import java.lang.classfile.instruction.ConstantInstruction; +import java.lang.classfile.instruction.ExceptionCatch; +import java.lang.classfile.instruction.FieldInstruction; +import java.lang.classfile.instruction.InvokeDynamicInstruction; +import java.lang.classfile.instruction.InvokeInstruction; +import java.lang.classfile.instruction.LineNumber; +import java.lang.classfile.instruction.LoadInstruction; +import java.lang.classfile.instruction.LookupSwitchInstruction; +import java.lang.classfile.instruction.NewMultiArrayInstruction; +import java.lang.classfile.instruction.NewObjectInstruction; +import java.lang.classfile.instruction.NewPrimitiveArrayInstruction; +import java.lang.classfile.instruction.NewReferenceArrayInstruction; +import java.lang.classfile.instruction.StoreInstruction; +import java.lang.classfile.instruction.SwitchCase; +import java.lang.classfile.instruction.TableSwitchInstruction; +import java.lang.classfile.instruction.TypeCheckInstruction; +import java.lang.constant.ClassDesc; +import java.lang.constant.MethodTypeDesc; +import java.util.List; +import java.util.Set; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.checker.signature.qual.FieldDescriptor; + +/** + * This class calculates the state of the operand stack via simulation. + * + *

During this process, it may modify the state of DCInstrument24.locals and + * DCInstrument24.stacks. + */ +public final class CalcStack24 { + + /** Do not instantiate. */ + private CalcStack24() { + throw new Error("Do not instantiate"); + } + + /** Sentinel ClassDesc representing {@code null} on the operand stack (not a real class). */ + static final ClassDesc NULL_CD = ClassDesc.of("daikon.dcomp.CalcStack24$NullSentinel"); + + /** Set of ClassDesc items that map to CD_int. */ + static final Set INTEGRAL = Set.of(CD_boolean, CD_byte, CD_char, CD_int, CD_short); + + /** + * Calculates changes in the operand stack based on the symbolic execution of a CodeElement. Note + * that we assume the class file is valid and make no attempt to verify the code's correctness. + * + * @param mgen method containing the instruction (currently unused) + * @param minfo for the given method's code (currently unused) + * @param ce CodeElement to be interpreted + * @param instIndex index of {@code ce} in code element list + * @param stack current state of operand stack; is side-effected + * @return true if control falls through to the next instruction, false otherwise (when {@code ce} + * is an instruction like jump or return) + * @throws DynCompError if we don't recognize {@code ce} + */ + static boolean simulateCodeElement( + MethodGen24 mgen, + MethodGen24.MInfo24 minfo, + CodeElement ce, + int instIndex, + OperandStack24 stack) { + + if (DCInstrument24.debugOperandStack) { + System.out.println("code element: " + ce); + } + switch (ce) { + case Instruction inst -> { + return simulateInstruction(inst, instIndex, stack); + } + + // We ignore most PseudoInstructions. + + case ExceptionCatch ec -> { + // Nothing needs to be done. + return true; + } + + // Technically, a Classfile element, not a PseudoInstruction. + case Label l -> { + if (DCInstrument24.stacks[instIndex] != null) { + // We've seen this label before. + DCInstrument24.verifyOperandStackMatches(l, DCInstrument24.stacks[instIndex], stack); + // Stacks match; we're done with this worklist. + return false; + } else { + // We have not seen this label before; remember the operand stack. + DCInstrument24.stacks[instIndex] = stack.getClone(); + if (DCInstrument24.debugOperandStack) { + System.out.println("save stack state at: " + l); + System.out.println(" " + instIndex + ", " + DCInstrument24.stacks[instIndex]); + } + return true; + } + } + + case LineNumber ln -> { + // Nothing needs to be done. + return true; + } + + default -> { + throw new DynCompError("Unexpected CodeElement: " + ce); + } + } + } + + /** + * Calculates changes in the operand stack based on the symbolic execution of a Java bytecode + * instruction. Note that we assume the class file is valid and make no attempt to verify the + * code's correctness. + * + * @param inst instruction to be interpreted + * @param instIndex index of inst in code element list + * @param stack current state of operand stack; is side-effected + * @return true if control falls through to the next instruction, false otherwise (when {@code + * inst} is an instruction like jump or return) + * @throws DynCompError if there is an error during the instruction simulation + */ + @SuppressWarnings("fallthrough") + static boolean simulateInstruction(Instruction inst, int instIndex, OperandStack24 stack) { + if (DCInstrument24.stacks[instIndex] != null) { + throw new DynCompError("instruction revisited at index " + instIndex + ": " + inst); + } else { + DCInstrument24.stacks[instIndex] = stack.getClone(); + } + if (DCInstrument24.debugOperandStack) { + System.out.println( + "save stack state at: " + instIndex + ", " + DCInstrument24.stacks[instIndex]); + System.out.println("opcode: " + inst.opcode()); + } + // calculate stack changes + switch (inst.opcode()) { + + // operand stack before: ..., arrayref, index + // operand stack after: ..., value + case Opcode.AALOAD: + { + stack.pop(); // discard the index + final ClassDesc t = stack.pop(); // pop the arrayref + if (t == null || NULL_CD.equals(t)) { + // The arrayref is null. A NullPointerException will be thrown at run time if this + // AALOAD is executed. + stack.push(NULL_CD); + } else { + final ClassDesc ct = t.componentType(); + if (ct == null) { + throw new DynCompError("stack item not an arrayref: " + inst); + } + stack.push(ct); + } + return true; + } + + // operand stack before: ..., arrayref, index, value + // operand stack after: ... + case Opcode.AASTORE: + case Opcode.BASTORE: + case Opcode.CASTORE: + case Opcode.DASTORE: + case Opcode.FASTORE: + case Opcode.IASTORE: + case Opcode.LASTORE: + case Opcode.SASTORE: + stack.pop(3); + return true; + + // operand stack before: ... + // operand stack after: ..., null + case Opcode.ACONST_NULL: + stack.push(NULL_CD); + return true; + + // operand stack before: ... + // operand stack after: ..., objectref + case Opcode.ALOAD: + case Opcode.ALOAD_0: + case Opcode.ALOAD_1: + case Opcode.ALOAD_2: + case Opcode.ALOAD_3: + case Opcode.ALOAD_W: + LoadInstruction li = (LoadInstruction) inst; + stack.push(DCInstrument24.locals[li.slot()]); + return true; + + // operand stack before: ..., count + // operand stack after: ..., arrayref + case Opcode.ANEWARRAY: + stack.pop(); // discard the count + final NewReferenceArrayInstruction nrai = (NewReferenceArrayInstruction) inst; + // make an array type from the component type + stack.push(nrai.componentType().asSymbol().arrayType(1)); + return true; + + // operand stack before: ... + // operand stack after: [empty] + case Opcode.ARETURN: + case Opcode.DRETURN: + case Opcode.FRETURN: + case Opcode.IRETURN: + case Opcode.LRETURN: + case Opcode.RETURN: + // execution pump will reset stack + return false; + + // operand stack before: ..., arrayref + // operand stack after: ..., length + case Opcode.ARRAYLENGTH: + stack.pop(); // discard the arrayref + stack.push(CD_int); + return true; + + // operand stack before: ..., value + // operand stack after: ... + case Opcode.ASTORE: + case Opcode.ASTORE_0: + case Opcode.ASTORE_1: + case Opcode.ASTORE_2: + case Opcode.ASTORE_3: + case Opcode.ASTORE_W: + case Opcode.DSTORE: + case Opcode.DSTORE_0: + case Opcode.DSTORE_1: + case Opcode.DSTORE_2: + case Opcode.DSTORE_3: + case Opcode.DSTORE_W: + case Opcode.FSTORE: + case Opcode.FSTORE_0: + case Opcode.FSTORE_1: + case Opcode.FSTORE_2: + case Opcode.FSTORE_3: + case Opcode.FSTORE_W: + case Opcode.ISTORE: + case Opcode.ISTORE_0: + case Opcode.ISTORE_1: + case Opcode.ISTORE_2: + case Opcode.ISTORE_3: + case Opcode.ISTORE_W: + case Opcode.LSTORE: + case Opcode.LSTORE_0: + case Opcode.LSTORE_1: + case Opcode.LSTORE_2: + case Opcode.LSTORE_3: + case Opcode.LSTORE_W: + StoreInstruction si = (StoreInstruction) inst; + // We assume code is correct and do not verify that si.typeKind() matches stack.pop(). + DCInstrument24.locals[si.slot()] = stack.pop(); + return true; + + // operand stack before: ..., objectref + // operand stack after: objectref + case Opcode.ATHROW: + // execution pump will reset stack + return false; + + // operand stack before: ..., arrayref, index + // operand stack after: ..., value + case Opcode.BALOAD: + case Opcode.CALOAD: + case Opcode.IALOAD: + case Opcode.SALOAD: + stack.pop(2); + stack.push(CD_int); + return true; + + // operand stack before: ... + // operand stack after: ..., value + case Opcode.BIPUSH: + case Opcode.ICONST_0: + case Opcode.ICONST_1: + case Opcode.ICONST_2: + case Opcode.ICONST_3: + case Opcode.ICONST_4: + case Opcode.ICONST_5: + case Opcode.ICONST_M1: + case Opcode.SIPUSH: + stack.push(CD_int); + return true; + + // operand stack before: ..., objectref + // operand stack after: ..., objectref + case Opcode.CHECKCAST: + { + final ClassDesc t = stack.pop(); // pop the objectref + if (t == null || NULL_CD.equals(t)) { + stack.push(NULL_CD); + } else { + TypeCheckInstruction tci = (TypeCheckInstruction) inst; + final ClassDesc ct = tci.type().asSymbol(); + // We assume the type check will succeed + stack.push(ct); + } + // just assume it will be ok, otherwise it will throw when executed + return true; + } + + // operand stack before: ..., value + // operand stack after: ..., result + case Opcode.D2F: // double to float + case Opcode.I2F: // integer to float + case Opcode.L2F: // long to float + stack.pop(); // discard the value + stack.push(CD_float); + return true; + + // operand stack before: ..., value + // operand stack after: ..., result + case Opcode.D2L: // double to long + case Opcode.F2L: // float to long + case Opcode.I2L: // integer to long + stack.pop(); // discard the value + stack.push(CD_long); + return true; + + // operand stack before: ..., value1, value2 + // operand stack after: ..., result + case Opcode.DADD: + case Opcode.DDIV: + case Opcode.DMUL: + case Opcode.DREM: + case Opcode.DSUB: + stack.pop(2); // discard the values + stack.push(CD_double); + return true; + + // operand stack before: ..., arrayref, index + // operand stack after: ..., value + case Opcode.DALOAD: + stack.pop(2); // discard the arrayref and index + stack.push(CD_double); + return true; + + // operand stack before: ..., value1, value2 + // operand stack after: ..., result + case Opcode.DCMPG: + case Opcode.DCMPL: + case Opcode.FCMPG: + case Opcode.FCMPL: + stack.pop(2); // discard the values + stack.push(CD_int); + return true; + + // operand stack before: ... + // operand stack after: ..., value + case Opcode.DCONST_0: + case Opcode.DCONST_1: + stack.push(CD_double); + return true; + + // operand stack before: ... + // operand stack after: ..., value + case Opcode.DLOAD: + case Opcode.DLOAD_0: + case Opcode.DLOAD_1: + case Opcode.DLOAD_2: + case Opcode.DLOAD_3: + case Opcode.DLOAD_W: + stack.push(CD_double); + return true; + + // operand stack before: ..., value + // operand stack after: ..., result + case Opcode.DNEG: + stack.pop(); // discard the value + stack.push(CD_double); + return true; + + // operand stack before: ..., value + // operand stack after: ..., value, value + case Opcode.DUP: + { + final ClassDesc t = stack.pop(); + stack.push(t); + stack.push(t); + return true; + } + + // operand stack before: ..., value2, value1 + // operand stack after: ..., value1, value2, value1 + case Opcode.DUP_X1: + { + final ClassDesc v1 = stack.pop(); + final ClassDesc v2 = stack.pop(); + stack.push(v1); + stack.push(v2); + stack.push(v1); + return true; + } + + // operand stack before: ..., value3, value2, value1 + // operand stack after: ..., value1, value3, value2, value1 + // where value1, value2, and value3 are all a category 1 computational type + // OR + // ..., value2, value1 + // ..., value1, value2, value1 + // where value1 is a category 1 computational type and value2 is a category 2 + // computational type + case Opcode.DUP_X2: + { + final ClassDesc v1 = stack.pop(); + final ClassDesc v2 = stack.pop(); + if (stack.slotSize(v2) == 2) { + stack.push(v1); + } else { + final ClassDesc v3 = stack.pop(); + stack.push(v1); + stack.push(v3); + } + stack.push(v2); + stack.push(v1); + return true; + } + + // operand stack before: ..., value2, value1 + // operand stack after: ..., value2, value1, value2, value1 + // where value1, and value2 are all a category 1 computational type + // OR + // ..., value + // ..., value, value + // where value is a category 2 computational type + case Opcode.DUP2: + { + final ClassDesc v1 = stack.pop(); + if (stack.slotSize(v1) == 2) { + stack.push(v1); + } else { // slotSize(v1) == 1 + final ClassDesc v2 = stack.pop(); + stack.push(v2); + stack.push(v1); + stack.push(v2); + } + stack.push(v1); + return true; + } + + // operand stack before: ..., value3, value2, value1 + // operand stack after: ..., value2, value1, value3, value2, value1 + // where value1, value2, and value3 are all a category 1 computational type + // OR + // ..., value2, value1 + // ..., value1, value2, value1 + // where value1 is a category 2 computational type and value2 a category 1 computational + // type + case Opcode.DUP2_X1: + { + final ClassDesc v1 = stack.pop(); + if (stack.slotSize(v1) == 2) { + final ClassDesc v2 = stack.pop(); + stack.push(v1); + stack.push(v2); + } else { // slotSize(v1) == 1 + final ClassDesc v2 = stack.pop(); + final ClassDesc v3 = stack.pop(); + stack.push(v2); + stack.push(v1); + stack.push(v3); + stack.push(v2); + } + stack.push(v1); + return true; + } + + // ..., value4, value3, value2, value1 + // ..., value2, value1, value4, value3, value2, value1 + // where value1, value2, value3, and value4 are all a category 1 computational type + // OR + // ..., value3, value2, value1 + // ..., value1, value3, value2, value1 + // where value1 is a category 2 computational type and value2 and value3 are both a + // category 1 computational type + // OR + // ..., value3, value2, value1 + // ..., value2, value1, value3, value2, value1 + // where value1 and value2 are both a category 1 computational type and value3 is a + // category 2 computational type + // OR + // ..., value2, value1 + // ..., value1, value2, value1 + // where value1 and value2 are both a category 2 computational type + case Opcode.DUP2_X2: + { + final ClassDesc v1 = stack.pop(); + if (stack.slotSize(v1) == 2) { + final ClassDesc v2 = stack.pop(); + if (stack.slotSize(v2) == 2) { + stack.push(v1); + } else { + final ClassDesc v3 = stack.pop(); + stack.push(v1); + stack.push(v3); + } + stack.push(v2); + stack.push(v1); + } else { // slotSize(v1) is 1 + final ClassDesc v2 = stack.pop(); + final ClassDesc v3 = stack.pop(); + if (stack.slotSize(v3) == 2) { + stack.push(v2); + stack.push(v1); + } else { + final ClassDesc v4 = stack.pop(); + stack.push(v2); + stack.push(v1); + stack.push(v4); + } + stack.push(v3); + stack.push(v2); + stack.push(v1); + } + return true; + } + + // operand stack before: ..., value + // operand stack after: ..., result + case Opcode.F2D: // float to double + case Opcode.I2D: // integer to double + case Opcode.L2D: // long to double + stack.pop(); // discard the value + stack.push(CD_double); + return true; + + // operand stack before: ..., value1, value2 + // operand stack after: ..., result + case Opcode.FADD: + case Opcode.FDIV: + case Opcode.FMUL: + case Opcode.FREM: + case Opcode.FSUB: + stack.pop(2); // discard the values + stack.push(CD_float); + return true; + + // operand stack before: ..., arrayref, index + // operand stack after: ..., value + case Opcode.FALOAD: + stack.pop(2); // discard the arrayref and index + stack.push(CD_float); + return true; + + // operand stack before: ... + // operand stack after: ..., value + case Opcode.FCONST_0: + case Opcode.FCONST_1: + case Opcode.FCONST_2: + stack.push(CD_float); + return true; + + // operand stack before: ... + // operand stack after: ..., value + case Opcode.FLOAD: + case Opcode.FLOAD_0: + case Opcode.FLOAD_1: + case Opcode.FLOAD_2: + case Opcode.FLOAD_3: + case Opcode.FLOAD_W: + stack.push(CD_float); + return true; + + // operand stack before: ..., value + // operand stack after: ..., result + case Opcode.FNEG: + stack.pop(); // discard the value + stack.push(CD_float); + return true; + + // operand stack before: ..., objectref + // operand stack after: ..., value + case Opcode.GETFIELD: + stack.pop(); // discard the objectref + + // fall through is intentional: + + // operand stack before: ... + // operand stack after: ..., value + case Opcode.GETSTATIC: + { + FieldInstruction fi = (FieldInstruction) inst; + pushResultClassDesc(fi.typeSymbol(), stack); + return true; + } + + // operand stack: no change + case Opcode.GOTO: + case Opcode.GOTO_W: + { + BranchInstruction bi = (BranchInstruction) inst; + addLabelsToWorklist(bi.target(), null, stack); + return false; + } + + // operand stack before: ..., value + // operand stack after: ..., result + case Opcode.I2B: // integer to byte + case Opcode.I2C: // integer to char + case Opcode.D2I: // double to integer + case Opcode.F2I: // float to integer + case Opcode.L2I: // long to int + case Opcode.I2S: // integer to short + stack.pop(); // discard the value + stack.push(CD_int); + return true; + + // operand stack before: ..., value1, value2 + // operand stack after: ..., result + case Opcode.IADD: + case Opcode.IAND: + case Opcode.IDIV: + case Opcode.IMUL: + case Opcode.IOR: + case Opcode.IREM: + case Opcode.ISHL: + case Opcode.ISHR: + case Opcode.ISUB: + case Opcode.IUSHR: + case Opcode.IXOR: + case Opcode.LCMP: + stack.pop(2); // discard the values + stack.push(CD_int); + return true; + + // operand stack before: ..., value1, value2 + // operand stack after: ... + case Opcode.IF_ACMPEQ: + case Opcode.IF_ACMPNE: + case Opcode.IF_ICMPEQ: + case Opcode.IF_ICMPGE: + case Opcode.IF_ICMPGT: + case Opcode.IF_ICMPLE: + case Opcode.IF_ICMPLT: + case Opcode.IF_ICMPNE: + { + stack.pop(2); // discard the values + BranchInstruction bi = (BranchInstruction) inst; + addLabelsToWorklist(bi.target(), null, stack); + return true; + } + + // operand stack before: ..., value + // operand stack after: ... + case Opcode.IFEQ: + case Opcode.IFNE: + case Opcode.IFLT: + case Opcode.IFGE: + case Opcode.IFGT: + case Opcode.IFLE: + case Opcode.IFNONNULL: + case Opcode.IFNULL: + stack.pop(); // discard the value + BranchInstruction bi = (BranchInstruction) inst; + addLabelsToWorklist(bi.target(), null, stack); + return true; + + // operand stack: no change + case Opcode.IINC: + case Opcode.IINC_W: + case Opcode.NOP: + return true; + + // operand stack before: ... + // operand stack after: ..., value + case Opcode.ILOAD: + case Opcode.ILOAD_0: + case Opcode.ILOAD_1: + case Opcode.ILOAD_2: + case Opcode.ILOAD_3: + case Opcode.ILOAD_W: + stack.push(CD_int); + return true; + + // operand stack before: ..., value + // operand stack after: ..., result + case Opcode.INEG: + // The top of the stack should be a CD_int, so we could just return true. However, we + // include the pop and push so the sequence is comparable to that of other operators. + stack.pop(); // discard the value + stack.push(CD_int); + return true; + + // operand stack before: ..., objectref + // operand stack after: ..., result + case Opcode.INSTANCEOF: + stack.pop(); // discard the value + stack.push(CD_int); + return true; + + // JSR and RET have been illegal since JDK 7 (class file version 51.0). + // We don't have a case label for JSR, JSR_W, RET or RET_W so that they fall + // into the default case and report an "Unexpected instruction opcode" error. + + // operand stack before: ..., value1, value2 + // operand stack after: ..., result + case Opcode.LADD: + case Opcode.LAND: + case Opcode.LDIV: + case Opcode.LMUL: + case Opcode.LOR: + case Opcode.LREM: + case Opcode.LSHL: + case Opcode.LSHR: + case Opcode.LSUB: + case Opcode.LUSHR: + case Opcode.LXOR: + stack.pop(2); // discard the values + stack.push(CD_long); + return true; + + // operand stack before: ..., arrayref, index + // operand stack after: ..., value + case Opcode.LALOAD: + stack.pop(2); // discard the arrayref and index + stack.push(CD_long); + return true; + + // operand stack before: ... + // operand stack after: ..., value + case Opcode.LCONST_0: + case Opcode.LCONST_1: + stack.push(CD_long); + return true; + + // operand stack before: ... + // operand stack after: ..., value + case Opcode.LDC: + case Opcode.LDC_W: + case Opcode.LDC2_W: + ConstantInstruction.LoadConstantInstruction ldc = + (ConstantInstruction.LoadConstantInstruction) inst; + LoadableConstantEntry lce = ldc.constantEntry(); + stack.push(lceToCD(lce)); + return true; + + // operand stack before: ... + // operand stack after: ..., value + case Opcode.LLOAD: + case Opcode.LLOAD_0: + case Opcode.LLOAD_1: + case Opcode.LLOAD_2: + case Opcode.LLOAD_3: + case Opcode.LLOAD_W: + stack.push(CD_long); + return true; + + // operand stack before: ..., value + // operand stack after: ..., result + case Opcode.LNEG: + stack.pop(); // discard the value + stack.push(CD_long); + return true; + + // operand stack before: ..., key + // operand stack after: ... + case Opcode.LOOKUPSWITCH: + stack.pop(); // discard the value + LookupSwitchInstruction lsi = (LookupSwitchInstruction) inst; + addLabelsToWorklist(lsi.defaultTarget(), lsi.cases(), stack); + return false; + + // operand stack before: ..., objectref + // operand stack after: ... + case Opcode.MONITORENTER: + case Opcode.MONITOREXIT: + stack.pop(); // discard the value + return true; + + // operand stack before: ..., count1, [count2, ...] + // operand stack after: ..., arrayref + case Opcode.MULTIANEWARRAY: + final NewMultiArrayInstruction nmai = (NewMultiArrayInstruction) inst; + stack.pop(nmai.dimensions()); // discard all the counts + stack.push(nmai.arrayType().asSymbol()); + return true; + + // operand stack before: ... + // operand stack after: ..., objectref + case Opcode.NEW: + final NewObjectInstruction noi = (NewObjectInstruction) inst; + stack.push(noi.className().asSymbol()); + return true; + + // operand stack before: ..., count + // operand stack after: ..., arrayref + case Opcode.NEWARRAY: + stack.pop(); // discard the count + final NewPrimitiveArrayInstruction npai = (NewPrimitiveArrayInstruction) inst; + stack.push(ClassDesc.ofDescriptor("[" + npaiToElementTypeDescriptor(npai))); + return true; + + // operand stack before: ..., value + // operand stack after: ... + case Opcode.POP: + stack.pop(); // discard the value + return true; + + // operand stack before: ..., value2, value1 + // operand stack after: ... + // where each of value1 and value2 is a category 1 computational type + // OR + // ..., value + // ... + // where value is of a category 2 computational type + case Opcode.POP2: + { + final ClassDesc v1 = stack.pop(); + if (stack.slotSize(v1) == 1) { + stack.pop(); + } + return true; + } + + // operand stack before: ..., objectref, value + // operand stack after: ... + case Opcode.PUTFIELD: + stack.pop(2); + return true; + + // operand stack before: ..., value + // operand stack after: ... + case Opcode.PUTSTATIC: + stack.pop(); // discard the value + return true; + + // operand stack before: ..., value1, value2 + // operand stack after: ..., value2, value1 + case Opcode.SWAP: + { + final ClassDesc v2 = stack.pop(); + final ClassDesc v1 = stack.pop(); + stack.push(v2); + stack.push(v1); + return true; + } + + // operand stack before: ..., index + // operand stack after: ... + case Opcode.TABLESWITCH: + stack.pop(); // discard the index + TableSwitchInstruction tsi = (TableSwitchInstruction) inst; + addLabelsToWorklist(tsi.defaultTarget(), tsi.cases(), stack); + return false; + + // operand stack before: ..., objectref, [arg1, [arg2 ...]] + // operand stack after: ... + case Opcode.INVOKEINTERFACE: + case Opcode.INVOKESPECIAL: + case Opcode.INVOKEVIRTUAL: + stack.pop(); // Discard the last argument (which is at the top of the stack), + // or the objectref if there are no arguments. + + // may actually be removing an arg, but we'll + // account for that when we remove args below + + // fall through is intentional: + + // operand stack before: ..., [arg1, [arg2 ...]] + // operand stack after: ... + case Opcode.INVOKESTATIC: + { + final InvokeInstruction ii = (InvokeInstruction) inst; + final MethodTypeDesc mtd = ii.typeSymbol(); + stack.pop(mtd.parameterCount()); // discard the arguments + // We are sure the invoked method will xRETURN eventually. + // We simulate xRETURN's functionality here because we don't + // really "jump into" and simulate the invoked method. + pushResultClassDesc(mtd.returnType(), stack); + return true; + } + + // operand stack before: ..., [arg1, [arg2 ...]] + // operand stack after: ... + case Opcode.INVOKEDYNAMIC: + { + final InvokeDynamicInstruction idi = (InvokeDynamicInstruction) inst; + final MethodTypeDesc mtd = idi.typeSymbol(); + stack.pop(mtd.parameterCount()); // discard the arguments + // We are sure the invoked method will xRETURN eventually. + // We simulate xRETURNs functionality here because we don't + // really "jump into" and simulate the invoked method. + pushResultClassDesc(mtd.returnType(), stack); + return true; + } + + default: + throw new DynCompError("Unexpected instruction opcode: " + inst); + } + } + + /** + * Calculate a ClassDesc from a LoadableConstantEntry. + * + * @param lce a LoadableConstantEntry + * @return the ClassDesc of {@code lce} + * @throws DynCompError if we don't recognize {@code lce} + */ + static ClassDesc lceToCD(LoadableConstantEntry lce) { + switch (lce) { + case ClassEntry cle -> { + return CD_Class; + } + case ConstantDynamicEntry cde -> { + return cde.typeSymbol(); + } + case DoubleEntry de -> { + return CD_double; // LDC2_W only, but we assume correct code + } + case FloatEntry fe -> { + return CD_float; + } + case IntegerEntry ie -> { + return CD_int; + } + case LongEntry le -> { + return CD_long; // LDC2_W only, but we assume correct code + } + case MethodHandleEntry mhe -> { + return ClassDesc.of("java.lang.invoke.MethodHandle"); + } + case MethodTypeEntry mte -> { + return ClassDesc.of("java.lang.invoke.MethodType"); + } + case StringEntry se -> { + return CD_String; + } + default -> { + throw new DynCompError("Illegal LoadableConstantEntry: " + lce); + } + } + } + + /** + * Calculate an element type descriptor from a NewPrimitiveArrayInstruction. + * + * @param npai a NewPrimitiveArrayInstruction + * @return a String containing the element type descriptor + * @throws DynCompError if we don't recognize the type of {@code npai} + */ + static @FieldDescriptor String npaiToElementTypeDescriptor(NewPrimitiveArrayInstruction npai) { + return switch (npai.typeKind()) { + case BOOLEAN -> "Z"; + case BYTE -> "B"; + case CHAR -> "C"; + case DOUBLE -> "D"; + case FLOAT -> "F"; + case INT -> "I"; + case LONG -> "J"; + case SHORT -> "S"; + default -> + throw new DynCompError("unknown primitive type " + npai.typeKind() + " in: " + npai); + }; + } + + /** + * Calculate a Java bytecode's result type and push it on the operand stack. If the type is void, + * nothing is pushed. If the type is boolean, byte, char, or short, then an int is pushed. + * Otherwise, the result itself is pushed. + * + * @param result the result type descriptor to push (may be void or a primitive/reference type) + * @param stack state of operand stack + */ + static void pushResultClassDesc(ClassDesc result, OperandStack24 stack) { + switch (result) { + case ClassDesc c when c.equals(CD_void) -> {} + case ClassDesc c when INTEGRAL.contains(c) -> { + stack.push(CD_int); + } + default -> { + stack.push(result); + } + } + } + + /** + * Calls DCInstrument24.addLabelToWorklist to create a set of worklist items. + * + * @param target label where to start operand stack simulation + * @param cases a set of case statement labels; additional start points for simulation + * @param stack state of operand stack at target and case labels + */ + static void addLabelsToWorklist( + Label target, @Nullable List cases, OperandStack24 stack) { + DCInstrument24.addLabelToWorklist(target, stack); + if (cases == null) { + return; + } + for (SwitchCase item : cases) { + DCInstrument24.addLabelToWorklist(item.target(), stack); + } + } +} diff --git a/java/daikon/dcomp/ClassGen24.java b/java/daikon/dcomp/ClassGen24.java new file mode 100644 index 0000000000..c85bbe8168 --- /dev/null +++ b/java/daikon/dcomp/ClassGen24.java @@ -0,0 +1,233 @@ +package daikon.dcomp; + +import daikon.chicory.Runtime; +import daikon.plumelib.reflection.Signatures; +import java.lang.classfile.AccessFlags; +import java.lang.classfile.ClassBuilder; +import java.lang.classfile.ClassModel; +import java.lang.classfile.MethodModel; +import java.lang.classfile.constantpool.ClassEntry; +import java.lang.constant.ClassDesc; +import java.lang.reflect.AccessFlag; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import org.checkerframework.checker.lock.qual.GuardSatisfied; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.checker.signature.qual.BinaryName; +import org.checkerframework.checker.signature.qual.Identifier; +import org.checkerframework.checker.signature.qual.MethodDescriptor; + +/** + * ClassGen24 represents a class. ClassGen24 is analogous to the BCEL ClassGen class. The similarity + * makes it easier to keep DCInstrument.java and DCInstrument24.java in sync. + * + *

ClassGen24 uses Java's {@code java.lang.classfile} APIs for reading and modifying .class + * files. Those APIs were added in JDK 24. Compared to BCEL, these APIs are more complete and robust + * (no more fiddling with StackMaps) and are always up to date with any .class file changes (since + * they are part of the JDK). (We will need to continue to support Instrument.java using BCEL, as we + * anticipate our clients using JDK 21 or less for quite some time.) + */ +public class ClassGen24 { + + /** + * Models the body of the class. + * + *

Several fields of ClassModel are cached as fields of ClassGen24 to better correspond to + * BCEL's version of ClassGen and to reduce re-computation. + */ + private final ClassModel classModel; + + // + // Start of ClassModel items. + // + + /** The class's access flags. */ + private final AccessFlags accessFlags; + + /** The list of interfaces this class implements. */ + private final List interfaceList; + + /** The name of the class's superclass, in binary name format. */ + private final @BinaryName String superclassName; + + /** The name of the class, in binary name format. */ + private final @BinaryName String className; + + // + // End of ClassModel items. + // + + /** The ClassBuilder for this class. */ + private final ClassBuilder classBuilder; + + /** True if this class is an interface. */ + private final boolean isInterface; + + /** True if this class is static. */ + private final boolean isStatic; + + /** + * Creates a ClassGen24 object. + * + * @param classModel for the class + * @param className class name, in binary name format + * @param classBuilder for the class + */ + public ClassGen24( + final ClassModel classModel, + final @BinaryName String className, + final ClassBuilder classBuilder) { + + this.classModel = classModel; + this.className = className; + this.classBuilder = classBuilder; + + accessFlags = classModel.flags(); + isInterface = accessFlags.has(AccessFlag.INTERFACE); + isStatic = accessFlags.has(AccessFlag.STATIC); + + superclassName = getSuperclassName(classModel); + + // The original interface list is immutable, so we need to make a copy, to accommodate method + // `addInterface()`. + interfaceList = new ArrayList(classModel.interfaces()); + } + + /** + * Add an interface to this class. + * + * @param name the interface name, in binary format + */ + public void addInterface(@BinaryName String name) { + String internalName = Runtime.binaryNameToInternalForm(name); + for (ClassEntry existing : interfaceList) { + if (existing.asInternalName().equals(internalName)) { + return; + } + } + ClassDesc ue = ClassDesc.of(name); + ClassEntry ce = classBuilder.constantPool().classEntry(ue); + interfaceList.add(ce); + } + + /** + * Returns the indicatated method, or null if this class does not contain it. + * + * @param name the method's name + * @param descriptor the method's type descriptor + * @return the MethodModel if found, null otherwise + */ + public @Nullable MethodModel containsMethod( + @Identifier String name, @MethodDescriptor String descriptor) { + for (MethodModel mm : classModel.methods()) { + if (mm.methodName().stringValue().equals(name) + && mm.methodType().stringValue().equals(descriptor)) { + return mm; + } + } + return null; + } + + /** + * Returns this class's access flags. + * + * @return the access flags + */ + public AccessFlags getAccessFlags() { + return accessFlags; + } + + /** + * Returns true if this class is an interface. + * + * @return true if this class is an interface + */ + public final boolean isInterface() { + return isInterface; + } + + /** + * Returns true if this class is static. + * + * @return true if this class is static + */ + public final boolean isStatic() { + return isStatic; + } + + /** + * Returns this class's name, in binary format. + * + * @return this class's name, in binary format + */ + public @BinaryName String getClassName() { + return className; + } + + /** + * Returns a {@code ClassModel}'s class name, in binary format. + * + * @return the class's name, in binary format + */ + public static @BinaryName String getClassName(ClassModel classModel) { + return Signatures.internalFormToBinaryName(classModel.thisClass().asInternalName()); + } + + /** + * Returns the name of the superclass of this class. If this class is {@link Object}, it will + * return itself ({@link Object}). This is probably incorrect but is consistent with the BCEL + * version of getSuperclassName. + * + * @return the binary name of this class's superclass + */ + public @BinaryName String getSuperclassName() { + return superclassName; + } + + /** + * Returns the name of the superclass of the argument. If the argument class is {@link Object}, it + * will return itself ({@link Object}). This is probably incorrect but is consistent with the BCEL + * version of getSuperclassName. + * + * @param classModel the class to check + * @return the binary name of the superclass of classModel or "java.lang.Object" if it has no + * superclass + */ + public static @BinaryName String getSuperclassName(ClassModel classModel) { + Optional ce = classModel.superclass(); + if (ce.isPresent()) { + return Runtime.internalFormToBinaryName(ce.get().asInternalName()); + } else { + return "java.lang.Object"; + } + } + + /** + * Returns the interfaces of this class. + * + * @return the interfaces of this class + */ + public List getInterfaceList() { + return interfaceList; + } + + /** + * Returns the class builder. + * + * @return the class builder + */ + public ClassBuilder getClassBuilder() { + return classBuilder; + } + + /** + * Returns the class name. + * + * @return the class name + */ + @Override + public final String toString(@GuardSatisfied ClassGen24 this) { + return className; + } +} diff --git a/java/daikon/dcomp/DCInstrument.java b/java/daikon/dcomp/DCInstrument.java index 3e22fe2e78..79f4a2c440 100644 --- a/java/daikon/dcomp/DCInstrument.java +++ b/java/daikon/dcomp/DCInstrument.java @@ -1,5 +1,219 @@ package daikon.dcomp; +import static org.apache.bcel.Const.AALOAD; +import static org.apache.bcel.Const.AASTORE; +import static org.apache.bcel.Const.ACC_ABSTRACT; +import static org.apache.bcel.Const.ACC_ANNOTATION; +import static org.apache.bcel.Const.ACC_BRIDGE; +import static org.apache.bcel.Const.ACC_FINAL; +import static org.apache.bcel.Const.ACC_NATIVE; +import static org.apache.bcel.Const.ACC_PUBLIC; +import static org.apache.bcel.Const.ACC_STATIC; +import static org.apache.bcel.Const.ACONST_NULL; +import static org.apache.bcel.Const.ALOAD; +import static org.apache.bcel.Const.ALOAD_0; +import static org.apache.bcel.Const.ALOAD_1; +import static org.apache.bcel.Const.ALOAD_2; +import static org.apache.bcel.Const.ALOAD_3; +import static org.apache.bcel.Const.ANEWARRAY; +import static org.apache.bcel.Const.APPEND_FRAME; +import static org.apache.bcel.Const.ARETURN; +import static org.apache.bcel.Const.ARRAYLENGTH; +import static org.apache.bcel.Const.ASTORE; +import static org.apache.bcel.Const.ASTORE_0; +import static org.apache.bcel.Const.ASTORE_1; +import static org.apache.bcel.Const.ASTORE_2; +import static org.apache.bcel.Const.ASTORE_3; +import static org.apache.bcel.Const.ATHROW; +import static org.apache.bcel.Const.BALOAD; +import static org.apache.bcel.Const.BASTORE; +import static org.apache.bcel.Const.BIPUSH; +import static org.apache.bcel.Const.CALOAD; +import static org.apache.bcel.Const.CASTORE; +import static org.apache.bcel.Const.CHECKCAST; +import static org.apache.bcel.Const.D2F; +import static org.apache.bcel.Const.D2I; +import static org.apache.bcel.Const.D2L; +import static org.apache.bcel.Const.DADD; +import static org.apache.bcel.Const.DALOAD; +import static org.apache.bcel.Const.DASTORE; +import static org.apache.bcel.Const.DCMPG; +import static org.apache.bcel.Const.DCMPL; +import static org.apache.bcel.Const.DCONST_0; +import static org.apache.bcel.Const.DCONST_1; +import static org.apache.bcel.Const.DDIV; +import static org.apache.bcel.Const.DLOAD; +import static org.apache.bcel.Const.DLOAD_0; +import static org.apache.bcel.Const.DLOAD_1; +import static org.apache.bcel.Const.DLOAD_2; +import static org.apache.bcel.Const.DLOAD_3; +import static org.apache.bcel.Const.DMUL; +import static org.apache.bcel.Const.DNEG; +import static org.apache.bcel.Const.DREM; +import static org.apache.bcel.Const.DRETURN; +import static org.apache.bcel.Const.DSTORE; +import static org.apache.bcel.Const.DSTORE_0; +import static org.apache.bcel.Const.DSTORE_1; +import static org.apache.bcel.Const.DSTORE_2; +import static org.apache.bcel.Const.DSTORE_3; +import static org.apache.bcel.Const.DSUB; +import static org.apache.bcel.Const.DUP; +import static org.apache.bcel.Const.DUP2; +import static org.apache.bcel.Const.DUP2_X1; +import static org.apache.bcel.Const.DUP2_X2; +import static org.apache.bcel.Const.DUP_X1; +import static org.apache.bcel.Const.DUP_X2; +import static org.apache.bcel.Const.F2D; +import static org.apache.bcel.Const.F2I; +import static org.apache.bcel.Const.F2L; +import static org.apache.bcel.Const.FADD; +import static org.apache.bcel.Const.FALOAD; +import static org.apache.bcel.Const.FASTORE; +import static org.apache.bcel.Const.FCMPG; +import static org.apache.bcel.Const.FCMPL; +import static org.apache.bcel.Const.FCONST_0; +import static org.apache.bcel.Const.FCONST_1; +import static org.apache.bcel.Const.FCONST_2; +import static org.apache.bcel.Const.FDIV; +import static org.apache.bcel.Const.FLOAD; +import static org.apache.bcel.Const.FLOAD_0; +import static org.apache.bcel.Const.FLOAD_1; +import static org.apache.bcel.Const.FLOAD_2; +import static org.apache.bcel.Const.FLOAD_3; +import static org.apache.bcel.Const.FMUL; +import static org.apache.bcel.Const.FNEG; +import static org.apache.bcel.Const.FREM; +import static org.apache.bcel.Const.FRETURN; +import static org.apache.bcel.Const.FSTORE; +import static org.apache.bcel.Const.FSTORE_0; +import static org.apache.bcel.Const.FSTORE_1; +import static org.apache.bcel.Const.FSTORE_2; +import static org.apache.bcel.Const.FSTORE_3; +import static org.apache.bcel.Const.FSUB; +import static org.apache.bcel.Const.FULL_FRAME; +import static org.apache.bcel.Const.GETFIELD; +import static org.apache.bcel.Const.GETSTATIC; +import static org.apache.bcel.Const.GOTO; +import static org.apache.bcel.Const.GOTO_W; +import static org.apache.bcel.Const.I2B; +import static org.apache.bcel.Const.I2C; +import static org.apache.bcel.Const.I2D; +import static org.apache.bcel.Const.I2F; +import static org.apache.bcel.Const.I2L; +import static org.apache.bcel.Const.I2S; +import static org.apache.bcel.Const.IADD; +import static org.apache.bcel.Const.IALOAD; +import static org.apache.bcel.Const.IAND; +import static org.apache.bcel.Const.IASTORE; +import static org.apache.bcel.Const.ICONST_0; +import static org.apache.bcel.Const.ICONST_1; +import static org.apache.bcel.Const.ICONST_2; +import static org.apache.bcel.Const.ICONST_3; +import static org.apache.bcel.Const.ICONST_4; +import static org.apache.bcel.Const.ICONST_5; +import static org.apache.bcel.Const.ICONST_M1; +import static org.apache.bcel.Const.IDIV; +import static org.apache.bcel.Const.IFEQ; +import static org.apache.bcel.Const.IFGE; +import static org.apache.bcel.Const.IFGT; +import static org.apache.bcel.Const.IFLE; +import static org.apache.bcel.Const.IFLT; +import static org.apache.bcel.Const.IFNE; +import static org.apache.bcel.Const.IFNONNULL; +import static org.apache.bcel.Const.IFNULL; +import static org.apache.bcel.Const.IF_ACMPEQ; +import static org.apache.bcel.Const.IF_ACMPNE; +import static org.apache.bcel.Const.IF_ICMPEQ; +import static org.apache.bcel.Const.IF_ICMPGE; +import static org.apache.bcel.Const.IF_ICMPGT; +import static org.apache.bcel.Const.IF_ICMPLE; +import static org.apache.bcel.Const.IF_ICMPLT; +import static org.apache.bcel.Const.IF_ICMPNE; +import static org.apache.bcel.Const.IINC; +import static org.apache.bcel.Const.ILOAD; +import static org.apache.bcel.Const.ILOAD_0; +import static org.apache.bcel.Const.ILOAD_1; +import static org.apache.bcel.Const.ILOAD_2; +import static org.apache.bcel.Const.ILOAD_3; +import static org.apache.bcel.Const.IMUL; +import static org.apache.bcel.Const.INEG; +import static org.apache.bcel.Const.INSTANCEOF; +import static org.apache.bcel.Const.INVOKEDYNAMIC; +import static org.apache.bcel.Const.INVOKEINTERFACE; +import static org.apache.bcel.Const.INVOKESPECIAL; +import static org.apache.bcel.Const.INVOKESTATIC; +import static org.apache.bcel.Const.INVOKEVIRTUAL; +import static org.apache.bcel.Const.IOR; +import static org.apache.bcel.Const.IREM; +import static org.apache.bcel.Const.IRETURN; +import static org.apache.bcel.Const.ISHL; +import static org.apache.bcel.Const.ISHR; +import static org.apache.bcel.Const.ISTORE; +import static org.apache.bcel.Const.ISTORE_0; +import static org.apache.bcel.Const.ISTORE_1; +import static org.apache.bcel.Const.ISTORE_2; +import static org.apache.bcel.Const.ISTORE_3; +import static org.apache.bcel.Const.ISUB; +import static org.apache.bcel.Const.ITEM_Object; +import static org.apache.bcel.Const.IUSHR; +import static org.apache.bcel.Const.IXOR; +import static org.apache.bcel.Const.JSR; +import static org.apache.bcel.Const.JSR_W; +import static org.apache.bcel.Const.L2D; +import static org.apache.bcel.Const.L2F; +import static org.apache.bcel.Const.L2I; +import static org.apache.bcel.Const.LADD; +import static org.apache.bcel.Const.LALOAD; +import static org.apache.bcel.Const.LAND; +import static org.apache.bcel.Const.LASTORE; +import static org.apache.bcel.Const.LCMP; +import static org.apache.bcel.Const.LCONST_0; +import static org.apache.bcel.Const.LCONST_1; +import static org.apache.bcel.Const.LDC; +import static org.apache.bcel.Const.LDC2_W; +import static org.apache.bcel.Const.LDC_W; +import static org.apache.bcel.Const.LDIV; +import static org.apache.bcel.Const.LLOAD; +import static org.apache.bcel.Const.LLOAD_0; +import static org.apache.bcel.Const.LLOAD_1; +import static org.apache.bcel.Const.LLOAD_2; +import static org.apache.bcel.Const.LLOAD_3; +import static org.apache.bcel.Const.LMUL; +import static org.apache.bcel.Const.LNEG; +import static org.apache.bcel.Const.LOOKUPSWITCH; +import static org.apache.bcel.Const.LOR; +import static org.apache.bcel.Const.LREM; +import static org.apache.bcel.Const.LRETURN; +import static org.apache.bcel.Const.LSHL; +import static org.apache.bcel.Const.LSHR; +import static org.apache.bcel.Const.LSTORE; +import static org.apache.bcel.Const.LSTORE_0; +import static org.apache.bcel.Const.LSTORE_1; +import static org.apache.bcel.Const.LSTORE_2; +import static org.apache.bcel.Const.LSTORE_3; +import static org.apache.bcel.Const.LSUB; +import static org.apache.bcel.Const.LUSHR; +import static org.apache.bcel.Const.LXOR; +import static org.apache.bcel.Const.MAJOR_1_8; +import static org.apache.bcel.Const.MAX_CODE_SIZE; +import static org.apache.bcel.Const.MONITORENTER; +import static org.apache.bcel.Const.MONITOREXIT; +import static org.apache.bcel.Const.MULTIANEWARRAY; +import static org.apache.bcel.Const.NEW; +import static org.apache.bcel.Const.NEWARRAY; +import static org.apache.bcel.Const.NOP; +import static org.apache.bcel.Const.POP; +import static org.apache.bcel.Const.POP2; +import static org.apache.bcel.Const.PUTFIELD; +import static org.apache.bcel.Const.PUTSTATIC; +import static org.apache.bcel.Const.RET; +import static org.apache.bcel.Const.RETURN; +import static org.apache.bcel.Const.SALOAD; +import static org.apache.bcel.Const.SASTORE; +import static org.apache.bcel.Const.SIPUSH; +import static org.apache.bcel.Const.SWAP; +import static org.apache.bcel.Const.TABLESWITCH; + import daikon.DynComp; import daikon.chicory.ClassInfo; import daikon.chicory.DaikonWriter; @@ -28,10 +242,10 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Pattern; -import org.apache.bcel.Const; import org.apache.bcel.classfile.AnnotationEntry; import org.apache.bcel.classfile.Annotations; import org.apache.bcel.classfile.Attribute; @@ -91,41 +305,48 @@ import org.apache.bcel.generic.StoreInstruction; import org.apache.bcel.generic.Type; import org.apache.bcel.verifier.structurals.OperandStack; +import org.checkerframework.checker.interning.qual.InternedDistinct; import org.checkerframework.checker.lock.qual.GuardSatisfied; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf; import org.checkerframework.checker.nullness.qual.KeyFor; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; import org.checkerframework.checker.signature.qual.BinaryName; import org.checkerframework.checker.signature.qual.ClassGetName; import org.checkerframework.checker.signature.qual.DotSeparatedIdentifiers; import org.checkerframework.checker.signature.qual.Identifier; +import org.checkerframework.checker.signature.qual.InternalForm; import org.checkerframework.dataflow.qual.Pure; /** * Instruments a class file to perform Dynamic Comparability. * - *

The DCInstrument class is responsible for modifying another class's bytecodes. Specifically, - * its main task is to add calls into the DynComp Runtime to calculate comparability values. These - * added calls are sometimes referred to as "hooks". + *

This class is responsible for modifying bytecodes. Specifically, its main task is to add calls + * into the DynComp Runtime to calculate comparability values. These added calls are sometimes + * referred to as "hooks". */ -@SuppressWarnings("nullness") public class DCInstrument extends InstructionListUtils { /** - * Used when testing to continue processing if an error occurs. Currently, This flag is only used + * Used when testing to continue processing if an error occurs. Currently, this flag is only used * by BuildJDK. */ @Option("Halt if an instrumentation error occurs") public static boolean quit_if_error = true; + /** The loader that loaded the Class to instrument. */ + protected @Nullable ClassLoader loader; + /** Unmodified version of input class. */ protected JavaClass orig_class; /** ClassGen for the current class. */ - protected ClassGen gen; + protected ClassGen classGen; /** MethodGen for the current method. */ - protected MethodGen mgen; + protected @MonotonicNonNull MethodGen mgen; /** Is the current class a member of the JDK? */ protected boolean in_jdk; @@ -133,42 +354,97 @@ public class DCInstrument extends InstructionListUtils { /** The BCEL InstructionFactory for generating byte code instructions. */ protected InstructionFactory ifact; - /** The loader that loaded the Class to instrument. */ - protected @Nullable ClassLoader loader; - /** Has an {@code } method completed initialization? */ protected boolean constructor_is_initialized; /** Local that stores the tag frame for the current method. */ - protected LocalVariableGen tag_frame_local; + protected @Nullable LocalVariableGen tagFrameLocal; - // Argument descriptors - /** Type array with two objects. */ - protected static Type[] two_objects = new Type[] {Type.OBJECT, Type.OBJECT}; + // Type descriptors: non-arrays - /** Type array with an object and an int. */ - protected static Type[] object_int = new Type[] {Type.OBJECT, Type.INT}; + /** Type for "java.lang.Class". */ + protected static ObjectType CD_Class = new ObjectType("java.lang.Class"); - /** Type array with a string. */ - protected static Type[] string_arg = new Type[] {Type.STRING}; + /** "java.lang.Object". */ + private static final ObjectType CD_Object = Type.OBJECT; + + // private static final ObjectType CD_Object = new ObjectType("java.lang.Object"); + + /** Type for "java.lang.String". */ + private static final ObjectType CD_String = Type.STRING; + + /** Type for "java.lang.Throwable". */ + private static final ObjectType CD_Throwable = Type.THROWABLE; + + // private static final ObjectType CD_Throwable = new ObjectType("java.lang.Throwable"); + + /** The special DCompMarker type. */ + protected final ObjectType dcomp_marker; + + /** Type for "boolean". */ + private static final @InternedDistinct BasicType CD_boolean = Type.BOOLEAN; + + /** Type for "byte". */ + private static final @InternedDistinct BasicType CD_byte = Type.BYTE; + + /** Type for "char". */ + private static final @InternedDistinct BasicType CD_char = Type.CHAR; + + /** Type for "double". */ + private static final @InternedDistinct BasicType CD_double = Type.DOUBLE; + + /** Type for "float". */ + private static final @InternedDistinct BasicType CD_float = Type.FLOAT; + + /** Type for "int". */ + private static final @InternedDistinct BasicType CD_int = Type.INT; + + /** Type for "long". */ + private static final @InternedDistinct BasicType CD_long = Type.LONG; + + /** Type for "short". */ + private static final @InternedDistinct BasicType CD_short = Type.SHORT; + + /** Type for "void". */ + private static final @InternedDistinct BasicType CD_void = Type.VOID; + + // Type descriptors: arrays + + /** "java.lang.Object[]". */ + protected static Type CD_Object_array = new ArrayType(CD_Object, 1); + + // Signature descriptors: no parameters + + /** Type array with no parameters. */ + protected static final Type[] noArgsSig = Type.NO_ARGS; + + // Signature descriptors: one parameter /** Type array with an int. */ - protected static Type[] integer_arg = new Type[] {Type.INT}; + protected static Type[] intSig = {CD_int}; + + /** Type array with a long. */ + protected static Type[] longSig = {CD_long}; + + /** Type array with a string. */ + protected static Type[] string_arg = {CD_String}; /** Type array with an object. */ - protected static Type[] object_arg = new Type[] {Type.OBJECT}; + protected static Type[] object_arg = {CD_Object}; - /** ObjectType for "java.lang.Class". */ - protected static Type javalangClass = new ObjectType("java.lang.Class"); + // Signature descriptors: two parameters - // Type descriptors - protected static Type object_arr = new ArrayType(Type.OBJECT, 1); - // private Type int_arr = new ArrayType (Type.INT, 1); - protected static ObjectType throwable = new ObjectType("java.lang.Throwable"); - protected ObjectType dcomp_marker; - protected static ObjectType javalangObject = new ObjectType("java.lang.Object"); + /** Type array with a long and an int. */ + protected static Type[] longIntSig = {CD_long, CD_int}; + + /** Type array with an object and an int. */ + protected static Type[] objectIntSig = {CD_Object, CD_int}; + + /** Type array with two objects. */ + protected static Type[] objectObjectSig = {CD_Object, CD_Object}; // Debug loggers + /** Log file if debug_native is enabled. */ protected static SimpleLog debug_native = new SimpleLog(false); @@ -183,7 +459,7 @@ public class DCInstrument extends InstructionListUtils { // Flags to enable additional console output for debugging /** If true, enable JUnit analysis debugging. */ - protected static final boolean debugJUnitAnalysis = false; + protected static final boolean debugJunitAnalysis = false; /** If true, enable {@link #getDefiningInterface} debugging. */ protected static final boolean debugGetDefiningInterface = false; @@ -191,110 +467,133 @@ public class DCInstrument extends InstructionListUtils { /** If true, enable {@link #handleInvoke} debugging. */ protected static final boolean debugHandleInvoke = false; + // End of debug loggers. + /** Keeps track of the methods that were not successfully instrumented. */ protected List skipped_methods = new ArrayList<>(); - /** Either "java.lang" or "daikon.dcomp". */ - protected @DotSeparatedIdentifiers String dcomp_prefix; + /** + * If we're using an instrumented JDK and the JDK version is 9 or higher, then "java.lang"; + * otherwise, "daikon.dcomp". + */ + protected @DotSeparatedIdentifiers String dcompRuntimePrefix; /** Either "daikon.dcomp.DCRuntime" or "java.lang.DCRuntime". */ - protected @DotSeparatedIdentifiers String dcompRuntimeClassName = "daikon.dcomp.DCRuntime"; + protected @BinaryName String dcompRuntimeClassName = "daikon.dcomp.DCRuntime"; /** Set of JUnit test classes. */ protected static Set junitTestClasses = new HashSet<>(); /** Possible states of JUnit test discovery. */ - protected enum JUnitState { + protected enum JunitState { + /** Have not seen a JUnit class file. */ NOT_SEEN, + /** Have seen a JUnit class file. */ STARTING, + /** Have seen a JUnit class file that loads JUnit test classes. */ TEST_DISCOVERY, - RUNNING - }; + /** Have completed identifying JUnit test classes and are instrumenting the code. */ + INSTRUMENTING + } /** Current state of JUnit test discovery. */ - protected static JUnitState junit_state = JUnitState.NOT_SEEN; + protected static JunitState junit_state = JunitState.NOT_SEEN; - /** Have we seen 'JUnitCommandLineParseResult.parse'? */ + /** Have we seen {@code JUnitCommandLineParseResult.parse}? */ protected static boolean junit_parse_seen = false; /** - * Map from each static field name to its unique integer id. Note that while it's intuitive to - * think that each static should show up exactly once, that is not the case. A static defined in a - * superclass can be accessed through each of its subclasses. Tag accessor methods must be added - * in each subclass and each should return the same id. We thus will lookup the same name multiple - * times. + * Map from each static qualified field name to a unique integer id. Note that while it's + * intuitive to think that each static should show up exactly once, that is not always true. A + * static defined in a superclass can be accessed through each of its subclasses. In this case, + * tag accessor methods must be added in each subclass and each should return the id of the field + * in the superclass. This map is populated in {@link build_field_to_offset_map} and used in + * {@link create_tag_accessors}. */ static Map static_field_id = new LinkedHashMap<>(); /** - * Map from class name to its access_flags. Used to cache the results of the lookup done in {@link - * #getAccessFlags}. If a class is marked ACC_ANNOTATION then it will not have been instrumented. + * Map from binary class name to its access_flags. Used to cache the results of the lookup done in + * {@link #getAccessFlags}. If a class is marked ACC_ANNOTATION then it will not have been + * instrumented. */ static Map accessFlags = new HashMap<>(); /** Integer constant of access_flag value of ACC_ANNOTATION. */ - static Integer Integer_ACC_ANNOTATION = Integer.valueOf(Const.ACC_ANNOTATION); + static Integer Integer_ACC_ANNOTATION = Integer.valueOf(ACC_ANNOTATION); /** - * Array of classes whose fields are not initialized from java. Since the fields are not - * initialized from java, their tag storage is not allocated as part of a store, but rather must - * be allocated as part of a load. We call a special runtime method for this so that we can check - * for this in other cases. + * Array of classes whose fields are not initialized from Java (i.e., these classes are + * initialized by the JVM). Since the fields are not initialized from Java, their tag storage is + * not allocated as part of a store, but rather must be allocated as part of a load. We call a + * special runtime method for this so that we can check for this in other cases. */ - protected static String[] uninit_classes = - new String[] { - "java.lang.String", - "java.lang.Class", - "java.lang.StringBuilder", - "java.lang.AbstractStringBuilder", - }; + protected static final String[] uninit_classes = { + "java.lang.String", + "java.lang.Class", + "java.lang.StringBuilder", + "java.lang.AbstractStringBuilder", + }; /** * List of Object methods. Since we can't instrument Object, none of these can be instrumented, - * and most of them don't provide useful comparability information anyway. The equals method and - * the clone method are special-cased in the {@link #handleInvoke} routine. + * and most of them don't provide useful comparability information anyway. + * + *

The equals method and the clone method are not listed here. They are special-cased in the + * {@link #handleInvoke} routine. */ - protected static MethodDef[] obj_methods = - new MethodDef[] { - new MethodDef("finalize", new Type[0]), - new MethodDef("getClass", new Type[0]), - new MethodDef("hashCode", new Type[0]), - new MethodDef("notify", new Type[0]), - new MethodDef("notifyall", new Type[0]), - new MethodDef("toString", new Type[0]), - new MethodDef("wait", new Type[0]), - new MethodDef("wait", new Type[] {Type.LONG}), - new MethodDef("wait", new Type[] {Type.LONG, Type.INT}), - }; - - protected static InstructionList global_catch_il = null; - protected static CodeExceptionGen global_exception_handler = null; - private InstructionHandle insertion_placeholder; - - /** Class that defines a method (by its name and argument types) */ + protected static final MethodDef[] obj_methods = { + new MethodDef("finalize", noArgsSig), + new MethodDef("getClass", noArgsSig), + new MethodDef("hashCode", noArgsSig), + new MethodDef("notify", noArgsSig), + new MethodDef("notifyall", noArgsSig), + new MethodDef("toString", noArgsSig), + new MethodDef("wait", noArgsSig), + new MethodDef("wait", longSig), + new MethodDef("wait", longIntSig), + }; + + /** Catch block for our handler. */ + protected static @Nullable InstructionList global_catch_il = null; + + /** Handler we add to surround entire method. */ + protected static @Nullable CodeExceptionGen global_exception_handler = null; + + /** Temporary location of runtime initialization code. */ + private @MonotonicNonNull InstructionHandle insertion_placeholder; + + /** Represents a method (by its name and parameter types). */ static class MethodDef { + /** Name of this method. */ String name; + + /** Parameter types for this method. */ Type[] arg_types; + /** + * Create a new MethodDef. + * + * @param name of method + * @param arg_types of method + */ MethodDef(String name, Type[] arg_types) { this.name = name; this.arg_types = arg_types; } + /** + * Equality test for MethodDef. + * + * @param name of method + * @param arg_types of method + */ @EnsuresNonNullIf(result = true, expression = "#1") boolean equals(@GuardSatisfied MethodDef this, String name, Type[] arg_types) { if (!name.equals(this.name)) { return false; } - if (this.arg_types.length != arg_types.length) { - return false; - } - for (int ii = 0; ii < arg_types.length; ii++) { - if (!arg_types[ii].equals(this.arg_types[ii])) { - return false; - } - } - return true; + return Arrays.equals(this.arg_types, arg_types); } @EnsuresNonNullIf(result = true, expression = "#1") @@ -311,37 +610,33 @@ public boolean equals(@GuardSatisfied MethodDef this, @GuardSatisfied @Nullable @Pure @Override public int hashCode(@GuardSatisfied MethodDef this) { - int code = name.hashCode(); - for (Type arg : arg_types) { - code += arg.hashCode(); - } - return code; + return Objects.hash(name, Arrays.hashCode(arg_types)); } } - /** Initialize with the original class and whether or not the class is part of the JDK. */ + /** Initialize the class information and whether or not that class is part of the JDK. */ @SuppressWarnings("StaticAssignmentInConstructor") // instrumentation_interface public DCInstrument(JavaClass orig_class, boolean in_jdk, @Nullable ClassLoader loader) { super(); this.orig_class = orig_class; this.in_jdk = in_jdk; this.loader = loader; - gen = new ClassGen(orig_class); - pool = gen.getConstantPool(); - ifact = new InstructionFactory(gen); + classGen = new ClassGen(orig_class); + pool = classGen.getConstantPool(); + ifact = new InstructionFactory(classGen); constructor_is_initialized = false; if (Premain.jdk_instrumented) { - dcomp_prefix = "java.lang"; + dcompRuntimePrefix = "java.lang"; } else { - dcomp_prefix = "daikon.dcomp"; + dcompRuntimePrefix = "daikon.dcomp"; } - dcomp_marker = new ObjectType(Signatures.addPackage(dcomp_prefix, "DCompMarker")); + dcomp_marker = new ObjectType(Signatures.addPackage(dcompRuntimePrefix, "DCompMarker")); if (BcelUtil.javaVersion == 8) { - dcomp_prefix = "daikon.dcomp"; + dcompRuntimePrefix = "daikon.dcomp"; } - DCRuntime.instrumentation_interface = Signatures.addPackage(dcomp_prefix, "DCompInstrumented"); + DCRuntime.instrumentation_interface = + Signatures.addPackage(dcompRuntimePrefix, "DCompInstrumented"); - // System.out.printf("DCInstrument %s%n", orig_class.getClassName()); // Turn on some of the logging based on debug option. debugInstrument.enabled = DynComp.debug || Premain.debug_dcinstrument; debug_native.enabled = DynComp.debug; @@ -349,23 +644,23 @@ public DCInstrument(JavaClass orig_class, boolean in_jdk, @Nullable ClassLoader } /** - * Instruments the original class to perform dynamic comparabilty and returns the new class - * definition. + * Instruments a class to perform dynamic comparability and returns the new class definition. A + * second version of each method in the class is created which is instrumented for comparability. * * @return the modified JavaClass */ public JavaClass instrument() { - @BinaryName String classname = gen.getClassName(); + @BinaryName String classname = classGen.getClassName(); // Don't know where I got this idea. They are executed. Don't remember why // adding dcomp marker causes problems. // Don't instrument annotations. They aren't executed and adding // the marker argument causes subtle errors - if ((gen.getModifiers() & Const.ACC_ANNOTATION) != 0) { + if ((classGen.getModifiers() & ACC_ANNOTATION) != 0) { debug_transform.log("Not instrumenting annotation %s%n", classname); // WHY NOT RETURN NULL? - return gen.getJavaClass().copy(); + return classGen.getJavaClass().copy(); } // If a class has an EvoSuite annotation it may be instrumented by Evosuite; @@ -376,219 +671,224 @@ public JavaClass instrument() { if (item.toString().startsWith("@Lorg/evosuite/runtime")) { debug_transform.log("Not instrumenting possible Evosuite target: %s%n", classname); // WHY NOT RETURN NULL? - return gen.getJavaClass().copy(); + return classGen.getJavaClass().copy(); } } } } - debug_transform.log("Instrumenting class %s%n", classname); - debug_transform.indent(); - // Create the ClassInfo for this class and its list of methods - ClassInfo class_info = new ClassInfo(classname, loader); - boolean track_class = false; + ClassInfo classInfo = new ClassInfo(classname, loader); + boolean trackClass = false; - // Handle object methods for this class - handle_object(gen); + debug_transform.log( + "%nInstrumenting class%s: %s%n", in_jdk ? " (in JDK)" : "", classInfo.class_name); + debug_transform.indent(); - // Have all top-level classes implement our interface - if (gen.getSuperclassName().equals("java.lang.Object")) { + // Handle object methods for this class. + add_clone_and_tostring_interfaces(classGen); + + // Have all top-level classes implement the DCompInstrumented interface. + if (classGen.getSuperclassName().equals("java.lang.Object")) { // Add equals method if it doesn't already exist. This ensures // that an instrumented version, equals(Object, DCompMarker), // will be created in this class. - Method eq = gen.containsMethod("equals", "(Ljava/lang/Object;)Z"); + Method eq = classGen.containsMethod("equals", "(Ljava/lang/Object;)Z"); if (eq == null) { debugInstrument.log("Added equals method%n"); - add_equals_method(gen); + add_equals_method(classGen); } // Add DCompInstrumented interface and the required // equals_dcomp_instrumented method. - add_dcomp_interface(gen); + add_dcomp_interface(classGen); } - // A very tricky special case: If JUnit is running and the current - // class has been passed to JUnit on the command line, then this - // is a JUnit test class and our normal instrumentation will - // cause JUnit to complain about multiple constructors and - // methods that should have no arguments. To work around these - // restrictions, we replace rather than duplicate each method - // we instrument and we do not add the dcomp marker parameter. - // We must also remember the class name so if we see a subsequent - // call to one of its methods we do not add the dcomp argument. - - debugInstrument.log("junit_state: %s%n", junit_state); - - StackTraceElement[] stack_trace; - - switch (junit_state) { - case NOT_SEEN: - if (classname.startsWith("org.junit")) { - junit_state = JUnitState.STARTING; - } - break; + boolean junit_test_class = false; - case STARTING: - // Now check to see if JUnit is looking for test class(es). - stack_trace = Thread.currentThread().getStackTrace(); - // [0] is getStackTrace - for (int i = 1; i < stack_trace.length; i++) { - if (debugJUnitAnalysis) { - System.out.printf( - "%s : %s%n", stack_trace[i].getClassName(), stack_trace[i].getMethodName()); - } - if (isJunitTrigger(stack_trace[i].getClassName(), stack_trace[i].getMethodName())) { - junit_parse_seen = true; - junit_state = JUnitState.TEST_DISCOVERY; - break; + if (!in_jdk) { + // A very tricky special case: If JUnit is running and the current + // class has been passed to JUnit on the command line, then this + // is a JUnit test class and our normal instrumentation will + // cause JUnit to complain about multiple constructors and + // methods that should have no arguments. To work around these + // restrictions, we replace rather than duplicate each method + // we instrument and we do not add the dcomp marker parameter. + // We must also remember the class name so if we see a subsequent + // call to one of its methods we do not add the dcomp argument. + + debugInstrument.log("junit_state: %s%n", junit_state); + + StackTraceElement[] stack_trace; + + switch (junit_state) { + case NOT_SEEN: + if (classname.startsWith("org.junit")) { + junit_state = JunitState.STARTING; } - } - break; + break; - case TEST_DISCOVERY: - // Now check to see if JUnit is done looking for test class(es). - boolean local_junit_parse_seen = false; - stack_trace = Thread.currentThread().getStackTrace(); - // [0] is getStackTrace - for (int i = 1; i < stack_trace.length; i++) { - if (debugJUnitAnalysis) { - System.out.printf( - "%s : %s%n", stack_trace[i].getClassName(), stack_trace[i].getMethodName()); - } - if (isJunitTrigger(stack_trace[i].getClassName(), stack_trace[i].getMethodName())) { - local_junit_parse_seen = true; - break; + case STARTING: + // Now check to see if JUnit is looking for test class(es). + stack_trace = Thread.currentThread().getStackTrace(); + // [0] is getStackTrace + for (int i = 1; i < stack_trace.length; i++) { + if (debugJunitAnalysis) { + System.out.printf( + "%s : %s%n", stack_trace[i].getClassName(), stack_trace[i].getMethodName()); + } + if (isJunitTrigger(stack_trace[i].getClassName(), stack_trace[i].getMethodName())) { + junit_parse_seen = true; + junit_state = JunitState.TEST_DISCOVERY; + break; + } } - } - if (junit_parse_seen && !local_junit_parse_seen) { - junit_parse_seen = false; - junit_state = JUnitState.RUNNING; - } else if (!junit_parse_seen && local_junit_parse_seen) { - junit_parse_seen = true; - } - break; + break; - case RUNNING: - if (debugJUnitAnalysis) { + case TEST_DISCOVERY: + // Now check to see if JUnit is done looking for test class(es). + boolean local_junit_parse_seen = false; stack_trace = Thread.currentThread().getStackTrace(); // [0] is getStackTrace for (int i = 1; i < stack_trace.length; i++) { - System.out.printf( - "%s : %s%n", stack_trace[i].getClassName(), stack_trace[i].getMethodName()); + if (debugJunitAnalysis) { + System.out.printf( + "%s : %s%n", stack_trace[i].getClassName(), stack_trace[i].getMethodName()); + } + if (isJunitTrigger(stack_trace[i].getClassName(), stack_trace[i].getMethodName())) { + local_junit_parse_seen = true; + break; + } } - } - // nothing to do - break; + if (junit_parse_seen && !local_junit_parse_seen) { + junit_parse_seen = false; + junit_state = JunitState.INSTRUMENTING; + } else if (!junit_parse_seen && local_junit_parse_seen) { + junit_parse_seen = true; + } + break; - default: - throw new Error("invalid junit_state"); - } + case INSTRUMENTING: + if (debugJunitAnalysis) { + stack_trace = Thread.currentThread().getStackTrace(); + // [0] is getStackTrace + for (int i = 1; i < stack_trace.length; i++) { + System.out.printf( + "%s : %s%n", stack_trace[i].getClassName(), stack_trace[i].getMethodName()); + } + } + // nothing to do + break; - debugInstrument.log("junit_state: %s%n", junit_state); + default: + throw new DynCompError("invalid junit_state"); + } - boolean junit_test_class = false; - if (junit_state == JUnitState.TEST_DISCOVERY) { - // We have a possible JUnit test class. We need to verify by - // one of two methods. Either the class is a subclass of - // junit.framework.TestCase or one of its methods has a - // RuntimeVisibleAnnotation of org/junit/Test. - Deque classnameStack = new ArrayDeque<>(); - String super_class; - String this_class = classname; - while (true) { - try { - super_class = getSuperclassName(this_class); - } catch (SuperclassNameError e) { - if (debugJUnitAnalysis) { - System.out.printf("Unable to get superclass for: %s%n", this_class); + debugInstrument.log("junit_state: %s%n", junit_state); + + if (junit_state == JunitState.TEST_DISCOVERY) { + // We have a possible JUnit test class. We need to verify by + // one of two methods. Either the class is a subclass of + // junit.framework.TestCase or one of its methods has a + // RuntimeVisibleAnnotation of org/junit/Test. + Deque classnameStack = new ArrayDeque<>(); + String super_class; + String this_class = classname; + while (true) { + try { + super_class = getSuperclassName(this_class); + } catch (SuperclassNameError e) { + if (debugJunitAnalysis) { + System.out.printf("Unable to get superclass for: %s%n", this_class); + } + break; } - break; - } - if (debugJUnitAnalysis) { - System.out.printf("this_class: %s%n", this_class); - System.out.printf("super_class: %s%n", super_class); - } - if (super_class.equals("junit.framework.TestCase")) { - // This is a junit test class and so are the - // elements of classnameStack. - junit_test_class = true; - junitTestClasses.add(this_class); - while (!classnameStack.isEmpty()) { - junitTestClasses.add(classnameStack.pop()); + if (debugJunitAnalysis) { + System.out.printf("this_class: %s%n", this_class); + System.out.printf("super_class: %s%n", super_class); } - break; - } else if (super_class.equals("java.lang.Object")) { - // We're done; not a junit test class. - // Ignore items on classnameStack. - break; + if (super_class.equals("junit.framework.TestCase")) { + // This is a JUnit test class and so are the + // elements of classnameStack. + junit_test_class = true; + junitTestClasses.add(this_class); + while (!classnameStack.isEmpty()) { + junitTestClasses.add(classnameStack.pop()); + } + break; + } else if (super_class.equals("java.lang.Object")) { + // We're done; not a JUnit test class. + // Ignore items on classnameStack. + break; + } + // Recurse and check the super_class. + classnameStack.push(this_class); + this_class = super_class; } - // Recurse and check the super_class. - classnameStack.push(this_class); - this_class = super_class; } - } - // Even if we have not detected that JUnit is active, any class that - // contains a method with a RuntimeVisibleAnnotation of org/junit/Test - // needs to be marked as a JUnit test class. (Daikon issue #536) - - if (!junit_test_class) { - // need to check for junit Test annotation on a method - searchloop: - for (Method m : gen.getMethods()) { - for (final Attribute attribute : m.getAttributes()) { - if (attribute instanceof RuntimeVisibleAnnotations) { - if (debugJUnitAnalysis) { - System.out.printf("attribute: %s%n", attribute.toString()); - } - for (final AnnotationEntry item : ((Annotations) attribute).getAnnotationEntries()) { - if (debugJUnitAnalysis) { - System.out.printf("item: %s%n", item.toString()); + // Even if we have not detected that JUnit is active, any class that + // contains a method with a RuntimeVisibleAnnotation of org/junit/Test + // needs to be marked as a JUnit test class. (Daikon issue #536) + + if (!junit_test_class) { + // need to check for JUnit Test annotation on a method + searchloop: + for (Method m : classGen.getMethods()) { + for (final Attribute attribute : m.getAttributes()) { + if (attribute instanceof RuntimeVisibleAnnotations) { + if (debugJunitAnalysis) { + System.out.printf("attribute: %s%n", attribute.toString()); } - if (item.toString().endsWith("org/junit/Test;") // JUnit 4 - || item.toString().endsWith("org/junit/jupiter/api/Test;") // JUnit 5 - ) { - junit_test_class = true; - junitTestClasses.add(classname); - break searchloop; + for (final AnnotationEntry item : ((Annotations) attribute).getAnnotationEntries()) { + String description = item.toString(); + if (debugJunitAnalysis) { + System.out.printf("item: %s%n", description); + } + if (description.endsWith("org/junit/Test;") // JUnit 4 + || description.endsWith("org/junit/jupiter/api/Test;") // JUnit 5 + ) { + junit_test_class = true; + junitTestClasses.add(classname); + break searchloop; + } } } } } } - } - if (junit_test_class) { - debugInstrument.log("JUnit test class: %s%n", classname); - } else { - debugInstrument.log("Not a JUnit test class: %s%n", classname); + if (junit_test_class) { + debugInstrument.log("JUnit test class: %s%n", classname); + } else { + debugInstrument.log("Not a JUnit test class: %s%n", classname); + } } - // Process each method - for (Method m : gen.getMethods()) { + // Process each method in the class. + for (Method m : classGen.getMethods()) { - tag_frame_local = null; + tagFrameLocal = null; try { // Note whether we want to track the daikon variables in this method boolean track = should_track(classname, m.getName(), methodEntryName(classname, m)); // We do not want to track bridge methods the compiler has synthesized as // they are overloaded on return type which normal Java does not support. - if ((m.getAccessFlags() & Const.ACC_BRIDGE) != 0) { + if ((m.getAccessFlags() & ACC_BRIDGE) != 0) { track = false; } // If any one method is tracked, then the class is tracked. if (track) { - track_class = true; + trackClass = true; } // If we are tracking variables, make sure the class is public - if (track && !gen.isPublic()) { - gen.isPrivate(false); - gen.isProtected(false); - gen.isPublic(true); + if (track && !classGen.isPublic()) { + classGen.isPrivate(false); + classGen.isProtected(false); + classGen.isPublic(true); } debug_transform.log(" Processing method %s, track=%b%n", simplify_method_name(m), track); @@ -600,7 +900,7 @@ public JavaClass instrument() { InstructionList il = mgen.getInstructionList(); boolean has_code = (il != null); if (has_code) { - setCurrentStackMapTable(mgen, gen.getMajor()); + setCurrentStackMapTable(mgen, classGen.getMajor()); buildUninitializedNewMap(il); } @@ -610,9 +910,9 @@ public JavaClass instrument() { if (mgen.isNative()) { // Create Java code that cleans up the tag stack and calls the real native method. - fix_native(gen, mgen); + fix_native(classGen, mgen); has_code = true; - setCurrentStackMapTable(mgen, gen.getMajor()); + setCurrentStackMapTable(mgen, classGen.getMajor()); // Add the DCompMarker parameter to distinguish our version add_dcomp_param(mgen); @@ -628,21 +928,35 @@ public JavaClass instrument() { // and exit line numbers (information not available via reflection) // and add it to the list for this class. if (has_code) { + assert stackMapTable != null + : "@AssumeAssertion(nullness): is set above when has_code is true"; MethodInfo mi = null; if (track) { - mi = create_method_info(class_info, mgen); - class_info.method_infos.add(mi); + mi = create_method_info_if_instrumented(classInfo, mgen); + assert mi != null : "@AssumeAssertion(nullness)"; + classInfo.method_infos.add(mi); DCRuntime.methods.add(mi); } - // Create the local to store the tag frame for this method - tag_frame_local = create_tag_frame_local(mgen); + tagFrameLocal = createTagFrameLocal(mgen); build_exception_handler(mgen); - instrument_method(mgen); + assert stackMapTable != null + : "@AssumeAssertion(nullness): checked above and not modified since"; + assert tagFrameLocal != null + : "@AssumeAssertion(nullness) set above and not modified by" + + " build_exception_handler"; + instrumentMethod(mgen); if (track) { + assert mi != null : "@AssumeAssertion(nullness): track == true => mi != null"; + assert tagFrameLocal != null + : "@AssumeAssertion(nullness) set above and not modified by" + + " build_exception_handler"; add_enter(mgen, mi, DCRuntime.methods.size() - 1); + assert tagFrameLocal != null : "@AssumeAssertion(nullness): ??"; add_exit(mgen, mi, DCRuntime.methods.size() - 1); } + assert this.stackMapTable != null + : "@AssumeAssertion(nullness): checked above and not modified since"; install_exception_handler(mgen); } } @@ -662,7 +976,7 @@ public JavaClass instrument() { // We do not want to copy the @HotSpotIntrinsicCandidate annotations from // the original method to our instrumented method as the signature will // not match anything in the JVM's list. This won't cause an execution - // problem but will produce a massive number of warnings. + // problem but will produce a number of warnings. // JDK 11: @HotSpotIntrinsicCandidate // JDK 17: @IntrinsicCandidate AnnotationEntryGen[] aes = mgen.getAnnotationEntries(); @@ -681,18 +995,18 @@ public JavaClass instrument() { il = mgen.getInstructionList(); InstructionHandle end = il.getEnd(); int length = end.getPosition() + end.getInstruction().getLength(); - if (length >= Const.MAX_CODE_SIZE) { + if (length >= MAX_CODE_SIZE) { throw new ClassGenException( - "Code array too big: must be smaller than " + Const.MAX_CODE_SIZE + " bytes."); + "Code array too big: must be smaller than " + MAX_CODE_SIZE + " bytes."); } } if (replacingMethod) { - gen.replaceMethod(m, mgen.getMethod()); + classGen.replaceMethod(m, mgen.getMethod()); if (BcelUtil.isMain(mgen)) { - gen.addMethod(create_dcomp_stub(mgen).getMethod()); + classGen.addMethod(create_dcomp_stub(mgen).getMethod()); } } else { - gen.addMethod(mgen.getMethod()); + classGen.addMethod(mgen.getMethod()); } } catch (Exception e) { String s = e.getMessage(); @@ -710,18 +1024,18 @@ public JavaClass instrument() { // first, restore unmodified method mgen = new MethodGen(m, classname, pool); // restore StackMapTable - setCurrentStackMapTable(mgen, gen.getMajor()); + setCurrentStackMapTable(mgen, classGen.getMajor()); // Add the DCompMarker parameter add_dcomp_param(mgen); remove_local_variable_type_table(mgen); // try again if (replacingMethod) { - gen.replaceMethod(m, mgen.getMethod()); + classGen.replaceMethod(m, mgen.getMethod()); if (BcelUtil.isMain(mgen)) { - gen.addMethod(create_dcomp_stub(mgen).getMethod()); + classGen.addMethod(create_dcomp_stub(mgen).getMethod()); } } else { - gen.addMethod(mgen.getMethod()); + classGen.addMethod(mgen.getMethod()); } } else { throw e; @@ -738,66 +1052,70 @@ public JavaClass instrument() { } } - // Add tag accessor methods for each primitive in the class - create_tag_accessors(gen); + assert mgen != null : "@AssumeAssertion(nullness): bug? when the class has no methods"; + + // Add tag accessor methods for each primitive in the class. + create_tag_accessors(classGen); // Keep track of when the class is initialized (so we don't look - // for fields in uninitialized classes) - track_class_init(); + // for fields in uninitialized classes). + trackClass_init(); debug_transform.exdent(); // The code that builds the list of daikon variables for each ppt // needs to know what classes are instrumented. Its looks in the // Chicory runtime for this information. - if (track_class) { - debug_transform.log("DCInstrument adding %s to all class list%n", class_info); + if (trackClass) { + debug_transform.log("DCInstrument adding %s to all class list%n", classInfo); synchronized (daikon.chicory.SharedData.all_classes) { - daikon.chicory.SharedData.all_classes.add(class_info); + daikon.chicory.SharedData.all_classes.add(classInfo); } } debug_transform.log("Instrumentation complete: %s%n", classname); - return gen.getJavaClass().copy(); + return classGen.getJavaClass().copy(); } /** - * Returns true if the specified classname.method_name is the root of JUnit startup code. + * Returns true if the specified classname.methodName is the root of JUnit startup code. * - * @param classname class to be checked - * @param method_name method to be checked + * @param classname class containing the given method + * @param methodName method to be checked * @return true if the given method is a JUnit trigger */ - boolean isJunitTrigger(String classname, @Identifier String method_name) { - if ((classname.contains("JUnitCommandLineParseResult") - && method_name.equals("parse")) // JUnit 4 - || (classname.contains("EngineDiscoveryRequestResolution") - && method_name.equals("resolve")) // JUnit 5 - ) { + boolean isJunitTrigger(String classname, @Identifier String methodName) { + if (classname.contains("JUnitCommandLineParseResult") && methodName.equals("parse")) { + // JUnit 4 + return true; + } + if (classname.contains("EngineDiscoveryRequestResolution") && methodName.equals("resolve")) { + // JUnit 5 return true; } return false; } + // /////////////////////////////////////////////////////////////////////////// // General Java Runtime instrumentation strategy: // - //

It is a bit of a misnomer, but the Daikon code and documentation uses the term JDK to refer + // It is a bit of a misnomer, but the Daikon code and documentation uses the term JDK to refer // to the Java Runtime Environment class libraries. In Java 8 and earlier, they were usually found // in {@code /jre/lib/rt.jar}. For these versions of Java, we // pre-instrumented the entire rt.jar. // - //

In Java 9 and later, the Java Runtime classes have been divided into modules that are + // In Java 9 and later, the Java Runtime classes have been divided into modules that are // usually found in: {@code /jmods/*.jmod}. // - //

With the conversion to modules for Java 9 and beyond, we have elected to pre-instrument only + // With the conversion to modules for Java 9 and beyond, we have elected to pre-instrument only // java.base.jmod and instrument all other Java Runtime (aka JDK) classes dynamically as they are // loaded. // - //

Post Java 8 there are increased security checks when loading JDK classes. In particular, the + // Post Java 8 there are increased security checks when loading JDK classes. In particular, the // core classes contained in the java.base module may not reference anything outside of java.base. // This means we cannot pre-instrument classes in the same manner as was done for Java 8 as this // would introduce external references to the DynComp runtime (DCRuntime.java). // - //

However, we can get around this restriction in the following manner: We create a shadow + // However, we can get around this restriction in the following manner: We create a shadow // DynComp runtime called java.lang.DCRuntime that contains all the public methods of // daikon.dcomp.DCRuntime, but with method bodies that contain only a return statement. We // pre-instrument java.base the same as we would for JDK 8, but change all references to @@ -814,35 +1132,40 @@ boolean isJunitTrigger(String classname, @Identifier String method_name) { * * @return the modified JavaClass */ - public JavaClass instrument_jdk() { + public JavaClass instrument_jdk_class() { - String classname = gen.getClassName(); + String classname = classGen.getClassName(); + // Don't know where I got this idea. They are executed. Don't remember why + // adding dcomp marker causes problems. // Don't instrument annotations. They aren't executed and adding // the marker argument causes subtle errors - if ((gen.getModifiers() & Const.ACC_ANNOTATION) != 0) { + if ((classGen.getModifiers() & ACC_ANNOTATION) != 0) { debug_transform.log("Not instrumenting annotation %s%n", classname); + // Return class file unmodified. // MUST NOT RETURN NULL - return gen.getJavaClass().copy(); + return classGen.getJavaClass().copy(); } int i = classname.lastIndexOf('.'); if (i > 0) { // Don't instrument problem packages. - // See Premain.java for a list and explainations. + // See Premain.java for a list and explanations. String packageName = classname.substring(0, i); if (Premain.problem_packages.contains(packageName)) { debug_transform.log("Skipping problem package %s%n", packageName); - return gen.getJavaClass().copy(); + // Return class file unmodified. + return classGen.getJavaClass().copy(); } } if (Runtime.isJava9orLater()) { // Don't instrument problem classes. - // See Premain.java for a list and explainations. + // See Premain.java for a list and explanations. if (Premain.problem_classes.contains(classname)) { debug_transform.log("Skipping problem class %s%n", classname); - return gen.getJavaClass().copy(); + // Return class file unmodified. + return classGen.getJavaClass().copy(); } dcompRuntimeClassName = "java.lang.DCRuntime"; } @@ -851,27 +1174,27 @@ public JavaClass instrument_jdk() { debug_transform.indent(); // Handle object methods for this class - handle_object(gen); + add_clone_and_tostring_interfaces(classGen); // Have all top-level classes implement our interface - if (gen.getSuperclassName().equals("java.lang.Object")) { + if (classGen.getSuperclassName().equals("java.lang.Object")) { // Add equals method if it doesn't already exist. This ensures // that an instrumented version, equals(Object, DCompMarker), // will be created in this class. - Method eq = gen.containsMethod("equals", "(Ljava/lang/Object;)Z"); + Method eq = classGen.containsMethod("equals", "(Ljava/lang/Object;)Z"); if (eq == null) { debugInstrument.log("Added equals method%n"); - add_equals_method(gen); + add_equals_method(classGen); } // Add DCompInstrumented interface and the required // equals_dcomp_instrumented method. - add_dcomp_interface(gen); + add_dcomp_interface(classGen); } // Process each method - for (Method m : gen.getMethods()) { + for (Method m : classGen.getMethods()) { - tag_frame_local = null; + tagFrameLocal = null; try { // Don't modify class initialization methods. They can't affect // user comparability and there isn't any way to get a second @@ -889,7 +1212,7 @@ public JavaClass instrument_jdk() { InstructionList il = mgen.getInstructionList(); boolean has_code = (il != null); if (has_code) { - setCurrentStackMapTable(mgen, gen.getMajor()); + setCurrentStackMapTable(mgen, classGen.getMajor()); buildUninitializedNewMap(il); } @@ -899,9 +1222,9 @@ public JavaClass instrument_jdk() { if (mgen.isNative()) { // Create Java code that cleans up the tag stack and calls the real native method. - fix_native(gen, mgen); + fix_native(classGen, mgen); has_code = true; - setCurrentStackMapTable(mgen, gen.getMajor()); + setCurrentStackMapTable(mgen, classGen.getMajor()); // Add the DCompMarker parameter to distinguish our version add_dcomp_param(mgen); @@ -914,9 +1237,14 @@ public JavaClass instrument_jdk() { // Instrument the method if (has_code) { // Create the local to store the tag frame for this method - tag_frame_local = create_tag_frame_local(mgen); + tagFrameLocal = createTagFrameLocal(mgen); build_exception_handler(mgen); - instrument_method(mgen); + assert stackMapTable != null : "@AssumeAssertion(nullness): ??"; + assert tagFrameLocal != null + : "@AssumeAssertion(nullness): set above and not modified by" + + " build_exception_handler"; + instrumentMethod(mgen); + assert stackMapTable != null : "@AssumeAssertion(nullness): ??"; install_exception_handler(mgen); } } @@ -952,12 +1280,12 @@ public JavaClass instrument_jdk() { il = mgen.getInstructionList(); InstructionHandle end = il.getEnd(); int length = end.getPosition() + end.getInstruction().getLength(); - if (length >= Const.MAX_CODE_SIZE) { + if (length >= MAX_CODE_SIZE) { throw new ClassGenException( - "Code array too big: must be smaller than " + Const.MAX_CODE_SIZE + " bytes."); + "Code array too big: must be smaller than " + MAX_CODE_SIZE + " bytes."); } } - gen.addMethod(mgen.getMethod()); + classGen.addMethod(mgen.getMethod()); } catch (Exception e) { String s = e.getMessage(); if (s == null) { @@ -974,12 +1302,12 @@ public JavaClass instrument_jdk() { // first, restore unmodified method mgen = new MethodGen(m, classname, pool); // restore StackMapTable - setCurrentStackMapTable(mgen, gen.getMajor()); + setCurrentStackMapTable(mgen, classGen.getMajor()); // Add the DCompMarker parameter add_dcomp_param(mgen); remove_local_variable_type_table(mgen); // try again - gen.addMethod(mgen.getMethod()); + classGen.addMethod(mgen.getMethod()); } else { throw e; } @@ -990,7 +1318,10 @@ public JavaClass instrument_jdk() { if (debugInstrument.enabled) { t.printStackTrace(); } - skip_method(mgen); + // TODO: Is it guaranteed that mgen is non-null by the time control reaches here? + if (mgen != null) { + skip_method(mgen); + } if (quit_if_error) { throw new Error("Error processing " + classname + "." + m.getName(), t); } else { @@ -1000,18 +1331,21 @@ public JavaClass instrument_jdk() { } } - // Add tag accessor methods for each primitive in the class - create_tag_accessors(gen); + assert mgen != null + : "@AssumeAssertion(nullness)"; // Bug? mgen could be null if the class has no methods + + // Add tag accessor methods for each primitive in the class. + create_tag_accessors(classGen); // We don't need to track class initialization in the JDK because // that is only used when printing comparability which is only done - // for client classes - // track_class_init(); + // for client classes. + // trackClass_init(); debug_transform.exdent(); debug_transform.log("Instrumentation complete: %s%n", classname); - return gen.getJavaClass().copy(); + return classGen.getJavaClass().copy(); } /** @@ -1019,9 +1353,11 @@ public JavaClass instrument_jdk() { * * @param mgen MethodGen for the method to be instrumented */ - public void instrument_method(MethodGen mgen) { + @RequiresNonNull({"stackMapTable", "tagFrameLocal"}) + @EnsuresNonNull("insertion_placeholder") + public void instrumentMethod(MethodGen mgen) { - // Because the tag_frame_local is active for the entire method + // Because the tagFrameLocal is active for the entire method // and its creation will change the state of the locals layout, // we need to insert the code to initialize it now so that the // stack anaylsis we are about to do is correct for potential @@ -1080,6 +1416,9 @@ public void instrument_method(MethodGen mgen) { // Get the stack information stack = stack_types.get(handle_offsets[index++]); + assert tagFrameLocal != null + : "@AssumeAssertion(nullness): precondition and not changed since"; + // Get the translation for this instruction (if any) new_il = xform_inst(mgen, ih, stack); @@ -1092,7 +1431,7 @@ public void instrument_method(MethodGen mgen) { // If the modified method is now too large, we quit instrumenting the method // and will rediscover the problem in the main instrumentation loop above // and deal with it there. - if (ih.getPosition() >= Const.MAX_CODE_SIZE) { + if (ih.getPosition() >= MAX_CODE_SIZE) { break; } @@ -1104,18 +1443,18 @@ public void instrument_method(MethodGen mgen) { * Adds the method name and containing class name to {@code skip_methods}, the list of * uninstrumented methods. * - * @param mgen method to add to skipped_methods list + * @param m method to add to skipped_methods list */ - void skip_method(MethodGen mgen) { - skipped_methods.add(mgen.getClassName() + "." + mgen.getName()); + void skip_method(MethodGen m) { + skipped_methods.add(m.getClassName() + "." + m.getName()); } /** - * Returns the list of uninstrumented methods. (Note: instrument_jdk() needs to have been called - * first.) + * Returns the list of uninstrumented methods. (Note: instrument_jdk_class() needs to have been + * called first.) */ public List get_skipped_methods() { - return new ArrayList(skipped_methods); + return new ArrayList<>(skipped_methods); } /** @@ -1134,7 +1473,7 @@ public void build_exception_handler(MethodGen mgen) { il.append(new DUP()); il.append( ifact.createInvoke( - dcompRuntimeClassName, "exception_exit", Type.VOID, object_arg, Const.INVOKESTATIC)); + dcompRuntimeClassName, "exception_exit", CD_void, object_arg, INVOKESTATIC)); il.append(new ATHROW()); add_exception_handler(mgen, il); @@ -1143,8 +1482,8 @@ public void build_exception_handler(MethodGen mgen) { /** Adds a try/catch block around the entire method. */ public void add_exception_handler(MethodGen mgen, InstructionList catch_il) { - // methods (constructors) turn out to be problematic - // for adding a whole method exception handler. The start of + // methods (constructors) are problematic + // for adding a whole-method exception handler. The start of // the exception handler should be after the primary object is // initialized - but this is hard to determine without a full // analysis of the code. Hence, we just skip these methods. @@ -1163,22 +1502,29 @@ public void add_exception_handler(MethodGen mgen, InstructionList catch_il) { // This is just a temporary handler to get the start and end // address tracked as we make code modifications. global_catch_il = catch_il; - global_exception_handler = new CodeExceptionGen(start, end, null, throwable); + @SuppressWarnings("nullness:argument") // looks like a genuine defect here in the call + CodeExceptionGen global_exception_handler_tmp = + new CodeExceptionGen(start, end, null, CD_Throwable); + global_exception_handler = global_exception_handler_tmp; } /** Adds a try/catch block around the entire method. */ + @SuppressWarnings("nullness") // calls to side-effecting methods + @RequiresNonNull({"stackMapTable"}) public void install_exception_handler(MethodGen mgen) { if (global_catch_il == null) { return; } + assert global_exception_handler != null : "@AssumeAssertion(nullness): ??"; + InstructionList cur_il = mgen.getInstructionList(); InstructionHandle start = global_exception_handler.getStartPC(); InstructionHandle end = global_exception_handler.getEndPC(); InstructionHandle exc = cur_il.append(global_catch_il); cur_il.setPositions(); - mgen.addExceptionHandler(start, end, exc, throwable); + mgen.addExceptionHandler(start, end, exc, CD_Throwable); // discard temporary handler global_catch_il = null; global_exception_handler = null; @@ -1204,8 +1550,7 @@ public void install_exception_handler(MethodGen mgen) { StackMapType[] param_map_types = new StackMapType[param_types.length + arg_index]; if (!mgen.isStatic()) { param_map_types[0] = - new StackMapType( - Const.ITEM_Object, pool.addClass(mgen.getClassName()), pool.getConstantPool()); + new StackMapType(ITEM_Object, pool.addClass(mgen.getClassName()), pool.getConstantPool()); } for (int ii = 0; ii < param_types.length; ii++) { param_map_types[arg_index++] = generateStackMapTypeFromType(param_types[ii]); @@ -1214,11 +1559,11 @@ public void install_exception_handler(MethodGen mgen) { StackMapEntry map_entry; StackMapType stack_map_type = new StackMapType( - Const.ITEM_Object, pool.addClass(throwable.getClassName()), pool.getConstantPool()); + ITEM_Object, pool.addClass(CD_Throwable.getClassName()), pool.getConstantPool()); StackMapType[] stack_map_types = {stack_map_type}; map_entry = new StackMapEntry( - Const.FULL_FRAME, map_offset, param_map_types, stack_map_types, pool.getConstantPool()); + FULL_FRAME, map_offset, param_map_types, stack_map_types, pool.getConstantPool()); int orig_size = stackMapTable.length; StackMapEntry[] new_stack_map_table = new StackMapEntry[orig_size + 1]; @@ -1228,12 +1573,15 @@ public void install_exception_handler(MethodGen mgen) { } /** - * Adds the code to create the tag frame to the beginning of the method. This needs to be before - * the call to DCRuntime.enter (since it passed to that method). + * Generates the code to create the tag frame for this method and store it in tagFrameLocal. This + * needs to be before the call to DCRuntime.enter (since it is passed to that method). */ + @SuppressWarnings("nullness") // calls to side-effecting methods + @RequiresNonNull({"tagFrameLocal", "stackMapTable"}) + @EnsuresNonNull("insertion_placeholder") public void add_create_tag_frame(MethodGen mgen) { - InstructionList nl = create_tag_frame(mgen, tag_frame_local); + InstructionList nl = create_tag_frame(mgen, tagFrameLocal); // We add a temporary NOP at the end of the create_tag_frame // code that we will replace with runtime initization code @@ -1285,11 +1633,10 @@ public void add_create_tag_frame(MethodGen mgen) { // Insert a new StackMapEntry at the beginning of the table // that adds the tag_frame variable. - StackMapType tag_frame_type = generateStackMapTypeFromType(object_arr); + StackMapType tag_frame_type = generateStackMapTypeFromType(CD_Object_array); StackMapType[] stack_map_type_arr = {tag_frame_type}; new_stack_map_table[0] = - new StackMapEntry( - Const.APPEND_FRAME, len_code, stack_map_type_arr, null, pool.getConstantPool()); + new StackMapEntry(APPEND_FRAME, len_code, stack_map_type_arr, null, pool.getConstantPool()); // We can just copy the rest of the stack frames over as the FULL_FRAME // ones were already updated when the tag_frame variable was allocated. @@ -1301,12 +1648,13 @@ public void add_create_tag_frame(MethodGen mgen) { } /** - * Adds the call to DCRuntime.enter to the beginning of the method. + * Adds a call to DCRuntime.enter at the beginning of the method. * * @param mgen method to modify - * @param mi MethodInfo for method + * @param mi MethodInfo for the given method's code * @param method_info_index index for MethodInfo */ + @RequiresNonNull({"tagFrameLocal", "insertion_placeholder"}) public void add_enter(MethodGen mgen, MethodInfo mi, int method_info_index) { InstructionList il = mgen.getInstructionList(); replaceInstructions( @@ -1319,56 +1667,61 @@ public void add_enter(MethodGen mgen, MethodInfo mi, int method_info_index) { * @param mgen method to modify * @return LocalVariableGen for the tag_frame local */ - LocalVariableGen create_tag_frame_local(MethodGen mgen) { - return create_method_scope_local(mgen, "dcomp_tag_frame$5a", object_arr); + LocalVariableGen createTagFrameLocal(MethodGen mgen) { + return create_method_scope_local(mgen, "dcomp_tag_frame$5a", CD_Object_array); } /** - * Creates code to create the tag frame for this method and store it in tag_frame_local. + * Creates code to create the tag frame for this method and store it in tagFrameLocal. * * @param mgen method to modify - * @param tag_frame_local LocalVariableGen for the tag_frame local - * @return InstructionList for tag_frame setup code + * @param tagFrameLocal LocalVariableGen for the tag_frame local + * @return instruction list for tag_frame setup code */ - InstructionList create_tag_frame(MethodGen mgen, LocalVariableGen tag_frame_local) { + InstructionList create_tag_frame(MethodGen mgen, LocalVariableGen tagFrameLocal) { Type paramTypes[] = mgen.getArgumentTypes(); - // Determine the offset of the first argument in the frame - int offset = 1; - if (mgen.isStatic()) { - offset = 0; - } + // Determine the offset of the first argument in the frame. + int offset = mgen.isStatic() ? 0 : 1; // allocate an extra slot to save the tag frame depth for debugging int frame_size = mgen.getMaxLocals() + 1; // unsigned byte max = 255. minus the character '0' (decimal 48) // Largest frame size noted so far is 123. - assert frame_size < 207 : frame_size + " " + mgen.getClassName() + "." + mgen.getName(); + if (frame_size > 206) { + throw new DynCompError( + "method too large (" + + frame_size + + ") to instrument: " + + mgen.getClassName() + + "." + + mgen.getName()); + } String params = Character.toString((char) (frame_size + '0')); // Character.forDigit (frame_size, Character.MAX_RADIX); - List plist = new ArrayList<>(); + List paramList = new ArrayList<>(); for (Type paramType : paramTypes) { if (paramType instanceof BasicType) { - plist.add(offset); + paramList.add(offset); } offset += paramType.getSize(); } - for (int ii = plist.size() - 1; ii >= 0; ii--) { - char tmpChar = (char) (plist.get(ii) + '0'); + for (int ii = paramList.size() - 1; ii >= 0; ii--) { + char tmpChar = (char) (paramList.get(ii) + '0'); params += tmpChar; - // Character.forDigit (plist.get(ii), Character.MAX_RADIX); + // Character.forDigit (paramList.get(ii), Character.MAX_RADIX); } - // Create code to create/init the tag frame and store in tag_frame_local + // Create code to create/init the tag frame and store in tagFrameLocal. InstructionList il = new InstructionList(); il.append(ifact.createConstant(params)); il.append( ifact.createInvoke( - dcompRuntimeClassName, "create_tag_frame", object_arr, string_arg, Const.INVOKESTATIC)); - il.append(InstructionFactory.createStore(object_arr, tag_frame_local.getIndex())); - debugInstrument.log("Store Tag frame local at index %d%n", tag_frame_local.getIndex()); + dcompRuntimeClassName, "create_tag_frame", CD_Object_array, string_arg, INVOKESTATIC)); + il.append(InstructionFactory.createStore(CD_Object_array, tagFrameLocal.getIndex())); + debugInstrument.log("Store Tag frame local at index %d%n", tagFrameLocal.getIndex()); return il; } @@ -1382,8 +1735,10 @@ InstructionList create_tag_frame(MethodGen mgen, LocalVariableGen tag_frame_loca * @param method_info_index index for MethodInfo * @param enterOrExit the method to invoke: "enter" or "exit" * @param line source line number if type is exit - * @return InstructionList for the enter or exit code + * @return instruction list for the enter or exit code */ + @SuppressWarnings("nullness") // calls to side-effecting methods + @RequiresNonNull("tagFrameLocal") InstructionList callEnterOrExit( MethodGen mgen, int method_info_index, String enterOrExit, int line) { @@ -1391,41 +1746,38 @@ InstructionList callEnterOrExit( Type[] paramTypes = mgen.getArgumentTypes(); // Push the tag frame - il.append(InstructionFactory.createLoad(object_arr, tag_frame_local.getIndex())); + il.append(InstructionFactory.createLoad(CD_Object_array, tagFrameLocal.getIndex())); - // Push the object. Null if this is a static method or a constructor + // Push the object. Push null if this is a static method or a constructor. if (mgen.isStatic() || (enterOrExit.equals("enter") && BcelUtil.isConstructor(mgen))) { il.append(new ACONST_NULL()); } else { // must be an instance method - il.append(InstructionFactory.createLoad(Type.OBJECT, 0)); + il.append(InstructionFactory.createLoad(CD_Object, 0)); } - // Determine the offset of the first parameter - int param_offset = 1; - if (mgen.isStatic()) { - param_offset = 0; - } + // The offset of the first parameter. + int param_offset = mgen.isStatic() ? 0 : 1; // Push the MethodInfo index il.append(ifact.createConstant(method_info_index)); // Create an array of objects with elements for each parameter il.append(ifact.createConstant(paramTypes.length)); - il.append(ifact.createNewArray(Type.OBJECT, (short) 1)); + il.append(ifact.createNewArray(CD_Object, (short) 1)); // Put each argument into the array int param_index = param_offset; for (int ii = 0; ii < paramTypes.length; ii++) { - il.append(InstructionFactory.createDup(object_arr.getSize())); + il.append(InstructionFactory.createDup(CD_Object_array.getSize())); il.append(ifact.createConstant(ii)); Type at = paramTypes[ii]; if (at instanceof BasicType) { il.append(new ACONST_NULL()); // il.append (createPrimitiveWrapper (c, at, param_index)); } else { // must be reference of some sort - il.append(InstructionFactory.createLoad(Type.OBJECT, param_index)); + il.append(InstructionFactory.createLoad(CD_Object, param_index)); } - il.append(InstructionFactory.createArrayStore(Type.OBJECT)); + il.append(InstructionFactory.createArrayStore(CD_Object)); param_index += at.getSize(); } @@ -1434,7 +1786,7 @@ InstructionList callEnterOrExit( // If the return value is a primitive, wrap it in the appropriate run-time wrapper. if (enterOrExit.equals("exit")) { Type returnType = mgen.getReturnType(); - if (returnType == Type.VOID) { + if (returnType == CD_void) { il.append(new ACONST_NULL()); } else { LocalVariableGen return_local = get_return_local(mgen, returnType); @@ -1442,7 +1794,7 @@ InstructionList callEnterOrExit( il.append(new ACONST_NULL()); // il.append (createPrimitiveWrapper (c, returnType, return_local.getIndex())); } else { - il.append(InstructionFactory.createLoad(Type.OBJECT, return_local.getIndex())); + il.append(InstructionFactory.createLoad(CD_Object, return_local.getIndex())); } } @@ -1450,17 +1802,17 @@ InstructionList callEnterOrExit( il.append(ifact.createConstant(line)); } - // Call the specified method - Type[] method_params; + // Call the specified method. + Type[] methodParams; if (enterOrExit.equals("exit")) { - method_params = - new Type[] {object_arr, Type.OBJECT, Type.INT, object_arr, Type.OBJECT, Type.INT}; + methodParams = + new Type[] {CD_Object_array, CD_Object, CD_int, CD_Object_array, CD_Object, CD_int}; } else { - method_params = new Type[] {object_arr, Type.OBJECT, Type.INT, object_arr}; + methodParams = new Type[] {CD_Object_array, CD_Object, CD_int, CD_Object_array}; } il.append( ifact.createInvoke( - dcompRuntimeClassName, enterOrExit, Type.VOID, method_params, Const.INVOKESTATIC)); + dcompRuntimeClassName, enterOrExit, CD_void, methodParams, INVOKESTATIC)); return il; } @@ -1473,6 +1825,7 @@ InstructionList callEnterOrExit( * @param ih handle of Instruction to translate * @param stack current contents of the stack */ + @RequiresNonNull("tagFrameLocal") @Nullable InstructionList xform_inst(MethodGen mgen, InstructionHandle ih, OperandStack stack) { Instruction inst = ih.getInstruction(); @@ -1481,180 +1834,155 @@ InstructionList callEnterOrExit( // Replace the object comparison instructions with a call to // DCRuntime.object_eq or DCRuntime.object_ne. Those methods - // return a boolean which is used in a ifeq/ifne instruction - case Const.IF_ACMPEQ: - return object_comparison((BranchInstruction) inst, "object_eq", Const.IFNE); - case Const.IF_ACMPNE: - return object_comparison((BranchInstruction) inst, "object_ne", Const.IFNE); + // return a boolean which is used in a ifeq/ifne instruction. + case IF_ACMPEQ: + return object_comparison((BranchInstruction) inst, "object_eq", IFNE); + case IF_ACMPNE: + return object_comparison((BranchInstruction) inst, "object_ne", IFNE); // These instructions compare the integer on the top of the stack // to zero. Nothing is made comparable by this, so we need only // discard the tag on the top of the stack. - case Const.IFEQ: - case Const.IFNE: - case Const.IFLT: - case Const.IFGE: - case Const.IFGT: - case Const.IFLE: + case IFEQ: + case IFNE: + case IFLT: + case IFGE: + case IFGT: + case IFLE: { return discard_tag_code(inst, 1); } // Instanceof pushes either 0 or 1 on the stack depending on whether - // the object on top of stack is of the specified type. We push a + // the object on top of stack is of the specified type. The DynComp runtime will push a + // new, unique // tag for a constant, since nothing is made comparable by this. - case Const.INSTANCEOF: - return build_il(dcr_call("push_const", Type.VOID, Type.NO_ARGS), inst); + case INSTANCEOF: + return build_il(dcr_call("push_const", CD_void, noArgsSig), inst); // Duplicates the item on the top of stack. If the value on the // top of the stack is a primitive, we need to do the same on the // tag stack. Otherwise, we need do nothing. - case Const.DUP: - { - return dup_tag(inst, stack); - } + case DUP: + return dup_tag(inst, stack); // Duplicates the item on the top of the stack and inserts it 2 // values down in the stack. If the value at the top of the stack - // is not a primitive, there is nothing to do here. If the second + // is not a primitive, there is nothing to do. If the second // value is not a primitive, then we need only to insert the duped - // value down 1 on the tag stack (which contains only primitives) - case Const.DUP_X1: - { - return dup_x1_tag(inst, stack); - } + // value down 1 on the tag stack (which contains only primitives). + case DUP_X1: + return dup_x1_tag(inst, stack); // Duplicates either the top 2 category 1 values or a single // category 2 value and inserts it 2 or 3 values down on the // stack. - case Const.DUP2_X1: - { - return dup2_x1_tag(inst, stack); - } + case DUP2_X1: + return dup2_x1_tag(inst, stack); // Duplicate either one category 2 value or two category 1 values. - case Const.DUP2: - { - return dup2_tag(inst, stack); - } + case DUP2: + return dup2_tag(inst, stack); // Dup the category 1 value on the top of the stack and insert it either // two or three values down on the stack. - case Const.DUP_X2: - { - return dup_x2(inst, stack); - } + case DUP_X2: + return dup_x2(inst, stack); - case Const.DUP2_X2: - { - return dup2_x2(inst, stack); - } + case DUP2_X2: + return dup2_x2(inst, stack); // Pop instructions discard the top of the stack. We want to discard // the top of the tag stack iff the item on the top of the stack is a // primitive. - case Const.POP: - { - return pop_tag(inst, stack); - } + case POP: + return pop_tag(inst, stack); // Pops either the top 2 category 1 values or a single category 2 value // from the top of the stack. We must do the same to the tag stack // if the values are primitives. - case Const.POP2: - { - return pop2_tag(inst, stack); - } + case POP2: + return pop2_tag(inst, stack); // Swaps the two category 1 types on the top of the stack. We need // to swap the top of the tag stack if the two top elements on the // real stack are primitives. - case Const.SWAP: - { - return swap_tag(inst, stack); - } - - case Const.IF_ICMPEQ: - case Const.IF_ICMPGE: - case Const.IF_ICMPGT: - case Const.IF_ICMPLE: - case Const.IF_ICMPLT: - case Const.IF_ICMPNE: - { - return build_il(dcr_call("cmp_op", Type.VOID, Type.NO_ARGS), inst); - } - - case Const.GETFIELD: - { - return load_store_field(mgen, (GETFIELD) inst); - } - - case Const.PUTFIELD: - { - return load_store_field(mgen, (PUTFIELD) inst); - } - - case Const.GETSTATIC: - { - return load_store_field(mgen, ((GETSTATIC) inst)); - } - - case Const.PUTSTATIC: + case SWAP: + return swap_tag(inst, stack); + + case IF_ICMPEQ: + case IF_ICMPGE: + case IF_ICMPGT: + case IF_ICMPLE: + case IF_ICMPLT: + case IF_ICMPNE: { - return load_store_field(mgen, ((PUTSTATIC) inst)); + return build_il(dcr_call("cmp_op", CD_void, noArgsSig), inst); } - case Const.DLOAD: - case Const.DLOAD_0: - case Const.DLOAD_1: - case Const.DLOAD_2: - case Const.DLOAD_3: - case Const.FLOAD: - case Const.FLOAD_0: - case Const.FLOAD_1: - case Const.FLOAD_2: - case Const.FLOAD_3: - case Const.ILOAD: - case Const.ILOAD_0: - case Const.ILOAD_1: - case Const.ILOAD_2: - case Const.ILOAD_3: - case Const.LLOAD: - case Const.LLOAD_0: - case Const.LLOAD_1: - case Const.LLOAD_2: - case Const.LLOAD_3: + case GETFIELD: + return load_store_field(mgen, (GETFIELD) inst); + + case PUTFIELD: + return load_store_field(mgen, (PUTFIELD) inst); + + case GETSTATIC: + return load_store_field(mgen, ((GETSTATIC) inst)); + + case PUTSTATIC: + return load_store_field(mgen, ((PUTSTATIC) inst)); + + case DLOAD: + case DLOAD_0: + case DLOAD_1: + case DLOAD_2: + case DLOAD_3: + case FLOAD: + case FLOAD_0: + case FLOAD_1: + case FLOAD_2: + case FLOAD_3: + case ILOAD: + case ILOAD_0: + case ILOAD_1: + case ILOAD_2: + case ILOAD_3: + case LLOAD: + case LLOAD_0: + case LLOAD_1: + case LLOAD_2: + case LLOAD_3: { - return load_store_local((LoadInstruction) inst, tag_frame_local, "push_local_tag"); + return load_store_local((LoadInstruction) inst, tagFrameLocal, "push_local_tag"); } - case Const.DSTORE: - case Const.DSTORE_0: - case Const.DSTORE_1: - case Const.DSTORE_2: - case Const.DSTORE_3: - case Const.FSTORE: - case Const.FSTORE_0: - case Const.FSTORE_1: - case Const.FSTORE_2: - case Const.FSTORE_3: - case Const.ISTORE: - case Const.ISTORE_0: - case Const.ISTORE_1: - case Const.ISTORE_2: - case Const.ISTORE_3: - case Const.LSTORE: - case Const.LSTORE_0: - case Const.LSTORE_1: - case Const.LSTORE_2: - case Const.LSTORE_3: + case DSTORE: + case DSTORE_0: + case DSTORE_1: + case DSTORE_2: + case DSTORE_3: + case FSTORE: + case FSTORE_0: + case FSTORE_1: + case FSTORE_2: + case FSTORE_3: + case ISTORE: + case ISTORE_0: + case ISTORE_1: + case ISTORE_2: + case ISTORE_3: + case LSTORE: + case LSTORE_0: + case LSTORE_1: + case LSTORE_2: + case LSTORE_3: { - return load_store_local((StoreInstruction) inst, tag_frame_local, "pop_local_tag"); + return load_store_local((StoreInstruction) inst, tagFrameLocal, "pop_local_tag"); } - case Const.LDC: - case Const.LDC_W: - case Const.LDC2_W: + case LDC: + case LDC_W: + case LDC2_W: { return ldc_tag(inst, stack); } @@ -1662,72 +1990,72 @@ InstructionList callEnterOrExit( // Push the tag for the array onto the tag stack. This causes // anything comparable to the length to be comparable to the array // as an index. - case Const.ARRAYLENGTH: + case ARRAYLENGTH: { return array_length(inst); } - case Const.BIPUSH: - case Const.SIPUSH: - case Const.DCONST_0: - case Const.DCONST_1: - case Const.FCONST_0: - case Const.FCONST_1: - case Const.FCONST_2: - case Const.ICONST_0: - case Const.ICONST_1: - case Const.ICONST_2: - case Const.ICONST_3: - case Const.ICONST_4: - case Const.ICONST_5: - case Const.ICONST_M1: - case Const.LCONST_0: - case Const.LCONST_1: + case BIPUSH: + case SIPUSH: + case DCONST_0: + case DCONST_1: + case FCONST_0: + case FCONST_1: + case FCONST_2: + case ICONST_0: + case ICONST_1: + case ICONST_2: + case ICONST_3: + case ICONST_4: + case ICONST_5: + case ICONST_M1: + case LCONST_0: + case LCONST_1: { - return build_il(dcr_call("push_const", Type.VOID, Type.NO_ARGS), inst); + return build_il(dcr_call("push_const", CD_void, noArgsSig), inst); } // Primitive Binary operators. Each is augmented with a call to // DCRuntime.binary_tag_op that merges the tags and updates the tag // Stack. - case Const.DADD: - case Const.DCMPG: - case Const.DCMPL: - case Const.DDIV: - case Const.DMUL: - case Const.DREM: - case Const.DSUB: - case Const.FADD: - case Const.FCMPG: - case Const.FCMPL: - case Const.FDIV: - case Const.FMUL: - case Const.FREM: - case Const.FSUB: - case Const.IADD: - case Const.IAND: - case Const.IDIV: - case Const.IMUL: - case Const.IOR: - case Const.IREM: - case Const.ISHL: - case Const.ISHR: - case Const.ISUB: - case Const.IUSHR: - case Const.IXOR: - case Const.LADD: - case Const.LAND: - case Const.LCMP: - case Const.LDIV: - case Const.LMUL: - case Const.LOR: - case Const.LREM: - case Const.LSHL: - case Const.LSHR: - case Const.LSUB: - case Const.LUSHR: - case Const.LXOR: - return build_il(dcr_call("binary_tag_op", Type.VOID, Type.NO_ARGS), inst); + case DADD: + case DCMPG: + case DCMPL: + case DDIV: + case DMUL: + case DREM: + case DSUB: + case FADD: + case FCMPG: + case FCMPL: + case FDIV: + case FMUL: + case FREM: + case FSUB: + case IADD: + case IAND: + case IDIV: + case IMUL: + case IOR: + case IREM: + case ISHL: + case ISHR: + case ISUB: + case IUSHR: + case IXOR: + case LADD: + case LAND: + case LCMP: + case LDIV: + case LMUL: + case LOR: + case LREM: + case LSHL: + case LSHR: + case LSUB: + case LUSHR: + case LXOR: + return build_il(dcr_call("binary_tag_op", CD_void, noArgsSig), inst); // Computed jump based on the int on the top of stack. Since that int // is not made comparable to anything, we just discard its tag. One @@ -1735,76 +2063,70 @@ InstructionList callEnterOrExit( // the jump table. But the tags for those values are not available. // And since they are all constants, its not clear how interesting it // would be anyway. - case Const.LOOKUPSWITCH: - case Const.TABLESWITCH: + case LOOKUPSWITCH: + case TABLESWITCH: return discard_tag_code(inst, 1); // Make the integer argument to ANEWARRAY comparable to the new // array's index. - case Const.ANEWARRAY: - case Const.NEWARRAY: - { - return new_array(inst); - } + case ANEWARRAY: + case NEWARRAY: + return new_array(inst); // If the new array has 2 dimensions, make the integer arguments // comparable to the corresponding indices of the new array. // For any other number of dimensions, discard the tags for the // arguments. - case Const.MULTIANEWARRAY: - { - return multi_newarray_dc(inst); - } + case MULTIANEWARRAY: + return multi_newarray_dc(inst); // Mark the array and its index as comparable. Also for primitives, // push the tag of the array element on the tag stack - case Const.AALOAD: - case Const.BALOAD: - case Const.CALOAD: - case Const.DALOAD: - case Const.FALOAD: - case Const.IALOAD: - case Const.LALOAD: - case Const.SALOAD: - { - return array_load(inst); - } + case AALOAD: + case BALOAD: + case CALOAD: + case DALOAD: + case FALOAD: + case IALOAD: + case LALOAD: + case SALOAD: + return array_load(inst); // Mark the array and its index as comparable. For primitives, store // the tag for the value on the top of the stack in the tag storage // for the array. - case Const.AASTORE: - return array_store(inst, "aastore", Type.OBJECT); - case Const.BASTORE: + case AASTORE: + return array_store(inst, "aastore", CD_Object); + case BASTORE: // The JVM uses bastore for both byte and boolean. // We need to differentiate. Type arr_type = stack.peek(2); if (arr_type.getSignature().equals("[Z")) { - return array_store(inst, "zastore", Type.BOOLEAN); + return array_store(inst, "zastore", CD_boolean); } else { - return array_store(inst, "bastore", Type.BYTE); + return array_store(inst, "bastore", CD_byte); } - case Const.CASTORE: - return array_store(inst, "castore", Type.CHAR); - case Const.DASTORE: - return array_store(inst, "dastore", Type.DOUBLE); - case Const.FASTORE: - return array_store(inst, "fastore", Type.FLOAT); - case Const.IASTORE: - return array_store(inst, "iastore", Type.INT); - case Const.LASTORE: - return array_store(inst, "lastore", Type.LONG); - case Const.SASTORE: - return array_store(inst, "sastore", Type.SHORT); + case CASTORE: + return array_store(inst, "castore", CD_char); + case DASTORE: + return array_store(inst, "dastore", CD_double); + case FASTORE: + return array_store(inst, "fastore", CD_float); + case IASTORE: + return array_store(inst, "iastore", CD_int); + case LASTORE: + return array_store(inst, "lastore", CD_long); + case SASTORE: + return array_store(inst, "sastore", CD_short); // Prefix the return with a call to the correct normal_exit method // to handle the tag stack - case Const.ARETURN: - case Const.DRETURN: - case Const.FRETURN: - case Const.IRETURN: - case Const.LRETURN: - case Const.RETURN: + case ARETURN: + case DRETURN: + case FRETURN: + case IRETURN: + case LRETURN: + case RETURN: { return return_tag(mgen, inst); } @@ -1813,64 +2135,69 @@ InstructionList callEnterOrExit( // to call the instrumented version (with the DCompMarker argument). // Calls to uninstrumented code (rare) discard primitive arguments // from the tag stack and produce an arbitrary return tag. - case Const.INVOKESTATIC: - case Const.INVOKEVIRTUAL: - case Const.INVOKESPECIAL: - case Const.INVOKEINTERFACE: - case Const.INVOKEDYNAMIC: - return handleInvoke((InvokeInstruction) inst); + case INVOKESTATIC: + case INVOKEVIRTUAL: + case INVOKESPECIAL: + case INVOKEINTERFACE: + case INVOKEDYNAMIC: + { + assert this.mgen != null + : "@AssumeAssertion(nullness): bug: local `mgen` is non-null, " + + "but `this.mgen` has not been set"; + return handleInvoke((InvokeInstruction) inst); + } // Throws an exception. This clears the operand stack of the current // frame. We need to clear the tag stack as well. - case Const.ATHROW: - return build_il(dcr_call("throw_op", Type.VOID, Type.NO_ARGS), inst); + case ATHROW: + return build_il(dcr_call("throw_op", CD_void, noArgsSig), inst); // Opcodes that don't need any modifications. Here for reference - case Const.ACONST_NULL: - case Const.ALOAD: - case Const.ALOAD_0: - case Const.ALOAD_1: - case Const.ALOAD_2: - case Const.ALOAD_3: - case Const.ASTORE: - case Const.ASTORE_0: - case Const.ASTORE_1: - case Const.ASTORE_2: - case Const.ASTORE_3: - case Const.CHECKCAST: - case Const.D2F: // double to float - case Const.D2I: // double to integer - case Const.D2L: // double to long - case Const.DNEG: // Negate double on top of stack - case Const.F2D: // float to double - case Const.F2I: // float to integer - case Const.F2L: // float to long - case Const.FNEG: // Negate float on top of stack - case Const.GOTO: - case Const.GOTO_W: - case Const.I2B: // integer to byte - case Const.I2C: // integer to char - case Const.I2D: // integer to double - case Const.I2F: // integer to float - case Const.I2L: // integer to long - case Const.I2S: // integer to short - case Const.IFNONNULL: - case Const.IFNULL: - case Const.IINC: // increment local variable by a constant - case Const.INEG: // negate integer on top of stack - case Const.JSR: // pushes return address on the stack, but that + case ACONST_NULL: + case ALOAD: + case ALOAD_0: + case ALOAD_1: + case ALOAD_2: + case ALOAD_3: + case ASTORE: + case ASTORE_0: + case ASTORE_1: + case ASTORE_2: + case ASTORE_3: + case CHECKCAST: + case D2F: // double to float + case D2I: // double to integer + case D2L: // double to long + case DNEG: // Negate double on top of stack + case F2D: // float to double + case F2I: // float to integer + case F2L: // float to long + case FNEG: // Negate float on top of stack + case GOTO: + case GOTO_W: + case I2B: // integer to byte + case I2C: // integer to char + case I2D: // integer to double + case I2F: // integer to float + case I2L: // integer to long + case I2S: // integer to short + case IFNONNULL: + case IFNULL: + case IINC: // increment local variable by a constant + case INEG: // negate integer on top of stack + case JSR: // pushes return address on the stack, but that // is thought of as an object, so we don't need // a tag for it. - case Const.JSR_W: - case Const.L2D: // long to double - case Const.L2F: // long to float - case Const.L2I: // long to int - case Const.LNEG: // negate long on top of stack - case Const.MONITORENTER: - case Const.MONITOREXIT: - case Const.NEW: - case Const.NOP: - case Const.RET: // this is the internal JSR return + case JSR_W: + case L2D: // long to double + case L2F: // long to float + case L2I: // long to int + case LNEG: // negate long on top of stack + case MONITORENTER: + case MONITOREXIT: + case NEW: + case NOP: + case RET: // this is the internal JSR return return null; // Make sure we didn't miss anything @@ -1888,12 +2215,13 @@ InstructionList callEnterOrExit( * @param mi MethodInfo for method * @param method_info_index index for MethodInfo */ + @RequiresNonNull("tagFrameLocal") void add_exit(MethodGen mgen, MethodInfo mi, int method_info_index) { // Iterator over all of the exit line numbers for this method, in order. // We will read one element from it each time that we encounter a // return instruction. - Iterator exit_iter = mi.exit_locations.iterator(); + Iterator exitLocationIter = mi.exit_locations.iterator(); // Loop through each instruction, looking for return instructions. InstructionList il = mgen.getInstructionList(); @@ -1908,12 +2236,15 @@ void add_exit(MethodGen mgen, MethodInfo mi, int method_info_index) { if (inst instanceof ReturnInstruction) { Type type = mgen.getReturnType(); InstructionList new_il = new InstructionList(); - if (type != Type.VOID) { + if (type != CD_void) { LocalVariableGen return_loc = get_return_local(mgen, type); new_il.append(InstructionFactory.createDup(type.getSize())); new_il.append(InstructionFactory.createStore(type, return_loc.getIndex())); } - new_il.append(callEnterOrExit(mgen, method_info_index, "exit", exit_iter.next())); + int exitLoc = exitLocationIter.next(); + assert tagFrameLocal != null + : "@AssumeAssertion(nullness): not modified since method entry"; + new_il.append(callEnterOrExit(mgen, method_info_index, "exit", exitLoc)); new_il.append(inst); replaceInstructions(mgen, il, ih, new_il); } @@ -1926,7 +2257,7 @@ void add_exit(MethodGen mgen, MethodInfo mi, int method_info_index) { * Returns the interface class containing the implementation of the given method. The interfaces * of {@code startClass} are recursively searched. * - * @param startClass the JavaClass whose interfaces are to be searched + * @param startClass the class whose interfaces are to be searched * @param methodName the target method to search for * @param paramTypes the target method's parameter types * @return the name of the interface class containing target method, or null if not found @@ -1993,13 +2324,14 @@ void add_exit(MethodGen mgen, MethodInfo mi, int method_info_index) { * @param invoke a method invocation bytecode instruction * @return instructions to replace the given instruction */ + @RequiresNonNull("mgen") private InstructionList handleInvoke(InvokeInstruction invoke) { // Get information about the call + @ClassGetName String classname = invoke.getClassName(pool); String methodName = invoke.getMethodName(pool); // getClassName does not work properly if invoke is INVOKEDYNAMIC. // We will deal with this later. - @ClassGetName String classname = invoke.getClassName(pool); Type returnType = invoke.getReturnType(pool); Type[] paramTypes = invoke.getArgumentTypes(pool); @@ -2008,16 +2340,16 @@ private InstructionList handleInvoke(InvokeInstruction invoke) { // Replace calls to Object's equals method with calls to our // replacement, a static method in DCRuntime. - Type[] new_param_types = new Type[] {javalangObject, javalangObject}; + Type[] new_param_types = {CD_Object, CD_Object}; InstructionList il = new InstructionList(); il.append( ifact.createInvoke( dcompRuntimeClassName, - (invoke.getOpcode() == Const.INVOKESPECIAL) ? "dcomp_super_equals" : "dcomp_equals", + (invoke.getOpcode() == INVOKESPECIAL) ? "dcomp_super_equals" : "dcomp_equals", returnType, new_param_types, - Const.INVOKESTATIC)); + INVOKESTATIC)); return il; } @@ -2033,15 +2365,15 @@ private InstructionList handleInvoke(InvokeInstruction invoke) { if (debugHandleInvoke) { System.out.printf("handleInvoke(%s)%n", invoke); - System.out.printf(" invoke host: %s%n", gen.getClassName() + "." + mgen.getName()); - System.out.printf(" invoke targ: %s%n", classname + "." + methodName); + System.out.printf(" invoke host: %s.%s%n", classGen.getClassName(), mgen.getName()); + System.out.printf(" invoke targ: %s.%s%n", classname, methodName); System.out.printf(" callee_instrumented: %s%n", callee_instrumented); } if (callee_instrumented) { InstructionList il = new InstructionList(); - // Add the DCompMarker argument so that it calls the instrumented version. + // Push the DCompMarker argument as we are calling the instrumented version. il.append(new ACONST_NULL()); Type[] new_param_types = ArraysPlume.append(paramTypes, dcomp_marker); Constant methodref = pool.getConstant(invoke.getIndex()); @@ -2067,11 +2399,11 @@ private InstructionList handleInvoke(InvokeInstruction invoke) { } // Add a tag for the return type if it is primitive. - if ((returnType instanceof BasicType) && (returnType != Type.VOID)) { + if ((returnType instanceof BasicType) && (returnType != CD_void)) { if (debugHandleInvoke) { System.out.printf("push tag for return type of %s%n", invoke.getReturnType(pool)); } - il.append(dcr_call("push_const", Type.VOID, Type.NO_ARGS)); + il.append(dcr_call("push_const", CD_void, noArgsSig)); } il.append(invoke); return il; @@ -2079,7 +2411,7 @@ private InstructionList handleInvoke(InvokeInstruction invoke) { } /** - * Returns instructions that will discard any primitive tags corresponding to the specified + * Returns instructions that will discard (pop) any primitive tags corresponding to the specified * parameters. Returns an empty instruction list if there are no primitive arguments to discard. * * @param paramTypes parameter types of target method @@ -2087,8 +2419,6 @@ private InstructionList handleInvoke(InvokeInstruction invoke) { * comparability data stack */ private InstructionList discard_primitive_tags(Type[] paramTypes) { - - InstructionList il = new InstructionList(); int primitive_cnt = 0; for (Type paramType : paramTypes) { if (paramType instanceof BasicType) { @@ -2096,20 +2426,22 @@ private InstructionList discard_primitive_tags(Type[] paramTypes) { } } if (primitive_cnt > 0) { - il.append(discard_tag_code(new NOP(), primitive_cnt)); + return discard_tag_code(new NOP(), primitive_cnt); } - return il; + // Must return a mutable array because some clients mutate it. + return new InstructionList(); } /** - * Returns true if the invoke target is instrumented. + * Returns true if the invoked method (the callee) is instrumented. * * @param invoke instruction whose target is to be checked - * @param classname target class of the invoke - * @param methodName target method of the invoke + * @param classname target class of the invoke (the callee) + * @param methodName target method of the invoke (the callee) * @param paramTypes parameter types of target method * @return true if the target is instrumented */ + @RequiresNonNull("mgen") private boolean isTargetInstrumented( InvokeInstruction invoke, @ClassGetName String classname, @@ -2131,14 +2463,14 @@ private boolean isTargetInstrumented( targetInstrumented = isClassnameInstrumented(classname, methodName); if (debugHandleInvoke) { - System.out.printf("invoke host: %s%n", gen.getClassName() + "." + mgen.getName()); - System.out.printf("invoke targ: %s%n", classname + "." + methodName); + System.out.printf("isClassnameInstrumented: %s%n", targetInstrumented); + System.out.printf("invoke host: %s.%s%n", classGen.getClassName(), mgen.getName()); + System.out.printf("invoke targ: %s.%s%n", classname, methodName); } if (Premain.problem_methods.contains(classname + "." + methodName)) { debugInstrument.log( - "Don't call instrumented version of problem method %s.%n", - classname + "." + methodName); + "Don't call instrumented version of problem method %s.%s.%n", classname, methodName); targetInstrumented = false; } @@ -2175,7 +2507,7 @@ private boolean isTargetInstrumented( && (invoke instanceof INVOKEINTERFACE || invoke instanceof INVOKEVIRTUAL)) { Integer access = getAccessFlags(classname); - if ((access.intValue() & Const.ACC_ANNOTATION) != 0) { + if ((access.intValue() & ACC_ANNOTATION) != 0) { targetInstrumented = false; } @@ -2205,7 +2537,7 @@ private boolean isTargetInstrumented( if (debugHandleInvoke) { System.out.println("method: " + methodName); System.out.println("paramTypes: " + Arrays.toString(paramTypes)); - System.out.printf("invoke host: %s%n", gen.getClassName() + "." + mgen.getName()); + System.out.printf("invoke host: %s.%s%n", classGen.getClassName(), mgen.getName()); } @ClassGetName String targetClassname = classname; @@ -2218,6 +2550,7 @@ private boolean isTargetInstrumented( try { targetClass = getJavaClass(targetClassname); } catch (Throwable e) { + System.out.printf("Problem while getting class: %s%n%s%n%n", targetClassname, e); targetClass = null; } if (targetClass == null) { @@ -2290,7 +2623,7 @@ private boolean isTargetInstrumented( } if (invoke instanceof INVOKESPECIAL) { - if (classname.equals(gen.getSuperclassName()) && methodName.equals("")) { + if (classname.equals(classGen.getSuperclassName()) && methodName.equals("")) { this.constructor_is_initialized = true; } } @@ -2336,7 +2669,8 @@ private Integer getAccessFlags(String classname) { } /** - * Returns true if the specified classname is instrumented. + * Returns true if the specified class is instrumented or we presume it will be instrumented by + * the time it is executed. * * @param classname class to be checked * @param methodName method to be checked (currently unused) @@ -2346,7 +2680,7 @@ private boolean isClassnameInstrumented( @ClassGetName String classname, @Identifier String methodName) { if (debugHandleInvoke) { - System.out.printf("Checking callee instrumented on %s%n", classname); + System.out.printf("Checking callee instrumented on %s.%s%n", classname, methodName); } // Our copy of daikon.plumelib is not instrumented. It would be odd, though, @@ -2360,7 +2694,9 @@ private boolean isClassnameInstrumented( return false; } - if (daikon.dcomp.Instrument.is_transformer(classname.replace('.', '/'))) { + @SuppressWarnings("signature:assignment") // string conversion + @InternalForm String internalName = classname.replace('.', '/'); + if (daikon.dcomp.Instrument.is_transformer(internalName)) { return false; } @@ -2387,12 +2723,10 @@ private boolean isClassnameInstrumented( } int i = classname.lastIndexOf('.'); - if (i > 0) { - if (Premain.problem_packages.contains(classname.substring(0, i))) { - debugInstrument.log( - "Don't call instrumented member of problem package %s%n", classname.substring(0, i)); - return false; - } + if (i > 0 && Premain.problem_packages.contains(classname.substring(0, i))) { + debugInstrument.log( + "Don't call instrumented member of problem package %s%n", classname.substring(0, i)); + return false; } if (Premain.problem_classes.contains(classname)) { @@ -2402,16 +2736,16 @@ private boolean isClassnameInstrumented( // We have decided not to use the instrumented version of Random as // the method generates values based on an initial seed value. - // (Typical of random() algorithms.) This has the undesirable side + // (Typical of random() algorithms.) Instrumentation would have the undesirable side // effect of putting all the generated values in the same comparison // set when they should be distinct. - // NOTE: If we find other classes that should not use the instrumented + // Note: If we find other classes that should not use the instrumented // versions, we should consider making this a searchable list. if (classname.equals("java.util.Random")) { return false; } - // If using the instrumented JDK, then everthing but object is instrumented + // If using the instrumented JDK, then everthing but object is instrumented. if (Premain.jdk_instrumented && !classname.equals("java.lang.Object")) { return true; } @@ -2420,14 +2754,15 @@ private boolean isClassnameInstrumented( } /** - * Given a classname return it's superclass name. Note that BCEL reports that the superclass of - * 'java.lang.Object' is 'java.lang.Object' rather than saying there is no superclass. + * Given a classname return its superclass name. Note that we copy BCEL and report that the + * superclass of {@code java.lang.Object} is {@code java.lang.Object} rather than saying there is + * no superclass. * * @param classname the fully-qualified name of the class in binary form. E.g., "java.util.List" * @return name of superclass * @throws SuperclassNameError if the class cannot be loaded */ - private @ClassGetName String getSuperclassName(String classname) { + private @BinaryName String getSuperclassName(String classname) { JavaClass jc = getJavaClass(classname); if (jc == null) { throw new SuperclassNameError(classname); @@ -2454,13 +2789,13 @@ private static class SuperclassNameError extends Error { /** * There are times when it is useful to inspect a class file other than the one we are currently - * instrumenting. Note we cannot use classForName to do this as it might trigger a recursive call - * to Instrument which would not work at this point. + * instrumenting. We cannot use {@code classForName} to do this as it might trigger a recursive + * call to Instrument which would not work at this point. * *

Given a class name, we treat it as a system resource and try to open it as an input stream * that we can pass to BCEL to read and convert to a JavaClass object. * - * @param classname the fully qualified name of the class in binary form, e.g., "java.util.List" + * @param classname the fully-qualified name of the class in binary form, e.g., "java.util.List" * @return the JavaClass of the corresponding classname or null */ private @Nullable JavaClass getJavaClass(String classname) { @@ -2473,14 +2808,15 @@ private static class SuperclassNameError extends Error { if (class_url != null) { try (InputStream inputStream = class_url.openStream()) { if (inputStream != null) { - // Parse the bytes of the classfile, die on any errors + // Parse the bytes of the class file, die on any errors ClassParser parser = new ClassParser(inputStream, classname + ""); JavaClass result = parser.parse(); javaClasses.put(classname, result); return result; } } catch (Throwable t) { - throw new Error("Error reading " + class_url, t); + throw new DynCompError( + String.format("Error while reading %s %s%n", classname, class_url), t); } } // Do not cache a null result, because a subsequent invocation might return non-null. @@ -2498,9 +2834,9 @@ private static class SuperclassNameError extends Error { @Pure boolean is_object_equals(@Identifier String methodName, Type returnType, Type[] paramTypes) { return (methodName.equals("equals") - && returnType == Type.BOOLEAN + && returnType == CD_boolean && paramTypes.length == 1 - && paramTypes[0].equals(javalangObject)); + && paramTypes[0].equals(CD_Object)); } /** @@ -2513,9 +2849,7 @@ boolean is_object_equals(@Identifier String methodName, Type returnType, Type[] */ @Pure boolean is_object_clone(@Identifier String methodName, Type returnType, Type[] paramTypes) { - return methodName.equals("clone") - && returnType.equals(javalangObject) - && (paramTypes.length == 0); + return methodName.equals("clone") && returnType.equals(CD_Object) && (paramTypes.length == 0); } /** @@ -2540,18 +2874,19 @@ InstructionList instrument_clone_call(InvokeInstruction invoke) { // push the target class il.append(new LDC(pool.addClass(classname))); - // if this is a super call - if (invoke.getOpcode() == Const.INVOKESPECIAL) { + if (invoke.getOpcode() == INVOKESPECIAL) { + // This is a super call. // Runtime will discover if the object's superclass has an instrumented clone method. // If so, call it; otherwise call the uninstrumented version. - il.append(dcr_call("dcomp_super_clone", returnType, new Type[] {Type.OBJECT, javalangClass})); + il.append(dcr_call("dcomp_super_clone", returnType, new Type[] {CD_Object, CD_Class})); - } else { // a regular (non-super) clone() call + } else { + // This is a regular (non-super) clone() call. // Runtime will discover if the object has an instrumented clone method. // If so, call it; otherwise call the uninstrumented version. - il.append(dcr_call("dcomp_clone", returnType, new Type[] {Type.OBJECT, javalangClass})); + il.append(dcr_call("dcomp_clone", returnType, new Type[] {CD_Object, CD_Class})); } return il; @@ -2559,75 +2894,75 @@ InstructionList instrument_clone_call(InvokeInstruction invoke) { /** * Create the instructions that replace the object eq or ne branch instruction. They are replaced - * by a call to the specified compare_method (which returns a boolean) followed by the specified + * by a call to the specified compareMethod (which returns a boolean) followed by the specified * boolean ifeq or ifne instruction. */ InstructionList object_comparison( - BranchInstruction branch, String compare_method, short boolean_if) { + BranchInstruction branch, String compareMethod, short boolean_if) { InstructionList il = new InstructionList(); il.append( ifact.createInvoke( - dcompRuntimeClassName, compare_method, Type.BOOLEAN, two_objects, Const.INVOKESTATIC)); + dcompRuntimeClassName, compareMethod, CD_boolean, objectObjectSig, INVOKESTATIC)); assert branch.getTarget() != null; il.append(InstructionFactory.createBranchInstruction(boolean_if, branch.getTarget())); return il; } /** - * Handles load and store field instructions. The instructions must be augmented to either push - * (load) or pop (store) the tag on the tag stack. This is accomplished by calling the tag get/set - * method for this field. + * Handles load and store field instructions. If the field is a primitive the instructions must be + * augmented to either push (load) or pop (store) the tag on the tag stack. This is accomplished + * by calling the tag get/set method for this field. */ - InstructionList load_store_field(MethodGen mgen, FieldInstruction f) { + @Nullable InstructionList load_store_field(MethodGen mgen, FieldInstruction fi) { - Type field_type = f.getFieldType(pool); + Type field_type = fi.getFieldType(pool); if (field_type instanceof ReferenceType) { return null; } - ObjectType obj_type = (ObjectType) f.getReferenceType(pool); + ObjectType obj_type = (ObjectType) fi.getReferenceType(pool); InstructionList il = new InstructionList(); String classname = obj_type.getClassName(); - // If this class doesn't support tag fields, don't load/store them + // If this class doesn't support tag fields, don't load/store them. if (!tag_fields_ok(mgen, classname)) { - if ((f instanceof GETFIELD) || (f instanceof GETSTATIC)) { - il.append(dcr_call("push_const", Type.VOID, Type.NO_ARGS)); + if ((fi instanceof GETFIELD) || (fi instanceof GETSTATIC)) { + il.append(dcr_call("push_const", CD_void, noArgsSig)); } else { il.append(ifact.createConstant(1)); - il.append(dcr_call("discard_tag", Type.VOID, integer_arg)); + il.append(dcr_call("discard_tag", CD_void, intSig)); } - // Perform the normal field command - il.append(f); + // Perform the orginal field command. + il.append(fi); return il; } - if (f instanceof GETSTATIC) { + if (fi instanceof GETSTATIC) { il.append( ifact.createInvoke( classname, - Premain.tag_method_name(Premain.GET_TAG, classname, f.getFieldName(pool)), - Type.VOID, - Type.NO_ARGS, - Const.INVOKESTATIC)); - } else if (f instanceof PUTSTATIC) { + Premain.tag_method_name(Premain.GET_TAG, classname, fi.getFieldName(pool)), + CD_void, + noArgsSig, + INVOKESTATIC)); + } else if (fi instanceof PUTSTATIC) { il.append( ifact.createInvoke( classname, - Premain.tag_method_name(Premain.SET_TAG, classname, f.getFieldName(pool)), - Type.VOID, - Type.NO_ARGS, - Const.INVOKESTATIC)); - } else if (f instanceof GETFIELD) { + Premain.tag_method_name(Premain.SET_TAG, classname, fi.getFieldName(pool)), + CD_void, + noArgsSig, + INVOKESTATIC)); + } else if (fi instanceof GETFIELD) { il.append(InstructionFactory.createDup(obj_type.getSize())); il.append( ifact.createInvoke( classname, - Premain.tag_method_name(Premain.GET_TAG, classname, f.getFieldName(pool)), - Type.VOID, - Type.NO_ARGS, - Const.INVOKEVIRTUAL)); + Premain.tag_method_name(Premain.GET_TAG, classname, fi.getFieldName(pool)), + CD_void, + noArgsSig, + INVOKEVIRTUAL)); } else { // must be put field if (field_type.getSize() == 2) { LocalVariableGen lv = get_tmp2_local(mgen, field_type); @@ -2636,10 +2971,10 @@ InstructionList load_store_field(MethodGen mgen, FieldInstruction f) { il.append( ifact.createInvoke( classname, - Premain.tag_method_name(Premain.SET_TAG, classname, f.getFieldName(pool)), - Type.VOID, - Type.NO_ARGS, - Const.INVOKEVIRTUAL)); + Premain.tag_method_name(Premain.SET_TAG, classname, fi.getFieldName(pool)), + CD_void, + noArgsSig, + INVOKEVIRTUAL)); il.append(InstructionFactory.createLoad(field_type, lv.getIndex())); } else { il.append(new SWAP()); @@ -2647,16 +2982,16 @@ InstructionList load_store_field(MethodGen mgen, FieldInstruction f) { il.append( ifact.createInvoke( classname, - Premain.tag_method_name(Premain.SET_TAG, classname, f.getFieldName(pool)), - Type.VOID, - Type.NO_ARGS, - Const.INVOKEVIRTUAL)); + Premain.tag_method_name(Premain.SET_TAG, classname, fi.getFieldName(pool)), + CD_void, + noArgsSig, + INVOKEVIRTUAL)); il.append(new SWAP()); } } - // Perform the normal field command - il.append(f); + // Perform the original field command. + il.append(fi); return il; } @@ -2667,7 +3002,7 @@ InstructionList load_store_field(MethodGen mgen, FieldInstruction f) { * method in DCRuntime and passing that method the tag frame and the offset of local/parameter. */ InstructionList load_store_local( - LocalVariableInstruction lvi, LocalVariableGen tag_frame_local, @Identifier String method) { + LocalVariableInstruction lvi, LocalVariableGen tagFrameLocal, @Identifier String method) { // Don't need tags for objects assert !(lvi instanceof ALOAD) && !(lvi instanceof ASTORE) : "lvi " + lvi; @@ -2675,8 +3010,8 @@ InstructionList load_store_local( InstructionList il = new InstructionList(); // Push the tag frame and the index of this local - il.append(InstructionFactory.createLoad(object_arr, tag_frame_local.getIndex())); - debugInstrument.log("CreateLoad %s %d%n", object_arr, tag_frame_local.getIndex()); + il.append(InstructionFactory.createLoad(CD_Object_array, tagFrameLocal.getIndex())); + debugInstrument.log("CreateLoad %s %d%n", CD_Object_array, tagFrameLocal.getIndex()); il.append(ifact.createConstant(lvi.getIndex())); // Call the runtime method to handle loading/storing the local/parameter @@ -2684,9 +3019,9 @@ InstructionList load_store_local( ifact.createInvoke( dcompRuntimeClassName, method, - Type.VOID, - new Type[] {object_arr, Type.INT}, - Const.INVOKESTATIC)); + CD_void, + new Type[] {CD_Object_array, CD_int}, + INVOKESTATIC)); il.append(lvi); return il; } @@ -2708,7 +3043,7 @@ int get_field_num(String name, ObjectType obj_type) { throw new Error("Can't find " + name + " in " + obj_type); } - // Look up the class using this classes class loader. This may + // Look up the class using this class's class loader. This may // not be the best way to accomplish this. Class obj_class; try { @@ -2770,12 +3105,13 @@ LocalVariableGen get_return_local(MethodGen mgen, @Nullable Type return_type) { if (return_local == null) { assert (return_type != null) : " return__$trace2_val doesn't exist"; } else { - assert return_type.equals(return_local.getType()) + assert return_type != null && return_type.equals(return_local.getType()) : " return_type = " + return_type + "; current type = " + return_local.getType(); } if (return_local == null) { // log ("Adding return local of type %s%n", return_type); + assert return_type != null : "@AssumeAssertion(nullness)"; return_local = mgen.addLocalVariable("return__$trace2_val", return_type, null, null); } @@ -2786,11 +3122,11 @@ LocalVariableGen get_return_local(MethodGen mgen, @Nullable Type return_type) { * Creates a MethodInfo corresponding to the specified method. The exit locations are filled in, * but the reflection information is not generated. Returns null if there are no instructions. * - * @param class_info class containing the method + * @param classInfo class containing the method * @param mgen method to inspect - * @return MethodInfo for the method + * @return a new MethodInfo for the method, or null if the method should not be instrumented */ - @Nullable MethodInfo create_method_info(ClassInfo class_info, MethodGen mgen) { + @Nullable MethodInfo create_method_info_if_instrumented(ClassInfo classInfo, MethodGen mgen) { // if (mgen.getName().equals("")) { // // This case DOES occur at run time. -MDE 1/22/2010 @@ -2819,7 +3155,7 @@ LocalVariableGen get_return_local(MethodGen mgen, @Nullable Type return_type) { // Loop through each instruction and find the line number for each // return opcode - List exit_locs = new ArrayList<>(); + List exit_line_numbers = new ArrayList<>(); // Tells whether each exit loc in the method is included or not // (based on filters) @@ -2828,7 +3164,7 @@ LocalVariableGen get_return_local(MethodGen mgen, @Nullable Type return_type) { // log ("Looking for exit points in %s%n", mgen.getName()); InstructionList il = mgen.getInstructionList(); int line_number = 0; - int last_line_number = 0; + int prev_line_number = 0; boolean foundLine; if (il == null) { @@ -2852,20 +3188,20 @@ LocalVariableGen get_return_local(MethodGen mgen, @Nullable Type return_type) { } switch (ih.getInstruction().getOpcode()) { - case Const.ARETURN: - case Const.DRETURN: - case Const.FRETURN: - case Const.IRETURN: - case Const.LRETURN: - case Const.RETURN: + case ARETURN: + case DRETURN: + case FRETURN: + case IRETURN: + case LRETURN: + case RETURN: // log ("Exit at line %d%n", line_number); // only do incremental lines if we don't have the line generator - if (line_number == last_line_number && foundLine == false) { + if (line_number == prev_line_number && foundLine == false) { line_number++; } - last_line_number = line_number; + prev_line_number = line_number; - exit_locs.add(line_number); + exit_line_numbers.add(line_number); isIncluded.add(true); break; @@ -2875,18 +3211,18 @@ LocalVariableGen get_return_local(MethodGen mgen, @Nullable Type return_type) { } return new MethodInfo( - class_info, mgen.getName(), paramNames, param_type_strings, exit_locs, isIncluded); + classInfo, mgen.getName(), paramNames, param_type_strings, exit_line_numbers, isIncluded); } /** * Adds a call to DCRuntime.set_class_initialized (String classname) to the class initializer for * this class. Creates a class initializer if one is not currently present. */ - void track_class_init() { + void trackClass_init() { // Look for the class init method. If not found, create an empty one. Method cinit = null; - for (Method m : gen.getMethods()) { + for (Method m : classGen.getMethods()) { if (m.getName().equals("")) { cinit = m; break; @@ -2894,49 +3230,45 @@ void track_class_init() { } if (cinit == null) { InstructionList il = new InstructionList(); - il.append(InstructionFactory.createReturn(Type.VOID)); - MethodGen cinit_gen = + il.append(InstructionFactory.createReturn(CD_void)); + MethodGen cinit_classGen = new MethodGen( - Const.ACC_STATIC, - Type.VOID, - Type.NO_ARGS, + ACC_STATIC, + CD_void, + noArgsSig, new String[0], "", - gen.getClassName(), + classGen.getClassName(), il, pool); - cinit_gen.setMaxLocals(); - cinit_gen.setMaxStack(); - cinit_gen.update(); - cinit = cinit_gen.getMethod(); - gen.addMethod(cinit); + cinit_classGen.setMaxLocals(); + cinit_classGen.setMaxStack(); + cinit_classGen.update(); + cinit = cinit_classGen.getMethod(); + classGen.addMethod(cinit); } try { - MethodGen cinit_gen = new MethodGen(cinit, gen.getClassName(), pool); - setCurrentStackMapTable(cinit_gen, gen.getMajor()); + MethodGen cinit_classGen = new MethodGen(cinit, classGen.getClassName(), pool); + setCurrentStackMapTable(cinit_classGen, classGen.getMajor()); // Add a call to DCRuntime.set_class_initialized to the beginning of the method InstructionList il = new InstructionList(); - il.append(ifact.createConstant(gen.getClassName())); + il.append(ifact.createConstant(classGen.getClassName())); il.append( ifact.createInvoke( - dcompRuntimeClassName, - "set_class_initialized", - Type.VOID, - string_arg, - Const.INVOKESTATIC)); - - insertAtMethodStart(cinit_gen, il); - createNewStackMapAttribute(cinit_gen); - cinit_gen.setMaxLocals(); - cinit_gen.setMaxStack(); - gen.replaceMethod(cinit, cinit_gen.getMethod()); + dcompRuntimeClassName, "set_class_initialized", CD_void, string_arg, INVOKESTATIC)); + + insertAtMethodStart(cinit_classGen, il); + createNewStackMapAttribute(cinit_classGen); + cinit_classGen.setMaxLocals(); + cinit_classGen.setMaxStack(); + classGen.replaceMethod(cinit, cinit_classGen.getMethod()); } catch (Throwable t) { if (debugInstrument.enabled) { t.printStackTrace(); } - throw new Error("Error processing " + gen.getClassName() + "." + cinit.getName(), t); + throw new Error("Error processing " + classGen.getClassName() + "." + cinit.getName(), t); } } @@ -2950,7 +3282,6 @@ void track_class_init() { * @return instruction list that calls the runtime to handle the array load instruction */ InstructionList array_load(Instruction inst) { - InstructionList il = new InstructionList(); // Duplicate the array ref and index and pass them to DCRuntime @@ -2961,11 +3292,11 @@ InstructionList array_load(Instruction inst) { String method = "primitive_array_load"; if (inst instanceof AALOAD) { method = "ref_array_load"; - } else if (is_uninit_class(gen.getClassName())) { + } else if (is_class_initialized_by_jvm(classGen.getClassName())) { method = "primitive_array_load_null_ok"; } - il.append(dcr_call(method, Type.VOID, new Type[] {Type.OBJECT, Type.INT})); + il.append(dcr_call(method, CD_void, new Type[] {CD_Object, CD_int})); // Perform the original instruction il.append(inst); @@ -2988,7 +3319,7 @@ InstructionList array_store(Instruction inst, @Identifier String method, Type ba InstructionList il = new InstructionList(); Type arr_type = new ArrayType(base_type, 1); - il.append(dcr_call(method, Type.VOID, new Type[] {arr_type, Type.INT, base_type})); + il.append(dcr_call(method, CD_void, new Type[] {arr_type, CD_int, base_type})); return il; } @@ -3002,13 +3333,12 @@ InstructionList array_store(Instruction inst, @Identifier String method, Type ba * @return instruction list that calls the runtime to handle the arraylength instruction */ InstructionList array_length(Instruction inst) { - InstructionList il = new InstructionList(); // Duplicate the array ref and pass it to DCRuntime which will push // it onto the tag stack. il.append(new DUP()); - il.append(dcr_call("push_array_tag", Type.VOID, new Type[] {Type.OBJECT})); + il.append(dcr_call("push_array_tag", CD_void, new Type[] {CD_Object})); // Perform the original instruction il.append(inst); @@ -3031,11 +3361,11 @@ InstructionList new_array(Instruction inst) { // Duplicate the array ref from the top of the stack and pass it // to DCRuntime which will push it onto the tag stack. il.append(new DUP()); - il.append(dcr_call("push_array_tag", Type.VOID, new Type[] {Type.OBJECT})); + il.append(dcr_call("push_array_tag", CD_void, new Type[] {CD_Object})); // Make the array and the count comparable. Also, pop the tags for // the array and the count off the tag stack. - il.append(dcr_call("cmp_op", Type.VOID, Type.NO_ARGS)); + il.append(dcr_call("cmp_op", CD_void, noArgsSig)); return il; } @@ -3060,8 +3390,8 @@ InstructionList multiarray2(Instruction inst) { // Stack is now: ..., arrayref, count1, count2, arrayref il.append(new DUP_X2()); - Type objArray = new ArrayType(Type.OBJECT, 1); - il.append(dcr_call("multianewarray2", Type.VOID, new Type[] {Type.INT, Type.INT, objArray})); + Type objArray = new ArrayType(CD_Object, 1); + il.append(dcr_call("multianewarray2", CD_void, new Type[] {CD_int, CD_int, objArray})); return il; } @@ -3094,7 +3424,7 @@ boolean should_track( return false; } - // call shouldIgnore to check ppt-omit-pattern(s) and ppt-select-pattern(s) + // Call `shouldIgnore` to check ppt-omit-patterns and ppt-select-patterns. boolean shouldIgnore = daikon.chicory.Instrument.shouldIgnore(className, methodName, pptName); if (shouldIgnore) { debug_transform.log("ignoring %s, not included in ppt_select patterns%n", pptName); @@ -3143,7 +3473,7 @@ static String methodEntryName(String fullClassName, Method m) { InvokeInstruction dcr_call(@Identifier String methodName, Type returnType, Type[] paramTypes) { return ifact.createInvoke( - dcompRuntimeClassName, methodName, returnType, paramTypes, Const.INVOKESTATIC); + dcompRuntimeClassName, methodName, returnType, paramTypes, INVOKESTATIC); } /** @@ -3156,22 +3486,22 @@ InvokeInstruction dcr_call(@Identifier String methodName, Type returnType, Type[ InstructionList discard_tag_code(Instruction inst, int tag_count) { InstructionList il = new InstructionList(); il.append(ifact.createConstant(tag_count)); - il.append(dcr_call("discard_tag", Type.VOID, integer_arg)); + il.append(dcr_call("discard_tag", CD_void, intSig)); append_inst(il, inst); return il; } /** - * Duplicates the item on the top of stack. If the value on the top of the stack is a primitive, - * we need to do the same on the tag stack. Otherwise, we need do nothing. + * Duplicates a category 1 item on the top of stack. If it is a primitive, we need to do the same + * to the tag stack. Otherwise, we do nothing. */ - InstructionList dup_tag(Instruction inst, OperandStack stack) { + @Nullable InstructionList dup_tag(Instruction inst, OperandStack stack) { Type top = stack.peek(); if (debug_dup.enabled) { debug_dup.log("DUP -> %s [... %s]%n", "dup", stack_contents(stack, 2)); } if (is_primitive(top)) { - return build_il(dcr_call("dup", Type.VOID, Type.NO_ARGS), inst); + return build_il(dcr_call("dup", CD_void, noArgsSig), inst); } return null; } @@ -3182,7 +3512,7 @@ InstructionList dup_tag(Instruction inst, OperandStack stack) { * value is not a primitive, then we need only to insert the duped value down 1 on the tag stack * (which contains only primitives). */ - InstructionList dup_x1_tag(Instruction inst, OperandStack stack) { + @Nullable InstructionList dup_x1_tag(Instruction inst, OperandStack stack) { Type top = stack.peek(); if (debug_dup.enabled) { debug_dup.log("DUP -> %s [... %s]%n", "dup_x1", stack_contents(stack, 2)); @@ -3194,14 +3524,14 @@ InstructionList dup_x1_tag(Instruction inst, OperandStack stack) { if (!is_primitive(stack.peek(1))) { method = "dup"; } - return build_il(dcr_call(method, Type.VOID, Type.NO_ARGS), inst); + return build_il(dcr_call(method, CD_void, noArgsSig), inst); } /** * Duplicates either the top 2 category 1 values or a single category 2 value and inserts it 2 or * 3 values down on the stack. */ - InstructionList dup2_x1_tag(Instruction inst, OperandStack stack) { + @Nullable InstructionList dup2_x1_tag(Instruction inst, OperandStack stack) { String op; Type top = stack.peek(); if (is_category2(top)) { @@ -3211,10 +3541,13 @@ InstructionList dup2_x1_tag(Instruction inst, OperandStack stack) { op = "dup"; } } else if (is_primitive(top)) { - if (is_primitive(stack.peek(1)) && is_primitive(stack.peek(2))) op = "dup2_x1"; - else if (is_primitive(stack.peek(1))) op = "dup2"; - else if (is_primitive(stack.peek(2))) op = "dup_x1"; - else { + if (is_primitive(stack.peek(1)) && is_primitive(stack.peek(2))) { + op = "dup2_x1"; + } else if (is_primitive(stack.peek(1))) { + op = "dup2"; + } else if (is_primitive(stack.peek(2))) { + op = "dup_x1"; + } else { // neither value 1 nor value 2 is primitive op = "dup"; } @@ -3224,78 +3557,78 @@ InstructionList dup2_x1_tag(Instruction inst, OperandStack stack) { } else if (is_primitive(stack.peek(1))) { op = "dup"; } else { // neither of the top two values is primitive - op = null; + return null; } } if (debug_dup.enabled) { debug_dup.log("DUP2_X1 -> %s [... %s]%n", op, stack_contents(stack, 3)); } - if (op != null) { - return build_il(dcr_call(op, Type.VOID, Type.NO_ARGS), inst); - } - return null; + return build_il(dcr_call(op, CD_void, noArgsSig), inst); } /** * Duplicate either one category 2 value or two category 1 values. The instruction is implemented * as necessary on the tag stack. */ - InstructionList dup2_tag(Instruction inst, OperandStack stack) { + @Nullable InstructionList dup2_tag(Instruction inst, OperandStack stack) { Type top = stack.peek(); String op; if (is_category2(top)) { op = "dup"; - } else if (is_primitive(top) && is_primitive(stack.peek(1))) op = "dup2"; - else if (is_primitive(top) || is_primitive(stack.peek(1))) op = "dup"; - else { + } else if (is_primitive(top) && is_primitive(stack.peek(1))) { + op = "dup2"; + } else if (is_primitive(top) || is_primitive(stack.peek(1))) { + op = "dup"; + } else { // both of the top two items are not primitive, nothing to dup - op = null; + return null; } if (debug_dup.enabled) { debug_dup.log("DUP2 -> %s [... %s]%n", op, stack_contents(stack, 2)); } - if (op != null) { - return build_il(dcr_call(op, Type.VOID, Type.NO_ARGS), inst); - } - return null; + return build_il(dcr_call(op, CD_void, noArgsSig), inst); } /** * Dup the category 1 value on the top of the stack and insert it either two or three values down * on the stack. */ - InstructionList dup_x2(Instruction inst, OperandStack stack) { + @Nullable InstructionList dup_x2(Instruction inst, OperandStack stack) { Type top = stack.peek(); - String op = null; - if (is_primitive(top)) { - if (is_category2(stack.peek(1))) op = "dup_x1"; - else if (is_primitive(stack.peek(1)) && is_primitive(stack.peek(2))) op = "dup_x2"; - else if (is_primitive(stack.peek(1)) || is_primitive(stack.peek(2))) op = "dup_x1"; - else { - op = "dup"; - } + if (!is_primitive(top)) { + return null; + } + String op; + if (is_category2(stack.peek(1))) { + op = "dup_x1"; + } else if (is_primitive(stack.peek(1)) && is_primitive(stack.peek(2))) { + op = "dup_x2"; + } else if (is_primitive(stack.peek(1)) || is_primitive(stack.peek(2))) { + op = "dup_x1"; + } else { + op = "dup"; } if (debug_dup.enabled) { debug_dup.log("DUP_X2 -> %s [... %s]%n", op, stack_contents(stack, 3)); } - if (op != null) { - return build_il(dcr_call(op, Type.VOID, Type.NO_ARGS), inst); - } - return null; + return build_il(dcr_call(op, CD_void, noArgsSig), inst); } /** * Duplicate the top one or two operand stack values and insert two, three, or four values down. */ - InstructionList dup2_x2(Instruction inst, OperandStack stack) { + @Nullable InstructionList dup2_x2(Instruction inst, OperandStack stack) { Type top = stack.peek(); String op; if (is_category2(top)) { - if (is_category2(stack.peek(1))) op = "dup_x1"; - else if (is_primitive(stack.peek(1)) && is_primitive(stack.peek(2))) op = "dup_x2"; - else if (is_primitive(stack.peek(1)) || is_primitive(stack.peek(2))) op = "dup_x1"; - else { + if (is_category2(stack.peek(1))) { + op = "dup_x1"; + } else if (is_primitive(stack.peek(1)) && is_primitive(stack.peek(2))) { + op = "dup_x2"; + } else if (is_primitive(stack.peek(1)) || is_primitive(stack.peek(2))) { + op = "dup_x1"; + } else { // both values are references op = "dup"; } @@ -3309,16 +3642,20 @@ InstructionList dup2_x2(Instruction inst, OperandStack stack) { op = "dup_x1"; } } else if (is_primitive(stack.peek(1))) { - if (is_primitive(stack.peek(2)) && is_primitive(stack.peek(3))) op = "dup2_x2"; - else if (is_primitive(stack.peek(2)) || is_primitive(stack.peek(3))) op = "dup2_x1"; - else { + if (is_primitive(stack.peek(2)) && is_primitive(stack.peek(3))) { + op = "dup2_x2"; + } else if (is_primitive(stack.peek(2)) || is_primitive(stack.peek(3))) { + op = "dup2_x1"; + } else { // both 2 and 3 are references op = "dup2"; } } else { // 1 is a reference - if (is_primitive(stack.peek(2)) && is_primitive(stack.peek(3))) op = "dup_x2"; - else if (is_primitive(stack.peek(2)) || is_primitive(stack.peek(3))) op = "dup_x1"; - else { + if (is_primitive(stack.peek(2)) && is_primitive(stack.peek(3))) { + op = "dup_x2"; + } else if (is_primitive(stack.peek(2)) || is_primitive(stack.peek(3))) { + op = "dup_x1"; + } else { // both 2 and 3 are references op = "dup"; } @@ -3330,33 +3667,32 @@ InstructionList dup2_x2(Instruction inst, OperandStack stack) { if (is_primitive(stack.peek(1))) { op = "dup_x1"; } else { - op = null; // nothing to dup + return null; // nothing to dup } } else if (is_primitive(stack.peek(1))) { - if (is_primitive(stack.peek(2)) && is_primitive(stack.peek(3))) op = "dup_x2"; - else if (is_primitive(stack.peek(2)) || is_primitive(stack.peek(3))) op = "dup_x1"; - else { + if (is_primitive(stack.peek(2)) && is_primitive(stack.peek(3))) { + op = "dup_x2"; + } else if (is_primitive(stack.peek(2)) || is_primitive(stack.peek(3))) { + op = "dup_x1"; + } else { // both 2 and 3 are references op = "dup"; } } else { // 1 is a reference - op = null; // nothing to dup + return null; // nothing to dup } } if (debug_dup.enabled) { debug_dup.log("DUP_X2 -> %s [... %s]%n", op, stack_contents(stack, 3)); } - if (op != null) { - return build_il(dcr_call(op, Type.VOID, Type.NO_ARGS), inst); - } - return null; + return build_il(dcr_call(op, CD_void, noArgsSig), inst); } /** * Pop instructions discard the top of the stack. We want to discard the top of the tag stack iff * the item on the top of the stack is a primitive. */ - InstructionList pop_tag(Instruction inst, OperandStack stack) { + @Nullable InstructionList pop_tag(Instruction inst, OperandStack stack) { Type top = stack.peek(); if (is_primitive(top)) { return discard_tag_code(inst, 1); @@ -3368,7 +3704,7 @@ InstructionList pop_tag(Instruction inst, OperandStack stack) { * Pops either the top 2 category 1 values or a single category 2 value from the top of the stack. * We must do the same to the tag stack if the values are primitives. */ - InstructionList pop2_tag(Instruction inst, OperandStack stack) { + @Nullable InstructionList pop2_tag(Instruction inst, OperandStack stack) { Type top = stack.peek(); if (is_category2(top)) { return discard_tag_code(inst, 1); @@ -3391,11 +3727,11 @@ InstructionList pop2_tag(Instruction inst, OperandStack stack) { * Swaps the two category 1 types on the top of the stack. We need to swap the top of the tag * stack if the two top elements on the real stack are primitives. */ - InstructionList swap_tag(Instruction inst, OperandStack stack) { + @Nullable InstructionList swap_tag(Instruction inst, OperandStack stack) { Type type1 = stack.peek(); Type type2 = stack.peek(1); if (is_primitive(type1) && is_primitive(type2)) { - return build_il(dcr_call("swap", Type.VOID, Type.NO_ARGS), inst); + return build_il(dcr_call("swap", CD_void, noArgsSig), inst); } return null; } @@ -3414,7 +3750,7 @@ InstructionList swap_tag(Instruction inst, OperandStack stack) { if (!(type instanceof BasicType)) { return null; } - return build_il(dcr_call("push_const", Type.VOID, Type.NO_ARGS), inst); + return build_il(dcr_call("push_const", CD_void, noArgsSig), inst); } /** @@ -3434,24 +3770,25 @@ InstructionList multi_newarray_dc(Instruction inst) { } /** - * Create an instruction list that calls the runtime to handle returns for the tag stack follow by - * the original return instruction. + * Create an instruction list that calls the runtime to handle returns for the tag stack followed + * by the original return instruction. * * @param mgen method to modify * @param inst return instruction to be replaced * @return the instruction list */ + @RequiresNonNull("tagFrameLocal") InstructionList return_tag(MethodGen mgen, Instruction inst) { Type type = mgen.getReturnType(); InstructionList il = new InstructionList(); // Push the tag frame - il.append(InstructionFactory.createLoad(object_arr, tag_frame_local.getIndex())); + il.append(InstructionFactory.createLoad(CD_Object_array, tagFrameLocal.getIndex())); - if ((type instanceof BasicType) && (type != Type.VOID)) { - il.append(dcr_call("normal_exit_primitive", Type.VOID, new Type[] {object_arr})); + if ((type instanceof BasicType) && (type != CD_void)) { + il.append(dcr_call("normal_exit_primitive", CD_void, new Type[] {CD_Object_array})); } else { - il.append(dcr_call("normal_exit", Type.VOID, new Type[] {object_arr})); + il.append(dcr_call("normal_exit", CD_void, new Type[] {CD_Object_array})); } il.append(inst); return il; @@ -3465,7 +3802,7 @@ InstructionList return_tag(MethodGen mgen, Instruction inst) { */ @Pure boolean is_primitive(Type type) { - return (type instanceof BasicType) && (type != Type.VOID); + return (type instanceof BasicType) && (type != CD_void); } /** @@ -3476,7 +3813,7 @@ boolean is_primitive(Type type) { */ @Pure boolean is_category2(Type type) { - return (type == Type.DOUBLE) || (type == Type.LONG); + return (type == CD_double) || (type == CD_long); } /** @@ -3487,27 +3824,27 @@ boolean is_category2(Type type) { * @param loader to use to locate class * @return instance of class */ - static Class type_to_class(Type t, ClassLoader loader) { + static Class type_to_class(Type t, @Nullable ClassLoader loader) { if (loader == null) { loader = DCInstrument.class.getClassLoader(); } - if (t == Type.BOOLEAN) { + if (t == CD_boolean) { return Boolean.TYPE; - } else if (t == Type.BYTE) { + } else if (t == CD_byte) { return Byte.TYPE; - } else if (t == Type.CHAR) { + } else if (t == CD_char) { return Character.TYPE; - } else if (t == Type.DOUBLE) { + } else if (t == CD_double) { return Double.TYPE; - } else if (t == Type.FLOAT) { + } else if (t == CD_float) { return Float.TYPE; - } else if (t == Type.INT) { + } else if (t == CD_int) { return Integer.TYPE; - } else if (t == Type.LONG) { + } else if (t == CD_long) { return Long.TYPE; - } else if (t == Type.SHORT) { + } else if (t == CD_short) { return Short.TYPE; } else if (t instanceof ObjectType || t instanceof ArrayType) { @ClassGetName String sig = typeToClassGetName(t); @@ -3528,10 +3865,10 @@ static Class type_to_class(Type t, ClassLoader loader) { * *

TODO: add a way to provide a synopsis for native methods that affect comparability. * - * @param gen current class + * @param classGen current class * @param mgen the interface method. Must be native. */ - void fix_native(ClassGen gen, MethodGen mgen) { + void fix_native(ClassGen classGen, MethodGen mgen) { InstructionList il = new InstructionList(); Type[] paramTypes = mgen.getArgumentTypes(); @@ -3561,13 +3898,13 @@ void fix_native(ClassGen gen, MethodGen mgen) { // push a tag if there is a primitive return value Type returnType = mgen.getReturnType(); - if ((returnType instanceof BasicType) && (returnType != Type.VOID)) { - il.append(dcr_call("push_const", Type.VOID, Type.NO_ARGS)); + if ((returnType instanceof BasicType) && (returnType != CD_void)) { + il.append(dcr_call("push_const", CD_void, noArgsSig)); } // If the method is not static, push the instance on the stack if (!mgen.isStatic()) { - il.append(InstructionFactory.createLoad(new ObjectType(gen.getClassName()), 0)); + il.append(InstructionFactory.createLoad(new ObjectType(classGen.getClassName()), 0)); } // System.out.printf("%s: atc = %d, anc = %d%n", mgen.getName(), paramTypes.length, @@ -3576,14 +3913,14 @@ void fix_native(ClassGen gen, MethodGen mgen) { // if call is sun.reflect.Reflection.getCallerClass (realFramesToSkip) if (mgen.getName().equals("getCallerClass") && (paramTypes.length == 1) - && gen.getClassName().equals("sun.reflect.Reflection")) { + && classGen.getClassName().equals("sun.reflect.Reflection")) { // The call returns the class realFramesToSkip up on the stack. Since we // have added this call in between, we need to increment that number by 1. - il.append(InstructionFactory.createLoad(Type.INT, 0)); + il.append(InstructionFactory.createLoad(CD_int, 0)); il.append(ifact.createConstant(1)); il.append(new IADD()); - // System.out.printf("adding 1 in %s.%s%n", gen.getClassName(), + // System.out.printf("adding 1 in %s.%s%n", classGen.getClassName(), // mgen.getName()); } else { // normal call @@ -3602,11 +3939,11 @@ void fix_native(ClassGen gen, MethodGen mgen) { // Call the method il.append( ifact.createInvoke( - gen.getClassName(), + classGen.getClassName(), mgen.getName(), mgen.getReturnType(), paramTypes, - (mgen.isStatic() ? Const.INVOKESTATIC : Const.INVOKEVIRTUAL))); + (mgen.isStatic() ? INVOKESTATIC : INVOKEVIRTUAL))); // If there is a return value, return it il.append(InstructionFactory.createReturn(mgen.getReturnType())); @@ -3620,7 +3957,7 @@ void fix_native(ClassGen gen, MethodGen mgen) { mgen.setMaxLocals(); // turn off the native flag - mgen.setAccessFlags(mgen.getAccessFlags() & ~Const.ACC_NATIVE); + mgen.setAccessFlags(mgen.getAccessFlags() & ~ACC_NATIVE); } /** @@ -3634,8 +3971,8 @@ void fix_native(ClassGen gen, MethodGen mgen) { boolean tag_fields_ok(MethodGen mgen, @ClassGetName String classname) { // Prior to Java 8 an interface could not contain any implementations. - if (gen.isInterface()) { - if (gen.getMajor() < Const.MAJOR_1_8) { + if (classGen.isInterface()) { + if (classGen.getMajor() < MAJOR_1_8) { return false; } } @@ -3656,14 +3993,10 @@ boolean tag_fields_ok(MethodGen mgen, @ClassGetName String classname) { return true; } - if (classname.equals("java.lang.String") + return !(classname.equals("java.lang.String") || classname.equals("java.lang.Class") || classname.equals("java.lang.Object") - || classname.equals("java.lang.ClassLoader")) { - return false; - } - - return true; + || classname.equals("java.lang.ClassLoader")); } /** @@ -3688,11 +4021,11 @@ static String stack_contents(OperandStack stack, int max_items) { } /** - * Creates tag get and set accessor methods for each field in gen. An accessor is created for each - * field (including final, static, and private fields). The accessors share the modifiers of their - * field (except that all are final). Accessors are named {@code ___$get_tag} and - * {@code ___$set_tag}. The class name must be included because field names can - * shadow one another. + * Creates tag get and set accessor methods for each field in the class. An accessor is created + * for each field (including final, static, and private fields). The accessors share the modifiers + * of their field (except that all are final). Accessors are named {@code + * ___$get_tag} and {@code ___$set_tag}. The class name must be + * included because field names can shadow one another. * *

If tag_fields_ok is true for the class, then tag fields are created and the accessor uses * the tag fields. If not, tag storage is created separately and accessed via the field number. @@ -3703,20 +4036,21 @@ static String stack_contents(OperandStack stack, int max_items) { * *

Any accessors created are added to the class. * - * @param gen class to check for fields + * @param classGen class to check for fields */ - void create_tag_accessors(ClassGen gen) { + @RequiresNonNull("mgen") + void create_tag_accessors(ClassGen classGen) { - String classname = gen.getClassName(); + String classname = classGen.getClassName(); // If this class doesn't support tag fields, don't create them if (!tag_fields_ok(mgen, classname)) return; Set field_set = new HashSet<>(); - Map field_map = build_field_map(gen.getJavaClass()); + Map field_to_offset_map = build_field_to_offset_map(classGen.getJavaClass()); // Build accessors for all fields declared in this class - for (Field f : gen.getFields()) { + for (Field f : classGen.getFields()) { assert !field_set.contains(f.getName()) : f.getName() + "-" + classname; field_set.add(f.getName()); @@ -3726,25 +4060,18 @@ void create_tag_accessors(ClassGen gen) { continue; } - MethodGen get_method; - MethodGen set_method; - if (f.isStatic()) { - String full_name = full_name(orig_class, f); - get_method = create_get_tag(gen, f, static_field_id.get(full_name)); - set_method = create_set_tag(gen, f, static_field_id.get(full_name)); - } else { - get_method = create_get_tag(gen, f, field_map.get(f)); - set_method = create_set_tag(gen, f, field_map.get(f)); - } - gen.addMethod(get_method.getMethod()); - gen.addMethod(set_method.getMethod()); + @SuppressWarnings("nullness:unboxing.of.nullable") + int tagOffset = + f.isStatic() ? static_field_id.get(full_name(orig_class, f)) : field_to_offset_map.get(f); + classGen.addMethod(create_get_tag(classGen, f, tagOffset).getMethod()); + classGen.addMethod(create_set_tag(classGen, f, tagOffset).getMethod()); } // Build accessors for each field declared in a superclass that is // is not shadowed in a subclass JavaClass[] super_classes; try { - super_classes = gen.getJavaClass().getSuperClasses(); + super_classes = classGen.getJavaClass().getSuperClasses(); } catch (Exception e) { throw new Error(e); } @@ -3761,18 +4088,13 @@ void create_tag_accessors(ClassGen gen) { } field_set.add(f.getName()); - MethodGen get_method; - MethodGen set_method; - if (f.isStatic()) { - String full_name = full_name(super_class, f); - get_method = create_get_tag(gen, f, static_field_id.get(full_name)); - set_method = create_set_tag(gen, f, static_field_id.get(full_name)); - } else { - get_method = create_get_tag(gen, f, field_map.get(f)); - set_method = create_set_tag(gen, f, field_map.get(f)); - } - gen.addMethod(get_method.getMethod()); - gen.addMethod(set_method.getMethod()); + @SuppressWarnings("nullness:unboxing.of.nullable") + int tagOffset = + f.isStatic() + ? static_field_id.get(full_name(super_class, f)) + : field_to_offset_map.get(f); + classGen.addMethod(create_get_tag(classGen, f, tagOffset).getMethod()); + classGen.addMethod(create_set_tag(classGen, f, tagOffset).getMethod()); } } } @@ -3785,7 +4107,7 @@ void create_tag_accessors(ClassGen gen) { * @param jc class to check for fields * @return field offset map */ - Map build_field_map(JavaClass jc) { + Map build_field_to_offset_map(JavaClass jc) { // Object doesn't have any primitive fields if (jc.getClassName().equals("java.lang.Object")) { @@ -3799,8 +4121,11 @@ Map build_field_map(JavaClass jc) { } catch (Exception e) { throw new Error("can't get superclass for " + jc, e); } - Map field_map = build_field_map(super_jc); - int offset = field_map.size(); + if (super_jc == null) { + throw new Error("null superclass for " + jc); + } + Map field_to_offset_map = build_field_to_offset_map(super_jc); + int offset = field_to_offset_map.size(); // Determine the offset for each primitive field in the class // Also make sure the static_tags list is large enough for @@ -3826,12 +4151,12 @@ Map build_field_map(JavaClass jc) { } } } else { - field_map.put(f, offset); + field_to_offset_map.put(f, offset); offset++; } } - return field_map; + return field_to_offset_map; } /** @@ -3847,28 +4172,32 @@ Map build_field_map(JavaClass jc) { * } * } * - * @param gen class whose accessors are being built. Not necessarily the class declaring f (if f - * is inherited). + * @param classGen class whose accessors are being built. Not necessarily the class declaring f + * (if f is inherited). * @param f field to build an accessor for * @param tag_offset offset of f in the tag storage for this field * @return the get tag method */ - MethodGen create_get_tag(ClassGen gen, Field f, int tag_offset) { + MethodGen create_get_tag(ClassGen classGen, Field f, int tag_offset) { // Determine the method to call in DCRuntime. Instance fields and static // fields are handled separately. Also instance fields in special // classes that are created by the JVM are handled separately since only // in those classes can fields be read without being written (in java) - String methodname = "push_field_tag"; - Type[] params = object_int; + String methodname; + Type[] params; if (f.isStatic()) { methodname = "push_static_tag"; - params = integer_arg; - } else if (is_uninit_class(gen.getClassName())) { + params = intSig; + } else if (is_class_initialized_by_jvm(classGen.getClassName())) { methodname = "push_field_tag_null_ok"; + params = objectIntSig; + } else { + methodname = "push_field_tag"; + params = objectIntSig; } - String classname = gen.getClassName(); + String classname = classGen.getClassName(); String accessor_name = Premain.tag_method_name(Premain.GET_TAG, classname, f.getName()); InstructionList il = new InstructionList(); @@ -3877,34 +4206,27 @@ MethodGen create_get_tag(ClassGen gen, Field f, int tag_offset) { il.append(InstructionFactory.createThis()); } il.append(ifact.createConstant(tag_offset)); - il.append(dcr_call(methodname, Type.VOID, params)); - il.append(InstructionFactory.createReturn(Type.VOID)); + il.append(dcr_call(methodname, CD_void, params)); + il.append(InstructionFactory.createReturn(CD_void)); int access_flags = f.getAccessFlags(); - if (gen.isInterface()) { + if (classGen.isInterface()) { // method in interface cannot be final - access_flags &= ~Const.ACC_FINAL; - if (gen.getMajor() < Const.MAJOR_1_8) { + access_flags &= ~ACC_FINAL; + if (classGen.getMajor() < MAJOR_1_8) { // If class file version is prior to 8 then a method in an interface // cannot be static (it's implicit) and must be abstract. - access_flags &= ~Const.ACC_STATIC; - access_flags |= Const.ACC_ABSTRACT; + access_flags &= ~ACC_STATIC; + access_flags |= ACC_ABSTRACT; } } else { - access_flags |= Const.ACC_FINAL; + access_flags |= ACC_FINAL; } // Create the get accessor method MethodGen get_method = new MethodGen( - access_flags, - Type.VOID, - Type.NO_ARGS, - new String[] {}, - accessor_name, - classname, - il, - pool); + access_flags, CD_void, noArgsSig, new String[] {}, accessor_name, classname, il, pool); get_method.isPrivate(false); get_method.isProtected(false); get_method.isPublic(true); @@ -3928,22 +4250,22 @@ MethodGen create_get_tag(ClassGen gen, Field f, int tag_offset) { * } * } * - * @param gen class whose accessors are being built. Not necessarily the class declaring f (if f - * is inherited). + * @param classGen class whose accessors are being built. Not necessarily the class declaring f + * (if f is inherited). * @param f field to build an accessor for * @param tag_offset offset of f in the tag storage for this field * @return the set tag method */ - MethodGen create_set_tag(ClassGen gen, Field f, int tag_offset) { + MethodGen create_set_tag(ClassGen classGen, Field f, int tag_offset) { String methodname = "pop_field_tag"; - Type[] params = object_int; + Type[] params = objectIntSig; if (f.isStatic()) { methodname = "pop_static_tag"; - params = integer_arg; + params = intSig; } - String classname = gen.getClassName(); + String classname = classGen.getClassName(); String setter_name = Premain.tag_method_name(Premain.SET_TAG, classname, f.getName()); InstructionList il = new InstructionList(); @@ -3952,34 +4274,27 @@ MethodGen create_set_tag(ClassGen gen, Field f, int tag_offset) { il.append(InstructionFactory.createThis()); } il.append(ifact.createConstant(tag_offset)); - il.append(dcr_call(methodname, Type.VOID, params)); - il.append(InstructionFactory.createReturn(Type.VOID)); + il.append(dcr_call(methodname, CD_void, params)); + il.append(InstructionFactory.createReturn(CD_void)); int access_flags = f.getAccessFlags(); - if (gen.isInterface()) { + if (classGen.isInterface()) { // method in interface cannot be final - access_flags &= ~Const.ACC_FINAL; - if (gen.getMajor() < Const.MAJOR_1_8) { + access_flags &= ~ACC_FINAL; + if (classGen.getMajor() < MAJOR_1_8) { // If class file version is prior to 8 then a method in an interface // cannot be static (it's implicit) and must be abstract. - access_flags &= ~Const.ACC_STATIC; - access_flags |= Const.ACC_ABSTRACT; + access_flags &= ~ACC_STATIC; + access_flags |= ACC_ABSTRACT; } } else { - access_flags |= Const.ACC_FINAL; + access_flags |= ACC_FINAL; } - // Create the setter method + // Create the setter method. MethodGen set_method = new MethodGen( - access_flags, - Type.VOID, - Type.NO_ARGS, - new String[] {}, - setter_name, - classname, - il, - pool); + access_flags, CD_void, noArgsSig, new String[] {}, setter_name, classname, il, pool); set_method.setMaxLocals(); set_method.setMaxStack(); // add_line_numbers(set_method, il); @@ -3988,7 +4303,7 @@ MethodGen create_set_tag(ClassGen gen, Field f, int tag_offset) { } /** - * Adds the DCompInstrumented interface to the given class. Adds the following method to the + * Adds the DCompInstrumented interface to the given class. Also adds the following method to the * class, so that it implements the DCompInstrumented interface: * *

{@code
@@ -3997,45 +4312,45 @@ MethodGen create_set_tag(ClassGen gen, Field f, int tag_offset) {
    * }
    * }
* - * The method does nothing except call the instrumented equals method (boolean equals(Object, - * DCompMarker)). + * The method does nothing except call the instrumented equals method {@code boolean + * equals(Object, DCompMarker)}. * - * @param gen class to add interface to + * @param classGen class to add interface to */ - void add_dcomp_interface(ClassGen gen) { - gen.addInterface(DCRuntime.instrumentation_interface); + void add_dcomp_interface(ClassGen classGen) { + classGen.addInterface(DCRuntime.instrumentation_interface); debugInstrument.log("Added interface DCompInstrumented%n"); InstructionList il = new InstructionList(); - int access_flags = Const.ACC_PUBLIC; - if (gen.isInterface()) { - access_flags |= Const.ACC_ABSTRACT; + int access_flags = ACC_PUBLIC; + if (classGen.isInterface()) { + access_flags |= ACC_ABSTRACT; } MethodGen method = new MethodGen( access_flags, - Type.BOOLEAN, - new Type[] {Type.OBJECT}, + CD_boolean, + new Type[] {CD_Object}, new String[] {"obj"}, "equals_dcomp_instrumented", - gen.getClassName(), + classGen.getClassName(), il, pool); - il.append(InstructionFactory.createLoad(Type.OBJECT, 0)); // load this - il.append(InstructionFactory.createLoad(Type.OBJECT, 1)); // load obj + il.append(InstructionFactory.createLoad(CD_Object, 0)); // load this + il.append(InstructionFactory.createLoad(CD_Object, 1)); // load obj il.append(new ACONST_NULL()); // use null for marker il.append( ifact.createInvoke( - gen.getClassName(), + classGen.getClassName(), "equals", - Type.BOOLEAN, - new Type[] {Type.OBJECT, dcomp_marker}, - Const.INVOKEVIRTUAL)); - il.append(InstructionFactory.createReturn(Type.BOOLEAN)); + CD_boolean, + new Type[] {CD_Object, dcomp_marker}, + INVOKEVIRTUAL)); + il.append(InstructionFactory.createReturn(CD_boolean)); method.setMaxStack(); method.setMaxLocals(); - gen.addMethod(method.getMethod()); + classGen.addMethod(method.getMethod()); il.dispose(); } @@ -4048,61 +4363,59 @@ void add_dcomp_interface(ClassGen gen) { * } * } * - * Must only be called if the Object equals method has not been overridden; if the equals method - * is already defined in the class, a ClassFormatError will result because of the duplicate - * method. + * Throws a ClassFormatError if the equals method is already defined in the class. * - * @param gen class to add method to + * @param classGen class to add method to */ - void add_equals_method(ClassGen gen) { + void add_equals_method(ClassGen classGen) { InstructionList il = new InstructionList(); - int access_flags = Const.ACC_PUBLIC; - if (gen.isInterface()) { - access_flags |= Const.ACC_ABSTRACT; + int access_flags = ACC_PUBLIC; + if (classGen.isInterface()) { + access_flags |= ACC_ABSTRACT; } MethodGen method = new MethodGen( access_flags, - Type.BOOLEAN, - new Type[] {Type.OBJECT}, + CD_boolean, + new Type[] {CD_Object}, new String[] {"obj"}, "equals", - gen.getClassName(), + classGen.getClassName(), il, pool); - il.append(InstructionFactory.createLoad(Type.OBJECT, 0)); // load this - il.append(InstructionFactory.createLoad(Type.OBJECT, 1)); // load obj + il.append(InstructionFactory.createLoad(CD_Object, 0)); // load this + il.append(InstructionFactory.createLoad(CD_Object, 1)); // load obj il.append( ifact.createInvoke( - gen.getSuperclassName(), + classGen.getSuperclassName(), "equals", - Type.BOOLEAN, - new Type[] {Type.OBJECT}, - Const.INVOKESPECIAL)); - il.append(InstructionFactory.createReturn(Type.BOOLEAN)); + CD_boolean, + new Type[] {CD_Object}, + INVOKESPECIAL)); + il.append(InstructionFactory.createReturn(CD_boolean)); method.setMaxStack(); method.setMaxLocals(); - gen.addMethod(method.getMethod()); + classGen.addMethod(method.getMethod()); il.dispose(); } /** - * Marks the class as implementing various object methods (currently clone and toString). Callers - * will call the instrumented version of the method if it exists, otherwise they will call the - * uninstrumented version. + * Adds interfaces to indicate which of the Object methods (currently clone and toString) the + * class overrides. Callers will call the instrumented version of the method if it exists, + * otherwise they will call the uninstrumented version. * - * @param gen class to check + * @param classGen class to check */ - void handle_object(ClassGen gen) { - Method cl = gen.containsMethod("clone", "()Ljava/lang/Object;"); + void add_clone_and_tostring_interfaces(ClassGen classGen) { + Method cl = classGen.containsMethod("clone", "()Ljava/lang/Object;"); if (cl != null) { - gen.addInterface(Signatures.addPackage(dcomp_prefix, "DCompClone")); + classGen.addInterface(Signatures.addPackage(dcompRuntimePrefix, "DCompClone")); } - Method ts = gen.containsMethod("toString", "()Ljava/lang/String;"); + Method ts = classGen.containsMethod("toString", "()Ljava/lang/String;"); if (ts != null) { - gen.addInterface(Signatures.addPackage(dcomp_prefix, "DCompToString")); + classGen.addInterface(Signatures.addPackage(dcompRuntimePrefix, "DCompToString")); } } @@ -4153,7 +4466,7 @@ boolean is_object_method(@Identifier String methodName, Type[] paramTypes) { * @return true if classname has members that are uninitialized */ @Pure - boolean is_uninit_class(String classname) { + boolean is_class_initialized_by_jvm(String classname) { for (String u_name : uninit_classes) { if (u_name.equals(classname)) { @@ -4190,9 +4503,9 @@ MethodGen create_dcomp_stub(MethodGen mgen) { } // Call the method - short kind = Const.INVOKEVIRTUAL; + short kind = INVOKEVIRTUAL; if (mgen.isStatic()) { - kind = Const.INVOKESTATIC; + kind = INVOKESTATIC; } il.append( ifact.createInvoke( @@ -4229,7 +4542,7 @@ MethodGen create_dcomp_stub(MethodGen mgen) { */ static void save_static_field_id(File file) throws IOException { - PrintStream ps = new PrintStream(file); + PrintStream ps = new PrintStream(file, "UTF-8"); // in Java 10+, use: StandardCharsets.UTF_8 for (Map.Entry<@KeyFor("static_field_id") String, Integer> entry : static_field_id.entrySet()) { ps.printf("%s %d%n", entry.getKey(), entry.getValue()); } @@ -4249,8 +4562,6 @@ static void restore_static_field_id(File file) throws IOException { String[] key_val = line.split(" *"); assert !static_field_id.containsKey(key_val[0]) : key_val[0] + " " + key_val[1]; static_field_id.put(key_val[0], Integer.valueOf(key_val[1])); - // System.out.printf("Adding %s %s to static map%n", key_val[0], - // key_val[1]); } } } @@ -4258,12 +4569,12 @@ static void restore_static_field_id(File file) throws IOException { /** * Returns the fully-qualified fieldname of the specified field. * - * @param jc class containing the field + * @param c class containing the field * @param f the field * @return string containing the fully-qualified name */ - protected String full_name(JavaClass jc, Field f) { - return jc.getClassName() + "." + f.getName(); + protected String full_name(JavaClass c, Field f) { + return c.getClassName() + "." + f.getName(); } /** diff --git a/java/daikon/dcomp/DCInstrument24.java b/java/daikon/dcomp/DCInstrument24.java new file mode 100644 index 0000000000..ca7cda5bdd --- /dev/null +++ b/java/daikon/dcomp/DCInstrument24.java @@ -0,0 +1,5234 @@ +package daikon.dcomp; + +import static java.lang.classfile.ClassFile.ACC_ABSTRACT; +import static java.lang.classfile.ClassFile.ACC_ANNOTATION; +import static java.lang.classfile.ClassFile.ACC_BRIDGE; +import static java.lang.classfile.ClassFile.ACC_NATIVE; +import static java.lang.classfile.ClassFile.ACC_PRIVATE; +import static java.lang.classfile.ClassFile.ACC_PROTECTED; +import static java.lang.classfile.ClassFile.ACC_PUBLIC; +import static java.lang.classfile.ClassFile.ACC_STATIC; +import static java.lang.classfile.Opcode.AALOAD; +import static java.lang.classfile.Opcode.AASTORE; +import static java.lang.classfile.Opcode.ACONST_NULL; +import static java.lang.classfile.Opcode.ALOAD; +import static java.lang.classfile.Opcode.ALOAD_0; +import static java.lang.classfile.Opcode.ALOAD_1; +import static java.lang.classfile.Opcode.ALOAD_2; +import static java.lang.classfile.Opcode.ALOAD_3; +import static java.lang.classfile.Opcode.ANEWARRAY; +import static java.lang.classfile.Opcode.ARETURN; +import static java.lang.classfile.Opcode.ARRAYLENGTH; +import static java.lang.classfile.Opcode.ASTORE; +import static java.lang.classfile.Opcode.ASTORE_0; +import static java.lang.classfile.Opcode.ASTORE_1; +import static java.lang.classfile.Opcode.ASTORE_2; +import static java.lang.classfile.Opcode.ASTORE_3; +import static java.lang.classfile.Opcode.ATHROW; +import static java.lang.classfile.Opcode.BALOAD; +import static java.lang.classfile.Opcode.BASTORE; +import static java.lang.classfile.Opcode.BIPUSH; +import static java.lang.classfile.Opcode.CALOAD; +import static java.lang.classfile.Opcode.CASTORE; +import static java.lang.classfile.Opcode.CHECKCAST; +import static java.lang.classfile.Opcode.D2F; +import static java.lang.classfile.Opcode.D2I; +import static java.lang.classfile.Opcode.D2L; +import static java.lang.classfile.Opcode.DADD; +import static java.lang.classfile.Opcode.DALOAD; +import static java.lang.classfile.Opcode.DASTORE; +import static java.lang.classfile.Opcode.DCMPG; +import static java.lang.classfile.Opcode.DCMPL; +import static java.lang.classfile.Opcode.DCONST_0; +import static java.lang.classfile.Opcode.DCONST_1; +import static java.lang.classfile.Opcode.DDIV; +import static java.lang.classfile.Opcode.DLOAD; +import static java.lang.classfile.Opcode.DLOAD_0; +import static java.lang.classfile.Opcode.DLOAD_1; +import static java.lang.classfile.Opcode.DLOAD_2; +import static java.lang.classfile.Opcode.DLOAD_3; +import static java.lang.classfile.Opcode.DMUL; +import static java.lang.classfile.Opcode.DNEG; +import static java.lang.classfile.Opcode.DREM; +import static java.lang.classfile.Opcode.DRETURN; +import static java.lang.classfile.Opcode.DSTORE; +import static java.lang.classfile.Opcode.DSTORE_0; +import static java.lang.classfile.Opcode.DSTORE_1; +import static java.lang.classfile.Opcode.DSTORE_2; +import static java.lang.classfile.Opcode.DSTORE_3; +import static java.lang.classfile.Opcode.DSUB; +import static java.lang.classfile.Opcode.DUP; +import static java.lang.classfile.Opcode.DUP2; +import static java.lang.classfile.Opcode.DUP2_X1; +import static java.lang.classfile.Opcode.DUP2_X2; +import static java.lang.classfile.Opcode.DUP_X1; +import static java.lang.classfile.Opcode.DUP_X2; +import static java.lang.classfile.Opcode.F2D; +import static java.lang.classfile.Opcode.F2I; +import static java.lang.classfile.Opcode.F2L; +import static java.lang.classfile.Opcode.FADD; +import static java.lang.classfile.Opcode.FALOAD; +import static java.lang.classfile.Opcode.FASTORE; +import static java.lang.classfile.Opcode.FCMPG; +import static java.lang.classfile.Opcode.FCMPL; +import static java.lang.classfile.Opcode.FCONST_0; +import static java.lang.classfile.Opcode.FCONST_1; +import static java.lang.classfile.Opcode.FCONST_2; +import static java.lang.classfile.Opcode.FDIV; +import static java.lang.classfile.Opcode.FLOAD; +import static java.lang.classfile.Opcode.FLOAD_0; +import static java.lang.classfile.Opcode.FLOAD_1; +import static java.lang.classfile.Opcode.FLOAD_2; +import static java.lang.classfile.Opcode.FLOAD_3; +import static java.lang.classfile.Opcode.FMUL; +import static java.lang.classfile.Opcode.FNEG; +import static java.lang.classfile.Opcode.FREM; +import static java.lang.classfile.Opcode.FRETURN; +import static java.lang.classfile.Opcode.FSTORE; +import static java.lang.classfile.Opcode.FSTORE_0; +import static java.lang.classfile.Opcode.FSTORE_1; +import static java.lang.classfile.Opcode.FSTORE_2; +import static java.lang.classfile.Opcode.FSTORE_3; +import static java.lang.classfile.Opcode.FSUB; +import static java.lang.classfile.Opcode.GETFIELD; +import static java.lang.classfile.Opcode.GETSTATIC; +import static java.lang.classfile.Opcode.GOTO; +import static java.lang.classfile.Opcode.GOTO_W; +import static java.lang.classfile.Opcode.I2B; +import static java.lang.classfile.Opcode.I2C; +import static java.lang.classfile.Opcode.I2D; +import static java.lang.classfile.Opcode.I2F; +import static java.lang.classfile.Opcode.I2L; +import static java.lang.classfile.Opcode.I2S; +import static java.lang.classfile.Opcode.IADD; +import static java.lang.classfile.Opcode.IALOAD; +import static java.lang.classfile.Opcode.IAND; +import static java.lang.classfile.Opcode.IASTORE; +import static java.lang.classfile.Opcode.ICONST_0; +import static java.lang.classfile.Opcode.ICONST_1; +import static java.lang.classfile.Opcode.ICONST_2; +import static java.lang.classfile.Opcode.ICONST_3; +import static java.lang.classfile.Opcode.ICONST_4; +import static java.lang.classfile.Opcode.ICONST_5; +import static java.lang.classfile.Opcode.ICONST_M1; +import static java.lang.classfile.Opcode.IDIV; +import static java.lang.classfile.Opcode.IFEQ; +import static java.lang.classfile.Opcode.IFGE; +import static java.lang.classfile.Opcode.IFGT; +import static java.lang.classfile.Opcode.IFLE; +import static java.lang.classfile.Opcode.IFLT; +import static java.lang.classfile.Opcode.IFNE; +import static java.lang.classfile.Opcode.IFNONNULL; +import static java.lang.classfile.Opcode.IFNULL; +import static java.lang.classfile.Opcode.IF_ACMPEQ; +import static java.lang.classfile.Opcode.IF_ACMPNE; +import static java.lang.classfile.Opcode.IF_ICMPEQ; +import static java.lang.classfile.Opcode.IF_ICMPGE; +import static java.lang.classfile.Opcode.IF_ICMPGT; +import static java.lang.classfile.Opcode.IF_ICMPLE; +import static java.lang.classfile.Opcode.IF_ICMPLT; +import static java.lang.classfile.Opcode.IF_ICMPNE; +import static java.lang.classfile.Opcode.IINC; +import static java.lang.classfile.Opcode.ILOAD; +import static java.lang.classfile.Opcode.ILOAD_0; +import static java.lang.classfile.Opcode.ILOAD_1; +import static java.lang.classfile.Opcode.ILOAD_2; +import static java.lang.classfile.Opcode.ILOAD_3; +import static java.lang.classfile.Opcode.IMUL; +import static java.lang.classfile.Opcode.INEG; +import static java.lang.classfile.Opcode.INSTANCEOF; +import static java.lang.classfile.Opcode.INVOKEDYNAMIC; +import static java.lang.classfile.Opcode.INVOKEINTERFACE; +import static java.lang.classfile.Opcode.INVOKESPECIAL; +import static java.lang.classfile.Opcode.INVOKESTATIC; +import static java.lang.classfile.Opcode.INVOKEVIRTUAL; +import static java.lang.classfile.Opcode.IOR; +import static java.lang.classfile.Opcode.IREM; +import static java.lang.classfile.Opcode.IRETURN; +import static java.lang.classfile.Opcode.ISHL; +import static java.lang.classfile.Opcode.ISHR; +import static java.lang.classfile.Opcode.ISTORE; +import static java.lang.classfile.Opcode.ISTORE_0; +import static java.lang.classfile.Opcode.ISTORE_1; +import static java.lang.classfile.Opcode.ISTORE_2; +import static java.lang.classfile.Opcode.ISTORE_3; +import static java.lang.classfile.Opcode.ISUB; +import static java.lang.classfile.Opcode.IUSHR; +import static java.lang.classfile.Opcode.IXOR; +import static java.lang.classfile.Opcode.JSR; +import static java.lang.classfile.Opcode.JSR_W; +import static java.lang.classfile.Opcode.L2D; +import static java.lang.classfile.Opcode.L2F; +import static java.lang.classfile.Opcode.L2I; +import static java.lang.classfile.Opcode.LADD; +import static java.lang.classfile.Opcode.LALOAD; +import static java.lang.classfile.Opcode.LAND; +import static java.lang.classfile.Opcode.LASTORE; +import static java.lang.classfile.Opcode.LCMP; +import static java.lang.classfile.Opcode.LCONST_0; +import static java.lang.classfile.Opcode.LCONST_1; +import static java.lang.classfile.Opcode.LDC; +import static java.lang.classfile.Opcode.LDC2_W; +import static java.lang.classfile.Opcode.LDC_W; +import static java.lang.classfile.Opcode.LDIV; +import static java.lang.classfile.Opcode.LLOAD; +import static java.lang.classfile.Opcode.LLOAD_0; +import static java.lang.classfile.Opcode.LLOAD_1; +import static java.lang.classfile.Opcode.LLOAD_2; +import static java.lang.classfile.Opcode.LLOAD_3; +import static java.lang.classfile.Opcode.LMUL; +import static java.lang.classfile.Opcode.LNEG; +import static java.lang.classfile.Opcode.LOOKUPSWITCH; +import static java.lang.classfile.Opcode.LOR; +import static java.lang.classfile.Opcode.LREM; +import static java.lang.classfile.Opcode.LRETURN; +import static java.lang.classfile.Opcode.LSHL; +import static java.lang.classfile.Opcode.LSHR; +import static java.lang.classfile.Opcode.LSTORE; +import static java.lang.classfile.Opcode.LSTORE_0; +import static java.lang.classfile.Opcode.LSTORE_1; +import static java.lang.classfile.Opcode.LSTORE_2; +import static java.lang.classfile.Opcode.LSTORE_3; +import static java.lang.classfile.Opcode.LSUB; +import static java.lang.classfile.Opcode.LUSHR; +import static java.lang.classfile.Opcode.LXOR; +import static java.lang.classfile.Opcode.MONITORENTER; +import static java.lang.classfile.Opcode.MONITOREXIT; +import static java.lang.classfile.Opcode.MULTIANEWARRAY; +import static java.lang.classfile.Opcode.NEW; +import static java.lang.classfile.Opcode.NEWARRAY; +import static java.lang.classfile.Opcode.NOP; +import static java.lang.classfile.Opcode.POP; +import static java.lang.classfile.Opcode.POP2; +import static java.lang.classfile.Opcode.PUTFIELD; +import static java.lang.classfile.Opcode.PUTSTATIC; +import static java.lang.classfile.Opcode.RET; +import static java.lang.classfile.Opcode.RETURN; +import static java.lang.classfile.Opcode.SALOAD; +import static java.lang.classfile.Opcode.SASTORE; +import static java.lang.classfile.Opcode.SIPUSH; +import static java.lang.classfile.Opcode.SWAP; +import static java.lang.classfile.Opcode.TABLESWITCH; +import static java.lang.constant.ConstantDescs.CD_Class; +import static java.lang.constant.ConstantDescs.CD_Object; +import static java.lang.constant.ConstantDescs.CD_String; +import static java.lang.constant.ConstantDescs.CD_Throwable; +import static java.lang.constant.ConstantDescs.CD_boolean; +import static java.lang.constant.ConstantDescs.CD_byte; +import static java.lang.constant.ConstantDescs.CD_char; +import static java.lang.constant.ConstantDescs.CD_double; +import static java.lang.constant.ConstantDescs.CD_float; +import static java.lang.constant.ConstantDescs.CD_int; +import static java.lang.constant.ConstantDescs.CD_long; +import static java.lang.constant.ConstantDescs.CD_short; +import static java.lang.constant.ConstantDescs.CD_void; +import static java.nio.charset.StandardCharsets.UTF_8; + +import daikon.DynComp; +import daikon.chicory.ClassInfo; +import daikon.chicory.DaikonWriter; +import daikon.chicory.Instrument24; +import daikon.chicory.MethodGen24; +import daikon.chicory.MethodInfo; +import daikon.chicory.Runtime; +import daikon.plumelib.bcelutil.BcelUtil; +import daikon.plumelib.bcelutil.SimpleLog; +import daikon.plumelib.options.Option; +import daikon.plumelib.reflection.Signatures; +import daikon.plumelib.util.EntryReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintStream; +import java.lang.classfile.Annotation; +import java.lang.classfile.Attribute; +import java.lang.classfile.ClassBuilder; +import java.lang.classfile.ClassElement; +import java.lang.classfile.ClassFile; +import java.lang.classfile.ClassModel; +import java.lang.classfile.ClassTransform; +import java.lang.classfile.CodeBuilder; +import java.lang.classfile.CodeElement; +import java.lang.classfile.CodeModel; +import java.lang.classfile.FieldModel; +import java.lang.classfile.Instruction; +import java.lang.classfile.Interfaces; +import java.lang.classfile.Label; +import java.lang.classfile.MethodBuilder; +import java.lang.classfile.MethodElement; +import java.lang.classfile.MethodModel; +import java.lang.classfile.Opcode; +import java.lang.classfile.TypeKind; +import java.lang.classfile.attribute.CodeAttribute; +import java.lang.classfile.attribute.RuntimeVisibleAnnotationsAttribute; +import java.lang.classfile.constantpool.ClassEntry; +import java.lang.classfile.constantpool.ConstantPoolBuilder; +import java.lang.classfile.constantpool.LoadableConstantEntry; +import java.lang.classfile.constantpool.MethodRefEntry; +import java.lang.classfile.constantpool.NameAndTypeEntry; +import java.lang.classfile.instruction.ArrayLoadInstruction; +import java.lang.classfile.instruction.ArrayStoreInstruction; +import java.lang.classfile.instruction.BranchInstruction; +import java.lang.classfile.instruction.ConstantInstruction; +import java.lang.classfile.instruction.ExceptionCatch; +import java.lang.classfile.instruction.FieldInstruction; +import java.lang.classfile.instruction.InvokeDynamicInstruction; +import java.lang.classfile.instruction.InvokeInstruction; +import java.lang.classfile.instruction.LabelTarget; +import java.lang.classfile.instruction.LineNumber; +import java.lang.classfile.instruction.LoadInstruction; +import java.lang.classfile.instruction.LocalVariable; +import java.lang.classfile.instruction.LocalVariableType; +import java.lang.classfile.instruction.LookupSwitchInstruction; +import java.lang.classfile.instruction.NewMultiArrayInstruction; +import java.lang.classfile.instruction.NewReferenceArrayInstruction; +import java.lang.classfile.instruction.OperatorInstruction; +import java.lang.classfile.instruction.ReturnInstruction; +import java.lang.classfile.instruction.StackInstruction; +import java.lang.classfile.instruction.StoreInstruction; +import java.lang.classfile.instruction.SwitchCase; +import java.lang.classfile.instruction.TableSwitchInstruction; +import java.lang.classfile.instruction.ThrowInstruction; +import java.lang.constant.ClassDesc; +import java.lang.constant.MethodTypeDesc; +import java.lang.reflect.AccessFlag; +import java.net.URL; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Deque; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; +import java.util.regex.Pattern; +import org.checkerframework.checker.lock.qual.GuardSatisfied; +import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf; +import org.checkerframework.checker.nullness.qual.KeyFor; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; +import org.checkerframework.checker.signature.qual.BinaryName; +import org.checkerframework.checker.signature.qual.ClassGetName; +import org.checkerframework.checker.signature.qual.DotSeparatedIdentifiers; +import org.checkerframework.checker.signature.qual.FieldDescriptor; +import org.checkerframework.checker.signature.qual.Identifier; +import org.checkerframework.checker.signature.qual.MethodDescriptor; +import org.checkerframework.dataflow.qual.Pure; + +/** + * Instruments a class file to perform Dynamic Comparability. + * + *

This class is responsible for modifying bytecodes. Specifically, its main task is to add calls + * into the DynComp Runtime to calculate comparability values. These added calls are sometimes + * referred to as "hooks". + * + *

Instrument24 and DCInstrument24 use Java's ({@code java.lang.classfile}) APIs for reading and + * modifying .class files. Those APIs were added in JDK 24. Compared to BCEL, these APIs are more + * complete and robust (no more fiddling with StackMaps) and are always up to date with any .class + * file changes (since they are part of the JDK). (We will need to continue to support + * Instrument.java using BCEL, as we anticipate our clients using JDK 21 or less for quite some + * time.) + */ +public class DCInstrument24 { + + /** A log to which to print debugging information about program instrumentation. */ + protected static SimpleLog debugInstrument = new SimpleLog(false); + + /** + * Used when testing to continue processing if an error occurs. Currently, this flag is only used + * by BuildJDK. + */ + @Option("Halt if an instrumentation error occurs") + public static boolean quit_if_error = true; + + /** + * Start of user code for current method, prior to instrumenting, as a CodeModel Label. Note that + * there are two kinds of Labels in the {@code java.lang.classfile} API. When a class file is + * input, it contains CodeModel labels that are immutable. When our instrumention generates new + * code that needs a label it gets one from the CodeBuilder. + */ + protected @MonotonicNonNull Label oldStartLabel; + + /** Start of user code for current method, after instrumenting, as a CodeBuilder Label. */ + protected @MonotonicNonNull Label newStartLabel; + + /** + * Index into current method's instruction list that points to where a call to the DynComp runtime + * enter routine should be inserted (if needed) and where to define the newStartLabel. + */ + protected int newStartIndex; + + // Variables used for calculating the state of a method's operand stack via simulation. + + // TODO: The following four variables are redefined for each method instrumented. + // Rather than being global variables, they should be wrapped in a data structure that is passed + // to the places that need it. + + /** Mapping from a label to its index into the method's codeList. */ + @SuppressWarnings("nullness:initialization.static.field.uninitialized") // TODO: method-local + protected static Map labelIndexMap; + + /** + * Mapping from a label to its operand stack. This is the state of the operand stack at the label, + * prior to the execution of any instruction at that label. + */ + @SuppressWarnings("nullness:initialization.static.field.uninitialized") // TODO: method-local + protected static Map labelOperandStackMap; + + /** + * The state of operand stack prior to each byte code instruction of the method being simulated. + * There is a unique array entry for each instruction. + */ + @SuppressWarnings("nullness:initialization.static.field.uninitialized") // TODO: method-local + protected static OperandStack24[] stacks; + + /** + * The type of each parameter and local variable of the method being simulated. This value is + * often unchanged during the method execution, but may differ when the compiler allocates more + * than one local variable at the same offset. + */ + @SuppressWarnings("nullness:initialization.static.field.uninitialized") // TODO: method-local + protected static ClassDesc[] locals; + + /** + * Record containing a work item for the operand stack calculation. The instructionIndex is an + * index into the method's instruction list. The stack is the state of the operand stack prior to + * the execution of the instruction. + */ + record WorkItem(int instructionIndex, OperandStack24 stack) {} + + /** + * Comparator to sort WorkItems with lowest instructionIndex first. This corresponds to the lowest + * byte code instruction offset. + */ + protected static Comparator indexComparator = + new Comparator<>() { + @Override + public int compare(WorkItem w1, WorkItem w2) { + return Integer.compare(w1.instructionIndex, w2.instructionIndex); + } + }; + + /** Queue of items for a method's operand stack calculation. */ + protected static SortedSet worklist = new TreeSet<>(indexComparator); + + // Variables used for the entire class. + + /** The current class file. */ + protected ClassFile classFile; + + /** The current class. */ + protected ClassModel classModel; + + /** ConstantPool builder for current class. */ + private ConstantPoolBuilder poolBuilder; + + /** True if we are tracking any variables in any methods of the current class. */ + private boolean trackClass = false; + + /** ClassGen for the current class. */ + protected ClassGen24 classGen; + + /** Is the current class a member of the JDK? */ + protected boolean in_jdk; + + /** Has an {@code } method completed initialization? */ + protected boolean constructor_is_initialized; + + /** + * Record used to describe a new LocalVariable. When instrumentation wants to create a new method, + * it creates a list containing one of these records for each local variable of the method. This + * list is passed to {@link #copyCode} and {@link #instrumentCode} where the new variables are + * created. + */ + public record myLocalVariable(int slot, String name, ClassDesc descriptor) {} + + /** + * A local variable added by our instrumentation to each method. It is an {@code Object} array, + * always named {@code dcomp_tag_frame$5A}. The DynComp runtime uses the array to store the + * current tag value for each parameter on method entry. These tag values are used to track + * variable interactions. This allows the DynComp runtime to group variables into comparability + * sets. All variables in a comparability set belong to the same "abstract type" of data that the + * programmer likely intended to represent. + */ + protected LocalVariable tagFrameLocal; + + // Type descriptors + + /** "java.lang.Object[]". */ + protected static final ClassDesc objectArrayCD = CD_Object.arrayType(1); + + /** The special DCompMarker type. */ + protected final ClassDesc dcomp_marker; + + // Signature descriptors + + // No parameters + + /** Type array with no parameters. */ + protected static final ClassDesc[] noArgsSig = new ClassDesc[0]; + + // One parameter + + /** Type array with an int. */ + protected static ClassDesc[] intSig = {CD_int}; + + /** Type array with a long. */ + protected static ClassDesc[] longSig = {CD_long}; + + /** Type array with an object. */ + protected static ClassDesc[] object_arg = {CD_Object}; + + /** Type array with an Object array. */ + protected static final ClassDesc[] objectArrayCD_arg = {objectArrayCD}; + + // Two parameters + + /** Type array with a long and an int. */ + protected static ClassDesc[] longIntSig = {CD_long, CD_int}; + + /** Type array with two objects. */ + protected static ClassDesc[] objectObjectSig = {CD_Object, CD_Object}; + + // Debug loggers + + /** Log file if debug_native is enabled. */ + protected static SimpleLog debug_native = new SimpleLog(false); + + /** Log file if debug_dup is enabled. */ + protected static SimpleLog debug_dup = new SimpleLog(false); + + /** + * Debug information about which classes and/or methods are transformed and why. Use + * debugInstrument for actual instrumentation details. + */ + protected static SimpleLog debug_transform = new SimpleLog(false); + + // Flags to enable additional console output for debugging + /** If true, enable JUnit analysis debugging. */ + protected static final boolean debugJunitAnalysis = false; + + /** If true, enable {@link #getDefiningInterface} debugging. */ + protected static final boolean debugGetDefiningInterface = false; + + /** If true, enable {@link #handleInvoke} debugging. */ + protected static final boolean debugHandleInvoke = false; + + /** If true, enable operand stack debugging. */ + protected static final boolean debugOperandStack = false; + + // End of debug loggers. + + /** Keeps track of the methods that were not successfully instrumented. */ + protected List skipped_methods = new ArrayList<>(); + + /** If we're using an instrumented JDK, then "java.lang"; otherwise, "daikon.dcomp". */ + protected @DotSeparatedIdentifiers String dcompMarkerPrefix; + + /** + * If we're using an instrumented JDK and the JDK version is 9 or higher, then "java.lang"; + * otherwise, "daikon.dcomp". + */ + protected @DotSeparatedIdentifiers String dcompRuntimePrefix; + + /** Either "daikon.dcomp.DCRuntime" or "java.lang.DCRuntime". */ + private @BinaryName String dcompRuntimeClassName; + + /** The ClassDesc for the DynComp runtime support class. */ + private ClassDesc runtimeCD; + + /** Set of JUnit test classes. */ + protected static Set junitTestClasses = new HashSet<>(); + + /** Possible states of JUnit test discovery. */ + protected enum JunitState { + /** Have not seen a JUnit class file. */ + NOT_SEEN, + /** Have seen a JUnit class file. */ + STARTING, + /** Have seen a JUnit class file that loads JUnit test classes. */ + TEST_DISCOVERY, + /** Have completed identifying JUnit test classes and are instrumenting the code. */ + INSTRUMENTING + } + + /** Current state of JUnit test discovery. */ + protected static JunitState junit_state = JunitState.NOT_SEEN; + + /** Have we seen {@code JUnitCommandLineParseResult.parse}? */ + protected static boolean junit_parse_seen = false; + + /** + * Map from each static qualified field name to a unique integer id. Note that while it's + * intuitive to think that each static should show up exactly once, that is not always true. A + * static defined in a superclass can be accessed through each of its subclasses. In this case, + * tag accessor methods must be added in each subclass and each should return the id of the field + * in the superclass. This map is populated in {@link build_field_to_offset_map} and used in + * {@link create_tag_accessors}. + */ + static Map static_field_id = new LinkedHashMap<>(); + + /** + * Map from binary class name to its access_flags. Used to cache the results of the lookup done in + * {@link #getAccessFlags}. If a class is marked ACC_ANNOTATION then it will not have been + * instrumented. + */ + static Map accessFlags = new HashMap<>(); + + /** Integer constant of access_flag value of ACC_ANNOTATION. */ + static Integer Integer_ACC_ANNOTATION = Integer.valueOf(ACC_ANNOTATION); + + /** + * Array of classes whose fields are not initialized from Java (i.e., these classes are + * initialized by the JVM). Since the fields are not initialized from Java, their tag storage is + * not allocated as part of a store, but rather must be allocated as part of a load. We call a + * special runtime method for this so that we can check for this in other cases. + */ + protected static final String[] uninit_classes = { + "java.lang.String", + "java.lang.Class", + "java.lang.StringBuilder", + "java.lang.AbstractStringBuilder", + }; + + /** + * List of Object methods. Since we can't instrument Object, none of these can be instrumented, + * and most of them don't provide useful comparability information anyway. + * + *

The equals method and the clone method are not listed here. They are special-cased in the + * {@link #handleInvoke} routine. + */ + protected static final MethodDef[] obj_methods = { + new MethodDef("finalize", noArgsSig), + new MethodDef("getClass", noArgsSig), + new MethodDef("hashCode", noArgsSig), + new MethodDef("notify", noArgsSig), + new MethodDef("notifyall", noArgsSig), + new MethodDef("toString", noArgsSig), + new MethodDef("wait", noArgsSig), + new MethodDef("wait", longSig), + new MethodDef("wait", longIntSig), + }; + + /** Represents a method (by its name and parameter types). */ + static class MethodDef { + /** Name of this method. */ + String name; + + /** Parameter types for this method. */ + ClassDesc[] arg_types; + + /** + * Create a new MethodDef. + * + * @param name of method + * @param arg_types of method + */ + MethodDef(String name, ClassDesc[] arg_types) { + this.name = name; + this.arg_types = arg_types; + } + + /** + * Equality test for MethodDef. + * + * @param name of method + * @param arg_types of method + */ + @EnsuresNonNullIf(result = true, expression = "#1") + boolean equals(@GuardSatisfied MethodDef this, String name, ClassDesc[] arg_types) { + if (!name.equals(this.name)) { + return false; + } + return Arrays.equals(this.arg_types, arg_types); + } + + @EnsuresNonNullIf(result = true, expression = "#1") + @Pure + @Override + public boolean equals(@GuardSatisfied MethodDef this, @GuardSatisfied @Nullable Object obj) { + if (!(obj instanceof MethodDef)) { + return false; + } + MethodDef md = (MethodDef) obj; + return equals(md.name, md.arg_types); + } + + @Pure + @Override + public int hashCode(@GuardSatisfied MethodDef this) { + return Objects.hash(name, Arrays.hashCode(arg_types)); + } + } + + /** + * Initialize the class information and whether or not that class is part of the JDK. Note that + * the fields poolBuilder, classGen and tagFrameLocal are not initialized until later. + */ + @SuppressWarnings("nullness:initialization") + public DCInstrument24(ClassFile classFile, ClassModel classModel, boolean in_jdk) { + this.classFile = classFile; + this.classModel = classModel; + this.in_jdk = in_jdk; + constructor_is_initialized = false; + if (Premain.jdk_instrumented) { + dcompMarkerPrefix = "java.lang"; + } else { + dcompMarkerPrefix = "daikon.dcomp"; + } + dcomp_marker = ClassDesc.of(Signatures.addPackage(dcompMarkerPrefix, "DCompMarker")); + dcompRuntimePrefix = dcompMarkerPrefix; + if (BcelUtil.javaVersion == 8) { + dcompRuntimePrefix = "daikon.dcomp"; + } + dcompRuntimeClassName = Signatures.addPackage(dcompRuntimePrefix, "DCRuntime"); + runtimeCD = ClassDesc.of(dcompRuntimeClassName); + + // Turn on some of the logging based on debug option. + debugInstrument.enabled = DynComp.debug || Premain.debug_dcinstrument; + debug_native.enabled = DynComp.debug; + debug_transform.enabled = daikon.dcomp.Instrument24.debug_transform.enabled; + + if (debugOperandStack) { + // Create a new PrintStream with autoflush enabled. + PrintStream newOut = new PrintStream(System.out, true, UTF_8); + // Reassign System.out to the new PrintStream. + System.setOut(newOut); + // Create a new PrintStream with autoflush enabled. + PrintStream newErr = new PrintStream(System.err, true, UTF_8); + // Reassign System.err to the new PrintStream. + System.setErr(newErr); + } + } + + /** + * Instruments a class to perform dynamic comparability and returns the new class definition. A + * second version of each method in the class is created which is instrumented for comparability. + * + * @return the modified JavaClass + */ + public byte @Nullable [] instrument(ClassInfo classInfo) { + + // Don't know where I got this idea. They are executed. Don't remember why + // adding dcomp marker causes problems. + // Don't instrument annotations. They aren't executed and adding + // the marker argument causes subtle errors + if (classModel.flags().has(AccessFlag.ANNOTATION)) { + debug_transform.log("Not instrumenting annotation %s%n", classInfo.class_name); + return null; + } + + // If a class has an EvoSuite annotation it may be instrumented by Evosuite; + // thus, we should not instrument it before Evosuite does. + for (final Attribute attribute : classModel.attributes()) { + if (attribute instanceof RuntimeVisibleAnnotationsAttribute rvaa) { + for (final Annotation item : rvaa.annotations()) { + if (item.className().stringValue().startsWith("Lorg/evosuite/runtime")) { + debug_transform.log( + "Not instrumenting possible Evosuite target: %s%n", classInfo.class_name); + return null; + } + } + } + } + + try { + return classFile.build( + classModel.thisClass().asSymbol(), + classBuilder -> instrumentClass(classBuilder, classModel, classInfo)); + } catch (Throwable t) { + if (debugInstrument.enabled) { + t.printStackTrace(); + } + System.err.printf( + "DynComp warning: Class %s is being skipped due to the following:%n", + classInfo.class_name); + System.err.printf("%s.%n", t); + return null; + } + } + + /** + * Instrument the given class. + * + * @param classBuilder for the class + * @param classModel for the class + * @param classInfo for the given class + */ + private void instrumentClass( + ClassBuilder classBuilder, ClassModel classModel, ClassInfo classInfo) { + + @BinaryName String classname = classInfo.class_name; + classGen = new ClassGen24(classModel, classname, classBuilder); + poolBuilder = classBuilder.constantPool(); + + debug_transform.log( + "%nInstrumenting class%s: %s%n", in_jdk ? " (in JDK)" : "", classInfo.class_name); + debug_transform.indent(); + + debugInstrument.log("Attributes:%n"); + for (Attribute a : classModel.attributes()) { + debugInstrument.log(" %s%n", a); + } + + debugInstrument.log("Class Interfaces:%n"); + for (ClassEntry ce : classModel.interfaces()) { + debugInstrument.log(" %s%n", ce.asInternalName()); + } + + trackClass = false; + + // Check to see if this class has a {@code clone} or {@code toString} method. + // If so, add the {@code DCompClone} and/or the {@code DCompToString} interface. + add_clone_and_tostring_interfaces(classGen); + + boolean junit_test_class = false; + + if (!in_jdk) { + // A very tricky special case: If JUnit is running and the current + // class has been passed to JUnit on the command line, then this + // is a JUnit test class and our normal instrumentation will + // cause JUnit to complain about multiple constructors and + // methods that should have no arguments. To work around these + // restrictions, we replace rather than duplicate each method + // we instrument and we do not add the dcomp marker parameter. + // We must also remember the class name so if we see a subsequent + // call to one of its methods we do not add the dcomp argument. + + debugInstrument.log("junit_state: %s%n", junit_state); + + StackTraceElement[] stack_trace; + + switch (junit_state) { + case NOT_SEEN: + if (classname.startsWith("org.junit")) { + junit_state = JunitState.STARTING; + } + break; + + case STARTING: + // Now check to see if JUnit is looking for test class(es). + stack_trace = Thread.currentThread().getStackTrace(); + // [0] is getStackTrace + for (int i = 1; i < stack_trace.length; i++) { + if (debugJunitAnalysis) { + System.out.printf( + "%s : %s%n", stack_trace[i].getClassName(), stack_trace[i].getMethodName()); + } + if (isJunitTrigger(stack_trace[i].getClassName(), stack_trace[i].getMethodName())) { + junit_parse_seen = true; + junit_state = JunitState.TEST_DISCOVERY; + break; + } + } + break; + + case TEST_DISCOVERY: + // Now check to see if JUnit is done looking for test class(es). + boolean local_junit_parse_seen = false; + stack_trace = Thread.currentThread().getStackTrace(); + // [0] is getStackTrace + for (int i = 1; i < stack_trace.length; i++) { + if (debugJunitAnalysis) { + System.out.printf( + "%s : %s%n", stack_trace[i].getClassName(), stack_trace[i].getMethodName()); + } + if (isJunitTrigger(stack_trace[i].getClassName(), stack_trace[i].getMethodName())) { + local_junit_parse_seen = true; + break; + } + } + if (junit_parse_seen && !local_junit_parse_seen) { + junit_parse_seen = false; + junit_state = JunitState.INSTRUMENTING; + } else if (!junit_parse_seen && local_junit_parse_seen) { + junit_parse_seen = true; + } + break; + + case INSTRUMENTING: + if (debugJunitAnalysis) { + stack_trace = Thread.currentThread().getStackTrace(); + // [0] is getStackTrace + for (int i = 1; i < stack_trace.length; i++) { + System.out.printf( + "%s : %s%n", stack_trace[i].getClassName(), stack_trace[i].getMethodName()); + } + } + // nothing to do + break; + + default: + throw new DynCompError("invalid junit_state"); + } + + debugInstrument.log("junit_state: %s%n", junit_state); + + if (junit_state == JunitState.TEST_DISCOVERY) { + // We have a possible JUnit test class. We need to verify by + // one of two methods. Either the class is a subclass of + // junit.framework.TestCase or one of its methods has a + // RuntimeVisibleAnnotation of org/junit/Test. + Deque classnameStack = new ArrayDeque<>(); + String super_class; + String this_class = classname; + while (true) { + super_class = getSuperclassName(this_class); + if (super_class == null) { + // something has gone wrong + break; + } + if (debugJunitAnalysis) { + System.out.printf("this_class: %s%n", this_class); + System.out.printf("super_class: %s%n", super_class); + } + if (super_class.equals("junit.framework.TestCase")) { + // This is a JUnit test class and so are the + // elements of classnameStack. + junit_test_class = true; + junitTestClasses.add(this_class); + while (!classnameStack.isEmpty()) { + junitTestClasses.add(classnameStack.pop()); + } + break; + } else if (super_class.equals("java.lang.Object")) { + // We're done; not a JUnit test class. + // Ignore items on classnameStack. + break; + } + // Recurse and check the super_class. + classnameStack.push(this_class); + this_class = super_class; + } + } + + // Even if we have not detected that JUnit is active, any class that + // contains a method with a RuntimeVisibleAnnotation of org/junit/Test + // needs to be marked as a JUnit test class. (Daikon issue #536) + + if (!junit_test_class) { + // need to check for JUnit Test annotation on a method + searchloop: + for (MethodModel mm : classModel.methods()) { + for (final Attribute attribute : mm.attributes()) { + if (attribute instanceof RuntimeVisibleAnnotationsAttribute rvaa) { + if (debugJunitAnalysis) { + System.out.printf("attribute: %s%n", attribute); + } + for (final Annotation item : rvaa.annotations()) { + String description = item.className().stringValue(); + if (debugJunitAnalysis) { + System.out.printf("item: %s%n", description); + } + if (description.endsWith("org/junit/Test;") // JUnit 4 + || description.endsWith("org/junit/jupiter/api/Test;") // JUnit 5 + ) { + junit_test_class = true; + junitTestClasses.add(classname); + break searchloop; + } + } + } + } + } + } + + if (junit_test_class) { + debugInstrument.log("JUnit test class: %s%n", classname); + } else { + debugInstrument.log("Not a JUnit test class: %s%n", classname); + } + } + + classInfo.isJunitTestClass = junit_test_class; + + if (classModel.majorVersion() < ClassFile.JAVA_6_VERSION) { + System.out.printf( + "DynComp warning: ClassFile: %s - class file version (%d) is out of date and may not be" + + " processed correctly.%n", + classname, classModel.majorVersion()); + // throw new DynCompError("Classfile out of date"); + } + + processAllMethods(classModel, classBuilder, classInfo); + + // Have all top-level classes implement the DCompInstrumented interface. + if (classGen.getSuperclassName().equals("java.lang.Object")) { + @SuppressWarnings("signature:assignment") // CF needs regex for @MethodDescriptor + @MethodDescriptor String objectToBoolean = "(Ljava/lang/Object;)Z"; + // Add equals method if it doesn't already exist. This ensures + // that an instrumented version, equals(Object, DCompMarker), + // will be created in this class. + MethodModel eq = classGen.containsMethod("equals", objectToBoolean); + if (eq == null) { + debugInstrument.log("Adding equals method%n"); + add_equals_method(classBuilder, classGen, classInfo); + } + + // Add DCompInstrumented interface and the required + // equals_dcomp_instrumented method. + add_dcomp_interface(classBuilder, classGen, classInfo); + } + + // Add interfaces to the class being built. + classBuilder.withInterfaces(classGen.getInterfaceList()); + + // Copy all other ClassElements to output class unchanged. + for (ClassElement ce : classModel) { + debugInstrument.log("ClassElement: %s%n", ce); + switch (ce) { + case MethodModel mm -> {} + case Interfaces i -> {} + // Copy all other ClassElements to output class unchanged. + default -> classBuilder.with(ce); + } + } + + // Add tag accessor methods for each primitive in the class. + create_tag_accessors(classGen); + + // We don't need to track class initialization in the JDK because + // that is only used when printing comparability which is only done + // for client classes. + if (!in_jdk) { + // If no clinit method, we need to add our own. + if (!classInfo.hasClinit) { + createClinit(classBuilder, classInfo); + } + + // The code that builds the list of daikon variables for each ppt + // needs to know what classes are instrumented. Its looks in the + // Chicory runtime for this information. + if (trackClass) { + debug_transform.log("DCInstrument adding %s to all class list%n", classInfo.class_name); + synchronized (daikon.chicory.SharedData.all_classes) { + daikon.chicory.SharedData.all_classes.add(classInfo); + } + } + } + + debug_transform.exdent(); + debug_transform.log("Instrumentation complete: %s%n", classInfo.class_name); + } + + /** + * Adds a call to the DynComp Runtime {@code set_class_initialized} method at the begining of + * {@code mgen}. Clients pass the class static initializer {@code } as {@code mgen}. + * + * @param mgen the method to modify, should be the class static initializer {@code } + * @param classInfo for the given class + */ + private void addInvokeToClinit(MethodGen24 mgen, ClassInfo classInfo) { + + try { + List il = mgen.getInstructionList(); + // point to start of list + ListIterator li = il.listIterator(); + + for (CodeElement ce : createCodeToMarkClassInitialized(poolBuilder, classInfo)) { + li.add(ce); + } + } catch (DynCompError e) { + throw e; + } catch (Throwable t) { + throw new DynCompError( + String.format("Error processing %s.%s.%n", mgen.getClassName(), mgen.getName()), t); + } + } + + /** + * Create a class initializer method, if none exists. We need a class initializer to have a place + * to insert a call to the DynComp Runtime {@code set_class_initialized} method. + * + * @param classBuilder for the given class + * @param classInfo for the given class + */ + private void createClinit(ClassBuilder classBuilder, ClassInfo classInfo) { + + List instructions = createCodeToMarkClassInitialized(poolBuilder, classInfo); + instructions.add(ReturnInstruction.of(TypeKind.VOID)); // need to return! + + classBuilder.withMethod( + "", + MethodTypeDesc.of(CD_void), + ACC_STATIC, + methodBuilder -> + methodBuilder.withCode(codeBuilder -> copyCode(codeBuilder, instructions, null))); + } + + /** + * Create the list of instructions for a call to {@code set_class_initialized}. This is used to + * keep track of when the class is initialized (so we don't look for fields in uninitialized + * classes). + * + * @param poolBuilder for the given class + * @param classInfo for the given class + * @return the instruction list + */ + private List createCodeToMarkClassInitialized( + ConstantPoolBuilder poolBuilder, ClassInfo classInfo) { + + List instructions = new ArrayList<>(); + + MethodRefEntry mre = + poolBuilder.methodRefEntry( + runtimeCD, "set_class_initialized", MethodTypeDesc.of(CD_void, CD_String)); + instructions.add(buildLDCInstruction(poolBuilder.stringEntry(classInfo.class_name))); + instructions.add(InvokeInstruction.of(INVOKESTATIC, mre)); + + return instructions; + } + + /** + * Instruments all the methods in a class to perform dynamic comparability. + * + * @param classModel for current class + * @param classBuilder for current class + * @param classInfo for the given class + */ + private void processAllMethods( + ClassModel classModel, ClassBuilder classBuilder, ClassInfo classInfo) { + for (MethodModel mm : classModel.methods()) { + processMethod(mm, classModel, classBuilder, classInfo); + } + } + + // The process of instrumenting a method is structured as a hierarchical pipeline: each layer + // operates at a different level of abstraction and delegates detailed processing to the next + // layer. + // + // processMethod: + // checks if need to track values of local variables + // adds DCompArgument, if required + // + // instrumentMethod: + // processes all non-code method elements and copies them to output + // calls instrumentCode to process the code attribute + // + // instrumentCode: + // process code labels + // calls instrumentCodeList + // copies instrumented code to output + // + // instrumentCodeList: + // add code to create the tag frame local + // calculate the state of the operand stack at each instruction + // calls instrumentInstruction + // + // instrumentInstruction: + // processes and instruments (if required) each individual instruction + // + + /** + * Instruments a method to perform dynamic comparability. It adds instrumentation code at the + * entry and at each return from the method. In addition, it adds instrumentation code to the body + * of the method to track variable interactions. When the instrumented method is executed, this + * instrumentation allows the DynComp runtime to group variables into comparability sets. All + * variables in a comparability set belong to the same "abstract type" of data that the programmer + * likely intended to represent. + * + * @param methodModel for current method + * @param classModel for current class + * @param classBuilder for current class + * @param classInfo for the given class + */ + private void processMethod( + MethodModel methodModel, + ClassModel classModel, + ClassBuilder classBuilder, + ClassInfo classInfo) { + + @BinaryName String classname = classInfo.class_name; + + try { + MethodGen24 mgen = new MethodGen24(methodModel, classname, classBuilder); + + if (debugInstrument.enabled) { + ClassDesc[] paramTypes = mgen.getParameterTypes(); + String[] paramNames = mgen.getParameterNames(); + LocalVariable[] local_vars = mgen.getOriginalLocalVariables(); + String types = "", names = "", locals = ""; + + for (int j = 0; j < paramTypes.length; j++) { + @FieldDescriptor String paramFD = paramTypes[j].descriptorString(); + types = + types + daikon.chicory.Instrument24.convertDescriptorToFqBinaryName(paramFD) + " "; + } + for (int j = 0; j < paramNames.length; j++) { + names = names + paramNames[j] + " "; + } + for (int j = 0; j < local_vars.length; j++) { + locals = locals + local_vars[j].name().stringValue() + " "; + } + debugInstrument.log("%nMethod = %s%n", mgen.getName()); + debugInstrument.log("paramTypes(%d): %s%n", paramTypes.length, types); + debugInstrument.log("paramNames(%d): %s%n", paramNames.length, names); + debugInstrument.log("localvars(%d): %s%n", local_vars.length, locals); + // debugInstrument.log("Original code: %s%n", mgen.getMethod().getCode()); + debugInstrument.log("Method Attributes:%n"); + for (Attribute a : methodModel.attributes()) { + debugInstrument.log(" %s%n", a); + } + debugInstrument.log("mgen.getSignature: %s%n", mgen.getSignature()); + MethodTypeDesc mtd = methodModel.methodTypeSymbol(); + debugInstrument.log("mtd.descriptorString: %s%n", mtd.descriptorString()); + debugInstrument.log("mtd.displayDescriptor: %s%n", mtd.displayDescriptor()); + } + + boolean track = false; + if (!in_jdk) { + // Note whether we want to track the variables in this method. + track = should_track(classname, mgen.getName(), methodEntryName(classname, mgen)); + + // We cannot track the variables in bridge methods the compiler has synthesized as + // they are overloaded on return type which normal Java does not support. + if ((mgen.getAccessFlagsMask() & ACC_BRIDGE) != 0) { + track = false; + } + + // If a method has its variables tracked, then we need to mark the class as having some + // instrumented variables. + if (track) { + trackClass = true; + } + + // If we are tracking variables, make sure the class is public + int access_flags = classModel.flags().flagsMask(); + if (track && (access_flags & ACC_PUBLIC) == 0) { + access_flags |= ACC_PUBLIC; + access_flags &= ~ACC_PROTECTED; + access_flags &= ~ACC_PRIVATE; + } + // reset class access flags in case they have been changed + classBuilder.withFlags(access_flags); + } else { + // If JDK, don't modify class initialization methods. They can't affect + // user comparability and there isn't any way to get a second + // copy of them. + if (mgen.isClinit()) { + debugInstrument.log("Copying method: %s%n", mgen.getName()); + debugInstrument.indent(); + outputMethodUnchanged(classBuilder, methodModel, mgen); + debugInstrument.exdent(); + debugInstrument.log("End of copy%n"); + return; + } + } + + debug_transform.log(" Processing method %s, track=%b%n", simplify_method_name(mgen), track); + debug_transform.indent(); + + // `final` because local variables referenced from a lambda expression must be final or + // effectively final. + final boolean trackMethod = track; + + // main methods, methods and all methods in a JUnit test class + // need special treatment. They are not duplicated and they do not have + // a DCompMarker added to their parameter list. + boolean addingDcompArg = true; + + if (mgen.isClinit()) { + classInfo.hasClinit = true; + addInvokeToClinit(mgen, classInfo); + addingDcompArg = false; + } + + if (mgen.isMain()) { + addingDcompArg = false; + createMainStub(mgen, classBuilder, classInfo); + } + + if (classInfo.isJunitTestClass) { + addingDcompArg = false; + } + + if (addingDcompArg) { + // make copy of original method + debugInstrument.log("Copying method: %s%n", mgen.getName()); + debugInstrument.indent(); + outputMethodUnchanged(classBuilder, methodModel, mgen); + debugInstrument.exdent(); + debugInstrument.log("End of copy%n"); + } + + MethodTypeDesc mtd = methodModel.methodTypeSymbol(); + if (addingDcompArg) { + // The original parameterList is immutable, so we need to make a copy. + List paramList = new ArrayList(mtd.parameterList()); + paramList.add(dcomp_marker); + mtd = MethodTypeDesc.of(mtd.returnType(), paramList); + } + classBuilder.withMethod( + methodModel.methodName().stringValue(), + mtd, + methodModel.flags().flagsMask(), + methodBuilder -> + instrumentMethod(methodBuilder, methodModel, mgen, classInfo, trackMethod)); + + debug_transform.exdent(); + } catch (Throwable t) { + if (debugInstrument.enabled) { + t.printStackTrace(); + } + String method = classname + "." + methodModel.methodName().stringValue(); + skip_method(method); + if (quit_if_error) { + if (t instanceof DynCompError) { + throw t; + } + throw new DynCompError("Error processing " + method, t); + } else { + System.err.printf("Error processing %s: %s%n", method, t); + System.err.printf("Method is NOT instrumented.%n"); + } + } + } + + /** + * Copy the given method from the input class file to the output class with no changes. Uses + * {@code copyMethod} to perform the actual copy. + * + * @param classBuilder for the output class + * @param mm MethodModel describes the input method + * @param mgen describes the output method + */ + private void outputMethodUnchanged(ClassBuilder classBuilder, MethodModel mm, MethodGen24 mgen) { + classBuilder.withMethod( + mm.methodName().stringValue(), + mm.methodTypeSymbol(), + mm.flags().flagsMask(), + methodBuilder -> copyMethod(methodBuilder, mm, mgen)); + } + + /** + * Copy the given method from the input class file to the output class with no changes. + * + * @param methodBuilder for the output class + * @param methodModel describes the input method + * @param mgen describes the output method + */ + private void copyMethod(MethodBuilder methodBuilder, MethodModel methodModel, MethodGen24 mgen) { + + for (MethodElement me : methodModel) { + debugInstrument.log("MethodElement: %s%n", me); + switch (me) { + case CodeModel codeModel -> + methodBuilder.withCode( + codeBuilder -> copyCode(codeBuilder, mgen.getInstructionList(), null)); + + // copy all other MethodElements to output class (unchanged) + default -> methodBuilder.with(me); + } + } + } + + /** + * Copy an instruction list into its corresponding method in the output file. In addition, if the + * {@code newLocals} list is not empty, create new local variables in the output method. + * + * @param codeBuilder for the given method's code + * @param instructions instruction list to copy + * @param newLocals method scope locals to define; may be null if none + */ + private void copyCode( + CodeBuilder codeBuilder, + List instructions, + @Nullable List newLocals) { + + if (newLocals != null) { + for (myLocalVariable lv : newLocals) { + codeBuilder.localVariable( + lv.slot(), + lv.name(), + lv.descriptor(), + codeBuilder.startLabel(), + codeBuilder.endLabel()); + } + } + + for (CodeElement ce : instructions) { + debugInstrument.log("CodeElement: %s%n", ce); + codeBuilder.with(ce); + } + } + + /** + * Instrument the specified method for dynamic comparability. + * + * @param methodBuilder for the given method + * @param methodModel for the given method + * @param mgen describes the given method + * @param classInfo for the current class + * @param trackMethod true iff we need to track the daikon variables in this method + */ + private void instrumentMethod( + MethodBuilder methodBuilder, + MethodModel methodModel, + MethodGen24 mgen, + ClassInfo classInfo, + boolean trackMethod) { + + try { + boolean codeModelSeen = false; + for (MethodElement me : methodModel) { + debugInstrument.log("MethodElement: %s%n", me); + switch (me) { + case CodeModel codeModel -> { + codeModelSeen = true; + methodBuilder.withCode( + codeBuilder -> + instrumentCode(codeBuilder, codeModel, null, mgen, classInfo, trackMethod)); + } + case RuntimeVisibleAnnotationsAttribute rvaa -> { + // We do not want to copy the @HotSpotIntrinsicCandidate annotations from + // the original method to our instrumented method as the signature will + // not match anything in the JVM's list. This won't cause an execution + // problem but will produce a number of warnings. + // JDK 11: @HotSpotIntrinsicCandidate + // JDK 17: @IntrinsicCandidate + boolean output = true; + for (final Annotation item : rvaa.annotations()) { + String description = item.className().stringValue(); + if (description.endsWith("IntrinsicCandidate;")) { + output = false; + debugInstrument.log("Annotation not copied: %s%n", description); + } + } + if (output) { + methodBuilder.with(me); + } + } + // copy all other MethodElements to output class (unchanged) + default -> methodBuilder.with(me); + } + } + if (!codeModelSeen) { + debugInstrument.log("No CodeModel for method: %s%n", mgen.getName()); + if ((mgen.getAccessFlagsMask() & ACC_NATIVE) != 0) { + // We need to build our wrapper method for a call to native code. + methodBuilder.withCode( + codeBuilder -> instrumentCode(codeBuilder, null, null, mgen, classInfo, trackMethod)); + + // Turn off the native flag in wrapper method. + methodBuilder.withFlags(mgen.getAccessFlagsMask() & ~ACC_NATIVE); + } else { + // Interface and/or Abstract; do nothing. + } + } + } catch (DynCompError e) { + throw e; + } catch (Throwable t) { + throw new DynCompError("Error processing " + classInfo.class_name + "." + mgen.getName(), t); + } + } + + /** + * Generate instrumentation code for the given method. This includes reading in and processing the + * original instruction list, calling {@code insertInstrumentationCode} to add the instrumentation + * code, and then copying the modified instruction list to the output method while updating the + * code labels, if needed. In addition, if the {@code newLocals} list is not empty, add them to + * the {@code localsTable}. + * + *

If DCInstrument24 generated the method or if it is a native method, then {@code codeModel} + * == null. + * + * @param codeBuilder for the given method's code + * @param codeModel for the input method's code; may be null + * @param newLocals method scope locals to define; may be null if none + * @param mgen describes the output method + * @param classInfo for the current class + * @param trackMethod true iff we need to track the daikon variables in this method + */ + private void instrumentCode( + CodeBuilder codeBuilder, + @Nullable CodeModel codeModel, + @Nullable List newLocals, + MethodGen24 mgen, + ClassInfo classInfo, + boolean trackMethod) { + + // method_info_index is not used at this point in DCInstrument + MethodGen24.MInfo24 minfo = new MethodGen24.MInfo24(0, mgen.getMaxLocals(), codeBuilder); + debugInstrument.log("nextLocalIndex: %d%n", minfo.nextLocalIndex); + + if (newLocals != null) { + for (myLocalVariable lv : newLocals) { + mgen.localsTable.add( + LocalVariable.of( + lv.slot(), + lv.name(), + lv.descriptor(), + codeBuilder.startLabel(), + codeBuilder.endLabel())); + } + mgen.setOriginalLocalVariables( + mgen.localsTable.toArray(new LocalVariable[mgen.localsTable.size()])); + } + + // Clean up parameter names and add in any unused parameters that the Java compiler has + // optimized out. + if (mgen.fixLocals(minfo)) { + // localsTable was changed + debugInstrument.log("Revised LocalVariableTable:%n"); + for (LocalVariable lv : mgen.localsTable) { + debugInstrument.log(" %s%n", lv); + } + } + + if (debugInstrument.enabled) { + String[] paramNames = mgen.getParameterNames(); + debugInstrument.log("paramNames: %s%n", paramNames.length); + for (String paramName : paramNames) { + debugInstrument.log("param name: %s%n", paramName); + } + } + + debugInstrument.log("nextLocalIndex: %d%n", minfo.nextLocalIndex); + + // If the method is native + if ((mgen.getAccessFlagsMask() & ACC_NATIVE) != 0) { + + debugInstrument.log("Native Method%n"); + // Create Java code that cleans up the tag stack and calls the real native method. + fix_native(mgen); + + // Add the DCompMarker parameter to distinguish our version. + add_dcomp_param(mgen, minfo); + + // Copy the modified local variable table to the output class. + debugInstrument.log("LocalVariableTable:%n"); + for (LocalVariable lv : mgen.localsTable) { + codeBuilder.localVariable( + lv.slot(), lv.name().stringValue(), lv.typeSymbol(), lv.startScope(), lv.endScope()); + @FieldDescriptor String lvFD = lv.typeSymbol().descriptorString(); + debugInstrument.log( + " %s : %s%n", lv, daikon.chicory.Instrument24.convertDescriptorToFqBinaryName(lvFD)); + } + + // Copy the modified instruction list to the output class. + for (CodeElement ce : mgen.getInstructionList()) { + debugInstrument.log("CodeElement: %s%n", ce); + codeBuilder.with(ce); + } + + } else { // normal method + + if (!classInfo.isJunitTestClass) { + // Add the DCompMarker parameter to distinguish our version. + add_dcomp_param(mgen, minfo); + } + + if (debugInstrument.enabled) { + System.out.printf("nextLocalIndex: %d%n", minfo.nextLocalIndex); + System.out.printf("LocalVariableTable:%n"); + for (LocalVariable lv : mgen.localsTable) { + System.out.printf(" %s%n", lv); + } + System.out.println( + "instrumentCode - param types: " + Arrays.toString(mgen.getParameterTypes())); + System.out.printf("length: %d%n", mgen.getParameterTypes().length); + } + + int paramCount = (mgen.isStatic() ? 0 : 1) + mgen.getParameterTypes().length; + debugInstrument.log("paramCount: %d%n", paramCount); + + // Create a MethodInfo that describes this method's parameters + // and exit line numbers (information not available via reflection) + // and add it to the list for this class. + MethodInfo mi = null; + if (trackMethod && !in_jdk) { + @SuppressWarnings("nullness:assignment") // the method exists + @NonNull MethodInfo miTmp = create_method_info_if_instrumented(classInfo, mgen); + mi = miTmp; + classInfo.method_infos.add(mi); + DCRuntime.methods.add(mi); + } + + @SuppressWarnings("JdkObsolete") + List codeList = new LinkedList<>(); + + // no codeModel if DCInstrument24 generated the method + if (codeModel != null) { + debugInstrument.log("Code Attributes:%n"); + for (Attribute a : codeModel.attributes()) { + debugInstrument.log(" %s%n", a); + } + } + + // Create the local to store the tag frame for this method + tagFrameLocal = createTagFrameLocal(mgen, minfo); + + debugInstrument.log("nextLocalIndex: %d%n", minfo.nextLocalIndex); + debugInstrument.log("maxlocals: %d%n", mgen.getMaxLocals()); + + // The localsTable was initialized in the MethodGen24 constructor. Here we initialize the + // codeList. We also remove the local variable type records. Some instrumentation changes + // require these to be updated, but it should be safe to just delete them since the + // LocalVariableTypeTable is optional and really only of use to a debugger. We also save the + // CodeModel label at the start of the byte codes, if there is one. If there isn't, that is + // okay as it means the original code did not reference byte code offset 0 so inserting our + // instrumentation code at that point will not cause a problem. + @SuppressWarnings("nullness:assignment") // can't have gotten here if CodeAttribute is null + @NonNull CodeAttribute ca = mgen.getCodeAttribute(); + for (CodeElement ce : mgen.getInstructionList()) { + debugInstrument.log("CodeElement: %s%n", ce); + switch (ce) { + case LocalVariable lv -> {} // we have alreay processed these + case LocalVariableType lvt -> {} // we can discard local variable types + case LabelTarget l -> { + if (ca.labelToBci(l.label()) == 0) { + oldStartLabel = l.label(); + } + codeList.add(ce); + } + default -> codeList.add(ce); // save all other elements + } + } + + // Create newStartLabel now so instrumentCodeList can use it. + newStartLabel = codeBuilder.newLabel(); + + // Instrument the method + instrumentCodeList(codeModel, mgen, minfo, codeList); + + if (trackMethod && !in_jdk) { + assert mi != null : "@AssumeAssertion(nullness): mi was assigned under same conditions"; + add_enter(mgen, minfo, codeList, DCRuntime.methods.size() - 1); + add_exit(mgen, mi, minfo, codeList, DCRuntime.methods.size() - 1); + } + + // Copy the modified local variable table to the output class. + debugInstrument.log("LocalVariableTable:%n"); + for (LocalVariable lv : mgen.localsTable) { + codeBuilder.localVariable( + lv.slot(), lv.name().stringValue(), lv.typeSymbol(), lv.startScope(), lv.endScope()); + @FieldDescriptor String lvFD = lv.typeSymbol().descriptorString(); + debugInstrument.log( + " %s : %s%n", lv, daikon.chicory.Instrument24.convertDescriptorToFqBinaryName(lvFD)); + } + + // Copy the modified instruction list to the output class. + ListIterator li = codeList.listIterator(); + CodeElement ce; + while (li.hasNext()) { + if (li.nextIndex() == newStartIndex) { + codeBuilder.labelBinding(newStartLabel); + debugInstrument.log("Label: %s%n", newStartLabel); + } + ce = li.next(); + // If this instruction references a Label, we need to see if it is the oldStartLabel + // and, if so, replace the target with the newStartLabel. + ce = retargetStartLabel(ce); + debugInstrument.log("CodeElement: %s%n", ce); + codeBuilder.with(ce); + } + + // generateExceptionHandlerCode returns null if there isn't one. + List handlerCode = generateExceptionHandlerCode(mgen); + if (handlerCode != null) { + Label handlerLabel = codeBuilder.newBoundLabel(); + for (CodeElement ceh : handlerCode) { + codeBuilder.with(ceh); + } + // Using handlerLabel for the end of the try region is technically one instruction + // too many, but it shouldn't matter and is easily available. + codeBuilder.exceptionCatch(newStartLabel, handlerLabel, handlerLabel, CD_Throwable); + } + } + } + + /** + * Returns true if the specified classname.methodName is the root of JUnit startup code. + * + * @param classname class containing the given method + * @param methodName method to be checked + * @return true if the given method is a JUnit trigger + */ + boolean isJunitTrigger(String classname, @Identifier String methodName) { + if (classname.contains("JUnitCommandLineParseResult") && methodName.equals("parse")) { + // JUnit 4 + return true; + } + if (classname.contains("EngineDiscoveryRequestResolution") && methodName.equals("resolve")) { + // JUnit 5 + return true; + } + return false; + } + + // /////////////////////////////////////////////////////////////////////////// + // General Java Runtime instrumentation strategy: + // + // It is a bit of a misnomer, but the Daikon code and documentation uses the term JDK to refer + // to the Java Runtime Environment class libraries. In Java 8 and earlier, they were usually found + // in {@code /jre/lib/rt.jar}. For these versions of Java, we + // pre-instrumented the entire rt.jar. + // + // In Java 9 and later, the Java Runtime classes have been divided into modules that are + // usually found in: {@code /jmods/*.jmod}. + // + // With the conversion to modules for Java 9 and beyond, we have elected to pre-instrument only + // java.base.jmod and instrument all other Java Runtime (aka JDK) classes dynamically as they are + // loaded. + // + // Post Java 8 there are increased security checks when loading JDK classes. In particular, the + // core classes contained in the java.base module may not reference anything outside of java.base. + // This means we cannot pre-instrument classes in the same manner as was done for Java 8 as this + // would introduce external references to the DynComp runtime (DCRuntime.java). + // + // However, we can get around this restriction in the following manner: We create a shadow + // DynComp runtime called java.lang.DCRuntime that contains all the public methods of + // daikon.dcomp.DCRuntime, but with method bodies that contain only a return statement. We + // pre-instrument java.base the same as we would for JDK 8, but change all references to + // daikon.dcomp.DCRuntime to refer to java.lang.DCRuntime instead, and thus pass the security load + // test. During DynComp initialization (in Premain) we use java.lang.instrument.redefineClasses to + // replace the dummy java.lang.DCRuntime with a version where each method calls the corresponding + // method in daikon.dcomp.DCRuntime. The Java runtime does not enforce the security check in this + // case. + + /** + * Instruments a JDK class to perform dynamic comparability and returns the new class definition. + * A second version of each method in the class is created which is instrumented for + * comparability. + * + * @return the modified JavaClass + */ + public byte @Nullable [] instrument_jdk_class(ClassInfo classInfo) { + + @BinaryName String classname = classInfo.class_name; + + // Don't know where I got this idea. They are executed. Don't remember why + // adding dcomp marker causes problems. + // Don't instrument annotations. They aren't executed and adding + // the marker argument causes subtle errors + if (classModel.flags().has(AccessFlag.ANNOTATION)) { + debug_transform.log("Not instrumenting annotation %s%n", classname); + // Return class file unmodified. + return classFile.transformClass(classModel, ClassTransform.ACCEPT_ALL); + } + + int i = classname.lastIndexOf('.'); + if (i > 0) { + // Don't instrument problem packages. + // See Premain.java for a list and explanations. + String packageName = classname.substring(0, i); + if (Premain.problem_packages.contains(packageName)) { + debug_transform.log("Skipping problem package %s%n", packageName); + // Return class file unmodified. + return classFile.transformClass(classModel, ClassTransform.ACCEPT_ALL); + } + } + + if (Runtime.isJava9orLater()) { + // Don't instrument problem classes. + // See Premain.java for a list and explanations. + if (Premain.problem_classes.contains(classname)) { + debug_transform.log("Skipping problem class %s%n", classname); + // Return class file unmodified. + return classFile.transformClass(classModel, ClassTransform.ACCEPT_ALL); + } + dcompRuntimeClassName = "java.lang.DCRuntime"; + } + + try { + return classFile.build( + classModel.thisClass().asSymbol(), + classBuilder -> instrumentClass(classBuilder, classModel, classInfo)); + } catch (Throwable t) { + if (debugInstrument.enabled) { + t.printStackTrace(); + } + System.err.printf( + "DynComp warning: Class %s is being skipped due to the following:%n", + classInfo.class_name); + System.err.printf("%s.%n", t); + // Return class file unmodified. + return classFile.transformClass(classModel, ClassTransform.ACCEPT_ALL); + } + } + + /** + * Instrument the specified method for dynamic comparability. + * + * @param codeModel for the method's code + * @param mgen describes the given method + * @param minfo for the given method's code + * @param instructions instruction list for method + */ + @RequiresNonNull("newStartLabel") + private void instrumentCodeList( + @Nullable CodeModel codeModel, + MethodGen24 mgen, + MethodGen24.MInfo24 minfo, + List instructions) { + + List newCode = create_tag_frame(mgen); + + // The start of the list of CodeElements looks as follows: + // LocalVariable declarations (if any) + // Label for start of code (if present) + // LineNumber for start of code (if present) + // + // + // We want to insert the tag_frame setup code after the LocalVariables (if any) and after the + // inital label (if present), but before any LineNumber or Instruction. + ListIterator li = instructions.listIterator(); + CodeElement inst; + try { + while (li.hasNext()) { + inst = li.next(); + if ((inst instanceof LineNumber) || (inst instanceof Instruction)) { + break; + } + } + + // Insert the TagFrame code before the LineNumber or Instruction we just located. + // Back up the iterator to point to just before `inst`, then copy the newCode. + li.previous(); + for (CodeElement ce : newCode) { + li.add(ce); + } + + // We want to remember the current location in the instruction list. It is where a call to the + // DynComp runtime enter routine should be inserted, if we are tracking this method. + // This will also be used to indicate where the newStartLabel should be defined. + // Finally, we will also return to this location for code instrumentation after calculating + // the operand stack values. + newStartIndex = li.nextIndex(); + + // The next section of code calculates the operand stack value(s) for the current method. + if (debugOperandStack) { + System.out.printf("%nStarting operand stack calculation%n"); + } + + // Build a map of labels to instruction list offsets + labelIndexMap = new HashMap<>(); + li = instructions.listIterator(); + while (li.hasNext()) { + inst = li.next(); + if (inst instanceof LabelTarget lt) { + if (debugOperandStack) { + System.out.println("label target: " + lt.label() + ", index: " + li.previousIndex()); + } + // remember where this label is located within the instruction list + labelIndexMap.put(lt.label(), li.previousIndex()); + } + } + if (oldStartLabel != null) { + // change offset of oldStartLabel to where we will define newStartLabel + labelIndexMap.put(oldStartLabel, newStartIndex); + } + labelIndexMap.put(newStartLabel, newStartIndex); + + // Create an array to hold the calculated operand stack + // prior to each byte code instruction. + stacks = new OperandStack24[instructions.size()]; + + // Create an array containing the type of each local variable. + // This will be indexed by the local variable's slot number. Note that a + // variable of type long or double takes two slots; hence, there may + // be unused entries in the locals table. + locals = new ClassDesc[mgen.getMaxLocals()]; + // UNDONE: Should we init locals for 'this' and params only? + for (final LocalVariable lv : mgen.localsTable) { + // System.out.printf("local(%d): %s%n", lv.slot(), lv.typeSymbol()); + locals[lv.slot()] = lv.typeSymbol(); + } + + labelOperandStackMap = new HashMap<>(); + + // Create a worklist of instruction locations and operand stacks. + + // Create a work item for start of user's code. + if (oldStartLabel != null) { + addLabelToWorklist(oldStartLabel, new OperandStack24(mgen.getMaxStack())); + } else { + addLabelToWorklist(newStartLabel, new OperandStack24(mgen.getMaxStack())); + } + + if (codeModel != null) { + // Create a work item for each exception handler. + for (ExceptionCatch ec : codeModel.exceptionHandlers()) { + Label l = ec.handler(); + Optional catchType = ec.catchType(); + OperandStack24 temp = new OperandStack24(mgen.getMaxStack()); + if (catchType.isPresent()) { + temp.push(catchType.get().asSymbol()); + } else { + temp.push(CD_Throwable); + } + addLabelToWorklist(l, temp); + } + } + + WorkItem item; + OperandStack24 stack; + int instIndex; + while (!worklist.isEmpty()) { + item = worklist.first(); + worklist.remove(item); + if (debugOperandStack) { + System.out.println( + "pull from worklist: " + item.instructionIndex() + ", stack: " + item.stack()); + } + li = instructions.listIterator(item.instructionIndex()); + stack = item.stack(); + boolean proceed = true; + while (proceed) { + if (!li.hasNext()) { + throw new DynCompError("error in instruction list"); + } + instIndex = li.nextIndex(); + inst = li.next(); + if (debugOperandStack) { + System.out.println("instIndex: " + instIndex); + System.out.println("Operand Stack in: " + stack); + } + proceed = CalcStack24.simulateCodeElement(mgen, minfo, inst, instIndex, stack); + if (debugOperandStack) { + System.out.println("Operand Stack out: " + stack); + System.out.println("proceed: " + proceed); + } + } + } + + if (debugOperandStack) { + System.out.printf("%nStarting instruction instrumentation%n"); + } + // set list iterator to start of the user instructions + li = instructions.listIterator(newStartIndex); + + // We set instIndex here and increment each time through loop. + // This should match the index into stacks we used above. + instIndex = li.nextIndex(); + while (li.hasNext()) { + inst = li.next(); + + if (debugOperandStack) { + System.out.println("code element in: " + inst); + System.out.println("current stack: " + stacks[instIndex]); + } + // Get the translation for this instruction (if any) + List new_il = instrumentInstruction(mgen, minfo, inst, stacks[instIndex]); + if (new_il != null) { + li.remove(); // remove the instruction we instrumented + for (CodeElement ce : new_il) { + if (debugOperandStack) { + System.out.println("code element out: " + ce); + } + li.add(ce); + } + } + instIndex++; + } + } catch (DynCompError e) { + throw e; + } catch (Throwable t) { + throw new DynCompError( + String.format("Error processing %s.%s.%n", mgen.getClassName(), mgen.getName()), t); + } + } + + /** + * Create a worklist item. + * + * @param target label where to start operand stack simulation + * @param stack state of operand stack at target + */ + protected static void addLabelToWorklist(Label target, OperandStack24 stack) { + OperandStack24 existing = labelOperandStackMap.get(target); + if (existing == null) { + if (debugOperandStack) { + System.out.println( + "push to worklist: " + target + ", " + labelIndexMap.get(target) + ", stack: " + stack); + } + labelOperandStackMap.put(target, stack.getClone()); + @SuppressWarnings("nullness:unboxing.of.nullable") + int indexInCodeList = labelIndexMap.get(target); + worklist.add(new WorkItem(indexInCodeList, stack.getClone())); + } else { + // will throw if stacks don't match + verifyOperandStackMatches(target, existing, stack); + // stacks match, don't add duplicate to worklist + if (debugOperandStack) { + System.out.println( + "duplicate worklist item: " + + target + + ", " + + labelIndexMap.get(target) + + ", stack: " + + stack); + } + } + } + + /** + * Verify that the operand stacks match at a label. + * + * @param target label where control flow merges + * @param existing state of operand stack at target + * @param current state of operand stack at transfer to target + */ + protected static void verifyOperandStackMatches( + Label target, OperandStack24 existing, OperandStack24 current) { + if (existing.equals(current)) { + if (debugOperandStack) { + System.out.println("operand stacks match at: " + target); + } + return; + } + // stacks don't match + System.err.flush(); + System.out.flush(); + System.out.println("operand stacks do not match at label:" + target); + System.out.println("existing stack: " + existing); + System.out.println("current stack: " + current); + System.out.flush(); + throw new DynCompError("operand stacks do not match at label:" + target); + } + + /** + * Adds the method name and containing class name to {@code skip_methods}, the list of + * uninstrumented methods. + * + * @param m method to add to skipped_methods list + */ + void skip_method(String m) { + skipped_methods.add(m); + } + + /** + * Returns the list of uninstrumented methods. (Note: instrument_jdk_class() needs to have been + * called first.) + */ + public List get_skipped_methods() { + return skipped_methods; + } + + /** + * In {@link #instrumentCode} we build a try/catch block around the entire method. Here we + * generate the exception handler code so that if an exception occurs when the instrumented method + * is executed, the tag stack is cleaned up and the exception is rethrown. + * + * @param mgen method to add exception handler + * @return code list for handler, or null if method should not have a handler + */ + public @Nullable List generateExceptionHandlerCode(MethodGen24 mgen) { + + if (mgen.getName().equals("main")) { + return null; + } + // methods (constructors) are problematic + // for adding a whole-method exception handler. The start of + // the exception handler should be after the primary object is + // initialized - but this is hard to determine without a full + // analysis of the code. Hence, we just skip these methods. + if (!mgen.isStatic() && mgen.isConstructor()) { + return null; + } + + List instructions = new ArrayList<>(); + + instructions.add(StackInstruction.of(DUP)); + MethodRefEntry mre = + poolBuilder.methodRefEntry( + runtimeCD, "exception_exit", MethodTypeDesc.of(CD_void, CD_Object)); + instructions.add(InvokeInstruction.of(INVOKESTATIC, mre)); + instructions.add(ThrowInstruction.of()); + return instructions; + } + + /** + * Adds a call to DCRuntime.enter at the beginning of the method. + * + * @param mgen method to modify + * @param minfo for the given method's code + * @param instructions instruction list for method + * @param method_info_index index for MethodInfo + */ + private void add_enter( + MethodGen24 mgen, + MethodGen24.MInfo24 minfo, + List instructions, + int method_info_index) { + List newCode = callEnterOrExit(mgen, minfo, method_info_index, "enter", -1); + instructions.addAll(newStartIndex, newCode); + // Update the newStartIndex to account for the instrumentation code we just added. + newStartIndex += newCode.size(); + } + + /** + * Creates the local used to store the tag frame and returns it. + * + * @param mgen method to modify + * @param minfo for the given method's code + * @return LocalVariable for the tag_frame local + */ + LocalVariable createTagFrameLocal(MethodGen24 mgen, MethodGen24.MInfo24 minfo) { + return StackMapUtils24.addNewSpecialLocal( + mgen, minfo, "dcomp_tag_frame$5a", objectArrayCD, false); + } + + /** + * Generates the code to create the tag frame for this method and store it in tagFrameLocal. This + * needs to be before the call to DCRuntime.enter (since it is passed to that method). + * + * @param mgen describes the given method + * @return instruction list for tag_frame setup code + */ + private List create_tag_frame(MethodGen24 mgen) { + + ClassDesc paramTypes[] = mgen.getParameterTypes(); + + // Determine the offset of the first argument in the frame. + int offset = mgen.isStatic() ? 0 : 1; + + // allocate an extra slot to save the tag frame depth for debugging + int frame_size = mgen.getMaxLocals() + 2; + + // unsigned byte max = 255. minus the character '0' (decimal 48) + // Largest frame size noted so far is 123. + if (frame_size > 206) { + throw new DynCompError( + "method too large (" + + frame_size + + ") to instrument: " + + mgen.getClassName() + + "." + + mgen.getName()); + } + String params = Character.toString((char) (frame_size + '0')); + // Character.forDigit (frame_size, Character.MAX_RADIX); + List paramList = new ArrayList<>(); + for (ClassDesc paramType : paramTypes) { + if (paramType.isPrimitive()) { + paramList.add(offset); + } + offset += TypeKind.from(paramType).slotSize(); + } + for (int ii = paramList.size() - 1; ii >= 0; ii--) { + char tmpChar = (char) (paramList.get(ii) + '0'); + params += tmpChar; + // Character.forDigit (paramList.get(ii), Character.MAX_RADIX); + } + + // Create code to create and init the tag_frame and store the result in tagFrameLocal. + List instructions = new ArrayList<>(); + + MethodRefEntry mre = + poolBuilder.methodRefEntry( + runtimeCD, "create_tag_frame", MethodTypeDesc.of(objectArrayCD, CD_String)); + instructions.add(buildLDCInstruction(poolBuilder.stringEntry(params))); + instructions.add(InvokeInstruction.of(INVOKESTATIC, mre)); + instructions.add(StoreInstruction.of(TypeKind.REFERENCE, tagFrameLocal.slot())); + + debugInstrument.log("Store Tag frame local at index %d%n", tagFrameLocal.slot()); + + return instructions; + } + + /** + * Pushes the object, method info index, parameters, and return value on the stack and calls the + * specified method (normally {@code enter()} or {@code exit}) in DCRuntime. The parameters are + * passed as an array of objects. + * + * @param mgen method to modify + * @param minfo for the given method's code + * @param method_info_index index for MethodInfo + * @param enterOrExit the method to invoke: "enter" or "exit" + * @param line source line number if type is exit + * @return instruction list for the enter or exit code + */ + private List callEnterOrExit( + MethodGen24 mgen, + MethodGen24.MInfo24 minfo, + int method_info_index, + String enterOrExit, + int line) { + + List instructions = new ArrayList<>(); + ClassDesc paramTypes[] = mgen.getParameterTypes(); + + // Push the tag frame + instructions.add(LoadInstruction.of(TypeKind.REFERENCE, tagFrameLocal.slot())); + + // Push the object. Push null if this is a static method or a constructor. + if (mgen.isStatic() || (enterOrExit.equals("enter") && mgen.isConstructor())) { + instructions.add(ConstantInstruction.ofIntrinsic(ACONST_NULL)); + } else { // must be an instance method + instructions.add(LoadInstruction.of(TypeKind.REFERENCE, 0)); + } + + // The offset of the first parameter. + int param_offset = mgen.isStatic() ? 0 : 1; + + // Push the MethodInfo index + instructions.add(loadIntegerConstant(method_info_index)); + + // Create an array of objects with elements for each parameter. + instructions.add(loadIntegerConstant(paramTypes.length)); + instructions.add(NewReferenceArrayInstruction.of(poolBuilder.classEntry(CD_Object))); + + // Put each argument into the array. + int param_index = param_offset; + for (int ii = 0; ii < paramTypes.length; ii++) { + instructions.add(StackInstruction.of(DUP)); + instructions.add(loadIntegerConstant(ii)); + ClassDesc at = paramTypes[ii]; + if (at.isPrimitive()) { + instructions.add(ConstantInstruction.ofIntrinsic(ACONST_NULL)); + } else { // it's a reference of some sort + instructions.add(LoadInstruction.of(TypeKind.REFERENCE, param_index)); + } + instructions.add(ArrayStoreInstruction.of(AASTORE)); + param_index += TypeKind.from(at).slotSize(); + } + + // If this is an exit, push the return value and line number. + // The return value is stored in the local "return__$trace2_val". + // If the return value is a primitive, push a null. + if (enterOrExit.equals("exit")) { + ClassDesc returnType = mgen.getReturnType(); + if (returnType.equals(CD_void)) { + instructions.add(ConstantInstruction.ofIntrinsic(ACONST_NULL)); + } else { + LocalVariable return_local = getReturnLocal(mgen, returnType, minfo); + if (returnType.isPrimitive()) { + instructions.add(ConstantInstruction.ofIntrinsic(ACONST_NULL)); + } else { + instructions.add(LoadInstruction.of(TypeKind.REFERENCE, return_local.slot())); + } + } + // push line number + instructions.add(loadIntegerConstant(line)); + } + + MethodTypeDesc methodParams; + // Call the specified method. + if (enterOrExit.equals("exit")) { + methodParams = + MethodTypeDesc.of( + CD_void, objectArrayCD, CD_Object, CD_int, objectArrayCD, CD_Object, CD_int); + } else { + methodParams = MethodTypeDesc.of(CD_void, objectArrayCD, CD_Object, CD_int, objectArrayCD); + } + MethodRefEntry mre = poolBuilder.methodRefEntry(runtimeCD, enterOrExit, methodParams); + instructions.add(InvokeInstruction.of(INVOKESTATIC, mre)); + + return instructions; + } + + /** + * Transforms one instruction to track comparability. Returns a list of instructions that replaces + * the specified instruction. Returns null if the instruction should not be replaced. + * + * @param mgen method to modify + * @param minfo for the given method's code + * @param ce instruction to be instrumented + * @param stack current contents of the operand stack + * @return instrumentation for inst, or null if none + */ + @Nullable List instrumentInstruction( + MethodGen24 mgen, MethodGen24.MInfo24 minfo, CodeElement ce, OperandStack24 stack) { + + switch (ce) { + case Instruction inst -> { + switch (inst.opcode()) { + + // Replace the object comparison instructions with a call to + // DCRuntime.object_eq or DCRuntime.object_ne. Those methods + // return a boolean which is used in a ifeq/ifne instruction. + case IF_ACMPEQ: + return object_comparison((BranchInstruction) inst, "object_eq", IFNE); + case IF_ACMPNE: + return object_comparison((BranchInstruction) inst, "object_ne", IFNE); + + // These instructions compare the integer on the top of the stack + // to zero. Nothing is made comparable by this, so we need only + // discard the tag on the top of the stack. + case IFEQ: + case IFNE: + case IFLT: + case IFGE: + case IFGT: + case IFLE: + { + return discard_tag_code(inst, 1); + } + + // Instanceof pushes either 0 or 1 on the stack depending on whether + // the object on top of stack is of the specified type. The DynComp runtime will push a + // new, unique + // tag for a constant, since nothing is made comparable by this. + case INSTANCEOF: + return build_il(dcr_call("push_const", CD_void, noArgsSig), inst); + + // Duplicates the item on the top of stack. If the value on the + // top of the stack is a primitive, we need to do the same on the + // tag stack. Otherwise, we need do nothing. + case DUP: + return dup_tag(inst, stack); + + // Duplicates the item on the top of the stack and inserts it 2 + // values down in the stack. If the value at the top of the stack + // is not a primitive, there is nothing to do. If the second + // value is not a primitive, then we need only to insert the duped + // value down 1 on the tag stack (which contains only primitives). + case DUP_X1: + return dup_x1_tag(inst, stack); + + // Duplicate the category 1 item on the top of the stack and insert it either + // two or three items down in the stack. + case DUP_X2: + return dup_x2_tag(inst, stack); + + // Duplicate either one category 2 item or two category 1 items. + case DUP2: + return dup2_tag(inst, stack); + + // Duplicate either the top 2 category 1 items or a single + // category 2 item and insert it 2 or 3 values down on the + // stack. + case DUP2_X1: + return dup2_x1_tag(inst, stack); + + // Duplicate the top one or two items and insert them two, three, or four values down. + case DUP2_X2: + return dup2_x2_tag(inst, stack); + + // Pop a category 1 item from the top of the stack. We want to discard + // the top of the tag stack iff the item on the top of the stack is a + // primitive. + case POP: + return pop_tag(inst, stack); + + // Pops either the top 2 category 1 items or a single category 2 item + // from the top of the stack. We must do the same to the tag stack + // if the values are primitives. + case POP2: + return pop2_tag(inst, stack); + + // Swaps the two category 1 items on the top of the stack. We need + // to swap the top of the tag stack if the two top elements on the + // real stack are primitives. + case SWAP: + return swap_tag(inst, stack); + + case IF_ICMPEQ: + case IF_ICMPGE: + case IF_ICMPGT: + case IF_ICMPLE: + case IF_ICMPLT: + case IF_ICMPNE: + { + return build_il(dcr_call("cmp_op", CD_void, noArgsSig), inst); + } + + case GETFIELD: + case PUTFIELD: + case GETSTATIC: + case PUTSTATIC: + { + return load_store_field(mgen, minfo, ((FieldInstruction) inst)); + } + + case DLOAD: + case DLOAD_0: + case DLOAD_1: + case DLOAD_2: + case DLOAD_3: + case FLOAD: + case FLOAD_0: + case FLOAD_1: + case FLOAD_2: + case FLOAD_3: + case ILOAD: + case ILOAD_0: + case ILOAD_1: + case ILOAD_2: + case ILOAD_3: + case LLOAD: + case LLOAD_0: + case LLOAD_1: + case LLOAD_2: + case LLOAD_3: + { + return load_local((LoadInstruction) inst, tagFrameLocal, "push_local_tag"); + } + + case DSTORE: + case DSTORE_0: + case DSTORE_1: + case DSTORE_2: + case DSTORE_3: + case FSTORE: + case FSTORE_0: + case FSTORE_1: + case FSTORE_2: + case FSTORE_3: + case ISTORE: + case ISTORE_0: + case ISTORE_1: + case ISTORE_2: + case ISTORE_3: + case LSTORE: + case LSTORE_0: + case LSTORE_1: + case LSTORE_2: + case LSTORE_3: + { + return store_local((StoreInstruction) inst, tagFrameLocal, "pop_local_tag"); + } + + // Adjusts the tag stack for load constant opcodes. If the constant is a primitive, pushes + // its tag on the tag stack. If the constant is a reference (string, class), does nothing. + case LDC: + case LDC_W: + case LDC2_W: + { + if (((ConstantInstruction) inst).typeKind().equals(TypeKind.REFERENCE)) { + return null; + } + return build_il(dcr_call("push_const", CD_void, noArgsSig), inst); + } + + // Push the tag for the array onto the tag stack. This causes + // anything comparable to the length to be comparable to the array + // as an index. + case ARRAYLENGTH: + { + return array_length(inst); + } + + case BIPUSH: + case SIPUSH: + case DCONST_0: + case DCONST_1: + case FCONST_0: + case FCONST_1: + case FCONST_2: + case ICONST_0: + case ICONST_1: + case ICONST_2: + case ICONST_3: + case ICONST_4: + case ICONST_5: + case ICONST_M1: + case LCONST_0: + case LCONST_1: + { + return build_il(dcr_call("push_const", CD_void, noArgsSig), inst); + } + + // Primitive Binary operators. Each is augmented with a call to + // DCRuntime.binary_tag_op that merges the tags and updates the tag + // Stack. + case DADD: + case DCMPG: + case DCMPL: + case DDIV: + case DMUL: + case DREM: + case DSUB: + case FADD: + case FCMPG: + case FCMPL: + case FDIV: + case FMUL: + case FREM: + case FSUB: + case IADD: + case IAND: + case IDIV: + case IMUL: + case IOR: + case IREM: + case ISHL: + case ISHR: + case ISUB: + case IUSHR: + case IXOR: + case LADD: + case LAND: + case LCMP: + case LDIV: + case LMUL: + case LOR: + case LREM: + case LSHL: + case LSHR: + case LSUB: + case LUSHR: + case LXOR: + return build_il(dcr_call("binary_tag_op", CD_void, noArgsSig), inst); + + // Computed jump based on the int on the top of stack. Since that int + // is not made comparable to anything, we just discard its tag. One + // might argue that the key should be made comparable to each value in + // the jump table. But the tags for those values are not available. + // And since they are all constants, its not clear how interesting it + // would be anyway. + case LOOKUPSWITCH: + case TABLESWITCH: + return discard_tag_code(inst, 1); + + // Make the integer argument to ANEWARRAY comparable to the new + // array's index. + case ANEWARRAY: + case NEWARRAY: + return new_array(inst); + + // If the new array has 2 dimensions, make the integer arguments + // comparable to the corresponding indices of the new array. + // For any other number of dimensions, discard the tags for the + // arguments. + case MULTIANEWARRAY: + return multi_newarray_dc((NewMultiArrayInstruction) inst); + + // Mark the array and its index as comparable. Also for primitives, + // push the tag of the array element on the tag stack + case AALOAD: + case BALOAD: + case CALOAD: + case DALOAD: + case FALOAD: + case IALOAD: + case LALOAD: + case SALOAD: + return array_load(mgen, (ArrayLoadInstruction) inst); + + // Prefix the return with a call to the correct normal_exit method + // to handle the tag stack + case ARETURN: + case DRETURN: + case FRETURN: + case IRETURN: + case LRETURN: + case RETURN: + return return_tag(mgen, inst); + + // Throws an exception. This clears the operand stack of the current + // frame. We need to clear the tag stack as well. + case ATHROW: + return build_il(dcr_call("throw_op", CD_void, noArgsSig), inst); + + // Opcodes that don't need any modifications. Here for reference. + // Note that while we include JSR, JSR_W, RET and RET_W here, it is + // only for documentation. They will throw a "Unexpected instruction opcode" + // error during the operand stack calculation phase. + case ACONST_NULL: + case ALOAD: + case ALOAD_0: + case ALOAD_1: + case ALOAD_2: + case ALOAD_3: + case ASTORE: + case ASTORE_0: + case ASTORE_1: + case ASTORE_2: + case ASTORE_3: + case CHECKCAST: + case D2F: // double to float + case D2I: // double to integer + case D2L: // double to long + case DNEG: // Negate double on top of stack + case F2D: // float to double + case F2I: // float to integer + case F2L: // float to long + case FNEG: // Negate float on top of stack + case GOTO: + case GOTO_W: + case I2B: // integer to byte + case I2C: // integer to char + case I2D: // integer to double + case I2F: // integer to float + case I2L: // integer to long + case I2S: // integer to short + case IFNONNULL: + case IFNULL: + case IINC: // increment local variable by a constant + case IINC_W: // increment local variable by a constant + case INEG: // negate integer on top of stack + case JSR: // pushes return address on the stack, but that + // is thought of as an object, so we don't need a tag for it. + case JSR_W: + case L2D: // long to double + case L2F: // long to float + case L2I: // long to int + case LNEG: // negate long on top of stack + case MONITORENTER: + case MONITOREXIT: + case NEW: + case NOP: + case RET: // this is the internal JSR return + case RET_W: + return null; + + // Handle subroutine calls. Calls to instrumented code are modified + // to call the instrumented version (with the DCompMarker argument). + // Calls to uninstrumented code (rare) discard primitive arguments + // from the tag stack and produce an arbitrary return tag. + case INVOKESTATIC: + case INVOKEVIRTUAL: + case INVOKESPECIAL: + case INVOKEINTERFACE: + return handleInvoke((InvokeInstruction) inst, mgen); + + case INVOKEDYNAMIC: + return handleInvokeDynamic((InvokeDynamicInstruction) inst); + + // Mark the array and its index as comparable. For primitives, store + // the tag for the value on the top of the stack in the tag storage + // for the array. + case AASTORE: + return array_store(inst, "aastore", CD_Object); + case BASTORE: + // The JVM uses bastore for both byte and boolean. + // We need to differentiate. + ClassDesc arrayref = stack.peek(2); + ClassDesc ct = arrayref.componentType(); + if (ct == null) { + throw new Error("stack item not an arrayref: " + inst); + } + if (ct.equals(CD_boolean)) { + return array_store(inst, "zastore", CD_boolean); + } else { + return array_store(inst, "bastore", CD_byte); + } + case CASTORE: + return array_store(inst, "castore", CD_char); + case DASTORE: + return array_store(inst, "dastore", CD_double); + case FASTORE: + return array_store(inst, "fastore", CD_float); + case IASTORE: + return array_store(inst, "iastore", CD_int); + case LASTORE: + return array_store(inst, "lastore", CD_long); + case SASTORE: + return array_store(inst, "sastore", CD_short); + + default: + throw new DynCompError("Unexpected instruction opcode: " + inst.opcode()); + } + } + + // We ignore PseudoInstructions. + + case ExceptionCatch ec -> { + // Ignore ExceptionCatch CodeElements. + return null; + } + + case Label l -> { + // Ignore Label CodeElements. + return null; + } + + case LineNumber ln -> { + // Ignore LineNumber CodeElements. + return null; + } + + default -> { + throw new DynCompError("Unexpected CodeElement: " + ce); + } + } + } + + /** + * Adds a call to DCruntime.exit() at each return from the method. This call calculates + * comparability on any variables that are being tracked. + * + * @param mgen method to modify + * @param mi MethodInfo for the given method's code + * @param minfo MInfo24 for the given method's code + * @param instructions instruction list for method + * @param method_info_index index for MethodInfo + */ + private void add_exit( + MethodGen24 mgen, + MethodInfo mi, + MethodGen24.MInfo24 minfo, + List instructions, + int method_info_index) { + + // Iterator over all of the exit line numbers for this method, in order. + // We will read one element from it each time that we encounter a + // return instruction. + Iterator exitLocationIter = mi.exit_locations.iterator(); + + // Loop through each instruction, looking for return instructions. + ListIterator li = instructions.listIterator(); + while (li.hasNext()) { + CodeElement inst = li.next(); + + // If this is a return instruction, Call DCRuntime.exit to calculate + // comparability on Daikon variables + if (inst instanceof ReturnInstruction) { + List newCode = new ArrayList<>(); + ClassDesc type = mgen.getReturnType(); + if (!type.equals(CD_void)) { + TypeKind typeKind = TypeKind.from(type); + LocalVariable returnLocal = getReturnLocal(mgen, type, minfo); + if (typeKind.slotSize() == 1) { + newCode.add(StackInstruction.of(DUP)); + } else { + newCode.add(StackInstruction.of(DUP2)); + } + newCode.add(StoreInstruction.of(typeKind, returnLocal.slot())); + } + if (!exitLocationIter.hasNext()) { + throw new DynCompError("Not enough exit locations in the exitLocationIter"); + } + newCode.addAll( + callEnterOrExit(mgen, minfo, method_info_index, "exit", exitLocationIter.next())); + + // Back up iterator to point to `inst`, the return instruction, and insert the + // instrumentation. + li.previous(); + for (CodeElement ce : newCode) { + li.add(ce); + } + // skip over the return instruction + li.next(); + } + } + } + + /** + * Returns the interface class name containing the implementation of the given method. The + * interfaces of {@code startClass} are recursively searched. + * + * @param startClass the class whose interfaces are to be searched + * @param methodName the target method to search for + * @param paramTypes the target method's parameter types + * @return the name of the interface class containing target method, or null if not found + */ + private @Nullable @BinaryName String getDefiningInterface( + ClassModel startClass, @Identifier String methodName, ClassDesc[] paramTypes) { + + if (debugGetDefiningInterface) { + System.out.println("searching interfaces of: " + ClassGen24.getClassName(startClass)); + } + for (ClassEntry classEntry : startClass.interfaces()) { + @BinaryName String interfaceName = Runtime.internalFormToBinaryName(classEntry.asInternalName()); + if (debugGetDefiningInterface) { + System.out.println("interface: " + interfaceName); + } + ClassModel cm; + try { + @SuppressWarnings("nullness:assignment") + @NonNull ClassModel cmTmp = getClassModel(interfaceName); + cm = cmTmp; + } catch (Throwable t) { + throw new DynCompError(String.format("Unable to load class: %s", interfaceName), t); + } + for (MethodModel jm : cm.methods()) { + String jmName = jm.methodName().stringValue(); + MethodTypeDesc mtd = jm.methodTypeSymbol(); + if (debugGetDefiningInterface) { + System.out.println(" " + jmName + Arrays.toString(mtd.parameterArray())); + } + if (jmName.equals(methodName) && Arrays.equals(mtd.parameterArray(), paramTypes)) { + // We have a match. + return interfaceName; + } + } + // no match found; does this interface extend other interfaces? + @BinaryName String foundAbove = getDefiningInterface(cm, methodName, paramTypes); + if (foundAbove != null) { + // We have a match. + return foundAbove; + } + } + // nothing found + return null; + } + + /** + * Process an InvokeDynamic instruction. We don't instrument lambda methods, so just clean up the + * tag stack. + * + * @param invoke a method invocation bytecode instruction + * @return instructions to replace the given instruction + */ + private List handleInvokeDynamic(InvokeDynamicInstruction invoke) { + + // Get information about the call + // There isn't really a class holding the method. + String classname = ""; + MethodTypeDesc mtd = invoke.typeSymbol(); + ClassDesc returnType = mtd.returnType(); + ClassDesc[] paramTypes = mtd.parameterArray(); + if (debugHandleInvoke) { + System.out.println("invokedynamic: " + invoke); + System.out.printf(" callee_instrumented: false%n"); + } + + return cleanInvokeTagStack(invoke, classname, returnType, paramTypes); + } + + /** + * Process an Invoke instruction. There are three cases: + * + *

    + *
  • convert calls to Object.equals to calls to dcomp_equals or dcomp_super_equals + *
  • convert calls to Object.clone to calls to dcomp_clone or dcomp_super_clone + *
  • otherwise, determine whether the target of the invoke is instrumented or not (this is the + * {@code callee_instrumented} variable) + *
      + *
    • If the target method is instrumented, add a DCompMarker parameter to the end of the + * parameter list. + *
    • If the target method is not instrumented, we must account for the fact that the + * instrumentation code generated up to this point has assumed that the target method + * is instrumented. Hence, generate code to discard a primitive tag from the + * DCRuntime's per-thread comparability data stack for each primitive argument. If the + * return type of the target method is a primitive, add code to push a tag onto the + * runtime comparability data stack to represent the primitive return value. + *
    + *
+ * + * @param invoke a method invocation bytecode instruction + * @param mgen host method of invoke + * @return instructions to replace the given instruction + */ + private List handleInvoke(InvokeInstruction invoke, MethodGen24 mgen) { + + // Get information about the call. + @BinaryName String classname = Runtime.internalFormToBinaryName(invoke.owner().asInternalName()); + String methodName = invoke.name().stringValue(); + MethodTypeDesc mtd = invoke.typeSymbol(); + ClassDesc returnType = mtd.returnType(); + ClassDesc[] paramTypes = mtd.parameterArray(); + + if (debugHandleInvoke) { + System.out.println(); + System.out.println("InvokeInst: " + invoke); + System.out.println("returnType: " + returnType); + System.out.println("classname: " + classname); + } + + if (is_object_equals(methodName, returnType, paramTypes)) { + + // Replace calls to Object's equals method with calls to our + // replacement, a static method in DCRuntime. + List il = new ArrayList<>(); + il.add( + dcr_call( + invoke.opcode().equals(INVOKESPECIAL) ? "dcomp_super_equals" : "dcomp_equals", + returnType, + new ClassDesc[] {CD_Object, CD_Object})); + return il; + } + + if (is_object_clone(methodName, returnType, paramTypes)) { + + // Replace calls to Object's clone method with calls to our + // replacement, a static method in DCRuntime. + + return instrument_clone_call(invoke, returnType, classname); + } + + boolean callee_instrumented = + isTargetInstrumented(invoke, mgen, classname, methodName, paramTypes); + + if (debugHandleInvoke) { + System.out.printf("handleInvoke(%s)%n", invoke); + System.out.printf(" invoke host: %s.%s%n", classGen.getClassName(), mgen.getName()); + System.out.printf(" invoke targ: %s.%s%n", classname, methodName); + System.out.printf(" callee_instrumented: %s%n", callee_instrumented); + } + + if (callee_instrumented) { + + List il = new ArrayList<>(); + + // Push the DCompMarker argument as we are calling the instrumented version. + il.add(ConstantInstruction.ofIntrinsic(ACONST_NULL)); + + // Add the DCompMarker type to the parameter types list. + List new_param_types = new ArrayList<>(Arrays.asList(paramTypes)); + new_param_types.add(dcomp_marker); + + NameAndTypeEntry nte = + poolBuilder.nameAndTypeEntry(methodName, MethodTypeDesc.of(returnType, new_param_types)); + il.add(InvokeInstruction.of(invoke.opcode(), invoke.owner(), nte, invoke.isInterface())); + return il; + + } else { // not instrumented, discard the tags before making the call + return cleanInvokeTagStack(invoke, classname, returnType, paramTypes); + } + } + + /** + * Clean up the tag stack for an invoke instruction that calls a non-instrumented method. Items + * were pushed onto the tag stack for each argument to the invoke. But we have since discovered + * that the target method is not instrumented. Thus the DynComp runtime must discard (pop) these + * tags before the invoke is executed. In addition, if the method being invoked has a primitive + * return type, the runtime must push a new, unique tag after the invoke. + * + * @param invoke a method invocation bytecode instruction + * @param classname target class of the invoke + * @param returnType return type of method + * @param paramTypes parameter types of target method + * @return instructions to replace the given instruction + */ + private List cleanInvokeTagStack( + Instruction invoke, String classname, ClassDesc returnType, ClassDesc[] paramTypes) { + List il = new ArrayList<>(); + + // JUnit test classes are a bit strange. They are marked as not being callee_instrumented + // because they do not have the dcomp_marker added to the parameter list, but + // they actually contain instrumentation code. So we do not want to discard + // the primitive tags prior to the call. + if (!junitTestClasses.contains(classname)) { + il = discard_primitive_tags(paramTypes); + } + + // Add a tag for the return type if it is primitive. + if (is_primitive(returnType)) { + if (debugHandleInvoke) { + System.out.printf("push tag for return type of %s%n", returnType); + } + il.add(dcr_call("push_const", CD_void, noArgsSig)); + } + il.add(invoke); + return il; + } + + /** + * Returns instructions that will discard (pop) any primitive tags corresponding to the specified + * parameters. Returns an empty instruction list if there are no primitive arguments to discard. + * + * @param paramTypes parameter types of target method + * @return an instruction list that discards primitive tags from DCRuntime's per-thread + * comparability data stack + */ + private List discard_primitive_tags(ClassDesc[] paramTypes) { + int primitive_cnt = 0; + for (ClassDesc paramType : paramTypes) { + if (paramType.isPrimitive()) { + primitive_cnt++; + } + } + if (primitive_cnt > 0) { + return discard_tag_code(null, primitive_cnt); + } + // Must return a mutable array because some clients mutate it. + return new ArrayList<>(); + } + + /** + * Returns true if the invoked method (the callee) is instrumented. + * + * @param invoke instruction whose target is to be checked + * @param mgen method containing the invoke + * @param classname target class of the invoke (the callee) + * @param methodName target method of the invoke (the callee) + * @param paramTypes parameter types of target method + * @return true if the target is instrumented + */ + private boolean isTargetInstrumented( + InvokeInstruction invoke, + MethodGen24 mgen, + @BinaryName String classname, + @Identifier String methodName, + ClassDesc[] paramTypes) { + + boolean targetInstrumented; + Opcode op = invoke.opcode(); + + if (is_object_method(methodName, paramTypes)) { + targetInstrumented = false; + } else { + // At this point, we will never see classname = java.lang.Object. + targetInstrumented = isClassnameInstrumented(classname, methodName); + + if (debugHandleInvoke) { + System.out.printf("isClassnameInstrumented: %s%n", targetInstrumented); + System.out.printf("invoke host: %s.%s%n", classGen.getClassName(), mgen.getName()); + System.out.printf("invoke targ: %s.%s%n", classname, methodName); + } + + if (Premain.problem_methods.contains(classname + "." + methodName)) { + debugInstrument.log( + "Don't call instrumented version of problem method %s.%s.%n", classname, methodName); + targetInstrumented = false; + } + + // targetInstrumented (the return value of this method) has been set. + // Now, adjust it for some special cases. + // Every adjustment is from `true` to `false`. + + // There are two special cases we need to detect: + // calls to annotations + // calls to functional interfaces + // + // Annotation classes are never instrumented so we must set + // the targetInstrumented flag to false. + // + // Functional interfaces are a bit more complicated. These are primarily (only?) + // used by Lambda functions. Lambda methods are generated dynamically at + // run time via the InvokeDynamic instruction. They are not seen by our + // ClassFileTransformer so are never instrumented. Thus we must set the + // targetInstrumented flag to false when we see a call to a Lambda method. + // The heuristic we use is to assume that any InvokeInterface or InvokeVirtual + // call to a functional interface is a call to a Lambda method. + // + // The Java compiler detects functional interfaces automatically, but the + // user can declare their intent with the @FunctionInterface annotation. + // The Java runtime is annotated in this manner. Hence, we look for this + // annotation to detect a call to a functional interface. In practice, we + // could detect functional interfaces in a manner similar to the Java + // compiler, but for now we will go with this simpler method. + // + // Note that to simplify our code we set the access flags for a functional + // interface to ANNOTATION in our accessFlags map. + // + if (targetInstrumented == true && (op.equals(INVOKEINTERFACE) || op.equals(INVOKEVIRTUAL))) { + Integer access = getAccessFlags(classname); + + if ((access & ACC_ANNOTATION) != 0) { + targetInstrumented = false; + } + + // UNDONE: New code added above should handle the case below. Need to find a test + // case and verify this code is no longer needed. + // This is a bit of a hack. An invokeinterface instruction with a + // a target of "java.util.stream." might be calling a + // lambda method in which case we don't want to add the dcomp_marker. + // Might lose something in "normal" cases, but no easy way to detect. + if (classname.startsWith("java.util.stream")) { + targetInstrumented = false; + } + + // In a similar fashion, when the Java runtime is processing annotations, there might + // be an invoke (via reflection) of a member of the java.lang.annotation package; this + // too should not have the dcomp_marker added. + if (classname.startsWith("java.lang.annotation")) { + targetInstrumented = false; + } + } + + // If we are not using the instrumented JDK, then we need to track down the + // actual target of an INVOKEVIRTUAL to see if it has been instrumented or not. + if (targetInstrumented == true && op.equals(INVOKEVIRTUAL)) { + if (!Premain.jdk_instrumented && !mgen.getName().equals("equals_dcomp_instrumented")) { + + if (debugHandleInvoke) { + System.out.println("method: " + methodName); + System.out.println("paramTypes: " + Arrays.toString(paramTypes)); + System.out.printf("invoke host: %s.%s%n", mgen.getClassName(), mgen.getName()); + } + + @BinaryName String targetClassname = classname; + // Search this class for the target method. If not found, set targetClassname to + // its superclass and try again. + mainloop: + while (true) { + // Check that the class exists + ClassModel targetClass; + try { + targetClass = getClassModel(targetClassname); + } catch (Throwable e) { + targetClass = null; + } + if (targetClass == null) { + // We cannot locate or read the .class file, better assume not instrumented. + if (debugHandleInvoke) { + System.out.printf("Unable to locate class: %s%n%n", targetClassname); + } + targetInstrumented = false; + break; + } + if (debugHandleInvoke) { + System.out.println("target class: " + targetClassname); + } + + for (MethodModel jm : targetClass.methods()) { + String jmName = jm.methodName().stringValue(); + MethodTypeDesc mtd = jm.methodTypeSymbol(); + if (debugHandleInvoke) { + System.out.println(" " + jmName + Arrays.toString(mtd.parameterArray())); + } + if (jmName.equals(methodName) && Arrays.equals(mtd.parameterArray(), paramTypes)) { + // We have a match. + if (debugHandleInvoke) { + System.out.printf("we have a match%n%n"); + } + if (BcelUtil.inJdk(targetClassname)) { + targetInstrumented = false; + } + break mainloop; + } + } + + { + // no methods match - search this class's interfaces + @BinaryName String found; + try { + found = getDefiningInterface(targetClass, methodName, paramTypes); + } catch (Throwable e) { + // We cannot locate or read the .class file, better assume it is not instrumented. + targetInstrumented = false; + break; + } + if (found != null) { + // We have a match. + if (debugHandleInvoke) { + System.out.printf("we have a match%n%n"); + } + if (BcelUtil.inJdk(found)) { + targetInstrumented = false; + } + break; + } + } + + // Method not found; perhaps inherited from superclass. + if (targetClassname.equals("java.lang.Object")) { + // The target class was Object; the search completed without finding a matching + // method. + if (debugHandleInvoke) { + System.out.printf("Unable to locate method: %s%n%n", methodName); + } + targetInstrumented = false; + break; + } + + // Recurse looking in the superclass. + targetClassname = getSuperclassName(targetClassname); + } + } + } + } + + if (op.equals(INVOKESPECIAL)) { + if (classname.equals(classGen.getSuperclassName()) && methodName.equals("")) { + this.constructor_is_initialized = true; + } + } + + return targetInstrumented; + } + + /** + * Returns the access flags for the given class. + * + * @param classname the class whose access flags to return + * @return the access flags for the given class + */ + private Integer getAccessFlags(String classname) { + Integer access = accessFlags.get(classname); + if (access == null) { + // We have not seen this class before. Check to see if the target class is + // an Annotation or a FunctionalInterface. + ClassModel cm = getClassModel(classname); + if (cm != null) { + access = cm.flags().flagsMask(); + + debugInstrument.log("classModel: %s%n", cm.thisClass().name().stringValue()); + debugInstrument.log("getAccessFlags: %s%n", classname); + // Now check for FunctionalInterface + searchloop: + for (Attribute attribute : cm.attributes()) { + debugInstrument.log("attribute: %s%n", attribute); + if (attribute instanceof RuntimeVisibleAnnotationsAttribute rvaa) { + for (final Annotation item : rvaa.annotations()) { + String annotation = item.className().stringValue(); + if (debugHandleInvoke) { + System.out.println("annotation: " + annotation); + } + if (annotation.endsWith("FunctionalInterface;")) { + access = Integer_ACC_ANNOTATION; + if (debugHandleInvoke) { + System.out.println("FunctionalInterface is true"); + } + break searchloop; + } + } + } + } + } else { + // We cannot locate or read the .class file, better pretend it is an Annotation. + if (debugHandleInvoke) { + System.out.printf("Unable to locate class: %s%n", classname); + } + access = Integer_ACC_ANNOTATION; + } + accessFlags.put(classname, access); + } + return access; + } + + /** + * Returns true if the specified class is instrumented or we presume it will be instrumented by + * the time it is executed. + * + * @param classname class to be checked + * @param methodName method to be checked (currently unused) + * @return true if classname is instrumented + */ + private boolean isClassnameInstrumented( + @BinaryName String classname, @Identifier String methodName) { + + if (debugHandleInvoke) { + System.out.printf("Checking callee instrumented on %s.%s%n", classname, methodName); + } + + // Our copy of daikon.plumelib is not instrumented. It would be odd, though, + // to see calls to this. + if (classname.startsWith("daikon.plumelib")) { + return false; + } + + // When a class contains an existing , it will be instrumented. Thus, we need to mark + // our added call to 'DCRuntime.set_class_initialized' as not instrumented. + if (classname.endsWith("DCRuntime") && methodName.equals("set_class_initialized")) { + return false; + } + + // Special-case JUnit test classes. + if (junitTestClasses.contains(classname)) { + return false; + } + + if (daikon.dcomp.Instrument24.is_transformer(classname.replace('.', '/'))) { + return false; + } + + // Special-case the execution trace tool. + if (classname.startsWith("minst.Minst")) { + return false; + } + + // We should probably change the interface to include method name + // and use "classname.methodname" as arg to pattern matcher. + // If any of the omit patterns match, use the uninstrumented version of the method + for (Pattern p : DynComp.ppt_omit_pattern) { + if (p.matcher(classname).find()) { + if (debugHandleInvoke) { + System.out.printf("callee instrumented = false: %s.%s%n", classname, methodName); + } + return false; + } + } + + // If it's not a JDK class, presume it's instrumented. + if (!BcelUtil.inJdk(classname)) { + return true; + } + + int i = classname.lastIndexOf('.'); + if (i > 0 && Premain.problem_packages.contains(classname.substring(0, i))) { + debugInstrument.log( + "Don't call instrumented member of problem package %s%n", classname.substring(0, i)); + return false; + } + + if (Premain.problem_classes.contains(classname)) { + debugInstrument.log("Don't call instrumented member of problem class %s%n", classname); + return false; + } + + // We have decided not to use the instrumented version of Random as + // the method generates values based on an initial seed value. + // (Typical of random() algorithms.) Instrumentation would have the undesirable side + // effect of putting all the generated values in the same comparison + // set when they should be distinct. + // Note: If we find other classes that should not use the instrumented + // versions, we should consider making this a searchable list. + if (classname.equals("java.util.Random")) { + return false; + } + + // If using the instrumented JDK, then everthing but object is instrumented. + if (Premain.jdk_instrumented && !classname.equals("java.lang.Object")) { + return true; + } + + return false; + } + + /** + * Returns a list of the superclasses of this class. This class itself is not in the list. The + * returned list is in ascending order; that is, java.lang.Object is always the last element, + * unless the argument is java.lang.Object. + * + * @return list of superclasses + */ + public List<@BinaryName String> getStrictSuperclassNames() { + final List<@BinaryName String> allSuperclassNames = new ArrayList<>(); + @BinaryName String classname = classGen.getClassName(); + while (!classname.equals("java.lang.Object")) { + classname = getSuperclassName(classname); + allSuperclassNames.add(classname); + } + return allSuperclassNames; + } + + /** + * Given a classname return its superclass name. Note that we copy BCEL and report that the + * superclass of {@code java.lang.Object} is {@code java.lang.Object} rather than saying there is + * no superclass. + * + * @param classname the fully-qualified name of the class in binary form. E.g., "java.util.List" + * @return name of superclass + */ + private @BinaryName String getSuperclassName(String classname) { + ClassModel cm = getClassModel(classname); + if (cm == null) { + throw new SuperclassNameError(classname); + } + return ClassGen24.getSuperclassName(cm); + } + + /** Unchecked exception thrown if {@link #getSuperclassName} cannot find a superclass name. */ + private static class SuperclassNameError extends Error { + static final long serialVersionUID = 20251203; + + /** + * Creates a SuperclassNameError. + * + * @param classname the name of the class whose parent cannot be found + */ + SuperclassNameError(String classname) { + super(classname); + } + } + + /** Cache for {@link #getClassModel} method. */ + private static Map classModelCache = new ConcurrentHashMap<>(); + + /** + * There are times when it is useful to inspect a class file other than the one we are currently + * instrumenting. We cannot use {@code classForName} to do this as it might trigger a recursive + * call to Instrument which would not work at this point. + * + *

Given a class name, we treat it as a system resource, create a {@code Path} to it and have + * {@code java.lang.classfile} read and create a {@code ClassModel} object. + * + * @param classname the fully-qualified name of the class in binary form, e.g., "java.util.List" + * @return the ClassModel of the corresponding classname or null + */ + private @Nullable ClassModel getClassModel(String classname) { + ClassModel cached = classModelCache.get(classname); + if (cached != null) { + return cached; + } + + ClassFile classFile = ClassFile.of(); + URL class_url = ClassLoader.getSystemResource(classname.replace('.', '/') + ".class"); + if (class_url != null) { + try (InputStream inputStream = class_url.openStream()) { + if (inputStream != null) { + byte[] buffer = inputStream.readAllBytes(); + ClassModel result = classFile.parse(buffer); + classModelCache.put(classname, result); + return result; + } + } catch (Throwable t) { + throw new DynCompError( + String.format("Error while reading %s %s%n", classname, class_url), t); + } + } + // Do not cache a null result, because a subsequent invocation might return non-null. + return null; + } + + /** + * Returns true if the method is Object.equals(). + * + * @param methodName method to check + * @param returnType return type of method + * @param paramTypes array of parameter types to method + * @return true if method is Object.equals() + */ + @Pure + boolean is_object_equals( + @Identifier String methodName, ClassDesc returnType, ClassDesc[] paramTypes) { + return (methodName.equals("equals") + && returnType.equals(CD_boolean) + && paramTypes.length == 1 + && paramTypes[0].equals(CD_Object)); + } + + /** + * Returns true if the specified method is Object.clone(). + * + * @param methodName method to check + * @param returnType return type of method + * @param paramTypes array of parameter types to method + * @return true if method is Object.clone() + */ + @Pure + boolean is_object_clone( + @Identifier String methodName, ClassDesc returnType, ClassDesc[] paramTypes) { + return methodName.equals("clone") && returnType.equals(CD_Object) && (paramTypes.length == 0); + } + + /** + * Instrument calls to the Object method {@code clone}. An instrumented version is called if it + * exists, the non-instrumented version if it does not. + * + * @param invoke invoke instruction to inspect and replace + * @param returnType return type of method + * @param classname target class + * @return instruction list to call the correct version of clone or toString + */ + private List instrument_clone_call( + InvokeInstruction invoke, ClassDesc returnType, @BinaryName String classname) { + + List il = new ArrayList<>(); + if (classname.startsWith("[")) { + // .clone() is never instrumented, return original invoke. + il.add(invoke); + return il; + } + + // push the target class + il.add(buildLDCInstruction(poolBuilder.classEntry(ClassDesc.of(classname)))); + + if (invoke.opcode().equals(INVOKESPECIAL)) { + // This is a super call. + + // Runtime will discover if the object's superclass has an instrumented clone method. + // If so, call it; otherwise call the uninstrumented version. + // use CD_Class + il.add(dcr_call("dcomp_super_clone", returnType, new ClassDesc[] {CD_Object, CD_Class})); + + } else { + // This is a regular (non-super) clone() call. + + // Runtime will discover if the object has an instrumented clone method. + // If so, call it; otherwise call the uninstrumented version. + il.add(dcr_call("dcomp_clone", returnType, new ClassDesc[] {CD_Object, CD_Class})); + } + + return il; + } + + /** + * Create the instructions that replace the object eq or ne branch instruction. They are replaced + * by a call to the specified compareMethod (which returns a boolean) followed by the specified + * boolean ifeq or ifne instruction. + * + * @param branch a branch instruction + * @param compareMethod name of DCRuntime routine to call + * @param boolean_if branch instruction to gerate of the runtime call + * @return instruction list to do object comparison + */ + private List object_comparison( + BranchInstruction branch, String compareMethod, Opcode boolean_if) { + List il = new ArrayList<>(); + + MethodRefEntry mre = + poolBuilder.methodRefEntry( + runtimeCD, compareMethod, MethodTypeDesc.of(CD_boolean, objectObjectSig)); + il.add(InvokeInstruction.of(INVOKESTATIC, mre)); + il.add(BranchInstruction.of(boolean_if, branch.target())); + return il; + } + + /** + * Handles load and store field instructions. If the field is a primitive the instructions must be + * augmented to either push (load) or pop (store) the tag on the tag stack. This is accomplished + * by calling the tag get/set method for this field. + * + * @param mgen describes the given method + * @param minfo for the given method's code + * @param fi the field instruction + * @return instruction list to access the field + */ + private @Nullable List load_store_field( + MethodGen24 mgen, MethodGen24.MInfo24 minfo, FieldInstruction fi) { + + ClassDesc field_type = fi.typeSymbol(); + debugInstrument.log("field_type: %s%n", field_type); + int field_size = TypeKind.from(field_type).slotSize(); + if (!field_type.isPrimitive()) { + return null; + } + + List il = new ArrayList<>(); + Opcode op = fi.opcode(); + String fieldName = fi.name().stringValue(); + @BinaryName String owner = Runtime.internalFormToBinaryName(fi.owner().asInternalName()); + ClassDesc ownerCD = fi.owner().asSymbol(); + + // If this class doesn't support tag fields, don't load/store them. + if (!tag_fields_ok(mgen, owner)) { + if (op.equals(GETFIELD) || op.equals(GETSTATIC)) { + il.add(dcr_call("push_const", CD_void, noArgsSig)); + } else { + il.add(loadIntegerConstant(1)); + il.add(dcr_call("discard_tag", CD_void, intSig)); + } + + // Perform the original field command. + il.add(fi); + return il; + } + + if (op.equals(GETSTATIC)) { + MethodRefEntry mre = + poolBuilder.methodRefEntry( + ownerCD, + Premain.tag_method_name(Premain.GET_TAG, owner, fieldName), + MethodTypeDesc.of(CD_void, noArgsSig)); + il.add(InvokeInstruction.of(INVOKESTATIC, mre)); + } else if (op.equals(PUTSTATIC)) { + MethodRefEntry mre = + poolBuilder.methodRefEntry( + ownerCD, + Premain.tag_method_name(Premain.SET_TAG, owner, fieldName), + MethodTypeDesc.of(CD_void, noArgsSig)); + il.add(InvokeInstruction.of(INVOKESTATIC, mre)); + } else if (op.equals(GETFIELD)) { + il.add(StackInstruction.of(DUP)); // dup 'this' + MethodRefEntry mre = + poolBuilder.methodRefEntry( + ownerCD, + Premain.tag_method_name(Premain.GET_TAG, owner, fieldName), + MethodTypeDesc.of(CD_void, noArgsSig)); + il.add(InvokeInstruction.of(INVOKEVIRTUAL, mre)); + } else { // must be PUTFIELD + if (field_size == 2) { + LocalVariable lv = get_tmp2_local(mgen, minfo, field_type); + il.add(StoreInstruction.of(TypeKind.from(field_type), lv.slot())); + il.add(StackInstruction.of(DUP)); // dup 'this' + MethodRefEntry mre = + poolBuilder.methodRefEntry( + ownerCD, + Premain.tag_method_name(Premain.SET_TAG, owner, fieldName), + MethodTypeDesc.of(CD_void, noArgsSig)); + il.add(InvokeInstruction.of(INVOKEVIRTUAL, mre)); + il.add(LoadInstruction.of(TypeKind.from(field_type), lv.slot())); + } else { + il.add(StackInstruction.of(SWAP)); // swap 'this' and 'value' + il.add(StackInstruction.of(DUP)); // dup 'this' + MethodRefEntry mre = + poolBuilder.methodRefEntry( + ownerCD, + Premain.tag_method_name(Premain.SET_TAG, owner, fieldName), + MethodTypeDesc.of(CD_void, noArgsSig)); + il.add(InvokeInstruction.of(INVOKEVIRTUAL, mre)); + il.add(StackInstruction.of(SWAP)); // swap 'value' and 'this' back + } + } + + // Perform the original field command. + il.add(fi); + + return il; + } + + /** + * Handles load local instructions. The instructions must be augmented to push the tag on the tag + * stack. This is accomplished by calling the specified method in DCRuntime and passing that + * method the tag_frame frame and the offset of local/parameter. + * + * @param load a load instruction + * @param tagFrameLocal local variable for the tag_frame + * @param method name of DCRuntime routine to call + * @return instruction list to do object comparison + */ + private List load_local( + LoadInstruction load, LocalVariable tagFrameLocal, String method) { + List il = new ArrayList<>(); + + // debugInstrument.log("CreateLoad %s %d%n", load.opcode(), load.slot()); + + // Push the tag_frame frame + il.add(LoadInstruction.of(TypeKind.REFERENCE, tagFrameLocal.slot())); + + // push index of local + il.add(loadIntegerConstant(load.slot())); + + // Call the runtime method to handle loading the local/parameter + MethodRefEntry mre = + poolBuilder.methodRefEntry( + runtimeCD, method, MethodTypeDesc.of(CD_void, objectArrayCD, CD_int)); + il.add(InvokeInstruction.of(INVOKESTATIC, mre)); + + // the original load instruction + il.add(load); + return il; + } + + /** + * Handles store local instructions. The instructions must be augmented to pop the tag off the tag + * stack. This is accomplished by calling the specified method in DCRuntime and passing that + * method the tag_frame frame and the offset of local/parameter. + * + * @param store a store instruction + * @param tagFrameLocal local variable for the tag_frame + * @param method name of DCRuntime routine to call + * @return instruction list to do object comparison + */ + private List store_local( + StoreInstruction store, LocalVariable tagFrameLocal, String method) { + List il = new ArrayList<>(); + + // debugInstrument.log("CreateStore %s %d%n", store.opcode(), store.slot()); + + // Push the tag_frame frame + il.add(LoadInstruction.of(TypeKind.REFERENCE, tagFrameLocal.slot())); + + // push index of local + il.add(loadIntegerConstant(store.slot())); + + // Call the runtime method to handle storeing the local/parameter + MethodRefEntry mre = + poolBuilder.methodRefEntry( + runtimeCD, method, MethodTypeDesc.of(CD_void, objectArrayCD, CD_int)); + il.add(InvokeInstruction.of(INVOKESTATIC, mre)); + + // the original store instruction + il.add(store); + return il; + } + + /** + * Gets the local variable used to store a category2 temporary. This is used in the PUTFIELD code + * to temporarily store the value being placed in the field. + * + * @param mgen describes the given method + * @param minfo for the given method's code + * @param type type of the local temp + * @return the local temp + */ + LocalVariable get_tmp2_local(MethodGen24 mgen, MethodGen24.MInfo24 minfo, ClassDesc type) { + + String name = "dcomp_$tmp_" + type.descriptorString(); + + // See if the local has already been created + for (LocalVariable lv : mgen.localsTable) { + if (lv.name().stringValue().equals(name)) { + assert lv.typeSymbol().equals(type) : lv + " " + type; + return lv; + } + } + + // Create the variable + return createLocalWithMethodScope(mgen, minfo, name, type); + } + + /** + * Returns the local variable used to store the return result. If it is not present, creates it + * with the specified type. If the variable is known to already exist, the type can be null. + * + * @param mgen describes the given method + * @param returnType the type of the return; may be null if the variable is known to already exist + * @param minfo for the given method's code + * @return a local variable to save the return value + */ + @SuppressWarnings("nullness") + private LocalVariable getReturnLocal( + MethodGen24 mgen, @Nullable ClassDesc returnType, MethodGen24.MInfo24 minfo) { + + // If a type was specified and the variable was found, they must match. + if (minfo.returnLocal == null) { + assert returnType != null : " return__$trace2_val doesn't exist"; + } else { + assert minfo.returnLocal.typeSymbol().equals((Object) returnType) + : " returnType = " + returnType + "; current type = " + minfo.returnLocal.typeSymbol(); + } + + if (minfo.returnLocal == null) { + debugInstrument.log("Adding return local of type %s%n", returnType); + minfo.returnLocal = + createLocalWithMethodScope(mgen, minfo, "return__$trace2_val", returnType); + } + + return minfo.returnLocal; + } + + /** + * Creates a MethodInfo corresponding to the specified method. The exit location information for + * the method is collected. Returns null if the method contains no instructions. + * + * @param classInfo class containing the method + * @param mgen method to inspect + * @return MethodInfo for the method + */ + private @Nullable MethodInfo create_method_info_if_instrumented( + ClassInfo classInfo, MethodGen24 mgen) { + + // Get the parameter names for this method + String[] paramNames = mgen.getParameterNames(); + + if (debugInstrument.enabled) { + debugInstrument.log("create_method_info_if_instrumented: %s%n", paramNames.length); + for (String paramName : paramNames) { + debugInstrument.log("param name: %s%n", paramName); + } + } + + // Get the parameter types for this method. + ClassDesc[] paramTypes = mgen.getParameterTypes(); + @ClassGetName String[] param_type_strings = new @ClassGetName String[paramTypes.length]; + for (int i = 0; i < paramTypes.length; i++) { + param_type_strings[i] = Instrument24.classDescToClassGetName(paramTypes[i]); + } + + // Loop through each instruction and find the line number for each return opcode. + List exit_line_numbers = new ArrayList<>(); + + // Tells whether each exit loc in the method is included or not (based on filters). + List isIncluded = new ArrayList<>(); + + debugInstrument.log("Looking for exit points in %s%n", mgen.getName()); + List il = mgen.getInstructionList(); + if (il.isEmpty()) { + return null; + } + + int line_number = 0; + int last_line_number = 0; + + for (CodeElement inst : il) { + boolean foundLine = false; + + if (inst instanceof LineNumber ln) { + line_number = ln.line(); + foundLine = true; + } + + if (inst instanceof ReturnInstruction) { + debugInstrument.log("Exit at line %d%n", line_number); + + // Only do incremental lines if we don't have the line generator. + if (line_number == last_line_number && foundLine == false) { + debugInstrument.log("Could not find line %d%n", line_number); + line_number++; + } + last_line_number = line_number; + + exit_line_numbers.add(line_number); + isIncluded.add(true); + } + } + + return new MethodInfo( + classInfo, mgen.getName(), paramNames, param_type_strings, exit_line_numbers, isIncluded); + } + + /** + * Creates code that makes the index comparable (for indexing purposes) with the array in array + * load instructions. First the arrayref and its index are duplicated on the stack. Then the + * appropriate array load method is called to mark them as comparable and update the tag stack. + * Finally the original load instruction is performed. + * + * @param mgen method to check + * @param inst an array load instruction + * @return instruction list that calls the runtime to handle the array load instruction + */ + private List array_load(MethodGen24 mgen, ArrayLoadInstruction inst) { + List il = new ArrayList<>(); + + // Duplicate the array ref and index and pass them to DCRuntime + // which will make the index comparable with the array. In the case + // of primtives it will also get the tag for the primitive and push + // it on the tag stack. + il.add(StackInstruction.of(DUP2)); + String method = "primitive_array_load"; + if (inst.typeKind().equals(TypeKind.REFERENCE)) { + method = "ref_array_load"; + } else if (is_class_initialized_by_jvm(mgen.getClassName())) { + method = "primitive_array_load_null_ok"; + } + + il.add(dcr_call(method, CD_void, new ClassDesc[] {CD_Object, CD_int})); + + // Perform the original instruction + il.add(inst); + + return il; + } + + /** + * Creates code to make the index comparable (for indexing purposes) with the array in the array + * store instruction. This is accomplished by calling the specified method and passing it the + * array reference, index, and value (of base_type). The method will mark the array and index as + * comparable and perform the array store. + * + * @param inst an array store instruction + * @param method runtime method to call + * @param base_type type of array store + * @return instruction list that calls the runtime to handle the array store instruction + */ + List array_store(CodeElement inst, @Identifier String method, ClassDesc base_type) { + List il = new ArrayList<>(); + ClassDesc array_type = base_type.arrayType(1); + // if (method.equals("aastore")) { + // System.out.println("array_store aastore: " + method); + // il.add(loadIntegerConstant(counter++)); + // il.add(dcr_call(method, CD_void, new ClassDesc[] {array_type, CD_int, base_type, CD_int})); + // } else { + // System.out.println("array_store other: " + method); + il.add(dcr_call(method, CD_void, new ClassDesc[] {array_type, CD_int, base_type})); + // } + return il; + } + + /** + * Creates code that pushes the array's tag onto the tag stack, so that the index is comparable to + * the array length. First, the arrayref is duplicated on the stack. Then a method is called to + * push the array's tag onto the tag stack. Finally the original arraylength instruction is + * performed. + * + * @param inst an arraylength instruction + * @return instruction list that calls the runtime to handle the arraylength instruction + */ + private List array_length(Instruction inst) { + List il = new ArrayList<>(); + + // Duplicate the array ref and pass it to DCRuntime which will push + // it onto the tag stack. + il.add(StackInstruction.of(DUP)); + il.add(dcr_call("push_array_tag", CD_void, object_arg)); + + // Perform the original instruction + il.add(inst); + + return il; + } + + /** + * Creates code to make the declared length of a new array comparable to its index. + * + * @param inst a anewarray or newarray instruction + * @return instruction list that calls the runtime to handle the newarray instruction + */ + private List new_array(Instruction inst) { + List il = new ArrayList<>(); + + // Perform the original instruction + il.add(inst); + + // Duplicate the array ref from the top of the stack and pass it + // to DCRuntime which will push it onto the tag stack. + il.add(StackInstruction.of(DUP)); + il.add(dcr_call("push_array_tag", CD_void, object_arg)); + + // Make the array and the count comparable. Also, pop the tags for + // the array and the count off the tag stack. + il.add(dcr_call("cmp_op", CD_void, noArgsSig)); + + return il; + } + + /** + * Creates code to make the declared lengths of a new two-dimensional array comparable to the + * corresponding indices. + * + * @param inst a multianewarray instruction + * @return instruction list that calls the runtime to handle the multianewarray instruction + */ + private List multiarray2(Instruction inst) { + List il = new ArrayList<>(); + + // Duplicate both count arguments + il.add(StackInstruction.of(DUP2)); + + // Perform the original instruction + il.add(inst); + + // Duplicate the new arrayref and put it below the count arguments + // Stack is now: ..., arrayref, count1, count2, arrayref + il.add(StackInstruction.of(DUP_X2)); + + il.add(dcr_call("multianewarray2", CD_void, new ClassDesc[] {CD_int, CD_int, objectArrayCD})); + + return il; + } + + /** + * Returns true if this ppt should be included. A ppt is included if it matches ones of the select + * patterns and doesn't match any of the omit patterns. + * + * @param className class to test + * @param methodName method to test + * @param pptName ppt to look for + * @return true if this ppt should be included + */ + boolean should_track( + @BinaryName String className, @Identifier String methodName, String pptName) { + + debugInstrument.log("Considering tracking ppt: %s, %s, %s%n", className, methodName, pptName); + + // Don't track any JDK classes + if (BcelUtil.inJdk(className)) { + debug_transform.log("not including %s as it is a JDK class%n", className); + return false; + } + + // Don't track toString methods because we call them in + // our debug statements. + if (pptName.contains("toString")) { + debug_transform.log("not including %s as it is a toString method%n", pptName); + return false; + } + + // Call `shouldIgnore` to check ppt-omit-patterns and ppt-select-patterns. + return !daikon.chicory.Instrument24.shouldIgnore(className, methodName, pptName); + } + + /** + * Constructs a ppt entry name from a Method. + * + * @param fullClassName class name + * @param mgen method + * @return corresponding ppt name + */ + static String methodEntryName(String fullClassName, MethodGen24 mgen) { + + // Get an array of the type names + ClassDesc[] paramTypes = mgen.getParameterTypes(); + String[] type_names = new String[paramTypes.length]; + for (int ii = 0; ii < paramTypes.length; ii++) { + @FieldDescriptor String paramFD = paramTypes[ii].descriptorString(); + type_names[ii] = daikon.chicory.Instrument24.convertDescriptorToFqBinaryName(paramFD); + } + + return DaikonWriter.methodEntryName(fullClassName, type_names, "", mgen.getName()); + } + + /** + * Constructs a call to a static method in DCRuntime. + * + * @param methodName method to call + * @param returnType type of method return + * @param paramTypes array of method parameter types + * @return InvokeInstruction for the call + */ + InvokeInstruction dcr_call( + @Identifier String methodName, ClassDesc returnType, ClassDesc[] paramTypes) { + MethodRefEntry mre = + poolBuilder.methodRefEntry( + runtimeCD, methodName, MethodTypeDesc.of(returnType, paramTypes)); + return InvokeInstruction.of(INVOKESTATIC, mre); + } + + /** + * Create the code to call discard_tag(tag_count). If inst is not null, append it to the end of + * that code. + * + * @param inst instruction to be replaced + * @param tag_count number of tags to discard + * @return instruction list to discard tag(s) + */ + List discard_tag_code(@Nullable CodeElement inst, int tag_count) { + List il = new ArrayList<>(); + il.add(loadIntegerConstant(tag_count)); + il.add(dcr_call("discard_tag", CD_void, intSig)); + if (inst != null) { + il.add(inst); + } + return il; + } + + /** + * Duplicates a category 1 item on the top of stack. If it is a primitive, we need to do the same + * to the tag stack. Otherwise, we do nothing. + */ + @Nullable List dup_tag(CodeElement inst, OperandStack24 stack) { + ClassDesc top = stack.peek(); + if (debug_dup.enabled) { + debug_dup.log("DUP -> %s [... %s]%n", "dup", stack_contents(stack, 2)); + } + if (top.isPrimitive()) { + return build_il(dcr_call("dup", CD_void, noArgsSig), inst); + } + return null; + } + + /** + * Duplicates the item on the top of the stack and inserts it 2 values down in the stack. If the + * value at the top of the stack is not a primitive, there is nothing to do here. If the second + * value is not a primitive, then we need only to insert the duped value down 1 on the tag stack + * (which contains only primitives). + */ + @Nullable List dup_x1_tag(CodeElement inst, OperandStack24 stack) { + ClassDesc top = stack.peek(); + String op; + if (!top.isPrimitive()) { + return null; + } else if (stack.peek(1).isPrimitive()) { + op = "dup_x1"; + } else { + op = "dup"; + } + if (debug_dup.enabled) { + debug_dup.log("DUP_X1 -> %s [... %s]%n", op, stack_contents(stack, 2)); + } + return build_il(dcr_call(op, CD_void, noArgsSig), inst); + } + + /** + * Dup the category 1 value on the top of the stack and insert it either two or three values down + * on the stack. + */ + @Nullable List dup_x2_tag(CodeElement inst, OperandStack24 stack) { + ClassDesc value1 = stack.peek(); + if (!value1.isPrimitive()) { + return null; + } + ClassDesc value2 = stack.peek(1); + String op; + if (is_category2(value2)) { + op = "dup_x1"; + } else { + ClassDesc value3 = stack.peek(2); + if (value2.isPrimitive() && value3.isPrimitive()) { + op = "dup_x2"; + } else if (value2.isPrimitive() || value3.isPrimitive()) { + op = "dup_x1"; + } else { + op = "dup"; + } + } + if (debug_dup.enabled) { + debug_dup.log("DUP_X2 -> %s [... %s]%n", op, stack_contents(stack, 3)); + } + return build_il(dcr_call(op, CD_void, noArgsSig), inst); + } + + /** + * Duplicate either one category 2 value or two category 1 values. If the value(s) are primitives + * we need to do the same to the tag stack. Otherwise, we do nothing. + */ + @Nullable List dup2_tag(CodeElement inst, OperandStack24 stack) { + ClassDesc top = stack.peek(); + String op; + if (is_category2(top)) { + op = "dup"; + } else if (top.isPrimitive() && stack.peek(1).isPrimitive()) { + op = "dup2"; + } else if (top.isPrimitive() || stack.peek(1).isPrimitive()) { + op = "dup"; + } else { + // both of the top two items are not primitive, nothing to dup + return null; + } + if (debug_dup.enabled) { + debug_dup.log("DUP2 -> %s [... %s]%n", op, stack_contents(stack, 2)); + } + return build_il(dcr_call(op, CD_void, noArgsSig), inst); + } + + /** + * Duplicates either the top 2 category 1 values or a single category 2 value and inserts it 2 or + * 3 values down on the stack. + */ + @Nullable List dup2_x1_tag(CodeElement inst, OperandStack24 stack) { + ClassDesc value1 = stack.peek(); + ClassDesc value2 = stack.peek(1); + String op; + if (is_category2(value1)) { + if (value2.isPrimitive()) { + op = "dup_x1"; + } else { // not a primitive, so just dup + op = "dup"; + } + } else { // value1 is not category 2 + ClassDesc value3 = stack.peek(2); + if (value1.isPrimitive()) { + if (value2.isPrimitive() && value3.isPrimitive()) { + op = "dup2_x1"; + } else if (value2.isPrimitive()) { + op = "dup2"; + } else if (value3.isPrimitive()) { + op = "dup_x1"; + } else { + // neither value2 nor value3 is primitive + op = "dup"; + } + } else { // value1 is not primitive + if (value2.isPrimitive() && value3.isPrimitive()) { + op = "dup_x1"; + } else if (value2.isPrimitive()) { + op = "dup"; + } else { // neither value2 or value3 is primitive + return null; + } + } + } + if (debug_dup.enabled) { + debug_dup.log("DUP2_X1 -> %s [... %s]%n", op, stack_contents(stack, 3)); + } + return build_il(dcr_call(op, CD_void, noArgsSig), inst); + } + + /** + * Duplicate the top one or two operand stack values and insert two, three, or four values down. + */ + @Nullable List dup2_x2_tag(CodeElement inst, OperandStack24 stack) { + ClassDesc value1 = stack.peek(); + ClassDesc value2 = stack.peek(1); + String op; + if (is_category2(value1)) { + if (is_category2(value2)) { + op = "dup_x1"; + } else { + ClassDesc value3 = stack.peek(2); + if (value2.isPrimitive() && value3.isPrimitive()) { + op = "dup_x2"; + } else if (value2.isPrimitive() || value3.isPrimitive()) { + op = "dup_x1"; + } else { + // neither value2 or value3 is primitive + op = "dup"; + } + } + } else { // value1 and value2 are not category 2 + ClassDesc value3 = stack.peek(2); + if (value1.isPrimitive()) { + if (is_category2(value2)) { + throw new DynCompError("not supposed to happen " + stack_contents(stack, 3)); + } else if (is_category2(value3)) { + if (value2.isPrimitive()) { + op = "dup2_x1"; + } else { + op = "dup_x1"; + } + } else if (value2.isPrimitive()) { + // value1 and value2 are primitive + ClassDesc value4 = stack.peek(3); + if (value3.isPrimitive() && value4.isPrimitive()) { + op = "dup2_x2"; + } else if (value3.isPrimitive() || value4.isPrimitive()) { + op = "dup2_x1"; + } else { + // neither value3 or value4 is primitive + op = "dup2"; + } + } else { // value1 is primitive value2 is not primitive + ClassDesc value4 = stack.peek(3); + if (value3.isPrimitive() && value4.isPrimitive()) { + op = "dup_x2"; + } else if (value3.isPrimitive() || value4.isPrimitive()) { + op = "dup_x1"; + } else { + // neither value3 or value4 is primitive + op = "dup"; + } + } + } else { // value1 is not primitive + if (is_category2(value2)) { + throw new DynCompError("not supposed to happen " + stack_contents(stack, 3)); + } else if (is_category2(value3)) { + if (value2.isPrimitive()) { + op = "dup_x1"; + } else { + return null; // nothing to dup + } + } else if (value2.isPrimitive()) { + // value1 is not primitive value2 is primitive + ClassDesc value4 = stack.peek(3); + if (value3.isPrimitive() && value4.isPrimitive()) { + op = "dup_x2"; + } else if (value3.isPrimitive() || value4.isPrimitive()) { + op = "dup_x1"; + } else { + // neither value3 or value4 is primitive + op = "dup"; + } + } else { // neither value1 or value2 is primitive + return null; // nothing to dup + } + } + } + if (debug_dup.enabled) { + debug_dup.log("DUP_X2 -> %s [... %s]%n", op, stack_contents(stack, 3)); + } + return build_il(dcr_call(op, CD_void, noArgsSig), inst); + } + + /** + * Pops a category 1 value from the top of the stack. We want to discard the top of the tag stack + * iff the item on the top of the stack is a primitive. + */ + @Nullable List pop_tag(CodeElement inst, OperandStack24 stack) { + ClassDesc top = stack.peek(); + if (debug_dup.enabled) { + debug_dup.log("POP -> %s [... %s]%n", "pop", stack_contents(stack, 1)); + } + if (top.isPrimitive()) { + return discard_tag_code(inst, 1); + } + return null; + } + + /** + * Pops either the top 2 category 1 values or a single category 2 value from the top of the stack. + * We must do the same to the tag stack if the values are primitives. + */ + @Nullable List pop2_tag(CodeElement inst, OperandStack24 stack) { + ClassDesc top = stack.peek(); + if (debug_dup.enabled) { + debug_dup.log("POP2 -> %s [... %s]%n", "pop2", stack_contents(stack, 1)); + } + if (is_category2(top)) { + return discard_tag_code(inst, 1); + } else { + int cnt = 0; + if (top.isPrimitive()) { + cnt++; + } + if (stack.peek(1).isPrimitive()) { + cnt++; + } + if (cnt > 0) { + return discard_tag_code(inst, cnt); + } + } + return null; + } + + /** + * Swaps the two category 1 values on the top of the stack. We need to swap the top of the tag + * stack if the two top elements on the real stack are primitives. + */ + @Nullable List swap_tag(CodeElement inst, OperandStack24 stack) { + ClassDesc type1 = stack.peek(); + ClassDesc type2 = stack.peek(1); + if (debug_dup.enabled) { + debug_dup.log("SWAP -> %s [... %s]%n", "swap", stack_contents(stack, 2)); + } + if (type1.isPrimitive() && type2.isPrimitive()) { + return build_il(dcr_call("swap", CD_void, noArgsSig), inst); + } + return null; + } + + /** + * Handle the instruction that allocates multi-dimensional arrays. If the new array has 2 + * dimensions, make the integer arguments comparable to the corresponding indices of the new + * array. For any other number of dimensions, discard the tags for the arguments. Higher + * dimensions should really be handled as well, but there are very few cases of this and the + * resulting code would be quite complex (see multiarray2 for details). + */ + private List multi_newarray_dc(NewMultiArrayInstruction inst) { + int dims = inst.dimensions(); + if (dims == 2) { + return multiarray2(inst); + } else { + return discard_tag_code(inst, dims); + } + } + + /** + * Create an instruction list that calls the runtime to handle returns for the tag stack followed + * by the original return instruction. + * + * @param mgen method to check + * @param inst return instruction to be replaced + * @return the instruction list + */ + private List return_tag(MethodGen24 mgen, Instruction inst) { + List il = new ArrayList<>(); + + ClassDesc type = mgen.getReturnType(); + + // Push the tag frame + il.add(LoadInstruction.of(TypeKind.REFERENCE, tagFrameLocal.slot())); + + if (is_primitive(type)) { + il.add(dcr_call("normal_exit_primitive", CD_void, objectArrayCD_arg)); + } else { + il.add(dcr_call("normal_exit", CD_void, objectArrayCD_arg)); + } + il.add(inst); + return il; + } + + /** + * Returns true if the specified type is a primitive (int, float, double, etc). + * + * @param type type to check + * @return true if type is primitive + */ + @Pure + boolean is_primitive(ClassDesc type) { + return type.isPrimitive() && !type.equals(CD_void); + } + + /** + * Returns true if the specified type is a category 2 (8 byte) type. + * + * @param type type to check + * @return true if type requires 8 bytes + */ + @Pure + boolean is_category2(ClassDesc type) { + return type.equals(CD_double) || type.equals(CD_long); + } + + /** + * Modify a doubled native method to call its original method. It pops all of the parameter tags + * off of the tag stack. If there is a primitive return value it puts a new tag value on the stack + * for it. + * + *

TODO: add a way to provide a synopsis for native methods that affect comparability. + * + * @param mgen the interface method. Must be native. + */ + void fix_native(MethodGen24 mgen) { + + ClassDesc[] paramTypes = mgen.getParameterTypes(); + + debug_native.log("Native call %s%n", mgen); + + // Discard the tags for any primitive arguments passed to the method. + List il = discard_primitive_tags(paramTypes); + + // push a tag if there is a primitive return value + ClassDesc returnType = mgen.getReturnType(); + if (is_primitive(returnType)) { + il.add(dcr_call("push_const", CD_void, noArgsSig)); + } + + // If the method is not static, push the instance on the stack + if (!mgen.isStatic()) { + il.add(LoadInstruction.of(TypeKind.REFERENCE, 0)); // load this + } + + // if call is sun.reflect.Reflection.getCallerClass(int depth) + // TODO: This method was deleted in JDK 9. At some point we should remove support. + if (mgen.getName().equals("getCallerClass") + && (paramTypes.length == 1) // 'int depth' + && mgen.getClassName().equals("sun.reflect.Reflection")) { + + // The call returns the class 'depth' frames up the stack. Since we have added + // our wrapper method call in between, we need to increment that number by 1. + il.add(LoadInstruction.of(TypeKind.INT, 0)); // load depth + il.add(loadIntegerConstant(1)); + il.add(OperatorInstruction.of(IADD)); + // System.out.printf("adding 1 in %s.%s%n", mgen.getClassName(), + // mgen.getName()); + + } else { // normal call + + // push each argument on the stack + int param_index = 1; + if (mgen.isStatic()) { + param_index = 0; + } + for (ClassDesc paramType : paramTypes) { + il.add(LoadInstruction.of(TypeKind.from(paramType), param_index)); + param_index += TypeKind.from(paramType).slotSize(); + } + } + + // Call the native method + MethodRefEntry mre = + poolBuilder.methodRefEntry( + ClassDesc.of(mgen.getClassName()), + mgen.getName(), + MethodTypeDesc.of(returnType, paramTypes)); + Opcode op = mgen.isStatic() ? INVOKESTATIC : INVOKEVIRTUAL; + il.add(InvokeInstruction.of(op, mre)); + + // generate return instruction + il.add(ReturnInstruction.of(TypeKind.from(returnType))); + + mgen.setInstructionList(il); + } + + /** + * Convenience function to build an instruction list. + * + * @param instructions a variable number of instructions + * @return an instruction list + */ + private List build_il(CodeElement... instructions) { + return new ArrayList<>(Arrays.asList(instructions)); + } + + /** + * Returns true if tag fields are used within the specified method of the specified class. We can + * safely use class fields except in Object, String, and Class. If checking a class, mgen is null. + * + * @param mgen method to check + * @param classname class containing {@code mgen} + * @return true if tag fields may be used in class for method + */ + boolean tag_fields_ok(@Nullable MethodGen24 mgen, @BinaryName String classname) { + + // Prior to Java 8 an interface could not contain any implementations. + if (classGen.isInterface()) { + if (classModel.majorVersion() < ClassFile.JAVA_8_VERSION) { + return false; + } + } + + if (mgen != null) { + if (mgen.getName().equals("")) { + if (!this.constructor_is_initialized) { + return false; + } + } + } + + if (!Premain.jdk_instrumented) { + if (BcelUtil.inJdk(classname)) { + return false; + } + } + + if (!classname.startsWith("java.lang")) { + return true; + } + + return !(classname.equals("java.lang.String") + || classname.equals("java.lang.Class") + || classname.equals("java.lang.Object") + || classname.equals("java.lang.ClassLoader")); + } + + /** + * Returns a string describing the top max_items items on the stack. + * + * @param stack OperandStack + * @param max_items number of items to describe + * @return string describing the top max_items on the operand stack + */ + static String stack_contents(OperandStack24 stack, int max_items) { + String contents = ""; + if (max_items >= stack.size()) { + max_items = stack.size() - 1; + } + for (int ii = max_items; ii >= 0; ii--) { + if (contents.length() != 0) { + contents += ", "; + } + contents += stack.peek(ii); + } + return contents; + } + + /** + * Creates tag get and set accessor methods for each field in the class. An accessor is created + * for each field (including final, static, and private fields). The accessors share the modifiers + * of their field (except that all are final). Accessors are named {@code + * ___$get_tag} and {@code ___$set_tag}. The class name must be + * included because field names can shadow one another. + * + *

If tag_fields_ok is true for the class, then tag fields are created and the accessor uses + * the tag fields. If not, tag storage is created separately and accessed via the field number. + * + *

Accessors are also created for each visible superclass field that is not hidden by a field + * in this class. These accessors just call the superclasses accessor. + * + *

Any accessors created are added to the class. + * + * @param classGen class to check for fields + */ + void create_tag_accessors(ClassGen24 classGen) { + + // If this class doesn't support tag fields, don't create them + if (!tag_fields_ok(null, classGen.getClassName())) { + return; + } + + Set field_set = new HashSet<>(); + Map field_to_offset_map = build_field_to_offset_map(classModel); + + // Build accessors for all fields declared in this class + for (FieldModel fm : classModel.fields()) { + + String fieldName = fm.fieldName().stringValue(); + assert !field_set.contains(fieldName) : fieldName + "-" + classGen.getClassName(); + field_set.add(fieldName); + + // skip primitive fields + // MLR: skip non primitive fields? + if (!fm.fieldTypeSymbol().isPrimitive()) { + continue; + } + + @SuppressWarnings("nullness:unboxing.of.nullable") + int tagOffset = + fm.flags().has(AccessFlag.STATIC) + ? static_field_id.get(full_name(classModel, fm)) + : field_to_offset_map.get(fm); + create_get_tag(classGen, fm, tagOffset); + create_set_tag(classGen, fm, tagOffset); + } + + // Build accessors for each field declared in a superclass that is + // is not shadowed in a subclass + for (@BinaryName String scn : getStrictSuperclassNames()) { + ClassModel scm = getClassModel(scn); + if (scm == null) { + throw new DynCompError("Can't load ClassModel for: " + scn); + } + + for (FieldModel fm : scm.fields()) { + if (fm.flags().has(AccessFlag.PRIVATE)) { + continue; + } + if (field_set.contains(fm.fieldName().stringValue())) { + continue; + } + if (!fm.fieldTypeSymbol().isPrimitive()) { + continue; + } + + field_set.add(fm.fieldName().stringValue()); + @SuppressWarnings("nullness:unboxing.of.nullable") + int tagOffset = + fm.flags().has(AccessFlag.STATIC) + ? static_field_id.get(full_name(scm, fm)) + : field_to_offset_map.get(fm); + create_get_tag(classGen, fm, tagOffset); + create_set_tag(classGen, fm, tagOffset); + } + } + } + + /** + * Builds a Map that relates each field in jc and each of its superclasses to a unique offset. The + * offset can be used to index into a tag array for this class. Instance fields are placed in the + * returned map and static fields are placed in static map (shared between all classes). + * + * @param classModel class to check for fields + * @return field offset map + */ + Map build_field_to_offset_map(ClassModel classModel) { + + Optional ce = classModel.superclass(); + if (!ce.isPresent()) { + // class is java.lang.Object, no primitive fields + return new LinkedHashMap<>(); + } + + // Get the offsets for each field in the superclass. + String superclassName = Runtime.internalFormToBinaryName(ce.get().asInternalName()); + ClassModel super_cm = getClassModel(superclassName); + if (super_cm == null) { + throw new DynCompError("Can't get superclass for " + superclassName); + } + + Map field_to_offset_map = build_field_to_offset_map(super_cm); + int offset = field_to_offset_map.size(); + + // Determine the offset for each primitive field in the class + // Also make sure the static_tags list is large enough for all + // of the tags. + for (FieldModel fm : classModel.fields()) { + if (!fm.fieldTypeSymbol().isPrimitive()) { + continue; + } + if (fm.flags().has(AccessFlag.STATIC)) { + if (!in_jdk) { + int min_size = static_field_id.size() + DCRuntime.max_jdk_static; + while (DCRuntime.static_tags.size() <= min_size) { + DCRuntime.static_tags.add(null); + } + static_field_id.put(full_name(classModel, fm), min_size); + } else { // building jdk + String full_name = full_name(classModel, fm); + if (static_field_id.containsKey(full_name)) { + // System.out.printf("Reusing static field %s value %d%n", + // full_name, static_field_id.get(full_name)); + } else { + // System.out.printf("Allocating new static field %s%n", + // full_name); + static_field_id.put(full_name, static_field_id.size() + 1); + } + } + } else { + field_to_offset_map.put(fm, offset); + offset++; + } + } + + return field_to_offset_map; + } + + /** + * Creates a get tag method for field fm. The tag corresponding to field fm will be pushed on the + * tag stack. + * + *

{@code
+   * void ___$get_tag() {
+   *   #if fm.isStatic()
+   *     DCRuntime.push_static_tag (tag_offset)
+   *   #else
+   *     DCRuntime.push_field_tag (this, tag_offset);
+   * }
+   * }
+ * + * @param classGen class whose accessors are being built. Not necessarily the class declaring fm + * (if fm is inherited). + * @param fm field to build an accessor for + * @param tag_offset offset of fm in the tag storage for this field + */ + void create_get_tag(ClassGen24 classGen, FieldModel fm, int tag_offset) { + + // Determine the method to call in DCRuntime. Instance fields and static + // fields are handled separately. Also instance fields in special + // classes that are created by the JVM are handled separately since only + // in those classes can fields be read without being written (in java) + String classname = classGen.getClassName(); + String methodname = "push_field_tag"; + ClassDesc[] params; + final boolean isStatic = fm.flags().has(AccessFlag.STATIC) ? true : false; + + if (isStatic) { + methodname = "push_static_tag"; + params = new ClassDesc[] {CD_int}; + } else if (is_class_initialized_by_jvm(classname)) { + methodname = "push_field_tag_null_ok"; + params = new ClassDesc[] {CD_Object, CD_int}; + } else { + methodname = "push_field_tag"; + params = new ClassDesc[] {CD_Object, CD_int}; + } + + String accessor_name = + Premain.tag_method_name(Premain.GET_TAG, classname, fm.fieldName().stringValue()); + + List newCode = new ArrayList<>(); + + newCode.add(loadIntegerConstant(tag_offset)); + newCode.add(dcr_call(methodname, CD_void, params)); + newCode.add(ReturnInstruction.of(TypeKind.VOID)); + + int access_flags = fm.flags().flagsMask(); + if (classGen.isInterface()) { + // method in interface cannot be final + access_flags &= ~AccessFlag.FINAL.mask(); + if (classModel.majorVersion() < ClassFile.JAVA_8_VERSION) { + // If class file version is prior to 8 then a method in an interface + // cannot be static (it's implicit) and must be abstract. + access_flags &= ~AccessFlag.STATIC.mask(); + access_flags |= AccessFlag.ABSTRACT.mask(); + } + } else { + access_flags |= AccessFlag.FINAL.mask(); + } + + // make method public + access_flags &= ~ACC_PRIVATE; + access_flags &= ~ACC_PROTECTED; + access_flags |= ACC_PUBLIC; + + // Create the get accessor method + classGen + .getClassBuilder() + .withMethod( + accessor_name, + MethodTypeDesc.of(CD_void), + access_flags, + methodBuilder -> + methodBuilder.withCode( + codeBuilder -> buildTagAccessor(codeBuilder, newCode, isStatic, classname))); + } + + /** + * Creates a set tag method for field fm. The tag on the top of the tag stack will be popped off + * and placed in the tag storeage corresponding to field + * + *
{@code
+   * void ___$set_tag() {
+   *   #if fm.isStatic()
+   *     DCRuntime.pop_static_tag (tag_offset)
+   *   #else
+   *     DCRuntime.pop_field_tag (this, tag_offset);
+   * }
+   * }
+ * + * @param classGen class whose accessors are being built. Not necessarily the class declaring fm + * (if fm is inherited). + * @param fm field to build an accessor for + * @param tag_offset offset of fm in the tag storage for this field + */ + void create_set_tag(ClassGen24 classGen, FieldModel fm, int tag_offset) { + + String classname = classGen.getClassName(); + String methodname = "pop_field_tag"; + ClassDesc[] params = {CD_Object, CD_int}; + final boolean isStatic = fm.flags().has(AccessFlag.STATIC) ? true : false; + + if (isStatic) { + methodname = "pop_static_tag"; + params = new ClassDesc[] {CD_int}; + } + + String setter_name = + Premain.tag_method_name(Premain.SET_TAG, classname, fm.fieldName().stringValue()); + + List newCode = new ArrayList<>(); + + newCode.add(loadIntegerConstant(tag_offset)); + newCode.add(dcr_call(methodname, CD_void, params)); + newCode.add(ReturnInstruction.of(TypeKind.VOID)); + + int access_flags = fm.flags().flagsMask(); + if (classGen.isInterface()) { + // method in interface cannot be final + access_flags &= ~AccessFlag.FINAL.mask(); + if (classModel.majorVersion() < ClassFile.JAVA_8_VERSION) { + // If class file version is prior to 8 then a method in an interface + // cannot be static (it's implicit) and must be abstract. + access_flags &= ~AccessFlag.STATIC.mask(); + access_flags |= AccessFlag.ABSTRACT.mask(); + } + } else { + access_flags |= AccessFlag.FINAL.mask(); + } + + // Create the setter method. + classGen + .getClassBuilder() + .withMethod( + setter_name, + MethodTypeDesc.of(CD_void), + access_flags, + methodBuilder -> + methodBuilder.withCode( + codeBuilder -> buildTagAccessor(codeBuilder, newCode, isStatic, classname))); + } + + /** + * Build a tag accessor method. + * + * @param codeBuilder for the given method's code + * @param instructions instruction list to copy + * @param isStatic true iff accessor is static + * @param classname classname holding accessor + */ + private void buildTagAccessor( + CodeBuilder codeBuilder, List instructions, boolean isStatic, String classname) { + + Label startLabel = codeBuilder.newLabel(); + Label endLabel = codeBuilder.newLabel(); + if (!isStatic) { + codeBuilder.localVariable(0, "this", ClassDesc.of(classname), startLabel, endLabel); + codeBuilder.labelBinding(startLabel); + codeBuilder.with(LoadInstruction.of(TypeKind.REFERENCE, 0)); // aload_0 (load this) + } + for (CodeElement ce : instructions) { + debugInstrument.log("CodeElement: %s%n", ce); + codeBuilder.with(ce); + } + codeBuilder.labelBinding(endLabel); // shouldn't matter if isStatic + } + + /** + * Adds the DCompInstrumented interface to the given class. Also adds the following method to the + * class, so that it implements the DCompInstrumented interface: + * + *
{@code
+   * public boolean equals_dcomp_instrumented(Object o) {
+   *   return this.equals(o, null);
+   * }
+   * }
+ * + * The method does nothing except call the instrumented equals method {@code boolean + * equals(Object, DCompMarker)}. + * + * @param classBuilder for the class + * @param classGen class to add method to + */ + void add_dcomp_interface(ClassBuilder classBuilder, ClassGen24 classGen, ClassInfo classInfo) { + + classGen.addInterface(DCRuntime.instrumentation_interface); + debugInstrument.log("Added interface DCompInstrumented%n"); + + List instructions = new ArrayList<>(); + List localsTable = new ArrayList<>(); + Consumer codeHandler1 = + methodBuilder -> { + methodBuilder.withCode(codeBuilder -> copyCode(codeBuilder, instructions, localsTable)); + }; + + int access_flags = ACC_PUBLIC; + if (classGen.isInterface()) { + access_flags |= ACC_ABSTRACT; + codeHandler1 = methodBuilder -> {}; + } + + // Defining local variables is not strictly necessary, but we do + // it to reduce the diffs with previous versions of DynComp. + localsTable.add(new myLocalVariable(0, "this", ClassDesc.of(classInfo.class_name))); + localsTable.add(new myLocalVariable(1, "obj", CD_Object)); + + MethodTypeDesc mtdNormal, mtdDComp; + mtdNormal = MethodTypeDesc.of(CD_boolean, CD_Object); + + instructions.add(LoadInstruction.of(TypeKind.REFERENCE, 0)); // load this + instructions.add(LoadInstruction.of(TypeKind.REFERENCE, 1)); // load obj + + if (!classInfo.isJunitTestClass) { + instructions.add(ConstantInstruction.ofIntrinsic(ACONST_NULL)); // use null for marker + mtdDComp = MethodTypeDesc.of(CD_boolean, CD_Object, dcomp_marker); + } else { + // for JUnit test class, the instrumented version has no dcomp arg + mtdDComp = mtdNormal; + } + + MethodRefEntry mre = + poolBuilder.methodRefEntry(ClassDesc.of(classGen.getClassName()), "equals", mtdDComp); + instructions.add(InvokeInstruction.of(INVOKEVIRTUAL, mre)); + instructions.add(ReturnInstruction.of(TypeKind.BOOLEAN)); + + if (!classInfo.isJunitTestClass) { + // build the uninstrumented equals_dcomp_instrumented method + classBuilder.withMethod("equals_dcomp_instrumented", mtdNormal, access_flags, codeHandler1); + } + + // now build the instrumented version of the equals_dcomp_instrumented method + if (classGen.isInterface()) { + // no code if interface + classBuilder.withMethod( + "equals_dcomp_instrumented", mtdDComp, access_flags, methodBuilder -> {}); + } else { + @BinaryName String classname = classInfo.class_name; + // create pseudo MethodGen24 + MethodGen24 mgen = + new MethodGen24( + classname, + classBuilder, + "equals_dcomp_instrumented", + access_flags, + mtdNormal, + instructions, + 3, // maxStack + 2); // maxLocals + boolean track = should_track(classname, mgen.getName(), methodEntryName(classname, mgen)); + classBuilder.withMethod( + "equals_dcomp_instrumented", + mtdDComp, + access_flags, + methodBuilder -> + methodBuilder.withCode( + codeBuilder -> + instrumentCode(codeBuilder, null, localsTable, mgen, classInfo, track))); + } + } + + /** + * Adds the following method to a class: + * + *
{@code
+   * public boolean equals (Object obj) {
+   *   return super.equals(obj);
+   * }
+   * }
+ * + * Throws a ClassFormatError if the equals method is already defined in the class. + * + * @param classBuilder for the class + * @param classGen class to add method to + */ + void add_equals_method(ClassBuilder classBuilder, ClassGen24 classGen, ClassInfo classInfo) { + + List instructions = new ArrayList<>(); + List localsTable = new ArrayList<>(); + Consumer codeHandler1 = + methodBuilder -> { + methodBuilder.withCode(codeBuilder -> copyCode(codeBuilder, instructions, localsTable)); + }; + + int access_flags = ACC_PUBLIC; + if (classGen.isInterface()) { + access_flags |= ACC_ABSTRACT; + codeHandler1 = methodBuilder -> {}; + } + + // Defining local variables is not strictly necessary, but we do + // it to reduce the diffs with previous versions of DynComp. + localsTable.add(new myLocalVariable(0, "this", ClassDesc.of(classInfo.class_name))); + localsTable.add(new myLocalVariable(1, "obj", CD_Object)); + + MethodTypeDesc mtdNormal, mtdDComp; + mtdNormal = MethodTypeDesc.of(CD_boolean, CD_Object); + + instructions.add(LoadInstruction.of(TypeKind.REFERENCE, 0)); // load this + instructions.add(LoadInstruction.of(TypeKind.REFERENCE, 1)); // load obj + MethodRefEntry mre = + poolBuilder.methodRefEntry(ClassDesc.of(classGen.getSuperclassName()), "equals", mtdNormal); + instructions.add(InvokeInstruction.of(INVOKESPECIAL, mre)); + instructions.add(ReturnInstruction.of(TypeKind.BOOLEAN)); + + if (!classInfo.isJunitTestClass) { + // build the uninstrumented equals method + classBuilder.withMethod("equals", mtdNormal, access_flags, codeHandler1); + // since not JUnit test class, add dcomp_marker to instrumented version of equals built below + mtdDComp = MethodTypeDesc.of(CD_boolean, CD_Object, dcomp_marker); + } else { + // for JUnit test class, we build only the instrumented version with no dcomp arg + mtdDComp = mtdNormal; + } + + // now build the instrumented version of the equals method + if (classGen.isInterface()) { + // no code if interface + classBuilder.withMethod("equals", mtdDComp, access_flags, methodBuilder -> {}); + } else { + @BinaryName String classname = classInfo.class_name; + // create pseudo MethodGen24 + MethodGen24 mgen = + new MethodGen24( + classname, classBuilder, "equals", access_flags, mtdNormal, instructions, 2, 2); + boolean track = should_track(classname, mgen.getName(), methodEntryName(classname, mgen)); + classBuilder.withMethod( + "equals", + mtdDComp, + access_flags, + methodBuilder -> + methodBuilder.withCode( + codeBuilder -> + instrumentCode(codeBuilder, null, localsTable, mgen, classInfo, track))); + } + } + + /** + * Adds interfaces to indicate which of the Object methods (currently clone and toString) the + * class overrides. Callers will call the instrumented version of the method if it exists, + * otherwise they will call the uninstrumented version. + * + * @param classGen class to check + */ + void add_clone_and_tostring_interfaces(ClassGen24 classGen) { + + @SuppressWarnings("signature:assignment") + @MethodDescriptor String noArgsReturnObject = "()Ljava/lang/Object;"; + MethodModel cl = classGen.containsMethod("clone", noArgsReturnObject); + if (cl != null) { + classGen.addInterface(Signatures.addPackage(dcompRuntimePrefix, "DCompClone")); + } + + @SuppressWarnings("signature:assignment") + @MethodDescriptor String noArgsReturnString = "()Ljava/lang/String;"; + MethodModel ts = classGen.containsMethod("toString", noArgsReturnString); + if (ts != null) { + classGen.addInterface(Signatures.addPackage(dcompRuntimePrefix, "DCompToString")); + } + } + + /** + * Add a dcomp marker parameter to indicate this is the instrumented version of the method. + * + * @param mgen method to add dcomp marker to + * @param minfo for the given method's code + */ + void add_dcomp_param(MethodGen24 mgen, MethodGen24.MInfo24 minfo) { + + // Don't modify main or the JVM won't be able to find it. + if (mgen.isMain()) { + return; + } + + // Don't modify class init methods, they don't take arguments + if (mgen.isClinit()) { + return; + } + + // Add the dcomp marker parameter to indicate this is the + // instrumented version of the method. + StackMapUtils24.addNewSpecialLocal(mgen, minfo, "marker", dcomp_marker, true); + } + + /** + * Returns true if the method is defined in Object. + * + * @param methodName method to check + * @param paramTypes array of parameter types to method + * @return true if method is member of Object + */ + @Pure + boolean is_object_method(@Identifier String methodName, ClassDesc[] paramTypes) { + // Note: kind of weird we don't check that classname = Object but it's been + // that way forever. Just means foo.finialize(), e.g., will be marked uninstrumented. + for (MethodDef md : obj_methods) { + if (md.equals(methodName, paramTypes)) { + return true; + } + } + return false; + } + + /** + * Returns true if the class is one of those that has values initialized by the JVM or native + * methods. + * + * @param classname class to check + * @return true if classname has members that are uninitialized + */ + @Pure + boolean is_class_initialized_by_jvm(String classname) { + + for (String u_name : uninit_classes) { + if (u_name.equals(classname)) { + return true; + } + } + + return false; + } + + /** + * Creates a pseudo "main" method with a DcompMarker parameter that does nothing but call the + * original "main" method without the DCompMarker argument. + * + * @param mgen describes the "main" method + * @param classBuilder for current class + * @param classInfo for the given class + */ + void createMainStub(MethodGen24 mgen, ClassBuilder classBuilder, ClassInfo classInfo) { + List instructions = new ArrayList<>(); + instructions.add(LoadInstruction.of(TypeKind.REFERENCE, 0)); // load arg0 + + MethodRefEntry mre = + poolBuilder.methodRefEntry( + ClassDesc.of(classInfo.class_name), + "main", + MethodTypeDesc.of(CD_void, CD_String.arrayType(1))); + instructions.add(InvokeInstruction.of(INVOKESTATIC, mre)); // call real main + instructions.add(ReturnInstruction.of(TypeKind.VOID)); + + classBuilder.withMethod( + "main", + MethodTypeDesc.of(CD_void, CD_String.arrayType(1), dcomp_marker), + mgen.getAccessFlagsMask(), + methodBuilder -> + methodBuilder.withCode(codeBuilder -> copyCode(codeBuilder, instructions, null))); + } + + /** + * Writes the static map from field names to their integer ids to the specified file. Can be read + * with restore_static_field_id. Each line contains a key/value combination with a blank + * separating them. + * + * @param file where to write the static field ids + * @throws IOException if unable to find or open the file + */ + static void save_static_field_id(File file) throws IOException { + + PrintStream ps = new PrintStream(file, UTF_8); + for (Map.Entry<@KeyFor("static_field_id") String, Integer> entry : static_field_id.entrySet()) { + ps.printf("%s %d%n", entry.getKey(), entry.getValue()); + } + ps.close(); + } + + /** + * Restores the static map from the specified file. + * + * @param file where to read the static field ids + * @throws IOException if unable to create an EntryReader + * @see #save_static_field_id(File) + */ + static void restore_static_field_id(File file) throws IOException { + try (EntryReader er = new EntryReader(file, "UTF-8")) { + for (String line : er) { + String[] key_val = line.split(" *"); + assert !static_field_id.containsKey(key_val[0]) : key_val[0] + " " + key_val[1]; + static_field_id.put(key_val[0], Integer.valueOf(key_val[1])); + } + } + } + + /** + * Returns the fully-qualified fieldname of the specified field. + * + * @param c class containing the field + * @param f the field + * @return string containing the fully-qualified name + */ + protected static String full_name(ClassModel c, FieldModel f) { + return ClassGen24.getClassName(c) + "." + f.fieldName().stringValue(); + } + + /** + * Returns simplified name of a method. Both exceptions and annotations are removed. + * + * @param mgen the method + * @return string containing the simplified method name + */ + protected String simplify_method_name(MethodGen24 mgen) { + // Remove exceptions from the full method name + // String full_name = m.toString().replaceFirst("\\s*throws.*", ""); + // Remove annotations from full method name + // return full_name.replaceFirst("(?s) \\[.*", ""); + return mgen.toString(); + } + + /** Used for processing a switch instruction. */ + private record ModifiedSwitchInfo(Label modifiedTarget, List modifiedCaseList) {} + + /** + * Checks to see if the instruction targets the method's CodeModel startLabel (held in + * oldStartLabel). If so, it replaces the target with the newStartLabel. Unfortunately, the class + * file API does not allow us to simply replace the label, we have to replace the entire + * instruction. Note that oldStartLabel may be null, but that is okay as any comparison to it will + * fail and we will do nothing. + * + * @param inst the instruction to check + * @return the original instruction or its replacement + */ + @RequiresNonNull("newStartLabel") + private CodeElement retargetStartLabel(CodeElement inst) { + ModifiedSwitchInfo info; + switch (inst) { + case BranchInstruction bi -> { + if (bi.target().equals(oldStartLabel)) { + return BranchInstruction.of(bi.opcode(), newStartLabel); + } + } + case ExceptionCatch ec -> { + if (ec.tryStart().equals(oldStartLabel)) { + return ExceptionCatch.of(ec.handler(), newStartLabel, ec.tryEnd(), ec.catchType()); + } + } + case LookupSwitchInstruction ls -> { + info = retargetStartLabel(ls.defaultTarget(), ls.cases()); + if (info != null) { + return LookupSwitchInstruction.of(info.modifiedTarget, info.modifiedCaseList); + } + } + case TableSwitchInstruction ts -> { + info = retargetStartLabel(ts.defaultTarget(), ts.cases()); + if (info != null) { + return TableSwitchInstruction.of( + ts.lowValue(), ts.highValue(), info.modifiedTarget, info.modifiedCaseList); + } + } + default -> {} + } + return inst; + } + + /** + * Checks to see if a switch instruction's default target or any of the case targets refer to + * oldStartLabel. If so, replace those targets with the newStartLabel, and return the result in a + * ModifiedSwitchInfo. Otherwise, return null. + * + * @param defaultTarget the default target for the switch instruction + * @param caseList the case list for the switch instruction + * @return a ModifiedSwitchInfo with the changed values, or null if no changes + */ + @RequiresNonNull("newStartLabel") + private @Nullable ModifiedSwitchInfo retargetStartLabel( + Label defaultTarget, List caseList) { + Label modifiedTarget; + boolean modified = false; + + if (defaultTarget.equals(oldStartLabel)) { + modifiedTarget = newStartLabel; + modified = true; + } else { + modifiedTarget = defaultTarget; + } + + List newCaseList = new ArrayList<>(); + for (SwitchCase item : caseList) { + if (item.target().equals(oldStartLabel)) { + newCaseList.add(SwitchCase.of(item.caseValue(), newStartLabel)); + modified = true; + } else { + newCaseList.add(item); + } + } + + if (modified) { + return new ModifiedSwitchInfo(modifiedTarget, newCaseList); + } else { + return null; + } + } + + /** + * Create a new local variable whose scope is the full method. + * + * @param mgen describes the given method + * @param minfo for the given method's code + * @param localName name of new local variable + * @param localType type of new local variable + * @return the new local variable + */ + protected LocalVariable createLocalWithMethodScope( + MethodGen24 mgen, MethodGen24.MInfo24 minfo, String localName, ClassDesc localType) { + LocalVariable newVar = + LocalVariable.of( + minfo.nextLocalIndex, localName, localType, minfo.startLabel, minfo.endLabel); + mgen.localsTable.add(newVar); + minfo.nextLocalIndex += TypeKind.from(localType).slotSize(); + mgen.setMaxLocals(minfo.nextLocalIndex); + return newVar; + } + + /** + * Build a dup instruction based on item size. + * + * @param size size of item to be duplicated + * @return a DUP or DUP2 instruction + */ + protected CodeElement buildDUPInstruction(int size) { + if (size == 1) { + return StackInstruction.of(DUP); + } else { + return StackInstruction.of(DUP2); + } + } + + /** + * Build a load constant instruction (LDC). Checks the offset of the constant pool element to be + * loaded and generates a LDC or LDC_W, as needed. + * + * @param entry describes the constant pool element to be loaded + * @return a LDC instruction + */ + protected CodeElement buildLDCInstruction(LoadableConstantEntry entry) { + if (entry.index() > 255) { + return ConstantInstruction.ofLoad(LDC_W, entry); + } else { + return ConstantInstruction.ofLoad(LDC, entry); + } + } + + /** + * Build a load constant instruction for values of type int, short, char, byte + * + * @param value to be pushed + * @return a push instruction + */ + protected CodeElement loadIntegerConstant(final int value) { + return switch (value) { + case -1 -> ConstantInstruction.ofIntrinsic(ICONST_M1); + case 0 -> ConstantInstruction.ofIntrinsic(ICONST_0); + case 1 -> ConstantInstruction.ofIntrinsic(ICONST_1); + case 2 -> ConstantInstruction.ofIntrinsic(ICONST_2); + case 3 -> ConstantInstruction.ofIntrinsic(ICONST_3); + case 4 -> ConstantInstruction.ofIntrinsic(ICONST_4); + case 5 -> ConstantInstruction.ofIntrinsic(ICONST_5); + default -> + (value >= Byte.MIN_VALUE && value <= Byte.MAX_VALUE) + ? ConstantInstruction.ofArgument(BIPUSH, value) + : (value >= Short.MIN_VALUE && value <= Short.MAX_VALUE) + ? ConstantInstruction.ofArgument(SIPUSH, value) + : buildLDCInstruction(poolBuilder.intEntry(value)); + }; + } +} diff --git a/java/daikon/dcomp/DynCompError.java b/java/daikon/dcomp/DynCompError.java new file mode 100644 index 0000000000..b620be6c63 --- /dev/null +++ b/java/daikon/dcomp/DynCompError.java @@ -0,0 +1,26 @@ +package daikon.dcomp; + +/** Indicates an unexpected error during DynComp processing. */ +public class DynCompError extends RuntimeException { + + private static final long serialVersionUID = 1L; + + /** + * Create a {@link DynCompError} with the given message. + * + * @param message the exception message + */ + public DynCompError(String message) { + super(message); + } + + /** + * Create a {@link DynCompError} with the given message and cause. + * + * @param message exception message + * @param cause exception cause + */ + public DynCompError(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/java/daikon/dcomp/Instrument.java b/java/daikon/dcomp/Instrument.java index 85789d455a..896c247be3 100644 --- a/java/daikon/dcomp/Instrument.java +++ b/java/daikon/dcomp/Instrument.java @@ -18,19 +18,21 @@ import org.checkerframework.dataflow.qual.Pure; /** - * This class is responsible for modifying another class's bytecodes. Specifically, its main task is - * to add calls into the DynComp Runtime for instrumentation purposes. These added calls are - * sometimes referred to as "hooks". - * - *

This class is loaded by Premain at startup. It is a ClassFileTransformer which means that its - * {@code transform} method gets called each time the JVM loads a class. + * This class is loaded by Premain at startup. It is a {@link ClassFileTransformer} which means that + * its {@link #transform} method gets called each time the JVM loads a class. This method then + * decides if the class should be instrumented or not. If it should, it calls DCInstrument to do the + * instrumentation. */ public class Instrument implements ClassFileTransformer { + // + // Start of diagnostics. + // + /** Directory for debug output. */ final File debug_dir; - /** Directory into which to dump debug-instrumented classes. */ + /** Directory into which to dump instrumented classes. */ final File debug_instrumented_dir; /** Directory into which to dump original classes. */ @@ -45,22 +47,6 @@ public class Instrument implements ClassFileTransformer { */ protected static final SimpleLog debug_transform = new SimpleLog(false); - /** Create an instrumenter. Setup debug directories, if needed. */ - public Instrument() { - debug_transform.enabled = - DynComp.debug || DynComp.debug_transform || Premain.debug_dcinstrument || DynComp.verbose; - daikon.chicory.Instrument.debug_ppt_omit.enabled = DynComp.debug; - - debug_dir = DynComp.debug_dir; - debug_instrumented_dir = new File(debug_dir, "instrumented"); - debug_uninstrumented_dir = new File(debug_dir, "uninstrumented"); - - if (DynComp.dump) { - debug_instrumented_dir.mkdirs(); - debug_uninstrumented_dir.mkdirs(); - } - } - /** Debug code for printing the current run-time call stack. */ public static void print_call_stack() { StackTraceElement[] stack_trace; @@ -74,13 +60,13 @@ public static void print_call_stack() { } /** - * Output a .class file and a .bcel version of the class file. + * Output a .class file and a .bcel version of it. * * @param c the Java class to output * @param directory output location for the files * @param className the current class */ - private void outputDebugFiles(JavaClass c, File directory, @BinaryName String className) { + private void writeDebugClassFiles(JavaClass c, File directory, @BinaryName String className) { try { debug_transform.log("Dumping .class and .bcel for %s to %s%n", className, directory); // Write the byte array to a .class file. @@ -90,8 +76,30 @@ private void outputDebugFiles(JavaClass c, File directory, @BinaryName String cl BcelUtil.dump(c, directory); } catch (Throwable t) { System.err.printf("Error %s writing debug files for: %s%n", t, className); - t.printStackTrace(); - // ignore the error, it shouldn't affect the instrumentation + if (debug_transform.enabled) { + t.printStackTrace(); + } + // Ignore the error, it shouldn't affect the instrumentation. + } + } + + // + // End of diagnostics. + // + + /** Create an instrumenter. Setup debug directories, if needed. */ + public Instrument() { + debug_transform.enabled = + DynComp.debug || DynComp.debug_transform || Premain.debug_dcinstrument || DynComp.verbose; + daikon.chicory.Instrument.debug_ppt_omit.enabled = DynComp.debug; + + debug_dir = DynComp.debug_dir; + debug_instrumented_dir = new File(debug_dir, "instrumented"); + debug_uninstrumented_dir = new File(debug_dir, "uninstrumented"); + + if (DynComp.dump) { + debug_instrumented_dir.mkdirs(); + debug_uninstrumented_dir.mkdirs(); } } @@ -111,39 +119,26 @@ private void outputDebugFiles(JavaClass c, File directory, @BinaryName String cl byte[] classfileBuffer) throws IllegalClassFormatException { - // for debugging + // For debugging. // new Throwable().printStackTrace(); - debug_transform.log("Entering dcomp.Instrument.transform(): class = %s%n", className); - - @BinaryName String binaryClassName = Signatures.internalFormToBinaryName(className); + debug_transform.log("%nEntering dcomp.Instrument.transform(): class = %s%n", className); if (className == null) { - /* - // debug code to display unnamed class - try { - // Parse the bytes of the classfile, die on any errors - ClassParser parser = new ClassParser(new ByteArrayInputStream(classfileBuffer), className); - JavaClass c = parser.parse(); - System.out.println(c.toString()); - } catch (Throwable e) { - System.out.printf("Error: %s%n", e); - e.printStackTrace(); - throw new RuntimeException("Error", e); - } - */ - // most likely a lambda related class + // most likely a lambda-related class return null; } + @BinaryName String binaryClassName = Signatures.internalFormToBinaryName(className); + // See comments in Premain.java about meaning and use of in_shutdown. if (Premain.in_shutdown) { debug_transform.log("Skipping in_shutdown class %s%n", binaryClassName); return null; } - // If already instrumented, nothing to do - // (This set will be empty if Premain.jdk_instrumented is false) + // If already instrumented, there is nothing to do. + // (This set will be empty if Premain.jdk_instrumented is false.) if (Premain.pre_instrumented.contains(className)) { debug_transform.log("Skipping pre_instrumented JDK class %s%n", binaryClassName); return null; @@ -168,11 +163,9 @@ private void outputDebugFiles(JavaClass c, File directory, @BinaryName String cl } } - if (Runtime.isJava9orLater()) { - if (Premain.problem_classes.contains(binaryClassName)) { - debug_transform.log("Skipping problem class %s%n", binaryClassName); - return null; - } + if (Runtime.isJava9orLater() && Premain.problem_classes.contains(binaryClassName)) { + debug_transform.log("Skipping problem class %s%n", binaryClassName); + return null; } if (className.contains("/$Proxy")) { @@ -180,6 +173,11 @@ private void outputDebugFiles(JavaClass c, File directory, @BinaryName String cl return null; } + if (className.startsWith("java/lang/instrument/")) { + debug_transform.log("Skipping java instrumentation class %s%n", binaryClassName); + return null; + } + if (className.equals("java/lang/DCRuntime")) { debug_transform.log("Skipping special DynComp runtime class %s%n", binaryClassName); return null; @@ -189,8 +187,8 @@ private void outputDebugFiles(JavaClass c, File directory, @BinaryName String cl debug_transform.log("Instrumenting JDK class %s%n", binaryClassName); } else { - // We're not in a JDK class - // Don't instrument our own classes + // We're not in a JDK class. + // Don't instrument our own classes. if (is_dcomp(className)) { debug_transform.log("Skipping is_dcomp class %s%n", binaryClassName); return null; @@ -214,11 +212,11 @@ private void outputDebugFiles(JavaClass c, File directory, @BinaryName String cl ClassLoader cfLoader; if (loader == null) { cfLoader = ClassLoader.getSystemClassLoader(); - debug_transform.log("Transforming class %s, loader %s - %s%n", className, loader, cfLoader); + debug_transform.log("Transforming class %s, loaders %s, %s%n", className, loader, cfLoader); } else { cfLoader = loader; debug_transform.log( - "Transforming class %s, loader %s - %s%n", className, loader, loader.getParent()); + "Transforming class %s, loaders %s, %s%n", className, loader, loader.getParent()); } // Parse the bytes of the classfile, die on any errors. @@ -227,36 +225,40 @@ private void outputDebugFiles(JavaClass c, File directory, @BinaryName String cl ClassParser parser = new ClassParser(bais, className); c = parser.parse(); } catch (Throwable t) { - System.err.printf("Error %s while reading %s%n", t, binaryClassName); - t.printStackTrace(); - // No changes to the bytecodes + System.err.printf("Error %s while parsing bytes of %s%n", t, binaryClassName); + if (debug_transform.enabled) { + t.printStackTrace(); + } + // No changes to the bytecodes. return null; } if (DynComp.dump) { - outputDebugFiles(c, debug_uninstrumented_dir, binaryClassName); + writeDebugClassFiles(c, debug_uninstrumented_dir, binaryClassName); } - // Instrument the classfile, die on any errors - JavaClass njc; + // Instrument the classfile, die on any errors. + JavaClass newJavaClass; try { DCInstrument dci = new DCInstrument(c, in_jdk, loader); - njc = dci.instrument(); + newJavaClass = dci.instrument(); } catch (Throwable t) { - RuntimeException re = - new RuntimeException(String.format("Error %s in transform of %s", t, binaryClassName), t); - re.printStackTrace(); - throw re; + System.err.printf("Error %s in transform of %s%n", t, binaryClassName); + if (debug_transform.enabled) { + t.printStackTrace(); + } + // No changes to the bytecodes. + return null; } - if (njc != null) { + if (newJavaClass != null) { if (DynComp.dump) { - outputDebugFiles(njc, debug_instrumented_dir, binaryClassName); + writeDebugClassFiles(newJavaClass, debug_instrumented_dir, binaryClassName); } - return njc.getBytes(); + return newJavaClass.getBytes(); } else { debug_transform.log("Didn't instrument %s%n", binaryClassName); - // No changes to the bytecodes + // No changes to the bytecodes. return null; } } @@ -265,23 +267,26 @@ private void outputDebugFiles(JavaClass c, File directory, @BinaryName String cl * Returns true if the specified class is part of dcomp itself (and thus should not be * instrumented). Some Daikon classes that are used by DynComp are included here as well. * - * @param classname class to be checked - * @return true if classname is a part of DynComp + * @param className class to be checked + * @return true if className is a part of DynComp */ @Pure - private static boolean is_dcomp(String classname) { + private static boolean is_dcomp(@InternalForm String className) { - if (classname.startsWith("daikon/dcomp/") && !classname.startsWith("daikon/dcomp/DcompTest")) { + if (className.startsWith("daikon/dcomp/") && !className.startsWith("daikon/dcomp/DcompTest")) { + return true; + } + if (className.startsWith("daikon/chicory/") + && !className.equals("daikon/chicory/ChicoryTest")) { return true; } - if (classname.startsWith("daikon/chicory/") - && !classname.equals("daikon/chicory/ChicoryTest")) { + if (className.equals("daikon/Chicory")) { return true; } - if (classname.equals("daikon/PptTopLevel$PptType")) { + if (className.equals("daikon/PptTopLevel$PptType")) { return true; } - if (classname.startsWith("daikon/plumelib")) { + if (className.startsWith("daikon/plumelib")) { return true; } return false; @@ -291,25 +296,25 @@ private static boolean is_dcomp(String classname) { * Returns true if the specified class is part of a tool known to do Java byte code * transformation. We need to warn the user that this may not work correctly. * - * @param classname class to be checked - * @return true if classname is a known transformer + * @param className class to be checked + * @return true if className is a known transformer */ @Pure - protected static boolean is_transformer(String classname) { + protected static boolean is_transformer(@InternalForm String className) { - if (classname.startsWith("org/codehaus/groovy")) { + if (className.startsWith("org/codehaus/groovy")) { return true; } - if (classname.startsWith("groovy/lang")) { + if (className.startsWith("groovy/lang")) { return true; } - if (classname.startsWith("org/mockito")) { + if (className.startsWith("org/mockito")) { return true; } - if (classname.startsWith("org/objenesis")) { + if (className.startsWith("org/objenesis")) { return true; } - if (classname.contains("ByMockito")) { + if (className.contains("ByMockito")) { return true; } return false; diff --git a/java/daikon/dcomp/Instrument24.java b/java/daikon/dcomp/Instrument24.java new file mode 100644 index 0000000000..b022547aff --- /dev/null +++ b/java/daikon/dcomp/Instrument24.java @@ -0,0 +1,375 @@ +package daikon.dcomp; + +import daikon.DynComp; +import daikon.chicory.ClassInfo; +import daikon.chicory.Runtime; +import daikon.plumelib.bcelutil.BcelUtil; +import daikon.plumelib.bcelutil.SimpleLog; +import daikon.plumelib.reflection.Signatures; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.lang.classfile.ClassFile; +import java.lang.classfile.ClassHierarchyResolver; +import java.lang.classfile.ClassModel; +import java.lang.classfile.ClassTransform; +import java.lang.instrument.ClassFileTransformer; +import java.lang.instrument.IllegalClassFormatException; +import java.nio.file.Files; +import java.security.ProtectionDomain; +import org.apache.bcel.classfile.ClassParser; +import org.apache.bcel.classfile.JavaClass; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.checker.signature.qual.BinaryName; +import org.checkerframework.checker.signature.qual.DotSeparatedIdentifiers; +import org.checkerframework.checker.signature.qual.InternalForm; +import org.checkerframework.dataflow.qual.Pure; + +/** + * This class is loaded by Premain at startup. It is a {@link ClassFileTransformer} which means that + * its {@link #transform} method gets called each time the JVM loads a class. This method then + * decides if the class should be instrumented or not. If it should, it calls DCInstrument24 to do + * the instrumentation. + * + *

Instrument24 and DCInstrument24 use Java's ({@code java.lang.classfile}) APIs for reading and + * modifying .class files. Those APIs were added in JDK 24. Compared to BCEL, these APIs are more + * complete and robust (no more fiddling with StackMaps) and are always up to date with any .class + * file changes (since they are part of the JDK). (We will need to continue to support + * Instrument.java using BCEL, as we anticipate our clients using JDK 21 or less for quite some + * time.) + */ +public class Instrument24 implements ClassFileTransformer { + + // + // Start of diagnostics. + // + + /** Directory for debug output. */ + final File debug_dir; + + /** Directory into which to dump instrumented classes. */ + final File debug_instrumented_dir; + + /** Directory into which to dump original classes. */ + final File debug_uninstrumented_dir; + + /** Have we seen a class member of a known transformer? */ + private static boolean transformer_seen = false; + + /** + * Debug information about which classes and/or methods are transformed and why. Use + * debugInstrument for actual instrumentation details. + */ + protected static final SimpleLog debug_transform = new SimpleLog(false); + + /** Debug code for printing the current run-time call stack. */ + public static void print_call_stack() { + StackTraceElement[] stack_trace; + stack_trace = Thread.currentThread().getStackTrace(); + // [0] is getStackTrace + // [1] is print_call_stack + for (int i = 2; i < stack_trace.length; i++) { + System.out.printf("call stack: %s%n", stack_trace[i]); + } + System.out.println(); + } + + /** + * Output a .class file and a .bcel version of it. + * + * @param classBytes a byte array of the class file to output + * @param directory output location for the files + * @param className the current class + */ + public void writeDebugClassFiles( + byte[] classBytes, File directory, @BinaryName String className) { + // UNDONE: Should we stop using bcel and use classModel.toDebugString() instead? + // Convert the classBytes to a BCEL JavaClass + JavaClass c = null; + try (ByteArrayInputStream bais = new ByteArrayInputStream(classBytes)) { + ClassParser parser = new ClassParser(bais, className); + c = parser.parse(); + } catch (Throwable t) { + System.err.printf("Error %s while parsing the bytes of %s%n", t, className); + if (debug_transform.enabled) { + t.printStackTrace(); + } + // Ignore the error, it shouldn't affect the instrumentation. + } + + try { + debug_transform.log("Dumping .class and .bcel for %s to %s%n", className, directory); + // Write the byte array to a .class file. + File outputFile = new File(directory, className + ".class"); + Files.write(outputFile.toPath(), classBytes); + // Write a BCEL-like file. + if (c != null) { + BcelUtil.dump(c, directory); + } + } catch (Throwable t) { + System.err.printf("Error %s writing debug files for: %s%n", t, className); + if (debug_transform.enabled) { + t.printStackTrace(); + } + // Ignore the error, it shouldn't affect the instrumentation. + } + } + + // + // End of diagnostics. + // + + /** Create an instrumenter. Setup debug directories, if needed. */ + public Instrument24() { + debug_transform.enabled = + DynComp.debug || DynComp.debug_transform || Premain.debug_dcinstrument || DynComp.verbose; + daikon.chicory.Instrument24.debug_ppt_omit.enabled = DynComp.debug; + + debug_dir = DynComp.debug_dir; + debug_instrumented_dir = new File(debug_dir, "instrumented"); + debug_uninstrumented_dir = new File(debug_dir, "uninstrumented"); + + if (DynComp.dump) { + debug_instrumented_dir.mkdirs(); + debug_uninstrumented_dir.mkdirs(); + } + } + + /** + * Given a class, return a transformed version of the class that contains instrumentation code. + * Because DynComp is invoked as a javaagent, the transform method is called by the Java runtime + * each time a new class is loaded. A return value of null leaves the byte codes unchanged. + * + *

{@inheritDoc} + */ + @Override + public byte @Nullable [] transform( + @Nullable ClassLoader loader, + @InternalForm String className, + @Nullable Class classBeingRedefined, + ProtectionDomain protectionDomain, + byte[] classfileBuffer) + throws IllegalClassFormatException { + + // For debugging. + // new Throwable().printStackTrace(); + + debug_transform.log("%nEntering dcomp.Instrument24.transform(): class = %s%n", className); + + if (className == null) { + // most likely a lambda-related class + return null; + } + + @BinaryName String binaryClassName = Signatures.internalFormToBinaryName(className); + @DotSeparatedIdentifiers String dcompPrefix; + + // See comments in Premain.java about meaning and use of in_shutdown. + if (Premain.in_shutdown) { + debug_transform.log("Skipping in_shutdown class %s%n", binaryClassName); + return null; + } + + // If already instrumented, there is nothing to do. + // (This set will be empty if Premain.jdk_instrumented is false.) + if (Premain.pre_instrumented.contains(className)) { + debug_transform.log("Skipping pre_instrumented JDK class %s%n", binaryClassName); + return null; + } + + boolean in_jdk = false; + + // Check if class is in JDK + if (BcelUtil.inJdkInternalform(className)) { + // If we are not using an instrumented JDK, then skip this class. + if (!Premain.jdk_instrumented) { + debug_transform.log("Skipping JDK class %s%n", binaryClassName); + return null; + } + + int lastSlashPos = className.lastIndexOf('/'); + if (lastSlashPos > 0) { + String packageName = className.substring(0, lastSlashPos).replace('/', '.'); + if (Premain.problem_packages.contains(packageName)) { + debug_transform.log("Skipping problem package %s%n", packageName); + return null; + } + } + + if (Runtime.isJava9orLater() && Premain.problem_classes.contains(binaryClassName)) { + debug_transform.log("Skipping problem class %s%n", binaryClassName); + return null; + } + + if (className.contains("/$Proxy")) { + debug_transform.log("Skipping proxy class %s%n", binaryClassName); + return null; + } + + if (className.startsWith("java/lang/instrument/")) { + debug_transform.log("Skipping java instrumentation class %s%n", binaryClassName); + return null; + } + + if (className.equals("java/lang/DCRuntime")) { + debug_transform.log("Skipping special DynComp runtime class %s%n", binaryClassName); + return null; + } + + in_jdk = true; + debug_transform.log("Instrumenting JDK class %s%n", binaryClassName); + } else { + + // We're not in a JDK class. + // Don't instrument our own classes. + if (is_dcomp(className)) { + debug_transform.log("Skipping is_dcomp class %s%n", binaryClassName); + return null; + } + + // Don't instrument other byte code transformers + if (is_transformer(className)) { + debug_transform.log("Skipping is_transformer class %s%n", binaryClassName); + if (!transformer_seen) { + transformer_seen = true; + System.err.printf( + "DynComp warning: This program uses a Java byte code transformer: %s%n", + binaryClassName); + System.err.printf( + "This may interfere with the DynComp transformer and cause DynComp to fail.%n"); + } + return null; + } + } + + ClassLoader cfLoader; + if (loader == null) { + cfLoader = ClassLoader.getSystemClassLoader(); + debug_transform.log("Transforming class %s, loaders %s, %s%n", className, loader, cfLoader); + } else { + cfLoader = loader; + debug_transform.log( + "Transforming class %s, loaders %s, %s%n", className, loader, loader.getParent()); + } + + // Parse the bytes of the classfile, die on any errors. + ClassFile classFile = + ClassFile.of( + ClassFile.ClassHierarchyResolverOption.of( + ClassHierarchyResolver.ofResourceParsing(cfLoader))); + + ClassModel classModel; + try { + classModel = classFile.parse(classfileBuffer); + } catch (Throwable t) { + System.err.printf("Error %s while parsing bytes of %s%n", t, binaryClassName); + if (debug_transform.enabled) { + t.printStackTrace(); + } + // No changes to the bytecodes. + return null; + } + + if (DynComp.dump) { + try { + writeDebugClassFiles( + classFile.transformClass(classModel, ClassTransform.ACCEPT_ALL), + debug_uninstrumented_dir, + binaryClassName); + } catch (Throwable t) { + debug_transform.log("Failed to dump uninstrumented class %s: %s%n", binaryClassName, t); + } + } + + // As `instrumentation_interface` is a static field, we initialize it here rather than + // in the DCInstrument24 constructor. + if (Premain.jdk_instrumented && Runtime.isJava9orLater()) { + dcompPrefix = "java.lang"; + } else { + dcompPrefix = "daikon.dcomp"; + } + DCRuntime.instrumentation_interface = Signatures.addPackage(dcompPrefix, "DCompInstrumented"); + + // Instrument the classfile, die on any errors. + ClassInfo classInfo = new ClassInfo(binaryClassName, cfLoader); + DCInstrument24 dci = new DCInstrument24(classFile, classModel, in_jdk); + byte @Nullable [] newBytes; + try { + newBytes = dci.instrument(classInfo); + } catch (Throwable t) { + System.err.printf("Error %s in transform of %s%n", t, binaryClassName); + if (debug_transform.enabled) { + t.printStackTrace(); + } + // No changes to the bytecodes. + return null; + } + + if (newBytes != null) { + if (DynComp.dump) { + writeDebugClassFiles(newBytes, debug_instrumented_dir, binaryClassName); + } + return newBytes; + } else { + debug_transform.log("Didn't instrument %s%n", binaryClassName); + // No changes to the bytecodes. + return null; + } + } + + /** + * Returns true if the specified class is part of dcomp itself (and thus should not be + * instrumented). Some Daikon classes that are used by DynComp are included here as well. + * + * @param className class to be checked + * @return true if className is a part of DynComp + */ + @Pure + private static boolean is_dcomp(@InternalForm String className) { + + if (className.startsWith("daikon/dcomp/") && !className.startsWith("daikon/dcomp/DcompTest")) { + return true; + } + if (className.startsWith("daikon/chicory/") + && !className.equals("daikon/chicory/ChicoryTest")) { + return true; + } + if (className.equals("daikon/Chicory")) { + return true; + } + if (className.equals("daikon/PptTopLevel$PptType")) { + return true; + } + if (className.startsWith("daikon/plumelib")) { + return true; + } + return false; + } + + /** + * Returns true if the specified class is part of a tool known to do Java byte code + * transformation. We need to warn the user that this may not work correctly. + * + * @param className class to be checked + * @return true if className is a known transformer + */ + @Pure + protected static boolean is_transformer(@InternalForm String className) { + + if (className.startsWith("org/codehaus/groovy")) { + return true; + } + if (className.startsWith("groovy/lang")) { + return true; + } + if (className.startsWith("org/mockito")) { + return true; + } + if (className.startsWith("org/objenesis")) { + return true; + } + if (className.contains("ByMockito")) { + return true; + } + return false; + } +} diff --git a/java/daikon/dcomp/OperandStack24.java b/java/daikon/dcomp/OperandStack24.java new file mode 100644 index 0000000000..54a21ebcce --- /dev/null +++ b/java/daikon/dcomp/OperandStack24.java @@ -0,0 +1,294 @@ +// This is a modified version of BCEL's OperandStack. See below for details. + +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package daikon.dcomp; + +import java.lang.classfile.TypeKind; +import java.lang.constant.ClassDesc; +import java.util.ArrayList; +import org.checkerframework.checker.index.qual.NonNegative; +import org.checkerframework.checker.lock.qual.GuardSatisfied; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.qual.Pure; + +/** + * This class implements a stack used for symbolic JVM stack simulation. (It's used as an operand + * stack substitute.) Elements of this stack are {@link ClassDesc} objects. + * + *

This is a modified version of BCEL's OperandStack. We assume that the class file has been + * previously verified, so we check very few error conditions. + */ +public class OperandStack24 implements Cloneable { + + /** The underlying stack delegate. */ + private ArrayList stack = new ArrayList<>(); + + /** The maximum number of stack slots this OperandStack instance may hold. */ + private final @NonNegative int maxStack; + + /** + * Creates an empty stack with a maximum of maxStack items. Note that this might be larger than + * necessary as a method's maxStack is the maximum number of stack slots used. This could be + * larger than the number of stack operands if any are of type {@code long} or {@code double}. + */ + public OperandStack24(final @NonNegative int maxStack) { + this.maxStack = maxStack; + } + + /** Clears the stack. */ + public void clear() { + stack.clear(); + } + + /** + * Returns a copy of this object. The clone contains a new stack. However, the ClassDesc objects + * on the stack are shared. + */ + @Override + public OperandStack24 clone(@GuardSatisfied OperandStack24 this) { + final OperandStack24 newstack; + try { + newstack = (OperandStack24) super.clone(); + } catch (CloneNotSupportedException e) { + throw new DynCompError("Error: ", e); + } + newstack.stack = new ArrayList<>(this.stack); + return newstack; + } + + /** + * Returns true if and only if this OperandStack equals another, meaning equal lengths and equal + * objects on the stacks. This method is used to verify that the operand stacks match when two + * execution paths meet. A special case is {@code null} on an operand stack, which matches + * anything but a primitive. As we are assuming that the class file is valid, then it is okay for + * an object to exist on one execution path and be null on the other. + */ + @Override + public boolean equals(@GuardSatisfied OperandStack24 this, @GuardSatisfied @Nullable Object o) { + if (this == o) { + return true; + } + if (!(o instanceof OperandStack24)) { + return false; + } + final OperandStack24 other = (OperandStack24) o; + if (this.stack.size() != other.stack.size()) { + return false; + } + for (int i = 0; i < this.stack.size(); i++) { + ClassDesc thisItem = this.stack.get(i); + ClassDesc otherItem = other.stack.get(i); + if (!compareOperandStackElements(thisItem, otherItem)) { + return false; + } + } + return true; + } + + /** + * Returns true if and only if the two OperandStack elements are equal. An element may be one of + * four possible items: + * + *

    + *
  • {@code null} a special case as it matches anything but a primitive + *
  • a primitive (such as {@code int}, {@code long}, etc.) + *
  • an array reference + *
  • an object reference + *
+ * + * @param thisItem one OperandStack element + * @param otherItem another OperandStack element + * @return true if and only if the items match + */ + protected boolean compareOperandStackElements( + @GuardSatisfied OperandStack24 this, + @Nullable ClassDesc thisItem, + @Nullable ClassDesc otherItem) { + if (thisItem == null) { + if (otherItem != null && otherItem.isPrimitive()) { + return false; + } + // We assume null matches any array or any class. + return true; + } else if (otherItem == null) { + // we know thisItem != null + if (thisItem.isPrimitive()) { + return false; + } + // We assume null matches any array or any class. + return true; + } else if (thisItem.isArray()) { + if (otherItem.isPrimitive()) { + return false; + } + // We assume an array matches any array or any class as they all have Object as a superclass + // at some point. + return true; + } else if (thisItem.isClassOrInterface()) { + if (otherItem.isPrimitive()) { + return false; + } + // We assume a class matches any array or any class as they all have Object as a superclass + // at some point. + return true; + } else if (thisItem.isPrimitive() && !otherItem.isPrimitive()) { + return false; + } + // Both operands are primitives - they better match. + if (thisItem.equals(otherItem)) { + return true; + } else { + throw new DynCompError( + "Operand stack primitives don't match: " + thisItem + ", " + otherItem); + } + } + + /** + * Returns a (typed!) clone of this. + * + * @see #clone() + */ + public OperandStack24 getClone() { + return this.clone(); + } + + /** + * @return a hash code value for the object + */ + @Override + public int hashCode(@GuardSatisfied OperandStack24 this) { + int result = 1; + for (ClassDesc item : stack) { + int elementHash; + if (item != null && item.isPrimitive()) { + // Primitives must match exactly in equals, so use their own hash code. + elementHash = item.hashCode(); + } else { + // `null`, arrays, and classes are all treated as equivalent in equals. + elementHash = 2; + } + result = 31 * result + elementHash; + } + return result; + } + + /** Returns true IFF this OperandStack is empty. */ + public boolean isEmpty() { + return stack.isEmpty(); + } + + /** Returns the number of stack slots this stack can hold. */ + @Pure + public @NonNegative int maxStack() { + return this.maxStack; + } + + /** Returns the element on top of the stack. The element is not popped off the stack! */ + @Pure + public ClassDesc peek() { + return peek(0); + } + + /** + * Returns the element that's i elements below the top element; that means, iff i==0 the top + * element is returned. The element is not popped off the stack! + */ + @Pure + public ClassDesc peek(@GuardSatisfied OperandStack24 this, final int i) { + return stack.get(size() - i - 1); + } + + /** Returns the element on top of the stack. The element is popped off the stack. */ + public ClassDesc pop() { + return stack.remove(size() - 1); + } + + /** + * Pops {@code count} elements off the stack. Always returns {@code null}. + * + * @return {@code null} + */ + public @Nullable ClassDesc pop(final int count) { + for (int j = 0; j < count; j++) { + pop(); + } + return null; + } + + /** Pushes a ClassDesc object onto the stack. */ + public void push(final ClassDesc type) { + if (slotsUsed() + slotSize(type) > maxStack) { + throw new DynCompError("Operand stack size exceeded: " + stack); + } + stack.add(type); + } + + /** + * Returns the size of this OperandStack; that means, how many ClassDesc objects it currently + * contains. + */ + @Pure + public @NonNegative int size(@GuardSatisfied OperandStack24 this) { + return stack.size(); + } + + /** + * Returns the total number of stack slots used. This could be larger than the total number of + * stack operands if any are of type {@code long} or {@code double}. + * + * @see #maxStack() + */ + public @NonNegative int slotsUsed(@GuardSatisfied OperandStack24 this) { + int slots = 0; + for (ClassDesc item : stack) { + slots += slotSize(item); + } + return slots; + } + + /** Returns a String representation of this OperandStack instance. */ + @Override + public String toString(@GuardSatisfied OperandStack24 this) { + final StringBuilder sb = new StringBuilder(60); + sb.append("Slots used: ") + .append(slotsUsed()) + .append(" MaxStack: ") + .append(maxStack) + .append('\n'); + for (int i = 0; i < size(); i++) { + sb.append(peek(i)).append(" (Size: ").append(String.valueOf(slotSize(peek(i)))).append(")\n"); + } + return sb.toString(); + } + + /** + * Calculate the size of an item on the operand stack. This is 2 for {@code long} or {@code + * double}, 1 for everything else. + * + * @param item type to get size of + * @return size of item + */ + int slotSize(@GuardSatisfied OperandStack24 this, ClassDesc item) { + if (item == null) { + return 1; + } else { + return TypeKind.from(item).slotSize(); + } + } +} diff --git a/java/daikon/dcomp/Premain.java b/java/daikon/dcomp/Premain.java index 8aa1fd3e87..fb2aedbad8 100644 --- a/java/daikon/dcomp/Premain.java +++ b/java/daikon/dcomp/Premain.java @@ -200,14 +200,12 @@ public static void premain(String agentArgs, Instrumentation inst) throws IOExce Thread shutdown_thread = new ShutdownThread(); java.lang.Runtime.getRuntime().addShutdownHook(shutdown_thread); - // UNDONE: turn on Instrument24 - String instrumenter = "daikon.dcomp.Instrument"; - // String instrumenter; - // if (daikon.chicory.Runtime.isJava24orLater()) { - // instrumenter = "daikon.dcomp.Instrument24"; - // } else { - // instrumenter = "daikon.dcomp.Instrument"; - // } + String instrumenter; + if (daikon.chicory.Runtime.isJava24orLater()) { + instrumenter = "daikon.dcomp.Instrument24"; + } else { + instrumenter = "daikon.dcomp.Instrument"; + } // Setup the transformer ClassFileTransformer transformer; diff --git a/java/daikon/dcomp/StackMapUtils24.java b/java/daikon/dcomp/StackMapUtils24.java new file mode 100644 index 0000000000..6bb186c6b5 --- /dev/null +++ b/java/daikon/dcomp/StackMapUtils24.java @@ -0,0 +1,191 @@ +package daikon.dcomp; + +import daikon.chicory.MethodGen24; +import daikon.plumelib.util.ArraysPlume; +import java.lang.classfile.CodeElement; +import java.lang.classfile.TypeKind; +import java.lang.classfile.instruction.DiscontinuedInstruction; +import java.lang.classfile.instruction.IncrementInstruction; +import java.lang.classfile.instruction.LoadInstruction; +import java.lang.classfile.instruction.LocalVariable; +import java.lang.classfile.instruction.StoreInstruction; +import java.lang.constant.ClassDesc; +import java.util.List; +import java.util.ListIterator; +import org.checkerframework.checker.signature.qual.Identifier; + +/** + * This class provides static methods for manipulating bytecode structures, including operations on + * local variables, parameter types, and instruction adjustments. It is loosely based on + * StackMapUtils.java located in the plume-lib/bcel-util repository. It implements a very small + * subset of the methods in StackMapUtils and does no manipulation of StackMaps at all. Its primary + * method is {@link #addNewSpecialLocal} which is a replacement for the two methods addNewParameter + * and create_method_scope_local in the original StackMapUtils. + * + *

StackMapUtils24 uses Java's {@code java.lang.classfile} APIs for reading and modifying .class + * files. Those APIs were added in JDK 24. Compared to BCEL, these APIs are more complete and robust + * and are always up to date with any .class file changes (since they are part of the JDK). + */ +public final class StackMapUtils24 { + + /** Do not instantiate. */ + private StackMapUtils24() { + throw new Error("Do not instantiate"); + } + + /* + * NOMENCLATURE + * + * 'index' is an item's subscript into a data structure. + * + * 'offset' is used to describe two different address types: + * * the offset of a byte code from the start of a method's byte codes + * * the offset of a variable from the start of a method's stack frame + * + * The Java Virtual Machine Specification uses 'index into the local variable + * array of the current frame' or 'slot number' to describe this second case. + */ + + /** + * Add {@code size} (1 or 2) to the slot number of each Instruction that references a local that + * is equal or higher in the local map than {@code offsetFirstLocalToBeMoved}. + * + * @param mgen MethodGen to be modified + * @param offsetFirstLocalToBeMoved original offset of first local moved "up" + * @param size the size (1 or 2) of of the new local that was just inserted at {@code + * offsetFirstLocalToBeMoved} + */ + static void adjustCodeForLocalsChange(MethodGen24 mgen, int offsetFirstLocalToBeMoved, int size) { + + DCInstrument24.debugInstrument.log( + "adjustCodeForLocalsChange: %d %d%n", offsetFirstLocalToBeMoved, size); + try { + List il = mgen.getInstructionList(); + ListIterator iter = il.listIterator(); + while (iter.hasNext()) { + + CodeElement inst = iter.next(); + switch (inst) { + case DiscontinuedInstruction.RetInstruction ret -> { + if (ret.slot() >= offsetFirstLocalToBeMoved) { + iter.set(DiscontinuedInstruction.RetInstruction.of(ret.slot() + size)); + } + } + case IncrementInstruction inc -> { + if (inc.slot() >= offsetFirstLocalToBeMoved) { + iter.set(IncrementInstruction.of(inc.slot() + size, inc.constant())); + } + } + case LoadInstruction load -> { + if (load.slot() >= offsetFirstLocalToBeMoved) { + iter.set(LoadInstruction.of(load.typeKind(), load.slot() + size)); + } + } + case StoreInstruction store -> { + if (store.slot() >= offsetFirstLocalToBeMoved) { + iter.set(StoreInstruction.of(store.typeKind(), store.slot() + size)); + } + } + default -> {} // No other instructions reference local variables. + } + } + } catch (Throwable t) { + if (DCInstrument24.debugInstrument.enabled) { + t.printStackTrace(); + } + throw new DynCompError("Unexpected exception encountered in adjustCodeForLocalsChange", t); + } + } + + /** + * Add a new local variable to the method. This will be inserted after any parameters and before + * any existing local variables. If there are existing local variables this will have the side + * effect of rewritting the method byte codes to adjust the slot numbers for the existing local + * variables. + * + *

DCInstrument24 uses this routine for two special variables: + * + *

    + *
  1. the dcomp marker - added as a parameter + *
  2. the tag frame array - added as a local + *
+ * + *

Must call {@link MethodGen24#fixLocals} before calling this routine. + * + * @param mgen MethodGen to be modified + * @param minfo for the given method's code + * @param varName name of new parameter + * @param varType type of new parameter + * @param isParam if true, the new local is a new parameter (the DCompMarker) + * @return a LocalVariable for the new variable + */ + public static LocalVariable addNewSpecialLocal( + MethodGen24 mgen, + MethodGen24.MInfo24 minfo, + @Identifier String varName, + ClassDesc varType, + boolean isParam) { + + LocalVariable varNew; + // get a copy of the locals before modification + List locals = mgen.localsTable; + ClassDesc[] paramTypes = mgen.getParameterTypes(); + int newIndex = 0; // index into `locals` + int newOffset = 0; // current local slot number + + int argSize = TypeKind.from(varType).slotSize(); + + if (!mgen.isStatic()) { + // Skip the 'this' pointer. + newIndex++; + newOffset++; // size of 'this' is 1 + } + + if (paramTypes.length > 0) { + LocalVariable lastArg; + newIndex = newIndex + paramTypes.length; + // `newIndex` is now strictly positive, because `paramTypes.length` is. + lastArg = locals.get(newIndex - 1); + newOffset = lastArg.slot() + TypeKind.from(lastArg.typeSymbol()).slotSize(); + } + + // Insert our new local variable into existing table at `newOffset`. + varNew = LocalVariable.of(newOffset, varName, varType, minfo.startLabel, minfo.endLabel); + mgen.localsTable.add(newIndex, varNew); + minfo.nextLocalIndex += argSize; + mgen.setMaxLocals(minfo.nextLocalIndex); + + if (isParam) { + // Update the method's parameter information. + paramTypes = ArraysPlume.append(paramTypes, varType); + @Identifier String[] paramNames = + ArraysPlume.<@Identifier String>append(mgen.getParameterNames(), varName); + mgen.setParameterTypes(paramTypes); + mgen.setParameterNames(paramNames); + } + + DCInstrument24.debugInstrument.log( + "Added a %s at slot %s.%n name: %s type: %s size: %s%n", + isParam ? "parameter" : "local", varNew.slot(), varNew.name(), varNew.type(), argSize); + + boolean hasCode = !mgen.getInstructionList().isEmpty(); + if (hasCode) { + // Adjust the offset of any locals after our insertion. + for (int i = newIndex + 1; i < locals.size(); i++) { + LocalVariable lv = locals.get(i); + locals.set( + i, + LocalVariable.of( + lv.slot() + argSize, lv.name(), lv.type(), lv.startScope(), lv.endScope())); + } + + // Now process the instruction list, adding 1 or 2 to the offset within each + // LocalVariableInstruction that references a local that is 'higher' in the + // local map than the new local we just inserted. + adjustCodeForLocalsChange(mgen, newOffset, argSize); + + // DCInstrument24.debugInstrument.log("New LocalVariableTable:%n%s%n", mgen.localsTable); + } + return varNew; + } +} diff --git a/java/daikon/suppress/NIS.java b/java/daikon/suppress/NIS.java index 3583da8adc..599d273e06 100644 --- a/java/daikon/suppress/NIS.java +++ b/java/daikon/suppress/NIS.java @@ -217,7 +217,7 @@ public static void init_ni_suppression() { suppressor_proto_invs = new ArrayList<@Prototype Invariant>(); // This should be the first statement in the method, but put it after the - // field initalizations so that the Initialization Checker doesn't complain. + // field initializations so that the Initialization Checker doesn't complain. if (!dkconfig_enabled) { return; } diff --git a/java/daikon/test/split/SplitterFactoryTestUpdater.java b/java/daikon/test/split/SplitterFactoryTestUpdater.java index ab9dd70a60..0c806f6071 100644 --- a/java/daikon/test/split/SplitterFactoryTestUpdater.java +++ b/java/daikon/test/split/SplitterFactoryTestUpdater.java @@ -1,7 +1,10 @@ package daikon.test.split; -import daikon.*; -import daikon.split.*; +import daikon.Daikon; +import daikon.FileIO; +import daikon.PptMap; +import daikon.split.PptSplitter; +import daikon.split.SplitterFactory; import java.io.BufferedWriter; import java.io.ByteArrayOutputStream; import java.io.File; diff --git a/java/lib/README b/java/lib/README index 070cf5097f..0042d501a1 100644 --- a/java/lib/README +++ b/java/lib/README @@ -45,8 +45,8 @@ daikon-plumelib.jar : Contains bcel-util, options, hashmap-util, plume-util, and reflection-util, but in package "daikon.plumelib". # Note that you must be using JDK17+ and gradle 8.11+. # First, adjust version numbers in file `build.gradle`. (TODO: make that unnecessary.) - gradle shadowjar - mv -f build/libs/daikon-plumelib.jar . + gradle shadowjar && \ + mv -f build/libs/daikon-plumelib.jar . && \ rm -rf build error-prone/ : https://repo1.maven.org/maven2/com/google/errorprone/error_prone_core/ diff --git a/java/lib/build.gradle b/java/lib/build.gradle index 74ccc54273..dc2057801d 100644 --- a/java/lib/build.gradle +++ b/java/lib/build.gradle @@ -1,6 +1,6 @@ plugins { id("java") - id("com.gradleup.shadow").version("9.2.2") + id("com.gradleup.shadow").version("9.3.1") } repositories { diff --git a/java/lib/commons-io-2.19.0.jar b/java/lib/commons-io-2.19.0.jar new file mode 100644 index 0000000000..38e7fd20a4 Binary files /dev/null and b/java/lib/commons-io-2.19.0.jar differ diff --git a/tests/daikon-tests/ncalc/README b/tests/daikon-tests/ncalc/README index 9930b1986b..567d232e3e 100644 --- a/tests/daikon-tests/ncalc/README +++ b/tests/daikon-tests/ncalc/README @@ -23,4 +23,4 @@ For Daikon testing we do this by adding the line: export LD_LIBRARY_PATH=$(PROJECT) && \ in tests/Makefile.common. This is inserted prior to every use of DynComp or Chicory. Note that this means it is set for every test case, but that should not be a problem as no other -test uses 'loadLibary'. +test uses 'loadLibrary'.