@eagleoutice/flowr-dev
Version:
Static Dataflow Analyzer and Program Slicer for the R Programming Language
351 lines • 13.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CallGraph = void 0;
const graph_1 = require("./graph");
const vertex_1 = require("./vertex");
const node_id_1 = require("../../r-bridge/lang-4.x/ast/model/processing/node-id");
const linker_1 = require("../internal/linker");
const edge_1 = require("./edge");
const built_in_proc_name_1 = require("../environments/built-in-proc-name");
const defaultmap_1 = require("../../util/collections/defaultmap");
const graph_helper_1 = require("./graph-helper");
const r_function_definition_1 = require("../../r-bridge/lang-4.x/ast/model/nodes/r-function-definition");
const r_binary_op_1 = require("../../r-bridge/lang-4.x/ast/model/nodes/r-binary-op");
const model_1 = require("../../r-bridge/lang-4.x/ast/model/model");
const type_1 = require("../../r-bridge/lang-4.x/ast/model/type");
const identifier_1 = require("../environments/identifier");
function processCds(vtx, graph, result, state) {
for (const tar of vtx.cds ?? []) {
const targetVtx = graph.getVertex(tar.id);
if (targetVtx) {
processUnknown(targetVtx, undefined, graph, result, state);
}
}
}
const UntargetedCallFollow = edge_1.EdgeType.Reads | edge_1.EdgeType.DefinedByOnCall | edge_1.EdgeType.DefinedBy | edge_1.EdgeType.Returns;
const UntargetedCallAvoid = edge_1.EdgeType.NonStandardEvaluation | edge_1.EdgeType.Argument;
/**
* This tracks the known symbol origins for a function call for which we know that flowr found no targets!
*/
function fallbackUntargetedCall(vtx, graph) {
// we track all aliases to their roots here, we know there is no known call target
const collected = new Set();
const visited = new Set();
const toVisit = [vtx.id];
while (toVisit.length > 0) {
const currentId = toVisit.pop();
if (visited.has(currentId)) {
continue;
}
visited.add(currentId);
const currentVtx = graph.getVertex(currentId);
if (!currentVtx) {
continue;
}
let addedNew = false;
for (const [tar, e] of graph.outgoingEdges(currentId) ?? graph_1.NoEdges) {
if (edge_1.DfEdge.includesType(e, UntargetedCallFollow) && edge_1.DfEdge.doesNotIncludeType(e, UntargetedCallAvoid)) {
addedNew = true;
toVisit.push(tar);
}
}
// we have reached our end(s)
if (!addedNew && currentId !== vtx.id) {
collected.add(currentId);
}
}
return collected;
}
function processCall(vtx, from, graph, result, state) {
const vid = vtx.id;
if (from) {
result.addEdge(from, vid, edge_1.EdgeType.Calls);
}
if (state.visited.has(vid)) {
return;
}
state.visited.add(vid);
result.addVertex(vtx, undefined, true);
processCds(vtx, graph, result, state);
// for each call, resolve the targets
const tars = (0, linker_1.getAllFunctionCallTargets)(vid, graph, vtx.environment);
let addedTarget = false;
let addedBiTarget = false;
for (const tar of tars) {
if (node_id_1.NodeId.isBuiltIn(tar)) {
result.addEdge(vid, tar, edge_1.EdgeType.Calls);
addedTarget = true;
addedBiTarget = true;
continue;
}
const targetVtx = graph.getVertex(tar);
if (!vertex_1.FunctionDefinitionVertex.is(targetVtx)) {
continue;
}
addedTarget = true;
processFunctionDefinition(targetVtx, vid, graph, result, state);
}
if (!addedBiTarget && vtx.origin !== 'unnamed') {
for (const origs of vtx.origin) {
if (origs.startsWith('builtin:')) {
addedTarget = true;
result.addEdge(vid, node_id_1.NodeId.toBuiltIn(origs.slice('builtin:'.length)), edge_1.EdgeType.Calls);
}
}
}
if (!addedTarget) {
const origs = fallbackUntargetedCall(vtx, graph);
for (const ori of origs) {
const oriVtx = graph.getVertex(ori);
if (!oriVtx) {
continue;
}
result.addEdge(vid, ori, edge_1.EdgeType.Calls);
const name = graph.idMap?.get(ori);
if (name?.lexeme && vertex_1.UseVertex.is(oriVtx)) {
result.addVertex({
...oriVtx,
tag: vertex_1.VertexType.FunctionCall,
name: name.lexeme,
onlyBuiltin: false,
origin: [built_in_proc_name_1.BuiltInProcName.Function],
args: []
}, oriVtx.environment);
}
}
}
// handle arguments, traversing the 'reads' and the 'returns' edges
for (const [tar, e] of graph.outgoingEdges(vtx.id) ?? graph_1.NoEdges) {
if (edge_1.DfEdge.doesNotIncludeType(e, edge_1.EdgeType.Reads | edge_1.EdgeType.Returns | edge_1.EdgeType.Argument)) {
continue;
}
const tVtx = graph.getVertex(tar);
if (!tVtx) {
continue;
}
processUnknown(tVtx, vtx.id, graph, result, state);
}
}
function processUnknown(vtx, from, graph, result, state) {
switch (vtx.tag) {
case vertex_1.VertexType.FunctionCall:
processCall(vtx, from, graph, result, state);
return;
case vertex_1.VertexType.FunctionDefinition:
if (from) {
result.addEdge(from, node_id_1.NodeId.toBuiltIn('function'), edge_1.EdgeType.Calls);
}
return;
default:
return;
}
}
function processFunctionDefinition(vtx, from, graph, result, state) {
if (from) {
result.addEdge(from, vtx.id, edge_1.EdgeType.Calls);
}
if (state.visited.has(vtx.id)) {
return;
}
state.visited.add(vtx.id);
result.addVertex(vtx, undefined, true);
processCds(vtx, graph, result, state);
const exits = new Set(vtx.exitPoints);
state.potentials.push([vtx.id, vtx.subflow.graph.difference(exits)]);
for (const { nodeId } of exits) {
const v = graph.getVertex(nodeId);
if (v) {
processUnknown(v, vtx.id, graph, result, state);
}
}
}
/** Follows the `Calls` edges from `seeds`, collecting everything they reach in `reached`. */
function followCalls(graph, seeds, reached) {
const toVisit = Array.from(seeds);
while (toVisit.length > 0) {
const current = toVisit.pop();
if (reached.has(current)) {
continue;
}
reached.add(current);
for (const [target, edge] of graph.outgoingEdges(current) ?? graph_1.NoEdges) {
if (edge_1.DfEdge.includesType(edge, edge_1.EdgeType.Calls)) {
toVisit.push(target);
}
}
}
}
/** The names the reached calls go by, which are the generics a method of the program may be dispatched to for. */
function reachedCallNames(graph, reached) {
const names = new Set();
for (const id of reached) {
const vertex = graph.getVertex(id);
if (vertex_1.FunctionCallVertex.is(vertex)) {
names.add(identifier_1.Identifier.getName(vertex.name));
}
}
return names;
}
/** Whether `name` is a `<generic>.<class>` of a generic that is called, as `print.foo` is when `print` is. */
function mayBeDispatchedTo(name, generics) {
for (let dot = name.indexOf('.'); dot > 0; dot = name.indexOf('.', dot + 1)) {
if (generics.has(name.slice(0, dot))) {
return true;
}
}
return false;
}
/**
* The unreached function definitions that may still run: one the code hands to another call (a callback, an
* S4 method, a shiny handler), and one bound to a `<generic>.<class>` name of a generic the program calls.
*/
function mayRunAnyway(graph, reached) {
const idMap = graph.idMap;
if (idMap === undefined) {
return [];
}
const seeds = [];
let generics;
for (const [id, vertex] of graph.vertices(true)) {
if (!vertex_1.FunctionDefinitionVertex.is(vertex) || reached.has(id)) {
continue;
}
const node = idMap.get(id);
if (node === undefined) {
continue;
}
const parent = model_1.RNode.directParent(node, idMap);
if (parent?.type === type_1.RType.Argument) {
seeds.push(id);
continue;
}
const bound = r_binary_op_1.RBinaryOp.is(parent) ? model_1.RNode.lexeme(parent.lhs) : undefined;
generics ??= reachedCallNames(graph, reached);
if (bound !== undefined && mayBeDispatchedTo(bound, generics)) {
seeds.push(id);
}
}
return seeds;
}
/**
* Helper object for call-graphs, you can compute new call graphs based on {@link CallGraph.compute}.
* @see {@link Dataflow}
* @see {@link CallGraph}
*/
exports.CallGraph = {
...graph_helper_1.GraphHelper,
name: 'CallGraph',
/**
* Extracts the sub call graph from the given call graph, starting from the given entry points.
*/
computeSubCallGraph(graph, entryPoints) {
const result = new graph_1.DataflowGraph(graph.idMap);
const toVisit = Array.from(entryPoints);
const visited = new Set();
while (toVisit.length > 0) {
const currentId = toVisit.pop();
if (visited.has(currentId)) {
continue;
}
visited.add(currentId);
const currentVtx = graph.getVertex(currentId);
if (!currentVtx) {
continue;
}
result.addVertex(currentVtx, undefined, true);
for (const [tar, e] of graph.outgoingEdges(currentId) ?? graph_1.NoEdges) {
if (edge_1.DfEdge.includesType(e, edge_1.EdgeType.Calls)) {
result.addEdge(currentId, tar, edge_1.EdgeType.Calls);
toVisit.push(tar);
}
}
}
return result;
},
/** The calls the program makes on its own: those outside of any function definition. */
entryPoints(graph) {
const entries = new Set();
const idMap = graph.idMap;
if (idMap === undefined) {
return entries;
}
for (const [id, vertex] of graph.vertices(true)) {
const node = vertex_1.FunctionCallVertex.is(vertex) ? idMap.get(id) : undefined;
if (node !== undefined && r_function_definition_1.RFunctionDefinition.wrappingFunctionDefinition(node, idMap) === undefined) {
entries.add(id);
}
}
return entries;
},
/**
* The calls no execution starting at the top level reaches: those in a function nothing (transitively) calls.
* What {@link mayRunAnyway} may still run is left out, so a call reported here really does not run.
*/
unreachableCalls(graph) {
const reached = new Set();
followCalls(graph, exports.CallGraph.entryPoints(graph), reached);
for (let seeds = mayRunAnyway(graph, reached); seeds.length > 0; seeds = mayRunAnyway(graph, reached)) {
followCalls(graph, seeds, reached);
}
return graph.vertices(true)
.filter(([id, vertex]) => vertex_1.FunctionCallVertex.is(vertex) && !reached.has(id))
.map(([id]) => id)
.toArray();
},
/**
* Reduces the call graph by dropping all transitive edges.
*/
dropTransitiveEdges(graph) {
const newCg = new graph_1.DataflowGraph(graph.idMap);
newCg.mergeVertices(graph);
const knownReachability = new defaultmap_1.DefaultMap(() => new Set());
// heuristically sort by dif in ids
const es = Array.from(graph.edges(), ([e, ts]) => ts.entries().map(([t, { types }]) => [e, t, types]).toArray()).flat()
.sort((a, b) => String(a[0]).localeCompare(String(a[1])) - String(b[0]).localeCompare(String(b[1])));
for (const [from, to, types] of es) {
if (!exports.CallGraph.reaches(from, to, newCg, knownReachability)) {
newCg.addEdge(from, to, types);
}
}
return newCg;
},
/**
* Computes the call graph from the given dataflow graph.
* @see {@link CallGraph} - for details
* @see {@link CallGraph.computeSubCallGraph} - to extract sub call graphs
* @see {@link CallGraph.dropTransitiveEdges} - to reduce the call graph by dropping transitive edges
*/
compute(graph) {
const result = new graph_1.DataflowGraph(graph.idMap);
const state = {
visited: new Set(),
potentials: []
};
for (const [, vert] of graph.vertices(false)) {
if (vertex_1.FunctionCallVertex.is(vert)) {
processCall(vert, undefined, graph, result, state);
}
else if (vertex_1.FunctionDefinitionVertex.is(vert)) {
processFunctionDefinition(vert, undefined, graph, result, state);
}
}
for (const [from, tos] of state.potentials) {
for (const to of tos) {
if (!result.hasVertex(to)) {
const v = graph.getVertex(to);
if (v) {
processUnknown(v, from, graph, result, state);
if (vertex_1.FunctionDefinitionVertex.is(v)) {
processFunctionDefinition(v, from, graph, result, state);
}
}
}
else {
result.addEdge(from, to, edge_1.EdgeType.Calls);
}
}
}
return result;
}
};
//# sourceMappingURL=call-graph.js.map