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 874133258e Minor tweak
874133258e is described below
commit 874133258e1175b9e831bd1474d5f139025cf1fb
Author: Daniel Sun <[email protected]>
AuthorDate: Fri Jul 17 23:47:19 2026 +0900
Minor tweak
---
.../asm/PeepholeOptimizingMethodVisitor.java | 292 +++++++++++++++++----
src/test/groovy/bugs/Groovy12162.groovy | 80 ++++++
.../asm/PeepholeOptimizingMethodVisitorTest.groovy | 231 +++++++++++++---
3 files changed, 513 insertions(+), 90 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 c152ed8682..425ad72e10 100644
---
a/src/main/java/org/codehaus/groovy/classgen/asm/PeepholeOptimizingMethodVisitor.java
+++
b/src/main/java/org/codehaus/groovy/classgen/asm/PeepholeOptimizingMethodVisitor.java
@@ -115,27 +115,37 @@ import static org.objectweb.asm.Opcodes.SIPUSH;
* Signed floating-point zeros ({@code -0.0f}/{@code -0.0d}) are
preserved
* via raw-bit comparison (GROOVY-9797).</li>
* <li><b>Dead loads</b> — a buffered load or constant followed by a matching
- * {@code POP}/{@code POP2}, or a void {@code RETURN}, is dropped. A
- * buffered {@code IINC} paired with {@code ILOAD} is retained as a side
- * effect when the loaded value itself is discarded.</li>
+ * {@code POP}/{@code POP2}, or a void {@code RETURN}, is dropped when
the
+ * load is pure (no attached {@code CHECKCAST}). A buffered {@code IINC}
+ * paired with {@code ILOAD} is retained as a side effect when the loaded
+ * value itself is discarded.</li>
* <li><b>CHECKCAST</b> — may attach to a pending {@code ALOAD} or reference
- * constant so {@code load}; {@code CHECKCAST}; {@code POP} collapses
- * entirely. A standalone cast before {@code POP} is dropped; before void
- * {@code RETURN} the cast is flushed and the value is popped so the
- * type-check side effect is kept and the void return stays
verifiable.</li>
+ * constant so the cast is emitted immediately after the load on flush.
+ * When the cast result is discarded ({@code POP} or void {@code
RETURN}),
+ * the cast is <em>always</em> kept so a possible {@code
ClassCastException}
+ * remains observable — matching Java and the void-return path. Pure
loads
+ * without a cast still collapse with the pop. Standalone casts of a
value
+ * already on the stack are likewise preserved before pop/return, with
+ * {@code POP} inserted before void {@code RETURN} so the frame stays
+ * verifiable.</li>
* <li><b>Compare-to-zero / null</b> —
* {@code ICONST_0}; {@code IF_ICMP*} → {@code IF*};
* {@code ACONST_NULL}; {@code IF_ACMP*} → {@code IFNULL}/{@code
IFNONNULL}.</li>
* <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)};
- * {@code Wrapper.xxxValue()} and {@code
DefaultTypeTransformation.box(p)};
- * {@code DefaultTypeTransformation.xxxUnbox(...)} with matching
primitive
- * types cancel to a no-op (the primitive remains on the stack). A boxed
- * value discarded by {@code POP}/{@code POP2} drops the box and pops the
- * original primitive instead (using {@code POP2} when the primitive is
- * wide). Mismatched unbox types are left intact.</li>
+ * <li><b>Box/unbox cancellation</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>
* <li><b>Boolean constant folding</b> — {@code GETSTATIC Boolean.TRUE/FALSE}
* followed by {@code booleanValue()} or
* {@code DefaultTypeTransformation.booleanUnbox} becomes
@@ -211,9 +221,11 @@ public final class PeepholeOptimizingMethodVisitor extends
MethodVisitor {
* <ul>
* <li>When {@link #pendingLoadKind} is {@link PendingLoadKind#VARIABLE}
or
* {@link PendingLoadKind#CONSTANT}: optional cast attached to that
load
- * (emitted after the load on flush, or dropped with the load on
pop).</li>
+ * (emitted after the load on flush; retained with the load when the
+ * value is discarded so the type-check side effect is
preserved).</li>
* <li>When the kind is {@link PendingLoadKind#CHECKCAST}: standalone
cast of
- * a value already written to the operand stack.</li>
+ * a value already written to the operand stack (also retained on
+ * discard for the same reason).</li>
* </ul>
*/
private String pendingCheckcastDescriptor;
@@ -620,11 +632,16 @@ public final class PeepholeOptimizingMethodVisitor
extends MethodVisitor {
/**
* Emits a buffered {@code Wrapper.valueOf} or
* {@code DefaultTypeTransformation.box} call to the delegate.
+ * <p>
+ * The primitive operand may still be held in the load window (so that
+ * {@code load}; {@code box}; {@code POP} can collapse entirely). Flush
that
+ * load first so the box always sees its argument on the operand stack.
*/
private void flushPendingBox() {
if (pendingBoxKind == PendingBoxKind.NONE) {
return;
}
+ flushPendingLoad();
switch (pendingBoxKind) {
case VALUE_OF:
super.visitMethodInsn(INVOKESTATIC, pendingBoxOwner, "valueOf",
@@ -718,18 +735,21 @@ public final class PeepholeOptimizingMethodVisitor
extends MethodVisitor {
}
/**
- * Drops a dead buffered load/constant (or standalone {@code CHECKCAST})
when
- * the next instruction is a matching {@code POP}/{@code POP2}.
+ * Handles a matching {@code POP}/{@code POP2} against the load window.
* <p>
- * For a standalone cast the value is already on the stack, so only the
cast is
- * removed and the pop is still emitted. For a buffered load, both the
load and
- * any attached cast are removed; a paired {@code IINC} is preserved as a
side
- * effect.
+ * Pure dead loads/constants are dropped (paired {@code IINC} kept). A
+ * {@code CHECKCAST} — whether attached to the load or standalone — is
+ * <em>not</em> dead: it is emitted so a possible {@code
ClassCastException}
+ * stays observable, and the pop still discards the value. This matches the
+ * void-{@code RETURN} path ({@link #tryDropPendingLoadOnReturn}) and
Java's
+ * treatment of discarded cast expressions.
*
* @return {@code true} if the pop was fully handled
*/
private boolean tryRemovePendingLoad(final int opcode) {
- if (pendingLoadKind == PendingLoadKind.NONE) {
+ // A pending box sits on top of any buffered load; only the box path
may
+ // discard in that case (see tryDropPendingBoxOnPop).
+ if (pendingLoadKind == PendingLoadKind.NONE || pendingBoxKind !=
PendingBoxKind.NONE) {
return false;
}
@@ -739,15 +759,27 @@ public final class PeepholeOptimizingMethodVisitor
extends MethodVisitor {
}
if (pendingLoadKind == PendingLoadKind.CHECKCAST) {
- // Value already on stack: drop the dead cast, keep the pop.
+ // Value already on stack: keep the type-check, keep the pop.
if (opcode != POP) {
return false;
}
+ super.visitTypeInsn(CHECKCAST, pendingCheckcastDescriptor);
clearPendingLoad();
super.visitInsn(POP);
return true;
}
+ // Attached CHECKCAST: same side-effect rule as the void-RETURN path.
+ if (pendingCheckcastDescriptor != null) {
+ if (opcode != POP) {
+ // Reference values occupy one slot; only POP discards them
cleanly.
+ return false;
+ }
+ flushPendingLoad();
+ super.visitInsn(POP);
+ return true;
+ }
+
if (stackSizeForPendingLoad() != popSize) {
return false;
}
@@ -758,8 +790,10 @@ public final class PeepholeOptimizingMethodVisitor extends
MethodVisitor {
}
/**
- * Handles void {@code RETURN} against the load window:
+ * Handles void {@code RETURN} against the load and box windows:
* <ul>
+ * <li>Pending box — the box is dropped; a still-buffered pure primitive
+ * producer is dropped too, otherwise the primitive is popped.</li>
* <li>Pure dead load/constant — dropped (paired {@code IINC} kept).</li>
* <li>Load with attached {@code CHECKCAST}, or standalone cast — the
cast is
* emitted so a type-check side effect is preserved, then {@code
POP} clears
@@ -769,7 +803,17 @@ public final class PeepholeOptimizingMethodVisitor extends
MethodVisitor {
* @return {@code true} if the return was fully handled
*/
private boolean tryDropPendingLoadOnReturn(final int opcode) {
- if (opcode != RETURN || pendingLoadKind == PendingLoadKind.NONE) {
+ if (opcode != RETURN) {
+ return false;
+ }
+
+ if (pendingBoxKind != PendingBoxKind.NONE) {
+ dropPendingBoxDiscardingValue();
+ super.visitInsn(RETURN);
+ return true;
+ }
+
+ if (pendingLoadKind == PendingLoadKind.NONE) {
return false;
}
@@ -1013,8 +1057,14 @@ public final class PeepholeOptimizingMethodVisitor
extends MethodVisitor {
/**
* {@code valueOf}/{@code box} followed by {@code POP}/{@code POP2} means
the
- * boxed reference is discarded. Drop the box and pop the original
primitive
- * instead ({@code POP2} when the primitive is {@code long} or {@code
double}).
+ * boxed reference is discarded. Drop the box; if the primitive producer is
+ * still held in the load window, drop that too, otherwise pop the original
+ * primitive ({@code POP2} when wide).
+ * <p>
+ * {@code POP} after boxing a wide primitive is legal (the boxed reference
is
+ * one slot) and is rewritten to {@code POP2} of the long/double.
+ * {@code POP2} after boxing a <em>narrow</em> primitive is treated as two
+ * one-slot discards: the boxed value plus the slot underneath.
*
* @return {@code true} if the pop was fully handled
*/
@@ -1027,17 +1077,66 @@ public final class PeepholeOptimizingMethodVisitor
extends MethodVisitor {
return false;
}
int primitiveSize =
stackSizeForPrimitiveDescriptor(pendingBoxPrimitiveDescriptor);
- // POP after boxing a wide primitive is legal (boxed ref is one slot)
→ POP2 the long/double.
- if (popSize == 1 && primitiveSize == 2) {
+
+ // Discard only the boxed value. POP on a wide primitive uses POP2 for
the
+ // long/double that remains after the box is dropped.
+ if (popSize == 1 || (popSize == 2 && primitiveSize == 2)) {
+ int slotsToRemove = (popSize == 1 && primitiveSize == 2) ? 2 :
primitiveSize;
clearPendingBox();
- super.visitInsn(POP2);
+ if (tryCancelPendingPrimitiveProducer(slotsToRemove)) {
+ return true;
+ }
+ super.visitInsn(slotsToRemove == 2 ? POP2 : POP);
return true;
}
- if (popSize != primitiveSize) {
- return false;
+
+ // POP2 on a narrow boxed value: two one-slot discards (box +
underneath).
+ if (popSize == 2 && primitiveSize == 1) {
+ clearPendingBox();
+ if (tryCancelPendingPrimitiveProducer(1)) {
+ super.visitInsn(POP);
+ } else {
+ super.visitInsn(POP2);
+ }
+ return true;
}
+
+ return false;
+ }
+
+ /**
+ * Drops a pending box whose result is unused at a void {@code RETURN},
+ * cancelling a still-buffered pure primitive producer when possible.
+ */
+ private void dropPendingBoxDiscardingValue() {
+ int primitiveSize =
stackSizeForPrimitiveDescriptor(pendingBoxPrimitiveDescriptor);
clearPendingBox();
- super.visitInsn(primitiveSize == 2 ? POP2 : POP);
+ if (!tryCancelPendingPrimitiveProducer(primitiveSize)) {
+ super.visitInsn(primitiveSize == 2 ? POP2 : POP);
+ }
+ }
+
+ /**
+ * Cancels a pure pending load/constant that produces a primitive of
+ * {@code primitiveSize} slots (the operand of a discarded box). Attached
+ * {@code CHECKCAST} and reference loads are never cancelled here.
+ *
+ * @return {@code true} if a producer was cancelled (nothing left on stack)
+ */
+ private boolean tryCancelPendingPrimitiveProducer(final int primitiveSize)
{
+ if (pendingLoadKind == PendingLoadKind.NONE
+ || pendingLoadKind == PendingLoadKind.CHECKCAST
+ || pendingCheckcastDescriptor != null) {
+ return false;
+ }
+ if (pendingLoadKind == PendingLoadKind.VARIABLE && pendingLoadOpcode
== ALOAD) {
+ return false;
+ }
+ if (stackSizeForPendingLoad() != primitiveSize) {
+ return false;
+ }
+ emitPreservedIincSideEffect();
+ clearPendingLoad();
return true;
}
@@ -1047,9 +1146,13 @@ public final class PeepholeOptimizingMethodVisitor
extends MethodVisitor {
/**
* Buffers {@code Wrapper.valueOf(prim)} or {@code
DefaultTypeTransformation.box(prim)}
- * so a following matching unbox (or discard via pop) can collapse the
pair.
- * Any previously pending state is flushed first so the primitive producer
is
- * already on the operand stack.
+ * so a following matching unbox (or discard via pop/return) can collapse
the pair.
+ * <p>
+ * When the load window already holds a pure primitive producer of the
right
+ * size, that load is kept pending under the box so {@code load}; {@code
box};
+ * {@code POP} can collapse to nothing. Otherwise the load (and any prior
box)
+ * is flushed so the primitive is on the operand stack before the box is
+ * recorded.
*
* @return {@code true} if the call was buffered
*/
@@ -1062,7 +1165,7 @@ public final class PeepholeOptimizingMethodVisitor
extends MethodVisitor {
if (primitive == null) {
return false;
}
- flushPending();
+ prepareBoxBuffer(primitive);
bufferValueOfBox(owner, primitive, isInterface);
return true;
}
@@ -1071,37 +1174,114 @@ public final class PeepholeOptimizingMethodVisitor
extends MethodVisitor {
if (primitive == null) {
return false;
}
- flushPending();
+ prepareBoxBuffer(primitive);
bufferDttBox(primitive);
return true;
}
return false;
}
+ /**
+ * Ensures the operand stack / load window is ready to record a new box of
+ * {@code primitiveDescriptor}. Keeps a matching pure pending load so a
later
+ * discard can eliminate both; flushes everything else.
+ */
+ private void prepareBoxBuffer(final String primitiveDescriptor) {
+ if (pendingBoxKind != PendingBoxKind.NONE) {
+ // Previous box must be emitted with its operand already on the
stack.
+ flushPendingLoad();
+ flushPendingDupStore();
+ flushPendingBox();
+ return;
+ }
+ if (!pendingLoadProducesPrimitive(primitiveDescriptor)) {
+ flushPendingLoad();
+ }
+ flushPendingDupStore();
+ }
+
+ /**
+ * {@code true} when the load window holds a pure primitive producer whose
+ * stack size matches {@code primitiveDescriptor} (suitable to keep under a
+ * pending box). Reference loads, casts, and size mismatches return
+ * {@code false}.
+ */
+ private boolean pendingLoadProducesPrimitive(final String
primitiveDescriptor) {
+ if (pendingLoadKind == PendingLoadKind.NONE
+ || pendingLoadKind == PendingLoadKind.CHECKCAST
+ || pendingCheckcastDescriptor != null) {
+ return false;
+ }
+ int need = stackSizeForPrimitiveDescriptor(primitiveDescriptor);
+ return switch (pendingLoadKind) {
+ case VARIABLE -> pendingLoadOpcode != ALOAD
+ && stackSizeForLoadOpcode(pendingLoadOpcode) == need;
+ case CONSTANT -> pendingConstant != null
+ && stackSizeForPendingLoad() == need
+ && constantCompatibleWithPrimitive(pendingConstant,
primitiveDescriptor);
+ case BOOLEAN_CONSTANT -> "Z".equals(primitiveDescriptor);
+ default -> false;
+ };
+ }
+
+ /**
+ * Whether a buffered constant can be the operand of a box for
+ * {@code primitiveDescriptor}. Size agreement is required; floating vs
+ * integral kinds must not mix.
+ */
+ private static boolean constantCompatibleWithPrimitive(final Object value,
final String primitiveDescriptor) {
+ return switch (primitiveDescriptor) {
+ case "Z", "B", "C", "S", "I" -> value instanceof Integer || value
instanceof Boolean;
+ case "J" -> value instanceof Long;
+ case "F" -> value instanceof Float;
+ case "D" -> value instanceof Double;
+ default -> false;
+ };
+ }
+
private boolean matchesPendingUnbox(final int opcode, final String owner,
final String name, final String descriptor) {
String expectedPrim = pendingBoxPrimitiveDescriptor;
- if (expectedPrim == null) {
+ if (expectedPrim == null || pendingBoxKind == PendingBoxKind.NONE) {
return false;
}
- if (pendingBoxKind == PendingBoxKind.VALUE_OF) {
- // Wrapper.xxxValue()X — e.g. Integer.intValue()I
- if (opcode != INVOKEVIRTUAL || !pendingBoxOwner.equals(owner)) {
- return false;
+ // Same-type Wrapper.xxxValue() — valid after valueOf or DTT.box.
+ if (isWrapperPrimitiveUnbox(opcode, owner, name, descriptor,
expectedPrim)) {
+ if (pendingBoxKind == PendingBoxKind.VALUE_OF) {
+ return pendingBoxOwner.equals(owner);
}
- String expectedName = primitiveValueMethodName(expectedPrim);
- String expectedDesc = "()" + expectedPrim;
- return expectedName != null && expectedName.equals(name) &&
expectedDesc.equals(descriptor);
+ // DTT.box → Object at the type system, but runtime value is the
wrapper.
+ return
owner.equals(wrapperInternalNameForDescriptor(expectedPrim));
}
- if (pendingBoxKind == PendingBoxKind.DTT_BOX) {
- // DefaultTypeTransformation.xxxUnbox(Object)X
- if (opcode != INVOKESTATIC || !DTT_OWNER.equals(owner)) {
- return false;
- }
- String expectedName = primitiveUnboxMethodName(expectedPrim);
- String expectedDesc = "(Ljava/lang/Object;)" + expectedPrim;
- return expectedName != null && expectedName.equals(name) &&
expectedDesc.equals(descriptor);
+ // Same-type DTT.xxxUnbox — valid after valueOf or DTT.box.
+ return isDttPrimitiveUnbox(opcode, owner, name, descriptor,
expectedPrim);
+ }
+
+ private static boolean isWrapperPrimitiveUnbox(final int opcode, final
String owner, final String name,
+ final String descriptor,
final String primitiveDescriptor) {
+ if (opcode != INVOKEVIRTUAL) {
+ return false;
}
- return false;
+ String expectedName = primitiveValueMethodName(primitiveDescriptor);
+ String expectedDesc = "()" + primitiveDescriptor;
+ return expectedName != null && expectedName.equals(name) &&
expectedDesc.equals(descriptor)
+ &&
owner.equals(wrapperInternalNameForDescriptor(primitiveDescriptor));
+ }
+
+ private static boolean isDttPrimitiveUnbox(final int opcode, final String
owner, final String name,
+ final String descriptor, final
String primitiveDescriptor) {
+ if (opcode != INVOKESTATIC || !DTT_OWNER.equals(owner)) {
+ return false;
+ }
+ String expectedName = primitiveUnboxMethodName(primitiveDescriptor);
+ String expectedDesc = "(Ljava/lang/Object;)" + primitiveDescriptor;
+ return expectedName != null && expectedName.equals(name) &&
expectedDesc.equals(descriptor);
+ }
+
+ private static String wrapperInternalNameForDescriptor(final String
primitiveDescriptor) {
+ if (primitiveDescriptor == null || primitiveDescriptor.length() != 1) {
+ return null;
+ }
+ return wrapperInternalName(Type.getType(primitiveDescriptor));
}
private static boolean isBooleanTrueFalse(final String owner, final String
name, final String descriptor) {
diff --git a/src/test/groovy/bugs/Groovy12162.groovy
b/src/test/groovy/bugs/Groovy12162.groovy
new file mode 100644
index 0000000000..ad91d4bd37
--- /dev/null
+++ b/src/test/groovy/bugs/Groovy12162.groovy
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package bugs
+
+import org.codehaus.groovy.classgen.asm.AbstractBytecodeTestCase
+import org.junit.jupiter.api.Test
+
+import static groovy.test.GroovyAssert.assertScript
+import static groovy.test.GroovyAssert.shouldFail
+
+/**
+ * GROOVY-12162: discarded casts must keep their type-check side effect.
+ * <p>
+ * Peephole compaction treats {@code CHECKCAST} before {@code POP} and before
+ * void {@code RETURN} the same way — the cast is kept. Opcode-level coverage
+ * lives in {@code PeepholeOptimizingMethodVisitorTest}; these tests pin the
+ * observable end-to-end behaviour under {@code @CompileStatic}.
+ */
+final class Groovy12162 extends AbstractBytecodeTestCase {
+
+ @Test
+ void discardedGroovyCastKeepsSideEffect() {
+ // SC emits invokedynamic cast (not bare CHECKCAST) for a general
Groovy
+ // cast; the call must not be dropped when the result is discarded.
+ def err =
shouldFail(org.codehaus.groovy.runtime.typehandling.GroovyCastException, '''
+ @groovy.transform.CompileStatic
+ void m(Object o) {
+ (Thread) o
+ }
+ m(42)
+ ''')
+ assert err.message.contains('Thread')
+ }
+
+ @Test
+ void discardedGroovyCastSucceedsWhenConversionApplies() {
+ assertScript '''
+ @groovy.transform.CompileStatic
+ void m(Object o) {
+ (String) o
+ }
+ m(42) // Integer → String via Groovy cast
+ m('ok')
+ '''
+ }
+
+ @Test
+ void discardedCheckcastAfterInstanceofIsPreservedInBytecode() {
+ // After instanceof String, SC emits a bare CHECKCAST for a discarded
+ // (String) o. The peephole must not erase that CHECKCAST (POP path).
+ def bytecode = compile(method: 'm', '''
+ @groovy.transform.CompileStatic
+ void m(Object o) {
+ if (o instanceof String) {
+ (String) o
+ }
+ }
+ ''')
+ assert bytecode.hasStrictSequence([
+ 'CHECKCAST java/lang/String',
+ 'POP',
+ ])
+ }
+}
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 ee2618a32c..8a1a29d972 100644
---
a/src/test/groovy/org/codehaus/groovy/classgen/asm/PeepholeOptimizingMethodVisitorTest.groovy
+++
b/src/test/groovy/org/codehaus/groovy/classgen/asm/PeepholeOptimizingMethodVisitorTest.groovy
@@ -330,7 +330,7 @@ final class PeepholeOptimizingMethodVisitorTest {
}
@Test
- void removesLoadAndAttachedCheckcastBeforePop() {
+ void preservesLoadAndAttachedCheckcastBeforePop() {
def bytecode = sequenceFor('(Ljava/lang/Object;)V') {
visitVarInsn(Opcodes.ALOAD, 0)
visitTypeInsn(Opcodes.CHECKCAST, 'java/lang/String')
@@ -338,12 +338,17 @@ final class PeepholeOptimizingMethodVisitorTest {
visitInsn(RETURN)
}
- // ALOAD + CHECKCAST are both dead once the value is discarded.
- assert opcodeLines(bytecode) == ['RETURN']
+ // Discarded cast must keep its ClassCastException side effect (same
rule as void RETURN).
+ assert opcodeLines(bytecode) == [
+ 'ALOAD 0',
+ 'CHECKCAST java/lang/String',
+ 'POP',
+ 'RETURN',
+ ]
}
@Test
- void removesStandaloneCheckcastBeforePop() {
+ void preservesStandaloneCheckcastBeforePop() {
def bytecode = sequenceFor('(Ljava/lang/Object;)V') {
visitVarInsn(Opcodes.ALOAD, 0)
visitInsn(Opcodes.NOP) // force the load to flush so CHECKCAST is
standalone
@@ -352,7 +357,14 @@ final class PeepholeOptimizingMethodVisitorTest {
visitInsn(RETURN)
}
- assert opcodeLines(bytecode) == ['ALOAD 0', 'NOP', 'POP', 'RETURN']
+ // Standalone cast is preserved; ALOAD is already flushed (NOP
boundary), so it stays.
+ assert opcodeLines(bytecode) == [
+ 'ALOAD 0',
+ 'NOP',
+ 'CHECKCAST java/lang/String',
+ 'POP',
+ 'RETURN',
+ ]
}
@Test
@@ -443,16 +455,14 @@ final class PeepholeOptimizingMethodVisitorTest {
@Test
void preservesStandaloneCheckcastBeforeReturn() {
- def bytecode = sequenceFor('(Ljava/lang/Object;)V') {
+ // POP and void RETURN use the same rule: keep CHECKCAST, discard the
value.
+ def withPop = sequenceFor('(Ljava/lang/Object;)V') {
visitVarInsn(Opcodes.ALOAD, 0)
- visitInsn(Opcodes.NOP)
+ visitInsn(Opcodes.NOP) // flush ALOAD so the cast is standalone
visitTypeInsn(Opcodes.CHECKCAST, 'java/lang/String')
visitInsn(Opcodes.POP)
visitInsn(RETURN)
}
-
- // Standalone CHECKCAST before POP is dropped; before RETURN it is
flushed
- // and the value is popped so the void return remains verifiable.
def returnOnly = sequenceFor('(Ljava/lang/Object;)V') {
visitVarInsn(Opcodes.ALOAD, 0)
visitInsn(Opcodes.NOP)
@@ -460,8 +470,20 @@ final class PeepholeOptimizingMethodVisitorTest {
visitInsn(RETURN)
}
- assert opcodeLines(bytecode) == ['ALOAD 0', 'NOP', 'POP', 'RETURN']
- assert opcodeLines(returnOnly) == ['ALOAD 0', 'NOP', 'CHECKCAST
java/lang/String', 'POP', 'RETURN']
+ assert opcodeLines(withPop) == [
+ 'ALOAD 0',
+ 'NOP',
+ 'CHECKCAST java/lang/String',
+ 'POP',
+ 'RETURN',
+ ]
+ assert opcodeLines(returnOnly) == [
+ 'ALOAD 0',
+ 'NOP',
+ 'CHECKCAST java/lang/String',
+ 'POP',
+ 'RETURN',
+ ]
}
@Test
@@ -592,7 +614,7 @@ final class PeepholeOptimizingMethodVisitorTest {
}
@Test
- void attachesCheckcastToReferenceConstantsAndDropsDeadCastChains() {
+ void attachesCheckcastToReferenceConstantsAndPreservesDiscardedCasts() {
def type = org.objectweb.asm.Type.getType(String)
def handle = new Handle(Opcodes.H_INVOKESTATIC, 'Owner', 'boot',
'()V', false)
def bytecode = sequenceFor {
@@ -611,7 +633,22 @@ final class PeepholeOptimizingMethodVisitorTest {
visitInsn(RETURN)
}
- assert opcodeLines(bytecode) == ['RETURN']
+ // Casts attach to reference constants but are kept on discard (CCE
side effect).
+ assert opcodeLines(bytecode) == [
+ 'LDC "text"',
+ 'CHECKCAST java/lang/String',
+ 'POP',
+ 'LDC Ljava/lang/String;.class',
+ 'CHECKCAST java/lang/Class',
+ 'POP',
+ 'LDC Owner.boot()V',
+ 'CHECKCAST java/lang/invoke/MethodHandle',
+ 'POP',
+ 'ACONST_NULL',
+ 'CHECKCAST java/lang/String',
+ 'POP',
+ 'RETURN',
+ ]
}
@Test
@@ -865,20 +902,27 @@ final class PeepholeOptimizingMethodVisitorTest {
}
@Test
- void dropsDeadLoadWithAttachedCheckcastOnPopOnly() {
+ void preservesAttachedCheckcastOnConstantBeforePop() {
def withPop = sequenceFor('(Ljava/lang/Object;)V') {
visitLdcInsn('x')
visitTypeInsn(Opcodes.CHECKCAST, 'java/lang/String')
visitInsn(Opcodes.POP)
visitInsn(RETURN)
}
- assert opcodeLines(withPop) == ['RETURN']
+ // Cast side effect retained even for a reference constant (consistent
with ALOAD path).
+ assert opcodeLines(withPop) == [
+ 'LDC "x"',
+ 'CHECKCAST java/lang/String',
+ 'POP',
+ 'RETURN',
+ ]
+ // POP2 does not match a one-slot cast result; cast is flushed
unchanged with POP2.
def standalonePop2 = sequenceFor('(Ljava/lang/Object;)V') {
visitVarInsn(Opcodes.ALOAD, 0)
- visitInsn(Opcodes.NOP)
+ visitInsn(Opcodes.NOP) // flush ALOAD so the cast is standalone
visitTypeInsn(Opcodes.CHECKCAST, 'java/lang/String')
- visitInsn(Opcodes.POP2) // only POP removes standalone cast
+ visitInsn(Opcodes.POP2)
visitInsn(RETURN)
}
assert opcodeLines(standalonePop2) == [
@@ -989,7 +1033,8 @@ final class PeepholeOptimizingMethodVisitorTest {
visitInsn(RETURN)
}
- assert opcodeLines(bytecode) == ['ILOAD 0', 'POP', 'RETURN']
+ // Box and pure ILOAD are both dead once the boxed value is discarded.
+ assert opcodeLines(bytecode) == ['RETURN']
}
@Test
@@ -1001,7 +1046,7 @@ final class PeepholeOptimizingMethodVisitorTest {
visitInsn(RETURN)
}
- assert opcodeLines(bytecode) == ['LLOAD 0', 'POP2', 'RETURN']
+ assert opcodeLines(bytecode) == ['RETURN']
}
@Test
@@ -1015,26 +1060,115 @@ final class PeepholeOptimizingMethodVisitorTest {
visitInsn(RETURN)
}
- // POP2 size matches the wide primitive that was boxed → drop box, pop
the double.
- assert opcodeLines(bytecode) == ['DLOAD 0', 'POP2', 'RETURN']
+ // POP2 matches the wide primitive under the box → drop box and
producer.
+ assert opcodeLines(bytecode) == ['RETURN']
}
@Test
- void doesNotTreatPop2AsDiscardOfNarrowBoxedValue() {
+ void treatsPop2OnNarrowBoxedValueAsTwoSlotDiscard() {
+ // Stack before POP2: [underneath, boxed]. Cancel pure ILOAD under the
box, POP the underneath.
def bytecode = sequenceFor('(I)V') {
+ visitInsn(Opcodes.ICONST_0)
visitVarInsn(Opcodes.ILOAD, 0)
visitMethodInsn(Opcodes.INVOKESTATIC, 'java/lang/Integer',
'valueOf', '(I)Ljava/lang/Integer;', false)
visitInsn(Opcodes.POP2)
visitInsn(RETURN)
}
- // POP2 does not match a one-slot boxed reference; flush the box and
keep POP2.
- assert opcodeLines(bytecode) == [
- 'ILOAD 0',
- 'INVOKESTATIC java/lang/Integer.valueOf
(I)Ljava/lang/Integer;',
- 'POP2',
- 'RETURN',
- ]
+ assert opcodeLines(bytecode) == ['ICONST_0', 'POP', 'RETURN']
+ }
+
+ @Test
+ void dropsBoxedValueDiscardedByVoidReturn() {
+ def bytecode = sequenceFor('(I)V') {
+ visitVarInsn(Opcodes.ILOAD, 0)
+ visitMethodInsn(Opcodes.INVOKESTATIC, 'java/lang/Integer',
'valueOf', '(I)Ljava/lang/Integer;', false)
+ visitInsn(RETURN)
+ }
+
+ assert opcodeLines(bytecode) == ['RETURN']
+ }
+
+ @Test
+ void dropsBoxedConstantDiscardedByPop() {
+ def bytecode = sequenceFor {
+ visitInsn(Opcodes.ICONST_1)
+ visitMethodInsn(Opcodes.INVOKESTATIC, 'java/lang/Integer',
'valueOf', '(I)Ljava/lang/Integer;', false)
+ visitInsn(Opcodes.POP)
+ visitInsn(RETURN)
+ }
+
+ assert opcodeLines(bytecode) == ['RETURN']
+ }
+
+ @Test
+ void preservesIincWhenBoxedLoadIsDiscarded() {
+ def bytecode = sequenceFor('(I)V') {
+ visitVarInsn(Opcodes.ILOAD, 0)
+ visitIincInsn(0, 1)
+ visitMethodInsn(Opcodes.INVOKESTATIC, 'java/lang/Integer',
'valueOf', '(I)Ljava/lang/Integer;', false)
+ visitInsn(Opcodes.POP)
+ visitInsn(RETURN)
+ }
+
+ // IINC is a side effect of the load window and must survive dead-box
elimination.
+ assert opcodeLines(bytecode) == ['IINC 0 1', 'RETURN']
+ }
+
+ @Test
+ void popsPrimitiveWhenBoxedProducerAlreadyFlushed() {
+ // NOP flushes ILOAD before valueOf is seen, so only the box is
pending.
+ def bytecode = sequenceFor('(I)V') {
+ visitVarInsn(Opcodes.ILOAD, 0)
+ visitInsn(Opcodes.NOP)
+ visitMethodInsn(Opcodes.INVOKESTATIC, 'java/lang/Integer',
'valueOf', '(I)Ljava/lang/Integer;', false)
+ visitInsn(Opcodes.POP)
+ visitInsn(RETURN)
+ }
+
+ assert opcodeLines(bytecode) == ['ILOAD 0', 'NOP', 'POP', 'RETURN']
+ }
+
+ @Test
+ void popsWidePrimitiveWhenBoxedProducerAlreadyFlushed() {
+ def bytecode = sequenceFor('(J)V') {
+ visitVarInsn(Opcodes.LLOAD, 0)
+ visitInsn(Opcodes.NOP)
+ visitMethodInsn(Opcodes.INVOKESTATIC, 'java/lang/Long', 'valueOf',
'(J)Ljava/lang/Long;', false)
+ visitInsn(Opcodes.POP)
+ visitInsn(RETURN)
+ }
+
+ // POP of boxed long → POP2 of the long still on the stack.
+ assert opcodeLines(bytecode) == ['LLOAD 0', 'NOP', 'POP2', 'RETURN']
+ }
+
+ @Test
+ void treatsPop2OnFlushedNarrowBoxAsTwoSlotDiscard() {
+ def bytecode = sequenceFor('(I)V') {
+ visitInsn(Opcodes.ICONST_0)
+ visitVarInsn(Opcodes.ILOAD, 0)
+ visitInsn(Opcodes.NOP) // flush ILOAD; int remains on stack under
the later box
+ visitMethodInsn(Opcodes.INVOKESTATIC, 'java/lang/Integer',
'valueOf', '(I)Ljava/lang/Integer;', false)
+ visitInsn(Opcodes.POP2)
+ visitInsn(RETURN)
+ }
+
+ // Drop box; POP2 removes the flushed int and the ICONST underneath.
+ assert opcodeLines(bytecode) == ['ICONST_0', 'ILOAD 0', 'NOP', 'POP2',
'RETURN']
+ }
+
+ @Test
+ void cancelsBoxUnboxAfterFlushedProducer() {
+ def bytecode = sequenceFor('(I)I') {
+ visitVarInsn(Opcodes.ILOAD, 0)
+ visitInsn(Opcodes.NOP)
+ visitMethodInsn(Opcodes.INVOKESTATIC, 'java/lang/Integer',
'valueOf', '(I)Ljava/lang/Integer;', false)
+ visitMethodInsn(Opcodes.INVOKEVIRTUAL, 'java/lang/Integer',
'intValue', '()I', false)
+ visitInsn(Opcodes.IRETURN)
+ }
+
+ assert opcodeLines(bytecode) == ['ILOAD 0', 'NOP', 'IRETURN']
}
@Test
@@ -1185,8 +1319,8 @@ final class PeepholeOptimizingMethodVisitorTest {
}
@Test
- void doesNotCancelDttBoxWithValueOfStyleUnbox() {
- // DTT.box then Integer.intValue — different unbox convention, must
not cancel.
+ void cancelsDttBoxWithMatchingWrapperUnbox() {
+ // Same primitive type: DTT.box then Integer.intValue is an identity
on the int.
def bytecode = sequenceFor('(I)I') {
visitVarInsn(Opcodes.ILOAD, 0)
visitMethodInsn(Opcodes.INVOKESTATIC,
@@ -1196,11 +1330,40 @@ final class PeepholeOptimizingMethodVisitorTest {
visitInsn(Opcodes.IRETURN)
}
+ assert opcodeLines(bytecode) == ['ILOAD 0', 'IRETURN']
+ }
+
+ @Test
+ void cancelsValueOfWithMatchingDttUnbox() {
+ def bytecode = sequenceFor('(I)I') {
+ visitVarInsn(Opcodes.ILOAD, 0)
+ visitMethodInsn(Opcodes.INVOKESTATIC, 'java/lang/Integer',
'valueOf', '(I)Ljava/lang/Integer;', false)
+ visitMethodInsn(Opcodes.INVOKESTATIC,
+
'org/codehaus/groovy/runtime/typehandling/DefaultTypeTransformation',
+ 'intUnbox', '(Ljava/lang/Object;)I', false)
+ visitInsn(Opcodes.IRETURN)
+ }
+
+ assert opcodeLines(bytecode) == ['ILOAD 0', 'IRETURN']
+ }
+
+ @Test
+ void doesNotCancelDttBoxWithMismatchedWrapperUnbox() {
+ // DTT.box(int) then Long.longValue — different primitive type, must
not cancel.
+ def bytecode = sequenceFor('(I)J') {
+ visitVarInsn(Opcodes.ILOAD, 0)
+ visitMethodInsn(Opcodes.INVOKESTATIC,
+
'org/codehaus/groovy/runtime/typehandling/DefaultTypeTransformation',
+ 'box', '(I)Ljava/lang/Object;', false)
+ visitMethodInsn(Opcodes.INVOKEVIRTUAL, 'java/lang/Long',
'longValue', '()J', false)
+ visitInsn(Opcodes.LRETURN)
+ }
+
assert opcodeLines(bytecode) == [
'ILOAD 0',
'INVOKESTATIC
org/codehaus/groovy/runtime/typehandling/DefaultTypeTransformation.box
(I)Ljava/lang/Object;',
- 'INVOKEVIRTUAL java/lang/Integer.intValue ()I',
- 'IRETURN',
+ 'INVOKEVIRTUAL java/lang/Long.longValue ()J',
+ 'LRETURN',
]
}