diff --git a/JAVA_MIGRATION.md b/JAVA_MIGRATION.md index d192385..a0c90b4 100644 --- a/JAVA_MIGRATION.md +++ b/JAVA_MIGRATION.md @@ -447,3 +447,7 @@ These ranges were formed before designing the characterization suite and should - Recorded a second source-only divergence: `NormalOcclusionTest` was a Scala `object`, so `NormalOcclusionTest(a, b)` apply sugar no longer compiles and recompiled Scala callers must write `NormalOcclusionTest.apply(a, b)`. Both static forwarders and the `MODULE$` singleton keep their descriptors, so binaries are unaffected. - Kept `scala.collection.Traversable` in two descriptors, as the ABI inventory predicted. They are drained with `JavaConversions.asJavaIterator(boxes.toIterator())`, which is the one interop route callable from Java. - Removed two more closure artifacts, `NormalOcclusionTest$$anonfun$apply$1` and its nested `$$anonfun$apply$2`, and corrected the scaladoc that claimed the test "returns true if the test fails". +- Ported `TFacePart`, the first trait where Java default methods are actually safe. `TMultiPart` declares neither `solid` nor `redstoneConductionMap`, so there is no superclass method for a default to lose to. The interface is descriptor-identical to the reference and no in-repo Scala needed changing. +- Adopted the resulting rule for the remaining traits: emit default methods only when no class in an implementor's superclass chain declares the same member, otherwise leave the member abstract and add explicit forwarders as `TCuboidPart` and `TNormalOcclusion` required. +- Made `TFacePart` inheritable from Java as a side effect. Both members were abstract on the reference interface, so `minecraft/McSidedMetaPart` had to declare them even to accept the defaults. That class stays descriptor-identical, so the change is purely additive. +- Added the first functional coverage of runtime class generation interacting with a ported type. Microblock classes are built by ASM at run time, so a plain-JVM test cannot show that a generated class resolves a Java interface default; the Forge server suite now generates a `FaceMicroblock` and asserts it inherits `redstoneConductionMap`, and generates a `HollowMicroblock` and asserts its own `0x10` and `false` still win. diff --git a/JAVA_MIGRATION_DIVERGENCES.md b/JAVA_MIGRATION_DIVERGENCES.md index 238f732..25cb2df 100644 --- a/JAVA_MIGRATION_DIVERGENCES.md +++ b/JAVA_MIGRATION_DIVERGENCES.md @@ -243,3 +243,52 @@ backwards. The Java doc comments now say the test returns true when the parts ma - Complete plain-JVM suite: 38 tests, 0 failures, 0 errors. - Clean `spotlessApply checkstyleTest build`: passing. - Java 8 Forge dedicated-server suite: 2 tests, 0 failures, 0 errors. + +## 2026-08-14 — TFacePart Java port + +### Observable behavior + +No known divergence. `solid` still returns true for every side, including negative and out-of-range values, and +`redstoneConductionMap` still returns 0. `TFacePart` still extends `TSlottedPart`. + +### The first trait where default methods are safe + +`TMultiPart` declares neither `solid` nor `redstoneConductionMap`, and neither does anything in the microblock +superclass chain. There is therefore no superclass method for an interface default to lose to, and both members are +emitted as real Java `default` methods rather than left abstract. + +This is the check to apply to every remaining trait: a default is safe only when no class in an implementor's +superclass chain declares the same member. `TCuboidPart` and `TNormalOcclusion` both failed that check and had to stay +abstract with explicit forwarders. + +As a result no in-repo Scala needed changing. `FaceMicroblock` and `HollowMicroblock` already override what they care +about, and `FaceMicroblock` picks up the `redstoneConductionMap` default exactly as it previously picked up the trait +implementation. + +### Improvement for Java implementors + +Both members were abstract on the reference interface, so a Java class implementing `TFacePart` had to supply them even +to accept the defaults; `minecraft/McSidedMetaPart` does exactly that. Java implementors can now inherit them. This is +additive: `McSidedMetaPart` is descriptor-identical to the reference, and existing implementors that declare their own +overrides are unaffected. + +### Supported JVM API + +- `TFacePart` is descriptor-identical to the reference, including both members remaining public and non-static on the + interface. Changing them from abstract to default does not affect method resolution for existing implementors, which + all declare their own. +- Retained `TFacePart$class` with all three statics for OpenComputers, ProjRed and ProjectBlue. It adds only a private + constructor. Its bodies hold the defaults directly rather than dispatching through the instance, matching the + reference and avoiding recursion from old forwarders. +- The emitted class-file set for this area is unchanged; the Scala implementation generated no closure classes here. + +### Validation + +- `FacePartCharacterizationTest`: 4 tests, 0 failures, 0 errors, unchanged from the Scala baseline. +- `TFacePartBinaryCompatibilityTest`: 1 test, 0 failures, 0 errors, loading a Scala 2.11.5 consumer whose forwarders + call all three statics. +- Complete plain-JVM suite: 43 tests, 0 failures, 0 errors. +- Clean `spotlessApply checkstyleTest build`: passing. +- Java 8 Forge dedicated-server suite: 3 tests, 0 failures, 0 errors. A new case covers what plain-JVM tests cannot: + microblock classes are generated by ASM at runtime, so it checks that a generated `FaceMicroblock` inherits the + `redstoneConductionMap` default and that a generated `HollowMicroblock` still returns its own `0x10` and `false`. diff --git a/src/functionalTest/java/codechicken/multipart/test/ForgeEnvironmentSmokeTest.java b/src/functionalTest/java/codechicken/multipart/test/ForgeEnvironmentSmokeTest.java index 7f86ea5..2c50b3e 100644 --- a/src/functionalTest/java/codechicken/multipart/test/ForgeEnvironmentSmokeTest.java +++ b/src/functionalTest/java/codechicken/multipart/test/ForgeEnvironmentSmokeTest.java @@ -13,6 +13,11 @@ import net.minecraft.server.MinecraftServer; import org.junit.jupiter.api.Test; +import codechicken.microblock.FaceMicroClass$; +import codechicken.microblock.HollowMicroClass$; +import codechicken.microblock.Microblock; +import codechicken.microblock.MicroblockGenerator; +import codechicken.multipart.TFacePart; import codechicken.multipart.TileMultipart; import codechicken.multipart.asm.MultipartMixinFactory; import codechicken.multipart.scalatraits.TSlottedTile; @@ -41,6 +46,25 @@ class ForgeEnvironmentSmokeTest { assertNotNull(server.worldServers[0]); } + /** + * Microblock classes are generated by ASM at runtime. TFacePart's members are now Java interface defaults rather + * than Scala trait forwarders, so a generated class inherits them instead of declaring its own; this checks the JVM + * resolves them for a class that never existed at compile time. + */ + @Test + void generatedMicroblockInheritsFacePartDefaults() { + Microblock face = MicroblockGenerator.create(FaceMicroClass$.MODULE$, 0, false); + Microblock hollow = MicroblockGenerator.create(HollowMicroClass$.MODULE$, 0, false); + + assertTrue(face instanceof TFacePart); + assertEquals(0, ((TFacePart) face).redstoneConductionMap()); + + // HollowMicroblock overrides both members, so its generated class must not pick up the defaults. + assertTrue(hollow instanceof TFacePart); + assertEquals(0x10, ((TFacePart) hollow).redstoneConductionMap()); + assertFalse(((TFacePart) hollow).solid(0)); + } + @Test void generatesAndCachesSlottedTileClass() { int traitId = MultipartMixinFactory.getId(TSlottedTile.class.getName().replace('.', '/')); diff --git a/src/main/scala/codechicken/multipart/TFacePart$class.java b/src/main/scala/codechicken/multipart/TFacePart$class.java new file mode 100644 index 0000000..0f90d30 --- /dev/null +++ b/src/main/scala/codechicken/multipart/TFacePart$class.java @@ -0,0 +1,21 @@ +package codechicken.multipart; + +/** + * Binary bridge for Scala implementations compiled against the original trait. Their forwarders call these statics + * directly, so the bodies cannot delegate back to the instance methods without recursing. + */ +@Deprecated +public abstract class TFacePart$class { + + private TFacePart$class() {} + + public static boolean solid(TFacePart part, int side) { + return true; + } + + public static int redstoneConductionMap(TFacePart part) { + return 0; + } + + public static void $init$(TFacePart part) {} +} diff --git a/src/main/scala/codechicken/multipart/TFacePart.java b/src/main/scala/codechicken/multipart/TFacePart.java new file mode 100644 index 0000000..78c0c05 --- /dev/null +++ b/src/main/scala/codechicken/multipart/TFacePart.java @@ -0,0 +1,22 @@ +package codechicken.multipart; + +/** + * Interface which must be implemented by parts that go in a face part. + *

