UNPKG

@eagleoutice/flowr-dev

Version:

Static Dataflow Analyzer and Program Slicer for the R Programming Language

300 lines 13.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.computeCallGraphSummaries = computeCallGraphSummaries; exports.linkMaterializedExportsToLoaders = linkMaterializedExportsToLoaders; exports.reResolveOpenReferences = reResolveOpenReferences; exports.propagateTransitiveSideEffects = propagateTransitiveSideEffects; const edge_1 = require("../../../../../graph/edge"); const node_id_1 = require("../../../../../../r-bridge/lang-4.x/ast/model/processing/node-id"); const environment_1 = require("../../../../../environments/environment"); const built_in_library_1 = require("./built-in-library"); const define_1 = require("../../../../../environments/define"); const vertex_1 = require("../../../../../graph/vertex"); const resolve_helper_1 = require("../../../../../environments/resolve-helper"); const graph_1 = require("../../../../../graph/graph"); /** * The function-definition vertices a `call` resolves to (via {@link EdgeType.Calls}). */ function calledDefinitions(graph, call) { const targets = []; for (const [target, edge] of graph.outgoingEdges(call) ?? graph_1.NoEdges) { if (edge_1.DfEdge.includesType(edge, edge_1.EdgeType.Calls) && vertex_1.FunctionDefinitionVertex.is(graph.getVertex(target))) { targets.push(target); } } return targets; } /** * Computes, for every function-definition vertex `F`, `summary(F) = own(F)` unioned with `summary(G)` over every transitive callee `G`. * @param graph - the fully linked dataflow graph * @param own - the effects a single function definition produces itself (its contribution to the summary) * @returns a map from each function-definition vertex to its transitive-effect summary * @useInstead {@link Dataflow.sideEffects.callGraphSummaries} */ function computeCallGraphSummaries(graph, own) { const summary = new Map(); const callees = new Map(); for (const [id, vertex] of graph.vertices(true)) { if (!vertex_1.FunctionDefinitionVertex.is(vertex)) { continue; } summary.set(id, new Set(own(id, vertex))); const targets = []; for (const node of vertex.subflow.graph) { if (vertex_1.FunctionCallVertex.is(graph.getVertex(node))) { targets.push(...calledDefinitions(graph, node)); } } callees.set(id, targets); } if (summary.size === 0) { return summary; } // reverse edges: callers of each function const callers = new Map(); for (const [id, targets] of callees) { for (const callee of targets) { const existing = callers.get(callee); if (existing === undefined) { callers.set(callee, [id]); } else { existing.push(id); } } } // worklist fixpoint: pull each callee's summary, re-enqueue callers when a function grew const queue = [...summary.keys()]; const queued = new Set(queue); while (queue.length > 0) { const id = queue.pop(); queued.delete(id); const effects = summary.get(id); let grew = false; for (const callee of callees.get(id) ?? []) { for (const effect of summary.get(callee) ?? []) { if (!effects.has(effect)) { effects.add(effect); grew = true; } } } if (grew) { for (const caller of callers.get(id) ?? []) { if (!queued.has(caller)) { queued.add(caller); queue.push(caller); } } } } return summary; } /** * Links every materialized package-export vertex back to the `library()`/`use()` call that loaded it, * reading the loading call from the export binding's `definedAt` in the (final, authoritative) environment. * Only vertices created on demand (exports actually referenced) get an edge, keeping the graph small. */ function linkMaterializedExportsToLoaders(graph, environment) { // export node id -> its loading `library()`/`use()` call, from the final environment const loaders = new Map(); for (let e = environment.current; e !== undefined && !e.builtInEnv; e = e.parent) { if (e.t !== environment_1.EnvType.Namespace) { continue; } for (const defs of e.memory.values()) { for (const d of defs) { if (!node_id_1.NodeId.isBuiltIn(d.definedAt)) { loaders.set(d.nodeId, d.definedAt); } } } } // only exports actually called (a materialized vertex or the target of a `calls` edge) get a loader edge for (const [id, loadedAt] of loaders) { const called = graph.hasVertex(id) || [...graph.ingoingEdges(id)?.values() ?? []].some(e => edge_1.DfEdge.includesType(e, edge_1.EdgeType.Calls)); if (called) { graph.addEdge(id, loadedAt, edge_1.EdgeType.Reads | edge_1.EdgeType.Calls); } } } /** The packages (see {@link EnvType}) attached directly within a function body, i.e. its own `library()` calls. */ function attachedPackages(_id, fdef) { const packages = []; for (let e = fdef.subflow.environment.current; e !== undefined && !e.builtInEnv; e = e.parent) { if (e.t === environment_1.EnvType.Namespace && e.n !== undefined) { // an export binding carries the loading `library()` call in `definedAt` let definedAt = node_id_1.NodeId.toBuiltIn(e.n); for (const defs of e.memory.values()) { const real = defs.find(d => !node_id_1.NodeId.isBuiltIn(d.definedAt)); if (real !== undefined) { definedAt = real.definedAt; break; } } packages.push({ pack: e.n, definedAt }); } } return packages; } /** The scopes outside a function's own frame, i.e. those a definition of its body can have escaped into. */ function* escapeTargetFrames(fdef) { for (let e = fdef.subflow.environment.current.parent; e !== undefined && !e.builtInEnv; e = e.parent) { yield e; } } /** * The non-built-in definitions a function body lets escape to an outer scope (e.g. a `<<-` super-assignment), * as a callback for {@link computeCallGraphSummaries}. Nested definitions share their outer frames, and a frame is * dominated by the built-ins it holds, so filtering each frame once per pass saves the bulk of the work. */ function escapedDefinitions() { const perFrame = new Map(); return (_id, fdef) => { const defs = []; for (const e of escapeTargetFrames(fdef)) { let escaped = perFrame.get(e.memory); if (escaped === undefined) { escaped = [...e.memory.values()].flatMap(ds => ds.filter(d => !node_id_1.NodeId.isBuiltIn(d.nodeId)).map(d => d.nodeId)); perFrame.set(e.memory, escaped); } for (const d of escaped) { defs.push(d); } } return defs; }; } /** Emits {@link EdgeType.SideEffectOnCall} edges for definitions that escape a function transitively. */ function propagateTransitiveDefinitions(graph, environment, ctx) { const summary = computeCallGraphSummaries(graph, escapedDefinitions()); for (const [id, vertex] of graph.vertices(true)) { if (!vertex_1.FunctionCallVertex.is(vertex)) { continue; } for (const target of calledDefinitions(graph, id)) { for (const def of summary.get(target) ?? []) { // an escaping package export is only in the environment; materialize its vertex on demand if (node_id_1.NodeId.isBuiltIn(def)) { (0, built_in_library_1.attachExportVertex)(graph, def, environment, ctx); } graph.addEdge(def, id, edge_1.EdgeType.SideEffectOnCall); } } } } /** * Attaches packages transitively loaded by top-level calls to `environment`. * `g <- function() library(A); f <- function() g(); f()` makes `A` available after `f()`. * @returns the enriched environment and whether it grew (so the caller can re-link and re-run to a fixpoint). */ function propagateTransitivePackages(graph, environment, ctx) { const summary = computeCallGraphSummaries(graph, attachedPackages); const reachable = new Map(); // package -> its loading `library()` call for (const [id, vertex] of graph.vertices(true)) { if (!vertex_1.FunctionCallVertex.is(vertex) || !graph.isRoot(id)) { continue; } for (const target of calledDefinitions(graph, id)) { for (const { pack, definedAt } of summary.get(target) ?? []) { if (!reachable.has(pack)) { reachable.set(pack, definedAt); } } } } let grew = false; for (const [pack, definedAt] of reachable) { const dependency = ctx.deps.getDependency(pack); if (dependency === undefined) { continue; } const next = (0, built_in_library_1.attachDependencyToEnvironment)(dependency, environment, ctx, {}, definedAt); if (next !== environment) { environment = next; grew = true; } } return { environment, grew }; } /** Maps each escaped definition's node id to its full (name-carrying) definition. */ function escapedDefinitionMap(graph) { const map = new Map(); const seen = new Set(); for (const [, vertex] of graph.vertices(true)) { if (!vertex_1.FunctionDefinitionVertex.is(vertex)) { continue; } for (const e of escapeTargetFrames(vertex)) { if (seen.has(e.memory)) { continue; // a frame shared with an already-visited definition contributes the same entries } seen.add(e.memory); for (const definitions of e.memory.values()) { for (const def of definitions) { if (!node_id_1.NodeId.isBuiltIn(def.nodeId) && def.name !== undefined) { map.set(def.nodeId, def); } } } } } return map; } /** * Folds the `<<-` definitions that escape transitively from top-level calls into `environment`. * `f <- function() x <<- 1; g <- function() f(); g(); print(x)` makes `x` resolvable. * @returns the enriched environment and whether it grew (so the extractor can re-resolve open reads and re-run). */ function propagateTransitiveEscapedDefinitions(graph, environment) { const summary = computeCallGraphSummaries(graph, escapedDefinitions()); const defs = escapedDefinitionMap(graph); const names = new Set(); let grew = false; for (const [id, vertex] of graph.vertices(true)) { if (!vertex_1.FunctionCallVertex.is(vertex) || !graph.isRoot(id)) { continue; } for (const target of calledDefinitions(graph, id)) { for (const nodeId of summary.get(target) ?? []) { const def = defs.get(nodeId); if (def === undefined) { continue; } names.add(def.name); if (resolve_helper_1.Resolve.byNameAndType(def.name, environment, def.type)?.some(d => d.nodeId === nodeId)) { continue; } environment = (0, define_1.define)(def, false, environment); grew = true; } } } return { environment, grew, names }; } /** Re-resolves still-open reads whose name is one of the transitively escaped `<<-` definitions in `escapedNames`, adding {@link EdgeType.Reads} edges. */ function reResolveOpenReferences(graph, environment, references, escapedNames) { for (const ref of references) { if (ref.name === undefined || !escapedNames.has(String(ref.name))) { continue; } for (const { nodeId } of resolve_helper_1.Resolve.byNameAndType(ref.name, environment, ref.type) ?? []) { if (!node_id_1.NodeId.isBuiltIn(nodeId) && nodeId !== ref.nodeId) { graph.addEdge(ref.nodeId, nodeId, edge_1.EdgeType.Reads); } } } } /** * Propagates every function's escaped side effects (attached packages and `<<-` definitions) to its transitive callers. * @returns the enriched top-level environment and whether it grew (so the extractor can re-link and re-run to a fixpoint). * @useInstead {@link Dataflow.sideEffects.propagateTransitive} */ function propagateTransitiveSideEffects(graph, environment, ctx) { propagateTransitiveDefinitions(graph, environment, ctx); const packages = propagateTransitivePackages(graph, environment, ctx); const escaped = propagateTransitiveEscapedDefinitions(graph, packages.environment); return { environment: escaped.environment, grew: packages.grew || escaped.grew, escapedNames: escaped.names }; } //# sourceMappingURL=transitive-side-effects.js.map