refactor: drop unused IDWriter and JPartialOcclusion Scala bridges

The downstream ABI inventory scanned 240 mod jars in GTNH daily
2026-08-14+678 and found no consumer of either bridge. Both were written
speculatively, before the inventory existed.

IDWriter had no downstream reference at all, so its four Scala function
accessors are removed and the class now selects a carrier width directly.
That also removes the Integer boxing and Function1/Function2 allocation
per read and write that the first port had preserved. Calling write or
read before setMax now throws IllegalStateException rather than
NullPointerException; no caller reaches that state.

JPartialOcclusion$class had no downstream reference either. The two
consumers that touch JPartialOcclusion only call getPartialOcclusionBoxes,
and the two jars that implement the interface emit no call to the helper.
The interface and both of its method descriptors are unchanged. The
ReferenceScalaPartialOcclusion fixture is removed with the helper it
existed to verify.

Validated with 27 plain-JVM tests, a clean checkstyleTest build with
Spotless re-enabled, and the Java 8 Forge dedicated-server suite.
This commit is contained in:
Algent committed 2026-08-14 19:52:28 +02:00
1 parent 78959e70e9
commit 246dafffda
9 files changed
+99 -157

No files matched your search

+3
View File
@@ -433,3 +433,6 @@ These ranges were formed before designing the characterization suite and should
- Answered open decision 4: Scala runtime removal is not achievable for the first Java release. ProjRed, OpenComputers, ProjectBlue, and ForgeRelocationFMP link against 16 static methods on 8 trait `$class` helpers plus 17 companion `MODULE$` singletons.
- Found only five referenced descriptors containing Scala types, and `TileMultipart.jPartList()` already outweighs `partList()` in downstream use. The Scala bridge surface is much smaller than the initial audit assumed.
- Found zero downstream references to `IDWriter`, so its four retained Scala function accessors are not load-bearing and can be dropped.
- Removed both speculative bridges the inventory proved dead: the four `IDWriter` Scala function accessors and the whole `JPartialOcclusion$class` helper, along with the `ReferenceScalaPartialOcclusion` fixture that only existed to verify the helper. `JPartialOcclusion` itself and both of its method descriptors are unchanged.
- `IDWriter` now selects a carrier width instead of storing Scala closures, removing the per-call `Integer` boxing and `Function1`/`Function2` allocation that the first port had preserved. The nine encoding cases still pass unchanged.
- Established the working rule for the remaining phases: check `JAVA_MIGRATION_ABI_INVENTORY.md` before writing a bridge, rather than writing one reflexively for every converted file.
+35
View File
@@ -50,3 +50,38 @@ Accepted divergence: the five generated `PartialOcclusionTest$$anonfun$*` classe
- Clean `checkstyleTest build`: passing.
- Java 8 Forge dedicated-server suite: 2 tests, 0 failures, 0 errors.
- `PartialOcclusionTest` class-file version: 52 (Java 8).
## 2026-08-14 — Removal of unused Scala bridges
The downstream ABI inventory (`JAVA_MIGRATION_ABI_INVENTORY.md`) scanned 240 mod jars and found no consumer of either
bridge below. Both were written speculatively before the inventory existed.
### Removed API
- `IDWriter.write()`, `IDWriter.write_$eq(Function2)`, `IDWriter.read()`, and `IDWriter.read_$eq(Function1)`.
No jar in the pack references `IDWriter` at all. Both in-repo callers, `MultiPartRegistry` and
`MicroMaterialRegistry`, already use `write(MCDataOutput, int)` and `read(MCDataInput)`.
- `JPartialOcclusion$class`, including the `allowCompleteOcclusion(JPartialOcclusion)` and `$init$(JPartialOcclusion)`
static descriptors. No jar references the helper. The two consumers that touch `JPartialOcclusion`,
ForgeRelocationFMP and WitchingGadgets, only call `getPartialOcclusionBoxes()Ljava/lang/Iterable;`, and the two
jars that implement the interface, WR-CBE and extrautilities, emit no call to the helper. The interface itself and
both of its method descriptors are unchanged.
The `ReferenceScalaPartialOcclusion` binary fixture and `JPartialOcclusionBinaryCompatibilityTest` were removed with
the helper they existed to verify.
### Observable behavior
`IDWriter` no longer stores Scala closures, so carrier selection and encoding happen without boxing an `Integer` or
allocating a `Function1`/`Function2` per call. Byte, unsigned-short, and integer selection and the encoded bytes are
unchanged, including negative maximums selecting the byte carrier.
Accepted divergence: calling `write` or `read` before `setMax` now throws `IllegalStateException` instead of
`NullPointerException`. No caller reaches this state; the change only improves the failure message.
### Validation
- `IDWriterCharacterizationTest`: 9 tests, 0 failures, 0 errors.
- Complete plain-JVM suite: 27 tests, 0 failures, 0 errors.
- Clean `checkstyleTest build` with Spotless re-enabled: passing.
- Java 8 Forge dedicated-server suite: 2 tests, 0 failures, 0 errors.
@@ -2,97 +2,50 @@ package codechicken.multipart;
import codechicken.lib.data.MCDataInput;
import codechicken.lib.data.MCDataOutput;
import scala.Function1;
import scala.Function2;
import scala.runtime.AbstractFunction1;
import scala.runtime.AbstractFunction2;
import scala.runtime.BoxedUnit;
/** Class for reading and writing IDs, widening the carrier data type as necessary. */
public class IDWriter {
private Function2<MCDataOutput, Object, BoxedUnit> write;
private Function1<MCDataInput, Object> read;
@Deprecated
public Function2<MCDataOutput, Object, BoxedUnit> write() {
return write;
}
@Deprecated
public void write_$eq(Function2<MCDataOutput, Object, BoxedUnit> write) {
this.write = write;
}
@Deprecated
public Function1<MCDataInput, Object> read() {
return read;
}
@Deprecated
public void read_$eq(Function1<MCDataInput, Object> read) {
this.read = read;
}
public void write(MCDataOutput data, int id) {
write.apply(data, id);
}
public int read(MCDataInput data) {
return (Integer) read.apply(data);
}
private int width;
public void setMax(int value) {
// The reference Scala implementation widens negative values as signed values.
long maximum = value;
if (maximum > 0xffffL) {
write_$eq(new AbstractFunction2<MCDataOutput, Object, BoxedUnit>() {
@Override
public BoxedUnit apply(MCDataOutput data, Object id) {
data.writeInt((Integer) id);
return BoxedUnit.UNIT;
}
});
read_$eq(new AbstractFunction1<MCDataInput, Object>() {
@Override
public Object apply(MCDataInput data) {
return data.readInt();
}
});
width = 4;
} else if (maximum > 0xffL) {
write_$eq(new AbstractFunction2<MCDataOutput, Object, BoxedUnit>() {
@Override
public BoxedUnit apply(MCDataOutput data, Object id) {
data.writeShort((Integer) id);
return BoxedUnit.UNIT;
}
});
read_$eq(new AbstractFunction1<MCDataInput, Object>() {
@Override
public Object apply(MCDataInput data) {
return data.readUShort();
}
});
width = 2;
} else {
write_$eq(new AbstractFunction2<MCDataOutput, Object, BoxedUnit>() {
width = 1;
}
}
@Override
public BoxedUnit apply(MCDataOutput data, Object id) {
data.writeByte((Integer) id);
return BoxedUnit.UNIT;
}
});
read_$eq(new AbstractFunction1<MCDataInput, Object>() {
public void write(MCDataOutput data, int id) {
switch (width) {
case 1:
data.writeByte(id);
break;
case 2:
data.writeShort(id);
break;
case 4:
data.writeInt(id);
break;
default:
throw new IllegalStateException("setMax was not called before writing an ID");
}
}
@Override
public Object apply(MCDataInput data) {
return (int) data.readUByte();
}
});
public int read(MCDataInput data) {
switch (width) {
case 1:
return data.readUByte();
case 2:
return data.readUShort();
case 4:
return data.readInt();
default:
throw new IllegalStateException("setMax was not called before reading an ID");
}
}
}
@@ -1,14 +0,0 @@
package codechicken.multipart;
/** Binary bridge for Scala implementations compiled against the original trait. */
@Deprecated
public abstract class JPartialOcclusion$class {
private JPartialOcclusion$class() {}
public static boolean allowCompleteOcclusion(JPartialOcclusion instance) {
return false;
}
public static void $init$(JPartialOcclusion instance) {}
}
+18 -4
View File
@@ -1,7 +1,21 @@
# Precompiled compatibility fixtures
# Compatibility fixtures
Sources below this directory document binary consumers compiled against the untouched `1.7.12` reference dev jar. They are not part of a Gradle source set and must not be recompiled against the current project during normal tests.
Files below this directory are frozen baselines, not a Gradle source set. They must be reviewed when they change
rather than regenerated automatically, otherwise an accidental change silently redefines the baseline.
`ReferenceScalaPartialOcclusion.scala` was compiled with Scala 2.11.5 against reference revision `f10595d062bfc1e6dd57a5320fb5b48c383a4f38` and dev jar SHA-256 `bc6e06a01ca8a50b7532aa7d173895c9e89bd2989e43ed977e9eddd3adba57a5`. Its class-file SHA-256 is `c29f2fe296d0a50352b1de1d12ab7a95ac75848a14352ddae06b7aa9640d404a`.
## `abi/`
The class is stored as `src/test/resources/compat/ReferenceScalaPartialOcclusion.class.b64` so the repository retains a fixed binary compiled against the reference API. The test decodes and defines that class directly; compiling the documented source against the port would invalidate the compatibility check.
`gtnh-daily-678-consumers.txt` is the constant-pool scan of every mod jar in GTNH daily `2026-08-14+678`, listing
every downstream reference into `codechicken/multipart` and `codechicken/microblock`. Regenerate with
`tools/AbiScan.java` and diff against this file at every public-API phase; a member present here but absent from the
port is a linkage break in a shipping mod. See `JAVA_MIGRATION_ABI_INVENTORY.md` for the analysis.
## Precompiled binary consumers
When a port changes descriptors that the ABI inventory shows are load-bearing, freeze a consumer compiled against the
reference dev jar here as a class file and load it directly in a test. Recompiling a consumer against the port would
hide exactly the linkage failure the fixture exists to catch.
None are currently retained. `ReferenceScalaPartialOcclusion` was removed together with the `JPartialOcclusion$class`
bridge once the inventory showed no downstream consumer of that helper. The trait helpers that do need this treatment
are listed in `JAVA_MIGRATION_ABI_INVENTORY.md`.
@@ -1,11 +0,0 @@
package codechicken.multipart.compat
import java.util.Collections
import codechicken.lib.vec.Cuboid6
import codechicken.multipart.JPartialOcclusion
final class ReferenceScalaPartialOcclusion extends JPartialOcclusion {
override def getPartialOcclusionBoxes: java.lang.Iterable[Cuboid6] =
Collections.emptyList[Cuboid6]()
}
@@ -2,6 +2,7 @@ package codechicken.multipart;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
@@ -9,6 +10,7 @@ import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.lang.reflect.Proxy;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
@@ -36,7 +38,7 @@ class IDWriterCharacterizationTest {
private static byte[] encode(IDWriter writer, int value) {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
MCDataOutput output = new MCDataOutputWrapper(new DataOutputStream(bytes));
writer.write().apply(output, value);
writer.write(output, value);
return bytes.toByteArray();
}
@@ -58,11 +60,19 @@ class IDWriterCharacterizationTest {
}
});
int value = (Integer) writer.read().apply(input);
int value = writer.read(input);
assertEquals(0, data.available(), "The selected reader must consume its whole carrier");
return value;
}
@Test
void rejectsUseBeforeSetMax() {
IDWriter writer = new IDWriter();
assertThrows(IllegalStateException.class, () -> writer.write(null, 0));
assertThrows(IllegalStateException.class, () -> writer.read(null));
}
private static byte[] hex(String value) {
String[] octets = value.split(" ");
byte[] bytes = new byte[octets.length];
@@ -1,47 +0,0 @@
package codechicken.multipart;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Objects;
import java.util.Scanner;
import org.junit.jupiter.api.Test;
class JPartialOcclusionBinaryCompatibilityTest {
@Test
void loadsScalaImplementorCompiledAgainstReferenceJar() throws Exception {
byte[] bytecode = loadFixture();
Class<?> fixtureClass = new FixtureClassLoader(JPartialOcclusion.class.getClassLoader()).define(bytecode);
Object fixture = fixtureClass.getDeclaredConstructor().newInstance();
assertTrue(fixture instanceof JPartialOcclusion);
JPartialOcclusion part = (JPartialOcclusion) fixture;
assertFalse(part.allowCompleteOcclusion());
assertFalse(part.getPartialOcclusionBoxes().iterator().hasNext());
}
private static byte[] loadFixture() {
InputStream input = Objects.requireNonNull(
JPartialOcclusionBinaryCompatibilityTest.class
.getResourceAsStream("/compat/ReferenceScalaPartialOcclusion.class.b64"));
try (Scanner scanner = new Scanner(input, StandardCharsets.US_ASCII.name()).useDelimiter("\\A")) {
return Base64.getMimeDecoder().decode(scanner.next());
}
}
private static final class FixtureClassLoader extends ClassLoader {
private FixtureClassLoader(ClassLoader parent) {
super(parent);
}
private Class<?> define(byte[] bytecode) {
return defineClass(null, bytecode, 0, bytecode.length);
}
}
}
@@ -1 +0,0 @@
yv66vgAAADIALAEAO2NvZGVjaGlja2VuL211bHRpcGFydC9jb21wYXQvUmVmZXJlbmNlU2NhbGFQYXJ0aWFsT2NjbHVzaW9uBwABAQAQamF2YS9sYW5nL09iamVjdAcAAwEAJ2NvZGVjaGlja2VuL211bHRpcGFydC9KUGFydGlhbE9jY2x1c2lvbgcABQEAJFJlZmVyZW5jZVNjYWxhUGFydGlhbE9jY2x1c2lvbi5zY2FsYQEAHkxzY2FsYS9yZWZsZWN0L1NjYWxhU2lnbmF0dXJlOwEABWJ5dGVzAQFMBgExMkEhAQIDEwlxIisaNGZlFnQ3LVoqZEMyDAcrGTp1UwZkd2pZMm1rTkx3ThwGAwcRCWFhWThuYQYkKEJBAwcDJWlXDxw7amEGFEhPQwEIAy0Zdw5aM2RRJhw3Lho4BAFNGQFBAwkRBS1xUSIBBwsDNQlRYV0yYlkGTCFhBAcDDQVzF1BVM2chCQkiI0QBBRMJGUJBQQlLIQYUSC9bMW0fDhxHLl46al86RFEhBgEFAlkJYQFQNW9TUnREI0EMEQVhAVEiAQIJC2kBQRFJDgIxHSxHD1Uxc2kmDEduVDJkWVYcGG4cOENfYiw3L0YBHSENaSIFSgcCPSkRcQRJAQVZBnR3TUMBIgMRURdNXjEKBQ1yIgFDJXVLSgwnDVwzEQUVUlMiARQLBR1CE2EBPGZHKhERRkIBBFkmFBdCQRYnBR0ZVU9ZOGpJWgIBABZhbGxvd0NvbXBsZXRlT2NjbHVzaW9uAQADKClaAQAtY29kZWNoaWNrZW4vbXVsdGlwYXJ0L0pQYXJ0aWFsT2NjbHVzaW9uJGNsYXNzBwANAQAsKExjb2RlY2hpY2tlbi9tdWx0aXBhcnQvSlBhcnRpYWxPY2NsdXNpb247KVoMAAsADwoADgAQAQAEdGhpcwEAPUxjb2RlY2hpY2tlbi9tdWx0aXBhcnQvY29tcGF0L1JlZmVyZW5jZVNjYWxhUGFydGlhbE9jY2x1c2lvbjsBABhnZXRQYXJ0aWFsT2NjbHVzaW9uQm94ZXMBABYoKUxqYXZhL2xhbmcvSXRlcmFibGU7AQAVamF2YS91dGlsL0NvbGxlY3Rpb25zBwAWAQAJZW1wdHlMaXN0AQASKClMamF2YS91dGlsL0xpc3Q7DAAYABkKABcAGgEABjxpbml0PgEAAygpVgwAHAAdCgAEAB4BAAYkaW5pdCQBACwoTGNvZGVjaGlja2VuL211bHRpcGFydC9KUGFydGlhbE9jY2x1c2lvbjspVgwAIAAhCgAOACIBAARDb2RlAQASTG9jYWxWYXJpYWJsZVRhYmxlAQAPTGluZU51bWJlclRhYmxlAQAJU2lnbmF0dXJlAQA1KClMamF2YS9sYW5nL0l0ZXJhYmxlPExjb2RlY2hpY2tlbi9saWIvdmVjL0N1Ym9pZDY7PjsBAApTb3VyY2VGaWxlAQAZUnVudGltZVZpc2libGVBbm5vdGF0aW9ucwEACFNjYWxhU2lnADEAAgAEAAEABgAAAAMAAQALAAwAAQAkAAAALwABAAEAAAAFKrgAEawAAAACACUAAAAMAAEAAAAFABIAEwAAACYAAAAGAAEAAAAIAAEAFAAVAAIAJAAAAC4AAQABAAAABLgAG7AAAAACACUAAAAMAAEAAAAEABIAEwAAACYAAAAGAAEAAAAKACcAAAACACgAAQAcAB0AAQAkAAAAMwABAAEAAAAJKrcAHyq4ACOxAAAAAgAlAAAADAABAAAACQASABMAAAAmAAAABgABAAAACAADACkAAAACAAcAKgAAAAsAAQAIAAEACXMACgArAAAAAwUAAA==