@eagleoutice/flowr-dev
Version:
Static Dataflow Analyzer and Program Slicer for the R Programming Language
163 lines • 8.18 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.NumericFns = void 0;
exports.resolveAsNumeric = resolveAsNumeric;
const type_1 = require("../../../r-bridge/lang-4.x/ast/model/type");
const identifier_1 = require("../../environments/identifier");
const r_value_1 = require("../../../util/r-value");
const r_value_2 = require("../values/r-value");
const interval_constants_1 = require("../values/intervals/interval-constants");
const vector_constants_1 = require("../values/vectors/vector-constants");
const match_arguments_1 = require("./match-arguments");
const resolve_helper_1 = require("../../environments/resolve-helper");
/** R breaks a tie to the even neighbor, unlike `Math.round`, which always goes up */
function roundHalfEven(x, digits = 0) {
const scale = 10 ** digits;
const scaled = x * scale;
const below = Math.floor(scaled);
const rounded = scaled - below !== 0.5 ? Math.round(scaled) : below % 2 === 0 ? below : below + 1;
return rounded / scale;
}
/** R rounds `%%` and `%/%` towards `-Inf`, unlike the JS `%` */
function mod(a, b) {
return a - Math.floor(a / b) * b;
}
/**
* Every numeric built-in the value solver folds, operators included: `-` is an entry like `sqrt` is, and both
* are reached through {@link resolveAsNumeric}. Teaching flowR one more is a line here plus the matching
* `evalHandler` in the built-in configuration -- a test checks that the two agree.
*
* An operator gets its operands under the names R gives them (`e1`, `e2`), and the ones that also exist as a
* unary form declare `e2` as optional. Anything the fold hands back that is not a finite number stays `Top`,
* so `sqrt(-1)` or `1/0` need no special case here.
*/
exports.NumericFns = {
/* the arithmetic operators; `+` and `-` fold their unary form as well, which is why `e2` may be missing */
'+': { params: ['e1', 'e2'], fold: (a, b) => b === undefined ? a : a + b },
'-': { params: ['e1', 'e2'], fold: (a, b) => b === undefined ? -a : a - b },
'*': { params: ['e1', 'e2'], fold: (a, b) => a * b },
'/': { params: ['e1', 'e2'], fold: (a, b) => a / b },
'^': { params: ['e1', 'e2'], fold: (a, b) => a ** b },
'**': { params: ['e1', 'e2'], fold: (a, b) => a ** b },
'%%': { params: ['e1', 'e2'], fold: mod },
'%/%': { params: ['e1', 'e2'], fold: (a, b) => Math.floor(a / b) },
/* rounding, each under the parameter names R documents */
abs: { params: ['x'], fold: Math.abs },
sqrt: { params: ['x'], fold: Math.sqrt },
floor: { params: ['x'], fold: Math.floor },
ceiling: { params: ['x'], fold: Math.ceil },
trunc: { params: ['x'], fold: Math.trunc },
sign: { params: ['x'], fold: Math.sign },
round: { params: ['x', 'digits'], fold: roundHalfEven },
signif: { params: ['x', 'digits'], fold: (x, digits = 6) => digits >= 1 && digits <= 21 ? Number(x.toPrecision(digits)) : undefined },
/* exponentials and logarithms; `log` takes its base as a second argument, the rest are fixed */
exp: { params: ['x'], fold: Math.exp },
expm1: { params: ['x'], fold: Math.expm1 },
log: { params: ['x', 'base'], fold: (x, base) => base === undefined ? Math.log(x) : Math.log(x) / Math.log(base) },
log2: { params: ['x'], fold: Math.log2 },
log10: { params: ['x'], fold: Math.log10 },
log1p: { params: ['x'], fold: Math.log1p },
/* trigonometry and its hyperbolic counterparts */
sin: { params: ['x'], fold: Math.sin },
cos: { params: ['x'], fold: Math.cos },
tan: { params: ['x'], fold: Math.tan },
asin: { params: ['x'], fold: Math.asin },
acos: { params: ['x'], fold: Math.acos },
atan: { params: ['x'], fold: Math.atan },
atan2: { params: ['y', 'x'], fold: Math.atan2 },
sinh: { params: ['x'], fold: Math.sinh },
cosh: { params: ['x'], fold: Math.cosh },
tanh: { params: ['x'], fold: Math.tanh },
asinh: { params: ['x'], fold: Math.asinh },
acosh: { params: ['x'], fold: Math.acosh },
atanh: { params: ['x'], fold: Math.atanh },
/* bit twiddling, which R defines on 32-bit integers just as JS does */
bitwAnd: { params: ['a', 'b'], fold: (a, b) => a & b },
bitwOr: { params: ['a', 'b'], fold: (a, b) => a | b },
bitwXor: { params: ['a', 'b'], fold: (a, b) => a ^ b },
bitwNot: { params: ['a'], fold: (a) => ~a },
bitwShiftL: { params: ['a', 'n'], fold: (a, n) => a << n },
bitwShiftR: { params: ['a', 'n'], fold: (a, n) => a >>> n }
};
/** the number an operand folds to, with a logical counting as its `0`/`1` just like R would coerce it */
function numeric(node, args) {
const value = (0, r_value_1.unliftRValue)(resolve_helper_1.Resolve.toValue(node, args));
if (typeof value === 'boolean') {
return Number(value);
}
else if ((0, r_value_1.isRNumberValue)(value)) {
return value.complexNumber ? undefined : value.num;
}
/* a vector folds elementwise, so it only counts when every element is a plain number */
if (!Array.isArray(value) || value.length === 0) {
return undefined;
}
const nums = value.map(e => (0, r_value_1.isRNumberValue)(e) && !e.complexNumber ? e.num : undefined);
return nums.every(n => n !== undefined) ? nums : undefined;
}
/** the name a node calls, whether it is written as an operator or as a plain call of the quoted operator */
function calledName(node) {
switch (node.type) {
case type_1.RType.UnaryOp:
case type_1.RType.BinaryOp:
return node.operator;
case type_1.RType.FunctionCall:
return node.named ? identifier_1.Identifier.getName(node.functionName.content) : undefined;
default:
return undefined;
}
}
/** apply `fold` to the operands, mapping over the one vector among them (R recycles, we only fold equal lengths) */
function apply(fold, operands) {
const lengths = operands.filter(o => Array.isArray(o)).map(o => o.length);
if (lengths.length === 0) {
return fold(...operands);
}
else if (lengths.some(l => l !== lengths[0])) {
return undefined; // R would recycle the shorter one, which is too easy to get wrong to guess at
}
const out = [];
for (let i = 0; i < lengths[0]; i++) {
const value = fold(...operands.map(o => Array.isArray(o) ? o[i] : o));
if (value === undefined || !Number.isFinite(value)) {
return undefined;
}
out.push(value);
}
return out;
}
/**
* Resolves any call of a {@link NumericFns} entry to a {@link Value}: the operators in prefix or infix form,
* the unary `+`/`-`, and the named functions with their arguments in any order R accepts. An operand may be a
* number, a logical (counting as its `0`/`1`), or a vector of numbers, which folds elementwise. Anything that
* does not resolve, a result that is not finite, and a vector length mismatch all stay `Top`.
*/
function resolveAsNumeric(args) {
const name = calledName(args.node);
const fn = name === undefined ? undefined : exports.NumericFns[name];
if (fn === undefined) {
return r_value_2.Top;
}
const nodes = (0, match_arguments_1.matchCallArguments)(args.node, fn.params);
if (nodes === undefined || nodes[0] === undefined) {
return r_value_2.Top;
}
const operands = [];
for (const node of nodes) {
if (node === undefined) {
break; // the parameters R gives a default are the trailing ones, so the fold sees a shorter prefix
}
const value = numeric(node, args);
if (value === undefined) {
return r_value_2.Top;
}
operands.push(value);
}
const folded = apply(fn.fold, operands);
if (typeof folded === 'number') {
return Number.isFinite(folded) ? (0, interval_constants_1.intervalFrom)(folded, folded) : r_value_2.Top;
}
/* a number is an exact interval everywhere else in the solver, so the elements are lifted the same way */
return folded === undefined ? r_value_2.Top : (0, vector_constants_1.vectorFrom)(folded.map(n => (0, interval_constants_1.intervalFrom)(n, n)));
}
//# sourceMappingURL=resolve-numbers.js.map