UNPKG

@eagleoutice/flowr-dev

Version:

Static Dataflow Analyzer and Program Slicer for the R Programming Language

322 lines 15.6 kB
"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