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:
+ *
+ *
Types: the fully qualified name of the type (e.g., {@code java.lang.Object})
+ *
Fields: the name / identifier of the field
+ *
Constructors: Always {@code }, regardless of the class
+ *
Methods: the name / identifier of the method
+ *
+ *
+ * @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.
+ *
+ * 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:
+ *
+ *
+ * Unless otherwise specified, this transformer and its tools use the internal name as specified by
+ * {@link Type#getInternalName()} when referring to class names.
+ *
+ *
+ * The term "descriptor" refers to the bytecode-level representation of types, such as
+ * field types, method return types or method parameter types as specified by
+ * Chapter 4.3
+ * of the Java Virtual Machine Specification.
+ *
+ *
+ *
+ * @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:
+ *
+ *
+ * @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