From 5f5651661ffa13b6c55214d4daf2d7f6fbc61e4a Mon Sep 17 00:00:00 2001 From: Fromant Date: Wed, 15 Oct 2025 19:14:24 +0300 Subject: [PATCH 01/32] pluginManager done --- src/PluginManager.cpp | 64 +++++++++++++++++++++++++++++++++++++++++++ src/PluginManager.hpp | 34 +++++++++++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 src/PluginManager.cpp create mode 100644 src/PluginManager.hpp diff --git a/src/PluginManager.cpp b/src/PluginManager.cpp new file mode 100644 index 0000000..228ce41 --- /dev/null +++ b/src/PluginManager.cpp @@ -0,0 +1,64 @@ +#include +#include + +#include "PluginManager.hpp" + +PluginManager::~PluginManager() = default; + +void PluginManager::loadPluginsFromDirectory(const std::string& dirPath) { + namespace fs = std::filesystem; + if (!fs::exists(dirPath) || !fs::is_directory(dirPath)) { + std::cerr << "Plugins directory not found: " << dirPath << std::endl; + return; + } + + for (const auto& entry : fs::directory_iterator(dirPath)) { + if (entry.path().extension() != ".dll") continue; + + HMODULE hmod = LoadLibraryW(entry.path().c_str()); + if (!hmod) { + std::cerr << "Failed to load DLL: " << entry.path().filename().string() << std::endl; + continue; + } + + auto get_name = reinterpret_cast( + GetProcAddress(hmod, "get_function_name") + ); + auto evaluate = reinterpret_cast( + GetProcAddress(hmod, "evaluate") + ); + + if (!get_name || !evaluate) { + std::cerr << "DLL missing required exports: " << entry.path().filename().string() << std::endl; + FreeLibrary(hmod); + continue; + } + + try { + const char* name_cstr = get_name(); + if (!name_cstr || std::string(name_cstr).empty()) { + std::cerr << "Empty function name in: " << entry.path().filename().string() << std::endl; + FreeLibrary(hmod); + continue; + } + std::string name(name_cstr); + + // Wrap C function into std::function with exception safety + auto wrapper = [evaluate, name](double x) -> double { + // We assume evaluate may throw; we let it propagate + return evaluate(x); + }; + + auto handle = std::make_unique(hmod); + handle->func = wrapper; + plugins.emplace(name, std::move(handle)); + functions.emplace(name, wrapper); + + std::cout << "Loaded function: " << name << " from " << entry.path().filename().string() << std::endl; + } catch (const std::exception& e) { + std::cerr << "Error initializing plugin " << entry.path().filename().string() + << ": " << e.what() << std::endl; + FreeLibrary(hmod); + } + } +} \ No newline at end of file diff --git a/src/PluginManager.hpp b/src/PluginManager.hpp new file mode 100644 index 0000000..61643cb --- /dev/null +++ b/src/PluginManager.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include +#include +#include +#include + +#ifdef _WIN32 + #include +#else + #error "Only Windows is supported for plugins (DLLs)" +#endif + +using FunctionMap = std::map>; + +class PluginManager { + struct PluginHandle { + HMODULE module; + std::function func; + PluginHandle(HMODULE m) : module(m) {} + ~PluginHandle() { if (module) FreeLibrary(module); } + PluginHandle(const PluginHandle&) = delete; + PluginHandle& operator=(const PluginHandle&) = delete; + }; + + std::map> plugins; + FunctionMap functions; + +public: + void loadPluginsFromDirectory(const std::string& dirPath); + const FunctionMap& getFunctions() const { return functions; } + ~PluginManager(); +}; + From ec3487e4f05ea42114b09a2835d919a3337eefc8 Mon Sep 17 00:00:00 2001 From: Fromant Date: Wed, 15 Oct 2025 19:24:14 +0300 Subject: [PATCH 02/32] error thrown from plugin now contain function name --- src/PluginManager.cpp | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/PluginManager.cpp b/src/PluginManager.cpp index 228ce41..3b58a31 100644 --- a/src/PluginManager.cpp +++ b/src/PluginManager.cpp @@ -43,10 +43,15 @@ void PluginManager::loadPluginsFromDirectory(const std::string& dirPath) { } std::string name(name_cstr); - // Wrap C function into std::function with exception safety + //capture function and name by value auto wrapper = [evaluate, name](double x) -> double { - // We assume evaluate may throw; we let it propagate - return evaluate(x); + try { + return evaluate(x); + } + catch (std::exception& e) { + // throw exception with additional data for debugging + throw std::runtime_error("Error evaluating function " + name + ": " + e.what()); + } }; auto handle = std::make_unique(hmod); @@ -55,10 +60,11 @@ void PluginManager::loadPluginsFromDirectory(const std::string& dirPath) { functions.emplace(name, wrapper); std::cout << "Loaded function: " << name << " from " << entry.path().filename().string() << std::endl; - } catch (const std::exception& e) { + } + catch (const std::exception& e) { std::cerr << "Error initializing plugin " << entry.path().filename().string() - << ": " << e.what() << std::endl; + << ": " << e.what() << std::endl; FreeLibrary(hmod); } } -} \ No newline at end of file +} From ce2b26b65fd57d7949d9d86658e442ca2a0ec3d9 Mon Sep 17 00:00:00 2001 From: Fromant Date: Wed, 15 Oct 2025 19:24:31 +0300 Subject: [PATCH 03/32] plugin structure documentation --- plugins/PLUGIN_STRUCTURE.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 plugins/PLUGIN_STRUCTURE.md diff --git a/plugins/PLUGIN_STRUCTURE.md b/plugins/PLUGIN_STRUCTURE.md new file mode 100644 index 0000000..b630762 --- /dev/null +++ b/plugins/PLUGIN_STRUCTURE.md @@ -0,0 +1,3 @@ +# Every calculator plugin should have 2 functions in ddlexport: +1. `const char* get_function_name()` - возвращает имя функции (`sin`, `cos`). Обращаться в калькуляторе будут именно к ней +2. `double evaluate(double x)` - вычисляет значение функции от аргумента `x` и возвращает результат, или выбрасывает исключение \ No newline at end of file From 90b84077c918b7f994dfc280772e603599714b7f Mon Sep 17 00:00:00 2001 From: Fromant Date: Wed, 15 Oct 2025 20:40:59 +0300 Subject: [PATCH 04/32] plugin example and structure clarification --- plugins/PLUGIN_STRUCTURE.md | 7 ++++++- plugins/sin_plugin.cpp | 11 +++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 plugins/sin_plugin.cpp diff --git a/plugins/PLUGIN_STRUCTURE.md b/plugins/PLUGIN_STRUCTURE.md index b630762..10e40fd 100644 --- a/plugins/PLUGIN_STRUCTURE.md +++ b/plugins/PLUGIN_STRUCTURE.md @@ -1,3 +1,8 @@ # Every calculator plugin should have 2 functions in ddlexport: 1. `const char* get_function_name()` - возвращает имя функции (`sin`, `cos`). Обращаться в калькуляторе будут именно к ней -2. `double evaluate(double x)` - вычисляет значение функции от аргумента `x` и возвращает результат, или выбрасывает исключение \ No newline at end of file +2. `double evaluate(double x)` - вычисляет значение функции от аргумента `x` и возвращает результат, или выбрасывает исключение + +## All functions in plugin should have `extern "C" __declspec(dllexport)` qualifier +Here's why: +- `extern "C"` disables name mangling +- `__declspec(dllexport)` exports the symbol from .dll \ No newline at end of file diff --git a/plugins/sin_plugin.cpp b/plugins/sin_plugin.cpp new file mode 100644 index 0000000..b894ac9 --- /dev/null +++ b/plugins/sin_plugin.cpp @@ -0,0 +1,11 @@ +#include +#include + +extern "C" __declspec(dllexport) const char* get_function_name() { + return "sin"; +} + +extern "C" __declspec(dllexport) double evaluate(double x) { + // sin expects radians + return std::sin(x); +} \ No newline at end of file From 74dbc68378cea628b45afa50780f7721d629f9ed Mon Sep 17 00:00:00 2001 From: Fromant Date: Sat, 18 Oct 2025 18:15:31 +0300 Subject: [PATCH 05/32] cmake to build all plugins --- plugins/CMakeLists.txt | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 plugins/CMakeLists.txt diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt new file mode 100644 index 0000000..f98d76d --- /dev/null +++ b/plugins/CMakeLists.txt @@ -0,0 +1,34 @@ +cmake_minimum_required(VERSION 3.14) + +# Automatically find all .cpp files in this directory +file(GLOB PLUGIN_SOURCES CONFIGURE_DEPENDS "*.cpp") + +if(NOT PLUGIN_SOURCES) + message(STATUS "No plugin sources found in ${CMAKE_CURRENT_SOURCE_DIR}") + return() +endif() + +set(PLUGIN_TARGETS "") + +foreach(source_file IN LISTS PLUGIN_SOURCES) + # Get filename without path and extension: "sin_plugin.cpp" -> "sin_plugin" + get_filename_component(plugin_name ${source_file} NAME_WE) + + # Create a shared library (DLL) for each plugin + add_library(${plugin_name} SHARED ${source_file}) + + # Ensure C++17 (or your chosen standard) + target_compile_features(${plugin_name} PRIVATE cxx_std_17) + + set_target_properties(${plugin_name} PROPERTIES + PREFIX "" # No "lib" prefix on Windows + OUTPUT_NAME "${plugin_name}" # Explicit name + ) + + list(APPEND PLUGIN_TARGETS ${plugin_name}) +endforeach() + +# Expose PLUGIN_TARGETS to parent scope so main CMakeLists can copy them +set(PLUGIN_TARGETS ${PLUGIN_TARGETS} PARENT_SCOPE) + +message(STATUS "Found ${CMAKE_CURRENT_SOURCE_DIR} plugins: ${PLUGIN_TARGETS}") \ No newline at end of file From 820dc0268e8d7e50ebb29a5a2a521cfd240f2165 Mon Sep 17 00:00:00 2001 From: Fromant Date: Sat, 18 Oct 2025 18:28:46 +0300 Subject: [PATCH 06/32] calculator frontend (tokenizer) --- src/Token.hpp | 26 ++++++++++++++++ src/Tokenizer.cpp | 79 +++++++++++++++++++++++++++++++++++++++++++++++ src/Tokenizer.hpp | 6 ++++ 3 files changed, 111 insertions(+) create mode 100644 src/Token.hpp create mode 100644 src/Tokenizer.cpp create mode 100644 src/Tokenizer.hpp diff --git a/src/Token.hpp b/src/Token.hpp new file mode 100644 index 0000000..7bfdce3 --- /dev/null +++ b/src/Token.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include +#include + + +struct Token { + enum TokenType { + NUMBER, + OPERATOR, + FUNCTION, + LPAREN, + RPAREN + }; + TokenType type; + std::string lexeme; // for FUNCTION and raw number string + char op = 0; // for OPERATOR: '+', '-', '*', '/' + double value = 0.0; // for NUMBER + + explicit Token(TokenType t, std::string l = "", double v = 0.0) + : type(t), lexeme(std::move(l)), value(v) {} + + Token(TokenType t, char o) + : type(t), op(o) {} +}; + diff --git a/src/Tokenizer.cpp b/src/Tokenizer.cpp new file mode 100644 index 0000000..e1425c5 --- /dev/null +++ b/src/Tokenizer.cpp @@ -0,0 +1,79 @@ +#include +#include +#include +#include + +#include "Token.hpp" + +std::vector tokenize(const std::string& input) { + std::vector tokens; + size_t i = 0; + + auto skipWhitespace = [&]() { + while (i < input.size() && std::isspace(static_cast(input[i]))) ++i; + }; + + while (i < input.size()) { + + skipWhitespace(); + + if (i >= input.size()) break; + char c = input[i]; + + if (std::isdigit(c) || c == '.') { + // Parse number: supports 123, 12.34, .5, 1e-3 etc. + size_t start = i; + while (i < input.size() && (std::isdigit(input[i]) || + input[i] == '.' || input[i] == 'e' || input[i] == 'E' || + input[i] == '+' || input[i] == '-')) { + // Allow +/- only after 'e' or 'E' + if ((input[i] == '+' || input[i] == '-') && i > start && + (input[i-1] == 'e' || input[i-1] == 'E')) { + ++i; + } else if (std::isdigit(input[i]) || + input[i] == '.' || input[i] == 'e' || input[i] == 'E') { + ++i; + } else { + break; + } + } + std::string numStr = input.substr(start, i - start); + try { + size_t pos; + double val = std::stod(numStr, &pos); + if (pos != numStr.size()) { + throw std::invalid_argument("Invalid number"); + } + tokens.emplace_back(Token::TokenType::NUMBER, numStr, val); + } catch (...) { + throw std::runtime_error("Invalid number at position " + std::to_string(start)); + } + } + else if (c == '+' || c == '-' || c == '*' || c == '/') { + tokens.emplace_back(Token::TokenType::OPERATOR, c); + ++i; + } + else if (c == '(') { + tokens.emplace_back(Token::TokenType::LPAREN); + ++i; + } + else if (c == ')') { + tokens.emplace_back(Token::TokenType::RPAREN); + ++i; + } + else if (std::isalpha(c)) { + // Parse function name: [a-zA-Z_][a-zA-Z0-9_]* + size_t start = i; + while (i < input.size() && (std::isalnum(input[i]) || input[i] == '_')) { + ++i; + } + std::string name = input.substr(start, i - start); + tokens.emplace_back(Token::TokenType::FUNCTION, name); + } + else { + throw std::runtime_error("Unexpected character: '" + std::string(1, c) + "'"); + } + } + + return tokens; +} diff --git a/src/Tokenizer.hpp b/src/Tokenizer.hpp new file mode 100644 index 0000000..da44c06 --- /dev/null +++ b/src/Tokenizer.hpp @@ -0,0 +1,6 @@ +#pragma once +#include + +#include "Token.hpp" + +std::vector tokenize(const std::string& input); From 12621e007dfe06253e92ea75511f022bb557c95c Mon Sep 17 00:00:00 2001 From: Fromant Date: Mon, 20 Oct 2025 20:38:24 +0300 Subject: [PATCH 07/32] Huge plugins rework: add custom operators support --- plugins/CMakeLists.txt | 5 +- plugins/PLUGIN_STRUCTURE.md | 11 ++-- plugins/sin_plugin.cpp | 18 +++--- src/PluginManager.cpp | 118 ++++++++++++++++++++---------------- src/PluginManager.hpp | 39 +++++++----- src/plugin_interface.h | 23 +++++++ 6 files changed, 129 insertions(+), 85 deletions(-) create mode 100644 src/plugin_interface.h diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index f98d76d..ebf9e31 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -11,15 +11,12 @@ endif() set(PLUGIN_TARGETS "") foreach(source_file IN LISTS PLUGIN_SOURCES) - # Get filename without path and extension: "sin_plugin.cpp" -> "sin_plugin" + # Get filename without path and extension get_filename_component(plugin_name ${source_file} NAME_WE) # Create a shared library (DLL) for each plugin add_library(${plugin_name} SHARED ${source_file}) - # Ensure C++17 (or your chosen standard) - target_compile_features(${plugin_name} PRIVATE cxx_std_17) - set_target_properties(${plugin_name} PROPERTIES PREFIX "" # No "lib" prefix on Windows OUTPUT_NAME "${plugin_name}" # Explicit name diff --git a/plugins/PLUGIN_STRUCTURE.md b/plugins/PLUGIN_STRUCTURE.md index 10e40fd..ac3f75e 100644 --- a/plugins/PLUGIN_STRUCTURE.md +++ b/plugins/PLUGIN_STRUCTURE.md @@ -1,8 +1,9 @@ + + # Every calculator plugin should have 2 functions in ddlexport: -1. `const char* get_function_name()` - возвращает имя функции (`sin`, `cos`). Обращаться в калькуляторе будут именно к ней -2. `double evaluate(double x)` - вычисляет значение функции от аргумента `x` и возвращает результат, или выбрасывает исключение +1. `PLUGIN_API const FunctionInfo* get_function_info();` - возвращает имя функции или оператора. +2. `PLUGIN API double name_eval(const double* args, size_t count)` - вычисляет значение функции от аргументов и возвращает результат, или выбрасывает исключение -## All functions in plugin should have `extern "C" __declspec(dllexport)` qualifier -Here's why: +### See src/plugin_interface.h for FunctionInfo and PLUGIN_API definition - `extern "C"` disables name mangling -- `__declspec(dllexport)` exports the symbol from .dll \ No newline at end of file +- `__declspec(dllexport)` exports the symbol from .dll diff --git a/plugins/sin_plugin.cpp b/plugins/sin_plugin.cpp index b894ac9..a3a4907 100644 --- a/plugins/sin_plugin.cpp +++ b/plugins/sin_plugin.cpp @@ -1,11 +1,15 @@ #include -#include +#include "../src/plugin_interface.h" -extern "C" __declspec(dllexport) const char* get_function_name() { - return "sin"; +struct FunctionInfo; + +static double sin_eval(const double* args, size_t count) { + if (count != 1) return 0.0; + return std::sin(args[0]); } -extern "C" __declspec(dllexport) double evaluate(double x) { - // sin expects radians - return std::sin(x); -} \ No newline at end of file +static const FunctionInfo info = { + "sin", 1, 90, Associativity::Left, false, sin_eval +}; + +PLUGIN_API const FunctionInfo* get_function_info() { return &info; } \ No newline at end of file diff --git a/src/PluginManager.cpp b/src/PluginManager.cpp index 3b58a31..fc24da1 100644 --- a/src/PluginManager.cpp +++ b/src/PluginManager.cpp @@ -1,70 +1,82 @@ -#include #include +#include +#include #include "PluginManager.hpp" +#include "plugin_interface.h" + +PluginManager::PluginManager() { + loadPlugins(); +} -PluginManager::~PluginManager() = default; +PluginManager::~PluginManager() { + for (void* handle : handles_) { + FreeLibrary(static_cast(handle)); + } +} -void PluginManager::loadPluginsFromDirectory(const std::string& dirPath) { +void PluginManager::loadPlugins() { namespace fs = std::filesystem; - if (!fs::exists(dirPath) || !fs::is_directory(dirPath)) { - std::cerr << "Plugins directory not found: " << dirPath << std::endl; + fs::path pluginDir = "plugins"; + if (!fs::exists(pluginDir) || !fs::is_directory(pluginDir)) { + std::cerr << "Plugin directory 'plugins' not found. Skipping plugins.\n"; return; } - for (const auto& entry : fs::directory_iterator(dirPath)) { - if (entry.path().extension() != ".dll") continue; - - HMODULE hmod = LoadLibraryW(entry.path().c_str()); - if (!hmod) { - std::cerr << "Failed to load DLL: " << entry.path().filename().string() << std::endl; - continue; + for (const auto& entry : fs::directory_iterator(pluginDir)) { + if (entry.path().extension() == ".dll") { + try { + loadPlugin(entry.path().string()); + } catch (const std::exception& e) { + std::cerr << "Failed to load plugin " << entry.path().filename() + << ": " << e.what() << "\n"; + } } + } +} + +void PluginManager::loadPlugin(const std::string& path) { + HMODULE handle = LoadLibraryA(path.c_str()); + if (!handle) { + throw std::runtime_error("LoadLibrary failed"); + } - auto get_name = reinterpret_cast( - GetProcAddress(hmod, "get_function_name") - ); - auto evaluate = reinterpret_cast( - GetProcAddress(hmod, "evaluate") - ); + using GetInfoFunc = const FunctionInfo* (*)(); + auto getInfo = reinterpret_cast( + GetProcAddress(handle, "get_function_info") + ); - if (!get_name || !evaluate) { - std::cerr << "DLL missing required exports: " << entry.path().filename().string() << std::endl; - FreeLibrary(hmod); - continue; - } + if (!getInfo) { + FreeLibrary(handle); + throw std::runtime_error("Symbol 'get_function_info' not found"); + } - try { - const char* name_cstr = get_name(); - if (!name_cstr || std::string(name_cstr).empty()) { - std::cerr << "Empty function name in: " << entry.path().filename().string() << std::endl; - FreeLibrary(hmod); - continue; - } - std::string name(name_cstr); + const FunctionInfo* info = getInfo(); + if (!info || !info->name || !info->evaluate) { + FreeLibrary(handle); + throw std::runtime_error("Invalid FunctionInfo"); + } - //capture function and name by value - auto wrapper = [evaluate, name](double x) -> double { - try { - return evaluate(x); - } - catch (std::exception& e) { - // throw exception with additional data for debugging - throw std::runtime_error("Error evaluating function " + name + ": " + e.what()); - } - }; + const RegisteredFunction rf{ + info->arity, + info->precedence, + info->is_operator, + info->associativity, + info->evaluate + }; - auto handle = std::make_unique(hmod); - handle->func = wrapper; - plugins.emplace(name, std::move(handle)); - functions.emplace(name, wrapper); + registry_[std::string(info->name)] = rf; + handles_.push_back(handle); +} - std::cout << "Loaded function: " << name << " from " << entry.path().filename().string() << std::endl; - } - catch (const std::exception& e) { - std::cerr << "Error initializing plugin " << entry.path().filename().string() - << ": " << e.what() << std::endl; - FreeLibrary(hmod); - } - } +bool PluginManager::hasFunction(const std::string& name) const { + return registry_.count(name) > 0; } + +const PluginManager::RegisteredFunction& PluginManager::getFunction(const std::string& name) const { + auto it = registry_.find(name); + if (it == registry_.end()) { + throw std::runtime_error("Function not found: " + name); + } + return it->second; +} \ No newline at end of file diff --git a/src/PluginManager.hpp b/src/PluginManager.hpp index 61643cb..160be9d 100644 --- a/src/PluginManager.hpp +++ b/src/PluginManager.hpp @@ -3,32 +3,39 @@ #include #include #include -#include + +#include "plugin_interface.h" #ifdef _WIN32 - #include +#include #else #error "Only Windows is supported for plugins (DLLs)" #endif +struct FunctionInfo; + using FunctionMap = std::map>; class PluginManager { - struct PluginHandle { - HMODULE module; - std::function func; - PluginHandle(HMODULE m) : module(m) {} - ~PluginHandle() { if (module) FreeLibrary(module); } - PluginHandle(const PluginHandle&) = delete; - PluginHandle& operator=(const PluginHandle&) = delete; +public: + struct RegisteredFunction { + int arity; + int precedence; + bool is_operator; + Associativity associativity; + double (*evaluate)(const double*, size_t); }; - std::map> plugins; - FunctionMap functions; - -public: - void loadPluginsFromDirectory(const std::string& dirPath); - const FunctionMap& getFunctions() const { return functions; } + PluginManager(); ~PluginManager(); -}; + void loadPlugins(); + bool hasFunction(const std::string& name) const; + const RegisteredFunction& getFunction(const std::string& name) const; + +private: + void loadPlugin(const std::string& path); + std::unordered_map registry_; + + std::vector handles_; // HMODULE +}; diff --git a/src/plugin_interface.h b/src/plugin_interface.h new file mode 100644 index 0000000..7b06e03 --- /dev/null +++ b/src/plugin_interface.h @@ -0,0 +1,23 @@ +#pragma once + +#ifdef _WIN32 +# define PLUGIN_API extern "C" __declspec(dllexport) +#else +#error "Only windows supported" +#endif + +enum class Associativity { + Left, + Right +}; + +struct FunctionInfo { + const char* name; + int arity; + int precedence; + Associativity associativity; + bool is_operator; + double (*evaluate)(const double* args, size_t count); +}; + +PLUGIN_API const FunctionInfo* get_function_info(); \ No newline at end of file From ee3864c3dcd0bba8945d3fb694100aa8c1e34fd1 Mon Sep 17 00:00:00 2001 From: Fromant Date: Sat, 25 Oct 2025 20:22:38 +0300 Subject: [PATCH 08/32] Plugin manager add checks for plugin name and duplicating plugins Tokenizer support new dynamic operators --- src/PluginManager.cpp | 80 +++++++++++++++++++++++++++----------- src/PluginManager.hpp | 27 +++++-------- src/Token.hpp | 15 ++------ src/Tokenizer.cpp | 90 ++++++++++++++++++------------------------- 4 files changed, 109 insertions(+), 103 deletions(-) diff --git a/src/PluginManager.cpp b/src/PluginManager.cpp index fc24da1..164f938 100644 --- a/src/PluginManager.cpp +++ b/src/PluginManager.cpp @@ -1,38 +1,64 @@ +#include "PluginManager.hpp" #include #include #include +#include -#include "PluginManager.hpp" -#include "plugin_interface.h" +#ifdef _WIN32 +#include +#else +#error "Windows only" +#endif + +void PluginManager::validatePlugin(FunctionInfo const* info) { + std::string name(info->name); + if (name.empty()) { + throw std::runtime_error("Plugin name is empty"); + } + static const std::set reserved = {"(", ")", ","}; + if (reserved.count(name)) { + throw std::runtime_error("Plugin cannot register reserved symbol: " + name); + } + if (std::isdigit(name[0])) { + throw std::runtime_error("Plugin name cannot start with a digit: " + name); + } + if (info->is_operator && name.length() != 1) { + throw std::runtime_error("Plugin operator can only have one char as a name: " + name); + } +} PluginManager::PluginManager() { loadPlugins(); } PluginManager::~PluginManager() { - for (void* handle : handles_) { - FreeLibrary(static_cast(handle)); + for (void* h : handles_) { + FreeLibrary(static_cast(h)); } } void PluginManager::loadPlugins() { namespace fs = std::filesystem; fs::path pluginDir = "plugins"; - if (!fs::exists(pluginDir) || !fs::is_directory(pluginDir)) { - std::cerr << "Plugin directory 'plugins' not found. Skipping plugins.\n"; - return; + if (!fs::exists(pluginDir)) { + throw std::runtime_error("Plugin directory 'plugins' not found"); } for (const auto& entry : fs::directory_iterator(pluginDir)) { if (entry.path().extension() == ".dll") { try { loadPlugin(entry.path().string()); - } catch (const std::exception& e) { + } + catch (const std::exception& e) { std::cerr << "Failed to load plugin " << entry.path().filename() - << ": " << e.what() << "\n"; + << ": " << e.what() << "\n"; } } } + + if (registry_.empty()) { + throw std::runtime_error("No valid plugins loaded"); + } } void PluginManager::loadPlugin(const std::string& path) { @@ -41,7 +67,8 @@ void PluginManager::loadPlugin(const std::string& path) { throw std::runtime_error("LoadLibrary failed"); } - using GetInfoFunc = const FunctionInfo* (*)(); + // typedef const FunctionInfo* (*GetInfoFunc)(); + using GetInfoFunc = FunctionInfo*(*)(); auto getInfo = reinterpret_cast( GetProcAddress(handle, "get_function_info") ); @@ -57,26 +84,35 @@ void PluginManager::loadPlugin(const std::string& path) { throw std::runtime_error("Invalid FunctionInfo"); } - const RegisteredFunction rf{ - info->arity, - info->precedence, - info->is_operator, - info->associativity, - info->evaluate - }; + validatePlugin(info); + + std::string name(info->name); + + // Check for duplicates + if (registry_.count(name)) { + FreeLibrary(handle); + throw std::runtime_error("Duplicate token name: " + name); + } + + registry_[name] = TokenInfo{ + info->arity, + info->precedence, + info->associativity, + info->is_operator, + info->evaluate + }; - registry_[std::string(info->name)] = rf; handles_.push_back(handle); } -bool PluginManager::hasFunction(const std::string& name) const { +bool PluginManager::hasToken(const std::string& name) const { return registry_.count(name) > 0; } -const PluginManager::RegisteredFunction& PluginManager::getFunction(const std::string& name) const { +const PluginManager::TokenInfo& PluginManager::getTokenInfo(const std::string& name) const { auto it = registry_.find(name); if (it == registry_.end()) { - throw std::runtime_error("Function not found: " + name); + throw std::runtime_error("Unknown token: " + name); } return it->second; -} \ No newline at end of file +} diff --git a/src/PluginManager.hpp b/src/PluginManager.hpp index 160be9d..07d7f5e 100644 --- a/src/PluginManager.hpp +++ b/src/PluginManager.hpp @@ -2,40 +2,31 @@ #include #include -#include #include "plugin_interface.h" -#ifdef _WIN32 -#include -#else - #error "Only Windows is supported for plugins (DLLs)" -#endif - -struct FunctionInfo; - -using FunctionMap = std::map>; - class PluginManager { public: - struct RegisteredFunction { + struct TokenInfo { int arity; int precedence; - bool is_operator; Associativity associativity; + bool is_operator; double (*evaluate)(const double*, size_t); }; PluginManager(); ~PluginManager(); - void loadPlugins(); - bool hasFunction(const std::string& name) const; - const RegisteredFunction& getFunction(const std::string& name) const; + bool hasToken(const std::string& name) const; + const TokenInfo& getTokenInfo(const std::string& name) const; private: + void loadPlugins(); void loadPlugin(const std::string& path); - std::unordered_map registry_; + void validatePlugin(FunctionInfo const* info); + + std::unordered_map registry_; - std::vector handles_; // HMODULE + std::vector handles_; }; diff --git a/src/Token.hpp b/src/Token.hpp index 7bfdce3..31ecdea 100644 --- a/src/Token.hpp +++ b/src/Token.hpp @@ -7,20 +7,13 @@ struct Token { enum TokenType { NUMBER, - OPERATOR, - FUNCTION, + IDENTIFIER, //operator or function LPAREN, - RPAREN + RPAREN, + COMMA }; TokenType type; - std::string lexeme; // for FUNCTION and raw number string - char op = 0; // for OPERATOR: '+', '-', '*', '/' + std::string lexeme; // for FUNCTION and OPERATOR raw number string double value = 0.0; // for NUMBER - - explicit Token(TokenType t, std::string l = "", double v = 0.0) - : type(t), lexeme(std::move(l)), value(v) {} - - Token(TokenType t, char o) - : type(t), op(o) {} }; diff --git a/src/Tokenizer.cpp b/src/Tokenizer.cpp index e1425c5..c7f374f 100644 --- a/src/Tokenizer.cpp +++ b/src/Tokenizer.cpp @@ -1,79 +1,65 @@ -#include +#include "Tokenizer.hpp" + #include #include -#include - -#include "Token.hpp" -std::vector tokenize(const std::string& input) { +std::vector tokenize(const std::string& expr) { std::vector tokens; size_t i = 0; - auto skipWhitespace = [&]() { - while (i < input.size() && std::isspace(static_cast(input[i]))) ++i; + auto skipSpaces = [&]() { + while (i < expr.size() && std::isspace(static_cast(expr[i]))) ++i; }; - while (i < input.size()) { + auto isDigit = [](char c) { return std::isdigit(static_cast(c)) || c == '.'; }; - skipWhitespace(); + while (i < expr.size()) { + skipSpaces(); + if (i >= expr.size()) break; - if (i >= input.size()) break; - char c = input[i]; + char c = expr[i]; - if (std::isdigit(c) || c == '.') { - // Parse number: supports 123, 12.34, .5, 1e-3 etc. + // Numbers + if (isDigit(c)) { size_t start = i; - while (i < input.size() && (std::isdigit(input[i]) || - input[i] == '.' || input[i] == 'e' || input[i] == 'E' || - input[i] == '+' || input[i] == '-')) { - // Allow +/- only after 'e' or 'E' - if ((input[i] == '+' || input[i] == '-') && i > start && - (input[i-1] == 'e' || input[i-1] == 'E')) { - ++i; - } else if (std::isdigit(input[i]) || - input[i] == '.' || input[i] == 'e' || input[i] == 'E') { - ++i; - } else { - break; - } - } - std::string numStr = input.substr(start, i - start); + while (i < expr.size() && isDigit(expr[i])) ++i; + std::string numStr = expr.substr(start, i - start); try { - size_t pos; - double val = std::stod(numStr, &pos); - if (pos != numStr.size()) { - throw std::invalid_argument("Invalid number"); - } - tokens.emplace_back(Token::TokenType::NUMBER, numStr, val); + double val = std::stod(numStr); + tokens.push_back({Token::TokenType::NUMBER, numStr, val}); } catch (...) { - throw std::runtime_error("Invalid number at position " + std::to_string(start)); + throw std::runtime_error("Invalid number: " + numStr); } } - else if (c == '+' || c == '-' || c == '*' || c == '/') { - tokens.emplace_back(Token::TokenType::OPERATOR, c); - ++i; - } + // Parentheses and comma else if (c == '(') { - tokens.emplace_back(Token::TokenType::LPAREN); + tokens.push_back({Token::TokenType::LPAREN, "("}); ++i; - } - else if (c == ')') { - tokens.emplace_back(Token::TokenType::RPAREN); + } else if (c == ')') { + tokens.push_back({Token::TokenType::RPAREN, ")"}); + ++i; + } else if (c == ',') { + tokens.push_back({Token::TokenType::COMMA, ","}); ++i; } - else if (std::isalpha(c)) { - // Parse function name: [a-zA-Z_][a-zA-Z0-9_]* + // Everything else: treat as identifier (including +, -, *, /, ^, @, etc.) + else { size_t start = i; - while (i < input.size() && (std::isalnum(input[i]) || input[i] == '_')) { + // Take **one character** as identifier (for symbols like +, ^) + // But allow multi-char names like "sin", "max" + if (!std::isalpha(c) && c != '_') { + // Single-symbol token (e.g. '+', '^', '@') + tokens.push_back({Token::TokenType::IDENTIFIER, std::string(1, c)}); ++i; + } else { + // Multi-character identifier (e.g. "sin", "log") + while (i < expr.size() && (std::isalnum(static_cast(expr[i])) || expr[i] == '_')) { + ++i; + } + tokens.push_back({Token::TokenType::IDENTIFIER, expr.substr(start, i - start)}); } - std::string name = input.substr(start, i - start); - tokens.emplace_back(Token::TokenType::FUNCTION, name); - } - else { - throw std::runtime_error("Unexpected character: '" + std::string(1, c) + "'"); } } return tokens; -} +} \ No newline at end of file From aa2bb775fb6c1dda540da7b144d2f73e0641ea73 Mon Sep 17 00:00:00 2001 From: Fromant Date: Sat, 25 Oct 2025 20:28:40 +0300 Subject: [PATCH 09/32] make validatePlugin() static --- src/PluginManager.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PluginManager.hpp b/src/PluginManager.hpp index 07d7f5e..d60f3c3 100644 --- a/src/PluginManager.hpp +++ b/src/PluginManager.hpp @@ -24,7 +24,7 @@ class PluginManager { private: void loadPlugins(); void loadPlugin(const std::string& path); - void validatePlugin(FunctionInfo const* info); + static static void validatePlugin(FunctionInfo const* info); std::unordered_map registry_; From 006ce295649d1a34afc353dab3132b18ca5d9e97 Mon Sep 17 00:00:00 2001 From: Fromant Date: Sat, 25 Oct 2025 20:30:01 +0300 Subject: [PATCH 10/32] make validatePlugin() static --- src/PluginManager.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PluginManager.hpp b/src/PluginManager.hpp index d60f3c3..95cf80e 100644 --- a/src/PluginManager.hpp +++ b/src/PluginManager.hpp @@ -24,7 +24,7 @@ class PluginManager { private: void loadPlugins(); void loadPlugin(const std::string& path); - static static void validatePlugin(FunctionInfo const* info); + static void validatePlugin(FunctionInfo const* info); std::unordered_map registry_; From d2abc46d71892fcd8678164fdbb36892a10e8fb5 Mon Sep 17 00:00:00 2001 From: Fromant Date: Sat, 25 Oct 2025 20:33:33 +0300 Subject: [PATCH 11/32] add shunting yard algorithm to transform arithmetical notation to postfix notation (aka RPN) --- src/ShuntingYard.cpp | 79 ++++++++++++++++++++++++++++++++++++++++++++ src/ShuntingYard.hpp | 8 +++++ 2 files changed, 87 insertions(+) create mode 100644 src/ShuntingYard.cpp create mode 100644 src/ShuntingYard.hpp diff --git a/src/ShuntingYard.cpp b/src/ShuntingYard.cpp new file mode 100644 index 0000000..5fe94ba --- /dev/null +++ b/src/ShuntingYard.cpp @@ -0,0 +1,79 @@ +#include "ShuntingYard.hpp" + +#include +#include + +std::vector shuntingYard(const std::vector& tokens, const PluginManager& pm) { + std::stack ops; + std::vector output; + + for (const auto& token : tokens) { + if (token.type == Token::TokenType::NUMBER) { + output.push_back(token); + } else if (token.type == Token::TokenType::IDENTIFIER) { + // Validate: must be known token + if (!pm.hasToken(token.lexeme)) { + throw std::runtime_error("Unknown token: " + token.lexeme); + } + const auto& curr = pm.getTokenInfo(token.lexeme); + + if (curr.is_operator) { + while (!ops.empty() && ops.top().type == Token::TokenType::IDENTIFIER) { + const auto& topTok = ops.top(); + if (!pm.hasToken(topTok.lexeme)) break; + const auto& prev = pm.getTokenInfo(topTok.lexeme); + if (!prev.is_operator) break; + + bool shouldPop = false; + if (curr.associativity == Associativity::Left) { + shouldPop = (curr.precedence <= prev.precedence); + } else { // Right + shouldPop = (curr.precedence < prev.precedence); + } + + if (!shouldPop) break; + + output.push_back(ops.top()); + ops.pop(); + } + } + + ops.push(token); + } else if (token.type == Token::TokenType::COMMA) { + while (!ops.empty() && ops.top().type != Token::TokenType::LPAREN) { + output.push_back(ops.top()); + ops.pop(); + } + } else if (token.type == Token::TokenType::LPAREN) { + ops.push(token); + } else if (token.type == Token::TokenType::RPAREN) { + while (!ops.empty() && ops.top().type != Token::TokenType::LPAREN) { + output.push_back(ops.top()); + ops.pop(); + } + if (ops.empty()) { + throw std::runtime_error("Mismatched parentheses"); + } + ops.pop(); // remove '(' + + // If function is on top, pop it + if (!ops.empty() && ops.top().type == Token::TokenType::IDENTIFIER) { + const auto& fn = pm.getTokenInfo(ops.top().lexeme); + if (!fn.is_operator) { + output.push_back(ops.top()); + ops.pop(); + } + } + } + } + + while (!ops.empty()) { + if (ops.top().type == Token::TokenType::LPAREN || ops.top().type == Token::TokenType::RPAREN) { + throw std::runtime_error("Mismatched parentheses"); + } + output.push_back(ops.top()); + ops.pop(); + } + + return output; +} \ No newline at end of file diff --git a/src/ShuntingYard.hpp b/src/ShuntingYard.hpp new file mode 100644 index 0000000..c1bbab3 --- /dev/null +++ b/src/ShuntingYard.hpp @@ -0,0 +1,8 @@ +#pragma once + +#include + +#include "Token.hpp" +#include "PluginManager.hpp" + +std::vector shuntingYard(const std::vector& tokens, const PluginManager& pm); \ No newline at end of file From 0dc2d5d9a70edbe810eb7befe96c60cfe100ef48 Mon Sep 17 00:00:00 2001 From: Fromant Date: Sat, 25 Oct 2025 20:34:14 +0300 Subject: [PATCH 12/32] add evaluating stack machine for rpn expressions --- src/RpnEvaluator.cpp | 34 ++++++++++++++++++++++++++++++++++ src/RpnEvaluator.hpp | 8 ++++++++ 2 files changed, 42 insertions(+) create mode 100644 src/RpnEvaluator.cpp create mode 100644 src/RpnEvaluator.hpp diff --git a/src/RpnEvaluator.cpp b/src/RpnEvaluator.cpp new file mode 100644 index 0000000..1a91b83 --- /dev/null +++ b/src/RpnEvaluator.cpp @@ -0,0 +1,34 @@ +#include "RpnEvaluator.hpp" +#include +#include + + +double evaluateRpn(const std::vector& rpn, const PluginManager& pm) { + std::stack values; + + for (const auto& token : rpn) { + if (token.type == Token::TokenType::NUMBER) { + values.push(token.value); + } else if (token.type == Token::TokenType::IDENTIFIER) { + const auto& info = pm.getTokenInfo(token.lexeme); + if (values.size() < static_cast(info.arity)) { + throw std::runtime_error("Not enough arguments for " + token.lexeme); + } + + std::vector args(info.arity); + for (int i = info.arity - 1; i >= 0; --i) { + args[i] = values.top(); + values.pop(); + } + + double result = info.evaluate(args.data(), args.size()); + values.push(result); + } + } + + if (values.size() != 1) { + throw std::runtime_error("Invalid expression"); + } + + return values.top(); +} \ No newline at end of file diff --git a/src/RpnEvaluator.hpp b/src/RpnEvaluator.hpp new file mode 100644 index 0000000..5181c57 --- /dev/null +++ b/src/RpnEvaluator.hpp @@ -0,0 +1,8 @@ +#pragma once + +#include + +#include "Token.hpp" +#include "PluginManager.hpp" + +double evaluateRpn(const std::vector& rpn, const PluginManager& pm); \ No newline at end of file From b74c3b42432e086650e4f41bf362669bde061081 Mon Sep 17 00:00:00 2001 From: Fromant Date: Sat, 25 Oct 2025 20:34:37 +0300 Subject: [PATCH 13/32] add calculator class that encapsulates all logic --- src/Calculator.cpp | 11 +++++++++++ src/Calculator.hpp | 11 +++++++++++ 2 files changed, 22 insertions(+) create mode 100644 src/Calculator.cpp create mode 100644 src/Calculator.hpp diff --git a/src/Calculator.cpp b/src/Calculator.cpp new file mode 100644 index 0000000..fb74647 --- /dev/null +++ b/src/Calculator.cpp @@ -0,0 +1,11 @@ +#include "Calculator.hpp" +#include "Tokenizer.hpp" +#include "ShuntingYard.hpp" +#include "RpnEvaluator.hpp" +#include "PluginManager.hpp" + +double Calculator::evaluate(const std::string& expr) { + auto tokens = tokenize(expr); + auto rpn = shuntingYard(tokens, pm); + return evaluateRpn(rpn, pm); +} diff --git a/src/Calculator.hpp b/src/Calculator.hpp new file mode 100644 index 0000000..b7eaf93 --- /dev/null +++ b/src/Calculator.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include + +#include "PluginManager.hpp" + +class Calculator { + PluginManager pm; +public: + double evaluate(const std::string& expression); +}; From 2757ad58558146df31dc975498071013c24147ca Mon Sep 17 00:00:00 2001 From: Fromant Date: Sat, 25 Oct 2025 20:35:11 +0300 Subject: [PATCH 14/32] add main function and cmake config --- CMakeLists.txt | 41 +++++++++++++++++++++++++++++++++++++++++ src/main.cpp | 17 +++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 CMakeLists.txt create mode 100644 src/main.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..6203976 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,41 @@ +cmake_minimum_required(VERSION 3.14) +project(CalcApp LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +if(WIN32) + set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON) +endif() + +add_executable(calc + src/Tokenizer.cpp + src/PluginManager.cpp + src/Token.hpp + src/Tokenizer.hpp + src/ShuntingYard.hpp + src/ShuntingYard.cpp + src/RpnEvaluator.hpp + src/RpnEvaluator.cpp + src/Calculator.hpp + src/Calculator.cpp + src/main.cpp +) + +target_include_directories(calc PRIVATE src) + +# Build plugins +add_subdirectory(plugins) + +# Post-build: copy DLLs to ./plugins next to calc.exe +add_custom_command(TARGET calc POST_BUILD + COMMAND ${CMAKE_COMMAND} -E make_directory $/plugins +) + +foreach(plugin ${PLUGIN_TARGETS}) + add_custom_command(TARGET calc POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy + $ + $/plugins/ + ) +endforeach() \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..48dd4b9 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,17 @@ +#include +#include + +#include "Calculator.hpp" + +int main() { + try { + std::string input; + std::getline(std::cin, input); + Calculator calc; + std::cout << calc.evaluate(input) << std::endl; + } catch (const std::exception& e) { + std::cerr << "Error: " << e.what() << std::endl; + return 1; + } + return 0; +} \ No newline at end of file From 55262bbf5fb07e00ad95e4976e7d5f9900eb42a8 Mon Sep 17 00:00:00 2001 From: Fromant Date: Sat, 25 Oct 2025 20:42:51 +0300 Subject: [PATCH 15/32] add +-*/^ operator plugins --- plugins/add_plugin.cpp | 13 +++++++++++++ plugins/div_plugin.cpp | 16 ++++++++++++++++ plugins/mul_plugin.cpp | 13 +++++++++++++ plugins/pow_plugin.cpp | 19 +++++++++++++++++++ plugins/sub_plugin.cpp | 13 +++++++++++++ 5 files changed, 74 insertions(+) create mode 100644 plugins/add_plugin.cpp create mode 100644 plugins/div_plugin.cpp create mode 100644 plugins/mul_plugin.cpp create mode 100644 plugins/pow_plugin.cpp create mode 100644 plugins/sub_plugin.cpp diff --git a/plugins/add_plugin.cpp b/plugins/add_plugin.cpp new file mode 100644 index 0000000..162fc2f --- /dev/null +++ b/plugins/add_plugin.cpp @@ -0,0 +1,13 @@ +#include "../src/plugin_interface.h" + +static double add(const double* args, size_t) { + return args[0] + args[1]; +} + +static const FunctionInfo info = { + "+", 2, 60, Associativity::Left, true, add +}; + +PLUGIN_API const FunctionInfo* get_function_info() { + return &info; +} diff --git a/plugins/div_plugin.cpp b/plugins/div_plugin.cpp new file mode 100644 index 0000000..36c76c6 --- /dev/null +++ b/plugins/div_plugin.cpp @@ -0,0 +1,16 @@ +#include "../src/plugin_interface.h" + +#include + +static double div(const double* args, size_t) { + if (args[1] == 0.0) throw std::domain_error("Division by zero"); + return args[0] / args[1]; +} + +static const FunctionInfo info = { + "/", 2, 70, Associativity::Left, true, div +}; + +PLUGIN_API const FunctionInfo* get_function_info() { + return &info; +} \ No newline at end of file diff --git a/plugins/mul_plugin.cpp b/plugins/mul_plugin.cpp new file mode 100644 index 0000000..0c1d774 --- /dev/null +++ b/plugins/mul_plugin.cpp @@ -0,0 +1,13 @@ +#include "../src/plugin_interface.h" + +static double mul(const double* args, size_t) { + return args[0] * args[1]; +} + +static const FunctionInfo info = { + "*", 2, 70, Associativity::Left, true, mul +}; + +PLUGIN_API const FunctionInfo* get_function_info() { + return &info; +} \ No newline at end of file diff --git a/plugins/pow_plugin.cpp b/plugins/pow_plugin.cpp new file mode 100644 index 0000000..5dc9578 --- /dev/null +++ b/plugins/pow_plugin.cpp @@ -0,0 +1,19 @@ +#include "../src/plugin_interface.h" + +#include +#include + +static double pow_eval(const double* args, size_t) { + double base = args[0], exp = args[1]; + if (base < 0 && std::floor(exp) != exp) + throw std::domain_error("pow: negative base with non-integer exponent"); + return std::pow(base, exp); +} + +static const FunctionInfo info = { + "^", 2, 80, Associativity::Right, true, pow_eval +}; + +PLUGIN_API const FunctionInfo* get_function_info() { + return &info; +} \ No newline at end of file diff --git a/plugins/sub_plugin.cpp b/plugins/sub_plugin.cpp new file mode 100644 index 0000000..c80b33f --- /dev/null +++ b/plugins/sub_plugin.cpp @@ -0,0 +1,13 @@ +#include "../src/plugin_interface.h" + +static double sub(const double* args, size_t) { + return args[0] - args[1]; +} + +static const FunctionInfo info = { + "-", 2, 60, Associativity::Left, true, sub +}; + +PLUGIN_API const FunctionInfo* get_function_info() { + return &info; +} \ No newline at end of file From 1c9cef713781e5c75586bdf0b9813f702a47e86b Mon Sep 17 00:00:00 2001 From: Fromant Date: Sat, 25 Oct 2025 20:43:17 +0300 Subject: [PATCH 16/32] fix outputting .dll.a static version of each plugin --- plugins/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index ebf9e31..22858cf 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -20,6 +20,7 @@ foreach(source_file IN LISTS PLUGIN_SOURCES) set_target_properties(${plugin_name} PROPERTIES PREFIX "" # No "lib" prefix on Windows OUTPUT_NAME "${plugin_name}" # Explicit name + ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/static_plugins # optional: control where .a goes ) list(APPEND PLUGIN_TARGETS ${plugin_name}) From 8eb5dfe806e804b2e8fb12d089d0c81a5291db22 Mon Sep 17 00:00:00 2001 From: Fromant Date: Sat, 25 Oct 2025 20:43:26 +0300 Subject: [PATCH 17/32] fix size_t not included --- src/plugin_interface.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/plugin_interface.h b/src/plugin_interface.h index 7b06e03..24b5e13 100644 --- a/src/plugin_interface.h +++ b/src/plugin_interface.h @@ -1,5 +1,7 @@ #pragma once +#include + #ifdef _WIN32 # define PLUGIN_API extern "C" __declspec(dllexport) #else From 61495630ddc1102f8a78df74336cc8848e766ec9 Mon Sep 17 00:00:00 2001 From: Fromant Date: Sat, 25 Oct 2025 21:14:04 +0300 Subject: [PATCH 18/32] make separate cmake projects for plugins, testing, and core library calculator logic --- CMakeLists.txt | 38 ++++++++++++++------------------------ src/main.cpp => main.cpp | 2 +- plugins/CMakeLists.txt | 4 ++++ src/CMakeLists.txt | 18 ++++++++++++++++++ tests/CMakeLists.txt | 16 ++++++++++++++++ 5 files changed, 53 insertions(+), 25 deletions(-) rename src/main.cpp => main.cpp (92%) create mode 100644 src/CMakeLists.txt create mode 100644 tests/CMakeLists.txt diff --git a/CMakeLists.txt b/CMakeLists.txt index 6203976..4f94e11 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,38 +4,28 @@ project(CalcApp LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) -if(WIN32) - set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON) -endif() - -add_executable(calc - src/Tokenizer.cpp - src/PluginManager.cpp - src/Token.hpp - src/Tokenizer.hpp - src/ShuntingYard.hpp - src/ShuntingYard.cpp - src/RpnEvaluator.hpp - src/RpnEvaluator.cpp - src/Calculator.hpp - src/Calculator.cpp - src/main.cpp -) -target_include_directories(calc PRIVATE src) +add_subdirectory(src) + +add_executable(CalcApp main.cpp) + +target_link_libraries(CalcApp CalcLib) # Build plugins add_subdirectory(plugins) # Post-build: copy DLLs to ./plugins next to calc.exe -add_custom_command(TARGET calc POST_BUILD - COMMAND ${CMAKE_COMMAND} -E make_directory $/plugins +add_custom_command(TARGET CalcApp POST_BUILD + COMMAND ${CMAKE_COMMAND} -E make_directory $/plugins ) -foreach(plugin ${PLUGIN_TARGETS}) - add_custom_command(TARGET calc POST_BUILD +foreach (plugin ${PLUGIN_TARGETS}) + add_custom_command(TARGET CalcApp POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy $ - $/plugins/ + $/plugins/ ) -endforeach() \ No newline at end of file +endforeach () + +enable_testing() +add_subdirectory(tests) diff --git a/src/main.cpp b/main.cpp similarity index 92% rename from src/main.cpp rename to main.cpp index 48dd4b9..c85abaf 100644 --- a/src/main.cpp +++ b/main.cpp @@ -1,7 +1,7 @@ #include #include -#include "Calculator.hpp" +#include "src/Calculator.hpp" int main() { try { diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 22858cf..33612c1 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -3,6 +3,10 @@ cmake_minimum_required(VERSION 3.14) # Automatically find all .cpp files in this directory file(GLOB PLUGIN_SOURCES CONFIGURE_DEPENDS "*.cpp") +if (WIN32) + set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON) +endif () + if(NOT PLUGIN_SOURCES) message(STATUS "No plugin sources found in ${CMAKE_CURRENT_SOURCE_DIR}") return() diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..1e02ca6 --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,18 @@ +cmake_minimum_required(VERSION 3.14) +project(CalcLib LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +add_library(CalcLib + Tokenizer.cpp + PluginManager.cpp + Token.hpp + Tokenizer.hpp + ShuntingYard.hpp + ShuntingYard.cpp + RpnEvaluator.hpp + RpnEvaluator.cpp + Calculator.hpp + Calculator.cpp +) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..8d676eb --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,16 @@ +# Download and build GoogleTest if not found +include(FetchContent) +FetchContent_Declare( + googletest + URL https://github.com/google/googletest/archive/refs/tags/v1.14.0.zip +) +set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) +FetchContent_MakeAvailable(googletest) + +# Test executable +file(GLOB TEST_SOURCES "*.cpp") +add_executable(calculator_tests ${TEST_SOURCES}) +target_link_libraries(calculator_tests calculator gtest gmock) +target_include_directories(calculator_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/..) + +add_test(NAME calculator_tests COMMAND calculator_tests) \ No newline at end of file From ab2c5be6adb5f11115db3a5c47c08c3f2a95a030 Mon Sep 17 00:00:00 2001 From: Fromant Date: Sat, 25 Oct 2025 21:14:29 +0300 Subject: [PATCH 19/32] make IPluginRegistry interface --- src/IPluginRegistry.hpp | 20 ++++++++++++++++++++ src/PluginManager.hpp | 17 ++++++----------- src/RpnEvaluator.cpp | 2 +- src/RpnEvaluator.hpp | 2 +- src/ShuntingYard.cpp | 2 +- src/ShuntingYard.hpp | 2 +- 6 files changed, 30 insertions(+), 15 deletions(-) create mode 100644 src/IPluginRegistry.hpp diff --git a/src/IPluginRegistry.hpp b/src/IPluginRegistry.hpp new file mode 100644 index 0000000..29982c9 --- /dev/null +++ b/src/IPluginRegistry.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include +#include "plugin_interface.h" + +class IPluginRegistry { +public: + virtual ~IPluginRegistry() = default; + + struct TokenInfo { + int arity; + int precedence; + Associativity associativity; + bool is_operator; + double (*evaluate)(const double*, size_t); + }; + + virtual bool hasToken(const std::string& name) const = 0; + virtual const TokenInfo& getTokenInfo(const std::string& name) const = 0; +}; diff --git a/src/PluginManager.hpp b/src/PluginManager.hpp index 95cf80e..04a17f7 100644 --- a/src/PluginManager.hpp +++ b/src/PluginManager.hpp @@ -3,23 +3,18 @@ #include #include +#include "IPluginRegistry.hpp" #include "plugin_interface.h" -class PluginManager { +class PluginManager : public IPluginRegistry{ public: - struct TokenInfo { - int arity; - int precedence; - Associativity associativity; - bool is_operator; - double (*evaluate)(const double*, size_t); - }; + using IPluginRegistry::TokenInfo; // reuse nested type PluginManager(); - ~PluginManager(); + ~PluginManager() override; - bool hasToken(const std::string& name) const; - const TokenInfo& getTokenInfo(const std::string& name) const; + bool hasToken(const std::string& name) const final; + const TokenInfo& getTokenInfo(const std::string& name) const final; private: void loadPlugins(); diff --git a/src/RpnEvaluator.cpp b/src/RpnEvaluator.cpp index 1a91b83..b1872a4 100644 --- a/src/RpnEvaluator.cpp +++ b/src/RpnEvaluator.cpp @@ -3,7 +3,7 @@ #include -double evaluateRpn(const std::vector& rpn, const PluginManager& pm) { +double evaluateRpn(const std::vector& rpn, const IPluginRegistry& pm) { std::stack values; for (const auto& token : rpn) { diff --git a/src/RpnEvaluator.hpp b/src/RpnEvaluator.hpp index 5181c57..9843e13 100644 --- a/src/RpnEvaluator.hpp +++ b/src/RpnEvaluator.hpp @@ -5,4 +5,4 @@ #include "Token.hpp" #include "PluginManager.hpp" -double evaluateRpn(const std::vector& rpn, const PluginManager& pm); \ No newline at end of file +double evaluateRpn(const std::vector& rpn, const IPluginRegistry& pm); \ No newline at end of file diff --git a/src/ShuntingYard.cpp b/src/ShuntingYard.cpp index 5fe94ba..f39d0e2 100644 --- a/src/ShuntingYard.cpp +++ b/src/ShuntingYard.cpp @@ -3,7 +3,7 @@ #include #include -std::vector shuntingYard(const std::vector& tokens, const PluginManager& pm) { +std::vector shuntingYard(const std::vector& tokens, const IPluginRegistry& pm) { std::stack ops; std::vector output; diff --git a/src/ShuntingYard.hpp b/src/ShuntingYard.hpp index c1bbab3..1389927 100644 --- a/src/ShuntingYard.hpp +++ b/src/ShuntingYard.hpp @@ -5,4 +5,4 @@ #include "Token.hpp" #include "PluginManager.hpp" -std::vector shuntingYard(const std::vector& tokens, const PluginManager& pm); \ No newline at end of file +std::vector shuntingYard(const std::vector& tokens, const IPluginRegistry& pm); \ No newline at end of file From 5ea6bd0249ac737a5a5600a263d3e1f4efb52052 Mon Sep 17 00:00:00 2001 From: Fromant Date: Sat, 25 Oct 2025 21:31:19 +0300 Subject: [PATCH 20/32] add simple unit tests --- tests/CMakeLists.txt | 9 +-- tests/MockPluginRegistry.hpp | 9 +++ tests/test_rpn_evaluator.cpp | 69 +++++++++++++++++++++++ tests/test_shunting_yard.cpp | 103 +++++++++++++++++++++++++++++++++++ tests/test_tokenizer.cpp | 54 ++++++++++++++++++ 5 files changed, 240 insertions(+), 4 deletions(-) create mode 100644 tests/MockPluginRegistry.hpp create mode 100644 tests/test_rpn_evaluator.cpp create mode 100644 tests/test_shunting_yard.cpp create mode 100644 tests/test_tokenizer.cpp diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 8d676eb..25fef3e 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -3,14 +3,15 @@ include(FetchContent) FetchContent_Declare( googletest URL https://github.com/google/googletest/archive/refs/tags/v1.14.0.zip + DOWNLOAD_EXTRACT_TIMESTAMP TRUE ) set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) FetchContent_MakeAvailable(googletest) # Test executable file(GLOB TEST_SOURCES "*.cpp") -add_executable(calculator_tests ${TEST_SOURCES}) -target_link_libraries(calculator_tests calculator gtest gmock) -target_include_directories(calculator_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/..) +add_executable(CalcTests ${TEST_SOURCES}) +target_link_libraries(CalcTests CalcLib gtest gmock gtest_main) +target_include_directories(CalcTests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/..) -add_test(NAME calculator_tests COMMAND calculator_tests) \ No newline at end of file +add_test(NAME CalcTests COMMAND CalcTests) \ No newline at end of file diff --git a/tests/MockPluginRegistry.hpp b/tests/MockPluginRegistry.hpp new file mode 100644 index 0000000..f852299 --- /dev/null +++ b/tests/MockPluginRegistry.hpp @@ -0,0 +1,9 @@ +#pragma once +#include "../src/IPluginRegistry.hpp" +#include + +class MockPluginRegistry : public IPluginRegistry { +public: + MOCK_METHOD(bool, hasToken, (const std::string& name), (const, override)); + MOCK_METHOD(const TokenInfo&, getTokenInfo, (const std::string& name), (const, override)); +}; \ No newline at end of file diff --git a/tests/test_rpn_evaluator.cpp b/tests/test_rpn_evaluator.cpp new file mode 100644 index 0000000..bfb1776 --- /dev/null +++ b/tests/test_rpn_evaluator.cpp @@ -0,0 +1,69 @@ +#include +#include +#include "../src/RpnEvaluator.hpp" +#include "../src/Token.hpp" +#include "MockPluginRegistry.hpp" + +using ::testing::ReturnRef; + +static double add_eval(const double* args, size_t) { + return args[0] + args[1]; +} + +static double sin_eval(const double* args, size_t) { + return std::sin(args[0]); +} + +TEST(RpnEvaluatorTest, BinaryOperator) { + MockPluginRegistry mock; + MockPluginRegistry::TokenInfo addInfo{2, 60, Associativity::Left, true, add_eval}; + + EXPECT_CALL(mock, getTokenInfo("+")).WillRepeatedly(ReturnRef(addInfo)); + + std::vector rpn = { + {Token::TokenType::NUMBER, "2", 2.0}, + {Token::TokenType::NUMBER, "3", 3.0}, + {Token::TokenType::IDENTIFIER, "+"} + }; + + double result = evaluateRpn(rpn, mock); + EXPECT_DOUBLE_EQ(result, 5.0); +} + +TEST(RpnEvaluatorTest, Function) { + MockPluginRegistry mock; + MockPluginRegistry::TokenInfo sinInfo{1, 90, Associativity::Left, false, sin_eval}; + + EXPECT_CALL(mock, getTokenInfo("sin")).WillRepeatedly(ReturnRef(sinInfo)); + + std::vector rpn = { + {Token::TokenType::NUMBER, "1.5708", 1.5708}, + {Token::TokenType::IDENTIFIER, "sin"} + }; + + double result = evaluateRpn(rpn, mock); + EXPECT_NEAR(result, 1.0, 1e-4); +} + +TEST(RpnEvaluatorTest, ComplexRpn) { + MockPluginRegistry mock; + MockPluginRegistry::TokenInfo addInfo{2, 60, Associativity::Left, true, add_eval}; + MockPluginRegistry::TokenInfo mulInfo{2, 70, Associativity::Left, true, [](const double* a, size_t) { + return a[0] * a[1]; + }}; + + EXPECT_CALL(mock, getTokenInfo("+")).WillRepeatedly(ReturnRef(addInfo)); + EXPECT_CALL(mock, getTokenInfo("*")).WillRepeatedly(ReturnRef(mulInfo)); + + // RPN for 2 + 3 * 4 → 2 3 4 * + + std::vector rpn = { + {Token::TokenType::NUMBER, "2", 2.0}, + {Token::TokenType::NUMBER, "3", 3.0}, + {Token::TokenType::NUMBER, "4", 4.0}, + {Token::TokenType::IDENTIFIER, "*"}, + {Token::TokenType::IDENTIFIER, "+"} + }; + + double result = evaluateRpn(rpn, mock); + EXPECT_DOUBLE_EQ(result, 14.0); +} \ No newline at end of file diff --git a/tests/test_shunting_yard.cpp b/tests/test_shunting_yard.cpp new file mode 100644 index 0000000..31df099 --- /dev/null +++ b/tests/test_shunting_yard.cpp @@ -0,0 +1,103 @@ +#include +#include +#include "../src/Tokenizer.hpp" +#include "../src/ShuntingYard.hpp" +#include "MockPluginRegistry.hpp" + +using ::testing::Return; +using ::testing::ReturnRef; + +// Dummy evaluator (not called in shunting yard) +static double dummy_eval(const double*, size_t) { return 0.0; } + +TEST(ShuntingYardTest, SimpleAddition) { + MockPluginRegistry mock; + MockPluginRegistry::TokenInfo addInfo{2, 60, Associativity::Left, true, dummy_eval}; + + EXPECT_CALL(mock, hasToken("+")).WillRepeatedly(Return(true)); + EXPECT_CALL(mock, getTokenInfo("+")).WillRepeatedly(ReturnRef(addInfo)); + + auto tokens = tokenize("1 + 2"); + auto rpn = shuntingYard(tokens, mock); + + ASSERT_EQ(rpn.size(), 3); + EXPECT_EQ(rpn[0].lexeme, "1"); + EXPECT_EQ(rpn[1].lexeme, "2"); + EXPECT_EQ(rpn[2].lexeme, "+"); +} + +TEST(ShuntingYardTest, OperatorPrecedence) { + MockPluginRegistry mock; + MockPluginRegistry::TokenInfo addInfo{2, 60, Associativity::Left, true, dummy_eval}; + MockPluginRegistry::TokenInfo mulInfo{2, 70, Associativity::Left, true, dummy_eval}; + + EXPECT_CALL(mock, hasToken("+")).WillRepeatedly(Return(true)); + EXPECT_CALL(mock, hasToken("*")).WillRepeatedly(Return(true)); + EXPECT_CALL(mock, getTokenInfo("+")).WillRepeatedly(ReturnRef(addInfo)); + EXPECT_CALL(mock, getTokenInfo("*")).WillRepeatedly(ReturnRef(mulInfo)); + + auto tokens = tokenize("2 + 3 * 4"); + auto rpn = shuntingYard(tokens, mock); + + // RPN: 2 3 4 * + + ASSERT_EQ(rpn.size(), 5); + EXPECT_EQ(rpn[0].lexeme, "2"); + EXPECT_EQ(rpn[1].lexeme, "3"); + EXPECT_EQ(rpn[2].lexeme, "4"); + EXPECT_EQ(rpn[3].lexeme, "*"); + EXPECT_EQ(rpn[4].lexeme, "+"); +} + +TEST(ShuntingYardTest, RightAssociativePower) { + MockPluginRegistry mock; + MockPluginRegistry::TokenInfo powInfo{2, 80, Associativity::Right, true, dummy_eval}; + + EXPECT_CALL(mock, hasToken("^")).WillRepeatedly(Return(true)); + EXPECT_CALL(mock, getTokenInfo("^")).WillRepeatedly(ReturnRef(powInfo)); + + auto tokens = tokenize("2 ^ 3 ^ 2"); + auto rpn = shuntingYard(tokens, mock); + + // Right-assoc: 2^(3^2) → RPN: 2 3 2 ^ ^ + ASSERT_EQ(rpn.size(), 5); + EXPECT_EQ(rpn[0].lexeme, "2"); + EXPECT_EQ(rpn[1].lexeme, "3"); + EXPECT_EQ(rpn[2].lexeme, "2"); + EXPECT_EQ(rpn[3].lexeme, "^"); // inner + EXPECT_EQ(rpn[4].lexeme, "^"); // outer +} + +TEST(ShuntingYardTest, FunctionCall) { + MockPluginRegistry mock; + MockPluginRegistry::TokenInfo sinInfo{1, 90, Associativity::Left, false, dummy_eval}; + + EXPECT_CALL(mock, hasToken("sin")).WillRepeatedly(Return(true)); + EXPECT_CALL(mock, getTokenInfo("sin")).WillRepeatedly(ReturnRef(sinInfo)); + + auto tokens = tokenize("sin(1.57)"); + auto rpn = shuntingYard(tokens, mock); + + ASSERT_EQ(rpn.size(), 2); + EXPECT_EQ(rpn[0].lexeme, "1.57"); + EXPECT_EQ(rpn[1].lexeme, "sin"); +} + +TEST(ShuntingYardTest, NestedFunction) { + MockPluginRegistry mock; + MockPluginRegistry::TokenInfo sinInfo{1, 90, Associativity::Left, false, dummy_eval}; + MockPluginRegistry::TokenInfo cosInfo{1, 90, Associativity::Left, false, dummy_eval}; + + EXPECT_CALL(mock, hasToken("sin")).WillRepeatedly(Return(true)); + EXPECT_CALL(mock, hasToken("cos")).WillRepeatedly(Return(true)); + EXPECT_CALL(mock, getTokenInfo("sin")).WillRepeatedly(ReturnRef(sinInfo)); + EXPECT_CALL(mock, getTokenInfo("cos")).WillRepeatedly(ReturnRef(cosInfo)); + + auto tokens = tokenize("sin(cos(0.5))"); + auto rpn = shuntingYard(tokens, mock); + + // RPN: 0.5 cos sin + ASSERT_EQ(rpn.size(), 3); + EXPECT_EQ(rpn[0].lexeme, "0.5"); + EXPECT_EQ(rpn[1].lexeme, "cos"); + EXPECT_EQ(rpn[2].lexeme, "sin"); +} \ No newline at end of file diff --git a/tests/test_tokenizer.cpp b/tests/test_tokenizer.cpp new file mode 100644 index 0000000..ead299e --- /dev/null +++ b/tests/test_tokenizer.cpp @@ -0,0 +1,54 @@ +#include + +#include "../src/Tokenizer.hpp" +#include "src/Token.hpp" + +TEST(TokenizerTest, ParsesNumbers) { + auto tokens = tokenize("42"); + ASSERT_EQ(tokens.size(), 1); + EXPECT_EQ(tokens[0].type, Token::TokenType::NUMBER); + EXPECT_DOUBLE_EQ(tokens[0].value, 42.0); +} + +TEST(TokenizerTest, ParsesFloats) { + auto tokens = tokenize("3.1415"); + ASSERT_EQ(tokens.size(), 1); + EXPECT_EQ(tokens[0].type, Token::TokenType::NUMBER); + EXPECT_DOUBLE_EQ(tokens[0].value, 3.1415); +} + +TEST(TokenizerTest, ParsesOperatorsAsIdentifiers) { + auto tokens = tokenize("+ - * / ^"); + ASSERT_EQ(tokens.size(), 5); + EXPECT_EQ(tokens[0].type, Token::TokenType::IDENTIFIER); + EXPECT_EQ(tokens[0].lexeme, "+"); + EXPECT_EQ(tokens[1].lexeme, "-"); + EXPECT_EQ(tokens[2].lexeme, "*"); + EXPECT_EQ(tokens[3].lexeme, "/"); + EXPECT_EQ(tokens[4].lexeme, "^"); +} + +TEST(TokenizerTest, ParsesFunctionCalls) { + auto tokens = tokenize("sin(90)"); + ASSERT_EQ(tokens.size(), 4); + EXPECT_EQ(tokens[0].type, Token::TokenType::IDENTIFIER); + EXPECT_EQ(tokens[0].lexeme, "sin"); + EXPECT_EQ(tokens[1].type, Token::TokenType::LPAREN); + EXPECT_EQ(tokens[2].type, Token::TokenType::NUMBER); + EXPECT_EQ(tokens[2].value, 90.0); + EXPECT_EQ(tokens[3].type, Token::TokenType::RPAREN); +} + +TEST(TokenizerTest, ParsesComplexExpression) { + auto tokens = tokenize("2^3 + sin(1.57)"); + ASSERT_EQ(tokens.size(), 8); + EXPECT_EQ(tokens[0].lexeme, "2"); + EXPECT_EQ(tokens[1].lexeme, "^"); + EXPECT_EQ(tokens[2].lexeme, "3"); + EXPECT_EQ(tokens[3].lexeme, "+"); + EXPECT_EQ(tokens[4].lexeme, "sin"); + EXPECT_EQ(tokens[5].type, Token::TokenType::LPAREN); + EXPECT_EQ(tokens[6].type, Token::TokenType::NUMBER); + EXPECT_EQ(tokens[6].value, 1.57); + EXPECT_EQ(tokens[7].type, Token::TokenType::RPAREN); +} \ No newline at end of file From 3b483ba407ab65fe4a71eef08c06ff159f1fedb7 Mon Sep 17 00:00:00 2001 From: Fromant Date: Sun, 26 Oct 2025 22:35:47 +0300 Subject: [PATCH 21/32] made calculator platform independent --- CMakeLists.txt | 1 - src/PluginManager.cpp | 25 ++++++++++++++++++++----- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4f94e11..226b629 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,7 +4,6 @@ project(CalcApp LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) - add_subdirectory(src) add_executable(CalcApp main.cpp) diff --git a/src/PluginManager.cpp b/src/PluginManager.cpp index 164f938..0e60763 100644 --- a/src/PluginManager.cpp +++ b/src/PluginManager.cpp @@ -4,10 +4,25 @@ #include #include +// Platform-specific includes #ifdef _WIN32 #include +#define DLOPEN(filename) LoadLibraryA(filename) +#define DLSYM(handle, sym) GetProcAddress((HMODULE)handle, sym) +#define DLCLOSE(handle) FreeLibrary((HMODULE)handle) +#define DLERROR() "LoadLibrary/GetProcAddress failed" +const char* PLUGIN_EXT = ".dll"; #else -#error "Windows only" +#include +#define DLOPEN(filename) dlopen(filename, RTLD_NOW) +#define DLSYM(handle, sym) dlsym(handle, sym) +#define DLCLOSE(handle) dlclose(handle) +#define DLERROR() dlerror() +#ifdef __APPLE__ +const char* PLUGIN_EXT = ".dylib"; +#else +const char* PLUGIN_EXT = ".so"; +#endif #endif void PluginManager::validatePlugin(FunctionInfo const* info) { @@ -33,7 +48,7 @@ PluginManager::PluginManager() { PluginManager::~PluginManager() { for (void* h : handles_) { - FreeLibrary(static_cast(h)); + if (h) DLCLOSE(h); } } @@ -62,7 +77,7 @@ void PluginManager::loadPlugins() { } void PluginManager::loadPlugin(const std::string& path) { - HMODULE handle = LoadLibraryA(path.c_str()); + HMODULE handle = DLOPEN(path.c_str()); if (!handle) { throw std::runtime_error("LoadLibrary failed"); } @@ -70,11 +85,11 @@ void PluginManager::loadPlugin(const std::string& path) { // typedef const FunctionInfo* (*GetInfoFunc)(); using GetInfoFunc = FunctionInfo*(*)(); auto getInfo = reinterpret_cast( - GetProcAddress(handle, "get_function_info") + DLSYM(handle, "get_function_info") ); if (!getInfo) { - FreeLibrary(handle); + DLCLOSE(handle); throw std::runtime_error("Symbol 'get_function_info' not found"); } From 66c3564aa40c057d91747ee889b3947f3c4c7f51 Mon Sep 17 00:00:00 2001 From: Fromant Date: Sun, 26 Oct 2025 22:37:02 +0300 Subject: [PATCH 22/32] fix: required plugin extension is now also platform independent --- src/PluginManager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PluginManager.cpp b/src/PluginManager.cpp index 0e60763..58ba866 100644 --- a/src/PluginManager.cpp +++ b/src/PluginManager.cpp @@ -60,7 +60,7 @@ void PluginManager::loadPlugins() { } for (const auto& entry : fs::directory_iterator(pluginDir)) { - if (entry.path().extension() == ".dll") { + if (entry.path().extension() == PLUGIN_EXT) { try { loadPlugin(entry.path().string()); } From 02228bc22f3e900e5815e38e0df016c43ac3a238 Mon Sep 17 00:00:00 2001 From: Fromant Date: Sun, 23 Nov 2025 00:01:05 +0300 Subject: [PATCH 23/32] add readme --- README.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..3e3d503 --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +# Simple calculator app +## Operators and functions are loaded from DLLs or SOs + +### Build: +1. `cmake -B ./build -S ./` (on windows you may want to also have `-g Ninja`) +2. `cmake --build ./build` + +This will build all the dlls from ./plugins/ and calculator itself + +### Running + +Simply start the build/CalcApp.exe executable from build directory + +### Usage + +Calculator will try to calculate any expression that is passed in first line of stdin and then exit +If any exception happened, that will be seen in console: + + +### Creating your own dll: +To create your very own function or operator you have to create a separate dll +To make it, follow this steps: + +1. create a `*.cpp` file in `plugins/` folder +2. include `#include "../src/plugin_interface.h"` in it +3. Define required functional: + - your function: `anyname` + - FunctionInfo + - `PLUGIN_API const FunctionInfo* get_function_info()` + - If not understood, see example plugins in `plugins/` folder + - Rebuild the app to create shared library binary from your new .cpp From b2f562225f06c7948c051798e03ba2d32ba98da4 Mon Sep 17 00:00:00 2001 From: Fromant Date: Sun, 23 Nov 2025 00:37:58 +0300 Subject: [PATCH 24/32] plugins rework --- plugins/add_plugin.cpp | 12 ++++++++---- plugins/div_plugin.cpp | 16 ++++++++++------ plugins/mul_plugin.cpp | 9 ++++++--- plugins/pow_plugin.cpp | 11 ++++++----- plugins/sin_plugin.cpp | 11 ++++++----- plugins/sub_plugin.cpp | 10 +++++++--- src/IPluginRegistry.hpp | 2 +- src/RpnEvaluator.cpp | 7 +++++-- src/plugin_interface.h | 35 +++++++++++++++++++++-------------- 9 files changed, 70 insertions(+), 43 deletions(-) diff --git a/plugins/add_plugin.cpp b/plugins/add_plugin.cpp index 162fc2f..b2d17ba 100644 --- a/plugins/add_plugin.cpp +++ b/plugins/add_plugin.cpp @@ -1,12 +1,16 @@ #include "../src/plugin_interface.h" -static double add(const double* args, size_t) { - return args[0] + args[1]; +//количество аргументов функции (оператора) +#define ARGC 2 + +PLUGIN_API PluginResult add_eval(const double* args, size_t count) { + if (count != ARGC) return PluginResult{0.0, "Should accept 2 arguments"}; + return PluginResult{args[0] + args[1], nullptr}; } static const FunctionInfo info = { - "+", 2, 60, Associativity::Left, true, add -}; + "+", ARGC, 60, Associativity::Left, true, add_eval + }; PLUGIN_API const FunctionInfo* get_function_info() { return &info; diff --git a/plugins/div_plugin.cpp b/plugins/div_plugin.cpp index 36c76c6..4c65b82 100644 --- a/plugins/div_plugin.cpp +++ b/plugins/div_plugin.cpp @@ -2,15 +2,19 @@ #include -static double div(const double* args, size_t) { - if (args[1] == 0.0) throw std::domain_error("Division by zero"); - return args[0] / args[1]; +//количество аргументов функции (оператора) +#define ARGC 2 + +PLUGIN_API PluginResult div_eval(const double* args, size_t count) { + if (count != ARGC) return PluginResult{0.0, "Should accept 2 arguments"}; + if (args[1] == 0.0) return PluginResult{0.0, "Division by zero"}; + return PluginResult{args[0] / args[1], nullptr}; } static const FunctionInfo info = { - "/", 2, 70, Associativity::Left, true, div -}; + "/", ARGC, 70, Associativity::Left, true, div_eval + }; PLUGIN_API const FunctionInfo* get_function_info() { return &info; -} \ No newline at end of file +} diff --git a/plugins/mul_plugin.cpp b/plugins/mul_plugin.cpp index 0c1d774..6c130bc 100644 --- a/plugins/mul_plugin.cpp +++ b/plugins/mul_plugin.cpp @@ -1,11 +1,14 @@ #include "../src/plugin_interface.h" -static double mul(const double* args, size_t) { - return args[0] * args[1]; +//количество аргументов функции (оператора) +#define ARGC 2 +PLUGIN_API PluginResult mul_eval(const double* args, size_t count) { + if (count != ARGC) return PluginResult{0.0, "Should accept 2 arguments"}; + return PluginResult{args[0] * args[1], nullptr}; } static const FunctionInfo info = { - "*", 2, 70, Associativity::Left, true, mul + "*", ARGC, 70, Associativity::Left, true, mul_eval }; PLUGIN_API const FunctionInfo* get_function_info() { diff --git a/plugins/pow_plugin.cpp b/plugins/pow_plugin.cpp index 5dc9578..a74cff5 100644 --- a/plugins/pow_plugin.cpp +++ b/plugins/pow_plugin.cpp @@ -3,15 +3,16 @@ #include #include -static double pow_eval(const double* args, size_t) { +//количество аргументов функции (оператора) +#define ARGC 2 + +PLUGIN_API PluginResult pow_eval(const double* args, size_t) { double base = args[0], exp = args[1]; - if (base < 0 && std::floor(exp) != exp) - throw std::domain_error("pow: negative base with non-integer exponent"); - return std::pow(base, exp); + return PluginResult{std::pow(base, exp), nullptr}; } static const FunctionInfo info = { - "^", 2, 80, Associativity::Right, true, pow_eval + "^", ARGC, 80, Associativity::Right, true, pow_eval }; PLUGIN_API const FunctionInfo* get_function_info() { diff --git a/plugins/sin_plugin.cpp b/plugins/sin_plugin.cpp index a3a4907..10a0314 100644 --- a/plugins/sin_plugin.cpp +++ b/plugins/sin_plugin.cpp @@ -1,15 +1,16 @@ #include #include "../src/plugin_interface.h" -struct FunctionInfo; +//количество аргументов функции (оператора) +#define ARGC 1 -static double sin_eval(const double* args, size_t count) { - if (count != 1) return 0.0; - return std::sin(args[0]); +PLUGIN_API PluginResult sin_eval(const double* args, size_t count) { + if (count != 1) return PluginResult{0.0, "Should accept 1 argument"}; + return PluginResult{std::sin(args[0]), nullptr}; } static const FunctionInfo info = { - "sin", 1, 90, Associativity::Left, false, sin_eval + "sin", ARGC, 90, Associativity::Left, false, sin_eval }; PLUGIN_API const FunctionInfo* get_function_info() { return &info; } \ No newline at end of file diff --git a/plugins/sub_plugin.cpp b/plugins/sub_plugin.cpp index c80b33f..00db519 100644 --- a/plugins/sub_plugin.cpp +++ b/plugins/sub_plugin.cpp @@ -1,11 +1,15 @@ #include "../src/plugin_interface.h" -static double sub(const double* args, size_t) { - return args[0] - args[1]; + +//количество аргументов функции (оператора) +#define ARGC 2 + +PLUGIN_API PluginResult sub_eval(const double* args, size_t) { + return PluginResult{args[0] - args[1], nullptr}; } static const FunctionInfo info = { - "-", 2, 60, Associativity::Left, true, sub + "-", ARGC, 60, Associativity::Left, true, sub_eval }; PLUGIN_API const FunctionInfo* get_function_info() { diff --git a/src/IPluginRegistry.hpp b/src/IPluginRegistry.hpp index 29982c9..640e195 100644 --- a/src/IPluginRegistry.hpp +++ b/src/IPluginRegistry.hpp @@ -12,7 +12,7 @@ class IPluginRegistry { int precedence; Associativity associativity; bool is_operator; - double (*evaluate)(const double*, size_t); + PluginResult (*evaluate)(const double*, size_t); }; virtual bool hasToken(const std::string& name) const = 0; diff --git a/src/RpnEvaluator.cpp b/src/RpnEvaluator.cpp index b1872a4..c8a5eae 100644 --- a/src/RpnEvaluator.cpp +++ b/src/RpnEvaluator.cpp @@ -21,8 +21,11 @@ double evaluateRpn(const std::vector& rpn, const IPluginRegistry& pm) { values.pop(); } - double result = info.evaluate(args.data(), args.size()); - values.push(result); + PluginResult result = info.evaluate(args.data(), args.size()); + if (result.error != nullptr) { + throw std::runtime_error("Error: " + token.lexeme + result.error); + } + values.push(result.value); } } diff --git a/src/plugin_interface.h b/src/plugin_interface.h index 24b5e13..514c981 100644 --- a/src/plugin_interface.h +++ b/src/plugin_interface.h @@ -1,6 +1,6 @@ #pragma once -#include +#include //for size_t #ifdef _WIN32 # define PLUGIN_API extern "C" __declspec(dllexport) @@ -8,18 +8,25 @@ #error "Only windows supported" #endif -enum class Associativity { - Left, - Right -}; +extern "C" { + enum class Associativity { + Left, + Right + }; -struct FunctionInfo { - const char* name; - int arity; - int precedence; - Associativity associativity; - bool is_operator; - double (*evaluate)(const double* args, size_t count); -}; + struct PluginResult { + double value; + const char* error; + }; -PLUGIN_API const FunctionInfo* get_function_info(); \ No newline at end of file + struct FunctionInfo { + const char* name; + int arity; + int precedence; + Associativity associativity; + bool is_operator; + PluginResult (*evaluate)(const double* args, size_t count); + }; + + PLUGIN_API const FunctionInfo* get_function_info(); +} From 12247836e4e800dd355d933b119ff1bad1e3631b Mon Sep 17 00:00:00 2001 From: Fromant Date: Sun, 23 Nov 2025 00:38:04 +0300 Subject: [PATCH 25/32] add cos --- plugins/cos_plugin.cpp | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 plugins/cos_plugin.cpp diff --git a/plugins/cos_plugin.cpp b/plugins/cos_plugin.cpp new file mode 100644 index 0000000..bc8e755 --- /dev/null +++ b/plugins/cos_plugin.cpp @@ -0,0 +1,18 @@ +#include "../src/plugin_interface.h" +#include + +//количество аргументов функции (оператора) +#define ARGC 1 + +PLUGIN_API PluginResult cos_eval(const double* args, size_t count) { + if (count != ARGC) return PluginResult{0.0, "Should accept 1 argument"}; + return PluginResult{std::cos(args[0]), nullptr}; +} + +static const FunctionInfo info = { + "cos", ARGC, 90, Associativity::Left, false, cos_eval +}; + +PLUGIN_API const FunctionInfo* get_function_info() { + return &info; +} \ No newline at end of file From a0379fa83d80d40e88aaf32fb409b1a34c5a044f Mon Sep 17 00:00:00 2001 From: Fromant Date: Sun, 23 Nov 2025 00:38:07 +0300 Subject: [PATCH 26/32] add ln --- plugins/ln_plugin.cpp | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 plugins/ln_plugin.cpp diff --git a/plugins/ln_plugin.cpp b/plugins/ln_plugin.cpp new file mode 100644 index 0000000..e17fced --- /dev/null +++ b/plugins/ln_plugin.cpp @@ -0,0 +1,21 @@ +#include "../src/plugin_interface.h" +#include + +//количество аргументов функции (оператора) +#define ARGC 1 + +PLUGIN_API PluginResult ln_eval(const double* args, size_t count) { + if (count != ARGC) return PluginResult{0.0, "Should accept 1 argument"}; + if (args[0] <= 0.0) { + return PluginResult{0.0, "Argument must be positive"}; + } + return PluginResult{std::log(args[0]), nullptr}; +} + +static const FunctionInfo info = { + "ln", ARGC, 90, Associativity::Left, false, ln_eval +}; + +PLUGIN_API const FunctionInfo* get_function_info() { + return &info; +} \ No newline at end of file From 90f430daf466ce03cc300ed3f7c22cfd3914f5bd Mon Sep 17 00:00:00 2001 From: Fromant Date: Sun, 23 Nov 2025 00:38:10 +0300 Subject: [PATCH 27/32] add tg --- plugins/tg_plugin.cpp | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 plugins/tg_plugin.cpp diff --git a/plugins/tg_plugin.cpp b/plugins/tg_plugin.cpp new file mode 100644 index 0000000..c80c159 --- /dev/null +++ b/plugins/tg_plugin.cpp @@ -0,0 +1,20 @@ +#include "../src/plugin_interface.h" +#include +#include + +//количество аргументов функции (оператора) +#define ARGC 1 + +PLUGIN_API PluginResult tan_eval(const double* args, size_t count) { + if (count != ARGC) return PluginResult{0.0, "Should accept 1 argument"}; + double x = args[0]; + return PluginResult{std::tan(x), nullptr}; +} + +static const FunctionInfo info = { + "tg", ARGC, 90, Associativity::Left, false, tan_eval +}; + +PLUGIN_API const FunctionInfo* get_function_info() { + return &info; +} \ No newline at end of file From e6673ebc04d030571e11c77f53eac0d4dedbf69d Mon Sep 17 00:00:00 2001 From: Fromant Date: Sun, 23 Nov 2025 10:45:17 +0300 Subject: [PATCH 28/32] add unary operators support --- plugins/unary_minus.cpp | 20 ++++++ src/IPluginRegistry.hpp | 11 +++- src/PluginManager.cpp | 91 +++++++++++++++++--------- src/PluginManager.hpp | 24 ++++--- src/RpnEvaluator.cpp | 35 +++++++--- src/ShuntingYard.cpp | 138 +++++++++++++++++++++++++--------------- src/Token.hpp | 5 +- src/Tokenizer.cpp | 7 +- 8 files changed, 221 insertions(+), 110 deletions(-) create mode 100644 plugins/unary_minus.cpp diff --git a/plugins/unary_minus.cpp b/plugins/unary_minus.cpp new file mode 100644 index 0000000..fc0fbf1 --- /dev/null +++ b/plugins/unary_minus.cpp @@ -0,0 +1,20 @@ +#include "../src/plugin_interface.h" +#include +#include + +#define ARGC 1 + +PLUGIN_API PluginResult uminus_eval(const double* args, size_t count) { + if (count != ARGC) { + return PluginResult{0.0, "Unary minus: expected 1 argument"}; + } + return PluginResult{-args[0], nullptr}; +} + +static const FunctionInfo info = { + "-", ARGC, 80, Associativity::Right, true, uminus_eval +}; + +PLUGIN_API const FunctionInfo* get_function_info() { + return &info; +} \ No newline at end of file diff --git a/src/IPluginRegistry.hpp b/src/IPluginRegistry.hpp index 640e195..326ebaa 100644 --- a/src/IPluginRegistry.hpp +++ b/src/IPluginRegistry.hpp @@ -11,10 +11,15 @@ class IPluginRegistry { int arity; int precedence; Associativity associativity; - bool is_operator; + bool is_operator; // now mostly for debug; type is known from context PluginResult (*evaluate)(const double*, size_t); }; - virtual bool hasToken(const std::string& name) const = 0; - virtual const TokenInfo& getTokenInfo(const std::string& name) const = 0; + virtual bool hasFunction(const std::string& name) const = 0; + virtual bool hasUnaryOperator(const std::string& name) const = 0; + virtual bool hasBinaryOperator(const std::string& name) const = 0; + + virtual const TokenInfo& getFunction(const std::string& name) const = 0; + virtual const TokenInfo& getUnaryOperator(const std::string& name) const = 0; + virtual const TokenInfo& getBinaryOperator(const std::string& name) const = 0; }; diff --git a/src/PluginManager.cpp b/src/PluginManager.cpp index 58ba866..4521f9e 100644 --- a/src/PluginManager.cpp +++ b/src/PluginManager.cpp @@ -71,63 +71,92 @@ void PluginManager::loadPlugins() { } } - if (registry_.empty()) { + if (functions_.empty() && unary_ops_.empty() && binary_ops_.empty()) { throw std::runtime_error("No valid plugins loaded"); } } void PluginManager::loadPlugin(const std::string& path) { - HMODULE handle = DLOPEN(path.c_str()); + void* handle = DLOPEN(path.c_str()); if (!handle) { - throw std::runtime_error("LoadLibrary failed"); + throw std::runtime_error("Failed to load plugin: " + path); } - // typedef const FunctionInfo* (*GetInfoFunc)(); using GetInfoFunc = FunctionInfo*(*)(); - auto getInfo = reinterpret_cast( - DLSYM(handle, "get_function_info") - ); - + auto getInfo = reinterpret_cast(DLSYM(handle, "get_function_info")); if (!getInfo) { DLCLOSE(handle); - throw std::runtime_error("Symbol 'get_function_info' not found"); + throw std::runtime_error("Symbol 'get_function_info' not found in " + path); } const FunctionInfo* info = getInfo(); if (!info || !info->name || !info->evaluate) { - FreeLibrary(handle); - throw std::runtime_error("Invalid FunctionInfo"); + DLCLOSE(handle); + throw std::runtime_error("Invalid FunctionInfo from " + path); } validatePlugin(info); std::string name(info->name); - - // Check for duplicates - if (registry_.count(name)) { - FreeLibrary(handle); - throw std::runtime_error("Duplicate token name: " + name); + TokenInfo tokenInfo{ + info->arity, + info->precedence, + info->associativity, + info->is_operator, + info->evaluate + }; + + if (info->is_operator) { + if (info->arity == 1) { + if (unary_ops_.count(name)) { + DLCLOSE(handle); + throw std::runtime_error("Duplicate unary operator: " + name); + } + unary_ops_[name] = tokenInfo; + } else if (info->arity == 2) { + if (binary_ops_.count(name)) { + DLCLOSE(handle); + throw std::runtime_error("Duplicate binary operator: " + name); + } + binary_ops_[name] = tokenInfo; + } else { + DLCLOSE(handle); + throw std::runtime_error("Operator must have arity 1 or 2: " + name); + } + } else { + // function + if (functions_.count(name)) { + DLCLOSE(handle); + throw std::runtime_error("Duplicate function: " + name); + } + functions_[name] = tokenInfo; } - registry_[name] = TokenInfo{ - info->arity, - info->precedence, - info->associativity, - info->is_operator, - info->evaluate - }; - handles_.push_back(handle); } -bool PluginManager::hasToken(const std::string& name) const { - return registry_.count(name) > 0; +bool PluginManager::hasFunction(const std::string& name) const { + return functions_.find(name) != functions_.end(); +} +bool PluginManager::hasUnaryOperator(const std::string& name) const { + return unary_ops_.find(name) != unary_ops_.end(); +} +bool PluginManager::hasBinaryOperator(const std::string& name) const { + return binary_ops_.find(name) != binary_ops_.end(); } -const PluginManager::TokenInfo& PluginManager::getTokenInfo(const std::string& name) const { - auto it = registry_.find(name); - if (it == registry_.end()) { - throw std::runtime_error("Unknown token: " + name); - } +const PluginManager::TokenInfo& PluginManager::getFunction(const std::string& name) const { + auto it = functions_.find(name); + if (it == functions_.end()) throw std::runtime_error("Function not found: " + name); + return it->second; +} +const PluginManager::TokenInfo& PluginManager::getUnaryOperator(const std::string& name) const { + auto it = unary_ops_.find(name); + if (it == unary_ops_.end()) throw std::runtime_error("Unary operator not found: " + name); + return it->second; +} +const PluginManager::TokenInfo& PluginManager::getBinaryOperator(const std::string& name) const { + auto it = binary_ops_.find(name); + if (it == binary_ops_.end()) throw std::runtime_error("Binary operator not found: " + name); return it->second; } diff --git a/src/PluginManager.hpp b/src/PluginManager.hpp index 04a17f7..b3cbf79 100644 --- a/src/PluginManager.hpp +++ b/src/PluginManager.hpp @@ -1,27 +1,33 @@ #pragma once #include -#include +#include +#include #include "IPluginRegistry.hpp" #include "plugin_interface.h" -class PluginManager : public IPluginRegistry{ +class PluginManager : public IPluginRegistry { public: - using IPluginRegistry::TokenInfo; // reuse nested type - PluginManager(); ~PluginManager() override; - bool hasToken(const std::string& name) const final; - const TokenInfo& getTokenInfo(const std::string& name) const final; + bool hasFunction(const std::string& name) const final; + bool hasUnaryOperator(const std::string& name) const final; + bool hasBinaryOperator(const std::string& name) const final; + + const TokenInfo& getFunction(const std::string& name) const final; + const TokenInfo& getUnaryOperator(const std::string& name) const final; + const TokenInfo& getBinaryOperator(const std::string& name) const final; private: void loadPlugins(); void loadPlugin(const std::string& path); - static void validatePlugin(FunctionInfo const* info); + static void validatePlugin(const FunctionInfo* info); - std::unordered_map registry_; + std::unordered_map functions_; + std::unordered_map unary_ops_; + std::unordered_map binary_ops_; std::vector handles_; -}; +}; \ No newline at end of file diff --git a/src/RpnEvaluator.cpp b/src/RpnEvaluator.cpp index c8a5eae..ba3faf4 100644 --- a/src/RpnEvaluator.cpp +++ b/src/RpnEvaluator.cpp @@ -1,36 +1,51 @@ #include "RpnEvaluator.hpp" +#include "IPluginRegistry.hpp" #include #include - double evaluateRpn(const std::vector& rpn, const IPluginRegistry& pm) { std::stack values; for (const auto& token : rpn) { - if (token.type == Token::TokenType::NUMBER) { + if (token.type == Token::NUMBER) { values.push(token.value); - } else if (token.type == Token::TokenType::IDENTIFIER) { - const auto& info = pm.getTokenInfo(token.lexeme); - if (values.size() < static_cast(info.arity)) { + } else { + const IPluginRegistry::TokenInfo* info = nullptr; + + switch (token.type) { + case Token::FUNCTION: + info = &pm.getFunction(token.lexeme); + break; + case Token::UNARY_OPERATOR: + info = &pm.getUnaryOperator(token.lexeme); + break; + case Token::BINARY_OPERATOR: + info = &pm.getBinaryOperator(token.lexeme); + break; + default: + throw std::runtime_error("Invalid token in RPN"); + } + + if (values.size() < static_cast(info->arity)) { throw std::runtime_error("Not enough arguments for " + token.lexeme); } - std::vector args(info.arity); - for (int i = info.arity - 1; i >= 0; --i) { + std::vector args(info->arity); + for (int i = info->arity - 1; i >= 0; --i) { args[i] = values.top(); values.pop(); } - PluginResult result = info.evaluate(args.data(), args.size()); + PluginResult result = info->evaluate(args.data(), args.size()); if (result.error != nullptr) { - throw std::runtime_error("Error: " + token.lexeme + result.error); + throw std::runtime_error("Runtime error in " + token.lexeme + ": " + std::string(result.error)); } values.push(result.value); } } if (values.size() != 1) { - throw std::runtime_error("Invalid expression"); + throw std::runtime_error("Invalid RPN expression"); } return values.top(); diff --git a/src/ShuntingYard.cpp b/src/ShuntingYard.cpp index f39d0e2..c14b296 100644 --- a/src/ShuntingYard.cpp +++ b/src/ShuntingYard.cpp @@ -1,79 +1,115 @@ #include "ShuntingYard.hpp" - -#include #include std::vector shuntingYard(const std::vector& tokens, const IPluginRegistry& pm) { - std::stack ops; std::vector output; + std::vector ops; + + bool expectOperand = true; for (const auto& token : tokens) { - if (token.type == Token::TokenType::NUMBER) { + if (token.type == Token::NUMBER) { output.push_back(token); - } else if (token.type == Token::TokenType::IDENTIFIER) { - // Validate: must be known token - if (!pm.hasToken(token.lexeme)) { - throw std::runtime_error("Unknown token: " + token.lexeme); + expectOperand = false; + } + else if (token.type == Token::LPAREN) { + ops.push_back(token); + expectOperand = true; + } + else if (token.type == Token::RPAREN) { + while (!ops.empty() && ops.back().type != Token::LPAREN) { + output.push_back(ops.back()); + ops.pop_back(); + } + if (ops.empty()) { + throw std::runtime_error("Mismatched parentheses"); + } + ops.pop_back(); // remove '(' + + if (!ops.empty() && ops.back().type == Token::FUNCTION) { + output.push_back(ops.back()); + ops.pop_back(); + } + expectOperand = false; + } + else if (token.type == Token::COMMA) { + while (!ops.empty() && ops.back().type != Token::LPAREN) { + output.push_back(ops.back()); + ops.pop_back(); + } + if (ops.empty()) { + throw std::runtime_error("Misplaced comma"); + } + expectOperand = true; + } + else if (token.type == Token::TokenType::IDENTIFIER) { + std::string name = token.lexeme; + + if (expectOperand) { + if (pm.hasUnaryOperator(name)) { + Token t = token; + t.type = Token::UNARY_OPERATOR; + ops.push_back(t); + } + else if (pm.hasFunction(name)) { + Token t = token; + t.type = Token::FUNCTION; + ops.push_back(t); + expectOperand = true; + } + else { + throw std::runtime_error("Unexpected token at operand position: " + name); + } } - const auto& curr = pm.getTokenInfo(token.lexeme); + else { + if (!pm.hasBinaryOperator(name)) { + throw std::runtime_error("Unknown binary operator: " + name); + } + const auto& info = pm.getBinaryOperator(name); - if (curr.is_operator) { - while (!ops.empty() && ops.top().type == Token::TokenType::IDENTIFIER) { - const auto& topTok = ops.top(); - if (!pm.hasToken(topTok.lexeme)) break; - const auto& prev = pm.getTokenInfo(topTok.lexeme); - if (!prev.is_operator) break; + while (!ops.empty()) { + const Token& top = ops.back(); + if (top.type == Token::LPAREN) break; + + if (top.type != Token::BINARY_OPERATOR && top.type != Token::UNARY_OPERATOR) + break; + + const auto* topInfo = (top.type == Token::BINARY_OPERATOR) + ? &pm.getBinaryOperator(top.lexeme) + : &pm.getUnaryOperator(top.lexeme); bool shouldPop = false; - if (curr.associativity == Associativity::Left) { - shouldPop = (curr.precedence <= prev.precedence); - } else { // Right - shouldPop = (curr.precedence < prev.precedence); + if (info.associativity == Associativity::Left) { + shouldPop = (topInfo->precedence >= info.precedence); + } + else { + shouldPop = (topInfo->precedence > info.precedence); } if (!shouldPop) break; - output.push_back(ops.top()); - ops.pop(); + output.push_back(ops.back()); + ops.pop_back(); } - } - ops.push(token); - } else if (token.type == Token::TokenType::COMMA) { - while (!ops.empty() && ops.top().type != Token::TokenType::LPAREN) { - output.push_back(ops.top()); - ops.pop(); - } - } else if (token.type == Token::TokenType::LPAREN) { - ops.push(token); - } else if (token.type == Token::TokenType::RPAREN) { - while (!ops.empty() && ops.top().type != Token::TokenType::LPAREN) { - output.push_back(ops.top()); - ops.pop(); - } - if (ops.empty()) { - throw std::runtime_error("Mismatched parentheses"); - } - ops.pop(); // remove '(' - - // If function is on top, pop it - if (!ops.empty() && ops.top().type == Token::TokenType::IDENTIFIER) { - const auto& fn = pm.getTokenInfo(ops.top().lexeme); - if (!fn.is_operator) { - output.push_back(ops.top()); - ops.pop(); - } + Token t = token; + t.type = Token::BINARY_OPERATOR; + ops.push_back(t); + expectOperand = true; } } + else { + throw std::runtime_error("Unknown token type"); + } } while (!ops.empty()) { - if (ops.top().type == Token::TokenType::LPAREN || ops.top().type == Token::TokenType::RPAREN) { + if (ops.back().type == Token::LPAREN || ops.back().type == Token::RPAREN) { throw std::runtime_error("Mismatched parentheses"); } - output.push_back(ops.top()); - ops.pop(); + output.push_back(ops.back()); + ops.pop_back(); } return output; -} \ No newline at end of file +} diff --git a/src/Token.hpp b/src/Token.hpp index 31ecdea..dbd5919 100644 --- a/src/Token.hpp +++ b/src/Token.hpp @@ -7,7 +7,10 @@ struct Token { enum TokenType { NUMBER, - IDENTIFIER, //operator or function + IDENTIFIER, + FUNCTION, + UNARY_OPERATOR, + BINARY_OPERATOR, LPAREN, RPAREN, COMMA diff --git a/src/Tokenizer.cpp b/src/Tokenizer.cpp index c7f374f..f9e66d2 100644 --- a/src/Tokenizer.cpp +++ b/src/Tokenizer.cpp @@ -42,17 +42,14 @@ std::vector tokenize(const std::string& expr) { tokens.push_back({Token::TokenType::COMMA, ","}); ++i; } - // Everything else: treat as identifier (including +, -, *, /, ^, @, etc.) + // Everything else: treat as identifier else { size_t start = i; - // Take **one character** as identifier (for symbols like +, ^) - // But allow multi-char names like "sin", "max" if (!std::isalpha(c) && c != '_') { - // Single-symbol token (e.g. '+', '^', '@') + // Single-symbol token tokens.push_back({Token::TokenType::IDENTIFIER, std::string(1, c)}); ++i; } else { - // Multi-character identifier (e.g. "sin", "log") while (i < expr.size() && (std::isalnum(static_cast(expr[i])) || expr[i] == '_')) { ++i; } From 53f8a5e5eba4ee8fe41a8b0683425b3fb20982ba Mon Sep 17 00:00:00 2001 From: Fromant Date: Sun, 23 Nov 2025 10:46:29 +0300 Subject: [PATCH 29/32] increase pow precedence --- plugins/pow_plugin.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/pow_plugin.cpp b/plugins/pow_plugin.cpp index a74cff5..f34f36b 100644 --- a/plugins/pow_plugin.cpp +++ b/plugins/pow_plugin.cpp @@ -12,7 +12,7 @@ PLUGIN_API PluginResult pow_eval(const double* args, size_t) { } static const FunctionInfo info = { - "^", ARGC, 80, Associativity::Right, true, pow_eval + "^", ARGC, 90, Associativity::Right, true, pow_eval }; PLUGIN_API const FunctionInfo* get_function_info() { From fc07ae493569e811ed99b9e44fea98cd8c90217a Mon Sep 17 00:00:00 2001 From: Fromant Date: Sun, 23 Nov 2025 10:53:35 +0300 Subject: [PATCH 30/32] ban functions without parentheses --- src/ShuntingYard.cpp | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/ShuntingYard.cpp b/src/ShuntingYard.cpp index c14b296..636845b 100644 --- a/src/ShuntingYard.cpp +++ b/src/ShuntingYard.cpp @@ -1,5 +1,6 @@ #include "ShuntingYard.hpp" #include +#include std::vector shuntingYard(const std::vector& tokens, const IPluginRegistry& pm) { std::vector output; @@ -7,7 +8,9 @@ std::vector shuntingYard(const std::vector& tokens, const IPluginR bool expectOperand = true; - for (const auto& token : tokens) { + for (size_t i = 0; i < tokens.size(); ++i) { + const auto& token = tokens[i]; + if (token.type == Token::NUMBER) { output.push_back(token); expectOperand = false; @@ -42,7 +45,7 @@ std::vector shuntingYard(const std::vector& tokens, const IPluginR } expectOperand = true; } - else if (token.type == Token::TokenType::IDENTIFIER) { + else if (token.type == Token::IDENTIFIER) { std::string name = token.lexeme; if (expectOperand) { @@ -52,13 +55,18 @@ std::vector shuntingYard(const std::vector& tokens, const IPluginR ops.push_back(t); } else if (pm.hasFunction(name)) { + if (i + 1 >= tokens.size() || tokens[i + 1].type != Token::LPAREN) { + throw std::runtime_error( + "Function '" + name + "' must be called with parentheses, e.g. " + name + "(x)" + ); + } Token t = token; t.type = Token::FUNCTION; ops.push_back(t); expectOperand = true; } else { - throw std::runtime_error("Unexpected token at operand position: " + name); + throw std::runtime_error("Unknown identifier: " + name); } } else { @@ -70,7 +78,6 @@ std::vector shuntingYard(const std::vector& tokens, const IPluginR while (!ops.empty()) { const Token& top = ops.back(); if (top.type == Token::LPAREN) break; - if (top.type != Token::BINARY_OPERATOR && top.type != Token::UNARY_OPERATOR) break; @@ -78,13 +85,9 @@ std::vector shuntingYard(const std::vector& tokens, const IPluginR ? &pm.getBinaryOperator(top.lexeme) : &pm.getUnaryOperator(top.lexeme); - bool shouldPop = false; - if (info.associativity == Associativity::Left) { - shouldPop = (topInfo->precedence >= info.precedence); - } - else { - shouldPop = (topInfo->precedence > info.precedence); - } + bool shouldPop = (info.associativity == Associativity::Left) + ? (topInfo->precedence >= info.precedence) + : (topInfo->precedence > info.precedence); if (!shouldPop) break; From e48a359c2819cc564fa6195c19fd0653eefcd1dc Mon Sep 17 00:00:00 2001 From: Fromant Date: Sun, 23 Nov 2025 11:06:36 +0300 Subject: [PATCH 31/32] update README.md --- README.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/README.md b/README.md index 3e3d503..3b07e89 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,12 @@ Simply start the build/CalcApp.exe executable from build directory Calculator will try to calculate any expression that is passed in first line of stdin and then exit If any exception happened, that will be seen in console: +### Supported syntax +- Calculator comes with some plugins out of the box (see `plugins/` dir): basic math and some functions +- Calculator supports parentheses, binary and unary operators, functions +- Function's arguments should be passed in parentheses: `sin(3.14)`, not `sin 3.14` +- Function may have any positive integer number of arguments that is `size_t` able to save: `max(1,2,3,4,5,...)` +- Operators should be single characters ### Creating your own dll: To create your very own function or operator you have to create a separate dll @@ -29,3 +35,15 @@ To make it, follow this steps: - `PLUGIN_API const FunctionInfo* get_function_info()` - If not understood, see example plugins in `plugins/` folder - Rebuild the app to create shared library binary from your new .cpp + +#### Plugin name restrictions: +- Not empty +- Not be '(', ')', ',' - reserved syntax tokens +- Shouldn't start with number +- Operators may have only 1 char as a name +- Plugins within categories (binary operator, unary operator, function) should not share a name inside a category. So, there may not be 2 unary '-' operators, but may be unary '-' and binary '-' and function "-" + +#### What's unary, binary and function? +- If plugin is an operator with arity=2 -> binary operator like `1+2` +- If plugin is an operator with arity=1 -> unary prefix operator `-1`, `(-1+2)`, `-(1+2)` +- if plugin is a function it's implemented as prefix function with any arity: `sin(3.14)`, `pow(2, 4)` \ No newline at end of file From 8c956c34a69d32cb99225f5b655b63a198c6cba0 Mon Sep 17 00:00:00 2001 From: Fromant Date: Sun, 23 Nov 2025 11:30:49 +0300 Subject: [PATCH 32/32] update tests --- tests/MockPluginRegistry.hpp | 9 ++- tests/test_rpn_evaluator.cpp | 76 +++++++++++++-------- tests/test_shunting_yard.cpp | 127 ++++++++++++++++++++++++----------- 3 files changed, 141 insertions(+), 71 deletions(-) diff --git a/tests/MockPluginRegistry.hpp b/tests/MockPluginRegistry.hpp index f852299..8e81181 100644 --- a/tests/MockPluginRegistry.hpp +++ b/tests/MockPluginRegistry.hpp @@ -4,6 +4,11 @@ class MockPluginRegistry : public IPluginRegistry { public: - MOCK_METHOD(bool, hasToken, (const std::string& name), (const, override)); - MOCK_METHOD(const TokenInfo&, getTokenInfo, (const std::string& name), (const, override)); + MOCK_METHOD(bool, hasFunction, (const std::string& name), (const, override)); + MOCK_METHOD(bool, hasUnaryOperator, (const std::string& name), (const, override)); + MOCK_METHOD(bool, hasBinaryOperator, (const std::string& name), (const, override)); + + MOCK_METHOD(const TokenInfo&, getFunction, (const std::string& name), (const, override)); + MOCK_METHOD(const TokenInfo&, getUnaryOperator, (const std::string& name), (const, override)); + MOCK_METHOD(const TokenInfo&, getBinaryOperator, (const std::string& name), (const, override)); }; \ No newline at end of file diff --git a/tests/test_rpn_evaluator.cpp b/tests/test_rpn_evaluator.cpp index bfb1776..8df7c09 100644 --- a/tests/test_rpn_evaluator.cpp +++ b/tests/test_rpn_evaluator.cpp @@ -1,29 +1,37 @@ #include #include +#include #include "../src/RpnEvaluator.hpp" #include "../src/Token.hpp" #include "MockPluginRegistry.hpp" using ::testing::ReturnRef; -static double add_eval(const double* args, size_t) { - return args[0] + args[1]; +static PluginResult add_eval(const double* args, size_t) { + return {args[0] + args[1], nullptr}; } -static double sin_eval(const double* args, size_t) { - return std::sin(args[0]); +static PluginResult sin_eval(const double* args, size_t) { + return {std::sin(args[0]), nullptr}; +} + +static PluginResult unary_minus_eval(const double* args, size_t) { + return {-args[0], nullptr}; +} + +static PluginResult pow_eval(const double* args, size_t) { + return {std::pow(args[0], args[1]), nullptr}; } TEST(RpnEvaluatorTest, BinaryOperator) { MockPluginRegistry mock; - MockPluginRegistry::TokenInfo addInfo{2, 60, Associativity::Left, true, add_eval}; - - EXPECT_CALL(mock, getTokenInfo("+")).WillRepeatedly(ReturnRef(addInfo)); + auto addInfo = IPluginRegistry::TokenInfo{2, 60, Associativity::Left, true, add_eval}; + EXPECT_CALL(mock, getBinaryOperator("+")).WillRepeatedly(ReturnRef(addInfo)); std::vector rpn = { - {Token::TokenType::NUMBER, "2", 2.0}, - {Token::TokenType::NUMBER, "3", 3.0}, - {Token::TokenType::IDENTIFIER, "+"} + {Token::NUMBER, "2", 2.0}, + {Token::NUMBER, "3", 3.0}, + {Token::BINARY_OPERATOR, "+"} }; double result = evaluateRpn(rpn, mock); @@ -32,38 +40,48 @@ TEST(RpnEvaluatorTest, BinaryOperator) { TEST(RpnEvaluatorTest, Function) { MockPluginRegistry mock; - MockPluginRegistry::TokenInfo sinInfo{1, 90, Associativity::Left, false, sin_eval}; - - EXPECT_CALL(mock, getTokenInfo("sin")).WillRepeatedly(ReturnRef(sinInfo)); + auto sinInfo = IPluginRegistry::TokenInfo{1, 90, Associativity::Left, false, sin_eval}; + EXPECT_CALL(mock, getFunction("sin")).WillRepeatedly(ReturnRef(sinInfo)); std::vector rpn = { - {Token::TokenType::NUMBER, "1.5708", 1.5708}, - {Token::TokenType::IDENTIFIER, "sin"} + {Token::NUMBER, "1.5708", 1.5708}, + {Token::FUNCTION, "sin"} }; double result = evaluateRpn(rpn, mock); EXPECT_NEAR(result, 1.0, 1e-4); } -TEST(RpnEvaluatorTest, ComplexRpn) { +TEST(RpnEvaluatorTest, UnaryOperator) { + MockPluginRegistry mock; + auto minusInfo = IPluginRegistry::TokenInfo{1, 90, Associativity::Right, true, unary_minus_eval}; + EXPECT_CALL(mock, getUnaryOperator("-")).WillRepeatedly(ReturnRef(minusInfo)); + + std::vector rpn = { + {Token::NUMBER, "5", 5.0}, + {Token::UNARY_OPERATOR, "-"} + }; + + double result = evaluateRpn(rpn, mock); + EXPECT_DOUBLE_EQ(result, -5.0); +} + +TEST(RpnEvaluatorTest, ComplexExpression_MinusPower) { MockPluginRegistry mock; - MockPluginRegistry::TokenInfo addInfo{2, 60, Associativity::Left, true, add_eval}; - MockPluginRegistry::TokenInfo mulInfo{2, 70, Associativity::Left, true, [](const double* a, size_t) { - return a[0] * a[1]; - }}; + auto powInfo = IPluginRegistry::TokenInfo{2, 100, Associativity::Right, true, pow_eval}; + auto minusInfo = IPluginRegistry::TokenInfo{1, 90, Associativity::Right, true, unary_minus_eval}; - EXPECT_CALL(mock, getTokenInfo("+")).WillRepeatedly(ReturnRef(addInfo)); - EXPECT_CALL(mock, getTokenInfo("*")).WillRepeatedly(ReturnRef(mulInfo)); + EXPECT_CALL(mock, getBinaryOperator("^")).WillRepeatedly(ReturnRef(powInfo)); + EXPECT_CALL(mock, getUnaryOperator("-")).WillRepeatedly(ReturnRef(minusInfo)); - // RPN for 2 + 3 * 4 → 2 3 4 * + + // RPN for -(2^4): 2 4 ^ - std::vector rpn = { - {Token::TokenType::NUMBER, "2", 2.0}, - {Token::TokenType::NUMBER, "3", 3.0}, - {Token::TokenType::NUMBER, "4", 4.0}, - {Token::TokenType::IDENTIFIER, "*"}, - {Token::TokenType::IDENTIFIER, "+"} + {Token::NUMBER, "2", 2.0}, + {Token::NUMBER, "4", 4.0}, + {Token::BINARY_OPERATOR, "^"}, + {Token::UNARY_OPERATOR, "-"} }; double result = evaluateRpn(rpn, mock); - EXPECT_DOUBLE_EQ(result, 14.0); + EXPECT_DOUBLE_EQ(result, -16.0); } \ No newline at end of file diff --git a/tests/test_shunting_yard.cpp b/tests/test_shunting_yard.cpp index 31df099..9b4afc2 100644 --- a/tests/test_shunting_yard.cpp +++ b/tests/test_shunting_yard.cpp @@ -1,40 +1,46 @@ +// test_shunting_yard.cpp #include #include #include "../src/Tokenizer.hpp" #include "../src/ShuntingYard.hpp" +#include "../src/Token.hpp" #include "MockPluginRegistry.hpp" using ::testing::Return; using ::testing::ReturnRef; -// Dummy evaluator (not called in shunting yard) -static double dummy_eval(const double*, size_t) { return 0.0; } +static PluginResult dummy_eval(const double*, size_t) { + return {0.0, nullptr}; +} TEST(ShuntingYardTest, SimpleAddition) { MockPluginRegistry mock; - MockPluginRegistry::TokenInfo addInfo{2, 60, Associativity::Left, true, dummy_eval}; + auto addInfo = IPluginRegistry::TokenInfo{ + 2, 60, Associativity::Left, true, dummy_eval + }; - EXPECT_CALL(mock, hasToken("+")).WillRepeatedly(Return(true)); - EXPECT_CALL(mock, getTokenInfo("+")).WillRepeatedly(ReturnRef(addInfo)); + EXPECT_CALL(mock, hasBinaryOperator("+")).WillRepeatedly(Return(true)); + EXPECT_CALL(mock, getBinaryOperator("+")).WillRepeatedly(ReturnRef(addInfo)); auto tokens = tokenize("1 + 2"); auto rpn = shuntingYard(tokens, mock); ASSERT_EQ(rpn.size(), 3); - EXPECT_EQ(rpn[0].lexeme, "1"); - EXPECT_EQ(rpn[1].lexeme, "2"); + EXPECT_EQ(rpn[0].type, Token::NUMBER); + EXPECT_EQ(rpn[1].type, Token::NUMBER); + EXPECT_EQ(rpn[2].type, Token::BINARY_OPERATOR); EXPECT_EQ(rpn[2].lexeme, "+"); } TEST(ShuntingYardTest, OperatorPrecedence) { MockPluginRegistry mock; - MockPluginRegistry::TokenInfo addInfo{2, 60, Associativity::Left, true, dummy_eval}; - MockPluginRegistry::TokenInfo mulInfo{2, 70, Associativity::Left, true, dummy_eval}; + auto addInfo = IPluginRegistry::TokenInfo{2, 60, Associativity::Left, true, dummy_eval}; + auto mulInfo = IPluginRegistry::TokenInfo{2, 70, Associativity::Left, true, dummy_eval}; - EXPECT_CALL(mock, hasToken("+")).WillRepeatedly(Return(true)); - EXPECT_CALL(mock, hasToken("*")).WillRepeatedly(Return(true)); - EXPECT_CALL(mock, getTokenInfo("+")).WillRepeatedly(ReturnRef(addInfo)); - EXPECT_CALL(mock, getTokenInfo("*")).WillRepeatedly(ReturnRef(mulInfo)); + EXPECT_CALL(mock, hasBinaryOperator("+")).WillRepeatedly(Return(true)); + EXPECT_CALL(mock, hasBinaryOperator("*")).WillRepeatedly(Return(true)); + EXPECT_CALL(mock, getBinaryOperator("+")).WillRepeatedly(ReturnRef(addInfo)); + EXPECT_CALL(mock, getBinaryOperator("*")).WillRepeatedly(ReturnRef(mulInfo)); auto tokens = tokenize("2 + 3 * 4"); auto rpn = shuntingYard(tokens, mock); @@ -44,60 +50,101 @@ TEST(ShuntingYardTest, OperatorPrecedence) { EXPECT_EQ(rpn[0].lexeme, "2"); EXPECT_EQ(rpn[1].lexeme, "3"); EXPECT_EQ(rpn[2].lexeme, "4"); + EXPECT_EQ(rpn[3].type, Token::BINARY_OPERATOR); EXPECT_EQ(rpn[3].lexeme, "*"); + EXPECT_EQ(rpn[4].type, Token::BINARY_OPERATOR); EXPECT_EQ(rpn[4].lexeme, "+"); } TEST(ShuntingYardTest, RightAssociativePower) { MockPluginRegistry mock; - MockPluginRegistry::TokenInfo powInfo{2, 80, Associativity::Right, true, dummy_eval}; + auto powInfo = IPluginRegistry::TokenInfo{2, 100, Associativity::Right, true, dummy_eval}; - EXPECT_CALL(mock, hasToken("^")).WillRepeatedly(Return(true)); - EXPECT_CALL(mock, getTokenInfo("^")).WillRepeatedly(ReturnRef(powInfo)); + EXPECT_CALL(mock, hasBinaryOperator("^")).WillRepeatedly(Return(true)); + EXPECT_CALL(mock, getBinaryOperator("^")).WillRepeatedly(ReturnRef(powInfo)); auto tokens = tokenize("2 ^ 3 ^ 2"); auto rpn = shuntingYard(tokens, mock); - // Right-assoc: 2^(3^2) → RPN: 2 3 2 ^ ^ + // 2 3 2 ^ ^ → right-assoc: 2^(3^2) ASSERT_EQ(rpn.size(), 5); - EXPECT_EQ(rpn[0].lexeme, "2"); - EXPECT_EQ(rpn[1].lexeme, "3"); - EXPECT_EQ(rpn[2].lexeme, "2"); - EXPECT_EQ(rpn[3].lexeme, "^"); // inner - EXPECT_EQ(rpn[4].lexeme, "^"); // outer + EXPECT_EQ(rpn[3].lexeme, "^"); + EXPECT_EQ(rpn[4].lexeme, "^"); } -TEST(ShuntingYardTest, FunctionCall) { +TEST(ShuntingYardTest, FunctionCallWithParens) { MockPluginRegistry mock; - MockPluginRegistry::TokenInfo sinInfo{1, 90, Associativity::Left, false, dummy_eval}; + auto sinInfo = IPluginRegistry::TokenInfo{1, 90, Associativity::Left, false, dummy_eval}; - EXPECT_CALL(mock, hasToken("sin")).WillRepeatedly(Return(true)); - EXPECT_CALL(mock, getTokenInfo("sin")).WillRepeatedly(ReturnRef(sinInfo)); + EXPECT_CALL(mock, hasFunction("sin")).WillRepeatedly(Return(true)); + EXPECT_CALL(mock, getFunction("sin")).WillRepeatedly(ReturnRef(sinInfo)); auto tokens = tokenize("sin(1.57)"); auto rpn = shuntingYard(tokens, mock); ASSERT_EQ(rpn.size(), 2); - EXPECT_EQ(rpn[0].lexeme, "1.57"); + EXPECT_EQ(rpn[0].type, Token::NUMBER); + EXPECT_EQ(rpn[1].type, Token::FUNCTION); EXPECT_EQ(rpn[1].lexeme, "sin"); } -TEST(ShuntingYardTest, NestedFunction) { +TEST(ShuntingYardTest, FunctionWithoutParens_Throws) { + MockPluginRegistry mock; + + EXPECT_CALL(mock, hasFunction("sin")).WillRepeatedly(Return(true)); + EXPECT_CALL(mock, hasUnaryOperator("sin")).WillRepeatedly(Return(false)); + + auto tokens = tokenize("sin 1.57"); // no parentheses + + EXPECT_THROW({ + shuntingYard(tokens, mock); + }, std::runtime_error); +} + +TEST(ShuntingYardTest, UnaryMinus) { MockPluginRegistry mock; - MockPluginRegistry::TokenInfo sinInfo{1, 90, Associativity::Left, false, dummy_eval}; - MockPluginRegistry::TokenInfo cosInfo{1, 90, Associativity::Left, false, dummy_eval}; + auto unaryMinus = IPluginRegistry::TokenInfo{1, 90, Associativity::Right, true, dummy_eval}; + auto addInfo = IPluginRegistry::TokenInfo{2, 60, Associativity::Left, true, dummy_eval}; - EXPECT_CALL(mock, hasToken("sin")).WillRepeatedly(Return(true)); - EXPECT_CALL(mock, hasToken("cos")).WillRepeatedly(Return(true)); - EXPECT_CALL(mock, getTokenInfo("sin")).WillRepeatedly(ReturnRef(sinInfo)); - EXPECT_CALL(mock, getTokenInfo("cos")).WillRepeatedly(ReturnRef(cosInfo)); + EXPECT_CALL(mock, hasUnaryOperator("-")).WillRepeatedly(Return(true)); + EXPECT_CALL(mock, hasBinaryOperator("-")).WillRepeatedly(Return(false)); // not used here + EXPECT_CALL(mock, hasBinaryOperator("+")).WillRepeatedly(Return(true)); + EXPECT_CALL(mock, getUnaryOperator("-")).WillRepeatedly(ReturnRef(unaryMinus)); + EXPECT_CALL(mock, getBinaryOperator("+")).WillRepeatedly(ReturnRef(addInfo)); - auto tokens = tokenize("sin(cos(0.5))"); + auto tokens = tokenize("-1 + 2"); auto rpn = shuntingYard(tokens, mock); - // RPN: 0.5 cos sin - ASSERT_EQ(rpn.size(), 3); - EXPECT_EQ(rpn[0].lexeme, "0.5"); - EXPECT_EQ(rpn[1].lexeme, "cos"); - EXPECT_EQ(rpn[2].lexeme, "sin"); + ASSERT_EQ(rpn.size(), 4); + EXPECT_EQ(rpn[0].type, Token::NUMBER); + EXPECT_EQ(rpn[0].value, 1.0); + EXPECT_EQ(rpn[1].type, Token::UNARY_OPERATOR); + EXPECT_EQ(rpn[1].lexeme, "-"); + EXPECT_EQ(rpn[2].type, Token::NUMBER); + EXPECT_EQ(rpn[3].type, Token::BINARY_OPERATOR); +} + +TEST(ShuntingYardTest, PowerVsUnaryMinus_PowerHasHigherPrecedence) { + MockPluginRegistry mock; + // ^ has higher precedence than unary - + auto powInfo = IPluginRegistry::TokenInfo{2, 100, Associativity::Right, true, dummy_eval}; + auto unaryMinus = IPluginRegistry::TokenInfo{1, 90, Associativity::Right, true, dummy_eval}; + + EXPECT_CALL(mock, hasBinaryOperator("^")).WillRepeatedly(Return(true)); + EXPECT_CALL(mock, hasUnaryOperator("-")).WillRepeatedly(Return(true)); + EXPECT_CALL(mock, getBinaryOperator("^")).WillRepeatedly(ReturnRef(powInfo)); + EXPECT_CALL(mock, getUnaryOperator("-")).WillRepeatedly(ReturnRef(unaryMinus)); + + auto tokens = tokenize("-2 ^ 4"); + auto rpn = shuntingYard(tokens, mock); + + // Expected RPN: 2 4 ^ - + // -(2^4) + ASSERT_EQ(rpn.size(), 4); + EXPECT_EQ(rpn[0].lexeme, "2"); + EXPECT_EQ(rpn[1].lexeme, "4"); + EXPECT_EQ(rpn[2].type, Token::BINARY_OPERATOR); + EXPECT_EQ(rpn[2].lexeme, "^"); + EXPECT_EQ(rpn[3].type, Token::UNARY_OPERATOR); + EXPECT_EQ(rpn[3].lexeme, "-"); } \ No newline at end of file