A simple scripting language in C++
Ferenc Szontágh
2025-04-13 86904d513734134beffc29c6f4012d53a99f25c5
some clean up, added function declaration
13 files modified
2 files renamed
4 files added
1194 ■■■■ changed files
CMakeLists.txt 53 ●●●● patch | view | raw | blame | history
cli/main.cpp 17 ●●●● patch | view | raw | blame | history
cmake/options.h.in 12 ●●●● patch | view | raw | blame | history
src/BaseFunction.hpp 66 ●●●● patch | view | raw | blame | history
src/Builtins/MathUtilsModule.hpp 28 ●●●●● patch | view | raw | blame | history
src/Builtins/PrintModule.hpp 47 ●●●●● patch | view | raw | blame | history
src/Builtins/SleepModule.hpp 43 ●●●●● patch | view | raw | blame | history
src/Lexer.cpp 110 ●●●●● patch | view | raw | blame | history
src/Lexer.hpp 27 ●●●● patch | view | raw | blame | history
src/ScriptException.hpp 124 ●●●●● patch | view | raw | blame | history
src/ScriptExceptionMacros.h 32 ●●●● patch | view | raw | blame | history
src/ScriptInterpreter.cpp 218 ●●●● patch | view | raw | blame | history
src/ScriptInterpreter.hpp 115 ●●●●● patch | view | raw | blame | history
src/ScriptInterpreterHelpers.hpp 101 ●●●●● patch | view | raw | blame | history
src/Token.hpp 136 ●●●●● patch | view | raw | blame | history
src/Value.hpp 58 ●●●● patch | view | raw | blame | history
src/VariableTypes.hpp 7 ●●●● patch | view | raw | blame | history
test_scripts/test1.vs patch | view | raw | blame | history
test_scripts/test2.vs patch | view | raw | blame | history
CMakeLists.txt
@@ -9,32 +9,61 @@
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set(NEED_CLI ON)
set(NEED_TESTS OFF)
set(NEED_STATIC_LIBS ON)
set(NEED_SHARED_LIBS ON)
option(BUILD_CLI, "Build example commandline intrepeter" ON)
option(BUILD_TESTS, "Build tests" OFF)
option(BUILD_SHARED_LIBS "Build shared library" ON)
option(BUILD_STATIC_LIBS "Build static library" ON)
option(BUILD_CLI, "Build example commandline intrepeter" ${NEED_CLI})
option(BUILD_TESTS, "Build tests" ${NEED_TESTS})
option(BUILD_SHARED_LIBS "Build shared library" ${NEED_SHARED_LIBS})
option(BUILD_STATIC_LIBS "Build static library" ${NEED_STATIC_LIBS})
if (BUILD_CLI)
    set(NEED_CLI ${BUILD_CLI})
endif()
if (BUILD_TESTS)
    set(NEED_TESTS ${BUILD_TESTS})
endif()
if (BUILD_SHARED_LIBS)
    set(NEED_SHARED_LIBS ${BUILD_SHARED_LIBS})
endif()
if (BUILD_STATIC_LIBS)
    set(NEED_STATIC_LIBS ${BUILD_STATIC_LIBS})
endif()
set(COMMENT_CHARACTER "#")
set(PARSER_OPEN_TAG "<?void")
set(PARSER_CLOSE_TAG "?>")
message(STATUS "BUILD_CLI: ${BUILD_CLI}")
message(STATUS "BUILD_TESTS: ${BUILD_TESTS}")
message(STATUS "\tCOMMENT_CHARACTER: ${COMMENT_CHARACTER}")
message(STATUS "BUILD_CLI:           ${NEED_CLI}")
message(STATUS "BUILD_TESTS:         ${NEED_TESTS}")
message(STATUS "BUILD_SHARED_LIBS:   ${NEED_SHARED_LIBS}")
message(STATUS "BUILD_STATIC_LIBS:   ${NEED_STATIC_LIBS}")
message(STATUS "  COMMENT_CHARACTER: ${COMMENT_CHARACTER}")
message(STATUS "  PARSER_OPEN_TAG:   ${PARSER_OPEN_TAG}")
message(STATUS "  PARSER_CLOSE_TAG:  ${PARSER_CLOSE_TAG}")
if (CMAKE_BUILD_TYPE STREQUAL "")
    set(CMAKE_BUILD_TYPE Release)
    message(STATUS "CMAKE_BUILD_TYPE is not set, defaulting to Release")
    set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
endif()
message(STATUS "CMAKE_BUILD_TYPE: ${CMAKE_BUILD_TYPE}")
if (CMAKE_BUILD_TYPE STREQUAL "Debug")
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g")
    set(DEBUG_BUILD ON)
endif()
configure_file("cmake/options.h.in" "include/options.h" @ONLY)
configure_file("test_scripts/test1.vs" "test_scripts/test1.vs" @ONLY)
configure_file("test_scripts/test2.vs" "test_scripts/test2.vs" @ONLY)
include_directories(${CMAKE_BINARY_DIR}/include)
include_directories(src)
@@ -45,12 +74,12 @@
    src/Lexer.cpp
)
if (BUILD_SHARED_LIBS)
if (NEED_SHARED_LIBS)
    add_library(${CMAKE_PROJECT_NAME} SHARED)
    target_sources(${CMAKE_PROJECT_NAME} PRIVATE ${SOURCES})
    set_target_properties(${CMAKE_PROJECT_NAME} PROPERTIES OUTPUT_NAME ${CMAKE_PROJECT_NAME})
endif()
if (BUILD_STATIC_LIBS)
if (NEED_STATIC_LIBS)
    add_library(${CMAKE_PROJECT_NAME}_static STATIC)
    target_sources(${CMAKE_PROJECT_NAME}_static PRIVATE ${SOURCES})
    set_target_properties(${CMAKE_PROJECT_NAME}_static PROPERTIES OUTPUT_NAME ${CMAKE_PROJECT_NAME})
@@ -59,7 +88,7 @@
set_target_properties(${CMAKE_PROJECT_NAME} PROPERTIES LINKER_LANGUAGE CXX)
if (BUILD_CLI)
if (NEED_CLI)
    add_executable(cli cli/main.cpp)
    add_dependencies(cli ${CMAKE_PROJECT_NAME})
    if (BUILD_SHARED_LIBS)
