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 1af9d641b6fdde7b8bbdca7be233ee6f8f7f9e35 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 | 410 +++++++++++++--- .../groovy/classgen/asm/WriterController.java | 10 +- .../asm/PeepholeOptimizingMethodVisitorTest.groovy | 526 ++++++++++++++++++++- 6 files changed, 963 insertions(+), 102 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..035387ea9f 100644 --- a/src/main/java/org/codehaus/groovy/classgen/asm/PeepholeOptimizingMethodVisitor.java +++ b/src/main/java/org/codehaus/groovy/classgen/asm/PeepholeOptimizingMethodVisitor.java @@ -25,8 +25,11 @@ 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; @@ -54,17 +57,12 @@ 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.INVOKESPECIAL; import static org.objectweb.asm.Opcodes.ISTORE; @@ -80,10 +78,65 @@ 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). 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, or standalone + * {@code CHECKCAST}) 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</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>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 { @@ -98,6 +151,7 @@ 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, @@ -105,6 +159,7 @@ public final class PeepholeOptimizingMethodVisitor extends MethodVisitor { CHECKCAST } + /** Kind of store buffered after a pending {@code DUP}/{@code DUP2}, if any. */ private enum PendingStoreKind { NONE, VARIABLE, @@ -117,6 +172,16 @@ public final class PeepholeOptimizingMethodVisitor extends MethodVisitor { 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; private int pendingDupOpcode = NO_OPCODE; @@ -127,10 +192,66 @@ public final class PeepholeOptimizingMethodVisitor extends MethodVisitor { private String pendingStoreName; private String pendingStoreDescriptor; + /** + * 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,7 +266,7 @@ 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)) { return; } @@ -237,12 +358,18 @@ public final class PeepholeOptimizingMethodVisitor extends MethodVisitor { @Override public void visitTypeInsn(final int opcode, final String descriptor) { - flushPendingLoad(); flushPendingDupStore(); if (opcode == CHECKCAST) { + if (canAttachCheckcastToPendingLoad()) { + pendingCheckcastDescriptor = descriptor; + return; + } + flushPendingLoad(); bufferCheckcast(descriptor); return; } + + flushPendingLoad(); super.visitTypeInsn(opcode, descriptor); } @@ -273,7 +400,7 @@ public final class PeepholeOptimizingMethodVisitor extends MethodVisitor { @Override public void visitJumpInsn(final int opcode, final Label label) { flushPendingDupStore(); - if (tryRewriteZeroCompare(opcode, label)) { + if (tryRewriteZeroCompare(opcode, label) || tryRewriteNullCompare(opcode, label)) { return; } @@ -303,7 +430,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,21 +507,28 @@ public final class PeepholeOptimizingMethodVisitor extends MethodVisitor { super.visitEnd(); } + /** Flushes both the load window and any pending {@code DUP}/store pair. */ private void flushPending() { flushPendingLoad(); flushPendingDupStore(); } + /** + * 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); @@ -404,6 +539,17 @@ public final class PeepholeOptimizingMethodVisitor extends MethodVisitor { clearPendingLoad(); } + /** 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 +562,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 +614,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 +636,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 +649,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; } @@ -631,6 +907,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 +924,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 +947,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 +959,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 +970,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 +987,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..e0f58e675c 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,455 @@ 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', + ] + } + private InstructionSequence sequenceFor(String descriptor = '()V', @DelegatesTo(MethodVisitor) Closure emitter) { traceSequence(methodNodeFor(descriptor, emitter)) }
