Support evaluating simple driver expressions without Python interpreter.

Recently @sergey found that hard-coding evaluation of certain very
common driver expressions without calling the Python interpreter
produces a 30-40% performance improvement. Since hard-coding is
obviously not suitable for production, I implemented a proper
parser and interpreter for simple arithmetic expressions in C.

The evaluator supports +, -, *, /, (), ==, !=, <, <=, >, >=,
and, or, not, ternary if; driver variables, frame, pi, True, False,
and a subset of standard math functions that seem most useful.

Booleans are represented as numbers, since within the supported
operation set it seems to be impossible to distinguish True/False
from 1.0/0.0. Boolean operations properly implement lazy evaluation
with jumps, and comparisons support chaining like 'a < b < c...'.

Expressions are parsed into a very simple stack machine program
that can then be safely evaluated in multiple threads.

Reviewers: sergey, campbellbarton

Differential Revision: https://developer.blender.org/D3698
This commit is contained in:
Alexander Gavrilov 2018-09-15 15:32:40 +03:00
parent 34ee9ab97c
commit bf2a54b058
Notes: blender-bot 2023-02-14 08:05:27 +01:00
Referenced by issue #67158, Blender becomes a lot much slower with parentheses in driver expression
Referenced by issue #47823, Adding "minimal Python Interpreter" for Drivers
13 changed files with 1495 additions and 26 deletions

View File

@ -108,6 +108,9 @@ bool driver_get_variable_property(
struct ChannelDriver *driver, struct DriverTarget *dtar,
struct PointerRNA *r_ptr, struct PropertyRNA **r_prop, int *r_index);
bool BKE_driver_has_simple_expression(struct ChannelDriver *driver);
void BKE_driver_invalidate_expression(struct ChannelDriver *driver, bool expr_changed, bool varname_changed);
float evaluate_driver(struct PathResolvedRNA *anim_rna, struct ChannelDriver *driver,
struct ChannelDriver *driver_orig, const float evaltime);

View File

