diff --git a/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/nodes/graphbuilderconf/GraphBuilderContext.java b/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/nodes/graphbuilderconf/GraphBuilderContext.java index fc96098c427d..42ecf2abdf27 100644 --- a/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/nodes/graphbuilderconf/GraphBuilderContext.java +++ b/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/nodes/graphbuilderconf/GraphBuilderContext.java @@ -75,6 +75,7 @@ import jdk.graal.compiler.nodes.type.StampTool; import jdk.internal.misc.ScopedMemoryAccess; import jdk.vm.ci.code.BailoutException; +import jdk.vm.ci.code.BytecodePosition; import jdk.vm.ci.meta.Assumptions; import jdk.vm.ci.meta.DeoptimizationAction; import jdk.vm.ci.meta.DeoptimizationReason; @@ -281,6 +282,19 @@ default int getDepth() { return result; } + /** + * Gets the inlining chain of this context. A null return value implies that this is the context + * for the parse root. + */ + default BytecodePosition getInliningChain() { + BytecodePosition inliningContext = null; + for (GraphBuilderContext cur = getParent(); cur != null; cur = cur.getParent()) { + BytecodePosition caller = new BytecodePosition(null, cur.getMethod(), cur.bci()); + inliningContext = inliningContext == null ? caller : inliningContext.addCaller(caller); + } + return inliningContext; + } + /** * Computes the recursive inlining depth of the provided method, i.e., counts how often the * provided method is already in the {@link #getParent()} chain starting at this context. diff --git a/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/replacements/IntrinsicGraphBuilder.java b/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/replacements/IntrinsicGraphBuilder.java index 27a18d59d57d..32becbc60695 100644 --- a/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/replacements/IntrinsicGraphBuilder.java +++ b/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/replacements/IntrinsicGraphBuilder.java @@ -68,6 +68,7 @@ import jdk.graal.compiler.nodes.spi.CoreProvidersDelegate; import jdk.graal.compiler.options.OptionValues; import jdk.vm.ci.code.BailoutException; +import jdk.vm.ci.code.BytecodePosition; import jdk.vm.ci.meta.DeoptimizationAction; import jdk.vm.ci.meta.DeoptimizationReason; import jdk.vm.ci.meta.JavaKind; @@ -325,6 +326,11 @@ public int getDepth() { return 0; } + @Override + public BytecodePosition getInliningChain() { + return null; + } + @Override public boolean parsingIntrinsic() { return false; diff --git a/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/replacements/PEGraphDecoder.java b/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/replacements/PEGraphDecoder.java index ba0af5322903..910cbe5705ff 100644 --- a/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/replacements/PEGraphDecoder.java +++ b/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/replacements/PEGraphDecoder.java @@ -42,6 +42,7 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import jdk.vm.ci.code.BytecodePosition; import org.graalvm.collections.Pair; import jdk.graal.compiler.api.replacements.Fold; @@ -412,6 +413,18 @@ public int getDepth() { return methodScope.inliningDepth; } + @Override + public BytecodePosition getInliningChain() { + BytecodePosition inliningContext = null; + int bci = methodScope.invokeData == null ? 0 : methodScope.invokeData.invoke.bci(); + for (PEMethodScope cur = methodScope.caller; cur != null; cur = cur.caller) { + BytecodePosition caller = new BytecodePosition(null, cur.method, bci); + inliningContext = inliningContext == null ? caller : inliningContext.addCaller(caller); + bci = cur.invokeData == null ? 0 : cur.invokeData.invoke.bci(); + } + return inliningContext; + } + @Override public int recursiveInliningDepth(ResolvedJavaMethod method) { int result = 0; diff --git a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/infrastructure/WrappedConstantPool.java b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/infrastructure/WrappedConstantPool.java index f0c6282a9823..e242c10b690e 100644 --- a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/infrastructure/WrappedConstantPool.java +++ b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/infrastructure/WrappedConstantPool.java @@ -218,4 +218,8 @@ public List getStaticArguments() { return wrapped.getStaticArguments().stream().map(WrappedConstantPool.this::lookupConstant).collect(Collectors.toList()); } } + + public ConstantPool getWrapped() { + return wrapped; + } } diff --git a/substratevm/src/com.oracle.svm.graal/src/com/oracle/svm/graal/hosted/runtimecompilation/RuntimeCompiledMethodSupport.java b/substratevm/src/com.oracle.svm.graal/src/com/oracle/svm/graal/hosted/runtimecompilation/RuntimeCompiledMethodSupport.java index fa6ee71f17c8..f6c80dec3be4 100644 --- a/substratevm/src/com.oracle.svm.graal/src/com/oracle/svm/graal/hosted/runtimecompilation/RuntimeCompiledMethodSupport.java +++ b/substratevm/src/com.oracle.svm.graal/src/com/oracle/svm/graal/hosted/runtimecompilation/RuntimeCompiledMethodSupport.java @@ -477,6 +477,11 @@ protected boolean tryInvocationPlugin(CallTargetNode.InvokeKind invokeKind, Valu protected boolean shouldVerifyFrameStates() { return Options.VerifyRuntimeCompilationFrameStates.getValue(); } + + @Override + protected boolean strictDynamicAccessInferenceIsApplicable() { + return false; + } } /** diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ResourcesFeature.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ResourcesFeature.java index 2f11afd2fc5e..8725bdcd3333 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ResourcesFeature.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ResourcesFeature.java @@ -55,6 +55,8 @@ import java.util.regex.Pattern; import java.util.stream.Collectors; +import com.oracle.svm.hosted.dynamicaccessinference.DynamicAccessInferenceLog; +import com.oracle.svm.hosted.dynamicaccessinference.StrictDynamicAccessInferenceFeature; import org.graalvm.nativeimage.ImageSingletons; import org.graalvm.nativeimage.hosted.RuntimeResourceAccess; import org.graalvm.nativeimage.impl.ConfigurationCondition; @@ -174,6 +176,8 @@ private record CompiledConditionalPattern(ConfigurationCondition condition, Reso private int loadedConfigurations; private ImageClassLoader imageClassLoader; + private DynamicAccessInferenceLog inferenceLog; + private class ResourcesRegistryImpl extends ConditionalConfigurationRegistry implements ResourcesRegistry { private final ClassInitializationSupport classInitializationSupport = ClassInitializationSupport.singleton(); @@ -392,6 +396,7 @@ public void afterRegistration(AfterRegistrationAccess a) { ImageSingletons.add(RuntimeResourceSupport.class, resourcesRegistry); EmbeddedResourcesInfo embeddedResourcesInfo = new EmbeddedResourcesInfo(); ImageSingletons.add(EmbeddedResourcesInfo.class, embeddedResourcesInfo); + inferenceLog = ImageSingletons.contains(DynamicAccessInferenceLog.class) ? DynamicAccessInferenceLog.singleton() : null; } private static ResourcesRegistryImpl resourceRegistryImpl() { @@ -672,7 +677,7 @@ public void beforeCompilation(BeforeCompilationAccess access) { @Override public void registerInvocationPlugins(Providers providers, GraphBuilderConfiguration.Plugins plugins, ParsingReason reason) { - if (!reason.duringAnalysis() || reason == ParsingReason.JITCompilation) { + if (!reason.duringAnalysis() || reason == ParsingReason.JITCompilation || StrictDynamicAccessInferenceFeature.isEnforced()) { return; } @@ -712,6 +717,9 @@ public boolean apply(GraphBuilderContext b, ResolvedJavaMethod targetMethod, Rec throw VMError.shouldNotReachHere(e); } b.add(ReachabilityRegistrationNode.create(() -> RuntimeResourceAccess.addResource(clazz.getModule(), resourceName), reason)); + if (inferenceLog != null) { + inferenceLog.logRegistration(b, reason, targetMethod, clazz, new String[]{resource}); + } return true; } return false; diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/SVMHost.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/SVMHost.java index 3a5f6dd7371d..894f0a00ca9e 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/SVMHost.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/SVMHost.java @@ -47,6 +47,8 @@ import java.util.function.Function; import java.util.function.Predicate; +import com.oracle.svm.hosted.dynamicaccessinference.StrictDynamicAccessInferenceFeature; +import com.oracle.svm.hosted.dynamicaccessinference.StrictDynamicAccessInferenceSupport; import org.graalvm.nativeimage.AnnotationAccess; import org.graalvm.nativeimage.ImageSingletons; import org.graalvm.nativeimage.Platform; @@ -219,6 +221,8 @@ public enum UsageKind { private final boolean buildingImageLayer = ImageLayerBuildingSupport.buildingImageLayer(); private final LayeredStaticFieldSupport layeredStaticFieldSupport; + private final StrictDynamicAccessInferenceSupport strictDynamicAccessInferenceSupport; + @SuppressWarnings("this-escape") public SVMHost(OptionValues options, ImageClassLoader loader, ClassInitializationSupport classInitializationSupport, AnnotationSubstitutionProcessor annotationSubstitutions, MissingRegistrationSupport missingRegistrationSupport) { @@ -255,6 +259,8 @@ public SVMHost(OptionValues options, ImageClassLoader loader, ClassInitializatio enableTrackAcrossLayers = ImageLayerBuildingSupport.buildingSharedLayer(); enableReachableInCurrentLayer = ImageLayerBuildingSupport.buildingExtensionLayer(); layeredStaticFieldSupport = ImageLayerBuildingSupport.buildingImageLayer() ? LayeredStaticFieldSupport.singleton() : null; + + strictDynamicAccessInferenceSupport = StrictDynamicAccessInferenceFeature.isDisabled() ? null : StrictDynamicAccessInferenceSupport.singleton(); } /** @@ -1270,4 +1276,8 @@ public boolean allowConstantFolding(AnalysisMethod method) { public SimulateClassInitializerSupport createSimulateClassInitializerSupport(AnalysisMetaAccess aMetaAccess) { return new SimulateClassInitializerSupport(aMetaAccess, this); } + + public StrictDynamicAccessInferenceSupport getStrictDynamicAccessInferenceSupport() { + return strictDynamicAccessInferenceSupport; + } } diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/dataflow/AbstractFrame.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/dataflow/AbstractFrame.java new file mode 100644 index 000000000000..88a0e7fb14a8 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/dataflow/AbstractFrame.java @@ -0,0 +1,358 @@ +/* + * Copyright (c) 2025, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.hosted.dataflow; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Predicate; + +/** + * Abstract representation of a bytecode execution frame, i.e., its + * {@link AbstractFrame#operandStack operand stack} and {@link AbstractFrame#localVariableTable + * local variable table}. + * + * @param The abstract representation of values pushed and popped from the operand stack and + * stored in the local variable table. + */ +public class AbstractFrame { + + private final OperandStack operandStack; + private final LocalVariableTable localVariableTable; + + /** + * Get the operand stack of this abstract frame. + */ + public OperandStack operandStack() { + return operandStack; + } + + /** + * Get the local variable table of this abstract frame. + */ + public LocalVariableTable localVariableTable() { + return localVariableTable; + } + + /** + * Transform the chosen values in the abstract frame. This affects both the values on the + * operand stack and in the local variable table. + * + * @param filterFunction Values which satisfy this predicate are subject to transformation with + * {@code transformFunction}. + * @param transformFunction The transformation function. + */ + public void transform(Predicate filterFunction, Function transformFunction) { + operandStack.transform(filterFunction, transformFunction); + localVariableTable.transform(filterFunction, transformFunction); + } + + AbstractFrame() { + this.operandStack = new OperandStack<>(); + this.localVariableTable = new LocalVariableTable<>(); + } + + AbstractFrame(AbstractFrame state) { + this.operandStack = new OperandStack<>(state.operandStack); + this.localVariableTable = new LocalVariableTable<>(state.localVariableTable); + } + + void mergeWith(AbstractFrame other, BiFunction mergeFunction) { + operandStack.mergeWith(other.operandStack, mergeFunction); + localVariableTable.mergeWith(other.localVariableTable, mergeFunction); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + AbstractFrame that = (AbstractFrame) o; + return Objects.equals(operandStack, that.operandStack) && Objects.equals(localVariableTable, that.localVariableTable); + } + + @Override + public int hashCode() { + return Objects.hash(operandStack, localVariableTable); + } + + @Override + public String toString() { + return operandStack + System.lineSeparator() + localVariableTable; + } + + /** + * Abstract representation of a bytecode operand stack. + */ + public static final class OperandStack { + + /* + * An ArrayList is used in order to allow for efficient lookups at arbitrary stack + * positions, as well as to preserve memory in comparison to allocating a plain array with a + * max stack size. + */ + private final ArrayList> stack; + + /** + * Get a value at the specified depth of the operand stack. The {@code depth} does not take + * into account the size of values, i.e., a {@link ValueWithSlots} with size equal to + * {@link ValueWithSlots.Slots#TWO_SLOTS TWO_SLOTS} contributes only as one value to the + * depth of the operand stack. + */ + public T getOperand(int depth) { + return peek(depth).value; + } + + /** + * Get the number of values currently on the operand stack. This does not take into account + * the size of values, i.e., a {@link ValueWithSlots} with size equal to + * {@link ValueWithSlots.Slots#TWO_SLOTS TWO_SLOTS} contributes only as one value for this + * method. + */ + public int size() { + return stack.size(); + } + + /** + * Transform the chosen values on the operand stack. + * + * @param filterFunction Values which satisfy this predicate are subject to transformation + * with {@code transformFunction}. + * @param transformFunction The transformation function. + */ + public void transform(Predicate filterFunction, Function transformFunction) { + for (int i = 0; i < stack.size(); i++) { + ValueWithSlots value = stack.get(i); + if (filterFunction.test(value.value())) { + stack.set(i, new ValueWithSlots<>(transformFunction.apply(value.value()), value.size())); + } + } + } + + OperandStack() { + this.stack = new ArrayList<>(); + } + + OperandStack(OperandStack stack) { + this.stack = new ArrayList<>(stack.stack); + } + + void push(ValueWithSlots value) { + stack.add(value); + } + + ValueWithSlots pop() { + assert !stack.isEmpty() : "Cannot pop from empty stack"; + return stack.removeLast(); + } + + ValueWithSlots peek(int depth) { + assert 0 <= depth && depth < size() : "Operand stack doesn't contain enough values"; + return stack.get(stack.size() - depth - 1); + } + + ValueWithSlots peek() { + return peek(0); + } + + void clear() { + stack.clear(); + } + + void mergeWith(OperandStack other, BiFunction mergeFunction) { + assert size() == other.size() : "Operand stack size must match upon merging"; + for (int i = 0; i < stack.size(); i++) { + ValueWithSlots thisValue = stack.get(i); + ValueWithSlots thatValue = other.stack.get(i); + assert thisValue.size() == thatValue.size() : "The size of operand stack values must match upon merging"; + ValueWithSlots mergedValue = new ValueWithSlots<>(mergeFunction.apply(thisValue.value(), thatValue.value()), thisValue.size()); + stack.set(i, mergedValue); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + OperandStack that = (OperandStack) o; + return Objects.equals(stack, that.stack); + } + + @Override + public int hashCode() { + return Objects.hashCode(stack); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("Operand stack:\n"); + for (ValueWithSlots value : stack.reversed()) { + builder.append("[").append(value.value()).append("]").append(System.lineSeparator()); + } + return builder.toString(); + } + } + + /** + * Abstract representation of a bytecode local variable table. + */ + public static final class LocalVariableTable { + + private final Map> variables; + + /** + * Get the value at the {@code index} slot of the local variable table. The {@code index} + * must be valid, i.e., must be in the set which would be returned by + * {@link #getVariableIndices()}. + */ + public T getVariable(int index) { + return get(index).value; + } + + /** + * Get the local variable table indices of entries currently stored in it. + */ + public Set getVariableIndices() { + return variables.keySet(); + } + + /** + * Transform the chosen values in the local variable table. + * + * @param filterFunction Values which satisfy this predicate are subject to transformation + * with {@code transformFunction}. + * @param transformFunction The transformation function. + */ + public void transform(Predicate filterFunction, Function transformFunction) { + for (Map.Entry> entry : variables.entrySet()) { + ValueWithSlots value = entry.getValue(); + if (filterFunction.test(value.value())) { + entry.setValue(new ValueWithSlots<>(transformFunction.apply(value.value()), value.size())); + } + } + } + + LocalVariableTable() { + this.variables = new HashMap<>(); + } + + LocalVariableTable(LocalVariableTable localVariableTable) { + this.variables = new HashMap<>(localVariableTable.variables); + } + + void put(int index, ValueWithSlots value) { + assert index >= 0 : "Local variable table index cannot be negative"; + ValueWithSlots previousInTable = variables.get(index - 1); + if (previousInTable != null && previousInTable.size == ValueWithSlots.Slots.TWO_SLOTS) { + /* + * Store operations into a local variable slot occupied by the second half of a two + * slot value is a legal operation, but it invalidates the variable previously + * occupying two slots. + */ + variables.remove(index - 1); + } + variables.put(index, value); + } + + ValueWithSlots get(int index) { + assert variables.containsKey(index) : "Attempted to access non-existent variable in local variable table"; + return variables.get(index); + } + + void mergeWith(LocalVariableTable other, BiFunction mergeFunction) { + for (Map.Entry> entry : variables.entrySet()) { + ValueWithSlots thisValue = entry.getValue(); + ValueWithSlots thatValue = other.variables.get(entry.getKey()); + if (thatValue != null && thisValue.size() == thatValue.size()) { + /* + * We can always merge matching values from the local variable table. If the + * merging makes no sense (i.e., the stored variable types do not match), we + * still allow it, as the resulting value should not be used during execution + * anyway (or else the method would fail bytecode verification). + */ + ValueWithSlots mergedValue = new ValueWithSlots<>(mergeFunction.apply(thisValue.value(), thatValue.value()), thisValue.size()); + entry.setValue(mergedValue); + } + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + LocalVariableTable that = (LocalVariableTable) o; + return Objects.equals(variables, that.variables); + } + + @Override + public int hashCode() { + return Objects.hashCode(variables); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("Local variable table:\n"); + variables.entrySet().stream().sorted(Map.Entry.comparingByKey()).forEach( + e -> { + Integer varIndex = e.getKey(); + ValueWithSlots value = e.getValue(); + builder.append(varIndex).append(": ").append(value.value()).append(System.lineSeparator()); + }); + return builder.toString(); + } + } + + /** + * Wrapper which assigns a computational type category to a value, i.e., assigns the number of + * slots the value takes up in the local variable table or operand stack. + */ + record ValueWithSlots(T value, Slots size) { + public enum Slots { + ONE_SLOT, + TWO_SLOTS + } + + public static ValueWithSlots wrap(T value, Slots size) { + return new ValueWithSlots<>(value, size); + } + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/dataflow/AbstractInterpreter.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/dataflow/AbstractInterpreter.java new file mode 100644 index 000000000000..7c8d8bb7b861 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/dataflow/AbstractInterpreter.java @@ -0,0 +1,1362 @@ +/* + * Copyright (c) 2025, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.hosted.dataflow; + +import java.util.ArrayList; +import java.util.List; + +import jdk.graal.compiler.bytecode.Bytecode; +import jdk.graal.compiler.bytecode.BytecodeLookupSwitch; +import jdk.graal.compiler.bytecode.BytecodeStream; +import jdk.graal.compiler.bytecode.BytecodeSwitch; +import jdk.graal.compiler.bytecode.BytecodeTableSwitch; +import jdk.graal.compiler.nodes.spi.CoreProviders; +import jdk.vm.ci.meta.Constant; +import jdk.vm.ci.meta.ConstantPool; +import jdk.vm.ci.meta.JavaConstant; +import jdk.vm.ci.meta.JavaField; +import jdk.vm.ci.meta.JavaKind; +import jdk.vm.ci.meta.JavaMethod; +import jdk.vm.ci.meta.JavaType; +import jdk.vm.ci.meta.ResolvedJavaMethod; +import jdk.vm.ci.meta.Signature; + +import static com.oracle.svm.hosted.dataflow.AbstractFrame.ValueWithSlots.Slots.ONE_SLOT; +import static com.oracle.svm.hosted.dataflow.AbstractFrame.ValueWithSlots.Slots.TWO_SLOTS; +import static com.oracle.svm.hosted.dataflow.AbstractFrame.ValueWithSlots.wrap; + +import static jdk.graal.compiler.bytecode.Bytecodes.AALOAD; +import static jdk.graal.compiler.bytecode.Bytecodes.AASTORE; +import static jdk.graal.compiler.bytecode.Bytecodes.ACONST_NULL; +import static jdk.graal.compiler.bytecode.Bytecodes.ALOAD; +import static jdk.graal.compiler.bytecode.Bytecodes.ALOAD_0; +import static jdk.graal.compiler.bytecode.Bytecodes.ALOAD_1; +import static jdk.graal.compiler.bytecode.Bytecodes.ALOAD_2; +import static jdk.graal.compiler.bytecode.Bytecodes.ALOAD_3; +import static jdk.graal.compiler.bytecode.Bytecodes.ANEWARRAY; +import static jdk.graal.compiler.bytecode.Bytecodes.ARETURN; +import static jdk.graal.compiler.bytecode.Bytecodes.ARRAYLENGTH; +import static jdk.graal.compiler.bytecode.Bytecodes.ASTORE; +import static jdk.graal.compiler.bytecode.Bytecodes.ASTORE_0; +import static jdk.graal.compiler.bytecode.Bytecodes.ASTORE_1; +import static jdk.graal.compiler.bytecode.Bytecodes.ASTORE_2; +import static jdk.graal.compiler.bytecode.Bytecodes.ASTORE_3; +import static jdk.graal.compiler.bytecode.Bytecodes.ATHROW; +import static jdk.graal.compiler.bytecode.Bytecodes.BALOAD; +import static jdk.graal.compiler.bytecode.Bytecodes.BASTORE; +import static jdk.graal.compiler.bytecode.Bytecodes.BIPUSH; +import static jdk.graal.compiler.bytecode.Bytecodes.BREAKPOINT; +import static jdk.graal.compiler.bytecode.Bytecodes.CALOAD; +import static jdk.graal.compiler.bytecode.Bytecodes.CASTORE; +import static jdk.graal.compiler.bytecode.Bytecodes.CHECKCAST; +import static jdk.graal.compiler.bytecode.Bytecodes.D2F; +import static jdk.graal.compiler.bytecode.Bytecodes.D2I; +import static jdk.graal.compiler.bytecode.Bytecodes.D2L; +import static jdk.graal.compiler.bytecode.Bytecodes.DADD; +import static jdk.graal.compiler.bytecode.Bytecodes.DALOAD; +import static jdk.graal.compiler.bytecode.Bytecodes.DASTORE; +import static jdk.graal.compiler.bytecode.Bytecodes.DCMPG; +import static jdk.graal.compiler.bytecode.Bytecodes.DCMPL; +import static jdk.graal.compiler.bytecode.Bytecodes.DCONST_0; +import static jdk.graal.compiler.bytecode.Bytecodes.DCONST_1; +import static jdk.graal.compiler.bytecode.Bytecodes.DDIV; +import static jdk.graal.compiler.bytecode.Bytecodes.DLOAD; +import static jdk.graal.compiler.bytecode.Bytecodes.DLOAD_0; +import static jdk.graal.compiler.bytecode.Bytecodes.DLOAD_1; +import static jdk.graal.compiler.bytecode.Bytecodes.DLOAD_2; +import static jdk.graal.compiler.bytecode.Bytecodes.DLOAD_3; +import static jdk.graal.compiler.bytecode.Bytecodes.DMUL; +import static jdk.graal.compiler.bytecode.Bytecodes.DNEG; +import static jdk.graal.compiler.bytecode.Bytecodes.DREM; +import static jdk.graal.compiler.bytecode.Bytecodes.DRETURN; +import static jdk.graal.compiler.bytecode.Bytecodes.DSTORE; +import static jdk.graal.compiler.bytecode.Bytecodes.DSTORE_0; +import static jdk.graal.compiler.bytecode.Bytecodes.DSTORE_1; +import static jdk.graal.compiler.bytecode.Bytecodes.DSTORE_2; +import static jdk.graal.compiler.bytecode.Bytecodes.DSTORE_3; +import static jdk.graal.compiler.bytecode.Bytecodes.DSUB; +import static jdk.graal.compiler.bytecode.Bytecodes.DUP; +import static jdk.graal.compiler.bytecode.Bytecodes.DUP2; +import static jdk.graal.compiler.bytecode.Bytecodes.DUP2_X1; +import static jdk.graal.compiler.bytecode.Bytecodes.DUP2_X2; +import static jdk.graal.compiler.bytecode.Bytecodes.DUP_X1; +import static jdk.graal.compiler.bytecode.Bytecodes.DUP_X2; +import static jdk.graal.compiler.bytecode.Bytecodes.F2D; +import static jdk.graal.compiler.bytecode.Bytecodes.F2I; +import static jdk.graal.compiler.bytecode.Bytecodes.F2L; +import static jdk.graal.compiler.bytecode.Bytecodes.FADD; +import static jdk.graal.compiler.bytecode.Bytecodes.FALOAD; +import static jdk.graal.compiler.bytecode.Bytecodes.FASTORE; +import static jdk.graal.compiler.bytecode.Bytecodes.FCMPG; +import static jdk.graal.compiler.bytecode.Bytecodes.FCMPL; +import static jdk.graal.compiler.bytecode.Bytecodes.FCONST_0; +import static jdk.graal.compiler.bytecode.Bytecodes.FCONST_1; +import static jdk.graal.compiler.bytecode.Bytecodes.FCONST_2; +import static jdk.graal.compiler.bytecode.Bytecodes.FDIV; +import static jdk.graal.compiler.bytecode.Bytecodes.FLOAD; +import static jdk.graal.compiler.bytecode.Bytecodes.FLOAD_0; +import static jdk.graal.compiler.bytecode.Bytecodes.FLOAD_1; +import static jdk.graal.compiler.bytecode.Bytecodes.FLOAD_2; +import static jdk.graal.compiler.bytecode.Bytecodes.FLOAD_3; +import static jdk.graal.compiler.bytecode.Bytecodes.FMUL; +import static jdk.graal.compiler.bytecode.Bytecodes.FNEG; +import static jdk.graal.compiler.bytecode.Bytecodes.FREM; +import static jdk.graal.compiler.bytecode.Bytecodes.FRETURN; +import static jdk.graal.compiler.bytecode.Bytecodes.FSTORE; +import static jdk.graal.compiler.bytecode.Bytecodes.FSTORE_0; +import static jdk.graal.compiler.bytecode.Bytecodes.FSTORE_1; +import static jdk.graal.compiler.bytecode.Bytecodes.FSTORE_2; +import static jdk.graal.compiler.bytecode.Bytecodes.FSTORE_3; +import static jdk.graal.compiler.bytecode.Bytecodes.FSUB; +import static jdk.graal.compiler.bytecode.Bytecodes.GETFIELD; +import static jdk.graal.compiler.bytecode.Bytecodes.GETSTATIC; +import static jdk.graal.compiler.bytecode.Bytecodes.GOTO; +import static jdk.graal.compiler.bytecode.Bytecodes.GOTO_W; +import static jdk.graal.compiler.bytecode.Bytecodes.I2B; +import static jdk.graal.compiler.bytecode.Bytecodes.I2C; +import static jdk.graal.compiler.bytecode.Bytecodes.I2D; +import static jdk.graal.compiler.bytecode.Bytecodes.I2F; +import static jdk.graal.compiler.bytecode.Bytecodes.I2L; +import static jdk.graal.compiler.bytecode.Bytecodes.I2S; +import static jdk.graal.compiler.bytecode.Bytecodes.IADD; +import static jdk.graal.compiler.bytecode.Bytecodes.IALOAD; +import static jdk.graal.compiler.bytecode.Bytecodes.IAND; +import static jdk.graal.compiler.bytecode.Bytecodes.IASTORE; +import static jdk.graal.compiler.bytecode.Bytecodes.ICONST_0; +import static jdk.graal.compiler.bytecode.Bytecodes.ICONST_1; +import static jdk.graal.compiler.bytecode.Bytecodes.ICONST_2; +import static jdk.graal.compiler.bytecode.Bytecodes.ICONST_3; +import static jdk.graal.compiler.bytecode.Bytecodes.ICONST_4; +import static jdk.graal.compiler.bytecode.Bytecodes.ICONST_5; +import static jdk.graal.compiler.bytecode.Bytecodes.ICONST_M1; +import static jdk.graal.compiler.bytecode.Bytecodes.IDIV; +import static jdk.graal.compiler.bytecode.Bytecodes.IFEQ; +import static jdk.graal.compiler.bytecode.Bytecodes.IFGE; +import static jdk.graal.compiler.bytecode.Bytecodes.IFGT; +import static jdk.graal.compiler.bytecode.Bytecodes.IFLE; +import static jdk.graal.compiler.bytecode.Bytecodes.IFLT; +import static jdk.graal.compiler.bytecode.Bytecodes.IFNE; +import static jdk.graal.compiler.bytecode.Bytecodes.IFNONNULL; +import static jdk.graal.compiler.bytecode.Bytecodes.IFNULL; +import static jdk.graal.compiler.bytecode.Bytecodes.IF_ACMPEQ; +import static jdk.graal.compiler.bytecode.Bytecodes.IF_ACMPNE; +import static jdk.graal.compiler.bytecode.Bytecodes.IF_ICMPEQ; +import static jdk.graal.compiler.bytecode.Bytecodes.IF_ICMPGE; +import static jdk.graal.compiler.bytecode.Bytecodes.IF_ICMPGT; +import static jdk.graal.compiler.bytecode.Bytecodes.IF_ICMPLE; +import static jdk.graal.compiler.bytecode.Bytecodes.IF_ICMPLT; +import static jdk.graal.compiler.bytecode.Bytecodes.IF_ICMPNE; +import static jdk.graal.compiler.bytecode.Bytecodes.IINC; +import static jdk.graal.compiler.bytecode.Bytecodes.ILOAD; +import static jdk.graal.compiler.bytecode.Bytecodes.ILOAD_0; +import static jdk.graal.compiler.bytecode.Bytecodes.ILOAD_1; +import static jdk.graal.compiler.bytecode.Bytecodes.ILOAD_2; +import static jdk.graal.compiler.bytecode.Bytecodes.ILOAD_3; +import static jdk.graal.compiler.bytecode.Bytecodes.IMUL; +import static jdk.graal.compiler.bytecode.Bytecodes.INEG; +import static jdk.graal.compiler.bytecode.Bytecodes.INSTANCEOF; +import static jdk.graal.compiler.bytecode.Bytecodes.INVOKEDYNAMIC; +import static jdk.graal.compiler.bytecode.Bytecodes.INVOKEINTERFACE; +import static jdk.graal.compiler.bytecode.Bytecodes.INVOKESPECIAL; +import static jdk.graal.compiler.bytecode.Bytecodes.INVOKESTATIC; +import static jdk.graal.compiler.bytecode.Bytecodes.INVOKEVIRTUAL; +import static jdk.graal.compiler.bytecode.Bytecodes.IOR; +import static jdk.graal.compiler.bytecode.Bytecodes.IREM; +import static jdk.graal.compiler.bytecode.Bytecodes.IRETURN; +import static jdk.graal.compiler.bytecode.Bytecodes.ISHL; +import static jdk.graal.compiler.bytecode.Bytecodes.ISHR; +import static jdk.graal.compiler.bytecode.Bytecodes.ISTORE; +import static jdk.graal.compiler.bytecode.Bytecodes.ISTORE_0; +import static jdk.graal.compiler.bytecode.Bytecodes.ISTORE_1; +import static jdk.graal.compiler.bytecode.Bytecodes.ISTORE_2; +import static jdk.graal.compiler.bytecode.Bytecodes.ISTORE_3; +import static jdk.graal.compiler.bytecode.Bytecodes.ISUB; +import static jdk.graal.compiler.bytecode.Bytecodes.IUSHR; +import static jdk.graal.compiler.bytecode.Bytecodes.IXOR; +import static jdk.graal.compiler.bytecode.Bytecodes.JSR; +import static jdk.graal.compiler.bytecode.Bytecodes.JSR_W; +import static jdk.graal.compiler.bytecode.Bytecodes.L2D; +import static jdk.graal.compiler.bytecode.Bytecodes.L2F; +import static jdk.graal.compiler.bytecode.Bytecodes.L2I; +import static jdk.graal.compiler.bytecode.Bytecodes.LADD; +import static jdk.graal.compiler.bytecode.Bytecodes.LALOAD; +import static jdk.graal.compiler.bytecode.Bytecodes.LAND; +import static jdk.graal.compiler.bytecode.Bytecodes.LASTORE; +import static jdk.graal.compiler.bytecode.Bytecodes.LCMP; +import static jdk.graal.compiler.bytecode.Bytecodes.LCONST_0; +import static jdk.graal.compiler.bytecode.Bytecodes.LCONST_1; +import static jdk.graal.compiler.bytecode.Bytecodes.LDC; +import static jdk.graal.compiler.bytecode.Bytecodes.LDC2_W; +import static jdk.graal.compiler.bytecode.Bytecodes.LDC_W; +import static jdk.graal.compiler.bytecode.Bytecodes.LDIV; +import static jdk.graal.compiler.bytecode.Bytecodes.LLOAD; +import static jdk.graal.compiler.bytecode.Bytecodes.LLOAD_0; +import static jdk.graal.compiler.bytecode.Bytecodes.LLOAD_1; +import static jdk.graal.compiler.bytecode.Bytecodes.LLOAD_2; +import static jdk.graal.compiler.bytecode.Bytecodes.LLOAD_3; +import static jdk.graal.compiler.bytecode.Bytecodes.LMUL; +import static jdk.graal.compiler.bytecode.Bytecodes.LNEG; +import static jdk.graal.compiler.bytecode.Bytecodes.LOOKUPSWITCH; +import static jdk.graal.compiler.bytecode.Bytecodes.LOR; +import static jdk.graal.compiler.bytecode.Bytecodes.LREM; +import static jdk.graal.compiler.bytecode.Bytecodes.LRETURN; +import static jdk.graal.compiler.bytecode.Bytecodes.LSHL; +import static jdk.graal.compiler.bytecode.Bytecodes.LSHR; +import static jdk.graal.compiler.bytecode.Bytecodes.LSTORE; +import static jdk.graal.compiler.bytecode.Bytecodes.LSTORE_0; +import static jdk.graal.compiler.bytecode.Bytecodes.LSTORE_1; +import static jdk.graal.compiler.bytecode.Bytecodes.LSTORE_2; +import static jdk.graal.compiler.bytecode.Bytecodes.LSTORE_3; +import static jdk.graal.compiler.bytecode.Bytecodes.LSUB; +import static jdk.graal.compiler.bytecode.Bytecodes.LUSHR; +import static jdk.graal.compiler.bytecode.Bytecodes.LXOR; +import static jdk.graal.compiler.bytecode.Bytecodes.MONITORENTER; +import static jdk.graal.compiler.bytecode.Bytecodes.MONITOREXIT; +import static jdk.graal.compiler.bytecode.Bytecodes.MULTIANEWARRAY; +import static jdk.graal.compiler.bytecode.Bytecodes.NEW; +import static jdk.graal.compiler.bytecode.Bytecodes.NEWARRAY; +import static jdk.graal.compiler.bytecode.Bytecodes.NOP; +import static jdk.graal.compiler.bytecode.Bytecodes.POP; +import static jdk.graal.compiler.bytecode.Bytecodes.POP2; +import static jdk.graal.compiler.bytecode.Bytecodes.PUTFIELD; +import static jdk.graal.compiler.bytecode.Bytecodes.PUTSTATIC; +import static jdk.graal.compiler.bytecode.Bytecodes.RET; +import static jdk.graal.compiler.bytecode.Bytecodes.RETURN; +import static jdk.graal.compiler.bytecode.Bytecodes.SALOAD; +import static jdk.graal.compiler.bytecode.Bytecodes.SASTORE; +import static jdk.graal.compiler.bytecode.Bytecodes.SIPUSH; +import static jdk.graal.compiler.bytecode.Bytecodes.SWAP; +import static jdk.graal.compiler.bytecode.Bytecodes.TABLESWITCH; + +/** + * A {@link ForwardDataFlowAnalyzer} where the data flow state is represented by an abstract + * bytecode execution frame. This analyzer assumes that the provided bytecode is valid and verified + * by a bytecode verifier. + *

+ * The interpreter records {@link AbstractFrame abstract frames} for each instruction in the + * bytecode sequence of a method. Each abstract frame represents the abstract state before the + * would-be execution of the corresponding bytecode instruction. + *

+ * JSR and RET opcodes are currently unsupported, and a {@link DataFlowAnalysisException} will be + * thrown in case the analyzed method contains them. + * + * @param The abstract representation of values pushed and popped from the operand stack and + * stored in the local variable table. + */ +@SuppressWarnings("unused") +public abstract class AbstractInterpreter extends ForwardDataFlowAnalyzer> { + + private final CoreProviders providers; + + public AbstractInterpreter(CoreProviders providers) { + this.providers = providers; + } + + public CoreProviders getProviders() { + return providers; + } + + @Override + protected AbstractFrame createInitialState(ResolvedJavaMethod method) { + /* + * The initial state has an empty operand stack and local variable table slots containing + * values corresponding to the method arguments and receiver (if non-static). + */ + AbstractFrame state = new AbstractFrame<>(); + + int variableIndex = 0; + if (!method.isStatic()) { + /* The argument position of the receiver is set to -1. */ + state.localVariableTable().put(variableIndex, wrap(storeMethodArgument(method, -1, variableIndex), ONE_SLOT)); + variableIndex++; + } + + Signature signature = method.getSignature(); + int numParameters = signature.getParameterCount(false); + for (int parameterIndex = 0; parameterIndex < numParameters; parameterIndex++) { + JavaKind kind = signature.getParameterKind(parameterIndex); + AbstractFrame.ValueWithSlots value = wrap(storeMethodArgument(method, parameterIndex, variableIndex), getSizeForKind(kind)); + state.localVariableTable().put(variableIndex, value); + variableIndex += kind.needsTwoSlots() ? 2 : 1; + } + + return state; + } + + @Override + protected AbstractFrame createExceptionState(AbstractFrame inState, List exceptionTypes) { + /* + * The initial frame state in exception handlers is created by clearing the operand stack + * and placing the caught exception object on it. + */ + AbstractFrame exceptionPathState = new AbstractFrame<>(inState); + exceptionPathState.operandStack().clear(); + + AbstractFrame.ValueWithSlots exceptionObject = wrap(pushExceptionObject(exceptionTypes), ONE_SLOT); + exceptionPathState.operandStack().push(exceptionObject); + return exceptionPathState; + } + + @Override + protected AbstractFrame copyState(AbstractFrame state) { + return new AbstractFrame<>(state); + } + + @Override + protected AbstractFrame mergeStates(AbstractFrame left, AbstractFrame right) { + AbstractFrame merged = copyState(left); + merged.mergeWith(right, this::merge); + return merged; + } + + @Override + @SuppressWarnings("DuplicateBranchesInSwitch") + protected AbstractFrame processInstruction(AbstractFrame inState, BytecodeStream stream, Bytecode code) { + AbstractFrame outState = copyState(inState); + + int bci = stream.currentBCI(); + int opcode = stream.currentBC(); + Context context = Context.create(code.getMethod(), bci, opcode); + + // @formatter:off + // Checkstyle: stop + switch (opcode) { + case NOP : break; + case ACONST_NULL : handleConstant(context, outState, JavaConstant.NULL_POINTER, ONE_SLOT); break; + case ICONST_M1 : handleConstant(context, outState, JavaConstant.forInt(-1), ONE_SLOT); break; + case ICONST_0 : // fall through + case ICONST_1 : // fall through + case ICONST_2 : // fall through + case ICONST_3 : // fall through + case ICONST_4 : // fall through + case ICONST_5 : handleConstant(context, outState, JavaConstant.forInt(opcode - ICONST_0), ONE_SLOT); break; + case LCONST_0 : // fall through + case LCONST_1 : handleConstant(context, outState, JavaConstant.forLong(opcode - LCONST_0), TWO_SLOTS); break; + case FCONST_0 : // fall through + case FCONST_1 : // fall through + case FCONST_2 : handleConstant(context, outState, JavaConstant.forFloat(opcode - FCONST_0), ONE_SLOT); break; + case DCONST_0 : // fall through + case DCONST_1 : handleConstant(context, outState, JavaConstant.forDouble(opcode - DCONST_0), TWO_SLOTS); break; + case BIPUSH : handleConstant(context, outState, JavaConstant.forByte(stream.readByte()), ONE_SLOT); break; + case SIPUSH : handleConstant(context, outState, JavaConstant.forShort(stream.readShort()), ONE_SLOT); break; + case LDC : // fall through + case LDC_W : handleConstant(context, outState, getConstant(code, stream), ONE_SLOT); break; + case LDC2_W : handleConstant(context, outState, getConstant(code, stream), TWO_SLOTS); break; + case ILOAD : // fall through + case LLOAD : // fall through + case FLOAD : // fall through + case DLOAD : // fall through + case ALOAD : handleVariableLoad(context, outState, stream.readLocalIndex()); break; + case ILOAD_0 : // fall through + case ILOAD_1 : // fall through + case ILOAD_2 : // fall through + case ILOAD_3 : handleVariableLoad(context, outState, opcode - ILOAD_0); break; + case LLOAD_0 : // fall through + case LLOAD_1 : // fall through + case LLOAD_2 : // fall through + case LLOAD_3 : handleVariableLoad(context, outState, opcode - LLOAD_0); break; + case FLOAD_0 : // fall through + case FLOAD_1 : // fall through + case FLOAD_2 : // fall through + case FLOAD_3 : handleVariableLoad(context, outState, opcode - FLOAD_0); break; + case DLOAD_0 : // fall through + case DLOAD_1 : // fall through + case DLOAD_2 : // fall through + case DLOAD_3 : handleVariableLoad(context, outState, opcode - DLOAD_0); break; + case ALOAD_0 : // fall through + case ALOAD_1 : // fall through + case ALOAD_2 : // fall through + case ALOAD_3 : handleVariableLoad(context, outState, opcode - ALOAD_0); break; + case IALOAD : handleArrayElementLoad(context, outState, ONE_SLOT); break; + case LALOAD : handleArrayElementLoad(context, outState, TWO_SLOTS); break; + case FALOAD : handleArrayElementLoad(context, outState, ONE_SLOT); break; + case DALOAD : handleArrayElementLoad(context, outState, TWO_SLOTS); break; + case AALOAD : // fall through + case BALOAD : // fall through + case CALOAD : // fall through + case SALOAD : handleArrayElementLoad(context, outState, ONE_SLOT); break; + case ISTORE : // fall through + case LSTORE : // fall through + case FSTORE : // fall through + case DSTORE : // fall through + case ASTORE : handleVariableStore(context, outState, stream.readLocalIndex()); break; + case ISTORE_0 : // fall through + case ISTORE_1 : // fall through + case ISTORE_2 : // fall through + case ISTORE_3 : handleVariableStore(context, outState, opcode - ISTORE_0); break; + case LSTORE_0 : // fall through + case LSTORE_1 : // fall through + case LSTORE_2 : // fall through + case LSTORE_3 : handleVariableStore(context, outState, opcode - LSTORE_0); break; + case FSTORE_0 : // fall through + case FSTORE_1 : // fall through + case FSTORE_2 : // fall through + case FSTORE_3 : handleVariableStore(context, outState, opcode - FSTORE_0); break; + case DSTORE_0 : // fall through + case DSTORE_1 : // fall through + case DSTORE_2 : // fall through + case DSTORE_3 : handleVariableStore(context, outState, opcode - DSTORE_0); break; + case ASTORE_0 : // fall through + case ASTORE_1 : // fall through + case ASTORE_2 : // fall through + case ASTORE_3 : handleVariableStore(context, outState, opcode - ASTORE_0); break; + case IASTORE : // fall through + case LASTORE : // fall through + case FASTORE : // fall through + case DASTORE : // fall through + case AASTORE : // fall through + case BASTORE : // fall through + case CASTORE : // fall through + case SASTORE : handleArrayElementStore(context, outState); break; + case POP : handlePop(outState); break; + case POP2 : handlePop2(outState); break; + case DUP : handleDup(outState); break; + case DUP_X1 : handleDupX1(outState); break; + case DUP_X2 : handleDupX2(outState); break; + case DUP2 : handleDup2(outState); break; + case DUP2_X1 : handleDup2X1(outState); break; + case DUP2_X2 : handleDup2X2(outState); break; + case SWAP : handleSwap(outState); break; + case IADD : // fall through + case ISUB : // fall through + case IMUL : // fall through + case IDIV : // fall through + case IREM : // fall through + case LADD : // fall through + case LSUB : // fall through + case LMUL : // fall through + case LDIV : // fall through + case LREM : // fall through + case FADD : // fall through + case FSUB : // fall through + case FMUL : // fall through + case FDIV : // fall through + case FREM : // fall through + case DADD : // fall through + case DSUB : // fall through + case DMUL : // fall through + case DDIV : // fall through + case DREM : handleBinaryOperation(context, outState); break; + case INEG : // fall through + case LNEG : // fall through + case FNEG : // fall through + case DNEG : handleUnaryOperation(context, outState); break; + case ISHL : // fall through + case ISHR : // fall through + case IUSHR : // fall through + case IAND : // fall through + case IOR : // fall through + case IXOR : // fall through + case LSHL : // fall through + case LSHR : // fall through + case LUSHR : // fall through + case LAND : // fall through + case LOR : // fall through + case LXOR : handleBinaryOperation(context, outState); break; + case IINC : handleIncrement(context, outState, stream.readLocalIndex(), stream.readIncrement()); break; + case I2F : handleCast(context, outState, ONE_SLOT); break; + case I2D : handleCast(context, outState, TWO_SLOTS); break; + case L2F : handleCast(context, outState, ONE_SLOT); break; + case L2D : handleCast(context, outState, TWO_SLOTS); break; + case F2I : handleCast(context, outState, ONE_SLOT); break; + case F2L : // fall through + case F2D : handleCast(context, outState, TWO_SLOTS); break; + case D2I : handleCast(context, outState, ONE_SLOT); break; + case D2L : handleCast(context, outState, TWO_SLOTS); break; + case D2F : handleCast(context, outState, ONE_SLOT); break; + case L2I : handleCast(context, outState, ONE_SLOT); break; + case I2L : handleCast(context, outState, TWO_SLOTS); break; + case I2B : // fall through + case I2S : // fall through + case I2C : handleCast(context, outState, ONE_SLOT); break; + case LCMP : // fall through + case FCMPL : // fall through + case FCMPG : // fall through + case DCMPL : // fall through + case DCMPG : handleCompare(context, outState); break; + case IFEQ : // fall through + case IFNE : // fall through + case IFLT : // fall through + case IFGE : // fall through + case IFGT : // fall through + case IFLE : handleUnaryConditionalJump(context, outState, stream.readBranchDest(), stream.nextBCI()); break; + case IF_ICMPEQ : // fall through + case IF_ICMPNE : // fall through + case IF_ICMPLT : // fall through + case IF_ICMPGE : // fall through + case IF_ICMPGT : // fall through + case IF_ICMPLE : // fall through + case IF_ACMPEQ : // fall through + case IF_ACMPNE : handleBinaryConditionalJump(context, outState, stream.readBranchDest(), stream.nextBCI()); break; + case GOTO : unconditionalJump(context, outState, stream.readBranchDest()); break; + case JSR : // fall through + case RET : throw new DataFlowAnalysisException("Unsupported opcode " + opcode); + case TABLESWITCH : handleSwitch(context, outState, new BytecodeTableSwitch(stream, bci)); break; + case LOOKUPSWITCH : handleSwitch(context, outState, new BytecodeLookupSwitch(stream, bci)); break; + case IRETURN : // fall through + case LRETURN : // fall through + case FRETURN : // fall through + case DRETURN : // fall through + case ARETURN : returnValue(context, outState, outState.operandStack().pop().value()); break; + case RETURN : returnVoid(context, outState); break; + case GETSTATIC : handleStaticFieldLoad(context, outState, getJavaField(code, stream)); break; + case PUTSTATIC : handleStaticFieldStore(context, outState, getJavaField(code, stream)); break; + case GETFIELD : handleFieldLoad(context, outState, getJavaField(code, stream)); break; + case PUTFIELD : handleFieldStore(context, outState, getJavaField(code, stream)); break; + case INVOKEVIRTUAL : handleInvoke(context, outState, getJavaMethod(code, stream), getAppendix(code, stream)); break; + case INVOKESPECIAL : // fall through + case INVOKESTATIC : // fall through + case INVOKEINTERFACE: handleInvoke(context, outState, getJavaMethod(code, stream), null); break; + case INVOKEDYNAMIC : handleInvoke(context, outState, getJavaMethod(code, stream), getAppendix(code, stream)); break; + case NEW : handleNew(context, outState, getJavaType(code, stream)); break; + case NEWARRAY : handleNewArray(context, outState, getJavaTypeFromPrimitiveArrayCode(stream.readLocalIndex()), 1); break; + case ANEWARRAY : handleNewArray(context, outState, getJavaType(code, stream), 1); break; + case ARRAYLENGTH : handleArrayLength(context, outState); break; + case ATHROW : doThrow(context, outState, outState.operandStack().pop().value()); break; + case CHECKCAST : // fall through + case INSTANCEOF : handleCastCheck(context, outState, getJavaType(code, stream)); break; + case MONITORENTER : // fall through + case MONITOREXIT : monitorOperation(context, outState, outState.operandStack().pop().value()); break; + case MULTIANEWARRAY : handleNewArray(context, outState, getJavaType(code, stream), stream.readUByte(bci + 3)); break; + case IFNULL : // fall through + case IFNONNULL : handleUnaryConditionalJump(context, outState, stream.readBranchDest(), stream.nextBCI()); break; + case GOTO_W : unconditionalJump(context, outState, stream.readBranchDest()); break; + case JSR_W : // fall through + case BREAKPOINT : // fall through + default : throw new DataFlowAnalysisException("Unsupported opcode " + opcode); + } + // @formatter:on + // Checkstyle: resume + + return outState; + } + + protected Object lookupConstant(ConstantPool constantPool, int cpi, int opcode) { + tryToResolve(constantPool, cpi, opcode); + return constantPool.lookupConstant(cpi, false); + } + + protected JavaType lookupType(ConstantPool constantPool, int cpi, int opcode) { + tryToResolve(constantPool, cpi, opcode); + return constantPool.lookupType(cpi, opcode); + } + + protected JavaField lookupField(ConstantPool constantPool, ResolvedJavaMethod method, int cpi, int opcode) { + tryToResolve(constantPool, cpi, opcode); + return constantPool.lookupField(cpi, method, opcode); + } + + protected JavaMethod lookupMethod(ConstantPool constantPool, ResolvedJavaMethod method, int cpi, int opcode) { + tryToResolve(constantPool, cpi, opcode); + return constantPool.lookupMethod(cpi, opcode, method); + } + + protected JavaConstant lookupAppendix(ConstantPool constantPool, int cpi, int opcode) { + tryToResolve(constantPool, cpi, opcode); + return constantPool.lookupAppendix(cpi, opcode); + } + + protected static void tryToResolve(ConstantPool constantPool, int cpi, int opcode) { + try { + constantPool.loadReferencedType(cpi, opcode, false); + } catch (Throwable t) { + // Ignore and leave the type unresolved. + } + } + + private Object getConstant(Bytecode code, BytecodeStream stream) { + return lookupConstant(code.getConstantPool(), stream.readCPI(), stream.currentBC()); + } + + private JavaType getJavaType(Bytecode code, BytecodeStream stream) { + return lookupType(code.getConstantPool(), stream.readCPI(), stream.currentBC()); + } + + private JavaField getJavaField(Bytecode code, BytecodeStream stream) { + return lookupField(code.getConstantPool(), code.getMethod(), stream.readCPI(), stream.currentBC()); + } + + private JavaMethod getJavaMethod(Bytecode code, BytecodeStream stream) { + int opcode = stream.currentBC(); + int cpi = opcode == INVOKEDYNAMIC ? stream.readCPI4() : stream.readCPI(); + return lookupMethod(code.getConstantPool(), code.getMethod(), cpi, opcode); + } + + private JavaConstant getAppendix(Bytecode code, BytecodeStream stream) { + int opcode = stream.currentBC(); + int cpi = opcode == INVOKEDYNAMIC ? stream.readCPI4() : stream.readCPI(); + return lookupAppendix(code.getConstantPool(), cpi, opcode); + } + + private void handleConstant(Context context, AbstractFrame state, Object value, AbstractFrame.ValueWithSlots.Slots size) { + if (value == null) { + /* + * The constant is an unresolved JVM_CONSTANT_Dynamic, JVM_CONSTANT_MethodHandle or + * JVM_CONSTANT_MethodType. + */ + state.operandStack().push(wrap(pushConstant(context, state, null), size)); + } else { + if (value instanceof Constant constant) { + state.operandStack().push(wrap(pushConstant(context, state, constant), size)); + } else if (value instanceof JavaType type) { + state.operandStack().push(wrap(pushType(context, state, type), size)); + } + } + } + + private void handleVariableLoad(Context context, AbstractFrame state, int variableIndex) { + AbstractFrame.ValueWithSlots value = state.localVariableTable().get(variableIndex); + state.operandStack().push(wrap(loadVariable(context, state, variableIndex, value.value()), value.size())); + } + + private void handleArrayElementLoad(Context context, AbstractFrame state, AbstractFrame.ValueWithSlots.Slots size) { + T index = state.operandStack().pop().value(); + T array = state.operandStack().pop().value(); + state.operandStack().push(wrap(loadArrayElement(context, state, array, index), size)); + } + + private void handleVariableStore(Context context, AbstractFrame state, int variableIndex) { + AbstractFrame.ValueWithSlots value = state.operandStack().pop(); + state.localVariableTable().put(variableIndex, wrap(storeVariable(context, state, variableIndex, value.value()), value.size())); + } + + private void handleArrayElementStore(Context context, AbstractFrame state) { + T value = state.operandStack().pop().value(); + T index = state.operandStack().pop().value(); + T array = state.operandStack().pop().value(); + storeArrayElement(context, state, array, index, value); + } + + private void handlePop(AbstractFrame state) { + state.operandStack().pop(); + } + + private void handlePop2(AbstractFrame state) { + AbstractFrame.ValueWithSlots value = state.operandStack().pop(); + if (value.size() == ONE_SLOT) { + state.operandStack().pop(); + } + } + + private void handleDup(AbstractFrame state) { + state.operandStack().push(state.operandStack().peek()); + } + + private void handleDupX1(AbstractFrame state) { + AbstractFrame.ValueWithSlots first = state.operandStack().pop(); + AbstractFrame.ValueWithSlots second = state.operandStack().pop(); + state.operandStack().push(first); + state.operandStack().push(second); + state.operandStack().push(first); + } + + @SuppressWarnings("Duplicates") + private void handleDupX2(AbstractFrame state) { + AbstractFrame.ValueWithSlots first = state.operandStack().pop(); + AbstractFrame.ValueWithSlots second = state.operandStack().pop(); + if (second.size() == ONE_SLOT) { + AbstractFrame.ValueWithSlots third = state.operandStack().pop(); + state.operandStack().push(first); + state.operandStack().push(third); + } else { + state.operandStack().push(first); + } + state.operandStack().push(second); + state.operandStack().push(first); + } + + private void handleDup2(AbstractFrame state) { + AbstractFrame.ValueWithSlots first = state.operandStack().pop(); + if (first.size() == ONE_SLOT) { + AbstractFrame.ValueWithSlots second = state.operandStack().peek(); + state.operandStack().push(first); + state.operandStack().push(second); + state.operandStack().push(first); + } else { + state.operandStack().push(first); + state.operandStack().push(first); + } + } + + private void handleDup2X1(AbstractFrame state) { + AbstractFrame.ValueWithSlots first = state.operandStack().pop(); + AbstractFrame.ValueWithSlots second = state.operandStack().pop(); + if (first.size() == ONE_SLOT) { + AbstractFrame.ValueWithSlots third = state.operandStack().pop(); + state.operandStack().push(second); + state.operandStack().push(first); + state.operandStack().push(third); + } else { + state.operandStack().push(first); + } + state.operandStack().push(second); + state.operandStack().push(first); + } + + @SuppressWarnings("Duplicates") + private void handleDup2X2(AbstractFrame state) { + AbstractFrame.ValueWithSlots first = state.operandStack().pop(); + AbstractFrame.ValueWithSlots second = state.operandStack().pop(); + if (first.size() == ONE_SLOT) { + AbstractFrame.ValueWithSlots third = state.operandStack().pop(); + if (third.size() == ONE_SLOT) { + AbstractFrame.ValueWithSlots fourth = state.operandStack().pop(); + state.operandStack().push(second); + state.operandStack().push(first); + state.operandStack().push(fourth); + } else { + state.operandStack().push(second); + state.operandStack().push(first); + } + state.operandStack().push(third); + } else { + if (second.size() == ONE_SLOT) { + AbstractFrame.ValueWithSlots third = state.operandStack().pop(); + state.operandStack().push(first); + state.operandStack().push(third); + } else { + state.operandStack().push(first); + } + } + state.operandStack().push(second); + state.operandStack().push(first); + } + + private void handleSwap(AbstractFrame state) { + AbstractFrame.ValueWithSlots first = state.operandStack().pop(); + AbstractFrame.ValueWithSlots second = state.operandStack().pop(); + state.operandStack().push(first); + state.operandStack().push(second); + } + + private void handleBinaryOperation(Context context, AbstractFrame state) { + AbstractFrame.ValueWithSlots right = state.operandStack().pop(); + AbstractFrame.ValueWithSlots left = state.operandStack().pop(); + state.operandStack().push(wrap(binaryOperation(context, state, left.value(), right.value()), left.size())); + } + + private void handleUnaryOperation(Context context, AbstractFrame state) { + AbstractFrame.ValueWithSlots value = state.operandStack().pop(); + state.operandStack().push(wrap(unaryOperation(context, state, value.value()), value.size())); + } + + private void handleIncrement(Context context, AbstractFrame state, int variableIndex, int incrementBy) { + AbstractFrame.ValueWithSlots value = state.localVariableTable().get(variableIndex); + state.localVariableTable().put(variableIndex, wrap(incrementVariable(context, state, variableIndex, incrementBy, value.value()), value.size())); + } + + private void handleCast(Context context, AbstractFrame state, AbstractFrame.ValueWithSlots.Slots size) { + T value = state.operandStack().pop().value(); + state.operandStack().push(wrap(castOperation(context, state, value), size)); + } + + private void handleCompare(Context context, AbstractFrame state) { + T right = state.operandStack().pop().value(); + T left = state.operandStack().pop().value(); + state.operandStack().push(wrap(comparisonOperation(context, state, left, right), ONE_SLOT)); + } + + private void handleUnaryConditionalJump(Context context, AbstractFrame state, int ifDestinationBci, int elseDestinationBci) { + T value = state.operandStack().pop().value(); + unaryConditionalJump(context, state, ifDestinationBci, elseDestinationBci, value); + } + + private void handleBinaryConditionalJump(Context context, AbstractFrame state, int ifDestinationBci, int elseDestinationBci) { + T right = state.operandStack().pop().value(); + T left = state.operandStack().pop().value(); + binaryConditionalJump(context, state, ifDestinationBci, elseDestinationBci, left, right); + } + + private void handleSwitch(Context context, AbstractFrame state, BytecodeSwitch bcSwitch) { + T value = state.operandStack().pop().value(); + switchJump(context, state, bcSwitch, value); + } + + private void handleStaticFieldLoad(Context context, AbstractFrame state, JavaField field) { + JavaKind fieldKind = field.getJavaKind(); + AbstractFrame.ValueWithSlots.Slots size = getSizeForKind(fieldKind); + state.operandStack().push(wrap(loadStaticField(context, state, field), size)); + } + + private void handleStaticFieldStore(Context context, AbstractFrame state, JavaField field) { + T value = state.operandStack().pop().value(); + storeStaticField(context, state, field, value); + } + + private void handleFieldLoad(Context context, AbstractFrame state, JavaField field) { + JavaKind fieldKind = field.getJavaKind(); + AbstractFrame.ValueWithSlots.Slots size = getSizeForKind(fieldKind); + T object = state.operandStack().pop().value(); + state.operandStack().push(wrap(loadField(context, state, field, object), size)); + } + + private void handleFieldStore(Context context, AbstractFrame state, JavaField field) { + T value = state.operandStack().pop().value(); + T object = state.operandStack().pop().value(); + storeField(context, state, field, object, value); + } + + private void handleInvoke(Context context, AbstractFrame state, JavaMethod method, JavaConstant appendix) { + if (appendix != null) { + state.operandStack().push(wrap(pushAppendix(method, appendix), ONE_SLOT)); + } + + /* + * HotSpot can rewrite some (method handle related) invocations, which can potentially lead + * to an INVOKEVIRTUAL instruction actually invoking a static method. This means that we + * cannot rely on the opcode to determine if the call has a receiver. + * + * https://wiki.openjdk.org/display/HotSpot/Method+handles+and+invokedynamic + */ + boolean hasReceiver; + if (context.opcode == INVOKEVIRTUAL && method instanceof ResolvedJavaMethod resolved) { + hasReceiver = resolved.hasReceiver(); + } else { + hasReceiver = context.opcode != INVOKESTATIC && context.opcode != INVOKEDYNAMIC; + } + + Signature signature = method.getSignature(); + + int operandCount = signature.getParameterCount(hasReceiver); + List operands = new ArrayList<>(operandCount); + for (int i = 0; i < operandCount; i++) { + operands.add(state.operandStack().pop().value()); + } + operands = operands.reversed(); + + JavaKind returnKind = signature.getReturnKind(); + if (returnKind.equals(JavaKind.Void)) { + invokeVoidMethod(context, state, method, operands); + } else { + AbstractFrame.ValueWithSlots.Slots size = getSizeForKind(returnKind); + state.operandStack().push(wrap(invokeMethod(context, state, method, operands), size)); + } + } + + private void handleNew(Context context, AbstractFrame state, JavaType type) { + state.operandStack().push(wrap(newObject(context, state, type), ONE_SLOT)); + } + + private void handleNewArray(Context context, AbstractFrame state, JavaType type, int dimensions) { + List counts = new ArrayList<>(dimensions); + for (int i = 0; i < dimensions; i++) { + counts.add(state.operandStack().pop().value()); + } + counts = counts.reversed(); + state.operandStack().push(wrap(newArray(context, state, type, counts), ONE_SLOT)); + } + + /** + * Type codes as defined by the JVM + * specification. + */ + private enum PrimitiveTypeArrayCode { + T_BOOLEAN(boolean.class), + T_CHAR(char.class), + T_FLOAT(float.class), + T_DOUBLE(double.class), + T_BYTE(byte.class), + T_SHORT(short.class), + T_INT(int.class), + T_LONG(long.class); + + private static final int TYPE_CODE_OFFSET = 4; + private final Class typeClass; + + PrimitiveTypeArrayCode(Class typeClass) { + this.typeClass = typeClass; + } + + static Class getType(int typeCode) { + int typeIndex = typeCode - TYPE_CODE_OFFSET; + assert typeIndex >= 0 && typeIndex < values().length : "Unexpected primitive type code: " + typeCode; + return values()[typeIndex].typeClass; + } + } + + private JavaType getJavaTypeFromPrimitiveArrayCode(int typeCode) { + return providers.getMetaAccess().lookupJavaType(PrimitiveTypeArrayCode.getType(typeCode)); + } + + private void handleArrayLength(Context context, AbstractFrame state) { + T array = state.operandStack().pop().value(); + state.operandStack().push(wrap(arrayLength(context, state, array), ONE_SLOT)); + } + + private void handleCastCheck(Context context, AbstractFrame state, JavaType type) { + T object = state.operandStack().pop().value(); + state.operandStack().push(wrap(castCheckOperation(context, state, type, object), ONE_SLOT)); + } + + private static AbstractFrame.ValueWithSlots.Slots getSizeForKind(JavaKind kind) { + return kind.needsTwoSlots() ? TWO_SLOTS : ONE_SLOT; + } + + /** + * Represents the execution context for an instruction. + */ + protected record Context(ResolvedJavaMethod method, int bci, int opcode) { + + public static Context create(ResolvedJavaMethod method, int bci, int opcode) { + return new Context(method, bci, opcode); + } + } + + /** + * @return The default abstract value. This value usually represents an over-saturated value + * from which no useful information can be inferred. + */ + protected abstract T defaultValue(); + + /** + * Merge two matching operand stack or local variable table values from divergent control flow + * paths. + * + * @return The merged value. + */ + protected abstract T merge(T left, T right); + + /** + * Put a variable corresponding to the analyzed method's arguments in the local variable table. + * This happens when constructing the initial abstract execution frame at the method's entry + * point. + * + * @param method The method being analyzed. + * @param argumentIndex The index of the argument being stored. If the method is non-static, the + * argument index for the receiver is set to -1. + * @param variableIndex The index of the local variable table entry the value is being stored + * to. + * @return The value to store in the local variable table. + */ + protected T storeMethodArgument(ResolvedJavaMethod method, int argumentIndex, int variableIndex) { + return defaultValue(); + } + + /** + * Push an exception object on the operand stack upon entering an exception handler. + * + * @param exceptionTypes The possible types of the exception object. + * @return The value representing the exception object pushed on the operand stack. + */ + protected T pushExceptionObject(List exceptionTypes) { + return defaultValue(); + } + + /** + * Create a constant value to be pushed on the operand stack. This handler is called for the + * ACONST_NULL, ICONST_M1, ICONST_0, ..., ICONST_5, LCONST_0, LCONST_1, FCONST_0, ..., FCONST_2, + * DCONST_0, DCONST_1, BIPUSH, SIPUSH, LDC, LDC_W and LDC2_W instructions. + * + * @param context The execution context for this handler. + * @param state The abstract frame being modified by this instruction. + * @param constant The constant being pushed onto the operand stack. + * @return The abstract value to be pushed on the operand stack. + */ + protected T pushConstant(Context context, AbstractFrame state, Constant constant) { + return defaultValue(); + } + + /** + * Create a constant type reference to be pushed on the operand stack. This handler is called + * for the LDC and LDC_W instructions. + * + * @param context The execution context for this handler. + * @param state The abstract frame being modified by this instruction. + * @param type The type being pushed onto the operand stack. + * @return The abstract value to be pushed on the operand stack. + */ + protected T pushType(Context context, AbstractFrame state, JavaType type) { + return defaultValue(); + } + + /** + * Load a variable from the local variable table and push its value on the operand stack. This + * handler is called for the various LOAD instructions. + * + * @param context The execution context for this handler. + * @param state The abstract frame being modified by this instruction. + * @param variableIndex The local variable table index of the variable being accessed. + * @param value The abstract value currently stored in the accessed local variable. + * @return The abstract value to be pushed on the operand stack. + */ + protected T loadVariable(Context context, AbstractFrame state, int variableIndex, T value) { + return value; + } + + /** + * Push an element value stored in the target array on the operand stack. This handler is called + * for the IALOAD, LALOAD, FALOAD, DALOAD, AALOAD, BALOAD, CALOAD and SALOAD instructions. + * + * @param context The execution context for this handler. + * @param state The abstract frame being modified by this instruction. + * @param array The abstract value representing the accessed array. + * @param index The abstract value representing the array index. + * @return The abstract value representing the loaded element to be pushed on the operand stack. + */ + protected T loadArrayElement(Context context, AbstractFrame state, T array, T index) { + return defaultValue(); + } + + /** + * Store a value in the local variable table. This handler is called for the various STORE + * instructions. + * + * @param context The execution context for this handler. + * @param state The abstract frame being modified by this instruction. + * @param variableIndex The local variable table index of the variable being accessed. + * @param value The abstract value from the operand stack. + * @return The abstract value to be stored in the local variable table. + */ + protected T storeVariable(Context context, AbstractFrame state, int variableIndex, T value) { + return value; + } + + /** + * Store a value in an array. This handler is called for the IASTORE, LASTORE, FASTORE, DASTORE, + * AASTORE, BASTORE, CASTORE and SASTORE instructions. + * + * @param context The execution context for this handler. + * @param state The abstract frame being modified by this instruction. + * @param array The abstract value representing the accessed array. + * @param index The abstract value representing the array index. + * @param value The abstract value to be stored in the array. + */ + protected void storeArrayElement(Context context, AbstractFrame state, T array, T index, T value) { + + } + + /** + * Handler for various binary operations. It is invoked for the ADD, SUB, MUL, DIV, REM, SHL, + * SHR, USHR, AND, OR and XOR instructions. + * + * @param context The execution context for this handler. + * @param state The abstract frame being modified by this instruction. + * @param left The abstract value representing the left operand of the instruction. + * @param right The abstract value representing the right operand of the instruction. + * @return The abstract value representing the result of the binary operation. + */ + protected T binaryOperation(Context context, AbstractFrame state, T left, T right) { + return defaultValue(); + } + + /** + * Handler for unary negation operations. It is invoked for the INEG, LNEG, FNEG and DNEG + * instructions. + * + * @param context The execution context for this handler. + * @param state The abstract frame being modified by this instruction. + * @param value The abstract value representing the operand of the instruction. + * @return The abstract value representing the result of the unary operation. + */ + protected T unaryOperation(Context context, AbstractFrame state, T value) { + return defaultValue(); + } + + /** + * Increment an integral variable in the local variable table. THis handler is called for the + * IINC instruction. + * + * @param context The execution context for this handler. + * @param state The abstract frame being modified by this instruction. + * @param variableIndex The local variable table index of the variable being accessed. + * @param incrementBy The integer value to increment the variable by. + * @param value The abstract value currently stored in the accessed local variable. + * @return The abstract value of the accessed local variable after incrementing. + */ + protected T incrementVariable(Context context, AbstractFrame state, int variableIndex, int incrementBy, T value) { + return defaultValue(); + } + + /** + * Cast a value into another type. This handler is invoked for the I2F, I2D, L2F, L2D, F2I, F2L, + * F2D, D2I, D2L, D2F, L2I, I2L, I2B, I2S and I2C instructions. + * + * @param context The execution context for this handler. + * @param state The abstract frame being modified by this instruction. + * @param value The abstract value from the operand stack to be cast into another type. + * @return The abstract value after casting. + */ + protected T castOperation(Context context, AbstractFrame state, T value) { + return defaultValue(); + } + + /** + * Compare two values. This handler is called for the LCMP, FCMPL, FCMPG, DCMPL and DCMPG + * instructions. + * + * @param context The execution context for this handler. + * @param state The abstract frame being modified by this instruction. + * @param left The abstract value representing the left operand of the instruction. + * @param right The abstract value representing the right operand of the instruction. + * @return The result of the comparison operation. + */ + protected T comparisonOperation(Context context, AbstractFrame state, T left, T right) { + return defaultValue(); + } + + /** + * Handler for unary conditional jumps. It is called for the IFEQ, IFNE, IFLT, IFGE, IFGT, IFLE, + * IFNULL and IFNONNULL instructions. + * + * @param context The execution context for this handler. + * @param state The abstract frame being modified by this instruction. + * @param ifDestinationBci The BCI of the if-then destination instruction. + * @param elseDestinationBci The BCI of the else destination instruction. + * @param value The abstract value representing the comparison operand. + */ + protected void unaryConditionalJump(Context context, AbstractFrame state, int ifDestinationBci, int elseDestinationBci, T value) { + + } + + /** + * Handler for binary conditional jumps. It is called for the IF_ICMPEQ, IF_ICMPNE, IF_ICMPLT, + * IF_ICMPGE, IF_ICMPGT, IF_ICMPLE, IF_ACMPEQ and IF_ACMPNE instructions. + * + * @param context The execution context for this handler. + * @param state The abstract frame being modified by this instruction. + * @param ifDestinationBci The BCI of the if-then destination instruction. + * @param elseDestinationBci The BCI of the else destination instruction. + * @param left The abstract value representing the left comparison operand. + * @param right The abstract value representing the right comparison operand. + */ + protected void binaryConditionalJump(Context context, AbstractFrame state, int ifDestinationBci, int elseDestinationBci, T left, T right) { + + } + + /** + * Handler for GOTO and GOTO_W instructions. + * + * @param context The execution context for this handler. + * @param state The abstract frame being modified by this instruction. + * @param destinationBci The BCI of the destination instruction. + */ + protected void unconditionalJump(Context context, AbstractFrame state, int destinationBci) { + + } + + /** + * Handler for the TABLESWITCH and LOOKUPSWITCH instructions. + * + * @param context The execution context for this handler. + * @param state The abstract frame being modified by this instruction. + * @param bcSwitch The switch instruction. + * @param value The abstract value representing the operand of the switch instruction. + */ + protected void switchJump(Context context, AbstractFrame state, BytecodeSwitch bcSwitch, T value) { + + } + + /** + * Handler for the IRETURN, LRETURN, FRETURN, DRETURN and ARETURN instructions. + * + * @param context The execution context for this handler. + * @param state The abstract frame being modified by this instruction. + * @param value The abstract value being returned. + */ + protected void returnValue(Context context, AbstractFrame state, Object value) { + + } + + /** + * Handler for the RETURN instruction. + * + * @param context The execution context for this handler. + * @param state The abstract frame being modified by this instruction. + */ + protected void returnVoid(Context context, AbstractFrame state) { + + } + + /** + * Load value from a static field and push it on the operand stack. This handler is called for + * the GETSTATIC instruction. + * + * @param context The execution context for this handler. + * @param state The abstract frame being modified by this instruction. + * @param field The field which is being accessed. + * @return The abstract representation of the accessed field's value. + */ + protected T loadStaticField(Context context, AbstractFrame state, JavaField field) { + return defaultValue(); + } + + /** + * Store an abstract value into a static field. This handler is called for the PUTSTATIC + * instruction. + * + * @param context The execution context for this handler. + * @param state The abstract frame being modified by this instruction. + * @param field The field which is being accessed. + * @param value The abstract representation of the value to be stored in the accessed field. + */ + protected void storeStaticField(Context context, AbstractFrame state, JavaField field, T value) { + + } + + /** + * Load value from a non-static field and push it on the operand stack. This handler is called + * for the GETFIELD instruction. + * + * @param context The execution context for this handler. + * @param state The abstract frame being modified by this instruction. + * @param field The field which is being accessed. + * @param object The abstract representation of the object the accessed field belongs to. + * @return The abstract representation of the accessed field's value. + */ + protected T loadField(Context context, AbstractFrame state, JavaField field, T object) { + return defaultValue(); + } + + /** + * Store an abstract value into a non-static field. This handler is called for the PUTFIELD + * instruction. + * + * @param context The execution context for this handler. + * @param state The abstract frame being modified by this instruction. + * @param field The field which is being accessed. + * @param object The abstract representation of the object the accessed field belongs to. + * @param value The abstract representation of the value to be stored in the accessed field. + */ + protected void storeField(Context context, AbstractFrame state, JavaField field, T object, T value) { + + } + + /** + * Invoke a non-void method. This handler is called for the INVOKEVIRTUAL, INVOKESPECIAL, + * INVOKESTATIC, INVOKEINTERFACE and INVOKEDYNAMIC instructions. + * + * @param context The execution context for this handler. + * @param state The abstract frame being modified by this instruction. + * @param method The method which is being invoked. + * @param operands The abstract representation of the operands. If the method has a receiver, + * its abstract value is stored as the first element of the list. + * @return The abstract representation of the result of the invocation. + */ + protected T invokeMethod(Context context, AbstractFrame state, JavaMethod method, List operands) { + return defaultValue(); + } + + /** + * Invoke a void method. This handler is called for the INVOKEVIRTUAL, INVOKESPECIAL, + * INVOKESTATIC, INVOKEINTERFACE and INVOKEDYNAMIC instructions. + * + * @param context The execution context for this handler. + * @param state The abstract frame being modified by this instruction. + * @param method The method which is being invoked. + * @param operands The abstract representation of the operands. If the method has a receiver, + * its abstract value is stored as the first element of the list. + */ + protected void invokeVoidMethod(Context context, AbstractFrame state, JavaMethod method, List operands) { + + } + + /** + * Handler for loading a method's appendix. + * This is + * used in INVOKEDYNAMIC instructions + * + * @param method The invoked method. + * @param appendix The invoked method's appendix. + * @return Abstract representation of the appendix. + */ + protected T pushAppendix(JavaMethod method, JavaConstant appendix) { + return defaultValue(); + } + + /** + * Create a new object and push its reference on the operand stack. This handler is called for + * the NEW instruction. + * + * @param context The execution context for this handler. + * @param state The abstract frame being modified by this instruction. + * @param type The type of the object whose reference is being pushed to the stack. + * @return The abstract representation of the object. + */ + protected T newObject(Context context, AbstractFrame state, JavaType type) { + return defaultValue(); + } + + /** + * Create a new array and push its reference on the operand stack. This handler is called for + * the NEWARRAY, ANEWARRAY and MULTIANEWARRAY instructions. + * + * @param context The execution context for this handler. + * @param state The abstract frame being modified by this instruction. + * @param type The type of the array elements. + * @param counts List of capacities for each of the array's dimensions. + * @return The abstract representation of the array. + */ + protected T newArray(Context context, AbstractFrame state, JavaType type, List counts) { + return defaultValue(); + } + + /** + * Handler for the ARRAYLENGTH instruction. + * + * @param context The execution context for this handler. + * @param state The abstract frame being modified by this instruction. + * @param array The abstract representation of the array. + * @return The abstract representation of the array's length. + */ + protected T arrayLength(Context context, AbstractFrame state, T array) { + return defaultValue(); + } + + /** + * Handler for the ATHROW instruction. + * + * @param context The execution context for this handler. + * @param state The abstract frame being modified by this instruction. + * @param throwable The abstract representation of the object being thrown. + */ + protected void doThrow(Context context, AbstractFrame state, T throwable) { + + } + + /** + * Handler for the CHECKCAST and INSTANCEOF instructions. + * + * @param context The execution context for this handler. + * @param state The abstract frame being modified by this instruction. + * @param type The type being checked against. + * @param object The abstract representation of the instruction operand. + * @return The abstract representation of the instruction's result. + */ + protected T castCheckOperation(Context context, AbstractFrame state, JavaType type, T object) { + return defaultValue(); + } + + /** + * Handler for the MONITORENTER and MONITOREXIT operations. + * + * @param context The execution context for this handler. + * @param state The abstract frame being modified by this instruction. + * @param object The abstract representation of the instruction operand. + */ + protected void monitorOperation(Context context, AbstractFrame state, T object) { + + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/dataflow/DataFlowAnalysisException.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/dataflow/DataFlowAnalysisException.java new file mode 100644 index 000000000000..597d9ef1b8bd --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/dataflow/DataFlowAnalysisException.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2025, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.hosted.dataflow; + +import java.io.Serial; + +/** + * Exceptions thrown during bytecode data-flow analysis. These should only be thrown in rare cases, + * such as unsupported opcodes, as {@link ForwardDataFlowAnalyzer} and {@link AbstractInterpreter} + * assume that the received bytecode is already verified. + */ +public class DataFlowAnalysisException extends RuntimeException { + + @Serial private static final long serialVersionUID = 1L; + + public DataFlowAnalysisException(String message) { + super(message); + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/dataflow/ForwardDataFlowAnalyzer.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/dataflow/ForwardDataFlowAnalyzer.java new file mode 100644 index 000000000000..21fb0de6e7a7 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/dataflow/ForwardDataFlowAnalyzer.java @@ -0,0 +1,233 @@ +/* + * Copyright (c) 2025, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.hosted.dataflow; + +import java.util.ArrayList; +import java.util.BitSet; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.graalvm.collections.Pair; + +import jdk.graal.compiler.bytecode.Bytecode; +import jdk.graal.compiler.bytecode.BytecodeStream; +import jdk.graal.compiler.debug.DebugContext; +import jdk.graal.compiler.java.BciBlockMapping; +import jdk.graal.compiler.java.BciBlockMapping.BciBlock; +import jdk.graal.compiler.options.OptionValues; +import jdk.vm.ci.meta.ExceptionHandler; +import jdk.vm.ci.meta.JavaType; +import jdk.vm.ci.meta.ResolvedJavaMethod; + +/** + * Abstract bytecode forward data-flow analyzer. Abstract program states, represented by {@code S}, + * are propagated through the control-flow graph of a method until a fixed point is reached, i.e., + * there are no more modifications of the abstract program states. + *

+ * This class can be used to implement simple data-flow algorithms, such as finding reaching + * definitions, or more complex algorithms where {@code S} is an abstract representation of the + * program's execution frames. + * + * @param The type of the abstract program state to be propagated during the data-flow analysis. + * {@code S} should override {@link Object#equals(Object)} in order to allow a fixed + * point to be reached by the analyzer. + */ +public abstract class ForwardDataFlowAnalyzer { + + /** + * Executes the data-flow analysis on {@link BciBlockMapping}. It is assumed that the received + * bytecode is valid and verified by a bytecode verifier. + * + * @param controlFlowGraph The control flow graph of the method to analyze. + * @return A mapping from bytecode instruction BCI to the inferred abstract state of the program + * before the execution of corresponding instruction. + * @throws DataFlowAnalysisException Can be thrown to signal unrecoverable exceptions in the + * analysis. + */ + public Map analyze(BciBlockMapping controlFlowGraph) { + ExceptionHandler[] exceptionHandlers = controlFlowGraph.code.getExceptionHandlers(); + + Map states = new HashMap<>(); + LinkedHashSet workList = new LinkedHashSet<>(); + + /* Create an initial (usually "empty") state at BCI 0. */ + states.put(controlFlowGraph.getStartBlock().getStartBci(), createInitialState(controlFlowGraph.code.getMethod())); + workList.add(controlFlowGraph.getStartBlock()); + + while (!workList.isEmpty()) { + BciBlock currentBlock = workList.getFirst(); + workList.remove(currentBlock); + + Pair outStateAndEndBCI = processBlock(currentBlock, controlFlowGraph.code, states); + /* + * We don't have to process the basic block's successors if we reach a fixed point + * within that block (i.e., there are no changes in the abstract state at some point + * during the processing of the basic block). + */ + if (outStateAndEndBCI == null) { + continue; + } + + S outState = outStateAndEndBCI.getRight(); + int blockEndBCI = outStateAndEndBCI.getLeft(); + + /* Go through all non-exception handler successors. */ + for (BciBlock successor : currentBlock.getSuccessors()) { + if (!successor.isInstructionBlock()) { + continue; + } + + S outStateCopy = copyState(outState); + mergeIntoSuccessorBlock(outStateCopy, successor, states, workList); + } + + BitSet handlers = exceptionHandlers.length > 0 + ? controlFlowGraph.getBciExceptionHandlerIDs(blockEndBCI) + : new BitSet(); + + /* Go through all the exception handler successors. */ + for (int i = handlers.nextSetBit(0); i >= 0;) { + BciBlock handlerBlock = controlFlowGraph.getHandlerBlock(i); + + /* Gather all the exception types caught by this handler. */ + List exceptionTypes = new ArrayList<>(); + while (i >= 0 && controlFlowGraph.getHandlerBlock(i) == handlerBlock) { + exceptionTypes.add(exceptionHandlers[i].getCatchType()); + i = handlers.nextSetBit(i + 1); + } + + S handlerState = createExceptionState(outState, exceptionTypes); + mergeIntoSuccessorBlock(handlerState, handlerBlock, states, workList); + } + } + + return states; + } + + /** + * Wrapper for {@link #analyze(BciBlockMapping)} which creates a control flow graph based on + * {@link Bytecode}. + */ + public Map analyze(Bytecode bytecode) { + OptionValues emptyOptions = new OptionValues(null, OptionValues.newOptionMap()); + DebugContext disabledDebugContext = DebugContext.disabled(emptyOptions); + BciBlockMapping controlFlowGraph = BciBlockMapping.create(new BytecodeStream(bytecode.getCode()), bytecode, emptyOptions, disabledDebugContext, false); + return analyze(controlFlowGraph); + } + + private Pair processBlock(BciBlock block, Bytecode code, Map states) { + BytecodeStream stream = new BytecodeStream(code.getCode()); + stream.setBCI(block.getStartBci()); + + S outState = processInstruction(states.get(stream.currentBCI()), stream, code); + while (stream.nextBCI() <= block.getEndBci()) { + S successorState = states.get(stream.nextBCI()); + if (!outState.equals(successorState)) { + states.put(stream.nextBCI(), outState); + } else { + return null; + } + stream.next(); + outState = processInstruction(states.get(stream.currentBCI()), stream, code); + } + + /* + * There seems to sometimes be a mismatch between the actual end BCI of a BciBlock and the + * BCI obtained from BciBlock::getEndBci. Because of that, we return the BCI of the last + * processed instruction in the block together with the state. + */ + return Pair.create(stream.currentBCI(), outState); + } + + private void mergeIntoSuccessorBlock(S state, BciBlock successorBlock, Map states, Set workList) { + int successorStartBCI = successorBlock.getStartBci(); + S successorState = states.get(successorStartBCI); + if (successorState == null) { + /* First time we enter this basic block. */ + states.put(successorStartBCI, state); + workList.add(successorBlock); + } else { + S mergedState = mergeStates(successorState, state); + if (!mergedState.equals(successorState)) { + states.put(successorStartBCI, mergedState); + workList.add(successorBlock); + } + } + } + + /** + * Create the initial state for the analysis. + * + * @param method The JVMCI method being analyzed. + * @return The abstract program state at the entry point of the method. + */ + protected abstract S createInitialState(ResolvedJavaMethod method); + + /** + * Create the state at the entry of an exception handler. + * + * @param inState The abstract program state before the entry into the exception handler. + * @param exceptionTypes The exception types of the handler which is being entered. + * @return The abstract program state at the entry point of the exception handler. + */ + protected abstract S createExceptionState(S inState, List exceptionTypes); + + /** + * Create a deep copy of a state. + * + * @param state State to be copied. + * @return Deep copy of the input state. + */ + protected abstract S copyState(S state); + + /** + * Merge two states from divergent control flow paths. The operation should be: + *

    + *
  • Idempotent: {@code mergeStates(s, s) = s};
  • + *
  • Commutative: {@code mergeStates(x, y) = mergeStates(y, x)};
  • + *
  • Associative: + * {@code mergeStates(mergeStates(x, y), z) = mergeStates(x, mergeStates(y, z))}.
  • + *
+ * + * @return The merged state of the left and right input states. + */ + protected abstract S mergeStates(S left, S right); + + /** + * The data-flow transfer function. The function should be monotonic, i.e., it should enable the + * fixed point to be reached with respect to the implementation {@link S#equals(Object)} and + * {@link #mergeStates(Object, Object)}. + * + * @param inState The abstract program state right before the execution of the instruction. + * @param stream A bytecode stream of which the position is set to the instruction currently + * being processed. The stream should not be modified from this method. + * @param code The bytecode of the method currently being analyzed. + * @return The abstract program state after the instruction being processed. + */ + protected abstract S processInstruction(S inState, BytecodeStream stream, Bytecode code); +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/dynamicaccessinference/ConstantExpressionAnalyzer.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/dynamicaccessinference/ConstantExpressionAnalyzer.java new file mode 100644 index 000000000000..61ca9e86e407 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/dynamicaccessinference/ConstantExpressionAnalyzer.java @@ -0,0 +1,678 @@ +/* + * Copyright (c) 2025, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.hosted.dynamicaccessinference; + +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.reflect.Array; +import java.lang.reflect.Constructor; +import java.lang.reflect.Executable; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; + +import com.oracle.graal.pointsto.infrastructure.OriginalClassProvider; +import com.oracle.graal.pointsto.infrastructure.OriginalMethodProvider; +import com.oracle.graal.pointsto.infrastructure.WrappedConstantPool; +import com.oracle.graal.pointsto.infrastructure.WrappedJavaMethod; +import com.oracle.svm.core.hub.ClassForNameSupport; +import com.oracle.svm.hosted.ImageClassLoader; +import com.oracle.svm.util.ReflectionUtil; +import com.oracle.svm.util.TypeResult; +import com.oracle.svm.hosted.dataflow.AbstractFrame; +import com.oracle.svm.hosted.dataflow.AbstractInterpreter; + +import jdk.graal.compiler.nodes.spi.CoreProviders; +import jdk.vm.ci.meta.Constant; +import jdk.vm.ci.meta.ConstantPool; +import jdk.vm.ci.meta.JavaConstant; +import jdk.vm.ci.meta.JavaField; +import jdk.vm.ci.meta.JavaMethod; +import jdk.vm.ci.meta.JavaType; +import jdk.vm.ci.meta.ResolvedJavaMethod; +import jdk.vm.ci.meta.ResolvedJavaType; + +import static jdk.graal.compiler.bytecode.Bytecodes.ACONST_NULL; +import static jdk.graal.compiler.bytecode.Bytecodes.ANEWARRAY; +import static jdk.graal.compiler.bytecode.Bytecodes.CHECKCAST; + +/** + * A bytecode-level constant expression analyzer for use in contexts which can affect native image + * execution semantics, such as build-time inferring of reflective calls as done by + * {@link StrictDynamicAccessInferenceFeature}. + *

+ * The analyzer builds {@link AbstractFrame abstract frames} for each bytecode instruction of the + * analyzed method. The {@link ConstantExpressionAnalyzer.Value abstract values} stored in the + * abstract frames can be either: + *

    + *
  • {@link NotACompileTimeConstant} - represents a value which cannot be inferred by the + * analyzer
  • + *
  • {@link CompileTimeConstant} - a value inferrable by the analyzer
  • + *
+ * Furthermore, the inferrable values can be either: + *
    + *
  • {@link CompileTimeValueConstant} - an inferrable value which is not an array
  • + *
  • {@link CompileTimeArrayConstant} - an inferrable array
  • + *
+ * Each {@link CompileTimeConstant} is represented by a pair (source BCI, inferred value). The + * source BCI represents the BCI (bytecode offset) of the instruction which last placed the constant + * into the abstract frame, while the inferred value represents the actual Java value which would be + * observed during the run-time execution of the corresponding instruction. + */ +public final class ConstantExpressionAnalyzer extends AbstractInterpreter { + + private static final NotACompileTimeConstant NOT_A_COMPILE_TIME_CONSTANT = new NotACompileTimeConstant(); + + private final ImageClassLoader classLoader; + private final Map> propagatingMethods; + + public ConstantExpressionAnalyzer(CoreProviders providers, ImageClassLoader classLoader) { + super(providers); + this.classLoader = classLoader; + this.propagatingMethods = buildPropagatingMethods(); + } + + @Override + protected Value defaultValue() { + return NOT_A_COMPILE_TIME_CONSTANT; + } + + @Override + protected Value merge(Value left, Value right) { + return left.equals(right) ? left : defaultValue(); + } + + /** + * Propagation of constants usually begins with instructions which push constants onto the + * operand stack, such as ACONST_NULL, LDC or ICONST_2. + *

+ * For example, an LDC instruction at BCI 6 and referencing a String constant pool entry + * "SomeValue" would push a (6, "SomeValue") {@link CompileTimeValueConstant compile-time + * constant} onto the abstract operand stack. + */ + @Override + protected Value pushConstant(Context context, AbstractFrame state, Constant constant) { + if (context.opcode() == ACONST_NULL) { + return new CompileTimeValueConstant<>(context.bci(), null); + } + if (constant instanceof JavaConstant javaConstant) { + /* + * The analyzer does not differentiate between boolean, byte, char, short and int + * primitive type values. + */ + Object javaValue = switch (javaConstant.getJavaKind().getStackKind()) { + case Int -> javaConstant.asInt(); + case Long -> javaConstant.asLong(); + case Float -> javaConstant.asFloat(); + case Double -> javaConstant.asDouble(); + case Object -> getProviders().getSnippetReflection().asObject(Object.class, javaConstant); + default -> null; + }; + if (javaValue != null) { + return new CompileTimeValueConstant<>(context.bci(), javaValue); + } + } + return defaultValue(); + } + + /** + * As with {@link ConstantExpressionAnalyzer#pushConstant(Context, AbstractFrame, Constant)}, + * load constant instructions referencing types in the constant pool push the appropriate + * {@link java.lang.Class} objects onto the abstract operand stack. If the reference to the type + * cannot be resolved, a {@link ConstantExpressionAnalyzer.NotACompileTimeConstant} is pushed + * onto the stack instead. + */ + @Override + protected Value pushType(Context context, AbstractFrame state, JavaType type) { + if (type instanceof ResolvedJavaType resolvedType) { + return new CompileTimeValueConstant<>(context.bci(), OriginalClassProvider.getJavaClass(resolvedType)); + } else { + return defaultValue(); + } + } + + /** + * Variable load instructions, e.g., ALOAD, propagate constants in the local variable table + * entry they point to hold a {@link CompileTimeValueConstant non-array type compile-time + * constant}. The inferred value is the same as of the local variable table entry, but the + * source BCI is changed to the BCI of the load instruction. + *

+ * For example, an ALOAD instruction at BCI 37 referencing a local variable table entry holding + * a compile-time constant (13, "SomeValue") would push a (37, "SomeValue") compile-time + * constant onto the abstract operand stack. + */ + @Override + protected Value loadVariable(Context context, AbstractFrame state, int variableIndex, Value value) { + if (value instanceof CompileTimeValueConstant constant) { + return new CompileTimeValueConstant<>(context.bci(), constant.getValue()); + } else { + return defaultValue(); + } + } + + /** + * Variable store instructions, e.g., ASTORE, propagate constants if their operand is a + * {@link CompileTimeValueConstant non-array type compile-time constant}. The inferred value is + * the same as of its operand, but the source BCI is changed to the BCI of the store + * instruction. + *

+ * For example, an ASTORE instruction at BCI 13 with compile-time constant operand (9, + * "SomeValue") would store a (13, "SomeValue") compile-time constant into the abstract local + * variable table. + *

+ * In case the operand of the store instruction is an {@link CompileTimeArrayConstant array type + * compile-time constant}, all references to that array on in the abstract frame are marked as + * non-compile-time constants. + *

+ * For example, if an array type compile-time constant (14, [int.class, String.class]) is the + * operand of the store instruction, all compile-time constants with source BCI of 14 in the + * {@code state} are transformed to non-compile-time constants. + */ + @Override + protected Value storeVariable(Context context, AbstractFrame state, int variableIndex, Value value) { + if (value instanceof CompileTimeValueConstant constant) { + return new CompileTimeValueConstant<>(context.bci(), constant.getValue()); + } else if (value instanceof CompileTimeArrayConstant constantArray) { + state.transform(v -> v.equals(constantArray), v -> defaultValue()); + return defaultValue(); + } else { + return defaultValue(); + } + } + + /** + * In case of an array store instruction with all compile-time constant operands (the array + * reference, the array index and the element to store), all the compile-time constants in the + * abstract frame which correspond to the array reference, i.e., have the same source BCI as the + * array reference compile-time constant, are modified by storing the value of the constant + * element into the appropriate position of their underlying inferred value array. + *

+ * Likewise, having a compile-time constant array reference operand, but with either the array + * index or element operand not being compile-time constants, all the corresponding compile-time + * constant arrays in the abstract frame are marked as {@link NotACompileTimeConstant not a + * compile time constant}. + */ + @Override + protected void storeArrayElement(Context context, AbstractFrame state, Value array, Value index, Value value) { + if (array instanceof CompileTimeArrayConstant constantArray) { + if (index instanceof CompileTimeValueConstant constantIndex && value instanceof CompileTimeValueConstant constantValue) { + CompileTimeArrayConstant newConstantArray = new CompileTimeArrayConstant<>(context.bci(), constantArray); + try { + int realIndex = ((Number) constantIndex.getValue()).intValue(); + newConstantArray.setElement(realIndex, constantValue.getValue()); + state.transform(v -> v.equals(constantArray), v -> newConstantArray); + } catch (Exception e) { + state.transform(v -> v.equals(constantArray), v -> defaultValue()); + } + } else { + state.transform(v -> v.equals(constantArray), v -> defaultValue()); + } + } + } + + /** + * In order to propagate {@link java.lang.Class} instances of primitive types, GETSTATIC + * instructions referencing the TYPE field of the appropriate wrapper class, e.g., + * {@link java.lang.Integer#TYPE}, push a compile-time constant carrying the corresponding + * {@link java.lang.Class} object. + *

+ * For example, a GETSTATIC instruction at BCI 42 and referencing the + * {@link java.lang.Integer#TYPE} field would push a (42, int.class) + * {@link CompileTimeValueConstant compile-time constant} onto the abstract operand stack. + */ + @Override + protected Value loadStaticField(Context context, AbstractFrame state, JavaField field) { + if (field.getName().equals("TYPE")) { + Class primitiveClass = switch (field.getDeclaringClass().toJavaName()) { + case "java.lang.Boolean" -> boolean.class; + case "java.lang.Character" -> char.class; + case "java.lang.Float" -> float.class; + case "java.lang.Double" -> double.class; + case "java.lang.Byte" -> byte.class; + case "java.lang.Short" -> short.class; + case "java.lang.Integer" -> int.class; + case "java.lang.Long" -> long.class; + case "java.lang.Void" -> void.class; + default -> null; + }; + if (primitiveClass != null) { + return new CompileTimeValueConstant<>(context.bci(), primitiveClass); + } + } + return defaultValue(); + } + + /** + * To prevent escaping of array references and their modification through fields interfering + * with the constant inference, having a PUTSTATIC instruction with a + * {@link CompileTimeArrayConstant compile-time array constant} operand marks all the + * corresponding compile-time constant arrays in the abstract frame as + * {@link NotACompileTimeConstant not a compile time constant}. + */ + @Override + protected void storeStaticField(Context context, AbstractFrame state, JavaField field, Value value) { + if (value instanceof CompileTimeArrayConstant constantArray) { + state.transform(v -> v.equals(constantArray), v -> defaultValue()); + } + } + + /** + * To prevent escaping of array references and their modification through fields interfering + * with the constant inference, having a PUTFIELD instruction with a + * {@link CompileTimeArrayConstant compile-time array constant} operand marks all the + * corresponding compile-time constant arrays in the abstract frame as + * {@link NotACompileTimeConstant not a compile time constant}. + */ + @Override + protected void storeField(Context context, AbstractFrame state, JavaField field, Value object, Value value) { + if (value instanceof CompileTimeArrayConstant constantArray) { + state.transform(v -> v.equals(constantArray), v -> defaultValue()); + } + } + + /** + * The results of certain method invocations, if they are successful, i.e., they do not throw an + * exception, are propagated by pushing the result onto the abstract operand stack as a + * compile-time constant. The methods which are propagated through are defined by + * {@link ConstantExpressionAnalyzer#propagatingMethods}. + *

+ * To prevent escaping of array references and their modification in different methods + * interfering with the constant inference, having a method invocation instruction + * {@link CompileTimeArrayConstant compile-time array constant} operand marks all the + * corresponding compile-time constant arrays in the abstract frame as + * {@link NotACompileTimeConstant not a compile time constant}. + */ + @Override + protected Value invokeMethod(Context context, AbstractFrame state, JavaMethod method, List operands) { + for (Value operand : operands) { + if (operand instanceof CompileTimeArrayConstant constantArray) { + state.transform(v -> v.equals(constantArray), v -> defaultValue()); + } + } + + Method javaMethod = getJavaMethod(method); + if (javaMethod == null) { + /* The method is either unresolved, or is actually a constructor. */ + return defaultValue(); + } + + Function handler = propagatingMethods.get(javaMethod); + return handler != null + ? handler.apply(new InvocationData(javaMethod, context, operands)) + : defaultValue(); + } + + /** + * To prevent escaping of array references and their modification in different methods + * interfering with the constant inference, having a method invocation instruction + * {@link CompileTimeArrayConstant compile-time array constant} operand marks all the + * corresponding compile-time constant arrays in the abstract frame as + * {@link NotACompileTimeConstant not a compile time constant}. + */ + @Override + protected void invokeVoidMethod(Context context, AbstractFrame state, JavaMethod method, List operands) { + for (Value operand : operands) { + if (operand instanceof CompileTimeArrayConstant constantArray) { + state.transform(v -> v.equals(constantArray), v -> defaultValue()); + } + } + } + + /** + * An ANEWARRAY instruction with a {@link CompileTimeValueConstant compile-time constant} array + * size operand creates a {@link CompileTimeArrayConstant compile-time array constant} and + * pushes it onto the abstract operand stack. + */ + @Override + protected Value newArray(Context context, AbstractFrame state, JavaType type, List counts) { + if (context.opcode() == ANEWARRAY && counts.getFirst() instanceof CompileTimeValueConstant size && type instanceof ResolvedJavaType) { + int realSize = ((Number) size.getValue()).intValue(); + return new CompileTimeArrayConstant<>(context.bci(), realSize, OriginalClassProvider.getJavaClass(type)); + } else { + return defaultValue(); + } + } + + /** + * CHECKCAST instructions with a {@link CompileTimeConstant compile-time constant} operand which + * are inferred to be null propagate that null value compile-time constant. + *

+ * For example, a CHECKCAST instruction at BCI 8 with a compile-time constant operand (7, null) + * will push a (8, null) compile-time constant onto the abstract operand stack. + */ + @Override + protected Value castCheckOperation(Context context, AbstractFrame state, JavaType type, Value object) { + if (context.opcode() == CHECKCAST && object instanceof CompileTimeConstant constant && constant.getValue() == null) { + return new CompileTimeValueConstant<>(context.bci(), null); + } else { + return defaultValue(); + } + } + + private Map> buildPropagatingMethods() { + return Map.ofEntries( + /* Propagate results of Class.forName invocations. */ + Map.entry(ReflectionUtil.lookupMethod(Class.class, "forName", String.class), d -> invokeForNameOne(d.context, d.operands)), + Map.entry(ReflectionUtil.lookupMethod(Class.class, "forName", String.class, boolean.class, ClassLoader.class), d -> invokeForNameThree(d.context, d.operands)), + + /* + * Propagate Class.getClassLoader for use in Class.forName(String, boolean, + * ClassLoader). + */ + Map.entry(ReflectionUtil.lookupMethod(Class.class, "getClassLoader"), this::invokeMethod), + + /* Propagate MethodType objects for use in MethodHandle lookups. */ + Map.entry(ReflectionUtil.lookupMethod(MethodType.class, "methodType", Class.class), this::invokeMethod), + Map.entry(ReflectionUtil.lookupMethod(MethodType.class, "methodType", Class.class, Class.class), this::invokeMethod), + Map.entry(ReflectionUtil.lookupMethod(MethodType.class, "methodType", Class.class, Class[].class), this::invokeMethod), + Map.entry(ReflectionUtil.lookupMethod(MethodType.class, "methodType", Class.class, Class.class, Class[].class), this::invokeMethod), + Map.entry(ReflectionUtil.lookupMethod(MethodType.class, "methodType", Class.class, MethodType.class), this::invokeMethod), + + /* Propagate Lookup objects for use in MethodHandle lookups. */ + Map.entry(ReflectionUtil.lookupMethod(MethodHandles.class, "lookup"), d -> getLookup(d.context)), + Map.entry(ReflectionUtil.lookupMethod(MethodHandles.class, "privateLookupIn", Class.class, MethodHandles.Lookup.class), this::invokeMethod)); + } + + private record InvocationData(Method method, Context context, List operands) { + + } + + private static Method getJavaMethod(JavaMethod method) { + if (method instanceof ResolvedJavaMethod resolved) { + Executable executable = OriginalMethodProvider.getJavaMethod(resolved); + if (executable instanceof Method m) { + return m; + } + } + return null; + } + + /** + * Note that boolean, byte, char and short types are all represented as int when using this + * method. + */ + private static T extractValue(Value value, Class type) { + if (value instanceof CompileTimeValueConstant constant) { + Object extracted = constant.getValue(); + if (extracted != null && type.isAssignableFrom(extracted.getClass())) { + return type.cast(extracted); + } + } + return null; + } + + private Value invokeMethod(InvocationData invocationData) { + boolean hasReceiver = !Modifier.isStatic(invocationData.method.getModifiers()); + Object receiver = null; + if (hasReceiver) { + Value aReceiver = invocationData.operands.getFirst(); + if (aReceiver instanceof CompileTimeConstant constant) { + receiver = constant.getValue(); + } else { + return defaultValue(); + } + } + Object[] arguments = new Object[invocationData.method.getParameterCount()]; + for (int i = 0; i < arguments.length; i++) { + Value aArgument = invocationData.operands.get(i + (hasReceiver ? 1 : 0)); + if (aArgument instanceof CompileTimeConstant constant) { + arguments[i] = constant.getValue(); + } else { + return defaultValue(); + } + } + try { + return new CompileTimeValueConstant<>(invocationData.context.bci(), invocationData.method.invoke(receiver, arguments)); + } catch (Throwable t) { + return defaultValue(); + } + } + + private Value invokeForNameOne(Context context, List operands) { + String className = extractValue(operands.getFirst(), String.class); + if (className == null) { + return defaultValue(); + } + ClassLoader loader = ClassForNameSupport.respectClassLoader() + ? OriginalClassProvider.getJavaClass(context.method().getDeclaringClass()).getClassLoader() + : classLoader.getClassLoader(); + return findClass(context, className, loader); + } + + private Value invokeForNameThree(Context context, List operands) { + String className = extractValue(operands.getFirst(), String.class); + Integer initialize = extractValue(operands.get(1), Integer.class); + if (className == null || initialize == null) { + return defaultValue(); + } + ClassLoader loader; + if (ClassForNameSupport.respectClassLoader()) { + if (operands.get(2) instanceof CompileTimeValueConstant constant) { + loader = (ClassLoader) constant.getValue(); + } else { + return defaultValue(); + } + } else { + loader = classLoader.getClassLoader(); + } + return findClass(context, className, loader); + } + + private Value findClass(Context context, String className, ClassLoader loader) { + TypeResult> clazz = ImageClassLoader.findClass(className, false, loader); + if (clazz.isPresent()) { + return new CompileTimeValueConstant<>(context.bci(), clazz.get()); + } else { + return defaultValue(); + } + } + + private static final Constructor LOOKUP_CONSTRUCTOR = ReflectionUtil.lookupConstructor(MethodHandles.Lookup.class, Class.class); + + private Value getLookup(Context context) { + Class callerClass = OriginalClassProvider.getJavaClass(context.method().getDeclaringClass()); + try { + MethodHandles.Lookup lookup = LOOKUP_CONSTRUCTOR.newInstance(callerClass); + return new CompileTimeValueConstant<>(context.bci(), lookup); + } catch (Throwable t) { + return defaultValue(); + } + } + + /** + * Looking up constants/types/fields/methods through a {@link WrappedConstantPool} can result in + * UnsupportedFeatureException(s) being thrown. To avoid these exceptions, we essentially + * simulate the behavior of the WrappedConstantPool when looking up the underlying JVMCI + * constant/type/field/method, but skip the analysis (or hosted) universe lookup. + */ + private static ConstantPool unwrapIfWrapped(ConstantPool constantPool) { + return constantPool instanceof WrappedConstantPool wrapper + ? wrapper.getWrapped() + : constantPool; + } + + private static ResolvedJavaMethod getOriginalIfWrapped(ResolvedJavaMethod method) { + return method instanceof WrappedJavaMethod + ? OriginalMethodProvider.getOriginalMethod(method) + : method; + } + + @Override + protected Object lookupConstant(ConstantPool constantPool, int cpi, int opcode) { + return super.lookupConstant(unwrapIfWrapped(constantPool), cpi, opcode); + } + + @Override + protected JavaType lookupType(ConstantPool constantPool, int cpi, int opcode) { + return super.lookupType(unwrapIfWrapped(constantPool), cpi, opcode); + } + + @Override + protected JavaField lookupField(ConstantPool constantPool, ResolvedJavaMethod method, int cpi, int opcode) { + return super.lookupField(unwrapIfWrapped(constantPool), getOriginalIfWrapped(method), cpi, opcode); + } + + @Override + protected JavaMethod lookupMethod(ConstantPool constantPool, ResolvedJavaMethod method, int cpi, int opcode) { + return super.lookupMethod(unwrapIfWrapped(constantPool), getOriginalIfWrapped(method), cpi, opcode); + } + + @Override + protected JavaConstant lookupAppendix(ConstantPool constantPool, int cpi, int opcode) { + return super.lookupAppendix(unwrapIfWrapped(constantPool), cpi, opcode); + } + + /** + * Marker interface for abstract values obtained during bytecode-level constant expression + * inference. + */ + public interface Value { + + } + + public static class NotACompileTimeConstant implements Value { + + @Override + public String toString() { + return "Not a compile-time constant"; + } + } + + public abstract static class CompileTimeConstant implements Value { + + private final int sourceBci; + + public CompileTimeConstant(int bci) { + this.sourceBci = bci; + } + + public int getSourceBci() { + return sourceBci; + } + + public abstract Object getValue(); + + /** + * The source BCI (BCI of the instruction that placed the value onto the operand stack or in + * the local variable table) is the source of truth when comparing two compile time constant + * values (an equal source BCI implies an equal value). + */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + CompileTimeConstant that = (CompileTimeConstant) o; + return sourceBci == that.sourceBci; + } + + @Override + public int hashCode() { + return Objects.hashCode(sourceBci); + } + } + + public static class CompileTimeValueConstant extends CompileTimeConstant { + + private final T value; + + public CompileTimeValueConstant(int bci, T value) { + super(bci); + this.value = value; + } + + @Override + public T getValue() { + return value; + } + + @Override + public String toString() { + return "(" + getSourceBci() + ", " + getValue() + ")"; + } + } + + public static class CompileTimeArrayConstant extends CompileTimeConstant { + + /* + * Sparse array representation to avoid possible large memory overheads when analyzing an + * array initialization with a large size. + */ + private final Map value; + private final int size; + private final Class elementType; + + public CompileTimeArrayConstant(int bci, int size, Class elementType) { + super(bci); + this.value = new HashMap<>(); + this.size = size; + this.elementType = elementType; + } + + public CompileTimeArrayConstant(int bci, CompileTimeArrayConstant arrayConstant) { + super(bci); + this.value = new HashMap<>(arrayConstant.value); + this.size = arrayConstant.size; + this.elementType = arrayConstant.elementType; + } + + public void setElement(int index, Object element) throws ArrayIndexOutOfBoundsException, ClassCastException { + if (index < 0 || index >= size) { + throw new ArrayIndexOutOfBoundsException(index); + } + if (!elementType.isAssignableFrom(element.getClass())) { + throw new ClassCastException(element.toString()); + } + value.put(index, elementType.cast(element)); + } + + @Override + public T[] getValue() { + @SuppressWarnings("unchecked") + T[] arrayValue = (T[]) Array.newInstance(elementType, size); + for (Map.Entry entry : value.entrySet()) { + arrayValue[entry.getKey()] = entry.getValue(); + } + return arrayValue; + } + + @Override + public String toString() { + if (size >= 32) { + return "(" + getSourceBci() + ", Array[" + size + "])"; + } else { + return "(" + getSourceBci() + ", " + Arrays.toString(getValue()) + ")"; + } + } + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/dynamicaccessinference/ConstantExpressionRegistry.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/dynamicaccessinference/ConstantExpressionRegistry.java new file mode 100644 index 000000000000..d8ccefc51586 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/dynamicaccessinference/ConstantExpressionRegistry.java @@ -0,0 +1,211 @@ +/* + * Copyright (c) 2025, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.hosted.dynamicaccessinference; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.graalvm.collections.Pair; +import org.graalvm.nativeimage.ImageSingletons; + +import com.oracle.svm.util.LogUtils; +import com.oracle.svm.hosted.dataflow.AbstractFrame; +import com.oracle.svm.hosted.dataflow.DataFlowAnalysisException; + +import jdk.graal.compiler.bytecode.Bytecode; +import jdk.graal.compiler.bytecode.BytecodeProvider; +import jdk.graal.compiler.bytecode.ResolvedJavaMethodBytecodeProvider; +import jdk.graal.compiler.nodes.graphbuilderconf.IntrinsicContext; +import jdk.vm.ci.meta.JavaKind; +import jdk.vm.ci.meta.ResolvedJavaMethod; + +/** + * Holds information on constant expressions as inferred by {@link ConstantExpressionAnalyzer}. + */ +public class ConstantExpressionRegistry { + + public static ConstantExpressionRegistry singleton() { + return ImageSingletons.lookup(ConstantExpressionRegistry.class); + } + + /** + * Representation of inferred {@code null} values in the registry. + */ + private static final Object NULL_MARKER = new Object(); + + /** + * Maps method and BCI pairs into abstract frames which represent the execution frame right + * before the corresponding bytecode instruction. + */ + private Map, AbstractFrame> registry; + private boolean isSealed; + + public ConstantExpressionRegistry() { + registry = new ConcurrentHashMap<>(); + isSealed = false; + } + + public void analyzeAndStore(ConstantExpressionAnalyzer analyzer, ResolvedJavaMethod method, IntrinsicContext intrinsicContext) { + assert !isSealed() : "Cannot store in registry when it is already sealed"; + Bytecode bytecode = getBytecode(method, intrinsicContext); + try { + Map> abstractFrames = analyzer.analyze(bytecode); + abstractFrames.forEach((key, value) -> registry.put(Pair.create(method, key), value)); + } catch (DataFlowAnalysisException e) { + LogUtils.warning("Constant expression analysis failed for " + method.format("%H.%n(%p)") + ": " + e.getMessage()); + } + } + + private static Bytecode getBytecode(ResolvedJavaMethod method, IntrinsicContext intrinsicContext) { + BytecodeProvider bytecodeProvider = intrinsicContext == null + ? ResolvedJavaMethodBytecodeProvider.INSTANCE + : intrinsicContext.getBytecodeProvider(); + return bytecodeProvider.getBytecode(method); + } + + /** + * Attempt to get the inferred receiver of a {@code targetMethod} invocation at the specified + * code location. + * + * @param callerMethod The method in which {@code targetMethod} is invoked + * @param bci The BCI of the invocation instruction with respect to {@code callerMethod} + * @param targetMethod The invoked method + * @return The Java value of the receiver if it can be inferred and null otherwise. A + * {@code null} value is represented by {@link ConstantExpressionRegistry#NULL_MARKER}. + */ + public Object getReceiver(ResolvedJavaMethod callerMethod, int bci, ResolvedJavaMethod targetMethod) { + assert !isSealed() : "Registry is already sealed"; + assert targetMethod.hasReceiver() : "Method " + targetMethod + " does not have receiver"; + AbstractFrame frame = registry.get(Pair.create(callerMethod, bci)); + if (frame == null) { + return null; + } + int numOfParameters = targetMethod.getSignature().getParameterCount(false); + ConstantExpressionAnalyzer.Value receiver = frame.operandStack().getOperand(numOfParameters); + if (receiver instanceof ConstantExpressionAnalyzer.CompileTimeConstant constant) { + Object receiverValue = constant.getValue(); + return receiverValue == null ? NULL_MARKER : receiverValue; + } else { + return null; + } + } + + /** + * Utility method which calls into + * {@link ConstantExpressionRegistry#getReceiver(ResolvedJavaMethod, int, ResolvedJavaMethod)}, + * but attempts to cast the inferred value into {@code type}. + *

+ * If the inferred value is {@code null}, i.e., {@link ConstantExpressionRegistry#NULL_MARKER}, + * {@code null} is returned, which is the same return value as if the receiver could not be + * inferred. + */ + public T getReceiver(ResolvedJavaMethod callerMethod, int bci, ResolvedJavaMethod targetMethod, Class type) { + Object receiver = getReceiver(callerMethod, bci, targetMethod); + return tryToCast(receiver, type); + } + + /** + * Attempt to get an inferred argument of a {@code targetMethod} invocation at the specified + * code location. + * + * @param callerMethod The method in which {@code targetMethod} is invoked + * @param bci The BCI of the invocation instruction with respect to {@code callerMethod} + * @param targetMethod The invoked method + * @param index The argument index + * @return The Java value of the argument if it can be inferred and null otherwise. A + * {@code null} value is represented by {@link ConstantExpressionRegistry#NULL_MARKER}. + */ + public Object getArgument(ResolvedJavaMethod callerMethod, int bci, ResolvedJavaMethod targetMethod, int index) { + assert !isSealed() : "Registry is already sealed"; + int numOfParameters = targetMethod.getSignature().getParameterCount(false); + assert 0 <= index && index < numOfParameters : "Argument index " + index + " out of bounds for " + targetMethod; + AbstractFrame frame = registry.get(Pair.create(callerMethod, bci)); + if (frame == null) { + return null; + } + ConstantExpressionAnalyzer.Value argument = frame.operandStack().getOperand(numOfParameters - index - 1); + if (argument instanceof ConstantExpressionAnalyzer.CompileTimeConstant constant) { + Object argumentValue = constant.getValue(); + if (argumentValue == null) { + return NULL_MARKER; + } else if (argumentValue instanceof Integer n) { + /* + * Since the analyzer doesn't differentiate between boolean, byte, short, char and + * int types, we have to check what the expected type is based on the signature of + * the target method and cast the value appropriately. + */ + JavaKind parameterKind = targetMethod.getSignature().getParameterKind(index); + return switch (parameterKind) { + case JavaKind.Boolean -> n != 0; + case JavaKind.Byte -> n.byteValue(); + case JavaKind.Short -> n.shortValue(); + case JavaKind.Char -> (char) ('0' + n); + default -> argumentValue; + }; + } else { + return argumentValue; + } + } else { + return null; + } + } + + /** + * Utility method which calls into + * {@link ConstantExpressionRegistry#getArgument(ResolvedJavaMethod, int, ResolvedJavaMethod, int)}, + * but attempts to cast the inferred value into {@code type}. + *

+ * If the inferred value is {@code null}, i.e., {@link ConstantExpressionRegistry#NULL_MARKER}, + * {@code null} is returned, which is the same return value as if the argument could not be + * inferred. + */ + public T getArgument(ResolvedJavaMethod callerMethod, int bci, ResolvedJavaMethod targetMethod, int index, Class type) { + Object argument = getArgument(callerMethod, bci, targetMethod, index); + return tryToCast(argument, type); + } + + private static T tryToCast(Object value, Class type) { + if (value == null || isNull(value) || !type.isAssignableFrom(value.getClass())) { + return null; + } + return type.cast(value); + } + + public boolean isSealed() { + return isSealed; + } + + public void seal() { + isSealed = true; + registry = null; + } + + /** + * Check if {@code value} represents a {@code null} Java value. + */ + public static boolean isNull(Object value) { + return value == NULL_MARKER; + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/dynamicaccessinference/DynamicAccessInferenceLog.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/dynamicaccessinference/DynamicAccessInferenceLog.java new file mode 100644 index 000000000000..f748d9430e8d --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/dynamicaccessinference/DynamicAccessInferenceLog.java @@ -0,0 +1,227 @@ +/* + * Copyright (c) 2025, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.hosted.dynamicaccessinference; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import jdk.vm.ci.code.BytecodePosition; +import org.graalvm.collections.Pair; +import org.graalvm.nativeimage.ImageSingletons; + +import com.oracle.svm.core.ParsingReason; +import com.oracle.svm.hosted.ReachabilityRegistrationNode; + +import jdk.graal.compiler.nodes.graphbuilderconf.GraphBuilderContext; +import jdk.graal.compiler.util.json.JsonBuilder; +import jdk.vm.ci.meta.ResolvedJavaMethod; + +public class DynamicAccessInferenceLog { + + public static DynamicAccessInferenceLog singleton() { + return ImageSingletons.lookup(DynamicAccessInferenceLog.class); + } + + private Queue entries = new ConcurrentLinkedQueue<>(); + private boolean isSealed = false; + + public void logConstant(GraphBuilderContext b, ParsingReason reason, ResolvedJavaMethod targetMethod, Object targetReceiver, Object[] targetArguments, Object value) { + logEntry(b, reason, () -> new ConstantLogEntry(b, targetMethod, targetReceiver, targetArguments, value)); + } + + public void logException(GraphBuilderContext b, ParsingReason reason, ResolvedJavaMethod targetMethod, Object targetReceiver, Object[] targetArguments, + Class exceptionClass) { + logEntry(b, reason, () -> new ExceptionLogEntry(b, targetMethod, targetReceiver, targetArguments, exceptionClass)); + } + + public void logRegistration(GraphBuilderContext b, ParsingReason reason, ResolvedJavaMethod targetMethod, Object targetReceiver, Object[] targetArguments) { + logEntry(b, reason, () -> new RegistrationLogEntry(b, targetMethod, targetReceiver, targetArguments)); + } + + private void logEntry(GraphBuilderContext b, ParsingReason reason, Supplier entrySupplier) { + if (reason.duringAnalysis() && reason != ParsingReason.JITCompilation) { + assert !isSealed : "Logging attempt when log is already sealed"; + LogEntry entry = entrySupplier.get(); + b.add(ReachabilityRegistrationNode.create(() -> entries.add(entry), reason)); + } + } + + Queue getEntries() { + return entries; + } + + public boolean isSealed() { + return isSealed; + } + + public void seal() { + isSealed = true; + entries = null; + } + + abstract static class LogEntry { + + final Pair callLocation; + final List callStack; + final ResolvedJavaMethod targetMethod; + final Object targetReceiver; + final Object[] targetArguments; + + LogEntry(GraphBuilderContext b, ResolvedJavaMethod targetMethod, Object targetReceiver, Object[] targetArguments) { + assert targetMethod.hasReceiver() == (targetReceiver != null) : "Inferred receiver does not match with target method signature"; + assert targetMethod.getSignature().getParameterCount(false) == targetArguments.length : "Inferred arguments do not match with target method signature"; + this.callLocation = Pair.create(b.getMethod(), b.bci()); + this.callStack = getCallStack(b); + this.targetMethod = targetMethod; + this.targetReceiver = targetReceiver; + this.targetArguments = targetArguments; + } + + private static List getCallStack(GraphBuilderContext b) { + BytecodePosition inliningContext = b.getInliningChain(); + List callStack = new ArrayList<>(); + if (inliningContext == null || !inliningContext.getMethod().equals(b.getMethod()) || inliningContext.getBCI() != b.bci()) { + callStack.add(b.getMethod().asStackTraceElement(b.bci())); + } + while (inliningContext != null) { + callStack.add(inliningContext.getMethod().asStackTraceElement(inliningContext.getBCI())); + inliningContext = inliningContext.getCaller(); + } + return callStack; + } + + @Override + public String toString() { + String targetArgumentsString = Stream.of(targetArguments) + .map(arg -> arg instanceof Object[] ? Arrays.toString((Object[]) arg) : Objects.toString(arg)).collect(Collectors.joining(", ")); + + if (targetReceiver != null) { + return String.format("Call to %s reachable in %s with receiver %s and arguments (%s) was inferred", + targetMethod.format("%H.%n(%p)"), callStack.getFirst(), targetReceiver, targetArgumentsString); + } else { + return String.format("Call to %s reachable in %s with arguments (%s) was inferred", + targetMethod.format("%H.%n(%p)"), callStack.getFirst(), targetArgumentsString); + } + } + + public void toJson(JsonBuilder.ObjectBuilder builder) throws IOException { + try (JsonBuilder.ArrayBuilder inliningContextBuilder = builder.append("inliningContext").array()) { + for (StackTraceElement element : callStack) { + inliningContextBuilder.append(element); + } + } + builder.append("targetMethod", targetMethod.format("%H.%n(%p)")); + if (targetReceiver != null) { + builder.append("targetCaller", targetReceiver); + } + try (JsonBuilder.ArrayBuilder argsBuilder = builder.append("targetArguments").array()) { + for (Object arg : targetArguments) { + argsBuilder.append(arg instanceof Object[] ? Arrays.toString((Object[]) arg) : Objects.toString(arg)); + } + } + } + } + + static class ConstantLogEntry extends LogEntry { + + final Object value; + + ConstantLogEntry(GraphBuilderContext b, ResolvedJavaMethod targetMethod, Object targetCaller, Object[] targetArguments, Object value) { + super(b, targetMethod, targetCaller, targetArguments); + this.value = value; + } + + @Override + public String toString() { + return super.toString() + " as the constant " + value; + } + + @Override + public void toJson(JsonBuilder.ObjectBuilder builder) throws IOException { + super.toJson(builder); + builder.append("constantValue", value); + } + } + + static class ExceptionLogEntry extends LogEntry { + + final Class exceptionClass; + + ExceptionLogEntry(GraphBuilderContext b, ResolvedJavaMethod targetMethod, Object targetCaller, Object[] targetArguments, Class exceptionClass) { + super(b, targetMethod, targetCaller, targetArguments); + this.exceptionClass = exceptionClass; + } + + @Override + public String toString() { + return super.toString() + " to throw " + exceptionClass.getName(); + } + + @Override + public void toJson(JsonBuilder.ObjectBuilder builder) throws IOException { + super.toJson(builder); + builder.append("exception", exceptionClass.getName()); + } + } + + static class RegistrationLogEntry extends LogEntry { + + RegistrationLogEntry(GraphBuilderContext b, ResolvedJavaMethod targetMethod, Object targetCaller, Object[] targetArguments) { + super(b, targetMethod, targetCaller, targetArguments); + } + + @Override + public String toString() { + return super.toString() + " and registered for runtime usage"; + } + + @Override + public void toJson(JsonBuilder.ObjectBuilder builder) throws IOException { + super.toJson(builder); + } + } + + private static final Object IGNORED_ARGUMENT_MARKER = new IgnoredArgumentValue(); + + public static Object ignoreArgument() { + return IGNORED_ARGUMENT_MARKER; + } + + private static final class IgnoredArgumentValue { + + @Override + public String toString() { + return ""; + } + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/dynamicaccessinference/DynamicAccessInferenceLoggingFeature.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/dynamicaccessinference/DynamicAccessInferenceLoggingFeature.java new file mode 100644 index 000000000000..08d804d897d1 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/dynamicaccessinference/DynamicAccessInferenceLoggingFeature.java @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2025, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.hosted.dynamicaccessinference; + +import java.io.IOException; +import java.util.List; + +import org.graalvm.collections.Pair; +import org.graalvm.nativeimage.ImageSingletons; + +import com.oracle.graal.pointsto.reports.ReportUtils; +import com.oracle.svm.core.SubstrateOptions; +import com.oracle.svm.core.feature.AutomaticallyRegisteredFeature; +import com.oracle.svm.core.feature.InternalFeature; +import com.oracle.svm.core.option.HostedOptionKey; +import com.oracle.svm.util.LogUtils; + +import jdk.graal.compiler.options.Option; +import jdk.graal.compiler.options.OptionStability; +import jdk.graal.compiler.util.json.JsonBuilder; +import jdk.graal.compiler.util.json.JsonPrettyWriter; +import jdk.graal.compiler.util.json.JsonWriter; +import jdk.vm.ci.meta.ResolvedJavaMethod; + +@AutomaticallyRegisteredFeature +public class DynamicAccessInferenceLoggingFeature implements InternalFeature { + + static class Options { + @Option(help = "Log inferred dynamic access invocations.", stability = OptionStability.EXPERIMENTAL)// + static final HostedOptionKey LogDynamicAccessInference = new HostedOptionKey<>(false); + } + + private DynamicAccessInferenceLog log; + + private static boolean isEnabled() { + return Options.LogDynamicAccessInference.getValue() || shouldWarnForNonStrictFolding(); + } + + @Override + public boolean isInConfiguration(IsInConfigurationAccess access) { + return isEnabled(); + } + + @Override + public void afterRegistration(AfterRegistrationAccess access) { + log = new DynamicAccessInferenceLog(); + ImageSingletons.add(DynamicAccessInferenceLog.class, log); + } + + @Override + public void afterAnalysis(AfterAnalysisAccess access) { + if (Options.LogDynamicAccessInference.getValue()) { + dumpLog(); + } + if (shouldWarnForNonStrictFolding()) { + warnForNonStrictFolding(); + } + /* The log is no longer used after this, so we can clean it up. */ + log.seal(); + } + + private void dumpLog() { + assert !log.isSealed() : "Attempt to access sealed log"; + String reportsPath = SubstrateOptions.reportsPath(); + ReportUtils.report("inferred dynamic access invocations", reportsPath, "dynamic_access_inference", "json", (writer) -> { + try (JsonWriter out = new JsonPrettyWriter(writer); + JsonBuilder.ArrayBuilder arrayBuilder = out.arrayBuilder()) { + for (DynamicAccessInferenceLog.LogEntry entry : log.getEntries()) { + try (JsonBuilder.ObjectBuilder objectBuilder = arrayBuilder.nextEntry().object()) { + entry.toJson(objectBuilder); + } + } + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } + + private static boolean shouldWarnForNonStrictFolding() { + return StrictDynamicAccessInferenceFeature.Options.StrictDynamicAccessInference.getValue() == StrictDynamicAccessInferenceFeature.Options.Mode.Warn; + } + + private void warnForNonStrictFolding() { + assert !log.isSealed() : "Attempt to access sealed log"; + ConstantExpressionRegistry registry = ConstantExpressionRegistry.singleton(); + List unsafeFoldingEntries = log.getEntries().stream().filter(entry -> !registryContainsConstantOperands(registry, entry)).toList(); + if (!unsafeFoldingEntries.isEmpty()) { + StringBuilder sb = new StringBuilder(); + sb.append("The following method invocations have been inferred outside of the strict constant expression mode:").append(System.lineSeparator()); + for (int i = 0; i < unsafeFoldingEntries.size(); i++) { + sb.append((i + 1)).append(". ").append(unsafeFoldingEntries.get(i)).append(System.lineSeparator()); + } + sb.delete(sb.length() - System.lineSeparator().length(), sb.length()); + LogUtils.warning(sb.toString()); + } + } + + private static boolean registryContainsConstantOperands(ConstantExpressionRegistry registry, DynamicAccessInferenceLog.LogEntry entry) { + Pair callLocation = entry.callLocation; + if (entry.targetMethod.hasReceiver()) { + Object receiver = registry.getReceiver(callLocation.getLeft(), callLocation.getRight(), entry.targetMethod); + if (entry.targetReceiver != DynamicAccessInferenceLog.ignoreArgument() && receiver == null) { + return false; + } + } + for (int i = 0; i < entry.targetArguments.length; i++) { + Object argument = registry.getArgument(callLocation.getLeft(), callLocation.getRight(), entry.targetMethod, i); + if (entry.targetArguments[i] != DynamicAccessInferenceLog.ignoreArgument() && argument == null) { + return false; + } + } + return true; + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/dynamicaccessinference/StrictDynamicAccessInferenceFeature.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/dynamicaccessinference/StrictDynamicAccessInferenceFeature.java new file mode 100644 index 000000000000..051b92e0f1ee --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/dynamicaccessinference/StrictDynamicAccessInferenceFeature.java @@ -0,0 +1,598 @@ +/* + * Copyright (c) 2025, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.hosted.dynamicaccessinference; + +import java.lang.invoke.MethodHandles; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Executable; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Proxy; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; + +import org.graalvm.nativeimage.ImageSingletons; +import org.graalvm.nativeimage.hosted.Feature; +import org.graalvm.nativeimage.hosted.RuntimeProxyCreation; +import org.graalvm.nativeimage.hosted.RuntimeReflection; +import org.graalvm.nativeimage.hosted.RuntimeResourceAccess; + +import com.oracle.graal.pointsto.infrastructure.OriginalClassProvider; +import com.oracle.graal.pointsto.meta.AnalysisUniverse; +import com.oracle.graal.pointsto.util.GraalAccess; +import com.oracle.svm.core.ParsingReason; +import com.oracle.svm.core.annotate.Delete; +import com.oracle.svm.core.feature.AutomaticallyRegisteredFeature; +import com.oracle.svm.core.feature.InternalFeature; +import com.oracle.svm.core.option.HostedOptionKey; +import com.oracle.svm.core.hub.ClassForNameSupport; +import com.oracle.svm.core.hub.PredefinedClassesSupport; +import com.oracle.svm.core.hub.RuntimeClassLoading; +import com.oracle.svm.core.util.VMError; +import com.oracle.svm.hosted.ExceptionSynthesizer; +import com.oracle.svm.hosted.FeatureImpl; +import com.oracle.svm.hosted.ImageClassLoader; +import com.oracle.svm.hosted.ReachabilityRegistrationNode; +import com.oracle.svm.hosted.substitute.DeletedElementException; +import com.oracle.svm.util.ReflectionUtil; +import com.oracle.svm.util.TypeResult; + +import jdk.graal.compiler.nodes.ConstantNode; +import jdk.graal.compiler.nodes.ValueNode; +import jdk.graal.compiler.nodes.graphbuilderconf.ClassInitializationPlugin; +import jdk.graal.compiler.nodes.graphbuilderconf.GraphBuilderConfiguration; +import jdk.graal.compiler.nodes.graphbuilderconf.GraphBuilderContext; +import jdk.graal.compiler.nodes.graphbuilderconf.InvocationPlugin; +import jdk.graal.compiler.nodes.graphbuilderconf.InvocationPlugins; +import jdk.graal.compiler.options.Option; +import jdk.graal.compiler.options.OptionStability; +import jdk.graal.compiler.phases.util.Providers; +import jdk.vm.ci.meta.JavaConstant; +import jdk.vm.ci.meta.JavaKind; +import jdk.vm.ci.meta.MetaAccessProvider; +import jdk.vm.ci.meta.ResolvedJavaMethod; + +@AutomaticallyRegisteredFeature +public class StrictDynamicAccessInferenceFeature implements InternalFeature { + + public static class Options { + + public enum Mode { + Disable, + Warn, + Enforce + } + + @Option(help = """ + Select the mode for the strict, build-time inference for invocations requiring dynamic access. + Possible values are: + "Disable" (default): Disable the strict mode and fall back to the optimization dependent inference for dynamic invocations; + "Warn": Use optimization dependent inference for dynamic invocations, but print a warning for invocations inferred outside of the strict mode; + "Enforce": Infer only dynamic invocations proven to be constant in the strict inference mode.""", stability = OptionStability.EXPERIMENTAL)// + public static final HostedOptionKey StrictDynamicAccessInference = new HostedOptionKey<>(Mode.Disable); + } + + public static boolean isDisabled() { + return Options.StrictDynamicAccessInference.getValue() == Options.Mode.Disable; + } + + public static boolean isEnforced() { + return Options.StrictDynamicAccessInference.getValue() == Options.Mode.Enforce; + } + + @Override + public boolean isInConfiguration(IsInConfigurationAccess access) { + return !isDisabled(); + } + + @Override + public void afterRegistration(AfterRegistrationAccess access) { + FeatureImpl.AfterRegistrationAccessImpl accessImpl = (FeatureImpl.AfterRegistrationAccessImpl) access; + + ConstantExpressionAnalyzer analyzer = new ConstantExpressionAnalyzer(GraalAccess.getOriginalProviders(), accessImpl.getImageClassLoader()); + ConstantExpressionRegistry registry = new ConstantExpressionRegistry(); + StrictDynamicAccessInferenceSupport support = new StrictDynamicAccessInferenceSupport(analyzer, registry); + + ImageSingletons.add(ConstantExpressionRegistry.class, registry); + ImageSingletons.add(StrictDynamicAccessInferenceSupport.class, support); + } + + @Override + public void beforeAnalysis(BeforeAnalysisAccess access) { + /* + * The strict dynamic access inference mode disables constant folding through method + * inlining, which leads to of sun.nio.ch.DatagramChannelImpl throwing a missing + * reflection registration error. This is a temporary fix until annotation guided analysis + * is implemented. + * + * An alternative to this approach would be creating invocation plugins for the methods + * defined in jdk.internal.invoke.MhUtil. + */ + registerFieldForReflectionIfExists(access, "sun.nio.ch.DatagramChannelImpl", "socket"); + } + + @SuppressWarnings("SameParameterValue") + private static void registerFieldForReflectionIfExists(BeforeAnalysisAccess access, String className, String fieldName) { + Class clazz = ReflectionUtil.lookupClass(true, className); + if (clazz == null) { + return; + } + Field field = ReflectionUtil.lookupField(true, clazz, fieldName); + if (field == null) { + return; + } + access.registerReachabilityHandler(a -> RuntimeReflection.register(field), clazz); + } + + @Override + public void afterAnalysis(AfterAnalysisAccess access) { + /* + * No more bytecode parsing should happen after analysis, so we can seal and clean up the + * registry. + */ + ConstantExpressionRegistry.singleton().seal(); + } +} + +@AutomaticallyRegisteredFeature +class StrictProxyInferenceFeature implements InternalFeature { + + private ConstantExpressionRegistry registry; + private DynamicAccessInferenceLog inferenceLog; + + @Override + public List> getRequiredFeatures() { + return List.of(StrictDynamicAccessInferenceFeature.class); + } + + @Override + public boolean isInConfiguration(IsInConfigurationAccess access) { + return StrictDynamicAccessInferenceFeature.isEnforced(); + } + + @Override + public void duringSetup(DuringSetupAccess access) { + registry = ConstantExpressionRegistry.singleton(); + inferenceLog = ImageSingletons.contains(DynamicAccessInferenceLog.class) ? DynamicAccessInferenceLog.singleton() : null; + } + + @Override + public void registerInvocationPlugins(Providers providers, GraphBuilderConfiguration.Plugins plugins, ParsingReason reason) { + if (reason != ParsingReason.PointsToAnalysis) { + return; + } + + InvocationPlugins invocationPlugins = plugins.getInvocationPlugins(); + + Method getProxyClass = ReflectionUtil.lookupMethod(Proxy.class, "getProxyClass", ClassLoader.class, Class[].class); + Method newProxyInstance = ReflectionUtil.lookupMethod(Proxy.class, "newProxyInstance", ClassLoader.class, Class[].class, InvocationHandler.class); + + for (Method method : List.of(getProxyClass, newProxyInstance)) { + invocationPlugins.register(method.getDeclaringClass(), new InvocationPlugin.RequiredInvocationPlugin(method.getName(), method.getParameterTypes()) { + @Override + public boolean isDecorator() { + return true; + } + + @Override + public boolean defaultHandler(GraphBuilderContext b, ResolvedJavaMethod targetMethod, Receiver receiver, ValueNode... args) { + Class[] interfaces = registry.getArgument(b.getMethod(), b.bci(), targetMethod, 1, Class[].class); + return registerProxy(b, reason, targetMethod, interfaces); + } + }); + } + } + + private boolean registerProxy(GraphBuilderContext b, ParsingReason reason, ResolvedJavaMethod targetMethod, Class[] interfaces) { + if (interfaces == null) { + return false; + } + + b.add(ReachabilityRegistrationNode.create(() -> RuntimeProxyCreation.register(interfaces), reason)); + if (inferenceLog != null) { + Object ignore = DynamicAccessInferenceLog.ignoreArgument(); + Object[] args = targetMethod.getParameters().length == 2 + ? new Object[]{ignore, interfaces} + : new Object[]{ignore, interfaces, ignore}; + inferenceLog.logRegistration(b, reason, targetMethod, null, args); + } + return true; + } +} + +@AutomaticallyRegisteredFeature +class StrictReflectionInferenceFeature implements InternalFeature { + + private ConstantExpressionRegistry registry; + private ImageClassLoader imageClassLoader; + private AnalysisUniverse analysisUniverse; + private DynamicAccessInferenceLog inferenceLog; + + @Override + public List> getRequiredFeatures() { + return List.of(StrictDynamicAccessInferenceFeature.class); + } + + @Override + public boolean isInConfiguration(IsInConfigurationAccess access) { + return StrictDynamicAccessInferenceFeature.isEnforced(); + } + + @Override + public void duringSetup(DuringSetupAccess access) { + FeatureImpl.DuringSetupAccessImpl accessImpl = (FeatureImpl.DuringSetupAccessImpl) access; + registry = ConstantExpressionRegistry.singleton(); + imageClassLoader = accessImpl.getImageClassLoader(); + analysisUniverse = accessImpl.getUniverse(); + inferenceLog = ImageSingletons.contains(DynamicAccessInferenceLog.class) ? DynamicAccessInferenceLog.singleton() : null; + } + + @Override + public void registerInvocationPlugins(Providers providers, GraphBuilderConfiguration.Plugins plugins, ParsingReason reason) { + if (reason != ParsingReason.PointsToAnalysis) { + return; + } + + InvocationPlugins invocationPlugins = plugins.getInvocationPlugins(); + ClassInitializationPlugin initializationPlugin = plugins.getClassInitializationPlugin(); + + invocationPlugins.register(Class.class, new InvocationPlugin.RequiredInlineOnlyInvocationPlugin("forName", String.class) { + @Override + public boolean defaultHandler(GraphBuilderContext b, ResolvedJavaMethod targetMethod, Receiver receiver, ValueNode... args) { + String className = registry.getArgument(b.getMethod(), b.bci(), targetMethod, 0, String.class); + ClassLoader classLoader = ClassForNameSupport.respectClassLoader() + ? OriginalClassProvider.getJavaClass(b.getMethod().getDeclaringClass()).getClassLoader() + : imageClassLoader.getClassLoader(); + return foldClassForName(initializationPlugin, b, reason, targetMethod, className, true, classLoader); + } + }); + + invocationPlugins.register(Class.class, new InvocationPlugin.RequiredInlineOnlyInvocationPlugin("forName", String.class, boolean.class, ClassLoader.class) { + @Override + public boolean defaultHandler(GraphBuilderContext b, ResolvedJavaMethod targetMethod, Receiver receiver, ValueNode... args) { + String className = registry.getArgument(b.getMethod(), b.bci(), targetMethod, 0, String.class); + Boolean initialize = registry.getArgument(b.getMethod(), b.bci(), targetMethod, 1, Boolean.class); + ClassLoader classLoader; + if (ClassForNameSupport.respectClassLoader()) { + Object loader = registry.getArgument(b.getMethod(), b.bci(), targetMethod, 2); + if (loader == null) { + return false; + } else if (ConstantExpressionRegistry.isNull(loader)) { + classLoader = null; + } else { + classLoader = (ClassLoader) loader; + } + } else { + classLoader = imageClassLoader.getClassLoader(); + } + return foldClassForName(initializationPlugin, b, reason, targetMethod, className, initialize, classLoader); + } + }); + + registerFoldInvocationPlugins(invocationPlugins, reason, Class.class, + "getField", "getDeclaredField", "getConstructor", + "getDeclaredConstructor", "getMethod", "getDeclaredMethod"); + + registerFoldInvocationPlugins(invocationPlugins, reason, MethodHandles.Lookup.class, + "findClass", "findVirtual", "findStatic", "findConstructor", + "findGetter", "findStaticGetter", "findSetter", "findStaticSetter", + "findVarHandle", "findStaticVarHandle", "findSpecial"); + + registerBulkQueryRegistrationPlugin(invocationPlugins, reason, "getClasses", RuntimeReflection::registerAllClasses); + registerBulkQueryRegistrationPlugin(invocationPlugins, reason, "getDeclaredClasses", RuntimeReflection::registerAllDeclaredClasses); + registerBulkQueryRegistrationPlugin(invocationPlugins, reason, "getConstructors", RuntimeReflection::registerAllConstructors); + registerBulkQueryRegistrationPlugin(invocationPlugins, reason, "getDeclaredConstructors", RuntimeReflection::registerAllDeclaredConstructors); + registerBulkQueryRegistrationPlugin(invocationPlugins, reason, "getFields", RuntimeReflection::registerAllFields); + registerBulkQueryRegistrationPlugin(invocationPlugins, reason, "getDeclaredFields", RuntimeReflection::registerAllDeclaredFields); + registerBulkQueryRegistrationPlugin(invocationPlugins, reason, "getMethods", RuntimeReflection::registerAllMethods); + registerBulkQueryRegistrationPlugin(invocationPlugins, reason, "getDeclaredMethods", RuntimeReflection::registerAllDeclaredMethods); + registerBulkQueryRegistrationPlugin(invocationPlugins, reason, "getNestMembers", RuntimeReflection::registerAllNestMembers); + registerBulkQueryRegistrationPlugin(invocationPlugins, reason, "getPermittedSubclasses", RuntimeReflection::registerAllPermittedSubclasses); + registerBulkQueryRegistrationPlugin(invocationPlugins, reason, "getRecordComponents", RuntimeReflection::registerAllRecordComponents); + registerBulkQueryRegistrationPlugin(invocationPlugins, reason, "getSigners", RuntimeReflection::registerAllSigners); + } + + private void registerFoldInvocationPlugins(InvocationPlugins plugins, ParsingReason reason, Class declaringClass, String... methodNames) { + Set methodNamesSet = Set.of(methodNames); + Arrays.stream(declaringClass.getDeclaredMethods()) + .filter(m -> methodNamesSet.contains(m.getName()) && !m.isSynthetic()) + .forEach(m -> registerFoldInvocationPlugin(plugins, reason, m)); + } + + private void registerFoldInvocationPlugin(InvocationPlugins plugins, ParsingReason reason, Method method) { + List> parameterTypes = new ArrayList<>(); + if (!Modifier.isStatic(method.getModifiers())) { + parameterTypes.add(InvocationPlugin.Receiver.class); + } + parameterTypes.addAll(Arrays.asList(method.getParameterTypes())); + + plugins.register(method.getDeclaringClass(), new InvocationPlugin.RequiredInvocationPlugin(method.getName(), parameterTypes.toArray(new Class[0])) { + @Override + public boolean defaultHandler(GraphBuilderContext b, ResolvedJavaMethod targetMethod, Receiver receiver, ValueNode... args) { + Object receiverValue = targetMethod.hasReceiver() ? registry.getReceiver(b.getMethod(), b.bci(), targetMethod) : null; + Object[] arguments = getArgumentsFromRegistry(b, targetMethod); + return foldInvocationUsingReflection(b, reason, targetMethod, method, receiverValue, arguments); + } + }); + } + + private void registerBulkQueryRegistrationPlugin(InvocationPlugins plugins, ParsingReason reason, String methodName, Consumer> registrationCallback) { + plugins.register(Class.class, new InvocationPlugin.RequiredInvocationPlugin(methodName, new Class[]{InvocationPlugin.Receiver.class}) { + @Override + public boolean isDecorator() { + return true; + } + + @Override + public boolean apply(GraphBuilderContext b, ResolvedJavaMethod targetMethod, Receiver receiver) { + Class clazz = registry.getReceiver(b.getMethod(), b.bci(), targetMethod, Class.class); + return registerBulkQuery(b, reason, targetMethod, clazz, registrationCallback); + } + }); + } + + private boolean registerBulkQuery(GraphBuilderContext b, ParsingReason reason, ResolvedJavaMethod targetMethod, Class clazz, Consumer> registrationCallback) { + if (clazz == null) { + return false; + } + + b.add(ReachabilityRegistrationNode.create(() -> { + try { + registrationCallback.accept(clazz); + } catch (LinkageError e) { + // Ignore + } + }, reason)); + if (inferenceLog != null) { + inferenceLog.logRegistration(b, reason, targetMethod, clazz, new Object[]{}); + } + return true; + } + + private boolean foldClassForName(ClassInitializationPlugin initializationPlugin, GraphBuilderContext b, ParsingReason reason, ResolvedJavaMethod targetMethod, String className, Boolean initialize, + ClassLoader classLoader) { + if (className == null || initialize == null) { + return false; + } + + Object[] argValues = targetMethod.getParameters().length == 1 + ? new Object[]{className} + : new Object[]{className, initialize, ClassForNameSupport.respectClassLoader() ? classLoader : DynamicAccessInferenceLog.ignoreArgument()}; + + TypeResult> type = ImageClassLoader.findClass(className, false, classLoader); + if (!type.isPresent()) { + if (RuntimeClassLoading.isSupported()) { + return false; + } + Throwable e = type.getException(); + return throwException(b, reason, targetMethod, null, argValues, e.getClass(), e.getMessage()); + } + + Class clazz = type.get(); + if (PredefinedClassesSupport.isPredefined(clazz)) { + return false; + } + + JavaConstant classConstant = pushConstant(b, reason, targetMethod, null, argValues, clazz); + if (classConstant == null) { + return false; + } + + if (initialize) { + initializationPlugin.apply(b, b.getMetaAccess().lookupJavaType(clazz), () -> null); + } + + return true; + } + + private boolean foldInvocationUsingReflection(GraphBuilderContext b, ParsingReason reason, ResolvedJavaMethod targetMethod, Method reflectionMethod, Object receiverValue, Object[] argValues) { + boolean isStatic = Modifier.isStatic(reflectionMethod.getModifiers()); + if (isStatic && (receiverValue == null || ConstantExpressionRegistry.isNull(receiverValue)) || argValues == null) { + return false; + } + + Object returnValue; + try { + returnValue = reflectionMethod.invoke(receiverValue, argValues); + } catch (InvocationTargetException e) { + return throwException(b, reason, targetMethod, receiverValue, argValues, e.getTargetException().getClass(), e.getTargetException().getMessage()); + } catch (Throwable e) { + return throwException(b, reason, targetMethod, receiverValue, argValues, e.getClass(), e.getMessage()); + } + + return pushConstant(b, reason, targetMethod, returnValue, argValues, returnValue) != null; + } + + private JavaConstant pushConstant(GraphBuilderContext b, ParsingReason reason, ResolvedJavaMethod targetMethod, Object receiver, Object[] arguments, Object returnValue) { + Object intrinsicValue = getIntrinsic(b, returnValue); + if (intrinsicValue == null) { + return null; + } + + JavaKind returnKind = targetMethod.getSignature().getReturnKind(); + + JavaConstant intrinsicConstant; + if (returnKind.isPrimitive()) { + intrinsicConstant = JavaConstant.forBoxedPrimitive(intrinsicValue); + } else if (ConstantExpressionRegistry.isNull(returnValue)) { + intrinsicConstant = JavaConstant.NULL_POINTER; + } else { + intrinsicConstant = b.getSnippetReflection().forObject(intrinsicValue); + } + + b.addPush(returnKind, ConstantNode.forConstant(intrinsicConstant, b.getMetaAccess())); + if (inferenceLog != null) { + inferenceLog.logConstant(b, reason, targetMethod, receiver, arguments, returnValue); + } + return intrinsicConstant; + } + + private boolean throwException(GraphBuilderContext b, ParsingReason reason, ResolvedJavaMethod targetMethod, Object receiver, Object[] arguments, Class exceptionClass, + String message) { + /* Get the exception throwing method that has a message parameter. */ + Method exceptionMethod = ExceptionSynthesizer.throwExceptionMethodOrNull(exceptionClass, String.class); + if (exceptionMethod == null) { + return false; + } + Method intrinsic = getIntrinsic(b, exceptionMethod); + if (intrinsic == null) { + return false; + } + + if (inferenceLog != null) { + inferenceLog.logException(b, reason, targetMethod, receiver, arguments, exceptionClass); + } + + ExceptionSynthesizer.throwException(b, exceptionMethod, message); + return true; + } + + @SuppressWarnings("unchecked") + private T getIntrinsic(GraphBuilderContext context, T element) { + if (isDeleted(element, context.getMetaAccess())) { + /* + * Should not intrinsify. Will fail during the reflective lookup at runtime. @Delete-ed + * elements are ignored by the reflection plugins regardless of the value of + * ReportUnsupportedElementsAtRuntime. + */ + return null; + } + return (T) analysisUniverse.replaceObject(element); + } + + private static boolean isDeleted(T element, MetaAccessProvider metaAccess) { + AnnotatedElement annotated = null; + try { + if (element instanceof Executable) { + annotated = metaAccess.lookupJavaMethod((Executable) element); + } else if (element instanceof Field) { + annotated = metaAccess.lookupJavaField((Field) element); + } + } catch (DeletedElementException ex) { + /* + * If ReportUnsupportedElementsAtRuntime is *not* set looking up a @Delete-ed element + * will result in a DeletedElementException. + */ + return true; + } + /* + * If ReportUnsupportedElementsAtRuntime is set looking up a @Delete-ed element will return + * a substitution method that has the @Delete annotation. + */ + return annotated != null && annotated.isAnnotationPresent(Delete.class); + } + + private Object[] getArgumentsFromRegistry(GraphBuilderContext b, ResolvedJavaMethod targetMethod) { + Object[] argValues = new Object[targetMethod.getSignature().getParameterCount(false)]; + for (int i = 0; i < argValues.length; i++) { + argValues[i] = registry.getArgument(b.getMethod(), b.bci(), targetMethod, i); + if (argValues[i] == null) { + return null; + } else if (ConstantExpressionRegistry.isNull(argValues[i])) { + argValues[i] = null; + } + } + return argValues; + } +} + +@AutomaticallyRegisteredFeature +class StrictResourceInferenceFeature implements InternalFeature { + + private ConstantExpressionRegistry registry; + private DynamicAccessInferenceLog inferenceLog; + + @Override + public List> getRequiredFeatures() { + return List.of(StrictDynamicAccessInferenceFeature.class); + } + + @Override + public boolean isInConfiguration(IsInConfigurationAccess access) { + return StrictDynamicAccessInferenceFeature.isEnforced(); + } + + @Override + public void duringSetup(DuringSetupAccess access) { + registry = ConstantExpressionRegistry.singleton(); + inferenceLog = ImageSingletons.contains(DynamicAccessInferenceLog.class) ? DynamicAccessInferenceLog.singleton() : null; + } + + @Override + public void registerInvocationPlugins(Providers providers, GraphBuilderConfiguration.Plugins plugins, ParsingReason reason) { + if (reason != ParsingReason.PointsToAnalysis) { + return; + } + + InvocationPlugins invocationPlugins = plugins.getInvocationPlugins(); + Method resolveResourceName = ReflectionUtil.lookupMethod(Class.class, "resolveName", String.class); + + Method getResource = ReflectionUtil.lookupMethod(Class.class, "getResource", String.class); + Method getResourceAsStream = ReflectionUtil.lookupMethod(Class.class, "getResourceAsStream", String.class); + + for (Method method : List.of(getResource, getResourceAsStream)) { + List> parameterTypes = new ArrayList<>(); + parameterTypes.add(InvocationPlugin.Receiver.class); + parameterTypes.addAll(Arrays.asList(method.getParameterTypes())); + invocationPlugins.register(method.getDeclaringClass(), new InvocationPlugin.RequiredInvocationPlugin(method.getName(), parameterTypes.toArray(new Class[0])) { + @Override + public boolean isDecorator() { + return true; + } + + @Override + public boolean defaultHandler(GraphBuilderContext b, ResolvedJavaMethod targetMethod, Receiver receiver, ValueNode... args) { + Class clazz = registry.getReceiver(b.getMethod(), b.bci(), targetMethod, Class.class); + String resource = registry.getArgument(b.getMethod(), b.bci(), targetMethod, 0, String.class); + return registerResource(b, reason, targetMethod, clazz, resource, resolveResourceName); + } + }); + } + } + + private boolean registerResource(GraphBuilderContext b, ParsingReason reason, ResolvedJavaMethod targetMethod, Class clazz, String resource, Method resolveResourceName) { + if (clazz == null || resource == null) { + return false; + } + + String resourceName; + try { + resourceName = (String) resolveResourceName.invoke(clazz, resource); + } catch (ReflectiveOperationException e) { + throw VMError.shouldNotReachHere(e); + } + b.add(ReachabilityRegistrationNode.create(() -> RuntimeResourceAccess.addResource(clazz.getModule(), resourceName), reason)); + if (inferenceLog != null) { + inferenceLog.logRegistration(b, reason, targetMethod, clazz, new String[]{resource}); + } + return true; + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/dynamicaccessinference/StrictDynamicAccessInferenceSupport.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/dynamicaccessinference/StrictDynamicAccessInferenceSupport.java new file mode 100644 index 000000000000..a7df010cb5c0 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/dynamicaccessinference/StrictDynamicAccessInferenceSupport.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2025, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.hosted.dynamicaccessinference; + +import org.graalvm.nativeimage.ImageSingletons; + +import jdk.graal.compiler.nodes.graphbuilderconf.IntrinsicContext; +import jdk.vm.ci.meta.ResolvedJavaMethod; + +public class StrictDynamicAccessInferenceSupport { + + public static StrictDynamicAccessInferenceSupport singleton() { + return ImageSingletons.lookup(StrictDynamicAccessInferenceSupport.class); + } + + private final ConstantExpressionAnalyzer analyzer; + private final ConstantExpressionRegistry registry; + + StrictDynamicAccessInferenceSupport(ConstantExpressionAnalyzer analyzer, ConstantExpressionRegistry registry) { + this.analyzer = analyzer; + this.registry = registry; + } + + public void analyze(ResolvedJavaMethod method, IntrinsicContext intrinsicContext) { + registry.analyzeAndStore(analyzer, method, intrinsicContext); + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/phases/AnalysisGraphBuilderPhase.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/phases/AnalysisGraphBuilderPhase.java index ed5a4290ba1b..5e46dcd94e95 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/phases/AnalysisGraphBuilderPhase.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/phases/AnalysisGraphBuilderPhase.java @@ -35,6 +35,7 @@ import com.oracle.svm.core.bootstrap.BootstrapMethodConfiguration; import com.oracle.svm.hosted.SVMHost; import com.oracle.svm.hosted.code.SubstrateCompilationDirectives; +import com.oracle.svm.hosted.dynamicaccessinference.StrictDynamicAccessInferenceSupport; import com.oracle.svm.util.ModuleSupport; import jdk.graal.compiler.core.common.type.StampFactory; @@ -106,6 +107,15 @@ protected AnalysisBytecodeParser(GraphBuilderPhase.Instance graphBuilderInstance this.hostVM = hostVM; } + @Override + protected void build(FixedWithNextNode startInstruction, FrameStateBuilder startFrameState) { + StrictDynamicAccessInferenceSupport dynamicAccessInferenceSupport = hostVM.getStrictDynamicAccessInferenceSupport(); + if (strictDynamicAccessInferenceIsApplicable() && dynamicAccessInferenceSupport != null) { + dynamicAccessInferenceSupport.analyze(method, intrinsicContext); + } + super.build(startInstruction, startFrameState); + } + @Override protected boolean tryInvocationPlugin(InvokeKind invokeKind, ValueNode[] args, ResolvedJavaMethod targetMethod, JavaKind resultType) { boolean result = super.tryInvocationPlugin(invokeKind, args, targetMethod, resultType); @@ -304,5 +314,9 @@ protected FrameStateBuilder createFrameStateForExceptionHandling(int bci) { } return dispatchState; } + + protected boolean strictDynamicAccessInferenceIsApplicable() { + return true; + } } } diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/snippets/ReflectionPlugins.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/snippets/ReflectionPlugins.java index 3454447cf0ae..ed89fa88aac8 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/snippets/ReflectionPlugins.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/snippets/ReflectionPlugins.java @@ -40,13 +40,9 @@ import java.util.Arrays; import java.util.HashSet; import java.util.List; -import java.util.Objects; import java.util.Set; import java.util.function.Consumer; import java.util.function.Predicate; -import java.util.function.Supplier; -import java.util.stream.Collectors; -import java.util.stream.Stream; import com.oracle.svm.core.TrackDynamicAccessEnabled; import com.oracle.svm.hosted.DynamicAccessDetectionFeature; @@ -71,6 +67,8 @@ import com.oracle.svm.hosted.ImageClassLoader; import com.oracle.svm.hosted.ReachabilityRegistrationNode; import com.oracle.svm.hosted.classinitialization.ClassInitializationSupport; +import com.oracle.svm.hosted.dynamicaccessinference.DynamicAccessInferenceLog; +import com.oracle.svm.hosted.dynamicaccessinference.StrictDynamicAccessInferenceFeature; import com.oracle.svm.hosted.substitute.AnnotationSubstitutionProcessor; import com.oracle.svm.hosted.substitute.DeletedElementException; import com.oracle.svm.util.ModuleSupport; @@ -127,6 +125,7 @@ static class Options { private final ClassInitializationSupport classInitializationSupport; private final boolean trackDynamicAccess; private final DynamicAccessDetectionFeature dynamicAccessDetectionFeature; + private final DynamicAccessInferenceLog inferenceLog; private ReflectionPlugins(ImageClassLoader imageClassLoader, AnnotationSubstitutionProcessor annotationSubstitutions, ClassInitializationPlugin classInitializationPlugin, AnalysisUniverse aUniverse, ParsingReason reason, FallbackFeature fallbackFeature) { @@ -141,6 +140,8 @@ private ReflectionPlugins(ImageClassLoader imageClassLoader, AnnotationSubstitut dynamicAccessDetectionFeature = trackDynamicAccess ? DynamicAccessDetectionFeature.instance() : null; this.classInitializationSupport = (ClassInitializationSupport) ImageSingletons.lookup(RuntimeClassInitializationSupport.class); + + this.inferenceLog = ImageSingletons.contains(DynamicAccessInferenceLog.class) ? DynamicAccessInferenceLog.singleton() : null; } public static void registerInvocationPlugins(ImageClassLoader imageClassLoader, AnnotationSubstitutionProcessor annotationSubstitutions, @@ -150,6 +151,16 @@ public static void registerInvocationPlugins(ImageClassLoader imageClassLoader, rp.registerClassPlugins(plugins); } + /** + * Used to check if invocation inference should not be handled by + * {@link StrictDynamicAccessInferenceFeature}. + * + * @return {@code true} if the inference should be unrestricted. + */ + private boolean nonStrictDynamicAccessInference() { + return !(StrictDynamicAccessInferenceFeature.isEnforced() && reason == ParsingReason.PointsToAnalysis); + } + private static final Class VAR_FORM_CLASS = ReflectionUtil.lookupClass(false, "java.lang.invoke.VarForm"); private static final Class MEMBER_NAME_CLASS = ReflectionUtil.lookupClass(false, "java.lang.invoke.MemberName"); @@ -176,32 +187,35 @@ public static void registerInvocationPlugins(ImageClassLoader imageClassLoader, private void registerMethodHandlesPlugins(InvocationPlugins plugins) { for (Class clazz : List.of(Boolean.class, Byte.class, Short.class, Character.class, Integer.class, Long.class, Float.class, Double.class)) { - registerFoldInvocationPlugins(plugins, clazz, "toString", "toBinaryString", "toOctalString", "toHexString"); + registerFoldInvocationPlugins(plugins, false, clazz, "toString", "toBinaryString", "toOctalString", "toHexString"); } - registerFoldInvocationPlugins(plugins, String.class, "valueOf"); + registerFoldInvocationPlugins(plugins, false, String.class, "valueOf"); - registerFoldInvocationPlugins(plugins, MethodHandles.class, + registerFoldInvocationPlugins(plugins, false, MethodHandles.class, "publicLookup", "privateLookupIn", "arrayConstructor", "arrayLength", "arrayElementGetter", "arrayElementSetter", "arrayElementVarHandle", "byteArrayViewVarHandle", "byteBufferViewVarHandle"); - registerFoldInvocationPlugins(plugins, MethodHandles.Lookup.class, - "in", - "findStatic", "findVirtual", "findConstructor", "findClass", "accessClass", "findSpecial", - "findGetter", "findSetter", "findVarHandle", - "findStaticGetter", "findStaticSetter", + registerFoldInvocationPlugins(plugins, false, MethodHandles.Lookup.class, + "in", "accessClass", "unreflect", "unreflectSpecial", "unreflectConstructor", "unreflectGetter", "unreflectSetter"); - registerFoldInvocationPlugins(plugins, MethodType.class, + if (nonStrictDynamicAccessInference()) { + registerFoldInvocationPlugins(plugins, true, MethodHandles.Lookup.class, + "findStatic", "findVirtual", "findConstructor", "findClass", "findSpecial", + "findGetter", "findSetter", "findVarHandle", "findStaticGetter", "findStaticSetter"); + } + + registerFoldInvocationPlugins(plugins, false, MethodType.class, "methodType", "genericMethodType", "changeParameterType", "insertParameterTypes", "appendParameterTypes", "replaceParameterTypes", "dropParameterTypes", "changeReturnType", "erase", "generic", "wrap", "unwrap", "parameterType", "parameterCount", "returnType", "lastParameterType"); - registerFoldInvocationPlugins(plugins, MethodHandle.class, "asType"); + registerFoldInvocationPlugins(plugins, false, MethodHandle.class, "asType"); - registerFoldInvocationPlugins(plugins, VAR_FORM_CLASS, "resolveMemberName"); + registerFoldInvocationPlugins(plugins, false, VAR_FORM_CLASS, "resolveMemberName"); registerConditionalFoldInvocationPlugins(plugins); @@ -252,24 +266,26 @@ private static ResolvedJavaField findField(ResolvedJavaType type, String name) { * about the reflection API methods implementation. */ private void registerConditionalFoldInvocationPlugins(InvocationPlugins plugins) { - Method methodHandlesLookupFindStaticVarHandle = ReflectionUtil.lookupMethod(MethodHandles.Lookup.class, "findStaticVarHandle", Class.class, String.class, Class.class); - registerFoldInvocationPlugin(plugins, methodHandlesLookupFindStaticVarHandle, (args) -> { - /* VarHandles.makeFieldHandle() triggers init of receiver class (JDK-8291065). */ - Object classArg = args[0]; - if (classArg instanceof Class) { - if (!classInitializationSupport.maybeInitializeAtBuildTime((Class) classArg)) { - /* Skip the folding and register the field for run time reflection. */ - if (reason.duringAnalysis()) { - Field field = ReflectionUtil.lookupField(true, (Class) args[0], (String) args[1]); - if (field != null) { - RuntimeReflection.register(field); + if (nonStrictDynamicAccessInference()) { + Method methodHandlesLookupFindStaticVarHandle = ReflectionUtil.lookupMethod(MethodHandles.Lookup.class, "findStaticVarHandle", Class.class, String.class, Class.class); + registerFoldInvocationPlugin(plugins, methodHandlesLookupFindStaticVarHandle, (args) -> { + /* VarHandles.makeFieldHandle() triggers init of receiver class (JDK-8291065). */ + Object classArg = args[0]; + if (classArg instanceof Class) { + if (!classInitializationSupport.maybeInitializeAtBuildTime((Class) classArg)) { + /* Skip the folding and register the field for run time reflection. */ + if (reason.duringAnalysis()) { + Field field = ReflectionUtil.lookupField(true, (Class) args[0], (String) args[1]); + if (field != null) { + RuntimeReflection.register(field); + } } + return false; } - return false; } - } - return true; - }); + return true; + }, true); + } Method methodHandlesLookupUnreflectVarHandle = ReflectionUtil.lookupMethod(MethodHandles.Lookup.class, "unreflectVarHandle", Field.class); registerFoldInvocationPlugin(plugins, methodHandlesLookupUnreflectVarHandle, (args) -> { @@ -289,13 +305,15 @@ private void registerConditionalFoldInvocationPlugins(InvocationPlugins plugins) } } return true; - }); + }, false); } private void registerClassPlugins(InvocationPlugins plugins) { - registerFoldInvocationPlugins(plugins, Class.class, - "getField", "getMethod", "getConstructor", - "getDeclaredField", "getDeclaredMethod", "getDeclaredConstructor"); + if (nonStrictDynamicAccessInference()) { + registerFoldInvocationPlugins(plugins, true, Class.class, + "getField", "getMethod", "getConstructor", + "getDeclaredField", "getDeclaredMethod", "getDeclaredConstructor"); + } /* * The class sun.nio.ch.Reflect contains various reflection lookup methods that then pass @@ -303,10 +321,10 @@ private void registerClassPlugins(InvocationPlugins plugins) { * things like calling setAccessible(true), so method inlining before analysis cannot * constant-fold them automatically. So we register them manually here for folding too. */ - registerFoldInvocationPlugins(plugins, ReflectionUtil.lookupClass(false, "sun.nio.ch.Reflect"), + registerFoldInvocationPlugins(plugins, false, ReflectionUtil.lookupClass(false, "sun.nio.ch.Reflect"), "lookupConstructor", "lookupMethod", "lookupField"); - if (MissingRegistrationUtils.throwMissingRegistrationErrors() && reason.duringAnalysis() && reason != ParsingReason.JITCompilation) { + if (nonStrictDynamicAccessInference() && MissingRegistrationUtils.throwMissingRegistrationErrors() && reason.duringAnalysis() && reason != ParsingReason.JITCompilation) { registerBulkInvocationPlugin(plugins, Class.class, "getClasses", RuntimeReflection::registerAllClasses); registerBulkInvocationPlugin(plugins, Class.class, "getDeclaredClasses", RuntimeReflection::registerAllDeclaredClasses); registerBulkInvocationPlugin(plugins, Class.class, "getConstructors", RuntimeReflection::registerAllConstructors); @@ -322,47 +340,49 @@ private void registerClassPlugins(InvocationPlugins plugins) { } Registration r = new Registration(plugins, Class.class); - r.register(new RequiredInlineOnlyInvocationPlugin("forName", String.class) { - @Override - public boolean apply(GraphBuilderContext b, ResolvedJavaMethod targetMethod, Receiver receiver, ValueNode nameNode) { - ClassLoader loader; - if (ClassForNameSupport.respectClassLoader()) { - Class callerClass = OriginalClassProvider.getJavaClass(b.getMethod().getDeclaringClass()); - loader = callerClass.getClassLoader(); - } else { - loader = imageClassLoader.getClassLoader(); - } - return processClassForName(b, targetMethod, nameNode, ConstantNode.forBoolean(true), loader); - } - }); - r.register(new RequiredInlineOnlyInvocationPlugin("forName", String.class, boolean.class, ClassLoader.class) { - @Override - public boolean apply(GraphBuilderContext b, ResolvedJavaMethod targetMethod, Receiver receiver, ValueNode nameNode, ValueNode initializeNode, ValueNode classLoaderNode) { - ClassLoader loader; - if (ClassForNameSupport.respectClassLoader()) { - if (!classLoaderNode.isJavaConstant()) { - return false; + if (nonStrictDynamicAccessInference()) { + r.register(new RequiredInlineOnlyInvocationPlugin("forName", String.class) { + @Override + public boolean apply(GraphBuilderContext b, ResolvedJavaMethod targetMethod, Receiver receiver, ValueNode nameNode) { + ClassLoader loader; + if (ClassForNameSupport.respectClassLoader()) { + Class callerClass = OriginalClassProvider.getJavaClass(b.getMethod().getDeclaringClass()); + loader = callerClass.getClassLoader(); + } else { + loader = imageClassLoader.getClassLoader(); } - loader = (ClassLoader) unboxObjectConstant(b, classLoaderNode.asJavaConstant()); - if (loader == NativeImageSystemClassLoader.singleton().defaultSystemClassLoader) { + return processClassForName(b, targetMethod, nameNode, ConstantNode.forBoolean(true), loader); + } + }); + r.register(new RequiredInlineOnlyInvocationPlugin("forName", String.class, boolean.class, ClassLoader.class) { + @Override + public boolean apply(GraphBuilderContext b, ResolvedJavaMethod targetMethod, Receiver receiver, ValueNode nameNode, ValueNode initializeNode, ValueNode classLoaderNode) { + ClassLoader loader; + if (ClassForNameSupport.respectClassLoader()) { + if (!classLoaderNode.isJavaConstant()) { + return false; + } + loader = (ClassLoader) unboxObjectConstant(b, classLoaderNode.asJavaConstant()); + if (loader == NativeImageSystemClassLoader.singleton().defaultSystemClassLoader) { + /* + * The run time's application class loader is the build time's image + * class loader. + */ + loader = imageClassLoader.getClassLoader(); + } + } else { /* - * The run time's application class loader is the build time's image class - * loader. + * When we ignore the ClassLoader parameter, we only intrinsify class names + * that are found by the ImageClassLoader, i.e., the application class + * loader at run time. We assume that every class loader used at run time + * delegates to the application class loader. */ loader = imageClassLoader.getClassLoader(); } - } else { - /* - * When we ignore the ClassLoader parameter, we only intrinsify class names that - * are found by the ImageClassLoader, i.e., the application class loader at run - * time. We assume that every class loader used at run time delegates to the - * application class loader. - */ - loader = imageClassLoader.getClassLoader(); + return processClassForName(b, targetMethod, nameNode, initializeNode, loader); } - return processClassForName(b, targetMethod, nameNode, initializeNode, loader); - } - }); + }); + } r.register(new RequiredInlineOnlyInvocationPlugin("getClassLoader", Receiver.class) { @Override public boolean apply(GraphBuilderContext b, ResolvedJavaMethod targetMethod, Receiver receiver) { @@ -381,8 +401,6 @@ public boolean apply(GraphBuilderContext b, ResolvedJavaMethod targetMethod, Rec * the constructor parameter. */ private boolean processMethodHandlesLookup(GraphBuilderContext b, ResolvedJavaMethod targetMethod) { - Supplier targetParameters = () -> ""; - if (StackTraceUtils.ignoredBySecurityStackWalk(b.getMetaAccess(), b.getMethod())) { /* * If our immediate caller (which is the only method available at the time the @@ -397,9 +415,9 @@ private boolean processMethodHandlesLookup(GraphBuilderContext b, ResolvedJavaMe /* The constructor of Lookup is not public, so we need to invoke it via reflection. */ lookup = LOOKUP_CONSTRUCTOR.newInstance(callerClass); } catch (Throwable ex) { - return throwException(b, targetMethod, targetParameters, ex.getClass(), ex.getMessage()); + return throwException(b, targetMethod, null, new Object[]{}, ex.getClass(), ex.getMessage(), false); } - return pushConstant(b, targetMethod, targetParameters, JavaKind.Object, lookup, false) != null; + return pushConstant(b, targetMethod, null, new Object[]{}, JavaKind.Object, lookup, false, false) != null; } /** @@ -416,7 +434,10 @@ private boolean processClassForName(GraphBuilderContext b, ResolvedJavaMethod ta } String className = (String) classNameValue; boolean initialize = (Boolean) initializeValue; - Supplier targetParameters = () -> className + ", " + initialize; + + Object[] arguments = targetMethod.getParameters().length == 1 + ? new Object[]{className} + : new Object[]{className, initialize, ClassForNameSupport.respectClassLoader() ? loader : DynamicAccessInferenceLog.ignoreArgument()}; TypeResult> typeResult = ImageClassLoader.findClass(className, false, loader); if (!typeResult.isPresent()) { @@ -424,14 +445,14 @@ private boolean processClassForName(GraphBuilderContext b, ResolvedJavaMethod ta return false; } Throwable e = typeResult.getException(); - return throwException(b, targetMethod, targetParameters, e.getClass(), e.getMessage()); + return throwException(b, targetMethod, null, arguments, e.getClass(), e.getMessage(), true); } Class clazz = typeResult.get(); if (PredefinedClassesSupport.isPredefined(clazz)) { return false; } - JavaConstant classConstant = pushConstant(b, targetMethod, targetParameters, JavaKind.Object, clazz, false); + JavaConstant classConstant = pushConstant(b, targetMethod, null, arguments, JavaKind.Object, clazz, false, true); if (classConstant == null) { return false; } @@ -469,7 +490,7 @@ private boolean processClassGetClassLoader(GraphBuilderContext b, ResolvedJavaMe if (result != null) { b.addPush(JavaKind.Object, ConstantNode.forConstant(result, b.getMetaAccess())); - traceConstant(b, targetMethod, clazz::getName, result); + traceConstant(b, targetMethod, clazz, new Object[]{}, result, false); return true; } @@ -481,23 +502,23 @@ private boolean processClassGetClassLoader(GraphBuilderContext b, ResolvedJavaMe * parameter types. It also simplifies handling of different JDK versions, because methods not * yet available in JDK 8 (like VarHandle methods) are silently ignored. */ - private void registerFoldInvocationPlugins(InvocationPlugins plugins, Class declaringClass, String... methodNames) { + private void registerFoldInvocationPlugins(InvocationPlugins plugins, boolean logAsInferredDynamicAccess, Class declaringClass, String... methodNames) { Set methodNamesSet = new HashSet<>(Arrays.asList(methodNames)); ModuleSupport.accessModuleByClass(ModuleSupport.Access.OPEN, ReflectionPlugins.class, declaringClass); for (Method method : declaringClass.getDeclaredMethods()) { if (methodNamesSet.contains(method.getName()) && !method.isSynthetic()) { - registerFoldInvocationPlugin(plugins, method); + registerFoldInvocationPlugin(plugins, method, logAsInferredDynamicAccess); } } } private static final Predicate alwaysAllowConstantFolding = args -> true; - private void registerFoldInvocationPlugin(InvocationPlugins plugins, Method reflectionMethod) { - registerFoldInvocationPlugin(plugins, reflectionMethod, alwaysAllowConstantFolding); + private void registerFoldInvocationPlugin(InvocationPlugins plugins, Method reflectionMethod, boolean logAsInferredDynamicAccess) { + registerFoldInvocationPlugin(plugins, reflectionMethod, alwaysAllowConstantFolding, logAsInferredDynamicAccess); } - private void registerFoldInvocationPlugin(InvocationPlugins plugins, Method reflectionMethod, Predicate allowConstantFolding) { + private void registerFoldInvocationPlugin(InvocationPlugins plugins, Method reflectionMethod, Predicate allowConstantFolding, boolean logAsInferredDynamicAccess) { if (!isAllowedReturnType(reflectionMethod.getReturnType())) { throw VMError.shouldNotReachHere("Return type of method " + reflectionMethod + " is not on the allow-list for types that are immutable"); } @@ -512,7 +533,7 @@ private void registerFoldInvocationPlugin(InvocationPlugins plugins, Method refl plugins.register(reflectionMethod.getDeclaringClass(), new RequiredInvocationPlugin(reflectionMethod.getName(), parameterTypes.toArray(new Class[0])) { @Override public boolean defaultHandler(GraphBuilderContext b, ResolvedJavaMethod targetMethod, Receiver receiver, ValueNode... args) { - return foldInvocationUsingReflection(b, targetMethod, reflectionMethod, receiver, args, allowConstantFolding); + return foldInvocationUsingReflection(b, targetMethod, reflectionMethod, receiver, args, allowConstantFolding, logAsInferredDynamicAccess); } }); } @@ -522,7 +543,7 @@ private static boolean isAllowedReturnType(Class returnType) { } private boolean foldInvocationUsingReflection(GraphBuilderContext b, ResolvedJavaMethod targetMethod, Method reflectionMethod, Receiver receiver, ValueNode[] args, - Predicate allowConstantFolding) { + Predicate allowConstantFolding, boolean logAsInferredDynamicAccess) { assert b.getMetaAccess().lookupJavaMethod(reflectionMethod).equals(targetMethod) : "Fold method mismatch: " + reflectionMethod + " != " + targetMethod; Object receiverValue; @@ -556,17 +577,13 @@ private boolean foldInvocationUsingReflection(GraphBuilderContext b, ResolvedJav return false; } - /* String representation of the parameters for debug printing. */ - Supplier targetParameters = () -> (receiverValue == null ? "" : receiverValue + "; ") + - Stream.of(argValues).map(arg -> arg instanceof Object[] ? Arrays.toString((Object[]) arg) : Objects.toString(arg)).collect(Collectors.joining(", ")); - Object returnValue; try { returnValue = reflectionMethod.invoke(receiverValue, argValues); } catch (InvocationTargetException ex) { - return throwException(b, targetMethod, targetParameters, ex.getTargetException().getClass(), ex.getTargetException().getMessage()); + return throwException(b, targetMethod, receiverValue, argValues, ex.getTargetException().getClass(), ex.getTargetException().getMessage(), logAsInferredDynamicAccess); } catch (Throwable ex) { - return throwException(b, targetMethod, targetParameters, ex.getClass(), ex.getMessage()); + return throwException(b, targetMethod, receiverValue, argValues, ex.getClass(), ex.getMessage(), logAsInferredDynamicAccess); } JavaKind returnKind = targetMethod.getSignature().getReturnKind(); @@ -574,11 +591,11 @@ private boolean foldInvocationUsingReflection(GraphBuilderContext b, ResolvedJav /* * The target method is a side-effect free void method that did not throw an exception. */ - traceConstant(b, targetMethod, targetParameters, JavaKind.Void); + traceConstant(b, targetMethod, receiverValue, argValues, JavaKind.Void, logAsInferredDynamicAccess); return true; } - return pushConstant(b, targetMethod, targetParameters, returnKind, returnValue, false) != null; + return pushConstant(b, targetMethod, receiverValue, argValues, returnKind, returnValue, false, logAsInferredDynamicAccess) != null; } private void registerBulkInvocationPlugin(InvocationPlugins plugins, Class declaringClass, String methodName, Consumer registrationCallback) { @@ -766,8 +783,8 @@ private static boolean isDeleted(T element, MetaAccessProvider metaAccess) { return annotated != null && annotated.isAnnotationPresent(Delete.class); } - private JavaConstant pushConstant(GraphBuilderContext b, ResolvedJavaMethod targetMethod, Supplier targetParameters, JavaKind returnKind, Object returnValue, - boolean allowNullReturnValue) { + private JavaConstant pushConstant(GraphBuilderContext b, ResolvedJavaMethod targetMethod, Object receiver, Object[] arguments, JavaKind returnKind, Object returnValue, + boolean allowNullReturnValue, boolean logAsInferredDynamicAccess) { Object intrinsicValue = getIntrinsic(b, returnValue == null && allowNullReturnValue ? NULL_MARKER : returnValue); if (intrinsicValue == null) { return null; @@ -783,11 +800,12 @@ private JavaConstant pushConstant(GraphBuilderContext b, ResolvedJavaMethod targ } b.addPush(returnKind, ConstantNode.forConstant(intrinsicConstant, b.getMetaAccess())); - traceConstant(b, targetMethod, targetParameters, intrinsicValue); + traceConstant(b, targetMethod, receiver, arguments, intrinsicValue, logAsInferredDynamicAccess); return intrinsicConstant; } - private boolean throwException(GraphBuilderContext b, ResolvedJavaMethod targetMethod, Supplier targetParameters, Class exceptionClass, String originalMessage) { + private boolean throwException(GraphBuilderContext b, ResolvedJavaMethod targetMethod, Object receiver, Object[] arguments, Class exceptionClass, String originalMessage, + boolean logAsInferredDynamicAccess) { /* Get the exception throwing method that has a message parameter. */ Method exceptionMethod = ExceptionSynthesizer.throwExceptionMethodOrNull(exceptionClass, String.class); if (exceptionMethod == null) { @@ -798,27 +816,39 @@ private boolean throwException(GraphBuilderContext b, ResolvedJavaMethod targetM return false; } + /* + * Because tracing can add a ReachabilityRegistrationNode to the graph, it has to happen + * before exception synthesis. + */ + traceException(b, targetMethod, receiver, arguments, exceptionClass, logAsInferredDynamicAccess); + String message = originalMessage + ". This exception was synthesized during native image building from a call to " + targetMethod.format("%H.%n(%p)") + " with constant arguments."; ExceptionSynthesizer.throwException(b, exceptionMethod, message); - traceException(b, targetMethod, targetParameters, exceptionClass); return true; } - private static void traceConstant(GraphBuilderContext b, ResolvedJavaMethod targetMethod, Supplier targetParameters, Object value) { + private void traceConstant(GraphBuilderContext b, ResolvedJavaMethod targetMethod, Object receiver, Object[] arguments, Object value, boolean logAsInferredDynamicAccess) { + if (logAsInferredDynamicAccess && inferenceLog != null) { + inferenceLog.logConstant(b, reason, targetMethod, receiver, arguments, value); + } if (Options.ReflectionPluginTracing.getValue()) { System.out.println("Call to " + targetMethod.format("%H.%n(%p)") + " reached in " + b.getMethod().format("%H.%n(%p)") + - " with parameters (" + targetParameters.get() + ")" + + " with parameters (" + Arrays.toString(arguments) + ")" + " was reduced to the constant " + value); } } - private static void traceException(GraphBuilderContext b, ResolvedJavaMethod targetMethod, Supplier targetParameters, Class exceptionClass) { + private void traceException(GraphBuilderContext b, ResolvedJavaMethod targetMethod, Object receiver, Object[] arguments, Class exceptionClass, + boolean logAsInferredDynamicAccess) { + if (logAsInferredDynamicAccess && inferenceLog != null) { + inferenceLog.logException(b, reason, targetMethod, receiver, arguments, exceptionClass); + } if (Options.ReflectionPluginTracing.getValue()) { System.out.println("Call to " + targetMethod.format("%H.%n(%p)") + " reached in " + b.getMethod().format("%H.%n(%p)") + - " with parameters (" + targetParameters.get() + ")" + + " with parameters (" + Arrays.toString(arguments) + ")" + " was reduced to a \"throw new " + exceptionClass.getName() + "(...)\""); } } diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/snippets/SubstrateGraphBuilderPlugins.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/snippets/SubstrateGraphBuilderPlugins.java index da4e04ba4ea3..2668242e6d47 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/snippets/SubstrateGraphBuilderPlugins.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/snippets/SubstrateGraphBuilderPlugins.java @@ -106,6 +106,8 @@ import com.oracle.svm.hosted.ReachabilityRegistrationNode; import com.oracle.svm.hosted.SharedArenaSupport; import com.oracle.svm.hosted.code.SubstrateCompilationDirectives; +import com.oracle.svm.hosted.dynamicaccessinference.DynamicAccessInferenceLog; +import com.oracle.svm.hosted.dynamicaccessinference.StrictDynamicAccessInferenceFeature; import com.oracle.svm.hosted.nodes.DeoptProxyNode; import com.oracle.svm.hosted.nodes.ReadReservedRegister; import com.oracle.svm.hosted.substitute.AnnotationSubstitutionProcessor; @@ -457,13 +459,19 @@ private static void registerImageInfoPlugins(InvocationPlugins plugins) { } private static void registerProxyPlugins(AnnotationSubstitutionProcessor annotationSubstitutions, InvocationPlugins plugins, ParsingReason reason) { + if (StrictDynamicAccessInferenceFeature.isEnforced() && reason == ParsingReason.PointsToAnalysis) { + return; + } + DynamicAccessInferenceLog inferenceLog = ImageSingletons.contains(DynamicAccessInferenceLog.class) + ? DynamicAccessInferenceLog.singleton() + : null; Registration proxyRegistration = new Registration(plugins, Proxy.class); - registerProxyPlugin(proxyRegistration, annotationSubstitutions, reason, "getProxyClass", ClassLoader.class, Class[].class); - registerProxyPlugin(proxyRegistration, annotationSubstitutions, reason, "newProxyInstance", ClassLoader.class, Class[].class, InvocationHandler.class); + registerProxyPlugin(proxyRegistration, annotationSubstitutions, reason, inferenceLog, "getProxyClass", ClassLoader.class, Class[].class); + registerProxyPlugin(proxyRegistration, annotationSubstitutions, reason, inferenceLog, "newProxyInstance", ClassLoader.class, Class[].class, InvocationHandler.class); } private static void registerProxyPlugin(Registration proxyRegistration, AnnotationSubstitutionProcessor annotationSubstitutions, ParsingReason reason, - String name, Class... parameterTypes) { + DynamicAccessInferenceLog inferenceLog, String name, Class... parameterTypes) { proxyRegistration.register(new RequiredInvocationPlugin(name, parameterTypes) { @Override public boolean isDecorator() { @@ -472,7 +480,7 @@ public boolean isDecorator() { @Override public boolean defaultHandler(GraphBuilderContext b, ResolvedJavaMethod targetMethod, Receiver receiver, ValueNode... args) { - Runnable proxyRegistrationRunnable = interceptProxyInterfaces(b, targetMethod, annotationSubstitutions, args[1]); + Runnable proxyRegistrationRunnable = interceptProxyInterfaces(b, targetMethod, annotationSubstitutions, reason, inferenceLog, args[1]); if (proxyRegistrationRunnable != null) { Class callerClass = OriginalClassProvider.getJavaClass(b.getMethod().getDeclaringClass()); boolean callerInScope = MissingRegistrationSupport.singleton().reportMissingRegistrationErrors(callerClass); @@ -493,7 +501,8 @@ public boolean defaultHandler(GraphBuilderContext b, ResolvedJavaMethod targetMe * Try to intercept proxy interfaces passed in as literal constants, and register the interfaces * in the {@link DynamicProxyRegistry}. */ - private static Runnable interceptProxyInterfaces(GraphBuilderContext b, ResolvedJavaMethod targetMethod, AnnotationSubstitutionProcessor annotationSubstitutions, ValueNode interfacesNode) { + private static Runnable interceptProxyInterfaces(GraphBuilderContext b, ResolvedJavaMethod targetMethod, AnnotationSubstitutionProcessor annotationSubstitutions, + ParsingReason reason, DynamicAccessInferenceLog inferenceLog, ValueNode interfacesNode) { Class[] interfaces = extractClassArray(b, annotationSubstitutions, interfacesNode); if (interfaces != null) { var caller = b.getGraph().method(); @@ -510,6 +519,13 @@ private static Runnable interceptProxyInterfaces(GraphBuilderContext b, Resolved System.out.println("Successfully determined constant value for interfaces argument of call to " + targetMethod.format("%H.%n(%p)") + " reached from " + caller.format("%H.%n(%p)") + ". " + "Registered proxy class for " + Arrays.toString(interfaces) + "."); } + if (inferenceLog != null) { + Object ignore = DynamicAccessInferenceLog.ignoreArgument(); + Object[] logArguments = targetMethod.getParameters().length == 3 + ? new Object[]{ignore, interfaces, ignore} + : new Object[]{ignore, interfaces}; + inferenceLog.logRegistration(b, reason, targetMethod, null, logArguments); + } }; } if (Options.DynamicProxyTracing.getValue() && !b.parsingIntrinsic()) {