blob: 0ffdeff5ed41deba8332d18632c72593171f6409 [file] [log] [blame]
/*
* Copyright (c) 2015, 2016, 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.
*
* 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 org.graalvm.compiler.core.test.tutorial;
import static org.graalvm.compiler.core.test.GraalCompilerTest.getInitialOptions;
import java.util.ArrayDeque;
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.graalvm.compiler.debug.DebugHandlersFactory;
import org.graalvm.compiler.debug.DebugContext;
import org.graalvm.compiler.debug.GraalError;
import org.graalvm.compiler.graph.Node;
import org.graalvm.compiler.graph.NodeMap;
import org.graalvm.compiler.java.GraphBuilderPhase;
import org.graalvm.compiler.nodes.CallTargetNode.InvokeKind;
import org.graalvm.compiler.nodes.ConstantNode;
import org.graalvm.compiler.nodes.FixedNode;
import org.graalvm.compiler.nodes.Invoke;
import org.graalvm.compiler.nodes.ParameterNode;
import org.graalvm.compiler.nodes.ReturnNode;
import org.graalvm.compiler.nodes.StructuredGraph;
import org.graalvm.compiler.nodes.ValueNode;
import org.graalvm.compiler.nodes.ValuePhiNode;
import org.graalvm.compiler.nodes.graphbuilderconf.GraphBuilderConfiguration;
import org.graalvm.compiler.nodes.graphbuilderconf.GraphBuilderConfiguration.BytecodeExceptionMode;
import org.graalvm.compiler.nodes.graphbuilderconf.GraphBuilderConfiguration.Plugins;
import org.graalvm.compiler.nodes.graphbuilderconf.InvocationPlugins;
import org.graalvm.compiler.nodes.java.LoadFieldNode;
import org.graalvm.compiler.nodes.java.MethodCallTargetNode;
import org.graalvm.compiler.nodes.java.NewArrayNode;
import org.graalvm.compiler.nodes.java.NewInstanceNode;
import org.graalvm.compiler.nodes.java.StoreFieldNode;
import org.graalvm.compiler.nodes.spi.StampProvider;
import org.graalvm.compiler.nodes.util.GraphUtil;
import org.graalvm.compiler.options.OptionValues;
import org.graalvm.compiler.phases.OptimisticOptimizations;
import org.graalvm.compiler.phases.graph.StatelessPostOrderNodeIterator;
import jdk.vm.ci.meta.JavaConstant;
import jdk.vm.ci.meta.JavaKind;
import jdk.vm.ci.meta.MetaAccessProvider;
import jdk.vm.ci.meta.ResolvedJavaField;
import jdk.vm.ci.meta.ResolvedJavaMethod;
import jdk.vm.ci.meta.ResolvedJavaType;
/**
* A simple context-insensitive static analysis based on the Graal API. It is intended for
* educational purposes, not for use in production. Only a limited set of Java functionality is
* supported to keep the code minimal.
* <p>
* The analysis builds a directed graph of {@link TypeFlow type flows}. If a type is added to type
* flow, it is propagated to all {@link TypeFlow#uses uses} of the type flow. Types are propagated
* using a {@link #worklist} of changed type flows until a fixpoint is reached, i.e., until no more
* types need to be added to any type state.
* <p>
* The type flows are constructed from a high-level Graal graph by the {@link TypeFlowBuilder}. All
* nodes that operate on {@link JavaKind#Object object} values are converted to the appropriate type
* flows. The analysis is context insensitive: every Java field has {@link Results#lookupField one
* list} of types assigned to the field; every Java method has {@link Results#lookupMethod one
* state} for each {@link MethodState#formalParameters parameter} as well as the
* {@link MethodState#formalReturn return value}.
*/
public class StaticAnalysis {
/** Access to type, method, and fields using the Graal API. */
private final MetaAccessProvider metaAccess;
/** Access to platform dependent stamps. */
private final StampProvider stampProvider;
/** The results of the static analysis. */
private final Results results;
/** Worklist for fixpoint iteration. */
private final Deque<WorklistEntry> worklist;
public StaticAnalysis(MetaAccessProvider metaAccess, StampProvider stampProvider) {
this.metaAccess = metaAccess;
this.stampProvider = stampProvider;
this.results = new Results();
this.worklist = new ArrayDeque<>();
}
/**
* Adds a root method to the static analysis. The method must be static and must not have any
* parameters, because the possible types of the parameters would not be known.
*/
public void addMethod(ResolvedJavaMethod method) {
if (!method.isStatic() || method.getSignature().getParameterCount(false) > 0) {
error("Entry point method is not static or has parameters: " + method.format("%H.%n(%p)"));
}
addToWorklist(results.lookupMethod(method));
}
/**
* Performs the fixed-point analysis that finds all methods transitively reachable from the
* {@link #addMethod root methods}.
*/
public void finish() {
while (!worklist.isEmpty()) {
worklist.removeFirst().process();
}
}
/**
* Returns the static analysis results computed by {@link StaticAnalysis#finish}.
*/
public Results getResults() {
return results;
}
protected void addToWorklist(WorklistEntry task) {
worklist.addLast(task);
}
protected static RuntimeException error(String msg) {
throw GraalError.shouldNotReachHere(msg);
}
/**
* Base class for all work items that can be {@link #addToWorklist added to the worklist}.
*/
abstract class WorklistEntry {
protected abstract void process();
}
/**
* The results computed by the static analysis.
*/
public class Results {
private final TypeFlow allInstantiatedTypes;
private final Map<ResolvedJavaField, TypeFlow> fields;
private final Map<ResolvedJavaMethod, MethodState> methods;
protected Results() {
allInstantiatedTypes = new TypeFlow();
fields = new HashMap<>();
methods = new HashMap<>();
}
/**
* All {@link TypeFlow#getTypes() types} that are found to be instantiated, i.e., all types
* allocated by the reachable instance and array allocation bytecodes.
*/
public TypeFlow getAllInstantiatedTypes() {
return allInstantiatedTypes;
}
/**
* All {@link TypeFlow#getTypes() types} that the given field can have, i.e., all types
* assigned by the reachable field store bytecodes.
*/
public TypeFlow lookupField(ResolvedJavaField field) {
TypeFlow result = fields.get(field);
if (result == null) {
result = new TypeFlow();
fields.put(field, result);
}
return result;
}
/**
* All {@link TypeFlow#getTypes() types} that {@link MethodState#formalParameters
* parameters} and {@link MethodState#formalReturn return value} of the given method can
* have.
*/
public MethodState lookupMethod(ResolvedJavaMethod method) {
MethodState result = methods.get(method);
if (result == null) {
result = new MethodState(method);
methods.put(method, result);
}
return result;
}
}
/**
* The {@link TypeFlow#getTypes() types} of the parameters and return value of a method. Also
* serves as the worklist element to parse the bytecodes of the method.
*/
public class MethodState extends WorklistEntry {
private final ResolvedJavaMethod method;
private final TypeFlow[] formalParameters;
private final TypeFlow formalReturn;
private boolean processed;
protected MethodState(ResolvedJavaMethod method) {
this.method = method;
formalParameters = new TypeFlow[method.getSignature().getParameterCount(!method.isStatic())];
for (int i = 0; i < formalParameters.length; i++) {
formalParameters[i] = new TypeFlow();
}
formalReturn = new TypeFlow();
}
/**
* All {@link TypeFlow#getTypes() types} that the parameters of this method can have.
*/
public TypeFlow[] getFormalParameters() {
return formalParameters;
}
/**
* All {@link TypeFlow#getTypes() types} that the return value of this method can have.
*/
public TypeFlow getFormalReturn() {
return formalReturn;
}
@Override
@SuppressWarnings("try")
protected void process() {
if (!processed) {
/* We want to process a method only once. */
processed = true;
/*
* Build the Graal graph for the method using the bytecode parser provided by Graal.
*/
OptionValues options = getInitialOptions();
DebugContext debug = DebugContext.create(options, DebugHandlersFactory.LOADER);
StructuredGraph graph = new StructuredGraph.Builder(options, debug).method(method).build();
/*
* Support for graph dumping, IGV uses this information to show the method name of a
* graph.
*/
try (DebugContext.Scope scope = debug.scope("graph building", graph)) {
/*
* We want all types to be resolved by the graph builder, i.e., we want classes
* referenced by the bytecodes to be loaded and initialized. Since we do not run
* the code before static analysis, the classes would otherwise be not loaded
* yet and the bytecode parser would only create a graph.
*/
Plugins plugins = new Plugins(new InvocationPlugins());
GraphBuilderConfiguration graphBuilderConfig = GraphBuilderConfiguration.getDefault(plugins).withEagerResolving(true).withUnresolvedIsError(true);
/*
* For simplicity, we ignore all exception handling during the static analysis.
* This is a constraint of this example code, a real static analysis needs to
* handle the Graal nodes for throwing and handling exceptions.
*/
graphBuilderConfig = graphBuilderConfig.withBytecodeExceptionMode(BytecodeExceptionMode.OmitAll);
/*
* We do not want Graal to perform any speculative optimistic optimizations,
* i.e., we do not want to use profiling information. Since we do not run the
* code before static analysis, the profiling information is empty and therefore
* wrong.
*/
OptimisticOptimizations optimisticOpts = OptimisticOptimizations.NONE;
GraphBuilderPhase.Instance graphBuilder = new GraphBuilderPhase.Instance(metaAccess, stampProvider, null, null, graphBuilderConfig, optimisticOpts, null);
graphBuilder.apply(graph);
} catch (Throwable ex) {
debug.handle(ex);
}
/*
* Build the type flow graph from the Graal graph, i.e., process all nodes that are
* deal with objects.
*/
TypeFlowBuilder typeFlowBuilder = new TypeFlowBuilder(graph);
typeFlowBuilder.apply();
}
}
}
/**
* The active element during static analysis: types are added until a fixed point is reached.
* When a new type is added, it is propagated to all usages by putting this element on the
* {@link StaticAnalysis#addToWorklist worklist}.
*/
public class TypeFlow extends WorklistEntry {
private final Set<ResolvedJavaType> types;
private final Set<TypeFlow> uses;
protected TypeFlow() {
types = new HashSet<>();
uses = new HashSet<>();
}
/** Returns the types of this element. */
public Set<ResolvedJavaType> getTypes() {
return types;
}
/**
* Adds new types to this element. If that changes the state of this element, it is added to
* the {@link StaticAnalysis#addToWorklist worklist} in order to propagate the added types
* to all usages.
*/
protected void addTypes(Set<ResolvedJavaType> newTypes) {
if (types.addAll(newTypes)) {
addToWorklist(this);
}
}
/**
* Adds a new use to this element. All types of this element are propagated to the new
* usage.
*/
protected void addUse(TypeFlow use) {
if (uses.add(use)) {
use.addTypes(types);
}
}
/**
* Processing of the worklist element: propagate the types to all usages. That in turn can
* add the usages to the worklist (if the types of the usage are changed).
*/
@Override
protected void process() {
for (TypeFlow use : uses) {
use.addTypes(types);
}
}
}
/**
* The active element for method invocations. For {@link InvokeKind#Virtual virtual} and
* {@link InvokeKind#Interface interface} calls, the {@link TypeFlow#getTypes() types} of this
* node are the receiver types. When a new receiver type is added, a new callee might be added.
* Adding a new callee means linking the type flow of the actual parameters with the formal
* parameters of the callee, and linking the return value of the callee with the return value
* state of the invocation.
*
* Statically bindable methods calls ({@link InvokeKind#Static static} and
* {@link InvokeKind#Special special} calls) have only one callee, but use the same code for
* simplicity.
*/
class InvokeTypeFlow extends TypeFlow {
private final MethodCallTargetNode callTarget;
private final TypeFlow[] actualParameters;
private final TypeFlow actualReturn;
private final Set<ResolvedJavaMethod> callees;
protected InvokeTypeFlow(MethodCallTargetNode callTarget, TypeFlow[] actualParameterFlows, TypeFlow actualReturnFlow) {
this.callTarget = callTarget;
this.actualParameters = actualParameterFlows;
this.actualReturn = actualReturnFlow;
this.callees = new HashSet<>();
}
private void linkCallee(ResolvedJavaMethod callee) {
if (callees.add(callee)) {
/* We have added a new callee. */
/*
* Connect the actual parameters of the invocation with the formal parameters of the
* callee.
*/
MethodState calleeState = results.lookupMethod(callee);
for (int i = 0; i < actualParameters.length; i++) {
if (actualParameters[i] != null) {
actualParameters[i].addUse(calleeState.formalParameters[i]);
}
}
/*
* Connect the formal return value of the callee with the actual return value of the
* invocation.
*/
if (actualReturn != null) {
calleeState.formalReturn.addUse(actualReturn);
}
addToWorklist(calleeState);
}
}
@Override
protected void process() {
if (callTarget.invokeKind().isDirect()) {
/* Static and special calls: link the statically known callee method. */
linkCallee(callTarget.targetMethod());
} else {
/* Virtual and interface call: Iterate all receiver types. */
for (ResolvedJavaType type : getTypes()) {
/*
* Resolve the method call for one exact receiver type. The method linking
* semantics of Java are complicated, but fortunatley we can use the linker of
* the hosting Java VM. The Graal API exposes this functionality.
*/
ResolvedJavaMethod method = type.resolveConcreteMethod(callTarget.targetMethod(), callTarget.invoke().getContextType());
/*
* Since the static analysis is conservative, the list of receiver types can
* contain types that actually do not provide the method to be called. Ignore
* these.
*/
if (method != null && !method.isAbstract()) {
linkCallee(method);
}
}
}
super.process();
}
}
/**
* Converts the Graal nodes of a method to a type flow graph. The main part of the algorithm is
* a reverse-postorder iteration of the Graal nodes, which is provided by the base class
* {@link StatelessPostOrderNodeIterator}.
*/
class TypeFlowBuilder extends StatelessPostOrderNodeIterator {
private final StructuredGraph graph;
private final MethodState methodState;
/**
* Mapping from Graal nodes to type flows. This uses an efficient Graal-provided
* {@link NodeMap collection class}.
*/
private final NodeMap<TypeFlow> typeFlows;
protected TypeFlowBuilder(StructuredGraph graph) {
super(graph.start());
this.graph = graph;
this.methodState = results.lookupMethod(graph.method());
this.typeFlows = new NodeMap<>(graph);
}
/**
* Register the type flow node for a Graal node.
*/
private void registerFlow(ValueNode node, TypeFlow flow) {
/*
* We ignore intermediate nodes used by Graal that, e.g., add more type information or
* encapsulate values flowing out of loops.
*/
ValueNode unproxiedNode = GraphUtil.unproxify(node);
assert typeFlows.get(unproxiedNode) == null : "overwriting existing value";
typeFlows.set(unproxiedNode, flow);
}
/**
* Lookup the type flow node for a Graal node.
*/
private TypeFlow lookupFlow(ValueNode node) {
ValueNode unproxiedNode = GraphUtil.unproxify(node);
TypeFlow result = typeFlows.get(unproxiedNode);
if (result == null) {
/*
* This is only the prototype of a static analysis, the handling of many Graal nodes
* (such as array accesses) is not implemented.
*/
throw error("Node is not supported yet by static analysis: " + node.getClass().getName());
}
return result;
}
private boolean isObject(ValueNode node) {
return node.getStackKind() == JavaKind.Object;
}
@Override
public void apply() {
/*
* Before the reverse-postorder iteration of fixed nodes, we handle some classes of
* floating nodes.
*/
for (Node n : graph.getNodes()) {
if (n instanceof ParameterNode) {
/*
* Incoming method parameter already have a type flow created by the
* MethodState.
*/
ParameterNode node = (ParameterNode) n;
if (isObject(node)) {
registerFlow(node, methodState.formalParameters[(node.index())]);
}
} else if (n instanceof ValuePhiNode) {
/*
* Phi functions for loops are cyclic. We create the type flow here (before
* processing any loop nodes), but link the phi values only later (after
* processing of all loop nodes.
*/
ValuePhiNode node = (ValuePhiNode) n;
if (isObject(node)) {
registerFlow(node, new TypeFlow());
}
} else if (n instanceof ConstantNode) {
/* Constants have a known type. */
ConstantNode node = (ConstantNode) n;
JavaConstant constant = node.asJavaConstant();
if (constant.isNull()) {
registerFlow(node, new TypeFlow());
}
}
}
super.apply();
for (Node n : graph.getNodes()) {
if (n instanceof ValuePhiNode) {
/*
* Post-processing of phi functions. Now the type flow for all input values has
* been created, so we can link the type flows together.
*/
ValuePhiNode node = (ValuePhiNode) n;
if (isObject(node)) {
TypeFlow phiFlow = lookupFlow(node);
for (ValueNode value : node.values()) {
lookupFlow(value).addUse(phiFlow);
}
}
}
}
}
private void allocation(ValueNode node, ResolvedJavaType type) {
/*
* The type flow of allocation nodes is one exact type. This is the source of the
* fixpoint iteration, the types are propagated downwards from these sources.
*/
TypeFlow flow = new TypeFlow();
flow.addTypes(Collections.singleton(type));
registerFlow(node, flow);
flow.addUse(results.getAllInstantiatedTypes());
}
@Override
protected void node(FixedNode n) {
if (n instanceof NewInstanceNode) {
NewInstanceNode node = (NewInstanceNode) n;
allocation(node, node.instanceClass());
} else if (n instanceof NewArrayNode) {
NewArrayNode node = (NewArrayNode) n;
allocation(node, node.elementType().getArrayClass());
} else if (n instanceof LoadFieldNode) {
/*
* The type flow of a field load is the type flow of the field itself. It
* accumulates all types ever stored to the field.
*/
LoadFieldNode node = (LoadFieldNode) n;
if (isObject(node)) {
registerFlow(node, results.lookupField(node.field()));
}
} else if (n instanceof StoreFieldNode) {
/*
* Connect the type flow of the stored value with the type flow of the field.
*/
StoreFieldNode node = (StoreFieldNode) n;
if (isObject(node.value())) {
TypeFlow fieldFlow = results.lookupField(node.field());
lookupFlow(node.value()).addUse(fieldFlow);
}
} else if (n instanceof ReturnNode) {
/*
* Connect the type flow of the returned value with the formal return type flow of
* the MethodState.
*/
ReturnNode node = (ReturnNode) n;
if (node.result() != null && isObject(node.result())) {
lookupFlow(node.result()).addUse(methodState.formalReturn);
}
} else if (n instanceof Invoke) {
/*
* Create the InvokeTypeFlow, which performs all the linking of actual and formal
* parameter values with all identified callees.
*/
Invoke invoke = (Invoke) n;
MethodCallTargetNode callTarget = (MethodCallTargetNode) invoke.callTarget();
TypeFlow[] actualParameters = new TypeFlow[callTarget.arguments().size()];
for (int i = 0; i < actualParameters.length; i++) {
ValueNode actualParam = callTarget.arguments().get(i);
if (isObject(actualParam)) {
actualParameters[i] = lookupFlow(actualParam);
}
}
TypeFlow actualReturn = null;
if (isObject(invoke.asNode())) {
actualReturn = new TypeFlow();
registerFlow(invoke.asNode(), actualReturn);
}
InvokeTypeFlow invokeFlow = new InvokeTypeFlow(callTarget, actualParameters, actualReturn);
if (callTarget.invokeKind().isIndirect()) {
/*
* For virtual and interface calls, new receiver types can lead to new callees.
* Connect the type flow of the receiver with the invocation flow.
*/
lookupFlow(callTarget.arguments().get(0)).addUse(invokeFlow);
}
/*
* Ensure the invocation is on the worklist at least once, even if it is a static
* call with not parameters that does not involve any type flows.
*/
addToWorklist(invokeFlow);
}
}
}
}