@ -49,6 +49,8 @@
#include "BLI_threads.h"
#include "BLI_string_utils.h"
#include "BLI_utildefines.h"
#include "BLI_simple_expr.h"
#include "BLI_alloca.h"
#include "BLT_translation.h"
@ -65,6 +67,8 @@
#include "RNA_access.h"
#include "atomic_ops.h"
#ifdef WITH_PYTHON
#include "BPY_extern.h"
#endif
@ -1694,11 +1698,8 @@ void driver_free_variable_ex(ChannelDriver *driver, DriverVar *dvar)
/* remove and free the driver variable */
driver_free_variable(&driver->variables, dvar);
#ifdef WITH_PYTHON
/* since driver variables are cached, the expression needs re-compiling too */
if (driver->type == DRIVER_TYPE_PYTHON)
driver->flag |= DRIVER_FLAG_RENAMEVAR;
#endif
BKE_driver_invalidate_expression(driver, false, true);
}
/* Copy driver variables from src_vars list to dst_vars list */
@ -1835,11 +1836,8 @@ DriverVar *driver_add_new_variable(ChannelDriver *driver)
/* set the default type to 'single prop' */
driver_change_variable_type(dvar, DVAR_TYPE_SINGLE_PROP);
#ifdef WITH_PYTHON
/* since driver variables are cached, the expression needs re-compiling too */
if (driver->type == DRIVER_TYPE_PYTHON)
driver->flag |= DRIVER_FLAG_RENAMEVAR;
#endif
BKE_driver_invalidate_expression(driver, false, true);
/* return the target */
return dvar;
@ -1868,6 +1866,8 @@ void fcurve_free_driver(FCurve *fcu)
BPY_DECREF(driver->expr_comp);
#endif
BLI_simple_expr_free(driver->expr_simple);
/* free driver itself, then set F-Curve's point to this to NULL (as the curve may still be used) */
MEM_freeN(driver);
fcu->driver = NULL;
@ -1885,6 +1885,7 @@ ChannelDriver *fcurve_copy_driver(const ChannelDriver *driver)
/* copy all data */
ndriver = MEM_dupallocN(driver);
ndriver->expr_comp = NULL;
ndriver->expr_simple = NULL;
/* copy variables */
BLI_listbase_clear(&ndriver->variables); /* to get rid of refs to non-copied data (that's still used on original) */
@ -1894,6 +1895,124 @@ ChannelDriver *fcurve_copy_driver(const ChannelDriver *driver)
return ndriver;
}
/* Driver Expression Evaluation --------------- */
static ParsedSimpleExpr *driver_compile_simple_expr_impl(ChannelDriver *driver)
{
/* Prepare parameter names. */
int num_vars = BLI_listbase_count(&driver->variables);
const char **names = BLI_array_alloca(names, num_vars + 1);
int i = 0;
names[i++] = "frame";
for (DriverVar *dvar = driver->variables.first; dvar; dvar = dvar->next) {
names[i++] = dvar->name;
}
return BLI_simple_expr_parse(driver->expression, num_vars + 1, names);
}
static bool driver_evaluate_simple_expr(ChannelDriver *driver, ParsedSimpleExpr *expr, float *result, float time)
{
/* Prepare parameter values. */
int num_vars = BLI_listbase_count(&driver->variables);
double *vars = BLI_array_alloca(vars, num_vars + 1);
int i = 0;
vars[i++] = time;
for (DriverVar *dvar = driver->variables.first; dvar; dvar = dvar->next) {
vars[i++] = driver_get_variable_value(driver, dvar);
}
/* Evaluate expression. */
double result_val;
eSimpleExpr_EvalStatus status = BLI_simple_expr_evaluate(expr, &result_val, num_vars + 1, vars);
const char *message;
switch (status) {
case SIMPLE_EXPR_SUCCESS:
if (isfinite(result_val)) {
*result = (float)result_val;
}
return true;
case SIMPLE_EXPR_DIV_BY_ZERO:
case SIMPLE_EXPR_MATH_ERROR:
message = (status == SIMPLE_EXPR_DIV_BY_ZERO) ? "Division by Zero" : "Math Domain Error";
fprintf(stderr, "\n%s in Driver: '%s'\n", message, driver->expression);
driver->flag |= DRIVER_FLAG_INVALID;
return true;
default:
/* arriving here means a bug, not user error */
printf("Error: simple driver expression evaluation failed: '%s'\n", driver->expression);
return false;
}
}
/* Compile and cache the driver expression if necessary, with thread safety. */
static bool driver_compile_simple_expr(ChannelDriver *driver)
{
if (driver->expr_simple != NULL) {
return true;
}
if (driver->type != DRIVER_TYPE_PYTHON) {
return false;
}
/* It's safe to parse in multiple threads; at worst it'll
* waste some effort, but in return avoids mutex contention. */
ParsedSimpleExpr *expr = driver_compile_simple_expr_impl(driver);
/* Store the result if the field is still NULL, or discard
* it if another thread got here first. */
if (atomic_cas_ptr((void**)&driver->expr_simple, NULL, expr) != NULL) {
BLI_simple_expr_free(expr);
}
return true;
}
/* Try using the simple expression evaluator to compute the result of the driver.
* On success, stores the result and returns true; on failure result is set to 0. */
static bool driver_try_evaluate_simple_expr(ChannelDriver *driver, ChannelDriver *driver_orig, float *result, float time)
{
*result = 0.0f;
return driver_compile_simple_expr(driver_orig) &&
BLI_simple_expr_is_valid(driver_orig->expr_simple) &&
driver_evaluate_simple_expr(driver, driver_orig->expr_simple, result, time);
}
/* Check if the expression in the driver conforms to the simple subset. */
bool BKE_driver_has_simple_expression(ChannelDriver *driver)
{
return driver_compile_simple_expr(driver) && BLI_simple_expr_is_valid(driver->expr_simple);
}
/* Reset cached compiled expression data */
void BKE_driver_invalidate_expression(ChannelDriver *driver, bool expr_changed, bool varname_changed)
{
if (expr_changed || varname_changed) {
BLI_simple_expr_free(driver->expr_simple);
driver->expr_simple = NULL;
}
#ifdef WITH_PYTHON
if (expr_changed) {
driver->flag |= DRIVER_FLAG_RECOMPILE;
}
if (varname_changed) {
driver->flag |= DRIVER_FLAG_RENAMEVAR;
}
#endif
}
/* Driver Evaluation -------------------------- */
/* Evaluate a Driver Variable to get a value that contributes to the final */
@ -1997,14 +2116,14 @@ float evaluate_driver(PathResolvedRNA *anim_rna, ChannelDriver *driver, ChannelD
}
case DRIVER_TYPE_PYTHON: /* expression */
{
#ifdef WITH_PYTHON
/* check for empty or invalid expression */
if ( (driver_orig->expression[0] == '\0') ||
(driver_orig->flag & DRIVER_FLAG_INVALID) )
{
driver->curval = 0.0f;
}
else {
else if (!driver_try_evaluate_simple_expr(driver, driver_orig, &driver->curval, evaltime)) {
#ifdef WITH_PYTHON
/* this evaluates the expression using Python, and returns its result:
* - on errors it reports, then returns 0.0f
*/
@ -2013,10 +2132,10 @@ float evaluate_driver(PathResolvedRNA *anim_rna, ChannelDriver *driver, ChannelD
driver->curval = BPY_driver_exec(anim_rna, driver, driver_orig, evaltime);
BLI_mutex_unlock(&python_driver_lock);
}
#else /* WITH_PYTHON*/
UNUSED_VARS(anim_rna, evaltime);
UNUSED_VARS(anim_rna, evaltime);
#endif /* WITH_PYTHON*/
}
break;
}
default:

View File

@ -0,0 +1,96 @@
/*
* ***** BEGIN GPL LICENSE BLOCK *****
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* The Original Code is Copyright (C) 2018 Blender Foundation, Alexander Gavrilov
* All rights reserved.
*
* Contributor(s): Alexander Gavrilov
*
* ***** END GPL LICENSE BLOCK *****
*/
#ifndef __BLI_SIMPLE_EXPR_H__
#define __BLI_SIMPLE_EXPR_H__
/** \file BLI_simple_expr.h
* \ingroup bli
* \author Alexander Gavrilov
* \since 2018
*
* Simple evaluator for a subset of Python expressions that can be
* computed using purely double precision floating point values.
*
* Supported subset:
*
* - Identifiers use only ASCII characters.
* - Literals:
* floating point and decimal integer.
* - Constants:
* pi, True, False
* - Operators:
* +, -, *, /, ==, !=, <, <=, >, >=, and, or, not, ternary if
* - Functions:
* radians, degrees,
* abs, fabs, floor, ceil, trunc, int,
* sin, cos, tan, asin, acos, atan, atan2,
* exp, log, sqrt, pow, fmod
*
* The implementation has no global state and can be used multithreaded.
*/
#ifdef __cplusplus
extern "C" {
#endif
/** Opaque structure containing pre-parsed data for evaluation. */
typedef struct ParsedSimpleExpr ParsedSimpleExpr;
/** Simple expression evaluation return code. */
typedef enum eSimpleExpr_EvalStatus {
SIMPLE_EXPR_SUCCESS = 0,
/* Computation errors; result is still set, but may be NaN */
SIMPLE_EXPR_DIV_BY_ZERO,
SIMPLE_EXPR_MATH_ERROR,
/* Expression dependent errors or bugs; result is 0 */
SIMPLE_EXPR_INVALID,
SIMPLE_EXPR_FATAL_ERROR,
} eSimpleExpr_EvalStatus;
/** Free the parsed data; NULL argument is ok. */
void BLI_simple_expr_free(struct ParsedSimpleExpr *expr);
/** Check if the parsing result is valid for evaluation. */
bool BLI_simple_expr_is_valid(struct ParsedSimpleExpr *expr);
/** Check if the parsed expression always evaluates to the same value. */
bool BLI_simple_expr_is_constant(struct ParsedSimpleExpr *expr);
/** Parse the expression for evaluation later.
* Returns non-NULL even on failure; use is_valid to check.
*/
ParsedSimpleExpr *BLI_simple_expr_parse(const char *expression, int num_params, const char **param_names);
/** Evaluate the expression with the given parameters.
* The order and number of parameters must match the names given to parse.
*/
eSimpleExpr_EvalStatus BLI_simple_expr_evaluate(struct ParsedSimpleExpr *expr, double *result, int num_params, const double *params);
#ifdef __cplusplus
}
#endif
#endif /* __BLI_SIMPLE_EXPR_H__*/

View File

@ -104,6 +104,7 @@ set(SRC
intern/rct.c
intern/scanfill.c
intern/scanfill_utils.c
intern/simple_expr.c
intern/smallhash.c
intern/sort.c
intern/sort_utils.c

View File

@ -0,0 +1,924 @@
/*
* ***** BEGIN GPL LICENSE BLOCK *****
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* The Original Code is Copyright (C) 2018 Blender Foundation, Alexander Gavrilov
* All rights reserved.
*
* The Original Code is: all of this file.
*
* Contributor(s): Alexander Gavrilov
*
* ***** END GPL LICENSE BLOCK *****
*/
/** \file blender/blenlib/intern/simple_expr.c
* \ingroup bli
*/
#include <math.h>
#include <stdio.h>
#include <stddef.h>
#include <string.h>
#include <float.h>
#include <ctype.h>
#include <stdlib.h>
#include <fenv.h>
#include "MEM_guardedalloc.h"
#include "BLI_simple_expr.h"
#include "BLI_blenlib.h"
#include "BLI_math.h"
#include "BLI_string_utils.h"
#include "BLI_utildefines.h"
#include "BLI_alloca.h"
#ifdef _MSC_VER
#pragma fenv_access (on)
#endif
/* Simple Expression Stack Machine ------------------------- */
typedef enum eSimpleExpr_Opcode {
/* Double constant: (-> dval) */
OPCODE_CONST,
/* 1 argument function call: (a -> func1(a)) */
OPCODE_FUNC1,
/* 2 argument function call: (a b -> func2(a,b)) */
OPCODE_FUNC2,
/* Parameter access: (-> params[ival]) */
OPCODE_PARAMETER,
/* Minimum of multiple inputs: (a b c... -> min); ival = arg count */
OPCODE_MIN,
/* Maximum of multiple inputs: (a b c... -> max); ival = arg count */
OPCODE_MAX,
/* Jump (pc += jmp_offset) */
OPCODE_JMP,
/* Pop and jump if zero: (a -> ); JUMP IF NOT a */
OPCODE_JMP_ELSE,
/* Jump if nonzero, or pop: (a -> a JUMP) IF a ELSE (a -> ) */
OPCODE_JMP_OR,
/* Jump if zero, or pop: (a -> a JUMP) IF NOT a ELSE (a -> ) */
OPCODE_JMP_AND,
/* For comparison chaining: (a b -> 0 JUMP) IF NOT func2(a,b) ELSE (a b -> b) */
OPCODE_CMP_CHAIN,
} eSimpleExpr_Opcode;
typedef double (*UnaryOpFunc)(double);
typedef double (*BinaryOpFunc)(double, double);
typedef struct SimpleExprOp {
eSimpleExpr_Opcode opcode;
int jmp_offset;
union {
int ival;
double dval;
void *ptr;
UnaryOpFunc func1;
BinaryOpFunc func2;
} arg;
} SimpleExprOp;
struct ParsedSimpleExpr {
int ops_count;
int max_stack;
SimpleExprOp ops[];
};
void BLI_simple_expr_free(ParsedSimpleExpr *expr)
{
if (expr != NULL) {
MEM_freeN(expr);
}
}
bool BLI_simple_expr_is_valid(ParsedSimpleExpr *expr)
{
return expr != NULL && expr->ops_count > 0;
}
bool BLI_simple_expr_is_constant(ParsedSimpleExpr *expr)
{
return expr != NULL && expr->ops_count == 1 && expr->ops[0].opcode == OPCODE_CONST;
}
/* Stack Machine Evaluation -------------------------------- */
eSimpleExpr_EvalStatus BLI_simple_expr_evaluate(ParsedSimpleExpr *expr, double *result, int num_params, const double *params)
{
*result = 0.0;
if (!BLI_simple_expr_is_valid(expr)) {
return SIMPLE_EXPR_INVALID;
}
#define FAIL_IF(condition) if (condition) { return SIMPLE_EXPR_FATAL_ERROR; }
/* Check the stack requirement is at least remotely sane and allocate on the actual stack. */
FAIL_IF(expr->max_stack <= 0 || expr->max_stack > 1000);
double *stack = BLI_array_alloca(stack, expr->max_stack);
/* Evaluate expression. */
SimpleExprOp *ops = expr->ops;
int sp = 0, pc;
feclearexcept(FE_ALL_EXCEPT);
for (pc = 0; pc >= 0 && pc < expr->ops_count; pc++) {
switch (ops[pc].opcode) {
/* Arithmetic */
case OPCODE_CONST:
FAIL_IF(sp >= expr->max_stack);
stack[sp++] = ops[pc].arg.dval;
break;
case OPCODE_PARAMETER:
FAIL_IF(sp >= expr->max_stack || ops[pc].arg.ival >= num_params);
stack[sp++] = params[ops[pc].arg.ival];
break;
case OPCODE_FUNC1:
FAIL_IF(sp < 1);
stack[sp - 1] = ops[pc].arg.func1(stack[sp - 1]);
break;
case OPCODE_FUNC2:
FAIL_IF(sp < 2);
stack[sp - 2] = ops[pc].arg.func2(stack[sp - 2], stack[sp - 1]);
sp--;
break;
case OPCODE_MIN:
FAIL_IF(sp < ops[pc].arg.ival);
for (int j = 1; j < ops[pc].arg.ival; j++, sp--) {
CLAMP_MAX(stack[sp - 2], stack[sp - 1]);
}
break;
case OPCODE_MAX:
FAIL_IF(sp < ops[pc].arg.ival);
for (int j = 1; j < ops[pc].arg.ival; j++, sp--) {
CLAMP_MIN(stack[sp - 2], stack[sp - 1]);
}
break;
/* Jumps */
case OPCODE_JMP:
pc += ops[pc].jmp_offset;
break;
case OPCODE_JMP_ELSE:
FAIL_IF(sp < 1);
if (!stack[--sp]) {
pc += ops[pc].jmp_offset;
}
break;
case OPCODE_JMP_OR:
case OPCODE_JMP_AND:
FAIL_IF(sp < 1);
if (!stack[sp - 1] == !(ops[pc].opcode == OPCODE_JMP_OR)) {
pc += ops[pc].jmp_offset;
}
else {
sp--;
}
break;
/* For chaining comparisons, i.e. "a < b < c" as "a < b and b < c" */
case OPCODE_CMP_CHAIN:
FAIL_IF(sp < 2);
/* If comparison fails, return 0 and jump to end. */
if (!ops[pc].arg.func2(stack[sp - 2], stack[sp - 1])) {
stack[sp - 2] = 0.0;
pc += ops[pc].jmp_offset;
}
/* Otherwise keep b on the stack and proceed. */
else {
stack[sp - 2] = stack[sp - 1];
}
sp--;
break;
default:
return SIMPLE_EXPR_FATAL_ERROR;
}
}
FAIL_IF(sp != 1 || pc != expr->ops_count);
#undef FAIL_IF
*result = stack[0];
/* Detect floating point evaluation errors. */
int flags = fetestexcept(FE_DIVBYZERO | FE_INVALID);
if (flags) {
return (flags & FE_INVALID) ? SIMPLE_EXPR_MATH_ERROR : SIMPLE_EXPR_DIV_BY_ZERO;
}
return SIMPLE_EXPR_SUCCESS;
}
/* Simple Expression Built-In Operations ------------------- */
static double op_negate(double arg) {
return -arg;
}
static double op_mul(double a, double b) {
return a * b;
}
static double op_div(double a, double b) {
return a / b;
}
static double op_add(double a, double b) {
return a + b;
}
static double op_sub(double a, double b) {
return a - b;
}
static double op_radians(double arg) {
return arg * M_PI / 180.0;
}
static double op_degrees(double arg) {
return arg * 180.0 / M_PI;
}
static double op_not(double a) {
return a ? 0.0 : 1.0;
}
static double op_eq(double a, double b) {
return a == b ? 1.0 : 0.0;
}
static double op_ne(double a, double b) {
return a != b ? 1.0 : 0.0;
}
static double op_lt(double a, double b) {
return a < b ? 1.0 : 0.0;
}
static double op_le(double a, double b) {
return a <= b ? 1.0 : 0.0;
}
static double op_gt(double a, double b) {
return a > b ? 1.0 : 0.0;
}
static double op_ge(double a, double b) {
return a >= b ? 1.0 : 0.0;
}
typedef struct BuiltinConstDef {
const char *name;
double value;
} BuiltinConstDef;
static BuiltinConstDef builtin_consts[] = {
{ "pi", M_PI },
{ "True", 1.0 },
{ "False", 0.0 },
{ NULL, 0.0 }
};
typedef struct BuiltinOpDef {
const char *name;
eSimpleExpr_Opcode op;
void *funcptr;
} BuiltinOpDef;
static BuiltinOpDef builtin_ops[] = {
{ "radians", OPCODE_FUNC1, op_radians },
{ "degrees", OPCODE_FUNC1, op_degrees },
{ "abs", OPCODE_FUNC1, abs },
{ "fabs", OPCODE_FUNC1, abs },
{ "floor", OPCODE_FUNC1, floor },
{ "ceil", OPCODE_FUNC1, ceil },
{ "trunc", OPCODE_FUNC1, trunc },
{ "int", OPCODE_FUNC1, trunc },
{ "sin", OPCODE_FUNC1, sin },
{ "cos", OPCODE_FUNC1, cos },
{ "tan", OPCODE_FUNC1, tan },
{ "asin", OPCODE_FUNC1, asin },
{ "acos", OPCODE_FUNC1, acos },
{ "atan", OPCODE_FUNC1, atan },
{ "atan2", OPCODE_FUNC2, atan2 },
{ "exp", OPCODE_FUNC1, exp },
{ "log", OPCODE_FUNC1, log },
{ "sqrt", OPCODE_FUNC1, sqrt },
{ "pow", OPCODE_FUNC2, pow },
{ "fmod", OPCODE_FUNC2, fmod },
{ NULL, OPCODE_CONST, NULL }
};
/* Simple Expression Parser State -------------------------- */
#define MAKE_CHAR2(a, b) (((a) << 8) | (b))
#define CHECK_ERROR(condition) if (!(condition)) { return false; }
/* For simplicity simple token types are represented by their own character;
* these are special identifiers for multi-character tokens. */
#define TOKEN_ID MAKE_CHAR2('I', 'D')
#define TOKEN_NUMBER MAKE_CHAR2('0', '0')
#define TOKEN_GE MAKE_CHAR2('>', '=')
#define TOKEN_LE MAKE_CHAR2('<', '=')
#define TOKEN_NE MAKE_CHAR2('!', '=')
#define TOKEN_EQ MAKE_CHAR2('=', '=')
#define TOKEN_AND MAKE_CHAR2('A', 'N')
#define TOKEN_OR MAKE_CHAR2('O', 'R')
#define TOKEN_NOT MAKE_CHAR2('N', 'O')
#define TOKEN_IF MAKE_CHAR2('I', 'F')
#define TOKEN_ELSE MAKE_CHAR2('E', 'L')
static const char *token_eq_characters = "!=><";
static const char *token_characters = "~`!@#$%^&*+-=/\\?:;<>(){}[]|.,\"'";
typedef struct KeywordTokenDef {
const char *name;
short token;
} KeywordTokenDef;
static KeywordTokenDef keyword_list[] = {
{ "and", TOKEN_AND },
{ "or", TOKEN_OR },
{ "not", TOKEN_NOT },
{ "if", TOKEN_IF },
{ "else", TOKEN_ELSE },
{ NULL, TOKEN_ID }
};
typedef struct SimpleExprParseState {
int param_count;
const char **param_names;
/* Original expression */
const char *expr;
const char *cur;
/* Current token */
short token;
char *tokenbuf;
double tokenval;
/* Opcode buffer */
int ops_count, max_ops, last_jmp;
SimpleExprOp *ops;
/* Stack space requirement tracking */
int stack_ptr, max_stack;
} SimpleExprParseState;
/* Reserve space for the specified number of operations in the buffer. */
static SimpleExprOp* parse_alloc_ops(SimpleExprParseState *state, int count)
{
if (state->ops_count + count > state->max_ops) {
state->max_ops = power_of_2_max_i(state->ops_count + count);
state->ops = MEM_reallocN(state->ops, state->max_ops * sizeof(SimpleExprOp));
}
SimpleExprOp *op = &state->ops[state->ops_count];
state->ops_count += count;
return op;
}
/* Add one operation and track stack usage. */
static SimpleExprOp* parse_add_op(SimpleExprParseState *state, eSimpleExpr_Opcode code, int stack_delta)
{
/* track evaluation stack depth */
state->stack_ptr += stack_delta;
CLAMP_MIN(state->stack_ptr, 0);
CLAMP_MIN(state->max_stack, state->stack_ptr);
/* allocate the new instruction */
SimpleExprOp *op = parse_alloc_ops(state, 1);
memset(op, 0, sizeof(SimpleExprOp));
op->opcode = code;
return op;
}
/* Add one jump operation and return an index for parse_set_jump. */
static int parse_add_jump(SimpleExprParseState *state, eSimpleExpr_Opcode code)
{
parse_add_op(state, code, -1);
return state->last_jmp = state->ops_count;
}
/* Set the jump offset in a previously added jump operation. */
static void parse_set_jump(SimpleExprParseState *state, int jump)
{
state->last_jmp = state->ops_count;
state->ops[jump - 1].jmp_offset = state->ops_count - jump;
}
/* Add a function call operation, applying constant folding when possible. */
static bool parse_add_func(SimpleExprParseState *state, eSimpleExpr_Opcode code, int args, void *funcptr)
{
SimpleExprOp *prev_ops = &state->ops[state->ops_count];
int jmp_gap = state->ops_count - state->last_jmp;
feclearexcept(FE_ALL_EXCEPT);
switch (code) {
case OPCODE_FUNC1:
CHECK_ERROR(args == 1);
if (jmp_gap >= 1 && prev_ops[-1].opcode == OPCODE_CONST) {
UnaryOpFunc func = funcptr;
double result = func(prev_ops[-1].arg.dval);
if (fetestexcept(FE_DIVBYZERO | FE_INVALID) == 0) {
prev_ops[-1].arg.dval = result;
return true;
}
}
break;
case OPCODE_FUNC2:
CHECK_ERROR(args == 2);
if (jmp_gap >= 2 && prev_ops[-2].opcode == OPCODE_CONST && prev_ops[-1].opcode == OPCODE_CONST) {
BinaryOpFunc func = funcptr;
double result = func(prev_ops[-2].arg.dval, prev_ops[-1].arg.dval);
if (fetestexcept(FE_DIVBYZERO | FE_INVALID) == 0) {
prev_ops[-2].arg.dval = result;
state->ops_count--;
state->stack_ptr--;
return true;
}
}
break;
default:
BLI_assert(false);
return false;
}
parse_add_op(state, code, 1-args)->arg.ptr = funcptr;
return true;
}
/* Extract the next token from raw characters. */
static bool parse_next_token(SimpleExprParseState *state)
{
/* Skip whitespace. */
while (isspace(*state->cur)) {
state->cur++;
}
/* End of string. */
if (*state->cur == 0) {
state->token = 0;
return true;
}
/* Floating point numbers. */
if (isdigit(*state->cur) || (state->cur[0] == '.' && isdigit(state->cur[1]))) {
char *end, *out = state->tokenbuf;
bool is_float = false;
while (isdigit(*state->cur))
*out++ = *state->cur++;
if (*state->cur == '.') {
is_float = true;
*out++ = *state->cur++;
while (isdigit(*state->cur))
*out++ = *state->cur++;
}
if (ELEM(*state->cur, 'e', 'E')) {
is_float = true;
*out++ = *state->cur++;
if (ELEM(*state->cur, '+', '-'))
*out++ = *state->cur++;
CHECK_ERROR(isdigit(*state->cur));
while (isdigit(*state->cur))
*out++ = *state->cur++;
}
*out = 0;
/* Forbid C-style octal constants. */
if (!is_float && state->tokenbuf[0] == '0') {
for (char *p = state->tokenbuf+1; *p; p++) {
if (*p != '0') {
return false;
}
}
}
state->token = TOKEN_NUMBER;
state->tokenval = strtod(state->tokenbuf, &end);
return (end == out);
}
/* ?= tokens */
if (state->cur[1] == '=' && strchr(token_eq_characters, state->cur[0])) {
state->token = MAKE_CHAR2(state->cur[0], state->cur[1]);
state->cur += 2;
return true;
}
/* Special characters (single character tokens) */
if (strchr(token_characters, *state->cur)) {
state->token = *state->cur++;
return true;
}
/* Identifiers */
if (isalpha(*state->cur) || ELEM(*state->cur, '_')) {
char *out = state->tokenbuf;
while (isalnum(*state->cur) || ELEM(*state->cur, '_'))
*out++ = *state->cur++;
*out = 0;
for (int i = 0; keyword_list[i].name; i++) {
if (STREQ(state->tokenbuf, keyword_list[i].name)) {
state->token = keyword_list[i].token;
return true;
}
}
state->token = TOKEN_ID;
return true;
}
return false;
}
/* Recursive Descent Parser -------------------------------- */
static bool parse_expr(SimpleExprParseState *state);
static int parse_function_args(SimpleExprParseState *state)
{
if (!parse_next_token(state) || state->token != '(' || !parse_next_token(state)) {
return -1;
}
int arg_count = 0;
for(;;) {
if (!parse_expr(state)) {
return -1;
}
arg_count++;
switch (state->token) {
case ',':
if (!parse_next_token(state)) {
return -1;
}
break;
case ')':
if (!parse_next_token(state)) {
return -1;
}
return arg_count;
default:
return -1;
}
}
}
static bool parse_unary(SimpleExprParseState *state)
{
int i;
switch (state->token) {
case '+':
return parse_next_token(state) && parse_unary(state);
case '-':
CHECK_ERROR(parse_next_token(state) && parse_unary(state));
parse_add_func(state, OPCODE_FUNC1, 1, op_negate);
return true;
case '(':
return parse_next_token(state) &&
parse_expr(state) &&
state->token == ')' &&
parse_next_token(state);
case TOKEN_NUMBER:
parse_add_op(state, OPCODE_CONST, 1)->arg.dval = state->tokenval;
return parse_next_token(state);
case TOKEN_ID:
/* Parameters: search in reverse order in case of duplicate names - the last one should win. */
for (i = state->param_count - 1; i >= 0; i--) {
if (STREQ(state->tokenbuf, state->param_names[i])) {
parse_add_op(state, OPCODE_PARAMETER, 1)->arg.ival = i;
return parse_next_token(state);
}
}
/* Ordinary builtin constants. */
for (i = 0; builtin_consts[i].name; i++) {
if (STREQ(state->tokenbuf, builtin_consts[i].name)) {
parse_add_op(state, OPCODE_CONST, 1)->arg.dval = builtin_consts[i].value;
return parse_next_token(state);
}
}
/* Ordinary builtin functions. */
for (i = 0; builtin_ops[i].name; i++) {
if (STREQ(state->tokenbuf, builtin_ops[i].name)) {
int args = parse_function_args(state);
return parse_add_func(state, builtin_ops[i].op, args, builtin_ops[i].funcptr);
}
}
/* Specially supported functions. */
if (STREQ(state->tokenbuf, "min")) {
int cnt = parse_function_args(state);
CHECK_ERROR(cnt > 0);
parse_add_op(state, OPCODE_MIN, 1-cnt)->arg.ival = cnt;
return true;
}
if (STREQ(state->tokenbuf, "max")) {
int cnt = parse_function_args(state);
CHECK_ERROR(cnt > 0);
parse_add_op(state, OPCODE_MAX, 1-cnt)->arg.ival = cnt;
return true;
}
return false;
default:
return false;
}
}
static bool parse_mul(SimpleExprParseState *state)
{
CHECK_ERROR(parse_unary(state));
for (;;) {
switch (state->token) {
case '*':
CHECK_ERROR(parse_next_token(state) && parse_unary(state));
parse_add_func(state, OPCODE_FUNC2, 2, op_mul);
break;
case '/':
CHECK_ERROR(parse_next_token(state) && parse_unary(state));
parse_add_func(state, OPCODE_FUNC2, 2, op_div);
break;
default:
return true;
}
}
}
static bool parse_add(SimpleExprParseState *state)
{
CHECK_ERROR(parse_mul(state));
for (;;) {
switch (state->token) {
case '+':
CHECK_ERROR(parse_next_token(state) && parse_mul(state));
parse_add_func(state, OPCODE_FUNC2, 2, op_add);
break;
case '-':
CHECK_ERROR(parse_next_token(state) && parse_mul(state));
parse_add_func(state, OPCODE_FUNC2, 2, op_sub);
break;
default:
return true;
}
}
}
static BinaryOpFunc parse_get_cmp_func(short token)
{
switch (token) {
case TOKEN_EQ:
return op_eq;
case TOKEN_NE:
return op_ne;
case '>':
return op_gt;
case TOKEN_GE:
return op_ge;
case '<':
return op_lt;
case TOKEN_LE:
return op_le;
default:
return NULL;
}
}
static bool parse_cmp_chain(SimpleExprParseState *state, BinaryOpFunc cur_func)
{
BinaryOpFunc next_func = parse_get_cmp_func(state->token);
if (next_func) {
parse_add_op(state, OPCODE_CMP_CHAIN, -1)->arg.func2 = cur_func;
int jump = state->last_jmp = state->ops_count;
CHECK_ERROR(parse_next_token(state) && parse_add(state));
CHECK_ERROR(parse_cmp_chain(state, next_func));
parse_set_jump(state, jump);
}
else {
parse_add_func(state, OPCODE_FUNC2, 2, cur_func);
}
return true;
}
static bool parse_cmp(SimpleExprParseState *state)
{
CHECK_ERROR(parse_add(state));
BinaryOpFunc func = parse_get_cmp_func(state->token);
if (func) {
CHECK_ERROR(parse_next_token(state) && parse_add(state));
return parse_cmp_chain(state, func);
}
return true;
}
static bool parse_not(SimpleExprParseState *state)
{
if (state->token == TOKEN_NOT) {
CHECK_ERROR(parse_next_token(state) && parse_not(state));
parse_add_func(state, OPCODE_FUNC1, 1, op_not);
return true;
}
return parse_cmp(state);
}
static bool parse_and(SimpleExprParseState *state)
{
CHECK_ERROR(parse_not(state));
if (state->token == TOKEN_AND) {
int jump = parse_add_jump(state, OPCODE_JMP_AND);
CHECK_ERROR(parse_next_token(state) && parse_and(state));
parse_set_jump(state, jump);
}
return true;
}
static bool parse_or(SimpleExprParseState *state)
{
CHECK_ERROR(parse_and(state));
if (state->token == TOKEN_OR) {
int jump = parse_add_jump(state, OPCODE_JMP_OR);
CHECK_ERROR(parse_next_token(state) && parse_or(state));
parse_set_jump(state, jump);
}
return true;
}
static bool parse_expr(SimpleExprParseState *state)
{
/* Temporarily set the constant expression evaluation barrier */
int prev_last_jmp = state->last_jmp;
int start = state->last_jmp = state->ops_count;
CHECK_ERROR(parse_or(state));
if (state->token == TOKEN_IF) {
/* Ternary IF expression in python requires swapping the
* main body with condition, so stash the body opcodes. */
int size = state->ops_count - start;
int bytes = size * sizeof(SimpleExprOp);
SimpleExprOp *body = MEM_mallocN(bytes, "driver if body");
memcpy(body, state->ops + start, bytes);
state->last_jmp = state->ops_count = start;
state->stack_ptr--;
/* Parse condition. */
if (!parse_next_token(state) || !parse_or(state) ||
state->token != TOKEN_ELSE || !parse_next_token(state))
{
MEM_freeN(body);
return false;
}
int jmp_else = parse_add_jump(state, OPCODE_JMP_ELSE);
/* Add body back. */
memcpy(parse_alloc_ops(state, size), body, bytes);
MEM_freeN(body);
state->stack_ptr++;
int jmp_end = parse_add_jump(state, OPCODE_JMP);
/* Parse the else block. */
parse_set_jump(state, jmp_else);
CHECK_ERROR(parse_expr(state));
parse_set_jump(state, jmp_end);
}
/* If no actual jumps happened, restore previous barrier */
else if (state->last_jmp == start) {
state->last_jmp = prev_last_jmp;
}
return true;
}
/* Main Parsing Function ----------------------------------- */
/* Compile the expression and return the result. */
ParsedSimpleExpr *BLI_simple_expr_parse(const char *expression, int num_params, const char **param_names)
{
/* Prepare the parser state. */
SimpleExprParseState state;
memset(&state, 0, sizeof(state));
state.cur = state.expr = expression;
state.param_count = num_params;
state.param_names = param_names;
state.tokenbuf = MEM_mallocN(strlen(expression) + 1, __func__);
state.max_ops = 16;
state.ops = MEM_mallocN(state.max_ops * sizeof(SimpleExprOp), __func__);
/* Parse the expression. */
ParsedSimpleExpr *expr;
if (parse_next_token(&state) && parse_expr(&state) && state.token == 0) {
BLI_assert(state.stack_ptr == 1);
int bytesize = sizeof(ParsedSimpleExpr) + state.ops_count * sizeof(SimpleExprOp);
expr = MEM_mallocN(bytesize, "ParsedSimpleExpr");
expr->ops_count = state.ops_count;
expr->max_stack = state.max_stack;
memcpy(expr->ops, state.ops, state.ops_count * sizeof(SimpleExprOp));
}
else {
/* Always return a non-NULL object so that parse failure can be cached. */
expr = MEM_callocN(sizeof(ParsedSimpleExpr), "ParsedSimpleExpr(empty)");
}
MEM_freeN(state.tokenbuf);
MEM_freeN(state.ops);
return expr;
}

View File

@ -2675,6 +2675,7 @@ static void direct_link_fcurves(FileData *fd, ListBase *list)
/* compiled expression data will need to be regenerated (old pointer may still be set here) */
driver->expr_comp = NULL;
driver->expr_simple = NULL;
/* give the driver a fresh chance - the operating environment may be different now
* (addons, etc. may be different) so the driver namespace may be sane now [#32155]

View File

@ -745,11 +745,8 @@ bool ANIM_driver_vars_paste(ReportList *reports, FCurve *fcu, bool replace)
driver->variables.last = tmp_list.last;
}
#ifdef WITH_PYTHON
/* since driver variables are cached, the expression needs re-compiling too */
if (driver->type == DRIVER_TYPE_PYTHON)
driver->flag |= DRIVER_FLAG_RENAMEVAR;
#endif
BKE_driver_invalidate_expression(driver, false, true);
return true;
}

View File

@ -168,7 +168,7 @@ bool ui_but_anim_expression_set(uiBut *but, const char *str)
BLI_strncpy_utf8(driver->expression, str, sizeof(driver->expression));
/* tag driver as needing to be recompiled */
driver->flag |= DRIVER_FLAG_RECOMPILE;
BKE_driver_invalidate_expression(driver, true, false);
/* clear invalid flags which may prevent this from working */
driver->flag &= ~DRIVER_FLAG_INVALID;
@ -237,7 +237,7 @@ bool ui_but_anim_expression_create(uiBut *but, const char *str)
BLI_strncpy_utf8(driver->expression, str, sizeof(driver->expression));
/* updates */
driver->flag |= DRIVER_FLAG_RECOMPILE;
BKE_driver_invalidate_expression(driver, true, false);
DEG_relations_tag_update(CTX_data_main(C));
WM_event_add_notifier(C, NC_ANIMATION | ND_KEYFRAME, NULL);
ok = true;

View File

@ -817,13 +817,18 @@ static void graph_draw_driver_settings_panel(uiLayout *layout, ID *id, FCurve *f
col = uiLayoutColumn(layout, true);
block = uiLayoutGetBlock(col);
if ((G.f & G_SCRIPT_AUTOEXEC) == 0) {
/* TODO: Add button to enable? */
uiItemL(col, IFACE_("WARNING: Python expressions limited for security"), ICON_ERROR);
}
else if (driver->flag & DRIVER_FLAG_INVALID) {
if (driver->flag & DRIVER_FLAG_INVALID) {
uiItemL(col, IFACE_("ERROR: Invalid Python expression"), ICON_CANCEL);
}
else if (!BKE_driver_has_simple_expression(driver)) {
if ((G.f & G_SCRIPT_AUTOEXEC) == 0) {
/* TODO: Add button to enable? */
uiItemL(col, IFACE_("WARNING: Python expressions limited for security"), ICON_ERROR);
}
else {
uiItemL(col, IFACE_("Slow Python expression"), ICON_INFO);
}
}
/* Explicit bpy-references are evil. Warn about these to prevent errors */
/* TODO: put these in a box? */

View File

@ -415,6 +415,8 @@ typedef struct ChannelDriver {
char expression[256]; /* expression to compile for evaluation */
void *expr_comp; /* PyObject - compiled expression, don't save this */
struct ParsedSimpleExpr *expr_simple; /* compiled simple arithmetic expression */
float curval; /* result of previous evaluation */
float influence; /* influence of driver on result */ // XXX to be implemented... this is like the constraint influence setting

View File

@ -132,6 +132,13 @@ static StructRNA *rna_FModifierType_refine(struct PointerRNA *ptr)
#include "DEG_depsgraph.h"
#include "DEG_depsgraph_build.h"
static bool rna_ChannelDriver_is_simple_expression_get(PointerRNA *ptr)
{
ChannelDriver *driver = ptr->data;
return BKE_driver_has_simple_expression(driver);
}
static void rna_ChannelDriver_update_data(Main *bmain, Scene *scene, PointerRNA *ptr)
{
ID *id = ptr->id.data;
@ -151,7 +158,7 @@ static void rna_ChannelDriver_update_expr(Main *bmain, Scene *scene, PointerRNA
ChannelDriver *driver = ptr->data;
/* tag driver as needing to be recompiled */
driver->flag |= DRIVER_FLAG_RECOMPILE;
BKE_driver_invalidate_expression(driver, true, false);
/* update_data() clears invalid flag and schedules for updates */
rna_ChannelDriver_update_data(bmain, scene, ptr);
@ -184,8 +191,7 @@ static void rna_DriverTarget_update_name(Main *bmain, Scene *scene, PointerRNA *
ChannelDriver *driver = ptr->data;
rna_DriverTarget_update_data(bmain, scene, ptr);
driver->flag |= DRIVER_FLAG_RENAMEVAR;
BKE_driver_invalidate_expression(driver, false, true);
}
/* ----------- */
@ -1675,6 +1681,10 @@ static void rna_def_channeldriver(BlenderRNA *brna)
RNA_def_property_boolean_negative_sdna(prop, NULL, "flag", DRIVER_FLAG_INVALID);
RNA_def_property_ui_text(prop, "Invalid", "Driver could not be evaluated in past, so should be skipped");
prop = RNA_def_property(srna, "is_simple_expression", PROP_BOOLEAN, PROP_NONE);
RNA_def_property_clear_flag(prop, PROP_EDITABLE);
RNA_def_property_boolean_funcs(prop, "rna_ChannelDriver_is_simple_expression_get", NULL);
RNA_def_property_ui_text(prop, "Simple Expression", "The scripted expression can be evaluated without using the full python interpreter");
/* Functions */
RNA_api_drivers(srna);

View File

@ -0,0 +1,310 @@
/* Apache License, Version 2.0 */
#include "testing/testing.h"
#include <string.h>
extern "C" {
#include "BLI_simple_expr.h"
#include "BLI_math.h"
};
#define TRUE_VAL 1.0
#define FALSE_VAL 0.0
static void simple_expr_parse_fail_test(const char *str)
{
ParsedSimpleExpr *expr = BLI_simple_expr_parse(str, 0, NULL);
EXPECT_FALSE(BLI_simple_expr_is_valid(expr));
BLI_simple_expr_free(expr);
}
static void simple_expr_const_test(const char *str, double value, bool force_const)
{
ParsedSimpleExpr *expr = BLI_simple_expr_parse(str, 0, NULL);
if (force_const) {
EXPECT_TRUE(BLI_simple_expr_is_constant(expr));
}
else {
EXPECT_TRUE(BLI_simple_expr_is_valid(expr));
EXPECT_FALSE(BLI_simple_expr_is_constant(expr));
}
double result;
eSimpleExpr_EvalStatus status = BLI_simple_expr_evaluate(expr, &result, 0, NULL);
EXPECT_EQ(status, SIMPLE_EXPR_SUCCESS);
EXPECT_EQ(result, value);
BLI_simple_expr_free(expr);
}
static ParsedSimpleExpr *parse_for_eval(const char *str, bool nonconst)
{
const char *names[1] = { "x" };
ParsedSimpleExpr *expr = BLI_simple_expr_parse(str, 1, names);
EXPECT_TRUE(BLI_simple_expr_is_valid(expr));
if (nonconst) {
EXPECT_FALSE(BLI_simple_expr_is_constant(expr));
}
return expr;
}
static void verify_eval_result(ParsedSimpleExpr *expr, double x, double value)
{
double result;
eSimpleExpr_EvalStatus status = BLI_simple_expr_evaluate(expr, &result, 1, &x);
EXPECT_EQ(status, SIMPLE_EXPR_SUCCESS);
EXPECT_EQ(result, value);
}
static void simple_expr_eval_test(const char *str, double x, double value)
{
ParsedSimpleExpr *expr = parse_for_eval(str, true);
verify_eval_result(expr, x, value);
BLI_simple_expr_free(expr);
}
static void simple_expr_error_test(const char *str, double x, eSimpleExpr_EvalStatus error)
{
ParsedSimpleExpr *expr = parse_for_eval(str, false);
double result;
eSimpleExpr_EvalStatus status = BLI_simple_expr_evaluate(expr, &result, 1, &x);
EXPECT_EQ(status, error);
BLI_simple_expr_free(expr);
}
#define TEST_PARSE_FAIL(name, str) \
TEST(simple_expr, ParseFail_##name) { simple_expr_parse_fail_test(str); }
TEST_PARSE_FAIL(Empty, "")
TEST_PARSE_FAIL(ConstHex, "0x0")
TEST_PARSE_FAIL(ConstOctal, "01")
TEST_PARSE_FAIL(Tail, "0 0")
TEST_PARSE_FAIL(ConstFloatExp, "0.5e+")
TEST_PARSE_FAIL(BadId, "Pi")
TEST_PARSE_FAIL(BadArgCount0, "sqrt")
TEST_PARSE_FAIL(BadArgCount1, "sqrt()")
TEST_PARSE_FAIL(BadArgCount2, "sqrt(1,2)")
TEST_PARSE_FAIL(BadArgCount3, "pi()")
TEST_PARSE_FAIL(BadArgCount4, "max()")
TEST_PARSE_FAIL(BadArgCount5, "min()")
TEST_PARSE_FAIL(Truncated1, "(1+2")
TEST_PARSE_FAIL(Truncated2, "1 if 2")
TEST_PARSE_FAIL(Truncated3, "1 if 2 else")
TEST_PARSE_FAIL(Truncated4, "1 < 2 <")
TEST_PARSE_FAIL(Truncated5, "1 +")
TEST_PARSE_FAIL(Truncated6, "1 *")
TEST_PARSE_FAIL(Truncated7, "1 and")
TEST_PARSE_FAIL(Truncated8, "1 or")
TEST_PARSE_FAIL(Truncated9, "sqrt(1")
TEST_PARSE_FAIL(Truncated10, "fmod(1,")
/* Constant expression with working constant folding */
#define TEST_CONST(name, str, value) \
TEST(simple_expr, Const_##name) { simple_expr_const_test(str, value, true); }
/* Constant expression but constant folding is not supported */
#define TEST_RESULT(name, str, value) \
TEST(simple_expr, Result_##name) { simple_expr_const_test(str, value, false); }
/* Expression with an argument */
#define TEST_EVAL(name, str, x, value) \
TEST(simple_expr, Eval_##name) { simple_expr_eval_test(str, x, value); }
TEST_CONST(Zero, "0", 0.0)
TEST_CONST(Zero2, "00", 0.0)
TEST_CONST(One, "1", 1.0)
TEST_CONST(OneF, "1.0", 1.0)
TEST_CONST(OneF2, "1.", 1.0)
TEST_CONST(OneE, "1e0", 1.0)
TEST_CONST(TenE, "1.e+1", 10.0)
TEST_CONST(Half, ".5", 0.5)
TEST_CONST(Pi, "pi", M_PI)
TEST_CONST(True, "True", TRUE_VAL)
TEST_CONST(False, "False", FALSE_VAL)
TEST_CONST(Sqrt, "sqrt(4)", 2.0)
TEST_EVAL(Sqrt, "sqrt(x)", 4.0, 2.0)
TEST_CONST(FMod, "fmod(3.5, 2)", 1.5)
TEST_EVAL(FMod, "fmod(x, 2)", 3.5, 1.5)
TEST_CONST(Pow, "pow(4, 0.5)", 2.0)
TEST_EVAL(Pow, "pow(4, x)", 0.5, 2.0)
TEST_RESULT(Min1, "min(3,1,2)", 1.0)
TEST_RESULT(Max1, "max(3,1,2)", 3.0)
TEST_RESULT(Min2, "min(1,2,3)", 1.0)
TEST_RESULT(Max2, "max(1,2,3)", 3.0)
TEST_RESULT(Min3, "min(2,3,1)", 1.0)
TEST_RESULT(Max3, "max(2,3,1)", 3.0)
TEST_CONST(UnaryPlus, "+1", 1.0)
TEST_CONST(UnaryMinus, "-1", -1.0)
TEST_EVAL(UnaryMinus, "-x", 1.0, -1.0)
TEST_CONST(BinaryPlus, "1+2", 3.0)
TEST_EVAL(BinaryPlus, "x+2", 1, 3.0)
TEST_CONST(BinaryMinus, "1-2", -1.0)
TEST_EVAL(BinaryMinus, "1-x", 2, -1.0)
TEST_CONST(BinaryMul, "2*3", 6.0)
TEST_EVAL(BinaryMul, "x*3", 2, 6.0)
TEST_CONST(BinaryDiv, "3/2", 1.5)
TEST_EVAL(BinaryDiv, "3/x", 2, 1.5)
TEST_CONST(Arith1, "1 + -2 * 3", -5.0)
TEST_CONST(Arith2, "(1 + -2) * 3", -3.0)
TEST_CONST(Arith3, "-1 + 2 * 3", 5.0)
TEST_CONST(Arith4, "3 * (-2 + 1)", -3.0)
TEST_EVAL(Arith1, "1 + -x * 3", 2, -5.0)
TEST_CONST(Eq1, "1 == 1.0", TRUE_VAL)
TEST_CONST(Eq2, "1 == 2.0", FALSE_VAL)
TEST_CONST(Eq3, "True == 1", TRUE_VAL)
TEST_CONST(Eq4, "False == 0", TRUE_VAL)
TEST_EVAL(Eq1, "1 == x", 1.0, TRUE_VAL)
TEST_EVAL(Eq2, "1 == x", 2.0, FALSE_VAL)
TEST_CONST(NEq1, "1 != 1.0", FALSE_VAL)
TEST_CONST(NEq2, "1 != 2.0", TRUE_VAL)
TEST_EVAL(NEq1, "1 != x", 1.0, FALSE_VAL)
TEST_EVAL(NEq2, "1 != x", 2.0, TRUE_VAL)
TEST_CONST(Lt1, "1 < 1", FALSE_VAL)
TEST_CONST(Lt2, "1 < 2", TRUE_VAL)
TEST_CONST(Lt3, "2 < 1", FALSE_VAL)
TEST_CONST(Le1, "1 <= 1", TRUE_VAL)
TEST_CONST(Le2, "1 <= 2", TRUE_VAL)
TEST_CONST(Le3, "2 <= 1", FALSE_VAL)
TEST_CONST(Gt1, "1 > 1", FALSE_VAL)
TEST_CONST(Gt2, "1 > 2", FALSE_VAL)
TEST_CONST(Gt3, "2 > 1", TRUE_VAL)
TEST_CONST(Ge1, "1 >= 1", TRUE_VAL)
TEST_CONST(Ge2, "1 >= 2", FALSE_VAL)
TEST_CONST(Ge3, "2 >= 1", TRUE_VAL)
TEST_CONST(Cmp1, "3 == 1 + 2", TRUE_VAL)
TEST_EVAL(Cmp1, "3 == x + 2", 1, TRUE_VAL)
TEST_EVAL(Cmp1b, "3 == x + 2", 1.5, FALSE_VAL)
TEST_RESULT(CmpChain1, "1 < 2 < 3", TRUE_VAL)
TEST_RESULT(CmpChain2, "1 < 2 == 2", TRUE_VAL)
TEST_RESULT(CmpChain3, "1 < 2 > -1", TRUE_VAL)
TEST_RESULT(CmpChain4, "1 < 2 < 2 < 3", FALSE_VAL)
TEST_RESULT(CmpChain5, "1 < 2 <= 2 < 3", TRUE_VAL)
TEST_EVAL(CmpChain1a, "1 < x < 3", 2, TRUE_VAL)
TEST_EVAL(CmpChain1b, "1 < x < 3", 1, FALSE_VAL)
TEST_EVAL(CmpChain1c, "1 < x < 3", 3, FALSE_VAL)
TEST_CONST(Not1, "not 2", FALSE_VAL)
TEST_CONST(Not2, "not 0", TRUE_VAL)
TEST_CONST(Not3, "not not 2", TRUE_VAL)
TEST_EVAL(Not1, "not x", 2, FALSE_VAL)
TEST_EVAL(Not2, "not x", 0, TRUE_VAL)
TEST_RESULT(And1, "2 and 3", 3.0)
TEST_RESULT(And2, "0 and 3", 0.0)
TEST_RESULT(Or1, "2 or 3", 2.0)
TEST_RESULT(Or2, "0 or 3", 3.0)
TEST_RESULT(Bool1, "2 or 3 and 4", 2.0)
TEST_RESULT(Bool2, "not 2 or 3 and 4", 4.0)
TEST(simple_expr, Eval_Ternary1)
{
ParsedSimpleExpr *expr = parse_for_eval("x / 2 if x < 4 else x - 2 if x < 8 else x*2 - 12", true);
for (int i = 0; i <= 10; i++) {
double x = i;
double v = (x < 4) ? (x / 2) : (x < 8) ? (x - 2) : (x*2 - 12);
verify_eval_result(expr, x, v);
}
BLI_simple_expr_free(expr);
}
TEST(simple_expr, MultipleArgs)
{
const char* names[3] = { "x", "y", "x" };
double values[3] = { 1.0, 2.0, 3.0 };
ParsedSimpleExpr *expr = BLI_simple_expr_parse("x*10 + y", 3, names);
EXPECT_TRUE(BLI_simple_expr_is_valid(expr));
double result;
eSimpleExpr_EvalStatus status = BLI_simple_expr_evaluate(expr, &result, 3, values);
EXPECT_EQ(status, SIMPLE_EXPR_SUCCESS);
EXPECT_EQ(result, 32.0);
BLI_simple_expr_free(expr);
}
#define TEST_ERROR(name, str, x, code) \
TEST(simple_expr, Error_##name) { simple_expr_error_test(str, x, code); }
TEST_ERROR(DivZero1, "0 / 0", 0.0, SIMPLE_EXPR_MATH_ERROR)
TEST_ERROR(DivZero2, "1 / 0", 0.0, SIMPLE_EXPR_DIV_BY_ZERO)
TEST_ERROR(DivZero3, "1 / x", 0.0, SIMPLE_EXPR_DIV_BY_ZERO)
TEST_ERROR(DivZero4, "1 / x", 1.0, SIMPLE_EXPR_SUCCESS)
TEST_ERROR(SqrtDomain1, "sqrt(-1)", 0.0, SIMPLE_EXPR_MATH_ERROR)
TEST_ERROR(SqrtDomain2, "sqrt(x)", -1.0, SIMPLE_EXPR_MATH_ERROR)
TEST_ERROR(SqrtDomain3, "sqrt(x)", 0.0, SIMPLE_EXPR_SUCCESS)
TEST_ERROR(PowDomain1, "pow(-1, 0.5)", 0.0, SIMPLE_EXPR_MATH_ERROR)
TEST_ERROR(PowDomain2, "pow(-1, x)", 0.5, SIMPLE_EXPR_MATH_ERROR)
TEST_ERROR(PowDomain3, "pow(-1, x)", 2.0, SIMPLE_EXPR_SUCCESS)
TEST_ERROR(Mixed1, "sqrt(x) + 1 / max(0, x)", -1.0, SIMPLE_EXPR_MATH_ERROR)
TEST_ERROR(Mixed2, "sqrt(x) + 1 / max(0, x)", 0.0, SIMPLE_EXPR_DIV_BY_ZERO)
TEST_ERROR(Mixed3, "sqrt(x) + 1 / max(0, x)", 1.0, SIMPLE_EXPR_SUCCESS)
TEST(simple_expr, Error_Invalid)
{
ParsedSimpleExpr *expr = BLI_simple_expr_parse("", 0, NULL);
double result;
EXPECT_EQ(BLI_simple_expr_evaluate(expr, &result, 0, NULL), SIMPLE_EXPR_INVALID);
BLI_simple_expr_free(expr);
}
TEST(simple_expr, Error_ArgumentCount)
{
ParsedSimpleExpr *expr = parse_for_eval("x", false);
double result;
EXPECT_EQ(BLI_simple_expr_evaluate(expr, &result, 0, NULL), SIMPLE_EXPR_FATAL_ERROR);
BLI_simple_expr_free(expr);
}

View File

@ -55,6 +55,7 @@ BLENDER_TEST(BLI_math_geom "bf_blenlib")
BLENDER_TEST(BLI_memiter "bf_blenlib")
BLENDER_TEST(BLI_path_util "${BLI_path_util_extra_libs}")
BLENDER_TEST(BLI_polyfill_2d "bf_blenlib")
BLENDER_TEST(BLI_simple_expr "bf_blenlib")
BLENDER_TEST(BLI_stack "bf_blenlib")
BLENDER_TEST(BLI_string "bf_blenlib")
BLENDER_TEST(BLI_string_utf8 "bf_blenlib")