diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 519a02d8..b909fd40 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,6 +4,7 @@ jagr = "org.sourcegrade:jagr-launcher:0.10.3" mockito = "org.mockito:mockito-core:5.14.1" junit = "org.junit.jupiter:junit-jupiter:5.11.2" asm = "org.ow2.asm:asm:9.7.1" +asmTree = "org.ow2.asm:asm-tree:9.7.1" dokkaKotlinAsJavaPlugin = "org.jetbrains.dokka:kotlin-as-java-plugin:1.9.20" dokkaBase = "org.jetbrains.dokka:dokka-base:1.9.20" [plugins] diff --git a/student/src/main/java/org/tudalgo/algoutils/student/annotation/ForceSignature.java b/student/src/main/java/org/tudalgo/algoutils/student/annotation/ForceSignature.java new file mode 100644 index 00000000..38b6aff7 --- /dev/null +++ b/student/src/main/java/org/tudalgo/algoutils/student/annotation/ForceSignature.java @@ -0,0 +1,73 @@ +package org.tudalgo.algoutils.student.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Forces the annotated type or member to be mapped to the specified one. + * Mappings must be 1:1, meaning multiple annotated members (or types) may not map to the same target and + * all members and types may be targeted by at most one annotation. + * + * @author Daniel Mangold + */ +@Target({ElementType.TYPE, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.METHOD}) +@Retention(RetentionPolicy.CLASS) +public @interface ForceSignature { + + /** + * The identifier of the annotated type / member. + * The value must be as follows: + * + * + * @return the type / member identifier + */ + String identifier(); + + /** + * The method descriptor as specified by + * Chapter 4.3 + * of the Java Virtual Machine Specification. + * If a value is set, it takes precedence over {@link #returnType()} and {@link #parameterTypes()}. + * + *

+ * Note: Setting this value has no effect for types or fields. + *

+ * + * @return the method's descriptor + */ + String descriptor() default ""; + + /** + * The class object specifying the method's return type. + * If a value is set, it will be overwritten if {@link #descriptor()} is also set. + * Default is no return type (void). + * + *

+ * Note: Setting this value has no effect for types or fields. + *

+ * + * @return the method's return type + */ + Class returnType() default void.class; + + /** + * An array of class objects specifying the method's parameter types. + * The classes need to be given in the same order as they are declared by the targeted method. + * If a value is set, it will be overwritten if {@link #descriptor()} is also set. + * Default is no parameters. + * + *

+ * Note: Setting this value has no effect for types or fields. + *

+ * + * @return the method's parameter types + */ + Class[] parameterTypes() default {}; +} diff --git a/tutor/build.gradle.kts b/tutor/build.gradle.kts index 5c096d2c..59a8870b 100644 --- a/tutor/build.gradle.kts +++ b/tutor/build.gradle.kts @@ -14,6 +14,7 @@ dependencies { api(libs.jagr) api(libs.mockito) api(libs.asm) + api(libs.asmTree) testImplementation(libs.junit) dokkaPlugin(libs.dokkaKotlinAsJavaPlugin) } diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionMergingClassTransformer.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionMergingClassTransformer.java new file mode 100644 index 00000000..d1d7b52d --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionMergingClassTransformer.java @@ -0,0 +1,284 @@ +package org.tudalgo.algoutils.transform; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.objectweb.asm.Opcodes; +import org.sourcegrade.jagr.api.testing.extension.JagrExecutionCondition; +import org.tudalgo.algoutils.student.annotation.ForceSignature; +import org.tudalgo.algoutils.transform.classes.*; +import org.tudalgo.algoutils.transform.util.headers.MethodHeader; +import org.tudalgo.algoutils.transform.util.TransformationContext; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.Type; +import org.sourcegrade.jagr.api.testing.ClassTransformer; +import org.tudalgo.algoutils.tutor.general.SubmissionInfo; + +import java.lang.reflect.Executable; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +/** + * A class transformer that allows logging, substitution and delegation of method invocations. + * This transformer uses two source sets: solution classes and submission classes. + *

+ * Solution classes + *

+ * Solution classes are compiled java classes located in the {@code resources/classes/} directory. + * Their class structure must match the one defined in the exercise sheet but they may define additional + * members such as fields and methods. + * Additional types (classes, interfaces, enums, etc.) may also be defined. + *
+ * The directory structure must match the one in the output / build directory. + * Due to limitations of {@link Class#getResourceAsStream(String)} the compiled classes cannot have + * the {@code .class} file extension, so {@code .bin} is used instead. + * For example, a class {@code MyClass} with an inner class {@code Inner} in package {@code my.package} + * would be compiled to {@code my/package/MyClass.class} and {@code my/package/MyClass$Inner.class}. + * In the solution classes directory they would be located at {@code resources/classes/my/package/MyClass.bin} + * and {@code resources/classes/my/package/MyClass$Inner.bin}, respectively. + *

+ * + * Submission classes + *

+ * Submission classes are the original java classes in the main module. + * They are compiled externally and processed one by one using {@link #transform(ClassReader, ClassWriter)}. + * In case classes or members are misnamed, this transformer will attempt to map them to the closest + * matching solution class / member. + * If both the direct and similarity matching approach fail, the intended target can be explicitly + * specified using the {@link ForceSignature} annotation. + *

+ *

+ * Implementation details: + * + * + * @see SubmissionClassVisitor + * @see SubmissionExecutionHandler + * @author Daniel Mangold + */ +@SuppressWarnings("unused") +public class SolutionMergingClassTransformer implements ClassTransformer { + + /** + * An object providing context throughout the transformation processing chain. + */ + private final TransformationContext transformationContext; + private boolean readSubmissionClasses = false; + + /** + * Constructs a new {@link SolutionMergingClassTransformer} instance. + * + * @param projectPrefix the root package containing all submission classes, usually the sheet number + * @param availableSolutionClasses the list of solution class names (fully qualified) to use + */ + public SolutionMergingClassTransformer(String projectPrefix, String... availableSolutionClasses) { + this(Arrays.stream(availableSolutionClasses).reduce(new Builder(projectPrefix), + Builder::addSolutionClass, + (builder, builder2) -> builder)); + } + + /** + * Constructs a new {@link SolutionMergingClassTransformer} instance with config settings from + * the given builder. + * + * @param builder the builder object + */ + @SuppressWarnings("unchecked") + private SolutionMergingClassTransformer(Builder builder) { + Map solutionClasses = new HashMap<>(); + Map submissionClasses = new ConcurrentHashMap<>(); + this.transformationContext = new TransformationContext(Collections.unmodifiableMap(builder.configuration), + solutionClasses, + submissionClasses); + ((Map>) builder.configuration.get(Config.SOLUTION_CLASSES)).keySet() + .forEach(className -> solutionClasses.put(className, transformationContext.readSolutionClass(className))); + } + + @Override + public String getName() { + return SolutionMergingClassTransformer.class.getSimpleName(); + } + + @Override + public int getWriterFlags() { + return ClassWriter.COMPUTE_MAXS; + } + + @Override + public void transform(ClassReader reader, ClassWriter writer) { + if (!new JagrExecutionCondition().evaluateExecutionCondition(null).isDisabled()) { // if Jagr is present + try { + Method getClassLoader = ClassWriter.class.getDeclaredMethod("getClassLoader"); + getClassLoader.setAccessible(true); + ClassLoader submissionClassLoader = (ClassLoader) getClassLoader.invoke(writer); + transformationContext.setSubmissionClassLoader(submissionClassLoader); + + if (!readSubmissionClasses) { + Set submissionClassNames = new ObjectMapper() + .readValue(submissionClassLoader.getResourceAsStream("submission-info.json"), SubmissionInfo.class) + .sourceSets() + .stream() + .flatMap(sourceSet -> { + List classNames = sourceSet.files().get("java"); + return classNames != null ? classNames.stream() : null; + }) + .map(submissionClassName -> submissionClassName.replaceAll("\\.java$", "")) + .collect(Collectors.toSet()); + transformationContext.setSubmissionClassNames(submissionClassNames); + transformationContext.computeClassesSimilarity(); + + readSubmissionClasses = true; + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } else { + // TODO: fix this for regular JUnit run + transformationContext.setSubmissionClassLoader(null); + } + + if ((reader.getAccess() & Opcodes.ACC_ENUM) != 0) { + reader.accept(new SubmissionEnumClassVisitor(writer, transformationContext, reader.getClassName()), 0); + } else { + reader.accept(new SubmissionClassVisitor(writer, transformationContext, reader.getClassName()), 0); + } + } + + @Override + public Map injectClasses() { + Set visitedClasses = transformationContext.getVisitedClasses(); + Map missingClasses = new HashMap<>(); + + transformationContext.getSolutionClasses() + .entrySet() + .stream() + .filter(entry -> !visitedClasses.contains(entry.getKey())) + .forEach(entry -> { + ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS); + entry.getValue().accept(new MissingClassVisitor(writer, transformationContext, entry.getValue())); + missingClasses.put(entry.getKey().replace('/', '.'), writer.toByteArray()); + }); + + return missingClasses; + } + + /** + * (Internal) Configuration keys + */ + public enum Config { + PROJECT_PREFIX(null), + SOLUTION_CLASSES(new HashMap<>()), + SIMILARITY(0.90), + METHOD_REPLACEMENTS(new HashMap()); + + private final Object defaultValue; + + Config(Object defaultValue) { + this.defaultValue = defaultValue; + } + } + + /** + * Builder for {@link SolutionMergingClassTransformer}. + */ + public static class Builder { + + private final Map configuration = new EnumMap<>(Config.class); + + /** + * Constructs a new {@link Builder}. + * + * @param projectPrefix the root package containing all submission classes, usually the sheet number + */ + public Builder(String projectPrefix) { + for (Config config : Config.values()) { + configuration.put(config, config.defaultValue); + } + configuration.put(Config.PROJECT_PREFIX, projectPrefix); + } + + @SuppressWarnings("unchecked") + public Builder addSolutionClass(String solutionClassName, String... altNames) { + ((Map>) configuration.get(Config.SOLUTION_CLASSES)).put( + solutionClassName.replace('.', '/'), + Arrays.stream(altNames).map(s -> s.replace('.', '/')).toList() + ); + return this; + } + + /** + * Sets the threshold for matching submission classes to solution classes via similarity matching. + * + * @param similarity the new similarity threshold + * @return the builder object + */ + public Builder setSimilarity(double similarity) { + configuration.put(Config.SIMILARITY, similarity); + return this; + } + + /** + * Replaces all calls to the target executable with calls to the replacement executable. + * The replacement executable must be accessible from the calling class, be static and declare + * the same parameter types and return type as the target. + * If the target executable is not static, the replacement must declare an additional parameter + * at the beginning to receive the object the target was called on.
+ * Example:
+ * Target: {@code public boolean equals(Object)} in class {@code String} => + * Replacement: {@code public static boolean (String, Object)} + * + * @param targetExecutable the targeted method / constructor + * @param replacementExecutable the replacement method / constructor + * @return the builder object + */ + public Builder addMethodReplacement(Executable targetExecutable, Executable replacementExecutable) { + return addMethodReplacement(new MethodHeader(targetExecutable), new MethodHeader(replacementExecutable)); + } + + /** + * Replaces all calls to the matching the target's method header with calls to the replacement. + * The replacement must be accessible from the calling class, be static and declare + * the same parameter types and return type as the target. + * If the target is not static, the replacement must declare an additional parameter + * at the beginning to receive the object the target was called on.
+ * Example:
+ * Target: {@code public boolean equals(Object)} in class {@code String} => + * Replacement: {@code public static boolean (String, Object)} + * + * @param targetMethodHeader the header of the targeted method / constructor + * @param replacementMethodHeader the header of the replacement method / constructor + * @return the builder object + */ + @SuppressWarnings("unchecked") + public Builder addMethodReplacement(MethodHeader targetMethodHeader, MethodHeader replacementMethodHeader) { + if (!Modifier.isStatic(replacementMethodHeader.access())) { + throw new IllegalArgumentException("Replacement method " + replacementMethodHeader + " is not static"); + } + + ((Map) configuration.get(Config.METHOD_REPLACEMENTS)) + .put(targetMethodHeader, replacementMethodHeader); + return this; + } + + /** + * Constructs the transformer. + * + * @return the configured {@link SolutionMergingClassTransformer} object + */ + public SolutionMergingClassTransformer build() { + return new SolutionMergingClassTransformer(this); + } + } +} diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionExecutionHandler.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionExecutionHandler.java new file mode 100644 index 00000000..de4daced --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionExecutionHandler.java @@ -0,0 +1,472 @@ +package org.tudalgo.algoutils.transform; + +import org.objectweb.asm.Type; +import org.tudalgo.algoutils.transform.classes.SubmissionClassVisitor; +import org.tudalgo.algoutils.transform.util.*; +import org.tudalgo.algoutils.transform.util.headers.ClassHeader; +import org.tudalgo.algoutils.transform.util.headers.FieldHeader; +import org.tudalgo.algoutils.transform.util.headers.MethodHeader; + +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.reflect.Executable; +import java.util.*; +import java.util.stream.Collectors; + +/** + * A singleton class to configure the way a submission is executed. + * This class can be used to + *
    + *
  • log method invocations
  • + *
  • delegate invocations to the solution / a pre-defined external class
  • + *
  • delegate invocations to a custom programmatically-defined method (e.g. lambdas)
  • + *
+ * By default, all method calls are delegated to the solution class, if one is present. + * To call the real method, delegation must be disabled before calling it. + * This can be done by calling {@link Delegation#disable}. + *
+ * To use any of these features, the submission classes need to be transformed by {@link SolutionMergingClassTransformer}. + *

+ * An example test class could look like this: + *
+ * {@code
+ * public class ExampleTest {
+ *
+ *     private final SubmissionExecutionHandler executionHandler = SubmissionExecutionHandler.getInstance();
+ *
+ *     @BeforeEach
+ *     public void setup() {
+ *         // Pre-test setup, if necessary. Useful for substitution:
+ *         Method substitutedMethod = TestedClass.class.getDeclaredMethod("dependencyForTest");
+ *         executionHandler.substituteMethod(substitutedMethod, invocation -> "Hello world!");
+ *     }
+ *
+ *     @AfterEach
+ *     public void reset() {
+ *         // Optionally reset invocation logs, substitutions, etc.
+ *         executionHandler.resetMethodInvocationLogging();
+ *         executionHandler.resetMethodDelegation();
+ *         executionHandler.resetMethodSubstitution();
+ *     }
+ *
+ *     @Test
+ *     public void test() throws ReflectiveOperationException {
+ *         Method method = TestedClass.class.getDeclaredMethod("methodUnderTest");
+ *         executionHandler.disableDelegation(method); // Disable delegation, i.e., use the original implementation
+ *         ...
+ *     }
+ * }
+ * }
+ * 
+ * + * @see SolutionMergingClassTransformer + * @see SubmissionClassVisitor + * @author Daniel Mangold + */ +@SuppressWarnings("unused") +public class SubmissionExecutionHandler { + + // declaring class => (method header => invocations) + private static final Map>> METHOD_INVOCATIONS = new HashMap<>(); + private static final Map> METHOD_SUBSTITUTIONS = new HashMap<>(); + private static final Map> METHOD_DELEGATION_EXCLUSIONS = new HashMap<>(); + + private SubmissionExecutionHandler() {} + + // Submission class info + + /** + * Returns the original class header for the given submission class. + * + * @param clazz the submission class + * @return the original class header + */ + public static ClassHeader getOriginalClassHeader(Class clazz) { + try { + return (ClassHeader) MethodHandles.lookup() + .findStatic(clazz, Constants.INJECTED_GET_ORIGINAL_CLASS_HEADER.name(), MethodType.methodType(ClassHeader.class)) + .invokeExact(); + } catch (RuntimeException e) { + throw e; + } catch (Throwable t) { + throw new RuntimeException(t); + } + } + + /** + * Returns the set of original field headers for the given submission class. + * + * @param clazz the submission class + * @return the set of original field headers + */ + @SuppressWarnings("unchecked") + public static Set getOriginalFieldHeaders(Class clazz) { + try { + return (Set) MethodHandles.lookup() + .findStatic(clazz, Constants.INJECTED_GET_ORIGINAL_FIELD_HEADERS.name(), MethodType.methodType(Set.class)) + .invokeExact(); + } catch (RuntimeException e) { + throw e; + } catch (Throwable t) { + throw new RuntimeException(t); + } + } + + public static FieldHeader getOriginalFieldHeader(Class clazz, String fieldName) { + return getOriginalFieldHeaders(clazz).stream() + .filter(fieldHeader -> fieldHeader.name().equals(fieldName)) + .findAny() + .orElse(null); + } + + /** + * Returns the set of original method headers for the given submission class. + * + * @param clazz the submission class + * @return the set of original method headers + */ + @SuppressWarnings("unchecked") + public static Set getOriginalMethodHeaders(Class clazz) { + try { + return (Set) MethodHandles.lookup() + .findStatic(clazz, Constants.INJECTED_GET_ORIGINAL_METHODS_HEADERS.name(), MethodType.methodType(Set.class)) + .invokeExact(); + } catch (RuntimeException e) { + throw e; + } catch (Throwable t) { + throw new RuntimeException(t); + } + } + + public static MethodHeader getOriginalMethodHeader(Class clazz, Class... parameterTypes) { + return getOriginalMethodHeader(clazz, "", parameterTypes); + } + + public static MethodHeader getOriginalMethodHeader(Class clazz, + String methodName, + Class... parameterTypes) { + String parameterDescriptor = Arrays.stream(parameterTypes) + .map(Type::getDescriptor) + .collect(Collectors.joining("", "(", ")")); + return getOriginalMethodHeaders(clazz).stream() + .filter(methodHeader -> methodHeader.name().equals(methodName) && + methodHeader.descriptor().startsWith(parameterDescriptor)) + .findAny() + .orElse(null); + } + + /** + * Returns the list of original enum constants for the given submission class. + * + * @param clazz the submission class + * @return the list of original enum constants + */ + @SuppressWarnings("unchecked") + public static > List getOriginalEnumConstants(Class clazz) { + try { + return (List) MethodHandles.lookup() + .findStatic(clazz, Constants.INJECTED_GET_ORIGINAL_ENUM_CONSTANTS.name(), MethodType.methodType(List.class)) + .invokeExact(); + } catch (RuntimeException e) { + throw e; + } catch (Throwable t) { + throw new RuntimeException(t); + } + } + + public static > EnumConstant getOriginalEnumConstant(Class clazz, String constantName) { + return getOriginalEnumConstants(clazz) + .stream() + .filter(constant -> constant.name().equals(constantName)) + .findAny() + .orElse(null); + } + + @SuppressWarnings("unchecked") + public static Map getOriginalStaticFieldValues(Class clazz) { + try { + return (Map) MethodHandles.lookup() + .findStatic(clazz, Constants.INJECTED_GET_ORIGINAL_STATIC_FIELD_VALUES.name(), MethodType.methodType(Map.class)) + .invokeExact(); + } catch (RuntimeException e) { + throw e; + } catch (Throwable t) { + throw new RuntimeException(t); + } + } + + public static Object getOriginalStaticFieldValue(Class clazz, String fieldName) { + return getOriginalStaticFieldValues(clazz).get(fieldName); + } + + /** + * Resets all mechanisms. + */ + public static void resetAll() { + Logging.reset(); + Substitution.reset(); + Delegation.reset(); + } + + public static final class Logging { + + private Logging() {} + + /** + * Enables logging of method / constructor invocations for the given executable. + * + * @param executable the method / constructor to enable invocation logging for + */ + public static void enable(Executable executable) { + enable(new MethodHeader(executable)); + } + + /** + * Enables logging of method invocations for the given method. + * + * @param methodHeader a method header describing the method + */ + public static void enable(MethodHeader methodHeader) { + METHOD_INVOCATIONS.computeIfAbsent(methodHeader.owner(), k -> new HashMap<>()) + .putIfAbsent(methodHeader, new ArrayList<>()); + } + + /** + * Disables logging of method / constructor invocations for the given executable. + * Note: This also discards all logged invocations. + * + * @param executable the method / constructor to disable invocation logging for + */ + public static void disable(Executable executable) { + disable(new MethodHeader(executable)); + } + + /** + * Disables logging of method invocations for the given method. + * Note: This also discards all logged invocations. + * + * @param methodHeader a method header describing the method + */ + public static void disable(MethodHeader methodHeader) { + Optional.ofNullable(METHOD_INVOCATIONS.get(methodHeader.owner())) + .ifPresent(map -> map.remove(methodHeader)); + } + + /** + * Resets the logging of method invocations to log no invocations. + */ + public static void reset() { + METHOD_INVOCATIONS.clear(); + } + + /** + * Returns all logged invocations for the given method / constructor. + * + * @param executable the method / constructor to get invocations of + * @return a list of invocations on the given method + */ + public static List getInvocations(Executable executable) { + return getInvocations(new MethodHeader(executable)); + } + + /** + * Returns all logged invocations for the given method. + * + * @param methodHeader a method header describing the method + * @return a list of invocations on the given method + */ + public static List getInvocations(MethodHeader methodHeader) { + return Optional.ofNullable(METHOD_INVOCATIONS.get(methodHeader.owner())) + .map(map -> map.get(methodHeader)) + .map(Collections::unmodifiableList) + .orElse(null); + } + } + + public static final class Substitution { + + private Substitution() {} + + /** + * Substitute calls to the given method / constructor with the invocation of the given {@link MethodSubstitution}. + * In other words, instead of executing the instructions of either the original submission or the solution, + * this can be used to make the method do and return anything at runtime. + * + * @param executable the method / constructor to substitute + * @param substitute the {@link MethodSubstitution} the method will be substituted with + */ + public static void enable(Executable executable, MethodSubstitution substitute) { + enable(new MethodHeader(executable), substitute); + } + + /** + * Substitute calls to the given method with the invocation of the given {@link MethodSubstitution}. + * In other words, instead of executing the instructions of either the original submission or the solution, + * this can be used to make the method do and return anything at runtime. + * + * @param methodHeader a method header describing the method + * @param substitute the {@link MethodSubstitution} the method will be substituted with + */ + public static void enable(MethodHeader methodHeader, MethodSubstitution substitute) { + METHOD_SUBSTITUTIONS.computeIfAbsent(methodHeader.owner(), k -> new HashMap<>()) + .put(methodHeader, substitute); + } + + /** + * Disables substitution for the given method / constructor. + * + * @param executable the substituted method / constructor + */ + public static void disable(Executable executable) { + disable(new MethodHeader(executable)); + } + + /** + * Disables substitution for the given method. + * + * @param methodHeader a method header describing the method + */ + public static void disable(MethodHeader methodHeader) { + Optional.ofNullable(METHOD_SUBSTITUTIONS.get(methodHeader.owner())) + .ifPresent(map -> map.remove(methodHeader)); + } + + /** + * Resets the substitution of methods. + */ + public static void reset() { + METHOD_SUBSTITUTIONS.clear(); + } + } + + public static final class Delegation { + + private Delegation() {} + + /** + * Enables delegation to the solution for the given executable. + * Note: Delegation is enabled by default, so this method usually does not have to be called before invocations. + * + * @param executable the method / constructor to enable delegation for. + */ + public static void enable(Executable executable) { + enable(new MethodHeader(executable)); + } + + /** + * Enables delegation to the solution for the given method. + * Note: Delegation is enabled by default, so this method usually does not have to be called before invocations. + * + * @param methodHeader a method header describing the method + */ + public static void enable(MethodHeader methodHeader) { + Optional.ofNullable(METHOD_DELEGATION_EXCLUSIONS.get(methodHeader.owner())) + .ifPresent(set -> set.remove(methodHeader)); + } + + /** + * Disables delegation to the solution for the given executable. + * + * @param executable the method / constructor to disable delegation for + */ + public static void disable(Executable executable) { + disable(new MethodHeader(executable)); + } + + /** + * Disables delegation to the solution for the given method. + * + * @param methodHeader a method header describing the method + */ + public static void disable(MethodHeader methodHeader) { + METHOD_DELEGATION_EXCLUSIONS.computeIfAbsent(methodHeader.owner(), k -> new HashSet<>()).add(methodHeader); + } + + /** + * Resets the delegation of methods. + */ + public static void reset() { + METHOD_DELEGATION_EXCLUSIONS.clear(); + } + } + + /** + * Collection of methods injected into the bytecode of transformed methods. + */ + public static final class Internal { + + private Internal() {} + + // Invocation logging + + /** + * Returns whether the calling method's invocation is logged, i.e. + * {@link #addInvocation(MethodHeader, Invocation)} may be called or not. + * Should only be used in bytecode transformations when intercepting method invocations. + * + * @param methodHeader a method header describing the method + * @return {@code true} if invocation logging is enabled for the given method, otherwise {@code false} + */ + public static boolean logInvocation(MethodHeader methodHeader) { + return Optional.ofNullable(METHOD_INVOCATIONS.get(methodHeader.owner())) + .map(map -> map.get(methodHeader)) + .isPresent(); + } + + /** + * Adds an invocation to the list of invocations for the calling method. + * Should only be used in bytecode transformations when intercepting method invocations. + * + * @param methodHeader a method header describing the method + * @param invocation the invocation on the method, i.e. the context it has been called with + */ + public static void addInvocation(MethodHeader methodHeader, Invocation invocation) { + Optional.ofNullable(METHOD_INVOCATIONS.get(methodHeader.owner())) + .map(map -> map.get(methodHeader)) + .ifPresent(list -> list.add(invocation)); + } + + // Method substitution + + /** + * Returns whether the given method has a substitute or not. + * Should only be used in bytecode transformations when intercepting method invocations. + * + * @param methodHeader a method header describing the method + * @return {@code true} if substitution is enabled for the given method, otherwise {@code false} + */ + public static boolean useSubstitution(MethodHeader methodHeader) { + return Optional.ofNullable(METHOD_SUBSTITUTIONS.get(methodHeader.owner())) + .map(map -> map.containsKey(methodHeader)) + .orElse(false); + } + + /** + * Returns the substitute for the given method. + * Should only be used in bytecode transformations when intercepting method invocations. + * + * @param methodHeader a method header describing the method + * @return the substitute for the given method + */ + public static MethodSubstitution getSubstitution(MethodHeader methodHeader) { + return Optional.ofNullable(METHOD_SUBSTITUTIONS.get(methodHeader.owner())) + .map(map -> map.get(methodHeader)) + .orElseThrow(); + } + + // Method delegation + + /** + * Returns whether the original instructions are used or not. + * Should only be used in bytecode transformations when intercepting method invocations. + * + * @param methodHeader a method header describing the method + * @return {@code true} if delegation is disabled for the given method, otherwise {@code false} + */ + public static boolean useSubmissionImpl(MethodHeader methodHeader) { + return Optional.ofNullable(METHOD_DELEGATION_EXCLUSIONS.get(methodHeader.owner())) + .map(set -> set.contains(methodHeader)) + .orElse(false); + } + } +} diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/ClassInfo.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/ClassInfo.java new file mode 100644 index 00000000..eedcb8de --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/ClassInfo.java @@ -0,0 +1,181 @@ +package org.tudalgo.algoutils.transform.classes; + +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.tudalgo.algoutils.transform.util.headers.ClassHeader; +import org.tudalgo.algoutils.transform.util.headers.FieldHeader; +import org.tudalgo.algoutils.transform.util.headers.MethodHeader; +import org.tudalgo.algoutils.transform.util.TransformationContext; + +import java.lang.reflect.Modifier; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * This class holds information about a class, such as its header, fields and methods. + *

+ * There are two sets of methods: those returning the original headers and those returning computed variants. + * The original header methods return the headers as they were declared in the class. + * The computed header methods may perform transforming operations on the header or map them to some other + * header and return the result of that operation. + *

+ *

+ * The class extends {@link ClassVisitor} so its methods can be used to read classes + * using the visitor pattern. + *

+ * + * @author Daniel Mangold + */ +public abstract class ClassInfo extends ClassVisitor { + + protected final TransformationContext transformationContext; + protected final Set superTypeMembers = new HashSet<>(); + + protected final Map fields = new HashMap<>(); // Mapping of fields in submission => usable fields + protected final Map methods = new HashMap<>(); // Mapping of methods in submission => usable methods + protected final Map superClassConstructors = new HashMap<>(); + + /** + * Initializes a new {@link ClassInfo} object. + * + * @param transformationContext the transformation context + */ + public ClassInfo(TransformationContext transformationContext) { + super(Opcodes.ASM9); + + this.transformationContext = transformationContext; + } + + /** + * Returns the original class header. + * + * @return the original class header + */ + public abstract ClassHeader getOriginalClassHeader(); + + /** + * Returns the computed class header. + * The computed header is the header of the associated solution class, if one is present. + * If no solution class is present, the computed header equals the original submission class header. + * + * @return the computed class header + */ + public abstract ClassHeader getComputedClassHeader(); + + /** + * Returns the original field headers for this class. + * + * @return the original field headers + */ + public abstract Set getOriginalFieldHeaders(); + + /** + * Returns the computed field header for the given field name. + * The computed field header is the field header of the corresponding field in the solution class, + * if one is present. + * If no solution class is present, the computed field header equals the original field header + * in the submission class. + * + * @param name the field name + * @return the computed field header + */ + public abstract FieldHeader getComputedFieldHeader(String name); + + /** + * Return the original method headers for this class. + * + * @return the original method headers + */ + public abstract Set getOriginalMethodHeaders(); + + /** + * Returns the computed method header for the given method signature. + * The computed method header is the method header of the corresponding method in the solution class, + * if one is present. + * If no solution class is present, the computed method header equals the original method header + * in the submission class. + * + * @param name the method name + * @param descriptor the method descriptor + * @return the computed method header + */ + public abstract MethodHeader getComputedMethodHeader(String name, String descriptor); + + /** + * Returns the original method headers of the direct superclass' constructors. + * + * @return the original superclass constructor headers + */ + public abstract Set getOriginalSuperClassConstructorHeaders(); + + /** + * Returns the computed superclass constructor header for the given method descriptor. + * If the direct superclass is part of the submission and has a corresponding solution class, + * the computed header is the constructor header of the solution class. + * Otherwise, it is the original constructor header. + * + * @param descriptor the constructor descriptor + * @return the computed constructor header + */ + public abstract MethodHeader getComputedSuperClassConstructorHeader(String descriptor); + + /** + * Recursively resolves all relevant members of the given type. + * + * @param superTypeMembers a set for recording type members + * @param typeName the name of the class / interface to process + */ + protected abstract void resolveSuperTypeMembers(Set superTypeMembers, String typeName); + + /** + * Recursively resolves the members of superclasses and interfaces. + * + * @param superTypeMembers a set for recording type members + * @param superClass the name of the superclass to process + * @param interfaces the names of the interfaces to process + */ + protected void resolveSuperTypeMembers(Set superTypeMembers, String superClass, String[] interfaces) { + resolveSuperTypeMembers(superTypeMembers, superClass); + if (interfaces != null) { + for (String interfaceName : interfaces) { + resolveSuperTypeMembers(superTypeMembers, interfaceName); + } + } + } + + /** + * Resolves the members of types that are neither submission classes nor solution classes. + * + * @param superTypeMembers a set for recording type members + * @param typeName the name of the type to process + * @param recursive whether to recursively resolve superclass and interfaces of the given type + */ + protected void resolveExternalSuperTypeMembers(Set superTypeMembers, String typeName, boolean recursive) { + try { + Class clazz = Class.forName(typeName.replace('/', '.')); + Map fieldHeaders = Arrays.stream(clazz.getDeclaredFields()) + .filter(field -> !Modifier.isPrivate(field.getModifiers())) + .map(FieldHeader::new) + .collect(Collectors.toMap(Function.identity(), Function.identity())); + Map methodHeaders = Stream.concat( + Arrays.stream(clazz.getDeclaredConstructors()), + Arrays.stream(clazz.getDeclaredMethods())) + .filter(executable -> !Modifier.isPrivate(executable.getModifiers())) + .map(MethodHeader::new) + .collect(Collectors.toMap(Function.identity(), Function.identity())); + superTypeMembers.add(new SuperTypeMembers(typeName, fieldHeaders, methodHeaders)); + if (clazz.getSuperclass() != null && recursive) { + resolveSuperTypeMembers(superTypeMembers, + Type.getInternalName(clazz.getSuperclass()), + Arrays.stream(clazz.getInterfaces()).map(Type::getInternalName).toArray(String[]::new)); + } + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + + public record SuperTypeMembers(String typeName, Map fields, Map methods) {} +} diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/MissingClassInfo.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/MissingClassInfo.java new file mode 100644 index 00000000..b7325c2b --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/MissingClassInfo.java @@ -0,0 +1,123 @@ +package org.tudalgo.algoutils.transform.classes; + +import org.objectweb.asm.ClassVisitor; +import org.tudalgo.algoutils.transform.util.headers.ClassHeader; +import org.tudalgo.algoutils.transform.util.headers.FieldHeader; +import org.tudalgo.algoutils.transform.util.headers.MethodHeader; +import org.tudalgo.algoutils.transform.util.TransformationContext; + +import java.util.Collections; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * Holds information about a class that is absent from the submission but present in the solution. + *

+ * Original and computed header methods return the same values since this class can only be + * used with solution classes. + * For the same reason, using {@link ClassVisitor} methods has no effect. + *

+ * + * @author Daniel Mangold + */ +public class MissingClassInfo extends ClassInfo { + + private final SolutionClassNode solutionClassNode; + + /** + * Constructs a new {@link MissingClassInfo} instance using the information stored + * in the given solution class. + * + * @param transformationContext the transformation context + * @param solutionClassNode the solution class + */ + public MissingClassInfo(TransformationContext transformationContext, SolutionClassNode solutionClassNode) { + super(transformationContext); + + this.solutionClassNode = solutionClassNode; + solutionClassNode.getFields() + .keySet() + .forEach(fieldHeader -> fields.put(fieldHeader, fieldHeader)); + solutionClassNode.getMethods() + .keySet() + .forEach(methodHeader -> methods.put(methodHeader, methodHeader)); + resolveSuperTypeMembers(superTypeMembers, getOriginalClassHeader().superName()); + } + + @Override + public ClassHeader getOriginalClassHeader() { + return solutionClassNode.getClassHeader(); + } + + @Override + public ClassHeader getComputedClassHeader() { + return getOriginalClassHeader(); + } + + @Override + public Set getOriginalFieldHeaders() { + return fields.keySet(); + } + + @Override + public FieldHeader getComputedFieldHeader(String name) { + return fields.keySet() + .stream() + .filter(fieldHeader -> fieldHeader.name().equals(name)) + .findAny() + .orElseThrow(); + } + + @Override + public Set getOriginalMethodHeaders() { + return methods.keySet(); + } + + @Override + public MethodHeader getComputedMethodHeader(String name, String descriptor) { + return methods.keySet() + .stream() + .filter(methodHeader -> methodHeader.name().equals(name) && methodHeader.descriptor().equals(descriptor)) + .findAny() + .orElseThrow(); + } + + @Override + public Set getOriginalSuperClassConstructorHeaders() { + return superClassConstructors.keySet(); + } + + @Override + public MethodHeader getComputedSuperClassConstructorHeader(String descriptor) { + return superClassConstructors.keySet() + .stream() + .filter(methodHeader -> methodHeader.descriptor().equals(descriptor)) + .findAny() + .orElseThrow(); + } + + @Override + protected void resolveSuperTypeMembers(Set superTypeMembers, String typeName) { + if (typeName == null) { + resolveSuperTypeMembers(superTypeMembers, "java/lang/Object"); + return; + } + + Optional superClassNode = Optional.ofNullable(transformationContext.getSolutionClass(typeName)); + if (superClassNode.isPresent()) { + superTypeMembers.add(new SuperTypeMembers( + typeName, + Collections.emptyMap(), + superClassNode.get() + .getMethods() + .keySet() + .stream() + .collect(Collectors.toMap(Function.identity(), Function.identity())) + )); + } else { + resolveExternalSuperTypeMembers(superTypeMembers, typeName, false); + } + } +} diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/MissingClassVisitor.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/MissingClassVisitor.java new file mode 100644 index 00000000..b20b25a6 --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/MissingClassVisitor.java @@ -0,0 +1,67 @@ +package org.tudalgo.algoutils.transform.classes; + +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.tudalgo.algoutils.transform.methods.MissingMethodVisitor; +import org.tudalgo.algoutils.transform.util.Constants; +import org.tudalgo.algoutils.transform.util.IncompatibleHeaderException; +import org.tudalgo.algoutils.transform.util.headers.MethodHeader; +import org.tudalgo.algoutils.transform.util.TransformationContext; + +/** + * A class visitor for visiting and transforming classes that are absent from the submission + * but present in the solution. + * + * @author Daniel Mangold + */ +public class MissingClassVisitor extends ClassVisitor { + + private final TransformationContext transformationContext; + private final SolutionClassNode solutionClassNode; + + /** + * Constructs a new {@link MissingClassVisitor} instance. + * + * @param delegate the class visitor to delegate to + * @param transformationContext the transformation context + * @param solutionClassNode the solution class + */ + public MissingClassVisitor(ClassVisitor delegate, + TransformationContext transformationContext, + SolutionClassNode solutionClassNode) { + super(Opcodes.ASM9, delegate); + + this.transformationContext = transformationContext; + this.solutionClassNode = solutionClassNode; + } + + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + MethodHeader methodHeader = new MethodHeader(solutionClassNode.getClassHeader().name(), access, name, descriptor, signature, exceptions); + return new MissingMethodVisitor(methodHeader.toMethodVisitor(getDelegate()), + transformationContext, + new MissingClassInfo(transformationContext, solutionClassNode), + methodHeader); + } + + @Override + public void visitEnd() { + injectMetadataMethod(Constants.INJECTED_GET_ORIGINAL_CLASS_HEADER); + injectMetadataMethod(Constants.INJECTED_GET_ORIGINAL_FIELD_HEADERS); + injectMetadataMethod(Constants.INJECTED_GET_ORIGINAL_METHODS_HEADERS); + injectMetadataMethod(Constants.INJECTED_GET_ORIGINAL_STATIC_FIELD_VALUES); + if ((solutionClassNode.access & Opcodes.ACC_ENUM) != 0) { + injectMetadataMethod(Constants.INJECTED_GET_ORIGINAL_ENUM_CONSTANTS); + } + + super.visitEnd(); + } + + private void injectMetadataMethod(MethodHeader methodHeader) { + MethodVisitor mv = methodHeader.toMethodVisitor(getDelegate()); + int maxStack = IncompatibleHeaderException.replicateInBytecode(mv, true, + "Class does not exist in submission or could not be matched", solutionClassNode.getClassHeader(), null); + mv.visitMaxs(maxStack, 0); + } +} diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/SolutionClassNode.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/SolutionClassNode.java new file mode 100644 index 00000000..4783c6c5 --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/SolutionClassNode.java @@ -0,0 +1,141 @@ +package org.tudalgo.algoutils.transform.classes; + +import org.objectweb.asm.*; +import org.tudalgo.algoutils.transform.util.*; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.FieldNode; +import org.objectweb.asm.tree.MethodNode; +import org.tudalgo.algoutils.transform.util.headers.ClassHeader; +import org.tudalgo.algoutils.transform.util.headers.FieldHeader; +import org.tudalgo.algoutils.transform.util.headers.MethodHeader; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import static org.objectweb.asm.Opcodes.*; + +/** + * A class node for recording bytecode instructions of solution classes. + * @author Daniel Mangold + */ +public class SolutionClassNode extends ClassNode { + + private final TransformationContext transformationContext; + private final String className; + private ClassHeader classHeader; + private final Map fields = new HashMap<>(); + private final Map methods = new HashMap<>(); + + /** + * Constructs a new {@link SolutionClassNode} instance. + * + * @param className the name of the solution class + */ + public SolutionClassNode(TransformationContext transformationContext, String className) { + super(Opcodes.ASM9); + this.transformationContext = transformationContext; + this.className = className; + } + + public ClassHeader getClassHeader() { + return classHeader; + } + + /** + * Returns the mapping of field headers to field nodes for this solution class. + * + * @return the field header => field node mapping + */ + public Map getFields() { + return fields; + } + + /** + * Returns the mapping of method headers to method nodes for this solution class. + * + * @return the method header => method node mapping + */ + public Map getMethods() { + return methods; + } + + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + classHeader = new ClassHeader(access, name, signature, superName, interfaces); + super.visit(version, access, name, signature, superName, interfaces); + } + + @Override + public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) { + FieldHeader fieldHeader = new FieldHeader(className, TransformationUtils.transformAccess(access), name, descriptor, signature); + FieldNode fieldNode = (FieldNode) super.visitField(TransformationUtils.transformAccess(access), name, descriptor, signature, value); + fields.put(fieldHeader, fieldNode); + return fieldNode; + } + + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + if (TransformationUtils.isLambdaMethod(access, name)) { + name += "$solution"; + } + MethodNode methodNode = getMethodNode(access, name, descriptor, signature, exceptions); + methods.put(new MethodHeader(className, TransformationUtils.transformAccess(access), name, descriptor, signature, exceptions), methodNode); + return methodNode; + } + + /** + * Constructs a new method node with the given information. + * The returned method node ensures that lambda methods of the solution class don't interfere + * with the ones defined in the submission class. + * + * @param access the method's modifiers + * @param name the method's name + * @param descriptor the method's descriptor + * @param signature the method's signature + * @param exceptions the method's declared exceptions + * @return a new {@link MethodNode} + */ + private MethodNode getMethodNode(int access, String name, String descriptor, String signature, String[] exceptions) { + MethodNode methodNode = new MethodNode(ASM9, TransformationUtils.transformAccess(access), name, descriptor, signature, exceptions) { + @Override + public void visitMethodInsn(int opcodeAndSource, String owner, String name, String descriptor, boolean isInterface) { + MethodHeader methodHeader = new MethodHeader(owner, name, descriptor); + if (transformationContext.methodHasReplacement(methodHeader)) { + MethodHeader replacementMethodHeader = transformationContext.getMethodReplacement(methodHeader); + super.visitMethodInsn(INVOKESTATIC, + replacementMethodHeader.owner(), + replacementMethodHeader.name(), + replacementMethodHeader.descriptor(), + false); + } else { + super.visitMethodInsn(opcodeAndSource, + owner, + name + (name.startsWith("lambda$") ? "$solution" : ""), + descriptor, + isInterface); + } + } + + @Override + public void visitInvokeDynamicInsn(String name, String descriptor, Handle bootstrapMethodHandle, Object... bootstrapMethodArguments) { + super.visitInvokeDynamicInsn(name, descriptor, bootstrapMethodHandle, Arrays.stream(bootstrapMethodArguments) + .map(o -> { + if (o instanceof Handle handle && handle.getName().startsWith("lambda$")) { + return new Handle(handle.getTag(), + handle.getOwner(), + handle.getName() + "$solution", + handle.getDesc(), + handle.isInterface()); + } else { + return o; + } + }) + .toArray()); + } + }; + + super.methods.add(methodNode); + return methodNode; + } +} diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/SubmissionClassInfo.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/SubmissionClassInfo.java new file mode 100644 index 00000000..48d6fce6 --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/SubmissionClassInfo.java @@ -0,0 +1,257 @@ +package org.tudalgo.algoutils.transform.classes; + +import org.tudalgo.algoutils.transform.util.*; +import org.objectweb.asm.*; +import org.tudalgo.algoutils.transform.util.headers.ClassHeader; +import org.tudalgo.algoutils.transform.util.headers.FieldHeader; +import org.tudalgo.algoutils.transform.util.headers.MethodHeader; +import org.tudalgo.algoutils.transform.util.matching.FieldSimilarityMapper; +import org.tudalgo.algoutils.transform.util.matching.MethodSimilarityMapper; + +import java.lang.reflect.Modifier; +import java.util.*; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +/** + * A class that holds information on a submission class. + *

+ * This class will attempt to find a corresponding solution class and map its members + * to the ones defined in the solution class. + * If no solution class can be found, for example because the submission class was added + * as a utility class, it will map its members to themselves to remain usable. + *

+ *

+ * Note: Since this class resolves members of supertypes recursively, it may lead to a stack overflow + * if it does so all in one step. + * Therefore, members (both of this class and supertypes) are only fully resolved once + * {@link #resolveMembers()} is called. + *

+ * + * @author Daniel Mangold + */ +public class SubmissionClassInfo extends ClassInfo { + + private final ForceSignatureAnnotationProcessor fsAnnotationProcessor; + + private ClassHeader originalClassHeader; + private ClassHeader computedClassHeader; + private SolutionClassNode solutionClass; + + /** + * Constructs a new {@link SubmissionClassInfo} instance. + * + * @param transformationContext a {@link TransformationContext} object + * @param fsAnnotationProcessor a {@link ForceSignatureAnnotationProcessor} for the submission class + */ + public SubmissionClassInfo(TransformationContext transformationContext, + ForceSignatureAnnotationProcessor fsAnnotationProcessor) { + super(transformationContext); + + this.fsAnnotationProcessor = fsAnnotationProcessor; + } + + @Override + public ClassHeader getOriginalClassHeader() { + return originalClassHeader; + } + + @Override + public ClassHeader getComputedClassHeader() { + return computedClassHeader; + } + + @Override + public Set getOriginalFieldHeaders() { + return fields.keySet(); + } + + @Override + public FieldHeader getComputedFieldHeader(String name) { + return fields.entrySet() + .stream() + .filter(entry -> entry.getKey().name().equals(name)) + .findAny() + .map(Map.Entry::getValue) + .orElseThrow(); + } + + @Override + public Set getOriginalMethodHeaders() { + return methods.keySet(); + } + + @Override + public MethodHeader getComputedMethodHeader(String name, String descriptor) { + return methods.entrySet() + .stream() + .filter(entry -> entry.getKey().name().equals(name) && entry.getKey().descriptor().equals(descriptor)) + .findAny() + .map(Map.Entry::getValue) + .orElseThrow(); + } + + @Override + public Set getOriginalSuperClassConstructorHeaders() { + return superClassConstructors.keySet(); + } + + @Override + public MethodHeader getComputedSuperClassConstructorHeader(String descriptor) { + return superClassConstructors.entrySet() + .stream() + .filter(entry -> entry.getKey().descriptor().equals(descriptor)) + .findAny() + .map(Map.Entry::getValue) + .orElseThrow(); + } + + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + // TODO: make sure interfaces is not null + originalClassHeader = new ClassHeader(access, name, signature, superName, interfaces); + String computedClassName; + if (fsAnnotationProcessor.classIdentifierIsForced()) { + computedClassName = fsAnnotationProcessor.forcedClassIdentifier(); + } else { + // If not forced, get the closest matching solution class + computedClassName = transformationContext.getSolutionClassName(originalClassHeader.name()); + } + solutionClass = transformationContext.getSolutionClass(computedClassName); + computedClassHeader = getSolutionClass().map(SolutionClassNode::getClassHeader).orElse(originalClassHeader); + } + + @Override + public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) { + fields.put(new FieldHeader(originalClassHeader.name(), access, name, descriptor, signature), null); + return null; + } + + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + MethodHeader submissionMethodHeader = new MethodHeader(originalClassHeader.name(), access, name, descriptor, signature, exceptions); + if (TransformationUtils.isLambdaMethod(access, name)) { + methods.put(submissionMethodHeader, submissionMethodHeader); + return null; + } + + methods.put(submissionMethodHeader, null); + return null; + } + + /** + * Returns the solution class associated with this submission class. + * + * @return an {@link Optional} object wrapping the associated solution class + */ + public Optional getSolutionClass() { + return Optional.ofNullable(solutionClass); + } + + /** + * Resolves the members of this submission class and all members from supertypes it inherits. + * This method must be called once to use this {@link SubmissionClassInfo} instance. + */ + public void resolveMembers() { + FieldSimilarityMapper fieldsSimilarityMapper = new FieldSimilarityMapper( + fields.keySet(), + getSolutionClass().map(solutionClass -> solutionClass.getFields().keySet()).orElseGet(Collections::emptySet), + transformationContext + ); + for (FieldHeader submissionFieldHeader : fields.keySet()) { + Supplier fallbackFieldHeader = () -> new FieldHeader(computedClassHeader.name(), + submissionFieldHeader.access(), + submissionFieldHeader.name(), + transformationContext.toComputedType(Type.getType(submissionFieldHeader.descriptor())).getDescriptor(), + submissionFieldHeader.signature()); + FieldHeader solutionFieldHeader; + if (fsAnnotationProcessor.fieldIdentifierIsForced(submissionFieldHeader.name())) { + solutionFieldHeader = fsAnnotationProcessor.forcedFieldHeader(submissionFieldHeader.name()); + } else if (solutionClass != null) { + solutionFieldHeader = solutionClass.getFields() + .keySet() + .stream() + .filter(fieldHeader -> fieldsSimilarityMapper.getBestMatch(submissionFieldHeader) + .map(fieldHeader::equals) + .orElse(false)) + .findAny() + .orElseGet(fallbackFieldHeader); + } else { + solutionFieldHeader = fallbackFieldHeader.get(); + } + fields.put(submissionFieldHeader, solutionFieldHeader); + } + + MethodSimilarityMapper methodsSimilarityMapper = new MethodSimilarityMapper( + methods.keySet(), + getSolutionClass().map(solutionClass -> solutionClass.getMethods().keySet()).orElseGet(Collections::emptySet), + transformationContext + ); + for (MethodHeader submissionMethodHeader : methods.keySet()) { + String submissionMethodName = submissionMethodHeader.name(); + String submissionMethodDescriptor = submissionMethodHeader.descriptor(); + Supplier fallbackMethodHeader = () -> new MethodHeader(computedClassHeader.name(), + submissionMethodHeader.access(), + submissionMethodHeader.name(), + transformationContext.toComputedDescriptor(submissionMethodHeader.descriptor()), + submissionMethodHeader.signature(), + submissionMethodHeader.exceptions()); + MethodHeader solutionMethodHeader; + if (fsAnnotationProcessor.methodSignatureIsForced(submissionMethodName, submissionMethodDescriptor)) { + solutionMethodHeader = fsAnnotationProcessor.forcedMethodHeader(submissionMethodName, submissionMethodDescriptor); + } else if (solutionClass != null) { + solutionMethodHeader = solutionClass.getMethods() + .keySet() + .stream() + .filter(methodHeader -> methodsSimilarityMapper.getBestMatch(submissionMethodHeader) + .map(methodHeader::equals) + .orElse(false)) + .findAny() + .orElseGet(fallbackMethodHeader); + } else { + solutionMethodHeader = fallbackMethodHeader.get(); + } + methods.put(submissionMethodHeader, solutionMethodHeader); + } + + resolveSuperTypeMembers(superTypeMembers, originalClassHeader.superName(), originalClassHeader.interfaces()); + for (SuperTypeMembers superTypeMembers : superTypeMembers) { + if (superTypeMembers.typeName().equals(originalClassHeader.superName())) { + superTypeMembers.methods() + .entrySet() + .stream() + .filter(entry -> entry.getKey().name().equals("")) + .forEach(entry -> superClassConstructors.put(entry.getKey(), entry.getValue())); + } + superTypeMembers.fields().forEach(fields::putIfAbsent); + superTypeMembers.methods() + .entrySet() + .stream() + .filter(entry -> !entry.getKey().name().equals("")) + .forEach(entry -> methods.putIfAbsent(entry.getKey(), entry.getValue())); + } + } + + @Override + protected void resolveSuperTypeMembers(Set superTypeMembers, String typeName) { + if (typeName == null) return; + + if (transformationContext.isSubmissionClass(typeName)) { + SubmissionClassInfo submissionClassInfo = transformationContext.getSubmissionClassInfo(typeName); + superTypeMembers.add(new SuperTypeMembers(typeName, + submissionClassInfo.fields.entrySet() + .stream() + .filter(entry -> !Modifier.isPrivate(entry.getKey().access())) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)), + submissionClassInfo.methods.entrySet() + .stream() + .filter(entry -> !Modifier.isPrivate(entry.getKey().access())) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)))); + resolveSuperTypeMembers(superTypeMembers, + submissionClassInfo.originalClassHeader.superName(), + submissionClassInfo.originalClassHeader.interfaces()); + } else { + resolveExternalSuperTypeMembers(superTypeMembers, typeName, true); + } + } +} diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/SubmissionClassVisitor.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/SubmissionClassVisitor.java new file mode 100644 index 00000000..c2e42857 --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/SubmissionClassVisitor.java @@ -0,0 +1,416 @@ +package org.tudalgo.algoutils.transform.classes; + +import kotlin.Pair; +import org.objectweb.asm.tree.MethodNode; +import org.tudalgo.algoutils.transform.SolutionMergingClassTransformer; +import org.tudalgo.algoutils.transform.SubmissionExecutionHandler; +import org.tudalgo.algoutils.transform.methods.ClassInitVisitor; +import org.tudalgo.algoutils.transform.methods.LambdaMethodVisitor; +import org.tudalgo.algoutils.transform.methods.MissingMethodVisitor; +import org.tudalgo.algoutils.transform.methods.SubmissionMethodVisitor; +import org.tudalgo.algoutils.transform.util.*; +import org.objectweb.asm.*; +import org.tudalgo.algoutils.transform.util.headers.ClassHeader; +import org.tudalgo.algoutils.transform.util.headers.FieldHeader; +import org.tudalgo.algoutils.transform.util.headers.MethodHeader; + +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import static org.objectweb.asm.Opcodes.*; + +/** + * A class visitor merging a submission class with its corresponding solution class, should one exist. + * The heart piece of the {@link SolutionMergingClassTransformer} processing chain. + *
+ * Main features: + *
    + *
  • + * Method invocation logging
    + * Logs the parameter values the method was called with. + * This allows the user to verify that a method was called and also that it was called with + * the right parameters. + * If the target method is not static or a constructor, the object the method was invoked on + * is logged as well. + *
  • + *
  • + * Method substitution
    + * Allows for "replacement" of a method at runtime. + * While the method itself must still be invoked, it will hand over execution to the provided + * substitution. + * This can be useful when a method should always return a certain value, regardless of object state + * or for making a non-deterministic method (e.g., RNG) return deterministic values. + * Replacing constructors is currently not supported. + * Can be combined with invocation logging. + *
  • + *
  • + * Method delegation
    + * Will effectively "replace" the code of the original submission with the one from the solution. + * While the instructions from both submission and solution are present in the merged method, only + * one can be active at a time. + * This allows for improved unit testing by not relying on submission code transitively. + * If this mechanism is used and no solution class is associated with this submission class or + * the solution class does not contain a matching method, the submission code will be used + * as a fallback. + * Can be combined with invocation logging. + *
  • + *
+ * All of these options can be enabled / disabled via {@link SubmissionExecutionHandler}. + * + *

+ * + * Generally, the body of a transformed method would look like this in Java source code: + *
+ * SubmissionExecutionHandler.Internal submissionExecutionHandler = SubmissionExecutionHandler.getInstance().new Internal();
+ * MethodHeader methodHeader = new MethodHeader(...);  // parameters are hardcoded during transformation
+ *
+ * if (submissionExecutionHandler.logInvocation(methodHeader)) {
+ *     submissionExecutionHandler.addInvocation(new Invocation(...)  // new Invocation() if constructor or static method
+ *         .addParameter(...)  // for each parameter
+ *         ...);
+ * }
+ * if (submissionExecutionHandler.useSubstitution(methodHeader)) {
+ *     MethodSubstitution methodSubstitution = submissionExecutionHandler.getSubstitution(methodHeader);
+ *
+ *     // if constructor
+ *     MethodSubstitution.ConstructorInvocation constructorInvocation = methodSubstitution.getConstructorInvocation();
+ *     if (constructorInvocation.owner().equals() && constructorInvocation.descriptor().equals() {
+ *         Object[] args = constructorInvocation.args();
+ *         super(args[0], args[1], ...);
+ *     }
+ *     else if ...  // for every superclass constructor
+ *     else if (constructorInvocation.owner().equals() && constructorInvocation.descriptor().equals() {
+ *         Object[] args = constructorInvocation.args();
+ *         this(args[0], args[1], ...);
+ *     }
+ *     else if ... // for every constructor in submission class
+ *     else {
+ *         throw new IllegalArgumentException(...);  // if no matching constructor was found
+ *     }
+ *
+ *     return methodSubstitution.execute(new Invocation(...) ...);  // same as above
+ * }
+ * if (submissionExecutionHandler.useSubmissionImpl(methodHeader)) {
+ *     ...  // submission code
+ * } else {
+ *     ...  // solution code
+ * }
+ * 
+ * If no solution class is associated with the submission class, the submission code is executed unconditionally. + *
+ * Additionally, the following methods are injected into the submission class: + *
+ * public static ClassHeader getOriginalClassHeader() {...}
+ * public static Set<FieldHeader> getOriginalFieldHeaders() {...}
+ * public static Set<MethodHeader> getOriginalMethodHeaders() {...}
+ * 
+ * + * @see SubmissionMethodVisitor + * @see SubmissionExecutionHandler + * @author Daniel Mangold + */ +public class SubmissionClassVisitor extends ClassVisitor { + + protected final TransformationContext transformationContext; + protected final SubmissionClassInfo submissionClassInfo; + protected final ClassHeader originalClassHeader; + protected final ClassHeader computedClassHeader; + + protected final Set visitedFields = new HashSet<>(); + protected final Set visitedMethods = new HashSet<>(); + + protected final Map> staticFieldValues = new HashMap<>(); + + /** + * Constructs a new {@link SubmissionClassVisitor} instance. + * + * @param classVisitor the class visitor to delegate to + * @param transformationContext the transformation context + * @param submissionClassName the name of the submission class that is visited + */ + public SubmissionClassVisitor(ClassVisitor classVisitor, + TransformationContext transformationContext, + String submissionClassName) { + super(ASM9, classVisitor); + this.transformationContext = transformationContext; + this.submissionClassInfo = transformationContext.getSubmissionClassInfo(submissionClassName); + this.originalClassHeader = submissionClassInfo.getOriginalClassHeader(); + this.computedClassHeader = submissionClassInfo.getComputedClassHeader(); + } + + /** + * Visits the header of the class, replacing it with the solution class' header, if one is present. + */ + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + ClassHeader classHeader = submissionClassInfo.getSolutionClass() + .map(SolutionClassNode::getClassHeader) + .orElse(originalClassHeader); + List classHeaderInterfaces = List.of(classHeader.interfaces()); + String[] additionalInterfaces = Arrays.stream(interfaces) + .map(transformationContext::toComputedInternalName) + .filter(Predicate.not(classHeaderInterfaces::contains)) + .toArray(String[]::new); + + classHeader.visitClass(getDelegate(), version, additionalInterfaces); + if (submissionClassInfo.getOriginalMethodHeaders().stream().anyMatch(mh -> mh.name().equals(""))) { + Constants.INJECTED_ORIGINAL_STATIC_FIELD_VALUES.toFieldVisitor(getDelegate(), null); + } + } + + /** + * Visits a field of the submission class and transforms it if a solution class is present. + */ + @Override + public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) { + FieldHeader fieldHeader = submissionClassInfo.getComputedFieldHeader(name); + + if (!TransformationUtils.contextIsCompatible(access, fieldHeader.access()) || + !transformationContext.descriptorIsCompatible(descriptor, fieldHeader.descriptor())) { + fieldHeader = new FieldHeader(computedClassHeader.name(), + TransformationUtils.transformAccess(access), + name + "$submission", + transformationContext.toComputedDescriptor(descriptor), + signature); + } + if ((computedClassHeader.access() & ACC_INTERFACE) != 0) { + fieldHeader = new FieldHeader(fieldHeader.owner(), + fieldHeader.access() | ACC_FINAL, + fieldHeader.name(), + fieldHeader.descriptor(), + fieldHeader.signature()); + } + if (value != null) { + staticFieldValues.put(fieldHeader.name(), new Pair<>(Type.getType(fieldHeader.descriptor()), value)); + } + visitedFields.add(fieldHeader); + return fieldHeader.toFieldVisitor(getDelegate(), value); + } + + /** + * Visits a method of a submission class and transforms it. + * Enables invocation logging, substitution and - if a solution class is present - delegation + * for non-lambda methods. + */ + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + if (TransformationUtils.isLambdaMethod(access, name)) { + MethodHeader methodHeader = new MethodHeader(originalClassHeader.name(), + access, + name, + transformationContext.toComputedDescriptor(descriptor), + signature, + exceptions); + return new LambdaMethodVisitor(methodHeader.toMethodVisitor(getDelegate()), + transformationContext, + submissionClassInfo, + methodHeader); + } else if (name.equals("")) { + MethodHeader methodHeader = submissionClassInfo.getComputedMethodHeader(name, descriptor); + visitedMethods.add(methodHeader); + return new ClassInitVisitor(methodHeader.toMethodVisitor(getDelegate()), + transformationContext, + submissionClassInfo); + } else { + MethodHeader originalMethodHeader = new MethodHeader(originalClassHeader.name(), access, name, descriptor, signature, exceptions); + MethodHeader computedMethodHeader = submissionClassInfo.getComputedMethodHeader(name, descriptor); + + if (!TransformationUtils.contextIsCompatible(access, computedMethodHeader.access()) || + !transformationContext.descriptorIsCompatible(descriptor, computedMethodHeader.descriptor())) { + computedMethodHeader = new MethodHeader(computedMethodHeader.owner(), + TransformationUtils.transformAccess(access), + name + "$submission", + transformationContext.toComputedDescriptor(descriptor), + signature, + exceptions); + } + visitedMethods.add(computedMethodHeader); + return new SubmissionMethodVisitor(computedMethodHeader.toMethodVisitor(getDelegate()), + transformationContext, + submissionClassInfo, + originalMethodHeader, + computedMethodHeader); + } + } + + /** + * Adds all remaining fields and methods from the solution class that have not already + * been visited (e.g., lambdas). + * Injects methods for retrieving the original class, field and method headers during runtime. + */ + @Override + public void visitEnd() { + Optional solutionClass = submissionClassInfo.getSolutionClass(); + if (solutionClass.isPresent()) { + // add missing fields + solutionClass.get() + .getFields() + .entrySet() + .stream() + .filter(entry -> !visitedFields.contains(entry.getKey())) + .map(Map.Entry::getValue) + .forEach(fieldNode -> fieldNode.accept(getDelegate())); + // add missing methods (including lambdas) + solutionClass.get() + .getMethods() + .entrySet() + .stream() + .filter(entry -> !visitedMethods.contains(entry.getKey())) + .forEach(entry -> { + MethodHeader methodHeader = entry.getKey(); + MethodNode methodNode = entry.getValue(); + + if (TransformationUtils.isLambdaMethod(methodHeader.access(), methodHeader.name())) { + methodNode.accept(getDelegate()); + } else { + MethodVisitor mv = methodHeader.toMethodVisitor(getDelegate()); + methodNode.accept(new MissingMethodVisitor(mv, transformationContext, submissionClassInfo, methodHeader)); + } + }); + } + + injectClassMetadata(); + injectFieldMetadata(); + injectMethodMetadata(); + if (submissionClassInfo.getOriginalMethodHeaders().stream().anyMatch(mh -> mh.name().equals(""))) { + injectStaticFieldValuesGetter(); + } else if (!staticFieldValues.isEmpty()) { + FieldHeader fieldHeader = Constants.INJECTED_ORIGINAL_STATIC_FIELD_VALUES; + Type hashMapType = Type.getType(HashMap.class); + + fieldHeader.toFieldVisitor(getDelegate(), null); + MethodVisitor mv = super.visitMethod(ACC_STATIC, "", "()V", null, null); + mv.visitTypeInsn(NEW, hashMapType.getInternalName()); + mv.visitInsn(DUP); + mv.visitMethodInsn(INVOKESPECIAL, hashMapType.getInternalName(), "", "()V", false); + mv.visitFieldInsn(PUTSTATIC, computedClassHeader.name(), fieldHeader.name(), fieldHeader.descriptor()); + mv.visitInsn(RETURN); + mv.visitMaxs(2, 0); + + injectStaticFieldValuesGetter(); + } + + transformationContext.addVisitedClass(computedClassHeader.name()); + super.visitEnd(); + } + + /** + * Injects a static method {@code getOriginalClassHeader()} into the submission class. + * This injected method returns the original class header of the class pre-transformation. + */ + private void injectClassMetadata() { + MethodVisitor mv = Constants.INJECTED_GET_ORIGINAL_CLASS_HEADER.toMethodVisitor(getDelegate()); + + int maxStack = originalClassHeader.buildHeader(mv); + mv.visitInsn(ARETURN); + mv.visitMaxs(maxStack, 0); + } + + /** + * Injects a static method {@code getOriginalFieldHeaders()} into the submission class. + * This injected method returns the set of original field headers of the class pre-transformation. + */ + private void injectFieldMetadata() { + Set fieldHeaders = submissionClassInfo.getOriginalFieldHeaders() + .stream() + .filter(fieldHeader -> (fieldHeader.access() & ACC_SYNTHETIC) == 0) + .collect(Collectors.toSet()); + int maxStack, stackSize; + MethodVisitor mv = Constants.INJECTED_GET_ORIGINAL_FIELD_HEADERS.toMethodVisitor(getDelegate()); + + mv.visitIntInsn(SIPUSH, fieldHeaders.size()); + mv.visitTypeInsn(ANEWARRAY, Type.getInternalName(Object.class)); + maxStack = stackSize = 1; + int i = 0; + for (FieldHeader fieldHeader : fieldHeaders) { + mv.visitInsn(DUP); + maxStack = Math.max(maxStack, ++stackSize); + mv.visitIntInsn(SIPUSH, i++); + maxStack = Math.max(maxStack, ++stackSize); + int stackSizeUsed = fieldHeader.buildHeader(mv); + maxStack = Math.max(maxStack, stackSize++ + stackSizeUsed); + mv.visitInsn(AASTORE); + stackSize -= 3; + } + mv.visitMethodInsn(INVOKESTATIC, + Constants.SET_TYPE.getInternalName(), + "of", + Type.getMethodDescriptor(Constants.SET_TYPE, Type.getType(Object[].class)), + true); + mv.visitInsn(ARETURN); + mv.visitMaxs(maxStack, 0); + } + + /** + * Injects a static method {@code getOriginalMethodHeaders()} into the submission class. + * This injected method returns the set of original method headers of the class pre-transformation. + */ + private void injectMethodMetadata() { + Set methodHeaders = submissionClassInfo.getOriginalMethodHeaders() + .stream() + .filter(methodHeader -> (methodHeader.access() & ACC_SYNTHETIC) == 0) + .collect(Collectors.toSet()); + int maxStack, stackSize; + MethodVisitor mv = Constants.INJECTED_GET_ORIGINAL_METHODS_HEADERS.toMethodVisitor(getDelegate()); + + mv.visitIntInsn(SIPUSH, methodHeaders.size()); + mv.visitTypeInsn(ANEWARRAY, Type.getInternalName(Object.class)); + maxStack = stackSize = 1; + int i = 0; + for (MethodHeader methodHeader : methodHeaders) { + mv.visitInsn(DUP); + maxStack = Math.max(maxStack, ++stackSize); + mv.visitIntInsn(SIPUSH, i++); + maxStack = Math.max(maxStack, ++stackSize); + int stackSizeUsed = methodHeader.buildHeader(mv); + maxStack = Math.max(maxStack, stackSize++ + stackSizeUsed); + mv.visitInsn(AASTORE); + stackSize -= 3; + } + mv.visitMethodInsn(INVOKESTATIC, + Constants.SET_TYPE.getInternalName(), + "of", + Type.getMethodDescriptor(Constants.SET_TYPE, Type.getType(Object[].class)), + true); + mv.visitInsn(ARETURN); + mv.visitMaxs(maxStack, 0); + } + + private void injectStaticFieldValuesGetter() { + Type hashMapType = Type.getType(HashMap.class); + MethodVisitor mv = Constants.INJECTED_GET_ORIGINAL_STATIC_FIELD_VALUES.toMethodVisitor(getDelegate()); + AtomicInteger maxStack = new AtomicInteger(); + + mv.visitTypeInsn(NEW, hashMapType.getInternalName()); + mv.visitInsn(DUP); + mv.visitFieldInsn(GETSTATIC, + computedClassHeader.name(), + Constants.INJECTED_ORIGINAL_STATIC_FIELD_VALUES.name(), + Constants.INJECTED_ORIGINAL_STATIC_FIELD_VALUES.descriptor()); + maxStack.set(3); + mv.visitMethodInsn(INVOKESPECIAL, + hashMapType.getInternalName(), + "", + Type.getMethodDescriptor(Type.VOID_TYPE, Constants.MAP_TYPE), + false); + + staticFieldValues.forEach((name, pair) -> { + mv.visitInsn(DUP); + mv.visitLdcInsn(name); + mv.visitLdcInsn(pair.getSecond()); + TransformationUtils.boxType(mv, pair.getFirst()); + mv.visitMethodInsn(INVOKEINTERFACE, + Constants.MAP_TYPE.getInternalName(), + "put", + Type.getMethodDescriptor(Constants.OBJECT_TYPE, Constants.OBJECT_TYPE, Constants.OBJECT_TYPE), + true); + mv.visitInsn(POP); + maxStack.set(4); + }); + + mv.visitInsn(ARETURN); + mv.visitMaxs(maxStack.get(), 0); + } +} diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/SubmissionEnumClassVisitor.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/SubmissionEnumClassVisitor.java new file mode 100644 index 00000000..2f080ec7 --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/SubmissionEnumClassVisitor.java @@ -0,0 +1,235 @@ +package org.tudalgo.algoutils.transform.classes; + +import org.objectweb.asm.*; +import org.tudalgo.algoutils.transform.methods.SubmissionMethodVisitor; +import org.tudalgo.algoutils.transform.util.Constants; +import org.tudalgo.algoutils.transform.util.TransformationContext; +import org.tudalgo.algoutils.transform.util.TransformationUtils; +import org.tudalgo.algoutils.transform.util.headers.FieldHeader; +import org.tudalgo.algoutils.transform.util.headers.MethodHeader; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import static org.objectweb.asm.Opcodes.*; + +public class SubmissionEnumClassVisitor extends SubmissionClassVisitor { + + private final MethodHeader classInitHeader = new MethodHeader(computedClassHeader.name(), + ACC_STATIC, "", "()V", null, null); + private final Set enumConstants = new HashSet<>(); + private final SolutionClassNode solutionClassNode; + + /** + * Constructs a new {@link SubmissionEnumClassVisitor} instance. + * + * @param classVisitor the class visitor to delegate to + * @param transformationContext the transformation context + * @param submissionClassName the name of the submission class that is visited + */ + public SubmissionEnumClassVisitor(ClassVisitor classVisitor, + TransformationContext transformationContext, + String submissionClassName) { + super(classVisitor, transformationContext, submissionClassName); + + this.solutionClassNode = submissionClassInfo.getSolutionClass().orElse(null); + } + + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + super.visit(version, access, name, signature, superName, interfaces); + Constants.INJECTED_ORIGINAL_ENUM_CONSTANTS.toFieldVisitor(getDelegate(), null); + + if (solutionClassNode != null) { + solutionClassNode.getFields() + .entrySet() + .stream() + .filter(entry -> (entry.getKey().access() & ACC_ENUM) != 0) + .forEach(entry -> { + visitedFields.add(entry.getKey()); + entry.getValue().accept(getDelegate()); + }); + } + } + + @Override + public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) { + if ((access & ACC_ENUM) != 0 && solutionClassNode != null) { + enumConstants.add(name); + return null; + } else { + return super.visitField(access, name, descriptor, signature, value); + } + } + + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + if (name.equals("")) { + visitedMethods.add(classInitHeader); + return new ClassInitVisitor(getDelegate().visitMethod(access, name, descriptor, signature, exceptions)); + } else { + return super.visitMethod(access, name, descriptor, signature, exceptions); + } + } + + @Override + public void visitEnd() { + MethodVisitor mv = Constants.INJECTED_GET_ORIGINAL_ENUM_CONSTANTS.toMethodVisitor(getDelegate()); + mv.visitFieldInsn(GETSTATIC, + computedClassHeader.name(), + Constants.INJECTED_ORIGINAL_ENUM_CONSTANTS.name(), + Constants.INJECTED_ORIGINAL_ENUM_CONSTANTS.descriptor()); + mv.visitInsn(ARETURN); + mv.visitMaxs(1, 0); + + super.visitEnd(); + } + + private class ClassInitVisitor extends SubmissionMethodVisitor { + + private ClassInitVisitor(MethodVisitor delegate) { + super(delegate, + SubmissionEnumClassVisitor.this.transformationContext, + SubmissionEnumClassVisitor.this.submissionClassInfo, + classInitHeader, + classInitHeader); + } + + @Override + public void visitCode() { + FieldHeader fieldHeader = Constants.INJECTED_ORIGINAL_ENUM_CONSTANTS; + Type arrayListType = Type.getType(ArrayList.class); + + delegate.visitTypeInsn(NEW, arrayListType.getInternalName()); + delegate.visitInsn(DUP); + delegate.visitMethodInsn(INVOKESPECIAL, + arrayListType.getInternalName(), + "", + "()V", + false); + delegate.visitFieldInsn(PUTSTATIC, + computedClassHeader.name(), + fieldHeader.name(), + fieldHeader.descriptor()); + } + + @Override + public void visitFieldInsn(int opcode, String owner, String name, String descriptor) { + if (!(owner.equals(originalClassHeader.name()) && (enumConstants.contains(name) || name.equals("$VALUES"))) || + solutionClassNode == null) { + super.visitFieldInsn(opcode, owner, name, descriptor); + } + } + + @Override + public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) { + if (solutionClassNode == null) { + super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); + return; + } + + if (opcode == INVOKESPECIAL && owner.equals(originalClassHeader.name()) && name.equals("")) { + Type[] argTypes = Type.getArgumentTypes(descriptor); + Label invocationStart = new Label(); + Label invocationEnd = new Label(); + + injectInvocation(argTypes, false); + delegate.visitVarInsn(ASTORE, fullFrameLocals.size()); + delegate.visitLabel(invocationStart); + delegate.visitTypeInsn(NEW, Constants.ENUM_CONSTANT_TYPE.getInternalName()); + delegate.visitInsn(DUP); + + delegate.visitVarInsn(ALOAD, fullFrameLocals.size()); + delegate.visitInsn(ICONST_0); + delegate.visitMethodInsn(INVOKEVIRTUAL, + Constants.INVOCATION_TYPE.getInternalName(), + "getParameter", + Type.getMethodDescriptor(Constants.OBJECT_TYPE, Type.INT_TYPE), + false); + delegate.visitTypeInsn(CHECKCAST, Constants.STRING_TYPE.getInternalName()); + + delegate.visitVarInsn(ALOAD, fullFrameLocals.size()); + delegate.visitInsn(ICONST_1); + delegate.visitMethodInsn(INVOKEVIRTUAL, + Constants.INVOCATION_TYPE.getInternalName(), + "getIntParameter", + Type.getMethodDescriptor(Type.INT_TYPE, Type.INT_TYPE), + false); + + delegate.visitIntInsn(SIPUSH, argTypes.length - 2); + delegate.visitTypeInsn(ANEWARRAY, Constants.OBJECT_TYPE.getInternalName()); + for (int i = 2; i < argTypes.length; i++) { + delegate.visitInsn(DUP); + delegate.visitIntInsn(SIPUSH, i - 2); + delegate.visitVarInsn(ALOAD, fullFrameLocals.size()); + delegate.visitIntInsn(SIPUSH, i); + delegate.visitMethodInsn(INVOKEVIRTUAL, + Constants.INVOCATION_TYPE.getInternalName(), + "getParameter", + Type.getMethodDescriptor(Constants.OBJECT_TYPE, Type.INT_TYPE), + false); + delegate.visitInsn(AASTORE); + } + + delegate.visitMethodInsn(INVOKESPECIAL, + Constants.ENUM_CONSTANT_TYPE.getInternalName(), + "", + Type.getMethodDescriptor(Type.VOID_TYPE, Constants.STRING_TYPE, Type.INT_TYPE, Constants.OBJECT_ARRAY_TYPE), + false); + delegate.visitFieldInsn(GETSTATIC, + computedClassHeader.name(), + Constants.INJECTED_ORIGINAL_ENUM_CONSTANTS.name(), + Constants.INJECTED_ORIGINAL_ENUM_CONSTANTS.descriptor()); + delegate.visitInsn(SWAP); + delegate.visitMethodInsn(INVOKEINTERFACE, + Constants.LIST_TYPE.getInternalName(), + "add", + Type.getMethodDescriptor(Type.BOOLEAN_TYPE, Constants.OBJECT_TYPE), + true); + delegate.visitInsn(POP); + + delegate.visitLabel(invocationEnd); + delegate.visitLocalVariable("invocation$injected", + Constants.INVOCATION_TYPE.getDescriptor(), + null, + invocationStart, + invocationEnd, + fullFrameLocals.size()); + + for (int i = argTypes.length - 1; i >= 0; i--) { + delegate.visitInsn(TransformationUtils.isCategory2Type(argTypes[i]) ? POP2 : POP); + } + delegate.visitInsn(POP2); // remove the new ref and its duplicate + } else if (!(owner.equals(originalClassHeader.name()) && name.equals("$values"))) { + super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); + } + } + + @Override + public void visitInsn(int opcode) { + if (opcode != RETURN || solutionClassNode == null) super.visitInsn(opcode); + } + + @Override + public void visitMaxs(int maxStack, int maxLocals) { + if (solutionClassNode == null) super.visitMaxs(maxStack, maxLocals); + } + + @Override + public void visitEnd() { + if (solutionClassNode != null) { + solutionClassNode.getMethods() + .entrySet() + .stream() + .filter(entry -> entry.getKey().name().equals("")) + .findAny() + .map(Map.Entry::getValue) + .ifPresent(methodNode -> methodNode.accept(delegate)); + } + + super.visitEnd(); + } + } +} diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/methods/BaseMethodVisitor.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/methods/BaseMethodVisitor.java new file mode 100644 index 00000000..a2495f51 --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/methods/BaseMethodVisitor.java @@ -0,0 +1,636 @@ +package org.tudalgo.algoutils.transform.methods; + +import org.objectweb.asm.*; +import org.objectweb.asm.tree.MethodNode; +import org.tudalgo.algoutils.transform.classes.ClassInfo; +import org.tudalgo.algoutils.transform.classes.SubmissionClassInfo; +import org.tudalgo.algoutils.transform.util.*; +import org.tudalgo.algoutils.transform.util.headers.FieldHeader; +import org.tudalgo.algoutils.transform.util.headers.MethodHeader; + +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.objectweb.asm.Opcodes.*; +import static org.tudalgo.algoutils.transform.util.TransformationUtils.boxType; +import static org.tudalgo.algoutils.transform.util.TransformationUtils.unboxType; + +public abstract class BaseMethodVisitor extends MethodVisitor { + + protected final MethodVisitor delegate; + protected final TransformationContext transformationContext; + protected final ClassInfo classInfo; + protected final MethodHeader originalMethodHeader; + protected final MethodHeader computedMethodHeader; + + protected final boolean headerMismatch; + protected final boolean isStatic; + protected final boolean isConstructor; + + protected final int nextLocalsIndex; + protected final List fullFrameLocals; + + protected BaseMethodVisitor(MethodVisitor delegate, + TransformationContext transformationContext, + ClassInfo classInfo, + MethodHeader originalMethodHeader, + MethodHeader computedMethodHeader) { + super(ASM9, delegate); + this.delegate = delegate; + this.transformationContext = transformationContext; + this.classInfo = classInfo; + this.originalMethodHeader = originalMethodHeader; + this.computedMethodHeader = computedMethodHeader; + + // Prevent bytecode to be added to the method if there is a header mismatch + this.headerMismatch = !transformationContext.descriptorIsCompatible(originalMethodHeader.descriptor(), + computedMethodHeader.descriptor()); + this.isStatic = (computedMethodHeader.access() & ACC_STATIC) != 0; + this.isConstructor = computedMethodHeader.name().equals(""); + + // calculate length of locals array, including "this" if applicable + this.nextLocalsIndex = (Type.getArgumentsAndReturnSizes(computedMethodHeader.descriptor()) >> 2) - (isStatic ? 1 : 0); + + this.fullFrameLocals = Arrays.stream(Type.getArgumentTypes(computedMethodHeader.descriptor())) + .map(type -> switch (type.getSort()) { + case Type.BOOLEAN, Type.BYTE, Type.SHORT, Type.CHAR, Type.INT -> INTEGER; + case Type.FLOAT -> FLOAT; + case Type.LONG -> LONG; + case Type.DOUBLE -> DOUBLE; + default -> type.getInternalName(); + }) + .collect(Collectors.toList()); + if (!isStatic) { + this.fullFrameLocals.addFirst(isConstructor ? UNINITIALIZED_THIS : computedMethodHeader.owner()); + } + } + + public enum LocalsObject { + METHOD_HEADER("methodHeader", Constants.METHOD_HEADER_TYPE.getDescriptor()), + METHOD_SUBSTITUTION("methodSubstitution", Constants.METHOD_SUBSTITUTION_TYPE.getDescriptor()), + CONSTRUCTOR_INVOCATION("constructorInvocation", Constants.METHOD_SUBSTITUTION_CONSTRUCTOR_INVOCATION_TYPE.getDescriptor()); + + private final String varName; + private final String descriptor; + + LocalsObject(String varName, String descriptor) { + this.varName = varName; + this.descriptor = descriptor; + } + + public String varName() { + return varName; + } + + public String descriptor() { + return descriptor; + } + + public void visitLocalVariable(BaseMethodVisitor bmv, Label start, Label end) { + bmv.visitLocalVariable(varName, descriptor, null, start, end, bmv.getLocalsIndex(this)); + } + } + + protected abstract int getLocalsIndex(LocalsObject localsObject); + + protected void injectSetupCode(Label methodHeaderVarLabel) { + // replicate method header in bytecode and store in locals array + computedMethodHeader.buildHeader(delegate); + delegate.visitVarInsn(ASTORE, getLocalsIndex(LocalsObject.METHOD_HEADER)); + delegate.visitLabel(methodHeaderVarLabel); + + delegate.visitFrame(F_APPEND, 1, new Object[] {computedMethodHeader.getHeaderType().getInternalName()}, 0, null); + fullFrameLocals.add(computedMethodHeader.getHeaderType().getInternalName()); + } + + protected void injectInvocationLoggingCode(Label nextLabel) { + // check if invocation should be logged + delegate.visitVarInsn(ALOAD, getLocalsIndex(LocalsObject.METHOD_HEADER)); + Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_LOG_INVOCATION.toMethodInsn(delegate, false); + delegate.visitJumpInsn(IFEQ, nextLabel); // jump to label if logInvocation(...) == false + + // intercept parameters + delegate.visitVarInsn(ALOAD, getLocalsIndex(LocalsObject.METHOD_HEADER)); + injectInvocation(Type.getArgumentTypes(computedMethodHeader.descriptor()), true); + Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_ADD_INVOCATION.toMethodInsn(delegate, false); + } + + protected void injectSubstitutionCode(Label substitutionCheckLabel, Label nextLabel) { + Label substitutionStartLabel = new Label(); + Label substitutionEndLabel = new Label(); + + // check if substitution exists for this method + delegate.visitFrame(F_SAME, 0, null, 0, null); + delegate.visitLabel(substitutionCheckLabel); + delegate.visitVarInsn(ALOAD, getLocalsIndex(LocalsObject.METHOD_HEADER)); + Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_USE_SUBSTITUTION.toMethodInsn(delegate, false); + delegate.visitJumpInsn(IFEQ, nextLabel); // jump to label if useSubstitution(...) == false + + // get substitution and execute it + delegate.visitVarInsn(ALOAD, getLocalsIndex(LocalsObject.METHOD_HEADER)); + Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_GET_SUBSTITUTION.toMethodInsn(delegate, false); + delegate.visitVarInsn(ASTORE, getLocalsIndex(LocalsObject.METHOD_SUBSTITUTION)); + delegate.visitFrame(F_APPEND, 1, new Object[] {Constants.METHOD_SUBSTITUTION_TYPE.getInternalName()}, 0, null); + fullFrameLocals.add(Constants.METHOD_SUBSTITUTION_TYPE.getInternalName()); + delegate.visitLabel(substitutionStartLabel); + + if (isConstructor) { + List superConstructors = classInfo.getOriginalSuperClassConstructorHeaders() + .stream() + .map(mh -> classInfo.getComputedSuperClassConstructorHeader(mh.descriptor())) + .toList(); + List constructors = classInfo.getOriginalMethodHeaders() + .stream() + .filter(mh -> mh.name().equals("")) + .map(mh -> classInfo.getComputedMethodHeader(mh.name(), mh.descriptor())) + .filter(mh -> !mh.descriptor().equals(computedMethodHeader.descriptor())) + .toList(); + Label[] labels = Stream.generate(Label::new) + .limit(superConstructors.size() + constructors.size() + 1) + .toArray(Label[]::new); + Label substitutionExecuteLabel = new Label(); + AtomicInteger labelIndex = new AtomicInteger(); + + /* + * Representation in source code: + * MethodSubstitution.ConstructorInvocation cb = methodSubstitution.getConstructorInvocation(); + * if (cb.owner().equals() && cb.descriptor().equals()) { + * super(...); + * } else if ... // for every superclass constructor + * else if (cb.owner().equals() && cb.descriptor().equals()) { + * this(...); + * } else if ... // for every regular constructor + * else { + * throw new IllegalArgumentException(...); + * } + */ + delegate.visitVarInsn(ALOAD, getLocalsIndex(LocalsObject.METHOD_SUBSTITUTION)); + Constants.METHOD_SUBSTITUTION_GET_CONSTRUCTOR_INVOCATION.toMethodInsn(delegate, true); + delegate.visitVarInsn(ASTORE, getLocalsIndex(LocalsObject.CONSTRUCTOR_INVOCATION)); + delegate.visitFrame(F_APPEND, 1, new Object[] {Constants.METHOD_SUBSTITUTION_CONSTRUCTOR_INVOCATION_TYPE.getInternalName()}, 0, null); + fullFrameLocals.add(Constants.METHOD_SUBSTITUTION_CONSTRUCTOR_INVOCATION_TYPE.getInternalName()); + delegate.visitLabel(labels[0]); + for (MethodHeader superConstructorHeader : superConstructors) { + injectConstructorInvocationBranch(superConstructorHeader, substitutionExecuteLabel, labels, labelIndex); + } + for (MethodHeader constructorHeader : constructors) { + injectConstructorInvocationBranch(constructorHeader, substitutionExecuteLabel, labels, labelIndex); + } + + // if no matching constructor was found, throw an IllegalArgumentException + { + Type illegalArgumentExceptionType = Type.getType(IllegalArgumentException.class); + delegate.visitTypeInsn(NEW, illegalArgumentExceptionType.getInternalName()); + delegate.visitInsn(DUP); + + delegate.visitLdcInsn("No matching constructor was found for owner %s and descriptor %s"); + delegate.visitInsn(ICONST_2); + delegate.visitTypeInsn(ANEWARRAY, Constants.STRING_TYPE.getInternalName()); + delegate.visitInsn(DUP); + delegate.visitInsn(ICONST_0); + delegate.visitVarInsn(ALOAD, getLocalsIndex(LocalsObject.CONSTRUCTOR_INVOCATION)); + Constants.METHOD_SUBSTITUTION_CONSTRUCTOR_INVOCATION_OWNER.toMethodInsn(delegate, false); + delegate.visitInsn(AASTORE); + delegate.visitInsn(DUP); + delegate.visitInsn(ICONST_1); + delegate.visitVarInsn(ALOAD, getLocalsIndex(LocalsObject.CONSTRUCTOR_INVOCATION)); + Constants.METHOD_SUBSTITUTION_CONSTRUCTOR_INVOCATION_DESCRIPTOR.toMethodInsn(delegate, false); + delegate.visitInsn(AASTORE); + delegate.visitMethodInsn(INVOKEVIRTUAL, + Constants.STRING_TYPE.getInternalName(), + "formatted", + Type.getMethodDescriptor(Constants.STRING_TYPE, Constants.OBJECT_ARRAY_TYPE), + false); + + delegate.visitMethodInsn(INVOKESPECIAL, + illegalArgumentExceptionType.getInternalName(), + "", + Type.getMethodDescriptor(Type.VOID_TYPE, Constants.STRING_TYPE), + false); + delegate.visitInsn(ATHROW); + } + + fullFrameLocals.removeLast(); + List locals = new ArrayList<>(fullFrameLocals); + locals.set(0, computedMethodHeader.owner()); + delegate.visitFrame(F_FULL, locals.size(), locals.toArray(), 0, null); + delegate.visitLabel(substitutionExecuteLabel); + LocalsObject.CONSTRUCTOR_INVOCATION.visitLocalVariable(this, labels[labelIndex.get()], substitutionExecuteLabel); + } + + delegate.visitVarInsn(ALOAD, getLocalsIndex(LocalsObject.METHOD_SUBSTITUTION)); + injectInvocation(Type.getArgumentTypes(computedMethodHeader.descriptor()), true); + Constants.METHOD_SUBSTITUTION_EXECUTE.toMethodInsn(delegate, true); + Type returnType = Type.getReturnType(computedMethodHeader.descriptor()); + unboxType(delegate, returnType); + delegate.visitInsn(returnType.getOpcode(IRETURN)); + delegate.visitLabel(substitutionEndLabel); + LocalsObject.METHOD_SUBSTITUTION.visitLocalVariable(this, substitutionStartLabel, substitutionEndLabel); + fullFrameLocals.removeLast(); + } + + protected void injectDelegationCode(MethodNode solutionMethodNode, + Label delegationCheckLabel, + Label submissionCodeLabel, + Label methodHeaderVarLabel) { + Label delegationCodeLabel = new Label(); + + // check if call should be delegated to solution or not + delegate.visitFrame(F_FULL, fullFrameLocals.size(), fullFrameLocals.toArray(), 0, new Object[0]); + delegate.visitLabel(delegationCheckLabel); + delegate.visitVarInsn(ALOAD, getLocalsIndex(LocalsObject.METHOD_HEADER)); + Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_USE_SUBMISSION_IMPL.toMethodInsn(delegate, false); + delegate.visitJumpInsn(IFNE, submissionCodeLabel); // jump to label if useSubmissionImpl(...) == true + + // replay instructions from solution + delegate.visitFrame(F_CHOP, 1, null, 0, null); + fullFrameLocals.removeLast(); + delegate.visitLabel(delegationCodeLabel); + LocalsObject.METHOD_HEADER.visitLocalVariable(this, methodHeaderVarLabel, delegationCodeLabel); + solutionMethodNode.accept(delegate); + + delegate.visitFrame(F_FULL, fullFrameLocals.size(), fullFrameLocals.toArray(), 0, new Object[0]); + delegate.visitLabel(submissionCodeLabel); + } + + protected void injectNoDelegationCode(Label submissionCodeLabel, Label methodHeaderVarLabel) { + fullFrameLocals.removeLast(); + delegate.visitFrame(F_FULL, fullFrameLocals.size(), fullFrameLocals.toArray(), 0, new Object[0]); + delegate.visitLabel(submissionCodeLabel); + LocalsObject.METHOD_HEADER.visitLocalVariable(this, methodHeaderVarLabel, submissionCodeLabel); + } + + /** + * Builds an {@link Invocation} in bytecode. + * + * @param argumentTypes an array of parameter types + * @param useLocals whether to use the locals array or the stack + */ + protected void injectInvocation(Type[] argumentTypes, boolean useLocals) { + Type threadType = Type.getType(Thread.class); + Type stackTraceElementArrayType = Type.getType(StackTraceElement[].class); + + delegate.visitTypeInsn(NEW, Constants.INVOCATION_TYPE.getInternalName()); + delegate.visitInsn(DUP); + delegate.visitLdcInsn(Type.getObjectType(computedMethodHeader.owner())); + computedMethodHeader.buildHeader(delegate); + delegate.visitMethodInsn(INVOKESTATIC, + threadType.getInternalName(), + "currentThread", + Type.getMethodDescriptor(threadType), + false); + delegate.visitMethodInsn(INVOKEVIRTUAL, + threadType.getInternalName(), + "getStackTrace", + Type.getMethodDescriptor(stackTraceElementArrayType), + false); + if (!isStatic && !isConstructor) { + delegate.visitVarInsn(ALOAD, 0); + Constants.INVOCATION_CONSTRUCTOR_WITH_INSTANCE.toMethodInsn(delegate, false); + } else { + Constants.INVOCATION_CONSTRUCTOR.toMethodInsn(delegate, false); + } + if (useLocals) { + // load parameter with opcode (ALOAD, ILOAD, etc.) for type and ignore "this", if it exists + for (int i = 0; i < argumentTypes.length; i++) { + delegate.visitInsn(DUP); + delegate.visitVarInsn(argumentTypes[i].getOpcode(ILOAD), TransformationUtils.getLocalsIndex(argumentTypes, i) + (isStatic ? 0 : 1)); + boxType(delegate, argumentTypes[i]); + Constants.INVOCATION_CONSTRUCTOR_ADD_PARAMETER.toMethodInsn(delegate, false); + } + } else { + Label invocationStart = new Label(); + Label invocationEnd = new Label(); + Label[] paramStartLabels = Stream.generate(Label::new).limit(argumentTypes.length).toArray(Label[]::new); + Label[] paramEndLabels = Stream.generate(Label::new).limit(argumentTypes.length).toArray(Label[]::new); + Map localsIndexes = new HashMap<>(); + + delegate.visitVarInsn(ASTORE, fullFrameLocals.size()); + delegate.visitLabel(invocationStart); + for (int i = argumentTypes.length - 1, category2Types = 0; i >= 0; i--) { + Type argType = argumentTypes[i]; + localsIndexes.put(i, fullFrameLocals.size() + category2Types + argumentTypes.length - i); + delegate.visitVarInsn(argType.getOpcode(ISTORE), localsIndexes.get(i)); + delegate.visitLabel(paramStartLabels[i]); + if (TransformationUtils.isCategory2Type(argType)) category2Types++; + } + + for (int i = 0; i < argumentTypes.length; i++) { + Type argType = argumentTypes[i]; + + delegate.visitVarInsn(ALOAD, fullFrameLocals.size()); + delegate.visitVarInsn(argType.getOpcode(ILOAD), localsIndexes.get(i)); + delegate.visitInsn(TransformationUtils.isCategory2Type(argType) ? DUP2_X1 : DUP_X1); + boxType(delegate, argType); + Constants.INVOCATION_CONSTRUCTOR_ADD_PARAMETER.toMethodInsn(delegate, false); + delegate.visitLabel(paramEndLabels[i]); + delegate.visitLocalVariable("var%d$injected".formatted(i), + argType.getDescriptor(), + null, + paramStartLabels[i], + paramEndLabels[i], + localsIndexes.get(i)); + } + delegate.visitVarInsn(ALOAD, fullFrameLocals.size()); + delegate.visitLabel(invocationEnd); + delegate.visitLocalVariable("invocation$injected", + Constants.INVOCATION_TYPE.getDescriptor(), + null, + invocationStart, + invocationEnd, + fullFrameLocals.size()); + } + } + + protected void injectConstructorInvocationBranch(MethodHeader constructorHeader, + Label substitutionExecuteLabel, + Label[] labels, + AtomicInteger labelIndex) { + delegate.visitVarInsn(ALOAD, getLocalsIndex(LocalsObject.CONSTRUCTOR_INVOCATION)); + Constants.METHOD_SUBSTITUTION_CONSTRUCTOR_INVOCATION_OWNER.toMethodInsn(delegate, false); + delegate.visitLdcInsn(constructorHeader.owner()); + delegate.visitMethodInsn(INVOKEVIRTUAL, + Constants.STRING_TYPE.getInternalName(), + "equals", + Type.getMethodDescriptor(Type.BOOLEAN_TYPE, Constants.OBJECT_TYPE), + false); + + delegate.visitVarInsn(ALOAD, getLocalsIndex(LocalsObject.CONSTRUCTOR_INVOCATION)); + Constants.METHOD_SUBSTITUTION_CONSTRUCTOR_INVOCATION_DESCRIPTOR.toMethodInsn(delegate, false); + delegate.visitLdcInsn(constructorHeader.descriptor()); + delegate.visitMethodInsn(INVOKEVIRTUAL, + Constants.STRING_TYPE.getInternalName(), + "equals", + Type.getMethodDescriptor(Type.BOOLEAN_TYPE, Constants.OBJECT_TYPE), + false); + + delegate.visitInsn(IAND); + delegate.visitJumpInsn(IFEQ, labels[labelIndex.get() + 1]); // jump to next branch if false + + Label argsVarStartLabel = new Label(); + delegate.visitVarInsn(ALOAD, getLocalsIndex(LocalsObject.CONSTRUCTOR_INVOCATION)); + Constants.METHOD_SUBSTITUTION_CONSTRUCTOR_INVOCATION_ARGS.toMethodInsn(delegate, false); + delegate.visitVarInsn(ASTORE, getLocalsIndex(LocalsObject.CONSTRUCTOR_INVOCATION) + 1); + delegate.visitFrame(F_APPEND, 1, new Object[] {Constants.OBJECT_ARRAY_TYPE.getInternalName()}, 0, null); + fullFrameLocals.add(Constants.OBJECT_ARRAY_TYPE.getInternalName()); + delegate.visitLabel(argsVarStartLabel); + + delegate.visitVarInsn(ALOAD, 0); + Type[] parameterTypes = Type.getArgumentTypes(constructorHeader.descriptor()); + for (int i = 0; i < parameterTypes.length; i++) { // unpack array + delegate.visitVarInsn(ALOAD, getLocalsIndex(LocalsObject.CONSTRUCTOR_INVOCATION) + 1); + delegate.visitIntInsn(SIPUSH, i); + delegate.visitInsn(AALOAD); + unboxType(delegate, parameterTypes[i]); + } + constructorHeader.toMethodInsn(delegate, false); + delegate.visitJumpInsn(GOTO, substitutionExecuteLabel); + + fullFrameLocals.removeLast(); + delegate.visitFrame(F_CHOP, 1, null, 0, new Object[0]); + delegate.visitLabel(labels[labelIndex.incrementAndGet()]); + delegate.visitLocalVariable("args", + Constants.OBJECT_ARRAY_TYPE.getDescriptor(), + null, + argsVarStartLabel, + labels[labelIndex.get()], + getLocalsIndex(LocalsObject.CONSTRUCTOR_INVOCATION) + 1); + } + + @Override + public void visitFieldInsn(int opcode, String owner, String name, String descriptor) { + if (headerMismatch) return; + + // skip transformation if owner is not part of the submission + if (!transformationContext.isSubmissionClass(owner)) { + delegate.visitFieldInsn(opcode, owner, name, descriptor); + } else { + FieldHeader computedFieldHeader = transformationContext.getSubmissionClassInfo(owner).getComputedFieldHeader(name); + if (TransformationUtils.opcodeIsCompatible(opcode, computedFieldHeader.access()) && + transformationContext.descriptorIsCompatible(descriptor, computedFieldHeader.descriptor())) { + delegate.visitFieldInsn(opcode, + transformationContext.toComputedInternalName(computedFieldHeader.owner()), + computedFieldHeader.name(), + computedFieldHeader.descriptor()); + } else { // if incompatible + delegate.visitFieldInsn(opcode, + computedFieldHeader.owner(), + name + "$submission", + transformationContext.toComputedDescriptor(descriptor)); + } + } + } + + @Override + public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) { + if (headerMismatch) return; + + MethodHeader methodHeader = new MethodHeader(owner, name, descriptor); + if (transformationContext.methodHasReplacement(methodHeader)) { + transformationContext.getMethodReplacement(methodHeader).toMethodInsn(delegate, false); + } else if (!transformationContext.isSubmissionClass(owner)) { + delegate.visitMethodInsn(opcode, owner, name, descriptor, isInterface); + } else if (owner.startsWith("[")) { + delegate.visitMethodInsn(opcode, transformationContext.toComputedInternalName(owner), name, descriptor, isInterface); + } else { + // methodHeader.owner() might have the wrong owner for inherited methods + String computedOwner = transformationContext.toComputedInternalName(owner); + methodHeader = transformationContext.getSubmissionClassInfo(owner).getComputedMethodHeader(name, descriptor); + if (TransformationUtils.opcodeIsCompatible(opcode, methodHeader.access()) && + transformationContext.descriptorIsCompatible(descriptor, methodHeader.descriptor())) { + delegate.visitMethodInsn(opcode, computedOwner, methodHeader.name(), methodHeader.descriptor(), isInterface); + } else { + delegate.visitMethodInsn(opcode, + computedOwner, + name + "$submission", + transformationContext.toComputedDescriptor(descriptor), + isInterface); + } + } + } + + @Override + public void visitLdcInsn(Object value) { + if (headerMismatch) return; + + super.visitLdcInsn(value instanceof Type type ? transformationContext.toComputedType(type) : value); + } + + @Override + public void visitTypeInsn(int opcode, String type) { + if (headerMismatch) return; + + super.visitTypeInsn(opcode, transformationContext.toComputedInternalName(type)); + } + + @Override + public void visitFrame(int type, int numLocal, Object[] local, int numStack, Object[] stack) { + if (headerMismatch) return; + + Object[] computedLocals = local == null ? null : Arrays.stream(local) + .map(o -> o instanceof String s ? transformationContext.toComputedInternalName(s) : o) + .toArray(); + Object[] computedStack = stack == null ? null : Arrays.stream(stack) + .map(o -> o instanceof String s ? transformationContext.toComputedInternalName(s) : o) + .toArray(); + super.visitFrame(type, numLocal, computedLocals, numStack, computedStack); + } + + @Override + public void visitInsn(int opcode) { + if (headerMismatch) return; + + super.visitInsn(opcode); + } + + @Override + public void visitIntInsn(int opcode, int operand) { + if (headerMismatch) return; + + super.visitIntInsn(opcode, operand); + } + + @Override + public void visitIincInsn(int varIndex, int increment) { + if (headerMismatch) return; + + super.visitIincInsn(varIndex, increment); + } + + @Override + public void visitVarInsn(int opcode, int varIndex) { + if (headerMismatch) return; + + super.visitVarInsn(opcode, varIndex); + } + + @Override + public void visitJumpInsn(int opcode, Label label) { + if (headerMismatch) return; + + super.visitJumpInsn(opcode, label); + } + + @Override + public void visitInvokeDynamicInsn(String name, String descriptor, Handle bootstrapMethodHandle, Object... bootstrapMethodArguments) { + if (headerMismatch) return; + + if (bootstrapMethodHandle.getOwner().equals("java/lang/invoke/LambdaMetafactory") && + bootstrapMethodHandle.getName().equals("metafactory")) { + /* + * Since this stuff is very confusing, some explanations... + * name: the name of the interface method to implement + * descriptor: + * arg types: types of the capture variables, i.e., variables that are used in the lambda expression + * but are not declared therein + * return type: the owner of the method that is implemented + * bootstrapMethodHandle: a method handle for the lambda metafactory, see docs on LambdaMetafactory + * bootstrapMethodArguments[0]: descriptor for the interface method that is implemented + * bootstrapMethodArguments[1]: a method handle for the actual implementation, i.e., + * the actual lambda method or some other method when using method references + * bootstrapMethodArguments[2]: the descriptor that should be enforced at invocation time, + * not sure if it includes the capture variables + */ + + String interfaceOwner = Type.getReturnType(descriptor).getInternalName(); + if (transformationContext.isSubmissionClass(interfaceOwner)) { + SubmissionClassInfo submissionClassInfo = transformationContext.getSubmissionClassInfo(interfaceOwner); + MethodHeader methodHeader = submissionClassInfo.getComputedMethodHeader(name, ((Type) bootstrapMethodArguments[0]).getDescriptor()); + name = methodHeader.name(); + bootstrapMethodArguments[0] = Type.getMethodType(methodHeader.descriptor()); + } + + Handle implementation = (Handle) bootstrapMethodArguments[1]; + if (transformationContext.isSubmissionClass(implementation.getOwner())) { + SubmissionClassInfo submissionClassInfo = transformationContext.getSubmissionClassInfo(implementation.getOwner()); + MethodHeader methodHeader = submissionClassInfo.getComputedMethodHeader(implementation.getName(), implementation.getDesc()); + bootstrapMethodArguments[1] = new Handle(implementation.getTag(), + methodHeader.owner(), + methodHeader.name(), + methodHeader.descriptor(), + implementation.isInterface()); + } + + bootstrapMethodArguments[2] = transformationContext.toComputedType((Type) bootstrapMethodArguments[2]); + } + + super.visitInvokeDynamicInsn(name, + transformationContext.toComputedDescriptor(descriptor), + bootstrapMethodHandle, + bootstrapMethodArguments); + } + + @Override + public void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels) { + if (headerMismatch) return; + + super.visitLookupSwitchInsn(dflt, keys, labels); + } + + @Override + public void visitTableSwitchInsn(int min, int max, Label dflt, Label... labels) { + if (headerMismatch) return; + + super.visitTableSwitchInsn(min, max, dflt, labels); + } + + @Override + public void visitMultiANewArrayInsn(String descriptor, int numDimensions) { + if (headerMismatch) return; + + super.visitMultiANewArrayInsn(transformationContext.toComputedDescriptor(descriptor), numDimensions); + } + + @Override + public void visitLabel(Label label) { + if (headerMismatch) return; + + super.visitLabel(label); + } + + @Override + public AnnotationVisitor visitInsnAnnotation(int typeRef, TypePath typePath, String descriptor, boolean visible) { + return headerMismatch ? null : super.visitInsnAnnotation(typeRef, typePath, descriptor, visible); + } + + @Override + public void visitTryCatchBlock(Label start, Label end, Label handler, String type) { + if (headerMismatch) return; + + super.visitTryCatchBlock(start, end, handler, type != null ? transformationContext.toComputedInternalName(type) : null); + } + + @Override + public AnnotationVisitor visitTryCatchAnnotation(int typeRef, TypePath typePath, String descriptor, boolean visible) { + return headerMismatch ? null : super.visitTryCatchAnnotation(typeRef, typePath, descriptor, visible); + } + + @Override + public void visitLocalVariable(String name, String descriptor, String signature, Label start, Label end, int index) { + if (headerMismatch && Arrays.stream(LocalsObject.values()).map(LocalsObject::varName).noneMatch(name::equals)) + return; + + super.visitLocalVariable(name, transformationContext.toComputedType(Type.getType(descriptor)).getDescriptor(), signature, start, end, index); + } + + @Override + public AnnotationVisitor visitLocalVariableAnnotation(int typeRef, TypePath typePath, Label[] start, Label[] end, int[] index, String descriptor, boolean visible) { + return headerMismatch ? null : super.visitLocalVariableAnnotation(typeRef, typePath, start, end, index, descriptor, visible); + } + + @Override + public void visitLineNumber(int line, Label start) { + if (headerMismatch) return; + + super.visitLineNumber(line, start); + } + + @Override + public void visitAttribute(Attribute attribute) { + if (headerMismatch) return; + + super.visitAttribute(attribute); + } +} diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/methods/ClassInitVisitor.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/methods/ClassInitVisitor.java new file mode 100644 index 00000000..516121b5 --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/methods/ClassInitVisitor.java @@ -0,0 +1,112 @@ +package org.tudalgo.algoutils.transform.methods; + +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.MethodNode; +import org.tudalgo.algoutils.transform.classes.SubmissionClassInfo; +import org.tudalgo.algoutils.transform.util.Constants; +import org.tudalgo.algoutils.transform.util.TransformationContext; +import org.tudalgo.algoutils.transform.util.TransformationUtils; +import org.tudalgo.algoutils.transform.util.headers.FieldHeader; +import org.tudalgo.algoutils.transform.util.headers.MethodHeader; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Set; + +import static org.objectweb.asm.Opcodes.*; + +public class ClassInitVisitor extends BaseMethodVisitor { + + private final Set solutionFields; + private final MethodNode solutionMethodNode; + + public ClassInitVisitor(MethodVisitor delegate, + TransformationContext transformationContext, + SubmissionClassInfo submissionClassInfo) { + super(delegate, + transformationContext, + submissionClassInfo, + getClassInitHeader(submissionClassInfo.getOriginalClassHeader().name()), + getClassInitHeader(submissionClassInfo.getComputedClassHeader().name())); + + this.solutionFields = submissionClassInfo.getSolutionClass() + .map(solutionClassNode -> solutionClassNode.getFields().keySet()) + .orElse(Collections.emptySet()); + this.solutionMethodNode = submissionClassInfo.getSolutionClass() + .map(solutionClassNode -> solutionClassNode.getMethods().get(computedMethodHeader)) + .orElse(null); + } + + // Unused + @Override + protected int getLocalsIndex(LocalsObject localsObject) { + return 0; + } + + @Override + public void visitCode() { + FieldHeader fieldHeader = Constants.INJECTED_ORIGINAL_STATIC_FIELD_VALUES; + Type hashMapType = Type.getType(HashMap.class); + + delegate.visitTypeInsn(NEW, hashMapType.getInternalName()); + delegate.visitInsn(DUP); + delegate.visitMethodInsn(INVOKESPECIAL, hashMapType.getInternalName(), "", "()V", false); + delegate.visitFieldInsn(PUTSTATIC, computedMethodHeader.owner(), fieldHeader.name(), fieldHeader.descriptor()); + } + + @Override + public void visitFieldInsn(int opcode, String owner, String name, String descriptor) { + if (opcode == PUTSTATIC && owner.equals(originalMethodHeader.owner())) { + FieldHeader originalStaticFieldValuesHeader = Constants.INJECTED_ORIGINAL_STATIC_FIELD_VALUES; + FieldHeader computedFieldHeader = classInfo.getComputedFieldHeader(name); + Type type = Type.getType(computedFieldHeader.descriptor()); + boolean isCategory2Type = TransformationUtils.isCategory2Type(type); + + delegate.visitInsn(isCategory2Type ? DUP2 : DUP); + delegate.visitFieldInsn(GETSTATIC, + computedMethodHeader.owner(), + originalStaticFieldValuesHeader.name(), + originalStaticFieldValuesHeader.descriptor()); + delegate.visitInsn(isCategory2Type ? DUP_X2 : DUP_X1); + delegate.visitInsn(POP); + delegate.visitLdcInsn(computedFieldHeader.name()); + delegate.visitInsn(isCategory2Type ? DUP_X2 : DUP_X1); + delegate.visitInsn(POP); + TransformationUtils.boxType(delegate, type); + delegate.visitMethodInsn(INVOKEINTERFACE, + Constants.MAP_TYPE.getInternalName(), + "put", + Type.getMethodDescriptor(Constants.OBJECT_TYPE, Constants.OBJECT_TYPE, Constants.OBJECT_TYPE), + true); + delegate.visitInsn(POP); + + if (solutionFields.contains(computedFieldHeader)) { + delegate.visitInsn(isCategory2Type ? POP2 : POP); + return; + } + } + super.visitFieldInsn(opcode, owner, name, descriptor); + } + + @Override + public void visitInsn(int opcode) { + if (opcode != RETURN || solutionMethodNode == null) super.visitInsn(opcode); + } + + @Override + public void visitMaxs(int maxStack, int maxLocals) { + if (solutionMethodNode == null) super.visitMaxs(maxStack, maxLocals); + } + + @Override + public void visitEnd() { + if (solutionMethodNode != null) { + solutionMethodNode.accept(delegate); + } + } + + private static MethodHeader getClassInitHeader(String owner) { + return new MethodHeader(owner, ACC_STATIC, "", "()V", null, null); + } +} diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/methods/LambdaMethodVisitor.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/methods/LambdaMethodVisitor.java new file mode 100644 index 00000000..09d86761 --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/methods/LambdaMethodVisitor.java @@ -0,0 +1,35 @@ +package org.tudalgo.algoutils.transform.methods; + +import org.objectweb.asm.MethodVisitor; +import org.tudalgo.algoutils.transform.classes.SubmissionClassInfo; +import org.tudalgo.algoutils.transform.util.headers.MethodHeader; +import org.tudalgo.algoutils.transform.util.TransformationContext; + +/** + * A method visitor for lambda methods in submission classes. + * This visitor does not inject any additional code, but it transforms the lambda's code + * so it doesn't cause any linkage errors. + */ +public class LambdaMethodVisitor extends BaseMethodVisitor { + + /** + * Constructs a new {@link LambdaMethodVisitor}. + * + * @param delegate the method visitor to delegate to + * @param transformationContext the transformation context + * @param submissionClassInfo information about the submission class this method belongs to + * @param methodHeader the (original) method header + */ + public LambdaMethodVisitor(MethodVisitor delegate, + TransformationContext transformationContext, + SubmissionClassInfo submissionClassInfo, + MethodHeader methodHeader) { + super(delegate, transformationContext, submissionClassInfo, methodHeader, methodHeader); + } + + // Unused + @Override + protected int getLocalsIndex(LocalsObject localsObject) { + return -1; + } +} diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/methods/MissingMethodVisitor.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/methods/MissingMethodVisitor.java new file mode 100644 index 00000000..3348de09 --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/methods/MissingMethodVisitor.java @@ -0,0 +1,87 @@ +package org.tudalgo.algoutils.transform.methods; + +import org.objectweb.asm.Label; +import org.objectweb.asm.MethodVisitor; +import org.tudalgo.algoutils.transform.classes.ClassInfo; +import org.tudalgo.algoutils.transform.util.*; +import org.tudalgo.algoutils.transform.util.headers.MethodHeader; + +import java.util.EnumMap; +import java.util.Map; + +import static org.objectweb.asm.Opcodes.*; +import static org.objectweb.asm.Opcodes.F_CHOP; + +public class MissingMethodVisitor extends BaseMethodVisitor { + + private final Map localsIndexes = new EnumMap<>(LocalsObject.class); + + public MissingMethodVisitor(MethodVisitor delegate, + TransformationContext transformationContext, + ClassInfo classInfo, + MethodHeader methodHeader) { + super(delegate, transformationContext, classInfo, methodHeader, methodHeader); + + localsIndexes.put(LocalsObject.METHOD_HEADER, nextLocalsIndex); + localsIndexes.put(LocalsObject.METHOD_SUBSTITUTION, nextLocalsIndex + 1); + localsIndexes.put(LocalsObject.CONSTRUCTOR_INVOCATION, nextLocalsIndex + 2); + } + + @Override + protected int getLocalsIndex(LocalsObject localsObject) { + return localsIndexes.get(localsObject); + } + + @Override + public void visitCode() { + Label methodHeaderVarLabel = new Label(); + Label substitutionCheckLabel = new Label(); + Label delegationCheckLabel = new Label(); + Label delegationCodeLabel = new Label(); + Label submissionCodeLabel = new Label(); + + // Setup + injectSetupCode(methodHeaderVarLabel); + + // Invocation logging + injectInvocationLoggingCode(substitutionCheckLabel); + + // Method substitution + injectSubstitutionCode(substitutionCheckLabel, delegationCheckLabel); + + // Method delegation + // check if call should be delegated to solution or not + delegate.visitFrame(F_FULL, fullFrameLocals.size(), fullFrameLocals.toArray(), 0, new Object[0]); + delegate.visitLabel(delegationCheckLabel); + delegate.visitVarInsn(ALOAD, getLocalsIndex(LocalsObject.METHOD_HEADER)); + Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_USE_SUBMISSION_IMPL.toMethodInsn(getDelegate(), false); + delegate.visitJumpInsn(IFEQ, submissionCodeLabel); // jump to label if useSubmissionImpl(...) == false + + // replay instructions from solution + delegate.visitFrame(F_CHOP, 1, null, 0, null); + fullFrameLocals.removeLast(); + delegate.visitLabel(delegationCodeLabel); + LocalsObject.METHOD_HEADER.visitLocalVariable(this, methodHeaderVarLabel, delegationCodeLabel); + IncompatibleHeaderException.replicateInBytecode(delegate, true, + "Method has incorrect return or parameter types", computedMethodHeader, null); + + delegate.visitFrame(F_SAME, 0, null, 0, null); + delegate.visitLabel(submissionCodeLabel); + delegate.visitCode(); + } + + @Override + public void visitFieldInsn(int opcode, String owner, String name, String descriptor) { + delegate.visitFieldInsn(opcode, owner, name, descriptor); + } + + @Override + public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) { + MethodHeader methodHeader = new MethodHeader(owner, name, descriptor); + if (transformationContext.methodHasReplacement(methodHeader)) { + transformationContext.getMethodReplacement(methodHeader).toMethodInsn(getDelegate(), false); + } else { + delegate.visitMethodInsn(opcode, owner, name, descriptor, isInterface); + } + } +} diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/methods/SubmissionMethodVisitor.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/methods/SubmissionMethodVisitor.java new file mode 100644 index 00000000..4b72982f --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/methods/SubmissionMethodVisitor.java @@ -0,0 +1,81 @@ +package org.tudalgo.algoutils.transform.methods; + +import org.objectweb.asm.*; +import org.objectweb.asm.tree.MethodNode; +import org.tudalgo.algoutils.transform.classes.SubmissionClassInfo; +import org.tudalgo.algoutils.transform.classes.SubmissionClassVisitor; +import org.tudalgo.algoutils.transform.util.*; +import org.tudalgo.algoutils.transform.util.headers.MethodHeader; + +import java.util.*; + +/** + * A method visitor for transforming submission methods. + * + * @see SubmissionClassVisitor + * @author Daniel Mangold + */ +public class SubmissionMethodVisitor extends BaseMethodVisitor { + + private final Map localsIndexes = new EnumMap<>(LocalsObject.class); + + /** + * Constructs a new {@link SubmissionMethodVisitor}. + * + * @param delegate the method visitor to delegate to + * @param transformationContext the transformation context + * @param submissionClassInfo information about the submission class this method belongs to + * @param originalMethodHeader the computed method header of this method + */ + public SubmissionMethodVisitor(MethodVisitor delegate, + TransformationContext transformationContext, + SubmissionClassInfo submissionClassInfo, + MethodHeader originalMethodHeader, + MethodHeader computedMethodHeader) { + super(delegate, transformationContext, submissionClassInfo, originalMethodHeader, computedMethodHeader); + + localsIndexes.put(LocalsObject.METHOD_HEADER, nextLocalsIndex); + localsIndexes.put(LocalsObject.METHOD_SUBSTITUTION, nextLocalsIndex + 1); + localsIndexes.put(LocalsObject.CONSTRUCTOR_INVOCATION, nextLocalsIndex + 2); + } + + @Override + protected int getLocalsIndex(LocalsObject localsObject) { + return localsIndexes.get(localsObject); + } + + @Override + public void visitCode() { + Optional solutionMethodNode = ((SubmissionClassInfo) classInfo).getSolutionClass() + .map(solutionClassNode -> solutionClassNode.getMethods().get(computedMethodHeader)); + Label methodHeaderVarLabel = new Label(); + Label substitutionCheckLabel = new Label(); + Label delegationCheckLabel = new Label(); + Label submissionCodeLabel = new Label(); + + // Setup + injectSetupCode(methodHeaderVarLabel); + + // Invocation logging + injectInvocationLoggingCode(substitutionCheckLabel); + + // Method substitution + injectSubstitutionCode(substitutionCheckLabel, solutionMethodNode.isPresent() ? delegationCheckLabel : submissionCodeLabel); + + // Method delegation + // if no solution method is present, skip delegation + if (solutionMethodNode.isPresent()) { + injectDelegationCode(solutionMethodNode.get(), delegationCheckLabel, submissionCodeLabel, methodHeaderVarLabel); + } else { + injectNoDelegationCode(submissionCodeLabel, methodHeaderVarLabel); + } + + if (headerMismatch) { + IncompatibleHeaderException.replicateInBytecode(delegate, true, + "Method has incorrect return or parameter types", computedMethodHeader, originalMethodHeader); + } else { + // visit original code + delegate.visitCode(); + } + } +} diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Constants.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Constants.java new file mode 100644 index 00000000..b4ab09cd --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Constants.java @@ -0,0 +1,127 @@ +package org.tudalgo.algoutils.transform.util; + +import org.objectweb.asm.Type; +import org.tudalgo.algoutils.student.annotation.ForceSignature; +import org.tudalgo.algoutils.transform.SubmissionExecutionHandler; +import org.tudalgo.algoutils.transform.util.headers.ClassHeader; +import org.tudalgo.algoutils.transform.util.headers.FieldHeader; +import org.tudalgo.algoutils.transform.util.headers.Header; +import org.tudalgo.algoutils.transform.util.headers.MethodHeader; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.objectweb.asm.Opcodes.ACC_PUBLIC; +import static org.objectweb.asm.Opcodes.ACC_STATIC; +import static org.objectweb.asm.Opcodes.ACC_FINAL; + +public final class Constants { + + // Types + + public static final Type OBJECT_TYPE = Type.getType(Object.class); + public static final Type OBJECT_ARRAY_TYPE = Type.getType(Object[].class); + public static final Type STRING_TYPE = Type.getType(String.class); + public static final Type STRING_ARRAY_TYPE = Type.getType(String[].class); + public static final Type SET_TYPE = Type.getType(Set.class); + public static final Type LIST_TYPE = Type.getType(List.class); + public static final Type MAP_TYPE = Type.getType(Map.class); + + public static final Type HEADER_TYPE = Type.getType(Header.class); + public static final Type CLASS_HEADER_TYPE = Type.getType(ClassHeader.class); + public static final Type FIELD_HEADER_TYPE = Type.getType(FieldHeader.class); + public static final Type METHOD_HEADER_TYPE = Type.getType(MethodHeader.class); + + public static final Type ENUM_CONSTANT_TYPE = Type.getType(EnumConstant.class); + public static final Type FORCE_SIGNATURE_TYPE = Type.getType(ForceSignature.class); + public static final Type INVOCATION_TYPE = Type.getType(Invocation.class); + public static final Type METHOD_SUBSTITUTION_TYPE = Type.getType(MethodSubstitution.class); + public static final Type METHOD_SUBSTITUTION_CONSTRUCTOR_INVOCATION_TYPE = Type.getType(MethodSubstitution.ConstructorInvocation.class); + + // Fields used in bytecode + + public static final FieldHeader INJECTED_ORIGINAL_STATIC_FIELD_VALUES = new FieldHeader(null, + ACC_PUBLIC | ACC_STATIC | ACC_FINAL, + "originalStaticFieldValues$injected", + MAP_TYPE.getDescriptor(), + "L%s<%s%s>;".formatted(MAP_TYPE.getInternalName(), STRING_TYPE.getDescriptor(), OBJECT_TYPE.getDescriptor())); + public static final FieldHeader INJECTED_ORIGINAL_ENUM_CONSTANTS = new FieldHeader(null, + ACC_PUBLIC | ACC_STATIC | ACC_FINAL, + "originalEnumConstants$injected", + LIST_TYPE.getDescriptor(), + "L%s<%s>;".formatted(LIST_TYPE.getInternalName(), ENUM_CONSTANT_TYPE.getDescriptor())); + + // Methods used in bytecode + + public static final MethodHeader INJECTED_GET_ORIGINAL_CLASS_HEADER = new MethodHeader(null, + ACC_PUBLIC | ACC_STATIC, + "getOriginalClassHeader", + Type.getMethodDescriptor(CLASS_HEADER_TYPE), + null, + null); + public static final MethodHeader INJECTED_GET_ORIGINAL_FIELD_HEADERS = new MethodHeader(null, + ACC_PUBLIC | ACC_STATIC, + "getOriginalFieldHeaders", + Type.getMethodDescriptor(SET_TYPE), + "()L%s<%s>;".formatted(SET_TYPE.getInternalName(), FIELD_HEADER_TYPE.getDescriptor()), + null); + public static final MethodHeader INJECTED_GET_ORIGINAL_METHODS_HEADERS = new MethodHeader(null, + ACC_PUBLIC | ACC_STATIC, + "getOriginalMethodHeaders", + Type.getMethodDescriptor(SET_TYPE), + "()L%s<%s>;".formatted(SET_TYPE.getInternalName(), METHOD_HEADER_TYPE.getDescriptor()), + null); + public static final MethodHeader INJECTED_GET_ORIGINAL_STATIC_FIELD_VALUES = new MethodHeader(null, + ACC_PUBLIC | ACC_STATIC, + "getOriginalStaticFieldValues", + Type.getMethodDescriptor(MAP_TYPE), + "()L%s<%s%s>;".formatted(MAP_TYPE.getInternalName(), STRING_TYPE.getDescriptor(), OBJECT_TYPE.getDescriptor()), + null); + public static final MethodHeader INJECTED_GET_ORIGINAL_ENUM_CONSTANTS = new MethodHeader(null, + ACC_PUBLIC | ACC_STATIC, + "getOriginalEnumConstants", + Type.getMethodDescriptor(LIST_TYPE), + "()L%s<%s>;".formatted(LIST_TYPE.getInternalName(), ENUM_CONSTANT_TYPE.getDescriptor()), + null); + + public static final MethodHeader SUBMISSION_EXECUTION_HANDLER_INTERNAL_LOG_INVOCATION; + public static final MethodHeader SUBMISSION_EXECUTION_HANDLER_INTERNAL_ADD_INVOCATION; + public static final MethodHeader SUBMISSION_EXECUTION_HANDLER_INTERNAL_USE_SUBSTITUTION; + public static final MethodHeader SUBMISSION_EXECUTION_HANDLER_INTERNAL_GET_SUBSTITUTION; + public static final MethodHeader SUBMISSION_EXECUTION_HANDLER_INTERNAL_USE_SUBMISSION_IMPL; + + public static final MethodHeader INVOCATION_CONSTRUCTOR; + public static final MethodHeader INVOCATION_CONSTRUCTOR_WITH_INSTANCE; + public static final MethodHeader INVOCATION_CONSTRUCTOR_ADD_PARAMETER; + + public static final MethodHeader METHOD_SUBSTITUTION_GET_CONSTRUCTOR_INVOCATION; + public static final MethodHeader METHOD_SUBSTITUTION_EXECUTE; + public static final MethodHeader METHOD_SUBSTITUTION_CONSTRUCTOR_INVOCATION_OWNER; + public static final MethodHeader METHOD_SUBSTITUTION_CONSTRUCTOR_INVOCATION_DESCRIPTOR; + public static final MethodHeader METHOD_SUBSTITUTION_CONSTRUCTOR_INVOCATION_ARGS; + + static { + try { + SUBMISSION_EXECUTION_HANDLER_INTERNAL_LOG_INVOCATION = new MethodHeader(SubmissionExecutionHandler.Internal.class.getDeclaredMethod("logInvocation", MethodHeader.class)); + SUBMISSION_EXECUTION_HANDLER_INTERNAL_ADD_INVOCATION = new MethodHeader(SubmissionExecutionHandler.Internal.class.getDeclaredMethod("addInvocation", MethodHeader.class, Invocation.class)); + SUBMISSION_EXECUTION_HANDLER_INTERNAL_USE_SUBSTITUTION = new MethodHeader(SubmissionExecutionHandler.Internal.class.getDeclaredMethod("useSubstitution", MethodHeader.class)); + SUBMISSION_EXECUTION_HANDLER_INTERNAL_GET_SUBSTITUTION = new MethodHeader(SubmissionExecutionHandler.Internal.class.getDeclaredMethod("getSubstitution", MethodHeader.class)); + SUBMISSION_EXECUTION_HANDLER_INTERNAL_USE_SUBMISSION_IMPL = new MethodHeader(SubmissionExecutionHandler.Internal.class.getDeclaredMethod("useSubmissionImpl", MethodHeader.class)); + + INVOCATION_CONSTRUCTOR = new MethodHeader(Invocation.class.getDeclaredConstructor(Class.class, MethodHeader.class, StackTraceElement[].class)); + INVOCATION_CONSTRUCTOR_WITH_INSTANCE = new MethodHeader(Invocation.class.getDeclaredConstructor(Class.class, MethodHeader.class, StackTraceElement[].class, Object.class)); + INVOCATION_CONSTRUCTOR_ADD_PARAMETER = new MethodHeader(Invocation.class.getDeclaredMethod("addParameter", Object.class)); + + METHOD_SUBSTITUTION_GET_CONSTRUCTOR_INVOCATION = new MethodHeader(MethodSubstitution.class.getDeclaredMethod("getConstructorInvocation")); + METHOD_SUBSTITUTION_EXECUTE = new MethodHeader(MethodSubstitution.class.getDeclaredMethod("execute", Invocation.class)); + METHOD_SUBSTITUTION_CONSTRUCTOR_INVOCATION_OWNER = new MethodHeader(MethodSubstitution.ConstructorInvocation.class.getDeclaredMethod("owner")); + METHOD_SUBSTITUTION_CONSTRUCTOR_INVOCATION_DESCRIPTOR = new MethodHeader(MethodSubstitution.ConstructorInvocation.class.getDeclaredMethod("descriptor")); + METHOD_SUBSTITUTION_CONSTRUCTOR_INVOCATION_ARGS = new MethodHeader(MethodSubstitution.ConstructorInvocation.class.getDeclaredMethod("args")); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + + private Constants() {} +} diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/EnumConstant.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/EnumConstant.java new file mode 100644 index 00000000..6f980644 --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/EnumConstant.java @@ -0,0 +1,3 @@ +package org.tudalgo.algoutils.transform.util; + +public record EnumConstant(String name, int ordinal, Object... values) {} diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/ForceSignatureAnnotationProcessor.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/ForceSignatureAnnotationProcessor.java new file mode 100644 index 00000000..c6fb1ec8 --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/ForceSignatureAnnotationProcessor.java @@ -0,0 +1,293 @@ +package org.tudalgo.algoutils.transform.util; + +import org.objectweb.asm.*; +import org.tudalgo.algoutils.student.annotation.ForceSignature; +import org.tudalgo.algoutils.transform.util.headers.FieldHeader; +import org.tudalgo.algoutils.transform.util.headers.MethodHeader; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * A processor for the {@link ForceSignature} annotation. + * An instance of this class processes and holds information on a single class and its members. + * @author Daniel Mangold + */ +public class ForceSignatureAnnotationProcessor extends ClassVisitor { + + private final ForceSignatureAnnotationVisitor annotationVisitor = new ForceSignatureAnnotationVisitor(); + private final List fieldLevelVisitors = new ArrayList<>(); + private final List methodLevelVisitors = new ArrayList<>(); + private String className; + + private String forcedClassName; + private final Map forcedFieldsMapping = new HashMap<>(); + private final Map forcedMethodsMapping = new HashMap<>(); + + /** + * Constructs a new {@link ForceSignatureAnnotationProcessor} instance. + */ + public ForceSignatureAnnotationProcessor() { + super(Opcodes.ASM9); + } + + /** + * Whether the class identifier / name is forced. + * + * @return true, if forced, otherwise false + */ + public boolean classIdentifierIsForced() { + return forcedClassName != null; + } + + /** + * Returns the forced class identifier. + * + * @return the forced class identifier + */ + public String forcedClassIdentifier() { + return forcedClassName.replace('.', '/'); + } + + /** + * Whether the given field is forced. + * + * @param name the original identifier / name of the field + * @return true, if forced, otherwise false + */ + public boolean fieldIdentifierIsForced(String name) { + return forcedFieldHeader(name) != null; + } + + /** + * Returns the field header for a forced field. + * + * @param name the original identifier / name of the field + * @return the field header + */ + public FieldHeader forcedFieldHeader(String name) { + return forcedFieldsMapping.entrySet() + .stream() + .filter(entry -> name.equals(entry.getKey().name())) + .findAny() + .map(Map.Entry::getValue) + .orElse(null); + } + + /** + * Whether the given method is forced. + * + * @param name the original identifier / name of the method + * @param descriptor the original descriptor of the method + * @return true, if forced, otherwise false + */ + public boolean methodSignatureIsForced(String name, String descriptor) { + return forcedMethodHeader(name, descriptor) != null; + } + + /** + * Returns the method header for a forced method. + * + * @param name the original identifier / name of the method + * @param descriptor the original descriptor of the method + * @return the method header + */ + public MethodHeader forcedMethodHeader(String name, String descriptor) { + return forcedMethodsMapping.entrySet() + .stream() + .filter(entry -> name.equals(entry.getKey().name()) && descriptor.equals(entry.getKey().descriptor())) + .findAny() + .map(Map.Entry::getValue) + .orElse(null); + } + + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + this.className = name; + } + + @Override + public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { + if (descriptor.equals(Constants.FORCE_SIGNATURE_TYPE.getDescriptor())) { + return annotationVisitor; + } else { + return null; + } + } + + @Override + public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) { + FieldLevelVisitor fieldLevelVisitor = new FieldLevelVisitor(className, access, name, descriptor, signature); + fieldLevelVisitors.add(fieldLevelVisitor); + return fieldLevelVisitor; + } + + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + MethodLevelVisitor methodLevelVisitor = new MethodLevelVisitor(className, access, name, descriptor, signature, exceptions); + methodLevelVisitors.add(methodLevelVisitor); + return methodLevelVisitor; + } + + @Override + public void visitEnd() { + forcedClassName = annotationVisitor.identifier; + + for (FieldLevelVisitor fieldLevelVisitor : fieldLevelVisitors) { + ForceSignatureAnnotationVisitor annotationVisitor = fieldLevelVisitor.annotationVisitor; + if (annotationVisitor == null) continue; + forcedFieldsMapping.put( + new FieldHeader(fieldLevelVisitor.owner, + fieldLevelVisitor.access, + fieldLevelVisitor.name, + fieldLevelVisitor.descriptor, + fieldLevelVisitor.signature), + new FieldHeader(fieldLevelVisitor.owner, + fieldLevelVisitor.access, + annotationVisitor.identifier, + fieldLevelVisitor.descriptor, + fieldLevelVisitor.signature) + ); + } + + for (MethodLevelVisitor methodLevelVisitor : methodLevelVisitors) { + ForceSignatureAnnotationVisitor annotationVisitor = methodLevelVisitor.annotationVisitor; + if (annotationVisitor == null) continue; + forcedMethodsMapping.put( + new MethodHeader(methodLevelVisitor.owner, + methodLevelVisitor.access, + methodLevelVisitor.name, + methodLevelVisitor.descriptor, + methodLevelVisitor.signature, + methodLevelVisitor.exceptions), + new MethodHeader(methodLevelVisitor.owner, + methodLevelVisitor.access, + annotationVisitor.identifier, + annotationVisitor.descriptor, + methodLevelVisitor.signature, + methodLevelVisitor.exceptions) + ); + } + } + + /** + * A field visitor for processing field-level annotations. + */ + private static class FieldLevelVisitor extends FieldVisitor { + + private final String owner; + private final int access; + private final String name; + private final String descriptor; + private final String signature; + private ForceSignatureAnnotationVisitor annotationVisitor; + + private FieldLevelVisitor(String owner, int access, String name, String descriptor, String signature) { + super(Opcodes.ASM9); + this.owner = owner; + this.access = access; + this.name = name; + this.descriptor = descriptor; + this.signature = signature; + } + + @Override + public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { + if (descriptor.equals(Constants.FORCE_SIGNATURE_TYPE.getDescriptor())) { + return annotationVisitor = new ForceSignatureAnnotationVisitor(); + } else { + return null; + } + } + } + + /** + * A method visitor for processing method-level annotations. + */ + private static class MethodLevelVisitor extends MethodVisitor { + + private final String owner; + private final int access; + private final String name; + private final String descriptor; + private final String signature; + private final String[] exceptions; + private ForceSignatureAnnotationVisitor annotationVisitor; + + private MethodLevelVisitor(String owner, int access, String name, String descriptor, String signature, String[] exceptions) { + super(Opcodes.ASM9); + this.owner = owner; + this.access = access; + this.name = name; + this.descriptor = descriptor; + this.signature = signature; + this.exceptions = exceptions; + } + + @Override + public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { + if (descriptor.equals(Constants.FORCE_SIGNATURE_TYPE.getDescriptor())) { + return annotationVisitor = new ForceSignatureAnnotationVisitor(); + } else { + return null; + } + } + } + + /** + * An annotation visitor for processing the actual {@link ForceSignature} annotation. + */ + private static class ForceSignatureAnnotationVisitor extends AnnotationVisitor { + + private String identifier; + private String descriptor; + private Type returnType; + private final List parameterTypes = new ArrayList<>(); + + ForceSignatureAnnotationVisitor() { + super(Opcodes.ASM9); + } + + @Override + public void visit(String name, Object value) { + switch (name) { + case "identifier" -> identifier = (String) value; + case "descriptor" -> descriptor = (String) value; + case "returnType" -> returnType = (Type) value; + } + } + + @Override + public AnnotationVisitor visitArray(String name) { + if (name.equals("parameterTypes")) { + return new ParameterTypesVisitor(); + } else { + return null; + } + } + + @Override + public void visitEnd() { + if ((descriptor == null || descriptor.isEmpty()) && returnType != null) { + descriptor = Type.getMethodDescriptor(returnType, parameterTypes.toArray(Type[]::new)); + } + } + + /** + * A specialized annotation visitor for visiting the values of {@link ForceSignature#parameterTypes()}. + */ + private class ParameterTypesVisitor extends AnnotationVisitor { + + private ParameterTypesVisitor() { + super(Opcodes.ASM9); + } + + @Override + public void visit(String name, Object value) { + parameterTypes.add((Type) value); + } + } + } +} diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/IncompatibleHeaderException.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/IncompatibleHeaderException.java new file mode 100644 index 00000000..7042f5d8 --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/IncompatibleHeaderException.java @@ -0,0 +1,87 @@ +package org.tudalgo.algoutils.transform.util; + +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Type; +import org.tudalgo.algoutils.transform.util.headers.Header; + +import static org.objectweb.asm.Opcodes.*; +import static org.objectweb.asm.Opcodes.ATHROW; + +/** + * Thrown to indicate that a class, field or method (including constructors) was declared incorrectly. + *

+ * For fields, this means that the field was declared static in the submission class while not being + * declared static in the solution class, or vice versa. + *

+ *

+ * For methods, it may indicate the same problem as for fields. + * It may also indicate that methods or constructors have the wrong number of parameters. + * Lastly, it may indicate that the return type or parameter types are incompatible, e.g., + * a submission class' method returns a primitive type while the solution class' method returns + * a reference type. + *

+ * + * @author Daniel Mangold + */ +public class IncompatibleHeaderException extends RuntimeException { + + private final String message; + private final Header expected; + private final Header actual; + + /** + * Constructs a new {@link IncompatibleHeaderException} instance. + * + * @param message the exception message + * @param expected the expected header + * @param actual the actual header, may be null + */ + public IncompatibleHeaderException(String message, Header expected, Header actual) { + super(); + this.message = message; + this.expected = expected; + this.actual = actual; + } + + @Override + public String getMessage() { + return "%s%nExpected: %s%nActual: %s%n".formatted(message, expected, actual); + } + + /** + * Replicates this exception in bytecode and optionally throws it. + * If it is not thrown, a reference to the newly created instance is located at the top + * of the method visitor's stack upon return. + * + * @param mv the method visitor to use + * @param throwException whether the exception should be thrown + * @param message the exception message + * @param expected the expected header + * @param actual the actual header, may be null + * @return the maximum stack size used + */ + public static int replicateInBytecode(MethodVisitor mv, boolean throwException, String message, Header expected, Header actual) { + int maxStack, stackSize; + + mv.visitTypeInsn(NEW, Type.getInternalName(IncompatibleHeaderException.class)); + mv.visitInsn(DUP); + mv.visitLdcInsn(message); + maxStack = stackSize = 3; + maxStack = Math.max(maxStack, stackSize++ + expected.buildHeader(mv)); + if (actual != null) { + maxStack = Math.max(maxStack, stackSize + actual.buildHeader(mv)); + } else { + mv.visitInsn(ACONST_NULL); + maxStack = Math.max(maxStack, ++stackSize); + } + mv.visitMethodInsn(INVOKESPECIAL, + Type.getInternalName(IncompatibleHeaderException.class), + "", + Type.getMethodDescriptor(Type.VOID_TYPE, Constants.STRING_TYPE, Constants.HEADER_TYPE, Constants.HEADER_TYPE), + false); + if (throwException) { + mv.visitInsn(ATHROW); + } + return maxStack; + } +} diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Invocation.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Invocation.java new file mode 100644 index 00000000..517319c6 --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Invocation.java @@ -0,0 +1,288 @@ +package org.tudalgo.algoutils.transform.util; + +import org.objectweb.asm.Opcodes; +import org.tudalgo.algoutils.transform.SubmissionExecutionHandler; +import org.tudalgo.algoutils.transform.util.headers.MethodHeader; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.util.*; + +/** + * This class holds information about the context of an invocation. + * Context means the instance the method was invoked on and the parameters it was invoked with + * as well as the stack trace up to the point of invocation. + * + * @author Daniel Mangold + */ +@SuppressWarnings("unused") +public class Invocation { + + private final Class declaringClass; + private final MethodHeader methodHeader; + private final StackTraceElement[] stackTrace; + private final Object instance; + private final List parameterValues = new ArrayList<>(); + + /** + * Constructs a new invocation (static variant). + * + * @param declaringClass the target method for this invocation + * @param stackTrace the stack trace up to the point of invocation + */ + public Invocation(Class declaringClass, MethodHeader methodHeader, StackTraceElement[] stackTrace) { + this(declaringClass, methodHeader, stackTrace, null); + } + + /** + * Constructs a new invocation (non-static variant). + * + * @param declaringClass the target method for this invocation + * @param stackTrace the stack trace up to the point of invocation + * @param instance the object on which this invocation takes place + */ + public Invocation(Class declaringClass, MethodHeader methodHeader, StackTraceElement[] stackTrace, Object instance) { + this.declaringClass = declaringClass; + this.methodHeader = methodHeader; + this.stackTrace = new StackTraceElement[stackTrace.length - 1]; + System.arraycopy(stackTrace, 1, this.stackTrace, 0, stackTrace.length - 1); + this.instance = instance; + } + + /** + * Returns the object the method was invoked on. + * + * @return the object the method was invoked on. + */ + @SuppressWarnings("unchecked") + public T getInstance() { + return (T) instance; + } + + /** + * Returns the object the method was invoked on. + * + * @param clazz the class the instance will be cast to + * @return the object the method was invoked on + */ + public T getInstance(Class clazz) { + return clazz.cast(instance); + } + + /** + * Returns the stack trace up to the point of this method's invocation. + * + * @return the stack trace + */ + public StackTraceElement[] getStackTrace() { + return stackTrace; + } + + /** + * Returns the stack trace element of the caller. + * + * @return the stack trace element + */ + public StackTraceElement getCallerStackTraceElement() { + return stackTrace[0]; + } + + /** + * Returns the list of parameter values the method was invoked with. + * + * @return the list of parameter values the method was invoked with. + */ + public List getParameters() { + return Collections.unmodifiableList(parameterValues); + } + + /** + * Returns the value of the parameter at the given index. + * + * @param index the parameter's index + * @return the parameter value + */ + @SuppressWarnings("unchecked") + public T getParameter(int index) { + return (T) parameterValues.get(index); + } + + /** + * Returns the value of the parameter at the given index, cast to the given class. + * + * @param index the parameter's index + * @param clazz the class the value will be cast to + * @return the parameter value, cast to the given class + */ + public T getParameter(int index, Class clazz) { + return clazz.cast(parameterValues.get(index)); + } + + /** + * Returns the value of the {@code boolean} parameter at the given index. + * + * @param index the parameter's index + * @return the parameter value + */ + public boolean getBooleanParameter(int index) { + return getParameter(index, Boolean.class); + } + + /** + * Returns the value of the {@code byte} parameter at the given index. + * + * @param index the parameter's index + * @return the parameter value + */ + public byte getByteParameter(int index) { + return getParameter(index, Byte.class); + } + + /** + * Returns the value of the {@code short} parameter at the given index. + * + * @param index the parameter's index + * @return the parameter value + */ + public short getShortParameter(int index) { + return getParameter(index, Short.class); + } + + /** + * Returns the value of the {@code char} parameter at the given index. + * + * @param index the parameter's index + * @return the parameter value + */ + public char getCharParameter(int index) { + return getParameter(index, Character.class); + } + + /** + * Returns the value of the {@code int} parameter at the given index. + * + * @param index the parameter's index + * @return the parameter value + */ + public int getIntParameter(int index) { + return getParameter(index, Integer.class); + } + + /** + * Returns the value of the {@code long} parameter at the given index. + * + * @param index the parameter's index + * @return the parameter value + */ + public long getLongParameter(int index) { + return getParameter(index, Long.class); + } + + /** + * Returns the value of the {@code float} parameter at the given index. + * + * @param index the parameter's index + * @return the parameter value + */ + public float getFloatParameter(int index) { + return getParameter(index, Float.class); + } + + /** + * Returns the value of the {@code double} parameter at the given index. + * + * @param index the parameter's index + * @return the parameter value + */ + public double getDoubleParameter(int index) { + return getParameter(index, Double.class); + } + + /** + * Adds a parameter value to the list of values. + * + * @param value the value to add + */ + public void addParameter(Object value) { + parameterValues.add(value); + } + + /** + * Calls the original method with the stored parameter values. + * + * @param delegate whether to use the solution (delegated) or submission class implementation (not delegated) + * @return the value returned the original method + */ + public Object callOriginalMethod(boolean delegate) { + return callOriginalMethod(delegate, parameterValues.toArray()); + } + + /** + * Calls the original method with the given parameter values. + * + * @param delegate whether to use the solution (delegated) or submission class implementation (not delegated) + * @param params the values to invoke the original method with + * @return the value returned the original method + */ + public Object callOriginalMethod(boolean delegate, Object... params) { + Object[] invocationArgs; + if (instance != null) { + invocationArgs = new Object[params.length + 1]; + invocationArgs[0] = instance; + System.arraycopy(params, 0, invocationArgs, 1, params.length); + } else { + invocationArgs = params; + } + + MethodSubstitution methodSubstitution = SubmissionExecutionHandler.Internal.getSubstitution(methodHeader); + SubmissionExecutionHandler.Substitution.disable(methodHeader); + boolean isDelegated = !SubmissionExecutionHandler.Internal.useSubmissionImpl(methodHeader); + if (delegate) { + SubmissionExecutionHandler.Delegation.enable(methodHeader); + } else { + SubmissionExecutionHandler.Delegation.disable(methodHeader); + } + + try { + MethodHandles.Lookup lookup = MethodHandles.lookup(); + MethodType methodType = MethodType.fromMethodDescriptorString(methodHeader.descriptor(), getClass().getClassLoader()); + MethodHandle methodHandle = switch (methodHeader.getOpcode()) { + case Opcodes.INVOKEVIRTUAL -> lookup.findVirtual(declaringClass, methodHeader.name(), methodType); + case Opcodes.INVOKESPECIAL -> lookup.findConstructor(declaringClass, methodType); + case Opcodes.INVOKESTATIC -> lookup.findStatic(declaringClass, methodHeader.name(), methodType); + default -> throw new IllegalArgumentException("Unsupported opcode: " + methodHeader.getOpcode()); + }; + return methodHandle.invokeWithArguments(invocationArgs); + } catch (Throwable e) { + throw new RuntimeException(e); + } finally { + SubmissionExecutionHandler.Substitution.enable(methodHeader, methodSubstitution); + if (isDelegated) { + SubmissionExecutionHandler.Delegation.enable(methodHeader); + } else { + SubmissionExecutionHandler.Delegation.disable(methodHeader); + } + } + } + + @Override + public String toString() { + return "Invocation{instance=%s, parameterValues=%s, stackTrace=%s}".formatted(instance, parameterValues, Arrays.toString(stackTrace)); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Invocation that = (Invocation) o; + return Objects.equals(instance, that.instance) && + Arrays.equals(stackTrace, that.stackTrace) && + Objects.equals(parameterValues, that.parameterValues); + } + + @Override + public int hashCode() { + return Objects.hash(instance, Arrays.hashCode(stackTrace), parameterValues); + } +} diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/MethodSubstitution.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/MethodSubstitution.java new file mode 100644 index 00000000..bd31a8c6 --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/MethodSubstitution.java @@ -0,0 +1,50 @@ +package org.tudalgo.algoutils.transform.util; + +import org.objectweb.asm.Type; + +/** + * This functional interface represents a substitution for a method. + * The functional method {@link #execute(Invocation)} is called with the original invocation's context. + * Its return value is also the value that will be returned by the substituted method. + */ +@FunctionalInterface +public interface MethodSubstitution { + + /** + * Defines the behaviour of method substitution when the substituted method is a constructor. + * When a constructor method is substituted, either {@code super(...)} or {@code this(...)} must be called + * before calling {@link #execute(Invocation)}. + * This method returns a {@link ConstructorInvocation} object storing... + *
    + *
  1. the internal class name / owner of the target constructor and
  2. + *
  3. the values that are passed to the constructor of that class.
  4. + *
+ * The owner must equal either the class whose constructor is substituted or its superclass. + * Default behaviour assumes calling the constructor of {@link Object}, i.e., + * a class that has no explicit superclass. + * + * @return a record containing the target method's owner and arguments + */ + default ConstructorInvocation getConstructorInvocation() { + return new ConstructorInvocation("java/lang/Object", "()V"); + } + + /** + * Defines the actions of the substituted method. + * + * @param invocation the context of an invocation + * @return the return value of the substituted method + */ + Object execute(Invocation invocation); + + /** + * A record storing the internal name of the class / owner of the target constructor, the constructor descriptor + * (see Chapter 4.3 of the JLS) + * and the arguments it is invoked with. + * + * @param owner the internal name of the target constructor's owner (see {@link Type#getInternalName()}) + * @param descriptor the descriptor of the target constructor + * @param args the arguments the constructor will be invoked with + */ + record ConstructorInvocation(String owner, String descriptor, Object... args) {} +} diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationContext.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationContext.java new file mode 100644 index 00000000..36af6635 --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationContext.java @@ -0,0 +1,298 @@ +package org.tudalgo.algoutils.transform.util; + +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.Type; +import org.tudalgo.algoutils.transform.classes.SolutionClassNode; +import org.tudalgo.algoutils.transform.SolutionMergingClassTransformer; +import org.tudalgo.algoutils.transform.classes.SubmissionClassInfo; +import org.tudalgo.algoutils.transform.util.headers.MethodHeader; +import org.tudalgo.algoutils.transform.util.matching.ClassSimilarityMapper; + +import java.io.IOException; +import java.io.InputStream; +import java.util.*; + +/** + * A record for holding context information for the transformation process. + * + * @author Daniel Mangold + */ +public final class TransformationContext { + + private final Map configuration; + private final Map solutionClasses; + private final Map submissionClasses; + private final Set visitedClasses = new HashSet<>(); + + private ClassLoader submissionClassLoader; + private Set submissionClassNames; + private ClassSimilarityMapper classSimilarityMapper; + + /** + * Constructs a new {@link TransformationContext}. + * + * @param configuration configuration for this transformer run + * @param solutionClasses a mapping of solution class names to their respective {@link SolutionClassNode} + * @param submissionClasses a mapping of submission class names to their respective {@link SubmissionClassInfo} + */ + public TransformationContext( + Map configuration, + Map solutionClasses, + Map submissionClasses + ) { + this.configuration = configuration; + this.solutionClasses = solutionClasses; + this.submissionClasses = submissionClasses; + } + + // Config and misc. stuff + + /** + * Returns the project prefix. + * + * @return the project prefix + */ + public String getProjectPrefix() { + return (String) configuration.get(SolutionMergingClassTransformer.Config.PROJECT_PREFIX); + } + + /** + * Returns the minimum similarity threshold. + * + * @return the minimum similarity threshold + */ + public double getSimilarity() { + return (Double) configuration.get(SolutionMergingClassTransformer.Config.SIMILARITY); + } + + /** + * Whether the given method call should be replaced. + * + * @param methodHeader the header of the target method + * @return true, if a replacement exists, otherwise false + */ + public boolean methodHasReplacement(MethodHeader methodHeader) { + return getMethodReplacement(methodHeader) != null; + } + + /** + * Returns the replacement method header for the given target method header. + * + * @param methodHeader the header of the target method + * @return the replacement method header + */ + @SuppressWarnings("unchecked") + public MethodHeader getMethodReplacement(MethodHeader methodHeader) { + return ((Map) configuration.get(SolutionMergingClassTransformer.Config.METHOD_REPLACEMENTS)) + .get(methodHeader); + } + + /** + * Sets the class loader for submission classes. + * + * @param submissionClassLoader the class loader + */ + public void setSubmissionClassLoader(ClassLoader submissionClassLoader) { + this.submissionClassLoader = submissionClassLoader; + } + + /** + * Sets the available submission classes to the specified value. + * + * @param submissionClassNames the available submission classes + */ + public void setSubmissionClassNames(Set submissionClassNames) { + this.submissionClassNames = submissionClassNames; + } + + public void addVisitedClass(String className) { + visitedClasses.add(className); + } + + public Set getVisitedClasses() { + return Collections.unmodifiableSet(visitedClasses); + } + + /** + * Computes similarities for mapping submission classes to solution classes. + */ + @SuppressWarnings("unchecked") + public void computeClassesSimilarity() { + classSimilarityMapper = new ClassSimilarityMapper(submissionClassNames, + (Map>) configuration.get(SolutionMergingClassTransformer.Config.SOLUTION_CLASSES), + getSimilarity()); + } + + // Submission classes + + /** + * Whether the given class is a submission class. + * The parameter must be either the internal name of a class or an array descriptor. + * + * @param submissionClassName the class name / array class descriptor + * @return true, if the given class is a submission class, otherwise false + */ + public boolean isSubmissionClass(String submissionClassName) { + if (submissionClassName.startsWith("[")) { + return isSubmissionClass(Type.getType(submissionClassName).getElementType().getInternalName()); + } else { + return submissionClassNames.contains(submissionClassName); + } + } + + /** + * Returns the {@link SubmissionClassInfo} for a given submission class name. + * If no mapping exists in {@link #submissionClasses}, will attempt to compute one. + * + * @param submissionClassName the submission class name + * @return the {@link SubmissionClassInfo} object + */ + public SubmissionClassInfo getSubmissionClassInfo(String submissionClassName) { + boolean isAbsent = !submissionClasses.containsKey(submissionClassName); + SubmissionClassInfo submissionClassInfo = submissionClasses.computeIfAbsent(submissionClassName, this::readSubmissionClass); + if (isAbsent && submissionClassInfo != null) { + submissionClassInfo.resolveMembers(); + } + return submissionClassInfo; + } + + /** + * Attempts to read and process a submission class. + * + * @param className the name of the submission class + * @return the resulting {@link SubmissionClassInfo} object + */ + public SubmissionClassInfo readSubmissionClass(String className) { + ClassReader submissionClassReader; + String submissionClassFilePath = className + ".class"; + try (InputStream is = submissionClassLoader.getResourceAsStream(submissionClassFilePath)) { + if (is == null) { + return null; + } + submissionClassReader = new ClassReader(is); + } catch (IOException e) { + throw new RuntimeException(e); + } + ForceSignatureAnnotationProcessor fsAnnotationProcessor = new ForceSignatureAnnotationProcessor(); + submissionClassReader.accept(fsAnnotationProcessor, 0); + SubmissionClassInfo submissionClassInfo = new SubmissionClassInfo(this, fsAnnotationProcessor); + submissionClassReader.accept(submissionClassInfo, 0); + return submissionClassInfo; + } + + // Solution classes + + /** + * Returns the solution class name for the given submission class name. + * If no matching solution class was found, returns the given submission class name. + * + * @param submissionClassName the submission class name + * @return the solution class name + */ + public String getSolutionClassName(String submissionClassName) { + return classSimilarityMapper.getBestMatch(submissionClassName).orElse(submissionClassName); + } + + /** + * Returns the solution class node for the given solution class name. + * + * @param name the solution class name + * @return the solution class node + */ + public SolutionClassNode getSolutionClass(String name) { + return solutionClasses.get(name); + } + + public Map getSolutionClasses() { + return Collections.unmodifiableMap(solutionClasses); + } + + /** + * Returns the computed (i.e., mapped from submission to solution) type. + * If the given value represents a method descriptor, this method will return a type with + * a descriptor where all parameter types and the return types have been computed. + * If the value represents an array, it will return a type where the component type has been computed. + * If the given type represents a primitive type or an object type that is not a submission class + * (or no corresponding solution class exists), it will return the original value. + * + * @param type the type to map + * @return the computed type + */ + public Type toComputedType(Type type) { + if (type.getSort() == Type.OBJECT) { + return Type.getObjectType(getSolutionClassName(type.getInternalName())); + } else if (type.getSort() == Type.ARRAY) { + int dimensions = type.getDimensions(); + Type elementType = type.getElementType(); + return Type.getType("[".repeat(dimensions) + toComputedType(elementType).getDescriptor()); + } else if (type.getSort() == Type.METHOD) { + Type returnType = toComputedType(type.getReturnType()); + Type[] parameterTypes = Arrays.stream(type.getArgumentTypes()).map(this::toComputedType).toArray(Type[]::new); + return Type.getMethodType(returnType, parameterTypes); + } else { + return type; + } + } + + /** + * Returns the computed internal name. + * Convenience method for {@code toComputedType(Type.getObjectType(name)).getInternalName()}. + * + * @param name the type name + * @return the computed internal name + */ + public String toComputedInternalName(String name) { + return toComputedType(Type.getObjectType(name)).getInternalName(); + } + + /** + * Returns the computed descriptor. + * Convenience method for {@code toComputedType(Type.getType(descriptor)).getDescriptor()}. + * + * @param descriptor the descriptor + * @return the computed descriptor + */ + public String toComputedDescriptor(String descriptor) { + return toComputedType(Type.getType(descriptor)).getDescriptor(); + } + + /** + * Whether the two given descriptors have the same argument and return types when computed. + * Convenience method for {@code toComputedDescriptor(descriptor).equals(computedDescriptor)}. + * + * @param descriptor the descriptor to compare + * @param computedDescriptor the computed descriptor to compare against + * @return true, if the descriptors are compatible, otherwise false + */ + public boolean descriptorIsCompatible(String descriptor, String computedDescriptor) { + return toComputedDescriptor(descriptor).equals(computedDescriptor); + } + + /** + * Attempts to read and process a solution class from {@code resources/classes/}. + * + * @param className the name of the solution class + * @return the resulting {@link SolutionClassNode} object + */ + public SolutionClassNode readSolutionClass(String className) { + ClassReader solutionClassReader; + String solutionClassFilePath = "/classes/%s.bin".formatted(className); + try (InputStream is = getClass().getResourceAsStream(solutionClassFilePath)) { + if (is == null) { + throw new IOException("No such resource: " + solutionClassFilePath); + } + solutionClassReader = new ClassReader(is); + } catch (IOException e) { + throw new RuntimeException(e); + } + SolutionClassNode solutionClassNode = new SolutionClassNode(this, className); + solutionClassReader.accept(solutionClassNode, 0); + return solutionClassNode; + } + + @Override + public String toString() { + return "TransformationContext[configuration=%s, solutionClasses=%s, submissionClasses=%s]" + .formatted(configuration, solutionClasses, submissionClasses); + } +} diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationUtils.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationUtils.java new file mode 100644 index 00000000..8c4b3a3b --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationUtils.java @@ -0,0 +1,304 @@ +package org.tudalgo.algoutils.transform.util; + +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Type; + +import java.util.Map; +import java.util.StringJoiner; + +import static org.objectweb.asm.Opcodes.*; + +/** + * A collection of utility methods useful for bytecode transformations. + * @author Daniel Mangold + */ +public final class TransformationUtils { + + private TransformationUtils() {} + + /** + * Returns transformed modifiers, enabling easy access from tests. + * The returned modifiers have public visibility and an unset final-flag. + * + * @param access the modifiers to transform + * @return the transformed modifiers + */ + public static int transformAccess(int access) { + return access & ~ACC_FINAL & ~ACC_PRIVATE & ~ACC_PROTECTED | ACC_PUBLIC; + } + + /** + * Whether the given members have the same execution context. + * Two members are considered to have the same execution context if they are either + * both static or both non-static. + * Furthermore, if they are non-static they need to be both abstract or non-abstract. + * + * @param access1 the modifiers of the first member + * @param access2 the modifiers of the second member + * @return true, if both members have the same execution context, otherwise false + */ + public static boolean contextIsCompatible(int access1, int access2) { + return (access1 & ACC_STATIC) == (access2 & ACC_STATIC) && (access1 & ACC_ABSTRACT) == (access2 & ACC_ABSTRACT); + } + + /** + * Whether the given opcode can be used on the given member. + * + * @param opcode the opcode to check + * @param access the member's modifiers + * @return true, if the opcode can be used on the member, otherwise false + */ + public static boolean opcodeIsCompatible(int opcode, int access) { + return (opcode == GETSTATIC || opcode == PUTSTATIC || opcode == INVOKESTATIC) == ((access & ACC_STATIC) != 0); + } + + /** + * Whether the given method is a lambda. + * + * @param access the method's modifiers + * @param name the method's name + * @return true, if the method is a lambda, otherwise false + */ + public static boolean isLambdaMethod(int access, String name) { + return (access & ACC_SYNTHETIC) != 0 && name.startsWith("lambda$"); + } + + /** + * Whether the given type is a + * category 2 computational type. + * + * @param type the type to check + * @return true, if the given type is a category 2 computational type, otherwise false + */ + public static boolean isCategory2Type(Type type) { + return type.getSort() == Type.LONG || type.getSort() == Type.DOUBLE; + } + + /** + * Calculates the true index of variables in the locals array. + * Variables with type long or double occupy two slots in the locals array, + * so the "expected" or "natural" index of these variables might be shifted. + * + * @param types the parameter types + * @param index the "natural" index of the variable + * @return the true index + */ + public static int getLocalsIndex(Type[] types, int index) { + int localsIndex = 0; + for (int i = 0; i < index; i++) { + localsIndex += isCategory2Type(types[i]) ? 2 : 1; + } + return localsIndex; + } + + /** + * Automatically box primitive types using the supplied {@link MethodVisitor}. + * If the given type is not a primitive type, this method does nothing. + * + * @param mv the {@link MethodVisitor} to use + * @param type the type of the value + */ + public static void boxType(MethodVisitor mv, Type type) { + switch (type.getSort()) { + case Type.BOOLEAN -> mv.visitMethodInsn(INVOKESTATIC, "java/lang/Boolean", "valueOf", "(Z)Ljava/lang/Boolean;", false); + case Type.BYTE -> mv.visitMethodInsn(INVOKESTATIC, "java/lang/Byte", "valueOf", "(B)Ljava/lang/Byte;", false); + case Type.SHORT -> mv.visitMethodInsn(INVOKESTATIC, "java/lang/Short", "valueOf", "(S)Ljava/lang/Short;", false); + case Type.CHAR -> mv.visitMethodInsn(INVOKESTATIC, "java/lang/Character", "valueOf", "(C)Ljava/lang/Character;", false); + case Type.INT -> mv.visitMethodInsn(INVOKESTATIC, "java/lang/Integer", "valueOf", "(I)Ljava/lang/Integer;", false); + case Type.FLOAT -> mv.visitMethodInsn(INVOKESTATIC, "java/lang/Float", "valueOf", "(F)Ljava/lang/Float;", false); + case Type.LONG -> mv.visitMethodInsn(INVOKESTATIC, "java/lang/Long", "valueOf", "(J)Ljava/lang/Long;", false); + case Type.DOUBLE -> mv.visitMethodInsn(INVOKESTATIC, "java/lang/Double", "valueOf", "(D)Ljava/lang/Double;", false); + } + } + + /** + * Automatically unbox primitive types using the supplied {@link MethodVisitor}. + * If the given type is not a primitive type, then this method will cast it to the specified type. + * + * @param mv the {@link MethodVisitor} to use + * @param type the type of the value + */ + public static void unboxType(MethodVisitor mv, Type type) { + switch (type.getSort()) { + case Type.BOOLEAN -> { + mv.visitTypeInsn(CHECKCAST, "java/lang/Boolean"); + mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Boolean", "booleanValue", "()Z", false); + } + case Type.BYTE -> { + mv.visitTypeInsn(CHECKCAST, "java/lang/Byte"); + mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Byte", "byteValue", "()B", false); + } + case Type.SHORT -> { + mv.visitTypeInsn(CHECKCAST, "java/lang/Short"); + mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Short", "shortValue", "()S", false); + } + case Type.CHAR -> { + mv.visitTypeInsn(CHECKCAST, "java/lang/Character"); + mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Character", "charValue", "()C", false); + } + case Type.INT -> { + mv.visitTypeInsn(CHECKCAST, "java/lang/Integer"); + mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Integer", "intValue", "()I", false); + } + case Type.FLOAT -> { + mv.visitTypeInsn(CHECKCAST, "java/lang/Float"); + mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Float", "floatValue", "()F", false); + } + case Type.LONG -> { + mv.visitTypeInsn(CHECKCAST, "java/lang/Long"); + mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Long", "longValue", "()J", false); + } + case Type.DOUBLE -> { + mv.visitTypeInsn(CHECKCAST, "java/lang/Double"); + mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Double", "doubleValue", "()D", false); + } + case Type.OBJECT, Type.ARRAY -> mv.visitTypeInsn(CHECKCAST, type.getInternalName()); + } + } + + /** + * Places the given type's default value on top of the method visitor's stack. + * + * @param mv the method visitor to use + * @param type the type to get the default value for + */ + public static void getDefaultValue(MethodVisitor mv, Type type) { + switch (type.getSort()) { + case Type.BOOLEAN, Type.BYTE, Type.SHORT, Type.CHAR, Type.INT -> mv.visitInsn(ICONST_0); + case Type.FLOAT -> mv.visitInsn(FCONST_0); + case Type.LONG -> mv.visitInsn(LCONST_0); + case Type.DOUBLE -> mv.visitInsn(DCONST_0); + case Type.OBJECT, Type.ARRAY -> mv.visitInsn(ACONST_NULL); + } + } + + /** + * Recursively replicates the given array with bytecode instructions using the supplied method visitor. + * Upon return, a reference to the newly created array is located at + * the top of the method visitor's stack. + * {@code componentType} must denote a primitive type, a type compatible with the LDC instruction + * and its variants, or an array of either. + * + * @param mv the method visitor to use + * @param componentType the array's component type + * @param array the array to replicate, may be null + * @return the maximum stack size used during the operation + */ + public static int buildArray(MethodVisitor mv, Type componentType, Object[] array) { + int componentTypeSort = componentType.getSort(); + int maxStack, stackSize; + if (array == null) { + mv.visitInsn(ACONST_NULL); + return 1; + } + + mv.visitIntInsn(SIPUSH, array.length); + if (componentTypeSort == Type.OBJECT || componentTypeSort == Type.ARRAY) { + mv.visitTypeInsn(ANEWARRAY, componentType.getInternalName()); + } else { + int operand = switch (componentTypeSort) { + case Type.BOOLEAN -> T_BOOLEAN; + case Type.BYTE -> T_BYTE; + case Type.SHORT -> T_SHORT; + case Type.CHAR -> T_CHAR; + case Type.INT -> T_INT; + case Type.FLOAT -> T_FLOAT; + case Type.LONG -> T_LONG; + case Type.DOUBLE -> T_DOUBLE; + default -> throw new IllegalArgumentException("Unsupported component type: " + componentType); + }; + mv.visitIntInsn(NEWARRAY, operand); + } + maxStack = stackSize = 1; + + for (int i = 0; i < array.length; i++, stackSize -= 3) { + mv.visitInsn(DUP); + mv.visitIntInsn(SIPUSH, i); + maxStack = Math.max(maxStack, stackSize += 2); + if (componentTypeSort == Type.ARRAY) { + int stackUsed = buildArray(mv, Type.getType(componentType.getDescriptor().substring(1)), (Object[]) array[i]); + maxStack = Math.max(maxStack, stackSize++ + stackUsed); + } else { + mv.visitLdcInsn(array[i]); + maxStack = Math.max(maxStack, ++stackSize); + } + mv.visitInsn(componentType.getOpcode(IASTORE)); + } + + return maxStack; + } + + /** + * Returns a human-readable form of the given modifiers. + * + * @param modifiers the modifiers to use + * @return a string with human-readable modifiers + */ + public static String toHumanReadableModifiers(int modifiers) { + Map readableModifiers = Map.of( + ACC_PUBLIC, "public", + ACC_PRIVATE, "private", + ACC_PROTECTED, "protected", + ACC_STATIC, "static", + ACC_FINAL, "final", + ACC_INTERFACE, "interface", + ACC_ABSTRACT, "abstract", + ACC_SYNTHETIC, "synthetic", + ACC_ENUM, "enum", + ACC_RECORD, "record" + ); + StringJoiner joiner = new StringJoiner(" "); + for (int i = 1; i <= ACC_RECORD; i = i << 1) { + if ((modifiers & i) != 0 && readableModifiers.containsKey(i)) { + joiner.add(readableModifiers.get(i)); + } + } + return joiner.toString(); + } + + /** + * Returns a human-readable form of the given type. + * + * @param type the type to use + * @return the human-readable type + */ + public static String toHumanReadableType(Type type) { + return switch (type.getSort()) { + case Type.VOID -> "void"; + case Type.BOOLEAN -> "boolean"; + case Type.BYTE -> "byte"; + case Type.SHORT -> "short"; + case Type.CHAR -> "char"; + case Type.INT -> "int"; + case Type.FLOAT -> "float"; + case Type.LONG -> "long"; + case Type.DOUBLE -> "double"; + case Type.ARRAY -> toHumanReadableType(type.getElementType()) + "[]".repeat(type.getDimensions()); + case Type.OBJECT -> type.getInternalName().replace('/', '.'); + default -> throw new IllegalStateException("Unexpected type: " + type); + }; + } + + /** + * Returns the class that represents the given type. + * + * @param type the type whose class representation to get + * @return the class that represents the given type + * @throws ClassNotFoundException if the class for a reference type could not be found + */ + public static Class getClassForType(Type type) throws ClassNotFoundException { + return switch (type.getSort()) { + case Type.VOID -> void.class; + case Type.BOOLEAN -> boolean.class; + case Type.BYTE -> byte.class; + case Type.SHORT -> short.class; + case Type.CHAR -> char.class; + case Type.INT -> int.class; + case Type.FLOAT -> float.class; + case Type.LONG -> long.class; + case Type.DOUBLE -> double.class; + case Type.OBJECT, Type.ARRAY -> Class.forName(type.getInternalName().replace('/', '.')); + default -> throw new IllegalArgumentException("Unsupported type: " + type); + }; + } +} diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/headers/ClassHeader.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/headers/ClassHeader.java new file mode 100644 index 00000000..92c2d05c --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/headers/ClassHeader.java @@ -0,0 +1,151 @@ +package org.tudalgo.algoutils.transform.util.headers; + +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.tudalgo.algoutils.transform.util.Constants; +import org.tudalgo.algoutils.transform.util.TransformationUtils; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * A record holding information on the header of a class as declared in Java bytecode. + * {@code name}, {@code superName} as well as the values of {@code interfaces} use the internal name + * of the corresponding class (see {@link Type#getInternalName()}). + * + * @param access the class' modifiers + * @param name the class' name + * @param signature the class' signature, if using type parameters + * @param superName the class' superclass + * @param interfaces the class' interfaces + * @author Daniel Mangold + */ +public record ClassHeader(int access, String name, String signature, String superName, String[] interfaces) implements Header { + + @Override + public Type getHeaderType() { + return Constants.CLASS_HEADER_TYPE; + } + + @Override + public HeaderRecordComponent[] getComponents() { + return new HeaderRecordComponent[] { + new HeaderRecordComponent(Type.INT_TYPE, access), + new HeaderRecordComponent(Constants.STRING_TYPE, name), + new HeaderRecordComponent(Constants.STRING_TYPE, signature), + new HeaderRecordComponent(Constants.STRING_TYPE, superName), + new HeaderRecordComponent(Constants.STRING_ARRAY_TYPE, interfaces) + }; + } + + /** + * Visits the class header using the information stored in this record. + * + * @param delegate the class visitor to use + * @param version the class version (see {@link ClassVisitor#visit(int, int, String, String, String, String[])}) + * @param additionalInterfaces the internal names of additional interfaces this class should implement + */ + public void visitClass(ClassVisitor delegate, int version, String... additionalInterfaces) { + String[] interfaces; + if (this.interfaces == null) { + interfaces = additionalInterfaces; + } else { + interfaces = new String[this.interfaces.length + additionalInterfaces.length]; + System.arraycopy(this.interfaces, 0, interfaces, 0, this.interfaces.length); + System.arraycopy(additionalInterfaces, 0, interfaces, this.interfaces.length, additionalInterfaces.length); + } + + delegate.visit(version, this.access, this.name, this.signature, this.superName, interfaces); + } + + /** + * Returns the modifiers of this class header. + * Alias of {@link #access()}. + * + * @return the modifiers of this class header + */ + @Override + public int modifiers() { + return access; + } + + /** + * Returns a class object that identifies the declared super class for + * the class represented by this class header. + * + * @return the declared super class for this class + */ + @SuppressWarnings("unchecked") + public Class getSuperType() { + try { + return (Class) TransformationUtils.getClassForType(Type.getObjectType(superName)); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + + /** + * Returns a list of class object that identify the declared interfaces for + * the class represented by this class header. + * + * @return the declared interfaces for this class + */ + @SuppressWarnings("unchecked") + public List> getInterfaceTypes() { + return Arrays.stream(interfaces) + .map(interfaceName -> { + try { + return (Class) TransformationUtils.getClassForType(Type.getObjectType(interfaceName)); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + }) + .toList(); + } + + /** + * Returns a new class header describing the given class. + * + * @param clazz the class + * @return the new class header object + */ + public static ClassHeader of(Class clazz) { + return new ClassHeader(clazz.getModifiers(), + Type.getInternalName(clazz), + null, + clazz.getSuperclass() != null ? Type.getInternalName(clazz.getSuperclass()) : null, + Arrays.stream(clazz.getInterfaces()).map(Type::getInternalName).toArray(String[]::new)); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof ClassHeader that)) return false; + return Objects.equals(name, that.name); + } + + @Override + public int hashCode() { + return Objects.hashCode(name); + } + + @Override + public String toString() { + String signatureString = "(signature: '%s')".formatted(signature); + String superClassString = "extends %s".formatted(superName != null ? superName.replace('/', '.') : ""); + String interfacesString = "implements %s".formatted(Arrays.stream(interfaces == null ? new String[0] : interfaces) + .map(s -> s.replace('/', '.')) + .collect(Collectors.joining(", "))); + return "%s %s %s %s %s".formatted( + TransformationUtils.toHumanReadableModifiers(access) + + (((Opcodes.ACC_INTERFACE | Opcodes.ACC_ENUM | Opcodes.ACC_RECORD) & access) == 0 ? " class" : ""), + TransformationUtils.toHumanReadableType(Type.getObjectType(name)), + superName != null ? superClassString : "", + interfaces != null && interfaces.length > 0 ? interfacesString : "", + signature != null ? signatureString : "" + ).trim(); + } +} diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/headers/FieldHeader.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/headers/FieldHeader.java new file mode 100644 index 00000000..0a44eec1 --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/headers/FieldHeader.java @@ -0,0 +1,136 @@ +package org.tudalgo.algoutils.transform.util.headers; + +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.Type; +import org.tudalgo.algoutils.transform.util.Constants; +import org.tudalgo.algoutils.transform.util.TransformationUtils; + +import java.lang.reflect.Field; +import java.util.Objects; + +/** + * A record holding information on the header of a field as declared in java bytecode. + * {@code owner} uses the internal name of the corresponding class (see {@link Type#getInternalName()}). + * {@link Type#getType(String)} can be used with {@code descriptor} to get a more user-friendly + * representation of this field's type. + * + * @param owner the field's owner or declaring class + * @param access the field's modifiers + * @param name the field's name + * @param descriptor the field's descriptor / type + * @param signature the field's signature, if using type parameters + * @author Daniel Mangold + */ +public record FieldHeader(String owner, int access, String name, String descriptor, String signature) implements Header { + + /** + * Constructs a new field header using the given field. + * + * @param field a java reflection field + */ + public FieldHeader(Field field) { + this(Type.getInternalName(field.getDeclaringClass()), + field.getModifiers(), + field.getName(), + Type.getDescriptor(field.getType()), + null); + } + + @Override + public Type getHeaderType() { + return Constants.FIELD_HEADER_TYPE; + } + + @Override + public HeaderRecordComponent[] getComponents() { + return new HeaderRecordComponent[] { + new HeaderRecordComponent(Constants.STRING_TYPE, owner), + new HeaderRecordComponent(Type.INT_TYPE, access), + new HeaderRecordComponent(Constants.STRING_TYPE, name), + new HeaderRecordComponent(Constants.STRING_TYPE, descriptor), + new HeaderRecordComponent(Constants.STRING_TYPE, signature) + }; + } + + /** + * Visits a field in the given class visitor using the information stored in this record. + * + * @param delegate the class visitor to use + * @param value an optional value for static fields + * (see {@link ClassVisitor#visitField(int, String, String, String, Object)}) + * @return the resulting {@link FieldVisitor} + */ + public FieldVisitor toFieldVisitor(ClassVisitor delegate, Object value) { + return delegate.visitField(access, name, descriptor, signature, value); + } + + /** + * Returns the modifiers of this field header. + * Alias of {@link #access()}. + * + * @return the modifiers of this field header + */ + @Override + public int modifiers() { + return access; + } + + /** + * Returns a class object that identifies the declared type for the field represented by this field header. + * + * @return the declared type for this field header + */ + @SuppressWarnings("unchecked") + public Class getType() { + try { + return (Class) TransformationUtils.getClassForType(Type.getType(descriptor)); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + + /** + * Returns a new field header describing the specified field. + * + * @param declaringClass the class the field is declared in + * @param name the field's name + * @return the new field header object + */ + public static FieldHeader of(Class declaringClass, String name) { + try { + return new FieldHeader(declaringClass.getDeclaredField(name)); + } catch (NoSuchFieldException e) { + throw new RuntimeException(e); + } + } + + /** + * Two instances of {@link FieldHeader} are considered equal if their names are equal. + * TODO: include owner and parent classes if possible + * {@inheritDoc} + */ + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof FieldHeader that)) return false; + return Objects.equals(name, that.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } + + @Override + public String toString() { + String signatureString = "(signature: '%s')".formatted(signature); + return "%s %s %s#%s %s".formatted( + TransformationUtils.toHumanReadableModifiers(access), + TransformationUtils.toHumanReadableType(Type.getType(descriptor)), + TransformationUtils.toHumanReadableType(Type.getObjectType(owner)), + name, + signature != null ? signatureString : "" + ).trim(); + } +} diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/headers/Header.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/headers/Header.java new file mode 100644 index 00000000..e1386193 --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/headers/Header.java @@ -0,0 +1,71 @@ +package org.tudalgo.algoutils.transform.util.headers; + +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Type; +import org.tudalgo.algoutils.transform.util.Constants; +import org.tudalgo.algoutils.transform.util.TransformationUtils; + +import java.util.Arrays; + +import static org.objectweb.asm.Opcodes.ACONST_NULL; +import static org.objectweb.asm.Opcodes.DUP; +import static org.objectweb.asm.Opcodes.INVOKESPECIAL; +import static org.objectweb.asm.Opcodes.NEW; + +/** + * Common interface of all header records. + */ +public sealed interface Header permits ClassHeader, FieldHeader, MethodHeader { + + /** + * Returns the type for this header. + * + * @return the type for this header + */ + Type getHeaderType(); + + HeaderRecordComponent[] getComponents(); + + int modifiers(); + + /** + * Replicates the given header with bytecode instructions using the supplied method visitor. + * Upon return, a reference to the newly created header object is located at + * the top of the method visitor's stack. + * + * @param mv the method visitor to use + * @return the maximum stack size used during the operation + */ + default int buildHeader(MethodVisitor mv) { + Type headerType = getHeaderType(); + HeaderRecordComponent[] components = getComponents(); + int maxStack, stackSize; + + mv.visitTypeInsn(NEW, getHeaderType().getInternalName()); + mv.visitInsn(DUP); + maxStack = stackSize = 2; + for (HeaderRecordComponent component : components) { + Object value = component.value(); + if (component.type().equals(Constants.STRING_ARRAY_TYPE)) { + int stackUsed = TransformationUtils.buildArray(mv, Constants.STRING_TYPE, (Object[]) value); + maxStack = Math.max(maxStack, stackSize++ + stackUsed); + } else { + if (value != null) { + mv.visitLdcInsn(value); + } else { + mv.visitInsn(ACONST_NULL); + } + maxStack = Math.max(maxStack, ++stackSize); + } + } + mv.visitMethodInsn(INVOKESPECIAL, + headerType.getInternalName(), + "", + Type.getMethodDescriptor(Type.VOID_TYPE, Arrays.stream(components).map(HeaderRecordComponent::type).toArray(Type[]::new)), + false); + + return maxStack; + } + + record HeaderRecordComponent(Type type, Object value) {} +} diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/headers/MethodHeader.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/headers/MethodHeader.java new file mode 100644 index 00000000..6ae2850f --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/headers/MethodHeader.java @@ -0,0 +1,238 @@ +package org.tudalgo.algoutils.transform.util.headers; + +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Type; +import org.tudalgo.algoutils.transform.util.Constants; +import org.tudalgo.algoutils.transform.util.TransformationUtils; + +import java.lang.reflect.Executable; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import static org.objectweb.asm.Opcodes.*; + +/** + * A record holding information on the header of a method as declared in java bytecode. + * {@code owner} as well as the values of {@code exceptions} use the internal name + * of the corresponding class (see {@link Type#getInternalName()}). + * {@link Type#getMethodType(String)} can be used with {@code descriptor} to get a more user-friendly + * representation of this method's return type and parameter types. + * + * @param owner the method's owner or declaring class + * @param access the method's modifiers + * @param name the method's name + * @param descriptor the method's descriptor / parameter types + return type + * @param signature the method's signature, if using type parameters + * @param exceptions exceptions declared in the method's {@code throws} clause + * @author Daniel Mangold + */ +public record MethodHeader(String owner, int access, String name, String descriptor, String signature, String[] exceptions) implements Header { + + /** + * Constructs a new method header with only necessary information. + * This method header should not invoke {@link #toMethodVisitor(ClassVisitor)}, + * {@link #toMethodInsn(MethodVisitor, boolean)} or {@link #getOpcode()}. + * + * @param owner the method's owner or declaring class + * @param name the method's name + * @param descriptor the method's descriptor / parameter types + return type + */ + public MethodHeader(String owner, String name, String descriptor) { + this(owner, 0, name, descriptor, null, null); + } + + /** + * Constructs a new method header using the given method / constructor. + * + * @param executable a java reflection method or constructor + */ + public MethodHeader(Executable executable) { + this(Type.getInternalName(executable.getDeclaringClass()), + executable.getModifiers(), + executable instanceof Method method ? method.getName() : "", + executable instanceof Method method ? + Type.getMethodDescriptor(method) : + Type.getMethodDescriptor(Type.VOID_TYPE, Arrays.stream(executable.getParameterTypes()) + .map(Type::getType) + .toArray(Type[]::new)), + null, + Arrays.stream(executable.getExceptionTypes()) + .map(Type::getInternalName) + .toArray(String[]::new)); + } + + @Override + public Type getHeaderType() { + return Constants.METHOD_HEADER_TYPE; + } + + @Override + public HeaderRecordComponent[] getComponents() { + return new HeaderRecordComponent[] { + new HeaderRecordComponent(Constants.STRING_TYPE, owner), + new HeaderRecordComponent(Type.INT_TYPE, access), + new HeaderRecordComponent(Constants.STRING_TYPE, name), + new HeaderRecordComponent(Constants.STRING_TYPE, descriptor), + new HeaderRecordComponent(Constants.STRING_TYPE, signature), + new HeaderRecordComponent(Constants.STRING_ARRAY_TYPE, exceptions) + }; + } + + /** + * Visits a method in the given class visitor using the information stored in this record. + * + * @param delegate the class visitor to use + * @return the resulting {@link MethodVisitor} + */ + public MethodVisitor toMethodVisitor(ClassVisitor delegate) { + return delegate.visitMethod(access, name, descriptor, signature, exceptions); + } + + /** + * Visits a method instruction in the given method visitor using the information stored in this record. + * + * @param methodVisitor the method visitor to use + * @param isInterface true, if the method's owner is an interface + */ + public void toMethodInsn(MethodVisitor methodVisitor, boolean isInterface) { + int opcode = isInterface ? INVOKEINTERFACE : getOpcode(); + methodVisitor.visitMethodInsn(opcode, + owner, + name, + descriptor, + isInterface); + } + + /** + * Returns the opcode needed to invoke this method (except INVOKEINTERFACE since it also depends on the class). + * + * @return the opcode + */ + public int getOpcode() { + if ((access & ACC_STATIC) != 0) { + return INVOKESTATIC; + } else if (name.equals("")) { + return INVOKESPECIAL; + } else if (TransformationUtils.isLambdaMethod(access, name)) { + return INVOKEDYNAMIC; + } else { + return INVOKEVIRTUAL; + } + } + + /** + * Returns the modifiers of this method header. + * Alias of {@link #access()}. + * + * @return the modifiers of this method header + */ + @Override + public int modifiers() { + return access; + } + + /** + * Returns a class object that identifies the declared return type for + * the method represented by this method header. + * + * @return the declared return type for this method + */ + @SuppressWarnings("unchecked") + public Class getReturnType() { + try { + return (Class) TransformationUtils.getClassForType(Type.getReturnType(descriptor)); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + + /** + * Returns a list of class objects that identify the declared parameter types for + * the method represented by this method header. + * + * @return the declared parameter types for this method + */ + @SuppressWarnings("unchecked") + public List> getParameterTypes() { + return Arrays.stream(Type.getArgumentTypes(descriptor)) + .map(type -> { + try { + return (Class) TransformationUtils.getClassForType(type); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + }).toList(); + } + + /** + * Returns a new method header describing the specified constructor. + * + * @param declaringClass the class the constructor is declared in + * @param parameterTypes the constructor's parameter types + * @return the new method header object + */ + public static MethodHeader of(Class declaringClass, Class... parameterTypes) { + try { + return new MethodHeader(declaringClass.getDeclaredConstructor(parameterTypes)); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + + /** + * Returns a new method header describing the specified method. + * For constructors use {@link #of(Class, Class...)}. + * + * @param declaringClass the class the method is declared in + * @param name the method's name + * @param parameterTypes the method's parameter types + * @return the new method header object + */ + public static MethodHeader of(Class declaringClass, String name, Class... parameterTypes) { + try { + return new MethodHeader(declaringClass.getDeclaredMethod(name, parameterTypes)); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + + /** + * Two instances of {@link MethodHeader} are considered equal if their names and descriptors are equal. + * TODO: include owner and parent classes if possible + * {@inheritDoc} + */ + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof MethodHeader that)) return false; + return Objects.equals(name, that.name) && Objects.equals(descriptor, that.descriptor); + } + + @Override + public int hashCode() { + return Objects.hash(name, descriptor); + } + + @Override + public String toString() { + String signatureString = "(signature: '%s')".formatted(signature); + String exceptionsString = "throws %s".formatted(Arrays.stream(exceptions == null ? new String[0] : exceptions) + .map(s -> s.replace('/', '.')) + .collect(Collectors.joining(", "))); + return "%s %s %s#%s(%s) %s %s".formatted( + TransformationUtils.toHumanReadableModifiers(access), + TransformationUtils.toHumanReadableType(Type.getReturnType(descriptor)), + TransformationUtils.toHumanReadableType(Type.getObjectType(owner)), + name, + Arrays.stream(Type.getArgumentTypes(descriptor)) + .map(TransformationUtils::toHumanReadableType) + .collect(Collectors.joining(", ")), + exceptions != null && exceptions.length > 0 ? exceptionsString : "", + signature != null ? signatureString : "" + ).trim(); + } +} diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/matching/ClassSimilarityMapper.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/matching/ClassSimilarityMapper.java new file mode 100644 index 00000000..8647f624 --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/matching/ClassSimilarityMapper.java @@ -0,0 +1,29 @@ +package org.tudalgo.algoutils.transform.util.matching; + +import org.tudalgo.algoutils.tutor.general.match.MatchingUtils; + +import java.util.*; +import java.util.stream.Stream; + +public class ClassSimilarityMapper extends SimilarityMapper { + + /** + * Creates a new {@link ClassSimilarityMapper} instance. + * + * @param submissionClassNames the submission classes to map from + * @param solutionClassNames the solution classes to map to, with the map's values being aliases of the key + * @param similarityThreshold the minimum similarity two values need to have to be considered a match + */ + public ClassSimilarityMapper(Collection submissionClassNames, + Map> solutionClassNames, + double similarityThreshold) { + super(submissionClassNames, solutionClassNames.keySet(), similarityThreshold); + + computeSimilarity((submissionClassName, solutionClassName) -> + Stream.concat(Stream.of(solutionClassName), solutionClassNames.get(solutionClassName).stream()) + .mapToDouble(value -> MatchingUtils.similarity(submissionClassName, value)) + .max() + .orElse(0)); + removeDuplicateMappings(); + } +} diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/matching/FieldSimilarityMapper.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/matching/FieldSimilarityMapper.java new file mode 100644 index 00000000..0e2b641f --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/matching/FieldSimilarityMapper.java @@ -0,0 +1,27 @@ +package org.tudalgo.algoutils.transform.util.matching; + +import org.tudalgo.algoutils.transform.util.headers.FieldHeader; +import org.tudalgo.algoutils.transform.util.TransformationContext; +import org.tudalgo.algoutils.tutor.general.match.MatchingUtils; + +import java.util.Collection; + +public class FieldSimilarityMapper extends SimilarityMapper { + + /** + * Creates a new {@link FieldSimilarityMapper} instance. + * + * @param submissionFields the field headers to map from + * @param solutionFields the field headers to map to + * @param transformationContext the transformation context + */ + public FieldSimilarityMapper(Collection submissionFields, + Collection solutionFields, + TransformationContext transformationContext) { + super(submissionFields, solutionFields, transformationContext.getSimilarity()); + + computeSimilarity((submissionField, solutionField) -> + MatchingUtils.similarity(submissionField.name(), solutionField.name())); + removeDuplicateMappings(); + } +} diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/matching/MethodSimilarityMapper.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/matching/MethodSimilarityMapper.java new file mode 100644 index 00000000..22086893 --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/matching/MethodSimilarityMapper.java @@ -0,0 +1,33 @@ +package org.tudalgo.algoutils.transform.util.matching; + +import org.tudalgo.algoutils.transform.util.headers.MethodHeader; +import org.tudalgo.algoutils.transform.util.TransformationContext; +import org.tudalgo.algoutils.tutor.general.match.MatchingUtils; + +import java.util.*; + +public class MethodSimilarityMapper extends SimilarityMapper { + + /** + * Creates a new {@link MethodSimilarityMapper} instance. + * + * @param submissionMethods the method headers to map from + * @param solutionMethods the method headers to map to + * @param transformationContext the transformation context + */ + public MethodSimilarityMapper(Collection submissionMethods, + Collection solutionMethods, + TransformationContext transformationContext) { + super(submissionMethods, solutionMethods, transformationContext.getSimilarity()); + + computeSimilarity((submissionMethod, solutionMethod) -> { + String computedDescriptor = transformationContext.toComputedDescriptor(submissionMethod.descriptor()); + if (!computedDescriptor.equals(solutionMethod.descriptor())) { + return 0d; + } else { + return MatchingUtils.similarity(submissionMethod.name() + computedDescriptor, solutionMethod.name() + solutionMethod.descriptor()); + } + }); + removeDuplicateMappings(); + } +} diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/matching/SimilarityMapper.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/matching/SimilarityMapper.java new file mode 100644 index 00000000..158bbd20 --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/matching/SimilarityMapper.java @@ -0,0 +1,59 @@ +package org.tudalgo.algoutils.transform.util.matching; + +import java.util.*; +import java.util.function.BiFunction; + +public abstract class SimilarityMapper { + + protected final List rows; + protected final List columns; + protected final double[][] similarityMatrix; + protected final double similarityThreshold; + protected final Map bestMatches = new HashMap<>(); // row => column + + public SimilarityMapper(Collection rows, Collection columns, double similarityThreshold) { + this.rows = new ArrayList<>(rows); + this.columns = new ArrayList<>(columns); + this.similarityMatrix = new double[rows.size()][columns.size()]; + this.similarityThreshold = similarityThreshold; + } + + protected void computeSimilarity(BiFunction similarityFunction) { + for (int rowIndex = 0; rowIndex < similarityMatrix.length; rowIndex++) { + T row = rows.get(rowIndex); + int bestMatchIndex = -1; + double bestSimilarity = similarityThreshold; + + for (int colIndex = 0; colIndex < similarityMatrix[rowIndex].length; colIndex++) { + similarityMatrix[rowIndex][colIndex] = similarityFunction.apply(row, columns.get(colIndex)); + if (similarityMatrix[rowIndex][colIndex] >= bestSimilarity) { + bestMatchIndex = colIndex; + bestSimilarity = similarityMatrix[rowIndex][colIndex]; + } + } + if (bestMatchIndex >= 0) { + bestMatches.put(row, columns.get(bestMatchIndex)); + } + } + } + + protected void removeDuplicateMappings() { + Map> reverseMappings = new HashMap<>(); + bestMatches.forEach((row, col) -> reverseMappings.computeIfAbsent(col, k -> new Stack<>()).add(row)); + reverseMappings.forEach((col, rows) -> { + rows.sort(Comparator.comparingDouble(row -> similarityMatrix[this.rows.indexOf(row)][columns.indexOf(col)])); + rows.pop(); // exclude best match + rows.forEach(bestMatches::remove); // remove the rest + }); + } + + /** + * Returns the best match for the given value, wrapped in an optional. + * + * @param t the value to find the best match for + * @return an optional wrapping the best match + */ + public Optional getBestMatch(T t) { + return Optional.ofNullable(bestMatches.get(t)); + } +} diff --git a/tutor/src/main/java/org/tudalgo/algoutils/tutor/general/SubmissionInfo.java b/tutor/src/main/java/org/tudalgo/algoutils/tutor/general/SubmissionInfo.java new file mode 100644 index 00000000..7c251b9d --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/tutor/general/SubmissionInfo.java @@ -0,0 +1,21 @@ +package org.tudalgo.algoutils.tutor.general; + +import java.util.List; +import java.util.Map; + +public record SubmissionInfo( + String assignmentId, + String jagrVersion, + List sourceSets, + DependencyConfiguration dependencyConfigurations, + List repositoryConfigurations, + String studentId, + String firstName, + String lastName +) { + public record SourceSet(String name, Map> files) {} + + public record DependencyConfiguration(List api, List implementation, List testImplementation) {} + + public record RepositoryConfiguration(String name, String url) {} +}