forked from wpilibsuite/allwpilib
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[glass] Add math expression input for NetworkTables numerical values (w…
- Loading branch information
Showing
5 changed files
with
492 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,374 @@ | ||
// Copyright (c) FIRST and other WPILib contributors. | ||
// Open Source Software; you can modify and/or share it under the terms of | ||
// the WPILib BSD license file in the root directory of this project. | ||
|
||
#include "glass/support/ExpressionParser.h" | ||
|
||
#include <cmath> | ||
#include <stack> | ||
#include <type_traits> | ||
|
||
#include <wpi/StringExtras.h> | ||
#include <wpi/expected> | ||
|
||
namespace glass::expression { | ||
|
||
enum class TokenType { | ||
Number, | ||
Add, | ||
Subtract, | ||
Multiply, | ||
Divide, | ||
Exponent, | ||
OpenParen, | ||
CloseParen, | ||
|
||
End, // Hit end of input | ||
Error, // Invalid character found | ||
}; | ||
|
||
struct Token { | ||
TokenType type; | ||
const char* str; | ||
int strLen; | ||
|
||
explicit Token(TokenType type) : type(type), str(nullptr), strLen(0) {} | ||
|
||
Token(TokenType type, const char* str, int strLen) | ||
: type(type), str(str), strLen(strLen) {} | ||
}; | ||
|
||
class Lexer { | ||
public: | ||
explicit Lexer(const char* input, bool isInteger) | ||
: input(input), isInteger(isInteger) {} | ||
|
||
Token NextToken() { | ||
// Skip leading whitespace | ||
startIdx = currentIdx; | ||
while (std::isspace(input[startIdx])) { | ||
startIdx++; | ||
} | ||
if (input[startIdx] == 0) { | ||
return Token(TokenType::End); | ||
} | ||
currentIdx = startIdx; | ||
|
||
char c = input[currentIdx]; | ||
currentIdx++; | ||
switch (c) { | ||
case '+': | ||
return Token(TokenType::Add); | ||
case '-': | ||
return Token(TokenType::Subtract); | ||
case '*': | ||
return Token(TokenType::Multiply); | ||
case '/': | ||
return Token(TokenType::Divide); | ||
case '^': | ||
return Token(TokenType::Exponent); | ||
case '(': | ||
return Token(TokenType::OpenParen); | ||
case ')': | ||
return Token(TokenType::CloseParen); | ||
default: | ||
currentIdx--; | ||
if (wpi::isDigit(c) || c == '.') { | ||
return nextNumber(); | ||
} | ||
return Token(TokenType::Error, &input[currentIdx], 1); | ||
} | ||
} | ||
|
||
// Makes NextToken() return the same token as previously | ||
void Repeat() { currentIdx = startIdx; } | ||
|
||
private: | ||
const char* input; | ||
bool isInteger; | ||
int startIdx = 0, currentIdx = 0; | ||
|
||
Token nextNumber() { | ||
// Read whole part | ||
bool hasDigitsBeforeDecimal = false; | ||
while (wpi::isDigit(input[currentIdx])) { | ||
currentIdx++; | ||
hasDigitsBeforeDecimal = true; | ||
} | ||
|
||
// Read decimal part if it exists | ||
if (input[currentIdx] == '.') { | ||
// Integers can't have fractional part | ||
if (isInteger) { | ||
return Token(TokenType::Error, &input[currentIdx], 1); | ||
} | ||
|
||
currentIdx++; | ||
// Report a single '.' with no digits as an error | ||
if (!hasDigitsBeforeDecimal && !wpi::isDigit(input[currentIdx])) { | ||
// Report the decimal as the unexpected char | ||
return Token(TokenType::Error, &input[currentIdx - 1], 1); | ||
} | ||
|
||
while (wpi::isDigit(input[currentIdx])) { | ||
currentIdx++; | ||
} | ||
|
||
// Make sure the number has at most one decimal point | ||
if (input[currentIdx] == '.') { | ||
return Token(TokenType::Error, &input[currentIdx], 1); | ||
} | ||
} | ||
|
||
return Token(TokenType::Number, input + startIdx, currentIdx - startIdx); | ||
} | ||
}; | ||
|
||
enum class Operator { Add, Subtract, Multiply, Divide, Exponent, Negate, None }; | ||
|
||
Operator GetOperator(TokenType type) { | ||
switch (type) { | ||
case TokenType::Add: | ||
return Operator::Add; | ||
case TokenType::Subtract: | ||
return Operator::Subtract; | ||
case TokenType::Multiply: | ||
return Operator::Multiply; | ||
case TokenType::Divide: | ||
return Operator::Divide; | ||
case TokenType::Exponent: | ||
return Operator::Exponent; | ||
default: | ||
return Operator::None; | ||
} | ||
} | ||
|
||
int OperatorPrecedence(Operator op) { | ||
switch (op) { | ||
case Operator::Add: | ||
case Operator::Subtract: | ||
return 1; | ||
case Operator::Multiply: | ||
case Operator::Divide: | ||
return 2; | ||
case Operator::Exponent: | ||
return 3; | ||
case Operator::Negate: | ||
return 4; | ||
case Operator::None: | ||
return 0; | ||
} | ||
return 0; | ||
} | ||
|
||
bool IsOperatorRightAssociative(Operator op) { | ||
return op == Operator::Exponent || op == Operator::Negate; | ||
} | ||
|
||
template <typename V> | ||
void ApplyOperator(std::stack<V>& valStack, Operator op) { | ||
V right = valStack.top(); | ||
valStack.pop(); | ||
V left = valStack.top(); | ||
valStack.pop(); | ||
|
||
V val = 0; | ||
switch (op) { | ||
case Operator::Add: | ||
val = left + right; | ||
break; | ||
case Operator::Subtract: | ||
val = left - right; | ||
break; | ||
case Operator::Multiply: | ||
val = left * right; | ||
break; | ||
case Operator::Divide: | ||
val = left / right; | ||
break; | ||
case Operator::Exponent: | ||
val = std::pow(left, right); | ||
break; | ||
case Operator::Negate: | ||
val = -right; | ||
break; | ||
case Operator::None: | ||
break; | ||
} | ||
|
||
valStack.push(val); | ||
} | ||
|
||
template <typename V> | ||
std::optional<V> ValueFromString(std::string_view str); | ||
|
||
template <> | ||
std::optional<int64_t> ValueFromString(std::string_view str) { | ||
return wpi::parse_integer<int64_t>(str, 10); | ||
} | ||
|
||
template <> | ||
std::optional<float> ValueFromString(std::string_view str) { | ||
return wpi::parse_float<float>(str); | ||
} | ||
|
||
template <> | ||
std::optional<double> ValueFromString(std::string_view str) { | ||
return wpi::parse_float<double>(str); | ||
} | ||
|
||
template <typename V> | ||
wpi::expected<V, std::string> EvalAll(std::stack<Operator>& operStack, | ||
std::stack<V>& valStack) { | ||
while (!operStack.empty()) { | ||
if (valStack.size() < 2) { | ||
return wpi::unexpected("Missing operand"); | ||
} | ||
ApplyOperator<V>(valStack, operStack.top()); | ||
operStack.pop(); | ||
} | ||
if (valStack.empty()) { | ||
return wpi::unexpected("No value"); | ||
} | ||
|
||
// Intentionally leaves the result value on top of valStack so unmatched | ||
// closing parentheses work | ||
return valStack.top(); | ||
} | ||
|
||
template <typename V> | ||
wpi::expected<V, std::string> ParseExpr(Lexer& lexer, bool insideParen) { | ||
std::stack<Operator> operStack; | ||
std::stack<V> valStack; | ||
|
||
bool prevWasOp = true; | ||
TokenType prevType = TokenType::Add; | ||
|
||
while (true) { | ||
Token token = lexer.NextToken(); | ||
|
||
bool wasOp = false; | ||
switch (token.type) { | ||
case TokenType::End: | ||
goto end; | ||
case TokenType::Number: { | ||
// Check for two numbers in a row (ex: "2 4"). Implicit multiplication | ||
// is probably not what the user intended in this case, so give them an | ||
// error. | ||
if (prevType == TokenType::Number) { | ||
return wpi::unexpected("Missing operator"); | ||
} | ||
|
||
// Implicit multiplication. Ex: "2(4 + 5)" | ||
if (!prevWasOp) { | ||
operStack.push(Operator::Multiply); | ||
} | ||
|
||
auto value = | ||
ValueFromString<V>(std::string_view(token.str, token.strLen)); | ||
if (value) { | ||
valStack.push(value.value()); | ||
} else { | ||
return wpi::unexpected("Invalid number"); | ||
} | ||
|
||
break; | ||
} | ||
|
||
case TokenType::OpenParen: { | ||
// Implicit multiplication | ||
if (!prevWasOp) { | ||
operStack.push(Operator::Multiply); | ||
} | ||
|
||
wpi::expected<V, std::string> result = ParseExpr<V>(lexer, true); | ||
if (!result) { | ||
return result; | ||
} | ||
valStack.push(result.value()); | ||
|
||
TokenType nextType = lexer.NextToken().type; | ||
if (nextType != TokenType::CloseParen) { | ||
if (nextType == TokenType::End) { | ||
goto end; // Act as if closed at end of expression | ||
} | ||
return wpi::unexpected("Expected )"); | ||
} | ||
break; | ||
} | ||
|
||
case TokenType::CloseParen: { | ||
if (insideParen) { | ||
lexer.Repeat(); | ||
goto end; | ||
} | ||
|
||
// Acts as if there was open paren at start of expression. EvalAll will | ||
// clear both stacks, and leave the result value on top of valStack. | ||
// This makes sure everything inside the parentheses is evaluated first | ||
wpi::expected<V, std::string> result = EvalAll<V>(operStack, valStack); | ||
if (!result) { | ||
return result; | ||
} | ||
break; | ||
} | ||
|
||
case TokenType::Error: | ||
return wpi::unexpected(std::string("Unexpected character: ") | ||
.append(token.str, token.strLen)); | ||
|
||
default: | ||
Operator op = GetOperator(token.type); | ||
if (op == Operator::None) { | ||
lexer.Repeat(); | ||
goto end; | ||
} | ||
if (op == Operator::Subtract && prevWasOp) { | ||
op = Operator::Negate; | ||
// Dummy left-hand side for negation | ||
valStack.push(0.0); | ||
} | ||
wasOp = true; | ||
|
||
while (!operStack.empty()) { | ||
Operator prevOp = operStack.top(); | ||
|
||
bool rightAssoc = IsOperatorRightAssociative(op); | ||
int precedence = OperatorPrecedence(op); | ||
int prevPrecedence = OperatorPrecedence(prevOp); | ||
|
||
if ((!rightAssoc && precedence == prevPrecedence) || | ||
precedence < prevPrecedence) { | ||
operStack.pop(); | ||
if (valStack.size() < 2) { | ||
return wpi::unexpected("Missing operand"); | ||
} | ||
ApplyOperator<V>(valStack, prevOp); | ||
} else { | ||
break; | ||
} | ||
} | ||
operStack.push(op); | ||
break; | ||
} | ||
prevType = token.type; | ||
prevWasOp = wasOp; | ||
} | ||
|
||
// Reached the end of the expression | ||
end: | ||
return EvalAll<V>(operStack, valStack); | ||
} | ||
|
||
// expr is null-terminated string, as ImGui::inputText() uses | ||
template <typename V> | ||
wpi::expected<V, std::string> TryParseExpr(const char* expr) { | ||
Lexer lexer(expr, std::is_integral_v<V>); | ||
return ParseExpr<V>(lexer, false); | ||
} | ||
|
||
template wpi::expected<double, std::string> TryParseExpr(const char*); | ||
template wpi::expected<float, std::string> TryParseExpr(const char*); | ||
template wpi::expected<int64_t, std::string> TryParseExpr(const char*); | ||
|
||
} // namespace glass::expression |
Oops, something went wrong.