@eagleoutice/flowr-dev
Version:
Static Dataflow Analyzer and Program Slicer for the R Programming Language
526 lines • 27 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.processFunctionDefinition = processFunctionDefinition;
exports.retrieveActiveEnvironment = retrieveActiveEnvironment;
exports.updateNestedFunctionCalls = updateNestedFunctionCalls;
const processor_1 = require("../../../../../processor");
const info_1 = require("../../../../../info");
const linker_1 = require("../../../../linker");
const known_call_handling_1 = require("../known-call-handling");
const unpack_argument_1 = require("../argument/unpack-argument");
const assert_1 = require("../../../../../../util/assert");
const logger_1 = require("../../../../../logger");
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 graph_1 = require("../../../../../graph/graph");
const identifier_1 = require("../../../../../environments/identifier");
const overwrite_1 = require("../../../../../environments/overwrite");
const vertex_1 = require("../../../../../graph/vertex");
const built_in_new_env_1 = require("./built-in-new-env");
const scoping_1 = require("../../../../../environments/scoping");
const edge_1 = require("../../../../../graph/edge");
const log_1 = require("../../../../../../util/log");
const built_in_library_1 = require("./built-in-library");
const type_1 = require("../../../../../../r-bridge/lang-4.x/ast/model/type");
const hooks_1 = require("../../../../../hooks");
const built_in_proc_name_1 = require("../../../../../environments/built-in-proc-name");
const resolve_helper_1 = require("../../../../../environments/resolve-helper");
/**
* Process a function definition, i.e., `function(a, b) { ... }`
*/
function processFunctionDefinition(name, args, rootId, data) {
if (args.length < 1) {
logger_1.dataflowLogger.warn(`Function Definition ${identifier_1.Identifier.toString(name.content)} does not have an argument, skipping`);
return (0, known_call_handling_1.processKnownFunctionCall)({ name, args, rootId, data, origin: 'default' }).information;
}
/* we remove the last argument, as it is the body */
const parameters = args.slice(0, -1);
const bodyArg = (0, unpack_argument_1.unpackNonameArg)(args.at(-1));
(0, assert_1.guard)(bodyArg !== undefined, () => `Function Definition ${JSON.stringify(args)} has no body! This is bad!`);
const originalEnvironment = data.environment;
// within a function def we do not pass on the outer binds as they could be overwritten when called
data = prepareFunctionEnvironment(data, rootId);
const subgraph = new graph_1.DataflowGraph(data.completeAst.idMap);
let readInParameters = [];
const allParameterReads = [];
const paramIds = [];
for (const param of parameters) {
(0, assert_1.guard)(param !== r_function_call_1.EmptyArgument, () => `Empty param arg in function definition ${identifier_1.Identifier.toString(name.content)}, ${JSON.stringify(args)}`);
const processed = (0, processor_1.processDataflowFor)(param, data);
if (param.value?.type === type_1.RType.Parameter) {
paramIds.push(param.value.name.info.id);
}
subgraph.mergeWith(processed.graph);
const read = processed.in.concat(processed.unknownReferences);
allParameterReads.push(...read);
(0, linker_1.linkInputs)(read, data.environment, readInParameters, subgraph, false);
data.environment = (0, overwrite_1.overwriteEnvironment)(data.environment, processed.environment);
}
const paramsEnvironments = data.environment;
const body = (0, processor_1.processDataflowFor)(bodyArg, data);
for (const [, v] of body.graph.verticesOfType(vertex_1.VertexType.FunctionCall)) {
if (!v.origin.includes(built_in_proc_name_1.BuiltInProcName.Rm)) {
continue;
}
const ea = v.args.find(a => a !== r_function_call_1.EmptyArgument && graph_1.FunctionArgument.isNamed(a) && a.name === 'envir');
if (!ea || !graph_1.FunctionArgument.isNamed(ea)) {
continue;
}
const offset = parseSysFrameOffset(data.completeAst.idMap.get(ea.valueId ?? ea.nodeId));
if (offset === undefined || offset > 0) {
continue;
}
const names = [];
for (const a of v.args) {
if (a === r_function_call_1.EmptyArgument || (graph_1.FunctionArgument.isNamed(a) && a.name === 'envir')) {
continue;
}
const node = data.completeAst.idMap.get(graph_1.FunctionArgument.isNamed(a) ? (a.valueId ?? a.nodeId) : a.nodeId);
if (node?.type === type_1.RType.String) {
names.push(node.content.str);
}
else if (node?.type === type_1.RType.Symbol) {
names.push(node.content);
}
}
const targetLevel = offset === 0 ? 0 : originalEnvironment.level + 1 + offset;
let targetEnv = originalEnvironment;
while (targetEnv.level > targetLevel && targetEnv.level > 0) {
targetEnv = (0, scoping_1.popLocalEnvironment)(targetEnv);
}
if (targetEnv.level === targetLevel) {
for (const n of names) {
targetEnv.current.remove(n);
}
}
}
// As we know, parameters cannot technically duplicate (i.e., their names are unique), we overwrite their environments.
// This is the correct behavior, even if someone uses non-`=` arguments in functions.
const bodyEnvironment = body.environment;
// a default read (e.g. `function(x, y = x)`) may also see a later body reassignment, as the default is a promise
const writesByName = groupBodyWrites(body.out);
const unresolvedParamReads = new Set(readInParameters);
for (const read of allParameterReads) {
if (read.name && !unresolvedParamReads.has(read)) {
linkParameterReadToBodyWrites(subgraph, read, writesByName);
}
}
readInParameters = findPromiseLinkagesForParameters(subgraph, readInParameters, paramsEnvironments, writesByName);
const readInBody = body.in.concat(body.unknownReferences);
// there is no uncertainty regarding the arguments, as if a function header is executed, so is its body
const remainingRead = (0, linker_1.linkInputs)(readInBody, paramsEnvironments, readInParameters.slice(), body.graph, true /* functions do not have to be called */);
// functions can be called multiple times,
// so if they have a global effect, we have to link them as if they would be executed a loop
/* theoretically, we should just check if there is a global effect-write somewhere within */
if (remainingRead.length > 0) {
const nameIdShares = (0, linker_1.produceNameSharedIdMap)(remainingRead);
const definedInLocalEnvironment = new Set();
for (const defs of bodyEnvironment.current.memory.values()) {
for (const d of defs) {
definedInLocalEnvironment.add(d.nodeId);
}
}
// Everything that is in body.out but not within the local environment populated for the function scope is a potential escape ~> global definition
const globalBodyOut = body.out.filter(d => !definedInLocalEnvironment.has(d.nodeId));
(0, linker_1.linkCircularRedefinitionsWithinALoop)(body.graph, nameIdShares, globalBodyOut);
}
subgraph.mergeWith(body.graph);
let outEnvironment = (0, overwrite_1.overwriteEnvironment)(paramsEnvironments, bodyEnvironment);
for (const read of remainingRead) {
if (read.name) {
subgraph.addVertex({
tag: vertex_1.VertexType.Use,
id: read.nodeId,
environment: undefined,
cds: undefined
}, data.ctx.env.makeCleanEnv());
}
}
const compactedHooks = (0, hooks_1.compactHookStates)(body.hooks);
const exitHooks = (0, hooks_1.getHookInformation)(compactedHooks, hooks_1.KnownHooks.OnFnExit);
// an on.exit hook's `<<-` escapes like a body `<<-`; fold its escaping writes into this function's subflow
for (const hook of exitHooks) {
const vert = subgraph.getVertex(hook.id);
if (!vertex_1.FunctionDefinitionVertex.is(vert)) {
continue;
}
let hookEnvironment = vert.subflow.environment;
while (hookEnvironment.level > outEnvironment.level) {
hookEnvironment = (0, scoping_1.popLocalEnvironment)(hookEnvironment);
}
outEnvironment = (0, overwrite_1.overwriteEnvironment)(outEnvironment, hookEnvironment);
}
const flow = {
unknownReferences: [],
in: remainingRead,
out: [],
entryPoint: body.entryPoint,
graph: new Set(subgraph.rootIds()),
environment: outEnvironment,
hooks: compactedHooks
};
updateDispatches(subgraph, parameters.map(p => {
if (p === r_function_call_1.EmptyArgument) {
return r_function_call_1.EmptyArgument;
}
else if (!p.name && p.value && p.value.type === type_1.RType.Parameter) {
return { type: identifier_1.ReferenceType.Argument, cds: data.cds, nodeId: p.value.name.info.id, name: p.value.name.content, valueId: p.value.defaultValue?.info.id };
}
else if (p.name) {
return { type: identifier_1.ReferenceType.Argument, valueId: p.value?.info.id, cds: data.cds, nodeId: p.name.info.id, name: p.name.content };
}
else {
return r_function_call_1.EmptyArgument;
}
}));
updateNestedFunctionClosures(subgraph, outEnvironment, name.info.id);
const exitPoints = body.exitPoints;
const readParams = {};
for (const paramId of paramIds) {
const ingoing = subgraph.ingoingEdges(paramId);
readParams[paramId] = ingoing?.values().some(e => edge_1.DfEdge.includesType(e, edge_1.EdgeType.Reads)) ?? false;
}
let afterHookExitPoints = exitPoints?.filter(e => e.type === 1 /* ExitPointType.Return */ || e.type === 0 /* ExitPointType.Default */ || e.type === 4 /* ExitPointType.Error */) ?? [];
for (const hook of exitHooks) {
const vert = subgraph.getVertex(hook.id);
if (!vertex_1.FunctionDefinitionVertex.is(vert)) {
continue;
}
// call all hooks
subgraph.addEdge(rootId, hook.id, edge_1.EdgeType.Calls);
const hookExitPoints = vert.exitPoints.filter(e => e.type === 1 /* ExitPointType.Return */ || e.type === 4 /* ExitPointType.Error */);
if (hookExitPoints.length > 0) {
afterHookExitPoints = (0, info_1.overwriteExitPoints)(afterHookExitPoints, hookExitPoints);
}
}
let returnEnvState;
if (data.ctx.config.solver.trackEnvironments) {
for (const ep of afterHookExitPoints) {
const epVertex = subgraph.getVertex(ep.nodeId);
if (vertex_1.FunctionCallVertex.hasOrigin(epVertex, built_in_proc_name_1.BuiltInProcName.NewEnv)) {
returnEnvState = (0, built_in_new_env_1.createFreshEnvState)(data, { graph: subgraph, entryPoint: ep.nodeId });
break;
}
const epNode = subgraph.idMap?.get(ep.nodeId);
if (epNode?.type === type_1.RType.Symbol) {
const defs = resolve_helper_1.Resolve.byNameAndType(epNode.content, outEnvironment, identifier_1.ReferenceType.Variable);
const def = defs?.find((d) => d.envState !== undefined);
if (def?.envState) {
returnEnvState = def.envState;
break;
}
}
}
}
const graph = new graph_1.DataflowGraph(data.completeAst.idMap).mergeWith(subgraph, false);
graph.addVertex({
tag: vertex_1.VertexType.FunctionDefinition,
id: name.info.id,
environment: (0, scoping_1.popLocalEnvironment)(outEnvironment),
cds: data.cds,
params: readParams,
subflow: flow,
exitPoints: afterHookExitPoints,
returnEnvState
}, data.ctx.env.makeCleanEnv());
return {
/* nothing escapes a function definition, but the function itself, will be forced in assignment: { nodeId: functionDefinition.info.id, scope: data.activeScope, used: 'always', name: functionDefinition.info.id as string } */
unknownReferences: [],
in: [],
out: [],
exitPoints: [],
entryPoint: name.info.id,
graph,
environment: originalEnvironment,
hooks: []
};
}
/**
* Retrieve the active environment when entering a function definition or call
* @param callerEnvironment - environment at the call site / function definition site
* @param baseEnvironment - base environment within the function definition / call
* @param ctx - analyzer context
* @returns active environment within the function definition / call
*/
function retrieveActiveEnvironment(callerEnvironment, baseEnvironment, ctx) {
callerEnvironment ??= ctx.env.makeCleanEnv();
let level = callerEnvironment.level ?? 0;
if (baseEnvironment.level !== level) {
while (baseEnvironment.level < level) {
baseEnvironment = (0, scoping_1.pushLocalEnvironment)(baseEnvironment);
}
while (baseEnvironment.level > level) {
callerEnvironment = (0, scoping_1.pushLocalEnvironment)(callerEnvironment);
level = callerEnvironment.level;
}
}
return (0, overwrite_1.overwriteEnvironment)(baseEnvironment, callerEnvironment);
}
function updateDispatches(graph, myArgs) {
for (const [, info] of graph.vertices(false)) {
if (!vertex_1.FunctionCallVertex.is(info) || (!info.origin.includes(built_in_proc_name_1.BuiltInProcName.S3Dispatch) && !info.origin.includes(built_in_proc_name_1.BuiltInProcName.S7Dispatch))) {
continue;
}
if (info.args.length === 0) {
info.args = myArgs;
for (const arg of myArgs) {
// add argument edges
if (arg !== r_function_call_1.EmptyArgument) {
graph.addEdge(info.id, arg.nodeId, edge_1.EdgeType.Argument);
}
}
}
}
}
/**
* Update the closure links of all nested function definitions
* @param graph - dataflow graph to collect the function definitions from and to update the closure links for
* @param outEnvironment - active environment on resolving closures (i.e., exit of the function definition)
* @param fnId - id of the function definition to update the closure links for
*/
function updateNestedFunctionClosures(graph, outEnvironment, fnId) {
// track *all* function definitions - including those nested within the current graph,
// try to resolve their 'in' by only using the lowest scope which will be popped after this definition
for (const [id, { subflow }] of graph.verticesOfType(vertex_1.VertexType.FunctionDefinition)) {
const ingoingRefs = subflow.in;
const remainingIn = [];
for (const ingoing of ingoingRefs) {
const resolved = ingoing.name ? resolve_helper_1.Resolve.byNameAndType(ingoing.name, outEnvironment, ingoing.type) : undefined;
if (resolved === undefined) {
remainingIn.push(ingoing);
continue;
}
const inId = ingoing.nodeId;
(0, log_1.expensiveTrace)(logger_1.dataflowLogger, () => `Found ${resolved.length} references to open ref ${id} in closure of function definition ${fnId}`);
let allBuiltIn = true;
for (const ref of resolved) {
graph.addEdge(inId, ref.nodeId, edge_1.EdgeType.Reads);
if (!(0, identifier_1.isReferenceType)(ref.type, identifier_1.ReferenceType.BuiltInConstant | identifier_1.ReferenceType.BuiltInFunction)) {
allBuiltIn = false;
}
}
if (allBuiltIn) {
remainingIn.push(ingoing);
}
}
(0, log_1.expensiveTrace)(logger_1.dataflowLogger, () => `Keeping ${remainingIn.length} references to open ref ${id} in closure of function definition ${fnId}`);
subflow.in = remainingIn;
linkSuperAssignmentsToOuterDefinitions(graph, subflow.graph, outEnvironment);
}
}
function linkSuperAssignmentsToOuterDefinitions(parentGraph, nestedGraphNodeIds, parentEnvironment) {
for (const nodeId of nestedGraphNodeIds) {
const vertex = parentGraph.getVertex(nodeId);
if (!vertex_1.FunctionCallVertex.hasOrigin(vertex, built_in_proc_name_1.BuiltInProcName.SuperAssignment)) {
continue;
}
const outgoingReturns = parentGraph.outgoingEdges(nodeId);
if (!outgoingReturns) {
continue;
}
for (const [targetId, edge] of outgoingReturns) {
if (!edge_1.DfEdge.includesType(edge, edge_1.EdgeType.Returns)) {
continue;
}
const targetVertex = parentGraph.getVertex(targetId);
if (!vertex_1.VariableDefinitionVertex.is(targetVertex)) {
continue;
}
const targetNode = parentGraph.idMap?.get(targetId);
if (targetNode?.type !== type_1.RType.Symbol) {
continue;
}
const varName = targetNode.content;
const resolved = resolve_helper_1.Resolve.byNameAndType(varName, parentEnvironment, identifier_1.ReferenceType.Variable);
if (resolved) {
for (const ref of resolved) {
if (ref.nodeId !== targetId && !node_id_1.NodeId.isBuiltIn(ref.nodeId)) {
parentGraph.addEdge(targetId, ref.nodeId, edge_1.EdgeType.Reads);
}
}
}
}
}
}
/**
* Update the closure links of all nested function calls, this is probably to be done once at the end of the script
* @param graph - dataflow graph to collect the function calls from and to update the closure links for
* @param outEnvironment - active environment on resolving closures (i.e., exit of the function definition)
* @lintIgnore vertex-has-origin
*/
function updateNestedFunctionCalls(graph, outEnvironment, ctx) {
// track *all* function definitions - including those nested within the current graph,
// try to resolve their 'in' by only using the lowest scope which will be popped after this definition
for (const [id, { onlyBuiltin, environment, name, args, origin }] of graph.verticesOfType(vertex_1.VertexType.FunctionCall)) {
if (onlyBuiltin || name === undefined) {
continue;
}
const effectiveEnvironment = environment ? (0, overwrite_1.overwriteEnvironment)(outEnvironment, environment) : outEnvironment;
const targets = new Set((0, linker_1.getAllFunctionCallTargets)(id, graph, effectiveEnvironment));
const collectedNextMethods = new Set();
const treatAsS3 = origin.includes(built_in_proc_name_1.BuiltInProcName.S3Dispatch);
for (const target of targets) {
if (node_id_1.NodeId.isBuiltIn(target)) {
// a package export resolved lazily here (nested), so materialize it and link to its loader
const loader = resolve_helper_1.Resolve.byNameAndType(name, effectiveEnvironment, identifier_1.ReferenceType.Function)?.find(r => r.nodeId === target)?.definedAt;
if (loader !== undefined && !node_id_1.NodeId.isBuiltIn(loader)) {
(0, built_in_library_1.attachExportVertex)(graph, target, effectiveEnvironment, ctx);
graph.addEdge(target, loader, edge_1.EdgeType.Reads | edge_1.EdgeType.Calls);
}
graph.addEdge(id, target, edge_1.EdgeType.Calls);
continue;
}
const targetVertex = graph.getVertex(target);
// support reads on symbols
if (!vertex_1.FunctionDefinitionVertex.is(targetVertex)) {
if (vertex_1.UseVertex.is(targetVertex)) {
graph.addEdge(id, target, edge_1.EdgeType.Reads);
}
continue;
}
graph.addEdge(id, target, edge_1.EdgeType.Calls);
for (const exitPoint of targetVertex.exitPoints) {
graph.addEdge(id, exitPoint.nodeId, edge_1.EdgeType.Returns);
}
if (treatAsS3) {
targetVertex.mode ??= [];
if (!targetVertex.mode.includes('s3')) {
targetVertex.mode.push('s3');
}
// collect all next method calls to link them to the same targets!
for (const s of targetVertex.subflow.graph) {
const v = graph.getVertex(s);
if (vertex_1.FunctionCallVertex.is(v) && v.origin.includes(built_in_proc_name_1.BuiltInProcName.S3DispatchNext)) {
collectedNextMethods.add(v.id);
}
}
}
const ingoingRefs = targetVertex.subflow.in;
const remainingIn = [];
for (const ingoing of ingoingRefs) {
const resolved = ingoing.name ? resolve_helper_1.Resolve.byNameAndType(ingoing.name, effectiveEnvironment, ingoing.type) : undefined;
if (resolved === undefined) {
remainingIn.push(ingoing);
continue;
}
const inId = ingoing.nodeId;
(0, log_1.expensiveTrace)(logger_1.dataflowLogger, () => `Found ${resolved.length} references to open ref ${id} in closure of function definition ${id}`);
for (const { nodeId } of resolved) {
if (!node_id_1.NodeId.isBuiltIn(nodeId)) {
graph.addEdge(inId, nodeId, edge_1.EdgeType.DefinedByOnCall);
graph.addEdge(id, nodeId, edge_1.EdgeType.DefinesOnCall);
}
}
}
(0, log_1.expensiveTrace)(logger_1.dataflowLogger, () => `Keeping ${remainingIn.length} references to open ref ${id} in closure of function definition ${id}`);
targetVertex.subflow.in = remainingIn;
const linkedParameters = graph.idMap?.get(target);
if (linkedParameters?.type === type_1.RType.FunctionDefinition) {
(0, linker_1.linkArgumentsOnCall)(args, linkedParameters.parameters, graph);
}
}
for (const nextMethodId of collectedNextMethods) {
for (const target of targets) {
const targetVertex = graph.getVertex(target);
if (vertex_1.UseVertex.is(targetVertex)) {
graph.addEdge(nextMethodId, target, edge_1.EdgeType.Reads);
}
else if (vertex_1.FunctionDefinitionVertex.is(targetVertex)) {
graph.addEdge(nextMethodId, target, edge_1.EdgeType.Calls);
}
}
}
}
}
function parseSysFrameOffset(node) {
if (!node || node.type !== type_1.RType.FunctionCall || !node.named || node.functionName.content !== 'sys.frame' || node.arguments.length !== 1) {
return undefined;
}
const arg = node.arguments[0];
if (arg === r_function_call_1.EmptyArgument || !arg.value) {
return undefined;
}
const v = arg.value;
if (v.type === type_1.RType.Number) {
return v.content.num;
}
if (v.type === type_1.RType.UnaryOp && v.operator === '-' && v.operand.type === type_1.RType.Number) {
return -v.operand.content.num;
}
return undefined;
}
function prepareFunctionEnvironment(data, rootId) {
let env = data.ctx.env.makeCleanEnv();
for (let i = 0; i < data.environment.level + 1 /* add another env */; i++) {
env = (0, scoping_1.pushLocalEnvironment)(env);
if (i === data.environment.level) {
env.current.setClosureNodeId(rootId);
}
}
return { ...data, environment: env };
}
/**
* Within something like `f <- function(a=b, m=3) { b <- 1; a; b <- 5; a + 1 }`
* `a` will be defined by `b` and `b` will be a promise object bound by the first definition of b it can find.
* This means that this function returns `2` due to the first `b <- 1` definition.
* If the code is `f <- function(a=b, m=3) { if(m > 3) { b <- 1; }; a; b <- 5; a + 1 }`, we need a link to `b <- 1` and `b <- 6`
* as `b` can be defined by either one of them.
* <p>
* <b>Currently we may be unable to narrow down every definition within the body as we have not implemented ways to track what covers the first definitions precisely</b>
*/
/** Links a parameter default read to the body writes of the same name (may), returning whether any was linked. */
/** Groups body writes by name, each list sorted by descending id (so the lowest id is last), for `linkParameterReadToBodyWrites`. */
function groupBodyWrites(out) {
const byName = new Map();
for (const o of out) {
if (o.name === undefined) {
continue;
}
const writes = byName.get(o.name);
if (writes === undefined) {
byName.set(o.name, [o]);
}
else {
writes.push(o);
}
}
for (const writes of byName.values()) {
writes.sort((a, b) => String(b.nodeId).localeCompare(String(a.nodeId)));
}
return byName;
}
function linkParameterReadToBodyWrites(graph, read, writesByName) {
const writingOuts = read.name === undefined ? undefined : writesByName.get(read.name);
if (writingOuts === undefined) {
return false;
}
if (writingOuts[0].cds === undefined) {
graph.addEdge(read.nodeId, writingOuts[0].nodeId, edge_1.EdgeType.Reads);
}
else {
for (const { nodeId } of writingOuts) {
graph.addEdge(read.nodeId, nodeId, edge_1.EdgeType.Reads);
}
}
return true;
}
function findPromiseLinkagesForParameters(parameters, readInParameters, parameterEnvs, writesByName) {
// first, we try to bind again within parameters - if we have it, fine
const remainingRead = [];
for (const read of readInParameters) {
const resolved = read.name ? resolve_helper_1.Resolve.byNameAndType(read.name, parameterEnvs, read.type) : undefined;
const rid = read.nodeId;
if (resolved !== undefined) {
for (const { nodeId } of resolved) {
parameters.addEdge(rid, nodeId, edge_1.EdgeType.Reads);
}
continue;
}
// If not resolved, link all outs within the body as potential reads.
if (!linkParameterReadToBodyWrites(parameters, read, writesByName)) {
remainingRead.push(read);
}
}
return remainingRead;
}
//# sourceMappingURL=built-in-function-definition.js.map