From 485846184715218cd2196c4267461f48ba4dfd74 Mon Sep 17 00:00:00 2001 From: Daniel Mangold Date: Fri, 15 Nov 2024 09:38:40 +0100 Subject: [PATCH 01/48] Add prototype for full project instrumentation --- gradle/libs.versions.toml | 1 + .../student/annotation/ForceSignature.java | 73 +++ tutor/build.gradle.kts | 1 + .../transform/SolutionClassNode.java | 131 +++++ .../SolutionMergingClassTransformer.java | 101 ++++ .../transform/SubmissionClassInfo.java | 298 +++++++++++ .../transform/SubmissionClassVisitor.java | 503 ++++++++++++++++++ .../transform/SubmissionExecutionHandler.java | 382 +++++++++++++ .../algoutils/transform/util/ClassHeader.java | 82 +++ .../algoutils/transform/util/FieldHeader.java | 80 +++ .../ForceSignatureAnnotationProcessor.java | 314 +++++++++++ .../algoutils/transform/util/Header.java | 12 + .../algoutils/transform/util/Invocation.java | 183 +++++++ .../transform/util/MethodHeader.java | 106 ++++ .../transform/util/TransformationContext.java | 33 ++ .../transform/util/TransformationUtils.java | 253 +++++++++ 16 files changed, 2553 insertions(+) create mode 100644 student/src/main/java/org/tudalgo/algoutils/student/annotation/ForceSignature.java create mode 100644 tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionClassNode.java create mode 100644 tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionMergingClassTransformer.java create mode 100644 tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassInfo.java create mode 100644 tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java create mode 100644 tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionExecutionHandler.java create mode 100644 tutor/src/main/java/org/tudalgo/algoutils/transform/util/ClassHeader.java create mode 100644 tutor/src/main/java/org/tudalgo/algoutils/transform/util/FieldHeader.java create mode 100644 tutor/src/main/java/org/tudalgo/algoutils/transform/util/ForceSignatureAnnotationProcessor.java create mode 100644 tutor/src/main/java/org/tudalgo/algoutils/transform/util/Header.java create mode 100644 tutor/src/main/java/org/tudalgo/algoutils/transform/util/Invocation.java create mode 100644 tutor/src/main/java/org/tudalgo/algoutils/transform/util/MethodHeader.java create mode 100644 tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationContext.java create mode 100644 tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationUtils.java 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/SolutionClassNode.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionClassNode.java new file mode 100644 index 00000000..b641acbf --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionClassNode.java @@ -0,0 +1,131 @@ +package org.tudalgo.algoutils.transform; + +import org.tudalgo.algoutils.transform.util.ClassHeader; +import org.tudalgo.algoutils.transform.util.FieldHeader; +import org.tudalgo.algoutils.transform.util.MethodHeader; +import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.Handle; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.FieldNode; +import org.objectweb.asm.tree.MethodNode; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import static org.objectweb.asm.Opcodes.ACC_SYNTHETIC; +import static org.objectweb.asm.Opcodes.ASM9; + +/** + * A class node for recording bytecode instructions of solution classes. + * @author Daniel Mangold + */ +public class SolutionClassNode extends ClassNode { + + 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(String className) { + super(Opcodes.ASM9); + 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, access, name, descriptor, signature); + FieldNode fieldNode = (FieldNode) super.visitField(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) { + MethodNode methodNode; + if ((access & ACC_SYNTHETIC) != 0 && name.startsWith("lambda$")) { // if method is lambda + methodNode = getMethodNode(access, name + "$solution", descriptor, signature, exceptions); + } else { + methodNode = getMethodNode(access, name, descriptor, signature, exceptions); + methods.put(new MethodHeader(className, 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) { + return new MethodNode(ASM9, access, name, descriptor, signature, exceptions) { + @Override + public void visitMethodInsn(int opcodeAndSource, String owner, String name, String descriptor, boolean isInterface) { + 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()); + } + }; + } +} 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..b4cd8366 --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionMergingClassTransformer.java @@ -0,0 +1,101 @@ +package org.tudalgo.algoutils.transform; + +import org.tudalgo.algoutils.student.annotation.ForceSignature; +import org.tudalgo.algoutils.transform.util.TransformationContext; +import org.tudalgo.algoutils.transform.util.TransformationUtils; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.Type; +import org.sourcegrade.jagr.api.testing.ClassTransformer; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 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: + *
    + *
  • + * 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 + */ +public class SolutionMergingClassTransformer implements ClassTransformer { + + /** + * An object providing context throughout the transformation processing chain. + */ + private final TransformationContext transformationContext; + + /** + * 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, List availableSolutionClasses) { + Map solutionClasses = new HashMap<>(); + Map submissionClasses = new ConcurrentHashMap<>(); + this.transformationContext = new TransformationContext(projectPrefix, solutionClasses, submissionClasses); + availableSolutionClasses.stream() + .map(s -> s.replace('.', '/')) + .forEach(className -> solutionClasses.put(className, TransformationUtils.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) { + String submissionClassName = reader.getClassName(); + reader.accept(new SubmissionClassVisitor(writer, transformationContext, submissionClassName), 0); + } +} diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassInfo.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassInfo.java new file mode 100644 index 00000000..b5ca8254 --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassInfo.java @@ -0,0 +1,298 @@ +package org.tudalgo.algoutils.transform; + +import org.tudalgo.algoutils.transform.util.*; +import kotlin.Pair; +import kotlin.Triple; +import org.objectweb.asm.*; +import org.tudalgo.algoutils.tutor.general.match.MatchingUtils; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.*; + +import static org.objectweb.asm.Opcodes.ACC_SYNTHETIC; + +/** + * 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 itself to remain usable. + * + * @author Daniel Mangold + */ +public class SubmissionClassInfo extends ClassVisitor { + + private final TransformationContext transformationContext; + private final String originalClassName; + private final String computedClassName; + private final Set, Map>> superClassMembers = new HashSet<>(); + private final ForceSignatureAnnotationProcessor fsAnnotationProcessor; + private final SolutionClassNode solutionClass; + + private String superClass; + private String[] interfaces; + + private ClassHeader submissionClassHeader; + + // Mapping of fields in submission => usable fields + private final Map fields = new HashMap<>(); + + // Mapping of methods in submission => usable methods + private final Map methods = new HashMap<>(); + + /** + * Constructs a new {@link SubmissionClassInfo} instance. + * + * @param transformationContext a {@link TransformationContext} object + * @param className the name of the submission class + * @param fsAnnotationProcessor a {@link ForceSignatureAnnotationProcessor} for the submission class + */ + public SubmissionClassInfo(TransformationContext transformationContext, + String className, + ForceSignatureAnnotationProcessor fsAnnotationProcessor) { + super(Opcodes.ASM9); + this.transformationContext = transformationContext; + this.originalClassName = className; + this.fsAnnotationProcessor = fsAnnotationProcessor; + + if (fsAnnotationProcessor.classIdentifierIsForced()) { + this.computedClassName = fsAnnotationProcessor.forcedClassIdentifier(); + } else { + // If not forced, get the closest matching solution class (at least 90% similarity) + this.computedClassName = transformationContext.solutionClasses() + .keySet() + .stream() + .map(s -> new Pair<>(s, MatchingUtils.similarity(originalClassName, s))) + .filter(pair -> pair.getSecond() >= 0.90) + .max(Comparator.comparing(Pair::getSecond)) + .map(Pair::getFirst) + .orElse(originalClassName); + } + this.solutionClass = transformationContext.solutionClasses().get(computedClassName); + } + + /** + * Returns the original class header. + * + * @return the original class header + */ + public ClassHeader getOriginalClassHeader() { + return submissionClassHeader; + } + + /** + * Returns the computed class name. + * The computed name is the name of the associated solution class, if one is present. + * If no solution class is present, the computed names equals the original submission class name. + * + * @return the computed class name + */ + public String getComputedClassName() { + return computedClassName; + } + + /** + * 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); + } + + /** + * Returns the original field headers for this class. + * + * @return the original field headers + */ + public Set getOriginalFieldHeaders() { + return fields.keySet(); + } + + /** + * 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 FieldHeader getComputedFieldHeader(String name) { + return fields.entrySet() + .stream() + .filter(entry -> entry.getKey().name().equals(name)) + .findAny() + .map(Map.Entry::getValue) + .orElseThrow(); + } + + /** + * Return the original method headers for this class. + * + * @return the original method headers + */ + public Set getOriginalMethodHeaders() { + return methods.keySet(); + } + + /** + * 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 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 void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + submissionClassHeader = new ClassHeader(access, name, signature, superName, interfaces); + resolveSuperClassMembers(superClassMembers, this.superClass = superName, this.interfaces = interfaces); + } + + @Override + public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) { + FieldHeader submissionFieldHeader = new FieldHeader(originalClassName, access, name, descriptor, signature); + FieldHeader solutionFieldHeader; + if (fsAnnotationProcessor.fieldIdentifierIsForced(name)) { + solutionFieldHeader = fsAnnotationProcessor.forcedFieldHeader(name); + } else if (solutionClass != null) { + solutionFieldHeader = solutionClass.getFields() + .keySet() + .stream() + .map(fieldHeader -> new Pair<>(fieldHeader, MatchingUtils.similarity(name, fieldHeader.name()))) + .filter(pair -> pair.getSecond() >= 0.90) + .max(Comparator.comparing(Pair::getSecond)) + .map(Pair::getFirst) + .orElse(submissionFieldHeader); + } else { + solutionFieldHeader = submissionFieldHeader; + } + + fields.put(submissionFieldHeader, solutionFieldHeader); + return null; + } + + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + MethodHeader submissionMethodHeader = new MethodHeader(originalClassName, access, name, descriptor, signature, exceptions); + if ((access & ACC_SYNTHETIC) != 0 && name.startsWith("lambda$")) { + methods.put(submissionMethodHeader, submissionMethodHeader); + return null; + } + + MethodHeader solutionMethodHeader; + if (fsAnnotationProcessor.methodSignatureIsForced(name, descriptor)) { + solutionMethodHeader = fsAnnotationProcessor.forcedMethodHeader(name, descriptor); + } else if (solutionClass != null) { + solutionMethodHeader = solutionClass.getMethods() + .keySet() + .stream() + .map(methodHeader -> new Triple<>(methodHeader, + MatchingUtils.similarity(name, methodHeader.name()), + MatchingUtils.similarity(descriptor, methodHeader.descriptor()))) + .filter(triple -> triple.getSecond() >= 0.90 && triple.getThird() >= 0.90) + .max(Comparator.comparing(Triple::getSecond).thenComparing(Triple::getThird)) + .map(Triple::getFirst) + .orElse(submissionMethodHeader); + } else { + solutionMethodHeader = submissionMethodHeader; + } + + methods.put(submissionMethodHeader, solutionMethodHeader); + return null; + } + + @Override + public void visitEnd() { + for (Triple, Map> triple : superClassMembers) { + triple.getSecond().forEach(fields::putIfAbsent); + triple.getThird().forEach(methods::putIfAbsent); + } + } + + /** + * Recursively resolves the members of superclasses and interfaces. + * + * @param superClassMembers a set for recording class members + * @param superClass the name of the superclass to process + * @param interfaces the names of the interfaces to process + */ + private void resolveSuperClassMembers(Set, Map>> superClassMembers, + String superClass, + String[] interfaces) { + resolveSuperClassMembers(superClassMembers, superClass); + if (interfaces != null) { + for (String interfaceName : interfaces) { + resolveSuperClassMembers(superClassMembers, interfaceName); + } + } + } + + /** + * Recursively resolves the members of the given class. + * + * @param superClassMembers a set for recording class members + * @param className the name of the class / interface to process + */ + private void resolveSuperClassMembers(Set, Map>> superClassMembers, + String className) { + if (className.startsWith(transformationContext.projectPrefix())) { + SubmissionClassInfo submissionClassInfo = transformationContext.getSubmissionClassInfo(className); + superClassMembers.add(new Triple<>(className, submissionClassInfo.fields, submissionClassInfo.methods)); + resolveSuperClassMembers(superClassMembers, submissionClassInfo.superClass, submissionClassInfo.interfaces); + } else { + try { + Class clazz = Class.forName(className.replace('/', '.')); + Map fieldHeaders = new HashMap<>(); + for (Field field : clazz.getDeclaredFields()) { + if ((field.getModifiers() & Modifier.PRIVATE) != 0) continue; + FieldHeader fieldHeader = new FieldHeader( + className, + field.getModifiers(), + field.getName(), + Type.getDescriptor(field.getType()), + null + ); + fieldHeaders.put(fieldHeader, fieldHeader); + } + Map methodHeaders = new HashMap<>(); + for (Method method : clazz.getDeclaredMethods()) { + if ((method.getModifiers() & Modifier.PRIVATE) != 0) continue; + MethodHeader methodHeader = new MethodHeader( + className, + method.getModifiers(), + method.getName(), + Type.getMethodDescriptor(method), + null, + Arrays.stream(method.getExceptionTypes()).map(Type::getInternalName).toArray(String[]::new) + ); + methodHeaders.put(methodHeader, methodHeader); + } + superClassMembers.add(new Triple<>(className, fieldHeaders, methodHeaders)); + if (clazz.getSuperclass() != null) { + resolveSuperClassMembers(superClassMembers, + Type.getInternalName(clazz.getSuperclass()), + Arrays.stream(clazz.getInterfaces()).map(Type::getInternalName).toArray(String[]::new)); + } + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + } +} diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java new file mode 100644 index 00000000..742a4e2b --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java @@ -0,0 +1,503 @@ +package org.tudalgo.algoutils.transform; + +import org.tudalgo.algoutils.transform.util.*; +import org.objectweb.asm.*; +import org.objectweb.asm.tree.FieldNode; +import org.objectweb.asm.tree.MethodNode; + +import java.util.*; +import java.util.stream.Collectors; + +import static org.tudalgo.algoutils.transform.util.TransformationUtils.*; +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}. + * + * @see SubmissionExecutionHandler + * @author Daniel Mangold + */ +class SubmissionClassVisitor extends ClassVisitor { + + private final boolean defaultTransformationsOnly; + private final TransformationContext transformationContext; + private final String className; + private final SubmissionClassInfo submissionClassInfo; + + private final Set visitedFields = new HashSet<>(); + private final Map solutionFieldNodes; + + private final Set visitedMethods = new HashSet<>(); + private final Map solutionMethodNodes; + + SubmissionClassVisitor(ClassVisitor classVisitor, + TransformationContext transformationContext, + String submissionClassName) { + super(ASM9, classVisitor); + this.transformationContext = transformationContext; + this.className = transformationContext.getSubmissionClassInfo(submissionClassName).getComputedClassName(); + this.submissionClassInfo = transformationContext.getSubmissionClassInfo(submissionClassName); + + Optional solutionClass = submissionClassInfo.getSolutionClass(); + if (solutionClass.isPresent()) { + this.defaultTransformationsOnly = false; + this.solutionFieldNodes = solutionClass.get().getFields(); + this.solutionMethodNodes = solutionClass.get().getMethods(); + } else { + System.err.printf("No corresponding solution class found for %s. Only applying default transformations.%n", submissionClassName); + this.defaultTransformationsOnly = true; + this.solutionFieldNodes = Collections.emptyMap(); + this.solutionMethodNodes = Collections.emptyMap(); + } + } + + /** + * 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) { + submissionClassInfo.getSolutionClass() + .map(SolutionClassNode::getClassHeader) + .orElse(submissionClassInfo.getOriginalClassHeader()) + .visitClass(getDelegate(), version); +// .visitClass(getDelegate(), version, Type.getInternalName(SubmissionClassMetadata.class)); + } + + /** + * 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) { + if (defaultTransformationsOnly) { + return super.visitField(access, name, descriptor, signature, value); + } + + FieldHeader fieldHeader = submissionClassInfo.getComputedFieldHeader(name); + 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. + */ + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + MethodHeader methodHeader = submissionClassInfo.getComputedMethodHeader(name, descriptor); + visitedMethods.add(methodHeader); + boolean isStatic = (methodHeader.access() & ACC_STATIC) != 0; + boolean isConstructor = methodHeader.name().equals(""); + + // calculate length of locals array, including "this" if applicable + int submissionExecutionHandlerIndex = (Type.getArgumentsAndReturnSizes(methodHeader.descriptor()) >> 2) - (isStatic ? 1 : 0); + int methodHeaderIndex = submissionExecutionHandlerIndex + 1; + + // calculate default locals for frames + List parameterTypes = Arrays.stream(Type.getArgumentTypes(methodHeader.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) { + parameterTypes.addFirst(isConstructor ? UNINITIALIZED_THIS : className); + } + Object[] fullFrameLocals = parameterTypes.toArray(); + + return new MethodVisitor(ASM9, methodHeader.toMethodVisitor(getDelegate())) { + @Override + public void visitCode() { + // if method is abstract or lambda, skip transformation + if ((methodHeader.access() & ACC_ABSTRACT) != 0 || + ((methodHeader.access() & ACC_SYNTHETIC) != 0 && methodHeader.name().startsWith("lambda$"))) { + super.visitCode(); + return; + } + + Label submissionExecutionHandlerVarLabel = new Label(); + Label methodHeaderVarLabel = new Label(); + Label substitutionCheckLabel = new Label(); + Label delegationCheckLabel = new Label(); + Label delegationCodeLabel = new Label(); + Label submissionCodeLabel = new Label(); + + // create SubmissionExecutionHandler$Internal instance and store in locals array + super.visitTypeInsn(NEW, SubmissionExecutionHandler.Internal.INTERNAL_TYPE.getInternalName()); + super.visitInsn(DUP); + super.visitMethodInsn(INVOKESTATIC, + SubmissionExecutionHandler.INTERNAL_TYPE.getInternalName(), + "getInstance", + Type.getMethodDescriptor(SubmissionExecutionHandler.INTERNAL_TYPE), + false); + super.visitMethodInsn(INVOKESPECIAL, + SubmissionExecutionHandler.Internal.INTERNAL_TYPE.getInternalName(), + "", + Type.getMethodDescriptor(Type.VOID_TYPE, SubmissionExecutionHandler.INTERNAL_TYPE), + false); + super.visitVarInsn(ASTORE, submissionExecutionHandlerIndex); + super.visitLabel(submissionExecutionHandlerVarLabel); + + // replicate method header in bytecode and store in locals array + buildMethodHeader(getDelegate(), methodHeader); + super.visitVarInsn(ASTORE, methodHeaderIndex); + super.visitLabel(methodHeaderVarLabel); + + super.visitFrame(F_APPEND, + 2, + new Object[] {SubmissionExecutionHandler.Internal.INTERNAL_TYPE.getInternalName(), MethodHeader.INTERNAL_TYPE.getInternalName()}, + 0, + null); + + // check if invocation should be logged + super.visitVarInsn(ALOAD, submissionExecutionHandlerIndex); + super.visitVarInsn(ALOAD, methodHeaderIndex); + super.visitMethodInsn(INVOKEVIRTUAL, + SubmissionExecutionHandler.Internal.INTERNAL_TYPE.getInternalName(), + "logInvocation", + Type.getMethodDescriptor(Type.BOOLEAN_TYPE, MethodHeader.INTERNAL_TYPE), + false); + super.visitJumpInsn(IFEQ, isConstructor ? // jump to label if logInvocation(...) == false + defaultTransformationsOnly ? submissionCodeLabel : delegationCheckLabel : + substitutionCheckLabel); + + // intercept parameters + super.visitVarInsn(ALOAD, submissionExecutionHandlerIndex); + super.visitVarInsn(ALOAD, methodHeaderIndex); + buildInvocation(Type.getArgumentTypes(methodHeader.descriptor())); + super.visitMethodInsn(INVOKEVIRTUAL, + SubmissionExecutionHandler.Internal.INTERNAL_TYPE.getInternalName(), + "addInvocation", + Type.getMethodDescriptor(Type.VOID_TYPE, + MethodHeader.INTERNAL_TYPE, + Invocation.INTERNAL_TYPE), + false); + + // check if substitution exists for this method if not constructor (because waaay too complex right now) + if (!isConstructor) { + super.visitFrame(F_SAME, 0, null, 0, null); + super.visitLabel(substitutionCheckLabel); + super.visitVarInsn(ALOAD, submissionExecutionHandlerIndex); + super.visitVarInsn(ALOAD, methodHeaderIndex); + super.visitMethodInsn(INVOKEVIRTUAL, + SubmissionExecutionHandler.Internal.INTERNAL_TYPE.getInternalName(), + "useSubstitution", + Type.getMethodDescriptor(Type.BOOLEAN_TYPE, MethodHeader.INTERNAL_TYPE), + false); + super.visitJumpInsn(IFEQ, defaultTransformationsOnly ? submissionCodeLabel : delegationCheckLabel); // jump to label if useSubstitution(...) == false + + // get substitution and execute it + super.visitVarInsn(ALOAD, submissionExecutionHandlerIndex); + super.visitVarInsn(ALOAD, methodHeaderIndex); + super.visitMethodInsn(INVOKEVIRTUAL, + SubmissionExecutionHandler.Internal.INTERNAL_TYPE.getInternalName(), + "getSubstitution", + Type.getMethodDescriptor(Invocation.INTERNAL_TYPE, + MethodHeader.INTERNAL_TYPE), + false); + buildInvocation(Type.getArgumentTypes(methodHeader.descriptor())); + super.visitMethodInsn(INVOKEINTERFACE, + SubmissionExecutionHandler.MethodSubstitution.INTERNAL_TYPE.getInternalName(), + "execute", + Type.getMethodDescriptor(Type.getType(Object.class), + Invocation.INTERNAL_TYPE), + true); + Type returnType = Type.getReturnType(methodHeader.descriptor()); + if (returnType.getSort() == Type.ARRAY || returnType.getSort() == Type.OBJECT) { + super.visitTypeInsn(CHECKCAST, returnType.getInternalName()); + } else { + unboxType(getDelegate(), returnType); + } + super.visitInsn(returnType.getOpcode(IRETURN)); + } + + // if only default transformations are applied, skip delegation + if (!defaultTransformationsOnly) { + // check if call should be delegated to solution or not + super.visitFrame(F_SAME, 0, null, 0, null); + super.visitLabel(delegationCheckLabel); + super.visitVarInsn(ALOAD, submissionExecutionHandlerIndex); + super.visitVarInsn(ALOAD, methodHeaderIndex); + super.visitMethodInsn(INVOKEVIRTUAL, + SubmissionExecutionHandler.Internal.INTERNAL_TYPE.getInternalName(), + "useSubmissionImpl", + Type.getMethodDescriptor(Type.BOOLEAN_TYPE, MethodHeader.INTERNAL_TYPE), + false); + super.visitJumpInsn(IFNE, submissionCodeLabel); // jump to label if useSubmissionImpl(...) == true + + // replay instructions from solution + super.visitFrame(F_CHOP, 2, null, 0, null); + super.visitLabel(delegationCodeLabel); + super.visitLocalVariable("submissionExecutionHandler", + SubmissionExecutionHandler.Internal.INTERNAL_TYPE.getDescriptor(), + null, + submissionExecutionHandlerVarLabel, + delegationCodeLabel, + submissionExecutionHandlerIndex); + super.visitLocalVariable("methodHeader", + MethodHeader.INTERNAL_TYPE.getDescriptor(), + null, + methodHeaderVarLabel, + delegationCodeLabel, + methodHeaderIndex); + solutionMethodNodes.get(methodHeader).accept(getDelegate()); + + super.visitFrame(F_FULL, fullFrameLocals.length, fullFrameLocals, 0, new Object[0]); + super.visitLabel(submissionCodeLabel); + } else { + super.visitFrame(F_CHOP, 2, null, 0, null); + super.visitLabel(submissionCodeLabel); + super.visitLocalVariable("submissionExecutionHandler", + SubmissionExecutionHandler.Internal.INTERNAL_TYPE.getDescriptor(), + null, + submissionExecutionHandlerVarLabel, + submissionCodeLabel, + submissionExecutionHandlerIndex); + super.visitLocalVariable("methodHeader", + MethodHeader.INTERNAL_TYPE.getDescriptor(), + null, + methodHeaderVarLabel, + submissionCodeLabel, + methodHeaderIndex); + } + + // visit original code + super.visitCode(); + } + + @Override + public void visitFieldInsn(int opcode, String owner, String name, String descriptor) { + // skip transformation if only default transformations are applied or owner is not part of the submission + if (defaultTransformationsOnly || !owner.startsWith(transformationContext.projectPrefix())) { + super.visitFieldInsn(opcode, owner, name, descriptor); + } else { + FieldHeader fieldHeader = transformationContext.getSubmissionClassInfo(owner).getComputedFieldHeader(name); + super.visitFieldInsn(opcode, fieldHeader.owner(), fieldHeader.name(), fieldHeader.descriptor()); + } + } + + @Override + public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) { + // skip transformation if only default transformations are applied or owner is not part of the submission + if (defaultTransformationsOnly || !owner.startsWith(transformationContext.projectPrefix())) { + super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); + } else { + MethodHeader methodHeader = transformationContext.getSubmissionClassInfo(owner).getComputedMethodHeader(name, descriptor); + super.visitMethodInsn(opcode, methodHeader.owner(), methodHeader.name(), methodHeader.descriptor(), isInterface); + } + } + + /** + * Builds an {@link Invocation} in bytecode. + * + * @param argumentTypes an array of parameter types + */ + private void buildInvocation(Type[] argumentTypes) { + super.visitTypeInsn(NEW, Invocation.INTERNAL_TYPE.getInternalName()); + super.visitInsn(DUP); + if (!isStatic && !isConstructor) { + super.visitVarInsn(ALOAD, 0); + super.visitMethodInsn(INVOKESPECIAL, + Invocation.INTERNAL_TYPE.getInternalName(), + "", + "(Ljava/lang/Object;)V", + false); + } else { + super.visitMethodInsn(INVOKESPECIAL, + Invocation.INTERNAL_TYPE.getInternalName(), + "", + "()V", + false); + } + for (int i = 0; i < argumentTypes.length; i++) { + super.visitInsn(DUP); + // load parameter with opcode (ALOAD, ILOAD, etc.) for type and ignore "this", if it exists + super.visitVarInsn(argumentTypes[i].getOpcode(ILOAD), getLocalsIndex(argumentTypes, i) + (isStatic ? 0 : 1)); + boxType(getDelegate(), argumentTypes[i]); + super.visitMethodInsn(INVOKEVIRTUAL, + Invocation.INTERNAL_TYPE.getInternalName(), + "addParameter", + "(Ljava/lang/Object;)V", + false); + } + } + }; + } + + /** + * Adds all remaining fields and methods from the solution class that have not already + * been visited (e.g., lambdas). + */ + @Override + public void visitEnd() { + if (!defaultTransformationsOnly) { + // add missing fields + solutionFieldNodes.entrySet() + .stream() + .filter(entry -> !visitedFields.contains(entry.getKey())) + .map(Map.Entry::getValue) + .forEach(fieldNode -> fieldNode.accept(getDelegate())); + // add missing methods (including lambdas) + solutionMethodNodes.entrySet() + .stream() + .filter(entry -> !visitedMethods.contains(entry.getKey())) + .map(Map.Entry::getValue) + .forEach(methodNode -> methodNode.accept(getDelegate())); + } + + classMetadata(); + fieldMetadata(); + methodMetadata(); + + super.visitEnd(); + } + + private void classMetadata() { + ClassHeader classHeader = submissionClassInfo.getOriginalClassHeader(); + Label startLabel = new Label(); + Label endLabel = new Label(); + int maxStack = 0; + MethodVisitor mv = super.visitMethod(ACC_PUBLIC | ACC_STATIC, + "getOriginalClassHeader", + Type.getMethodDescriptor(classHeader.getType()), + null, + null); + + mv.visitLabel(startLabel); + maxStack += buildClassHeader(mv, classHeader); + mv.visitInsn(ARETURN); + mv.visitLabel(endLabel); + mv.visitLocalVariable("this", + Type.getObjectType(className).getDescriptor(), + null, + startLabel, + endLabel, + 0); + mv.visitMaxs(maxStack, 1); + } + + private void fieldMetadata() { + Set fieldHeaders = submissionClassInfo.getOriginalFieldHeaders(); + Label startLabel = new Label(); + Label endLabel = new Label(); + int maxStack, stackSize; + Type setType = Type.getType(Set.class); + MethodVisitor mv = super.visitMethod(ACC_PUBLIC | ACC_STATIC, + "getOriginalFieldHeaders", + Type.getMethodDescriptor(setType), + "()L%s<%s>;".formatted(setType.getInternalName(), Type.getDescriptor(FieldHeader.class)), + null); + + mv.visitLabel(startLabel); + 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 = buildFieldHeader(mv, fieldHeader); + maxStack = Math.max(maxStack, stackSize + stackSizeUsed); + stackSize++; + mv.visitInsn(AASTORE); + stackSize -= 3; + } + mv.visitMethodInsn(INVOKESTATIC, + setType.getInternalName(), + "of", + Type.getMethodDescriptor(setType, Type.getType(Object[].class)), + true); + mv.visitInsn(ARETURN); + mv.visitLabel(endLabel); + mv.visitLocalVariable("this", + Type.getObjectType(className).getDescriptor(), + null, + startLabel, + endLabel, + 0); + mv.visitMaxs(maxStack, 1); + } + + private void methodMetadata() { + Set methodHeaders = submissionClassInfo.getOriginalMethodHeaders(); + Label startLabel = new Label(); + Label endLabel = new Label(); + int maxStack, stackSize; + Type setType = Type.getType(Set.class); + MethodVisitor mv = super.visitMethod(ACC_PUBLIC | ACC_STATIC, + "getOriginalMethodHeaders", + Type.getMethodDescriptor(setType), + "()L%s<%s>;".formatted(setType.getInternalName(), Type.getDescriptor(MethodHeader.class)), + null); + + mv.visitLabel(startLabel); + 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 = buildMethodHeader(mv, methodHeader); + maxStack = Math.max(maxStack, stackSize); + stackSize++; + mv.visitInsn(AASTORE); + stackSize -= 3; + } + mv.visitMethodInsn(INVOKESTATIC, + setType.getInternalName(), + "of", + Type.getMethodDescriptor(setType, Type.getType(Object[].class)), + true); + mv.visitInsn(ARETURN); + mv.visitLabel(endLabel); + mv.visitLocalVariable("this", + Type.getObjectType(className).getDescriptor(), + null, + startLabel, + endLabel, + 0); + mv.visitMaxs(maxStack, 1); + } +} 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..513d9dda --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionExecutionHandler.java @@ -0,0 +1,382 @@ +package org.tudalgo.algoutils.transform; + +import org.tudalgo.algoutils.transform.util.*; +import kotlin.Pair; +import org.objectweb.asm.Type; + +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.reflect.Method; +import java.util.*; + +/** + * 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 either explicitly using {@link #disableMethodDelegation} or implicitly using + * {@link #substituteMethod}. + *
+ * 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();
+ *
+ *     @BeforeAll
+ *     public static void start() {
+ *         Utils.transformSubmission(); // In case Jagr is not present
+ *     }
+ *
+ *     @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 { + + public static final Type INTERNAL_TYPE = Type.getType(SubmissionExecutionHandler.class); + + private static SubmissionExecutionHandler instance; + + // declaring class => (method header => invocations) + private final Map>> methodInvocations = new HashMap<>(); + private final Map> methodSubstitutions = new HashMap<>(); + private final Map> methodDelegationAllowlist = new HashMap<>(); + + private SubmissionExecutionHandler() {} + + /** + * Returns the global {@link SubmissionExecutionHandler} instance. + * + * @return the global {@link SubmissionExecutionHandler} instance + * @throws IllegalStateException if no global instance is present, i.e., the project has not been + * transformed by {@link SolutionMergingClassTransformer} + */ + public static SubmissionExecutionHandler getInstance() { + if (instance == null) { + instance = new SubmissionExecutionHandler(); + } + return instance; + } + + // 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, "getOriginalClassHeader", MethodType.methodType(ClassHeader.class)) + .invokeExact(); + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + + /** + * 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, "getOriginalFieldHeaders", MethodType.methodType(Set.class)) + .invokeExact(); + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + + /** + * 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, "getOriginalMethodHeaders", MethodType.methodType(Set.class)) + .invokeExact(); + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + + // Invocation logging + + /** + * Resets the logging of method invocations to log no invocations. + */ + public void resetMethodInvocationLogging() { + methodInvocations.clear(); + } + + /** + * Enables logging of method invocations for the given method. + * + * @param method the method to enable invocation logging for + */ + public void enableMethodInvocationLogging(Method method) { + enableMethodInvocationLogging(new MethodHeader(method)); + } + + /** + * Enables logging of method invocations for the given method. + * + * @param methodHeader a method header describing the method + */ + public void enableMethodInvocationLogging(MethodHeader methodHeader) { + methodInvocations.computeIfAbsent(methodHeader.owner(), k -> new HashMap<>()) + .putIfAbsent(methodHeader, new ArrayList<>()); + } + + /** + * Returns all logged invocations for the given method. + * + * @param method the method to get invocations of + * @return a list of invocations on the given method + */ + public List getInvocationsForMethod(Method method) { + return getInvocationsForMethod(new MethodHeader(method)); + } + + /** + * 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 List getInvocationsForMethod(MethodHeader methodHeader) { + return Optional.ofNullable(methodInvocations.get(methodHeader.owner())) + .map(map -> map.get(methodHeader)) + .map(Collections::unmodifiableList) + .orElse(null); + } + + // Method substitution + + /** + * Resets the substitution of methods. + */ + public void resetMethodSubstitution() { + methodSubstitutions.clear(); + } + + /** + * 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 during runtime. + * + * @param method the method to substitute + * @param substitute the {@link MethodSubstitution} the method will be substituted with + */ + public void substituteMethod(Method method, MethodSubstitution substitute) { + substituteMethod(new MethodHeader(method), 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 during runtime. + * + * @param methodHeader a method header describing the method + * @param substitute the {@link MethodSubstitution} the method will be substituted with + */ + public void substituteMethod(MethodHeader methodHeader, MethodSubstitution substitute) { + methodSubstitutions.computeIfAbsent(methodHeader.owner(), k -> new HashMap<>()) + .put(methodHeader, substitute); + } + + // Method delegation + + /** + * Resets the delegation of methods. + */ + public void resetMethodDelegation() { + methodDelegationAllowlist.clear(); + } + + /** + * Disables delegation to the solution for the given method. + * + * @param method the method to disable delegation for + */ + public void disableMethodDelegation(Method method) { + disableMethodDelegation(new MethodHeader(method)); + } + + /** + * Disables delegation to the solution for the given method. + * + * @param methodHeader a method header describing the method + */ + public void disableMethodDelegation(MethodHeader methodHeader) { + methodDelegationAllowlist.computeIfAbsent(methodHeader.owner(), k -> new HashMap<>()) + .put(methodHeader, true); + } + + /** + * 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 { + + Type INTERNAL_TYPE = Type.getType(MethodSubstitution.class); + + /** + * DO NOT USE, THIS METHOD HAS NO EFFECT RIGHT NOW. + * TODO: implement constructor substitution + *

+ * 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 pair consisting of... + *
    + *
  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 first pair entry must be either the original method's owner (for {@code this(...)}) or + * the superclass (for {@code super(...)}). + * The second entry is an array of parameter values for that constructor. + * Default behaviour assumes calling the constructor of {@link Object}, i.e., a class that has no superclass. + * + * @return a pair containing the target method's owner and arguments + */ + default Pair constructorBehaviour() { + return new Pair<>("java/lang/Object", new Object[0]); + } + + /** + * 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); + } + + /** + * Collection of methods injected into the bytecode of transformed methods. + */ + public final class Internal { + + public static final Type INTERNAL_TYPE = Type.getType(Internal.class); + + // 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 boolean logInvocation(MethodHeader methodHeader) { + return Optional.ofNullable(methodInvocations.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 void addInvocation(MethodHeader methodHeader, Invocation invocation) { + Optional.ofNullable(methodInvocations.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 boolean useSubstitution(MethodHeader methodHeader) { + return Optional.ofNullable(methodSubstitutions.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 MethodSubstitution getSubstitution(MethodHeader methodHeader) { + return Optional.ofNullable(methodSubstitutions.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 boolean useSubmissionImpl(MethodHeader methodHeader) { + return Optional.ofNullable(methodDelegationAllowlist.get(methodHeader.owner())) + .map(map -> map.get(methodHeader)) + .orElse(false); + } + } +} diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/ClassHeader.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/ClassHeader.java new file mode 100644 index 00000000..5c702c0f --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/ClassHeader.java @@ -0,0 +1,82 @@ +package org.tudalgo.algoutils.transform.util; + +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.Type; + +import java.util.Objects; + +/** + * A record holding information on the header of a class as declared in Java bytecode. + * + * @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 { + + private static final Type INTERNAL_TYPE = Type.getType(ClassHeader.class); + private static final Type[] INTERNAL_CONSTRUCTOR_TYPES = new Type[] { + Type.INT_TYPE, + Type.getType(String.class), + Type.getType(String.class), + Type.getType(String.class), + Type.getType(String[].class) + }; + + @Override + public Type getType() { + return INTERNAL_TYPE; + } + + @Override + public Type[] getConstructorParameterTypes() { + return INTERNAL_CONSTRUCTOR_TYPES; + } + + @Override + public Object getValue(String name) { + return switch (name) { + case "access" -> this.access; + case "name" -> this.name; + case "signature" -> this.signature; + case "superName" -> this.superName; + case "interfaces" -> this.interfaces; + default -> throw new IllegalArgumentException("Invalid name: " + name); + }; + } + + /** + * 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); + } + + @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); + } +} diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/FieldHeader.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/FieldHeader.java new file mode 100644 index 00000000..8a3a9503 --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/FieldHeader.java @@ -0,0 +1,80 @@ +package org.tudalgo.algoutils.transform.util; + +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.Type; + +import java.util.Objects; + +/** + * A record holding information on the header of a field as declared in java bytecode. + * + * @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 { + + private static final Type INTERNAL_TYPE = Type.getType(FieldHeader.class); + private static final Type[] INTERNAL_CONSTRUCTOR_TYPES = new Type[] { + Type.getType(String.class), + Type.INT_TYPE, + Type.getType(String.class), + Type.getType(String.class), + Type.getType(String.class) + }; + + @Override + public Type getType() { + return INTERNAL_TYPE; + } + + @Override + public Type[] getConstructorParameterTypes() { + return INTERNAL_CONSTRUCTOR_TYPES; + } + + @Override + public Object getValue(String name) { + return switch (name) { + case "owner" -> this.owner; + case "access" -> this.access; + case "name" -> this.name; + case "descriptor" -> this.descriptor; + case "signature" -> this.signature; + default -> throw new IllegalArgumentException("Invalid name: " + name); + }; + } + + /** + * 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); + } + + /** + * 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); + } +} 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..71675c3f --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/ForceSignatureAnnotationProcessor.java @@ -0,0 +1,314 @@ +package org.tudalgo.algoutils.transform.util; + +import org.objectweb.asm.*; +import org.tudalgo.algoutils.student.annotation.ForceSignature; + +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 { + + private static final Type FORCE_SIGNATURE_TYPE = Type.getType(ForceSignature.class); + + /** + * The forced identifier of the class, if any. + */ + private String forcedClassIdentifier; + + /** + * A mapping of the actual field header to the forced one. + */ + private final Map forcedFieldsMapping = new HashMap<>(); + + /** + * A mapping of the actual method header to the forced one. + */ + private final Map forcedMethodsMapping = new HashMap<>(); + + /** + * Constructs a new {@link ForceSignatureAnnotationProcessor} instance and processes the + * {@link ForceSignature} annotation using the given class reader. + * + * @param reader the class reader to use for processing + */ + public ForceSignatureAnnotationProcessor(ClassReader reader) { + reader.accept(new ClassLevelVisitor(reader.getClassName()), 0); + } + + /** + * Whether the class identifier / name is forced. + * + * @return true, if forced, otherwise false + */ + public boolean classIdentifierIsForced() { + return forcedClassIdentifier != null; + } + + /** + * Returns the forced class identifier. + * + * @return the forced class identifier + */ + public String forcedClassIdentifier() { + return forcedClassIdentifier.replace('.', '/'); + } + + /** + * Whether the given field is forced. + * + * @param identifier the original identifier / name of the field + * @return true, if forced, otherwise false + */ + public boolean fieldIdentifierIsForced(String identifier) { + return forcedFieldHeader(identifier) != null; + } + + /** + * Returns the field header for a forced field. + * + * @param identifier the original identifier / name of the field + * @return the field header + */ + public FieldHeader forcedFieldHeader(String identifier) { + return forcedFieldsMapping.entrySet() + .stream() + .filter(entry -> identifier.equals(entry.getKey().name())) + .findAny() + .map(Map.Entry::getValue) + .orElse(null); + } + + /** + * Whether the given method is forced. + * + * @param identifier 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 identifier, String descriptor) { + return forcedMethodHeader(identifier, descriptor) != null; + } + + /** + * Returns the method header for a forced method. + * + * @param identifier the original identifier / name of the method + * @param descriptor the original descriptor of the method + * @return the method header + */ + public MethodHeader forcedMethodHeader(String identifier, String descriptor) { + return forcedMethodsMapping.entrySet() + .stream() + .filter(entry -> identifier.equals(entry.getKey().name()) && descriptor.equals(entry.getKey().descriptor())) + .findAny() + .map(Map.Entry::getValue) + .orElse(null); + } + + /** + * A visitor for processing class-level annotations. + */ + private class ClassLevelVisitor extends ClassVisitor { + + private final String name; + + private ForceSignatureAnnotationVisitor annotationVisitor; + private final List fieldLevelVisitors = new ArrayList<>(); + private final List methodLevelVisitors = new ArrayList<>(); + + private ClassLevelVisitor(String name) { + super(Opcodes.ASM9); + this.name = name; + } + + @Override + public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { + if (descriptor.equals(FORCE_SIGNATURE_TYPE.getDescriptor())) { + return annotationVisitor = new ForceSignatureAnnotationVisitor(); + } else { + return null; + } + } + + @Override + public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) { + FieldLevelVisitor fieldLevelVisitor = new FieldLevelVisitor(this.name, 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(this.name, access, name, descriptor, signature, exceptions); + methodLevelVisitors.add(methodLevelVisitor); + return methodLevelVisitor; + } + + @Override + public void visitEnd() { + forcedClassIdentifier = annotationVisitor != null ? annotationVisitor.identifier : null; + + 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(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(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/Header.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Header.java new file mode 100644 index 00000000..cfba416e --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Header.java @@ -0,0 +1,12 @@ +package org.tudalgo.algoutils.transform.util; + +import org.objectweb.asm.Type; + +public interface Header { + + Type getType(); + + Type[] getConstructorParameterTypes(); + + Object getValue(String name); +} 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..f26fbd8f --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Invocation.java @@ -0,0 +1,183 @@ +package org.tudalgo.algoutils.transform.util; + +import org.objectweb.asm.Type; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** + * This class holds information about the context of an invocation. + * Context means the object a method was invoked on and the parameters it was invoked with. + */ +public class Invocation { + + public static final Type INTERNAL_TYPE = Type.getType(Invocation.class); + + private final Object instance; + private final List parameterValues = new ArrayList<>(); + + /** + * Constructs a new invocation. + */ + public Invocation() { + this(null); + } + + /** + * Constructs a new invocation. + * + * @param instance the object on which this invocation takes place + */ + public Invocation(Object instance) { + this.instance = instance; + } + + /** + * Returns the object the method was invoked on. + * + * @return the object the method was invoked on. + */ + public Object getInstance() { + return instance; + } + + /** + * 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); + } + + @Override + public String toString() { + return "Invocation{instance=%s, parameterValues=%s}".formatted(instance, parameterValues); + } + + @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(parameterValues, that.parameterValues); + } + + @Override + public int hashCode() { + return Objects.hash(parameterValues); + } +} diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/MethodHeader.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/MethodHeader.java new file mode 100644 index 00000000..bcb94c7e --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/MethodHeader.java @@ -0,0 +1,106 @@ +package org.tudalgo.algoutils.transform.util; + +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Type; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Objects; + +/** + * A record holding information on the header of a method as declared in java bytecode. + * + * @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 { + + public static final Type INTERNAL_TYPE = Type.getType(MethodHeader.class); + public static final String INTERNAL_CONSTRUCTOR_DESCRIPTOR = Type.getMethodDescriptor(Type.VOID_TYPE, + Type.getType(String.class), + Type.INT_TYPE, + Type.getType(String.class), + Type.getType(String.class), + Type.getType(String.class), + Type.getType(String[].class)); + private static final Type[] INTERNAL_CONSTRUCTOR_TYPES = new Type[] { + Type.getType(String.class), + Type.INT_TYPE, + Type.getType(String.class), + Type.getType(String.class), + Type.getType(String.class), + Type.getType(String[].class) + }; + + @Override + public Type getType() { + return INTERNAL_TYPE; + } + + @Override + public Type[] getConstructorParameterTypes() { + return INTERNAL_CONSTRUCTOR_TYPES; + } + + @Override + public Object getValue(String name) { + return switch (name) { + case "owner" -> this.owner; + case "access" -> this.access; + case "name" -> this.name; + case "descriptor" -> this.descriptor; + case "signature" -> this.signature; + case "exceptions" -> this.exceptions; + default -> throw new IllegalArgumentException("Invalid name: " + name); + }; + } + + /** + * Constructs a new method header using the given method. + * + * @param method a java reflection method + */ + public MethodHeader(Method method) { + this(Type.getInternalName(method.getDeclaringClass()), + method.getModifiers(), + method.getName(), + Type.getMethodDescriptor(method), + null, + Arrays.stream(method.getExceptionTypes()) + .map(Type::getInternalName) + .toArray(String[]::new)); + } + + /** + * 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); + } + + /** + * 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); + } +} 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..c2e3e1dc --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationContext.java @@ -0,0 +1,33 @@ +package org.tudalgo.algoutils.transform.util; + +import org.tudalgo.algoutils.transform.SolutionClassNode; +import org.tudalgo.algoutils.transform.SubmissionClassInfo; + +import java.util.Map; + +/** + * A record for holding context information for the transformation process. + * + * @param projectPrefix the root package for all submission classes + * @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} + * @author Daniel Mangold + */ +public record TransformationContext( + String projectPrefix, + Map solutionClasses, + Map submissionClasses +) { + + /** + * 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) { + return submissionClasses.computeIfAbsent(submissionClassName, + className -> TransformationUtils.readSubmissionClass(this, className)); + } +} 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..e73b6992 --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationUtils.java @@ -0,0 +1,253 @@ +package org.tudalgo.algoutils.transform.util; + +import org.tudalgo.algoutils.transform.SolutionClassNode; +import org.tudalgo.algoutils.transform.SolutionMergingClassTransformer; +import org.tudalgo.algoutils.transform.SubmissionClassInfo; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Type; + +import java.io.IOException; +import java.io.InputStream; + +import static org.objectweb.asm.Opcodes.*; + +/** + * A collection of utility methods useful for bytecode transformations. + * @author Daniel Mangold + */ +public final class TransformationUtils { + + private TransformationUtils() {} + + /** + * 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, this method does nothing. + * + * @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); + } + } + } + + /** + * 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 += (types[i].getSort() == Type.LONG || types[i].getSort() == Type.DOUBLE) ? 2 : 1; + } + return localsIndex; + } + + /** + * Builds a class header with bytecode instructions using the given method visitor and information + * stored in the given class header. + * Upon return, a reference to the newly created {@link ClassHeader} object is located at + * the top of the method visitor's stack. + * + * @param mv the method visitor to use + * @param classHeader the class header to replicate in bytecode + * @return the maximum stack size during the operation + */ + public static int buildClassHeader(MethodVisitor mv, ClassHeader classHeader) { + return buildHeader(mv, classHeader, "access", "name", "signature", "superName", "interfaces"); + } + + /** + * Builds a field header with bytecode instructions using the given method visitor and information + * stored in the given field header. + * Upon return, a reference to the newly created {@link FieldHeader} object is located at + * the top of the method visitor's stack. + * + * @param mv the method visitor to use + * @param fieldHeader the field header to replicate in bytecode + * @return the maximum stack size during the operation + */ + public static int buildFieldHeader(MethodVisitor mv, FieldHeader fieldHeader) { + return buildHeader(mv, fieldHeader, "owner", "access", "name", "descriptor", "signature"); + } + + /** + * Builds a method header with bytecode instructions using the given method visitor and information + * stored in the given method header. + * Upon return, a reference to the newly created {@link MethodHeader} object is located at + * the top of the method visitor's stack. + * + * @param mv the method visitor to use + * @param methodHeader the method header to replicate in bytecode + * @return the maximum stack size during the operation + */ + public static int buildMethodHeader(MethodVisitor mv, MethodHeader methodHeader) { + return buildHeader(mv, methodHeader, "owner", "access", "name", "descriptor", "signature", "exceptions"); + } + + /** + * 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 static SolutionClassNode readSolutionClass(String className) { + ClassReader solutionClassReader; + String solutionClassFilePath = "/classes/%s.bin".formatted(className); + try (InputStream is = SolutionMergingClassTransformer.class.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(className); + solutionClassReader.accept(solutionClassNode, 0); + return solutionClassNode; + } + + /** + * Attempts to read and process a submission class. + * + * @param transformationContext a {@link TransformationContext} object + * @param className the name of the submission class + * @return the resulting {@link SubmissionClassInfo} object + */ + public static SubmissionClassInfo readSubmissionClass(TransformationContext transformationContext, String className) { + ClassReader submissionClassReader; + String submissionClassFilePath = "/%s.class".formatted(className); + try (InputStream is = SolutionMergingClassTransformer.class.getResourceAsStream(submissionClassFilePath)) { + submissionClassReader = new ClassReader(is); + } catch (IOException e) { + return null; + } + SubmissionClassInfo submissionClassInfo = new SubmissionClassInfo( + transformationContext, + submissionClassReader.getClassName(), + new ForceSignatureAnnotationProcessor(submissionClassReader) + ); + submissionClassReader.accept(submissionClassInfo, 0); + return submissionClassInfo; + } + + /** + * 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. + *
+ * Note: The number of keys must equal the length of the array returned by + * {@link Header#getConstructorParameterTypes()}. + * Furthermore, the result of calling {@link Header#getValue(String)} with {@code keys[i]} must + * be assignable to the constructor parameter type at index {@code i}. + * + * @param mv the method visitor to use + * @param header the header object to replicate in bytecode + * @param keys the keys to get values for + * @return the maximum stack size during the operation + */ + private static int buildHeader(MethodVisitor mv, Header header, String... keys) { + Type headerType = header.getType(); + Type[] constructorParameterTypes = header.getConstructorParameterTypes(); + int maxStack, stackSize; + + mv.visitTypeInsn(NEW, header.getType().getInternalName()); + mv.visitInsn(DUP); + maxStack = stackSize = 2; + for (int i = 0; i < keys.length; i++) { + Object value = header.getValue(keys[i]); + if (constructorParameterTypes[i].equals(Type.getType(String[].class))) { + Object[] array = (Object[]) value; + if (array != null) { + mv.visitIntInsn(SIPUSH, array.length); + mv.visitTypeInsn(ANEWARRAY, Type.getInternalName(String.class)); + maxStack = Math.max(maxStack, ++stackSize); + for (int j = 0; j < array.length; j++) { + mv.visitInsn(DUP); + maxStack = Math.max(maxStack, ++stackSize); + mv.visitIntInsn(SIPUSH, j); + maxStack = Math.max(maxStack, ++stackSize); + mv.visitLdcInsn(array[j]); + maxStack = Math.max(maxStack, ++stackSize); + mv.visitInsn(AASTORE); + stackSize -= 3; + } + } else { + mv.visitInsn(ACONST_NULL); + maxStack = Math.max(maxStack, ++stackSize); + } + } 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, constructorParameterTypes), + false); + return maxStack; + } +} From 3e3e7b11db53b9525d2180ae97399c1bcad7e5b8 Mon Sep 17 00:00:00 2001 From: Daniel Mangold Date: Sun, 17 Nov 2024 14:50:10 +0100 Subject: [PATCH 02/48] Add more documentation --- .../algoutils/transform/util/ClassHeader.java | 2 ++ .../algoutils/transform/util/FieldHeader.java | 3 +++ .../algoutils/transform/util/Header.java | 21 ++++++++++++++++++- .../transform/util/MethodHeader.java | 4 ++++ .../transform/util/TransformationUtils.java | 8 +++---- 5 files changed, 33 insertions(+), 5 deletions(-) diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/ClassHeader.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/ClassHeader.java index 5c702c0f..a25a48bd 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/ClassHeader.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/ClassHeader.java @@ -7,6 +7,8 @@ /** * 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 diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/FieldHeader.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/FieldHeader.java index 8a3a9503..f846dc35 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/FieldHeader.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/FieldHeader.java @@ -8,6 +8,9 @@ /** * 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 diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Header.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Header.java index cfba416e..17ee359c 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Header.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Header.java @@ -2,11 +2,30 @@ import org.objectweb.asm.Type; -public interface Header { +/** + * Common interface of all header records. + */ +interface Header { + /** + * Returns the type for this header. + * + * @return the type for this header + */ Type getType(); + /** + * Returns the parameter types for this record's primary constructor. + * + * @return the parameter types + */ Type[] getConstructorParameterTypes(); + /** + * Returns the stored value for the given record component's name. + * + * @param name the name of the record component + * @return the record component's value + */ Object getValue(String name); } diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/MethodHeader.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/MethodHeader.java index bcb94c7e..7bf6be1b 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/MethodHeader.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/MethodHeader.java @@ -10,6 +10,10 @@ /** * 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 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 index e73b6992..e138172f 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationUtils.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationUtils.java @@ -109,7 +109,7 @@ public static int getLocalsIndex(Type[] types, int index) { * * @param mv the method visitor to use * @param classHeader the class header to replicate in bytecode - * @return the maximum stack size during the operation + * @return the maximum stack size used during the operation */ public static int buildClassHeader(MethodVisitor mv, ClassHeader classHeader) { return buildHeader(mv, classHeader, "access", "name", "signature", "superName", "interfaces"); @@ -123,7 +123,7 @@ public static int buildClassHeader(MethodVisitor mv, ClassHeader classHeader) { * * @param mv the method visitor to use * @param fieldHeader the field header to replicate in bytecode - * @return the maximum stack size during the operation + * @return the maximum stack size used during the operation */ public static int buildFieldHeader(MethodVisitor mv, FieldHeader fieldHeader) { return buildHeader(mv, fieldHeader, "owner", "access", "name", "descriptor", "signature"); @@ -137,7 +137,7 @@ public static int buildFieldHeader(MethodVisitor mv, FieldHeader fieldHeader) { * * @param mv the method visitor to use * @param methodHeader the method header to replicate in bytecode - * @return the maximum stack size during the operation + * @return the maximum stack size used during the operation */ public static int buildMethodHeader(MethodVisitor mv, MethodHeader methodHeader) { return buildHeader(mv, methodHeader, "owner", "access", "name", "descriptor", "signature", "exceptions"); @@ -202,7 +202,7 @@ public static SubmissionClassInfo readSubmissionClass(TransformationContext tran * @param mv the method visitor to use * @param header the header object to replicate in bytecode * @param keys the keys to get values for - * @return the maximum stack size during the operation + * @return the maximum stack size used during the operation */ private static int buildHeader(MethodVisitor mv, Header header, String... keys) { Type headerType = header.getType(); From 9656943c35f92dafda6ca58828af9272c3c8d45a Mon Sep 17 00:00:00 2001 From: Daniel Mangold Date: Sun, 17 Nov 2024 14:53:03 +0100 Subject: [PATCH 03/48] Refactoring --- .../transform/SubmissionClassVisitor.java | 25 +++--- .../transform/util/MethodHeader.java | 9 +-- .../transform/util/TransformationUtils.java | 78 ++++++++++++++----- 3 files changed, 71 insertions(+), 41 deletions(-) diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java index 742a4e2b..f12a91e7 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java @@ -182,7 +182,7 @@ public void visitCode() { super.visitFrame(F_APPEND, 2, - new Object[] {SubmissionExecutionHandler.Internal.INTERNAL_TYPE.getInternalName(), MethodHeader.INTERNAL_TYPE.getInternalName()}, + new Object[] {SubmissionExecutionHandler.Internal.INTERNAL_TYPE.getInternalName(), methodHeader.getType().getInternalName()}, 0, null); @@ -192,7 +192,7 @@ public void visitCode() { super.visitMethodInsn(INVOKEVIRTUAL, SubmissionExecutionHandler.Internal.INTERNAL_TYPE.getInternalName(), "logInvocation", - Type.getMethodDescriptor(Type.BOOLEAN_TYPE, MethodHeader.INTERNAL_TYPE), + Type.getMethodDescriptor(Type.BOOLEAN_TYPE, methodHeader.getType()), false); super.visitJumpInsn(IFEQ, isConstructor ? // jump to label if logInvocation(...) == false defaultTransformationsOnly ? submissionCodeLabel : delegationCheckLabel : @@ -206,7 +206,7 @@ public void visitCode() { SubmissionExecutionHandler.Internal.INTERNAL_TYPE.getInternalName(), "addInvocation", Type.getMethodDescriptor(Type.VOID_TYPE, - MethodHeader.INTERNAL_TYPE, + methodHeader.getType(), Invocation.INTERNAL_TYPE), false); @@ -219,7 +219,7 @@ public void visitCode() { super.visitMethodInsn(INVOKEVIRTUAL, SubmissionExecutionHandler.Internal.INTERNAL_TYPE.getInternalName(), "useSubstitution", - Type.getMethodDescriptor(Type.BOOLEAN_TYPE, MethodHeader.INTERNAL_TYPE), + Type.getMethodDescriptor(Type.BOOLEAN_TYPE, methodHeader.getType()), false); super.visitJumpInsn(IFEQ, defaultTransformationsOnly ? submissionCodeLabel : delegationCheckLabel); // jump to label if useSubstitution(...) == false @@ -230,7 +230,7 @@ public void visitCode() { SubmissionExecutionHandler.Internal.INTERNAL_TYPE.getInternalName(), "getSubstitution", Type.getMethodDescriptor(Invocation.INTERNAL_TYPE, - MethodHeader.INTERNAL_TYPE), + methodHeader.getType()), false); buildInvocation(Type.getArgumentTypes(methodHeader.descriptor())); super.visitMethodInsn(INVOKEINTERFACE, @@ -258,7 +258,7 @@ public void visitCode() { super.visitMethodInsn(INVOKEVIRTUAL, SubmissionExecutionHandler.Internal.INTERNAL_TYPE.getInternalName(), "useSubmissionImpl", - Type.getMethodDescriptor(Type.BOOLEAN_TYPE, MethodHeader.INTERNAL_TYPE), + Type.getMethodDescriptor(Type.BOOLEAN_TYPE, methodHeader.getType()), false); super.visitJumpInsn(IFNE, submissionCodeLabel); // jump to label if useSubmissionImpl(...) == true @@ -272,7 +272,7 @@ public void visitCode() { delegationCodeLabel, submissionExecutionHandlerIndex); super.visitLocalVariable("methodHeader", - MethodHeader.INTERNAL_TYPE.getDescriptor(), + methodHeader.getType().getDescriptor(), null, methodHeaderVarLabel, delegationCodeLabel, @@ -291,7 +291,7 @@ public void visitCode() { submissionCodeLabel, submissionExecutionHandlerIndex); super.visitLocalVariable("methodHeader", - MethodHeader.INTERNAL_TYPE.getDescriptor(), + methodHeader.getType().getDescriptor(), null, methodHeaderVarLabel, submissionCodeLabel, @@ -393,7 +393,6 @@ private void classMetadata() { ClassHeader classHeader = submissionClassInfo.getOriginalClassHeader(); Label startLabel = new Label(); Label endLabel = new Label(); - int maxStack = 0; MethodVisitor mv = super.visitMethod(ACC_PUBLIC | ACC_STATIC, "getOriginalClassHeader", Type.getMethodDescriptor(classHeader.getType()), @@ -401,7 +400,7 @@ private void classMetadata() { null); mv.visitLabel(startLabel); - maxStack += buildClassHeader(mv, classHeader); + int maxStack = buildClassHeader(mv, classHeader); mv.visitInsn(ARETURN); mv.visitLabel(endLabel); mv.visitLocalVariable("this", @@ -436,8 +435,7 @@ private void fieldMetadata() { mv.visitIntInsn(SIPUSH, i++); maxStack = Math.max(maxStack, ++stackSize); int stackSizeUsed = buildFieldHeader(mv, fieldHeader); - maxStack = Math.max(maxStack, stackSize + stackSizeUsed); - stackSize++; + maxStack = Math.max(maxStack, stackSize++ + stackSizeUsed); mv.visitInsn(AASTORE); stackSize -= 3; } @@ -480,8 +478,7 @@ private void methodMetadata() { mv.visitIntInsn(SIPUSH, i++); maxStack = Math.max(maxStack, ++stackSize); int stackSizeUsed = buildMethodHeader(mv, methodHeader); - maxStack = Math.max(maxStack, stackSize); - stackSize++; + maxStack = Math.max(maxStack, stackSize++ + stackSizeUsed); mv.visitInsn(AASTORE); stackSize -= 3; } diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/MethodHeader.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/MethodHeader.java index 7bf6be1b..a423f502 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/MethodHeader.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/MethodHeader.java @@ -25,14 +25,7 @@ */ public record MethodHeader(String owner, int access, String name, String descriptor, String signature, String[] exceptions) implements Header { - public static final Type INTERNAL_TYPE = Type.getType(MethodHeader.class); - public static final String INTERNAL_CONSTRUCTOR_DESCRIPTOR = Type.getMethodDescriptor(Type.VOID_TYPE, - Type.getType(String.class), - Type.INT_TYPE, - Type.getType(String.class), - Type.getType(String.class), - Type.getType(String.class), - Type.getType(String[].class)); + private static final Type INTERNAL_TYPE = Type.getType(MethodHeader.class); private static final Type[] INTERNAL_CONSTRUCTOR_TYPES = new Type[] { Type.getType(String.class), Type.INT_TYPE, 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 index e138172f..98c64d2f 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationUtils.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationUtils.java @@ -189,6 +189,62 @@ public static SubmissionClassInfo readSubmissionClass(TransformationContext tran return submissionClassInfo; } + /** + * 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; + } + /** * 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 @@ -215,25 +271,8 @@ private static int buildHeader(MethodVisitor mv, Header header, String... keys) for (int i = 0; i < keys.length; i++) { Object value = header.getValue(keys[i]); if (constructorParameterTypes[i].equals(Type.getType(String[].class))) { - Object[] array = (Object[]) value; - if (array != null) { - mv.visitIntInsn(SIPUSH, array.length); - mv.visitTypeInsn(ANEWARRAY, Type.getInternalName(String.class)); - maxStack = Math.max(maxStack, ++stackSize); - for (int j = 0; j < array.length; j++) { - mv.visitInsn(DUP); - maxStack = Math.max(maxStack, ++stackSize); - mv.visitIntInsn(SIPUSH, j); - maxStack = Math.max(maxStack, ++stackSize); - mv.visitLdcInsn(array[j]); - maxStack = Math.max(maxStack, ++stackSize); - mv.visitInsn(AASTORE); - stackSize -= 3; - } - } else { - mv.visitInsn(ACONST_NULL); - maxStack = Math.max(maxStack, ++stackSize); - } + int stackUsed = buildArray(mv, Type.getType(String[].class), (Object[]) value); + maxStack = Math.max(maxStack, stackSize++ + stackUsed); } else { if (value != null) { mv.visitLdcInsn(value); @@ -248,6 +287,7 @@ private static int buildHeader(MethodVisitor mv, Header header, String... keys) "", Type.getMethodDescriptor(Type.VOID_TYPE, constructorParameterTypes), false); + return maxStack; } } From ce0934950174511f8a34c25459cef33718e7f51b Mon Sep 17 00:00:00 2001 From: Daniel Mangold Date: Sun, 17 Nov 2024 14:53:49 +0100 Subject: [PATCH 04/48] Add configuration options --- .../SolutionMergingClassTransformer.java | 51 ++++++++++++++++--- .../transform/SubmissionClassInfo.java | 8 +-- .../transform/SubmissionClassVisitor.java | 4 +- .../transform/util/TransformationContext.java | 13 ++++- 4 files changed, 62 insertions(+), 14 deletions(-) diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionMergingClassTransformer.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionMergingClassTransformer.java index b4cd8366..9e67131b 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionMergingClassTransformer.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionMergingClassTransformer.java @@ -8,9 +8,7 @@ import org.objectweb.asm.Type; import org.sourcegrade.jagr.api.testing.ClassTransformer; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.concurrent.ConcurrentHashMap; /** @@ -74,11 +72,18 @@ public class SolutionMergingClassTransformer implements ClassTransformer { * @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, List availableSolutionClasses) { + public SolutionMergingClassTransformer(String projectPrefix, String... availableSolutionClasses) { + this(new Builder(projectPrefix, availableSolutionClasses)); + } + + @SuppressWarnings("unchecked") + private SolutionMergingClassTransformer(Builder builder) { Map solutionClasses = new HashMap<>(); Map submissionClasses = new ConcurrentHashMap<>(); - this.transformationContext = new TransformationContext(projectPrefix, solutionClasses, submissionClasses); - availableSolutionClasses.stream() + this.transformationContext = new TransformationContext(Collections.unmodifiableMap(builder.configuration), + solutionClasses, + submissionClasses); + ((List) builder.configuration.get(Config.SOLUTION_CLASSES)).stream() .map(s -> s.replace('.', '/')) .forEach(className -> solutionClasses.put(className, TransformationUtils.readSolutionClass(className))); } @@ -98,4 +103,38 @@ public void transform(ClassReader reader, ClassWriter writer) { String submissionClassName = reader.getClassName(); reader.accept(new SubmissionClassVisitor(writer, transformationContext, submissionClassName), 0); } + + public enum Config { + PROJECT_PREFIX(null), + SOLUTION_CLASSES(null), + SIMILARITY(0.90); + + private final Object defaultValue; + + Config(Object defaultValue) { + this.defaultValue = defaultValue; + } + } + + public static class Builder { + + private final Map configuration = new EnumMap<>(Config.class); + + public Builder(String projectPrefix, String... solutionClasses) { + for (Config config : Config.values()) { + configuration.put(config, config.defaultValue); + } + configuration.put(Config.PROJECT_PREFIX, projectPrefix); + configuration.put(Config.SOLUTION_CLASSES, List.of(solutionClasses)); + } + + public Builder setSimilarity(double similarity) { + configuration.put(Config.SIMILARITY, similarity); + return this; + } + + public SolutionMergingClassTransformer build() { + return new SolutionMergingClassTransformer(this); + } + } } diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassInfo.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassInfo.java index b5ca8254..f46a52aa 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassInfo.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassInfo.java @@ -65,7 +65,7 @@ public SubmissionClassInfo(TransformationContext transformationContext, .keySet() .stream() .map(s -> new Pair<>(s, MatchingUtils.similarity(originalClassName, s))) - .filter(pair -> pair.getSecond() >= 0.90) + .filter(pair -> pair.getSecond() >= transformationContext.getSimilarity()) .max(Comparator.comparing(Pair::getSecond)) .map(Pair::getFirst) .orElse(originalClassName); @@ -176,7 +176,7 @@ public FieldVisitor visitField(int access, String name, String descriptor, Strin .keySet() .stream() .map(fieldHeader -> new Pair<>(fieldHeader, MatchingUtils.similarity(name, fieldHeader.name()))) - .filter(pair -> pair.getSecond() >= 0.90) + .filter(pair -> pair.getSecond() >= transformationContext.getSimilarity()) .max(Comparator.comparing(Pair::getSecond)) .map(Pair::getFirst) .orElse(submissionFieldHeader); @@ -206,7 +206,7 @@ public MethodVisitor visitMethod(int access, String name, String descriptor, Str .map(methodHeader -> new Triple<>(methodHeader, MatchingUtils.similarity(name, methodHeader.name()), MatchingUtils.similarity(descriptor, methodHeader.descriptor()))) - .filter(triple -> triple.getSecond() >= 0.90 && triple.getThird() >= 0.90) + .filter(triple -> triple.getSecond() >= transformationContext.getSimilarity() && triple.getThird() >= transformationContext.getSimilarity()) .max(Comparator.comparing(Triple::getSecond).thenComparing(Triple::getThird)) .map(Triple::getFirst) .orElse(submissionMethodHeader); @@ -252,7 +252,7 @@ private void resolveSuperClassMembers(Set, Map>> superClassMembers, String className) { - if (className.startsWith(transformationContext.projectPrefix())) { + if (className.startsWith(transformationContext.getProjectPrefix())) { SubmissionClassInfo submissionClassInfo = transformationContext.getSubmissionClassInfo(className); superClassMembers.add(new Triple<>(className, submissionClassInfo.fields, submissionClassInfo.methods)); resolveSuperClassMembers(superClassMembers, submissionClassInfo.superClass, submissionClassInfo.interfaces); diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java index f12a91e7..876cfbd0 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java @@ -305,7 +305,7 @@ public void visitCode() { @Override public void visitFieldInsn(int opcode, String owner, String name, String descriptor) { // skip transformation if only default transformations are applied or owner is not part of the submission - if (defaultTransformationsOnly || !owner.startsWith(transformationContext.projectPrefix())) { + if (defaultTransformationsOnly || !owner.startsWith(transformationContext.getProjectPrefix())) { super.visitFieldInsn(opcode, owner, name, descriptor); } else { FieldHeader fieldHeader = transformationContext.getSubmissionClassInfo(owner).getComputedFieldHeader(name); @@ -316,7 +316,7 @@ public void visitFieldInsn(int opcode, String owner, String name, String descrip @Override public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) { // skip transformation if only default transformations are applied or owner is not part of the submission - if (defaultTransformationsOnly || !owner.startsWith(transformationContext.projectPrefix())) { + if (defaultTransformationsOnly || !owner.startsWith(transformationContext.getProjectPrefix())) { super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); } else { MethodHeader methodHeader = transformationContext.getSubmissionClassInfo(owner).getComputedMethodHeader(name, descriptor); 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 index c2e3e1dc..281514bf 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationContext.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationContext.java @@ -1,6 +1,7 @@ package org.tudalgo.algoutils.transform.util; import org.tudalgo.algoutils.transform.SolutionClassNode; +import org.tudalgo.algoutils.transform.SolutionMergingClassTransformer; import org.tudalgo.algoutils.transform.SubmissionClassInfo; import java.util.Map; @@ -8,17 +9,25 @@ /** * A record for holding context information for the transformation process. * - * @param projectPrefix the root package for all submission classes + * @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} * @author Daniel Mangold */ public record TransformationContext( - String projectPrefix, + Map configuration, Map solutionClasses, Map submissionClasses ) { + public String getProjectPrefix() { + return (String) configuration.get(SolutionMergingClassTransformer.Config.PROJECT_PREFIX); + } + + public double getSimilarity() { + return (Double) configuration.get(SolutionMergingClassTransformer.Config.SIMILARITY); + } + /** * Returns the {@link SubmissionClassInfo} for a given submission class name. * If no mapping exists in {@link #submissionClasses}, will attempt to compute one. From 6e0a0fe952b37e8c2a3b2537106bca30fd221761 Mon Sep 17 00:00:00 2001 From: Daniel Mangold Date: Sun, 17 Nov 2024 15:39:14 +0100 Subject: [PATCH 05/48] Add more missing documentation --- .../SolutionMergingClassTransformer.java | 29 ++++++++++++ .../transform/SubmissionClassVisitor.java | 44 +++++++++++++++++++ .../transform/util/TransformationContext.java | 10 +++++ 3 files changed, 83 insertions(+) diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionMergingClassTransformer.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionMergingClassTransformer.java index 9e67131b..d393dc82 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionMergingClassTransformer.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionMergingClassTransformer.java @@ -76,6 +76,12 @@ public SolutionMergingClassTransformer(String projectPrefix, String... available this(new Builder(projectPrefix, availableSolutionClasses)); } + /** + * 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<>(); @@ -104,6 +110,9 @@ public void transform(ClassReader reader, ClassWriter writer) { reader.accept(new SubmissionClassVisitor(writer, transformationContext, submissionClassName), 0); } + /** + * (Internal) Configuration keys + */ public enum Config { PROJECT_PREFIX(null), SOLUTION_CLASSES(null), @@ -116,10 +125,19 @@ public enum Config { } } + /** + * 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 + * @param solutionClasses the list of solution class names (fully qualified) to use + */ public Builder(String projectPrefix, String... solutionClasses) { for (Config config : Config.values()) { configuration.put(config, config.defaultValue); @@ -128,11 +146,22 @@ public Builder(String projectPrefix, String... solutionClasses) { configuration.put(Config.SOLUTION_CLASSES, List.of(solutionClasses)); } + /** + * 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; } + /** + * 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/SubmissionClassVisitor.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java index 876cfbd0..2dd125e5 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java @@ -49,6 +49,37 @@ * * 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)) {  // if not constructor
+ *     submissionExecutionHandler.getSubstitution(methodHeader)
+ *         .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 SubmissionExecutionHandler * @author Daniel Mangold */ @@ -364,6 +395,7 @@ private void buildInvocation(Type[] argumentTypes) { /** * 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() { @@ -389,6 +421,10 @@ public void visitEnd() { 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 classMetadata() { ClassHeader classHeader = submissionClassInfo.getOriginalClassHeader(); Label startLabel = new Label(); @@ -412,6 +448,10 @@ private void classMetadata() { mv.visitMaxs(maxStack, 1); } + /** + * 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 fieldMetadata() { Set fieldHeaders = submissionClassInfo.getOriginalFieldHeaders(); Label startLabel = new Label(); @@ -455,6 +495,10 @@ private void fieldMetadata() { mv.visitMaxs(maxStack, 1); } + /** + * 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 methodMetadata() { Set methodHeaders = submissionClassInfo.getOriginalMethodHeaders(); Label startLabel = new Label(); 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 index 281514bf..f6c32a69 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationContext.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationContext.java @@ -20,10 +20,20 @@ public record TransformationContext( Map submissionClasses ) { + /** + * 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); } From a3f4cbf969934a2af029b57825505b5d21fcb7cd Mon Sep 17 00:00:00 2001 From: Daniel Mangold Date: Mon, 18 Nov 2024 23:21:57 +0100 Subject: [PATCH 06/48] Fixes and refactoring --- .../transform/SubmissionClassInfo.java | 66 +++++-- .../transform/SubmissionClassVisitor.java | 173 +++++++++++------- .../transform/SubmissionExecutionHandler.java | 77 ++------ .../algoutils/transform/util/FieldHeader.java | 9 + .../transform/util/MethodHeader.java | 37 ++-- .../transform/util/MethodSubstitution.java | 54 ++++++ .../transform/util/TransformationUtils.java | 5 +- 7 files changed, 254 insertions(+), 167 deletions(-) create mode 100644 tutor/src/main/java/org/tudalgo/algoutils/transform/util/MethodSubstitution.java diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassInfo.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassInfo.java index f46a52aa..37f9c986 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassInfo.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassInfo.java @@ -6,10 +6,12 @@ import org.objectweb.asm.*; import org.tudalgo.algoutils.tutor.general.match.MatchingUtils; +import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.*; +import java.util.stream.Collectors; import static org.objectweb.asm.Opcodes.ACC_SYNTHETIC; @@ -42,6 +44,8 @@ public class SubmissionClassInfo extends ClassVisitor { // Mapping of methods in submission => usable methods private final Map methods = new HashMap<>(); + private final Map superClassConstructors = new HashMap<>(); + /** * Constructs a new {@link SubmissionClassInfo} instance. * @@ -159,6 +163,19 @@ public MethodHeader getComputedMethodHeader(String name, String descriptor) { .orElseThrow(); } + public Set getOriginalSuperClassConstructorHeaders() { + return superClassConstructors.keySet(); + } + + 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) { submissionClassHeader = new ClassHeader(access, name, signature, superName, interfaces); @@ -221,8 +238,19 @@ public MethodVisitor visitMethod(int access, String name, String descriptor, Str @Override public void visitEnd() { for (Triple, Map> triple : superClassMembers) { + if (triple.getFirst().equals(superClass)) { + triple.getThird() + .entrySet() + .stream() + .filter(entry -> entry.getKey().name().equals("")) + .forEach(entry -> superClassConstructors.put(entry.getKey(), entry.getValue())); + } triple.getSecond().forEach(fields::putIfAbsent); - triple.getThird().forEach(methods::putIfAbsent); + triple.getThird() + .entrySet() + .stream() + .filter(entry -> !entry.getKey().name().equals("")) + .forEach(entry -> methods.putIfAbsent(entry.getKey(), entry.getValue())); } } @@ -254,34 +282,34 @@ private void resolveSuperClassMembers(Set(className, submissionClassInfo.fields, submissionClassInfo.methods)); + superClassMembers.add(new Triple<>(className, + 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)))); resolveSuperClassMembers(superClassMembers, submissionClassInfo.superClass, submissionClassInfo.interfaces); } else { try { Class clazz = Class.forName(className.replace('/', '.')); Map fieldHeaders = new HashMap<>(); for (Field field : clazz.getDeclaredFields()) { - if ((field.getModifiers() & Modifier.PRIVATE) != 0) continue; - FieldHeader fieldHeader = new FieldHeader( - className, - field.getModifiers(), - field.getName(), - Type.getDescriptor(field.getType()), - null - ); + if (Modifier.isPrivate(field.getModifiers())) continue; + FieldHeader fieldHeader = new FieldHeader(field); fieldHeaders.put(fieldHeader, fieldHeader); } Map methodHeaders = new HashMap<>(); + for (Constructor constructor : clazz.getDeclaredConstructors()) { + if (Modifier.isPrivate(constructor.getModifiers())) continue; + MethodHeader methodHeader = new MethodHeader(constructor); + methodHeaders.put(methodHeader, methodHeader); + } for (Method method : clazz.getDeclaredMethods()) { - if ((method.getModifiers() & Modifier.PRIVATE) != 0) continue; - MethodHeader methodHeader = new MethodHeader( - className, - method.getModifiers(), - method.getName(), - Type.getMethodDescriptor(method), - null, - Arrays.stream(method.getExceptionTypes()).map(Type::getInternalName).toArray(String[]::new) - ); + if (Modifier.isPrivate(method.getModifiers())) continue; + MethodHeader methodHeader = new MethodHeader(method); methodHeaders.put(methodHeader, methodHeader); } superClassMembers.add(new Triple<>(className, fieldHeaders, methodHeaders)); diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java index 2dd125e5..02865878 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java @@ -149,17 +149,26 @@ public FieldVisitor visitField(int access, String name, String descriptor, Strin */ @Override public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + Type objectType = Type.getType(Object.class); + Type objectArrayType = Type.getType(Object[].class); + Type stringType = Type.getType(String.class); + Type submissionExecutionHandlerType = SubmissionExecutionHandler.INTERNAL_TYPE; + Type sehInternalType = SubmissionExecutionHandler.Internal.INTERNAL_TYPE; + Type methodSubstitutionType = MethodSubstitution.INTERNAL_TYPE; + MethodHeader methodHeader = submissionClassInfo.getComputedMethodHeader(name, descriptor); visitedMethods.add(methodHeader); boolean isStatic = (methodHeader.access() & ACC_STATIC) != 0; boolean isConstructor = methodHeader.name().equals(""); // calculate length of locals array, including "this" if applicable - int submissionExecutionHandlerIndex = (Type.getArgumentsAndReturnSizes(methodHeader.descriptor()) >> 2) - (isStatic ? 1 : 0); - int methodHeaderIndex = submissionExecutionHandlerIndex + 1; + int nextLocalsIndex = (Type.getArgumentsAndReturnSizes(methodHeader.descriptor()) >> 2) - (isStatic ? 1 : 0); + int submissionExecutionHandlerIndex = nextLocalsIndex++; + int methodHeaderIndex = nextLocalsIndex++; + int methodSubstitutionIndex = nextLocalsIndex; // calculate default locals for frames - List parameterTypes = Arrays.stream(Type.getArgumentTypes(methodHeader.descriptor())) + List fullFrameLocals = Arrays.stream(Type.getArgumentTypes(methodHeader.descriptor())) .map(type -> switch (type.getSort()) { case Type.BOOLEAN, Type.BYTE, Type.SHORT, Type.CHAR, Type.INT -> INTEGER; case Type.FLOAT -> FLOAT; @@ -169,9 +178,8 @@ public MethodVisitor visitMethod(int access, String name, String descriptor, Str }) .collect(Collectors.toList()); if (!isStatic) { - parameterTypes.addFirst(isConstructor ? UNINITIALIZED_THIS : className); + fullFrameLocals.addFirst(isConstructor ? UNINITIALIZED_THIS : className); } - Object[] fullFrameLocals = parameterTypes.toArray(); return new MethodVisitor(ASM9, methodHeader.toMethodVisitor(getDelegate())) { @Override @@ -186,69 +194,76 @@ public void visitCode() { Label submissionExecutionHandlerVarLabel = new Label(); Label methodHeaderVarLabel = new Label(); Label substitutionCheckLabel = new Label(); + Label substitutionStartLabel = new Label(); + Label substitutionEndLabel = new Label(); Label delegationCheckLabel = new Label(); Label delegationCodeLabel = new Label(); Label submissionCodeLabel = new Label(); - // create SubmissionExecutionHandler$Internal instance and store in locals array - super.visitTypeInsn(NEW, SubmissionExecutionHandler.Internal.INTERNAL_TYPE.getInternalName()); - super.visitInsn(DUP); - super.visitMethodInsn(INVOKESTATIC, - SubmissionExecutionHandler.INTERNAL_TYPE.getInternalName(), - "getInstance", - Type.getMethodDescriptor(SubmissionExecutionHandler.INTERNAL_TYPE), - false); - super.visitMethodInsn(INVOKESPECIAL, - SubmissionExecutionHandler.Internal.INTERNAL_TYPE.getInternalName(), - "", - Type.getMethodDescriptor(Type.VOID_TYPE, SubmissionExecutionHandler.INTERNAL_TYPE), - false); - super.visitVarInsn(ASTORE, submissionExecutionHandlerIndex); - super.visitLabel(submissionExecutionHandlerVarLabel); - - // replicate method header in bytecode and store in locals array - buildMethodHeader(getDelegate(), methodHeader); - super.visitVarInsn(ASTORE, methodHeaderIndex); - super.visitLabel(methodHeaderVarLabel); - - super.visitFrame(F_APPEND, - 2, - new Object[] {SubmissionExecutionHandler.Internal.INTERNAL_TYPE.getInternalName(), methodHeader.getType().getInternalName()}, - 0, - null); - - // check if invocation should be logged - super.visitVarInsn(ALOAD, submissionExecutionHandlerIndex); - super.visitVarInsn(ALOAD, methodHeaderIndex); - super.visitMethodInsn(INVOKEVIRTUAL, - SubmissionExecutionHandler.Internal.INTERNAL_TYPE.getInternalName(), - "logInvocation", - Type.getMethodDescriptor(Type.BOOLEAN_TYPE, methodHeader.getType()), - false); - super.visitJumpInsn(IFEQ, isConstructor ? // jump to label if logInvocation(...) == false - defaultTransformationsOnly ? submissionCodeLabel : delegationCheckLabel : - substitutionCheckLabel); - - // intercept parameters - super.visitVarInsn(ALOAD, submissionExecutionHandlerIndex); - super.visitVarInsn(ALOAD, methodHeaderIndex); - buildInvocation(Type.getArgumentTypes(methodHeader.descriptor())); - super.visitMethodInsn(INVOKEVIRTUAL, - SubmissionExecutionHandler.Internal.INTERNAL_TYPE.getInternalName(), - "addInvocation", - Type.getMethodDescriptor(Type.VOID_TYPE, - methodHeader.getType(), - Invocation.INTERNAL_TYPE), - false); - - // check if substitution exists for this method if not constructor (because waaay too complex right now) + // Setup + { + // create SubmissionExecutionHandler$Internal instance and store in locals array + super.visitTypeInsn(NEW, sehInternalType.getInternalName()); + super.visitInsn(DUP); + super.visitMethodInsn(INVOKESTATIC, + submissionExecutionHandlerType.getInternalName(), + "getInstance", + Type.getMethodDescriptor(submissionExecutionHandlerType), + false); + super.visitMethodInsn(INVOKESPECIAL, + sehInternalType.getInternalName(), + "", + Type.getMethodDescriptor(Type.VOID_TYPE, submissionExecutionHandlerType), + false); + super.visitVarInsn(ASTORE, submissionExecutionHandlerIndex); + super.visitLabel(submissionExecutionHandlerVarLabel); + + // replicate method header in bytecode and store in locals array + buildMethodHeader(getDelegate(), methodHeader); + super.visitVarInsn(ASTORE, methodHeaderIndex); + super.visitLabel(methodHeaderVarLabel); + + super.visitFrame(F_APPEND, + 2, + new Object[] {sehInternalType.getInternalName(), methodHeader.getType().getInternalName()}, + 0, + null); + fullFrameLocals.add(sehInternalType.getInternalName()); + fullFrameLocals.add(methodHeader.getType().getInternalName()); + } + + // Invocation logging + { + // check if invocation should be logged + super.visitVarInsn(ALOAD, submissionExecutionHandlerIndex); + super.visitVarInsn(ALOAD, methodHeaderIndex); + super.visitMethodInsn(INVOKEVIRTUAL, + sehInternalType.getInternalName(), + "logInvocation", + Type.getMethodDescriptor(Type.BOOLEAN_TYPE, methodHeader.getType()), + false); + super.visitJumpInsn(IFEQ, substitutionCheckLabel); // jump to label if logInvocation(...) == false + + // intercept parameters + super.visitVarInsn(ALOAD, submissionExecutionHandlerIndex); + super.visitVarInsn(ALOAD, methodHeaderIndex); + buildInvocation(Type.getArgumentTypes(methodHeader.descriptor())); + super.visitMethodInsn(INVOKEVIRTUAL, + sehInternalType.getInternalName(), + "addInvocation", + Type.getMethodDescriptor(Type.VOID_TYPE, methodHeader.getType(), Invocation.INTERNAL_TYPE), + false); + } + + // Method substitution if (!isConstructor) { + // check if substitution exists for this method super.visitFrame(F_SAME, 0, null, 0, null); super.visitLabel(substitutionCheckLabel); super.visitVarInsn(ALOAD, submissionExecutionHandlerIndex); super.visitVarInsn(ALOAD, methodHeaderIndex); super.visitMethodInsn(INVOKEVIRTUAL, - SubmissionExecutionHandler.Internal.INTERNAL_TYPE.getInternalName(), + sehInternalType.getInternalName(), "useSubstitution", Type.getMethodDescriptor(Type.BOOLEAN_TYPE, methodHeader.getType()), false); @@ -258,17 +273,21 @@ public void visitCode() { super.visitVarInsn(ALOAD, submissionExecutionHandlerIndex); super.visitVarInsn(ALOAD, methodHeaderIndex); super.visitMethodInsn(INVOKEVIRTUAL, - SubmissionExecutionHandler.Internal.INTERNAL_TYPE.getInternalName(), + sehInternalType.getInternalName(), "getSubstitution", - Type.getMethodDescriptor(Invocation.INTERNAL_TYPE, - methodHeader.getType()), + Type.getMethodDescriptor(methodSubstitutionType, methodHeader.getType()), false); + super.visitVarInsn(ASTORE, methodSubstitutionIndex); + super.visitFrame(F_APPEND, 1, new Object[] {methodSubstitutionType.getInternalName()}, 0, null); + fullFrameLocals.add(methodSubstitutionType.getInternalName()); + super.visitLabel(substitutionStartLabel); + + super.visitVarInsn(ALOAD, methodSubstitutionIndex); buildInvocation(Type.getArgumentTypes(methodHeader.descriptor())); super.visitMethodInsn(INVOKEINTERFACE, - SubmissionExecutionHandler.MethodSubstitution.INTERNAL_TYPE.getInternalName(), + methodSubstitutionType.getInternalName(), "execute", - Type.getMethodDescriptor(Type.getType(Object.class), - Invocation.INTERNAL_TYPE), + Type.getMethodDescriptor(objectType, Invocation.INTERNAL_TYPE), true); Type returnType = Type.getReturnType(methodHeader.descriptor()); if (returnType.getSort() == Type.ARRAY || returnType.getSort() == Type.OBJECT) { @@ -277,17 +296,26 @@ public void visitCode() { unboxType(getDelegate(), returnType); } super.visitInsn(returnType.getOpcode(IRETURN)); + super.visitLabel(substitutionEndLabel); + super.visitLocalVariable("methodSubstitution", + methodSubstitutionType.getDescriptor(), + null, + substitutionStartLabel, + substitutionEndLabel, + methodSubstitutionIndex); } + // Method delegation // if only default transformations are applied, skip delegation if (!defaultTransformationsOnly) { // check if call should be delegated to solution or not - super.visitFrame(F_SAME, 0, null, 0, null); + fullFrameLocals.removeLast(); + super.visitFrame(F_FULL, fullFrameLocals.size(), fullFrameLocals.toArray(), 0, new Object[0]); super.visitLabel(delegationCheckLabel); super.visitVarInsn(ALOAD, submissionExecutionHandlerIndex); super.visitVarInsn(ALOAD, methodHeaderIndex); super.visitMethodInsn(INVOKEVIRTUAL, - SubmissionExecutionHandler.Internal.INTERNAL_TYPE.getInternalName(), + sehInternalType.getInternalName(), "useSubmissionImpl", Type.getMethodDescriptor(Type.BOOLEAN_TYPE, methodHeader.getType()), false); @@ -295,9 +323,11 @@ public void visitCode() { // replay instructions from solution super.visitFrame(F_CHOP, 2, null, 0, null); + fullFrameLocals.removeLast(); + fullFrameLocals.removeLast(); super.visitLabel(delegationCodeLabel); super.visitLocalVariable("submissionExecutionHandler", - SubmissionExecutionHandler.Internal.INTERNAL_TYPE.getDescriptor(), + sehInternalType.getDescriptor(), null, submissionExecutionHandlerVarLabel, delegationCodeLabel, @@ -310,13 +340,16 @@ public void visitCode() { methodHeaderIndex); solutionMethodNodes.get(methodHeader).accept(getDelegate()); - super.visitFrame(F_FULL, fullFrameLocals.length, fullFrameLocals, 0, new Object[0]); + super.visitFrame(F_FULL, fullFrameLocals.size(), fullFrameLocals.toArray(), 0, new Object[0]); super.visitLabel(submissionCodeLabel); } else { - super.visitFrame(F_CHOP, 2, null, 0, null); + fullFrameLocals.removeLast(); + fullFrameLocals.removeLast(); + fullFrameLocals.removeLast(); + super.visitFrame(F_FULL, fullFrameLocals.size(), fullFrameLocals.toArray(), 0, new Object[0]); super.visitLabel(submissionCodeLabel); super.visitLocalVariable("submissionExecutionHandler", - SubmissionExecutionHandler.Internal.INTERNAL_TYPE.getDescriptor(), + sehInternalType.getDescriptor(), null, submissionExecutionHandlerVarLabel, submissionCodeLabel, diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionExecutionHandler.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionExecutionHandler.java index 513d9dda..af050043 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionExecutionHandler.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionExecutionHandler.java @@ -1,12 +1,11 @@ package org.tudalgo.algoutils.transform; import org.tudalgo.algoutils.transform.util.*; -import kotlin.Pair; import org.objectweb.asm.Type; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; -import java.lang.reflect.Method; +import java.lang.reflect.Executable; import java.util.*; /** @@ -155,12 +154,12 @@ public void resetMethodInvocationLogging() { } /** - * Enables logging of method invocations for the given method. + * Enables logging of method / constructor invocations for the given executable. * - * @param method the method to enable invocation logging for + * @param executable the method / constructor to enable invocation logging for */ - public void enableMethodInvocationLogging(Method method) { - enableMethodInvocationLogging(new MethodHeader(method)); + public void enableMethodInvocationLogging(Executable executable) { + enableMethodInvocationLogging(new MethodHeader(executable)); } /** @@ -174,13 +173,13 @@ public void enableMethodInvocationLogging(MethodHeader methodHeader) { } /** - * Returns all logged invocations for the given method. + * Returns all logged invocations for the given method / constructor. * - * @param method the method to get invocations of + * @param executable the method / constructor to get invocations of * @return a list of invocations on the given method */ - public List getInvocationsForMethod(Method method) { - return getInvocationsForMethod(new MethodHeader(method)); + public List getInvocationsForMethod(Executable executable) { + return getInvocationsForMethod(new MethodHeader(executable)); } /** @@ -206,15 +205,15 @@ public void resetMethodSubstitution() { } /** - * Substitute calls to the given method with the invocation of the given {@link MethodSubstitution}. + * 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 during runtime. * - * @param method the method to substitute + * @param executable the method / constructor to substitute * @param substitute the {@link MethodSubstitution} the method will be substituted with */ - public void substituteMethod(Method method, MethodSubstitution substitute) { - substituteMethod(new MethodHeader(method), substitute); + public void substituteMethod(Executable executable, MethodSubstitution substitute) { + substituteMethod(new MethodHeader(executable), substitute); } /** @@ -240,12 +239,12 @@ public void resetMethodDelegation() { } /** - * Disables delegation to the solution for the given method. + * Disables delegation to the solution for the given executable. * - * @param method the method to disable delegation for + * @param executable the method / constructor to disable delegation for */ - public void disableMethodDelegation(Method method) { - disableMethodDelegation(new MethodHeader(method)); + public void disableMethodDelegation(Executable executable) { + disableMethodDelegation(new MethodHeader(executable)); } /** @@ -258,48 +257,6 @@ public void disableMethodDelegation(MethodHeader methodHeader) { .put(methodHeader, true); } - /** - * 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 { - - Type INTERNAL_TYPE = Type.getType(MethodSubstitution.class); - - /** - * DO NOT USE, THIS METHOD HAS NO EFFECT RIGHT NOW. - * TODO: implement constructor substitution - *

- * 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 pair consisting of... - *
    - *
  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 first pair entry must be either the original method's owner (for {@code this(...)}) or - * the superclass (for {@code super(...)}). - * The second entry is an array of parameter values for that constructor. - * Default behaviour assumes calling the constructor of {@link Object}, i.e., a class that has no superclass. - * - * @return a pair containing the target method's owner and arguments - */ - default Pair constructorBehaviour() { - return new Pair<>("java/lang/Object", new Object[0]); - } - - /** - * 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); - } - /** * Collection of methods injected into the bytecode of transformed methods. */ diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/FieldHeader.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/FieldHeader.java index f846dc35..f12dd8ff 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/FieldHeader.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/FieldHeader.java @@ -4,6 +4,7 @@ import org.objectweb.asm.FieldVisitor; import org.objectweb.asm.Type; +import java.lang.reflect.Field; import java.util.Objects; /** @@ -30,6 +31,14 @@ public record FieldHeader(String owner, int access, String name, String descript Type.getType(String.class) }; + public FieldHeader(Field field) { + this(Type.getInternalName(field.getDeclaringClass()), + field.getModifiers(), + field.getName(), + Type.getDescriptor(field.getType()), + null); + } + @Override public Type getType() { return INTERNAL_TYPE; diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/MethodHeader.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/MethodHeader.java index a423f502..b3f5c7dd 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/MethodHeader.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/MethodHeader.java @@ -4,6 +4,7 @@ import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Type; +import java.lang.reflect.Executable; import java.lang.reflect.Method; import java.util.Arrays; import java.util.Objects; @@ -35,6 +36,26 @@ public record MethodHeader(String owner, int access, String name, String descrip Type.getType(String[].class) }; + /** + * 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 getType() { return INTERNAL_TYPE; @@ -58,22 +79,6 @@ public Object getValue(String name) { }; } - /** - * Constructs a new method header using the given method. - * - * @param method a java reflection method - */ - public MethodHeader(Method method) { - this(Type.getInternalName(method.getDeclaringClass()), - method.getModifiers(), - method.getName(), - Type.getMethodDescriptor(method), - null, - Arrays.stream(method.getExceptionTypes()) - .map(Type::getInternalName) - .toArray(String[]::new)); - } - /** * Visits a method in the given class visitor using the information stored in this record. * 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..54e016f3 --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/MethodSubstitution.java @@ -0,0 +1,54 @@ +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 { + + Type INTERNAL_TYPE = Type.getType(MethodSubstitution.class); + + /** + * 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 ConstructorBehaviour} 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 ConstructorBehaviour getConstructorBehaviour() { + return new ConstructorBehaviour("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 ConstructorBehaviour(String owner, String descriptor, Object... args) { + public static final Type INTERNAL_TYPE = Type.getType(ConstructorBehaviour.class); + } +} 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 index 98c64d2f..e7f9e112 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationUtils.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationUtils.java @@ -42,7 +42,7 @@ public static void boxType(MethodVisitor mv, Type type) { /** * Automatically unbox primitive types using the supplied {@link MethodVisitor}. - * If the given type is not a primitive type, this method does nothing. + * 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 @@ -81,6 +81,7 @@ public static void unboxType(MethodVisitor mv, Type type) { 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()); } } @@ -271,7 +272,7 @@ private static int buildHeader(MethodVisitor mv, Header header, String... keys) for (int i = 0; i < keys.length; i++) { Object value = header.getValue(keys[i]); if (constructorParameterTypes[i].equals(Type.getType(String[].class))) { - int stackUsed = buildArray(mv, Type.getType(String[].class), (Object[]) value); + int stackUsed = buildArray(mv, Type.getType(String.class), (Object[]) value); maxStack = Math.max(maxStack, stackSize++ + stackUsed); } else { if (value != null) { From c545b48e8e8ffa5fa9264943f68dedbd84f30ad9 Mon Sep 17 00:00:00 2001 From: Daniel Mangold Date: Mon, 18 Nov 2024 23:25:26 +0100 Subject: [PATCH 07/48] Implement constructor substitution --- .../transform/SubmissionClassVisitor.java | 181 +++++++++++++++++- 1 file changed, 179 insertions(+), 2 deletions(-) diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java index 02865878..ddfaefa0 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java @@ -6,7 +6,9 @@ import org.objectweb.asm.tree.MethodNode; import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; +import java.util.stream.Stream; import static org.tudalgo.algoutils.transform.util.TransformationUtils.*; import static org.objectweb.asm.Opcodes.*; @@ -155,6 +157,7 @@ public MethodVisitor visitMethod(int access, String name, String descriptor, Str Type submissionExecutionHandlerType = SubmissionExecutionHandler.INTERNAL_TYPE; Type sehInternalType = SubmissionExecutionHandler.Internal.INTERNAL_TYPE; Type methodSubstitutionType = MethodSubstitution.INTERNAL_TYPE; + Type constructorBehaviourType = MethodSubstitution.ConstructorBehaviour.INTERNAL_TYPE; MethodHeader methodHeader = submissionClassInfo.getComputedMethodHeader(name, descriptor); visitedMethods.add(methodHeader); @@ -165,7 +168,8 @@ public MethodVisitor visitMethod(int access, String name, String descriptor, Str int nextLocalsIndex = (Type.getArgumentsAndReturnSizes(methodHeader.descriptor()) >> 2) - (isStatic ? 1 : 0); int submissionExecutionHandlerIndex = nextLocalsIndex++; int methodHeaderIndex = nextLocalsIndex++; - int methodSubstitutionIndex = nextLocalsIndex; + int methodSubstitutionIndex = nextLocalsIndex++; + int constructorBehaviourIndex = nextLocalsIndex; // calculate default locals for frames List fullFrameLocals = Arrays.stream(Type.getArgumentTypes(methodHeader.descriptor())) @@ -256,7 +260,7 @@ public void visitCode() { } // Method substitution - if (!isConstructor) { + { // check if substitution exists for this method super.visitFrame(F_SAME, 0, null, 0, null); super.visitLabel(substitutionCheckLabel); @@ -282,6 +286,106 @@ public void visitCode() { fullFrameLocals.add(methodSubstitutionType.getInternalName()); super.visitLabel(substitutionStartLabel); + if (isConstructor) { + List superConstructors = submissionClassInfo.getOriginalSuperClassConstructorHeaders() + .stream() + .map(mh -> submissionClassInfo.getComputedSuperClassConstructorHeader(mh.descriptor())) + .toList(); + List constructors = submissionClassInfo.getOriginalMethodHeaders() + .stream() + .filter(mh -> mh.name().equals("")) + .map(mh -> submissionClassInfo.getComputedMethodHeader(mh.name(), mh.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.ConstructorBehaviour cb = methodSubstitution.getConstructorBehaviour(); + * 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(...); + * } + */ + super.visitVarInsn(ALOAD, methodSubstitutionIndex); + super.visitMethodInsn(INVOKEINTERFACE, + methodSubstitutionType.getInternalName(), + "getConstructorBehaviour", + Type.getMethodDescriptor(constructorBehaviourType), + true); + super.visitVarInsn(ASTORE, constructorBehaviourIndex); + super.visitFrame(F_APPEND, 1, new Object[] {constructorBehaviourType.getInternalName()}, 0, null); + fullFrameLocals.add(constructorBehaviourType.getInternalName()); + super.visitLabel(labels[0]); + for (MethodHeader superConstructorHeader : superConstructors) { + buildConstructorInvocationBranch(superConstructorHeader, substitutionExecuteLabel, labels, labelIndex); + } + for (MethodHeader constructorHeader : constructors) { + buildConstructorInvocationBranch(constructorHeader, substitutionExecuteLabel, labels, labelIndex); + } + + // if no matching constructor was found, throw an IllegalArgumentException + { + Type illegalArgumentExceptionType = Type.getType(IllegalArgumentException.class); + super.visitTypeInsn(NEW, illegalArgumentExceptionType.getInternalName()); + super.visitInsn(DUP); + + super.visitLdcInsn("No matching constructor was found for owner %s and descriptor %s"); + super.visitInsn(ICONST_2); + super.visitTypeInsn(ANEWARRAY, stringType.getInternalName()); + super.visitInsn(DUP); + super.visitInsn(ICONST_0); + super.visitVarInsn(ALOAD, constructorBehaviourIndex); + super.visitMethodInsn(INVOKEVIRTUAL, + constructorBehaviourType.getInternalName(), + "owner", + Type.getMethodDescriptor(stringType), + false); + super.visitInsn(AASTORE); + super.visitInsn(DUP); + super.visitInsn(ICONST_1); + super.visitVarInsn(ALOAD, constructorBehaviourIndex); + super.visitMethodInsn(INVOKEVIRTUAL, + constructorBehaviourType.getInternalName(), + "descriptor", + Type.getMethodDescriptor(stringType), + false); + super.visitInsn(AASTORE); + super.visitMethodInsn(INVOKEVIRTUAL, + stringType.getInternalName(), + "formatted", + Type.getMethodDescriptor(stringType, objectArrayType), + false); + + super.visitMethodInsn(INVOKESPECIAL, + illegalArgumentExceptionType.getInternalName(), + "", + Type.getMethodDescriptor(Type.VOID_TYPE, stringType), + false); + super.visitInsn(ATHROW); + } + + fullFrameLocals.removeLast(); + List locals = new ArrayList<>(fullFrameLocals); + locals.set(0, className); + super.visitFrame(F_FULL, locals.size(), locals.toArray(), 0, null); + super.visitLabel(substitutionExecuteLabel); + super.visitLocalVariable("constructorBehaviour", + constructorBehaviourType.getDescriptor(), + null, + labels[labelIndex.get()], + substitutionExecuteLabel, + constructorBehaviourIndex); + } + super.visitVarInsn(ALOAD, methodSubstitutionIndex); buildInvocation(Type.getArgumentTypes(methodHeader.descriptor())); super.visitMethodInsn(INVOKEINTERFACE, @@ -366,6 +470,79 @@ public void visitCode() { super.visitCode(); } + private void buildConstructorInvocationBranch(MethodHeader constructorHeader, + Label substitutionExecuteLabel, + Label[] labels, + AtomicInteger labelIndex) { + super.visitVarInsn(ALOAD, constructorBehaviourIndex); + super.visitMethodInsn(INVOKEVIRTUAL, + constructorBehaviourType.getInternalName(), + "owner", + Type.getMethodDescriptor(stringType), + false); + super.visitLdcInsn(constructorHeader.owner()); + super.visitMethodInsn(INVOKEVIRTUAL, + stringType.getInternalName(), + "equals", + Type.getMethodDescriptor(Type.BOOLEAN_TYPE, objectType), + false); + + super.visitVarInsn(ALOAD, constructorBehaviourIndex); + super.visitMethodInsn(INVOKEVIRTUAL, + constructorBehaviourType.getInternalName(), + "descriptor", + Type.getMethodDescriptor(stringType), + false); + super.visitLdcInsn(constructorHeader.descriptor()); + super.visitMethodInsn(INVOKEVIRTUAL, + stringType.getInternalName(), + "equals", + Type.getMethodDescriptor(Type.BOOLEAN_TYPE, objectType), + false); + + super.visitInsn(IAND); + super.visitJumpInsn(IFEQ, labels[labelIndex.get() + 1]); // jump to next branch if false + + Label argsVarStartLabel = new Label(); + super.visitVarInsn(ALOAD, constructorBehaviourIndex); + super.visitMethodInsn(INVOKEVIRTUAL, + constructorBehaviourType.getInternalName(), + "args", + Type.getMethodDescriptor(objectArrayType), + false); + super.visitVarInsn(ASTORE, constructorBehaviourIndex + 1); + super.visitFrame(F_APPEND, 1, new Object[] {objectArrayType.getInternalName()}, 0, null); + fullFrameLocals.add(objectArrayType.getInternalName()); + super.visitLabel(argsVarStartLabel); + + super.visitVarInsn(ALOAD, 0); + Type[] parameterTypes = Type.getArgumentTypes(constructorHeader.descriptor()); + for (int i = 0; i < parameterTypes.length; i++) { // unpack array + super.visitVarInsn(ALOAD, constructorBehaviourIndex + 1); + super.visitIntInsn(SIPUSH, i); + super.visitInsn(AALOAD); + unboxType(getDelegate(), parameterTypes[i]); + } + super.visitMethodInsn(INVOKESPECIAL, + constructorHeader.owner(), + "", + constructorHeader.descriptor(), + false); + super.visitJumpInsn(GOTO, substitutionExecuteLabel); + + fullFrameLocals.removeLast(); +// List locals = new ArrayList<>(fullFrameLocals); +// locals.set(0, className); + super.visitFrame(F_CHOP, 1, null, 0, new Object[0]); + super.visitLabel(labels[labelIndex.incrementAndGet()]); + super.visitLocalVariable("args", + objectArrayType.getDescriptor(), + null, + argsVarStartLabel, + labels[labelIndex.get()], + constructorBehaviourIndex + 1); + } + @Override public void visitFieldInsn(int opcode, String owner, String name, String descriptor) { // skip transformation if only default transformations are applied or owner is not part of the submission From cd102dade07085bc0c45e757c64be018739156d2 Mon Sep 17 00:00:00 2001 From: Daniel Mangold Date: Tue, 19 Nov 2024 11:48:04 +0100 Subject: [PATCH 08/48] Record stack trace in invocation --- .../transform/SubmissionClassVisitor.java | 44 ++++++++++++++----- .../algoutils/transform/util/Invocation.java | 30 ++++++++++--- 2 files changed, 59 insertions(+), 15 deletions(-) diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java index ddfaefa0..ab005f00 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java @@ -156,6 +156,7 @@ public MethodVisitor visitMethod(int access, String name, String descriptor, Str Type stringType = Type.getType(String.class); Type submissionExecutionHandlerType = SubmissionExecutionHandler.INTERNAL_TYPE; Type sehInternalType = SubmissionExecutionHandler.Internal.INTERNAL_TYPE; + Type invocationType = Invocation.INTERNAL_TYPE; Type methodSubstitutionType = MethodSubstitution.INTERNAL_TYPE; Type constructorBehaviourType = MethodSubstitution.ConstructorBehaviour.INTERNAL_TYPE; @@ -255,7 +256,7 @@ public void visitCode() { super.visitMethodInsn(INVOKEVIRTUAL, sehInternalType.getInternalName(), "addInvocation", - Type.getMethodDescriptor(Type.VOID_TYPE, methodHeader.getType(), Invocation.INTERNAL_TYPE), + Type.getMethodDescriptor(Type.VOID_TYPE, methodHeader.getType(), invocationType), false); } @@ -391,7 +392,7 @@ public void visitCode() { super.visitMethodInsn(INVOKEINTERFACE, methodSubstitutionType.getInternalName(), "execute", - Type.getMethodDescriptor(objectType, Invocation.INTERNAL_TYPE), + Type.getMethodDescriptor(objectType, invocationType), true); Type returnType = Type.getReturnType(methodHeader.descriptor()); if (returnType.getSort() == Type.ARRAY || returnType.getSort() == Type.OBJECT) { @@ -571,31 +572,54 @@ public void visitMethodInsn(int opcode, String owner, String name, String descri * @param argumentTypes an array of parameter types */ private void buildInvocation(Type[] argumentTypes) { - super.visitTypeInsn(NEW, Invocation.INTERNAL_TYPE.getInternalName()); + Type threadType = Type.getType(Thread.class); + Type stackTraceElementArrayType = Type.getType(StackTraceElement[].class); + + super.visitTypeInsn(NEW, invocationType.getInternalName()); super.visitInsn(DUP); if (!isStatic && !isConstructor) { super.visitVarInsn(ALOAD, 0); + super.visitMethodInsn(INVOKESTATIC, + threadType.getInternalName(), + "currentThread", + Type.getMethodDescriptor(threadType), + false); + super.visitMethodInsn(INVOKEVIRTUAL, + threadType.getInternalName(), + "getStackTrace", + Type.getMethodDescriptor(stackTraceElementArrayType), + false); super.visitMethodInsn(INVOKESPECIAL, - Invocation.INTERNAL_TYPE.getInternalName(), + invocationType.getInternalName(), "", - "(Ljava/lang/Object;)V", + Type.getMethodDescriptor(Type.VOID_TYPE, objectType, stackTraceElementArrayType), false); } else { + super.visitMethodInsn(INVOKESTATIC, + threadType.getInternalName(), + "currentThread", + Type.getMethodDescriptor(threadType), + false); + super.visitMethodInsn(INVOKEVIRTUAL, + threadType.getInternalName(), + "getStackTrace", + Type.getMethodDescriptor(stackTraceElementArrayType), + false); super.visitMethodInsn(INVOKESPECIAL, - Invocation.INTERNAL_TYPE.getInternalName(), + invocationType.getInternalName(), "", - "()V", + Type.getMethodDescriptor(Type.VOID_TYPE, stackTraceElementArrayType), false); } + // load parameter with opcode (ALOAD, ILOAD, etc.) for type and ignore "this", if it exists for (int i = 0; i < argumentTypes.length; i++) { super.visitInsn(DUP); - // load parameter with opcode (ALOAD, ILOAD, etc.) for type and ignore "this", if it exists super.visitVarInsn(argumentTypes[i].getOpcode(ILOAD), getLocalsIndex(argumentTypes, i) + (isStatic ? 0 : 1)); boxType(getDelegate(), argumentTypes[i]); super.visitMethodInsn(INVOKEVIRTUAL, - Invocation.INTERNAL_TYPE.getInternalName(), + invocationType.getInternalName(), "addParameter", - "(Ljava/lang/Object;)V", + Type.getMethodDescriptor(Type.VOID_TYPE, objectType), false); } } 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 index f26fbd8f..e4f7d285 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Invocation.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Invocation.java @@ -9,29 +9,36 @@ /** * This class holds information about the context of an invocation. - * Context means the object a method was invoked on and the parameters it was invoked with. + * Context means the object a 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 */ public class Invocation { public static final Type INTERNAL_TYPE = Type.getType(Invocation.class); private final Object instance; + private final StackTraceElement[] stackTrace; private final List parameterValues = new ArrayList<>(); /** * Constructs a new invocation. + * + * @param stackTrace the stack trace up to the point of invocation */ - public Invocation() { - this(null); + public Invocation(StackTraceElement[] stackTrace) { + this(null, stackTrace); } /** * Constructs a new invocation. * - * @param instance the object on which this invocation takes place + * @param instance the object on which this invocation takes place + * @param stackTrace the stack trace up to the point of invocation */ - public Invocation(Object instance) { + public Invocation(Object instance, StackTraceElement[] stackTrace) { this.instance = instance; + this.stackTrace = stackTrace; } /** @@ -43,6 +50,19 @@ public Object getInstance() { return instance; } + /** + * Returns the stack trace up to the point of this method's invocation. + * + * @return the stack trace + */ + public StackTraceElement[] getStackTrace() { + return stackTrace; + } + + public StackTraceElement getCallerStackTraceElement() { + return stackTrace[1]; + } + /** * Returns the list of parameter values the method was invoked with. * From 35a7d2479d6033cd6e17cce0c436c91c722136a0 Mon Sep 17 00:00:00 2001 From: Daniel Mangold Date: Tue, 19 Nov 2024 14:56:37 +0100 Subject: [PATCH 09/48] Implement method call replacement --- .../transform/SolutionClassNode.java | 28 +++++++---- .../SolutionMergingClassTransformer.java | 46 ++++++++++++++++++- .../transform/SubmissionClassInfo.java | 4 ++ .../transform/SubmissionClassVisitor.java | 12 ++++- .../transform/util/TransformationContext.java | 22 +++++++++ .../transform/util/TransformationUtils.java | 12 +++-- 6 files changed, 108 insertions(+), 16 deletions(-) diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionClassNode.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionClassNode.java index b641acbf..58d67d09 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionClassNode.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionClassNode.java @@ -10,13 +10,13 @@ import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.FieldNode; import org.objectweb.asm.tree.MethodNode; +import org.tudalgo.algoutils.transform.util.TransformationContext; import java.util.Arrays; import java.util.HashMap; import java.util.Map; -import static org.objectweb.asm.Opcodes.ACC_SYNTHETIC; -import static org.objectweb.asm.Opcodes.ASM9; +import static org.objectweb.asm.Opcodes.*; /** * A class node for recording bytecode instructions of solution classes. @@ -24,6 +24,7 @@ */ public class SolutionClassNode extends ClassNode { + private final TransformationContext transformationContext; private final String className; private ClassHeader classHeader; private final Map fields = new HashMap<>(); @@ -34,8 +35,9 @@ public class SolutionClassNode extends ClassNode { * * @param className the name of the solution class */ - public SolutionClassNode(String className) { + public SolutionClassNode(TransformationContext transformationContext, String className) { super(Opcodes.ASM9); + this.transformationContext = transformationContext; this.className = className; } @@ -103,11 +105,21 @@ private MethodNode getMethodNode(int access, String name, String descriptor, Str return new MethodNode(ASM9, access, name, descriptor, signature, exceptions) { @Override public void visitMethodInsn(int opcodeAndSource, String owner, String name, String descriptor, boolean isInterface) { - super.visitMethodInsn(opcodeAndSource, - owner, - name + (name.startsWith("lambda$") ? "$solution" : ""), - descriptor, - isInterface); + MethodHeader methodHeader = new MethodHeader(owner, 0, name, descriptor, null, null); + 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 diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionMergingClassTransformer.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionMergingClassTransformer.java index d393dc82..a55f170c 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionMergingClassTransformer.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionMergingClassTransformer.java @@ -1,6 +1,7 @@ package org.tudalgo.algoutils.transform; import org.tudalgo.algoutils.student.annotation.ForceSignature; +import org.tudalgo.algoutils.transform.util.MethodHeader; import org.tudalgo.algoutils.transform.util.TransformationContext; import org.tudalgo.algoutils.transform.util.TransformationUtils; import org.objectweb.asm.ClassReader; @@ -8,6 +9,7 @@ import org.objectweb.asm.Type; import org.sourcegrade.jagr.api.testing.ClassTransformer; +import java.lang.reflect.Executable; import java.util.*; import java.util.concurrent.ConcurrentHashMap; @@ -91,7 +93,7 @@ private SolutionMergingClassTransformer(Builder builder) { submissionClasses); ((List) builder.configuration.get(Config.SOLUTION_CLASSES)).stream() .map(s -> s.replace('.', '/')) - .forEach(className -> solutionClasses.put(className, TransformationUtils.readSolutionClass(className))); + .forEach(className -> solutionClasses.put(className, TransformationUtils.readSolutionClass(transformationContext, className))); } @Override @@ -116,7 +118,8 @@ public void transform(ClassReader reader, ClassWriter writer) { public enum Config { PROJECT_PREFIX(null), SOLUTION_CLASSES(null), - SIMILARITY(0.90); + SIMILARITY(0.90), + METHOD_REPLACEMENTS(new HashMap()); private final Object defaultValue; @@ -157,6 +160,45 @@ public Builder setSimilarity(double 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) { + ((Map) configuration.get(Config.METHOD_REPLACEMENTS)) + .put(targetMethodHeader, replacementMethodHeader); + return this; + } + /** * Constructs the transformer. * diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassInfo.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassInfo.java index 37f9c986..fc183c9b 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassInfo.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassInfo.java @@ -280,6 +280,10 @@ private void resolveSuperClassMembers(Set, Map>> superClassMembers, String className) { + if (className == null) { + return; + } + if (className.startsWith(transformationContext.getProjectPrefix())) { SubmissionClassInfo submissionClassInfo = transformationContext.getSubmissionClassInfo(className); superClassMembers.add(new Triple<>(className, diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java index ab005f00..90b877fd 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java @@ -558,10 +558,18 @@ public void visitFieldInsn(int opcode, String owner, String name, String descrip @Override public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) { // skip transformation if only default transformations are applied or owner is not part of the submission - if (defaultTransformationsOnly || !owner.startsWith(transformationContext.getProjectPrefix())) { + MethodHeader methodHeader = new MethodHeader(owner, 0, name, descriptor, null, null); + if (transformationContext.methodHasReplacement(methodHeader)) { + MethodHeader replacementMethodHeader = transformationContext.getMethodReplacement(methodHeader); + super.visitMethodInsn(INVOKESTATIC, + replacementMethodHeader.owner(), + replacementMethodHeader.name(), + replacementMethodHeader.descriptor(), + false); + } else if (defaultTransformationsOnly || !owner.startsWith(transformationContext.getProjectPrefix())) { super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); } else { - MethodHeader methodHeader = transformationContext.getSubmissionClassInfo(owner).getComputedMethodHeader(name, descriptor); + methodHeader = transformationContext.getSubmissionClassInfo(owner).getComputedMethodHeader(name, descriptor); super.visitMethodInsn(opcode, methodHeader.owner(), methodHeader.name(), methodHeader.descriptor(), isInterface); } } 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 index f6c32a69..c0f6fa01 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationContext.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationContext.java @@ -38,6 +38,28 @@ 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); + } + /** * Returns the {@link SubmissionClassInfo} for a given submission class name. * If no mapping exists in {@link #submissionClasses}, will attempt to compute one. 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 index e7f9e112..a56d8aa6 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationUtils.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationUtils.java @@ -147,10 +147,11 @@ public static int buildMethodHeader(MethodVisitor mv, MethodHeader methodHeader) /** * Attempts to read and process a solution class from {@code resources/classes/}. * - * @param className the name of the solution class + * @param transformationContext a {@link TransformationContext} object + * @param className the name of the solution class * @return the resulting {@link SolutionClassNode} object */ - public static SolutionClassNode readSolutionClass(String className) { + public static SolutionClassNode readSolutionClass(TransformationContext transformationContext, String className) { ClassReader solutionClassReader; String solutionClassFilePath = "/classes/%s.bin".formatted(className); try (InputStream is = SolutionMergingClassTransformer.class.getResourceAsStream(solutionClassFilePath)) { @@ -161,7 +162,7 @@ public static SolutionClassNode readSolutionClass(String className) { } catch (IOException e) { throw new RuntimeException(e); } - SolutionClassNode solutionClassNode = new SolutionClassNode(className); + SolutionClassNode solutionClassNode = new SolutionClassNode(transformationContext, className); solutionClassReader.accept(solutionClassNode, 0); return solutionClassNode; } @@ -177,9 +178,12 @@ public static SubmissionClassInfo readSubmissionClass(TransformationContext tran ClassReader submissionClassReader; String submissionClassFilePath = "/%s.class".formatted(className); try (InputStream is = SolutionMergingClassTransformer.class.getResourceAsStream(submissionClassFilePath)) { + if (is == null) { + return null; + } submissionClassReader = new ClassReader(is); } catch (IOException e) { - return null; + throw new RuntimeException(e); } SubmissionClassInfo submissionClassInfo = new SubmissionClassInfo( transformationContext, From ebfbde111c26071cbd5eeeb0dc9b95e4b9819a3a Mon Sep 17 00:00:00 2001 From: Daniel Mangold Date: Tue, 19 Nov 2024 20:06:45 +0100 Subject: [PATCH 10/48] Refactoring --- .../transform/SubmissionClassVisitor.java | 512 +---------------- .../transform/SubmissionExecutionHandler.java | 5 - .../transform/SubmissionMethodVisitor.java | 523 ++++++++++++++++++ .../algoutils/transform/util/ClassHeader.java | 13 +- .../algoutils/transform/util/Constants.java | 50 ++ .../algoutils/transform/util/FieldHeader.java | 13 +- .../ForceSignatureAnnotationProcessor.java | 8 +- .../algoutils/transform/util/Invocation.java | 17 +- .../transform/util/MethodHeader.java | 14 +- .../transform/util/MethodSubstitution.java | 12 +- .../transform/util/TransformationUtils.java | 4 +- 11 files changed, 622 insertions(+), 549 deletions(-) create mode 100644 tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionMethodVisitor.java create mode 100644 tutor/src/main/java/org/tudalgo/algoutils/transform/util/Constants.java diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java index 90b877fd..3320b52b 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java @@ -6,9 +6,6 @@ import org.objectweb.asm.tree.MethodNode; import java.util.*; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.stream.Collectors; -import java.util.stream.Stream; import static org.tudalgo.algoutils.transform.util.TransformationUtils.*; import static org.objectweb.asm.Opcodes.*; @@ -63,9 +60,26 @@ * .addParameter(...) // for each parameter * ...); * } - * if (submissionExecutionHandler.useSubstitution(methodHeader)) { // if not constructor - * submissionExecutionHandler.getSubstitution(methodHeader) - * .execute(new Invocation(...) ...); // same as above + * 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 @@ -82,6 +96,7 @@ * public static Set<MethodHeader> getOriginalMethodHeaders() {...} * * + * @see SubmissionMethodVisitor * @see SubmissionExecutionHandler * @author Daniel Mangold */ @@ -128,7 +143,6 @@ public void visit(int version, int access, String name, String signature, String .map(SolutionClassNode::getClassHeader) .orElse(submissionClassInfo.getOriginalClassHeader()) .visitClass(getDelegate(), version); -// .visitClass(getDelegate(), version, Type.getInternalName(SubmissionClassMetadata.class)); } /** @@ -151,487 +165,17 @@ public FieldVisitor visitField(int access, String name, String descriptor, Strin */ @Override public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { - Type objectType = Type.getType(Object.class); - Type objectArrayType = Type.getType(Object[].class); - Type stringType = Type.getType(String.class); - Type submissionExecutionHandlerType = SubmissionExecutionHandler.INTERNAL_TYPE; - Type sehInternalType = SubmissionExecutionHandler.Internal.INTERNAL_TYPE; - Type invocationType = Invocation.INTERNAL_TYPE; - Type methodSubstitutionType = MethodSubstitution.INTERNAL_TYPE; - Type constructorBehaviourType = MethodSubstitution.ConstructorBehaviour.INTERNAL_TYPE; - MethodHeader methodHeader = submissionClassInfo.getComputedMethodHeader(name, descriptor); + MethodVisitor methodVisitor = methodHeader.toMethodVisitor(getDelegate()); visitedMethods.add(methodHeader); - boolean isStatic = (methodHeader.access() & ACC_STATIC) != 0; - boolean isConstructor = methodHeader.name().equals(""); - // calculate length of locals array, including "this" if applicable - int nextLocalsIndex = (Type.getArgumentsAndReturnSizes(methodHeader.descriptor()) >> 2) - (isStatic ? 1 : 0); - int submissionExecutionHandlerIndex = nextLocalsIndex++; - int methodHeaderIndex = nextLocalsIndex++; - int methodSubstitutionIndex = nextLocalsIndex++; - int constructorBehaviourIndex = nextLocalsIndex; - - // calculate default locals for frames - List fullFrameLocals = Arrays.stream(Type.getArgumentTypes(methodHeader.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) { - fullFrameLocals.addFirst(isConstructor ? UNINITIALIZED_THIS : className); + // if method is abstract or lambda, skip transformation + if ((methodHeader.access() & ACC_ABSTRACT) != 0 || + ((methodHeader.access() & ACC_SYNTHETIC) != 0 && methodHeader.name().startsWith("lambda$"))) { + return methodVisitor; + } else { + return new SubmissionMethodVisitor(methodVisitor, transformationContext, submissionClassInfo, methodHeader); } - - return new MethodVisitor(ASM9, methodHeader.toMethodVisitor(getDelegate())) { - @Override - public void visitCode() { - // if method is abstract or lambda, skip transformation - if ((methodHeader.access() & ACC_ABSTRACT) != 0 || - ((methodHeader.access() & ACC_SYNTHETIC) != 0 && methodHeader.name().startsWith("lambda$"))) { - super.visitCode(); - return; - } - - Label submissionExecutionHandlerVarLabel = new Label(); - Label methodHeaderVarLabel = new Label(); - Label substitutionCheckLabel = new Label(); - Label substitutionStartLabel = new Label(); - Label substitutionEndLabel = new Label(); - Label delegationCheckLabel = new Label(); - Label delegationCodeLabel = new Label(); - Label submissionCodeLabel = new Label(); - - // Setup - { - // create SubmissionExecutionHandler$Internal instance and store in locals array - super.visitTypeInsn(NEW, sehInternalType.getInternalName()); - super.visitInsn(DUP); - super.visitMethodInsn(INVOKESTATIC, - submissionExecutionHandlerType.getInternalName(), - "getInstance", - Type.getMethodDescriptor(submissionExecutionHandlerType), - false); - super.visitMethodInsn(INVOKESPECIAL, - sehInternalType.getInternalName(), - "", - Type.getMethodDescriptor(Type.VOID_TYPE, submissionExecutionHandlerType), - false); - super.visitVarInsn(ASTORE, submissionExecutionHandlerIndex); - super.visitLabel(submissionExecutionHandlerVarLabel); - - // replicate method header in bytecode and store in locals array - buildMethodHeader(getDelegate(), methodHeader); - super.visitVarInsn(ASTORE, methodHeaderIndex); - super.visitLabel(methodHeaderVarLabel); - - super.visitFrame(F_APPEND, - 2, - new Object[] {sehInternalType.getInternalName(), methodHeader.getType().getInternalName()}, - 0, - null); - fullFrameLocals.add(sehInternalType.getInternalName()); - fullFrameLocals.add(methodHeader.getType().getInternalName()); - } - - // Invocation logging - { - // check if invocation should be logged - super.visitVarInsn(ALOAD, submissionExecutionHandlerIndex); - super.visitVarInsn(ALOAD, methodHeaderIndex); - super.visitMethodInsn(INVOKEVIRTUAL, - sehInternalType.getInternalName(), - "logInvocation", - Type.getMethodDescriptor(Type.BOOLEAN_TYPE, methodHeader.getType()), - false); - super.visitJumpInsn(IFEQ, substitutionCheckLabel); // jump to label if logInvocation(...) == false - - // intercept parameters - super.visitVarInsn(ALOAD, submissionExecutionHandlerIndex); - super.visitVarInsn(ALOAD, methodHeaderIndex); - buildInvocation(Type.getArgumentTypes(methodHeader.descriptor())); - super.visitMethodInsn(INVOKEVIRTUAL, - sehInternalType.getInternalName(), - "addInvocation", - Type.getMethodDescriptor(Type.VOID_TYPE, methodHeader.getType(), invocationType), - false); - } - - // Method substitution - { - // check if substitution exists for this method - super.visitFrame(F_SAME, 0, null, 0, null); - super.visitLabel(substitutionCheckLabel); - super.visitVarInsn(ALOAD, submissionExecutionHandlerIndex); - super.visitVarInsn(ALOAD, methodHeaderIndex); - super.visitMethodInsn(INVOKEVIRTUAL, - sehInternalType.getInternalName(), - "useSubstitution", - Type.getMethodDescriptor(Type.BOOLEAN_TYPE, methodHeader.getType()), - false); - super.visitJumpInsn(IFEQ, defaultTransformationsOnly ? submissionCodeLabel : delegationCheckLabel); // jump to label if useSubstitution(...) == false - - // get substitution and execute it - super.visitVarInsn(ALOAD, submissionExecutionHandlerIndex); - super.visitVarInsn(ALOAD, methodHeaderIndex); - super.visitMethodInsn(INVOKEVIRTUAL, - sehInternalType.getInternalName(), - "getSubstitution", - Type.getMethodDescriptor(methodSubstitutionType, methodHeader.getType()), - false); - super.visitVarInsn(ASTORE, methodSubstitutionIndex); - super.visitFrame(F_APPEND, 1, new Object[] {methodSubstitutionType.getInternalName()}, 0, null); - fullFrameLocals.add(methodSubstitutionType.getInternalName()); - super.visitLabel(substitutionStartLabel); - - if (isConstructor) { - List superConstructors = submissionClassInfo.getOriginalSuperClassConstructorHeaders() - .stream() - .map(mh -> submissionClassInfo.getComputedSuperClassConstructorHeader(mh.descriptor())) - .toList(); - List constructors = submissionClassInfo.getOriginalMethodHeaders() - .stream() - .filter(mh -> mh.name().equals("")) - .map(mh -> submissionClassInfo.getComputedMethodHeader(mh.name(), mh.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.ConstructorBehaviour cb = methodSubstitution.getConstructorBehaviour(); - * 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(...); - * } - */ - super.visitVarInsn(ALOAD, methodSubstitutionIndex); - super.visitMethodInsn(INVOKEINTERFACE, - methodSubstitutionType.getInternalName(), - "getConstructorBehaviour", - Type.getMethodDescriptor(constructorBehaviourType), - true); - super.visitVarInsn(ASTORE, constructorBehaviourIndex); - super.visitFrame(F_APPEND, 1, new Object[] {constructorBehaviourType.getInternalName()}, 0, null); - fullFrameLocals.add(constructorBehaviourType.getInternalName()); - super.visitLabel(labels[0]); - for (MethodHeader superConstructorHeader : superConstructors) { - buildConstructorInvocationBranch(superConstructorHeader, substitutionExecuteLabel, labels, labelIndex); - } - for (MethodHeader constructorHeader : constructors) { - buildConstructorInvocationBranch(constructorHeader, substitutionExecuteLabel, labels, labelIndex); - } - - // if no matching constructor was found, throw an IllegalArgumentException - { - Type illegalArgumentExceptionType = Type.getType(IllegalArgumentException.class); - super.visitTypeInsn(NEW, illegalArgumentExceptionType.getInternalName()); - super.visitInsn(DUP); - - super.visitLdcInsn("No matching constructor was found for owner %s and descriptor %s"); - super.visitInsn(ICONST_2); - super.visitTypeInsn(ANEWARRAY, stringType.getInternalName()); - super.visitInsn(DUP); - super.visitInsn(ICONST_0); - super.visitVarInsn(ALOAD, constructorBehaviourIndex); - super.visitMethodInsn(INVOKEVIRTUAL, - constructorBehaviourType.getInternalName(), - "owner", - Type.getMethodDescriptor(stringType), - false); - super.visitInsn(AASTORE); - super.visitInsn(DUP); - super.visitInsn(ICONST_1); - super.visitVarInsn(ALOAD, constructorBehaviourIndex); - super.visitMethodInsn(INVOKEVIRTUAL, - constructorBehaviourType.getInternalName(), - "descriptor", - Type.getMethodDescriptor(stringType), - false); - super.visitInsn(AASTORE); - super.visitMethodInsn(INVOKEVIRTUAL, - stringType.getInternalName(), - "formatted", - Type.getMethodDescriptor(stringType, objectArrayType), - false); - - super.visitMethodInsn(INVOKESPECIAL, - illegalArgumentExceptionType.getInternalName(), - "", - Type.getMethodDescriptor(Type.VOID_TYPE, stringType), - false); - super.visitInsn(ATHROW); - } - - fullFrameLocals.removeLast(); - List locals = new ArrayList<>(fullFrameLocals); - locals.set(0, className); - super.visitFrame(F_FULL, locals.size(), locals.toArray(), 0, null); - super.visitLabel(substitutionExecuteLabel); - super.visitLocalVariable("constructorBehaviour", - constructorBehaviourType.getDescriptor(), - null, - labels[labelIndex.get()], - substitutionExecuteLabel, - constructorBehaviourIndex); - } - - super.visitVarInsn(ALOAD, methodSubstitutionIndex); - buildInvocation(Type.getArgumentTypes(methodHeader.descriptor())); - super.visitMethodInsn(INVOKEINTERFACE, - methodSubstitutionType.getInternalName(), - "execute", - Type.getMethodDescriptor(objectType, invocationType), - true); - Type returnType = Type.getReturnType(methodHeader.descriptor()); - if (returnType.getSort() == Type.ARRAY || returnType.getSort() == Type.OBJECT) { - super.visitTypeInsn(CHECKCAST, returnType.getInternalName()); - } else { - unboxType(getDelegate(), returnType); - } - super.visitInsn(returnType.getOpcode(IRETURN)); - super.visitLabel(substitutionEndLabel); - super.visitLocalVariable("methodSubstitution", - methodSubstitutionType.getDescriptor(), - null, - substitutionStartLabel, - substitutionEndLabel, - methodSubstitutionIndex); - } - - // Method delegation - // if only default transformations are applied, skip delegation - if (!defaultTransformationsOnly) { - // check if call should be delegated to solution or not - fullFrameLocals.removeLast(); - super.visitFrame(F_FULL, fullFrameLocals.size(), fullFrameLocals.toArray(), 0, new Object[0]); - super.visitLabel(delegationCheckLabel); - super.visitVarInsn(ALOAD, submissionExecutionHandlerIndex); - super.visitVarInsn(ALOAD, methodHeaderIndex); - super.visitMethodInsn(INVOKEVIRTUAL, - sehInternalType.getInternalName(), - "useSubmissionImpl", - Type.getMethodDescriptor(Type.BOOLEAN_TYPE, methodHeader.getType()), - false); - super.visitJumpInsn(IFNE, submissionCodeLabel); // jump to label if useSubmissionImpl(...) == true - - // replay instructions from solution - super.visitFrame(F_CHOP, 2, null, 0, null); - fullFrameLocals.removeLast(); - fullFrameLocals.removeLast(); - super.visitLabel(delegationCodeLabel); - super.visitLocalVariable("submissionExecutionHandler", - sehInternalType.getDescriptor(), - null, - submissionExecutionHandlerVarLabel, - delegationCodeLabel, - submissionExecutionHandlerIndex); - super.visitLocalVariable("methodHeader", - methodHeader.getType().getDescriptor(), - null, - methodHeaderVarLabel, - delegationCodeLabel, - methodHeaderIndex); - solutionMethodNodes.get(methodHeader).accept(getDelegate()); - - super.visitFrame(F_FULL, fullFrameLocals.size(), fullFrameLocals.toArray(), 0, new Object[0]); - super.visitLabel(submissionCodeLabel); - } else { - fullFrameLocals.removeLast(); - fullFrameLocals.removeLast(); - fullFrameLocals.removeLast(); - super.visitFrame(F_FULL, fullFrameLocals.size(), fullFrameLocals.toArray(), 0, new Object[0]); - super.visitLabel(submissionCodeLabel); - super.visitLocalVariable("submissionExecutionHandler", - sehInternalType.getDescriptor(), - null, - submissionExecutionHandlerVarLabel, - submissionCodeLabel, - submissionExecutionHandlerIndex); - super.visitLocalVariable("methodHeader", - methodHeader.getType().getDescriptor(), - null, - methodHeaderVarLabel, - submissionCodeLabel, - methodHeaderIndex); - } - - // visit original code - super.visitCode(); - } - - private void buildConstructorInvocationBranch(MethodHeader constructorHeader, - Label substitutionExecuteLabel, - Label[] labels, - AtomicInteger labelIndex) { - super.visitVarInsn(ALOAD, constructorBehaviourIndex); - super.visitMethodInsn(INVOKEVIRTUAL, - constructorBehaviourType.getInternalName(), - "owner", - Type.getMethodDescriptor(stringType), - false); - super.visitLdcInsn(constructorHeader.owner()); - super.visitMethodInsn(INVOKEVIRTUAL, - stringType.getInternalName(), - "equals", - Type.getMethodDescriptor(Type.BOOLEAN_TYPE, objectType), - false); - - super.visitVarInsn(ALOAD, constructorBehaviourIndex); - super.visitMethodInsn(INVOKEVIRTUAL, - constructorBehaviourType.getInternalName(), - "descriptor", - Type.getMethodDescriptor(stringType), - false); - super.visitLdcInsn(constructorHeader.descriptor()); - super.visitMethodInsn(INVOKEVIRTUAL, - stringType.getInternalName(), - "equals", - Type.getMethodDescriptor(Type.BOOLEAN_TYPE, objectType), - false); - - super.visitInsn(IAND); - super.visitJumpInsn(IFEQ, labels[labelIndex.get() + 1]); // jump to next branch if false - - Label argsVarStartLabel = new Label(); - super.visitVarInsn(ALOAD, constructorBehaviourIndex); - super.visitMethodInsn(INVOKEVIRTUAL, - constructorBehaviourType.getInternalName(), - "args", - Type.getMethodDescriptor(objectArrayType), - false); - super.visitVarInsn(ASTORE, constructorBehaviourIndex + 1); - super.visitFrame(F_APPEND, 1, new Object[] {objectArrayType.getInternalName()}, 0, null); - fullFrameLocals.add(objectArrayType.getInternalName()); - super.visitLabel(argsVarStartLabel); - - super.visitVarInsn(ALOAD, 0); - Type[] parameterTypes = Type.getArgumentTypes(constructorHeader.descriptor()); - for (int i = 0; i < parameterTypes.length; i++) { // unpack array - super.visitVarInsn(ALOAD, constructorBehaviourIndex + 1); - super.visitIntInsn(SIPUSH, i); - super.visitInsn(AALOAD); - unboxType(getDelegate(), parameterTypes[i]); - } - super.visitMethodInsn(INVOKESPECIAL, - constructorHeader.owner(), - "", - constructorHeader.descriptor(), - false); - super.visitJumpInsn(GOTO, substitutionExecuteLabel); - - fullFrameLocals.removeLast(); -// List locals = new ArrayList<>(fullFrameLocals); -// locals.set(0, className); - super.visitFrame(F_CHOP, 1, null, 0, new Object[0]); - super.visitLabel(labels[labelIndex.incrementAndGet()]); - super.visitLocalVariable("args", - objectArrayType.getDescriptor(), - null, - argsVarStartLabel, - labels[labelIndex.get()], - constructorBehaviourIndex + 1); - } - - @Override - public void visitFieldInsn(int opcode, String owner, String name, String descriptor) { - // skip transformation if only default transformations are applied or owner is not part of the submission - if (defaultTransformationsOnly || !owner.startsWith(transformationContext.getProjectPrefix())) { - super.visitFieldInsn(opcode, owner, name, descriptor); - } else { - FieldHeader fieldHeader = transformationContext.getSubmissionClassInfo(owner).getComputedFieldHeader(name); - super.visitFieldInsn(opcode, fieldHeader.owner(), fieldHeader.name(), fieldHeader.descriptor()); - } - } - - @Override - public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) { - // skip transformation if only default transformations are applied or owner is not part of the submission - MethodHeader methodHeader = new MethodHeader(owner, 0, name, descriptor, null, null); - if (transformationContext.methodHasReplacement(methodHeader)) { - MethodHeader replacementMethodHeader = transformationContext.getMethodReplacement(methodHeader); - super.visitMethodInsn(INVOKESTATIC, - replacementMethodHeader.owner(), - replacementMethodHeader.name(), - replacementMethodHeader.descriptor(), - false); - } else if (defaultTransformationsOnly || !owner.startsWith(transformationContext.getProjectPrefix())) { - super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); - } else { - methodHeader = transformationContext.getSubmissionClassInfo(owner).getComputedMethodHeader(name, descriptor); - super.visitMethodInsn(opcode, methodHeader.owner(), methodHeader.name(), methodHeader.descriptor(), isInterface); - } - } - - /** - * Builds an {@link Invocation} in bytecode. - * - * @param argumentTypes an array of parameter types - */ - private void buildInvocation(Type[] argumentTypes) { - Type threadType = Type.getType(Thread.class); - Type stackTraceElementArrayType = Type.getType(StackTraceElement[].class); - - super.visitTypeInsn(NEW, invocationType.getInternalName()); - super.visitInsn(DUP); - if (!isStatic && !isConstructor) { - super.visitVarInsn(ALOAD, 0); - super.visitMethodInsn(INVOKESTATIC, - threadType.getInternalName(), - "currentThread", - Type.getMethodDescriptor(threadType), - false); - super.visitMethodInsn(INVOKEVIRTUAL, - threadType.getInternalName(), - "getStackTrace", - Type.getMethodDescriptor(stackTraceElementArrayType), - false); - super.visitMethodInsn(INVOKESPECIAL, - invocationType.getInternalName(), - "", - Type.getMethodDescriptor(Type.VOID_TYPE, objectType, stackTraceElementArrayType), - false); - } else { - super.visitMethodInsn(INVOKESTATIC, - threadType.getInternalName(), - "currentThread", - Type.getMethodDescriptor(threadType), - false); - super.visitMethodInsn(INVOKEVIRTUAL, - threadType.getInternalName(), - "getStackTrace", - Type.getMethodDescriptor(stackTraceElementArrayType), - false); - super.visitMethodInsn(INVOKESPECIAL, - invocationType.getInternalName(), - "", - Type.getMethodDescriptor(Type.VOID_TYPE, stackTraceElementArrayType), - false); - } - // load parameter with opcode (ALOAD, ILOAD, etc.) for type and ignore "this", if it exists - for (int i = 0; i < argumentTypes.length; i++) { - super.visitInsn(DUP); - super.visitVarInsn(argumentTypes[i].getOpcode(ILOAD), getLocalsIndex(argumentTypes, i) + (isStatic ? 0 : 1)); - boxType(getDelegate(), argumentTypes[i]); - super.visitMethodInsn(INVOKEVIRTUAL, - invocationType.getInternalName(), - "addParameter", - Type.getMethodDescriptor(Type.VOID_TYPE, objectType), - false); - } - } - }; } /** diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionExecutionHandler.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionExecutionHandler.java index af050043..b54e27c5 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionExecutionHandler.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionExecutionHandler.java @@ -1,7 +1,6 @@ package org.tudalgo.algoutils.transform; import org.tudalgo.algoutils.transform.util.*; -import org.objectweb.asm.Type; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; @@ -67,8 +66,6 @@ @SuppressWarnings("unused") public class SubmissionExecutionHandler { - public static final Type INTERNAL_TYPE = Type.getType(SubmissionExecutionHandler.class); - private static SubmissionExecutionHandler instance; // declaring class => (method header => invocations) @@ -262,8 +259,6 @@ public void disableMethodDelegation(MethodHeader methodHeader) { */ public final class Internal { - public static final Type INTERNAL_TYPE = Type.getType(Internal.class); - // Invocation logging /** diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionMethodVisitor.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionMethodVisitor.java new file mode 100644 index 00000000..99759db4 --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionMethodVisitor.java @@ -0,0 +1,523 @@ +package org.tudalgo.algoutils.transform; + +import org.objectweb.asm.Label; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Type; +import org.tudalgo.algoutils.transform.util.*; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +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.objectweb.asm.Opcodes.INVOKEVIRTUAL; +import static org.tudalgo.algoutils.transform.util.TransformationUtils.*; +import static org.tudalgo.algoutils.transform.util.TransformationUtils.boxType; + +/** + * A method visitor for transforming submission methods. + * + * @see SubmissionClassVisitor + * @author Daniel Mangold + */ +class SubmissionMethodVisitor extends MethodVisitor { + + private final TransformationContext transformationContext; + private final MethodHeader methodHeader; + private final SubmissionClassInfo submissionClassInfo; + private final String className; + private final boolean defaultTransformationsOnly; + + private final boolean isStatic; + private final boolean isConstructor; + + private final int submissionExecutionHandlerIndex; + private final int methodHeaderIndex; + private final int methodSubstitutionIndex; + private final int constructorInvocationIndex; + + private final List fullFrameLocals; + + /** + * 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 methodHeader the computed method header of this method + */ + SubmissionMethodVisitor(MethodVisitor delegate, + TransformationContext transformationContext, + SubmissionClassInfo submissionClassInfo, + MethodHeader methodHeader) { + super(ASM9, delegate); + this.transformationContext = transformationContext; + this.submissionClassInfo = submissionClassInfo; + this.methodHeader = methodHeader; + this.className = submissionClassInfo.getComputedClassName(); + this.defaultTransformationsOnly = submissionClassInfo.getSolutionClass().isEmpty(); + + this.isStatic = (methodHeader.access() & ACC_STATIC) != 0; + this.isConstructor = methodHeader.name().equals(""); + + // calculate length of locals array, including "this" if applicable + int nextLocalsIndex = (Type.getArgumentsAndReturnSizes(methodHeader.descriptor()) >> 2) - (isStatic ? 1 : 0); + this.submissionExecutionHandlerIndex = nextLocalsIndex; + this.methodHeaderIndex = nextLocalsIndex + 1; + this.methodSubstitutionIndex = nextLocalsIndex + 2; + this.constructorInvocationIndex = nextLocalsIndex + 3; + + this.fullFrameLocals = Arrays.stream(Type.getArgumentTypes(methodHeader.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 : className); + } + } + + @Override + public void visitCode() { + Label submissionExecutionHandlerVarLabel = new Label(); + Label methodHeaderVarLabel = new Label(); + Label substitutionCheckLabel = new Label(); + Label substitutionStartLabel = new Label(); + Label substitutionEndLabel = new Label(); + Label delegationCheckLabel = new Label(); + Label delegationCodeLabel = new Label(); + Label submissionCodeLabel = new Label(); + + // Setup + { + // create SubmissionExecutionHandler$Internal instance and store in locals array + super.visitTypeInsn(NEW, Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_TYPE.getInternalName()); + super.visitInsn(DUP); + super.visitMethodInsn(INVOKESTATIC, + Constants.SUBMISSION_EXECUTION_HANDLER_TYPE.getInternalName(), + "getInstance", + Type.getMethodDescriptor(Constants.SUBMISSION_EXECUTION_HANDLER_TYPE), + false); + super.visitMethodInsn(INVOKESPECIAL, + Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_TYPE.getInternalName(), + "", + Type.getMethodDescriptor(Type.VOID_TYPE, Constants.SUBMISSION_EXECUTION_HANDLER_TYPE), + false); + super.visitVarInsn(ASTORE, submissionExecutionHandlerIndex); + super.visitLabel(submissionExecutionHandlerVarLabel); + + // replicate method header in bytecode and store in locals array + buildMethodHeader(getDelegate(), methodHeader); + super.visitVarInsn(ASTORE, methodHeaderIndex); + super.visitLabel(methodHeaderVarLabel); + + super.visitFrame(F_APPEND, + 2, + new Object[] {Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_TYPE.getInternalName(), methodHeader.getType().getInternalName()}, + 0, + null); + fullFrameLocals.add(Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_TYPE.getInternalName()); + fullFrameLocals.add(methodHeader.getType().getInternalName()); + } + + // Invocation logging + { + // check if invocation should be logged + super.visitVarInsn(ALOAD, submissionExecutionHandlerIndex); + super.visitVarInsn(ALOAD, methodHeaderIndex); + super.visitMethodInsn(INVOKEVIRTUAL, + Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_TYPE.getInternalName(), + "logInvocation", + Type.getMethodDescriptor(Type.BOOLEAN_TYPE, methodHeader.getType()), + false); + super.visitJumpInsn(IFEQ, substitutionCheckLabel); // jump to label if logInvocation(...) == false + + // intercept parameters + super.visitVarInsn(ALOAD, submissionExecutionHandlerIndex); + super.visitVarInsn(ALOAD, methodHeaderIndex); + buildInvocation(Type.getArgumentTypes(methodHeader.descriptor())); + super.visitMethodInsn(INVOKEVIRTUAL, + Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_TYPE.getInternalName(), + "addInvocation", + Type.getMethodDescriptor(Type.VOID_TYPE, methodHeader.getType(), Constants.INVOCATION_TYPE), + false); + } + + // Method substitution + { + // check if substitution exists for this method + super.visitFrame(F_SAME, 0, null, 0, null); + super.visitLabel(substitutionCheckLabel); + super.visitVarInsn(ALOAD, submissionExecutionHandlerIndex); + super.visitVarInsn(ALOAD, methodHeaderIndex); + super.visitMethodInsn(INVOKEVIRTUAL, + Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_TYPE.getInternalName(), + "useSubstitution", + Type.getMethodDescriptor(Type.BOOLEAN_TYPE, methodHeader.getType()), + false); + super.visitJumpInsn(IFEQ, defaultTransformationsOnly ? submissionCodeLabel : delegationCheckLabel); // jump to label if useSubstitution(...) == false + + // get substitution and execute it + super.visitVarInsn(ALOAD, submissionExecutionHandlerIndex); + super.visitVarInsn(ALOAD, methodHeaderIndex); + super.visitMethodInsn(INVOKEVIRTUAL, + Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_TYPE.getInternalName(), + "getSubstitution", + Type.getMethodDescriptor(Constants.METHOD_SUBSTITUTION_TYPE, methodHeader.getType()), + false); + super.visitVarInsn(ASTORE, methodSubstitutionIndex); + super.visitFrame(F_APPEND, 1, new Object[] {Constants.METHOD_SUBSTITUTION_TYPE.getInternalName()}, 0, null); + fullFrameLocals.add(Constants.METHOD_SUBSTITUTION_TYPE.getInternalName()); + super.visitLabel(substitutionStartLabel); + + if (isConstructor) { + List superConstructors = submissionClassInfo.getOriginalSuperClassConstructorHeaders() + .stream() + .map(mh -> submissionClassInfo.getComputedSuperClassConstructorHeader(mh.descriptor())) + .toList(); + List constructors = submissionClassInfo.getOriginalMethodHeaders() + .stream() + .filter(mh -> mh.name().equals("")) + .map(mh -> submissionClassInfo.getComputedMethodHeader(mh.name(), mh.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(...); + * } + */ + super.visitVarInsn(ALOAD, methodSubstitutionIndex); + super.visitMethodInsn(INVOKEINTERFACE, + Constants.METHOD_SUBSTITUTION_TYPE.getInternalName(), + "getConstructorInvocation", + Type.getMethodDescriptor(Constants.METHOD_SUBSTITUTION_CONSTRUCTOR_INVOCATION_TYPE), + true); + super.visitVarInsn(ASTORE, constructorInvocationIndex); + super.visitFrame(F_APPEND, 1, new Object[] {Constants.METHOD_SUBSTITUTION_CONSTRUCTOR_INVOCATION_TYPE.getInternalName()}, 0, null); + fullFrameLocals.add(Constants.METHOD_SUBSTITUTION_CONSTRUCTOR_INVOCATION_TYPE.getInternalName()); + super.visitLabel(labels[0]); + for (MethodHeader superConstructorHeader : superConstructors) { + buildConstructorInvocationBranch(superConstructorHeader, substitutionExecuteLabel, labels, labelIndex); + } + for (MethodHeader constructorHeader : constructors) { + buildConstructorInvocationBranch(constructorHeader, substitutionExecuteLabel, labels, labelIndex); + } + + // if no matching constructor was found, throw an IllegalArgumentException + { + Type illegalArgumentExceptionType = Type.getType(IllegalArgumentException.class); + super.visitTypeInsn(NEW, illegalArgumentExceptionType.getInternalName()); + super.visitInsn(DUP); + + super.visitLdcInsn("No matching constructor was found for owner %s and descriptor %s"); + super.visitInsn(ICONST_2); + super.visitTypeInsn(ANEWARRAY, Constants.STRING_TYPE.getInternalName()); + super.visitInsn(DUP); + super.visitInsn(ICONST_0); + super.visitVarInsn(ALOAD, constructorInvocationIndex); + super.visitMethodInsn(INVOKEVIRTUAL, + Constants.METHOD_SUBSTITUTION_CONSTRUCTOR_INVOCATION_TYPE.getInternalName(), + "owner", + Type.getMethodDescriptor(Constants.STRING_TYPE), + false); + super.visitInsn(AASTORE); + super.visitInsn(DUP); + super.visitInsn(ICONST_1); + super.visitVarInsn(ALOAD, constructorInvocationIndex); + super.visitMethodInsn(INVOKEVIRTUAL, + Constants.METHOD_SUBSTITUTION_CONSTRUCTOR_INVOCATION_TYPE.getInternalName(), + "descriptor", + Type.getMethodDescriptor(Constants.STRING_TYPE), + false); + super.visitInsn(AASTORE); + super.visitMethodInsn(INVOKEVIRTUAL, + Constants.STRING_TYPE.getInternalName(), + "formatted", + Type.getMethodDescriptor(Constants.STRING_TYPE, Constants.OBJECT_ARRAY_TYPE), + false); + + super.visitMethodInsn(INVOKESPECIAL, + illegalArgumentExceptionType.getInternalName(), + "", + Type.getMethodDescriptor(Type.VOID_TYPE, Constants.STRING_TYPE), + false); + super.visitInsn(ATHROW); + } + + fullFrameLocals.removeLast(); + List locals = new ArrayList<>(fullFrameLocals); + locals.set(0, className); + super.visitFrame(F_FULL, locals.size(), locals.toArray(), 0, null); + super.visitLabel(substitutionExecuteLabel); + super.visitLocalVariable("constructorInvocation", + Constants.METHOD_SUBSTITUTION_CONSTRUCTOR_INVOCATION_TYPE.getDescriptor(), + null, + labels[labelIndex.get()], + substitutionExecuteLabel, + constructorInvocationIndex); + } + + super.visitVarInsn(ALOAD, methodSubstitutionIndex); + buildInvocation(Type.getArgumentTypes(methodHeader.descriptor())); + super.visitMethodInsn(INVOKEINTERFACE, + Constants.METHOD_SUBSTITUTION_TYPE.getInternalName(), + "execute", + Type.getMethodDescriptor(Constants.OBJECT_TYPE, Constants.INVOCATION_TYPE), + true); + Type returnType = Type.getReturnType(methodHeader.descriptor()); + if (returnType.getSort() == Type.ARRAY || returnType.getSort() == Type.OBJECT) { + super.visitTypeInsn(CHECKCAST, returnType.getInternalName()); + } else { + unboxType(getDelegate(), returnType); + } + super.visitInsn(returnType.getOpcode(IRETURN)); + super.visitLabel(substitutionEndLabel); + super.visitLocalVariable("methodSubstitution", + Constants.METHOD_SUBSTITUTION_TYPE.getDescriptor(), + null, + substitutionStartLabel, + substitutionEndLabel, + methodSubstitutionIndex); + } + + // Method delegation + // if only default transformations are applied, skip delegation + if (!defaultTransformationsOnly) { + // check if call should be delegated to solution or not + fullFrameLocals.removeLast(); + super.visitFrame(F_FULL, fullFrameLocals.size(), fullFrameLocals.toArray(), 0, new Object[0]); + super.visitLabel(delegationCheckLabel); + super.visitVarInsn(ALOAD, submissionExecutionHandlerIndex); + super.visitVarInsn(ALOAD, methodHeaderIndex); + super.visitMethodInsn(INVOKEVIRTUAL, + Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_TYPE.getInternalName(), + "useSubmissionImpl", + Type.getMethodDescriptor(Type.BOOLEAN_TYPE, methodHeader.getType()), + false); + super.visitJumpInsn(IFNE, submissionCodeLabel); // jump to label if useSubmissionImpl(...) == true + + // replay instructions from solution + super.visitFrame(F_CHOP, 2, null, 0, null); + fullFrameLocals.removeLast(); + fullFrameLocals.removeLast(); + super.visitLabel(delegationCodeLabel); + super.visitLocalVariable("submissionExecutionHandler", + Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_TYPE.getDescriptor(), + null, + submissionExecutionHandlerVarLabel, + delegationCodeLabel, + submissionExecutionHandlerIndex); + super.visitLocalVariable("methodHeader", + methodHeader.getType().getDescriptor(), + null, + methodHeaderVarLabel, + delegationCodeLabel, + methodHeaderIndex); + //noinspection OptionalGetWithoutIsPresent + submissionClassInfo.getSolutionClass().get().getMethods().get(methodHeader).accept(getDelegate()); + + super.visitFrame(F_FULL, fullFrameLocals.size(), fullFrameLocals.toArray(), 0, new Object[0]); + super.visitLabel(submissionCodeLabel); + } else { + fullFrameLocals.removeLast(); + fullFrameLocals.removeLast(); + fullFrameLocals.removeLast(); + super.visitFrame(F_FULL, fullFrameLocals.size(), fullFrameLocals.toArray(), 0, new Object[0]); + super.visitLabel(submissionCodeLabel); + super.visitLocalVariable("submissionExecutionHandler", + Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_TYPE.getDescriptor(), + null, + submissionExecutionHandlerVarLabel, + submissionCodeLabel, + submissionExecutionHandlerIndex); + super.visitLocalVariable("methodHeader", + methodHeader.getType().getDescriptor(), + null, + methodHeaderVarLabel, + submissionCodeLabel, + methodHeaderIndex); + } + + // visit original code + super.visitCode(); + } + + @Override + public void visitFieldInsn(int opcode, String owner, String name, String descriptor) { + // skip transformation if only default transformations are applied or owner is not part of the submission + if (defaultTransformationsOnly || !owner.startsWith(transformationContext.getProjectPrefix())) { + super.visitFieldInsn(opcode, owner, name, descriptor); + } else { + FieldHeader fieldHeader = transformationContext.getSubmissionClassInfo(owner).getComputedFieldHeader(name); + super.visitFieldInsn(opcode, fieldHeader.owner(), fieldHeader.name(), fieldHeader.descriptor()); + } + } + + @Override + public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) { + // skip transformation if only default transformations are applied or owner is not part of the submission + MethodHeader methodHeader = new MethodHeader(owner, 0, name, descriptor, null, null); + if (transformationContext.methodHasReplacement(methodHeader)) { + MethodHeader replacementMethodHeader = transformationContext.getMethodReplacement(methodHeader); + super.visitMethodInsn(INVOKESTATIC, + replacementMethodHeader.owner(), + replacementMethodHeader.name(), + replacementMethodHeader.descriptor(), + false); + } else if (defaultTransformationsOnly || !owner.startsWith(transformationContext.getProjectPrefix())) { + super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); + } else { + methodHeader = transformationContext.getSubmissionClassInfo(owner).getComputedMethodHeader(name, descriptor); + super.visitMethodInsn(opcode, methodHeader.owner(), methodHeader.name(), methodHeader.descriptor(), isInterface); + } + } + + /** + * Builds an {@link Invocation} in bytecode. + * + * @param argumentTypes an array of parameter types + */ + private void buildInvocation(Type[] argumentTypes) { + Type threadType = Type.getType(Thread.class); + Type stackTraceElementArrayType = Type.getType(StackTraceElement[].class); + + super.visitTypeInsn(NEW, Constants.INVOCATION_TYPE.getInternalName()); + super.visitInsn(DUP); + if (!isStatic && !isConstructor) { + super.visitVarInsn(ALOAD, 0); + super.visitMethodInsn(INVOKESTATIC, + threadType.getInternalName(), + "currentThread", + Type.getMethodDescriptor(threadType), + false); + super.visitMethodInsn(INVOKEVIRTUAL, + threadType.getInternalName(), + "getStackTrace", + Type.getMethodDescriptor(stackTraceElementArrayType), + false); + super.visitMethodInsn(INVOKESPECIAL, + Constants.INVOCATION_TYPE.getInternalName(), + "", + Type.getMethodDescriptor(Type.VOID_TYPE, Constants.OBJECT_TYPE, stackTraceElementArrayType), + false); + } else { + super.visitMethodInsn(INVOKESTATIC, + threadType.getInternalName(), + "currentThread", + Type.getMethodDescriptor(threadType), + false); + super.visitMethodInsn(INVOKEVIRTUAL, + threadType.getInternalName(), + "getStackTrace", + Type.getMethodDescriptor(stackTraceElementArrayType), + false); + super.visitMethodInsn(INVOKESPECIAL, + Constants.INVOCATION_TYPE.getInternalName(), + "", + Type.getMethodDescriptor(Type.VOID_TYPE, stackTraceElementArrayType), + false); + } + // load parameter with opcode (ALOAD, ILOAD, etc.) for type and ignore "this", if it exists + for (int i = 0; i < argumentTypes.length; i++) { + super.visitInsn(DUP); + super.visitVarInsn(argumentTypes[i].getOpcode(ILOAD), getLocalsIndex(argumentTypes, i) + (isStatic ? 0 : 1)); + boxType(getDelegate(), argumentTypes[i]); + super.visitMethodInsn(INVOKEVIRTUAL, + Constants.INVOCATION_TYPE.getInternalName(), + "addParameter", + Type.getMethodDescriptor(Type.VOID_TYPE, Constants.OBJECT_TYPE), + false); + } + } + + private void buildConstructorInvocationBranch(MethodHeader constructorHeader, + Label substitutionExecuteLabel, + Label[] labels, + AtomicInteger labelIndex) { + super.visitVarInsn(ALOAD, constructorInvocationIndex); + super.visitMethodInsn(INVOKEVIRTUAL, + Constants.METHOD_SUBSTITUTION_CONSTRUCTOR_INVOCATION_TYPE.getInternalName(), + "owner", + Type.getMethodDescriptor(Constants.STRING_TYPE), + false); + super.visitLdcInsn(constructorHeader.owner()); + super.visitMethodInsn(INVOKEVIRTUAL, + Constants.STRING_TYPE.getInternalName(), + "equals", + Type.getMethodDescriptor(Type.BOOLEAN_TYPE, Constants.OBJECT_TYPE), + false); + + super.visitVarInsn(ALOAD, constructorInvocationIndex); + super.visitMethodInsn(INVOKEVIRTUAL, + Constants.METHOD_SUBSTITUTION_CONSTRUCTOR_INVOCATION_TYPE.getInternalName(), + "descriptor", + Type.getMethodDescriptor(Constants.STRING_TYPE), + false); + super.visitLdcInsn(constructorHeader.descriptor()); + super.visitMethodInsn(INVOKEVIRTUAL, + Constants.STRING_TYPE.getInternalName(), + "equals", + Type.getMethodDescriptor(Type.BOOLEAN_TYPE, Constants.OBJECT_TYPE), + false); + + super.visitInsn(IAND); + super.visitJumpInsn(IFEQ, labels[labelIndex.get() + 1]); // jump to next branch if false + + Label argsVarStartLabel = new Label(); + super.visitVarInsn(ALOAD, constructorInvocationIndex); + super.visitMethodInsn(INVOKEVIRTUAL, + Constants.METHOD_SUBSTITUTION_CONSTRUCTOR_INVOCATION_TYPE.getInternalName(), + "args", + Type.getMethodDescriptor(Constants.OBJECT_ARRAY_TYPE), + false); + super.visitVarInsn(ASTORE, constructorInvocationIndex + 1); + super.visitFrame(F_APPEND, 1, new Object[] {Constants.OBJECT_ARRAY_TYPE.getInternalName()}, 0, null); + fullFrameLocals.add(Constants.OBJECT_ARRAY_TYPE.getInternalName()); + super.visitLabel(argsVarStartLabel); + + super.visitVarInsn(ALOAD, 0); + Type[] parameterTypes = Type.getArgumentTypes(constructorHeader.descriptor()); + for (int i = 0; i < parameterTypes.length; i++) { // unpack array + super.visitVarInsn(ALOAD, constructorInvocationIndex + 1); + super.visitIntInsn(SIPUSH, i); + super.visitInsn(AALOAD); + unboxType(getDelegate(), parameterTypes[i]); + } + super.visitMethodInsn(INVOKESPECIAL, + constructorHeader.owner(), + "", + constructorHeader.descriptor(), + false); + super.visitJumpInsn(GOTO, substitutionExecuteLabel); + + fullFrameLocals.removeLast(); + super.visitFrame(F_CHOP, 1, null, 0, new Object[0]); + super.visitLabel(labels[labelIndex.incrementAndGet()]); + super.visitLocalVariable("args", + Constants.OBJECT_ARRAY_TYPE.getDescriptor(), + null, + argsVarStartLabel, + labels[labelIndex.get()], + constructorInvocationIndex + 1); + } +} diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/ClassHeader.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/ClassHeader.java index a25a48bd..219c1f9f 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/ClassHeader.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/ClassHeader.java @@ -19,23 +19,14 @@ */ public record ClassHeader(int access, String name, String signature, String superName, String[] interfaces) implements Header { - private static final Type INTERNAL_TYPE = Type.getType(ClassHeader.class); - private static final Type[] INTERNAL_CONSTRUCTOR_TYPES = new Type[] { - Type.INT_TYPE, - Type.getType(String.class), - Type.getType(String.class), - Type.getType(String.class), - Type.getType(String[].class) - }; - @Override public Type getType() { - return INTERNAL_TYPE; + return Constants.CLASS_HEADER_TYPE; } @Override public Type[] getConstructorParameterTypes() { - return INTERNAL_CONSTRUCTOR_TYPES; + return Constants.CLASS_HEADER_CONSTRUCTOR_TYPES; } @Override 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..4c498399 --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Constants.java @@ -0,0 +1,50 @@ +package org.tudalgo.algoutils.transform.util; + +import org.objectweb.asm.Type; +import org.tudalgo.algoutils.student.annotation.ForceSignature; +import org.tudalgo.algoutils.transform.SubmissionExecutionHandler; + +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 CLASS_HEADER_TYPE = Type.getType(ClassHeader.class); + public static final Type[] CLASS_HEADER_CONSTRUCTOR_TYPES = new Type[] { + Type.INT_TYPE, + STRING_TYPE, + STRING_TYPE, + STRING_TYPE, + STRING_ARRAY_TYPE + }; + public static final Type FIELD_HEADER_TYPE = Type.getType(FieldHeader.class); + public static final Type[] FIELD_HEADER_CONSTRUCTOR_TYPES = new Type[] { + STRING_TYPE, + Type.INT_TYPE, + STRING_TYPE, + STRING_TYPE, + STRING_TYPE + }; + public static final Type METHOD_HEADER_TYPE = Type.getType(MethodHeader.class); + public static final Type[] METHOD_HEADER_CONSTRUCTOR_TYPES = new Type[] { + STRING_TYPE, + Type.INT_TYPE, + STRING_TYPE, + STRING_TYPE, + STRING_TYPE, + STRING_ARRAY_TYPE + }; + + 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); + public static final Type SUBMISSION_EXECUTION_HANDLER_TYPE = Type.getType(SubmissionExecutionHandler.class); + public static final Type SUBMISSION_EXECUTION_HANDLER_INTERNAL_TYPE = Type.getType(SubmissionExecutionHandler.Internal.class); + + private Constants() {} +} diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/FieldHeader.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/FieldHeader.java index f12dd8ff..8ca06fe2 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/FieldHeader.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/FieldHeader.java @@ -22,15 +22,6 @@ */ public record FieldHeader(String owner, int access, String name, String descriptor, String signature) implements Header { - private static final Type INTERNAL_TYPE = Type.getType(FieldHeader.class); - private static final Type[] INTERNAL_CONSTRUCTOR_TYPES = new Type[] { - Type.getType(String.class), - Type.INT_TYPE, - Type.getType(String.class), - Type.getType(String.class), - Type.getType(String.class) - }; - public FieldHeader(Field field) { this(Type.getInternalName(field.getDeclaringClass()), field.getModifiers(), @@ -41,12 +32,12 @@ public FieldHeader(Field field) { @Override public Type getType() { - return INTERNAL_TYPE; + return Constants.FIELD_HEADER_TYPE; } @Override public Type[] getConstructorParameterTypes() { - return INTERNAL_CONSTRUCTOR_TYPES; + return Constants.FIELD_HEADER_CONSTRUCTOR_TYPES; } @Override 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 index 71675c3f..8cc299a3 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/ForceSignatureAnnotationProcessor.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/ForceSignatureAnnotationProcessor.java @@ -15,8 +15,6 @@ */ public class ForceSignatureAnnotationProcessor { - private static final Type FORCE_SIGNATURE_TYPE = Type.getType(ForceSignature.class); - /** * The forced identifier of the class, if any. */ @@ -130,7 +128,7 @@ private ClassLevelVisitor(String name) { @Override public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { - if (descriptor.equals(FORCE_SIGNATURE_TYPE.getDescriptor())) { + if (descriptor.equals(Constants.FORCE_SIGNATURE_TYPE.getDescriptor())) { return annotationVisitor = new ForceSignatureAnnotationVisitor(); } else { return null; @@ -216,7 +214,7 @@ private FieldLevelVisitor(String owner, int access, String name, String descript @Override public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { - if (descriptor.equals(FORCE_SIGNATURE_TYPE.getDescriptor())) { + if (descriptor.equals(Constants.FORCE_SIGNATURE_TYPE.getDescriptor())) { return annotationVisitor = new ForceSignatureAnnotationVisitor(); } else { return null; @@ -249,7 +247,7 @@ private MethodLevelVisitor(String owner, int access, String name, String descrip @Override public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { - if (descriptor.equals(FORCE_SIGNATURE_TYPE.getDescriptor())) { + if (descriptor.equals(Constants.FORCE_SIGNATURE_TYPE.getDescriptor())) { return annotationVisitor = new ForceSignatureAnnotationVisitor(); } else { return null; 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 index e4f7d285..79817f33 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Invocation.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Invocation.java @@ -1,11 +1,6 @@ package org.tudalgo.algoutils.transform.util; -import org.objectweb.asm.Type; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Objects; +import java.util.*; /** * This class holds information about the context of an invocation. @@ -15,8 +10,6 @@ */ public class Invocation { - public static final Type INTERNAL_TYPE = Type.getType(Invocation.class); - private final Object instance; private final StackTraceElement[] stackTrace; private final List parameterValues = new ArrayList<>(); @@ -185,7 +178,7 @@ public void addParameter(Object value) { @Override public String toString() { - return "Invocation{instance=%s, parameterValues=%s}".formatted(instance, parameterValues); + return "Invocation{instance=%s, stackTrace=%s, parameterValues=%s}".formatted(instance, Arrays.toString(stackTrace), parameterValues); } @Override @@ -193,11 +186,13 @@ 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(parameterValues, that.parameterValues); + return Objects.equals(instance, that.instance) && + Arrays.equals(stackTrace, that.stackTrace) && + Objects.equals(parameterValues, that.parameterValues); } @Override public int hashCode() { - return Objects.hash(parameterValues); + return Objects.hash(instance, Arrays.hashCode(stackTrace), parameterValues); } } diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/MethodHeader.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/MethodHeader.java index b3f5c7dd..0b1b5986 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/MethodHeader.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/MethodHeader.java @@ -26,16 +26,6 @@ */ public record MethodHeader(String owner, int access, String name, String descriptor, String signature, String[] exceptions) implements Header { - private static final Type INTERNAL_TYPE = Type.getType(MethodHeader.class); - private static final Type[] INTERNAL_CONSTRUCTOR_TYPES = new Type[] { - Type.getType(String.class), - Type.INT_TYPE, - Type.getType(String.class), - Type.getType(String.class), - Type.getType(String.class), - Type.getType(String[].class) - }; - /** * Constructs a new method header using the given method / constructor. * @@ -58,12 +48,12 @@ public MethodHeader(Executable executable) { @Override public Type getType() { - return INTERNAL_TYPE; + return Constants.METHOD_HEADER_TYPE; } @Override public Type[] getConstructorParameterTypes() { - return INTERNAL_CONSTRUCTOR_TYPES; + return Constants.METHOD_HEADER_CONSTRUCTOR_TYPES; } @Override 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 index 54e016f3..bd31a8c6 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/MethodSubstitution.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/MethodSubstitution.java @@ -10,13 +10,11 @@ @FunctionalInterface public interface MethodSubstitution { - Type INTERNAL_TYPE = Type.getType(MethodSubstitution.class); - /** * 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 ConstructorBehaviour} object storing... + * 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. @@ -27,8 +25,8 @@ public interface MethodSubstitution { * * @return a record containing the target method's owner and arguments */ - default ConstructorBehaviour getConstructorBehaviour() { - return new ConstructorBehaviour("java/lang/Object", "()V"); + default ConstructorInvocation getConstructorInvocation() { + return new ConstructorInvocation("java/lang/Object", "()V"); } /** @@ -48,7 +46,5 @@ default ConstructorBehaviour getConstructorBehaviour() { * @param descriptor the descriptor of the target constructor * @param args the arguments the constructor will be invoked with */ - record ConstructorBehaviour(String owner, String descriptor, Object... args) { - public static final Type INTERNAL_TYPE = Type.getType(ConstructorBehaviour.class); - } + record ConstructorInvocation(String owner, String descriptor, Object... args) {} } 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 index a56d8aa6..1c9aaf27 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationUtils.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationUtils.java @@ -275,8 +275,8 @@ private static int buildHeader(MethodVisitor mv, Header header, String... keys) maxStack = stackSize = 2; for (int i = 0; i < keys.length; i++) { Object value = header.getValue(keys[i]); - if (constructorParameterTypes[i].equals(Type.getType(String[].class))) { - int stackUsed = buildArray(mv, Type.getType(String.class), (Object[]) value); + if (constructorParameterTypes[i].equals(Constants.STRING_ARRAY_TYPE)) { + int stackUsed = buildArray(mv, Constants.STRING_TYPE, (Object[]) value); maxStack = Math.max(maxStack, stackSize++ + stackUsed); } else { if (value != null) { From c8c2e90bc77e0772235eb1e11085a86b42f75564 Mon Sep 17 00:00:00 2001 From: Daniel Mangold Date: Wed, 20 Nov 2024 18:28:33 +0100 Subject: [PATCH 11/48] Refactor method calling instructions --- .../transform/SolutionClassNode.java | 7 +- .../SolutionMergingClassTransformer.java | 5 + .../transform/SubmissionClassVisitor.java | 28 ++--- .../transform/SubmissionExecutionHandler.java | 11 +- .../transform/SubmissionMethodVisitor.java | 115 +++--------------- .../algoutils/transform/util/Constants.java | 69 +++++++++++ .../transform/util/MethodHeader.java | 17 +++ 7 files changed, 121 insertions(+), 131 deletions(-) diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionClassNode.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionClassNode.java index 58d67d09..ccacdbbe 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionClassNode.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionClassNode.java @@ -107,12 +107,7 @@ private MethodNode getMethodNode(int access, String name, String descriptor, Str public void visitMethodInsn(int opcodeAndSource, String owner, String name, String descriptor, boolean isInterface) { MethodHeader methodHeader = new MethodHeader(owner, 0, name, descriptor, null, null); if (transformationContext.methodHasReplacement(methodHeader)) { - MethodHeader replacementMethodHeader = transformationContext.getMethodReplacement(methodHeader); - super.visitMethodInsn(INVOKESTATIC, - replacementMethodHeader.owner(), - replacementMethodHeader.name(), - replacementMethodHeader.descriptor(), - false); + transformationContext.getMethodReplacement(methodHeader).toMethodInsn(getDelegate(), false); } else { super.visitMethodInsn(opcodeAndSource, owner, diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionMergingClassTransformer.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionMergingClassTransformer.java index a55f170c..aaba1ddb 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionMergingClassTransformer.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionMergingClassTransformer.java @@ -10,6 +10,7 @@ import org.sourcegrade.jagr.api.testing.ClassTransformer; import java.lang.reflect.Executable; +import java.lang.reflect.Modifier; import java.util.*; import java.util.concurrent.ConcurrentHashMap; @@ -194,6 +195,10 @@ public Builder addMethodReplacement(Executable targetExecutable, Executable repl */ @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; diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java index 3320b52b..61da8bd5 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java @@ -215,11 +215,7 @@ private void classMetadata() { ClassHeader classHeader = submissionClassInfo.getOriginalClassHeader(); Label startLabel = new Label(); Label endLabel = new Label(); - MethodVisitor mv = super.visitMethod(ACC_PUBLIC | ACC_STATIC, - "getOriginalClassHeader", - Type.getMethodDescriptor(classHeader.getType()), - null, - null); + MethodVisitor mv = Constants.INJECTED_GET_ORIGINAL_CLASS_HEADER.toMethodVisitor(getDelegate()); mv.visitLabel(startLabel); int maxStack = buildClassHeader(mv, classHeader); @@ -243,12 +239,7 @@ private void fieldMetadata() { Label startLabel = new Label(); Label endLabel = new Label(); int maxStack, stackSize; - Type setType = Type.getType(Set.class); - MethodVisitor mv = super.visitMethod(ACC_PUBLIC | ACC_STATIC, - "getOriginalFieldHeaders", - Type.getMethodDescriptor(setType), - "()L%s<%s>;".formatted(setType.getInternalName(), Type.getDescriptor(FieldHeader.class)), - null); + MethodVisitor mv = Constants.INJECTED_GET_ORIGINAL_FIELD_HEADERS.toMethodVisitor(getDelegate()); mv.visitLabel(startLabel); mv.visitIntInsn(SIPUSH, fieldHeaders.size()); @@ -266,9 +257,9 @@ private void fieldMetadata() { stackSize -= 3; } mv.visitMethodInsn(INVOKESTATIC, - setType.getInternalName(), + Constants.SET_TYPE.getInternalName(), "of", - Type.getMethodDescriptor(setType, Type.getType(Object[].class)), + Type.getMethodDescriptor(Constants.SET_TYPE, Type.getType(Object[].class)), true); mv.visitInsn(ARETURN); mv.visitLabel(endLabel); @@ -290,12 +281,7 @@ private void methodMetadata() { Label startLabel = new Label(); Label endLabel = new Label(); int maxStack, stackSize; - Type setType = Type.getType(Set.class); - MethodVisitor mv = super.visitMethod(ACC_PUBLIC | ACC_STATIC, - "getOriginalMethodHeaders", - Type.getMethodDescriptor(setType), - "()L%s<%s>;".formatted(setType.getInternalName(), Type.getDescriptor(MethodHeader.class)), - null); + MethodVisitor mv = Constants.INJECTED_GET_ORIGINAL_METHODS_HEADERS.toMethodVisitor(getDelegate()); mv.visitLabel(startLabel); mv.visitIntInsn(SIPUSH, methodHeaders.size()); @@ -313,9 +299,9 @@ private void methodMetadata() { stackSize -= 3; } mv.visitMethodInsn(INVOKESTATIC, - setType.getInternalName(), + Constants.SET_TYPE.getInternalName(), "of", - Type.getMethodDescriptor(setType, Type.getType(Object[].class)), + Type.getMethodDescriptor(Constants.SET_TYPE, Type.getType(Object[].class)), true); mv.visitInsn(ARETURN); mv.visitLabel(endLabel); diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionExecutionHandler.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionExecutionHandler.java index b54e27c5..18e902ac 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionExecutionHandler.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionExecutionHandler.java @@ -29,11 +29,6 @@ * * private final SubmissionExecutionHandler executionHandler = SubmissionExecutionHandler.getInstance(); * - * @BeforeAll - * public static void start() { - * Utils.transformSubmission(); // In case Jagr is not present - * } - * * @BeforeEach * public void setup() { * // Pre-test setup, if necessary. Useful for substitution: @@ -100,7 +95,7 @@ public static SubmissionExecutionHandler getInstance() { public static ClassHeader getOriginalClassHeader(Class clazz) { try { return (ClassHeader) MethodHandles.lookup() - .findStatic(clazz, "getOriginalClassHeader", MethodType.methodType(ClassHeader.class)) + .findStatic(clazz, Constants.INJECTED_GET_ORIGINAL_CLASS_HEADER.name(), MethodType.methodType(ClassHeader.class)) .invokeExact(); } catch (Throwable e) { throw new RuntimeException(e); @@ -117,7 +112,7 @@ public static ClassHeader getOriginalClassHeader(Class clazz) { public static Set getOriginalFieldHeaders(Class clazz) { try { return (Set) MethodHandles.lookup() - .findStatic(clazz, "getOriginalFieldHeaders", MethodType.methodType(Set.class)) + .findStatic(clazz, Constants.INJECTED_GET_ORIGINAL_FIELD_HEADERS.name(), MethodType.methodType(Set.class)) .invokeExact(); } catch (Throwable e) { throw new RuntimeException(e); @@ -134,7 +129,7 @@ public static Set getOriginalFieldHeaders(Class clazz) { public static Set getOriginalMethodHeaders(Class clazz) { try { return (Set) MethodHandles.lookup() - .findStatic(clazz, "getOriginalMethodHeaders", MethodType.methodType(Set.class)) + .findStatic(clazz, Constants.INJECTED_GET_ORIGINAL_METHODS_HEADERS.name(), MethodType.methodType(Set.class)) .invokeExact(); } catch (Throwable e) { throw new RuntimeException(e); diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionMethodVisitor.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionMethodVisitor.java index 99759db4..1582c80d 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionMethodVisitor.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionMethodVisitor.java @@ -100,16 +100,8 @@ public void visitCode() { // create SubmissionExecutionHandler$Internal instance and store in locals array super.visitTypeInsn(NEW, Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_TYPE.getInternalName()); super.visitInsn(DUP); - super.visitMethodInsn(INVOKESTATIC, - Constants.SUBMISSION_EXECUTION_HANDLER_TYPE.getInternalName(), - "getInstance", - Type.getMethodDescriptor(Constants.SUBMISSION_EXECUTION_HANDLER_TYPE), - false); - super.visitMethodInsn(INVOKESPECIAL, - Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_TYPE.getInternalName(), - "", - Type.getMethodDescriptor(Type.VOID_TYPE, Constants.SUBMISSION_EXECUTION_HANDLER_TYPE), - false); + Constants.SUBMISSION_EXECUTION_HANDLER_GET_INSTANCE.toMethodInsn(getDelegate(), false); + Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_CONSTRUCTOR.toMethodInsn(getDelegate(), false); super.visitVarInsn(ASTORE, submissionExecutionHandlerIndex); super.visitLabel(submissionExecutionHandlerVarLabel); @@ -132,22 +124,14 @@ public void visitCode() { // check if invocation should be logged super.visitVarInsn(ALOAD, submissionExecutionHandlerIndex); super.visitVarInsn(ALOAD, methodHeaderIndex); - super.visitMethodInsn(INVOKEVIRTUAL, - Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_TYPE.getInternalName(), - "logInvocation", - Type.getMethodDescriptor(Type.BOOLEAN_TYPE, methodHeader.getType()), - false); + Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_LOG_INVOCATION.toMethodInsn(getDelegate(), false); super.visitJumpInsn(IFEQ, substitutionCheckLabel); // jump to label if logInvocation(...) == false // intercept parameters super.visitVarInsn(ALOAD, submissionExecutionHandlerIndex); super.visitVarInsn(ALOAD, methodHeaderIndex); buildInvocation(Type.getArgumentTypes(methodHeader.descriptor())); - super.visitMethodInsn(INVOKEVIRTUAL, - Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_TYPE.getInternalName(), - "addInvocation", - Type.getMethodDescriptor(Type.VOID_TYPE, methodHeader.getType(), Constants.INVOCATION_TYPE), - false); + Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_ADD_INVOCATION.toMethodInsn(getDelegate(), false); } // Method substitution @@ -157,21 +141,13 @@ public void visitCode() { super.visitLabel(substitutionCheckLabel); super.visitVarInsn(ALOAD, submissionExecutionHandlerIndex); super.visitVarInsn(ALOAD, methodHeaderIndex); - super.visitMethodInsn(INVOKEVIRTUAL, - Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_TYPE.getInternalName(), - "useSubstitution", - Type.getMethodDescriptor(Type.BOOLEAN_TYPE, methodHeader.getType()), - false); + Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_USE_SUBSTITUTION.toMethodInsn(getDelegate(), false); super.visitJumpInsn(IFEQ, defaultTransformationsOnly ? submissionCodeLabel : delegationCheckLabel); // jump to label if useSubstitution(...) == false // get substitution and execute it super.visitVarInsn(ALOAD, submissionExecutionHandlerIndex); super.visitVarInsn(ALOAD, methodHeaderIndex); - super.visitMethodInsn(INVOKEVIRTUAL, - Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_TYPE.getInternalName(), - "getSubstitution", - Type.getMethodDescriptor(Constants.METHOD_SUBSTITUTION_TYPE, methodHeader.getType()), - false); + Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_GET_SUBSTITUTION.toMethodInsn(getDelegate(), false); super.visitVarInsn(ASTORE, methodSubstitutionIndex); super.visitFrame(F_APPEND, 1, new Object[] {Constants.METHOD_SUBSTITUTION_TYPE.getInternalName()}, 0, null); fullFrameLocals.add(Constants.METHOD_SUBSTITUTION_TYPE.getInternalName()); @@ -207,11 +183,7 @@ public void visitCode() { * } */ super.visitVarInsn(ALOAD, methodSubstitutionIndex); - super.visitMethodInsn(INVOKEINTERFACE, - Constants.METHOD_SUBSTITUTION_TYPE.getInternalName(), - "getConstructorInvocation", - Type.getMethodDescriptor(Constants.METHOD_SUBSTITUTION_CONSTRUCTOR_INVOCATION_TYPE), - true); + Constants.METHOD_SUBSTITUTION_GET_CONSTRUCTOR_INVOCATION.toMethodInsn(getDelegate(), true); super.visitVarInsn(ASTORE, constructorInvocationIndex); super.visitFrame(F_APPEND, 1, new Object[] {Constants.METHOD_SUBSTITUTION_CONSTRUCTOR_INVOCATION_TYPE.getInternalName()}, 0, null); fullFrameLocals.add(Constants.METHOD_SUBSTITUTION_CONSTRUCTOR_INVOCATION_TYPE.getInternalName()); @@ -235,20 +207,12 @@ public void visitCode() { super.visitInsn(DUP); super.visitInsn(ICONST_0); super.visitVarInsn(ALOAD, constructorInvocationIndex); - super.visitMethodInsn(INVOKEVIRTUAL, - Constants.METHOD_SUBSTITUTION_CONSTRUCTOR_INVOCATION_TYPE.getInternalName(), - "owner", - Type.getMethodDescriptor(Constants.STRING_TYPE), - false); + Constants.METHOD_SUBSTITUTION_CONSTRUCTOR_INVOCATION_OWNER.toMethodInsn(getDelegate(), false); super.visitInsn(AASTORE); super.visitInsn(DUP); super.visitInsn(ICONST_1); super.visitVarInsn(ALOAD, constructorInvocationIndex); - super.visitMethodInsn(INVOKEVIRTUAL, - Constants.METHOD_SUBSTITUTION_CONSTRUCTOR_INVOCATION_TYPE.getInternalName(), - "descriptor", - Type.getMethodDescriptor(Constants.STRING_TYPE), - false); + Constants.METHOD_SUBSTITUTION_CONSTRUCTOR_INVOCATION_DESCRIPTOR.toMethodInsn(getDelegate(), false); super.visitInsn(AASTORE); super.visitMethodInsn(INVOKEVIRTUAL, Constants.STRING_TYPE.getInternalName(), @@ -279,11 +243,7 @@ public void visitCode() { super.visitVarInsn(ALOAD, methodSubstitutionIndex); buildInvocation(Type.getArgumentTypes(methodHeader.descriptor())); - super.visitMethodInsn(INVOKEINTERFACE, - Constants.METHOD_SUBSTITUTION_TYPE.getInternalName(), - "execute", - Type.getMethodDescriptor(Constants.OBJECT_TYPE, Constants.INVOCATION_TYPE), - true); + Constants.METHOD_SUBSTITUTION_EXECUTE.toMethodInsn(getDelegate(), true); Type returnType = Type.getReturnType(methodHeader.descriptor()); if (returnType.getSort() == Type.ARRAY || returnType.getSort() == Type.OBJECT) { super.visitTypeInsn(CHECKCAST, returnType.getInternalName()); @@ -309,11 +269,7 @@ public void visitCode() { super.visitLabel(delegationCheckLabel); super.visitVarInsn(ALOAD, submissionExecutionHandlerIndex); super.visitVarInsn(ALOAD, methodHeaderIndex); - super.visitMethodInsn(INVOKEVIRTUAL, - Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_TYPE.getInternalName(), - "useSubmissionImpl", - Type.getMethodDescriptor(Type.BOOLEAN_TYPE, methodHeader.getType()), - false); + Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_USE_SUBMISSION_IMPL.toMethodInsn(getDelegate(), false); super.visitJumpInsn(IFNE, submissionCodeLabel); // jump to label if useSubmissionImpl(...) == true // replay instructions from solution @@ -378,12 +334,7 @@ public void visitMethodInsn(int opcode, String owner, String name, String descri // skip transformation if only default transformations are applied or owner is not part of the submission MethodHeader methodHeader = new MethodHeader(owner, 0, name, descriptor, null, null); if (transformationContext.methodHasReplacement(methodHeader)) { - MethodHeader replacementMethodHeader = transformationContext.getMethodReplacement(methodHeader); - super.visitMethodInsn(INVOKESTATIC, - replacementMethodHeader.owner(), - replacementMethodHeader.name(), - replacementMethodHeader.descriptor(), - false); + transformationContext.getMethodReplacement(methodHeader).toMethodInsn(getDelegate(), false); } else if (defaultTransformationsOnly || !owner.startsWith(transformationContext.getProjectPrefix())) { super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); } else { @@ -415,11 +366,7 @@ private void buildInvocation(Type[] argumentTypes) { "getStackTrace", Type.getMethodDescriptor(stackTraceElementArrayType), false); - super.visitMethodInsn(INVOKESPECIAL, - Constants.INVOCATION_TYPE.getInternalName(), - "", - Type.getMethodDescriptor(Type.VOID_TYPE, Constants.OBJECT_TYPE, stackTraceElementArrayType), - false); + Constants.INVOCATION_CONSTRUCTOR_WITH_INSTANCE.toMethodInsn(getDelegate(), false); } else { super.visitMethodInsn(INVOKESTATIC, threadType.getInternalName(), @@ -431,22 +378,14 @@ private void buildInvocation(Type[] argumentTypes) { "getStackTrace", Type.getMethodDescriptor(stackTraceElementArrayType), false); - super.visitMethodInsn(INVOKESPECIAL, - Constants.INVOCATION_TYPE.getInternalName(), - "", - Type.getMethodDescriptor(Type.VOID_TYPE, stackTraceElementArrayType), - false); + Constants.INVOCATION_CONSTRUCTOR.toMethodInsn(getDelegate(), false); } // load parameter with opcode (ALOAD, ILOAD, etc.) for type and ignore "this", if it exists for (int i = 0; i < argumentTypes.length; i++) { super.visitInsn(DUP); super.visitVarInsn(argumentTypes[i].getOpcode(ILOAD), getLocalsIndex(argumentTypes, i) + (isStatic ? 0 : 1)); boxType(getDelegate(), argumentTypes[i]); - super.visitMethodInsn(INVOKEVIRTUAL, - Constants.INVOCATION_TYPE.getInternalName(), - "addParameter", - Type.getMethodDescriptor(Type.VOID_TYPE, Constants.OBJECT_TYPE), - false); + Constants.INVOCATION_CONSTRUCTOR_ADD_PARAMETER.toMethodInsn(getDelegate(), false); } } @@ -455,11 +394,7 @@ private void buildConstructorInvocationBranch(MethodHeader constructorHeader, Label[] labels, AtomicInteger labelIndex) { super.visitVarInsn(ALOAD, constructorInvocationIndex); - super.visitMethodInsn(INVOKEVIRTUAL, - Constants.METHOD_SUBSTITUTION_CONSTRUCTOR_INVOCATION_TYPE.getInternalName(), - "owner", - Type.getMethodDescriptor(Constants.STRING_TYPE), - false); + Constants.METHOD_SUBSTITUTION_CONSTRUCTOR_INVOCATION_OWNER.toMethodInsn(getDelegate(), false); super.visitLdcInsn(constructorHeader.owner()); super.visitMethodInsn(INVOKEVIRTUAL, Constants.STRING_TYPE.getInternalName(), @@ -468,11 +403,7 @@ private void buildConstructorInvocationBranch(MethodHeader constructorHeader, false); super.visitVarInsn(ALOAD, constructorInvocationIndex); - super.visitMethodInsn(INVOKEVIRTUAL, - Constants.METHOD_SUBSTITUTION_CONSTRUCTOR_INVOCATION_TYPE.getInternalName(), - "descriptor", - Type.getMethodDescriptor(Constants.STRING_TYPE), - false); + Constants.METHOD_SUBSTITUTION_CONSTRUCTOR_INVOCATION_DESCRIPTOR.toMethodInsn(getDelegate(), false); super.visitLdcInsn(constructorHeader.descriptor()); super.visitMethodInsn(INVOKEVIRTUAL, Constants.STRING_TYPE.getInternalName(), @@ -485,11 +416,7 @@ private void buildConstructorInvocationBranch(MethodHeader constructorHeader, Label argsVarStartLabel = new Label(); super.visitVarInsn(ALOAD, constructorInvocationIndex); - super.visitMethodInsn(INVOKEVIRTUAL, - Constants.METHOD_SUBSTITUTION_CONSTRUCTOR_INVOCATION_TYPE.getInternalName(), - "args", - Type.getMethodDescriptor(Constants.OBJECT_ARRAY_TYPE), - false); + Constants.METHOD_SUBSTITUTION_CONSTRUCTOR_INVOCATION_ARGS.toMethodInsn(getDelegate(), false); super.visitVarInsn(ASTORE, constructorInvocationIndex + 1); super.visitFrame(F_APPEND, 1, new Object[] {Constants.OBJECT_ARRAY_TYPE.getInternalName()}, 0, null); fullFrameLocals.add(Constants.OBJECT_ARRAY_TYPE.getInternalName()); @@ -503,11 +430,7 @@ private void buildConstructorInvocationBranch(MethodHeader constructorHeader, super.visitInsn(AALOAD); unboxType(getDelegate(), parameterTypes[i]); } - super.visitMethodInsn(INVOKESPECIAL, - constructorHeader.owner(), - "", - constructorHeader.descriptor(), - false); + constructorHeader.toMethodInsn(getDelegate(), false); super.visitJumpInsn(GOTO, substitutionExecuteLabel); fullFrameLocals.removeLast(); 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 index 4c498399..3ae4a640 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Constants.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Constants.java @@ -4,6 +4,11 @@ import org.tudalgo.algoutils.student.annotation.ForceSignature; import org.tudalgo.algoutils.transform.SubmissionExecutionHandler; +import java.util.Set; + +import static org.objectweb.asm.Opcodes.ACC_PUBLIC; +import static org.objectweb.asm.Opcodes.ACC_STATIC; + public final class Constants { // Types @@ -12,6 +17,7 @@ public final class Constants { 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 CLASS_HEADER_TYPE = Type.getType(ClassHeader.class); public static final Type[] CLASS_HEADER_CONSTRUCTOR_TYPES = new Type[] { @@ -46,5 +52,68 @@ public final class Constants { public static final Type SUBMISSION_EXECUTION_HANDLER_TYPE = Type.getType(SubmissionExecutionHandler.class); public static final Type SUBMISSION_EXECUTION_HANDLER_INTERNAL_TYPE = Type.getType(SubmissionExecutionHandler.Internal.class); + // 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.getInternalName()), + 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.getInternalName()), + null); + + public static final MethodHeader SUBMISSION_EXECUTION_HANDLER_GET_INSTANCE; + public static final MethodHeader SUBMISSION_EXECUTION_HANDLER_INTERNAL_CONSTRUCTOR; + 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_GET_INSTANCE = new MethodHeader(SubmissionExecutionHandler.class.getDeclaredMethod("getInstance")); + SUBMISSION_EXECUTION_HANDLER_INTERNAL_CONSTRUCTOR = new MethodHeader(SubmissionExecutionHandler.Internal.class.getDeclaredConstructor(SubmissionExecutionHandler.class)); + 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("useSubstitution", MethodHeader.class)); + + INVOCATION_CONSTRUCTOR = new MethodHeader(Invocation.class.getDeclaredConstructor(StackTraceElement[].class)); + INVOCATION_CONSTRUCTOR_WITH_INSTANCE = new MethodHeader(Invocation.class.getDeclaredConstructor(Object.class, StackTraceElement[].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/MethodHeader.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/MethodHeader.java index 0b1b5986..214dd825 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/MethodHeader.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/MethodHeader.java @@ -9,6 +9,8 @@ import java.util.Arrays; import java.util.Objects; +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 @@ -79,6 +81,21 @@ 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 : (access & ACC_STATIC) != 0 ? INVOKESTATIC : name.equals("") ? INVOKESPECIAL : INVOKEVIRTUAL; + methodVisitor.visitMethodInsn(opcode, + owner, + name, + descriptor, + isInterface); + } + /** * Two instances of {@link MethodHeader} are considered equal if their names and descriptors are equal. * TODO: include owner and parent classes if possible From 390c10202c4d57fa099fa5f122f2c64b37c77fcc Mon Sep 17 00:00:00 2001 From: Daniel Mangold Date: Thu, 21 Nov 2024 10:12:31 +0100 Subject: [PATCH 12/48] Fix method signature for getOriginalFieldHeaders and getOrignalMethodHeaders --- .../java/org/tudalgo/algoutils/transform/util/Constants.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index 3ae4a640..d1e98966 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Constants.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Constants.java @@ -64,13 +64,13 @@ public final class Constants { ACC_PUBLIC | ACC_STATIC, "getOriginalFieldHeaders", Type.getMethodDescriptor(SET_TYPE), - "()L%s<%s>;".formatted(SET_TYPE.getInternalName(), FIELD_HEADER_TYPE.getInternalName()), + "()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.getInternalName()), + "()L%s<%s>;".formatted(SET_TYPE.getInternalName(), METHOD_HEADER_TYPE.getDescriptor()), null); public static final MethodHeader SUBMISSION_EXECUTION_HANDLER_GET_INSTANCE; From f5ba41840791f3b8c00593993d7b1af6953e2be4 Mon Sep 17 00:00:00 2001 From: Daniel Mangold Date: Thu, 21 Nov 2024 10:32:41 +0100 Subject: [PATCH 13/48] Filter synthetic fields and methods from original headers --- .../algoutils/transform/SubmissionClassVisitor.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java index 61da8bd5..3c83bdcd 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java @@ -6,6 +6,7 @@ import org.objectweb.asm.tree.MethodNode; import java.util.*; +import java.util.stream.Collectors; import static org.tudalgo.algoutils.transform.util.TransformationUtils.*; import static org.objectweb.asm.Opcodes.*; @@ -235,7 +236,10 @@ private void classMetadata() { * This injected method returns the set of original field headers of the class pre-transformation. */ private void fieldMetadata() { - Set fieldHeaders = submissionClassInfo.getOriginalFieldHeaders(); + Set fieldHeaders = submissionClassInfo.getOriginalFieldHeaders() + .stream() + .filter(fieldHeader -> (fieldHeader.access() & ACC_SYNTHETIC) == 0) + .collect(Collectors.toSet()); Label startLabel = new Label(); Label endLabel = new Label(); int maxStack, stackSize; @@ -277,7 +281,10 @@ private void fieldMetadata() { * This injected method returns the set of original method headers of the class pre-transformation. */ private void methodMetadata() { - Set methodHeaders = submissionClassInfo.getOriginalMethodHeaders(); + Set methodHeaders = submissionClassInfo.getOriginalMethodHeaders() + .stream() + .filter(methodHeader -> (methodHeader.access() & ACC_SYNTHETIC) == 0) + .collect(Collectors.toSet());; Label startLabel = new Label(); Label endLabel = new Label(); int maxStack, stackSize; From b256ed4ad57d7298ede6c091707e6bfd6e2f55f4 Mon Sep 17 00:00:00 2001 From: Daniel Mangold Date: Thu, 21 Nov 2024 20:29:10 +0100 Subject: [PATCH 14/48] Fix NPE in SolutionClassNode --- .../org/tudalgo/algoutils/transform/SolutionClassNode.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionClassNode.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionClassNode.java index ccacdbbe..58d67d09 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionClassNode.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionClassNode.java @@ -107,7 +107,12 @@ private MethodNode getMethodNode(int access, String name, String descriptor, Str public void visitMethodInsn(int opcodeAndSource, String owner, String name, String descriptor, boolean isInterface) { MethodHeader methodHeader = new MethodHeader(owner, 0, name, descriptor, null, null); if (transformationContext.methodHasReplacement(methodHeader)) { - transformationContext.getMethodReplacement(methodHeader).toMethodInsn(getDelegate(), false); + MethodHeader replacementMethodHeader = transformationContext.getMethodReplacement(methodHeader); + super.visitMethodInsn(INVOKESTATIC, + replacementMethodHeader.owner(), + replacementMethodHeader.name(), + replacementMethodHeader.descriptor(), + false); } else { super.visitMethodInsn(opcodeAndSource, owner, From 8185233869fa67d4756c2b21bfa7b68260ae32bf Mon Sep 17 00:00:00 2001 From: Daniel Mangold Date: Sat, 23 Nov 2024 13:04:21 +0100 Subject: [PATCH 15/48] Fix bug where submission class was loaded from wrong class loader --- .../SolutionMergingClassTransformer.java | 16 +++- .../transform/util/TransformationContext.java | 74 +++++++++++++++++-- .../transform/util/TransformationUtils.java | 4 +- 3 files changed, 82 insertions(+), 12 deletions(-) diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionMergingClassTransformer.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionMergingClassTransformer.java index aaba1ddb..c789dd30 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionMergingClassTransformer.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionMergingClassTransformer.java @@ -1,5 +1,6 @@ package org.tudalgo.algoutils.transform; +import org.sourcegrade.jagr.api.testing.extension.JagrExecutionCondition; import org.tudalgo.algoutils.student.annotation.ForceSignature; import org.tudalgo.algoutils.transform.util.MethodHeader; import org.tudalgo.algoutils.transform.util.TransformationContext; @@ -10,6 +11,7 @@ import org.sourcegrade.jagr.api.testing.ClassTransformer; import java.lang.reflect.Executable; +import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.*; import java.util.concurrent.ConcurrentHashMap; @@ -109,8 +111,18 @@ public int getWriterFlags() { @Override public void transform(ClassReader reader, ClassWriter writer) { - String submissionClassName = reader.getClassName(); - reader.accept(new SubmissionClassVisitor(writer, transformationContext, submissionClassName), 0); + if (!new JagrExecutionCondition().evaluateExecutionCondition(null).isDisabled()) { // if Jagr is present + try { + Method getClassLoader = ClassWriter.class.getDeclaredMethod("getClassLoader"); + getClassLoader.setAccessible(true); + transformationContext.setSubmissionClassLoader((ClassLoader) getClassLoader.invoke(writer)); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } else { + transformationContext.setSubmissionClassLoader(getClass().getClassLoader()); + } + reader.accept(new SubmissionClassVisitor(writer, transformationContext, reader.getClassName()), 0); } /** 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 index c0f6fa01..1987130a 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationContext.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationContext.java @@ -5,20 +5,36 @@ import org.tudalgo.algoutils.transform.SubmissionClassInfo; import java.util.Map; +import java.util.Objects; /** * A record for holding context information for the transformation process. * - * @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} * @author Daniel Mangold */ -public record TransformationContext( - Map configuration, - Map solutionClasses, - Map submissionClasses -) { +public final class TransformationContext { + + private final Map configuration; + private final Map solutionClasses; + private final Map submissionClasses; + private ClassLoader submissionClassLoader; + + /** + * 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; + } /** * Returns the project prefix. @@ -48,6 +64,14 @@ public boolean methodHasReplacement(MethodHeader methodHeader) { return getMethodReplacement(methodHeader) != null; } + public void setSubmissionClassLoader(ClassLoader submissionClassLoader) { + this.submissionClassLoader = submissionClassLoader; + } + + public ClassLoader getSubmissionClassLoader() { + return submissionClassLoader; + } + /** * Returns the replacement method header for the given target method header. * @@ -71,4 +95,38 @@ public SubmissionClassInfo getSubmissionClassInfo(String submissionClassName) { return submissionClasses.computeIfAbsent(submissionClassName, className -> TransformationUtils.readSubmissionClass(this, className)); } + + public Map configuration() { + return configuration; + } + + public Map solutionClasses() { + return solutionClasses; + } + + public Map submissionClasses() { + return submissionClasses; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) return true; + if (obj == null || obj.getClass() != this.getClass()) return false; + var that = (TransformationContext) obj; + return Objects.equals(this.configuration, that.configuration) && + Objects.equals(this.solutionClasses, that.solutionClasses) && + Objects.equals(this.submissionClasses, that.submissionClasses); + } + + @Override + public int hashCode() { + return Objects.hash(configuration, solutionClasses, submissionClasses); + } + + @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 index 1c9aaf27..75fa3d35 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationUtils.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationUtils.java @@ -176,8 +176,8 @@ public static SolutionClassNode readSolutionClass(TransformationContext transfor */ public static SubmissionClassInfo readSubmissionClass(TransformationContext transformationContext, String className) { ClassReader submissionClassReader; - String submissionClassFilePath = "/%s.class".formatted(className); - try (InputStream is = SolutionMergingClassTransformer.class.getResourceAsStream(submissionClassFilePath)) { + String submissionClassFilePath = className + ".class"; + try (InputStream is = transformationContext.getSubmissionClassLoader().getResourceAsStream(submissionClassFilePath)) { if (is == null) { return null; } From 3fe4a442269e1b0045e1e4ae4a20b00ec19d914f Mon Sep 17 00:00:00 2001 From: Daniel Mangold Date: Tue, 26 Nov 2024 18:51:47 +0100 Subject: [PATCH 16/48] Use submission-info.json for finding submission classes --- .../SolutionMergingClassTransformer.java | 29 +++- .../transform/SubmissionClassInfo.java | 154 ++++++++++-------- .../transform/util/SimilarityMapper.java | 43 +++++ .../transform/util/TransformationContext.java | 45 +++-- 4 files changed, 179 insertions(+), 92 deletions(-) create mode 100644 tutor/src/main/java/org/tudalgo/algoutils/transform/util/SimilarityMapper.java diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionMergingClassTransformer.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionMergingClassTransformer.java index c789dd30..a27b13a8 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionMergingClassTransformer.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionMergingClassTransformer.java @@ -1,5 +1,6 @@ package org.tudalgo.algoutils.transform; +import com.fasterxml.jackson.databind.ObjectMapper; import org.sourcegrade.jagr.api.testing.extension.JagrExecutionCondition; import org.tudalgo.algoutils.student.annotation.ForceSignature; import org.tudalgo.algoutils.transform.util.MethodHeader; @@ -15,6 +16,7 @@ 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. @@ -70,6 +72,7 @@ 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. @@ -110,17 +113,37 @@ public int getWriterFlags() { } @Override + @SuppressWarnings("unchecked") 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); - transformationContext.setSubmissionClassLoader((ClassLoader) getClassLoader.invoke(writer)); - } catch (ReflectiveOperationException e) { + ClassLoader submissionClassLoader = (ClassLoader) getClassLoader.invoke(writer); + transformationContext.setSubmissionClassLoader(submissionClassLoader); + + if (!readSubmissionClasses) { + Map submissionInfo = new ObjectMapper() + .readValue(submissionClassLoader.getResourceAsStream("submission-info.json"), Map.class); + Set submissionClassNames = ((List>) submissionInfo.get("sourceSets")).stream() + .filter(sourceSet -> sourceSet.get("name").equals("main")) + .findAny() + .map(sourceSet -> ((Map>) sourceSet.get("files")).get("java")) + .orElseThrow() + .stream() + .map(submissionClassName -> submissionClassName.replaceAll("\\.java$", "")) + .collect(Collectors.toSet()); + transformationContext.setSubmissionClassNames(submissionClassNames); + transformationContext.computeClassesSimilarity(); + + readSubmissionClasses = true; + } + } catch (Exception e) { throw new RuntimeException(e); } } else { - transformationContext.setSubmissionClassLoader(getClass().getClassLoader()); + // TODO: fix this for regular JUnit run + transformationContext.setSubmissionClassLoader(null); } reader.accept(new SubmissionClassVisitor(writer, transformationContext, reader.getClassName()), 0); } diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassInfo.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassInfo.java index fc183c9b..de86ac4c 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassInfo.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassInfo.java @@ -1,17 +1,14 @@ package org.tudalgo.algoutils.transform; import org.tudalgo.algoutils.transform.util.*; -import kotlin.Pair; import kotlin.Triple; import org.objectweb.asm.*; -import org.tudalgo.algoutils.tutor.general.match.MatchingUtils; -import java.lang.reflect.Constructor; -import java.lang.reflect.Field; -import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.*; +import java.util.function.Function; import java.util.stream.Collectors; +import java.util.stream.Stream; import static org.objectweb.asm.Opcodes.ACC_SYNTHETIC; @@ -64,14 +61,8 @@ public SubmissionClassInfo(TransformationContext transformationContext, if (fsAnnotationProcessor.classIdentifierIsForced()) { this.computedClassName = fsAnnotationProcessor.forcedClassIdentifier(); } else { - // If not forced, get the closest matching solution class (at least 90% similarity) - this.computedClassName = transformationContext.solutionClasses() - .keySet() - .stream() - .map(s -> new Pair<>(s, MatchingUtils.similarity(originalClassName, s))) - .filter(pair -> pair.getSecond() >= transformationContext.getSimilarity()) - .max(Comparator.comparing(Pair::getSecond)) - .map(Pair::getFirst) + // If not forced, get the closest matching solution class + this.computedClassName = Optional.ofNullable(transformationContext.getSolutionClassName(originalClassName)) .orElse(originalClassName); } this.solutionClass = transformationContext.solutionClasses().get(computedClassName); @@ -184,59 +175,87 @@ public void visit(int version, int access, String name, String signature, String @Override public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) { - FieldHeader submissionFieldHeader = new FieldHeader(originalClassName, access, name, descriptor, signature); - FieldHeader solutionFieldHeader; - if (fsAnnotationProcessor.fieldIdentifierIsForced(name)) { - solutionFieldHeader = fsAnnotationProcessor.forcedFieldHeader(name); - } else if (solutionClass != null) { - solutionFieldHeader = solutionClass.getFields() - .keySet() - .stream() - .map(fieldHeader -> new Pair<>(fieldHeader, MatchingUtils.similarity(name, fieldHeader.name()))) - .filter(pair -> pair.getSecond() >= transformationContext.getSimilarity()) - .max(Comparator.comparing(Pair::getSecond)) - .map(Pair::getFirst) - .orElse(submissionFieldHeader); - } else { - solutionFieldHeader = submissionFieldHeader; - } - - fields.put(submissionFieldHeader, solutionFieldHeader); + fields.put(new FieldHeader(originalClassName, 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(originalClassName, access, name, descriptor, signature, exceptions); - if ((access & ACC_SYNTHETIC) != 0 && name.startsWith("lambda$")) { + if ((access & ACC_SYNTHETIC) != 0 && name.startsWith("lambda$")) { // if method is lambda methods.put(submissionMethodHeader, submissionMethodHeader); return null; } - MethodHeader solutionMethodHeader; - if (fsAnnotationProcessor.methodSignatureIsForced(name, descriptor)) { - solutionMethodHeader = fsAnnotationProcessor.forcedMethodHeader(name, descriptor); - } else if (solutionClass != null) { - solutionMethodHeader = solutionClass.getMethods() - .keySet() - .stream() - .map(methodHeader -> new Triple<>(methodHeader, - MatchingUtils.similarity(name, methodHeader.name()), - MatchingUtils.similarity(descriptor, methodHeader.descriptor()))) - .filter(triple -> triple.getSecond() >= transformationContext.getSimilarity() && triple.getThird() >= transformationContext.getSimilarity()) - .max(Comparator.comparing(Triple::getSecond).thenComparing(Triple::getThird)) - .map(Triple::getFirst) - .orElse(submissionMethodHeader); - } else { - solutionMethodHeader = submissionMethodHeader; - } - - methods.put(submissionMethodHeader, solutionMethodHeader); + methods.put(submissionMethodHeader, null); return null; } - @Override - public void visitEnd() { + public void mapToSolutionClass() { + SimilarityMapper fieldsSimilarityMapper = new SimilarityMapper<>( + fields.keySet(), + solutionClass == null ? Collections.emptyList() : solutionClass.getFields().keySet(), + transformationContext.getSimilarity(), + FieldHeader::name + ); + for (FieldHeader submissionFieldHeader : fields.keySet()) { + FieldHeader solutionFieldHeader; + Type fieldType = Type.getType(submissionFieldHeader.descriptor()); + FieldHeader fallbackFieldHeader = new FieldHeader(computedClassName, + submissionFieldHeader.access(), + submissionFieldHeader.name(), + fieldType.getSort() == Type.OBJECT ? + "L" + TransformationUtils.getComputedName(transformationContext, fieldType.getInternalName()) + ";" : + fieldType.getSort() == Type.ARRAY ? + TransformationUtils.getComputedName(transformationContext, fieldType.getInternalName()) : + submissionFieldHeader.descriptor(), + submissionFieldHeader.signature()); + if (fsAnnotationProcessor.fieldIdentifierIsForced(submissionFieldHeader.name())) { + solutionFieldHeader = fsAnnotationProcessor.forcedFieldHeader(submissionFieldHeader.name()); + } else if (solutionClass != null) { + solutionFieldHeader = solutionClass.getFields() + .keySet() + .stream() + .filter(fieldHeader -> fieldHeader.equals(fieldsSimilarityMapper.getBestMatch(submissionFieldHeader))) + .findAny() + .orElse(fallbackFieldHeader); + } else { + solutionFieldHeader = fallbackFieldHeader; + } + fields.put(submissionFieldHeader, solutionFieldHeader); + } + + SimilarityMapper methodsSimilarityMapper = new SimilarityMapper<>( + methods.keySet(), + solutionClass == null ? Collections.emptyList() : solutionClass.getMethods().keySet(), + transformationContext.getSimilarity(), + methodHeader -> methodHeader.name() + methodHeader.descriptor() + ); + for (MethodHeader submissionMethodHeader : methods.keySet()) { + String submissionMethodName = submissionMethodHeader.name(); + String submissionMethodDescriptor = submissionMethodHeader.descriptor(); + MethodHeader solutionMethodHeader; + MethodHeader fallbackMethodHeader = new MethodHeader(computedClassName, + submissionMethodHeader.access(), + submissionMethodHeader.name(), + submissionMethodHeader.descriptor(), + submissionMethodHeader.signature(), + submissionMethodHeader.exceptions()); + if (fsAnnotationProcessor.methodSignatureIsForced(submissionMethodName, submissionMethodDescriptor)) { + solutionMethodHeader = fsAnnotationProcessor.forcedMethodHeader(submissionMethodName, submissionMethodDescriptor); + } else if (solutionClass != null) { + solutionMethodHeader = solutionClass.getMethods() + .keySet() + .stream() + .filter(methodHeader -> methodHeader.equals(methodsSimilarityMapper.getBestMatch(submissionMethodHeader))) + .findAny() + .orElse(fallbackMethodHeader); + } else { + solutionMethodHeader = fallbackMethodHeader; + } + methods.put(submissionMethodHeader, solutionMethodHeader); + } + for (Triple, Map> triple : superClassMembers) { if (triple.getFirst().equals(superClass)) { triple.getThird() @@ -284,7 +303,7 @@ private void resolveSuperClassMembers(Set(className, submissionClassInfo.fields.entrySet() @@ -299,23 +318,16 @@ private void resolveSuperClassMembers(Set clazz = Class.forName(className.replace('/', '.')); - Map fieldHeaders = new HashMap<>(); - for (Field field : clazz.getDeclaredFields()) { - if (Modifier.isPrivate(field.getModifiers())) continue; - FieldHeader fieldHeader = new FieldHeader(field); - fieldHeaders.put(fieldHeader, fieldHeader); - } - Map methodHeaders = new HashMap<>(); - for (Constructor constructor : clazz.getDeclaredConstructors()) { - if (Modifier.isPrivate(constructor.getModifiers())) continue; - MethodHeader methodHeader = new MethodHeader(constructor); - methodHeaders.put(methodHeader, methodHeader); - } - for (Method method : clazz.getDeclaredMethods()) { - if (Modifier.isPrivate(method.getModifiers())) continue; - MethodHeader methodHeader = new MethodHeader(method); - methodHeaders.put(methodHeader, methodHeader); - } + 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())); superClassMembers.add(new Triple<>(className, fieldHeaders, methodHeaders)); if (clazz.getSuperclass() != null) { resolveSuperClassMembers(superClassMembers, diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/SimilarityMapper.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/SimilarityMapper.java new file mode 100644 index 00000000..2bbc1ec2 --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/SimilarityMapper.java @@ -0,0 +1,43 @@ +package org.tudalgo.algoutils.transform.util; + +import org.tudalgo.algoutils.tutor.general.match.MatchingUtils; + +import java.util.*; +import java.util.function.Function; + +public class SimilarityMapper { + + private final Map bestMatches = new HashMap<>(); + + @SuppressWarnings("unchecked") + public SimilarityMapper(Collection from, Collection to, double similarityThreshold) { + this((Collection) from, (Collection) to, similarityThreshold, s -> (String) s); + } + + public SimilarityMapper(Collection from, + Collection to, + double similarityThreshold, + Function mappingFunction) { + List rowMapping = new ArrayList<>(from); + List columnMapping = new ArrayList<>(to); + double[][] similarityMatrix = new double[from.size()][to.size()]; + + for (int i = 0; i < similarityMatrix.length; i++) { + int bestMatchIndex = -1; + double bestSimilarity = similarityThreshold; + for (int j = 0; j < similarityMatrix[i].length; j++) { + similarityMatrix[i][j] = MatchingUtils.similarity(mappingFunction.apply(rowMapping.get(i)), + mappingFunction.apply(columnMapping.get(j))); + if (similarityMatrix[i][j] >= bestSimilarity) { + bestMatchIndex = j; + bestSimilarity = similarityMatrix[i][j]; + } + } + bestMatches.put(rowMapping.get(i), bestMatchIndex >= 0 ? columnMapping.get(bestMatchIndex) : null); + } + } + + public T getBestMatch(T t) { + return bestMatches.get(t); + } +} 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 index 1987130a..9e3077a4 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationContext.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationContext.java @@ -1,11 +1,11 @@ package org.tudalgo.algoutils.transform.util; +import org.objectweb.asm.Type; import org.tudalgo.algoutils.transform.SolutionClassNode; import org.tudalgo.algoutils.transform.SolutionMergingClassTransformer; import org.tudalgo.algoutils.transform.SubmissionClassInfo; -import java.util.Map; -import java.util.Objects; +import java.util.*; /** * A record for holding context information for the transformation process. @@ -17,7 +17,10 @@ public final class TransformationContext { private final Map configuration; private final Map solutionClasses; private final Map submissionClasses; + private ClassLoader submissionClassLoader; + private Set submissionClassNames; + private SimilarityMapper classSimilarityMapper; /** * Constructs a new {@link TransformationContext}. @@ -84,6 +87,18 @@ public MethodHeader getMethodReplacement(MethodHeader methodHeader) { .get(methodHeader); } + public void setSubmissionClassNames(Set submissionClassNames) { + this.submissionClassNames = submissionClassNames; + } + + 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. @@ -92,12 +107,13 @@ public MethodHeader getMethodReplacement(MethodHeader methodHeader) { * @return the {@link SubmissionClassInfo} object */ public SubmissionClassInfo getSubmissionClassInfo(String submissionClassName) { - return submissionClasses.computeIfAbsent(submissionClassName, + boolean isAbsent = !submissionClasses.containsKey(submissionClassName); + SubmissionClassInfo submissionClassInfo = submissionClasses.computeIfAbsent(submissionClassName, className -> TransformationUtils.readSubmissionClass(this, className)); - } - - public Map configuration() { - return configuration; + if (isAbsent && submissionClassInfo != null) { + submissionClassInfo.mapToSolutionClass(); + } + return submissionClassInfo; } public Map solutionClasses() { @@ -108,19 +124,12 @@ public Map submissionClasses() { return submissionClasses; } - @Override - public boolean equals(Object obj) { - if (obj == this) return true; - if (obj == null || obj.getClass() != this.getClass()) return false; - var that = (TransformationContext) obj; - return Objects.equals(this.configuration, that.configuration) && - Objects.equals(this.solutionClasses, that.solutionClasses) && - Objects.equals(this.submissionClasses, that.submissionClasses); + public void computeClassesSimilarity() { + classSimilarityMapper = new SimilarityMapper<>(submissionClassNames, solutionClasses.keySet(), getSimilarity()); } - @Override - public int hashCode() { - return Objects.hash(configuration, solutionClasses, submissionClasses); + public String getSolutionClassName(String submissionClassName) { + return classSimilarityMapper.getBestMatch(submissionClassName); } @Override From 23b5c45430a56e37a64ae8b9b261198915e2f510 Mon Sep 17 00:00:00 2001 From: Daniel Mangold Date: Tue, 26 Nov 2024 18:56:08 +0100 Subject: [PATCH 17/48] Fixes and refactoring --- .../transform/SolutionClassNode.java | 2 +- .../transform/SubmissionClassVisitor.java | 12 +-- .../algoutils/transform/util/ClassHeader.java | 20 ++++ .../algoutils/transform/util/Constants.java | 2 +- .../algoutils/transform/util/FieldHeader.java | 17 ++- .../algoutils/transform/util/Header.java | 2 +- .../transform/util/MethodHeader.java | 20 ++++ .../transform/util/TransformationUtils.java | 100 ++++++++++++++++++ 8 files changed, 165 insertions(+), 10 deletions(-) diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionClassNode.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionClassNode.java index 58d67d09..e41e7215 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionClassNode.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionClassNode.java @@ -72,7 +72,7 @@ public void visit(int version, int access, String name, String signature, String @Override public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) { FieldHeader fieldHeader = new FieldHeader(className, access, name, descriptor, signature); - FieldNode fieldNode = (FieldNode) super.visitField(access, name, descriptor, signature, value); + FieldNode fieldNode = (FieldNode) super.visitField(access & ~ACC_FINAL, name, descriptor, signature, value); fields.put(fieldHeader, fieldNode); return fieldNode; } diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java index 3c83bdcd..4a915def 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java @@ -201,9 +201,9 @@ public void visitEnd() { .forEach(methodNode -> methodNode.accept(getDelegate())); } - classMetadata(); - fieldMetadata(); - methodMetadata(); + injectClassMetadata(); + injectFieldMetadata(); + injectMethodMetadata(); super.visitEnd(); } @@ -212,7 +212,7 @@ public void 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 classMetadata() { + private void injectClassMetadata() { ClassHeader classHeader = submissionClassInfo.getOriginalClassHeader(); Label startLabel = new Label(); Label endLabel = new Label(); @@ -235,7 +235,7 @@ private void classMetadata() { * 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 fieldMetadata() { + private void injectFieldMetadata() { Set fieldHeaders = submissionClassInfo.getOriginalFieldHeaders() .stream() .filter(fieldHeader -> (fieldHeader.access() & ACC_SYNTHETIC) == 0) @@ -280,7 +280,7 @@ private void fieldMetadata() { * 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 methodMetadata() { + private void injectMethodMetadata() { Set methodHeaders = submissionClassInfo.getOriginalMethodHeaders() .stream() .filter(methodHeader -> (methodHeader.access() & ACC_SYNTHETIC) == 0) diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/ClassHeader.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/ClassHeader.java index 219c1f9f..ecd44d1c 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/ClassHeader.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/ClassHeader.java @@ -1,9 +1,12 @@ package org.tudalgo.algoutils.transform.util; import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; +import java.util.Arrays; import java.util.Objects; +import java.util.stream.Collectors; /** * A record holding information on the header of a class as declared in Java bytecode. @@ -72,4 +75,21 @@ public boolean equals(Object o) { 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/Constants.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Constants.java index d1e98966..80416277 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Constants.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Constants.java @@ -99,7 +99,7 @@ public final class Constants { 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("useSubstitution", 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(StackTraceElement[].class)); INVOCATION_CONSTRUCTOR_WITH_INSTANCE = new MethodHeader(Invocation.class.getDeclaredConstructor(Object.class, StackTraceElement[].class)); diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/FieldHeader.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/FieldHeader.java index 8ca06fe2..8d028308 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/FieldHeader.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/FieldHeader.java @@ -2,10 +2,13 @@ import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; import java.lang.reflect.Field; +import java.util.Arrays; import java.util.Objects; +import java.util.stream.Collectors; /** * A record holding information on the header of a field as declared in java bytecode. @@ -61,7 +64,7 @@ public Object getValue(String name) { * @return the resulting {@link FieldVisitor} */ public FieldVisitor toFieldVisitor(ClassVisitor delegate, Object value) { - return delegate.visitField(access, name, descriptor, signature, value); + return delegate.visitField(access & ~Opcodes.ACC_FINAL, name, descriptor, signature, value); } /** @@ -80,4 +83,16 @@ public boolean equals(Object o) { 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/Header.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Header.java index 17ee359c..3ce4ac82 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Header.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Header.java @@ -5,7 +5,7 @@ /** * Common interface of all header records. */ -interface Header { +public interface Header { /** * Returns the type for this header. diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/MethodHeader.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/MethodHeader.java index 214dd825..e13e76f8 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/MethodHeader.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/MethodHeader.java @@ -8,6 +8,7 @@ import java.lang.reflect.Method; import java.util.Arrays; import java.util.Objects; +import java.util.stream.Collectors; import static org.objectweb.asm.Opcodes.*; @@ -112,4 +113,23 @@ public boolean equals(Object o) { 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/TransformationUtils.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationUtils.java index 75fa3d35..339b65e1 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationUtils.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationUtils.java @@ -1,5 +1,6 @@ package org.tudalgo.algoutils.transform.util; +import org.opentest4j.AssertionFailedError; import org.tudalgo.algoutils.transform.SolutionClassNode; import org.tudalgo.algoutils.transform.SolutionMergingClassTransformer; import org.tudalgo.algoutils.transform.SubmissionClassInfo; @@ -9,6 +10,8 @@ import java.io.IOException; import java.io.InputStream; +import java.util.Map; +import java.util.StringJoiner; import static org.objectweb.asm.Opcodes.*; @@ -250,6 +253,103 @@ public static int buildArray(MethodVisitor mv, Type componentType, Object[] arra return maxStack; } + 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(); + } + + 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); + }; + } + + public static String getComputedName(TransformationContext transformationContext, String className) { + if (transformationContext.isSubmissionClass(className)) { + Type type = className.startsWith("[") ? Type.getType(className) : Type.getObjectType(className); + if (type.getSort() == Type.OBJECT) { + return transformationContext.getSubmissionClassInfo(className).getComputedClassName(); + } else { // else must be array + return "%sL%s;".formatted("[".repeat(type.getDimensions()), + transformationContext.getSubmissionClassInfo(type.getElementType().getInternalName()).getComputedClassName()); + } + } else { + return className; + } + } + + public static Type getComputedType(TransformationContext transformationContext, Type type) { + if (type.getSort() == Type.OBJECT) { + return Type.getObjectType(getComputedName(transformationContext, type.getInternalName())); + } else if (type.getSort() == Type.ARRAY) { + return Type.getType(getComputedName(transformationContext, type.getDescriptor())); + } else { + return type; + } + } + + public static void buildExceptionForHeaderMismatch(MethodVisitor mv, String message, T expected, T actual) { + mv.visitTypeInsn(NEW, Type.getInternalName(AssertionFailedError.class)); + mv.visitInsn(DUP); + mv.visitLdcInsn(message); + if (expected instanceof ClassHeader) { + TransformationUtils.buildClassHeader(mv, (ClassHeader) expected); + TransformationUtils.buildClassHeader(mv, (ClassHeader) actual); + } else if (expected instanceof FieldHeader) { + TransformationUtils.buildFieldHeader(mv, (FieldHeader) expected); + TransformationUtils.buildFieldHeader(mv, (FieldHeader) actual); + } else if (expected instanceof MethodHeader) { + TransformationUtils.buildMethodHeader(mv, (MethodHeader) expected); + TransformationUtils.buildMethodHeader(mv, (MethodHeader) actual); + } else { + throw new IllegalArgumentException("Unsupported header type: " + expected.getClass()); + } + mv.visitMethodInsn(INVOKESPECIAL, + Type.getInternalName(AssertionFailedError.class), + "", + Type.getMethodDescriptor(Type.VOID_TYPE, Constants.STRING_TYPE, Constants.OBJECT_TYPE, Constants.OBJECT_TYPE), + false); + mv.visitInsn(ATHROW); + } + + 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); + } + } + /** * 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 From 3ad69191c8e27ec431662f5af84a5ec5a45a4211 Mon Sep 17 00:00:00 2001 From: Daniel Mangold Date: Tue, 26 Nov 2024 18:56:41 +0100 Subject: [PATCH 18/48] Make framework more robust for incompatible fields and methods (static vs non-static) --- .../transform/SubmissionClassVisitor.java | 48 ++- .../transform/SubmissionMethodVisitor.java | 294 ++++++++++++++++-- 2 files changed, 298 insertions(+), 44 deletions(-) diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java index 4a915def..c5313007 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java @@ -151,13 +151,14 @@ public void visit(int version, int access, String name, String signature, String */ @Override public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) { - if (defaultTransformationsOnly) { - return super.visitField(access, name, descriptor, signature, value); - } - FieldHeader fieldHeader = submissionClassInfo.getComputedFieldHeader(name); - visitedFields.add(fieldHeader); - return fieldHeader.toFieldVisitor(getDelegate(), value); + + if ((access & ACC_STATIC) == (fieldHeader.access() & ACC_STATIC)) { + visitedFields.add(fieldHeader); + return fieldHeader.toFieldVisitor(getDelegate(), value); + } else { + return super.visitField(access & ~ACC_FINAL, name + "$submission", fieldHeader.descriptor(), fieldHeader.signature(), value); + } } /** @@ -166,16 +167,35 @@ public FieldVisitor visitField(int access, String name, String descriptor, Strin */ @Override public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { - MethodHeader methodHeader = submissionClassInfo.getComputedMethodHeader(name, descriptor); - MethodVisitor methodVisitor = methodHeader.toMethodVisitor(getDelegate()); - visitedMethods.add(methodHeader); + MethodHeader originalMethodHeader = new MethodHeader(submissionClassInfo.getOriginalClassHeader().name(), access, name, descriptor, signature, exceptions); + MethodHeader computedMethodHeader = submissionClassInfo.getComputedMethodHeader(name, descriptor); - // if method is abstract or lambda, skip transformation - if ((methodHeader.access() & ACC_ABSTRACT) != 0 || - ((methodHeader.access() & ACC_SYNTHETIC) != 0 && methodHeader.name().startsWith("lambda$"))) { - return methodVisitor; + // if method is lambda, skip transformation + if ((access & ACC_SYNTHETIC) != 0 && originalMethodHeader.name().startsWith("lambda$")) { + return originalMethodHeader.toMethodVisitor(getDelegate()); + } else if ((originalMethodHeader.access() & ACC_STATIC) != (computedMethodHeader.access() & ACC_STATIC)) { + Type returnType = TransformationUtils.getComputedType(transformationContext, Type.getReturnType(descriptor)); + Type[] parameterTypes = Arrays.stream(Type.getArgumentTypes(descriptor)) + .map(type -> TransformationUtils.getComputedType(transformationContext, type)) + .toArray(Type[]::new); + MethodHeader methodHeader = new MethodHeader(computedMethodHeader.owner(), + access, + name + "$submission", + Type.getMethodDescriptor(returnType, parameterTypes), + signature, + exceptions); + return new SubmissionMethodVisitor(methodHeader.toMethodVisitor(getDelegate()), + transformationContext, + submissionClassInfo, + originalMethodHeader, + methodHeader); } else { - return new SubmissionMethodVisitor(methodVisitor, transformationContext, submissionClassInfo, methodHeader); + visitedMethods.add(computedMethodHeader); + return new SubmissionMethodVisitor(computedMethodHeader.toMethodVisitor(getDelegate()), + transformationContext, + submissionClassInfo, + originalMethodHeader, + submissionClassInfo.getComputedMethodHeader(originalMethodHeader.name(), originalMethodHeader.descriptor())); } } diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionMethodVisitor.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionMethodVisitor.java index 1582c80d..7aa348e4 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionMethodVisitor.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionMethodVisitor.java @@ -1,8 +1,6 @@ package org.tudalgo.algoutils.transform; -import org.objectweb.asm.Label; -import org.objectweb.asm.MethodVisitor; -import org.objectweb.asm.Type; +import org.objectweb.asm.*; import org.tudalgo.algoutils.transform.util.*; import java.util.ArrayList; @@ -26,11 +24,13 @@ class SubmissionMethodVisitor extends MethodVisitor { private final TransformationContext transformationContext; - private final MethodHeader methodHeader; + private final MethodHeader originalMethodHeader; + private final MethodHeader computedMethodHeader; private final SubmissionClassInfo submissionClassInfo; private final String className; private final boolean defaultTransformationsOnly; + private final boolean headerMismatch; private final boolean isStatic; private final boolean isConstructor; @@ -47,30 +47,34 @@ class SubmissionMethodVisitor extends MethodVisitor { * @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 computed method header of this method + * @param originalMethodHeader the computed method header of this method */ SubmissionMethodVisitor(MethodVisitor delegate, TransformationContext transformationContext, SubmissionClassInfo submissionClassInfo, - MethodHeader methodHeader) { + MethodHeader originalMethodHeader, + MethodHeader computedMethodHeader) { super(ASM9, delegate); this.transformationContext = transformationContext; this.submissionClassInfo = submissionClassInfo; - this.methodHeader = methodHeader; + this.originalMethodHeader = originalMethodHeader; + this.computedMethodHeader = computedMethodHeader; this.className = submissionClassInfo.getComputedClassName(); - this.defaultTransformationsOnly = submissionClassInfo.getSolutionClass().isEmpty(); + this.defaultTransformationsOnly = submissionClassInfo.getSolutionClass() + .map(solutionClassNode -> !solutionClassNode.getMethods().containsKey(computedMethodHeader)) + .orElse(true); - this.isStatic = (methodHeader.access() & ACC_STATIC) != 0; - this.isConstructor = methodHeader.name().equals(""); + this.isStatic = (computedMethodHeader.access() & ACC_STATIC) != 0; + this.isConstructor = computedMethodHeader.name().equals(""); // calculate length of locals array, including "this" if applicable - int nextLocalsIndex = (Type.getArgumentsAndReturnSizes(methodHeader.descriptor()) >> 2) - (isStatic ? 1 : 0); + int nextLocalsIndex = (Type.getArgumentsAndReturnSizes(computedMethodHeader.descriptor()) >> 2) - (isStatic ? 1 : 0); this.submissionExecutionHandlerIndex = nextLocalsIndex; this.methodHeaderIndex = nextLocalsIndex + 1; this.methodSubstitutionIndex = nextLocalsIndex + 2; this.constructorInvocationIndex = nextLocalsIndex + 3; - this.fullFrameLocals = Arrays.stream(Type.getArgumentTypes(methodHeader.descriptor())) + 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; @@ -82,6 +86,16 @@ class SubmissionMethodVisitor extends MethodVisitor { if (!isStatic) { this.fullFrameLocals.addFirst(isConstructor ? UNINITIALIZED_THIS : className); } + + int[] originalParameterTypes = Arrays.stream(Type.getArgumentTypes(originalMethodHeader.descriptor())) + .mapToInt(Type::getSort) + .toArray(); + int originalReturnType = Type.getReturnType(originalMethodHeader.descriptor()).getSort(); + int[] computedParameterTypes = Arrays.stream(Type.getArgumentTypes(computedMethodHeader.descriptor())) + .mapToInt(Type::getSort) + .toArray(); + int computedReturnType = Type.getReturnType(computedMethodHeader.descriptor()).getSort(); + this.headerMismatch = !(Arrays.equals(originalParameterTypes, computedParameterTypes) && originalReturnType == computedReturnType); } @Override @@ -106,17 +120,17 @@ public void visitCode() { super.visitLabel(submissionExecutionHandlerVarLabel); // replicate method header in bytecode and store in locals array - buildMethodHeader(getDelegate(), methodHeader); + buildMethodHeader(getDelegate(), computedMethodHeader); super.visitVarInsn(ASTORE, methodHeaderIndex); super.visitLabel(methodHeaderVarLabel); super.visitFrame(F_APPEND, 2, - new Object[] {Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_TYPE.getInternalName(), methodHeader.getType().getInternalName()}, + new Object[] {Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_TYPE.getInternalName(), computedMethodHeader.getType().getInternalName()}, 0, null); fullFrameLocals.add(Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_TYPE.getInternalName()); - fullFrameLocals.add(methodHeader.getType().getInternalName()); + fullFrameLocals.add(computedMethodHeader.getType().getInternalName()); } // Invocation logging @@ -130,7 +144,7 @@ public void visitCode() { // intercept parameters super.visitVarInsn(ALOAD, submissionExecutionHandlerIndex); super.visitVarInsn(ALOAD, methodHeaderIndex); - buildInvocation(Type.getArgumentTypes(methodHeader.descriptor())); + buildInvocation(Type.getArgumentTypes(computedMethodHeader.descriptor())); Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_ADD_INVOCATION.toMethodInsn(getDelegate(), false); } @@ -242,9 +256,9 @@ public void visitCode() { } super.visitVarInsn(ALOAD, methodSubstitutionIndex); - buildInvocation(Type.getArgumentTypes(methodHeader.descriptor())); + buildInvocation(Type.getArgumentTypes(computedMethodHeader.descriptor())); Constants.METHOD_SUBSTITUTION_EXECUTE.toMethodInsn(getDelegate(), true); - Type returnType = Type.getReturnType(methodHeader.descriptor()); + Type returnType = Type.getReturnType(computedMethodHeader.descriptor()); if (returnType.getSort() == Type.ARRAY || returnType.getSort() == Type.OBJECT) { super.visitTypeInsn(CHECKCAST, returnType.getInternalName()); } else { @@ -284,13 +298,13 @@ public void visitCode() { delegationCodeLabel, submissionExecutionHandlerIndex); super.visitLocalVariable("methodHeader", - methodHeader.getType().getDescriptor(), + computedMethodHeader.getType().getDescriptor(), null, methodHeaderVarLabel, delegationCodeLabel, methodHeaderIndex); //noinspection OptionalGetWithoutIsPresent - submissionClassInfo.getSolutionClass().get().getMethods().get(methodHeader).accept(getDelegate()); + submissionClassInfo.getSolutionClass().get().getMethods().get(computedMethodHeader).accept(getDelegate()); super.visitFrame(F_FULL, fullFrameLocals.size(), fullFrameLocals.toArray(), 0, new Object[0]); super.visitLabel(submissionCodeLabel); @@ -307,42 +321,122 @@ public void visitCode() { submissionCodeLabel, submissionExecutionHandlerIndex); super.visitLocalVariable("methodHeader", - methodHeader.getType().getDescriptor(), + computedMethodHeader.getType().getDescriptor(), null, methodHeaderVarLabel, submissionCodeLabel, methodHeaderIndex); } - // visit original code - super.visitCode(); + if (headerMismatch) { + TransformationUtils.buildExceptionForHeaderMismatch(getDelegate(), + "Method has incorrect return or parameter types", + computedMethodHeader, + originalMethodHeader); + } else { + // visit original code + super.visitCode(); + } } @Override public void visitFieldInsn(int opcode, String owner, String name, String descriptor) { - // skip transformation if only default transformations are applied or owner is not part of the submission - if (defaultTransformationsOnly || !owner.startsWith(transformationContext.getProjectPrefix())) { + if (headerMismatch) return; + + // skip transformation if owner is not part of the submission + if (!transformationContext.isSubmissionClass(owner)) { super.visitFieldInsn(opcode, owner, name, descriptor); } else { - FieldHeader fieldHeader = transformationContext.getSubmissionClassInfo(owner).getComputedFieldHeader(name); - super.visitFieldInsn(opcode, fieldHeader.owner(), fieldHeader.name(), fieldHeader.descriptor()); + if (owner.startsWith("[")) { // stupid edge cases + Type ownerType = Type.getType(owner); + Type actualOwner = Type.getObjectType(transformationContext.getSubmissionClassInfo(ownerType.getElementType().getInternalName()) + .getComputedClassName()); + super.visitFieldInsn(opcode, + "[".repeat(ownerType.getDimensions()) + actualOwner.getDescriptor(), + name, + TransformationUtils.getComputedType(transformationContext, Type.getType(descriptor)).getDescriptor()); + } else { + FieldHeader computedFieldHeader = transformationContext.getSubmissionClassInfo(owner).getComputedFieldHeader(name); + boolean isStaticOpcode = opcode == GETSTATIC || opcode == PUTSTATIC; + boolean isStaticField = (computedFieldHeader.access() & ACC_STATIC) != 0; + if (isStaticOpcode == isStaticField) { + super.visitFieldInsn(opcode, + TransformationUtils.getComputedName(transformationContext, computedFieldHeader.owner()), + computedFieldHeader.name(), + computedFieldHeader.descriptor()); + } else { // if incompatible + super.visitFieldInsn(opcode, computedFieldHeader.owner(), name + "$submission", computedFieldHeader.descriptor()); + } + } } } @Override public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) { - // skip transformation if only default transformations are applied or owner is not part of the submission + if (headerMismatch) return; + + // skip transformation if owner is not part of the submission MethodHeader methodHeader = new MethodHeader(owner, 0, name, descriptor, null, null); if (transformationContext.methodHasReplacement(methodHeader)) { transformationContext.getMethodReplacement(methodHeader).toMethodInsn(getDelegate(), false); - } else if (defaultTransformationsOnly || !owner.startsWith(transformationContext.getProjectPrefix())) { + } else if (!transformationContext.isSubmissionClass(owner)) { super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); + } else if (owner.startsWith("[")) { // stupid edge cases + Type ownerType = Type.getType(owner); + Type actualOwner = Type.getObjectType(transformationContext.getSubmissionClassInfo(ownerType.getElementType().getInternalName()) + .getComputedClassName()); + super.visitMethodInsn(opcode, "[".repeat(ownerType.getDimensions()) + actualOwner.getDescriptor(), name, descriptor, isInterface); } else { methodHeader = transformationContext.getSubmissionClassInfo(owner).getComputedMethodHeader(name, descriptor); - super.visitMethodInsn(opcode, methodHeader.owner(), methodHeader.name(), methodHeader.descriptor(), isInterface); + if ((opcode == INVOKESTATIC) == ((methodHeader.access() & ACC_STATIC) != 0)) { + super.visitMethodInsn(opcode, methodHeader.owner(), methodHeader.name(), methodHeader.descriptor(), isInterface); + } else { + Type returnType = TransformationUtils.getComputedType(transformationContext, Type.getReturnType(descriptor)); + Type[] parameterTypes = Arrays.stream(Type.getArgumentTypes(descriptor)) + .map(type -> TransformationUtils.getComputedType(transformationContext, type)) + .toArray(Type[]::new); + super.visitMethodInsn(opcode, + methodHeader.owner(), + name + "$submission", + Type.getMethodDescriptor(returnType, parameterTypes), + isInterface); + } } } + @Override + public void visitLdcInsn(Object value) { + if (headerMismatch) return; + + if (value instanceof Type type && transformationContext.isSubmissionClass(type.getInternalName())) { + if (type.getSort() == Type.OBJECT) { + value = Type.getObjectType(transformationContext.getSubmissionClassInfo(type.getInternalName()).getComputedClassName()); + } else { // else must be array + Type elementType = Type.getObjectType(transformationContext.getSubmissionClassInfo(type.getElementType().getInternalName()) + .getComputedClassName()); + value = Type.getObjectType("[".repeat(type.getDimensions()) + elementType.getDescriptor()); + } + } + super.visitLdcInsn(value); + } + + @Override + public void visitTypeInsn(int opcode, String type) { + if (headerMismatch) return; + + Type paramType = type.startsWith("[") ? Type.getType(type) : Type.getObjectType(type); + if (transformationContext.isSubmissionClass(paramType.getInternalName())) { + if (paramType.getSort() == Type.OBJECT) { + type = transformationContext.getSubmissionClassInfo(paramType.getInternalName()).getComputedClassName(); + } else { // else must be array + Type elementType = Type.getObjectType(transformationContext.getSubmissionClassInfo(paramType.getElementType().getInternalName()) + .getComputedClassName()); + type = "[".repeat(paramType.getDimensions()) + elementType.getDescriptor(); + } + } + super.visitTypeInsn(opcode, type); + } + /** * Builds an {@link Invocation} in bytecode. * @@ -443,4 +537,144 @@ private void buildConstructorInvocationBranch(MethodHeader constructorHeader, labels[labelIndex.get()], constructorInvocationIndex + 1); } + + // Prevent bytecode to be added to the method if there is a header mismatch + + @Override + public void visitFrame(int type, int numLocal, Object[] local, int numStack, Object[] stack) { + if (!headerMismatch) { + Object[] computedLocals = Arrays.stream(local) + .map(o -> { + if (o instanceof String s && transformationContext.isSubmissionClass(s)) { + return TransformationUtils.getComputedName(transformationContext, s); + } else { + return o; + } + }) + .toArray(); + Object[] computedStack = Arrays.stream(stack) + .map(o -> { + if (o instanceof String s && transformationContext.isSubmissionClass(s)) { + return TransformationUtils.getComputedName(transformationContext, s); + } else { + return o; + } + }) + .toArray(); + super.visitFrame(type, numLocal, computedLocals, numStack, computedStack); + } + } + + @Override + public void visitInsn(int opcode) { + if (!headerMismatch) { + super.visitInsn(opcode); + } + } + + @Override + public void visitIntInsn(int opcode, int operand) { + if (!headerMismatch) { + super.visitIntInsn(opcode, operand); + } + } + + @Override + public void visitIincInsn(int varIndex, int increment) { + if (!headerMismatch) { + super.visitIincInsn(varIndex, increment); + } + } + + @Override + public void visitVarInsn(int opcode, int varIndex) { + if (!headerMismatch) { + super.visitVarInsn(opcode, varIndex); + } + } + + @Override + public void visitJumpInsn(int opcode, Label label) { + if (!headerMismatch) { + super.visitJumpInsn(opcode, label); + } + } + + @Override + public void visitInvokeDynamicInsn(String name, String descriptor, Handle bootstrapMethodHandle, Object... bootstrapMethodArguments) { + if (!headerMismatch) { + super.visitInvokeDynamicInsn(name, descriptor, bootstrapMethodHandle, bootstrapMethodArguments); + } + } + + @Override + public void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels) { + if (!headerMismatch) { + super.visitLookupSwitchInsn(dflt, keys, labels); + } + } + + @Override + public void visitTableSwitchInsn(int min, int max, Label dflt, Label... labels) { + if (!headerMismatch) { + super.visitTableSwitchInsn(min, max, dflt, labels); + } + } + + @Override + public void visitMultiANewArrayInsn(String descriptor, int numDimensions) { + if (!headerMismatch) { + super.visitMultiANewArrayInsn(descriptor, numDimensions); + } + } + + @Override + public void visitLabel(Label label) { + if (!headerMismatch) { + 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) { + super.visitTryCatchBlock(start, end, handler, type); + } + } + + @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) { + super.visitLocalVariable(name, descriptor, 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) { + super.visitLineNumber(line, start); + } + } + + @Override + public void visitAttribute(Attribute attribute) { + if (!headerMismatch) { + super.visitAttribute(attribute); + } + } } From 9bc6a861da1c988dde42e694848e1eb6d79ef9a9 Mon Sep 17 00:00:00 2001 From: Daniel Mangold Date: Wed, 27 Nov 2024 14:08:43 +0100 Subject: [PATCH 19/48] Simplify header replication --- .../transform/SubmissionClassVisitor.java | 6 +- .../transform/SubmissionMethodVisitor.java | 2 +- .../algoutils/transform/util/ClassHeader.java | 5 ++ .../algoutils/transform/util/FieldHeader.java | 5 ++ .../algoutils/transform/util/Header.java | 7 ++ .../transform/util/MethodHeader.java | 5 ++ .../transform/util/TransformationUtils.java | 71 ++----------------- 7 files changed, 33 insertions(+), 68 deletions(-) diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java index c5313007..91c0f7a8 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java @@ -239,7 +239,7 @@ private void injectClassMetadata() { MethodVisitor mv = Constants.INJECTED_GET_ORIGINAL_CLASS_HEADER.toMethodVisitor(getDelegate()); mv.visitLabel(startLabel); - int maxStack = buildClassHeader(mv, classHeader); + int maxStack = buildHeader(mv, classHeader); mv.visitInsn(ARETURN); mv.visitLabel(endLabel); mv.visitLocalVariable("this", @@ -275,7 +275,7 @@ private void injectFieldMetadata() { maxStack = Math.max(maxStack, ++stackSize); mv.visitIntInsn(SIPUSH, i++); maxStack = Math.max(maxStack, ++stackSize); - int stackSizeUsed = buildFieldHeader(mv, fieldHeader); + int stackSizeUsed = buildHeader(mv, fieldHeader); maxStack = Math.max(maxStack, stackSize++ + stackSizeUsed); mv.visitInsn(AASTORE); stackSize -= 3; @@ -320,7 +320,7 @@ private void injectMethodMetadata() { maxStack = Math.max(maxStack, ++stackSize); mv.visitIntInsn(SIPUSH, i++); maxStack = Math.max(maxStack, ++stackSize); - int stackSizeUsed = buildMethodHeader(mv, methodHeader); + int stackSizeUsed = buildHeader(mv, methodHeader); maxStack = Math.max(maxStack, stackSize++ + stackSizeUsed); mv.visitInsn(AASTORE); stackSize -= 3; diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionMethodVisitor.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionMethodVisitor.java index 7aa348e4..40230a3e 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionMethodVisitor.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionMethodVisitor.java @@ -120,7 +120,7 @@ public void visitCode() { super.visitLabel(submissionExecutionHandlerVarLabel); // replicate method header in bytecode and store in locals array - buildMethodHeader(getDelegate(), computedMethodHeader); + buildHeader(getDelegate(), computedMethodHeader); super.visitVarInsn(ASTORE, methodHeaderIndex); super.visitLabel(methodHeaderVarLabel); diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/ClassHeader.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/ClassHeader.java index ecd44d1c..35939e1a 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/ClassHeader.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/ClassHeader.java @@ -32,6 +32,11 @@ public Type[] getConstructorParameterTypes() { return Constants.CLASS_HEADER_CONSTRUCTOR_TYPES; } + @Override + public String[] getRecordComponents() { + return new String[] {"access", "name", "signature", "superName", "interfaces"}; + } + @Override public Object getValue(String name) { return switch (name) { diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/FieldHeader.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/FieldHeader.java index 8d028308..b93c0bcd 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/FieldHeader.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/FieldHeader.java @@ -43,6 +43,11 @@ public Type[] getConstructorParameterTypes() { return Constants.FIELD_HEADER_CONSTRUCTOR_TYPES; } + @Override + public String[] getRecordComponents() { + return new String[] {"owner", "access", "name", "descriptor", "signature"}; + } + @Override public Object getValue(String name) { return switch (name) { diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Header.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Header.java index 3ce4ac82..d65b4078 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Header.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Header.java @@ -21,6 +21,13 @@ public interface Header { */ Type[] getConstructorParameterTypes(); + /** + * Returns the values that can be passed to {@link #getValue(String)}. + * + * @return the values + */ + String[] getRecordComponents(); + /** * Returns the stored value for the given record component's name. * diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/MethodHeader.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/MethodHeader.java index e13e76f8..be72f1be 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/MethodHeader.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/MethodHeader.java @@ -59,6 +59,11 @@ public Type[] getConstructorParameterTypes() { return Constants.METHOD_HEADER_CONSTRUCTOR_TYPES; } + @Override + public String[] getRecordComponents() { + return new String[] {"owner", "access", "name", "descriptor", "signature", "exceptions"}; + } + @Override public Object getValue(String name) { return switch (name) { 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 index 339b65e1..8753b4f9 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationUtils.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationUtils.java @@ -105,48 +105,6 @@ public static int getLocalsIndex(Type[] types, int index) { return localsIndex; } - /** - * Builds a class header with bytecode instructions using the given method visitor and information - * stored in the given class header. - * Upon return, a reference to the newly created {@link ClassHeader} object is located at - * the top of the method visitor's stack. - * - * @param mv the method visitor to use - * @param classHeader the class header to replicate in bytecode - * @return the maximum stack size used during the operation - */ - public static int buildClassHeader(MethodVisitor mv, ClassHeader classHeader) { - return buildHeader(mv, classHeader, "access", "name", "signature", "superName", "interfaces"); - } - - /** - * Builds a field header with bytecode instructions using the given method visitor and information - * stored in the given field header. - * Upon return, a reference to the newly created {@link FieldHeader} object is located at - * the top of the method visitor's stack. - * - * @param mv the method visitor to use - * @param fieldHeader the field header to replicate in bytecode - * @return the maximum stack size used during the operation - */ - public static int buildFieldHeader(MethodVisitor mv, FieldHeader fieldHeader) { - return buildHeader(mv, fieldHeader, "owner", "access", "name", "descriptor", "signature"); - } - - /** - * Builds a method header with bytecode instructions using the given method visitor and information - * stored in the given method header. - * Upon return, a reference to the newly created {@link MethodHeader} object is located at - * the top of the method visitor's stack. - * - * @param mv the method visitor to use - * @param methodHeader the method header to replicate in bytecode - * @return the maximum stack size used during the operation - */ - public static int buildMethodHeader(MethodVisitor mv, MethodHeader methodHeader) { - return buildHeader(mv, methodHeader, "owner", "access", "name", "descriptor", "signature", "exceptions"); - } - /** * Attempts to read and process a solution class from {@code resources/classes/}. * @@ -316,22 +274,12 @@ public static Type getComputedType(TransformationContext transformationContext, } } - public static void buildExceptionForHeaderMismatch(MethodVisitor mv, String message, T expected, T actual) { + public static void buildExceptionForHeaderMismatch(MethodVisitor mv, String message, Header expected, Header actual) { mv.visitTypeInsn(NEW, Type.getInternalName(AssertionFailedError.class)); mv.visitInsn(DUP); mv.visitLdcInsn(message); - if (expected instanceof ClassHeader) { - TransformationUtils.buildClassHeader(mv, (ClassHeader) expected); - TransformationUtils.buildClassHeader(mv, (ClassHeader) actual); - } else if (expected instanceof FieldHeader) { - TransformationUtils.buildFieldHeader(mv, (FieldHeader) expected); - TransformationUtils.buildFieldHeader(mv, (FieldHeader) actual); - } else if (expected instanceof MethodHeader) { - TransformationUtils.buildMethodHeader(mv, (MethodHeader) expected); - TransformationUtils.buildMethodHeader(mv, (MethodHeader) actual); - } else { - throw new IllegalArgumentException("Unsupported header type: " + expected.getClass()); - } + buildHeader(mv, expected); + buildHeader(mv, actual); mv.visitMethodInsn(INVOKESPECIAL, Type.getInternalName(AssertionFailedError.class), "", @@ -354,27 +302,22 @@ public static void getDefaultValue(MethodVisitor mv, Type type) { * 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. - *
    - * Note: The number of keys must equal the length of the array returned by - * {@link Header#getConstructorParameterTypes()}. - * Furthermore, the result of calling {@link Header#getValue(String)} with {@code keys[i]} must - * be assignable to the constructor parameter type at index {@code i}. * * @param mv the method visitor to use * @param header the header object to replicate in bytecode - * @param keys the keys to get values for * @return the maximum stack size used during the operation */ - private static int buildHeader(MethodVisitor mv, Header header, String... keys) { + public static int buildHeader(MethodVisitor mv, Header header) { Type headerType = header.getType(); Type[] constructorParameterTypes = header.getConstructorParameterTypes(); + String[] components = header.getRecordComponents(); int maxStack, stackSize; mv.visitTypeInsn(NEW, header.getType().getInternalName()); mv.visitInsn(DUP); maxStack = stackSize = 2; - for (int i = 0; i < keys.length; i++) { - Object value = header.getValue(keys[i]); + for (int i = 0; i < components.length; i++) { + Object value = header.getValue(components[i]); if (constructorParameterTypes[i].equals(Constants.STRING_ARRAY_TYPE)) { int stackUsed = buildArray(mv, Constants.STRING_TYPE, (Object[]) value); maxStack = Math.max(maxStack, stackSize++ + stackUsed); From 98111aa95185b67d69863163d4447fcff3bb5d08 Mon Sep 17 00:00:00 2001 From: Daniel Mangold Date: Thu, 28 Nov 2024 16:55:26 +0100 Subject: [PATCH 20/48] Allow defining alternative names for solution class matching --- .../SolutionMergingClassTransformer.java | 22 +++++++++---- .../transform/SubmissionClassInfo.java | 4 +-- .../transform/SubmissionClassVisitor.java | 4 +-- .../transform/SubmissionMethodVisitor.java | 12 +++---- .../transform/util/SimilarityMapper.java | 24 ++++++++++++-- .../transform/util/TransformationContext.java | 33 ++++++++++++++++--- .../transform/util/TransformationUtils.java | 24 -------------- 7 files changed, 75 insertions(+), 48 deletions(-) diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionMergingClassTransformer.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionMergingClassTransformer.java index a27b13a8..4ea6761a 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionMergingClassTransformer.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionMergingClassTransformer.java @@ -81,7 +81,9 @@ public class SolutionMergingClassTransformer implements ClassTransformer { * @param availableSolutionClasses the list of solution class names (fully qualified) to use */ public SolutionMergingClassTransformer(String projectPrefix, String... availableSolutionClasses) { - this(new Builder(projectPrefix, availableSolutionClasses)); + this(Arrays.stream(availableSolutionClasses).reduce(new Builder(projectPrefix), + Builder::addSolutionClass, + (builder, builder2) -> builder)); } /** @@ -97,8 +99,7 @@ private SolutionMergingClassTransformer(Builder builder) { this.transformationContext = new TransformationContext(Collections.unmodifiableMap(builder.configuration), solutionClasses, submissionClasses); - ((List) builder.configuration.get(Config.SOLUTION_CLASSES)).stream() - .map(s -> s.replace('.', '/')) + ((Map>) builder.configuration.get(Config.SOLUTION_CLASSES)).keySet() .forEach(className -> solutionClasses.put(className, TransformationUtils.readSolutionClass(transformationContext, className))); } @@ -153,7 +154,7 @@ public void transform(ClassReader reader, ClassWriter writer) { */ public enum Config { PROJECT_PREFIX(null), - SOLUTION_CLASSES(null), + SOLUTION_CLASSES(new HashMap<>()), SIMILARITY(0.90), METHOD_REPLACEMENTS(new HashMap()); @@ -175,14 +176,21 @@ public static class Builder { * Constructs a new {@link Builder}. * * @param projectPrefix the root package containing all submission classes, usually the sheet number - * @param solutionClasses the list of solution class names (fully qualified) to use */ - public Builder(String projectPrefix, String... solutionClasses) { + public Builder(String projectPrefix) { for (Config config : Config.values()) { configuration.put(config, config.defaultValue); } configuration.put(Config.PROJECT_PREFIX, projectPrefix); - configuration.put(Config.SOLUTION_CLASSES, List.of(solutionClasses)); + } + + @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; } /** diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassInfo.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassInfo.java index de86ac4c..e60e2079 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassInfo.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassInfo.java @@ -205,9 +205,9 @@ public void mapToSolutionClass() { submissionFieldHeader.access(), submissionFieldHeader.name(), fieldType.getSort() == Type.OBJECT ? - "L" + TransformationUtils.getComputedName(transformationContext, fieldType.getInternalName()) + ";" : + "L" + transformationContext.getComputedName(fieldType.getInternalName()) + ";" : fieldType.getSort() == Type.ARRAY ? - TransformationUtils.getComputedName(transformationContext, fieldType.getInternalName()) : + transformationContext.getComputedName(fieldType.getInternalName()) : submissionFieldHeader.descriptor(), submissionFieldHeader.signature()); if (fsAnnotationProcessor.fieldIdentifierIsForced(submissionFieldHeader.name())) { diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java index 91c0f7a8..18bfc5c0 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java @@ -174,9 +174,9 @@ public MethodVisitor visitMethod(int access, String name, String descriptor, Str if ((access & ACC_SYNTHETIC) != 0 && originalMethodHeader.name().startsWith("lambda$")) { return originalMethodHeader.toMethodVisitor(getDelegate()); } else if ((originalMethodHeader.access() & ACC_STATIC) != (computedMethodHeader.access() & ACC_STATIC)) { - Type returnType = TransformationUtils.getComputedType(transformationContext, Type.getReturnType(descriptor)); + Type returnType = transformationContext.getComputedType(Type.getReturnType(descriptor)); Type[] parameterTypes = Arrays.stream(Type.getArgumentTypes(descriptor)) - .map(type -> TransformationUtils.getComputedType(transformationContext, type)) + .map(transformationContext::getComputedType) .toArray(Type[]::new); MethodHeader methodHeader = new MethodHeader(computedMethodHeader.owner(), access, diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionMethodVisitor.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionMethodVisitor.java index 40230a3e..8db410cc 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionMethodVisitor.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionMethodVisitor.java @@ -354,14 +354,14 @@ public void visitFieldInsn(int opcode, String owner, String name, String descrip super.visitFieldInsn(opcode, "[".repeat(ownerType.getDimensions()) + actualOwner.getDescriptor(), name, - TransformationUtils.getComputedType(transformationContext, Type.getType(descriptor)).getDescriptor()); + transformationContext.getComputedType(Type.getType(descriptor)).getDescriptor()); } else { FieldHeader computedFieldHeader = transformationContext.getSubmissionClassInfo(owner).getComputedFieldHeader(name); boolean isStaticOpcode = opcode == GETSTATIC || opcode == PUTSTATIC; boolean isStaticField = (computedFieldHeader.access() & ACC_STATIC) != 0; if (isStaticOpcode == isStaticField) { super.visitFieldInsn(opcode, - TransformationUtils.getComputedName(transformationContext, computedFieldHeader.owner()), + transformationContext.getComputedName(computedFieldHeader.owner()), computedFieldHeader.name(), computedFieldHeader.descriptor()); } else { // if incompatible @@ -391,9 +391,9 @@ public void visitMethodInsn(int opcode, String owner, String name, String descri if ((opcode == INVOKESTATIC) == ((methodHeader.access() & ACC_STATIC) != 0)) { super.visitMethodInsn(opcode, methodHeader.owner(), methodHeader.name(), methodHeader.descriptor(), isInterface); } else { - Type returnType = TransformationUtils.getComputedType(transformationContext, Type.getReturnType(descriptor)); + Type returnType = transformationContext.getComputedType(Type.getReturnType(descriptor)); Type[] parameterTypes = Arrays.stream(Type.getArgumentTypes(descriptor)) - .map(type -> TransformationUtils.getComputedType(transformationContext, type)) + .map(transformationContext::getComputedType) .toArray(Type[]::new); super.visitMethodInsn(opcode, methodHeader.owner(), @@ -546,7 +546,7 @@ public void visitFrame(int type, int numLocal, Object[] local, int numStack, Obj Object[] computedLocals = Arrays.stream(local) .map(o -> { if (o instanceof String s && transformationContext.isSubmissionClass(s)) { - return TransformationUtils.getComputedName(transformationContext, s); + return transformationContext.getComputedName(s); } else { return o; } @@ -555,7 +555,7 @@ public void visitFrame(int type, int numLocal, Object[] local, int numStack, Obj Object[] computedStack = Arrays.stream(stack) .map(o -> { if (o instanceof String s && transformationContext.isSubmissionClass(s)) { - return TransformationUtils.getComputedName(transformationContext, s); + return transformationContext.getComputedName(s); } else { return o; } diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/SimilarityMapper.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/SimilarityMapper.java index 2bbc1ec2..96f730cf 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/SimilarityMapper.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/SimilarityMapper.java @@ -4,14 +4,34 @@ import java.util.*; import java.util.function.Function; +import java.util.stream.Stream; public class SimilarityMapper { private final Map bestMatches = new HashMap<>(); @SuppressWarnings("unchecked") - public SimilarityMapper(Collection from, Collection to, double similarityThreshold) { - this((Collection) from, (Collection) to, similarityThreshold, s -> (String) s); + public SimilarityMapper(Collection from, Map> to, double similarityThreshold) { + List rowMapping = new ArrayList<>(from); + List columnMapping = new ArrayList<>(to.keySet()); + double[][] similarityMatrix = new double[from.size()][to.size()]; + + for (int i = 0; i < similarityMatrix.length; i++) { + final int finalI = i; + int bestMatchIndex = -1; + double bestSimilarity = similarityThreshold; + for (int j = 0; j < similarityMatrix[i].length; j++) { + similarityMatrix[i][j] = Stream.concat(Stream.of(columnMapping.get(j)), to.get(columnMapping.get(j)).stream()) + .mapToDouble(value -> MatchingUtils.similarity(rowMapping.get(finalI), value)) + .max() + .getAsDouble(); + if (similarityMatrix[i][j] >= bestSimilarity) { + bestMatchIndex = j; + bestSimilarity = similarityMatrix[i][j]; + } + } + bestMatches.put((T) rowMapping.get(i), bestMatchIndex >= 0 ? (T) columnMapping.get(bestMatchIndex) : null); + } } public SimilarityMapper(Collection from, 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 index 9e3077a4..bf07d05f 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationContext.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationContext.java @@ -120,18 +120,41 @@ public Map solutionClasses() { return solutionClasses; } - public Map submissionClasses() { - return submissionClasses; - } - + @SuppressWarnings("unchecked") public void computeClassesSimilarity() { - classSimilarityMapper = new SimilarityMapper<>(submissionClassNames, solutionClasses.keySet(), getSimilarity()); + classSimilarityMapper = new SimilarityMapper<>(submissionClassNames, + (Map>) configuration.get(SolutionMergingClassTransformer.Config.SOLUTION_CLASSES), + getSimilarity()); } public String getSolutionClassName(String submissionClassName) { return classSimilarityMapper.getBestMatch(submissionClassName); } + public String getComputedName(String className) { + if (isSubmissionClass(className)) { + Type type = className.startsWith("[") ? Type.getType(className) : Type.getObjectType(className); + if (type.getSort() == Type.OBJECT) { + return getSubmissionClassInfo(className).getComputedClassName(); + } else { // else must be array + return "%sL%s;".formatted("[".repeat(type.getDimensions()), + getSubmissionClassInfo(type.getElementType().getInternalName()).getComputedClassName()); + } + } else { + return className; + } + } + + public Type getComputedType(Type type) { + if (type.getSort() == Type.OBJECT) { + return Type.getObjectType(getComputedName(type.getInternalName())); + } else if (type.getSort() == Type.ARRAY) { + return Type.getType(getComputedName(type.getDescriptor())); + } else { + return type; + } + } + @Override public String toString() { return "TransformationContext[configuration=%s, solutionClasses=%s, submissionClasses=%s]" 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 index 8753b4f9..e52970c0 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationUtils.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationUtils.java @@ -250,30 +250,6 @@ public static String toHumanReadableType(Type type) { }; } - public static String getComputedName(TransformationContext transformationContext, String className) { - if (transformationContext.isSubmissionClass(className)) { - Type type = className.startsWith("[") ? Type.getType(className) : Type.getObjectType(className); - if (type.getSort() == Type.OBJECT) { - return transformationContext.getSubmissionClassInfo(className).getComputedClassName(); - } else { // else must be array - return "%sL%s;".formatted("[".repeat(type.getDimensions()), - transformationContext.getSubmissionClassInfo(type.getElementType().getInternalName()).getComputedClassName()); - } - } else { - return className; - } - } - - public static Type getComputedType(TransformationContext transformationContext, Type type) { - if (type.getSort() == Type.OBJECT) { - return Type.getObjectType(getComputedName(transformationContext, type.getInternalName())); - } else if (type.getSort() == Type.ARRAY) { - return Type.getType(getComputedName(transformationContext, type.getDescriptor())); - } else { - return type; - } - } - public static void buildExceptionForHeaderMismatch(MethodVisitor mv, String message, Header expected, Header actual) { mv.visitTypeInsn(NEW, Type.getInternalName(AssertionFailedError.class)); mv.visitInsn(DUP); From d2d1a8947dda0cf382619124205f86da71a8d2df Mon Sep 17 00:00:00 2001 From: Daniel Mangold Date: Fri, 29 Nov 2024 18:40:15 +0100 Subject: [PATCH 21/48] Refactoring, documentation, etc. --- .../transform/SolutionClassNode.java | 9 +- .../SolutionMergingClassTransformer.java | 17 +- .../transform/SubmissionClassInfo.java | 97 ++++---- .../transform/SubmissionClassVisitor.java | 113 ++++----- .../transform/SubmissionMethodVisitor.java | 117 +++------ .../algoutils/transform/util/ClassHeader.java | 25 +- .../algoutils/transform/util/Constants.java | 22 -- .../algoutils/transform/util/FieldHeader.java | 27 +- .../ForceSignatureAnnotationProcessor.java | 185 +++++++------- .../algoutils/transform/util/Header.java | 63 +++-- .../algoutils/transform/util/Invocation.java | 31 ++- .../transform/util/MethodHeader.java | 27 +- .../transform/util/SimilarityMapper.java | 27 +- .../transform/util/TransformationContext.java | 179 +++++++++++--- .../transform/util/TransformationUtils.java | 231 ++++++++---------- .../tutor/general/SubmissionInfo.java | 21 ++ 16 files changed, 600 insertions(+), 591 deletions(-) create mode 100644 tutor/src/main/java/org/tudalgo/algoutils/tutor/general/SubmissionInfo.java diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionClassNode.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionClassNode.java index e41e7215..ab9ba788 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionClassNode.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionClassNode.java @@ -1,8 +1,6 @@ package org.tudalgo.algoutils.transform; -import org.tudalgo.algoutils.transform.util.ClassHeader; -import org.tudalgo.algoutils.transform.util.FieldHeader; -import org.tudalgo.algoutils.transform.util.MethodHeader; +import org.tudalgo.algoutils.transform.util.*; import org.objectweb.asm.FieldVisitor; import org.objectweb.asm.Handle; import org.objectweb.asm.MethodVisitor; @@ -10,7 +8,6 @@ import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.FieldNode; import org.objectweb.asm.tree.MethodNode; -import org.tudalgo.algoutils.transform.util.TransformationContext; import java.util.Arrays; import java.util.HashMap; @@ -72,7 +69,7 @@ public void visit(int version, int access, String name, String signature, String @Override public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) { FieldHeader fieldHeader = new FieldHeader(className, access, name, descriptor, signature); - FieldNode fieldNode = (FieldNode) super.visitField(access & ~ACC_FINAL, name, descriptor, signature, value); + FieldNode fieldNode = (FieldNode) super.visitField(TransformationUtils.transformAccess(access), name, descriptor, signature, value); fields.put(fieldHeader, fieldNode); return fieldNode; } @@ -102,7 +99,7 @@ public MethodVisitor visitMethod(int access, String name, String descriptor, Str * @return a new {@link MethodNode} */ private MethodNode getMethodNode(int access, String name, String descriptor, String signature, String[] exceptions) { - return new MethodNode(ASM9, access, name, descriptor, signature, exceptions) { + return 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, 0, name, descriptor, null, null); diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionMergingClassTransformer.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionMergingClassTransformer.java index 4ea6761a..5ae2a0d1 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionMergingClassTransformer.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionMergingClassTransformer.java @@ -5,11 +5,11 @@ import org.tudalgo.algoutils.student.annotation.ForceSignature; import org.tudalgo.algoutils.transform.util.MethodHeader; import org.tudalgo.algoutils.transform.util.TransformationContext; -import org.tudalgo.algoutils.transform.util.TransformationUtils; 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; @@ -66,6 +66,7 @@ * @see SubmissionExecutionHandler * @author Daniel Mangold */ +@SuppressWarnings("unused") public class SolutionMergingClassTransformer implements ClassTransformer { /** @@ -100,7 +101,7 @@ private SolutionMergingClassTransformer(Builder builder) { solutionClasses, submissionClasses); ((Map>) builder.configuration.get(Config.SOLUTION_CLASSES)).keySet() - .forEach(className -> solutionClasses.put(className, TransformationUtils.readSolutionClass(transformationContext, className))); + .forEach(className -> solutionClasses.put(className, transformationContext.readSolutionClass(className))); } @Override @@ -114,7 +115,6 @@ public int getWriterFlags() { } @Override - @SuppressWarnings("unchecked") public void transform(ClassReader reader, ClassWriter writer) { if (!new JagrExecutionCondition().evaluateExecutionCondition(null).isDisabled()) { // if Jagr is present try { @@ -124,14 +124,11 @@ public void transform(ClassReader reader, ClassWriter writer) { transformationContext.setSubmissionClassLoader(submissionClassLoader); if (!readSubmissionClasses) { - Map submissionInfo = new ObjectMapper() - .readValue(submissionClassLoader.getResourceAsStream("submission-info.json"), Map.class); - Set submissionClassNames = ((List>) submissionInfo.get("sourceSets")).stream() - .filter(sourceSet -> sourceSet.get("name").equals("main")) - .findAny() - .map(sourceSet -> ((Map>) sourceSet.get("files")).get("java")) - .orElseThrow() + Set submissionClassNames = new ObjectMapper() + .readValue(submissionClassLoader.getResourceAsStream("submission-info.json"), SubmissionInfo.class) + .sourceSets() .stream() + .flatMap(sourceSet -> sourceSet.files().get("java").stream()) .map(submissionClassName -> submissionClassName.replaceAll("\\.java$", "")) .collect(Collectors.toSet()); transformationContext.setSubmissionClassNames(submissionClassNames); diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassInfo.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassInfo.java index e60e2079..75127c31 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassInfo.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassInfo.java @@ -7,11 +7,10 @@ import java.lang.reflect.Modifier; import java.util.*; import java.util.function.Function; +import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; -import static org.objectweb.asm.Opcodes.ACC_SYNTHETIC; - /** * A class that holds information on a submission class. * This class will attempt to find a corresponding solution class and map its members @@ -24,16 +23,12 @@ public class SubmissionClassInfo extends ClassVisitor { private final TransformationContext transformationContext; - private final String originalClassName; - private final String computedClassName; - private final Set, Map>> superClassMembers = new HashSet<>(); private final ForceSignatureAnnotationProcessor fsAnnotationProcessor; - private final SolutionClassNode solutionClass; - - private String superClass; - private String[] interfaces; + private final Set, Map>> superClassMembers = new HashSet<>(); - private ClassHeader submissionClassHeader; + private ClassHeader originalClassHeader; + private ClassHeader computedClassHeader; + private SolutionClassNode solutionClass; // Mapping of fields in submission => usable fields private final Map fields = new HashMap<>(); @@ -47,25 +42,13 @@ public class SubmissionClassInfo extends ClassVisitor { * Constructs a new {@link SubmissionClassInfo} instance. * * @param transformationContext a {@link TransformationContext} object - * @param className the name of the submission class * @param fsAnnotationProcessor a {@link ForceSignatureAnnotationProcessor} for the submission class */ public SubmissionClassInfo(TransformationContext transformationContext, - String className, ForceSignatureAnnotationProcessor fsAnnotationProcessor) { super(Opcodes.ASM9); this.transformationContext = transformationContext; - this.originalClassName = className; this.fsAnnotationProcessor = fsAnnotationProcessor; - - if (fsAnnotationProcessor.classIdentifierIsForced()) { - this.computedClassName = fsAnnotationProcessor.forcedClassIdentifier(); - } else { - // If not forced, get the closest matching solution class - this.computedClassName = Optional.ofNullable(transformationContext.getSolutionClassName(originalClassName)) - .orElse(originalClassName); - } - this.solutionClass = transformationContext.solutionClasses().get(computedClassName); } /** @@ -74,7 +57,7 @@ public SubmissionClassInfo(TransformationContext transformationContext, * @return the original class header */ public ClassHeader getOriginalClassHeader() { - return submissionClassHeader; + return originalClassHeader; } /** @@ -84,8 +67,8 @@ public ClassHeader getOriginalClassHeader() { * * @return the computed class name */ - public String getComputedClassName() { - return computedClassName; + public ClassHeader getComputedClassHeader() { + return computedClassHeader; } /** @@ -169,20 +152,28 @@ public MethodHeader getComputedSuperClassConstructorHeader(String descriptor) { @Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { - submissionClassHeader = new ClassHeader(access, name, signature, superName, interfaces); - resolveSuperClassMembers(superClassMembers, this.superClass = superName, this.interfaces = interfaces); + 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(originalClassName, access, name, descriptor, signature), null); + 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(originalClassName, access, name, descriptor, signature, exceptions); - if ((access & ACC_SYNTHETIC) != 0 && name.startsWith("lambda$")) { // if method is lambda + MethodHeader submissionMethodHeader = new MethodHeader(originalClassHeader.name(), access, name, descriptor, signature, exceptions); + if (TransformationUtils.isLambdaMethod(access, name)) { methods.put(submissionMethodHeader, submissionMethodHeader); return null; } @@ -191,73 +182,73 @@ public MethodVisitor visitMethod(int access, String name, String descriptor, Str return null; } - public void mapToSolutionClass() { + public void computeMembers() { SimilarityMapper fieldsSimilarityMapper = new SimilarityMapper<>( fields.keySet(), - solutionClass == null ? Collections.emptyList() : solutionClass.getFields().keySet(), + getSolutionClass().map(solutionClass -> solutionClass.getFields().keySet()).orElseGet(Collections::emptySet), transformationContext.getSimilarity(), FieldHeader::name ); for (FieldHeader submissionFieldHeader : fields.keySet()) { - FieldHeader solutionFieldHeader; - Type fieldType = Type.getType(submissionFieldHeader.descriptor()); - FieldHeader fallbackFieldHeader = new FieldHeader(computedClassName, + Supplier fallbackFieldHeader = () -> new FieldHeader(computedClassHeader.name(), submissionFieldHeader.access(), submissionFieldHeader.name(), - fieldType.getSort() == Type.OBJECT ? - "L" + transformationContext.getComputedName(fieldType.getInternalName()) + ";" : - fieldType.getSort() == Type.ARRAY ? - transformationContext.getComputedName(fieldType.getInternalName()) : - submissionFieldHeader.descriptor(), + 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 -> fieldHeader.equals(fieldsSimilarityMapper.getBestMatch(submissionFieldHeader))) + .filter(fieldHeader -> fieldsSimilarityMapper.getBestMatch(submissionFieldHeader) + .map(fieldHeader::equals) + .orElse(false)) .findAny() - .orElse(fallbackFieldHeader); + .orElseGet(fallbackFieldHeader); } else { - solutionFieldHeader = fallbackFieldHeader; + solutionFieldHeader = fallbackFieldHeader.get(); } fields.put(submissionFieldHeader, solutionFieldHeader); } SimilarityMapper methodsSimilarityMapper = new SimilarityMapper<>( methods.keySet(), - solutionClass == null ? Collections.emptyList() : solutionClass.getMethods().keySet(), + getSolutionClass().map(solutionClass -> solutionClass.getMethods().keySet()).orElseGet(Collections::emptySet), transformationContext.getSimilarity(), methodHeader -> methodHeader.name() + methodHeader.descriptor() ); for (MethodHeader submissionMethodHeader : methods.keySet()) { String submissionMethodName = submissionMethodHeader.name(); String submissionMethodDescriptor = submissionMethodHeader.descriptor(); - MethodHeader solutionMethodHeader; - MethodHeader fallbackMethodHeader = new MethodHeader(computedClassName, + Supplier fallbackMethodHeader = () -> new MethodHeader(computedClassHeader.name(), submissionMethodHeader.access(), submissionMethodHeader.name(), - submissionMethodHeader.descriptor(), + transformationContext.toComputedType(submissionMethodHeader.descriptor()).getDescriptor(), 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 -> methodHeader.equals(methodsSimilarityMapper.getBestMatch(submissionMethodHeader))) + .filter(methodHeader -> methodsSimilarityMapper.getBestMatch(submissionMethodHeader) + .map(methodHeader::equals) + .orElse(false)) .findAny() - .orElse(fallbackMethodHeader); + .orElseGet(fallbackMethodHeader); } else { - solutionMethodHeader = fallbackMethodHeader; + solutionMethodHeader = fallbackMethodHeader.get(); } methods.put(submissionMethodHeader, solutionMethodHeader); } + resolveSuperClassMembers(superClassMembers, originalClassHeader.superName(), originalClassHeader.interfaces()); for (Triple, Map> triple : superClassMembers) { - if (triple.getFirst().equals(superClass)) { + if (triple.getFirst().equals(originalClassHeader.superName())) { triple.getThird() .entrySet() .stream() @@ -314,7 +305,9 @@ private void resolveSuperClassMembers(Set !Modifier.isPrivate(entry.getKey().access())) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)))); - resolveSuperClassMembers(superClassMembers, submissionClassInfo.superClass, submissionClassInfo.interfaces); + resolveSuperClassMembers(superClassMembers, + submissionClassInfo.originalClassHeader.superName(), + submissionClassInfo.originalClassHeader.interfaces()); } else { try { Class clazz = Class.forName(className.replace('/', '.')); diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java index 18bfc5c0..0de8ce66 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java @@ -2,13 +2,10 @@ import org.tudalgo.algoutils.transform.util.*; import org.objectweb.asm.*; -import org.objectweb.asm.tree.FieldNode; -import org.objectweb.asm.tree.MethodNode; import java.util.*; import java.util.stream.Collectors; -import static org.tudalgo.algoutils.transform.util.TransformationUtils.*; import static org.objectweb.asm.Opcodes.*; /** @@ -103,36 +100,22 @@ */ class SubmissionClassVisitor extends ClassVisitor { - private final boolean defaultTransformationsOnly; private final TransformationContext transformationContext; - private final String className; private final SubmissionClassInfo submissionClassInfo; + private final ClassHeader originalClassHeader; + private final ClassHeader computedClassHeader; private final Set visitedFields = new HashSet<>(); - private final Map solutionFieldNodes; - private final Set visitedMethods = new HashSet<>(); - private final Map solutionMethodNodes; SubmissionClassVisitor(ClassVisitor classVisitor, TransformationContext transformationContext, String submissionClassName) { super(ASM9, classVisitor); this.transformationContext = transformationContext; - this.className = transformationContext.getSubmissionClassInfo(submissionClassName).getComputedClassName(); this.submissionClassInfo = transformationContext.getSubmissionClassInfo(submissionClassName); - - Optional solutionClass = submissionClassInfo.getSolutionClass(); - if (solutionClass.isPresent()) { - this.defaultTransformationsOnly = false; - this.solutionFieldNodes = solutionClass.get().getFields(); - this.solutionMethodNodes = solutionClass.get().getMethods(); - } else { - System.err.printf("No corresponding solution class found for %s. Only applying default transformations.%n", submissionClassName); - this.defaultTransformationsOnly = true; - this.solutionFieldNodes = Collections.emptyMap(); - this.solutionMethodNodes = Collections.emptyMap(); - } + this.originalClassHeader = submissionClassInfo.getOriginalClassHeader(); + this.computedClassHeader = submissionClassInfo.getComputedClassHeader(); } /** @@ -142,7 +125,7 @@ class SubmissionClassVisitor extends ClassVisitor { public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { submissionClassInfo.getSolutionClass() .map(SolutionClassNode::getClassHeader) - .orElse(submissionClassInfo.getOriginalClassHeader()) + .orElse(originalClassHeader) .visitClass(getDelegate(), version); } @@ -153,11 +136,15 @@ public void visit(int version, int access, String name, String signature, String public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) { FieldHeader fieldHeader = submissionClassInfo.getComputedFieldHeader(name); - if ((access & ACC_STATIC) == (fieldHeader.access() & ACC_STATIC)) { + if (TransformationUtils.contextIsCompatible(access, fieldHeader.access())) { visitedFields.add(fieldHeader); return fieldHeader.toFieldVisitor(getDelegate(), value); } else { - return super.visitField(access & ~ACC_FINAL, name + "$submission", fieldHeader.descriptor(), fieldHeader.signature(), value); + return super.visitField(TransformationUtils.transformAccess(access), + name + "$submission", + fieldHeader.descriptor(), + fieldHeader.signature(), + value); } } @@ -167,35 +154,33 @@ public FieldVisitor visitField(int access, String name, String descriptor, Strin */ @Override public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { - MethodHeader originalMethodHeader = new MethodHeader(submissionClassInfo.getOriginalClassHeader().name(), access, name, descriptor, signature, exceptions); - MethodHeader computedMethodHeader = submissionClassInfo.getComputedMethodHeader(name, descriptor); - // if method is lambda, skip transformation - if ((access & ACC_SYNTHETIC) != 0 && originalMethodHeader.name().startsWith("lambda$")) { - return originalMethodHeader.toMethodVisitor(getDelegate()); - } else if ((originalMethodHeader.access() & ACC_STATIC) != (computedMethodHeader.access() & ACC_STATIC)) { - Type returnType = transformationContext.getComputedType(Type.getReturnType(descriptor)); - Type[] parameterTypes = Arrays.stream(Type.getArgumentTypes(descriptor)) - .map(transformationContext::getComputedType) - .toArray(Type[]::new); - MethodHeader methodHeader = new MethodHeader(computedMethodHeader.owner(), - access, - name + "$submission", - Type.getMethodDescriptor(returnType, parameterTypes), - signature, - exceptions); - return new SubmissionMethodVisitor(methodHeader.toMethodVisitor(getDelegate()), - transformationContext, - submissionClassInfo, - originalMethodHeader, - methodHeader); + if (TransformationUtils.isLambdaMethod(access, name)) { + return super.visitMethod(access, name, descriptor, signature, exceptions); } else { - visitedMethods.add(computedMethodHeader); - return new SubmissionMethodVisitor(computedMethodHeader.toMethodVisitor(getDelegate()), - transformationContext, - submissionClassInfo, - originalMethodHeader, - submissionClassInfo.getComputedMethodHeader(originalMethodHeader.name(), originalMethodHeader.descriptor())); + MethodHeader originalMethodHeader = new MethodHeader(originalClassHeader.name(), access, name, descriptor, signature, exceptions); + MethodHeader computedMethodHeader = submissionClassInfo.getComputedMethodHeader(name, descriptor); + + if (!TransformationUtils.contextIsCompatible(access, computedMethodHeader.access())) { + MethodHeader methodHeader = new MethodHeader(computedMethodHeader.owner(), + access, + name + "$submission", + transformationContext.toComputedType(descriptor).getDescriptor(), + signature, + exceptions); + return new SubmissionMethodVisitor(methodHeader.toMethodVisitor(getDelegate()), + transformationContext, + submissionClassInfo, + originalMethodHeader, + methodHeader); + } else { + visitedMethods.add(computedMethodHeader); + return new SubmissionMethodVisitor(computedMethodHeader.toMethodVisitor(getDelegate()), + transformationContext, + submissionClassInfo, + originalMethodHeader, + computedMethodHeader); + } } } @@ -206,15 +191,20 @@ public MethodVisitor visitMethod(int access, String name, String descriptor, Str */ @Override public void visitEnd() { - if (!defaultTransformationsOnly) { + Optional solutionClass = submissionClassInfo.getSolutionClass(); + if (solutionClass.isPresent()) { // add missing fields - solutionFieldNodes.entrySet() + 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) - solutionMethodNodes.entrySet() + solutionClass.get() + .getMethods() + .entrySet() .stream() .filter(entry -> !visitedMethods.contains(entry.getKey())) .map(Map.Entry::getValue) @@ -233,17 +223,16 @@ public void visitEnd() { * This injected method returns the original class header of the class pre-transformation. */ private void injectClassMetadata() { - ClassHeader classHeader = submissionClassInfo.getOriginalClassHeader(); Label startLabel = new Label(); Label endLabel = new Label(); MethodVisitor mv = Constants.INJECTED_GET_ORIGINAL_CLASS_HEADER.toMethodVisitor(getDelegate()); mv.visitLabel(startLabel); - int maxStack = buildHeader(mv, classHeader); + int maxStack = originalClassHeader.buildHeader(mv); mv.visitInsn(ARETURN); mv.visitLabel(endLabel); mv.visitLocalVariable("this", - Type.getObjectType(className).getDescriptor(), + Type.getObjectType(computedClassHeader.name()).getDescriptor(), null, startLabel, endLabel, @@ -275,7 +264,7 @@ private void injectFieldMetadata() { maxStack = Math.max(maxStack, ++stackSize); mv.visitIntInsn(SIPUSH, i++); maxStack = Math.max(maxStack, ++stackSize); - int stackSizeUsed = buildHeader(mv, fieldHeader); + int stackSizeUsed = fieldHeader.buildHeader(mv); maxStack = Math.max(maxStack, stackSize++ + stackSizeUsed); mv.visitInsn(AASTORE); stackSize -= 3; @@ -288,7 +277,7 @@ private void injectFieldMetadata() { mv.visitInsn(ARETURN); mv.visitLabel(endLabel); mv.visitLocalVariable("this", - Type.getObjectType(className).getDescriptor(), + Type.getObjectType(computedClassHeader.name()).getDescriptor(), null, startLabel, endLabel, @@ -304,7 +293,7 @@ private void injectMethodMetadata() { Set methodHeaders = submissionClassInfo.getOriginalMethodHeaders() .stream() .filter(methodHeader -> (methodHeader.access() & ACC_SYNTHETIC) == 0) - .collect(Collectors.toSet());; + .collect(Collectors.toSet()); Label startLabel = new Label(); Label endLabel = new Label(); int maxStack, stackSize; @@ -320,7 +309,7 @@ private void injectMethodMetadata() { maxStack = Math.max(maxStack, ++stackSize); mv.visitIntInsn(SIPUSH, i++); maxStack = Math.max(maxStack, ++stackSize); - int stackSizeUsed = buildHeader(mv, methodHeader); + int stackSizeUsed = methodHeader.buildHeader(mv); maxStack = Math.max(maxStack, stackSize++ + stackSizeUsed); mv.visitInsn(AASTORE); stackSize -= 3; @@ -333,7 +322,7 @@ private void injectMethodMetadata() { mv.visitInsn(ARETURN); mv.visitLabel(endLabel); mv.visitLocalVariable("this", - Type.getObjectType(className).getDescriptor(), + Type.getObjectType(computedClassHeader.name()).getDescriptor(), null, startLabel, endLabel, diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionMethodVisitor.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionMethodVisitor.java index 8db410cc..1f5a4275 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionMethodVisitor.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionMethodVisitor.java @@ -1,11 +1,13 @@ package org.tudalgo.algoutils.transform; import org.objectweb.asm.*; +import org.objectweb.asm.tree.MethodNode; import org.tudalgo.algoutils.transform.util.*; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Optional; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -27,8 +29,6 @@ class SubmissionMethodVisitor extends MethodVisitor { private final MethodHeader originalMethodHeader; private final MethodHeader computedMethodHeader; private final SubmissionClassInfo submissionClassInfo; - private final String className; - private final boolean defaultTransformationsOnly; private final boolean headerMismatch; private final boolean isStatic; @@ -59,10 +59,6 @@ class SubmissionMethodVisitor extends MethodVisitor { this.submissionClassInfo = submissionClassInfo; this.originalMethodHeader = originalMethodHeader; this.computedMethodHeader = computedMethodHeader; - this.className = submissionClassInfo.getComputedClassName(); - this.defaultTransformationsOnly = submissionClassInfo.getSolutionClass() - .map(solutionClassNode -> !solutionClassNode.getMethods().containsKey(computedMethodHeader)) - .orElse(true); this.isStatic = (computedMethodHeader.access() & ACC_STATIC) != 0; this.isConstructor = computedMethodHeader.name().equals(""); @@ -84,7 +80,7 @@ class SubmissionMethodVisitor extends MethodVisitor { }) .collect(Collectors.toList()); if (!isStatic) { - this.fullFrameLocals.addFirst(isConstructor ? UNINITIALIZED_THIS : className); + this.fullFrameLocals.addFirst(isConstructor ? UNINITIALIZED_THIS : computedMethodHeader.owner()); } int[] originalParameterTypes = Arrays.stream(Type.getArgumentTypes(originalMethodHeader.descriptor())) @@ -100,6 +96,9 @@ class SubmissionMethodVisitor extends MethodVisitor { @Override public void visitCode() { + Optional solutionMethodNode = submissionClassInfo.getSolutionClass() + .map(solutionClassNode -> solutionClassNode.getMethods().get(computedMethodHeader)); + Label submissionExecutionHandlerVarLabel = new Label(); Label methodHeaderVarLabel = new Label(); Label substitutionCheckLabel = new Label(); @@ -120,7 +119,7 @@ public void visitCode() { super.visitLabel(submissionExecutionHandlerVarLabel); // replicate method header in bytecode and store in locals array - buildHeader(getDelegate(), computedMethodHeader); + computedMethodHeader.buildHeader(getDelegate()); super.visitVarInsn(ASTORE, methodHeaderIndex); super.visitLabel(methodHeaderVarLabel); @@ -156,7 +155,7 @@ public void visitCode() { super.visitVarInsn(ALOAD, submissionExecutionHandlerIndex); super.visitVarInsn(ALOAD, methodHeaderIndex); Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_USE_SUBSTITUTION.toMethodInsn(getDelegate(), false); - super.visitJumpInsn(IFEQ, defaultTransformationsOnly ? submissionCodeLabel : delegationCheckLabel); // jump to label if useSubstitution(...) == false + super.visitJumpInsn(IFEQ, solutionMethodNode.isPresent() ? delegationCheckLabel : submissionCodeLabel); // jump to label if useSubstitution(...) == false // get substitution and execute it super.visitVarInsn(ALOAD, submissionExecutionHandlerIndex); @@ -244,7 +243,7 @@ public void visitCode() { fullFrameLocals.removeLast(); List locals = new ArrayList<>(fullFrameLocals); - locals.set(0, className); + locals.set(0, computedMethodHeader.owner()); super.visitFrame(F_FULL, locals.size(), locals.toArray(), 0, null); super.visitLabel(substitutionExecuteLabel); super.visitLocalVariable("constructorInvocation", @@ -276,7 +275,7 @@ public void visitCode() { // Method delegation // if only default transformations are applied, skip delegation - if (!defaultTransformationsOnly) { + if (solutionMethodNode.isPresent()) { // check if call should be delegated to solution or not fullFrameLocals.removeLast(); super.visitFrame(F_FULL, fullFrameLocals.size(), fullFrameLocals.toArray(), 0, new Object[0]); @@ -303,8 +302,7 @@ public void visitCode() { methodHeaderVarLabel, delegationCodeLabel, methodHeaderIndex); - //noinspection OptionalGetWithoutIsPresent - submissionClassInfo.getSolutionClass().get().getMethods().get(computedMethodHeader).accept(getDelegate()); + solutionMethodNode.get().accept(getDelegate()); super.visitFrame(F_FULL, fullFrameLocals.size(), fullFrameLocals.toArray(), 0, new Object[0]); super.visitLabel(submissionCodeLabel); @@ -348,20 +346,15 @@ public void visitFieldInsn(int opcode, String owner, String name, String descrip super.visitFieldInsn(opcode, owner, name, descriptor); } else { if (owner.startsWith("[")) { // stupid edge cases - Type ownerType = Type.getType(owner); - Type actualOwner = Type.getObjectType(transformationContext.getSubmissionClassInfo(ownerType.getElementType().getInternalName()) - .getComputedClassName()); super.visitFieldInsn(opcode, - "[".repeat(ownerType.getDimensions()) + actualOwner.getDescriptor(), + transformationContext.toComputedType(owner).getInternalName(), name, - transformationContext.getComputedType(Type.getType(descriptor)).getDescriptor()); + transformationContext.toComputedType(descriptor).getDescriptor()); } else { FieldHeader computedFieldHeader = transformationContext.getSubmissionClassInfo(owner).getComputedFieldHeader(name); - boolean isStaticOpcode = opcode == GETSTATIC || opcode == PUTSTATIC; - boolean isStaticField = (computedFieldHeader.access() & ACC_STATIC) != 0; - if (isStaticOpcode == isStaticField) { + if (TransformationUtils.opcodeIsCompatible(opcode, computedFieldHeader.access())) { super.visitFieldInsn(opcode, - transformationContext.getComputedName(computedFieldHeader.owner()), + transformationContext.toComputedType(computedFieldHeader.owner()).getInternalName(), computedFieldHeader.name(), computedFieldHeader.descriptor()); } else { // if incompatible @@ -382,23 +375,20 @@ public void visitMethodInsn(int opcode, String owner, String name, String descri } else if (!transformationContext.isSubmissionClass(owner)) { super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); } else if (owner.startsWith("[")) { // stupid edge cases - Type ownerType = Type.getType(owner); - Type actualOwner = Type.getObjectType(transformationContext.getSubmissionClassInfo(ownerType.getElementType().getInternalName()) - .getComputedClassName()); - super.visitMethodInsn(opcode, "[".repeat(ownerType.getDimensions()) + actualOwner.getDescriptor(), name, descriptor, isInterface); + super.visitMethodInsn(opcode, + transformationContext.toComputedType(owner).getInternalName(), + name, + transformationContext.toComputedType(descriptor).getDescriptor(), + isInterface); } else { methodHeader = transformationContext.getSubmissionClassInfo(owner).getComputedMethodHeader(name, descriptor); - if ((opcode == INVOKESTATIC) == ((methodHeader.access() & ACC_STATIC) != 0)) { + if (TransformationUtils.opcodeIsCompatible(opcode, methodHeader.access())) { super.visitMethodInsn(opcode, methodHeader.owner(), methodHeader.name(), methodHeader.descriptor(), isInterface); } else { - Type returnType = transformationContext.getComputedType(Type.getReturnType(descriptor)); - Type[] parameterTypes = Arrays.stream(Type.getArgumentTypes(descriptor)) - .map(transformationContext::getComputedType) - .toArray(Type[]::new); super.visitMethodInsn(opcode, methodHeader.owner(), name + "$submission", - Type.getMethodDescriptor(returnType, parameterTypes), + transformationContext.toComputedType(descriptor).getDescriptor(), isInterface); } } @@ -408,33 +398,14 @@ public void visitMethodInsn(int opcode, String owner, String name, String descri public void visitLdcInsn(Object value) { if (headerMismatch) return; - if (value instanceof Type type && transformationContext.isSubmissionClass(type.getInternalName())) { - if (type.getSort() == Type.OBJECT) { - value = Type.getObjectType(transformationContext.getSubmissionClassInfo(type.getInternalName()).getComputedClassName()); - } else { // else must be array - Type elementType = Type.getObjectType(transformationContext.getSubmissionClassInfo(type.getElementType().getInternalName()) - .getComputedClassName()); - value = Type.getObjectType("[".repeat(type.getDimensions()) + elementType.getDescriptor()); - } - } - super.visitLdcInsn(value); + super.visitLdcInsn(value instanceof Type type ? transformationContext.toComputedType(type) : value); } @Override public void visitTypeInsn(int opcode, String type) { if (headerMismatch) return; - Type paramType = type.startsWith("[") ? Type.getType(type) : Type.getObjectType(type); - if (transformationContext.isSubmissionClass(paramType.getInternalName())) { - if (paramType.getSort() == Type.OBJECT) { - type = transformationContext.getSubmissionClassInfo(paramType.getInternalName()).getComputedClassName(); - } else { // else must be array - Type elementType = Type.getObjectType(transformationContext.getSubmissionClassInfo(paramType.getElementType().getInternalName()) - .getComputedClassName()); - type = "[".repeat(paramType.getDimensions()) + elementType.getDescriptor(); - } - } - super.visitTypeInsn(opcode, type); + super.visitTypeInsn(opcode, transformationContext.toComputedType(type).getInternalName()); } /** @@ -544,22 +515,10 @@ private void buildConstructorInvocationBranch(MethodHeader constructorHeader, public void visitFrame(int type, int numLocal, Object[] local, int numStack, Object[] stack) { if (!headerMismatch) { Object[] computedLocals = Arrays.stream(local) - .map(o -> { - if (o instanceof String s && transformationContext.isSubmissionClass(s)) { - return transformationContext.getComputedName(s); - } else { - return o; - } - }) + .map(o -> o instanceof String s ? transformationContext.toComputedType(s).getInternalName() : o) .toArray(); Object[] computedStack = Arrays.stream(stack) - .map(o -> { - if (o instanceof String s && transformationContext.isSubmissionClass(s)) { - return transformationContext.getComputedName(s); - } else { - return o; - } - }) + .map(o -> o instanceof String s ? transformationContext.toComputedType(s).getInternalName() : o) .toArray(); super.visitFrame(type, numLocal, computedLocals, numStack, computedStack); } @@ -602,9 +561,9 @@ public void visitJumpInsn(int opcode, Label label) { @Override public void visitInvokeDynamicInsn(String name, String descriptor, Handle bootstrapMethodHandle, Object... bootstrapMethodArguments) { - if (!headerMismatch) { - super.visitInvokeDynamicInsn(name, descriptor, bootstrapMethodHandle, bootstrapMethodArguments); - } + if (headerMismatch) return; + + super.visitInvokeDynamicInsn(name, descriptor, bootstrapMethodHandle, bootstrapMethodArguments); } @Override @@ -623,9 +582,9 @@ public void visitTableSwitchInsn(int min, int max, Label dflt, Label... labels) @Override public void visitMultiANewArrayInsn(String descriptor, int numDimensions) { - if (!headerMismatch) { - super.visitMultiANewArrayInsn(descriptor, numDimensions); - } + if (headerMismatch) return; + + super.visitMultiANewArrayInsn(transformationContext.toComputedType(descriptor).getDescriptor(), numDimensions); } @Override @@ -642,9 +601,9 @@ public AnnotationVisitor visitInsnAnnotation(int typeRef, TypePath typePath, Str @Override public void visitTryCatchBlock(Label start, Label end, Label handler, String type) { - if (!headerMismatch) { - super.visitTryCatchBlock(start, end, handler, type); - } + if (headerMismatch) return; + + super.visitTryCatchBlock(start, end, handler, type != null ? transformationContext.toComputedType(type).getInternalName() : null); } @Override @@ -654,9 +613,9 @@ public AnnotationVisitor visitTryCatchAnnotation(int typeRef, TypePath typePath, @Override public void visitLocalVariable(String name, String descriptor, String signature, Label start, Label end, int index) { - if (!headerMismatch) { - super.visitLocalVariable(name, descriptor, signature, start, end, index); - } + if (headerMismatch) return; + + super.visitLocalVariable(name, transformationContext.toComputedType(Type.getType(descriptor)).getDescriptor(), signature, start, end, index); } @Override diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/ClassHeader.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/ClassHeader.java index 35939e1a..dc757d5a 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/ClassHeader.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/ClassHeader.java @@ -28,24 +28,13 @@ public Type getType() { } @Override - public Type[] getConstructorParameterTypes() { - return Constants.CLASS_HEADER_CONSTRUCTOR_TYPES; - } - - @Override - public String[] getRecordComponents() { - return new String[] {"access", "name", "signature", "superName", "interfaces"}; - } - - @Override - public Object getValue(String name) { - return switch (name) { - case "access" -> this.access; - case "name" -> this.name; - case "signature" -> this.signature; - case "superName" -> this.superName; - case "interfaces" -> this.interfaces; - default -> throw new IllegalArgumentException("Invalid name: " + name); + 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) }; } 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 index 80416277..c657b580 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Constants.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Constants.java @@ -20,30 +20,8 @@ public final class Constants { public static final Type SET_TYPE = Type.getType(Set.class); public static final Type CLASS_HEADER_TYPE = Type.getType(ClassHeader.class); - public static final Type[] CLASS_HEADER_CONSTRUCTOR_TYPES = new Type[] { - Type.INT_TYPE, - STRING_TYPE, - STRING_TYPE, - STRING_TYPE, - STRING_ARRAY_TYPE - }; public static final Type FIELD_HEADER_TYPE = Type.getType(FieldHeader.class); - public static final Type[] FIELD_HEADER_CONSTRUCTOR_TYPES = new Type[] { - STRING_TYPE, - Type.INT_TYPE, - STRING_TYPE, - STRING_TYPE, - STRING_TYPE - }; public static final Type METHOD_HEADER_TYPE = Type.getType(MethodHeader.class); - public static final Type[] METHOD_HEADER_CONSTRUCTOR_TYPES = new Type[] { - STRING_TYPE, - Type.INT_TYPE, - STRING_TYPE, - STRING_TYPE, - STRING_TYPE, - STRING_ARRAY_TYPE - }; public static final Type FORCE_SIGNATURE_TYPE = Type.getType(ForceSignature.class); public static final Type INVOCATION_TYPE = Type.getType(Invocation.class); diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/FieldHeader.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/FieldHeader.java index b93c0bcd..291bf137 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/FieldHeader.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/FieldHeader.java @@ -6,9 +6,7 @@ import org.objectweb.asm.Type; import java.lang.reflect.Field; -import java.util.Arrays; import java.util.Objects; -import java.util.stream.Collectors; /** * A record holding information on the header of a field as declared in java bytecode. @@ -39,24 +37,13 @@ public Type getType() { } @Override - public Type[] getConstructorParameterTypes() { - return Constants.FIELD_HEADER_CONSTRUCTOR_TYPES; - } - - @Override - public String[] getRecordComponents() { - return new String[] {"owner", "access", "name", "descriptor", "signature"}; - } - - @Override - public Object getValue(String name) { - return switch (name) { - case "owner" -> this.owner; - case "access" -> this.access; - case "name" -> this.name; - case "descriptor" -> this.descriptor; - case "signature" -> this.signature; - default -> throw new IllegalArgumentException("Invalid name: " + name); + 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) }; } 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 index 8cc299a3..e7298dbf 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/ForceSignatureAnnotationProcessor.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/ForceSignatureAnnotationProcessor.java @@ -13,31 +13,22 @@ * An instance of this class processes and holds information on a single class and its members. * @author Daniel Mangold */ -public class ForceSignatureAnnotationProcessor { +public class ForceSignatureAnnotationProcessor extends ClassVisitor { - /** - * The forced identifier of the class, if any. - */ - private String forcedClassIdentifier; + private final ForceSignatureAnnotationVisitor annotationVisitor = new ForceSignatureAnnotationVisitor(); + private final List fieldLevelVisitors = new ArrayList<>(); + private final List methodLevelVisitors = new ArrayList<>(); + private String className; - /** - * A mapping of the actual field header to the forced one. - */ + private String forcedClassName; private final Map forcedFieldsMapping = new HashMap<>(); - - /** - * A mapping of the actual method header to the forced one. - */ private final Map forcedMethodsMapping = new HashMap<>(); /** - * Constructs a new {@link ForceSignatureAnnotationProcessor} instance and processes the - * {@link ForceSignature} annotation using the given class reader. - * - * @param reader the class reader to use for processing + * Constructs a new {@link ForceSignatureAnnotationProcessor} instance. */ - public ForceSignatureAnnotationProcessor(ClassReader reader) { - reader.accept(new ClassLevelVisitor(reader.getClassName()), 0); + public ForceSignatureAnnotationProcessor() { + super(Opcodes.ASM9); } /** @@ -46,7 +37,7 @@ public ForceSignatureAnnotationProcessor(ClassReader reader) { * @return true, if forced, otherwise false */ public boolean classIdentifierIsForced() { - return forcedClassIdentifier != null; + return forcedClassName != null; } /** @@ -55,29 +46,29 @@ public boolean classIdentifierIsForced() { * @return the forced class identifier */ public String forcedClassIdentifier() { - return forcedClassIdentifier.replace('.', '/'); + return forcedClassName.replace('.', '/'); } /** * Whether the given field is forced. * - * @param identifier the original identifier / name of the field + * @param name the original identifier / name of the field * @return true, if forced, otherwise false */ - public boolean fieldIdentifierIsForced(String identifier) { - return forcedFieldHeader(identifier) != null; + public boolean fieldIdentifierIsForced(String name) { + return forcedFieldHeader(name) != null; } /** * Returns the field header for a forced field. * - * @param identifier the original identifier / name of the field + * @param name the original identifier / name of the field * @return the field header */ - public FieldHeader forcedFieldHeader(String identifier) { + public FieldHeader forcedFieldHeader(String name) { return forcedFieldsMapping.entrySet() .stream() - .filter(entry -> identifier.equals(entry.getKey().name())) + .filter(entry -> name.equals(entry.getKey().name())) .findAny() .map(Map.Entry::getValue) .orElse(null); @@ -86,108 +77,96 @@ public FieldHeader forcedFieldHeader(String identifier) { /** * Whether the given method is forced. * - * @param identifier the original identifier / name of the method + * @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 identifier, String descriptor) { - return forcedMethodHeader(identifier, descriptor) != null; + public boolean methodSignatureIsForced(String name, String descriptor) { + return forcedMethodHeader(name, descriptor) != null; } /** * Returns the method header for a forced method. * - * @param identifier the original identifier / name of the 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 identifier, String descriptor) { + public MethodHeader forcedMethodHeader(String name, String descriptor) { return forcedMethodsMapping.entrySet() .stream() - .filter(entry -> identifier.equals(entry.getKey().name()) && descriptor.equals(entry.getKey().descriptor())) + .filter(entry -> name.equals(entry.getKey().name()) && descriptor.equals(entry.getKey().descriptor())) .findAny() .map(Map.Entry::getValue) .orElse(null); } - /** - * A visitor for processing class-level annotations. - */ - private class ClassLevelVisitor extends ClassVisitor { - - private final String name; - - private ForceSignatureAnnotationVisitor annotationVisitor; - private final List fieldLevelVisitors = new ArrayList<>(); - private final List methodLevelVisitors = new ArrayList<>(); + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + this.className = name; + } - private ClassLevelVisitor(String name) { - super(Opcodes.ASM9); - this.name = name; + @Override + public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { + if (descriptor.equals(Constants.FORCE_SIGNATURE_TYPE.getDescriptor())) { + return annotationVisitor; + } else { + return null; } + } - @Override - public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { - if (descriptor.equals(Constants.FORCE_SIGNATURE_TYPE.getDescriptor())) { - return annotationVisitor = new ForceSignatureAnnotationVisitor(); - } 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 FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) { - FieldLevelVisitor fieldLevelVisitor = new FieldLevelVisitor(this.name, 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 MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { - MethodLevelVisitor methodLevelVisitor = new MethodLevelVisitor(this.name, 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) + ); } - @Override - public void visitEnd() { - forcedClassIdentifier = annotationVisitor != null ? annotationVisitor.identifier : null; - - 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) - ); - } + 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) + ); } } diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Header.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Header.java index d65b4078..1c6624bc 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Header.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Header.java @@ -1,11 +1,19 @@ package org.tudalgo.algoutils.transform.util; +import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Type; +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 interface Header { +public sealed interface Header permits ClassHeader, FieldHeader, MethodHeader { /** * Returns the type for this header. @@ -14,25 +22,46 @@ public interface Header { */ Type getType(); - /** - * Returns the parameter types for this record's primary constructor. - * - * @return the parameter types - */ - Type[] getConstructorParameterTypes(); + HeaderRecordComponent[] getComponents(); /** - * Returns the values that can be passed to {@link #getValue(String)}. + * 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. * - * @return the values + * @param mv the method visitor to use + * @return the maximum stack size used during the operation */ - String[] getRecordComponents(); + default int buildHeader(MethodVisitor mv) { + Type headerType = getType(); + HeaderRecordComponent[] components = getComponents(); + int maxStack, stackSize; - /** - * Returns the stored value for the given record component's name. - * - * @param name the name of the record component - * @return the record component's value - */ - Object getValue(String name); + mv.visitTypeInsn(NEW, getType().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/Invocation.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Invocation.java index 79817f33..d337f68a 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Invocation.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Invocation.java @@ -4,10 +4,12 @@ /** * This class holds information about the context of an invocation. - * Context means the object a method was invoked on and the parameters it was invoked with + * 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 Object instance; @@ -31,7 +33,8 @@ public Invocation(StackTraceElement[] stackTrace) { */ public Invocation(Object instance, StackTraceElement[] stackTrace) { this.instance = instance; - this.stackTrace = stackTrace; + this.stackTrace = new StackTraceElement[stackTrace.length - 1]; + System.arraycopy(stackTrace, 1, this.stackTrace, 0, stackTrace.length - 1); } /** @@ -39,8 +42,19 @@ public Invocation(Object instance, StackTraceElement[] stackTrace) { * * @return the object the method was invoked on. */ - public Object getInstance() { - return instance; + @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); } /** @@ -52,8 +66,13 @@ public StackTraceElement[] getStackTrace() { return stackTrace; } + /** + * Returns the stack trace element of the caller. + * + * @return the stack trace element + */ public StackTraceElement getCallerStackTraceElement() { - return stackTrace[1]; + return stackTrace[0]; } /** @@ -178,7 +197,7 @@ public void addParameter(Object value) { @Override public String toString() { - return "Invocation{instance=%s, stackTrace=%s, parameterValues=%s}".formatted(instance, Arrays.toString(stackTrace), parameterValues); + return "Invocation{instance=%s, parameterValues=%s, stackTrace=%s}".formatted(instance, parameterValues, Arrays.toString(stackTrace)); } @Override diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/MethodHeader.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/MethodHeader.java index be72f1be..d86d8ce5 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/MethodHeader.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/MethodHeader.java @@ -55,25 +55,14 @@ public Type getType() { } @Override - public Type[] getConstructorParameterTypes() { - return Constants.METHOD_HEADER_CONSTRUCTOR_TYPES; - } - - @Override - public String[] getRecordComponents() { - return new String[] {"owner", "access", "name", "descriptor", "signature", "exceptions"}; - } - - @Override - public Object getValue(String name) { - return switch (name) { - case "owner" -> this.owner; - case "access" -> this.access; - case "name" -> this.name; - case "descriptor" -> this.descriptor; - case "signature" -> this.signature; - case "exceptions" -> this.exceptions; - default -> throw new IllegalArgumentException("Invalid name: " + name); + 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) }; } diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/SimilarityMapper.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/SimilarityMapper.java index 96f730cf..14912b9b 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/SimilarityMapper.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/SimilarityMapper.java @@ -8,29 +8,32 @@ public class SimilarityMapper { + private final double[][] similarityMatrix; private final Map bestMatches = new HashMap<>(); @SuppressWarnings("unchecked") public SimilarityMapper(Collection from, Map> to, double similarityThreshold) { List rowMapping = new ArrayList<>(from); List columnMapping = new ArrayList<>(to.keySet()); - double[][] similarityMatrix = new double[from.size()][to.size()]; + this.similarityMatrix = new double[from.size()][to.size()]; for (int i = 0; i < similarityMatrix.length; i++) { - final int finalI = i; + String row = rowMapping.get(i); int bestMatchIndex = -1; double bestSimilarity = similarityThreshold; for (int j = 0; j < similarityMatrix[i].length; j++) { similarityMatrix[i][j] = Stream.concat(Stream.of(columnMapping.get(j)), to.get(columnMapping.get(j)).stream()) - .mapToDouble(value -> MatchingUtils.similarity(rowMapping.get(finalI), value)) + .mapToDouble(value -> MatchingUtils.similarity(row, value)) .max() - .getAsDouble(); + .orElseThrow(); if (similarityMatrix[i][j] >= bestSimilarity) { bestMatchIndex = j; bestSimilarity = similarityMatrix[i][j]; } } - bestMatches.put((T) rowMapping.get(i), bestMatchIndex >= 0 ? (T) columnMapping.get(bestMatchIndex) : null); + if (bestMatchIndex >= 0) { + bestMatches.put((T) rowMapping.get(i), (T) columnMapping.get(bestMatchIndex)); + } } } @@ -40,24 +43,26 @@ public SimilarityMapper(Collection from, Function mappingFunction) { List rowMapping = new ArrayList<>(from); List columnMapping = new ArrayList<>(to); - double[][] similarityMatrix = new double[from.size()][to.size()]; + this.similarityMatrix = new double[from.size()][to.size()]; for (int i = 0; i < similarityMatrix.length; i++) { + String row = mappingFunction.apply(rowMapping.get(i)); int bestMatchIndex = -1; double bestSimilarity = similarityThreshold; for (int j = 0; j < similarityMatrix[i].length; j++) { - similarityMatrix[i][j] = MatchingUtils.similarity(mappingFunction.apply(rowMapping.get(i)), - mappingFunction.apply(columnMapping.get(j))); + similarityMatrix[i][j] = MatchingUtils.similarity(row, mappingFunction.apply(columnMapping.get(j))); if (similarityMatrix[i][j] >= bestSimilarity) { bestMatchIndex = j; bestSimilarity = similarityMatrix[i][j]; } } - bestMatches.put(rowMapping.get(i), bestMatchIndex >= 0 ? columnMapping.get(bestMatchIndex) : null); + if (bestMatchIndex >= 0) { + bestMatches.put(rowMapping.get(i), columnMapping.get(bestMatchIndex)); + } } } - public T getBestMatch(T t) { - return bestMatches.get(t); + public Optional getBestMatch(T t) { + return Optional.ofNullable(bestMatches.get(t)); } } 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 index bf07d05f..165c223c 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationContext.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationContext.java @@ -1,10 +1,13 @@ package org.tudalgo.algoutils.transform.util; +import org.objectweb.asm.ClassReader; import org.objectweb.asm.Type; import org.tudalgo.algoutils.transform.SolutionClassNode; import org.tudalgo.algoutils.transform.SolutionMergingClassTransformer; import org.tudalgo.algoutils.transform.SubmissionClassInfo; +import java.io.IOException; +import java.io.InputStream; import java.util.*; /** @@ -39,6 +42,8 @@ public TransformationContext( this.submissionClasses = submissionClasses; } + // Config and misc. stuff + /** * Returns the project prefix. * @@ -67,14 +72,6 @@ public boolean methodHasReplacement(MethodHeader methodHeader) { return getMethodReplacement(methodHeader) != null; } - public void setSubmissionClassLoader(ClassLoader submissionClassLoader) { - this.submissionClassLoader = submissionClassLoader; - } - - public ClassLoader getSubmissionClassLoader() { - return submissionClassLoader; - } - /** * Returns the replacement method header for the given target method header. * @@ -87,10 +84,43 @@ public MethodHeader getMethodReplacement(MethodHeader methodHeader) { .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; } + /** + * Computes similarities for mapping submission classes to solution classes. + */ + @SuppressWarnings("unchecked") + public void computeClassesSimilarity() { + classSimilarityMapper = new SimilarityMapper<>(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()); @@ -108,57 +138,138 @@ public boolean isSubmissionClass(String submissionClassName) { */ public SubmissionClassInfo getSubmissionClassInfo(String submissionClassName) { boolean isAbsent = !submissionClasses.containsKey(submissionClassName); - SubmissionClassInfo submissionClassInfo = submissionClasses.computeIfAbsent(submissionClassName, - className -> TransformationUtils.readSubmissionClass(this, className)); + SubmissionClassInfo submissionClassInfo = submissionClasses.computeIfAbsent(submissionClassName, this::readSubmissionClass); if (isAbsent && submissionClassInfo != null) { - submissionClassInfo.mapToSolutionClass(); + submissionClassInfo.computeMembers(); } return submissionClassInfo; } - public Map solutionClasses() { - return solutionClasses; + /** + * 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; } - @SuppressWarnings("unchecked") - public void computeClassesSimilarity() { - classSimilarityMapper = new SimilarityMapper<>(submissionClassNames, - (Map>) configuration.get(SolutionMergingClassTransformer.Config.SOLUTION_CLASSES), - getSimilarity()); - } + // 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); + return classSimilarityMapper.getBestMatch(submissionClassName).orElse(submissionClassName); } - public String getComputedName(String className) { - if (isSubmissionClass(className)) { - Type type = className.startsWith("[") ? Type.getType(className) : Type.getObjectType(className); - if (type.getSort() == Type.OBJECT) { - return getSubmissionClassInfo(className).getComputedClassName(); - } else { // else must be array - return "%sL%s;".formatted("[".repeat(type.getDimensions()), - getSubmissionClassInfo(type.getElementType().getInternalName()).getComputedClassName()); - } + /** + * 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); + } + + /** + * Returns the computed (i.e., mapped from submission to solution) type. + * The given value may be an internal class name or a descriptor. + * If the value is a method descriptor, this method will return a descriptor where + * all parameter types and the return types have been computed. + * If the value is an array descriptor, it will return a descriptor where + * the component type has been computed. + * If the given internal class name or descriptor are not part of the submission + * or no corresponding solution class exists, it will return a {@link Type} object + * representing the original name / descriptor. + * + * @param descriptor the class name or descriptor + * @return the computed type + */ + public Type toComputedType(String descriptor) { + if (descriptor.startsWith("(")) { // method descriptor + return toComputedType(Type.getMethodType(descriptor)); + } else if (descriptor.startsWith("[") || descriptor.endsWith(";")) { // array or reference descriptor + return toComputedType(Type.getType(descriptor)); + } else if (descriptor.length() == 1 && "VZBSCIFJD".contains(descriptor)) { // primitive type + return Type.getType(descriptor); } else { - return className; + return toComputedType(Type.getObjectType(descriptor)); } } - public Type getComputedType(Type type) { + /** + * 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(getComputedName(type.getInternalName())); + return Type.getObjectType(getSolutionClassName(type.getInternalName())); } else if (type.getSort() == Type.ARRAY) { - return Type.getType(getComputedName(type.getDescriptor())); + 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; } } + /** + * 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 index e52970c0..b0e77cf7 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationUtils.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationUtils.java @@ -1,15 +1,9 @@ package org.tudalgo.algoutils.transform.util; import org.opentest4j.AssertionFailedError; -import org.tudalgo.algoutils.transform.SolutionClassNode; -import org.tudalgo.algoutils.transform.SolutionMergingClassTransformer; -import org.tudalgo.algoutils.transform.SubmissionClassInfo; -import org.objectweb.asm.ClassReader; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Type; -import java.io.IOException; -import java.io.InputStream; import java.util.Map; import java.util.StringJoiner; @@ -23,6 +17,69 @@ 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 is they are either + * both static or both non-static. + * + * @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); + } + + /** + * 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$"); + } + + /** + * 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 += (types[i].getSort() == Type.LONG || types[i].getSort() == Type.DOUBLE) ? 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. @@ -89,70 +146,19 @@ public static void unboxType(MethodVisitor mv, Type type) { } /** - * 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 += (types[i].getSort() == Type.LONG || types[i].getSort() == Type.DOUBLE) ? 2 : 1; - } - return localsIndex; - } - - /** - * Attempts to read and process a solution class from {@code resources/classes/}. - * - * @param transformationContext a {@link TransformationContext} object - * @param className the name of the solution class - * @return the resulting {@link SolutionClassNode} object - */ - public static SolutionClassNode readSolutionClass(TransformationContext transformationContext, String className) { - ClassReader solutionClassReader; - String solutionClassFilePath = "/classes/%s.bin".formatted(className); - try (InputStream is = SolutionMergingClassTransformer.class.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(transformationContext, className); - solutionClassReader.accept(solutionClassNode, 0); - return solutionClassNode; - } - - /** - * Attempts to read and process a submission class. + * Places the given type's default value on top of the method visitor's stack. * - * @param transformationContext a {@link TransformationContext} object - * @param className the name of the submission class - * @return the resulting {@link SubmissionClassInfo} object + * @param mv the method visitor to use + * @param type the type to get the default value for */ - public static SubmissionClassInfo readSubmissionClass(TransformationContext transformationContext, String className) { - ClassReader submissionClassReader; - String submissionClassFilePath = className + ".class"; - try (InputStream is = transformationContext.getSubmissionClassLoader().getResourceAsStream(submissionClassFilePath)) { - if (is == null) { - return null; - } - submissionClassReader = new ClassReader(is); - } catch (IOException e) { - throw new RuntimeException(e); + 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); } - SubmissionClassInfo submissionClassInfo = new SubmissionClassInfo( - transformationContext, - submissionClassReader.getClassName(), - new ForceSignatureAnnotationProcessor(submissionClassReader) - ); - submissionClassReader.accept(submissionClassInfo, 0); - return submissionClassInfo; } /** @@ -211,6 +217,26 @@ public static int buildArray(MethodVisitor mv, Type componentType, Object[] arra return maxStack; } + public static void buildExceptionForHeaderMismatch(MethodVisitor mv, String message, Header expected, Header actual) { + mv.visitTypeInsn(NEW, Type.getInternalName(AssertionFailedError.class)); + mv.visitInsn(DUP); + mv.visitLdcInsn(message); + expected.buildHeader(mv); + actual.buildHeader(mv); + mv.visitMethodInsn(INVOKESPECIAL, + Type.getInternalName(AssertionFailedError.class), + "", + Type.getMethodDescriptor(Type.VOID_TYPE, Constants.STRING_TYPE, Constants.OBJECT_TYPE, Constants.OBJECT_TYPE), + false); + mv.visitInsn(ATHROW); + } + + /** + * 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", @@ -233,6 +259,12 @@ public static String toHumanReadableModifiers(int modifiers) { 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"; @@ -249,69 +281,4 @@ public static String toHumanReadableType(Type type) { default -> throw new IllegalStateException("Unexpected type: " + type); }; } - - public static void buildExceptionForHeaderMismatch(MethodVisitor mv, String message, Header expected, Header actual) { - mv.visitTypeInsn(NEW, Type.getInternalName(AssertionFailedError.class)); - mv.visitInsn(DUP); - mv.visitLdcInsn(message); - buildHeader(mv, expected); - buildHeader(mv, actual); - mv.visitMethodInsn(INVOKESPECIAL, - Type.getInternalName(AssertionFailedError.class), - "", - Type.getMethodDescriptor(Type.VOID_TYPE, Constants.STRING_TYPE, Constants.OBJECT_TYPE, Constants.OBJECT_TYPE), - false); - mv.visitInsn(ATHROW); - } - - 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); - } - } - - /** - * 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 - * @param header the header object to replicate in bytecode - * @return the maximum stack size used during the operation - */ - public static int buildHeader(MethodVisitor mv, Header header) { - Type headerType = header.getType(); - Type[] constructorParameterTypes = header.getConstructorParameterTypes(); - String[] components = header.getRecordComponents(); - int maxStack, stackSize; - - mv.visitTypeInsn(NEW, header.getType().getInternalName()); - mv.visitInsn(DUP); - maxStack = stackSize = 2; - for (int i = 0; i < components.length; i++) { - Object value = header.getValue(components[i]); - if (constructorParameterTypes[i].equals(Constants.STRING_ARRAY_TYPE)) { - int stackUsed = 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, constructorParameterTypes), - false); - - return maxStack; - } } 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..93d917dc --- /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 implementation, List testImplementation) {} + + public record RepositoryConfiguration(String name, String url) {} +} From 44e2e7421690afb34fb825bcfd9ed672e60ec454 Mon Sep 17 00:00:00 2001 From: Daniel Mangold Date: Sat, 30 Nov 2024 12:55:55 +0100 Subject: [PATCH 22/48] Create separate exception for irrecoverable header mismatches --- .../transform/SubmissionClassVisitor.java | 17 ++-- .../transform/SubmissionMethodVisitor.java | 6 +- .../algoutils/transform/util/Constants.java | 1 + .../util/IncompatibleHeaderException.java | 78 +++++++++++++++++++ .../transform/util/TransformationUtils.java | 14 ---- 5 files changed, 87 insertions(+), 29 deletions(-) create mode 100644 tutor/src/main/java/org/tudalgo/algoutils/transform/util/IncompatibleHeaderException.java diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java index 0de8ce66..58c49309 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java @@ -162,25 +162,20 @@ public MethodVisitor visitMethod(int access, String name, String descriptor, Str MethodHeader computedMethodHeader = submissionClassInfo.getComputedMethodHeader(name, descriptor); if (!TransformationUtils.contextIsCompatible(access, computedMethodHeader.access())) { - MethodHeader methodHeader = new MethodHeader(computedMethodHeader.owner(), + computedMethodHeader = new MethodHeader(computedMethodHeader.owner(), access, name + "$submission", transformationContext.toComputedType(descriptor).getDescriptor(), signature, exceptions); - return new SubmissionMethodVisitor(methodHeader.toMethodVisitor(getDelegate()), - transformationContext, - submissionClassInfo, - originalMethodHeader, - methodHeader); } else { visitedMethods.add(computedMethodHeader); - return new SubmissionMethodVisitor(computedMethodHeader.toMethodVisitor(getDelegate()), - transformationContext, - submissionClassInfo, - originalMethodHeader, - computedMethodHeader); } + return new SubmissionMethodVisitor(computedMethodHeader.toMethodVisitor(getDelegate()), + transformationContext, + submissionClassInfo, + originalMethodHeader, + computedMethodHeader); } } diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionMethodVisitor.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionMethodVisitor.java index 1f5a4275..dbb4203a 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionMethodVisitor.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionMethodVisitor.java @@ -327,10 +327,8 @@ public void visitCode() { } if (headerMismatch) { - TransformationUtils.buildExceptionForHeaderMismatch(getDelegate(), - "Method has incorrect return or parameter types", - computedMethodHeader, - originalMethodHeader); + new IncompatibleHeaderException("Method has incorrect return or parameter types", computedMethodHeader, originalMethodHeader) + .replicateInBytecode(getDelegate(), true); } else { // visit original code super.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 index c657b580..dde63448 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Constants.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Constants.java @@ -19,6 +19,7 @@ public final class Constants { 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 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); 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..ab427584 --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/IncompatibleHeaderException.java @@ -0,0 +1,78 @@ +package org.tudalgo.algoutils.transform.util; + +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Type; + +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 + */ + 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 + * @return the maximum stack size used + */ + public int replicateInBytecode(MethodVisitor mv, boolean throwException) { + int maxStack, stackSize; + + mv.visitTypeInsn(NEW, Type.getInternalName(getClass())); + mv.visitInsn(DUP); + mv.visitLdcInsn(message); + maxStack = stackSize = 3; + maxStack = Math.max(maxStack, stackSize++ + expected.buildHeader(mv)); + maxStack = Math.max(maxStack, stackSize + actual.buildHeader(mv)); + mv.visitMethodInsn(INVOKESPECIAL, + Type.getInternalName(getClass()), + "", + 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/TransformationUtils.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationUtils.java index b0e77cf7..5eb4578e 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationUtils.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationUtils.java @@ -217,20 +217,6 @@ public static int buildArray(MethodVisitor mv, Type componentType, Object[] arra return maxStack; } - public static void buildExceptionForHeaderMismatch(MethodVisitor mv, String message, Header expected, Header actual) { - mv.visitTypeInsn(NEW, Type.getInternalName(AssertionFailedError.class)); - mv.visitInsn(DUP); - mv.visitLdcInsn(message); - expected.buildHeader(mv); - actual.buildHeader(mv); - mv.visitMethodInsn(INVOKESPECIAL, - Type.getInternalName(AssertionFailedError.class), - "", - Type.getMethodDescriptor(Type.VOID_TYPE, Constants.STRING_TYPE, Constants.OBJECT_TYPE, Constants.OBJECT_TYPE), - false); - mv.visitInsn(ATHROW); - } - /** * Returns a human-readable form of the given modifiers. * From d5bcfa6b13113337646ccfc583ba3bd682703d74 Mon Sep 17 00:00:00 2001 From: Daniel Mangold Date: Sat, 30 Nov 2024 15:17:48 +0100 Subject: [PATCH 23/48] Refactor SimilarityMatcher and prevent duplicates --- .../transform/util/SimilarityMapper.java | 102 ++++++++++++------ .../transform/util/TransformationContext.java | 6 +- 2 files changed, 74 insertions(+), 34 deletions(-) diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/SimilarityMapper.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/SimilarityMapper.java index 14912b9b..46900411 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/SimilarityMapper.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/SimilarityMapper.java @@ -1,68 +1,106 @@ package org.tudalgo.algoutils.transform.util; +import kotlin.Pair; import org.tudalgo.algoutils.tutor.general.match.MatchingUtils; import java.util.*; import java.util.function.Function; +import java.util.stream.Collectors; import java.util.stream.Stream; +/** + * Computes the similarity for the cross product of two given collections. + * This creates a mapping of values in the first collection to the best match + * in the second collection, if any. + * + * @param the type of the collection's elements + */ public class SimilarityMapper { + private final List rowMapping; + private final List columnMapping; private final double[][] similarityMatrix; - private final Map bestMatches = new HashMap<>(); + private final Map> bestMatches = new HashMap<>(); - @SuppressWarnings("unchecked") - public SimilarityMapper(Collection from, Map> to, double similarityThreshold) { - List rowMapping = new ArrayList<>(from); - List columnMapping = new ArrayList<>(to.keySet()); + /** + * Creates a new {@link SimilarityMapper} instance, allowing columns to have aliases. + * + * @param from the values to map from (rows) + * @param to the values to map to (columns), 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 + * @param mappingFunction a function for mapping the collection's elements to strings + */ + public SimilarityMapper(Collection from, + Map> to, + double similarityThreshold, + Function mappingFunction) { + this.rowMapping = new ArrayList<>(from); + this.columnMapping = new ArrayList<>(to.keySet()); this.similarityMatrix = new double[from.size()][to.size()]; - - for (int i = 0; i < similarityMatrix.length; i++) { - String row = rowMapping.get(i); - int bestMatchIndex = -1; - double bestSimilarity = similarityThreshold; - for (int j = 0; j < similarityMatrix[i].length; j++) { - similarityMatrix[i][j] = Stream.concat(Stream.of(columnMapping.get(j)), to.get(columnMapping.get(j)).stream()) - .mapToDouble(value -> MatchingUtils.similarity(row, value)) - .max() - .orElseThrow(); - if (similarityMatrix[i][j] >= bestSimilarity) { - bestMatchIndex = j; - bestSimilarity = similarityMatrix[i][j]; - } - } - if (bestMatchIndex >= 0) { - bestMatches.put((T) rowMapping.get(i), (T) columnMapping.get(bestMatchIndex)); - } - } + computeSimilarity(to, similarityThreshold, mappingFunction); } + /** + * Creates a new {@link SimilarityMapper} instance. + * + * @param from the values to map from (rows) + * @param to the values to map to (columns) + * @param similarityThreshold the minimum similarity two values need to have to be considered a match + * @param mappingFunction a function for mapping the collection's elements to strings + */ public SimilarityMapper(Collection from, Collection to, double similarityThreshold, Function mappingFunction) { - List rowMapping = new ArrayList<>(from); - List columnMapping = new ArrayList<>(to); + this.rowMapping = new ArrayList<>(from); + this.columnMapping = new ArrayList<>(to); this.similarityMatrix = new double[from.size()][to.size()]; + computeSimilarity(to.stream().collect(Collectors.toMap(Function.identity(), t -> Collections.emptyList())), + similarityThreshold, + mappingFunction); + } + + /** + * 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)).map(Pair::getFirst); + } + /** + * Computes the similarity for each entry in the cross product of the two input collections. + * Also extracts the best matches and stores them in {@link #bestMatches} for easy access. + * + * @param to a mapping of columns to their aliases + * @param similarityThreshold the minimum similarity two values need to have to be considered a match + * @param mappingFunction a function for mapping the collection's elements to strings + */ + private void computeSimilarity(Map> to, + double similarityThreshold, + Function mappingFunction) { for (int i = 0; i < similarityMatrix.length; i++) { String row = mappingFunction.apply(rowMapping.get(i)); int bestMatchIndex = -1; double bestSimilarity = similarityThreshold; for (int j = 0; j < similarityMatrix[i].length; j++) { - similarityMatrix[i][j] = MatchingUtils.similarity(row, mappingFunction.apply(columnMapping.get(j))); + similarityMatrix[i][j] = Stream.concat(Stream.of(columnMapping.get(j)), to.get(columnMapping.get(j)).stream()) + .map(mappingFunction) + .mapToDouble(value -> MatchingUtils.similarity(row, value)) + .max() + .orElseThrow(); if (similarityMatrix[i][j] >= bestSimilarity) { bestMatchIndex = j; bestSimilarity = similarityMatrix[i][j]; } } if (bestMatchIndex >= 0) { - bestMatches.put(rowMapping.get(i), columnMapping.get(bestMatchIndex)); + Pair pair = new Pair<>(columnMapping.get(bestMatchIndex), bestSimilarity); + bestMatches.merge(rowMapping.get(i), pair, (oldPair, newPair) -> + newPair.getSecond() > oldPair.getSecond() ? newPair : oldPair); } } } - - public Optional getBestMatch(T t) { - return Optional.ofNullable(bestMatches.get(t)); - } } 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 index 165c223c..d1185ded 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationContext.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationContext.java @@ -9,6 +9,7 @@ import java.io.IOException; import java.io.InputStream; import java.util.*; +import java.util.function.Function; /** * A record for holding context information for the transformation process. @@ -108,8 +109,9 @@ public void setSubmissionClassNames(Set submissionClassNames) { @SuppressWarnings("unchecked") public void computeClassesSimilarity() { classSimilarityMapper = new SimilarityMapper<>(submissionClassNames, - (Map>) configuration.get(SolutionMergingClassTransformer.Config.SOLUTION_CLASSES), - getSimilarity()); + (Map>) configuration.get(SolutionMergingClassTransformer.Config.SOLUTION_CLASSES), + getSimilarity(), + Function.identity()); } // Submission classes From 09aec1e3f4c9024bf723ebd2732712a65ddc88a9 Mon Sep 17 00:00:00 2001 From: Daniel Mangold Date: Sat, 30 Nov 2024 22:02:27 +0100 Subject: [PATCH 24/48] Allow targeting specific methods for disabling logging, substitution and enabling delegation --- .../transform/SubmissionExecutionHandler.java | 95 +++++++++++++++---- 1 file changed, 78 insertions(+), 17 deletions(-) diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionExecutionHandler.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionExecutionHandler.java index 18e902ac..b03b1149 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionExecutionHandler.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionExecutionHandler.java @@ -138,13 +138,6 @@ public static Set getOriginalMethodHeaders(Class clazz) { // Invocation logging - /** - * Resets the logging of method invocations to log no invocations. - */ - public void resetMethodInvocationLogging() { - methodInvocations.clear(); - } - /** * Enables logging of method / constructor invocations for the given executable. * @@ -164,6 +157,34 @@ public void enableMethodInvocationLogging(MethodHeader methodHeader) { .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 void disableMethodInvocationLogging(Executable executable) { + disableMethodInvocationLogging(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 void disableMethodInvocationLogging(MethodHeader methodHeader) { + Optional.ofNullable(methodInvocations.get(methodHeader.owner())) + .ifPresent(map -> map.remove(methodHeader)); + } + + /** + * Resets the logging of method invocations to log no invocations. + */ + public void resetMethodInvocationLogging() { + methodInvocations.clear(); + } + /** * Returns all logged invocations for the given method / constructor. * @@ -189,13 +210,6 @@ public List getInvocationsForMethod(MethodHeader methodHeader) { // Method substitution - /** - * Resets the substitution of methods. - */ - public void resetMethodSubstitution() { - methodSubstitutions.clear(); - } - /** * 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, @@ -221,13 +235,53 @@ public void substituteMethod(MethodHeader methodHeader, MethodSubstitution subst .put(methodHeader, substitute); } + /** + * Disables substitution for the given method / constructor. + * + * @param executable the substituted method / constructor + */ + public void disableMethodSubstitution(Executable executable) { + disableMethodSubstitution(new MethodHeader(executable)); + } + + /** + * Disables substitution for the given method. + * + * @param methodHeader a method header describing the method + */ + public void disableMethodSubstitution(MethodHeader methodHeader) { + Optional.ofNullable(methodSubstitutions.get(methodHeader.owner())) + .ifPresent(map -> map.remove(methodHeader)); + } + + /** + * Resets the substitution of methods. + */ + public void resetMethodSubstitution() { + methodSubstitutions.clear(); + } + // Method delegation /** - * Resets the delegation of methods. + * 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 void resetMethodDelegation() { - methodDelegationAllowlist.clear(); + public void enableMethodDelegation(Executable executable) { + enableMethodDelegation(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 void enableMethodDelegation(MethodHeader methodHeader) { + methodDelegationAllowlist.computeIfAbsent(methodHeader.owner(), k -> new HashMap<>()) + .put(methodHeader, false); } /** @@ -249,6 +303,13 @@ public void disableMethodDelegation(MethodHeader methodHeader) { .put(methodHeader, true); } + /** + * Resets the delegation of methods. + */ + public void resetMethodDelegation() { + methodDelegationAllowlist.clear(); + } + /** * Collection of methods injected into the bytecode of transformed methods. */ From a645cfe0c15b124276eae54ce2a27b76b4491b62 Mon Sep 17 00:00:00 2001 From: Daniel Mangold Date: Sat, 30 Nov 2024 22:03:51 +0100 Subject: [PATCH 25/48] Allow calling the original method from substituted methods --- .../transform/SubmissionMethodVisitor.java | 32 +++---- .../algoutils/transform/util/Constants.java | 4 +- .../algoutils/transform/util/Invocation.java | 92 +++++++++++++++++-- .../transform/util/MethodHeader.java | 19 +++- 4 files changed, 114 insertions(+), 33 deletions(-) diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionMethodVisitor.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionMethodVisitor.java index dbb4203a..b40314c4 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionMethodVisitor.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionMethodVisitor.java @@ -417,30 +417,22 @@ private void buildInvocation(Type[] argumentTypes) { super.visitTypeInsn(NEW, Constants.INVOCATION_TYPE.getInternalName()); super.visitInsn(DUP); + super.visitLdcInsn(Type.getObjectType(computedMethodHeader.owner())); + computedMethodHeader.buildHeader(getDelegate()); + super.visitMethodInsn(INVOKESTATIC, + threadType.getInternalName(), + "currentThread", + Type.getMethodDescriptor(threadType), + false); + super.visitMethodInsn(INVOKEVIRTUAL, + threadType.getInternalName(), + "getStackTrace", + Type.getMethodDescriptor(stackTraceElementArrayType), + false); if (!isStatic && !isConstructor) { super.visitVarInsn(ALOAD, 0); - super.visitMethodInsn(INVOKESTATIC, - threadType.getInternalName(), - "currentThread", - Type.getMethodDescriptor(threadType), - false); - super.visitMethodInsn(INVOKEVIRTUAL, - threadType.getInternalName(), - "getStackTrace", - Type.getMethodDescriptor(stackTraceElementArrayType), - false); Constants.INVOCATION_CONSTRUCTOR_WITH_INSTANCE.toMethodInsn(getDelegate(), false); } else { - super.visitMethodInsn(INVOKESTATIC, - threadType.getInternalName(), - "currentThread", - Type.getMethodDescriptor(threadType), - false); - super.visitMethodInsn(INVOKEVIRTUAL, - threadType.getInternalName(), - "getStackTrace", - Type.getMethodDescriptor(stackTraceElementArrayType), - false); Constants.INVOCATION_CONSTRUCTOR.toMethodInsn(getDelegate(), false); } // load parameter with opcode (ALOAD, ILOAD, etc.) for type and ignore "this", if it exists 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 index dde63448..54fb6cca 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Constants.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Constants.java @@ -80,8 +80,8 @@ public final class Constants { 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(StackTraceElement[].class)); - INVOCATION_CONSTRUCTOR_WITH_INSTANCE = new MethodHeader(Invocation.class.getDeclaredConstructor(Object.class, StackTraceElement[].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")); 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 index d337f68a..1b7a2208 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Invocation.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Invocation.java @@ -1,5 +1,11 @@ package org.tudalgo.algoutils.transform.util; +import org.objectweb.asm.Opcodes; +import org.tudalgo.algoutils.transform.SubmissionExecutionHandler; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; import java.util.*; /** @@ -12,29 +18,35 @@ @SuppressWarnings("unused") public class Invocation { - private final Object instance; + 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. + * Constructs a new invocation (static / constructor variant). * - * @param stackTrace the stack trace up to the point of invocation + * @param declaringClass the target method for this invocation + * @param stackTrace the stack trace up to the point of invocation */ - public Invocation(StackTraceElement[] stackTrace) { - this(null, stackTrace); + public Invocation(Class declaringClass, MethodHeader methodHeader, StackTraceElement[] stackTrace) { + this(declaringClass, methodHeader, stackTrace, null); } /** - * Constructs a new invocation. + * Constructs a new invocation (non-static / non-constructor variant). * - * @param instance the object on which this invocation takes place - * @param stackTrace the stack trace up to the point of invocation + * @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(Object instance, StackTraceElement[] stackTrace) { - this.instance = instance; + 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; } /** @@ -195,6 +207,66 @@ 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; + } + + SubmissionExecutionHandler executionHandler = SubmissionExecutionHandler.getInstance(); + SubmissionExecutionHandler.Internal sehInternal = executionHandler.new Internal(); + MethodSubstitution methodSubstitution = sehInternal.getSubstitution(methodHeader); + executionHandler.disableMethodSubstitution(methodHeader); + boolean isDelegated = !sehInternal.useSubmissionImpl(methodHeader); + if (delegate) { + executionHandler.enableMethodDelegation(methodHeader); + } else { + executionHandler.disableMethodDelegation(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.invoke(invocationArgs); + } catch (Throwable e) { + throw new RuntimeException(e); + } finally { + executionHandler.substituteMethod(methodHeader, methodSubstitution); + if (isDelegated) { + executionHandler.enableMethodDelegation(methodHeader); + } else { + executionHandler.disableMethodDelegation(methodHeader); + } + } + } + @Override public String toString() { return "Invocation{instance=%s, parameterValues=%s, stackTrace=%s}".formatted(instance, parameterValues, Arrays.toString(stackTrace)); diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/MethodHeader.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/MethodHeader.java index d86d8ce5..7664c6f1 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/MethodHeader.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/MethodHeader.java @@ -83,7 +83,7 @@ public MethodVisitor toMethodVisitor(ClassVisitor delegate) { * @param isInterface true, if the method's owner is an interface */ public void toMethodInsn(MethodVisitor methodVisitor, boolean isInterface) { - int opcode = isInterface ? INVOKEINTERFACE : (access & ACC_STATIC) != 0 ? INVOKESTATIC : name.equals("") ? INVOKESPECIAL : INVOKEVIRTUAL; + int opcode = isInterface ? INVOKEINTERFACE : getOpcode(); methodVisitor.visitMethodInsn(opcode, owner, name, @@ -91,6 +91,23 @@ public void toMethodInsn(MethodVisitor methodVisitor, boolean isInterface) { 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; + } + } + /** * Two instances of {@link MethodHeader} are considered equal if their names and descriptors are equal. * TODO: include owner and parent classes if possible From 71721e25a6c495c182f4c6195643cdfcc0e13f10 Mon Sep 17 00:00:00 2001 From: Daniel Mangold Date: Tue, 3 Dec 2024 09:52:44 +0100 Subject: [PATCH 26/48] Current state --- .../transform/SolutionClassNode.java | 10 +- .../transform/SubmissionClassInfo.java | 1 + .../transform/SubmissionClassVisitor.java | 34 +- .../transform/SubmissionMethodVisitor.java | 402 +++++------------- .../transform/methods/BaseMethodVisitor.java | 212 +++++++++ .../methods/InjectingMethodVisitor.java | 386 +++++++++++++++++ .../util/IncompatibleHeaderException.java | 9 +- .../algoutils/transform/util/Invocation.java | 4 +- .../transform/util/SimilarityMapper.java | 13 + .../transform/util/TransformationUtils.java | 6 +- .../tutor/general/SubmissionInfo.java | 2 +- 11 files changed, 765 insertions(+), 314 deletions(-) create mode 100644 tutor/src/main/java/org/tudalgo/algoutils/transform/methods/BaseMethodVisitor.java create mode 100644 tutor/src/main/java/org/tudalgo/algoutils/transform/methods/InjectingMethodVisitor.java diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionClassNode.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionClassNode.java index ab9ba788..751a5f7c 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionClassNode.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionClassNode.java @@ -76,13 +76,11 @@ public FieldVisitor visitField(int access, String name, String descriptor, Strin @Override public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { - MethodNode methodNode; - if ((access & ACC_SYNTHETIC) != 0 && name.startsWith("lambda$")) { // if method is lambda - methodNode = getMethodNode(access, name + "$solution", descriptor, signature, exceptions); - } else { - methodNode = getMethodNode(access, name, descriptor, signature, exceptions); - methods.put(new MethodHeader(className, access, name, descriptor, signature, exceptions), methodNode); + if (TransformationUtils.isLambdaMethod(access, name)) { + name += "$solution"; } + MethodNode methodNode = getMethodNode(access, name, descriptor, signature, exceptions); + methods.put(new MethodHeader(className, access, name, descriptor, signature, exceptions), methodNode); return methodNode; } diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassInfo.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassInfo.java index 75127c31..5d724f73 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassInfo.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassInfo.java @@ -152,6 +152,7 @@ public MethodHeader getComputedSuperClassConstructorHeader(String descriptor) { @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()) { diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java index 58c49309..60cbdd7a 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java @@ -1,5 +1,7 @@ package org.tudalgo.algoutils.transform; +import org.objectweb.asm.tree.MethodNode; +import org.tudalgo.algoutils.transform.methods.InjectingMethodVisitor; import org.tudalgo.algoutils.transform.util.*; import org.objectweb.asm.*; @@ -136,15 +138,15 @@ public void visit(int version, int access, String name, String signature, String public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) { FieldHeader fieldHeader = submissionClassInfo.getComputedFieldHeader(name); - if (TransformationUtils.contextIsCompatible(access, fieldHeader.access())) { - visitedFields.add(fieldHeader); - return fieldHeader.toFieldVisitor(getDelegate(), value); - } else { + if (!TransformationUtils.contextIsCompatible(access, fieldHeader.access()) || visitedFields.contains(fieldHeader)) { return super.visitField(TransformationUtils.transformAccess(access), name + "$submission", fieldHeader.descriptor(), fieldHeader.signature(), value); + } else { + visitedFields.add(fieldHeader); + return fieldHeader.toFieldVisitor(getDelegate(), value); } } @@ -160,8 +162,17 @@ public MethodVisitor visitMethod(int access, String name, String descriptor, Str } else { MethodHeader originalMethodHeader = new MethodHeader(originalClassHeader.name(), access, name, descriptor, signature, exceptions); MethodHeader computedMethodHeader = submissionClassInfo.getComputedMethodHeader(name, descriptor); + int[] originalParameterTypes = Arrays.stream(Type.getArgumentTypes(originalMethodHeader.descriptor())) + .mapToInt(Type::getSort) + .toArray(); + int originalReturnType = Type.getReturnType(originalMethodHeader.descriptor()).getSort(); + int[] computedParameterTypes = Arrays.stream(Type.getArgumentTypes(computedMethodHeader.descriptor())) + .mapToInt(Type::getSort) + .toArray(); + int computedReturnType = Type.getReturnType(computedMethodHeader.descriptor()).getSort(); + boolean headerMismatch = !(Arrays.equals(originalParameterTypes, computedParameterTypes) && originalReturnType == computedReturnType); - if (!TransformationUtils.contextIsCompatible(access, computedMethodHeader.access())) { + if (!TransformationUtils.contextIsCompatible(access, computedMethodHeader.access()) || visitedMethods.contains(computedMethodHeader) || headerMismatch) { computedMethodHeader = new MethodHeader(computedMethodHeader.owner(), access, name + "$submission", @@ -202,8 +213,17 @@ public void visitEnd() { .entrySet() .stream() .filter(entry -> !visitedMethods.contains(entry.getKey())) - .map(Map.Entry::getValue) - .forEach(methodNode -> methodNode.accept(getDelegate())); + .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 InjectingMethodVisitor(mv, transformationContext, submissionClassInfo, methodHeader)); + } + }); } injectClassMetadata(); diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionMethodVisitor.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionMethodVisitor.java index b40314c4..139b2bad 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionMethodVisitor.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionMethodVisitor.java @@ -2,14 +2,13 @@ import org.objectweb.asm.*; import org.objectweb.asm.tree.MethodNode; +import org.tudalgo.algoutils.transform.methods.BaseMethodVisitor; import org.tudalgo.algoutils.transform.util.*; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.Optional; import java.util.concurrent.atomic.AtomicInteger; -import java.util.stream.Collectors; import java.util.stream.Stream; import static org.objectweb.asm.Opcodes.*; @@ -23,24 +22,13 @@ * @see SubmissionClassVisitor * @author Daniel Mangold */ -class SubmissionMethodVisitor extends MethodVisitor { - - private final TransformationContext transformationContext; - private final MethodHeader originalMethodHeader; - private final MethodHeader computedMethodHeader; - private final SubmissionClassInfo submissionClassInfo; - - private final boolean headerMismatch; - private final boolean isStatic; - private final boolean isConstructor; +class SubmissionMethodVisitor extends BaseMethodVisitor { private final int submissionExecutionHandlerIndex; private final int methodHeaderIndex; private final int methodSubstitutionIndex; private final int constructorInvocationIndex; - private final List fullFrameLocals; - /** * Constructs a new {@link SubmissionMethodVisitor}. * @@ -54,44 +42,12 @@ class SubmissionMethodVisitor extends MethodVisitor { SubmissionClassInfo submissionClassInfo, MethodHeader originalMethodHeader, MethodHeader computedMethodHeader) { - super(ASM9, delegate); - this.transformationContext = transformationContext; - this.submissionClassInfo = submissionClassInfo; - this.originalMethodHeader = originalMethodHeader; - this.computedMethodHeader = computedMethodHeader; + super(delegate, transformationContext, submissionClassInfo, originalMethodHeader, computedMethodHeader); - this.isStatic = (computedMethodHeader.access() & ACC_STATIC) != 0; - this.isConstructor = computedMethodHeader.name().equals(""); - - // calculate length of locals array, including "this" if applicable - int nextLocalsIndex = (Type.getArgumentsAndReturnSizes(computedMethodHeader.descriptor()) >> 2) - (isStatic ? 1 : 0); this.submissionExecutionHandlerIndex = nextLocalsIndex; this.methodHeaderIndex = nextLocalsIndex + 1; this.methodSubstitutionIndex = nextLocalsIndex + 2; this.constructorInvocationIndex = nextLocalsIndex + 3; - - 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()); - } - - int[] originalParameterTypes = Arrays.stream(Type.getArgumentTypes(originalMethodHeader.descriptor())) - .mapToInt(Type::getSort) - .toArray(); - int originalReturnType = Type.getReturnType(originalMethodHeader.descriptor()).getSort(); - int[] computedParameterTypes = Arrays.stream(Type.getArgumentTypes(computedMethodHeader.descriptor())) - .mapToInt(Type::getSort) - .toArray(); - int computedReturnType = Type.getReturnType(computedMethodHeader.descriptor()).getSort(); - this.headerMismatch = !(Arrays.equals(originalParameterTypes, computedParameterTypes) && originalReturnType == computedReturnType); } @Override @@ -111,19 +67,19 @@ public void visitCode() { // Setup { // create SubmissionExecutionHandler$Internal instance and store in locals array - super.visitTypeInsn(NEW, Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_TYPE.getInternalName()); - super.visitInsn(DUP); + delegate.visitTypeInsn(NEW, Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_TYPE.getInternalName()); + delegate.visitInsn(DUP); Constants.SUBMISSION_EXECUTION_HANDLER_GET_INSTANCE.toMethodInsn(getDelegate(), false); Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_CONSTRUCTOR.toMethodInsn(getDelegate(), false); - super.visitVarInsn(ASTORE, submissionExecutionHandlerIndex); - super.visitLabel(submissionExecutionHandlerVarLabel); + delegate.visitVarInsn(ASTORE, submissionExecutionHandlerIndex); + delegate.visitLabel(submissionExecutionHandlerVarLabel); // replicate method header in bytecode and store in locals array computedMethodHeader.buildHeader(getDelegate()); - super.visitVarInsn(ASTORE, methodHeaderIndex); - super.visitLabel(methodHeaderVarLabel); + delegate.visitVarInsn(ASTORE, methodHeaderIndex); + delegate.visitLabel(methodHeaderVarLabel); - super.visitFrame(F_APPEND, + delegate.visitFrame(F_APPEND, 2, new Object[] {Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_TYPE.getInternalName(), computedMethodHeader.getType().getInternalName()}, 0, @@ -135,14 +91,14 @@ public void visitCode() { // Invocation logging { // check if invocation should be logged - super.visitVarInsn(ALOAD, submissionExecutionHandlerIndex); - super.visitVarInsn(ALOAD, methodHeaderIndex); + delegate.visitVarInsn(ALOAD, submissionExecutionHandlerIndex); + delegate.visitVarInsn(ALOAD, methodHeaderIndex); Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_LOG_INVOCATION.toMethodInsn(getDelegate(), false); - super.visitJumpInsn(IFEQ, substitutionCheckLabel); // jump to label if logInvocation(...) == false + delegate.visitJumpInsn(IFEQ, substitutionCheckLabel); // jump to label if logInvocation(...) == false // intercept parameters - super.visitVarInsn(ALOAD, submissionExecutionHandlerIndex); - super.visitVarInsn(ALOAD, methodHeaderIndex); + delegate.visitVarInsn(ALOAD, submissionExecutionHandlerIndex); + delegate.visitVarInsn(ALOAD, methodHeaderIndex); buildInvocation(Type.getArgumentTypes(computedMethodHeader.descriptor())); Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_ADD_INVOCATION.toMethodInsn(getDelegate(), false); } @@ -150,21 +106,21 @@ public void visitCode() { // Method substitution { // check if substitution exists for this method - super.visitFrame(F_SAME, 0, null, 0, null); - super.visitLabel(substitutionCheckLabel); - super.visitVarInsn(ALOAD, submissionExecutionHandlerIndex); - super.visitVarInsn(ALOAD, methodHeaderIndex); + delegate.visitFrame(F_SAME, 0, null, 0, null); + delegate.visitLabel(substitutionCheckLabel); + delegate.visitVarInsn(ALOAD, submissionExecutionHandlerIndex); + delegate.visitVarInsn(ALOAD, methodHeaderIndex); Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_USE_SUBSTITUTION.toMethodInsn(getDelegate(), false); - super.visitJumpInsn(IFEQ, solutionMethodNode.isPresent() ? delegationCheckLabel : submissionCodeLabel); // jump to label if useSubstitution(...) == false + delegate.visitJumpInsn(IFEQ, solutionMethodNode.isPresent() ? delegationCheckLabel : submissionCodeLabel); // jump to label if useSubstitution(...) == false // get substitution and execute it - super.visitVarInsn(ALOAD, submissionExecutionHandlerIndex); - super.visitVarInsn(ALOAD, methodHeaderIndex); + delegate.visitVarInsn(ALOAD, submissionExecutionHandlerIndex); + delegate.visitVarInsn(ALOAD, methodHeaderIndex); Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_GET_SUBSTITUTION.toMethodInsn(getDelegate(), false); - super.visitVarInsn(ASTORE, methodSubstitutionIndex); - super.visitFrame(F_APPEND, 1, new Object[] {Constants.METHOD_SUBSTITUTION_TYPE.getInternalName()}, 0, null); + delegate.visitVarInsn(ASTORE, methodSubstitutionIndex); + delegate.visitFrame(F_APPEND, 1, new Object[] {Constants.METHOD_SUBSTITUTION_TYPE.getInternalName()}, 0, null); fullFrameLocals.add(Constants.METHOD_SUBSTITUTION_TYPE.getInternalName()); - super.visitLabel(substitutionStartLabel); + delegate.visitLabel(substitutionStartLabel); if (isConstructor) { List superConstructors = submissionClassInfo.getOriginalSuperClassConstructorHeaders() @@ -175,6 +131,7 @@ public void visitCode() { .stream() .filter(mh -> mh.name().equals("")) .map(mh -> submissionClassInfo.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) @@ -195,12 +152,12 @@ public void visitCode() { * throw new IllegalArgumentException(...); * } */ - super.visitVarInsn(ALOAD, methodSubstitutionIndex); + delegate.visitVarInsn(ALOAD, methodSubstitutionIndex); Constants.METHOD_SUBSTITUTION_GET_CONSTRUCTOR_INVOCATION.toMethodInsn(getDelegate(), true); - super.visitVarInsn(ASTORE, constructorInvocationIndex); - super.visitFrame(F_APPEND, 1, new Object[] {Constants.METHOD_SUBSTITUTION_CONSTRUCTOR_INVOCATION_TYPE.getInternalName()}, 0, null); + delegate.visitVarInsn(ASTORE, constructorInvocationIndex); + 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()); - super.visitLabel(labels[0]); + delegate.visitLabel(labels[0]); for (MethodHeader superConstructorHeader : superConstructors) { buildConstructorInvocationBranch(superConstructorHeader, substitutionExecuteLabel, labels, labelIndex); } @@ -211,42 +168,42 @@ public void visitCode() { // if no matching constructor was found, throw an IllegalArgumentException { Type illegalArgumentExceptionType = Type.getType(IllegalArgumentException.class); - super.visitTypeInsn(NEW, illegalArgumentExceptionType.getInternalName()); - super.visitInsn(DUP); - - super.visitLdcInsn("No matching constructor was found for owner %s and descriptor %s"); - super.visitInsn(ICONST_2); - super.visitTypeInsn(ANEWARRAY, Constants.STRING_TYPE.getInternalName()); - super.visitInsn(DUP); - super.visitInsn(ICONST_0); - super.visitVarInsn(ALOAD, constructorInvocationIndex); + 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, constructorInvocationIndex); Constants.METHOD_SUBSTITUTION_CONSTRUCTOR_INVOCATION_OWNER.toMethodInsn(getDelegate(), false); - super.visitInsn(AASTORE); - super.visitInsn(DUP); - super.visitInsn(ICONST_1); - super.visitVarInsn(ALOAD, constructorInvocationIndex); + delegate.visitInsn(AASTORE); + delegate.visitInsn(DUP); + delegate.visitInsn(ICONST_1); + delegate.visitVarInsn(ALOAD, constructorInvocationIndex); Constants.METHOD_SUBSTITUTION_CONSTRUCTOR_INVOCATION_DESCRIPTOR.toMethodInsn(getDelegate(), false); - super.visitInsn(AASTORE); - super.visitMethodInsn(INVOKEVIRTUAL, + delegate.visitInsn(AASTORE); + delegate.visitMethodInsn(INVOKEVIRTUAL, Constants.STRING_TYPE.getInternalName(), "formatted", Type.getMethodDescriptor(Constants.STRING_TYPE, Constants.OBJECT_ARRAY_TYPE), false); - super.visitMethodInsn(INVOKESPECIAL, + delegate.visitMethodInsn(INVOKESPECIAL, illegalArgumentExceptionType.getInternalName(), "", Type.getMethodDescriptor(Type.VOID_TYPE, Constants.STRING_TYPE), false); - super.visitInsn(ATHROW); + delegate.visitInsn(ATHROW); } fullFrameLocals.removeLast(); List locals = new ArrayList<>(fullFrameLocals); locals.set(0, computedMethodHeader.owner()); - super.visitFrame(F_FULL, locals.size(), locals.toArray(), 0, null); - super.visitLabel(substitutionExecuteLabel); - super.visitLocalVariable("constructorInvocation", + delegate.visitFrame(F_FULL, locals.size(), locals.toArray(), 0, null); + delegate.visitLabel(substitutionExecuteLabel); + delegate.visitLocalVariable("constructorInvocation", Constants.METHOD_SUBSTITUTION_CONSTRUCTOR_INVOCATION_TYPE.getDescriptor(), null, labels[labelIndex.get()], @@ -254,18 +211,18 @@ public void visitCode() { constructorInvocationIndex); } - super.visitVarInsn(ALOAD, methodSubstitutionIndex); + delegate.visitVarInsn(ALOAD, methodSubstitutionIndex); buildInvocation(Type.getArgumentTypes(computedMethodHeader.descriptor())); Constants.METHOD_SUBSTITUTION_EXECUTE.toMethodInsn(getDelegate(), true); Type returnType = Type.getReturnType(computedMethodHeader.descriptor()); if (returnType.getSort() == Type.ARRAY || returnType.getSort() == Type.OBJECT) { - super.visitTypeInsn(CHECKCAST, returnType.getInternalName()); + delegate.visitTypeInsn(CHECKCAST, returnType.getInternalName()); } else { unboxType(getDelegate(), returnType); } - super.visitInsn(returnType.getOpcode(IRETURN)); - super.visitLabel(substitutionEndLabel); - super.visitLocalVariable("methodSubstitution", + delegate.visitInsn(returnType.getOpcode(IRETURN)); + delegate.visitLabel(substitutionEndLabel); + delegate.visitLocalVariable("methodSubstitution", Constants.METHOD_SUBSTITUTION_TYPE.getDescriptor(), null, substitutionStartLabel, @@ -278,25 +235,25 @@ public void visitCode() { if (solutionMethodNode.isPresent()) { // check if call should be delegated to solution or not fullFrameLocals.removeLast(); - super.visitFrame(F_FULL, fullFrameLocals.size(), fullFrameLocals.toArray(), 0, new Object[0]); - super.visitLabel(delegationCheckLabel); - super.visitVarInsn(ALOAD, submissionExecutionHandlerIndex); - super.visitVarInsn(ALOAD, methodHeaderIndex); + delegate.visitFrame(F_FULL, fullFrameLocals.size(), fullFrameLocals.toArray(), 0, new Object[0]); + delegate.visitLabel(delegationCheckLabel); + delegate.visitVarInsn(ALOAD, submissionExecutionHandlerIndex); + delegate.visitVarInsn(ALOAD, methodHeaderIndex); Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_USE_SUBMISSION_IMPL.toMethodInsn(getDelegate(), false); - super.visitJumpInsn(IFNE, submissionCodeLabel); // jump to label if useSubmissionImpl(...) == true + delegate.visitJumpInsn(IFNE, submissionCodeLabel); // jump to label if useSubmissionImpl(...) == true // replay instructions from solution - super.visitFrame(F_CHOP, 2, null, 0, null); + delegate.visitFrame(F_CHOP, 2, null, 0, null); fullFrameLocals.removeLast(); fullFrameLocals.removeLast(); - super.visitLabel(delegationCodeLabel); - super.visitLocalVariable("submissionExecutionHandler", + delegate.visitLabel(delegationCodeLabel); + delegate.visitLocalVariable("submissionExecutionHandler", Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_TYPE.getDescriptor(), null, submissionExecutionHandlerVarLabel, delegationCodeLabel, submissionExecutionHandlerIndex); - super.visitLocalVariable("methodHeader", + delegate.visitLocalVariable("methodHeader", computedMethodHeader.getType().getDescriptor(), null, methodHeaderVarLabel, @@ -304,21 +261,21 @@ public void visitCode() { methodHeaderIndex); solutionMethodNode.get().accept(getDelegate()); - super.visitFrame(F_FULL, fullFrameLocals.size(), fullFrameLocals.toArray(), 0, new Object[0]); - super.visitLabel(submissionCodeLabel); + delegate.visitFrame(F_FULL, fullFrameLocals.size(), fullFrameLocals.toArray(), 0, new Object[0]); + delegate.visitLabel(submissionCodeLabel); } else { fullFrameLocals.removeLast(); fullFrameLocals.removeLast(); fullFrameLocals.removeLast(); - super.visitFrame(F_FULL, fullFrameLocals.size(), fullFrameLocals.toArray(), 0, new Object[0]); - super.visitLabel(submissionCodeLabel); - super.visitLocalVariable("submissionExecutionHandler", + delegate.visitFrame(F_FULL, fullFrameLocals.size(), fullFrameLocals.toArray(), 0, new Object[0]); + delegate.visitLabel(submissionCodeLabel); + delegate.visitLocalVariable("submissionExecutionHandler", Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_TYPE.getDescriptor(), null, submissionExecutionHandlerVarLabel, submissionCodeLabel, submissionExecutionHandlerIndex); - super.visitLocalVariable("methodHeader", + delegate.visitLocalVariable("methodHeader", computedMethodHeader.getType().getDescriptor(), null, methodHeaderVarLabel, @@ -331,7 +288,7 @@ public void visitCode() { .replicateInBytecode(getDelegate(), true); } else { // visit original code - super.visitCode(); + delegate.visitCode(); } } @@ -341,22 +298,22 @@ public void visitFieldInsn(int opcode, String owner, String name, String descrip // skip transformation if owner is not part of the submission if (!transformationContext.isSubmissionClass(owner)) { - super.visitFieldInsn(opcode, owner, name, descriptor); + delegate.visitFieldInsn(opcode, owner, name, descriptor); } else { if (owner.startsWith("[")) { // stupid edge cases - super.visitFieldInsn(opcode, + delegate.visitFieldInsn(opcode, transformationContext.toComputedType(owner).getInternalName(), name, transformationContext.toComputedType(descriptor).getDescriptor()); } else { FieldHeader computedFieldHeader = transformationContext.getSubmissionClassInfo(owner).getComputedFieldHeader(name); if (TransformationUtils.opcodeIsCompatible(opcode, computedFieldHeader.access())) { - super.visitFieldInsn(opcode, + delegate.visitFieldInsn(opcode, transformationContext.toComputedType(computedFieldHeader.owner()).getInternalName(), computedFieldHeader.name(), computedFieldHeader.descriptor()); } else { // if incompatible - super.visitFieldInsn(opcode, computedFieldHeader.owner(), name + "$submission", computedFieldHeader.descriptor()); + delegate.visitFieldInsn(opcode, computedFieldHeader.owner(), name + "$submission", computedFieldHeader.descriptor()); } } } @@ -371,20 +328,21 @@ public void visitMethodInsn(int opcode, String owner, String name, String descri if (transformationContext.methodHasReplacement(methodHeader)) { transformationContext.getMethodReplacement(methodHeader).toMethodInsn(getDelegate(), false); } else if (!transformationContext.isSubmissionClass(owner)) { - super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); + delegate.visitMethodInsn(opcode, owner, name, descriptor, isInterface); } else if (owner.startsWith("[")) { // stupid edge cases - super.visitMethodInsn(opcode, + delegate.visitMethodInsn(opcode, transformationContext.toComputedType(owner).getInternalName(), name, transformationContext.toComputedType(descriptor).getDescriptor(), isInterface); } else { + String computedOwner = transformationContext.toComputedType(owner).getInternalName(); methodHeader = transformationContext.getSubmissionClassInfo(owner).getComputedMethodHeader(name, descriptor); if (TransformationUtils.opcodeIsCompatible(opcode, methodHeader.access())) { - super.visitMethodInsn(opcode, methodHeader.owner(), methodHeader.name(), methodHeader.descriptor(), isInterface); + delegate.visitMethodInsn(opcode, computedOwner, methodHeader.name(), methodHeader.descriptor(), isInterface); } else { - super.visitMethodInsn(opcode, - methodHeader.owner(), + delegate.visitMethodInsn(opcode, + computedOwner, name + "$submission", transformationContext.toComputedType(descriptor).getDescriptor(), isInterface); @@ -392,20 +350,6 @@ public void visitMethodInsn(int opcode, String owner, String name, String descri } } - @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.toComputedType(type).getInternalName()); - } - /** * Builds an {@link Invocation} in bytecode. * @@ -415,30 +359,30 @@ private void buildInvocation(Type[] argumentTypes) { Type threadType = Type.getType(Thread.class); Type stackTraceElementArrayType = Type.getType(StackTraceElement[].class); - super.visitTypeInsn(NEW, Constants.INVOCATION_TYPE.getInternalName()); - super.visitInsn(DUP); - super.visitLdcInsn(Type.getObjectType(computedMethodHeader.owner())); + delegate.visitTypeInsn(NEW, Constants.INVOCATION_TYPE.getInternalName()); + delegate.visitInsn(DUP); + delegate.visitLdcInsn(Type.getObjectType(computedMethodHeader.owner())); computedMethodHeader.buildHeader(getDelegate()); - super.visitMethodInsn(INVOKESTATIC, + delegate.visitMethodInsn(INVOKESTATIC, threadType.getInternalName(), "currentThread", Type.getMethodDescriptor(threadType), false); - super.visitMethodInsn(INVOKEVIRTUAL, + delegate.visitMethodInsn(INVOKEVIRTUAL, threadType.getInternalName(), "getStackTrace", Type.getMethodDescriptor(stackTraceElementArrayType), false); if (!isStatic && !isConstructor) { - super.visitVarInsn(ALOAD, 0); + delegate.visitVarInsn(ALOAD, 0); Constants.INVOCATION_CONSTRUCTOR_WITH_INSTANCE.toMethodInsn(getDelegate(), false); } else { Constants.INVOCATION_CONSTRUCTOR.toMethodInsn(getDelegate(), false); } // load parameter with opcode (ALOAD, ILOAD, etc.) for type and ignore "this", if it exists for (int i = 0; i < argumentTypes.length; i++) { - super.visitInsn(DUP); - super.visitVarInsn(argumentTypes[i].getOpcode(ILOAD), getLocalsIndex(argumentTypes, i) + (isStatic ? 0 : 1)); + delegate.visitInsn(DUP); + delegate.visitVarInsn(argumentTypes[i].getOpcode(ILOAD), getLocalsIndex(argumentTypes, i) + (isStatic ? 0 : 1)); boxType(getDelegate(), argumentTypes[i]); Constants.INVOCATION_CONSTRUCTOR_ADD_PARAMETER.toMethodInsn(getDelegate(), false); } @@ -448,182 +392,54 @@ private void buildConstructorInvocationBranch(MethodHeader constructorHeader, Label substitutionExecuteLabel, Label[] labels, AtomicInteger labelIndex) { - super.visitVarInsn(ALOAD, constructorInvocationIndex); + delegate.visitVarInsn(ALOAD, constructorInvocationIndex); Constants.METHOD_SUBSTITUTION_CONSTRUCTOR_INVOCATION_OWNER.toMethodInsn(getDelegate(), false); - super.visitLdcInsn(constructorHeader.owner()); - super.visitMethodInsn(INVOKEVIRTUAL, + delegate.visitLdcInsn(constructorHeader.owner()); + delegate.visitMethodInsn(INVOKEVIRTUAL, Constants.STRING_TYPE.getInternalName(), "equals", Type.getMethodDescriptor(Type.BOOLEAN_TYPE, Constants.OBJECT_TYPE), false); - super.visitVarInsn(ALOAD, constructorInvocationIndex); + delegate.visitVarInsn(ALOAD, constructorInvocationIndex); Constants.METHOD_SUBSTITUTION_CONSTRUCTOR_INVOCATION_DESCRIPTOR.toMethodInsn(getDelegate(), false); - super.visitLdcInsn(constructorHeader.descriptor()); - super.visitMethodInsn(INVOKEVIRTUAL, + delegate.visitLdcInsn(constructorHeader.descriptor()); + delegate.visitMethodInsn(INVOKEVIRTUAL, Constants.STRING_TYPE.getInternalName(), "equals", Type.getMethodDescriptor(Type.BOOLEAN_TYPE, Constants.OBJECT_TYPE), false); - super.visitInsn(IAND); - super.visitJumpInsn(IFEQ, labels[labelIndex.get() + 1]); // jump to next branch if false + delegate.visitInsn(IAND); + delegate.visitJumpInsn(IFEQ, labels[labelIndex.get() + 1]); // jump to next branch if false Label argsVarStartLabel = new Label(); - super.visitVarInsn(ALOAD, constructorInvocationIndex); + delegate.visitVarInsn(ALOAD, constructorInvocationIndex); Constants.METHOD_SUBSTITUTION_CONSTRUCTOR_INVOCATION_ARGS.toMethodInsn(getDelegate(), false); - super.visitVarInsn(ASTORE, constructorInvocationIndex + 1); - super.visitFrame(F_APPEND, 1, new Object[] {Constants.OBJECT_ARRAY_TYPE.getInternalName()}, 0, null); + delegate.visitVarInsn(ASTORE, constructorInvocationIndex + 1); + delegate.visitFrame(F_APPEND, 1, new Object[] {Constants.OBJECT_ARRAY_TYPE.getInternalName()}, 0, null); fullFrameLocals.add(Constants.OBJECT_ARRAY_TYPE.getInternalName()); - super.visitLabel(argsVarStartLabel); + delegate.visitLabel(argsVarStartLabel); - super.visitVarInsn(ALOAD, 0); + delegate.visitVarInsn(ALOAD, 0); Type[] parameterTypes = Type.getArgumentTypes(constructorHeader.descriptor()); for (int i = 0; i < parameterTypes.length; i++) { // unpack array - super.visitVarInsn(ALOAD, constructorInvocationIndex + 1); - super.visitIntInsn(SIPUSH, i); - super.visitInsn(AALOAD); + delegate.visitVarInsn(ALOAD, constructorInvocationIndex + 1); + delegate.visitIntInsn(SIPUSH, i); + delegate.visitInsn(AALOAD); unboxType(getDelegate(), parameterTypes[i]); } constructorHeader.toMethodInsn(getDelegate(), false); - super.visitJumpInsn(GOTO, substitutionExecuteLabel); + delegate.visitJumpInsn(GOTO, substitutionExecuteLabel); fullFrameLocals.removeLast(); - super.visitFrame(F_CHOP, 1, null, 0, new Object[0]); - super.visitLabel(labels[labelIndex.incrementAndGet()]); - super.visitLocalVariable("args", + 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()], constructorInvocationIndex + 1); } - - // Prevent bytecode to be added to the method if there is a header mismatch - - @Override - public void visitFrame(int type, int numLocal, Object[] local, int numStack, Object[] stack) { - if (!headerMismatch) { - Object[] computedLocals = Arrays.stream(local) - .map(o -> o instanceof String s ? transformationContext.toComputedType(s).getInternalName() : o) - .toArray(); - Object[] computedStack = Arrays.stream(stack) - .map(o -> o instanceof String s ? transformationContext.toComputedType(s).getInternalName() : o) - .toArray(); - super.visitFrame(type, numLocal, computedLocals, numStack, computedStack); - } - } - - @Override - public void visitInsn(int opcode) { - if (!headerMismatch) { - super.visitInsn(opcode); - } - } - - @Override - public void visitIntInsn(int opcode, int operand) { - if (!headerMismatch) { - super.visitIntInsn(opcode, operand); - } - } - - @Override - public void visitIincInsn(int varIndex, int increment) { - if (!headerMismatch) { - super.visitIincInsn(varIndex, increment); - } - } - - @Override - public void visitVarInsn(int opcode, int varIndex) { - if (!headerMismatch) { - super.visitVarInsn(opcode, varIndex); - } - } - - @Override - public void visitJumpInsn(int opcode, Label label) { - if (!headerMismatch) { - super.visitJumpInsn(opcode, label); - } - } - - @Override - public void visitInvokeDynamicInsn(String name, String descriptor, Handle bootstrapMethodHandle, Object... bootstrapMethodArguments) { - if (headerMismatch) return; - - super.visitInvokeDynamicInsn(name, descriptor, bootstrapMethodHandle, bootstrapMethodArguments); - } - - @Override - public void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels) { - if (!headerMismatch) { - super.visitLookupSwitchInsn(dflt, keys, labels); - } - } - - @Override - public void visitTableSwitchInsn(int min, int max, Label dflt, Label... labels) { - if (!headerMismatch) { - super.visitTableSwitchInsn(min, max, dflt, labels); - } - } - - @Override - public void visitMultiANewArrayInsn(String descriptor, int numDimensions) { - if (headerMismatch) return; - - super.visitMultiANewArrayInsn(transformationContext.toComputedType(descriptor).getDescriptor(), numDimensions); - } - - @Override - public void visitLabel(Label label) { - if (!headerMismatch) { - 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.toComputedType(type).getInternalName() : 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) 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) { - super.visitLineNumber(line, start); - } - } - - @Override - public void visitAttribute(Attribute attribute) { - if (!headerMismatch) { - super.visitAttribute(attribute); - } - } } 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..7ba961af --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/methods/BaseMethodVisitor.java @@ -0,0 +1,212 @@ +package org.tudalgo.algoutils.transform.methods; + +import org.objectweb.asm.*; +import org.tudalgo.algoutils.transform.SubmissionClassInfo; +import org.tudalgo.algoutils.transform.util.MethodHeader; +import org.tudalgo.algoutils.transform.util.TransformationContext; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import static org.objectweb.asm.Opcodes.*; + +public abstract class BaseMethodVisitor extends MethodVisitor { + + protected final MethodVisitor delegate; + protected final TransformationContext transformationContext; + protected final SubmissionClassInfo submissionClassInfo; + 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, + SubmissionClassInfo submissionClassInfo, + MethodHeader originalMethodHeader, + MethodHeader computedMethodHeader) { + super(ASM9, delegate); + this.delegate = delegate; + this.transformationContext = transformationContext; + this.submissionClassInfo = submissionClassInfo; + this.originalMethodHeader = originalMethodHeader; + this.computedMethodHeader = computedMethodHeader; + + 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()); + } + + int[] originalParameterTypes = Arrays.stream(Type.getArgumentTypes(originalMethodHeader.descriptor())) + .mapToInt(Type::getSort) + .toArray(); + int originalReturnType = Type.getReturnType(originalMethodHeader.descriptor()).getSort(); + int[] computedParameterTypes = Arrays.stream(Type.getArgumentTypes(computedMethodHeader.descriptor())) + .mapToInt(Type::getSort) + .toArray(); + int computedReturnType = Type.getReturnType(computedMethodHeader.descriptor()).getSort(); + this.headerMismatch = !(Arrays.equals(originalParameterTypes, computedParameterTypes) && originalReturnType == computedReturnType); + } + + // Prevent bytecode to be added to the method if there is a header mismatch + + @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.toComputedType(type).getInternalName()); + } + + @Override + public void visitFrame(int type, int numLocal, Object[] local, int numStack, Object[] stack) { + if (!headerMismatch) { + Object[] computedLocals = local == null ? null : Arrays.stream(local) + .map(o -> o instanceof String s ? transformationContext.toComputedType(s).getInternalName() : o) + .toArray(); + Object[] computedStack = stack == null ? null : Arrays.stream(stack) + .map(o -> o instanceof String s ? transformationContext.toComputedType(s).getInternalName() : o) + .toArray(); + super.visitFrame(type, numLocal, computedLocals, numStack, computedStack); + } + } + + @Override + public void visitInsn(int opcode) { + if (!headerMismatch) { + super.visitInsn(opcode); + } + } + + @Override + public void visitIntInsn(int opcode, int operand) { + if (!headerMismatch) { + super.visitIntInsn(opcode, operand); + } + } + + @Override + public void visitIincInsn(int varIndex, int increment) { + if (!headerMismatch) { + super.visitIincInsn(varIndex, increment); + } + } + + @Override + public void visitVarInsn(int opcode, int varIndex) { + if (!headerMismatch) { + super.visitVarInsn(opcode, varIndex); + } + } + + @Override + public void visitJumpInsn(int opcode, Label label) { + if (!headerMismatch) { + super.visitJumpInsn(opcode, label); + } + } + + @Override + public void visitInvokeDynamicInsn(String name, String descriptor, Handle bootstrapMethodHandle, Object... bootstrapMethodArguments) { + if (headerMismatch) return; + + super.visitInvokeDynamicInsn(name, descriptor, bootstrapMethodHandle, bootstrapMethodArguments); + } + + @Override + public void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels) { + if (!headerMismatch) { + super.visitLookupSwitchInsn(dflt, keys, labels); + } + } + + @Override + public void visitTableSwitchInsn(int min, int max, Label dflt, Label... labels) { + if (!headerMismatch) { + super.visitTableSwitchInsn(min, max, dflt, labels); + } + } + + @Override + public void visitMultiANewArrayInsn(String descriptor, int numDimensions) { + if (headerMismatch) return; + + super.visitMultiANewArrayInsn(transformationContext.toComputedType(descriptor).getDescriptor(), numDimensions); + } + + @Override + public void visitLabel(Label label) { + if (!headerMismatch) { + 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.toComputedType(type).getInternalName() : 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) 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) { + super.visitLineNumber(line, start); + } + } + + @Override + public void visitAttribute(Attribute attribute) { + if (!headerMismatch) { + super.visitAttribute(attribute); + } + } +} diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/methods/InjectingMethodVisitor.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/methods/InjectingMethodVisitor.java new file mode 100644 index 00000000..bc73c354 --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/methods/InjectingMethodVisitor.java @@ -0,0 +1,386 @@ +package org.tudalgo.algoutils.transform.methods; + +import org.objectweb.asm.Label; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Type; +import org.tudalgo.algoutils.transform.SubmissionClassInfo; +import org.tudalgo.algoutils.transform.util.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Stream; + +import static org.objectweb.asm.Opcodes.*; +import static org.objectweb.asm.Opcodes.F_CHOP; +import static org.tudalgo.algoutils.transform.util.TransformationUtils.*; +import static org.tudalgo.algoutils.transform.util.TransformationUtils.unboxType; + +public class InjectingMethodVisitor extends BaseMethodVisitor { + + private final int submissionExecutionHandlerIndex; + private final int methodHeaderIndex; + private final int methodSubstitutionIndex; + private final int constructorInvocationIndex; + + public InjectingMethodVisitor(MethodVisitor delegate, + TransformationContext transformationContext, + SubmissionClassInfo submissionClassInfo, + MethodHeader methodHeader) { + super(delegate, transformationContext, submissionClassInfo, methodHeader, methodHeader); + + this.submissionExecutionHandlerIndex = nextLocalsIndex; + this.methodHeaderIndex = nextLocalsIndex + 1; + this.methodSubstitutionIndex = nextLocalsIndex + 2; + this.constructorInvocationIndex = nextLocalsIndex + 3; + } + + @Override + public void visitCode() { + Label submissionExecutionHandlerVarLabel = new Label(); + Label methodHeaderVarLabel = new Label(); + Label substitutionCheckLabel = new Label(); + Label substitutionStartLabel = new Label(); + Label substitutionEndLabel = new Label(); + Label delegationCheckLabel = new Label(); + Label delegationCodeLabel = new Label(); + Label submissionCodeLabel = new Label(); + + // Setup + { + // create SubmissionExecutionHandler$Internal instance and store in locals array + delegate.visitTypeInsn(NEW, Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_TYPE.getInternalName()); + delegate.visitInsn(DUP); + Constants.SUBMISSION_EXECUTION_HANDLER_GET_INSTANCE.toMethodInsn(getDelegate(), false); + Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_CONSTRUCTOR.toMethodInsn(getDelegate(), false); + delegate.visitVarInsn(ASTORE, submissionExecutionHandlerIndex); + delegate.visitLabel(submissionExecutionHandlerVarLabel); + + // replicate method header in bytecode and store in locals array + computedMethodHeader.buildHeader(getDelegate()); + delegate.visitVarInsn(ASTORE, methodHeaderIndex); + delegate.visitLabel(methodHeaderVarLabel); + + delegate.visitFrame(F_APPEND, + 2, + new Object[] {Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_TYPE.getInternalName(), computedMethodHeader.getType().getInternalName()}, + 0, + null); + fullFrameLocals.add(Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_TYPE.getInternalName()); + fullFrameLocals.add(computedMethodHeader.getType().getInternalName()); + } + + // Invocation logging + { + // check if invocation should be logged + delegate.visitVarInsn(ALOAD, submissionExecutionHandlerIndex); + delegate.visitVarInsn(ALOAD, methodHeaderIndex); + Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_LOG_INVOCATION.toMethodInsn(getDelegate(), false); + delegate.visitJumpInsn(IFEQ, substitutionCheckLabel); // jump to label if logInvocation(...) == false + + // intercept parameters + delegate.visitVarInsn(ALOAD, submissionExecutionHandlerIndex); + delegate.visitVarInsn(ALOAD, methodHeaderIndex); + buildInvocation(Type.getArgumentTypes(computedMethodHeader.descriptor())); + Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_ADD_INVOCATION.toMethodInsn(getDelegate(), false); + } + + // Method substitution + { + // check if substitution exists for this method + delegate.visitFrame(F_SAME, 0, null, 0, null); + delegate.visitLabel(substitutionCheckLabel); + delegate.visitVarInsn(ALOAD, submissionExecutionHandlerIndex); + delegate.visitVarInsn(ALOAD, methodHeaderIndex); + Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_USE_SUBSTITUTION.toMethodInsn(getDelegate(), false); + delegate.visitJumpInsn(IFEQ, delegationCheckLabel); // jump to label if useSubstitution(...) == false + + // get substitution and execute it + delegate.visitVarInsn(ALOAD, submissionExecutionHandlerIndex); + delegate.visitVarInsn(ALOAD, methodHeaderIndex); + Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_GET_SUBSTITUTION.toMethodInsn(getDelegate(), false); + delegate.visitVarInsn(ASTORE, methodSubstitutionIndex); + 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 = submissionClassInfo.getOriginalSuperClassConstructorHeaders() + .stream() + .map(mh -> submissionClassInfo.getComputedSuperClassConstructorHeader(mh.descriptor())) + .toList(); + List constructors = submissionClassInfo.getOriginalMethodHeaders() + .stream() + .filter(mh -> mh.name().equals("")) + .map(mh -> submissionClassInfo.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, methodSubstitutionIndex); + Constants.METHOD_SUBSTITUTION_GET_CONSTRUCTOR_INVOCATION.toMethodInsn(getDelegate(), true); + delegate.visitVarInsn(ASTORE, constructorInvocationIndex); + 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) { + buildConstructorInvocationBranch(superConstructorHeader, substitutionExecuteLabel, labels, labelIndex); + } + for (MethodHeader constructorHeader : constructors) { + buildConstructorInvocationBranch(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, constructorInvocationIndex); + Constants.METHOD_SUBSTITUTION_CONSTRUCTOR_INVOCATION_OWNER.toMethodInsn(getDelegate(), false); + delegate.visitInsn(AASTORE); + delegate.visitInsn(DUP); + delegate.visitInsn(ICONST_1); + delegate.visitVarInsn(ALOAD, constructorInvocationIndex); + Constants.METHOD_SUBSTITUTION_CONSTRUCTOR_INVOCATION_DESCRIPTOR.toMethodInsn(getDelegate(), 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); + delegate.visitLocalVariable("constructorInvocation", + Constants.METHOD_SUBSTITUTION_CONSTRUCTOR_INVOCATION_TYPE.getDescriptor(), + null, + labels[labelIndex.get()], + substitutionExecuteLabel, + constructorInvocationIndex); + } + + delegate.visitVarInsn(ALOAD, methodSubstitutionIndex); + buildInvocation(Type.getArgumentTypes(computedMethodHeader.descriptor())); + Constants.METHOD_SUBSTITUTION_EXECUTE.toMethodInsn(getDelegate(), true); + Type returnType = Type.getReturnType(computedMethodHeader.descriptor()); + if (returnType.getSort() == Type.ARRAY || returnType.getSort() == Type.OBJECT) { + delegate.visitTypeInsn(CHECKCAST, returnType.getInternalName()); + } else { + unboxType(getDelegate(), returnType); + } + delegate.visitInsn(returnType.getOpcode(IRETURN)); + delegate.visitLabel(substitutionEndLabel); + delegate.visitLocalVariable("methodSubstitution", + Constants.METHOD_SUBSTITUTION_TYPE.getDescriptor(), + null, + substitutionStartLabel, + substitutionEndLabel, + methodSubstitutionIndex); + } + + // Method delegation + // if only default transformations are applied, skip delegation + { + // check if call should be delegated to solution or not + fullFrameLocals.removeLast(); + delegate.visitFrame(F_FULL, fullFrameLocals.size(), fullFrameLocals.toArray(), 0, new Object[0]); + delegate.visitLabel(delegationCheckLabel); + delegate.visitVarInsn(ALOAD, submissionExecutionHandlerIndex); + delegate.visitVarInsn(ALOAD, methodHeaderIndex); + 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, 2, null, 0, null); + fullFrameLocals.removeLast(); + fullFrameLocals.removeLast(); + delegate.visitLabel(delegationCodeLabel); + delegate.visitLocalVariable("submissionExecutionHandler", + Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_TYPE.getDescriptor(), + null, + submissionExecutionHandlerVarLabel, + delegationCodeLabel, + submissionExecutionHandlerIndex); + delegate.visitLocalVariable("methodHeader", + computedMethodHeader.getType().getDescriptor(), + null, + methodHeaderVarLabel, + delegationCodeLabel, + methodHeaderIndex); + new IncompatibleHeaderException("Method has incorrect return or parameter types", computedMethodHeader, null) + .replicateInBytecode(getDelegate(), true); + + delegate.visitFrame(F_FULL, fullFrameLocals.size(), fullFrameLocals.toArray(), 0, new Object[0]); + delegate.visitLabel(submissionCodeLabel); + } + + delegate.visitCode(); + } + + @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 { + if (owner.startsWith("[")) { // stupid edge cases + delegate.visitFieldInsn(opcode, + transformationContext.toComputedType(owner).getInternalName(), + name, + transformationContext.toComputedType(descriptor).getDescriptor()); + } else { + FieldHeader computedFieldHeader = transformationContext.getSubmissionClassInfo(owner).getComputedFieldHeader(name); + if (TransformationUtils.opcodeIsCompatible(opcode, computedFieldHeader.access())) { + delegate.visitFieldInsn(opcode, + transformationContext.toComputedType(computedFieldHeader.owner()).getInternalName(), + computedFieldHeader.name(), + computedFieldHeader.descriptor()); + } else { // if incompatible + delegate.visitFieldInsn(opcode, computedFieldHeader.owner(), name + "$submission", computedFieldHeader.descriptor()); + } + } + } + } + + @Override + public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) { + if (headerMismatch) return; + + // skip transformation if owner is not part of the submission + MethodHeader methodHeader = new MethodHeader(owner, 0, name, descriptor, null, null); + if (transformationContext.methodHasReplacement(methodHeader)) { + transformationContext.getMethodReplacement(methodHeader).toMethodInsn(getDelegate(), false); + } else { + delegate.visitMethodInsn(opcode, owner, name, descriptor, isInterface); + } + } + + /** + * Builds an {@link Invocation} in bytecode. + * + * @param argumentTypes an array of parameter types + */ + private void buildInvocation(Type[] argumentTypes) { + 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(getDelegate()); + 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(getDelegate(), false); + } else { + Constants.INVOCATION_CONSTRUCTOR.toMethodInsn(getDelegate(), false); + } + // 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), getLocalsIndex(argumentTypes, i) + (isStatic ? 0 : 1)); + boxType(getDelegate(), argumentTypes[i]); + Constants.INVOCATION_CONSTRUCTOR_ADD_PARAMETER.toMethodInsn(getDelegate(), false); + } + } + + private void buildConstructorInvocationBranch(MethodHeader constructorHeader, + Label substitutionExecuteLabel, + Label[] labels, + AtomicInteger labelIndex) { + delegate.visitVarInsn(ALOAD, constructorInvocationIndex); + Constants.METHOD_SUBSTITUTION_CONSTRUCTOR_INVOCATION_OWNER.toMethodInsn(getDelegate(), 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, constructorInvocationIndex); + Constants.METHOD_SUBSTITUTION_CONSTRUCTOR_INVOCATION_DESCRIPTOR.toMethodInsn(getDelegate(), 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, constructorInvocationIndex); + Constants.METHOD_SUBSTITUTION_CONSTRUCTOR_INVOCATION_ARGS.toMethodInsn(getDelegate(), false); + delegate.visitVarInsn(ASTORE, constructorInvocationIndex + 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, constructorInvocationIndex + 1); + delegate.visitIntInsn(SIPUSH, i); + delegate.visitInsn(AALOAD); + unboxType(getDelegate(), parameterTypes[i]); + } + constructorHeader.toMethodInsn(getDelegate(), 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()], + constructorInvocationIndex + 1); + } +} 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 index ab427584..7e345890 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/IncompatibleHeaderException.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/IncompatibleHeaderException.java @@ -33,7 +33,7 @@ public class IncompatibleHeaderException extends RuntimeException { * * @param message the exception message * @param expected the expected header - * @param actual the actual header + * @param actual the actual header, may be null */ public IncompatibleHeaderException(String message, Header expected, Header actual) { super(); @@ -64,7 +64,12 @@ public int replicateInBytecode(MethodVisitor mv, boolean throwException) { mv.visitLdcInsn(message); maxStack = stackSize = 3; maxStack = Math.max(maxStack, stackSize++ + expected.buildHeader(mv)); - maxStack = Math.max(maxStack, stackSize + actual.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(getClass()), "", 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 index 1b7a2208..57d0123f 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Invocation.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Invocation.java @@ -25,7 +25,7 @@ public class Invocation { private final List parameterValues = new ArrayList<>(); /** - * Constructs a new invocation (static / constructor variant). + * 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 @@ -35,7 +35,7 @@ public Invocation(Class declaringClass, MethodHeader methodHeader, StackTrace } /** - * Constructs a new invocation (non-static / non-constructor variant). + * 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 diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/SimilarityMapper.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/SimilarityMapper.java index 46900411..d44e5759 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/SimilarityMapper.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/SimilarityMapper.java @@ -102,5 +102,18 @@ private void computeSimilarity(Map> to, newPair.getSecond() > oldPair.getSecond() ? newPair : oldPair); } } + + // find and remove duplicate mappings + Map> reverseMapping = new HashMap<>(); // column => rows + bestMatches.forEach((t, pair) -> reverseMapping.computeIfAbsent(pair.getFirst(), k -> new Stack<>()).push(t)); + reverseMapping.entrySet() + .stream() + .filter(entry -> entry.getValue().size() > 1) + .forEach(entry -> { + Stack stack = entry.getValue(); + stack.sort(Comparator.comparingDouble(t -> bestMatches.get(t).getSecond())); + stack.pop(); // exclude the best match from removal + stack.forEach(bestMatches::remove); // remove the rest + }); } } 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 index 5eb4578e..21a42f02 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationUtils.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationUtils.java @@ -1,6 +1,5 @@ package org.tudalgo.algoutils.transform.util; -import org.opentest4j.AssertionFailedError; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Type; @@ -30,15 +29,16 @@ public static int transformAccess(int access) { /** * Whether the given members have the same execution context. - * Two members are considered to have the same execution context is they are either + * 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); + return (access1 & ACC_STATIC) == (access2 & ACC_STATIC) && (access1 & ACC_ABSTRACT) == (access2 & ACC_ABSTRACT); } /** 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 index 93d917dc..7c251b9d 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/tutor/general/SubmissionInfo.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/tutor/general/SubmissionInfo.java @@ -15,7 +15,7 @@ public record SubmissionInfo( ) { public record SourceSet(String name, Map> files) {} - public record DependencyConfiguration(List implementation, List testImplementation) {} + public record DependencyConfiguration(List api, List implementation, List testImplementation) {} public record RepositoryConfiguration(String name, String url) {} } From 527fa0681e4b212de0d8e2e0065292bb4b3b73fd Mon Sep 17 00:00:00 2001 From: Daniel Mangold Date: Tue, 3 Dec 2024 14:21:38 +0100 Subject: [PATCH 27/48] Refactor method visitors --- .../transform/SubmissionClassVisitor.java | 15 +- .../transform/SubmissionMethodVisitor.java | 445 ------------------ .../transform/methods/BaseMethodVisitor.java | 395 +++++++++++++++- .../methods/InjectingMethodVisitor.java | 331 +------------ .../methods/SubmissionMethodVisitor.java | 86 ++++ 5 files changed, 493 insertions(+), 779 deletions(-) delete mode 100644 tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionMethodVisitor.java create mode 100644 tutor/src/main/java/org/tudalgo/algoutils/transform/methods/SubmissionMethodVisitor.java diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java index 60cbdd7a..91f5b23d 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java @@ -2,6 +2,7 @@ import org.objectweb.asm.tree.MethodNode; import org.tudalgo.algoutils.transform.methods.InjectingMethodVisitor; +import org.tudalgo.algoutils.transform.methods.SubmissionMethodVisitor; import org.tudalgo.algoutils.transform.util.*; import org.objectweb.asm.*; @@ -100,7 +101,7 @@ * @see SubmissionExecutionHandler * @author Daniel Mangold */ -class SubmissionClassVisitor extends ClassVisitor { +public class SubmissionClassVisitor extends ClassVisitor { private final TransformationContext transformationContext; private final SubmissionClassInfo submissionClassInfo; @@ -162,15 +163,9 @@ public MethodVisitor visitMethod(int access, String name, String descriptor, Str } else { MethodHeader originalMethodHeader = new MethodHeader(originalClassHeader.name(), access, name, descriptor, signature, exceptions); MethodHeader computedMethodHeader = submissionClassInfo.getComputedMethodHeader(name, descriptor); - int[] originalParameterTypes = Arrays.stream(Type.getArgumentTypes(originalMethodHeader.descriptor())) - .mapToInt(Type::getSort) - .toArray(); - int originalReturnType = Type.getReturnType(originalMethodHeader.descriptor()).getSort(); - int[] computedParameterTypes = Arrays.stream(Type.getArgumentTypes(computedMethodHeader.descriptor())) - .mapToInt(Type::getSort) - .toArray(); - int computedReturnType = Type.getReturnType(computedMethodHeader.descriptor()).getSort(); - boolean headerMismatch = !(Arrays.equals(originalParameterTypes, computedParameterTypes) && originalReturnType == computedReturnType); + boolean headerMismatch = !transformationContext.toComputedType(originalMethodHeader.descriptor()) + .getDescriptor() + .equals(computedMethodHeader.descriptor()); if (!TransformationUtils.contextIsCompatible(access, computedMethodHeader.access()) || visitedMethods.contains(computedMethodHeader) || headerMismatch) { computedMethodHeader = new MethodHeader(computedMethodHeader.owner(), diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionMethodVisitor.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionMethodVisitor.java deleted file mode 100644 index 139b2bad..00000000 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionMethodVisitor.java +++ /dev/null @@ -1,445 +0,0 @@ -package org.tudalgo.algoutils.transform; - -import org.objectweb.asm.*; -import org.objectweb.asm.tree.MethodNode; -import org.tudalgo.algoutils.transform.methods.BaseMethodVisitor; -import org.tudalgo.algoutils.transform.util.*; - -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.stream.Stream; - -import static org.objectweb.asm.Opcodes.*; -import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL; -import static org.tudalgo.algoutils.transform.util.TransformationUtils.*; -import static org.tudalgo.algoutils.transform.util.TransformationUtils.boxType; - -/** - * A method visitor for transforming submission methods. - * - * @see SubmissionClassVisitor - * @author Daniel Mangold - */ -class SubmissionMethodVisitor extends BaseMethodVisitor { - - private final int submissionExecutionHandlerIndex; - private final int methodHeaderIndex; - private final int methodSubstitutionIndex; - private final int constructorInvocationIndex; - - /** - * 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 - */ - SubmissionMethodVisitor(MethodVisitor delegate, - TransformationContext transformationContext, - SubmissionClassInfo submissionClassInfo, - MethodHeader originalMethodHeader, - MethodHeader computedMethodHeader) { - super(delegate, transformationContext, submissionClassInfo, originalMethodHeader, computedMethodHeader); - - this.submissionExecutionHandlerIndex = nextLocalsIndex; - this.methodHeaderIndex = nextLocalsIndex + 1; - this.methodSubstitutionIndex = nextLocalsIndex + 2; - this.constructorInvocationIndex = nextLocalsIndex + 3; - } - - @Override - public void visitCode() { - Optional solutionMethodNode = submissionClassInfo.getSolutionClass() - .map(solutionClassNode -> solutionClassNode.getMethods().get(computedMethodHeader)); - - Label submissionExecutionHandlerVarLabel = new Label(); - Label methodHeaderVarLabel = new Label(); - Label substitutionCheckLabel = new Label(); - Label substitutionStartLabel = new Label(); - Label substitutionEndLabel = new Label(); - Label delegationCheckLabel = new Label(); - Label delegationCodeLabel = new Label(); - Label submissionCodeLabel = new Label(); - - // Setup - { - // create SubmissionExecutionHandler$Internal instance and store in locals array - delegate.visitTypeInsn(NEW, Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_TYPE.getInternalName()); - delegate.visitInsn(DUP); - Constants.SUBMISSION_EXECUTION_HANDLER_GET_INSTANCE.toMethodInsn(getDelegate(), false); - Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_CONSTRUCTOR.toMethodInsn(getDelegate(), false); - delegate.visitVarInsn(ASTORE, submissionExecutionHandlerIndex); - delegate.visitLabel(submissionExecutionHandlerVarLabel); - - // replicate method header in bytecode and store in locals array - computedMethodHeader.buildHeader(getDelegate()); - delegate.visitVarInsn(ASTORE, methodHeaderIndex); - delegate.visitLabel(methodHeaderVarLabel); - - delegate.visitFrame(F_APPEND, - 2, - new Object[] {Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_TYPE.getInternalName(), computedMethodHeader.getType().getInternalName()}, - 0, - null); - fullFrameLocals.add(Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_TYPE.getInternalName()); - fullFrameLocals.add(computedMethodHeader.getType().getInternalName()); - } - - // Invocation logging - { - // check if invocation should be logged - delegate.visitVarInsn(ALOAD, submissionExecutionHandlerIndex); - delegate.visitVarInsn(ALOAD, methodHeaderIndex); - Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_LOG_INVOCATION.toMethodInsn(getDelegate(), false); - delegate.visitJumpInsn(IFEQ, substitutionCheckLabel); // jump to label if logInvocation(...) == false - - // intercept parameters - delegate.visitVarInsn(ALOAD, submissionExecutionHandlerIndex); - delegate.visitVarInsn(ALOAD, methodHeaderIndex); - buildInvocation(Type.getArgumentTypes(computedMethodHeader.descriptor())); - Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_ADD_INVOCATION.toMethodInsn(getDelegate(), false); - } - - // Method substitution - { - // check if substitution exists for this method - delegate.visitFrame(F_SAME, 0, null, 0, null); - delegate.visitLabel(substitutionCheckLabel); - delegate.visitVarInsn(ALOAD, submissionExecutionHandlerIndex); - delegate.visitVarInsn(ALOAD, methodHeaderIndex); - Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_USE_SUBSTITUTION.toMethodInsn(getDelegate(), false); - delegate.visitJumpInsn(IFEQ, solutionMethodNode.isPresent() ? delegationCheckLabel : submissionCodeLabel); // jump to label if useSubstitution(...) == false - - // get substitution and execute it - delegate.visitVarInsn(ALOAD, submissionExecutionHandlerIndex); - delegate.visitVarInsn(ALOAD, methodHeaderIndex); - Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_GET_SUBSTITUTION.toMethodInsn(getDelegate(), false); - delegate.visitVarInsn(ASTORE, methodSubstitutionIndex); - 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 = submissionClassInfo.getOriginalSuperClassConstructorHeaders() - .stream() - .map(mh -> submissionClassInfo.getComputedSuperClassConstructorHeader(mh.descriptor())) - .toList(); - List constructors = submissionClassInfo.getOriginalMethodHeaders() - .stream() - .filter(mh -> mh.name().equals("")) - .map(mh -> submissionClassInfo.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, methodSubstitutionIndex); - Constants.METHOD_SUBSTITUTION_GET_CONSTRUCTOR_INVOCATION.toMethodInsn(getDelegate(), true); - delegate.visitVarInsn(ASTORE, constructorInvocationIndex); - 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) { - buildConstructorInvocationBranch(superConstructorHeader, substitutionExecuteLabel, labels, labelIndex); - } - for (MethodHeader constructorHeader : constructors) { - buildConstructorInvocationBranch(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, constructorInvocationIndex); - Constants.METHOD_SUBSTITUTION_CONSTRUCTOR_INVOCATION_OWNER.toMethodInsn(getDelegate(), false); - delegate.visitInsn(AASTORE); - delegate.visitInsn(DUP); - delegate.visitInsn(ICONST_1); - delegate.visitVarInsn(ALOAD, constructorInvocationIndex); - Constants.METHOD_SUBSTITUTION_CONSTRUCTOR_INVOCATION_DESCRIPTOR.toMethodInsn(getDelegate(), 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); - delegate.visitLocalVariable("constructorInvocation", - Constants.METHOD_SUBSTITUTION_CONSTRUCTOR_INVOCATION_TYPE.getDescriptor(), - null, - labels[labelIndex.get()], - substitutionExecuteLabel, - constructorInvocationIndex); - } - - delegate.visitVarInsn(ALOAD, methodSubstitutionIndex); - buildInvocation(Type.getArgumentTypes(computedMethodHeader.descriptor())); - Constants.METHOD_SUBSTITUTION_EXECUTE.toMethodInsn(getDelegate(), true); - Type returnType = Type.getReturnType(computedMethodHeader.descriptor()); - if (returnType.getSort() == Type.ARRAY || returnType.getSort() == Type.OBJECT) { - delegate.visitTypeInsn(CHECKCAST, returnType.getInternalName()); - } else { - unboxType(getDelegate(), returnType); - } - delegate.visitInsn(returnType.getOpcode(IRETURN)); - delegate.visitLabel(substitutionEndLabel); - delegate.visitLocalVariable("methodSubstitution", - Constants.METHOD_SUBSTITUTION_TYPE.getDescriptor(), - null, - substitutionStartLabel, - substitutionEndLabel, - methodSubstitutionIndex); - } - - // Method delegation - // if only default transformations are applied, skip delegation - if (solutionMethodNode.isPresent()) { - // check if call should be delegated to solution or not - fullFrameLocals.removeLast(); - delegate.visitFrame(F_FULL, fullFrameLocals.size(), fullFrameLocals.toArray(), 0, new Object[0]); - delegate.visitLabel(delegationCheckLabel); - delegate.visitVarInsn(ALOAD, submissionExecutionHandlerIndex); - delegate.visitVarInsn(ALOAD, methodHeaderIndex); - Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_USE_SUBMISSION_IMPL.toMethodInsn(getDelegate(), false); - delegate.visitJumpInsn(IFNE, submissionCodeLabel); // jump to label if useSubmissionImpl(...) == true - - // replay instructions from solution - delegate.visitFrame(F_CHOP, 2, null, 0, null); - fullFrameLocals.removeLast(); - fullFrameLocals.removeLast(); - delegate.visitLabel(delegationCodeLabel); - delegate.visitLocalVariable("submissionExecutionHandler", - Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_TYPE.getDescriptor(), - null, - submissionExecutionHandlerVarLabel, - delegationCodeLabel, - submissionExecutionHandlerIndex); - delegate.visitLocalVariable("methodHeader", - computedMethodHeader.getType().getDescriptor(), - null, - methodHeaderVarLabel, - delegationCodeLabel, - methodHeaderIndex); - solutionMethodNode.get().accept(getDelegate()); - - delegate.visitFrame(F_FULL, fullFrameLocals.size(), fullFrameLocals.toArray(), 0, new Object[0]); - delegate.visitLabel(submissionCodeLabel); - } else { - fullFrameLocals.removeLast(); - fullFrameLocals.removeLast(); - fullFrameLocals.removeLast(); - delegate.visitFrame(F_FULL, fullFrameLocals.size(), fullFrameLocals.toArray(), 0, new Object[0]); - delegate.visitLabel(submissionCodeLabel); - delegate.visitLocalVariable("submissionExecutionHandler", - Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_TYPE.getDescriptor(), - null, - submissionExecutionHandlerVarLabel, - submissionCodeLabel, - submissionExecutionHandlerIndex); - delegate.visitLocalVariable("methodHeader", - computedMethodHeader.getType().getDescriptor(), - null, - methodHeaderVarLabel, - submissionCodeLabel, - methodHeaderIndex); - } - - if (headerMismatch) { - new IncompatibleHeaderException("Method has incorrect return or parameter types", computedMethodHeader, originalMethodHeader) - .replicateInBytecode(getDelegate(), true); - } else { - // visit original code - delegate.visitCode(); - } - } - - @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 { - if (owner.startsWith("[")) { // stupid edge cases - delegate.visitFieldInsn(opcode, - transformationContext.toComputedType(owner).getInternalName(), - name, - transformationContext.toComputedType(descriptor).getDescriptor()); - } else { - FieldHeader computedFieldHeader = transformationContext.getSubmissionClassInfo(owner).getComputedFieldHeader(name); - if (TransformationUtils.opcodeIsCompatible(opcode, computedFieldHeader.access())) { - delegate.visitFieldInsn(opcode, - transformationContext.toComputedType(computedFieldHeader.owner()).getInternalName(), - computedFieldHeader.name(), - computedFieldHeader.descriptor()); - } else { // if incompatible - delegate.visitFieldInsn(opcode, computedFieldHeader.owner(), name + "$submission", computedFieldHeader.descriptor()); - } - } - } - } - - @Override - public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) { - if (headerMismatch) return; - - // skip transformation if owner is not part of the submission - MethodHeader methodHeader = new MethodHeader(owner, 0, name, descriptor, null, null); - if (transformationContext.methodHasReplacement(methodHeader)) { - transformationContext.getMethodReplacement(methodHeader).toMethodInsn(getDelegate(), false); - } else if (!transformationContext.isSubmissionClass(owner)) { - delegate.visitMethodInsn(opcode, owner, name, descriptor, isInterface); - } else if (owner.startsWith("[")) { // stupid edge cases - delegate.visitMethodInsn(opcode, - transformationContext.toComputedType(owner).getInternalName(), - name, - transformationContext.toComputedType(descriptor).getDescriptor(), - isInterface); - } else { - String computedOwner = transformationContext.toComputedType(owner).getInternalName(); - methodHeader = transformationContext.getSubmissionClassInfo(owner).getComputedMethodHeader(name, descriptor); - if (TransformationUtils.opcodeIsCompatible(opcode, methodHeader.access())) { - delegate.visitMethodInsn(opcode, computedOwner, methodHeader.name(), methodHeader.descriptor(), isInterface); - } else { - delegate.visitMethodInsn(opcode, - computedOwner, - name + "$submission", - transformationContext.toComputedType(descriptor).getDescriptor(), - isInterface); - } - } - } - - /** - * Builds an {@link Invocation} in bytecode. - * - * @param argumentTypes an array of parameter types - */ - private void buildInvocation(Type[] argumentTypes) { - 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(getDelegate()); - 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(getDelegate(), false); - } else { - Constants.INVOCATION_CONSTRUCTOR.toMethodInsn(getDelegate(), false); - } - // 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), getLocalsIndex(argumentTypes, i) + (isStatic ? 0 : 1)); - boxType(getDelegate(), argumentTypes[i]); - Constants.INVOCATION_CONSTRUCTOR_ADD_PARAMETER.toMethodInsn(getDelegate(), false); - } - } - - private void buildConstructorInvocationBranch(MethodHeader constructorHeader, - Label substitutionExecuteLabel, - Label[] labels, - AtomicInteger labelIndex) { - delegate.visitVarInsn(ALOAD, constructorInvocationIndex); - Constants.METHOD_SUBSTITUTION_CONSTRUCTOR_INVOCATION_OWNER.toMethodInsn(getDelegate(), 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, constructorInvocationIndex); - Constants.METHOD_SUBSTITUTION_CONSTRUCTOR_INVOCATION_DESCRIPTOR.toMethodInsn(getDelegate(), 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, constructorInvocationIndex); - Constants.METHOD_SUBSTITUTION_CONSTRUCTOR_INVOCATION_ARGS.toMethodInsn(getDelegate(), false); - delegate.visitVarInsn(ASTORE, constructorInvocationIndex + 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, constructorInvocationIndex + 1); - delegate.visitIntInsn(SIPUSH, i); - delegate.visitInsn(AALOAD); - unboxType(getDelegate(), parameterTypes[i]); - } - constructorHeader.toMethodInsn(getDelegate(), 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()], - constructorInvocationIndex + 1); - } -} 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 index 7ba961af..70b8cddc 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/methods/BaseMethodVisitor.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/methods/BaseMethodVisitor.java @@ -1,15 +1,20 @@ package org.tudalgo.algoutils.transform.methods; import org.objectweb.asm.*; +import org.objectweb.asm.tree.MethodNode; import org.tudalgo.algoutils.transform.SubmissionClassInfo; -import org.tudalgo.algoutils.transform.util.MethodHeader; -import org.tudalgo.algoutils.transform.util.TransformationContext; +import org.tudalgo.algoutils.transform.util.*; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; +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 { @@ -38,6 +43,9 @@ protected BaseMethodVisitor(MethodVisitor delegate, this.originalMethodHeader = originalMethodHeader; this.computedMethodHeader = computedMethodHeader; + this.headerMismatch = !transformationContext.toComputedType(originalMethodHeader.descriptor()) + .getDescriptor() + .equals(computedMethodHeader.descriptor()); this.isStatic = (computedMethodHeader.access() & ACC_STATIC) != 0; this.isConstructor = computedMethodHeader.name().equals(""); @@ -56,20 +64,382 @@ protected BaseMethodVisitor(MethodVisitor delegate, if (!isStatic) { this.fullFrameLocals.addFirst(isConstructor ? UNINITIALIZED_THIS : computedMethodHeader.owner()); } + } + + public enum LocalsObject { + SUBMISSION_EXECUTION_HANDLER("submissionExecutionHandler", Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_TYPE.getDescriptor()), + 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 mv, Label start, Label end) { + mv.visitLocalVariable(varName, descriptor, null, start, end, mv.getLocalsIndex(this)); + } + } + + protected abstract int getLocalsIndex(LocalsObject localsObject); + + protected void injectSetupCode(Label submissionExecutionHandlerVarLabel, Label methodHeaderVarLabel) { + // create SubmissionExecutionHandler$Internal instance and store in locals array + delegate.visitTypeInsn(NEW, Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_TYPE.getInternalName()); + delegate.visitInsn(DUP); + Constants.SUBMISSION_EXECUTION_HANDLER_GET_INSTANCE.toMethodInsn(delegate, false); + Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_CONSTRUCTOR.toMethodInsn(delegate, false); + delegate.visitVarInsn(ASTORE, getLocalsIndex(LocalsObject.SUBMISSION_EXECUTION_HANDLER)); + delegate.visitLabel(submissionExecutionHandlerVarLabel); + + // 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, + 2, + new Object[] {Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_TYPE.getInternalName(), computedMethodHeader.getType().getInternalName()}, + 0, + null); + fullFrameLocals.add(Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_TYPE.getInternalName()); + fullFrameLocals.add(computedMethodHeader.getType().getInternalName()); + } + + protected void injectInvocationLoggingCode(Label nextLabel) { + // check if invocation should be logged + delegate.visitVarInsn(ALOAD, getLocalsIndex(LocalsObject.SUBMISSION_EXECUTION_HANDLER)); + 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.SUBMISSION_EXECUTION_HANDLER)); + delegate.visitVarInsn(ALOAD, getLocalsIndex(LocalsObject.METHOD_HEADER)); + injectInvocation(Type.getArgumentTypes(computedMethodHeader.descriptor())); + 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.SUBMISSION_EXECUTION_HANDLER)); + 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.SUBMISSION_EXECUTION_HANDLER)); + 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 = submissionClassInfo.getOriginalSuperClassConstructorHeaders() + .stream() + .map(mh -> submissionClassInfo.getComputedSuperClassConstructorHeader(mh.descriptor())) + .toList(); + List constructors = submissionClassInfo.getOriginalMethodHeaders() + .stream() + .filter(mh -> mh.name().equals("")) + .map(mh -> submissionClassInfo.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())); + 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 submissionExecutionHandlerVarLabel, + 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.SUBMISSION_EXECUTION_HANDLER)); + 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, 2, null, 0, null); + fullFrameLocals.removeLast(); + fullFrameLocals.removeLast(); + delegate.visitLabel(delegationCodeLabel); + LocalsObject.SUBMISSION_EXECUTION_HANDLER.visitLocalVariable(this, submissionExecutionHandlerVarLabel, 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 submissionExecutionHandlerVarLabel, + Label methodHeaderVarLabel) { + fullFrameLocals.removeLast(); + fullFrameLocals.removeLast(); + delegate.visitFrame(F_FULL, fullFrameLocals.size(), fullFrameLocals.toArray(), 0, new Object[0]); + delegate.visitLabel(submissionCodeLabel); + LocalsObject.SUBMISSION_EXECUTION_HANDLER.visitLocalVariable(this, submissionExecutionHandlerVarLabel, submissionCodeLabel); + LocalsObject.METHOD_HEADER.visitLocalVariable(this, methodHeaderVarLabel, submissionCodeLabel); + } - int[] originalParameterTypes = Arrays.stream(Type.getArgumentTypes(originalMethodHeader.descriptor())) - .mapToInt(Type::getSort) - .toArray(); - int originalReturnType = Type.getReturnType(originalMethodHeader.descriptor()).getSort(); - int[] computedParameterTypes = Arrays.stream(Type.getArgumentTypes(computedMethodHeader.descriptor())) - .mapToInt(Type::getSort) - .toArray(); - int computedReturnType = Type.getReturnType(computedMethodHeader.descriptor()).getSort(); - this.headerMismatch = !(Arrays.equals(originalParameterTypes, computedParameterTypes) && originalReturnType == computedReturnType); + /** + * Builds an {@link Invocation} in bytecode. + * + * @param argumentTypes an array of parameter types + */ + protected void injectInvocation(Type[] argumentTypes) { + 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); + } + // 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); + } + } + + 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); } // Prevent bytecode to be added to the method if there is a header mismatch + @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 { + if (owner.startsWith("[")) { // stupid edge cases + delegate.visitFieldInsn(opcode, + transformationContext.toComputedType(owner).getInternalName(), + name, + transformationContext.toComputedType(descriptor).getDescriptor()); + } else { + FieldHeader computedFieldHeader = transformationContext.getSubmissionClassInfo(owner).getComputedFieldHeader(name); + if (TransformationUtils.opcodeIsCompatible(opcode, computedFieldHeader.access())) { + delegate.visitFieldInsn(opcode, + transformationContext.toComputedType(computedFieldHeader.owner()).getInternalName(), + computedFieldHeader.name(), + computedFieldHeader.descriptor()); + } else { // if incompatible + delegate.visitFieldInsn(opcode, computedFieldHeader.owner(), name + "$submission", computedFieldHeader.descriptor()); + } + } + } + } + + @Override + public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) { + if (headerMismatch) return; + + // skip transformation if owner is not part of the submission + MethodHeader methodHeader = new MethodHeader(owner, 0, name, descriptor, null, null); + 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("[")) { // stupid edge cases + delegate.visitMethodInsn(opcode, + transformationContext.toComputedType(owner).getInternalName(), + name, + transformationContext.toComputedType(descriptor).getDescriptor(), + isInterface); + } else { + String computedOwner = transformationContext.toComputedType(owner).getInternalName(); + methodHeader = transformationContext.getSubmissionClassInfo(owner).getComputedMethodHeader(name, descriptor); + if (TransformationUtils.opcodeIsCompatible(opcode, methodHeader.access())) { + delegate.visitMethodInsn(opcode, computedOwner, methodHeader.name(), methodHeader.descriptor(), isInterface); + } else { + delegate.visitMethodInsn(opcode, + computedOwner, + name + "$submission", + transformationContext.toComputedType(descriptor).getDescriptor(), + isInterface); + } + } + } + @Override public void visitLdcInsn(Object value) { if (headerMismatch) return; @@ -186,7 +556,8 @@ public AnnotationVisitor visitTryCatchAnnotation(int typeRef, TypePath typePath, @Override public void visitLocalVariable(String name, String descriptor, String signature, Label start, Label end, int index) { - if (headerMismatch) return; + 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); } diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/methods/InjectingMethodVisitor.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/methods/InjectingMethodVisitor.java index bc73c354..19afaacf 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/methods/InjectingMethodVisitor.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/methods/InjectingMethodVisitor.java @@ -2,26 +2,18 @@ import org.objectweb.asm.Label; import org.objectweb.asm.MethodVisitor; -import org.objectweb.asm.Type; import org.tudalgo.algoutils.transform.SubmissionClassInfo; import org.tudalgo.algoutils.transform.util.*; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.stream.Stream; +import java.util.EnumMap; +import java.util.Map; import static org.objectweb.asm.Opcodes.*; import static org.objectweb.asm.Opcodes.F_CHOP; -import static org.tudalgo.algoutils.transform.util.TransformationUtils.*; -import static org.tudalgo.algoutils.transform.util.TransformationUtils.unboxType; public class InjectingMethodVisitor extends BaseMethodVisitor { - private final int submissionExecutionHandlerIndex; - private final int methodHeaderIndex; - private final int methodSubstitutionIndex; - private final int constructorInvocationIndex; + private final Map localsIndexes = new EnumMap<>(LocalsObject.class); public InjectingMethodVisitor(MethodVisitor delegate, TransformationContext transformationContext, @@ -29,10 +21,15 @@ public InjectingMethodVisitor(MethodVisitor delegate, MethodHeader methodHeader) { super(delegate, transformationContext, submissionClassInfo, methodHeader, methodHeader); - this.submissionExecutionHandlerIndex = nextLocalsIndex; - this.methodHeaderIndex = nextLocalsIndex + 1; - this.methodSubstitutionIndex = nextLocalsIndex + 2; - this.constructorInvocationIndex = nextLocalsIndex + 3; + localsIndexes.put(LocalsObject.SUBMISSION_EXECUTION_HANDLER, nextLocalsIndex); + localsIndexes.put(LocalsObject.METHOD_HEADER, nextLocalsIndex + 1); + localsIndexes.put(LocalsObject.METHOD_SUBSTITUTION, nextLocalsIndex + 2); + localsIndexes.put(LocalsObject.CONSTRUCTOR_INVOCATION, nextLocalsIndex + 3); + } + + @Override + protected int getLocalsIndex(LocalsObject localsObject) { + return localsIndexes.get(localsObject); } @Override @@ -40,187 +37,27 @@ public void visitCode() { Label submissionExecutionHandlerVarLabel = new Label(); Label methodHeaderVarLabel = new Label(); Label substitutionCheckLabel = new Label(); - Label substitutionStartLabel = new Label(); - Label substitutionEndLabel = new Label(); Label delegationCheckLabel = new Label(); Label delegationCodeLabel = new Label(); Label submissionCodeLabel = new Label(); // Setup - { - // create SubmissionExecutionHandler$Internal instance and store in locals array - delegate.visitTypeInsn(NEW, Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_TYPE.getInternalName()); - delegate.visitInsn(DUP); - Constants.SUBMISSION_EXECUTION_HANDLER_GET_INSTANCE.toMethodInsn(getDelegate(), false); - Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_CONSTRUCTOR.toMethodInsn(getDelegate(), false); - delegate.visitVarInsn(ASTORE, submissionExecutionHandlerIndex); - delegate.visitLabel(submissionExecutionHandlerVarLabel); - - // replicate method header in bytecode and store in locals array - computedMethodHeader.buildHeader(getDelegate()); - delegate.visitVarInsn(ASTORE, methodHeaderIndex); - delegate.visitLabel(methodHeaderVarLabel); - - delegate.visitFrame(F_APPEND, - 2, - new Object[] {Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_TYPE.getInternalName(), computedMethodHeader.getType().getInternalName()}, - 0, - null); - fullFrameLocals.add(Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_TYPE.getInternalName()); - fullFrameLocals.add(computedMethodHeader.getType().getInternalName()); - } + injectSetupCode(submissionExecutionHandlerVarLabel, methodHeaderVarLabel); // Invocation logging - { - // check if invocation should be logged - delegate.visitVarInsn(ALOAD, submissionExecutionHandlerIndex); - delegate.visitVarInsn(ALOAD, methodHeaderIndex); - Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_LOG_INVOCATION.toMethodInsn(getDelegate(), false); - delegate.visitJumpInsn(IFEQ, substitutionCheckLabel); // jump to label if logInvocation(...) == false - - // intercept parameters - delegate.visitVarInsn(ALOAD, submissionExecutionHandlerIndex); - delegate.visitVarInsn(ALOAD, methodHeaderIndex); - buildInvocation(Type.getArgumentTypes(computedMethodHeader.descriptor())); - Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_ADD_INVOCATION.toMethodInsn(getDelegate(), false); - } + injectInvocationLoggingCode(substitutionCheckLabel); // Method substitution - { - // check if substitution exists for this method - delegate.visitFrame(F_SAME, 0, null, 0, null); - delegate.visitLabel(substitutionCheckLabel); - delegate.visitVarInsn(ALOAD, submissionExecutionHandlerIndex); - delegate.visitVarInsn(ALOAD, methodHeaderIndex); - Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_USE_SUBSTITUTION.toMethodInsn(getDelegate(), false); - delegate.visitJumpInsn(IFEQ, delegationCheckLabel); // jump to label if useSubstitution(...) == false - - // get substitution and execute it - delegate.visitVarInsn(ALOAD, submissionExecutionHandlerIndex); - delegate.visitVarInsn(ALOAD, methodHeaderIndex); - Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_GET_SUBSTITUTION.toMethodInsn(getDelegate(), false); - delegate.visitVarInsn(ASTORE, methodSubstitutionIndex); - 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 = submissionClassInfo.getOriginalSuperClassConstructorHeaders() - .stream() - .map(mh -> submissionClassInfo.getComputedSuperClassConstructorHeader(mh.descriptor())) - .toList(); - List constructors = submissionClassInfo.getOriginalMethodHeaders() - .stream() - .filter(mh -> mh.name().equals("")) - .map(mh -> submissionClassInfo.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, methodSubstitutionIndex); - Constants.METHOD_SUBSTITUTION_GET_CONSTRUCTOR_INVOCATION.toMethodInsn(getDelegate(), true); - delegate.visitVarInsn(ASTORE, constructorInvocationIndex); - 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) { - buildConstructorInvocationBranch(superConstructorHeader, substitutionExecuteLabel, labels, labelIndex); - } - for (MethodHeader constructorHeader : constructors) { - buildConstructorInvocationBranch(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, constructorInvocationIndex); - Constants.METHOD_SUBSTITUTION_CONSTRUCTOR_INVOCATION_OWNER.toMethodInsn(getDelegate(), false); - delegate.visitInsn(AASTORE); - delegate.visitInsn(DUP); - delegate.visitInsn(ICONST_1); - delegate.visitVarInsn(ALOAD, constructorInvocationIndex); - Constants.METHOD_SUBSTITUTION_CONSTRUCTOR_INVOCATION_DESCRIPTOR.toMethodInsn(getDelegate(), 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); - delegate.visitLocalVariable("constructorInvocation", - Constants.METHOD_SUBSTITUTION_CONSTRUCTOR_INVOCATION_TYPE.getDescriptor(), - null, - labels[labelIndex.get()], - substitutionExecuteLabel, - constructorInvocationIndex); - } - - delegate.visitVarInsn(ALOAD, methodSubstitutionIndex); - buildInvocation(Type.getArgumentTypes(computedMethodHeader.descriptor())); - Constants.METHOD_SUBSTITUTION_EXECUTE.toMethodInsn(getDelegate(), true); - Type returnType = Type.getReturnType(computedMethodHeader.descriptor()); - if (returnType.getSort() == Type.ARRAY || returnType.getSort() == Type.OBJECT) { - delegate.visitTypeInsn(CHECKCAST, returnType.getInternalName()); - } else { - unboxType(getDelegate(), returnType); - } - delegate.visitInsn(returnType.getOpcode(IRETURN)); - delegate.visitLabel(substitutionEndLabel); - delegate.visitLocalVariable("methodSubstitution", - Constants.METHOD_SUBSTITUTION_TYPE.getDescriptor(), - null, - substitutionStartLabel, - substitutionEndLabel, - methodSubstitutionIndex); - } + injectSubstitutionCode(substitutionCheckLabel, delegationCheckLabel); // Method delegation // if only default transformations are applied, skip delegation { // check if call should be delegated to solution or not - fullFrameLocals.removeLast(); delegate.visitFrame(F_FULL, fullFrameLocals.size(), fullFrameLocals.toArray(), 0, new Object[0]); delegate.visitLabel(delegationCheckLabel); - delegate.visitVarInsn(ALOAD, submissionExecutionHandlerIndex); - delegate.visitVarInsn(ALOAD, methodHeaderIndex); + delegate.visitVarInsn(ALOAD, getLocalsIndex(LocalsObject.SUBMISSION_EXECUTION_HANDLER)); + 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 @@ -229,18 +66,8 @@ public void visitCode() { fullFrameLocals.removeLast(); fullFrameLocals.removeLast(); delegate.visitLabel(delegationCodeLabel); - delegate.visitLocalVariable("submissionExecutionHandler", - Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_TYPE.getDescriptor(), - null, - submissionExecutionHandlerVarLabel, - delegationCodeLabel, - submissionExecutionHandlerIndex); - delegate.visitLocalVariable("methodHeader", - computedMethodHeader.getType().getDescriptor(), - null, - methodHeaderVarLabel, - delegationCodeLabel, - methodHeaderIndex); + LocalsObject.SUBMISSION_EXECUTION_HANDLER.visitLocalVariable(this, submissionExecutionHandlerVarLabel, delegationCodeLabel); + LocalsObject.METHOD_HEADER.visitLocalVariable(this, methodHeaderVarLabel, delegationCodeLabel); new IncompatibleHeaderException("Method has incorrect return or parameter types", computedMethodHeader, null) .replicateInBytecode(getDelegate(), true); @@ -251,33 +78,6 @@ public void visitCode() { delegate.visitCode(); } - @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 { - if (owner.startsWith("[")) { // stupid edge cases - delegate.visitFieldInsn(opcode, - transformationContext.toComputedType(owner).getInternalName(), - name, - transformationContext.toComputedType(descriptor).getDescriptor()); - } else { - FieldHeader computedFieldHeader = transformationContext.getSubmissionClassInfo(owner).getComputedFieldHeader(name); - if (TransformationUtils.opcodeIsCompatible(opcode, computedFieldHeader.access())) { - delegate.visitFieldInsn(opcode, - transformationContext.toComputedType(computedFieldHeader.owner()).getInternalName(), - computedFieldHeader.name(), - computedFieldHeader.descriptor()); - } else { // if incompatible - delegate.visitFieldInsn(opcode, computedFieldHeader.owner(), name + "$submission", computedFieldHeader.descriptor()); - } - } - } - } - @Override public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) { if (headerMismatch) return; @@ -290,97 +90,4 @@ public void visitMethodInsn(int opcode, String owner, String name, String descri delegate.visitMethodInsn(opcode, owner, name, descriptor, isInterface); } } - - /** - * Builds an {@link Invocation} in bytecode. - * - * @param argumentTypes an array of parameter types - */ - private void buildInvocation(Type[] argumentTypes) { - 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(getDelegate()); - 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(getDelegate(), false); - } else { - Constants.INVOCATION_CONSTRUCTOR.toMethodInsn(getDelegate(), false); - } - // 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), getLocalsIndex(argumentTypes, i) + (isStatic ? 0 : 1)); - boxType(getDelegate(), argumentTypes[i]); - Constants.INVOCATION_CONSTRUCTOR_ADD_PARAMETER.toMethodInsn(getDelegate(), false); - } - } - - private void buildConstructorInvocationBranch(MethodHeader constructorHeader, - Label substitutionExecuteLabel, - Label[] labels, - AtomicInteger labelIndex) { - delegate.visitVarInsn(ALOAD, constructorInvocationIndex); - Constants.METHOD_SUBSTITUTION_CONSTRUCTOR_INVOCATION_OWNER.toMethodInsn(getDelegate(), 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, constructorInvocationIndex); - Constants.METHOD_SUBSTITUTION_CONSTRUCTOR_INVOCATION_DESCRIPTOR.toMethodInsn(getDelegate(), 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, constructorInvocationIndex); - Constants.METHOD_SUBSTITUTION_CONSTRUCTOR_INVOCATION_ARGS.toMethodInsn(getDelegate(), false); - delegate.visitVarInsn(ASTORE, constructorInvocationIndex + 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, constructorInvocationIndex + 1); - delegate.visitIntInsn(SIPUSH, i); - delegate.visitInsn(AALOAD); - unboxType(getDelegate(), parameterTypes[i]); - } - constructorHeader.toMethodInsn(getDelegate(), 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()], - constructorInvocationIndex + 1); - } } 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..90c10613 --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/methods/SubmissionMethodVisitor.java @@ -0,0 +1,86 @@ +package org.tudalgo.algoutils.transform.methods; + +import org.objectweb.asm.*; +import org.objectweb.asm.tree.MethodNode; +import org.tudalgo.algoutils.transform.SubmissionClassInfo; +import org.tudalgo.algoutils.transform.SubmissionClassVisitor; +import org.tudalgo.algoutils.transform.util.*; + +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.SUBMISSION_EXECUTION_HANDLER, nextLocalsIndex); + localsIndexes.put(LocalsObject.METHOD_HEADER, nextLocalsIndex + 1); + localsIndexes.put(LocalsObject.METHOD_SUBSTITUTION, nextLocalsIndex + 2); + localsIndexes.put(LocalsObject.CONSTRUCTOR_INVOCATION, nextLocalsIndex + 3); + } + + @Override + protected int getLocalsIndex(LocalsObject localsObject) { + return localsIndexes.get(localsObject); + } + + @Override + public void visitCode() { + Optional solutionMethodNode = submissionClassInfo.getSolutionClass() + .map(solutionClassNode -> solutionClassNode.getMethods().get(computedMethodHeader)); + Label submissionExecutionHandlerVarLabel = new Label(); + Label methodHeaderVarLabel = new Label(); + Label substitutionCheckLabel = new Label(); + Label delegationCheckLabel = new Label(); + Label submissionCodeLabel = new Label(); + + // Setup + injectSetupCode(submissionExecutionHandlerVarLabel, 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, + submissionExecutionHandlerVarLabel, + methodHeaderVarLabel); + } else { + injectNoDelegationCode(submissionCodeLabel, submissionExecutionHandlerVarLabel, methodHeaderVarLabel); + } + + if (headerMismatch) { + new IncompatibleHeaderException("Method has incorrect return or parameter types", computedMethodHeader, originalMethodHeader) + .replicateInBytecode(getDelegate(), true); + } else { + // visit original code + delegate.visitCode(); + } + } +} From c61bf017539db199ee18f6853639b8bc2286de47 Mon Sep 17 00:00:00 2001 From: Daniel Mangold Date: Tue, 3 Dec 2024 15:40:48 +0100 Subject: [PATCH 28/48] Add mechanism for injecting missing classes --- .../SolutionMergingClassTransformer.java | 19 +++++++++ .../transform/SubmissionClassVisitor.java | 1 + .../classes/MissingClassVisitor.java | 39 +++++++++++++++++++ .../transform/util/TransformationContext.java | 13 +++++++ 4 files changed, 72 insertions(+) create mode 100644 tutor/src/main/java/org/tudalgo/algoutils/transform/classes/MissingClassVisitor.java diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionMergingClassTransformer.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionMergingClassTransformer.java index 5ae2a0d1..3907f1e0 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionMergingClassTransformer.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionMergingClassTransformer.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.sourcegrade.jagr.api.testing.extension.JagrExecutionCondition; import org.tudalgo.algoutils.student.annotation.ForceSignature; +import org.tudalgo.algoutils.transform.classes.MissingClassVisitor; import org.tudalgo.algoutils.transform.util.MethodHeader; import org.tudalgo.algoutils.transform.util.TransformationContext; import org.objectweb.asm.ClassReader; @@ -146,6 +147,24 @@ public void transform(ClassReader reader, ClassWriter writer) { 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(0); + entry.getValue().accept(new MissingClassVisitor(writer)); + missingClasses.put(entry.getKey(), writer.toByteArray()); + }); + + return missingClasses; + } + /** * (Internal) Configuration keys */ diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java index 91f5b23d..e2dc531d 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java @@ -225,6 +225,7 @@ public void visitEnd() { injectFieldMetadata(); injectMethodMetadata(); + transformationContext.addVisitedClass(computedClassHeader.name()); super.visitEnd(); } 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..b328c858 --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/MissingClassVisitor.java @@ -0,0 +1,39 @@ +package org.tudalgo.algoutils.transform.classes; + +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.MethodVisitor; +import org.tudalgo.algoutils.transform.util.Constants; +import org.tudalgo.algoutils.transform.util.MethodHeader; + +import static org.objectweb.asm.Opcodes.ACONST_NULL; +import static org.objectweb.asm.Opcodes.ARETURN; +import static org.objectweb.asm.Opcodes.ASM9; + +public class MissingClassVisitor extends ClassVisitor { + + public MissingClassVisitor(ClassVisitor delegate) { + super(ASM9, delegate); + } + + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + // TODO: transform to use with SubmissionExecutionHandler + return super.visitMethod(access, name, descriptor, signature, exceptions); + } + + @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); + + super.visitEnd(); + } + + private void injectMetadataMethod(MethodHeader methodHeader) { + MethodVisitor mv = methodHeader.toMethodVisitor(getDelegate()); + mv.visitInsn(ACONST_NULL); + mv.visitInsn(ARETURN); + mv.visitMaxs(1, 0); + } +} 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 index d1185ded..e3266d22 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationContext.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationContext.java @@ -21,6 +21,7 @@ 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; @@ -103,6 +104,14 @@ 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. */ @@ -194,6 +203,10 @@ 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. * The given value may be an internal class name or a descriptor. From 860b077fd6dabf5485e140e55986842d029d1bf8 Mon Sep 17 00:00:00 2001 From: Daniel Mangold Date: Tue, 3 Dec 2024 15:52:15 +0100 Subject: [PATCH 29/48] Remove locals from injected methods since they're static --- .../transform/SubmissionClassVisitor.java | 36 ++----------------- 1 file changed, 3 insertions(+), 33 deletions(-) diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java index e2dc531d..3ba617ab 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java @@ -234,21 +234,11 @@ public void visitEnd() { * This injected method returns the original class header of the class pre-transformation. */ private void injectClassMetadata() { - Label startLabel = new Label(); - Label endLabel = new Label(); MethodVisitor mv = Constants.INJECTED_GET_ORIGINAL_CLASS_HEADER.toMethodVisitor(getDelegate()); - mv.visitLabel(startLabel); int maxStack = originalClassHeader.buildHeader(mv); mv.visitInsn(ARETURN); - mv.visitLabel(endLabel); - mv.visitLocalVariable("this", - Type.getObjectType(computedClassHeader.name()).getDescriptor(), - null, - startLabel, - endLabel, - 0); - mv.visitMaxs(maxStack, 1); + mv.visitMaxs(maxStack, 0); } /** @@ -260,12 +250,9 @@ private void injectFieldMetadata() { .stream() .filter(fieldHeader -> (fieldHeader.access() & ACC_SYNTHETIC) == 0) .collect(Collectors.toSet()); - Label startLabel = new Label(); - Label endLabel = new Label(); int maxStack, stackSize; MethodVisitor mv = Constants.INJECTED_GET_ORIGINAL_FIELD_HEADERS.toMethodVisitor(getDelegate()); - mv.visitLabel(startLabel); mv.visitIntInsn(SIPUSH, fieldHeaders.size()); mv.visitTypeInsn(ANEWARRAY, Type.getInternalName(Object.class)); maxStack = stackSize = 1; @@ -286,14 +273,7 @@ private void injectFieldMetadata() { Type.getMethodDescriptor(Constants.SET_TYPE, Type.getType(Object[].class)), true); mv.visitInsn(ARETURN); - mv.visitLabel(endLabel); - mv.visitLocalVariable("this", - Type.getObjectType(computedClassHeader.name()).getDescriptor(), - null, - startLabel, - endLabel, - 0); - mv.visitMaxs(maxStack, 1); + mv.visitMaxs(maxStack, 0); } /** @@ -305,12 +285,9 @@ private void injectMethodMetadata() { .stream() .filter(methodHeader -> (methodHeader.access() & ACC_SYNTHETIC) == 0) .collect(Collectors.toSet()); - Label startLabel = new Label(); - Label endLabel = new Label(); int maxStack, stackSize; MethodVisitor mv = Constants.INJECTED_GET_ORIGINAL_METHODS_HEADERS.toMethodVisitor(getDelegate()); - mv.visitLabel(startLabel); mv.visitIntInsn(SIPUSH, methodHeaders.size()); mv.visitTypeInsn(ANEWARRAY, Type.getInternalName(Object.class)); maxStack = stackSize = 1; @@ -331,13 +308,6 @@ private void injectMethodMetadata() { Type.getMethodDescriptor(Constants.SET_TYPE, Type.getType(Object[].class)), true); mv.visitInsn(ARETURN); - mv.visitLabel(endLabel); - mv.visitLocalVariable("this", - Type.getObjectType(computedClassHeader.name()).getDescriptor(), - null, - startLabel, - endLabel, - 0); - mv.visitMaxs(maxStack, 1); + mv.visitMaxs(maxStack, 0); } } From 2d7430060daeb92b3e092cf1bab8676b78629440 Mon Sep 17 00:00:00 2001 From: Daniel Mangold Date: Tue, 3 Dec 2024 18:15:41 +0100 Subject: [PATCH 30/48] Refactoring and enable injecting missing classes into Jagr --- .../SolutionMergingClassTransformer.java | 7 +- .../transform/SubmissionExecutionHandler.java | 1 + .../transform/classes/ClassInfo.java | 91 +++++++++++++++ .../transform/classes/MissingClassInfo.java | 105 ++++++++++++++++++ .../classes/MissingClassVisitor.java | 19 +++- .../{ => classes}/SolutionClassNode.java | 2 +- .../{ => classes}/SubmissionClassInfo.java | 101 +++++------------ .../{ => classes}/SubmissionClassVisitor.java | 10 +- .../transform/methods/BaseMethodVisitor.java | 16 +-- ...Visitor.java => MissingMethodVisitor.java} | 19 ++-- .../methods/SubmissionMethodVisitor.java | 6 +- .../transform/util/TransformationContext.java | 4 +- 12 files changed, 278 insertions(+), 103 deletions(-) create mode 100644 tutor/src/main/java/org/tudalgo/algoutils/transform/classes/ClassInfo.java create mode 100644 tutor/src/main/java/org/tudalgo/algoutils/transform/classes/MissingClassInfo.java rename tutor/src/main/java/org/tudalgo/algoutils/transform/{ => classes}/SolutionClassNode.java (99%) rename tutor/src/main/java/org/tudalgo/algoutils/transform/{ => classes}/SubmissionClassInfo.java (73%) rename tutor/src/main/java/org/tudalgo/algoutils/transform/{ => classes}/SubmissionClassVisitor.java (96%) rename tutor/src/main/java/org/tudalgo/algoutils/transform/methods/{InjectingMethodVisitor.java => MissingMethodVisitor.java} (85%) diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionMergingClassTransformer.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionMergingClassTransformer.java index 3907f1e0..211e7e15 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionMergingClassTransformer.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionMergingClassTransformer.java @@ -4,6 +4,9 @@ import org.sourcegrade.jagr.api.testing.extension.JagrExecutionCondition; import org.tudalgo.algoutils.student.annotation.ForceSignature; import org.tudalgo.algoutils.transform.classes.MissingClassVisitor; +import org.tudalgo.algoutils.transform.classes.SolutionClassNode; +import org.tudalgo.algoutils.transform.classes.SubmissionClassInfo; +import org.tudalgo.algoutils.transform.classes.SubmissionClassVisitor; import org.tudalgo.algoutils.transform.util.MethodHeader; import org.tudalgo.algoutils.transform.util.TransformationContext; import org.objectweb.asm.ClassReader; @@ -158,8 +161,8 @@ public Map injectClasses() { .filter(entry -> !visitedClasses.contains(entry.getKey())) .forEach(entry -> { ClassWriter writer = new ClassWriter(0); - entry.getValue().accept(new MissingClassVisitor(writer)); - missingClasses.put(entry.getKey(), writer.toByteArray()); + entry.getValue().accept(new MissingClassVisitor(writer, transformationContext, entry.getValue())); + missingClasses.put(entry.getKey().replace('/', '.'), writer.toByteArray()); }); return missingClasses; diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionExecutionHandler.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionExecutionHandler.java index b03b1149..413c3a3f 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionExecutionHandler.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionExecutionHandler.java @@ -1,5 +1,6 @@ package org.tudalgo.algoutils.transform; +import org.tudalgo.algoutils.transform.classes.SubmissionClassVisitor; import org.tudalgo.algoutils.transform.util.*; import java.lang.invoke.MethodHandles; 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..559621c1 --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/ClassInfo.java @@ -0,0 +1,91 @@ +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.ClassHeader; +import org.tudalgo.algoutils.transform.util.FieldHeader; +import org.tudalgo.algoutils.transform.util.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; + +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<>(); + + public ClassInfo(TransformationContext transformationContext) { + super(Opcodes.ASM9); + + this.transformationContext = transformationContext; + } + + public abstract ClassHeader getOriginalClassHeader(); + + public abstract ClassHeader getComputedClassHeader(); + + public abstract Set getOriginalFieldHeaders(); + + public abstract FieldHeader getComputedFieldHeader(String name); + + public abstract Set getOriginalMethodHeaders(); + + public abstract MethodHeader getComputedMethodHeader(String name, String descriptor); + + public abstract Set getOriginalSuperClassConstructorHeaders(); + + public abstract MethodHeader getComputedSuperClassConstructorHeader(String descriptor); + + protected abstract void resolveSuperTypeMembers(Set superTypeMembers, String typeName); + + /** + * Recursively resolves the members of superclasses and interfaces. + * + * @param superTypeMembers a set for recording class 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); + } + } + } + + 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..19c95dc3 --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/MissingClassInfo.java @@ -0,0 +1,105 @@ +package org.tudalgo.algoutils.transform.classes; + +import org.tudalgo.algoutils.transform.util.ClassHeader; +import org.tudalgo.algoutils.transform.util.FieldHeader; +import org.tudalgo.algoutils.transform.util.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; + +public class MissingClassInfo extends ClassInfo { + + private final SolutionClassNode solutionClassNode; + + 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 index b328c858..956fbf31 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/MissingClassVisitor.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/MissingClassVisitor.java @@ -2,8 +2,10 @@ import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.MethodVisitor; +import org.tudalgo.algoutils.transform.methods.MissingMethodVisitor; import org.tudalgo.algoutils.transform.util.Constants; import org.tudalgo.algoutils.transform.util.MethodHeader; +import org.tudalgo.algoutils.transform.util.TransformationContext; import static org.objectweb.asm.Opcodes.ACONST_NULL; import static org.objectweb.asm.Opcodes.ARETURN; @@ -11,14 +13,25 @@ public class MissingClassVisitor extends ClassVisitor { - public MissingClassVisitor(ClassVisitor delegate) { + private final TransformationContext transformationContext; + private final SolutionClassNode solutionClassNode; + + public MissingClassVisitor(ClassVisitor delegate, + TransformationContext transformationContext, + SolutionClassNode solutionClassNode) { super(ASM9, delegate); + + this.transformationContext = transformationContext; + this.solutionClassNode = solutionClassNode; } @Override public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { - // TODO: transform to use with SubmissionExecutionHandler - return super.visitMethod(access, name, descriptor, signature, 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 diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionClassNode.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/SolutionClassNode.java similarity index 99% rename from tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionClassNode.java rename to tutor/src/main/java/org/tudalgo/algoutils/transform/classes/SolutionClassNode.java index 751a5f7c..373f5195 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionClassNode.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/SolutionClassNode.java @@ -1,4 +1,4 @@ -package org.tudalgo.algoutils.transform; +package org.tudalgo.algoutils.transform.classes; import org.tudalgo.algoutils.transform.util.*; import org.objectweb.asm.FieldVisitor; diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassInfo.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/SubmissionClassInfo.java similarity index 73% rename from tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassInfo.java rename to tutor/src/main/java/org/tudalgo/algoutils/transform/classes/SubmissionClassInfo.java index 5d724f73..bfc24948 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassInfo.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/SubmissionClassInfo.java @@ -1,15 +1,12 @@ -package org.tudalgo.algoutils.transform; +package org.tudalgo.algoutils.transform.classes; import org.tudalgo.algoutils.transform.util.*; -import kotlin.Triple; import org.objectweb.asm.*; import java.lang.reflect.Modifier; import java.util.*; -import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; -import java.util.stream.Stream; /** * A class that holds information on a submission class. @@ -20,24 +17,14 @@ * * @author Daniel Mangold */ -public class SubmissionClassInfo extends ClassVisitor { +public class SubmissionClassInfo extends ClassInfo { - private final TransformationContext transformationContext; private final ForceSignatureAnnotationProcessor fsAnnotationProcessor; - private final Set, Map>> superClassMembers = new HashSet<>(); private ClassHeader originalClassHeader; private ClassHeader computedClassHeader; private SolutionClassNode solutionClass; - // Mapping of fields in submission => usable fields - private final Map fields = new HashMap<>(); - - // Mapping of methods in submission => usable methods - private final Map methods = new HashMap<>(); - - private final Map superClassConstructors = new HashMap<>(); - /** * Constructs a new {@link SubmissionClassInfo} instance. * @@ -46,8 +33,8 @@ public class SubmissionClassInfo extends ClassVisitor { */ public SubmissionClassInfo(TransformationContext transformationContext, ForceSignatureAnnotationProcessor fsAnnotationProcessor) { - super(Opcodes.ASM9); - this.transformationContext = transformationContext; + super(transformationContext); + this.fsAnnotationProcessor = fsAnnotationProcessor; } @@ -56,6 +43,7 @@ public SubmissionClassInfo(TransformationContext transformationContext, * * @return the original class header */ + @Override public ClassHeader getOriginalClassHeader() { return originalClassHeader; } @@ -67,6 +55,7 @@ public ClassHeader getOriginalClassHeader() { * * @return the computed class name */ + @Override public ClassHeader getComputedClassHeader() { return computedClassHeader; } @@ -85,6 +74,7 @@ public Optional getSolutionClass() { * * @return the original field headers */ + @Override public Set getOriginalFieldHeaders() { return fields.keySet(); } @@ -99,6 +89,7 @@ public Set getOriginalFieldHeaders() { * @param name the field name * @return the computed field header */ + @Override public FieldHeader getComputedFieldHeader(String name) { return fields.entrySet() .stream() @@ -113,6 +104,7 @@ public FieldHeader getComputedFieldHeader(String name) { * * @return the original method headers */ + @Override public Set getOriginalMethodHeaders() { return methods.keySet(); } @@ -128,6 +120,7 @@ public Set getOriginalMethodHeaders() { * @param descriptor the method descriptor * @return the computed method header */ + @Override public MethodHeader getComputedMethodHeader(String name, String descriptor) { return methods.entrySet() .stream() @@ -137,10 +130,12 @@ public MethodHeader getComputedMethodHeader(String name, String descriptor) { .orElseThrow(); } + @Override public Set getOriginalSuperClassConstructorHeaders() { return superClassConstructors.keySet(); } + @Override public MethodHeader getComputedSuperClassConstructorHeader(String descriptor) { return superClassConstructors.entrySet() .stream() @@ -247,17 +242,17 @@ public void computeMembers() { methods.put(submissionMethodHeader, solutionMethodHeader); } - resolveSuperClassMembers(superClassMembers, originalClassHeader.superName(), originalClassHeader.interfaces()); - for (Triple, Map> triple : superClassMembers) { - if (triple.getFirst().equals(originalClassHeader.superName())) { - triple.getThird() + 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())); } - triple.getSecond().forEach(fields::putIfAbsent); - triple.getThird() + superTypeMembers.fields().forEach(fields::putIfAbsent); + superTypeMembers.methods() .entrySet() .stream() .filter(entry -> !entry.getKey().name().equals("")) @@ -265,39 +260,19 @@ public void computeMembers() { } } - /** - * Recursively resolves the members of superclasses and interfaces. - * - * @param superClassMembers a set for recording class members - * @param superClass the name of the superclass to process - * @param interfaces the names of the interfaces to process - */ - private void resolveSuperClassMembers(Set, Map>> superClassMembers, - String superClass, - String[] interfaces) { - resolveSuperClassMembers(superClassMembers, superClass); - if (interfaces != null) { - for (String interfaceName : interfaces) { - resolveSuperClassMembers(superClassMembers, interfaceName); - } - } - } - /** * Recursively resolves the members of the given class. * - * @param superClassMembers a set for recording class members - * @param className the name of the class / interface to process + * @param superTypeMembers a set for recording class members + * @param typeName the name of the class / interface to process */ - private void resolveSuperClassMembers(Set, Map>> superClassMembers, - String className) { - if (className == null) { - return; - } + @Override + protected void resolveSuperTypeMembers(Set superTypeMembers, String typeName) { + if (typeName == null) return; - if (transformationContext.isSubmissionClass(className)) { - SubmissionClassInfo submissionClassInfo = transformationContext.getSubmissionClassInfo(className); - superClassMembers.add(new Triple<>(className, + 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())) @@ -306,31 +281,11 @@ private void resolveSuperClassMembers(Set !Modifier.isPrivate(entry.getKey().access())) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)))); - resolveSuperClassMembers(superClassMembers, + resolveSuperTypeMembers(superTypeMembers, submissionClassInfo.originalClassHeader.superName(), submissionClassInfo.originalClassHeader.interfaces()); } else { - try { - Class clazz = Class.forName(className.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())); - superClassMembers.add(new Triple<>(className, fieldHeaders, methodHeaders)); - if (clazz.getSuperclass() != null) { - resolveSuperClassMembers(superClassMembers, - Type.getInternalName(clazz.getSuperclass()), - Arrays.stream(clazz.getInterfaces()).map(Type::getInternalName).toArray(String[]::new)); - } - } catch (ClassNotFoundException e) { - throw new RuntimeException(e); - } + resolveExternalSuperTypeMembers(superTypeMembers, typeName, true); } } } diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/SubmissionClassVisitor.java similarity index 96% rename from tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java rename to tutor/src/main/java/org/tudalgo/algoutils/transform/classes/SubmissionClassVisitor.java index 3ba617ab..e8575222 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/SubmissionClassVisitor.java @@ -1,7 +1,9 @@ -package org.tudalgo.algoutils.transform; +package org.tudalgo.algoutils.transform.classes; import org.objectweb.asm.tree.MethodNode; -import org.tudalgo.algoutils.transform.methods.InjectingMethodVisitor; +import org.tudalgo.algoutils.transform.SolutionMergingClassTransformer; +import org.tudalgo.algoutils.transform.SubmissionExecutionHandler; +import org.tudalgo.algoutils.transform.methods.MissingMethodVisitor; import org.tudalgo.algoutils.transform.methods.SubmissionMethodVisitor; import org.tudalgo.algoutils.transform.util.*; import org.objectweb.asm.*; @@ -111,7 +113,7 @@ public class SubmissionClassVisitor extends ClassVisitor { private final Set visitedFields = new HashSet<>(); private final Set visitedMethods = new HashSet<>(); - SubmissionClassVisitor(ClassVisitor classVisitor, + public SubmissionClassVisitor(ClassVisitor classVisitor, TransformationContext transformationContext, String submissionClassName) { super(ASM9, classVisitor); @@ -216,7 +218,7 @@ public void visitEnd() { methodNode.accept(getDelegate()); } else { MethodVisitor mv = methodHeader.toMethodVisitor(getDelegate()); - methodNode.accept(new InjectingMethodVisitor(mv, transformationContext, submissionClassInfo, methodHeader)); + methodNode.accept(new MissingMethodVisitor(mv, transformationContext, submissionClassInfo, methodHeader)); } }); } 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 index 70b8cddc..16d10e4a 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/methods/BaseMethodVisitor.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/methods/BaseMethodVisitor.java @@ -2,7 +2,7 @@ import org.objectweb.asm.*; import org.objectweb.asm.tree.MethodNode; -import org.tudalgo.algoutils.transform.SubmissionClassInfo; +import org.tudalgo.algoutils.transform.classes.ClassInfo; import org.tudalgo.algoutils.transform.util.*; import java.util.ArrayList; @@ -20,7 +20,7 @@ public abstract class BaseMethodVisitor extends MethodVisitor { protected final MethodVisitor delegate; protected final TransformationContext transformationContext; - protected final SubmissionClassInfo submissionClassInfo; + protected final ClassInfo classInfo; protected final MethodHeader originalMethodHeader; protected final MethodHeader computedMethodHeader; @@ -33,13 +33,13 @@ public abstract class BaseMethodVisitor extends MethodVisitor { protected BaseMethodVisitor(MethodVisitor delegate, TransformationContext transformationContext, - SubmissionClassInfo submissionClassInfo, + ClassInfo classInfo, MethodHeader originalMethodHeader, MethodHeader computedMethodHeader) { super(ASM9, delegate); this.delegate = delegate; this.transformationContext = transformationContext; - this.submissionClassInfo = submissionClassInfo; + this.classInfo = classInfo; this.originalMethodHeader = originalMethodHeader; this.computedMethodHeader = computedMethodHeader; @@ -154,14 +154,14 @@ protected void injectSubstitutionCode(Label substitutionCheckLabel, Label nextLa delegate.visitLabel(substitutionStartLabel); if (isConstructor) { - List superConstructors = submissionClassInfo.getOriginalSuperClassConstructorHeaders() + List superConstructors = classInfo.getOriginalSuperClassConstructorHeaders() .stream() - .map(mh -> submissionClassInfo.getComputedSuperClassConstructorHeader(mh.descriptor())) + .map(mh -> classInfo.getComputedSuperClassConstructorHeader(mh.descriptor())) .toList(); - List constructors = submissionClassInfo.getOriginalMethodHeaders() + List constructors = classInfo.getOriginalMethodHeaders() .stream() .filter(mh -> mh.name().equals("")) - .map(mh -> submissionClassInfo.getComputedMethodHeader(mh.name(), mh.descriptor())) + .map(mh -> classInfo.getComputedMethodHeader(mh.name(), mh.descriptor())) .filter(mh -> !mh.descriptor().equals(computedMethodHeader.descriptor())) .toList(); Label[] labels = Stream.generate(Label::new) diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/methods/InjectingMethodVisitor.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/methods/MissingMethodVisitor.java similarity index 85% rename from tutor/src/main/java/org/tudalgo/algoutils/transform/methods/InjectingMethodVisitor.java rename to tutor/src/main/java/org/tudalgo/algoutils/transform/methods/MissingMethodVisitor.java index 19afaacf..5ba19e7a 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/methods/InjectingMethodVisitor.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/methods/MissingMethodVisitor.java @@ -2,7 +2,7 @@ import org.objectweb.asm.Label; import org.objectweb.asm.MethodVisitor; -import org.tudalgo.algoutils.transform.SubmissionClassInfo; +import org.tudalgo.algoutils.transform.classes.ClassInfo; import org.tudalgo.algoutils.transform.util.*; import java.util.EnumMap; @@ -11,15 +11,15 @@ import static org.objectweb.asm.Opcodes.*; import static org.objectweb.asm.Opcodes.F_CHOP; -public class InjectingMethodVisitor extends BaseMethodVisitor { +public class MissingMethodVisitor extends BaseMethodVisitor { private final Map localsIndexes = new EnumMap<>(LocalsObject.class); - public InjectingMethodVisitor(MethodVisitor delegate, - TransformationContext transformationContext, - SubmissionClassInfo submissionClassInfo, - MethodHeader methodHeader) { - super(delegate, transformationContext, submissionClassInfo, methodHeader, methodHeader); + public MissingMethodVisitor(MethodVisitor delegate, + TransformationContext transformationContext, + ClassInfo classInfo, + MethodHeader methodHeader) { + super(delegate, transformationContext, classInfo, methodHeader, methodHeader); localsIndexes.put(LocalsObject.SUBMISSION_EXECUTION_HANDLER, nextLocalsIndex); localsIndexes.put(LocalsObject.METHOD_HEADER, nextLocalsIndex + 1); @@ -78,6 +78,11 @@ public void visitCode() { 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) { if (headerMismatch) return; 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 index 90c10613..8467e720 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/methods/SubmissionMethodVisitor.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/methods/SubmissionMethodVisitor.java @@ -2,8 +2,8 @@ import org.objectweb.asm.*; import org.objectweb.asm.tree.MethodNode; -import org.tudalgo.algoutils.transform.SubmissionClassInfo; -import org.tudalgo.algoutils.transform.SubmissionClassVisitor; +import org.tudalgo.algoutils.transform.classes.SubmissionClassInfo; +import org.tudalgo.algoutils.transform.classes.SubmissionClassVisitor; import org.tudalgo.algoutils.transform.util.*; import java.util.*; @@ -46,7 +46,7 @@ protected int getLocalsIndex(LocalsObject localsObject) { @Override public void visitCode() { - Optional solutionMethodNode = submissionClassInfo.getSolutionClass() + Optional solutionMethodNode = ((SubmissionClassInfo) classInfo).getSolutionClass() .map(solutionClassNode -> solutionClassNode.getMethods().get(computedMethodHeader)); Label submissionExecutionHandlerVarLabel = new Label(); Label methodHeaderVarLabel = new Label(); 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 index e3266d22..9c25a583 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationContext.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationContext.java @@ -2,9 +2,9 @@ import org.objectweb.asm.ClassReader; import org.objectweb.asm.Type; -import org.tudalgo.algoutils.transform.SolutionClassNode; +import org.tudalgo.algoutils.transform.classes.SolutionClassNode; import org.tudalgo.algoutils.transform.SolutionMergingClassTransformer; -import org.tudalgo.algoutils.transform.SubmissionClassInfo; +import org.tudalgo.algoutils.transform.classes.SubmissionClassInfo; import java.io.IOException; import java.io.InputStream; From c509df6b83b83b8fd6eacfeab31f443d682f50ae Mon Sep 17 00:00:00 2001 From: Daniel Mangold Date: Wed, 4 Dec 2024 16:33:40 +0100 Subject: [PATCH 31/48] Small fixes and some refactoring --- .../transform/SubmissionExecutionHandler.java | 18 +- .../classes/MissingClassVisitor.java | 14 +- .../transform/classes/SolutionClassNode.java | 2 +- .../classes/SubmissionClassInfo.java | 4 +- .../classes/SubmissionClassVisitor.java | 42 ++-- .../transform/methods/BaseMethodVisitor.java | 182 ++++++++++-------- .../methods/LambdaMethodVisitor.java | 35 ++++ .../methods/MissingMethodVisitor.java | 49 ++--- .../methods/SubmissionMethodVisitor.java | 4 +- .../util/IncompatibleHeaderException.java | 9 +- .../transform/util/MethodHeader.java | 13 ++ .../transform/util/TransformationContext.java | 62 +++--- 12 files changed, 265 insertions(+), 169 deletions(-) create mode 100644 tutor/src/main/java/org/tudalgo/algoutils/transform/methods/LambdaMethodVisitor.java diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionExecutionHandler.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionExecutionHandler.java index 413c3a3f..bf96728b 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionExecutionHandler.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionExecutionHandler.java @@ -98,8 +98,10 @@ public static ClassHeader getOriginalClassHeader(Class clazz) { return (ClassHeader) MethodHandles.lookup() .findStatic(clazz, Constants.INJECTED_GET_ORIGINAL_CLASS_HEADER.name(), MethodType.methodType(ClassHeader.class)) .invokeExact(); - } catch (Throwable e) { - throw new RuntimeException(e); + } catch (RuntimeException e) { + throw e; + } catch (Throwable t) { + throw new RuntimeException(t); } } @@ -115,8 +117,10 @@ public static Set getOriginalFieldHeaders(Class clazz) { return (Set) MethodHandles.lookup() .findStatic(clazz, Constants.INJECTED_GET_ORIGINAL_FIELD_HEADERS.name(), MethodType.methodType(Set.class)) .invokeExact(); - } catch (Throwable e) { - throw new RuntimeException(e); + } catch (RuntimeException e) { + throw e; + } catch (Throwable t) { + throw new RuntimeException(t); } } @@ -132,8 +136,10 @@ public static Set getOriginalMethodHeaders(Class clazz) { return (Set) MethodHandles.lookup() .findStatic(clazz, Constants.INJECTED_GET_ORIGINAL_METHODS_HEADERS.name(), MethodType.methodType(Set.class)) .invokeExact(); - } catch (Throwable e) { - throw new RuntimeException(e); + } catch (RuntimeException e) { + throw e; + } catch (Throwable t) { + throw new RuntimeException(t); } } 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 index 956fbf31..4962ad8e 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/MissingClassVisitor.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/MissingClassVisitor.java @@ -2,15 +2,13 @@ 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.MethodHeader; import org.tudalgo.algoutils.transform.util.TransformationContext; -import static org.objectweb.asm.Opcodes.ACONST_NULL; -import static org.objectweb.asm.Opcodes.ARETURN; -import static org.objectweb.asm.Opcodes.ASM9; - public class MissingClassVisitor extends ClassVisitor { private final TransformationContext transformationContext; @@ -19,7 +17,7 @@ public class MissingClassVisitor extends ClassVisitor { public MissingClassVisitor(ClassVisitor delegate, TransformationContext transformationContext, SolutionClassNode solutionClassNode) { - super(ASM9, delegate); + super(Opcodes.ASM9, delegate); this.transformationContext = transformationContext; this.solutionClassNode = solutionClassNode; @@ -45,8 +43,8 @@ public void visitEnd() { private void injectMetadataMethod(MethodHeader methodHeader) { MethodVisitor mv = methodHeader.toMethodVisitor(getDelegate()); - mv.visitInsn(ACONST_NULL); - mv.visitInsn(ARETURN); - mv.visitMaxs(1, 0); + 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 index 373f5195..d5a63b65 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/SolutionClassNode.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/SolutionClassNode.java @@ -100,7 +100,7 @@ private MethodNode getMethodNode(int access, String name, String descriptor, Str return 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, 0, name, descriptor, null, null); + MethodHeader methodHeader = new MethodHeader(owner, name, descriptor); if (transformationContext.methodHasReplacement(methodHeader)) { MethodHeader replacementMethodHeader = transformationContext.getMethodReplacement(methodHeader); super.visitMethodInsn(INVOKESTATIC, 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 index bfc24948..767e51aa 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/SubmissionClassInfo.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/SubmissionClassInfo.java @@ -178,7 +178,7 @@ public MethodVisitor visitMethod(int access, String name, String descriptor, Str return null; } - public void computeMembers() { + public void resolveMembers() { SimilarityMapper fieldsSimilarityMapper = new SimilarityMapper<>( fields.keySet(), getSolutionClass().map(solutionClass -> solutionClass.getFields().keySet()).orElseGet(Collections::emptySet), @@ -221,7 +221,7 @@ public void computeMembers() { Supplier fallbackMethodHeader = () -> new MethodHeader(computedClassHeader.name(), submissionMethodHeader.access(), submissionMethodHeader.name(), - transformationContext.toComputedType(submissionMethodHeader.descriptor()).getDescriptor(), + transformationContext.toComputedDescriptor(submissionMethodHeader.descriptor()), submissionMethodHeader.signature(), submissionMethodHeader.exceptions()); MethodHeader solutionMethodHeader; 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 index e8575222..0c2888ad 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/SubmissionClassVisitor.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/SubmissionClassVisitor.java @@ -3,6 +3,7 @@ import org.objectweb.asm.tree.MethodNode; import org.tudalgo.algoutils.transform.SolutionMergingClassTransformer; import org.tudalgo.algoutils.transform.SubmissionExecutionHandler; +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.*; @@ -114,8 +115,8 @@ public class SubmissionClassVisitor extends ClassVisitor { private final Set visitedMethods = new HashSet<>(); public SubmissionClassVisitor(ClassVisitor classVisitor, - TransformationContext transformationContext, - String submissionClassName) { + TransformationContext transformationContext, + String submissionClassName) { super(ASM9, classVisitor); this.transformationContext = transformationContext; this.submissionClassInfo = transformationContext.getSubmissionClassInfo(submissionClassName); @@ -141,15 +142,16 @@ public void visit(int version, int access, String name, String signature, String public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) { FieldHeader fieldHeader = submissionClassInfo.getComputedFieldHeader(name); - if (!TransformationUtils.contextIsCompatible(access, fieldHeader.access()) || visitedFields.contains(fieldHeader)) { + if (TransformationUtils.contextIsCompatible(access, fieldHeader.access()) && + transformationContext.descriptorIsCompatible(descriptor, fieldHeader.descriptor())) { + visitedFields.add(fieldHeader); + return fieldHeader.toFieldVisitor(getDelegate(), value); + } else { return super.visitField(TransformationUtils.transformAccess(access), name + "$submission", - fieldHeader.descriptor(), - fieldHeader.signature(), + transformationContext.toComputedDescriptor(descriptor), + signature, value); - } else { - visitedFields.add(fieldHeader); - return fieldHeader.toFieldVisitor(getDelegate(), value); } } @@ -159,25 +161,31 @@ public FieldVisitor visitField(int access, String name, String descriptor, Strin */ @Override public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { - // if method is lambda, skip transformation if (TransformationUtils.isLambdaMethod(access, name)) { - return super.visitMethod(access, name, descriptor, signature, exceptions); + MethodHeader methodHeader = new MethodHeader(originalClassHeader.name(), + access, + name, + transformationContext.toComputedDescriptor(descriptor), + signature, + exceptions); + return new LambdaMethodVisitor(methodHeader.toMethodVisitor(getDelegate()), + transformationContext, + submissionClassInfo, + methodHeader); } else { MethodHeader originalMethodHeader = new MethodHeader(originalClassHeader.name(), access, name, descriptor, signature, exceptions); MethodHeader computedMethodHeader = submissionClassInfo.getComputedMethodHeader(name, descriptor); - boolean headerMismatch = !transformationContext.toComputedType(originalMethodHeader.descriptor()) - .getDescriptor() - .equals(computedMethodHeader.descriptor()); - if (!TransformationUtils.contextIsCompatible(access, computedMethodHeader.access()) || visitedMethods.contains(computedMethodHeader) || headerMismatch) { + if (TransformationUtils.contextIsCompatible(access, computedMethodHeader.access()) && + transformationContext.descriptorIsCompatible(descriptor, computedMethodHeader.descriptor())) { + visitedMethods.add(computedMethodHeader); + } else { computedMethodHeader = new MethodHeader(computedMethodHeader.owner(), access, name + "$submission", - transformationContext.toComputedType(descriptor).getDescriptor(), + transformationContext.toComputedDescriptor(descriptor), signature, exceptions); - } else { - visitedMethods.add(computedMethodHeader); } return new SubmissionMethodVisitor(computedMethodHeader.toMethodVisitor(getDelegate()), transformationContext, 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 index 16d10e4a..2d2aed31 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/methods/BaseMethodVisitor.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/methods/BaseMethodVisitor.java @@ -3,6 +3,7 @@ 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 java.util.ArrayList; @@ -43,9 +44,9 @@ protected BaseMethodVisitor(MethodVisitor delegate, this.originalMethodHeader = originalMethodHeader; this.computedMethodHeader = computedMethodHeader; - this.headerMismatch = !transformationContext.toComputedType(originalMethodHeader.descriptor()) - .getDescriptor() - .equals(computedMethodHeader.descriptor()); + // 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(""); @@ -88,8 +89,8 @@ public String descriptor() { return descriptor; } - public void visitLocalVariable(BaseMethodVisitor mv, Label start, Label end) { - mv.visitLocalVariable(varName, descriptor, null, start, end, mv.getLocalsIndex(this)); + public void visitLocalVariable(BaseMethodVisitor bmv, Label start, Label end) { + bmv.visitLocalVariable(varName, descriptor, null, start, end, bmv.getLocalsIndex(this)); } } @@ -380,8 +381,6 @@ protected void injectConstructorInvocationBranch(MethodHeader constructorHeader, getLocalsIndex(LocalsObject.CONSTRUCTOR_INVOCATION) + 1); } - // Prevent bytecode to be added to the method if there is a header mismatch - @Override public void visitFieldInsn(int opcode, String owner, String name, String descriptor) { if (headerMismatch) return; @@ -390,21 +389,15 @@ public void visitFieldInsn(int opcode, String owner, String name, String descrip if (!transformationContext.isSubmissionClass(owner)) { delegate.visitFieldInsn(opcode, owner, name, descriptor); } else { - if (owner.startsWith("[")) { // stupid edge cases + FieldHeader computedFieldHeader = transformationContext.getSubmissionClassInfo(owner).getComputedFieldHeader(name); + if (TransformationUtils.opcodeIsCompatible(opcode, computedFieldHeader.access()) && + transformationContext.descriptorIsCompatible(descriptor, computedFieldHeader.descriptor())) { delegate.visitFieldInsn(opcode, - transformationContext.toComputedType(owner).getInternalName(), - name, - transformationContext.toComputedType(descriptor).getDescriptor()); - } else { - FieldHeader computedFieldHeader = transformationContext.getSubmissionClassInfo(owner).getComputedFieldHeader(name); - if (TransformationUtils.opcodeIsCompatible(opcode, computedFieldHeader.access())) { - delegate.visitFieldInsn(opcode, - transformationContext.toComputedType(computedFieldHeader.owner()).getInternalName(), - computedFieldHeader.name(), - computedFieldHeader.descriptor()); - } else { // if incompatible - delegate.visitFieldInsn(opcode, computedFieldHeader.owner(), name + "$submission", computedFieldHeader.descriptor()); - } + transformationContext.toComputedInternalName(computedFieldHeader.owner()), + computedFieldHeader.name(), + computedFieldHeader.descriptor()); + } else { // if incompatible + delegate.visitFieldInsn(opcode, computedFieldHeader.owner(), name + "$submission", computedFieldHeader.descriptor()); } } } @@ -413,28 +406,25 @@ public void visitFieldInsn(int opcode, String owner, String name, String descrip public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) { if (headerMismatch) return; - // skip transformation if owner is not part of the submission - MethodHeader methodHeader = new MethodHeader(owner, 0, name, descriptor, null, null); + 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("[")) { // stupid edge cases - delegate.visitMethodInsn(opcode, - transformationContext.toComputedType(owner).getInternalName(), - name, - transformationContext.toComputedType(descriptor).getDescriptor(), - isInterface); + } else if (owner.startsWith("[")) { + delegate.visitMethodInsn(opcode, transformationContext.toComputedInternalName(owner), name, descriptor, isInterface); } else { - String computedOwner = transformationContext.toComputedType(owner).getInternalName(); + // 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())) { + 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.toComputedType(descriptor).getDescriptor(), + transformationContext.toComputedDescriptor(descriptor), isInterface); } } @@ -451,90 +441,132 @@ public void visitLdcInsn(Object value) { public void visitTypeInsn(int opcode, String type) { if (headerMismatch) return; - super.visitTypeInsn(opcode, transformationContext.toComputedType(type).getInternalName()); + super.visitTypeInsn(opcode, transformationContext.toComputedInternalName(type)); } @Override public void visitFrame(int type, int numLocal, Object[] local, int numStack, Object[] stack) { - if (!headerMismatch) { - Object[] computedLocals = local == null ? null : Arrays.stream(local) - .map(o -> o instanceof String s ? transformationContext.toComputedType(s).getInternalName() : o) - .toArray(); - Object[] computedStack = stack == null ? null : Arrays.stream(stack) - .map(o -> o instanceof String s ? transformationContext.toComputedType(s).getInternalName() : o) - .toArray(); - super.visitFrame(type, numLocal, computedLocals, numStack, computedStack); - } + 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) { - super.visitInsn(opcode); - } + if (headerMismatch) return; + + super.visitInsn(opcode); } @Override public void visitIntInsn(int opcode, int operand) { - if (!headerMismatch) { - super.visitIntInsn(opcode, operand); - } + if (headerMismatch) return; + + super.visitIntInsn(opcode, operand); } @Override public void visitIincInsn(int varIndex, int increment) { - if (!headerMismatch) { - super.visitIincInsn(varIndex, increment); - } + if (headerMismatch) return; + + super.visitIincInsn(varIndex, increment); } @Override public void visitVarInsn(int opcode, int varIndex) { - if (!headerMismatch) { - super.visitVarInsn(opcode, varIndex); - } + if (headerMismatch) return; + + super.visitVarInsn(opcode, varIndex); } @Override public void visitJumpInsn(int opcode, Label label) { - if (!headerMismatch) { - super.visitJumpInsn(opcode, label); - } + if (headerMismatch) return; + + super.visitJumpInsn(opcode, label); } @Override public void visitInvokeDynamicInsn(String name, String descriptor, Handle bootstrapMethodHandle, Object... bootstrapMethodArguments) { if (headerMismatch) return; - super.visitInvokeDynamicInsn(name, descriptor, bootstrapMethodHandle, bootstrapMethodArguments); + 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) { - super.visitLookupSwitchInsn(dflt, keys, labels); - } + if (headerMismatch) return; + + super.visitLookupSwitchInsn(dflt, keys, labels); } @Override public void visitTableSwitchInsn(int min, int max, Label dflt, Label... labels) { - if (!headerMismatch) { - super.visitTableSwitchInsn(min, max, dflt, labels); - } + if (headerMismatch) return; + + super.visitTableSwitchInsn(min, max, dflt, labels); } @Override public void visitMultiANewArrayInsn(String descriptor, int numDimensions) { if (headerMismatch) return; - super.visitMultiANewArrayInsn(transformationContext.toComputedType(descriptor).getDescriptor(), numDimensions); + super.visitMultiANewArrayInsn(transformationContext.toComputedDescriptor(descriptor), numDimensions); } @Override public void visitLabel(Label label) { - if (!headerMismatch) { - super.visitLabel(label); - } + if (headerMismatch) return; + + super.visitLabel(label); } @Override @@ -546,7 +578,7 @@ public AnnotationVisitor visitInsnAnnotation(int typeRef, TypePath typePath, Str public void visitTryCatchBlock(Label start, Label end, Label handler, String type) { if (headerMismatch) return; - super.visitTryCatchBlock(start, end, handler, type != null ? transformationContext.toComputedType(type).getInternalName() : null); + super.visitTryCatchBlock(start, end, handler, type != null ? transformationContext.toComputedInternalName(type) : null); } @Override @@ -569,15 +601,15 @@ public AnnotationVisitor visitLocalVariableAnnotation(int typeRef, TypePath type @Override public void visitLineNumber(int line, Label start) { - if (!headerMismatch) { - super.visitLineNumber(line, start); - } + if (headerMismatch) return; + + super.visitLineNumber(line, start); } @Override public void visitAttribute(Attribute attribute) { - if (!headerMismatch) { - super.visitAttribute(attribute); - } + if (headerMismatch) return; + + super.visitAttribute(attribute); } } 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..61f1af50 --- /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.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 index 5ba19e7a..1f4c0352 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/methods/MissingMethodVisitor.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/methods/MissingMethodVisitor.java @@ -51,30 +51,26 @@ public void visitCode() { injectSubstitutionCode(substitutionCheckLabel, delegationCheckLabel); // Method delegation - // if only default transformations are applied, skip 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.SUBMISSION_EXECUTION_HANDLER)); - 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, 2, null, 0, null); - fullFrameLocals.removeLast(); - fullFrameLocals.removeLast(); - delegate.visitLabel(delegationCodeLabel); - LocalsObject.SUBMISSION_EXECUTION_HANDLER.visitLocalVariable(this, submissionExecutionHandlerVarLabel, delegationCodeLabel); - LocalsObject.METHOD_HEADER.visitLocalVariable(this, methodHeaderVarLabel, delegationCodeLabel); - new IncompatibleHeaderException("Method has incorrect return or parameter types", computedMethodHeader, null) - .replicateInBytecode(getDelegate(), true); - - delegate.visitFrame(F_FULL, fullFrameLocals.size(), fullFrameLocals.toArray(), 0, new Object[0]); - delegate.visitLabel(submissionCodeLabel); - } - + // 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.SUBMISSION_EXECUTION_HANDLER)); + 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, 2, null, 0, null); + fullFrameLocals.removeLast(); + fullFrameLocals.removeLast(); + delegate.visitLabel(delegationCodeLabel); + LocalsObject.SUBMISSION_EXECUTION_HANDLER.visitLocalVariable(this, submissionExecutionHandlerVarLabel, 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(); } @@ -85,10 +81,7 @@ public void visitFieldInsn(int opcode, String owner, String name, String descrip @Override public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) { - if (headerMismatch) return; - - // skip transformation if owner is not part of the submission - MethodHeader methodHeader = new MethodHeader(owner, 0, name, descriptor, null, null); + MethodHeader methodHeader = new MethodHeader(owner, name, descriptor); if (transformationContext.methodHasReplacement(methodHeader)) { transformationContext.getMethodReplacement(methodHeader).toMethodInsn(getDelegate(), false); } else { 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 index 8467e720..dd190c1a 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/methods/SubmissionMethodVisitor.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/methods/SubmissionMethodVisitor.java @@ -76,8 +76,8 @@ public void visitCode() { } if (headerMismatch) { - new IncompatibleHeaderException("Method has incorrect return or parameter types", computedMethodHeader, originalMethodHeader) - .replicateInBytecode(getDelegate(), true); + 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/IncompatibleHeaderException.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/IncompatibleHeaderException.java index 7e345890..3006fab4 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/IncompatibleHeaderException.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/IncompatibleHeaderException.java @@ -54,12 +54,15 @@ public String getMessage() { * * @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 int replicateInBytecode(MethodVisitor mv, boolean throwException) { + public static int replicateInBytecode(MethodVisitor mv, boolean throwException, String message, Header expected, Header actual) { int maxStack, stackSize; - mv.visitTypeInsn(NEW, Type.getInternalName(getClass())); + mv.visitTypeInsn(NEW, Type.getInternalName(IncompatibleHeaderException.class)); mv.visitInsn(DUP); mv.visitLdcInsn(message); maxStack = stackSize = 3; @@ -71,7 +74,7 @@ public int replicateInBytecode(MethodVisitor mv, boolean throwException) { maxStack = Math.max(maxStack, ++stackSize); } mv.visitMethodInsn(INVOKESPECIAL, - Type.getInternalName(getClass()), + Type.getInternalName(IncompatibleHeaderException.class), "", Type.getMethodDescriptor(Type.VOID_TYPE, Constants.STRING_TYPE, Constants.HEADER_TYPE, Constants.HEADER_TYPE), false); diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/MethodHeader.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/MethodHeader.java index 7664c6f1..3e84390c 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/MethodHeader.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/MethodHeader.java @@ -29,6 +29,19 @@ */ public record MethodHeader(String owner, int access, String name, String descriptor, String signature, String[] exceptions) implements Header { + /** + * Constructs a new method header with reduced 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. * 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 index 9c25a583..feb52128 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationContext.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationContext.java @@ -151,7 +151,7 @@ public SubmissionClassInfo getSubmissionClassInfo(String submissionClassName) { boolean isAbsent = !submissionClasses.containsKey(submissionClassName); SubmissionClassInfo submissionClassInfo = submissionClasses.computeIfAbsent(submissionClassName, this::readSubmissionClass); if (isAbsent && submissionClassInfo != null) { - submissionClassInfo.computeMembers(); + submissionClassInfo.resolveMembers(); } return submissionClassInfo; } @@ -207,32 +207,6 @@ public Map getSolutionClasses() { return Collections.unmodifiableMap(solutionClasses); } - /** - * Returns the computed (i.e., mapped from submission to solution) type. - * The given value may be an internal class name or a descriptor. - * If the value is a method descriptor, this method will return a descriptor where - * all parameter types and the return types have been computed. - * If the value is an array descriptor, it will return a descriptor where - * the component type has been computed. - * If the given internal class name or descriptor are not part of the submission - * or no corresponding solution class exists, it will return a {@link Type} object - * representing the original name / descriptor. - * - * @param descriptor the class name or descriptor - * @return the computed type - */ - public Type toComputedType(String descriptor) { - if (descriptor.startsWith("(")) { // method descriptor - return toComputedType(Type.getMethodType(descriptor)); - } else if (descriptor.startsWith("[") || descriptor.endsWith(";")) { // array or reference descriptor - return toComputedType(Type.getType(descriptor)); - } else if (descriptor.length() == 1 && "VZBSCIFJD".contains(descriptor)) { // primitive type - return Type.getType(descriptor); - } else { - return toComputedType(Type.getObjectType(descriptor)); - } - } - /** * 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 @@ -260,6 +234,40 @@ public Type toComputedType(Type 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/}. * From d30049694ed4de751e2968584fa3e638bb2d9dc7 Mon Sep 17 00:00:00 2001 From: Daniel Mangold Date: Wed, 4 Dec 2024 16:34:54 +0100 Subject: [PATCH 32/48] Add missing documentation --- .../transform/classes/ClassInfo.java | 92 ++++++++++++++++++- .../transform/classes/MissingClassInfo.java | 18 ++++ .../classes/MissingClassVisitor.java | 13 +++ .../classes/SubmissionClassInfo.java | 87 +++++------------- .../classes/SubmissionClassVisitor.java | 10 +- 5 files changed, 156 insertions(+), 64 deletions(-) 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 index 559621c1..964aefd5 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/ClassInfo.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/ClassInfo.java @@ -14,6 +14,21 @@ 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; @@ -23,34 +38,102 @@ public abstract class ClassInfo extends ClassVisitor { 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 class members + * @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 */ @@ -63,6 +146,13 @@ protected void resolveSuperTypeMembers(Set superTypeMembers, S } } + /** + * 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('/', '.')); 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 index 19c95dc3..7b94d8d8 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/MissingClassInfo.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/MissingClassInfo.java @@ -1,5 +1,6 @@ package org.tudalgo.algoutils.transform.classes; +import org.objectweb.asm.ClassVisitor; import org.tudalgo.algoutils.transform.util.ClassHeader; import org.tudalgo.algoutils.transform.util.FieldHeader; import org.tudalgo.algoutils.transform.util.MethodHeader; @@ -11,10 +12,27 @@ 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); 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 index 4962ad8e..6b3f7b92 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/MissingClassVisitor.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/MissingClassVisitor.java @@ -9,11 +9,24 @@ import org.tudalgo.algoutils.transform.util.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) { 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 index 767e51aa..bda2ef21 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/SubmissionClassInfo.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/SubmissionClassInfo.java @@ -10,10 +10,18 @@ /** * 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 itself to remain usable. + *

    + * 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 */ @@ -38,57 +46,21 @@ public SubmissionClassInfo(TransformationContext transformationContext, this.fsAnnotationProcessor = fsAnnotationProcessor; } - /** - * Returns the original class header. - * - * @return the original class header - */ @Override public ClassHeader getOriginalClassHeader() { return originalClassHeader; } - /** - * Returns the computed class name. - * The computed name is the name of the associated solution class, if one is present. - * If no solution class is present, the computed names equals the original submission class name. - * - * @return the computed class name - */ @Override public ClassHeader getComputedClassHeader() { return computedClassHeader; } - /** - * 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); - } - - /** - * Returns the original field headers for this class. - * - * @return the original field headers - */ @Override public Set getOriginalFieldHeaders() { return fields.keySet(); } - /** - * 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 - */ @Override public FieldHeader getComputedFieldHeader(String name) { return fields.entrySet() @@ -99,27 +71,11 @@ public FieldHeader getComputedFieldHeader(String name) { .orElseThrow(); } - /** - * Return the original method headers for this class. - * - * @return the original method headers - */ @Override public Set getOriginalMethodHeaders() { return methods.keySet(); } - /** - * 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 - */ @Override public MethodHeader getComputedMethodHeader(String name, String descriptor) { return methods.entrySet() @@ -178,6 +134,19 @@ public MethodVisitor visitMethod(int access, String name, String descriptor, Str 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() { SimilarityMapper fieldsSimilarityMapper = new SimilarityMapper<>( fields.keySet(), @@ -260,12 +229,6 @@ public void resolveMembers() { } } - /** - * Recursively resolves the members of the given class. - * - * @param superTypeMembers a set for recording class members - * @param typeName the name of the class / interface to process - */ @Override protected void resolveSuperTypeMembers(Set superTypeMembers, String typeName) { if (typeName == null) return; 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 index 0c2888ad..9a332240 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/SubmissionClassVisitor.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/SubmissionClassVisitor.java @@ -114,6 +114,13 @@ public class SubmissionClassVisitor extends ClassVisitor { private final Set visitedFields = new HashSet<>(); private final Set visitedMethods = new HashSet<>(); + /** + * 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) { @@ -157,7 +164,8 @@ public FieldVisitor visitField(int access, String name, String descriptor, Strin /** * Visits a method of a submission class and transforms it. - * Enables invocation logging, substitution and, if a solution class is present, delegation. + * 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) { From f52c3f12fa20db859a0a6f8859a0b40b1a5dca3a Mon Sep 17 00:00:00 2001 From: Daniel Mangold Date: Thu, 5 Dec 2024 10:46:26 +0100 Subject: [PATCH 33/48] Rework SubmissionExecutionHandler API --- .../transform/SubmissionExecutionHandler.java | 343 ++++++++---------- .../transform/methods/BaseMethodVisitor.java | 30 +- .../methods/MissingMethodVisitor.java | 12 +- .../methods/SubmissionMethodVisitor.java | 18 +- .../algoutils/transform/util/Constants.java | 6 - .../algoutils/transform/util/Invocation.java | 18 +- 6 files changed, 182 insertions(+), 245 deletions(-) diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionExecutionHandler.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionExecutionHandler.java index bf96728b..7a1a91b5 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionExecutionHandler.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionExecutionHandler.java @@ -18,8 +18,7 @@ * * 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 either explicitly using {@link #disableMethodDelegation} or implicitly using - * {@link #substituteMethod}. + * 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}. *

    @@ -62,29 +61,13 @@ @SuppressWarnings("unused") public class SubmissionExecutionHandler { - private static SubmissionExecutionHandler instance; - // declaring class => (method header => invocations) - private final Map>> methodInvocations = new HashMap<>(); - private final Map> methodSubstitutions = new HashMap<>(); - private final Map> methodDelegationAllowlist = new HashMap<>(); + 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() {} - /** - * Returns the global {@link SubmissionExecutionHandler} instance. - * - * @return the global {@link SubmissionExecutionHandler} instance - * @throws IllegalStateException if no global instance is present, i.e., the project has not been - * transformed by {@link SolutionMergingClassTransformer} - */ - public static SubmissionExecutionHandler getInstance() { - if (instance == null) { - instance = new SubmissionExecutionHandler(); - } - return instance; - } - // Submission class info /** @@ -143,184 +126,180 @@ public static Set getOriginalMethodHeaders(Class clazz) { } } - // Invocation logging - /** - * Enables logging of method / constructor invocations for the given executable. - * - * @param executable the method / constructor to enable invocation logging for + * Resets all mechanisms. */ - public void enableMethodInvocationLogging(Executable executable) { - enableMethodInvocationLogging(new MethodHeader(executable)); + public static void resetAll() { + Logging.reset(); + Substitution.reset(); + Delegation.reset(); } - /** - * Enables logging of method invocations for the given method. - * - * @param methodHeader a method header describing the method - */ - public void enableMethodInvocationLogging(MethodHeader methodHeader) { - methodInvocations.computeIfAbsent(methodHeader.owner(), k -> new HashMap<>()) - .putIfAbsent(methodHeader, new ArrayList<>()); - } + public static final class Logging { - /** - * 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 void disableMethodInvocationLogging(Executable executable) { - disableMethodInvocationLogging(new MethodHeader(executable)); - } + private Logging() {} - /** - * 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 void disableMethodInvocationLogging(MethodHeader methodHeader) { - Optional.ofNullable(methodInvocations.get(methodHeader.owner())) - .ifPresent(map -> map.remove(methodHeader)); - } + /** + * 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)); + } - /** - * Resets the logging of method invocations to log no invocations. - */ - public void resetMethodInvocationLogging() { - methodInvocations.clear(); - } + /** + * 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<>()); + } - /** - * 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 List getInvocationsForMethod(Executable executable) { - return getInvocationsForMethod(new MethodHeader(executable)); - } + /** + * 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)); + } - /** - * 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 List getInvocationsForMethod(MethodHeader methodHeader) { - return Optional.ofNullable(methodInvocations.get(methodHeader.owner())) - .map(map -> map.get(methodHeader)) - .map(Collections::unmodifiableList) - .orElse(null); + /** + * 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(); + } } - // Method substitution + public static final class 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 during runtime. - * - * @param executable the method / constructor to substitute - * @param substitute the {@link MethodSubstitution} the method will be substituted with - */ - public void substituteMethod(Executable executable, MethodSubstitution substitute) { - substituteMethod(new MethodHeader(executable), substitute); - } + private Substitution() {} - /** - * 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 during runtime. - * - * @param methodHeader a method header describing the method - * @param substitute the {@link MethodSubstitution} the method will be substituted with - */ - public void substituteMethod(MethodHeader methodHeader, MethodSubstitution substitute) { - methodSubstitutions.computeIfAbsent(methodHeader.owner(), k -> new HashMap<>()) - .put(methodHeader, substitute); - } + /** + * 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); + } - /** - * Disables substitution for the given method / constructor. - * - * @param executable the substituted method / constructor - */ - public void disableMethodSubstitution(Executable executable) { - disableMethodSubstitution(new MethodHeader(executable)); - } + /** + * 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. - * - * @param methodHeader a method header describing the method - */ - public void disableMethodSubstitution(MethodHeader methodHeader) { - Optional.ofNullable(methodSubstitutions.get(methodHeader.owner())) - .ifPresent(map -> map.remove(methodHeader)); - } + /** + * Disables substitution for the given method / constructor. + * + * @param executable the substituted method / constructor + */ + public static void disable(Executable executable) { + disable(new MethodHeader(executable)); + } - /** - * Resets the substitution of methods. - */ - public void resetMethodSubstitution() { - methodSubstitutions.clear(); + /** + * 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(); + } } - // Method delegation + public static final class 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 void enableMethodDelegation(Executable executable) { - enableMethodDelegation(new MethodHeader(executable)); - } + private Delegation() {} - /** - * 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 void enableMethodDelegation(MethodHeader methodHeader) { - methodDelegationAllowlist.computeIfAbsent(methodHeader.owner(), k -> new HashMap<>()) - .put(methodHeader, false); - } + /** + * 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)); + } - /** - * Disables delegation to the solution for the given executable. - * - * @param executable the method / constructor to disable delegation for - */ - public void disableMethodDelegation(Executable executable) { - disableMethodDelegation(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 method. - * - * @param methodHeader a method header describing the method - */ - public void disableMethodDelegation(MethodHeader methodHeader) { - methodDelegationAllowlist.computeIfAbsent(methodHeader.owner(), k -> new HashMap<>()) - .put(methodHeader, true); - } + /** + * 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)); + } - /** - * Resets the delegation of methods. - */ - public void resetMethodDelegation() { - methodDelegationAllowlist.clear(); + /** + * 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 final class Internal { + public static final class Internal { + + private Internal() {} // Invocation logging @@ -332,8 +311,8 @@ public final class Internal { * @param methodHeader a method header describing the method * @return {@code true} if invocation logging is enabled for the given method, otherwise {@code false} */ - public boolean logInvocation(MethodHeader methodHeader) { - return Optional.ofNullable(methodInvocations.get(methodHeader.owner())) + public static boolean logInvocation(MethodHeader methodHeader) { + return Optional.ofNullable(METHOD_INVOCATIONS.get(methodHeader.owner())) .map(map -> map.get(methodHeader)) .isPresent(); } @@ -345,8 +324,8 @@ public boolean logInvocation(MethodHeader methodHeader) { * @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 void addInvocation(MethodHeader methodHeader, Invocation invocation) { - Optional.ofNullable(methodInvocations.get(methodHeader.owner())) + 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)); } @@ -360,8 +339,8 @@ public void addInvocation(MethodHeader methodHeader, Invocation invocation) { * @param methodHeader a method header describing the method * @return {@code true} if substitution is enabled for the given method, otherwise {@code false} */ - public boolean useSubstitution(MethodHeader methodHeader) { - return Optional.ofNullable(methodSubstitutions.get(methodHeader.owner())) + public static boolean useSubstitution(MethodHeader methodHeader) { + return Optional.ofNullable(METHOD_SUBSTITUTIONS.get(methodHeader.owner())) .map(map -> map.containsKey(methodHeader)) .orElse(false); } @@ -373,8 +352,8 @@ public boolean useSubstitution(MethodHeader methodHeader) { * @param methodHeader a method header describing the method * @return the substitute for the given method */ - public MethodSubstitution getSubstitution(MethodHeader methodHeader) { - return Optional.ofNullable(methodSubstitutions.get(methodHeader.owner())) + public static MethodSubstitution getSubstitution(MethodHeader methodHeader) { + return Optional.ofNullable(METHOD_SUBSTITUTIONS.get(methodHeader.owner())) .map(map -> map.get(methodHeader)) .orElseThrow(); } @@ -388,9 +367,9 @@ public MethodSubstitution getSubstitution(MethodHeader methodHeader) { * @param methodHeader a method header describing the method * @return {@code true} if delegation is disabled for the given method, otherwise {@code false} */ - public boolean useSubmissionImpl(MethodHeader methodHeader) { - return Optional.ofNullable(methodDelegationAllowlist.get(methodHeader.owner())) - .map(map -> map.get(methodHeader)) + 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/methods/BaseMethodVisitor.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/methods/BaseMethodVisitor.java index 2d2aed31..c2cfebac 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/methods/BaseMethodVisitor.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/methods/BaseMethodVisitor.java @@ -68,7 +68,6 @@ protected BaseMethodVisitor(MethodVisitor delegate, } public enum LocalsObject { - SUBMISSION_EXECUTION_HANDLER("submissionExecutionHandler", Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_TYPE.getDescriptor()), 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()); @@ -96,38 +95,23 @@ public void visitLocalVariable(BaseMethodVisitor bmv, Label start, Label end) { protected abstract int getLocalsIndex(LocalsObject localsObject); - protected void injectSetupCode(Label submissionExecutionHandlerVarLabel, Label methodHeaderVarLabel) { - // create SubmissionExecutionHandler$Internal instance and store in locals array - delegate.visitTypeInsn(NEW, Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_TYPE.getInternalName()); - delegate.visitInsn(DUP); - Constants.SUBMISSION_EXECUTION_HANDLER_GET_INSTANCE.toMethodInsn(delegate, false); - Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_CONSTRUCTOR.toMethodInsn(delegate, false); - delegate.visitVarInsn(ASTORE, getLocalsIndex(LocalsObject.SUBMISSION_EXECUTION_HANDLER)); - delegate.visitLabel(submissionExecutionHandlerVarLabel); - + 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, - 2, - new Object[] {Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_TYPE.getInternalName(), computedMethodHeader.getType().getInternalName()}, - 0, - null); - fullFrameLocals.add(Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_TYPE.getInternalName()); + delegate.visitFrame(F_APPEND, 1, new Object[] {computedMethodHeader.getType().getInternalName()}, 0, null); fullFrameLocals.add(computedMethodHeader.getType().getInternalName()); } protected void injectInvocationLoggingCode(Label nextLabel) { // check if invocation should be logged - delegate.visitVarInsn(ALOAD, getLocalsIndex(LocalsObject.SUBMISSION_EXECUTION_HANDLER)); 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.SUBMISSION_EXECUTION_HANDLER)); delegate.visitVarInsn(ALOAD, getLocalsIndex(LocalsObject.METHOD_HEADER)); injectInvocation(Type.getArgumentTypes(computedMethodHeader.descriptor())); Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_ADD_INVOCATION.toMethodInsn(delegate, false); @@ -140,13 +124,11 @@ protected void injectSubstitutionCode(Label substitutionCheckLabel, Label nextLa // check if substitution exists for this method delegate.visitFrame(F_SAME, 0, null, 0, null); delegate.visitLabel(substitutionCheckLabel); - delegate.visitVarInsn(ALOAD, getLocalsIndex(LocalsObject.SUBMISSION_EXECUTION_HANDLER)); 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.SUBMISSION_EXECUTION_HANDLER)); delegate.visitVarInsn(ALOAD, getLocalsIndex(LocalsObject.METHOD_HEADER)); Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_GET_SUBSTITUTION.toMethodInsn(delegate, false); delegate.visitVarInsn(ASTORE, getLocalsIndex(LocalsObject.METHOD_SUBSTITUTION)); @@ -252,14 +234,12 @@ protected void injectSubstitutionCode(Label substitutionCheckLabel, Label nextLa protected void injectDelegationCode(MethodNode solutionMethodNode, Label delegationCheckLabel, Label submissionCodeLabel, - Label submissionExecutionHandlerVarLabel, 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.SUBMISSION_EXECUTION_HANDLER)); 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 @@ -269,7 +249,6 @@ protected void injectDelegationCode(MethodNode solutionMethodNode, fullFrameLocals.removeLast(); fullFrameLocals.removeLast(); delegate.visitLabel(delegationCodeLabel); - LocalsObject.SUBMISSION_EXECUTION_HANDLER.visitLocalVariable(this, submissionExecutionHandlerVarLabel, delegationCodeLabel); LocalsObject.METHOD_HEADER.visitLocalVariable(this, methodHeaderVarLabel, delegationCodeLabel); solutionMethodNode.accept(delegate); @@ -277,14 +256,11 @@ protected void injectDelegationCode(MethodNode solutionMethodNode, delegate.visitLabel(submissionCodeLabel); } - protected void injectNoDelegationCode(Label submissionCodeLabel, - Label submissionExecutionHandlerVarLabel, - Label methodHeaderVarLabel) { + protected void injectNoDelegationCode(Label submissionCodeLabel, Label methodHeaderVarLabel) { fullFrameLocals.removeLast(); fullFrameLocals.removeLast(); delegate.visitFrame(F_FULL, fullFrameLocals.size(), fullFrameLocals.toArray(), 0, new Object[0]); delegate.visitLabel(submissionCodeLabel); - LocalsObject.SUBMISSION_EXECUTION_HANDLER.visitLocalVariable(this, submissionExecutionHandlerVarLabel, submissionCodeLabel); LocalsObject.METHOD_HEADER.visitLocalVariable(this, methodHeaderVarLabel, submissionCodeLabel); } 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 index 1f4c0352..f1e0da21 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/methods/MissingMethodVisitor.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/methods/MissingMethodVisitor.java @@ -21,10 +21,9 @@ public MissingMethodVisitor(MethodVisitor delegate, MethodHeader methodHeader) { super(delegate, transformationContext, classInfo, methodHeader, methodHeader); - localsIndexes.put(LocalsObject.SUBMISSION_EXECUTION_HANDLER, nextLocalsIndex); - localsIndexes.put(LocalsObject.METHOD_HEADER, nextLocalsIndex + 1); - localsIndexes.put(LocalsObject.METHOD_SUBSTITUTION, nextLocalsIndex + 2); - localsIndexes.put(LocalsObject.CONSTRUCTOR_INVOCATION, nextLocalsIndex + 3); + localsIndexes.put(LocalsObject.METHOD_HEADER, nextLocalsIndex); + localsIndexes.put(LocalsObject.METHOD_SUBSTITUTION, nextLocalsIndex + 1); + localsIndexes.put(LocalsObject.CONSTRUCTOR_INVOCATION, nextLocalsIndex + 2); } @Override @@ -34,7 +33,6 @@ protected int getLocalsIndex(LocalsObject localsObject) { @Override public void visitCode() { - Label submissionExecutionHandlerVarLabel = new Label(); Label methodHeaderVarLabel = new Label(); Label substitutionCheckLabel = new Label(); Label delegationCheckLabel = new Label(); @@ -42,7 +40,7 @@ public void visitCode() { Label submissionCodeLabel = new Label(); // Setup - injectSetupCode(submissionExecutionHandlerVarLabel, methodHeaderVarLabel); + injectSetupCode(methodHeaderVarLabel); // Invocation logging injectInvocationLoggingCode(substitutionCheckLabel); @@ -54,7 +52,6 @@ public void visitCode() { // 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.SUBMISSION_EXECUTION_HANDLER)); 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 @@ -64,7 +61,6 @@ public void visitCode() { fullFrameLocals.removeLast(); fullFrameLocals.removeLast(); delegate.visitLabel(delegationCodeLabel); - LocalsObject.SUBMISSION_EXECUTION_HANDLER.visitLocalVariable(this, submissionExecutionHandlerVarLabel, delegationCodeLabel); LocalsObject.METHOD_HEADER.visitLocalVariable(this, methodHeaderVarLabel, delegationCodeLabel); IncompatibleHeaderException.replicateInBytecode(delegate, true, "Method has incorrect return or parameter types", computedMethodHeader, null); 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 index dd190c1a..e901f9ce 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/methods/SubmissionMethodVisitor.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/methods/SubmissionMethodVisitor.java @@ -33,10 +33,9 @@ public SubmissionMethodVisitor(MethodVisitor delegate, MethodHeader computedMethodHeader) { super(delegate, transformationContext, submissionClassInfo, originalMethodHeader, computedMethodHeader); - localsIndexes.put(LocalsObject.SUBMISSION_EXECUTION_HANDLER, nextLocalsIndex); - localsIndexes.put(LocalsObject.METHOD_HEADER, nextLocalsIndex + 1); - localsIndexes.put(LocalsObject.METHOD_SUBSTITUTION, nextLocalsIndex + 2); - localsIndexes.put(LocalsObject.CONSTRUCTOR_INVOCATION, nextLocalsIndex + 3); + localsIndexes.put(LocalsObject.METHOD_HEADER, nextLocalsIndex); + localsIndexes.put(LocalsObject.METHOD_SUBSTITUTION, nextLocalsIndex + 1); + localsIndexes.put(LocalsObject.CONSTRUCTOR_INVOCATION, nextLocalsIndex + 2); } @Override @@ -48,14 +47,13 @@ protected int getLocalsIndex(LocalsObject localsObject) { public void visitCode() { Optional solutionMethodNode = ((SubmissionClassInfo) classInfo).getSolutionClass() .map(solutionClassNode -> solutionClassNode.getMethods().get(computedMethodHeader)); - Label submissionExecutionHandlerVarLabel = new Label(); Label methodHeaderVarLabel = new Label(); Label substitutionCheckLabel = new Label(); Label delegationCheckLabel = new Label(); Label submissionCodeLabel = new Label(); // Setup - injectSetupCode(submissionExecutionHandlerVarLabel, methodHeaderVarLabel); + injectSetupCode(methodHeaderVarLabel); // Invocation logging injectInvocationLoggingCode(substitutionCheckLabel); @@ -66,13 +64,9 @@ public void visitCode() { // Method delegation // if no solution method is present, skip delegation if (solutionMethodNode.isPresent()) { - injectDelegationCode(solutionMethodNode.get(), - delegationCheckLabel, - submissionCodeLabel, - submissionExecutionHandlerVarLabel, - methodHeaderVarLabel); + injectDelegationCode(solutionMethodNode.get(), delegationCheckLabel, submissionCodeLabel, methodHeaderVarLabel); } else { - injectNoDelegationCode(submissionCodeLabel, submissionExecutionHandlerVarLabel, methodHeaderVarLabel); + injectNoDelegationCode(submissionCodeLabel, methodHeaderVarLabel); } if (headerMismatch) { 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 index 54fb6cca..d2d07867 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Constants.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Constants.java @@ -28,8 +28,6 @@ public final class Constants { 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); - public static final Type SUBMISSION_EXECUTION_HANDLER_TYPE = Type.getType(SubmissionExecutionHandler.class); - public static final Type SUBMISSION_EXECUTION_HANDLER_INTERNAL_TYPE = Type.getType(SubmissionExecutionHandler.Internal.class); // Methods used in bytecode @@ -52,8 +50,6 @@ public final class Constants { "()L%s<%s>;".formatted(SET_TYPE.getInternalName(), METHOD_HEADER_TYPE.getDescriptor()), null); - public static final MethodHeader SUBMISSION_EXECUTION_HANDLER_GET_INSTANCE; - public static final MethodHeader SUBMISSION_EXECUTION_HANDLER_INTERNAL_CONSTRUCTOR; 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; @@ -72,8 +68,6 @@ public final class Constants { static { try { - SUBMISSION_EXECUTION_HANDLER_GET_INSTANCE = new MethodHeader(SubmissionExecutionHandler.class.getDeclaredMethod("getInstance")); - SUBMISSION_EXECUTION_HANDLER_INTERNAL_CONSTRUCTOR = new MethodHeader(SubmissionExecutionHandler.Internal.class.getDeclaredConstructor(SubmissionExecutionHandler.class)); 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)); 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 index 57d0123f..15dea5a8 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Invocation.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Invocation.java @@ -234,15 +234,13 @@ public Object callOriginalMethod(boolean delegate, Object... params) { invocationArgs = params; } - SubmissionExecutionHandler executionHandler = SubmissionExecutionHandler.getInstance(); - SubmissionExecutionHandler.Internal sehInternal = executionHandler.new Internal(); - MethodSubstitution methodSubstitution = sehInternal.getSubstitution(methodHeader); - executionHandler.disableMethodSubstitution(methodHeader); - boolean isDelegated = !sehInternal.useSubmissionImpl(methodHeader); + MethodSubstitution methodSubstitution = SubmissionExecutionHandler.Internal.getSubstitution(methodHeader); + SubmissionExecutionHandler.Substitution.disable(methodHeader); + boolean isDelegated = !SubmissionExecutionHandler.Internal.useSubmissionImpl(methodHeader); if (delegate) { - executionHandler.enableMethodDelegation(methodHeader); + SubmissionExecutionHandler.Delegation.enable(methodHeader); } else { - executionHandler.disableMethodDelegation(methodHeader); + SubmissionExecutionHandler.Delegation.disable(methodHeader); } try { @@ -258,11 +256,11 @@ public Object callOriginalMethod(boolean delegate, Object... params) { } catch (Throwable e) { throw new RuntimeException(e); } finally { - executionHandler.substituteMethod(methodHeader, methodSubstitution); + SubmissionExecutionHandler.Substitution.enable(methodHeader, methodSubstitution); if (isDelegated) { - executionHandler.enableMethodDelegation(methodHeader); + SubmissionExecutionHandler.Delegation.enable(methodHeader); } else { - executionHandler.disableMethodDelegation(methodHeader); + SubmissionExecutionHandler.Delegation.disable(methodHeader); } } } From 528db93a1f33eb25188ba56660b4d3992de9fb77 Mon Sep 17 00:00:00 2001 From: Daniel Mangold Date: Thu, 5 Dec 2024 11:14:07 +0100 Subject: [PATCH 34/48] Add some utility method to the header classes --- .../algoutils/transform/util/ClassHeader.java | 14 ++++++++ .../algoutils/transform/util/FieldHeader.java | 20 +++++++++++ .../transform/util/MethodHeader.java | 34 ++++++++++++++++++- 3 files changed, 67 insertions(+), 1 deletion(-) diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/ClassHeader.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/ClassHeader.java index dc757d5a..f21bc3f2 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/ClassHeader.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/ClassHeader.java @@ -58,6 +58,20 @@ public void visitClass(ClassVisitor delegate, int version, String... additionalI delegate.visit(version, this.access, this.name, this.signature, this.superName, interfaces); } + /** + * 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; diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/FieldHeader.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/FieldHeader.java index 291bf137..44747fd5 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/FieldHeader.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/FieldHeader.java @@ -23,6 +23,11 @@ */ 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(), @@ -59,6 +64,21 @@ public FieldVisitor toFieldVisitor(ClassVisitor delegate, Object value) { return delegate.visitField(access & ~Opcodes.ACC_FINAL, name, descriptor, signature, value); } + /** + * 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 diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/MethodHeader.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/MethodHeader.java index 3e84390c..71363606 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/MethodHeader.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/MethodHeader.java @@ -30,7 +30,7 @@ public record MethodHeader(String owner, int access, String name, String descriptor, String signature, String[] exceptions) implements Header { /** - * Constructs a new method header with reduced information. + * 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()}. * @@ -121,6 +121,38 @@ public int getOpcode() { } } + /** + * 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 From b350bac07f072771b67e4a557c569f6b9910c1fc Mon Sep 17 00:00:00 2001 From: Daniel Mangold Date: Thu, 5 Dec 2024 15:20:10 +0100 Subject: [PATCH 35/48] Improve similarity matching for classes and their members --- .../SolutionMergingClassTransformer.java | 2 +- .../classes/SubmissionClassInfo.java | 12 +- .../transform/util/SimilarityMapper.java | 119 ------------------ .../transform/util/TransformationContext.java | 11 +- .../util/matching/ClassSimilarityMapper.java | 29 +++++ .../util/matching/FieldSimilarityMapper.java | 27 ++++ .../util/matching/MethodSimilarityMapper.java | 33 +++++ .../util/matching/SimilarityMapper.java | 59 +++++++++ 8 files changed, 160 insertions(+), 132 deletions(-) delete mode 100644 tutor/src/main/java/org/tudalgo/algoutils/transform/util/SimilarityMapper.java create mode 100644 tutor/src/main/java/org/tudalgo/algoutils/transform/util/matching/ClassSimilarityMapper.java create mode 100644 tutor/src/main/java/org/tudalgo/algoutils/transform/util/matching/FieldSimilarityMapper.java create mode 100644 tutor/src/main/java/org/tudalgo/algoutils/transform/util/matching/MethodSimilarityMapper.java create mode 100644 tutor/src/main/java/org/tudalgo/algoutils/transform/util/matching/SimilarityMapper.java diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionMergingClassTransformer.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionMergingClassTransformer.java index 211e7e15..b3657270 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionMergingClassTransformer.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionMergingClassTransformer.java @@ -205,7 +205,7 @@ public Builder(String projectPrefix) { @SuppressWarnings("unchecked") public Builder addSolutionClass(String solutionClassName, String... altNames) { - ((Map>) configuration.get(Config.SOLUTION_CLASSES)).put( + ((Map>) configuration.get(Config.SOLUTION_CLASSES)).put( solutionClassName.replace('.', '/'), Arrays.stream(altNames).map(s -> s.replace('.', '/')).toList() ); 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 index bda2ef21..242339b0 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/SubmissionClassInfo.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/SubmissionClassInfo.java @@ -2,6 +2,8 @@ import org.tudalgo.algoutils.transform.util.*; import org.objectweb.asm.*; +import org.tudalgo.algoutils.transform.util.matching.FieldSimilarityMapper; +import org.tudalgo.algoutils.transform.util.matching.MethodSimilarityMapper; import java.lang.reflect.Modifier; import java.util.*; @@ -148,11 +150,10 @@ public Optional getSolutionClass() { * This method must be called once to use this {@link SubmissionClassInfo} instance. */ public void resolveMembers() { - SimilarityMapper fieldsSimilarityMapper = new SimilarityMapper<>( + FieldSimilarityMapper fieldsSimilarityMapper = new FieldSimilarityMapper( fields.keySet(), getSolutionClass().map(solutionClass -> solutionClass.getFields().keySet()).orElseGet(Collections::emptySet), - transformationContext.getSimilarity(), - FieldHeader::name + transformationContext ); for (FieldHeader submissionFieldHeader : fields.keySet()) { Supplier fallbackFieldHeader = () -> new FieldHeader(computedClassHeader.name(), @@ -178,11 +179,10 @@ public void resolveMembers() { fields.put(submissionFieldHeader, solutionFieldHeader); } - SimilarityMapper methodsSimilarityMapper = new SimilarityMapper<>( + MethodSimilarityMapper methodsSimilarityMapper = new MethodSimilarityMapper( methods.keySet(), getSolutionClass().map(solutionClass -> solutionClass.getMethods().keySet()).orElseGet(Collections::emptySet), - transformationContext.getSimilarity(), - methodHeader -> methodHeader.name() + methodHeader.descriptor() + transformationContext ); for (MethodHeader submissionMethodHeader : methods.keySet()) { String submissionMethodName = submissionMethodHeader.name(); diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/SimilarityMapper.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/SimilarityMapper.java deleted file mode 100644 index d44e5759..00000000 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/SimilarityMapper.java +++ /dev/null @@ -1,119 +0,0 @@ -package org.tudalgo.algoutils.transform.util; - -import kotlin.Pair; -import org.tudalgo.algoutils.tutor.general.match.MatchingUtils; - -import java.util.*; -import java.util.function.Function; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -/** - * Computes the similarity for the cross product of two given collections. - * This creates a mapping of values in the first collection to the best match - * in the second collection, if any. - * - * @param the type of the collection's elements - */ -public class SimilarityMapper { - - private final List rowMapping; - private final List columnMapping; - private final double[][] similarityMatrix; - private final Map> bestMatches = new HashMap<>(); - - /** - * Creates a new {@link SimilarityMapper} instance, allowing columns to have aliases. - * - * @param from the values to map from (rows) - * @param to the values to map to (columns), 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 - * @param mappingFunction a function for mapping the collection's elements to strings - */ - public SimilarityMapper(Collection from, - Map> to, - double similarityThreshold, - Function mappingFunction) { - this.rowMapping = new ArrayList<>(from); - this.columnMapping = new ArrayList<>(to.keySet()); - this.similarityMatrix = new double[from.size()][to.size()]; - computeSimilarity(to, similarityThreshold, mappingFunction); - } - - /** - * Creates a new {@link SimilarityMapper} instance. - * - * @param from the values to map from (rows) - * @param to the values to map to (columns) - * @param similarityThreshold the minimum similarity two values need to have to be considered a match - * @param mappingFunction a function for mapping the collection's elements to strings - */ - public SimilarityMapper(Collection from, - Collection to, - double similarityThreshold, - Function mappingFunction) { - this.rowMapping = new ArrayList<>(from); - this.columnMapping = new ArrayList<>(to); - this.similarityMatrix = new double[from.size()][to.size()]; - computeSimilarity(to.stream().collect(Collectors.toMap(Function.identity(), t -> Collections.emptyList())), - similarityThreshold, - mappingFunction); - } - - /** - * 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)).map(Pair::getFirst); - } - - /** - * Computes the similarity for each entry in the cross product of the two input collections. - * Also extracts the best matches and stores them in {@link #bestMatches} for easy access. - * - * @param to a mapping of columns to their aliases - * @param similarityThreshold the minimum similarity two values need to have to be considered a match - * @param mappingFunction a function for mapping the collection's elements to strings - */ - private void computeSimilarity(Map> to, - double similarityThreshold, - Function mappingFunction) { - for (int i = 0; i < similarityMatrix.length; i++) { - String row = mappingFunction.apply(rowMapping.get(i)); - int bestMatchIndex = -1; - double bestSimilarity = similarityThreshold; - for (int j = 0; j < similarityMatrix[i].length; j++) { - similarityMatrix[i][j] = Stream.concat(Stream.of(columnMapping.get(j)), to.get(columnMapping.get(j)).stream()) - .map(mappingFunction) - .mapToDouble(value -> MatchingUtils.similarity(row, value)) - .max() - .orElseThrow(); - if (similarityMatrix[i][j] >= bestSimilarity) { - bestMatchIndex = j; - bestSimilarity = similarityMatrix[i][j]; - } - } - if (bestMatchIndex >= 0) { - Pair pair = new Pair<>(columnMapping.get(bestMatchIndex), bestSimilarity); - bestMatches.merge(rowMapping.get(i), pair, (oldPair, newPair) -> - newPair.getSecond() > oldPair.getSecond() ? newPair : oldPair); - } - } - - // find and remove duplicate mappings - Map> reverseMapping = new HashMap<>(); // column => rows - bestMatches.forEach((t, pair) -> reverseMapping.computeIfAbsent(pair.getFirst(), k -> new Stack<>()).push(t)); - reverseMapping.entrySet() - .stream() - .filter(entry -> entry.getValue().size() > 1) - .forEach(entry -> { - Stack stack = entry.getValue(); - stack.sort(Comparator.comparingDouble(t -> bestMatches.get(t).getSecond())); - stack.pop(); // exclude the best match from removal - stack.forEach(bestMatches::remove); // remove the rest - }); - } -} 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 index feb52128..47809d9a 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationContext.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationContext.java @@ -5,11 +5,11 @@ 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.matching.ClassSimilarityMapper; import java.io.IOException; import java.io.InputStream; import java.util.*; -import java.util.function.Function; /** * A record for holding context information for the transformation process. @@ -25,7 +25,7 @@ public final class TransformationContext { private ClassLoader submissionClassLoader; private Set submissionClassNames; - private SimilarityMapper classSimilarityMapper; + private ClassSimilarityMapper classSimilarityMapper; /** * Constructs a new {@link TransformationContext}. @@ -117,10 +117,9 @@ public Set getVisitedClasses() { */ @SuppressWarnings("unchecked") public void computeClassesSimilarity() { - classSimilarityMapper = new SimilarityMapper<>(submissionClassNames, - (Map>) configuration.get(SolutionMergingClassTransformer.Config.SOLUTION_CLASSES), - getSimilarity(), - Function.identity()); + classSimilarityMapper = new ClassSimilarityMapper(submissionClassNames, + (Map>) configuration.get(SolutionMergingClassTransformer.Config.SOLUTION_CLASSES), + getSimilarity()); } // Submission classes 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..1be9c1ad --- /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.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..f8133a33 --- /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.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..abb69ad5 --- /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[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)); + } +} From f8f6e3360e8b1f937c16c5d1d7e3b14199ea701b Mon Sep 17 00:00:00 2001 From: Daniel Mangold Date: Thu, 5 Dec 2024 17:09:31 +0100 Subject: [PATCH 36/48] Re-add accidentally deleted methods --- .../transform/SubmissionExecutionHandler.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionExecutionHandler.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionExecutionHandler.java index 7a1a91b5..93a3d42e 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionExecutionHandler.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionExecutionHandler.java @@ -185,6 +185,29 @@ public static void disable(MethodHeader methodHeader) { 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 { From 3a836d5f7321b907a8d19da49ee0a65d52129829 Mon Sep 17 00:00:00 2001 From: Daniel Mangold Date: Thu, 5 Dec 2024 19:03:51 +0100 Subject: [PATCH 37/48] Small fixes --- .../algoutils/transform/methods/BaseMethodVisitor.java | 4 +--- .../algoutils/transform/methods/MissingMethodVisitor.java | 3 +-- 2 files changed, 2 insertions(+), 5 deletions(-) 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 index c2cfebac..27d8a881 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/methods/BaseMethodVisitor.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/methods/BaseMethodVisitor.java @@ -245,8 +245,7 @@ protected void injectDelegationCode(MethodNode solutionMethodNode, delegate.visitJumpInsn(IFNE, submissionCodeLabel); // jump to label if useSubmissionImpl(...) == true // replay instructions from solution - delegate.visitFrame(F_CHOP, 2, null, 0, null); - fullFrameLocals.removeLast(); + delegate.visitFrame(F_CHOP, 1, null, 0, null); fullFrameLocals.removeLast(); delegate.visitLabel(delegationCodeLabel); LocalsObject.METHOD_HEADER.visitLocalVariable(this, methodHeaderVarLabel, delegationCodeLabel); @@ -257,7 +256,6 @@ protected void injectDelegationCode(MethodNode solutionMethodNode, } protected void injectNoDelegationCode(Label submissionCodeLabel, Label methodHeaderVarLabel) { - fullFrameLocals.removeLast(); fullFrameLocals.removeLast(); delegate.visitFrame(F_FULL, fullFrameLocals.size(), fullFrameLocals.toArray(), 0, new Object[0]); delegate.visitLabel(submissionCodeLabel); 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 index f1e0da21..27d7dc73 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/methods/MissingMethodVisitor.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/methods/MissingMethodVisitor.java @@ -57,8 +57,7 @@ public void visitCode() { delegate.visitJumpInsn(IFEQ, submissionCodeLabel); // jump to label if useSubmissionImpl(...) == false // replay instructions from solution - delegate.visitFrame(F_CHOP, 2, null, 0, null); - fullFrameLocals.removeLast(); + delegate.visitFrame(F_CHOP, 1, null, 0, null); fullFrameLocals.removeLast(); delegate.visitLabel(delegationCodeLabel); LocalsObject.METHOD_HEADER.visitLocalVariable(this, methodHeaderVarLabel, delegationCodeLabel); From 306fb7f096ff2e9d6062f0d7013f5f2fc5a8659b Mon Sep 17 00:00:00 2001 From: Daniel Mangold Date: Fri, 6 Dec 2024 18:56:01 +0100 Subject: [PATCH 38/48] Some more fixes --- .../classes/SubmissionClassVisitor.java | 37 +++++++++++-------- .../util/matching/SimilarityMapper.java | 2 +- 2 files changed, 22 insertions(+), 17 deletions(-) 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 index 9a332240..2e031d78 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/SubmissionClassVisitor.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/SubmissionClassVisitor.java @@ -10,6 +10,7 @@ import org.objectweb.asm.*; import java.util.*; +import java.util.function.Predicate; import java.util.stream.Collectors; import static org.objectweb.asm.Opcodes.*; @@ -136,10 +137,16 @@ public SubmissionClassVisitor(ClassVisitor classVisitor, */ @Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { - submissionClassInfo.getSolutionClass() + ClassHeader classHeader = submissionClassInfo.getSolutionClass() .map(SolutionClassNode::getClassHeader) - .orElse(originalClassHeader) - .visitClass(getDelegate(), version); + .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); } /** @@ -149,17 +156,16 @@ public void visit(int version, int access, String name, String signature, String 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())) { - visitedFields.add(fieldHeader); - return fieldHeader.toFieldVisitor(getDelegate(), value); - } else { - return super.visitField(TransformationUtils.transformAccess(access), + 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, - value); + signature); } + visitedFields.add(fieldHeader); + return fieldHeader.toFieldVisitor(getDelegate(), value); } /** @@ -184,17 +190,16 @@ public MethodVisitor visitMethod(int access, String name, String descriptor, Str 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())) { - visitedMethods.add(computedMethodHeader); - } else { + if (!TransformationUtils.contextIsCompatible(access, computedMethodHeader.access()) || + !transformationContext.descriptorIsCompatible(descriptor, computedMethodHeader.descriptor())) { computedMethodHeader = new MethodHeader(computedMethodHeader.owner(), - access, + TransformationUtils.transformAccess(access), name + "$submission", transformationContext.toComputedDescriptor(descriptor), signature, exceptions); } + visitedMethods.add(computedMethodHeader); return new SubmissionMethodVisitor(computedMethodHeader.toMethodVisitor(getDelegate()), transformationContext, submissionClassInfo, 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 index abb69ad5..158bbd20 100644 --- 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 @@ -41,7 +41,7 @@ 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[rows.indexOf(row)][columns.indexOf(col)])); + 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 }); From aaf0971f1d4a8c218178e2200b1a51569e7fe7f9 Mon Sep 17 00:00:00 2001 From: Daniel Mangold Date: Fri, 6 Dec 2024 19:09:13 +0100 Subject: [PATCH 39/48] Move headers classes to own package --- .../transform/SolutionMergingClassTransformer.java | 2 +- .../algoutils/transform/SubmissionExecutionHandler.java | 3 +++ .../org/tudalgo/algoutils/transform/classes/ClassInfo.java | 6 +++--- .../algoutils/transform/classes/MissingClassInfo.java | 6 +++--- .../algoutils/transform/classes/MissingClassVisitor.java | 2 +- .../algoutils/transform/classes/SolutionClassNode.java | 3 +++ .../algoutils/transform/classes/SubmissionClassInfo.java | 3 +++ .../algoutils/transform/classes/SubmissionClassVisitor.java | 3 +++ .../algoutils/transform/methods/BaseMethodVisitor.java | 2 ++ .../algoutils/transform/methods/LambdaMethodVisitor.java | 2 +- .../algoutils/transform/methods/MissingMethodVisitor.java | 1 + .../transform/methods/SubmissionMethodVisitor.java | 1 + .../org/tudalgo/algoutils/transform/util/Constants.java | 4 ++++ .../transform/util/ForceSignatureAnnotationProcessor.java | 2 ++ .../transform/util/IncompatibleHeaderException.java | 1 + .../org/tudalgo/algoutils/transform/util/Invocation.java | 1 + .../algoutils/transform/util/TransformationContext.java | 1 + .../algoutils/transform/util/{ => headers}/ClassHeader.java | 4 +++- .../algoutils/transform/util/{ => headers}/FieldHeader.java | 4 +++- .../algoutils/transform/util/{ => headers}/Header.java | 4 +++- .../transform/util/{ => headers}/MethodHeader.java | 4 +++- .../transform/util/matching/FieldSimilarityMapper.java | 2 +- .../transform/util/matching/MethodSimilarityMapper.java | 2 +- 23 files changed, 48 insertions(+), 15 deletions(-) rename tutor/src/main/java/org/tudalgo/algoutils/transform/util/{ => headers}/ClassHeader.java (96%) rename tutor/src/main/java/org/tudalgo/algoutils/transform/util/{ => headers}/FieldHeader.java (95%) rename tutor/src/main/java/org/tudalgo/algoutils/transform/util/{ => headers}/Header.java (92%) rename tutor/src/main/java/org/tudalgo/algoutils/transform/util/{ => headers}/MethodHeader.java (97%) diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionMergingClassTransformer.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionMergingClassTransformer.java index b3657270..7d8b2464 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionMergingClassTransformer.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionMergingClassTransformer.java @@ -7,7 +7,7 @@ import org.tudalgo.algoutils.transform.classes.SolutionClassNode; import org.tudalgo.algoutils.transform.classes.SubmissionClassInfo; import org.tudalgo.algoutils.transform.classes.SubmissionClassVisitor; -import org.tudalgo.algoutils.transform.util.MethodHeader; +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; diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionExecutionHandler.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionExecutionHandler.java index 93a3d42e..f102c2f7 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionExecutionHandler.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionExecutionHandler.java @@ -2,6 +2,9 @@ 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; 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 index 964aefd5..eedcb8de 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/ClassInfo.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/ClassInfo.java @@ -3,9 +3,9 @@ import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; -import org.tudalgo.algoutils.transform.util.ClassHeader; -import org.tudalgo.algoutils.transform.util.FieldHeader; -import org.tudalgo.algoutils.transform.util.MethodHeader; +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; 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 index 7b94d8d8..b7325c2b 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/MissingClassInfo.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/MissingClassInfo.java @@ -1,9 +1,9 @@ package org.tudalgo.algoutils.transform.classes; import org.objectweb.asm.ClassVisitor; -import org.tudalgo.algoutils.transform.util.ClassHeader; -import org.tudalgo.algoutils.transform.util.FieldHeader; -import org.tudalgo.algoutils.transform.util.MethodHeader; +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; 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 index 6b3f7b92..8e2c4857 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/MissingClassVisitor.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/MissingClassVisitor.java @@ -6,7 +6,7 @@ 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.MethodHeader; +import org.tudalgo.algoutils.transform.util.headers.MethodHeader; import org.tudalgo.algoutils.transform.util.TransformationContext; /** 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 index d5a63b65..5fe3e25d 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/SolutionClassNode.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/SolutionClassNode.java @@ -8,6 +8,9 @@ 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; 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 index 242339b0..48d6fce6 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/SubmissionClassInfo.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/SubmissionClassInfo.java @@ -2,6 +2,9 @@ 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; 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 index 2e031d78..a48abb3b 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/SubmissionClassVisitor.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/SubmissionClassVisitor.java @@ -8,6 +8,9 @@ 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.function.Predicate; 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 index 27d8a881..79f05b28 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/methods/BaseMethodVisitor.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/methods/BaseMethodVisitor.java @@ -5,6 +5,8 @@ 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.ArrayList; import java.util.Arrays; 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 index 61f1af50..09d86761 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/methods/LambdaMethodVisitor.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/methods/LambdaMethodVisitor.java @@ -2,7 +2,7 @@ import org.objectweb.asm.MethodVisitor; import org.tudalgo.algoutils.transform.classes.SubmissionClassInfo; -import org.tudalgo.algoutils.transform.util.MethodHeader; +import org.tudalgo.algoutils.transform.util.headers.MethodHeader; import org.tudalgo.algoutils.transform.util.TransformationContext; /** 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 index 27d7dc73..3348de09 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/methods/MissingMethodVisitor.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/methods/MissingMethodVisitor.java @@ -4,6 +4,7 @@ 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; 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 index e901f9ce..4b72982f 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/methods/SubmissionMethodVisitor.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/methods/SubmissionMethodVisitor.java @@ -5,6 +5,7 @@ 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.*; 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 index d2d07867..3939ecae 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Constants.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Constants.java @@ -3,6 +3,10 @@ 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.Set; 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 index e7298dbf..c6fb1ec8 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/ForceSignatureAnnotationProcessor.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/ForceSignatureAnnotationProcessor.java @@ -2,6 +2,8 @@ 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; 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 index 3006fab4..7042f5d8 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/IncompatibleHeaderException.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/IncompatibleHeaderException.java @@ -2,6 +2,7 @@ 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; 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 index 15dea5a8..2fa1081d 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Invocation.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Invocation.java @@ -2,6 +2,7 @@ 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; 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 index 47809d9a..36af6635 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationContext.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationContext.java @@ -5,6 +5,7 @@ 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; diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/ClassHeader.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/headers/ClassHeader.java similarity index 96% rename from tutor/src/main/java/org/tudalgo/algoutils/transform/util/ClassHeader.java rename to tutor/src/main/java/org/tudalgo/algoutils/transform/util/headers/ClassHeader.java index f21bc3f2..14585748 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/ClassHeader.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/headers/ClassHeader.java @@ -1,8 +1,10 @@ -package org.tudalgo.algoutils.transform.util; +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.Objects; diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/FieldHeader.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/headers/FieldHeader.java similarity index 95% rename from tutor/src/main/java/org/tudalgo/algoutils/transform/util/FieldHeader.java rename to tutor/src/main/java/org/tudalgo/algoutils/transform/util/headers/FieldHeader.java index 44747fd5..31bb3ca8 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/FieldHeader.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/headers/FieldHeader.java @@ -1,9 +1,11 @@ -package org.tudalgo.algoutils.transform.util; +package org.tudalgo.algoutils.transform.util.headers; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.FieldVisitor; 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.lang.reflect.Field; import java.util.Objects; diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Header.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/headers/Header.java similarity index 92% rename from tutor/src/main/java/org/tudalgo/algoutils/transform/util/Header.java rename to tutor/src/main/java/org/tudalgo/algoutils/transform/util/headers/Header.java index 1c6624bc..824d8088 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Header.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/headers/Header.java @@ -1,7 +1,9 @@ -package org.tudalgo.algoutils.transform.util; +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; diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/MethodHeader.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/headers/MethodHeader.java similarity index 97% rename from tutor/src/main/java/org/tudalgo/algoutils/transform/util/MethodHeader.java rename to tutor/src/main/java/org/tudalgo/algoutils/transform/util/headers/MethodHeader.java index 71363606..a3bd77ef 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/MethodHeader.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/headers/MethodHeader.java @@ -1,8 +1,10 @@ -package org.tudalgo.algoutils.transform.util; +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; 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 index 1be9c1ad..0e2b641f 100644 --- 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 @@ -1,6 +1,6 @@ package org.tudalgo.algoutils.transform.util.matching; -import org.tudalgo.algoutils.transform.util.FieldHeader; +import org.tudalgo.algoutils.transform.util.headers.FieldHeader; import org.tudalgo.algoutils.transform.util.TransformationContext; import org.tudalgo.algoutils.tutor.general.match.MatchingUtils; 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 index f8133a33..22086893 100644 --- 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 @@ -1,6 +1,6 @@ package org.tudalgo.algoutils.transform.util.matching; -import org.tudalgo.algoutils.transform.util.MethodHeader; +import org.tudalgo.algoutils.transform.util.headers.MethodHeader; import org.tudalgo.algoutils.transform.util.TransformationContext; import org.tudalgo.algoutils.tutor.general.match.MatchingUtils; From 276834d71b90ba93475cecf18a3cd66110eee712 Mon Sep 17 00:00:00 2001 From: Daniel Mangold Date: Fri, 6 Dec 2024 19:12:10 +0100 Subject: [PATCH 40/48] Rename getType() => getHeaderType() in header classes --- .../algoutils/transform/methods/BaseMethodVisitor.java | 4 ++-- .../algoutils/transform/util/headers/ClassHeader.java | 2 +- .../algoutils/transform/util/headers/FieldHeader.java | 2 +- .../tudalgo/algoutils/transform/util/headers/Header.java | 6 +++--- .../algoutils/transform/util/headers/MethodHeader.java | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) 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 index 79f05b28..2da48d1e 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/methods/BaseMethodVisitor.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/methods/BaseMethodVisitor.java @@ -103,8 +103,8 @@ protected void injectSetupCode(Label methodHeaderVarLabel) { delegate.visitVarInsn(ASTORE, getLocalsIndex(LocalsObject.METHOD_HEADER)); delegate.visitLabel(methodHeaderVarLabel); - delegate.visitFrame(F_APPEND, 1, new Object[] {computedMethodHeader.getType().getInternalName()}, 0, null); - fullFrameLocals.add(computedMethodHeader.getType().getInternalName()); + delegate.visitFrame(F_APPEND, 1, new Object[] {computedMethodHeader.getHeaderType().getInternalName()}, 0, null); + fullFrameLocals.add(computedMethodHeader.getHeaderType().getInternalName()); } protected void injectInvocationLoggingCode(Label nextLabel) { 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 index 14585748..70d5156f 100644 --- 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 @@ -25,7 +25,7 @@ public record ClassHeader(int access, String name, String signature, String superName, String[] interfaces) implements Header { @Override - public Type getType() { + public Type getHeaderType() { return Constants.CLASS_HEADER_TYPE; } 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 index 31bb3ca8..2eefd2f5 100644 --- 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 @@ -39,7 +39,7 @@ public FieldHeader(Field field) { } @Override - public Type getType() { + public Type getHeaderType() { return Constants.FIELD_HEADER_TYPE; } 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 index 824d8088..e917e53d 100644 --- 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 @@ -22,7 +22,7 @@ public sealed interface Header permits ClassHeader, FieldHeader, MethodHeader { * * @return the type for this header */ - Type getType(); + Type getHeaderType(); HeaderRecordComponent[] getComponents(); @@ -35,11 +35,11 @@ public sealed interface Header permits ClassHeader, FieldHeader, MethodHeader { * @return the maximum stack size used during the operation */ default int buildHeader(MethodVisitor mv) { - Type headerType = getType(); + Type headerType = getHeaderType(); HeaderRecordComponent[] components = getComponents(); int maxStack, stackSize; - mv.visitTypeInsn(NEW, getType().getInternalName()); + mv.visitTypeInsn(NEW, getHeaderType().getInternalName()); mv.visitInsn(DUP); maxStack = stackSize = 2; for (HeaderRecordComponent component : components) { 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 index a3bd77ef..96cff2d0 100644 --- 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 @@ -65,7 +65,7 @@ public MethodHeader(Executable executable) { } @Override - public Type getType() { + public Type getHeaderType() { return Constants.METHOD_HEADER_TYPE; } From b1f6fce6977b234b445e9e106d866d3124b1a490 Mon Sep 17 00:00:00 2001 From: Daniel Mangold Date: Fri, 6 Dec 2024 19:39:57 +0100 Subject: [PATCH 41/48] Add methods to get the class representations for the different types --- .../transform/util/TransformationUtils.java | 23 ++++++++++ .../transform/util/headers/ClassHeader.java | 46 +++++++++++++++++++ .../transform/util/headers/FieldHeader.java | 25 ++++++++++ .../transform/util/headers/Header.java | 2 + .../transform/util/headers/MethodHeader.java | 45 ++++++++++++++++++ 5 files changed, 141 insertions(+) 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 index 21a42f02..66e005ac 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationUtils.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationUtils.java @@ -267,4 +267,27 @@ public static String toHumanReadableType(Type type) { 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 index 70d5156f..92c2d05c 100644 --- 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 @@ -7,6 +7,7 @@ import org.tudalgo.algoutils.transform.util.TransformationUtils; import java.util.Arrays; +import java.util.List; import java.util.Objects; import java.util.stream.Collectors; @@ -60,6 +61,51 @@ public void visitClass(ClassVisitor delegate, int version, String... additionalI 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. * 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 index 2eefd2f5..91bfa191 100644 --- 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 @@ -66,6 +66,31 @@ public FieldVisitor toFieldVisitor(ClassVisitor delegate, Object value) { return delegate.visitField(access & ~Opcodes.ACC_FINAL, 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. * 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 index e917e53d..e1386193 100644 --- 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 @@ -26,6 +26,8 @@ public sealed interface Header permits ClassHeader, FieldHeader, MethodHeader { 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 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 index 96cff2d0..6ae2850f 100644 --- 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 @@ -9,6 +9,7 @@ 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; @@ -123,6 +124,50 @@ public int getOpcode() { } } + /** + * 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. * From df73861b923cbceee4559db1487e8ffa14b852a4 Mon Sep 17 00:00:00 2001 From: Daniel Mangold Date: Sun, 8 Dec 2024 09:08:04 +0100 Subject: [PATCH 42/48] Add method to check type category --- .../transform/util/TransformationUtils.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) 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 index 66e005ac..8c4b3a3b 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationUtils.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationUtils.java @@ -63,6 +63,17 @@ 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, @@ -75,7 +86,7 @@ public static boolean isLambdaMethod(int access, String name) { public static int getLocalsIndex(Type[] types, int index) { int localsIndex = 0; for (int i = 0; i < index; i++) { - localsIndex += (types[i].getSort() == Type.LONG || types[i].getSort() == Type.DOUBLE) ? 2 : 1; + localsIndex += isCategory2Type(types[i]) ? 2 : 1; } return localsIndex; } From a363114937f1d749785a6781fe8db9fc9c684495 Mon Sep 17 00:00:00 2001 From: Daniel Mangold Date: Sun, 8 Dec 2024 10:15:46 +0100 Subject: [PATCH 43/48] Add some more convenience methods --- .../transform/SubmissionExecutionHandler.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionExecutionHandler.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionExecutionHandler.java index f102c2f7..63497ebe 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionExecutionHandler.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionExecutionHandler.java @@ -1,5 +1,6 @@ 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; @@ -10,6 +11,7 @@ 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. @@ -110,6 +112,13 @@ public static Set getOriginalFieldHeaders(Class clazz) { } } + 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. * @@ -129,6 +138,23 @@ public static Set getOriginalMethodHeaders(Class clazz) { } } + 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); + } + /** * Resets all mechanisms. */ From 7574bb91daa1216ef3ab8914e331f57a761bb035 Mon Sep 17 00:00:00 2001 From: Daniel Mangold Date: Sun, 8 Dec 2024 10:18:23 +0100 Subject: [PATCH 44/48] Enable invocation injection to work with the stack --- .../transform/methods/BaseMethodVisitor.java | 66 +++++++++++++++---- 1 file changed, 54 insertions(+), 12 deletions(-) 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 index 2da48d1e..ea1fcb86 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/methods/BaseMethodVisitor.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/methods/BaseMethodVisitor.java @@ -8,9 +8,7 @@ import org.tudalgo.algoutils.transform.util.headers.FieldHeader; import org.tudalgo.algoutils.transform.util.headers.MethodHeader; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; +import java.util.*; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -115,7 +113,7 @@ protected void injectInvocationLoggingCode(Label nextLabel) { // intercept parameters delegate.visitVarInsn(ALOAD, getLocalsIndex(LocalsObject.METHOD_HEADER)); - injectInvocation(Type.getArgumentTypes(computedMethodHeader.descriptor())); + injectInvocation(Type.getArgumentTypes(computedMethodHeader.descriptor()), true); Constants.SUBMISSION_EXECUTION_HANDLER_INTERNAL_ADD_INVOCATION.toMethodInsn(delegate, false); } @@ -223,7 +221,7 @@ protected void injectSubstitutionCode(Label substitutionCheckLabel, Label nextLa } delegate.visitVarInsn(ALOAD, getLocalsIndex(LocalsObject.METHOD_SUBSTITUTION)); - injectInvocation(Type.getArgumentTypes(computedMethodHeader.descriptor())); + injectInvocation(Type.getArgumentTypes(computedMethodHeader.descriptor()), true); Constants.METHOD_SUBSTITUTION_EXECUTE.toMethodInsn(delegate, true); Type returnType = Type.getReturnType(computedMethodHeader.descriptor()); unboxType(delegate, returnType); @@ -268,8 +266,9 @@ protected void injectNoDelegationCode(Label submissionCodeLabel, Label methodHea * 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) { + protected void injectInvocation(Type[] argumentTypes, boolean useLocals) { Type threadType = Type.getType(Thread.class); Type stackTraceElementArrayType = Type.getType(StackTraceElement[].class); @@ -293,12 +292,55 @@ protected void injectInvocation(Type[] argumentTypes) { } else { Constants.INVOCATION_CONSTRUCTOR.toMethodInsn(delegate, false); } - // 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); + 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()); } } From e0c592940a15b455abf611a3024ea03583915eb6 Mon Sep 17 00:00:00 2001 From: Daniel Mangold Date: Sun, 8 Dec 2024 10:20:20 +0100 Subject: [PATCH 45/48] Fix small bug in SolutionClassNode --- .../algoutils/transform/classes/SolutionClassNode.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index 5fe3e25d..4567530d 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/SolutionClassNode.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/SolutionClassNode.java @@ -71,7 +71,7 @@ public void visit(int version, int access, String name, String signature, String @Override public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) { - FieldHeader fieldHeader = new FieldHeader(className, access, name, descriptor, signature); + 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; @@ -83,7 +83,7 @@ public MethodVisitor visitMethod(int access, String name, String descriptor, Str name += "$solution"; } MethodNode methodNode = getMethodNode(access, name, descriptor, signature, exceptions); - methods.put(new MethodHeader(className, access, name, descriptor, signature, exceptions), methodNode); + methods.put(new MethodHeader(className, TransformationUtils.transformAccess(access), name, descriptor, signature, exceptions), methodNode); return methodNode; } From 72c4a4912be8c70166eb651fae8a214f667d0501 Mon Sep 17 00:00:00 2001 From: Daniel Mangold Date: Sun, 8 Dec 2024 10:24:47 +0100 Subject: [PATCH 46/48] Save original enum constants to be retrieved later --- .../SolutionMergingClassTransformer.java | 13 +- .../transform/SubmissionExecutionHandler.java | 27 +++ .../classes/SubmissionClassVisitor.java | 12 +- .../classes/SubmissionEnumClassVisitor.java | 229 ++++++++++++++++++ .../algoutils/transform/util/Constants.java | 18 ++ .../transform/util/EnumConstant.java | 3 + 6 files changed, 291 insertions(+), 11 deletions(-) create mode 100644 tutor/src/main/java/org/tudalgo/algoutils/transform/classes/SubmissionEnumClassVisitor.java create mode 100644 tutor/src/main/java/org/tudalgo/algoutils/transform/util/EnumConstant.java diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionMergingClassTransformer.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionMergingClassTransformer.java index 7d8b2464..40b4a9fe 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionMergingClassTransformer.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionMergingClassTransformer.java @@ -1,12 +1,10 @@ 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.MissingClassVisitor; -import org.tudalgo.algoutils.transform.classes.SolutionClassNode; -import org.tudalgo.algoutils.transform.classes.SubmissionClassInfo; -import org.tudalgo.algoutils.transform.classes.SubmissionClassVisitor; +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; @@ -147,7 +145,12 @@ public void transform(ClassReader reader, ClassWriter writer) { // TODO: fix this for regular JUnit run transformationContext.setSubmissionClassLoader(null); } - reader.accept(new SubmissionClassVisitor(writer, transformationContext, reader.getClassName()), 0); + + 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 diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionExecutionHandler.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionExecutionHandler.java index 63497ebe..f5103d6e 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionExecutionHandler.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionExecutionHandler.java @@ -155,6 +155,33 @@ public static MethodHeader getOriginalMethodHeader(Class clazz, .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); + } + /** * Resets all mechanisms. */ 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 index a48abb3b..c7e240e7 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/SubmissionClassVisitor.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/SubmissionClassVisitor.java @@ -110,13 +110,13 @@ */ public class SubmissionClassVisitor extends ClassVisitor { - private final TransformationContext transformationContext; - private final SubmissionClassInfo submissionClassInfo; - private final ClassHeader originalClassHeader; - private final ClassHeader computedClassHeader; + protected final TransformationContext transformationContext; + protected final SubmissionClassInfo submissionClassInfo; + protected final ClassHeader originalClassHeader; + protected final ClassHeader computedClassHeader; - private final Set visitedFields = new HashSet<>(); - private final Set visitedMethods = new HashSet<>(); + protected final Set visitedFields = new HashSet<>(); + protected final Set visitedMethods = new HashSet<>(); /** * Constructs a new {@link SubmissionClassVisitor} instance. 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..9d5ab323 --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/SubmissionEnumClassVisitor.java @@ -0,0 +1,229 @@ +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) { + 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")))) { + super.visitFieldInsn(opcode, owner, name, descriptor); + } + } + + @Override + public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) { + 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/util/Constants.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Constants.java index 3939ecae..e1f26dd0 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Constants.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Constants.java @@ -8,8 +8,10 @@ import org.tudalgo.algoutils.transform.util.headers.Header; import org.tudalgo.algoutils.transform.util.headers.MethodHeader; +import java.util.List; import java.util.Set; +import static org.objectweb.asm.Opcodes.ACC_PRIVATE; import static org.objectweb.asm.Opcodes.ACC_PUBLIC; import static org.objectweb.asm.Opcodes.ACC_STATIC; @@ -22,17 +24,27 @@ public final class Constants { 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 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_ENUM_CONSTANTS = new FieldHeader(null, + ACC_PRIVATE | ACC_STATIC, + "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, @@ -53,6 +65,12 @@ public final class Constants { Type.getMethodDescriptor(SET_TYPE), "()L%s<%s>;".formatted(SET_TYPE.getInternalName(), METHOD_HEADER_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; 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) {} From 8c743ab31c7159e9966f5a0bb69db5ecc16a98c4 Mon Sep 17 00:00:00 2001 From: Daniel Mangold Date: Sun, 8 Dec 2024 14:32:44 +0100 Subject: [PATCH 47/48] Save original static field values to be retrieved later --- .../transform/SubmissionExecutionHandler.java | 17 +++ .../classes/SubmissionClassVisitor.java | 70 +++++++++++ .../transform/methods/ClassInitVisitor.java | 112 ++++++++++++++++++ .../algoutils/transform/util/Constants.java | 13 ++ 4 files changed, 212 insertions(+) create mode 100644 tutor/src/main/java/org/tudalgo/algoutils/transform/methods/ClassInitVisitor.java diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionExecutionHandler.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionExecutionHandler.java index f5103d6e..de4daced 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionExecutionHandler.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionExecutionHandler.java @@ -182,6 +182,23 @@ public static > EnumConstant getOriginalEnumConstant(Class .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. */ 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 index c7e240e7..dc084674 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/SubmissionClassVisitor.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/SubmissionClassVisitor.java @@ -1,8 +1,10 @@ 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; @@ -13,6 +15,7 @@ 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; @@ -118,6 +121,8 @@ public class SubmissionClassVisitor extends ClassVisitor { protected final Set visitedFields = new HashSet<>(); protected final Set visitedMethods = new HashSet<>(); + protected final Map> staticFieldValues = new HashMap<>(); + /** * Constructs a new {@link SubmissionClassVisitor} instance. * @@ -150,6 +155,9 @@ public void visit(int version, int access, String name, String signature, String .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); + } } /** @@ -167,6 +175,9 @@ public FieldVisitor visitField(int access, String name, String descriptor, Strin transformationContext.toComputedDescriptor(descriptor), signature); } + if (value != null) { + staticFieldValues.put(fieldHeader.name(), new Pair<>(Type.getType(fieldHeader.descriptor()), value)); + } visitedFields.add(fieldHeader); return fieldHeader.toFieldVisitor(getDelegate(), value); } @@ -189,6 +200,12 @@ public MethodVisitor visitMethod(int access, String name, String descriptor, Str 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); @@ -250,6 +267,23 @@ public void visitEnd() { 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(); @@ -336,4 +370,40 @@ private void injectMethodMetadata() { 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/methods/ClassInitVisitor.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/methods/ClassInitVisitor.java new file mode 100644 index 00000000..0c2c237d --- /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 ? DUP2_X1 : DUP_X1); + delegate.visitInsn(POP); + delegate.visitLdcInsn(computedFieldHeader.name()); + delegate.visitInsn(isCategory2Type ? DUP2_X1 : 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/util/Constants.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Constants.java index e1f26dd0..02a3564b 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Constants.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Constants.java @@ -9,6 +9,7 @@ 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_PRIVATE; @@ -25,6 +26,7 @@ public final class Constants { 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); @@ -39,6 +41,11 @@ public final class Constants { // Fields used in bytecode + public static final FieldHeader INJECTED_ORIGINAL_STATIC_FIELD_VALUES = new FieldHeader(null, + ACC_PRIVATE | ACC_STATIC, + "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_PRIVATE | ACC_STATIC, "originalEnumConstants$injected", @@ -65,6 +72,12 @@ public final class Constants { 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", From e95c3ba0469306ad56382742bd0956190e82d036 Mon Sep 17 00:00:00 2001 From: Daniel Mangold Date: Mon, 9 Dec 2024 12:29:48 +0100 Subject: [PATCH 48/48] More bug fixes --- .../transform/SolutionMergingClassTransformer.java | 7 +++++-- .../transform/classes/MissingClassVisitor.java | 4 ++++ .../algoutils/transform/classes/SolutionClassNode.java | 10 +++++----- .../transform/classes/SubmissionClassVisitor.java | 7 +++++++ .../transform/classes/SubmissionEnumClassVisitor.java | 10 ++++++++-- .../algoutils/transform/methods/BaseMethodVisitor.java | 5 ++++- .../algoutils/transform/methods/ClassInitVisitor.java | 4 ++-- .../tudalgo/algoutils/transform/util/Constants.java | 6 +++--- .../tudalgo/algoutils/transform/util/Invocation.java | 2 +- .../algoutils/transform/util/headers/FieldHeader.java | 3 +-- 10 files changed, 40 insertions(+), 18 deletions(-) diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionMergingClassTransformer.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionMergingClassTransformer.java index 40b4a9fe..d1d7b52d 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionMergingClassTransformer.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionMergingClassTransformer.java @@ -130,7 +130,10 @@ public void transform(ClassReader reader, ClassWriter writer) { .readValue(submissionClassLoader.getResourceAsStream("submission-info.json"), SubmissionInfo.class) .sourceSets() .stream() - .flatMap(sourceSet -> sourceSet.files().get("java").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); @@ -163,7 +166,7 @@ public Map injectClasses() { .stream() .filter(entry -> !visitedClasses.contains(entry.getKey())) .forEach(entry -> { - ClassWriter writer = new ClassWriter(0); + ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS); entry.getValue().accept(new MissingClassVisitor(writer, transformationContext, entry.getValue())); missingClasses.put(entry.getKey().replace('/', '.'), writer.toByteArray()); }); 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 index 8e2c4857..b20b25a6 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/MissingClassVisitor.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/MissingClassVisitor.java @@ -50,6 +50,10 @@ 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(); } 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 index 4567530d..4783c6c5 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/SolutionClassNode.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/SolutionClassNode.java @@ -1,10 +1,7 @@ package org.tudalgo.algoutils.transform.classes; +import org.objectweb.asm.*; import org.tudalgo.algoutils.transform.util.*; -import org.objectweb.asm.FieldVisitor; -import org.objectweb.asm.Handle; -import org.objectweb.asm.MethodVisitor; -import org.objectweb.asm.Opcodes; import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.FieldNode; import org.objectweb.asm.tree.MethodNode; @@ -100,7 +97,7 @@ public MethodVisitor visitMethod(int access, String name, String descriptor, Str * @return a new {@link MethodNode} */ private MethodNode getMethodNode(int access, String name, String descriptor, String signature, String[] exceptions) { - return new MethodNode(ASM9, TransformationUtils.transformAccess(access), name, descriptor, signature, 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); @@ -137,5 +134,8 @@ public void visitInvokeDynamicInsn(String name, String descriptor, Handle bootst .toArray()); } }; + + super.methods.add(methodNode); + return methodNode; } } 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 index dc084674..c2e42857 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/SubmissionClassVisitor.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/SubmissionClassVisitor.java @@ -175,6 +175,13 @@ public FieldVisitor visitField(int access, String name, String descriptor, Strin 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)); } 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 index 9d5ab323..2f080ec7 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/SubmissionEnumClassVisitor.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/classes/SubmissionEnumClassVisitor.java @@ -56,7 +56,7 @@ public void visit(int version, int access, String name, String signature, String @Override public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) { - if ((access & ACC_ENUM) != 0) { + if ((access & ACC_ENUM) != 0 && solutionClassNode != null) { enumConstants.add(name); return null; } else { @@ -117,13 +117,19 @@ public void visitCode() { @Override public void visitFieldInsn(int opcode, String owner, String name, String descriptor) { - if (!(owner.equals(originalClassHeader.name()) && (enumConstants.contains(name) || name.equals("$VALUES")))) { + 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(); 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 index ea1fcb86..a2495f51 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/methods/BaseMethodVisitor.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/methods/BaseMethodVisitor.java @@ -415,7 +415,10 @@ public void visitFieldInsn(int opcode, String owner, String name, String descrip computedFieldHeader.name(), computedFieldHeader.descriptor()); } else { // if incompatible - delegate.visitFieldInsn(opcode, computedFieldHeader.owner(), name + "$submission", computedFieldHeader.descriptor()); + delegate.visitFieldInsn(opcode, + computedFieldHeader.owner(), + name + "$submission", + transformationContext.toComputedDescriptor(descriptor)); } } } 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 index 0c2c237d..516121b5 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/methods/ClassInitVisitor.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/methods/ClassInitVisitor.java @@ -68,10 +68,10 @@ public void visitFieldInsn(int opcode, String owner, String name, String descrip computedMethodHeader.owner(), originalStaticFieldValuesHeader.name(), originalStaticFieldValuesHeader.descriptor()); - delegate.visitInsn(isCategory2Type ? DUP2_X1 : DUP_X1); + delegate.visitInsn(isCategory2Type ? DUP_X2 : DUP_X1); delegate.visitInsn(POP); delegate.visitLdcInsn(computedFieldHeader.name()); - delegate.visitInsn(isCategory2Type ? DUP2_X1 : DUP_X1); + delegate.visitInsn(isCategory2Type ? DUP_X2 : DUP_X1); delegate.visitInsn(POP); TransformationUtils.boxType(delegate, type); delegate.visitMethodInsn(INVOKEINTERFACE, 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 index 02a3564b..b4ab09cd 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Constants.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Constants.java @@ -12,9 +12,9 @@ import java.util.Map; import java.util.Set; -import static org.objectweb.asm.Opcodes.ACC_PRIVATE; 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 { @@ -42,12 +42,12 @@ public final class Constants { // Fields used in bytecode public static final FieldHeader INJECTED_ORIGINAL_STATIC_FIELD_VALUES = new FieldHeader(null, - ACC_PRIVATE | ACC_STATIC, + 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_PRIVATE | ACC_STATIC, + ACC_PUBLIC | ACC_STATIC | ACC_FINAL, "originalEnumConstants$injected", LIST_TYPE.getDescriptor(), "L%s<%s>;".formatted(LIST_TYPE.getInternalName(), ENUM_CONSTANT_TYPE.getDescriptor())); 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 index 2fa1081d..517319c6 100644 --- a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Invocation.java +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Invocation.java @@ -253,7 +253,7 @@ public Object callOriginalMethod(boolean delegate, Object... params) { case Opcodes.INVOKESTATIC -> lookup.findStatic(declaringClass, methodHeader.name(), methodType); default -> throw new IllegalArgumentException("Unsupported opcode: " + methodHeader.getOpcode()); }; - return methodHandle.invoke(invocationArgs); + return methodHandle.invokeWithArguments(invocationArgs); } catch (Throwable e) { throw new RuntimeException(e); } finally { 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 index 91bfa191..0a44eec1 100644 --- 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 @@ -2,7 +2,6 @@ import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.FieldVisitor; -import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; import org.tudalgo.algoutils.transform.util.Constants; import org.tudalgo.algoutils.transform.util.TransformationUtils; @@ -63,7 +62,7 @@ public HeaderRecordComponent[] getComponents() { * @return the resulting {@link FieldVisitor} */ public FieldVisitor toFieldVisitor(ClassVisitor delegate, Object value) { - return delegate.visitField(access & ~Opcodes.ACC_FINAL, name, descriptor, signature, value); + return delegate.visitField(access, name, descriptor, signature, value); } /**