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