diff --git a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/AbstractAnalysisEngine.java b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/AbstractAnalysisEngine.java index d2cbc1088009..9182cad59297 100644 --- a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/AbstractAnalysisEngine.java +++ b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/AbstractAnalysisEngine.java @@ -80,6 +80,7 @@ public abstract class AbstractAnalysisEngine implements BigBang { protected final int maxConstantObjectsPerType; protected final boolean profileConstantObjects; protected final boolean optimizeReturnedParameter; + protected final boolean useExperimentalReachabilityAnalysis; protected final OptionValues options; protected final DebugContext debug; @@ -124,6 +125,8 @@ public AbstractAnalysisEngine(OptionValues options, AnalysisUniverse universe, H maxConstantObjectsPerType = PointstoOptions.MaxConstantObjectsPerType.getValue(options); profileConstantObjects = PointstoOptions.ProfileConstantObjects.getValue(options); optimizeReturnedParameter = PointstoOptions.OptimizeReturnedParameter.getValue(options); + useExperimentalReachabilityAnalysis = PointstoOptions.UseExperimentalReachabilityAnalysis.getValue(options); + this.snippetReflectionProvider = snippetReflectionProvider; this.constantReflectionProvider = constantReflectionProvider; this.wordTypes = wordTypes; @@ -250,6 +253,11 @@ public boolean optimizeReturnedParameter() { return optimizeReturnedParameter; } + @Override + public boolean isPointsToAnalysis() { + return !useExperimentalReachabilityAnalysis; + } + public void profileConstantObject(AnalysisType type) { if (profileConstantObjects) { PointsToAnalysis.ConstantObjectsProfiler.registerConstant(type); diff --git a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/BigBang.java b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/BigBang.java index ff6e4183ab98..df43a639476c 100644 --- a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/BigBang.java +++ b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/BigBang.java @@ -62,6 +62,8 @@ public interface BigBang extends ReachabilityAnalysis { UnsupportedFeatures getUnsupportedFeatures(); + boolean isPointsToAnalysis(); + /** * Checks if all user defined limitations such as the number of types are satisfied. */ diff --git a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/meta/AnalysisMethod.java b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/meta/AnalysisMethod.java index 34c3e40883b2..6f6d212ca929 100644 --- a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/meta/AnalysisMethod.java +++ b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/meta/AnalysisMethod.java @@ -577,6 +577,9 @@ public Set getCallers() { /** Get the list of all invoke locations for this method, as inferred by the static analysis. */ public abstract List getInvokeLocations(); + /** Get the node markers used to store per-node mappings to metadata for encoded nodes. */ + public abstract Iterable getEncodedNodeReferences(); + /** * Returns true if this method is a native entrypoint, i.e. it may be called from the host * environment. diff --git a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/meta/PointsToAnalysisMethod.java b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/meta/PointsToAnalysisMethod.java index 6250d4df1769..1767d7e519f7 100644 --- a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/meta/PointsToAnalysisMethod.java +++ b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/meta/PointsToAnalysisMethod.java @@ -43,6 +43,7 @@ import com.oracle.graal.pointsto.util.ConcurrentLightHashMap; import com.oracle.svm.common.meta.MultiMethod; +import jdk.graal.compiler.nodes.EncodedGraph; import jdk.vm.ci.code.BytecodePosition; import jdk.vm.ci.meta.JavaKind; import jdk.vm.ci.meta.ResolvedJavaMethod; @@ -125,6 +126,11 @@ public static Object unwrapInvokeReason(Object reason) { return reason; } + @Override + public Iterable getEncodedNodeReferences() { + return typeFlow.getMethodFlowsGraph().getNodeFlows().getKeys(); + } + @Override public List getInvokeLocations() { List locations = new ArrayList<>(); diff --git a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/results/ReachabilitySimplifier.java b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/results/ReachabilitySimplifier.java new file mode 100644 index 000000000000..f98fa1216f66 --- /dev/null +++ b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/results/ReachabilitySimplifier.java @@ -0,0 +1,346 @@ +/* + * 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.graal.pointsto.results; + +import java.util.function.Function; +import java.util.function.Supplier; + +import com.oracle.graal.pointsto.heap.ImageHeapConstant; +import com.oracle.graal.pointsto.meta.AnalysisField; +import com.oracle.graal.pointsto.meta.AnalysisMethod; +import com.oracle.graal.pointsto.meta.AnalysisType; +import com.oracle.graal.pointsto.util.AnalysisError; + +import jdk.graal.compiler.core.common.type.AbstractObjectStamp; +import jdk.graal.compiler.core.common.type.ObjectStamp; +import jdk.graal.compiler.core.common.type.Stamp; +import jdk.graal.compiler.core.common.type.StampFactory; +import jdk.graal.compiler.core.common.type.TypeReference; +import jdk.graal.compiler.graph.Node; +import jdk.graal.compiler.nodes.CallTargetNode; +import jdk.graal.compiler.nodes.ConstantNode; +import jdk.graal.compiler.nodes.FixedNode; +import jdk.graal.compiler.nodes.FixedWithNextNode; +import jdk.graal.compiler.nodes.FrameState; +import jdk.graal.compiler.nodes.Invoke; +import jdk.graal.compiler.nodes.LogicConstantNode; +import jdk.graal.compiler.nodes.LogicNode; +import jdk.graal.compiler.nodes.NodeView; +import jdk.graal.compiler.nodes.PhiNode; +import jdk.graal.compiler.nodes.PiNode; +import jdk.graal.compiler.nodes.StructuredGraph; +import jdk.graal.compiler.nodes.ValueNode; +import jdk.graal.compiler.nodes.extended.BytecodeExceptionNode; +import jdk.graal.compiler.nodes.extended.FieldOffsetProvider; +import jdk.graal.compiler.nodes.java.ClassIsAssignableFromNode; +import jdk.graal.compiler.nodes.java.InstanceOfNode; +import jdk.graal.compiler.nodes.java.MethodCallTargetNode; +import jdk.graal.compiler.nodes.spi.CoreProviders; +import jdk.graal.compiler.nodes.spi.LimitedValueProxy; +import jdk.graal.compiler.nodes.spi.SimplifierTool; +import jdk.graal.compiler.nodes.util.GraphUtil; +import jdk.graal.compiler.phases.common.CanonicalizerPhase.CustomSimplification; +import jdk.graal.compiler.phases.common.inlining.InliningUtil; +import jdk.graal.compiler.replacements.nodes.MacroInvokable; +import jdk.vm.ci.meta.JavaKind; +import jdk.vm.ci.meta.ResolvedJavaType; + +/** + * Simplify graphs based on reachability information tracked by the static analysis. + */ +class ReachabilitySimplifier implements CustomSimplification { + + protected final StrengthenGraphs strengthenGraphs; + protected final StructuredGraph graph; + + /** + * For runtime compiled methods, we must be careful to ensure new SubstrateTypes are not created + * due to the optimizations performed during the + * {@link StrengthenGraphs.AnalysisStrengthenGraphsPhase}. + */ + protected final Function toTargetFunction; + + ReachabilitySimplifier(StrengthenGraphs strengthenGraphs, AnalysisMethod method, StructuredGraph graph) { + this.strengthenGraphs = strengthenGraphs; + this.graph = graph; + this.toTargetFunction = strengthenGraphs.bb.getHostVM().getStrengthenGraphsToTargetFunction(method.getMultiMethodKey()); + } + + @Override + public void simplify(Node n, SimplifierTool tool) { + if (n instanceof ValueNode node) { + tryImproveStamp(node, tool); + } + + if (strengthenGraphs.simplifyDelegate(n, tool)) { + // Handled in the delegate simplification. + return; + } + + switch (n) { + case InstanceOfNode node -> handleInstanceOf(node, tool); + case ClassIsAssignableFromNode node -> handleClassIsAssignableFrom(node, tool); + case BytecodeExceptionNode node -> handleBytecodeException(node, tool); + case FrameState node -> handleFrameState(node); + case PiNode node -> handlePi(node, tool); + case Invoke invoke -> handleInvoke(invoke, tool); + case null, default -> { + } + } + } + + protected void tryImproveStamp(ValueNode node, SimplifierTool tool) { + if (!(node instanceof LimitedValueProxy) && !(node instanceof PhiNode) && !(node instanceof MacroInvokable)) { + /* + * The stamp of proxy nodes and phi nodes is inferred automatically, so we do not need + * to improve them. Macro nodes prohibit changing their stamp because it is derived from + * the macro's fallback invoke. First ask the node to improve the stamp itself, to + * incorporate already improved input stamps. + */ + node.inferStamp(); + /* + * Since this new stamp is not based on a type flow, it is valid for the entire method + * and we can update the stamp of the node directly. We do not need an anchored PiNode. + */ + updateStampInPlace(node, strengthenStamp(node.stamp(NodeView.DEFAULT)), tool); + } + } + + protected void updateStampInPlace(ValueNode node, Stamp newStamp, SimplifierTool tool) { + if (newStamp != null) { + Stamp oldStamp = node.stamp(NodeView.DEFAULT); + Stamp computedStamp = oldStamp.improveWith(newStamp); + if (!oldStamp.equals(computedStamp)) { + node.setStamp(newStamp); + tool.addToWorkList(node.usages()); + } + } + } + + protected void handleInstanceOf(InstanceOfNode node, SimplifierTool tool) { + ObjectStamp oldStamp = node.getCheckedStamp(); + Stamp newStamp = strengthenStamp(oldStamp); + if (newStamp != null) { + LogicNode replacement = graph.addOrUniqueWithInputs(InstanceOfNode.createHelper((ObjectStamp) oldStamp.improveWith(newStamp), node.getValue(), node.profile(), node.getAnchor())); + /* + * GR-59681: Once isAssignable is implemented for BaseLayerType, this check can be + * removed + */ + AnalysisError.guarantee(node != replacement, "The new stamp needs to be different from the old stamp"); + node.replaceAndDelete(replacement); + tool.addToWorkList(replacement); + } else { + strengthenGraphs.maybeAssignInstanceOfProfiles(node); + } + } + + protected void handleClassIsAssignableFrom(ClassIsAssignableFromNode node, SimplifierTool tool) { + if (strengthenGraphs.isClosedTypeWorld) { + /* + * If the constant receiver of a Class#isAssignableFrom is an unreachable type we can + * constant-fold the ClassIsAssignableFromNode to false. See also + * MethodTypeFlowBuilder#ignoreConstant where we avoid marking the corresponding type as + * reachable just because it is used by the ClassIsAssignableFromNode. We only apply + * this optimization if it's a closed type world, for open world we cannot fold the type + * check since the type may be used later. + */ + AnalysisType nonReachableType = asConstantNonReachableType(node.getThisClass(), tool); + if (nonReachableType != null) { + node.replaceAndDelete(LogicConstantNode.contradiction(graph)); + } + } + } + + protected void handleBytecodeException(BytecodeExceptionNode node, SimplifierTool tool) { + /* + * We do not want a type to be reachable only to be used for the error message of a + * ClassCastException. Therefore, in that case we replace the java.lang.Class with a + * java.lang.String that is then used directly in the error message. We can apply this + * optimization optimistically for both closed and open type world. + */ + if (node.getExceptionKind() == BytecodeExceptionNode.BytecodeExceptionKind.CLASS_CAST) { + AnalysisType nonReachableType = asConstantNonReachableType(node.getArguments().get(1), tool); + if (nonReachableType != null) { + node.getArguments().set(1, ConstantNode.forConstant(tool.getConstantReflection().forString(strengthenGraphs.getTypeName(nonReachableType)), tool.getMetaAccess(), graph)); + } + } + } + + private static AnalysisType asConstantNonReachableType(ValueNode value, CoreProviders providers) { + if (value != null && value.isConstant()) { + AnalysisType expectedType = (AnalysisType) providers.getConstantReflection().asJavaType(value.asConstant()); + if (expectedType != null && !expectedType.isReachable()) { + return expectedType; + } + } + return null; + } + + protected void handleFrameState(FrameState node) { + /* + * We do not want a constant to be reachable only to be used for debugging purposes in a + * FrameState. + */ + for (int i = 0; i < node.values().size(); i++) { + if (node.values().get(i) instanceof ConstantNode constantNode && constantNode.getValue() instanceof ImageHeapConstant imageHeapConstant && !imageHeapConstant.isReachable()) { + node.values().set(i, ConstantNode.defaultForKind(JavaKind.Object, graph)); + } + if (node.values().get(i) instanceof FieldOffsetProvider fieldOffsetProvider && !((AnalysisField) fieldOffsetProvider.getField()).isUnsafeAccessed()) { + /* + * We use a unique marker constant as the replacement value, so that a search in the + * code base for the value leads us to here. + */ + node.values().set(i, ConstantNode.forIntegerKind(fieldOffsetProvider.asNode().getStackKind(), 0xDEA51106, graph)); + } + } + } + + protected void handlePi(PiNode node, SimplifierTool tool) { + Stamp oldStamp = node.piStamp(); + Stamp newStamp = strengthenStamp(oldStamp); + if (newStamp != null) { + Stamp newPiStamp = oldStamp.improveWith(newStamp); + /* + * GR-59681: Once isAssignable is implemented for BaseLayerType, this check can be + * removed + */ + AnalysisError.guarantee(!newPiStamp.equals(oldStamp), "The new stamp needs to be different from the old stamp"); + node.strengthenPiStamp(newPiStamp); + tool.addToWorkList(node); + } + } + + private void handleInvoke(Invoke invoke, SimplifierTool tool) { + if (invoke.callTarget() instanceof MethodCallTargetNode) { + maybeMarkUnreachable(invoke, tool); + } + } + + protected boolean maybeMarkUnreachable(Invoke invoke, SimplifierTool tool) { + MethodCallTargetNode callTarget = (MethodCallTargetNode) invoke.callTarget(); + AnalysisMethod targetMethod = (AnalysisMethod) callTarget.targetMethod(); + if (callTarget.invokeKind().isDirect() && !targetMethod.isSimplyImplementationInvoked()) { + /* + * This is a direct call to a method that the static analysis did not see as invoked. + * This can happen when the receiver is always null. In most cases, the method profile + * also has a length of 0 and the below code to kill the invoke would trigger. But when + * only running the reachability analysis, there is no detailed list of callees. + */ + unreachableInvoke(invoke, tool, () -> location(invoke) + ": target method is not marked as simply implementation invoked"); + return true; + } + return false; + } + + /** + * The invoke has no callee, i.e., it is unreachable. + */ + protected void unreachableInvoke(Invoke invoke, SimplifierTool tool, Supplier messageSupplier) { + if (invoke.getInvokeKind() != CallTargetNode.InvokeKind.Static) { + /* + * Ensure that a null check for the receiver remains in the graph. There should be + * already an explicit null check in the graph, but we are paranoid and check again. + */ + InliningUtil.nonNullReceiver(invoke); + } + + makeUnreachable(invoke.asFixedNode(), tool, messageSupplier); + } + + protected void makeUnreachable(FixedNode node, CoreProviders providers, Supplier message) { + FixedNode unreachableNode = strengthenGraphs.createUnreachable(graph, providers, message); + ((FixedWithNextNode) node.predecessor()).setNext(unreachableNode); + GraphUtil.killCFG(node); + } + + protected String location(Invoke invoke) { + return "method " + StrengthenGraphs.getQualifiedName(graph) + ", node " + invoke; + } + + protected String location(Node node) { + return "method " + StrengthenGraphs.getQualifiedName(graph) + ", node " + node; + } + + private Stamp strengthenStamp(Stamp s) { + if (!(s instanceof AbstractObjectStamp stamp)) { + return null; + } + AnalysisType originalType = (AnalysisType) stamp.type(); + if (originalType == null) { + return null; + } + + /* In open world the type may become reachable later. */ + if (strengthenGraphs.isClosedTypeWorld && !originalType.isReachable()) { + /* We must be in dead code. */ + if (stamp.nonNull()) { + /* We must be in dead code. */ + return StampFactory.empty(JavaKind.Object); + } else { + return StampFactory.alwaysNull(); + } + } + + AnalysisType singleImplementorType = strengthenGraphs.getSingleImplementorType(originalType); + if (singleImplementorType != null && (!stamp.isExactType() || !singleImplementorType.equals(originalType))) { + ResolvedJavaType targetType = toTargetFunction.apply(singleImplementorType); + if (targetType != null) { + TypeReference typeRef = TypeReference.createExactTrusted(targetType); + return StampFactory.object(typeRef, stamp.nonNull()); + } + } + + AnalysisType strengthenType = strengthenGraphs.getStrengthenStampType(originalType); + if (originalType.equals(strengthenType)) { + /* Nothing to strengthen. */ + return null; + } + + Stamp newStamp; + if (strengthenType == null) { + /* The type and its subtypes are not instantiated. */ + if (stamp.nonNull()) { + /* We must be in dead code. */ + newStamp = StampFactory.empty(JavaKind.Object); + } else { + newStamp = StampFactory.alwaysNull(); + } + + } else { + if (stamp.isExactType()) { + /* We must be in dead code. */ + newStamp = StampFactory.empty(JavaKind.Object); + } else { + ResolvedJavaType targetType = toTargetFunction.apply(strengthenType); + if (targetType == null) { + return null; + } + TypeReference typeRef = TypeReference.createTrustedWithoutAssumptions(targetType); + newStamp = StampFactory.object(typeRef, stamp.nonNull()); + } + } + return newStamp; + } +} diff --git a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/results/StrengthenGraphs.java b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/results/StrengthenGraphs.java index 222fdb1cb6e9..ace51cc01842 100644 --- a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/results/StrengthenGraphs.java +++ b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/results/StrengthenGraphs.java @@ -24,108 +24,57 @@ */ package com.oracle.graal.pointsto.results; -import java.util.ArrayList; import java.util.Collection; -import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicLong; -import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Stream; -import org.graalvm.collections.EconomicSet; -import org.graalvm.nativeimage.AnnotationAccess; - import com.oracle.graal.pointsto.BigBang; -import com.oracle.graal.pointsto.PointsToAnalysis; import com.oracle.graal.pointsto.api.HostVM; import com.oracle.graal.pointsto.api.PointstoOptions; import com.oracle.graal.pointsto.flow.AnalysisParsedGraph; -import com.oracle.graal.pointsto.flow.InvokeTypeFlow; -import com.oracle.graal.pointsto.flow.MethodFlowsGraph; -import com.oracle.graal.pointsto.flow.MethodTypeFlow; -import com.oracle.graal.pointsto.flow.PrimitiveFilterTypeFlow; -import com.oracle.graal.pointsto.flow.TypeFlow; -import com.oracle.graal.pointsto.heap.ImageHeapConstant; import com.oracle.graal.pointsto.infrastructure.Universe; -import com.oracle.graal.pointsto.meta.AnalysisField; import com.oracle.graal.pointsto.meta.AnalysisMethod; import com.oracle.graal.pointsto.meta.AnalysisType; -import com.oracle.graal.pointsto.meta.PointsToAnalysisField; -import com.oracle.graal.pointsto.meta.PointsToAnalysisMethod; -import com.oracle.graal.pointsto.typestate.PrimitiveConstantTypeState; import com.oracle.graal.pointsto.typestate.TypeState; -import com.oracle.graal.pointsto.util.AnalysisError; -import com.oracle.svm.core.annotate.Delete; import com.oracle.svm.util.ImageBuildStatistics; -import jdk.graal.compiler.core.common.type.AbstractObjectStamp; -import jdk.graal.compiler.core.common.type.IntegerStamp; -import jdk.graal.compiler.core.common.type.ObjectStamp; -import jdk.graal.compiler.core.common.type.Stamp; -import jdk.graal.compiler.core.common.type.StampFactory; -import jdk.graal.compiler.core.common.type.TypeReference; import jdk.graal.compiler.debug.DebugContext; import jdk.graal.compiler.debug.GraalError; import jdk.graal.compiler.graph.Node; -import jdk.graal.compiler.graph.NodeBitMap; -import jdk.graal.compiler.graph.NodeInputList; -import jdk.graal.compiler.graph.NodeMap; -import jdk.graal.compiler.nodeinfo.InputType; import jdk.graal.compiler.nodes.AbstractBeginNode; import jdk.graal.compiler.nodes.CallTargetNode; import jdk.graal.compiler.nodes.ConstantNode; -import jdk.graal.compiler.nodes.FixedGuardNode; import jdk.graal.compiler.nodes.FixedNode; -import jdk.graal.compiler.nodes.FixedWithNextNode; -import jdk.graal.compiler.nodes.FrameState; import jdk.graal.compiler.nodes.GraphEncoder; import jdk.graal.compiler.nodes.GraphState; import jdk.graal.compiler.nodes.IfNode; import jdk.graal.compiler.nodes.Invoke; -import jdk.graal.compiler.nodes.InvokeWithExceptionNode; -import jdk.graal.compiler.nodes.LogicConstantNode; import jdk.graal.compiler.nodes.LogicNode; -import jdk.graal.compiler.nodes.NodeView; -import jdk.graal.compiler.nodes.ParameterNode; -import jdk.graal.compiler.nodes.PhiNode; import jdk.graal.compiler.nodes.PiNode; -import jdk.graal.compiler.nodes.StartNode; -import jdk.graal.compiler.nodes.StateSplit; import jdk.graal.compiler.nodes.StructuredGraph; -import jdk.graal.compiler.nodes.ValueNode; import jdk.graal.compiler.nodes.calc.ConditionalNode; import jdk.graal.compiler.nodes.calc.IntegerEqualsNode; import jdk.graal.compiler.nodes.calc.IntegerLowerThanNode; import jdk.graal.compiler.nodes.calc.IsNullNode; -import jdk.graal.compiler.nodes.extended.BytecodeExceptionNode; -import jdk.graal.compiler.nodes.extended.FieldOffsetProvider; import jdk.graal.compiler.nodes.extended.ValueAnchorNode; -import jdk.graal.compiler.nodes.java.ClassIsAssignableFromNode; import jdk.graal.compiler.nodes.java.InstanceOfNode; import jdk.graal.compiler.nodes.java.LoadFieldNode; -import jdk.graal.compiler.nodes.java.LoadIndexedNode; import jdk.graal.compiler.nodes.java.MethodCallTargetNode; import jdk.graal.compiler.nodes.spi.CoreProviders; -import jdk.graal.compiler.nodes.spi.LimitedValueProxy; import jdk.graal.compiler.nodes.spi.SimplifierTool; -import jdk.graal.compiler.nodes.util.GraphUtil; import jdk.graal.compiler.options.Option; import jdk.graal.compiler.options.OptionKey; import jdk.graal.compiler.phases.BasePhase; import jdk.graal.compiler.phases.common.CanonicalizerPhase; import jdk.graal.compiler.phases.common.CanonicalizerPhase.CustomSimplification; -import jdk.graal.compiler.phases.common.inlining.InliningUtil; import jdk.graal.compiler.printer.GraalDebugHandlersFactory; -import jdk.graal.compiler.replacements.nodes.MacroInvokable; -import jdk.vm.ci.meta.Constant; -import jdk.vm.ci.meta.JavaConstant; import jdk.vm.ci.meta.JavaKind; import jdk.vm.ci.meta.JavaMethodProfile; import jdk.vm.ci.meta.JavaTypeProfile; -import jdk.vm.ci.meta.PrimitiveConstant; import jdk.vm.ci.meta.ResolvedJavaMethod; import jdk.vm.ci.meta.ResolvedJavaType; import jdk.vm.ci.meta.TriState; @@ -186,7 +135,7 @@ public boolean equals(Object obj) { /* Cached option values to avoid repeated option lookup. */ private final int analysisSizeCutoff; - private final boolean strengthenGraphWithConstants; + protected final boolean strengthenGraphWithConstants; private final StrengthenGraphsCounters beforeCounters; private final StrengthenGraphsCounters afterCounters; @@ -236,10 +185,6 @@ private static void reportNeverNullInstanceFields(BigBang bb) { @SuppressWarnings("try") public final void applyResults(AnalysisMethod method) { - var nodeReferences = method instanceof PointsToAnalysisMethod ptaMethod && ptaMethod.getTypeFlow().flowsGraphCreated() - ? ptaMethod.getTypeFlow().getMethodFlowsGraph().getNodeFlows().getKeys() - : null; - var debug = new DebugContext.Builder(bb.getOptions(), new GraalDebugHandlersFactory(bb.getSnippetReflectionProvider())).build(); if (method.analyzedInPriorLayer()) { /* @@ -253,11 +198,15 @@ public final void applyResults(AnalysisMethod method) { return; } - var graph = method.decodeAnalyzedGraph(debug, nodeReferences); - if (graph == null) { + if (method.getAnalyzedGraph() == null) { + /* Method was not analyzed, so there is nothing to strengthen. */ return; } + var nodeReferences = method.getEncodedNodeReferences(); + var debug = new DebugContext.Builder(bb.getOptions(), new GraalDebugHandlersFactory(bb.getSnippetReflectionProvider())).build(); + var graph = method.decodeAnalyzedGraph(debug, nodeReferences); + preStrengthenGraphs(graph, method); graph.resetDebug(debug); @@ -275,6 +224,7 @@ public final void applyResults(AnalysisMethod method) { postStrengthenGraphs(graph, method); + /* Preserve the strengthened graph in an encoded format. */ method.setAnalyzedGraph(GraphEncoder.encodeSingleGraph(graph, AnalysisParsedGraph.HOST_ARCHITECTURE)); persistStrengthenGraph(method); @@ -328,7 +278,13 @@ public class AnalysisStrengthenGraphsPhase extends BasePhase { final CanonicalizerPhase phase; AnalysisStrengthenGraphsPhase(AnalysisMethod method, StructuredGraph graph) { - phase = CanonicalizerPhase.create().copyWithCustomSimplification(new StrengthenSimplifier(method, graph)); + ReachabilitySimplifier simplifier; + if (bb.isPointsToAnalysis()) { + simplifier = new TypeFlowSimplifier(StrengthenGraphs.this, method, graph); + } else { + simplifier = new ReachabilitySimplifier(StrengthenGraphs.this, method, graph); + } + phase = CanonicalizerPhase.create().copyWithCustomSimplification(simplifier); } @Override @@ -347,854 +303,12 @@ public CharSequence getName() { } } - class StrengthenSimplifier implements CustomSimplification { - - private final StructuredGraph graph; - private final MethodTypeFlow methodFlow; - - private final NodeBitMap createdPiNodes; - - private final TypeFlow[] parameterFlows; - private final NodeMap> nodeFlows; - - private final boolean allowConstantFolding; - private final boolean allowOptimizeReturnParameter; - private final EconomicSet unreachableValues = EconomicSet.create(); - - /** - * For runtime compiled methods, we must be careful to ensure new SubstrateTypes are not - * created due to the optimizations performed during the - * {@link AnalysisStrengthenGraphsPhase}. - */ - private final Function toTargetFunction; - - StrengthenSimplifier(AnalysisMethod method, StructuredGraph graph) { - this.graph = graph; - this.createdPiNodes = new NodeBitMap(graph); - - if (method instanceof PointsToAnalysisMethod ptaMethod && ptaMethod.getTypeFlow().flowsGraphCreated()) { - methodFlow = ptaMethod.getTypeFlow(); - MethodFlowsGraph originalFlows = methodFlow.getMethodFlowsGraph(); - parameterFlows = originalFlows.getParameters(); - nodeFlows = new NodeMap<>(graph); - var cursor = originalFlows.getNodeFlows().getEntries(); - while (cursor.advance()) { - Node node = cursor.getKey().getNode(); - assert nodeFlows.get(node) == null : "overwriting existing entry for " + node; - nodeFlows.put(node, cursor.getValue()); - } - - allowConstantFolding = strengthenGraphWithConstants && bb.getHostVM().allowConstantFolding(method); - - /* - * In deoptimization target methods optimizing the return parameter can make new - * values live across deoptimization entrypoints. - * - * In runtime-compiled methods invokes may be intrinsified during runtime partial - * evaluation and change the behavior of the invoke. This would be a problem if the - * behavior of the method completely changed; however, currently this - * intrinsification is used to improve the stamp of the returned value, but not to - * alter the semantics. Hence, it is preferred to continue to use the return value - * of the invoke (as opposed to the parameter value). - */ - allowOptimizeReturnParameter = method.isOriginalMethod() && ((PointsToAnalysis) bb).optimizeReturnedParameter(); - - } else { - methodFlow = null; - parameterFlows = null; - nodeFlows = null; - allowConstantFolding = false; - allowOptimizeReturnParameter = false; - } - - this.toTargetFunction = bb.getHostVM().getStrengthenGraphsToTargetFunction(method.getMultiMethodKey()); - } - - protected TypeFlow getNodeFlow(Node node) { - return nodeFlows == null || nodeFlows.isNew(node) ? null : nodeFlows.get(node); - } - - @Override - public void simplify(Node n, SimplifierTool tool) { - if (n instanceof ValueNode && !(n instanceof LimitedValueProxy) && !(n instanceof PhiNode) && !(n instanceof MacroInvokable)) { - /* - * The stamp of proxy nodes and phi nodes is inferred automatically, so we do not - * need to improve them. Macro nodes prohibit changing their stamp because it is - * derived from the macro's fallback invoke. - */ - ValueNode node = (ValueNode) n; - /* - * First ask the node to improve the stamp itself, to incorporate already improved - * input stamps. - */ - node.inferStamp(); - /* - * Since this new stamp is not based on a type flow, it is valid for the entire - * method and we can update the stamp of the node directly. We do not need an - * anchored PiNode. - */ - updateStampInPlace(node, strengthenStamp(node.stamp(NodeView.DEFAULT)), tool); - } - - if (simplifyDelegate(n, tool)) { - // handled elsewhere - } else if (n instanceof ParameterNode node && parameterFlows != null) { - StartNode anchorPoint = graph.start(); - Object newStampOrConstant = strengthenStampFromTypeFlow(node, parameterFlows[node.index()], anchorPoint, tool); - updateStampUsingPiNode(node, newStampOrConstant, anchorPoint, tool); - - } else if (n instanceof LoadFieldNode node && node.field() instanceof PointsToAnalysisField field) { - /* - * First step: it is beneficial to strengthen the stamp of the LoadFieldNode because - * then there is no artificial anchor after which the more precise type is - * available. However, the memory load will be a floating node later, so we can only - * update the stamp directly to the stamp that is correct for the whole method and - * all inlined methods. - */ - Object fieldNewStampOrConstant = strengthenStampFromTypeFlow(node, field.getSinkFlow(), node, tool); - if (fieldNewStampOrConstant instanceof JavaConstant) { - ConstantNode replacement = ConstantNode.forConstant((JavaConstant) fieldNewStampOrConstant, bb.getMetaAccess(), graph); - graph.replaceFixedWithFloating(node, replacement); - tool.addToWorkList(replacement); - } else { - updateStampInPlace(node, (Stamp) fieldNewStampOrConstant, tool); - - /* - * Second step: strengthen using context-sensitive analysis results, which - * requires an anchored PiNode. - */ - Object nodeNewStampOrConstant = strengthenStampFromTypeFlow(node, getNodeFlow(node), node, tool); - updateStampUsingPiNode(node, nodeNewStampOrConstant, node, tool); - } - - } else if (n instanceof LoadIndexedNode node) { - Object newStampOrConstant = strengthenStampFromTypeFlow(node, getNodeFlow(node), node, tool); - updateStampUsingPiNode(node, newStampOrConstant, node, tool); - - } else if (n instanceof Invoke) { - Invoke invoke = (Invoke) n; - if (invoke.callTarget() instanceof MethodCallTargetNode) { - handleInvoke(invoke, tool); - } - - } else if (n instanceof IfNode) { - IfNode node = (IfNode) n; - boolean trueUnreachable = isUnreachable(node.trueSuccessor()); - boolean falseUnreachable = isUnreachable(node.falseSuccessor()); - - if (trueUnreachable && falseUnreachable) { - makeUnreachable(node, tool, () -> "method " + getQualifiedName(graph) + ", node " + node + ": both successors of IfNode are unreachable"); - - } else if (trueUnreachable || falseUnreachable) { - AbstractBeginNode killedBegin = node.successor(trueUnreachable); - AbstractBeginNode survivingBegin = node.successor(!trueUnreachable); - - if (survivingBegin.hasUsages()) { - /* - * Even when we know that the IfNode is not necessary because the condition - * is statically proven, all PiNode that are anchored at the surviving - * branch must remain anchored at exactly this point. It would be wrong to - * anchor the PiNode at the BeginNode of the preceding block, because at - * that point the condition is not proven yet. - */ - ValueAnchorNode anchor = graph.add(new ValueAnchorNode()); - graph.addAfterFixed(survivingBegin, anchor); - survivingBegin.replaceAtUsages(anchor, InputType.Guard, InputType.Anchor); - } - graph.removeSplit(node, survivingBegin); - GraphUtil.killCFG(killedBegin); - } - - } else if (n instanceof FixedGuardNode) { - FixedGuardNode node = (FixedGuardNode) n; - if (isUnreachable(node)) { - node.setCondition(LogicConstantNode.tautology(graph), true); - tool.addToWorkList(node); - } - - } else if (n instanceof InstanceOfNode) { - InstanceOfNode node = (InstanceOfNode) n; - ObjectStamp oldStamp = node.getCheckedStamp(); - Stamp newStamp = strengthenStamp(oldStamp); - if (newStamp != null) { - LogicNode replacement = graph.addOrUniqueWithInputs(InstanceOfNode.createHelper((ObjectStamp) oldStamp.improveWith(newStamp), node.getValue(), node.profile(), node.getAnchor())); - /* - * GR-59681: Once isAssignable is implemented for BaseLayerType, this check can - * be removed - */ - AnalysisError.guarantee(node != replacement, "The new stamp needs to be different from the old stamp"); - node.replaceAndDelete(replacement); - tool.addToWorkList(replacement); - } else { - maybeAssignInstanceOfProfiles(node); - } - - } else if (n instanceof ClassIsAssignableFromNode node) { - if (isClosedTypeWorld) { - /* - * If the constant receiver of a Class#isAssignableFrom is an unreachable type - * we can constant-fold the ClassIsAssignableFromNode to false. See also - * MethodTypeFlowBuilder#ignoreConstant where we avoid marking the corresponding - * type as reachable just because it is used by the ClassIsAssignableFromNode. - * We only apply this optimization if it's a closed type world, for open world - * we cannot fold the type check since the type may be used later. - */ - AnalysisType nonReachableType = asConstantNonReachableType(node.getThisClass(), tool); - if (nonReachableType != null) { - node.replaceAndDelete(LogicConstantNode.contradiction(graph)); - } - } - } else if (n instanceof BytecodeExceptionNode node) { - /* - * We do not want a type to be reachable only to be used for the error message of a - * ClassCastException. Therefore, in that case we replace the java.lang.Class with a - * java.lang.String that is then used directly in the error message. We can apply - * this optimization optimistically for both closed and open type world. - */ - if (node.getExceptionKind() == BytecodeExceptionNode.BytecodeExceptionKind.CLASS_CAST) { - AnalysisType nonReachableType = asConstantNonReachableType(node.getArguments().get(1), tool); - if (nonReachableType != null) { - node.getArguments().set(1, ConstantNode.forConstant(tool.getConstantReflection().forString(getTypeName(nonReachableType)), tool.getMetaAccess(), graph)); - } - } - - } else if (n instanceof FrameState) { - /* - * We do not want a constant to be reachable only to be used for debugging purposes - * in a FrameState. - */ - FrameState node = (FrameState) n; - for (int i = 0; i < node.values().size(); i++) { - if (node.values().get(i) instanceof ConstantNode constantNode && constantNode.getValue() instanceof ImageHeapConstant imageHeapConstant && !imageHeapConstant.isReachable()) { - node.values().set(i, ConstantNode.defaultForKind(JavaKind.Object, graph)); - } - if (node.values().get(i) instanceof FieldOffsetProvider fieldOffsetProvider && !((AnalysisField) fieldOffsetProvider.getField()).isUnsafeAccessed()) { - /* - * We use a unique marker constant as the replacement value, so that a - * search in the code base for the value leads us to here. - */ - node.values().set(i, ConstantNode.forIntegerKind(fieldOffsetProvider.asNode().getStackKind(), 0xDEA51106, graph)); - } - } - - } else if (n instanceof PiNode) { - PiNode node = (PiNode) n; - Stamp oldStamp = node.piStamp(); - Stamp newStamp = strengthenStamp(oldStamp); - if (newStamp != null) { - Stamp newPiStamp = oldStamp.improveWith(newStamp); - /* - * GR-59681: Once isAssignable is implemented for BaseLayerType, this check can - * be removed - */ - AnalysisError.guarantee(!newPiStamp.equals(oldStamp), "The new stamp needs to be different from the old stamp"); - node.strengthenPiStamp(newPiStamp); - tool.addToWorkList(node); - } - } - } - - private AnalysisType asConstantNonReachableType(ValueNode value, CoreProviders providers) { - if (value != null && value.isConstant()) { - AnalysisType expectedType = (AnalysisType) providers.getConstantReflection().asJavaType(value.asConstant()); - if (expectedType != null && !expectedType.isReachable()) { - return expectedType; - } - } - return null; - } - - private void handleInvoke(Invoke invoke, SimplifierTool tool) { - - FixedNode node = invoke.asFixedNode(); - MethodCallTargetNode callTarget = (MethodCallTargetNode) invoke.callTarget(); - - AnalysisMethod targetMethod = (AnalysisMethod) callTarget.targetMethod(); - if (callTarget.invokeKind().isDirect() && !targetMethod.isSimplyImplementationInvoked()) { - /* - * This is a direct call to a method that the static analysis did not see as - * invoked. This can happen when the receiver is always null. In most cases, the - * method profile also has a length of 0 and the below code to kill the invoke would - * trigger. But when only running the reachability analysis, there is no detailed - * list of callees. - */ - unreachableInvoke(invoke, tool, () -> "method " + getQualifiedName(graph) + ", node " + invoke + - ": target method is not marked as simply implementation invoked"); - /* Invoke is unreachable, there is no point in improving any types further. */ - return; - } - - InvokeTypeFlow invokeFlow = (InvokeTypeFlow) getNodeFlow(node); - if (invokeFlow == null) { - /* No points-to analysis results. */ - return; - } - if (!invokeFlow.isFlowEnabled()) { - unreachableInvoke(invoke, tool, () -> "method " + getQualifiedName(graph) + ", node " + invoke + - ": flow is not enabled by its predicate " + invokeFlow.getPredicate()); - /* Invoke is unreachable, there is no point in improving any types further. */ - return; - } - - Collection callees = invokeFlow.getOriginalCallees(); - if (callees.isEmpty()) { - if (isClosedTypeWorld) { - /* Invoke is unreachable, there is no point in improving any types further. */ - unreachableInvoke(invoke, tool, () -> "method " + getQualifiedName(graph) + ", node " + invoke + - ": empty list of callees for call to " + ((AnalysisMethod) invoke.callTarget().targetMethod()).getQualifiedName()); - } - /* In open world we cannot make any assumptions about an invoke with 0 callees. */ - return; - } - assert invokeFlow.isFlowEnabled() : "Disabled invoke should have no callees: " + invokeFlow + ", in method " + getQualifiedName(graph); - - FixedWithNextNode beforeInvoke = (FixedWithNextNode) invoke.predecessor(); - NodeInputList arguments = callTarget.arguments(); - for (int i = 0; i < arguments.size(); i++) { - ValueNode argument = arguments.get(i); - Object newStampOrConstant = strengthenStampFromTypeFlow(argument, invokeFlow.getActualParameters()[i], beforeInvoke, tool); - if (node.isDeleted()) { - /* Parameter stamp was empty, so invoke is unreachable. */ - return; - } - if (i == 0 && invoke.getInvokeKind() != CallTargetNode.InvokeKind.Static) { - /* - * Check for null receiver. If so, the invoke is unreachable. - * - * Note it is not necessary to check for an empty stamp, as in that case - * strengthenStampFromTypeFlow will make the invoke unreachable. - */ - boolean nullReceiver = false; - if (argument instanceof ConstantNode constantNode) { - nullReceiver = constantNode.getValue().isDefaultForKind(); - } - if (!nullReceiver && newStampOrConstant instanceof ObjectStamp stamp) { - nullReceiver = stamp.alwaysNull(); - } - if (!nullReceiver && newStampOrConstant instanceof Constant constantValue) { - nullReceiver = constantValue.isDefaultForKind(); - } - if (nullReceiver) { - invokeWithNullReceiver(invoke); - return; - } - } - if (newStampOrConstant != null) { - ValueNode pi = insertPi(argument, newStampOrConstant, beforeInvoke); - if (pi != null && pi != argument) { - callTarget.replaceAllInputs(argument, pi); - } - } - } - - if (callTarget.invokeKind().isDirect()) { - /* - * Note: A direct invoke doesn't necessarily imply that the analysis should have - * discovered a single callee. When dealing with interfaces it is in fact possible - * that the Graal stamps are more accurate than the analysis results. So an - * interface call may have already been optimized to a special call by stamp - * strengthening of the receiver object, hence the invoke kind is direct, whereas - * the points-to analysis inaccurately concluded there can be more than one callee. - * - * Below we just check that if there is a direct invoke *and* the analysis - * discovered a single callee, then the callee should match the target method. - */ - if (callees.size() == 1) { - AnalysisMethod singleCallee = callees.iterator().next(); - assert targetMethod.equals(singleCallee) : "Direct invoke target mismatch: " + targetMethod + " != " + singleCallee + ". Called from " + graph.method().format("%H.%n"); - } - } else if (AnnotationAccess.isAnnotationPresent(targetMethod, Delete.class)) { - /* We de-virtualize invokes to deleted methods since the callee must be unique. */ - AnalysisError.guarantee(callees.size() == 1, "@Delete methods should have a single callee."); - AnalysisMethod singleCallee = callees.iterator().next(); - devirtualizeInvoke(singleCallee, invoke); - } else if (targetMethod.canBeStaticallyBound() || isClosedTypeWorld) { - /* - * We only de-virtualize invokes if we run a closed type world analysis or the - * target method can be trivially statically bound. - */ - if (callees.size() == 1) { - AnalysisMethod singleCallee = callees.iterator().next(); - devirtualizeInvoke(singleCallee, invoke); - } else { - TypeState receiverTypeState = null; - /* If the receiver flow is saturated, its exact type state does not matter. */ - if (invokeFlow.getTargetMethod().hasReceiver() && !methodFlow.isSaturated((PointsToAnalysis) bb, invokeFlow.getReceiver())) { - receiverTypeState = methodFlow.foldTypeFlow((PointsToAnalysis) bb, invokeFlow.getReceiver()); - } - assignInvokeProfiles(invoke, invokeFlow, callees, receiverTypeState, false); - } - } else { - /* Last resort, try to inject profiles optimistically. */ - TypeState receiverTypeState = null; - if (invokeFlow.getTargetMethod().hasReceiver()) { - if (methodFlow.isSaturated((PointsToAnalysis) bb, invokeFlow)) { - /* - * For saturated invokes use all seen instantiated subtypes of target method - * declaring class. In an open world this is incomplete as new types may be - * seen later, but it is an optimistic approximation. - */ - receiverTypeState = targetMethod.getDeclaringClass().getTypeFlow(bb, false).getState(); - } else { - receiverTypeState = methodFlow.foldTypeFlow((PointsToAnalysis) bb, invokeFlow.getReceiver()); - } - } - if (receiverTypeState != null && receiverTypeState.typesCount() <= MAX_TYPES_OPTIMISTIC_PROFILES) { - assignInvokeProfiles(invoke, invokeFlow, callees, receiverTypeState, true); - } - } - - if (allowOptimizeReturnParameter && (isClosedTypeWorld || callTarget.invokeKind().isDirect() || targetMethod.canBeStaticallyBound())) { - /* Can only optimize returned parameter when all possible callees are visible. */ - optimizeReturnedParameter(callees, arguments, node, tool); - } - - FixedWithNextNode anchorPointAfterInvoke = (FixedWithNextNode) (invoke instanceof InvokeWithExceptionNode ? invoke.next() : invoke); - TypeFlow nodeFlow = invokeFlow.getResult(); - if (nodeFlow != null && node.getStackKind() == JavaKind.Void && !methodFlow.isSaturated((PointsToAnalysis) bb, nodeFlow)) { - /* - * We track the reachability of return statements in void methods via returning - * either Empty or AnyPrimitive TypeState, therefore we perform an emptiness check. - */ - var typeState = methodFlow.foldTypeFlow((PointsToAnalysis) bb, nodeFlow); - if (typeState.isEmpty() && unreachableValues.add(node)) { - makeUnreachable(anchorPointAfterInvoke.next(), tool, - () -> "method " + getQualifiedName(graph) + ", node " + node + ": return from void method was proven unreachable"); - } - } - Object newStampOrConstant = strengthenStampFromTypeFlow(node, nodeFlow, anchorPointAfterInvoke, tool); - updateStampUsingPiNode(node, newStampOrConstant, anchorPointAfterInvoke, tool); - } - - /** - * Maximum number of types seen in a {@link TypeState} for a virtual {@link Invoke} to - * consider optimistic profile injection. See {@link #handleInvoke(Invoke, SimplifierTool)} - * for more details. Note that this is a footprint consideration - we do not want to carry - * around gargantuan {@link JavaTypeProfile} in {@link MethodCallTargetNode} that cannot be - * used anyway. - */ - private static final int MAX_TYPES_OPTIMISTIC_PROFILES = 100; - - private void assignInvokeProfiles(Invoke invoke, InvokeTypeFlow invokeFlow, Collection callees, TypeState receiverTypeState, boolean assumeNotRecorded) { - /* - * In an open type world we cannot trust the type state of the receiver for virtual - * calls as new subtypes could be added later. - * - * Note: assumeNotRecorded specifies if profiles are injected for a closed or open - * world. For a closed world with precise analysis results we never have a - * notRecordedProbabiltiy in any profile. For the open world we always assume that there - * is a not recorded probability in the profile. Such a not recorded probability will be - * injected if assumeNotRecorded==true. - */ - JavaTypeProfile typeProfile = makeTypeProfile(receiverTypeState, assumeNotRecorded); - /* - * In a closed type world analysis the method profile of an invoke is complete and - * contains all the callees reachable at that invocation location. Even if that invoke - * is saturated it is still correct as it contains all the reachable implementations of - * the target method. However, in an open type world the method profile of an invoke, - * saturated or not, is incomplete, as there can be implementations that we haven't yet - * seen. - */ - JavaMethodProfile methodProfile = makeMethodProfile(callees, assumeNotRecorded); - - assert typeProfile == null || typeProfile.getTypes().length > 1 || assumeNotRecorded : "Should devirtualize with typeProfile=" + typeProfile + " and methodProfile=" + methodProfile + - " and callees" + callees + " invoke " + invokeFlow + " " + invokeFlow.getReceiver() + " in method " + getQualifiedName(graph); - assert methodProfile == null || methodProfile.getMethods().length > 1 || assumeNotRecorded : "Should devirtualize with typeProfile=" + typeProfile + " and methodProfile=" + methodProfile + - " and callees" + callees + " invoke " + invokeFlow + " " + invokeFlow.getReceiver() + " in method " + getQualifiedName(graph); - - setInvokeProfiles(invoke, typeProfile, methodProfile); - } - - /** - * If all possible callees return the same parameter, then we can replace the invoke with - * that parameter at all usages. This is the same that would happen when the callees are - * inlined. So we get a bit of the benefits of method inlining without actually performing - * the inlining. - */ - private void optimizeReturnedParameter(Collection callees, NodeInputList arguments, FixedNode invoke, SimplifierTool tool) { - int returnedParameterIndex = -1; - for (AnalysisMethod callee : callees) { - if (callee.hasNeverInlineDirective()) { - /* - * If the method is explicitly marked as "never inline", it might be an - * intentional sink to prevent an optimization. Mostly, this is a pattern we use - * in unit tests. So this reduces the surprise that tests are - * "too well optimized" without doing any harm for real-world methods. - */ - return; - } - int returnedCalleeParameterIndex = PointsToAnalysis.assertPointsToAnalysisMethod(callee).getTypeFlow().getReturnedParameterIndex(); - if (returnedCalleeParameterIndex == -1) { - /* This callee does not return a parameter. */ - return; - } - if (returnedParameterIndex == -1) { - returnedParameterIndex = returnedCalleeParameterIndex; - } else if (returnedParameterIndex != returnedCalleeParameterIndex) { - /* This callee returns a different parameter than a previous callee. */ - return; - } - } - assert returnedParameterIndex != -1 : callees; - - ValueNode returnedActualParameter = arguments.get(returnedParameterIndex); - tool.addToWorkList(invoke.usages()); - invoke.replaceAtUsages(returnedActualParameter); - } - - /** - * The invoke always has a null receiver, so it can be removed. - */ - protected void invokeWithNullReceiver(Invoke invoke) { - FixedNode replacement = createInvokeWithNullReceiverReplacement(graph); - ((FixedWithNextNode) invoke.predecessor()).setNext(replacement); - GraphUtil.killCFG(invoke.asFixedNode()); - } - - /** - * The invoke has no callee, i.e., it is unreachable. - */ - private void unreachableInvoke(Invoke invoke, SimplifierTool tool, Supplier messageSupplier) { - if (invoke.getInvokeKind() != CallTargetNode.InvokeKind.Static) { - /* - * Ensure that a null check for the receiver remains in the graph. There should be - * already an explicit null check in the graph, but we are paranoid and check again. - */ - InliningUtil.nonNullReceiver(invoke); - } - - makeUnreachable(invoke.asFixedNode(), tool, messageSupplier); - } - - /** - * The invoke has only one callee, i.e., the call can be devirtualized to this callee. This - * allows later inlining of the callee. - */ - private void devirtualizeInvoke(AnalysisMethod singleCallee, Invoke invoke) { - if (ImageBuildStatistics.Options.CollectImageBuildStatistics.getValue(graph.getOptions())) { - ImageBuildStatistics.counters().incDevirtualizedInvokeCounter(); - } - - Stamp anchoredReceiverStamp = StampFactory.object(TypeReference.createWithoutAssumptions(singleCallee.getDeclaringClass())); - ValueNode piReceiver = insertPi(invoke.getReceiver(), anchoredReceiverStamp, (FixedWithNextNode) invoke.asNode().predecessor()); - if (piReceiver != null) { - invoke.callTarget().replaceFirstInput(invoke.getReceiver(), piReceiver); - } - - assert invoke.getInvokeKind().isIndirect() : invoke; - invoke.callTarget().setInvokeKind(CallTargetNode.InvokeKind.Special); - invoke.callTarget().setTargetMethod(singleCallee); - } - - private boolean isUnreachable(Node branch) { - TypeFlow branchFlow = getNodeFlow(branch); - if (branchFlow != null && !methodFlow.isSaturated(((PointsToAnalysis) bb), branchFlow)) { - if (!branchFlow.isFlowEnabled()) { - return true; - } - TypeState typeState = methodFlow.foldTypeFlow((PointsToAnalysis) bb, branchFlow); - if (branchFlow.isPrimitiveFlow()) { - /* - * This assert is a safeguard to verify the assumption that only one type of - * flow has to be considered as a branch predicate at the moment. - */ - assert branchFlow instanceof PrimitiveFilterTypeFlow : "Unexpected type of primitive flow encountered as branch predicate: " + branchFlow; - } - return typeState.isEmpty(); - } - return false; - } - - private void updateStampInPlace(ValueNode node, Stamp newStamp, SimplifierTool tool) { - if (newStamp != null) { - Stamp oldStamp = node.stamp(NodeView.DEFAULT); - Stamp computedStamp = oldStamp.improveWith(newStamp); - if (!oldStamp.equals(computedStamp)) { - node.setStamp(newStamp); - tool.addToWorkList(node.usages()); - } - } - } - - private void updateStampUsingPiNode(ValueNode node, Object newStampOrConstant, FixedWithNextNode anchorPoint, SimplifierTool tool) { - if (newStampOrConstant != null && node.hasUsages() && !createdPiNodes.isMarked(node)) { - ValueNode pi = insertPi(node, newStampOrConstant, anchorPoint); - if (pi != null) { - /* - * The Canonicalizer that drives all of our node processing is iterative. We - * only want to insert the PiNode the first time we handle a node. - */ - createdPiNodes.mark(node); - - if (pi.isConstant()) { - node.replaceAtUsages(pi); - } else { - FrameState anchorState = node instanceof StateSplit ? ((StateSplit) node).stateAfter() : graph.start().stateAfter(); - node.replaceAtUsages(pi, usage -> usage != pi && usage != anchorState); - } - tool.addToWorkList(pi.usages()); - } - } - } - - /* - * See comment on {@link StrengthenGraphs} on why anchoring is necessary. - */ - private ValueNode insertPi(ValueNode input, Object newStampOrConstant, FixedWithNextNode anchorPoint) { - if (newStampOrConstant instanceof JavaConstant) { - JavaConstant constant = (JavaConstant) newStampOrConstant; - if (input.isConstant()) { - assert bb.getConstantReflectionProvider().constantEquals(input.asConstant(), constant) : input.asConstant() + ", " + constant; - return null; - } - return ConstantNode.forConstant(constant, bb.getMetaAccess(), graph); - } - - Stamp piStamp = (Stamp) newStampOrConstant; - Stamp oldStamp = input.stamp(NodeView.DEFAULT); - Stamp computedStamp = oldStamp.improveWith(piStamp); - if (oldStamp.equals(computedStamp)) { - /* The PiNode does not give any additional information. */ - return null; - } - - ValueAnchorNode anchor = graph.add(new ValueAnchorNode()); - graph.addAfterFixed(anchorPoint, anchor); - return graph.unique(new PiNode(input, piStamp, anchor)); - } - - private Object strengthenStampFromTypeFlow(ValueNode node, TypeFlow nodeFlow, FixedWithNextNode anchorPoint, SimplifierTool tool) { - if (nodeFlow == null || !((PointsToAnalysis) bb).isSupportedJavaKind(node.getStackKind())) { - return null; - } - if (methodFlow.isSaturated((PointsToAnalysis) bb, nodeFlow)) { - /* The type flow is saturated, its type state does not matter. */ - return null; - } - if (unreachableValues.contains(node)) { - /* This node has already been made unreachable - no further action is needed. */ - return null; - } - /* - * If there are no usages of the node, then adding a PiNode would only bloat the graph. - * However, we don't immediately return null since the stamp can still indicate this - * node is unreachable. - */ - boolean hasUsages = node.usages().filter(n -> !(n instanceof FrameState)).isNotEmpty(); - - if (!nodeFlow.isFlowEnabled()) { - makeUnreachable(anchorPoint.next(), tool, - () -> "method " + getQualifiedName(graph) + ", node " + node + ": flow is not enabled by its predicate " + nodeFlow.getPredicate()); - unreachableValues.add(node); - return null; - } - TypeState nodeTypeState = methodFlow.foldTypeFlow((PointsToAnalysis) bb, nodeFlow); - - if (hasUsages && allowConstantFolding && !nodeTypeState.canBeNull()) { - JavaConstant constantValue = nodeTypeState.asConstant(); - if (constantValue != null) { - return constantValue; - } - } - - node.inferStamp(); - Stamp s = node.stamp(NodeView.DEFAULT); - if (s.isIntegerStamp() || nodeTypeState.isPrimitive()) { - return getIntegerStamp(node, ((IntegerStamp) s), anchorPoint, nodeTypeState, tool); - } - - ObjectStamp oldStamp = (ObjectStamp) s; - AnalysisType oldType = (AnalysisType) oldStamp.type(); - boolean nonNull = oldStamp.nonNull() || !nodeTypeState.canBeNull(); - - /* - * Find all types of the TypeState that are compatible with the current stamp. Since - * stamps are propagated around immediately by the Canonicalizer it is possible and - * allowed that the stamp is already more precise than the static analysis results. - */ - List typeStateTypes = new ArrayList<>(nodeTypeState.typesCount()); - for (AnalysisType typeStateType : nodeTypeState.types(bb)) { - if (oldType == null || (oldStamp.isExactType() ? oldType.equals(typeStateType) : oldType.isJavaLangObject() || oldType.isAssignableFrom(typeStateType))) { - typeStateTypes.add(typeStateType); - } - } - - if (typeStateTypes.size() == 0) { - if (nonNull) { - makeUnreachable(anchorPoint.next(), tool, - () -> "method " + getQualifiedName(graph) + ", node " + node + ": empty object type state when strengthening oldStamp " + oldStamp); - unreachableValues.add(node); - return null; - } else { - return hasUsages ? StampFactory.alwaysNull() : null; - } - - } else if (!hasUsages) { - // no need to return strengthened stamp if it is unused - return null; - } else if (typeStateTypes.size() == 1) { - AnalysisType exactType = typeStateTypes.get(0); - assert getSingleImplementorType(exactType) == null || exactType.equals(getSingleImplementorType(exactType)) : "exactType=" + exactType + ", singleImplementor=" + - getSingleImplementorType(exactType); - assert exactType.equals(getStrengthenStampType(exactType)) : exactType; - - if (!oldStamp.isExactType() || !exactType.equals(oldType)) { - ResolvedJavaType targetType = toTargetFunction.apply(exactType); - if (targetType != null) { - TypeReference typeRef = TypeReference.createExactTrusted(targetType); - return StampFactory.object(typeRef, nonNull); - } - } - - } else if (!oldStamp.isExactType()) { - assert typeStateTypes.size() > 1 : typeStateTypes; - AnalysisType baseType = typeStateTypes.get(0); - for (int i = 1; i < typeStateTypes.size(); i++) { - if (baseType.isJavaLangObject()) { - break; - } - baseType = baseType.findLeastCommonAncestor(typeStateTypes.get(i)); - } - - if (oldType != null && !oldType.isAssignableFrom(baseType)) { - /* - * When the original stamp is an interface type, we do not want to weaken that - * type with the common base class of all implementation types (which could even - * be java.lang.Object). - */ - baseType = oldType; - } - - /* - * With more than one type in the type state, there cannot be a single implementor. - * Because that single implementor would need to be the only type in the type state. - */ - assert getSingleImplementorType(baseType) == null || baseType.equals(getSingleImplementorType(baseType)) : "baseType=" + baseType + ", singleImplementor=" + - getSingleImplementorType(baseType); - - AnalysisType newType = getStrengthenStampType(baseType); - - assert typeStateTypes.stream().map(typeStateType -> newType.isAssignableFrom(typeStateType)).reduce(Boolean::logicalAnd).get() : typeStateTypes; - - if (!newType.equals(oldType) && (oldType != null || !newType.isJavaLangObject())) { - ResolvedJavaType targetType = toTargetFunction.apply(newType); - if (targetType != null) { - TypeReference typeRef = TypeReference.createTrustedWithoutAssumptions(targetType); - return StampFactory.object(typeRef, nonNull); - } - } - } - - if (nonNull != oldStamp.nonNull()) { - assert nonNull : oldStamp; - return oldStamp.asNonNull(); - } - /* Nothing to strengthen. */ - return null; - } - - private IntegerStamp getIntegerStamp(ValueNode node, IntegerStamp originalStamp, FixedWithNextNode anchorPoint, TypeState nodeTypeState, SimplifierTool tool) { - assert bb.trackPrimitiveValues() : nodeTypeState + "," + node + " in " + node.graph(); - assert nodeTypeState != null && (nodeTypeState.isEmpty() || nodeTypeState.isPrimitive()) : nodeTypeState + "," + node + " in " + node.graph(); - if (nodeTypeState.isEmpty()) { - makeUnreachable(anchorPoint.next(), tool, - () -> "method " + getQualifiedName(graph) + ", node " + node + ": empty primitive type state when strengthening oldStamp " + originalStamp); - unreachableValues.add(node); - return null; - } - if (nodeTypeState instanceof PrimitiveConstantTypeState constantTypeState) { - long constantValue = constantTypeState.getValue(); - if (node instanceof ConstantNode constant) { - /* - * Sanity check, verify that what was proven by the analysis is consistent with - * the constant node in the graph. - */ - Constant value = constant.getValue(); - assert value instanceof PrimitiveConstant : "Node " + value + " should be a primitive constant when extracting an integer stamp, method " + node.graph().method(); - assert ((PrimitiveConstant) value).asLong() == constantValue : "The actual value of node: " + value + " is different than the value " + constantValue + - " computed by points-to analysis, method in " + node.graph().method(); - } else { - return IntegerStamp.createConstant(originalStamp.getBits(), constantValue); - } - } - return null; - } - - private void makeUnreachable(FixedNode node, CoreProviders providers, Supplier message) { - FixedNode unreachableNode = createUnreachable(graph, providers, message); - ((FixedWithNextNode) node.predecessor()).setNext(unreachableNode); - GraphUtil.killCFG(node); - } - - private Stamp strengthenStamp(Stamp s) { - if (!(s instanceof AbstractObjectStamp)) { - return null; - } - AbstractObjectStamp stamp = (AbstractObjectStamp) s; - AnalysisType originalType = (AnalysisType) stamp.type(); - if (originalType == null) { - return null; - } - - /* In open world the type may become reachable later. */ - if (isClosedTypeWorld && !originalType.isReachable()) { - /* We must be in dead code. */ - if (stamp.nonNull()) { - /* We must be in dead code. */ - return StampFactory.empty(JavaKind.Object); - } else { - return StampFactory.alwaysNull(); - } - } - - AnalysisType singleImplementorType = getSingleImplementorType(originalType); - if (singleImplementorType != null && (!stamp.isExactType() || !singleImplementorType.equals(originalType))) { - ResolvedJavaType targetType = toTargetFunction.apply(singleImplementorType); - if (targetType != null) { - TypeReference typeRef = TypeReference.createExactTrusted(targetType); - return StampFactory.object(typeRef, stamp.nonNull()); - } - } - - AnalysisType strengthenType = getStrengthenStampType(originalType); - if (originalType.equals(strengthenType)) { - /* Nothing to strengthen. */ - return null; - } - - Stamp newStamp; - if (strengthenType == null) { - /* The type and its subtypes are not instantiated. */ - if (stamp.nonNull()) { - /* We must be in dead code. */ - newStamp = StampFactory.empty(JavaKind.Object); - } else { - newStamp = StampFactory.alwaysNull(); - } - - } else { - if (stamp.isExactType()) { - /* We must be in dead code. */ - newStamp = StampFactory.empty(JavaKind.Object); - } else { - ResolvedJavaType targetType = toTargetFunction.apply(strengthenType); - if (targetType == null) { - return null; - } - TypeReference typeRef = TypeReference.createTrustedWithoutAssumptions(targetType); - newStamp = StampFactory.object(typeRef, stamp.nonNull()); - } - } - return newStamp; - } - } - @SuppressWarnings("unused") protected void maybeAssignInstanceOfProfiles(InstanceOfNode iof) { // placeholder } - private static String getQualifiedName(StructuredGraph graph) { + static String getQualifiedName(StructuredGraph graph) { return ((AnalysisMethod) graph.method()).getQualifiedName(); } @@ -1243,7 +357,6 @@ private CachedJavaMethodProfile createMethodProfile(Collection c } return new CachedJavaMethodProfile(new JavaMethodProfile(injectNotRecordedProbability ? probability : 0, pitems), hashCode); } - } /** diff --git a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/results/TypeFlowSimplifier.java b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/results/TypeFlowSimplifier.java new file mode 100644 index 000000000000..2cd1a2e56089 --- /dev/null +++ b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/results/TypeFlowSimplifier.java @@ -0,0 +1,722 @@ +/* + * 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.graal.pointsto.results; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.graalvm.collections.EconomicSet; +import org.graalvm.nativeimage.AnnotationAccess; + +import com.oracle.graal.pointsto.PointsToAnalysis; +import com.oracle.graal.pointsto.flow.InvokeTypeFlow; +import com.oracle.graal.pointsto.flow.MethodFlowsGraph; +import com.oracle.graal.pointsto.flow.MethodTypeFlow; +import com.oracle.graal.pointsto.flow.PrimitiveFilterTypeFlow; +import com.oracle.graal.pointsto.flow.TypeFlow; +import com.oracle.graal.pointsto.meta.AnalysisMethod; +import com.oracle.graal.pointsto.meta.AnalysisType; +import com.oracle.graal.pointsto.meta.PointsToAnalysisField; +import com.oracle.graal.pointsto.meta.PointsToAnalysisMethod; +import com.oracle.graal.pointsto.typestate.PrimitiveConstantTypeState; +import com.oracle.graal.pointsto.typestate.TypeState; +import com.oracle.graal.pointsto.util.AnalysisError; +import com.oracle.svm.core.annotate.Delete; +import com.oracle.svm.util.ImageBuildStatistics; + +import jdk.graal.compiler.core.common.type.IntegerStamp; +import jdk.graal.compiler.core.common.type.ObjectStamp; +import jdk.graal.compiler.core.common.type.Stamp; +import jdk.graal.compiler.core.common.type.StampFactory; +import jdk.graal.compiler.core.common.type.TypeReference; +import jdk.graal.compiler.graph.Node; +import jdk.graal.compiler.graph.NodeBitMap; +import jdk.graal.compiler.graph.NodeInputList; +import jdk.graal.compiler.graph.NodeMap; +import jdk.graal.compiler.nodeinfo.InputType; +import jdk.graal.compiler.nodes.AbstractBeginNode; +import jdk.graal.compiler.nodes.CallTargetNode; +import jdk.graal.compiler.nodes.ConstantNode; +import jdk.graal.compiler.nodes.FixedGuardNode; +import jdk.graal.compiler.nodes.FixedNode; +import jdk.graal.compiler.nodes.FixedWithNextNode; +import jdk.graal.compiler.nodes.FrameState; +import jdk.graal.compiler.nodes.IfNode; +import jdk.graal.compiler.nodes.Invoke; +import jdk.graal.compiler.nodes.InvokeWithExceptionNode; +import jdk.graal.compiler.nodes.LogicConstantNode; +import jdk.graal.compiler.nodes.NodeView; +import jdk.graal.compiler.nodes.ParameterNode; +import jdk.graal.compiler.nodes.PiNode; +import jdk.graal.compiler.nodes.StartNode; +import jdk.graal.compiler.nodes.StateSplit; +import jdk.graal.compiler.nodes.StructuredGraph; +import jdk.graal.compiler.nodes.ValueNode; +import jdk.graal.compiler.nodes.extended.BytecodeExceptionNode; +import jdk.graal.compiler.nodes.extended.ValueAnchorNode; +import jdk.graal.compiler.nodes.java.ClassIsAssignableFromNode; +import jdk.graal.compiler.nodes.java.InstanceOfNode; +import jdk.graal.compiler.nodes.java.LoadFieldNode; +import jdk.graal.compiler.nodes.java.LoadIndexedNode; +import jdk.graal.compiler.nodes.java.MethodCallTargetNode; +import jdk.graal.compiler.nodes.spi.SimplifierTool; +import jdk.graal.compiler.nodes.util.GraphUtil; +import jdk.vm.ci.meta.Constant; +import jdk.vm.ci.meta.JavaConstant; +import jdk.vm.ci.meta.JavaKind; +import jdk.vm.ci.meta.JavaMethodProfile; +import jdk.vm.ci.meta.JavaTypeProfile; +import jdk.vm.ci.meta.PrimitiveConstant; +import jdk.vm.ci.meta.ResolvedJavaType; + +/** + * Simplify graphs based on reachability information tracked by the static analysis. Additionally, + * simplify graphs based on more concrete type information proven by the points-to analysis. The + * optimizations enabled by the points-to analysis are a superset of the optimizations enabled by + * the reachability analysis. + */ +class TypeFlowSimplifier extends ReachabilitySimplifier { + + private final PointsToAnalysis analysis; + + private final MethodTypeFlow methodFlow; + private final TypeFlow[] parameterFlows; + private final NodeMap> nodeFlows; + + private final boolean allowConstantFolding; + private final boolean allowOptimizeReturnParameter; + + private final EconomicSet unreachableValues = EconomicSet.create(); + private final NodeBitMap createdPiNodes; + + TypeFlowSimplifier(StrengthenGraphs strengthenGraphs, AnalysisMethod method, StructuredGraph graph) { + super(strengthenGraphs, method, graph); + analysis = (PointsToAnalysis) strengthenGraphs.bb; + methodFlow = ((PointsToAnalysisMethod) method).getTypeFlow(); + AnalysisError.guarantee(methodFlow.flowsGraphCreated(), "Trying to strengthen a method without a type flows graph: %s.", method); + MethodFlowsGraph originalFlows = methodFlow.getMethodFlowsGraph(); + parameterFlows = originalFlows.getParameters(); + nodeFlows = new NodeMap<>(graph); + var cursor = originalFlows.getNodeFlows().getEntries(); + while (cursor.advance()) { + Node node = cursor.getKey().getNode(); + assert nodeFlows.get(node) == null : "overwriting existing entry for " + node; + nodeFlows.put(node, cursor.getValue()); + } + createdPiNodes = new NodeBitMap(graph); + + allowConstantFolding = strengthenGraphs.strengthenGraphWithConstants && analysis.getHostVM().allowConstantFolding(method); + + /* + * In deoptimization target methods optimizing the return parameter can make new values live + * across deoptimization entrypoints. + * + * In runtime-compiled methods invokes may be intrinsified during runtime partial evaluation + * and change the behavior of the invoke. This would be a problem if the behavior of the + * method completely changed; however, currently this intrinsification is used to improve + * the stamp of the returned value, but not to alter the semantics. Hence, it is preferred + * to continue to use the return value of the invoke (as opposed to the parameter value). + */ + allowOptimizeReturnParameter = method.isOriginalMethod() && analysis.optimizeReturnedParameter(); + } + + private TypeFlow getNodeFlow(Node node) { + return nodeFlows == null || nodeFlows.isNew(node) ? null : nodeFlows.get(node); + } + + @Override + public void simplify(Node n, SimplifierTool tool) { + if (n instanceof ValueNode node) { + super.tryImproveStamp(node, tool); + } + + if (strengthenGraphs.simplifyDelegate(n, tool)) { + // Handled in the delegate simplification. + return; + } + + switch (n) { + case ParameterNode node -> handleParameter(node, tool); + case LoadFieldNode node -> handleLoadField(node, tool); + case LoadIndexedNode node -> handleLoadIndexed(node, tool); + case IfNode node -> handleIf(node, tool); + case FixedGuardNode node -> handleFixedGuard(node, tool); + case Invoke invoke -> handleInvoke(invoke, tool); + // Next simplifications don't use type states and are shared with reachability analysis. + case InstanceOfNode node -> super.handleInstanceOf(node, tool); + case ClassIsAssignableFromNode node -> super.handleClassIsAssignableFrom(node, tool); + case BytecodeExceptionNode node -> super.handleBytecodeException(node, tool); + case FrameState node -> super.handleFrameState(node); + case PiNode node -> super.handlePi(node, tool); + case null, default -> { + } + } + } + + private void handleParameter(ParameterNode node, SimplifierTool tool) { + StartNode anchorPoint = graph.start(); + Object newStampOrConstant = strengthenStampFromTypeFlow(node, parameterFlows[node.index()], anchorPoint, tool); + updateStampUsingPiNode(node, newStampOrConstant, anchorPoint, tool); + } + + private void handleLoadField(LoadFieldNode node, SimplifierTool tool) { + /* + * First step: it is beneficial to strengthen the stamp of the LoadFieldNode because then + * there is no artificial anchor after which the more precise type is available. However, + * the memory load will be a floating node later, so we can only update the stamp directly + * to the stamp that is correct for the whole method and all inlined methods. + */ + PointsToAnalysisField field = (PointsToAnalysisField) node.field(); + Object fieldNewStampOrConstant = strengthenStampFromTypeFlow(node, field.getSinkFlow(), node, tool); + if (fieldNewStampOrConstant instanceof JavaConstant) { + ConstantNode replacement = ConstantNode.forConstant((JavaConstant) fieldNewStampOrConstant, analysis.getMetaAccess(), graph); + graph.replaceFixedWithFloating(node, replacement); + tool.addToWorkList(replacement); + } else { + super.updateStampInPlace(node, (Stamp) fieldNewStampOrConstant, tool); + + /* + * Second step: strengthen using context-sensitive analysis results, which requires an + * anchored PiNode. + */ + Object nodeNewStampOrConstant = strengthenStampFromTypeFlow(node, getNodeFlow(node), node, tool); + updateStampUsingPiNode(node, nodeNewStampOrConstant, node, tool); + } + } + + private void handleLoadIndexed(LoadIndexedNode node, SimplifierTool tool) { + Object newStampOrConstant = strengthenStampFromTypeFlow(node, getNodeFlow(node), node, tool); + updateStampUsingPiNode(node, newStampOrConstant, node, tool); + } + + private void handleIf(IfNode node, SimplifierTool tool) { + boolean trueUnreachable = isUnreachable(node.trueSuccessor()); + boolean falseUnreachable = isUnreachable(node.falseSuccessor()); + + if (trueUnreachable && falseUnreachable) { + super.makeUnreachable(node, tool, () -> super.location(node) + ": both successors of IfNode are unreachable"); + + } else if (trueUnreachable || falseUnreachable) { + AbstractBeginNode killedBegin = node.successor(trueUnreachable); + AbstractBeginNode survivingBegin = node.successor(!trueUnreachable); + + if (survivingBegin.hasUsages()) { + /* + * Even when we know that the IfNode is not necessary because the condition is + * statically proven, all PiNode that are anchored at the surviving branch must + * remain anchored at exactly this point. It would be wrong to anchor the PiNode at + * the BeginNode of the preceding block, because at that point the condition is not + * proven yet. + */ + ValueAnchorNode anchor = graph.add(new ValueAnchorNode()); + graph.addAfterFixed(survivingBegin, anchor); + survivingBegin.replaceAtUsages(anchor, InputType.Guard, InputType.Anchor); + } + graph.removeSplit(node, survivingBegin); + GraphUtil.killCFG(killedBegin); + } + } + + private void handleFixedGuard(FixedGuardNode node, SimplifierTool tool) { + if (isUnreachable(node)) { + node.setCondition(LogicConstantNode.tautology(graph), true); + tool.addToWorkList(node); + } + } + + private boolean isUnreachable(Node branch) { + TypeFlow branchFlow = getNodeFlow(branch); + if (branchFlow != null && !methodFlow.isSaturated(analysis, branchFlow)) { + if (!branchFlow.isFlowEnabled()) { + return true; + } + TypeState typeState = methodFlow.foldTypeFlow(analysis, branchFlow); + if (branchFlow.isPrimitiveFlow()) { + /* + * This assert is a safeguard to verify the assumption that only one type of flow + * has to be considered as a branch predicate at the moment. + */ + assert branchFlow instanceof PrimitiveFilterTypeFlow : "Unexpected type of primitive flow encountered as branch predicate: " + branchFlow; + } + return typeState.isEmpty(); + } + return false; + } + + private void handleInvoke(Invoke invoke, SimplifierTool tool) { + if (!(invoke.callTarget() instanceof MethodCallTargetNode callTarget)) { + return; + } + if (super.maybeMarkUnreachable(invoke, tool)) { + /* Invoke is unreachable, there is no point in improving any types further. */ + return; + } + + FixedNode node = invoke.asFixedNode(); + InvokeTypeFlow invokeFlow = (InvokeTypeFlow) getNodeFlow(node); + if (invokeFlow == null) { + /* No points-to analysis results. */ + return; + } + if (!invokeFlow.isFlowEnabled()) { + super.unreachableInvoke(invoke, tool, () -> super.location(invoke) + ": flow is not enabled by its predicate " + invokeFlow.getPredicate()); + /* Invoke is unreachable, there is no point in improving any types further. */ + return; + } + + AnalysisMethod targetMethod = (AnalysisMethod) callTarget.targetMethod(); + + Collection callees = invokeFlow.getOriginalCallees(); + if (callees.isEmpty()) { + if (strengthenGraphs.isClosedTypeWorld) { + /* Invoke is unreachable, there is no point in improving any types further. */ + super.unreachableInvoke(invoke, tool, () -> super.location(invoke) + ": empty list of callees for call to " + targetMethod.getQualifiedName()); + } + /* In open world we cannot make any assumptions about an invoke with 0 callees. */ + return; + } + assert invokeFlow.isFlowEnabled() : "Disabled invoke should have no callees: " + invokeFlow + ", in method " + StrengthenGraphs.getQualifiedName(graph); + + FixedWithNextNode beforeInvoke = (FixedWithNextNode) invoke.predecessor(); + NodeInputList arguments = callTarget.arguments(); + for (int i = 0; i < arguments.size(); i++) { + ValueNode argument = arguments.get(i); + Object newStampOrConstant = strengthenStampFromTypeFlow(argument, invokeFlow.getActualParameters()[i], beforeInvoke, tool); + if (node.isDeleted()) { + /* Parameter stamp was empty, so invoke is unreachable. */ + return; + } + if (i == 0 && invoke.getInvokeKind() != CallTargetNode.InvokeKind.Static) { + /* + * Check for null receiver. If so, the invoke is unreachable. + * + * Note it is not necessary to check for an empty stamp, as in that case + * strengthenStampFromTypeFlow will make the invoke unreachable. + */ + boolean nullReceiver = false; + if (argument instanceof ConstantNode constantNode) { + nullReceiver = constantNode.getValue().isDefaultForKind(); + } + if (!nullReceiver && newStampOrConstant instanceof ObjectStamp stamp) { + nullReceiver = stamp.alwaysNull(); + } + if (!nullReceiver && newStampOrConstant instanceof Constant constantValue) { + nullReceiver = constantValue.isDefaultForKind(); + } + if (nullReceiver) { + invokeWithNullReceiver(invoke); + return; + } + } + if (newStampOrConstant != null) { + ValueNode pi = insertPi(argument, newStampOrConstant, beforeInvoke); + if (pi != null && pi != argument) { + callTarget.replaceAllInputs(argument, pi); + } + } + } + + if (callTarget.invokeKind().isDirect()) { + /* + * Note: A direct invoke doesn't necessarily imply that the analysis should have + * discovered a single callee. When dealing with interfaces it is in fact possible that + * the Graal stamps are more accurate than the analysis results. So an interface call + * may have already been optimized to a special call by stamp strengthening of the + * receiver object, hence the invoke kind is direct, whereas the points-to analysis + * inaccurately concluded there can be more than one callee. + * + * Below we just check that if there is a direct invoke *and* the analysis discovered a + * single callee, then the callee should match the target method. + */ + if (callees.size() == 1) { + AnalysisMethod singleCallee = callees.iterator().next(); + assert targetMethod.equals(singleCallee) : "Direct invoke target mismatch: " + targetMethod + " != " + singleCallee + ". Called from " + graph.method().format("%H.%n"); + } + } else if (AnnotationAccess.isAnnotationPresent(targetMethod, Delete.class)) { + /* We de-virtualize invokes to deleted methods since the callee must be unique. */ + AnalysisError.guarantee(callees.size() == 1, "@Delete methods should have a single callee."); + AnalysisMethod singleCallee = callees.iterator().next(); + devirtualizeInvoke(singleCallee, invoke); + } else if (targetMethod.canBeStaticallyBound() || strengthenGraphs.isClosedTypeWorld) { + /* + * We only de-virtualize invokes if we run a closed type world analysis or the target + * method can be trivially statically bound. + */ + if (callees.size() == 1) { + AnalysisMethod singleCallee = callees.iterator().next(); + devirtualizeInvoke(singleCallee, invoke); + } else { + TypeState receiverTypeState = null; + /* If the receiver flow is saturated, its exact type state does not matter. */ + if (invokeFlow.getTargetMethod().hasReceiver() && !methodFlow.isSaturated(analysis, invokeFlow.getReceiver())) { + receiverTypeState = methodFlow.foldTypeFlow(analysis, invokeFlow.getReceiver()); + } + assignInvokeProfiles(invoke, invokeFlow, callees, receiverTypeState, false); + } + } else { + /* Last resort, try to inject profiles optimistically. */ + TypeState receiverTypeState = null; + if (invokeFlow.getTargetMethod().hasReceiver()) { + if (methodFlow.isSaturated(analysis, invokeFlow)) { + /* + * For saturated invokes use all seen instantiated subtypes of target method + * declaring class. In an open world this is incomplete as new types may be seen + * later, but it is an optimistic approximation. + */ + receiverTypeState = targetMethod.getDeclaringClass().getTypeFlow(analysis, false).getState(); + } else { + receiverTypeState = methodFlow.foldTypeFlow(analysis, invokeFlow.getReceiver()); + } + } + if (receiverTypeState != null && receiverTypeState.typesCount() <= MAX_TYPES_OPTIMISTIC_PROFILES) { + assignInvokeProfiles(invoke, invokeFlow, callees, receiverTypeState, true); + } + } + + if (allowOptimizeReturnParameter && (strengthenGraphs.isClosedTypeWorld || callTarget.invokeKind().isDirect() || targetMethod.canBeStaticallyBound())) { + /* Can only optimize returned parameter when all possible callees are visible. */ + optimizeReturnedParameter(callees, arguments, node, tool); + } + + FixedWithNextNode anchorPointAfterInvoke = (FixedWithNextNode) (invoke instanceof InvokeWithExceptionNode ? invoke.next() : invoke); + TypeFlow nodeFlow = invokeFlow.getResult(); + if (nodeFlow != null && node.getStackKind() == JavaKind.Void && !methodFlow.isSaturated(analysis, nodeFlow)) { + /* + * We track the reachability of return statements in void methods via returning either + * Empty or AnyPrimitive TypeState, therefore we perform an emptiness check. + */ + var typeState = methodFlow.foldTypeFlow(analysis, nodeFlow); + if (typeState.isEmpty() && unreachableValues.add(node)) { + super.makeUnreachable(anchorPointAfterInvoke.next(), tool, () -> super.location(node) + ": return from void method was proven unreachable"); + } + } + Object newStampOrConstant = strengthenStampFromTypeFlow(node, nodeFlow, anchorPointAfterInvoke, tool); + updateStampUsingPiNode(node, newStampOrConstant, anchorPointAfterInvoke, tool); + } + + /** + * The invoke always has a null receiver, so it can be removed. + */ + private void invokeWithNullReceiver(Invoke invoke) { + FixedNode replacement = strengthenGraphs.createInvokeWithNullReceiverReplacement(graph); + ((FixedWithNextNode) invoke.predecessor()).setNext(replacement); + GraphUtil.killCFG(invoke.asFixedNode()); + } + + /** + * The invoke has only one callee, i.e., the call can be devirtualized to this callee. This + * allows later inlining of the callee. + */ + private void devirtualizeInvoke(AnalysisMethod singleCallee, Invoke invoke) { + if (ImageBuildStatistics.Options.CollectImageBuildStatistics.getValue(graph.getOptions())) { + ImageBuildStatistics.counters().incDevirtualizedInvokeCounter(); + } + + Stamp anchoredReceiverStamp = StampFactory.object(TypeReference.createWithoutAssumptions(singleCallee.getDeclaringClass())); + ValueNode piReceiver = insertPi(invoke.getReceiver(), anchoredReceiverStamp, (FixedWithNextNode) invoke.asNode().predecessor()); + if (piReceiver != null) { + invoke.callTarget().replaceFirstInput(invoke.getReceiver(), piReceiver); + } + + assert invoke.getInvokeKind().isIndirect() : invoke; + invoke.callTarget().setInvokeKind(CallTargetNode.InvokeKind.Special); + invoke.callTarget().setTargetMethod(singleCallee); + } + + /** + * Maximum number of types seen in a {@link TypeState} for a virtual {@link Invoke} to consider + * optimistic profile injection. See {@link #handleInvoke(Invoke, SimplifierTool)} for more + * details. Note that this is a footprint consideration - we do not want to carry around + * gargantuan {@link JavaTypeProfile} in {@link MethodCallTargetNode} that cannot be used + * anyway. + */ + private static final int MAX_TYPES_OPTIMISTIC_PROFILES = 100; + + private void assignInvokeProfiles(Invoke invoke, InvokeTypeFlow invokeFlow, Collection callees, TypeState receiverTypeState, boolean assumeNotRecorded) { + /* + * In an open type world we cannot trust the type state of the receiver for virtual calls as + * new subtypes could be added later. + * + * Note: assumeNotRecorded specifies if profiles are injected for a closed or open world. + * For a closed world with precise analysis results we never have a notRecordedProbabiltiy + * in any profile. For the open world we always assume that there is a not recorded + * probability in the profile. Such a not recorded probability will be injected if + * assumeNotRecorded==true. + */ + JavaTypeProfile typeProfile = strengthenGraphs.makeTypeProfile(receiverTypeState, assumeNotRecorded); + /* + * In a closed type world analysis the method profile of an invoke is complete and contains + * all the callees reachable at that invocation location. Even if that invoke is saturated + * it is still correct as it contains all the reachable implementations of the target + * method. However, in an open type world the method profile of an invoke, saturated or not, + * is incomplete, as there can be implementations that we haven't yet seen. + */ + JavaMethodProfile methodProfile = strengthenGraphs.makeMethodProfile(callees, assumeNotRecorded); + + assert typeProfile == null || typeProfile.getTypes().length > 1 || assumeNotRecorded : "Should devirtualize with typeProfile=" + typeProfile + " and methodProfile=" + methodProfile + + " and callees" + callees + " invoke " + invokeFlow + " " + invokeFlow.getReceiver() + " in method " + StrengthenGraphs.getQualifiedName(graph); + assert methodProfile == null || methodProfile.getMethods().length > 1 || assumeNotRecorded : "Should devirtualize with typeProfile=" + typeProfile + " and methodProfile=" + methodProfile + + " and callees" + callees + " invoke " + invokeFlow + " " + invokeFlow.getReceiver() + " in method " + StrengthenGraphs.getQualifiedName(graph); + + strengthenGraphs.setInvokeProfiles(invoke, typeProfile, methodProfile); + } + + /** + * If all possible callees return the same parameter, then we can replace the invoke with that + * parameter at all usages. This is the same that would happen when the callees are inlined. So + * we get a bit of the benefits of method inlining without actually performing the inlining. + */ + private static void optimizeReturnedParameter(Collection callees, NodeInputList arguments, FixedNode invoke, SimplifierTool tool) { + int returnedParameterIndex = -1; + for (AnalysisMethod callee : callees) { + if (callee.hasNeverInlineDirective()) { + /* + * If the method is explicitly marked as "never inline", it might be an intentional + * sink to prevent an optimization. Mostly, this is a pattern we use in unit tests. + * So this reduces the surprise that tests are "too well optimized" without doing + * any harm for real-world methods. + */ + return; + } + int returnedCalleeParameterIndex = PointsToAnalysis.assertPointsToAnalysisMethod(callee).getTypeFlow().getReturnedParameterIndex(); + if (returnedCalleeParameterIndex == -1) { + /* This callee does not return a parameter. */ + return; + } + if (returnedParameterIndex == -1) { + returnedParameterIndex = returnedCalleeParameterIndex; + } else if (returnedParameterIndex != returnedCalleeParameterIndex) { + /* This callee returns a different parameter than a previous callee. */ + return; + } + } + assert returnedParameterIndex != -1 : callees; + + ValueNode returnedActualParameter = arguments.get(returnedParameterIndex); + tool.addToWorkList(invoke.usages()); + invoke.replaceAtUsages(returnedActualParameter); + } + + private void updateStampUsingPiNode(ValueNode node, Object newStampOrConstant, FixedWithNextNode anchorPoint, SimplifierTool tool) { + if (newStampOrConstant != null && node.hasUsages() && !createdPiNodes.isMarked(node)) { + ValueNode pi = insertPi(node, newStampOrConstant, anchorPoint); + if (pi != null) { + /* + * The Canonicalizer that drives all of our node processing is iterative. We only + * want to insert the PiNode the first time we handle a node. + */ + createdPiNodes.mark(node); + + if (pi.isConstant()) { + node.replaceAtUsages(pi); + } else { + FrameState anchorState = node instanceof StateSplit ? ((StateSplit) node).stateAfter() : graph.start().stateAfter(); + node.replaceAtUsages(pi, usage -> usage != pi && usage != anchorState); + } + tool.addToWorkList(pi.usages()); + } + } + } + + /** + * See comment on {@link StrengthenGraphs} on why anchoring is necessary. + */ + private ValueNode insertPi(ValueNode input, Object newStampOrConstant, FixedWithNextNode anchorPoint) { + if (newStampOrConstant instanceof JavaConstant constant) { + if (input.isConstant()) { + assert analysis.getConstantReflectionProvider().constantEquals(input.asConstant(), constant) : input.asConstant() + ", " + constant; + return null; + } + return ConstantNode.forConstant(constant, analysis.getMetaAccess(), graph); + } + + Stamp piStamp = (Stamp) newStampOrConstant; + Stamp oldStamp = input.stamp(NodeView.DEFAULT); + Stamp computedStamp = oldStamp.improveWith(piStamp); + if (oldStamp.equals(computedStamp)) { + /* The PiNode does not give any additional information. */ + return null; + } + + ValueAnchorNode anchor = graph.add(new ValueAnchorNode()); + graph.addAfterFixed(anchorPoint, anchor); + return graph.unique(new PiNode(input, piStamp, anchor)); + } + + private Object strengthenStampFromTypeFlow(ValueNode node, TypeFlow nodeFlow, FixedWithNextNode anchorPoint, SimplifierTool tool) { + if (nodeFlow == null || !analysis.isSupportedJavaKind(node.getStackKind())) { + return null; + } + if (methodFlow.isSaturated(analysis, nodeFlow)) { + /* The type flow is saturated, its type state does not matter. */ + return null; + } + if (unreachableValues.contains(node)) { + /* This node has already been made unreachable - no further action is needed. */ + return null; + } + /* + * If there are no usages of the node, then adding a PiNode would only bloat the graph. + * However, we don't immediately return null since the stamp can still indicate this node is + * unreachable. + */ + boolean hasUsages = node.usages().filter(n -> !(n instanceof FrameState)).isNotEmpty(); + + if (!nodeFlow.isFlowEnabled()) { + super.makeUnreachable(anchorPoint.next(), tool, () -> super.location(node) + ": flow is not enabled by its predicate " + nodeFlow.getPredicate()); + unreachableValues.add(node); + return null; + } + TypeState nodeTypeState = methodFlow.foldTypeFlow(analysis, nodeFlow); + + if (hasUsages && allowConstantFolding && !nodeTypeState.canBeNull()) { + JavaConstant constantValue = nodeTypeState.asConstant(); + if (constantValue != null) { + return constantValue; + } + } + + node.inferStamp(); + Stamp s = node.stamp(NodeView.DEFAULT); + if (s.isIntegerStamp() || nodeTypeState.isPrimitive()) { + return getIntegerStamp(node, ((IntegerStamp) s), anchorPoint, nodeTypeState, tool); + } + + ObjectStamp oldStamp = (ObjectStamp) s; + AnalysisType oldType = (AnalysisType) oldStamp.type(); + boolean nonNull = oldStamp.nonNull() || !nodeTypeState.canBeNull(); + + /* + * Find all types of the TypeState that are compatible with the current stamp. Since stamps + * are propagated around immediately by the Canonicalizer it is possible and allowed that + * the stamp is already more precise than the static analysis results. + */ + List typeStateTypes = new ArrayList<>(nodeTypeState.typesCount()); + for (AnalysisType typeStateType : nodeTypeState.types(analysis)) { + if (oldType == null || (oldStamp.isExactType() ? oldType.equals(typeStateType) : oldType.isJavaLangObject() || oldType.isAssignableFrom(typeStateType))) { + typeStateTypes.add(typeStateType); + } + } + + if (typeStateTypes.isEmpty()) { + if (nonNull) { + super.makeUnreachable(anchorPoint.next(), tool, () -> super.location(node) + ": empty object type state when strengthening oldStamp " + oldStamp); + unreachableValues.add(node); + return null; + } else { + return hasUsages ? StampFactory.alwaysNull() : null; + } + + } else if (!hasUsages) { + // no need to return strengthened stamp if it is unused + return null; + } else if (typeStateTypes.size() == 1) { + AnalysisType exactType = typeStateTypes.get(0); + assert strengthenGraphs.getSingleImplementorType(exactType) == null || exactType.equals(strengthenGraphs.getSingleImplementorType(exactType)) : "exactType=" + exactType + + ", singleImplementor=" + strengthenGraphs.getSingleImplementorType(exactType); + assert exactType.equals(strengthenGraphs.getStrengthenStampType(exactType)) : exactType; + + if (!oldStamp.isExactType() || !exactType.equals(oldType)) { + ResolvedJavaType targetType = toTargetFunction.apply(exactType); + if (targetType != null) { + TypeReference typeRef = TypeReference.createExactTrusted(targetType); + return StampFactory.object(typeRef, nonNull); + } + } + + } else if (!oldStamp.isExactType()) { + assert typeStateTypes.size() > 1 : typeStateTypes; + AnalysisType baseType = typeStateTypes.get(0); + for (int i = 1; i < typeStateTypes.size(); i++) { + if (baseType.isJavaLangObject()) { + break; + } + baseType = baseType.findLeastCommonAncestor(typeStateTypes.get(i)); + } + + if (oldType != null && !oldType.isAssignableFrom(baseType)) { + /* + * When the original stamp is an interface type, we do not want to weaken that type + * with the common base class of all implementation types (which could even be + * java.lang.Object). + */ + baseType = oldType; + } + + /* + * With more than one type in the type state, there cannot be a single implementor. + * Because that single implementor would need to be the only type in the type state. + */ + assert strengthenGraphs.getSingleImplementorType(baseType) == null || baseType.equals(strengthenGraphs.getSingleImplementorType(baseType)) : "baseType=" + baseType + + ", singleImplementor=" + strengthenGraphs.getSingleImplementorType(baseType); + + AnalysisType newType = strengthenGraphs.getStrengthenStampType(baseType); + + assert typeStateTypes.stream().map(newType::isAssignableFrom).reduce(Boolean::logicalAnd).get() : typeStateTypes; + + if (!newType.equals(oldType) && (oldType != null || !newType.isJavaLangObject())) { + ResolvedJavaType targetType = toTargetFunction.apply(newType); + if (targetType != null) { + TypeReference typeRef = TypeReference.createTrustedWithoutAssumptions(targetType); + return StampFactory.object(typeRef, nonNull); + } + } + } + + if (nonNull != oldStamp.nonNull()) { + assert nonNull : oldStamp; + return oldStamp.asNonNull(); + } + /* Nothing to strengthen. */ + return null; + } + + private IntegerStamp getIntegerStamp(ValueNode node, IntegerStamp originalStamp, FixedWithNextNode anchorPoint, TypeState nodeTypeState, SimplifierTool tool) { + assert analysis.trackPrimitiveValues() : nodeTypeState + "," + node + " in " + node.graph(); + assert nodeTypeState != null && (nodeTypeState.isEmpty() || nodeTypeState.isPrimitive()) : nodeTypeState + "," + node + " in " + node.graph(); + if (nodeTypeState.isEmpty()) { + super.makeUnreachable(anchorPoint.next(), tool, () -> super.location(node) + ": empty primitive type state when strengthening oldStamp " + originalStamp); + unreachableValues.add(node); + return null; + } + if (nodeTypeState instanceof PrimitiveConstantTypeState constantTypeState) { + long constantValue = constantTypeState.getValue(); + if (node instanceof ConstantNode constant) { + /* + * Sanity check, verify that what was proven by the analysis is consistent with the + * constant node in the graph. + */ + Constant value = constant.getValue(); + assert value instanceof PrimitiveConstant : "Node " + value + " should be a primitive constant when extracting an integer stamp, method " + node.graph().method(); + assert ((PrimitiveConstant) value).asLong() == constantValue : "The actual value of node: " + value + " is different than the value " + constantValue + + " computed by points-to analysis, method in " + node.graph().method(); + } else { + return IntegerStamp.createConstant(originalStamp.getBits(), constantValue); + } + } + return null; + } +} diff --git a/substratevm/src/com.oracle.graal.reachability/src/com/oracle/graal/reachability/ReachabilityAnalysisMethod.java b/substratevm/src/com.oracle.graal.reachability/src/com/oracle/graal/reachability/ReachabilityAnalysisMethod.java index c3f8bd72fa71..fb1f3341043f 100644 --- a/substratevm/src/com.oracle.graal.reachability/src/com/oracle/graal/reachability/ReachabilityAnalysisMethod.java +++ b/substratevm/src/com.oracle.graal.reachability/src/com/oracle/graal/reachability/ReachabilityAnalysisMethod.java @@ -40,6 +40,7 @@ import jdk.graal.compiler.debug.DebugContext; import jdk.graal.compiler.debug.GraalError; +import jdk.graal.compiler.nodes.EncodedGraph; import jdk.graal.compiler.nodes.GraphEncoder; import jdk.graal.compiler.nodes.Invoke; import jdk.graal.compiler.nodes.StructuredGraph; @@ -110,6 +111,11 @@ public List getInvokeLocations() { return calledFrom; } + @Override + public Iterable getEncodedNodeReferences() { + return null; + } + public void addCaller(BytecodePosition bytecodePosition) { calledFrom.add(bytecodePosition); }