Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow partial expression evaluation on the CPU #15415

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
166 changes: 107 additions & 59 deletions src/ol/expr/cpu.js
Expand Up @@ -26,30 +26,37 @@ import {
* value. The evaluator function should do as little allocation and work as possible.
*/

export const UNKNOWN_VALUE = {};

/**
* @typedef {Object} EvaluationContext
* @property {Object} properties The values for properties used in 'get' expressions.
* @property {Object} variables The values for variables used in 'var' expressions.
* @property {number} resolution The map resolution.
* @property {string|number|null} featureId The feature id.
* @property {string} geometryType Geometry type of the current object.
* Each of these values can be set to UNKNOWN_VALUE, which means that they are not known in the current context.
* @property {Object|UNKNOWN_VALUE} properties The values for properties used in 'get' expressions.
* @property {Object|UNKNOWN_VALUE} variables The values for variables used in 'var' expressions.
* @property {number|UNKNOWN_VALUE} resolution The map resolution.
* @property {string|number|UNKNOWN_VALUE} featureId The feature id.
* @property {string|UNKNOWN_VALUE} geometryType Geometry type of the current object.
*/

/**
* @return {EvaluationContext} A new evaluation context.
*/
export function newEvaluationContext() {
return {
variables: {},
properties: {},
resolution: NaN,
featureId: null,
geometryType: '',
variables: UNKNOWN_VALUE,
properties: UNKNOWN_VALUE,
resolution: UNKNOWN_VALUE,
featureId: UNKNOWN_VALUE,
geometryType: UNKNOWN_VALUE,
};
}

/**
* @typedef {function(EvaluationContext):import("./expression.js").LiteralValue} ExpressionEvaluator
* @typedef {import("./expression.js").LiteralValue} LiteralValue
*/

/**
* @typedef {function(EvaluationContext):LiteralValue} ExpressionEvaluator
*/

/**
Expand Down Expand Up @@ -138,8 +145,9 @@ function compileExpression(expression, context) {
}
case Ops.Concat: {
const args = expression.args.map((e) => compileExpression(e, context));
return (context) =>
''.concat(...args.map((arg) => arg(context).toString()));
return checkForUnknown(args, (evaluatedArgs) =>
''.concat(...evaluatedArgs.map((arg) => arg.toString()))
);
}
case Ops.Resolution: {
return (context) => context.resolution;
Expand Down Expand Up @@ -217,6 +225,9 @@ function compileAssertionExpression(expression, context) {
return (context) => {
for (let i = 0; i < length; ++i) {
const value = args[i](context);
if (value === UNKNOWN_VALUE) {
return UNKNOWN_VALUE;
}
if (typeof value === type) {
return value;
}
Expand All @@ -240,17 +251,43 @@ function compileAccessorExpression(expression, context) {
const name = /** @type {string} */ (nameExpression.value);
switch (expression.operator) {
case Ops.Get: {
return (context) => context.properties[name];
return (context) =>
context.properties === UNKNOWN_VALUE
? UNKNOWN_VALUE
: context.properties[name];
}
case Ops.Var: {
return (context) => context.variables[name];
return (context) =>
context.variables === UNKNOWN_VALUE
? UNKNOWN_VALUE
: context.variables[name];
}
default: {
throw new Error(`Unsupported accessor operator ${expression.operator}`);
}
}
}

/**
* @param {Array<ExpressionEvaluator>} argEvaluators Argument evaluators
* @param {function(Array): ReturnType} evaluator Final evaluator taking in the evaluated args
* @return {function(EvaluationContext):ReturnType|UNKNOWN_VALUE} the evaluator function; if any arg evaluated to UNKNOWN_VALUE, will return UNKNOWN_VALUE
* @template ReturnType
*/
function checkForUnknown(argEvaluators, evaluator) {
return (context) => {
const evaluatedArgs = new Array(argEvaluators.length);
for (let i = 0, ii = evaluatedArgs.length; i < ii; i++) {
const value = argEvaluators[i](context);
if (value === UNKNOWN_VALUE) {
return UNKNOWN_VALUE;
}
evaluatedArgs[i] = value;
}
return evaluator(evaluatedArgs);
};
}

