blob: 8cfd86a5ca42b48151eb0db084bb3a3c9adffdc1 [file] [log] [blame]
// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
library kernel.type_propagation.visualizer;
import 'constraints.dart';
import 'builder.dart';
import 'solver.dart';
import '../ast.dart';
import '../text/ast_to_text.dart';
import '../class_hierarchy.dart';
/// Visualizes the constraint system using a Graphviz dot graph.
///
/// Variables are visualized as nodes and constraints as labeled edges.
class Visualizer {
final Program program;
final Map<int, GraphNode> variableNodes = <int, GraphNode>{};
final Map<int, FunctionNode> value2function = <int, FunctionNode>{};
final Map<FunctionNode, int> function2value = <FunctionNode, int>{};
final Map<int, Annotation> latticePointAnnotation = <int, Annotation>{};
final Map<int, Annotation> valueAnnotation = <int, Annotation>{};
FieldNames fieldNames;
ConstraintSystem constraints;
Solver solver;
Builder builder;
ClassHierarchy get hierarchy => builder.hierarchy;
final Map<Member, Set<GraphNode>> _graphNodesInMember =
<Member, Set<GraphNode>>{};
Visualizer(this.program);
static Set<GraphNode> _makeGraphNodeSet() => new Set<GraphNode>();
Annotator getTextAnnotator() {
return new TextAnnotator(this);
}
GraphNode getVariableNode(int variable) {
return variableNodes[variable] ??= new GraphNode(variable);
}
/// Called from the builder to associate information with a variable.
///
/// The [node] has two purposes: it ensures that the variable will show
/// up in the graph for a the enclosing member, and the textual form of the
/// node will be part of its label.
///
/// The optional [info] argument provides additional context beyond the AST
/// node. When a constraint variable has no logical 1:1 corresondence with
/// an AST node, it is best to pick a nearby AST node and set the [info] to
/// clarify its relationship with the node.
void annotateVariable(int variable, TreeNode astNode, [String info]) {
if (astNode != null || info != null) {
if (astNode is VariableSet ||
astNode is PropertySet ||
astNode is StaticSet) {
// These will also be registered for the right-hand side, which makes
// for a better annotation.
return;
}
var node = getVariableNode(variable);
Member member = _getEnclosingMember(astNode);
node.addAnnotation(member, astNode, info);
_graphNodesInMember.putIfAbsent(member, _makeGraphNodeSet).add(node);
}
}
void annotateAssign(int source, int destination, TreeNode node) {
addEdge(source, destination, _getEnclosingMember(node), '');
}
void annotateSink(int source, int destination, TreeNode node) {
addEdge(source, destination, _getEnclosingMember(node), 'sink');
}
void annotateLoad(int object, int field, int destination, Member member) {
String fieldName = fieldNames.getDiagnosticNameOfField(field);
addEdge(object, destination, member, 'Load[$fieldName]');
}
void annotateStore(int object, int field, int source, Member member) {
String fieldName = fieldNames.getDiagnosticNameOfField(field);
addEdge(source, object, member, 'Store[$fieldName]');
}
void annotateDirectStore(int object, int field, int source, Member member) {
String fieldName = fieldNames.getDiagnosticNameOfField(field);
addEdge(source, object, member, 'Store![$fieldName]');
}
void annotateLatticePoint(int point, TreeNode node, [String info]) {
latticePointAnnotation[point] = new Annotation(node, info);
}
void annotateValue(int value, TreeNode node, [String info]) {
valueAnnotation[value] = new Annotation(node, info);
}
String getLatticePointName(int latticePoint) {
if (latticePoint < 0) return 'bottom';
return latticePointAnnotation[latticePoint].toLabel();
}
String getValueName(int value) {
return valueAnnotation[value].toLabel();
}
static Member _getEnclosingMember(TreeNode node) {
while (node != null) {
if (node is Member) return node;
node = node.parent;
}
return null;
}
void addEdge(int source, int destination, Member member, String label) {
var sourceNode = getVariableNode(source);
var destinationNode = getVariableNode(destination);
_graphNodesInMember.putIfAbsent(member, _makeGraphNodeSet)
..add(sourceNode)
..add(destinationNode);
sourceNode.addEdgeTo(destinationNode, member, label);
}
void annotateFunction(int value, FunctionNode function) {
value2function[value] = function;
function2value[function] = value;
}
FunctionNode getFunctionFromValue(int value) {
return value2function[value];
}
int getFunctionValue(FunctionNode node) {
return function2value[node];
}
Set<GraphNode> _getNodesInMember(Member member) {
return _graphNodesInMember.putIfAbsent(member, _makeGraphNodeSet);
}
String _getCodeAsLabel(Member member) {
String code = debugNodeToString(member);
code = escapeLabel(code);
// Replace line-breaks with left-aligned breaks.
code = code.replaceAll('\n', '\\l');
return code;
}
String _getValueLabel(GraphNode node) {
int latticePoint = solver.getVariableValue(node.variable);
if (latticePoint < 0) return 'bottom';
return escapeLabel(shorten(getLatticePointName(latticePoint)));
}
/// Returns the Graphviz Dot code a the subgraph relevant for [member].
String dumpMember(Member member) {
int freshIdCounter = 0;
StringBuffer buffer = new StringBuffer();
buffer.writeln('digraph {');
String source = _getCodeAsLabel(member);
buffer.writeln('source [shape=box,label="$source"]');
for (GraphNode node in _getNodesInMember(member)) {
int id = node.variable;
String label = node.getAnnotationInContextOf(member);
// Global nodes have a ton of edges that are visualized specially.
// If the global node has a local annotation, also print its annotated
// version somewhere, but omit all its edges.
if (node.isGlobal) {
if (label != '') {
label += '\n${node.globalAnnotation.toLabel()}';
buffer.writeln('$id [shape=record,label="$label"]');
}
continue;
}
String value = _getValueLabel(node);
buffer.writeln('$id [shape=record,label="{$label|$value}"]');
// Add outgoing edges.
// Keep track of all that edges leave the context of the current member
// ("external edges"). There can be a huge number of these, so we compact
// them into a single outgoing edge so as not to flood the graph.
Set<String> outgoingExternalEdgeLabels = new Set<String>();
for (Edge edge in node.outputs) {
if (edge.to.isLocal(member)) {
buffer.writeln('$id -> ${edge.to.variable} [label="${edge.label}"]');
} else if (outgoingExternalEdgeLabels.length < 3) {
String annotation = edge.to.externalLabel;
if (annotation != '') {
if (edge.label != '') {
annotation = '${edge.label} → $annotation';
}
outgoingExternalEdgeLabels.add(annotation);
}
} else if (outgoingExternalEdgeLabels.length == 3) {
outgoingExternalEdgeLabels.add('...');
}
}
// Emit the outgoing external edge.
if (outgoingExternalEdgeLabels.isNotEmpty) {
int freshId = ++freshIdCounter;
String outLabel = outgoingExternalEdgeLabels.join('\n');
buffer.writeln('x$freshId [shape=box,style=dotted,label="$outLabel"]');
buffer.writeln('$id -> x$freshId [style=dotted]');
}
// Show ingoing external edges. As before, avoid flooding the graph in
// case there are too many of them.
Set<String> ingoingExternalEdgeLabels = new Set<String>();
for (Edge edge in node.inputs) {
GraphNode source = edge.from;
if (source.isLocal(member)) continue;
String annotation = source.externalLabel;
if (annotation != '') {
if (ingoingExternalEdgeLabels.length < 3) {
if (edge.label != '') {
annotation = '$annotation → ${edge.label}';
}
ingoingExternalEdgeLabels.add(annotation);
} else {
ingoingExternalEdgeLabels.add('...');
break;
}
}
}
// Emit the ingoing external edge.
if (ingoingExternalEdgeLabels.isNotEmpty) {
int freshId = ++freshIdCounter;
String sourceLabel = ingoingExternalEdgeLabels.join('\n');
buffer.writeln('x$freshId '
'[shape=box,style=dotted,label="$sourceLabel"]');
buffer.writeln('x$freshId -> ${node.variable} [style=dotted]');
}
}
buffer.writeln('}');
return '$buffer';
}
}
class Annotation {
final TreeNode node;
final String info;
Annotation(this.node, this.info);
String toLabel() {
if (node == null && info == null) return '(missing annotation)';
if (node == null) return escapeLabel(info);
String label = node is NullLiteral
? 'null literal'
: node is FunctionNode ? shorten('${node.parent}') : shorten('$node');
if (info != null) {
label = '$info: $label';
}
label = escapeLabel(label);
return label;
}
String toLabelWithContext(Member member) {
String label = toLabel();
if (node == member) {
return label;
} else {
return '$label in $member';
}
}
}
class GraphNode {
final int variable;
final List<Edge> inputs = <Edge>[];
final List<Edge> outputs = <Edge>[];
final List<Annotation> annotations = <Annotation>[];
/// The annotation to show when visualized in the context of a given member.
final Map<Member, Annotation> annotationForContext = <Member, Annotation>{};
GraphNode(this.variable);
bool get isGlobal => annotationForContext.containsKey(null);
Annotation get globalAnnotation => annotationForContext[null];
bool isInScope(Member member) => annotationForContext.containsKey(member);
bool isLocal(Member member) => !isGlobal && isInScope(member);
/// The label to show for the given node when seen from the context of
/// another member.
String get externalLabel {
if (isGlobal) return globalAnnotation.toLabel();
if (annotationForContext.isEmpty) return '$variable';
Member member = annotationForContext.keys.first;
Annotation annotation = annotationForContext[member];
return '$variable:' + annotation.toLabelWithContext(member);
}
String getAnnotationInContextOf(Member member) {
if (annotationForContext.isEmpty) return '';
Annotation annotation = annotationForContext[member];
if (annotation != null) return '$variable:' + annotation.toLabel();
annotation =
annotationForContext[null] ?? annotationForContext.values.first;
return '$variable:' + annotation.toLabelWithContext(member);
}
void addEdgeTo(GraphNode other, Member member, String label) {
Edge edge = new Edge(this, other, member, label);
outputs.add(edge);
other.inputs.add(edge);
}
void addAnnotation(Member member, TreeNode astNode, String info) {
var annotation = new Annotation(astNode, info);
annotations.add(annotation);
annotationForContext[member] = annotation;
}
}
class Edge {
final GraphNode from, to;
final Member member;
final String label;
Edge(this.from, this.to, this.member, this.label);
}
final RegExp escapeRegexp = new RegExp('["{}<>|]', multiLine: true);
/// Escapes characters in [text] so it can be used as part of a label.
String escapeLabel(String text) {
return text.replaceAllMapped(escapeRegexp, (m) => '\\${m.group(0)}');
}
String shorten(String text) {
text = text.replaceAll('\n ', ' ').replaceAll('\n', ' ').trim();
if (text.length > 60) {
return text.substring(0, 30) + '...' + text.substring(text.length - 27);
}
return text;
}
class TextAnnotator extends Annotator {
final Visualizer visualizer;
final Map<VariableDeclaration, int> variables = <VariableDeclaration, int>{};
final Map<FunctionNode, int> functionReturns = <FunctionNode, int>{};
Builder get builder => visualizer.builder;
String getReference(Node node, Printer printer) {
if (node is Class) return printer.getClassReference(node);
if (node is Member) return printer.getMemberReference(node);
if (node is Library) return printer.getLibraryReference(node);
return debugNodeToString(node);
}
String getValueForVariable(Printer printer, int variable) {
if (variable == null) {
return '<missing type>';
}
var value = visualizer.solver.getValueInferredForVariable(variable);
return printer.getInferredValueString(value);
}
TextAnnotator(this.visualizer) {
// The correspondence between AST and constraint system is exposed by the
// builder, but only at the level of Members.
// To get to the correspondence at the statement/expression level, we use
// the annotation map from the visualizer API.
// TODO(asgerf): If we use these annotations for testing, the necessary
// bindings should arguably be part of the API for the Builder.
visualizer.variableNodes.forEach((int variable, GraphNode node) {
for (Annotation annotation in node.annotations) {
if (annotation.node is VariableDeclaration && annotation.info == null) {
variables[annotation.node] = variable;
}
if (annotation.node is FunctionNode && annotation.info == 'return') {
functionReturns[annotation.node] = variable;
}
}
});
}
String annotateVariable(Printer printer, VariableDeclaration node) {
return getValueForVariable(
printer, builder.global.parameters[node] ?? variables[node]);
}
String annotateReturn(Printer printer, FunctionNode node) {
if (node.parent is Constructor) return null;
return getValueForVariable(printer, builder.global.returns[node]);
}
String annotateField(Printer printer, Field node) {
return getValueForVariable(printer, builder.global.fields[node]);
}
}