cli/main.cpp
@@ -2,6 +2,7 @@
#include <fstream>
#include "Builtins/PrintModule.hpp"
#include "Builtins/SleepModule.hpp"
#include "ScriptInterpreter.hpp"
static bool DEBUG = false;
@@ -15,9 +16,20 @@
    std::string file;
    if (argc == 2) {
        file = argv[1];
    } else if (argc == 3 && (std::string(argv[1]) == "-d" || std::string(argv[1]) == "--debug")) {
    } else if (argc == 3) {
        if (std::string(argv[1]) == "-d" || std::string(argv[1]) == "--debug") {
        DEBUG = true;
        file  = argv[2];
        } else if (argv[1] == "-h" || argv[1] == "--help") {
            std::cout << "Usage: " << argv[0] << " [-d / --debug] <script_file>\n";
            return 0;
        } else if (argv[1] == "-v" || argv[1] == "--vrsion") {
            std::cout << "VoidScript v" << VERSION_STRING << "\n";
            return 0;
        } else {
            std::cerr << "Usage: " << argv[0] << " [-d / --debug] <script_file>\n";
            return 1;
        }
    } else {
        std::cerr << "Usage: " << argv[0] << " [-d / --debug] <script_file>\n";
        return 1;
@@ -39,7 +51,8 @@
        std::string        content((std::istreambuf_iterator<char>(input)), std::istreambuf_iterator<char>());
        ScriptInterpreter interp;
        interp.registerFunction("print", std::make_shared<PrintFunction>());
        interp.registerModule("print", std::make_shared<PrintFunction>());
        interp.registerModule("sleep", std::make_shared<SleepFunction>());
        interp.executeScript(content, filename, DEBUG);
    } catch (const std::exception & e) {
        std::cerr << "Parser error: " << e.what() << "\n";
cmake/options.h.in
@@ -1,3 +1,7 @@
#ifndef VOIDSCRIPT_OPTIONS_H
#define VOIDSCRIPT_OPTIONS_H
/*
* THIS IS A GENERATED FILE. DO NOT EDIT.
*/
@@ -7,10 +11,12 @@
const char EOL = '\n';
#endif
const char   COMMENT_CHARACTER = '@COMMENT_CHARACTER@';
const static char * PARSER_OPEN_TAG   = "<?void";
const static char * PARSER_CLOSE_TAG  = "?>";
const static char * PARSER_OPEN_TAG   = "@PARSER_OPEN_TAG@";
const static char * PARSER_CLOSE_TAG  = "@PARSER_CLOSE_TAG@";
const static char * VERSION_MINOR     = "@CMAKE_PROJECT_VERSION_MINOR@";
const static char * VERSION_MAJOR     = "@CMAKE_PROJECT_VERSION_MAJOR@";
const static char * VERSION_PATCH     = "@CMAKE_PROJECT_VERSION_PATCH@";
const static char * VERSION_STRING    = "@CMAKE_PROJECT_VERSION@-@CMAKE_BUILD_TYPE@-@CMAKE_SYSTEM@";
#cmakedefine DEBUG_BUILD
#cmakedefine01 DEBUG_BUILD
#endif // VOIDSCRIPT_OPTIONS_H
src/BaseFunction.hpp
@@ -1,26 +1,78 @@
// ScriptFunction.hpp
#ifndef SCRIPT_FUNCTION_HPP
#define SCRIPT_FUNCTION_HPP
#include <functional>
#include <stdexcept>
#include <unordered_map>
#include <utility>
#include <vector>
#include "ScriptExceptionMacros.h"
#include "ScriptInterpreterHelpers.hpp"
#include "Token.hpp"
#include "Value.hpp"
class ScriptInterpreter;
using CallbackFunction = std::function<Value(const std::vector<Value> &)>;
using CallBackStorage  = std::unordered_map<std::string, CallbackFunction>;
class BaseFunction {
  protected:
    std::string name;
    CallBackStorage functionMap;
  public:
    virtual ~BaseFunction()                                                       = default;
    virtual void  validate(const std::vector<Token> & tokens, size_t & i) const   = 0;
    BaseFunction(const std::string & functionName) : name(functionName) {}
    virtual void addFunction(const std::string & name, std::function<Value(const std::vector<Value> &)> callback) {
        functionMap[name] = std::move(callback);
    }
    virtual void validate(const std::vector<Token> & tokens, size_t & i,
                          const std::unordered_map<std::string, Value> & variables) {
        size_t index = i;
        if (tokens[index].type != TokenType::Identifier) {
            THROW_UNEXPECTED_TOKEN_ERROR(tokens[index], "identifier");
        }
        index++;  // skip function name
        if (tokens[index].type != TokenType::LeftParenthesis) {
            THROW_UNEXPECTED_TOKEN_ERROR(tokens[index], "(");
        }
        index++;  // skip '('
        std::vector<Token> args;
        while (tokens[index].type != TokenType::RightParenthesis) {
            if (tokens[index].type == TokenType::Comma) {
                index++;
                continue;
            }
            if (tokens[index].type == TokenType::Variable && !variables.contains(tokens[index].lexeme)) {
                THROW_UNDEFINED_VARIABLE_ERROR(tokens[index].lexeme, tokens[index]);
            }
            args.push_back(tokens[index]);
            index++;
        }
        index++;  // skip ')'
        if (tokens[index].type != TokenType::Semicolon) {
            THROW_UNEXPECTED_TOKEN_ERROR(tokens[index], ";");
        }
        this->validateArgs(args, variables);
        ScriptInterpreterHelpers::expectSemicolon(tokens, index, "function call");
    }
    virtual void validateArgs(const std::vector<Token> &                     args,
                              const std::unordered_map<std::string, Value> & variables) = 0;
    virtual Value call(const std::vector<Value> & args, bool debug = false) const = 0;
    template <typename FuncClass> void registerFunctionTo(ScriptInterpreter & interp) {
        FuncClass::registerTo(interp);
    }
    template <typename FuncClass> void registerFunctionTo(ScriptInterpreter & interp) { FuncClass::registerTo(interp); }
};
#endif  // SCRIPT_FUNCTION_HPP
src/Builtins/MathUtilsModule.hpp
New file
@@ -0,0 +1,28 @@
#ifndef MATH_UTILS_MODULE_HPP
#define MATH_UTILS_MODULE_HPP
#include <stdexcept>
#include <vector>
#include "ScriptExceptionMacros.h"
#include "Value.hpp"
class MathUtils {
  public:
    static Value multiply(const std::vector<Value> & args) {
        if (args.size() != 2) {
            throw std::runtime_error("multiply expects two arguments.");
        }
        if (args[0].type == Variables::Type::VT_INT && args[1].type == Variables::Type::VT_INT) {
            int  left   = args[0].ToInt();
            int  right  = args[1].ToInt();
            auto result = Value();
            result.data = left * right;
            result.type = Variables::Type::VT_INT;
            return result;
            //return Value::fromInt(left * right);
        }
        THROW_INVALID_FUNCTION_ARGUMENT_ERROR("multiply", args[0].TypeToString(), args[0].GetToken());
    };
};
#endif
src/Builtins/PrintModule.hpp
@@ -5,54 +5,37 @@
#include "BaseFunction.hpp"
#include "ScriptExceptionMacros.h"
#include "ScriptInterpreter.hpp"
#include "Token.hpp"
#include "Value.hpp"
class PrintFunction : public BaseFunction {
  private:
    const std::string name = "print";
    bool              addNewLine = false;
  public:
    void validate(const std::vector<Token> & tokens, size_t & i) const override {
        auto index = i;
        if (tokens[index].type != TokenType::Identifier) {
            THROW_UNEXPECTED_TOKEN_ERROR(tokens[index], "identifier: " + name);
    PrintFunction() : BaseFunction(name) {}
    void validateArgs(const std::vector<Token> &                     args,
                      const std::unordered_map<std::string, Value> & variables) override {
        if (args.size() == 0) {
            THROW_UNEXPECTED_TOKEN_ERROR(args[0], "at least one argument");
        }
        index++;  // skip function name
        if (tokens[index].type != TokenType::LeftParenthesis) {
            THROW_UNEXPECTED_TOKEN_ERROR(tokens[index], "('");
        }
        index++;  // skip '('
        if (tokens[index].type != TokenType::StringLiteral && tokens[index].type != TokenType::Variable &&
            tokens[index].type != TokenType::IntLiteral && tokens[index].type != TokenType::DoubleLiteral) {
            THROW_UNEXPECTED_TOKEN_ERROR(tokens[index], "string, int, double or variable as argument");
        }
        size_t count = 0;
        while (tokens[index].type != TokenType::RightParenthesis) {
            if (tokens[index].type == TokenType::StringLiteral || tokens[index].type == TokenType::Variable ||
                tokens[index].type == TokenType::IntLiteral || tokens[index].type == TokenType::DoubleLiteral) {
                count++;
                index++;
            } else if (tokens[index].type == TokenType::Comma) {
                index++;
            } else {
                THROW_UNEXPECTED_TOKEN_ERROR(tokens[index], "string, int, double or variable as argument");
        for (const auto & arg : args) {
            if (arg.type == TokenType::Variable) {
                if (!variables.contains(arg.lexeme)) {
                    THROW_UNDEFINED_VARIABLE_ERROR(arg.lexeme, arg);
            }
        }
        if (count == 0) {
            throw std::runtime_error("print() requires at least one argument at");
        }
        index++;  // skip ')'
        if (tokens[index].type == TokenType::Semicolon) {
            index++;
        } else {
            THROW_UNEXPECTED_TOKEN_ERROR(tokens[index], ";");
        if (args.end()->variableType == Variables::Type::VT_INT || args.end()->type == TokenType::IntLiteral) {
            this->addNewLine = true;
        }
    }
    Value call(const std::vector<Value> & args, bool debug = false) const override {
        for (const auto & arg : args) {
            std::cout << arg.ToString();
            std::cout << arg.ToString(); // todo: add endline if the last parameter is bool
        }
        return Value();
    }
src/Builtins/SleepModule.hpp
New file
@@ -0,0 +1,43 @@
#ifndef SLEEPFUNCTION_HPP
#define SLEEPFUNCTION_HPP
#include <thread>
#include "BaseFunction.hpp"
class SleepFunction : public BaseFunction {
  public:
    SleepFunction() : BaseFunction("sleep") {}
    void validateArgs(const std::vector<Token> &                     args,
                      const std::unordered_map<std::string, Value> & variables) override {
        if (args.size() != 1) {
            throw std::runtime_error("sleep() requires exactly one argument");
        }
        const Token & arg = args[0];
        if (arg.type == TokenType::IntLiteral) {
            return;
        }
        if (arg.type == TokenType::Variable) {
            const auto & value = variables.at(arg.lexeme);
            if (value.type != Variables::Type::VT_INT) {
                THROW_VARIABLE_TYPE_MISSMATCH_ERROR(arg.lexeme, Variables::TypeToString(Variables::Type::VT_INT), "",
                                                    Variables::TypeToString(value.type), arg);
            }
            return;
        }
        THROW_UNEXPECTED_TOKEN_ERROR(arg, "int literal or variable");
    }
    Value call(const std::vector<Value> & args, bool debug = false) const override {
        std::this_thread::sleep_for(std::chrono::seconds(args[0].ToInt()));
        return Value();
    }
};
#endif  // SLEEPFUNCTION_HPP
src/Lexer.cpp
@@ -2,7 +2,7 @@
#include <cctype>
#include "Value.hpp"
#include "options.h"
Lexer::Lexer(const std::string & source, const std::string & filename) :
    src(source),
@@ -128,17 +128,18 @@
    while (isalpha(peek())) {
        lexeme += advance();
    }
    if (Variables::StringToTypeMap.contains(lexeme)) {
        const auto type = Variables::StringToTypeMap.at(lexeme);
    auto it = Variables::StringToTypeMap.find(lexeme);
    if (it != Variables::StringToTypeMap.end()) {
        const auto & type = it->second;
        while (isspace(peek())) {
            advance();
        }
        if (peek() == '$') {
            return variableDeclaration(type);
            return this->variableDeclaration(type);
        }
        return { TokenType::Identifier, lexeme, filename, lineNumber, startCol };
    }
    return { TokenType::Identifier, lexeme, filename, lineNumber, startCol };
}
@@ -151,19 +152,15 @@
        while (isalnum(peek()) || peek() == '_') {
            varName += advance();
        }
        switch (type) {
            case Variables::Type::VT_INT:
                return { TokenType::IntDeclaration, varName, filename, lineNumber, startCol };
            case Variables::Type::VT_DOUBLE:
                return { TokenType::DoubleDeclaration, varName, filename, lineNumber, startCol };
            case Variables::Type::VT_STRING:
                return { TokenType::StringDeclaration, varName, filename, lineNumber, startCol };
            default:
        for (auto it = Variables::StringToTypeMap.begin(); it != Variables::StringToTypeMap.end(); ++it) {
            if (it->second == type) {
                return { getTokenTypeFromValueDeclaration(it->second), varName, filename, lineNumber, startCol };
            }
        }
                return { TokenType::Unknown, "Invalid variable type in declaration", filename, lineNumber, startCol };
        }
    } else {
        return { TokenType::Unknown, "$ followed by invalid character in declaration", filename, lineNumber, startCol };
    }
}
Token Lexer::singleCharToken(TokenType type, const std::string & lexeme) {
@@ -172,15 +169,30 @@
    return { type, lexeme, filename, lineNumber, startCol };
}
bool Lexer::matchSequence(const std::string & sequence) const {
    if (pos + sequence.length() > src.length()) {
bool Lexer::matchSequence(const std::string & sequence, bool caseSensitive) const {
    if (this->pos + sequence.size() > src.size()) {
        return false;
    }
    return src.substr(pos, sequence.length()) == sequence;
    for (size_t i = 0; i < sequence.size(); ++i) {
        char srcChar = src[this->pos + i];
        char seqChar = sequence[i];
        if (!caseSensitive) {
            srcChar = std::tolower(static_cast<unsigned char>(srcChar));
            seqChar = std::tolower(static_cast<unsigned char>(seqChar));
}
void Lexer::matchAndConsume(const std::string & sequence) {
    if (matchSequence(sequence)) {
        if (srcChar != seqChar) {
            return false;
        }
    }
    return true;
}
void Lexer::matchAndConsume(const std::string & sequence, bool caseSensitive) {
    if (matchSequence(sequence, caseSensitive)) {
        for (size_t i = 0; i < sequence.length(); ++i) {
            advance();
        }
@@ -189,18 +201,19 @@
std::vector<Token> Lexer::tokenize() {
    std::vector<Token> tokens;
    tokens.reserve(src.size() / 4);
    while (!isAtEnd()) {
        char c = peek();
    while (pos < src.size()) {
        char c = src[pos];
        if (isspace(c)) {
            advance();
            continue;
        }
        if (c == '\n') {
            tokens.push_back(singleCharToken(TokenType::EndOfLine, ""));
            tokens.push_back(singleCharToken(TokenType::EndOfLine, "\n"));
            continue;
        }
        if (c == '#') {
        if (c == COMMENT_CHARACTER) {
            tokens.push_back(comment());
            advance();  // Skip newline after comment
            continue;
@@ -217,27 +230,56 @@
            tokens.push_back({ TokenType::ParserCloseTag, PARSER_CLOSE_TAG, filename, lineNumber, startCol });
            continue;
        }
        if (isalpha(c)) {
        if (matchSequence("if")) {
            size_t startCol = colNumber;
            matchAndConsume("if");
            tokens.push_back({ TokenType::ParserIfStatement, "if", filename, lineNumber, startCol });
            continue;
        }
        switch (c) {
            case 'a' ... 'z':
            case 'A' ... 'Z':
            tokens.push_back(keywordOrIdentifier());
        } else if (c == '$') {
                break;
            case '$':
            tokens.push_back(variable());
        } else if (isdigit(c)) {
                break;
            case '0' ... '9':
            tokens.push_back(number());
        } else if (c == '"' || c == '\'') {
                break;
            case '"':
            case '\'':
            tokens.push_back(string());
        } else if (c == '(') {
                break;
            case '(':
            tokens.push_back(singleCharToken(TokenType::LeftParenthesis, "("));
        } else if (c == ')') {
                break;
            case ')':
            tokens.push_back(singleCharToken(TokenType::RightParenthesis, ")"));
        } else if (c == ',') {
                break;
            case ',':
            tokens.push_back(singleCharToken(TokenType::Comma, ","));
        } else if (c == ';') {
                break;
            case ';':
            tokens.push_back(singleCharToken(TokenType::Semicolon, ";"));
        } else if (c == '=') {
                break;
            case '=':
            tokens.push_back(singleCharToken(TokenType::Equals, "="));
        } else {
                break;
            case '+':
                tokens.push_back(singleCharToken(TokenType::Plus, "+"));
                break;
            case '{':
                tokens.push_back(singleCharToken(TokenType::LeftCurlyBracket, "{"));
                break;
            case '}':
                tokens.push_back(singleCharToken(TokenType::RightCurlyBracket, "}"));
                break;
            default:
            tokens.push_back({ TokenType::Unknown, std::string(1, c), filename, lineNumber, colNumber });
            advance();
                break;
        }
    }
src/Lexer.hpp
@@ -1,13 +1,13 @@
#ifndef LEXER_HPP
#define LEXER_HPP
#include <algorithm>
#include <istream>
#include <sstream>
#include <vector>
#include "VariableTypes.hpp"
#include "options.h"
#include "Token.hpp"
#include "VariableTypes.hpp"
class Lexer {
  public:
@@ -32,11 +32,11 @@
    Token variable();
    Token comment();
    Token keywordOrIdentifier();
    Token boolean();
    Token singleCharToken(TokenType type, const std::string & lexeme);
    bool matchSequence(const std::string & sequence) const;
    bool  matchSequence(const std::string & sequence, bool caseSensitive = true) const;
    Token variableDeclaration(Variables::Type type);
    void matchAndConsume(const std::string & sequence);
    void  matchAndConsume(const std::string & sequence, bool caseSensitive = true);
    // validate number types from string
    template <typename Numeric> static bool is_number(const std::string & s) {
@@ -44,7 +44,22 @@
        return ((std::istringstream(s) >> n >> std::ws).eof());
    }
    bool matchSequence(const std::string & sequence) { return src.substr(pos, sequence.length()) == sequence; }
    bool matchSequence(const std::string & sequence, bool caseSensitive = true) {
        if (caseSensitive) {
            return src.substr(pos, sequence.length()) == sequence;
        }
        std::string srcSubstr = src.substr(pos, sequence.length());
        std::string seqLower  = sequence;
        std::transform(srcSubstr.begin(), srcSubstr.end(), srcSubstr.begin(),
                       [](unsigned char c) { return std::tolower(c); });
        std::transform(seqLower.begin(), seqLower.end(), seqLower.begin(),
                       [](unsigned char c) { return std::tolower(c); });
        return srcSubstr == seqLower;
    }
};
#endif  // LEXER_HPP
src/ScriptException.hpp
New file
@@ -0,0 +1,124 @@
#ifndef SCRIPTEXCEPTION_HPP
#define SCRIPTEXCEPTION_HPP
#include <stdexcept>
#include <string>
#include "options.h"
#include "Token.hpp"
enum class ScriptErrorType : std::uint8_t {
    UnexpectedToken,
    UndefinedVariable,
    UndefinedFunction,
    VariableTypeMismatch,
    VariableRedefinition,
    Custom
};
class ScriptException : public std::runtime_error {
  public:
    ScriptException(ScriptErrorType type, const std::string & message, const std::string & file = "", int line = 0,
                    const Token & token = Token()) :
        std::runtime_error(message),
        type_(type),
        file_(file),
        line_(line),
        token_(token),
        fullMessage_(formatMessage(message)) {}
    const char * what() const noexcept override { return fullMessage_.c_str(); }
    ScriptErrorType type() const { return type_; }
    const std::string & file() const { return file_; }
    int line() const { return line_; }
    const Token & token() const { return token_; }
    static ScriptException makeUnexpectedTokenError(const Token & token, const std::string & expected = "",
                                                    const std::string & file = "", int line = 0) {
        std::string msg = "unexpected token: '" + token.lexeme + "'";
#if DEBUG_BUILD == 1
        msg.append(" token type: " + tokenTypeNames.at(token.type));
#endif
        if (!expected.empty()) {
            msg += ", expected: '" + expected + "'";
        }
        return ScriptException(ScriptErrorType::UnexpectedToken, msg, file, line, token);
    }
    static ScriptException makeUndefinedVariableError(const std::string & name, const Token & token,
                                                      const std::string & file = "", int line = 0) {
        std::string msg = "undefined variable: '$" + name + "'";
        return ScriptException(ScriptErrorType::UndefinedVariable, msg, file, line, token);
    }
    static ScriptException makeUndefinedFunctionError(const std::string & name, const Token & token,
                                                      const std::string & file = "", int line = 0) {
        std::string msg = "undefined function: '" + name + "'";
#if DEBUG_BUILD == 1
        msg.append(", type: " + tokenTypeNames.at(token.type));
#endif
        return ScriptException(ScriptErrorType::UndefinedFunction, msg, file, line, token);
    }
    static ScriptException makeVariableRedefinitionError(const std::string & name, const Token & token,
                                                         const std::string & file = "", int line = 0) {
        std::string msg = "variable already defined: '" + name + "'";
        return ScriptException(ScriptErrorType::VariableRedefinition, msg, file, line, token);
    }
    static ScriptException makeVariableTypeMismatchError(const std::string & targetVar, const std::string & targetType,
                                                         const std::string & sourceVar, const std::string & sourceType,
                                                         const Token & token, const std::string & file = "",
                                                         int line = 0) {
        std::string msg = "variable type mismatch: '$" + targetVar + "' declared type: '" + targetType + "'";
        if (!sourceVar.empty()) {
            msg += ", source variable: '" + sourceVar + "'";
        }
        if (!sourceType.empty()) {
            msg += ", assigned type: '" + sourceType + "'";
        }
        return ScriptException(ScriptErrorType::VariableTypeMismatch, msg, file, line, token);
    }
    static ScriptException makeFunctionRedefinitionError(const std::string & name, const Token & token,
                                                         const std::string & file = "", int line = 0) {
        std::string msg = "variable already defined: '" + name + "'";
        return ScriptException(ScriptErrorType::VariableRedefinition, msg, file, line, token);
    }
    static ScriptException makeFunctionInvalidArgumentError(const std::string & functionName,
                                                            const std::string & argName, const Token & token,
                                                            const std::string & file = "", int line = 0) {
        std::string msg = "invalid argument for function '" + functionName + "': '" + argName + "'";
        return ScriptException(ScriptErrorType::Custom, msg, file, line, token);
    }
  private:
    ScriptErrorType type_;
    std::string     file_;
    int             line_;
    Token           token_;
    std::string     fullMessage_;
    std::string formatMessage(const std::string & base) const {
        std::string formatted = base;
        if (!token_.file.empty()) {
            formatted += " in file: " + token_.file + ":" + std::to_string(token_.lineNumber) + ":" +
                         std::to_string(token_.columnNumber);
        }
#if DEBUG_BUILD == 1
        if (!file_.empty()) {
            formatted = file_ + ":" + std::to_string(line_) + "\n" + formatted;
        }
#endif
        return formatted;
    }
};
#endif  // SCRIPTEXCEPTION_HPP
src/ScriptExceptionMacros.h
@@ -1,19 +1,43 @@
#ifndef SCRIPT_EXCEPTION_MACROS_H
#define SCRIPT_EXCEPTION_MACROS_H
#include "ScriptException.hpp"
//
// Purpose of macros: unified exception handling with extended error information (source file and line number)
//
// Invalid token type - expected different type
#define THROW_UNEXPECTED_TOKEN_ERROR(token, expected) \
    ScriptInterpreter::throwUnexpectedTokenError(token, expected, __FILE__, __LINE__)
    throw ScriptException::makeUnexpectedTokenError(token, expected, __FILE__, __LINE__)
#define THROW_UNEXPECTED_TOKEN_ERROR_HELPER(token, expected, file, line) \
    throw ScriptException::makeUnexpectedTokenError(token, expected, file, line)
// Accessing unknown (undefined) variable
#define THROW_UNDEFINED_VARIABLE_ERROR(name, token) \
    ScriptInterpreter::throwUndefinedVariableError(name, token, __FILE__, __LINE__)
    throw ScriptException::makeUndefinedVariableError(name, token, __FILE__, __LINE__)
// Unknown (undefined) function call
#define THROW_UNDEFINED_FUNCTION_ERROR(name, token) \
    throw ScriptException::makeUndefinedFunctionError(name, token, __FILE__, __LINE__)
// Variable type mismatch - e.g. string instead of number
#define THROW_VARIABLE_TYPE_MISSMATCH_ERROR(target_variable_name, target_variable_type, source_variable_name,       \
                                            source_variable_type, token)                                            \
    ScriptInterpreter::throwVariableTypeMissmatchError(target_variable_name, target_variable_type,                  \
    throw ScriptException::makeVariableTypeMismatchError(target_variable_name, target_variable_type,                  \
                                                       source_variable_name, source_variable_type, token, __FILE__, \
                                                       __LINE__)
// Redefining a variable with the same name is not allowed
#define THROW_VARIABLE_REDEFINITION_ERROR(name, token) \
    ScriptInterpreter::throwVariableRedefinitionError(name, token, __FILE__, __LINE__)
    throw ScriptException::makeVariableRedefinitionError(name, token, __FILE__, __LINE__)
#define THROW_FUNCTION_REDEFINITION_ERROR(name, token) \
    throw ScriptException::makeFunctionRedefinitionError(name, token, __FILE__, __LINE__)
// Invalid or incorrect function argument
#define THROW_INVALID_FUNCTION_ARGUMENT_ERROR(functionName, argName, token) \
    throw ScriptException::makeFunctionInvalidArgumentError(functionName, argName, token, __FILE__, __LINE__)
#endif  // SCRIPT_EXCEPTION_MACROS_H
src/ScriptInterpreter.cpp
@@ -9,17 +9,17 @@
#include "ScriptExceptionMacros.h"
#include "Value.hpp"
void ScriptInterpreter::registerFunction(const std::string & name, std::shared_ptr<BaseFunction> fn) {
void ScriptInterpreter::registerModule(const std::string & name, std::shared_ptr<BaseFunction> fn) {
    functionObjects[name] = std::move(fn);
}
Value ScriptInterpreter::evaluateExpression(const Token & token) const {
    if (token.type == TokenType::StringLiteral) {
        return Value::fromString(token.lexeme);
        return Value::fromString(token);
    }
    if (token.type == TokenType::IntLiteral) {
        try {
            return Value::fromInt(std::stoi(token.lexeme));
            return Value::fromInt(token);
        } catch (const std::invalid_argument & e) {
            throw std::runtime_error("Invalid integer literal: " + token.lexeme);
        } catch (const std::out_of_range & e) {
@@ -28,7 +28,7 @@
    }
    if (token.type == TokenType::DoubleLiteral) {
        try {
            return Value::fromDouble(std::stod(token.lexeme));
            return Value::fromDouble(token);
        } catch (const std::invalid_argument & e) {
            throw std::runtime_error("Invalid double literal: " + token.lexeme);
        } catch (const std::out_of_range & e) {
@@ -36,24 +36,22 @@
        }
    }
    if (token.type == TokenType::Variable) {
        if (variables.find(token.lexeme) != variables.end()) {
            return variables.at(token.lexeme);
        return this->getVariable(token);
        }
        THROW_UNDEFINED_VARIABLE_ERROR(token.lexeme, token);
    } else {
        THROW_UNEXPECTED_TOKEN_ERROR(token, "string, integer, double, or variable");
    }
    return Value();
}
std::vector<Value> ScriptInterpreter::parseArguments(const std::vector<Token> & tokens,
                                                      std::size_t &              current_index) const {
std::vector<Value> ScriptInterpreter::parseFunctionArguments(const std::vector<Token> & tokens,
                                                             std::size_t &              index) const {
    std::vector<Value> args;
    size_t             current_index = index;
    if (current_index >= tokens.size() || tokens[current_index].type != TokenType::Identifier) {
        THROW_UNEXPECTED_TOKEN_ERROR(tokens[current_index], tokenTypeNames.at(TokenType::Identifier));
    }
    current_index++;  // Skip function name
    //   if (current_index >= tokens.size() || tokens[current_index].type != TokenType::Identifier) {
    //       THROW_UNEXPECTED_TOKEN_ERROR(tokens[current_index], tokenTypeNames.at(TokenType::Identifier));
    //   }
    //   current_index++;  // Skip function name
    if (current_index >= tokens.size() || tokens[current_index].type != TokenType::LeftParenthesis) {
        THROW_UNEXPECTED_TOKEN_ERROR(tokens[current_index], tokenTypeNames.at(TokenType::LeftParenthesis));
@@ -77,10 +75,50 @@
        THROW_UNEXPECTED_TOKEN_ERROR(tokens[current_index], "')'");
    }
    current_index++;  // Skip ')'
    current_index = current_index;
    index = current_index;
    return args;
}
void ScriptInterpreter::handleBooleanDeclaration(const std::vector<Token> & tokens, std::size_t & i) {
    const auto varName = tokens[i].lexeme;
    const auto varType = tokens[i].variableType;
    i++;      // Skip variable name
    if (i < tokens.size() && tokens[i].type == TokenType::Equals) {
        i++;  // Skip '='
        if (i < tokens.size() && tokens[i].type == TokenType::Variable) {
            const auto variable = this->getVariable(tokens[i]);
            if (variable.type != Variables::Type::VT_BOOLEAN) {
                THROW_VARIABLE_TYPE_MISSMATCH_ERROR(varName, Variables::TypeToString(Variables::Type::VT_BOOLEAN),
                                                    tokens[i].lexeme, variable.TypeToString(), tokens[i]);
            }
            this->setVariable(varName, variable);
            i++;  // Skip variable name
            EXPECT_SEMICOLON(tokens, i, "after bool variable declaration");
        } else if (i < tokens.size() && tokens[i].type == TokenType::Identifier) {
            std::string lowered = tokens[i].lexeme;
            std::transform(lowered.begin(), lowered.end(), lowered.begin(),
                           [](unsigned char c) { return std::tolower(c); });
            if (lowered == "true") {
                this->setVariable(varName, Value::fromBoolean(tokens[i], true));
            } else if (lowered == "false") {
                this->setVariable(varName, Value::fromBoolean(tokens[i], false));
            } else {
                THROW_UNEXPECTED_TOKEN_ERROR(tokens[i], "true or false after '='");
            }
            i++;  // Skip boolean literal
            EXPECT_SEMICOLON(tokens, i, "after bool declaration");
        } else {
            THROW_UNEXPECTED_TOKEN_ERROR(tokens[i], "bool literal after '='");
        }
    } else {
        THROW_UNEXPECTED_TOKEN_ERROR(tokens[i], "= after bool declaration");
    }
};
void ScriptInterpreter::handleStringDeclaration(const std::vector<Token> & tokens, std::size_t & i) {
    const auto varName = tokens[i].lexeme;
@@ -91,23 +129,21 @@
        i++;  // Skip '='
        if (i < tokens.size() && tokens[i].type == TokenType::Variable) {
            if (variables.find(tokens[i].lexeme) == variables.end()) {
                THROW_UNDEFINED_VARIABLE_ERROR(tokens[i].lexeme, tokens[i]);
            } else {
                if (variables[tokens[i].lexeme].type != Variables::Type::VT_STRING) {
                    THROW_VARIABLE_TYPE_MISSMATCH_ERROR(varName, Variables::TypeToString(Variables::Type::VT_STRING),
                                                        tokens[i].lexeme, variables[tokens[i].lexeme].TypeToString(),
                                                        tokens[i]);
                }
            const auto variable = this->getVariable(tokens[i]);
                variables[varName] = variables[tokens[i].lexeme];
                i++;  // Skip variable name
                expectSemicolon(tokens, i, "after string variable declaration");
            if (variable.type != Variables::Type::VT_STRING) {
                THROW_VARIABLE_TYPE_MISSMATCH_ERROR(varName, Variables::TypeToString(Variables::Type::VT_STRING),
                                                    tokens[i].lexeme, variable.TypeToString(), tokens[i]);
            }
            this->setVariable(varName, variable);
            //variables[varName] = variables[tokens[i].lexeme];
            i++;  // Skip variable name
            EXPECT_SEMICOLON(tokens, i, "after string variable declaration");
        } else if (i < tokens.size() && tokens[i].type == TokenType::StringLiteral) {
            variables[varName] = Value::fromString(tokens[i].lexeme);
            this->setVariable(varName, Value::fromString(tokens[i]));
            i++;  // Skip string literal
            expectSemicolon(tokens, i, "after string declaration");
            EXPECT_SEMICOLON(tokens, i, "after string declaration");
        } else {
            THROW_UNEXPECTED_TOKEN_ERROR(tokens[i], "string literal after '='");
        }
@@ -126,10 +162,12 @@
        if (i < tokens.size()) {
            if (type == TokenType::IntDeclaration && tokens[i].type == TokenType::IntLiteral) {
                try {
                    if (variables.find(varName) != variables.end()) {
                        THROW_VARIABLE_REDEFINITION_ERROR(varName, tokens[i]);
                    const auto variable = this->getVariable(tokens[i]);
                    if (variable.type != Variables::Type::VT_INT) {
                        THROW_VARIABLE_TYPE_MISSMATCH_ERROR(varName, Variables::TypeToString(Variables::Type::VT_INT),
                                                            tokens[i].lexeme, variable.TypeToString(), tokens[i]);
                    }
                    variables[varName] = Value::fromInt(std::stoi(tokens[i].lexeme));
                    this->setVariable(varName, Value::fromInt(tokens[i]));
                    i++;  // Skip int literal
                } catch (const std::invalid_argument & e) {
                    throw std::runtime_error("Invalid integer literal in declaration: " + tokens[i].lexeme);
@@ -138,10 +176,13 @@
                }
            } else if (type == TokenType::DoubleDeclaration && tokens[i].type == TokenType::DoubleLiteral) {
                try {
                    if (variables.find(varName) != variables.end()) {
                        THROW_VARIABLE_REDEFINITION_ERROR(varName, tokens[i]);
                    const auto variable = this->getVariable(tokens[i]);
                    if (variable.type != Variables::Type::VT_DOUBLE) {
                        THROW_VARIABLE_TYPE_MISSMATCH_ERROR(varName,
                                                            Variables::TypeToString(Variables::Type::VT_DOUBLE),
                                                            tokens[i].lexeme, variable.TypeToString(), tokens[i]);
                    }
                    variables[varName] = Value::fromDouble(std::stod(tokens[i].lexeme));
                    this->setVariable(varName, Value::fromDouble(tokens[i]));
                    i++;  // Skip double literal
                } catch (const std::invalid_argument & e) {
                    throw std::runtime_error("Invalid double literal in declaration: " + tokens[i].lexeme);
@@ -153,7 +194,7 @@
                THROW_VARIABLE_TYPE_MISSMATCH_ERROR(varName, expectedType, "",
                                                    getVariableTypeFromTokenTypeAsString(tokens[i].type), tokens[i]);
            }
            expectSemicolon(tokens, i, "after variable declaration");
            EXPECT_SEMICOLON(tokens, i, "after variable declaration");
        } else {
            THROW_UNEXPECTED_TOKEN_ERROR(tokens[i - 1], "literal after '='");
        }
@@ -162,34 +203,87 @@
    }
}
void ScriptInterpreter::handleFunctionDeclaration(const std::vector<Token> & tokens, std::size_t & i) {
    const auto varName = tokens[i].lexeme;
    const auto varType = tokens[i].variableType;
    i++;  // skip funct name
    if (i < tokens.size() && tokens[i].type != TokenType::Equals) {
        THROW_UNEXPECTED_TOKEN_ERROR(tokens[i], "= after function declaration");
    }
    i++;  // skip '='
    if (this->functionParameters.find(varName) != this->functionParameters.end()) {
        THROW_FUNCTION_REDEFINITION_ERROR(varName, tokens[i]);
    }
    if (i < tokens.size() && tokens[i].type != TokenType::LeftParenthesis) {
        THROW_UNEXPECTED_TOKEN_ERROR(tokens[i - 1], "");
    }
    i++;
    // parse arg definitions
    const auto args = ScriptInterpreterHelpers::parseFunctionDeclarationArguments(tokens, i);
    std::cout << "args: " << args.size() << std::endl;
    for (const auto & arg : args) {
        std::cout << "arg name: " << arg.GetToken().lexeme << " type: " << arg.TypeToString() << std::endl;
    }
    this->functionParameters[varName].assign(args.begin(), args.end());
    const std::string body        = ScriptInterpreterHelpers::getFunctionBody(tokens, i);
    this->functionBodies[varName] = body;
    // recheck the close curly brace
    if (i >= tokens.size() || tokens[i].type != TokenType::RightCurlyBracket) {
        THROW_UNEXPECTED_TOKEN_ERROR(tokens[i], "}");
    }
    i++;
    //this->functionBodies[varName] = std::make_shared<ScriptInterpreter>();
    //this->functionBodies[varName]->executeScript(body, this->filename, true);
    //EXPECT_SEMICOLON(tokens, i, "after function declaration");
    // there is no need semicolon to the end of the function declaration
    std::cout << "function body: \n\"" << body << "\"" << std::endl;
}
void ScriptInterpreter::handleFunctionCall(const std::vector<Token> & tokens, std::size_t & i) {
    auto        index    = i;
    std::string funcName = tokens[i].lexeme;
    index++;  // skip funct name
    if (index < tokens.size() && tokens[index].type != TokenType::LeftParenthesis) {
        THROW_UNEXPECTED_TOKEN_ERROR(tokens[index - 1], "");
    }
    auto        it       = functionObjects.find(funcName);
    if (it == functionObjects.end()) {
        throw std::runtime_error("Unknown function: " + funcName);
        THROW_UNDEFINED_FUNCTION_ERROR(funcName, tokens[i]);
    }
    it->second->validate(tokens, i);
    std::vector<Value> args = parseArguments(tokens, i);
    //    it->second->validate(tokens, i, this->variables);
    std::vector<Value> args = parseFunctionArguments(tokens, index);
    it->second->call(args);
    if (i < tokens.size() && tokens[i].type == TokenType::Semicolon) {
        i++;  // Skip ';' after function call
    }
    i = index;
    EXPECT_SEMICOLON(tokens, i, "after function call");
    //    if (i < tokens.size() && tokens[i].type == TokenType::Semicolon) {
    //        i++;  // Skip ';' after function call
    //    }
}
void ScriptInterpreter::handleVariableReference(const std::vector<Token> & tokens, std::size_t & i) {
    //THROW_UNEXPECTED_TOKEN_ERROR(tokens[i], "function call or variable assignment (not yet implemented)");
    const auto varName = tokens[i].lexeme;
    const auto varType = tokens[i].variableType;
//    const auto varName = tokens[i].lexeme;
//    const auto varType = tokens[i].variableType;
    const auto& varToken = tokens[i];
    i++;      // Skip variable token to avoid infinite loop
    if (i < tokens.size() && tokens[i].type == TokenType::Equals) {
        i++;  // Skip '='
        if (i < tokens.size()) {
            if (variables.find(varName) == variables.end()) {
                THROW_UNDEFINED_VARIABLE_ERROR(varName, tokens[i]);
            }
            variables[varName] = evaluateExpression(tokens[i]);
            const auto variable = this->getVariable(varToken);
            this->setVariable(varToken.lexeme, evaluateExpression(tokens[i]));
            i++;  // Skip value
            expectSemicolon(tokens, i, "after variable assignment");
            EXPECT_SEMICOLON(tokens, i, "after variable assignment");
        } else {
            THROW_UNEXPECTED_TOKEN_ERROR(tokens[i - 1], "value after '='");
        }
@@ -206,16 +300,8 @@
    i++;  // Skip semicolon token
}
void ScriptInterpreter::expectSemicolon(const std::vector<Token> & tokens, std::size_t & i,
                                         const std::string & message) const {
    if (i >= tokens.size() || tokens[i].type != TokenType::Semicolon) {
        THROW_UNEXPECTED_TOKEN_ERROR(tokens[i - 1], "; " + message);
    } else {
        i++;  // Skip ';'
    }
}
void ScriptInterpreter::executeScript(const std::string & source, const std::string & filename, bool debug) {
void ScriptInterpreter::executeScript(const std::string & source, const std::string & filename, bool ignore_tags) {
    this->filename = filename;
    Lexer lexer(source, filename);
    auto  tokens = lexer.tokenize();
@@ -240,17 +326,21 @@
            continue;
        }
        if (!insideScript) {
            // Csak kiíratás, ha nem vagyunk script tagben
            std::cout << token.lexeme;
        if (insideScript == false && ignore_tags == false) {
            //std::cout << token.lexeme;
            i++;
            continue;
        }
        // A szokásos feldolgozás csak ha belül vagyunk
        switch (token.type) {
            case TokenType::StringDeclaration:
                handleStringDeclaration(tokens, i);
                break;
            case TokenType::BooleanDeclaration:
                handleBooleanDeclaration(tokens, i);
                break;
            case TokenType::FunctionDeclaration:
                handleFunctionDeclaration(tokens, i);
                break;
            case TokenType::IntDeclaration:
            case TokenType::DoubleDeclaration:
@@ -269,7 +359,7 @@
                handleSemicolon(i);
                break;
            default:
                throw std::runtime_error("Unexpected token inside script: " + token.lexeme);
                THROW_UNEXPECTED_TOKEN_ERROR(token, "");
        }
    }
}
src/ScriptInterpreter.hpp
@@ -1,6 +1,7 @@
#ifndef SSCRIPTINTERPRETER_HPP
#define SSCRIPTINTERPRETER_HPP
#include <functional>
#include <map>
#include <memory>
#include <string>
#include <unordered_map>
@@ -10,90 +11,66 @@
#include "Token.hpp"
#include "Value.hpp"
using FunctionValidator = std::function<void(const std::vector<Token> &, size_t &)>;
using FunctionValidator =
    std::function<void(const std::vector<Token> &, size_t &, const std::unordered_map<std::string, Value> &)>;
using VariableContext = std::unordered_map<std::string, Value>;
class ScriptInterpreter {
  public:
    void registerFunction(const std::string & name, std::shared_ptr<BaseFunction> fn);
    void executeScript(const std::string & source, const std::string & filenaneame, bool debug = false);
    static void throwUnexpectedTokenError(const Token & token, const std::string & expected = "",
                                          const std::string & file = "", const int & line = 0) {
        const std::string error_content =
            "unexpected token: '" + token.lexeme + "' type: " + tokenTypeNames.at(token.type) +
            (expected.empty() ? "" : ", expected: '" + expected + "'") + " in file: " + token.file + ":" +
            std::to_string(token.lineNumber) + ":" + std::to_string(token.columnNumber);
#ifdef DEBUG_BUILD
        const std::string error_message = file + ":" + std::to_string(line) + "\n" + error_content;
#else
        const std::string& error_message = error_content;
#endif
        throw std::runtime_error(error_message);
    };
    static void throwUndefinedVariableError(const std::string & name, const Token & token, const std::string & file,
                                            const int & line = 0) {
        const std::string error_content = "undefined variable: '$" + name + "' in file: " + token.file + ":" +
                                          std::to_string(token.lineNumber) + ":" + std::to_string(token.columnNumber);
#ifdef DEBUG_BUILD
        const std::string error_message = file + ":" + std::to_string(line) + "\n" + error_content;
#else
        const std::string& error_message = error_content;
#endif
        throw std::runtime_error(error_message);
    }
    static void throwVariableTypeMissmatchError(const std::string & target_variable_name,
                                                const std::string & target_type,
                                                const std::string & source_variable_name,
                                                const std::string & source_type, const Token & token,
                                                const std::string & file = "", const int & line = 0) {
        std::string error_content =
            "variable type missmatch: '$" + target_variable_name + "' declared type: '" + target_type + "'";
        if (!source_variable_name.empty()) {
            error_content += " source variable: '" + source_variable_name + "'";
        }
        if (!source_type.empty()) {
            error_content += " assigned type: '" + source_type + "'";
        }
        error_content += " in file: " + token.file + ":" + std::to_string(token.lineNumber) + ":" +
                         std::to_string(token.columnNumber);
#ifdef DEBUG_BUILD
        const std::string error_message = file + ":" + std::to_string(line) + "\n" + error_content;
#else
        const std::string error_message = error_content;
#endif
        throw std::runtime_error(error_message);
    }
    static void throwVariableRedefinitionError(const std::string & name, const Token & token,
                                               const std::string & file = "", const int line = 0) {
        const std::string error_content = "variable alread defined: " + name + " in file: " + token.file + ":" +
                                          std::to_string(token.lineNumber) + ":" + std::to_string(token.columnNumber);
#ifdef DEBUG_BUILD
        const std::string error_message = file + ":" + std::to_string(line) + "\n" + error_content;
#else
        const std::string& error_message = error_content;
#endif
        throw std::runtime_error(error_message);
    }
    void registerModule(const std::string & name, std::shared_ptr<BaseFunction> fn);
    void executeScript(const std::string & source, const std::string & filenaneame, bool ignore_tags = false);
  private:
    std::unordered_map<std::string, FunctionValidator>             functionValidators;
    std::unordered_map<std::string, std::shared_ptr<BaseFunction>> functionObjects;
    std::unordered_map<std::string, Value>                         variables;
    // store the current script's variables
    std::unordered_map<std::string, VariableContext>               variables;
    // store the current script's function arguments
    std::unordered_map<std::string, std::vector<Value>>            functionParameters;
    std::unordered_map<std::string, std::string>                   functionBodies;
    std::string                                                    filename;
    std::vector<Value> parseArguments(const std::vector<Token> & tokens, std::size_t & current_index) const;
    Value              evaluateExpression(const Token & token) const;
    [[nodiscard]] std::vector<Value> parseFunctionArguments(const std::vector<Token> & tokens,
                                                            std::size_t &              index) const;
    [[nodiscard]] Value              evaluateExpression(const Token & token) const;
    // type handlers
    void expectSemicolon(const std::vector<Token> & tokens, std::size_t & i, const std::string & message) const;
    void setVariable(const std::string & name, const Value & value, const std::string & context = "default") {
        this->variables[context][name] = value;
    }
    [[nodiscard]] Value getVariable(const std::string & name, const std::string & context = "default") const {
        for (auto it = variables.begin(); it != variables.end(); ++it) {
            if (it->first == context) {
                const auto & innerMap = it->second.find(name);
                if (innerMap != it->second.end()) {
                    return it->second.at(name);
                }
            }
        }
        throw std::runtime_error("Variable not found: " + name);
    };
    [[nodiscard]] Value getVariable(const Token & token, const std::string & context = "default") const {
        for (auto it = variables.begin(); it != variables.end(); ++it) {
            if (it->first == context) {
                const auto & innerMap = it->second.find(token.lexeme);
                if (innerMap != it->second.end()) {
                    return it->second.at(token.lexeme);
                }
            }
        }
        THROW_UNDEFINED_VARIABLE_ERROR(token.lexeme, token);
    };
    void handleFunctionCall(const std::vector<Token> & tokens, std::size_t & i);
    void handleVariableReference(const std::vector<Token> & tokens, std::size_t & i);
    void handleComment(std::size_t & i);
    void handleSemicolon(std::size_t & i);
    void handleStringDeclaration(const std::vector<Token> & tokens, std::size_t & i);
    void handleBooleanDeclaration(const std::vector<Token> & tokens, std::size_t & i);
    void handleNumberDeclaration(const std::vector<Token> & tokens, std::size_t & i, TokenType type);
    void handleFunctionDeclaration(const std::vector<Token> & tokens, std::size_t & i);
};
#endif  // SSCRIPTINTERPRETER_HPP
src/ScriptInterpreterHelpers.hpp
New file
@@ -0,0 +1,101 @@
#ifndef SCRIPTINTERPRETERHELPERS_HPP
#define SCRIPTINTERPRETERHELPERS_HPP
#include <iostream>
#include <ostream>
#include <vector>
#include "ScriptExceptionMacros.h"
#include "Token.hpp"
#include "Value.hpp"
#define EXPECT_SEMICOLON(tokens, i, message) \
    ScriptInterpreterHelpers::expectSemicolon(tokens, i, message, __FILE__, __LINE__)
namespace ScriptInterpreterHelpers {
static std::string extractSubstring(const std::string & str, size_t start, size_t end) {
    if (start >= 0 && start < str.length() && end >= start && end < str.length()) {
        return str.substr(start, end - start + 1);
    }
    return "";
}
static void expectSemicolon(const std::vector<Token> & tokens, std::size_t & i, const std::string & message,
                            const std::string & file = __FILE__, int line = __LINE__) {
    if (i >= tokens.size() || tokens[i].type != TokenType::Semicolon) {
        THROW_UNEXPECTED_TOKEN_ERROR_HELPER(tokens[i - 1], "; " + message, file, line);
    }
    i++;  // Skip ';'
}
[[nodiscard]] static std::vector<Value> parseFunctionDeclarationArguments(const std::vector<Token> & tokens,
                                                                          std::size_t &              i,
                                                                          const std::string &        file = __FILE__,
                                                                          int                        line = __LINE__) {
    std::vector<Value> arguments;
    // check the arguments types
    if (tokens[i].type != TokenType::StringDeclaration && tokens[i].type != TokenType::BooleanDeclaration &&
        tokens[i].type != TokenType::IntDeclaration && tokens[i].type != TokenType::DoubleDeclaration) {
        THROW_UNEXPECTED_TOKEN_ERROR_HELPER(tokens[i], "variable declaration", file, line);
    }
    const auto parameter_type = getVariableTypeFromTokenTypeDeclaration(tokens[i].type);
    if (parameter_type == Variables::Type::VT_NOT_DEFINED) {
        THROW_UNEXPECTED_TOKEN_ERROR_HELPER(tokens[i], "valid type identifier", file, line);
    }
    if (parameter_type == Variables::Type::VT_FUNCTION) {
        THROW_UNEXPECTED_TOKEN_ERROR_HELPER(tokens[i], "valid type identifier", file, line);
    }
    if (parameter_type == Variables::Type::VT_NULL) {
        THROW_UNEXPECTED_TOKEN_ERROR_HELPER(tokens[i], "valid type identifier", file, line);
    }
    Value val;
    val.type  = parameter_type;
    val.token = tokens[i];
    arguments.emplace_back(std::move(val));
    i++;  // Skip variable declaration
    if (tokens[i].type != TokenType::RightParenthesis) {
        THROW_UNEXPECTED_TOKEN_ERROR_HELPER(tokens[i], ") - Only one argument is allowed", file, line);
    }
    i++;  // Skip ')'
    return arguments;
}
[[nodiscard]] static std::string getFunctionBody(const std::vector<Token> & tokens, std::size_t & i) {
    const size_t first_index = i;
    if (i >= tokens.size() || tokens[i].type != TokenType::LeftCurlyBracket) {
        THROW_UNEXPECTED_TOKEN_ERROR(tokens[i], "{");
    }
    i++;  // Skip '{'
    std::string lines;
    while (i < tokens.size() && tokens[i].type != TokenType::RightCurlyBracket) {
        if (tokens[i].type == TokenType::EndOfLine) {
            lines += "\n";
            i++;
            continue;
        }
        if (tokens[i].type == TokenType::EndOfFile) {
            throw std::runtime_error("Unexpected end of file");
            break;
        }
        lines += tokens[i].lexeme + " ";
        i++;
    }
    if (i >= tokens.size() || tokens[i].type != TokenType::RightCurlyBracket) {
        THROW_UNEXPECTED_TOKEN_ERROR(tokens[i], "}");
    }
    return lines;
};
};  // namespace ScriptInterpreterHelpers
#endif  // SCRIPTINTERPRETERHELPERS_HPP
src/Token.hpp
@@ -1,9 +1,6 @@
#ifndef TOKEN_HPP
#define TOKEN_HPP
#include <cstdint>
#include <iostream>
#include <ostream>
#include <stdexcept>
#include <string>
#include <unordered_map>
@@ -12,35 +9,59 @@
enum class TokenType : std::uint8_t {
    ParserOpenTag,
    ParserCloseTag,
    ParserIfStatement,  // if
    FileClose,
    Identifier,
    StringLiteral,
    IntLiteral,
    DoubleLiteral,
    LeftParenthesis,
    RightParenthesis,
    Comma,
    Semicolon,
    BooleanLiteral,
    LeftParenthesis,      // (
    RightParenthesis,     // )
    Comma,                // ,
    Semicolon,            // ;
    Variable,           // $variable
    VariableSign,       // $ variable start sign
    StringDeclaration,  // string $variable
    IntDeclaration,     // int $variable
    DoubleDeclaration,  // double $variable
    Equals,             // = jel
    EndOfFile,
    EndOfLine,
    Comment,
    Unknown
    BooleanDeclaration,   // bool $variable
    FunctionDeclaration,  // function $variable
    Equals,               // =
    Plus,                 // +
    Minus,                // -
    Multiply,             // *
    Divide,               // /
    Modulo,               // %
    GreaterThan,          // >
    LessThan,             // <
    GreaterThanOrEqual,   // >=
    LessThanOrEqual,      // <=
    NotEqual,             // !=
    Equal,                // ==
    Not,                  // !
    And,                  // &&
    Or,                   // ||
    LeftBracket,          // [
    RightBracket,         // ]
    LeftCurlyBracket,     // {
    RightCurlyBracket,    // }
    EndOfFile,            // \0
    EndOfLine,            // \n
    Comment,              // #
    Unknown               // Unknown
};
const static std::unordered_map<TokenType, std::string> tokenTypeNames = {
    { TokenType::ParserOpenTag,     "ParserOpenTag"     },
    { TokenType::ParserCloseTag,    "ParserCloseTag"    },
    { TokenType::ParserIfStatement,   "ParserIfStatement"   },
    { TokenType::FileClose,         "FileClose"         },
    { TokenType::Identifier,        "Identifier"        },
    { TokenType::StringLiteral,     "StringLiteral"     },
    { TokenType::IntLiteral,        "IntLiteral"        },
    { TokenType::DoubleLiteral,     "DoubleLiteral"     },
    { TokenType::BooleanLiteral,      "BooleanLiteral"      },
    { TokenType::LeftParenthesis,   "LeftParenthesis"   },
    { TokenType::RightParenthesis,  "RightParenthesis"  },
    { TokenType::Comma,             "Comma"             },
@@ -50,17 +71,54 @@
    { TokenType::StringDeclaration, "StringDeclaration" },
    { TokenType::IntDeclaration,    "IntDeclaration"    },
    { TokenType::DoubleDeclaration, "DoubleDeclaration" },
    { TokenType::BooleanDeclaration,  "BooleanDeclaration"  },
    { TokenType::FunctionDeclaration, "FunctionDeclaration" },
    { TokenType::Equals,            "Equals"            },
    { TokenType::Plus,                "Plus"                },
    { TokenType::Minus,               "Minus"               },
    { TokenType::Multiply,            "Multiply"            },
    { TokenType::Divide,              "Divide"              },
    { TokenType::Modulo,              "Modulo"              },
    { TokenType::GreaterThan,         "GreaterThan"         },
    { TokenType::LessThan,            "LessThan"            },
    { TokenType::GreaterThanOrEqual,  "GreaterThanOrEqual"  },
    { TokenType::LessThanOrEqual,     "LessThanOrEqual"     },
    { TokenType::NotEqual,            "NotEqual"            },
    { TokenType::Equal,               "Equal"               },
    { TokenType::Not,                 "Not"                 },
    { TokenType::And,                 "And"                 },
    { TokenType::Or,                  "Or"                  },
    { TokenType::LeftBracket,         "LeftBracket"         },
    { TokenType::RightBracket,        "RightBracket"        },
    { TokenType::LeftCurlyBracket,    "LeftCurlyBracket"    },
    { TokenType::RightCurlyBracket,   "RightCurlyBracket"   },
    { TokenType::EndOfFile,         "EndOfFile"         },
    { TokenType::EndOfLine,         "EndOfLine"         },
    { TokenType::Comment,           "Comment"           },
    { TokenType::Unknown,           "Unknown"           }
};
[[nodiscard]] static inline std::string getTokenTypeAsString(TokenType type) {
    auto it = tokenTypeNames.find(type);
    if (it != tokenTypeNames.end()) {
        return it->second;
    }
    return "Unknown";
    //throw std::runtime_error("Unknown token type");
};
static const std::unordered_map<TokenType, Variables::Type> tokenTypeToVariableType = {
    { TokenType::StringLiteral, Variables::Type::VT_STRING },
    { TokenType::IntLiteral,    Variables::Type::VT_INT    },
    { TokenType::DoubleLiteral, Variables::Type::VT_DOUBLE }
    { TokenType::DoubleLiteral,  Variables::Type::VT_DOUBLE  },
    { TokenType::BooleanLiteral, Variables::Type::VT_BOOLEAN }
};
static const std::unordered_map<Variables::Type, TokenType> variableTypeToTokenType = {
    { Variables::Type::VT_STRING,  TokenType::StringLiteral  },
    { Variables::Type::VT_INT,     TokenType::IntLiteral     },
    { Variables::Type::VT_DOUBLE,  TokenType::DoubleLiteral  },
    { Variables::Type::VT_BOOLEAN, TokenType::BooleanLiteral }
};
[[nodiscard]] static inline Variables::Type getVariableTypeFromTokenType(TokenType type) {
@@ -76,12 +134,64 @@
    return Variables::TypeToString(getVariableTypeFromTokenType(type));
}
[[nodiscard]] static inline TokenType getTokenTypeFromVariableType(Variables::Type type) {
    auto it = variableTypeToTokenType.find(type);
    if (it != variableTypeToTokenType.end()) {
        return it->second;
    }
    return TokenType::Unknown;
};
[[nodiscard]] static inline TokenType getTokenTypeFromValueDeclaration(const Variables::Type & declaration) {
    if (declaration == Variables::Type::VT_STRING) {
        return TokenType::StringDeclaration;
    }
    if (declaration == Variables::Type::VT_INT) {
        return TokenType::IntDeclaration;
    }
    if (declaration == Variables::Type::VT_DOUBLE) {
        return TokenType::DoubleDeclaration;
    }
    if (declaration == Variables::Type::VT_BOOLEAN) {
        return TokenType::BooleanDeclaration;
    }
    if (declaration == Variables::Type::VT_FUNCTION) {
        return TokenType::FunctionDeclaration;
    }
    return TokenType::Unknown;
}
[[nodiscard]] static inline Variables::Type getVariableTypeFromTokenTypeDeclaration(const TokenType & type) {
    if (type == TokenType::StringDeclaration) {
        return Variables::Type::VT_STRING;
    }
    if (type == TokenType::IntDeclaration) {
        return Variables::Type::VT_INT;
    }
    if (type == TokenType::DoubleDeclaration) {
        return Variables::Type::VT_DOUBLE;
    }
    if (type == TokenType::BooleanDeclaration) {
        return Variables::Type::VT_BOOLEAN;
    }
    if (type == TokenType::FunctionDeclaration) {
        return Variables::Type::VT_FUNCTION;
    }
    return Variables::Type::VT_NULL;
};
struct TokenPos {
    size_t start;
    size_t end;
};
struct Token {
    TokenType       type;
    std::string     lexeme;
    std::string     file;
    int             lineNumber;
    size_t          columnNumber;
    TokenPos        pos;
    Variables::Type variableType = Variables::Type::VT_NULL;
    [[nodiscard]] std::string getTypeName() const { return tokenTypeNames.at(type); }
src/Value.hpp
@@ -4,35 +4,77 @@
#include <string>
#include <variant>
#include "Token.hpp"
#include "VariableTypes.hpp"
class Value {
  public:
    Variables::Type          type = Variables::Type::VT_NULL;
    Variables::DataContainer data;
    Token                    token;
    Value() : type(Variables::Type::VT_NULL) {}
    Value(Variables::Type t, double val) : type(t), data(std::move(val)) {}
    //Value(Variables::Type t, double val) : type(t), data(std::move(val)) {}
    Value(Variables::Type t, int val) : type(t), data(std::move(val)) {}
    //Value(Variables::Type t, int val) : type(t), data(std::move(val)) {}
    Value(Variables::Type t, const std::string & val) : type(t), data(val) {}
    //Value(Variables::Type t, const std::string & val) : type(t), data(val) {}
    Value(Variables::Type t, bool val) : type(t), data(std::move(val)) {}
    //Value(Variables::Type t, bool val) : type(t), data(std::move(val)) {}
    static Value fromInt(int val) { return Value(Variables::Type::VT_INT, val); }
    Value(Variables::Type variable_type, const Token & token) : type(variable_type), token(token) {
        if (type == Variables::Type::VT_INT) {
            data = std::stoi(token.lexeme);
        } else if (type == Variables::Type::VT_DOUBLE) {
            data = std::stod(token.lexeme);
        } else if (type == Variables::Type::VT_BOOLEAN) {
            data = token.lexeme == "true";
        } else if (type == Variables::Type::VT_NULL) {
            data = Variables::TypeToString(type);
        } else if (type == Variables::Type::VT_NOT_DEFINED) {
            data = Variables::TypeToString(type);
        } else {
            data = token.lexeme;
        }
    }
    static Value fromDouble(double val) { return Value(Variables::Type::VT_DOUBLE, val); }
    //Value(const Token & token) : token(token), type(token.variableType), data(token.lexeme) {}
    static Value fromString(const std::string & val) { return { Variables::Type::VT_STRING, val }; }
    //static Value fromInt(int val) { return Value(Variables::Type::VT_INT, val); }
    static Value fromBoolean(bool val) { return { Variables::Type::VT_BOOLEAN, val }; }
    //static Value fromDouble(double val) { return Value(Variables::Type::VT_DOUBLE, val); }
    //static Value fromString(const std::string & val) { return { Variables::Type::VT_STRING, val }; }
    //static Value fromBoolean(bool val) { return { Variables::Type::VT_BOOLEAN, val }; }
    static Value fromInt(const Token & token) { return Value(Variables::Type::VT_INT, token); }
    static Value fromDouble(const Token & token) { return Value(Variables::Type::VT_DOUBLE, token); }
    static Value fromString(const Token & token) { return Value(Variables::Type::VT_STRING, token); }
    static Value fromBoolean(const Token & token, bool state) {
        auto result = Value(Variables::Type::VT_BOOLEAN, token);
        result.data = state;
        return result;
    }
    std::string ToString() const { return decodeEscapes(Variables::ToString(data, type)); }
    int ToInt() const { return std::get<int>(data); }
    double ToDouble() const { return std::get<double>(data); }
    bool ToBool() const { return std::get<bool>(data); }
    std::string TypeToString() const { return Variables::TypeToString(type); }
    void SetToken(const Token & token) { this->token = token; }
    const Token & GetToken() const { return token; }
  private:
    Value(Variables::Type t, std::variant<int, double, std::string, bool> && val) : type(t), data(std::move(val)) {}
src/VariableTypes.hpp
@@ -10,22 +10,25 @@
using DataContainer = std::variant<int, double, std::string, bool>;
enum class Type : std::uint8_t { VT_INT, VT_DOUBLE, VT_STRING, VT_BOOLEAN, VT_NULL, VT_NOT_DEFINED };
enum class Type : std::uint8_t { VT_INT, VT_DOUBLE, VT_STRING, VT_BOOLEAN, VT_NULL, VT_FUNCTION, VT_NOT_DEFINED };
const std::unordered_map<std::string, Type> StringToTypeMap = {
    { "int",         Type::VT_INT         },
    { "double",      Type::VT_DOUBLE      },
    { "string",      Type::VT_STRING      },
    { "bool",        Type::VT_BOOLEAN     },
    { "boolean",     Type::VT_BOOLEAN     },
    { "null",        Type::VT_NULL        },
    { "function",    Type::VT_FUNCTION    },
    { "not_defined", Type::VT_NOT_DEFINED },
};
const std::unordered_map<Type, std::string> StypeToStringMap = {
    { Type::VT_INT,         "int"         },
    { Type::VT_DOUBLE,      "double"      },
    { Type::VT_STRING,      "string"      },
    { Type::VT_BOOLEAN,     "boolean"     },
    { Type::VT_BOOLEAN,     "bool"        },
    { Type::VT_NULL,        "null"        },
    { Type::VT_FUNCTION,    "function"    },
    { Type::VT_NOT_DEFINED, "not_defined" },
};
test_scripts/test1.vs
test_scripts/test2.vs