diff --git a/.gitignore b/.gitignore index 53b547ca5..175597689 100644 --- a/.gitignore +++ b/.gitignore @@ -28,4 +28,7 @@ forge*changelog.txt # Files generated by ./gradlew check ? /logs/latest.log -/logs/ \ No newline at end of file +/logs/ + +# JVM crash logs +**pid*.log \ No newline at end of file diff --git a/CubicChunksCore b/CubicChunksCore index f087cf00f..51399cc8a 160000 --- a/CubicChunksCore +++ b/CubicChunksCore @@ -1 +1 @@ -Subproject commit f087cf00f7d39e5fec77d0c0c3fa2bb4086de97b +Subproject commit 51399cc8ad426108e008ebe2f84a1a0fd45b981e diff --git a/build.gradle.kts b/build.gradle.kts index 37527dc8f..f406ba4c3 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,6 +1,7 @@ @file:Suppress("INACCESSIBLE_TYPE", "UnstableApiUsage") import io.github.opencubicchunks.gradle.GeneratePackageInfo +import io.github.opencubicchunks.gradle.TypeTransformConfigGen import org.gradle.internal.os.OperatingSystem import java.util.* @@ -18,6 +19,7 @@ plugins { id("io.github.opencubicchunks.gradle.mcGitVersion") id("io.github.opencubicchunks.gradle.mixingen") id("io.github.opencubicchunks.gradle.dasm") + id("io.github.opencubicchunks.gradle.tt") } val minecraftVersion: String by project diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 0bef3f078..e82eec945 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -17,10 +17,9 @@ repositories { } dependencies { - implementation "com.github.OpenCubicChunks:dasm:83a8175258" - implementation 'net.fabricmc:fabric-loom:1.1-SNAPSHOT' implementation 'net.fabricmc:mapping-io:0.2.1' + implementation 'com.github.OpenCubicChunks:dasm:9709b98' implementation 'org.ow2.asm:asm:9.3' implementation 'org.ow2.asm:asm-tree:9.1' implementation 'org.ow2.asm:asm-util:9.1' diff --git a/buildSrc/src/main/java/io/github/opencubicchunks/gradle/TypeTransformConfigGen.java b/buildSrc/src/main/java/io/github/opencubicchunks/gradle/TypeTransformConfigGen.java new file mode 100644 index 000000000..afd02b7df --- /dev/null +++ b/buildSrc/src/main/java/io/github/opencubicchunks/gradle/TypeTransformConfigGen.java @@ -0,0 +1,381 @@ +package io.github.opencubicchunks.gradle; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.StringWriter; +import java.util.ArrayDeque; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; +import com.google.gson.stream.JsonWriter; +import net.fabricmc.loom.LoomGradleExtension; +import net.fabricmc.mappingio.tree.MappingTree; +import net.fabricmc.mappingio.tree.MappingTreeView; +import net.fabricmc.mappingio.tree.MemoryMappingTree; +import org.gradle.api.Project; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.artifacts.Dependency; +import org.gradle.api.artifacts.PublishArtifact; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.Handle; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.ClassNode; + +public class TypeTransformConfigGen { + private static final Gson GSON = new Gson(); + + private static final String MAP_FROM = "named"; + private static final String MAP_TO = "intermediary"; + + private final JsonElement config; + private final MappingTree mappings; + + private final int toIdx; + private final int fromIdx; + + private final Project project; + + private final Set jars = new HashSet<>(); + + private final Map classCache = new HashMap<>(); + + public TypeTransformConfigGen(Project project, MappingTree mappings, String content) throws IOException { + this.config = GSON.fromJson(content, JsonElement.class); + this.mappings = mappings; + this.project = project; + + this.fromIdx = this.mappings.getDstNamespaces().indexOf(MAP_FROM); + this.toIdx = this.mappings.getDstNamespaces().indexOf(MAP_TO); + + if (fromIdx == -1 && !MAP_FROM.equals(this.mappings.getSrcNamespace())) { + throw new IllegalStateException("Cannot find namespace " + MAP_FROM + " in mappings"); + } + + if (toIdx == -1 && !MAP_TO.equals(this.mappings.getSrcNamespace())) { + throw new IllegalStateException("Cannot find namespace " + MAP_TO + " in mappings"); + } + + + for (Configuration config: project.getConfigurations()) { + if (config.getName().startsWith("runtime") && config.isCanBeResolved()) { + for (File file : config.resolve()) { + if (file.getName().endsWith(".jar")) { + jars.add(new ZipFile(file)); + } + } + } + } + + for (ZipFile jar : jars) { + System.out.println("Loading " + jar.getName()); + } + } + + public String generate() { + JsonObject root = config.getAsJsonObject(); + this.generateTypeInfo(root); + + if (root.has("invokers")) { + this.processInvokerData(root.getAsJsonArray("invokers")); + } + + JsonElement res = walkAndMapNames(root); + + //Format the output with 2 spaces + StringWriter writer = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(writer); + jsonWriter.setIndent(" "); + + GSON.toJson(res, jsonWriter); + + return writer.toString(); + } + + private void generateTypeInfo(JsonObject root) { + JsonArray typeInfo = new JsonArray(); + Set processed = new HashSet<>(); + Queue queue = new ArrayDeque<>(); + + JsonObject meta = root.getAsJsonObject("type_meta_info"); + JsonArray inspect = meta.getAsJsonArray("inspect"); + + for (JsonElement e : inspect) { + ClassNode classNode = findClass(e.getAsString()); + queue.addAll(getAllUsedTypes(classNode)); + } + + while (!queue.isEmpty()) { + Type top = queue.poll(); + + if (processed.contains(top)) continue; + processed.add(top); + + JsonObject typeInfoEntry = new JsonObject(); + ClassNode classNode = findClass(top.getInternalName()); + + typeInfoEntry.addProperty("name", top.getClassName().replace('.', '/')); + typeInfoEntry.addProperty("is_interface", (classNode.access & Opcodes.ACC_INTERFACE) != 0); + + typeInfoEntry.addProperty("superclass", classNode.superName); + if (classNode.superName != null) { + queue.add(Type.getObjectType(classNode.superName)); + } + + JsonArray interfaces = new JsonArray(); + for (String iface : classNode.interfaces) { + interfaces.add(iface); + queue.add(Type.getObjectType(iface)); + } + typeInfoEntry.add("interfaces", interfaces); + + typeInfo.add(typeInfoEntry); + } + + root.add("type_info", typeInfo); + } + + private Set getAllUsedTypes(ClassNode node) { + Set types = new HashSet<>(); + types.add(Type.getObjectType(node.name)); + + ClassVisitor visitor = new ClassVisitor(Opcodes.ASM9) { + @Override public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) { + types.add(Type.getType(descriptor)); + return null; + } + + @Override public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + Type[] args = Type.getArgumentTypes(descriptor); + types.addAll(Arrays.asList(args)); + types.add(Type.getReturnType(descriptor)); + return new MethodVisitor(Opcodes.ASM9) { + @Override public void visitTypeInsn(int opcode, String type) { + types.add(Type.getObjectType(type)); + } + + @Override public void visitFieldInsn(int opcode, String owner, String name, String descriptor) { + types.add(Type.getObjectType(owner)); + types.add(Type.getType(descriptor)); + } + + @Override public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) { + types.add(Type.getObjectType(owner)); + Type[] args = Type.getArgumentTypes(descriptor); + types.addAll(Arrays.asList(args)); + types.add(Type.getReturnType(descriptor)); + } + + @Override public void visitInvokeDynamicInsn(String name, String descriptor, Handle bootstrapMethodHandle, Object... bootstrapMethodArguments) { + //TODO: Do this better + types.add(Type.getReturnType(descriptor)); + } + + @Override public void visitMultiANewArrayInsn(String descriptor, int numDimensions) { + types.add(Type.getType(descriptor)); + } + }; + } + }; + + node.accept(visitor); + + return types.stream() + .map(t -> t.getSort() == Type.ARRAY ? t.getElementType() : t) + .filter(t -> t.getSort() == Type.OBJECT) + .collect(Collectors.toSet()); + } + + private ClassNode findClass(String name) { + if (classCache.containsKey(name)) { + return classCache.get(name); + } + + String path = name.replace('.', '/') + ".class"; + + InputStream in = null; + for (ZipFile jar : jars) { + ZipEntry entry = jar.getEntry(path); + if (entry != null) { + try { + in = jar.getInputStream(entry); + break; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + + if (in == null) { + try { + in = ClassLoader.getSystemResourceAsStream(path); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + if (in == null) { + throw new IllegalStateException("Cannot find class " + name); + } + + try { + ClassReader reader = new ClassReader(in); + ClassNode node = new ClassNode(); + reader.accept(node, 0); + classCache.put(name, node); + System.out.println("Found class " + name); + return node; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private void processInvokerData(JsonArray element) { + for (JsonElement invokerDataElem: element) { + JsonObject invokerData = invokerDataElem.getAsJsonObject(); + + String target = invokerData.get("target").getAsString(); + + for (JsonElement method: invokerData.getAsJsonArray("methods")) { + JsonObject methodObj = method.getAsJsonObject(); + + String[] name = methodObj.get("name").getAsString().split(" "); + String calls = methodObj.get("calls").getAsString(); + + methodObj.addProperty("name", name[0] + " " + mapMethodDesc(name[1])); + methodObj.addProperty("calls", mapMethodName(target, calls, name[1])); + } + } + } + + private JsonElement walkAndMapNames(JsonElement element) { + //Check if element is a method object + if (element.isJsonObject()) { + JsonObject obj = element.getAsJsonObject(); + if (obj.has("owner") && obj.has("name") && obj.has("desc") && obj.has("call_type")) { + String owner = obj.get("owner").getAsString(); + String name = obj.get("name").getAsString(); + String desc = obj.get("desc").getAsString(); + + obj.addProperty("owner", mapClassName(owner)); + obj.addProperty("name", mapMethodName(owner, name, desc)); + obj.addProperty("desc", mapMethodDesc(desc)); + } else { + JsonObject newObject = new JsonObject(); + + for (Map.Entry entry : obj.entrySet()) { + newObject.add(mapClassName(entry.getKey()), walkAndMapNames(entry.getValue())); + } + + return newObject; + } + } else if (element.isJsonArray()) { + JsonArray array = element.getAsJsonArray(); + + for (int i = 0; i < array.size(); i++) { + array.set(i, walkAndMapNames(array.get(i))); + } + + return array; + } else if (element.isJsonPrimitive()) { + JsonPrimitive primitive = element.getAsJsonPrimitive(); + + if (primitive.isString()) { + String str = primitive.getAsString(); + + if (!str.contains("(")) { + return new JsonPrimitive(mapClassName(str)); + } else { + String[] split = str.split(" "); + + if (split.length == 3) { + String[] ownerAndName = split[1].split("#"); + + return new JsonPrimitive( + split[0] + " " + + mapClassName(ownerAndName[0]) + "#" + + mapMethodName(ownerAndName[0], ownerAndName[1], split[2]) + " " + + mapMethodDesc(split[2]) + ); + } + + return primitive; + } + } + + return primitive; + } + + return element; + } + + private String mapClassName(String name) { + MappingTree.ClassMapping classMapping = this.fromIdx == -1 ? this.mappings.getClass(name) : this.mappings.getClass(name, this.fromIdx); + + if (classMapping == null) { + return name; + } + + return map((MappingTreeView.ElementMappingView) classMapping); + } + + private String mapMethodName(String owner, String name, String desc) { + MappingTree.MethodMapping methodMapping = this.fromIdx == -1 ? this.mappings.getMethod(owner, name, desc) : this.mappings.getMethod(owner, name, desc, this.fromIdx); + + if (methodMapping == null) { + return name; + } + + return map((MappingTreeView.ElementMappingView) methodMapping); + } + + private String mapMethodDesc(String desc) { + Type returnType = Type.getReturnType(desc); + Type[] argumentTypes = Type.getArgumentTypes(desc); + + returnType = mapType(returnType); + + for (int i = 0; i < argumentTypes.length; i++) { + argumentTypes[i] = mapType(argumentTypes[i]); + } + + return Type.getMethodDescriptor(returnType, argumentTypes); + } + + private Type mapType(Type type) { + if (type.getSort() == Type.ARRAY) { + int dimensions = type.getDimensions(); + return Type.getType("[".repeat(dimensions) + mapType(type.getElementType()).getDescriptor()); + } else if (type.getSort() == Type.OBJECT) { + return Type.getObjectType(mapClassName(type.getInternalName())); + } else { + return type; + } + } + + private String map(MappingTreeView.ElementMappingView element) { + if (this.toIdx == -1) { + return element.getSrcName(); + } else { + return element.getDstName(this.toIdx); + } + } +} diff --git a/buildSrc/src/main/java/io/github/opencubicchunks/gradle/TypeTransformConfigGenPlugin.java b/buildSrc/src/main/java/io/github/opencubicchunks/gradle/TypeTransformConfigGenPlugin.java new file mode 100644 index 000000000..60a5da5fe --- /dev/null +++ b/buildSrc/src/main/java/io/github/opencubicchunks/gradle/TypeTransformConfigGenPlugin.java @@ -0,0 +1,43 @@ +package io.github.opencubicchunks.gradle; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; + +import net.fabricmc.loom.api.LoomGradleExtensionAPI; +import net.fabricmc.loom.extension.LoomGradleExtensionImpl; +import net.fabricmc.loom.util.service.ScopedSharedServiceManager; +import net.fabricmc.mappingio.tree.MemoryMappingTree; +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.language.jvm.tasks.ProcessResources; + +public class TypeTransformConfigGenPlugin implements Plugin { + @Override + public void apply(Project project) { + project.afterEvaluate( + proj -> { + ProcessResources processResources = ((ProcessResources) project.getTasks().getByName("processResources")); + LoomGradleExtensionAPI loomApi = project.getExtensions().getByType(LoomGradleExtensionAPI.class); + // TODO: try to use LoomGradleExtensionAPI#getMappingsFile() instead of loom internals + MemoryMappingTree mappings = ((LoomGradleExtensionImpl) loomApi).getMappingConfiguration().getMappingsService(new ScopedSharedServiceManager()).getMappingTree(); + + File destinationDir = processResources.getDestinationDir(); + processResources.filesMatching("**/type-transform.json", copySpec -> { + copySpec.exclude(); + try { + File file = copySpec.getFile(); + String content = Files.readString(file.toPath()); + File output = copySpec.getRelativePath().getFile(destinationDir); + + TypeTransformConfigGen generator = new TypeTransformConfigGen(proj, mappings, content); + + Files.writeString(output.toPath(), generator.generate()); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } + ); + } +} diff --git a/buildSrc/src/main/resources/META-INF/gradle-plugins/io.github.opencubicchunks.gradle.tt.properties b/buildSrc/src/main/resources/META-INF/gradle-plugins/io.github.opencubicchunks.gradle.tt.properties new file mode 100644 index 000000000..63d93eb1d --- /dev/null +++ b/buildSrc/src/main/resources/META-INF/gradle-plugins/io.github.opencubicchunks.gradle.tt.properties @@ -0,0 +1 @@ +implementation-class=io.github.opencubicchunks.gradle.TypeTransformConfigGenPlugin \ No newline at end of file diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index da2fc4ba5..6719ac811 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -136,8 +136,8 @@ + + + + + diff --git a/config/checkstyle/suppressions.xml b/config/checkstyle/suppressions.xml index b72cd1400..de71e2c6d 100644 --- a/config/checkstyle/suppressions.xml +++ b/config/checkstyle/suppressions.xml @@ -6,9 +6,9 @@ - + - + @@ -35,9 +35,12 @@ - - + + - + + + + \ No newline at end of file diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/debug/DebugVisualization.java b/src/main/java/io/github/opencubicchunks/cubicchunks/debug/DebugVisualization.java index 2504a5a06..85b45d769 100644 --- a/src/main/java/io/github/opencubicchunks/cubicchunks/debug/DebugVisualization.java +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/debug/DebugVisualization.java @@ -172,7 +172,7 @@ void main() { private static float screenWidth = 854.0f; private static float screenHeight = 480f; private static GLCapabilities debugGlCapabilities; - private static boolean enabled; + private static boolean enabled = false; private static VisualizationMode mode = VisualizationMode.AVAILABLE_MODES[0]; diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/levelgen/aquifer/CubicAquifer.java b/src/main/java/io/github/opencubicchunks/cubicchunks/levelgen/aquifer/CubicAquifer.java index afee0d6fe..8b6085c1b 100644 --- a/src/main/java/io/github/opencubicchunks/cubicchunks/levelgen/aquifer/CubicAquifer.java +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/levelgen/aquifer/CubicAquifer.java @@ -93,7 +93,16 @@ private int getIndex(int x, int y, int z) { int localX = x - this.minGridX; int localY = y - this.minGridY; int localZ = z - this.minGridZ; - return (localY * this.gridSizeZ + localZ) * this.gridSizeX + localX; + + int index = (localY * this.gridSizeZ + localZ) * this.gridSizeX + localX; + + //FIXME: Because of the way the cache is implemented, this can cause an ArrayIndexOutOfBoundsException. + //The coordinates are unpacked from longs so when outside the proper range, they wrap around and this returns nonsense values. + if (index < 0 || index >= this.aquiferCache.length) { + return 0; + } + + return index; } @Override @@ -124,6 +133,7 @@ public BlockState computeSubstance(DensityFunction.FunctionContext context, doub int firstDistance2 = Integer.MAX_VALUE; int secondDistance2 = Integer.MAX_VALUE; int thirdDistance2 = Integer.MAX_VALUE; + long firstSource = 0; long secondSource = 0; long thirdSource = 0; diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/ASMConfigPlugin.java b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/ASMConfigPlugin.java index 3f574f4b3..c727cebf8 100644 --- a/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/ASMConfigPlugin.java +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/ASMConfigPlugin.java @@ -6,6 +6,8 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -19,6 +21,8 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; +import io.github.opencubicchunks.cubicchunks.mixin.transform.MainTransformer; +import io.github.opencubicchunks.cubicchunks.utils.TestMappingUtils; import io.github.opencubicchunks.dasm.MappingsProvider; import io.github.opencubicchunks.dasm.RedirectsParseException; import io.github.opencubicchunks.dasm.RedirectsParser; @@ -26,6 +30,7 @@ import io.github.opencubicchunks.dasm.TypeRedirect; import net.fabricmc.loader.api.FabricLoader; import net.fabricmc.loader.api.MappingResolver; +import org.objectweb.asm.ClassWriter; import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.MethodNode; import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin; @@ -147,7 +152,7 @@ private String findWholeClassTypeRedirectFor(RedirectsParser.ClassTarget target, //Ideally the input json would all have the same, and we'd just figure it out here RedirectsParser.ClassTarget target = classTargetByName.get(targetClassName); if (target == null) { - throw new RuntimeException(new ClassNotFoundException(String.format("Couldn't find target class %s to remap", targetClassName))); + return; //throw new RuntimeException(new ClassNotFoundException(String.format("Couldn't find target class %s to remap", targetClassName))); } if (target.isWholeClass()) { ClassNode duplicate = new ClassNode(); @@ -210,7 +215,53 @@ private void replaceClassContent(ClassNode node, ClassNode replaceWith) { } @Override public void postApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { + MappingResolver map = TestMappingUtils.getMappingResolver(); + String dynamicGraphMinFixedPoint = map.mapClassName("intermediary", "net.minecraft.class_3554"); + String layerLightEngine = map.mapClassName("intermediary", "net.minecraft.class_3558"); + String layerLightSectionStorage = map.mapClassName("intermediary", "net.minecraft.class_3560"); + String blockLightSectionStorage = map.mapClassName("intermediary", "net.minecraft.class_3547"); + String skyLightSectionStorage = map.mapClassName("intermediary", "net.minecraft.class_3569"); + String sectionPos = map.mapClassName("intermediary", "net.minecraft.class_4076"); + String blockLightEngine = map.mapClassName("intermediary", "net.minecraft.class_3552"); + String skyLightEngine = map.mapClassName("intermediary", "net.minecraft.class_3572"); + String noiseBasedAquifer = map.mapClassName("intermediary", "net.minecraft.class_6350$class_5832"); + + Set defaulted = Set.of( + blockLightSectionStorage, + skyLightSectionStorage, + blockLightEngine, + skyLightEngine + ); + + if (targetClassName.equals(dynamicGraphMinFixedPoint)) { + MainTransformer.transformDynamicGraphMinFixedPoint(targetClass); + } else if (targetClassName.equals(layerLightEngine)) { + MainTransformer.transformLayerLightEngine(targetClass); + } else if (targetClassName.equals(layerLightSectionStorage)) { + MainTransformer.transformLayerLightSectionStorage(targetClass); + } else if (targetClassName.equals(sectionPos)) { + MainTransformer.transformSectionPos(targetClass); + } else if (targetClassName.equals(noiseBasedAquifer)) { + MainTransformer.transformNoiseBasedAquifer(targetClass); + } else if (defaulted.contains(targetClassName)) { + MainTransformer.defaultTransform(targetClass); + } else { + return; + } + //Save it without computing extra stuff (like maxs) which means that if the frames are wrong and mixin fails to save it, it will be saved elsewhere + Path savePath = TestMappingUtils.getGameDir().resolve("longpos-out").resolve(targetClassName.replace('.', '/') + ".class"); + try { + Files.createDirectories(savePath.getParent()); + + ClassWriter writer = new ClassWriter(0); + targetClass.accept(writer); + byte[] bytes = writer.toByteArray(); + Files.write(savePath, bytes); + System.out.println("Saved " + targetClassName + " to " + savePath); + } catch (IOException e) { + throw new IllegalStateException(e); + } } private JsonElement parseFileAsJson(String fileName) { diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/asm/common/MixinAsmTarget.java b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/asm/common/MixinAsmTarget.java index 68379d8ad..f78e518a2 100644 --- a/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/asm/common/MixinAsmTarget.java +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/asm/common/MixinAsmTarget.java @@ -2,12 +2,21 @@ import io.github.opencubicchunks.cubicchunks.server.level.CubeTaskPriorityQueue; import io.github.opencubicchunks.cubicchunks.server.level.CubeTaskPriorityQueueSorter; +import net.minecraft.core.SectionPos; import net.minecraft.server.level.ChunkHolder; import net.minecraft.server.level.ChunkMap; import net.minecraft.server.level.ChunkTaskPriorityQueue; import net.minecraft.server.level.ChunkTaskPriorityQueueSorter; import net.minecraft.server.level.DistanceManager; import net.minecraft.world.level.NaturalSpawner; +import net.minecraft.world.level.levelgen.Aquifer; +import net.minecraft.world.level.lighting.BlockLightEngine; +import net.minecraft.world.level.lighting.BlockLightSectionStorage; +import net.minecraft.world.level.lighting.DynamicGraphMinFixedPoint; +import net.minecraft.world.level.lighting.LayerLightEngine; +import net.minecraft.world.level.lighting.LayerLightSectionStorage; +import net.minecraft.world.level.lighting.SkyLightEngine; +import net.minecraft.world.level.lighting.SkyLightSectionStorage; import org.spongepowered.asm.mixin.Mixin; @Mixin({ @@ -15,11 +24,24 @@ ChunkMap.class, ChunkHolder.class, NaturalSpawner.class, + DistanceManager.class, ChunkTaskPriorityQueue.class, CubeTaskPriorityQueue.class, ChunkTaskPriorityQueueSorter.class, - CubeTaskPriorityQueueSorter.class + CubeTaskPriorityQueueSorter.class, + + //Long Pos Transforms + DynamicGraphMinFixedPoint.class, + BlockLightEngine.class, + BlockLightSectionStorage.class, + SkyLightEngine.class, + LayerLightEngine.class, + SectionPos.class, + LayerLightSectionStorage.class, + SkyLightSectionStorage.class, + BlockLightSectionStorage.class, + Aquifer.NoiseBasedAquifer.class }) public class MixinAsmTarget { // intentionally empty diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/core/common/MixinCrashReport.java b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/core/common/MixinCrashReport.java new file mode 100644 index 000000000..f296e6331 --- /dev/null +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/core/common/MixinCrashReport.java @@ -0,0 +1,93 @@ +package io.github.opencubicchunks.cubicchunks.mixin.core.common; + +import java.lang.reflect.Method; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import javax.annotation.Nullable; + +import io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.CCSynthetic; +import io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.TypeTransformer; +import io.github.opencubicchunks.cubicchunks.mixin.transform.util.ASMUtil; +import net.minecraft.CrashReport; +import net.minecraft.CrashReportCategory; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; +import org.spongepowered.asm.mixin.injection.callback.LocalCapture; + +//This will append an extra message to the crash report warning the user that it may be caused by 3-int transforms +@Mixin(CrashReport.class) +public class MixinCrashReport { + @Shadow private StackTraceElement[] uncategorizedStackTrace; + + @Shadow @Final private List details; + + @Inject( + method = "getFriendlyReport", + at = @At( + value = "INVOKE", + target = "Ljava/lang/StringBuilder;append(Ljava/lang/String;)Ljava/lang/StringBuilder;", + ordinal = 3, + shift = At.Shift.AFTER + ), + locals = LocalCapture.CAPTURE_FAILHARD + ) + private void appendWarning(CallbackInfoReturnable cir, StringBuilder sb) { + Set methods = new HashSet<>(); + + if (this.uncategorizedStackTrace != null) { + findSynthetic(methods, this.uncategorizedStackTrace); + } + + for (CrashReportCategory detail : this.details) { + if (detail.getStacktrace() != null) { + findSynthetic(methods, detail.getStacktrace()); + } + } + + if (!methods.isEmpty()) { + sb.append("WARNING: The following stacktrace(s) contain methods generated by cubic chunks. The methods of interest are the following:\n"); + + int i = 1; + for (Method method : methods) { + CCSynthetic synthetic = method.getAnnotation(CCSynthetic.class); + String original = synthetic.original(); + int idx = original.indexOf('('); + String name = original.substring(0, idx); + String desc = original.substring(idx); + + sb.append(" - Method #").append(i).append(":\n"); + sb.append(" - Class: ").append(method.getDeclaringClass().getName()).append("\n"); + sb.append(" - Transformed Method: ").append(method.getName()).append(" ").append(ASMUtil.getDescriptor(method)).append("\n"); + sb.append(" - Original Method: ").append(name).append(" ").append(desc).append("\n"); + i++; + } + } + } + + private void findSynthetic(Set set, StackTraceElement[] stackTrace) { + for (StackTraceElement element : stackTrace) { + Method method = getMethodIfSynthetic(element); + + if (method != null) { + set.add(method); + } + } + } + + private @Nullable Method getMethodIfSynthetic(StackTraceElement element) { + try { + Class clazz = Class.forName(element.getClassName()); + return TypeTransformer.getSyntheticMethod(clazz, element.getMethodName(), element.getLineNumber()); + } catch (ClassNotFoundException e) { + //Do nothing + } + + return null; + } +} diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/core/common/chunk/MixinChunkMap.java b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/core/common/chunk/MixinChunkMap.java index 3b01d3f3f..e54295bfa 100644 --- a/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/core/common/chunk/MixinChunkMap.java +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/core/common/chunk/MixinChunkMap.java @@ -180,6 +180,7 @@ public BlockGetter getChunkForLighting(int i, int j) { // NOTE: used from ASM, don't rename private final LongSet cubeEntitiesInLevel = new LongOpenHashSet(); + // used from ASM private final Long2ObjectLinkedOpenHashMap pendingCubeUnloads = new Long2ObjectLinkedOpenHashMap<>(); // worldgenMailbox @@ -190,6 +191,7 @@ public BlockGetter getChunkForLighting(int i, int j) { private final AtomicInteger tickingGeneratedCubes = new AtomicInteger(); private final Long2ByteMap cubeTypeCache = new Long2ByteOpenHashMap(); + // used from ASM private final Queue cubeUnloadQueue = Queues.newConcurrentLinkedQueue(); private ServerChunkCache serverChunkCache; @@ -291,13 +293,6 @@ protected void onTickScheduleUnloads(BooleanSupplier hasMoreTime, CallbackInfo c this.processCubeUnloads(hasMoreTime); } - // Forge dimension stuff gone in 1.16, TODO when forge readds dimension code - // @Redirect(method = "tick", at = @At(value = "INVOKE", target = "Lit/unimi/dsi/fastutil/longs/Long2ObjectLinkedOpenHashMap;isEmpty()Z")) - // private boolean canUnload(Long2ObjectLinkedOpenHashMap loadedChunks) - // { - // return loadedChunks.isEmpty() && loadedCubes.isEmpty(); - // } - @Inject(method = "saveAllChunks", at = @At("HEAD")) protected void save(boolean flush, CallbackInfo ci) { if (!((CubicLevelHeightAccessor) this.level).isCubic()) { @@ -372,12 +367,18 @@ protected void save(boolean flush, CallbackInfo ci) { } + // used from ASM + private void flushCubeWorker() { + regionCubeIO.flush(); + LOGGER.info("Cube Storage ({}): All cubes are saved", this.storageName); + } + @Override public void setServerChunkCache(ServerChunkCache cache) { serverChunkCache = cache; } // save() - private boolean cubeSave(CubeAccess cube) { + public boolean cubeSave(CubeAccess cube) { ((CubicSectionStorage) this.poiManager).flush(cube.getCubePos()); if (!cube.isUnsaved()) { return false; @@ -477,7 +478,6 @@ private CompoundTag readCubeNBT(CubePos cubePos) throws IOException { return regionCubeIO.loadCubeNBT(cubePos); } - // processUnloads private void processCubeUnloads(BooleanSupplier hasMoreTime) { LongIterator longiterator = this.cubesToDrop.iterator(); @@ -498,6 +498,11 @@ private void processCubeUnloads(BooleanSupplier hasMoreTime) { } } + // used from ASM + private void writeCube(CubePos pos, CompoundTag tag) { + regionCubeIO.saveCubeNBT(pos, tag); + } + // scheduleUnload private void scheduleCubeUnload(long cubePos, ChunkHolder cubeHolder) { CompletableFuture toSaveFuture = ((CubeHolder) cubeHolder).getCubeToSave(); @@ -554,6 +559,7 @@ private void scheduleCubeUnload(long cubePos, ChunkHolder cubeHolder) { }); } + // used from ASM // markPositionReplaceable @Override public void markCubePositionReplaceable(CubePos cubePos) { this.cubeTypeCache.put(cubePos.asLong(), (byte) -1); @@ -651,6 +657,7 @@ public Iterable getCubes() { return Iterables.unmodifiableIterable(this.visibleCubeMap.values()); } + // This can't be ASM, the changes for column load order are too invasive @Override public CompletableFuture> scheduleCube(ChunkHolder cubeHolder, ChunkStatus chunkStatus) { CubePos cubePos = ((CubeHolder) cubeHolder).getCubePos(); diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/MainTransformer.java b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/MainTransformer.java new file mode 100644 index 000000000..b820ed8b1 --- /dev/null +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/MainTransformer.java @@ -0,0 +1,182 @@ +package io.github.opencubicchunks.cubicchunks.mixin.transform; + +import static org.objectweb.asm.Type.ARRAY; +import static org.objectweb.asm.Type.OBJECT; +import static org.objectweb.asm.Type.getObjectType; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Objects; + +import io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.TypeTransformer; +import io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.config.Config; +import io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.config.ConfigLoader; +import io.github.opencubicchunks.cubicchunks.utils.TestMappingUtils; +import net.fabricmc.loader.api.MappingResolver; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.NotNull; +import org.objectweb.asm.Type; +import org.objectweb.asm.commons.Method; +import org.objectweb.asm.tree.ClassNode; + +public class MainTransformer { + public static final Config TRANSFORM_CONFIG; + private static final Logger LOGGER = LogManager.getLogger(); + private static final boolean IS_DEV = TestMappingUtils.isDev(); + + public static void transformDynamicGraphMinFixedPoint(ClassNode targetClass) { + TypeTransformer transformer = new TypeTransformer(TRANSFORM_CONFIG, targetClass, true); + + transformer.analyzeAllMethods(); + + transformer.makeConstructor("(III)V"); + + transformer.transformAllMethods(); + } + + public static void transformLayerLightEngine(ClassNode targetClass) { + TypeTransformer transformer = new TypeTransformer(TRANSFORM_CONFIG, targetClass, true); + + transformer.analyzeAllMethods(); + transformer.transformAllMethods(); + + transformer.callMagicSuperConstructor(); + } + + public static void transformNoiseBasedAquifer(ClassNode target) { + defaultTransform(target); + } + + public static void transformSectionPos(ClassNode targetClass) { + TypeTransformer transformer = new TypeTransformer(TRANSFORM_CONFIG, targetClass, true); + + ClassMethod blockToSection = remapMethod( + new ClassMethod( + getObjectType("net/minecraft/class_4076"), + new Method("method_18691", "(J)J"), + getObjectType("net/minecraft/class_4076") + ) + ); + + transformer.analyzeMethod(blockToSection.method.getName(), blockToSection.method.getDescriptor()); + transformer.cleanUpAnalysis(); + + transformer.generateTransformedMethod(blockToSection.method.getName(), blockToSection.method.getDescriptor()); + transformer.cleanUpTransform(); + } + + public static void defaultTransform(ClassNode targetClass) { + TypeTransformer transformer = new TypeTransformer(TRANSFORM_CONFIG, targetClass, true); + + transformer.analyzeAllMethods(); + transformer.transformAllMethods(); + } + + public static void transformLayerLightSectionStorage(ClassNode targetClass) { + defaultTransform(targetClass); + } + + @NotNull private static ClassMethod remapMethod(ClassMethod clMethod) { + MappingResolver mappingResolver = TestMappingUtils.getMappingResolver(); + Type[] params = Type.getArgumentTypes(clMethod.method.getDescriptor()); + Type returnType = Type.getReturnType(clMethod.method.getDescriptor()); + + Type mappedType = remapType(clMethod.owner); + String mappedName = mappingResolver.mapMethodName("intermediary", + clMethod.mappingOwner.getClassName(), clMethod.method.getName(), clMethod.method.getDescriptor()); + if (clMethod.method.getName().contains("method") && IS_DEV && mappedName.equals(clMethod.method.getName())) { + throw new Error("Fail! Mapping method " + clMethod.method.getName() + " failed in dev!"); + } + Type[] mappedParams = new Type[params.length]; + for (int i = 0; i < params.length; i++) { + mappedParams[i] = remapDescType(params[i]); + } + Type mappedReturnType = remapDescType(returnType); + return new ClassMethod(mappedType, new Method(mappedName, mappedReturnType, mappedParams)); + } + + private static Type remapDescType(Type t) { + if (t.getSort() == ARRAY) { + int dimCount = t.getDimensions(); + StringBuilder prefix = new StringBuilder(dimCount); + for (int i = 0; i < dimCount; i++) { + prefix.append('['); + } + return Type.getType(prefix + remapDescType(t.getElementType()).getDescriptor()); + } + if (t.getSort() != OBJECT) { + return t; + } + MappingResolver mappingResolver = TestMappingUtils.getMappingResolver(); + String unmapped = t.getClassName(); + if (unmapped.endsWith(";")) { + unmapped = unmapped.substring(1, unmapped.length() - 1); + } + String mapped = mappingResolver.mapClassName("intermediary", unmapped); + String mappedDesc = 'L' + mapped.replace('.', '/') + ';'; + if (unmapped.contains("class") && IS_DEV && mapped.equals(unmapped)) { + throw new Error("Fail! Mapping class " + unmapped + " failed in dev!"); + } + return Type.getType(mappedDesc); + } + + private static Type remapType(Type t) { + MappingResolver mappingResolver = TestMappingUtils.getMappingResolver(); + String unmapped = t.getClassName(); + String mapped = mappingResolver.mapClassName("intermediary", unmapped); + if (unmapped.contains("class") && IS_DEV && mapped.equals(unmapped)) { + throw new Error("Fail! Mapping class " + unmapped + " failed in dev!"); + } + return Type.getObjectType(mapped.replace('.', '/')); + } + + static { + //Load config + try { + InputStream is = MainTransformer.class.getResourceAsStream("/type-transform.json"); + TRANSFORM_CONFIG = ConfigLoader.loadConfig(is); + is.close(); + } catch (IOException e) { + throw new RuntimeException("Couldn't load transform config", e); + } + } + + public static final class ClassMethod { + public final Type owner; + public final Method method; + public final Type mappingOwner; + + ClassMethod(Type owner, Method method) { + this.owner = owner; + this.method = method; + this.mappingOwner = owner; + } + + // mapping owner because mappings owner may not be the same as in the call site + ClassMethod(Type owner, Method method, Type mappingOwner) { + this.owner = owner; + this.method = method; + this.mappingOwner = mappingOwner; + } + + @Override public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ClassMethod that = (ClassMethod) o; + return owner.equals(that.owner) && method.equals(that.method) && mappingOwner.equals(that.mappingOwner); + } + + @Override public int hashCode() { + return Objects.hash(owner, method, mappingOwner); + } + + @Override public String toString() { + return "ClassMethod{" + + "owner=" + owner + + ", method=" + method + + ", mappingOwner=" + mappingOwner + + '}'; + } + } +} \ No newline at end of file diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/bytecodegen/BytecodeFactory.java b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/bytecodegen/BytecodeFactory.java new file mode 100644 index 000000000..fd42508a2 --- /dev/null +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/bytecodegen/BytecodeFactory.java @@ -0,0 +1,20 @@ +package io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.bytecodegen; + +import java.util.function.Function; + +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.InsnList; + +/** + * An interface which generates a bytecode instruction list. + */ +public interface BytecodeFactory { + /** + * Generates a bytecode instruction list. + * + * @param variableAllocator A function which, when given a type, returns an appropriate variable slot for that type. + * + * @return A bytecode instruction list. + */ + InsnList generate(Function variableAllocator); +} diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/bytecodegen/ConstantFactory.java b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/bytecodegen/ConstantFactory.java new file mode 100644 index 000000000..23b5bba0b --- /dev/null +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/bytecodegen/ConstantFactory.java @@ -0,0 +1,52 @@ +package io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.bytecodegen; + +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.InsnNode; +import org.objectweb.asm.tree.IntInsnNode; +import org.objectweb.asm.tree.LdcInsnNode; + +/** + * An instruction factory which creates an instruction which creates a provided constant + */ +public class ConstantFactory implements InstructionFactory { + private final Object value; + + /** + * Creates a new constant factory + * @param value The value of the constant that this factory will create + */ + public ConstantFactory(Object value) { + this.value = value; + } + + @Override + public AbstractInsnNode create() { + if (value instanceof Integer || value instanceof Short || value instanceof Byte) { + Number number = (Number) value; + int n = number.intValue(); + + if (n >= -1 && n <= 5) { + return new InsnNode(Opcodes.ICONST_0 + n); + } else if (n < 256) { + return new IntInsnNode(Opcodes.BIPUSH, n); + } else if (n < 65536) { + return new IntInsnNode(Opcodes.SIPUSH, n); + } + } else if (value instanceof Long l) { + if (l == 0 || l == 1) { + return new InsnNode((int) (Opcodes.LCONST_0 + l)); + } + } else if (value instanceof Float f) { + if (f == 0.0f || f == 1.0f || f == 2.0f) { + return new InsnNode((int) (Opcodes.FCONST_0 + f)); + } + } else if (value instanceof Double d) { + if (d == 0.0d || d == 1.0d) { + return new InsnNode((int) (Opcodes.DCONST_0 + d)); + } + } + + return new LdcInsnNode(value); + } +} diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/bytecodegen/InstructionFactory.java b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/bytecodegen/InstructionFactory.java new file mode 100644 index 000000000..f199fb732 --- /dev/null +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/bytecodegen/InstructionFactory.java @@ -0,0 +1,20 @@ +package io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.bytecodegen; + +import java.util.function.Function; + +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.InsnList; + +/** + * A factory that creates a single instruction. Can be used as a {@link BytecodeFactory}. + */ +public interface InstructionFactory extends BytecodeFactory { + AbstractInsnNode create(); + + default InsnList generate(Function variableAllocator) { + InsnList list = new InsnList(); + list.add(create()); + return list; + } +} diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/bytecodegen/JSONBytecodeFactory.java b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/bytecodegen/JSONBytecodeFactory.java new file mode 100644 index 000000000..4a3e963b3 --- /dev/null +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/bytecodegen/JSONBytecodeFactory.java @@ -0,0 +1,330 @@ +package io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.bytecodegen; + +import static org.objectweb.asm.Opcodes.*; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.Function; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.config.ConfigLoader; +import io.github.opencubicchunks.cubicchunks.mixin.transform.util.MethodID; +import net.fabricmc.loader.api.MappingResolver; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.InsnList; +import org.objectweb.asm.tree.InsnNode; +import org.objectweb.asm.tree.TypeInsnNode; +import org.objectweb.asm.tree.VarInsnNode; + +public class JSONBytecodeFactory implements BytecodeFactory { + //The names of the JVM instructions that affect local variables + private static final String[] VAR_INSNS = { + "ILOAD", "LLOAD", "FLOAD", "DLOAD", "ALOAD", + "ISTORE", "LSTORE", "FSTORE", "DSTORE", "ASTORE" + }; + + //The corresponding types + private static final Type[] TYPES = { + Type.INT_TYPE, Type.LONG_TYPE, Type.FLOAT_TYPE, Type.DOUBLE_TYPE, Type.getType(Object.class), + Type.INT_TYPE, Type.LONG_TYPE, Type.FLOAT_TYPE, Type.DOUBLE_TYPE, Type.getType(Object.class) + }; + + //The actual opcodes + private static final int[] VAR_OPCODES = { + ILOAD, LLOAD, FLOAD, DLOAD, ALOAD, + ISTORE, LSTORE, FSTORE, DSTORE, ASTORE + }; + + //Every instruction is added individually to the InsnList. The int array is an array with slots for variables + private final List> instructionGenerators = new ArrayList<>(); + //The types of the variable. These correspond to the array passed to the functions above + private final List varTypes = new ArrayList<>(); + + /** + * Creates a new JSONBytecodeFactory + * @param data A JSONArray where each element corresponds to an instruction. Simple instructions are represented by a single string, while + * instructions that have parameters are represented by a JSONObject. + * @param mappings The mappings used to remap types to their current names + */ + public JSONBytecodeFactory(JsonArray data, MappingResolver mappings) { + //Find all variable names + Map varNames = new HashMap<>(); + + for (JsonElement element : data) { + //Check if it is a var instruction (begins with one of the VAR_INSNS) + if (element.isJsonPrimitive()) { + String name = element.getAsString(); + for (int i = 0; i < VAR_INSNS.length; i++) { + String insnName = VAR_INSNS[i]; + if (name.startsWith(insnName)) { + //Extract the variable name. In this instruction ("ILOAD {x}"), the variable name is "x" + String namePart = name.substring(insnName.length() + 1); + + if (!namePart.matches("\\{[0-9a-zA-Z_]+}")) { + throw new IllegalArgumentException("Variables instructions must be of the form 'OPCODE {NAME}'"); + } + + String actualName = namePart.substring(1, namePart.length() - 1); + Type t = TYPES[i]; + + //Check if the variable name is already in use + if (varNames.containsKey(actualName)) { + //The variable name has already been seen. If it has a different type, throw an exception + if (!varTypes.get(varNames.get(actualName)).equals(t)) { + throw new IllegalArgumentException("Variable " + actualName + " has already been defined with a different type"); + } + } else { + //Store variable name and type + varNames.put(actualName, varNames.size()); + varTypes.add(t); + } + + break; + } + } + } + } + + //Create each InstructionFactory + for (JsonElement element : data) { + if (element.isJsonPrimitive()) { + //It is a simple instruction (a string) + instructionGenerators.add(createInstructionFactoryFromName(element.getAsString(), varNames)); + } else { + //It is a complex instruction + instructionGenerators.add(createInstructionFactoryFromObject(element.getAsJsonObject(), mappings)); + } + } + } + + private BiConsumer createInstructionFactoryFromObject(JsonObject object, MappingResolver mappings) { + String type = object.get("type").getAsString(); + + if (type.equals("INVOKEVIRTUAL") || type.equals("INVOKESTATIC") || type.equals("INVOKESPECIAL") || type.equals("INVOKEINTERFACE")) { + return generateMethodCall(object, mappings, type); + } else if (type.equals("LDC")) { + return generateConstantInsn(object); + } else if (type.equals("NEW") || type.equals("ANEWARRAY") || type.equals("CHECKCAST") || type.equals("INSTANCEOF")) { + return generateTypeInsn(object, mappings, type); + } + + throw new IllegalArgumentException("Unknown instruction type: " + type); + } + + private BiConsumer generateMethodCall(JsonObject object, MappingResolver mappings, String type) { + JsonElement method = object.get("method"); + //Get the call type + MethodID.CallType callType = switch (type) { + case "INVOKEVIRTUAL" -> MethodID.CallType.VIRTUAL; + case "INVOKESTATIC" -> MethodID.CallType.STATIC; + case "INVOKESPECIAL" -> MethodID.CallType.SPECIAL; + case "INVOKEINTERFACE" -> MethodID.CallType.INTERFACE; + + default -> throw new IllegalArgumentException("Invalid call type: " + type); //This will never be reached but the compiler gets angry if it isn't here + }; + + MethodID methodID = ConfigLoader.loadMethodID(method, mappings, callType); + + + MethodID finalMethodID = methodID; + return (insnList, __) -> insnList.add(finalMethodID.callNode()); + } + + private BiConsumer generateConstantInsn(JsonObject object) { + //We just need to get the constant type and then we can just use ConstantFactory + String constantType = object.get("constant_type").getAsString(); + + JsonElement element = object.get("value"); + + Object constant = switch (constantType) { + case "string" -> element.getAsString(); + case "int" -> element.getAsInt(); + case "float" -> element.getAsFloat(); + case "long" -> element.getAsLong(); + case "double" -> element.getAsDouble(); + default -> throw new IllegalArgumentException("Invalid constant type: " + constantType); + }; + + InstructionFactory generator = new ConstantFactory(constant); + + return (insnList, __) -> insnList.add(generator.create()); + } + + private BiConsumer generateTypeInsn(JsonObject object, MappingResolver mappings, String type) { + //We just need to get the type + JsonElement classNameJson = object.get("class"); + Type t = Type.getObjectType(classNameJson.getAsString()); + Type mappedType = ConfigLoader.remapType(t, mappings); + + int opcode = switch (type) { + case "NEW" -> Opcodes.NEW; + case "ANEWARRAY" -> Opcodes.ANEWARRAY; + case "CHECKCAST" -> Opcodes.CHECKCAST; + case "INSTANCEOF" -> Opcodes.INSTANCEOF; + default -> { + throw new IllegalArgumentException("Impossible to reach this point"); + } + }; + + return (insnList, __) -> insnList.add(new TypeInsnNode(opcode, mappedType.getInternalName())); + } + + private BiConsumer createInstructionFactoryFromName(String insnName, Map varNames) { + //Check if it is a var insn (in the format " {}") + for (int i = 0; i < VAR_INSNS.length; i++) { + if (insnName.startsWith(VAR_INSNS[i])) { + String varInsnName = VAR_INSNS[i]; + String varName = insnName.substring(varInsnName.length() + 2, insnName.length() - 1); + int varIndex = varNames.get(varName); + int opcode = VAR_OPCODES[i]; + + //The actual variable slot can be fetched from the variable indices array + return (insnList, variableIndices) -> insnList.add(new VarInsnNode(opcode, variableIndices[varIndex])); + } + } + + int opcode = opcodeFromName(insnName); + + return (insnList, __) -> insnList.add(new InsnNode(opcode)); + } + + private int opcodeFromName(String name) { + //Since simple instructions have no parameters we just need the opcode + + return switch (name) { + case "NOP" -> NOP; + case "ACONST_NULL" -> ACONST_NULL; + case "ICONST_M1" -> ICONST_M1; + case "ICONST_0" -> ICONST_0; + case "ICONST_1" -> ICONST_1; + case "ICONST_2" -> ICONST_2; + case "ICONST_3" -> ICONST_3; + case "ICONST_4" -> ICONST_4; + case "ICONST_5" -> ICONST_5; + case "LCONST_0" -> LCONST_0; + case "LCONST_1" -> LCONST_1; + case "FCONST_0" -> FCONST_0; + case "FCONST_1" -> FCONST_1; + case "FCONST_2" -> FCONST_2; + case "DCONST_0" -> DCONST_0; + case "DCONST_1" -> DCONST_1; + case "IALOAD" -> IALOAD; + case "LALOAD" -> LALOAD; + case "FALOAD" -> FALOAD; + case "DALOAD" -> DALOAD; + case "AALOAD" -> AALOAD; + case "BALOAD" -> BALOAD; + case "CALOAD" -> CALOAD; + case "SALOAD" -> SALOAD; + case "IASTORE" -> IASTORE; + case "LASTORE" -> LASTORE; + case "FASTORE" -> FASTORE; + case "DASTORE" -> DASTORE; + case "AASTORE" -> AASTORE; + case "BASTORE" -> BASTORE; + case "CASTORE" -> CASTORE; + case "SASTORE" -> SASTORE; + case "POP" -> POP; + case "POP2" -> POP2; + case "DUP" -> DUP; + case "DUP_X1" -> DUP_X1; + case "DUP_X2" -> DUP_X2; + case "DUP2" -> DUP2; + case "DUP2_X1" -> DUP2_X1; + case "DUP2_X2" -> DUP2_X2; + case "SWAP" -> SWAP; + case "IADD" -> IADD; + case "LADD" -> LADD; + case "FADD" -> FADD; + case "DADD" -> DADD; + case "ISUB" -> ISUB; + case "LSUB" -> LSUB; + case "FSUB" -> FSUB; + case "DSUB" -> DSUB; + case "IMUL" -> IMUL; + case "LMUL" -> LMUL; + case "FMUL" -> FMUL; + case "DMUL" -> DMUL; + case "IDIV" -> IDIV; + case "LDIV" -> LDIV; + case "FDIV" -> FDIV; + case "DDIV" -> DDIV; + case "IREM" -> IREM; + case "LREM" -> LREM; + case "FREM" -> FREM; + case "DREM" -> DREM; + case "INEG" -> INEG; + case "LNEG" -> LNEG; + case "FNEG" -> FNEG; + case "DNEG" -> DNEG; + case "ISHL" -> ISHL; + case "LSHL" -> LSHL; + case "ISHR" -> ISHR; + case "LSHR" -> LSHR; + case "IUSHR" -> IUSHR; + case "LUSHR" -> LUSHR; + case "IAND" -> IAND; + case "LAND" -> LAND; + case "IOR" -> IOR; + case "LOR" -> LOR; + case "IXOR" -> IXOR; + case "LXOR" -> LXOR; + case "I2L" -> I2L; + case "I2F" -> I2F; + case "I2D" -> I2D; + case "L2I" -> L2I; + case "L2F" -> L2F; + case "L2D" -> L2D; + case "F2I" -> F2I; + case "F2L" -> F2L; + case "F2D" -> F2D; + case "D2I" -> D2I; + case "D2L" -> D2L; + case "D2F" -> D2F; + case "I2B" -> I2B; + case "I2C" -> I2C; + case "I2S" -> I2S; + case "LCMP" -> LCMP; + case "FCMPL" -> FCMPL; + case "FCMPG" -> FCMPG; + case "DCMPL" -> DCMPL; + case "DCMPG" -> DCMPG; + case "IRETURN" -> IRETURN; + case "LRETURN" -> LRETURN; + case "FRETURN" -> FRETURN; + case "DRETURN" -> DRETURN; + case "ARETURN" -> ARETURN; + case "RETURN" -> RETURN; + case "ARRAYLENGTH" -> ARRAYLENGTH; + case "ATHROW" -> ATHROW; + case "MONITORENTER" -> MONITORENTER; + case "MONITOREXIT" -> MONITOREXIT; + default -> throw new IllegalArgumentException("Error when reading JSON bytecode. Unknown instruction '" + name + "'"); + }; + } + + @Override + public InsnList generate(Function varAllocator) { + //When generating the instruction list, we first allocate all the variables and create the variable index array + + int[] vars = new int[this.varTypes.size()]; + + for (int i = 0; i < this.varTypes.size(); i++) { + vars[i] = varAllocator.apply(this.varTypes.get(i)); + } + + InsnList insnList = new InsnList(); + + for (var generator : instructionGenerators) { + generator.accept(insnList, vars); + } + + return insnList; + } +} diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/bytecodegen/package-info.java b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/bytecodegen/package-info.java new file mode 100644 index 000000000..52621a256 --- /dev/null +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/bytecodegen/package-info.java @@ -0,0 +1,7 @@ +@ParametersAreNonnullByDefault +@MethodsReturnNonnullByDefault +package io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.bytecodegen; + +import javax.annotation.ParametersAreNonnullByDefault; + +import io.github.opencubicchunks.cc_core.annotation.MethodsReturnNonnullByDefault; diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/CCSynthetic.java b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/CCSynthetic.java new file mode 100644 index 000000000..9afa1d80c --- /dev/null +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/CCSynthetic.java @@ -0,0 +1,16 @@ +package io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * This annotation is generated through ASM and should not be added to methods manually. It tracks which methods are added through ASM and how they were made. + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface CCSynthetic { + String type(); + String original(); +} diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/TypeTransformer.java b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/TypeTransformer.java new file mode 100644 index 000000000..94ab68a20 --- /dev/null +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/TypeTransformer.java @@ -0,0 +1,1999 @@ +package io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.lang.reflect.Method; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; + +import io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.bytecodegen.BytecodeFactory; +import io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.analysis.AnalysisResults; +import io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.analysis.DerivedTransformType; +import io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.analysis.FutureMethodBinding; +import io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.analysis.TransformTrackingInterpreter; +import io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.analysis.TransformTrackingValue; +import io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.config.ClassTransformInfo; +import io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.config.Config; +import io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.config.ConstructorReplacer; +import io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.config.MethodParameterInfo; +import io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.config.MethodReplacement; +import io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.config.TransformType; +import io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.config.TypeInfo; +import io.github.opencubicchunks.cubicchunks.mixin.transform.util.ASMUtil; +import io.github.opencubicchunks.cubicchunks.mixin.transform.util.AncestorHashMap; +import io.github.opencubicchunks.cubicchunks.mixin.transform.util.FieldID; +import io.github.opencubicchunks.cubicchunks.mixin.transform.util.MethodID; +import io.github.opencubicchunks.cubicchunks.utils.TestMappingUtils; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import org.jetbrains.annotations.Nullable; +import org.objectweb.asm.Handle; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.AnnotationNode; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.FieldInsnNode; +import org.objectweb.asm.tree.FieldNode; +import org.objectweb.asm.tree.IincInsnNode; +import org.objectweb.asm.tree.InsnList; +import org.objectweb.asm.tree.InsnNode; +import org.objectweb.asm.tree.InvokeDynamicInsnNode; +import org.objectweb.asm.tree.JumpInsnNode; +import org.objectweb.asm.tree.LabelNode; +import org.objectweb.asm.tree.LdcInsnNode; +import org.objectweb.asm.tree.LineNumberNode; +import org.objectweb.asm.tree.LocalVariableNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; +import org.objectweb.asm.tree.MultiANewArrayInsnNode; +import org.objectweb.asm.tree.ParameterNode; +import org.objectweb.asm.tree.TypeInsnNode; +import org.objectweb.asm.tree.VarInsnNode; +import org.objectweb.asm.tree.analysis.AnalyzerException; +import org.objectweb.asm.tree.analysis.Frame; + +/** + * This class is responsible for transforming the methods and fields of a single class according to the configuration. See {@link + * io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.config.ConfigLoader} + *

+ * Definitions: + *
    Emitter: Any instruction that pushes one or more values onto the stack
+ *
    Consumer: Any instruction that pops one or more values from the stack
+ */ +public class TypeTransformer { + public static final boolean VERBOSE = true; + //Postfix that gets appended to some names to prevent conflicts + public static final String MIX = "$$cc_transformed"; + //A value that should be passed to transformed constructors. Any other value will cause an error + public static final int MAGIC = 0xDEADBEEF; + //When safety is enabled, if a long-pos method is called for a 3-int object a warning will be created. This keeps track of all warnings. + private static final Set WARNINGS = new HashSet<>(); + //Path to file where errors should be logged + private static final Path ERROR_LOG = TestMappingUtils.getGameDir().resolve("errors.log"); + + private static final Map> CC_SYNTHETIC_LOOKUP = new HashMap<>(); + + //The global configuration loaded by ConfigLoader + private final Config config; + //The original class node + private final ClassNode classNode; + //Stores all the analysis results per method + private final Map analysisResults = new HashMap<>(); + //Keeps track of bindings to un-analyzed methods + private final Map> futureMethodBindings = new HashMap<>(); + //Stores values for each field in the class. These can be bound (set same type) like any other values and allows + //for easy tracking of the transform-type of a field + private final AncestorHashMap fieldPseudoValues; + //Per-class configuration + private final ClassTransformInfo transformInfo; + private final boolean inPlace; + //The field ID (owner, name, desc) of a field which stores whether an instance was created with a transformed constructor and has transformed fields + private FieldID isTransformedField; + private boolean hasTransformedFields; + //Whether safety checks/dispatches/warnings should be inserted into the code. + private final boolean addSafety; + //Stores the lambdaTransformers that need to be added + private final Set lambdaTransformers = new HashSet<>(); + //Stores any other methods that need to be added. There really isn't much of a reason for these two to be separate. + private final Set newMethods = new HashSet<>(); + + private final Map externalMethodReplacements = new HashMap<>(); + + + /** + * Constructs a new TypeTransformer for a given class. + * + * @param config The global configuration loaded by ConfigLoader + * @param classNode The original class node + * @param addSafety Whether safety checks/dispatches/warnings should be inserted into the code. + */ + public TypeTransformer(Config config, ClassNode classNode, boolean addSafety) { + this.config = config; + this.classNode = classNode; + this.fieldPseudoValues = new AncestorHashMap<>(config.getTypeInfo()); + this.addSafety = addSafety; + + //Create field pseudo values + for (var field : classNode.fields) { + TransformTrackingValue value = new TransformTrackingValue(Type.getType(field.desc), fieldPseudoValues, config); + fieldPseudoValues.put(FieldID.of(classNode.name, field), value); + } + + //Extract per-class config from the global config + this.transformInfo = config.getClasses().get(Type.getObjectType(classNode.name)); + + if (transformInfo != null) { + this.inPlace = transformInfo.isInPlace(); + } else { + this.inPlace = false; + } + } + + // ANALYSIS + + /** + * Analyzes every method (except {@code } and {@code }) in the class and stores the results + */ + public void analyzeAllMethods() { + long startTime = System.currentTimeMillis(); + for (MethodNode methodNode : classNode.methods) { + if ((methodNode.access & Opcodes.ACC_NATIVE) != 0) { + throw new IllegalStateException("Cannot analyze/transform native methods"); + } + + if ((methodNode.name.equals("") || methodNode.name.equals("")) && !inPlace) { + continue; + } + + if ((methodNode.access & Opcodes.ACC_ABSTRACT) != 0) { + addDummyValues(methodNode); + + continue; + } + analyzeMethod(methodNode); + } + + cleanUpAnalysis(); + + if (VERBOSE) { + printAnalysisResults(); + } + + System.out.println("Finished analysis of " + classNode.name + " in " + (System.currentTimeMillis() - startTime) + "ms"); + } + + private void printAnalysisResults() { + for (AnalysisResults results : analysisResults.values()) { + results.print(System.out, false); + } + + System.out.println("\nField Transforms:"); + + for (var entry : fieldPseudoValues.entrySet()) { + if (entry.getValue().getTransformType() == null) { + System.out.println(entry.getKey() + ": [NO CHANGE]"); + } else { + System.out.println(entry.getKey() + ": " + entry.getValue().getTransformType()); + } + } + } + + public void analyzeMethod(String name, String desc) { + MethodNode method = classNode.methods.stream().filter(m -> m.name.equals(name) && m.desc.equals(desc)).findFirst().orElse(null); + + if (method == null) { + throw new IllegalStateException("Method " + name + desc + " not found in class " + classNode.name); + } + + analyzeMethod(method); + } + + /** + * Analyzes a single method and stores the results + * + * @param methodNode The method to analyze + */ + public AnalysisResults analyzeMethod(MethodNode methodNode) { + long startTime = System.currentTimeMillis(); + config.getInterpreter().reset(); //Clear all info stored about previous methods + config.getInterpreter().setResultLookup(analysisResults); + config.getInterpreter().setFutureBindings(futureMethodBindings); + config.getInterpreter().setCurrentClass(classNode); + config.getInterpreter().setFieldBindings(fieldPseudoValues); + + MethodID methodID = MethodID.of(classNode, methodNode); + + //Get any type hints for this method + List typeHints; + if (transformInfo != null) { + typeHints = transformInfo.getTypeHints().get(methodID); + } else { + typeHints = null; + } + + if (typeHints != null) { + //Set the type hints + config.getInterpreter().setLocalVarOverrides(methodID, typeHints); + } + + try { + var frames = config.getAnalyzer().analyze(classNode.name, methodNode); + AnalysisResults results = new AnalysisResults(methodNode, frames); + analysisResults.put(methodID, results); + + //Bind previous calls + for (FutureMethodBinding binding : futureMethodBindings.getOrDefault(methodID, List.of())) { + TransformTrackingInterpreter.bindValuesToMethod(results, binding.offset(), binding.parameters()); + } + + System.out.println("Analyzed method " + methodID + " in " + (System.currentTimeMillis() - startTime) + "ms"); + + return results; + } catch (AnalyzerException e) { + throw new RuntimeException("Analysis failed for method " + methodNode.name, e); + } + } + + private void addDummyValues(MethodNode methodNode) { + //We still want to infer the argument types of abstract methods, so we create a single frame whose locals represent the arguments + Type[] args = Type.getArgumentTypes(methodNode.desc); + + MethodID methodID = new MethodID(classNode.name, methodNode.name, methodNode.desc, MethodID.CallType.VIRTUAL); + + var typeHints = transformInfo.getTypeHints().get(methodID); + + DerivedTransformType[] argTransformKinds = new DerivedTransformType[args.length]; + int index = 1; //Abstract methods can't be static, so they have the 'this' argument + for (int i = 0; i < args.length; i++) { + argTransformKinds[i] = DerivedTransformType.createDefault(args[i]); + + if (typeHints != null && typeHints.size() > index && typeHints.get(index) != null) { + argTransformKinds[i] = DerivedTransformType.of(typeHints.get(index)); + } + + index += args[i].getSize(); + } + + Frame[] frames = new Frame[1]; + + int numLocals = methodID.getCallType().getOffset(); + for (Type argType : args) { + numLocals += argType.getSize(); + } + frames[0] = new Frame<>(numLocals, 0); + + frames[0].setLocal(0, new TransformTrackingValue(Type.getObjectType(classNode.name), fieldPseudoValues, config)); + + int varIndex = 1; + + for (int i = 0; i < args.length; i++) { + Type argType = args[i]; + + DerivedTransformType copyFrom = argTransformKinds[i]; + TransformTrackingValue value = new TransformTrackingValue(argType, fieldPseudoValues, config); + value.getTransform().setArrayDimensionality(copyFrom.getArrayDimensionality()); + value.getTransform().setKind(copyFrom.getKind()); + value.setTransformType(copyFrom.getTransformType()); + frames[0].setLocal(varIndex, value); + varIndex += argType.getSize(); + } + + AnalysisResults results = new AnalysisResults(methodNode, frames); + analysisResults.put(methodID, results); + + //Bind previous calls + for (FutureMethodBinding binding : futureMethodBindings.getOrDefault(methodID, List.of())) { + TransformTrackingInterpreter.bindValuesToMethod(results, binding.offset(), binding.parameters()); + } + } + + /** + * Must be called after all analysis and before all transformations + */ + public void cleanUpAnalysis() { + //Check for transformed fields + for (var entry : fieldPseudoValues.entrySet()) { + if (entry.getValue().getTransformType() != null) { + hasTransformedFields = true; + break; + } + } + + //Add safety field if necessary + if (hasTransformedFields && !inPlace) { + addSafetyField(); + } + } + + /** + * Creates a boolean field named isTransformed that stores whether the fields of the class have transformed types + */ + private void addSafetyField() { + isTransformedField = new FieldID(Type.getObjectType(classNode.name), "isTransformed" + MIX, Type.BOOLEAN_TYPE); + classNode.fields.add(isTransformedField.toNode(false, Opcodes.ACC_FINAL)); + } + + + // TRANSFORMATION + + public void transformAllMethods() { + int size = classNode.methods.size(); + for (int i = 0; i < size; i++) { + MethodNode methodNode = classNode.methods.get(i); + if (!methodNode.name.equals("") && !methodNode.name.equals("") || inPlace) { + try { + generateTransformedMethod(methodNode); + } catch (Exception e) { + throw new RuntimeException("Failed to transform method " + methodNode.name + methodNode.desc, e); + } + } + } + + cleanUpTransform(); + } + + public void generateTransformedMethod(String name, String desc) { + MethodNode methodNode = classNode.methods.stream().filter(m -> m.name.equals(name) && m.desc.equals(desc)).findAny().orElse(null); + if (methodNode == null) { + throw new RuntimeException("Method " + name + desc + " not found in class " + classNode.name); + } + try { + generateTransformedMethod(methodNode); + } catch (Exception e) { + throw new RuntimeException("Failed to transform method " + name + desc, e); + } + } + + /** + * Creates a copy of the method and transforms it according to the config. This method then gets added to the necessary class. The main goal of this method is to create the transform + * context. It then passes that on to the necessary methods. This method does not modify the method much. + * + * @param methodNode The method to transform. + */ + public void generateTransformedMethod(MethodNode methodNode) { + long start = System.currentTimeMillis(); + + //Look up the analysis results for this method + MethodID methodID = new MethodID(classNode.name, methodNode.name, methodNode.desc, MethodID.CallType.VIRTUAL); //Call type doesn't matter much + AnalysisResults results = analysisResults.get(methodID); + + if (results == null) { + throw new RuntimeException("Method " + methodID + " not analyzed"); + } + + //Create the new method node + MethodNode newMethod = ASMUtil.copy(methodNode); + if (this.config.getTypesWithSuffixedTransforms().contains(methodID.getOwner())) { + newMethod.name += MIX; + } + //Add it to newMethods so that it gets added later and doesn't cause a ConcurrentModificationException if iterating over the methods. + newMethods.add(newMethod); + + //See TransformContext + AbstractInsnNode[] insns = newMethod.instructions.toArray(); + int[] vars = new int[newMethod.maxLocals]; + DerivedTransformType[][] varTypes = new DerivedTransformType[insns.length][newMethod.maxLocals]; + + //Generate var table + int[] maxVarWidth = new int[newMethod.maxLocals]; + Arrays.fill(maxVarWidth, 0); + + for (int i = 0; i < insns.length; i++) { + Frame frame = results.frames()[i]; + if (frame == null) continue; + + for (int j = 0; j < newMethod.maxLocals; j += frame.getLocal(j).getSize()) { + TransformTrackingValue local = frame.getLocal(j); + maxVarWidth[j] = Math.max(maxVarWidth[j], local.getTransformedSize()); + varTypes[i][j] = local.getTransform(); + } + } + + int totalSize = 0; + for (int i = 0; i < newMethod.maxLocals; i++) { + vars[i] = totalSize; + totalSize += maxVarWidth[i]; + } + + VariableAllocator varCreator = new VariableAllocator(totalSize, insns.length); + + //Analysis results come from the original method, and we need to transform the new method, so we need to be able to get the new instructions that correspond to the old ones + Map indexLookup = new HashMap<>(); + + AbstractInsnNode[] oldInsns = newMethod.instructions.toArray(); + + for (int i = 0; i < oldInsns.length; i++) { + indexLookup.put(insns[i], i); + indexLookup.put(oldInsns[i], i); + } + + AbstractInsnNode[] instructions = newMethod.instructions.toArray(); + Frame[] frames = results.frames(); + + //Resolve the method parameter infos + MethodParameterInfo[] methodInfos = new MethodParameterInfo[insns.length]; + getAllMethodInfo(insns, instructions, frames, methodInfos); + + //Create context + TransformContext context = new TransformContext(newMethod, results, instructions, vars, varTypes, varCreator, indexLookup, methodInfos); + + if ((newMethod.access & Opcodes.ACC_ABSTRACT) != 0) { + transformAbstractMethod(newMethod, start, methodID, newMethod, context); + } else { + generateTransformedMethod(methodNode, newMethod, context); + + markSynthetic(newMethod, "AUTO-TRANSFORMED", methodNode.name + methodNode.desc, classNode.name); + } + + System.out.println("Transformed method '" + methodID + "' in " + (System.currentTimeMillis() - start) + "ms"); + } + + /** + * Actually modifies the method + * + * @param oldMethod The original method, may be modified for safety checks + * @param methodNode The method to modify + * @param context Transform context + */ + private void generateTransformedMethod(MethodNode oldMethod, MethodNode methodNode, TransformContext context) { + transformDesc(methodNode, context); + + //Change variable names to make it easier to debug + modifyLVT(methodNode, context); + + //Change the code + modifyCode(context); + + if (!ASMUtil.isStatic(methodNode)) { + if (addSafety && (methodNode.access & Opcodes.ACC_SYNTHETIC) == 0) { + //This can be disabled by setting addSafety to false in the constructor + //but this means that if a single piece of code calls the wrong method then everything could crash. + InsnList dispatch = new InsnList(); + LabelNode label = new LabelNode(); + + dispatch.add(jumpIfNotTransformed(label)); + + if (!oldMethod.desc.equals(methodNode.desc)) { + dispatch.add(generateEmitWarningCall("Incorrect Invocation of " + classNode.name + "." + oldMethod.name + oldMethod.desc, 3)); + } + + //Push all the parameters onto the stack and transform them if needed + dispatch.add(new VarInsnNode(Opcodes.ALOAD, 0)); + int index = 1; + for (Type arg : Type.getArgumentTypes(oldMethod.desc)) { + DerivedTransformType argType = context.varTypes[0][index]; + int finalIndex = index; + dispatch.add(argType.convertToTransformed(() -> { + InsnList load = new InsnList(); + load.add(new VarInsnNode(arg.getOpcode(Opcodes.ILOAD), finalIndex)); + return load; + }, lambdaTransformers, classNode.name)); + index += arg.getSize(); + } + + dispatch.add(new MethodInsnNode(Opcodes.INVOKESPECIAL, classNode.name, methodNode.name, methodNode.desc, false)); + dispatch.add(new InsnNode(Type.getReturnType(methodNode.desc).getOpcode(Opcodes.IRETURN))); + + dispatch.add(label); + + oldMethod.instructions.insertBefore(oldMethod.instructions.getFirst(), dispatch); + } + } + } + + /** + * Modifies the code of the method to use the transformed types instead of the original types + * + * @param context The context of the transformation + */ + private void modifyCode(TransformContext context) { + AbstractInsnNode[] instructions = context.instructions(); + Frame[] frames = context.analysisResults().frames(); + + //Iterate through every instruction of the instructions array. We use the array because it will not change unlike methodNode.instructions + for (int i = 0; i < instructions.length; i++) { + try { + AbstractInsnNode instruction = instructions[i]; + Frame frame = frames[i]; + + dispatchInstructionTransform(context, frames, i, instruction, frame); + } catch (Exception e) { + throw new RuntimeException("Error transforming instruction #" + i + ": " + ASMUtil.textify(instructions[i]), e); + } + } + } + + private void dispatchInstructionTransform(TransformContext context, Frame[] frames, int i, AbstractInsnNode instruction, Frame frame) { + int opcode = instruction.getOpcode(); + + if (instruction instanceof MethodInsnNode methodCall) { + transformMethodCall(context, frames, i, frame, methodCall); + } else if (instruction instanceof VarInsnNode varNode) { + transformVarInsn(context, i, varNode); + } else if (instruction instanceof IincInsnNode iincNode) { + transformIincInsn(context, iincNode); + } else if (ASMUtil.isConstant(instruction)) { + transformConstantInsn(context, frames[i + 1], i, instruction); + } else if (isACompare(opcode)) { + transformCmp(context, i, instruction, frame, opcode); + } else if (instruction instanceof InvokeDynamicInsnNode dynamicInsnNode) { + transformInvokeDynamicInsn(frames, i, frame, dynamicInsnNode); + } else if (opcode == Opcodes.NEW) { + transformNewInsn(frames[i + 1], (TypeInsnNode) instruction); + } else if (opcode == Opcodes.ANEWARRAY || opcode == Opcodes.NEWARRAY) { + transformNewArray(context, i, frames[i + 1], instruction, 1); + } else if (instruction instanceof MultiANewArrayInsnNode arrayInsn) { + transformNewArray(context, i, frames[i + 1], instruction, arrayInsn.dims); + } else if (isArrayLoad(instruction.getOpcode())) { + transformArrayLoad(context, i, instruction, frame); + } else if (isArrayStore(instruction.getOpcode())) { + transformArrayStore(context, i, instruction, frame); + } + + if (inPlace) { + if (opcode == Opcodes.GETSTATIC || opcode == Opcodes.PUTSTATIC || opcode == Opcodes.GETFIELD || opcode == Opcodes.PUTFIELD) { + transformFieldInsn(context, i, (FieldInsnNode) instruction); + } + } + } + + private void transformMethodCall(TransformContext context, Frame[] frames, int insnIdx, Frame frame, MethodInsnNode methodCall) { + MethodID methodID = MethodID.from(methodCall); + + //Get the return value (if it exists). It is on the top of the stack if the next frame + TransformTrackingValue returnValue = null; + if (methodID.getDescriptor().getReturnType() != Type.VOID_TYPE) { + returnValue = ASMUtil.getTop(frames[insnIdx + 1]); + } + + //Get all the values that are passed to the method call + int argCount = ASMUtil.argumentCount(methodID.getDescriptor().getDescriptor(), methodID.isStatic()); + TransformTrackingValue[] args = new TransformTrackingValue[argCount]; + for (int j = 0; j < args.length; j++) { + args[j] = frame.getStack(frame.getStackSize() - argCount + j); + } + + //Find replacement information for the method call + MethodParameterInfo info = context.methodInfos[insnIdx]; + + if (info != null && info.getReplacement() != null) { + applyReplacement(context, methodCall, info, args); + } else { + //If there is none, we create a default transform + if (returnValue != null && returnValue.getTransform().resultingTypes().size() > 1) { + throw new IllegalStateException("Cannot generate default replacement for method with multiple return types '" + methodID + "'"); + } + + applyDefaultReplacement(context, insnIdx, methodCall, returnValue, args); + } + } + + /** + * Transform a method call whose replacement is given in the config + * + * @param context Transform context + * @param methodCall The actual method call insn + * @param info The replacement to apply + * @param args The arguments of the method call. This should include the instance ('this') if it is a non-static method + */ + private void applyReplacement(TransformContext context, MethodInsnNode methodCall, MethodParameterInfo info, TransformTrackingValue[] args) { + MethodReplacement replacement = info.getReplacement(); + + if (!replacement.changeParameters() && info.getReturnType().resultingTypes().size() > 1) { + throw new IllegalStateException("Multiple return types not supported"); + } + + int insnIndex = context.indexLookup.get(methodCall); + + //Store all the parameters + InsnList replacementInstructions = new InsnList(); + + int totalSize = 0; + int[][] offsets = new int[args.length][]; + + for (int i = 0; i < args.length; i++) { + DerivedTransformType transform = args[i].getTransform(); + offsets[i] = transform.getIndices(); + + for (int j = 0; j < offsets[i].length; j++) { + offsets[i][j] += totalSize; + } + + totalSize += transform.getTransformType() == null ? args[i].getSize() : transform.getTransformedSize(); + } + + int baseIdx = context.variableAllocator.allocate(insnIndex, insnIndex + 1, totalSize); + + for (int i = args.length - 1; i >= 0; i--) { + storeStackInLocals(args[i].getTransform(), replacementInstructions, baseIdx + offsets[i][0]); + } + + for (int i = 0; i < replacement.getBytecodeFactories().length; i++) { + BytecodeFactory factory = replacement.getBytecodeFactories()[i]; + List[] indices = replacement.getParameterIndices()[i]; + + loadIndices(args, replacementInstructions, offsets, baseIdx, indices); + replacementInstructions.add( + factory.generate(s -> context.variableAllocator.allocate(insnIndex, insnIndex + 1, s)) + ); + } + + if (replacement.getFinalizer() != null) { + loadIndices(args, replacementInstructions, offsets, baseIdx, replacement.getFinalizerIndices()); + replacementInstructions.add( + replacement.getFinalizer().generate(s -> context.variableAllocator.allocate(insnIndex, insnIndex + 1, s)) + ); + } + + //Insert new code + context.target().instructions.insert(methodCall, replacementInstructions); + context.target().instructions.remove(methodCall); + } + + /** + * Transform a method call with which doesn't have a provided replacement. This is done by getting the transformed type of every value that is passed to the method and changing the + * descriptor so as to match that. It will assume that this method exists. + * + * @param context Transform context + * @param methodCall The actual method call + * @param returnValue The return value of the method call, if the method returns void this should be null + * @param args The arguments of the method call. This should include the instance ('this') if it is a non-static method + */ + private void applyDefaultReplacement(TransformContext context, int i, MethodInsnNode methodCall, @Nullable TransformTrackingValue returnValue, TransformTrackingValue[] args) { + //Special case Arrays.fill + if (methodCall.owner.equals("java/util/Arrays") && methodCall.name.equals("fill")) { + transformArraysFill(context, i, methodCall, args); + return; + } + + //Get the actual values passed to the method. If the method is not static then the first value is the instance + boolean isStatic = (methodCall.getOpcode() == Opcodes.INVOKESTATIC); + int staticOffset = isStatic ? 0 : 1; + + DerivedTransformType returnType = DerivedTransformType.createDefault(Type.getReturnType(methodCall.desc)); + DerivedTransformType[] argTypes = new DerivedTransformType[args.length - staticOffset]; + + if (returnValue != null) { + returnType = returnValue.getTransform(); + } + + for (int j = staticOffset; j < args.length; j++) { + argTypes[j - staticOffset] = args[j].getTransform(); + } + + //Create the new descriptor + methodCall.desc = MethodParameterInfo.getNewDesc(returnType, argTypes, methodCall.desc); + + addSuffixIfNeeded(methodCall, args, staticOffset); + + if (!isStatic) { + updateMethodOwner(methodCall, args); + } + } + + private void addSuffixIfNeeded(MethodInsnNode methodCall, TransformTrackingValue[] args, int staticOffset) { + Type methodOwner = Type.getObjectType(methodCall.owner); + if (this.config.getTypesWithSuffixedTransforms().contains(methodOwner)) { + if (args.length == staticOffset) { + methodCall.name += MIX; + } else { + for (TransformTrackingValue arg : args) { + if (arg.getTransformType() != null) { + methodCall.name += MIX; + break; + } + } + } + } + } + + private void updateMethodOwner(MethodInsnNode methodCall, TransformTrackingValue[] args) { + List types = args[0].transformedTypes(); + if (types.size() != 1) { + throw new IllegalStateException( + "Expected 1 type but got " + types.size() + ". Define a custom replacement for this method (" + methodCall.owner + "#" + methodCall.name + methodCall.desc + ")"); + } + + TypeInfo hierarchy = config.getTypeInfo(); + + Type potentialOwner = types.get(0); + if (methodCall.getOpcode() != Opcodes.INVOKESPECIAL) { + findOwnerNormal(methodCall, hierarchy, potentialOwner); + } else { + findOwnerInvokeSpecial(methodCall, args, hierarchy, potentialOwner); + } + } + + private void transformArraysFill(TransformContext context, int i, MethodInsnNode methodCall, TransformTrackingValue[] args) { + TransformTrackingValue fillWith = args[1]; + TransformTrackingValue array = args[0]; + + if (!array.isTransformed()) return; + + int arrayBase = context.variableAllocator.allocate(i, i + 1, array.getTransformedSize()); + int fillWithBase = context.variableAllocator.allocate(i, i + 1, fillWith.getTransformedSize()); + + int[] arrayOffsets = array.getTransform().getIndices(); + int[] fillWithOffsets = fillWith.getTransform().getIndices(); + + List arrayTypes = array.getTransform().resultingTypes(); + List fillWithTypes = fillWith.getTransform().resultingTypes(); + + InsnList replacement = new InsnList(); + + storeStackInLocals(fillWith.getTransform(), replacement, fillWithBase); + storeStackInLocals(array.getTransform(), replacement, arrayBase); + + for (int j = 0; j < arrayOffsets.length; j++) { + replacement.add(new VarInsnNode(arrayTypes.get(j).getOpcode(Opcodes.ILOAD), arrayBase + arrayOffsets[j])); + replacement.add(new VarInsnNode(fillWithTypes.get(j).getOpcode(Opcodes.ILOAD), fillWithBase + fillWithOffsets[j])); + + Type baseType = simplify(fillWithTypes.get(j)); + Type arrayType = Type.getType("[" + baseType.getDescriptor()); + + replacement.add(new MethodInsnNode( + Opcodes.INVOKESTATIC, + "java/util/Arrays", + "fill", + Type.getMethodType(Type.VOID_TYPE, arrayType, baseType).getDescriptor(), + false + )); + } + + context.target.instructions.insert(methodCall, replacement); + context.target.instructions.remove(methodCall); + } + + private void findOwnerNormal(MethodInsnNode methodCall, TypeInfo hierarchy, Type potentialOwner) { + int opcode = methodCall.getOpcode(); + + if (opcode == Opcodes.INVOKEVIRTUAL || opcode == Opcodes.INVOKEINTERFACE) { + if (!potentialOwner.equals(Type.getObjectType(methodCall.owner))) { + boolean isNewTypeInterface = hierarchy.recognisesInterface(potentialOwner); + opcode = isNewTypeInterface ? Opcodes.INVOKEINTERFACE : Opcodes.INVOKEVIRTUAL; + + methodCall.itf = isNewTypeInterface; + } + } + + methodCall.owner = potentialOwner.getInternalName(); + methodCall.setOpcode(opcode); + } + + private void findOwnerInvokeSpecial(MethodInsnNode methodCall, TransformTrackingValue[] args, TypeInfo hierarchy, Type potentialOwner) { + String currentOwner = methodCall.owner; + TypeInfo.Node current = hierarchy.getNode(Type.getObjectType(currentOwner)); + TypeInfo.Node potential = hierarchy.getNode(potentialOwner); + TypeInfo.Node given = hierarchy.getNode(args[0].getType()); + + if (given == null || current == null) { + System.err.println("Don't have hierarchy for " + args[0].getType() + " or " + methodCall.owner); + methodCall.owner = potentialOwner.getInternalName(); + } else if (given.isDirectDescendantOf(current)) { + if (potential == null || potential.getSuperclass() == null) { + throw new IllegalStateException("Cannot change owner of super call if hierarchy for " + potentialOwner + " is not defined"); + } + + Type newOwner = potential.getSuperclass().getValue(); + methodCall.owner = newOwner.getInternalName(); + } else { + methodCall.owner = potentialOwner.getInternalName(); + } + } + + private void transformVarInsn(TransformContext context, int insnIdx, VarInsnNode varNode) { + /* + * There are two reasons this is needed. + * 1. Some values take up different amount of variable slots because of their transforms, so we need to shift all variables accesses' + * 2. When actually storing or loading a transformed value, we need to store all of it's transformed values correctly + */ + + //Get the shifted variable index + int originalVarIndex = varNode.var; + int newVarIndex = context.varLookup()[originalVarIndex]; + + //Base opcode makes it easier to determine what kind of instruction we are dealing with + int baseOpcode = switch (varNode.getOpcode()) { + case Opcodes.ALOAD, Opcodes.ILOAD, Opcodes.FLOAD, Opcodes.DLOAD, Opcodes.LLOAD -> Opcodes.ILOAD; + case Opcodes.ASTORE, Opcodes.ISTORE, Opcodes.FSTORE, Opcodes.DSTORE, Opcodes.LSTORE -> Opcodes.ISTORE; + default -> throw new IllegalStateException("Unknown opcode: " + varNode.getOpcode()); + }; + + //If the variable is being loaded, it is in the current frame, if it is being stored, it will be in the next frame + DerivedTransformType varType = context.varTypes()[insnIdx + (baseOpcode == Opcodes.ISTORE ? 1 : 0)][originalVarIndex]; + //Get the actual types that need to be stored or loaded + List types = varType.resultingTypes(); + + //Get the indices for each of these types + List vars = new ArrayList<>(); + for (Type type : types) { + vars.add(newVarIndex); + newVarIndex += type.getSize(); + } + + /* + * If the variable is being stored we must reverse the order of the types. + * This is because in the following code if a and b have transform-type long -> (int "x", int "y", int "z"): + * + * long b = a; + * + * The loading of a would get expanded to something like: + * ILOAD 3 Stack: [] -> [a_x] + * ILOAD 4 Stack: [a_x] -> [a_x, a_y] + * ILOAD 5 Stack: [a_x, a_y] -> [a_x, a_y, a_z] + * + * If the storing into b was in the same order it would be: + * ISTORE 3 Stack: [a_x, a_y, a_z] -> [a_x, a_y] (a_z gets stored into b_x) + * ISTORE 4 Stack: [a_x, a_y] -> [a_x] (a_y gets stored into b_y) + * ISTORE 5 Stack: [a_x] -> [] (a_x gets stored into b_z) + * And so we see that this ordering is wrong. + * + * To fix this, we reverse the order of the types. + * The previous example becomes: + * ISTORE 5 Stack: [a_x, a_y, a_z] -> [a_x, a_y] (a_z gets stored into b_z) + * ISTORE 4 Stack: [a_x, a_y] -> [a_x] (a_y gets stored into b_y) + * ISTORE 3 Stack: [a_x] -> [] (a_x gets stored into b_x) + */ + if (baseOpcode == Opcodes.ISTORE) { + Collections.reverse(types); + Collections.reverse(vars); + } + + //For the first operation we can just modify the original instruction instead of creating more + varNode.var = vars.get(0); + varNode.setOpcode(types.get(0).getOpcode(baseOpcode)); + + InsnList extra = new InsnList(); + + for (int j = 1; j < types.size(); j++) { + extra.add(new VarInsnNode(types.get(j).getOpcode(baseOpcode), vars.get(j))); //Creating the new instructions + } + + context.target().instructions.insert(varNode, extra); + } + + private void transformIincInsn(TransformContext context, IincInsnNode iincNode) { + //We just need to shift the index of the variable because incrementing transformed values is not supported + int originalVarIndex = iincNode.var; + iincNode.var = context.varLookup()[originalVarIndex]; + } + + private void transformConstantInsn(TransformContext context, Frame nextFrame, int insnIndex, AbstractInsnNode instruction) { + //Check if value is transformed + TransformTrackingValue value = ASMUtil.getTop(nextFrame); + if (value.getTransformType() != null) { + if (value.getTransform().getKind() != DerivedTransformType.Kind.NONE) { + throw new IllegalStateException("Cannot expand constant value of kind " + value.getTransform().getKind()); + } + + Object constant = ASMUtil.getConstant(instruction); + + /* + * Check if there is a given constant replacement for this value an example of this is where Long.MAX_VALUE is used as a marker + * for an invalid position. To convert it to 3int we turn it into (Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE) + */ + BytecodeFactory[] replacement = value.getTransformType().getConstantReplacements().get(constant); + if (replacement == null) { + throw new IllegalStateException("Cannot expand constant value of kind " + value.getTransformType()); + } + + InsnList newInstructions = new InsnList(); + for (BytecodeFactory factory : replacement) { + newInstructions.add(factory.generate(t -> context.variableAllocator.allocate(insnIndex, insnIndex + 1, t))); + } + + context.target().instructions.insert(instruction, newInstructions); + context.target().instructions.remove(instruction); //Remove the original instruction + } + } + + private void transformCmp(TransformContext context, int insnIdx, AbstractInsnNode instruction, Frame frame, + int opcode) { + //Get the actual values that are being compared + TransformTrackingValue left = frame.getStack(frame.getStackSize() - 2); + TransformTrackingValue right = frame.getStack(frame.getStackSize() - 1); + + if (left.getTransformType() == null) return; //No transform needed + + if (!left.getTransform().equals(right.getTransform())) { + //Should be unreachable + throw new IllegalStateException("Expected same transform, found " + left.getTransform() + " and " + right.getTransform()); + } + + DerivedTransformType transformType = left.getTransform(); + List types = transformType.resultingTypes(); + int[] varOffsets = transformType.getIndices(); + int size = transformType.getTransformedSize(); + + InsnList list = new InsnList(); + + //Store both values on the stack into locals + int baseIdx = context.variableAllocator.allocate(insnIdx, insnIdx + 1, size * 2); + + int leftIdx = baseIdx; + int rightIdx = baseIdx + size; + + storeStackInLocals(transformType, list, rightIdx); + storeStackInLocals(transformType, list, leftIdx); + + if (opcode == Opcodes.LCMP || opcode == Opcodes.FCMPL || opcode == Opcodes.FCMPG || opcode == Opcodes.DCMPL || opcode == Opcodes.DCMPG) { + //TODO: For now this will return 0 if all the values are equal and 1 otherwise. Could be improved to allow for LE and GE + LabelNode escape = new LabelNode(); + + list.add(new LdcInsnNode(1)); + + for (int i = 0; i < types.size(); i++) { + list.add(new VarInsnNode(types.get(i).getOpcode(Opcodes.ILOAD), leftIdx + varOffsets[i])); + list.add(new VarInsnNode(types.get(i).getOpcode(Opcodes.ILOAD), rightIdx + varOffsets[i])); + + ASMUtil.jumpIfCmp(list, types.get(i), false, escape); + } + + list.add(new InsnNode(Opcodes.POP)); + list.add(new LdcInsnNode(0)); + list.add(escape); + } else { + JumpInsnNode jump = (JumpInsnNode) instruction; + + LabelNode target = jump.label; + LabelNode normal = new LabelNode(); + + boolean isEq = switch (opcode) { + case Opcodes.IF_ICMPEQ, Opcodes.IF_ACMPEQ -> true; + case Opcodes.IF_ICMPNE, Opcodes.IF_ACMPNE -> false; + default -> throw new IllegalStateException("Unexpected value: " + opcode); + }; + + for (int i = 0; i < types.size(); i++) { + list.add(new VarInsnNode(types.get(i).getOpcode(Opcodes.ILOAD), leftIdx + varOffsets[i])); + list.add(new VarInsnNode(types.get(i).getOpcode(Opcodes.ILOAD), rightIdx + varOffsets[i])); + + ASMUtil.jumpIfCmp(list, types.get(i), false, isEq ? normal : target); + } + } + + context.target().instructions.insert(instruction, list); + context.target().instructions.remove(instruction); + } + + private void transformInvokeDynamicInsn(Frame[] frames, int insnIdx, Frame frame, InvokeDynamicInsnNode dynamicInsnNode) { + //Check if it LambdaMetafactory.metafactory + if (dynamicInsnNode.bsm.getOwner().equals("java/lang/invoke/LambdaMetafactory")) { + Handle methodReference = (Handle) dynamicInsnNode.bsmArgs[1]; + boolean isStatic = methodReference.getTag() == Opcodes.H_INVOKESTATIC; + int staticOffset = isStatic ? 0 : 1; + + //Create new descriptor + Type[] args = Type.getArgumentTypes(dynamicInsnNode.desc); + TransformTrackingValue[] values = new TransformTrackingValue[args.length]; + for (int j = 0; j < values.length; j++) { + values[j] = frame.getStack(frame.getStackSize() - args.length + j); + } + + //The return value (the lambda) is on the top of the stack of the next frame + TransformTrackingValue returnValue = ASMUtil.getTop(frames[insnIdx + 1]); + + dynamicInsnNode.desc = MethodParameterInfo.getNewDesc(returnValue, values, dynamicInsnNode.desc); + + String methodName = methodReference.getName(); + String methodDesc = methodReference.getDesc(); + String methodOwner = methodReference.getOwner(); + boolean itf = methodReference.isInterface(); + + int tag = methodReference.getTag(); + if (!methodOwner.equals(classNode.name)) { + MethodID method = new MethodID(Type.getObjectType(methodOwner), methodName, Type.getMethodType(methodDesc), isStatic ? MethodID.CallType.STATIC : MethodID.CallType.VIRTUAL); + MethodID newMethod = this.makeOwnMethod(method, values, ((Type) dynamicInsnNode.bsmArgs[0]).getReturnType().getSort() == Type.VOID); + + methodName = newMethod.getName(); + methodDesc = newMethod.getDescriptor().getDescriptor(); + methodOwner = newMethod.getOwner().getInternalName(); + itf = false; + + tag = Opcodes.H_INVOKESTATIC; + staticOffset = 0; + } + + //Get analysis results of the actual method + //For lookups we do need to use the old owner + MethodID methodID = new MethodID(classNode.name, methodName, methodDesc, MethodID.CallType.VIRTUAL); // call type doesn't matter + AnalysisResults results = analysisResults.get(methodID); + if (results == null) { + throw new IllegalStateException("Method not analyzed '" + methodID + "'"); + } + + //Create new lambda descriptor + String newDesc = results.getNewDesc(); + Type[] newArgs = Type.getArgumentTypes(newDesc); + Type[] referenceArgs = newArgs; + + Type[] lambdaArgs = new Type[newArgs.length - values.length + staticOffset]; + System.arraycopy(newArgs, values.length - staticOffset, lambdaArgs, 0, lambdaArgs.length); + + String newReferenceDesc = Type.getMethodType(Type.getReturnType(newDesc), referenceArgs).getDescriptor(); + String lambdaDesc = Type.getMethodType(Type.getReturnType(newDesc), lambdaArgs).getDescriptor(); + + String newName = methodName; + if (config.getTypesWithSuffixedTransforms().contains(Type.getObjectType(methodOwner))) { + newName += MIX; + } + + //This is by no means a good solution but it's good enough for now + Type[] actualLambdaArgs = new Type[lambdaArgs.length]; + for (int j = 0; j < actualLambdaArgs.length; j++) { + actualLambdaArgs[j] = simplify(lambdaArgs[j]); + } + Type actualLambdaReturnType = simplify(Type.getReturnType(newDesc)); + + dynamicInsnNode.bsmArgs = new Object[] { + Type.getType(Type.getMethodType(actualLambdaReturnType, actualLambdaArgs).getDescriptor()), + new Handle(tag, methodOwner, newName, newReferenceDesc, itf), + Type.getMethodType(lambdaDesc) + }; + } + } + + private void transformNewInsn(Frame frame, TypeInsnNode instruction) { + TransformTrackingValue value = ASMUtil.getTop(frame); + if (value.getTransform().getTransformType() != null) { + instruction.desc = value.getTransform().getSingleType().getInternalName(); + } + } + + private void transformNewArray(TransformContext context, int insnIdx, Frame nextFrame, AbstractInsnNode instruction, int dimsKnown) { + TransformTrackingValue top = ASMUtil.getTop(nextFrame); + + if (!top.isTransformed()) return; + + int dimsNeeded = top.getTransform().getArrayDimensionality(); + + int dimsSaved = context.variableAllocator.allocate(insnIdx, insnIdx + 1, dimsKnown); + + for (int j = dimsKnown - 1; j < dimsNeeded; j++) { + context.target.instructions.insertBefore(instruction, new VarInsnNode(Opcodes.ISTORE, dimsSaved + j)); + } + + for (Type result : top.getTransform().resultingTypes()) { + for (int j = 0; j < dimsNeeded; j++) { + context.target.instructions.insertBefore(instruction, new VarInsnNode(Opcodes.ILOAD, dimsSaved + j)); + } + + context.target.instructions.insertBefore(instruction, ASMUtil.makeNew(result, dimsKnown)); + } + + context.target.instructions.remove(instruction); + } + + private void transformArrayLoad(TransformContext context, int insnIdx, AbstractInsnNode instruction, Frame frame) { + TransformTrackingValue array = frame.getStack(frame.getStackSize() - 2); + + if (!array.isTransformed()) return; + + InsnList list = new InsnList(); + + int indexIdx = context.variableAllocator.allocateSingle(insnIdx, insnIdx + 1); + list.add(new VarInsnNode(Opcodes.ISTORE, indexIdx)); + + int arrayIdx = context.variableAllocator.allocate(insnIdx, insnIdx + 1, array.getTransformedSize()); + int[] arrayOffsets = array.getTransform().getIndices(); + storeStackInLocals(array.getTransform(), list, arrayIdx); + + List types = array.getTransform().resultingTypes(); + + for (int j = 0; j < arrayOffsets.length; j++) { + list.add(new VarInsnNode(Opcodes.ALOAD, arrayIdx + arrayOffsets[j])); + list.add(new VarInsnNode(Opcodes.ILOAD, indexIdx)); + + Type type = types.get(j); + Type resultType = Type.getType("[".repeat(type.getDimensions() - 1) + type.getElementType().getDescriptor()); + + list.add(new InsnNode(resultType.getOpcode(Opcodes.IALOAD))); + } + + context.target.instructions.insert(instruction, list); + context.target.instructions.remove(instruction); + } + + private void transformArrayStore(TransformContext context, int insnIdx, AbstractInsnNode instruction, Frame frame) { + TransformTrackingValue array = frame.getStack(frame.getStackSize() - 3); + TransformTrackingValue value = frame.getStack(frame.getStackSize() - 1); + + if (!array.isTransformed()) return; + + InsnList list = new InsnList(); + + int valueIdx = context.variableAllocator.allocate(insnIdx, insnIdx + 1, value.getTransformedSize()); + int[] valueOffsets = value.getTransform().getIndices(); + storeStackInLocals(value.getTransform(), list, valueIdx); + + int indexIdx = context.variableAllocator.allocateSingle(insnIdx, insnIdx + 1); + list.add(new VarInsnNode(Opcodes.ISTORE, indexIdx)); + + int arrayIdx = context.variableAllocator.allocate(insnIdx, insnIdx + 1, array.getTransformedSize()); + int[] arrayOffsets = array.getTransform().getIndices(); + storeStackInLocals(array.getTransform(), list, arrayIdx); + + List types = value.getTransform().resultingTypes(); + + for (int j = 0; j < arrayOffsets.length; j++) { + Type type = types.get(j); + + list.add(new VarInsnNode(Opcodes.ALOAD, arrayIdx + arrayOffsets[j])); + list.add(new VarInsnNode(Opcodes.ILOAD, indexIdx)); + list.add(new VarInsnNode(type.getOpcode(Opcodes.ILOAD), valueIdx + valueOffsets[j])); + + list.add(new InsnNode(type.getOpcode(Opcodes.IASTORE))); + } + + context.target.instructions.insert(instruction, list); + context.target.instructions.remove(instruction); + } + + private void transformFieldInsn(TransformContext context, int i, FieldInsnNode instruction) { + FieldID fieldID = new FieldID(Type.getObjectType(instruction.owner), instruction.name, Type.getType(instruction.desc)); + boolean isStatic = instruction.getOpcode() == Opcodes.GETSTATIC || instruction.getOpcode() == Opcodes.PUTSTATIC; + boolean isPut = instruction.getOpcode() == Opcodes.PUTSTATIC || instruction.getOpcode() == Opcodes.PUTFIELD; + + if (!fieldID.owner().getInternalName().equals(this.classNode.name)) { + return; + } + + TransformTrackingValue field = this.fieldPseudoValues.get(fieldID); + + if (!field.isTransformed()) return; + + InsnList result = new InsnList(); + + int objIdx = -1; + int arrayIdx = -1; + int[] offsets = field.getTransform().getIndices(); + + if (isPut) { + arrayIdx = context.variableAllocator.allocate(i, i + 1, field.getTransformedSize()); + storeStackInLocals(field.getTransform(), result, arrayIdx); + } + + if (!isStatic) { + objIdx = context.variableAllocator().allocate(i, i + 1, 1); + result.add(new VarInsnNode(Opcodes.ASTORE, objIdx)); + } + + List types = field.getTransform().resultingTypes(); + List names = new ArrayList<>(); + + for (int j = 0; j < types.size(); j++) { + names.add(this.getExpandedFieldName(fieldID, j)); + } + + for (int j = 0; j < types.size(); j++) { + Type type = types.get(j); + String name = names.get(j); + + if (!isStatic) { + result.add(new VarInsnNode(Opcodes.ALOAD, objIdx)); + } + + if (isPut) { + result.add(new VarInsnNode(type.getOpcode(Opcodes.ILOAD), arrayIdx + offsets[j])); + } + + result.add(new FieldInsnNode(instruction.getOpcode(), this.classNode.name, name, type.getDescriptor())); + } + + context.target.instructions.insertBefore(instruction, result); + context.target.instructions.remove(instruction); + } + + /** + * Transform and add a constructor. Replacement info must be provided + * + * @param desc The descriptor of the original constructor + */ + public void makeConstructor(String desc) { + ConstructorReplacer replacer = transformInfo.getConstructorReplacers().get(desc); + + if (replacer == null) { + throw new RuntimeException("No replacement info found for constructor " + desc); + } + + makeConstructor(desc, replacer.make(this)); + } + + /** + * Add a constructor to the class + * + * @param desc The descriptor of the original constructor + * @param constructor Code for the new constructor. This code is expected to initialize all fields (except 'isTransformed') with transformed values + */ + public void makeConstructor(String desc, InsnList constructor) { + //Add int to end of descriptor signature so we can call this new constructor + Type[] args = Type.getArgumentTypes(desc); + int totalSize = 1; + for (Type arg : args) { + totalSize += arg.getSize(); + } + + Type[] newArgs = new Type[args.length + 1]; + newArgs[newArgs.length - 1] = Type.INT_TYPE; + System.arraycopy(args, 0, newArgs, 0, args.length); + String newDesc = Type.getMethodDescriptor(Type.VOID_TYPE, newArgs); + + //If the extra integer passed is not equal to MAGIC (0xDEADBEEF), then we throw an error. This is to prevent accidental use of this constructor + InsnList safetyCheck = new InsnList(); + LabelNode label = new LabelNode(); + safetyCheck.add(new VarInsnNode(Opcodes.ILOAD, totalSize)); + safetyCheck.add(new LdcInsnNode(MAGIC)); + safetyCheck.add(new JumpInsnNode(Opcodes.IF_ICMPEQ, label)); + safetyCheck.add(new TypeInsnNode(Opcodes.NEW, "java/lang/IllegalArgumentException")); + safetyCheck.add(new InsnNode(Opcodes.DUP)); + safetyCheck.add(new LdcInsnNode("Wrong magic value '")); + safetyCheck.add(new VarInsnNode(Opcodes.ILOAD, totalSize)); + safetyCheck.add(new MethodInsnNode(Opcodes.INVOKESTATIC, "java/lang/Integer", "toHexString", "(I)Ljava/lang/String;", false)); + safetyCheck.add(new LdcInsnNode("'")); + safetyCheck.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, "java/lang/String", "concat", "(Ljava/lang/String;)Ljava/lang/String;", false)); + safetyCheck.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, "java/lang/String", "concat", "(Ljava/lang/String;)Ljava/lang/String;", false)); + safetyCheck.add(new MethodInsnNode(Opcodes.INVOKESPECIAL, "java/lang/IllegalArgumentException", "", "(Ljava/lang/String;)V", false)); + safetyCheck.add(new InsnNode(Opcodes.ATHROW)); + safetyCheck.add(label); + safetyCheck.add(new VarInsnNode(Opcodes.ALOAD, 0)); + safetyCheck.add(new InsnNode(Opcodes.ICONST_1)); + safetyCheck.add(new FieldInsnNode(Opcodes.PUTFIELD, classNode.name, isTransformedField.name(), "Z")); + + AbstractInsnNode[] nodes = constructor.toArray(); + + //Find super call + for (AbstractInsnNode node : nodes) { + if (node.getOpcode() == Opcodes.INVOKESPECIAL) { + MethodInsnNode methodNode = (MethodInsnNode) node; + if (methodNode.owner.equals(classNode.superName)) { + //Insert the safety check right after the super call + constructor.insert(methodNode, safetyCheck); + break; + } + } + } + + //Shift variables + for (AbstractInsnNode node : nodes) { + if (node instanceof VarInsnNode varNode) { + if (varNode.var >= totalSize) { + varNode.var++; + } + } else if (node instanceof IincInsnNode iincNode) { + if (iincNode.var >= totalSize) { + iincNode.var++; + } + } + } + + MethodNode methodNode = new MethodNode(Opcodes.ACC_PUBLIC, "", newDesc, null, null); + methodNode.instructions.add(constructor); + + markSynthetic(methodNode, "CONSTRUCTOR", "" + desc, classNode.name); + + newMethods.add(methodNode); + } + + + private void transformAbstractMethod(MethodNode methodNode, long start, MethodID methodID, MethodNode newMethod, TransformContext context) { + //If the method is abstract, we don't need to transform its code, just it's descriptor + transformDesc(newMethod, context); + + if (methodNode.parameters != null) { + this.modifyParameterTable(newMethod, context); + } + } + + /** + * Modifies the variable and parameter tables (if they exist) to make it easier to read the generated code when decompiled + * + * @param methodNode The method to modify + * @param context The transform context + */ + private void modifyLVT(MethodNode methodNode, TransformContext context) { + if (methodNode.localVariables != null) { + modifyVariableTable(methodNode, context); + } + + //Similar algorithm for parameters + if (methodNode.parameters != null) { + modifyParameterTable(methodNode, context); + } + } + + private void modifyParameterTable(MethodNode methodNode, TransformContext context) { + List original = methodNode.parameters; + + List newParameters = new ArrayList<>(); + + int index = 0; + if ((methodNode.access & Opcodes.ACC_STATIC) == 0) { + index++; + } + for (ParameterNode param : original) { + TransformTrackingValue value = context.analysisResults.frames()[0].getLocal(index); + if (value.getTransformType() == null || value.getTransform().getKind() != DerivedTransformType.Kind.NONE) { + newParameters.add(new ParameterNode(param.name, param.access)); + } else { + String[] postfixes = value.getTransformType().getPostfix(); + for (String postfix : postfixes) { + newParameters.add(new ParameterNode(param.name + postfix, param.access)); + } + } + index += value.getSize(); + } + + methodNode.parameters = newParameters; + } + + private void modifyVariableTable(MethodNode methodNode, TransformContext context) { + List original = methodNode.localVariables; + List newLocalVariables = new ArrayList<>(); + + for (LocalVariableNode local : original) { + int codeIndex = context.indexLookup().get(local.start); //The index of the first frame with that variable + int newIndex = context.varLookup[local.index]; + + TransformTrackingValue value = context.analysisResults().frames()[codeIndex].getLocal(local.index); //Get the value of that variable, so we can get its transform + if (value.getTransformType() == null || value.getTransform().getKind() != DerivedTransformType.Kind.NONE) { + String desc; + if (value.getTransformType() == null) { + Type type = value.getType(); + if (type == null) { + continue; + } else { + desc = value.getType().getDescriptor(); + } + } else { + desc = value.getTransform().getSingleType().getDescriptor(); + } + newLocalVariables.add(new LocalVariableNode(local.name, desc, local.signature, local.start, local.end, newIndex)); + } else { + String[] postfixes = value.getTransformType().getPostfix(); + int varIndex = newIndex; + for (int j = 0; j < postfixes.length; j++) { + newLocalVariables.add( + new LocalVariableNode( + local.name + postfixes[j], + value.getTransformType().getTo()[j].getDescriptor(), + local.signature, local.start, local.end, varIndex + ) + ); + varIndex += value.getTransformType().getTo()[j].getSize(); + } + } + } + + methodNode.localVariables = newLocalVariables; + } + + private void getAllMethodInfo(AbstractInsnNode[] insns, AbstractInsnNode[] instructions, Frame[] frames, MethodParameterInfo[] methodInfos) { + for (int i = 0; i < insns.length; i++) { + AbstractInsnNode insn = instructions[i]; + Frame frame = frames[i]; + if (insn instanceof MethodInsnNode methodCall) { + MethodID calledMethod = MethodID.from(methodCall); + + TransformTrackingValue returnValue = null; + if (calledMethod.getDescriptor().getReturnType() != Type.VOID_TYPE) { + returnValue = ASMUtil.getTop(frames[i + 1]); + } + + int argCount = ASMUtil.argumentCount(calledMethod.getDescriptor().getDescriptor(), calledMethod.isStatic()); + TransformTrackingValue[] args = new TransformTrackingValue[argCount]; + for (int j = 0; j < args.length; j++) { + args[j] = frame.getStack(frame.getStackSize() - argCount + j); + } + + //Lookup the possible method transforms + List infos = config.getMethodParameterInfo().get(calledMethod); + + if (infos != null) { + //Check all possible transforms to see if any of them match + for (MethodParameterInfo info : infos) { + if (info.getTransformCondition().checkValidity(returnValue, args) == 1) { + methodInfos[i] = info; + break; + } + } + } + } + } + } + + private MethodID makeOwnMethod(MethodID method, TransformTrackingValue[] argsForAnalysis, boolean returnVoid) { + if (this.externalMethodReplacements.containsKey(method)) { + return this.externalMethodReplacements.get(method); + } + + Type[] args = method.getDescriptor().getArgumentTypes(); + Type[] actualArgs; + if (method.isStatic()) { + actualArgs = args; + } else { + actualArgs = new Type[args.length + 1]; + actualArgs[0] = argsForAnalysis[0].getType(); + System.arraycopy(args, 0, actualArgs, 1, args.length); + } + + String name = "external_" + method.getOwner().getClassName().replace('.', '_') + "_" + method.getName() + "_" + externalMethodReplacements.size(); + + if (!method.isStatic()) { + name += actualArgs[0].getClassName().replace('.', '_'); + } + + MethodNode node = new MethodNode( + Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC | Opcodes.ACC_SYNTHETIC, + name, + Type.getMethodDescriptor(returnVoid ? Type.VOID_TYPE : method.getDescriptor().getReturnType(), actualArgs), + null, + null + ); + + int localSize = 0; + for (Type actualArg : actualArgs) { + node.instructions.add(new VarInsnNode(actualArg.getOpcode(Opcodes.ILOAD), localSize)); + localSize += actualArg.getSize(); + } + + boolean itf = config.getTypeInfo().recognisesInterface(method.getOwner()); + node.instructions.add( + new MethodInsnNode( + method.isStatic() ? Opcodes.INVOKESTATIC : (itf ? Opcodes.INVOKEINTERFACE : Opcodes.INVOKEVIRTUAL), + method.getOwner().getInternalName(), + method.getName(), + method.getDescriptor().getDescriptor(), + itf + ) + ); + + if (!returnVoid) { + node.instructions.add(new InsnNode(method.getDescriptor().getReturnType().getOpcode(Opcodes.IRETURN))); + } else { + if (method.getDescriptor().getReturnType().getSort() != Type.VOID) { + if (method.getDescriptor().getReturnType().getSize() == 2) { + node.instructions.add(new InsnNode(Opcodes.POP2)); + } else { + node.instructions.add(new InsnNode(Opcodes.POP)); + } + + node.instructions.add(new InsnNode(Opcodes.RETURN)); + } + } + + node.maxLocals = localSize; + node.maxStack = actualArgs.length; + + MethodID newMethod = new MethodID(Type.getObjectType(this.classNode.name), node.name, Type.getMethodType(node.desc), MethodID.CallType.STATIC); + this.externalMethodReplacements.put(method, newMethod); + + AnalysisResults results = this.analyzeMethod(node); + + TransformTrackingInterpreter.bindValuesToMethod(results, 0, argsForAnalysis); + + this.generateTransformedMethod(node); + + this.newMethods.add(node); + + return newMethod; + } + + /** + * Makes all call to super constructor add the magic value so that it is initialized transformed + */ + public void callMagicSuperConstructor() { + for (MethodNode methodNode : classNode.methods) { + if (methodNode.name.equals("")) { + MethodInsnNode superCall = findSuperCall(methodNode); + String[] parts = superCall.desc.split("\\)"); + String newDesc = parts[0] + "I)" + parts[1]; + superCall.desc = newDesc; + methodNode.instructions.insertBefore(superCall, new LdcInsnNode(MAGIC)); + } + } + } + + /** + * Should be called after all transforms have been applied. + */ + public void cleanUpTransform() { + //Add methods that need to be added + classNode.methods.addAll(lambdaTransformers); + + for (MethodNode newMethod : newMethods) { + MethodNode existing = classNode.methods.stream().filter(m -> m.name.equals(newMethod.name) && m.desc.equals(newMethod.desc)).findFirst().orElse(null); + + if (existing != null) { + if (!inPlace) { + throw new IllegalStateException("Method " + newMethod.name + newMethod.desc + " already exists in class " + classNode.name); + } else { + classNode.methods.remove(existing); + } + } + + classNode.methods.add(newMethod); + } + + if (hasTransformedFields && !inPlace) { + addSafetyFieldSetter(); + } + + if (inPlace) { + modifyFields(); + } else { + makeFieldCasts(); + } + } + + private void addSafetyFieldSetter() { + for (MethodNode methodNode : classNode.methods) { + if (methodNode.name.equals("")) { + if (isSynthetic(methodNode)) continue; + + insertAtReturn(methodNode, (variableAllocator) -> { + InsnList instructions = new InsnList(); + instructions.add(new VarInsnNode(Opcodes.ALOAD, 0)); + instructions.add(new InsnNode(Opcodes.ICONST_0)); + instructions.add(new FieldInsnNode(Opcodes.PUTFIELD, classNode.name, isTransformedField.name(), "Z")); + return instructions; + }); + } + } + } + + private void modifyFields() { + List toAdd = new ArrayList<>(); + List toRemove = new ArrayList<>(); + + for (FieldNode field : classNode.fields) { + FieldID fieldID = new FieldID(Type.getObjectType(classNode.name), field.name, Type.getType(field.desc)); + TransformTrackingValue value = fieldPseudoValues.get(fieldID); + + if (!value.isTransformed()) continue; + + if ((field.access & Opcodes.ACC_PRIVATE) == 0) { + throw new IllegalStateException("Field " + field.name + " in class " + classNode.name + " is not private"); + } + + List types = value.getTransform().resultingTypes(); + List names = new ArrayList<>(); + + //Make new fields + for (int i = 0; i < types.size(); i++) { + Type type = types.get(i); + String name = getExpandedFieldName(fieldID, i); + names.add(name); + + FieldNode newField = new FieldNode(field.access, name, type.getDescriptor(), null, null); + toAdd.add(newField); + } + + //Remove old field + toRemove.add(field); + } + + classNode.fields.removeAll(toRemove); + classNode.fields.addAll(toAdd); + } + + /** + * One of the aspects of this transformer is that if the original methods are called then the behaviour should be normal. This means that if a field's type needs to be changed then old + * methods would still need to use the old field type and new methods would need to use the new field type. Instead of duplicating each field, we turn the type of each of these fields + * into {@link Object} and cast them to their needed type. To initialize these fields to their transformed types, we create a new constructor. + *

(This does not apply for "in place" transformations) + *

+ * Example: + *
+     *     public class A {
+     *         private final LongList list;
+     *
+     *         public A() {
+     *             Initialization...
+     *         }
+     *
+     *         public void exampleMethod() {
+     *             long pos = list.get(0);
+     *             ...
+     *         }
+     *     }
+     * 
+ * Would become + *
+     *     public class A {
+     *         private final Object list;
+     *
+     *         public A() {
+     *             Initialization...
+     *         }
+     *
+     *         //This constructor would need to be added by makeConstructor
+     *         public A(int magic){
+     *             Transformed initialization...
+     *         }
+     *
+     *         public void exampleMethod() {
+     *             long pos = ((LongList)list).get(0);
+     *             ...
+     *         }
+     *     }
+     * 
+ */ + private void makeFieldCasts() { + for (var entry : fieldPseudoValues.entrySet()) { + if (entry.getValue().getTransformType() == null) { + continue; + } + + DerivedTransformType transformType = entry.getValue().getTransform(); + FieldID fieldID = entry.getKey(); + + String originalType = entry.getValue().getType().getInternalName(); + String transformedType = transformType.getSingleType().getInternalName(); + + ASMUtil.changeFieldType(classNode, fieldID, Type.getObjectType("java/lang/Object"), (method) -> { + InsnList insnList = new InsnList(); + if (isSynthetic(method)) { + insnList.add(new TypeInsnNode(Opcodes.CHECKCAST, transformedType)); + } else { + insnList.add(new TypeInsnNode(Opcodes.CHECKCAST, originalType)); + } + return insnList; + }); + } + } + + private String getExpandedFieldName(FieldID field, int idx) { + TransformTrackingValue value = this.fieldPseudoValues.get(field); + + if (!value.isTransformed()) throw new IllegalArgumentException("Field " + field + " is not transformed"); + + return field.name() + "_expanded" + value.getTransformType().getPostfix()[idx]; + } + + + // CC-SYNTHETIC METHOD ANNOTATIONS + + /** + * Adds the {@link CCSynthetic} annotation to the provided method + * + * @param methodNode The method to mark + * @param type The type of synthetic method this is - either "AUTO-TRANSFORMED" or "CONSTRUCTOR" + * @param original The original method this is a synthetic version of + */ + private static void markSynthetic(MethodNode methodNode, String type, String original, String ownerName) { + List annotations = methodNode.visibleAnnotations; + if (annotations == null) { + annotations = new ArrayList<>(); + methodNode.visibleAnnotations = annotations; + } + + AnnotationNode synthetic = new AnnotationNode(Type.getDescriptor(CCSynthetic.class)); + + synthetic.values = new ArrayList<>(); + synthetic.values.add("type"); + synthetic.values.add(type); + synthetic.values.add("original"); + synthetic.values.add(original); + + annotations.add(synthetic); + + //Stack traces don't specify the descriptor so we set the line numbers to a known value to detect whether we were in a CC synthetic emthod + + int lineStart = 60000; + Int2ObjectMap descLookup = CC_SYNTHETIC_LOOKUP.computeIfAbsent(ownerName, k -> new Int2ObjectOpenHashMap<>()); + while (descLookup.containsKey(lineStart)) { + lineStart += 10; + + if (lineStart >= (1 << 16)) { + throw new RuntimeException("Too many CC synthetic methods"); + } + } + + //Remove previous line numbers + for (AbstractInsnNode insnNode : methodNode.instructions.toArray()) { + if (insnNode instanceof LineNumberNode) { + methodNode.instructions.remove(insnNode); + } + } + + descLookup.put(lineStart, methodNode.desc); + + //Add our own + LabelNode start = new LabelNode(); + methodNode.instructions.insertBefore(methodNode.instructions.getFirst(), new LineNumberNode(lineStart, start)); + methodNode.instructions.insertBefore(methodNode.instructions.getFirst(), start); + } + + public static @Nullable Method getSyntheticMethod(Class owner, String name, int lineNumber) { + String ownerName = owner.getName().replace('.', '/'); + + Int2ObjectMap descLookup = CC_SYNTHETIC_LOOKUP.computeIfAbsent(ownerName, k -> new Int2ObjectOpenHashMap<>()); + String desc = descLookup.get((lineNumber / 10) * 10); + + if (desc == null) { + return null; + } + + Class[] types = Arrays.stream(Type.getArgumentTypes(desc)) + .map((t) -> { + if (t == Type.BYTE_TYPE) { + return byte.class; + } else if (t == Type.SHORT_TYPE) { + return short.class; + } else if (t == Type.INT_TYPE) { + return int.class; + } else if (t == Type.LONG_TYPE) { + return long.class; + } else if (t == Type.FLOAT_TYPE) { + return float.class; + } else if (t == Type.DOUBLE_TYPE) { + return double.class; + } else if (t == Type.BOOLEAN_TYPE) { + return boolean.class; + } else if (t == Type.CHAR_TYPE) { + return char.class; + } else if (t == Type.VOID_TYPE) { + return void.class; + } else { + try { + return Class.forName(t.getClassName()); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + }).toArray(Class[]::new); + + try { + Method method = owner.getMethod(name, types); + + CCSynthetic annotation = method.getAnnotation(CCSynthetic.class); + + if (annotation == null) { + return null; + } + + return method; + } catch (NoSuchMethodException e) { + return null; + } + } + + /** + * Checks if the provided method has the {@link CCSynthetic} annotation + * + * @param methodNode The method to check + * + * @return True if the method is synthetic, false otherwise + */ + private static boolean isSynthetic(MethodNode methodNode) { + List annotations = methodNode.visibleAnnotations; + if (annotations == null) { + return false; + } + + for (AnnotationNode annotation : annotations) { + if (annotation.desc.equals(Type.getDescriptor(CCSynthetic.class))) { + return true; + } + } + + return false; + } + + /** + * This method is called by safety dispatches (Called from ASM - DO NOT RENAME/REMOVE) + * + * @param message The message to print + */ + public static void emitWarning(String message, int callerDepth) { + //Gather info about exactly where this was called + StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); + StackTraceElement caller = stackTrace[callerDepth]; + + String warningID = message + " at " + caller.getClassName() + "." + caller.getMethodName() + ":" + caller.getLineNumber(); + if (WARNINGS.add(warningID)) { + System.out.println("[CC Warning] " + warningID); + try { + FileOutputStream fos = new FileOutputStream(ERROR_LOG.toFile()); + for (String warning : WARNINGS) { + fos.write(warning.getBytes()); + fos.write("\n".getBytes()); + } + fos.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + public static InsnList generateEmitWarningCall(String message, int callerDepth) { + InsnList instructions = new InsnList(); + + instructions.add(new LdcInsnNode(message)); + instructions.add(new LdcInsnNode(callerDepth)); + + instructions.add(new MethodInsnNode(Opcodes.INVOKESTATIC, "io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/TypeTransformer", "emitWarning", + "(Ljava/lang/String;I)V", + false)); + + return instructions; + } + + private static Type simplify(Type type) { + if (type.getSort() == Type.ARRAY || type.getSort() == Type.OBJECT) { + return Type.getType(Object.class); + } else { + return type; + } + } + + private void storeStackInLocals(DerivedTransformType transform, InsnList insnList, int baseIdx) { + List types = transform.resultingTypes(); + int[] offsets = transform.getIndices(); + + for (int i = types.size(); i > 0; i--) { + Type type = types.get(i - 1); + int offset = offsets[i - 1]; + insnList.add(new VarInsnNode(type.getOpcode(Opcodes.ISTORE), baseIdx + offset)); + } + } + + private void loadIndices(TransformTrackingValue[] args, InsnList replacementInstructions, int[][] offsets, int baseIdx, List[] indices) { + for (int j = 0; j < indices.length; j++) { + List types = args[j].transformedTypes(); + for (int index: indices[j]) { + int offset = offsets[j][index]; + replacementInstructions.add(new VarInsnNode(types.get(index).getOpcode(Opcodes.ILOAD), baseIdx + offset)); + } + } + } + + /** + * Insert the provided code before EVERY return statement in a method + * + * @param methodNode The method to insert the code into + * @param insn The code to insert + */ + private static void insertAtReturn(MethodNode methodNode, BytecodeFactory insn) { + InsnList instructions = methodNode.instructions; + AbstractInsnNode[] nodes = instructions.toArray(); + + for (AbstractInsnNode node : nodes) { + if (node.getOpcode() == Opcodes.RETURN + || node.getOpcode() == Opcodes.ARETURN + || node.getOpcode() == Opcodes.IRETURN + || node.getOpcode() == Opcodes.FRETURN + || node.getOpcode() == Opcodes.DRETURN + || node.getOpcode() == Opcodes.LRETURN) { + + //Since we are inserting code right before the return statement, there are no live variables, so we can use whatever variables we want. + //For tidyness reasons we won't replace params + + int base = ASMUtil.isStatic(methodNode) ? 0 : 1; + for (Type t : Type.getArgumentTypes(methodNode.desc)) { + base += t.getSize(); + } + + int finalBase = base; + Function varAllocator = new Function<>() { + int curr = finalBase; + + @Override + public Integer apply(Type type) { + int slot = curr; + curr += type.getSize(); + return slot; + } + }; + + instructions.insertBefore(node, insn.generate(varAllocator)); + } + } + } + + private MethodInsnNode findSuperCall(MethodNode constructor) { + for (AbstractInsnNode insn : constructor.instructions.toArray()) { + if (insn.getOpcode() == Opcodes.INVOKESPECIAL) { + MethodInsnNode methodInsn = (MethodInsnNode) insn; + if (methodInsn.owner.equals(classNode.superName) && methodInsn.name.equals("")) { + return methodInsn; + } + } + } + + throw new RuntimeException("Could not find super constructor call"); + } + + private void transformDesc(MethodNode methodNode, TransformContext context) { + DerivedTransformType[] actualParameters; + if ((methodNode.access & Opcodes.ACC_STATIC) == 0) { + actualParameters = new DerivedTransformType[context.analysisResults().getArgTypes().length - 1]; + System.arraycopy(context.analysisResults().getArgTypes(), 1, actualParameters, 0, actualParameters.length); + } else { + actualParameters = context.analysisResults().getArgTypes(); + } + + //Change descriptor + String newDescriptor = MethodParameterInfo.getNewDesc(DerivedTransformType.createDefault(Type.getReturnType(methodNode.desc)), actualParameters, methodNode.desc); + methodNode.desc = newDescriptor; + } + + /** + * This method creates a jump to the given label if the fields hold transformed types or none of the fields need to be transformed. + * + * @param label The label to jump to. + * + * @return The instructions to jump to the given label. + */ + public InsnList jumpIfNotTransformed(LabelNode label) { + InsnList instructions = new InsnList(); + if (hasTransformedFields && !inPlace) { + instructions.add(new VarInsnNode(Opcodes.ALOAD, 0)); + instructions.add(new FieldInsnNode(Opcodes.GETFIELD, isTransformedField.owner().getInternalName(), isTransformedField.name(), isTransformedField.desc().getDescriptor())); + instructions.add(new JumpInsnNode(Opcodes.IFEQ, label)); + } + + //If there are no transformed fields then we never jump. + return instructions; + } + + private boolean isACompare(int opcode) { + return opcode == Opcodes.LCMP || opcode == Opcodes.FCMPL || opcode == Opcodes.FCMPG || opcode == Opcodes.DCMPL || opcode == Opcodes.DCMPG || opcode == Opcodes.IF_ICMPEQ + || opcode == Opcodes.IF_ICMPNE || opcode == Opcodes.IF_ACMPEQ || opcode == Opcodes.IF_ACMPNE; + } + + private boolean isArrayLoad(int opcode) { + return opcode == Opcodes.IALOAD || opcode == Opcodes.LALOAD || opcode == Opcodes.FALOAD || opcode == Opcodes.DALOAD || opcode == Opcodes.AALOAD || opcode == Opcodes.BALOAD + || opcode == Opcodes.CALOAD || opcode == Opcodes.SALOAD; + } + + private boolean isArrayStore(int opcode) { + return opcode == Opcodes.IASTORE || opcode == Opcodes.LASTORE || opcode == Opcodes.FASTORE || opcode == Opcodes.DASTORE || opcode == Opcodes.AASTORE || opcode == Opcodes.BASTORE + || opcode == Opcodes.CASTORE || opcode == Opcodes.SASTORE; + } + + public Map getAnalysisResults() { + return analysisResults; + } + + public ClassNode getClassNode() { + return classNode; + } + + public Config getConfig() { + return config; + } + + /** + * Stores all information needed to transform a method. + * + * @param target The method that is being transformed. + * @param analysisResults The analysis results for this method that were generated by the analysis phase. + * @param instructions The instructions of {@code target} before any transformations. + * @param varLookup Stores the new index of a variable. varLookup[insnIndex][oldVarIndex] gives the new var index. + * @param variableAllocator The variable allocator allows for the creation of new variables. + * @param indexLookup A map from instruction object to index in the instructions array. This map contains keys for the instructions of both the old and new methods. This is useful + * mainly because TransformTrackingValue.getSource() will return instructions from the old method and to manipulate the InsnList of the new method (which is a linked list) we need an + * element which is in that InsnList. + * @param methodInfos If an instruction is a method invocation, this will store information about how to transform it. + */ + private record TransformContext( + MethodNode target, + AnalysisResults analysisResults, + AbstractInsnNode[] instructions, + int[] varLookup, + DerivedTransformType[][] varTypes, + VariableAllocator variableAllocator, + Map indexLookup, + MethodParameterInfo[] methodInfos + ) { } +} \ No newline at end of file diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/VariableAllocator.java b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/VariableAllocator.java new file mode 100644 index 000000000..245c39f2d --- /dev/null +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/VariableAllocator.java @@ -0,0 +1,111 @@ +package io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +import org.objectweb.asm.Type; + +/** + * This class allows for the creation of new local variables for a method. This class is by no means made to be efficient but it works + */ +public class VariableAllocator { + private final int baseline; //The maxLocals of the original method. No variable will be allocated in this range + private final int maxLength; //The length of the instructions + private final List variables = new ArrayList<>(); //Stores which slots are used for each frame + + /** + * Creates a new VariableManager with the given maxLocals and instruction length + * + * @param maxLocals The maxLocals of the method + * @param maxLength The length of the instructions + */ + public VariableAllocator(int maxLocals, int maxLength) { + this.baseline = maxLocals; + this.maxLength = maxLength; + } + + /** + * Allocates n consecutive slots + * + * @param from The index of the first place this variable will be used + * @param to The index of the last place this variable will be used + * @param n The number of consecutive slots to allocate + */ + public int allocate(int from, int to, int n) { + int level = 0; + while (true) { + while (level + n - 1 >= variables.size()) { + variables.add(new boolean[maxLength]); + } + + boolean[][] vars = new boolean[n][]; + for (int i = 0; i < n; i++) { + vars[i] = variables.get(level + i); + } + + //Check that all of it is free + boolean free = true; + out: + for (int i = from; i < to; i++) { + for (boolean[] var : vars) { + if (var[i]) { + free = false; + break out; + } + } + } + + if (free) { + //Mark it as used + for (int i = from; i < to; i++) { + for (boolean[] var : vars) { + var[i] = true; + } + } + + return level + baseline; + } + + level++; + } + } + + /** + * Allocates a variable + * + * @param minIndex The minimum index of the variable + * @param maxIndex The maximum index of the variable + * @param type The type of the variable + * + * @return The index of the variable + */ + public int allocate(int minIndex, int maxIndex, Type type) { + return this.allocate(minIndex, maxIndex, type.getSize()); + } + + /** + * Allocates a variable which takes up a single slot + * + * @param from The index of the first place this variable will be used + * @param to The index of the last place this variable will be used + * + * @return The index of the variable + */ + public int allocateSingle(int from, int to) { + return this.allocate(from, to, 1); + } + + public static Function makeBasicAllocator(int baseline) { + return new Function<>() { + int curr = baseline; + + @Override + public Integer apply(Type type) { + int index = curr; + curr += type.getSize(); + return index; + } + }; + } +} diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/analysis/AnalysisResults.java b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/analysis/AnalysisResults.java new file mode 100644 index 000000000..a473d2288 --- /dev/null +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/analysis/AnalysisResults.java @@ -0,0 +1,78 @@ +package io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.analysis; + +import java.io.PrintStream; + +import io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.config.MethodParameterInfo; +import io.github.opencubicchunks.cubicchunks.mixin.transform.util.ASMUtil; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.MethodNode; +import org.objectweb.asm.tree.analysis.Frame; + +/** + * Holds the results of the analysis of a single method. + * @param methodNode The method these results are for + * If the method is not static, this includes information for the 'this' parameter + */ +public record AnalysisResults(MethodNode methodNode, Frame[] frames) { + /** + * Prints information about the analysis results. + * @param out Where to print the information. + * @param printFrames Whether information should be printed for every frame. + */ + public void print(PrintStream out, boolean printFrames) { + out.println("Analysis Results for " + methodNode.name); + out.println(" Arg Types:"); + for (DerivedTransformType argType : this.getArgTypes()) { + out.println(" " + argType); + } + + if (printFrames) { + out.println(" Frames:"); + for (int i = 0; i < frames.length; i++) { + Frame frame = frames[i]; + if (frame != null) { + out.println(" Frame " + i); + out.println(" Stack:"); + for (int j = 0; j < frames[i].getStackSize(); j++) { + out.println(" " + frames[i].getStack(j)); + } + out.println(" Locals:"); + for (int j = 0; j < frames[i].getLocals(); j++) { + out.println(" " + frames[i].getLocal(j)); + } + } + } + } + } + + public DerivedTransformType[] getArgTypes() { + int offset = ASMUtil.isStatic(methodNode) ? 0 : 1; + Type[] args = Type.getArgumentTypes(methodNode.desc); + DerivedTransformType[] argTypes = new DerivedTransformType[args.length + offset]; + + int i = 0; + for (int idx = 0; idx < argTypes.length; idx++) { + argTypes[idx] = frames[0].getLocal(i).getTransform(); + i += frames[0].getLocal(i).getSize(); + } + + return argTypes; + } + + /** + * Creates the new description using the transformed argument types + * @return A descriptor as a string + */ + public String getNewDesc() { + DerivedTransformType[] argTypes = getArgTypes(); + DerivedTransformType[] types = argTypes; + if (!ASMUtil.isStatic(methodNode)) { + //If the method is not static then the first element of this.types is the 'this' argument. + //This argument is not shown in method descriptors, so we must exclude it + types = new DerivedTransformType[types.length - 1]; + System.arraycopy(argTypes, 1, types, 0, types.length); + } + + return MethodParameterInfo.getNewDesc(DerivedTransformType.createDefault(Type.getReturnType(methodNode.desc)), types, methodNode.desc); + } +} diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/analysis/DerivedTransformType.java b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/analysis/DerivedTransformType.java new file mode 100644 index 000000000..c3fe99eff --- /dev/null +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/analysis/DerivedTransformType.java @@ -0,0 +1,452 @@ +package io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.analysis; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.config.Config; +import io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.config.TransformType; +import org.jetbrains.annotations.Nullable; +import org.objectweb.asm.Handle; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.InsnList; +import org.objectweb.asm.tree.InsnNode; +import org.objectweb.asm.tree.InvokeDynamicInsnNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; +import org.objectweb.asm.tree.VarInsnNode; + +public class DerivedTransformType { + //A reference to a TransformType. This means when the transform type gets changed all referenced ones can get notified + private final TransformTypeRef transformType; + //Array dimensionality of type. So an array of longs (with type long -> (int, int, int)) would have dimensionality 1 + private int arrayDimensionality; + //The kind. Either NONE, CONSUMER or PREDICATE + private Kind kind; + + private final Type originalType; + + public DerivedTransformType(TransformTypeRef transformType, int arrayDimensionality, Kind kind, Type originalType) { + this.transformType = transformType; + this.arrayDimensionality = arrayDimensionality; + this.kind = kind; + this.originalType = originalType; + } + + /** + * Get the transform type of this object. + * @return The transform type of this object. This may be null if the object doesn't have a transform type or + * it has not been inferred yet. + */ + public @Nullable TransformType getTransformType() { + return transformType.getValue(); + } + + /** + * For internal use only + * @return The reference to the transform type + */ + TransformTypeRef getTransformTypePtr() { + return transformType; + } + + public int getArrayDimensionality() { + return arrayDimensionality; + } + + public Kind getKind() { + return kind; + } + + public void setArrayDimensionality(int arrayDimensionality) { + this.arrayDimensionality = arrayDimensionality; + } + + public void setKind(Kind transformKind) { + this.kind = transformKind; + } + + /** + * @return A derived transform type for which nothing is known yet. The transform type is null, array dimensionality is 0 and + * the kind is NONE + */ + public static DerivedTransformType createDefault(Type type) { + return new DerivedTransformType(new TransformTypeRef(null), 0, Kind.NONE, type); + } + + /** + * Create a derived transform type from a string. + *
Example: "blockpos consumer[][]" would create a derived type of a 2D array of blockpos consumers + * @param s The string to load it from + * @param transformLookup The transform types + * @return The parsed derived type + */ + public static DerivedTransformType fromString(String s, Map transformLookup) { + int arrIndex = s.indexOf('['); + int arrDimensionality = 0; + if (arrIndex != -1) { + arrDimensionality = (s.length() - arrIndex) / 2; + s = s.substring(0, arrIndex); + } + + String[] parts = s.split(" "); + Kind kind; + TransformType transformType = transformLookup.get(parts[0]); + if (parts.length == 1) { + kind = Kind.NONE; + } else { + kind = Kind.fromString(parts[1]); + } + + return new DerivedTransformType(new TransformTypeRef(transformType), arrDimensionality, kind, getRawType(transformType, kind)); + } + + /** + * Creates a DerivedTransformType + * @param transformType The transform type of the derived type + * @return A derived type with the given transform type and no array dimensionality + */ + public static DerivedTransformType of(@Nullable TransformType transformType) { + return new DerivedTransformType(new TransformTypeRef(transformType), 0, Kind.NONE, getRawType(transformType, Kind.NONE)); + } + + /** + * Creates a DerivedTransformType + * @return A derived type with no array dimensionality + */ + public static DerivedTransformType of(TransformType transformType, Kind kind) { + return new DerivedTransformType(new TransformTypeRef(transformType), 0, kind, getRawType(transformType, kind)); + } + + /** + * @param transform A transform type + * @return The original type of a value with the given transform type and the kind of this derived type + */ + public Type getRawType(TransformType transform) { + return getRawType(transform, this.kind); + } + + private static Type getRawType(TransformType transform, Kind kind) { + return switch (kind) { + case NONE -> transform.getFrom(); + case PREDICATE -> transform.getOriginalPredicateType(); + case CONSUMER -> transform.getOriginalConsumerType(); + }; + } + + /** + * @param type A type + * @return The potential kind of the type. If unknown, returns NONE + */ + public static Kind getKindFor(@Nullable Type type, Config config) { + while (type != null) { + if (type.getSort() == Type.OBJECT) { + for (var t: config.getTypeInfo().ancestry(type)) { + if (config.getRegularTypes().contains(t)) { + return Kind.NONE; + } else if (config.getConsumerTypes().contains(t)) { + return Kind.CONSUMER; + } else if (config.getPredicateTypes().contains(t)) { + return Kind.PREDICATE; + } + } + } else { + if (config.getRegularTypes().contains(type)) { + return Kind.NONE; + } else if (config.getConsumerTypes().contains(type)) { + return Kind.CONSUMER; + } else if (config.getPredicateTypes().contains(type)) { + return Kind.PREDICATE; + } + } + + if (type.getSort() != Type.ARRAY) { + break; + } else { + type = type.getElementType(); + } + } + + + return Kind.NONE; + } + + /** + * @return Same as {@link #resultingTypes} but only returns the first element. + * @throws IllegalStateException If {@link #resultingTypes()} returns more than one element + * use {@link #resultingTypes()} + */ + public Type getSingleType() { + List allTypes = this.resultingTypes(); + + if (allTypes.size() != 1) { + throw new IllegalStateException("Cannot get single type of a transform type with multiple types"); + } + + return allTypes.get(0); + } + + /** + * @return The list of types that should replace a value with this derived type. + * If this represents a value that does not need to be transformed, it returns a singleton list with the original type. + */ + public List resultingTypes() { + if (transformType.getValue() == null) { + if (this.originalType != null) { + return List.of(this.originalType); + } else { + return List.of(Type.VOID_TYPE); + } + } + + List types = new ArrayList<>(); + if (kind == Kind.NONE) { + types.addAll(Arrays.asList(transformType.getValue().getTo())); + } else if (kind == Kind.CONSUMER) { + types.add(transformType.getValue().getTransformedConsumerType()); + } else { + types.add(transformType.getValue().getTransformedPredicateType()); + } + + if (arrayDimensionality != 0) { + types = types.stream().map(t -> Type.getType("[".repeat(arrayDimensionality) + t.getDescriptor())).collect(Collectors.toList()); + } + + return types; + } + + public int[] getIndices() { + List types = resultingTypes(); + int[] indices = new int[types.size()]; + + for (int i = 1; i < indices.length; i++) { + indices[i] = indices[i - 1] + types.get(i - 1).getSize(); + } + + return indices; + } + + /** + * Gets the transform size (in local var slots) of a transform value with this derived type. + * @return The size + */ + public int getTransformedSize() { + if (transformType.getValue() == null) { + return Objects.requireNonNull(this.originalType).getSize(); + } + + if (kind == Kind.NONE && this.arrayDimensionality == 0) { + return transformType.getValue().getTransformedSize(); + } else if (this.arrayDimensionality != 0) { + return this.resultingTypes().size(); + } else { + return 1; + } + } + + public enum Kind { + NONE, + PREDICATE, + CONSUMER; + + public static Kind fromString(String part) { + return switch (part.toLowerCase(Locale.ROOT)) { + case "predicate" -> PREDICATE; + case "consumer" -> CONSUMER; + default -> { + System.err.println("Unknown kind: " + part); + yield NONE; + } + }; + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + DerivedTransformType that = (DerivedTransformType) o; + return arrayDimensionality == that.arrayDimensionality && transformType.getValue() == that.transformType.getValue() && kind == that.kind; + } + + @Override + public int hashCode() { + return Objects.hash(transformType, arrayDimensionality, kind); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + + if (transformType.getValue() == null) { + if (kind == Kind.NONE) { + return "No transform"; + } else { + sb.append(kind.name().toLowerCase(Locale.ROOT)); + sb.append(" candidate"); + return sb.toString(); + } + } + + sb.append(transformType.getValue()); + + if (kind != Kind.NONE) { + sb.append(" "); + sb.append(kind.name().toLowerCase(Locale.ROOT)); + } + + if (arrayDimensionality > 0) { + for (int i = 0; i < arrayDimensionality; i++) { + sb.append("[]"); + } + } + + return sb.toString(); + } + + /** + * Converts a value into the transformed value + * + * @param originalSupplier The supplier of the original value + * @param transformers A set. This should be unique per-class. + * @param className The name of the class being transformed. + * + * @return The transformed value + */ + public InsnList convertToTransformed(Supplier originalSupplier, Set transformers, String className) { + if (transformType.getValue() == null) { + //No transform needed + return originalSupplier.get(); + } + + if (arrayDimensionality != 0) { + throw new IllegalStateException("Not supported yet"); + } + + if (kind == Kind.NONE) { + return transformType.getValue().convertToTransformed(originalSupplier); + } else if (kind == Kind.CONSUMER || kind == Kind.PREDICATE) { + /* + * Example: + * LongConsumer c = ...; + * Becomes: + * Int3Consumer c1 = (x, y, z) -> c.accept(BlockPos.asLong(x, y, z)); + * We need to create a new lambda which turns the takes transformed values, turns them into the original values, and then calls the original lambda. + */ + + InsnList list = new InsnList(); + list.add(originalSupplier.get()); + + Type returnType; + String transformerType; + String methodName; + + Type originalLambdaType; + Type transformedLambdaType; + + if (kind == Kind.CONSUMER) { + returnType = Type.VOID_TYPE; + transformerType = "consumer"; + methodName = "accept"; + originalLambdaType = transformType.getValue().getOriginalConsumerType(); + transformedLambdaType = transformType.getValue().getTransformedConsumerType(); + } else { + returnType = Type.BOOLEAN_TYPE; + transformerType = "predicate"; + methodName = "test"; + originalLambdaType = transformType.getValue().getOriginalPredicateType(); + transformedLambdaType = transformType.getValue().getTransformedPredicateType(); + } + + String returnDescriptor = returnType.getDescriptor(); + + //Name that is unique per-type for easy lookup + String transformerName = "lambdaTransformer_" + transformerType + "_" + transformType.getValue().getName(); + + //Find if the transformer has already been created + MethodNode transformer = null; + for (MethodNode mn : transformers) { + if (mn.name.equals(transformerName)) { + transformer = mn; + } + } + + if (transformer == null) { + /* + * This is the lambda method that the call will get passed to. For the above example this would be: + * private static void lambdaTransformer_consumer_blockpos(LongConsumer c, int x, int y, int z){ + * c.accept(BlockPos.asLong(x, y, z)); + * } + */ + int access = Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC | Opcodes.ACC_SYNTHETIC; //Remove synthetic flag to make it easier to debug + + //Create the new descriptor for the lambda + StringBuilder descBuilder = new StringBuilder(); + descBuilder.append("("); + descBuilder.append(originalLambdaType.getDescriptor()); + for (Type t : transformType.getValue().getTo()) { + descBuilder.append(t.getDescriptor()); + } + descBuilder.append(")"); + descBuilder.append(returnDescriptor); + + transformer = new MethodNode(Opcodes.ASM9, access, transformerName, descBuilder.toString(), null, null); + + InsnList l = new InsnList(); + //Load original consumer + l.add(new VarInsnNode(Opcodes.ALOAD, 0)); + + //Load transformed values + int index = 1; + for (Type t : transformType.getValue().getTo()) { + l.add(new VarInsnNode(t.getOpcode(Opcodes.ILOAD), index)); + index += t.getSize(); + } + + //Transform the transformed values back into the original values + l.add(transformType.getValue().getToOriginal().callNode()); + + //Call original + String newDesc = Type.getMethodDescriptor(returnType, transformType.getValue().getFrom()); + l.add(new MethodInsnNode(Opcodes.INVOKEINTERFACE, originalLambdaType.getInternalName(), methodName, newDesc, true)); + l.add(new InsnNode(returnType.getOpcode(Opcodes.IRETURN))); + + //Actually insert the code + transformer.instructions.add(l); + + //Add the lambda to the transformers + transformers.add(transformer); + } + + //Create the actual MethodHandle + Handle transformerHandle = new Handle(Opcodes.H_INVOKESTATIC, className, transformer.name, transformer.desc, false); + + list.add(new InvokeDynamicInsnNode( + methodName, + "(" + originalLambdaType.getDescriptor() + ")" + transformedLambdaType.getDescriptor(), + new Handle( + Opcodes.H_INVOKESTATIC, + "java/lang/invoke/LambdaMetafactory", + "metafactory", + "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;" + + "Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;", + false + ), + Type.getMethodType(returnType, transformType.getValue().getTo()), + transformerHandle, + Type.getMethodType(returnType, transformType.getValue().getTo()) + )); + + return list; + } + + throw new IllegalArgumentException("Unsupported kind: " + kind); + } +} diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/analysis/FutureMethodBinding.java b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/analysis/FutureMethodBinding.java new file mode 100644 index 000000000..6a3d1b32f --- /dev/null +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/analysis/FutureMethodBinding.java @@ -0,0 +1,10 @@ +package io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.analysis; + +/** + * Stores information about values that should be bound to a method. But the method + * has not been analyzed yet + * @param offset The method parameter index of the first value that should be bound + * @param parameters The values that should be bound to the method parameters + */ +public record FutureMethodBinding(int offset, TransformTrackingValue... parameters) { +} diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/analysis/TransformTrackingInterpreter.java b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/analysis/TransformTrackingInterpreter.java new file mode 100644 index 000000000..504164e88 --- /dev/null +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/analysis/TransformTrackingInterpreter.java @@ -0,0 +1,567 @@ +package io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.analysis; + +import static org.objectweb.asm.Opcodes.*; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.config.Config; +import io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.config.MethodParameterInfo; +import io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.config.TransformType; +import io.github.opencubicchunks.cubicchunks.mixin.transform.util.ASMUtil; +import io.github.opencubicchunks.cubicchunks.mixin.transform.util.AncestorHashMap; +import io.github.opencubicchunks.cubicchunks.mixin.transform.util.FieldID; +import io.github.opencubicchunks.cubicchunks.mixin.transform.util.MethodID; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.objectweb.asm.Handle; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.FieldInsnNode; +import org.objectweb.asm.tree.IntInsnNode; +import org.objectweb.asm.tree.InvokeDynamicInsnNode; +import org.objectweb.asm.tree.LdcInsnNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MultiANewArrayInsnNode; +import org.objectweb.asm.tree.TypeInsnNode; +import org.objectweb.asm.tree.analysis.AnalyzerException; +import org.objectweb.asm.tree.analysis.BasicInterpreter; +import org.objectweb.asm.tree.analysis.Frame; +import org.objectweb.asm.tree.analysis.Interpreter; + +/** + * An interpreter which infers transforms that should be applied to values + */ +public class TransformTrackingInterpreter extends Interpreter { + private final Config config; + private final Map localVarOverrides = new HashMap<>(); + + private Map resultLookup = new HashMap<>(); + private Map> futureMethodBindings; + private ClassNode currentClass; + private AncestorHashMap fieldBindings; + + /** + * Constructs a new {@link Interpreter}. + * + * @param api the ASM API version supported by this interpreter. Must be one of {@link Opcodes#ASM4}, {@link Opcodes#ASM5}, {@link Opcodes#ASM6} or {@link Opcodes#ASM7}. + */ + public TransformTrackingInterpreter(int api, Config config) { + super(api); + this.config = config; + this.fieldBindings = new AncestorHashMap<>(config.getTypeInfo()); + } + + public void reset() { + localVarOverrides.clear(); + } + + @Override + public @Nullable TransformTrackingValue newValue(@Nullable Type type) { + if (type == null) { + return new TransformTrackingValue(null, fieldBindings, config); + } + if (type.getSort() == Type.VOID) return null; + if (type.getSort() == Type.METHOD) throw new RuntimeException("Method type not supported"); + return new TransformTrackingValue(type, fieldBindings, config); + } + + @Override + public @Nullable TransformTrackingValue newParameterValue(boolean isInstanceMethod, int local, Type type) { + //Use parameter overrides to try to get the types + if (type == Type.VOID_TYPE) return null; + TransformTrackingValue value = new TransformTrackingValue(type, fieldBindings, config); + if (localVarOverrides.containsKey(local)) { + value.setTransformType(localVarOverrides.get(local)); + } + return value; + } + + @Override + public TransformTrackingValue newOperation(AbstractInsnNode insn) throws AnalyzerException { + return switch (insn.getOpcode()) { + case Opcodes.ACONST_NULL -> new TransformTrackingValue(BasicInterpreter.NULL_TYPE, fieldBindings, config); + case Opcodes.ICONST_M1, Opcodes.ICONST_0, Opcodes.ICONST_1, Opcodes.ICONST_2, Opcodes.ICONST_3, + Opcodes.ICONST_4, Opcodes.ICONST_5 -> new TransformTrackingValue(Type.INT_TYPE, fieldBindings, config); + case Opcodes.LCONST_0, Opcodes.LCONST_1 -> new TransformTrackingValue(Type.LONG_TYPE, fieldBindings, config); + case Opcodes.FCONST_0, Opcodes.FCONST_1, Opcodes.FCONST_2 -> new TransformTrackingValue(Type.FLOAT_TYPE, fieldBindings, config); + case Opcodes.DCONST_0, Opcodes.DCONST_1 -> new TransformTrackingValue(Type.DOUBLE_TYPE, fieldBindings, config); + case Opcodes.BIPUSH -> new TransformTrackingValue(Type.BYTE_TYPE, fieldBindings, config); + case Opcodes.SIPUSH -> new TransformTrackingValue(Type.SHORT_TYPE, fieldBindings, config); + case Opcodes.LDC -> { + Object value = ((LdcInsnNode) insn).cst; + if (value instanceof Integer) { + yield new TransformTrackingValue(Type.INT_TYPE, fieldBindings, config); + } else if (value instanceof Float) { + yield new TransformTrackingValue(Type.FLOAT_TYPE, fieldBindings, config); + } else if (value instanceof Long) { + yield new TransformTrackingValue(Type.LONG_TYPE, fieldBindings, config); + } else if (value instanceof Double) { + yield new TransformTrackingValue(Type.DOUBLE_TYPE, fieldBindings, config); + } else if (value instanceof String) { + yield new TransformTrackingValue(Type.getObjectType("java/lang/String"), fieldBindings, config); + } else if (value instanceof Type) { + int sort = ((Type) value).getSort(); + if (sort == Type.OBJECT || sort == Type.ARRAY) { + yield new TransformTrackingValue(Type.getObjectType("java/lang/Class"), fieldBindings, config); + } else if (sort == Type.METHOD) { + yield new TransformTrackingValue(Type.getObjectType("java/lang/invoke/MethodType"), fieldBindings, config); + } else { + throw new AnalyzerException(insn, "Illegal LDC value " + value); + } + } + throw new IllegalStateException("This shouldn't happen"); + } + case Opcodes.JSR -> new TransformTrackingValue(Type.VOID_TYPE, fieldBindings, config); + case Opcodes.GETSTATIC -> new TransformTrackingValue(Type.getType(((FieldInsnNode) insn).desc), fieldBindings, config); + case Opcodes.NEW -> new TransformTrackingValue(Type.getObjectType(((TypeInsnNode) insn).desc), fieldBindings, config); + default -> throw new IllegalStateException("Unexpected value: " + insn.getType()); + }; + } + + @Override + public TransformTrackingValue copyOperation(AbstractInsnNode insn, TransformTrackingValue value) { + TransformTrackingValue result = new TransformTrackingValue(value.getType(), value.getTransform(), fieldBindings, config); + TransformTrackingValue.setSameType(result, value); + return result; + } + + @Override + public @Nullable TransformTrackingValue unaryOperation(AbstractInsnNode insn, TransformTrackingValue value) throws AnalyzerException { + + FieldInsnNode fieldInsnNode; + switch (insn.getOpcode()) { + case INEG: + case IINC: + case L2I: + case F2I: + case D2I: + case I2B: + case I2C: + case I2S: + case INSTANCEOF: + case ARRAYLENGTH: + return new TransformTrackingValue(Type.INT_TYPE, fieldBindings, config); + case FNEG: + case I2F: + case L2F: + case D2F: + return new TransformTrackingValue(Type.FLOAT_TYPE, fieldBindings, config); + case LNEG: + case I2L: + case F2L: + case D2L: + return new TransformTrackingValue(Type.LONG_TYPE, fieldBindings, config); + case DNEG: + case I2D: + case L2D: + case F2D: + return new TransformTrackingValue(Type.DOUBLE_TYPE, fieldBindings, config); + case IFEQ: + case IFNE: + case IFLT: + case IFGE: + case IFGT: + case IFLE: + case TABLESWITCH: + case LOOKUPSWITCH: + case IRETURN: + case LRETURN: + case FRETURN: + case DRETURN: + case ARETURN: + return null; + case PUTSTATIC: + fieldInsnNode = (FieldInsnNode) insn; + + if (fieldInsnNode.owner.equals(currentClass.name)) { + FieldID fieldAndDesc = new FieldID(Type.getObjectType(fieldInsnNode.owner), fieldInsnNode.name, Type.getType(fieldInsnNode.desc)); + TransformTrackingValue field = fieldBindings.get(fieldAndDesc); + + if (field != null) { + TransformTrackingValue.setSameType(field, value); + } + } + return null; + case GETFIELD: { + //Add field source and set the value to have the same transform as the field + fieldInsnNode = (FieldInsnNode) insn; + TransformTrackingValue fieldValue = new TransformTrackingValue(Type.getType(((FieldInsnNode) insn).desc), fieldBindings, config); + + if (fieldInsnNode.owner.equals(currentClass.name)) { + FieldID fieldAndDesc = new FieldID(Type.getObjectType(fieldInsnNode.owner), fieldInsnNode.name, Type.getType(fieldInsnNode.desc)); + TransformTrackingValue fieldBinding = fieldBindings.get(fieldAndDesc); + if (fieldBinding != null) { + TransformTrackingValue.setSameType(fieldValue, fieldBinding); + } + } + + return fieldValue; + } + case NEWARRAY: + switch (((IntInsnNode) insn).operand) { + case T_BOOLEAN: + return new TransformTrackingValue(Type.getType("[Z"), fieldBindings, config); + case T_CHAR: + return new TransformTrackingValue(Type.getType("[C"), fieldBindings, config); + case T_BYTE: + return new TransformTrackingValue(Type.getType("[B"), fieldBindings, config); + case T_SHORT: + return new TransformTrackingValue(Type.getType("[S"), fieldBindings, config); + case T_INT: + return new TransformTrackingValue(Type.getType("[I"), fieldBindings, config); + case T_FLOAT: + return new TransformTrackingValue(Type.getType("[F"), fieldBindings, config); + case T_DOUBLE: + return new TransformTrackingValue(Type.getType("[D"), fieldBindings, config); + case T_LONG: + return new TransformTrackingValue(Type.getType("[J"), fieldBindings, config); + default: + break; + } + throw new AnalyzerException(insn, "Invalid array type"); + case ANEWARRAY: + return new TransformTrackingValue(Type.getType("[" + Type.getObjectType(((TypeInsnNode) insn).desc)), fieldBindings, config); + case ATHROW: + return null; + case CHECKCAST: + return new TransformTrackingValue(Type.getObjectType(((TypeInsnNode) insn).desc), fieldBindings, config); + case MONITORENTER: + case MONITOREXIT: + case IFNULL: + case IFNONNULL: + return null; + default: + throw new AssertionError(); + } + } + + @Override + public @Nullable TransformTrackingValue binaryOperation(AbstractInsnNode insn, TransformTrackingValue value1, TransformTrackingValue value2) { + TransformTrackingValue value; + + switch (insn.getOpcode()) { + case IALOAD: + case BALOAD: + case CALOAD: + case SALOAD: + case IADD: + case ISUB: + case IMUL: + case IDIV: + case IREM: + case ISHL: + case ISHR: + case IUSHR: + case IAND: + case IOR: + case IXOR: + value = new TransformTrackingValue(Type.INT_TYPE, fieldBindings, config); + if (insn.getOpcode() == IALOAD || insn.getOpcode() == BALOAD || insn.getOpcode() == CALOAD || insn.getOpcode() == SALOAD) { + TransformTrackingValue.setSameType(value1, value); + } + return value; + case FALOAD: + case FADD: + case FSUB: + case FMUL: + case FDIV: + case FREM: + value = new TransformTrackingValue(Type.FLOAT_TYPE, fieldBindings, config); + if (insn.getOpcode() == FALOAD) { + TransformTrackingValue.setSameType(value1, value); + } + return value; + case LALOAD: + case LADD: + case LSUB: + case LMUL: + case LDIV: + case LREM: + case LSHL: + case LSHR: + case LUSHR: + case LAND: + case LOR: + case LXOR: + value = new TransformTrackingValue(Type.LONG_TYPE, fieldBindings, config); + if (insn.getOpcode() == LALOAD) { + TransformTrackingValue.setSameType(value1, value); + } + return value; + case DALOAD: + case DADD: + case DSUB: + case DMUL: + case DDIV: + case DREM: + value = new TransformTrackingValue(Type.DOUBLE_TYPE, fieldBindings, config); + if (insn.getOpcode() == DALOAD) { + TransformTrackingValue.setSameType(value1, value); + } + return value; + case AALOAD: + value = new TransformTrackingValue(ASMUtil.getArrayElement(value1.getType()), fieldBindings, config); + TransformTrackingValue.setSameType(value1, value); + return value; + case LCMP: + case FCMPL: + case FCMPG: + case DCMPL: + case DCMPG: + TransformTrackingValue.setSameType(value1, value2); + return new TransformTrackingValue(Type.INT_TYPE, fieldBindings, config); + case IF_ICMPEQ: + case IF_ICMPNE: + case IF_ICMPLT: + case IF_ICMPGE: + case IF_ICMPGT: + case IF_ICMPLE: + case IF_ACMPEQ: + case IF_ACMPNE: + return null; + case PUTFIELD: + FieldInsnNode fieldInsnNode = (FieldInsnNode) insn; + + if (fieldInsnNode.owner.equals(currentClass.name)) { + FieldID fieldAndDesc = new FieldID(Type.getObjectType(fieldInsnNode.owner), fieldInsnNode.name, Type.getType(fieldInsnNode.desc)); + TransformTrackingValue field = fieldBindings.get(fieldAndDesc); + + if (field != null) { + TransformTrackingValue.setSameType(field, value2); + } + } + return null; + default: + throw new AssertionError(); + } + } + + @Override + public @Nullable TransformTrackingValue ternaryOperation(AbstractInsnNode insn, TransformTrackingValue value1, TransformTrackingValue value2, TransformTrackingValue value3) { + return null; + } + + @Override + public @Nullable TransformTrackingValue naryOperation(AbstractInsnNode insn, List values) { + int opcode = insn.getOpcode(); + if (opcode == MULTIANEWARRAY) { + return new TransformTrackingValue(Type.getType(((MultiANewArrayInsnNode) insn).desc), fieldBindings, config); + } else if (opcode == INVOKEDYNAMIC) { + return invokeDynamicOperation(insn, values); + } else { + return methodCallOperation(insn, values, opcode); + } + } + + @Nullable + private TransformTrackingValue methodCallOperation(AbstractInsnNode insn, List values, int opcode) { + //Create bindings to the method parameters + MethodInsnNode methodCall = (MethodInsnNode) insn; + Type returnType = Type.getReturnType(methodCall.desc); + + MethodID methodID = new MethodID(methodCall.owner, methodCall.name, methodCall.desc, MethodID.CallType.fromOpcode(opcode)); + + bindValues(methodID, 0, values.toArray(new TransformTrackingValue[0])); + + List possibilities = config.getMethodParameterInfo().get(methodID); + + if (possibilities != null) { + TransformTrackingValue returnValue = null; + + if (returnType != null) { + returnValue = new TransformTrackingValue(returnType, fieldBindings, config); + } + + for (MethodParameterInfo info : possibilities) { + TransformTrackingValue[] parameterValues = new TransformTrackingValue[info.getParameterTypes().length]; + for (int i = 0; i < values.size(); i++) { + parameterValues[i] = values.get(i); + } + + UnresolvedMethodTransform unresolvedTransform = new UnresolvedMethodTransform(info, returnValue, parameterValues); + + int checkResult = unresolvedTransform.check(); + if (checkResult == 0) { + if (returnValue != null) { + returnValue.possibleTransformChecks.add(unresolvedTransform); + } + + for (TransformTrackingValue parameterValue : parameterValues) { + parameterValue.possibleTransformChecks.add(unresolvedTransform); + } + } else if (checkResult == 1) { + unresolvedTransform.accept(); + break; + } + } + + return returnValue; + } + + if (methodCall.owner.equals("java/util/Arrays") && methodCall.name.equals("fill")) { + //TODO: Maybe make this kind of thing configurable? + TransformTrackingValue array = values.get(0); + TransformTrackingValue value = values.get(1); + + TransformTrackingValue.setSameType(array, value); + } + + if (returnType.getSort() == Type.VOID) return null; + + return new TransformTrackingValue(returnType, fieldBindings, config); + } + + @Nullable private TransformTrackingValue invokeDynamicOperation(AbstractInsnNode insn, List values) { + //Bind the lambda captured parameters and lambda types + InvokeDynamicInsnNode node = (InvokeDynamicInsnNode) insn; + Type returnType = Type.getReturnType(node.desc); + + TransformTrackingValue ret = new TransformTrackingValue(returnType, fieldBindings, config); + + //Make sure this is LambdaMetafactory.metafactory + if (node.bsm.getOwner().equals("java/lang/invoke/LambdaMetafactory") && node.bsm.getName().equals("metafactory")) { + //Bind values + Handle referenceMethod = (Handle) node.bsmArgs[1]; + MethodID.CallType callType = getDynamicCallType(referenceMethod); + MethodID methodID = new MethodID(referenceMethod.getOwner(), referenceMethod.getName(), referenceMethod.getDesc(), callType); + + bindValues(methodID, 0, values.toArray(new TransformTrackingValue[0])); + + boolean isTransformPredicate = ret.getTransform().getKind() == DerivedTransformType.Kind.PREDICATE; + boolean isTransformConsumer = ret.getTransform().getKind() == DerivedTransformType.Kind.CONSUMER; + + if (isTransformConsumer || isTransformPredicate) { + int offset = values.size() + callType.getOffset(); + bindValues(methodID, offset, ret); + } + } + + if (returnType.getSort() == Type.VOID) return null; + + return ret; + } + + //Sets the values to have the same types as the method parameters. If the method hasn't + //been analyzed, it will bind them later. + private void bindValues(MethodID methodID, int offset, TransformTrackingValue... values) { + if (resultLookup.containsKey(methodID)) { + bindValuesToMethod(resultLookup.get(methodID), offset, values); + } else { + futureMethodBindings.computeIfAbsent(methodID, k -> new ArrayList<>()).add( + new FutureMethodBinding(offset, values) + ); + } + } + + @NotNull private MethodID.CallType getDynamicCallType(Handle referenceMethod) { + return switch (referenceMethod.getTag()) { + case H_INVOKESTATIC -> MethodID.CallType.STATIC; + case H_INVOKEVIRTUAL -> MethodID.CallType.VIRTUAL; + case H_INVOKESPECIAL, H_NEWINVOKESPECIAL -> MethodID.CallType.SPECIAL; + case H_INVOKEINTERFACE -> MethodID.CallType.INTERFACE; + default -> throw new AssertionError(); + }; + } + + @Override + public void returnOperation(AbstractInsnNode insn, TransformTrackingValue value, TransformTrackingValue expected) throws AnalyzerException { + if (value.getTransformType() != null) { + if (expected.transformedTypes().size() != 1) { + //A method cannot return multiple values. + throw new AnalyzerException(insn, "A method cannot return a value that would be transformed into multiple values"); + } + } + } + + @Override + public TransformTrackingValue merge(TransformTrackingValue value1, TransformTrackingValue value2) { + if (!Objects.equals(value1.getType(), value2.getType())) { + //There are a few cases where this can be reached and is fine. + //Mainly when two branches of code use the same var slot for different kinds of values + if (value1 == null) { + return value2; + } else if (value2 == null) { + return value1; + } else { + return value1; + } + } + + return value1.merge(value2); + } + + public void setLocalVarOverrides(MethodID id, List<@Nullable TransformType> parameterOverrides) { + this.localVarOverrides.clear(); + + if (parameterOverrides.isEmpty()) return; + + int localVarIdx = 0; + int parameterIdx = 0; + + if (!id.isStatic()) { + this.localVarOverrides.put(localVarIdx++, parameterOverrides.get(parameterIdx++)); + } + + Type[] argumentTypes = id.getDescriptor().getArgumentTypes(); + int typeIdx = 0; + + for (; parameterIdx < parameterOverrides.size(); parameterIdx++) { + this.localVarOverrides.put(localVarIdx, parameterOverrides.get(parameterIdx)); + + localVarIdx += argumentTypes[typeIdx++].getSize(); + } + } + + public static void bindValuesToMethod(AnalysisResults methodResults, int parameterOffset, TransformTrackingValue... parameters) { + Frame firstFrame = methodResults.frames()[0]; + + Type[] argumentTypes = Type.getArgumentTypes(methodResults.methodNode().desc); + Type[] allTypes; + if (!ASMUtil.isStatic(methodResults.methodNode())) { + // Add the first element representing 'this' + allTypes = new Type[argumentTypes.length + 1]; + allTypes[0] = Type.getObjectType("java/lang/Object"); //The actual type doesn't matter + System.arraycopy(argumentTypes, 0, allTypes, 1, argumentTypes.length); + } else { + allTypes = argumentTypes; + } + + int paramIndex = 0; + int varIndex = 0; + + for (int i = 0; i < parameterOffset; i++) { + varIndex += allTypes[i].getSize(); + } + + for (Type parameterType : allTypes) { + if (paramIndex >= parameters.length) { //This can happen for invokedynamic + break; + } + TransformTrackingValue.setSameType(firstFrame.getLocal(varIndex), parameters[paramIndex]); + varIndex += parameterType.getSize(); + paramIndex++; + } + } + + public void setResultLookup(Map analysisResults) { + this.resultLookup = analysisResults; + } + + public void setFutureBindings(Map> bindings) { + this.futureMethodBindings = bindings; + } + + public void setCurrentClass(ClassNode currentClass) { + this.currentClass = currentClass; + } + + public void setFieldBindings(AncestorHashMap fieldPseudoValues) { + this.fieldBindings = fieldPseudoValues; + } +} diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/analysis/TransformTrackingValue.java b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/analysis/TransformTrackingValue.java new file mode 100644 index 000000000..ae8f6642a --- /dev/null +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/analysis/TransformTrackingValue.java @@ -0,0 +1,182 @@ +package io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.analysis; + +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +import io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.config.Config; +import io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.config.TransformType; +import io.github.opencubicchunks.cubicchunks.mixin.transform.util.ASMUtil; +import io.github.opencubicchunks.cubicchunks.mixin.transform.util.AncestorHashMap; +import io.github.opencubicchunks.cubicchunks.mixin.transform.util.FieldID; +import org.jetbrains.annotations.Nullable; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.analysis.Value; + +/** + * A value that infers its transform type and tracks the instructions that created it, consumed it and + * the fields it comes from + */ +public class TransformTrackingValue implements Value { + final Set possibleTransformChecks = new HashSet<>(); //Used to track possible transform checks + + private final @Nullable Type type; + private final AncestorHashMap pseudoValues; + private final DerivedTransformType transform; + private final Set valuesWithSameType = new HashSet<>(); + private final Config config; + + public TransformTrackingValue(@Nullable Type type, AncestorHashMap fieldPseudoValues, Config config) { + this(type, DerivedTransformType.createDefault(type), fieldPseudoValues, config); + } + + public TransformTrackingValue(@Nullable Type type, DerivedTransformType transform, AncestorHashMap fieldPseudoValues, Config config) { + this.type = type; + this.transform = transform; + this.pseudoValues = fieldPseudoValues; + this.config = config; + + this.transform.getTransformTypePtr().addTrackingValue(this); + this.transform.setKind(DerivedTransformType.getKindFor(type, config)); + } + + public TransformTrackingValue merge(TransformTrackingValue other) { + if (transform.getTransformType() != null && other.transform.getTransformType() != null && transform.getTransformType() != other.transform.getTransformType()) { + throw new RuntimeException("Merging incompatible values. (Different transform types had already been assigned)"); + } + + setSameType(this, other); + + return new TransformTrackingValue( + type, + transform, + pseudoValues, + config + ); + } + + public @Nullable TransformType getTransformType() { + return transform.getTransformType(); + } + + public void setTransformType(TransformType transformType) { + if (this.transform.getTransformType() != null && transformType != this.transform.getTransformType()) { + throw new RuntimeException("Transform type already set"); + } + + if (this.transform.getTransformType() == transformType) { + return; + } + + Type rawType = this.transform.getRawType(transformType); + int dimension = ASMUtil.getDimensions(this.type) - ASMUtil.getDimensions(rawType); + this.transform.setArrayDimensionality(dimension); + + this.transform.getTransformTypePtr().setValue(transformType); + } + + public void updateType(TransformType newType) { + //Set appropriate array dimensions + Set copy = new HashSet<>(valuesWithSameType); + valuesWithSameType.clear(); //To prevent infinite recursion + + for (TransformTrackingValue value : copy) { + value.setTransformType(newType); + } + + Type rawType = this.transform.getRawType(newType); + int dimension = ASMUtil.getDimensions(this.type) - ASMUtil.getDimensions(rawType); + this.transform.setArrayDimensionality(dimension); + + for (UnresolvedMethodTransform check : possibleTransformChecks) { + int validity = check.check(); + if (validity == -1) { + check.reject(); + } else if (validity == 1) { + check.accept(); + } + } + } + + @Override + public int getSize() { + return type == Type.LONG_TYPE || type == Type.DOUBLE_TYPE ? 2 : 1; + } + + @Override public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + TransformTrackingValue that = (TransformTrackingValue) o; + return Objects.equals(type, that.type) && Objects.equals(transform, that.transform); + } + + @Override public int hashCode() { + return Objects.hash(type, transform); + } + + public @Nullable Type getType() { + return type; + } + + public static void setSameType(TransformTrackingValue first, TransformTrackingValue second) { + if (first.type == null || second.type == null) { + //System.err.println("WARNING: Attempted to set same subType on null subType"); + return; + } + + if (first.getTransformType() == null && second.getTransformType() == null) { + first.valuesWithSameType.add(second); + second.valuesWithSameType.add(first); + return; + } + + if (first.getTransformType() != null && second.getTransformType() != null && first.getTransformType() != second.getTransformType()) { + throw new RuntimeException("Merging incompatible values. (Different types had already been assigned)"); + } + + if (first.getTransformType() != null) { + second.getTransformTypeRef().setValue(first.getTransformType()); + } else if (second.getTransformType() != null) { + first.getTransformTypeRef().setValue(second.getTransformType()); + } + } + + public TransformTypeRef getTransformTypeRef() { + return transform.getTransformTypePtr(); + } + + @Override + public String toString() { + if (type == null) { + return "null"; + } + StringBuilder sb = new StringBuilder(type.toString()); + + if (transform.getTransformType() != null) { + sb.append(" (").append(transform).append(")"); + } + + return sb.toString(); + } + + public DerivedTransformType getTransform() { + return transform; + } + + public int getTransformedSize() { + if (transform.getTransformType() == null) { + return getSize(); + } else { + return transform.getTransformedSize(); + } + } + + public List transformedTypes() { + return this.transform.resultingTypes(); + } + + public boolean isTransformed() { + return this.transform.getTransformType() != null; + } +} diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/analysis/TransformTypeRef.java b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/analysis/TransformTypeRef.java new file mode 100644 index 000000000..54466e59d --- /dev/null +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/analysis/TransformTypeRef.java @@ -0,0 +1,52 @@ +package io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.analysis; + +import java.util.Set; + +import io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.config.TransformType; +import it.unimi.dsi.fastutil.Hash; +import it.unimi.dsi.fastutil.objects.ObjectOpenCustomHashSet; +import org.jetbrains.annotations.Nullable; + +public class TransformTypeRef { + private @Nullable TransformType value; + private final Set trackingValues = new ObjectOpenCustomHashSet<>(new Hash.Strategy<>() { + @Override public int hashCode(TransformTrackingValue transformTrackingValue) { + return System.identityHashCode(transformTrackingValue); + } + + @Override public boolean equals(TransformTrackingValue transformTrackingValue, TransformTrackingValue k1) { + return transformTrackingValue == k1; + } + }); + + public TransformTypeRef(@Nullable TransformType value) { + this.value = value; + } + + public void addTrackingValue(TransformTrackingValue trackingValue) { + trackingValues.add(trackingValue); + } + + private void updateType(@Nullable TransformType oldType, TransformType newType) { + if (oldType == newType) { + return; + } + + for (TransformTrackingValue trackingValue : trackingValues) { + trackingValue.updateType(newType); + } + } + + public void setValue(TransformType value) { + TransformType oldType = this.value; + this.value = value; + + if (oldType != value) { + updateType(oldType, value); + } + } + + public @Nullable TransformType getValue() { + return value; + } +} diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/analysis/UnresolvedMethodTransform.java b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/analysis/UnresolvedMethodTransform.java new file mode 100644 index 000000000..9a4cb287b --- /dev/null +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/analysis/UnresolvedMethodTransform.java @@ -0,0 +1,45 @@ +package io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.analysis; + +import io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.config.MethodParameterInfo; + +/** + * A method transform which may need to be applied + */ +public record UnresolvedMethodTransform(MethodParameterInfo transform, TransformTrackingValue returnValue, TransformTrackingValue[] parameters) { + public int check() { + return transform.getTransformCondition().checkValidity(returnValue, parameters); + } + + public void reject() { + if (returnValue != null) { + returnValue.possibleTransformChecks.remove(this); + } + for (TransformTrackingValue value : parameters) { + value.possibleTransformChecks.remove(this); + } + } + + public void accept() { + //Clear all possible transforms + if (returnValue != null) { + returnValue.possibleTransformChecks.clear(); + } + for (TransformTrackingValue value : parameters) { + value.possibleTransformChecks.clear(); + } + + if (returnValue != null) { + if (transform.getReturnType() != null) { + returnValue.getTransformTypeRef().setValue(transform.getReturnType().getTransformType()); + } + } + + int i = 0; + for (DerivedTransformType type : transform.getParameterTypes()) { + if (type != null) { + parameters[i].getTransformTypeRef().setValue(type.getTransformType()); + } + i++; + } + } +} diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/analysis/package-info.java b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/analysis/package-info.java new file mode 100644 index 000000000..5654dc433 --- /dev/null +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/analysis/package-info.java @@ -0,0 +1,7 @@ +@ParametersAreNonnullByDefault +@MethodsReturnNonnullByDefault +package io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.analysis; + +import javax.annotation.ParametersAreNonnullByDefault; + +import io.github.opencubicchunks.cc_core.annotation.MethodsReturnNonnullByDefault; diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/config/ClassTransformInfo.java b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/config/ClassTransformInfo.java new file mode 100644 index 000000000..3c306f5c7 --- /dev/null +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/config/ClassTransformInfo.java @@ -0,0 +1,30 @@ +package io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.config; + +import java.util.List; +import java.util.Map; + +import io.github.opencubicchunks.cubicchunks.mixin.transform.util.MethodID; + +public class ClassTransformInfo { + private final Map> typeHints; + private final Map constructorReplacers; + private final boolean inPlace; + + public ClassTransformInfo(Map> typeHints, Map constructorReplacers, boolean inPlace) { + this.typeHints = typeHints; + this.constructorReplacers = constructorReplacers; + this.inPlace = inPlace; + } + + public Map> getTypeHints() { + return typeHints; + } + + public Map getConstructorReplacers() { + return constructorReplacers; + } + + public boolean isInPlace() { + return inPlace; + } +} diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/config/Config.java b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/config/Config.java new file mode 100644 index 000000000..2cf107e86 --- /dev/null +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/config/Config.java @@ -0,0 +1,97 @@ +package io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.config; + +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.analysis.TransformTrackingInterpreter; +import io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.analysis.TransformTrackingValue; +import io.github.opencubicchunks.cubicchunks.mixin.transform.util.AncestorHashMap; +import io.github.opencubicchunks.cubicchunks.mixin.transform.util.MethodID; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.analysis.Analyzer; + +public class Config { + private final TypeInfo typeInfo; + private final Map types; + private final AncestorHashMap> methodParameterInfo; + private final Map classes; + private final List typesWithSuffixedTransforms; + + private final Set regularTypes = new HashSet<>(); + private final Set consumerTypes = new HashSet<>(); + private final Set predicateTypes = new HashSet<>(); + + private TransformTrackingInterpreter interpreter; + private Analyzer analyzer; + + public Config(TypeInfo typeInfo, Map transformTypeMap, AncestorHashMap> parameterInfo, + Map classes, + List typesWithSuffixedTransforms) { + this.types = transformTypeMap; + this.methodParameterInfo = parameterInfo; + this.typeInfo = typeInfo; + this.classes = classes; + this.typesWithSuffixedTransforms = typesWithSuffixedTransforms; + + for (TransformType type : this.types.values()) { + regularTypes.add(type.getFrom()); + consumerTypes.add(type.getOriginalConsumerType()); + predicateTypes.add(type.getOriginalPredicateType()); + } + } + + public TypeInfo getTypeInfo() { + return typeInfo; + } + + public Map getTypes() { + return types; + } + + public Map> getMethodParameterInfo() { + return methodParameterInfo; + } + + public Set getRegularTypes() { + return regularTypes; + } + + public Set getConsumerTypes() { + return consumerTypes; + } + + public Set getPredicateTypes() { + return predicateTypes; + } + + public List getTypesWithSuffixedTransforms() { + return typesWithSuffixedTransforms; + } + + public TransformTrackingInterpreter getInterpreter() { + if (interpreter == null) { + interpreter = new TransformTrackingInterpreter(Opcodes.ASM9, this); + } + + return interpreter; + } + + public Analyzer getAnalyzer() { + if (analyzer == null) { + makeAnalyzer(); + } + + return analyzer; + } + + private void makeAnalyzer() { + analyzer = new Analyzer<>(getInterpreter()); + } + + public Map getClasses() { + return classes; + } +} diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/config/ConfigLoader.java b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/config/ConfigLoader.java new file mode 100644 index 000000000..fad0ce7f1 --- /dev/null +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/config/ConfigLoader.java @@ -0,0 +1,689 @@ +package io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.config; + +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.JsonPrimitive; +import io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.bytecodegen.BytecodeFactory; +import io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.bytecodegen.ConstantFactory; +import io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.bytecodegen.JSONBytecodeFactory; +import io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.analysis.DerivedTransformType; +import io.github.opencubicchunks.cubicchunks.mixin.transform.util.AncestorHashMap; +import io.github.opencubicchunks.cubicchunks.mixin.transform.util.MethodID; +import io.github.opencubicchunks.cubicchunks.utils.TestMappingUtils; +import net.fabricmc.loader.api.MappingResolver; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.objectweb.asm.Type; +import org.objectweb.asm.commons.Method; + +public class ConfigLoader { + public static Config loadConfig(InputStream is) { + JsonParser parser = new JsonParser(); + JsonObject root = parser.parse(new InputStreamReader(is)).getAsJsonObject(); + + MappingResolver map = getMapper(); + + TypeInfo hierarchy = loadHierarchy(root.getAsJsonArray("type_info"), map); + + Map transformTypeMap = loadTransformTypes(root.get("types"), map); + AncestorHashMap> parameterInfo = loadMethodParameterInfo(root.get("methods"), map, transformTypeMap, hierarchy); + Map classes = loadClassInfo(root.get("classes"), map, transformTypeMap, hierarchy); + Map invokers = loadInvokers(root.get("invokers"), map, transformTypeMap); + + for (TransformType type : transformTypeMap.values()) { + type.addParameterInfoTo(parameterInfo); + } + + for (InvokerInfo invoker : invokers.values()) { + invoker.addReplacementTo(parameterInfo); + } + + JsonArray suffixedMethods = root.getAsJsonArray("suffixed_methods"); + List typesWithSuffixedMethods = new ArrayList<>(); + + for (JsonElement element : suffixedMethods) { + typesWithSuffixedMethods.add(remapType(Type.getObjectType(element.getAsString()), map)); + } + + Config config = new Config( + hierarchy, + transformTypeMap, + parameterInfo, + classes, + typesWithSuffixedMethods + ); + + return config; + } + + private static Map loadInvokers(JsonElement accessors, MappingResolver map, Map transformTypeMap) { + JsonArray arr = accessors.getAsJsonArray(); + Map interfaces = new HashMap<>(); + + for (JsonElement e : arr) { + JsonObject obj = e.getAsJsonObject(); + String name = obj.get("name").getAsString(); + + String targetName = obj.get("target").getAsString(); + Type target = remapType(Type.getObjectType(targetName), map); + + JsonArray methods = obj.get("methods").getAsJsonArray(); + List methodInfos = new ArrayList<>(); + for (JsonElement m : methods) { + JsonObject obj2 = m.getAsJsonObject(); + String[] methodInfo = obj2.get("name").getAsString().split(" "); + Method method = new Method(methodInfo[0], methodInfo[1]); + + String targetMethod = obj2.get("calls").getAsString(); + targetMethod = map.mapMethodName("intermediary", targetName.replace('/', '.'), targetMethod, method.getDescriptor()); + + Type[] args = Type.getArgumentTypes(method.getDescriptor()); + DerivedTransformType[] transformTypes = new DerivedTransformType[args.length]; + + JsonArray types = obj2.get("types").getAsJsonArray(); + + int i; + for (i = 0; i < types.size(); i++) { + String type = types.get(i).getAsString(); + transformTypes[i] = DerivedTransformType.fromString(type, transformTypeMap); + } + + for (; i < transformTypes.length; i++) { + transformTypes[i] = DerivedTransformType.createDefault(args[i]); + } + + methodInfos.add(new InvokerInfo.InvokerMethodInfo(transformTypes, method.getName(), targetMethod, method.getDescriptor())); + } + + InvokerInfo invoker = new InvokerInfo(Type.getObjectType(name), target, methodInfos); + interfaces.put(target, invoker); + } + + return interfaces; + } + + private static Map loadClassInfo(JsonElement classes, MappingResolver map, Map transformTypeMap, + TypeInfo hierarchy) { + JsonArray arr = classes.getAsJsonArray(); + Map classInfo = new HashMap<>(); + for (JsonElement element : arr) { + JsonObject obj = element.getAsJsonObject(); + Type type = remapType(Type.getObjectType(obj.get("class").getAsString()), map); + + JsonElement typeHintsElem = obj.get("type_hints"); + Map> typeHints = new AncestorHashMap<>(hierarchy); + if (typeHintsElem != null) { + JsonArray typeHintsArr = typeHintsElem.getAsJsonArray(); + for (JsonElement typeHint : typeHintsArr) { + MethodID method = loadMethodID(typeHint.getAsJsonObject().get("method"), map, null); + List paramTypes = new ArrayList<>(); + JsonArray paramTypesArr = typeHint.getAsJsonObject().get("types").getAsJsonArray(); + for (int i = 0; i < paramTypesArr.size(); i++) { + JsonElement paramType = paramTypesArr.get(i); + if (!paramType.isJsonNull()) { + paramTypes.add(transformTypeMap.get(paramType.getAsString())); + } else { + paramTypes.add(null); + } + } + typeHints.put(method, paramTypes); + } + } + + JsonElement constructorReplacersArr = obj.get("constructor_replacers"); + Map constructorReplacers = new HashMap<>(); + if (constructorReplacersArr != null) { + for (JsonElement constructorReplacer : constructorReplacersArr.getAsJsonArray()) { + JsonObject constructorReplacerObj = constructorReplacer.getAsJsonObject(); + String original = constructorReplacerObj.get("original").getAsString(); + + if (!original.contains("(")) { + original = "(" + original + ")V"; + } + + Map replacements = new HashMap<>(); + for (Map.Entry replacement : constructorReplacerObj.get("type_replacements").getAsJsonObject().entrySet()) { + Type type1 = remapType(Type.getObjectType(replacement.getKey()), map); + Type type2 = remapType(Type.getObjectType(replacement.getValue().getAsString()), map); + + replacements.put(type1.getInternalName(), type2.getInternalName()); + } + + constructorReplacers.put(original, new ConstructorReplacer(original, replacements)); + } + } + + boolean inPlace = false; + if (obj.has("in_place")) { + inPlace = obj.get("in_place").getAsBoolean(); + } + + ClassTransformInfo info = new ClassTransformInfo(typeHints, constructorReplacers, inPlace); + classInfo.put(type, info); + } + + return classInfo; + } + + private static TypeInfo loadHierarchy(JsonArray data, MappingResolver map) { + return new TypeInfo(data, t -> remapType(t, map)); + } + + private static AncestorHashMap> loadMethodParameterInfo(JsonElement methods, MappingResolver map, + Map transformTypes, TypeInfo hierarchy) { + final AncestorHashMap> parameterInfo = new AncestorHashMap<>(hierarchy); + + if (methods == null) return parameterInfo; + + if (!methods.isJsonArray()) { + System.err.println("Method parameter info is not an array. Cannot read it"); + return parameterInfo; + } + + for (JsonElement method : methods.getAsJsonArray()) { + JsonObject obj = method.getAsJsonObject(); + MethodID methodID = loadMethodID(obj.get("method"), map, null); + List paramInfo = new ArrayList<>(); + JsonArray possibilites = obj.get("possibilities").getAsJsonArray(); + for (JsonElement possibilityElement : possibilites) { + JsonObject possibility = possibilityElement.getAsJsonObject(); + JsonArray paramsJson = possibility.get("parameters").getAsJsonArray(); + DerivedTransformType[] params = loadParameterTypes(methodID, transformTypes, paramsJson); + + DerivedTransformType returnType = DerivedTransformType.createDefault(methodID.getDescriptor().getReturnType()); + JsonElement returnTypeJson = possibility.get("return"); + + if (returnTypeJson != null) { + if (returnTypeJson.isJsonPrimitive()) { + returnType = DerivedTransformType.fromString(returnTypeJson.getAsString(), transformTypes); + } + } + + int expansionsNeeded = returnType.resultingTypes().size(); + + List[][] indices = new List[expansionsNeeded][params.length]; + + JsonElement replacementJson = possibility.get("replacement"); + JsonArray replacementJsonArray = null; + if (replacementJson != null) { + if (replacementJson.isJsonArray()) { + replacementJsonArray = replacementJson.getAsJsonArray(); + //Generate default indices + generateDefaultIndices(params, expansionsNeeded, indices); + } else { + JsonObject replacementObject = replacementJson.getAsJsonObject(); + replacementJsonArray = replacementObject.get("expansion").getAsJsonArray(); + loadProvidedIndices(expansionsNeeded, indices, replacementObject); + } + } + + MethodReplacement mr = + getMethodReplacement(map, possibility, params, expansionsNeeded, indices, replacementJsonArray); + + JsonElement minimumsJson = possibility.get("minimums"); + MethodTransformChecker.MinimumConditions[] minimumConditions = getMinimums(methodID, transformTypes, minimumsJson); + + MethodParameterInfo info = new MethodParameterInfo(methodID, returnType, params, minimumConditions, mr); + paramInfo.add(info); + } + parameterInfo.put(methodID, paramInfo); + } + + return parameterInfo; + } + + @NotNull private static DerivedTransformType[] loadParameterTypes(MethodID method, Map transformTypes, JsonArray paramsJson) { + DerivedTransformType[] params = new DerivedTransformType[paramsJson.size()]; + Type[] args = method.getDescriptor().getArgumentTypes(); + + for (int i = 0; i < paramsJson.size(); i++) { + JsonElement param = paramsJson.get(i); + if (param.isJsonPrimitive()) { + params[i] = DerivedTransformType.fromString(param.getAsString(), transformTypes); + } else if (param.isJsonNull()) { + if (method.isStatic()) { + params[i] = DerivedTransformType.createDefault(args[i]); + } else if (i == 0) { + params[i] = DerivedTransformType.createDefault(method.getOwner()); + } else { + params[i] = DerivedTransformType.createDefault(args[i - 1]); + } + } + } + + + return params; + } + + private static void loadProvidedIndices(int expansionsNeeded, List[][] indices, JsonObject replacementObject) { + JsonArray indicesJson = replacementObject.get("indices").getAsJsonArray(); + for (int i = 0; i < indicesJson.size(); i++) { + JsonElement indices1 = indicesJson.get(i); + if (indices1.isJsonArray()) { + for (int j = 0; j < indices1.getAsJsonArray().size(); j++) { + List l = new ArrayList<>(); + indices[i][j] = l; + JsonElement indices2 = indices1.getAsJsonArray().get(j); + if (indices2.isJsonArray()) { + for (JsonElement index : indices2.getAsJsonArray()) { + l.add(index.getAsInt()); + } + } else { + l.add(indices2.getAsInt()); + } + } + } else { + throw new IllegalArgumentException("Indices must be an array of arrays"); + } + } + } + + private static void generateDefaultIndices(DerivedTransformType[] params, int expansionsNeeded, List[][] indices) { + for (int i = 0; i < params.length; i++) { + DerivedTransformType param = params[i]; + + if (param == null) { + for (int j = 0; j < expansionsNeeded; j++) { + indices[j][i] = Collections.singletonList(0); + } + continue; + } + + List types = param.resultingTypes(); + if (types.size() != 1 && types.size() != expansionsNeeded && expansionsNeeded != 1) { + throw new IllegalArgumentException("Expansion size does not match parameter size"); + } + + if (types.size() == 1) { + for (int j = 0; j < expansionsNeeded; j++) { + indices[j][i] = Collections.singletonList(0); + } + } else if (expansionsNeeded != 1) { + for (int j = 0; j < expansionsNeeded; j++) { + indices[j][i] = Collections.singletonList(j); + } + } else { + indices[0][i] = new ArrayList<>(types.size()); + for (int j = 0; j < types.size(); j++) { + indices[0][i].add(j); + } + } + } + } + + @Nullable private static MethodTransformChecker.MinimumConditions[] getMinimums(MethodID method, Map transformTypes, JsonElement minimumsJson) { + MethodTransformChecker.MinimumConditions[] minimumConditions = null; + if (minimumsJson != null) { + if (!minimumsJson.isJsonArray()) { + throw new RuntimeException("Minimums are not an array. Cannot read them"); + } + minimumConditions = new MethodTransformChecker.MinimumConditions[minimumsJson.getAsJsonArray().size()]; + for (int i = 0; i < minimumsJson.getAsJsonArray().size(); i++) { + JsonObject minimum = minimumsJson.getAsJsonArray().get(i).getAsJsonObject(); + + DerivedTransformType minimumReturnType; + if (minimum.has("return")) { + minimumReturnType = DerivedTransformType.fromString(minimum.get("return").getAsString(), transformTypes); + } else { + minimumReturnType = DerivedTransformType.createDefault(method.getDescriptor().getReturnType()); + } + + DerivedTransformType[] argTypes = new DerivedTransformType[minimum.get("parameters").getAsJsonArray().size()]; + Type[] args = method.getDescriptor().getArgumentTypes(); + for (int j = 0; j < argTypes.length; j++) { + JsonElement argType = minimum.get("parameters").getAsJsonArray().get(j); + if (!argType.isJsonNull()) { + argTypes[j] = DerivedTransformType.fromString(argType.getAsString(), transformTypes); + } else if (j == 0 && !method.isStatic()) { + argTypes[j] = DerivedTransformType.createDefault(method.getOwner()); + } else { + argTypes[j] = DerivedTransformType.createDefault(args[j - method.getCallType().getOffset()]); + } + } + + minimumConditions[i] = new MethodTransformChecker.MinimumConditions(minimumReturnType, argTypes); + } + } + return minimumConditions; + } + + @Nullable + private static MethodReplacement getMethodReplacement(MappingResolver map, JsonObject possibility, DerivedTransformType[] params, int expansionsNeeded, + List[][] indices, JsonArray replacementJsonArray) { + MethodReplacement mr; + if (replacementJsonArray == null) { + mr = null; + } else { + BytecodeFactory[] factories = new BytecodeFactory[expansionsNeeded]; + for (int i = 0; i < expansionsNeeded; i++) { + factories[i] = new JSONBytecodeFactory(replacementJsonArray.get(i).getAsJsonArray(), map); + } + + JsonElement finalizerJson = possibility.get("finalizer"); + BytecodeFactory finalizer = null; + List[] finalizerIndices = null; + + if (finalizerJson != null) { + JsonArray finalizerJsonArray = finalizerJson.getAsJsonArray(); + finalizer = new JSONBytecodeFactory(finalizerJsonArray, map); + + finalizerIndices = new List[params.length]; + JsonElement finalizerIndicesJson = possibility.get("finalizer_indices"); + if (finalizerIndicesJson != null) { + JsonArray finalizerIndicesJsonArray = finalizerIndicesJson.getAsJsonArray(); + for (int i = 0; i < finalizerIndicesJsonArray.size(); i++) { + JsonElement finalizerIndicesJsonElement = finalizerIndicesJsonArray.get(i); + if (finalizerIndicesJsonElement.isJsonArray()) { + finalizerIndices[i] = new ArrayList<>(); + for (JsonElement finalizerIndicesJsonElement1 : finalizerIndicesJsonElement.getAsJsonArray()) { + finalizerIndices[i].add(finalizerIndicesJsonElement1.getAsInt()); + } + } else { + finalizerIndices[i] = Collections.singletonList(finalizerIndicesJsonElement.getAsInt()); + } + } + } else { + for (int i = 0; i < params.length; i++) { + List l = new ArrayList<>(); + for (int j = 0; j < params[i].resultingTypes().size(); j++) { + l.add(j); + } + finalizerIndices[i] = l; + } + } + } + + mr = new MethodReplacement(factories, indices, finalizer, finalizerIndices); + } + return mr; + } + + private static Map loadTransformTypes(JsonElement typeJson, MappingResolver map) { + Map types = new HashMap<>(); + + JsonArray typeArray = typeJson.getAsJsonArray(); + for (JsonElement type : typeArray) { + JsonObject obj = type.getAsJsonObject(); + String id = obj.get("id").getAsString(); + + if (id.contains(" ")) { + throw new IllegalArgumentException("Transform type id cannot contain spaces"); + } + + Type original = remapType(Type.getType(obj.get("original").getAsString()), map); + JsonArray transformedTypesArray = obj.get("transformed").getAsJsonArray(); + Type[] transformedTypes = new Type[transformedTypesArray.size()]; + for (int i = 0; i < transformedTypesArray.size(); i++) { + transformedTypes[i] = remapType(Type.getType(transformedTypesArray.get(i).getAsString()), map); + } + + JsonElement fromOriginalJson = obj.get("from_original"); + MethodID[] fromOriginal = loadFromOriginalTransform(map, transformedTypes, fromOriginalJson); + + MethodID toOriginal = null; + JsonElement toOriginalJson = obj.get("to_original"); + if (toOriginalJson != null) { + toOriginal = loadMethodID(obj.get("to_original"), map, null); + } + + Type originalPredicateType = loadObjectType(obj, "original_predicate", map); + + Type transformedPredicateType = loadObjectType(obj, "transformed_predicate", map); + + Type originalConsumerType = loadObjectType(obj, "original_consumer", map); + + Type transformedConsumerType = loadObjectType(obj, "transformed_consumer", map); + + String[] postfix = loadPostfix(obj, id, transformedTypes); + + Map constantReplacements = + loadConstantReplacements(map, obj, original, transformedTypes); + + + TransformType transformType = + new TransformType(id, original, transformedTypes, fromOriginal, toOriginal, originalPredicateType, transformedPredicateType, originalConsumerType, transformedConsumerType, + postfix, constantReplacements); + types.put(id, transformType); + } + + return types; + } + + @Nullable private static MethodID[] loadFromOriginalTransform(MappingResolver map, Type[] transformedTypes, JsonElement fromOriginalJson) { + MethodID[] fromOriginal = null; + + if (fromOriginalJson != null) { + JsonArray fromOriginalArray = fromOriginalJson.getAsJsonArray(); + fromOriginal = new MethodID[fromOriginalArray.size()]; + if (fromOriginalArray.size() != transformedTypes.length) { + throw new IllegalArgumentException("Number of from_original methods does not match number of transformed types"); + } + for (int i = 0; i < fromOriginalArray.size(); i++) { + JsonElement fromOriginalElement = fromOriginalArray.get(i); + fromOriginal[i] = loadMethodID(fromOriginalElement, map, null); + } + } + + return fromOriginal; + } + + @Nullable + private static Type loadObjectType(JsonObject object, String key, MappingResolver map) { + JsonElement typeElement = object.get(key); + if (typeElement == null) { + return null; + } + return remapType(Type.getObjectType(typeElement.getAsString()), map); + } + + @NotNull private static String[] loadPostfix(JsonObject obj, String id, Type[] transformedTypes) { + String[] postfix = new String[transformedTypes.length]; + JsonElement postfixJson = obj.get("postfix"); + if (postfixJson != null) { + JsonArray postfixArray = postfixJson.getAsJsonArray(); + for (int i = 0; i < postfixArray.size(); i++) { + postfix[i] = postfixArray.get(i).getAsString(); + } + } else if (postfix.length != 1) { + for (int i = 0; i < postfix.length; i++) { + postfix[i] = "_" + id + "_" + i; + } + } else { + postfix[0] = "_" + id; + } + return postfix; + } + + @NotNull + private static Map loadConstantReplacements(MappingResolver map, JsonObject obj, Type original, Type[] transformedTypes) { + Map constantReplacements = new HashMap<>(); + JsonElement constantReplacementsJson = obj.get("constant_replacements"); + if (constantReplacementsJson != null) { + JsonArray constantReplacementsArray = constantReplacementsJson.getAsJsonArray(); + for (int i = 0; i < constantReplacementsArray.size(); i++) { + JsonObject constantReplacementsObject = constantReplacementsArray.get(i).getAsJsonObject(); + JsonPrimitive constantReplacementsFrom = constantReplacementsObject.get("from").getAsJsonPrimitive(); + + Object from; + if (constantReplacementsFrom.isString()) { + from = constantReplacementsFrom.getAsString(); + } else { + from = constantReplacementsFrom.getAsNumber(); + from = getNumber(from, original.getSize() == 2); + } + + JsonArray toArray = constantReplacementsObject.get("to").getAsJsonArray(); + BytecodeFactory[] to = new BytecodeFactory[toArray.size()]; + for (int j = 0; j < toArray.size(); j++) { + JsonElement toElement = toArray.get(j); + if (toElement.isJsonPrimitive()) { + JsonPrimitive toPrimitive = toElement.getAsJsonPrimitive(); + if (toPrimitive.isString()) { + to[j] = new ConstantFactory(toPrimitive.getAsString()); + } else { + Number constant = toPrimitive.getAsNumber(); + constant = getNumber(constant, transformedTypes[j].getSize() == 2); + to[j] = new ConstantFactory(constant); + } + } else { + to[j] = new JSONBytecodeFactory(toElement.getAsJsonArray(), map); + } + } + + constantReplacements.put(from, to); + } + } + return constantReplacements; + } + + private static Number getNumber(Object from, boolean doubleSize) { + String s = from.toString(); + if (doubleSize) { + if (s.contains(".")) { + return Double.parseDouble(s); + } else { + return Long.parseLong(s); + } + } else { + if (s.contains(".")) { + return Float.parseFloat(s); + } else { + return Integer.parseInt(s); + } + } + } + + /** + * Parses a method ID from a JSON object or the string + * @param method The JSON object or string + * @param map The mapping resolver + * @param defaultCallType The default call type to use if the json doesn't specify it. Used when the call type can be inferred from context. + * @return The parsed method ID + */ + public static MethodID loadMethodID(JsonElement method, @Nullable MappingResolver map, @Nullable MethodID.CallType defaultCallType) { + MethodID methodID; + if (method.isJsonPrimitive()) { + String id = method.getAsString(); + String[] parts = id.split(" "); + + MethodID.CallType callType; + int nameIndex; + int descIndex; + if (parts.length == 3) { + char callChar = parts[0].charAt(0); + callType = switch (callChar) { + case 'v' -> MethodID.CallType.VIRTUAL; + case 's' -> MethodID.CallType.STATIC; + case 'i' -> MethodID.CallType.INTERFACE; + case 'S' -> MethodID.CallType.SPECIAL; + default -> { + System.err.println("Invalid call type: " + callChar + ". Using default VIRTUAL type"); + yield MethodID.CallType.VIRTUAL; + } + }; + + nameIndex = 1; + descIndex = 2; + } else { + if (defaultCallType == null) { + throw new IllegalArgumentException("Invalid method ID: " + id); + } + + callType = defaultCallType; + + nameIndex = 0; + descIndex = 1; + } + + String desc = parts[descIndex]; + + String[] ownerAndName = parts[nameIndex].split("#"); + String owner = ownerAndName[0]; + String name = ownerAndName[1]; + + methodID = new MethodID(Type.getObjectType(owner), name, Type.getMethodType(desc), callType); + } else { + String owner = method.getAsJsonObject().get("owner").getAsString(); + String name = method.getAsJsonObject().get("name").getAsString(); + String desc = method.getAsJsonObject().get("desc").getAsString(); + + MethodID.CallType callType; + JsonElement callTypeElement = method.getAsJsonObject().get("call_type"); + + if (callTypeElement == null) { + if (defaultCallType == null) { + throw new IllegalArgumentException("Invalid method ID: " + method); + } + + callType = defaultCallType; + } else { + String callTypeStr = callTypeElement.getAsString(); + callType = MethodID.CallType.valueOf(callTypeStr.toUpperCase()); + } + + methodID = new MethodID(Type.getObjectType(owner), name, Type.getMethodType(desc), callType); + } + + if (map != null) { + //Remap the method ID + methodID = remapMethod(methodID, map); + } + + return methodID; + } + + public static MethodID remapMethod(MethodID methodID, @NotNull MappingResolver map) { + //Map owner + Type mappedOwner = remapType(methodID.getOwner(), map); + + //Map name + String mappedName = map.mapMethodName("intermediary", + methodID.getOwner().getClassName(), methodID.getName(), methodID.getDescriptor().getInternalName() + ); + + //Map desc + Type[] args = methodID.getDescriptor().getArgumentTypes(); + Type returnType = methodID.getDescriptor().getReturnType(); + + Type[] mappedArgs = new Type[args.length]; + for (int i = 0; i < args.length; i++) { + mappedArgs[i] = remapType(args[i], map); + } + + Type mappedReturnType = remapType(returnType, map); + + Type mappedDesc = Type.getMethodType(mappedReturnType, mappedArgs); + + return new MethodID(mappedOwner, mappedName, mappedDesc, methodID.getCallType()); + } + + public static Type remapType(Type type, MappingResolver map) { + if (type.getSort() == Type.ARRAY) { + Type componentType = remapType(type.getElementType(), map); + return Type.getType("[" + componentType.getDescriptor()); + } else if (type.getSort() == Type.OBJECT) { + String unmapped = type.getClassName(); + String mapped = map.mapClassName("intermediary", unmapped); + if (mapped == null) { + return type; + } + return Type.getObjectType(mapped.replace('.', '/')); + } else { + return type; + } + } + + private static MappingResolver getMapper() { + return TestMappingUtils.getMappingResolver(); + } +} diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/config/ConstructorReplacer.java b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/config/ConstructorReplacer.java new file mode 100644 index 000000000..d5c6c07d2 --- /dev/null +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/config/ConstructorReplacer.java @@ -0,0 +1,74 @@ +package io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.config; + +import java.util.HashMap; +import java.util.Map; + +import io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.TypeTransformer; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.InsnList; +import org.objectweb.asm.tree.LabelNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; +import org.objectweb.asm.tree.TypeInsnNode; + +public record ConstructorReplacer(String originalDesc, Map replacements) { + public InsnList make(TypeTransformer transformer) { + //Get original + MethodNode original = transformer.getClassNode().methods.stream().filter(m -> m.name.equals("") && m.desc.equals(originalDesc)).findAny().orElseThrow(); + InsnList originalCode = original.instructions; + + InsnList newCode = new InsnList(); + + //Make label copies + Map labelCopies = new HashMap<>(); + for (AbstractInsnNode insn : originalCode) { + if (insn instanceof LabelNode labelNode) { + labelCopies.put(labelNode, new LabelNode()); + } + } + + //Copy original code and modify required types + for (AbstractInsnNode insn : originalCode) { + if (insn instanceof LabelNode labelNode) { + newCode.add(labelCopies.get(labelNode)); + } else if (insn instanceof TypeInsnNode typeInsnNode) { + String desc = typeInsnNode.desc; + if (replacements.containsKey(desc)) { + desc = replacements.get(desc); + } + + newCode.add(new TypeInsnNode(typeInsnNode.getOpcode(), desc)); + } else if (insn instanceof MethodInsnNode methodInsnNode) { + Type owner = Type.getObjectType(methodInsnNode.owner); + Type[] args = Type.getArgumentTypes(methodInsnNode.desc); + + if (replacements.containsKey(owner.getInternalName())) { + owner = Type.getObjectType(replacements.get(owner.getInternalName())); + } + + for (int i = 0; i < args.length; i++) { + if (replacements.containsKey(args[i].getInternalName())) { + args[i] = Type.getObjectType(replacements.get(args[i].getInternalName())); + } + } + + //Check for itf + int opcode = methodInsnNode.getOpcode(); + boolean itf = opcode == Opcodes.INVOKEINTERFACE; + if (itf || opcode == Opcodes.INVOKEVIRTUAL) { + itf = transformer.getConfig().getTypeInfo().recognisesInterface(owner); + + opcode = itf ? Opcodes.INVOKEINTERFACE : Opcodes.INVOKEVIRTUAL; + } + + newCode.add(new MethodInsnNode(opcode, owner.getInternalName(), methodInsnNode.name, Type.getMethodDescriptor(Type.getReturnType(methodInsnNode.desc), args), itf)); + } else { + newCode.add(insn.clone(labelCopies)); + } + } + + return newCode; + } +} diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/config/InvokerInfo.java b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/config/InvokerInfo.java new file mode 100644 index 000000000..8c008e55f --- /dev/null +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/config/InvokerInfo.java @@ -0,0 +1,136 @@ +package io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.config; + +import java.util.ArrayList; +import java.util.List; + +import io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.bytecodegen.BytecodeFactory; +import io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.analysis.DerivedTransformType; +import io.github.opencubicchunks.cubicchunks.mixin.transform.util.AncestorHashMap; +import io.github.opencubicchunks.cubicchunks.mixin.transform.util.MethodID; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.InsnList; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.TypeInsnNode; +import org.objectweb.asm.tree.VarInsnNode; + +public class InvokerInfo { + private final Type mixinClass; + private final Type targetClass; + private final List methods; + + public InvokerInfo(Type mixinClass, Type targetClass, List methods) { + this.mixinClass = mixinClass; + this.targetClass = targetClass; + this.methods = methods; + } + + public void addReplacementTo(AncestorHashMap> parameterInfo) { + for (InvokerMethodInfo method : methods) { + method.addReplacementTo(parameterInfo, this); + } + } + + public List getMethods() { + return methods; + } + + public record InvokerMethodInfo(DerivedTransformType[] argTypes, String mixinMethodName, String targetMethodName, String desc) { + public void addReplacementTo(AncestorHashMap> parameterInfo, InvokerInfo invokerInfo) { + List transformedTypes = new ArrayList<>(); + + for (int i = 0; i < argTypes.length; i++) { + transformedTypes.addAll(argTypes[i].resultingTypes()); + } + + StringBuilder newDescBuilder = new StringBuilder(); + newDescBuilder.append("("); + for (Type type : transformedTypes) { + newDescBuilder.append(type.getDescriptor()); + } + newDescBuilder.append(")"); + newDescBuilder.append(Type.getReturnType(desc).getDescriptor()); + + String newDesc = newDescBuilder.toString(); + + BytecodeFactory replacement = generateReplacement(newDesc, transformedTypes, invokerInfo); + + MethodID methodID = new MethodID(invokerInfo.mixinClass.getInternalName(), mixinMethodName, desc, MethodID.CallType.INTERFACE); + + //Generate the actual argTypes array who's first element is `this` + DerivedTransformType[] newArgTypes = new DerivedTransformType[argTypes.length + 1]; + newArgTypes[0] = DerivedTransformType.createDefault(methodID.getOwner()); + System.arraycopy(argTypes, 0, newArgTypes, 1, argTypes.length); + + //Generate minimums + List minimumConditions = new ArrayList<>(); + Type[] args = methodID.getDescriptor().getArgumentTypes(); + + for (int j = 0; j < argTypes.length; j++) { + if (argTypes[j].getTransformType() != null) { + DerivedTransformType[] min = new DerivedTransformType[newArgTypes.length]; + + for (int k = 0; k < min.length; k++) { + if (k != j + 1) { + min[k] = DerivedTransformType.createDefault(args[j]); + } else { + min[k] = argTypes[j]; + } + } + + minimumConditions.add(new MethodTransformChecker.MinimumConditions(DerivedTransformType.createDefault(args[j]), min)); + } + } + + MethodParameterInfo info = new MethodParameterInfo( + methodID, + DerivedTransformType.createDefault(methodID.getDescriptor().getReturnType()), + newArgTypes, + minimumConditions.toArray(new MethodTransformChecker.MinimumConditions[0]), + new MethodReplacement(replacement, newArgTypes) + ); + + parameterInfo.computeIfAbsent(methodID, k -> new ArrayList<>()).add(info); + } + + private BytecodeFactory generateReplacement(String newDesc, List transformedTypes, InvokerInfo invokerInfo) { + if (transformedTypes.size() == 0) { + return (__) -> { + InsnList list = new InsnList(); + list.add(new TypeInsnNode(Opcodes.CHECKCAST, invokerInfo.targetClass.getInternalName())); + list.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, invokerInfo.targetClass.getInternalName(), targetMethodName, newDesc, false)); + return list; + }; + } else { + return (varAllocator) -> { + InsnList insnList = new InsnList(); + + //Save the arguments in variables + //Step 1: Allocate vars + List vars = new ArrayList<>(); + for (Type t : transformedTypes) { + vars.add(varAllocator.apply(t)); + } + + //Step 2: Save the arguments in the allocated vars + for (int i = vars.size() - 1; i >= 0; i--) { + insnList.add(new VarInsnNode(transformedTypes.get(i).getOpcode(Opcodes.ISTORE), vars.get(i))); + } + + //Cast the object to the interface + insnList.add(new TypeInsnNode(Opcodes.CHECKCAST, invokerInfo.targetClass.getInternalName())); + + //Load the arguments back + for (int i = 0; i < vars.size(); i++) { + insnList.add(new VarInsnNode(transformedTypes.get(i).getOpcode(Opcodes.ILOAD), vars.get(i))); + } + + //Call the method + insnList.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, invokerInfo.targetClass.getInternalName(), targetMethodName, newDesc, false)); + + return insnList; + }; + } + } + } +} diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/config/MethodParameterInfo.java b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/config/MethodParameterInfo.java new file mode 100644 index 000000000..8ff52e348 --- /dev/null +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/config/MethodParameterInfo.java @@ -0,0 +1,123 @@ +package io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.config; + +import io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.analysis.DerivedTransformType; +import io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.analysis.TransformTrackingValue; +import io.github.opencubicchunks.cubicchunks.mixin.transform.util.MethodID; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.objectweb.asm.Type; + +public class MethodParameterInfo { + private final MethodID method; + private final DerivedTransformType returnType; + private final DerivedTransformType[] parameterTypes; + private final MethodTransformChecker transformCondition; + private final @Nullable MethodReplacement replacement; + + public MethodParameterInfo( + MethodID method, @NotNull DerivedTransformType returnType, @NotNull DerivedTransformType[] parameterTypes, + MethodTransformChecker.MinimumConditions[] minimumConditions, @Nullable MethodReplacement replacement + ) { + this.method = method; + this.returnType = returnType; + this.transformCondition = new MethodTransformChecker(this, minimumConditions); + this.replacement = replacement; + this.parameterTypes = parameterTypes; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + + if (returnType.getTransformType() == null) { + sb.append(getOnlyName(method.getDescriptor().getReturnType())); + } else { + sb.append('['); + sb.append(returnType.getTransformType().getName()); + sb.append(']'); + } + + Type[] types = method.getDescriptor().getArgumentTypes(); + + sb.append(" "); + sb.append(getOnlyName(method.getOwner())); + sb.append("#"); + sb.append(method.getName()); + sb.append("("); + for (int i = 0; i < parameterTypes.length; i++) { + TransformType type = parameterTypes[i].getTransformType(); + if (type != null) { + sb.append('['); + sb.append(type.getName()); + sb.append(']'); + } else { + sb.append(getOnlyName(types[i])); + } + if (i != parameterTypes.length - 1) { + sb.append(", "); + } + } + + sb.append(")"); + return sb.toString(); + } + + private static String getOnlyName(Type type) { + String name = type.getClassName(); + return name.substring(name.lastIndexOf('.') + 1); + } + + public MethodID getMethod() { + return method; + } + + public @Nullable DerivedTransformType getReturnType() { + return returnType; + } + + public DerivedTransformType[] getParameterTypes() { + return parameterTypes; + } + + public MethodTransformChecker getTransformCondition() { + return transformCondition; + } + + public @Nullable MethodReplacement getReplacement() { + return replacement; + } + + public static String getNewDesc(DerivedTransformType returnType, DerivedTransformType[] parameterTypes, String originalDesc) { + Type[] types = Type.getArgumentTypes(originalDesc); + StringBuilder sb = new StringBuilder("("); + for (int i = 0; i < parameterTypes.length; i++) { + if (parameterTypes[i] != null && parameterTypes[i].getTransformType() != null) { + for (Type type : parameterTypes[i].resultingTypes()) { + sb.append(type.getDescriptor()); + } + } else { + sb.append(types[i].getDescriptor()); + } + } + sb.append(")"); + if (returnType != null && returnType.getTransformType() != null) { + if (returnType.resultingTypes().size() != 1) { + throw new IllegalArgumentException("Return type must have exactly one transform type"); + } + sb.append(returnType.resultingTypes().get(0).getDescriptor()); + } else { + sb.append(Type.getReturnType(originalDesc).getDescriptor()); + } + return sb.toString(); + } + + public static String getNewDesc(TransformTrackingValue returnValue, TransformTrackingValue[] parameters, String originalDesc) { + DerivedTransformType returnType = returnValue.getTransform(); + DerivedTransformType[] parameterTypes = new DerivedTransformType[parameters.length]; + for (int i = 0; i < parameters.length; i++) { + parameterTypes[i] = parameters[i].getTransform(); + } + + return getNewDesc(returnType, parameterTypes, originalDesc); + } +} diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/config/MethodReplacement.java b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/config/MethodReplacement.java new file mode 100644 index 000000000..6fa03dba0 --- /dev/null +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/config/MethodReplacement.java @@ -0,0 +1,75 @@ +package io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.config; + +import java.util.ArrayList; +import java.util.List; + +import io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.bytecodegen.BytecodeFactory; +import io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.analysis.DerivedTransformType; +import org.jetbrains.annotations.Nullable; + +public class MethodReplacement { + private final BytecodeFactory[] bytecodeFactories; + private final boolean changeParameters; + private final List[][] parameterIndexes; + private final BytecodeFactory finalizer; + private final List[] finalizerIndices; + + public MethodReplacement(BytecodeFactory factory, DerivedTransformType[] argTypes) { + this.bytecodeFactories = new BytecodeFactory[] { factory }; + this.changeParameters = false; + this.finalizer = null; + this.finalizerIndices = null; + + //Compute default indices + this.parameterIndexes = new List[1][argTypes.length]; + + for (int i = 0; i < argTypes.length; i++) { + List indices = new ArrayList<>(1); + parameterIndexes[0][i] = indices; + + if (argTypes[i].getTransformType() == null) { + indices.add(0); + } else { + for (int j = 0; j < argTypes[i].resultingTypes().size(); j++) { + indices.add(j); + } + } + } + } + + public MethodReplacement(BytecodeFactory[] bytecodeFactories, List[][] parameterIndexes) { + this.bytecodeFactories = bytecodeFactories; + this.parameterIndexes = parameterIndexes; + this.changeParameters = true; + this.finalizer = null; + this.finalizerIndices = null; + } + + public MethodReplacement(BytecodeFactory[] bytecodeFactories, List[][] parameterIndexes, BytecodeFactory finalizer, List[] finalizerIndices) { + this.bytecodeFactories = bytecodeFactories; + this.parameterIndexes = parameterIndexes; + this.changeParameters = true; + this.finalizer = finalizer; + this.finalizerIndices = finalizerIndices; + } + + public BytecodeFactory[] getBytecodeFactories() { + return bytecodeFactories; + } + + public boolean changeParameters() { + return changeParameters; + } + + public @Nullable List[][] getParameterIndices() { + return parameterIndexes; + } + + public @Nullable BytecodeFactory getFinalizer() { + return finalizer; + } + + public @Nullable List[] getFinalizerIndices() { + return finalizerIndices; + } +} diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/config/MethodTransformChecker.java b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/config/MethodTransformChecker.java new file mode 100644 index 000000000..03718868e --- /dev/null +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/config/MethodTransformChecker.java @@ -0,0 +1,90 @@ +package io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.config; + +import io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.analysis.DerivedTransformType; +import io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.analysis.TransformTrackingValue; +import org.jetbrains.annotations.Nullable; + +public class MethodTransformChecker { + private final MethodParameterInfo target; + private final @Nullable MinimumConditions[] minimumConditions; + + public MethodTransformChecker(MethodParameterInfo target, @Nullable MinimumConditions[] minimumConditions) { + this.target = target; + this.minimumConditions = minimumConditions; + } + + /** + * Checks if the passed in values could be of a transformed method + * + * @param returnValue The current return value + * @param parameters The current parameters + * + * @return -1 if they are incompatible, 0 if they are not yet rejected nor accepted, 1 if they should definitely be transformed + */ + public int checkValidity(@Nullable TransformTrackingValue returnValue, TransformTrackingValue... parameters) { + //First check if it is still possible + if (returnValue != null) { + if (!isApplicable(returnValue.getTransform(), target.getReturnType())) { + return -1; + } + } + + //Check if the parameters are compatible + for (int i = 0; i < parameters.length; i++) { + if (!isApplicable(parameters[i].getTransform(), target.getParameterTypes()[i])) { + return -1; + } + } + + if (minimumConditions != null) { + //Check if any minimums are met + for (MinimumConditions conditions : this.minimumConditions) { + if (conditions.isMet(returnValue, parameters)) { + return 1; + } + } + return 0; + } + + //If no minimums are given, we assume that it should be transformed + return 1; + } + + private static boolean isApplicable(DerivedTransformType current, @Nullable DerivedTransformType target) { + if (target == null) { + return true; + } + + if (current.getTransformType() == null) { + return true; + } + + //Current is not null + if (target.getTransformType() == null) { + return false; + } + + //Current is not null and target is not null + return current.equals(target); + } + + public record MinimumConditions(DerivedTransformType returnType, DerivedTransformType... parameterTypes) { + public boolean isMet(TransformTrackingValue returnValue, TransformTrackingValue[] parameters) { + if (returnType.getTransformType() != null) { + if (!returnValue.getTransform().equals(returnType)) { + return false; + } + } + + for (int i = 0; i < parameterTypes.length; i++) { + if (parameterTypes[i].getTransformType() != null) { + if (!parameters[i].getTransform().equals(parameterTypes[i])) { + return false; + } + } + } + + return true; + } + } +} diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/config/TransformType.java b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/config/TransformType.java new file mode 100644 index 000000000..a9113b850 --- /dev/null +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/config/TransformType.java @@ -0,0 +1,242 @@ +package io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.config; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.function.Supplier; + +import io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.bytecodegen.BytecodeFactory; +import io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.analysis.DerivedTransformType; +import io.github.opencubicchunks.cubicchunks.mixin.transform.util.ASMUtil; +import io.github.opencubicchunks.cubicchunks.mixin.transform.util.MethodID; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.InsnList; +import org.objectweb.asm.tree.MethodInsnNode; + +public class TransformType { + private final String id; + private final Type from; + private final Type[] to; + private final MethodID[] fromOriginal; + private final MethodID toOriginal; + + private final Type originalPredicateType; + private final Type transformedPredicateType; + + private final Type originalConsumerType; + private final Type transformedConsumerType; + + private final String[] postfix; + + private final Map constantReplacements; + + private final int transformedSize; + + public TransformType(String id, Type from, Type[] to, MethodID[] fromOriginal, MethodID toOriginal, Type originalPredicateType, Type transformedPredicateType, Type originalConsumerType, + Type transformedConsumerType, String[] postfix, Map constantReplacements) { + this.id = id; + this.from = from; + this.to = to; + this.fromOriginal = fromOriginal; + this.toOriginal = toOriginal; + this.originalPredicateType = originalPredicateType; + this.transformedPredicateType = transformedPredicateType; + this.originalConsumerType = originalConsumerType; + this.transformedConsumerType = transformedConsumerType; + this.constantReplacements = constantReplacements; + + int size = 0; + for (Type t : to) { + size += t.getSize(); + } + this.transformedSize = size; + this.postfix = postfix; + } + + @Override + public String toString() { + StringBuilder str = new StringBuilder("Transform Type " + id + "[" + ASMUtil.onlyClassName(from.getClassName()) + " -> ("); + + for (int i = 0; i < to.length; i++) { + str.append(ASMUtil.onlyClassName(to[i].getClassName())); + if (i < to.length - 1) { + str.append(", "); + } + } + + str.append(")]"); + + return str.toString(); + } + + public void addParameterInfoTo(Map> parameterInfo) { + if (fromOriginal != null) { + addFromOriginalInfo(parameterInfo); + } + + if (toOriginal != null) { + addToOriginalInfo(parameterInfo); + } + + if (originalPredicateType != null) { + addSpecialInfo(parameterInfo, originalPredicateType, "test", Type.BOOLEAN_TYPE, DerivedTransformType.Kind.PREDICATE, transformedPredicateType); + } + + if (originalConsumerType != null) { + addSpecialInfo(parameterInfo, originalConsumerType, "accept", Type.VOID_TYPE, DerivedTransformType.Kind.CONSUMER, transformedConsumerType); + } + } + + private void addFromOriginalInfo(Map> parameterInfo) { + int i = 0; + for (MethodID methodID : fromOriginal) { + MethodReplacement methodReplacement = new MethodReplacement( + new BytecodeFactory[] { + (Function variableAllocator) -> new InsnList() + }, + new List[][] { + new List[] { + Collections.singletonList(i) + } + } + ); + MethodParameterInfo info = new MethodParameterInfo( + methodID, + DerivedTransformType.createDefault(methodID.getDescriptor().getReturnType()), + new DerivedTransformType[] { DerivedTransformType.of(this) }, + null, + methodReplacement + ); + parameterInfo.computeIfAbsent(methodID, k -> new ArrayList<>()).add(info); + i++; + } + } + + private void addToOriginalInfo(Map> parameterInfo) { + BytecodeFactory[] expansions = new BytecodeFactory[to.length]; + for (int i = 0; i < to.length; i++) { + expansions[i] = (Function variableAllocator) -> new InsnList(); + } + + DerivedTransformType[] parameterTypes = new DerivedTransformType[this.to.length]; + for (int i = 0; i < parameterTypes.length; i++) { + parameterTypes[i] = DerivedTransformType.createDefault(this.to[i]); + } + + List[][] indices = new List[parameterTypes.length][parameterTypes.length]; + for (int i = 0; i < parameterTypes.length; i++) { + for (int j = 0; j < parameterTypes.length; j++) { + if (i == j) { + indices[i][j] = Collections.singletonList(0); + } else { + indices[i][j] = Collections.emptyList(); + } + } + } + + MethodParameterInfo info = new MethodParameterInfo(toOriginal, DerivedTransformType.of(this), parameterTypes, null, new MethodReplacement(expansions, indices)); + parameterInfo.computeIfAbsent(toOriginal, k -> new ArrayList<>()).add(info); + } + + private void addSpecialInfo(Map> parameterInfo, Type type, String methodName, Type returnType, DerivedTransformType.Kind kind, + Type transformedType) { + MethodID consumerID = new MethodID(type, methodName, Type.getMethodType(returnType, from), MethodID.CallType.INTERFACE); + + DerivedTransformType[] argTypes = new DerivedTransformType[] { DerivedTransformType.of(this, kind), DerivedTransformType.of(this) }; + + MethodReplacement methodReplacement = new MethodReplacement( + (Function variableAllocator) -> { + InsnList list = new InsnList(); + list.add(new MethodInsnNode(Opcodes.INVOKEINTERFACE, transformedType.getInternalName(), methodName, Type.getMethodDescriptor(returnType, to))); + return list; + }, + + argTypes + ); + + MethodTransformChecker.MinimumConditions[] minimumConditions = new MethodTransformChecker.MinimumConditions[] { + new MethodTransformChecker.MinimumConditions( + DerivedTransformType.createDefault(returnType), + DerivedTransformType.of(this, kind), + DerivedTransformType.createDefault(this.from) + ), + new MethodTransformChecker.MinimumConditions( + DerivedTransformType.createDefault(returnType), + DerivedTransformType.createDefault(type), + DerivedTransformType.of(this) + ) + }; + + MethodParameterInfo info = new MethodParameterInfo( + consumerID, + DerivedTransformType.createDefault(consumerID.getDescriptor().getReturnType()), + argTypes, + minimumConditions, + methodReplacement + ); + parameterInfo.computeIfAbsent(consumerID, k -> new ArrayList<>()).add(info); + } + + public String getName() { + return id; + } + + public Type getFrom() { + return from; + } + + public Type[] getTo() { + return to; + } + + public MethodID[] getFromOriginal() { + return fromOriginal; + } + + public MethodID getToOriginal() { + return toOriginal; + } + + public Type getOriginalPredicateType() { + return originalPredicateType; + } + + public Type getTransformedPredicateType() { + return transformedPredicateType; + } + + public Type getOriginalConsumerType() { + return originalConsumerType; + } + + public Type getTransformedConsumerType() { + return transformedConsumerType; + } + + public int getTransformedSize() { + return transformedSize; + } + + public String[] getPostfix() { + return postfix; + } + + public Map getConstantReplacements() { + return constantReplacements; + } + + public InsnList convertToTransformed(Supplier originalSupplier) { + InsnList list = new InsnList(); + + //Use the methods provided in the config + for (MethodID methodID : fromOriginal) { + list.add(originalSupplier.get()); + list.add(methodID.callNode()); + } + + return list; + } +} diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/config/TypeInfo.java b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/config/TypeInfo.java new file mode 100644 index 000000000..645c50e06 --- /dev/null +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/config/TypeInfo.java @@ -0,0 +1,168 @@ +package io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.config; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import org.jetbrains.annotations.Nullable; +import org.objectweb.asm.Type; + +public class TypeInfo { + private final Map lookup = new HashMap<>(); + + public TypeInfo(JsonArray array, Function mapper) { + Map types = new HashMap<>(); + + for (JsonElement element : array) { + JsonObject object = element.getAsJsonObject(); + Type type = Type.getObjectType(object.get("name").getAsString()); + types.put(type, object); + } + + for (Map.Entry entry : types.entrySet()) { + this.load(entry.getKey(), entry.getValue(), types, mapper); + } + } + + private Node load(Type type, JsonObject data, Map loadInfo, Function mapper) { + Type mappedType = mapper.apply(type); + if (this.lookup.containsKey(mappedType)) { + return this.lookup.get(mappedType); + } + + Node superClass = null; + if (data.has("superclass")) { + String superName = data.get("superclass").getAsString(); + Type superType = Type.getObjectType(superName); + superClass = this.load(superType, loadInfo.get(superType), loadInfo, mapper); + } + + Set interfaces = new HashSet<>(); + for (JsonElement element : data.getAsJsonArray("interfaces")) { + String interfaceName = element.getAsString(); + Type interfaceType = Type.getObjectType(interfaceName); + interfaces.add(this.load(interfaceType, loadInfo.get(interfaceType), loadInfo, mapper)); + } + + boolean isItf = data.get("is_interface").getAsBoolean(); + + Node node = new Node(mappedType, interfaces, superClass, isItf); + + if (superClass != null) { + superClass.getChildren().add(node); + } + + for (Node itf : interfaces) { + itf.getChildren().add(node); + } + + this.lookup.put(mappedType, node); + return node; + } + + public Iterable ancestry(Type type) { + if (!this.lookup.containsKey(type)) { + return List.of(type); + } + + //Breadth first traversal + List ancestry = new ArrayList<>(); + Set visited = new HashSet<>(); + + Queue queue = new ArrayDeque<>(); + queue.add(this.lookup.get(type)); + + while (!queue.isEmpty()) { + Node node = queue.remove(); + if (visited.contains(node.getValue())) { + continue; + } + visited.add(node.getValue()); + ancestry.add(node.getValue()); + + queue.addAll(node.getParents()); + } + + return ancestry; + } + + public Collection nodes() { + return lookup.values(); + } + + public @Nullable Node getNode(Type owner) { + return lookup.get(owner); + } + + public boolean recognisesInterface(Type potentialOwner) { + Node node = this.lookup.get(potentialOwner); + + return node != null && node.isInterface(); + } + + public Set getKnownInterfaces() { + return this.lookup.values().stream().filter(Node::isInterface).map(Node::getValue).collect(Collectors.toSet()); + } + + public static class Node { + private final Type value; + private final Set children = new HashSet<>(); + private final Set interfaces; + private final @Nullable Node superclass; + private final boolean isInterface; + + public Node(Type value, Set interfaces, @Nullable Node superclass, boolean itf) { + this.value = value; + this.interfaces = interfaces; + this.superclass = superclass; + this.isInterface = itf; + } + + public Type getValue() { + return value; + } + + public Set getChildren() { + return children; + } + + public @Nullable Node getSuperclass() { + return superclass; + } + + public boolean isInterface() { + return isInterface; + } + + public Set getInterfaces() { + return interfaces.stream().map(Node::getValue).collect(Collectors.toSet()); + } + + public Collection getParents() { + List parents = new ArrayList<>(); + + if (this.superclass != null) { + parents.add(this.superclass); + } + + parents.addAll(this.interfaces); + + return parents; + } + + public boolean isDirectDescendantOf(Node potentialParent) { + return potentialParent.getChildren().contains(this); + } + } +} diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/config/package-info.java b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/config/package-info.java new file mode 100644 index 000000000..feb7f3002 --- /dev/null +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/config/package-info.java @@ -0,0 +1,7 @@ +@ParametersAreNonnullByDefault +@MethodsReturnNonnullByDefault +package io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.config; + +import javax.annotation.ParametersAreNonnullByDefault; + +import io.github.opencubicchunks.cc_core.annotation.MethodsReturnNonnullByDefault; diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/package-info.java b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/package-info.java new file mode 100644 index 000000000..fe86598cb --- /dev/null +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/typetransformer/transformer/package-info.java @@ -0,0 +1,7 @@ +@ParametersAreNonnullByDefault +@MethodsReturnNonnullByDefault +package io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer; + +import javax.annotation.ParametersAreNonnullByDefault; + +import io.github.opencubicchunks.cc_core.annotation.MethodsReturnNonnullByDefault; diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/util/ASMUtil.java b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/util/ASMUtil.java new file mode 100644 index 000000000..0d18595a3 --- /dev/null +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/util/ASMUtil.java @@ -0,0 +1,472 @@ +package io.github.opencubicchunks.cubicchunks.mixin.transform.util; + +import static org.objectweb.asm.Opcodes.*; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Method; +import java.util.function.Function; +import java.util.function.Predicate; + +import org.jetbrains.annotations.Nullable; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.FieldInsnNode; +import org.objectweb.asm.tree.FieldNode; +import org.objectweb.asm.tree.FrameNode; +import org.objectweb.asm.tree.InsnList; +import org.objectweb.asm.tree.InsnNode; +import org.objectweb.asm.tree.IntInsnNode; +import org.objectweb.asm.tree.JumpInsnNode; +import org.objectweb.asm.tree.LabelNode; +import org.objectweb.asm.tree.LdcInsnNode; +import org.objectweb.asm.tree.LineNumberNode; +import org.objectweb.asm.tree.LookupSwitchInsnNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; +import org.objectweb.asm.tree.MultiANewArrayInsnNode; +import org.objectweb.asm.tree.TableSwitchInsnNode; +import org.objectweb.asm.tree.TypeInsnNode; +import org.objectweb.asm.tree.analysis.Frame; +import org.objectweb.asm.tree.analysis.Value; + +public class ASMUtil { + public static boolean isStatic(MethodNode methodNode) { + return (methodNode.access & ACC_STATIC) != 0; + } + + public static int argumentCount(String desc, boolean isStatic) { + Type[] argTypes = Type.getArgumentTypes(desc); + + int size = argTypes.length; + if (!isStatic) { + size++; + } + + return size; + } + + public static void jumpIfCmp(InsnList list, Type type, boolean equal, LabelNode label) { + switch (type.getSort()) { + case Type.BOOLEAN, Type.CHAR, Type.BYTE, Type.SHORT, Type.INT -> list.add(new JumpInsnNode(equal ? IF_ICMPEQ : IF_ICMPNE, label)); + case Type.ARRAY, Type.OBJECT -> list.add(new JumpInsnNode(equal ? IF_ACMPEQ : IF_ACMPNE, label)); + default -> { + list.add( + new InsnNode( + switch (type.getSort()) { + case Type.FLOAT -> FCMPL; + case Type.LONG -> LCMP; + case Type.DOUBLE -> DCMPL; + default -> throw new IllegalArgumentException("Invalid type: " + type); + } + ) + ); + list.add(new JumpInsnNode(equal ? IFEQ : IFNE, label)); + } + } + } + + public static String onlyClassName(String name) { + name = name.replace('/', '.'); + int index = name.lastIndexOf('.'); + if (index == -1) { + return name; + } + return name.substring(index + 1); + } + + public static T getTop(Frame frame) { + return frame.getStack(frame.getStackSize() - 1); + } + + public static boolean isConstant(AbstractInsnNode insn) { + if (insn instanceof LdcInsnNode) { + return true; + } else if (insn instanceof IntInsnNode && insn.getOpcode() != NEWARRAY) { + return true; + } + + int opcode = insn.getOpcode(); + return opcode == ICONST_M1 || opcode == ICONST_0 || opcode == ICONST_1 || opcode == ICONST_2 || opcode == ICONST_3 || opcode == ICONST_4 || opcode == ICONST_5 || opcode == LCONST_0 + || opcode == LCONST_1 || opcode == FCONST_0 || opcode == FCONST_1 || opcode == FCONST_2 || opcode == DCONST_0 || opcode == DCONST_1; + } + + public static Object getConstant(AbstractInsnNode insn) { + if (!isConstant(insn)) { + throw new IllegalArgumentException("Not a constant instruction!"); + } + + if (insn instanceof LdcInsnNode cst) { + return cst.cst; + } else if (insn instanceof IntInsnNode cst) { + return cst.operand; + } + + int opcode = insn.getOpcode(); + + return switch (opcode) { + case ICONST_M1 -> -1; + case ICONST_0 -> 0; + case ICONST_1 -> 1; + case ICONST_2 -> 2; + case ICONST_3 -> 3; + case ICONST_4 -> 4; + case ICONST_5 -> 5; + case LCONST_0 -> 0L; + case LCONST_1 -> 1L; + case FCONST_0 -> 0.0f; + case FCONST_1 -> 1.0f; + case FCONST_2 -> 2.0f; + case DCONST_0 -> 0.0; + case DCONST_1 -> 1.0; + default -> { + throw new UnsupportedOperationException("Opcode " + opcode + " is not supported!"); + } + }; + } + + public static MethodNode copy(MethodNode original) { + ClassNode classNode = new ClassNode(); + original.accept(classNode); + return classNode.methods.get(0); + } + + public static void changeFieldType(ClassNode target, FieldID fieldID, Type newType, Function postLoad) { + String owner = target.name; + String name = fieldID.name(); + String desc = fieldID.desc().getDescriptor(); + + FieldNode field = target.fields.stream().filter(f -> f.name.equals(name) && f.desc.equals(desc)).findFirst().orElse(null); + if (field == null) { + throw new IllegalArgumentException("Field " + name + " not found!"); + } + + field.desc = newType.getDescriptor(); + + for (MethodNode method : target.methods) { + for (AbstractInsnNode insn : method.instructions.toArray()) { + if (insn instanceof FieldInsnNode fieldInsn) { + if (fieldInsn.owner.equals(owner) && fieldInsn.name.equals(name) && fieldInsn.desc.equals(desc)) { + fieldInsn.desc = newType.getDescriptor(); + + if (fieldInsn.getOpcode() == GETFIELD || fieldInsn.getOpcode() == GETSTATIC) { + method.instructions.insert(insn, postLoad.apply(method)); + } + } + } + } + } + + } + + public static int getDimensions(Type t) { + if (t.getSort() == Type.ARRAY) { + return t.getDimensions(); + } else { + return 0; + } + } + + /** + * Converts an instruction into a human-readable string. This is not made to be fast, but it is meant to be used for debugging. + * + * @param instruction The instruction. + * + * @return The string. + */ + public static String textify(AbstractInsnNode instruction) { + StringBuilder builder = new StringBuilder(); + textify(instruction, builder); + return builder.toString(); + } + + private static void textify(AbstractInsnNode instruction, StringBuilder builder) { + if (instruction instanceof LabelNode labelNode) { + builder.append(labelNode.getLabel().toString()).append(": "); + } else if (instruction instanceof LineNumberNode lineNumberNode) { + builder.append("Line ").append(lineNumberNode.line).append(": "); + } else if (instruction instanceof FrameNode frameNode) { + builder.append("Frame Node"); + } else { + builder.append(opcodeName(instruction.getOpcode()).toLowerCase()).append(" "); + if (instruction instanceof FieldInsnNode fieldInsnNode) { + builder.append(fieldInsnNode.owner).append(".").append(fieldInsnNode.name).append(" ").append(fieldInsnNode.desc); + } else if (instruction instanceof MethodInsnNode methodInsnNode) { + builder.append(methodInsnNode.owner).append(".").append(methodInsnNode.name).append(" ").append(methodInsnNode.desc); + } else if (instruction instanceof TableSwitchInsnNode tableSwitchInsnNode) { + builder.append("TableSwitchInsnNode"); + } else if (instruction instanceof LookupSwitchInsnNode lookupSwitchInsnNode) { + builder.append("LookupSwitchInsnNode"); + } else if (instruction instanceof IntInsnNode intInsnNode) { + builder.append(intInsnNode.operand); + } else if (instruction instanceof LdcInsnNode ldcInsnNode) { + builder.append(ldcInsnNode.cst); + } + } + } + + /** + * Get the name of a JVM Opcode + * + * @param opcode The opcode as an integer + * + * @return The mnemonic of the opcode + */ + public static String opcodeName(int opcode) { + return switch (opcode) { + case NOP -> "nop"; + case ACONST_NULL -> "aconst_null"; + case ICONST_M1 -> "iconst_m1"; + case ICONST_0 -> "iconst_0"; + case ICONST_1 -> "iconst_1"; + case ICONST_2 -> "iconst_2"; + case ICONST_3 -> "iconst_3"; + case ICONST_4 -> "iconst_4"; + case ICONST_5 -> "iconst_5"; + case LCONST_0 -> "lconst_0"; + case LCONST_1 -> "lconst_1"; + case FCONST_0 -> "fconst_0"; + case FCONST_1 -> "fconst_1"; + case FCONST_2 -> "fconst_2"; + case DCONST_0 -> "dconst_0"; + case DCONST_1 -> "dconst_1"; + case BIPUSH -> "bipush"; + case SIPUSH -> "sipush"; + case LDC -> "ldc"; + case ILOAD -> "iload"; + case LLOAD -> "lload"; + case FLOAD -> "fload"; + case DLOAD -> "dload"; + case ALOAD -> "aload"; + case IALOAD -> "iaload"; + case LALOAD -> "laload"; + case FALOAD -> "faload"; + case DALOAD -> "daload"; + case AALOAD -> "aaload"; + case BALOAD -> "baload"; + case CALOAD -> "caload"; + case SALOAD -> "saload"; + case ISTORE -> "istore"; + case LSTORE -> "lstore"; + case FSTORE -> "fstore"; + case DSTORE -> "dstore"; + case ASTORE -> "astore"; + case IASTORE -> "iastore"; + case LASTORE -> "lastore"; + case FASTORE -> "fastore"; + case DASTORE -> "dastore"; + case AASTORE -> "aastore"; + case BASTORE -> "bastore"; + case CASTORE -> "castore"; + case SASTORE -> "sastore"; + case POP -> "pop"; + case POP2 -> "pop2"; + case DUP -> "dup"; + case DUP_X1 -> "dup_x1"; + case DUP_X2 -> "dup_x2"; + case DUP2 -> "dup2"; + case DUP2_X1 -> "dup2_x1"; + case DUP2_X2 -> "dup2_x2"; + case SWAP -> "swap"; + case IADD -> "iadd"; + case LADD -> "ladd"; + case FADD -> "fadd"; + case DADD -> "dadd"; + case ISUB -> "isub"; + case LSUB -> "lsub"; + case FSUB -> "fsub"; + case DSUB -> "dsub"; + case IMUL -> "imul"; + case LMUL -> "lmul"; + case FMUL -> "fmul"; + case DMUL -> "dmul"; + case IDIV -> "idiv"; + case LDIV -> "ldiv"; + case FDIV -> "fdiv"; + case DDIV -> "ddiv"; + case IREM -> "irem"; + case LREM -> "lrem"; + case FREM -> "frem"; + case DREM -> "drem"; + case INEG -> "ineg"; + case LNEG -> "lneg"; + case FNEG -> "fneg"; + case DNEG -> "dneg"; + case ISHL -> "ishl"; + case LSHL -> "lshl"; + case ISHR -> "ishr"; + case LSHR -> "lshr"; + case IUSHR -> "iushr"; + case LUSHR -> "lushr"; + case IAND -> "iand"; + case LAND -> "land"; + case IOR -> "ior"; + case LOR -> "lor"; + case IXOR -> "ixor"; + case LXOR -> "lxor"; + case IINC -> "iinc"; + case I2L -> "i2l"; + case I2F -> "i2f"; + case I2D -> "i2d"; + case L2I -> "l2i"; + case L2F -> "l2f"; + case L2D -> "l2d"; + case F2I -> "f2i"; + case F2L -> "f2l"; + case F2D -> "f2d"; + case D2I -> "d2i"; + case D2L -> "d2l"; + case D2F -> "d2f"; + case I2B -> "i2b"; + case I2C -> "i2c"; + case I2S -> "i2s"; + case LCMP -> "lcmp"; + case FCMPL -> "fcmpl"; + case FCMPG -> "fcmpg"; + case DCMPL -> "dcmpl"; + case DCMPG -> "dcmpg"; + case IFEQ -> "ifeq"; + case IFNE -> "ifne"; + case IFLT -> "iflt"; + case IFGE -> "ifge"; + case IFGT -> "ifgt"; + case IFLE -> "ifle"; + case IF_ICMPEQ -> "if_icmpeq"; + case IF_ICMPNE -> "if_icmpne"; + case IF_ICMPLT -> "if_icmplt"; + case IF_ICMPGE -> "if_icmpge"; + case IF_ICMPGT -> "if_icmpgt"; + case IF_ICMPLE -> "if_icmple"; + case IF_ACMPEQ -> "if_acmpeq"; + case IF_ACMPNE -> "if_acmpne"; + case GOTO -> "goto"; + case JSR -> "jsr"; + case RET -> "ret"; + case TABLESWITCH -> "tableswitch"; + case LOOKUPSWITCH -> "lookupswitch"; + case IRETURN -> "ireturn"; + case LRETURN -> "lreturn"; + case FRETURN -> "freturn"; + case DRETURN -> "dreturn"; + case ARETURN -> "areturn"; + case RETURN -> "return"; + case GETSTATIC -> "getstatic"; + case PUTSTATIC -> "putstatic"; + case GETFIELD -> "getfield"; + case PUTFIELD -> "putfield"; + case INVOKEVIRTUAL -> "invokevirtual"; + case INVOKESPECIAL -> "invokespecial"; + case INVOKESTATIC -> "invokestatic"; + case INVOKEINTERFACE -> "invokeinterface"; + case INVOKEDYNAMIC -> "invokedynamic"; + case NEW -> "new"; + case NEWARRAY -> "newarray"; + case ANEWARRAY -> "anewarray"; + case ARRAYLENGTH -> "arraylength"; + case ATHROW -> "athrow"; + case CHECKCAST -> "checkcast"; + case INSTANCEOF -> "instanceof"; + case MONITORENTER -> "monitorenter"; + case MONITOREXIT -> "monitorexit"; + default -> "UNKNOWN (" + opcode + ")"; + }; + } + + public static String prettyPrintMethod(String name, String descriptor) { + Type[] types = Type.getArgumentTypes(descriptor); + Type returnType = Type.getReturnType(descriptor); + + StringBuilder sb = new StringBuilder(); + sb.append(onlyClassName(returnType.getClassName())); + sb.append(" "); + sb.append(name); + sb.append("("); + for (int i = 0; i < types.length; i++) { + if (i > 0) { + sb.append(", "); + } + + sb.append(onlyClassName(types[i].getClassName())); + } + + sb.append(")"); + + return sb.toString(); + } + + public static ClassNode loadClassNode(Class clazz) { + return loadClassNode(clazz.getName().replace('.', '/') + ".class"); + } + + public static ClassNode loadClassNode(String path) { + try { + ClassNode classNode = new ClassNode(); + InputStream is = ClassLoader.getSystemResourceAsStream(path); + if (is == null) { + throw new IllegalArgumentException("Could not find class: " + path); + } + ClassReader classReader = new ClassReader(is); + classReader.accept(classNode, 0); + return classNode; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public static String getDescriptor(Method method) { + Type[] types = new Type[method.getParameterCount()]; + + for (int i = 0; i < types.length; i++) { + types[i] = Type.getType(method.getParameterTypes()[i]); + } + + return Type.getMethodDescriptor(Type.getType(method.getReturnType()), types); + } + + public static AbstractInsnNode makeNew(Type type, int dimsAmount) { + int totalDims = type.getDimensions(); + + if (totalDims == 0) { + return new TypeInsnNode(NEW, type.getInternalName()); + } else { + return new MultiANewArrayInsnNode(type.getDescriptor(), dimsAmount); + } + } + + public static Type getArrayElement(Type type) { + if (type.getSort() != Type.ARRAY) { + throw new IllegalArgumentException("Type is not an array: " + type); + } + + return Type.getType(type.getDescriptor().substring(1)); + } + + public static record MethodCondition(String name, @Nullable String desc) implements Predicate { + @Override + public boolean test(MethodNode methodNode) { + if (!methodNode.name.equals(name)) { + return false; + } + + if (desc != null && !methodNode.desc.equals(desc)) { + return false; + } + + return true; + } + + public boolean testMethodID(MethodID id) { + if (!id.getName().equals(name)) { + return false; + } + + if (desc != null && !id.getDescriptor().getDescriptor().equals(desc)) { + return false; + } + + return true; + } + } +} diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/util/AncestorHashMap.java b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/util/AncestorHashMap.java new file mode 100644 index 000000000..fd7f4ce6f --- /dev/null +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/util/AncestorHashMap.java @@ -0,0 +1,118 @@ +package io.github.opencubicchunks.cubicchunks.mixin.transform.util; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.config.TypeInfo; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.objectweb.asm.Type; + +public class AncestorHashMap, T> implements Map { + private final Map map = new HashMap<>(); + private final TypeInfo hierarchy; + + public AncestorHashMap(TypeInfo hierarchy) { + this.hierarchy = hierarchy; + } + + @Override + public int size() { + return map.size(); + } + + @Override + public boolean isEmpty() { + return map.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + if (key instanceof Ancestralizable method) { + for (Type superType : hierarchy.ancestry(method.getAssociatedType())) { + Ancestralizable id = method.withType(superType); + if (map.containsKey(id)) { + return true; + } + } + } + + return false; + } + + @Override + public boolean containsValue(Object value) { + return map.containsValue(value); + } + + @Override + public @Nullable T get(Object key) { + if (key instanceof Ancestralizable method) { + if (hierarchy.getNode(method.getAssociatedType()) == null) { + return map.get(method); + } + + for (Type superType : hierarchy.ancestry(method.getAssociatedType())) { + Ancestralizable id = method.withType(superType); + T value = map.get(id); + if (value != null) { + return value; + } + } + } + + return null; + } + + @Nullable + @Override + public T put(U key, T value) { + return map.put(key, value); + } + + @Override + @Nullable + public T remove(Object key) { + if (key instanceof Ancestralizable method) { + for (Type superType : hierarchy.ancestry(method.getAssociatedType())) { + Ancestralizable id = method.withType(superType); + T value = map.remove(key); + if (value != null) { + return value; + } + } + } + + return null; + } + + @Override + public void putAll(@NotNull Map m) { + map.putAll(m); + } + + @Override + public void clear() { + map.clear(); + } + + @NotNull + @Override + public Set keySet() { + return map.keySet(); + } + + @NotNull + @Override + public Collection values() { + return map.values(); + } + + @NotNull + @Override + public Set> entrySet() { + return map.entrySet(); + } +} diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/util/Ancestralizable.java b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/util/Ancestralizable.java new file mode 100644 index 000000000..62f25f379 --- /dev/null +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/util/Ancestralizable.java @@ -0,0 +1,8 @@ +package io.github.opencubicchunks.cubicchunks.mixin.transform.util; + +import org.objectweb.asm.Type; + +public interface Ancestralizable> { + Type getAssociatedType(); + T withType(Type type); +} diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/util/FabricMappingsProvider.java b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/util/FabricMappingsProvider.java new file mode 100644 index 000000000..1a2af4791 --- /dev/null +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/util/FabricMappingsProvider.java @@ -0,0 +1,56 @@ +package io.github.opencubicchunks.cubicchunks.mixin.transform.util; + +import io.github.opencubicchunks.dasm.MappingsProvider; +import net.fabricmc.loader.api.MappingResolver; + +public class FabricMappingsProvider implements MappingsProvider { + private final MappingsProvider fallback; + private final MappingResolver fabricResolver; + private final String namespace; + + public FabricMappingsProvider(MappingResolver fabricResolver, String namespace) { + this(MappingsProvider.IDENTITY, fabricResolver, namespace); + } + + public FabricMappingsProvider(MappingsProvider fallback, MappingResolver fabricResolver, String namespace) { + this.fallback = fallback; + this.fabricResolver = fabricResolver; + this.namespace = namespace; + } + + @Override + public String mapFieldName(String owner, String fieldName, String descriptor) { + boolean slash = owner.contains("/"); + if (slash) { + owner = owner.replace('/', '.'); + } + + String res = fabricResolver.mapFieldName(namespace, owner, fieldName, descriptor); + String mapped = res == null ? fallback.mapFieldName(owner, fieldName, descriptor) : res; + return slash ? mapped.replace('.', '/') : mapped; + } + + @Override + public String mapMethodName(String owner, String methodName, String descriptor) { + boolean slash = owner.contains("/"); + if (slash) { + owner = owner.replace('/', '.'); + } + + String res = fabricResolver.mapMethodName(namespace, owner, methodName, descriptor); + String mapped = res == null ? fallback.mapMethodName(owner, methodName, descriptor) : res; + return slash ? mapped.replace('.', '/') : mapped; + } + + @Override + public String mapClassName(String className) { + boolean slash = className.contains("/"); + if (slash) { + className = className.replace('/', '.'); + } + + String res = fabricResolver.mapClassName(namespace, className); + String mapped = res == null ? fallback.mapClassName(className) : res; + return slash ? mapped.replace('.', '/') : mapped; + } +} diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/util/FieldID.java b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/util/FieldID.java new file mode 100644 index 000000000..6d3c2a07d --- /dev/null +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/util/FieldID.java @@ -0,0 +1,34 @@ +package io.github.opencubicchunks.cubicchunks.mixin.transform.util; + +import org.jetbrains.annotations.Nullable; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.FieldNode; + +public record FieldID(Type owner, String name, Type desc) implements Ancestralizable { + public static FieldID of(String owner, FieldNode field) { + return new FieldID(Type.getObjectType(owner), field.name, Type.getType(field.desc)); + } + + @Override + public Type getAssociatedType() { + return owner; + } + + @Override + public FieldID withType(Type type) { + return new FieldID(type, name, desc); + } + + @Override + public String toString() { + return ASMUtil.onlyClassName(owner.getClassName()) + "." + name; + } + + public FieldNode toNode(int access) { + return toNode(null, access); + } + + public FieldNode toNode(@Nullable Object defaultValue, int access) { + return new FieldNode(access, name, desc.getDescriptor(), null, defaultValue); + } +} diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/util/MethodID.java b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/util/MethodID.java new file mode 100644 index 000000000..2c7f95ae0 --- /dev/null +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/util/MethodID.java @@ -0,0 +1,158 @@ +package io.github.opencubicchunks.cubicchunks.mixin.transform.util; + +import java.util.Objects; + +import it.unimi.dsi.fastutil.Hash; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; + +public class MethodID implements Ancestralizable { + public static final Hash.Strategy HASH_CALL_TYPE = new Hash.Strategy<>() { + @Override public int hashCode(MethodID o) { + return Objects.hash(o.callType, o.owner, o.name, o.descriptor); + } + + @Override public boolean equals(MethodID a, MethodID b) { + if (a == b) return true; + if (a == null || b == null) return false; + + return a.callType == b.callType && a.owner.equals(b.owner) && a.name.equals(b.name) && a.descriptor.equals(b.descriptor); + } + }; + + private final Type owner; + private final String name; + private final Type descriptor; + + private final CallType callType; + + public MethodID(Type owner, String name, Type descriptor, CallType callType) { + this.owner = owner; + this.name = name; + this.descriptor = descriptor; + this.callType = callType; + } + + public MethodID(String owner, String name, String desc, CallType callType) { + this(Type.getObjectType(owner), name, Type.getMethodType(desc), callType); + + } + + public static MethodID from(MethodInsnNode methodCall) { + Type owner = Type.getObjectType(methodCall.owner); + Type descriptor = Type.getMethodType(methodCall.desc); + String name = methodCall.name; + CallType callType = CallType.fromOpcode(methodCall.getOpcode()); + + return new MethodID(owner, name, descriptor, callType); + } + + public static MethodID of(ClassNode owner, MethodNode method) { + return new MethodID(Type.getObjectType(owner.name), method.name, Type.getMethodType(method.desc), ASMUtil.isStatic(method) ? CallType.STATIC : CallType.VIRTUAL); + } + + public Type getOwner() { + return owner; + } + + public String getName() { + return name; + } + + public Type getDescriptor() { + return descriptor; + } + + public CallType getCallType() { + return callType; + } + + public MethodInsnNode callNode() { + return new MethodInsnNode(callType.getOpcode(), owner.getInternalName(), name, descriptor.getDescriptor(), callType == CallType.INTERFACE); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + MethodID methodID = (MethodID) o; + return Objects.equals(owner, methodID.owner) && Objects.equals(name, methodID.name) && Objects.equals(descriptor, methodID.descriptor); + } + + @Override + public int hashCode() { + return Objects.hash(owner, name, descriptor); + } + + @Override + public Type getAssociatedType() { + return owner; + } + + @Override + public MethodID withType(Type type) { + return new MethodID(type, name, descriptor, callType); + } + + @Override + public String toString() { + String ownerName = ASMUtil.onlyClassName(owner.getClassName()); + + String returnTypeName = ASMUtil.onlyClassName(descriptor.getReturnType().getClassName()); + + StringBuilder sb = new StringBuilder(); + sb.append(returnTypeName).append(" "); + sb.append(ownerName).append(".").append(name).append("("); + int i = 0; + for (Type argType : descriptor.getArgumentTypes()) { + if (i > 0) { + sb.append(", "); + } + sb.append(ASMUtil.onlyClassName(argType.getClassName())); + i++; + } + sb.append(")"); + return sb.toString(); + } + + public boolean isStatic() { + return callType == CallType.STATIC; + } + + public enum CallType { + VIRTUAL(Opcodes.INVOKEVIRTUAL), + STATIC(Opcodes.INVOKESTATIC), + SPECIAL(Opcodes.INVOKESPECIAL), + INTERFACE(Opcodes.INVOKEINTERFACE); + + private final int opcode; + + CallType(int opcode) { + this.opcode = opcode; + } + + public static CallType fromOpcode(int opcode) { + return switch (opcode) { + case Opcodes.INVOKEVIRTUAL -> VIRTUAL; + case Opcodes.INVOKESTATIC -> STATIC; + case Opcodes.INVOKESPECIAL -> SPECIAL; + case Opcodes.INVOKEINTERFACE -> INTERFACE; + default -> throw new IllegalArgumentException("Unknown opcode " + opcode); + }; + } + + public int getOpcode() { + return opcode; + } + + public int getOffset() { + if (this == STATIC) { + return 0; + } + return 1; + } + } +} diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/util/package-info.java b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/util/package-info.java new file mode 100644 index 000000000..f4b085bbd --- /dev/null +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/transform/util/package-info.java @@ -0,0 +1,7 @@ +@ParametersAreNonnullByDefault +@MethodsReturnNonnullByDefault +package io.github.opencubicchunks.cubicchunks.mixin.transform.util; + +import javax.annotation.ParametersAreNonnullByDefault; + +import io.github.opencubicchunks.cc_core.annotation.MethodsReturnNonnullByDefault; diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/utils/Int3Iterator.java b/src/main/java/io/github/opencubicchunks/cubicchunks/utils/Int3Iterator.java new file mode 100644 index 000000000..945fcc8a9 --- /dev/null +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/utils/Int3Iterator.java @@ -0,0 +1,9 @@ +package io.github.opencubicchunks.cubicchunks.utils; + +public interface Int3Iterator { + boolean hasNext(); + + int getNextX(); + int getNextY(); + int getNextZ(); +} diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/utils/Int3List.java b/src/main/java/io/github/opencubicchunks/cubicchunks/utils/Int3List.java new file mode 100644 index 000000000..b756a2b72 --- /dev/null +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/utils/Int3List.java @@ -0,0 +1,308 @@ +package io.github.opencubicchunks.cubicchunks.utils; + +import java.util.Collection; +import java.util.Iterator; +import java.util.function.LongConsumer; + +import io.netty.util.internal.PlatformDependent; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Vec3i; + +public class Int3List implements AutoCloseable { + protected static final long X_VALUE_OFFSET = 0; + protected static final long Y_VALUE_OFFSET = X_VALUE_OFFSET + Integer.BYTES; + protected static final long Z_VALUE_OFFSET = Y_VALUE_OFFSET + Integer.BYTES; + + protected static final long VALUE_SIZE = Z_VALUE_OFFSET + Integer.BYTES; + + protected static final int DEFAULT_CAPACITY = 16; + + static { + if (!PlatformDependent.isUnaligned()) { + throw new AssertionError("your CPU doesn't support unaligned memory access!"); + } + } + + protected long arrayAddr = 0; + protected boolean closed = false; + + protected int capacity; + protected int size; + + public Int3List() { + this.capacity = DEFAULT_CAPACITY; + this.size = 0; + } + + public boolean add(int x, int y, int z) { + long arrayAddr = this.arrayAddr; + if (this.arrayAddr == 0) { + arrayAddr = this.arrayAddr = allocateTable(capacity); + } + + if (this.size >= this.capacity) { + arrayAddr = resize(); + } + + long putAt = arrayAddr + this.size * VALUE_SIZE; + PlatformDependent.putInt(putAt + X_VALUE_OFFSET, x); + PlatformDependent.putInt(putAt + Y_VALUE_OFFSET, y); + PlatformDependent.putInt(putAt + Z_VALUE_OFFSET, z); + + this.size++; + + return true; + } + + public void set(int index, int x, int y, int z) { + if (index < 0 || index >= this.size) { + throw new IndexOutOfBoundsException(); + } + + long arrayAddr = this.arrayAddr; + if (this.arrayAddr == 0) { + arrayAddr = this.arrayAddr = allocateTable(capacity); + } + + long putAt = arrayAddr + index * VALUE_SIZE; + PlatformDependent.putInt(putAt + X_VALUE_OFFSET, x); + PlatformDependent.putInt(putAt + Y_VALUE_OFFSET, y); + PlatformDependent.putInt(putAt + Z_VALUE_OFFSET, z); + } + + public void insert(int index, int x, int y, int z) { + if (index < 0 || index > this.size) { + throw new IndexOutOfBoundsException(); + } + + long arrayAddr = this.arrayAddr; + if (this.arrayAddr == 0) { + arrayAddr = this.arrayAddr = allocateTable(capacity); + } + + if (size >= capacity) { + resizeAndInsert(index, x, y, z); + return; + } + + //Shift all values that come after it + for (int j = size - 1; j >= index; j--) { + long copyFrom = arrayAddr + j * VALUE_SIZE; + long copyTo = copyFrom + VALUE_SIZE; + + PlatformDependent.copyMemory(copyFrom, copyTo, VALUE_SIZE); + } + + this.size++; + set(index, x, y, z); + } + + public void remove(int index) { + if (index < 0 || index >= this.size) { + throw new IndexOutOfBoundsException(); + } + + long arrayAddr = this.arrayAddr; + if (this.arrayAddr == 0) { + arrayAddr = this.arrayAddr = allocateTable(capacity); + } + + //Shift all values back one + for (int j = index + 1; j < size; j++) { + long copyFrom = arrayAddr + j * VALUE_SIZE; + long copyTo = copyFrom - VALUE_SIZE; + + PlatformDependent.copyMemory(copyFrom, copyTo, VALUE_SIZE); + } + + this.size--; + } + + public boolean remove(int x, int y, int z) { + for (int index = 0; index < size; index++) { + if (getX(index) == x && getY(index) == y && getZ(index) == z) { + remove(index); + return true; + } + } + return false; + } + + public void addAll(Collection positions) { + int necessaryCapacity = this.size + positions.size(); + + if (necessaryCapacity > capacity) { + resizeToFit((int) (necessaryCapacity * 1.5f)); + } + + int start = this.size; + this.size += positions.size(); + + Iterator iterator = positions.iterator(); + + for (; start < this.size; start++) { + Vec3i item = iterator.next(); + set(start, item.getX(), item.getY(), item.getZ()); + } + } + + public Vec3i[] toArray() { + Vec3i[] array = new Vec3i[size]; + + for (int i = 0; i < size; i++) { + array[i] = getVec3i(i); + } + + return array; + } + + public long[] toLongArray() { + long[] array = new long[size]; + + for (int i = 0; i < size; i++) { + array[i] = getAsBlockPos(i); + } + + return array; + } + + public int getX(int index) { + if (index < 0 || index >= this.size) { + throw new IndexOutOfBoundsException(); + } + + if (this.arrayAddr == 0) { + this.arrayAddr = allocateTable(capacity); + } + + return PlatformDependent.getInt(arrayAddr + index * VALUE_SIZE + X_VALUE_OFFSET); + } + + public int getY(int index) { + if (index < 0 || index >= this.size) { + throw new IndexOutOfBoundsException(); + } + + if (this.arrayAddr == 0) { + this.arrayAddr = allocateTable(capacity); + } + + return PlatformDependent.getInt(arrayAddr + index * VALUE_SIZE + Y_VALUE_OFFSET); + } + + public int getZ(int index) { + if (index < 0 || index >= this.size) { + throw new IndexOutOfBoundsException(); + } + + if (this.arrayAddr == 0) { + this.arrayAddr = allocateTable(capacity); + } + + return PlatformDependent.getInt(arrayAddr + index * VALUE_SIZE + Z_VALUE_OFFSET); + } + + public long getAsBlockPos(int index) { + if (index < 0 || index >= this.size) { + throw new IndexOutOfBoundsException(); + } + + if (this.arrayAddr == 0) { + this.arrayAddr = allocateTable(capacity); + } + + return BlockPos.asLong( + PlatformDependent.getInt(arrayAddr + index * VALUE_SIZE + X_VALUE_OFFSET), + PlatformDependent.getInt(arrayAddr + index * VALUE_SIZE + Y_VALUE_OFFSET), + PlatformDependent.getInt(arrayAddr + index * VALUE_SIZE + Z_VALUE_OFFSET) + ); + } + + public Vec3i getVec3i(int index) { + if (index < 0 || index >= this.size) { + throw new IndexOutOfBoundsException(); + } + + if (this.arrayAddr == 0) { + this.arrayAddr = allocateTable(capacity); + } + + return new Vec3i( + PlatformDependent.getInt(arrayAddr + index * VALUE_SIZE + X_VALUE_OFFSET), + PlatformDependent.getInt(arrayAddr + index * VALUE_SIZE + Y_VALUE_OFFSET), + PlatformDependent.getInt(arrayAddr + index * VALUE_SIZE + Z_VALUE_OFFSET) + ); + } + + public void forEach(LongConsumer consumer) { + for (int i = 0; i < size; i++) { + consumer.accept(getAsBlockPos(i)); + } + } + + public void forEach(XYZConsumer consumer) { + for (int i = 0; i < size; i++) { + consumer.accept(getX(i), getY(i), getZ(i)); + } + } + + public int size() { + return size; + } + + private void resizeToFit(int capacity) { + this.capacity = capacity; + this.arrayAddr = PlatformDependent.reallocateMemory(this.arrayAddr, capacity * VALUE_SIZE); + } + + private void resizeAndInsert(int index, int x, int y, int z) { + while (this.capacity <= this.size) { + this.capacity <<= 1; + } + + long newArrayAddr = allocateTable(this.capacity); + + PlatformDependent.copyMemory(this.arrayAddr, newArrayAddr, index * VALUE_SIZE); + PlatformDependent.copyMemory(this.arrayAddr + index * VALUE_SIZE, newArrayAddr + (index + 1) * VALUE_SIZE, (size - index) * VALUE_SIZE); + + PlatformDependent.freeMemory(this.arrayAddr); + this.arrayAddr = newArrayAddr; + + this.size++; + set(index, x, y, z); + } + + private long resize() { + while (this.capacity <= this.size) { + this.capacity <<= 1; + } + + return this.arrayAddr = PlatformDependent.reallocateMemory(this.arrayAddr, this.capacity * VALUE_SIZE); + } + + protected static long allocateTable(int capacity) { + long addr = PlatformDependent.allocateMemory(capacity * VALUE_SIZE); + PlatformDependent.setMemory(addr, capacity * VALUE_SIZE, (byte) 0); + return addr; + } + + @Override + public void close() { + if (closed) return; + + closed = true; + if (arrayAddr != 0L) { + PlatformDependent.freeMemory(arrayAddr); + } + } + + @Override + @SuppressWarnings("deprecation") + protected void finalize() { + close(); + } + + public void clear() { + this.size = 0; + } +} diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/utils/Int3UByteLinkedHashMap.java b/src/main/java/io/github/opencubicchunks/cubicchunks/utils/Int3UByteLinkedHashMap.java new file mode 100644 index 000000000..e81c687e4 --- /dev/null +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/utils/Int3UByteLinkedHashMap.java @@ -0,0 +1,808 @@ +package io.github.opencubicchunks.cubicchunks.utils; + +import io.netty.util.internal.PlatformDependent; +import net.minecraft.world.level.lighting.DynamicGraphMinFixedPoint; + +/** + * A fast hash-map implementation for 3-dimensional vectors with {@code int} components, mapped to unsigned {@code byte} values. + *

+ * Optimized for the case where queries will be close to each other. + *

+ * Buckets are arranged into a doubly linked list, which allows efficient iteration when the table is sparse and is crucial to keep {@link #poll(EntryConsumer)}'s average runtime nearly + * constant. + *

+ * Not thread-safe. Attempting to use this concurrently from multiple threads will likely have catastrophic results (read: JVM crashes). + * + * @author DaPorkchop_ + */ +public class Int3UByteLinkedHashMap implements AutoCloseable { + public static final int DEFAULT_RETURN_VALUE = -1; + + protected static final int BUCKET_AXIS_BITS = 2; //the number of bits per axis which are used inside of the bucket rather than identifying the bucket + protected static final int BUCKET_AXIS_MASK = (1 << BUCKET_AXIS_BITS) - 1; + protected static final int BUCKET_SIZE = 1 << (BUCKET_AXIS_BITS * 3); //the number of entries per bucket + + /* + * struct key_t { + * int x; + * int y; + * int z; + * }; + */ + + protected static final long KEY_X_OFFSET = 0L; + protected static final long KEY_Y_OFFSET = KEY_X_OFFSET + Integer.BYTES; + protected static final long KEY_Z_OFFSET = KEY_Y_OFFSET + Integer.BYTES; + protected static final long KEY_BYTES = KEY_Z_OFFSET + Integer.BYTES; + + /* + * struct value_t { + * long flags; + * byte vals[BUCKET_SIZE]; + * }; + */ + + protected static final long VALUE_FLAGS_OFFSET = 0L; + protected static final long VALUE_VALS_OFFSET = VALUE_FLAGS_OFFSET + Long.BYTES; + protected static final long VALUE_BYTES = VALUE_VALS_OFFSET + BUCKET_SIZE * Byte.BYTES; + + /* + * struct bucket_t { + * key_t key; + * value_t value; + * long prevIndex; + * long nextIndex; + * }; + */ + + protected static final long BUCKET_KEY_OFFSET = 0L; + protected static final long BUCKET_VALUE_OFFSET = BUCKET_KEY_OFFSET + KEY_BYTES; + protected static final long BUCKET_PREVINDEX_OFFSET = BUCKET_VALUE_OFFSET + VALUE_BYTES; + protected static final long BUCKET_NEXTINDEX_OFFSET = BUCKET_PREVINDEX_OFFSET + Long.BYTES; + protected static final long BUCKET_BYTES = BUCKET_NEXTINDEX_OFFSET + Long.BYTES; + + protected static final long DEFAULT_TABLE_SIZE = 16L; + + static { + if (!PlatformDependent.isUnaligned()) { + throw new AssertionError("your CPU doesn't support unaligned memory access!"); + } + } + + protected long tableAddr = 0L; //the address of the table in memory + protected long tableSize = 0L; //the physical size of the table (in buckets). always a non-zero power of two + protected long resizeThreshold = 0L; + protected long usedBuckets = 0L; + + protected long size = 0L; //the number of values stored in the set + + protected long firstBucketIndex = -1L; //index of the first known assigned bucket in the list + protected long lastBucketIndex = -1L; //index of the last known assigned bucket in the list + + protected boolean closed = false; + + //Used in DynamicGraphMinFixedPoint transform constructor + public Int3UByteLinkedHashMap(DynamicGraphMinFixedPoint $1, int $2, float $3, int $4) { + this(); + } + + public Int3UByteLinkedHashMap() { + this.setTableSize(DEFAULT_TABLE_SIZE); + } + + public Int3UByteLinkedHashMap(int initialCapacity) { + initialCapacity = (int) Math.ceil(initialCapacity * (1.0d / 0.75d)); //scale according to resize threshold + initialCapacity = 1 << (Integer.SIZE - Integer.numberOfLeadingZeros(initialCapacity - 1)); //round up to next power of two + this.setTableSize(Math.max(initialCapacity, DEFAULT_TABLE_SIZE)); + } + + protected Int3UByteLinkedHashMap(Int3UByteLinkedHashMap src) { + if (src.tableAddr != 0L) { //source table is allocated, let's copy it + long tableSizeBytes = src.tableSize * BUCKET_BYTES; + this.tableAddr = PlatformDependent.allocateMemory(tableSizeBytes); + PlatformDependent.copyMemory(src.tableAddr, this.tableAddr, tableSizeBytes); + } + + this.tableSize = src.tableSize; + this.resizeThreshold = src.resizeThreshold; + this.usedBuckets = src.usedBuckets; + this.size = src.size; + this.firstBucketIndex = src.firstBucketIndex; + this.lastBucketIndex = src.lastBucketIndex; + } + + /** + * Faster memcpy routine (for small ranges) which JIT can optimize specifically for the range size. + * + * @param srcAddr the source address + * @param dstAddr the destination address + */ + protected static void memcpy(long srcAddr, long dstAddr, long size) { + long offset = 0L; + + while (size - offset >= Long.BYTES) { //copy as many longs as possible + PlatformDependent.putLong(dstAddr + offset, PlatformDependent.getLong(srcAddr + offset)); + offset += Long.BYTES; + } + + while (size - offset >= Integer.BYTES) { //pad with ints + PlatformDependent.putInt(dstAddr + offset, PlatformDependent.getInt(srcAddr + offset)); + offset += Integer.BYTES; + } + + while (size - offset >= Byte.BYTES) { //pad with bytes + PlatformDependent.putByte(dstAddr + offset, PlatformDependent.getByte(srcAddr + offset)); + offset += Byte.BYTES; + } + + assert offset == size; + } + + protected static long hashPosition(int x, int y, int z) { + return x * 1403638657883916319L //some random prime numbers + + y * 4408464607732138253L + + z * 2587306874955016303L; + } + + protected static int positionIndex(int x, int y, int z) { + return ((x & BUCKET_AXIS_MASK) << (BUCKET_AXIS_BITS * 2)) | ((y & BUCKET_AXIS_MASK) << BUCKET_AXIS_BITS) | (z & BUCKET_AXIS_MASK); + } + + protected static long positionFlag(int x, int y, int z) { + return 1L << positionIndex(x, y, z); + } + + protected static long allocateTable(long tableSize) { + long size = tableSize * BUCKET_BYTES; + long addr = PlatformDependent.allocateMemory(size); //allocate + PlatformDependent.setMemory(addr, size, (byte) 0); //clear + return addr; + } + + /** + * Inserts an entry into this map at the given position with the given value. + *

+ * If an entry with the given position is already present in this map, it will be replaced. + * + * @param x the position's X coordinate + * @param y the position's Y coordinate + * @param z the position's Z coordinate + * @param value the value to insert. Must be an unsigned {@code byte} + * + * @return the previous entry's value, or {@link #DEFAULT_RETURN_VALUE} if no such entry was present + * + * @see java.util.Map#put(Object, Object) + */ + public int put(int x, int y, int z, int value) { + assert (value & 0xFF) == value : "value not in range [0,255]: " + value; + + int index = positionIndex(x, y, z); + long flag = positionFlag(x, y, z); + long bucket = this.findBucket(x >> BUCKET_AXIS_BITS, y >> BUCKET_AXIS_BITS, z >> BUCKET_AXIS_BITS, true); + + int oldValue; + long flags = PlatformDependent.getLong(bucket + BUCKET_VALUE_OFFSET + VALUE_FLAGS_OFFSET); + if ((flags & flag) == 0L) { //flag wasn't previously set + PlatformDependent.putLong(bucket + BUCKET_VALUE_OFFSET + VALUE_FLAGS_OFFSET, flags | flag); + this.size++; //the position was newly added, so we need to increment the total size + oldValue = DEFAULT_RETURN_VALUE; + } else { //the flag was already set + oldValue = PlatformDependent.getByte(bucket + BUCKET_VALUE_OFFSET + VALUE_VALS_OFFSET + index * Byte.BYTES) & 0xFF; + } + + //store value into bucket + PlatformDependent.putByte(bucket + BUCKET_VALUE_OFFSET + VALUE_VALS_OFFSET + index * Byte.BYTES, (byte) value); + return oldValue; + } + + /** + * Inserts an entry into this map at the given position with the given value. + *

+ * If an entry with the given position is already present in this map, the map will not be modified. + * + * @param x the position's X coordinate + * @param y the position's Y coordinate + * @param z the position's Z coordinate + * @param value the value to insert. Must be an unsigned {@code byte} + * + * @return the previous entry's value, or {@link #DEFAULT_RETURN_VALUE} if no such entry was present and the entry was inserted + * + * @see java.util.Map#putIfAbsent(Object, Object) + */ + public int putIfAbsent(int x, int y, int z, int value) { + assert (value & 0xFF) == value : "value not in range [0,255]: " + value; + + int index = positionIndex(x, y, z); + long flag = positionFlag(x, y, z); + long bucket = this.findBucket(x >> BUCKET_AXIS_BITS, y >> BUCKET_AXIS_BITS, z >> BUCKET_AXIS_BITS, true); + + long flags = PlatformDependent.getLong(bucket + BUCKET_VALUE_OFFSET + VALUE_FLAGS_OFFSET); + if ((flags & flag) == 0L) { //flag wasn't previously set + PlatformDependent.putLong(bucket + BUCKET_VALUE_OFFSET + VALUE_FLAGS_OFFSET, flags | flag); + this.size++; //the position was newly added, so we need to increment the total size + PlatformDependent.putByte(bucket + BUCKET_VALUE_OFFSET + VALUE_VALS_OFFSET + index * Byte.BYTES, (byte) value); + return DEFAULT_RETURN_VALUE; + } else { //the flag was already set + return PlatformDependent.getByte(bucket + BUCKET_VALUE_OFFSET + VALUE_VALS_OFFSET + index * Byte.BYTES) & 0xFF; + } + } + + /** + * Checks whether or not an entry at the given position is present in this map. + * + * @param x the position's X coordinate + * @param y the position's Y coordinate + * @param z the position's Z coordinate + * + * @return whether or not the position is present + * + * @see java.util.Map#containsKey(Object) + */ + public boolean containsKey(int x, int y, int z) { + long flag = positionFlag(x, y, z); + long bucket = this.findBucket(x >> BUCKET_AXIS_BITS, y >> BUCKET_AXIS_BITS, z >> BUCKET_AXIS_BITS, false); + + return bucket != 0L //bucket exists + && (PlatformDependent.getLong(bucket + BUCKET_VALUE_OFFSET + VALUE_FLAGS_OFFSET) & flag) != 0L; //flag is set + } + + /** + * Gets the value of the entry associated with the given position. + * + * @param x the position's X coordinate + * @param y the position's Y coordinate + * @param z the position's Z coordinate + * + * @return the entry's value, or {@link #DEFAULT_RETURN_VALUE} if no such entry was present + * + * @see java.util.Map#get(Object) + */ + public int get(int x, int y, int z) { + int index = positionIndex(x, y, z); + long flag = positionFlag(x, y, z); + long bucket = this.findBucket(x >> BUCKET_AXIS_BITS, y >> BUCKET_AXIS_BITS, z >> BUCKET_AXIS_BITS, false); + + if (bucket != 0L //bucket exists + && (PlatformDependent.getLong(bucket + BUCKET_VALUE_OFFSET + VALUE_FLAGS_OFFSET) & flag) != 0L) { //flag is set + return PlatformDependent.getByte(bucket + BUCKET_VALUE_OFFSET + VALUE_VALS_OFFSET + index * Byte.BYTES) & 0xFF; + } else { //bucket doesn't exist or doesn't contain the position + return DEFAULT_RETURN_VALUE; + } + } + + protected long findBucket(int x, int y, int z, boolean createIfAbsent) { + long tableSize = this.tableSize; + long tableAddr = this.tableAddr; + if (tableAddr == 0L) { + if (createIfAbsent) { //the table hasn't been allocated yet - let's make a new one! + this.tableAddr = tableAddr = allocateTable(tableSize); + } else { //the table isn't even allocated yet, so the bucket clearly isn't present + return 0L; + } + } + + long mask = tableSize - 1L; //tableSize is always a power of two, so we can safely create a bitmask like this + long hash = hashPosition(x, y, z); + + for (long i = 0L; ; i++) { + long bucketIndex = (hash + i) & mask; + long bucketAddr = tableAddr + bucketIndex * BUCKET_BYTES; + + if (PlatformDependent.getLong(bucketAddr + BUCKET_VALUE_OFFSET + VALUE_FLAGS_OFFSET) == 0L) { //if the value's flags are 0, it means the bucket hasn't been assigned yet + if (createIfAbsent) { + if (this.usedBuckets < this.resizeThreshold) { //let's assign the bucket to our current position + this.usedBuckets++; + PlatformDependent.putInt(bucketAddr + BUCKET_KEY_OFFSET + KEY_X_OFFSET, x); + PlatformDependent.putInt(bucketAddr + BUCKET_KEY_OFFSET + KEY_Y_OFFSET, y); + PlatformDependent.putInt(bucketAddr + BUCKET_KEY_OFFSET + KEY_Z_OFFSET, z); + + //add bucket to linked list + long prevBucketIndex = -1L; + long nextBucketIndex = -1L; + if (this.firstBucketIndex < 0L) { //no other buckets exist + this.firstBucketIndex = bucketIndex; + } else { //there are other buckets, let's insert this bucket at the back of the list + prevBucketIndex = this.lastBucketIndex; + + long prevBucketAddr = tableAddr + prevBucketIndex * BUCKET_BYTES; + PlatformDependent.putLong(prevBucketAddr + BUCKET_NEXTINDEX_OFFSET, bucketIndex); + } + PlatformDependent.putLong(bucketAddr + BUCKET_PREVINDEX_OFFSET, prevBucketIndex); + PlatformDependent.putLong(bucketAddr + BUCKET_NEXTINDEX_OFFSET, nextBucketIndex); + this.lastBucketIndex = bucketIndex; + + return bucketAddr; + } else { + //we've established that there's no matching bucket, but the table is full. let's resize it before allocating a bucket + // to avoid overfilling the table + this.resize(); + return this.findBucket(x, y, z, createIfAbsent); //tail recursion will probably be optimized away + } + } else { //empty bucket, abort search - there won't be anything else later on + return 0L; + } + } + + //the bucket is set. check coordinates to see if it matches the one we're searching for + if (PlatformDependent.getInt(bucketAddr + BUCKET_KEY_OFFSET + KEY_X_OFFSET) == x + && PlatformDependent.getInt(bucketAddr + BUCKET_KEY_OFFSET + KEY_Y_OFFSET) == y + && PlatformDependent.getInt(bucketAddr + BUCKET_KEY_OFFSET + KEY_Z_OFFSET) == z) { //we found the matching bucket! + return bucketAddr; + } + + //continue search... + } + } + + protected void resize() { + long oldTableSize = this.tableSize; + long oldTableAddr = this.tableAddr; + + //allocate new table + long newTableSize = oldTableSize << 1L; + this.setTableSize(newTableSize); + long newTableAddr = this.tableAddr = allocateTable(newTableSize); + long newMask = newTableSize - 1L; + + //iterate through every bucket in the old table and copy it to the new one + for (long oldBucketIndex = 0; oldBucketIndex < oldTableSize; oldBucketIndex++) { + long oldBucketAddr = oldTableAddr + oldBucketIndex * BUCKET_BYTES; + + //read the key into registers + int x = PlatformDependent.getInt(oldBucketAddr + BUCKET_KEY_OFFSET + KEY_X_OFFSET); + int y = PlatformDependent.getInt(oldBucketAddr + BUCKET_KEY_OFFSET + KEY_Y_OFFSET); + int z = PlatformDependent.getInt(oldBucketAddr + BUCKET_KEY_OFFSET + KEY_Z_OFFSET); + if (PlatformDependent.getLong(oldBucketAddr + BUCKET_VALUE_OFFSET + VALUE_FLAGS_OFFSET) == 0L) { //the bucket is unset, so there's no reason to copy it + continue; + } + + for (long hash = hashPosition(x, y, z), j = 0L; ; j++) { + long newBucketAddr = newTableAddr + ((hash + j) & newMask) * BUCKET_BYTES; + + if (PlatformDependent.getLong(newBucketAddr + BUCKET_VALUE_OFFSET + VALUE_FLAGS_OFFSET) == 0L) { //if the bucket value is 0, it means the bucket hasn't been assigned yet + //write bucket into new table + PlatformDependent.putInt(newBucketAddr + BUCKET_KEY_OFFSET + KEY_X_OFFSET, x); + PlatformDependent.putInt(newBucketAddr + BUCKET_KEY_OFFSET + KEY_Y_OFFSET, y); + PlatformDependent.putInt(newBucketAddr + BUCKET_KEY_OFFSET + KEY_Z_OFFSET, z); + memcpy(oldBucketAddr + BUCKET_VALUE_OFFSET, newBucketAddr + BUCKET_VALUE_OFFSET, VALUE_BYTES); + PlatformDependent.putLong(newBucketAddr + BUCKET_PREVINDEX_OFFSET, -1L); + PlatformDependent.putLong(newBucketAddr + BUCKET_NEXTINDEX_OFFSET, -1L); + break; //advance to next bucket in old table + } + + //continue search... + } + } + + //delete old table + PlatformDependent.freeMemory(oldTableAddr); + + //iterate through every bucket in the new table and append non-empty buckets to the new linked list + long prevBucketIndex = -1L; + for (long bucketIndex = 0; bucketIndex < newTableSize; bucketIndex++) { + long bucketAddr = newTableAddr + bucketIndex * BUCKET_BYTES; + + if (PlatformDependent.getLong(bucketAddr + BUCKET_VALUE_OFFSET + VALUE_FLAGS_OFFSET) == 0L) { //the bucket is unset, so there's no reason to add it to the list + continue; + } + + if (prevBucketIndex < 0L) { //this is first bucket we've encountered in the list so far + this.firstBucketIndex = bucketIndex; + } else { //append current bucket to list + long prevBucketAddr = newTableAddr + prevBucketIndex * BUCKET_BYTES; + + PlatformDependent.putLong(prevBucketAddr + BUCKET_NEXTINDEX_OFFSET, bucketIndex); + PlatformDependent.putLong(bucketAddr + BUCKET_PREVINDEX_OFFSET, prevBucketIndex); + } + prevBucketIndex = bucketIndex; + } + this.lastBucketIndex = prevBucketIndex; + } + + /** + * Runs the given callback function on every entry in this map. + *

+ * The callback function must not modify this map. + * + * @param action the callback function + * + * @see java.util.Map#forEach(java.util.function.BiConsumer) + */ + public void forEach(EntryConsumer action) { + if (this.tableAddr == 0L //table hasn't even been allocated + || this.isEmpty()) { //no entries are present + return; //there's nothing to iterate over... + } + + if (this.usedBuckets >= (this.tableSize >> 1L)) { //table is at least half-full + this.forEachFull(action); + } else { + this.forEachSparse(action); + } + } + + protected void forEachFull(EntryConsumer action) { //optimized for the case where the table is mostly full + //haha yes, c-style iterators + for (long bucketAddr = this.tableAddr, end = bucketAddr + this.tableSize * BUCKET_BYTES; bucketAddr != end; bucketAddr += BUCKET_BYTES) { + this.forEachInBucket(action, bucketAddr); + } + } + + protected void forEachSparse(EntryConsumer action) { //optimized for the case where the table is mostly empty + long tableAddr = this.tableAddr; + + for (long bucketIndex = this.firstBucketIndex, bucketAddr = tableAddr + bucketIndex * BUCKET_BYTES; + bucketIndex >= 0L; + bucketIndex = PlatformDependent.getLong(bucketAddr + BUCKET_NEXTINDEX_OFFSET), bucketAddr = tableAddr + bucketIndex * BUCKET_BYTES) { + this.forEachInBucket(action, bucketAddr); + } + } + + protected void forEachInBucket(EntryConsumer action, long bucketAddr) { + //read the bucket's key and flags into registers + int bucketX = PlatformDependent.getInt(bucketAddr + BUCKET_KEY_OFFSET + KEY_X_OFFSET); + int bucketY = PlatformDependent.getInt(bucketAddr + BUCKET_KEY_OFFSET + KEY_Y_OFFSET); + int bucketZ = PlatformDependent.getInt(bucketAddr + BUCKET_KEY_OFFSET + KEY_Z_OFFSET); + long flags = PlatformDependent.getLong(bucketAddr + BUCKET_VALUE_OFFSET + VALUE_FLAGS_OFFSET); + + while (flags != 0L) { + //this is intrinsic and compiles into TZCNT, which has a latency of 3 cycles - much faster than iterating through all 64 bits + // and checking each one individually! + int index = Long.numberOfTrailingZeros(flags); + + //clear the bit in question so that it won't be returned next time around + flags &= ~(1L << index); + + int dx = index >> (BUCKET_AXIS_BITS * 2); + int dy = (index >> BUCKET_AXIS_BITS) & BUCKET_AXIS_MASK; + int dz = index & BUCKET_AXIS_MASK; + int val = PlatformDependent.getByte(bucketAddr + BUCKET_VALUE_OFFSET + VALUE_VALS_OFFSET + index * Byte.BYTES) & 0xFF; + action.accept((bucketX << BUCKET_AXIS_BITS) + dx, (bucketY << BUCKET_AXIS_BITS) + dy, (bucketZ << BUCKET_AXIS_BITS) + dz, val); + } + } + + /** + * Removes the entry at the given position from this map. + * + * @param x the position's X coordinate + * @param y the position's Y coordinate + * @param z the position's Z coordinate + * + * @return the old value at the given position, or {@link #DEFAULT_RETURN_VALUE} if the position wasn't present + * + * @see java.util.Map#remove(Object) + */ + public int remove(int x, int y, int z) { + long tableAddr = this.tableAddr; + if (tableAddr == 0L) { //the table isn't even allocated yet, there's nothing to remove... + return DEFAULT_RETURN_VALUE; + } + + long mask = this.tableSize - 1L; //tableSize is always a power of two, so we can safely create a bitmask like this + + long flag = positionFlag(x, y, z); + int searchBucketX = x >> BUCKET_AXIS_BITS; + int searchBucketY = y >> BUCKET_AXIS_BITS; + int searchBucketZ = z >> BUCKET_AXIS_BITS; + long hash = hashPosition(searchBucketX, searchBucketY, searchBucketZ); + + for (long i = 0L; ; i++) { + long bucketIndex = (hash + i) & mask; + long bucketAddr = tableAddr + bucketIndex * BUCKET_BYTES; + + //read the bucket's key and flags into registers + int bucketX = PlatformDependent.getInt(bucketAddr + BUCKET_KEY_OFFSET + KEY_X_OFFSET); + int bucketY = PlatformDependent.getInt(bucketAddr + BUCKET_KEY_OFFSET + KEY_Y_OFFSET); + int bucketZ = PlatformDependent.getInt(bucketAddr + BUCKET_KEY_OFFSET + KEY_Z_OFFSET); + long flags = PlatformDependent.getLong(bucketAddr + BUCKET_VALUE_OFFSET + VALUE_FLAGS_OFFSET); + if (flags == 0L) { //the bucket is unset. we've reached the end of the bucket chain for this hash, which means it doesn't exist + return DEFAULT_RETURN_VALUE; + } else if (bucketX != searchBucketX || bucketY != searchBucketY || bucketZ != searchBucketZ) { //the bucket doesn't match, so the search must go on + continue; + } else if ((flags & flag) == 0L) { //we've found a matching bucket, but the position's flag is unset. there's nothing for us to do... + return DEFAULT_RETURN_VALUE; + } + + //load the old value in order to return it later (there's no reason to zero it out, since the flag bit will be cleared anyway) + int oldVal = PlatformDependent.getByte(bucketAddr + BUCKET_VALUE_OFFSET + VALUE_VALS_OFFSET + positionIndex(x, y, z) * Byte.BYTES) & 0xFF; + + //remove entry from map + this.removeEntry(tableAddr, mask, bucketIndex, bucketAddr, flags, flag); + + return oldVal; + } + } + + /** + * Gets and removes an entry from this map, then passes it to the given callback function. + *

+ * The callback function is allowed to modify this map. + * + * @param action the callback function + * + * @return whether or not the callback function was invoked. A return value of {@code false} indicates that the map was already empty + */ + public boolean poll(EntryConsumer action) { + long bucketIndex = this.firstBucketIndex; + if (bucketIndex >= 0L) { + long tableAddr = this.tableAddr; + long bucketAddr = tableAddr + bucketIndex * BUCKET_BYTES; + + //read the bucket's key and flags into registers + int bucketX = PlatformDependent.getInt(bucketAddr + BUCKET_KEY_OFFSET + KEY_X_OFFSET); + int bucketY = PlatformDependent.getInt(bucketAddr + BUCKET_KEY_OFFSET + KEY_Y_OFFSET); + int bucketZ = PlatformDependent.getInt(bucketAddr + BUCKET_KEY_OFFSET + KEY_Z_OFFSET); + long flags = PlatformDependent.getLong(bucketAddr + BUCKET_VALUE_OFFSET + VALUE_FLAGS_OFFSET); + + assert flags != 0L : "polled empty bucket?!?"; + + //this is intrinsic and compiles into TZCNT, which has a latency of 3 cycles - much faster than iterating through all 64 bits + // and checking each one individually! + int index = Long.numberOfTrailingZeros(flags); + + //compute entry position within bucket + int dx = index >> (BUCKET_AXIS_BITS * 2); + int dy = (index >> BUCKET_AXIS_BITS) & BUCKET_AXIS_MASK; + int dz = index & BUCKET_AXIS_MASK; + int val = PlatformDependent.getByte(bucketAddr + BUCKET_VALUE_OFFSET + VALUE_VALS_OFFSET + index * Byte.BYTES) & 0xFF; + + //remove entry from bucket + this.removeEntry(tableAddr, this.tableSize - 1L, bucketIndex, bucketAddr, flags, 1L << index); + + //run the callback + action.accept((bucketX << BUCKET_AXIS_BITS) + dx, (bucketY << BUCKET_AXIS_BITS) + dy, (bucketZ << BUCKET_AXIS_BITS) + dz, val); + return true; + } else { + return false; + } + } + + //assumes that the entry is present in the bucket + protected void removeEntry(long tableAddr, long mask, long bucketIndex, long bucketAddr, long flags, long flag) { + //the bucket that we found contains the position, so now we remove it from the set + this.size--; + + //update bucket flags + flags &= ~flag; + PlatformDependent.putLong(bucketAddr + BUCKET_VALUE_OFFSET + VALUE_FLAGS_OFFSET, flags); + + if (flags == 0L) { //this position was the only position in the bucket, so we need to delete the bucket + this.usedBuckets--; + + //remove the bucket from the linked list + long prevBucketIndex = PlatformDependent.getLong(bucketAddr + BUCKET_PREVINDEX_OFFSET); + long nextBucketIndex = PlatformDependent.getLong(bucketAddr + BUCKET_NEXTINDEX_OFFSET); + + if (prevBucketIndex < 0L) { //previous bucket is nullptr, meaning the current bucket used to be at the front + this.firstBucketIndex = nextBucketIndex; + } else { + long prevBucketAddr = tableAddr + prevBucketIndex * BUCKET_BYTES; + PlatformDependent.putLong(prevBucketAddr + BUCKET_NEXTINDEX_OFFSET, nextBucketIndex); + } + if (nextBucketIndex < 0L) { //next bucket is nullptr, meaning the current bucket used to be at the back + this.lastBucketIndex = prevBucketIndex; + } else { + long nextBucketAddr = tableAddr + nextBucketIndex * BUCKET_BYTES; + PlatformDependent.putLong(nextBucketAddr + BUCKET_PREVINDEX_OFFSET, prevBucketIndex); + } + + //shifting the buckets IS expensive, yes, but it'll only happen when the entire bucket is deleted, which won't happen on every removal + this.shiftBuckets(tableAddr, bucketIndex, mask); + } + } + + //adapted from it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap#shiftKeys(int) + protected void shiftBuckets(long tableAddr, long pos, long mask) { + long last; + long slot; + + while (true) { + for (pos = ((last = pos) + 1L) & mask; ; pos = (pos + 1L) & mask) { + long currAddr = tableAddr + pos * BUCKET_BYTES; + if (PlatformDependent.getLong(currAddr + BUCKET_VALUE_OFFSET + VALUE_FLAGS_OFFSET) == 0L) { //curr points to an unset bucket + if (PlatformDependent.getLong(tableAddr + last * BUCKET_BYTES + BUCKET_VALUE_OFFSET + VALUE_FLAGS_OFFSET) != 0L) { + System.out.println("non-zero!"); + } + //PlatformDependent.putLong(tableAddr + last * BUCKET_BYTES + BUCKET_VALUE_OFFSET + VALUE_FLAGS_OFFSET, 0L); //delete last bucket + return; + } + + slot = hashPosition( + PlatformDependent.getInt(currAddr + BUCKET_KEY_OFFSET + KEY_X_OFFSET), + PlatformDependent.getInt(currAddr + BUCKET_KEY_OFFSET + KEY_Y_OFFSET), + PlatformDependent.getInt(currAddr + BUCKET_KEY_OFFSET + KEY_Z_OFFSET)) & mask; + + if (last <= pos ? last >= slot || slot > pos : last >= slot && slot > pos) { //move the bucket + long newAddr = tableAddr + last * BUCKET_BYTES; + + //copy bucket to new address + memcpy(currAddr, newAddr, BUCKET_BYTES); + + //clear flags in bucket's old position to mark it as empty + PlatformDependent.putLong(currAddr + BUCKET_VALUE_OFFSET + VALUE_FLAGS_OFFSET, 0L); + + //update pointer to self in linked list neighbors + long prevBucketIndex = PlatformDependent.getLong(currAddr + BUCKET_PREVINDEX_OFFSET); + long nextBucketIndex = PlatformDependent.getLong(currAddr + BUCKET_NEXTINDEX_OFFSET); + if (prevBucketIndex < 0L) { //previous bucket is nullptr, meaning the current bucket used to be at the front + this.firstBucketIndex = last; + } else { + long prevBucketAddr = tableAddr + prevBucketIndex * BUCKET_BYTES; + PlatformDependent.putLong(prevBucketAddr + BUCKET_NEXTINDEX_OFFSET, last); + } + if (nextBucketIndex < 0L) { //next bucket is nullptr, meaning the current bucket used to be at the back + this.lastBucketIndex = last; + } else { + long nextBucketAddr = tableAddr + nextBucketIndex * BUCKET_BYTES; + PlatformDependent.putLong(nextBucketAddr + BUCKET_PREVINDEX_OFFSET, last); + } + + break; + } + } + } + } + + /** + * Removes every entry from this set. + * + * @see java.util.Map#clear() + */ + public void clear() { + if (this.isEmpty()) { //if the set is empty, there's nothing to clear + return; + } + + //fill the entire table with zeroes + // (since the table isn't empty, we can be sure that the table has been allocated so there's no reason to check for it) + PlatformDependent.setMemory(this.tableAddr, this.tableSize * BUCKET_BYTES, (byte) 0); + + //reset all size counters + this.usedBuckets = 0L; + this.size = 0L; + this.firstBucketIndex = -1L; + this.lastBucketIndex = -1L; + } + + protected void setTableSize(long tableSize) { + this.tableSize = tableSize; + this.resizeThreshold = (tableSize >> 1L) + (tableSize >> 2L); //count * 0.75 + } + + /** + * Called longSize to ensure compatibility with the vanilla use of {@code int Long2ByteOpenHashMap.size()} + * + * @return the number of entries stored in this map + */ + public long longSize() { + return this.size; + } + + /** + * @return whether or not this map is empty (contains no entries) + */ + public boolean isEmpty() { + return this.size == 0L; + } + + @Override + public Int3UByteLinkedHashMap clone() { + return new Int3UByteLinkedHashMap(this); + } + + /** + * Irrevocably releases the resources claimed by this instance. + *

+ * Once this method has been called, all methods in this class will produce undefined behavior. + */ + @Override + public void close() { + if (this.closed) { + return; + } + this.closed = true; + + //actually release memory + if (this.tableAddr != 0L) { + PlatformDependent.freeMemory(this.tableAddr); + } + } + + @Override + @SuppressWarnings("deprecation") + protected void finalize() { + //using a finalizer is bad, i know. however, there's no other reasonable way for me to clean up the memory without pulling in PorkLib:unsafe or + // using sun.misc.Cleaner directly... + this.close(); + } + + public Int3KeySet int3KeySet() { + return new Int3KeySet(); + } + + public int size() { + return (int) size; + } + + //TODO: Make this more efficient + public LinkedInt3HashSet keySet() { + LinkedInt3HashSet set = new LinkedInt3HashSet(); + this.forEach((x, y, z, __) -> set.add(x, y, z)); + return set; + } + + private void forEachKeySparse(XYZConsumer action) { + long tableAddr = this.tableAddr; + + for (long bucketIndex = this.firstBucketIndex, bucketAddr = tableAddr + bucketIndex * BUCKET_BYTES; + bucketIndex >= 0L; + bucketIndex = PlatformDependent.getLong(bucketAddr + BUCKET_NEXTINDEX_OFFSET), bucketAddr = tableAddr + bucketIndex * BUCKET_BYTES) { + this.forEachKeyInBucket(action, bucketAddr); + } + } + + private void forEachKeyFull(XYZConsumer action) { + for (long bucketAddr = this.tableAddr, end = bucketAddr + this.tableSize * BUCKET_BYTES; bucketAddr != end; bucketAddr += BUCKET_BYTES) { + this.forEachKeyInBucket(action, bucketAddr); + } + } + + private void forEachKeyInBucket(XYZConsumer action, long bucketAddr) { + //read the bucket's key and flags into registers + int bucketX = PlatformDependent.getInt(bucketAddr + BUCKET_KEY_OFFSET + KEY_X_OFFSET); + int bucketY = PlatformDependent.getInt(bucketAddr + BUCKET_KEY_OFFSET + KEY_Y_OFFSET); + int bucketZ = PlatformDependent.getInt(bucketAddr + BUCKET_KEY_OFFSET + KEY_Z_OFFSET); + long flags = PlatformDependent.getLong(bucketAddr + BUCKET_VALUE_OFFSET + VALUE_FLAGS_OFFSET); + + while (flags != 0L) { + //this is intrinsic and compiles into TZCNT, which has a latency of 3 cycles - much faster than iterating through all 64 bits + // and checking each one individually! + int index = Long.numberOfTrailingZeros(flags); + + //clear the bit in question so that it won't be returned next time around + flags &= ~(1L << index); + + int dx = index >> (BUCKET_AXIS_BITS * 2); + int dy = (index >> BUCKET_AXIS_BITS) & BUCKET_AXIS_MASK; + int dz = index & BUCKET_AXIS_MASK; + action.accept((bucketX << BUCKET_AXIS_BITS) + dx, (bucketY << BUCKET_AXIS_BITS) + dy, (bucketZ << BUCKET_AXIS_BITS) + dz); + } + } + + /** + * This is a dummy function used in the transformation of the DynamicGraphMinFixedPoint constructor. Calling it will do nothing. + * + * @deprecated So no one touches it + */ + @Deprecated + public void defaultReturnValue(byte b) { + if (b != -1) { + throw new IllegalStateException("Default return value is not -1"); + } + } + + //These methods are very similar to ones defined above + public class Int3KeySet { + //This is the only method that ever gets called on it + public void forEach(XYZConsumer action) { + if (tableAddr == 0L //table hasn't even been allocated + || isEmpty()) { //no entries are present + return; //there's nothing to iterate over... + } + + if (usedBuckets >= (tableSize >> 1L)) { //table is at least half-full + forEachKeyFull(action); + } else { + forEachKeySparse(action); + } + } + } + + /** + * A function which accepts a map entry (consisting of 3 {@code int}s for the key and 1 {@code int} for the value) as a parameter. + */ + @FunctionalInterface + public interface EntryConsumer { + void accept(int x, int y, int z, int value); + } +} diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/utils/LinkedInt3HashSet.java b/src/main/java/io/github/opencubicchunks/cubicchunks/utils/LinkedInt3HashSet.java new file mode 100644 index 000000000..29cca0932 --- /dev/null +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/utils/LinkedInt3HashSet.java @@ -0,0 +1,641 @@ +package io.github.opencubicchunks.cubicchunks.utils; + +import java.util.NoSuchElementException; + +import io.netty.util.internal.PlatformDependent; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.lighting.DynamicGraphMinFixedPoint; +import org.apache.commons.lang3.mutable.MutableInt; + +/** + * Modification of DaPorkchop_'s original {@code Int3HashSet} which keeps track of a linked list which allows for easy lookup of elements. This makes it a bit slower and uses up more memory + *

+ * Original Description (By DaPorkchop_): + *
+ * A fast hash-set implementation for 3-dimensional vectors with {@code int} components. + *

+ * Optimized for the case where queries will be close to each other. + *

+ * Not thread-safe. Attempting to use this concurrently from multiple threads will likely have catastrophic results (read: JVM crashes). + * + * @author DaPorkchop_ & Salamander + */ +public class LinkedInt3HashSet implements AutoCloseable { + protected static final long KEY_X_OFFSET = 0L; + protected static final long KEY_Y_OFFSET = KEY_X_OFFSET + Integer.BYTES; + protected static final long KEY_Z_OFFSET = KEY_Y_OFFSET + Integer.BYTES; + protected static final long KEY_BYTES = KEY_Z_OFFSET + Integer.BYTES; + + protected static final long VALUE_BYTES = Long.BYTES; + protected static final long NEXT_VALUE_BYTES = Long.BYTES; + protected static final long PREV_VALUE_BYTES = Long.BYTES; + protected static final long NEXT_VALUE_OFFSET = KEY_BYTES + VALUE_BYTES; + protected static final long PREV_VALUE_OFFSET = NEXT_VALUE_OFFSET + NEXT_VALUE_BYTES; + + protected static final long BUCKET_KEY_OFFSET = 0L; + protected static final long BUCKET_VALUE_OFFSET = BUCKET_KEY_OFFSET + KEY_BYTES; + protected static final long BUCKET_BYTES = BUCKET_VALUE_OFFSET + VALUE_BYTES + NEXT_VALUE_BYTES + PREV_VALUE_BYTES; + + protected static final long DEFAULT_TABLE_SIZE = 16L; + + protected static final int BUCKET_AXIS_BITS = 2; //the number of bits per axis which are used inside of the bucket rather than identifying the bucket + protected static final int BUCKET_AXIS_MASK = (1 << BUCKET_AXIS_BITS) - 1; + protected static final int BUCKET_SIZE = (BUCKET_AXIS_MASK << (BUCKET_AXIS_BITS * 2)) | (BUCKET_AXIS_MASK << BUCKET_AXIS_BITS) | BUCKET_AXIS_MASK; + + protected static long hashPosition(int x, int y, int z) { + return x * 1403638657883916319L //some random prime numbers + + y * 4408464607732138253L + + z * 2587306874955016303L; + } + + protected static long positionFlag(int x, int y, int z) { + return 1L << (((x & BUCKET_AXIS_MASK) << (BUCKET_AXIS_BITS * 2)) | ((y & BUCKET_AXIS_MASK) << BUCKET_AXIS_BITS) | (z & BUCKET_AXIS_MASK)); + } + + protected static long allocateTable(long tableSize) { + long size = tableSize * BUCKET_BYTES; + long addr = PlatformDependent.allocateMemory(size); //allocate + PlatformDependent.setMemory(addr, size, (byte) 0); //clear + return addr; + } + + protected long tableAddr = 0L; //the address of the table in memory + protected long tableSize = 0L; //the physical size of the table (in buckets). always a non-zero power of two + protected long resizeThreshold = 0L; + protected long usedBuckets = 0L; + + protected long size = 0L; //the number of values stored in the set + + protected boolean closed = false; + + protected long first = 0; + protected long last = 0; + + //Cached index of the first set bit in the first bucket + //This is mainly used in getFirstX(), getFirstY(), and getFirstX() + private int cachedIndex = -1; + + //Used in DynamicGraphMinFixedPoint transform constructor + public LinkedInt3HashSet(DynamicGraphMinFixedPoint $1, int $2, float $3, int $4) { + this(); + } + + public LinkedInt3HashSet() { + this.setTableSize(DEFAULT_TABLE_SIZE); + } + + public LinkedInt3HashSet(int initialCapacity) { + initialCapacity = (int) Math.ceil(initialCapacity * (1.0d / 0.75d)); //scale according to resize threshold + initialCapacity = 1 << (Integer.SIZE - Integer.numberOfLeadingZeros(initialCapacity - 1)); //round up to next power of two + this.setTableSize(Math.max(initialCapacity, DEFAULT_TABLE_SIZE)); + } + + /** + * Adds the given position to this set. + * + * @param x the position's X coordinate + * @param y the position's Y coordinate + * @param z the position's Z coordinate + * + * @return whether or not the position was added (i.e. was previously absent) + * + * @see java.util.Set#add(Object) + */ + public boolean add(int x, int y, int z) { + long flag = positionFlag(x, y, z); + long bucket = this.findBucket(x >> BUCKET_AXIS_BITS, y >> BUCKET_AXIS_BITS, z >> BUCKET_AXIS_BITS, true); + + long value = PlatformDependent.getLong(bucket + BUCKET_VALUE_OFFSET); + if ((value & flag) == 0L) { //flag wasn't previously set + PlatformDependent.putLong(bucket + BUCKET_VALUE_OFFSET, value | flag); + this.size++; //the position was newly added, so we need to increment the total size + return true; + } else { //flag was already set + return false; + } + } + + /** + * Checks whether or not the given position is present in this set. + * + * @param x the position's X coordinate + * @param y the position's Y coordinate + * @param z the position's Z coordinate + * + * @return whether or not the position is present + * + * @see java.util.Set#contains(Object) + */ + public boolean contains(int x, int y, int z) { + long flag = positionFlag(x, y, z); + long bucket = this.findBucket(x >> BUCKET_AXIS_BITS, y >> BUCKET_AXIS_BITS, z >> BUCKET_AXIS_BITS, false); + + return bucket != 0L //bucket exists + && (PlatformDependent.getLong(bucket + BUCKET_VALUE_OFFSET) & flag) != 0L; //flag is set + } + + protected long findBucket(int x, int y, int z, boolean createIfAbsent) { + long tableSize = this.tableSize; + long tableAddr = this.tableAddr; + if (tableAddr == 0L) { + if (createIfAbsent) { //the table hasn't been allocated yet - let's make a new one! + this.tableAddr = tableAddr = allocateTable(tableSize); + } else { //the table isn't even allocated yet, so the bucket clearly isn't present + return 0L; + } + } + + long mask = tableSize - 1L; //tableSize is always a power of two, so we can safely create a bitmask like this + long hash = hashPosition(x, y, z); + + for (long i = 0L; ; i++) { + long bucketAddr = tableAddr + ((hash + i) & mask) * BUCKET_BYTES; + + if (PlatformDependent.getLong(bucketAddr + BUCKET_VALUE_OFFSET) == 0L) { //if the bucket value is 0, it means the bucket hasn't been assigned yet + if (createIfAbsent) { + if (this.usedBuckets < this.resizeThreshold) { //let's assign the bucket to our current position + this.usedBuckets++; + PlatformDependent.putInt(bucketAddr + BUCKET_KEY_OFFSET + KEY_X_OFFSET, x); + PlatformDependent.putInt(bucketAddr + BUCKET_KEY_OFFSET + KEY_Y_OFFSET, y); + PlatformDependent.putInt(bucketAddr + BUCKET_KEY_OFFSET + KEY_Z_OFFSET, z); + + if (first == 0) { + first = bucketAddr; + } + + //If last is set, set the last value's pointer to point here and set this pointer to last + if (last != 0) { + PlatformDependent.putLong(last + NEXT_VALUE_OFFSET, bucketAddr); + PlatformDependent.putLong(bucketAddr + PREV_VALUE_OFFSET, last); + } + + last = bucketAddr; + + return bucketAddr; + } else { + //we've established that there's no matching bucket, but the table is full. let's resize it before allocating a bucket + // to avoid overfilling the table + this.resize(); + return this.findBucket(x, y, z, createIfAbsent); //tail recursion will probably be optimized away + } + } else { //empty bucket, abort search - there won't be anything else later on + return 0L; + } + } + + //the bucket is set. check coordinates to see if it matches the one we're searching for + if (PlatformDependent.getInt(bucketAddr + BUCKET_KEY_OFFSET + KEY_X_OFFSET) == x + && PlatformDependent.getInt(bucketAddr + BUCKET_KEY_OFFSET + KEY_Y_OFFSET) == y + && PlatformDependent.getInt(bucketAddr + BUCKET_KEY_OFFSET + KEY_Z_OFFSET) == z) { //we found the matching bucket! + return bucketAddr; + } + + //continue search... + } + } + + protected void resize() { + this.cachedIndex = -1; //Invalidate cached index + + long oldTableSize = this.tableSize; + long oldTableAddr = this.tableAddr; + + //allocate new table + long newTableSize = oldTableSize << 1L; + this.setTableSize(newTableSize); + long newTableAddr = this.tableAddr = allocateTable(newTableSize); + long newMask = newTableSize - 1L; + + //iterate through every bucket in the old table and copy it to the new one + long prevBucket = 0; + long bucket = first; + + while (bucket != 0) { + int x = PlatformDependent.getInt(bucket + BUCKET_KEY_OFFSET + KEY_X_OFFSET); + int y = PlatformDependent.getInt(bucket + BUCKET_KEY_OFFSET + KEY_Y_OFFSET); + int z = PlatformDependent.getInt(bucket + BUCKET_KEY_OFFSET + KEY_Z_OFFSET); + long value = PlatformDependent.getLong(bucket + BUCKET_VALUE_OFFSET); + + long newBucketAddr; + for (long hash = hashPosition(x, y, z), j = 0L; ; j++) { + newBucketAddr = newTableAddr + ((hash + j) & newMask) * BUCKET_BYTES; + + if (PlatformDependent.getLong(newBucketAddr + BUCKET_VALUE_OFFSET) == 0L) { //if the bucket value is 0, it means the bucket hasn't been assigned yet + PlatformDependent.putInt(newBucketAddr + BUCKET_KEY_OFFSET + KEY_X_OFFSET, x); + PlatformDependent.putInt(newBucketAddr + BUCKET_KEY_OFFSET + KEY_Y_OFFSET, y); + PlatformDependent.putInt(newBucketAddr + BUCKET_KEY_OFFSET + KEY_Z_OFFSET, z); + PlatformDependent.putLong(newBucketAddr + BUCKET_VALUE_OFFSET, value); + + PlatformDependent.putLong(newBucketAddr + PREV_VALUE_OFFSET, prevBucket); + + if (prevBucket == 0) { + this.first = newBucketAddr; + } else { + PlatformDependent.putLong(prevBucket + NEXT_VALUE_OFFSET, newBucketAddr); + } + this.last = newBucketAddr; + + break; + } + } + + prevBucket = newBucketAddr; + bucket = PlatformDependent.getLong(bucket + NEXT_VALUE_OFFSET); + } + + //delete old table + PlatformDependent.freeMemory(oldTableAddr); + } + + /** + * Runs the given function on every position in this set. + * + * @param action the function to run + * + * @see java.util.Set#forEach(java.util.function.Consumer) + */ + public void forEach(XYZConsumer action) { + long tableAddr = this.tableAddr; + if (tableAddr == 0L) { //the table isn't even allocated yet, there's nothing to iterate through... + return; + } + + long bucket = first; + + while (bucket != 0) { + int bucketX = PlatformDependent.getInt(bucket + BUCKET_KEY_OFFSET + KEY_X_OFFSET); + int bucketY = PlatformDependent.getInt(bucket + BUCKET_KEY_OFFSET + KEY_Y_OFFSET); + int bucketZ = PlatformDependent.getInt(bucket + BUCKET_KEY_OFFSET + KEY_Z_OFFSET); + long value = PlatformDependent.getLong(bucket + BUCKET_VALUE_OFFSET); + + for (int i = 0; i <= BUCKET_SIZE; i++) { //check each flag in the bucket value to see if it's set + if ((value & (1L << i)) == 0L) { //the flag isn't set + continue; + } + + int dx = i >> (BUCKET_AXIS_BITS * 2); + int dy = (i >> BUCKET_AXIS_BITS) & BUCKET_AXIS_MASK; + int dz = i & BUCKET_AXIS_MASK; + action.accept((bucketX << BUCKET_AXIS_BITS) + dx, (bucketY << BUCKET_AXIS_BITS) + dy, (bucketZ << BUCKET_AXIS_BITS) + dz); + } + + bucket = PlatformDependent.getLong(bucket + NEXT_VALUE_OFFSET); + } + } + + /** + * Removes the given position from this set. + * + * @param x the position's X coordinate + * @param y the position's Y coordinate + * @param z the position's Z coordinate + * + * @return whether or not the position was removed (i.e. was previously present) + * + * @see java.util.Set#remove(Object) + */ + public boolean remove(int x, int y, int z) { + long tableAddr = this.tableAddr; + if (tableAddr == 0L) { //the table isn't even allocated yet, there's nothing to remove... + return false; + } + + cachedIndex = -1; + + long mask = this.tableSize - 1L; //tableSize is always a power of two, so we can safely create a bitmask like this + + long flag = positionFlag(x, y, z); + int searchBucketX = x >> BUCKET_AXIS_BITS; + int searchBucketY = y >> BUCKET_AXIS_BITS; + int searchBucketZ = z >> BUCKET_AXIS_BITS; + long hash = hashPosition(searchBucketX, searchBucketY, searchBucketZ); + + for (long i = 0L; ; i++) { + long bucketAddr = tableAddr + ((hash + i) & mask) * BUCKET_BYTES; + + //read the bucket into registers + int bucketX = PlatformDependent.getInt(bucketAddr + BUCKET_KEY_OFFSET + KEY_X_OFFSET); + int bucketY = PlatformDependent.getInt(bucketAddr + BUCKET_KEY_OFFSET + KEY_Y_OFFSET); + int bucketZ = PlatformDependent.getInt(bucketAddr + BUCKET_KEY_OFFSET + KEY_Z_OFFSET); + long value = PlatformDependent.getLong(bucketAddr + BUCKET_VALUE_OFFSET); + if (value == 0L) { //the bucket is unset. we've reached the end of the bucket chain for this hash, which means + return false; + } else if (bucketX != searchBucketX || bucketY != searchBucketY || bucketZ != searchBucketZ) { //the bucket doesn't match, so the search must go on + continue; + } else if ((value & flag) == 0L) { //we've found a matching bucket, but the position's flag is unset. there's nothing for us to do... + return false; + } + + //the bucket that we found contains the position, so now we remove it from the set + this.size--; + + if ((value & ~flag) == 0L) { //this position is the only position in the bucket, so we need to delete the bucket + removeBucket(bucketAddr); + + //shifting the buckets IS expensive, yes, but it'll only happen when the entire bucket is deleted, which won't happen on every removal + this.shiftBuckets(tableAddr, (hash + i) & mask, mask); + } else { //update bucket value with this position removed + PlatformDependent.putLong(bucketAddr + BUCKET_VALUE_OFFSET, value & ~flag); + } + + return true; + } + } + + protected void removeBucket(long bucketAddr) { + this.usedBuckets--; + + patchRemoval(bucketAddr); + if (bucketAddr == this.first) { + long newFirst = PlatformDependent.getLong(bucketAddr + NEXT_VALUE_OFFSET); + if (newFirst != 0) { + PlatformDependent.putLong(newFirst + PREV_VALUE_OFFSET, 0); + } + this.first = newFirst; + } + + if (bucketAddr == this.last) { + long newLast = PlatformDependent.getLong(bucketAddr + PREV_VALUE_OFFSET); + if (newLast != 0) { + PlatformDependent.putLong(newLast + NEXT_VALUE_OFFSET, 0); + } + this.last = newLast; + } + } + + //adapted from it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap#shiftKeys(int) + protected void shiftBuckets(long tableAddr, long pos, long mask) { + long last; + long slot; + + int currX; + int currY; + int currZ; + long currValue; + long currPrev; + long currNext; + long currAddr; + + for (; ; ) { + pos = ((last = pos) + 1L) & mask; + for (; ; pos = (pos + 1L) & mask) { + currAddr = tableAddr + pos * BUCKET_BYTES; + if ((currValue = PlatformDependent.getLong(currAddr + BUCKET_VALUE_OFFSET)) == 0L) { //curr points to an unset bucket + long ptr = tableAddr + last * BUCKET_BYTES; + PlatformDependent.setMemory(ptr, BUCKET_BYTES, (byte) 0); //delete last bucket + return; + } + + slot = hashPosition( + currX = PlatformDependent.getInt(currAddr + BUCKET_KEY_OFFSET + KEY_X_OFFSET), + currY = PlatformDependent.getInt(currAddr + BUCKET_KEY_OFFSET + KEY_Y_OFFSET), + currZ = PlatformDependent.getInt(currAddr + BUCKET_KEY_OFFSET + KEY_Z_OFFSET)) & mask; + + if (last <= pos ? last >= slot || slot > pos : last >= slot && slot > pos) { + currPrev = PlatformDependent.getLong(currAddr + PREV_VALUE_OFFSET); + currNext = PlatformDependent.getLong(currAddr + NEXT_VALUE_OFFSET); + + break; + } + } + + long lastAddr = tableAddr + last * BUCKET_BYTES; + + patchMove(currAddr, lastAddr); + + PlatformDependent.putInt(lastAddr + BUCKET_KEY_OFFSET + KEY_X_OFFSET, currX); + PlatformDependent.putInt(lastAddr + BUCKET_KEY_OFFSET + KEY_Y_OFFSET, currY); + PlatformDependent.putInt(lastAddr + BUCKET_KEY_OFFSET + KEY_Z_OFFSET, currZ); + PlatformDependent.putLong(lastAddr + BUCKET_VALUE_OFFSET, currValue); + PlatformDependent.putLong(lastAddr + NEXT_VALUE_OFFSET, currNext); + PlatformDependent.putLong(lastAddr + PREV_VALUE_OFFSET, currPrev); + } + } + + private void patchMove(long currPtr, long newPtr) { + long ptrPrev = PlatformDependent.getLong(currPtr + PREV_VALUE_OFFSET); + long ptrNext = PlatformDependent.getLong(currPtr + NEXT_VALUE_OFFSET); + + if (ptrPrev != 0) { + PlatformDependent.putLong(ptrPrev + NEXT_VALUE_OFFSET, newPtr); + } + + if (ptrNext != 0) { + PlatformDependent.putLong(ptrNext + PREV_VALUE_OFFSET, newPtr); + } + + if (currPtr == this.first) { + first = newPtr; + } + + if (currPtr == this.last) { + this.last = newPtr; + } + } + + public void patchRemoval(long ptr) { + long ptrPrev = PlatformDependent.getLong(ptr + PREV_VALUE_OFFSET); + long ptrNext = PlatformDependent.getLong(ptr + NEXT_VALUE_OFFSET); + + if (ptrPrev != 0) { + PlatformDependent.putLong(ptrPrev + NEXT_VALUE_OFFSET, ptrNext); + } + + if (ptrNext != 0) { + PlatformDependent.putLong(ptrNext + PREV_VALUE_OFFSET, ptrPrev); + } + } + + /** + * Removes every position from this set. + * + * @see java.util.Set#clear() + */ + public void clear() { + if (this.isEmpty()) { //if the set is empty, there's nothing to clear + return; + } + + //fill the entire table with zeroes + // (since the table isn't empty, we can be sure that the table has been allocated so there's no reason to check for it) + PlatformDependent.setMemory(this.tableAddr, this.tableSize * BUCKET_BYTES, (byte) 0); + + //reset all size counters + this.usedBuckets = 0L; + this.size = 0L; + + cachedIndex = -1; + + this.first = 0L; + this.last = 0L; + } + + //Called from ASM + public int getFirstX() { + if (size == 0) { + throw new NoSuchElementException(); + } + + if (cachedIndex == -1) { + getFirstSetBitInFirstBucket(); + } + + int x = PlatformDependent.getInt(first + KEY_X_OFFSET); + + int dx = cachedIndex >> (BUCKET_AXIS_BITS * 2); + return (x << BUCKET_AXIS_BITS) + dx; + } + + //Called from ASM + public int getFirstY() { + if (size == 0) { + throw new NoSuchElementException(); + } + + if (cachedIndex == -1) { + getFirstSetBitInFirstBucket(); + } + + int y = PlatformDependent.getInt(first + KEY_Y_OFFSET); + + int dy = (cachedIndex >> BUCKET_AXIS_BITS) & BUCKET_AXIS_MASK; + return (y << BUCKET_AXIS_BITS) + dy; + } + + //Called from ASM + public int getFirstZ() { + if (size == 0) { + throw new NoSuchElementException(); + } + + if (cachedIndex == -1) { + getFirstSetBitInFirstBucket(); + } + + int z = PlatformDependent.getInt(first + KEY_Z_OFFSET); + + int dz = cachedIndex & BUCKET_AXIS_MASK; + return (z << BUCKET_AXIS_BITS) + dz; + } + + //Called from ASM + public void removeFirstValue() { + if (size == 0) { + throw new NoSuchElementException(); + } + + if (cachedIndex == -1) { + getFirstSetBitInFirstBucket(); + } + + long value = PlatformDependent.getLong(first + BUCKET_VALUE_OFFSET); + + value ^= 1L << cachedIndex; + + this.size--; + + if (value == 0) { + long pos = (this.first - tableAddr) / BUCKET_BYTES; + removeBucket(this.first); + this.shiftBuckets(tableAddr, pos, tableSize - 1L); + + cachedIndex = -1; + } else { + PlatformDependent.putLong(first + BUCKET_VALUE_OFFSET, value); + getFirstSetBitInFirstBucket(); + } + } + + protected void getFirstSetBitInFirstBucket() { + long value = PlatformDependent.getLong(first + BUCKET_VALUE_OFFSET); + + cachedIndex = Long.numberOfTrailingZeros(value); + } + + protected void setTableSize(long tableSize) { + this.tableSize = tableSize; + this.resizeThreshold = (tableSize >> 1L) + (tableSize >> 2L); //count * 0.75 + } + + /** + * @return the number of values stored in this set + */ + public long size() { + return this.size; + } + + /** + * @return whether or not this set is empty (contains no values) + */ + public boolean isEmpty() { + return this.size == 0L; + } + + /** + * Irrevocably releases the resources claimed by this instance. + *

+ * Once this method has been calls, all methods in this class will produce undefined behavior. + */ + @Override + public void close() { + if (this.closed) { + return; + } + this.closed = true; + + //actually release memory + if (this.tableAddr != 0L) { + PlatformDependent.freeMemory(this.tableAddr); + } + } + + @Override + protected void finalize() throws Throwable { + //using a finalizer is bad, i know. however, there's no other reasonable way for me to clean up the memory without pulling in PorkLib:unsafe or + // using sun.misc.Cleaner directly... + this.close(); + } + + //These methods probably won't be used by any CC code but should help ensure some compatibility if other mods access the light engine + + public boolean add(long l) { + return add(BlockPos.getX(l), BlockPos.getY(l), BlockPos.getZ(l)); + } + + public boolean contains(long l) { + return contains(BlockPos.getX(l), BlockPos.getY(l), BlockPos.getZ(l)); + } + + public boolean remove(long l) { + return remove(BlockPos.getX(l), BlockPos.getY(l), BlockPos.getZ(l)); + } + + public long removeFirstLong() { + int x = getFirstX(); + int y = getFirstY(); + int z = getFirstZ(); + + removeFirstValue(); + return BlockPos.asLong(x, y, z); + } + + //Should only be used during tests + + public XYZTriple[] toArray() { + XYZTriple[] arr = new XYZTriple[(int) size]; + + MutableInt i = new MutableInt(0); + forEach((x, y, z) -> { + arr[i.getAndIncrement()] = new XYZTriple(x, y, z); + }); + + if (i.getValue() != size) { + throw new IllegalStateException("Size mismatch"); + } + + return arr; + } + + public static record XYZTriple(int x, int y, int z) { + } +} diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/utils/TestMappingUtils.java b/src/main/java/io/github/opencubicchunks/cubicchunks/utils/TestMappingUtils.java new file mode 100644 index 000000000..62a4304b6 --- /dev/null +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/utils/TestMappingUtils.java @@ -0,0 +1,75 @@ +package io.github.opencubicchunks.cubicchunks.utils; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.nio.file.Path; +import java.util.function.Supplier; + +import net.fabricmc.loader.api.FabricLoader; +import net.fabricmc.loader.api.MappingResolver; +import net.fabricmc.loader.impl.launch.MappingConfiguration; + +public class TestMappingUtils { + private static MappingResolver mappingResolver; + + /** + * This method is useful for when running unit tests + * + * @return A mapping resolver. If fabric is not running/properly initialized it will create a mapping resolver. ("intermediary" -> "named") + */ + public static MappingResolver getMappingResolver() { + if (mappingResolver != null) { + return mappingResolver; + } + + mappingResolver = makeResolver(); + return mappingResolver; + } + + /** + * This method is useful for when running unit tests + * + * @return Whether fabric is running in a development environment. If fabric is not running/properly initialized it will return true. + */ + public static boolean isDev() { + try { + return FabricLoader.getInstance().isDevelopmentEnvironment(); + } catch (NullPointerException e) { + return true; + } + } + + public static Path getGameDir() { + Path dir = null; + Path def = Path.of("run").toAbsolutePath(); + try { + dir = FabricLoader.getInstance().getGameDir(); + } catch (IllegalStateException e) { + System.err.println("Fabric not initialized, using assumed game dir: " + def); + } + + if (dir == null) { + dir = def; + } + + return dir; + } + + private static MappingResolver makeResolver() { + try { + return FabricLoader.getInstance().getMappingResolver(); + } catch (NullPointerException e) { + System.err.println("Fabric is not running properly. Creating a mapping resolver."); + //FabricMappingResolver's constructor is package-private so we call it with reflection + try { + MappingConfiguration config = new MappingConfiguration(); + Class mappingResolverClass = Class.forName("net.fabricmc.loader.impl.MappingResolverImpl"); + Constructor constructor = mappingResolverClass.getDeclaredConstructor(Supplier.class, String.class); + constructor.setAccessible(true); + return (MappingResolver) constructor.newInstance((Supplier) config::getMappings, "named"); + } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e1) { + throw new RuntimeException(e1); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/utils/XYZConsumer.java b/src/main/java/io/github/opencubicchunks/cubicchunks/utils/XYZConsumer.java new file mode 100644 index 000000000..2ae0de5bb --- /dev/null +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/utils/XYZConsumer.java @@ -0,0 +1,6 @@ +package io.github.opencubicchunks.cubicchunks.utils; + +@FunctionalInterface +public interface XYZConsumer { + void accept(int x, int y, int z); +} diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/utils/XYZPredicate.java b/src/main/java/io/github/opencubicchunks/cubicchunks/utils/XYZPredicate.java new file mode 100644 index 000000000..28221e945 --- /dev/null +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/utils/XYZPredicate.java @@ -0,0 +1,6 @@ +package io.github.opencubicchunks.cubicchunks.utils; + +@FunctionalInterface +public interface XYZPredicate { + boolean test(int x, int y, int z); +} diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/world/storage/CubeSerializer.java b/src/main/java/io/github/opencubicchunks/cubicchunks/world/storage/CubeSerializer.java index 58ba52968..92988a232 100644 --- a/src/main/java/io/github/opencubicchunks/cubicchunks/world/storage/CubeSerializer.java +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/world/storage/CubeSerializer.java @@ -325,6 +325,11 @@ private static void logErrors(CubePos cubePos, int i, String string) { LOGGER.error("Recoverable errors when loading section [" + cubePos.getX() + ", " + cubePos.getY() + ", " + cubePos.getZ() + ", " + i + "]: " + string); } + // used from ASM + public static CompoundTag write(ServerLevel serverLevel, CubeAccess cube) { + return write(serverLevel, cube, null); + } + public static CompoundTag write(ServerLevel serverLevel, CubeAccess cube, AsyncSaveData data) { CubePos pos = cube.getCubePos(); diff --git a/src/main/resources/cubicchunks.mixins.core.json b/src/main/resources/cubicchunks.mixins.core.json index c0898c278..cd0d57dab 100644 --- a/src/main/resources/cubicchunks.mixins.core.json +++ b/src/main/resources/cubicchunks.mixins.core.json @@ -58,7 +58,8 @@ "common.network.MixinFriendlyByteBuf", "common.progress.MixinLoggerChunkProgressListener", "common.server.MixinPlayerList", - "common.ticket.MixinDistanceManager" + "common.ticket.MixinDistanceManager", + "common.MixinCrashReport" ], "client": [ "client.chunk.MixinLevelChunk", diff --git a/src/main/resources/type-transform.json b/src/main/resources/type-transform.json new file mode 100644 index 000000000..eb603da56 --- /dev/null +++ b/src/main/resources/type-transform.json @@ -0,0 +1,862 @@ +{ + "types": [ + { + "id": "blockpos", + "original": "J", + "transformed": [ + "I", + "I", + "I" + ], + "from_original": [ + { + "owner": "net/minecraft/core/BlockPos", + "name": "getX", + "desc": "(J)I", + "call_type": "static" + }, + { + "owner": "net/minecraft/core/BlockPos", + "name": "getY", + "desc": "(J)I", + "call_type": "static" + }, + { + "owner": "net/minecraft/core/BlockPos", + "name": "getZ", + "desc": "(J)I", + "call_type": "static" + } + ], + "to_original": { + "owner": "net/minecraft/core/BlockPos", + "name": "asLong", + "desc": "(III)J", + "call_type": "static" + }, + "constant_replacements": [ + { + "from": 9223372036854775807, + "to": [ + 2147483647, + 2147483647, + 2147483647 + ] + }, + { + "from": 0, + "to": [ + 0, + 0, + 0 + ] + } + ], + "original_predicate": "java/util/function/LongPredicate", + "transformed_predicate": "io/github/opencubicchunks/cubicchunks/utils/XYZPredicate", + "original_consumer": "java/util/function/LongConsumer", + "transformed_consumer": "io/github/opencubicchunks/cubicchunks/utils/XYZConsumer", + "postfix": [ + "_x", + "_y", + "_z" + ] + }, + { + "id": "blockpos_set", + "original": "Lit/unimi/dsi/fastutil/longs/LongSet;", + "transformed": [ + "Lio/github/opencubicchunks/cubicchunks/utils/LinkedInt3HashSet;" + ] + }, + { + "id": "blockpos_byte_map", + "original": "Lit/unimi/dsi/fastutil/longs/Long2ByteMap;", + "transformed": [ + "Lio/github/opencubicchunks/cubicchunks/utils/Int3UByteLinkedHashMap;" + ] + }, + { + "id": "blockpos_list", + "original": "Lit/unimi/dsi/fastutil/longs/LongList;", + "transformed": [ + "Lio/github/opencubicchunks/cubicchunks/utils/Int3List;" + ], + "postfix": [ + "_blockpos" + ] + } + ], + "methods": [ + { + "method": "v net/minecraft/core/BlockPos#asLong ()J", + "possibilities": [ + { + "parameters": [ + null + ], + "return": "blockpos", + "replacement": [ + [ + { + "type": "INVOKEVIRTUAL", + "method": { + "owner": "net/minecraft/core/Vec3i", + "name": "getX", + "desc": "()I", + "call_type": "virtual" + } + } + ], + [ + { + "type": "INVOKEVIRTUAL", + "method": { + "owner": "net/minecraft/core/Vec3i", + "name": "getY", + "desc": "()I", + "call_type": "virtual" + } + } + ], + [ + { + "type": "INVOKEVIRTUAL", + "method": { + "owner": "net/minecraft/core/Vec3i", + "name": "getZ", + "desc": "()I", + "call_type": "virtual" + } + } + ] + ] + } + ] + }, + { + "method": "s net/minecraft/core/BlockPos#offset (JLnet/minecraft/core/Direction;)J", + "possibilities": [ + { + "parameters": [ + "blockpos", + null + ], + "return": "blockpos", + "replacement": [ + [ + { + "type": "INVOKEVIRTUAL", + "method": { + "owner": "net/minecraft/core/Direction", + "name": "getStepX", + "desc": "()I", + "call_type": "virtual" + } + }, + "IADD" + ], + [ + { + "type": "INVOKEVIRTUAL", + "method": { + "owner": "net/minecraft/core/Direction", + "name": "getStepY", + "desc": "()I", + "call_type": "virtual" + } + }, + "IADD" + ], + [ + { + "type": "INVOKEVIRTUAL", + "method": { + "owner": "net/minecraft/core/Direction", + "name": "getStepZ", + "desc": "()I", + "call_type": "virtual" + } + }, + "IADD" + ] + ] + } + ] + }, + { + "method": "i it/unimi/dsi/fastutil/longs/LongSet#remove (J)Z", + "possibilities": [ + { + "parameters": [ + "blockpos_set", + "blockpos" + ], + "minimums": [ + { + "parameters": [ + "blockpos_set", + null + ] + }, + { + "parameters": [ + null, + "blockpos" + ] + } + ] + } + ] + }, + { + "method": "i it/unimi/dsi/fastutil/longs/Long2ByteFunction#remove (J)B", + "possibilities": [ + { + "parameters": [ + "blockpos_byte_map", + "blockpos" + ], + "minimums": [ + { + "parameters": [ + "blockpos_byte_map", + null + ] + }, + { + "parameters": [ + null, + "blockpos" + ] + } + ], + "replacement": [ + [ + { + "type": "INVOKEVIRTUAL", + "method": "v io/github/opencubicchunks/cubicchunks/utils/Int3UByteLinkedHashMap#remove (III)I" + }, + "I2B" + ] + ] + } + ] + }, + { + "method": "i it/unimi/dsi/fastutil/longs/Long2ByteMap#keySet ()Lit/unimi/dsi/fastutil/longs/LongSet;", + "possibilities": [ + { + "parameters": [ + "blockpos_byte_map" + ], + "return": "blockpos_set", + "replacement": [ + [ + { + "type": "INVOKEVIRTUAL", + "method": "v io/github/opencubicchunks/cubicchunks/utils/Int3UByteLinkedHashMap#keySet ()Lio/github/opencubicchunks/cubicchunks/utils/LinkedInt3HashSet;" + } + ] + ], + "minimums": [ + { + "parameters": [ + "blockpos_byte_map" + ] + } + ] + } + ] + }, + { + "method": "i it/unimi/dsi/fastutil/longs/LongSet#forEach (Ljava/util/function/LongConsumer;)V", + "possibilities": [ + { + "parameters": [ + "blockpos_set", + "blockpos consumer" + ], + "minimums": [ + { + "parameters": [ + "blockpos_set", + null + ] + }, + { + "parameters": [ + null, + "blockpos consumer" + ] + } + ] + } + ] + }, + { + "method": "i it/unimi/dsi/fastutil/longs/LongSet#forEach (Lit/unimi/dsi/fastutil/longs/LongConsumer;)V", + "possibilities": [ + { + "parameters": [ + "blockpos_set", + "blockpos consumer" + ], + "minimums": [ + { + "parameters": [ + "blockpos_set", + null + ] + }, + { + "parameters": [ + null, + "blockpos consumer" + ] + } + ] + } + ] + }, + { + "method": "i it/unimi/dsi/fastutil/longs/LongList#forEach (Ljava/util/function/LongConsumer;)V", + "possibilities": [ + { + "parameters": [ + "blockpos_set", + "blockpos consumer" + ], + "minimums": [ + { + "parameters": [ + "blockpos_set", + null + ] + }, + { + "parameters": [ + null, + "blockpos consumer" + ] + } + ] + } + ] + }, + { + "method": "i it/unimi/dsi/fastutil/longs/LongList#forEach (Lit/unimi/dsi/fastutil/longs/LongConsumer;)V", + "possibilities": [ + { + "parameters": [ + "blockpos_set", + "blockpos consumer" + ], + "minimums": [ + { + "parameters": [ + "blockpos_set", + null + ] + }, + { + "parameters": [ + null, + "blockpos consumer" + ] + } + ] + } + ] + }, + { + "method": "i it/unimi/dsi/fastutil/longs/Long2ByteMap#get (J)B", + "possibilities": [ + { + "parameters": [ + "blockpos_byte_map", + "blockpos" + ], + "minimums": [ + { + "parameters": [ + "blockpos_byte_map", + null + ] + }, + { + "parameters": [ + null, + "blockpos" + ] + } + ], + "replacement": [ + [ + { + "type": "INVOKEVIRTUAL", + "method": "v io/github/opencubicchunks/cubicchunks/utils/Int3UByteLinkedHashMap#get (III)I" + }, + "I2B" + ] + ] + } + ] + }, + { + "method": "i it/unimi/dsi/fastutil/longs/Long2ByteMap#put (JB)B", + "possibilities": [ + { + "parameters": [ + "blockpos_byte_map", + "blockpos", + null + ], + "minimums": [ + { + "parameters": [ + "blockpos_byte_map", + null + ] + }, + { + "parameters": [ + null, + "blockpos" + ] + } + ], + "replacement": [ + [ + { + "type": "INVOKEVIRTUAL", + "method": "v io/github/opencubicchunks/cubicchunks/utils/Int3UByteLinkedHashMap#put (IIII)I" + }, + "I2B" + ] + ] + } + ] + }, + { + "method": "i it/unimi/dsi/fastutil/longs/Long2ByteMap#putIfAbsent (JB)B", + "possibilities": [ + { + "parameters": [ + "blockpos_byte_map", + "blockpos", + null + ], + "minimums": [ + { + "parameters": [ + "blockpos_byte_map", + null + ] + }, + { + "parameters": [ + null, + "blockpos" + ] + } + ], + "replacement": [ + [ + { + "type": "INVOKEVIRTUAL", + "method": "v io/github/opencubicchunks/cubicchunks/utils/Int3UByteLinkedHashMap#putIfAbsent (IIII)I" + }, + "I2B" + ] + ] + } + ] + }, + { + "method": "v it/unimi/dsi/fastutil/longs/LongLinkedOpenHashSet#removeFirstLong ()J", + "possibilities": [ + { + "parameters": [ + "blockpos_set" + ], + "return": "blockpos", + "minimums": [ + { + "parameters": [ + "blockpos_set" + ] + }, + { + "parameters": [ + null + ], + "return": "blockpos" + } + ], + "replacement": [ + [ + { + "type": "INVOKEVIRTUAL", + "method": "v io/github/opencubicchunks/cubicchunks/utils/LinkedInt3HashSet#getFirstX ()I" + } + ], + [ + { + "type": "INVOKEVIRTUAL", + "method": "v io/github/opencubicchunks/cubicchunks/utils/LinkedInt3HashSet#getFirstY ()I" + } + ], + [ + { + "type": "INVOKEVIRTUAL", + "method": "v io/github/opencubicchunks/cubicchunks/utils/LinkedInt3HashSet#getFirstZ ()I" + } + ] + ], + "finalizer": [ + { + "type": "INVOKEVIRTUAL", + "method": "v io/github/opencubicchunks/cubicchunks/utils/LinkedInt3HashSet#removeFirstValue ()V" + } + ] + } + ] + }, + { + "method": "i it/unimi/dsi/fastutil/longs/LongList#add (J)Z", + "possibilities": [ + { + "parameters": [ + "blockpos_list", + "blockpos" + ], + "minimums": [ + { + "parameters": [ + "blockpos_list", + null + ] + }, + { + "parameters": [ + null, + "blockpos" + ] + } + ] + } + ] + }, + { + "method": "v net/minecraft/core/BlockPos$MutableBlockPos#set (J)Lnet/minecraft/core/BlockPos$MutableBlockPos;", + "possibilities": [ + { + "parameters": [ + null, + "blockpos" + ] + } + ] + }, + { + "method": "s net/minecraft/core/BlockPos#getFlatIndex (J)J", + "possibilities": [ + { + "parameters": [ + "blockpos" + ], + "return": "blockpos", + "replacement": [ + [], + [ + { + "type": "LDC", + "constant_type": "int", + "value": -16 + }, + "IAND" + ], + [] + ] + } + ] + }, + { + "method": "s net/minecraft/core/BlockPos#offset (JIII)J", + "possibilities": [ + { + "parameters": [ + "blockpos", + null, + null, + null + ], + "return": "blockpos", + "replacement": { + "expansion": [ + [ + "IADD" + ], + [ + "IADD" + ], + [ + "IADD" + ] + ], + "indices": [ + [ + 0, + 0, + [], + [] + ], + [ + 1, + [], + 0, + [] + ], + [ + 2, + [], + [], + 0 + ] + ] + } + } + ] + }, + { + "method": "s net/minecraft/core/BlockPos#of (J)Lnet/minecraft/core/BlockPos;", + "possibilities": [ + { + "parameters": [ + "blockpos" + ], + "replacement": [ + [ + "ISTORE {z}", + "ISTORE {y}", + "ISTORE {x}", + { + "type": "NEW", + "class": "net/minecraft/core/BlockPos" + }, + "DUP", + "ILOAD {x}", + "ILOAD {y}", + "ILOAD {z}", + { + "type": "INVOKESPECIAL", + "method": "S net/minecraft/core/BlockPos#\u003cinit\u003e (III)V" + } + ] + ] + } + ] + }, + { + "method": "s net/minecraft/core/SectionPos#blockToSection (J)J", + "possibilities": [ + { + "parameters": [ + "blockpos" + ] + } + ] + }, + { + "method": "v net/minecraft/world/level/lighting/DynamicGraphMinFixedPoint#checkEdge (JJIZ)V", + "possibilities": [ + { + "parameters": [ + null, + "blockpos", + "blockpos", + null, + null + ], + "minimums": [ + { + "parameters": [ + null, + "blockpos", + null, + null, + null + ] + }, + { + "parameters": [ + null, + null, + "blockpos", + null, + null + ] + } + ] + } + ] + } + ], + "classes": [ + { + "class": "net/minecraft/world/level/lighting/DynamicGraphMinFixedPoint", + "type_hints": [ + { + "method": { + "owner": "net/minecraft/world/level/lighting/DynamicGraphMinFixedPoint", + "name": "removeFromQueue", + "desc": "(J)V", + "call_type": "virtual" + }, + "types": [ + null, + "blockpos" + ] + } + ], + "constructor_replacers": [ + { + "original": "III", + "type_replacements": { + "it/unimi/dsi/fastutil/longs/LongLinkedOpenHashSet": "io/github/opencubicchunks/cubicchunks/utils/LinkedInt3HashSet", + "it/unimi/dsi/fastutil/longs/Long2ByteOpenHashMap": "io/github/opencubicchunks/cubicchunks/utils/Int3UByteLinkedHashMap", + "net/minecraft/world/level/lighting/DynamicGraphMinFixedPoint$1": "io/github/opencubicchunks/cubicchunks/utils/LinkedInt3HashSet", + "net/minecraft/world/level/lighting/DynamicGraphMinFixedPoint$2": "io/github/opencubicchunks/cubicchunks/utils/Int3UByteLinkedHashMap", + "it/unimi/dsi/fastutil/longs/Long2ByteMap": "io/github/opencubicchunks/cubicchunks/utils/Int3UByteLinkedHashMap" + } + } + ] + }, + { + "class": "net/minecraft/world/level/lighting/LayerLightEngine", + "type_hints": [ + { + "method": "v net/minecraft/world/level/lighting/DynamicGraphMinFixedPoint#isSource (J)Z", + "types": [ + null, + "blockpos" + ] + }, + { + "mapped": "getComputedLevel", + "method": "v net/minecraft/world/level/lighting/DynamicGraphMinFixedPoint#getComputedLevel (JJI)I", + "types": [ + null, + "blockpos", + "blockpos" + ] + }, + { + "mapped": "getLevel", + "method": "v net/minecraft/world/level/lighting/DynamicGraphMinFixedPoint#getLevel (J)I", + "types": [ + null, + "blockpos" + ] + }, + { + "mapped": "setLevel", + "method": "v net/minecraft/world/level/lighting/DynamicGraphMinFixedPoint#setLevel (JI)V", + "types": [ + null, + "blockpos" + ] + }, + { + "mapped": "computeLevelFromNeighbor", + "method": "v net/minecraft/world/level/lighting/DynamicGraphMinFixedPoint#computeLevelFromNeighbor (JJI)I", + "types": [ + null, + "blockpos", + "blockpos" + ] + }, + { + "mapped": "getDebugData", + "method": "v net/minecraft/world/level/lighting/LayerLightEngine#getDebugData (J)Ljava/lang/String;", + "types": [ + null, + "blockpos" + ] + } + ] + }, + { + "class": "net/minecraft/world/level/lighting/LayerLightSectionStorage", + "type_hints": [ + { + "method": "v net/minecraft/world/level/lighting/LayerLightSectionStorage#getLightValue (J)I", + "mappedName": "getLightValue", + "types": [ + null, + "blockpos" + ] + } + ] + }, + { + "class": "net/minecraft/world/level/levelgen/Aquifer$NoiseBasedAquifer", + "in_place": true + } + ], + "invokers": [ + { + "name": "io/github/opencubicchunks/cubicchunks/mixin/access/common/DynamicGraphMinFixedPointAccess", + "target": "net/minecraft/world/level/lighting/DynamicGraphMinFixedPoint", + "methods": [ + { + "name": "invokeCheckEdge (JJIZ)V", + "calls": "checkEdge", + "types": [ + "blockpos", + "blockpos" + ] + }, + { + "name": "invokeComputeLevelFromNeighbor (JJI)I", + "calls": "computeLevelFromNeighbor", + "types": [ + "blockpos", + "blockpos" + ] + }, + { + "name": "invokeGetLevel (J)I", + "calls": "getLevel", + "types": [ + "blockpos" + ] + } + ] + } + ], + + "suffixed_methods": [ + "net/minecraft/world/level/lighting/DynamicGraphMinFixedPoint", + "net/minecraft/world/level/lighting/LayerLightEngine", + "net/minecraft/world/level/lighting/LayerLightSectionStorage", + "net/minecraft/world/level/lighting/BlockLightEngine", + "net/minecraft/world/level/lighting/SkyLightEngine", + "net/minecraft/world/level/lighting/BlockLightSectionStorage", + "net/minecraft/world/level/lighting/SkyLightSectionStorage" + ], + + "type_meta_info": { + "inspect": [ + "net/minecraft/world/level/lighting/DynamicGraphMinFixedPoint", + "net/minecraft/world/level/lighting/LayerLightEngine", + "net/minecraft/world/level/lighting/LayerLightSectionStorage", + "net/minecraft/world/level/lighting/BlockLightEngine", + "net/minecraft/world/level/lighting/SkyLightEngine", + "net/minecraft/world/level/lighting/BlockLightSectionStorage", + "net/minecraft/world/level/lighting/SkyLightSectionStorage", + "net/minecraft/world/level/levelgen/Aquifer$NoiseBasedAquifer" + ] + } +} \ No newline at end of file diff --git a/src/test/java/io/github/opencubicchunks/cubicchunks/typetransformer/ConfigTest.java b/src/test/java/io/github/opencubicchunks/cubicchunks/typetransformer/ConfigTest.java new file mode 100644 index 000000000..8ca5f1e64 --- /dev/null +++ b/src/test/java/io/github/opencubicchunks/cubicchunks/typetransformer/ConfigTest.java @@ -0,0 +1,182 @@ +package io.github.opencubicchunks.cubicchunks.typetransformer; + +import java.util.ArrayList; +import java.util.List; + +import io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.bytecodegen.BytecodeFactory; +import io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.VariableAllocator; +import io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.analysis.DerivedTransformType; +import io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.config.Config; +import io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.config.MethodParameterInfo; +import io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.config.MethodReplacement; +import io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.config.TypeInfo; +import org.junit.jupiter.api.Test; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.InsnList; +import org.objectweb.asm.tree.InsnNode; +import org.objectweb.asm.tree.MethodNode; +import org.objectweb.asm.tree.VarInsnNode; +import org.objectweb.asm.tree.analysis.Analyzer; +import org.objectweb.asm.tree.analysis.AnalyzerException; +import org.objectweb.asm.tree.analysis.BasicValue; +import org.objectweb.asm.tree.analysis.SimpleVerifier; + +public class ConfigTest { + public static final Config CONFIG = TypeInferenceTest.CONFIG; + + @Test + public void verifyHierarchy() throws ClassNotFoundException { + TypeInfo tree = CONFIG.getTypeInfo(); + + //Verify known interfaces + for (Type itf : tree.getKnownInterfaces()) { + Class clazz = loadClass(itf.getClassName()); + if (!clazz.isInterface()) { + throw new AssertionError("Class " + clazz.getName() + " is not an interface"); + } + } + + //Verify super info + for (TypeInfo.Node node : tree.nodes()) { + Class clazz = loadClass(node.getValue().getClassName()); + + //Check interfaces + for (Type itf : node.getInterfaces()) { + Class itfClazz = loadClass(itf.getClassName()); + + if (!itfClazz.isAssignableFrom(clazz)) { + throw new AssertionError("Class " + clazz.getName() + " does not implement " + itfClazz.getName()); + } + + if (!itfClazz.isInterface()) { + throw new AssertionError("Class " + itfClazz.getName() + " is not an interface"); + } + } + + //Check super + if (clazz.isInterface()) { + continue; + } + + if (node.getSuperclass() != null) { + if (clazz.getSuperclass() == null) { + throw new AssertionError("Class " + clazz.getName() + " does not have a superclass"); + } + + if (!clazz.getSuperclass().getName().equals(node.getSuperclass().getValue().getClassName())) { + throw new AssertionError( + "Class " + clazz.getName() + " has a superclass " + clazz.getSuperclass().getName() + " but config gives " + node.getSuperclass().getValue().getClassName()); + } + } else if (clazz.getSuperclass() != null) { + throw new AssertionError("Class " + clazz.getName() + " has a superclass"); + } + } + } + + @Test + public void verifyReplacements() { + for (List info : CONFIG.getMethodParameterInfo().values()) { + for (MethodParameterInfo methodInfo : info) { + if (methodInfo.getReplacement() != null) { + verifyReplacement(methodInfo); + } + } + } + } + + private void verifyReplacement(MethodParameterInfo methodInfo) { + MethodReplacement replacement = methodInfo.getReplacement(); + + Type returnType = methodInfo.getMethod().getDescriptor().getReturnType(); + Type[] argTypesWithoutThis = methodInfo.getMethod().getDescriptor().getArgumentTypes(); + + Type[] argTypes; + if (!methodInfo.getMethod().isStatic()) { + argTypes = new Type[argTypesWithoutThis.length + 1]; + argTypes[0] = methodInfo.getMethod().getOwner(); + System.arraycopy(argTypesWithoutThis, 0, argTypes, 1, argTypesWithoutThis.length); + } else { + argTypes = argTypesWithoutThis; + } + + Type[] returnTypes = methodInfo.getReturnType() == null ? + new Type[] { Type.VOID_TYPE } : + methodInfo.getReturnType().resultingTypes().toArray(new Type[0]); + + for (int i = 0; i < replacement.getParameterIndices().length; i++) { + List types = getTypesFromIndices(methodInfo, argTypes, replacement.getParameterIndices()[i]); + + verifyFactory(replacement.getBytecodeFactories()[i], types, returnTypes[i]); + } + + //Verify finalizer + if (replacement.getFinalizer() == null) return; + + List types = getTypesFromIndices(methodInfo, argTypes, replacement.getFinalizerIndices()); + + verifyFactory(replacement.getFinalizer(), types, Type.VOID_TYPE); + } + + private List getTypesFromIndices(MethodParameterInfo methodInfo, Type[] argTypes, List[] indices) { + List types = new ArrayList<>(); + + for (int j = 0; j < indices.length; j++) { + DerivedTransformType derivedType = methodInfo.getParameterTypes()[j]; + List transformedTypes = derivedType.resultingTypes(); + + for (int index : indices[j]) { + if (transformedTypes.get(index).getSort() == Type.VOID) { + types.add(argTypes[j]); + } else { + types.add(transformedTypes.get(index)); + } + } + } + return types; + } + + private void verifyFactory(BytecodeFactory bytecodeFactory, List argTypes, Type expectedReturnType) { + String desc = Type.getMethodDescriptor(expectedReturnType, argTypes.toArray(new Type[0])); + + MethodNode method = new MethodNode(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, "test", desc, null, null); + InsnList insns = method.instructions; + + //Load args onto stack + int varIndex = 0; + for (Type argType : argTypes) { + insns.add(new VarInsnNode(argType.getOpcode(Opcodes.ILOAD), varIndex)); + varIndex += argType.getSize(); + } + + //We don't actually know these, so we make them big enough for anything reasonable + method.maxLocals = 100; + method.maxStack = 100; + + insns.add(bytecodeFactory.generate(VariableAllocator.makeBasicAllocator(varIndex))); + + //Return + insns.add(new InsnNode(expectedReturnType.getOpcode(Opcodes.IRETURN))); + + //Verify! + SimpleVerifier verifier = new SimpleVerifier(); + Analyzer analyzer = new Analyzer<>(verifier); + try { + analyzer.analyze("no/such/Class", method); + } catch (AnalyzerException e) { + throw new AssertionError("Failed to verify bytecode factory", e); + } + } + + /** + * Loads class without initializing it + * @param name The full binary name of the class to load (separated by dots) + */ + private static Class loadClass(String name) { + try { + return Class.forName(name, false, ClassLoader.getSystemClassLoader()); + } catch (ClassNotFoundException e) { + throw new AssertionError("Class " + name + " not found", e); + } + } +} diff --git a/src/test/java/io/github/opencubicchunks/cubicchunks/typetransformer/TypeInferenceTest.java b/src/test/java/io/github/opencubicchunks/cubicchunks/typetransformer/TypeInferenceTest.java new file mode 100644 index 000000000..45f35e615 --- /dev/null +++ b/src/test/java/io/github/opencubicchunks/cubicchunks/typetransformer/TypeInferenceTest.java @@ -0,0 +1,287 @@ +package io.github.opencubicchunks.cubicchunks.typetransformer; + +import java.util.Map; + +import io.github.opencubicchunks.cubicchunks.mixin.transform.MainTransformer; +import io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.TypeTransformer; +import io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.analysis.AnalysisResults; +import io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.analysis.DerivedTransformType; +import io.github.opencubicchunks.cubicchunks.mixin.transform.typetransformer.transformer.config.Config; +import io.github.opencubicchunks.cubicchunks.mixin.transform.util.ASMUtil; +import io.github.opencubicchunks.cubicchunks.mixin.transform.util.MethodID; +import net.minecraft.core.SectionPos; +import net.minecraft.world.level.lighting.BlockLightEngine; +import net.minecraft.world.level.lighting.BlockLightSectionStorage; +import net.minecraft.world.level.lighting.DynamicGraphMinFixedPoint; +import net.minecraft.world.level.lighting.LayerLightEngine; +import net.minecraft.world.level.lighting.LayerLightSectionStorage; +import net.minecraft.world.level.lighting.SkyLightEngine; +import net.minecraft.world.level.lighting.SkyLightSectionStorage; +import org.junit.jupiter.api.Test; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.ClassNode; + +public class TypeInferenceTest { + public static final Config CONFIG = MainTransformer.TRANSFORM_CONFIG; + public static final ClassCheck[] CHECKS_TO_PERFORM = { + new ClassCheck( + BlockLightEngine.class, + + MethodCheck.of("getLightEmission", "blockpos"), + MethodCheck.of("computeLevelFromNeighbor", "blockpos", "blockpos"), + MethodCheck.of("checkNeighborsAfterUpdate", "blockpos"), + MethodCheck.of("getComputedLevel", "blockpos", "blockpos"), + MethodCheck.of("onBlockEmissionIncrease") + ), + + new ClassCheck( + SkyLightEngine.class, + + MethodCheck.of("computeLevelFromNeighbor", "blockpos", "blockpos"), + MethodCheck.of("checkNeighborsAfterUpdate", "blockpos"), + MethodCheck.of("getComputedLevel", "blockpos", "blockpos"), + MethodCheck.of("checkNode", "blockpos") + ), + + new ClassCheck( + LayerLightEngine.class, + + MethodCheck.of("checkNode", "blockpos"), + MethodCheck.of("getChunk"), + MethodCheck.of("getStateAndOpacity", "blockpos"), + MethodCheck.of("getShape", null, "blockpos"), + MethodCheck.of("getLightBlockInto"), + MethodCheck.of("isSource", "blockpos"), + MethodCheck.of("getComputedLevel", "blockpos", "blockpos"), + MethodCheck.ofWithDesc("getLevel", "(J)I", "blockpos"), + MethodCheck.ofWithDesc("getLevel", "(Lnet/minecraft/world/level/chunk/DataLayer;J)I", null, "blockpos"), + MethodCheck.of("setLevel", "blockpos"), + MethodCheck.of("computeLevelFromNeighbor", "blockpos", "blockpos"), + MethodCheck.of("runUpdates"), + MethodCheck.of("queueSectionData"), + MethodCheck.of("getDataLayerData"), + MethodCheck.of("getLightValue"), + MethodCheck.of("checkBlock"), + MethodCheck.of("onBlockEmissionIncrease"), + MethodCheck.of("updateSectionStatus"), + MethodCheck.of("enableLightSources"), + MethodCheck.of("retainData") + ), + + new ClassCheck( + SectionPos.class, + + MethodCheck.of("blockToSection", "blockpos") + ), + + new ClassCheck( + LayerLightSectionStorage.class, + + MethodCheck.of("storingLightForSection"), + MethodCheck.ofWithDesc("getDataLayer", "(JZ)Lnet/minecraft/world/level/chunk/DataLayer;"), + MethodCheck.ofWithDesc("getDataLayer", "(Lnet/minecraft/world/level/lighting/DataLayerStorageMap;J)Lnet/minecraft/world/level/chunk/DataLayer;"), + MethodCheck.of("getDataLayerData"), + MethodCheck.of("getLightValue", "blockpos"), + MethodCheck.of("getStoredLevel", "blockpos"), + MethodCheck.of("setStoredLevel", "blockpos"), + MethodCheck.of("getLevel"), + MethodCheck.of("getLevelFromSource"), + MethodCheck.of("setLevel"), + MethodCheck.of("createDataLayer"), + MethodCheck.of("clearQueuedSectionBlocks"), + MethodCheck.of("markNewInconsistencies"), + MethodCheck.of("checkEdgesForSection"), + MethodCheck.of("onNodeAdded"), + MethodCheck.of("onNodeRemoved"), + MethodCheck.of("enableLightSources"), + MethodCheck.of("retainData"), + MethodCheck.of("queueSectionData"), + MethodCheck.of("updateSectionStatus") + ), + + new ClassCheck( + SkyLightSectionStorage.class, + + MethodCheck.ofWithDesc("getLightValue", "(J)I", "blockpos"), + MethodCheck.ofWithDesc("getLightValue", "(JZ)I", "blockpos"), + MethodCheck.of("onNodeAdded"), + MethodCheck.of("queueRemoveSource"), + MethodCheck.of("queueAddSource"), + MethodCheck.of("onNodeRemoved"), + MethodCheck.of("enableLightSources"), + MethodCheck.of("createDataLayer"), + MethodCheck.of("repeatFirstLayer"), + MethodCheck.of("markNewInconsistencies"), + MethodCheck.of("hasSectionsBelow"), + MethodCheck.of("isAboveData"), + MethodCheck.of("lightOnInSection") + ), + + new ClassCheck( + BlockLightSectionStorage.class, + + MethodCheck.of("getLightValue", "blockpos") + ), + + new ClassCheck( + DynamicGraphMinFixedPoint.class, + + MethodCheck.of("getKey"), + MethodCheck.of("checkFirstQueuedLevel"), + MethodCheck.of("removeFromQueue", "blockpos"), + MethodCheck.of("removeIf", "blockpos predicate"), + MethodCheck.of("dequeue", "blockpos"), + MethodCheck.of("enqueue", "blockpos"), + MethodCheck.of("checkNode", "blockpos"), + MethodCheck.ofWithDesc("checkEdge", "(JJIZ)V", "blockpos", "blockpos"), + MethodCheck.ofWithDesc("checkEdge", "(JJIIIZ)V", "blockpos", "blockpos"), + MethodCheck.of("checkNeighbor", "blockpos", "blockpos"), + MethodCheck.of("runUpdates"), + + MethodCheck.of("isSource", "blockpos"), + MethodCheck.of("getComputedLevel", "blockpos", "blockpos"), + MethodCheck.of("checkNeighborsAfterUpdate", "blockpos"), + MethodCheck.of("getLevel", "blockpos"), + MethodCheck.of("setLevel", "blockpos"), + MethodCheck.of("computeLevelFromNeighbor", "blockpos", "blockpos") + ) + }; + + @Test + public void runTests() { + for (ClassCheck check : CHECKS_TO_PERFORM) { + Map analysisResults = getAnalysisResults(check.clazz); + + for (MethodCheck methodCheck : check.methods) { + AnalysisResults results = methodCheck.findWanted(analysisResults); + if (results == null) { + throw new RuntimeException("No results for " + methodCheck.finder); + } + + if (!methodCheck.check(results)) { + StringBuilder error = new StringBuilder(); + + error.append("Unexpected type inference results for ").append(results.methodNode().name).append(" ").append(results.methodNode().desc); + error.append("\n"); + error.append("Expected: \n\t[ "); + + int numArgs = Type.getArgumentTypes(results.methodNode().desc).length; + boolean isStatic = ASMUtil.isStatic(results.methodNode()); + + int i; + for (i = 0; i < methodCheck.expected.length; i++) { + if (i > 0) { + error.append(", "); + } + + error.append(methodCheck.expected[i]); + } + + for (; i < numArgs; i++) { + if (i > 0) { + error.append(", "); + } + + error.append(DerivedTransformType.createDefault(null)); + } + + error.append(" ]\n\nActual: \n\t[ "); + + boolean start = true; + for (i = isStatic ? 0 : 1; i < results.getArgTypes().length; i++) { + if (!start) { + error.append(", "); + } + start = false; + + error.append(results.getArgTypes()[i]); + } + + error.append(" ]"); + + throw new AssertionError(error.toString()); + } + } + } + } + + private Map getAnalysisResults(Class clazz) { + ClassNode classNode = ASMUtil.loadClassNode(clazz); + + TypeTransformer typeTransformer = new TypeTransformer(CONFIG, classNode, false); + typeTransformer.analyzeAllMethods(); + + return typeTransformer.getAnalysisResults(); + } + + private static record ClassCheck(Class clazz, MethodCheck... methods) { + + } + + private static record MethodCheck(ASMUtil.MethodCondition finder, DerivedTransformType... expected) { + public AnalysisResults findWanted(Map results) { + for (Map.Entry entry : results.entrySet()) { + if (finder.testMethodID(entry.getKey())) { + return entry.getValue(); + } + } + + return null; + } + + public boolean check(AnalysisResults results) { + DerivedTransformType[] args = results.getArgTypes(); + + int argsIndex = ASMUtil.isStatic(results.methodNode()) ? 0 : 1; + + for (int i = 0; i < expected.length; i++, argsIndex++) { + if (!expected[i].equals(args[argsIndex])) { + return false; + } + } + + //Check the rest of args are default types + for (int i = argsIndex; i < args.length; i++) { + if (args[i].getTransformType() != null) { + return false; + } + } + + return true; + } + + public static MethodCheck of(String methodName, String... types) { + ASMUtil.MethodCondition finder = new ASMUtil.MethodCondition(methodName, null); + + DerivedTransformType[] expected = new DerivedTransformType[types.length]; + + for (int i = 0; i < types.length; i++) { + if (types[i] == null) { + expected[i] = DerivedTransformType.createDefault(Type.VOID_TYPE); + continue; + } + + expected[i] = DerivedTransformType.fromString(types[i], CONFIG.getTypes()); + } + + return new MethodCheck(finder, expected); + } + + public static MethodCheck ofWithDesc(String methodName, String desc, String... types) { + ASMUtil.MethodCondition finder = new ASMUtil.MethodCondition(methodName, desc); + + DerivedTransformType[] expected = new DerivedTransformType[types.length]; + + for (int i = 0; i < types.length; i++) { + if (types[i] == null) { + expected[i] = DerivedTransformType.createDefault(Type.VOID_TYPE); + continue; + } + + expected[i] = DerivedTransformType.fromString(types[i], CONFIG.getTypes()); + } + + return new MethodCheck(finder, expected); + } + } +} diff --git a/src/test/java/io/github/opencubicchunks/cubicchunks/typetransformer/TypeTransformerMethods.java b/src/test/java/io/github/opencubicchunks/cubicchunks/typetransformer/TypeTransformerMethods.java new file mode 100644 index 000000000..5c3f8b173 --- /dev/null +++ b/src/test/java/io/github/opencubicchunks/cubicchunks/typetransformer/TypeTransformerMethods.java @@ -0,0 +1,289 @@ +package io.github.opencubicchunks.cubicchunks.typetransformer; + +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintWriter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import io.github.opencubicchunks.cubicchunks.mixin.ASMConfigPlugin; +import io.github.opencubicchunks.cubicchunks.mixin.transform.MainTransformer; +import io.github.opencubicchunks.cubicchunks.mixin.transform.util.ASMUtil; +import io.github.opencubicchunks.cubicchunks.mixin.transform.util.MethodID; +import io.github.opencubicchunks.cubicchunks.utils.TestMappingUtils; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenCustomHashMap; +import it.unimi.dsi.fastutil.objects.ObjectOpenCustomHashSet; +import net.fabricmc.loader.api.MappingResolver; +import org.junit.jupiter.api.Test; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.InsnList; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; +import org.objectweb.asm.util.CheckClassAdapter; +import org.spongepowered.asm.launch.MixinBootstrap; +import org.spongepowered.asm.mixin.transformer.IMixinTransformer; + +/** + * This class runs the TypeTransformer on all required classes and tracks the methods which are assumed to exist. This test makes the assumption that an untransformed class is completely + * correct. + */ +public class TypeTransformerMethods { + private static final boolean LOAD_FROM_MIXIN_OUT = false; + + private static final Path ASSUMED_MIXIN_OUT = TestMappingUtils.getGameDir().resolve(".mixin.out/class"); + private static final Map CACHED_CLASSES = new HashMap<>(); + private static IMixinTransformer transformer; + private ASMConfigPlugin plugin = new ASMConfigPlugin(); + private final MappingResolver map = TestMappingUtils.getMappingResolver(); + private final Set classNamesToTransform = Stream.of( + "net.minecraft.class_3554", //DynamicGraphMixFixedPoint + "net.minecraft.class_3558", //LayerLightEngine + "net.minecraft.class_3560", //LayerLightSectionStorage + "net.minecraft.class_3547", //BlockLightSectionStorage + "net.minecraft.class_3569", //SkyLightSectionStorage + "net.minecraft.class_4076", //SectionPos + "net.minecraft.class_3552", //BlockLightEngine + "net.minecraft.class_3572", //SkyLightEngine + "net.minecraft.class_6350$class_5832" //Aquifer$NoiseBasedAquifer + ).map(name -> map.mapClassName("intermediary", name)).collect(Collectors.toSet()); + + @Test + public void transformAndTest() { + System.out.println("Config: " + MainTransformer.TRANSFORM_CONFIG); //Load MainTransformer + + Set methodsUsed = new ObjectOpenCustomHashSet<>(MethodID.HASH_CALL_TYPE); + Map> usages = new Object2ObjectOpenCustomHashMap<>(MethodID.HASH_CALL_TYPE); + + for (String className : classNamesToTransform) { + ClassNode classNode = getClassNode(className); + + //Find all methods which are called + for (MethodNode methodNode : classNode.methods) { + InsnList instructions = methodNode.instructions; + int i = 0; + for (AbstractInsnNode instruction : instructions.toArray()) { + int opcode = instruction.getOpcode(); + if (opcode == Opcodes.INVOKESTATIC || opcode == Opcodes.INVOKEVIRTUAL || opcode == Opcodes.INVOKEINTERFACE || opcode == Opcodes.INVOKESPECIAL) { + MethodID methodID = MethodID.from((MethodInsnNode) instruction); + methodsUsed.add(methodID); + + List usagesOfMethod = usages.computeIfAbsent(methodID, k -> new ArrayList<>()); + + usagesOfMethod.add(ASMUtil.onlyClassName(className) + " " + ASMUtil.prettyPrintMethod(methodNode.name, methodNode.desc) + " @ " + i); + } + i++; + } + } + } + + System.out.println("Identified all used methods"); + Map faultyUses = new HashMap<>(); //MethodID -> Reason + + for (MethodID methodID : methodsUsed) { + String result = checkMethod(methodID); + if (result != null) { + faultyUses.put(methodID, result); + } + } + + if (!faultyUses.isEmpty()) { + System.out.println("Found faulty uses:"); + for (Map.Entry entry : faultyUses.entrySet()) { + System.out.println(" - " + entry.getKey() + ": " + entry.getValue()); + for (String usage : usages.get(entry.getKey())) { + System.out.println(" - " + usage); + } + } + throw new RuntimeException("Found faulty uses"); + } + } + + private String checkMethod(MethodID methodID) { + ClassNode classNode = getClassNode(methodID.getOwner().getClassName()); + ClassNode earliestDeclaringClass = null; + MethodNode earliestDefinition = null; + + Set interfacesToCheck = new HashSet<>(); + + boolean isInterface = (classNode.access & Opcodes.ACC_INTERFACE) != 0; + + while (true) { + Optional methodNodeOptional = + classNode.methods.stream().filter(m -> m.name.equals(methodID.getName()) && m.desc.equals(methodID.getDescriptor().getDescriptor())).findFirst(); + + if (methodNodeOptional.isPresent()) { + earliestDeclaringClass = classNode; + earliestDefinition = methodNodeOptional.get(); + } + + if (methodID.getCallType() == MethodID.CallType.SPECIAL) { + break; + } + + if (classNode.interfaces != null) { + interfacesToCheck.addAll(classNode.interfaces); + } + + if (classNode.superName == null) { + break; + } + + classNode = getClassNode(classNode.superName); + } + + //Find all implemented interfaces + Set implementedInterfaces = findImplementedInterfaces(interfacesToCheck); + + //Check interfaces + for (String interfaceName : implementedInterfaces) { + ClassNode interfaceNode = getClassNode(interfaceName); + Optional methodNodeOptional = + interfaceNode.methods.stream().filter(m -> m.name.equals(methodID.getName()) && m.desc.equals(methodID.getDescriptor().getDescriptor())).findFirst(); + + if (methodNodeOptional.isPresent()) { + earliestDeclaringClass = interfaceNode; + earliestDefinition = methodNodeOptional.get(); + break; + } + } + + if (earliestDeclaringClass == null) { + return "No declaration found"; + } + + return computeAndCheckCallType(methodID, earliestDefinition, isInterface); + } + + private String computeAndCheckCallType(MethodID methodID, MethodNode earliestDefinition, boolean isInterface) { + boolean isStatic = ASMUtil.isStatic(earliestDefinition); + if (isStatic) { + isInterface = false; + } + boolean isVirtual = !isStatic && !isInterface; + + return checkCallType(methodID, isInterface, isStatic, isVirtual); + } + + private String checkCallType(MethodID methodID, boolean isInterface, boolean isStatic, boolean isVirtual) { + if (isStatic && methodID.getCallType() != MethodID.CallType.STATIC) { + return "Static method is not called with INVOKESTATIC"; + } else if (isInterface && methodID.getCallType() != MethodID.CallType.INTERFACE) { + return "Interface method is not called with INVOKEINTERFACE"; + } else if (isVirtual && (methodID.getCallType() != MethodID.CallType.VIRTUAL && methodID.getCallType() != MethodID.CallType.SPECIAL)) { + return "Virtual method is not called with INVOKEVIRTUAL or INVOKESPECIAL"; + } else { + return null; + } + } + + private Set findImplementedInterfaces(Set interfacesToCheck) { + Set implementedInterfaces = new HashSet<>(); + Set toCheck = new HashSet<>(interfacesToCheck); + while (!toCheck.isEmpty()) { + Set newToCheck = new HashSet<>(); + for (String interfaceName : toCheck) { + ClassNode interfaceNode = getClassNode(interfaceName); + if (interfaceNode.interfaces != null) { + newToCheck.addAll(interfaceNode.interfaces); + } + } + implementedInterfaces.addAll(toCheck); + toCheck = newToCheck; + toCheck.removeAll(implementedInterfaces); + } + return implementedInterfaces; + } + + private ClassNode getClassNode(String className) { + className = className.replace('.', '/'); + + ClassNode classNode = CACHED_CLASSES.get(className); + + if (classNode == null) { + + if (LOAD_FROM_MIXIN_OUT) { + classNode = loadClassNodeFromMixinOut(className); + } + + if (classNode == null) { + System.err.println("Couldn't find class " + className + " in .mixin.out"); + classNode = loadClassNodeFromClassPath(className); + plugin.postApply(className.replace('/', '.'), classNode, null, null); + } + + CACHED_CLASSES.put(className, classNode); + } + + return classNode; + } + + private ClassNode loadClassNodeFromClassPath(String className) { + byte[] bytes; + + InputStream is; + + is = ClassLoader.getSystemResourceAsStream(className + ".class"); + + if (is == null) { + throw new RuntimeException("Could not find class " + className); + } + + ClassNode classNode = new ClassNode(); + try { + ClassReader classReader = new ClassReader(is); + classReader.accept(classNode, 0); + } catch (IOException e) { + throw new RuntimeException("Could not read class " + className, e); + } + + CACHED_CLASSES.put(className, classNode); + + return classNode; + } + + private ClassNode loadClassNodeFromMixinOut(String className) { + try { + InputStream is = Files.newInputStream(ASSUMED_MIXIN_OUT.resolve(className + ".class")); + + ClassNode classNode = new ClassNode(); + ClassReader classReader = new ClassReader(is); + classReader.accept(classNode, ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES); + + return classNode; + } catch (IOException e) { + return null; + } + } + + private void verify(ClassNode classNode) { + ClassWriter verifyWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES); + classNode.accept(verifyWriter); + + CheckClassAdapter.verify(new ClassReader(verifyWriter.toByteArray()), false, new PrintWriter(System.out)); + } + + private IMixinTransformer getMixinTransformer() { + if (transformer == null) { + makeTransformer(); + } + + return transformer; + } + + private void makeTransformer() { + MixinBootstrap.init(); + } +} diff --git a/src/test/java/io/github/opencubicchunks/cubicchunks/utils/Int3ListTest.java b/src/test/java/io/github/opencubicchunks/cubicchunks/utils/Int3ListTest.java new file mode 100644 index 000000000..4486242b9 --- /dev/null +++ b/src/test/java/io/github/opencubicchunks/cubicchunks/utils/Int3ListTest.java @@ -0,0 +1,79 @@ +package io.github.opencubicchunks.cubicchunks.utils; + +import static org.junit.Assert.assertEquals; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +import net.minecraft.core.Vec3i; +import org.junit.Test; + +public class Int3ListTest { + @Test + public void randomTest() { + Random random = new Random(); + + long seed = random.nextLong(); + System.out.println("Seed: " + seed); + random.setSeed(seed); + + try (Int3List list = new Int3List()) { + List tester = new ArrayList<>(); + + for (int i = 0; i < 100000; i++) { + int x, y, z, index; + switch (random.nextInt(4)) { + case 0: + x = random.nextInt(50); + y = random.nextInt(50); + z = random.nextInt(50); + list.add(x, y, z); + tester.add(new Vec3i(x, y, z)); + break; + case 1: + if (list.size() == 0) break; + index = random.nextInt(list.size()); + list.remove(index); + tester.remove(index); + break; + case 2: + if (list.size() == 0) break; + index = random.nextInt(list.size()); + x = random.nextInt(50); + y = random.nextInt(50); + z = random.nextInt(50); + list.set(index, x, y, z); + tester.set(index, new Vec3i(x, y, z)); + break; + case 3: + index = random.nextInt(list.size() + 1); + x = random.nextInt(50); + y = random.nextInt(50); + z = random.nextInt(50); + list.insert(index, x, y, z); + tester.add(index, new Vec3i(x, y, z)); + } + + if (random.nextInt(10000) == 0) { + list.clear(); + tester.clear(); + } + + assertEqualList(list, tester); + } + } + } + + private void assertEqualList(Int3List list, List tester) { + assertEquals("Different Sizes", list.size(), tester.size()); + + for (int i = 0; i < list.size(); i++) { + Vec3i vec = tester.get(i); + + assertEquals(vec.getX(), list.getX(i)); + assertEquals(vec.getY(), list.getY(i)); + assertEquals(vec.getZ(), list.getZ(i)); + } + } +} diff --git a/src/test/java/io/github/opencubicchunks/cubicchunks/utils/Int3UByteLinkedHashMapTest.java b/src/test/java/io/github/opencubicchunks/cubicchunks/utils/Int3UByteLinkedHashMapTest.java new file mode 100644 index 000000000..c21b1979a --- /dev/null +++ b/src/test/java/io/github/opencubicchunks/cubicchunks/utils/Int3UByteLinkedHashMapTest.java @@ -0,0 +1,250 @@ +package io.github.opencubicchunks.cubicchunks.utils; + +import static com.google.common.base.Preconditions.checkState; + +import java.util.Iterator; +import java.util.concurrent.ThreadLocalRandom; +import java.util.function.BiConsumer; +import java.util.function.ToIntFunction; +import java.util.stream.IntStream; + +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import it.unimi.dsi.fastutil.objects.Object2IntMaps; +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; +import net.minecraft.core.Vec3i; +import org.junit.Test; + +public class Int3UByteLinkedHashMapTest { + @Test + public void test1000BigCoordinates() { + IntStream.range(0, 1024).parallel().forEach(i -> this.test(1000, ThreadLocalRandom::nextInt)); + } + + @Test + public void test1000000BigCoordinates() { + this.test(1000000, ThreadLocalRandom::nextInt); + } + + @Test + public void test1000SmallCoordinates() { + IntStream.range(0, 1024).parallel().forEach(i -> this.test(1000, r -> r.nextInt() & 1023)); + } + + @Test + public void test1000000SmallCoordinates() { + this.test(1000000, r -> r.nextInt() & 1023); + } + + protected void test(int nPoints, ToIntFunction rng) { + Object2IntMap reference = new Object2IntOpenHashMap<>(nPoints); + reference.defaultReturnValue(-1); + + ThreadLocalRandom r = ThreadLocalRandom.current(); + + try (Int3UByteLinkedHashMap test = new Int3UByteLinkedHashMap()) { + for (int i = 0; i < nPoints; i++) { //insert some random values + int x = rng.applyAsInt(r); + int y = rng.applyAsInt(r); + int z = rng.applyAsInt(r); + int value = r.nextInt() & 0xFF; + + int v0 = reference.put(new Vec3i(x, y, z), value); + int v1 = test.put(x, y, z, value); + checkState(v0 == v1); + } + + this.ensureEqual(reference, test); + + for (Iterator> itr = reference.object2IntEntrySet().iterator(); itr.hasNext();) { //remove some positions at random + Object2IntMap.Entry entry = itr.next(); + Vec3i pos = entry.getKey(); + int value = entry.getIntValue(); + + if ((r.nextInt() & 3) == 0) { + itr.remove(); + + int removed = test.remove(pos.getX(), pos.getY(), pos.getZ()); + checkState(value == removed); + } + } + + this.ensureEqual(reference, test); + } + } + + protected void ensureEqual(Object2IntMap reference, Int3UByteLinkedHashMap test) { + checkState(reference.size() == test.longSize()); + + class Tester implements BiConsumer, Int3UByteLinkedHashMap.EntryConsumer, Runnable { + int countReference; + int countTest; + + @Override public void accept(Vec3i k, Integer value) { + this.countReference++; + + checkState(test.containsKey(k.getX(), k.getY(), k.getZ())); + checkState(test.get(k.getX(), k.getY(), k.getZ()) == value); + } + + @Override public void accept(int x, int y, int z, int value) { + this.countTest++; + + checkState(reference.containsKey(new Vec3i(x, y, z))); + checkState(reference.getInt(new Vec3i(x, y, z)) == value); + } + + @Override + public void run() { + reference.forEach(this); + test.forEach(this); + + checkState(this.countReference == this.countTest); + } + } + + new Tester().run(); + } + + @Test + public void testDuplicateInsertionBigCoordinates() { + IntStream.range(0, 1024).parallel().forEach(i -> this.testDuplicateInsertion(ThreadLocalRandom::nextInt)); + } + + @Test + public void testDuplicateInsertionSmallCoordinates() { + IntStream.range(0, 1024).parallel().forEach(i -> this.testDuplicateInsertion(r -> r.nextInt() & 1023)); + } + + protected void testDuplicateInsertion(ToIntFunction rng) { + Object2IntMap reference = new Object2IntOpenHashMap<>(); + reference.defaultReturnValue(-1); + + ThreadLocalRandom r = ThreadLocalRandom.current(); + + try (Int3UByteLinkedHashMap test = new Int3UByteLinkedHashMap()) { + this.ensureEqual(reference, test); + + for (int i = 0; i < 10000; i++) { + int x = rng.applyAsInt(r); + int y = rng.applyAsInt(r); + int z = rng.applyAsInt(r); + int value = r.nextInt() & 0xFF; + + if (reference.putIfAbsent(new Vec3i(x, y, z), value) >= 0) { + i--; + continue; + } + + int v0 = test.put(x, y, z, value); + int v1 = test.putIfAbsent(x, y, z, (value + 1) & 0xFF); + int v2 = test.put(x, y, z, value); + checkState(v0 == Int3UByteLinkedHashMap.DEFAULT_RETURN_VALUE && v1 == value && v2 == value); + } + + this.ensureEqual(reference, test); + } + } + + @Test + public void testDuplicateRemovalBigCoordinates() { + IntStream.range(0, 1024).parallel().forEach(i -> this.testDuplicateRemoval(ThreadLocalRandom::nextInt)); + } + + @Test + public void testDuplicateRemovalSmallCoordinates() { + IntStream.range(0, 1024).parallel().forEach(i -> this.testDuplicateRemoval(r -> r.nextInt() & 1023)); + } + + protected void testDuplicateRemoval(ToIntFunction rng) { + Object2IntMap reference = new Object2IntOpenHashMap<>(); + reference.defaultReturnValue(-1); + + ThreadLocalRandom r = ThreadLocalRandom.current(); + + try (Int3UByteLinkedHashMap test = new Int3UByteLinkedHashMap()) { + this.ensureEqual(reference, test); + + for (int i = 0; i < 10000; i++) { + int x = rng.applyAsInt(r); + int y = rng.applyAsInt(r); + int z = rng.applyAsInt(r); + int value = r.nextInt() & 0xFF; + + int v0 = reference.put(new Vec3i(x, y, z), value); + int v1 = test.put(x, y, z, value); + checkState(v0 == v1); + } + + this.ensureEqual(reference, test); + + reference.forEach((k, v) -> { + int v0 = test.remove(k.getX(), k.getY(), k.getZ()); + int v1 = test.remove(k.getX(), k.getY(), k.getZ()); + checkState(v0 == v && v1 == Int3UByteLinkedHashMap.DEFAULT_RETURN_VALUE); + }); + + this.ensureEqual(Object2IntMaps.emptyMap(), test); + } + } + + @Test + public void testPollBigCoordinates() { + IntStream.range(0, 1024).parallel().forEach(i -> this.testPoll(ThreadLocalRandom::nextInt)); + } + + @Test + public void testPollSmallCoordinates() { + IntStream.range(0, 1024).parallel().forEach(i -> this.testPoll(r -> r.nextInt() & 1023)); + } + + protected void testPoll(ToIntFunction rng) { + Object2IntMap reference = new Object2IntOpenHashMap<>(); + reference.defaultReturnValue(-1); + + ThreadLocalRandom r = ThreadLocalRandom.current(); + + try (Int3UByteLinkedHashMap test = new Int3UByteLinkedHashMap()) { + this.ensureEqual(reference, test); + + for (int i = 0; i < 10000; i++) { + int x = rng.applyAsInt(r); + int y = rng.applyAsInt(r); + int z = rng.applyAsInt(r); + int value = r.nextInt() & 0xFF; + + int v0 = reference.put(new Vec3i(x, y, z), value); + int v1 = test.put(x, y, z, value); + checkState(v0 == v1); + } + + this.ensureEqual(reference, test); + + + Int3UByteLinkedHashMap.EntryConsumer callback = (x, y, z, value) -> { + checkState(!test.containsKey(x, y, z)); + checkState(reference.containsKey(new Vec3i(x, y, z))); + checkState(reference.getInt(new Vec3i(x, y, z)) == value); + + checkState(reference.removeInt(new Vec3i(x, y, z)) == value); + + if (r.nextBoolean()) { //low chance of inserting a new entry + int nx = rng.applyAsInt(r); + int ny = rng.applyAsInt(r); + int nz = rng.applyAsInt(r); + int nvalue = r.nextInt() & 0xFF; + + int v0 = reference.put(new Vec3i(nx, ny, nz), nvalue); + int v1 = test.put(nx, ny, nz, nvalue); + checkState(v0 == v1); + } + }; + + while (test.poll(callback)) { + //empty + } + + + this.ensureEqual(Object2IntMaps.emptyMap(), test); + } + } +} diff --git a/src/test/java/io/github/opencubicchunks/cubicchunks/utils/LinkedInt3HashSetTest.java b/src/test/java/io/github/opencubicchunks/cubicchunks/utils/LinkedInt3HashSetTest.java new file mode 100644 index 000000000..2e45b2ae8 --- /dev/null +++ b/src/test/java/io/github/opencubicchunks/cubicchunks/utils/LinkedInt3HashSetTest.java @@ -0,0 +1,88 @@ +package io.github.opencubicchunks.cubicchunks.utils; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +import java.util.Random; + +import net.minecraft.core.BlockPos; +import org.junit.Test; + +public class LinkedInt3HashSetTest { + @Test + public void test1() { + LinkedInt3HashSet set = new LinkedInt3HashSet(); + + set.add(5, 8, -4); + set.add(9, 8, 7); + set.add(0, 10, 11); + + assertEquals(set.getFirstX(), 5); + assertEquals(set.getFirstY(), 8); + assertEquals(set.getFirstZ(), -4); + + assertEquals(set.size(), 3); + + assertEquals(BlockPos.asLong(5, 8, -4), set.removeFirstLong()); + assertEquals(set.size(), 2); + + assertEquals(BlockPos.asLong(9, 8, 7), set.removeFirstLong()); + assertEquals(set.size(), 1); + + assertEquals(BlockPos.asLong(0, 10, 11), set.removeFirstLong()); + assertEquals(set.size(), 0); + + set.add(0, 0, 0); + set.add(0, 0, 1); + set.add(0, 2, 3); + set.add(3, 1, 0); + + assertArrayEquals(new LinkedInt3HashSet.XYZTriple[] { + new LinkedInt3HashSet.XYZTriple(0, 0, 0), + new LinkedInt3HashSet.XYZTriple(0, 0, 1), + new LinkedInt3HashSet.XYZTriple(0, 2, 3), + new LinkedInt3HashSet.XYZTriple(3, 1, 0) + }, set.toArray()); + + set.remove(0, 2, 3); + + assertEquals(BlockPos.asLong(0, 0, 0), set.removeFirstLong()); + assertEquals(BlockPos.asLong(0, 0, 1), set.removeFirstLong()); + assertEquals(BlockPos.asLong(3, 1, 0), set.removeFirstLong()); + } + + @Test + public void test2() { + //Do random shit and see if it crashes + Random random = new Random(); + + long seed = random.nextLong(); + random.setSeed(seed); + + LinkedInt3HashSet set = new LinkedInt3HashSet(); + + System.out.println("Seed: " + seed); + + for (int i = 0; i < 100000; i++) { + + int n = random.nextInt(4); + + switch (n) { + case 0: + case 3: + set.add(random.nextInt(10), random.nextInt(10), random.nextInt(10)); + break; + case 1: + set.remove(random.nextInt(10), random.nextInt(10), random.nextInt(10)); + break; + case 2: + if (set.size > 0) { + set.removeFirstLong(); + } + break; + } + + set.toArray(); + } + } +} diff --git a/type-transformer-config-docs.md b/type-transformer-config-docs.md new file mode 100644 index 000000000..9d6277d52 --- /dev/null +++ b/type-transformer-config-docs.md @@ -0,0 +1,162 @@ +# Type Transformer Config + +The type transformer is the system responsible for converting positions stored as `long` internally into triplets of `int`. +However, the system is generalizable to any kind of similar transform and so the `long -> 3 int` transform needs to be specified in the config file (stored in `type-transform.json`). +This json document contains six top-level fields: +- `types` +- `methods` +- `classes` +- `invokers` +- `suffixed_methods` +- `type_meta_info` + + +## `types` +This field tells the transformer exactly what kind of types we want to transform into what other types. For example, +we can tell it that we want any `long` that stores a block position to be turned into 3 `int`s. We must specify every type +that we will want to transform. For example, we also need to specify transforming a `LongSet` into a set for triplets of `int`s. +Same thing for a map or a list. + +The field is a list of objects, each of which can have the following fields: + +### `id` +This is simply a string containing a name for the kind of type we are transforming. This is not too important, but is used for +providing debug information and for specifying information in `methods`. + +### `original` +This is a string specifying the type we are transforming from. This is given as a java descriptor. Examples are: +- `J` +- `Lit/unimi/dsi/fastutil/longs/LongSet;` + +### `transformed` +This is a list of strings, each of which is a java descriptor. It specifies which types the original type will get expanded into. +For example, for our `long -> 3 int` transform it is `["I", "I", "I"]`. + +### `from_original` (optional) +A list of methods which specifies how to turn an instance of the original type into an instance of the resulting types. +The list must have the same length as `transformed`. Each element is specified as a method id (see lower) and the `i`th method must accept +the original type as the only argument and return the `i`th transformed type. + +### `to_original` (optional) +A single method id which specifies how to convert the transformed types back into the original type. The method must accept exactly the types specified in `transformed` and return the +original type. + +### `constant_replacements` (optional) +Constant replacements tell the transformer how to deal with literal constants which are of the original type and need to be transformed. +If the transformer encounters such a case and either `constant_replacements` is not supplied or does not include that specified value, an error will be thrown. +`constant_replacements` is simply a list of objects. These objects have the field `from` which gives the original literal value. +They also have the field `to` which gives the transformed value. The transformed value must be a list of the same length as `transformed` and each element must be a literal of the corresponding type. + +### `original_predicate` (optional) +Should provide the descriptor of an interface for a predicate which acts on the original type. + +### `transformed_predicate` (optional) +Should provide the descriptor of an interface for a predicate which acts on the transformed types. + +### `original_consumer` (optional) +Should provide the descriptor of an interface for a consumer which acts on the original type. + +### `transformed_consumer` (optional) +Should provide the descriptor of an interface for a consumer which acts on the transformed types. + +All of the above `predicate` and `consumer` fields are used to transform lambdas and method references. + +## `methods` +The type transformer does still need to detect if a certain value is of a type we desire to transform. We can infer these by specifying some methods +which always accept as an argument a specific kind of value or maybe return a specific kind of value. For example, if the transform knows that `BlockPos.asLong()` returns +a long containing a block position, then in the code below, it is able to detect that `foo` is such a block position and then propagate this information. +```java +long foo = pos.toLong(); +``` + +Note: The type transformer will automatically know about methods specified in `to_original` and `from_original` in the `types` field so they should not be specified here. + +On top of that, this can also specify how the type transformer should transform a method call. + +The `methods` field is a list of objects, each of which must have the following fields: + +### `method` +A method id which specifies the method we are providing information about. + +### `possibilities` +A list of objects which each specify a "possibility" for this method. A possibility just specifies exactly what kind of values the method is accepting. +A possibility contains the following fields: + +#### `parameters` +A list of strings (or nulls) which specify exactly what this possibility expects the method to be called with. Each element in the list is a string +giving the id of a type transform (specified in `types`) or null if that parameter doesn't need to be transformed. Note that if the target method is not static, +the first element in the list specifies information about the `this`. + +#### `return_type` (optional) +A string (or null, by default) which specifies what the method returns. This string is exactly the same as what is described above for `parameters`. + +#### `minimumConditions` (optional) +The `mininums` field specifies the minimumConditions conditions that must be met to be sure that this possibility is what is actually being used. +If this field is omitted then this possibility will always be accepted. This field is a list of objects each with a `parameters` and (optionally) +a `return` field. These fields are similar to the `parameters` and `return_type` fields above. The difference is that the `parameters` and `return` fields can have +more nulls. A minimumConditions is "accepted" if every non-null value in `parameters` and `return_type` match the known situation when inferring transform types. +A possibility is accepted if any of its minimumConditions are accepted. + +#### `replacement` (optional) +This field allows you to override how the type transformer will transform a method call matching this possibility. This field is **required** if in the current possibility +the method returns a transform type which expands to more than one type. This is because Java does not support returning multiples values from a method in an efficient manner. + +Typically, this field is an array whose length is equal to the number of types the method returns. Each element in the array is an array which gives the java bytecode +to replace the call with. By default, each of these bytecode snippets will be run with all the transformed method arguments on the stack in order. However, if +the method parameters has types which expand to exactly the same number of types as the method returns, then the stack for the `i`th bytecode snippet will be run with only the +`i`th transformed element (as well as the rest of the method arguments). + +This method of putting the arguments onto the stack can be overriden by turning replacement into an object which contains an `expansion` field. This field contains +the array specified above. As well, it should contain an `indices` field. This field should be an array of equal length to `expansion`. + +Each element specifies what parameters should be loaded onto the stack for the corresponding bytecode snippet. The element should be an array of length equal +to the number of parameters the original method takes. Each of these elements in the subarrays specify which transformed parameters should be pushed onto the stack for the snippet. +This is specified by an array of integers each integer being the index of the target type of the parameter to push onto the stack. If the parameter does not have a transform type, then +the only accepted value is 0. For conciseness, if these arrays contain only a single element, then the array can be replaced with just that element. + +The bytecode snippets are specified as a list of bytecode instructions. Simple instructions such as `IADD` or `POP` are specified as a string with +the instruction name (in all caps). + +Method calls are represented by the following object: +```json +{ + "type": "INVOKEVIRTUAL" | "INVOKESTATIC" | "INVOKESPECIAL" | "INVOKEINTERFACE", + "method": +} +``` + +Constant loads are represented by the following object: +```json +{ + "type": "LDC", + "constant_type": "string" | "int" | "long" | "float" | "double", + "value": +} +``` + +Instructions based on a type are represented by the following object: +```json +{ + "type": "NEW" | "ANEWARRAY" | "CHECKCAST" | "INSTANCEOF", + "class": "class/name/Here" +} +``` + +Note: Bytecode snippets are checked at config load time to ensure type safety, that they do not underflow the stack, and that they return the expected value. + +### 'finalizer' (optional) +This field is a bytecode snippet as specified above. It will be run after everything in the expansion. + +### 'finalizer_indices' (optional) +Parameter indices similar to those specified above. + +NOTE: The discrepancy between how indices are specified for the expansion and the finalizer isn't great. + +## `classes` +This field provides extra information on how specific classes should be transformed. +It is an array of objects each of which specify information for one class. + +Each object must have a `class` field which specifies the class this is for. + +It can also have the following fields: +### `type_hints` (optional)