UNPKG

@eagleoutice/flowr-dev

Version:

Static Dataflow Analyzer and Program Slicer for the R Programming Language

240 lines 11.2 kB
"use strict"; 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