@eagleoutice/flowr-dev
Version:
Static Dataflow Analyzer and Program Slicer for the R Programming Language
240 lines • 11.2 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.BuiltInIndex = void 0;
exports.queryFnProps = queryFnProps;
exports.callFnProps = callFnProps;
exports.inferFnProps = inferFnProps;
exports.builtInNames = builtInNames;
const built_in_props_1 = require("./built-in-props");
const default_builtin_config_1 = require("./default-builtin-config");
const environment_1 = require("./environment");
const identifier_1 = require("./identifier");
const node_id_1 = require("../../r-bridge/lang-4.x/ast/model/processing/node-id");
const resolve_helper_1 = require("./resolve-helper");
const vertex_1 = require("../graph/vertex");
const df_helper_1 = require("../graph/df-helper");
/**
* What flowR states about the package export `definition` binds, `undefined` if it binds none or if flowR
* states nothing about it. `library(pkg)` binds every export itself, and that binding hides the built-in layer.
*/
function statedBuiltIn(definition, environment) {
const pkgFn = definition.type === identifier_1.ReferenceType.Function ? node_id_1.NodeId.toPkgFn(definition.nodeId) : undefined;
if (pkgFn === undefined) {
return undefined;
}
const [pkg, fn] = pkgFn;
const known = environment_1.REnvironment.findBuiltIn(environment.current).memory.get(fn)
?.filter(d => d.type === identifier_1.ReferenceType.BuiltInFunction && identifier_1.Identifier.getNamespace(d.name ?? '') === pkg);
return known?.length ? known : undefined;
}
/** `definitions` with every package export replaced by what flowR states about it, the array itself if none is one. */
function withStatedBuiltIns(definitions, environment) {
let replaced;
for (const [at, definition] of definitions.entries()) {
const stated = statedBuiltIn(definition, environment);
if (stated === undefined) {
replaced?.push(definition);
}
else {
replaced ??= definitions.slice(0, at);
replaced.push(...stated);
}
}
return replaced ?? definitions;
}
function ofDefinitions(definitions) {
let sig, props;
for (const d of definitions ?? []) {
if (d.type !== identifier_1.ReferenceType.BuiltInFunction) {
continue;
}
const info = d.config;
sig ??= info?.sig;
props = info?.props === undefined ? props : (props ?? 0) | info.props;
}
return sig === undefined && props === undefined ? undefined : { sig, props };
}
/**
* What flowR knows about the function `name`: what the built-in definitions state, filled up with what the
* signature database knows (see {@link fnInfoFromSignature}). A declared signature wins, the properties of
* both are joined. `undefined` if neither has anything to say, and also if the name resolves to a definition
* in the analyzed code, which shadows any built-in.
*/
function queryFnProps(name, { environment, builtIns, signatures, version }) {
let info;
if (environment !== undefined) {
const resolved = resolve_helper_1.Resolve.byNameAndType(name, environment, identifier_1.ReferenceType.Function);
const stated = resolved === undefined ? undefined : withStatedBuiltIns(resolved, environment);
if (stated?.some(d => d.type !== identifier_1.ReferenceType.BuiltInFunction)) {
return undefined;
}
info = ofDefinitions(stated);
}
else if (builtIns !== undefined) {
info = ofDefinitions(builtIns.builtInMemory.get(identifier_1.Identifier.getName(name)));
}
const pkg = identifier_1.Identifier.getNamespace(name);
if (signatures === undefined || pkg === undefined) {
return info;
}
const known = inferFnProps(signatures, pkg, identifier_1.Identifier.getName(name), version);
if (known === undefined) {
return info;
}
return { sig: info?.sig ?? known.sig, props: (info?.props ?? 0) | (known.props ?? 0) };
}
/** What flowR states about the call `id` makes, together with the name the call resolved to. */
function callFnProps(id, { graph, environment }) {
const vertex = graph.getVertex(id);
if (!vertex_1.FunctionCallVertex.is(vertex)) {
return undefined;
}
/* what the call resolved to decides, as a definition in the analyzed code shadows the built-in; a call
* flowR settled on the built-ins keeps no environment, and a later redefinition must not speak for it */
const known = vertex.environment ?? environment;
const name = df_helper_1.Dataflow.qualify(id, graph, false) ?? vertex.name;
return {
name,
...queryFnProps(name, {
environment: vertex.onlyBuiltin ? { level: 0, current: environment_1.REnvironment.findBuiltIn(known.current) } : known
})
};
}
/**
* What the signature database implies for a package function: what its own entry states
* (see {@link fnInfoFromSignature}) plus the {@link PropagatedProps} of everything it calls, transitively.
* A package function that ends up in `system()` runs a system command as well.
*/
function inferFnProps(src, pkg, name, version) {
const fn = src.functionByName(pkg, name, version);
if (fn === undefined) {
return undefined;
}
const own = (0, built_in_props_1.fnInfoFromSignature)(fn);
const known = BuiltInIndex.default();
let props = own.props ?? 0;
for (const callee of src.transitiveCallees(pkg, name, version) ?? fn.callees) {
props |= (known.propsOf(callee) ?? 0) & built_in_props_1.PropagatedProps;
}
return { sig: own.sig, props };
}
/** The identifiers a definition registers, with the suffixes of a replacement spelled out. */
function builtInNames(definition) {
if (definition.type !== 'replacement') {
return definition.names;
}
return definition.names.flatMap(n => definition.suffixes.map(s => identifier_1.Identifier.make(`${identifier_1.Identifier.getName(n)}${s}`, identifier_1.Identifier.getNamespace(n))));
}
function entryOfDefinition(definition) {
if (definition.type === 'constant') {
return [];
}
const info = definition.config;
const folds = definition.type === 'function' && definition.evalHandler !== undefined;
return builtInNames(definition).map(name => ({ name, props: info?.props, sig: info?.sig, folds }));
}
function entriesOfMemory(builtIns) {
const out = [];
for (const [registered, definitions] of builtIns.builtInMemory) {
for (const d of definitions) {
if (d.type !== identifier_1.ReferenceType.BuiltInFunction) {
continue;
}
const info = d.config;
/* the memory is keyed by the bare name, the definition keeps the namespace it was declared with */
out.push({ name: d.name ?? registered, props: info?.props, sig: info?.sig, folds: d.evalHandler !== undefined });
}
}
return out;
}
let defaultIndex;
/**
* The one place to ask what flowR's built-ins are: _every pure function_, _every call that reads a file_,
* _every parameter that names a resource_, _everything the value solver can fold_. Each answer is derived
* from the {@link BuiltInFnInfo} the definitions carry, so a built-in that states its {@link CallProp} bits and
* its {@link FnSig} is found here without anything else being registered.
*
* Build one over the {@link DefaultBuiltinConfig} with {@link BuiltInIndex.default} (computed once and shared),
* over your own definitions with {@link BuiltInIndex.of}, or over the built-ins an analysis actually registered
* with {@link BuiltInIndex.ofEnvironment}, which reflects configured overrides. For a single name (where a
* definition in the analyzed code shadows the built-in) use {@link queryFnProps} instead.
*/
class BuiltInIndex {
entries;
byName = new Map();
cache = new Map();
constructor(entries) {
this.entries = entries;
for (const e of entries) {
this.byName.set(identifier_1.Identifier.getName(e.name), e);
}
}
/** The index of flowR's own {@link DefaultBuiltinConfig}, computed on first use and shared from then on. */
static default() {
return defaultIndex ??= BuiltInIndex.of(default_builtin_config_1.DefaultBuiltinConfig);
}
/** The index of a set of built-in definitions, e.g. the ones a flowR config adds. */
static of(definitions) {
return new BuiltInIndex(definitions.flatMap(entryOfDefinition));
}
/** The index of the built-ins an analysis registered, so a configured or overwritten built-in is what shows up. */
static ofEnvironment(builtIns) {
return new BuiltInIndex(entriesOfMemory(builtIns));
}
/** answers are keyed by what was asked, as every caller asks the same handful of questions over and over */
cached(key, filter) {
let found = this.cache.get(key);
if (found === undefined) {
this.cache.set(key, found = this.entries.filter(filter).map(e => e.name));
}
return found;
}
/** Every built-in whose props carry at least one bit of `props`, like {@link CallProp.File} for the file calls. */
with(props) {
return this.cached(`with:${props}`, e => ((e.props ?? 0) & props) !== 0);
}
/**
* Every built-in whose props carry *every* bit of `props`, for the questions a single bit cannot answer,
* like {@link FileInputProps} for the calls that read a file rather than only write one.
*/
withAll(props) {
return this.cached(`all:${props}`, e => e.props !== undefined && (e.props & props) === props);
}
/**
* Every built-in that states its props but carries no bit of `props`. With {@link InputProps} this yields
* the calls that derive their result from their arguments alone.
*/
without(props) {
return this.cached(`without:${props}`, e => e.props !== undefined && (e.props & props) === 0);
}
/** Every built-in flowR states computes a result and nothing else ({@link CallProp.Pure}). */
get pure() {
return this.with(built_in_props_1.CallProp.Pure);
}
/** Every built-in the value solver can fold to a constant (the ones with a `evalHandler`). */
get folding() {
return this.cached('folding', e => e.folds);
}
/** Every parameter whose argument carries at least one bit of `props`, like {@link ArgProp.Resource}. */
params(props) {
const found = [];
for (const e of this.entries) {
e.sig?.forEach(([name, p], index) => {
if ((p & props) !== 0) {
found.push({ call: e.name, index, name, props: p });
}
});
}
return found;
}
/** What the index states about `name`, ignoring any namespace (built-ins are registered by their bare name). */
get(name) {
return this.byName.get(identifier_1.Identifier.getName(name));
}
/** The {@link CallProp} bits of `name`, `undefined` when no built-in of that name states any. */
propsOf(name) {
return this.get(name)?.props;
}
}
exports.BuiltInIndex = BuiltInIndex;
//# sourceMappingURL=query-fn-props.js.map