UNPKG

@eagleoutice/flowr-dev

Version:

Static Dataflow Analyzer and Program Slicer for the R Programming Language

644 lines 25.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.DataflowGraph = exports.UnknownSideEffect = exports.NoEdges = exports.FunctionArgument = void 0; const assert_1 = require("../../util/assert"); const edge_1 = require("./edge"); const vertex_1 = require("./vertex"); const arrays_1 = require("../../util/collections/arrays"); const r_function_call_1 = require("../../r-bridge/lang-4.x/ast/model/nodes/r-function-call"); const node_id_1 = require("../../r-bridge/lang-4.x/ast/model/processing/node-id"); const environment_1 = require("../environments/environment"); const clone_1 = require("../environments/clone"); const vertex_2 = require("./vertex"); /** * Helper functions to work with {@link FunctionArgument}s. * @see {@link EmptyArgument} - the marker for empty arguments */ exports.FunctionArgument = { name: 'FunctionArgument', /** * Checks whether the given argument is a positional argument. * @example * ```r * foo(b=3, 2) # the second argument is positional * ``` */ isPositional(arg) { return arg !== r_function_call_1.EmptyArgument && arg.name === undefined; }, /** * Checks whether the given argument is a named argument. * @example * ```r * foo(b=3, 2) # the first argument is named * ``` * @see {@link isPositional} * @see {@link isEmpty} * @see {@link hasName} */ isNamed(arg) { return arg !== r_function_call_1.EmptyArgument && arg.name !== undefined; }, /** * Checks whether the given argument is an unnamed argument (either positional or empty). * @example * ```r * foo(, 2) # the first argument is unnamed (empty) * foo(3, 2) # both arguments are unnamed (positional) * ``` * @see {@link isNamed} */ isUnnamed(arg) { return arg === r_function_call_1.EmptyArgument || arg.name === undefined; }, /** * Checks whether the given argument is an empty argument. * @example * ```r * foo(, 2) # the first argument is empty * ``` * @see {@link isNotEmpty} */ isEmpty(arg) { return arg === r_function_call_1.EmptyArgument; }, /** * Checks whether the given argument is not an empty argument. * @see {@link FunctionArgument.isEmpty} */ isNotEmpty(arg) { return arg !== r_function_call_1.EmptyArgument; }, /** * Returns the id of a non-empty argument. * @example * ```r * foo(a=3, 2) # returns the node id of either `a` or `2` * ``` * @see {@link FunctionArgument.getReference} * @see {@link FunctionArgument.getName} */ getId(arg) { if (arg !== r_function_call_1.EmptyArgument) { return arg?.nodeId; } return undefined; }, /** * Returns the name of a named argument. * @example * ```r * foo(a = 3, 2) # returns 'a' or undefined * ``` * @see {@link FunctionArgument.getId} */ getName(arg) { return exports.FunctionArgument.isNamed(arg) ? arg.name : undefined; }, /** * Returns the reference of a non-empty argument. * @example * ```r * foo(a=3, 2) # returns the node id of either `3` or `2`, but skips a * ``` * @see {@link FunctionArgument.getId} */ getReference(arg) { if (arg === r_function_call_1.EmptyArgument) { return undefined; } else if (arg.name === undefined) { return arg.nodeId; } return arg.valueId; }, /** * Checks whether the given argument is a named argument with the specified name. * Please note that this only checks whether the name is exactly identical and not whether * R's argument matching resolves to the correct argument. * For this, please refer to the {@link pMatch} function! * @see {@link isNamed} */ hasName(arg, name) { return exports.FunctionArgument.isNamed(arg) && arg.name === name; } }; /** * The shared answer for a vertex without edges. Use it instead of a fresh `[]` fallback, the lookups sit in * traversal loops where a miss is the common case. * @example * ```ts * for(const [target, edge] of graph.outgoingEdges(id) ?? NoEdges) { /* ... *\/ } * ``` */ exports.NoEdges = new Map(); /** * Helpers for the {@link UnknownSideEffect} union, which is either a plain {@link NodeId} or a * `{ id, linkTo }` object. Use these instead of hand-rolling `typeof x === 'object' ? x.id : x` * checks so the object/non-object discrimination lives in one place. */ exports.UnknownSideEffect = { name: 'UnknownSideEffect', /** Whether the effect carries a {@link LinkTo} target (i.e., is the object variant). */ isLinked(effect) { return typeof effect === 'object'; }, /** The affected node id, regardless of whether the effect is plain or linked. */ id(effect) { return typeof effect === 'object' ? effect.id : effect; }, /** The {@link LinkTo} target of the effect, or `undefined` if it is a plain (unlinked) one. */ linkTo(effect) { return typeof effect === 'object' ? effect.linkTo : undefined; }, /** Both {@link id} and {@link linkTo} in one pass (a single discrimination); use when you need both. */ split(effect) { return typeof effect === 'object' ? effect : { id: effect, linkTo: undefined }; } }; /** * The dataflow graph holds the dataflow information found within the given AST. * We differentiate the directed edges in {@link EdgeType} and the vertices indicated by {@link DataflowGraphVertexArgument}. * The helper object associated with the DFG is {@link Dataflow}. * * The vertices of the graph are organized in a hierarchical fashion, with a function-definition node containing the node ids of its subgraph. * However, all *edges* are hoisted at the top level in the form of an (attributed) adjacency list. * After the dataflow analysis, all sources and targets of the edges *must* be part of the vertices. * However, this does not have to hold during the construction as edges may point from or to vertices which are yet to be constructed. * * All methods return the modified graph to allow for chaining. * @see {@link DataflowGraph#addEdge|`addEdge`} - to add an edge to the graph * @see {@link DataflowGraph#addVertex|`addVertex`} - to add a vertex to the graph * @see {@link DataflowGraph#fromJson|`fromJson`} - to construct a dataflow graph object from a deserialized JSON object. * @see {@link emptyGraph|`emptyGraph`} - to create an empty graph (useful in tests) */ class DataflowGraph { _idMap; /* * Set of vertices which have sideEffects that we do not know anything about. * As a (temporary) solution until we have FD edges, a side effect may also store known target links * that have to be/should be resolved (as globals) as a separate pass before the df analysis ends. */ _unknownSideEffects = new Set(); constructor(idMap) { this._idMap = idMap; } /** Contains the vertices of the root level graph (i.e., included those vertices from the complete graph, that are nested within function definitions) */ rootVertices = new Set(); /** All vertices in the complete graph (including those nested in function definition) */ vertexInformation = new Map(); /** All edges in the complete graph (including those nested in function definition) */ edgeInformation = new Map(); /* the reverse of `edgeInformation`, built on demand, then kept up to date by `addEdge` and dropped only on a * bulk write (`mergeEdges`, `fromJson`): rebuilding it per lookup made every caller asking for the ingoing * edges of many nodes scan the whole graph once per node */ incomingIndex; types = new Map(); toJSON() { return { rootVertices: Array.from(this.rootVertices), vertexInformation: Array.from(this.vertexInformation.entries()), edgeInformation: Array.from(this.edgeInformation.entries()).map(([id, edges]) => [id, Array.from(edges.entries())]), _unknownSideEffects: Array.from(this._unknownSideEffects) }; } /** * Get the {@link DataflowGraphVertexInfo} attached to a node as well as all outgoing edges. * @param id - The id of the node to get * @param includeDefinedFunctions - If true this will search function definitions as well and not just the toplevel * @returns the node info for the given id (if it exists) * @see #getVertex */ get(id, includeDefinedFunctions = true) { // if we do not want to include function definitions, only retrieve the value if the id is part of the root vertices const vertex = includeDefinedFunctions ? this.getVertex(id) : this.getRootVertex(id); return vertex === undefined ? undefined : [vertex, this.outgoingEdges(id) ?? new Map()]; } /** * Get the {@link DataflowGraphVertexInfo} attached to a vertex. * @param id - The id of the node to get * @returns the node info for the given id (if it exists) * @see #get * @see #getRootVertex */ getVertex(id) { return this.vertexInformation.get(id); } /** * Get the {@link DataflowGraphVertexInfo} attached to a root-level vertex. * @param id - The id of the node to get * @returns the node info for the given id (if it exists) * @see #get * @see #getVertex */ getRootVertex(id) { if (!this.rootVertices.has(id)) { return undefined; } return this.vertexInformation.get(id); } outgoingEdges(id) { return this.edgeInformation.get(id); } ingoingEdges(id) { if (this.incomingIndex === undefined) { const index = new Map(); for (const [source, outgoing] of this.edgeInformation.entries()) { for (const [target, edge] of outgoing) { const into = index.get(target); if (into === undefined) { index.set(target, new Map([[source, edge]])); } else { into.set(source, edge); } } } this.incomingIndex = index; } /* the historic contract is an (possibly empty) map for every id, never `undefined` */ return this.incomingIndex.get(id) ?? new Map(); } /** * Given a node in the normalized AST this either: * returns the id if the node directly exists in the DFG * returns the ids of all vertices in the DFG that are linked to this * returns undefined if the node is not part of the DFG and not linked to any node */ getLinked(nodeId) { if (this.vertexInformation.has(nodeId)) { return [nodeId]; } const linked = []; for (const [id, vtx] of this.vertexInformation) { if (vtx.link?.origin.includes(nodeId)) { linked.push(id); } } return linked.length > 0 ? linked : undefined; } /** Retrieves the id-map to the normalized AST attached to the dataflow graph */ get idMap() { return this._idMap; } /** * Retrieves the set of vertices which have side effects that we do not know anything about. */ get unknownSideEffects() { return this._unknownSideEffects; } /** Allows setting the id-map explicitly (which should only be used when, e.g., you plan to compare two dataflow graphs on the same AST-basis) */ setIdMap(idMap) { this._idMap = idMap; } /** * @param includeDefinedFunctions - If true this will iterate over function definitions as well and not just the toplevel * @returns the ids of all toplevel vertices in the graph together with their vertex information * @see #edges */ *vertices(includeDefinedFunctions) { if (includeDefinedFunctions) { yield* this.vertexInformation.entries(); } else { for (const id of this.rootVertices) { yield [id, this.vertexInformation.get(id)]; } } } *verticesOfType(type) { const ids = this.types.get(type) ?? []; for (const id of ids) { yield [id, this.vertexInformation.get(id)]; } } vertexIdsOfType(type) { return this.types.get(type) ?? []; } /** * @returns the ids of all edges in the graph together with their edge information * @see #vertices */ *edges() { yield* this.edgeInformation.entries(); } /** * Returns true if the graph contains a node with the given id. * @param id - The id to check for * @param includeDefinedFunctions - If true this will check function definitions as well and not just the toplevel */ hasVertex(id, includeDefinedFunctions = true) { return includeDefinedFunctions ? this.vertexInformation.has(id) : this.rootVertices.has(id); } /** * Returns true if the root level of the graph contains a node with the given id. */ isRoot(id) { return this.rootVertices.has(id); } rootIds() { return this.rootVertices; } /** * Takes `type` off the edge `fromId -> toId`, dropping the edge itself once it states nothing. * An edge with no type is no edge: every removal has to go through here so none is left behind. */ removeEdgeType(fromId, toId, type) { const from = node_id_1.NodeId.normalize(fromId), to = node_id_1.NodeId.normalize(toId); const targets = this.edgeInformation.get(from); const edge = targets?.get(to); if (edge === undefined || targets === undefined) { return this; } edge.types &= ~type; if (edge_1.DfEdge.hasAnyType(edge)) { /* the reverse index holds this very object, so a narrowed type is already visible through it */ return this; } targets.delete(to); if (targets.size === 0) { this.edgeInformation.delete(from); } const into = this.incomingIndex?.get(to); into?.delete(from); if (into?.size === 0) { this.incomingIndex?.delete(to); } return this; } /** * Adds a new vertex to the graph, for ease of use, some arguments are optional and filled automatically. * @param vertex - The vertex to add * @param fallbackEnv - A clean environment to use if no environment is given in the vertex * @param asRoot - If false, this will only add the vertex but do not add it to the {@link rootIds|root vertices} of the graph. * This is probably only of use, when you construct dataflow graphs for tests. * @param overwrite - If true, this will overwrite the vertex if it already exists in the graph (based on the id). * @see DataflowGraphVertexInfo * @see DataflowGraphVertexArgument */ addVertex(vertex, fallbackEnv, asRoot = true, overwrite = false) { const vid = vertex.id; if (this.vertexInformation.has(vid) && !overwrite) { return this; } const vtag = vertex.tag; // keep a clone of the original environment, isolating the snapshot from later updates vertex.environment = vertex.environment ? (0, clone_1.cloneEnvironmentInformation)(vertex.environment) : (vtag === vertex_1.VertexType.FunctionDefinition || (vtag === vertex_1.VertexType.FunctionCall && !vertex.onlyBuiltin) ? fallbackEnv : undefined); this.vertexInformation.set(vid, vertex); const typeIds = this.types.get(vtag); if (typeIds) { typeIds.push(vid); } else { this.types.set(vtag, [vid]); } if (asRoot) { this.rootVertices.add(vid); } return this; } addEdge(fromId, toId, type) { if (fromId === toId) { return this; } const fromEdges = this.edgeInformation.get(fromId); const existing = fromEdges?.get(toId); if (existing !== undefined) { /* the reverse index holds this very object, so a widened type is already visible through it */ existing.types |= type; return this; } const added = { types: type }; if (fromEdges === undefined) { this.edgeInformation.set(fromId, new Map([[toId, added]])); } else { fromEdges.set(toId, added); } /* * carry the new edge into the reverse index rather than dropping it: construction interleaves `addEdge` * with `ingoingEdges`, and rebuilding costs a pass over every edge in the graph each time */ const into = this.incomingIndex?.get(toId); if (into !== undefined) { into.set(fromId, added); } else { this.incomingIndex?.set(toId, new Map([[fromId, added]])); } return this; } /** * Merges the other graph into *this* one (in-place). The return value is only for convenience. * @param otherGraph - The graph to merge into this one * @param mergeRootVertices - If false, this will only merge the vertices and edges but exclude the root vertices this is probably only of use * in the context of function definitions */ mergeWith(otherGraph, mergeRootVertices = true) { if (otherGraph === undefined || otherGraph === this) { return this; } this.mergeVertices(otherGraph, mergeRootVertices); for (const [type, ids] of otherGraph.types) { const existing = this.types.get(type); if (existing) { if (existing !== ids) { for (const id of ids) { existing.push(id); } } } else { this.types.set(type, ids.slice()); } } this.mergeEdges(otherGraph); return this; } mergeVertices(otherGraph, mergeRootVertices = true) { // merge root ids if (mergeRootVertices) { for (const root of otherGraph.rootVertices) { this.rootVertices.add(root); } } for (const unknown of otherGraph.unknownSideEffects) { this._unknownSideEffects.add(unknown); } for (const [id, info] of otherGraph.vertexInformation) { const currentInfo = this.vertexInformation.get(id); this.vertexInformation.set(id, currentInfo === undefined ? info : mergeNodeInfos(currentInfo, info)); } } mergeEdges(otherGraph) { this.incomingIndex = undefined; for (const [id, edges] of otherGraph.edgeInformation.entries()) { let existing = this.edgeInformation.get(id); if (existing === undefined) { existing = new Map(edges); this.edgeInformation.set(id, existing); } else { for (const [target, edge] of edges) { const get = existing.get(target); if (get === undefined) { existing.set(target, edge); } else { get.types |= edge.types; } } } } } /** * Marks a vertex in the graph to be a definition * @param reference - The reference to the vertex to mark as definition * @param sourceIds - The id of the source vertex of the def, if available */ setDefinitionOfVertex(reference, sourceIds) { const vertex = this.getVertex(reference.nodeId); (0, assert_1.guard)(vertex !== undefined, () => `node must be defined for ${JSON.stringify(reference)} to set reference`); if (vertex_2.FunctionDefinitionVertex.is(vertex) || vertex_2.VariableDefinitionVertex.is(vertex)) { vertex.cds = reference.cds; } else { const oldTag = vertex.tag; const vid = reference.nodeId; vertex.tag = vertex_1.VertexType.VariableDefinition; if (sourceIds) { vertex.source = sourceIds; } const oldIds = this.types.get(oldTag); if (oldIds) { for (let idx = oldIds.lastIndexOf(vid); idx >= 0; idx = oldIds.lastIndexOf(vid)) { oldIds.splice(idx, 1); } } const newIds = this.types.get(vertex_1.VertexType.VariableDefinition); if (newIds) { newIds.push(vid); } else { this.types.set(vertex_1.VertexType.VariableDefinition, [vid]); } } } /** * Marks a vertex in the graph to be a function call with the new information * @param info - The information about the new function call node */ updateToFunctionCall(info) { const infoId = info.id; const vertex = this.getVertex(infoId); (0, assert_1.guard)(vertex !== undefined && (vertex_2.UseVertex.is(vertex) || vertex_2.ValueVertex.is(vertex)), () => `node must be a use or value node for ${JSON.stringify(info.id)} to update it to a function call but is ${vertex?.tag}`); const previousTag = vertex.tag; this.vertexInformation.set(infoId, { ...vertex, ...info, tag: vertex_1.VertexType.FunctionCall }); const prevIds = this.types.get(previousTag); if (prevIds) { for (let idx = prevIds.lastIndexOf(infoId); idx >= 0; idx = prevIds.lastIndexOf(infoId)) { prevIds.splice(idx, 1); } } const callIds = this.types.get(vertex_1.VertexType.FunctionCall); if (callIds) { callIds.push(infoId); } else { this.types.set(vertex_1.VertexType.FunctionCall, [infoId]); } } /** If you do not pass the `to` node, this will just mark the node as maybe */ addControlDependency(from, to, when) { to = node_id_1.NodeId.normalize(to); const vertex = this.getVertex(from); (0, assert_1.guard)(vertex !== undefined, () => `node must be defined for ${from} to add control dependency`); if (vertex.cds) { for (const { id, when: cond } of vertex.cds) { if (id === to && when !== cond) { return this; } } } else { vertex.cds = []; } vertex.cds.push({ id: to, when }); return this; } /** Marks the given node as having unknown side effects */ markIdForUnknownSideEffects(id, target) { if (target) { this._unknownSideEffects.add({ id: node_id_1.NodeId.normalize(id), linkTo: typeof target.callName === 'string' ? { ...target, callName: new RegExp(target.callName) } : target }); return this; } this._unknownSideEffects.add(node_id_1.NodeId.normalize(id)); return this; } /** * Constructs a dataflow graph instance from the given JSON data and returns the result. * This can be useful for data sent by the flowR server when analyzing it further. * @param data - The JSON data to construct the graph from */ static fromJson(data) { const graph = new DataflowGraph(undefined); graph.rootVertices = new Set(data.rootVertices); graph.vertexInformation = new Map(data.vertexInformation); for (const [, vertex] of graph.vertexInformation) { if (vertex.environment) { vertex.environment = renvFromJson(vertex.environment); } } graph.edgeInformation = new Map(data.edgeInformation.map(([id, edges]) => [id, new Map(edges)])); graph.incomingIndex = undefined; for (const unknown of data._unknownSideEffects) { graph._unknownSideEffects.add(unknown); } return graph; } } exports.DataflowGraph = DataflowGraph; function mergeNodeInfos(current, next) { if (current.tag !== next.tag) { return current; } else if (vertex_2.FunctionDefinitionVertex.is(current)) { const n = next; current.exitPoints = (0, arrays_1.uniqueArrayMerge)(current.exitPoints, n.exitPoints); if (n.mode && n.mode.length > 0) { current.mode ??= []; for (const m of n.mode) { if (!current.mode.includes(m)) { current.mode.push(m); } } } } return current; } function envFromJson(json) { const parent = json.parent ? envFromJson(json.parent) : undefined; const memory = new Map(); for (const [key, value] of Object.entries(json.memory)) { memory.set(key, value); } const obj = new environment_1.Environment(parent, json.builtInEnv); obj.id = json.id; obj.memory = memory; const env = obj; env.n = json.n; env.t = json.t; env.globalEnv = json.globalEnv; return env; } function renvFromJson(json) { const current = envFromJson(json.current); return { current, level: json.level }; } //# sourceMappingURL=graph.js.map