+ * TMultiPart declares neither member, so unlike the other converted traits these can safely be default methods. + */ +public interface TFacePart extends TSlottedPart { + + /** Passed down from Block.isSideSolid. Return true if this part is solid and opaque on the specified side. */ + default boolean solid(int side) { + return true; + } + + /** + * Return the redstone conduction map for which signal can pass through this part on the face. Eg, hollow covers + * return 0x10 as signal can pass through the center hole. + */ + default int redstoneConductionMap() { + return 0; + } +} diff --git a/src/main/scala/codechicken/multipart/TFacePart.scala b/src/main/scala/codechicken/multipart/TFacePart.scala deleted file mode 100644 index c41a949..0000000 --- a/src/main/scala/codechicken/multipart/TFacePart.scala +++ /dev/null @@ -1,17 +0,0 @@ -package codechicken.multipart - -/** Inteface which must be implemented by parts that go in a face part. - */ -trait TFacePart extends TSlottedPart { - - /** Passed down from Block.isSideSolid. Return true if this part is solid and - * opaque on the specified side - */ - def solid(side: Int): Boolean = true - - /** Return the redstone conduction map for which signal can pass through this - * part on the face. Eg, hollow covers return 0x10 as signal can pass through - * the center hole. - */ - def redstoneConductionMap = 0 -}