/**
* @param {import('./expression.js').CallExpression} expression The call expression.
* @param {import('./expression.js').ParsingContext} context The parsing context.
Expand All @@ -262,22 +299,22 @@ function compileComparisonExpression(expression, context) {
const right = compileExpression(expression.args[1], context);
switch (op) {
case Ops.Equal: {
return (context) => left(context) === right(context);
return checkForUnknown([left, right], ([left, right]) => left === right);
}
case Ops.NotEqual: {
return (context) => left(context) !== right(context);
return checkForUnknown([left, right], ([left, right]) => left !== right);
}
case Ops.LessThan: {
return (context) => left(context) < right(context);
return checkForUnknown([left, right], ([left, right]) => left < right);
}
case Ops.LessThanOrEqualTo: {
return (context) => left(context) <= right(context);
return checkForUnknown([left, right], ([left, right]) => left <= right);
}
case Ops.GreaterThan: {
return (context) => left(context) > right(context);
return checkForUnknown([left, right], ([left, right]) => left > right);
}
case Ops.GreaterThanOrEqualTo: {
return (context) => left(context) >= right(context);
return checkForUnknown([left, right], ([left, right]) => left >= right);
}
default: {
throw new Error(`Unsupported comparison operator ${op}`);
Expand All @@ -300,27 +337,27 @@ function compileLogicalExpression(expression, context) {
}
switch (op) {
case Ops.Any: {
return (context) => {
return checkForUnknown(args, (evaluatedArgs) => {
for (let i = 0; i < length; ++i) {
if (args[i](context)) {
if (evaluatedArgs[i]) {
return true;
}
}
return false;
};
});
}
case Ops.All: {
return (context) => {
return checkForUnknown(args, (evaluatedArgs) => {
for (let i = 0; i < length; ++i) {
if (!args[i](context)) {
if (!evaluatedArgs[i]) {
return false;
}
}
return true;
};
});
}
case Ops.Not: {
return (context) => !args[0](context);
return checkForUnknown(args, ([arg]) => !arg);
}
default: {
throw new Error(`Unsupported logical operator ${op}`);
Expand All @@ -343,75 +380,76 @@ function compileNumericExpression(expression, context) {
}
switch (op) {
case Ops.Multiply: {
return (context) => {
return checkForUnknown(args, (evaluatedArgs) => {
let value = 1;
for (let i = 0; i < length; ++i) {
value *= args[i](context);
value *= evaluatedArgs[i];
}
return value;
};
});
}
case Ops.Divide: {
return (context) => args[0](context) / args[1](context);
return checkForUnknown(args, ([first, second]) => first / second);
}
case Ops.Add: {
return (context) => {
return checkForUnknown(args, (evaluatedArgs) => {
let value = 0;
for (let i = 0; i < length; ++i) {
value += args[i](context);
value += evaluatedArgs[i];
}
return value;
};
});
}
case Ops.Subtract: {
return (context) => args[0](context) - args[1](context);
return checkForUnknown(args, ([first, second]) => first - second);
}
case Ops.Clamp: {
return (context) => {
const value = args[0](context);
const min = args[1](context);
return checkForUnknown(args, ([value, min, max]) => {
if (value < min) {
return min;
}
const max = args[2](context);
if (value > max) {
return max;
}
return value;
};
});
}
case Ops.Mod: {
return (context) => args[0](context) % args[1](context);
return checkForUnknown(args, ([first, second]) => first % second);
}
case Ops.Pow: {
return (context) => Math.pow(args[0](context), args[1](context));
return checkForUnknown(args, ([first, second]) =>
Math.pow(first, second)
);
}
case Ops.Abs: {
return (context) => Math.abs(args[0](context));
return checkForUnknown(args, ([arg]) => Math.abs(arg));
}
case Ops.Floor: {
return (context) => Math.floor(args[0](context));
return checkForUnknown(args, ([arg]) => Math.floor(arg));
}
case Ops.Ceil: {
return (context) => Math.ceil(args[0](context));
return checkForUnknown(args, ([arg]) => Math.ceil(arg));
}
case Ops.Round: {
return (context) => Math.round(args[0](context));
return checkForUnknown(args, ([arg]) => Math.round(arg));
}
case Ops.Sin: {
return (context) => Math.sin(args[0](context));
return checkForUnknown(args, ([arg]) => Math.sin(arg));
}
case Ops.Cos: {
return (context) => Math.cos(args[0](context));
return checkForUnknown(args, ([arg]) => Math.cos(arg));
}
case Ops.Atan: {
if (length === 2) {
return (context) => Math.atan2(args[0](context), args[1](context));
return checkForUnknown(args, ([first, second]) =>
Math.atan2(first, second)
);
}
return (context) => Math.atan(args[0](context));
return checkForUnknown(args, ([arg]) => Math.atan(arg));
}
case Ops.Sqrt: {
return (context) => Math.sqrt(args[0](context));
return checkForUnknown(args, ([arg]) => Math.sqrt(arg));
}
default: {
throw new Error(`Unsupported numeric operator ${op}`);
Expand All @@ -433,6 +471,9 @@ function compileCaseExpression(expression, context) {
return (context) => {
for (let i = 0; i < length - 1; i += 2) {
const condition = args[i](context);
if (condition === UNKNOWN_VALUE) {
return UNKNOWN_VALUE;
}
if (condition) {
return args[i + 1](context);
}
Expand All @@ -454,8 +495,15 @@ function compileMatchExpression(expression, context) {
}
return (context) => {
const value = args[0](context);
if (value === UNKNOWN_VALUE) {
return UNKNOWN_VALUE;
}
for (let i = 1; i < length; i += 2) {
if (value === args[i](context)) {
const matched = args[i](context);
if (matched === UNKNOWN_VALUE) {
return UNKNOWN_VALUE;
}
if (value === matched) {
return args[i + 1](context);
}
}
Expand All @@ -474,15 +522,15 @@ function compileInterpolateExpression(expression, context) {
for (let i = 0; i < length; ++i) {
args[i] = compileExpression(expression.args[i], context);
}
return (context) => {
const base = args[0](context);
const value = args[1](context);
return checkForUnknown(args, (evaluatedArgs) => {
const base = evaluatedArgs[0];
const value = evaluatedArgs[1];

let previousInput;
let previousOutput;
for (let i = 2; i < length; i += 2) {
const input = args[i](context);
let output = args[i + 1](context);
const input = evaluatedArgs[i];
let output = evaluatedArgs[i + 1];
const isColor = Array.isArray(output);
if (isColor) {
output = withAlpha(output);
Expand Down Expand Up @@ -514,7 +562,7 @@ function compileInterpolateExpression(expression, context) {
previousOutput = output;
}
return previousOutput;
};
});
}

/**
Expand Down