refactor: port slotted tile trait to java

This commit is contained in:
Algent committed 2026-08-28 11:42:06 +03:00
1 parent 7894c98352
commit 39e0858006
7 files changed
+177 -87

No files matched your search

+24 -12
View File
@@ -24,10 +24,10 @@ No trustworthy tool will produce a maintainable Java port automatically. A decom
## Current status
The binary and source-level consumer audits, low-coupling Phase 3 queue, audit-derived immediate compatibility gate,
focused profile, first traversal optimization, complete redstone interaction helper unit, `MicroRecipe` port, and the
no-field `TPartialOcclusionTile` Java-trait pilot are complete. Direct Java source now survives the existing
`registerJavaTrait` rewrite with its generated interface, super accessor, behavior, and class caching intact. The next
checkpoint is the stateful `TSlottedTile`; see `JAVA_MIGRATION_PROFILE.md`.
focused profile, first traversal optimization, complete redstone interaction helper unit, `MicroRecipe` port, and both
Java-trait checkpoints are complete. `TPartialOcclusionTile` proved the no-field path; `TSlottedTile` now proves field
accessors, per-instance initialization, rebinding/copying, callbacks, super bridges, and class caching. The next
measured target is `TRedstoneTile`; see `JAVA_MIGRATION_PROFILE.md`.
That audit found and this branch has now repaired one existing regression: Schematica 1.12.6 reflects the original private
`MultiPartRegistry$` field `codechicken$multipart$MultiPartRegistry$$typeMap` and casts it to a Scala mutable map. The
@@ -329,7 +329,7 @@ and the already-ported registry again supports Schematica.
- [x] Rewrite `TileMultipart` update/operate traversal without per-call collection allocation.
- [ ] Rewrite redstone queries without closure allocation or `IntRef` boxing.
- [x] Replace non-local returns in recipe scans with ordinary Java control flow.
- [ ] Replace the remaining non-local return in the generated `TSlottedTile` slot scan.
- [x] Replace the remaining non-local return in the generated `TSlottedTile` slot scan.
- [x] Keep fastutil out of these paths because profiling did not justify another dependency or data structure.
- [x] Re-profile the same scenarios and record both improvements and regressions.
@@ -337,9 +337,10 @@ Status: active. `operate` now preserves its captured-sequence mutation semantics
and the six-interface `IRedstonePart`/`RedstoneInteractions$` unit is ported with its class list and descriptors intact.
The comparison proves its allocation is owned by `scalatraits/TRedstoneTile.scala`, so that part waits for Phase 5.
`MicroRecipe` is also Java: all five recipe forms and precedence are characterized, its published Scala façade remains,
and its closure-backed scans and non-local returns are gone. `TPartialOcclusionTile` has completed the simple no-field
Java-trait pilot. The remaining two hot-path items are stateful generated traits: port `TSlottedTile` as the field and
lifecycle checkpoint before changing `TRedstoneTile` and re-running the redstone profile.
and its closure-backed scans and non-local returns are gone. `TPartialOcclusionTile` completed the simple no-field
Java-trait pilot, and `TSlottedTile` completed the stateful field/lifecycle checkpoint while removing the remaining
slot-scan closures and exception-backed return. The remaining measured item is `TRedstoneTile`; characterize its exact
generated surface and real consumers, then re-profile immediately before and after its Java port.
Exit condition: identified steady-state Scala allocation sites are removed with compatible results.
@@ -348,13 +349,13 @@ Exit condition: identified steady-state Scala allocation sites are removed with
- [x] Characterize the direct class shape, generated interface, super dispatch, behavior, and class caching of a
no-field pilot.
- [x] Port one simple built-in trait as a pilot using `registerJavaTrait`.
- [ ] Verify generated field accessors, initialization, copying, lifecycle callbacks, and class caching on the
stateful `TSlottedTile` checkpoint. The no-field pilot proves methods/interfaces/caching but cannot prove state.
- [x] Verify generated field accessors, initialization, copying, lifecycle callbacks, super bridges, and class caching
on the stateful `TSlottedTile` checkpoint.
- [ ] Convert remaining traits in small related groups.
- [ ] Document or deliberately relax the Java trait restrictions only when a real trait requires it.
- [ ] Preserve pass-through interface behavior.
- [ ] Explicitly cover OpenComputers' `TSlottedTile.v_partMap` mutation/rebinding before that port, AE2's
`TIInventoryTile.rebuildSlotMap` before the inventory trait, and ProjectRed/Extra Utilities behavior before
- [ ] Keep the completed OpenComputers `TSlottedTile.v_partMap` mutation/rebinding case green; explicitly cover AE2's
`TIInventoryTile.rebuildSlotMap` before the inventory trait and ProjectRed/Extra Utilities behavior before
`TRedstoneTile`.
Exit condition: built-in behavior is implemented in Java while the established runtime composition mechanism remains stable.
@@ -680,3 +681,14 @@ generated-trait compatibility work. Scala-runtime removal remains a later projec
two behavior methods plus its super accessor, caches the generated class, and passes all 37 server tests.
- The pilot raises the plain-JVM baseline to 145 tests. It deliberately proves no field or lifecycle behavior, so the
next checkpoint is `TSlottedTile`, not a bulk conversion of the remaining traits.
- Characterized `TSlottedTile` against untouched Scala with six focused Forge cases plus the existing live-world
lifecycle fixture. The gate freezes its exact 13-method runtime interface, 27-slot per-instance initialization,
getter/setter rebinding, copy identity, clear/remove/bind behavior, occupied-slot rejection, Scala value equality,
generated field, super accessors, and class cache.
- Ported `TSlottedTile` to a concrete Java mixin input with one public array field and ordinary loops. The runtime
rewrite remains interface- and behavior-identical, while the four Scala range closures, `$class` artifact in the raw
jar, and `NonLocalReturnControl` slot rejection disappear. All 145 plain-JVM and 43 Forge tests pass.
- Found a Phase 5 source-build constraint: shipping binaries still see the same runtime interface, but a consumer
recompiled directly against the untransformed dev jar sees the Java mixin input as a class. Consumers that name a
generated trait therefore need a transformed compile stub or must avoid direct trait invocations; record this before
claiming source-compatible downstream rebuilds.
+4 -3
View File
@@ -501,9 +501,10 @@ behavior:
converting microblocks or Minecraft part implementations.
6. Treat Scala runtime removal as downstream coordination work after the Java port, not as a requirement of it.
Current-branch generator verification: `ForgeEnvironmentSmokeTest` pins cached built-in `TSlottedTile` generation,
an external Scala microblock trait registered by name like ProjectRed's `LightMicroblock`, direct-Java-source
`TPartialOcclusionTile` rewriting and override dispatch, and server-only pass-through forwarding, overloading,
Current-branch generator verification: `ForgeEnvironmentSmokeTest` pins exact direct-Java-source `TSlottedTile`
interface/field generation, initialization, rebinding and caching, an external Scala microblock trait registered by
name like ProjectRed's `LightMicroblock`, direct-Java-source `TPartialOcclusionTile` rewriting and override dispatch,
and server-only pass-through forwarding, overloading,
single-implementor selection, copying/rebinding, and removal cleanup. Full downstream mixed tiles and client-side
selection remain Phase 7 integration cases.
+52
View File
@@ -1407,3 +1407,55 @@ assumed safe from this result; `TSlottedTile` is the explicit stateful checkpoin
- Complete plain-JVM suite: 145 tests, 0 failures, 0 errors.
- Java 8 Forge dedicated-server suite: 37 tests, 0 failures, 0 errors.
- Runtime reflection verifies the exact generated interface methods, no fields, override behavior, and class reuse.
## 2026-08-28 — TSlottedTile Java-trait port
This is the stateful `registerJavaTrait` checkpoint. OpenComputers casts generated tiles to this type, retrieves the
public `v_partMap()` array, mutates matching entries in place, and calls `bindPart` to rebuild them. The frozen binary
inventory contains that exact getter descriptor and no reference to `TSlottedTile$class`.
### Observable behavior
No known runtime divergence. Every generated tile receives its own 27-entry array. `copyFrom` still calls its super
implementation first and then shares the source array by identity only when the source is slotted. `partMap` preserves
normal array indexing. `clearParts` still clears both the superclass part list and every entry in the current array.
`partRemoved` still calls super first, acts only on `TSlottedPart`, scans exactly 27 slots, and uses Scala-style
null-safe value equality; Java uses `Objects.equals` to preserve the last detail. `canAddPart` still rejects the first
occupied masked slot before delegating to the base tile, while non-slotted and conflict-free parts delegate normally.
`bindPart` still calls super first and fills each of the 27 positive masked slots. OpenComputers' external mutation and
rebind pattern is exercised directly.
### Generated ABI and raw input divergence
At Forge runtime `TSlottedTile` remains a public interface with no fields and the exact same 13 abstract methods: six
behavior methods, `v_partMap` getter/setter, and five super accessors. The generated composite still owns a
`TMultiPart[] v_partMap` field, runs the 27-entry initializer for every instance, and reuses its class for equal trait
sets. All 27 shipping consumer jars therefore retain their binary linkage.
The untransformed artifact necessarily changes shape. Scala emitted an interface, a static `$class` implementation
helper, and four `$$anonfun$` range classes. Java emits one concrete `TileMultipart` subclass with a public array field,
one no-arg constructor, and six overrides; `registerJavaTrait` creates the interface/helper at class-load time. No
shipping consumer references the removed raw helper or closure classes.
This produces an accepted source-build limitation: code recompiled directly against the raw dev jar sees a class and
public field, so it may emit `invokevirtual` or `getfield`; Forge later rewrites that owner to an interface, for which
those opcodes are invalid. Existing binaries such as OpenComputers use `invokeinterface` and remain valid. A future
downstream-source release needs either a transformed compile stub/API artifact or explicit avoidance of direct
generated-trait calls; runtime binary compatibility does not by itself solve that development artifact problem.
### Performance boundary
Ordinary loops remove the four Scala `Range.foreach` closure classes and the `NonLocalReturnControl` used for occupied
slot rejection. The retained focused profiler does not perform slotted placement, so this is a structural removal, not
a numeric performance claim. No new dependency or generator change was needed.
### Validation
- `TSlottedTileFunctionalTest`: 6 Forge tests covering copy identity, non-slotted copy, external mutation/rebinding,
bind/remove/clear semantics, value equality, and occupied/free/non-slotted placement.
- `ForgeEnvironmentSmokeTest` verifies the exact 13-method runtime interface, generated field, distinct initialized
arrays, setter rebinding, and class-cache identity.
- Existing live-world lifecycle tests continue to cover add/remove slot caches and relocation of the generated tile.
- Complete plain-JVM suite: 145 tests, 0 failures, 0 errors.
- Java 8 Forge dedicated-server suite: 43 tests, 0 failures, 0 errors.
+19 -12
View File
@@ -12,7 +12,7 @@ what breaks, and what is left. The other documents hold the reasoning.
| `JAVA_MIGRATION_MANUAL_CHECKS.md` | What no automated test can cover, and must be checked by hand in a client |
| `JAVA_MIGRATION_PROFILE.md` | The focused baseline, first measured result, findings, and exact rerun command |
Branch: `algent/java`. Base: `master`. 70 commits including the `TPartialOcclusionTile` characterization and Java port.
Branch: `algent/java`. Base: `master`. 72 commits including both `TSlottedTile` commits.
## The one rule that matters
@@ -66,7 +66,7 @@ awk -F'"' '/<testsuite /{t+=$4;f+=$8;e+=$10} END{print "tests="t" failures="f" e
grep -o 'tests="[0-9]*" skipped="[0-9]*" failures="[0-9]*" errors="[0-9]*"' run/server/junit-out/TEST-*.xml
```
Current baseline: **145 plain-JVM tests, 37 Forge server tests, all passing at their last completed runs.**
Current baseline: **145 plain-JVM tests, 43 Forge server tests, all passing at their last completed runs.**
### ABI diff against the reference
@@ -189,14 +189,15 @@ All eight load-bearing `$class` helpers from the inventory, both registries, and
`IDWriter`, `PartialOcclusionTest`/`JPartialOcclusion`, `TCuboidPart`/`JCuboidPart`, the `TNormalOcclusion` unit,
`TFacePart`, the `TIconHitEffects` unit, `TItemMultiPart`/`JItemMultiPart`, `TEdgePart`, `Saw`, `MicroMaterialRegistry`,
`MultiPartRegistry`, `TileMultipart`, `TMultiPart`, `TickScheduler`, `BlockMultipart`, the complete
`IRedstonePart`/`RedstoneInteractions` unit, `MicroRecipe`, and the no-field `TPartialOcclusionTile` Java-trait pilot.
`IRedstonePart`/`RedstoneInteractions` unit, `MicroRecipe`, and the `TPartialOcclusionTile` and `TSlottedTile`
Java-trait checkpoints.
Plus the six marker interfaces: `TSlottedPart`, `IRandomDisplayTick`, `INeighborTileChange`, `TRandomUpdateTick`,
`ISidedHollowConnect`, `IMicroMaterialRender`, plus `MultipartHelper`, `TileCache`, `PacketScheduler` and the `ControlKeyModifer` pair.
Both `package.scala` objects are gone, removed rather than ported. `MultipartRenderer` is done.
83 Java files, 45 Scala files, ~5,852 Scala lines left (non-blank; that is the metric this figure has always used).
84 Java files, 44 Scala files, ~5,806 Scala lines left (non-blank; that is the metric this figure has always used).
## What is left, and in what order
@@ -243,16 +244,21 @@ plain-JVM tests freeze its input class and behavior. The Forge harness proves th
to the exact three-method runtime interface, preserves override/super dispatch, and reuses the generated composite
class. It has no fields or lifecycle callbacks, so this is not evidence for stateful traits.
`TSlottedTile` is now Java and supplies that stateful evidence. The generated runtime interface still has the exact 13
methods, including public array getter/setter and five super accessors. Each tile receives a distinct 27-entry array;
copying shares the source array exactly as before; external array mutation plus `bindPart`, clear/removal behavior,
occupied-slot rejection, value equality, actual add/remove/move lifecycle, and generated-class caching are all green.
**Medium.** The `handler` packages on both sides, `ItemMicroPart`,
`MicroblockPlacement`, `PlacementGrids`, `BlockMicroMaterial`, `MissingMicroMaterial`, `ConfigContent`,
`DefaultContent`.
**Phase 5, needs the `registerJavaTrait` path.** `TileMultipartClient` and everything in `multipart/scalatraits/`.
These are registered with the ASM generator; converting them is a different mechanism with documented restrictions.
The no-field `TPartialOcclusionTile` pilot is complete without a generator change. Next characterize and port
`TSlottedTile` as the stateful checkpoint. Freeze the generated getter/setter and 27-entry initialization, array identity
through `copyFrom`, clearing/removal/binding, occupied-slot rejection, super-call order, class caching, and
OpenComputers' direct `v_partMap()` mutation pattern before changing its source. Only then proceed to `TRedstoneTile`.
The no-field and stateful checkpoints are complete without a generator change. Next characterize `TRedstoneTile`'s
exact generated interface and every ProjectRed/Extra Utilities call, capture a fresh allocation baseline, port its
queries with ordinary loops, and immediately rerun the same profile. Keep `TIInventoryTile` separate until AE2's
`rebuildSlotMap` and inventory-field behavior are frozen.
**Phase 6/7, last.** `Microblock` and the microblock shape hierarchy, `MicroblockGenerator`, `MultipartGenerator`, and
all of `multipart/asm/`. The ASM subsystem should be last; freeze generated-class fixtures before touching it.
@@ -266,15 +272,16 @@ all of `multipart/asm/`. The ASM subsystem should be last; freeze generated-clas
work and is required before release claims about real TPS.
- The server side of flag-sensitive pass-through registration is automated; client-side exclusion still belongs to
the packaged-client/manual compatibility run.
- The completed `TPartialOcclusionTile` pilot is stateless. It does not prove the generator's field-to-accessor rewrite,
constructor initialization injection, setter rebinding, or lifecycle ordering; `TSlottedTile` is the required
checkpoint for those mechanisms.
- A shipping consumer compiled against the old `TSlottedTile` interface remains binary-compatible because Forge still
exposes that exact interface at runtime. A consumer recompiled against the untransformed dev jar instead sees the
concrete Java mixin input and can emit class/field opcodes that are invalid after Forge rewrites it to an interface.
Provide a transformed compile stub or downstream source guidance before claiming source-compatible rebuilds.
- Microblock-specific NBT and packet payloads still need characterization immediately before that subsystem changes;
the compact core tile/part fixture is complete.
- `TileMultipart`'s `partList` still republishes an immutable Scala `Seq` on every mutation. `operate` no longer calls
the Java port's copying `parts()` helper, but less frequent read and mutation paths still do. Optimize those only if
a representative profile identifies them; their mutable snapshots are intentional at mutation sites.
- The focused redstone allocation remains 80.5 B per three-query iteration. It is generated by
`scalatraits/TRedstoneTile.scala`, not `RedstoneInteractions`; address it after the stateful `TSlottedTile` checkpoint.
`scalatraits/TRedstoneTile.scala`, not `RedstoneInteractions`; it is now the next measured target.
- Phase 1 never properly started. GTNHLib is currently commented out in `dependencies.gradle`; it had been added at
`api` scope to satisfy a test compile rather than because a migration change needed it.
+7 -5
View File
@@ -122,8 +122,10 @@ requires the Phase 5 `registerJavaTrait` path and must not be smuggled into this
characterized, and ordinary loops replaced its range/closure scans and exception-backed non-local returns. The focused
server workload does not craft recipes, so no timing claim is made for that structural removal.
The remaining measured redstone allocation and slot-scan non-local return live in generated traits. The simpler
`TPartialOcclusionTile` Java-source pilot is complete and has no profile claim because this workload does not exercise
partial-occlusion placement. It proves direct Java input, generated method/super dispatch, and class caching, but has no
fields or lifecycle callbacks. Use `TSlottedTile` as the stateful generator checkpoint, then port `TRedstoneTile` and
re-run this workload immediately before and after that change.
The generated-trait checkpoints are complete. `TPartialOcclusionTile` proved the no-field path; `TSlottedTile` proved
field/accessor generation, initialization, copying, lifecycle behavior, and caching. Its ordinary loops remove four
Scala range closures and the exception-backed slot-scan return structurally, but the focused workload has no slotted
placement phase, so no numeric performance claim is made for that port.
The remaining measured redstone allocation lives in `TRedstoneTile`. Capture a fresh baseline with this exact workload,
characterize ProjectRed and Extra Utilities behavior, port the queries, and rerun immediately afterward.
@@ -0,0 +1,71 @@
package codechicken.multipart.scalatraits;
import java.util.Arrays;
import java.util.Objects;
import codechicken.multipart.TMultiPart;
import codechicken.multipart.TSlottedPart;
import codechicken.multipart.TileMultipart;
/** Mixin implementation for slotted parts. */
public class TSlottedTile extends TileMultipart {
public TMultiPart[] v_partMap = new TMultiPart[27];
@Override
public void copyFrom(TileMultipart that) {
super.copyFrom(that);
if (that instanceof TSlottedTile) {
v_partMap = ((TSlottedTile) that).v_partMap;
}
}
@Override
public TMultiPart partMap(int slot) {
return v_partMap[slot];
}
@Override
public void clearParts() {
super.clearParts();
Arrays.fill(v_partMap, null);
}
@Override
public void partRemoved(TMultiPart part, int position) {
super.partRemoved(part, position);
if (part instanceof TSlottedPart) {
for (int index = 0; index < 27; index++) {
if (Objects.equals(partMap(index), part)) {
v_partMap[index] = null;
}
}
}
}
@Override
public boolean canAddPart(TMultiPart part) {
if (part instanceof TSlottedPart) {
int slotMask = ((TSlottedPart) part).getSlotMask();
for (int index = 0; index < v_partMap.length; index++) {
if ((slotMask & (1 << index)) != 0 && partMap(index) != null) {
return false;
}
}
}
return super.canAddPart(part);
}
@Override
public void bindPart(TMultiPart part) {
super.bindPart(part);
if (part instanceof TSlottedPart) {
int slotMask = ((TSlottedPart) part).getSlotMask();
for (int index = 0; index < 27; index++) {
if ((slotMask & (1 << index)) > 0) {
v_partMap[index] = part;
}
}
}
}
}
@@ -1,55 +0,0 @@
package codechicken.multipart.scalatraits
import codechicken.multipart.TileMultipart
import codechicken.multipart.TMultiPart
import codechicken.multipart.TSlottedPart
/** Mixin implementation for TSlottedPart. Puts parts into a slot array for
* quick access at the cost of memory consumption
*/
trait TSlottedTile extends TileMultipart {
var v_partMap = new Array[TMultiPart](27)
override def copyFrom(that: TileMultipart) {
super.copyFrom(that)
if (that.isInstanceOf[TSlottedTile])
v_partMap = that.asInstanceOf[TSlottedTile].v_partMap
}
override def partMap(slot: Int) = v_partMap(slot)
override def clearParts() {
super.clearParts()
for (i <- 0 until v_partMap.length)
v_partMap(i) = null
}
override def partRemoved(part: TMultiPart, p: Int) {
super.partRemoved(part, p)
if (part.isInstanceOf[TSlottedPart])
for (i <- 0 until 27)
if (partMap(i) == part)
v_partMap(i) = null
}
override def canAddPart(part: TMultiPart): Boolean = {
if (part.isInstanceOf[TSlottedPart]) {
val slotMask = part.asInstanceOf[TSlottedPart].getSlotMask
for (i <- 0 until v_partMap.length)
if ((slotMask & 1 << i) != 0 && partMap(i) != null)
return false
}
return super.canAddPart(part)
}
override def bindPart(part: TMultiPart) {
super.bindPart(part)
if (part.isInstanceOf[TSlottedPart]) {
val mask = part.asInstanceOf[TSlottedPart].getSlotMask
for (i <- 0 until 27)
if ((mask & 1 << i) > 0)
v_partMap(i) = part
}
}
}