This is an automated email from the ASF dual-hosted git repository. asf-gitbox-commits pushed a commit to branch GROOVY-12162 in repository https://gitbox.apache.org/repos/asf/groovy.git
commit 39bc3fcdb36ed65fdf9dd22498024d205517ace9 Author: Daniel Sun <[email protected]> AuthorDate: Tue Jul 14 01:32:24 2026 +0900 GROOVY-12162: Harden and extend bytecode peephole optimization --- .../groovy/classgen/AsmClassGenerator.java | 37 +- .../groovy/classgen/asm/CallSiteWriter.java | 6 +- .../asm/PeepholeOptimizingClassVisitor.java | 76 ++ .../asm/PeepholeOptimizingMethodVisitor.java | 779 +++++++++++++++++-- .../groovy/classgen/asm/WriterController.java | 10 +- .../asm/PeepholeOptimizingMethodVisitorTest.groovy | 859 ++++++++++++++++++++- 6 files changed, 1663 insertions(+), 104 deletions(-) diff --git a/src/main/java/org/codehaus/groovy/classgen/AsmClassGenerator.java b/src/main/java/org/codehaus/groovy/classgen/AsmClassGenerator.java index 46bea1fb5d..c0fc16f8ec 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.RecordComponentVisitor; 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 @@ public class AsmClassGenerator extends ClassGenerator { 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 @@ public class AsmClassGenerator extends ClassGenerator { 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 class AsmClassGenerator extends ClassGenerator { while (index<size) { String methodName = "$createListEntry_" + controller.getNextHelperMethodIndex(); methods.add(methodName); - mv = new PeepholeOptimizingMethodVisitor( - controller.getClassVisitor().visitMethod( - ACC_PRIVATE + ACC_STATIC + ACC_SYNTHETIC, - methodName, - "([Ljava/lang/Object;)V", - null, null)); + mv = controller.getClassVisitor().visitMethod( + ACC_PRIVATE + ACC_STATIC + ACC_SYNTHETIC, + methodName, + "([Ljava/lang/Object;)V", + null, null); controller.setMethodVisitor(mv); mv.visitCode(); int methodBlockSize = Math.min(size-index, maxInit); diff --git a/src/main/java/org/codehaus/groovy/classgen/asm/CallSiteWriter.java b/src/main/java/org/codehaus/groovy/classgen/asm/CallSiteWriter.java index 8196cb0030..9f1b52fcf3 100644 --- a/src/main/java/org/codehaus/groovy/classgen/asm/CallSiteWriter.java +++ b/src/main/java/org/codehaus/groovy/classgen/asm/CallSiteWriter.java @@ -188,8 +188,7 @@ public class CallSiteWriter { methodIndex++; String methodName = "$createCallSiteArray_" + methodIndex; callSiteInitMethods.add(methodName); - MethodVisitor mv = new PeepholeOptimizingMethodVisitor( - controller.getClassVisitor().visitMethod(MOD_PRIVSS, methodName, "([Ljava/lang/String;)V", null, null)); + MethodVisitor mv = controller.getClassVisitor().visitMethod(MOD_PRIVSS, methodName, "([Ljava/lang/String;)V", null, null); controller.setMethodVisitor(mv); mv.visitCode(); int methodLimit = size; @@ -208,8 +207,7 @@ public class CallSiteWriter { mv.visitEnd(); } // create base createCallSiteArray method - MethodVisitor mv = new PeepholeOptimizingMethodVisitor( - controller.getClassVisitor().visitMethod(MOD_PRIVSS, CREATE_CSA_METHOD, GET_CALLSITEARRAY_DESC, null, null)); + MethodVisitor mv = controller.getClassVisitor().visitMethod(MOD_PRIVSS, CREATE_CSA_METHOD, GET_CALLSITEARRAY_DESC, null, null); controller.setMethodVisitor(mv); mv.visitCode(); mv.visitLdcInsn(size); diff --git a/src/main/java/org/codehaus/groovy/classgen/asm/PeepholeOptimizingClassVisitor.java b/src/main/java/org/codehaus/groovy/classgen/asm/PeepholeOptimizingClassVisitor.java new file mode 100644 index 0000000000..e54f449abf --- /dev/null +++ b/src/main/java/org/codehaus/groovy/classgen/asm/PeepholeOptimizingClassVisitor.java @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.codehaus.groovy.classgen.asm; + +import org.codehaus.groovy.control.CompilerConfiguration; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.MethodVisitor; + +/** + * Class-level installation point for Groovy's single-pass bytecode peephole layer. + * <p> + * Wraps each {@link MethodVisitor} returned by the delegate's + * {@link #visitMethod(int, String, String, String, String[])} with + * {@link PeepholeOptimizingMethodVisitor}, so <em>every</em> 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). + * <p> + * 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. + * <p> + * 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} + * <p> + * 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 6acdc28738..c152ed8682 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; @@ -46,6 +50,7 @@ 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.ICONST_0; import static org.objectweb.asm.Opcodes.ICONST_1; import static org.objectweb.asm.Opcodes.ICONST_2; @@ -54,19 +59,16 @@ import static org.objectweb.asm.Opcodes.ICONST_4; 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.LCONST_0; import static org.objectweb.asm.Opcodes.LCONST_1; @@ -80,10 +82,78 @@ import static org.objectweb.asm.Opcodes.RETURN; 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. + * <p> + * 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. * + * <h2>Model</h2> + * At most one <em>pending load</em> (constant, variable load, standalone + * {@code CHECKCAST}, or {@code Boolean.TRUE}/{@code FALSE}), at most one + * <em>pending box</em> ({@code Wrapper.valueOf} or + * {@link DefaultTypeTransformation#box}), and at most one + * <em>pending {@code DUP}/{@code DUP2}</em> (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: + * <ul> + * <li>labels, jump targets, table/lookup switches</li> + * <li>stack map frames</li> + * <li>line numbers, local-variable tables, and type annotations</li> + * <li>method and {@code invokedynamic} calls (except matched box/unbox pairs)</li> + * <li>{@code visitMaxs} / {@code visitEnd}</li> + * </ul> + * + * <h2>Rewrites</h2> + * <ul> + * <li><b>Constant narrowing</b> — {@code LDC}/push forms become + * {@code ICONST_*}, {@code BIPUSH}, {@code SIPUSH}, {@code LCONST_*}, + * {@code FCONST_*}, {@code DCONST_*}, or {@code ACONST_NULL} when legal. + * Signed floating-point zeros ({@code -0.0f}/{@code -0.0d}) are preserved + * via raw-bit comparison (GROOVY-9797).</li> + * <li><b>Dead loads</b> — a buffered load or constant followed by a matching + * {@code POP}/{@code POP2}, or a void {@code RETURN}, is dropped. A + * buffered {@code IINC} paired with {@code ILOAD} is retained as a side + * effect when the loaded value itself is discarded.</li> + * <li><b>CHECKCAST</b> — may attach to a pending {@code ALOAD} or reference + * constant so {@code load}; {@code CHECKCAST}; {@code POP} collapses + * entirely. A standalone cast before {@code POP} is dropped; before void + * {@code RETURN} the cast is flushed and the value is popped so the + * type-check side effect is kept and the void return stays verifiable.</li> + * <li><b>Compare-to-zero / null</b> — + * {@code ICONST_0}; {@code IF_ICMP*} → {@code IF*}; + * {@code ACONST_NULL}; {@code IF_ACMP*} → {@code IFNULL}/{@code IFNONNULL}.</li> + * <li><b>DUP + store + pop</b> — {@code DUP}/{@code DUP2}; store; + * matching pop becomes a plain store; bare {@code DUP}/{@code DUP2} with a + * matching pop is eliminated.</li> + * <li><b>Box/unbox cancellation</b> — {@code Wrapper.valueOf(p)}; + * {@code Wrapper.xxxValue()} and {@code DefaultTypeTransformation.box(p)}; + * {@code DefaultTypeTransformation.xxxUnbox(...)} with matching primitive + * types cancel to a no-op (the primitive remains on the stack). A boxed + * value discarded by {@code POP}/{@code POP2} drops the box and pops the + * original primitive instead (using {@code POP2} when the primitive is + * wide). Mismatched unbox types are left intact.</li> + * <li><b>Boolean constant folding</b> — {@code GETSTATIC Boolean.TRUE/FALSE} + * followed by {@code booleanValue()} or + * {@code DefaultTypeTransformation.booleanUnbox} becomes + * {@code ICONST_1}/{@code ICONST_0}.</li> + * <li><b>Big number lowering</b> — buffered {@link BigDecimal}/{@link BigInteger} + * constants become {@code new Type(String)} construction on flush.</li> + * </ul> + * + * <h2>Installation</h2> + * 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 + * <em>delegate</em> 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 +161,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 +171,54 @@ 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}. + * <ul> + * <li>When {@link #pendingLoadKind} is {@link PendingLoadKind#VARIABLE} or + * {@link PendingLoadKind#CONSTANT}: optional cast attached to that load + * (emitted after the load on flush, or dropped with the load on pop).</li> + * <li>When the kind is {@link PendingLoadKind#CHECKCAST}: standalone cast of + * a value already written to the operand stack.</li> + * </ul> + */ 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 +228,73 @@ public final class PeepholeOptimizingMethodVisitor extends MethodVisitor { 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. + * <p> + * 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}. + * <p> + * {@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 +309,23 @@ public final class PeepholeOptimizingMethodVisitor extends MethodVisitor { @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 +381,7 @@ public final class PeepholeOptimizingMethodVisitor extends MethodVisitor { public void visitIntInsn(final int opcode, final int operand) { flushPendingLoad(); flushPendingDupStore(); + flushPendingBox(); if (opcode == BIPUSH || opcode == SIPUSH) { bufferConstant(operand); return; @@ -223,11 +393,13 @@ public final class PeepholeOptimizingMethodVisitor extends MethodVisitor { 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 +409,19 @@ public final class PeepholeOptimizingMethodVisitor extends MethodVisitor { @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 +429,32 @@ public final class PeepholeOptimizingMethodVisitor extends MethodVisitor { 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 (tryBufferBox(opcode, owner, name, descriptor, isInterface)) { + return; + } + flushPending(); super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); } @@ -273,7 +468,8 @@ public final class PeepholeOptimizingMethodVisitor extends MethodVisitor { @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 +487,7 @@ public final class PeepholeOptimizingMethodVisitor extends MethodVisitor { public void visitLdcInsn(final Object value) { flushPendingLoad(); flushPendingDupStore(); + flushPendingBox(); if (value instanceof ConstantDynamic) { super.visitLdcInsn(value); return; @@ -303,7 +500,8 @@ public final class PeepholeOptimizingMethodVisitor extends MethodVisitor { if (pendingLoadKind == PendingLoadKind.VARIABLE && pendingLoadOpcode == ILOAD && pendingLoadVar == varIndex - && !pendingLoadHasIinc) { + && !pendingLoadHasIinc + && pendingCheckcastDescriptor == null) { pendingLoadHasIinc = true; pendingLoadIncrement = increment; return; @@ -379,31 +577,82 @@ public final class PeepholeOptimizingMethodVisitor extends MethodVisitor { 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. + */ + private void flushPendingBox() { + if (pendingBoxKind == PendingBoxKind.NONE) { + return; + } + 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 +665,50 @@ public final class PeepholeOptimizingMethodVisitor extends MethodVisitor { clearPendingDupStore(); } + /** + * Rewrites {@code …; ICONST_0; IF_ICMPxx L} to {@code …; IFxx L} when the + * pending constant is integer zero. + * <p> + * 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,6 +717,17 @@ public final class PeepholeOptimizingMethodVisitor extends MethodVisitor { return true; } + /** + * Drops a dead buffered load/constant (or standalone {@code CHECKCAST}) when + * the next instruction is a matching {@code POP}/{@code POP2}. + * <p> + * For a standalone cast the value is already on the stack, so only the cast is + * removed and the pop is still emitted. For a buffered load, both the load and + * any attached cast are removed; a paired {@code IINC} is preserved as a side + * effect. + * + * @return {@code true} if the pop was fully handled + */ private boolean tryRemovePendingLoad(final int opcode) { if (pendingLoadKind == PendingLoadKind.NONE) { return false; @@ -450,6 +739,7 @@ public final class PeepholeOptimizingMethodVisitor extends MethodVisitor { } if (pendingLoadKind == PendingLoadKind.CHECKCAST) { + // Value already on stack: drop the dead cast, keep the pop. if (opcode != POP) { return false; } @@ -462,56 +752,145 @@ public final class PeepholeOptimizingMethodVisitor extends MethodVisitor { return false; } - if (pendingLoadKind == PendingLoadKind.VARIABLE && pendingLoadHasIinc) { - super.visitIincInsn(pendingLoadVar, pendingLoadIncrement); - } + emitPreservedIincSideEffect(); clearPendingLoad(); return true; } + /** + * Handles void {@code RETURN} against the load window: + * <ul> + * <li>Pure dead load/constant — dropped (paired {@code IINC} kept).</li> + * <li>Load with attached {@code CHECKCAST}, or standalone cast — the cast is + * emitted so a type-check side effect is preserved, then {@code POP} clears + * the operand stack so the void return is verifiable.</li> + * </ul> + * + * @return {@code true} if the return was fully handled + */ private boolean tryDropPendingLoadOnReturn(final int opcode) { if (opcode != RETURN || pendingLoadKind == PendingLoadKind.NONE) { return false; } - if (pendingLoadKind == PendingLoadKind.VARIABLE && pendingLoadHasIinc) { - super.visitIincInsn(pendingLoadVar, pendingLoadIncrement); + 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: + * <ul> + * <li>{@code DUP}; {@code xSTORE}; {@code POP} → {@code xSTORE}</li> + * <li>{@code DUP2}; {@code PUTSTATIC J/D}; {@code POP2} → {@code PUTSTATIC}</li> + * <li>{@code DUP}/{@code DUP2}; matching pop with no store → eliminated entirely</li> + * </ul> + * + * @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 +930,7 @@ public final class PeepholeOptimizingMethodVisitor extends MethodVisitor { pendingLoadHasIinc = false; pendingLoadIncrement = 0; pendingCheckcastDescriptor = null; + pendingBooleanValue = false; } private void clearPendingDupStore() { @@ -563,11 +943,272 @@ public final class PeepholeOptimizingMethodVisitor extends MethodVisitor { 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; + } + + /** + * {@code valueOf}/{@code box} followed by {@code POP}/{@code POP2} means the + * boxed reference is discarded. Drop the box and pop the original primitive + * instead ({@code POP2} when the primitive is {@code long} or {@code double}). + * + * @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); + // POP after boxing a wide primitive is legal (boxed ref is one slot) → POP2 the long/double. + if (popSize == 1 && primitiveSize == 2) { + clearPendingBox(); + super.visitInsn(POP2); + return true; + } + if (popSize != primitiveSize) { + return false; + } + clearPendingBox(); + super.visitInsn(primitiveSize == 2 ? POP2 : POP); + 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) can collapse the pair. + * Any previously pending state is flushed first so the primitive producer is + * already on the operand stack. + * + * @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; + } + flushPending(); + bufferValueOfBox(owner, primitive, isInterface); + return true; + } + if ("box".equals(name) && DTT_OWNER.equals(owner)) { + String primitive = primitiveOperandOfDttBox(descriptor); + if (primitive == null) { + return false; + } + flushPending(); + bufferDttBox(primitive); + return true; + } + return false; + } + + private boolean matchesPendingUnbox(final int opcode, final String owner, final String name, final String descriptor) { + String expectedPrim = pendingBoxPrimitiveDescriptor; + if (expectedPrim == null) { + return false; + } + if (pendingBoxKind == PendingBoxKind.VALUE_OF) { + // Wrapper.xxxValue()X — e.g. Integer.intValue()I + if (opcode != INVOKEVIRTUAL || !pendingBoxOwner.equals(owner)) { + return false; + } + String expectedName = primitiveValueMethodName(expectedPrim); + String expectedDesc = "()" + expectedPrim; + return expectedName != null && expectedName.equals(name) && expectedDesc.equals(descriptor); + } + if (pendingBoxKind == PendingBoxKind.DTT_BOX) { + // DefaultTypeTransformation.xxxUnbox(Object)X + if (opcode != INVOKESTATIC || !DTT_OWNER.equals(owner)) { + return false; + } + String expectedName = primitiveUnboxMethodName(expectedPrim); + String expectedDesc = "(Ljava/lang/Object;)" + expectedPrim; + return expectedName != null && expectedName.equals(name) && expectedDesc.equals(descriptor); + } + return false; + } + + 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 +1272,11 @@ public final class PeepholeOptimizingMethodVisitor extends MethodVisitor { }; } + /** + * 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 +1289,8 @@ public final class PeepholeOptimizingMethodVisitor extends MethodVisitor { } 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 +1312,10 @@ public final class PeepholeOptimizingMethodVisitor extends MethodVisitor { 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 +1324,7 @@ public final class PeepholeOptimizingMethodVisitor extends MethodVisitor { super.visitMethodInsn(INVOKESPECIAL, type, "<init>", 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 +1335,10 @@ public final class PeepholeOptimizingMethodVisitor extends MethodVisitor { } } + /** + * 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 +1352,10 @@ public final class PeepholeOptimizingMethodVisitor extends MethodVisitor { } } + /** + * 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 d69cb00d61..6178523f01 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 class WriterController { } 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 241aaff00c..ee2618a32c 100644 --- a/src/test/groovy/org/codehaus/groovy/classgen/asm/PeepholeOptimizingMethodVisitorTest.groovy +++ b/src/test/groovy/org/codehaus/groovy/classgen/asm/PeepholeOptimizingMethodVisitorTest.groovy @@ -329,16 +329,91 @@ final class PeepholeOptimizingMethodVisitorTest { assert opcodeLines(bytecode) == ['IINC 0 1', 'RETURN'] } + @Test + void removesLoadAndAttachedCheckcastBeforePop() { + def bytecode = sequenceFor('(Ljava/lang/Object;)V') { + visitVarInsn(Opcodes.ALOAD, 0) + visitTypeInsn(Opcodes.CHECKCAST, 'java/lang/String') + visitInsn(Opcodes.POP) + visitInsn(RETURN) + } + + // ALOAD + CHECKCAST are both dead once the value is discarded. + assert opcodeLines(bytecode) == ['RETURN'] + } + @Test void removesStandaloneCheckcastBeforePop() { 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'] + assert opcodeLines(bytecode) == ['ALOAD 0', 'NOP', '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 +441,788 @@ final class PeepholeOptimizingMethodVisitorTest { ] } + @Test + void preservesStandaloneCheckcastBeforeReturn() { + def bytecode = sequenceFor('(Ljava/lang/Object;)V') { + visitVarInsn(Opcodes.ALOAD, 0) + visitInsn(Opcodes.NOP) + visitTypeInsn(Opcodes.CHECKCAST, 'java/lang/String') + visitInsn(Opcodes.POP) + visitInsn(RETURN) + } + + // Standalone CHECKCAST before POP is dropped; before RETURN it is flushed + // and the value is popped so the void return remains verifiable. + def returnOnly = sequenceFor('(Ljava/lang/Object;)V') { + visitVarInsn(Opcodes.ALOAD, 0) + visitInsn(Opcodes.NOP) + visitTypeInsn(Opcodes.CHECKCAST, 'java/lang/String') + visitInsn(RETURN) + } + + assert opcodeLines(bytecode) == ['ALOAD 0', 'NOP', '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 attachesCheckcastToReferenceConstantsAndDropsDeadCastChains() { + 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) + } + + assert opcodeLines(bytecode) == ['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 dropsDeadLoadWithAttachedCheckcastOnPopOnly() { + def withPop = sequenceFor('(Ljava/lang/Object;)V') { + visitLdcInsn('x') + visitTypeInsn(Opcodes.CHECKCAST, 'java/lang/String') + visitInsn(Opcodes.POP) + visitInsn(RETURN) + } + assert opcodeLines(withPop) == ['RETURN'] + + def standalonePop2 = sequenceFor('(Ljava/lang/Object;)V') { + visitVarInsn(Opcodes.ALOAD, 0) + visitInsn(Opcodes.NOP) + visitTypeInsn(Opcodes.CHECKCAST, 'java/lang/String') + visitInsn(Opcodes.POP2) // only POP removes standalone cast + 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) + } + + assert opcodeLines(bytecode) == ['ILOAD 0', 'POP', '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) == ['LLOAD 0', 'POP2', '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 size matches the wide primitive that was boxed → drop box, pop the double. + assert opcodeLines(bytecode) == ['DLOAD 0', 'POP2', 'RETURN'] + } + + @Test + void doesNotTreatPop2AsDiscardOfNarrowBoxedValue() { + def bytecode = sequenceFor('(I)V') { + visitVarInsn(Opcodes.ILOAD, 0) + visitMethodInsn(Opcodes.INVOKESTATIC, 'java/lang/Integer', 'valueOf', '(I)Ljava/lang/Integer;', false) + visitInsn(Opcodes.POP2) + visitInsn(RETURN) + } + + // POP2 does not match a one-slot boxed reference; flush the box and keep POP2. + assert opcodeLines(bytecode) == [ + 'ILOAD 0', + 'INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;', + 'POP2', + 'RETURN', + ] + } + + @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 doesNotCancelMismatchedBoxUnboxTypes() { + 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', + 'INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;', + 'INVOKEVIRTUAL java/lang/Integer.longValue ()J', + '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 doesNotCancelValueOfWithMismatchedWrapperUnbox() { + // Integer.valueOf then Long.longValue — different owners, must not cancel. + 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', + 'INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;', + 'INVOKEVIRTUAL java/lang/Long.longValue ()J', + '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 doesNotCancelDttBoxWithValueOfStyleUnbox() { + // DTT.box then Integer.intValue — different unbox convention, must not cancel. + 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', + 'INVOKESTATIC org/codehaus/groovy/runtime/typehandling/DefaultTypeTransformation.box (I)Ljava/lang/Object;', + 'INVOKEVIRTUAL java/lang/Integer.intValue ()I', + 'IRETURN', + ] + } + + @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)) }
