@eagleoutice/flowr-dev
Version:
Static Dataflow Analyzer and Program Slicer for the R Programming Language
195 lines • 9.44 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.Nse = exports.Unquote = void 0;
const model_1 = require("../../../../../r-bridge/lang-4.x/ast/model/model");
const r_unary_op_1 = require("../../../../../r-bridge/lang-4.x/ast/model/nodes/r-unary-op");
const r_function_call_1 = require("../../../../../r-bridge/lang-4.x/ast/model/nodes/r-function-call");
const identifier_1 = require("../../../../environments/identifier");
const data_masking_functions_1 = require("../../../../environments/data-masking-functions");
const graph_1 = require("../../../../graph/graph");
const vertex_1 = require("../../../../graph/vertex");
const edge_1 = require("../../../../graph/edge");
/** The escape a quoting function offers, a property of the function: `quote(!!x)` negates, `expr(!!x)` splices. */
var Unquote;
(function (Unquote) {
/** no escape at all, everything within the argument stays unevaluated */
Unquote["None"] = "none";
/** rlang's `!!` and `!!!` splice in the value of their operand */
Unquote["Rlang"] = "rlang";
/** `bquote`'s `.(x)` evaluates its operand */
Unquote["Bquote"] = "bquote";
})(Unquote || (exports.Unquote = Unquote = {}));
/** `!!x` parses as `!(!x)`, so peel the run of `!` to reach the operand. */
function unquotedOperandOf(node, style) {
if (style === Unquote.Bquote) {
if (!r_function_call_1.RFunctionCall.isNamed(node) || identifier_1.Identifier.getName(node.functionName.content) !== '.' || node.arguments.length !== 1) {
return undefined;
}
const arg = node.arguments[0];
return arg === r_function_call_1.EmptyArgument ? undefined : arg.value;
}
if (!isNegation(node) || !isNegation(node.operand)) {
return undefined;
}
let operand = node.operand;
while (isNegation(operand)) {
operand = operand.operand;
}
return operand;
}
function isNegation(node) {
return r_unary_op_1.RUnaryOp.is(node) && node.operator === '!';
}
/**
* Whether the definition is a bare function: the vertex itself, or a variable bound straight to a function
* definition. A binding that anything was done to on the way (`structure(f, class = "cls")`, `class(f) <- `)
* is not one, and deliberately so: such a closure carries a class, `>` may dispatch on it, and flowR cannot
* tell that it does not. Only where nothing at all can be attached is a function ruled out as an operand.
*/
function definesAFunction(graph, id) {
const vertex = graph.getVertex(id);
if (vertex_1.FunctionDefinitionVertex.is(vertex)) {
return true;
}
else if (!vertex_1.VariableDefinitionVertex.is(vertex)) {
return false;
}
for (const [target, edge] of graph.outgoingEdges(id) ?? graph_1.NoEdges) {
if (edge_1.DfEdge.includesType(edge, edge_1.EdgeType.DefinedBy) && vertex_1.FunctionDefinitionVertex.is(graph.getVertex(target))) {
return true;
}
}
return false;
}
/**
* Whether `id` reads something and everything it reads is a bare function. In a data mask such a name is a
* column after all: R asks the data first, and a plain closure is not what `id > 2` compares. A closure that
* was given a class is left alone, see {@link definesAFunction}.
*/
function readsOnlyFunctions(graph, id) {
let reads = false;
for (const [target, edge] of graph.outgoingEdges(id) ?? graph_1.NoEdges) {
if (!edge_1.DfEdge.includesType(edge, edge_1.EdgeType.Reads)) {
continue;
}
else if (!definesAFunction(graph, target)) {
return false;
}
reads = true;
}
return reads;
}
/** The parts of a call R does not evaluate the standard way. */
exports.Nse = {
name: 'Nse',
/** The ids an {@link Unquote} escape hands back to standard evaluation, the markers themselves excluded. */
unquoted(root, style) {
if (root === undefined || style === Unquote.None) {
return undefined;
}
const evaluated = new Set();
model_1.RNode.visitAst(root, node => {
const operand = unquotedOperandOf(node, style);
if (operand === undefined) {
return false;
}
model_1.RNode.visitAst(operand, inner => {
evaluated.add(inner.info.id);
return false;
});
return true;
});
return evaluated.size > 0 ? evaluated : undefined;
},
/** Whether everything below `node` is unquoted. */
isUnquote(node, style) {
return style !== Unquote.None && unquotedOperandOf(node, style) !== undefined;
},
/**
* Drops the mask mark from names the caller binds to a value: while `filter(d, k)` is processed `k` has no
* read yet. A name bound to a function keeps its mark, as `filter(d, id > 2)` next to `id <- function(...)`
* still means the column. Hands back the masking call and the names it just unmarked, `undefined` when
* `name` does not mask at all, so {@link linkMasksToData} need not ask a second time.
*/
dropResolvedMask(graph, id, name) {
if (!data_masking_functions_1.DataMaskingFunctionNames.has(identifier_1.Identifier.getName(name))) {
return undefined;
}
return { id, bound: exports.Nse.unmark(graph, id, target => !exports.Nse.suppliedByMask(graph, target) && !readsOnlyFunctions(graph, target)) };
},
/** Drops the non-standard-evaluation mark from the outgoing edges of `id` that `which` accepts, and reports them. */
unmark(graph, id, which = () => true) {
const dropped = [];
for (const [target, edge] of graph.outgoingEdges(id) ?? graph_1.NoEdges) {
if (edge_1.DfEdge.includesType(edge, edge_1.EdgeType.NonStandardEvaluation) && which(target)) {
graph.removeEdgeType(id, target, edge_1.EdgeType.NonStandardEvaluation);
dropped.push(target);
}
}
return dropped;
},
/** Whether the vertex is a name a data mask may supply, which is every name appearing in the mask. */
maskCandidate(vertex) {
return vertex_1.UseVertex.is(vertex);
},
/** Whether `id` is a name the data mask supplies, i.e. a use the caller does not bind itself. */
suppliedByMask(graph, id, vertex = graph.getVertex(id)) {
return vertex_1.UseVertex.is(vertex) && !graph.outgoingEdges(id)?.values().some(e => edge_1.DfEdge.includesType(e, edge_1.EdgeType.Reads));
},
/**
* Whether `id` is a name a data mask supplied, which the mark the masking call carries is what records.
* Unlike {@link suppliedByMask} this survives {@link linkMasksToData}, so ask this one after the dataflow
* is complete and that one while a call is still being processed.
*/
maskedName(graph, id) {
for (const [source, edge] of graph.ingoingEdges(id) ?? graph_1.NoEdges) {
const call = edge_1.DfEdge.includesType(edge, edge_1.EdgeType.NonStandardEvaluation) ? graph.getVertex(source) : undefined;
if (vertex_1.FunctionCallVertex.is(call) && call.name !== undefined && data_masking_functions_1.DataMaskingFunctionNames.has(identifier_1.Identifier.getName(call.name))) {
return true;
}
}
return false;
},
/**
* Links every name the data masks of `calls` supply to the data it comes from: `id` in `filter(df, id > 2)`
* is a column of `df`, so it reads `df`. Run once every mark has settled, as adding the read makes
* {@link suppliedByMask} false for the name; a call whose first argument is masked too has no data to link
* against.
*/
linkMasksToData(graph, calls) {
const links = [];
for (const { id, bound } of calls) {
const call = graph.getVertex(id);
const data = vertex_1.FunctionCallVertex.is(call) ? call.args.find(a => a !== r_function_call_1.EmptyArgument) : undefined;
if (data === undefined) {
continue;
}
/* what is still marked are the names the data supplies, the unmarked ones mean the binding too */
const masked = new Set(bound);
for (const [target, edge] of graph.outgoingEdges(id) ?? graph_1.NoEdges) {
if (edge_1.DfEdge.includesType(edge, edge_1.EdgeType.NonStandardEvaluation)) {
masked.add(target);
}
}
/* a call masking its first argument too, like `aes(x, y)`, has no data to link against */
if (masked.has(data.nodeId)) {
continue;
}
for (const target of masked) {
/* a function of that name is not what the mask means, so the column takes its place */
if (readsOnlyFunctions(graph, target)) {
for (const [definition, edge] of graph.outgoingEdges(target) ?? graph_1.NoEdges) {
if (edge_1.DfEdge.includesType(edge, edge_1.EdgeType.Reads)) {
graph.removeEdgeType(target, definition, edge_1.EdgeType.Reads);
}
}
}
links.push([target, data.nodeId]);
}
}
for (const [from, to] of links) {
graph.addEdge(from, to, edge_1.EdgeType.Reads);
}
}
};
//# sourceMappingURL=nse.js.map