@eagleoutice/flowr-dev
Version:
Static Dataflow Analyzer and Program Slicer for the R Programming Language
245 lines • 12.2 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.slicerLogger = void 0;
exports.staticSlice = staticSlice;
exports.staticDice = staticDice;
exports.updatePotentialAddition = updatePotentialAddition;
const assert_1 = require("../../util/assert");
const log_1 = require("../../util/log");
const visiting_queue_1 = require("./visiting-queue");
const slice_call_1 = require("./slice-call");
const node_id_1 = require("../../r-bridge/lang-4.x/ast/model/processing/node-id");
const vertex_1 = require("../../dataflow/graph/vertex");
const edge_1 = require("../../dataflow/graph/edge");
const df_helper_1 = require("../../dataflow/graph/df-helper");
const slice_direction_1 = require("../../util/slice-direction");
const model_1 = require("../../r-bridge/lang-4.x/ast/model/model");
const graph_1 = require("../../dataflow/graph/graph");
exports.slicerLogger = log_1.log.getSubLogger({ name: 'slicer' });
/**
* Computes the node ids to include in a static slice, starting from the given seed ids.
* The returned ids can be used with {@link reconstructToCode} to reproduce executable R code.
*/
function staticSlice(options) {
const { ctx, info, ids, cache, sliceGraph } = options;
const { idMap } = options.ast;
const direction = options.direction ?? slice_direction_1.SliceDirection.Backward;
const threshold = options.threshold ?? 75;
(0, assert_1.guard)(ids.length > 0, 'must have at least one seed id to calculate slice');
// includeCallees only makes sense on the original (non-reduced) graph, backward
const trackCallees = (options.includeCallees ?? false) && direction === slice_direction_1.SliceDirection.Backward && sliceGraph === undefined;
// enclosing function definitions whose callees still need to be considered, mapped to the env to enqueue them in
const pendingCalleeBoundaries = new Map();
const resolvedCalleeBoundaries = new Set();
let graph;
if (sliceGraph !== undefined) {
graph = sliceGraph;
}
else {
graph = info.graph;
if (direction === slice_direction_1.SliceDirection.Forward) {
graph = df_helper_1.Dataflow.invertGraph(graph, ctx.env.makeCleanEnv());
}
}
const queue = new visiting_queue_1.VisitingQueue(threshold, cache, id => graph.hasVertex(id), ctx.gas.scope(options.gas));
let minNesting = Number.MAX_SAFE_INTEGER;
const sliceSeedIds = new Set();
// every node ships the call environment which registers the calling environment
{
const emptyEnv = ctx.env.makeCleanEnv();
const basePrint = ctx.env.getCleanEnvFingerprint();
for (const startId of ids) {
queue.add(startId, emptyEnv, basePrint, false);
// retrieve the minimum nesting of all nodes to only add control dependencies if they are "part" of the current execution
minNesting = Math.min(minNesting, idMap.get(startId)?.info.nest ?? minNesting);
sliceSeedIds.add(startId);
}
/* additionally,
* include all the implicit side effects that we have to consider as we are unable to narrow them down
*/
for (const id of graph.unknownSideEffects) {
if (typeof id !== 'object') {
/* otherwise, their target is just missing */
queue.add(id, emptyEnv, basePrint, true);
}
}
}
do {
while (queue.nonEmpty()) {
processNode();
}
// the queue drained: only now do we know the full body slice, so decide per boundary whether the callers
// can actually influence the result (i.e., the slice reaches a parameter or a captured scope variable).
// resolving a boundary may enqueue new nodes, hence the surrounding do-while re-enters the traversal.
resolveCalleeBoundaries();
} while (queue.nonEmpty());
function processNode() {
const current = queue.next();
const { baseEnvironment, id, onlyForSideEffects, envFingerprint: baseEnvFingerprint } = current;
const currentInfo = graph.get(id, true);
if (currentInfo === undefined) {
exports.slicerLogger.warn(`id: ${id} must be in graph but can not be found, keep in slice to be sure`);
return;
}
const [currentVertex, currentEdges] = currentInfo;
// includeCallees: note the enclosing function definition (if any) so its callees can be considered once the body slice is complete
if (trackCallees) {
const enclosingFnDef = (0, slice_call_1.findEnclosingFunctionDefinition)(id, idMap);
if (enclosingFnDef !== undefined && !resolvedCalleeBoundaries.has(enclosingFnDef) && !pendingCalleeBoundaries.has(enclosingFnDef)) {
pendingCalleeBoundaries.set(enclosingFnDef, [baseEnvironment, baseEnvFingerprint]);
}
}
// we only add control dependencies iff 1) we are in different function call or 2) they have, at least, the same nesting as the slicing seed
if (currentVertex.cds && currentVertex.cds.length > 0) {
const topLevel = graph.isRoot(id) || sliceSeedIds.has(id);
for (const cd of currentVertex.cds.filter(({ id }) => !queue.hasId(id))) {
if (!topLevel || (idMap.get(cd.id)?.info.nest ?? 0) >= minNesting) {
queue.add(cd.id, baseEnvironment, baseEnvFingerprint, false);
}
}
}
if (!onlyForSideEffects) {
if (vertex_1.FunctionCallVertex.is(currentVertex) && !currentVertex.onlyBuiltin) {
(0, slice_call_1.sliceForCall)(current, currentVertex, info, queue, ctx);
}
const ret = (0, slice_call_1.handleReturns)(id, queue, currentEdges, baseEnvFingerprint, baseEnvironment);
if (ret) {
return;
}
}
for (const [target, e] of currentEdges) {
const t = (0, edge_1.shouldTraverseEdge)(e);
switch (t) {
case 0 /* TraverseEdge.Never */:
continue;
case 3 /* TraverseEdge.Always */:
queue.add(target, baseEnvironment, baseEnvFingerprint, false);
continue;
case 2 /* TraverseEdge.OnlyIfBoth */:
updatePotentialAddition(queue, id, target, baseEnvironment, baseEnvFingerprint);
continue;
case 1 /* TraverseEdge.SideEffect */:
queue.add(target, baseEnvironment, baseEnvFingerprint, true);
continue;
default:
(0, assert_1.assertUnreachable)(t);
}
}
}
function resolveCalleeBoundaries() {
if (pendingCalleeBoundaries.size === 0) {
return;
}
const boundaries = [...pendingCalleeBoundaries];
pendingCalleeBoundaries.clear();
for (const [fnDefId, [env, fingerprint]] of boundaries) {
resolvedCalleeBoundaries.add(fnDefId);
// only continue past the boundary if the callers can actually influence the sliced result
if ((0, slice_call_1.sliceReachesFunctionInterface)(fnDefId, info.graph, queue, idMap, ctx)) {
(0, slice_call_1.includeCalleesOfDefinition)(fnDefId, info.graph, queue, env, fingerprint);
}
}
}
const status = queue.status();
const result = ctx.config.solver.slicer?.autoExtend ? extendSlices(status.result, idMap) : status.result;
return { ...status, slicedFor: ids, result, freeNames: freeNamesOf(result, info.graph) };
}
/**
* The names the slice reads without defining them: a use whose definitions all stayed outside meets that name
* undefined, and so does one that reads nothing at all (a name the program never defines). Reading a built-in
* is no such case, as those are there whatever the slice contains.
*/
function freeNamesOf(slice, graph) {
const free = new Set();
for (const id of slice) {
if (!vertex_1.UseVertex.is(graph.getVertex(id))) {
continue;
}
let defined = false;
for (const [target, edge] of graph.outgoingEdges(id) ?? graph_1.NoEdges) {
if (edge_1.DfEdge.includesType(edge, edge_1.EdgeType.Reads) && (slice.has(target) || node_id_1.NodeId.isBuiltIn(target))) {
defined = true;
break;
}
}
const name = defined ? undefined : (0, node_id_1.recoverName)(id, graph.idMap);
if (name !== undefined) {
free.add(name);
}
}
return [...free].sort();
}
/**
* Computes a program dice: only those nodes reachable forward from `startIds` that are also in the backward slice of `endIds`.
* This effectively selects all paths from the given start nodes that lead to the given end nodes.
*
* For performance, the backward slice is computed first (typically the smaller set), then the graph is
* reduced to that set and its edges are inverted in a single pass via {@link Dataflow.reduceAndInvertGraph}.
* The forward traversal then runs only within that subgraph, avoiding nodes that cannot contribute to the dice.
*/
function staticDice(ctx, info, ast, startIds, endIds, threshold = 75, includeCallees = false, gas) {
(0, assert_1.guard)(startIds.length > 0 && endIds.length > 0, 'must have at least one start and one end id for dicing');
const backward = staticSlice({ ctx, info, ast, ids: endIds, direction: slice_direction_1.SliceDirection.Backward, threshold, includeCallees, gas });
// reduce to backward result and invert edges in one pass; original info kept for sliceForCall
const invertedReduced = df_helper_1.Dataflow.reduceAndInvertGraph(info.graph, backward.result, ctx.env.makeCleanEnv());
const forward = staticSlice({ ctx, info, ast, ids: startIds, direction: slice_direction_1.SliceDirection.Backward, threshold, sliceGraph: invertedReduced, gas });
// explicit intersection handles seed nodes that landed outside the reduced graph
const result = new Set();
for (const id of forward.result) {
if (backward.result.has(id)) {
result.add(id);
}
}
return {
timesHitThreshold: forward.timesHitThreshold + backward.timesHitThreshold,
result,
slicedFor: [...startIds, ...endIds],
...(forward.stoppedEarly || backward.stoppedEarly ? {
stoppedEarly: true,
progress: {
visited: (forward.progress?.visited ?? 0) + (backward.progress?.visited ?? 0),
frontier: (forward.progress?.frontier ?? 0) + (backward.progress?.frontier ?? 0)
}
} : {})
};
}
function extendSlices(results, ast) {
const res = new Set();
for (const id of results) {
res.add(id);
let parent = ast.get(id);
while (parent && parent.info.role !== "root" /* RoleInParent.Root */ && parent.info.role !== "el-c" /* RoleInParent.ExpressionListChild */) {
parent = parent.info.parent ? ast.get(parent.info.parent) : undefined;
}
if (!parent) {
continue; // no parent, no need to extend
}
for (const id of model_1.RNode.collectAllIds(parent)) {
res.add(id);
}
}
return res;
}
/**
* Updates the potential addition for the given target node in the visiting queue.
* This describes vertices that might be added *if* another path reaches them.
*/
function updatePotentialAddition(queue, id, target, baseEnvironment, envFingerprint) {
const n = queue.potentialAdditions.get(target);
if (n) {
const [addedBy, { baseEnvironment, onlyForSideEffects }] = n;
if (addedBy !== id) {
queue.add(target, baseEnvironment, envFingerprint, onlyForSideEffects);
queue.potentialAdditions.delete(target);
}
}
else {
queue.potentialAdditions.set(target, [id, {
id: target,
baseEnvironment,
envFingerprint,
onlyForSideEffects: false
}]);
}
}
//# sourceMappingURL=static-slicer.js.map