UNPKG

@eagleoutice/flowr-dev

Version:

Static Dataflow Analyzer and Program Slicer for the R Programming Language

575 lines 24.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.REnvironment = exports.GlobalEnvEntryName = exports.SearchPathPackagePrefix = exports.DefaultAttachPosition = exports.Environment = exports.EnvType = void 0; exports.isDefaultBuiltInEnvironment = isDefaultBuiltInEnvironment; exports.builtInEnvJsonReplacer = builtInEnvJsonReplacer; /** * Provides an environment structure similar to R. * @module */ const json_1 = require("../../util/json"); const identifier_1 = require("./identifier"); const assert_1 = require("../../util/assert"); const info_1 = require("../info"); const append_1 = require("./append"); const log_1 = require("../../util/log"); var EnvType; (function (EnvType) { EnvType["Namespace"] = "ns"; EnvType["Imports"] = "imp"; /** `requireNamespace("pkg")`: `pkg::fn` resolves, bare `fn` does not */ EnvType["LoadedNamespace"] = "lns"; })(EnvType || (exports.EnvType = EnvType = {})); /** * Use only if you do not know the object type; otherwise rely on {@link IEnvironment#builtInEnv}. */ function isDefaultBuiltInEnvironment(obj) { return typeof obj === 'object' && obj !== null && (obj.builtInEnv === true); } let environmentIdCounter = 1; // Zero is reserved for built-in environment /** @see REnvironmentInformation */ class Environment { id; /** Optional name for namespaced/non-anonymous environments, please only set if you know what you are doing */ n; /** which search-path layer this env is (package/namespace/imports), if any */ t; /** if created by a closure, the node id of that closure */ c; parent; memory; cache; builtInEnv; /** {@link memory} is shared with a clone; writing needs {@link writableMemory} to unshare it first */ sharedMemory; /** marks the global environment (`.GlobalEnv`); attached packages (see {@link EnvType}) live below it */ globalEnv; constructor(parent, isBuiltInDefault = undefined) { this.id = isBuiltInDefault ? 0 : environmentIdCounter++; this.parent = parent; this.memory = new Map(); // do not store if not needed! if (isBuiltInDefault) { this.builtInEnv = isBuiltInDefault; } } /** Marks this as an attached-package layer (see {@link EnvType}) for package `name`. */ asLibrary(name, type) { this.n = name; this.t = type; return this; } /** Marks this as the global environment (`.GlobalEnv`); see {@link globalEnv}. */ asGlobal() { this.globalEnv = true; return this; } /** please only use if you know what you are doing */ setClosureNodeId(nodeId) { this.c = nodeId; } /** Provides the closure linked to this environment. */ get closure() { return this.c; } /** * This environment's {@link memory}, ready to be written to. Every in-place write must go through this * rather than through {@link memory} directly, as {@link clone} hands the map itself to the clone and only * the first writer of either side copies it (copy-on-write). */ get writableMemory() { if (this.sharedMemory) { this.memory = new Map(this.memory); this.sharedMemory = undefined; } return this.memory; } /** * Create a clone of this environment. * * The clone shares this environment's {@link memory} until either side writes to it (see * {@link writableMemory}); cloning a frame is therefore independent of how many definitions it holds, which * matters because attached packages contribute frames with thousands of them. * @param recurseParents - Whether to also clone parent environments */ clone(recurseParents) { if (this.builtInEnv) { return this; // do not clone the built-in environment } const parent = recurseParents ? this.parent.clone(recurseParents) : this.parent; const clone = new Environment(parent, this.builtInEnv); clone.c = this.c; clone.n = this.n; clone.t = this.t; clone.globalEnv = this.globalEnv; clone.memory = this.memory; clone.sharedMemory = this.sharedMemory = true; return clone; } /** * Define a new identifier definition within this environment. * @param definition - The definition to add. */ define(definition) { const [name, ns] = identifier_1.Identifier.toArray(definition.name); if (ns !== undefined && this.n !== ns) { return this.defineInNamespace(definition, ns); } const newEnvironment = this.clone(false); newEnvironment.apply(name, definition); return newEnvironment; } /** * Define several identifiers at once in a more performant fashion. * @param definitions - The definitions to add. */ defineAll(definitions) { let env = this.clone(false); for (const definition of definitions) { const [name, ns] = identifier_1.Identifier.toArray(definition.name); if (ns !== undefined && env.n !== ns) { env = env.defineInNamespace(definition, ns); } else { env.apply(name, definition); } } return env; } /** Only sound on an environment nobody else holds yet. */ apply(name, definition) { /* isolate the cds from the originating reference, which may still be updated in place */ if (definition.cds !== undefined) { definition = { ...definition, cds: definition.cds.slice() }; } // When there are defined indices, merge the definitions if (definition.cds === undefined) { this.writableMemory.set(name, [definition]); } else { const existing = this.memory.get(name); const inGraphDefinition = definition; if (existing !== undefined && inGraphDefinition.cds === undefined) { this.writableMemory.set(name, [inGraphDefinition]); } else if (existing === undefined || definition.cds === undefined) { this.writableMemory.set(name, [definition]); } else { /* the array may be shared with clones, so replace instead of push */ this.writableMemory.set(name, [...existing, definition]); } } } defineInNamespace(definition, ns) { if (this.n === ns) { return this.define(definition); } // navigate to parent until either before built-in or matching namespace const newEnvironment = this.clone(false); let current = newEnvironment; /* the match has to be re-checked after every step: a post-condition on `current.n` would end the loop on * the very layer it was looking for, before the body could define anything in it */ for (;;) { if (current.n === ns) { /* every layer walked here is a fresh clone, and `apply` copies the shared memory on write, * so this adds to the layer in place -- `define` would clone and the definition would be lost */ current.apply(identifier_1.Identifier.getName(definition.name), definition); return newEnvironment; } else if (current.parent && !current.parent.builtInEnv) { // clone parent current.parent = current.parent.clone(false); current = current.parent; } else { break; } } // we did not find the namespace, so we inject a new environment here log_1.log.warn(`Defining ${identifier_1.Identifier.getName(definition.name)} in namespace ${ns}, which did not exist yet in the environment chain => create (r should fail or we miss attachment).`); const env = new Environment(current.parent); env.n = ns; current.parent = env.define(definition); return newEnvironment; } defineSuper(definition) { const [name, ns] = identifier_1.Identifier.toArray(definition.name); /* isolate the cds from the originating reference, see {@link define} */ if (definition.cds !== undefined) { definition = { ...definition, cds: definition.cds.slice() }; } const newEnvironment = this.clone(false); if (ns !== undefined && this.n !== ns) { newEnvironment.parent = newEnvironment.parent.defineInNamespace(definition, ns); return newEnvironment; } let current = newEnvironment; let last = undefined; let found = false; do { if (current.memory.has(name)) { current.writableMemory.set(name, [definition]); found = true; break; } // `<<-` falls back to the global env, never an attached package below it if (current.globalEnv) { current.writableMemory.set(name, [definition]); found = true; break; } last = current; current.parent = current.parent.clone(false); current = current.parent; } while (!current.builtInEnv); if (!found) { (0, assert_1.guard)(last !== undefined, () => `Could not find global scope for ${name}`); last.writableMemory.set(name, [definition]); } return newEnvironment; } /** * Definitions within `other` replace those here by name; if all of `other`'s are maybe, they are appended instead (turning existing ones maybe too), like {@link appendEnvironment}. Always recurses parents. */ overwrite(other, applyCds) { if (!other || this === other) { return this; } const shortcut = this.mergeShortcut(other); if (shortcut !== undefined) { return shortcut; } const map = new Map(this.memory); for (const [key, values] of other.memory) { const hasMaybe = applyCds === undefined ? values.length === 0 || values.some(v => v.cds !== undefined) : true; if (hasMaybe) { const old = map.get(key); if (!old && applyCds === undefined) { map.set(key, values); continue; } // we need to make a copy to avoid side effects for old reference in other environments const updated = old?.slice() ?? []; for (const v of values) { const { nodeId, definedAt } = v; if (updated.some(o => o.nodeId === nodeId && o.definedAt === definedAt)) { continue; } if (applyCds === undefined) { updated.push(v); } else { updated.push({ ...v, cds: v.cds ? applyCds.concat(v.cds) : applyCds.slice() }); } } map.set(key, updated); } else { map.set(key, values); } } const out = new Environment(this.parent.overwrite(other.parent, applyCds)); out.c = this.c; out.n = this.n; out.t = this.t; out.globalEnv = this.globalEnv; out.memory = map; return out; } /** * Adds all writes of `other` to this environment (`other`'s operations *might* happen). Always recurses parents. */ append(other) { if (!other || this === other) { return this; } const shortcut = this.mergeShortcut(other); if (shortcut !== undefined) { return shortcut; } const map = new Map(this.memory); for (const [key, value] of other.memory) { const old = map.get(key); if (old) { map.set(key, (0, append_1.uniqueMergeValuesInDefinitions)(old, value)); } else { map.set(key, value); } } const out = new Environment(this.parent.append(other.parent)); out.c = this.c; out.n = this.n; out.t = this.t; out.globalEnv = this.globalEnv; out.memory = map; return out; } /** * The environment a merge with `other` settles on without touching either memory, `undefined` if the * memories have to be merged. Package blocks are always unioned, never overwritten or appended to. */ mergeShortcut(other) { if (this.t !== undefined || other.t !== undefined) { return this.mergePackageBlocks(other); } return this.builtInEnv || this.n !== other.n ? this : undefined; } /** * Unions two attached-package blocks, keeping every package once (memory merged for a package in both). */ mergePackageBlocks(other) { const [thisLayers, thisBase] = splitLibraryLayers(this); const [otherLayers, otherBase] = splitLibraryLayers(other); /* * One block is the other plus the packages attached since (blocks grow at the front), so their union is * the longer one and it is already in R's order: the most recently attached package is searched first, * `library(a); library(b)` resolves `b` before `a`. This is the overwhelmingly common case, since every * statement after a `library` call merges two environments that still hold its layers. */ const keep = layersEndWith(thisLayers, otherLayers) ? thisLayers : layersEndWith(otherLayers, thisLayers) ? otherLayers : undefined; if (keep !== undefined) { const base = thisBase.append(otherBase); if (keep === thisLayers && base === thisBase) { return this; } return relinkLayers(keep.map(l => l.clone(false)), base); } /* * Neither block is recognizably an extension of the other, so union them. The longer attach history goes * first so its order survives and the other block only contributes what it alone saw: R searches the most * recently attached package first, and only the longer block still records that order. * Keyed by type and then by name, rather than by a `t:n` string built for every layer of every merge. */ const order = []; const merged = new Map(); for (const layers of thisLayers.length > otherLayers.length ? [thisLayers, otherLayers] : [otherLayers, thisLayers]) { for (const layer of layers) { let byName = merged.get(layer.t); if (byName === undefined) { byName = new Map(); merged.set(layer.t, byName); } const existing = byName.get(layer.n); if (existing === undefined) { const cloned = layer.clone(false); byName.set(layer.n, cloned); order.push(cloned); } else if (existing.memory !== layer.memory) { for (const [name, value] of layer.memory) { const old = existing.memory.get(name); if (old !== value) { existing.writableMemory.set(name, old ? (0, append_1.uniqueMergeValuesInDefinitions)(old, value) : value); } } } } } return relinkLayers(order, thisBase.append(otherBase)); } remove(id) { if (this.builtInEnv) { return this; } const [name, ns] = identifier_1.Identifier.toArray(id); if (ns !== undefined && this.n !== ns) { this.parent.remove(id); return this; } const definition = this.memory.get(name); let cont = true; if (definition !== undefined) { this.writableMemory.delete(name); this.cache?.delete(name); cont = !definition.every(d => (0, info_1.happensInEveryBranch)(d.cds)); } if (cont) { this.parent.remove(name); } return this; } removeAll(names) { if (this.builtInEnv || names.length === 0) { return this; } const newEnv = this.clone(true); // we should optimize this later for (const { name } of names) { newEnv.remove(name); } return newEnv; } toJSON() { return this.builtInEnv ? { id: this.id, parent: this.parent, builtInEnv: this.builtInEnv, memory: this.memory, } : { id: this.id, parent: this.parent, memory: this.memory, // markers needed to rebuild the search path after a round-trip (undefined values are dropped by JSON.stringify) n: this.n, t: this.t, globalEnv: this.globalEnv, }; } } exports.Environment = Environment; /** Walks up to the global environment (see {@link Environment#globalEnv}), falling back to the last non-builtin env. */ function findGlobalEnvironment(env) { let current = env; while (!current.globalEnv && !current.parent.builtInEnv) { current = current.parent; } return current; } /** Walks up to the built-in environment. */ function findBuiltInEnvironment(env) { let current = env; while (!current.builtInEnv) { current = current.parent; } return current; } /** The `search()` position directly below the global environment; where R attaches by default. */ exports.DefaultAttachPosition = 2; /** Prefix of a package's entry in R's `search()` list. */ exports.SearchPathPackagePrefix = 'package:'; /** Name of the global environment in R's `search()` list. */ exports.GlobalEnvEntryName = '.GlobalEnv'; /** * Splices a package block (`blockTop`..`blockBottom`) into the search path at the 1-based `search()` position `pos` * ({@link DefaultAttachPosition|2} being directly below the global environment, the default). A position past the end * of the search path attaches directly above the built-in environment, mirroring R's clamping. Returns a fresh * `current`, cloning only the path down to the insertion point. */ function attachPackageAt(current, blockTop, blockBottom, pos = exports.DefaultAttachPosition) { const clonedCurrent = current.clone(false); let anchor = clonedCurrent; while (!anchor.globalEnv && !anchor.parent.builtInEnv) { anchor.parent = anchor.parent.clone(false); anchor = anchor.parent; } /* walk past the `pos - 2` search entries below the global; an imports layer belongs to the entry above it and is never one itself */ for (let skip = pos - exports.DefaultAttachPosition; skip > 0 && !anchor.parent.builtInEnv; skip--) { do { anchor.parent = anchor.parent.clone(false); anchor = anchor.parent; } while (anchor.parent.t === EnvType.Imports); } blockBottom.parent = anchor.parent; // the built-in env, or the packages attached further down anchor.parent = blockTop; return clonedCurrent; } /** * The 1-based `search()` position of the entry called `name` (`.GlobalEnv`, `package:x`, or a bare package name), * or `undefined` if no such entry is on the search path. `package:base` resolves to the built-in environment at the * very bottom if base R is not attached as its own layer. */ function searchPositionOf(env, name) { const target = name.startsWith(exports.SearchPathPackagePrefix) ? name.slice(exports.SearchPathPackagePrefix.length) : name; if (target === exports.GlobalEnvEntryName) { return 1; } let pos = 1; for (let e = findGlobalEnvironment(env).parent; !e.builtInEnv; e = e.parent) { if (e.t === EnvType.Imports) { continue; // internal layer, not a search-path entry } pos++; if (e.n === target) { return pos; } } return target === "base" /* PkgName.Base */ ? pos + 1 : undefined; // base is the built-in env when it is not attached as a layer } /** * The packages attached below the global environment, i.e. those whose exports R resolves without a namespace. * Base is always among them, as it backs the built-in environment even when it is no layer of its own. */ function attachedPackagesOf(env) { const attached = new Set(["base" /* PkgName.Base */]); for (let e = findGlobalEnvironment(env).parent; !e.builtInEnv; e = e.parent) { if (e.t !== EnvType.Imports && e.n !== undefined) { attached.add(e.n); } } return attached; } /** * Helpers for navigating and manipulating {@link REnvironmentInformation|environments} around the global environment and attached-package search path. */ exports.REnvironment = { name: 'REnvironment', /** Walks up to the global environment (`.GlobalEnv`); see {@link findGlobalEnvironment}. */ findGlobal: findGlobalEnvironment, /** Walks up to the built-in environment; see {@link findBuiltInEnvironment}. */ findBuiltIn: findBuiltInEnvironment, /** Attaches a package block at a `search()` position, below the global by default; see {@link attachPackageAt}. */ attachAt: attachPackageAt, /** The `search()` position of a named entry; see {@link searchPositionOf}. */ searchPosition: searchPositionOf, /** The packages on the search path; see {@link attachedPackagesOf}. */ attachedPackages: attachedPackagesOf, }; /** * Whether the package block `layers` ends with `tail`, i.e. `tail` is `layers` without some of the attachments * made since. A block grows at the front ({@link attachPackageAt} inserts at `search()` position 2), so this is * the shape two branches take when one of them attached more packages than the other. Equal blocks qualify. * * Frames cloned from one another keep the same {@link Environment#memory} map until one of them is written to * (see {@link Environment#clone}), so comparing the maps by identity recognizes untouched blocks without * scanning them; blocks that only happen to hold equal definitions are treated as different, which merely costs * the general union below. */ function layersEndWith(layers, tail) { const offset = layers.length - tail.length; if (offset < 0) { return false; } for (let i = 0; i < tail.length; i++) { const l = layers[offset + i], t = tail[i]; if (l !== t && (l.t !== t.t || l.n !== t.n || l.memory !== t.memory)) { return false; } } return true; } /** Stacks `layers` (top first) on top of `base`, returning the new top of the chain. The layers must not be shared. */ function relinkLayers(layers, base) { let current = base; for (let i = layers.length - 1; i >= 0; i--) { layers[i].parent = current; current = layers[i]; } return current; } /** Splits a package block (a contiguous run of attached-package layers, see {@link EnvType}) into its layers and the env below them. */ function splitLibraryLayers(env) { const layers = []; let current = env; while (current.t !== undefined && !current.builtInEnv) { layers.push(current); current = current.parent; } return [layers, current]; } /** Serializes an environment, replacing the built-in environment with a placeholder. */ function builtInEnvJsonReplacer(k, v) { if (isDefaultBuiltInEnvironment(v)) { return '<BuiltInEnvironment>'; } else { return (0, json_1.jsonReplacer)(k, v); } } //# sourceMappingURL=environment.js.map