@eagleoutice/flowr-dev
Version:
Static Dataflow Analyzer and Program Slicer for the R Programming Language
453 lines • 20.5 kB
JavaScript
"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 (=>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