@specs-feup/clava
Version:
A C/C++ source-to-source compiler written in Typescript
702 lines • 28.8 kB
JavaScript
import { debug } from "@specs-feup/lara/api/lara/core/LaraCore.js";
import Graphs from "@specs-feup/lara/api/lara/graphs/Graphs.js";
import Query from "@specs-feup/lara/api/weaver/Query.js";
import { FunctionJp, LabelStmt, Loop, Scope, Statement, } from "../../../Joinpoints.js";
import ClavaJoinPoints from "../../ClavaJoinPoints.js";
import CfgEdge from "./CfgEdge.js";
import CfgEdgeType from "./CfgEdgeType.js";
import CfgNodeType from "./CfgNodeType.js";
import CfgUtils from "./CfgUtils.js";
import NextCfgNode from "./NextCfgNode.js";
import DataFactory from "./nodedata/DataFactory.js";
export default class CfgBuilder {
/**
* AST node to process
*/
jp;
/**
* Graph being built
*/
graph;
/**
* Maps stmts to graph nodes
*/
nodes;
/**
* The start node of the graph
*/
startNode;
/**
* The end node of the graph
*/
endNode;
/**
* Maps the astId to the corresponding temporary statement
*/
temporaryStmts;
/**
* If true, uses deterministic ids for the graph ids (e.g. id_0, id_1...). Otherwise, uses $jp.astId whenever possible.
*/
deterministicIds;
/**
* Current id, in case deterministic ids are used
*/
currentId;
/**
* An instance of DataFactory, for creating graph node data
*/
dataFactory;
/**
* Calculates what node is unconditionally executed after a given statement
*/
nextNodes;
/**
* If true, each instruction list should be split
*/
splitInstList;
/**
* If true, the goto nodes should be excluded from the graph
*/
removeGotoNodes;
/**
* If true, the label nodes should be excluded from the graph
*/
removeLabelNodes;
/**
* If true, the temporary scope statements should not be removed from the graph
*/
keepTemporaryScopeStmts;
/**
* Creates a new instance of the CfgBuilder class
* @param $jp -
* @param deterministicIds - If true, uses deterministic ids for the graph ids (e.g. id_0, id_1...). Otherwise, uses $jp.astId whenever possible
* @param options - An object containing configuration options for the cfg
*/
constructor($jp, deterministicIds = false, options = {
/**
* If true, statements of each instruction list must be split
*/
splitInstList: false,
/**
* If true, the nodes that correspond to goto statements will be excluded from the resulting graph
*/
removeGotoNodes: false,
/**
* If true, the nodes that correspond to label statements will be excluded from the resulting graph
*/
removeLabelNodes: false,
/**
* If true, the temporary scope statements will be kept in the resulting graph
*/
keepTemporaryScopeStmts: false,
}) {
this.jp = $jp;
this.splitInstList = options.splitInstList ?? false;
this.removeGotoNodes = options.removeGotoNodes ?? false;
this.removeLabelNodes = options.removeLabelNodes ?? false;
this.keepTemporaryScopeStmts = options.keepTemporaryScopeStmts ?? false;
this.deterministicIds = deterministicIds;
this.currentId = 0;
this.dataFactory = new DataFactory(this.jp);
this.graph = Graphs.newGraph();
this.nodes = new Map();
// Create start and end nodes
// Do not add them to #nodes, since they have no associated statements
this.startNode = Graphs.addNode(this.graph, this.dataFactory.newData(CfgNodeType.START, undefined, "start", this.splitInstList));
this.endNode = Graphs.addNode(this.graph, this.dataFactory.newData(CfgNodeType.END, undefined, "end", this.splitInstList));
this.temporaryStmts = {};
this.nextNodes = new NextCfgNode(this.jp, this.nodes, this.endNode);
}
/**
* Returns the next id that will be used to identify a graph node
*/
nextId() {
const nextId = "id_" + this.currentId;
this.currentId++;
return nextId;
}
/**
* Connects two nodes using a directed edge
* @param source - Starting point from which the edge originates
* @param target - Destination point to which the edge points
* @param edgeType - The edge label that connects
*/
addEdge(source, target, edgeType) {
Graphs.addEdge(this.graph, source, target, new CfgEdge(edgeType));
}
/**
* Builds the control flow graph
* @returns An array that includes the built graph, the nodes, the start and end nodes
*/
build() {
this.addAuxComments();
this.createNodes();
//this._fillNodes();
this.connectNodes();
this.cleanCfg();
// TODO: Check graph invariants
// 1. Each node has either one unconditional outgoing edge,
// or two outgoing edges that must be a pair true/false,
// or if there is no outgoing edge must be the end node
return [this.graph, this.nodes, this.startNode, this.endNode];
}
/**
* Inserts comments that specify the beginning and end of a scope
*/
addAuxComments() {
for (const $currentJp of this.jp.descendants) {
if ($currentJp instanceof Scope) {
const $scopeStart = $currentJp.insertBegin(ClavaJoinPoints.comment("SCOPE_START"));
this.temporaryStmts[$scopeStart.astId] = $scopeStart;
const $scopeEnd = $currentJp.insertEnd(ClavaJoinPoints.comment("SCOPE_END"));
this.temporaryStmts[$scopeEnd.astId] = $scopeEnd;
}
}
}
/**
* Creates all nodes (except start and end), with only the leader statement
*/
createNodes() {
// Test all statements for leadership
// If they are leaders, create node
for (const $stmt of Query.searchFromInclusive(this.jp, Statement)) {
if (CfgUtils.isLeader($stmt)) {
if (this.splitInstList &&
CfgUtils.getNodeType($stmt) === CfgNodeType.INST_LIST) {
this.getOrAddNode($stmt, true, CfgNodeType.INST_LIST);
for (const $rightJp of $stmt.siblingsRight) {
const $right = $rightJp;
if (!CfgUtils.isLeader($right))
this.getOrAddNode($right, true, CfgNodeType.INST_LIST);
else
break;
}
}
else
this.getOrAddNode($stmt, true);
}
}
// Special case: if starting node is a statement and a graph node was not created for it (e.g. creating a graph starting from an arbitrary statement),
// create one with the type INST_LIST
if (this.jp instanceof Statement &&
this.nodes.get(this.jp.astId) === undefined) {
this.getOrAddNode(this.jp, true, CfgNodeType.INST_LIST);
}
}
/**
* Connects a node associated with a statement that is an instance of a "if" statement.
* @param node - Node whose type is "IF"
*/
connectIfNode(node) {
const ifStmt = node.data().if;
if (ifStmt === undefined) {
throw new Error("If statement is undefined");
}
const thenStmt = ifStmt.then;
if (thenStmt === undefined) {
throw new Error("Then statement is undefined");
}
const thenNode = this.nodes.get(thenStmt.astId);
if (thenNode === undefined) {
throw new Error("Node for then statement is undefined: " + thenStmt.astId);
}
this.addEdge(node, thenNode, CfgEdgeType.TRUE);
const elseStmt = ifStmt.else;
if (elseStmt !== undefined) {
const elseNode = this.nodes.get(elseStmt.astId);
if (elseNode === undefined) {
throw new Error("Node for else statement is undefined: " + elseStmt.astId);
}
this.addEdge(node, elseNode, CfgEdgeType.FALSE);
}
else {
// Usually there should always be a sibling, because of inserted comments
// However, if an arbitary statement is given as the starting point,
// sometimes there might not be nothing after. In this case, connect to the
// end node.
const afterNode = this.nextNodes.nextExecutedNode(ifStmt);
// Add edge
this.addEdge(node, afterNode, CfgEdgeType.FALSE);
}
}
/**
* Connects a node associated with a statement that is an instance of a "loop" statement.
* @param node - Node whose type is "LOOP"
*/
connectLoopNode(node) {
const $loop = node.data().loop;
if ($loop === undefined) {
throw new Error("Loop statement is undefined");
}
let afterStmt = undefined;
switch ($loop.kind) {
case "for":
afterStmt = $loop.init;
break;
case "while":
afterStmt = $loop.cond;
break;
case "dowhile":
afterStmt = $loop.body;
break;
default:
throw new Error("Case not defined for loops of kind " + $loop.kind);
}
const afterNode = this.nodes.get(afterStmt.astId) ?? this.endNode;
this.addEdge(node, afterNode, CfgEdgeType.UNCONDITIONAL);
}
/**
* Connects a node associated with a statement that is part of a loop header and corresponds to the loop condition
* @param node - Node whose type is "COND"
*/
connectCondNode(node) {
// Get kind of loop
const $condStmt = node.data().nodeStmt;
if ($condStmt === undefined) {
throw new Error("Cond statement is undefined");
}
const $loop = $condStmt.parent;
if ($loop === undefined) {
throw new Error("Loop is undefined");
}
if (!($loop instanceof Loop)) {
throw new Error("$loop is not an instance of Loop");
}
// True - first stmt of the loop body
const trueNode = this.nodes.get($loop.body.astId) ?? this.endNode;
this.addEdge(node, trueNode, CfgEdgeType.TRUE);
// False - next stmt of the loop
const falseNode = this.nextNodes.nextExecutedNode($loop);
// Create edge
this.addEdge(node, falseNode, CfgEdgeType.FALSE);
}
/**
* Connects a node associated with a statement that is an instance of a "break" statement.
* @param node - Node whose type is "BREAK"
*/
connectBreakNode(node) {
const $breakStmt = node.data().nodeStmt;
if ($breakStmt === undefined) {
throw new Error("Break statement is undefined");
}
const $enclosingStmt = $breakStmt.enclosingStmt;
const afterNode = this.nextNodes.nextExecutedNode($enclosingStmt);
this.addEdge(node, afterNode, CfgEdgeType.UNCONDITIONAL);
}
/**
* Connects a node associated with a statement that is an instance of a "continue" statement.
* @param node - Node whose type is "CONTINUE"
*/
connectContinueNode(node) {
const $continueStmt = node.data().nodeStmt;
if ($continueStmt === undefined) {
throw new Error("Continue statement is undefined");
}
const $loop = $continueStmt.getAncestor("loop");
if ($loop === undefined) {
throw new Error("Loop is undefined");
}
const $afterStmt = $loop.kind === "for" ? $loop.step : $loop.cond;
const afterNode = this.nodes.get($afterStmt.astId) ?? this.endNode;
this.addEdge(node, afterNode, CfgEdgeType.UNCONDITIONAL);
}
/**
* Connects a node associated with a statement that is an instance of a "switch" statement.
* @param node - Node whose type is "SWITCH"
*/
connectSwitchNode(node) {
const $switchStmt = node.data().switch;
if ($switchStmt === undefined) {
throw new Error("Switch statement is undefined");
}
let firstReachedCase = undefined;
// The first reached case is the first non-default case.
// If the switch only has one case statement, and it is the default case, then this default case will be the first reached case
for (const $case of $switchStmt.cases) {
firstReachedCase = this.nodes.get($case.astId);
if (!$case.isDefault) {
break;
}
}
if (firstReachedCase) {
this.addEdge(node, firstReachedCase, CfgEdgeType.UNCONDITIONAL);
}
}
/**
* Connects a node associated with a statement that is an instance of a "case" statement.
* @param node - Node whose type is "CASE"
*/
connectCaseNode(node) {
const $caseStmt = node.data().case;
if ($caseStmt === undefined) {
throw new Error("Case statement is undefined");
}
const $switchStmt = $caseStmt.getAncestor("switch");
if ($switchStmt === undefined) {
throw new Error("Switch statement is undefined");
}
const numCases = $switchStmt.cases.length;
const hasIntermediateDefault = $switchStmt.hasDefaultCase && !$switchStmt.cases[numCases - 1].isDefault;
// Connect the node to the first instruction to be executed
const firstExecutedInst = this.nextNodes.nextExecutedNode($caseStmt);
if ($caseStmt.isDefault)
this.addEdge(node, firstExecutedInst, CfgEdgeType.UNCONDITIONAL);
else
this.addEdge(node, firstExecutedInst, CfgEdgeType.TRUE);
let falseNode = undefined;
if ($caseStmt.nextCase !== undefined) {
// Not the last case
// If the next case is an intermediate default case, the node should be connected to the CASE node following the default case
if (hasIntermediateDefault && $caseStmt.nextCase.isDefault)
falseNode = this.nodes.get($caseStmt.nextCase.nextCase.astId);
else if (!$caseStmt.isDefault)
// Else, if it is not an intermediate default case, it should be connected to the next case
falseNode = this.nodes.get($caseStmt.nextCase.astId);
}
else if (!$caseStmt.isDefault) {
// Last case but not a default case
// If switch statement has an intermediate default case, connect the current statement to the default case
if (hasIntermediateDefault)
falseNode = this.nodes.get($switchStmt.getDefaultCase.astId);
// Else, connect it to the statement following the switch
else
falseNode = this.nextNodes.nextExecutedNode($switchStmt);
}
if (falseNode !== undefined)
this.addEdge(node, falseNode, CfgEdgeType.FALSE);
}
/**
* Connects a node associated with a statement that is part of a loop header and corresponds to the loop initialization
* @param node - Node whose type is "INIT"
*/
connectInitNode(node) {
const $initStmt = node.data().nodeStmt;
if ($initStmt === undefined) {
throw new Error("Init statement is undefined");
}
const $loop = $initStmt.parent;
if ($loop === undefined) {
throw new Error("Loop is undefined");
}
if (!($loop instanceof Loop)) {
throw new Error("$loop is not an instance of Loop");
}
if ($loop.kind !== "for") {
throw new Error("Not implemented for loops of kind " + $loop.kind);
}
const $condStmt = $loop.cond;
if ($condStmt === undefined) {
throw new Error("Not implemented when for loops do not have a condition statement");
}
const afterNode = this.nodes.get($condStmt.astId) ?? this.endNode;
this.addEdge(node, afterNode, CfgEdgeType.UNCONDITIONAL);
}
/**
* Connects a node associated with a statement that is part of a loop header and corresponds to the loop step
* @param node - Node whose type is "STEP"
*/
connectStepNode(node) {
// Get loop
const $stepStmt = node.data().nodeStmt;
if ($stepStmt === undefined) {
throw new Error("Step statement is undefined");
}
const $loop = $stepStmt.parent;
if ($loop === undefined) {
throw new Error("Loop is undefined");
}
if (!($loop instanceof Loop)) {
throw new Error("$loop is not an instance of Loop");
}
if ($loop.kind !== "for") {
throw new Error("Not implemented for loops of kind " + $loop.kind);
}
const $condStmt = $loop.cond;
if ($condStmt === undefined) {
throw new Error("Not implemented when for loops do not have a condition statement");
}
const afterNode = this.nodes.get($condStmt.astId) ?? this.endNode;
this.addEdge(node, afterNode, CfgEdgeType.UNCONDITIONAL);
}
/**
* @param node - Node whose type is "INST_LIST"
*/
connectInstListNode(node) {
const $lastStmt = node.data().getLastStmt();
if ($lastStmt === undefined) {
throw new Error("Last statement is undefined");
}
const afterNode = this.nextNodes.nextExecutedNode($lastStmt);
this.addEdge(node, afterNode, CfgEdgeType.UNCONDITIONAL);
}
/**
* @param node - Node whose type is "GOTO"
*/
connectGotoNode(node) {
const $gotoStmt = node.data().nodeStmt;
if ($gotoStmt === undefined) {
throw new Error("Goto statement is undefined");
}
const labelName = $gotoStmt.label.name;
const $labelStmt = Query.searchFromInclusive(this.jp, LabelStmt, {
decl: (decl) => decl.name == labelName,
}).first();
if ($labelStmt === undefined) {
throw new Error("Label statement is undefined");
}
const afterNode = this.nodes.get($labelStmt.astId);
if (afterNode === undefined) {
throw new Error("Node for label statement is undefined: " + $labelStmt.astId);
}
this.addEdge(node, afterNode, CfgEdgeType.UNCONDITIONAL);
}
/**
* @param node - Node whose type is "LABEL"
*/
connectLabelNode(node) {
const $labelStmt = node.data().nodeStmt;
if ($labelStmt === undefined) {
throw new Error("Label statement is undefined");
}
const afterNode = this.nextNodes.nextExecutedNode($labelStmt);
this.addEdge(node, afterNode, CfgEdgeType.UNCONDITIONAL);
}
/**
* Connects a node associated with a statement that is an instance of a "return" statement.
* @param node - Node whose type is "RETURN"
*/
connectReturnNode(node) {
this.addEdge(node, this.endNode, CfgEdgeType.UNCONDITIONAL);
}
/**
* Connects a node associated with a statement that is an instance of a "scope" statement.
* @param node - Node whose type is "SCOPE", "THEN" or "ELSE"
*/
connectScopeNode(node) {
const $scope = node.data().scope;
// Scope connects to its own first statement that will be an INST_LIST
const afterNode = this.nodes.get($scope.firstStmt.astId);
if (afterNode === undefined) {
throw new Error("Node for first statement of scope is undefined: " +
$scope.firstStmt.astId);
}
this.addEdge(node, afterNode, CfgEdgeType.UNCONDITIONAL);
}
/**
* Connects the leader statement nodes according to their type
*/
connectNodes() {
// Connect start
let startAstNode = this.jp;
if (startAstNode instanceof FunctionJp) {
startAstNode = startAstNode.body;
}
if (!(startAstNode instanceof Statement)) {
throw new Error("Not defined how to connect the Start node to an AST node of type " +
this.jp.joinPointType);
}
const afterNode = this.nodes.get(startAstNode.astId);
if (afterNode === undefined) {
throw new Error("Node for first statement of scope is undefined: " + startAstNode.astId);
}
// Add edge
this.addEdge(this.startNode, afterNode, CfgEdgeType.UNCONDITIONAL);
for (const astId of this.nodes.keys()) {
const node = this.nodes.get(astId);
if (node === undefined) {
throw new Error("Node is undefined for astId " + astId);
}
const nodeData = node.data();
const nodeStmt = nodeData.nodeStmt;
if (nodeStmt === undefined) {
throw new Error("Node statement is undefined");
}
// Only add connections for astIds of leader statements
if (nodeStmt.astId !== astId) {
continue;
}
const nodeType = nodeData.type;
if (nodeType === undefined) {
throw new Error("Node type is undefined: ");
}
switch (nodeType) {
case CfgNodeType.IF:
this.connectIfNode(node);
break;
case CfgNodeType.LOOP:
this.connectLoopNode(node);
break;
case CfgNodeType.COND:
this.connectCondNode(node);
break;
case CfgNodeType.BREAK:
this.connectBreakNode(node);
break;
case CfgNodeType.CONTINUE:
this.connectContinueNode(node);
break;
case CfgNodeType.SWITCH:
this.connectSwitchNode(node);
break;
case CfgNodeType.CASE:
this.connectCaseNode(node);
break;
case CfgNodeType.INIT:
this.connectInitNode(node);
break;
case CfgNodeType.STEP:
this.connectStepNode(node);
break;
case CfgNodeType.INST_LIST:
this.connectInstListNode(node);
break;
case CfgNodeType.GOTO:
this.connectGotoNode(node);
break;
case CfgNodeType.LABEL:
this.connectLabelNode(node);
break;
case CfgNodeType.RETURN:
this.connectReturnNode(node);
break;
case CfgNodeType.SCOPE:
case CfgNodeType.THEN:
case CfgNodeType.ELSE:
this.connectScopeNode(node);
break;
}
}
}
cleanCfg() {
// Remove temporary instructions from the code
if (!this.keepTemporaryScopeStmts) {
for (const stmtId in this.temporaryStmts) {
this.temporaryStmts[stmtId].detach();
}
}
// Remove temporary instructions from the instList nodes and this.nodes
for (const node of this.nodes.values()) {
const nodeData = node.data();
// Only inst lists need to be cleaned
if (nodeData.type !== CfgNodeType.INST_LIST) {
const tempStmts = nodeData.stmts.filter(($stmt) => this.temporaryStmts[$stmt.astId] !== undefined);
if (tempStmts.length > 0) {
console.log("Node '" +
nodeData.type.toString() +
"' has temporary stmts: " +
tempStmts.toString());
}
continue;
}
// Filter stmts that are temporary statements
if (this.keepTemporaryScopeStmts) {
continue;
}
const filteredStmts = [];
for (const $stmt of nodeData.stmts) {
// If not a temporary stmt, add to filtered list
if (this.temporaryStmts[$stmt.astId] === undefined) {
filteredStmts.push($stmt);
}
// Otherwise, remove from this.nodes
else {
this.nodes.delete($stmt.astId);
}
}
if (filteredStmts.length !== nodeData.stmts.length) {
nodeData.stmts = filteredStmts;
}
}
// Remove empty instList CFG nodes
for (const node of this.graph.nodes()) {
const nodeData = node.data();
// Only nodes that are inst lists
if (nodeData.type !== CfgNodeType.INST_LIST) {
continue;
}
// Only empty nodes
if (nodeData.stmts.length > 0) {
continue;
}
// Remove node, replacing the connections with a new connection of the same type and the incoming edge
// of the node being removed
Graphs.removeNode(this.graph, node, (incoming) => new CfgEdge(incoming.data().type));
}
// Remove label nodes
if (this.removeLabelNodes) {
for (const node of this.graph.nodes()) {
const nodeData = node.data();
// Only nodes whose type is "LABEL"
if (nodeData.type !== CfgNodeType.LABEL)
continue;
Graphs.removeNode(this.graph, node, (incoming) => new CfgEdge(incoming.data().type));
if (nodeData.nodeStmt === undefined) {
throw new Error("Node statement is undefined");
}
this.nodes.delete(nodeData.nodeStmt.astId);
}
}
// Remove goto nodes
if (this.removeGotoNodes) {
for (const node of this.graph.nodes()) {
const nodeData = node.data();
// Only nodes whose type is "GOTO"
if (nodeData.type !== CfgNodeType.GOTO)
continue;
Graphs.removeNode(this.graph, node, (incoming) => new CfgEdge(incoming.data().type));
if (nodeData.nodeStmt === undefined) {
throw new Error("Node statement is undefined");
}
this.nodes.delete(nodeData.nodeStmt.astId);
}
}
// Remove nodes that have no incoming edge and are not start
for (const node of this.graph.nodes()) {
const nodeData = node.data();
// Only nodes that are not start
if (nodeData.type === CfgNodeType.START) {
continue;
}
// Ignore nodes with incoming edges
if (node.incomers().length > 0) {
continue;
}
// Remove node
debug("[CfgBuilder] Removing statement that is not executed (e.g. is after a return): " +
nodeData.stmts.toString());
// Removes nodes. As there are no incoming edges, the edge handler is a dummy as it is not called
Graphs.removeNode(this.graph, node, () => new CfgEdge(CfgEdgeType.UNCONDITIONAL));
}
}
/**
* Returns the node corresponding to this statement, or creates a new one if one does not exist yet.
*/
getOrAddNode($stmt, create = false, forceNodeType) {
let node = this.nodes.get($stmt.astId);
// If there is not yet a node for this statement, create
if (node === undefined && create) {
const nodeType = forceNodeType ?? CfgUtils.getNodeType($stmt);
if (nodeType === undefined) {
throw new Error("Node type is undefined for statement at line " + $stmt.line);
}
const nodeId = this.deterministicIds ? this.nextId() : undefined;
node = Graphs.addNode(this.graph, this.dataFactory.newData(nodeType, $stmt, nodeId, this.splitInstList));
// Associate all statements of graph node
for (const $nodeStmt of node.data().stmts) {
// Check if it has not been already added
if (this.nodes.get($nodeStmt.astId) !== undefined) {
throw new Error("Adding mapping twice for statement " +
$nodeStmt.astId +
"@" +
$nodeStmt.location);
}
this.nodes.set($nodeStmt.astId, node);
}
}
else {
throw new Error("No node for statement at line " + $stmt.line);
}
return node;
}
}
//# sourceMappingURL=CfgBuilder.js.map