Add bytecode analysis to filter ICapabilityProvider impls where possible

Currently disabled by default till more testing is completed
This commit is contained in:
embeddedt 2026-02-26 20:42:20 -05:00
parent 878b3798f3
commit b9933b1158
No known key found for this signature in database
GPG Key ID: A69433EC199B5613
6 changed files with 576 additions and 37 deletions

View File

@ -174,6 +174,7 @@ public class ModernFixEarlyConfig {
.put("mixin.feature.blockentity_incorrect_thread", false)
.put("mixin.perf.clear_mixin_classinfo", false)
.put("mixin.perf.deduplicate_climate_parameters", false)
.put("mixin.perf.faster_capabilities.bytecode_analysis", false)
.put("mixin.bugfix.packet_leak", false)
.put("mixin.perf.deduplicate_location", false)
.put("mixin.perf.dynamic_entity_renderers", false)

View File

@ -2,13 +2,21 @@ package org.embeddedt.modernfix.forge.capability;
import net.minecraftforge.common.capabilities.ICapabilityProvider;
import org.embeddedt.modernfix.ModernFix;
import org.embeddedt.modernfix.forge.capability.analysis.CapabilityAnalysisResult;
import org.embeddedt.modernfix.forge.capability.analysis.CapabilityAnalyzer;
import org.embeddedt.modernfix.forge.capability.analysis.CapabilityRef;
import org.objectweb.asm.*;
import org.objectweb.asm.commons.GeneratorAdapter;
import org.objectweb.asm.commons.Method;
import java.lang.reflect.Modifier;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
@ -23,6 +31,7 @@ import static org.objectweb.asm.Opcodes.*;
* and performs direct dispatch instead of megamorphic virtual calls.
*/
public class CapabilityProviderDispatcherGenerator {
private static final String GENERATED_CLASSES_FOLDER = System.getProperty("modernfix.generatedCapabilityDispatcherClassDumpFolder", "");
private static final ConcurrentHashMap<List<Class<? extends ICapabilityProvider>>, MethodHandle> cache =
new ConcurrentHashMap<>();
@ -67,10 +76,27 @@ public class CapabilityProviderDispatcherGenerator {
}
private static MethodHandle generateClass(List<Class<? extends ICapabilityProvider>> providerTypes) {
ModernFix.LOGGER.debug("Generating capability dispatcher for types: [{}]", providerTypes.stream().map(Class::getName).collect(Collectors.joining(", ")));
try {
String className = "org.embeddedt.modernfix.forge.capability.CapabilityDispatcher$Generated$" + classCounter.incrementAndGet();
byte[] classBytes = generateClassBytes(className, providerTypes);
// Analyze each provider type
List<CapabilityAnalysisResult> analysisResults = new ArrayList<>(providerTypes.size());
for (Class<? extends ICapabilityProvider> type : providerTypes) {
CapabilityAnalysisResult result = CapabilityAnalyzer.analyze(type);
analysisResults.add(result);
}
int generatedClassId = classCounter.incrementAndGet();
String className = "org.embeddedt.modernfix.forge.capability.CapabilityDispatcher$Generated$" + generatedClassId;
ModernFix.LOGGER.debug("Generating capability dispatcher #{} for types: [{}]", () -> generatedClassId, () -> {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < providerTypes.size(); i++) {
if (i > 0) sb.append(", ");
sb.append(providerTypes.get(i).getName()).append(" -> ").append(formatAnalysisResult(analysisResults.get(i)));
}
return sb;
});
byte[] classBytes = generateClassBytes(className, providerTypes, analysisResults);
// Define the hidden class
MethodHandles.Lookup hiddenLookup = lookup.defineHiddenClass(
@ -79,6 +105,12 @@ public class CapabilityProviderDispatcherGenerator {
MethodHandles.Lookup.ClassOption.NESTMATE
);
if (!GENERATED_CLASSES_FOLDER.isBlank()) {
Path path = Paths.get(GENERATED_CLASSES_FOLDER, "generatedDispatcher" + generatedClassId + ".class");
Files.createDirectories(path.getParent());
Files.write(path, classBytes);
}
// Return a MethodHandle to the constructor
// Constructor signature: (ICapabilityProvider[])V
// The constructor is adapted to take an Object and return an ICapabilityProvider to match
@ -92,7 +124,7 @@ public class CapabilityProviderDispatcherGenerator {
}
}
private static byte[] generateClassBytes(String className, List<Class<? extends ICapabilityProvider>> providerTypes) {
private static byte[] generateClassBytes(String className, List<Class<? extends ICapabilityProvider>> providerTypes, List<CapabilityAnalysisResult> analysisResults) {
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
// Class declaration: implements ICapabilityProvider
@ -105,28 +137,36 @@ public class CapabilityProviderDispatcherGenerator {
new String[] { "net/minecraftforge/common/capabilities/ICapabilityProvider" }
);
// Compute field descriptors: use concrete type when possible for JIT devirtualization
String[] fieldDescs = new String[providerTypes.size()];
for (int i = 0; i < providerTypes.size(); i++) {
Class<? extends ICapabilityProvider> type = providerTypes.get(i);
fieldDescs[i] = (!type.isHidden() && Modifier.isPublic(type.getModifiers()))
? Type.getDescriptor(type) : ICAP_PROVIDER_DESC;
}
// Generate final fields for each provider
for (int i = 0; i < providerTypes.size(); i++) {
cw.visitField(
ACC_PRIVATE | ACC_FINAL,
"provider" + i,
ICAP_PROVIDER_DESC,
fieldDescs[i],
null,
null
).visitEnd();
}
// Generate constructor
generateConstructor(cw, className, providerTypes.size());
generateConstructor(cw, className, providerTypes.size(), fieldDescs);
// Generate getCapability method with sided parameter
generateGetCapabilityMethod(cw, className, providerTypes.size());
generateGetCapabilityMethod(cw, className, fieldDescs, analysisResults);
cw.visitEnd();
return cw.toByteArray();
}
private static void generateConstructor(ClassWriter cw, String className, int providerCount) {
private static void generateConstructor(ClassWriter cw, String className, int providerCount, String[] fieldDescs) {
Method constructor = Method.getMethod("void <init>(net.minecraftforge.common.capabilities.ICapabilityProvider[])");
GeneratorAdapter mg = new GeneratorAdapter(ACC_PUBLIC, constructor, null, null, cw);
@ -136,14 +176,18 @@ public class CapabilityProviderDispatcherGenerator {
// Unpack array into final fields
for (int i = 0; i < providerCount; i++) {
Type fieldType = Type.getType(fieldDescs[i]);
mg.loadThis(); // this
mg.loadArg(0); // array
mg.push(i); // index
mg.arrayLoad(Type.getType(ICAP_PROVIDER_DESC)); // array[i]
if (!fieldDescs[i].equals(ICAP_PROVIDER_DESC)) {
mg.checkCast(fieldType);
}
mg.putField(
Type.getObjectType(className.replace('.', '/')),
"provider" + i,
Type.getType(ICAP_PROVIDER_DESC)
fieldType
);
}
@ -151,7 +195,9 @@ public class CapabilityProviderDispatcherGenerator {
mg.endMethod();
}
private static void generateGetCapabilityMethod(ClassWriter cw, String className, int providerCount) {
private static void generateGetCapabilityMethod(ClassWriter cw, String className, String[] fieldDescs, List<CapabilityAnalysisResult> analysisResults) {
int providerCount = fieldDescs.length;
// Method: <T> LazyOptional<T> getCapability(Capability<T>, Direction)
MethodVisitor mv = cw.visitMethod(
ACC_PUBLIC,
@ -168,15 +214,45 @@ public class CapabilityProviderDispatcherGenerator {
Label endLabel = new Label();
for (int i = 0; i < providerCount; i++) {
CapabilityAnalysisResult analysis = analysisResults.get(i);
Label nextLabel = new Label();
// AlwaysEmpty: skip code generation for this provider entirely
if (analysis instanceof CapabilityAnalysisResult.AlwaysEmpty) {
continue;
}
// KnownCapabilities: emit guard checks before dispatch
if (analysis instanceof CapabilityAnalysisResult.KnownCapabilities known
&& known.capabilities().size() <= 5) {
if (known.capabilities().size() == 1) {
// Single cap: if (cap != KNOWN_CAP) goto nextProvider
CapabilityRef ref = known.capabilities().iterator().next();
mv.visitVarInsn(ALOAD, 1); // cap parameter
mv.visitFieldInsn(GETSTATIC, ref.owner(), ref.fieldName(), CAPABILITY_DESC);
mv.visitJumpInsn(IF_ACMPNE, nextLabel);
} else {
// Multiple caps: check each, jump to callProvider on match
Label callProvider = new Label();
for (CapabilityRef ref : known.capabilities()) {
mv.visitVarInsn(ALOAD, 1); // cap parameter
mv.visitFieldInsn(GETSTATIC, ref.owner(), ref.fieldName(), CAPABILITY_DESC);
mv.visitJumpInsn(IF_ACMPEQ, callProvider);
}
// No match, skip this provider
mv.visitJumpInsn(GOTO, nextLabel);
mv.visitLabel(callProvider);
}
}
// Indeterminate: no guard, fall through to dispatch
// LazyOptional<T> result = this.providerN.getCapability(cap, side);
mv.visitVarInsn(ALOAD, 0); // this
mv.visitFieldInsn(
GETFIELD,
className.replace('.', '/'),
"provider" + i,
ICAP_PROVIDER_DESC
fieldDescs[i]
);
mv.visitVarInsn(ALOAD, 1); // cap parameter
mv.visitVarInsn(ALOAD, 2); // side parameter
@ -211,9 +287,6 @@ public class CapabilityProviderDispatcherGenerator {
mv.visitInsn(ARETURN);
mv.visitLabel(nextLabel);
if (i < providerCount - 1) {
mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
}
}
// If no provider returned a capability, return empty
@ -231,29 +304,16 @@ public class CapabilityProviderDispatcherGenerator {
mv.visitEnd();
}
/**
* Creates an instance of the optimized dispatcher for the given providers.
*/
public static ICapabilityProvider createDispatcher(ICapabilityProvider[] providers) {
try {
MethodHandle constructor = getOrGenerateConstructor(providers);
return (ICapabilityProvider) constructor.invoke(providers);
} catch (Throwable e) {
throw new RuntimeException("Failed to create capability dispatcher instance", e);
private static String formatAnalysisResult(CapabilityAnalysisResult result) {
if (result instanceof CapabilityAnalysisResult.AlwaysEmpty) {
return "always empty (skipped)";
} else if (result instanceof CapabilityAnalysisResult.KnownCapabilities known) {
return "known caps: " + known.capabilities().stream()
.map(ref -> ref.owner() + "#" + ref.fieldName())
.collect(Collectors.joining(", "));
} else if (result instanceof CapabilityAnalysisResult.Indeterminate ind) {
return "indeterminate (" + ind.reason() + ")";
}
}
/**
* Clears the cache of generated classes. Use with caution.
*/
public static void clearCache() {
cache.clear();
}
/**
* Returns the number of cached dispatcher classes.
*/
public static int getCacheSize() {
return cache.size();
return result.toString();
}
}

View File

@ -0,0 +1,23 @@
package org.embeddedt.modernfix.forge.capability.analysis;
import java.util.Set;
/**
* Result of analyzing a capability provider's {@code getCapability} bytecode.
*/
public sealed interface CapabilityAnalysisResult {
/**
* The provider can only return non-empty for these specific capabilities.
*/
record KnownCapabilities(Set<CapabilityRef> capabilities) implements CapabilityAnalysisResult {}
/**
* The provider always returns {@code LazyOptional.empty()}.
*/
record AlwaysEmpty() implements CapabilityAnalysisResult {}
/**
* Analysis could not determine the set of capabilities; fall back to unguarded dispatch.
*/
record Indeterminate(String reason) implements CapabilityAnalysisResult {}
}

View File

@ -0,0 +1,392 @@
package org.embeddedt.modernfix.forge.capability.analysis;
import net.minecraft.core.Direction;
import net.minecraftforge.common.capabilities.Capability;
import net.minecraftforge.common.capabilities.ICapabilityProvider;
import org.embeddedt.modernfix.ModernFix;
import org.embeddedt.modernfix.core.ModernFixMixinPlugin;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.*;
import org.objectweb.asm.tree.analysis.Analyzer;
import org.objectweb.asm.tree.analysis.AnalyzerException;
import org.objectweb.asm.tree.analysis.Frame;
import org.objectweb.asm.tree.analysis.SourceValue;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* Analyzes {@code getCapability} bytecode to determine which capabilities a provider handles.
*/
public class CapabilityAnalyzer {
private static final ConcurrentHashMap<Class<?>, CapabilityAnalysisResult> cache = new ConcurrentHashMap<>();
private static final String CAPABILITY_INTERNAL = "net/minecraftforge/common/capabilities/Capability";
private static final String CAPABILITY_DESC = "Lnet/minecraftforge/common/capabilities/Capability;";
private static final String LAZY_OPTIONAL_INTERNAL = "net/minecraftforge/common/util/LazyOptional";
private static final String DIRECTION_DESC = "Lnet/minecraft/core/Direction;";
private static final String LAZY_OPTIONAL_DESC = "Lnet/minecraftforge/common/util/LazyOptional;";
private static final String GET_CAPABILITY_DESC = "(" + CAPABILITY_DESC + DIRECTION_DESC + ")" + LAZY_OPTIONAL_DESC;
private static final String ICAP_PROVIDER_INTERNAL = "net/minecraftforge/common/capabilities/ICapabilityProvider";
public static CapabilityAnalysisResult analyze(Class<? extends ICapabilityProvider> clazz) {
if (!ModernFixMixinPlugin.instance.isOptionEnabled("perf.faster_capabilities.bytecode_analysis.CapabilityAnalyzer")) {
return new CapabilityAnalysisResult.Indeterminate("bytecode analysis disabled");
}
CapabilityAnalysisResult result = cache.get(clazz);
if (result != null) return result;
result = doAnalyzeSafe(clazz);
CapabilityAnalysisResult existing = cache.putIfAbsent(clazz, result);
return existing != null ? existing : result;
}
private static CapabilityAnalysisResult doAnalyzeSafe(Class<?> clazz) {
try {
return doAnalyze(clazz);
} catch (Exception e) {
ModernFix.LOGGER.debug("Capability analysis failed for {}: {}", clazz.getName(), e.getMessage());
return new CapabilityAnalysisResult.Indeterminate("analysis exception: " + e.getMessage());
}
}
private static CapabilityAnalysisResult doAnalyze(Class<?> clazz) throws IOException, AnalyzerException {
// Find the class that actually declares getCapability via reflection
Class<?> declaringClass;
try {
declaringClass = clazz.getMethod("getCapability", Capability.class, Direction.class).getDeclaringClass();
} catch (NoSuchMethodException e) {
return new CapabilityAnalysisResult.AlwaysEmpty();
}
if (declaringClass.getName().replace('.', '/').equals(ICAP_PROVIDER_INTERNAL)) {
return new CapabilityAnalysisResult.AlwaysEmpty();
}
String declaringClassName = declaringClass.getName().replace('.', '/');
ClassNode declaringClassNode = readClass(declaringClass);
if (declaringClassNode == null) {
return new CapabilityAnalysisResult.Indeterminate("cannot read bytecode for " + declaringClass.getName());
}
MethodNode getCapMethod = findGetCapabilityMethod(declaringClassNode);
if (getCapMethod == null) {
return new CapabilityAnalysisResult.Indeterminate("method not found in bytecode for " + declaringClass.getName());
}
// Run the source analysis
CapabilitySourceInterpreter interpreter = new CapabilitySourceInterpreter();
Analyzer<SourceValue> analyzer = new Analyzer<>(interpreter);
Frame<SourceValue>[] frames = analyzer.analyze(declaringClassName, getCapMethod);
// Build if-guard map: maps instruction indices to CapabilityRef for guarded regions
List<GuardRegion> guardRegions = findGuardRegions(getCapMethod, frames);
// Classify each ARETURN
InsnList instructions = getCapMethod.instructions;
Set<CapabilityRef> knownCaps = new HashSet<>();
boolean hasIndeterminate = false;
String indeterminateReason = null;
for (int i = 0; i < instructions.size(); i++) {
AbstractInsnNode insn = instructions.get(i);
if (insn.getOpcode() != Opcodes.ARETURN) continue;
Frame<SourceValue> frame = frames[i];
if (frame == null) continue; // dead code
SourceValue topOfStack = frame.getStack(frame.getStackSize() - 1);
ReturnClassification classification = classifyReturnSources(
topOfStack, interpreter, i, guardRegions, clazz, instructions);
if (classification instanceof ReturnClassification.Known known) {
knownCaps.addAll(known.caps);
} else if (classification instanceof ReturnClassification.Unknown unknown) {
hasIndeterminate = true;
indeterminateReason = unknown.reason;
}
// Empty: skip
}
if (hasIndeterminate) {
CapabilityAnalysisResult result = new CapabilityAnalysisResult.Indeterminate(indeterminateReason);
ModernFix.LOGGER.debug("Capability analysis for {}: {}", clazz.getName(), result);
return result;
}
if (knownCaps.isEmpty()) {
ModernFix.LOGGER.debug("Capability analysis for {}: AlwaysEmpty", clazz.getName());
return new CapabilityAnalysisResult.AlwaysEmpty();
}
CapabilityAnalysisResult result = new CapabilityAnalysisResult.KnownCapabilities(Set.copyOf(knownCaps));
ModernFix.LOGGER.debug("Capability analysis for {}: {}", clazz.getName(), result);
return result;
}
private static ReturnClassification classifyReturnSources(
SourceValue topOfStack,
CapabilitySourceInterpreter interpreter,
int returnIndex,
List<GuardRegion> guardRegions,
Class<?> originalClass,
InsnList instructions) {
Set<CapabilityRef> caps = new HashSet<>();
List<AbstractInsnNode> unknownSources = new ArrayList<>();
String unknownReason = null;
for (AbstractInsnNode source : topOfStack.insns) {
ReturnClassification sourceClassification = classifySingleSource(
source, interpreter, originalClass);
if (sourceClassification instanceof ReturnClassification.Unknown unknown) {
unknownSources.add(source);
unknownReason = unknown.reason;
} else if (sourceClassification instanceof ReturnClassification.Known known) {
caps.addAll(known.caps);
}
}
// If any source is unknown, try the guard region fallback before giving up
if (!unknownSources.isEmpty()) {
// Check if the return itself is in a guard region
for (GuardRegion guard : guardRegions) {
if (returnIndex > guard.guardIndex && returnIndex < guard.targetIndex) {
return new ReturnClassification.Known(Set.of(guard.capabilityRef));
}
}
// Also check if each unknown source instruction is inside a guard region.
// This handles ternary patterns where both branches merge into a single
// ARETURN after the guard, but the unknown value was produced inside it.
boolean allResolved = true;
for (AbstractInsnNode unknownSource : unknownSources) {
int sourceIndex = instructions.indexOf(unknownSource);
boolean resolved = false;
for (GuardRegion guard : guardRegions) {
if (sourceIndex > guard.guardIndex && sourceIndex < guard.targetIndex) {
caps.add(guard.capabilityRef);
resolved = true;
break;
}
}
if (!resolved) {
allResolved = false;
break;
}
}
if (!allResolved) {
return new ReturnClassification.Unknown(unknownReason);
}
}
if (caps.isEmpty()) {
return ReturnClassification.EMPTY;
}
return new ReturnClassification.Known(caps);
}
private static ReturnClassification classifySingleSource(
AbstractInsnNode source,
CapabilitySourceInterpreter interpreter,
Class<?> originalClass) {
if (source instanceof MethodInsnNode methodInsn) {
// Case: Capability.orEmpty(...)
if (methodInsn.getOpcode() == Opcodes.INVOKEVIRTUAL
&& methodInsn.owner.equals(CAPABILITY_INTERNAL)
&& methodInsn.name.equals("orEmpty")) {
return classifyOrEmptyCall(methodInsn, interpreter);
}
// Case: LazyOptional.empty()
if (methodInsn.getOpcode() == Opcodes.INVOKESTATIC
&& methodInsn.owner.equals(LAZY_OPTIONAL_INTERNAL)
&& methodInsn.name.equals("empty")) {
return ReturnClassification.EMPTY;
}
// Case: LazyOptional.of(null)
if (methodInsn.getOpcode() == Opcodes.INVOKESTATIC
&& methodInsn.owner.equals(LAZY_OPTIONAL_INTERNAL)
&& methodInsn.name.equals("of")) {
List<SourceValue> args = interpreter.getCallArguments(methodInsn);
if (!args.isEmpty()) {
SourceValue arg = args.get(0);
if (arg.insns.size() == 1
&& arg.insns.iterator().next().getOpcode() == Opcodes.ACONST_NULL) {
return ReturnClassification.EMPTY;
}
}
}
// Case: super.getCapability(...)
if (methodInsn.getOpcode() == Opcodes.INVOKESPECIAL
&& methodInsn.name.equals("getCapability")
&& methodInsn.desc.equals(GET_CAPABILITY_DESC)) {
return classifySuperDelegation(originalClass);
}
}
return new ReturnClassification.Unknown("unclassified source: " + source.getClass().getSimpleName()
+ " opcode=" + source.getOpcode());
}
private static ReturnClassification classifyOrEmptyCall(
MethodInsnNode methodInsn, CapabilitySourceInterpreter interpreter) {
List<SourceValue> args = interpreter.getCallArguments(methodInsn);
if (args.isEmpty()) {
return new ReturnClassification.Unknown("orEmpty call with no recorded arguments");
}
// arg 0 is the receiver (the Capability instance)
SourceValue receiver = args.get(0);
for (AbstractInsnNode recvSource : receiver.insns) {
if (recvSource instanceof FieldInsnNode fieldInsn
&& fieldInsn.getOpcode() == Opcodes.GETSTATIC
&& fieldInsn.desc.equals(CAPABILITY_DESC)) {
return new ReturnClassification.Known(
Set.of(new CapabilityRef(fieldInsn.owner, fieldInsn.name)));
}
}
return new ReturnClassification.Unknown("orEmpty receiver is not a static Capability field");
}
private static ReturnClassification classifySuperDelegation(Class<?> originalClass) {
Class<?> superClass = originalClass.getSuperclass();
if (superClass == null || superClass == Object.class) {
return ReturnClassification.EMPTY;
}
@SuppressWarnings("unchecked")
Class<? extends ICapabilityProvider> superProvider =
(Class<? extends ICapabilityProvider>) superClass;
CapabilityAnalysisResult superResult = analyze(superProvider);
if (superResult instanceof CapabilityAnalysisResult.KnownCapabilities known) {
return new ReturnClassification.Known(known.capabilities());
} else if (superResult instanceof CapabilityAnalysisResult.AlwaysEmpty) {
return ReturnClassification.EMPTY;
} else if (superResult instanceof CapabilityAnalysisResult.Indeterminate ind) {
return new ReturnClassification.Unknown("super delegation: " + ind.reason());
}
return ReturnClassification.EMPTY;
}
private static List<GuardRegion> findGuardRegions(MethodNode method, Frame<SourceValue>[] frames) {
List<GuardRegion> regions = new ArrayList<>();
InsnList instructions = method.instructions;
for (int i = 0; i < instructions.size(); i++) {
AbstractInsnNode insn = instructions.get(i);
int opcode = insn.getOpcode();
if (opcode != Opcodes.IF_ACMPEQ && opcode != Opcodes.IF_ACMPNE) continue;
Frame<SourceValue> frame = frames[i];
if (frame == null || frame.getStackSize() < 2) continue;
SourceValue val1 = frame.getStack(frame.getStackSize() - 2);
SourceValue val2 = frame.getStack(frame.getStackSize() - 1);
// Check if one traces to ALOAD 1 (cap parameter) and other to GETSTATIC Capability
CapabilityRef capRef = findCapRef(val1);
if (capRef == null) capRef = findCapRef(val2);
boolean hasParamLoad = isCapParamLoad(val1) || isCapParamLoad(val2);
if (capRef != null && hasParamLoad) {
JumpInsnNode jumpInsn = (JumpInsnNode) insn;
int targetIndex = instructions.indexOf(jumpInsn.label);
if (opcode == Opcodes.IF_ACMPNE) {
// if (cap != KNOWN_CAP) goto target -> the code between insn and target is for KNOWN_CAP
regions.add(new GuardRegion(capRef, i, targetIndex));
} else {
// IF_ACMPEQ: if (cap == KNOWN_CAP) goto target -> the target is the guarded code
// Find the end of the guarded region (next label or return)
// For simplicity, mark from target to the next unconditional branch or return
// TODO: that simplification may have edge cases
int endIndex = findGuardedRegionEnd(instructions, targetIndex);
regions.add(new GuardRegion(capRef, targetIndex, endIndex));
}
}
}
return regions;
}
private static int findGuardedRegionEnd(InsnList instructions, int startIndex) {
for (int i = startIndex; i < instructions.size(); i++) {
AbstractInsnNode insn = instructions.get(i);
int opcode = insn.getOpcode();
if (opcode == Opcodes.GOTO || opcode == Opcodes.ARETURN
|| opcode == Opcodes.RETURN || opcode == Opcodes.ATHROW) {
return i + 1;
}
}
return instructions.size();
}
private static CapabilityRef findCapRef(SourceValue sv) {
CapabilityRef ref = null;
for (AbstractInsnNode src : sv.insns) {
if (src instanceof FieldInsnNode fieldInsn
&& fieldInsn.getOpcode() == Opcodes.GETSTATIC
&& fieldInsn.desc.equals(CAPABILITY_DESC)) {
if (ref == null) {
ref = new CapabilityRef(fieldInsn.owner, fieldInsn.name);
} else if (!ref.owner().equals(fieldInsn.owner) || !ref.fieldName().equals(fieldInsn.name)) {
return null; // ambiguous: multiple different capability fields
}
} else {
return null; // non-capability source
}
}
return ref;
}
private static boolean isCapParamLoad(SourceValue sv) {
if (sv.insns.isEmpty()) return false;
for (AbstractInsnNode src : sv.insns) {
if (!(src instanceof VarInsnNode varInsn)
|| varInsn.getOpcode() != Opcodes.ALOAD
|| varInsn.var != 1) {
return false;
}
}
return true;
}
private static MethodNode findGetCapabilityMethod(ClassNode classNode) {
for (MethodNode method : classNode.methods) {
if (method.name.equals("getCapability") && method.desc.equals(GET_CAPABILITY_DESC)) {
return method;
}
}
return null;
}
private static ClassNode readClass(Class<?> clazz) throws IOException {
String resourcePath = "/" + clazz.getName().replace('.', '/') + ".class";
try (InputStream is = clazz.getResourceAsStream(resourcePath)) {
if (is == null) return null;
ClassReader reader = new ClassReader(is);
ClassNode node = new ClassNode();
reader.accept(node, 0);
return node;
}
}
private sealed interface ReturnClassification {
ReturnClassification EMPTY = new Empty();
record Known(Set<CapabilityRef> caps) implements ReturnClassification {}
record Empty() implements ReturnClassification {}
record Unknown(String reason) implements ReturnClassification {}
}
private record GuardRegion(CapabilityRef capabilityRef, int guardIndex, int targetIndex) {}
}

View File

@ -0,0 +1,14 @@
package org.embeddedt.modernfix.forge.capability.analysis;
/**
* Identifies a capability by the static field that holds it.
*
* @param owner internal class name (e.g. {@code net/minecraftforge/common/capabilities/ForgeCapabilities})
* @param fieldName field name (e.g. {@code ITEM_HANDLER})
*/
public record CapabilityRef(String owner, String fieldName) {
@Override
public String toString() {
return owner + "." + fieldName;
}
}

View File

@ -0,0 +1,49 @@
package org.embeddedt.modernfix.forge.capability.analysis;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.analysis.SourceInterpreter;
import org.objectweb.asm.tree.analysis.SourceValue;
import java.util.*;
/**
* Extends {@link SourceInterpreter} with two enhancements:
* <ul>
* <li>Propagates source values through copy operations (ALOAD/ASTORE), so that
* {@code result = cap.orEmpty(...); return result;} traces back to the INVOKEVIRTUAL.</li>
* <li>Records argument {@link SourceValue}s for each call instruction, so we can later
* inspect the receiver/arguments of {@code orEmpty()} calls.</li>
* </ul>
*/
public class CapabilitySourceInterpreter extends SourceInterpreter {
private final Map<AbstractInsnNode, List<SourceValue>> callArguments = new HashMap<>();
public CapabilitySourceInterpreter() {
super(ASM9);
}
/**
* Returns the recorded argument SourceValues for a given call instruction.
*/
public List<SourceValue> getCallArguments(AbstractInsnNode insn) {
return callArguments.getOrDefault(insn, Collections.emptyList());
}
@Override
public SourceValue copyOperation(AbstractInsnNode insn, SourceValue value) {
// Propagate sources through loads/stores instead of creating a new source.
// However, if the value has no sources (e.g. a method parameter), fall back to
// the default behavior so the ALOAD instruction itself is recorded as the source.
if (value.insns.isEmpty()) {
return super.copyOperation(insn, value);
}
return value;
}
@Override
public SourceValue naryOperation(AbstractInsnNode insn, List<? extends SourceValue> values) {
callArguments.put(insn, new ArrayList<>(values));
return super.naryOperation(insn, values);
}
}