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..3e77e8ca61ff 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,21 @@ default int getDepth() { return result; } + /** + * Gets the inlining chain of this context. + * + * @return the inlining chain of this context represented as a {@link BytecodePosition}, or + * {@code null} if 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 a7bde648b441..b7032d3c8e26 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 @@ -141,6 +141,7 @@ import jdk.vm.ci.code.Architecture; import jdk.vm.ci.code.BailoutException; import jdk.vm.ci.code.BytecodeFrame; +import jdk.vm.ci.code.BytecodePosition; import jdk.vm.ci.meta.DeoptimizationAction; import jdk.vm.ci.meta.DeoptimizationReason; import jdk.vm.ci.meta.JavaConstant; @@ -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 a3c65a6b2136..3fe3bd14171b 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 @@ -233,4 +233,8 @@ public JavaConstant lookup() { return lookupConstant(wrapped.lookup()); } } + + 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 9fb24d73577f..773ccdc4df18 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 da387b8655f8..52bb03bed822 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 @@ -90,6 +90,8 @@ import com.oracle.svm.core.util.VMError; import com.oracle.svm.hosted.classinitialization.ClassInitializationSupport; import com.oracle.svm.hosted.config.ConfigurationParserUtils; +import com.oracle.svm.hosted.dynamicaccessinference.DynamicAccessInferenceLog; +import com.oracle.svm.hosted.dynamicaccessinference.StrictDynamicAccessInferenceFeature; import com.oracle.svm.hosted.imagelayer.HostedImageLayerBuildingSupport; import com.oracle.svm.hosted.jdk.localization.LocalizationFeature; import com.oracle.svm.hosted.reflect.NativeImageConditionResolver; @@ -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(); @@ -484,6 +488,8 @@ public void beforeAnalysis(BeforeAnalysisAccess a) { globWorkSet = Set.of(); resourceRegistryImpl().setAnalysisAccess(access); + + inferenceLog = DynamicAccessInferenceLog.singletonOrNull(); } private static final class ResourceCollectorImpl extends ConditionalConfigurationRegistry implements ResourceCollector { @@ -672,7 +678,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 +718,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 407b1974daac..ba26058c96dc 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 @@ -120,6 +120,8 @@ import com.oracle.svm.hosted.code.InliningUtilities; import com.oracle.svm.hosted.code.SubstrateCompilationDirectives; import com.oracle.svm.hosted.code.UninterruptibleAnnotationChecker; +import com.oracle.svm.hosted.dynamicaccessinference.ConstantExpressionRegistry; +import com.oracle.svm.hosted.dynamicaccessinference.StrictDynamicAccessInferenceFeature; import com.oracle.svm.hosted.fieldfolding.StaticFinalFieldFoldingPhase; import com.oracle.svm.hosted.heap.PodSupport; import com.oracle.svm.hosted.imagelayer.HostedDynamicLayerInfo; @@ -229,6 +231,8 @@ public enum UsageKind { private final LayeredStaticFieldSupport layeredStaticFieldSupport; private final MetaAccessProvider originalMetaAccess; + private final ConstantExpressionRegistry constantExpressionRegistry; + @SuppressWarnings("this-escape") public SVMHost(OptionValues options, ImageClassLoader loader, ClassInitializationSupport classInitializationSupport, AnnotationSubstitutionProcessor annotationSubstitutions, MissingRegistrationSupport missingRegistrationSupport) { @@ -268,6 +272,8 @@ public SVMHost(OptionValues options, ImageClassLoader loader, ClassInitializatio featureType = lookupOriginalType(Feature.class); verifyNamingConventions = SubstrateOptions.VerifyNamingConventions.getValue(); + + constantExpressionRegistry = StrictDynamicAccessInferenceFeature.isActive() ? ConstantExpressionRegistry.singleton() : null; } /** @@ -1384,4 +1390,8 @@ public boolean allowConstantFolding(AnalysisMethod method) { public SimulateClassInitializerSupport createSimulateClassInitializerSupport(AnalysisMetaAccess aMetaAccess) { return new SimulateClassInitializerSupport(aMetaAccess, this); } + + public ConstantExpressionRegistry getConstantExpressionRegistry() { + return constantExpressionRegistry; + } } 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..bacb06c6b850 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/dynamicaccessinference/ConstantExpressionAnalyzer.java @@ -0,0 +1,656 @@ +/* + * 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.ArrayList; +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.hosted.dynamicaccessinference.dataflow.AbstractFrame; +import com.oracle.svm.hosted.dynamicaccessinference.dataflow.AbstractInterpreter; +import com.oracle.svm.util.ReflectionUtil; +import com.oracle.svm.util.TypeResult; + +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.INVOKEDYNAMIC; + +/** + * A bytecode-level constant expression analyzer for use in contexts which can affect native image + * execution semantics, such as build-time inference 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 then be safely inferred at the point of execution of the corresponding + * instruction if they are a subtype of {@link CompileTimeConstant}. + */ +final class ConstantExpressionAnalyzer extends AbstractInterpreter { + + private static final NotACompileTimeConstant NOT_A_COMPILE_TIME_CONSTANT = new NotACompileTimeConstant(); + + private final CoreProviders providers; + private final ClassLoader classLoader; + private final Map> propagatingMethods; + + ConstantExpressionAnalyzer(CoreProviders providers, ClassLoader classLoader) { + this.providers = 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) { + if (left.equals(right)) { + return left; + } else { + /* + * In case we attempt to merge a compile-time array constant with another value, and + * that merge fails, the array value can no longer be safely inferred as its reference + * can no longer be tracked and could escape through the merged (not-a-constant) result. + * + * To handle this, a special FailedArrayMergeValue, which carries information on the + * constant arrays used in the merge operation, is used as the merge result. Immediately + * after the construction of the merged frame is finished, the failed merge value, as + * well as the array operands it carries, are all marked as not-a-constant values + * throughout the entire frame. + */ + List> arraysToMerge = extractArrayConstants(left, right); + if (!arraysToMerge.isEmpty()) { + return new FailedArrayMergeValue(arraysToMerge); + } else { + return defaultValue(); + } + } + } + + private static List> extractArrayConstants(Value... values) { + ArrayList> arrayConstants = new ArrayList<>(); + for (Value value : values) { + if (value instanceof CompileTimeArrayConstant constantArray) { + arrayConstants.add(constantArray); + } + } + return arrayConstants; + } + + @Override + protected AbstractFrame mergeStates(AbstractFrame left, AbstractFrame right) { + AbstractFrame mergedStates = super.mergeStates(left, right); + /* + * If there were any failed attempts at merging compile-time constant arrays, we mark those + * arrays as not constant. + */ + mergedStates.transform(v -> v instanceof FailedArrayMergeValue, v -> { + var failedArrayMerge = (FailedArrayMergeValue) v; + for (CompileTimeArrayConstant constantArray : failedArrayMerge.arraysToMerge()) { + mergedStates.transform(arr -> arr.equals(constantArray), arr -> defaultValue()); + } + return defaultValue(); + }); + return mergedStates; + } + + @Override + protected Value loadConstant(InstructionContext context, Constant constant) { + if (context.opcode() == ACONST_NULL) { + return new CompileTimeImmutableConstant<>(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 -> providers.getSnippetReflection().asObject(Object.class, javaConstant); + default -> null; + }; + if (javaValue != null) { + return new CompileTimeImmutableConstant<>(context.bci(), javaValue); + } + } + return defaultValue(); + } + + @Override + protected Value loadType(InstructionContext context, JavaType type) { + if (type instanceof ResolvedJavaType resolvedType) { + return new CompileTimeImmutableConstant<>(context.bci(), OriginalClassProvider.getJavaClass(resolvedType)); + } else { + return defaultValue(); + } + } + + @Override + protected Value loadVariable(InstructionContext context, Value value) { + if (value instanceof CompileTimeImmutableConstant constant) { + return new CompileTimeImmutableConstant<>(context.bci(), constant.getValue()); + } else { + return defaultValue(); + } + } + + @Override + protected Value loadStaticField(InstructionContext context, JavaField field) { + /* + * Instead of compiling to an LDC instruction, class literals for primitive types (e.g., + * int.class) get compiled GETSTATIC instructions which reference the TYPE field of the + * appropriate primitive type wrapper class. + */ + if (field.getName().equals("TYPE")) { + Class primitiveClass = switch (field.getDeclaringClass().toJavaName()) { + case "java.lang.Boolean" -> boolean.class; + case "java.lang.Byte" -> byte.class; + case "java.lang.Short" -> short.class; + case "java.lang.Character" -> char.class; + case "java.lang.Integer" -> int.class; + case "java.lang.Long" -> long.class; + case "java.lang.Float" -> float.class; + case "java.lang.Double" -> double.class; + case "java.lang.Void" -> void.class; + default -> null; + }; + if (primitiveClass != null) { + return new CompileTimeImmutableConstant<>(context.bci(), primitiveClass); + } + } + return defaultValue(); + } + + @Override + protected Value storeVariable(InstructionContext context, Value value) { + if (value instanceof CompileTimeImmutableConstant constant) { + return new CompileTimeImmutableConstant<>(context.bci(), constant.getValue()); + } + if (value instanceof CompileTimeArrayConstant constantArray) { + /* + * Even though storing an array reference in a local variable doesn't cause it to + * possibly escape to another method, we still disallow this when inferring constants in + * order to avoid complicated Java-level definitions of when an array is considered + * constant. + * + * Due to this rule, the only arrays we consider constant are the ones where their + * initialization is directly used. + */ + context.state().transform(v -> v.equals(constantArray), v -> defaultValue()); + } + return defaultValue(); + } + + @Override + protected void storeArrayElement(InstructionContext context, Value array, Value index, Value value) { + if (array instanceof CompileTimeArrayConstant constantArray) { + if (index instanceof CompileTimeImmutableConstant constantIndex && value instanceof CompileTimeImmutableConstant constantValue) { + CompileTimeArrayConstant newConstantArray = new CompileTimeArrayConstant<>(context.bci(), constantArray); + try { + int realIndex = ((Number) constantIndex.getValue()).intValue(); + newConstantArray.setElement(realIndex, constantValue.getValue()); + context.state().transform(v -> v.equals(constantArray), v -> newConstantArray); + } catch (Exception e) { + context.state().transform(v -> v.equals(constantArray), v -> defaultValue()); + } + } else { + context.state().transform(v -> v.equals(constantArray), v -> defaultValue()); + } + } + } + + @Override + protected Value invokeNonVoidMethod(InstructionContext context, JavaMethod method, Value receiver, List operands) { + 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, receiver, operands)) + : defaultValue(); + } + + @Override + protected Value newObjectArray(InstructionContext context, JavaType type, Value size) { + if (size instanceof CompileTimeImmutableConstant constantSize && type instanceof ResolvedJavaType) { + int realSize = ((Number) constantSize.getValue()).intValue(); + return new CompileTimeArrayConstant<>(context.bci(), realSize, OriginalClassProvider.getJavaClass(type)); + } else { + return defaultValue(); + } + } + + @Override + protected Value checkCast(InstructionContext context, JavaType type, Value object) { + /* + * A CHECKCAST instruction on a null value always succeeds and leaves the operand stack + * unchanged. It is useful to consider this case a compile-time constant in order to be able + * to infer code patterns such as "SomeClass.class.getMethod("someMethod", (Class[]) null);" + * which are sometimes used. + */ + if (object instanceof CompileTimeImmutableConstant c && c.getValue() == null) { + return new CompileTimeImmutableConstant<>(context.bci(), null); + } else { + return defaultValue(); + } + } + + @Override + protected void onValueEscape(InstructionContext context, Value value) { + /* + * Arrays are mutable, making any guarantees on the inferred value of the array void if a + * reference to the array escapes to a different method. This can happen explicitly, i.e., + * by using a compile-time constant array as an argument to a method (from which it can + * possibly be modified), or storing the reference in a field or array (and then get + * accessed in other methods through those). + */ + if (value instanceof CompileTimeArrayConstant constantArray) { + context.state().transform(v -> v.equals(constantArray), v -> 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, InstructionContext context, Value receiver, 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 CompileTimeImmutableConstant 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) { + if (invocationData.receiver() instanceof CompileTimeConstant constant) { + receiver = constant.getValue(); + } else { + return defaultValue(); + } + } + assert invocationData.method().getParameterCount() == invocationData.operands.size(); + Object[] arguments = new Object[invocationData.method.getParameterCount()]; + for (int i = 0; i < arguments.length; i++) { + if (invocationData.operands.get(i) instanceof CompileTimeConstant constant) { + arguments[i] = constant.getValue(); + } else { + return defaultValue(); + } + } + try { + Object result = invocationData.method.invoke(receiver, arguments); + return new CompileTimeImmutableConstant<>(invocationData.context.bci(), result); + } catch (Throwable t) { + return defaultValue(); + } + } + + private Value invokeForNameOne(InstructionContext 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; + return findClass(context, className, loader); + } + + private Value invokeForNameThree(InstructionContext 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 CompileTimeImmutableConstant constant) { + loader = (ClassLoader) constant.getValue(); + } else { + return defaultValue(); + } + } else { + loader = classLoader; + } + return findClass(context, className, loader); + } + + private Value findClass(InstructionContext context, String className, ClassLoader loader) { + TypeResult> clazz = ImageClassLoader.findClass(className, false, loader); + if (clazz.isPresent()) { + return new CompileTimeImmutableConstant<>(context.bci(), clazz.get()); + } else { + return defaultValue(); + } + } + + private static final Constructor LOOKUP_CONSTRUCTOR = ReflectionUtil.lookupConstructor(MethodHandles.Lookup.class, Class.class); + + private Value getLookup(InstructionContext context) { + Class callerClass = OriginalClassProvider.getJavaClass(context.method().getDeclaringClass()); + try { + MethodHandles.Lookup lookup = LOOKUP_CONSTRUCTOR.newInstance(callerClass); + return new CompileTimeImmutableConstant<>(context.bci(), lookup); + } catch (Throwable t) { + return defaultValue(); + } + } + + /** + * Looking up constants/types/fields/methods through a {@link WrappedConstantPool} can result in + * {@link com.oracle.graal.pointsto.constraints.UnsupportedFeatureException + * UnsupportedFeatureException(s)} being thrown. To avoid this, we simulate the behavior of the + * {@link 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) { + tryToResolve(constantPool, cpi, opcode); + return unwrapIfWrapped(constantPool).lookupConstant(cpi, false); + } + + @Override + protected JavaType lookupType(ConstantPool constantPool, int cpi, int opcode) { + tryToResolve(constantPool, cpi, opcode); + return unwrapIfWrapped(constantPool).lookupType(cpi, opcode); + } + + @Override + protected JavaMethod lookupMethod(ConstantPool constantPool, int cpi, int opcode, ResolvedJavaMethod caller) { + /* + * Resolving the call site reference for an indy can result in the bootstrap method being + * executed at build-time, which should be avoided in the general case. + */ + if (opcode != INVOKEDYNAMIC) { + tryToResolve(constantPool, cpi, opcode); + } + return unwrapIfWrapped(constantPool).lookupMethod(cpi, opcode, getOriginalIfWrapped(caller)); + } + + @Override + protected JavaConstant lookupAppendix(ConstantPool constantPool, int cpi, int opcode) { + return unwrapIfWrapped(constantPool).lookupAppendix(cpi, opcode); + } + + @Override + protected JavaField lookupField(ConstantPool constantPool, int cpi, int opcode, ResolvedJavaMethod caller) { + return unwrapIfWrapped(constantPool).lookupField(cpi, getOriginalIfWrapped(caller), opcode); + } + + private static void tryToResolve(ConstantPool constantPool, int cpi, int opcode) { + try { + constantPool.loadReferencedType(cpi, opcode, false); + } catch (Throwable t) { + // Ignore and leave the type unresolved. + } + } + + /** + * Marker interface for abstract values obtained during bytecode-level constant expression + * inference. + */ + interface Value { + + } + + /** + * A value for which the value can be inferred at build-time is considered a + * {@link CompileTimeConstant compile-time constant}. Each such value is represented by a pair + * (source BCI, inferred value), where the BCI component represents the bytecode offset of the + * instruction which last placed/modified the value in the abstract frame, and the inferred + * value component represents the actual Java value as would be observed at run-time. + *

+ * An example of such a value would be a String {@code "SomeString"} pushed onto the operand + * stack by an LDC instruction at BCI 42 - the corresponding compile-time constant in that case + * would be the pair {@code (42, "SomeString")}. + */ + abstract static class CompileTimeConstant implements Value { + + private final int sourceBci; + + CompileTimeConstant(int bci) { + this.sourceBci = bci; + } + + public int getSourceBci() { + return sourceBci; + } + + public abstract Object getValue(); + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + CompileTimeConstant that = (CompileTimeConstant) o; + /* + * 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). + */ + return sourceBci == that.sourceBci; + } + + @Override + public int hashCode() { + return Objects.hashCode(sourceBci); + } + } + + /** + * A special value representing unsuccessful merging (into a compile-time constant) of one or + * more {@link CompileTimeArrayConstant constant arrays} with other values. + */ + private record FailedArrayMergeValue(List> arraysToMerge) implements Value { + + } + + private static final class NotACompileTimeConstant implements Value { + + @Override + public String toString() { + return "Not a compile-time constant"; + } + } + + /** + * Values of certain types (e.g., strings) are immutable, meaning that their inferred value is + * guaranteed to remain valid even if a reference to such a value escapes the analyzed method + * (by using it as an argument to a method, storing it in a field, etc.). + */ + private static final class CompileTimeImmutableConstant extends CompileTimeConstant { + + private final T value; + + CompileTimeImmutableConstant(int bci, T value) { + super(bci); + this.value = value; + } + + @Override + public T getValue() { + return value; + } + + @Override + public String toString() { + return "(" + getSourceBci() + ", " + getValue() + ")"; + } + } + + /** + * Unlike {@link CompileTimeImmutableConstant immutable constants}, arrays are always mutable. + * If a reference to an array escapes the analyzed method, it can arbitrarily be modified + * outside of it. Because of this, inferred array values require special handling and are + * subject to certain restrictions in the inference scheme, such as only being able to be used + * as method argument once before no longer being considered a compile-time constant. + */ + private static final class CompileTimeArrayConstant extends CompileTimeConstant { + + /* + * Sparse array representation to avoid possible large memory overhead when analyzing an + * array initialization with a large size. + */ + private final Map value; + private final int size; + private final Class elementType; + + CompileTimeArrayConstant(int bci, int size, Class elementType) { + super(bci); + this.value = new HashMap<>(); + this.size = size; + this.elementType = elementType; + } + + 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..2c02eea8e227 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/dynamicaccessinference/ConstantExpressionRegistry.java @@ -0,0 +1,207 @@ +/* + * 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.nativeimage.ImageSingletons; + +import com.oracle.svm.hosted.dynamicaccessinference.dataflow.AbstractFrame; +import com.oracle.svm.hosted.dynamicaccessinference.dataflow.DataFlowAnalysisException; + +import jdk.graal.compiler.bytecode.Bytecode; +import jdk.vm.ci.code.BytecodePosition; +import jdk.vm.ci.meta.JavaKind; +import jdk.vm.ci.meta.ResolvedJavaMethod; + +/** + * Holds information on constant expressions as inferred by {@link ConstantExpressionAnalyzer}. + */ +public final 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> registry = new ConcurrentHashMap<>(); + private boolean sealed = false; + + private final ConstantExpressionAnalyzer analyzer; + + ConstantExpressionRegistry(ConstantExpressionAnalyzer analyzer) { + this.analyzer = analyzer; + } + + /** + * Analyze the provided {@code bytecode} for constant expressions and store the results in the + * registry. + */ + public void inferConstantExpressions(Bytecode bytecode) { + assert !sealed : "Cannot store in registry when it is already sealed"; + try { + Map> abstractFrames = analyzer.analyze(bytecode); + abstractFrames.forEach((key, value) -> registry.put(new BytecodePosition(null, bytecode.getMethod(), key), value)); + } catch (DataFlowAnalysisException e) { + // Ignore. Constant expression inference will not work for this 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 !sealed : "Registry is already sealed"; + assert targetMethod.hasReceiver() : "Method " + targetMethod + " does not have receiver"; + if (callerMethod == null) { + return null; + } + AbstractFrame frame = registry.get(new BytecodePosition(null, callerMethod, bci)); + if (frame == null) { + return null; + } + int numOfParameters = targetMethod.getSignature().getParameterCount(false); + ConstantExpressionAnalyzer.Value receiver = frame.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 !sealed : "Registry is already sealed"; + int numOfParameters = targetMethod.getSignature().getParameterCount(false); + assert 0 <= index && index < numOfParameters : "Argument index " + index + " out of bounds for " + targetMethod; + if (callerMethod == null) { + return null; + } + AbstractFrame frame = registry.get(new BytecodePosition(null, callerMethod, bci)); + if (frame == null) { + return null; + } + ConstantExpressionAnalyzer.Value argument = frame.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); + } + + void seal() { + assert !sealed : "Registry has already been sealed"; + sealed = 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..4453b2014e10 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/dynamicaccessinference/DynamicAccessInferenceLog.java @@ -0,0 +1,252 @@ +/* + * 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 org.graalvm.nativeimage.ImageSingletons; + +import com.oracle.svm.core.ParsingReason; +import com.oracle.svm.core.feature.InternalFeature; +import com.oracle.svm.hosted.ReachabilityRegistrationNode; + +import jdk.graal.compiler.nodes.graphbuilderconf.GraphBuilderContext; +import jdk.graal.compiler.util.json.JsonBuilder; +import jdk.vm.ci.code.BytecodePosition; +import jdk.vm.ci.meta.ResolvedJavaMethod; + +/** + * Log folding information on build-time inferred dynamic access invocations. + */ +public final class DynamicAccessInferenceLog { + + private static final Object IGNORED_ARGUMENT_MARKER = new IgnoredArgumentValue(); + + private final Queue entries = new ConcurrentLinkedQueue<>(); + + public static DynamicAccessInferenceLog singleton() { + return ImageSingletons.lookup(DynamicAccessInferenceLog.class); + } + + public static DynamicAccessInferenceLog singletonOrNull() { + return ImageSingletons.contains(DynamicAccessInferenceLog.class) ? ImageSingletons.lookup(DynamicAccessInferenceLog.class) : null; + } + + public static Object ignoreArgument() { + return IGNORED_ARGUMENT_MARKER; + } + + public void logFolding(GraphBuilderContext b, ParsingReason reason, ResolvedJavaMethod targetMethod, Object targetReceiver, Object[] targetArguments, Object value) { + logEntry(b, reason, () -> new FoldingLogEntry(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) { + LogEntry entry = entrySupplier.get(); + /* + * Using a reachability node avoids reporting for unreachable invocations, as well as + * invocations that were potentially folded during the exploration phase of + * InlineBeforeAnalysis (but not in the final graph). + */ + b.add(ReachabilityRegistrationNode.create(() -> entries.add(entry), reason)); + } + } + + Iterable getEntries() { + return entries; + } + + abstract static class LogEntry { + + private final BytecodePosition callLocation; + private final List callStack; + private final ResolvedJavaMethod targetMethod; + private final Object targetReceiver; + private 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 = new BytecodePosition(null, 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)); + } + } + } + + public BytecodePosition getCallLocation() { + return callLocation; + } + + public ResolvedJavaMethod getTargetMethod() { + return targetMethod; + } + + public Object getReceiver() { + return targetReceiver; + } + + public Object[] getArguments() { + return targetArguments; + } + } + + private static class FoldingLogEntry extends LogEntry { + + private final Object value; + + FoldingLogEntry(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); + } + } + + private static class ExceptionLogEntry extends LogEntry { + + private 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()); + } + } + + private 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 class IgnoredArgumentValue { + + @Override + public String toString() { + return ""; + } + } +} + +final class DynamicAccessInferenceLogFeature implements InternalFeature { + + @Override + public void afterRegistration(AfterRegistrationAccess access) { + ImageSingletons.add(DynamicAccessInferenceLog.class, new DynamicAccessInferenceLog()); + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/dynamicaccessinference/DynamicAccessInferenceReportFeature.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/dynamicaccessinference/DynamicAccessInferenceReportFeature.java new file mode 100644 index 000000000000..7aea6505bdd4 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/dynamicaccessinference/DynamicAccessInferenceReportFeature.java @@ -0,0 +1,82 @@ +/* + * 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.nativeimage.hosted.Feature; + +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 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; + +@AutomaticallyRegisteredFeature +final class DynamicAccessInferenceReportFeature implements InternalFeature { + + static class Options { + @Option(help = "Report inferred dynamic access invocations.", stability = OptionStability.EXPERIMENTAL)// + static final HostedOptionKey ReportDynamicAccessInference = new HostedOptionKey<>(false); + } + + @Override + public boolean isInConfiguration(IsInConfigurationAccess access) { + return Options.ReportDynamicAccessInference.getValue(); + } + + @Override + public List> getRequiredFeatures() { + return List.of(DynamicAccessInferenceLogFeature.class); + } + + @Override + public void afterAnalysis(AfterAnalysisAccess access) { + writeReport(); + } + + private static void writeReport() { + 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()) { + DynamicAccessInferenceLog log = DynamicAccessInferenceLog.singleton(); + for (DynamicAccessInferenceLog.LogEntry entry : log.getEntries()) { + try (JsonBuilder.ObjectBuilder objectBuilder = arrayBuilder.nextEntry().object()) { + entry.toJson(objectBuilder); + } + } + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } +} 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..de7ebb247035 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/dynamicaccessinference/StrictDynamicAccessInferenceFeature.java @@ -0,0 +1,636 @@ +/* + * 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.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.Objects; +import java.util.function.Consumer; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +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.LogUtils; +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.code.BytecodePosition; +import jdk.vm.ci.meta.JavaConstant; +import jdk.vm.ci.meta.JavaKind; +import jdk.vm.ci.meta.MetaAccessProvider; +import jdk.vm.ci.meta.ResolvedJavaMethod; + +/** + * Feature for controlling the optimization independent inference of invocations which would + * otherwise require an explicit reachability metadata entry. Unlike the graph-level inference + * scheme, this restricted inference is executed directly on method bytecode and does not depend on + * the inlining done by {@link com.oracle.graal.pointsto.phases.InlineBeforeAnalysis} and other IR + * graph optimizations. + */ +@AutomaticallyRegisteredFeature +public final class StrictDynamicAccessInferenceFeature implements InternalFeature { + + static class Options { + + 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)// + static final HostedOptionKey StrictDynamicAccessInference = new HostedOptionKey<>(Mode.Disable); + } + + private ClassLoader applicationClassLoader; + private AnalysisUniverse analysisUniverse; + + private ConstantExpressionRegistry registry; + private DynamicAccessInferenceLog inferenceLog; + + public static boolean isEnforced() { + return Options.StrictDynamicAccessInference.getValue() == Options.Mode.Enforce; + } + + public static boolean shouldWarn() { + return Options.StrictDynamicAccessInference.getValue() == Options.Mode.Warn; + } + + public static boolean isActive() { + return isEnforced() || shouldWarn(); + } + + @Override + public boolean isInConfiguration(IsInConfigurationAccess access) { + return isActive(); + } + + @Override + public List> getRequiredFeatures() { + return shouldWarn() ? List.of(DynamicAccessInferenceLogFeature.class) : List.of(); + } + + @Override + public void afterRegistration(AfterRegistrationAccess access) { + FeatureImpl.AfterRegistrationAccessImpl accessImpl = (FeatureImpl.AfterRegistrationAccessImpl) access; + applicationClassLoader = accessImpl.getApplicationClassLoader(); + ConstantExpressionAnalyzer analyzer = new ConstantExpressionAnalyzer(GraalAccess.getOriginalProviders(), applicationClassLoader); + registry = new ConstantExpressionRegistry(analyzer); + ImageSingletons.add(ConstantExpressionRegistry.class, registry); + } + + @Override + public void duringSetup(DuringSetupAccess access) { + FeatureImpl.DuringSetupAccessImpl accessImpl = (FeatureImpl.DuringSetupAccessImpl) access; + analysisUniverse = accessImpl.getUniverse(); + } + + @Override + public void beforeAnalysis(BeforeAnalysisAccess access) { + inferenceLog = DynamicAccessInferenceLog.singletonOrNull(); + /* + * 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 (GR-66140). + * + * 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 registerInvocationPlugins(Providers providers, GraphBuilderConfiguration.Plugins plugins, ParsingReason reason) { + /* + * Dynamic access inference should only be restricted during analysis. In other cases, we + * keep the inference unrestricted, as is done in ReflectionPlugins. + */ + if (isEnforced() && reason.duringAnalysis() && reason != ParsingReason.JITCompilation) { + new StrictReflectionInferencePlugins().register(plugins, reason); + new StrictResourceInferencePlugins().register(plugins, reason); + } + } + + @Override + public void afterAnalysis(AfterAnalysisAccess access) { + if (shouldWarn()) { + warnForNonStrictInference(); + } + /* + * No more bytecode parsing should happen after analysis, so we can seal and clean up the + * registry. + */ + registry.seal(); + } + + private void warnForNonStrictInference() { + List unsafeFoldingEntries = StreamSupport.stream(inferenceLog.getEntries().spliterator(), false) + .filter(entry -> !entryIsInRegistry(entry, registry)) + .toList(); + if (!unsafeFoldingEntries.isEmpty()) { + StringBuilder sb = new StringBuilder(); + sb.append("The following dynamic access method invocations have been inferred outside of the strict inference 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 entryIsInRegistry(DynamicAccessInferenceLog.LogEntry entry, ConstantExpressionRegistry registry) { + BytecodePosition callLocation = entry.getCallLocation(); + ResolvedJavaMethod targetMethod = entry.getTargetMethod(); + if (targetMethod.hasReceiver()) { + Object receiver = registry.getReceiver(callLocation.getMethod(), callLocation.getBCI(), targetMethod); + if (entry.getReceiver() != DynamicAccessInferenceLog.ignoreArgument() && receiver == null) { + return false; + } + } + Object[] arguments = entry.getArguments(); + for (int i = 0; i < arguments.length; i++) { + Object argument = registry.getArgument(callLocation.getMethod(), callLocation.getBCI(), targetMethod, i); + if (arguments[i] != DynamicAccessInferenceLog.ignoreArgument() && argument == null) { + return false; + } + } + return true; + } + + private static Class[] getArgumentTypesForPlugin(Method method) { + ArrayList> argumentTypes = new ArrayList<>(); + if (!Modifier.isStatic(method.getModifiers())) { + argumentTypes.add(InvocationPlugin.Receiver.class); + } + argumentTypes.addAll(Arrays.asList(method.getParameterTypes())); + return argumentTypes.toArray(new Class[0]); + } + + private final class StrictReflectionInferencePlugins { + + public void register(GraphBuilderConfiguration.Plugins plugins, ParsingReason reason) { + registerClassPlugins(plugins, reason); + registerMethodHandlePlugins(plugins, reason); + registerProxyPlugins(plugins, reason); + } + + private void registerClassPlugins(GraphBuilderConfiguration.Plugins plugins, ParsingReason reason) { + InvocationPlugins invocationPlugins = plugins.getInvocationPlugins(); + + registerForNameOnePlugin(plugins.getInvocationPlugins(), reason, plugins.getClassInitializationPlugin()); + registerForNameThreePlugin(plugins.getInvocationPlugins(), reason, plugins.getClassInitializationPlugin()); + + Method getField = ReflectionUtil.lookupMethod(true, Class.class, "getField", String.class); + Method getDeclaredField = ReflectionUtil.lookupMethod(true, Class.class, "getDeclaredField", String.class); + + Method getConstructor = ReflectionUtil.lookupMethod(true, Class.class, "getConstructor", Class[].class); + Method getDeclaredConstructor = ReflectionUtil.lookupMethod(true, Class.class, "getDeclaredConstructor", Class[].class); + + Method getMethod = ReflectionUtil.lookupMethod(true, Class.class, "getMethod", String.class, Class[].class); + Method getDeclaredMethod = ReflectionUtil.lookupMethod(true, Class.class, "getDeclaredMethod", String.class, Class[].class); + + Stream.of(getField, getDeclaredField, getConstructor, getDeclaredConstructor, getMethod, getDeclaredMethod) + .filter(Objects::nonNull) + .forEach(m -> registerFoldingPlugin(invocationPlugins, reason, m)); + + registerBulkPlugin(invocationPlugins, reason, "getClasses", RuntimeReflection::registerAllClasses); + registerBulkPlugin(invocationPlugins, reason, "getDeclaredClasses", RuntimeReflection::registerAllDeclaredClasses); + registerBulkPlugin(invocationPlugins, reason, "getConstructors", RuntimeReflection::registerAllConstructors); + registerBulkPlugin(invocationPlugins, reason, "getDeclaredConstructors", RuntimeReflection::registerAllDeclaredConstructors); + registerBulkPlugin(invocationPlugins, reason, "getFields", RuntimeReflection::registerAllFields); + registerBulkPlugin(invocationPlugins, reason, "getDeclaredFields", RuntimeReflection::registerAllDeclaredFields); + registerBulkPlugin(invocationPlugins, reason, "getMethods", RuntimeReflection::registerAllMethods); + registerBulkPlugin(invocationPlugins, reason, "getDeclaredMethods", RuntimeReflection::registerAllDeclaredMethods); + registerBulkPlugin(invocationPlugins, reason, "getNestMembers", RuntimeReflection::registerAllNestMembers); + registerBulkPlugin(invocationPlugins, reason, "getPermittedSubclasses", RuntimeReflection::registerAllPermittedSubclasses); + registerBulkPlugin(invocationPlugins, reason, "getRecordComponents", RuntimeReflection::registerAllRecordComponents); + registerBulkPlugin(invocationPlugins, reason, "getSigners", RuntimeReflection::registerAllSigners); + } + + private void registerForNameOnePlugin(InvocationPlugins invocationPlugins, ParsingReason reason, ClassInitializationPlugin initializationPlugin) { + 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() + : applicationClassLoader; + return tryToFoldClassForName(b, reason, initializationPlugin, targetMethod, className, true, classLoader); + } + }); + } + + private void registerForNameThreePlugin(InvocationPlugins invocationPlugins, ParsingReason reason, ClassInitializationPlugin initializationPlugin) { + 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; + } + classLoader = ConstantExpressionRegistry.isNull(loader) ? null : (ClassLoader) loader; + } else { + classLoader = applicationClassLoader; + } + return tryToFoldClassForName(b, reason, initializationPlugin, targetMethod, className, initialize, classLoader); + } + }); + } + + private boolean tryToFoldClassForName(GraphBuilderContext b, ParsingReason reason, ClassInitializationPlugin initializationPlugin, 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 void registerFoldingPlugin(InvocationPlugins invocationPlugins, ParsingReason reason, Method method) { + invocationPlugins.register(method.getDeclaringClass(), new InvocationPlugin.RequiredInvocationPlugin(method.getName(), getArgumentTypesForPlugin(method)) { + @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 tryToFoldInvocationUsingReflection(b, reason, targetMethod, method, receiverValue, arguments); + } + }); + } + + 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; + } + + private boolean tryToFoldInvocationUsingReflection(GraphBuilderContext b, ParsingReason reason, ResolvedJavaMethod targetMethod, Method reflectionMethod, Object receiverValue, + Object[] argValues) { + if (!targetMethod.isStatic() && (receiverValue == null || ConstantExpressionRegistry.isNull(receiverValue))) { + return false; + } + + if (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, receiverValue, 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.logFolding(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 void registerBulkPlugin(InvocationPlugins invocationPlugins, ParsingReason reason, String methodName, Consumer> registrationCallback) { + Method method = ReflectionUtil.lookupMethod(true, Class.class, methodName); + if (method != null) { + registerBulkPlugin(invocationPlugins, reason, method, registrationCallback); + } + } + + private void registerBulkPlugin(InvocationPlugins invocationPlugins, ParsingReason reason, Method method, Consumer> registrationCallback) { + invocationPlugins.register(method.getDeclaringClass(), new InvocationPlugin.RequiredInvocationPlugin(method.getName(), getArgumentTypesForPlugin(method)) { + @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 tryToRegisterBulkQuery(b, reason, targetMethod, clazz, registrationCallback); + } + }); + } + + private boolean tryToRegisterBulkQuery(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 void registerMethodHandlePlugins(GraphBuilderConfiguration.Plugins plugins, ParsingReason reason) { + InvocationPlugins invocationPlugins = plugins.getInvocationPlugins(); + + Method findClass = ReflectionUtil.lookupMethod(true, MethodHandles.Lookup.class, "findClass", String.class); + + Method findConstructor = ReflectionUtil.lookupMethod(true, MethodHandles.Lookup.class, "findConstructor", Class.class, MethodType.class); + + Method findVirtual = ReflectionUtil.lookupMethod(true, MethodHandles.Lookup.class, "findVirtual", Class.class, String.class, MethodType.class); + Method findStatic = ReflectionUtil.lookupMethod(true, MethodHandles.Lookup.class, "findStatic", Class.class, String.class, MethodType.class); + Method findSpecial = ReflectionUtil.lookupMethod(true, MethodHandles.Lookup.class, "findSpecial", Class.class, String.class, MethodType.class, Class.class); + + Method findGetter = ReflectionUtil.lookupMethod(true, MethodHandles.Lookup.class, "findGetter", Class.class, String.class, Class.class); + Method findStaticGetter = ReflectionUtil.lookupMethod(true, MethodHandles.Lookup.class, "findStaticGetter", Class.class, String.class, Class.class); + Method findSetter = ReflectionUtil.lookupMethod(true, MethodHandles.Lookup.class, "findSetter", Class.class, String.class, Class.class); + Method findStaticSetter = ReflectionUtil.lookupMethod(true, MethodHandles.Lookup.class, "findStaticSetter", Class.class, String.class, String.class); + Method findVarHandle = ReflectionUtil.lookupMethod(true, MethodHandles.Lookup.class, "findVarHandle", Class.class, String.class, Class.class); + Method findStaticVarHandle = ReflectionUtil.lookupMethod(true, MethodHandles.Lookup.class, "findStaticVarHandle", Class.class, String.class, Class.class); + + Stream.of(findClass, findConstructor, findVirtual, findStatic, findSpecial, findGetter, findStaticGetter, findSetter, findStaticSetter, findVarHandle, findStaticVarHandle) + .filter(Objects::nonNull) + .forEach(m -> registerFoldingPlugin(invocationPlugins, reason, m)); + } + + private void registerProxyPlugins(GraphBuilderConfiguration.Plugins plugins, ParsingReason reason) { + Method getProxyClass = ReflectionUtil.lookupMethod(true, Proxy.class, "getProxyClass", ClassLoader.class, Class[].class); + Method newProxyInstance = ReflectionUtil.lookupMethod(true, Proxy.class, "newProxyInstance", ClassLoader.class, Class[].class, InvocationHandler.class); + Stream.of(getProxyClass, newProxyInstance) + .filter(Objects::nonNull) + .forEach(m -> registerProxyPlugin(plugins.getInvocationPlugins(), reason, m)); + } + + private void registerProxyPlugin(InvocationPlugins invocationPlugins, ParsingReason reason, Method method) { + invocationPlugins.register(method.getDeclaringClass(), new InvocationPlugin.RequiredInvocationPlugin(method.getName(), getArgumentTypesForPlugin(method)) { + @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 tryToRegisterProxy(b, reason, targetMethod, interfaces); + } + }); + } + + private boolean tryToRegisterProxy(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[] args = targetMethod.getParameters().length == 2 + ? new Object[]{DynamicAccessInferenceLog.ignoreArgument(), interfaces} + : new Object[]{DynamicAccessInferenceLog.ignoreArgument(), interfaces, DynamicAccessInferenceLog.ignoreArgument()}; + inferenceLog.logRegistration(b, reason, targetMethod, null, args); + } + return true; + } + } + + private final class StrictResourceInferencePlugins { + + private final Method resourceNameResolver = ReflectionUtil.lookupMethod(Class.class, "resolveName", String.class); + + public void register(GraphBuilderConfiguration.Plugins plugins, ParsingReason reason) { + Method getResource = ReflectionUtil.lookupMethod(true, Class.class, "getResource", String.class); + Method getResourceAsStream = ReflectionUtil.lookupMethod(true, Class.class, "getResourceAsStream", String.class); + Stream.of(getResource, getResourceAsStream) + .filter(Objects::nonNull) + .forEach(m -> registerResourcePlugin(plugins.getInvocationPlugins(), reason, m)); + } + + private void registerResourcePlugin(InvocationPlugins invocationPlugins, ParsingReason reason, Method method) { + invocationPlugins.register(method.getDeclaringClass(), new InvocationPlugin.RequiredInvocationPlugin(method.getName(), getArgumentTypesForPlugin(method)) { + @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 tryToRegisterResource(b, reason, targetMethod, clazz, resource); + } + }); + } + + private boolean tryToRegisterResource(GraphBuilderContext b, ParsingReason reason, ResolvedJavaMethod targetMethod, Class clazz, String resource) { + if (clazz == null || resource == null) { + return false; + } + b.add(ReachabilityRegistrationNode.create(() -> RuntimeResourceAccess.addResource(clazz.getModule(), resolveResourceName(clazz, resource)), reason)); + if (inferenceLog != null) { + inferenceLog.logRegistration(b, reason, targetMethod, clazz, new String[]{resource}); + } + return true; + } + + private String resolveResourceName(Class clazz, String name) { + try { + return (String) resourceNameResolver.invoke(clazz, name); + } catch (ReflectiveOperationException e) { + throw VMError.shouldNotReachHere(e); + } + } + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/dynamicaccessinference/dataflow/AbstractFrame.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/dynamicaccessinference/dataflow/AbstractFrame.java new file mode 100644 index 000000000000..3ceb0af3875e --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/dynamicaccessinference/dataflow/AbstractFrame.java @@ -0,0 +1,428 @@ +/* + * 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.dataflow; + +import java.util.Arrays; +import java.util.Objects; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Predicate; + +import jdk.vm.ci.meta.ResolvedJavaMethod; + +/** + * Abstract representation of a bytecode execution frame for an instruction, i.e., its + * {@link AbstractFrame#operandStack operand stack} and {@link AbstractFrame#localVariableTable + * local variable table}, right before the execution of said instruction. + * + * @param The abstract representation of values stored in the frame. + */ +public final class AbstractFrame { + + /** + * Second slot marker for values requiring two slots on the operand stack and in the local + * variable table (i.e., values of type long and double). + */ + private static final Object TWO_SLOT_MARKER = new Object(); + + @SuppressWarnings("unchecked") + private static T twoSlotMarker() { + return (T) TWO_SLOT_MARKER; + } + + final OperandStack operandStack; + final LocalVariableTable localVariableTable; + + AbstractFrame(ResolvedJavaMethod method) { + this.operandStack = new OperandStack<>(method.getMaxStackSize()); + this.localVariableTable = new LocalVariableTable<>(method.getMaxLocals()); + } + + AbstractFrame(AbstractFrame other) { + this.operandStack = new OperandStack<>(other.operandStack); + this.localVariableTable = new LocalVariableTable<>(other.localVariableTable); + } + + private AbstractFrame(OperandStack operandStack, LocalVariableTable localVariableTable) { + this.operandStack = operandStack; + this.localVariableTable = localVariableTable; + } + + /** + * Get the value on the operand stack at the specified {@code depth}. The {@code depth} + * parameter corresponds to actual values on the operand stack, and not the frames. This means + * that a value occupying two stack frames contributes only once to the operand depth. + */ + public T getOperand(int depth) { + int currentDepth = 0; + int frameFromTop = 0; + while (currentDepth <= depth) { + T frame = operandStack.peekFrame(frameFromTop); + if (frame != TWO_SLOT_MARKER) { + currentDepth++; + } + frameFromTop++; + } + return operandStack.peekFrame(frameFromTop - 1); + } + + /** + * 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 merge(AbstractFrame other, BiFunction mergeFunction) { + OperandStack mergedOperandStack = operandStack.merge(other.operandStack, mergeFunction); + LocalVariableTable mergedLocalVariableTable = localVariableTable.merge(other.localVariableTable, mergeFunction); + return new AbstractFrame<>(mergedOperandStack, mergedLocalVariableTable); + } + + @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; + } + + static final class OperandStack { + + private final T[] stack; + private int size; + + @SuppressWarnings("unchecked") + OperandStack(int maxStackSize) { + this.stack = (T[]) new Object[maxStackSize]; + this.size = 0; + } + + OperandStack(OperandStack other) { + this.stack = other.stack.clone(); + this.size = other.size; + } + + void push(T value, boolean needsTwoSlots) { + pushFrame(value); + if (needsTwoSlots) { + pushFrame(twoSlotMarker()); + } + } + + T pop() { + T frame = popFrame(); + if (frame == TWO_SLOT_MARKER) { + frame = popFrame(); + } + assert frame != TWO_SLOT_MARKER : "A value cannot be partially popped from the stack"; + return frame; + } + + void clear() { + Arrays.fill(stack, null); + size = 0; + } + + void applyPop() { + T f1 = popFrame(); + assert f1 != TWO_SLOT_MARKER : "POP expects a single-slot value"; + } + + void applyPop2() { + popFrame(); + T f2 = popFrame(); + assert f2 != TWO_SLOT_MARKER : "POP2 expects either a single two-slot value, or two single-slot values"; + } + + void applyDup() { + T f1 = peekFrame(0); + assert f1 != TWO_SLOT_MARKER : "DUP expects a single-slot value"; + pushFrame(f1); + } + + void applyDupX1() { + T f1 = popFrame(); + T f2 = popFrame(); + assert f1 != TWO_SLOT_MARKER && f2 != TWO_SLOT_MARKER : "DUP_X1 expects two single-slot values"; + pushFrame(f1); + pushFrame(f2); + pushFrame(f1); + } + + void applyDupX2() { + T f1 = popFrame(); + T f2 = popFrame(); + T f3 = popFrame(); + assert f1 != TWO_SLOT_MARKER && f3 != TWO_SLOT_MARKER : "Unexpected value sizes for DUP_X2"; + pushFrame(f1); + pushFrame(f3); + pushFrame(f2); + pushFrame(f1); + } + + void applyDup2() { + T f1 = popFrame(); + T f2 = popFrame(); + assert f2 != TWO_SLOT_MARKER : "DUP2 expects either a single two-slot value, or two single-slot values"; + pushFrame(f2); + pushFrame(f1); + pushFrame(f2); + pushFrame(f1); + } + + void applyDup2X1() { + T f1 = popFrame(); + T f2 = popFrame(); + T f3 = popFrame(); + assert f2 != TWO_SLOT_MARKER && f3 != TWO_SLOT_MARKER : "Unexpected value sizes for DUP2_X1"; + pushFrame(f2); + pushFrame(f1); + pushFrame(f3); + pushFrame(f2); + pushFrame(f1); + } + + void applyDup2X2() { + T f1 = popFrame(); + T f2 = popFrame(); + T f3 = popFrame(); + T f4 = popFrame(); + pushFrame(f2); + pushFrame(f1); + pushFrame(f4); + pushFrame(f3); + pushFrame(f2); + pushFrame(f1); + } + + void applySwap() { + T f1 = popFrame(); + T f2 = popFrame(); + assert f1 != TWO_SLOT_MARKER && f2 != TWO_SLOT_MARKER : "SWAP expects two single-slot values"; + pushFrame(f1); + pushFrame(f2); + } + + private void transform(Predicate filterFunction, Function transformFunction) { + for (int i = 0; i < size; i++) { + T frame = stack[i]; + if (frame != null && frame != TWO_SLOT_MARKER && filterFunction.test(frame)) { + stack[i] = transformFunction.apply(frame); + } + } + } + + private OperandStack merge(OperandStack other, BiFunction mergeFunction) { + assert size == other.size : "Operand stacks must be of the same size when merging"; + OperandStack merged = new OperandStack<>(this); + for (int i = 0; i < size; i++) { + T thisFrame = stack[i]; + T otherFrame = other.stack[i]; + if (thisFrame == TWO_SLOT_MARKER) { + assert otherFrame == TWO_SLOT_MARKER : "Positions of two-slot markers must match in merged operand stacks"; + merged.stack[i] = twoSlotMarker(); + } else { + assert otherFrame != TWO_SLOT_MARKER : "Positions of two-slot markers must match in merged operand stacks"; + merged.stack[i] = mergeFunction.apply(thisFrame, otherFrame); + } + } + return merged; + } + + private void pushFrame(T frame) { + assert size < stack.length : "Cannot push frames over the maximum stack size"; + stack[size++] = frame; + } + + private T popFrame() { + assert size > 0 : "Cannot pop frames from empty stack"; + T popped = stack[--size]; + stack[size] = null; + return popped; + } + + private T peekFrame(int depth) { + assert 0 <= depth && depth < size : "Depth out of range"; + return stack[size - depth - 1]; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + OperandStack that = (OperandStack) o; + return size == that.size && Objects.deepEquals(stack, that.stack); + } + + @Override + public int hashCode() { + return Objects.hash(Arrays.hashCode(stack), size); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("Operand stack:").append(System.lineSeparator()); + for (int i = size - 1; i >= 0; i--) { + T frame = stack[i]; + if (frame != TWO_SLOT_MARKER) { + sb.append("[").append(frame).append("]").append(System.lineSeparator()); + } + } + return sb.toString(); + } + } + + static final class LocalVariableTable { + + private final T[] variables; + + @SuppressWarnings("unchecked") + LocalVariableTable(int maxLocals) { + this.variables = (T[]) new Object[maxLocals]; + } + + LocalVariableTable(LocalVariableTable other) { + this.variables = other.variables.clone(); + } + + void put(T value, int index, boolean needsTwoSlots) { + if (variables[index] == TWO_SLOT_MARKER) { + /* + * 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. + */ + putFrame(null, index - 1); + } + int nextIndex = index + 1; + if (nextIndex < variables.length && variables[nextIndex] == TWO_SLOT_MARKER) { + putFrame(null, nextIndex); + } + putFrame(value, index); + if (needsTwoSlots) { + putFrame(twoSlotMarker(), nextIndex); + } + } + + T get(int index) { + assert 0 <= index && index < variables.length : "Index out of range"; + T frame = variables[index]; + assert frame != null && frame != TWO_SLOT_MARKER : "Cannot access non-value frame"; + return frame; + } + + private void transform(Predicate filterFunction, Function transformFunction) { + for (int i = 0; i < variables.length; i++) { + T frame = variables[i]; + if (frame != null && frame != TWO_SLOT_MARKER && filterFunction.test(frame)) { + variables[i] = transformFunction.apply(frame); + } + } + } + + private LocalVariableTable merge(LocalVariableTable other, BiFunction mergeFunction) { + LocalVariableTable merged = new LocalVariableTable<>(this); + for (int i = 0; i < variables.length; i++) { + T thisFrame = variables[i]; + T otherFrame = other.variables[i]; + if (thisFrame != null && otherFrame != null) { + /* + * 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 can + * still allow it, as the resulting value should not be used during execution + * anyway (or else the method would fail bytecode verification). + */ + if (thisFrame == TWO_SLOT_MARKER && otherFrame == TWO_SLOT_MARKER) { + merged.variables[i] = twoSlotMarker(); + } else if (thisFrame != TWO_SLOT_MARKER && otherFrame != TWO_SLOT_MARKER) { + merged.variables[i] = mergeFunction.apply(thisFrame, otherFrame); + } + } + } + return merged; + } + + private void putFrame(T frame, int index) { + assert 0 <= index && index < variables.length : "Index out of range"; + variables[index] = frame; + } + + @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.deepEquals(variables, that.variables); + } + + @Override + public int hashCode() { + return Arrays.hashCode(variables); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("Local variable table:").append(System.lineSeparator()); + for (int i = 0; i < variables.length; i++) { + T frame = variables[i]; + if (frame != TWO_SLOT_MARKER && frame != null) { + sb.append(i).append(": ").append(frame).append(System.lineSeparator()); + } + } + return sb.toString(); + } + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/dynamicaccessinference/dataflow/AbstractInterpreter.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/dynamicaccessinference/dataflow/AbstractInterpreter.java new file mode 100644 index 000000000000..fbf88a142276 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/dynamicaccessinference/dataflow/AbstractInterpreter.java @@ -0,0 +1,694 @@ +/* + * 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.dataflow; + +import java.util.List; +import java.util.stream.IntStream; + +import jdk.graal.compiler.bytecode.Bytecode; +import jdk.graal.compiler.bytecode.BytecodeStream; +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 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. + */ +public abstract class AbstractInterpreter extends ForwardDataFlowAnalyzer> { + + @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<>(method); + + int variableIndex = 0; + if (method.hasReceiver()) { + state.localVariableTable.put(defaultValue(), variableIndex, false); + variableIndex++; + } + + Signature signature = method.getSignature(); + int numOfParameters = signature.getParameterCount(false); + for (int i = 0; i < numOfParameters; i++) { + boolean parameterNeedsTwoSlots = signature.getParameterKind(i).needsTwoSlots(); + state.localVariableTable.put(defaultValue(), variableIndex, parameterNeedsTwoSlots); + variableIndex += parameterNeedsTwoSlots ? 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 exceptionState = new AbstractFrame<>(inState); + exceptionState.operandStack.clear(); + exceptionState.operandStack.push(defaultValue(), false); + return exceptionState; + } + + @Override + protected AbstractFrame copyState(AbstractFrame state) { + return new AbstractFrame<>(state); + } + + @Override + protected AbstractFrame mergeStates(AbstractFrame left, AbstractFrame right) { + return left.merge(right, this::merge); + } + + @Override + @SuppressWarnings("DuplicateBranchesInSwitch") + protected AbstractFrame processInstruction(AbstractFrame inState, BytecodeStream stream, Bytecode code) { + AbstractFrame outState = copyState(inState); + + var stack = outState.operandStack; + var variables = outState.localVariableTable; + + int bci = stream.currentBCI(); + int opcode = stream.currentBC(); + + InstructionContext context = new InstructionContext<>(code.getMethod(), bci, opcode, outState); + ConstantPool cp = code.getConstantPool(); + + // @formatter:off + // Checkstyle: stop + switch (opcode) { + case NOP : break; + case ACONST_NULL : handleConstant(context, JavaConstant.NULL_POINTER, false); break; + case ICONST_M1 : handleConstant(context, JavaConstant.forInt(-1), false); 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, JavaConstant.forInt(opcode - ICONST_0), false); break; + case LCONST_0 : // fall through + case LCONST_1 : handleConstant(context, JavaConstant.forLong(opcode - LCONST_0), true); break; + case FCONST_0 : // fall through + case FCONST_1 : // fall through + case FCONST_2 : handleConstant(context, JavaConstant.forFloat(opcode - FCONST_0), false); break; + case DCONST_0 : // fall through + case DCONST_1 : handleConstant(context, JavaConstant.forDouble(opcode - DCONST_0), true); break; + case BIPUSH : handleConstant(context, JavaConstant.forByte(stream.readByte()), false); break; + case SIPUSH : handleConstant(context, JavaConstant.forShort(stream.readShort()), false); break; + case LDC : // fall through + case LDC_W : handleConstant(context, lookupConstant(cp, stream.readCPI(), opcode), false); break; + case LDC2_W : handleConstant(context, lookupConstant(cp, stream.readCPI(), opcode), true); break; + case ILOAD : handleVariableLoad(context, stream.readLocalIndex(), false); break; + case LLOAD : handleVariableLoad(context, stream.readLocalIndex(), true); break; + case FLOAD : handleVariableLoad(context, stream.readLocalIndex(), false); break; + case DLOAD : handleVariableLoad(context, stream.readLocalIndex(), true); break; + case ALOAD : handleVariableLoad(context, stream.readLocalIndex(), false); break; + case ILOAD_0 : // fall through + case ILOAD_1 : // fall through + case ILOAD_2 : // fall through + case ILOAD_3 : handleVariableLoad(context, opcode - ILOAD_0, false); break; + case LLOAD_0 : // fall through + case LLOAD_1 : // fall through + case LLOAD_2 : // fall through + case LLOAD_3 : handleVariableLoad(context, opcode - LLOAD_0, true); break; + case FLOAD_0 : // fall through + case FLOAD_1 : // fall through + case FLOAD_2 : // fall through + case FLOAD_3 : handleVariableLoad(context, opcode - FLOAD_0, false); break; + case DLOAD_0 : // fall through + case DLOAD_1 : // fall through + case DLOAD_2 : // fall through + case DLOAD_3 : handleVariableLoad(context, opcode - DLOAD_0, true); break; + case ALOAD_0 : // fall through + case ALOAD_1 : // fall through + case ALOAD_2 : // fall through + case ALOAD_3 : handleVariableLoad(context, opcode - ALOAD_0, false); break; + case IALOAD : stack.pop(); stack.pop(); stack.push(defaultValue(), false); break; + case LALOAD : stack.pop(); stack.pop(); stack.push(defaultValue(), true); break; + case FALOAD : stack.pop(); stack.pop(); stack.push(defaultValue(), false); break; + case DALOAD : stack.pop(); stack.pop(); stack.push(defaultValue(), true); break; + case AALOAD : // fall through + case BALOAD : // fall through + case CALOAD : // fall through + case SALOAD : stack.pop(); stack.pop(); stack.push(defaultValue(), false); break; + case ISTORE : handleVariableStore(context, stream.readLocalIndex(), false); break; + case LSTORE : handleVariableStore(context, stream.readLocalIndex(), true); break; + case FSTORE : handleVariableStore(context, stream.readLocalIndex(), false); break; + case DSTORE : handleVariableStore(context, stream.readLocalIndex(), true); break; + case ASTORE : handleVariableStore(context, stream.readLocalIndex(), false); break; + case ISTORE_0 : // fall through + case ISTORE_1 : // fall through + case ISTORE_2 : // fall through + case ISTORE_3 : handleVariableStore(context, opcode - ISTORE_0, false); break; + case LSTORE_0 : // fall through + case LSTORE_1 : // fall through + case LSTORE_2 : // fall through + case LSTORE_3 : handleVariableStore(context, opcode - LSTORE_0, true); break; + case FSTORE_0 : // fall through + case FSTORE_1 : // fall through + case FSTORE_2 : // fall through + case FSTORE_3 : handleVariableStore(context, opcode - FSTORE_0, false); break; + case DSTORE_0 : // fall through + case DSTORE_1 : // fall through + case DSTORE_2 : // fall through + case DSTORE_3 : handleVariableStore(context, opcode - DSTORE_0, true); break; + case ASTORE_0 : // fall through + case ASTORE_1 : // fall through + case ASTORE_2 : // fall through + case ASTORE_3 : handleVariableStore(context, opcode - ASTORE_0, false); 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); break; + case POP : stack.applyPop(); break; + case POP2 : stack.applyPop2(); break; + case DUP : stack.applyDup(); break; + case DUP_X1 : stack.applyDupX1(); break; + case DUP_X2 : stack.applyDupX2(); break; + case DUP2 : stack.applyDup2(); break; + case DUP2_X1 : stack.applyDup2X1(); break; + case DUP2_X2 : stack.applyDup2X2(); break; + case SWAP : stack.applySwap(); break; + case IADD : // fall through + case ISUB : // fall through + case IMUL : // fall through + case IDIV : // fall through + case IREM : stack.pop(); stack.pop(); stack.push(defaultValue(), false); break; + case LADD : // fall through + case LSUB : // fall through + case LMUL : // fall through + case LDIV : // fall through + case LREM : stack.pop(); stack.pop(); stack.push(defaultValue(), true); break; + case FADD : // fall through + case FSUB : // fall through + case FMUL : // fall through + case FDIV : // fall through + case FREM : stack.pop(); stack.pop(); stack.push(defaultValue(), false); break; + case DADD : // fall through + case DSUB : // fall through + case DMUL : // fall through + case DDIV : // fall through + case DREM : stack.pop(); stack.pop(); stack.push(defaultValue(), true); break; + case INEG : stack.pop(); stack.push(defaultValue(), false); break; + case LNEG : stack.pop(); stack.push(defaultValue(), true); break; + case FNEG : stack.pop(); stack.push(defaultValue(), false); break; + case DNEG : stack.pop(); stack.push(defaultValue(), true); break; + case ISHL : // fall through + case ISHR : // fall through + case IUSHR : // fall through + case IAND : // fall through + case IOR : // fall through + case IXOR : stack.pop(); stack.pop(); stack.push(defaultValue(), false); break; + case LSHL : // fall through + case LSHR : // fall through + case LUSHR : // fall through + case LAND : // fall through + case LOR : // fall through + case LXOR : stack.pop(); stack.pop(); stack.push(defaultValue(), true); break; + case IINC : variables.put(defaultValue(), stream.readLocalIndex(), false); break; + case I2F : stack.pop(); stack.push(defaultValue(), false); break; + case I2D : stack.pop(); stack.push(defaultValue(), true); break; + case L2F : stack.pop(); stack.push(defaultValue(), false); break; + case L2D : stack.pop(); stack.push(defaultValue(), true); break; + case F2I : stack.pop(); stack.push(defaultValue(), false); break; + case F2L : // fall through + case F2D : stack.pop(); stack.push(defaultValue(), true); break; + case D2I : stack.pop(); stack.push(defaultValue(), false); break; + case D2L : stack.pop(); stack.push(defaultValue(), true); break; + case D2F : // fall through + case L2I : stack.pop(); stack.push(defaultValue(), false); break; + case I2L : stack.pop(); stack.push(defaultValue(), true); break; + case I2B : // fall through + case I2S : // fall through + case I2C : stack.pop(); stack.push(defaultValue(), false); break; + case LCMP : // fall through + case FCMPL : // fall through + case FCMPG : // fall through + case DCMPL : // fall through + case DCMPG : stack.pop(); stack.pop(); stack.push(defaultValue(), false); break; + case IFEQ : // fall through + case IFNE : // fall through + case IFLT : // fall through + case IFGE : // fall through + case IFGT : // fall through + case IFLE : stack.pop(); 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 : stack.pop(); stack.pop(); break; + case GOTO : break; + case JSR : // fall through + case RET : throw new DataFlowAnalysisException("Unsupported opcode " + opcode); + case TABLESWITCH : // fall through + case LOOKUPSWITCH : stack.pop(); break; + case IRETURN : // fall through + case LRETURN : // fall through + case FRETURN : // fall through + case DRETURN : // fall through + case ARETURN : stack.pop(); break; + case RETURN : break; + case GETSTATIC : handleStaticFieldLoad(context, lookupField(cp, stream.readCPI(), opcode, code.getMethod())); break; + case PUTSTATIC : onValueEscape(context, stack.pop()); break; + case GETFIELD : handleFieldLoad(context, lookupField(cp, stream.readCPI(), opcode, code.getMethod())); break; + case PUTFIELD : onValueEscape(context, stack.pop()); stack.pop(); break; + case INVOKEVIRTUAL : handleInvoke(context, lookupMethod(cp, stream.readCPI(), opcode, code.getMethod()), lookupAppendix(cp, stream.readCPI(), opcode)); break; + case INVOKESPECIAL : // fall through + case INVOKESTATIC : // fall through + case INVOKEINTERFACE: handleInvoke(context, lookupMethod(cp, stream.readCPI(), opcode, code.getMethod()), null); break; + case INVOKEDYNAMIC : handleInvoke(context, lookupMethod(cp, stream.readCPI4(), opcode, code.getMethod()), lookupAppendix(cp, stream.readCPI4(), opcode)); break; + case NEW : stack.push(defaultValue(), false); break; + case NEWARRAY : stack.pop(); stack.push(defaultValue(), false); break; + case ANEWARRAY : handleNewObjectArray(context, lookupType(cp, stream.readCPI(), opcode)); break; + case ARRAYLENGTH : stack.pop(); stack.push(defaultValue(), false); break; + case ATHROW : stack.pop(); break; + case CHECKCAST : handleCastCheck(context, lookupType(cp, stream.readCPI(), opcode)); break; + case INSTANCEOF : stack.pop(); stack.push(defaultValue(), false); break; + case MONITORENTER : // fall through + case MONITOREXIT : stack.pop(); break; + case MULTIANEWARRAY : popOperands(stack, stream.readUByte(bci + 3)); stack.push(defaultValue(), false); break; + case IFNULL : // fall through + case IFNONNULL : stack.pop(); break; + case GOTO_W : break; + case JSR_W : // fall through + case BREAKPOINT : // fall through + default : throw new DataFlowAnalysisException("Unsupported opcode " + opcode); + } + // @formatter:on + // Checkstyle: resume + + return outState; + } + + /** + * Execution context of a bytecode instruction. + * + * @param method The method to which this instruction belongs. + * @param bci The bytecode index of this instruction. + * @param opcode The opcode of this instruction. + * @param state The abstract state of the bytecode frame right before the execution of this + * instruction (its input state). Any modifications of the {@code state} will be + * reflected on the input state of successor instructions. + */ + protected record InstructionContext(ResolvedJavaMethod method, int bci, int opcode, AbstractFrame state) { + + } + + /** + * @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); + + protected abstract T loadConstant(InstructionContext context, Constant constant); + + protected abstract T loadType(InstructionContext context, JavaType type); + + protected abstract T loadVariable(InstructionContext context, T value); + + protected abstract T loadStaticField(InstructionContext context, JavaField field); + + protected abstract T storeVariable(InstructionContext context, T value); + + protected abstract void storeArrayElement(InstructionContext context, T array, T index, T value); + + protected abstract T invokeNonVoidMethod(InstructionContext context, JavaMethod method, T receiver, List operands); + + protected abstract T newObjectArray(InstructionContext context, JavaType type, T size); + + protected abstract T checkCast(InstructionContext context, JavaType type, T object); + + /** + * This method is invoked whenever a {@code value} escapes {@link AbstractFrame}, be it by + * storing it in an array, a field, or using it as a method argument. + */ + protected abstract void onValueEscape(InstructionContext context, T value); + + protected abstract Object lookupConstant(ConstantPool constantPool, int cpi, int opcode); + + protected abstract JavaType lookupType(ConstantPool constantPool, int cpi, int opcode); + + protected abstract JavaMethod lookupMethod(ConstantPool constantPool, int cpi, int opcode, ResolvedJavaMethod caller); + + protected abstract JavaConstant lookupAppendix(ConstantPool constantPool, int cpi, int opcode); + + protected abstract JavaField lookupField(ConstantPool constantPool, int cpi, int opcode, ResolvedJavaMethod caller); + + private List popOperands(AbstractFrame.OperandStack stack, int n) { + return IntStream.range(0, n).mapToObj(i -> stack.pop()).toList().reversed(); + } + + private void handleConstant(InstructionContext context, Object value, boolean needsTwoSlots) { + var stack = context.state.operandStack; + if (value == null) { + /* + * The constant is an unresolved JVM_CONSTANT_Dynamic, JVM_CONSTANT_MethodHandle or + * JVM_CONSTANT_MethodType. + */ + stack.push(loadConstant(context, null), needsTwoSlots); + } else { + if (value instanceof Constant constant) { + stack.push(loadConstant(context, constant), needsTwoSlots); + } else if (value instanceof JavaType type) { + assert !needsTwoSlots : "Type references occupy a single stack slot"; + stack.push(loadType(context, type), false); + } + } + } + + private void handleVariableLoad(InstructionContext context, int index, boolean needsTwoSlots) { + T value = context.state.localVariableTable.get(index); + context.state.operandStack.push(loadVariable(context, value), needsTwoSlots); + } + + private void handleVariableStore(InstructionContext context, int index, boolean needsTwoSlots) { + T value = context.state.operandStack.pop(); + context.state.localVariableTable.put(storeVariable(context, value), index, needsTwoSlots); + } + + private void handleInvoke(InstructionContext context, JavaMethod method, JavaConstant appendix) { + var stack = context.state.operandStack; + if (appendix != null) { + stack.push(defaultValue(), false); + } + /* + * 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 (method instanceof ResolvedJavaMethod resolved) { + hasReceiver = resolved.hasReceiver(); + } else { + hasReceiver = context.opcode != INVOKESTATIC && context.opcode != INVOKEDYNAMIC; + } + + Signature signature = method.getSignature(); + + T receiver = null; + if (hasReceiver) { + receiver = stack.pop(); + onValueEscape(context, receiver); + } + List operands = popOperands(stack, signature.getParameterCount(false)); + operands.forEach(op -> onValueEscape(context, op)); + + JavaKind returnKind = signature.getReturnKind(); + if (!returnKind.equals(JavaKind.Void)) { + T returnValue = invokeNonVoidMethod(context, method, receiver, operands); + stack.push(returnValue, returnKind.needsTwoSlots()); + } + } + + private void handleStaticFieldLoad(InstructionContext context, JavaField field) { + T value = loadStaticField(context, field); + context.state.operandStack.push(value, field.getJavaKind().needsTwoSlots()); + } + + private void handleFieldLoad(InstructionContext context, JavaField field) { + context.state.operandStack.pop(); + context.state.operandStack.push(defaultValue(), field.getJavaKind().needsTwoSlots()); + } + + private void handleNewObjectArray(InstructionContext context, JavaType type) { + T size = context.state.operandStack.pop(); + context.state.operandStack.push(newObjectArray(context, type, size), false); + } + + private void handleArrayElementStore(InstructionContext context) { + var stack = context.state.operandStack; + T value = stack.pop(); + T index = stack.pop(); + T array = stack.pop(); + onValueEscape(context, value); + storeArrayElement(context, array, index, value); + } + + private void handleCastCheck(InstructionContext context, JavaType type) { + T object = context.state.operandStack.pop(); + context.state.operandStack.push(checkCast(context, type, object), false); + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/dynamicaccessinference/dataflow/DataFlowAnalysisException.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/dynamicaccessinference/dataflow/DataFlowAnalysisException.java new file mode 100644 index 000000000000..3b76a3faa6a7 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/dynamicaccessinference/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.dynamicaccessinference.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/dynamicaccessinference/dataflow/ForwardDataFlowAnalyzer.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/dynamicaccessinference/dataflow/ForwardDataFlowAnalyzer.java new file mode 100644 index 000000000000..15b69bc7e6fb --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/dynamicaccessinference/dataflow/ForwardDataFlowAnalyzer.java @@ -0,0 +1,236 @@ +/* + * 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.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.removeFirst(); + + 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)) { + /* + * If a fixed point is reached within a basic block, further instructions of that + * block do not have to be processed. This early exit is signaled by returning null. + */ + return null; + } else { + states.put(stream.nextBCI(), outState); + } + 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: + *

+ * + * @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/phases/AnalysisGraphBuilderPhase.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/phases/AnalysisGraphBuilderPhase.java index ed5a4290ba1b..77d5c2510d50 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.ConstantExpressionRegistry; 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) { + ConstantExpressionRegistry constantExpressionRegistry = hostVM.getConstantExpressionRegistry(); + if (strictDynamicAccessInferenceIsApplicable() && constantExpressionRegistry != null) { + constantExpressionRegistry.inferConstantExpressions(getCode()); + } + 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..038f5646a862 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 @@ -44,7 +44,6 @@ 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; @@ -71,6 +70,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 +128,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 +143,8 @@ private ReflectionPlugins(ImageClassLoader imageClassLoader, AnnotationSubstitut dynamicAccessDetectionFeature = trackDynamicAccess ? DynamicAccessDetectionFeature.instance() : null; this.classInitializationSupport = (ClassInitializationSupport) ImageSingletons.lookup(RuntimeClassInitializationSupport.class); + + this.inferenceLog = DynamicAccessInferenceLog.singletonOrNull(); } public static void registerInvocationPlugins(ImageClassLoader imageClassLoader, AnnotationSubstitutionProcessor annotationSubstitutions, @@ -150,6 +154,24 @@ public static void registerInvocationPlugins(ImageClassLoader imageClassLoader, rp.registerClassPlugins(plugins); } + /** + * Guards plugin registrations that should be active *only* when the inference strategy for + * dynamic access invocations is non-strict, i.e., the success of plugin applications may depend + * on graph optimizations that can uncover more optimization potential. Since the non-strict + * inference can lead to unstable results, i.e., a change in optimization level may lead to less + * statically folded dynamic access invocations and subsequently to missing registration errors, + * the intention is to transition all the reflection plugins to a strict invocation inference. + * In the meantime, we only enforce the strict inference for the subset of the plugins that can + * 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 Object[] EMPTY_ARGUMENTS = {}; + 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 +198,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 +277,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 +316,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 +332,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 +351,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 +412,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 +426,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, EMPTY_ARGUMENTS, ex.getClass(), ex.getMessage(), false); } - return pushConstant(b, targetMethod, targetParameters, JavaKind.Object, lookup, false) != null; + return pushConstant(b, targetMethod, null, EMPTY_ARGUMENTS, JavaKind.Object, lookup, false, false) != null; } /** @@ -416,7 +445,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 +456,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 +501,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, EMPTY_ARGUMENTS, result, false); return true; } @@ -481,23 +513,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 subjectToStrictDynamicAccessInference, 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, subjectToStrictDynamicAccessInference); } } } 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 subjectToStrictDynamicAccessInference) { + registerFoldInvocationPlugin(plugins, reflectionMethod, alwaysAllowConstantFolding, subjectToStrictDynamicAccessInference); } - private void registerFoldInvocationPlugin(InvocationPlugins plugins, Method reflectionMethod, Predicate allowConstantFolding) { + private void registerFoldInvocationPlugin(InvocationPlugins plugins, Method reflectionMethod, Predicate allowConstantFolding, boolean subjectToStrictDynamicAccessInference) { 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 +544,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, subjectToStrictDynamicAccessInference); } }); } @@ -522,7 +554,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 subjectToStrictDynamicAccessInference) { assert b.getMetaAccess().lookupJavaMethod(reflectionMethod).equals(targetMethod) : "Fold method mismatch: " + reflectionMethod + " != " + targetMethod; Object receiverValue; @@ -556,17 +588,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(), subjectToStrictDynamicAccessInference); } catch (Throwable ex) { - return throwException(b, targetMethod, targetParameters, ex.getClass(), ex.getMessage()); + return throwException(b, targetMethod, receiverValue, argValues, ex.getClass(), ex.getMessage(), subjectToStrictDynamicAccessInference); } JavaKind returnKind = targetMethod.getSignature().getReturnKind(); @@ -574,11 +602,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, subjectToStrictDynamicAccessInference); return true; } - return pushConstant(b, targetMethod, targetParameters, returnKind, returnValue, false) != null; + return pushConstant(b, targetMethod, receiverValue, argValues, returnKind, returnValue, false, subjectToStrictDynamicAccessInference) != null; } private void registerBulkInvocationPlugin(InvocationPlugins plugins, Class declaringClass, String methodName, Consumer registrationCallback) { @@ -766,8 +794,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 subjectToStrictDynamicAccessInference) { Object intrinsicValue = getIntrinsic(b, returnValue == null && allowNullReturnValue ? NULL_MARKER : returnValue); if (intrinsicValue == null) { return null; @@ -783,11 +811,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, subjectToStrictDynamicAccessInference); 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 subjectToStrictDynamicAccessInference) { /* Get the exception throwing method that has a message parameter. */ Method exceptionMethod = ExceptionSynthesizer.throwExceptionMethodOrNull(exceptionClass, String.class); if (exceptionMethod == null) { @@ -798,28 +827,63 @@ 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, subjectToStrictDynamicAccessInference); + 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) { + /** + * Log successful constant folding of an invocation. + * + * @param subjectToStrictDynamicAccessInference if {@code true} log successful folding + * information via {@link DynamicAccessInferenceLog} for {@code targetMethod} + * invocations that are also handled by {@link StrictDynamicAccessInferenceFeature}. + * In that case the reflection plugin registration is also guarded by + * {@link ReflectionPlugins#nonStrictDynamicAccessInference()}. In other words: this + * produces a report with all dynamic invocations that would be handled by + * {@link StrictDynamicAccessInferenceFeature} if it was enabled. Additionally, it + * avoids logging and warning for non-strict folding of invocations which are not + * reflective, such as {@link Integer#toString()}. + */ + private void traceConstant(GraphBuilderContext b, ResolvedJavaMethod targetMethod, Object receiver, Object[] arguments, Object value, boolean subjectToStrictDynamicAccessInference) { + if (subjectToStrictDynamicAccessInference && inferenceLog != null) { + inferenceLog.logFolding(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() + ")" + - " was reduced to the constant " + value); + String receiverAndArguments = buildReceiverAndArgumentsString(receiver, arguments); + System.out.printf("Call to %s reached in %s with %s was reduced to the constant %s%n", + targetMethod.format("%H.%n(%p)"), b.getMethod().format("%H.%n(%p)"), receiverAndArguments, value); } } - private static void traceException(GraphBuilderContext b, ResolvedJavaMethod targetMethod, Supplier targetParameters, Class exceptionClass) { + /** + * Log constant folding of an invocation which was inferred to throw an exception at run-time. + */ + private void traceException(GraphBuilderContext b, ResolvedJavaMethod targetMethod, Object receiver, Object[] arguments, Class exceptionClass, + boolean subjectToStrictDynamicAccessInference) { + if (subjectToStrictDynamicAccessInference && 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() + ")" + - " was reduced to a \"throw new " + exceptionClass.getName() + "(...)\""); + String receiverAndArguments = buildReceiverAndArgumentsString(receiver, arguments); + System.out.printf("Call to %s reached in %s with %s was reduced to a \"throw new %s(...)\"%n", + targetMethod.format("%H.%n(%p)"), b.getMethod().format("%H.%n(%p)"), receiverAndArguments, exceptionClass.getName()); } } + + private static String buildReceiverAndArgumentsString(Object receiver, Object[] arguments) { + String argumentListString = Stream.of(arguments) + .map(arg -> arg instanceof Object[] array ? Arrays.toString(array) : Objects.toString(arg)) + .collect(Collectors.joining(", ")); + return receiver != null + ? String.format("receiver \"%s\" and arguments (%s)", receiver, argumentListString) + : String.format("arguments (%s)", argumentListString); + } } 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 faadc45c4f2f..8752be53769a 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 @@ -101,6 +101,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; @@ -446,13 +448,17 @@ 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 = DynamicAccessInferenceLog.singletonOrNull(); 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() { @@ -467,6 +473,14 @@ public boolean defaultHandler(GraphBuilderContext b, ResolvedJavaMethod targetMe boolean callerInScope = MissingRegistrationSupport.singleton().reportMissingRegistrationErrors(callerClass); if (callerInScope && reason.duringAnalysis() && reason != ParsingReason.JITCompilation) { b.add(ReachabilityRegistrationNode.create(proxyRegistrationRunnable, reason)); + if (inferenceLog != null) { + Object ignore = DynamicAccessInferenceLog.ignoreArgument(); + Class[] interfaces = extractClassArray(b, annotationSubstitutions, args[1]); + Object[] logArguments = targetMethod.getParameters().length == 3 + ? new Object[]{ignore, interfaces, ignore} + : new Object[]{ignore, interfaces}; + inferenceLog.logRegistration(b, reason, targetMethod, null, logArguments); + } return true; } @@ -680,6 +694,8 @@ private static FixedNode unwrapNode(FixedNode node) { } else if (successor instanceof AbstractBeginNode) { /* Useless block begins can occur during parsing or graph decoding. */ successor = ((AbstractBeginNode) successor).next(); + } else if (successor instanceof ReachabilityRegistrationNode) { + successor = ((ReachabilityRegistrationNode) successor).next(); } else { return successor; }