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