From 6726cfd54b39f593d3bc4d248e4ff9e4676c3885 Mon Sep 17 00:00:00 2001 From: Vaxry Date: Wed, 7 May 2025 17:50:22 +0100 Subject: [PATCH] parser: add support for basic arithmetic Adds support for expressions that take left and right hand side and an operation (+-*/) -> e.g. fixes #67 --- CMakeLists.txt | 2 +- src/config.cpp | 79 +++++++++++++++++++++++++++++++++++++--- src/config.hpp | 2 + tests/config/config.conf | 3 ++ tests/parse/main.cpp | 9 +++++ 5 files changed, 89 insertions(+), 6 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4ec92f4..ccb2053 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -37,7 +37,7 @@ add_compile_options( set(CMAKE_EXPORT_COMPILE_COMMANDS TRUE) find_package(PkgConfig REQUIRED) -pkg_check_modules(deps REQUIRED IMPORTED_TARGET hyprutils>=0.1.1) +pkg_check_modules(deps REQUIRED IMPORTED_TARGET hyprutils>=0.7.1) file(GLOB_RECURSE SRCFILES CONFIGURE_DEPENDS "src/*.cpp" "include/hyprlang.hpp") diff --git a/src/config.cpp b/src/config.cpp index f1c01ca..a4f99d9 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -12,6 +12,7 @@ #include #include #include +#include using namespace Hyprlang; using namespace Hyprutils::String; @@ -38,7 +39,7 @@ static size_t seekABIStructSize(const void* begin, size_t startOffset, size_t ma return 0; } -static std::expected getNextLine(std::istream& str, int &rawLineNum, int &lineNum) { +static std::expected getNextLine(std::istream& str, int& rawLineNum, int& lineNum) { std::string line = ""; std::string nextLine = ""; @@ -510,6 +511,56 @@ void CConfigImpl::parseComment(const std::string& comment) { currentFlags.noError = args[2] == "true" || args[2] == "yes" || args[2] == "enable" || args[2] == "enabled" || args[2] == "set"; } +std::expected CConfigImpl::parseExpression(const std::string& s) { + // for now, we only support very basic expressions. + // + - * / and only one per $() + // TODO: something better + + if (s.empty()) + return std::unexpected("Expression is empty"); + + CConstVarList args(s, 0, 's', true); + + if (args[1] != "+" && args[1] != "-" && args[1] != "*" && args[1] != "/") + return std::unexpected("Invalid expression type: supported +, -, *, /"); + + auto LHS_VAR = std::ranges::find_if(variables, [&](const auto& v) { return v.name == args[0]; }); + auto RHS_VAR = std::ranges::find_if(variables, [&](const auto& v) { return v.name == args[2]; }); + + float left = 0; + float right = 0; + + if (LHS_VAR != variables.end()) { + try { + left = std::stof(LHS_VAR->value); + } catch (...) { return std::unexpected("Failed to parse expression: value 1 holds a variable that does not look like a number"); } + } else { + try { + left = std::stof(std::string{args[0]}); + } catch (...) { return std::unexpected("Failed to parse expression: value 1 does not look like a number or the variable doesn't exist"); } + } + + if (RHS_VAR != variables.end()) { + try { + right = std::stof(RHS_VAR->value); + } catch (...) { return std::unexpected("Failed to parse expression: value 1 holds a variable that does not look like a number"); } + } else { + try { + right = std::stof(std::string{args[2]}); + } catch (...) { return std::unexpected("Failed to parse expression: value 1 does not look like a number or the variable doesn't exist"); } + } + + switch (args[1][0]) { + case '+': return left + right; + case '-': return left - right; + case '*': return left * right; + case '/': return left / right; + default: break; + } + + return std::unexpected("Unknown error while parsing expression"); +} + CParseResult CConfig::parseLine(std::string line, bool dynamic) { CParseResult result; @@ -571,6 +622,8 @@ CParseResult CConfig::parseLine(std::string line, bool dynamic) { // limit unwrapping iterations to 100. if exceeds, raise error for (size_t i = 0; i < 100; ++i) { bool anyMatch = false; + + // parse variables for (auto& var : impl->variables) { // don't parse LHS variables if this is a variable... const auto LHSIT = ISVARIABLE ? std::string::npos : LHS.find("$" + var.name); @@ -589,6 +642,24 @@ CParseResult CConfig::parseLine(std::string line, bool dynamic) { anyMatch = true; } + // parse expressions $(somevar + 2) + // We only support single expressions for now + while (RHS.contains("$(")) { + const auto BEGIN_EXPR = RHS.find("$("); + const auto END_EXPR = RHS.find(')', BEGIN_EXPR + 2); + if (END_EXPR != std::string::npos) { + // try to parse the expression + const auto RESULT = impl->parseExpression(RHS.substr(BEGIN_EXPR + 2, END_EXPR - BEGIN_EXPR - 2)); + if (!RESULT.has_value()) { + result.setError(RESULT.error()); + return result; + } + + RHS = RHS.substr(0, BEGIN_EXPR) + std::format("{}", RESULT.value()) + RHS.substr(END_EXPR + 1); + } else + break; + } + if (!anyMatch) break; @@ -728,8 +799,7 @@ CParseResult CConfig::parseRawStream(const std::string& stream) { if (!line) { switch (line.error()) { - case GETNEXTLINEFAILURE_EOF: - break; + case GETNEXTLINEFAILURE_EOF: break; case GETNEXTLINEFAILURE_BACKSLASH: if (!impl->parseError.empty()) impl->parseError += "\n"; @@ -781,8 +851,7 @@ CParseResult CConfig::parseFile(const char* file) { if (!line) { switch (line.error()) { - case GETNEXTLINEFAILURE_EOF: - break; + case GETNEXTLINEFAILURE_EOF: break; case GETNEXTLINEFAILURE_BACKSLASH: if (!impl->parseError.empty()) impl->parseError += "\n"; diff --git a/src/config.hpp b/src/config.hpp index 764b348..c6e9c1e 100644 --- a/src/config.hpp +++ b/src/config.hpp @@ -4,6 +4,7 @@ #include #include #include +#include struct SHandler { std::string name = ""; @@ -95,6 +96,7 @@ class CConfigImpl { Hyprlang::SConfigOptions configOptions; void parseComment(const std::string& comment); + std::expected parseExpression(const std::string& s); struct { bool noError = false; diff --git a/tests/config/config.conf b/tests/config/config.conf index b7f7b6e..0087f1b 100644 --- a/tests/config/config.conf +++ b/tests/config/config.conf @@ -14,6 +14,9 @@ $MY_VAR = 1337 $MY_VAR_2 = $MY_VAR testVar = $MY_VAR$MY_VAR_2 +$EXPR_VAR = $(MY_VAR + 2) +testExpr = $(EXPR_VAR - 4) + testEnv = $SHELL source = ./colors.conf diff --git a/tests/parse/main.cpp b/tests/parse/main.cpp index f0397ca..2ceeb0c 100644 --- a/tests/parse/main.cpp +++ b/tests/parse/main.cpp @@ -107,6 +107,7 @@ int main(int argc, char** argv, char** envp) { // setup config config.addConfigValue("testInt", (Hyprlang::INT)0); + config.addConfigValue("testExpr", (Hyprlang::INT)0); config.addConfigValue("testFloat", 0.F); config.addConfigValue("testVec", Hyprlang::SVector2D{.x = 69, .y = 420}); config.addConfigValue("testString", ""); @@ -197,6 +198,10 @@ int main(int argc, char** argv, char** envp) { EXPECT(*T3, EXP); EXPECT(*T4, "Hello World! # This is not a comment!"); + // test expressions + std::cout << " → Testing expressions\n"; + EXPECT(std::any_cast(config.getConfigValue("testExpr")), 1335); + // test static values std::cout << " → Testing static values\n"; static auto* const PTESTINT = config.getConfigValuePtr("testInt")->getDataStaticPtr(); @@ -243,6 +248,10 @@ int main(int argc, char** argv, char** envp) { EXPECT(config.parseDynamic("$RECURSIVE1 = d").error, false); EXPECT(std::any_cast(config.getConfigValue("testStringRecursive")), std::string{"dbc"}); + // test dynamic exprs + EXPECT(config.parseDynamic("testExpr = $(EXPR_VAR * 2)").error, false); + EXPECT(std::any_cast(config.getConfigValue("testExpr")), 1339L * 2); + // test env variables std::cout << " → Testing env variables\n"; EXPECT(std::any_cast(config.getConfigValue("testEnv")), std::string{getenv("SHELL")});