diff --git a/src/main/java/org/codehaus/groovy/classgen/AsmClassGenerator.java b/src/main/java/org/codehaus/groovy/classgen/AsmClassGenerator.java index 46bea1fb5d3..c0fc16f8ec7 100644 --- a/src/main/java/org/codehaus/groovy/classgen/AsmClassGenerator.java +++ b/src/main/java/org/codehaus/groovy/classgen/AsmClassGenerator.java @@ -118,7 +118,6 @@ import org.objectweb.asm.Type; import org.objectweb.asm.TypePath; import org.objectweb.asm.TypeReference; -import org.objectweb.asm.util.TraceMethodVisitor; import java.io.PrintWriter; import java.io.Writer; @@ -628,13 +627,12 @@ protected void visitConstructorOrMethod(final MethodNode node, final boolean isC parameters = Arrays.copyOfRange(parameters, 1, parameters.length); } - MethodVisitor mv = new PeepholeOptimizingMethodVisitor( - classVisitor.visitMethod( - node.getModifiers() | (isVargs(parameters) ? ACC_VARARGS : 0), - node.getName(), - BytecodeHelper.getMethodDescriptor(node.getReturnType(), parameters), - BytecodeHelper.getGenericsMethodSignature(node), - buildExceptions(node.getExceptions()))); + MethodVisitor mv = classVisitor.visitMethod( + node.getModifiers() | (isVargs(parameters) ? ACC_VARARGS : 0), + node.getName(), + BytecodeHelper.getMethodDescriptor(node.getReturnType(), parameters), + BytecodeHelper.getGenericsMethodSignature(node), + buildExceptions(node.getExceptions())); controller.setMethodVisitor(mv); controller.resetLineNumber(); @@ -687,11 +685,13 @@ protected void visitConstructorOrMethod(final MethodNode node, final boolean isC mv.visitMaxs(0, 0); } catch (Throwable t) { Writer writer = null; - if (mv instanceof TraceMethodVisitor tmv) { - writer = new StringBuilderWriter(); - PrintWriter p = new PrintWriter(writer); - tmv.p.print(p); - p.flush(); + // Method visitors are wrapped by PeepholeOptimizingClassVisitor; unwrap to + // reach a TraceMethodVisitor when classgen logging is enabled. + StringBuilderWriter buffer = new StringBuilderWriter(); + PrintWriter printer = new PrintWriter(buffer); + if (PeepholeOptimizingMethodVisitor.printTraceBytecode(mv, printer)) { + printer.flush(); + writer = buffer; } StringBuilder message = new StringBuilder(64); message.append("ASM reporting processing error for "); @@ -2340,12 +2340,11 @@ public void visitListExpression(final ListExpression expression) { while (index + * Wraps each {@link MethodVisitor} returned by the delegate's + * {@link #visitMethod(int, String, String, String, String[])} with + * {@link PeepholeOptimizingMethodVisitor}, so every method body receives + * the same stack-local compaction — user methods, constructors, static initializers, + * and synthetic helpers alike (MOP bridges, call-site array initializers, + * {@code class$}/{@code $get$} resolvers, large-list init chunks, and so on). + *

+ * Centralizing the wrap here avoids ad-hoc + * {@code new PeepholeOptimizingMethodVisitor(...)} at individual emission sites and + * keeps compaction in lockstep with {@link OperandStack}, which emits integer and + * other primitive constants via {@code visitLdcInsn} and relies on the peephole + * visitor to narrow them to {@code ICONST_*}, {@code BIPUSH}, {@code SIPUSH}, etc. + *

+ * Installed once by {@link WriterController} when the class visitor chain is built + * (optionally outside a {@code LoggableClassVisitor} / {@code TraceClassVisitor} + * pair used for classgen logging). Field and attribute visits are left unchanged; + * only method bodies are optimized. + * + * @see PeepholeOptimizingMethodVisitor + * @see WriterController + * @since 6.0.0 + */ +public final class PeepholeOptimizingClassVisitor extends ClassVisitor { + + /** + * Creates a visitor that peephole-optimizes every method written to + * {@code classVisitor}. + * + * @param classVisitor the next visitor in the class-generation chain + * (typically a {@link org.objectweb.asm.ClassWriter}, optionally + * preceded by logging / tracing adapters) + */ + public PeepholeOptimizingClassVisitor(final ClassVisitor classVisitor) { + super(CompilerConfiguration.ASM_API_VERSION, classVisitor); + } + + /** + * {@inheritDoc} + *

+ * When the delegate returns a non-{@code null} visitor, the result is a + * {@link PeepholeOptimizingMethodVisitor} (or the same instance if the + * delegate already wrapped the method). A {@code null} from the delegate is + * propagated unchanged so methods can still be skipped per the ASM contract. + */ + @Override + public MethodVisitor visitMethod(final int access, final String name, final String descriptor, final String signature, final String[] exceptions) { + return PeepholeOptimizingMethodVisitor.wrap(super.visitMethod(access, name, descriptor, signature, exceptions)); + } +} 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 6acdc287382..4a8b8473ac8 100644 --- a/src/main/java/org/codehaus/groovy/classgen/asm/PeepholeOptimizingMethodVisitor.java +++ b/src/main/java/org/codehaus/groovy/classgen/asm/PeepholeOptimizingMethodVisitor.java @@ -19,14 +19,18 @@ package org.codehaus.groovy.classgen.asm; import org.codehaus.groovy.control.CompilerConfiguration; +import org.codehaus.groovy.runtime.typehandling.DefaultTypeTransformation; import org.objectweb.asm.AnnotationVisitor; import org.objectweb.asm.Attribute; import org.objectweb.asm.ConstantDynamic; import org.objectweb.asm.Handle; import org.objectweb.asm.Label; import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Type; import org.objectweb.asm.TypePath; +import org.objectweb.asm.util.TraceMethodVisitor; +import java.io.PrintWriter; import java.math.BigDecimal; import java.math.BigInteger; @@ -35,17 +39,30 @@ 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; @@ -54,20 +71,20 @@ import static org.objectweb.asm.Opcodes.ICONST_5; import static org.objectweb.asm.Opcodes.ICONST_M1; import static org.objectweb.asm.Opcodes.IFEQ; -import static org.objectweb.asm.Opcodes.IFGE; -import static org.objectweb.asm.Opcodes.IFGT; -import static org.objectweb.asm.Opcodes.IFLE; -import static org.objectweb.asm.Opcodes.IFLT; -import static org.objectweb.asm.Opcodes.IFNE; +import static org.objectweb.asm.Opcodes.IFNONNULL; +import static org.objectweb.asm.Opcodes.IFNULL; +import static org.objectweb.asm.Opcodes.IF_ACMPEQ; +import static org.objectweb.asm.Opcodes.IF_ACMPNE; import static org.objectweb.asm.Opcodes.IF_ICMPEQ; -import static org.objectweb.asm.Opcodes.IF_ICMPGE; -import static org.objectweb.asm.Opcodes.IF_ICMPGT; import static org.objectweb.asm.Opcodes.IF_ICMPLE; -import static org.objectweb.asm.Opcodes.IF_ICMPLT; -import static org.objectweb.asm.Opcodes.IF_ICMPNE; import static org.objectweb.asm.Opcodes.ILOAD; +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; @@ -80,10 +97,95 @@ import static org.objectweb.asm.Opcodes.SIPUSH; /** - * Single-pass bytecode compaction inspired by Groovy++'s peephole adapters. - * The visitor only buffers the current stack-local candidate and flushes before - * labels, frames, debug metadata, and other non-local boundaries. + * Single-pass, stack-local bytecode compaction for methods emitted by the Groovy + * class generator. Inspired by Groovy++'s peephole adapters. + *

+ * Upstream writers such as {@link OperandStack} and {@link BytecodeHelper} may emit + * a uniform, easy-to-generate form (for example {@code visitLdcInsn} for every + * integer, or box immediately after a primitive producer). This visitor rewrites + * those sequences, within a single basic-block window, into the densest equivalent + * JVM opcodes without a second compilation pass or a full data-flow analysis. * + *

Model

+ * At most one pending load (constant, variable load, standalone + * {@code CHECKCAST}, or {@code Boolean.TRUE}/{@code FALSE}), at most one + * pending box ({@code Wrapper.valueOf} or + * {@link DefaultTypeTransformation#box}), and at most one + * pending {@code DUP}/{@code DUP2} (optionally followed by a buffered + * store) are held at a time. Pending state is flushed to the delegate before any + * non-local boundary so control flow, frames, and debug metadata stay correct: + * + * + *

Rewrites

+ * + * + *

Installation

+ * Prefer {@link PeepholeOptimizingClassVisitor} (wired from + * {@link WriterController}) so every method is covered. Use {@link #wrap(MethodVisitor)} + * only when constructing a method visitor outside that chain (for example unit tests). + * Integer constants are re-emitted through {@link BytecodeHelper#pushConstant} on the + * delegate so they are not re-buffered by this visitor. + * + * @see PeepholeOptimizingClassVisitor + * @see OperandStack + * @see BytecodeHelper#pushConstant(MethodVisitor, int) * @since 6.0.0 */ public final class PeepholeOptimizingMethodVisitor extends MethodVisitor { @@ -91,6 +193,9 @@ public final class PeepholeOptimizingMethodVisitor extends MethodVisitor { private static final String BIG_DECIMAL_TYPE = "java/math/BigDecimal"; private static final String BIG_INTEGER_TYPE = "java/math/BigInteger"; private static final String STRING_CTOR_DESCRIPTOR = "(Ljava/lang/String;)V"; + private static final String BOOLEAN_OWNER = "java/lang/Boolean"; + private static final String BOOLEAN_DESC = "Ljava/lang/Boolean;"; + private static final String DTT_OWNER = Type.getInternalName(DefaultTypeTransformation.class); private static final int NO_OPCODE = -1; private static final int FLOAT_TO_RAW_INT_BITS_0 = Float.floatToRawIntBits(0f); private static final int FLOAT_TO_RAW_INT_BITS_1 = Float.floatToRawIntBits(1f); @@ -98,26 +203,56 @@ public final class PeepholeOptimizingMethodVisitor extends MethodVisitor { private static final long DOUBLE_TO_RAW_LONG_BITS_0 = Double.doubleToRawLongBits(0d); private static final long DOUBLE_TO_RAW_LONG_BITS_1 = Double.doubleToRawLongBits(1d); + /** Kind of value currently held in the single-slot load window, if any. */ private enum PendingLoadKind { NONE, CONSTANT, VARIABLE, - CHECKCAST + CHECKCAST, + /** Buffered {@code GETSTATIC java/lang/Boolean.TRUE} or {@code FALSE}. */ + BOOLEAN_CONSTANT } + /** Kind of store buffered after a pending {@code DUP}/{@code DUP2}, if any. */ private enum PendingStoreKind { NONE, VARIABLE, STATIC_FIELD } + /** + * Kind of boxing call buffered while waiting for a matching unbox or for the + * boxed value to be discarded. + */ + private enum PendingBoxKind { + NONE, + /** {@code Wrapper.valueOf(primitive)}. */ + VALUE_OF, + /** {@code DefaultTypeTransformation.box(primitive)}. */ + DTT_BOX + } + private PendingLoadKind pendingLoadKind = PendingLoadKind.NONE; private Object pendingConstant; private int pendingLoadOpcode = NO_OPCODE; private int pendingLoadVar = NO_OPCODE; private boolean pendingLoadHasIinc; private int pendingLoadIncrement; + /** + * Internal name / type descriptor for a pending {@code CHECKCAST}. + * + */ private String pendingCheckcastDescriptor; + /** Value of a pending {@link PendingLoadKind#BOOLEAN_CONSTANT} load. */ + private boolean pendingBooleanValue; private int pendingDupOpcode = NO_OPCODE; private PendingStoreKind pendingStoreKind = PendingStoreKind.NONE; @@ -127,10 +262,73 @@ private enum PendingStoreKind { private String pendingStoreName; private String pendingStoreDescriptor; + private PendingBoxKind pendingBoxKind = PendingBoxKind.NONE; + /** Wrapper internal name for {@link PendingBoxKind#VALUE_OF} (e.g. {@code java/lang/Integer}). */ + private String pendingBoxOwner; + /** Primitive descriptor of the boxed value ({@code I}, {@code J}, …). */ + private String pendingBoxPrimitiveDescriptor; + private boolean pendingBoxIsInterface; + + /** + * Creates a peephole visitor that forwards compacted instructions to + * {@code delegate}. + * + * @param delegate the next method visitor in the chain (for example a + * {@link org.objectweb.asm.MethodWriter} or {@link TraceMethodVisitor}) + */ public PeepholeOptimizingMethodVisitor(final MethodVisitor delegate) { super(CompilerConfiguration.ASM_API_VERSION, delegate); } + /** + * Idempotent factory: returns {@code delegate} unchanged when it is + * {@code null} or already a {@link PeepholeOptimizingMethodVisitor}, + * otherwise wraps it. + *

+ * Returning {@code null} unchanged matches the ASM contract that + * {@link org.objectweb.asm.ClassVisitor#visitMethod} may return {@code null} + * to skip a method body. Used by {@link PeepholeOptimizingClassVisitor#visitMethod} + * so nested or repeated wrapping does not stack multiple peephole layers. + * + * @param delegate the visitor to wrap, or {@code null} to skip + * @return a peephole-optimizing method visitor, or {@code null} when + * {@code delegate} is {@code null} + */ + public static MethodVisitor wrap(final MethodVisitor delegate) { + if (delegate == null || delegate instanceof PeepholeOptimizingMethodVisitor) { + return delegate; + } + return new PeepholeOptimizingMethodVisitor(delegate); + } + + /** + * Walks {@code visitor} and any nested {@link PeepholeOptimizingMethodVisitor} + * layers to find a {@link TraceMethodVisitor}, then prints that visitor's + * recorded instruction text to {@code out}. + *

+ * {@link org.codehaus.groovy.classgen.AsmClassGenerator} uses this when + * {@code visitMaxs} fails under classgen logging: the outer visitor is a + * peephole wrapper, so a direct {@code instanceof TraceMethodVisitor} check + * would miss the tracer sitting further down the chain. + * + * @param visitor the method visitor active during class generation (may be + * a peephole wrapper); {@code null} is treated as “not found” + * @param out destination for the traced bytecode listing + * @return {@code true} if a {@link TraceMethodVisitor} was found and printed; + * {@code false} if none was present in the chain + */ + public static boolean printTraceBytecode(final MethodVisitor visitor, final PrintWriter out) { + MethodVisitor current = visitor; + while (current instanceof PeepholeOptimizingMethodVisitor peephole) { + current = peephole.mv; + } + if (current instanceof TraceMethodVisitor tmv) { + tmv.p.print(out); + return true; + } + return false; + } + @Override public void visitAttribute(final Attribute attribute) { flushPending(); @@ -145,18 +343,23 @@ public void visitFrame(final int type, final int numLocal, final Object[] local, @Override public void visitInsn(final int opcode) { - if (tryRemovePendingLoad(opcode) || tryDropPendingLoadOnReturn(opcode) || tryRemovePendingDupStore(opcode)) { + if (tryRemovePendingLoad(opcode) + || tryDropPendingLoadOnReturn(opcode) + || tryCollapsePendingDup(opcode) + || tryDropPendingBoxOnPop(opcode)) { return; } flushPendingLoad(); if (opcode == DUP || opcode == DUP2) { flushPendingDupStore(); + flushPendingBox(); pendingDupOpcode = opcode; return; } flushPendingDupStore(); + flushPendingBox(); switch (opcode) { case ACONST_NULL: bufferConstant(null); @@ -212,6 +415,7 @@ public void visitInsn(final int opcode) { public void visitIntInsn(final int opcode, final int operand) { flushPendingLoad(); flushPendingDupStore(); + flushPendingBox(); if (opcode == BIPUSH || opcode == SIPUSH) { bufferConstant(operand); return; @@ -223,11 +427,13 @@ public void visitIntInsn(final int opcode, final int operand) { public void visitVarInsn(final int opcode, final int varIndex) { flushPendingLoad(); if (pendingDupOpcode != NO_OPCODE && pendingStoreKind == PendingStoreKind.NONE && isStoreOpcode(opcode)) { + flushPendingBox(); bufferVariableStore(opcode, varIndex); return; } flushPendingDupStore(); + flushPendingBox(); if (isLoadOpcode(opcode)) { bufferVariableLoad(opcode, varIndex); return; @@ -237,12 +443,19 @@ public void visitVarInsn(final int opcode, final int varIndex) { @Override public void visitTypeInsn(final int opcode, final String descriptor) { - flushPendingLoad(); flushPendingDupStore(); + flushPendingBox(); if (opcode == CHECKCAST) { + if (canAttachCheckcastToPendingLoad()) { + pendingCheckcastDescriptor = descriptor; + return; + } + flushPendingLoad(); bufferCheckcast(descriptor); return; } + + flushPendingLoad(); super.visitTypeInsn(opcode, descriptor); } @@ -250,16 +463,35 @@ public void visitTypeInsn(final int opcode, final String descriptor) { public void visitFieldInsn(final int opcode, final String owner, final String name, final String descriptor) { flushPendingLoad(); if (pendingDupOpcode != NO_OPCODE && pendingStoreKind == PendingStoreKind.NONE && opcode == PUTSTATIC) { + flushPendingBox(); bufferStaticStore(opcode, owner, name, descriptor); return; } flushPendingDupStore(); + flushPendingBox(); + if (opcode == GETSTATIC && isBooleanTrueFalse(owner, name, descriptor)) { + bufferBooleanConstant("TRUE".equals(name)); + return; + } super.visitFieldInsn(opcode, owner, name, descriptor); } @Override public void visitMethodInsn(final int opcode, final String owner, final String name, final String descriptor, final boolean isInterface) { + if (tryFoldBooleanConstantUnbox(opcode, owner, name, descriptor)) { + return; + } + if (tryCancelBoxUnbox(opcode, owner, name, descriptor)) { + return; + } + if (tryRewriteBoxUnboxConversion(opcode, owner, name, descriptor)) { + return; + } + if (tryBufferBox(opcode, owner, name, descriptor, isInterface)) { + return; + } + flushPending(); super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); } @@ -273,7 +505,8 @@ public void visitInvokeDynamicInsn(final String name, final String descriptor, f @Override public void visitJumpInsn(final int opcode, final Label label) { flushPendingDupStore(); - if (tryRewriteZeroCompare(opcode, label)) { + flushPendingBox(); + if (tryRewriteZeroCompare(opcode, label) || tryRewriteNullCompare(opcode, label)) { return; } @@ -291,6 +524,7 @@ public void visitLabel(final Label label) { public void visitLdcInsn(final Object value) { flushPendingLoad(); flushPendingDupStore(); + flushPendingBox(); if (value instanceof ConstantDynamic) { super.visitLdcInsn(value); return; @@ -303,7 +537,8 @@ public void visitIincInsn(final int varIndex, final int increment) { if (pendingLoadKind == PendingLoadKind.VARIABLE && pendingLoadOpcode == ILOAD && pendingLoadVar == varIndex - && !pendingLoadHasIinc) { + && !pendingLoadHasIinc + && pendingCheckcastDescriptor == null) { pendingLoadHasIinc = true; pendingLoadIncrement = increment; return; @@ -379,31 +614,87 @@ public void visitEnd() { super.visitEnd(); } + /** + * Flushes the load window, any pending {@code DUP}/store pair, and any + * pending box call. + */ private void flushPending() { flushPendingLoad(); flushPendingDupStore(); + flushPendingBox(); } + /** + * Emits the buffered load/constant (and any attached {@code CHECKCAST} or + * {@code IINC}) to the delegate, then clears the load window. + */ private void flushPendingLoad() { switch (pendingLoadKind) { case CONSTANT: emitConstant(pendingConstant); + emitAttachedCheckcast(); break; case VARIABLE: super.visitVarInsn(pendingLoadOpcode, pendingLoadVar); if (pendingLoadHasIinc) { super.visitIincInsn(pendingLoadVar, pendingLoadIncrement); } + emitAttachedCheckcast(); break; case CHECKCAST: super.visitTypeInsn(CHECKCAST, pendingCheckcastDescriptor); break; + case BOOLEAN_CONSTANT: + super.visitFieldInsn(GETSTATIC, BOOLEAN_OWNER, + pendingBooleanValue ? "TRUE" : "FALSE", BOOLEAN_DESC); + break; case NONE: default: } clearPendingLoad(); } + /** + * Emits a buffered {@code Wrapper.valueOf} or + * {@code DefaultTypeTransformation.box} call to the delegate. + *

+ * 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", + "(" + pendingBoxPrimitiveDescriptor + ")L" + pendingBoxOwner + ";", + pendingBoxIsInterface); + break; + case DTT_BOX: + super.visitMethodInsn(INVOKESTATIC, DTT_OWNER, "box", + "(" + pendingBoxPrimitiveDescriptor + ")Ljava/lang/Object;", + false); + break; + case NONE: + default: + } + clearPendingBox(); + } + + /** Emits a {@code CHECKCAST} that was attached to a pending variable/constant load. */ + private void emitAttachedCheckcast() { + if (pendingCheckcastDescriptor != null) { + super.visitTypeInsn(CHECKCAST, pendingCheckcastDescriptor); + } + } + + /** + * Emits a buffered {@code DUP}/{@code DUP2} and, when present, the following + * store that was waiting to see whether the duplicated value would be discarded. + */ private void flushPendingDupStore() { if (pendingDupOpcode == NO_OPCODE) { return; @@ -416,21 +707,50 @@ private void flushPendingDupStore() { clearPendingDupStore(); } + /** + * Rewrites {@code …; ICONST_0; IF_ICMPxx L} to {@code …; IFxx L} when the + * pending constant is integer zero. + *

+ * The six {@code IF_ICMP*} opcodes occupy a contiguous range whose relative + * order matches {@code IFEQ}…{@code IFLE}, so the rewrite is a constant + * offset rather than a per-opcode table (see JVM opcode encoding). + * + * @return {@code true} if the jump was rewritten and no further action is needed + */ private boolean tryRewriteZeroCompare(final int opcode, final Label label) { - if (!(pendingLoadKind == PendingLoadKind.CONSTANT && pendingConstant instanceof Integer intValue && intValue == 0)) { + if (pendingLoadKind != PendingLoadKind.CONSTANT + || pendingCheckcastDescriptor != null + || !(pendingConstant instanceof Integer intValue) + || intValue != 0 + || opcode < IF_ICMPEQ + || opcode > IF_ICMPLE) { return false; } - int replacement = switch (opcode) { - case IF_ICMPEQ -> IFEQ; - case IF_ICMPNE -> IFNE; - case IF_ICMPGE -> IFGE; - case IF_ICMPGT -> IFGT; - case IF_ICMPLE -> IFLE; - case IF_ICMPLT -> IFLT; - default -> NO_OPCODE; - }; - if (replacement == NO_OPCODE) { + // IF_ICMPEQ..IF_ICMPLE share the same order as IFEQ..IFLE (offset = IFEQ - IF_ICMPEQ). + clearPendingLoad(); + super.visitJumpInsn(opcode + (IFEQ - IF_ICMPEQ), label); + return true; + } + + /** + * Rewrites {@code …; ACONST_NULL; IF_ACMPEQ/NE L} to {@code …; IFNULL/IFNONNULL L}. + * + * @return {@code true} if the jump was rewritten and no further action is needed + */ + private boolean tryRewriteNullCompare(final int opcode, final Label label) { + if (pendingLoadKind != PendingLoadKind.CONSTANT + || pendingCheckcastDescriptor != null + || pendingConstant != null) { + return false; + } + + final int replacement; + if (opcode == IF_ACMPEQ) { + replacement = IFNULL; + } else if (opcode == IF_ACMPNE) { + replacement = IFNONNULL; + } else { return false; } @@ -439,8 +759,22 @@ private boolean tryRewriteZeroCompare(final int opcode, final Label label) { return true; } + /** + * Handles a matching {@code POP}/{@code POP2} against the load window. + *

+ * Pure dead loads/constants are dropped (paired {@code IINC} kept). A + * {@code CHECKCAST} — whether attached to the load or standalone — is + * not 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; } @@ -450,68 +784,182 @@ private boolean tryRemovePendingLoad(final int opcode) { } if (pendingLoadKind == PendingLoadKind.CHECKCAST) { + // 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; } - if (pendingLoadKind == PendingLoadKind.VARIABLE && pendingLoadHasIinc) { - super.visitIincInsn(pendingLoadVar, pendingLoadIncrement); - } + emitPreservedIincSideEffect(); clearPendingLoad(); return true; } + /** + * Handles void {@code RETURN} against the load and box windows: + *

+ * + * @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 (pendingLoadKind == PendingLoadKind.VARIABLE && pendingLoadHasIinc) { - super.visitIincInsn(pendingLoadVar, pendingLoadIncrement); + if (pendingBoxKind != PendingBoxKind.NONE) { + dropPendingBoxDiscardingValue(); + super.visitInsn(RETURN); + return true; + } + + if (pendingLoadKind == PendingLoadKind.NONE) { + return false; + } + + if (pendingLoadKind == PendingLoadKind.CHECKCAST) { + super.visitTypeInsn(CHECKCAST, pendingCheckcastDescriptor); + clearPendingLoad(); + super.visitInsn(POP); + super.visitInsn(RETURN); + return true; + } + + if (pendingCheckcastDescriptor != null) { + // Keep the cast side effect; discard the value for a valid void return. + flushPendingLoad(); + super.visitInsn(POP); + super.visitInsn(RETURN); + return true; } + + emitPreservedIincSideEffect(); clearPendingLoad(); super.visitInsn(RETURN); return true; } - private boolean tryRemovePendingDupStore(final int opcode) { - if (pendingStoreKind == PendingStoreKind.NONE) { + /** Emits a buffered {@code IINC} that must survive dead-load elimination. */ + private void emitPreservedIincSideEffect() { + if (pendingLoadKind == PendingLoadKind.VARIABLE && pendingLoadHasIinc) { + super.visitIincInsn(pendingLoadVar, pendingLoadIncrement); + } + } + + /** + * Collapses {@code DUP}/{@code DUP2} with an optional store and a matching pop: + * + * + * @return {@code true} if the pop was fully handled + */ + private boolean tryCollapsePendingDup(final int opcode) { + if (pendingDupOpcode == NO_OPCODE) { return false; } int popSize = stackSizeForPop(opcode); - if (popSize == 0 || stackSizeForDup() != popSize || stackSizeForPendingStore() != popSize) { + if (popSize == 0 || stackSizeForDup() != popSize) { return false; } - emitPendingStore(); + if (pendingStoreKind != PendingStoreKind.NONE) { + if (stackSizeForPendingStore() != popSize) { + return false; + } + emitPendingStore(); + } clearPendingDupStore(); return true; } + /** + * Whether the next {@code CHECKCAST} can be recorded as an adornment on the + * current pending reference load instead of flushing that load first. + * Restricted to {@code ALOAD} and reference-typed constants so primitive + * loads are never paired with an invalid cast. + */ + private boolean canAttachCheckcastToPendingLoad() { + if (pendingCheckcastDescriptor != null) { + return false; + } + if (pendingLoadKind == PendingLoadKind.VARIABLE + && pendingLoadOpcode == ALOAD + && !pendingLoadHasIinc) { + return true; + } + return pendingLoadKind == PendingLoadKind.CONSTANT && isReferenceConstant(pendingConstant); + } + + /** + * {@code true} for constant values that occupy a single reference slot on the + * operand stack ({@code null}, {@link String}, {@link Type}, {@link Handle}). + * {@link ConstantDynamic} is intentionally excluded: it is never buffered + * (see {@link #visitLdcInsn}) because resolving it may run a bootstrap method + * with observable side effects, so it must not be subject to dead-load removal. + */ + private static boolean isReferenceConstant(final Object value) { + return value == null + || value instanceof String + || value instanceof Type + || value instanceof Handle; + } + + /** + * Replaces the load window with a constant candidate. Any previously buffered + * load is flushed first so candidates are never silently discarded. + */ private void bufferConstant(final Object value) { - clearPendingLoad(); + flushPendingLoad(); pendingLoadKind = PendingLoadKind.CONSTANT; pendingConstant = value; } + /** + * Replaces the load window with a variable-load candidate, flushing any prior + * pending load first. + */ private void bufferVariableLoad(final int opcode, final int varIndex) { - clearPendingLoad(); + flushPendingLoad(); pendingLoadKind = PendingLoadKind.VARIABLE; pendingLoadOpcode = opcode; pendingLoadVar = varIndex; } + /** + * Buffers a standalone {@code CHECKCAST} of a value already on the stack + * (the preceding producer was not held in the load window). + */ private void bufferCheckcast(final String descriptor) { - clearPendingLoad(); + flushPendingLoad(); pendingLoadKind = PendingLoadKind.CHECKCAST; pendingCheckcastDescriptor = descriptor; } @@ -551,6 +999,7 @@ private void clearPendingLoad() { pendingLoadHasIinc = false; pendingLoadIncrement = 0; pendingCheckcastDescriptor = null; + pendingBooleanValue = false; } private void clearPendingDupStore() { @@ -563,11 +1012,630 @@ private void clearPendingDupStore() { pendingStoreDescriptor = null; } + private void clearPendingBox() { + pendingBoxKind = PendingBoxKind.NONE; + pendingBoxOwner = null; + pendingBoxPrimitiveDescriptor = null; + pendingBoxIsInterface = false; + } + + /** + * Buffers {@code GETSTATIC Boolean.TRUE/FALSE} so a following unbox can fold + * to {@code ICONST_0}/{@code ICONST_1}. + */ + private void bufferBooleanConstant(final boolean value) { + flushPendingLoad(); + pendingLoadKind = PendingLoadKind.BOOLEAN_CONSTANT; + pendingBooleanValue = value; + } + + private void bufferValueOfBox(final String owner, final String primitiveDescriptor, final boolean isInterface) { + clearPendingBox(); + pendingBoxKind = PendingBoxKind.VALUE_OF; + pendingBoxOwner = owner; + pendingBoxPrimitiveDescriptor = primitiveDescriptor; + pendingBoxIsInterface = isInterface; + } + + private void bufferDttBox(final String primitiveDescriptor) { + clearPendingBox(); + pendingBoxKind = PendingBoxKind.DTT_BOX; + pendingBoxOwner = DTT_OWNER; + pendingBoxPrimitiveDescriptor = primitiveDescriptor; + pendingBoxIsInterface = false; + } + + /** + * Folds {@code Boolean.TRUE/FALSE}; unbox into {@code ICONST_1}/{@code ICONST_0}. + * + * @return {@code true} if the unbox was fully rewritten + */ + private boolean tryFoldBooleanConstantUnbox(final int opcode, final String owner, final String name, final String descriptor) { + if (pendingLoadKind != PendingLoadKind.BOOLEAN_CONSTANT) { + return false; + } + if (!isBooleanUnbox(opcode, owner, name, descriptor)) { + return false; + } + boolean value = pendingBooleanValue; + clearPendingLoad(); + super.visitInsn(value ? ICONST_1 : ICONST_0); + return true; + } + + /** + * Cancels a pending box when the next call is the matching unbox for the same + * primitive type. The primitive already on the stack is left untouched. + * + * @return {@code true} if box and unbox were both dropped + */ + private boolean tryCancelBoxUnbox(final int opcode, final String owner, final String name, final String descriptor) { + if (pendingBoxKind == PendingBoxKind.NONE) { + return false; + } + if (!matchesPendingUnbox(opcode, owner, name, descriptor)) { + return false; + } + clearPendingBox(); + return true; + } + + /** + * Rewrites {@code box(P); unbox-as-Q} to a primitive conversion when {@code P} + * and {@code Q} are different numeric types. + *

+ * 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. + *

+ * 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 + * still held in the load window, drop that too, otherwise pop the original + * primitive ({@code POP2} when wide). + *

+ * {@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 narrow primitive is treated as two + * one-slot discards: the boxed value plus the slot underneath. + * + * @return {@code true} if the pop was fully handled + */ + private boolean tryDropPendingBoxOnPop(final int opcode) { + if (pendingBoxKind == PendingBoxKind.NONE) { + return false; + } + int popSize = stackSizeForPop(opcode); + if (popSize == 0) { + return false; + } + int primitiveSize = stackSizeForPrimitiveDescriptor(pendingBoxPrimitiveDescriptor); + + // 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(); + if (tryCancelPendingPrimitiveProducer(slotsToRemove)) { + return true; + } + super.visitInsn(slotsToRemove == 2 ? POP2 : POP); + return true; + } + + // 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(); + 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; + } + + private static int stackSizeForPrimitiveDescriptor(final String descriptor) { + return ("J".equals(descriptor) || "D".equals(descriptor)) ? 2 : 1; + } + + /** + * Buffers {@code Wrapper.valueOf(prim)} or {@code DefaultTypeTransformation.box(prim)} + * so a following matching unbox (or discard via pop/return) can collapse the pair. + *

+ * 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 + */ + private boolean tryBufferBox(final int opcode, final String owner, final String name, final String descriptor, final boolean isInterface) { + if (opcode != INVOKESTATIC) { + return false; + } + if ("valueOf".equals(name)) { + String primitive = primitiveOperandOfValueOf(owner, descriptor); + if (primitive == null) { + return false; + } + prepareBoxBuffer(primitive); + bufferValueOfBox(owner, primitive, isInterface); + return true; + } + if ("box".equals(name) && DTT_OWNER.equals(owner)) { + String primitive = primitiveOperandOfDttBox(descriptor); + if (primitive == null) { + return false; + } + 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 || pendingBoxKind == PendingBoxKind.NONE) { + 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); + } + // DTT.box → Object at the type system, but runtime value is the wrapper. + return owner.equals(wrapperInternalNameForDescriptor(expectedPrim)); + } + // 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; + } + 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) { + return BOOLEAN_OWNER.equals(owner) + && BOOLEAN_DESC.equals(descriptor) + && ("TRUE".equals(name) || "FALSE".equals(name)); + } + + private static boolean isBooleanUnbox(final int opcode, final String owner, final String name, final String descriptor) { + if (opcode == INVOKESTATIC && DTT_OWNER.equals(owner) + && "booleanUnbox".equals(name) && "(Ljava/lang/Object;)Z".equals(descriptor)) { + return true; + } + return opcode == INVOKEVIRTUAL && BOOLEAN_OWNER.equals(owner) + && "booleanValue".equals(name) && "()Z".equals(descriptor); + } + + /** + * Parses {@code valueOf} descriptors such as {@code (I)Ljava/lang/Integer;} and + * returns the primitive operand descriptor when the owner is the matching wrapper. + */ + private static String primitiveOperandOfValueOf(final String owner, final String descriptor) { + Type[] args = Type.getArgumentTypes(descriptor); + Type ret = Type.getReturnType(descriptor); + if (args.length != 1 || ret.getSort() != Type.OBJECT) { + return null; + } + if (!owner.equals(ret.getInternalName())) { + return null; + } + Type arg = args[0]; + if (!isBoxablePrimitive(arg)) { + return null; + } + String expectedWrapper = wrapperInternalName(arg); + return owner.equals(expectedWrapper) ? arg.getDescriptor() : null; + } + + /** + * Parses {@code DefaultTypeTransformation.box} descriptors such as + * {@code (I)Ljava/lang/Object;} and returns the primitive operand descriptor. + */ + private static String primitiveOperandOfDttBox(final String descriptor) { + Type[] args = Type.getArgumentTypes(descriptor); + Type ret = Type.getReturnType(descriptor); + if (args.length != 1 || ret.getSort() != Type.OBJECT) { + return null; + } + Type arg = args[0]; + return isBoxablePrimitive(arg) ? arg.getDescriptor() : null; + } + + private static boolean isBoxablePrimitive(final Type type) { + return switch (type.getSort()) { + case Type.BOOLEAN, Type.BYTE, Type.CHAR, Type.SHORT, + Type.INT, Type.LONG, Type.FLOAT, Type.DOUBLE -> true; + default -> false; + }; + } + + private static String wrapperInternalName(final Type primitive) { + return switch (primitive.getSort()) { + case Type.BOOLEAN -> "java/lang/Boolean"; + case Type.BYTE -> "java/lang/Byte"; + case Type.CHAR -> "java/lang/Character"; + case Type.SHORT -> "java/lang/Short"; + case Type.INT -> "java/lang/Integer"; + case Type.LONG -> "java/lang/Long"; + case Type.FLOAT -> "java/lang/Float"; + case Type.DOUBLE -> "java/lang/Double"; + default -> null; + }; + } + + private static String primitiveValueMethodName(final String primitiveDescriptor) { + return switch (primitiveDescriptor) { + case "Z" -> "booleanValue"; + case "B" -> "byteValue"; + case "C" -> "charValue"; + case "S" -> "shortValue"; + case "I" -> "intValue"; + case "J" -> "longValue"; + case "F" -> "floatValue"; + case "D" -> "doubleValue"; + default -> null; + }; + } + + private static String primitiveUnboxMethodName(final String primitiveDescriptor) { + return switch (primitiveDescriptor) { + case "Z" -> "booleanUnbox"; + case "B" -> "byteUnbox"; + case "C" -> "charUnbox"; + case "S" -> "shortUnbox"; + case "I" -> "intUnbox"; + case "J" -> "longUnbox"; + case "F" -> "floatUnbox"; + case "D" -> "doubleUnbox"; + default -> null; + }; + } + private int stackSizeForPendingLoad() { return switch (pendingLoadKind) { case VARIABLE -> stackSizeForLoadOpcode(pendingLoadOpcode); case CONSTANT -> (pendingConstant instanceof Long || pendingConstant instanceof Double) ? 2 : 1; - case CHECKCAST -> 1; + case CHECKCAST, BOOLEAN_CONSTANT -> 1; case NONE -> 0; }; } @@ -631,6 +1699,11 @@ private static boolean isStoreOpcode(final int opcode) { }; } + /** + * Writes {@code value} to the delegate using the densest legal constant form. + * Integers go through {@link BytecodeHelper#pushConstant} on the ASM delegate + * so the specialized opcodes are not intercepted and re-buffered by this visitor. + */ private void emitConstant(final Object value) { if (value == null) { super.visitInsn(ACONST_NULL); @@ -643,7 +1716,8 @@ private void emitConstant(final Object value) { } if (value instanceof Integer intValue) { - emitIntConstant(intValue); + // Emit through the delegate so BytecodeHelper's forms are not re-buffered. + BytecodeHelper.pushConstant(mv, intValue); return; } @@ -665,6 +1739,10 @@ private void emitConstant(final Object value) { super.visitLdcInsn(value); } + /** + * Lowers a {@link BigDecimal} or {@link BigInteger} constant to + * {@code new Type(value.toString())} on the delegate. + */ private void emitStringConstructedConstant(final Object value) { String type = (value instanceof BigDecimal ? BIG_DECIMAL_TYPE : BIG_INTEGER_TYPE); super.visitTypeInsn(NEW, type); @@ -673,41 +1751,7 @@ private void emitStringConstructedConstant(final Object value) { super.visitMethodInsn(INVOKESPECIAL, type, "", STRING_CTOR_DESCRIPTOR, false); } - private void emitIntConstant(final int value) { - switch (value) { - case -1: - super.visitInsn(ICONST_M1); - return; - case 0: - super.visitInsn(ICONST_0); - return; - case 1: - super.visitInsn(ICONST_1); - return; - case 2: - super.visitInsn(ICONST_2); - return; - case 3: - super.visitInsn(ICONST_3); - return; - case 4: - super.visitInsn(ICONST_4); - return; - case 5: - super.visitInsn(ICONST_5); - return; - default: - } - - if (value >= Byte.MIN_VALUE && value <= Byte.MAX_VALUE) { - super.visitIntInsn(BIPUSH, value); - } else if (value >= Short.MIN_VALUE && value <= Short.MAX_VALUE) { - super.visitIntInsn(SIPUSH, value); - } else { - super.visitLdcInsn(value); - } - } - + /** Emits {@code LCONST_0}/{@code LCONST_1} when possible, otherwise {@code LDC}. */ private void emitLongConstant(final long value) { if (value == 0L) { super.visitInsn(LCONST_0); @@ -718,6 +1762,10 @@ private void emitLongConstant(final long value) { } } + /** + * Emits {@code FCONST_0/1/2} when the raw bits match; otherwise {@code LDC}. + * Raw-bit comparison keeps {@code +0.0f} and {@code -0.0f} distinct (GROOVY-9797). + */ private void emitFloatConstant(final float value) { int rawBits = Float.floatToRawIntBits(value); if (rawBits == FLOAT_TO_RAW_INT_BITS_0) { @@ -731,6 +1779,10 @@ private void emitFloatConstant(final float value) { } } + /** + * Emits {@code DCONST_0/1} when the raw bits match; otherwise {@code LDC}. + * Raw-bit comparison keeps {@code +0.0d} and {@code -0.0d} distinct (GROOVY-9797). + */ private void emitDoubleConstant(final double value) { long rawBits = Double.doubleToRawLongBits(value); if (rawBits == DOUBLE_TO_RAW_LONG_BITS_0) { diff --git a/src/main/java/org/codehaus/groovy/classgen/asm/WriterController.java b/src/main/java/org/codehaus/groovy/classgen/asm/WriterController.java index d69cb00d61b..6178523f017 100644 --- a/src/main/java/org/codehaus/groovy/classgen/asm/WriterController.java +++ b/src/main/java/org/codehaus/groovy/classgen/asm/WriterController.java @@ -161,10 +161,14 @@ public void init(final AsmClassGenerator asmClassGenerator, final GeneratorConte } private static ClassVisitor createClassVisitor(final ClassVisitor cv, final CompilerConfiguration config) { - if (!config.isLogClassgen() || cv instanceof LoggableClassVisitor) { - return cv; + ClassVisitor visitor = cv; + if (config.isLogClassgen() && !(cv instanceof LoggableClassVisitor)) { + visitor = new LoggableClassVisitor(cv, config); } - return new LoggableClassVisitor(cv, config); + // Apply peephole compaction to every method, including synthetic helpers. + return visitor instanceof PeepholeOptimizingClassVisitor + ? visitor + : new PeepholeOptimizingClassVisitor(visitor); } //-------------------------------------------------------------------------- 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 241aaff00c5..e837fdda1c1 100644 --- a/src/test/groovy/org/codehaus/groovy/classgen/asm/PeepholeOptimizingMethodVisitorTest.groovy +++ b/src/test/groovy/org/codehaus/groovy/classgen/asm/PeepholeOptimizingMethodVisitorTest.groovy @@ -19,6 +19,7 @@ package org.codehaus.groovy.classgen.asm import org.codehaus.groovy.control.CompilerConfiguration +import org.codehaus.groovy.runtime.typehandling.GroovyCastException import org.junit.jupiter.api.Test import org.objectweb.asm.Attribute import org.objectweb.asm.ConstantDynamic @@ -42,11 +43,56 @@ import org.objectweb.asm.util.Printer import org.objectweb.asm.util.Textifier import org.objectweb.asm.util.TraceMethodVisitor +import static groovy.test.GroovyAssert.shouldFail import static org.objectweb.asm.Opcodes.ACC_PUBLIC import static org.objectweb.asm.Opcodes.ACC_STATIC import static org.objectweb.asm.Opcodes.RETURN -final class PeepholeOptimizingMethodVisitorTest { +final class PeepholeOptimizingMethodVisitorTest 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(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', + ]) + } @Test void compactsNumericConstants() { @@ -330,15 +376,102 @@ final class PeepholeOptimizingMethodVisitorTest { } @Test - void removesStandaloneCheckcastBeforePop() { + void preservesLoadAndAttachedCheckcastBeforePop() { + def bytecode = sequenceFor('(Ljava/lang/Object;)V') { + visitVarInsn(Opcodes.ALOAD, 0) + visitTypeInsn(Opcodes.CHECKCAST, 'java/lang/String') + visitInsn(Opcodes.POP) + visitInsn(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 preservesStandaloneCheckcastBeforePop() { def bytecode = sequenceFor('(Ljava/lang/Object;)V') { visitVarInsn(Opcodes.ALOAD, 0) + visitInsn(Opcodes.NOP) // force the load to flush so CHECKCAST is standalone visitTypeInsn(Opcodes.CHECKCAST, 'java/lang/String') visitInsn(Opcodes.POP) visitInsn(RETURN) } - assert opcodeLines(bytecode) == ['ALOAD 0', '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 + void rewritesNullComparisonsAgainstAconstNull() { + def nullComparisons = [ + (Opcodes.IF_ACMPEQ): 'IFNULL', + (Opcodes.IF_ACMPNE): 'IFNONNULL', + ] + + nullComparisons.each { opcode, expected -> + def label = new Label() + def bytecode = sequenceFor('(Ljava/lang/Object;)V') { + visitVarInsn(Opcodes.ALOAD, 0) + visitInsn(Opcodes.ACONST_NULL) + visitJumpInsn(opcode, label) + visitInsn(RETURN) + visitLabel(label) + visitInsn(RETURN) + } + def lines = opcodeLines(bytecode) + + assert lines.size() == 4 + assert lines[0] == 'ALOAD 0' + assert lines[1].startsWith(expected) + assert lines[2..3] == ['RETURN', 'RETURN'] + assert !lines[1].startsWith(Printer.OPCODES[opcode]) + } + } + + @Test + void removesBareDupBeforeMatchingPop() { + def bytecode = sequenceFor { + visitInsn(Opcodes.ICONST_1) + visitInsn(Opcodes.DUP) + visitInsn(Opcodes.POP) + visitLdcInsn(2L) + visitInsn(Opcodes.DUP2) + visitInsn(Opcodes.POP2) + visitInsn(RETURN) + } + + assert opcodeLines(bytecode) == [ + 'ICONST_1', + 'LDC 2L', + 'RETURN', + ] + } + + @Test + void preservesAttachedCheckcastWhenTheValueIsUsed() { + def bytecode = sequenceFor('(Ljava/lang/Object;)Ljava/lang/String;') { + visitVarInsn(Opcodes.ALOAD, 0) + visitTypeInsn(Opcodes.CHECKCAST, 'java/lang/String') + visitInsn(Opcodes.ARETURN) + } + + assert opcodeLines(bytecode) == [ + 'ALOAD 0', + 'CHECKCAST java/lang/String', + 'ARETURN', + ] } @Test @@ -366,6 +499,1029 @@ final class PeepholeOptimizingMethodVisitorTest { ] } + @Test + void preservesStandaloneCheckcastBeforeReturn() { + // 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) // flush ALOAD so the cast is standalone + visitTypeInsn(Opcodes.CHECKCAST, 'java/lang/String') + visitInsn(Opcodes.POP) + visitInsn(RETURN) + } + def returnOnly = sequenceFor('(Ljava/lang/Object;)V') { + visitVarInsn(Opcodes.ALOAD, 0) + visitInsn(Opcodes.NOP) + visitTypeInsn(Opcodes.CHECKCAST, 'java/lang/String') + visitInsn(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 + void preservesAttachedCheckcastSideEffectBeforeReturn() { + def bytecode = sequenceFor('(Ljava/lang/Object;)V') { + visitVarInsn(Opcodes.ALOAD, 0) + visitTypeInsn(Opcodes.CHECKCAST, 'java/lang/String') + visitInsn(RETURN) + } + + // Cast is kept for its ClassCastException side effect; POP keeps void return valid. + assert opcodeLines(bytecode) == [ + 'ALOAD 0', + 'CHECKCAST java/lang/String', + 'POP', + 'RETURN', + ] + } + + @Test + void wrapIsIdempotentAndPropagatesNull() { + assert PeepholeOptimizingMethodVisitor.wrap(null) == null + + def methodNode = new MethodNode(CompilerConfiguration.ASM_API_VERSION, ACC_PUBLIC | ACC_STATIC, 'sample', '()V', null, null) + def once = PeepholeOptimizingMethodVisitor.wrap(methodNode) + assert once instanceof PeepholeOptimizingMethodVisitor + assert PeepholeOptimizingMethodVisitor.wrap(once).is(once) + } + + @Test + void printTraceBytecodeFindsNestedTracer() { + def textifier = new Textifier() + def tracer = new TraceMethodVisitor(textifier) + def peephole = new PeepholeOptimizingMethodVisitor(tracer) + peephole.visitCode() + peephole.visitInsn(Opcodes.ICONST_1) + peephole.visitVarInsn(Opcodes.ISTORE, 0) // keep the constant live for tracing + peephole.visitInsn(RETURN) + peephole.visitMaxs(0, 0) + peephole.visitEnd() + + def out = new StringWriter() + def printer = new PrintWriter(out) + assert PeepholeOptimizingMethodVisitor.printTraceBytecode(peephole, printer) + printer.flush() + assert out.toString().contains('ICONST_1') + assert out.toString().contains('ISTORE') + assert out.toString().contains('RETURN') + + assert !PeepholeOptimizingMethodVisitor.printTraceBytecode(null, printer) + assert !PeepholeOptimizingMethodVisitor.printTraceBytecode(new MethodNode(CompilerConfiguration.ASM_API_VERSION, ACC_PUBLIC | ACC_STATIC, 'x', '()V', null, null), printer) + } + + @Test + void classVisitorWrapsEveryMethodAndSkipsNullDelegates() { + def written = [] + def delegate = new org.objectweb.asm.ClassVisitor(CompilerConfiguration.ASM_API_VERSION) { + @Override + MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + if (name == 'skip') { + return null + } + def mn = new MethodNode(CompilerConfiguration.ASM_API_VERSION, access, name, descriptor, signature, exceptions) + written << mn + return mn + } + } + def peepholeClass = new PeepholeOptimizingClassVisitor(delegate) + + def optimized = peepholeClass.visitMethod(ACC_PUBLIC | ACC_STATIC, 'run', '()V', null, null) + assert optimized instanceof PeepholeOptimizingMethodVisitor + optimized.visitCode() + optimized.visitLdcInsn(0) + optimized.visitVarInsn(Opcodes.ISTORE, 0) + optimized.visitInsn(RETURN) + optimized.visitMaxs(0, 0) + optimized.visitEnd() + + assert peepholeClass.visitMethod(ACC_PUBLIC | ACC_STATIC, 'skip', '()V', null, null) == null + assert written.size() == 1 + assert opcodeLines(traceSequence(written[0])) == ['ICONST_0', 'ISTORE 0', 'RETURN'] + } + + @Test + void doesNotRewriteNonMatchingCompareJumps() { + def zeroLabel = new Label() + def zeroWithIfnull = sequenceFor('(I)V') { + visitVarInsn(Opcodes.ILOAD, 0) + visitLdcInsn(0) + visitJumpInsn(Opcodes.IFNULL, zeroLabel) + visitInsn(RETURN) + visitLabel(zeroLabel) + visitInsn(RETURN) + } + assert opcodeLines(zeroWithIfnull)[1] == 'ICONST_0' + assert opcodeLines(zeroWithIfnull)[2].startsWith('IFNULL') + + def nullLabel = new Label() + def nullWithIfeq = sequenceFor('(Ljava/lang/Object;)V') { + visitVarInsn(Opcodes.ALOAD, 0) + visitInsn(Opcodes.ACONST_NULL) + visitJumpInsn(Opcodes.IFEQ, nullLabel) + visitInsn(RETURN) + visitLabel(nullLabel) + visitInsn(RETURN) + } + assert opcodeLines(nullWithIfeq)[1] == 'ACONST_NULL' + assert opcodeLines(nullWithIfeq)[2].startsWith('IFEQ') + } + + @Test + void preservesMismatchedPopSizes() { + def bytecode = sequenceFor('(IJ)V') { + visitVarInsn(Opcodes.ILOAD, 0) + visitInsn(Opcodes.POP2) + visitVarInsn(Opcodes.LLOAD, 1) + visitInsn(Opcodes.POP) + visitInsn(RETURN) + } + + assert opcodeLines(bytecode) == [ + 'ILOAD 0', + 'POP2', + 'LLOAD 1', + 'POP', + 'RETURN', + ] + } + + @Test + void attachesCheckcastToReferenceConstantsAndPreservesDiscardedCasts() { + def type = org.objectweb.asm.Type.getType(String) + def handle = new Handle(Opcodes.H_INVOKESTATIC, 'Owner', 'boot', '()V', false) + def bytecode = sequenceFor { + visitLdcInsn('text') + visitTypeInsn(Opcodes.CHECKCAST, 'java/lang/String') + visitInsn(Opcodes.POP) + visitLdcInsn(type) + visitTypeInsn(Opcodes.CHECKCAST, 'java/lang/Class') + visitInsn(Opcodes.POP) + visitLdcInsn(handle) + visitTypeInsn(Opcodes.CHECKCAST, 'java/lang/invoke/MethodHandle') + visitInsn(Opcodes.POP) + visitInsn(Opcodes.ACONST_NULL) + visitTypeInsn(Opcodes.CHECKCAST, 'java/lang/String') + visitInsn(Opcodes.POP) + visitInsn(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 + void flushesBeforeMethodAndInvokeDynamicCalls() { + def handle = new Handle(Opcodes.H_INVOKESTATIC, 'Owner', 'bootstrap', '()Ljava/lang/invoke/CallSite;', false) + def bytecode = sequenceFor('(Ljava/lang/Object;)V') { + visitVarInsn(Opcodes.ALOAD, 0) + visitMethodInsn(Opcodes.INVOKEVIRTUAL, 'java/lang/Object', 'toString', '()Ljava/lang/String;', false) + visitInsn(Opcodes.POP) + visitLdcInsn(1) + visitInvokeDynamicInsn('dyn', '()I', handle) + visitInsn(Opcodes.POP) + visitInsn(RETURN) + } + + def lines = opcodeLines(bytecode) + assert lines[0] == 'ALOAD 0' + assert lines[1] == 'INVOKEVIRTUAL java/lang/Object.toString ()Ljava/lang/String;' + assert lines[2] == 'POP' + assert lines[3] == 'ICONST_1' + assert lines[4].startsWith('INVOKEDYNAMIC dyn') + assert lines[-2] == 'POP' + assert lines[-1] == 'RETURN' + } + + @Test + void flushesPendingLoadOnLineNumberAndNonCheckcastTypeInsn() { + def start = new Label() + def bytecode = sequenceFor('(Ljava/lang/Object;)V') { + visitLabel(start) + visitVarInsn(Opcodes.ALOAD, 0) + visitLineNumber(42, start) + visitTypeInsn(Opcodes.INSTANCEOF, 'java/lang/String') + visitInsn(Opcodes.POP) + visitInsn(RETURN) + } + + assert opcodeLines(bytecode) == [ + 'ALOAD 0', + 'INSTANCEOF java/lang/String', + 'POP', + 'RETURN', + ] + } + + @Test + void doesNotAttachSecondCheckcastAndFlushesIincOnConflict() { + def chainedCasts = sequenceFor('(Ljava/lang/Object;)Ljava/lang/Object;') { + visitVarInsn(Opcodes.ALOAD, 0) + visitTypeInsn(Opcodes.CHECKCAST, 'java/lang/CharSequence') + visitTypeInsn(Opcodes.CHECKCAST, 'java/lang/String') + visitInsn(Opcodes.ARETURN) + } + assert opcodeLines(chainedCasts) == [ + 'ALOAD 0', + 'CHECKCAST java/lang/CharSequence', + 'CHECKCAST java/lang/String', + 'ARETURN', + ] + + def secondIincFlushes = sequenceFor('(I)V') { + visitVarInsn(Opcodes.ILOAD, 0) + visitIincInsn(0, 1) + visitIincInsn(0, 2) // already has IINC → flush load+first IINC, emit second + visitInsn(Opcodes.POP) + visitInsn(RETURN) + } + assert opcodeLines(secondIincFlushes) == [ + 'ILOAD 0', + 'IINC 0 1', + 'IINC 0 2', + 'POP', + 'RETURN', + ] + + def differentVarIincFlushes = sequenceFor('(I)V') { + visitVarInsn(Opcodes.ILOAD, 0) + visitIincInsn(1, 1) // different variable → flush load, emit IINC + visitInsn(Opcodes.POP) + visitInsn(RETURN) + } + assert opcodeLines(differentVarIincFlushes) == [ + 'ILOAD 0', + 'IINC 1 1', + 'POP', + 'RETURN', + ] + } + + @Test + void flushesPendingDupWhenStoreIsNotEligibleForCollapse() { + def bytecode = sequenceFor { + visitInsn(Opcodes.ICONST_1) + visitInsn(Opcodes.DUP) + visitFieldInsn(Opcodes.PUTFIELD, 'Owner', 'value', 'I') // not PUTSTATIC → flush dup + visitInsn(Opcodes.ICONST_2) + visitInsn(Opcodes.DUP) + visitVarInsn(Opcodes.ISTORE, 0) + visitInsn(Opcodes.NOP) // keep the duplicate + visitInsn(RETURN) + } + + assert opcodeLines(bytecode) == [ + 'ICONST_1', + 'DUP', + 'PUTFIELD Owner.value : I', + 'ICONST_2', + 'DUP', + 'ISTORE 0', + 'NOP', + 'RETURN', + ] + } + + @Test + void passesConstantDynamicThroughWithoutBuffering() { + def handle = new Handle(Opcodes.H_INVOKESTATIC, 'Owner', 'bootstrap', '()I', false) + def dynamic = new ConstantDynamic('answer', 'I', handle) + def bytecode = sequenceFor { + visitLdcInsn(dynamic) + visitInsn(Opcodes.POP) + visitInsn(RETURN) + } + + // Bootstrap side effects must not be dropped by dead-load elimination. + def lines = opcodeLines(bytecode) + assert lines.size() == 3 + assert lines[0].startsWith('LDC') + assert lines[1] == 'POP' + assert lines[2] == 'RETURN' + } + + @Test + void emitsNonSpecializedNumericConstantsViaLdc() { + def bytecode = sequenceFor { + visitLdcInsn(3L) + visitLdcInsn(4f) + visitLdcInsn(5d) + visitLdcInsn('literal') + visitVarInsn(Opcodes.ASTORE, 0) + visitInsn(RETURN) + } + + assert opcodeLines(bytecode) == [ + 'LDC 3L', + 'LDC 4.0F', + 'LDC 5.0D', + 'LDC "literal"', + 'ASTORE 0', + 'RETURN', + ] + } + + @Test + void passesThroughNewarrayIntInsn() { + def bytecode = sequenceFor { + visitLdcInsn(2) + visitIntInsn(Opcodes.NEWARRAY, Opcodes.T_INT) + visitInsn(Opcodes.POP) + visitInsn(RETURN) + } + + assert opcodeLines(bytecode) == [ + 'ICONST_2', + 'NEWARRAY T_INT', + 'POP', + 'RETURN', + ] + } + + @Test + void dropsDeadFloatAndDoubleLoads() { + def bytecode = sequenceFor('(FD)V') { + visitVarInsn(Opcodes.FLOAD, 0) + visitInsn(Opcodes.POP) + visitVarInsn(Opcodes.DLOAD, 1) + visitInsn(Opcodes.POP2) + visitInsn(Opcodes.FCONST_0) + visitInsn(Opcodes.POP) + visitInsn(Opcodes.DCONST_0) + visitInsn(Opcodes.POP2) + visitInsn(RETURN) + } + + assert opcodeLines(bytecode) == ['RETURN'] + } + + @Test + void collapsesDup2WithWideVariableStore() { + def bytecode = sequenceFor { + visitLdcInsn(9L) + visitInsn(Opcodes.DUP2) + visitVarInsn(Opcodes.LSTORE, 0) + visitInsn(Opcodes.POP2) + visitLdcInsn(8.0d) + visitInsn(Opcodes.DUP2) + visitVarInsn(Opcodes.DSTORE, 2) + visitInsn(Opcodes.POP2) + visitInsn(RETURN) + } + + assert opcodeLines(bytecode) == [ + 'LDC 9L', + 'LSTORE 0', + 'LDC 8.0D', + 'DSTORE 2', + 'RETURN', + ] + } + + @Test + void collapsesDupStorePopForReferenceStaticField() { + def bytecode = sequenceFor { + visitLdcInsn('value') + visitInsn(Opcodes.DUP) + visitFieldInsn(Opcodes.PUTSTATIC, 'Owner', 'NAME', 'Ljava/lang/String;') + visitInsn(Opcodes.POP) + visitInsn(RETURN) + } + + assert opcodeLines(bytecode) == [ + 'LDC "value"', + 'PUTSTATIC Owner.NAME : Ljava/lang/String;', + 'RETURN', + ] + } + + @Test + void preservesMismatchedDupAndPopSizes() { + def bytecode = sequenceFor { + visitInsn(Opcodes.ICONST_1) + visitInsn(Opcodes.DUP) + visitInsn(Opcodes.POP2) // size mismatch → keep DUP + visitLdcInsn(2L) + visitInsn(Opcodes.DUP2) + visitVarInsn(Opcodes.ISTORE, 0) // wide dup + narrow store → no collapse on later pop + visitInsn(Opcodes.POP2) + visitInsn(RETURN) + } + + assert opcodeLines(bytecode) == [ + 'ICONST_1', + 'DUP', + 'POP2', + 'LDC 2L', + 'DUP2', + 'ISTORE 0', + 'POP2', + 'RETURN', + ] + } + + @Test + void preservesAttachedCheckcastOnConstantBeforePop() { + def withPop = sequenceFor('(Ljava/lang/Object;)V') { + visitLdcInsn('x') + visitTypeInsn(Opcodes.CHECKCAST, 'java/lang/String') + visitInsn(Opcodes.POP) + visitInsn(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) // flush ALOAD so the cast is standalone + visitTypeInsn(Opcodes.CHECKCAST, 'java/lang/String') + visitInsn(Opcodes.POP2) + visitInsn(RETURN) + } + assert opcodeLines(standalonePop2) == [ + 'ALOAD 0', + 'NOP', + 'CHECKCAST java/lang/String', + 'POP2', + 'RETURN', + ] + } + + // --- box/unbox cancellation and Boolean folding (from gjit) --- + + @Test + void foldsBooleanTrueWithBooleanUnbox() { + def bytecode = sequenceFor('()Z') { + visitFieldInsn(Opcodes.GETSTATIC, 'java/lang/Boolean', 'TRUE', 'Ljava/lang/Boolean;') + visitMethodInsn(Opcodes.INVOKESTATIC, + 'org/codehaus/groovy/runtime/typehandling/DefaultTypeTransformation', + 'booleanUnbox', '(Ljava/lang/Object;)Z', false) + visitInsn(Opcodes.IRETURN) + } + + assert opcodeLines(bytecode) == ['ICONST_1', 'IRETURN'] + } + + @Test + void foldsBooleanFalseWithBooleanValue() { + def bytecode = sequenceFor('()Z') { + visitFieldInsn(Opcodes.GETSTATIC, 'java/lang/Boolean', 'FALSE', 'Ljava/lang/Boolean;') + visitMethodInsn(Opcodes.INVOKEVIRTUAL, 'java/lang/Boolean', 'booleanValue', '()Z', false) + visitInsn(Opcodes.IRETURN) + } + + assert opcodeLines(bytecode) == ['ICONST_0', 'IRETURN'] + } + + @Test + void preservesBooleanConstantWhenNotUnboxed() { + def bytecode = sequenceFor('()Ljava/lang/Boolean;') { + visitFieldInsn(Opcodes.GETSTATIC, 'java/lang/Boolean', 'TRUE', 'Ljava/lang/Boolean;') + visitInsn(Opcodes.ARETURN) + } + + assert opcodeLines(bytecode) == [ + 'GETSTATIC java/lang/Boolean.TRUE : Ljava/lang/Boolean;', + 'ARETURN', + ] + } + + @Test + void dropsDeadBooleanConstantOnPop() { + def bytecode = sequenceFor('()V') { + visitFieldInsn(Opcodes.GETSTATIC, 'java/lang/Boolean', 'TRUE', 'Ljava/lang/Boolean;') + visitInsn(Opcodes.POP) + visitInsn(RETURN) + } + + assert opcodeLines(bytecode) == ['RETURN'] + } + + @Test + void cancelsIntegerValueOfWithIntValue() { + 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/Integer', 'intValue', '()I', false) + visitInsn(Opcodes.IRETURN) + } + + assert opcodeLines(bytecode) == ['ILOAD 0', 'IRETURN'] + } + + @Test + void cancelsLongValueOfWithLongValue() { + def bytecode = sequenceFor('(J)J') { + visitVarInsn(Opcodes.LLOAD, 0) + visitMethodInsn(Opcodes.INVOKESTATIC, 'java/lang/Long', 'valueOf', '(J)Ljava/lang/Long;', false) + visitMethodInsn(Opcodes.INVOKEVIRTUAL, 'java/lang/Long', 'longValue', '()J', false) + visitInsn(Opcodes.LRETURN) + } + + assert opcodeLines(bytecode) == ['LLOAD 0', 'LRETURN'] + } + + @Test + void cancelsDttBoxWithMatchingUnbox() { + def bytecode = sequenceFor('(D)D') { + visitVarInsn(Opcodes.DLOAD, 0) + visitMethodInsn(Opcodes.INVOKESTATIC, + 'org/codehaus/groovy/runtime/typehandling/DefaultTypeTransformation', + 'box', '(D)Ljava/lang/Object;', false) + visitMethodInsn(Opcodes.INVOKESTATIC, + 'org/codehaus/groovy/runtime/typehandling/DefaultTypeTransformation', + 'doubleUnbox', '(Ljava/lang/Object;)D', false) + visitInsn(Opcodes.DRETURN) + } + + assert opcodeLines(bytecode) == ['DLOAD 0', 'DRETURN'] + } + + @Test + void dropsBoxedValueDiscardedByPop() { + def bytecode = sequenceFor('(I)V') { + visitVarInsn(Opcodes.ILOAD, 0) + visitMethodInsn(Opcodes.INVOKESTATIC, 'java/lang/Integer', 'valueOf', '(I)Ljava/lang/Integer;', false) + visitInsn(Opcodes.POP) + visitInsn(RETURN) + } + + // Box and pure ILOAD are both dead once the boxed value is discarded. + assert opcodeLines(bytecode) == ['RETURN'] + } + + @Test + void dropsWideBoxedValueDiscardedByPop() { + def bytecode = sequenceFor('(J)V') { + visitVarInsn(Opcodes.LLOAD, 0) + visitMethodInsn(Opcodes.INVOKESTATIC, 'java/lang/Long', 'valueOf', '(J)Ljava/lang/Long;', false) + visitInsn(Opcodes.POP) + visitInsn(RETURN) + } + + assert opcodeLines(bytecode) == ['RETURN'] + } + + @Test + void dropsWideBoxedValueDiscardedByPop2() { + def bytecode = sequenceFor('(D)V') { + visitVarInsn(Opcodes.DLOAD, 0) + visitMethodInsn(Opcodes.INVOKESTATIC, + 'org/codehaus/groovy/runtime/typehandling/DefaultTypeTransformation', + 'box', '(D)Ljava/lang/Object;', false) + visitInsn(Opcodes.POP2) + visitInsn(RETURN) + } + + // POP2 matches the wide primitive under the box → drop box and producer. + assert opcodeLines(bytecode) == ['RETURN'] + } + + @Test + 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) + } + + 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 + void flushesBoxWhenValueIsStored() { + def bytecode = sequenceFor('(I)V') { + visitVarInsn(Opcodes.ILOAD, 0) + visitMethodInsn(Opcodes.INVOKESTATIC, 'java/lang/Integer', 'valueOf', '(I)Ljava/lang/Integer;', false) + visitVarInsn(Opcodes.ASTORE, 1) + visitInsn(RETURN) + } + + assert opcodeLines(bytecode) == [ + 'ILOAD 0', + 'INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;', + 'ASTORE 1', + 'RETURN', + ] + } + + @Test + 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) + visitMethodInsn(Opcodes.INVOKEVIRTUAL, 'java/lang/Integer', 'longValue', '()J', false) + visitInsn(Opcodes.LRETURN) + } + + assert opcodeLines(bytecode) == ['ILOAD 0', 'I2L', 'LRETURN'] + } + + @Test + void doesNotBufferNonPrimitiveValueOf() { + def bytecode = sequenceFor('(Ljava/lang/String;)Ljava/lang/Integer;') { + visitVarInsn(Opcodes.ALOAD, 0) + visitMethodInsn(Opcodes.INVOKESTATIC, 'java/lang/Integer', 'valueOf', '(Ljava/lang/String;)Ljava/lang/Integer;', false) + visitInsn(Opcodes.ARETURN) + } + + assert opcodeLines(bytecode) == [ + 'ALOAD 0', + 'INVOKESTATIC java/lang/Integer.valueOf (Ljava/lang/String;)Ljava/lang/Integer;', + 'ARETURN', + ] + } + + @Test + 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) + visitMethodInsn(Opcodes.INVOKEVIRTUAL, 'java/lang/Long', 'longValue', '()J', false) + visitInsn(Opcodes.LRETURN) + } + + assert opcodeLines(bytecode) == ['ILOAD 0', 'I2L', 'LRETURN'] + } + + @Test + void doesNotFoldBooleanConstantWithNonUnboxCall() { + def bytecode = sequenceFor('()Ljava/lang/String;') { + visitFieldInsn(Opcodes.GETSTATIC, 'java/lang/Boolean', 'TRUE', 'Ljava/lang/Boolean;') + visitMethodInsn(Opcodes.INVOKEVIRTUAL, 'java/lang/Boolean', 'toString', '()Ljava/lang/String;', false) + visitInsn(Opcodes.ARETURN) + } + + assert opcodeLines(bytecode) == [ + 'GETSTATIC java/lang/Boolean.TRUE : Ljava/lang/Boolean;', + 'INVOKEVIRTUAL java/lang/Boolean.toString ()Ljava/lang/String;', + 'ARETURN', + ] + } + + @Test + void cancelsFloatAndDoubleValueOfPairs() { + def floatBytecode = sequenceFor('(F)F') { + visitVarInsn(Opcodes.FLOAD, 0) + visitMethodInsn(Opcodes.INVOKESTATIC, 'java/lang/Float', 'valueOf', '(F)Ljava/lang/Float;', false) + visitMethodInsn(Opcodes.INVOKEVIRTUAL, 'java/lang/Float', 'floatValue', '()F', false) + visitInsn(Opcodes.FRETURN) + } + def doubleBytecode = sequenceFor('(D)D') { + visitVarInsn(Opcodes.DLOAD, 0) + visitMethodInsn(Opcodes.INVOKESTATIC, 'java/lang/Double', 'valueOf', '(D)Ljava/lang/Double;', false) + visitMethodInsn(Opcodes.INVOKEVIRTUAL, 'java/lang/Double', 'doubleValue', '()D', false) + visitInsn(Opcodes.DRETURN) + } + + assert opcodeLines(floatBytecode) == ['FLOAD 0', 'FRETURN'] + assert opcodeLines(doubleBytecode) == ['DLOAD 0', 'DRETURN'] + } + + @Test + void cancelsBooleanByteCharShortValueOfPairs() { + def cases = [ + ['java/lang/Boolean', 'Z', 'booleanValue', Opcodes.ILOAD, Opcodes.IRETURN], + ['java/lang/Byte', 'B', 'byteValue', Opcodes.ILOAD, Opcodes.IRETURN], + ['java/lang/Character', 'C', 'charValue', Opcodes.ILOAD, Opcodes.IRETURN], + ['java/lang/Short', 'S', 'shortValue', Opcodes.ILOAD, Opcodes.IRETURN], + ] + cases.each { owner, prim, unboxName, load, ret -> + def bytecode = sequenceFor("($prim)$prim") { + visitVarInsn(load, 0) + visitMethodInsn(Opcodes.INVOKESTATIC, owner, 'valueOf', "($prim)L${owner};", false) + visitMethodInsn(Opcodes.INVOKEVIRTUAL, owner, unboxName, "()$prim", false) + visitInsn(ret) + } + assert opcodeLines(bytecode) == ["${Printer.OPCODES[load]} 0", Printer.OPCODES[ret]] + } + } + + @Test + void cancelsAllDttBoxUnboxPairs() { + def cases = [ + ['Z', 'booleanUnbox', Opcodes.ILOAD, Opcodes.IRETURN], + ['B', 'byteUnbox', Opcodes.ILOAD, Opcodes.IRETURN], + ['C', 'charUnbox', Opcodes.ILOAD, Opcodes.IRETURN], + ['S', 'shortUnbox', Opcodes.ILOAD, Opcodes.IRETURN], + ['I', 'intUnbox', Opcodes.ILOAD, Opcodes.IRETURN], + ['J', 'longUnbox', Opcodes.LLOAD, Opcodes.LRETURN], + ['F', 'floatUnbox', Opcodes.FLOAD, Opcodes.FRETURN], + ['D', 'doubleUnbox', Opcodes.DLOAD, Opcodes.DRETURN], + ] + cases.each { prim, unboxName, load, ret -> + def bytecode = sequenceFor("($prim)$prim") { + visitVarInsn(load, 0) + visitMethodInsn(Opcodes.INVOKESTATIC, + 'org/codehaus/groovy/runtime/typehandling/DefaultTypeTransformation', + 'box', "($prim)Ljava/lang/Object;", false) + visitMethodInsn(Opcodes.INVOKESTATIC, + 'org/codehaus/groovy/runtime/typehandling/DefaultTypeTransformation', + unboxName, "(Ljava/lang/Object;)$prim", false) + visitInsn(ret) + } + assert opcodeLines(bytecode) == ["${Printer.OPCODES[load]} 0", Printer.OPCODES[ret]] + } + } + + @Test + 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, + 'org/codehaus/groovy/runtime/typehandling/DefaultTypeTransformation', + 'box', '(I)Ljava/lang/Object;', false) + visitMethodInsn(Opcodes.INVOKEVIRTUAL, 'java/lang/Integer', 'intValue', '()I', false) + 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 rewritesDttBoxIntWithLongLongValueToI2L() { + // DTT.box(int) then Long.longValue → I2L (known boxed int → long). + 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', '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 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() + def bytecode = sequenceFor('(I)V') { + visitVarInsn(Opcodes.ILOAD, 0) + visitMethodInsn(Opcodes.INVOKESTATIC, 'java/lang/Integer', 'valueOf', '(I)Ljava/lang/Integer;', false) + visitLabel(label) + visitInsn(Opcodes.POP) + visitInsn(RETURN) + } + + assert opcodeLines(bytecode) == [ + 'ILOAD 0', + 'INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;', + 'POP', + 'RETURN', + ] + } + private InstructionSequence sequenceFor(String descriptor = '()V', @DelegatesTo(MethodVisitor) Closure emitter) { traceSequence(methodNodeFor(descriptor, emitter)) }