This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch GROOVY-12162
in repository https://gitbox.apache.org/repos/asf/groovy.git
The following commit(s) were added to refs/heads/GROOVY-12162 by this push:
new cc7a4921a9 rewrite box+mismatched unbox to primitive conversion
cc7a4921a9 is described below
commit cc7a4921a9bc14222aa63a74cb2a3eec941ff9f6
Author: Daniel Sun <[email protected]>
AuthorDate: Sun Jul 19 14:18:51 2026 +0900
rewrite box+mismatched unbox to primitive conversion
---
.../asm/PeepholeOptimizingMethodVisitor.java | 263 ++++++++++++++++++++-
.../asm/PeepholeOptimizingMethodVisitorTest.groovy | 130 ++++++++--
2 files changed, 365 insertions(+), 28 deletions(-)
diff --git
a/src/main/java/org/codehaus/groovy/classgen/asm/PeepholeOptimizingMethodVisitor.java
b/src/main/java/org/codehaus/groovy/classgen/asm/PeepholeOptimizingMethodVisitor.java
index 425ad72e10..4a8b8473ac 100644
---
a/src/main/java/org/codehaus/groovy/classgen/asm/PeepholeOptimizingMethodVisitor.java
+++
b/src/main/java/org/codehaus/groovy/classgen/asm/PeepholeOptimizingMethodVisitor.java
@@ -39,18 +39,30 @@ import static org.objectweb.asm.Opcodes.ALOAD;
import static org.objectweb.asm.Opcodes.ASTORE;
import static org.objectweb.asm.Opcodes.BIPUSH;
import static org.objectweb.asm.Opcodes.CHECKCAST;
+import static org.objectweb.asm.Opcodes.D2F;
+import static org.objectweb.asm.Opcodes.D2I;
+import static org.objectweb.asm.Opcodes.D2L;
import static org.objectweb.asm.Opcodes.DCONST_0;
import static org.objectweb.asm.Opcodes.DCONST_1;
import static org.objectweb.asm.Opcodes.DLOAD;
import static org.objectweb.asm.Opcodes.DSTORE;
import static org.objectweb.asm.Opcodes.DUP;
import static org.objectweb.asm.Opcodes.DUP2;
+import static org.objectweb.asm.Opcodes.F2D;
+import static org.objectweb.asm.Opcodes.F2I;
+import static org.objectweb.asm.Opcodes.F2L;
import static org.objectweb.asm.Opcodes.FCONST_0;
import static org.objectweb.asm.Opcodes.FCONST_1;
import static org.objectweb.asm.Opcodes.FCONST_2;
import static org.objectweb.asm.Opcodes.FLOAD;
import static org.objectweb.asm.Opcodes.FSTORE;
import static org.objectweb.asm.Opcodes.GETSTATIC;
+import static org.objectweb.asm.Opcodes.I2B;
+import static org.objectweb.asm.Opcodes.I2C;
+import static org.objectweb.asm.Opcodes.I2D;
+import static org.objectweb.asm.Opcodes.I2F;
+import static org.objectweb.asm.Opcodes.I2L;
+import static org.objectweb.asm.Opcodes.I2S;
import static org.objectweb.asm.Opcodes.ICONST_0;
import static org.objectweb.asm.Opcodes.ICONST_1;
import static org.objectweb.asm.Opcodes.ICONST_2;
@@ -70,6 +82,9 @@ import static org.objectweb.asm.Opcodes.INVOKESTATIC;
import static org.objectweb.asm.Opcodes.INVOKESPECIAL;
import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL;
import static org.objectweb.asm.Opcodes.ISTORE;
+import static org.objectweb.asm.Opcodes.L2D;
+import static org.objectweb.asm.Opcodes.L2F;
+import static org.objectweb.asm.Opcodes.L2I;
import static org.objectweb.asm.Opcodes.LCONST_0;
import static org.objectweb.asm.Opcodes.LCONST_1;
import static org.objectweb.asm.Opcodes.LLOAD;
@@ -134,18 +149,25 @@ import static org.objectweb.asm.Opcodes.SIPUSH;
* <li><b>DUP + store + pop</b> — {@code DUP}/{@code DUP2}; store;
* matching pop becomes a plain store; bare {@code DUP}/{@code DUP2}
with a
* matching pop is eliminated.</li>
- * <li><b>Box/unbox cancellation</b> — {@code Wrapper.valueOf(p)} /
+ * <li><b>Box/unbox cancellation and conversion</b> — {@code
Wrapper.valueOf(p)} /
* {@code DefaultTypeTransformation.box(p)} followed by a same-type unbox
* ({@code Wrapper.xxxValue()} or {@code
DefaultTypeTransformation.xxxUnbox})
* cancel to a no-op (the primitive remains on the stack).
Cross-convention
* pairs of the same primitive type are included (for example
- * {@code DTT.box(I)} then {@code Integer.intValue()}). A boxed value
- * discarded by {@code POP}/{@code POP2} (or void {@code RETURN}) drops
the
- * box and, when the primitive producer is still in the load window,
drops
- * that producer too; otherwise it pops the original primitive
- * ({@code POP2} when wide). {@code POP2} on a narrow boxed value is
treated
- * as two one-slot discards (box/primitive plus the slot underneath).
- * Mismatched unbox types are left intact.</li>
+ * {@code DTT.box(I)} then {@code Integer.intValue()}). When the unbox
+ * targets a <em>different</em> numeric primitive, the pair is rewritten
to
+ * the matching JVM conversion ({@code I2L}, {@code L2I}, {@code I2F},
…),
+ * including cross-wrapper forms such as {@code Integer.valueOf} then
+ * {@code Long.longValue}: after a pending box the runtime value is
known to
+ * be a properly boxed primitive, so a wrong-owner unbox cannot express a
+ * conditional CCE path (and real classgen uses {@code I2L} for
+ * {@code long l = intExpr} directly). Boolean is excluded from
conversion
+ * rewrites (only same-type cancel applies). A boxed value discarded by
+ * {@code POP}/{@code POP2} (or void {@code RETURN}) drops the box and,
when
+ * the primitive producer is still in the load window, drops that
producer
+ * too; otherwise it pops the original primitive ({@code POP2} when
wide).
+ * {@code POP2} on a narrow boxed value is treated as two one-slot
discards
+ * (box/primitive plus the slot underneath).</li>
* <li><b>Boolean constant folding</b> — {@code GETSTATIC Boolean.TRUE/FALSE}
* followed by {@code booleanValue()} or
* {@code DefaultTypeTransformation.booleanUnbox} becomes
@@ -463,6 +485,9 @@ public final class PeepholeOptimizingMethodVisitor extends
MethodVisitor {
if (tryCancelBoxUnbox(opcode, owner, name, descriptor)) {
return;
}
+ if (tryRewriteBoxUnboxConversion(opcode, owner, name, descriptor)) {
+ return;
+ }
if (tryBufferBox(opcode, owner, name, descriptor, isInterface)) {
return;
}
@@ -1055,6 +1080,228 @@ public final class PeepholeOptimizingMethodVisitor
extends MethodVisitor {
return true;
}
+ /**
+ * Rewrites {@code box(P); unbox-as-Q} to a primitive conversion when
{@code P}
+ * and {@code Q} are different numeric types.
+ * <p>
+ * After a pending box the operand is known to be a properly boxed {@code
P},
+ * so forms such as {@code Integer.valueOf}; {@code Long.longValue} (wrong
+ * owner for the receiver) are equivalent to {@code I2L} rather than a
+ * data-dependent CCE path. Real classgen typically emits {@code I2L} for
+ * {@code long l = intExpr} directly; this rewrite recovers the same shape
+ * when an intermediate box/unbox pair was produced instead.
+ * <p>
+ * Boolean is not converted here (only same-type cancel via
+ * {@link #tryCancelBoxUnbox}). Same-type pairs that failed the owner check
+ * in {@link #matchesPendingUnbox} are treated as identity (drop
box/unbox).
+ *
+ * @return {@code true} if the unbox was fully rewritten
+ */
+ private boolean tryRewriteBoxUnboxConversion(final int opcode, final
String owner, final String name, final String descriptor) {
+ if (pendingBoxKind == PendingBoxKind.NONE) {
+ return false;
+ }
+ String targetPrim = unboxTargetPrimitive(opcode, owner, name,
descriptor);
+ if (targetPrim == null) {
+ return false;
+ }
+ String sourcePrim = pendingBoxPrimitiveDescriptor;
+ if (sourcePrim == null) {
+ return false;
+ }
+ // Boolean only participates in same-type cancel, not numeric
conversion.
+ if ("Z".equals(sourcePrim) || "Z".equals(targetPrim)) {
+ return false;
+ }
+
+ if (sourcePrim.equals(targetPrim)) {
+ // Same primitive, but not a matchesPendingUnbox hit (e.g. wrong
wrapper
+ // owner). Known boxed value → identity; leave the primitive on
the stack.
+ clearPendingBox();
+ return true;
+ }
+
+ if (!canConvertPrimitive(sourcePrim, targetPrim)) {
+ return false;
+ }
+
+ // Primitive must be on the operand stack before the conversion
opcode(s).
+ clearPendingBox();
+ flushPendingLoad();
+ emitPrimitiveConversion(sourcePrim, targetPrim);
+ return true;
+ }
+
+ /**
+ * Target primitive descriptor of a recognized unbox call, or {@code null}.
+ * Accepts any wrapper owner for {@code xxxValue()} and DTT {@code
xxxUnbox};
+ * owner matching is not required (see {@link
#tryRewriteBoxUnboxConversion}).
+ */
+ private static String unboxTargetPrimitive(final int opcode, final String
owner, final String name, final String descriptor) {
+ if (opcode == INVOKEVIRTUAL && isKnownWrapperOwner(owner)) {
+ Type[] args = Type.getArgumentTypes(descriptor);
+ Type ret = Type.getReturnType(descriptor);
+ if (args.length != 0 || !isBoxablePrimitive(ret)) {
+ return null;
+ }
+ String expectedName =
primitiveValueMethodName(ret.getDescriptor());
+ return expectedName != null && expectedName.equals(name) ?
ret.getDescriptor() : null;
+ }
+ if (opcode == INVOKESTATIC && DTT_OWNER.equals(owner)) {
+ Type[] args = Type.getArgumentTypes(descriptor);
+ Type ret = Type.getReturnType(descriptor);
+ if (args.length != 1 || args[0].getSort() != Type.OBJECT ||
!isBoxablePrimitive(ret)) {
+ return null;
+ }
+ String expectedName =
primitiveUnboxMethodName(ret.getDescriptor());
+ return expectedName != null && expectedName.equals(name) ?
ret.getDescriptor() : null;
+ }
+ return null;
+ }
+
+ private static boolean isKnownWrapperOwner(final String owner) {
+ return "java/lang/Boolean".equals(owner)
+ || "java/lang/Byte".equals(owner)
+ || "java/lang/Character".equals(owner)
+ || "java/lang/Short".equals(owner)
+ || "java/lang/Integer".equals(owner)
+ || "java/lang/Long".equals(owner)
+ || "java/lang/Float".equals(owner)
+ || "java/lang/Double".equals(owner);
+ }
+
+ /**
+ * Whether a JVM primitive conversion from {@code fromDescriptor} to
+ * {@code toDescriptor} exists (including multi-step narrowing such as
+ * {@code long} → {@code byte} via {@code L2I}; {@code I2B}).
+ */
+ private static boolean canConvertPrimitive(final String fromDescriptor,
final String toDescriptor) {
+ if (fromDescriptor == null || toDescriptor == null) {
+ return false;
+ }
+ if (fromDescriptor.equals(toDescriptor)) {
+ return true;
+ }
+ // Boolean has no numeric conversion opcodes here.
+ if ("Z".equals(fromDescriptor) || "Z".equals(toDescriptor)) {
+ return false;
+ }
+ char from = stackCategory(fromDescriptor);
+ char to = toDescriptor.charAt(0);
+ return from != 0 && (to == 'B' || to == 'C' || to == 'S' || to == 'I'
|| to == 'J' || to == 'F' || to == 'D');
+ }
+
+ /**
+ * Stack category used for conversion: {@code B}/{@code C}/{@code
S}/{@code I}
+ * share the int slot ({@code 'I'}); {@code J}/{@code F}/{@code D} keep
their
+ * own category.
+ */
+ private static char stackCategory(final String primitiveDescriptor) {
+ return switch (primitiveDescriptor) {
+ case "B", "C", "S", "I" -> 'I';
+ case "J" -> 'J';
+ case "F" -> 'F';
+ case "D" -> 'D';
+ default -> 0;
+ };
+ }
+
+ /**
+ * Emits the JVM conversion from a pending boxed primitive to {@code
toDescriptor}.
+ * Caller must ensure the source primitive is already on the operand stack
and
+ * that {@link #canConvertPrimitive} is true.
+ */
+ private void emitPrimitiveConversion(final String fromDescriptor, final
String toDescriptor) {
+ if (fromDescriptor.equals(toDescriptor)) {
+ return;
+ }
+ char from = stackCategory(fromDescriptor);
+ char to = toDescriptor.charAt(0);
+ switch (from) {
+ case 'I' -> emitConversionFromInt(to);
+ case 'J' -> emitConversionFromLong(to);
+ case 'F' -> emitConversionFromFloat(to);
+ case 'D' -> emitConversionFromDouble(to);
+ default -> throw new IllegalStateException("unsupported conversion
source: " + fromDescriptor);
+ }
+ }
+
+ private void emitConversionFromInt(final char to) {
+ switch (to) {
+ case 'B' -> super.visitInsn(I2B);
+ case 'C' -> super.visitInsn(I2C);
+ case 'S' -> super.visitInsn(I2S);
+ case 'I' -> { /* already int-slot (byte/short/char/int source) */ }
+ case 'J' -> super.visitInsn(I2L);
+ case 'F' -> super.visitInsn(I2F);
+ case 'D' -> super.visitInsn(I2D);
+ default -> throw new IllegalStateException("unsupported int
conversion target: " + to);
+ }
+ }
+
+ private void emitConversionFromLong(final char to) {
+ switch (to) {
+ case 'B' -> {
+ super.visitInsn(L2I);
+ super.visitInsn(I2B);
+ }
+ case 'C' -> {
+ super.visitInsn(L2I);
+ super.visitInsn(I2C);
+ }
+ case 'S' -> {
+ super.visitInsn(L2I);
+ super.visitInsn(I2S);
+ }
+ case 'I' -> super.visitInsn(L2I);
+ case 'F' -> super.visitInsn(L2F);
+ case 'D' -> super.visitInsn(L2D);
+ default -> throw new IllegalStateException("unsupported long
conversion target: " + to);
+ }
+ }
+
+ private void emitConversionFromFloat(final char to) {
+ switch (to) {
+ case 'B' -> {
+ super.visitInsn(F2I);
+ super.visitInsn(I2B);
+ }
+ case 'C' -> {
+ super.visitInsn(F2I);
+ super.visitInsn(I2C);
+ }
+ case 'S' -> {
+ super.visitInsn(F2I);
+ super.visitInsn(I2S);
+ }
+ case 'I' -> super.visitInsn(F2I);
+ case 'J' -> super.visitInsn(F2L);
+ case 'D' -> super.visitInsn(F2D);
+ default -> throw new IllegalStateException("unsupported float
conversion target: " + to);
+ }
+ }
+
+ private void emitConversionFromDouble(final char to) {
+ switch (to) {
+ case 'B' -> {
+ super.visitInsn(D2I);
+ super.visitInsn(I2B);
+ }
+ case 'C' -> {
+ super.visitInsn(D2I);
+ super.visitInsn(I2C);
+ }
+ case 'S' -> {
+ super.visitInsn(D2I);
+ super.visitInsn(I2S);
+ }
+ case 'I' -> super.visitInsn(D2I);
+ case 'J' -> super.visitInsn(D2L);
+ case 'F' -> super.visitInsn(D2F);
+ default -> throw new IllegalStateException("unsupported double
conversion target: " + to);
+ }
+ }
+
/**
* {@code valueOf}/{@code box} followed by {@code POP}/{@code POP2} means
the
* boxed reference is discarded. Drop the box; if the primitive producer is
diff --git
a/src/test/groovy/org/codehaus/groovy/classgen/asm/PeepholeOptimizingMethodVisitorTest.groovy
b/src/test/groovy/org/codehaus/groovy/classgen/asm/PeepholeOptimizingMethodVisitorTest.groovy
index 4687ecda3d..e837fdda1c 100644
---
a/src/test/groovy/org/codehaus/groovy/classgen/asm/PeepholeOptimizingMethodVisitorTest.groovy
+++
b/src/test/groovy/org/codehaus/groovy/classgen/asm/PeepholeOptimizingMethodVisitorTest.groovy
@@ -1235,7 +1235,8 @@ final class PeepholeOptimizingMethodVisitorTest extends
AbstractBytecodeTestCase
}
@Test
- void doesNotCancelMismatchedBoxUnboxTypes() {
+ void rewritesIntegerValueOfWithIntegerLongValueToI2L() {
+ // Same-wrapper Number conversion: Integer.valueOf; Integer.longValue
→ I2L
def bytecode = sequenceFor('(I)J') {
visitVarInsn(Opcodes.ILOAD, 0)
visitMethodInsn(Opcodes.INVOKESTATIC, 'java/lang/Integer',
'valueOf', '(I)Ljava/lang/Integer;', false)
@@ -1243,12 +1244,7 @@ final class PeepholeOptimizingMethodVisitorTest extends
AbstractBytecodeTestCase
visitInsn(Opcodes.LRETURN)
}
- assert opcodeLines(bytecode) == [
- 'ILOAD 0',
- 'INVOKESTATIC java/lang/Integer.valueOf
(I)Ljava/lang/Integer;',
- 'INVOKEVIRTUAL java/lang/Integer.longValue ()J',
- 'LRETURN',
- ]
+ assert opcodeLines(bytecode) == ['ILOAD 0', 'I2L', 'LRETURN']
}
@Test
@@ -1267,8 +1263,11 @@ final class PeepholeOptimizingMethodVisitorTest extends
AbstractBytecodeTestCase
}
@Test
- void doesNotCancelValueOfWithMismatchedWrapperUnbox() {
- // Integer.valueOf then Long.longValue — different owners, must not
cancel.
+ void rewritesIntegerValueOfWithLongLongValueToI2L() {
+ // Cross-wrapper unbox after a pending box: the value is known to be a
+ // boxed int, so Integer.valueOf; Long.longValue compresses to I2L
+ // (same as long l = intExpr). Real classgen usually emits I2L
directly;
+ // this recovers that shape when an intermediate box/unbox was
produced.
def bytecode = sequenceFor('(I)J') {
visitVarInsn(Opcodes.ILOAD, 0)
visitMethodInsn(Opcodes.INVOKESTATIC, 'java/lang/Integer',
'valueOf', '(I)Ljava/lang/Integer;', false)
@@ -1276,12 +1275,7 @@ final class PeepholeOptimizingMethodVisitorTest extends
AbstractBytecodeTestCase
visitInsn(Opcodes.LRETURN)
}
- assert opcodeLines(bytecode) == [
- 'ILOAD 0',
- 'INVOKESTATIC java/lang/Integer.valueOf
(I)Ljava/lang/Integer;',
- 'INVOKEVIRTUAL java/lang/Long.longValue ()J',
- 'LRETURN',
- ]
+ assert opcodeLines(bytecode) == ['ILOAD 0', 'I2L', 'LRETURN']
}
@Test
@@ -1394,8 +1388,8 @@ final class PeepholeOptimizingMethodVisitorTest extends
AbstractBytecodeTestCase
}
@Test
- void doesNotCancelDttBoxWithMismatchedWrapperUnbox() {
- // DTT.box(int) then Long.longValue — different primitive type, must
not cancel.
+ void rewritesDttBoxIntWithLongLongValueToI2L() {
+ // DTT.box(int) then Long.longValue → I2L (known boxed int → long).
def bytecode = sequenceFor('(I)J') {
visitVarInsn(Opcodes.ILOAD, 0)
visitMethodInsn(Opcodes.INVOKESTATIC,
@@ -1405,14 +1399,110 @@ final class PeepholeOptimizingMethodVisitorTest
extends AbstractBytecodeTestCase
visitInsn(Opcodes.LRETURN)
}
+ assert opcodeLines(bytecode) == ['ILOAD 0', 'I2L', 'LRETURN']
+ }
+
+ @Test
+ void rewritesNumericBoxUnboxConversions() {
+ // Widening / narrowing pairs after valueOf (covers I/J/F/D conversion
tables).
+ def cases = [
+ // load, boxOwner, boxPrim, unboxOwner, unboxName, retPrim,
ret, expected middle opcodes
+ [Opcodes.ILOAD, 'java/lang/Integer', 'I', 'java/lang/Double',
'doubleValue', 'D', Opcodes.DRETURN, ['I2D']],
+ [Opcodes.ILOAD, 'java/lang/Integer', 'I', 'java/lang/Float',
'floatValue', 'F', Opcodes.FRETURN, ['I2F']],
+ [Opcodes.ILOAD, 'java/lang/Integer', 'I', 'java/lang/Byte',
'byteValue', 'B', Opcodes.IRETURN, ['I2B']],
+ [Opcodes.ILOAD, 'java/lang/Integer', 'I', 'java/lang/Short',
'shortValue', 'S', Opcodes.IRETURN, ['I2S']],
+ [Opcodes.ILOAD, 'java/lang/Integer', 'I',
'java/lang/Character', 'charValue', 'C', Opcodes.IRETURN, ['I2C']],
+ [Opcodes.LLOAD, 'java/lang/Long', 'J', 'java/lang/Integer',
'intValue', 'I', Opcodes.IRETURN, ['L2I']],
+ [Opcodes.LLOAD, 'java/lang/Long', 'J', 'java/lang/Float',
'floatValue', 'F', Opcodes.FRETURN, ['L2F']],
+ [Opcodes.LLOAD, 'java/lang/Long', 'J', 'java/lang/Double',
'doubleValue', 'D', Opcodes.DRETURN, ['L2D']],
+ [Opcodes.LLOAD, 'java/lang/Long', 'J', 'java/lang/Byte',
'byteValue', 'B', Opcodes.IRETURN, ['L2I', 'I2B']],
+ [Opcodes.LLOAD, 'java/lang/Long', 'J', 'java/lang/Short',
'shortValue', 'S', Opcodes.IRETURN, ['L2I', 'I2S']],
+ [Opcodes.LLOAD, 'java/lang/Long', 'J', 'java/lang/Character',
'charValue', 'C', Opcodes.IRETURN, ['L2I', 'I2C']],
+ [Opcodes.FLOAD, 'java/lang/Float', 'F', 'java/lang/Integer',
'intValue', 'I', Opcodes.IRETURN, ['F2I']],
+ [Opcodes.FLOAD, 'java/lang/Float', 'F', 'java/lang/Long',
'longValue', 'J', Opcodes.LRETURN, ['F2L']],
+ [Opcodes.FLOAD, 'java/lang/Float', 'F', 'java/lang/Double',
'doubleValue', 'D', Opcodes.DRETURN, ['F2D']],
+ [Opcodes.FLOAD, 'java/lang/Float', 'F', 'java/lang/Byte',
'byteValue', 'B', Opcodes.IRETURN, ['F2I', 'I2B']],
+ [Opcodes.FLOAD, 'java/lang/Float', 'F', 'java/lang/Short',
'shortValue', 'S', Opcodes.IRETURN, ['F2I', 'I2S']],
+ [Opcodes.FLOAD, 'java/lang/Float', 'F', 'java/lang/Character',
'charValue', 'C', Opcodes.IRETURN, ['F2I', 'I2C']],
+ [Opcodes.DLOAD, 'java/lang/Double', 'D', 'java/lang/Integer',
'intValue', 'I', Opcodes.IRETURN, ['D2I']],
+ [Opcodes.DLOAD, 'java/lang/Double', 'D', 'java/lang/Long',
'longValue', 'J', Opcodes.LRETURN, ['D2L']],
+ [Opcodes.DLOAD, 'java/lang/Double', 'D', 'java/lang/Float',
'floatValue', 'F', Opcodes.FRETURN, ['D2F']],
+ [Opcodes.DLOAD, 'java/lang/Double', 'D', 'java/lang/Byte',
'byteValue', 'B', Opcodes.IRETURN, ['D2I', 'I2B']],
+ [Opcodes.DLOAD, 'java/lang/Double', 'D', 'java/lang/Short',
'shortValue', 'S', Opcodes.IRETURN, ['D2I', 'I2S']],
+ [Opcodes.DLOAD, 'java/lang/Double', 'D',
'java/lang/Character', 'charValue', 'C', Opcodes.IRETURN, ['D2I', 'I2C']],
+ // int-slot source (byte) widened to long
+ [Opcodes.ILOAD, 'java/lang/Byte', 'B', 'java/lang/Long',
'longValue', 'J', Opcodes.LRETURN, ['I2L']],
+ // int-slot source to int-slot target of different kind (no-op
on the int slot after box drop)
+ [Opcodes.ILOAD, 'java/lang/Byte', 'B', 'java/lang/Integer',
'intValue', 'I', Opcodes.IRETURN, []],
+ ]
+ cases.each { load, boxOwner, boxPrim, unboxOwner, unboxName, retPrim,
ret, middle ->
+ def bytecode = sequenceFor("($boxPrim)$retPrim") {
+ visitVarInsn(load, 0)
+ visitMethodInsn(Opcodes.INVOKESTATIC, boxOwner, 'valueOf',
"($boxPrim)L${boxOwner};", false)
+ visitMethodInsn(Opcodes.INVOKEVIRTUAL, unboxOwner, unboxName,
"()$retPrim", false)
+ visitInsn(ret)
+ }
+ assert opcodeLines(bytecode) == ["${Printer.OPCODES[load]} 0",
*middle, Printer.OPCODES[ret]]
+ }
+
+ // DTT.box + DTT.longUnbox after int → I2L
+ def dtt = sequenceFor('(I)J') {
+ visitVarInsn(Opcodes.ILOAD, 0)
+ visitMethodInsn(Opcodes.INVOKESTATIC,
+
'org/codehaus/groovy/runtime/typehandling/DefaultTypeTransformation',
+ 'box', '(I)Ljava/lang/Object;', false)
+ visitMethodInsn(Opcodes.INVOKESTATIC,
+
'org/codehaus/groovy/runtime/typehandling/DefaultTypeTransformation',
+ 'longUnbox', '(Ljava/lang/Object;)J', false)
+ visitInsn(Opcodes.LRETURN)
+ }
+ assert opcodeLines(dtt) == ['ILOAD 0', 'I2L', 'LRETURN']
+ }
+
+ @Test
+ void rewritesSameTypeWrongOwnerUnboxAsIdentity() {
+ // Integer.valueOf then Long.intValue: known boxed int, wrong owner →
drop both.
+ def bytecode = sequenceFor('(I)I') {
+ visitVarInsn(Opcodes.ILOAD, 0)
+ visitMethodInsn(Opcodes.INVOKESTATIC, 'java/lang/Integer',
'valueOf', '(I)Ljava/lang/Integer;', false)
+ visitMethodInsn(Opcodes.INVOKEVIRTUAL, 'java/lang/Long',
'intValue', '()I', false)
+ visitInsn(Opcodes.IRETURN)
+ }
+
+ assert opcodeLines(bytecode) == ['ILOAD 0', 'IRETURN']
+ }
+
+ @Test
+ void doesNotConvertBooleanBoxToNumericUnbox() {
+ // Boolean is excluded from numeric conversion rewrites.
+ def bytecode = sequenceFor('(Z)I') {
+ visitVarInsn(Opcodes.ILOAD, 0)
+ visitMethodInsn(Opcodes.INVOKESTATIC, 'java/lang/Boolean',
'valueOf', '(Z)Ljava/lang/Boolean;', false)
+ visitMethodInsn(Opcodes.INVOKEVIRTUAL, 'java/lang/Integer',
'intValue', '()I', false)
+ visitInsn(Opcodes.IRETURN)
+ }
+
assert opcodeLines(bytecode) == [
'ILOAD 0',
- 'INVOKESTATIC
org/codehaus/groovy/runtime/typehandling/DefaultTypeTransformation.box
(I)Ljava/lang/Object;',
- 'INVOKEVIRTUAL java/lang/Long.longValue ()J',
- 'LRETURN',
+ 'INVOKESTATIC java/lang/Boolean.valueOf
(Z)Ljava/lang/Boolean;',
+ 'INVOKEVIRTUAL java/lang/Integer.intValue ()I',
+ 'IRETURN',
]
}
+ @Test
+ void rewritesBoxUnboxConversionAfterFlushedProducer() {
+ def bytecode = sequenceFor('(I)J') {
+ visitVarInsn(Opcodes.ILOAD, 0)
+ visitInsn(Opcodes.NOP) // flush ILOAD; int remains on stack under
the box
+ visitMethodInsn(Opcodes.INVOKESTATIC, 'java/lang/Integer',
'valueOf', '(I)Ljava/lang/Integer;', false)
+ visitMethodInsn(Opcodes.INVOKEVIRTUAL, 'java/lang/Long',
'longValue', '()J', false)
+ visitInsn(Opcodes.LRETURN)
+ }
+
+ assert opcodeLines(bytecode) == ['ILOAD 0', 'NOP', 'I2L', 'LRETURN']
+ }
+
@Test
void flushesPendingBoxBeforeStructuralBoundaries() {
def label = new Label()