UNPKG

@eagleoutice/flowr-dev

Version:

Static Dataflow Analyzer and Program Slicer for the R Programming Language

453 lines 20.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.getAliases = getAliases; exports.resolveIdToValue = resolveIdToValue; exports.resolveIdToSingleString = resolveIdToSingleString; exports.trackAliasInEnvironments = trackAliasInEnvironments; exports.trackAliasesInGraph = trackAliasesInGraph; exports.resolveToConstants = resolveToConstants; const config_1 = require("../../../config"); const node_id_1 = require("../../../r-bridge/lang-4.x/ast/model/processing/node-id"); const type_1 = require("../../../r-bridge/lang-4.x/ast/model/type"); const visiting_queue_1 = require("../../../slicing/static/visiting-queue"); const assert_1 = require("../../../util/assert"); const identifier_1 = require("../../environments/identifier"); const edge_1 = require("../../graph/edge"); const r_for_loop_1 = require("../../../r-bridge/lang-4.x/ast/model/nodes/r-for-loop"); const unknown_replacement_1 = require("../../graph/unknown-replacement"); const unknown_side_effect_1 = require("../../graph/unknown-side-effect"); const vertex_1 = require("../../graph/vertex"); const general_1 = require("../values/general"); const r_value_1 = require("../values/r-value"); const set_constants_1 = require("../values/sets/set-constants"); const resolve_1 = require("./resolve"); const model_1 = require("../../../r-bridge/lang-4.x/ast/model/model"); const resolve_helper_1 = require("../../environments/resolve-helper"); const node_value_1 = require("./node-value"); const graph_1 = require("../../graph/graph"); const AliasHandler = { [vertex_1.VertexType.Value]: (sourceId) => [sourceId], [vertex_1.VertexType.Use]: getUseAlias, [vertex_1.VertexType.FunctionCall]: getFunctionCallAlias, [vertex_1.VertexType.FunctionDefinition]: () => undefined, [vertex_1.VertexType.VariableDefinition]: () => undefined }; function getFunctionCallAlias(sourceId, dataflow, environment) { const vertex = dataflow.getVertex(sourceId); /* the lexeme of an infix call like `a %% b` is the whole expression, so we prefer the effective name of the vertex */ const identifier = vertex_1.FunctionCallVertex.is(vertex) ? vertex.name : (0, node_id_1.recoverName)(sourceId, dataflow.idMap); if (identifier === undefined) { return undefined; } const defs = resolve_helper_1.Resolve.byNameAndType(identifier, environment, identifier_1.ReferenceType.Function); if (defs?.length !== 1) { return undefined; } return [sourceId]; } function getUseAlias(sourceId, dataflow, environment) { const definitions = []; // Source is Symbol -> resolve definitions of symbol const identifier = (0, node_id_1.recoverName)(sourceId, dataflow.idMap); if (identifier === undefined) { return undefined; } const defs = resolve_helper_1.Resolve.byName(identifier, environment); if (defs === undefined) { return undefined; } for (const def of defs) { // If one definition is not constant (or a variable aliasing a constant) // we can't say for sure what value the source has if (def.type === identifier_1.ReferenceType.Variable) { if (def.value === undefined) { return undefined; } definitions.push(...def.value); } else if (def.type === identifier_1.ReferenceType.Constant || def.type === identifier_1.ReferenceType.BuiltInConstant) { definitions.push(def.nodeId); } else { return undefined; } } return definitions; } /** * Gets the definitions / aliases of a node * * This function is called by the built-in-assignment processor so that we can * track assignments inside the environment. The returned ids are stored in * the sourceIds value field of their InGraphIdentifierDefinition. This enables * us later, in the {@link trackAliasInEnvironments} function, to get all the * aliases of an identifier. * @param sourceIds - node ids to get the definitions for * @param dataflow - dataflow graph * @param environment - environment * @returns node id of alias */ function getAliases(sourceIds, dataflow, environment) { const definitions = new Set(); for (const sourceId of sourceIds) { const info = dataflow.getVertex(sourceId); if (info === undefined) { return undefined; } else if (vertex_1.FunctionDefinitionVertex.is(info)) { definitions.add(sourceId); continue; } const defs = AliasHandler[info.tag](sourceId, dataflow, environment); for (const def of defs ?? []) { definitions.add(def); } } return Array.from(definitions); } /** * Evaluates the value of a node in the set domain. * * resolveIdToValue tries to resolve the value using the data it has been given. * If the environment is provided the approximation is more precise, as we can * track aliases in the environment. * Otherwise, the graph is used to try and resolve the nodes value. * If neither is provided the value cannot be resolved. * * This function is also used by the Resolve Value Query and the Dependency Query * to resolve values. For e.g. in the Dependency Query it is used to resolve calls * like `lapply(c("a", "b", "c"), library, character.only = TRUE)` * @param id - The node id or node to resolve * @param environment - The current environment used for name resolution * @param graph - The graph to resolve in * @param idMap - The id map to resolve the node if given as an id * @param full - Whether to track aliases on resolve * @param resolve - Variable resolve mode * @param ctx - Context used for clean environment * @param blocked - If set, the ids that should not be considered during resolution (=&gt;top) * @useInstead {@link Resolve.toValue} */ function resolveIdToValue(id, { environment, graph, idMap, full = true, ctx, resolve = ctx.config.solver.variables, blocked }) { blocked ??= new Set(); if (id === undefined) { return r_value_1.Top; } idMap ??= graph?.idMap; const node = typeof id === 'object' ? id : idMap?.get(id); if (node === undefined || blocked.has(node.info.id)) { return r_value_1.Top; } blocked.add(node.info.id); switch (node.type) { case type_1.RType.Argument: /* a missing argument (`f(x=)`) carries no value, and it is no symbol to resolve either */ return node.value ? resolveIdToValue(node.value.info.id, { environment, graph, idMap, full, resolve, ctx, blocked }) : r_value_1.Top; case type_1.RType.Symbol: if (full) { if (environment) { return trackAliasInEnvironments(identifier_1.Identifier.toString(node.content), environment, { idMap, resolve, ctx, graph, blocked }); } else if (graph && resolve === config_1.VariableResolve.Alias) { return trackAliasesInGraph(node.info.id, graph, ctx, idMap, blocked); } } return r_value_1.Top; case type_1.RType.FunctionDefinition: return (0, set_constants_1.setFrom)({ type: 'function-definition' }); case type_1.RType.FunctionCall: case type_1.RType.BinaryOp: case type_1.RType.UnaryOp: case type_1.RType.ExpressionList: return (0, set_constants_1.setFrom)((0, resolve_1.resolveNode)({ resolve, node, ctx, environment, graph, idMap, blocked })); case type_1.RType.String: case type_1.RType.Number: case type_1.RType.Logical: return (0, set_constants_1.setFrom)((0, general_1.valueFromRNodeConstant)(node)); default: return r_value_1.Top; } } /** Resolves an id to a single constant string value, or `undefined` if it is not a unique string constant. */ /** * @useInstead {@link Resolve.toSingleString} */ function resolveIdToSingleString(id, info) { const element = node_value_1.NodeValue.sole((0, general_1.valueSetGuard)(resolveIdToValue(id, info)), 'string'); return element !== undefined && (0, r_value_1.isValue)(element.value) ? element.value.str : undefined; } /** the values one element of a sequence may take: a vector contributes its elements, a set what its members do */ function iteratedElements(value) { if (value.type === 'vector' && (0, r_value_1.isValue)(value.elements)) { return value.elements.flatMap(iteratedElements); } else if (value.type === 'set' && (0, r_value_1.isValue)(value.elements)) { return value.elements.flatMap(iteratedElements); } return [value]; } /** * Please use {@link resolveIdToValue} * * Uses the aliases that were tracked in the environments (by the * {@link getAliases} function) to resolve a node to a value. * @param identifier - Identifier to resolve * @param environment - Environment to use * @param r - Resolve information (env, ctx, ...) * @returns Value of Identifier or Top */ function trackAliasInEnvironments(identifier, environment, { blocked, idMap, resolve = config_1.VariableResolve.Alias, ctx, graph }) { if (identifier === undefined) { return r_value_1.Top; } const defs = resolve_helper_1.Resolve.byName(identifier, environment); if (defs === undefined) { return r_value_1.Top; } const values = new Set(); for (const def of defs) { if (def.type === identifier_1.ReferenceType.BuiltInConstant) { values.add((0, general_1.valueFromTsValue)(def.value)); } else if (def.type === identifier_1.ReferenceType.BuiltInFunction) { // Tracked in #1207 } else if (def.value !== undefined) { /* if there is at least one location for which we have no idea, we have to give up for now! */ if (def.value.length === 0) { return r_value_1.Top; } for (const alias of def.value) { const definitionOfAlias = idMap?.get(alias); if (definitionOfAlias !== undefined) { /* the environment attempt below marks what it visited as blocked, so the retry starts from a copy */ const beforeAttempt = blocked === undefined ? undefined : new Set(blocked); let value = (0, resolve_1.resolveNode)({ resolve, node: definitionOfAlias, ctx, environment, graph, idMap, blocked }); if ((0, r_value_1.isTop)(value) && graph?.unknownSideEffects.size === 0) { /* `x <- x + 1`: the environment holds only the definition we are resolving, so its own operand * finds nothing there, while the graph still knows which definition that operand reads. Only * when nothing unknown happened, as then the environment is right to give up */ value = (0, resolve_1.resolveNode)({ resolve, node: definitionOfAlias, ctx, graph, idMap, blocked: beforeAttempt }); } if ((0, r_value_1.isTop)(value)) { return r_value_1.Top; } /* the sequence is what it runs over, so what the name holds is one of its elements */ if (def.iterated) { iteratedElements(value).forEach(e => values.add(e)); } else { values.add(value); } } } } } if (values.size === 0) { return r_value_1.Top; } return (0, set_constants_1.setFrom)(...values); } /** given an unknown alias, we have to clear all values in the environments */ (0, unknown_side_effect_1.onUnknownSideEffect)((_graph, env, _id, target) => { if (target) { return; } let current = env.current; while (current) { current.memory.forEach(mem => mem.forEach((def) => { if (def.type !== identifier_1.ReferenceType.BuiltInConstant && def.type !== identifier_1.ReferenceType.BuiltInFunction && def.value !== undefined) { def.value.length = 0; } })); current = current.parent; } }); (0, unknown_replacement_1.onReplacementOperator)((args) => { if (!args.target) { return; } let current = args.env.current; while (current) { const defs = current.memory.get(args.target); defs?.forEach(def => { if (def.type !== identifier_1.ReferenceType.BuiltInConstant && def.type !== identifier_1.ReferenceType.BuiltInFunction && def.value !== undefined) { def.value.length = 0; } }); current = current.parent; } }); function isNestedInLoop(node, ast) { return model_1.RNode.iterateParents(node, ast).some(model_1.RLoopConstructs.is); } /** whether the node is (or sits in) a parameter's default, which any call site may override */ function isParameterDefault(node, idMap) { return node !== undefined && (node.info.role === "param-v" /* RoleInParent.ParameterDefaultValue */ || model_1.RNode.iterateParents(node, idMap).some(p => p.info.role === "param-v" /* RoleInParent.ParameterDefaultValue */)); } /** * The sequence a use of a `for` loop's variable runs over, `undefined` when `id` is no such use. Every * definition it reads has to be that variable: once anything else writes the name (`i <- i + 1` in the body), * the sequence no longer states what the name holds. */ function iteratedSequence(id, graph, idMap) { let sequence; for (const [target, edge] of graph.outgoingEdges(id) ?? graph_1.NoEdges) { if (!edge_1.DfEdge.includesType(edge, edge_1.EdgeType.Reads)) { continue; } const definition = idMap.get(target); const loop = definition?.info.parent === undefined ? undefined : idMap.get(definition.info.parent); if (!r_for_loop_1.RForLoop.is(loop) || loop.variable.info.id !== definition?.info.id || (sequence !== undefined && sequence.info.id !== loop.vector.info.id)) { return undefined; } sequence = loop.vector; } return sequence; } /** * Please use {@link resolveIdToValue} * * Tries to resolve the value of a node by traversing the dataflow graph * @param id - node to resolve * @param ctx - analysis context * @param graph - dataflow graph * @param idMap - idmap of dataflow graph * @param blocked - the ids already being resolved, so a cyclic definition stops * @returns Value of node or Top/Bottom */ function trackAliasesInGraph(id, graph, ctx, idMap, blocked) { if (!graph.get(id)) { return r_value_1.Bottom; } idMap ??= graph.idMap; (0, assert_1.guard)(idMap !== undefined, 'The ID map is required to get the lineage of a node'); /* a loop makes the walk below give up, yet the variable of a `for` is precisely its sequence, elementwise */ const sequence = iteratedSequence(id, graph, idMap); if (sequence !== undefined) { const value = resolveIdToValue(sequence.info.id, { graph, idMap, ctx, blocked }); return (0, r_value_1.isTop)(value) ? r_value_1.Top : (0, set_constants_1.setFrom)(...iteratedElements(value)); } const queue = new visiting_queue_1.VisitingQueue(10); const clean = ctx.env.makeCleanEnv(); const cleanFingerprint = ctx.env.getCleanEnvFingerprint(); queue.add(id, clean, cleanFingerprint, false); let forceTop = false; const resultIds = []; /* what a call the walk ended in folds to, see below */ const folded = []; while (queue.nonEmpty()) { const { id, baseEnvironment } = queue.next(); const vertex = graph.getVertex(id); if (!vertex) { continue; } const cds = vertex.cds; for (const cd of cds ?? []) { const target = graph.idMap?.get(cd.id); if (target === undefined) { continue; } if (model_1.RLoopConstructs.is(target)) { forceTop = true; break; } } if (!forceTop && (cds?.length === 0 && isNestedInLoop(idMap.get(id), idMap))) { forceTop = true; } if (forceTop) { break; } const t = vertex.tag; /* a replacement (`df[1] <- v`) defines its target from just a part of it, so what it is defined by is not * the value of the target */ if (t === vertex_1.VertexType.VariableDefinition && idMap.get(id)?.info.role === "acc" /* RoleInParent.Accessed */) { return r_value_1.Top; } if (t === vertex_1.VertexType.Value || t === vertex_1.VertexType.FunctionDefinition) { resultIds.push(id); continue; } const isFn = t === vertex_1.VertexType.FunctionCall; const outgoingEdges = graph.outgoingEdges(id) ?? graph_1.NoEdges; let foundRetuns = false; // travel all read and defined-by edges for (const [targetId, edge] of outgoingEdges) { if (isFn) { if (edge_1.DfEdge.isOnlyType(edge, edge_1.EdgeType.Returns) || edge_1.DfEdge.isOnlyType(edge, edge_1.EdgeType.DefinedByOnCall) || edge_1.DfEdge.isOnlyType(edge, edge_1.EdgeType.DefinedBy)) { queue.add(targetId, baseEnvironment, cleanFingerprint, false); } foundRetuns ||= edge_1.DfEdge.includesType(edge, edge_1.EdgeType.Returns); continue; } // currently, they have to be exact! if (edge_1.DfEdge.isOnlyType(edge, edge_1.EdgeType.Reads) || edge_1.DfEdge.isOnlyType(edge, edge_1.EdgeType.DefinedBy) || edge_1.DfEdge.isOnlyType(edge, edge_1.EdgeType.DefinedByOnCall)) { queue.add(targetId, baseEnvironment, cleanFingerprint, false); } } if (isFn && !foundRetuns) { /* a call that hands back none of its arguments has no `Returns` edge to follow, yet the value solver may * well know what it produces (`p <- file.path("data", "x.csv")`), so we fold it instead of giving up */ const node = idMap.get(id); const values = isParameterDefault(node, idMap) ? undefined : (0, general_1.valueSetGuard)(resolveIdToValue(node, { graph, idMap, ctx, resolve: config_1.VariableResolve.Alias, blocked })); if (values === undefined || values.elements.some(r_value_1.isTop)) { return r_value_1.Top; } folded.push(...values.elements); continue; } } if (forceTop || (resultIds.length === 0 && folded.length === 0)) { return r_value_1.Top; } const values = new Set(folded); for (const id of resultIds) { const vertex = graph.getVertex(id); if (vertex_1.ValueVertex.is(vertex) && vertex.value !== undefined) { values.add(vertex.value); continue; } const node = idMap.get(id); if (node !== undefined) { if (isParameterDefault(node, idMap)) { return r_value_1.Top; } values.add((0, general_1.valueFromRNodeConstant)(node)); } } return values.size === 0 ? r_value_1.Top : (0, set_constants_1.setFrom)(...values); } /** * Please use {@link resolveIdToValue} * * Resolve an Identifier to a constant, if the identifier is a constant * @param name - Identifier to resolve * @param environment - Environment to use * @returns Value of Constant or Top * @useInstead {@link Resolve.toConstants} */ function resolveToConstants(name, environment) { if (name === undefined) { return r_value_1.Top; } const definitions = resolve_helper_1.Resolve.byNameAndType(name, environment, identifier_1.ReferenceType.Constant); if (definitions === undefined) { return r_value_1.Top; } const values = new Set(); definitions.forEach(def => { const d = def.value; values.add(d === undefined ? r_value_1.Top : (0, general_1.valueFromTsValue)(d)); }); return (0, set_constants_1.setFrom)(...values); } //# sourceMappingURL=alias-tracking.js.map