UNPKG

@eagleoutice/flowr-dev

Version:

Static Dataflow Analyzer and Program Slicer for the R Programming Language

245 lines 12.2 kB
"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