@eagleoutice/flowr-dev
Version:
Static Dataflow Analyzer and Program Slicer for the R Programming Language
322 lines • 15.6 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.processExpressionList = processExpressionList;
const info_1 = require("../../../../../info");
const processor_1 = require("../../../../../processor");
const linker_1 = require("../../../../linker");
const assert_1 = require("../../../../../../util/assert");
const unpack_argument_1 = require("../argument/unpack-argument");
const common_1 = require("../common");
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 edge_1 = require("../../../../../graph/edge");
const vertex_1 = require("../../../../../graph/vertex");
const scoping_1 = require("../../../../../environments/scoping");
const overwrite_1 = require("../../../../../environments/overwrite");
const logger_1 = require("../../../../../logger");
const log_1 = require("../../../../../../util/log");
const reference_to_maybe_1 = require("../../../../../environments/reference-to-maybe");
const apply_kill_1 = require("../../../../../environments/apply-kill");
const built_in_proc_name_1 = require("../../../../../environments/built-in-proc-name");
const general_1 = require("../../../../../eval/values/general");
const vertex_2 = require("../../../../../graph/vertex");
const resolve_helper_1 = require("../../../../../environments/resolve-helper");
/**
* Whether the definitions of this list among the `targets` of a read cover every branch, alone or together.
* Only then the read is resolved here and must not bubble up as an ingoing reference.
*/
function coveredByListDefinitions(targets, listEnvironments) {
let cds;
for (const target of targets) {
if (!listEnvironments.has(target.nodeId)) {
continue;
}
else if (target.cds === undefined) {
return true;
}
(cds ??= []).push(...target.cds);
}
return cds !== undefined && (0, info_1.happensInEveryBranch)(cds);
}
function linkReadNameToWriteIfPossible(read, environments, listEnvironments, remainingRead, nextGraph) {
const readName = read.name && identifier_1.Identifier.isDotDotDotAccess(read.name) ? identifier_1.Identifier.dotdotdot() : read.name;
const probableTarget = readName ? resolve_helper_1.Resolve.byNameAndType(readName, environments, read.type) : undefined;
if (probableTarget === undefined || !coveredByListDefinitions(probableTarget, listEnvironments)) {
const readId = readName ? identifier_1.Identifier.getName(readName) : undefined;
const has = remainingRead.get(readId);
if (has) {
if (!has.some(h => h.nodeId === read.nodeId && h.name === read.name && h.cds === read.cds)) {
has.push(read);
}
}
else {
remainingRead.set(readId, [read]);
}
}
// keep it, for we have no target, as read-ids are unique within the same fold, this should work for same links
// we keep them if they are defined outside the current parent and maybe throw them away later
if (probableTarget === undefined) {
return;
}
const rid = read.nodeId;
const isFunc = read.type === identifier_1.ReferenceType.Function || read.type === identifier_1.ReferenceType.BuiltInFunction;
for (const target of probableTarget) {
const tid = target.nodeId;
if (node_id_1.NodeId.isBuiltIn(target.definedAt) && isFunc) {
nextGraph.addEdge(rid, tid, edge_1.EdgeType.Reads | edge_1.EdgeType.Calls);
}
else {
nextGraph.addEdge(rid, tid, edge_1.EdgeType.Reads);
}
if (target.type === identifier_1.ReferenceType.BuiltInConstant) {
nextGraph.addVertex({
tag: vertex_1.VertexType.Value,
id: tid,
cds: undefined,
value: (0, general_1.valueFromTsValue)((target).value)
}, environments, false);
}
}
}
/**
* Expands the directly-called function definitions (`direct`) with those they transitively call (resolved by name in
* `resolveEnv`). A sibling/outer callee is invisible inside its caller's (clean) body, so its escaped globals never fold
* into the caller's subflow; pulling them here lets a super-assignment escaping through several call levels reach the
* outer read. Escapes are folded popped to the fold level, so a write binding an intermediate frame stops there.
* Package attachments keep their own (lazy) propagation, so transitive callees only contribute their `<<-` escapes.
*/
function* transitivelyCalledDefinitions(initial, graph, resolveEnv) {
const seen = new Set();
const stack = initial.map(fn => ({ fn, direct: true }));
while (stack.length > 0) {
const { fn, direct } = stack.pop();
if (!vertex_2.FunctionDefinitionVertex.is(fn) || seen.has(fn.id)) {
continue;
}
seen.add(fn.id);
yield { fn, direct };
for (const nodeId of fn.subflow.graph) {
const call = graph.getVertex(nodeId);
if (!vertex_2.FunctionCallVertex.is(call) || call.onlyBuiltin || call.name === undefined) {
continue;
}
const resolved = resolve_helper_1.Resolve.byNameAndType(call.name, resolveEnv, identifier_1.ReferenceType.Function);
if (resolved === undefined) {
continue;
}
const [targets] = (0, linker_1.getAllLinkedFunctionDefinitions)(new Set(resolved.map(r => r.nodeId)), graph);
for (const target of targets) {
if (!seen.has(target.id)) {
stack.push({ fn: target, direct: false });
}
}
}
}
}
/** Rebuilds `env` without its attached-package/namespace layers (`t !== undefined`), keeping only the lexical frames a `<<-` can bind. */
function withoutPackageLayers(env) {
let builtIn = env.current;
const core = [];
let hasPackage = false;
while (!builtIn.builtInEnv) {
if (builtIn.t === undefined) {
core.push(builtIn);
}
else {
hasPackage = true;
}
builtIn = builtIn.parent;
}
if (!hasPackage) {
return env;
}
let parent = builtIn;
for (let i = core.length - 1; i >= 0; i--) {
const cloned = core[i].clone(false);
cloned.parent = parent;
parent = cloned;
}
return { current: parent, level: env.level };
}
function updateSideEffectsForCalledFunctions(calledEnvs, inputEnvironment, nextGraph, localDefs) {
for (const { functionCall, called } of calledEnvs) {
let callDependencies = null;
for (const { fn: calledFn, direct } of transitivelyCalledDefinitions(called, nextGraph, inputEnvironment)) {
(0, assert_1.guard)(vertex_2.FunctionDefinitionVertex.is(calledFn), 'called function must be a function definition');
// only merge the environments they have in common
let environment = direct ? calledFn.subflow.environment : withoutPackageLayers(calledFn.subflow.environment);
while (environment.level > inputEnvironment.level) {
environment = (0, scoping_1.popLocalEnvironment)(environment);
}
// update alle definitions to be defined at this function call
let current = environment.current;
let hasUpdate = false;
while (!current?.builtInEnv) {
// a package attached inside the body (library()) must propagate to the caller, like R attaching globally
if (current.t !== undefined) {
hasUpdate = true;
}
for (const definitions of current.memory.values()) {
for (const def of definitions) {
if (!node_id_1.NodeId.isBuiltIn(def.definedAt)) {
hasUpdate = true;
nextGraph.addEdge(def.nodeId, functionCall, edge_1.EdgeType.SideEffectOnCall);
}
}
}
current = current.parent;
}
if (hasUpdate) {
// we update all definitions to be linked with the corresponding function call
// we, however, have to ignore expression-local writes!
if (localDefs.length > 0) {
environment = {
current: environment.current.removeAll(localDefs.filter(d => (0, assert_1.isNotUndefined)(d.name))),
level: environment.level
};
}
if (callDependencies === null) {
callDependencies = nextGraph.getVertex(functionCall)?.cds;
}
inputEnvironment = (0, overwrite_1.overwriteEnvironment)(inputEnvironment, environment, callDependencies);
}
}
}
return inputEnvironment;
}
/**
* Processes a list of expressions joining their dataflow graphs accordingly.
*/
function processExpressionList(name, args, rootId, data) {
(0, log_1.expensiveTrace)(logger_1.dataflowLogger, () => `[expr list] with ${args.length} expressions`);
let { environment } = data;
// used to detect if a "write" happens within the same expression list
const listEnvironments = new Set();
const remainingRead = new Map();
const nextGraph = new graph_1.DataflowGraph(data.completeAst.idMap);
const out = [];
/* lazily created - `rm` is rare, so rm-free lists never allocate this */
let killed;
const exitPoints = [];
const activeCdsAtStart = data.cds;
const invertExitCds = [];
const processedExpressions = [];
let defaultReturnExpr = undefined;
let hooks;
for (const arg of args) {
const expression = (0, unpack_argument_1.unpackNonameArg)(arg);
if (expression === undefined) {
processedExpressions.push(undefined);
continue;
}
// use the current environments for processing
data.environment = environment;
const processed = (0, processor_1.processDataflowFor)(expression, data);
processedExpressions.push(processed);
nextGraph.mergeWith(processed.graph);
defaultReturnExpr = processed;
// if the expression contained next or break anywhere before the next loop, the "overwrite" should be an "append", because we do not know if the rest is executed
// update the environments for the next iteration with the previous writes
if (exitPoints.length > 0) {
processed.out = (0, reference_to_maybe_1.makeAllMaybe)(processed.out, nextGraph, processed.environment, true, invertExitCds);
processed.in = (0, reference_to_maybe_1.makeAllMaybe)(processed.in, nextGraph, processed.environment, false, invertExitCds);
processed.unknownReferences = (0, reference_to_maybe_1.makeAllMaybe)(processed.unknownReferences, nextGraph, processed.environment, false);
}
if (processed.hooks.length > 0) {
(hooks ??= []).push(...processed.hooks);
}
// all inputs that have not been written until now are read!
for (const read of processed.in) {
linkReadNameToWriteIfPossible(read, environment, listEnvironments, remainingRead, nextGraph);
}
for (const read of processed.unknownReferences) {
linkReadNameToWriteIfPossible(read, environment, listEnvironments, remainingRead, nextGraph);
}
const calledEnvs = (0, linker_1.linkFunctionCalls)(nextGraph, data.completeAst.idMap, processed.graph);
for (const c of calledEnvs) {
if (c.propagateExitPoints.length > 0) {
for (const exit of c.propagateExitPoints) {
processed.exitPoints.push(exit);
}
}
}
(0, info_1.addNonDefaultExitPoints)(exitPoints, invertExitCds, activeCdsAtStart, processed.exitPoints);
environment = exitPoints.length > 0 ? (0, overwrite_1.overwriteEnvironment)(environment, processed.environment) : processed.environment;
// if the called function has global redefinitions, we have to keep them within our environment
environment = updateSideEffectsForCalledFunctions(calledEnvs, environment, nextGraph, processed.out);
// removals are already reflected in the threaded environment; we only bubble them (net of later writes)
if (killed && processed.out.length > 0) {
killed = (0, apply_kill_1.cancelRevivedKills)(killed, processed.out);
}
if (processed.kill?.length) {
// if we may have already exited (break/next), the removal only happens maybe
const kills = exitPoints.length > 0 ? (0, apply_kill_1.makeKillsMaybe)(processed.kill, invertExitCds) : processed.kill;
killed ??= [];
for (const kill of kills) {
killed.push(kill);
}
}
for (const ref of processed.out) {
out.push(ref);
listEnvironments.add(ref.nodeId);
}
/** if at least built-one of the exit points encountered happens unconditionally, we exit here (dead code)! */
if ((0, info_1.alwaysExits)(processed)) {
/* if there is an always-exit expression, there is no default return active anymore */
defaultReturnExpr = undefined;
break;
}
}
if (defaultReturnExpr) {
exitPoints.push(data.cds ? {
type: 0 /* ExitPointType.Default */,
nodeId: defaultReturnExpr.entryPoint,
cds: data.cds
} : {
type: 0 /* ExitPointType.Default */,
nodeId: defaultReturnExpr.entryPoint
});
}
const ingoing = [];
for (const refs of remainingRead.values()) {
for (const ref of refs) {
ingoing.push(ref);
}
}
const rootNode = data.completeAst.idMap.get(rootId);
const withGroup = rootNode?.grouping;
if (withGroup) {
ingoing.push({ nodeId: rootId, name: name.content, cds: data.cds, type: identifier_1.ReferenceType.Function });
(0, common_1.patchFunctionCall)({
nextGraph,
rootId,
name,
data,
argumentProcessResult: processedExpressions,
origin: built_in_proc_name_1.BuiltInProcName.ExpressionList
});
nextGraph.addEdge(rootId, node_id_1.NodeId.toBuiltIn('{'), edge_1.EdgeType.Reads | edge_1.EdgeType.Calls);
// process all exit points as potential returns:
for (const exit of exitPoints) {
if (exit.type === 1 /* ExitPointType.Return */ || exit.type === 0 /* ExitPointType.Default */) {
nextGraph.addEdge(rootId, exit.nodeId, edge_1.EdgeType.Returns);
}
}
}
const meId = withGroup ? rootId : (processedExpressions.find(assert_1.isNotUndefined)?.entryPoint ?? rootId);
return {
/* no active nodes remain, they are consumed within the remaining read collection */
unknownReferences: [],
in: ingoing,
out,
environment: environment,
graph: nextGraph,
/* if we have no group, we take the last evaluated expr */
entryPoint: meId,
exitPoints: exitPoints,
hooks: hooks ?? [],
kill: killed,
};
}
//# sourceMappingURL=built-in-expression-list.js.map