@eagleoutice/flowr-dev
Version:
Static Dataflow Analyzer and Program Slicer for the R Programming Language
196 lines • 10.5 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getAllFunctionCallTargetsForSlice = getAllFunctionCallTargetsForSlice;
exports.sliceForCall = sliceForCall;
exports.findEnclosingFunctionDefinition = findEnclosingFunctionDefinition;
exports.sliceReachesFunctionInterface = sliceReachesFunctionInterface;
exports.includeCalleesOfDefinition = includeCalleesOfDefinition;
exports.handleReturns = handleReturns;
const assert_1 = require("../../util/assert");
const fingerprint_1 = require("./fingerprint");
const linker_1 = require("../../dataflow/internal/linker");
const graph_1 = require("../../dataflow/graph/graph");
const resolve_helper_1 = require("../../dataflow/environments/resolve-helper");
const edge_1 = require("../../dataflow/graph/edge");
const node_id_1 = require("../../r-bridge/lang-4.x/ast/model/processing/node-id");
const identifier_1 = require("../../dataflow/environments/identifier");
const built_in_function_definition_1 = require("../../dataflow/internal/process/functions/call/built-in/built-in-function-definition");
const static_slicer_1 = require("./static-slicer");
const type_1 = require("../../r-bridge/lang-4.x/ast/model/type");
const model_1 = require("../../r-bridge/lang-4.x/ast/model/model");
const vertex_1 = require("../../dataflow/graph/vertex");
/**
* Returns the function call targets (definitions) by the given caller
*/
function getAllFunctionCallTargetsForSlice(dataflowGraph, callerInfo, baseEnvironment, queue, ctx) {
// bind with call-local environments during slicing
const outgoingEdges = dataflowGraph.get(callerInfo.id, true);
(0, assert_1.guard)(outgoingEdges !== undefined, () => `outgoing edges of id: ${callerInfo.id} must be in graph but can not be found, keep in slice to be sure`);
// lift baseEnv on the same level
const activeEnvironment = (0, built_in_function_definition_1.retrieveActiveEnvironment)(callerInfo.environment, baseEnvironment, ctx);
const name = callerInfo.name;
(0, assert_1.guard)(name !== undefined, () => `name of id: ${callerInfo.id} can not be found in id map`);
const functionCallDefs = resolve_helper_1.Resolve.byName(name, activeEnvironment)?.filter(d => !node_id_1.NodeId.isBuiltIn(d.definedAt))?.map(d => d.nodeId) ?? [];
for (const [target, outgoingEdge] of outgoingEdges[1].entries()) {
if (edge_1.DfEdge.includesType(outgoingEdge, edge_1.EdgeType.Calls)) {
functionCallDefs.push(target);
}
}
const functionCallTargets = queue.memoizeCallTargets(functionCallDefs.join(';'), () => (0, linker_1.getAllLinkedFunctionDefinitions)(new Set(functionCallDefs), dataflowGraph)[0]);
return [functionCallTargets, activeEnvironment];
}
function includeArgumentFunctionCallClosure(arg, activeEnvironment, activeEnvironmentFingerprint, queue, dataflowGraph) {
const valueRoot = graph_1.FunctionArgument.getReference(arg);
if (!valueRoot) {
return;
}
const callTargets = queue.memoizeCallTargets(valueRoot, () => (0, linker_1.getAllLinkedFunctionDefinitions)(new Set([valueRoot]), dataflowGraph)[0]);
linkCallTargets(false, callTargets, activeEnvironment, activeEnvironmentFingerprint, queue);
}
function linkCallTargets(onlyForSideEffects, functionCallTargets, activeEnvironment, activeEnvironmentFingerprint, queue) {
for (const functionCallTarget of functionCallTargets) {
for (const exitPoint of functionCallTarget.exitPoints) {
queue.add(exitPoint.nodeId, activeEnvironment, activeEnvironmentFingerprint, onlyForSideEffects);
}
// handle open reads
for (const openIn of functionCallTarget.subflow.in) {
// resolve them in the active env
if (openIn.name) {
const resolved = resolve_helper_1.Resolve.byName(openIn.name, activeEnvironment);
for (const res of resolved ?? []) {
(0, static_slicer_1.updatePotentialAddition)(queue, functionCallTarget.id, res.nodeId, activeEnvironment, activeEnvironmentFingerprint);
}
}
}
}
}
/** returns the new threshold hit count */
function sliceForCall(current, callerInfo, { graph }, queue, ctx) {
const [functionCallTargets, activeEnvironment] = getAllFunctionCallTargetsForSlice(graph, callerInfo, current.baseEnvironment, queue, ctx);
if (functionCallTargets.size === 0) {
/*
* if we do not have any call to resolve this function, we have to assume that every function passed is actually called!
* hence, we add a new flag and add all argument values to the queue causing directly
*/
const argEnvironmentFingerprint = (0, fingerprint_1.envFingerprint)(activeEnvironment);
for (const arg of callerInfo.args) {
includeArgumentFunctionCallClosure(arg, activeEnvironment, argEnvironmentFingerprint, queue, graph);
}
return;
}
const activeEnvironmentFingerprint = (0, fingerprint_1.envFingerprint)(activeEnvironment);
linkCallTargets(current.onlyForSideEffects, functionCallTargets, activeEnvironment, activeEnvironmentFingerprint, queue);
}
/**
* Finds the nearest enclosing function-definition node for the given id by walking up the AST parent chain.
* Used by `includeCallees` to detect the function-definition boundary a node sits inside, as backward slicing
* does not otherwise visit the function-definition vertex itself (nothing within the body links to it).
*/
function findEnclosingFunctionDefinition(id, idMap) {
let node = idMap.get(id);
while (node !== undefined) {
if (node.type === type_1.RType.FunctionDefinition) {
return node.info.id;
}
node = node.info.parent !== undefined ? idMap.get(node.info.parent) : undefined;
}
return undefined;
}
/**
* For `includeCallees`: decides whether the current slice of a function definition's body actually depends on
* the function's interface, i.e., whether the callers can influence the sliced result at all. This is the case iff
* the slice reaches one of the definition's parameters, or it reads a free reference captured from the enclosing
* scope. If the sliced body is self-contained (only locally-defined variables, no parameter and no captured
* variable), the callers are irrelevant and the boundary must not be crossed.
*/
function sliceReachesFunctionInterface(fnDefId, graph, queue, idMap, ctx) {
const vertex = graph.getVertex(fnDefId);
if (vertex === undefined || !vertex_1.FunctionDefinitionVertex.is(vertex)) {
return false;
}
// (a) the slice reaches a parameter of this definition
for (const paramId of Object.keys(vertex.params)) {
if (queue.hasId(node_id_1.NodeId.normalize(paramId))) {
return true;
}
}
// (b) a sliced body node captures a variable from the enclosing scope: it links (via `defined-by-on-call`, the
// closure/argument binding resolved at the call site) to a non-builtin definition that lives outside this body.
const fnNode = idMap.get(fnDefId);
const bodyIds = fnNode ? new Set(model_1.RNode.collectAllIds(fnNode)) : new Set();
for (const bodyId of bodyIds) {
if (!queue.hasId(bodyId)) {
continue;
}
const outgoing = graph.outgoingEdges(bodyId);
if (outgoing === undefined) {
continue;
}
for (const [target, edge] of outgoing) {
if (edge_1.DfEdge.includesType(edge, edge_1.EdgeType.DefinedByOnCall) && !node_id_1.NodeId.isBuiltIn(target) && !bodyIds.has(target)) {
return true;
}
}
}
// (c) fallback for captures that could not be resolved at definition time: an open in-reference that is in the
// slice and resolves to a non-builtin definition in the enclosing scope.
const definitionEnvironment = vertex.environment ?? ctx.env.makeCleanEnv();
for (const open of vertex.subflow.in) {
if (open.name === undefined || !queue.hasId(open.nodeId)) {
continue;
}
const resolved = resolve_helper_1.Resolve.byNameAndType(open.name, definitionEnvironment, open.type ?? identifier_1.ReferenceType.Unknown);
if (resolved?.some(d => !node_id_1.NodeId.isBuiltIn(d.nodeId))) {
return true;
}
}
return false;
}
const CalleeBoundaryEdges = edge_1.EdgeType.DefinedBy | edge_1.EdgeType.Calls;
/**
* For `includeCallees`: given the id of a function-definition vertex, enqueues the vertex that binds/defines
* the function (e.g. `f <- function...`, via the `defined-by` edge) as well as all of its call sites (via
* `calls` edges). Call site arguments are picked up automatically once the call vertex is processed normally,
* as `argument` edges are always traversed.
* This is the reverse of what {@link sliceForCall} does for call -\> definition linking.
*/
function includeCalleesOfDefinition(fnDefId, graph, queue, baseEnvironment, baseEnvFingerprint) {
const ingoing = graph.ingoingEdges(fnDefId);
if (ingoing === undefined) {
return;
}
for (const [source, edge] of ingoing) {
if (edge_1.DfEdge.includesType(edge, CalleeBoundaryEdges)) {
queue.add(source, baseEnvironment, baseEnvFingerprint, false);
}
}
}
const PotentialFollowOnReturn = edge_1.EdgeType.DefinesOnCall | edge_1.EdgeType.DefinedByOnCall | edge_1.EdgeType.Argument;
/** Returns true if we found at least one return edge */
function handleReturns(from, queue, currentEdges, baseEnvFingerprint, baseEnvironment) {
let returns = false;
for (const edge of currentEdges.values()) {
if (edge_1.DfEdge.includesType(edge, edge_1.EdgeType.Returns)) {
returns = true;
break;
}
}
if (!returns) {
return false;
}
for (const [target, edge] of currentEdges) {
if (edge_1.DfEdge.includesType(edge, edge_1.EdgeType.Returns)) {
queue.add(target, baseEnvironment, baseEnvFingerprint, false);
}
}
for (const [target, edge] of currentEdges) {
if (edge_1.DfEdge.includesType(edge, edge_1.EdgeType.Reads)) {
queue.add(target, baseEnvironment, baseEnvFingerprint, false);
}
else if (edge_1.DfEdge.includesType(edge, PotentialFollowOnReturn)) {
(0, static_slicer_1.updatePotentialAddition)(queue, from, target, baseEnvironment, baseEnvFingerprint);
}
}
return true;
}
//# sourceMappingURL=slice-call.js.map