diff --git a/glass/src/lib/native/cpp/support/ExpressionParser.cpp b/glass/src/lib/native/cpp/support/ExpressionParser.cpp new file mode 100644 index 0000000000..6522b3cf8b --- /dev/null +++ b/glass/src/lib/native/cpp/support/ExpressionParser.cpp @@ -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 +#include +#include + +#include +#include + +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 +void ApplyOperator(std::stack& 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 +std::optional ValueFromString(std::string_view str); + +template <> +std::optional ValueFromString(std::string_view str) { + return wpi::parse_integer(str, 10); +} + +template <> +std::optional ValueFromString(std::string_view str) { + return wpi::parse_float(str); +} + +template <> +std::optional ValueFromString(std::string_view str) { + return wpi::parse_float(str); +} + +template +wpi::expected EvalAll(std::stack& operStack, + std::stack& valStack) { + while (!operStack.empty()) { + if (valStack.size() < 2) { + return wpi::unexpected("Missing operand"); + } + ApplyOperator(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 +wpi::expected ParseExpr(Lexer& lexer, bool insideParen) { + std::stack operStack; + std::stack 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(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 result = ParseExpr(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 result = EvalAll(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(valStack, prevOp); + } else { + break; + } + } + operStack.push(op); + break; + } + prevType = token.type; + prevWasOp = wasOp; + } + +// Reached the end of the expression +end: + return EvalAll(operStack, valStack); +} + +// expr is null-terminated string, as ImGui::inputText() uses +template +wpi::expected TryParseExpr(const char* expr) { + Lexer lexer(expr, std::is_integral_v); + return ParseExpr(lexer, false); +} + +template wpi::expected TryParseExpr(const char*); +template wpi::expected TryParseExpr(const char*); +template wpi::expected TryParseExpr(const char*); + +} // namespace glass::expression diff --git a/glass/src/lib/native/cpp/support/ExtraGuiWidgets.cpp b/glass/src/lib/native/cpp/support/ExtraGuiWidgets.cpp index dede4a0b7d..9e09fc1b8e 100644 --- a/glass/src/lib/native/cpp/support/ExtraGuiWidgets.cpp +++ b/glass/src/lib/native/cpp/support/ExtraGuiWidgets.cpp @@ -8,7 +8,14 @@ #include #include +#include +#include +#include + +#include + #include "glass/DataSource.h" +#include "glass/support/ExpressionParser.h" namespace glass { @@ -217,4 +224,70 @@ bool HamburgerButton(const ImGuiID id, const ImVec2 position) { return pressed; } +static const int kBufferSize = 256; + +struct InputExprState { + char inputBuffer[kBufferSize]; +}; + +static wpi::DenseMap exprStates; +// Shared string buffer for inactive inputs +static char previewBuffer[kBufferSize]; + +template +bool InputExpr(const char* label, V* v, const char* format, + ImGuiInputTextFlags flags) { + int id = ImGui::GetID(label); + + char* inputBuffer; + bool hasState = exprStates.contains(id); + if (hasState) { + InputExprState& state = exprStates[id]; + inputBuffer = state.inputBuffer; + } else { + inputBuffer = previewBuffer; +#ifdef __GNUC__ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wformat-nonliteral" +#endif + // Preview stored value + std::snprintf(inputBuffer, kBufferSize, format, *v); +#ifdef __GNUC__ +#pragma GCC diagnostic pop +#endif + } + + bool changed = ImGui::InputText(label, inputBuffer, kBufferSize, flags); + bool active = ImGui::IsItemActive(); + + if (active || changed) { + InputExprState& state = exprStates[id]; + if (!hasState) { + // State was just created, copy in contents of preview buffer + std::strncpy(state.inputBuffer, previewBuffer, kBufferSize); + } + + // Attempt to parse current value + auto result = glass::expression::TryParseExpr(state.inputBuffer); + if (result) { + *v = result.value(); + } else if (active) { + ImGui::TextColored(ImVec4(1.0f, 0.0f, 0.0f, 1.0f), "%s", + result.error().c_str()); + } + } + + // Don't need the state anymore if not editing + if (!active) { + exprStates.erase(id); + } + + return changed; +} + +template bool InputExpr(const char*, int64_t*, const char*, + ImGuiInputTextFlags); +template bool InputExpr(const char*, float*, const char*, ImGuiInputTextFlags); +template bool InputExpr(const char*, double*, const char*, ImGuiInputTextFlags); + } // namespace glass diff --git a/glass/src/lib/native/include/glass/support/ExpressionParser.h b/glass/src/lib/native/include/glass/support/ExpressionParser.h new file mode 100644 index 0000000000..ec03a93fb0 --- /dev/null +++ b/glass/src/lib/native/include/glass/support/ExpressionParser.h @@ -0,0 +1,22 @@ +// 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. + +#pragma once + +#include + +#include + +#include + +namespace glass::expression { + +template +wpi::expected TryParseExpr(const char* expr); + +extern template wpi::expected TryParseExpr(const char*); +extern template wpi::expected TryParseExpr(const char*); +extern template wpi::expected TryParseExpr(const char*); + +} // namespace glass::expression diff --git a/glass/src/lib/native/include/glass/support/ExtraGuiWidgets.h b/glass/src/lib/native/include/glass/support/ExtraGuiWidgets.h index d56f3426f7..ec4f20ab92 100644 --- a/glass/src/lib/native/include/glass/support/ExtraGuiWidgets.h +++ b/glass/src/lib/native/include/glass/support/ExtraGuiWidgets.h @@ -4,6 +4,8 @@ #pragma once +#include + #define IMGUI_DEFINE_MATH_OPERATORS #include @@ -99,4 +101,17 @@ bool HeaderDeleteButton(const char* label); */ bool HamburgerButton(const ImGuiID id, const ImVec2 position); +/** + * Edit a value with expression input. Similar to ImGui::InputScalar() + */ +template +bool InputExpr(const char* label, V* v, const char* format, + ImGuiInputTextFlags flags = 0); +extern template bool InputExpr(const char*, int64_t*, const char*, + ImGuiInputTextFlags); +extern template bool InputExpr(const char*, float*, const char*, + ImGuiInputTextFlags); +extern template bool InputExpr(const char*, double*, const char*, + ImGuiInputTextFlags); + } // namespace glass diff --git a/glass/src/libnt/native/cpp/NetworkTables.cpp b/glass/src/libnt/native/cpp/NetworkTables.cpp index 71ff8439ab..c5127fd9b8 100644 --- a/glass/src/libnt/native/cpp/NetworkTables.cpp +++ b/glass/src/libnt/native/cpp/NetworkTables.cpp @@ -33,6 +33,7 @@ #include "glass/Context.h" #include "glass/DataSource.h" #include "glass/Storage.h" +#include "glass/support/ExtraGuiWidgets.h" using namespace glass; using namespace mpack; @@ -1383,8 +1384,8 @@ static void EmitEntryValueEditable(NetworkTablesModel* model, } case NT_INTEGER: { int64_t v = val.GetInteger(); - if (ImGui::InputScalar(typeStr, ImGuiDataType_S64, &v, nullptr, nullptr, - nullptr, ImGuiInputTextFlags_EnterReturnsTrue)) { + if (InputExpr(typeStr, &v, "%d", + ImGuiInputTextFlags_EnterReturnsTrue)) { if (entry.publisher == 0) { entry.publisher = nt::Publish(entry.info.topic, NT_INTEGER, "int"); } @@ -1394,8 +1395,8 @@ static void EmitEntryValueEditable(NetworkTablesModel* model, } case NT_FLOAT: { float v = val.GetFloat(); - if (ImGui::InputFloat(typeStr, &v, 0, 0, "%.6f", - ImGuiInputTextFlags_EnterReturnsTrue)) { + if (InputExpr(typeStr, &v, "%.6f", + ImGuiInputTextFlags_EnterReturnsTrue)) { if (entry.publisher == 0) { entry.publisher = nt::Publish(entry.info.topic, NT_FLOAT, "float"); } @@ -1411,9 +1412,9 @@ static void EmitEntryValueEditable(NetworkTablesModel* model, #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wformat-nonliteral" #endif - if (ImGui::InputDouble(typeStr, &v, 0, 0, - fmt::format("%.{}f", precision).c_str(), - ImGuiInputTextFlags_EnterReturnsTrue)) { + if (InputExpr(typeStr, &v, + fmt::format("%.{}f", precision).c_str(), + ImGuiInputTextFlags_EnterReturnsTrue)) { if (entry.publisher == 0) { entry.publisher = nt::Publish(entry.info.topic, NT_DOUBLE, "double"); }