diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..226b629 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,30 @@ +cmake_minimum_required(VERSION 3.14) +project(CalcApp LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +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 CalcApp POST_BUILD + COMMAND ${CMAKE_COMMAND} -E make_directory $/plugins +) + +foreach (plugin ${PLUGIN_TARGETS}) + add_custom_command(TARGET CalcApp POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy + $ + $/plugins/ + ) +endforeach () + +enable_testing() +add_subdirectory(tests) diff --git a/README.md b/README.md new file mode 100644 index 0000000..3b07e89 --- /dev/null +++ b/README.md @@ -0,0 +1,49 @@ +# 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: + +### 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 +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 + +#### 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 diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..c85abaf --- /dev/null +++ b/main.cpp @@ -0,0 +1,17 @@ +#include +#include + +#include "src/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 diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt new file mode 100644 index 0000000..33612c1 --- /dev/null +++ b/plugins/CMakeLists.txt @@ -0,0 +1,36 @@ +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() +endif() + +set(PLUGIN_TARGETS "") + +foreach(source_file IN LISTS PLUGIN_SOURCES) + # 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}) + + 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}) +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 diff --git a/plugins/PLUGIN_STRUCTURE.md b/plugins/PLUGIN_STRUCTURE.md new file mode 100644 index 0000000..ac3f75e --- /dev/null +++ b/plugins/PLUGIN_STRUCTURE.md @@ -0,0 +1,9 @@ + + +# Every calculator plugin should have 2 functions in ddlexport: +1. `PLUGIN_API const FunctionInfo* get_function_info();` - возвращает имя функции или оператора. +2. `PLUGIN API double name_eval(const double* args, size_t count)` - вычисляет значение функции от аргументов и возвращает результат, или выбрасывает исключение + +### See src/plugin_interface.h for FunctionInfo and PLUGIN_API definition +- `extern "C"` disables name mangling +- `__declspec(dllexport)` exports the symbol from .dll diff --git a/plugins/add_plugin.cpp b/plugins/add_plugin.cpp new file mode 100644 index 0000000..b2d17ba --- /dev/null +++ b/plugins/add_plugin.cpp @@ -0,0 +1,17 @@ +#include "../src/plugin_interface.h" + +//количество аргументов функции (оператора) +#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 = { + "+", ARGC, 60, Associativity::Left, true, add_eval + }; + +PLUGIN_API const FunctionInfo* get_function_info() { + return &info; +} 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 diff --git a/plugins/div_plugin.cpp b/plugins/div_plugin.cpp new file mode 100644 index 0000000..4c65b82 --- /dev/null +++ b/plugins/div_plugin.cpp @@ -0,0 +1,20 @@ +#include "../src/plugin_interface.h" + +#include + +//количество аргументов функции (оператора) +#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 = { + "/", ARGC, 70, Associativity::Left, true, div_eval + }; + +PLUGIN_API const FunctionInfo* get_function_info() { + return &info; +} 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 diff --git a/plugins/mul_plugin.cpp b/plugins/mul_plugin.cpp new file mode 100644 index 0000000..6c130bc --- /dev/null +++ b/plugins/mul_plugin.cpp @@ -0,0 +1,16 @@ +#include "../src/plugin_interface.h" + +//количество аргументов функции (оператора) +#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 = { + "*", ARGC, 70, Associativity::Left, true, mul_eval +}; + +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..f34f36b --- /dev/null +++ b/plugins/pow_plugin.cpp @@ -0,0 +1,20 @@ +#include "../src/plugin_interface.h" + +#include +#include + +//количество аргументов функции (оператора) +#define ARGC 2 + +PLUGIN_API PluginResult pow_eval(const double* args, size_t) { + double base = args[0], exp = args[1]; + return PluginResult{std::pow(base, exp), nullptr}; +} + +static const FunctionInfo info = { + "^", ARGC, 90, Associativity::Right, true, pow_eval +}; + +PLUGIN_API const FunctionInfo* get_function_info() { + return &info; +} \ No newline at end of file diff --git a/plugins/sin_plugin.cpp b/plugins/sin_plugin.cpp new file mode 100644 index 0000000..10a0314 --- /dev/null +++ b/plugins/sin_plugin.cpp @@ -0,0 +1,16 @@ +#include +#include "../src/plugin_interface.h" + +//количество аргументов функции (оператора) +#define ARGC 1 + +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", 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 new file mode 100644 index 0000000..00db519 --- /dev/null +++ b/plugins/sub_plugin.cpp @@ -0,0 +1,17 @@ +#include "../src/plugin_interface.h" + + +//количество аргументов функции (оператора) +#define ARGC 2 + +PLUGIN_API PluginResult sub_eval(const double* args, size_t) { + return PluginResult{args[0] - args[1], nullptr}; +} + +static const FunctionInfo info = { + "-", ARGC, 60, Associativity::Left, true, sub_eval +}; + +PLUGIN_API const FunctionInfo* get_function_info() { + return &info; +} \ No newline at end of file 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 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/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/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); +}; diff --git a/src/IPluginRegistry.hpp b/src/IPluginRegistry.hpp new file mode 100644 index 0000000..326ebaa --- /dev/null +++ b/src/IPluginRegistry.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include +#include "plugin_interface.h" + +class IPluginRegistry { +public: + virtual ~IPluginRegistry() = default; + + struct TokenInfo { + int arity; + int precedence; + Associativity associativity; + bool is_operator; // now mostly for debug; type is known from context + PluginResult (*evaluate)(const double*, size_t); + }; + + 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 new file mode 100644 index 0000000..4521f9e --- /dev/null +++ b/src/PluginManager.cpp @@ -0,0 +1,162 @@ +#include "PluginManager.hpp" +#include +#include +#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 +#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) { + 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* h : handles_) { + if (h) DLCLOSE(h); + } +} + +void PluginManager::loadPlugins() { + namespace fs = std::filesystem; + fs::path pluginDir = "plugins"; + 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() == PLUGIN_EXT) { + try { + loadPlugin(entry.path().string()); + } + catch (const std::exception& e) { + std::cerr << "Failed to load plugin " << entry.path().filename() + << ": " << e.what() << "\n"; + } + } + } + + if (functions_.empty() && unary_ops_.empty() && binary_ops_.empty()) { + throw std::runtime_error("No valid plugins loaded"); + } +} + +void PluginManager::loadPlugin(const std::string& path) { + void* handle = DLOPEN(path.c_str()); + if (!handle) { + throw std::runtime_error("Failed to load plugin: " + path); + } + + using GetInfoFunc = FunctionInfo*(*)(); + auto getInfo = reinterpret_cast(DLSYM(handle, "get_function_info")); + if (!getInfo) { + DLCLOSE(handle); + throw std::runtime_error("Symbol 'get_function_info' not found in " + path); + } + + const FunctionInfo* info = getInfo(); + if (!info || !info->name || !info->evaluate) { + DLCLOSE(handle); + throw std::runtime_error("Invalid FunctionInfo from " + path); + } + + validatePlugin(info); + + std::string name(info->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; + } + + handles_.push_back(handle); +} + +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::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 new file mode 100644 index 0000000..b3cbf79 --- /dev/null +++ b/src/PluginManager.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include +#include +#include + +#include "IPluginRegistry.hpp" +#include "plugin_interface.h" + +class PluginManager : public IPluginRegistry { +public: + PluginManager(); + ~PluginManager() override; + + 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(const FunctionInfo* info); + + 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 new file mode 100644 index 0000000..ba3faf4 --- /dev/null +++ b/src/RpnEvaluator.cpp @@ -0,0 +1,52 @@ +#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::NUMBER) { + values.push(token.value); + } 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) { + args[i] = values.top(); + values.pop(); + } + + PluginResult result = info->evaluate(args.data(), args.size()); + if (result.error != nullptr) { + 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 RPN 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..9843e13 --- /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 IPluginRegistry& pm); \ No newline at end of file diff --git a/src/ShuntingYard.cpp b/src/ShuntingYard.cpp new file mode 100644 index 0000000..636845b --- /dev/null +++ b/src/ShuntingYard.cpp @@ -0,0 +1,118 @@ +#include "ShuntingYard.hpp" +#include +#include + +std::vector shuntingYard(const std::vector& tokens, const IPluginRegistry& pm) { + std::vector output; + std::vector ops; + + bool expectOperand = true; + + 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; + } + 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::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)) { + 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("Unknown identifier: " + name); + } + } + else { + if (!pm.hasBinaryOperator(name)) { + throw std::runtime_error("Unknown binary operator: " + name); + } + const auto& info = pm.getBinaryOperator(name); + + 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 = (info.associativity == Associativity::Left) + ? (topInfo->precedence >= info.precedence) + : (topInfo->precedence > info.precedence); + + if (!shouldPop) break; + + output.push_back(ops.back()); + ops.pop_back(); + } + + 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.back().type == Token::LPAREN || ops.back().type == Token::RPAREN) { + throw std::runtime_error("Mismatched parentheses"); + } + output.push_back(ops.back()); + ops.pop_back(); + } + + return output; +} diff --git a/src/ShuntingYard.hpp b/src/ShuntingYard.hpp new file mode 100644 index 0000000..1389927 --- /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 IPluginRegistry& pm); \ No newline at end of file diff --git a/src/Token.hpp b/src/Token.hpp new file mode 100644 index 0000000..dbd5919 --- /dev/null +++ b/src/Token.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include +#include + + +struct Token { + enum TokenType { + NUMBER, + IDENTIFIER, + FUNCTION, + UNARY_OPERATOR, + BINARY_OPERATOR, + LPAREN, + RPAREN, + COMMA + }; + TokenType type; + std::string lexeme; // for FUNCTION and OPERATOR raw number string + double value = 0.0; // for NUMBER +}; + diff --git a/src/Tokenizer.cpp b/src/Tokenizer.cpp new file mode 100644 index 0000000..f9e66d2 --- /dev/null +++ b/src/Tokenizer.cpp @@ -0,0 +1,62 @@ +#include "Tokenizer.hpp" + +#include +#include + +std::vector tokenize(const std::string& expr) { + std::vector tokens; + size_t i = 0; + + auto skipSpaces = [&]() { + while (i < expr.size() && std::isspace(static_cast(expr[i]))) ++i; + }; + + auto isDigit = [](char c) { return std::isdigit(static_cast(c)) || c == '.'; }; + + while (i < expr.size()) { + skipSpaces(); + if (i >= expr.size()) break; + + char c = expr[i]; + + // Numbers + if (isDigit(c)) { + size_t start = i; + while (i < expr.size() && isDigit(expr[i])) ++i; + std::string numStr = expr.substr(start, i - start); + try { + double val = std::stod(numStr); + tokens.push_back({Token::TokenType::NUMBER, numStr, val}); + } catch (...) { + throw std::runtime_error("Invalid number: " + numStr); + } + } + // Parentheses and comma + else if (c == '(') { + tokens.push_back({Token::TokenType::LPAREN, "("}); + ++i; + } else if (c == ')') { + tokens.push_back({Token::TokenType::RPAREN, ")"}); + ++i; + } else if (c == ',') { + tokens.push_back({Token::TokenType::COMMA, ","}); + ++i; + } + // Everything else: treat as identifier + else { + size_t start = i; + if (!std::isalpha(c) && c != '_') { + // Single-symbol token + tokens.push_back({Token::TokenType::IDENTIFIER, std::string(1, c)}); + ++i; + } else { + while (i < expr.size() && (std::isalnum(static_cast(expr[i])) || expr[i] == '_')) { + ++i; + } + tokens.push_back({Token::TokenType::IDENTIFIER, expr.substr(start, i - start)}); + } + } + } + + return tokens; +} \ No newline at end of file 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); diff --git a/src/plugin_interface.h b/src/plugin_interface.h new file mode 100644 index 0000000..514c981 --- /dev/null +++ b/src/plugin_interface.h @@ -0,0 +1,32 @@ +#pragma once + +#include //for size_t + +#ifdef _WIN32 +# define PLUGIN_API extern "C" __declspec(dllexport) +#else +#error "Only windows supported" +#endif + +extern "C" { + enum class Associativity { + Left, + Right + }; + + struct PluginResult { + double value; + const char* error; + }; + + 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(); +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..25fef3e --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,17 @@ +# 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 + 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(CalcTests ${TEST_SOURCES}) +target_link_libraries(CalcTests CalcLib gtest gmock gtest_main) +target_include_directories(CalcTests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/..) + +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..8e81181 --- /dev/null +++ b/tests/MockPluginRegistry.hpp @@ -0,0 +1,14 @@ +#pragma once +#include "../src/IPluginRegistry.hpp" +#include + +class MockPluginRegistry : public IPluginRegistry { +public: + 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 new file mode 100644 index 0000000..8df7c09 --- /dev/null +++ b/tests/test_rpn_evaluator.cpp @@ -0,0 +1,87 @@ +#include +#include +#include +#include "../src/RpnEvaluator.hpp" +#include "../src/Token.hpp" +#include "MockPluginRegistry.hpp" + +using ::testing::ReturnRef; + +static PluginResult add_eval(const double* args, size_t) { + return {args[0] + args[1], nullptr}; +} + +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; + auto addInfo = IPluginRegistry::TokenInfo{2, 60, Associativity::Left, true, add_eval}; + EXPECT_CALL(mock, getBinaryOperator("+")).WillRepeatedly(ReturnRef(addInfo)); + + std::vector rpn = { + {Token::NUMBER, "2", 2.0}, + {Token::NUMBER, "3", 3.0}, + {Token::BINARY_OPERATOR, "+"} + }; + + double result = evaluateRpn(rpn, mock); + EXPECT_DOUBLE_EQ(result, 5.0); +} + +TEST(RpnEvaluatorTest, Function) { + MockPluginRegistry mock; + auto sinInfo = IPluginRegistry::TokenInfo{1, 90, Associativity::Left, false, sin_eval}; + EXPECT_CALL(mock, getFunction("sin")).WillRepeatedly(ReturnRef(sinInfo)); + + std::vector rpn = { + {Token::NUMBER, "1.5708", 1.5708}, + {Token::FUNCTION, "sin"} + }; + + double result = evaluateRpn(rpn, mock); + EXPECT_NEAR(result, 1.0, 1e-4); +} + +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; + 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, getBinaryOperator("^")).WillRepeatedly(ReturnRef(powInfo)); + EXPECT_CALL(mock, getUnaryOperator("-")).WillRepeatedly(ReturnRef(minusInfo)); + + // RPN for -(2^4): 2 4 ^ - + std::vector rpn = { + {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, -16.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..9b4afc2 --- /dev/null +++ b/tests/test_shunting_yard.cpp @@ -0,0 +1,150 @@ +// 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; + +static PluginResult dummy_eval(const double*, size_t) { + return {0.0, nullptr}; +} + +TEST(ShuntingYardTest, SimpleAddition) { + MockPluginRegistry mock; + auto addInfo = IPluginRegistry::TokenInfo{ + 2, 60, Associativity::Left, true, dummy_eval + }; + + 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].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; + 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, 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); + + // 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].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; + auto powInfo = IPluginRegistry::TokenInfo{2, 100, Associativity::Right, true, dummy_eval}; + + 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); + + // 2 3 2 ^ ^ → right-assoc: 2^(3^2) + ASSERT_EQ(rpn.size(), 5); + EXPECT_EQ(rpn[3].lexeme, "^"); + EXPECT_EQ(rpn[4].lexeme, "^"); +} + +TEST(ShuntingYardTest, FunctionCallWithParens) { + MockPluginRegistry mock; + auto sinInfo = IPluginRegistry::TokenInfo{1, 90, Associativity::Left, false, dummy_eval}; + + 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].type, Token::NUMBER); + EXPECT_EQ(rpn[1].type, Token::FUNCTION); + EXPECT_EQ(rpn[1].lexeme, "sin"); +} + +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; + 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, 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("-1 + 2"); + auto rpn = shuntingYard(tokens, mock); + + 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 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