build: compile directly for Java 8

Keep modern java out of that for now, it make build script too complicated as long as scala still here to force J8 path
This commit is contained in:
Algent committed 2026-09-15 22:48:00 +02:00
1 parent d3f1b561a3
commit 12eb146d02
14 files changed
+376 -330

No files matched your search

+1 -3
View File
@@ -26,9 +26,7 @@ jobs:
uses: actions/setup-java@v5
with:
distribution: zulu
java-version: |
8
25
java-version: 8
- name: Set up Gradle
uses: gradle/actions/setup-gradle@v5
+1 -2
View File
@@ -6,8 +6,7 @@ An API for dynamically handling different functional parts in the one block spac
Requirements:
* The latest version of CCL (files.minecraftforge.net/CodeChickenLib/)
* Forge
* JDK 8 and JDK 25. The mod targets Java 8, but a small set of helpers is compiled at Java 21 by the
`compileModernJava` task and downgraded to Java 8, so both toolchains must be resolvable to build.
* JDK 8. The mod compiles directly for Java 8.
* A scala compatible IDE and project (For eclipse, download the scala eclipse plugin and right click on the project -> Add Scala Nature).
Most of the codebase is Java now; the Java sources live under `src/main/scala` so they can be joint-compiled
with the Scala files that remain.
+1 -46
View File
@@ -25,52 +25,7 @@ tasks.test {
}
}
// Scope modern syntax to these helpers; retained Scala and joint Java stay on Java 8.
apply plugin: xyz.wagyourtail.jvmdg.gradle.JVMDowngraderPlugin
// Single source of truth for the modern-syntax helpers. These are compiled by compileModernJava
// and must be excluded from compileScala; keep one list, because a file excluded from the joint
// Scala compile but absent from this list is never compiled at all, and nothing reports it.
def modernJavaPaths = [
'codechicken/multipart/asm/StackAnalyserLogic.java',
'codechicken/multipart/asm/JavaTraitRegistration.java',
'codechicken/multipart/asm/ClassInfoLookup.java',
'codechicken/multipart/asm/ScalaSignatureParser.java',
'codechicken/microblock/HollowMicroblockTraitLogic.java',
'codechicken/microblock/PostMicroblockClientLogic.java',
'codechicken/microblock/HollowMicroblockClientLogic.java',
'codechicken/microblock/TMicroOcclusionLogic.java',
]
def modernJavaSources = sourceSets.main.scala.matching { include modernJavaPaths }
def legacyScala = tasks.named('compileScala') {
tasks.named('compileScala') {
// Zinc does not invalidate joint-compiled Java annotations when Tags.VERSION changes.
scalaCompileOptions.force = true
// Scalac reads the Java declaration without asking Java 8 javac to compile its body.
exclude modernJavaPaths
scalaCompileOptions.additionalParameters = ['-sourcepath', file('src/main/scala').absolutePath]
inputs.files(modernJavaSources).withPropertyName('modernJavaDeclarations').withPathSensitivity(PathSensitivity.RELATIVE)
}
def compileModernJava = tasks.register('compileModernJava', JavaCompile) {
source(modernJavaSources)
classpath = sourceSets.main.compileClasspath + files(
tasks.named('compileJava').flatMap { it.destinationDirectory },
legacyScala.flatMap { it.destinationDirectory })
destinationDirectory = layout.buildDirectory.dir('classes/java/modern')
javaCompiler = javaToolchains.compilerFor { languageVersion = JavaLanguageVersion.of(25) }
options.release = 21
options.encoding = 'UTF-8'
}
def downgradeModernJava = tasks.register('downgradeModernJava', xyz.wagyourtail.jvmdg.gradle.task.files.DowngradeFiles) {
inputCollection = files(compileModernJava.flatMap { it.destinationDirectory })
classpath = compileModernJava.get().classpath
downgradeTo = JavaVersion.VERSION_1_8
multiReleaseOriginal = false
multiReleaseVersions = [] as Set
// The bundled plugin otherwise declares outputs only when inputs already exist.
outputs.dirs(outputMap.values())
}
sourceSets.main.output.classesDirs.from(downgradeModernJava.map { it.outputCollection })
tasks.named('classes') { dependsOn(downgradeModernJava) }
-8
View File
@@ -205,14 +205,6 @@ These are accepted once for the migration, not repeated for every otherwise equi
the retained getter and old Iguana reflection. Its name, type and visibility remain unchanged.
- TickScheduler's private Scala scheduler types become package-private static nested Java classes, retaining their
binary names.
- The modern-syntax helpers that have nested or anonymous classes carry `xyz.wagyourtail.jvmdg.j11.NestHost` and
`NestMembers` annotations on 23 packaged classes, recording nest relationships Java 8 class files cannot express.
These annotation types are deliberately not shipped and JVM Downgrader is not a runtime dependency: the packaged
classes contain no executable Downgrader API reference, and reflection skips an annotation whose type is absent
instead of throwing, so `getAnnotations()` on these classes returns an empty array on a Java 8 runtime. Forge and
FMP's own scanners read annotation tables through ASM without loading the types. Do not shade the Downgrader API
jar to resolve them; that would place FMP classes in a namespace it does not own and duplicate types other
installed mods already provide.
## Updating this ledger
+6 -19
View File
@@ -10,29 +10,16 @@ These constraints explain the retained build and runtime machinery. Consumer mig
modern-runtime policy spans Java 17–26; validate packaged releases on the selected supported runtime. Equal
throughput across Java versions is not required. The deobfuscated Forge test runner uses Java 8; the current
modern dedicated-server run tasks are not a substitute for packaged validation.
- Keep `enableModernJavaSyntax = false` while Scala 2.11.5 remains. The convention plugin's global JVM Downgrader
mode forces a modern toolchain that Scala cannot run on. With gtnhgradle 2.0.29, forcing Java 8 also conflicts
with the mode's multi-release version constraints. Revisit the supported global mode after Scala removal,
including an explicit stubs-provider decision; `gtnhlib` adds a runtime dependency and shading is rejected in
[DIVERGENCES.md](DIVERGENCES.md).
- Normal Java and joint Scala/Java compilation use Java 8. `modernJavaPaths` in `build.gradle` is the single list
that both excludes modern helpers from joint javac and includes them in `compileModernJava`. Scalac resolves
their declarations through `-sourcepath`; JDK 25 compiles their bodies with `--release 21` after Scala compilation.
Only `downgradeModernJava`'s Java 8 output joins main outputs, tests, Forge and packaged jars.
- Keep declaration inputs and downgrade output directories declared to Gradle. A modern-source exclusion without
a matching inclusion can silently omit a class. Helpers importing retained Scala models cannot simply move to
`src/main/java`: `compileJava` runs before those Scala types exist. Retire per-file routing when the dependency
cycle permits a source-set boundary, and remove the special routing after Scala removal.
- Keep `enableModernJavaSyntax = false` while Scala 2.11.5 remains. Normal Java and joint Scala/Java compilation
use Java 8. Helpers importing retained Scala models belong in the joint `src/main/scala` source set because
`compileJava` runs before those Scala types exist.
- `scalaCompileOptions.force = true` prevents stale `Tags.VERSION` constants in joint-compiled Java `@Mod`
annotations. Zinc otherwise misses those consumers when the generated constant changes. Verify all five mod
annotations in dev/release artifacts against their artifact version; replace the guard only with equivalent coverage.
- Modernize bounded helpers only after checking declaration visibility, compile order, ABI and generated output.
Java 21 is the validated source ceiling. Records/sealed types are not automatic replacements for ABI-sensitive
models. New library APIs may need runtime stubs even when syntax downgrades successfully.
- Keep registered trait inputs and compatibility facades conservative. Moving `RedstoneInteractions$` or
`BlockMultipart` pulls transformer-sensitive dependencies; `RenderPartResolver` is needed by joint compilation
before the modern stage. Bootstrap and mixin source sets are not downgraded by this task. The explicit
`Microblock` cast in `PostMicroblockTraitLogic` is required by the retained Scala declaration.
before Java helpers that use it. The explicit `Microblock` cast in `PostMicroblockTraitLogic` is required by the
retained Scala declaration.
- GTNHLib is compile-only here. Its fastutil compile classpath does not grant a runtime dependency. Declare that
dependency explicitly before using fastutil in ordinary code; require profiling evidence for such a change.
@@ -106,7 +93,7 @@ mechanical ports. Coverage percentages and signature counts alone are insufficie
A Scala-to-Java replacement needs clean verification: Zinc can retain the removed Scala class and package it
instead of exercising the new source. Stop the daemon if Windows holds build outputs open. Verify packaged
class version 52, unexpected downgrader API references and mod-version freshness, including after the release commit.
class version 52 and mod-version freshness, including after the release commit.
JUnit reports are under `build/test-results/test/` and `run/server/junit-out/`; inspect counts, failures, errors and
skips. The Forge task rejects missing/failed reports. Keep that CI gate required. Generated dumps are under
+1 -22
View File
@@ -39,34 +39,13 @@ remoteMappings = https\://raw.githubusercontent.com/MinecraftForge/FML/1.7.10/co
# `./gradlew runClient --username=AnotherPlayer`, or configuring this command in your IDE.
developmentEnvironmentUserName = Developer
# Enables modern Java syntax support. Valid values:
# - false: No modern syntax, Java 8 only
# - jabel: Jabel syntax-only support, compiles to J8 bytecode
# - jvmDowngrader: Full modern Java via JVM Downgrader (syntax + stdlib APIs)
# - modern: Native modern Java bytecode, no downgrading
# Retained Scala and Java sources compile directly for Java 8.
enableModernJavaSyntax = false
# If set, ignores the above setting and compiles with the given toolchain. This may cause unexpected issues,
# and should *not* be used in most situations. -1 disables this.
# forceToolchainVersion = -1
# Target JVM version for JVM Downgrader bytecode downgrading.
# Only used when enableModernJavaSyntax = jvmDowngrader
# downgradeTargetVersion = 8
# Comma-separated list of Java versions for multi-release jar support (JVM Downgrader only).
# Classes will be available in META-INF/versions/N/ for each version N in this list.
# Default: "21,25" (J25+ gets native classes, J21-24 gets partial downgrade, J8-20 gets full downgrade).
# jvmDowngraderMultiReleaseVersions = 21,25
# Specifies how JVM Downgrader API stubs are provided. Options:
# - shade: Shade minimized stubs into the jar
# - gtnhlib: GTNHLib provides stubs at runtime (adds version constraint)
# - external: Another dependency provides stubs (no constraint, no warning)
# - (empty): Warning reminding you to configure stubs
# Note: 'shade' option requires you to verify license compliance, see: https://github.com/unimined/JvmDowngrader/blob/main/LICENSE.md
# jvmDowngraderStubsProvider =
# Enables injecting missing generics into the decompiled source code for a better coding experience.
# Turns most publicly visible List, Map, etc. into proper List<E>, Map<K, V> types.
enableGenericInjection = false
@@ -75,7 +75,8 @@ final class HollowMicroblockClientLogic {
int slot = part.getSlot();
int internalMask = 0;
switch (slot) {
case 0, 1 -> {
case 0:
case 1:
if (face) internalMask = 0x3c;
f.apply(pos, material, pass, new Cuboid6(d1, y1, d2, d2, y2, z2), 0x3b | internalMask);
f.apply(pos, material, pass, new Cuboid6(d1, y1, z1, d2, y2, d1), 0x37 | internalMask);
@@ -83,8 +84,9 @@ final class HollowMicroblockClientLogic {
f.apply(pos, material, pass, new Cuboid6(x1, y1, d1, d1, y2, d2), sideMask & 0x13 | 0xc | internalMask);
f.apply(pos, material, pass, new Cuboid6(x1, y1, d2, x2, y2, z2), sideMask & 0x3b | 4 | internalMask);
f.apply(pos, material, pass, new Cuboid6(x1, y1, z1, x2, y2, d1), sideMask & 0x37 | 8 | internalMask);
}
case 2, 3 -> {
break;
case 2:
case 3:
if (face) internalMask = 0x33;
f.apply(pos, material, pass, new Cuboid6(d2, d1, z1, x2, d2, z2), 0x2f | internalMask);
f.apply(pos, material, pass, new Cuboid6(x1, d1, z1, d1, d2, z2), 0x1f | internalMask);
@@ -102,8 +104,9 @@ final class HollowMicroblockClientLogic {
pass,
new Cuboid6(x1, y1, z1, d1, y2, z2),
sideMask & 0x1f | 0x20 | internalMask);
}
case 4, 5 -> {
break;
case 4:
case 5:
if (face) internalMask = 0xf;
f.apply(pos, material, pass, new Cuboid6(x1, d2, d1, x2, y2, d2), 0x3e | internalMask);
f.apply(pos, material, pass, new Cuboid6(x1, y1, d1, x2, d1, d2), 0x3d | internalMask);
@@ -111,8 +114,9 @@ final class HollowMicroblockClientLogic {
f.apply(pos, material, pass, new Cuboid6(x1, d1, z1, x2, d2, d1), sideMask & 0x34 | 3 | internalMask);
f.apply(pos, material, pass, new Cuboid6(x1, d2, z1, x2, y2, z2), sideMask & 0x3e | 1 | internalMask);
f.apply(pos, material, pass, new Cuboid6(x1, y1, z1, x2, d1, z2), sideMask & 0x3d | 2 | internalMask);
}
default -> throw new MatchError(slot);
break;
default:
throw new MatchError(slot);
}
}
@@ -42,8 +42,8 @@ final class HollowMicroblockTraitLogic {
}
// The second virtual lookup is observable when a part overrides tile().
TMultiPart center = ((TMultiPart) part).tile().partMap(6);
if (center instanceof ISidedHollowConnect connection) {
return connection.getHollowSize(part.getSlot());
if (center instanceof ISidedHollowConnect) {
return ((ISidedHollowConnect) center).getHollowSize(part.getSlot());
}
return 8;
}
@@ -60,15 +60,26 @@ final class HollowMicroblockTraitLogic {
double z1 = c.min.z;
double z2 = c.max.z;
int slot = part.getSlot();
Cuboid6[] boxes = switch (slot) {
case 0, 1 -> new Cuboid6[] { new Cuboid6(d2, y1, d1, x2, y2, d2), new Cuboid6(x1, y1, d1, d1, y2, d2),
new Cuboid6(x1, y1, d2, x2, y2, z2), new Cuboid6(x1, y1, z1, x2, y2, d1) };
case 2, 3 -> new Cuboid6[] { new Cuboid6(d1, d2, z1, d2, y2, z2), new Cuboid6(d1, y1, z1, d2, d1, z2),
new Cuboid6(d2, y1, z1, x2, y2, z2), new Cuboid6(x1, y1, z1, d1, y2, z2) };
case 4, 5 -> new Cuboid6[] { new Cuboid6(x1, d1, d2, x2, d2, z2), new Cuboid6(x1, d1, z1, x2, d2, d1),
new Cuboid6(x1, d2, z1, x2, y2, z2), new Cuboid6(x1, y1, z1, x2, d1, z2) };
default -> throw new MatchError(slot);
};
Cuboid6[] boxes;
switch (slot) {
case 0:
case 1:
boxes = new Cuboid6[] { new Cuboid6(d2, y1, d1, x2, y2, d2), new Cuboid6(x1, y1, d1, d1, y2, d2),
new Cuboid6(x1, y1, d2, x2, y2, z2), new Cuboid6(x1, y1, z1, x2, y2, d1) };
break;
case 2:
case 3:
boxes = new Cuboid6[] { new Cuboid6(d1, d2, z1, d2, y2, z2), new Cuboid6(d1, y1, z1, d2, d1, z2),
new Cuboid6(d2, y1, z1, x2, y2, z2), new Cuboid6(x1, y1, z1, d1, y2, z2) };
break;
case 4:
case 5:
boxes = new Cuboid6[] { new Cuboid6(x1, d1, d2, x2, d2, z2), new Cuboid6(x1, d1, z1, x2, d2, d1),
new Cuboid6(x1, d2, z1, x2, y2, z2), new Cuboid6(x1, y1, z1, x2, d1, z2) };
break;
default:
throw new MatchError(slot);
}
return JavaConversions.seqAsJavaList(sequence(boxes));
}
@@ -74,8 +74,8 @@ final class PostMicroblockClientLogic {
@Override
public BoxedUnit apply(TMultiPart other) {
if (other instanceof PostMicroblock post && !other.equals(part)) {
part.shrinkPost(post);
if (other instanceof PostMicroblock && !other.equals(part)) {
part.shrinkPost((PostMicroblock) other);
}
return BoxedUnit.UNIT;
}
@@ -9,9 +9,10 @@ final class TMicroOcclusionLogic {
private TMicroOcclusionLogic() {}
static boolean occlusionTest(TMicroOcclusion part, TMultiPart next) {
if (!(next instanceof TMicroOcclusion other)) {
if (!(next instanceof TMicroOcclusion)) {
return true;
}
TMicroOcclusion other = (TMicroOcclusion) next;
int shape1 = MicroOcclusion$.MODULE$.shapePriority(part.getSlot());
int shape2 = MicroOcclusion$.MODULE$.shapePriority(other.getSlot());
if (other.getSize() + part.getSize() > 8) {
@@ -191,7 +191,7 @@ final class ClassInfoLookup {
// Preserve the compiler's argument-count indexing, including its behavior for wide stack values.
StackEntry receiver = stack.peek(Type.getType(call.desc).getArgumentTypes().length);
if (!(receiver instanceof Load load) || !(load.e() instanceof This)) return Option.empty();
if (!(receiver instanceof Load) || !(((Load) receiver).e() instanceof This)) return Option.empty();
return ASMMixinCompiler$.MODULE$.getClassInfo(stack.owner().getInternalName()).superClass()
.flatMap(new AbstractFunction1<ClassInfo, Option<MethodInfo>>() {
@@ -189,7 +189,8 @@ final class JavaTraitRegistration {
InsnList instructions = method.instructions;
AbstractInsnNode instruction = instructions.getFirst();
while (instruction != null) {
if (instruction instanceof FieldInsnNode field) {
if (instruction instanceof FieldInsnNode) {
FieldInsnNode field = (FieldInsnNode) instruction;
if (instruction.getOpcode() == GETFIELD) instruction = replace(
instructions,
instruction,
@@ -208,7 +209,8 @@ final class JavaTraitRegistration {
fieldName(field.name) + "_$eq",
"(" + field.desc + ")V",
true));
} else if (instruction instanceof MethodInsnNode call) {
} else if (instruction instanceof MethodInsnNode) {
MethodInsnNode call = (MethodInsnNode) instruction;
if (instruction.getOpcode() == INVOKESPECIAL) {
if (ASMMixinCompiler$.MODULE$.getSuper(call, stack).isDefined())
instruction = replace(instructions, instruction, superInstruction(call));
@@ -284,12 +286,14 @@ final class JavaTraitRegistration {
StackAnalyser stack = new StackAnalyser(Type.getObjectType(input.name), source);
AbstractInsnNode instruction = constructor.instructions.getFirst();
while (instruction != null) {
if (instruction.getOpcode() == INVOKESPECIAL && instruction instanceof MethodInsnNode call) {
if (instruction.getOpcode() == INVOKESPECIAL && instruction instanceof MethodInsnNode) {
MethodInsnNode call = (MethodInsnNode) instruction;
if (Objects.equals(call.owner, input.superName) && Objects.equals(call.name, "<init>")) {
int argumentWidth = 0;
for (Type argument : Type.getArgumentTypes(call.desc)) argumentWidth += argument.getSize();
StackAnalyser.StackEntry receiver = stack.peek(argumentWidth);
if (receiver instanceof StackAnalyser.Load load && load.e() instanceof StackAnalyser.This) {
if (receiver instanceof StackAnalyser.Load
&& ((StackAnalyser.Load) receiver).e() instanceof StackAnalyser.This) {
removeRange(constructor.instructions, receiver.insn(), instruction);
return;
}
@@ -157,18 +157,22 @@ final class ScalaSignatureParser {
Bytes bytes = entry.bytes();
ByteCodeReader reader = bytes.reader();
byte id = entry.id();
return switch (id) {
case 1, 2 -> reader.readString(bytes.len());
case 3 -> sig.NoSymbol().full();
case 9, 10 -> {
switch (id) {
case 1:
case 2:
return reader.readString(bytes.len());
case 3:
return sig.NoSymbol().full();
case 9:
case 10:
String name = sig.evalS(reader.readNat());
if (bytes.pos() + bytes.len() > reader.pos()) {
name = sig.evalS(reader.readNat()) + "." + name;
}
yield name;
}
default -> throw new MatchError(Byte.valueOf(id));
};
return name;
default:
throw new MatchError(Byte.valueOf(id));
}
}
static List<?> evalList(ScalaSignature sig, ByteCodeReader reader) {
@@ -181,36 +185,61 @@ final class ScalaSignatureParser {
static Object eval(ScalaSignature sig, int index, Object entry, ByteCodeReader reader, byte id) {
// The Scala optimizer constructs case classes directly, without initializing their companions.
return switch (id) {
case 1, 2 -> sig.evalS(index);
case 3 -> sig.NoSymbol();
case 6 -> sig.new ClassSymbol(sig.evalS(reader.readNat()), sig.evalT(reader.readNat()), reader.readNat(),
reader.readNat());
case 7 -> sig.new ObjectSymbol(sig.evalS(reader.readNat()), sig.evalT(reader.readNat()), reader.readNat(),
reader.readNat());
case 8 -> sig.new MethodSymbol(sig.evalS(reader.readNat()), sig.evalT(reader.readNat()), reader.readNat(),
reader.readNat());
case 9, 10 -> sig.new ExternalSymbol(sig.evalS(index));
// NoPrefixType is treated as NoType in the reference.
case 11, 12 -> sig.NoType();
case 13 -> sig.new ThisType(sig.evalT(reader.readNat()));
case 14 -> sig.new SingleType(sig.evalT(reader.readNat()), sig.evalT(reader.readNat()));
// Retain the reference's parameterless interpretation of a bounded super type.
case 21, 48 -> sig.new ParameterlessType(sig.evalT(reader.readNat()));
case 25 -> sig.new BooleanLiteral(reader.readLong() != 0);
case 26 -> sig.new ByteLiteral((byte) reader.readLong());
case 27 -> sig.new ShortLiteral((short) reader.readLong());
case 28 -> sig.new CharLiteral((char) reader.readLong());
case 29 -> sig.new IntLiteral((int) reader.readLong());
case 30 -> sig.new LongLiteral(reader.readLong());
case 31 -> sig.new FloatLiteral(Float.intBitsToFloat((int) reader.readLong()));
case 32 -> sig.new DoubleLiteral(Double.longBitsToDouble(reader.readLong()));
case 33 -> sig.new StringLiteral(sig.evalS(reader.readNat()));
case 34 -> sig.NullLiteral();
case 35 -> sig.new TypeLiteral(sig.evalT(reader.readNat()));
case 36 -> sig.new EnumLiteral(sig.evalT(reader.readNat()));
default -> entry;
};
switch (id) {
case 1:
case 2:
return sig.evalS(index);
case 3:
return sig.NoSymbol();
case 6:
return sig.new ClassSymbol(sig.evalS(reader.readNat()), sig.evalT(reader.readNat()), reader.readNat(),
reader.readNat());
case 7:
return sig.new ObjectSymbol(sig.evalS(reader.readNat()), sig.evalT(reader.readNat()), reader.readNat(),
reader.readNat());
case 8:
return sig.new MethodSymbol(sig.evalS(reader.readNat()), sig.evalT(reader.readNat()), reader.readNat(),
reader.readNat());
case 9:
case 10:
return sig.new ExternalSymbol(sig.evalS(index));
case 11:
case 12: // NoPrefixType is treated as NoType in the reference.
return sig.NoType();
case 13:
return sig.new ThisType(sig.evalT(reader.readNat()));
case 14:
return sig.new SingleType(sig.evalT(reader.readNat()), sig.evalT(reader.readNat()));
case 21:
case 48: // Retain the reference's parameterless interpretation of a bounded super type.
return sig.new ParameterlessType(sig.evalT(reader.readNat()));
case 25:
return sig.new BooleanLiteral(reader.readLong() != 0);
case 26:
return sig.new ByteLiteral((byte) reader.readLong());
case 27:
return sig.new ShortLiteral((short) reader.readLong());
case 28:
return sig.new CharLiteral((char) reader.readLong());
case 29:
return sig.new IntLiteral((int) reader.readLong());
case 30:
return sig.new LongLiteral(reader.readLong());
case 31:
return sig.new FloatLiteral(Float.intBitsToFloat((int) reader.readLong()));
case 32:
return sig.new DoubleLiteral(Double.longBitsToDouble(reader.readLong()));
case 33:
return sig.new StringLiteral(sig.evalS(reader.readNat()));
case 34:
return sig.NullLiteral();
case 35:
return sig.new TypeLiteral(sig.evalT(reader.readNat()));
case 36:
return sig.new EnumLiteral(sig.evalT(reader.readNat()));
default:
return entry;
}
}
static <T> IndexedSeq<T> collect(ScalaSignature sig, int id) {
@@ -56,20 +56,18 @@ final class StackAnalyserLogic {
static Type constType(Const constant) {
Object value = constant.c();
return switch (value) {
case Byte ignored -> BYTE_TYPE;
case Short ignored -> SHORT_TYPE;
case Integer ignored -> INT_TYPE;
case Long ignored -> LONG_TYPE;
case Float ignored -> FLOAT_TYPE;
case Double ignored -> DOUBLE_TYPE;
case Character ignored -> CHAR_TYPE;
case Boolean ignored -> BOOLEAN_TYPE;
case String ignored -> getObjectType("java/lang/String");
case null -> getObjectType("java/lang/Object");
// The failure message rereads the virtual accessor after classification.
default -> throw new IllegalArgumentException("Unknown const " + constant.c());
};
if (value instanceof Byte) return BYTE_TYPE;
if (value instanceof Short) return SHORT_TYPE;
if (value instanceof Integer) return INT_TYPE;
if (value instanceof Long) return LONG_TYPE;
if (value instanceof Float) return FLOAT_TYPE;
if (value instanceof Double) return DOUBLE_TYPE;
if (value instanceof Character) return CHAR_TYPE;
if (value instanceof Boolean) return BOOLEAN_TYPE;
if (value instanceof String) return getObjectType("java/lang/String");
if (value == null) return getObjectType("java/lang/Object");
// The failure message rereads the virtual accessor after classification.
throw new IllegalArgumentException("Unknown const " + constant.c());
}
static void initialize(StackAnalyser a, Type owner, MethodNode method,
@@ -126,167 +124,256 @@ final class StackAnalyserLogic {
}
static void visitInsn(StackAnalyser a, AbstractInsnNode insn) {
switch (insn) {
case InsnNode ignored -> {
int op = insn.getOpcode();
switch (op) {
case ACONST_NULL -> a.push(new Const(null, insn));
case ICONST_M1 -> a.push(new Const(-1, insn));
case ICONST_0 -> a.push(new Const(0, insn));
case ICONST_1 -> a.push(new Const(1, insn));
case ICONST_2 -> a.push(new Const(2, insn));
case ICONST_3 -> a.push(new Const(3, insn));
case ICONST_4 -> a.push(new Const(4, insn));
case ICONST_5 -> a.push(new Const(5, insn));
case LCONST_0 -> a.push(new Const(0L, insn));
case LCONST_1 -> a.push(new Const(1L, insn));
case FCONST_0 -> a.push(new Const(0f, insn));
case FCONST_1 -> a.push(new Const(1f, insn));
case FCONST_2 -> a.push(new Const(2f, insn));
case DCONST_0 -> a.push(new Const(0d, insn));
case DCONST_1 -> a.push(new Const(1d, insn));
case POP -> rawPop(a);
case POP2 -> {
rawPop(a);
rawPop(a);
}
case DUP -> a.push(peek(a));
case DUP_X1 -> a.insert(2, peek(a));
case DUP_X2 -> a.insert(3, peek(a));
case DUP2 -> {
a.push(a.peek(1));
a.push(a.peek(1));
}
case DUP2_X1 -> {
a.insert(3, a.peek(1));
a.insert(3, peek(a));
}
case DUP2_X2 -> {
a.insert(4, a.peek(1));
a.insert(4, peek(a));
}
case SWAP -> a.push(a.pop(1));
// Preserve the reference's inferred types, including conversions to int reporting double.
case L2I, F2I, D2I -> a.push(new PrimitiveCast(pop(a), DOUBLE_TYPE, insn));
case I2L, F2L, D2L -> a.push(new PrimitiveCast(pop(a), LONG_TYPE, insn));
case I2F, L2F, D2F -> a.push(new PrimitiveCast(pop(a), FLOAT_TYPE, insn));
case I2D, L2D, F2D -> a.push(new PrimitiveCast(pop(a), DOUBLE_TYPE, insn));
case I2B -> a.push(new PrimitiveCast(pop(a), BYTE_TYPE, insn));
case I2C -> a.push(new PrimitiveCast(pop(a), CHAR_TYPE, insn));
case I2S -> a.push(new PrimitiveCast(pop(a), SHORT_TYPE, insn));
case ARRAYLENGTH -> a.push(new ArrayLength(pop(a), insn));
case ATHROW, MONITORENTER, MONITOREXIT -> pop(a);
default -> {
if (op >= IALOAD && op <= SALOAD) {
a.push(new ArrayLoad(pop(a), pop(a), insn));
} else if (op >= IASTORE && op <= SASTORE) {
pop(a);
pop(a);
pop(a);
} else if (op >= IADD && op <= DREM || op >= ISHL && op <= LXOR || op >= LCMP && op <= DCMPG) {
a.push(new BinaryOp(op, pop(a), pop(a), insn));
} else if (op >= INEG && op <= DNEG) {
a.push(new UnaryOp(op, pop(a), insn));
} else if (op >= IRETURN && op <= ARETURN) {
pop(a);
}
}
}
}
case IntInsnNode intInsn -> {
switch (insn.getOpcode()) {
case BIPUSH -> a.push(new Const((byte) intInsn.operand, insn));
case SIPUSH -> a.push(new Const((short) intInsn.operand, insn));
default -> throw new MatchError(insn.getOpcode());
}
}
case LdcInsnNode ldc -> {
if (insn.getOpcode() != LDC) throw new MatchError(insn.getOpcode());
a.push(new Const(ldc.cst, insn));
}
case VarInsnNode varInsn -> {
int op = insn.getOpcode();
if (op >= ILOAD && op <= ALOAD) {
a.push(new Load(a.locals().apply(varInsn.var), insn));
} else if (op >= ISTORE && op <= ASTORE) {
a.setL(varInsn.var, new Store(pop(a), insn));
} else {
throw new MatchError(op);
}
}
case IincInsnNode incInsn -> {
if (insn.getOpcode() != IINC) throw new MatchError(insn.getOpcode());
a.setL(
incInsn.var,
new Store(
new BinaryOp(
IINC,
new Const(incInsn.incr, insn),
new Load(a.locals().apply(incInsn.var), insn),
insn),
insn));
}
case JumpInsnNode ignored -> {
int op = insn.getOpcode();
if (op >= IFEQ && op <= IFLE || op == IFNULL || op == IFNONNULL) {
if (insn instanceof InsnNode) {
int op = insn.getOpcode();
switch (op) {
case ACONST_NULL:
a.push(new Const(null, insn));
break;
case ICONST_M1:
a.push(new Const(-1, insn));
break;
case ICONST_0:
a.push(new Const(0, insn));
break;
case ICONST_1:
a.push(new Const(1, insn));
break;
case ICONST_2:
a.push(new Const(2, insn));
break;
case ICONST_3:
a.push(new Const(3, insn));
break;
case ICONST_4:
a.push(new Const(4, insn));
break;
case ICONST_5:
a.push(new Const(5, insn));
break;
case LCONST_0:
a.push(new Const(0L, insn));
break;
case LCONST_1:
a.push(new Const(1L, insn));
break;
case FCONST_0:
a.push(new Const(0f, insn));
break;
case FCONST_1:
a.push(new Const(1f, insn));
break;
case FCONST_2:
a.push(new Const(2f, insn));
break;
case DCONST_0:
a.push(new Const(0d, insn));
break;
case DCONST_1:
a.push(new Const(1d, insn));
break;
case POP:
rawPop(a);
break;
case POP2:
rawPop(a);
rawPop(a);
break;
case DUP:
a.push(peek(a));
break;
case DUP_X1:
a.insert(2, peek(a));
break;
case DUP_X2:
a.insert(3, peek(a));
break;
case DUP2:
a.push(a.peek(1));
a.push(a.peek(1));
break;
case DUP2_X1:
a.insert(3, a.peek(1));
a.insert(3, peek(a));
break;
case DUP2_X2:
a.insert(4, a.peek(1));
a.insert(4, peek(a));
break;
case SWAP:
a.push(a.pop(1));
break;
// Preserve the reference's inferred types, including conversions to int reporting double.
case L2I:
case F2I:
case D2I:
a.push(new PrimitiveCast(pop(a), DOUBLE_TYPE, insn));
break;
case I2L:
case F2L:
case D2L:
a.push(new PrimitiveCast(pop(a), LONG_TYPE, insn));
break;
case I2F:
case L2F:
case D2F:
a.push(new PrimitiveCast(pop(a), FLOAT_TYPE, insn));
break;
case I2D:
case L2D:
case F2D:
a.push(new PrimitiveCast(pop(a), DOUBLE_TYPE, insn));
break;
case I2B:
a.push(new PrimitiveCast(pop(a), BYTE_TYPE, insn));
break;
case I2C:
a.push(new PrimitiveCast(pop(a), CHAR_TYPE, insn));
break;
case I2S:
a.push(new PrimitiveCast(pop(a), SHORT_TYPE, insn));
break;
case ARRAYLENGTH:
a.push(new ArrayLength(pop(a), insn));
break;
case ATHROW:
case MONITORENTER:
case MONITOREXIT:
pop(a);
} else if (op >= IF_ICMPEQ && op <= IF_ACMPNE) {
pop(a);
pop(a);
} else if (op == JSR) {
a.push(new ReturnAddress(insn));
} else if (op != GOTO) {
throw new MatchError(op);
}
}
case TableSwitchInsnNode ignored -> pop(a);
case LookupSwitchInsnNode ignored -> pop(a);
case FieldInsnNode field -> {
switch (insn.getOpcode()) {
case GETSTATIC -> a.push(new GetField(null, field, insn));
case PUTSTATIC -> pop(a);
case GETFIELD -> a.push(new GetField(pop(a), field, insn));
case PUTFIELD -> {
break;
default:
if (op >= IALOAD && op <= SALOAD) {
a.push(new ArrayLoad(pop(a), pop(a), insn));
} else if (op >= IASTORE && op <= SASTORE) {
pop(a);
pop(a);
pop(a);
} else if (op >= IADD && op <= DREM || op >= ISHL && op <= LXOR || op >= LCMP && op <= DCMPG) {
a.push(new BinaryOp(op, pop(a), pop(a), insn));
} else if (op >= INEG && op <= DNEG) {
a.push(new UnaryOp(op, pop(a), insn));
} else if (op >= IRETURN && op <= ARETURN) {
pop(a);
}
default -> throw new MatchError(insn.getOpcode());
}
}
case MethodInsnNode method -> {
switch (insn.getOpcode()) {
case INVOKEVIRTUAL, INVOKESPECIAL, INVOKEINTERFACE ->
a.push(new Invoke(insn.getOpcode(), a.popArgs(method.desc), pop(a), method, insn));
case INVOKESTATIC ->
a.push(new Invoke(insn.getOpcode(), a.popArgs(method.desc), null, method, insn));
default -> throw new MatchError(insn.getOpcode());
}
} else if (insn instanceof IntInsnNode) {
IntInsnNode intInsn = (IntInsnNode) insn;
switch (insn.getOpcode()) {
case BIPUSH:
a.push(new Const((byte) intInsn.operand, insn));
break;
case SIPUSH:
a.push(new Const((short) intInsn.operand, insn));
break;
default:
throw new MatchError(insn.getOpcode());
}
case TypeInsnNode typeInsn -> {
switch (insn.getOpcode()) {
case NEW -> a.push(new New(getObjectType(typeInsn.desc), insn));
case NEWARRAY -> a.push(new NewArray(pop(a), getObjectType(typeInsn.desc), insn));
case ANEWARRAY -> a.push(new NewArray(pop(a), getObjectType("[" + typeInsn.desc), insn));
case CHECKCAST -> a.push(new Cast(pop(a), getObjectType(typeInsn.desc), insn));
case INSTANCEOF -> a.push(new UnaryOp(INSTANCEOF, pop(a), insn));
default -> throw new MatchError(insn.getOpcode());
}
} else if (insn instanceof LdcInsnNode) {
if (insn.getOpcode() != LDC) throw new MatchError(insn.getOpcode());
a.push(new Const(((LdcInsnNode) insn).cst, insn));
} else if (insn instanceof VarInsnNode) {
VarInsnNode varInsn = (VarInsnNode) insn;
int op = insn.getOpcode();
if (op >= ILOAD && op <= ALOAD) {
a.push(new Load(a.locals().apply(varInsn.var), insn));
} else if (op >= ISTORE && op <= ASTORE) {
a.setL(varInsn.var, new Store(pop(a), insn));
} else {
throw new MatchError(op);
}
case MultiANewArrayInsnNode multi -> {
StackEntry[] sizes = new StackEntry[multi.dims];
for (int i = 0; i < sizes.length; i++) sizes[i] = pop(a);
a.push(new NewMultiArray(sizes, getType(multi.desc), insn));
} else if (insn instanceof IincInsnNode) {
IincInsnNode incInsn = (IincInsnNode) insn;
if (insn.getOpcode() != IINC) throw new MatchError(insn.getOpcode());
a.setL(
incInsn.var,
new Store(
new BinaryOp(
IINC,
new Const(incInsn.incr, insn),
new Load(a.locals().apply(incInsn.var), insn),
insn),
insn));
} else if (insn instanceof JumpInsnNode) {
int op = insn.getOpcode();
if (op >= IFEQ && op <= IFLE || op == IFNULL || op == IFNONNULL) {
pop(a);
} else if (op >= IF_ICMPEQ && op <= IF_ACMPNE) {
pop(a);
pop(a);
} else if (op == JSR) {
a.push(new ReturnAddress(insn));
} else if (op != GOTO) {
throw new MatchError(op);
}
case LabelNode label -> {
Option<TryCatchBlockNode> handler = a.codechicken$multipart$asm$StackAnalyser$$catchHandlers()
.get(label);
if (handler instanceof Some) {
a.push(new CaughtException(Type.getType(((Some<TryCatchBlockNode>) handler).x().type), insn));
} else if (!None$.MODULE$.equals(handler)) {
throw new MatchError(handler);
}
} else if (insn instanceof TableSwitchInsnNode || insn instanceof LookupSwitchInsnNode) {
pop(a);
} else if (insn instanceof FieldInsnNode) {
FieldInsnNode field = (FieldInsnNode) insn;
switch (insn.getOpcode()) {
case GETSTATIC:
a.push(new GetField(null, field, insn));
break;
case PUTSTATIC:
pop(a);
break;
case GETFIELD:
a.push(new GetField(pop(a), field, insn));
break;
case PUTFIELD:
pop(a);
pop(a);
break;
default:
throw new MatchError(insn.getOpcode());
}
} else if (insn instanceof MethodInsnNode) {
MethodInsnNode method = (MethodInsnNode) insn;
switch (insn.getOpcode()) {
case INVOKEVIRTUAL:
case INVOKESPECIAL:
case INVOKEINTERFACE:
a.push(new Invoke(insn.getOpcode(), a.popArgs(method.desc), pop(a), method, insn));
break;
case INVOKESTATIC:
a.push(new Invoke(insn.getOpcode(), a.popArgs(method.desc), null, method, insn));
break;
default:
throw new MatchError(insn.getOpcode());
}
} else if (insn instanceof TypeInsnNode) {
TypeInsnNode typeInsn = (TypeInsnNode) insn;
switch (insn.getOpcode()) {
case NEW:
a.push(new New(getObjectType(typeInsn.desc), insn));
break;
case NEWARRAY:
a.push(new NewArray(pop(a), getObjectType(typeInsn.desc), insn));
break;
case ANEWARRAY:
a.push(new NewArray(pop(a), getObjectType("[" + typeInsn.desc), insn));
break;
case CHECKCAST:
a.push(new Cast(pop(a), getObjectType(typeInsn.desc), insn));
break;
case INSTANCEOF:
a.push(new UnaryOp(INSTANCEOF, pop(a), insn));
break;
default:
throw new MatchError(insn.getOpcode());
}
} else if (insn instanceof MultiANewArrayInsnNode) {
MultiANewArrayInsnNode multi = (MultiANewArrayInsnNode) insn;
StackEntry[] sizes = new StackEntry[multi.dims];
for (int i = 0; i < sizes.length; i++) sizes[i] = pop(a);
a.push(new NewMultiArray(sizes, getType(multi.desc), insn));
} else if (insn instanceof LabelNode) {
Option<TryCatchBlockNode> handler = a.codechicken$multipart$asm$StackAnalyser$$catchHandlers()
.get((LabelNode) insn);
if (handler instanceof Some) {
a.push(new CaughtException(Type.getType(((Some<TryCatchBlockNode>) handler).x().type), insn));
} else if (!None$.MODULE$.equals(handler)) {
throw new MatchError(handler);
}
case null, default -> {}
}
}
}