From 99223d0a3c8848fbef5d8ce16f834747c8efe2e5 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Thu, 3 Dec 2015 15:02:00 -0800 Subject: [PATCH] AnimExpression: support for parsing simple expressions supports parens, binary +, -, / and *. / and * have higher precedence then + and - --- libraries/animation/src/AnimExpression.cpp | 349 +++++++++++++++++---- libraries/animation/src/AnimExpression.h | 24 +- tests/animation/src/AnimTests.cpp | 174 ++++++---- tests/animation/src/AnimTests.h | 1 + 4 files changed, 417 insertions(+), 131 deletions(-) diff --git a/libraries/animation/src/AnimExpression.cpp b/libraries/animation/src/AnimExpression.cpp index a03925f8f9..b76aaff3f9 100644 --- a/libraries/animation/src/AnimExpression.cpp +++ b/libraries/animation/src/AnimExpression.cpp @@ -17,9 +17,16 @@ AnimExpression::AnimExpression(const QString& str) : _expression(str) { auto iter = str.begin(); - parseExpression(_expression, iter); + parseExpr(_expression, iter); + while(!_tokenStack.empty()) { + _tokenStack.pop(); + } } +// +// Tokenizer +// + void AnimExpression::unconsumeToken(const Token& token) { _tokenStack.push(token); } @@ -49,6 +56,7 @@ AnimExpression::Token AnimExpression::consumeToken(const QString& str, QString:: case '-': ++iter; return Token(Token::Minus); case '+': ++iter; return Token(Token::Plus); case '*': ++iter; return Token(Token::Multiply); + case '/': ++iter; return Token(Token::Divide); case '%': ++iter; return Token(Token::Modulus); case ',': ++iter; return Token(Token::Comma); default: @@ -184,73 +192,137 @@ AnimExpression::Token AnimExpression::consumeNot(const QString& str, QString::co } } -bool AnimExpression::parseExpression(const QString& str, QString::const_iterator& iter) { +// +// Parser +// + +/* +Expr → Term Expr' +Expr' → + Term Expr' + | – Term Expr' + | ε +Term → Factor Term' +Term' → * Term' + | / Term' + | ε +Factor → INT + | FLOAT + | IDENTIFIER + | (Expr) +*/ + +bool AnimExpression::parseExpr(const QString& str, QString::const_iterator& iter) { + if (!parseTerm(str, iter)) { + return false; + } + if (!parseExprPrime(str, iter)) { + return false; + } + return true; +} + +bool AnimExpression::parseExprPrime(const QString& str, QString::const_iterator& iter) { auto token = consumeToken(str, iter); - if (token.type == Token::Identifier) { - if (token.strVal == "true") { - _opCodes.push_back(OpCode {true}); - } else if (token.strVal == "false") { - _opCodes.push_back(OpCode {false}); - } else { - _opCodes.push_back(OpCode {token.strVal}); + if (token.type == Token::Plus) { + if (!parseTerm(str, iter)) { + unconsumeToken(token); + return false; } + if (!parseExprPrime(str, iter)) { + unconsumeToken(token); + return false; + } + _opCodes.push_back(OpCode {OpCode::Add}); return true; - } else if (token.type == Token::Int) { + } else if (token.type == Token::Minus) { + if (!parseTerm(str, iter)) { + unconsumeToken(token); + return false; + } + if (!parseExprPrime(str, iter)) { + unconsumeToken(token); + return false; + } + _opCodes.push_back(OpCode {OpCode::Subtract}); + return true; + } else { + unconsumeToken(token); + return true; + } +} + +bool AnimExpression::parseTerm(const QString& str, QString::const_iterator& iter) { + if (!parseFactor(str, iter)) { + return false; + } + if (!parseTermPrime(str, iter)) { + return false; + } + return true; +} + +bool AnimExpression::parseTermPrime(const QString& str, QString::const_iterator& iter) { + auto token = consumeToken(str, iter); + if (token.type == Token::Multiply) { + if (!parseTerm(str, iter)) { + unconsumeToken(token); + return false; + } + if (!parseTermPrime(str, iter)) { + unconsumeToken(token); + return false; + } + _opCodes.push_back(OpCode {OpCode::Multiply}); + return true; + } else if (token.type == Token::Divide) { + if (!parseTerm(str, iter)) { + unconsumeToken(token); + return false; + } + if (!parseTermPrime(str, iter)) { + unconsumeToken(token); + return false; + } + _opCodes.push_back(OpCode {OpCode::Divide}); + return true; + } else { + unconsumeToken(token); + return true; + } +} + +bool AnimExpression::parseFactor(const QString& str, QString::const_iterator& iter) { + auto token = consumeToken(str, iter); + if (token.type == Token::Int) { _opCodes.push_back(OpCode {token.intVal}); return true; } else if (token.type == Token::Float) { _opCodes.push_back(OpCode {token.floatVal}); return true; + } else if (token.type == Token::Identifier) { + _opCodes.push_back(OpCode {token.strVal}); + return true; } else if (token.type == Token::LeftParen) { - if (parseUnaryExpression(str, iter)) { - token = consumeToken(str, iter); - if (token.type != Token::RightParen) { - qCCritical(animation) << "Error parsing expression, expected ')'"; - return false; - } else { - return true; - } - } else { + if (!parseExpr(str, iter)) { + unconsumeToken(token); return false; } + auto nextToken = consumeToken(str, iter); + if (nextToken.type != Token::RightParen) { + unconsumeToken(nextToken); + unconsumeToken(token); + return false; + } + return true; } else { unconsumeToken(token); - if (parseUnaryExpression(str, iter)) { - return true; - } else { - qCCritical(animation) << "Error parsing expression"; - return false; - } + return false; } } -bool AnimExpression::parseUnaryExpression(const QString& str, QString::const_iterator& iter) { - auto token = consumeToken(str, iter); - if (token.type == Token::Plus) { // unary plus is a no op. - if (parseExpression(str, iter)) { - return true; - } else { - return false; - } - } else if (token.type == Token::Minus) { - if (parseExpression(str, iter)) { - _opCodes.push_back(OpCode {OpCode::Minus}); - return true; - } else { - return false; - } - } else if (token.type == Token::Not) { - if (parseExpression(str, iter)) { - _opCodes.push_back(OpCode {OpCode::Not}); - return true; - } else { - return false; - } - } else { - unconsumeToken(token); - return parseExpression(str, iter); - } -} +// +// Evaluator +// AnimExpression::OpCode AnimExpression::evaluate(const AnimVariantMap& map) const { std::stack stack; @@ -274,8 +346,9 @@ AnimExpression::OpCode AnimExpression::evaluate(const AnimVariantMap& map) const case OpCode::Subtract: evalSubtract(map, stack); break; case OpCode::Add: evalAdd(map, stack); break; case OpCode::Multiply: evalMultiply(map, stack); break; + case OpCode::Divide: evalDivide(map, stack); break; case OpCode::Modulus: evalModulus(map, stack); break; - case OpCode::Minus: evalMinus(map, stack); break; + case OpCode::UnaryMinus: evalUnaryMinus(map, stack); break; } } return stack.top(); @@ -362,15 +435,107 @@ void AnimExpression::evalSubtract(const AnimVariantMap& map, std::stack& PUSH(0.0f); } -void AnimExpression::evalAdd(const AnimVariantMap& map, std::stack& stack) const { - OpCode lhs = stack.top(); stack.pop(); - OpCode rhs = stack.top(); stack.pop(); +void AnimExpression::add(int lhs, const OpCode& rhs, std::stack& stack) const { + switch (rhs.type) { + case OpCode::Bool: + case OpCode::Int: + PUSH(lhs + rhs.intVal); + break; + case OpCode::Float: + PUSH((float)lhs + rhs.floatVal); + break; + default: + PUSH(lhs); + } +} - // TODO: - PUSH(0.0f); +void AnimExpression::add(float lhs, const OpCode& rhs, std::stack& stack) const { + switch (rhs.type) { + case OpCode::Bool: + case OpCode::Int: + PUSH(lhs + (float)rhs.intVal); + break; + case OpCode::Float: + PUSH(lhs + rhs.floatVal); + break; + default: + PUSH(lhs); + } +} + +void AnimExpression::evalAdd(const AnimVariantMap& map, std::stack& stack) const { + OpCode lhs = coerseToValue(map, stack.top()); + stack.pop(); + OpCode rhs = coerseToValue(map, stack.top()); + stack.pop(); + + switch (lhs.type) { + case OpCode::Bool: + add(lhs.intVal, rhs, stack); + break; + case OpCode::Int: + add(lhs.intVal, rhs, stack); + break; + case OpCode::Float: + add(lhs.floatVal, rhs, stack); + break; + default: + add(0, rhs, stack); + break; + } } void AnimExpression::evalMultiply(const AnimVariantMap& map, std::stack& stack) const { + OpCode lhs = coerseToValue(map, stack.top()); + stack.pop(); + OpCode rhs = coerseToValue(map, stack.top()); + stack.pop(); + + switch(lhs.type) { + case OpCode::Bool: + mul(lhs.intVal, rhs, stack); + break; + case OpCode::Int: + mul(lhs.intVal, rhs, stack); + break; + case OpCode::Float: + mul(lhs.floatVal, rhs, stack); + break; + default: + mul(0, rhs, stack); + break; + } +} + +void AnimExpression::mul(int lhs, const OpCode& rhs, std::stack& stack) const { + switch (rhs.type) { + case OpCode::Bool: + case OpCode::Int: + PUSH(lhs * rhs.intVal); + break; + case OpCode::Float: + PUSH((float)lhs * rhs.floatVal); + break; + default: + PUSH(lhs); + } +} + +void AnimExpression::mul(float lhs, const OpCode& rhs, std::stack& stack) const { + switch (rhs.type) { + case OpCode::Bool: + case OpCode::Int: + PUSH(lhs * (float)rhs.intVal); + break; + case OpCode::Float: + PUSH(lhs * rhs.floatVal); + break; + default: + PUSH(lhs); + } +} + +void AnimExpression::evalDivide(const AnimVariantMap& map, std::stack& stack) const { OpCode lhs = stack.top(); stack.pop(); OpCode rhs = stack.top(); stack.pop(); @@ -386,7 +551,7 @@ void AnimExpression::evalModulus(const AnimVariantMap& map, std::stack& PUSH((int)0); } -void AnimExpression::evalMinus(const AnimVariantMap& map, std::stack& stack) const { +void AnimExpression::evalUnaryMinus(const AnimVariantMap& map, std::stack& stack) const { OpCode rhs = stack.top(); stack.pop(); switch (rhs.type) { @@ -429,3 +594,69 @@ void AnimExpression::evalMinus(const AnimVariantMap& map, std::stack& st break; } } + +AnimExpression::OpCode AnimExpression::coerseToValue(const AnimVariantMap& map, const OpCode& opCode) const { + switch (opCode.type) { + case OpCode::Identifier: + { + const AnimVariant& var = map.get(opCode.strVal); + switch (var.getType()) { + case AnimVariant::Type::Bool: + qCWarning(animation) << "AnimExpression: type missmatch, expected a number not a bool"; + return OpCode(0); + break; + case AnimVariant::Type::Int: + return OpCode(var.getInt()); + break; + case AnimVariant::Type::Float: + return OpCode(var.getFloat()); + break; + default: + // TODO: Vec3, Quat are unsupported + assert(false); + return OpCode(0); + break; + } + } + break; + case OpCode::Bool: + case OpCode::Int: + case OpCode::Float: + return opCode; + default: + qCCritical(animation) << "AnimExpression: ERROR expected a number, type = " << opCode.type; + assert(false); + return OpCode(0); + break; + } +} + +void AnimExpression::dumpOpCodes() const { + QString tmp; + for (auto& op : _opCodes) { + switch (op.type) { + case OpCode::Identifier: tmp += QString(" %1").arg(op.strVal); break; + case OpCode::Bool: tmp += QString(" %1").arg(op.intVal ? "true" : "false"); break; + case OpCode::Int: tmp += QString(" %1").arg(op.intVal); break; + case OpCode::Float: tmp += QString(" %1").arg(op.floatVal); break; + case OpCode::And: tmp += " &&"; break; + case OpCode::Or: tmp += " ||"; break; + case OpCode::GreaterThan: tmp += " >"; break; + case OpCode::GreaterThanEqual: tmp += " >="; break; + case OpCode::LessThan: tmp += " <"; break; + case OpCode::LessThanEqual: tmp += " <="; break; + case OpCode::Equal: tmp += " =="; break; + case OpCode::NotEqual: tmp += " !="; break; + case OpCode::Not: tmp += " !"; break; + case OpCode::Subtract: tmp += " -"; break; + case OpCode::Add: tmp += " +"; break; + case OpCode::Multiply: tmp += " *"; break; + case OpCode::Divide: tmp += " /"; break; + case OpCode::Modulus: tmp += " %"; break; + case OpCode::UnaryMinus: tmp += " unary-"; break; + default: tmp += " ???"; break; + } + } + qCDebug(animation).nospace().noquote() << "opCodes =" << tmp; + qCDebug(animation).resetFormat(); +} diff --git a/libraries/animation/src/AnimExpression.h b/libraries/animation/src/AnimExpression.h index afada44e86..6e204483d5 100644 --- a/libraries/animation/src/AnimExpression.h +++ b/libraries/animation/src/AnimExpression.h @@ -43,6 +43,7 @@ protected: Minus, Plus, Multiply, + Divide, Modulus, Comma, Error @@ -75,8 +76,9 @@ protected: Subtract, Add, Multiply, + Divide, Modulus, - Minus + UnaryMinus }; OpCode(Type type) : type {type} {} OpCode(const QStringRef& strRef) : type {Type::Identifier}, strVal {strRef.toString()} {} @@ -111,8 +113,11 @@ protected: Token consumeLessThan(const QString& str, QString::const_iterator& iter) const; Token consumeNot(const QString& str, QString::const_iterator& iter) const; - bool parseExpression(const QString& str, QString::const_iterator& iter); - bool parseUnaryExpression(const QString& str, QString::const_iterator& iter); + bool parseExpr(const QString& str, QString::const_iterator& iter); + bool parseExprPrime(const QString& str, QString::const_iterator& iter); + bool parseTerm(const QString& str, QString::const_iterator& iter); + bool parseTermPrime(const QString& str, QString::const_iterator& iter); + bool parseFactor(const QString& str, QString::const_iterator& iter); OpCode evaluate(const AnimVariantMap& map) const; void evalAnd(const AnimVariantMap& map, std::stack& stack) const; @@ -126,13 +131,24 @@ protected: void evalNot(const AnimVariantMap& map, std::stack& stack) const; void evalSubtract(const AnimVariantMap& map, std::stack& stack) const; void evalAdd(const AnimVariantMap& map, std::stack& stack) const; + void add(int lhs, const OpCode& rhs, std::stack& stack) const; + void add(float lhs, const OpCode& rhs, std::stack& stack) const; void evalMultiply(const AnimVariantMap& map, std::stack& stack) const; + void mul(int lhs, const OpCode& rhs, std::stack& stack) const; + void mul(float lhs, const OpCode& rhs, std::stack& stack) const; + void evalDivide(const AnimVariantMap& map, std::stack& stack) const; void evalModulus(const AnimVariantMap& map, std::stack& stack) const; - void evalMinus(const AnimVariantMap& map, std::stack& stack) const; + void evalUnaryMinus(const AnimVariantMap& map, std::stack& stack) const; + + OpCode coerseToValue(const AnimVariantMap& map, const OpCode& opCode) const; QString _expression; mutable std::stack _tokenStack; // TODO: remove, only needed during parsing std::vector _opCodes; + +#ifndef NDEBUG + void dumpOpCodes() const; +#endif }; #endif diff --git a/tests/animation/src/AnimTests.cpp b/tests/animation/src/AnimTests.cpp index 58fd3a1d35..6765553a25 100644 --- a/tests/animation/src/AnimTests.cpp +++ b/tests/animation/src/AnimTests.cpp @@ -366,82 +366,120 @@ void AnimTests::testExpressionParser() { vars.set("five", (float)5.0f); vars.set("forty", (float)40.0f); - AnimExpression e("(!f)"); - QVERIFY(e._opCodes.size() == 2); - if (e._opCodes.size() == 2) { - QVERIFY(e._opCodes[0].type == AnimExpression::OpCode::Identifier); - QVERIFY(e._opCodes[0].strVal == "f"); - QVERIFY(e._opCodes[1].type == AnimExpression::OpCode::Not); - - auto opCode = e.evaluate(vars); - QVERIFY(opCode.type == AnimExpression::OpCode::Bool); - QVERIFY((opCode.intVal != 0) == true); - } - - e = AnimExpression("!!f"); - QVERIFY(e._opCodes.size() == 3); - if (e._opCodes.size() == 3) { - QVERIFY(e._opCodes[0].type == AnimExpression::OpCode::Identifier); - QVERIFY(e._opCodes[0].strVal == "f"); - QVERIFY(e._opCodes[1].type == AnimExpression::OpCode::Not); - QVERIFY(e._opCodes[2].type == AnimExpression::OpCode::Not); - - auto opCode = e.evaluate(vars); - QVERIFY(opCode.type == AnimExpression::OpCode::Bool); - QVERIFY((opCode.intVal != 0) == false); - } - - e = AnimExpression("!!(!f)"); - QVERIFY(e._opCodes.size() == 4); - if (e._opCodes.size() == 4) { - QVERIFY(e._opCodes[0].type == AnimExpression::OpCode::Identifier); - QVERIFY(e._opCodes[0].strVal == "f"); - QVERIFY(e._opCodes[1].type == AnimExpression::OpCode::Not); - QVERIFY(e._opCodes[2].type == AnimExpression::OpCode::Not); - QVERIFY(e._opCodes[3].type == AnimExpression::OpCode::Not); - - auto opCode = e.evaluate(vars); - QVERIFY(opCode.type == AnimExpression::OpCode::Bool); - QVERIFY((opCode.intVal != 0) == true); - } -/* - e = AnimExpression("f || !f"); - QVERIFY(e._opCodes.size() == 4); - if (e._opCodes.size() == 4) { - QVERIFY(e._opCodes[0].type == AnimExpression::OpCode::Identifier); - QVERIFY(e._opCodes[0].strVal == "f"); - QVERIFY(e._opCodes[1].type == AnimExpression::OpCode::Identifier); - QVERIFY(e._opCodes[1].strVal == "f"); - QVERIFY(e._opCodes[2].type == AnimExpression::OpCode::Not); - QVERIFY(e._opCodes[3].type == AnimExpression::OpCode::Or); - - auto opCode = e.evaluate(vars); - QVERIFY(opCode.type == AnimExpression::OpCode::Bool); - QVERIFY((opCode.intVal != 0) == true); - } -*/ - e = AnimExpression("-10"); - QVERIFY(e._opCodes.size() == 2); + AnimExpression e("10"); + QVERIFY(e._opCodes.size() == 1); if (e._opCodes.size() == 1) { QVERIFY(e._opCodes[0].type == AnimExpression::OpCode::Int); QVERIFY(e._opCodes[0].intVal == 10); - QVERIFY(e._opCodes[1].type == AnimExpression::OpCode::Minus); - - auto opCode = e.evaluate(vars); - QVERIFY(opCode.type == AnimExpression::OpCode::Int); - QVERIFY(opCode.intVal == 10); } - e = AnimExpression("-ten"); - QVERIFY(e._opCodes.size() == 2); + e = AnimExpression("(10)"); + QVERIFY(e._opCodes.size() == 1); + if (e._opCodes.size() == 1) { + QVERIFY(e._opCodes[0].type == AnimExpression::OpCode::Int); + QVERIFY(e._opCodes[0].intVal == 10); + } + + e = AnimExpression("((10))"); + QVERIFY(e._opCodes.size() == 1); + if (e._opCodes.size() == 1) { + QVERIFY(e._opCodes[0].type == AnimExpression::OpCode::Int); + QVERIFY(e._opCodes[0].intVal == 10); + } + + e = AnimExpression("12.5"); + QVERIFY(e._opCodes.size() == 1); + if (e._opCodes.size() == 1) { + QVERIFY(e._opCodes[0].type == AnimExpression::OpCode::Float); + QVERIFY(e._opCodes[0].floatVal == 12.5f); + } + + e = AnimExpression("twenty"); + QVERIFY(e._opCodes.size() == 1); if (e._opCodes.size() == 1) { QVERIFY(e._opCodes[0].type == AnimExpression::OpCode::Identifier); - QVERIFY(e._opCodes[0].strVal == "ten"); - QVERIFY(e._opCodes[1].type == AnimExpression::OpCode::Minus); + QVERIFY(e._opCodes[0].strVal == "twenty"); + } - auto opCode = e.evaluate(vars); - QVERIFY(opCode.type == AnimExpression::OpCode::Int); - QVERIFY(opCode.intVal == 10); + e = AnimExpression("2 + 3"); + QVERIFY(e._opCodes.size() == 3); + if (e._opCodes.size() == 3) { + QVERIFY(e._opCodes[0].type == AnimExpression::OpCode::Int); + QVERIFY(e._opCodes[0].intVal == 2); + QVERIFY(e._opCodes[1].type == AnimExpression::OpCode::Int); + QVERIFY(e._opCodes[1].intVal == 3); + QVERIFY(e._opCodes[2].type == AnimExpression::OpCode::Add); + } + + e = AnimExpression("2 + 3 * 10"); + QVERIFY(e._opCodes.size() == 5); + if (e._opCodes.size() == 5) { + QVERIFY(e._opCodes[0].type == AnimExpression::OpCode::Int); + QVERIFY(e._opCodes[0].intVal == 2); + QVERIFY(e._opCodes[1].type == AnimExpression::OpCode::Int); + QVERIFY(e._opCodes[1].intVal == 3); + QVERIFY(e._opCodes[2].type == AnimExpression::OpCode::Int); + QVERIFY(e._opCodes[2].intVal == 10); + QVERIFY(e._opCodes[3].type == AnimExpression::OpCode::Multiply); + QVERIFY(e._opCodes[4].type == AnimExpression::OpCode::Add); + } + + e = AnimExpression("(2 + 3) * 10"); + QVERIFY(e._opCodes.size() == 5); + if (e._opCodes.size() == 5) { + QVERIFY(e._opCodes[0].type == AnimExpression::OpCode::Int); + QVERIFY(e._opCodes[0].intVal == 2); + QVERIFY(e._opCodes[1].type == AnimExpression::OpCode::Int); + QVERIFY(e._opCodes[1].intVal == 3); + QVERIFY(e._opCodes[2].type == AnimExpression::OpCode::Add); + QVERIFY(e._opCodes[3].type == AnimExpression::OpCode::Int); + QVERIFY(e._opCodes[3].intVal == 10); + QVERIFY(e._opCodes[4].type == AnimExpression::OpCode::Multiply); } } +void AnimTests::testExpressionEvaluator() { + auto vars = AnimVariantMap(); + vars.set("f", false); + vars.set("t", true); + vars.set("ten", (int)10); + vars.set("twenty", (int)20); + vars.set("five", (float)5.0f); + vars.set("forty", (float)40.0f); + + AnimExpression::OpCode result = AnimExpression("10").evaluate(vars); + QVERIFY(result.type == AnimExpression::OpCode::Int); + QVERIFY(result.intVal == 10); + + result = AnimExpression("(10)").evaluate(vars); + QVERIFY(result.type == AnimExpression::OpCode::Int); + QVERIFY(result.intVal == 10); + + result = AnimExpression("2 + 3").evaluate(vars); + QVERIFY(result.type == AnimExpression::OpCode::Int); + QVERIFY(result.intVal == 2 + 3); + + result = AnimExpression("2 + 3 * 5").evaluate(vars); + QVERIFY(result.type == AnimExpression::OpCode::Int); + QVERIFY(result.intVal == 2 + 3 * 5); + + result = AnimExpression("(2 + 3) * 5").evaluate(vars); + QVERIFY(result.type == AnimExpression::OpCode::Int); + QVERIFY(result.intVal == (2 + 3) * 5); + + result = AnimExpression("(2 + 3) * (5 + 3)").evaluate(vars); + QVERIFY(result.type == AnimExpression::OpCode::Int); + QVERIFY(result.intVal == (2 + 3) * (5 + 3)); + + result = AnimExpression("(ten + twenty) * 5").evaluate(vars); + QVERIFY(result.type == AnimExpression::OpCode::Int); + QVERIFY(result.intVal == (10 + 20) * 5); + + result = AnimExpression("(ten + twenty) * 5.0").evaluate(vars); + QVERIFY(result.type == AnimExpression::OpCode::Float); + QVERIFY(result.floatVal == (10 + 20) * 5.0f); + + result = AnimExpression("five * forty").evaluate(vars); + QVERIFY(result.type == AnimExpression::OpCode::Float); + QVERIFY(result.floatVal == 5.0f * 40.0f); +} diff --git a/tests/animation/src/AnimTests.h b/tests/animation/src/AnimTests.h index 94b3eddd25..a70acc21f7 100644 --- a/tests/animation/src/AnimTests.h +++ b/tests/animation/src/AnimTests.h @@ -28,6 +28,7 @@ private slots: void testAccumulateTime(); void testExpressionTokenizer(); void testExpressionParser(); + void testExpressionEvaluator(); }; #endif // hifi_AnimTests_h